@xnetjs/data-bridge 0.0.2 → 0.0.3
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 +5 -0
- package/dist/chunk-25WNZV7W.js +831 -0
- package/dist/chunk-5GTIP33X.js +201 -0
- package/dist/chunk-EPNW4GGU.js +460 -0
- package/dist/index.d.ts +259 -449
- package/dist/index.js +1335 -575
- package/dist/native-bridge.d.ts +126 -0
- package/dist/native-bridge.js +13 -0
- package/dist/query-descriptor-D0k2gUQ0.d.ts +298 -0
- package/dist/types-BRvuTwEn.d.ts +547 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +0 -0
- package/dist/worker/data-worker.d.ts +161 -1
- package/dist/worker/data-worker.js +468 -144
- package/package.json +16 -6
- package/dist/chunk-X6F5CPJI.js +0 -386
package/dist/index.js
CHANGED
|
@@ -1,20 +1,437 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
NativeBridge,
|
|
3
|
+
QueryCache,
|
|
4
|
+
createNativeBridge,
|
|
5
|
+
createQueryErrorMetadata,
|
|
6
|
+
createQueryMetadata,
|
|
7
|
+
createQuerySnapshotMetadata,
|
|
8
|
+
isExpo,
|
|
9
|
+
isReactNative
|
|
10
|
+
} from "./chunk-25WNZV7W.js";
|
|
11
|
+
import {
|
|
12
|
+
NodeStateDecoder,
|
|
13
|
+
NodeStateEncoder,
|
|
14
|
+
PortSQLiteAdapter,
|
|
15
|
+
decodeNodeStates,
|
|
16
|
+
decodeWorkerQuerySnapshot,
|
|
17
|
+
encodeNodeStates,
|
|
18
|
+
encodeWorkerQuerySnapshot,
|
|
19
|
+
groupNodeChangeEventsBySchema,
|
|
20
|
+
shouldUseBinaryEncoding
|
|
21
|
+
} from "./chunk-EPNW4GGU.js";
|
|
22
|
+
import {
|
|
23
|
+
BOUNDED_QUERY_OVERFETCH,
|
|
24
|
+
applyNodeChangeToBoundedQueryResult,
|
|
25
|
+
applyNodeChangeToQueryResult,
|
|
26
|
+
applyQueryDescriptor,
|
|
27
|
+
createBoundedWorkingSet,
|
|
28
|
+
createBoundedWorkingSetDescriptor,
|
|
29
|
+
createQueryDescriptor,
|
|
30
|
+
decodeQueryCursor,
|
|
31
|
+
encodeQueryCursor,
|
|
32
|
+
filterQueryNodes,
|
|
33
|
+
matchesQueryDescriptor,
|
|
34
|
+
queryDescriptorNeedsBoundedReload,
|
|
35
|
+
queryDescriptorSupportsBoundedDelta,
|
|
36
|
+
queryDescriptorToOptions,
|
|
37
|
+
reuseEquivalentNodeReferences,
|
|
38
|
+
serializeQueryDescriptor,
|
|
39
|
+
sortQueryNodes
|
|
40
|
+
} from "./chunk-5GTIP33X.js";
|
|
41
|
+
|
|
42
|
+
// src/query-stream.ts
|
|
43
|
+
function createQueryStreamState(input) {
|
|
44
|
+
return {
|
|
45
|
+
data: input?.data ?? null,
|
|
46
|
+
metadata: input?.metadata ?? null,
|
|
47
|
+
progress: input?.progress ?? null,
|
|
48
|
+
error: input?.error ?? null,
|
|
49
|
+
status: input?.status ?? "idle"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function getNextMetadata(event, currentMetadata) {
|
|
53
|
+
return "metadata" in event && event.metadata !== void 0 ? event.metadata : currentMetadata;
|
|
54
|
+
}
|
|
55
|
+
function insertNode(nodes, node, index) {
|
|
56
|
+
const withoutExisting = nodes.filter((item) => item.id !== node.id);
|
|
57
|
+
if (index === void 0) return [...withoutExisting, node];
|
|
58
|
+
const boundedIndex = Math.max(0, Math.min(index, withoutExisting.length));
|
|
59
|
+
return [...withoutExisting.slice(0, boundedIndex), node, ...withoutExisting.slice(boundedIndex)];
|
|
60
|
+
}
|
|
61
|
+
function updateNode(nodes, nodeId, node) {
|
|
62
|
+
return nodes.map((item) => item.id === nodeId ? node : item);
|
|
63
|
+
}
|
|
64
|
+
function deleteNode(nodes, nodeId) {
|
|
65
|
+
return nodes.filter((item) => item.id !== nodeId);
|
|
66
|
+
}
|
|
67
|
+
function reduceQueryStreamEvent(state, event) {
|
|
68
|
+
const metadata = getNextMetadata(event, state.metadata);
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case "snapshot":
|
|
71
|
+
return {
|
|
72
|
+
data: event.nodes,
|
|
73
|
+
metadata,
|
|
74
|
+
progress: state.progress,
|
|
75
|
+
error: null,
|
|
76
|
+
status: "ready"
|
|
77
|
+
};
|
|
78
|
+
case "insert":
|
|
79
|
+
return {
|
|
80
|
+
data: insertNode(state.data ?? [], event.node, event.index),
|
|
81
|
+
metadata,
|
|
82
|
+
progress: state.progress,
|
|
83
|
+
error: null,
|
|
84
|
+
status: "ready"
|
|
85
|
+
};
|
|
86
|
+
case "update":
|
|
87
|
+
return {
|
|
88
|
+
data: updateNode(state.data ?? [], event.nodeId, event.node),
|
|
89
|
+
metadata,
|
|
90
|
+
progress: state.progress,
|
|
91
|
+
error: null,
|
|
92
|
+
status: "ready"
|
|
93
|
+
};
|
|
94
|
+
case "delete":
|
|
95
|
+
return {
|
|
96
|
+
data: deleteNode(state.data ?? [], event.nodeId),
|
|
97
|
+
metadata,
|
|
98
|
+
progress: state.progress,
|
|
99
|
+
error: null,
|
|
100
|
+
status: "ready"
|
|
101
|
+
};
|
|
102
|
+
case "reset":
|
|
103
|
+
return {
|
|
104
|
+
data: event.nodes ?? null,
|
|
105
|
+
metadata,
|
|
106
|
+
progress: state.progress,
|
|
107
|
+
error: null,
|
|
108
|
+
status: event.nodes ? "ready" : "loading"
|
|
109
|
+
};
|
|
110
|
+
case "progress":
|
|
111
|
+
return {
|
|
112
|
+
data: state.data,
|
|
113
|
+
metadata,
|
|
114
|
+
progress: event.progress,
|
|
115
|
+
error: state.error,
|
|
116
|
+
status: state.data === null ? "loading" : state.status
|
|
117
|
+
};
|
|
118
|
+
case "error":
|
|
119
|
+
return {
|
|
120
|
+
data: state.data,
|
|
121
|
+
metadata,
|
|
122
|
+
progress: state.progress,
|
|
123
|
+
error: event.error,
|
|
124
|
+
status: event.recoverable ? state.status : "error"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function reduceQueryStreamEvents(state, events) {
|
|
129
|
+
return events.reduce(reduceQueryStreamEvent, state);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/remote-query-protocol.ts
|
|
133
|
+
var REMOTE_NODE_QUERY_PROTOCOL = "xnet.node-query";
|
|
134
|
+
var REMOTE_NODE_QUERY_PROTOCOL_VERSION = 1;
|
|
135
|
+
function isRemoteNodeQuerySource(source) {
|
|
136
|
+
return source === "hub" || source === "federated";
|
|
137
|
+
}
|
|
138
|
+
function createRemoteNodeQueryRequest(input) {
|
|
139
|
+
return {
|
|
140
|
+
protocol: REMOTE_NODE_QUERY_PROTOCOL,
|
|
141
|
+
version: REMOTE_NODE_QUERY_PROTOCOL_VERSION,
|
|
142
|
+
requestId: input.requestId,
|
|
143
|
+
descriptor: input.descriptor,
|
|
144
|
+
mode: input.mode ?? "remote",
|
|
145
|
+
source: input.source,
|
|
146
|
+
requestedAt: input.requestedAt ?? Date.now(),
|
|
147
|
+
...input.auth ? { auth: input.auth } : {},
|
|
148
|
+
...input.client ? { client: input.client } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function isRemoteNodeQuerySuccess(response) {
|
|
152
|
+
return response.type === "node-query/result";
|
|
153
|
+
}
|
|
154
|
+
function isRemoteNodeQueryError(response) {
|
|
155
|
+
return response.type === "node-query/error";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/remote-query-execution.ts
|
|
159
|
+
var REMOTE_QUERY_MODES = /* @__PURE__ */ new Set([
|
|
160
|
+
"local-then-remote",
|
|
161
|
+
"remote",
|
|
162
|
+
"live",
|
|
163
|
+
"stream"
|
|
164
|
+
]);
|
|
165
|
+
var DEFAULT_NODE_QUERY_ROUTER_THRESHOLDS = {
|
|
166
|
+
localRowThreshold: 1e4,
|
|
167
|
+
hybridRowThreshold: 1e5,
|
|
168
|
+
searchToRemote: true,
|
|
169
|
+
spatialToRemote: true
|
|
170
|
+
};
|
|
171
|
+
function getRemoteQueryMode(descriptor) {
|
|
172
|
+
return descriptor.mode && REMOTE_QUERY_MODES.has(descriptor.mode) ? descriptor.mode : null;
|
|
173
|
+
}
|
|
174
|
+
function getRemoteQuerySource(descriptor) {
|
|
175
|
+
return isRemoteNodeQuerySource(descriptor.source) ? descriptor.source : "hub";
|
|
176
|
+
}
|
|
177
|
+
function shouldRunRemoteQuery(descriptor) {
|
|
178
|
+
return descriptor.source !== "local" && getRemoteQueryMode(descriptor) !== null;
|
|
179
|
+
}
|
|
180
|
+
function shouldUseRemoteOnlyQuery(descriptor) {
|
|
181
|
+
return getRemoteQueryMode(descriptor) === "remote";
|
|
182
|
+
}
|
|
183
|
+
function normalizeNodeQueryRouterThresholds(thresholds) {
|
|
184
|
+
return {
|
|
185
|
+
...DEFAULT_NODE_QUERY_ROUTER_THRESHOLDS,
|
|
186
|
+
...thresholds
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function routeRemoteNodeQuery(input) {
|
|
190
|
+
const { descriptor, localRowCount, hasRemoteClient } = input;
|
|
191
|
+
const thresholds = normalizeNodeQueryRouterThresholds(input.thresholds);
|
|
192
|
+
const mode = getRemoteQueryMode(descriptor);
|
|
193
|
+
const remoteSource = getRemoteQuerySource(descriptor);
|
|
194
|
+
if (descriptor.source === "local" || descriptor.mode === "local") {
|
|
195
|
+
return {
|
|
196
|
+
shouldRunRemote: false,
|
|
197
|
+
source: "local",
|
|
198
|
+
reason: "local-source",
|
|
199
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
200
|
+
thresholds
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (mode === "remote") {
|
|
204
|
+
return {
|
|
205
|
+
shouldRunRemote: true,
|
|
206
|
+
mode,
|
|
207
|
+
source: remoteSource,
|
|
208
|
+
reason: "explicit-remote-mode",
|
|
209
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
210
|
+
thresholds
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (mode === "live" || mode === "stream") {
|
|
214
|
+
return {
|
|
215
|
+
shouldRunRemote: false,
|
|
216
|
+
source: "local",
|
|
217
|
+
reason: "stream-lifecycle",
|
|
218
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
219
|
+
thresholds
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (descriptor.source !== "auto") {
|
|
223
|
+
return mode === "local-then-remote" ? {
|
|
224
|
+
shouldRunRemote: true,
|
|
225
|
+
mode,
|
|
226
|
+
source: remoteSource,
|
|
227
|
+
reason: "explicit-progressive-mode",
|
|
228
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
229
|
+
thresholds
|
|
230
|
+
} : {
|
|
231
|
+
shouldRunRemote: false,
|
|
232
|
+
source: "local",
|
|
233
|
+
reason: "local-by-default",
|
|
234
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
235
|
+
thresholds
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (!hasRemoteClient) {
|
|
239
|
+
return {
|
|
240
|
+
shouldRunRemote: false,
|
|
241
|
+
source: "local",
|
|
242
|
+
reason: "auto-no-remote-client",
|
|
243
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
244
|
+
thresholds
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const progressiveMode = mode ?? "local-then-remote";
|
|
248
|
+
if (descriptor.search && thresholds.searchToRemote) {
|
|
249
|
+
return {
|
|
250
|
+
shouldRunRemote: true,
|
|
251
|
+
mode: progressiveMode,
|
|
252
|
+
source: remoteSource,
|
|
253
|
+
reason: "auto-search",
|
|
254
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
255
|
+
thresholds
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (descriptor.spatial && thresholds.spatialToRemote) {
|
|
259
|
+
return {
|
|
260
|
+
shouldRunRemote: true,
|
|
261
|
+
mode: progressiveMode,
|
|
262
|
+
source: remoteSource,
|
|
263
|
+
reason: "auto-spatial",
|
|
264
|
+
...localRowCount !== void 0 ? { localRowCount } : {},
|
|
265
|
+
thresholds
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (localRowCount === void 0) {
|
|
269
|
+
return {
|
|
270
|
+
shouldRunRemote: false,
|
|
271
|
+
source: "local",
|
|
272
|
+
reason: "auto-unknown-row-count",
|
|
273
|
+
thresholds
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (localRowCount >= thresholds.hybridRowThreshold) {
|
|
277
|
+
return {
|
|
278
|
+
shouldRunRemote: true,
|
|
279
|
+
mode: progressiveMode,
|
|
280
|
+
source: remoteSource,
|
|
281
|
+
reason: "auto-large-result",
|
|
282
|
+
localRowCount,
|
|
283
|
+
thresholds
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (localRowCount >= thresholds.localRowThreshold) {
|
|
287
|
+
return {
|
|
288
|
+
shouldRunRemote: true,
|
|
289
|
+
mode: progressiveMode,
|
|
290
|
+
source: remoteSource,
|
|
291
|
+
reason: "auto-medium-result",
|
|
292
|
+
localRowCount,
|
|
293
|
+
thresholds
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
shouldRunRemote: false,
|
|
298
|
+
source: "local",
|
|
299
|
+
reason: "auto-small-result",
|
|
300
|
+
localRowCount,
|
|
301
|
+
thresholds
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function createQueryRoutingMetadata(route) {
|
|
305
|
+
return {
|
|
306
|
+
source: route.shouldRunRemote ? route.source : "local",
|
|
307
|
+
reason: route.reason,
|
|
308
|
+
...route.localRowCount !== void 0 ? { localRowCount: route.localRowCount } : {},
|
|
309
|
+
thresholds: {
|
|
310
|
+
localRowThreshold: route.thresholds.localRowThreshold,
|
|
311
|
+
hybridRowThreshold: route.thresholds.hybridRowThreshold
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function chooseNewestNode(left, right) {
|
|
316
|
+
if (right.updatedAt > left.updatedAt) return right;
|
|
317
|
+
if (right.updatedAt < left.updatedAt) return left;
|
|
318
|
+
return right;
|
|
319
|
+
}
|
|
320
|
+
function mergeRemoteNodeSnapshots(localNodes, remoteNodes) {
|
|
321
|
+
const merged = /* @__PURE__ */ new Map();
|
|
322
|
+
for (const node of remoteNodes) {
|
|
323
|
+
const existing = merged.get(node.id);
|
|
324
|
+
merged.set(node.id, existing ? chooseNewestNode(existing, node) : node);
|
|
325
|
+
}
|
|
326
|
+
for (const node of localNodes) {
|
|
327
|
+
const existing = merged.get(node.id);
|
|
328
|
+
merged.set(node.id, existing ? chooseNewestNode(existing, node) : node);
|
|
329
|
+
}
|
|
330
|
+
return [...merged.values()];
|
|
331
|
+
}
|
|
332
|
+
function isRemoteVerificationFailed(verification) {
|
|
333
|
+
return verification?.status === "failed";
|
|
334
|
+
}
|
|
335
|
+
function filterRemoteNodesByVerification(nodes, verification) {
|
|
336
|
+
if (!verification || verification.status === "verified" || verification.status === "unverified") {
|
|
337
|
+
return [...nodes];
|
|
338
|
+
}
|
|
339
|
+
if (verification.status === "failed") {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
const verifiedNodeIds = verification.verifiedNodeIds ? new Set(verification.verifiedNodeIds) : null;
|
|
343
|
+
const failedNodeIds = new Set(verification.failedNodeIds ?? []);
|
|
344
|
+
return nodes.filter((node) => {
|
|
345
|
+
if (failedNodeIds.has(node.id)) return false;
|
|
346
|
+
return verifiedNodeIds ? verifiedNodeIds.has(node.id) : true;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function createRemoteVerificationError(input) {
|
|
350
|
+
return {
|
|
351
|
+
type: "node-query/error",
|
|
352
|
+
requestId: input.requestId,
|
|
353
|
+
source: input.source,
|
|
354
|
+
code: "VERIFICATION_FAILED",
|
|
355
|
+
message: input.message ?? "Remote query result verification failed"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function createRemoteSuccessMetadata(input) {
|
|
359
|
+
const { response, source, loadedCount } = input;
|
|
360
|
+
const pageInfo = {
|
|
361
|
+
...response.pageInfo,
|
|
362
|
+
loadedCount
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
...response.metadata,
|
|
366
|
+
source,
|
|
367
|
+
updatedAt: Date.now(),
|
|
368
|
+
pageInfo,
|
|
369
|
+
completeness: response.completeness,
|
|
370
|
+
staleness: response.staleness,
|
|
371
|
+
verification: response.verification
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function createRemoteFallbackMetadata(input) {
|
|
375
|
+
const { localMetadata, error } = input;
|
|
376
|
+
const message = error instanceof Error ? error.message : error.message;
|
|
377
|
+
const reason = !(error instanceof Error) && error.code === "TIMEOUT" ? "source-timeout" : !(error instanceof Error) && error.code === "VERIFICATION_FAILED" ? "verification-failed" : "remote-unavailable";
|
|
378
|
+
const verification = !(error instanceof Error) && error.code === "VERIFICATION_FAILED" ? { status: "failed" } : localMetadata.verification ?? { status: "unverified" };
|
|
379
|
+
return {
|
|
380
|
+
...localMetadata,
|
|
381
|
+
source: localMetadata.source === "local" ? "hybrid" : localMetadata.source,
|
|
382
|
+
updatedAt: Date.now(),
|
|
383
|
+
completeness: {
|
|
384
|
+
level: "partial",
|
|
385
|
+
reason
|
|
386
|
+
},
|
|
387
|
+
staleness: localMetadata.staleness ?? {
|
|
388
|
+
level: "stale",
|
|
389
|
+
asOf: localMetadata.updatedAt
|
|
390
|
+
},
|
|
391
|
+
verification,
|
|
392
|
+
error: message
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function withRemoteErrorVerificationMetadata(metadata, error) {
|
|
396
|
+
if (error instanceof Error || error.code !== "VERIFICATION_FAILED") {
|
|
397
|
+
return metadata;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
...metadata,
|
|
401
|
+
verification: {
|
|
402
|
+
status: "failed"
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
4
406
|
|
|
5
407
|
// src/main-thread-bridge.ts
|
|
408
|
+
var BULK_STORE_CHANGE_RELOAD_THRESHOLD = 250;
|
|
6
409
|
var MainThreadBridge = class {
|
|
7
410
|
store;
|
|
8
411
|
cache;
|
|
9
412
|
statusListeners = /* @__PURE__ */ new Set();
|
|
10
413
|
storeUnsubscribe = null;
|
|
414
|
+
storeBatchUnsubscribe = null;
|
|
11
415
|
_syncManager = null;
|
|
12
|
-
|
|
416
|
+
remoteNodeQueryClient;
|
|
417
|
+
remoteNodeQueryRouting;
|
|
418
|
+
remoteLoads = /* @__PURE__ */ new Map();
|
|
419
|
+
remoteStreams = /* @__PURE__ */ new Map();
|
|
420
|
+
remoteInvalidations = null;
|
|
421
|
+
pendingStoreChanges = [];
|
|
422
|
+
storeChangeFlushQueued = false;
|
|
423
|
+
constructor(store, options) {
|
|
13
424
|
this.store = store;
|
|
14
425
|
this.cache = new QueryCache();
|
|
426
|
+
this.remoteNodeQueryClient = options?.remoteNodeQueryClient ?? null;
|
|
427
|
+
this.remoteNodeQueryRouting = options?.remoteNodeQueryRouting;
|
|
15
428
|
this.storeUnsubscribe = this.store.subscribe((event) => {
|
|
16
|
-
this.
|
|
429
|
+
this.enqueueStoreChange(event);
|
|
17
430
|
});
|
|
431
|
+
this.storeBatchUnsubscribe = this.store.subscribeToBatchChanges((event) => {
|
|
432
|
+
this.handleStoreBatchChange(event);
|
|
433
|
+
});
|
|
434
|
+
this.startRemoteInvalidationSubscription();
|
|
18
435
|
}
|
|
19
436
|
/**
|
|
20
437
|
* Set the SyncManager for Y.Doc acquisition.
|
|
@@ -25,53 +442,704 @@ var MainThreadBridge = class {
|
|
|
25
442
|
}
|
|
26
443
|
// ─── Queries ────────────────────────────────────────────
|
|
27
444
|
query(schema, options) {
|
|
28
|
-
const
|
|
29
|
-
const queryId =
|
|
30
|
-
this.cache.initEntry(queryId,
|
|
31
|
-
if (
|
|
32
|
-
this.
|
|
445
|
+
const descriptor = createQueryDescriptor(schema._schemaId, options);
|
|
446
|
+
const queryId = serializeQueryDescriptor(descriptor);
|
|
447
|
+
this.cache.initEntry(queryId, descriptor);
|
|
448
|
+
if (this.cache.get(queryId) === null) {
|
|
449
|
+
void this.loadInitialQuery(queryId, descriptor);
|
|
33
450
|
}
|
|
34
451
|
return {
|
|
35
452
|
getSnapshot: () => this.cache.get(queryId),
|
|
36
|
-
|
|
453
|
+
getMetadata: () => this.cache.getMetadata(queryId),
|
|
454
|
+
subscribe: (callback) => this.subscribeToQuery(queryId, descriptor, callback)
|
|
37
455
|
};
|
|
38
456
|
}
|
|
457
|
+
subscribeToQuery(queryId, descriptor, callback) {
|
|
458
|
+
const unsubscribe = this.cache.subscribe(queryId, callback);
|
|
459
|
+
this.startRemoteQueryStream(queryId, descriptor);
|
|
460
|
+
return () => {
|
|
461
|
+
unsubscribe();
|
|
462
|
+
if (this.cache.getSubscriberCount(queryId) === 0) {
|
|
463
|
+
this.stopRemoteQueryStream(queryId);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async reloadQuery(descriptor) {
|
|
468
|
+
await this.loadInitialQuery(serializeQueryDescriptor(descriptor), descriptor);
|
|
469
|
+
}
|
|
39
470
|
/**
|
|
40
|
-
* Load query data
|
|
471
|
+
* Load query data according to the descriptor's local/remote execution mode.
|
|
41
472
|
*/
|
|
42
|
-
async
|
|
473
|
+
async loadInitialQuery(queryId, descriptor) {
|
|
474
|
+
if (shouldUseRemoteOnlyQuery(descriptor)) {
|
|
475
|
+
await this.loadRemoteQuery(queryId, descriptor);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const result = await this.loadLocalQuery(queryId, descriptor);
|
|
479
|
+
const route = routeRemoteNodeQuery({
|
|
480
|
+
descriptor,
|
|
481
|
+
hasRemoteClient: this.remoteNodeQueryClient !== null,
|
|
482
|
+
localRowCount: this.getQueryRouteRowCount(result),
|
|
483
|
+
thresholds: this.remoteNodeQueryRouting
|
|
484
|
+
});
|
|
485
|
+
if (route.shouldRunRemote) {
|
|
486
|
+
await this.loadRemoteQuery(queryId, descriptor, route);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
getQueryRouteRowCount(result) {
|
|
490
|
+
return result?.totalCount ?? result?.plan.candidateNodeCount ?? result?.nodes.length;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Load query data from the local store and update cache.
|
|
494
|
+
*/
|
|
495
|
+
async loadLocalQuery(queryId, descriptor) {
|
|
43
496
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
497
|
+
const useBoundedWorkingSet = queryDescriptorSupportsBoundedDelta(descriptor);
|
|
498
|
+
const result = await this.store.query(
|
|
499
|
+
useBoundedWorkingSet ? createBoundedWorkingSetDescriptor(descriptor) : descriptor
|
|
500
|
+
);
|
|
501
|
+
this.debugQueryPlan(queryId, result.plan);
|
|
502
|
+
const previousWorkingSet = this.cache.getWorkingSet(queryId);
|
|
503
|
+
const mergedNodes = reuseEquivalentNodeReferences(result.nodes, [
|
|
504
|
+
...previousWorkingSet?.nodes ?? [],
|
|
505
|
+
...this.cache.get(queryId) ?? []
|
|
506
|
+
]);
|
|
507
|
+
const visibleNodes = useBoundedWorkingSet ? mergedNodes.slice(0, descriptor.limit) : mergedNodes;
|
|
508
|
+
const visibleResult = { ...result, nodes: visibleNodes };
|
|
509
|
+
this.cache.set(
|
|
510
|
+
queryId,
|
|
511
|
+
visibleNodes,
|
|
512
|
+
descriptor,
|
|
513
|
+
void 0,
|
|
514
|
+
{
|
|
515
|
+
...createQueryMetadata({ descriptor, result: visibleResult, source: "local" }),
|
|
516
|
+
source: "local"
|
|
517
|
+
},
|
|
518
|
+
useBoundedWorkingSet ? createBoundedWorkingSet(descriptor, mergedNodes) : void 0
|
|
519
|
+
);
|
|
520
|
+
return result;
|
|
521
|
+
} catch (err) {
|
|
522
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
523
|
+
console.error("[MainThreadBridge] Failed to load query:", error);
|
|
524
|
+
this.cache.set(
|
|
525
|
+
queryId,
|
|
526
|
+
[],
|
|
527
|
+
descriptor,
|
|
528
|
+
void 0,
|
|
529
|
+
createQueryErrorMetadata({ descriptor, source: "local", error })
|
|
530
|
+
);
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Load query data from the configured remote query client.
|
|
536
|
+
*/
|
|
537
|
+
async loadRemoteQuery(queryId, descriptor, route) {
|
|
538
|
+
if (!route?.shouldRunRemote && !shouldRunRemoteQuery(descriptor)) return;
|
|
539
|
+
if (!this.remoteNodeQueryClient) {
|
|
540
|
+
this.handleRemoteQueryError(queryId, descriptor, new Error("Remote query client unavailable"));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const existingLoad = this.remoteLoads.get(queryId);
|
|
544
|
+
if (existingLoad) {
|
|
545
|
+
await existingLoad;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const load = this.executeRemoteQuery(queryId, descriptor, route).finally(() => {
|
|
549
|
+
this.remoteLoads.delete(queryId);
|
|
550
|
+
});
|
|
551
|
+
this.remoteLoads.set(queryId, load);
|
|
552
|
+
await load;
|
|
553
|
+
}
|
|
554
|
+
shouldUseRemoteStream(descriptor) {
|
|
555
|
+
const mode = getRemoteQueryMode(descriptor);
|
|
556
|
+
return descriptor.source !== "local" && (mode === "live" || mode === "stream");
|
|
557
|
+
}
|
|
558
|
+
startRemoteQueryStream(queryId, descriptor) {
|
|
559
|
+
if (!this.shouldUseRemoteStream(descriptor)) return;
|
|
560
|
+
if (this.remoteStreams.has(queryId)) return;
|
|
561
|
+
if (!this.remoteNodeQueryClient) {
|
|
562
|
+
this.handleRemoteQueryError(queryId, descriptor, new Error("Remote query client unavailable"));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (!this.remoteNodeQueryClient.stream) {
|
|
566
|
+
void this.loadRemoteQuery(queryId, descriptor);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const localSnapshot = this.cache.get(queryId);
|
|
570
|
+
const runtime = {
|
|
571
|
+
state: createQueryStreamState({
|
|
572
|
+
data: localSnapshot,
|
|
573
|
+
metadata: this.cache.getMetadata(queryId),
|
|
574
|
+
status: localSnapshot === null ? "loading" : "ready"
|
|
575
|
+
}),
|
|
576
|
+
unsubscribe: null,
|
|
577
|
+
cancelled: false,
|
|
578
|
+
start: Promise.resolve()
|
|
579
|
+
};
|
|
580
|
+
const mode = getRemoteQueryMode(descriptor);
|
|
581
|
+
if (!mode) return;
|
|
582
|
+
this.remoteStreams.set(queryId, runtime);
|
|
583
|
+
const request = createRemoteNodeQueryRequest({
|
|
584
|
+
requestId: queryId,
|
|
585
|
+
descriptor,
|
|
586
|
+
mode,
|
|
587
|
+
source: getRemoteQuerySource(descriptor),
|
|
588
|
+
client: {
|
|
589
|
+
knownNodeIds: localSnapshot?.map((node) => node.id) ?? []
|
|
55
590
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
591
|
+
});
|
|
592
|
+
runtime.start = Promise.resolve(
|
|
593
|
+
this.remoteNodeQueryClient.stream(request, {
|
|
594
|
+
next: (event) => {
|
|
595
|
+
if (this.remoteStreams.get(queryId) !== runtime || runtime.cancelled) return;
|
|
596
|
+
this.applyRemoteStreamEvent(queryId, descriptor, event);
|
|
597
|
+
},
|
|
598
|
+
error: (error) => {
|
|
599
|
+
if (this.remoteStreams.get(queryId) !== runtime || runtime.cancelled) return;
|
|
600
|
+
this.applyRemoteStreamEvent(queryId, descriptor, {
|
|
601
|
+
type: "error",
|
|
602
|
+
error: error.message
|
|
603
|
+
});
|
|
604
|
+
this.stopRemoteQueryStream(queryId);
|
|
605
|
+
},
|
|
606
|
+
complete: () => {
|
|
607
|
+
if (this.remoteStreams.get(queryId) !== runtime || runtime.cancelled) return;
|
|
608
|
+
this.applyRemoteStreamEvent(queryId, descriptor, {
|
|
609
|
+
type: "progress",
|
|
610
|
+
progress: { phase: "complete" }
|
|
611
|
+
});
|
|
612
|
+
this.stopRemoteQueryStream(queryId);
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
).then((subscription) => {
|
|
616
|
+
const unsubscribe = this.normalizeRemoteStreamSubscription(subscription);
|
|
617
|
+
if (runtime.cancelled) {
|
|
618
|
+
unsubscribe?.();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
runtime.unsubscribe = unsubscribe;
|
|
622
|
+
}).catch((err) => {
|
|
623
|
+
if (runtime.cancelled) return;
|
|
624
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
625
|
+
this.handleRemoteQueryError(queryId, descriptor, error);
|
|
626
|
+
this.remoteStreams.delete(queryId);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
stopRemoteQueryStream(queryId) {
|
|
630
|
+
const runtime = this.remoteStreams.get(queryId);
|
|
631
|
+
if (!runtime) return;
|
|
632
|
+
runtime.cancelled = true;
|
|
633
|
+
this.remoteStreams.delete(queryId);
|
|
634
|
+
runtime.unsubscribe?.();
|
|
635
|
+
}
|
|
636
|
+
normalizeRemoteStreamSubscription(subscription) {
|
|
637
|
+
return this.normalizeRemoteSubscription(subscription);
|
|
638
|
+
}
|
|
639
|
+
normalizeRemoteInvalidationSubscription(subscription) {
|
|
640
|
+
return this.normalizeRemoteSubscription(subscription);
|
|
641
|
+
}
|
|
642
|
+
normalizeRemoteSubscription(subscription) {
|
|
643
|
+
if (!subscription) return null;
|
|
644
|
+
if (typeof subscription === "function") return subscription;
|
|
645
|
+
return () => {
|
|
646
|
+
subscription.unsubscribe();
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
startRemoteInvalidationSubscription() {
|
|
650
|
+
if (this.remoteInvalidations) return;
|
|
651
|
+
if (!this.remoteNodeQueryClient?.subscribeInvalidations) return;
|
|
652
|
+
const runtime = {
|
|
653
|
+
unsubscribe: null,
|
|
654
|
+
cancelled: false,
|
|
655
|
+
start: Promise.resolve()
|
|
656
|
+
};
|
|
657
|
+
this.remoteInvalidations = runtime;
|
|
658
|
+
runtime.start = Promise.resolve(
|
|
659
|
+
this.remoteNodeQueryClient.subscribeInvalidations({
|
|
660
|
+
next: (event) => {
|
|
661
|
+
if (this.remoteInvalidations !== runtime || runtime.cancelled) return;
|
|
662
|
+
this.handleRemoteQueryInvalidation(event);
|
|
663
|
+
},
|
|
664
|
+
error: (error) => {
|
|
665
|
+
if (this.remoteInvalidations !== runtime || runtime.cancelled) return;
|
|
666
|
+
console.error("[MainThreadBridge] Remote query invalidation subscription failed:", error);
|
|
667
|
+
this.stopRemoteInvalidationSubscription();
|
|
668
|
+
},
|
|
669
|
+
complete: () => {
|
|
670
|
+
if (this.remoteInvalidations !== runtime || runtime.cancelled) return;
|
|
671
|
+
this.stopRemoteInvalidationSubscription();
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
).then((subscription) => {
|
|
675
|
+
const unsubscribe = this.normalizeRemoteInvalidationSubscription(subscription);
|
|
676
|
+
if (runtime.cancelled) {
|
|
677
|
+
unsubscribe?.();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
runtime.unsubscribe = unsubscribe;
|
|
681
|
+
}).catch((err) => {
|
|
682
|
+
if (runtime.cancelled) return;
|
|
683
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
684
|
+
console.error("[MainThreadBridge] Failed to subscribe to remote invalidations:", error);
|
|
685
|
+
this.remoteInvalidations = null;
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
stopRemoteInvalidationSubscription() {
|
|
689
|
+
const runtime = this.remoteInvalidations;
|
|
690
|
+
if (!runtime) return;
|
|
691
|
+
runtime.cancelled = true;
|
|
692
|
+
this.remoteInvalidations = null;
|
|
693
|
+
runtime.unsubscribe?.();
|
|
694
|
+
}
|
|
695
|
+
handleRemoteQueryInvalidation(event) {
|
|
696
|
+
for (const entry of this.getRemoteInvalidationEntries(event)) {
|
|
697
|
+
if (this.cache.getSubscriberCount(entry.queryId) === 0) continue;
|
|
698
|
+
const route = routeRemoteNodeQuery({
|
|
699
|
+
descriptor: entry.descriptor,
|
|
700
|
+
hasRemoteClient: this.remoteNodeQueryClient !== null,
|
|
701
|
+
localRowCount: this.getRemoteInvalidationRouteRowCount(entry.queryId, entry.data),
|
|
702
|
+
thresholds: this.remoteNodeQueryRouting
|
|
703
|
+
});
|
|
704
|
+
if (route.shouldRunRemote) {
|
|
705
|
+
void this.loadRemoteQuery(entry.queryId, entry.descriptor, route);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (shouldRunRemoteQuery(entry.descriptor)) {
|
|
709
|
+
void this.loadRemoteQuery(entry.queryId, entry.descriptor);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
getRemoteInvalidationRouteRowCount(queryId, data) {
|
|
714
|
+
const totalCount = this.cache.getMetadata(queryId)?.pageInfo?.totalCount;
|
|
715
|
+
return typeof totalCount === "number" ? totalCount : data?.length ?? void 0;
|
|
716
|
+
}
|
|
717
|
+
getRemoteInvalidationEntries(event) {
|
|
718
|
+
const matches = /* @__PURE__ */ new Map();
|
|
719
|
+
const addEntry = (entry) => {
|
|
720
|
+
matches.set(entry.queryId, entry);
|
|
721
|
+
};
|
|
722
|
+
const addQueryId = (queryId) => {
|
|
723
|
+
if (!queryId || !this.cache.has(queryId)) return;
|
|
724
|
+
const descriptor = this.cache.getDescriptor(queryId);
|
|
725
|
+
if (!descriptor) return;
|
|
726
|
+
addEntry({ queryId, descriptor, data: this.cache.get(queryId) });
|
|
727
|
+
};
|
|
728
|
+
addQueryId(event.requestId);
|
|
729
|
+
if (event.descriptor) {
|
|
730
|
+
addQueryId(serializeQueryDescriptor(event.descriptor));
|
|
731
|
+
}
|
|
732
|
+
const schemaEntries = event.schemaId !== void 0 ? this.cache.getEntriesForSchema(event.schemaId) : event.requestId || event.descriptor || event.nodeIds ? [] : this.cache.getEntries();
|
|
733
|
+
schemaEntries.forEach(addEntry);
|
|
734
|
+
if (event.nodeIds && event.nodeIds.length > 0) {
|
|
735
|
+
const nodeIds = new Set(event.nodeIds);
|
|
736
|
+
const candidates = event.schemaId !== void 0 ? this.cache.getEntriesForSchema(event.schemaId) : this.cache.getEntries();
|
|
737
|
+
candidates.filter((entry) => {
|
|
738
|
+
if (entry.descriptor.nodeId && nodeIds.has(entry.descriptor.nodeId)) return true;
|
|
739
|
+
return (entry.data ?? []).some((node) => nodeIds.has(node.id));
|
|
740
|
+
}).forEach(addEntry);
|
|
741
|
+
}
|
|
742
|
+
return Array.from(matches.values());
|
|
743
|
+
}
|
|
744
|
+
applyRemoteStreamEvent(queryId, descriptor, event) {
|
|
745
|
+
const runtime = this.remoteStreams.get(queryId);
|
|
746
|
+
if (!runtime) return;
|
|
747
|
+
const normalizedEvent = this.normalizeRemoteStreamEvent(descriptor, runtime.state, event);
|
|
748
|
+
const eventWithMetadata = event.type === "error" && event.metadata === void 0 ? {
|
|
749
|
+
...normalizedEvent,
|
|
750
|
+
metadata: this.createStreamErrorMetadata(
|
|
751
|
+
descriptor,
|
|
752
|
+
runtime.state,
|
|
753
|
+
normalizedEvent
|
|
754
|
+
)
|
|
755
|
+
} : normalizedEvent;
|
|
756
|
+
const nextState = reduceQueryStreamEvent(runtime.state, eventWithMetadata);
|
|
757
|
+
runtime.state = nextState;
|
|
758
|
+
this.cache.set(
|
|
759
|
+
queryId,
|
|
760
|
+
nextState.data,
|
|
761
|
+
descriptor,
|
|
762
|
+
void 0,
|
|
763
|
+
this.withStreamMetadata(descriptor, nextState, eventWithMetadata)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
normalizeRemoteStreamEvent(descriptor, state, event) {
|
|
767
|
+
const verification = event.metadata?.verification;
|
|
768
|
+
if (isRemoteVerificationFailed(verification)) {
|
|
769
|
+
return {
|
|
770
|
+
type: "error",
|
|
771
|
+
code: "VERIFICATION_FAILED",
|
|
772
|
+
error: "Remote stream event verification failed",
|
|
773
|
+
metadata: this.createStreamErrorMetadata(descriptor, state, {
|
|
774
|
+
type: "error",
|
|
775
|
+
code: "VERIFICATION_FAILED",
|
|
776
|
+
error: "Remote stream event verification failed"
|
|
777
|
+
})
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (!verification || verification.status !== "mixed") {
|
|
781
|
+
return event;
|
|
782
|
+
}
|
|
783
|
+
switch (event.type) {
|
|
784
|
+
case "snapshot":
|
|
785
|
+
return {
|
|
786
|
+
...event,
|
|
787
|
+
nodes: filterRemoteNodesByVerification(event.nodes, verification)
|
|
788
|
+
};
|
|
789
|
+
case "reset":
|
|
790
|
+
return event.nodes ? {
|
|
791
|
+
...event,
|
|
792
|
+
nodes: filterRemoteNodesByVerification(event.nodes, verification)
|
|
793
|
+
} : event;
|
|
794
|
+
case "insert":
|
|
795
|
+
return filterRemoteNodesByVerification([event.node], verification).length > 0 ? event : {
|
|
796
|
+
type: "delete",
|
|
797
|
+
nodeId: event.node.id,
|
|
798
|
+
metadata: event.metadata
|
|
799
|
+
};
|
|
800
|
+
case "update":
|
|
801
|
+
return filterRemoteNodesByVerification([event.node], verification).length > 0 ? event : {
|
|
802
|
+
type: "delete",
|
|
803
|
+
nodeId: event.nodeId,
|
|
804
|
+
metadata: event.metadata
|
|
805
|
+
};
|
|
806
|
+
default:
|
|
807
|
+
return event;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
withStreamMetadata(descriptor, state, event) {
|
|
811
|
+
const base = state.metadata ?? createQuerySnapshotMetadata({
|
|
812
|
+
descriptor,
|
|
813
|
+
nodes: state.data ?? [],
|
|
814
|
+
source: getRemoteQuerySource(descriptor)
|
|
815
|
+
});
|
|
816
|
+
const metadata = { ...base };
|
|
817
|
+
if (!state.error) {
|
|
818
|
+
delete metadata.error;
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
...metadata,
|
|
822
|
+
updatedAt: Date.now(),
|
|
823
|
+
stream: {
|
|
824
|
+
status: state.status,
|
|
825
|
+
lastEvent: event.type,
|
|
826
|
+
lastEventAt: Date.now(),
|
|
827
|
+
...state.progress ? { progress: state.progress } : {},
|
|
828
|
+
...state.error ? { error: state.error } : {},
|
|
829
|
+
...event.type === "reset" ? { resetReason: event.reason } : {}
|
|
830
|
+
},
|
|
831
|
+
...state.error ? { error: state.error } : {}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
createStreamErrorMetadata(descriptor, state, event) {
|
|
835
|
+
const currentMetadata = state.metadata ?? this.cache.getMetadata(serializeQueryDescriptor(descriptor));
|
|
836
|
+
const error = event.code === "VERIFICATION_FAILED" ? createRemoteVerificationError({
|
|
837
|
+
requestId: serializeQueryDescriptor(descriptor),
|
|
838
|
+
source: getRemoteQuerySource(descriptor),
|
|
839
|
+
message: event.error
|
|
840
|
+
}) : new Error(event.error);
|
|
841
|
+
if (currentMetadata && state.data !== null) {
|
|
842
|
+
return createRemoteFallbackMetadata({
|
|
843
|
+
localMetadata: currentMetadata,
|
|
844
|
+
error
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return withRemoteErrorVerificationMetadata(
|
|
848
|
+
createQueryErrorMetadata({
|
|
849
|
+
descriptor,
|
|
850
|
+
source: getRemoteQuerySource(descriptor),
|
|
851
|
+
error: new Error(event.error)
|
|
852
|
+
}),
|
|
853
|
+
error
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
async executeRemoteQuery(queryId, descriptor, route) {
|
|
857
|
+
const mode = route?.shouldRunRemote ? route.mode : getRemoteQueryMode(descriptor);
|
|
858
|
+
if (!mode) return;
|
|
859
|
+
const source = route?.shouldRunRemote ? route.source : getRemoteQuerySource(descriptor);
|
|
860
|
+
const localSnapshot = this.cache.get(queryId) ?? [];
|
|
861
|
+
const requestDescriptor = {
|
|
862
|
+
...descriptor,
|
|
863
|
+
mode,
|
|
864
|
+
source
|
|
865
|
+
};
|
|
866
|
+
try {
|
|
867
|
+
const response = await this.remoteNodeQueryClient.query(
|
|
868
|
+
createRemoteNodeQueryRequest({
|
|
869
|
+
requestId: queryId,
|
|
870
|
+
descriptor: requestDescriptor,
|
|
871
|
+
mode,
|
|
872
|
+
source,
|
|
873
|
+
client: {
|
|
874
|
+
knownNodeIds: localSnapshot.map((node) => node.id)
|
|
875
|
+
}
|
|
876
|
+
})
|
|
877
|
+
);
|
|
878
|
+
if (isRemoteNodeQueryError(response)) {
|
|
879
|
+
this.handleRemoteQueryError(queryId, descriptor, response);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (!isRemoteNodeQuerySuccess(response)) return;
|
|
883
|
+
if (isRemoteVerificationFailed(response.verification)) {
|
|
884
|
+
this.handleRemoteQueryError(
|
|
885
|
+
queryId,
|
|
886
|
+
descriptor,
|
|
887
|
+
createRemoteVerificationError({
|
|
888
|
+
requestId: response.requestId,
|
|
889
|
+
source: response.source
|
|
890
|
+
})
|
|
891
|
+
);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const verifiedRemoteNodes = filterRemoteNodesByVerification(
|
|
895
|
+
response.nodes,
|
|
896
|
+
response.verification
|
|
897
|
+
);
|
|
898
|
+
const nodes = mode === "local-then-remote" ? mergeRemoteNodeSnapshots(localSnapshot, verifiedRemoteNodes) : verifiedRemoteNodes;
|
|
899
|
+
const metadata = createRemoteSuccessMetadata({
|
|
900
|
+
response,
|
|
901
|
+
source: mode === "local-then-remote" ? "hybrid" : response.source,
|
|
902
|
+
loadedCount: nodes.length
|
|
903
|
+
});
|
|
904
|
+
this.cache.set(queryId, nodes, descriptor, void 0, {
|
|
905
|
+
...metadata,
|
|
906
|
+
...route ? { routing: createQueryRoutingMetadata(route) } : {}
|
|
907
|
+
});
|
|
59
908
|
} catch (err) {
|
|
60
|
-
|
|
61
|
-
|
|
909
|
+
this.handleRemoteQueryError(
|
|
910
|
+
queryId,
|
|
911
|
+
descriptor,
|
|
912
|
+
err instanceof Error ? err : new Error(String(err))
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
handleRemoteQueryError(queryId, descriptor, error) {
|
|
917
|
+
const localSnapshot = this.cache.get(queryId);
|
|
918
|
+
const localMetadata = this.cache.getMetadata(queryId);
|
|
919
|
+
if ((descriptor.mode === "local-then-remote" || descriptor.mode === "live" || descriptor.mode === "stream") && localSnapshot && localMetadata) {
|
|
920
|
+
this.cache.set(
|
|
921
|
+
queryId,
|
|
922
|
+
localSnapshot,
|
|
923
|
+
descriptor,
|
|
924
|
+
void 0,
|
|
925
|
+
createRemoteFallbackMetadata({ localMetadata, error })
|
|
926
|
+
);
|
|
927
|
+
return;
|
|
62
928
|
}
|
|
929
|
+
const fallbackSource = getRemoteQuerySource(descriptor);
|
|
930
|
+
const message = error instanceof Error ? error.message : error.message;
|
|
931
|
+
const metadata = createQueryErrorMetadata({
|
|
932
|
+
descriptor,
|
|
933
|
+
source: fallbackSource,
|
|
934
|
+
error: new Error(message)
|
|
935
|
+
});
|
|
936
|
+
this.cache.set(
|
|
937
|
+
queryId,
|
|
938
|
+
[],
|
|
939
|
+
descriptor,
|
|
940
|
+
void 0,
|
|
941
|
+
withRemoteErrorVerificationMetadata(metadata, error)
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
debugQueryPlan(queryId, plan) {
|
|
945
|
+
if (typeof localStorage === "undefined") return;
|
|
946
|
+
if (localStorage.getItem("xnet:sync:debug") !== "true") return;
|
|
947
|
+
console.debug("[MainThreadBridge] query", {
|
|
948
|
+
queryId,
|
|
949
|
+
durationMs: plan.durationMs,
|
|
950
|
+
hydratedNodeCount: plan.hydratedNodeCount
|
|
951
|
+
});
|
|
63
952
|
}
|
|
64
953
|
/**
|
|
65
954
|
* Handle store changes and invalidate affected caches.
|
|
66
955
|
*/
|
|
956
|
+
enqueueStoreChange(event) {
|
|
957
|
+
this.pendingStoreChanges.push(event);
|
|
958
|
+
if (this.storeChangeFlushQueued) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
this.storeChangeFlushQueued = true;
|
|
962
|
+
queueMicrotask(() => {
|
|
963
|
+
this.flushStoreChanges();
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
flushStoreChanges() {
|
|
967
|
+
const events = this.pendingStoreChanges;
|
|
968
|
+
this.pendingStoreChanges = [];
|
|
969
|
+
this.storeChangeFlushQueued = false;
|
|
970
|
+
if (events.length === 0) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (!this.isBulkStoreChangeSet(events) && events.length === 1) {
|
|
974
|
+
this.handleStoreChange(events[0]);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
this.handleStoreChangeSet(events);
|
|
978
|
+
}
|
|
979
|
+
isBulkStoreChangeSet(events) {
|
|
980
|
+
return events.length > BULK_STORE_CHANGE_RELOAD_THRESHOLD || events.some((event) => (event.change.batchSize ?? 1) > BULK_STORE_CHANGE_RELOAD_THRESHOLD);
|
|
981
|
+
}
|
|
982
|
+
handleStoreChangeSet(events) {
|
|
983
|
+
const eventsBySchema = groupNodeChangeEventsBySchema(events);
|
|
984
|
+
for (const [schemaId, schemaEvents] of eventsBySchema) {
|
|
985
|
+
const shouldReload = this.isBulkStoreChangeSet(schemaEvents);
|
|
986
|
+
const changes = schemaEvents.map((event) => ({
|
|
987
|
+
nodeId: event.change.payload.nodeId,
|
|
988
|
+
nextNode: event.node ?? null
|
|
989
|
+
}));
|
|
990
|
+
for (const entry of this.cache.getEntriesForSchema(schemaId)) {
|
|
991
|
+
if (entry.descriptor.mode === "remote") continue;
|
|
992
|
+
if (entry.data === null || shouldReload) {
|
|
993
|
+
void this.loadInitialQuery(entry.queryId, entry.descriptor);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
this.applyChangesToEntry(entry, changes);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
handleStoreBatchChange(event) {
|
|
1001
|
+
if (event.nodeIds.length > BULK_STORE_CHANGE_RELOAD_THRESHOLD) {
|
|
1002
|
+
this.reloadEntriesForSchemas(event.schemaIds);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
void this.applyStoreBatchChangeDeltas(event).catch((err) => {
|
|
1006
|
+
console.error("[MainThreadBridge] Failed to apply batch change deltas:", err);
|
|
1007
|
+
this.reloadEntriesForSchemas(event.schemaIds);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
reloadEntriesForSchemas(schemaIds) {
|
|
1011
|
+
for (const schemaId of schemaIds) {
|
|
1012
|
+
for (const entry of this.cache.getEntriesForSchema(schemaId)) {
|
|
1013
|
+
if (entry.descriptor.mode === "remote") continue;
|
|
1014
|
+
void this.loadInitialQuery(entry.queryId, entry.descriptor);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async applyStoreBatchChangeDeltas(event) {
|
|
1019
|
+
const nodes = await Promise.all(event.nodeIds.map((nodeId) => this.store.get(nodeId)));
|
|
1020
|
+
const changes = event.nodeIds.map((nodeId, index) => ({
|
|
1021
|
+
nodeId,
|
|
1022
|
+
nextNode: nodes[index]
|
|
1023
|
+
}));
|
|
1024
|
+
for (const schemaId of event.schemaIds) {
|
|
1025
|
+
for (const entry of this.cache.getEntriesForSchema(schemaId)) {
|
|
1026
|
+
if (entry.descriptor.mode === "remote") continue;
|
|
1027
|
+
if (entry.data === null) {
|
|
1028
|
+
void this.loadInitialQuery(entry.queryId, entry.descriptor);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
this.applyChangesToEntry(entry, changes);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Apply a list of node changes to a single cache entry, falling back to a
|
|
1037
|
+
* storage re-query only when a delta is ambiguous. Optimistic
|
|
1038
|
+
* (pre-persistence) applies skip ambiguous entries instead of reloading —
|
|
1039
|
+
* storage still holds the OLD state, and the durable change event that
|
|
1040
|
+
* follows will reconcile them.
|
|
1041
|
+
*/
|
|
1042
|
+
applyChangesToEntry(entry, changes, options) {
|
|
1043
|
+
let data = entry.data ?? [];
|
|
1044
|
+
let workingSet = entry.workingSet;
|
|
1045
|
+
let changed = false;
|
|
1046
|
+
for (const change of changes) {
|
|
1047
|
+
const applied = this.applyChangeToEntryState({
|
|
1048
|
+
descriptor: entry.descriptor,
|
|
1049
|
+
data,
|
|
1050
|
+
workingSet,
|
|
1051
|
+
nodeId: change.nodeId,
|
|
1052
|
+
nextNode: change.nextNode
|
|
1053
|
+
});
|
|
1054
|
+
if (applied.kind === "reload") {
|
|
1055
|
+
if (options?.onAmbiguous !== "skip") {
|
|
1056
|
+
void this.loadInitialQuery(entry.queryId, entry.descriptor);
|
|
1057
|
+
}
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (applied.changed) {
|
|
1061
|
+
data = applied.data;
|
|
1062
|
+
workingSet = applied.workingSet;
|
|
1063
|
+
changed = true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (changed) {
|
|
1067
|
+
this.cache.set(entry.queryId, data, void 0, void 0, void 0, workingSet);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Find the freshest cached snapshot of a node across all cache entries.
|
|
1072
|
+
*/
|
|
1073
|
+
findCachedNode(nodeId) {
|
|
1074
|
+
for (const entry of this.cache.getEntries()) {
|
|
1075
|
+
const fromWorkingSet = entry.workingSet?.nodes.find((node) => node.id === nodeId);
|
|
1076
|
+
if (fromWorkingSet) return fromWorkingSet;
|
|
1077
|
+
const fromData = entry.data?.find((node) => node.id === nodeId);
|
|
1078
|
+
if (fromData) return fromData;
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Synchronously apply an optimistic node mutation to every affected cache
|
|
1084
|
+
* entry before persistence, so subscribers see the edit immediately.
|
|
1085
|
+
* Returns a revert function that restores authoritative state by
|
|
1086
|
+
* re-querying storage (used when persistence fails).
|
|
1087
|
+
*/
|
|
1088
|
+
applyOptimisticNodeChange(nodeId, mutate) {
|
|
1089
|
+
const current = this.findCachedNode(nodeId);
|
|
1090
|
+
if (!current) {
|
|
1091
|
+
return () => {
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
const nextNode = mutate(current);
|
|
1095
|
+
const changes = [{ nodeId, nextNode }];
|
|
1096
|
+
for (const entry of this.cache.getEntriesForSchema(current.schemaId)) {
|
|
1097
|
+
if (entry.descriptor.mode === "remote") continue;
|
|
1098
|
+
if (entry.data === null) continue;
|
|
1099
|
+
this.applyChangesToEntry(entry, changes, { onAmbiguous: "skip" });
|
|
1100
|
+
}
|
|
1101
|
+
return () => {
|
|
1102
|
+
this.reloadEntriesForSchemas([current.schemaId]);
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
applyChangeToEntryState(input) {
|
|
1106
|
+
if (input.workingSet && queryDescriptorSupportsBoundedDelta(input.descriptor)) {
|
|
1107
|
+
const delta2 = applyNodeChangeToBoundedQueryResult({
|
|
1108
|
+
descriptor: input.descriptor,
|
|
1109
|
+
workingSet: input.workingSet,
|
|
1110
|
+
nodeId: input.nodeId,
|
|
1111
|
+
nextNode: input.nextNode
|
|
1112
|
+
});
|
|
1113
|
+
if (delta2.kind === "reload") return { kind: "reload" };
|
|
1114
|
+
if (delta2.kind === "noop") {
|
|
1115
|
+
return { kind: "ok", data: input.data, workingSet: input.workingSet, changed: false };
|
|
1116
|
+
}
|
|
1117
|
+
return { kind: "ok", data: delta2.data, workingSet: delta2.workingSet, changed: true };
|
|
1118
|
+
}
|
|
1119
|
+
const delta = applyNodeChangeToQueryResult({
|
|
1120
|
+
descriptor: input.descriptor,
|
|
1121
|
+
currentData: input.data,
|
|
1122
|
+
nodeId: input.nodeId,
|
|
1123
|
+
nextNode: input.nextNode
|
|
1124
|
+
});
|
|
1125
|
+
if (delta.kind === "reload") return { kind: "reload" };
|
|
1126
|
+
if (delta.kind === "noop") {
|
|
1127
|
+
return { kind: "ok", data: input.data, workingSet: input.workingSet, changed: false };
|
|
1128
|
+
}
|
|
1129
|
+
return { kind: "ok", data: delta.data, workingSet: null, changed: true };
|
|
1130
|
+
}
|
|
67
1131
|
handleStoreChange(event) {
|
|
68
1132
|
const { node, change } = event;
|
|
69
1133
|
const schemaId = node?.schemaId ?? change.payload.schemaId;
|
|
70
1134
|
if (!schemaId) return;
|
|
71
|
-
const
|
|
72
|
-
for (const
|
|
73
|
-
|
|
74
|
-
|
|
1135
|
+
const changes = [{ nodeId: change.payload.nodeId, nextNode: node ?? null }];
|
|
1136
|
+
for (const entry of this.cache.getEntriesForSchema(schemaId)) {
|
|
1137
|
+
if (entry.descriptor.mode === "remote") continue;
|
|
1138
|
+
if (entry.data === null) {
|
|
1139
|
+
void this.loadInitialQuery(entry.queryId, entry.descriptor);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
this.applyChangesToEntry(entry, changes);
|
|
75
1143
|
}
|
|
76
1144
|
}
|
|
77
1145
|
// ─── Mutations ──────────────────────────────────────────
|
|
@@ -83,14 +1151,41 @@ var MainThreadBridge = class {
|
|
|
83
1151
|
});
|
|
84
1152
|
}
|
|
85
1153
|
async update(nodeId, changes) {
|
|
86
|
-
|
|
1154
|
+
const revert = this.applyOptimisticNodeChange(nodeId, (node) => ({
|
|
1155
|
+
...node,
|
|
1156
|
+
properties: { ...node.properties, ...changes },
|
|
1157
|
+
updatedAt: Date.now()
|
|
1158
|
+
}));
|
|
1159
|
+
try {
|
|
1160
|
+
return await this.store.update(nodeId, { properties: changes });
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
revert();
|
|
1163
|
+
throw err;
|
|
1164
|
+
}
|
|
87
1165
|
}
|
|
88
1166
|
async delete(nodeId) {
|
|
89
|
-
|
|
1167
|
+
const revert = this.applyOptimisticNodeChange(nodeId, (node) => ({
|
|
1168
|
+
...node,
|
|
1169
|
+
deleted: true,
|
|
1170
|
+
updatedAt: Date.now()
|
|
1171
|
+
}));
|
|
1172
|
+
try {
|
|
1173
|
+
await this.store.delete(nodeId);
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
revert();
|
|
1176
|
+
throw err;
|
|
1177
|
+
}
|
|
90
1178
|
}
|
|
91
1179
|
async restore(nodeId) {
|
|
92
1180
|
return this.store.restore(nodeId);
|
|
93
1181
|
}
|
|
1182
|
+
async bulkWrite(input) {
|
|
1183
|
+
return this.store.batchWrite(input);
|
|
1184
|
+
}
|
|
1185
|
+
async transaction(operations) {
|
|
1186
|
+
const tx = await this.store.transaction(operations);
|
|
1187
|
+
return { batchId: tx.batchId, results: tx.results, tempIds: tx.tempIds };
|
|
1188
|
+
}
|
|
94
1189
|
// ─── Documents ─────────────────────────────────────────
|
|
95
1190
|
/**
|
|
96
1191
|
* Acquire a Y.Doc for editing.
|
|
@@ -120,13 +1215,31 @@ var MainThreadBridge = class {
|
|
|
120
1215
|
}
|
|
121
1216
|
}
|
|
122
1217
|
// ─── Lifecycle ──────────────────────────────────────────
|
|
1218
|
+
async initialize(config) {
|
|
1219
|
+
const nextRemoteNodeQueryClient = config.remoteNodeQueryClient ?? this.remoteNodeQueryClient;
|
|
1220
|
+
if (nextRemoteNodeQueryClient !== this.remoteNodeQueryClient) {
|
|
1221
|
+
this.stopRemoteInvalidationSubscription();
|
|
1222
|
+
}
|
|
1223
|
+
this.remoteNodeQueryClient = nextRemoteNodeQueryClient;
|
|
1224
|
+
this.remoteNodeQueryRouting = config.remoteNodeQueryRouting ?? this.remoteNodeQueryRouting;
|
|
1225
|
+
this.startRemoteInvalidationSubscription();
|
|
1226
|
+
}
|
|
123
1227
|
destroy() {
|
|
1228
|
+
this.stopRemoteInvalidationSubscription();
|
|
1229
|
+
for (const queryId of this.remoteStreams.keys()) {
|
|
1230
|
+
this.stopRemoteQueryStream(queryId);
|
|
1231
|
+
}
|
|
124
1232
|
if (this.storeUnsubscribe) {
|
|
125
1233
|
this.storeUnsubscribe();
|
|
126
1234
|
this.storeUnsubscribe = null;
|
|
127
1235
|
}
|
|
1236
|
+
if (this.storeBatchUnsubscribe) {
|
|
1237
|
+
this.storeBatchUnsubscribe();
|
|
1238
|
+
this.storeBatchUnsubscribe = null;
|
|
1239
|
+
}
|
|
128
1240
|
this.cache.clear();
|
|
129
1241
|
this.statusListeners.clear();
|
|
1242
|
+
this.remoteLoads.clear();
|
|
130
1243
|
}
|
|
131
1244
|
// ─── Status ─────────────────────────────────────────────
|
|
132
1245
|
get status() {
|
|
@@ -156,12 +1269,12 @@ var MainThreadBridge = class {
|
|
|
156
1269
|
return this.store.list(options);
|
|
157
1270
|
}
|
|
158
1271
|
};
|
|
159
|
-
function createMainThreadBridge(store) {
|
|
160
|
-
return new MainThreadBridge(store);
|
|
1272
|
+
function createMainThreadBridge(store, options) {
|
|
1273
|
+
return new MainThreadBridge(store, options);
|
|
161
1274
|
}
|
|
162
1275
|
|
|
163
1276
|
// src/worker-bridge.ts
|
|
164
|
-
import { wrap, proxy } from "comlink";
|
|
1277
|
+
import { wrap, proxy, transfer } from "comlink";
|
|
165
1278
|
import { Awareness } from "y-protocols/awareness";
|
|
166
1279
|
import * as Y from "yjs";
|
|
167
1280
|
|
|
@@ -435,8 +1548,10 @@ var WorkerBridge = class {
|
|
|
435
1548
|
remote;
|
|
436
1549
|
cache = new QueryCache();
|
|
437
1550
|
subscriptions = /* @__PURE__ */ new Map();
|
|
438
|
-
|
|
1551
|
+
activeRemoteSubscriptions = /* @__PURE__ */ new Set();
|
|
439
1552
|
statusListeners = /* @__PURE__ */ new Set();
|
|
1553
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
1554
|
+
changeFeedStarted = false;
|
|
440
1555
|
_status = "connecting";
|
|
441
1556
|
initialized = false;
|
|
442
1557
|
// Mirror Y.Docs on the main thread (for TipTap binding)
|
|
@@ -447,13 +1562,21 @@ var WorkerBridge = class {
|
|
|
447
1562
|
}
|
|
448
1563
|
/**
|
|
449
1564
|
* Initialize the bridge and underlying worker.
|
|
1565
|
+
*
|
|
1566
|
+
* When `config.storagePort` is provided (a MessagePort connected to the
|
|
1567
|
+
* SQLite worker), it is transferred to the data worker so storage calls
|
|
1568
|
+
* run worker-to-worker without a main-thread hop.
|
|
450
1569
|
*/
|
|
451
1570
|
async initialize(config) {
|
|
452
|
-
|
|
1571
|
+
const workerConfig = {
|
|
453
1572
|
dbName: config.dbName ?? "xnet",
|
|
454
1573
|
authorDID: config.authorDID,
|
|
455
|
-
signingKey: Array.from(config.signingKey)
|
|
456
|
-
|
|
1574
|
+
signingKey: Array.from(config.signingKey),
|
|
1575
|
+
...config.storagePort ? { storagePort: config.storagePort } : {}
|
|
1576
|
+
};
|
|
1577
|
+
await this.remote.initialize(
|
|
1578
|
+
config.storagePort ? transfer(workerConfig, [config.storagePort]) : workerConfig
|
|
1579
|
+
);
|
|
457
1580
|
this.remote.onStatusChange(
|
|
458
1581
|
proxy((status) => {
|
|
459
1582
|
this._status = status;
|
|
@@ -467,15 +1590,16 @@ var WorkerBridge = class {
|
|
|
467
1590
|
}
|
|
468
1591
|
// ─── Queries ─────────────────────────────────────────────────────────────────
|
|
469
1592
|
query(schema, options) {
|
|
470
|
-
const
|
|
471
|
-
const queryId =
|
|
472
|
-
|
|
473
|
-
this.
|
|
474
|
-
|
|
475
|
-
this.startWorkerSubscription(queryId,
|
|
1593
|
+
const descriptor = createQueryDescriptor(schema._schemaId, options);
|
|
1594
|
+
const queryId = serializeQueryDescriptor(descriptor);
|
|
1595
|
+
this.cache.initEntry(queryId, descriptor);
|
|
1596
|
+
if (this.initialized && !this.activeRemoteSubscriptions.has(queryId)) {
|
|
1597
|
+
this.activeRemoteSubscriptions.add(queryId);
|
|
1598
|
+
void this.startWorkerSubscription(queryId, descriptor);
|
|
476
1599
|
}
|
|
477
1600
|
return {
|
|
478
1601
|
getSnapshot: () => this.cache.get(queryId),
|
|
1602
|
+
getMetadata: () => this.cache.getMetadata(queryId),
|
|
479
1603
|
subscribe: (callback) => {
|
|
480
1604
|
const subs = this.subscriptions.get(queryId) ?? /* @__PURE__ */ new Set();
|
|
481
1605
|
subs.add(callback);
|
|
@@ -486,26 +1610,62 @@ var WorkerBridge = class {
|
|
|
486
1610
|
unsubCache();
|
|
487
1611
|
if (subs.size === 0) {
|
|
488
1612
|
this.subscriptions.delete(queryId);
|
|
489
|
-
this.
|
|
1613
|
+
if (this.activeRemoteSubscriptions.delete(queryId)) {
|
|
1614
|
+
this.remote.unsubscribe(queryId).catch(console.error);
|
|
1615
|
+
}
|
|
490
1616
|
}
|
|
491
1617
|
};
|
|
492
1618
|
}
|
|
493
1619
|
};
|
|
494
1620
|
}
|
|
495
|
-
async
|
|
1621
|
+
async reloadQuery(descriptor) {
|
|
1622
|
+
if (!this.initialized) {
|
|
1623
|
+
throw new Error("WorkerBridge not initialized");
|
|
1624
|
+
}
|
|
1625
|
+
const queryId = serializeQueryDescriptor(descriptor);
|
|
1626
|
+
if (!this.activeRemoteSubscriptions.has(queryId)) {
|
|
1627
|
+
this.cache.initEntry(queryId, descriptor);
|
|
1628
|
+
this.activeRemoteSubscriptions.add(queryId);
|
|
1629
|
+
await this.startWorkerSubscription(queryId, descriptor);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
const data = decodeWorkerQuerySnapshot(await this.remote.reloadQuery(queryId));
|
|
1633
|
+
this.cache.set(
|
|
1634
|
+
queryId,
|
|
1635
|
+
data,
|
|
1636
|
+
descriptor,
|
|
1637
|
+
void 0,
|
|
1638
|
+
createQuerySnapshotMetadata({ descriptor, nodes: data, source: "memory" })
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
async startWorkerSubscription(queryId, descriptor) {
|
|
496
1642
|
try {
|
|
497
|
-
const
|
|
1643
|
+
const snapshot = await this.remote.subscribe(
|
|
498
1644
|
queryId,
|
|
499
|
-
schemaId,
|
|
500
|
-
|
|
1645
|
+
descriptor.schemaId,
|
|
1646
|
+
this.serializeOptions(queryDescriptorToOptions(descriptor)),
|
|
501
1647
|
proxy((delta) => {
|
|
502
1648
|
this.applyDelta(queryId, delta);
|
|
503
1649
|
})
|
|
504
1650
|
);
|
|
505
|
-
|
|
1651
|
+
const initial = decodeWorkerQuerySnapshot(snapshot);
|
|
1652
|
+
this.cache.set(
|
|
1653
|
+
queryId,
|
|
1654
|
+
initial,
|
|
1655
|
+
descriptor,
|
|
1656
|
+
void 0,
|
|
1657
|
+
createQuerySnapshotMetadata({ descriptor, nodes: initial, source: "memory" })
|
|
1658
|
+
);
|
|
506
1659
|
} catch (err) {
|
|
507
1660
|
console.error("[WorkerBridge] Failed to subscribe:", err);
|
|
508
|
-
this.
|
|
1661
|
+
this.activeRemoteSubscriptions.delete(queryId);
|
|
1662
|
+
this.cache.set(
|
|
1663
|
+
queryId,
|
|
1664
|
+
[],
|
|
1665
|
+
descriptor,
|
|
1666
|
+
void 0,
|
|
1667
|
+
createQuerySnapshotMetadata({ descriptor, nodes: [], source: "memory" })
|
|
1668
|
+
);
|
|
509
1669
|
}
|
|
510
1670
|
}
|
|
511
1671
|
applyDelta(queryId, delta) {
|
|
@@ -521,12 +1681,15 @@ var WorkerBridge = class {
|
|
|
521
1681
|
case "update":
|
|
522
1682
|
updated = current.map((n) => n.id === delta.nodeId ? delta.node : n);
|
|
523
1683
|
break;
|
|
1684
|
+
case "reload":
|
|
1685
|
+
updated = delta.data;
|
|
1686
|
+
break;
|
|
524
1687
|
}
|
|
525
|
-
const
|
|
1688
|
+
const descriptor = this.cache.getDescriptor(queryId);
|
|
526
1689
|
const options = this.cache.getOptions(queryId);
|
|
527
|
-
if (
|
|
1690
|
+
if (descriptor && options) {
|
|
528
1691
|
updated = this.cache.sortNodes(updated, options);
|
|
529
|
-
this.cache.set(queryId, updated,
|
|
1692
|
+
this.cache.set(queryId, updated, descriptor);
|
|
530
1693
|
}
|
|
531
1694
|
}
|
|
532
1695
|
serializeOptions(options) {
|
|
@@ -537,7 +1700,13 @@ var WorkerBridge = class {
|
|
|
537
1700
|
includeDeleted: options.includeDeleted,
|
|
538
1701
|
orderBy: options.orderBy,
|
|
539
1702
|
limit: options.limit,
|
|
540
|
-
offset: options.offset
|
|
1703
|
+
offset: options.offset,
|
|
1704
|
+
page: options.page,
|
|
1705
|
+
spatial: options.spatial,
|
|
1706
|
+
search: options.search,
|
|
1707
|
+
materializedView: options.materializedView,
|
|
1708
|
+
mode: options.mode,
|
|
1709
|
+
source: options.source
|
|
541
1710
|
};
|
|
542
1711
|
}
|
|
543
1712
|
// ─── Mutations ───────────────────────────────────────────────────────────────
|
|
@@ -565,6 +1734,44 @@ var WorkerBridge = class {
|
|
|
565
1734
|
}
|
|
566
1735
|
return this.remote.restore(nodeId);
|
|
567
1736
|
}
|
|
1737
|
+
async bulkWrite(input) {
|
|
1738
|
+
if (!this.initialized) {
|
|
1739
|
+
throw new Error("WorkerBridge not initialized");
|
|
1740
|
+
}
|
|
1741
|
+
return this.remote.bulkWrite(input);
|
|
1742
|
+
}
|
|
1743
|
+
async transaction(operations) {
|
|
1744
|
+
if (!this.initialized) {
|
|
1745
|
+
throw new Error("WorkerBridge not initialized");
|
|
1746
|
+
}
|
|
1747
|
+
return this.remote.transaction(operations);
|
|
1748
|
+
}
|
|
1749
|
+
// ─── Change Feed ─────────────────────────────────────────────────────────────
|
|
1750
|
+
/**
|
|
1751
|
+
* Subscribe to the worker's store change feed (devtools and other
|
|
1752
|
+
* instrumentation). A single proxied forwarder is registered with the
|
|
1753
|
+
* worker on first use; events fan out to local listeners from there.
|
|
1754
|
+
*/
|
|
1755
|
+
subscribeToChanges(listener) {
|
|
1756
|
+
this.changeListeners.add(listener);
|
|
1757
|
+
if (!this.changeFeedStarted) {
|
|
1758
|
+
this.changeFeedStarted = true;
|
|
1759
|
+
this.remote.subscribeToChanges(
|
|
1760
|
+
proxy((event) => {
|
|
1761
|
+
for (const handler of this.changeListeners) {
|
|
1762
|
+
try {
|
|
1763
|
+
handler(event);
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
console.error("[WorkerBridge] Change listener error:", err);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
})
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
return () => {
|
|
1772
|
+
this.changeListeners.delete(listener);
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
568
1775
|
// ─── Documents ────────────────────────────────────────────────────────────────
|
|
569
1776
|
/**
|
|
570
1777
|
* Acquire a Y.Doc for editing.
|
|
@@ -582,6 +1789,7 @@ var WorkerBridge = class {
|
|
|
582
1789
|
}
|
|
583
1790
|
const existing = this.mirrorDocs.get(nodeId);
|
|
584
1791
|
if (existing) {
|
|
1792
|
+
existing.refCount += 1;
|
|
585
1793
|
return {
|
|
586
1794
|
doc: existing.doc,
|
|
587
1795
|
awareness: existing.awareness
|
|
@@ -621,6 +1829,7 @@ var WorkerBridge = class {
|
|
|
621
1829
|
const entry = {
|
|
622
1830
|
doc: mirrorDoc,
|
|
623
1831
|
awareness,
|
|
1832
|
+
refCount: 1,
|
|
624
1833
|
applyingRemote: false,
|
|
625
1834
|
updateBatcher,
|
|
626
1835
|
cleanup: () => {
|
|
@@ -643,6 +1852,10 @@ var WorkerBridge = class {
|
|
|
643
1852
|
releaseDoc(nodeId) {
|
|
644
1853
|
const entry = this.mirrorDocs.get(nodeId);
|
|
645
1854
|
if (!entry) return;
|
|
1855
|
+
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
1856
|
+
if (entry.refCount > 0) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
646
1859
|
entry.cleanup();
|
|
647
1860
|
entry.doc.destroy();
|
|
648
1861
|
this.mirrorDocs.delete(nodeId);
|
|
@@ -660,6 +1873,8 @@ var WorkerBridge = class {
|
|
|
660
1873
|
this.cache.clear();
|
|
661
1874
|
this.subscriptions.clear();
|
|
662
1875
|
this.statusListeners.clear();
|
|
1876
|
+
this.changeListeners.clear();
|
|
1877
|
+
this.changeFeedStarted = false;
|
|
663
1878
|
this.initialized = false;
|
|
664
1879
|
this._status = "disconnected";
|
|
665
1880
|
}
|
|
@@ -682,199 +1897,6 @@ function createWorkerBridge(workerUrl) {
|
|
|
682
1897
|
return new WorkerBridge(workerUrl);
|
|
683
1898
|
}
|
|
684
1899
|
|
|
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
1900
|
// src/create-bridge.ts
|
|
879
1901
|
function isWorkerSupported() {
|
|
880
1902
|
return typeof Worker !== "undefined";
|
|
@@ -882,6 +1904,9 @@ function isWorkerSupported() {
|
|
|
882
1904
|
function isNodeEnvironment() {
|
|
883
1905
|
return typeof process !== "undefined" && process.versions?.node !== void 0;
|
|
884
1906
|
}
|
|
1907
|
+
function getDefaultDataWorkerUrl() {
|
|
1908
|
+
return new URL("./worker/data-worker.js", import.meta.url);
|
|
1909
|
+
}
|
|
885
1910
|
async function createDataBridge(options) {
|
|
886
1911
|
const { nodeStore, config, workerUrl, mode = "auto" } = options;
|
|
887
1912
|
const useWorker = mode === "worker" || mode === "auto" && workerUrl && isWorkerSupported() && !isNodeEnvironment();
|
|
@@ -892,355 +1917,90 @@ async function createDataBridge(options) {
|
|
|
892
1917
|
if (!isWorkerSupported()) {
|
|
893
1918
|
throw new Error("Web Workers are not supported in this environment");
|
|
894
1919
|
}
|
|
895
|
-
const
|
|
896
|
-
await
|
|
897
|
-
return
|
|
898
|
-
}
|
|
899
|
-
|
|
1920
|
+
const bridge2 = new WorkerBridge(workerUrl);
|
|
1921
|
+
await bridge2.initialize(config);
|
|
1922
|
+
return bridge2;
|
|
1923
|
+
}
|
|
1924
|
+
const bridge = new MainThreadBridge(nodeStore, {
|
|
1925
|
+
remoteNodeQueryClient: config.remoteNodeQueryClient,
|
|
1926
|
+
remoteNodeQueryRouting: config.remoteNodeQueryRouting
|
|
1927
|
+
});
|
|
1928
|
+
await bridge.initialize(config);
|
|
1929
|
+
return bridge;
|
|
900
1930
|
}
|
|
901
|
-
function createMainThreadBridgeSync(nodeStore) {
|
|
902
|
-
return new MainThreadBridge(nodeStore);
|
|
1931
|
+
function createMainThreadBridgeSync(nodeStore, options) {
|
|
1932
|
+
return new MainThreadBridge(nodeStore, options);
|
|
903
1933
|
}
|
|
904
1934
|
function createWorkerBridgeSync(workerUrl) {
|
|
905
1935
|
return new WorkerBridge(workerUrl);
|
|
906
1936
|
}
|
|
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
1937
|
export {
|
|
1938
|
+
BOUNDED_QUERY_OVERFETCH,
|
|
1939
|
+
DEFAULT_NODE_QUERY_ROUTER_THRESHOLDS,
|
|
1224
1940
|
MainThreadBridge,
|
|
1225
1941
|
NativeBridge,
|
|
1226
1942
|
NodeStateDecoder,
|
|
1227
1943
|
NodeStateEncoder,
|
|
1944
|
+
PortSQLiteAdapter,
|
|
1228
1945
|
QueryCache,
|
|
1946
|
+
REMOTE_NODE_QUERY_PROTOCOL,
|
|
1947
|
+
REMOTE_NODE_QUERY_PROTOCOL_VERSION,
|
|
1229
1948
|
WorkerBridge,
|
|
1949
|
+
applyNodeChangeToBoundedQueryResult,
|
|
1950
|
+
applyNodeChangeToQueryResult,
|
|
1951
|
+
applyQueryDescriptor,
|
|
1952
|
+
createBoundedWorkingSet,
|
|
1953
|
+
createBoundedWorkingSetDescriptor,
|
|
1230
1954
|
createDataBridge,
|
|
1231
1955
|
createDeltaBatcher,
|
|
1232
1956
|
createMainThreadBridge,
|
|
1233
1957
|
createMainThreadBridgeSync,
|
|
1234
1958
|
createNativeBridge,
|
|
1959
|
+
createQueryDescriptor,
|
|
1960
|
+
createQueryErrorMetadata,
|
|
1961
|
+
createQueryMetadata,
|
|
1962
|
+
createQueryRoutingMetadata,
|
|
1963
|
+
createQuerySnapshotMetadata,
|
|
1964
|
+
createQueryStreamState,
|
|
1965
|
+
createRemoteFallbackMetadata,
|
|
1966
|
+
createRemoteNodeQueryRequest,
|
|
1967
|
+
createRemoteSuccessMetadata,
|
|
1235
1968
|
createUpdateBatcher,
|
|
1236
1969
|
createWorkerBridge,
|
|
1237
1970
|
createWorkerBridgeSync,
|
|
1238
1971
|
debounce,
|
|
1239
1972
|
decodeNodeStates,
|
|
1973
|
+
decodeQueryCursor,
|
|
1974
|
+
decodeWorkerQuerySnapshot,
|
|
1240
1975
|
encodeNodeStates,
|
|
1976
|
+
encodeQueryCursor,
|
|
1977
|
+
encodeWorkerQuerySnapshot,
|
|
1978
|
+
filterQueryNodes,
|
|
1979
|
+
filterRemoteNodesByVerification,
|
|
1980
|
+
getDefaultDataWorkerUrl,
|
|
1981
|
+
getRemoteQueryMode,
|
|
1982
|
+
getRemoteQuerySource,
|
|
1241
1983
|
isExpo,
|
|
1242
1984
|
isNodeEnvironment,
|
|
1243
1985
|
isReactNative,
|
|
1986
|
+
isRemoteNodeQueryError,
|
|
1987
|
+
isRemoteNodeQuerySource,
|
|
1988
|
+
isRemoteNodeQuerySuccess,
|
|
1989
|
+
isRemoteVerificationFailed,
|
|
1244
1990
|
isWorkerSupported,
|
|
1245
|
-
|
|
1991
|
+
matchesQueryDescriptor,
|
|
1992
|
+
mergeRemoteNodeSnapshots,
|
|
1993
|
+
normalizeNodeQueryRouterThresholds,
|
|
1994
|
+
queryDescriptorNeedsBoundedReload,
|
|
1995
|
+
queryDescriptorSupportsBoundedDelta,
|
|
1996
|
+
queryDescriptorToOptions,
|
|
1997
|
+
reduceQueryStreamEvent,
|
|
1998
|
+
reduceQueryStreamEvents,
|
|
1999
|
+
routeRemoteNodeQuery,
|
|
2000
|
+
serializeQueryDescriptor,
|
|
2001
|
+
shouldRunRemoteQuery,
|
|
2002
|
+
shouldUseBinaryEncoding,
|
|
2003
|
+
shouldUseRemoteOnlyQuery,
|
|
2004
|
+
sortQueryNodes,
|
|
2005
|
+
withRemoteErrorVerificationMetadata
|
|
1246
2006
|
};
|