@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1da679c

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 (146) hide show
  1. package/dist/lib/browser/{chunk-TQJTKNMS.mjs → chunk-KQYT6ADL.mjs} +109 -3
  2. package/dist/lib/browser/chunk-KQYT6ADL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-35I6ERLG.mjs → chunk-XGG76KKU.mjs} +513 -350
  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 +1371 -601
  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 +119 -56
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/lib/node-esm/{chunk-5BHLPT24.mjs → chunk-CHMJJ4DG.mjs} +513 -350
  12. package/dist/lib/node-esm/chunk-CHMJJ4DG.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-RVK35BS7.mjs → chunk-W4ACY3YC.mjs} +109 -3
  14. package/dist/lib/node-esm/chunk-W4ACY3YC.mjs.map +7 -0
  15. package/dist/lib/node-esm/filter/index.mjs +3 -1
  16. package/dist/lib/node-esm/index.mjs +1371 -601
  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 +119 -56
  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 +42 -38
  82. package/src/automerge/automerge-host.test.ts +18 -8
  83. package/src/automerge/automerge-host.ts +251 -65
  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 +40 -7
  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 +13 -14
  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 +23 -3
  107. package/src/filter/filter-match.ts +148 -3
  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 +66 -11
  115. package/src/query/query-planner.test.ts +146 -2
  116. package/src/query/query-planner.ts +52 -8
  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-35I6ERLG.mjs.map +0 -7
  131. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  132. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  133. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  134. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  135. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  136. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  137. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  138. package/dist/lib/node/filter/index.cjs +0 -32
  139. package/dist/lib/node/filter/index.cjs.map +0 -7
  140. package/dist/lib/node/index.cjs +0 -4699
  141. package/dist/lib/node/index.cjs.map +0 -7
  142. package/dist/lib/node/meta.json +0 -1
  143. package/dist/lib/node/testing/index.cjs +0 -753
  144. package/dist/lib/node/testing/index.cjs.map +0 -7
  145. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  146. package/dist/lib/node-esm/chunk-RVK35BS7.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
  }
@@ -382,10 +387,6 @@ export class QueryExecutor extends Resource {
382
387
  step: QueryPlan.FilterDeletedStep,
383
388
  workingSet: QueryItem[],
384
389
  ): Promise<StepExecutionResult> {
385
- if (workingSet.length === 6) {
386
- log.info('FilterDeletedStep', { step, workingSet });
387
- }
388
-
389
390
  const expected = step.mode === 'only-deleted';
390
391
  const result = workingSet.filter((item) => ObjectStructure.isDeleted(item.doc) === expected);
391
392
  return {
@@ -428,7 +429,7 @@ export class QueryExecutor extends Resource {
428
429
  }
429
430
  : null;
430
431
  } catch {
431
- log.warn('Invalid reference', { ref: ref['/'] });
432
+ log.warn('invalid reference', { ref: ref['/'] });
432
433
  return null;
433
434
  }
434
435
  });
@@ -491,7 +492,7 @@ export class QueryExecutor extends Resource {
491
492
  spaceId: item.spaceId,
492
493
  };
493
494
  } catch {
494
- log.warn('Invalid reference', { ref: ref['/'] });
495
+ log.warn('invalid reference', { ref: ref['/'] });
495
496
  return null;
496
497
  }
497
498
  })
@@ -590,6 +591,60 @@ export class QueryExecutor extends Resource {
590
591
  };
591
592
  }
592
593
 
