@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc674ce

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.
Files changed (154) hide show
  1. package/dist/lib/browser/{chunk-WJJXJTNS.mjs → chunk-FJPXA75J.mjs} +7 -12
  2. package/dist/lib/browser/chunk-FJPXA75J.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-FR6GJ4S4.mjs → chunk-WIQUYO7H.mjs} +39 -29
  4. package/dist/lib/browser/chunk-WIQUYO7H.mjs.map +7 -0
  5. package/dist/lib/browser/filter/index.mjs +1 -2
  6. package/dist/lib/browser/index.mjs +1933 -620
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +28 -201
  10. package/dist/lib/browser/testing/index.mjs.map +4 -4
  11. package/dist/lib/node-esm/{chunk-JQCCVDLT.mjs → chunk-7PL6UK4B.mjs} +39 -29
  12. package/dist/lib/node-esm/chunk-7PL6UK4B.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-LEQ77KAT.mjs → chunk-S5KGVQFZ.mjs} +7 -12
  14. package/dist/lib/node-esm/chunk-S5KGVQFZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/filter/index.mjs +1 -2
  16. package/dist/lib/node-esm/index.mjs +1933 -620
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/testing/index.mjs +28 -201
  20. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  21. package/dist/types/src/automerge/automerge-host.d.ts +36 -9
  22. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  23. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  24. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  25. package/dist/types/src/automerge/collection-synchronizer.d.ts +3 -2
  26. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts +11 -6
  28. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  29. package/dist/types/src/automerge/echo-replicator.d.ts +9 -6
  30. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  31. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  32. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  33. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +4 -4
  34. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  35. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +7 -7
  36. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  37. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  38. package/dist/types/src/db-host/automerge-data-source.d.ts +34 -0
  39. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  40. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  41. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  42. package/dist/types/src/db-host/data-service.d.ts +4 -3
  43. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  44. package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -1
  45. package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
  46. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  47. package/dist/types/src/db-host/echo-host.d.ts +43 -11
  48. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  49. package/dist/types/src/db-host/index.d.ts +1 -0
  50. package/dist/types/src/db-host/index.d.ts.map +1 -1
  51. package/dist/types/src/db-host/local-queue-service.d.ts +15 -0
  52. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  53. package/dist/types/src/db-host/query-service.d.ts +7 -2
  54. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  55. package/dist/types/src/db-host/queue-data-source.d.ts +28 -0
  56. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  57. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  58. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  59. package/dist/types/src/db-host/space-state-manager.d.ts +1 -0
  60. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  61. package/dist/types/src/db-host/stub.d.ts +10 -0
  62. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  63. package/dist/types/src/edge/echo-edge-replicator.d.ts +2 -2
  64. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  65. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  66. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  67. package/dist/types/src/pipeline/pipeline.d.ts +2 -2
  68. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  69. package/dist/types/src/query/errors.d.ts +11 -15
  70. package/dist/types/src/query/errors.d.ts.map +1 -1
  71. package/dist/types/src/query/plan.d.ts +37 -3
  72. package/dist/types/src/query/plan.d.ts.map +1 -1
  73. package/dist/types/src/query/query-executor.d.ts +39 -3
  74. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  75. package/dist/types/src/query/query-planner.d.ts +6 -0
  76. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  77. package/dist/types/src/space/auth.d.ts +3 -3
  78. package/dist/types/src/space/auth.d.ts.map +1 -1
  79. package/dist/types/src/space/control-pipeline.d.ts +2 -2
  80. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  81. package/dist/types/src/space/space-manager.d.ts +6 -6
  82. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  83. package/dist/types/src/space/space-protocol.d.ts +5 -5
  84. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  85. package/dist/types/src/space/space.d.ts +3 -3
  86. package/dist/types/src/space/space.d.ts.map +1 -1
  87. package/dist/types/src/testing/index.d.ts +0 -2
  88. package/dist/types/src/testing/index.d.ts.map +1 -1
  89. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  90. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  91. package/dist/types/src/testing/test-replicator.d.ts +6 -6
  92. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  93. package/dist/types/src/util.d.ts.map +1 -1
  94. package/dist/types/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +48 -38
  96. package/src/automerge/automerge-host.test.ts +37 -14
  97. package/src/automerge/automerge-host.ts +130 -36
  98. package/src/automerge/automerge-repo.test.ts +147 -30
  99. package/src/automerge/collection-synchronizer.test.ts +16 -10
  100. package/src/automerge/collection-synchronizer.ts +41 -11
  101. package/src/automerge/echo-network-adapter.ts +23 -11
  102. package/src/automerge/echo-replicator.ts +10 -6
  103. package/src/automerge/heads-store.ts +14 -2
  104. package/src/automerge/leveldb-storage-adapter.ts +4 -4
  105. package/src/automerge/mesh-echo-replicator-connection.ts +7 -7
  106. package/src/automerge/mesh-echo-replicator.ts +2 -2
  107. package/src/db-host/automerge-data-source.test.ts +294 -0
  108. package/src/db-host/automerge-data-source.ts +144 -0
  109. package/src/db-host/data-service.ts +10 -3
  110. package/src/db-host/documents-iterator.ts +3 -1
  111. package/src/db-host/documents-synchronizer.test.ts +77 -13
  112. package/src/db-host/documents-synchronizer.ts +51 -50
  113. package/src/db-host/echo-host.ts +191 -28
  114. package/src/db-host/index.ts +1 -0
  115. package/src/db-host/local-queue-service.ts +127 -0
  116. package/src/db-host/query-service.ts +17 -4
  117. package/src/db-host/queue-data-source.ts +132 -0
  118. package/src/db-host/queue-service.test.ts +141 -0
  119. package/src/db-host/space-state-manager.ts +4 -0
  120. package/src/db-host/stub.ts +28 -0
  121. package/src/edge/echo-edge-replicator.ts +12 -8
  122. package/src/filter/filter-match.test.ts +31 -25
  123. package/src/filter/filter-match.ts +8 -14
  124. package/src/metadata/metadata-store.ts +5 -2
  125. package/src/pipeline/pipeline.ts +2 -2
  126. package/src/query/errors.ts +2 -2
  127. package/src/query/plan.ts +48 -3
  128. package/src/query/query-executor.ts +612 -131
  129. package/src/query/query-planner.test.ts +509 -35
  130. package/src/query/query-planner.ts +187 -13
  131. package/src/space/admission-discovery-extension.ts +1 -1
  132. package/src/space/auth.ts +6 -6
  133. package/src/space/control-pipeline.ts +2 -2
  134. package/src/space/space-manager.ts +6 -6
  135. package/src/space/space-protocol.ts +11 -11
  136. package/src/space/space.ts +3 -3
  137. package/src/testing/index.ts +0 -2
  138. package/src/testing/test-network-adapter.ts +16 -3
  139. package/src/testing/test-replicator.ts +8 -7
  140. package/src/util.ts +7 -3
  141. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  142. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  143. package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
  145. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  146. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  147. package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
  148. package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
  149. package/dist/types/src/testing/test-data.d.ts +0 -18
  150. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  151. package/dist/types/src/testing/test-schema.d.ts +0 -37
  152. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  153. package/src/testing/test-data.ts +0 -127
  154. package/src/testing/test-schema.ts +0 -53
