@xnetjs/data-bridge 0.0.2
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/chunk-X6F5CPJI.js +386 -0
- package/dist/index.d.ts +1011 -0
- package/dist/index.js +1246 -0
- package/dist/worker/data-worker.d.ts +2 -0
- package/dist/worker/data-worker.js +304 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QueryCache
|
|
3
|
+
} from "./chunk-X6F5CPJI.js";
|
|
4
|
+
|
|
5
|
+
// src/main-thread-bridge.ts
|
|
6
|
+
var MainThreadBridge = class {
|
|
7
|
+
store;
|
|
8
|
+
cache;
|
|
9
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
10
|
+
storeUnsubscribe = null;
|
|
11
|
+
_syncManager = null;
|
|
12
|
+
constructor(store) {
|
|
13
|
+
this.store = store;
|
|
14
|
+
this.cache = new QueryCache();
|
|
15
|
+
this.storeUnsubscribe = this.store.subscribe((event) => {
|
|
16
|
+
this.handleStoreChange(event);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set the SyncManager for Y.Doc acquisition.
|
|
21
|
+
* This is called by XNetProvider after the SyncManager is created.
|
|
22
|
+
*/
|
|
23
|
+
setSyncManager(syncManager) {
|
|
24
|
+
this._syncManager = syncManager;
|
|
25
|
+
}
|
|
26
|
+
// ─── Queries ────────────────────────────────────────────
|
|
27
|
+
query(schema, options) {
|
|
28
|
+
const schemaId = schema._schemaId;
|
|
29
|
+
const queryId = this.cache.computeQueryId(schemaId, options);
|
|
30
|
+
this.cache.initEntry(queryId, schemaId, options ?? {});
|
|
31
|
+
if (!this.cache.has(queryId) || this.cache.get(queryId) === null) {
|
|
32
|
+
this.loadQuery(queryId, schemaId, options);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
getSnapshot: () => this.cache.get(queryId),
|
|
36
|
+
subscribe: (callback) => this.cache.subscribe(queryId, callback)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load query data from the store and update cache.
|
|
41
|
+
*/
|
|
42
|
+
async loadQuery(queryId, schemaId, options) {
|
|
43
|
+
try {
|
|
44
|
+
let nodes;
|
|
45
|
+
if (options?.nodeId) {
|
|
46
|
+
const node = await this.store.get(options.nodeId);
|
|
47
|
+
nodes = node && node.schemaId === schemaId && !node.deleted ? [node] : [];
|
|
48
|
+
} else {
|
|
49
|
+
nodes = await this.store.list({
|
|
50
|
+
schemaId,
|
|
51
|
+
includeDeleted: options?.includeDeleted,
|
|
52
|
+
limit: options?.limit,
|
|
53
|
+
offset: options?.offset
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
nodes = this.cache.filterNodes(nodes, options);
|
|
57
|
+
nodes = this.cache.sortNodes(nodes, options);
|
|
58
|
+
this.cache.set(queryId, nodes, schemaId, options ?? {});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error("[MainThreadBridge] Failed to load query:", err);
|
|
61
|
+
this.cache.set(queryId, [], schemaId, options ?? {});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle store changes and invalidate affected caches.
|
|
66
|
+
*/
|
|
67
|
+
handleStoreChange(event) {
|
|
68
|
+
const { node, change } = event;
|
|
69
|
+
const schemaId = node?.schemaId ?? change.payload.schemaId;
|
|
70
|
+
if (!schemaId) return;
|
|
71
|
+
const affectedQueries = this.cache.getQueriesForSchema(schemaId);
|
|
72
|
+
for (const queryId of affectedQueries) {
|
|
73
|
+
const options = this.cache.getOptions(queryId);
|
|
74
|
+
this.loadQuery(queryId, schemaId, options);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// ─── Mutations ──────────────────────────────────────────
|
|
78
|
+
async create(schema, data, id) {
|
|
79
|
+
return this.store.create({
|
|
80
|
+
id,
|
|
81
|
+
schemaId: schema._schemaId,
|
|
82
|
+
properties: data
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async update(nodeId, changes) {
|
|
86
|
+
return this.store.update(nodeId, { properties: changes });
|
|
87
|
+
}
|
|
88
|
+
async delete(nodeId) {
|
|
89
|
+
await this.store.delete(nodeId);
|
|
90
|
+
}
|
|
91
|
+
async restore(nodeId) {
|
|
92
|
+
return this.store.restore(nodeId);
|
|
93
|
+
}
|
|
94
|
+
// ─── Documents ─────────────────────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* Acquire a Y.Doc for editing.
|
|
97
|
+
* Delegates to SyncManager if available, otherwise throws.
|
|
98
|
+
*
|
|
99
|
+
* @throws Error if SyncManager is not set
|
|
100
|
+
*/
|
|
101
|
+
async acquireDoc(nodeId) {
|
|
102
|
+
if (!this._syncManager) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"MainThreadBridge.acquireDoc requires SyncManager. Call setSyncManager() first or use useNode with SyncManager context."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const doc = await this._syncManager.acquire(nodeId);
|
|
108
|
+
const awareness = this._syncManager.getAwareness(nodeId);
|
|
109
|
+
if (!awareness) {
|
|
110
|
+
throw new Error(`Failed to get awareness for node ${nodeId}`);
|
|
111
|
+
}
|
|
112
|
+
return { doc, awareness };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Release a Y.Doc when no longer editing.
|
|
116
|
+
*/
|
|
117
|
+
releaseDoc(nodeId) {
|
|
118
|
+
if (this._syncManager) {
|
|
119
|
+
this._syncManager.release(nodeId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ─── Lifecycle ──────────────────────────────────────────
|
|
123
|
+
destroy() {
|
|
124
|
+
if (this.storeUnsubscribe) {
|
|
125
|
+
this.storeUnsubscribe();
|
|
126
|
+
this.storeUnsubscribe = null;
|
|
127
|
+
}
|
|
128
|
+
this.cache.clear();
|
|
129
|
+
this.statusListeners.clear();
|
|
130
|
+
}
|
|
131
|
+
// ─── Status ─────────────────────────────────────────────
|
|
132
|
+
get status() {
|
|
133
|
+
return "connected";
|
|
134
|
+
}
|
|
135
|
+
on(event, handler) {
|
|
136
|
+
if (event === "status") {
|
|
137
|
+
this.statusListeners.add(handler);
|
|
138
|
+
return () => {
|
|
139
|
+
this.statusListeners.delete(handler);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return () => {
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ─── Direct Store Access (Phase 0 compatibility) ────────
|
|
146
|
+
get nodeStore() {
|
|
147
|
+
return this.store;
|
|
148
|
+
}
|
|
149
|
+
subscribeToChanges(listener) {
|
|
150
|
+
return this.store.subscribe(listener);
|
|
151
|
+
}
|
|
152
|
+
async get(nodeId) {
|
|
153
|
+
return this.store.get(nodeId);
|
|
154
|
+
}
|
|
155
|
+
async list(options) {
|
|
156
|
+
return this.store.list(options);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
function createMainThreadBridge(store) {
|
|
160
|
+
return new MainThreadBridge(store);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/worker-bridge.ts
|
|
164
|
+
import { wrap, proxy } from "comlink";
|
|
165
|
+
import { Awareness } from "y-protocols/awareness";
|
|
166
|
+
import * as Y from "yjs";
|
|
167
|
+
|
|
168
|
+
// src/utils/debounce.ts
|
|
169
|
+
function debounce(func, options) {
|
|
170
|
+
const { wait, maxWait, leading = false } = options;
|
|
171
|
+
let timeoutId = null;
|
|
172
|
+
let maxTimeoutId = null;
|
|
173
|
+
let lastArgs = null;
|
|
174
|
+
function invokeFunc() {
|
|
175
|
+
if (lastArgs === null) return;
|
|
176
|
+
const args = lastArgs;
|
|
177
|
+
lastArgs = null;
|
|
178
|
+
func(...args);
|
|
179
|
+
}
|
|
180
|
+
function cancelTimers() {
|
|
181
|
+
if (timeoutId !== null) {
|
|
182
|
+
clearTimeout(timeoutId);
|
|
183
|
+
timeoutId = null;
|
|
184
|
+
}
|
|
185
|
+
if (maxTimeoutId !== null) {
|
|
186
|
+
clearTimeout(maxTimeoutId);
|
|
187
|
+
maxTimeoutId = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function startTimer() {
|
|
191
|
+
if (timeoutId !== null) {
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
}
|
|
194
|
+
timeoutId = setTimeout(() => {
|
|
195
|
+
cancelTimers();
|
|
196
|
+
invokeFunc();
|
|
197
|
+
}, wait);
|
|
198
|
+
}
|
|
199
|
+
function startMaxTimer() {
|
|
200
|
+
if (maxWait === void 0 || maxTimeoutId !== null) return;
|
|
201
|
+
maxTimeoutId = setTimeout(() => {
|
|
202
|
+
cancelTimers();
|
|
203
|
+
invokeFunc();
|
|
204
|
+
}, maxWait);
|
|
205
|
+
}
|
|
206
|
+
function debounced(...args) {
|
|
207
|
+
const isFirstCall = timeoutId === null;
|
|
208
|
+
if (leading) {
|
|
209
|
+
if (isFirstCall) {
|
|
210
|
+
func(...args);
|
|
211
|
+
timeoutId = setTimeout(() => {
|
|
212
|
+
timeoutId = null;
|
|
213
|
+
}, wait);
|
|
214
|
+
return;
|
|
215
|
+
} else {
|
|
216
|
+
if (timeoutId !== null) {
|
|
217
|
+
clearTimeout(timeoutId);
|
|
218
|
+
}
|
|
219
|
+
timeoutId = setTimeout(() => {
|
|
220
|
+
timeoutId = null;
|
|
221
|
+
}, wait);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
lastArgs = args;
|
|
226
|
+
startTimer();
|
|
227
|
+
if (isFirstCall && maxWait !== void 0) {
|
|
228
|
+
startMaxTimer();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
debounced.cancel = function() {
|
|
232
|
+
cancelTimers();
|
|
233
|
+
lastArgs = null;
|
|
234
|
+
};
|
|
235
|
+
debounced.flush = function() {
|
|
236
|
+
cancelTimers();
|
|
237
|
+
invokeFunc();
|
|
238
|
+
};
|
|
239
|
+
debounced.pending = function() {
|
|
240
|
+
return timeoutId !== null;
|
|
241
|
+
};
|
|
242
|
+
return debounced;
|
|
243
|
+
}
|
|
244
|
+
function createUpdateBatcher(options) {
|
|
245
|
+
const { wait, maxWait, onFlush } = options;
|
|
246
|
+
let pendingUpdates = [];
|
|
247
|
+
let timeoutId = null;
|
|
248
|
+
let maxTimeoutId = null;
|
|
249
|
+
let yjsModule = null;
|
|
250
|
+
let yjsLoadPromise = null;
|
|
251
|
+
function loadYjs() {
|
|
252
|
+
if (yjsModule) return Promise.resolve(yjsModule);
|
|
253
|
+
if (yjsLoadPromise) return yjsLoadPromise;
|
|
254
|
+
yjsLoadPromise = import("yjs").then((Y2) => {
|
|
255
|
+
yjsModule = Y2;
|
|
256
|
+
return Y2;
|
|
257
|
+
}).catch((err) => {
|
|
258
|
+
yjsLoadPromise = null;
|
|
259
|
+
throw err;
|
|
260
|
+
});
|
|
261
|
+
return yjsLoadPromise;
|
|
262
|
+
}
|
|
263
|
+
function flush() {
|
|
264
|
+
if (pendingUpdates.length === 0) return;
|
|
265
|
+
const updates = pendingUpdates;
|
|
266
|
+
pendingUpdates = [];
|
|
267
|
+
if (timeoutId !== null) {
|
|
268
|
+
clearTimeout(timeoutId);
|
|
269
|
+
timeoutId = null;
|
|
270
|
+
}
|
|
271
|
+
if (maxTimeoutId !== null) {
|
|
272
|
+
clearTimeout(maxTimeoutId);
|
|
273
|
+
maxTimeoutId = null;
|
|
274
|
+
}
|
|
275
|
+
if (updates.length === 1) {
|
|
276
|
+
onFlush(updates[0]);
|
|
277
|
+
} else if (yjsModule) {
|
|
278
|
+
const merged = yjsModule.mergeUpdates(updates);
|
|
279
|
+
onFlush(merged);
|
|
280
|
+
} else {
|
|
281
|
+
loadYjs().then((Y2) => {
|
|
282
|
+
const merged = Y2.mergeUpdates(updates);
|
|
283
|
+
onFlush(merged);
|
|
284
|
+
}).catch((err) => {
|
|
285
|
+
console.warn(
|
|
286
|
+
"[UpdateBatcher] Failed to load Yjs for merging, flushing updates individually:",
|
|
287
|
+
err
|
|
288
|
+
);
|
|
289
|
+
for (const update of updates) {
|
|
290
|
+
onFlush(update);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function startTimer() {
|
|
296
|
+
if (timeoutId !== null) return;
|
|
297
|
+
timeoutId = setTimeout(() => {
|
|
298
|
+
timeoutId = null;
|
|
299
|
+
flush();
|
|
300
|
+
}, wait);
|
|
301
|
+
}
|
|
302
|
+
function startMaxTimer() {
|
|
303
|
+
if (maxTimeoutId !== null) return;
|
|
304
|
+
maxTimeoutId = setTimeout(() => {
|
|
305
|
+
maxTimeoutId = null;
|
|
306
|
+
flush();
|
|
307
|
+
}, maxWait);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
add(update) {
|
|
311
|
+
pendingUpdates.push(update);
|
|
312
|
+
if (pendingUpdates.length === 2) {
|
|
313
|
+
loadYjs().catch(() => {
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
startTimer();
|
|
317
|
+
startMaxTimer();
|
|
318
|
+
},
|
|
319
|
+
cancel() {
|
|
320
|
+
pendingUpdates = [];
|
|
321
|
+
if (timeoutId !== null) {
|
|
322
|
+
clearTimeout(timeoutId);
|
|
323
|
+
timeoutId = null;
|
|
324
|
+
}
|
|
325
|
+
if (maxTimeoutId !== null) {
|
|
326
|
+
clearTimeout(maxTimeoutId);
|
|
327
|
+
maxTimeoutId = null;
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
flush,
|
|
331
|
+
pending() {
|
|
332
|
+
return pendingUpdates.length > 0;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function createDeltaBatcher(options) {
|
|
337
|
+
const { wait, maxWait, onFlush } = options;
|
|
338
|
+
const pendingDeltas = /* @__PURE__ */ new Map();
|
|
339
|
+
let timeoutId = null;
|
|
340
|
+
let maxTimeoutId = null;
|
|
341
|
+
function flush() {
|
|
342
|
+
if (pendingDeltas.size === 0) return;
|
|
343
|
+
const deltas = [];
|
|
344
|
+
for (const delta of pendingDeltas.values()) {
|
|
345
|
+
if (delta !== null) {
|
|
346
|
+
deltas.push(delta);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
pendingDeltas.clear();
|
|
350
|
+
if (timeoutId !== null) {
|
|
351
|
+
clearTimeout(timeoutId);
|
|
352
|
+
timeoutId = null;
|
|
353
|
+
}
|
|
354
|
+
if (maxTimeoutId !== null) {
|
|
355
|
+
clearTimeout(maxTimeoutId);
|
|
356
|
+
maxTimeoutId = null;
|
|
357
|
+
}
|
|
358
|
+
if (deltas.length > 0) {
|
|
359
|
+
onFlush(deltas);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function startTimer() {
|
|
363
|
+
if (timeoutId !== null) return;
|
|
364
|
+
timeoutId = setTimeout(() => {
|
|
365
|
+
timeoutId = null;
|
|
366
|
+
flush();
|
|
367
|
+
}, wait);
|
|
368
|
+
}
|
|
369
|
+
function startMaxTimer() {
|
|
370
|
+
if (maxTimeoutId !== null) return;
|
|
371
|
+
maxTimeoutId = setTimeout(() => {
|
|
372
|
+
maxTimeoutId = null;
|
|
373
|
+
flush();
|
|
374
|
+
}, maxWait);
|
|
375
|
+
}
|
|
376
|
+
function getNodeId(delta) {
|
|
377
|
+
return delta.type === "add" ? delta.node.id : delta.nodeId;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
add(delta) {
|
|
381
|
+
const nodeId = getNodeId(delta);
|
|
382
|
+
const existing = pendingDeltas.get(nodeId);
|
|
383
|
+
if (existing === void 0) {
|
|
384
|
+
pendingDeltas.set(nodeId, delta);
|
|
385
|
+
} else if (existing === null) {
|
|
386
|
+
pendingDeltas.set(nodeId, delta);
|
|
387
|
+
} else {
|
|
388
|
+
const coalesced = coalesceDeltas(existing, delta);
|
|
389
|
+
pendingDeltas.set(nodeId, coalesced);
|
|
390
|
+
}
|
|
391
|
+
startTimer();
|
|
392
|
+
startMaxTimer();
|
|
393
|
+
},
|
|
394
|
+
cancel() {
|
|
395
|
+
pendingDeltas.clear();
|
|
396
|
+
if (timeoutId !== null) {
|
|
397
|
+
clearTimeout(timeoutId);
|
|
398
|
+
timeoutId = null;
|
|
399
|
+
}
|
|
400
|
+
if (maxTimeoutId !== null) {
|
|
401
|
+
clearTimeout(maxTimeoutId);
|
|
402
|
+
maxTimeoutId = null;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
flush,
|
|
406
|
+
pending() {
|
|
407
|
+
return pendingDeltas.size > 0;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function coalesceDeltas(existing, incoming) {
|
|
412
|
+
if (existing.type === "add" && incoming.type === "remove") {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
if (existing.type === "add" && incoming.type === "update") {
|
|
416
|
+
return { type: "add", node: incoming.node, index: existing.index };
|
|
417
|
+
}
|
|
418
|
+
if (existing.type === "remove" && incoming.type === "add") {
|
|
419
|
+
return { type: "update", nodeId: existing.nodeId, node: incoming.node };
|
|
420
|
+
}
|
|
421
|
+
if (existing.type === "update" && incoming.type === "update") {
|
|
422
|
+
return incoming;
|
|
423
|
+
}
|
|
424
|
+
if (existing.type === "update" && incoming.type === "remove") {
|
|
425
|
+
return incoming;
|
|
426
|
+
}
|
|
427
|
+
return incoming;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/worker-bridge.ts
|
|
431
|
+
var UPDATE_BATCH_WAIT = 16;
|
|
432
|
+
var UPDATE_BATCH_MAX_WAIT = 100;
|
|
433
|
+
var WorkerBridge = class {
|
|
434
|
+
worker;
|
|
435
|
+
remote;
|
|
436
|
+
cache = new QueryCache();
|
|
437
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
438
|
+
queryCounter = 0;
|
|
439
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
440
|
+
_status = "connecting";
|
|
441
|
+
initialized = false;
|
|
442
|
+
// Mirror Y.Docs on the main thread (for TipTap binding)
|
|
443
|
+
mirrorDocs = /* @__PURE__ */ new Map();
|
|
444
|
+
constructor(workerUrl) {
|
|
445
|
+
this.worker = new Worker(workerUrl, { type: "module" });
|
|
446
|
+
this.remote = wrap(this.worker);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Initialize the bridge and underlying worker.
|
|
450
|
+
*/
|
|
451
|
+
async initialize(config) {
|
|
452
|
+
await this.remote.initialize({
|
|
453
|
+
dbName: config.dbName ?? "xnet",
|
|
454
|
+
authorDID: config.authorDID,
|
|
455
|
+
signingKey: Array.from(config.signingKey)
|
|
456
|
+
});
|
|
457
|
+
this.remote.onStatusChange(
|
|
458
|
+
proxy((status) => {
|
|
459
|
+
this._status = status;
|
|
460
|
+
for (const handler of this.statusListeners) {
|
|
461
|
+
handler(status);
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
this.initialized = true;
|
|
466
|
+
this._status = "connected";
|
|
467
|
+
}
|
|
468
|
+
// ─── Queries ─────────────────────────────────────────────────────────────────
|
|
469
|
+
query(schema, options) {
|
|
470
|
+
const schemaId = schema._schemaId;
|
|
471
|
+
const queryId = `q${this.queryCounter++}`;
|
|
472
|
+
const serializedOptions = this.serializeOptions(options);
|
|
473
|
+
this.cache.initEntry(queryId, schemaId, serializedOptions);
|
|
474
|
+
if (this.initialized) {
|
|
475
|
+
this.startWorkerSubscription(queryId, schemaId, serializedOptions);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
getSnapshot: () => this.cache.get(queryId),
|
|
479
|
+
subscribe: (callback) => {
|
|
480
|
+
const subs = this.subscriptions.get(queryId) ?? /* @__PURE__ */ new Set();
|
|
481
|
+
subs.add(callback);
|
|
482
|
+
this.subscriptions.set(queryId, subs);
|
|
483
|
+
const unsubCache = this.cache.subscribe(queryId, callback);
|
|
484
|
+
return () => {
|
|
485
|
+
subs.delete(callback);
|
|
486
|
+
unsubCache();
|
|
487
|
+
if (subs.size === 0) {
|
|
488
|
+
this.subscriptions.delete(queryId);
|
|
489
|
+
this.remote.unsubscribe(queryId).catch(console.error);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async startWorkerSubscription(queryId, schemaId, options) {
|
|
496
|
+
try {
|
|
497
|
+
const initial = await this.remote.subscribe(
|
|
498
|
+
queryId,
|
|
499
|
+
schemaId,
|
|
500
|
+
options,
|
|
501
|
+
proxy((delta) => {
|
|
502
|
+
this.applyDelta(queryId, delta);
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
this.cache.set(queryId, initial, schemaId, options);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error("[WorkerBridge] Failed to subscribe:", err);
|
|
508
|
+
this.cache.set(queryId, [], schemaId, options);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
applyDelta(queryId, delta) {
|
|
512
|
+
const current = this.cache.get(queryId) ?? [];
|
|
513
|
+
let updated;
|
|
514
|
+
switch (delta.type) {
|
|
515
|
+
case "add":
|
|
516
|
+
updated = [...current, delta.node];
|
|
517
|
+
break;
|
|
518
|
+
case "remove":
|
|
519
|
+
updated = current.filter((n) => n.id !== delta.nodeId);
|
|
520
|
+
break;
|
|
521
|
+
case "update":
|
|
522
|
+
updated = current.map((n) => n.id === delta.nodeId ? delta.node : n);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
const schemaId = this.cache.getSchemaId(queryId);
|
|
526
|
+
const options = this.cache.getOptions(queryId);
|
|
527
|
+
if (schemaId && options) {
|
|
528
|
+
updated = this.cache.sortNodes(updated, options);
|
|
529
|
+
this.cache.set(queryId, updated, schemaId, options);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
serializeOptions(options) {
|
|
533
|
+
if (!options) return {};
|
|
534
|
+
return {
|
|
535
|
+
nodeId: options.nodeId,
|
|
536
|
+
where: options.where,
|
|
537
|
+
includeDeleted: options.includeDeleted,
|
|
538
|
+
orderBy: options.orderBy,
|
|
539
|
+
limit: options.limit,
|
|
540
|
+
offset: options.offset
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// ─── Mutations ───────────────────────────────────────────────────────────────
|
|
544
|
+
async create(schema, data, id) {
|
|
545
|
+
if (!this.initialized) {
|
|
546
|
+
throw new Error("WorkerBridge not initialized");
|
|
547
|
+
}
|
|
548
|
+
return this.remote.create(schema._schemaId, data, id);
|
|
549
|
+
}
|
|
550
|
+
async update(nodeId, changes) {
|
|
551
|
+
if (!this.initialized) {
|
|
552
|
+
throw new Error("WorkerBridge not initialized");
|
|
553
|
+
}
|
|
554
|
+
return this.remote.update(nodeId, changes);
|
|
555
|
+
}
|
|
556
|
+
async delete(nodeId) {
|
|
557
|
+
if (!this.initialized) {
|
|
558
|
+
throw new Error("WorkerBridge not initialized");
|
|
559
|
+
}
|
|
560
|
+
await this.remote.delete(nodeId);
|
|
561
|
+
}
|
|
562
|
+
async restore(nodeId) {
|
|
563
|
+
if (!this.initialized) {
|
|
564
|
+
throw new Error("WorkerBridge not initialized");
|
|
565
|
+
}
|
|
566
|
+
return this.remote.restore(nodeId);
|
|
567
|
+
}
|
|
568
|
+
// ─── Documents ────────────────────────────────────────────────────────────────
|
|
569
|
+
/**
|
|
570
|
+
* Acquire a Y.Doc for editing.
|
|
571
|
+
*
|
|
572
|
+
* Implements the split Y.Doc pattern:
|
|
573
|
+
* 1. Worker maintains "source of truth" Y.Doc (handles persistence & network sync)
|
|
574
|
+
* 2. Main thread gets a mirror Y.Doc for TipTap binding
|
|
575
|
+
* 3. Updates flow bidirectionally:
|
|
576
|
+
* - Local edits → worker (for persistence & broadcast)
|
|
577
|
+
* - Remote edits → main thread (for rendering)
|
|
578
|
+
*/
|
|
579
|
+
async acquireDoc(nodeId) {
|
|
580
|
+
if (!this.initialized) {
|
|
581
|
+
throw new Error("WorkerBridge not initialized");
|
|
582
|
+
}
|
|
583
|
+
const existing = this.mirrorDocs.get(nodeId);
|
|
584
|
+
if (existing) {
|
|
585
|
+
return {
|
|
586
|
+
doc: existing.doc,
|
|
587
|
+
awareness: existing.awareness
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const mirrorDoc = new Y.Doc({ guid: nodeId, gc: false });
|
|
591
|
+
const awareness = new Awareness(mirrorDoc);
|
|
592
|
+
let applyingRemote = false;
|
|
593
|
+
const acquired = await this.remote.acquireDoc(
|
|
594
|
+
nodeId,
|
|
595
|
+
proxy((update, _origin) => {
|
|
596
|
+
applyingRemote = true;
|
|
597
|
+
try {
|
|
598
|
+
Y.applyUpdate(mirrorDoc, update, "remote");
|
|
599
|
+
} finally {
|
|
600
|
+
applyingRemote = false;
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
if (acquired.state.length > 0) {
|
|
605
|
+
Y.applyUpdate(mirrorDoc, acquired.state, "initial");
|
|
606
|
+
}
|
|
607
|
+
const updateBatcher = createUpdateBatcher({
|
|
608
|
+
wait: UPDATE_BATCH_WAIT,
|
|
609
|
+
maxWait: UPDATE_BATCH_MAX_WAIT,
|
|
610
|
+
onFlush: (mergedUpdate) => {
|
|
611
|
+
this.remote.applyLocalUpdate(nodeId, mergedUpdate);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
const updateHandler = (update, origin) => {
|
|
615
|
+
if (applyingRemote || origin === "remote" || origin === "initial") {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
updateBatcher.add(update);
|
|
619
|
+
};
|
|
620
|
+
mirrorDoc.on("update", updateHandler);
|
|
621
|
+
const entry = {
|
|
622
|
+
doc: mirrorDoc,
|
|
623
|
+
awareness,
|
|
624
|
+
applyingRemote: false,
|
|
625
|
+
updateBatcher,
|
|
626
|
+
cleanup: () => {
|
|
627
|
+
updateBatcher.flush();
|
|
628
|
+
updateBatcher.cancel();
|
|
629
|
+
mirrorDoc.off("update", updateHandler);
|
|
630
|
+
awareness.destroy();
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
this.mirrorDocs.set(nodeId, entry);
|
|
634
|
+
return {
|
|
635
|
+
doc: mirrorDoc,
|
|
636
|
+
awareness
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Release a Y.Doc when no longer editing.
|
|
641
|
+
* The worker continues syncing in the background.
|
|
642
|
+
*/
|
|
643
|
+
releaseDoc(nodeId) {
|
|
644
|
+
const entry = this.mirrorDocs.get(nodeId);
|
|
645
|
+
if (!entry) return;
|
|
646
|
+
entry.cleanup();
|
|
647
|
+
entry.doc.destroy();
|
|
648
|
+
this.mirrorDocs.delete(nodeId);
|
|
649
|
+
this.remote.releaseDoc(nodeId);
|
|
650
|
+
}
|
|
651
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
652
|
+
destroy() {
|
|
653
|
+
for (const entry of this.mirrorDocs.values()) {
|
|
654
|
+
entry.cleanup();
|
|
655
|
+
entry.doc.destroy();
|
|
656
|
+
}
|
|
657
|
+
this.mirrorDocs.clear();
|
|
658
|
+
this.remote.destroy().catch(console.error);
|
|
659
|
+
this.worker.terminate();
|
|
660
|
+
this.cache.clear();
|
|
661
|
+
this.subscriptions.clear();
|
|
662
|
+
this.statusListeners.clear();
|
|
663
|
+
this.initialized = false;
|
|
664
|
+
this._status = "disconnected";
|
|
665
|
+
}
|
|
666
|
+
// ─── Status ──────────────────────────────────────────────────────────────────
|
|
667
|
+
get status() {
|
|
668
|
+
return this._status;
|
|
669
|
+
}
|
|
670
|
+
on(event, handler) {
|
|
671
|
+
if (event === "status") {
|
|
672
|
+
this.statusListeners.add(handler);
|
|
673
|
+
return () => {
|
|
674
|
+
this.statusListeners.delete(handler);
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
return () => {
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
function createWorkerBridge(workerUrl) {
|
|
682
|
+
return new WorkerBridge(workerUrl);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/native-bridge.ts
|
|
686
|
+
var NativeBridge = class {
|
|
687
|
+
store;
|
|
688
|
+
cache;
|
|
689
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
690
|
+
storeUnsubscribe = null;
|
|
691
|
+
storageAdapter;
|
|
692
|
+
_status = "disconnected";
|
|
693
|
+
destroyed = false;
|
|
694
|
+
constructor(config) {
|
|
695
|
+
this.store = config.store;
|
|
696
|
+
this.storageAdapter = config.storageAdapter;
|
|
697
|
+
this.cache = new QueryCache();
|
|
698
|
+
this.storeUnsubscribe = this.store.subscribe((event) => {
|
|
699
|
+
this.handleStoreChange(event);
|
|
700
|
+
});
|
|
701
|
+
this._status = "connected";
|
|
702
|
+
}
|
|
703
|
+
// ─── Queries ────────────────────────────────────────────
|
|
704
|
+
query(schema, options) {
|
|
705
|
+
const schemaId = schema._schemaId;
|
|
706
|
+
const queryId = this.cache.computeQueryId(schemaId, options);
|
|
707
|
+
this.cache.initEntry(queryId, schemaId, options ?? {});
|
|
708
|
+
if (!this.cache.has(queryId) || this.cache.get(queryId) === null) {
|
|
709
|
+
this.loadQuery(queryId, schemaId, options);
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
getSnapshot: () => this.cache.get(queryId),
|
|
713
|
+
subscribe: (callback) => this.cache.subscribe(queryId, callback)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Load query data from the store and update cache.
|
|
718
|
+
*/
|
|
719
|
+
async loadQuery(queryId, schemaId, options) {
|
|
720
|
+
if (this.destroyed) return;
|
|
721
|
+
try {
|
|
722
|
+
let nodes;
|
|
723
|
+
if (options?.nodeId) {
|
|
724
|
+
const node = await this.store.get(options.nodeId);
|
|
725
|
+
nodes = node && node.schemaId === schemaId && !node.deleted ? [node] : [];
|
|
726
|
+
} else {
|
|
727
|
+
nodes = await this.store.list({
|
|
728
|
+
schemaId,
|
|
729
|
+
includeDeleted: options?.includeDeleted,
|
|
730
|
+
limit: options?.limit,
|
|
731
|
+
offset: options?.offset
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
nodes = this.cache.filterNodes(nodes, options);
|
|
735
|
+
nodes = this.cache.sortNodes(nodes, options);
|
|
736
|
+
if (!this.destroyed) {
|
|
737
|
+
this.cache.set(queryId, nodes, schemaId, options ?? {});
|
|
738
|
+
}
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.error("[NativeBridge] Failed to load query:", err);
|
|
741
|
+
if (!this.destroyed) {
|
|
742
|
+
this.cache.set(queryId, [], schemaId, options ?? {});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Handle store changes and invalidate affected caches.
|
|
748
|
+
*/
|
|
749
|
+
handleStoreChange(event) {
|
|
750
|
+
if (this.destroyed) return;
|
|
751
|
+
const { node, change } = event;
|
|
752
|
+
const schemaId = node?.schemaId ?? change.payload.schemaId;
|
|
753
|
+
if (!schemaId) return;
|
|
754
|
+
const affectedQueries = this.cache.getQueriesForSchema(schemaId);
|
|
755
|
+
for (const queryId of affectedQueries) {
|
|
756
|
+
const options = this.cache.getOptions(queryId);
|
|
757
|
+
this.loadQuery(queryId, schemaId, options);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ─── Mutations ──────────────────────────────────────────
|
|
761
|
+
async create(schema, data, id) {
|
|
762
|
+
if (this.destroyed) {
|
|
763
|
+
throw new Error("NativeBridge has been destroyed");
|
|
764
|
+
}
|
|
765
|
+
return this.store.create({
|
|
766
|
+
id,
|
|
767
|
+
schemaId: schema._schemaId,
|
|
768
|
+
properties: data
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
async update(nodeId, changes) {
|
|
772
|
+
if (this.destroyed) {
|
|
773
|
+
throw new Error("NativeBridge has been destroyed");
|
|
774
|
+
}
|
|
775
|
+
return this.store.update(nodeId, { properties: changes });
|
|
776
|
+
}
|
|
777
|
+
async delete(nodeId) {
|
|
778
|
+
if (this.destroyed) {
|
|
779
|
+
throw new Error("NativeBridge has been destroyed");
|
|
780
|
+
}
|
|
781
|
+
await this.store.delete(nodeId);
|
|
782
|
+
}
|
|
783
|
+
async restore(nodeId) {
|
|
784
|
+
if (this.destroyed) {
|
|
785
|
+
throw new Error("NativeBridge has been destroyed");
|
|
786
|
+
}
|
|
787
|
+
return this.store.restore(nodeId);
|
|
788
|
+
}
|
|
789
|
+
// ─── Documents ─────────────────────────────────────────
|
|
790
|
+
/**
|
|
791
|
+
* Acquire a Y.Doc for editing.
|
|
792
|
+
*
|
|
793
|
+
* Note: Y.Doc management in React Native is limited compared to web.
|
|
794
|
+
* For now, this throws an error. Future versions will support Y.Doc
|
|
795
|
+
* via native WebSocket sync or JSI bindings.
|
|
796
|
+
*/
|
|
797
|
+
async acquireDoc(_nodeId) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
"Y.Doc editing is not yet supported in NativeBridge. Use a WebView-based editor or wait for Turbo Module support."
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Release a Y.Doc when no longer editing.
|
|
804
|
+
*/
|
|
805
|
+
releaseDoc(_nodeId) {
|
|
806
|
+
}
|
|
807
|
+
// ─── Lifecycle ──────────────────────────────────────────
|
|
808
|
+
/**
|
|
809
|
+
* Initialize the bridge.
|
|
810
|
+
* Opens storage adapter if provided.
|
|
811
|
+
*/
|
|
812
|
+
async initialize(_config) {
|
|
813
|
+
if (this.storageAdapter) {
|
|
814
|
+
await this.storageAdapter.open();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
destroy() {
|
|
818
|
+
this.destroyed = true;
|
|
819
|
+
if (this.storeUnsubscribe) {
|
|
820
|
+
this.storeUnsubscribe();
|
|
821
|
+
this.storeUnsubscribe = null;
|
|
822
|
+
}
|
|
823
|
+
if (this.storageAdapter) {
|
|
824
|
+
this.storageAdapter.close().catch((err) => {
|
|
825
|
+
console.error("[NativeBridge] Failed to close storage:", err);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
this.cache.clear();
|
|
829
|
+
this.statusListeners.clear();
|
|
830
|
+
this._status = "disconnected";
|
|
831
|
+
}
|
|
832
|
+
// ─── Status ─────────────────────────────────────────────
|
|
833
|
+
get status() {
|
|
834
|
+
return this._status;
|
|
835
|
+
}
|
|
836
|
+
on(event, handler) {
|
|
837
|
+
if (event === "status") {
|
|
838
|
+
this.statusListeners.add(handler);
|
|
839
|
+
return () => {
|
|
840
|
+
this.statusListeners.delete(handler);
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return () => {
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
setStatus(status) {
|
|
847
|
+
if (this._status !== status) {
|
|
848
|
+
this._status = status;
|
|
849
|
+
for (const listener of this.statusListeners) {
|
|
850
|
+
listener(status);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// ─── Direct Store Access (compatibility) ────────────────
|
|
855
|
+
get nodeStore() {
|
|
856
|
+
return this.store;
|
|
857
|
+
}
|
|
858
|
+
subscribeToChanges(listener) {
|
|
859
|
+
return this.store.subscribe(listener);
|
|
860
|
+
}
|
|
861
|
+
async get(nodeId) {
|
|
862
|
+
return this.store.get(nodeId);
|
|
863
|
+
}
|
|
864
|
+
async list(options) {
|
|
865
|
+
return this.store.list(options);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
function createNativeBridge(config) {
|
|
869
|
+
return new NativeBridge(config);
|
|
870
|
+
}
|
|
871
|
+
function isReactNative() {
|
|
872
|
+
return typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
|
873
|
+
}
|
|
874
|
+
function isExpo() {
|
|
875
|
+
return typeof global !== "undefined" && global.expo !== void 0;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/create-bridge.ts
|
|
879
|
+
function isWorkerSupported() {
|
|
880
|
+
return typeof Worker !== "undefined";
|
|
881
|
+
}
|
|
882
|
+
function isNodeEnvironment() {
|
|
883
|
+
return typeof process !== "undefined" && process.versions?.node !== void 0;
|
|
884
|
+
}
|
|
885
|
+
async function createDataBridge(options) {
|
|
886
|
+
const { nodeStore, config, workerUrl, mode = "auto" } = options;
|
|
887
|
+
const useWorker = mode === "worker" || mode === "auto" && workerUrl && isWorkerSupported() && !isNodeEnvironment();
|
|
888
|
+
if (useWorker) {
|
|
889
|
+
if (!workerUrl) {
|
|
890
|
+
throw new Error('workerUrl is required when mode is "worker"');
|
|
891
|
+
}
|
|
892
|
+
if (!isWorkerSupported()) {
|
|
893
|
+
throw new Error("Web Workers are not supported in this environment");
|
|
894
|
+
}
|
|
895
|
+
const bridge = new WorkerBridge(workerUrl);
|
|
896
|
+
await bridge.initialize(config);
|
|
897
|
+
return bridge;
|
|
898
|
+
}
|
|
899
|
+
return new MainThreadBridge(nodeStore);
|
|
900
|
+
}
|
|
901
|
+
function createMainThreadBridgeSync(nodeStore) {
|
|
902
|
+
return new MainThreadBridge(nodeStore);
|
|
903
|
+
}
|
|
904
|
+
function createWorkerBridgeSync(workerUrl) {
|
|
905
|
+
return new WorkerBridge(workerUrl);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/utils/binary-state.ts
|
|
909
|
+
var TAG_NULL = 0;
|
|
910
|
+
var TAG_UNDEFINED = 1;
|
|
911
|
+
var TAG_BOOLEAN_FALSE = 2;
|
|
912
|
+
var TAG_BOOLEAN_TRUE = 3;
|
|
913
|
+
var TAG_NUMBER = 4;
|
|
914
|
+
var TAG_STRING = 5;
|
|
915
|
+
var TAG_UINT8ARRAY = 6;
|
|
916
|
+
var TAG_ARRAY = 7;
|
|
917
|
+
var TAG_OBJECT = 8;
|
|
918
|
+
var TAG_BIGINT = 9;
|
|
919
|
+
var NodeStateEncoder = class {
|
|
920
|
+
chunks = [];
|
|
921
|
+
textEncoder = new TextEncoder();
|
|
922
|
+
/**
|
|
923
|
+
* Encode an array of NodeState objects.
|
|
924
|
+
*/
|
|
925
|
+
encode(states) {
|
|
926
|
+
this.chunks = [];
|
|
927
|
+
this.writeUint32(states.length);
|
|
928
|
+
for (const state of states) {
|
|
929
|
+
this.writeNodeState(state);
|
|
930
|
+
}
|
|
931
|
+
return this.finish();
|
|
932
|
+
}
|
|
933
|
+
writeNodeState(state) {
|
|
934
|
+
this.writeString(state.id);
|
|
935
|
+
this.writeString(state.schemaId);
|
|
936
|
+
this.writeProperties(state.properties);
|
|
937
|
+
this.writeTimestamps(state.timestamps);
|
|
938
|
+
this.writeByte(state.deleted ? 1 : 0);
|
|
939
|
+
if (state.deletedAt) {
|
|
940
|
+
this.writeByte(1);
|
|
941
|
+
this.writeTimestamp(state.deletedAt);
|
|
942
|
+
} else {
|
|
943
|
+
this.writeByte(0);
|
|
944
|
+
}
|
|
945
|
+
this.writeFloat64(state.createdAt);
|
|
946
|
+
this.writeString(state.createdBy);
|
|
947
|
+
this.writeFloat64(state.updatedAt);
|
|
948
|
+
this.writeString(state.updatedBy);
|
|
949
|
+
if (state.documentContent) {
|
|
950
|
+
this.writeByte(1);
|
|
951
|
+
this.writeUint8Array(state.documentContent);
|
|
952
|
+
} else {
|
|
953
|
+
this.writeByte(0);
|
|
954
|
+
}
|
|
955
|
+
if (state._unknown && Object.keys(state._unknown).length > 0) {
|
|
956
|
+
this.writeByte(1);
|
|
957
|
+
this.writeProperties(state._unknown);
|
|
958
|
+
} else {
|
|
959
|
+
this.writeByte(0);
|
|
960
|
+
}
|
|
961
|
+
if (state._schemaVersion) {
|
|
962
|
+
this.writeByte(1);
|
|
963
|
+
this.writeString(state._schemaVersion);
|
|
964
|
+
} else {
|
|
965
|
+
this.writeByte(0);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
writeProperties(props) {
|
|
969
|
+
const keys = Object.keys(props);
|
|
970
|
+
this.writeUint32(keys.length);
|
|
971
|
+
for (const key of keys) {
|
|
972
|
+
this.writeString(key);
|
|
973
|
+
this.writeValue(props[key]);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
writeTimestamps(timestamps) {
|
|
977
|
+
const keys = Object.keys(timestamps);
|
|
978
|
+
this.writeUint32(keys.length);
|
|
979
|
+
for (const key of keys) {
|
|
980
|
+
this.writeString(key);
|
|
981
|
+
this.writeTimestamp(timestamps[key]);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
writeTimestamp(ts) {
|
|
985
|
+
this.writeUint32(ts.lamport.time);
|
|
986
|
+
this.writeString(ts.lamport.author);
|
|
987
|
+
this.writeFloat64(ts.wallTime);
|
|
988
|
+
}
|
|
989
|
+
writeValue(value) {
|
|
990
|
+
if (value === null) {
|
|
991
|
+
this.writeByte(TAG_NULL);
|
|
992
|
+
} else if (value === void 0) {
|
|
993
|
+
this.writeByte(TAG_UNDEFINED);
|
|
994
|
+
} else if (typeof value === "boolean") {
|
|
995
|
+
this.writeByte(value ? TAG_BOOLEAN_TRUE : TAG_BOOLEAN_FALSE);
|
|
996
|
+
} else if (typeof value === "number") {
|
|
997
|
+
this.writeByte(TAG_NUMBER);
|
|
998
|
+
this.writeFloat64(value);
|
|
999
|
+
} else if (typeof value === "string") {
|
|
1000
|
+
this.writeByte(TAG_STRING);
|
|
1001
|
+
this.writeString(value);
|
|
1002
|
+
} else if (value instanceof Uint8Array) {
|
|
1003
|
+
this.writeByte(TAG_UINT8ARRAY);
|
|
1004
|
+
this.writeUint8Array(value);
|
|
1005
|
+
} else if (Array.isArray(value)) {
|
|
1006
|
+
this.writeByte(TAG_ARRAY);
|
|
1007
|
+
this.writeUint32(value.length);
|
|
1008
|
+
for (const item of value) {
|
|
1009
|
+
this.writeValue(item);
|
|
1010
|
+
}
|
|
1011
|
+
} else if (typeof value === "bigint") {
|
|
1012
|
+
this.writeByte(TAG_BIGINT);
|
|
1013
|
+
this.writeString(value.toString());
|
|
1014
|
+
} else if (typeof value === "object") {
|
|
1015
|
+
this.writeByte(TAG_OBJECT);
|
|
1016
|
+
const obj = value;
|
|
1017
|
+
const keys = Object.keys(obj);
|
|
1018
|
+
this.writeUint32(keys.length);
|
|
1019
|
+
for (const key of keys) {
|
|
1020
|
+
this.writeString(key);
|
|
1021
|
+
this.writeValue(obj[key]);
|
|
1022
|
+
}
|
|
1023
|
+
} else {
|
|
1024
|
+
this.writeByte(TAG_STRING);
|
|
1025
|
+
this.writeString(JSON.stringify(value));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
writeByte(value) {
|
|
1029
|
+
this.chunks.push(new Uint8Array([value]));
|
|
1030
|
+
}
|
|
1031
|
+
writeUint32(value) {
|
|
1032
|
+
const buf = new ArrayBuffer(4);
|
|
1033
|
+
new DataView(buf).setUint32(0, value, true);
|
|
1034
|
+
this.chunks.push(new Uint8Array(buf));
|
|
1035
|
+
}
|
|
1036
|
+
writeFloat64(value) {
|
|
1037
|
+
const buf = new ArrayBuffer(8);
|
|
1038
|
+
new DataView(buf).setFloat64(0, value, true);
|
|
1039
|
+
this.chunks.push(new Uint8Array(buf));
|
|
1040
|
+
}
|
|
1041
|
+
writeString(value) {
|
|
1042
|
+
const bytes = this.textEncoder.encode(value);
|
|
1043
|
+
this.writeUint32(bytes.length);
|
|
1044
|
+
this.chunks.push(bytes);
|
|
1045
|
+
}
|
|
1046
|
+
writeUint8Array(value) {
|
|
1047
|
+
this.writeUint32(value.length);
|
|
1048
|
+
this.chunks.push(value);
|
|
1049
|
+
}
|
|
1050
|
+
finish() {
|
|
1051
|
+
let totalSize = 0;
|
|
1052
|
+
for (const chunk of this.chunks) {
|
|
1053
|
+
totalSize += chunk.length;
|
|
1054
|
+
}
|
|
1055
|
+
const result = new Uint8Array(totalSize);
|
|
1056
|
+
let offset = 0;
|
|
1057
|
+
for (const chunk of this.chunks) {
|
|
1058
|
+
result.set(chunk, offset);
|
|
1059
|
+
offset += chunk.length;
|
|
1060
|
+
}
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
var NodeStateDecoder = class {
|
|
1065
|
+
data;
|
|
1066
|
+
view;
|
|
1067
|
+
offset = 0;
|
|
1068
|
+
textDecoder = new TextDecoder();
|
|
1069
|
+
constructor(data) {
|
|
1070
|
+
this.data = data;
|
|
1071
|
+
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Decode a Uint8Array back to an array of NodeState objects.
|
|
1075
|
+
*/
|
|
1076
|
+
decode() {
|
|
1077
|
+
const count = this.readUint32();
|
|
1078
|
+
const states = [];
|
|
1079
|
+
for (let i = 0; i < count; i++) {
|
|
1080
|
+
states.push(this.readNodeState());
|
|
1081
|
+
}
|
|
1082
|
+
return states;
|
|
1083
|
+
}
|
|
1084
|
+
readNodeState() {
|
|
1085
|
+
const id = this.readString();
|
|
1086
|
+
const schemaId = this.readString();
|
|
1087
|
+
const properties = this.readProperties();
|
|
1088
|
+
const timestamps = this.readTimestamps();
|
|
1089
|
+
const deleted = this.readByte() === 1;
|
|
1090
|
+
const hasDeletedAt = this.readByte() === 1;
|
|
1091
|
+
const deletedAt = hasDeletedAt ? this.readTimestamp() : void 0;
|
|
1092
|
+
const createdAt = this.readFloat64();
|
|
1093
|
+
const createdBy = this.readString();
|
|
1094
|
+
const updatedAt = this.readFloat64();
|
|
1095
|
+
const updatedBy = this.readString();
|
|
1096
|
+
const hasDocumentContent = this.readByte() === 1;
|
|
1097
|
+
const documentContent = hasDocumentContent ? this.readUint8Array() : void 0;
|
|
1098
|
+
const hasUnknown = this.readByte() === 1;
|
|
1099
|
+
const _unknown = hasUnknown ? this.readProperties() : void 0;
|
|
1100
|
+
const hasSchemaVersion = this.readByte() === 1;
|
|
1101
|
+
const _schemaVersion = hasSchemaVersion ? this.readString() : void 0;
|
|
1102
|
+
const state = {
|
|
1103
|
+
id,
|
|
1104
|
+
schemaId,
|
|
1105
|
+
properties,
|
|
1106
|
+
timestamps,
|
|
1107
|
+
deleted,
|
|
1108
|
+
createdAt,
|
|
1109
|
+
createdBy,
|
|
1110
|
+
updatedAt,
|
|
1111
|
+
updatedBy
|
|
1112
|
+
};
|
|
1113
|
+
if (deletedAt) state.deletedAt = deletedAt;
|
|
1114
|
+
if (documentContent) state.documentContent = documentContent;
|
|
1115
|
+
if (_unknown) state._unknown = _unknown;
|
|
1116
|
+
if (_schemaVersion) state._schemaVersion = _schemaVersion;
|
|
1117
|
+
return state;
|
|
1118
|
+
}
|
|
1119
|
+
readProperties() {
|
|
1120
|
+
const count = this.readUint32();
|
|
1121
|
+
const props = {};
|
|
1122
|
+
for (let i = 0; i < count; i++) {
|
|
1123
|
+
const key = this.readString();
|
|
1124
|
+
const value = this.readValue();
|
|
1125
|
+
props[key] = value;
|
|
1126
|
+
}
|
|
1127
|
+
return props;
|
|
1128
|
+
}
|
|
1129
|
+
readTimestamps() {
|
|
1130
|
+
const count = this.readUint32();
|
|
1131
|
+
const timestamps = {};
|
|
1132
|
+
for (let i = 0; i < count; i++) {
|
|
1133
|
+
const key = this.readString();
|
|
1134
|
+
timestamps[key] = this.readTimestamp();
|
|
1135
|
+
}
|
|
1136
|
+
return timestamps;
|
|
1137
|
+
}
|
|
1138
|
+
readTimestamp() {
|
|
1139
|
+
const time = this.readUint32();
|
|
1140
|
+
const author = this.readString();
|
|
1141
|
+
const wallTime = this.readFloat64();
|
|
1142
|
+
return {
|
|
1143
|
+
lamport: { time, author },
|
|
1144
|
+
wallTime
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
readValue() {
|
|
1148
|
+
const tag = this.readByte();
|
|
1149
|
+
switch (tag) {
|
|
1150
|
+
case TAG_NULL:
|
|
1151
|
+
return null;
|
|
1152
|
+
case TAG_UNDEFINED:
|
|
1153
|
+
return void 0;
|
|
1154
|
+
case TAG_BOOLEAN_FALSE:
|
|
1155
|
+
return false;
|
|
1156
|
+
case TAG_BOOLEAN_TRUE:
|
|
1157
|
+
return true;
|
|
1158
|
+
case TAG_NUMBER:
|
|
1159
|
+
return this.readFloat64();
|
|
1160
|
+
case TAG_STRING:
|
|
1161
|
+
return this.readString();
|
|
1162
|
+
case TAG_UINT8ARRAY:
|
|
1163
|
+
return this.readUint8Array();
|
|
1164
|
+
case TAG_ARRAY: {
|
|
1165
|
+
const length = this.readUint32();
|
|
1166
|
+
const arr = [];
|
|
1167
|
+
for (let i = 0; i < length; i++) {
|
|
1168
|
+
arr.push(this.readValue());
|
|
1169
|
+
}
|
|
1170
|
+
return arr;
|
|
1171
|
+
}
|
|
1172
|
+
case TAG_BIGINT:
|
|
1173
|
+
return BigInt(this.readString());
|
|
1174
|
+
case TAG_OBJECT: {
|
|
1175
|
+
const length = this.readUint32();
|
|
1176
|
+
const obj = {};
|
|
1177
|
+
for (let i = 0; i < length; i++) {
|
|
1178
|
+
const key = this.readString();
|
|
1179
|
+
obj[key] = this.readValue();
|
|
1180
|
+
}
|
|
1181
|
+
return obj;
|
|
1182
|
+
}
|
|
1183
|
+
default:
|
|
1184
|
+
throw new Error(`Unknown tag: ${tag}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
readByte() {
|
|
1188
|
+
return this.data[this.offset++];
|
|
1189
|
+
}
|
|
1190
|
+
readUint32() {
|
|
1191
|
+
const value = this.view.getUint32(this.offset, true);
|
|
1192
|
+
this.offset += 4;
|
|
1193
|
+
return value;
|
|
1194
|
+
}
|
|
1195
|
+
readFloat64() {
|
|
1196
|
+
const value = this.view.getFloat64(this.offset, true);
|
|
1197
|
+
this.offset += 8;
|
|
1198
|
+
return value;
|
|
1199
|
+
}
|
|
1200
|
+
readString() {
|
|
1201
|
+
const length = this.readUint32();
|
|
1202
|
+
const bytes = this.data.subarray(this.offset, this.offset + length);
|
|
1203
|
+
this.offset += length;
|
|
1204
|
+
return this.textDecoder.decode(bytes);
|
|
1205
|
+
}
|
|
1206
|
+
readUint8Array() {
|
|
1207
|
+
const length = this.readUint32();
|
|
1208
|
+
const bytes = this.data.slice(this.offset, this.offset + length);
|
|
1209
|
+
this.offset += length;
|
|
1210
|
+
return bytes;
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
function encodeNodeStates(states) {
|
|
1214
|
+
return new NodeStateEncoder().encode(states);
|
|
1215
|
+
}
|
|
1216
|
+
function decodeNodeStates(data) {
|
|
1217
|
+
return new NodeStateDecoder(data).decode();
|
|
1218
|
+
}
|
|
1219
|
+
function shouldUseBinaryEncoding(states) {
|
|
1220
|
+
if (states.length > 100) return true;
|
|
1221
|
+
return states.some((s) => s.documentContent && s.documentContent.length > 1e3);
|
|
1222
|
+
}
|
|
1223
|
+
export {
|
|
1224
|
+
MainThreadBridge,
|
|
1225
|
+
NativeBridge,
|
|
1226
|
+
NodeStateDecoder,
|
|
1227
|
+
NodeStateEncoder,
|
|
1228
|
+
QueryCache,
|
|
1229
|
+
WorkerBridge,
|
|
1230
|
+
createDataBridge,
|
|
1231
|
+
createDeltaBatcher,
|
|
1232
|
+
createMainThreadBridge,
|
|
1233
|
+
createMainThreadBridgeSync,
|
|
1234
|
+
createNativeBridge,
|
|
1235
|
+
createUpdateBatcher,
|
|
1236
|
+
createWorkerBridge,
|
|
1237
|
+
createWorkerBridgeSync,
|
|
1238
|
+
debounce,
|
|
1239
|
+
decodeNodeStates,
|
|
1240
|
+
encodeNodeStates,
|
|
1241
|
+
isExpo,
|
|
1242
|
+
isNodeEnvironment,
|
|
1243
|
+
isReactNative,
|
|
1244
|
+
isWorkerSupported,
|
|
1245
|
+
shouldUseBinaryEncoding
|
|
1246
|
+
};
|