@dxos/echo-pipeline 0.8.4-main.84f28bd → 0.8.4-main.a4bbb77

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 (135) hide show
  1. package/dist/lib/browser/chunk-BVR7OOZK.mjs +251 -0
  2. package/dist/lib/browser/chunk-BVR7OOZK.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-GBFX5J5B.mjs → chunk-XGG76KKU.mjs} +487 -324
  4. package/dist/lib/browser/chunk-XGG76KKU.mjs.map +7 -0
  5. package/dist/lib/browser/filter/index.mjs +3 -1
  6. package/dist/lib/browser/index.mjs +1379 -562
  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 +107 -44
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/lib/node-esm/{chunk-FQFKWA3X.mjs → chunk-CHMJJ4DG.mjs} +487 -324
  12. package/dist/lib/node-esm/chunk-CHMJJ4DG.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-L3ZEKVDR.mjs +251 -0
  14. package/dist/lib/node-esm/chunk-L3ZEKVDR.mjs.map +7 -0
  15. package/dist/lib/node-esm/filter/index.mjs +3 -1
  16. package/dist/lib/node-esm/index.mjs +1379 -562
  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 +107 -44
  20. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  21. package/dist/types/src/automerge/automerge-host.d.ts +15 -28
  22. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  23. package/dist/types/src/automerge/collection-synchronizer.d.ts +1 -1
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  25. package/dist/types/src/automerge/echo-network-adapter.d.ts +8 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/echo-replicator.d.ts +21 -2
  28. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  29. package/dist/types/src/automerge/index.d.ts +1 -1
  30. package/dist/types/src/automerge/index.d.ts.map +1 -1
  31. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +1 -1
  32. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  33. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +1 -0
  34. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  35. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  36. package/dist/types/src/common/codec.d.ts +1 -1
  37. package/dist/types/src/common/codec.d.ts.map +1 -1
  38. package/dist/types/src/db-host/data-service.d.ts +2 -2
  39. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  40. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  41. package/dist/types/src/db-host/documents-synchronizer.d.ts +2 -2
  42. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  43. package/dist/types/src/db-host/echo-host.d.ts +2 -2
  44. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  45. package/dist/types/src/db-host/query-service.d.ts +1 -1
  46. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  47. package/dist/types/src/db-host/space-state-manager.d.ts +1 -1
  48. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  49. package/dist/types/src/edge/echo-edge-replicator.d.ts +4 -2
  50. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  51. package/dist/types/src/filter/filter-match.d.ts +4 -1
  52. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  53. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  54. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  55. package/dist/types/src/pipeline/pipeline.d.ts +1 -1
  56. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  57. package/dist/types/src/query/errors.d.ts +24 -8
  58. package/dist/types/src/query/errors.d.ts.map +1 -1
  59. package/dist/types/src/query/plan.d.ts +8 -1
  60. package/dist/types/src/query/plan.d.ts.map +1 -1
  61. package/dist/types/src/query/query-executor.d.ts +4 -1
  62. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  63. package/dist/types/src/query/query-planner.d.ts +2 -0
  64. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  65. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  66. package/dist/types/src/space/control-pipeline.d.ts +1 -1
  67. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  68. package/dist/types/src/space/space-manager.d.ts +1 -1
  69. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  70. package/dist/types/src/space/space-protocol.d.ts +1 -1
  71. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  72. package/dist/types/src/space/space.d.ts +1 -1
  73. package/dist/types/src/space/space.d.ts.map +1 -1
  74. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  75. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  76. package/dist/types/src/testing/test-replicator.d.ts +1 -0
  77. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  78. package/dist/types/src/util.d.ts +1 -1
  79. package/dist/types/src/util.d.ts.map +1 -1
  80. package/dist/types/tsconfig.tsbuildinfo +1 -1
  81. package/package.json +41 -38
  82. package/src/automerge/automerge-host.test.ts +18 -8
  83. package/src/automerge/automerge-host.ts +266 -70
  84. package/src/automerge/automerge-repo.test.ts +67 -16
  85. package/src/automerge/collection-synchronizer.test.ts +2 -2
  86. package/src/automerge/collection-synchronizer.ts +4 -4
  87. package/src/automerge/echo-data-monitor.ts +1 -1
  88. package/src/automerge/echo-network-adapter.test.ts +3 -3
  89. package/src/automerge/echo-network-adapter.ts +38 -6
  90. package/src/automerge/echo-replicator.ts +23 -2
  91. package/src/automerge/index.ts +1 -1
  92. package/src/automerge/leveldb-storage-adapter.ts +7 -7
  93. package/src/automerge/mesh-echo-replicator-connection.ts +4 -0
  94. package/src/automerge/mesh-echo-replicator.ts +2 -1
  95. package/src/automerge/storage-adapter.test.ts +1 -1
  96. package/src/common/space-id.ts +1 -1
  97. package/src/db-host/data-service.ts +9 -17
  98. package/src/db-host/database-root.ts +2 -2
  99. package/src/db-host/documents-synchronizer.test.ts +1 -1
  100. package/src/db-host/documents-synchronizer.ts +39 -26
  101. package/src/db-host/echo-host.ts +14 -15
  102. package/src/db-host/query-service.ts +8 -1
  103. package/src/db-host/space-state-manager.ts +2 -2
  104. package/src/edge/echo-edge-replicator.test.ts +5 -3
  105. package/src/edge/echo-edge-replicator.ts +75 -18
  106. package/src/filter/filter-match.test.ts +33 -13
  107. package/src/filter/filter-match.ts +177 -10
  108. package/src/metadata/metadata-store.ts +3 -3
  109. package/src/pipeline/pipeline-stress.test.ts +4 -2
  110. package/src/pipeline/pipeline.test.ts +3 -2
  111. package/src/pipeline/pipeline.ts +8 -5
  112. package/src/query/errors.ts +2 -0
  113. package/src/query/plan.ts +12 -1
  114. package/src/query/query-executor.ts +71 -11
  115. package/src/query/query-planner.test.ts +146 -2
  116. package/src/query/query-planner.ts +81 -11
  117. package/src/space/admission-discovery-extension.ts +2 -2
  118. package/src/space/control-pipeline.test.ts +4 -3
  119. package/src/space/control-pipeline.ts +9 -6
  120. package/src/space/space-manager.browser.test.ts +1 -1
  121. package/src/space/space-manager.ts +5 -4
  122. package/src/space/space-protocol.browser.test.ts +2 -2
  123. package/src/space/space-protocol.test.ts +3 -2
  124. package/src/space/space-protocol.ts +6 -3
  125. package/src/space/space.test.ts +1 -1
  126. package/src/space/space.ts +3 -2
  127. package/src/testing/test-agent-builder.ts +4 -3
  128. package/src/testing/test-replicator.ts +4 -0
  129. package/src/util.ts +1 -1
  130. package/dist/lib/browser/chunk-ANZAS5CC.mjs +0 -126
  131. package/dist/lib/browser/chunk-ANZAS5CC.mjs.map +0 -7
  132. package/dist/lib/browser/chunk-GBFX5J5B.mjs.map +0 -7
  133. package/dist/lib/node-esm/chunk-2SAZ7CCF.mjs +0 -126
  134. package/dist/lib/node-esm/chunk-2SAZ7CCF.mjs.map +0 -7
  135. package/dist/lib/node-esm/chunk-FQFKWA3X.mjs.map +0 -7
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import type { AutomergeUrl, DocumentId } from '@automerge/automerge-repo';
6
- import { Match } from 'effect';
6
+ import { Match, Predicate } from 'effect';
7
7
 