@@ -3,11 +3,23 @@
3
3
  //
4
4
 
5
5
  import type { AutomergeUrl, DocumentId } from '@automerge/automerge-repo';
6
+ import type * as SqlClient from '@effect/sql/SqlClient';
7
+ import type * as Effect from 'effect/Effect';
6
8
  import * as Match from 'effect/Match';
7
- import * as Predicate from 'effect/Predicate';
9
+ import * as Runtime from 'effect/Runtime';
8
10
 
9
11
  import { Context, ContextDisposedError, LifecycleState, Resource } from '@dxos/context';
10
- import { DatabaseDirectory, ObjectStructure, type QueryAST, isEncodedReference } from '@dxos/echo-protocol';
12
+ import type { Obj } from '@dxos/echo';
13
+ import { ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET } from '@dxos/echo/internal';
14
+ import {
15
+ DatabaseDirectory,
16
+ type ObjectPropPath,
17
+ ObjectStructure,
18
+ type QueryAST,
19
+ isEncodedReference,
20
+ } from '@dxos/echo-protocol';
21
+ import { type RuntimeProvider, runAndForwardErrors, unwrapExit } from '@dxos/effect';
22
+ import { type IndexEngine, type ObjectMeta, type ReverseRef } from '@dxos/index-core';
11
23
  import { EscapedPropPath, type FindResult, type Indexer } from '@dxos/indexing';
12
24
  import { invariant } from '@dxos/invariant';
13
25
  import { DXN, type ObjectId, PublicKey, type SpaceId } from '@dxos/keys';
@@ -19,15 +31,15 @@ import { getDeep, isNonNullable } from '@dxos/util';
19
31
  import type { AutomergeHost } from '../automerge';
20
32
  import { createIdFromSpaceKey } from '../common';
21
33
  import type { SpaceStateManager } from '../db-host';
22
- import { filterMatchObject } from '../filter';
34
+ import { filterMatchObject, filterMatchObjectJSON } from '../filter';
23
35
 
24
36
  import type { QueryPlan } from './plan';
25
37
  import { QueryPlanner } from './query-planner';
26
38
 