594
+ private async _execOrderStep(step: QueryPlan.OrderStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
595
+ const compareItems = (a: QueryItem, b: QueryItem): number =>
596
+ step.order.reduce((comparison, order) => {
597
+ // If we already have a definitive result, return it
598
+ if (comparison !== 0) {
599
+ return comparison;
600
+ }
601
+
602
+ return this._compareByOrder(a, b, order);
603
+ }, 0);
604
+
605
+ const sortedWorkingSet = [...workingSet].sort(compareItems);
606
+
607
+ return {
608
+ workingSet: sortedWorkingSet,
609
+ trace: {
610
+ ...ExecutionTrace.makeEmpty(),
611
+ name: 'Order',
612
+ details: JSON.stringify(step.order),
613
+ objectCount: sortedWorkingSet.length,
614
+ },
615
+ };
616
+ }
617
+
618
+ private _compareByOrder(a: QueryItem, b: QueryItem, order: QueryAST.Order): number {
619
+ return Match.type<QueryAST.Order>().pipe(
620
+ Match.withReturnType<number>(),
621
+ Match.when({ kind: 'natural' }, () => a.objectId.localeCompare(b.objectId)),
622
+ Match.when({ kind: 'property' }, ({ property, direction }) => {
623
+ const comparison = this._compareByProperty(a, b, property);
624
+ return direction === 'desc' ? -comparison : comparison;
625
+ }),
626
+ Match.exhaustive,
627
+ )(order);
628
+ }
629
+
630
+ private _compareByProperty(a: QueryItem, b: QueryItem, property: string): number {
631
+ const aValue = a.doc.data[property];
632
+ const bValue = b.doc.data[property];
633
+
634
+ return Match.type<{ a: unknown; b: unknown }>().pipe(
635
+ Match.withReturnType<number>(),
636
+ Match.when({ a: isNullable, b: isNullable }, () => 0),
637
+ Match.when({ a: isNullable }, () => 1),
638
+ Match.when({ b: isNullable }, () => -1),
639
+ Match.when({ a: Match.string, b: Match.string }, ({ a, b }) => a.localeCompare(b)),
640
+ Match.when({ a: Match.number, b: Match.number }, ({ a, b }) => a - b),
641
+ Match.when({ a: Match.boolean, b: Match.boolean }, ({ a, b }) => (a === b ? 0 : a ? 1 : -1)),
642
+ Match.when({ a: Match.defined, b: Match.defined }, ({ a, b }) => String(a).localeCompare(String(b))),
643
+ // TODO(wittjosiah): Why does Match.exhaustive fail here?
644
+ Match.orElse(() => 0),
645
+ )({ a: aValue, b: bValue });
646
+ }
647
+
593
648
  private async _loadDocumentsAfterIndexQuery(indexHits: FindResult[]): Promise<(QueryItem | null)[]> {
594
649
  return Promise.all(
595
650
  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
  }
@@ -102,7 +107,10 @@ export class QueryPlanner {
102
107
  ]);
103
108
  }
104
109
  if (context.selectionInverted) {
105
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
110
+ throw new QueryError({
111
+ message: 'Query too complex',
112
+ context: { query: context.originalQuery },
113
+ });
106
114
  }
107
115
 
108
116
  // Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
@@ -172,18 +180,18 @@ export class QueryPlanner {
172
180
  ]);
173
181
  }
174
182
  case 'compare':
175
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
183
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
176
184
  case 'in':
177
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
185
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
178
186
  case 'range':
179
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
187
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
180
188
  case 'not':
181
189
  return this._generateSelectionFromFilter(filter.filter, {
182
190
  ...context,
183
191
  selectionInverted: !context.selectionInverted,
184
192
  });
185
193
  case 'and':
186
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
194
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
187
195
  case 'or':
188
196
  // Optimized case
189
197
  if (filter.filters.every(isTrivialTypenameFilter)) {
@@ -204,11 +212,12 @@ export class QueryPlanner {
204
212
  ...this._generateDeletedHandlingSteps(context),
205
213
  ]);
206
214
  } else {
207
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
215
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
208
216
  }
209
217
 
210
218
  default:
211
- throw new QueryError(`Unsupported filter type: ${(filter as any).type}`, {
219
+ throw new QueryError({
220
+ message: `Unsupported filter type: ${(filter as any).type}`,
212
221
  context: { query: context.originalQuery },
213
222
  });
214
223
  }
@@ -425,6 +434,41 @@ export class QueryPlanner {
425
434
  // TODO(dmaretskyi): Implement this.
426
435
  return plan;
427
436
  }
437
+
438
+ private _generateOrderClause(query: QueryAST.QueryOrderClause, context: GenerationContext): QueryPlan.Plan {
439
+ return QueryPlan.Plan.make([
440
+ ...this._generate(query.query, context).steps,
441
+ {
442
+ _tag: 'OrderStep',
443
+ order: query.order,
444
+ },
445
+ ]);
446
+ }
447
+
448
+ // After complete plan is built, inspect it from the end:
449
+ // - Walk backwards until hitting an object set changer.
450
+ // - If an order step is found, skip.
451
+ // - Otherwise append natural order to the end.
452
+ private _ensureOrderStep(plan: QueryPlan.Plan): QueryPlan.Plan {
453
+ const OBJECT_SET_CHANGERS = new Set(['SelectStep', 'TraverseStep', 'UnionStep', 'SetDifferenceStep']);
454
+ for (let i = plan.steps.length - 1; i >= 0; i--) {
455
+ const step = plan.steps[i];
456
+ if (step._tag === 'OrderStep') {
457
+ return plan;
458
+ }
459
+ if (OBJECT_SET_CHANGERS.has(step._tag)) {
460
+ break;
461
+ }
462
+ }
463
+
464
+ return QueryPlan.Plan.make([
465
+ ...plan.steps,
466
+ {
467
+ _tag: 'OrderStep',
468
+ order: [Order.natural.ast],
469
+ },
470
+ ]);
471
+ }
428
472
  }
429
473
 
