@soulcraft/sdk 1.5.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/modules/brainy/ydoc-manager.d.ts +175 -0
- package/dist/modules/brainy/ydoc-manager.d.ts.map +1 -0
- package/dist/modules/brainy/ydoc-manager.js +424 -0
- package/dist/modules/brainy/ydoc-manager.js.map +1 -0
- package/dist/server/handlers.d.ts +108 -12
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/handlers.js +167 -5
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/transports/ws.d.ts +10 -0
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +15 -0
- package/dist/transports/ws.js.map +1 -1
- package/dist/transports/yjs-provider.d.ts +101 -0
- package/dist/transports/yjs-provider.d.ts.map +1 -0
- package/dist/transports/yjs-provider.js +241 -0
- package/dist/transports/yjs-provider.js.map +1 -0
- package/package.json +5 -2
package/dist/client/index.d.ts
CHANGED
|
@@ -59,6 +59,8 @@ export { PostMessageTransport } from '../transports/postmessage.js';
|
|
|
59
59
|
export type { PostMessageRequest, PostMessageResponse } from '../transports/postmessage.js';
|
|
60
60
|
export type { SDKTransport } from '../transports/transport.js';
|
|
61
61
|
export { createBrainyProxy } from '../modules/brainy/proxy.js';
|
|
62
|
+
export { createYjsProvider } from '../transports/yjs-provider.js';
|
|
63
|
+
export type { YjsProvider } from '../transports/yjs-provider.js';
|
|
62
64
|
export { joinHallRoom } from '../modules/hall/browser.js';
|
|
63
65
|
export type { JoinHallRoomOptions } from '../modules/hall/browser.js';
|
|
64
66
|
export type { HallRoomHandle, HallRoomHandleEvents, TranscriptEvent, ConceptMentionEvent, RelationProposedEvent, SpeakerChangedEvent, PeerJoinedEvent, PeerLeftEvent, RoomOptions, RecordingManifest, ConceptInput, } from '../modules/hall/types.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAGxD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AACnE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAC3F,YAAY,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,0BAA0B,CAAA;AAGjC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,sBAAsB,GACvB,MAAM,6BAA6B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAGxD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AACnE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAC3F,YAAY,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAGhE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,0BAA0B,CAAA;AAGjC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,sBAAsB,GACvB,MAAM,6BAA6B,CAAA"}
|
package/dist/client/index.js
CHANGED
|
@@ -60,6 +60,8 @@ export { SseTransport } from '../transports/sse.js';
|
|
|
60
60
|
export { PostMessageTransport } from '../transports/postmessage.js';
|
|
61
61
|
// ── Proxy factory ─────────────────────────────────────────────────────────────
|
|
62
62
|
export { createBrainyProxy } from '../modules/brainy/proxy.js';
|
|
63
|
+
// ── Y.js provider (collaborative editing) ─────────────────────────────────────
|
|
64
|
+
export { createYjsProvider } from '../transports/yjs-provider.js';
|
|
63
65
|
// ── Hall room client (browser WebRTC) ─────────────────────────────────────────
|
|
64
66
|
export { joinHallRoom } from '../modules/hall/browser.js';
|
|
65
67
|
export { SDKError, SDKDisconnectedError, SDKTimeoutError, SDKAuthError, SDKForbiddenError, SDKRpcError, SDKMethodNotFoundError, } from '../modules/brainy/errors.js';
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,iFAAiF;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,iFAAiF;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AAInE,iFAAiF;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAE9D,iFAAiF;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAoBzD,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,sBAAsB,GACvB,MAAM,6BAA6B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,iFAAiF;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,iFAAiF;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AAInE,iFAAiF;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAE9D,iFAAiF;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAGjE,iFAAiF;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAoBzD,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,sBAAsB,GACvB,MAAM,6BAA6B,CAAA"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module modules/brainy/ydoc-manager
|
|
3
|
+
* @description Server-side Y.js document pool with binary append-log persistence.
|
|
4
|
+
*
|
|
5
|
+
* `YDocManager` maintains a pool of live `Y.Doc` instances, one per open file across
|
|
6
|
+
* all connected peers. Every Y.js update (each keypress or structural change) is
|
|
7
|
+
* immediately appended to a `.ydoc` binary file in Brainy's VFS alongside the
|
|
8
|
+
* human-readable `.wdoc`/`.wslide`/`.wviz` file. Brainy's mmap/SSD storage makes
|
|
9
|
+
* per-keypress appends essentially free.
|
|
10
|
+
*
|
|
11
|
+
* ## Persistence strategy
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* /project/chapter-1.wdoc ← WDOC JSON, flushed from Y.Doc every 30s
|
|
15
|
+
* /project/chapter-1.ydoc ← binary Y.js update log, every keypress
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* - **On every Y.js update:** append binary update to `.ydoc` via `vfs.appendFile`.
|
|
19
|
+
* - **Every 30s (debounced):** flush the live Y.Doc to `.wdoc` JSON so the canonical
|
|
20
|
+
* format stays fresh for API consumers that read `.wdoc` directly.
|
|
21
|
+
* - **On last peer disconnect:** flush `.wdoc` immediately, then discard the Y.Doc
|
|
22
|
+
* from memory. No warm cache — `.ydoc` binary replay is instant on mmap/SSD.
|
|
23
|
+
* - **On cold start (next peer):** reconstruct Y.Doc by replaying the `.ydoc` log
|
|
24
|
+
* via `Y.applyUpdate`. Falls back to converting `.wdoc` JSON if no `.ydoc` exists.
|
|
25
|
+
*
|
|
26
|
+
* ## Invalidation
|
|
27
|
+
*
|
|
28
|
+
* When a VFS write bypasses Y.js (e.g. import, API overwrite), call `invalidate()`
|
|
29
|
+
* to rebuild the `.ydoc` log from the new `.wdoc` content so future peers get
|
|
30
|
+
* the correct document state.
|
|
31
|
+
*
|
|
32
|
+
* ## Scope key
|
|
33
|
+
*
|
|
34
|
+
* Documents are keyed by `${scope}:${filePath}` where scope is `${emailHash}:${workspaceId}`.
|
|
35
|
+
* This matches the WebSocket session scope so the manager can be addressed directly
|
|
36
|
+
* from message handlers without additional routing.
|
|
37
|
+
*/
|
|
38
|
+
import * as Y from 'yjs';
|
|
39
|
+
import type { Brainy } from '@soulcraft/brainy';
|
|
40
|
+
/**
|
|
41
|
+
* Server-side pool of Y.js documents with binary append-log persistence.
|
|
42
|
+
*
|
|
43
|
+
* One shared instance per product server process. Pass it to `createBrainyWsHandler`
|
|
44
|
+
* so the WebSocket message handler can route Y.js messages to the correct doc.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* import { YDocManager } from '@soulcraft/sdk/server'
|
|
49
|
+
* import { pool } from './instance-pool'
|
|
50
|
+
*
|
|
51
|
+
* export const ydocManager = new YDocManager()
|
|
52
|
+
*
|
|
53
|
+
* const wsHandler = createBrainyWsHandler({
|
|
54
|
+
* resolveBrain: (scope) => pool.forTenant(scope),
|
|
55
|
+
* authenticate: (token, scope) => verifyCapabilityToken(token, process.env.WS_TOKEN_SECRET!),
|
|
56
|
+
* ydocManager,
|
|
57
|
+
* })
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare class YDocManager {
|
|
61
|
+
/** Pool of live Y.Doc instances keyed by `${scope}:${filePath}`. */
|
|
62
|
+
private readonly docs;
|
|
63
|
+
/**
|
|
64
|
+
* Returns the live Y.Doc for the given scope + file, loading from the `.ydoc`
|
|
65
|
+
* binary log (or converting from the canonical format) on first access.
|
|
66
|
+
*
|
|
67
|
+
* Registers `peerId` as an active peer so the manager can track when the last
|
|
68
|
+
* peer disconnects.
|
|
69
|
+
*
|
|
70
|
+
* @param scope - The session scope (`${emailHash}:${workspaceId}`).
|
|
71
|
+
* @param filePath - Absolute VFS path to the source file (e.g. `/proj/doc.wdoc`).
|
|
72
|
+
* @param brain - The Brainy instance owning the VFS for this scope.
|
|
73
|
+
* @param peerId - Unique ID for the connecting peer.
|
|
74
|
+
* @returns The live `Y.Doc` for this file.
|
|
75
|
+
*/
|
|
76
|
+
getOrCreate(scope: string, filePath: string, brain: Brainy, peerId: string): Promise<Y.Doc>;
|
|
77
|
+
/**
|
|
78
|
+
* Applies a Y.js update received from a peer.
|
|
79
|
+
*
|
|
80
|
+
* - Applies the update to the in-memory Y.Doc.
|
|
81
|
+
* - Appends the raw binary update to the `.ydoc` log immediately.
|
|
82
|
+
*
|
|
83
|
+
* The debounced 30s flush to the canonical format runs independently.
|
|
84
|
+
*
|
|
85
|
+
* @param scope - Session scope.
|
|
86
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
87
|
+
* @param update - Raw binary Y.js update (from `Y.encodeStateAsUpdate` or an observer).
|
|
88
|
+
*/
|
|
89
|
+
applyUpdate(scope: string, filePath: string, update: Uint8Array): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Returns the current Y.js state vector for the given document.
|
|
92
|
+
*
|
|
93
|
+
* Used during the Y.js sync protocol: the client sends its state vector,
|
|
94
|
+
* the server replies with the diff (all updates the client is missing).
|
|
95
|
+
*
|
|
96
|
+
* @param scope - Session scope.
|
|
97
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
98
|
+
* @returns Encoded state vector as `Uint8Array`.
|
|
99
|
+
*/
|
|
100
|
+
getStateVector(scope: string, filePath: string, brain: Brainy): Promise<Uint8Array>;
|
|
101
|
+
/**
|
|
102
|
+
* Returns the Y.js diff between the server's state and the client's state vector.
|
|
103
|
+
*
|
|
104
|
+
* Called after the client sends a `y-sync` message. The diff contains all updates
|
|
105
|
+
* the client is missing so it can catch up to the server's current state.
|
|
106
|
+
*
|
|
107
|
+
* @param scope - Session scope.
|
|
108
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
109
|
+
* @param clientStateVector - The state vector encoded by the client.
|
|
110
|
+
* @returns Encoded update diff as `Uint8Array`.
|
|
111
|
+
*/
|
|
112
|
+
getDiff(scope: string, filePath: string, brain: Brainy, clientStateVector: Uint8Array): Promise<Uint8Array>;
|
|
113
|
+
/**
|
|
114
|
+
* Notifies the manager that a peer has disconnected from the given document.
|
|
115
|
+
*
|
|
116
|
+
* If this was the last peer:
|
|
117
|
+
* 1. Flushes the Y.Doc to the canonical format immediately.
|
|
118
|
+
* 2. Cancels the periodic flush timer.
|
|
119
|
+
* 3. Destroys and discards the Y.Doc from memory.
|
|
120
|
+
*
|
|
121
|
+
* No warm cache — `.ydoc` binary replay is instant on mmap/SSD.
|
|
122
|
+
*
|
|
123
|
+
* @param scope - Session scope.
|
|
124
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
125
|
+
* @param peerId - The peer that disconnected.
|
|
126
|
+
*/
|
|
127
|
+
onPeerDisconnect(scope: string, filePath: string, peerId: string): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Invalidates a cached Y.Doc when the canonical file is overwritten externally
|
|
130
|
+
* (e.g. via import or direct API write that bypasses Y.js).
|
|
131
|
+
*
|
|
132
|
+
* If the document is currently in memory (has active peers), it is rebuilt from
|
|
133
|
+
* the new canonical file content. If no peers are connected, any stale `.ydoc`
|
|
134
|
+
* log is deleted so the next cold start reads the new canonical file.
|
|
135
|
+
*
|
|
136
|
+
* @param scope - Session scope.
|
|
137
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
138
|
+
* @param brain - The Brainy instance owning the VFS.
|
|
139
|
+
*/
|
|
140
|
+
invalidate(scope: string, filePath: string, brain: Brainy): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Notifies the manager that a peer has disconnected from ALL documents it had open.
|
|
143
|
+
*
|
|
144
|
+
* Called from `BrainyWsHandler.handleClose()` when a WebSocket connection closes.
|
|
145
|
+
* Iterates all documents in this scope and removes the peer — if it was the last
|
|
146
|
+
* peer on any document, that document is flushed and discarded immediately.
|
|
147
|
+
*
|
|
148
|
+
* @param scope - Session scope (`${emailHash}:${workspaceId}`).
|
|
149
|
+
* @param peerId - The peer that disconnected.
|
|
150
|
+
*/
|
|
151
|
+
onPeerDisconnectAll(scope: string, peerId: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Flushes and discards all Y.Docs for a given scope.
|
|
154
|
+
*
|
|
155
|
+
* Called during unpublish to clean up all open documents for a project before
|
|
156
|
+
* the project is removed from the publish registry.
|
|
157
|
+
*
|
|
158
|
+
* @param scope - Session scope (`${emailHash}:${workspaceId}`).
|
|
159
|
+
*/
|
|
160
|
+
flushAndDiscardScope(scope: string): Promise<void>;
|
|
161
|
+
/**
|
|
162
|
+
* Loads a Y.Doc from the `.ydoc` binary log, falling back to converting the
|
|
163
|
+
* canonical file if no log exists yet.
|
|
164
|
+
*/
|
|
165
|
+
private _load;
|
|
166
|
+
/**
|
|
167
|
+
* Schedules the periodic 30s flush for the given document entry.
|
|
168
|
+
*/
|
|
169
|
+
private _scheduleFlush;
|
|
170
|
+
/**
|
|
171
|
+
* Flushes the live Y.Doc to the canonical format in the VFS.
|
|
172
|
+
*/
|
|
173
|
+
private _flush;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=ydoc-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ydoc-manager.d.ts","sourceRoot":"","sources":["../../../src/modules/brainy/ydoc-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AA0C/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,WAAW;IACtB,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IAInD;;;;;;;;;;;;OAYG;IACG,WAAW,CACf,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAwBjB;;;;;;;;;;;OAWG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrF;;;;;;;;;OASG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAYzF;;;;;;;;;;OAUG;IACG,OAAO,CACX,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,iBAAiB,EAAE,UAAU,GAC5B,OAAO,CAAC,UAAU,CAAC;IAYtB;;;;;;;;;;;;;OAaG;IACG,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtF;;;;;;;;;;;OAWG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2B/E;;;;;;;;;OASG;IACG,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvE;;;;;;;OAOG;IACG,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxD;;;OAGG;YACW,KAAK;IA2BnB;;OAEG;IACH,OAAO,CAAC,cAAc;IAQtB;;OAEG;YACW,MAAM;CAQrB"}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module modules/brainy/ydoc-manager
|
|
3
|
+
* @description Server-side Y.js document pool with binary append-log persistence.
|
|
4
|
+
*
|
|
5
|
+
* `YDocManager` maintains a pool of live `Y.Doc` instances, one per open file across
|
|
6
|
+
* all connected peers. Every Y.js update (each keypress or structural change) is
|
|
7
|
+
* immediately appended to a `.ydoc` binary file in Brainy's VFS alongside the
|
|
8
|
+
* human-readable `.wdoc`/`.wslide`/`.wviz` file. Brainy's mmap/SSD storage makes
|
|
9
|
+
* per-keypress appends essentially free.
|
|
10
|
+
*
|
|
11
|
+
* ## Persistence strategy
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* /project/chapter-1.wdoc ← WDOC JSON, flushed from Y.Doc every 30s
|
|
15
|
+
* /project/chapter-1.ydoc ← binary Y.js update log, every keypress
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* - **On every Y.js update:** append binary update to `.ydoc` via `vfs.appendFile`.
|
|
19
|
+
* - **Every 30s (debounced):** flush the live Y.Doc to `.wdoc` JSON so the canonical
|
|
20
|
+
* format stays fresh for API consumers that read `.wdoc` directly.
|
|
21
|
+
* - **On last peer disconnect:** flush `.wdoc` immediately, then discard the Y.Doc
|
|
22
|
+
* from memory. No warm cache — `.ydoc` binary replay is instant on mmap/SSD.
|
|
23
|
+
* - **On cold start (next peer):** reconstruct Y.Doc by replaying the `.ydoc` log
|
|
24
|
+
* via `Y.applyUpdate`. Falls back to converting `.wdoc` JSON if no `.ydoc` exists.
|
|
25
|
+
*
|
|
26
|
+
* ## Invalidation
|
|
27
|
+
*
|
|
28
|
+
* When a VFS write bypasses Y.js (e.g. import, API overwrite), call `invalidate()`
|
|
29
|
+
* to rebuild the `.ydoc` log from the new `.wdoc` content so future peers get
|
|
30
|
+
* the correct document state.
|
|
31
|
+
*
|
|
32
|
+
* ## Scope key
|
|
33
|
+
*
|
|
34
|
+
* Documents are keyed by `${scope}:${filePath}` where scope is `${emailHash}:${workspaceId}`.
|
|
35
|
+
* This matches the WebSocket session scope so the manager can be addressed directly
|
|
36
|
+
* from message handlers without additional routing.
|
|
37
|
+
*/
|
|
38
|
+
import * as Y from 'yjs';
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Types
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
/** Extension appended to the source file path to form the Y.js update log path. */
|
|
43
|
+
const YDOC_EXT = '.ydoc';
|
|
44
|
+
/** Interval between debounced flushes of the live Y.Doc to the canonical format (ms). */
|
|
45
|
+
const FLUSH_INTERVAL_MS = 30_000;
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
// YDocManager
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Server-side pool of Y.js documents with binary append-log persistence.
|
|
51
|
+
*
|
|
52
|
+
* One shared instance per product server process. Pass it to `createBrainyWsHandler`
|
|
53
|
+
* so the WebSocket message handler can route Y.js messages to the correct doc.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { YDocManager } from '@soulcraft/sdk/server'
|
|
58
|
+
* import { pool } from './instance-pool'
|
|
59
|
+
*
|
|
60
|
+
* export const ydocManager = new YDocManager()
|
|
61
|
+
*
|
|
62
|
+
* const wsHandler = createBrainyWsHandler({
|
|
63
|
+
* resolveBrain: (scope) => pool.forTenant(scope),
|
|
64
|
+
* authenticate: (token, scope) => verifyCapabilityToken(token, process.env.WS_TOKEN_SECRET!),
|
|
65
|
+
* ydocManager,
|
|
66
|
+
* })
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export class YDocManager {
|
|
70
|
+
/** Pool of live Y.Doc instances keyed by `${scope}:${filePath}`. */
|
|
71
|
+
docs = new Map();
|
|
72
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Returns the live Y.Doc for the given scope + file, loading from the `.ydoc`
|
|
75
|
+
* binary log (or converting from the canonical format) on first access.
|
|
76
|
+
*
|
|
77
|
+
* Registers `peerId` as an active peer so the manager can track when the last
|
|
78
|
+
* peer disconnects.
|
|
79
|
+
*
|
|
80
|
+
* @param scope - The session scope (`${emailHash}:${workspaceId}`).
|
|
81
|
+
* @param filePath - Absolute VFS path to the source file (e.g. `/proj/doc.wdoc`).
|
|
82
|
+
* @param brain - The Brainy instance owning the VFS for this scope.
|
|
83
|
+
* @param peerId - Unique ID for the connecting peer.
|
|
84
|
+
* @returns The live `Y.Doc` for this file.
|
|
85
|
+
*/
|
|
86
|
+
async getOrCreate(scope, filePath, brain, peerId) {
|
|
87
|
+
const key = _key(scope, filePath);
|
|
88
|
+
let entry = this.docs.get(key);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
const ydoc = await this._load(filePath, brain);
|
|
91
|
+
entry = {
|
|
92
|
+
ydoc,
|
|
93
|
+
peers: new Set(),
|
|
94
|
+
flushTimer: null,
|
|
95
|
+
flushPending: false,
|
|
96
|
+
brain,
|
|
97
|
+
filePath,
|
|
98
|
+
ydocPath: filePath + YDOC_EXT,
|
|
99
|
+
format: _formatFromPath(filePath),
|
|
100
|
+
};
|
|
101
|
+
this.docs.set(key, entry);
|
|
102
|
+
this._scheduleFlush(key, entry);
|
|
103
|
+
}
|
|
104
|
+
entry.peers.add(peerId);
|
|
105
|
+
return entry.ydoc;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Applies a Y.js update received from a peer.
|
|
109
|
+
*
|
|
110
|
+
* - Applies the update to the in-memory Y.Doc.
|
|
111
|
+
* - Appends the raw binary update to the `.ydoc` log immediately.
|
|
112
|
+
*
|
|
113
|
+
* The debounced 30s flush to the canonical format runs independently.
|
|
114
|
+
*
|
|
115
|
+
* @param scope - Session scope.
|
|
116
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
117
|
+
* @param update - Raw binary Y.js update (from `Y.encodeStateAsUpdate` or an observer).
|
|
118
|
+
*/
|
|
119
|
+
async applyUpdate(scope, filePath, update) {
|
|
120
|
+
const key = _key(scope, filePath);
|
|
121
|
+
const entry = this.docs.get(key);
|
|
122
|
+
if (!entry)
|
|
123
|
+
return;
|
|
124
|
+
Y.applyUpdate(entry.ydoc, update);
|
|
125
|
+
// Append to the binary log — mmap/SSD makes this free.
|
|
126
|
+
await entry.brain.vfs.appendFile(entry.ydocPath, Buffer.from(update));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns the current Y.js state vector for the given document.
|
|
130
|
+
*
|
|
131
|
+
* Used during the Y.js sync protocol: the client sends its state vector,
|
|
132
|
+
* the server replies with the diff (all updates the client is missing).
|
|
133
|
+
*
|
|
134
|
+
* @param scope - Session scope.
|
|
135
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
136
|
+
* @returns Encoded state vector as `Uint8Array`.
|
|
137
|
+
*/
|
|
138
|
+
async getStateVector(scope, filePath, brain) {
|
|
139
|
+
const key = _key(scope, filePath);
|
|
140
|
+
const entry = this.docs.get(key);
|
|
141
|
+
if (entry)
|
|
142
|
+
return Y.encodeStateVector(entry.ydoc);
|
|
143
|
+
// Not in memory — load transiently just for the state vector, then discard.
|
|
144
|
+
const ydoc = await this._load(filePath, brain);
|
|
145
|
+
const sv = Y.encodeStateVector(ydoc);
|
|
146
|
+
ydoc.destroy();
|
|
147
|
+
return sv;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Returns the Y.js diff between the server's state and the client's state vector.
|
|
151
|
+
*
|
|
152
|
+
* Called after the client sends a `y-sync` message. The diff contains all updates
|
|
153
|
+
* the client is missing so it can catch up to the server's current state.
|
|
154
|
+
*
|
|
155
|
+
* @param scope - Session scope.
|
|
156
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
157
|
+
* @param clientStateVector - The state vector encoded by the client.
|
|
158
|
+
* @returns Encoded update diff as `Uint8Array`.
|
|
159
|
+
*/
|
|
160
|
+
async getDiff(scope, filePath, brain, clientStateVector) {
|
|
161
|
+
const key = _key(scope, filePath);
|
|
162
|
+
const entry = this.docs.get(key);
|
|
163
|
+
if (entry)
|
|
164
|
+
return Y.encodeStateAsUpdate(entry.ydoc, clientStateVector);
|
|
165
|
+
// Not in memory — load transiently, compute diff, discard.
|
|
166
|
+
const ydoc = await this._load(filePath, brain);
|
|
167
|
+
const diff = Y.encodeStateAsUpdate(ydoc, clientStateVector);
|
|
168
|
+
ydoc.destroy();
|
|
169
|
+
return diff;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Notifies the manager that a peer has disconnected from the given document.
|
|
173
|
+
*
|
|
174
|
+
* If this was the last peer:
|
|
175
|
+
* 1. Flushes the Y.Doc to the canonical format immediately.
|
|
176
|
+
* 2. Cancels the periodic flush timer.
|
|
177
|
+
* 3. Destroys and discards the Y.Doc from memory.
|
|
178
|
+
*
|
|
179
|
+
* No warm cache — `.ydoc` binary replay is instant on mmap/SSD.
|
|
180
|
+
*
|
|
181
|
+
* @param scope - Session scope.
|
|
182
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
183
|
+
* @param peerId - The peer that disconnected.
|
|
184
|
+
*/
|
|
185
|
+
async onPeerDisconnect(scope, filePath, peerId) {
|
|
186
|
+
const key = _key(scope, filePath);
|
|
187
|
+
const entry = this.docs.get(key);
|
|
188
|
+
if (!entry)
|
|
189
|
+
return;
|
|
190
|
+
entry.peers.delete(peerId);
|
|
191
|
+
if (entry.peers.size === 0) {
|
|
192
|
+
// Last peer — flush immediately and discard. No warm cache.
|
|
193
|
+
if (entry.flushTimer) {
|
|
194
|
+
clearInterval(entry.flushTimer);
|
|
195
|
+
entry.flushTimer = null;
|
|
196
|
+
}
|
|
197
|
+
await this._flush(entry);
|
|
198
|
+
entry.ydoc.destroy();
|
|
199
|
+
this.docs.delete(key);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Invalidates a cached Y.Doc when the canonical file is overwritten externally
|
|
204
|
+
* (e.g. via import or direct API write that bypasses Y.js).
|
|
205
|
+
*
|
|
206
|
+
* If the document is currently in memory (has active peers), it is rebuilt from
|
|
207
|
+
* the new canonical file content. If no peers are connected, any stale `.ydoc`
|
|
208
|
+
* log is deleted so the next cold start reads the new canonical file.
|
|
209
|
+
*
|
|
210
|
+
* @param scope - Session scope.
|
|
211
|
+
* @param filePath - Absolute VFS path to the source file.
|
|
212
|
+
* @param brain - The Brainy instance owning the VFS.
|
|
213
|
+
*/
|
|
214
|
+
async invalidate(scope, filePath, brain) {
|
|
215
|
+
const key = _key(scope, filePath);
|
|
216
|
+
const entry = this.docs.get(key);
|
|
217
|
+
const ydocPath = filePath + YDOC_EXT;
|
|
218
|
+
if (entry) {
|
|
219
|
+
// Rebuild in-place so active peers automatically get the new state.
|
|
220
|
+
const newYdoc = await _loadFromCanonical(filePath, brain);
|
|
221
|
+
const update = Y.encodeStateAsUpdate(newYdoc);
|
|
222
|
+
Y.applyUpdate(entry.ydoc, update);
|
|
223
|
+
newYdoc.destroy();
|
|
224
|
+
// Rewrite the .ydoc log with the new full state.
|
|
225
|
+
await brain.vfs.writeFile(ydocPath, Buffer.from(Y.encodeStateAsUpdate(entry.ydoc)));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// No active peers — just delete the stale .ydoc so next cold start is clean.
|
|
229
|
+
try {
|
|
230
|
+
await brain.vfs.writeFile(ydocPath, Buffer.from(Y.encodeStateAsUpdate(await _loadFromCanonical(filePath, brain))));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// File may not exist yet — ignore.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Notifies the manager that a peer has disconnected from ALL documents it had open.
|
|
239
|
+
*
|
|
240
|
+
* Called from `BrainyWsHandler.handleClose()` when a WebSocket connection closes.
|
|
241
|
+
* Iterates all documents in this scope and removes the peer — if it was the last
|
|
242
|
+
* peer on any document, that document is flushed and discarded immediately.
|
|
243
|
+
*
|
|
244
|
+
* @param scope - Session scope (`${emailHash}:${workspaceId}`).
|
|
245
|
+
* @param peerId - The peer that disconnected.
|
|
246
|
+
*/
|
|
247
|
+
async onPeerDisconnectAll(scope, peerId) {
|
|
248
|
+
const prefix = scope + ':';
|
|
249
|
+
for (const [key, entry] of this.docs) {
|
|
250
|
+
if (key.startsWith(prefix) && entry.peers.has(peerId)) {
|
|
251
|
+
entry.peers.delete(peerId);
|
|
252
|
+
if (entry.peers.size === 0) {
|
|
253
|
+
if (entry.flushTimer) {
|
|
254
|
+
clearInterval(entry.flushTimer);
|
|
255
|
+
entry.flushTimer = null;
|
|
256
|
+
}
|
|
257
|
+
await this._flush(entry);
|
|
258
|
+
entry.ydoc.destroy();
|
|
259
|
+
this.docs.delete(key);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Flushes and discards all Y.Docs for a given scope.
|
|
266
|
+
*
|
|
267
|
+
* Called during unpublish to clean up all open documents for a project before
|
|
268
|
+
* the project is removed from the publish registry.
|
|
269
|
+
*
|
|
270
|
+
* @param scope - Session scope (`${emailHash}:${workspaceId}`).
|
|
271
|
+
*/
|
|
272
|
+
async flushAndDiscardScope(scope) {
|
|
273
|
+
const prefix = scope + ':';
|
|
274
|
+
const toDiscard = [];
|
|
275
|
+
for (const [key, entry] of this.docs) {
|
|
276
|
+
if (key.startsWith(prefix)) {
|
|
277
|
+
if (entry.flushTimer)
|
|
278
|
+
clearInterval(entry.flushTimer);
|
|
279
|
+
await this._flush(entry);
|
|
280
|
+
entry.ydoc.destroy();
|
|
281
|
+
toDiscard.push(key);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const key of toDiscard)
|
|
285
|
+
this.docs.delete(key);
|
|
286
|
+
}
|
|
287
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
288
|
+
/**
|
|
289
|
+
* Loads a Y.Doc from the `.ydoc` binary log, falling back to converting the
|
|
290
|
+
* canonical file if no log exists yet.
|
|
291
|
+
*/
|
|
292
|
+
async _load(filePath, brain) {
|
|
293
|
+
const ydocPath = filePath + YDOC_EXT;
|
|
294
|
+
const ydoc = new Y.Doc();
|
|
295
|
+
try {
|
|
296
|
+
const buf = await brain.vfs.readFile(ydocPath);
|
|
297
|
+
if (buf && buf.length > 0) {
|
|
298
|
+
// Binary replay — instant on mmap/SSD.
|
|
299
|
+
Y.applyUpdate(ydoc, new Uint8Array(buf));
|
|
300
|
+
return ydoc;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// .ydoc not found — fall through to canonical load.
|
|
305
|
+
}
|
|
306
|
+
// No .ydoc yet — bootstrap from the canonical format.
|
|
307
|
+
const bootstrapYdoc = await _loadFromCanonical(filePath, brain);
|
|
308
|
+
const bootstrapUpdate = Y.encodeStateAsUpdate(bootstrapYdoc);
|
|
309
|
+
Y.applyUpdate(ydoc, bootstrapUpdate);
|
|
310
|
+
bootstrapYdoc.destroy();
|
|
311
|
+
// Write the initial .ydoc log so future cold starts use binary replay.
|
|
312
|
+
await brain.vfs.writeFile(ydocPath, Buffer.from(bootstrapUpdate));
|
|
313
|
+
return ydoc;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Schedules the periodic 30s flush for the given document entry.
|
|
317
|
+
*/
|
|
318
|
+
_scheduleFlush(key, entry) {
|
|
319
|
+
entry.flushTimer = setInterval(async () => {
|
|
320
|
+
if (this.docs.has(key)) {
|
|
321
|
+
await this._flush(entry);
|
|
322
|
+
}
|
|
323
|
+
}, FLUSH_INTERVAL_MS);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Flushes the live Y.Doc to the canonical format in the VFS.
|
|
327
|
+
*/
|
|
328
|
+
async _flush(entry) {
|
|
329
|
+
try {
|
|
330
|
+
const json = _ydocToCanonicalJson(entry.ydoc, entry.format);
|
|
331
|
+
await entry.brain.vfs.writeFile(entry.filePath, JSON.stringify(json, null, 2));
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
console.error(`[YDocManager] flush failed for ${entry.filePath}:`, err);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
339
|
+
// Helpers — format conversion
|
|
340
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
341
|
+
/**
|
|
342
|
+
* Builds the pool key from scope and file path.
|
|
343
|
+
*
|
|
344
|
+
* @param scope - Session scope.
|
|
345
|
+
* @param filePath - Absolute VFS path.
|
|
346
|
+
* @returns Compound key string.
|
|
347
|
+
*/
|
|
348
|
+
function _key(scope, filePath) {
|
|
349
|
+
return `${scope}:${filePath}`;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Derives the flush format from a file path extension.
|
|
353
|
+
*
|
|
354
|
+
* @param filePath - Absolute VFS path.
|
|
355
|
+
* @returns One of the supported flush format identifiers.
|
|
356
|
+
*/
|
|
357
|
+
function _formatFromPath(filePath) {
|
|
358
|
+
if (filePath.endsWith('.wdoc'))
|
|
359
|
+
return 'wdoc';
|
|
360
|
+
if (filePath.endsWith('.wslide'))
|
|
361
|
+
return 'wslide';
|
|
362
|
+
if (filePath.endsWith('.wviz'))
|
|
363
|
+
return 'wviz';
|
|
364
|
+
if (filePath.endsWith('.wquiz'))
|
|
365
|
+
return 'wquiz';
|
|
366
|
+
return 'text';
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Loads a Y.Doc from the canonical format file (`.wdoc`, `.wslide`, etc.).
|
|
370
|
+
*
|
|
371
|
+
* Each format stores its content as the root XmlFragment's children in the Y.Doc.
|
|
372
|
+
* For v1, all formats serialize as a JSON string stored in a Y.Text field named
|
|
373
|
+
* `'content'` — simple and sufficient for the sync protocol to work. The rich
|
|
374
|
+
* TipTap↔Y.js binding (`prosemirror-binding`) handles the structured mapping
|
|
375
|
+
* on the client side; the server only needs the raw bytes.
|
|
376
|
+
*
|
|
377
|
+
* @param filePath - Absolute VFS path to the canonical file.
|
|
378
|
+
* @param brain - The Brainy instance owning the VFS.
|
|
379
|
+
* @returns A bootstrapped `Y.Doc` containing the file's current content.
|
|
380
|
+
*/
|
|
381
|
+
async function _loadFromCanonical(filePath, brain) {
|
|
382
|
+
const ydoc = new Y.Doc();
|
|
383
|
+
try {
|
|
384
|
+
const raw = await brain.vfs.readFile(filePath);
|
|
385
|
+
if (raw && raw.length > 0) {
|
|
386
|
+
// Store the canonical JSON as a Y.Text so Y.js can manage it as a shared type.
|
|
387
|
+
// TipTap's Collaboration extension replaces this with the ProseMirror Y.XmlFragment
|
|
388
|
+
// once the first peer connects with a real editor.
|
|
389
|
+
const text = ydoc.getText('content');
|
|
390
|
+
text.insert(0, raw.toString('utf-8'));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// File doesn't exist yet — empty doc.
|
|
395
|
+
}
|
|
396
|
+
return ydoc;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Serializes a Y.Doc back to the canonical JSON format for writing to the VFS.
|
|
400
|
+
*
|
|
401
|
+
* For the `text` format (code files), returns the raw string content.
|
|
402
|
+
* For all document formats (wdoc, wslide, wviz, wquiz), returns the parsed JSON
|
|
403
|
+
* object so the VFS file stays human-readable and API-consumable.
|
|
404
|
+
*
|
|
405
|
+
* @param ydoc - The live Y.Doc to serialize.
|
|
406
|
+
* @param format - The target format.
|
|
407
|
+
* @returns The canonical content as a JavaScript value (JSON.stringify'd by the caller).
|
|
408
|
+
*/
|
|
409
|
+
function _ydocToCanonicalJson(ydoc, format) {
|
|
410
|
+
const text = ydoc.getText('content').toString();
|
|
411
|
+
if (format === 'text' || !text)
|
|
412
|
+
return text;
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse(text);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// If TipTap's Y.XmlFragment binding has taken over, the 'content' Y.Text may be
|
|
418
|
+
// empty. In that case, fall back to the XmlFragment serialization path.
|
|
419
|
+
// This is handled by the TipTap binding on the client — server sees the canonical
|
|
420
|
+
// JSON via the 30s flush path which the client drives via onChange.
|
|
421
|
+
return {};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
//# sourceMappingURL=ydoc-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ydoc-manager.js","sourceRoot":"","sources":["../../../src/modules/brainy/ydoc-manager.ts"],"names":[],"mappings":"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"}
|