8
8
  import { Context, ContextDisposedError, LifecycleState, Resource } from '@dxos/context';
9
- import { DatabaseDirectory, isEncodedReference, ObjectStructure, type QueryAST } from '@dxos/echo-protocol';
9
+ import { DatabaseDirectory, ObjectStructure, type QueryAST, isEncodedReference } from '@dxos/echo-protocol';
10
10
  import { EscapedPropPath, type FindResult, type Indexer } from '@dxos/indexing';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { DXN, type ObjectId, PublicKey, type SpaceId } from '@dxos/keys';
@@ -15,13 +15,16 @@ import { objectPointerCodec } from '@dxos/protocols';
15
15
  import { type QueryReactivity, type QueryResult } from '@dxos/protocols/proto/dxos/echo/query';
16
16
  import { getDeep, isNonNullable } from '@dxos/util';
17
17
 
18
- import type { QueryPlan } from './plan';
19
- import { QueryPlanner } from './query-planner';
20
18
  import type { AutomergeHost } from '../automerge';
21
19
  import { createIdFromSpaceKey } from '../common';
22
20
  import type { SpaceStateManager } from '../db-host';
23
21
  import { filterMatchObject } from '../filter';
24
22
 
23
+ import type { QueryPlan } from './plan';
24
+ import { QueryPlanner } from './query-planner';
25
+
26
+ const isNullable: Predicate.Refinement<unknown, null | undefined> = Predicate.isNullable;
27
+
25
28
  type QueryExecutorOptions = {
26
29
  indexer: Indexer;
27
30
  automergeHost: AutomergeHost;
@@ -189,7 +192,6 @@ export class QueryExecutor extends Resource {
189
192
  );
190
193
 
191
194
  if (TRACE_QUERY_EXECUTION) {
192
- // eslint-disable-next-line no-console
193
195
  console.log(ExecutionTrace.format(trace));
194
196
  }
195
197
 
@@ -245,6 +247,9 @@ export class QueryExecutor extends Resource {
245
247
  case 'TraverseStep':
246
248
  ({ workingSet: newWorkingSet, trace } = await this._execTraverseStep(step, workingSet));
247
249
  break;
250
+ case 'OrderStep':
251
+ ({ workingSet: newWorkingSet, trace } = await this._execOrderStep(step, workingSet));
252
+ break;
248
253
  default:
249
254
  throw new Error(`Unknown step type: ${(step as any)._tag}`);
250
255
  }
@@ -286,6 +291,7 @@ export class QueryExecutor extends Resource {
286
291
 
287
292
  break;
288
293
  }
294
+
289
295
  case 'IdSelector': {
290
296
  const beginLoad = performance.now();
291
297
  const items = await Promise.all(
@@ -299,6 +305,7 @@ export class QueryExecutor extends Resource {
299
305
  trace.objectCount = workingSet.length;
300
306
  break;
301
307
  }
308
+
302
309
  case 'TypeSelector': {
303
310
  const beginIndexQuery = performance.now();
304
311
  const indexHits = await this._indexer.execQuery({
@@ -322,6 +329,7 @@ export class QueryExecutor extends Resource {
322
329
 
323
330
  break;
324
331
  }
332
+
325
333
  case 'TextSelector': {
326
334
  const beginIndexQuery = performance.now();
327
335
  const indexHits = await this._indexer.execQuery({
@@ -352,6 +360,7 @@ export class QueryExecutor extends Resource {
352
360
  trace.objectCount = workingSet.length;
353
361
  break;
354
362
  }
363
+
355
364
  default:
356
365
  throw new Error(`Unknown selector type: ${(step.selector as any)._tag}`);
357
366
  }
@@ -367,6 +376,7 @@ export class QueryExecutor extends Resource {
367
376
  doc: item.doc,
368
377
  }),
369
378
  );
379
+
370
380
  return {
371
381
  workingSet: result,
372
382
  trace: {
@@ -382,10 +392,6 @@ export class QueryExecutor extends Resource {
382
392
  step: QueryPlan.FilterDeletedStep,
383
393
  workingSet: QueryItem[],
384
394
  ): Promise<StepExecutionResult> {
385
- if (workingSet.length === 6) {
386
- log.info('FilterDeletedStep', { step, workingSet });
387
- }
388
-
389
395
  const expected = step.mode === 'only-deleted';
390
396
  const result = workingSet.filter((item) => ObjectStructure.isDeleted(item.doc) === expected);
391
397
  return {
@@ -428,7 +434,7 @@ export class QueryExecutor extends Resource {
428
434
  }
429
435
  : null;
430
436
  } catch {
431
- log.warn('Invalid reference', { ref: ref['/'] });
437
+ log.warn('invalid reference', { ref: ref['/'] });
432
438
  return null;
433
439
  }
434
440
  });
@@ -491,7 +497,7 @@ export class QueryExecutor extends Resource {
491
497
  spaceId: item.spaceId,
492
498
  };
493
499
  } catch {
494
- log.warn('Invalid reference', { ref: ref['/'] });
500
+ log.warn('invalid reference', { ref: ref['/'] });
495
501
  return null;
496
502
  }
497
503
  })
@@ -590,6 +596,60 @@ export class QueryExecutor extends Resource {
590
596
  };
591
597
  }
592
598
 
599
+ private async _execOrderStep(step: QueryPlan.OrderStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
600
+ const compareItems = (a: QueryItem, b: QueryItem): number =>
601
+ step.order.reduce((comparison, order) => {
602
+ // If we already have a definitive result, return it
603
+ if (comparison !== 0) {
604
+ return comparison;
605
+ }
606
+
607
+ return this._compareByOrder(a, b, order);
608
+ }, 0);
609
+
610
+ const sortedWorkingSet = [...workingSet].sort(compareItems);
611
+
612
+ return {
613
+ workingSet: sortedWorkingSet,
614
+ trace: {
615
+ ...ExecutionTrace.makeEmpty(),
616
+ name: 'Order',
617
+ details: JSON.stringify(step.order),
618
+ objectCount: sortedWorkingSet.length,
619
+ },
620
+ };
621
+ }
622
+
623
+ private _compareByOrder(a: QueryItem, b: QueryItem, order: QueryAST.Order): number {
624
+ return Match.type<QueryAST.Order>().pipe(
625
+ Match.withReturnType<number>(),
626
+ Match.when({ kind: 'natural' }, () => a.objectId.localeCompare(b.objectId)),
627
+ Match.when({ kind: 'property' }, ({ property, direction }) => {
628
+ const comparison = this._compareByProperty(a, b, property);
629
+ return direction === 'desc' ? -comparison : comparison;
630
+ }),
631
+ Match.exhaustive,
632
+ )(order);
633
+ }
634
+
635
+ private _compareByProperty(a: QueryItem, b: QueryItem, property: string): number {
636
+ const aValue = a.doc.data[property];
637
+ const bValue = b.doc.data[property];
638
+
639
+ return Match.type<{ a: unknown; b: unknown }>().pipe(
640
+ Match.withReturnType<number>(),
641
+ Match.when({ a: isNullable, b: isNullable }, () => 0),
642
+ Match.when({ a: isNullable }, () => 1),
643
+ Match.when({ b: isNullable }, () => -1),
644
+ Match.when({ a: Match.string, b: Match.string }, ({ a, b }) => a.localeCompare(b)),
645
+ Match.when({ a: Match.number, b: Match.number }, ({ a, b }) => a - b),
646
+ Match.when({ a: Match.boolean, b: Match.boolean }, ({ a, b }) => (a === b ? 0 : a ? 1 : -1)),
647
+ Match.when({ a: Match.defined, b: Match.defined }, ({ a, b }) => String(a).localeCompare(String(b))),
648
+ // TODO(wittjosiah): Why does Match.exhaustive fail here?
649
+ Match.orElse(() => 0),
650
+ )({ a: aValue, b: bValue });
651
+ }
652
+
593
653
  private async _loadDocumentsAfterIndexQuery(indexHits: FindResult[]): Promise<(QueryItem | null)[]> {
594
654
  return Promise.all(
595
655
  indexHits.map(async (hit): Promise<QueryItem | null> => {
@@ -4,13 +4,14 @@
4
4
 
5
5
  import { describe, expect, test } from 'vitest';
6
6
 
7
- import { Filter, Query } from '@dxos/echo';
7
+ import { Filter, Order, Query } from '@dxos/echo';
8
8
  import { type QueryAST } from '@dxos/echo-protocol';
9
9
  import { SpaceId } from '@dxos/keys';
10
10
 
11
- import { QueryPlanner } from './query-planner';
12
11
  import { TestSchema } from '../testing';
13
12
 
13
+ import { QueryPlanner } from './query-planner';
14
+
14
15
  describe('QueryPlanner', () => {
15
16
  const planner = new QueryPlanner();
16
17
 
@@ -38,6 +39,53 @@ describe('QueryPlanner', () => {
38
39
  "_tag": "FilterDeletedStep",
39
40
  "mode": "only-non-deleted",
40
41
  },
42
+ {
43
+ "_tag": "OrderStep",
44
+ "order": [
45
+ {
46
+ "kind": "natural",
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ }
52
+ `);
53
+ });
54
+
55
+ test('get all people ordered by name', () => {
56
+ const query = Query.select(Filter.type(TestSchema.Person)).orderBy(Order.property('name', 'asc'));
57
+
58
+ const plan = planner.createPlan(withSpaceIdOptions(query.ast));
59
+ expect(plan).toMatchInlineSnapshot(`
60
+ {
61
+ "steps": [
62
+ {
63
+ "_tag": "SelectStep",
64
+ "selector": {
65
+ "_tag": "TypeSelector",
66
+ "inverted": false,
67
+ "typename": [
68
+ "dxn:type:dxos.org/type/Person:0.1.0",
69
+ ],
70
+ },
71
+ "spaces": [
72
+ "B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO",
73
+ ],
74
+ },
75
+ {
76
+ "_tag": "FilterDeletedStep",
77
+ "mode": "only-non-deleted",
78
+ },
79
+ {
80
+ "_tag": "OrderStep",
81
+ "order": [
82
+ {
83
+ "direction": "asc",
84
+ "kind": "property",
85
+ "property": "name",
86
+ },
87
+ ],
88
+ },
41
89
  ],
42
90
  }
43
91
  `);
@@ -82,6 +130,14 @@ describe('QueryPlanner', () => {
82
130
  "typename": null,
83
131
  },
84
132
  },
133
+ {
134
+ "_tag": "OrderStep",
135
+ "order": [
136
+ {
137
+ "kind": "natural",
138
+ },
139
+ ],
140
+ },
85
141
  ],
86
142
  }
87
143
  `);
@@ -158,6 +214,14 @@ describe('QueryPlanner', () => {
158
214
  "_tag": "FilterDeletedStep",
159
215
  "mode": "only-non-deleted",
160
216
  },
217
+ {
218
+ "_tag": "OrderStep",
219
+ "order": [
220
+ {
221
+ "kind": "natural",
222
+ },
223
+ ],
224
+ },
161
225
  ],
162
226
  }
163
227
  `);
@@ -218,6 +282,14 @@ describe('QueryPlanner', () => {
218
282
  "typename": "dxn:type:dxos.org/type/Task:0.1.0",
219
283
  },
220
284
  },
285
+ {
286
+ "_tag": "OrderStep",
287
+ "order": [
288
+ {
289
+ "kind": "natural",
290
+ },
291
+ ],
292
+ },
221
293
  ],
222
294
  }
223
295
  `);
@@ -316,6 +388,14 @@ describe('QueryPlanner', () => {
316
388
  "typename": "dxn:type:dxos.org/type/Task:0.1.0",
317
389
  },
318
390
  },
391
+ {
392
+ "_tag": "OrderStep",
393
+ "order": [
394
+ {
395
+ "kind": "natural",
396
+ },
397
+ ],
398
+ },
319
399
  ],
320
400
  }
321
401
  `);
@@ -378,6 +458,14 @@ describe('QueryPlanner', () => {
378
458
  },
379
459
  ],
380
460
  },
461
+ {
462
+ "_tag": "OrderStep",
463
+ "order": [
464
+ {
465
+ "kind": "natural",
466
+ },
467
+ ],
468
+ },
381
469
  ],
382
470
  }
383
471
  `);
@@ -487,6 +575,14 @@ describe('QueryPlanner', () => {
487
575
  ],
488
576
  },
489
577
  },
578
+ {
579
+ "_tag": "OrderStep",
580
+ "order": [
581
+ {
582
+ "kind": "natural",
583
+ },
584
+ ],
585
+ },
490
586
  ],
491
587
  }
492
588
  `);
@@ -543,6 +639,14 @@ describe('QueryPlanner', () => {
543
639
  "_tag": "FilterDeletedStep",
544
640
  "mode": "only-non-deleted",
545
641
  },
642
+ {
643
+ "_tag": "OrderStep",
644
+ "order": [
645
+ {
646
+ "kind": "natural",
647
+ },
648
+ ],
649
+ },
546
650
  ],
547
651
  }
548
652
  `);
@@ -579,6 +683,14 @@ describe('QueryPlanner', () => {
579
683
  "typename": "dxn:type:dxos.org/type/Person:0.1.0",
580
684
  },
581
685
  },
686
+ {
687
+ "_tag": "OrderStep",
688
+ "order": [
689
+ {
690
+ "kind": "natural",
691
+ },
692
+ ],
693
+ },
582
694
  ],
583
695
  }