430
474
  /**
@@ -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
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { type AutomergeUrl, parseAutomergeUrl } from '@automerge/automerge-repo';
6
6
 
7
- import { synchronized, trackLeaks, Trigger } from '@dxos/async';
8
- import { getCredentialAssertion, type DelegateInvitationCredential, type MemberInfo } from '@dxos/credentials';
7
+ import { Trigger, synchronized, trackLeaks } from '@dxos/async';
8
+ import { type DelegateInvitationCredential, type MemberInfo, getCredentialAssertion } from '@dxos/credentials';
9
9
  import { failUndefined } from '@dxos/debug';
10
10
  import { type FeedStore } from '@dxos/feed-store';
11
11
  import { PublicKey } from '@dxos/keys';
@@ -19,11 +19,12 @@ import { type Teleport } from '@dxos/teleport';
19
19
  import { type BlobStore } from '@dxos/teleport-extension-object-sync';
20
20
  import { ComplexMap } from '@dxos/util';
21
21
 
22
+ import { createIdFromSpaceKey } from '../common/space-id';
23
+ import { type MetadataStore } from '../metadata';
24
+
22
25
  import { CredentialRetrieverExtension } from './admission-discovery-extension';
23
26
  import { Space } from './space';
24
27
  import { SpaceProtocol, type SwarmIdentity } from './space-protocol';
25
- import { createIdFromSpaceKey } from '../common/space-id';
26
- import { type MetadataStore } from '../metadata';
27
28
 
28
29
  export type SpaceManagerParams = {
29
30
  feedStore: FeedStore<FeedMessage>;
@@ -2,14 +2,14 @@
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 { Keyring } from '@dxos/keyring';
8
8
  import { PublicKey } from '@dxos/keys';
9
9
  import { createStorage } from '@dxos/random-access-storage';
10
10
  import { Timeframe } from '@dxos/timeframe';
11
11
 
12
- import { TestFeedBuilder, TestAgentBuilder, WebsocketNetworkManagerProvider } from '../testing';
12
+ import { TestAgentBuilder, TestFeedBuilder, WebsocketNetworkManagerProvider } from '../testing';
13
13
 
14
14
  // TODO(burdon): Config.
15
15
  // Signal server will be started by the setup script.
@@ -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 { PublicKey } from '@dxos/keys';
8
8
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
@@ -11,9 +11,10 @@ import { StorageType, createStorage } from '@dxos/random-access-storage';
11
11
  import { BlobStore } from '@dxos/teleport-extension-object-sync';
12
12
  import { Timeframe } from '@dxos/timeframe';
13
13
 
14
- import { AuthStatus, MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, SpaceProtocol } from './space-protocol';
15
14
  import { TestAgentBuilder, TestFeedBuilder } from '../testing';
16
15
 
16
+ import { AuthStatus, MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, SpaceProtocol } from './space-protocol';
17
+
17
18
  describe('space/space-protocol', () => {
18
19
  // Flaky.
19
20
  test('two peers discover each other via presence', async () => {
@@ -9,8 +9,8 @@ import { PublicKey } from '@dxos/keys';
9
9
  import { log, logInfo } from '@dxos/log';
10
10
  import {
11
11
  MMSTTopology,
12
- type SwarmNetworkManager,
13
12
  type SwarmConnection,
13
+ type SwarmNetworkManager,
14
14
  type WireProtocol,
15
15
  type WireProtocolParams,
16
16
  type WireProtocolProvider,
@@ -20,7 +20,7 @@ import { type MuxerStats, Teleport } from '@dxos/teleport';
20
20
  import { type BlobStore, BlobSync } from '@dxos/teleport-extension-object-sync';
21
21
  import { ReplicatorExtension } from '@dxos/teleport-extension-replicator';
22
22
  import { trace } from '@dxos/tracing';
23
- import { CallbackCollection, ComplexMap, type AsyncCallback } from '@dxos/util';
23
+ import { type AsyncCallback, CallbackCollection, ComplexMap } from '@dxos/util';
24
24
 
25
25
  import { AuthExtension, type AuthProvider, type AuthVerifier } from './auth';
26
26
 
@@ -118,7 +118,10 @@ export class SpaceProtocol {
118
118
  this.blobSync = new BlobSync({ blobStore });
119
119
 
120
120
  // TODO(burdon): Async race condition? Move to start?
121
- this._topic = subtleCrypto.digest('SHA-256', topic.asBuffer()).then(discoveryKey).then(PublicKey.from);
121
+ this._topic = subtleCrypto
122
+ .digest('SHA-256', topic.asBuffer() as ArrayBufferView<ArrayBuffer>)
123
+ .then(discoveryKey)
124
+ .then(PublicKey.from);
122
125
 
123
126
  this._disableP2pReplication = disableP2pReplication ?? false;
124
127
  }