(noteId: string)
| 174 | // settings (e.g. agent prefs, last-seen versions) doesn't require |
| 175 | // a schema migration each time. |
| 176 | this.sql.exec(` |
| 177 | CREATE TABLE IF NOT EXISTS smfs_config ( |
| 178 | key TEXT PRIMARY KEY, |
| 179 | value TEXT NOT NULL |
| 180 | ) |
| 181 | `); |
| 182 | }); |
| 183 | } |
| 184 | |
| 185 | private getYDoc(noteId: string): Y.Doc { |
| 186 | let doc = this.docs.get(noteId); |
| 187 | if (doc) return doc; |
| 188 | |
| 189 | doc = new Y.Doc(); |
| 190 | const rows = this.sql.exec("SELECT yjs_state FROM notes WHERE id = ?", noteId).toArray(); |
| 191 | if (rows[0]?.yjs_state) { |
| 192 | Y.applyUpdate(doc, new Uint8Array(rows[0].yjs_state as ArrayBuffer)); |
| 193 | } |
| 194 | |
| 195 | doc.on("update", (update: Uint8Array, origin: any) => { |
| 196 | // Broadcast to all connected clients except the sender. |
| 197 | // origin is the WebSocket that caused this update (null for DB loads). |
| 198 | if (origin) { |
| 199 | const encoder = encoding.createEncoder(); |
| 200 | encoding.writeVarUint(encoder, MSG_SYNC); |
| 201 | syncProtocol.writeUpdate(encoder, update); |
| 202 | const msg = encoding.toUint8Array(encoder); |
| 203 | for (const other of this.ctx.getWebSockets(noteId)) { |
| 204 | if (other !== origin) { |
| 205 | try { other.send(msg); } catch { other.close(); } |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | this.scheduleDocPersist(noteId, doc!, 2000); |
| 211 | }); |
| 212 | |
| 213 | this.docs.set(noteId, doc); |
| 214 | return doc; |
| 215 | } |
| 216 | |
| 217 | // Persist a live Yjs doc to the notes row: yjs_state always; content + |
| 218 | // title when serialization yields a non-empty doc. Never replaces |
| 219 | // non-empty column content with an empty doc — if serialization ever |
| 220 | // regresses (or the doc is transiently blank), losing the row content is |
| 221 | // the worst outcome. Shared by the debounced save and the last-close flush |
| 222 | // so the content column can never lag behind yjs_state. |
| 223 | private persistDocNow(noteId: string, doc: Y.Doc) { |
| 224 | const state = Y.encodeStateAsUpdate(doc); |
| 225 | let content = this.extractContentJson(noteId); |
| 226 | if (content && isEmptyDoc(content)) { |
| 227 | const existing = this.sql.exec("SELECT content FROM notes WHERE id = ?", noteId).toArray()[0] as any; |
| 228 | if (existing?.content && !isEmptyDoc(existing.content)) content = null; |
| 229 | } |
| 230 | if (content && !isEmptyDoc(content)) { |
| 231 | // Real content: UPSERT so a brand-new note persists even when the HTTP |
| 232 | // autosave never created the row (or failed, e.g. a transient 503). |
no test coverage detected