584
696
  `);
@@ -606,6 +718,14 @@ describe('QueryPlanner', () => {
606
718
  "_tag": "FilterDeletedStep",
607
719
  "mode": "only-non-deleted",
608
720
  },
721
+ {
722
+ "_tag": "OrderStep",
723
+ "order": [
724
+ {
725
+ "kind": "natural",
726
+ },
727
+ ],
728
+ },
609
729
  ],
610
730
  }
611
731
  `);
@@ -636,6 +756,14 @@ describe('QueryPlanner', () => {
636
756
  "_tag": "FilterDeletedStep",
637
757
  "mode": "only-non-deleted",
638
758
  },
759
+ {
760
+ "_tag": "OrderStep",
761
+ "order": [
762
+ {
763
+ "kind": "natural",
764
+ },
765
+ ],
766
+ },
639
767
  ],
640
768
  }
641
769
  `);
@@ -676,6 +804,14 @@ describe('QueryPlanner', () => {
676
804
  "_tag": "FilterDeletedStep",
677
805
  "mode": "only-non-deleted",
678
806
  },
807
+ {
808
+ "_tag": "OrderStep",
809
+ "order": [
810
+ {
811
+ "kind": "natural",
812
+ },
813
+ ],
814
+ },
679
815
  ],
680
816
  }
681
817
  `);
