@tdxvolt/volt-drive-mcp 0.0.0-stage → 0.1.1
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/README.md +289 -2
- package/bin/http.js +78 -0
- package/bin/stdio.js +72 -0
- package/package.json +54 -3
- package/src/blob.js +192 -0
- package/src/bytes.js +73 -0
- package/src/config.js +116 -0
- package/src/connect.js +43 -0
- package/src/doc-state.js +131 -0
- package/src/drive-index.js +370 -0
- package/src/drive-service.js +1083 -0
- package/src/drive-session.js +283 -0
- package/src/errors.js +23 -0
- package/src/http.js +229 -0
- package/src/index.js +4 -0
- package/src/logger.js +38 -0
- package/src/paths.js +145 -0
- package/src/server.js +338 -0
- package/src/sync-doc.js +268 -0
package/src/sync-doc.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { timeout as timeoutError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers around the Volt `SyncProvider`, which mirrors a server side synapse
|
|
5
|
+
* document into a local `Y.Doc`.
|
|
6
|
+
*
|
|
7
|
+
* Providers are EventEmitters: an `error` event with no listener throws, so
|
|
8
|
+
* every provider created here gets a permanent listener.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function toError(err) {
|
|
12
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a provider with a permanent error listener.
|
|
17
|
+
* @returns {import("node:events").EventEmitter & object}
|
|
18
|
+
*/
|
|
19
|
+
export function createProvider(ctx, driveId, doc, options = {}) {
|
|
20
|
+
const provider = new ctx.SyncProvider(ctx.Y, ctx.client, driveId, doc, {
|
|
21
|
+
readOnlyFallback: true,
|
|
22
|
+
...options
|
|
23
|
+
});
|
|
24
|
+
provider.on("error", (err) => {
|
|
25
|
+
ctx.logger?.debug?.(
|
|
26
|
+
`sync error on ${doc.guid || "?"}: ${toError(err).message}`
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
return provider;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start `provider` and resolve once the initial sync with the Volt is done.
|
|
34
|
+
* Rejects on a sync error or after `timeoutMs`.
|
|
35
|
+
*/
|
|
36
|
+
export function startAndWaitForSync(provider, timeoutMs, what = "document") {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let settled = false;
|
|
39
|
+
let timer = null;
|
|
40
|
+
|
|
41
|
+
const cleanup = () => {
|
|
42
|
+
settled = true;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
provider.off("sync", onSync);
|
|
45
|
+
provider.off("error", onError);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const onSync = (synced) => {
|
|
49
|
+
if (synced && !settled) {
|
|
50
|
+
cleanup();
|
|
51
|
+
resolve();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onError = (err) => {
|
|
56
|
+
if (!settled) {
|
|
57
|
+
cleanup();
|
|
58
|
+
try {
|
|
59
|
+
provider.stopSync();
|
|
60
|
+
} catch {
|
|
61
|
+
// already stopped
|
|
62
|
+
}
|
|
63
|
+
reject(toError(err));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
timer = setTimeout(() => {
|
|
68
|
+
if (settled) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
cleanup();
|
|
72
|
+
try {
|
|
73
|
+
provider.stopSync();
|
|
74
|
+
} catch {
|
|
75
|
+
// already stopped
|
|
76
|
+
}
|
|
77
|
+
reject(timeoutError(`timed out waiting for ${what} to sync`));
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
|
|
80
|
+
provider.on("sync", onSync);
|
|
81
|
+
provider.on("error", onError);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
provider.startSync();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
onError(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Stop `provider` and wait (best effort) until its stream has closed, which
|
|
93
|
+
* is when the Volt has finished receiving everything we sent.
|
|
94
|
+
*/
|
|
95
|
+
export function stopAndWait(provider, timeoutMs = 10_000) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let timer = null;
|
|
98
|
+
const done = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
provider.off("sync", onSync);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
const onSync = (synced) => {
|
|
104
|
+
if (!synced) {
|
|
105
|
+
done();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
timer = setTimeout(done, timeoutMs);
|
|
109
|
+
provider.on("sync", onSync);
|
|
110
|
+
try {
|
|
111
|
+
provider.stopSync();
|
|
112
|
+
} catch {
|
|
113
|
+
done();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Open the document `documentId` of drive `driveId`, run `fn(doc)` once it has
|
|
120
|
+
* synced, and always close the provider afterwards. Resolves with `fn`'s
|
|
121
|
+
* result.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} ctx - { Y, SyncProvider, client, logger }
|
|
124
|
+
* @param {object} [options]
|
|
125
|
+
* @param {number} [options.timeoutMs]
|
|
126
|
+
* @param {boolean} [options.flush] - wait for outbound updates to drain and for
|
|
127
|
+
* the stream to close before resolving (needed after writing).
|
|
128
|
+
*/
|
|
129
|
+
export async function withSyncedDocument(
|
|
130
|
+
ctx,
|
|
131
|
+
driveId,
|
|
132
|
+
documentId,
|
|
133
|
+
fn,
|
|
134
|
+
{ timeoutMs = 30_000, flush = false } = {}
|
|
135
|
+
) {
|
|
136
|
+
const doc = new ctx.Y.Doc();
|
|
137
|
+
doc.guid = documentId;
|
|
138
|
+
const provider = createProvider(ctx, driveId, doc);
|
|
139
|
+
|
|
140
|
+
await startAndWaitForSync(provider, timeoutMs, `document ${documentId}`);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const result = await fn(doc, provider);
|
|
144
|
+
if (flush) {
|
|
145
|
+
await provider.whenOutboundDrained();
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
} finally {
|
|
149
|
+
await (flush ? stopAndWait(provider) : stopFireAndForget(provider));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stopFireAndForget(provider) {
|
|
154
|
+
try {
|
|
155
|
+
provider.stopSync();
|
|
156
|
+
} catch {
|
|
157
|
+
// already stopped
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Read the `content` Y.Text of a text document. */
|
|
162
|
+
export function readTextContent(ctx, driveId, documentId, options) {
|
|
163
|
+
return withSyncedDocument(
|
|
164
|
+
ctx,
|
|
165
|
+
driveId,
|
|
166
|
+
documentId,
|
|
167
|
+
(doc) => doc.getText("content").toString(),
|
|
168
|
+
options
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Read the legacy `content` Y.Array-of-bytes encoding of a binary document. */
|
|
173
|
+
export function readLegacyArrayContent(ctx, driveId, documentId, options) {
|
|
174
|
+
return withSyncedDocument(
|
|
175
|
+
ctx,
|
|
176
|
+
driveId,
|
|
177
|
+
documentId,
|
|
178
|
+
(doc) => {
|
|
179
|
+
const values = doc.getArray("content").toArray();
|
|
180
|
+
const bytes = new Uint8Array(values.length);
|
|
181
|
+
values.forEach((value, index) => {
|
|
182
|
+
bytes[index] =
|
|
183
|
+
typeof value === "number" && value >= 0 && value <= 255 ? value : 0;
|
|
184
|
+
});
|
|
185
|
+
return bytes;
|
|
186
|
+
},
|
|
187
|
+
options
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const isHighSurrogate = (code) => code >= 0xd800 && code <= 0xdbff;
|
|
192
|
+
const isLowSurrogate = (code) => code >= 0xdc00 && code <= 0xdfff;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Turn `ytext`'s current content into `newText` with a single minimal splice
|
|
196
|
+
* (common prefix and suffix are left untouched). Editing in place, rather than
|
|
197
|
+
* deleting and re-inserting everything, lets concurrent edits from the native
|
|
198
|
+
* VoltDrive daemon or other clients merge instead of being clobbered.
|
|
199
|
+
*
|
|
200
|
+
* @returns {boolean} true when the text changed.
|
|
201
|
+
*/
|
|
202
|
+
export function applyTextDiff(ytext, newText) {
|
|
203
|
+
const oldText = ytext.toString();
|
|
204
|
+
if (oldText === newText) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const limit = Math.min(oldText.length, newText.length);
|
|
209
|
+
let prefix = 0;
|
|
210
|
+
while (prefix < limit && oldText.charCodeAt(prefix) === newText.charCodeAt(prefix)) {
|
|
211
|
+
prefix++;
|
|
212
|
+
}
|
|
213
|
+
// Never split a surrogate pair across the edit boundary.
|
|
214
|
+
while (prefix > 0 && isHighSurrogate(oldText.charCodeAt(prefix - 1))) {
|
|
215
|
+
prefix--;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let suffix = 0;
|
|
219
|
+
while (
|
|
220
|
+
suffix < limit - prefix &&
|
|
221
|
+
oldText.charCodeAt(oldText.length - 1 - suffix) ===
|
|
222
|
+
newText.charCodeAt(newText.length - 1 - suffix)
|
|
223
|
+
) {
|
|
224
|
+
suffix++;
|
|
225
|
+
}
|
|
226
|
+
while (suffix > 0 && isLowSurrogate(oldText.charCodeAt(oldText.length - suffix))) {
|
|
227
|
+
suffix--;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const removeCount = oldText.length - prefix - suffix;
|
|
231
|
+
const insertText = newText.slice(prefix, newText.length - suffix);
|
|
232
|
+
|
|
233
|
+
const apply = () => {
|
|
234
|
+
if (removeCount > 0) {
|
|
235
|
+
ytext.delete(prefix, removeCount);
|
|
236
|
+
}
|
|
237
|
+
if (insertText.length > 0) {
|
|
238
|
+
ytext.insert(prefix, insertText);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (ytext.doc) {
|
|
243
|
+
ytext.doc.transact(apply);
|
|
244
|
+
} else {
|
|
245
|
+
apply();
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Write `text` into the `content` Y.Text of an existing or new text document.
|
|
252
|
+
* Resolves once the Volt has received the update.
|
|
253
|
+
* @returns {Promise<boolean>} true when the stored content changed.
|
|
254
|
+
*/
|
|
255
|
+
export function writeTextContent(ctx, driveId, documentId, text, options) {
|
|
256
|
+
return withSyncedDocument(
|
|
257
|
+
ctx,
|
|
258
|
+
driveId,
|
|
259
|
+
documentId,
|
|
260
|
+
(doc, provider) => {
|
|
261
|
+
if (provider.isReadOnly) {
|
|
262
|
+
throw new Error("this document is read-only for the current identity");
|
|
263
|
+
}
|
|
264
|
+
return applyTextDiff(doc.getText("content"), text);
|
|
265
|
+
},
|
|
266
|
+
{ ...options, flush: true }
|
|
267
|
+
);
|
|
268
|
+
}
|