Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

fix(frontend): rich mode uses sendable.version as clientVersion and skips this.version on 409

+24 -19
+24 -19
static/collab-client.js
··· 6 6 export class CollabClient { 7 7 constructor(rkey, initialVersion, applyRemoteSteps) { 8 8 this.rkey = rkey; 9 - this.version = initialVersion; 9 + this.version = initialVersion; // authoritative for source mode; advisory for rich mode 10 10 this.applyRemoteSteps = applyRemoteSteps; 11 11 this._inflight = false; 12 - this._queue = []; // used by source mode (text-patch steps) 13 - this._pmGetSendable = null; // set by rich mode; returns {steps, clientID} or null 12 + this._queue = []; // source mode only 13 + this._pmGetSendable = null; // rich mode: () => sendableSteps(pmView.state) | null 14 14 } 15 15 16 16 // Source mode: queue explicit step objects and flush. ··· 19 19 this._flush(); 20 20 } 21 21 22 - // Rich mode: register a callback that returns sendable PM steps on demand. 23 - // The callback should call sendableSteps(pmView.state) and return the result, 24 - // or null if nothing to send. 22 + // Rich mode: register the sendable-steps provider. 25 23 setPMSendable(fn) { 26 24 this._pmGetSendable = fn; 25 + this._queue = []; // clear any stale source-mode queue 27 26 } 28 27 29 - // Signal that a PM transaction occurred; try to flush if not already in-flight. 28 + // Rich mode: call after every PM transaction to trigger a flush attempt. 30 29 notifyPMChange() { 31 30 this._flush(); 32 31 } ··· 35 34 if (this._inflight) return; 36 35 37 36 let toSend; 38 - let isRichMode = false; 37 + let clientVersion; 38 + const isRichMode = !!this._pmGetSendable; 39 39 40 - if (this._pmGetSendable) { 41 - // Rich mode: ask prosemirror-collab what needs sending. 40 + if (isRichMode) { 42 41 const sendable = this._pmGetSendable(); 43 42 if (!sendable || sendable.steps.length === 0) return; 43 + // Use version from the collab plugin state — always authoritative. 44 + clientVersion = sendable.version; 44 45 toSend = sendable.steps.map(s => ({type: 'pm-step', json: JSON.stringify(s.toJSON())})); 45 - isRichMode = true; 46 46 } else { 47 - // Source mode: drain the queue. 48 47 if (this._queue.length === 0) return; 48 + clientVersion = this.version; 49 49 toSend = this._queue.slice(); 50 50 } 51 51 ··· 55 55 method: 'POST', 56 56 headers: {'Content-Type': 'application/json'}, 57 57 body: JSON.stringify({ 58 - clientVersion: this.version, 58 + clientVersion, 59 59 steps: toSend.map(s => JSON.stringify(s)), 60 60 clientID: this._clientID || '', 61 61 }), ··· 67 67 if (!isRichMode) { 68 68 this._queue = this._queue.slice(toSend.length); 69 69 } 70 - // Rich mode: prosemirror-collab tracks confirmation via receiveTransaction; 71 - // no queue to drain here. 70 + // Rich mode: the collab plugin tracks confirmation internally; 71 + // this.version is updated here as a fallback for WebSocket-less paths. 72 72 } else if (resp.status === 409) { 73 - const {version, steps: missedJSON} = await resp.json(); 73 + const {steps: missedJSON} = await resp.json(); 74 74 const missed = missedJSON.map(s => JSON.parse(s)); 75 + // Apply missed steps — for rich mode this calls receiveTransaction 76 + // which rebases our unconfirmed steps and updates the collab plugin state. 77 + // Do NOT update this.version here; the collab plugin owns that for rich mode. 75 78 this.applyRemoteSteps(missed); 76 - this.version = version; 79 + // Retry after a short delay so the PM state settles. 80 + setTimeout(() => this._flush(), 30); 81 + return; // skip the finally retry below 77 82 } else { 78 83 console.error('CollabClient: unexpected status', resp.status); 79 84 } ··· 81 86 console.error('CollabClient: fetch error', e); 82 87 } finally { 83 88 this._inflight = false; 84 - // Retry if there's more to send. 85 - const hasMore = this._pmGetSendable 89 + const hasMore = isRichMode 86 90 ? (this._pmGetSendable()?.steps.length > 0) 87 91 : this._queue.length > 0; 88 92 if (hasMore) { ··· 94 98 handleWSMessage(msg, myClientID) { 95 99 if (msg.type !== 'steps') return; 96 100 if (msg.clientID === myClientID) { 101 + // Our steps confirmed via WebSocket broadcast — update advisory version. 97 102 this.version = msg.version; 98 103 return; 99 104 }