@@ -705,6 +841,14 @@ describe('QueryPlanner', () => {
705
841
  "_tag": "FilterDeletedStep",
706
842
  "mode": "only-deleted",
707
843
  },
844
+ {
845
+ "_tag": "OrderStep",
846
+ "order": [
847
+ {
848
+ "kind": "natural",
849
+ },
850
+ ],
851
+ },
708
852
  ],
709
853
  }
710
854
  `);
@@ -2,6 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Order } from '@dxos/echo';
5
6
  import { type QueryAST } from '@dxos/echo-protocol';
6
7
  import { invariant } from '@dxos/invariant';
7
8
  import type { DXN, SpaceId } from '@dxos/keys';
@@ -35,6 +36,7 @@ export class QueryPlanner {
35
36
  let plan = this._generate(query, { ...DEFAULT_CONTEXT, originalQuery: query });
36
37
  plan = this._optimizeEmptyFilters(plan);
37
38
  plan = this._optimizeSoloUnions(plan);
39
+ plan = this._ensureOrderStep(plan);
38
40
  return plan;
39
41
  }
40
42
 
@@ -58,8 +60,11 @@ export class QueryPlanner {
58
60
  return this._generateUnionClause(query, context);
59
61
  case 'set-difference':
60
62
  return this._generateSetDifferenceClause(query, context);
63
+ case 'order':
64
+ return this._generateOrderClause(query, context);
61
65
  default:
62
- throw new QueryError(`Unsupported query type: ${(query as any).type}`, {
66
+ throw new QueryError({
67
+ message: `Unsupported query type: ${(query as any).type}`,
63
68
  context: { query: context.originalQuery },
64
69
  });
65
70
  }
@@ -86,6 +91,7 @@ export class QueryPlanner {
86
91
  // TODO(dmaretskyi): If the tip of the query ast is a [select, ...filter] shape we can reorder the filters so the query is most efficient.
87
92
  private _generateSelectionFromFilter(filter: QueryAST.Filter, context: GenerationContext): QueryPlan.Plan {
88
93
  switch (filter.type) {
94
+ // Props
89
95
  case 'object': {
90
96
  if (
91
97
  context.selectionInverted &&
@@ -102,7 +108,10 @@ export class QueryPlanner {
102
108
  ]);
103
109
  }
104
110
  if (context.selectionInverted) {
105
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
111
+ throw new QueryError({
112
+ message: 'Query too complex',
113
+ context: { query: context.originalQuery },
114
+ });
106
115
  }
107
116
 
108
117
  // Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
@@ -157,6 +166,26 @@ export class QueryPlanner {
157
166
  ]);
158
167
  }
159
168
  }
169
+
170
+ // Tag
171
+ case 'tag': {
172
+ return QueryPlan.Plan.make([
173
+ {
174
+ _tag: 'SelectStep',
175
+ spaces: context.selectionSpaces,
176
+ selector: {
177
+ _tag: 'WildcardSelector',
178
+ },
179
+ },
180
+ ...this._generateDeletedHandlingSteps(context),
181
+ {
182
+ _tag: 'FilterStep',
183
+ filter: { ...filter },
184
+ },
185
+ ]);
186
+ }
187
+
188
+ // Text
160
189
  case 'text-search': {
161
190
  return QueryPlan.Plan.make([
162
191
  {
@@ -171,26 +200,31 @@ export class QueryPlanner {
171
200
  ...this._generateDeletedHandlingSteps(context),
172
201
  ]);
173
202
  }
203
+
204
+ // Compare
174
205
  case 'compare':
175
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
206
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
176
207
  case 'in':
177
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
208
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
178
209
  case 'range':
179
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
210
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
211
+
212
+ // Boolean
180
213
  case 'not':
181
214
  return this._generateSelectionFromFilter(filter.filter, {
182
215
  ...context,
183
216
  selectionInverted: !context.selectionInverted,
184
217
  });
185
218
  case 'and':
186
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
219
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
187
220
  case 'or':
188
221
  // Optimized case
189
222
  if (filter.filters.every(isTrivialTypenameFilter)) {
190
- const typenames = filter.filters.map((f) => {
191
- invariant(f.type === 'object' && f.typename !== null);
192
- return f.typename;
223
+ const typenames = filter.filters.map((filter) => {
224
+ invariant(filter.type === 'object' && filter.typename !== null);
225
+ return filter.typename;
193
226
  });
227
+
194
228
  return QueryPlan.Plan.make([
195
229
  {
196
230
  _tag: 'SelectStep',
@@ -204,11 +238,12 @@ export class QueryPlanner {
204
238
  ...this._generateDeletedHandlingSteps(context),
205
239
  ]);
206
240
  } else {
207
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
241
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
208
242
  }
209
243
 
210
244
  default:
211
- throw new QueryError(`Unsupported filter type: ${(filter as any).type}`, {
245
+ throw new QueryError({
246
+ message: `Unsupported filter type: ${(filter as any).type}`,
212
247
  context: { query: context.originalQuery },
213
248
  });
214
249
  }
@@ -425,6 +460,41 @@ export class QueryPlanner {
425
460
  // TODO(dmaretskyi): Implement this.
426
461
  return plan;
427
462
  }
463
+
464
+ private _generateOrderClause(query: QueryAST.QueryOrderClause, context: GenerationContext): QueryPlan.Plan {
465
+ return QueryPlan.Plan.make([
466
+ ...this._generate(query.query, context).steps,
467
+ {
468
+ _tag: 'OrderStep',
469
+ order: query.order,
470
+ },
471
+ ]);
472
+ }
473
+
474
+ // After complete plan is built, inspect it from the end:
475
+ // - Walk backwards until hitting an object set changer.
476
+ // - If an order step is found, skip.
477
+ // - Otherwise append natural order to the end.
478
+ private _ensureOrderStep(plan: QueryPlan.Plan): QueryPlan.Plan {
479
+ const OBJECT_SET_CHANGERS = new Set(['SelectStep', 'TraverseStep', 'UnionStep', 'SetDifferenceStep']);
480
+ for (let i = plan.steps.length - 1; i >= 0; i--) {
481
+ const step = plan.steps[i];
482
+ if (step._tag === 'OrderStep') {
483
+ return plan;
484
+ }
485
+ if (OBJECT_SET_CHANGERS.has(step._tag)) {
486
+ break;
487
+ }
488
+ }
489
+
490
+ return QueryPlan.Plan.make([
491
+ ...plan.steps,
492
+ {
493
+ _tag: 'OrderStep',
494
+ order: [Order.natural.ast],
495
+ },
496
+ ]);
497
+ }
428
498
  }
429
499
 
430
500
  /**
@@ -2,15 +2,15 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { scheduleTask, type Trigger } from '@dxos/async';
5
+ import { type Trigger, scheduleTask } from '@dxos/async';
6
6
  import { Context } from '@dxos/context';
7
7
  import { ProtocolError } from '@dxos/protocols';
8
8
  import { schema } from '@dxos/protocols/proto';
9
9
  import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
10
10
  import {
11
11
  type AdmissionDiscoveryService,
12
- type GetAdmissionCredentialResponse,
13
12
  type GetAdmissionCredentialRequest,
13
+ type GetAdmissionCredentialResponse,
14
14
  } from '@dxos/protocols/proto/dxos/mesh/teleport';
15
15
  import { type ExtensionContext, RpcExtension } from '@dxos/teleport';
16
16
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expect, test, onTestFinished } from 'vitest';
5
+ import { describe, expect, onTestFinished, test } from 'vitest';
6
6
 
7
7
  import { CredentialGenerator, createCredential } from '@dxos/credentials';
8
8
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
@@ -11,13 +11,14 @@ import { type PublicKey } from '@dxos/keys';
11
11
  import { log } from '@dxos/log';
12
12
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
13
  import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
14
- import { createStorage, StorageType } from '@dxos/random-access-storage';
14
+ import { StorageType, createStorage } from '@dxos/random-access-storage';
15
15
  import { Timeframe } from '@dxos/timeframe';
16
16
 
17
- import { ControlPipeline } from './control-pipeline';
18
17
  import { valueEncoding } from '../common';
19
18
  import { MetadataStore } from '../metadata';
20
19
 
20
+ import { ControlPipeline } from './control-pipeline';
21
+
21
22
  describe('space/control-pipeline', () => {
22
23
  test('admits feeds', async () => {
23
24
  const keyring = new Keyring();
@@ -2,14 +2,14 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { DeferredTask, sleepWithContext, trackLeaks } from '@dxos/async';
5
+ import { DeferredTask, scheduleMicroTask, sleepWithContext, trackLeaks } from '@dxos/async';
6
6
  import { Context } from '@dxos/context';
7
7
  import {
8
- SpaceStateMachine,
9
- type SpaceState,
10
- type MemberInfo,
11
- type FeedInfo,
12
8
  type DelegateInvitationCredential,
9
+ type FeedInfo,
10
+ type MemberInfo,
11
+ type SpaceState,
12
+ SpaceStateMachine,
13
13
  } from '@dxos/credentials';
14
14
  import { type FeedWrapper } from '@dxos/feed-store';
15
15
  import { PublicKey } from '@dxos/keys';
@@ -83,9 +83,12 @@ export class ControlPipeline {
83
83
 
84
84
  // TODO(burdon): Check not stopping.
85
85
  if (info.assertion.designation === AdmittedFeed.Designation.CONTROL && !info.key.equals(genesisFeed.key)) {
86
- queueMicrotask(async () => {
86
+ scheduleMicroTask(this._ctx, async () => {
87
87
  try {
88
88
  const feed = await feedProvider(info.key);
89
+ if (this._ctx.disposed) {
90
+ return;
91
+ }
89
92
  if (!this._pipeline.hasFeed(feed.key)) {
90
93
  await this._pipeline.addFeed(feed);
91
94
  }
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { describe, test, onTestFinished } from 'vitest';
5
+ import { describe, onTestFinished, test } from 'vitest';
6
6
 
7
7
  import { createStorage } from '@dxos/random-access-storage';
8
8