@@ -73,12 +73,7 @@ const INVALIDATE_DOCUMENT_SCRIPT =
7373const ADOPT_GENERATION_SCRIPT =
7474 "local generation = redis.call('get', KEYS[2]); if generation then return generation end; if redis.call('xlen', KEYS[1]) == 0 then return false end; redis.call('set', KEYS[2], ARGV[1], 'EX', ARGV[2]); return ARGV[1]"
7575
76- /**
77- * Append an ordinary update only while the live-document generation is valid. A durable replacement
78- * that cannot be represented by the rich editor first sets the invalidation tombstone and then removes
79- * the stream; keeping the guard and XADD in one script prevents an old room from recreating that stream.
80- * Returns the new stream id, or `false` while invalidated.
81- */
76+ /** Atomically fence XADD so stale rooms cannot recreate a replaced or expired stream. Returns false when fenced. */
8277const APPEND_UPDATE_SCRIPT =
8378 "local generation = redis.call('get', KEYS[2]) or ''; if generation ~= ARGV[4] or redis.call('exists', KEYS[1]) == 0 then return false end; if ARGV[3] ~= '' then return redis.call('xadd', KEYS[1], '*', ARGV[1], ARGV[2], ARGV[3], '1') else return redis.call('xadd', KEYS[1], '*', ARGV[1], ARGV[2]) end"
8479
@@ -403,13 +398,8 @@ export class FileDocStore {
403398 }
404399
405400 /**
406- * PULL the shared state into a registered room: read the stream and apply every entry the doc has
407- * not integrated yet (origin {@link REDIS_ORIGIN}), advancing `lastId` so the tailer resumes exactly
408- * after it. This is the ONLY way a room loads shared state, so a caller that must not depend on the
409- * tailer's asynchronous push — the join, which may not serve a client a half-assembled document —
410- * can converge on demand. Idempotent and safe to call repeatedly; no-op when disabled or the room is
411- * not registered (a fast open→close detached it). Throws without applying partial state when replay
412- * cannot complete, so a join never serves a prefix of the shared document.
401+ * Completes shared replay before applying entries, so joins never receive a partial document.
402+ * Repeated calls skip integrated entries; detached rooms and disabled stores are ignored.
413403 */
414404 async catchUp ( name : string ) : Promise < void > {
415405 if ( ! this . enabled ) return
@@ -543,10 +533,8 @@ export class FileDocStore {
543533 }
544534
545535 /**
546- * Append a user update exactly once within the stream's bounded deduplication window and return only
547- * after Redis has accepted it. The client keeps the batch in IndexedDB until this promise succeeds
548- * and its socket acknowledgement arrives, so a relay restart or lost acknowledgement is safe to
549- * retry throughout that window.
536+ * Waits for Redis acceptance before the relay acknowledges the client. Retries are deduplicated
537+ * within the bounded window; clients retain their journal until the acknowledgement arrives.
550538 */
551539 async publishClientUpdateAndWait (
552540 name : string ,
@@ -656,10 +644,8 @@ export class FileDocStore {
656644 }
657645
658646 /**
659- * Invalidate the current live-document generation after a durable replacement that the rich editor
660- * cannot represent. The invalid generation marker is written before deleting the stream, so stale
661- * room publishers cannot recreate it; the next authoritative seed atomically replaces the marker
662- * with its generation. The same TTL as the stream bounds abandoned markers.
647+ * Fences an unsupported durable replacement before deleting its stream. The next authoritative
648+ * seed replaces the tombstone; abandoned tombstones expire with the stream TTL.
663649 */
664650 async invalidateDocument ( name : string , version : number ) : Promise < boolean > {
665651 if ( ! this . enabled ) {
@@ -830,12 +816,11 @@ export class FileDocStore {
830816 }
831817 }
832818
833- /** A moving head means compaction may have removed unread dependencies. Replay its snapshot . */
819+ /** Compaction appends its snapshot before trimming; extend the barrier without rereading it . */
834820 const currentFirstId = ( await this . write . xRange ( key , '-' , '+' , { COUNT : 1 } ) ) [ 0 ] ?. id
835821 if ( ! currentFirstId ) throw new FileDocInvalidatedError ( )
836822 if ( currentFirstId === firstId ) return entriesRead
837823 firstId = currentFirstId
838- cursor = '0-0'
839824 const currentTail = await this . write . xRevRange ( key , '+' , '-' , { COUNT : 1 } )
840825 if ( currentTail . length === 0 ) throw new FileDocInvalidatedError ( )
841826 endId = currentTail [ 0 ] . id
0 commit comments