@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/dist/index.js CHANGED
@@ -1,20 +1,437 @@
1
1
  import {
2
- QueryCache
3
- } from "./chunk-X6F5CPJI.js";
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
- constructor(store) {
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.handleStoreChange(event);
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 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);
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
- subscribe: (callback) => this.cache.subscribe(queryId, callback)
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 from the store and update cache.
471
+ * Load query data according to the descriptor's local/remote execution mode.
41
472
  */
42
- async loadQuery(queryId, schemaId, options) {
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
- 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
- });
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
- nodes = this.cache.filterNodes(nodes, options);
57
- nodes = this.cache.sortNodes(nodes, options);
58
- this.cache.set(queryId, nodes, schemaId, options ?? {});
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
- console.error("[MainThreadBridge] Failed to load query:", err);
61
- this.cache.set(queryId, [], schemaId, options ?? {});
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 affectedQueries = this.cache.getQueriesForSchema(schemaId);
72
- for (const queryId of affectedQueries) {
73
- const options = this.cache.getOptions(queryId);
74
- this.loadQuery(queryId, schemaId, options);
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
- return this.store.update(nodeId, { properties: changes });
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
- await this.store.delete(nodeId);
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
- queryCounter = 0;
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
- await this.remote.initialize({
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 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);
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.remote.unsubscribe(queryId).catch(console.error);
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 startWorkerSubscription(queryId, schemaId, options) {
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 initial = await this.remote.subscribe(
1643
+ const snapshot = await this.remote.subscribe(
498
1644
  queryId,
499
- schemaId,
500
- options,
1645
+ descriptor.schemaId,
1646
+ this.serializeOptions(queryDescriptorToOptions(descriptor)),
501
1647
  proxy((delta) => {
502
1648
  this.applyDelta(queryId, delta);
503
1649
  })
504
1650
  );
505
- this.cache.set(queryId, initial, schemaId, options);
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.cache.set(queryId, [], schemaId, options);
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 schemaId = this.cache.getSchemaId(queryId);
1688
+ const descriptor = this.cache.getDescriptor(queryId);
526
1689
  const options = this.cache.getOptions(queryId);
527
- if (schemaId && options) {
1690
+ if (descriptor && options) {
528
1691
  updated = this.cache.sortNodes(updated, options);
529
- this.cache.set(queryId, updated, schemaId, options);
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 bridge = new WorkerBridge(workerUrl);
896
- await bridge.initialize(config);
897
- return bridge;
898
- }
899
- return new MainThreadBridge(nodeStore);
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
- shouldUseBinaryEncoding
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
  };