@agent-native/core 0.22.43 → 0.22.45
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/AssistantChat.d.ts +2 -2
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +51 -23
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +8 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +10 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-chat.d.ts +2 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +20 -1
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +16 -2
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/storage.d.ts +8 -0
- package/dist/collab/storage.d.ts.map +1 -1
- package/dist/collab/storage.js +55 -7
- package/dist/collab/storage.js.map +1 -1
- package/dist/collab/ydoc-manager.d.ts.map +1 -1
- package/dist/collab/ydoc-manager.js +121 -69
- package/dist/collab/ydoc-manager.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Server-side Yjs document manager with LRU caching and SQL persistence.
|
|
3
3
|
*/
|
|
4
4
|
import * as Y from "yjs";
|
|
5
|
-
import { loadYDocState, saveYDocState } from "./storage.js";
|
|
5
|
+
import { loadYDocRecord, loadYDocState, saveYDocState, trySaveYDocState, } from "./storage.js";
|
|
6
6
|
import { applyTextToYDoc, initYDocWithText } from "./text-to-yjs.js";
|
|
7
7
|
import { searchAndReplaceInYXml, extractTextFromYXml } from "./xml-ops.js";
|
|
8
8
|
import { applyJsonDiff, applyJsonPatch, yDocToJson, initYDocWithJson, } from "./json-to-yjs.js";
|
|
@@ -11,6 +11,7 @@ import { uint8ArrayToBase64 } from "./storage.js";
|
|
|
11
11
|
const DEFAULT_FIELD = "content";
|
|
12
12
|
const MAX_CACHE = 50;
|
|
13
13
|
const _cache = new Map();
|
|
14
|
+
const _writeLocks = new Map();
|
|
14
15
|
function evictIfNeeded() {
|
|
15
16
|
if (_cache.size <= MAX_CACHE)
|
|
16
17
|
return;
|
|
@@ -29,6 +30,44 @@ function evictIfNeeded() {
|
|
|
29
30
|
_cache.delete(oldest);
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
async function withDocWriteLock(docId, fn) {
|
|
34
|
+
const previous = _writeLocks.get(docId) ?? Promise.resolve();
|
|
35
|
+
let release;
|
|
36
|
+
const current = new Promise((resolve) => {
|
|
37
|
+
release = resolve;
|
|
38
|
+
});
|
|
39
|
+
const chained = previous.catch(() => { }).then(() => current);
|
|
40
|
+
_writeLocks.set(docId, chained);
|
|
41
|
+
await previous.catch(() => { });
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
release();
|
|
47
|
+
if (_writeLocks.get(docId) === chained) {
|
|
48
|
+
_writeLocks.delete(docId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function applyStoredState(docId, doc) {
|
|
53
|
+
const stored = await loadYDocState(docId);
|
|
54
|
+
if (stored && stored.length > 0) {
|
|
55
|
+
Y.applyUpdate(doc, stored);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function persistMergedState(docId, doc, getTextSnapshot) {
|
|
59
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
60
|
+
const latest = await loadYDocRecord(docId);
|
|
61
|
+
if (latest?.state && latest.state.length > 0) {
|
|
62
|
+
Y.applyUpdate(doc, latest.state);
|
|
63
|
+
}
|
|
64
|
+
const saved = await trySaveYDocState(docId, Y.encodeStateAsUpdate(doc), getTextSnapshot(), latest?.version ?? null);
|
|
65
|
+
if (saved)
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await applyStoredState(docId, doc);
|
|
69
|
+
await saveYDocState(docId, Y.encodeStateAsUpdate(doc), getTextSnapshot());
|
|
70
|
+
}
|
|
32
71
|
/**
|
|
33
72
|
* Get or load a Yjs document by ID. Creates a new empty doc if none exists.
|
|
34
73
|
*/
|
|
@@ -52,12 +91,13 @@ export async function getDoc(docId) {
|
|
|
52
91
|
* Persists the result and emits a change event.
|
|
53
92
|
*/
|
|
54
93
|
export async function applyUpdate(docId, update, requestSource) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
94
|
+
return withDocWriteLock(docId, async () => {
|
|
95
|
+
const doc = await getDoc(docId);
|
|
96
|
+
await applyStoredState(docId, doc);
|
|
97
|
+
Y.applyUpdate(doc, update);
|
|
98
|
+
await persistMergedState(docId, doc, () => doc.getText(DEFAULT_FIELD).toString());
|
|
99
|
+
emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
|
|
100
|
+
});
|
|
61
101
|
}
|
|
62
102
|
/**
|
|
63
103
|
* Apply a text change to a document. Computes the minimal diff and
|
|
@@ -66,16 +106,17 @@ export async function applyUpdate(docId, update, requestSource) {
|
|
|
66
106
|
* Returns the text snapshot after the update.
|
|
67
107
|
*/
|
|
68
108
|
export async function applyText(docId, newText, fieldName = DEFAULT_FIELD, requestSource) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
return withDocWriteLock(docId, async () => {
|
|
110
|
+
const doc = await getDoc(docId);
|
|
111
|
+
await applyStoredState(docId, doc);
|
|
112
|
+
const update = applyTextToYDoc(doc, fieldName, newText, "server");
|
|
113
|
+
if (update.length === 0) {
|
|
114
|
+
return doc.getText(fieldName).toString();
|
|
115
|
+
}
|
|
116
|
+
await persistMergedState(docId, doc, () => doc.getText(fieldName).toString());
|
|
117
|
+
emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
|
|
72
118
|
return doc.getText(fieldName).toString();
|
|
73
|
-
}
|
|
74
|
-
const state = Y.encodeStateAsUpdate(doc);
|
|
75
|
-
const text = doc.getText(fieldName).toString();
|
|
76
|
-
await saveYDocState(docId, state, text);
|
|
77
|
-
emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
|
|
78
|
-
return text;
|
|
119
|
+
});
|
|
79
120
|
}
|
|
80
121
|
/**
|
|
81
122
|
* Search-and-replace text within a Y.XmlFragment (ProseMirror tree).
|
|
@@ -84,34 +125,35 @@ export async function applyText(docId, newText, fieldName = DEFAULT_FIELD, reque
|
|
|
84
125
|
* Returns whether the text was found and the binary update.
|
|
85
126
|
*/
|
|
86
127
|
export async function searchAndReplace(docId, find, replace, requestSource) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
update =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
found =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
return withDocWriteLock(docId, async () => {
|
|
129
|
+
const doc = await getDoc(docId);
|
|
130
|
+
await applyStoredState(docId, doc);
|
|
131
|
+
const fragment = doc.getXmlFragment("default");
|
|
132
|
+
// Capture the update produced by the transaction
|
|
133
|
+
let update = new Uint8Array(0);
|
|
134
|
+
const handler = (u) => {
|
|
135
|
+
update = u;
|
|
136
|
+
};
|
|
137
|
+
doc.on("update", handler);
|
|
138
|
+
let found = false;
|
|
139
|
+
doc.transact(() => {
|
|
140
|
+
found = searchAndReplaceInYXml(fragment, find, replace);
|
|
141
|
+
}, "agent");
|
|
142
|
+
doc.off("update", handler);
|
|
143
|
+
if (!found || update.length === 0) {
|
|
144
|
+
return { found: false, update: new Uint8Array(0) };
|
|
145
|
+
}
|
|
146
|
+
await persistMergedState(docId, doc, () => extractTextFromYXml(fragment));
|
|
147
|
+
emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
|
|
148
|
+
return { found: true, update };
|
|
149
|
+
});
|
|
109
150
|
}
|
|
110
151
|
/**
|
|
111
152
|
* Get the current text content of a document field.
|
|
112
153
|
*/
|
|
113
154
|
export async function getText(docId, fieldName = DEFAULT_FIELD) {
|
|
114
155
|
const doc = await getDoc(docId);
|
|
156
|
+
await applyStoredState(docId, doc);
|
|
115
157
|
return doc.getText(fieldName).toString();
|
|
116
158
|
}
|
|
117
159
|
/**
|
|
@@ -119,6 +161,7 @@ export async function getText(docId, fieldName = DEFAULT_FIELD) {
|
|
|
119
161
|
*/
|
|
120
162
|
export async function getState(docId) {
|
|
121
163
|
const doc = await getDoc(docId);
|
|
164
|
+
await applyStoredState(docId, doc);
|
|
122
165
|
return Y.encodeStateAsUpdate(doc);
|
|
123
166
|
}
|
|
124
167
|
/**
|
|
@@ -126,6 +169,7 @@ export async function getState(docId) {
|
|
|
126
169
|
*/
|
|
127
170
|
export async function getIncUpdate(docId, clientStateVector) {
|
|
128
171
|
const doc = await getDoc(docId);
|
|
172
|
+
await applyStoredState(docId, doc);
|
|
129
173
|
return Y.encodeStateAsUpdate(doc, clientStateVector);
|
|
130
174
|
}
|
|
131
175
|
/**
|
|
@@ -133,14 +177,16 @@ export async function getIncUpdate(docId, clientStateVector) {
|
|
|
133
177
|
* Only seeds if no collab state exists yet.
|
|
134
178
|
*/
|
|
135
179
|
export async function seedFromText(docId, text, fieldName = DEFAULT_FIELD) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
return withDocWriteLock(docId, async () => {
|
|
181
|
+
const existing = await loadYDocState(docId);
|
|
182
|
+
if (existing && existing.length > 0)
|
|
183
|
+
return; // Already seeded
|
|
184
|
+
const { doc, state } = initYDocWithText(fieldName, text);
|
|
185
|
+
await saveYDocState(docId, state, text);
|
|
186
|
+
// Cache the doc
|
|
187
|
+
evictIfNeeded();
|
|
188
|
+
_cache.set(docId, { doc, lastAccess: Date.now() });
|
|
189
|
+
});
|
|
144
190
|
}
|
|
145
191
|
// ─── Structured JSON Operations ─────────────────────────────────────
|
|
146
192
|
/**
|
|
@@ -148,32 +194,36 @@ export async function seedFromText(docId, text, fieldName = DEFAULT_FIELD) {
|
|
|
148
194
|
* and converts it to Yjs operations on Y.Map/Y.Array.
|
|
149
195
|
*/
|
|
150
196
|
export async function applyJson(docId, newJson, fieldName = "data", type = "map", requestSource) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
197
|
+
return withDocWriteLock(docId, async () => {
|
|
198
|
+
const doc = await getDoc(docId);
|
|
199
|
+
await applyStoredState(docId, doc);
|
|
200
|
+
const update = applyJsonDiff(doc, fieldName, newJson, "server");
|
|
201
|
+
if (update.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
await persistMergedState(docId, doc, () => JSON.stringify(newJson));
|
|
204
|
+
emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
|
|
205
|
+
});
|
|
158
206
|
}
|
|
159
207
|
/**
|
|
160
208
|
* Apply surgical JSON patch operations to a document.
|
|
161
209
|
*/
|
|
162
210
|
export async function applyPatchOps(docId, ops, fieldName = "data", requestSource) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
211
|
+
return withDocWriteLock(docId, async () => {
|
|
212
|
+
const doc = await getDoc(docId);
|
|
213
|
+
await applyStoredState(docId, doc);
|
|
214
|
+
const update = applyJsonPatch(doc, fieldName, ops, "server");
|
|
215
|
+
if (update.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
await persistMergedState(docId, doc, () => JSON.stringify(yDocToJson(doc, fieldName)));
|
|
218
|
+
emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
|
|
219
|
+
});
|
|
171
220
|
}
|
|
172
221
|
/**
|
|
173
222
|
* Get the current JSON state of a document field.
|
|
174
223
|
*/
|
|
175
224
|
export async function getJson(docId, fieldName = "data") {
|
|
176
225
|
const doc = await getDoc(docId);
|
|
226
|
+
await applyStoredState(docId, doc);
|
|
177
227
|
return yDocToJson(doc, fieldName);
|
|
178
228
|
}
|
|
179
229
|
/**
|
|
@@ -181,14 +231,16 @@ export async function getJson(docId, fieldName = "data") {
|
|
|
181
231
|
* Only seeds if no collab state exists yet.
|
|
182
232
|
*/
|
|
183
233
|
export async function seedFromJson(docId, json, fieldName = "data", type = "map") {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
234
|
+
return withDocWriteLock(docId, async () => {
|
|
235
|
+
const existing = await loadYDocState(docId);
|
|
236
|
+
if (existing && existing.length > 0)
|
|
237
|
+
return; // Already seeded
|
|
238
|
+
const { doc, state } = initYDocWithJson(fieldName, json, type);
|
|
239
|
+
await saveYDocState(docId, state, JSON.stringify(json));
|
|
240
|
+
// Cache the doc
|
|
241
|
+
evictIfNeeded();
|
|
242
|
+
_cache.set(docId, { doc, lastAccess: Date.now() });
|
|
243
|
+
});
|
|
192
244
|
}
|
|
193
245
|
/**
|
|
194
246
|
* Release a document from the in-memory cache.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ydoc-manager.js","sourceRoot":"","sources":["../../src/collab/ydoc-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side Yjs document manager with LRU caching and SQL persistence.\n */\n\nimport * as Y from \"yjs\";\nimport { loadYDocState, saveYDocState } from \"./storage.js\";\nimport { applyTextToYDoc, initYDocWithText } from \"./text-to-yjs.js\";\nimport { searchAndReplaceInYXml, extractTextFromYXml } from \"./xml-ops.js\";\nimport {\n applyJsonDiff,\n applyJsonPatch,\n yDocToJson,\n initYDocWithJson,\n type PatchOp,\n} from \"./json-to-yjs.js\";\nimport { emitCollabUpdate } from \"./emitter.js\";\nimport { uint8ArrayToBase64 } from \"./storage.js\";\n\nconst DEFAULT_FIELD = \"content\";\nconst MAX_CACHE = 50;\n\ninterface CacheEntry {\n doc: Y.Doc;\n lastAccess: number;\n}\n\nconst _cache = new Map<string, CacheEntry>();\n\nfunction evictIfNeeded(): void {\n if (_cache.size <= MAX_CACHE) return;\n // Evict least-recently-accessed entry\n let oldest: string | null = null;\n let oldestTime = Infinity;\n for (const [id, entry] of _cache) {\n if (entry.lastAccess < oldestTime) {\n oldestTime = entry.lastAccess;\n oldest = id;\n }\n }\n if (oldest) {\n const entry = _cache.get(oldest);\n entry?.doc.destroy();\n _cache.delete(oldest);\n }\n}\n\n/**\n * Get or load a Yjs document by ID. Creates a new empty doc if none exists.\n */\nexport async function getDoc(docId: string): Promise<Y.Doc> {\n const cached = _cache.get(docId);\n if (cached) {\n cached.lastAccess = Date.now();\n return cached.doc;\n }\n\n const doc = new Y.Doc();\n const stored = await loadYDocState(docId);\n if (stored && stored.length > 0) {\n Y.applyUpdate(doc, stored);\n }\n\n evictIfNeeded();\n _cache.set(docId, { doc, lastAccess: Date.now() });\n return doc;\n}\n\n/**\n * Apply a binary Yjs update (from a client) to a document.\n * Persists the result and emits a change event.\n */\nexport async function applyUpdate(\n docId: string,\n update: Uint8Array,\n requestSource?: string,\n): Promise<void> {\n const doc = await getDoc(docId);\n Y.applyUpdate(doc, update);\n\n const state = Y.encodeStateAsUpdate(doc);\n const text = doc.getText(DEFAULT_FIELD).toString();\n await saveYDocState(docId, state, text);\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n}\n\n/**\n * Apply a text change to a document. Computes the minimal diff and\n * converts it to Yjs operations.\n *\n * Returns the text snapshot after the update.\n */\nexport async function applyText(\n docId: string,\n newText: string,\n fieldName: string = DEFAULT_FIELD,\n requestSource?: string,\n): Promise<string> {\n const doc = await getDoc(docId);\n const update = applyTextToYDoc(doc, fieldName, newText, \"server\");\n\n if (update.length === 0) {\n return doc.getText(fieldName).toString();\n }\n\n const state = Y.encodeStateAsUpdate(doc);\n const text = doc.getText(fieldName).toString();\n await saveYDocState(docId, state, text);\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n return text;\n}\n\n/**\n * Search-and-replace text within a Y.XmlFragment (ProseMirror tree).\n * Produces minimal Yjs operations for cursor-preserving updates.\n *\n * Returns whether the text was found and the binary update.\n */\nexport async function searchAndReplace(\n docId: string,\n find: string,\n replace: string,\n requestSource?: string,\n): Promise<{ found: boolean; update: Uint8Array }> {\n const doc = await getDoc(docId);\n const fragment = doc.getXmlFragment(\"default\");\n\n // Capture the update produced by the transaction\n let update: Uint8Array = new Uint8Array(0);\n const handler = (u: Uint8Array) => {\n update = u;\n };\n doc.on(\"update\", handler);\n\n let found = false;\n doc.transact(() => {\n found = searchAndReplaceInYXml(fragment, find, replace);\n }, \"agent\");\n\n doc.off(\"update\", handler);\n\n if (!found || update.length === 0) {\n return { found: false, update: new Uint8Array(0) };\n }\n\n // Persist and emit\n const state = Y.encodeStateAsUpdate(doc);\n const textSnapshot = extractTextFromYXml(fragment);\n await saveYDocState(docId, state, textSnapshot);\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n\n return { found: true, update };\n}\n\n/**\n * Get the current text content of a document field.\n */\nexport async function getText(\n docId: string,\n fieldName: string = DEFAULT_FIELD,\n): Promise<string> {\n const doc = await getDoc(docId);\n return doc.getText(fieldName).toString();\n}\n\n/**\n * Get the full document state as a Uint8Array.\n */\nexport async function getState(docId: string): Promise<Uint8Array> {\n const doc = await getDoc(docId);\n return Y.encodeStateAsUpdate(doc);\n}\n\n/**\n * Get an incremental update relative to a client's state vector.\n */\nexport async function getIncUpdate(\n docId: string,\n clientStateVector: Uint8Array,\n): Promise<Uint8Array> {\n const doc = await getDoc(docId);\n return Y.encodeStateAsUpdate(doc, clientStateVector);\n}\n\n/**\n * Seed a document from existing text content (for migration).\n * Only seeds if no collab state exists yet.\n */\nexport async function seedFromText(\n docId: string,\n text: string,\n fieldName: string = DEFAULT_FIELD,\n): Promise<void> {\n const existing = await loadYDocState(docId);\n if (existing && existing.length > 0) return; // Already seeded\n\n const { doc, state } = initYDocWithText(fieldName, text);\n await saveYDocState(docId, state, text);\n\n // Cache the doc\n evictIfNeeded();\n _cache.set(docId, { doc, lastAccess: Date.now() });\n}\n\n// ─── Structured JSON Operations ─────────────────────────────────────\n\n/**\n * Apply a full JSON update to a document. Computes the minimal diff\n * and converts it to Yjs operations on Y.Map/Y.Array.\n */\nexport async function applyJson(\n docId: string,\n newJson: any,\n fieldName: string = \"data\",\n type: \"map\" | \"array\" = \"map\",\n requestSource?: string,\n): Promise<void> {\n const doc = await getDoc(docId);\n const update = applyJsonDiff(doc, fieldName, newJson, \"server\");\n\n if (update.length === 0) return;\n\n const state = Y.encodeStateAsUpdate(doc);\n await saveYDocState(docId, state, JSON.stringify(newJson));\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n}\n\n/**\n * Apply surgical JSON patch operations to a document.\n */\nexport async function applyPatchOps(\n docId: string,\n ops: PatchOp[],\n fieldName: string = \"data\",\n requestSource?: string,\n): Promise<void> {\n const doc = await getDoc(docId);\n const update = applyJsonPatch(doc, fieldName, ops, \"server\");\n\n if (update.length === 0) return;\n\n const state = Y.encodeStateAsUpdate(doc);\n const json = yDocToJson(doc, fieldName);\n await saveYDocState(docId, state, JSON.stringify(json));\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n}\n\n/**\n * Get the current JSON state of a document field.\n */\nexport async function getJson(\n docId: string,\n fieldName: string = \"data\",\n): Promise<any> {\n const doc = await getDoc(docId);\n return yDocToJson(doc, fieldName);\n}\n\n/**\n * Seed a document from existing JSON content (for migration).\n * Only seeds if no collab state exists yet.\n */\nexport async function seedFromJson(\n docId: string,\n json: any,\n fieldName: string = \"data\",\n type: \"map\" | \"array\" = \"map\",\n): Promise<void> {\n const existing = await loadYDocState(docId);\n if (existing && existing.length > 0) return; // Already seeded\n\n const { doc, state } = initYDocWithJson(fieldName, json, type);\n await saveYDocState(docId, state, JSON.stringify(json));\n\n // Cache the doc\n evictIfNeeded();\n _cache.set(docId, { doc, lastAccess: Date.now() });\n}\n\n/**\n * Release a document from the in-memory cache.\n */\nexport function releaseDoc(docId: string): void {\n const entry = _cache.get(docId);\n if (entry) {\n entry.doc.destroy();\n _cache.delete(docId);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ydoc-manager.js","sourceRoot":"","sources":["../../src/collab/ydoc-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side Yjs document manager with LRU caching and SQL persistence.\n */\n\nimport * as Y from \"yjs\";\nimport {\n loadYDocRecord,\n loadYDocState,\n saveYDocState,\n trySaveYDocState,\n} from \"./storage.js\";\nimport { applyTextToYDoc, initYDocWithText } from \"./text-to-yjs.js\";\nimport { searchAndReplaceInYXml, extractTextFromYXml } from \"./xml-ops.js\";\nimport {\n applyJsonDiff,\n applyJsonPatch,\n yDocToJson,\n initYDocWithJson,\n type PatchOp,\n} from \"./json-to-yjs.js\";\nimport { emitCollabUpdate } from \"./emitter.js\";\nimport { uint8ArrayToBase64 } from \"./storage.js\";\n\nconst DEFAULT_FIELD = \"content\";\nconst MAX_CACHE = 50;\n\ninterface CacheEntry {\n doc: Y.Doc;\n lastAccess: number;\n}\n\nconst _cache = new Map<string, CacheEntry>();\nconst _writeLocks = new Map<string, Promise<void>>();\n\nfunction evictIfNeeded(): void {\n if (_cache.size <= MAX_CACHE) return;\n // Evict least-recently-accessed entry\n let oldest: string | null = null;\n let oldestTime = Infinity;\n for (const [id, entry] of _cache) {\n if (entry.lastAccess < oldestTime) {\n oldestTime = entry.lastAccess;\n oldest = id;\n }\n }\n if (oldest) {\n const entry = _cache.get(oldest);\n entry?.doc.destroy();\n _cache.delete(oldest);\n }\n}\n\nasync function withDocWriteLock<T>(\n docId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const previous = _writeLocks.get(docId) ?? Promise.resolve();\n let release!: () => void;\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const chained = previous.catch(() => {}).then(() => current);\n _writeLocks.set(docId, chained);\n\n await previous.catch(() => {});\n try {\n return await fn();\n } finally {\n release();\n if (_writeLocks.get(docId) === chained) {\n _writeLocks.delete(docId);\n }\n }\n}\n\nasync function applyStoredState(docId: string, doc: Y.Doc): Promise<void> {\n const stored = await loadYDocState(docId);\n if (stored && stored.length > 0) {\n Y.applyUpdate(doc, stored);\n }\n}\n\nasync function persistMergedState(\n docId: string,\n doc: Y.Doc,\n getTextSnapshot: () => string,\n): Promise<void> {\n for (let attempt = 0; attempt < 5; attempt++) {\n const latest = await loadYDocRecord(docId);\n if (latest?.state && latest.state.length > 0) {\n Y.applyUpdate(doc, latest.state);\n }\n\n const saved = await trySaveYDocState(\n docId,\n Y.encodeStateAsUpdate(doc),\n getTextSnapshot(),\n latest?.version ?? null,\n );\n if (saved) return;\n }\n\n await applyStoredState(docId, doc);\n await saveYDocState(docId, Y.encodeStateAsUpdate(doc), getTextSnapshot());\n}\n\n/**\n * Get or load a Yjs document by ID. Creates a new empty doc if none exists.\n */\nexport async function getDoc(docId: string): Promise<Y.Doc> {\n const cached = _cache.get(docId);\n if (cached) {\n cached.lastAccess = Date.now();\n return cached.doc;\n }\n\n const doc = new Y.Doc();\n const stored = await loadYDocState(docId);\n if (stored && stored.length > 0) {\n Y.applyUpdate(doc, stored);\n }\n\n evictIfNeeded();\n _cache.set(docId, { doc, lastAccess: Date.now() });\n return doc;\n}\n\n/**\n * Apply a binary Yjs update (from a client) to a document.\n * Persists the result and emits a change event.\n */\nexport async function applyUpdate(\n docId: string,\n update: Uint8Array,\n requestSource?: string,\n): Promise<void> {\n return withDocWriteLock(docId, async () => {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n Y.applyUpdate(doc, update);\n\n await persistMergedState(docId, doc, () =>\n doc.getText(DEFAULT_FIELD).toString(),\n );\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n });\n}\n\n/**\n * Apply a text change to a document. Computes the minimal diff and\n * converts it to Yjs operations.\n *\n * Returns the text snapshot after the update.\n */\nexport async function applyText(\n docId: string,\n newText: string,\n fieldName: string = DEFAULT_FIELD,\n requestSource?: string,\n): Promise<string> {\n return withDocWriteLock(docId, async () => {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n const update = applyTextToYDoc(doc, fieldName, newText, \"server\");\n\n if (update.length === 0) {\n return doc.getText(fieldName).toString();\n }\n\n await persistMergedState(docId, doc, () =>\n doc.getText(fieldName).toString(),\n );\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n return doc.getText(fieldName).toString();\n });\n}\n\n/**\n * Search-and-replace text within a Y.XmlFragment (ProseMirror tree).\n * Produces minimal Yjs operations for cursor-preserving updates.\n *\n * Returns whether the text was found and the binary update.\n */\nexport async function searchAndReplace(\n docId: string,\n find: string,\n replace: string,\n requestSource?: string,\n): Promise<{ found: boolean; update: Uint8Array }> {\n return withDocWriteLock(docId, async () => {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n const fragment = doc.getXmlFragment(\"default\");\n\n // Capture the update produced by the transaction\n let update: Uint8Array = new Uint8Array(0);\n const handler = (u: Uint8Array) => {\n update = u;\n };\n doc.on(\"update\", handler);\n\n let found = false;\n doc.transact(() => {\n found = searchAndReplaceInYXml(fragment, find, replace);\n }, \"agent\");\n\n doc.off(\"update\", handler);\n\n if (!found || update.length === 0) {\n return { found: false, update: new Uint8Array(0) };\n }\n\n await persistMergedState(docId, doc, () => extractTextFromYXml(fragment));\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n\n return { found: true, update };\n });\n}\n\n/**\n * Get the current text content of a document field.\n */\nexport async function getText(\n docId: string,\n fieldName: string = DEFAULT_FIELD,\n): Promise<string> {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n return doc.getText(fieldName).toString();\n}\n\n/**\n * Get the full document state as a Uint8Array.\n */\nexport async function getState(docId: string): Promise<Uint8Array> {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n return Y.encodeStateAsUpdate(doc);\n}\n\n/**\n * Get an incremental update relative to a client's state vector.\n */\nexport async function getIncUpdate(\n docId: string,\n clientStateVector: Uint8Array,\n): Promise<Uint8Array> {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n return Y.encodeStateAsUpdate(doc, clientStateVector);\n}\n\n/**\n * Seed a document from existing text content (for migration).\n * Only seeds if no collab state exists yet.\n */\nexport async function seedFromText(\n docId: string,\n text: string,\n fieldName: string = DEFAULT_FIELD,\n): Promise<void> {\n return withDocWriteLock(docId, async () => {\n const existing = await loadYDocState(docId);\n if (existing && existing.length > 0) return; // Already seeded\n\n const { doc, state } = initYDocWithText(fieldName, text);\n await saveYDocState(docId, state, text);\n\n // Cache the doc\n evictIfNeeded();\n _cache.set(docId, { doc, lastAccess: Date.now() });\n });\n}\n\n// ─── Structured JSON Operations ─────────────────────────────────────\n\n/**\n * Apply a full JSON update to a document. Computes the minimal diff\n * and converts it to Yjs operations on Y.Map/Y.Array.\n */\nexport async function applyJson(\n docId: string,\n newJson: any,\n fieldName: string = \"data\",\n type: \"map\" | \"array\" = \"map\",\n requestSource?: string,\n): Promise<void> {\n return withDocWriteLock(docId, async () => {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n const update = applyJsonDiff(doc, fieldName, newJson, \"server\");\n\n if (update.length === 0) return;\n\n await persistMergedState(docId, doc, () => JSON.stringify(newJson));\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n });\n}\n\n/**\n * Apply surgical JSON patch operations to a document.\n */\nexport async function applyPatchOps(\n docId: string,\n ops: PatchOp[],\n fieldName: string = \"data\",\n requestSource?: string,\n): Promise<void> {\n return withDocWriteLock(docId, async () => {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n const update = applyJsonPatch(doc, fieldName, ops, \"server\");\n\n if (update.length === 0) return;\n\n await persistMergedState(docId, doc, () =>\n JSON.stringify(yDocToJson(doc, fieldName)),\n );\n\n emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);\n });\n}\n\n/**\n * Get the current JSON state of a document field.\n */\nexport async function getJson(\n docId: string,\n fieldName: string = \"data\",\n): Promise<any> {\n const doc = await getDoc(docId);\n await applyStoredState(docId, doc);\n return yDocToJson(doc, fieldName);\n}\n\n/**\n * Seed a document from existing JSON content (for migration).\n * Only seeds if no collab state exists yet.\n */\nexport async function seedFromJson(\n docId: string,\n json: any,\n fieldName: string = \"data\",\n type: \"map\" | \"array\" = \"map\",\n): Promise<void> {\n return withDocWriteLock(docId, async () => {\n const existing = await loadYDocState(docId);\n if (existing && existing.length > 0) return; // Already seeded\n\n const { doc, state } = initYDocWithJson(fieldName, json, type);\n await saveYDocState(docId, state, JSON.stringify(json));\n\n // Cache the doc\n evictIfNeeded();\n _cache.set(docId, { doc, lastAccess: Date.now() });\n });\n}\n\n/**\n * Release a document from the in-memory cache.\n */\nexport function releaseDoc(docId: string): void {\n const entry = _cache.get(docId);\n if (entry) {\n entry.doc.destroy();\n _cache.delete(docId);\n }\n}\n"]}
|
package/package.json
CHANGED