@@ -62,6 +62,9 @@ export class ReadStateManager {
6262 private maxFetchedCreatedAt = 0 ;
6363 private forcedContexts = new Set < string > ( ) ;
6464 private contextSourceCreatedAt = new Map < string , number > ( ) ;
65+ private pendingSyncedRollbacks = new Set < string > ( ) ;
66+ private pendingSyncedAdvances = new Set < string > ( ) ;
67+ private destroyed = false ;
6568
6669 constructor ( pubkey : string , relayClient : RelayClient ) {
6770 this . pubkey = pubkey ;
@@ -75,17 +78,25 @@ export class ReadStateManager {
7578 }
7679
7780 async initialize ( ) : Promise < void > {
78- if ( this . initialized ) return ;
81+ if ( this . initialized || this . destroyed ) return ;
82+ console . debug (
83+ `[ReadStateManager] initialize pubkey=${ this . pubkey . substring ( 0 , 8 ) } … clientId=${ this . clientId . substring ( 0 , 8 ) } … slotId=${ this . slotId } ` ,
84+ ) ;
7985
8086 this . hydrateFromLocalStorage ( ) ;
8187
8288 await this . fetchAndMerge ( ) ;
89+ if ( this . destroyed ) return ;
8390 await this . startLiveSubscription ( ) ;
91+ if ( this . destroyed ) return ;
8492 if ( ! this . isIdenticalToLastPublished ( this . currentContexts ( ) ) ) {
8593 this . schedulePublish ( ) ;
8694 }
8795
8896 this . initialized = true ;
97+ console . debug (
98+ `[ReadStateManager] initialize complete maxFetchedCreatedAt=${ this . maxFetchedCreatedAt } contexts=${ this . effectiveState . size } ` ,
99+ ) ;
89100 this . notifyListeners ( ) ;
90101 }
91102
@@ -152,6 +163,7 @@ export class ReadStateManager {
152163 }
153164
154165 destroy ( ) : void {
166+ this . destroyed = true ;
155167 // Flush any pending writes immediately
156168 if ( this . debounceTimer !== null ) {
157169 window . clearTimeout ( this . debounceTimer ) ;
@@ -177,7 +189,8 @@ export class ReadStateManager {
177189 since : Math . floor ( Date . now ( ) / 1_000 ) - READ_STATE_HORIZON_SECONDS ,
178190 limit : READ_STATE_FETCH_LIMIT ,
179191 } ) ;
180- } catch {
192+ } catch ( error ) {
193+ console . debug ( "[ReadStateManager] fetchAndMerge failed:" , error ) ;
181194 // If fetch fails, proceed with local state only
182195 return ;
183196 }
@@ -219,7 +232,11 @@ export class ReadStateManager {
219232 client_id : parsed . client_id ,
220233 contexts : sanitizeContexts ( parsed . contexts ) ,
221234 } ;
222- } catch {
235+ } catch ( error ) {
236+ console . debug (
237+ `[ReadStateManager] mergeEvents decrypt failed event=${ event . id . substring ( 0 , 8 ) } …:` ,
238+ error ,
239+ ) ;
223240 continue ;
224241 }
225242
@@ -260,7 +277,11 @@ export class ReadStateManager {
260277 localStorage . setItem ( slotIdKey ( this . pubkey ) , this . slotId ) ;
261278 break ;
262279 }
263- } catch {
280+ } catch ( error ) {
281+ console . debug (
282+ `[ReadStateManager] conflict check decrypt failed event=${ event . id . substring ( 0 , 8 ) } …:` ,
283+ error ,
284+ ) ;
264285 // Decrypt failure — skip this event
265286 }
266287 }
@@ -286,14 +307,24 @@ export class ReadStateManager {
286307 void this . handleIncomingEvent ( event ) ;
287308 } ,
288309 ) ;
310+ if ( this . destroyed ) {
311+ unsub ( ) ;
312+ return ;
313+ }
289314 this . unsubscribeLive = unsub ;
290- } catch {
315+ console . debug ( "[ReadStateManager] live subscription established" ) ;
316+ } catch ( error ) {
317+ console . debug ( "[ReadStateManager] live subscription FAILED:" , error ) ;
291318 // Non-fatal: we can still work with local state
292319 }
293320 }
294321
295322 private async handleIncomingEvent ( event : RelayEvent ) : Promise < void > {
296323 if ( event . pubkey !== this . pubkey ) return ;
324+ if ( this . destroyed ) return ;
325+ console . debug (
326+ `[ReadStateManager] incoming event=${ event . id . substring ( 0 , 8 ) } … created_at=${ event . created_at } ` ,
327+ ) ;
297328
298329 const dTags = event . tags . filter ( ( t ) => t [ 0 ] === "d" ) ;
299330 if ( dTags . length !== 1 ) return ;
@@ -320,7 +351,11 @@ export class ReadStateManager {
320351 client_id : parsed . client_id ,
321352 contexts : sanitizeContexts ( parsed . contexts ) ,
322353 } ;
323- } catch {
354+ } catch ( error ) {
355+ console . debug (
356+ `[ReadStateManager] incoming event decrypt/parse failed event=${ event . id . substring ( 0 , 8 ) } …:` ,
357+ error ,
358+ ) ;
324359 return ;
325360 }
326361
@@ -331,6 +366,16 @@ export class ReadStateManager {
331366 const current = this . effectiveState . get ( ctx ) ?? 0 ;
332367 if ( event . created_at > sourceCreatedAt ) {
333368 if ( this . effectiveState . get ( ctx ) !== ts ) {
369+ if ( ts < current && current > 0 ) {
370+ this . pendingSyncedRollbacks . add ( ctx ) ;
371+ this . pendingSyncedAdvances . delete ( ctx ) ;
372+ console . debug (
373+ `[ReadStateManager] synced rollback ctx=${ ctx . substring ( 0 , 12 ) } … from=${ current } to=${ ts } ` ,
374+ ) ;
375+ } else if ( ts > current ) {
376+ this . pendingSyncedAdvances . add ( ctx ) ;
377+ this . pendingSyncedRollbacks . delete ( ctx ) ;
378+ }
334379 this . effectiveState . set ( ctx , ts ) ;
335380 anyAdvanced = true ;
336381 }
@@ -344,6 +389,9 @@ export class ReadStateManager {
344389 anyAdvanced = true ;
345390 }
346391 }
392+ console . debug (
393+ `[ReadStateManager] incoming result anyAdvanced=${ anyAdvanced } clientId=${ blob . client_id . substring ( 0 , 8 ) } …` ,
394+ ) ;
347395
348396 if ( anyAdvanced ) {
349397 this . persistLocalState ( ) ;
@@ -368,6 +416,7 @@ export class ReadStateManager {
368416 }
369417
370418 private async publish ( ) : Promise < void > {
419+ console . debug ( `[ReadStateManager] publish starting slotId=${ this . slotId } ` ) ;
371420 await this . fetchOwnBlobBeforePublish ( ) ;
372421
373422 // Build blob from contexts this client is allowed to publish.
@@ -408,12 +457,17 @@ export class ReadStateManager {
408457 "Timed out publishing read state." ,
409458 "Failed to publish read state." ,
410459 ) ;
460+ console . debug (
461+ `[ReadStateManager] publish accepted createdAt=${ createdAt } ` ,
462+ ) ;
411463
412- this . lastPublishedContexts = contexts ;
413- this . forcedContexts . clear ( ) ;
414464 for ( const key of Object . keys ( contexts ) ) {
415- this . contextSourceCreatedAt . set ( key , createdAt ) ;
465+ if ( this . lastPublishedContexts [ key ] !== contexts [ key ] ) {
466+ this . contextSourceCreatedAt . set ( key , createdAt ) ;
467+ }
416468 }
469+ this . lastPublishedContexts = contexts ;
470+ this . forcedContexts . clear ( ) ;
417471 this . maxFetchedCreatedAt = Math . max (
418472 this . maxFetchedCreatedAt ,
419473 event . created_at ,
@@ -435,7 +489,11 @@ export class ReadStateManager {
435489
436490 await this . mergeEvents ( events ) ;
437491 this . persistLocalState ( ) ;
438- } catch {
492+ } catch ( error ) {
493+ console . debug (
494+ "[ReadStateManager] fetchOwnBlobBeforePublish failed:" ,
495+ error ,
496+ ) ;
439497 // Per NIP-RS, proceed with reachable data and merge on a later fetch.
440498 }
441499 }
@@ -486,11 +544,24 @@ export class ReadStateManager {
486544 ) ;
487545 }
488546
547+ drainSyncedRollbacks ( ) : ReadonlySet < string > {
548+ const drained = this . pendingSyncedRollbacks ;
549+ this . pendingSyncedRollbacks = new Set < string > ( ) ;
550+ return drained ;
551+ }
552+
553+ drainSyncedAdvances ( ) : ReadonlySet < string > {
554+ const drained = this . pendingSyncedAdvances ;
555+ this . pendingSyncedAdvances = new Set < string > ( ) ;
556+ return drained ;
557+ }
558+
489559 private notifyListeners ( ) : void {
490560 for ( const listener of this . listeners ) {
491561 try {
492562 listener ( ) ;
493- } catch {
563+ } catch ( error ) {
564+ console . debug ( "[ReadStateManager] listener threw:" , error ) ;
494565 // Don't let a broken listener break the manager
495566 }
496567 }
0 commit comments