27
- const isNullable: Predicate.Refinement<unknown, null | undefined> = Predicate.isNullable;
28
-
29
39
  type QueryExecutorOptions = {
30
40
  indexer: Indexer;
41
+ indexer2?: IndexEngine;
42
+ runtime?: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
31
43
  automergeHost: AutomergeHost;
32
44
  spaceStateManager: SpaceStateManager;
33
45
 
@@ -48,11 +60,70 @@ type QueryExecutionResult = {
48
60
  */
49
61
  type QueryItem = {
50
62
  objectId: ObjectId;
51
- documentId: DocumentId;
63
+
52
64
  spaceId: SpaceId;
53
- doc: ObjectStructure;
65
+ queueId: ObjectId | null;
66
+ queueNamespace: string | null;
67
+
68
+ // For objects from automerge documents.
69
+ documentId: DocumentId | null;
70
+
71
+ // For objects from automerge documents.
72
+ doc: ObjectStructure | null;
73
+
74
+ // For objects from queues.
75
+ data: Obj.JSON | null;
76
+
77
+ /**
78
+ * Relevance rank for this item.
79
+ * Higher values indicate better matches for FTS/vector searches.
80
+ * Defaults to 1 for non-ranked queries (predicate matches).
81
+ */
82
+ rank: number;
54
83
  };
55
84
 
85
+ const QueryItem = Object.freeze({
86
+ isDeleted: (item: QueryItem) => {
87
+ if (item.doc) {
88
+ return ObjectStructure.isDeleted(item.doc);
89
+ } else if (item.data) {
90
+ return item.data['@deleted'] === true;
91
+ } else {
92
+ throw new Error('Invalid query item');
93
+ }
94
+ },
95
+
96
+ getProperty: (item: QueryItem, property: ObjectPropPath) => {
97
+ if (item.doc) {
98
+ return getDeep(item.doc.data, property);
99
+ } else if (item.data) {
100
+ return getDeep(item.data, property);
101
+ } else {
102
+ throw new Error('Invalid query item');
103
+ }
104
+ },
105
+
106
+ getRelationSource: (item: QueryItem): DXN.String => {
107
+ if (item.doc) {
108
+ return ObjectStructure.getRelationSource(item.doc)?.['/'] as DXN.String;
109
+ } else if (item.data) {
110
+ return item.data[ATTR_RELATION_SOURCE] as DXN.String;
111
+ } else {
112
+ throw new Error('Invalid query item');
113
+ }
114
+ },
115
+
116
+ getRelationTarget: (item: QueryItem): DXN.String => {
117
+ if (item.doc) {
118
+ return ObjectStructure.getRelationTarget(item.doc)?.['/'] as DXN.String;
119
+ } else if (item.data) {
120
+ return item.data[ATTR_RELATION_TARGET] as DXN.String;
121
+ } else {
122
+ throw new Error('Invalid query item');
123
+ }
124
+ },
125
+ });
126
+
56
127
  /**
57
128
  * Recursive data structure that represents the execution trace of a query.
58
129
  */
@@ -149,6 +220,8 @@ const TRACE_QUERY_EXECUTION = false;
149
220
  */
150
221
  export class QueryExecutor extends Resource {
151
222
  private readonly _indexer: Indexer;
223
+ private readonly _indexer2?: IndexEngine;
224
+ private readonly _runtime?: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
152
225
  private readonly _automergeHost: AutomergeHost;
153
226
  private readonly _spaceStateManager: SpaceStateManager;
154
227
  /**
@@ -167,6 +240,8 @@ export class QueryExecutor extends Resource {
167
240
  super();
168
241
 
169
242
  this._indexer = options.indexer;
243
+ this._indexer2 = options.indexer2;
244
+ this._runtime = options.runtime;
170
245
  this._automergeHost = options.automergeHost;
171
246
  this._spaceStateManager = options.spaceStateManager;
172
247
 
@@ -190,19 +265,18 @@ export class QueryExecutor extends Resource {
190
265
  return this._trace;
191
266
  }
192
267
 
193
- protected override async _open(ctx: Context): Promise<void> {}
194
-
195
- protected override async _close(ctx: Context): Promise<void> {}
196
-
197
268
  getResults(): QueryResult[] {
198
269
  return this._lastResultSet.map(
199
270
  (item): QueryResult => ({
200
271
  id: item.objectId,
201
- documentId: item.documentId,
272
+ documentId: item.documentId ?? undefined,
273
+ queueId: item.queueId ?? undefined,
274
+ queueNamespace: item.queueNamespace ?? undefined,
202
275
  spaceId: item.spaceId,
203
276
 
204
- // TODO(dmaretskyi): Plumb through the rank.
205
- rank: 0,
277
+ rank: item.rank,
278
+
279
+ documentJson: item.doc ? JSON.stringify(item.doc) : item.data ? JSON.stringify(item.data) : undefined,
206
280
  }),
207
281
  );
208
282
  }
@@ -230,6 +304,7 @@ export class QueryExecutor extends Resource {
230
304
  // ExecutionTrace.putOnPerformanceTimeline(trace);
231
305
 
232
306
  if (TRACE_QUERY_EXECUTION) {
307
+ // eslint-disable-next-line no-console
233
308
  console.log(ExecutionTrace.format(trace));
234
309
  }
235
310
 
@@ -240,7 +315,6 @@ export class QueryExecutor extends Resource {
240
315
 
241
316
  private async _execPlan(plan: QueryPlan.Plan, workingSet: QueryItem[]): Promise<StepExecutionResult> {
242
317
  const trace = ExecutionTrace.makeEmpty();
243
- const begin = performance.now();
244
318
  for (const step of plan.steps) {
245
319
  if (this._ctx.disposed) {
246
320
  throw new ContextDisposedError();
@@ -288,6 +362,9 @@ export class QueryExecutor extends Resource {
288
362
  case 'OrderStep':
289
363
  ({ workingSet: newWorkingSet, trace } = await this._execOrderStep(step, workingSet));
290
364
  break;
365
+ case 'LimitStep':
366
+ ({ workingSet: newWorkingSet, trace } = await this._execLimitStep(step, workingSet));
367
+ break;
291
368
  default:
292
369
  throw new Error(`Unknown step type: ${(step as any)._tag}`);
293
370
  }
@@ -309,24 +386,42 @@ export class QueryExecutor extends Resource {
309
386
 
310
387
  switch (step.selector._tag) {
311
388
  case 'WildcardSelector': {
312
- const beginIndexQuery = performance.now();
313
- const indexHits = await this._indexer.execQuery({
314
- typenames: [],
315
- inverted: false,
316
- });
317
- trace.indexHits = +indexHits.length;
318
- trace.indexQueryTime += performance.now() - beginIndexQuery;
389
+ if (this._supportsSqlIndexing()) {
390
+ const beginIndexQuery = performance.now();
391
+ const metas = await this._queryAllFromSqlIndex(step.spaces);
392
+ trace.indexHits = metas.length;
393
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
394
+
395
+ if (this._ctx.disposed) {
396
+ return { workingSet, trace };
397
+ }
319
398
 
320
- if (this._ctx.disposed) {
321
- return { workingSet, trace };
322
- }
399
+ const documentLoadStart = performance.now();
400
+ const results = await this._loadDocumentsAfterSqlQuery(metas);
401
+ trace.documentsLoaded += results.length;
402
+ trace.documentLoadTime += performance.now() - documentLoadStart;
403
+
404
+ workingSet.push(...results.filter(isNonNullable));
405
+ } else {
406
+ const beginIndexQuery = performance.now();
407
+ const indexHits = await this._indexer.execQuery({
408
+ typenames: [],
409
+ inverted: false,
410
+ });
411
+ trace.indexHits = +indexHits.length;
412
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
413
+
414
+ if (this._ctx.disposed) {
415
+ return { workingSet, trace };
416
+ }
323
417
 
324
- const documentLoadStart = performance.now();
325
- const results = await this._loadDocumentsAfterIndexQuery(indexHits);
326
- trace.documentsLoaded += results.length;
327
- trace.documentLoadTime += performance.now() - documentLoadStart;
418
+ const documentLoadStart = performance.now();
419
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
420
+ trace.documentsLoaded += results.length;
421
+ trace.documentLoadTime += performance.now() - documentLoadStart;
328
422
 
329
- workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
423
+ workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
424
+ }
330
425
  trace.objectCount = workingSet.length;
331
426
 
332
427
  break;
@@ -347,57 +442,161 @@ export class QueryExecutor extends Resource {
347
442
  }
348
443
 
349
444
  case 'TypeSelector': {
350
- const beginIndexQuery = performance.now();
351
- const indexHits = await this._indexer.execQuery({
352
- typenames: step.selector.typename,
353
- inverted: step.selector.inverted,
354
- });
355
- trace.indexHits = +indexHits.length;
356
- trace.indexQueryTime += performance.now() - beginIndexQuery;
445
+ if (this._supportsSqlIndexing()) {
446
+ const beginIndexQuery = performance.now();
447
+ const metas = await this._queryTypesFromSqlIndex(step.spaces, step.selector.typename, step.selector.inverted);
448
+ trace.indexHits = metas.length;
449
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
450
+
451
+ if (this._ctx.disposed) {
452
+ return { workingSet, trace };
453
+ }
357
454
 
358
- if (this._ctx.disposed) {
359
- return { workingSet, trace };
360
- }
455
+ const documentLoadStart = performance.now();
456
+ const results = await this._loadDocumentsAfterSqlQuery(metas);
457
+ trace.documentsLoaded += results.length;
458
+ trace.documentLoadTime += performance.now() - documentLoadStart;
459
+
460
+ workingSet.push(...results.filter(isNonNullable));
461
+ } else {
462
+ const beginIndexQuery = performance.now();
463
+ const indexHits = await this._indexer.execQuery({
464
+ typenames: step.selector.typename,
465
+ inverted: step.selector.inverted,
466
+ });
467
+ trace.indexHits = +indexHits.length;
468
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
469
+
470
+ if (this._ctx.disposed) {
471
+ return { workingSet, trace };
472
+ }
361
473
 
362
- const documentLoadStart = performance.now();
363
- const results = await this._loadDocumentsAfterIndexQuery(indexHits);
364
- trace.documentsLoaded += results.length;
365
- trace.documentLoadTime += performance.now() - documentLoadStart;
474
+ const documentLoadStart = performance.now();
475
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
476
+ trace.documentsLoaded += results.length;
477
+ trace.documentLoadTime += performance.now() - documentLoadStart;
366
478
 
367
- workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
479
+ workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
480
+ }
368
481
  trace.objectCount = workingSet.length;
369
482
 
370
483
  break;
371
484
  }
372
485
 
373
486
  case 'TextSelector': {
374
- const beginIndexQuery = performance.now();
375
- const indexHits = await this._indexer.execQuery({
376
- typenames: [],
377
- text: {
378
- query: step.selector.text,
379
- kind: Match.type<QueryPlan.TextSearchKind>().pipe(
380
- Match.withReturnType<'text' | 'vector'>(),
381
- Match.when('full-text', () => 'text'),
382
- Match.when('vector', () => 'vector'),
383
- Match.orElseAbsurd,
384
- )(step.selector.searchKind),
385
- },
386
- });
387
- trace.indexHits = +indexHits.length;
388
- trace.indexQueryTime += performance.now() - beginIndexQuery;
487
+ // TODO(dmaretskyi): type + FTS queries would be very common so we should support those, maybe chunk the fts index.
488
+ // TODO(dmaretskyi): nice to have matched text snippets/highlighting.
489
+ if (step.selector.searchKind === 'full-text' && this._supportsSqlIndexing()) {
490
+ // Use indexer2 for full-text search.
491
+ const beginIndexQuery = performance.now();
492
+ invariant(this._indexer2, 'SQL indexer is required.');
493
+ invariant(step.spaces.length <= 1, 'Multiple spaces are not supported for full-text search');
494
+ // Extract queue IDs from DXN strings.
495
+ const queueIds =
496
+ step.queues.length > 0
497
+ ? (step.queues.map((dxnStr) => DXN.parse(dxnStr).asQueueDXN()?.queueId).filter(Boolean) as ObjectId[])
498
+ : null;
499
+ const textResults = await this._runInRuntime(
500
+ this._indexer2.queryText({
501
+ query: step.selector.text,
502
+ spaceId: step.spaces,
503
+ includeAllQueues: step.allQueuesFromSpaces,
504
+ queueIds,
505
+ }),
506
+ );
507
+ trace.indexHits = textResults.length;
508
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
509
+
510
+ if (this._ctx.disposed) {
511
+ return { workingSet, trace };
512
+ }
389
513
 
390
- if (this._ctx.disposed) {
391
- return { workingSet, trace };
392
- }
514
+ // Load documents from the results.
515
+ const documentLoadStart = performance.now();
393
516
 
394
- const documentLoadStart = performance.now();
395
- const results = await this._loadDocumentsAfterIndexQuery(indexHits);
396
- trace.documentsLoaded += results.length;
397
- trace.documentLoadTime += performance.now() - documentLoadStart;
517
+ // Separate queue items from space items.
518
+ const queueResults = textResults.filter((r) => r.queueId);
519
+ const spaceResults = textResults.filter((r) => !r.queueId);
398
520
 
399
- workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
400
- trace.objectCount = workingSet.length;
521
+ // Build a map from recordId to rank for all FTS results.
522
+ const rankMap = new Map(textResults.map((r) => [r.recordId, r.rank]));
523
+
524
+ // Load queue items from indexed snapshots.
525
+ let queueItems: QueryItem[] = [];
526
+ if (queueResults.length > 0) {
527
+ const snapshots = await this._runInRuntime(
528
+ this._indexer2.querySnapshotsJSON(queueResults.map((r) => r.recordId)),
529
+ );
530
+ const snapshotMap = new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
531
+ queueItems = queueResults
532
+ .map((result): QueryItem | null => {
533
+ const snapshot = snapshotMap.get(result.recordId);
534
+ if (!snapshot || typeof snapshot !== 'object') {
535
+ return null;
536
+ }
537
+ return {
538
+ objectId: result.objectId as ObjectId,
539
+ spaceId: result.spaceId as SpaceId,
540
+ queueId: result.queueId as ObjectId,
541
+ queueNamespace: 'data',
542
+ documentId: null,
543
+ doc: null,
544
+ data: snapshot as Obj.JSON,
545
+ rank: rankMap.get(result.recordId) ?? 1,
546
+ };
547
+ })
548
+ .filter(isNonNullable);
549
+ }
550
+
551
+ // Load space items from documents.
552
+ const spaceItems = await Promise.all(
553
+ spaceResults.map(async (result): Promise<QueryItem | null> => {
554
+ const dxn = DXN.fromLocalObjectId(result.objectId);
555
+ const item = await this._loadFromDXN(dxn, { sourceSpaceId: result.spaceId as SpaceId });
556
+ if (item) {
557
+ // Override the default rank with the FTS rank.
558
+ item.rank = rankMap.get(result.recordId) ?? 1;
559
+ }
560
+ return item;
561
+ }),
562
+ );
563
+
564
+ const items = [...queueItems, ...spaceItems.filter(isNonNullable)];
565
+ trace.documentsLoaded += items.length;
566
+ trace.documentLoadTime += performance.now() - documentLoadStart;
567
+
568
+ workingSet.push(...items.filter((item) => step.spaces.includes(item.spaceId)));
569
+ trace.objectCount = workingSet.length;
570
+ } else {
571
+ // Fall back to old indexer for vector search.
572
+ const beginIndexQuery = performance.now();
573
+ const indexHits = await this._indexer.execQuery({
574
+ typenames: [],
575
+ text: {
576
+ query: step.selector.text,
577
+ kind: Match.type<QueryPlan.TextSearchKind>().pipe(
578
+ Match.withReturnType<'text' | 'vector'>(),
579
+ Match.when('full-text', () => 'text'),
580
+ Match.when('vector', () => 'vector'),
581
+ Match.orElseAbsurd,
582
+ )(step.selector.searchKind),
583
+ },
584
+ });
585
+ trace.indexHits = +indexHits.length;
586
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
587
+
588
+ if (this._ctx.disposed) {
589
+ return { workingSet, trace };
590
+ }
591
+
592
+ const documentLoadStart = performance.now();
593
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
594
+ trace.documentsLoaded += results.length;
595
+ trace.documentLoadTime += performance.now() - documentLoadStart;
596
+
597
+ workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
598
+ trace.objectCount = workingSet.length;
599
+ }
401
600
  break;
402
601
  }
403
602
 
@@ -405,17 +604,29 @@ export class QueryExecutor extends Resource {
405
604
  throw new Error(`Unknown selector type: ${(step.selector as any)._tag}`);
406
605
  }
407
606
 
607
+ // Apply limit if specified on the select step.
608
+ if (step.limit !== undefined && workingSet.length > step.limit) {
609
+ workingSet = workingSet.slice(0, step.limit);
610
+ trace.objectCount = workingSet.length;
611
+ }
612
+
408
613
  return { workingSet, trace };
409
614
  }
410
615
 
411
616
  private async _execFilterStep(step: QueryPlan.FilterStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
412
- const result = workingSet.filter((item) =>
413
- filterMatchObject(step.filter, {
414
- id: item.objectId,
415
- spaceId: item.spaceId,
416
- doc: item.doc,
417
- }),
418
- );
617
+ const result = workingSet.filter((item) => {
618
+ if (item.doc) {
619
+ return filterMatchObject(step.filter, {
620
+ id: item.objectId,
621
+ spaceId: item.spaceId,
622
+ doc: item.doc,
623
+ });
624
+ } else if (item.data) {
625
+ return filterMatchObjectJSON(step.filter, item.data);
626
+ } else {
627
+ return false;
628
+ }
629
+ });
419
630
 
420
631
  return {
421
632
  workingSet: result,
@@ -433,7 +644,8 @@ export class QueryExecutor extends Resource {
433
644
  workingSet: QueryItem[],
434
645
  ): Promise<StepExecutionResult> {
435
646
  const expected = step.mode === 'only-deleted';
436
- const result = workingSet.filter((item) => ObjectStructure.isDeleted(item.doc) === expected);
647
+ // TODO(dmaretskyi): How do we handle items with parents and cascade deletions? -- perhaps we forbid queue items from having parents -- i.e. queue is their parent.
648
+ const result = workingSet.filter((item) => QueryItem.isDeleted(item) === expected);
437
649
  return {
438
650
  workingSet: result,
439
651
  trace: {
@@ -459,11 +671,12 @@ export class QueryExecutor extends Resource {
459
671
  case 'ReferenceTraversal': {
460
672
  switch (step.traversal.direction) {
461
673
  case 'outgoing': {
674
+ invariant(step.traversal.property !== null, 'Outgoing reference traversal requires a property');
462
675
  const property = EscapedPropPath.unescape(step.traversal.property);
463
676
 
464
677
  const refs = workingSet
465
678
  .flatMap((item) => {
466
- const ref = getDeep(item.doc.data, property);
679
+ const ref = QueryItem.getProperty(item, property);
467
680
  const refs = Array.isArray(ref) ? ref : [ref];
468
681
  return refs.map((ref) => {
469
682
  try {
@@ -493,24 +706,39 @@ export class QueryExecutor extends Resource {
493
706
  break;
494
707
  }
495
708
  case 'incoming': {
496
- const indexHits = await this._indexer.execQuery({
497
- typenames: [],
498
- inverted: false,
499
- graph: {
500
- kind: 'inbound-reference',
501
- property: step.traversal.property,
502
- anchors: workingSet.map((item) => item.objectId),
503
- },
504
- });
505
- trace.indexHits += indexHits.length;
506
-
507
- const documentLoadStart = performance.now();
508
- const results = await this._loadDocumentsAfterIndexQuery(indexHits);
509
- trace.documentsLoaded += results.length;
510
- trace.documentLoadTime += performance.now() - documentLoadStart;
511
-
512
- newWorkingSet.push(...results.filter(isNonNullable));
513
- trace.objectCount = newWorkingSet.length;
709
+ if (this._supportsSqlIndexing()) {
710
+ const beginIndexQuery = performance.now();
711
+ const metas = await this._queryIncomingReferencesFromSqlIndex(workingSet, step.traversal.property);
712
+ trace.indexHits += metas.length;
713
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
714
+
715
+ const documentLoadStart = performance.now();
716
+ const results = await this._loadDocumentsAfterSqlQuery(metas);
717
+ trace.documentsLoaded += results.length;
718
+ trace.documentLoadTime += performance.now() - documentLoadStart;
719
+
720
+ newWorkingSet.push(...results.filter(isNonNullable));
721
+ trace.objectCount = newWorkingSet.length;
722
+ } else {
723
+ const indexHits = await this._indexer.execQuery({
724
+ typenames: [],
725
+ inverted: false,
726
+ graph: {
727
+ kind: 'inbound-reference',
728
+ property: step.traversal.property,
729
+ anchors: workingSet.map((item) => item.objectId),
730
+ },
731
+ });
732
+ trace.indexHits += indexHits.length;
733
+
734
+ const documentLoadStart = performance.now();
735
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
736
+ trace.documentsLoaded += results.length;
737
+ trace.documentLoadTime += performance.now() - documentLoadStart;
738
+
739
+ newWorkingSet.push(...results.filter(isNonNullable));
740
+ trace.objectCount = newWorkingSet.length;
741
+ }
514
742
 
515
743
  break;
516
744
  }
@@ -523,21 +751,17 @@ export class QueryExecutor extends Resource {
523
751
  case 'relation-to-target': {
524
752
  const refs = workingSet
525
753
  .map((item) => {
526
- const ref =
754
+ const dxn =
527
755
  step.traversal.direction === 'relation-to-source'
528
- ? ObjectStructure.getRelationSource(item.doc)
529
- : ObjectStructure.getRelationTarget(item.doc);
530
-
531
- if (!isEncodedReference(ref)) {
532
- return null;
533
- }
756
+ ? QueryItem.getRelationSource(item)
757
+ : QueryItem.getRelationTarget(item);
534
758
  try {
535
759
  return {
536
- ref: DXN.parse(ref['/']),
760
+ ref: DXN.parse(dxn),
537
761
  spaceId: item.spaceId,
538
762
  };
539
763
  } catch {
540
- log.warn('invalid reference', { ref: ref['/'] });
764
+ log.warn('invalid reference', { ref: dxn });
541
765
  return null;
542
766
  }
543
767
  })
@@ -557,25 +781,43 @@ export class QueryExecutor extends Resource {
557
781
 
558
782
  case 'source-to-relation':
559
783
  case 'target-to-relation': {
560
- const indexHits = await this._indexer.execQuery({
561
- typenames: [],
562
- inverted: false,
563
- graph: {
564
- kind: step.traversal.direction === 'source-to-relation' ? 'relation-source' : 'relation-target',
565
- anchors: workingSet.map((item) => item.objectId),
566
- property: null,
567
- },
568
- });
569
-
570
- trace.indexHits += indexHits.length;
571
-
572
- const documentLoadStart = performance.now();
573
- const results = await this._loadDocumentsAfterIndexQuery(indexHits);
574
- trace.documentsLoaded += results.length;
575
- trace.documentLoadTime += performance.now() - documentLoadStart;
576
-
577
- newWorkingSet.push(...results.filter(isNonNullable));
578
- trace.objectCount = newWorkingSet.length;
784
+ if (this._supportsSqlIndexing()) {
785
+ const beginIndexQuery = performance.now();
786
+ const metas = await this._queryRelationsFromSqlIndex(
787
+ workingSet,
788
+ step.traversal.direction === 'source-to-relation' ? 'source' : 'target',
789
+ );
790
+ trace.indexHits += metas.length;
791
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
792
+
793
+ const documentLoadStart = performance.now();
794
+ const results = await this._loadDocumentsAfterSqlQuery(metas);
795
+ trace.documentsLoaded += results.length;
796
+ trace.documentLoadTime += performance.now() - documentLoadStart;
797
+
798
+ newWorkingSet.push(...results.filter(isNonNullable));
799
+ trace.objectCount = newWorkingSet.length;
800
+ } else {
801
+ const indexHits = await this._indexer.execQuery({
802
+ typenames: [],
803
+ inverted: false,
804
+ graph: {
805
+ kind: step.traversal.direction === 'source-to-relation' ? 'relation-source' : 'relation-target',
806
+ anchors: workingSet.map((item) => item.objectId),
807
+ property: null,
808
+ },
809
+ });
810
+
811
+ trace.indexHits += indexHits.length;
812
+
813
+ const documentLoadStart = performance.now();
814
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
815
+ trace.documentsLoaded += results.length;
816
+ trace.documentLoadTime += performance.now() - documentLoadStart;
817
+
818
+ newWorkingSet.push(...results.filter(isNonNullable));
819
+ trace.objectCount = newWorkingSet.length;
820
+ }
579
821
 
580
822
  break;
581
823
  }
@@ -637,19 +879,38 @@ export class QueryExecutor extends Resource {
637
879
  }
638
880
 
639
881
  private async _execOrderStep(step: QueryPlan.OrderStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
640
- const sortedWorkingSet = [...workingSet].sort((a, b) => this._compareMultiOrder(a, b, step.order));
882
+ let sortedWorkingSet = [...workingSet].sort((a, b) => this._compareMultiOrder(a, b, step.order));
883
+
884
+ // Apply limit if specified on the order step.
885
+ if (step.limit !== undefined && sortedWorkingSet.length > step.limit) {
886
+ sortedWorkingSet = sortedWorkingSet.slice(0, step.limit);
887
+ }
641
888
 
642
889
  return {
643
890
  workingSet: sortedWorkingSet,
644
891
  trace: {
645
892
  ...ExecutionTrace.makeEmpty(),
646
893
  name: 'Order',
647
- details: JSON.stringify(step.order),
894
+ details: JSON.stringify({ order: step.order, limit: step.limit }),
648
895
  objectCount: sortedWorkingSet.length,
649
896
  },
650
897
  };
651
898
  }
652
899
 
900
+ private async _execLimitStep(step: QueryPlan.LimitStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
901
+ const limitedWorkingSet = workingSet.slice(0, step.limit);
902
+
903
+ return {
904
+ workingSet: limitedWorkingSet,
905
+ trace: {
906
+ ...ExecutionTrace.makeEmpty(),
907
+ name: 'Limit',
908
+ details: JSON.stringify({ limit: step.limit }),
909
+ objectCount: limitedWorkingSet.length,
910
+ },
911
+ };
912
+ }
913
+
653
914
  private _compareMultiOrder(a: QueryItem, b: QueryItem, orders: readonly QueryAST.Order[]): number {
654
915
  // Short circuit for common cases.
655
916
  if (orders.length === 0) {
@@ -675,15 +936,20 @@ export class QueryExecutor extends Resource {
675
936
  const comparison = this._compareByProperty(a, b, order.property);
676
937
  return order.direction === 'desc' ? -comparison : comparison;
677
938
  }
939
+ case 'rank': {
940
+ // Higher rank = better match. By default, descending order (best first).
941
+ const comparison = a.rank - b.rank;
942
+ return order.direction === 'desc' ? -comparison : comparison;
943
+ }
678
944
  default:
679
- // Should never reach here with proper TypeScript types
945
+ // Should never reach here with proper TypeScript types.
680
946
  return 0;
681
947
  }
682
948
  }
683
949
 
684
950
  private _compareByProperty(a: QueryItem, b: QueryItem, property: string): number {
685
- const aValue = a.doc.data[property];
686
- const bValue = b.doc.data[property];
951
+ const aValue = QueryItem.getProperty(a, [property]);
952
+ const bValue = QueryItem.getProperty(b, [property]);
687
953
 
688
954
  // Both null or undefined
689
955
  if (aValue == null && bValue == null) {
@@ -727,6 +993,205 @@ export class QueryExecutor extends Resource {
727
993
  );
728
994
  }
729
995
 
996
+ private _supportsSqlIndexing(): boolean {
997
+ return !!this._indexer2 && !!this._runtime;
998
+ }
999
+
1000
+ private async _runInRuntime<T>(effect: Effect.Effect<T, unknown, SqlClient.SqlClient>): Promise<T> {
1001
+ const runtimeProvider = this._runtime;
1002
+ invariant(runtimeProvider, 'SQL runtime is required.');
1003
+ const runtime = await runAndForwardErrors(runtimeProvider);
1004
+ return await unwrapExit(await effect.pipe(Runtime.runPromiseExit(runtime)));
1005
+ }
1006
+
1007
+ private async _queryAllFromSqlIndex(spaceIds: readonly SpaceId[]): Promise<readonly ObjectMeta[]> {
1008
+ invariant(this._indexer2, 'SQL indexer is required.');
1009
+ return await this._runInRuntime(this._indexer2.queryAll({ spaceIds }));
1010
+ }
1011
+
1012
+ private async _queryTypesFromSqlIndex(
1013
+ spaceIds: readonly SpaceId[],
1014
+ typeDxns: readonly string[],
1015
+ inverted: boolean,
1016
+ ): Promise<readonly ObjectMeta[]> {
1017
+ invariant(this._indexer2, 'SQL indexer is required.');
1018
+ return await this._runInRuntime(this._indexer2.queryTypes({ spaceIds, typeDxns, inverted }));
1019
+ }
1020
+
1021
+ private async _queryIncomingReferencesFromSqlIndex(
1022
+ workingSet: QueryItem[],
1023
+ property: EscapedPropPath | null,
1024
+ ): Promise<readonly ObjectMeta[]> {
1025
+ const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
1026
+ invariant(this._indexer2, 'SQL indexer is required.');
1027
+ const rows: readonly ReverseRef[] = (
1028
+ await Promise.all(
1029
+ anchorDxns.map((targetDxn) => this._runInRuntime(this._indexer2!.queryReverseRef({ targetDxn }))),
1030
+ )
1031
+ ).flat();
1032
+
1033
+ const recordIds = rows
1034
+ .filter((row) => {
1035
+ if (property === null) {
1036
+ return true;
1037
+ }
1038
+
1039
+ const queryPath = EscapedPropPath.unescape(property);
1040
+ const rowPath = EscapedPropPath.unescape(row.propPath);
1041
+ return QueryExecutor._matchesReferencePropertyPath(rowPath, queryPath);
1042
+ })
1043
+ .map((row) => row.recordId);
1044
+
1045
+ const uniqueRecordIds = Array.from(new Set<number>(recordIds));
1046
+ return await this._runInRuntime(this._indexer2.lookupByRecordIds(uniqueRecordIds));
1047
+ }
1048
+
1049
+ /**
1050
+ * Matches a reverse-reference row path against a query property path.
1051
+ * Allows numeric segments in the row path (array indices) that are not present in the query.
1052
+ *
1053
+ * Examples:
1054
+ * - query: ['assignee'] matches row: ['assignee'] and ['assignee', '0'].
1055
+ * - query: ['items', 'assignee'] matches row: ['items', '0', 'assignee'].
1056
+ * - query: ['a', 'b'] does NOT match row: ['a'].
1057
+ * - query: ['a'] does NOT match row: ['a', 'b'].
1058
+ */
1059
+ private static _matchesReferencePropertyPath(rowPath: readonly string[], queryPath: readonly string[]): boolean {
1060
+ const isNumericSegment = (segment: string) => /^[0-9]+$/.test(segment);
1061
+
1062
+ let i = 0; // queryPath index.
1063
+ let j = 0; // rowPath index.
1064
+
1065
+ while (i < queryPath.length && j < rowPath.length) {
1066
+ if (rowPath[j] === queryPath[i]) {
1067
+ i++;
1068
+ j++;
1069
+ continue;
1070
+ }
1071
+
1072
+ // Row may contain array indices that aren't present in the query path.
1073
+ if (isNumericSegment(rowPath[j])) {
1074
+ j++;
1075
+ continue;
1076
+ }
1077
+
1078
+ return false;
1079
+ }
1080
+
1081
+ // Must consume full query path.
1082
+ if (i !== queryPath.length) {
1083
+ return false;
1084
+ }
1085
+
1086
+ // Any remaining row segments must be numeric (array indices).
1087
+ for (; j < rowPath.length; j++) {
1088
+ if (!isNumericSegment(rowPath[j])) {
1089
+ return false;
1090
+ }
1091
+ }
1092
+
1093
+ return true;
1094
+ }
1095
+
1096
+ private async _queryRelationsFromSqlIndex(
1097
+ workingSet: QueryItem[],
1098
+ endpoint: 'source' | 'target',
1099
+ ): Promise<readonly ObjectMeta[]> {
1100
+ invariant(this._indexer2, 'SQL indexer is required.');
1101
+ const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
1102
+ return await this._runInRuntime(this._indexer2.queryRelations({ endpoint, anchorDxns }));
1103
+ }
1104
+
1105
+ private async _loadDocumentsAfterSqlQuery(metas: readonly ObjectMeta[]): Promise<(QueryItem | null)[]> {
1106
+ const snapshotMap = await this._loadQueueSnapshotMap(metas);
1107
+ return await Promise.all(
1108
+ metas.map(async (meta) => {
1109
+ // Branch 1: Document-backed object.
1110
+ if (meta.documentId) {
1111
+ return this._loadFromAutomerge(meta);
1112
+ }
1113
+
1114
+ // Branch 2: Queue-backed object.
1115
+ if (meta.queueId) {
1116
+ return this._loadFromQueue(meta, snapshotMap);
1117
+ }
1118
+
1119
+ return null;
1120
+ }),
1121
+ );
1122
+ }
1123
+
1124
+ private async _loadQueueSnapshotMap(metas: readonly ObjectMeta[]): Promise<Map<number, unknown>> {
1125
+ const queueMetas = metas.filter((meta) => !meta.documentId && !!meta.queueId);
1126
+ if (queueMetas.length === 0) {
1127
+ return new Map();
1128
+ }
1129
+
1130
+ invariant(this._indexer2, 'SQL indexer is required.');
1131
+ const snapshots = await this._runInRuntime(
1132
+ this._indexer2.querySnapshotsJSON(queueMetas.map((meta) => meta.recordId)),
1133
+ );
1134
+ return new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
1135
+ }
1136
+
1137
+ /**
1138
+ * Loads a queue-backed object from an indexed snapshot (by `recordId`).
1139
+ * Returns `null` when the snapshot is missing or is not a JSON object.
1140
+ */
1141
+ private _loadFromQueue(meta: ObjectMeta, snapshotMap: Map<number, unknown>): QueryItem | null {
1142
+ const snapshot = snapshotMap.get(meta.recordId);
1143
+ if (!snapshot || typeof snapshot !== 'object') {
1144
+ return null;
1145
+ }
1146
+
1147
+ return {
1148
+ objectId: meta.objectId as ObjectId,
1149
+ spaceId: meta.spaceId as SpaceId,
1150
+ queueId: meta.queueId as ObjectId,
1151
+ queueNamespace: 'data',
1152
+ documentId: null,
1153
+ doc: null,
1154
+ data: snapshot as Obj.JSON,
1155
+ rank: 1,
1156
+ };
1157
+ }
1158
+
1159
+ /**
1160
+ * Loads a document-backed object from an Automerge `DatabaseDirectory`.
1161
+ * Returns `null` if the document can't be loaded, the inline object isn't present, or if the meta does not have a
1162
+ * document id (e.g. queue-backed objects).
1163
+ */
1164
+ private async _loadFromAutomerge(meta: ObjectMeta): Promise<QueryItem | null> {
1165
+ if (!meta.documentId) {
1166
+ return null;
1167
+ }
1168
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(
1169
+ Context.default(),
1170
+ meta.documentId as DocumentId,
1171
+ {
1172
+ fetchFromNetwork: true,
1173
+ },
1174
+ );
1175
+ const doc = handle.doc();
1176
+ if (!doc) {
1177
+ return null;
1178
+ }
1179
+ const object = DatabaseDirectory.getInlineObject(doc, meta.objectId);
1180
+ if (!object) {
1181
+ return null;
1182
+ }
1183
+ return {
1184
+ objectId: meta.objectId,
1185
+ documentId: meta.documentId as DocumentId,
1186
+ spaceId: meta.spaceId as SpaceId,
1187
+ queueId: null,
1188
+ queueNamespace: null,
1189
+ doc: object,
1190
+ data: null,
1191
+ rank: 1,
1192
+ };
1193
+ }
1194
+
730
1195
  /**
731
1196
  * Space key hex -> SpaceId.
732
1197
  */
@@ -735,7 +1200,9 @@ export class QueryExecutor extends Resource {
735
1200
  private async _loadFromIndexHit(hit: FindResult): Promise<QueryItem | null> {
736
1201
  const { objectId, documentId, spaceKey: spaceKeyInIndex } = objectPointerCodec.decode(hit.id);
737
1202
 
738
- const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), documentId as DocumentId);
1203
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), documentId as DocumentId, {
1204
+ fetchFromNetwork: true,
1205
+ });
739
1206
  const doc = handle.doc();
740
1207
  if (!doc) {
741
1208
  return null;
@@ -761,7 +1228,11 @@ export class QueryExecutor extends Resource {
761
1228
  objectId,
762
1229
  documentId: documentId as DocumentId,
763
1230
  spaceId,
1231
+ queueId: null,
1232
+ queueNamespace: null,
1233
+ data: null,
764
1234
  doc: object,
1235
+ rank: hit.rank,
765
1236
  };
766
1237
  }
767
1238
 
@@ -791,7 +1262,11 @@ export class QueryExecutor extends Resource {
791
1262
  objectId: echoDxn.echoId,
792
1263
  documentId: spaceRoot.documentId,
793
1264
  spaceId,
1265
+ queueId: null,
1266
+ queueNamespace: null,
1267
+ data: null,
794
1268
  doc: inlineObject,
1269
+ rank: 1,
795
1270
  };
796
1271
  }
797
1272
 
@@ -800,7 +1275,9 @@ export class QueryExecutor extends Resource {
800
1275
  return null;
801
1276
  }
802
1277
 
803
- const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl);
1278
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl, {
1279
+ fetchFromNetwork: true,
1280
+ });
804
1281
  const doc = handle.doc();
805
1282
  if (!doc) {
806
1283
  return null;
@@ -815,7 +1292,11 @@ export class QueryExecutor extends Resource {
815
1292
  objectId: echoDxn.echoId,
816
1293
  documentId: handle.documentId,
817
1294
  spaceId,
1295
+ queueId: null,
1296
+ queueNamespace: null,
1297
+ data: null,
818
1298
  doc: object,
1299
+ rank: 1,
819
1300
  };
820
1301
  }
821
1302
  }