@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
@@ -2,17 +2,24 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { cbor } from '@automerge/automerge-repo';
5
+ import { type DocumentId, type Heads, cbor } from '@automerge/automerge-repo';
6
6
 
7
- import { Mutex, scheduleTask, scheduleMicroTask } from '@dxos/async';
7
+ import { Mutex, scheduleMicroTask, scheduleTask } from '@dxos/async';
8
8
  import { Context, Resource } from '@dxos/context';
9
9
  import { randomUUID } from '@dxos/crypto';
10
10
  import type { CollectionId } from '@dxos/echo-protocol';
11
- import { type EdgeConnection } from '@dxos/edge-client';
11
+ import { type EdgeConnection, type EdgeHttpClient } from '@dxos/edge-client';
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import type { SpaceId } from '@dxos/keys';
14
14
  import { log } from '@dxos/log';
15
- import { EdgeService, type AutomergeProtocolMessage, type PeerId } from '@dxos/protocols';
15
+ import {
16
+ type AutomergeProtocolMessage,
17
+ DocumentCodec,
18
+ EdgeService,
19
+ type ExportBundleRequest,
20
+ type ImportBundleRequest,
21
+ type PeerId,
22
+ } from '@dxos/protocols';
16
23
  import { buf } from '@dxos/protocols/buf';
17
24
  import {
18
25
  type Message as RouterMessage,
@@ -20,16 +27,17 @@ import {
20
27
  } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
21
28
  import { bufferToArray } from '@dxos/util';
22
29
 
23
- import { InflightRequestLimiter } from './inflight-request-limiter';
24
30
  import {
25
- getSpaceIdFromCollectionId,
26
31
  type EchoReplicator,
27
32
  type EchoReplicatorContext,
28
33
  type ReplicatorConnection,
29
34
  type ShouldAdvertiseParams,
30
35
  type ShouldSyncCollectionParams,
36
+ getSpaceIdFromCollectionId,
31
37
  } from '../automerge';
32
38
 
39
+ import { InflightRequestLimiter } from './inflight-request-limiter';
40
+
33
41
  /**
34
42
  * Delay before restarting the connection after receiving a forbidden error.
35
43
  */
@@ -39,11 +47,13 @@ const MAX_RESTART_DELAY = 5000;
39
47
 
40
48
  export type EchoEdgeReplicatorParams = {
41
49
  edgeConnection: EdgeConnection;
50
+ edgeHttpClient: EdgeHttpClient;
42
51
  disableSharePolicy?: boolean;
43
52
  };
44
53
 
45
54
  export class EchoEdgeReplicator implements EchoReplicator {
46
55
  private readonly _edgeConnection: EdgeConnection;
56
+ private readonly _edgeHttpClient: EdgeHttpClient;
47
57
  private readonly _mutex = new Mutex();
48
58
 
49
59
  private _ctx?: Context = undefined;
@@ -52,8 +62,9 @@ export class EchoEdgeReplicator implements EchoReplicator {
52
62
  private _connections = new Map<SpaceId, EdgeReplicatorConnection>();
53
63
  private _sharePolicyEnabled = true;
54
64
 
55
- constructor({ edgeConnection, disableSharePolicy }: EchoEdgeReplicatorParams) {
65
+ constructor({ edgeConnection, edgeHttpClient, disableSharePolicy }: EchoEdgeReplicatorParams) {
56
66
  this._edgeConnection = edgeConnection;
67
+ this._edgeHttpClient = edgeHttpClient;
57
68
  this._sharePolicyEnabled = !disableSharePolicy;
58
69
  }
59
70
 
@@ -127,14 +138,17 @@ export class EchoEdgeReplicator implements EchoReplicator {
127
138
  let restartScheduled = false;
128
139
 
129
140
  const connection = new EdgeReplicatorConnection({
141
+ edgeHttpClient: this._edgeHttpClient,
130
142
  edgeConnection: this._edgeConnection,
131
143
  spaceId,
132
144
  context: this._context,
133
145
  sharedPolicyEnabled: this._sharePolicyEnabled,
134
146
  onRemoteConnected: async () => {
147
+ log.trace('dxos.echo.edge.replicator.onRemoteConnected', { spaceId });
135
148
  this._context?.onConnectionOpen(connection);
136
149
  },
137
150
  onRemoteDisconnected: async () => {
151
+ log.trace('dxos.echo.edge.replicator.onRemoteDisconnected', { spaceId });
138
152
  this._context?.onConnectionClosed(connection);
139
153
  },
140
154
  onRestartRequested: async () => {
@@ -162,6 +176,7 @@ export class EchoEdgeReplicator implements EchoReplicator {
162
176
  if (ctx?.disposed) {
163
177
  return;
164
178
  }
179
+ log.trace('dxos.echo.edge.replicator.restart', { spaceId, reconnects, restartDelay });
165
180
  await this._openConnection(spaceId, reconnects + 1);
166
181
  },
167
182
  restartDelay,
@@ -176,6 +191,7 @@ export class EchoEdgeReplicator implements EchoReplicator {
176
191
 
177
192
  type EdgeReplicatorConnectionsParams = {
178
193
  edgeConnection: EdgeConnection;
194
+ edgeHttpClient: EdgeHttpClient;
179
195
  spaceId: SpaceId;
180
196
  context: EchoReplicatorContext;
181
197
  sharedPolicyEnabled: boolean;
@@ -189,6 +205,7 @@ const MAX_RATE_LIMIT_WAIT_TIME_MS = 3000;
189
205
 
190
206
  class EdgeReplicatorConnection extends Resource implements ReplicatorConnection {
191
207
  private readonly _edgeConnection: EdgeConnection;
208
+ private readonly _edgeHttpClient: EdgeHttpClient;
192
209
  private readonly _remotePeerId: string | null = null;
193
210
  private readonly _targetServiceId: string;
194
211
  private readonly _spaceId: SpaceId;
@@ -210,6 +227,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
210
227
 
211
228
  constructor({
212
229
  edgeConnection,
230
+ edgeHttpClient,
213
231
  spaceId,
214
232
  context,
215
233
  sharedPolicyEnabled,
@@ -219,6 +237,7 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
219
237
  }: EdgeReplicatorConnectionsParams) {
220
238
  super();
221
239
  this._edgeConnection = edgeConnection;
240
+ this._edgeHttpClient = edgeHttpClient;
222
241
  this._spaceId = spaceId;
223
242
  this._context = context;
224
243
  // Generate a unique peer id for every connection.
@@ -252,13 +271,26 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
252
271
 
253
272
  await this._requestLimiter.open();
254
273
 
255
- // TODO: handle reconnects
256
274
  this._ctx.onDispose(
257
275
  this._edgeConnection.onMessage((msg: RouterMessage) => {
258
276
  this._onMessage(msg);
259
277
  }),
260
278
  );
261
279
 
280
+ let firstReconnect = true;
281
+ this._ctx.onDispose(
282
+ // NOTE: This will fire immediately if the connection is already open.
283
+ this._edgeConnection.onReconnected(async () => {
284
+ if (firstReconnect) {
285
+ log.verbose('first reconnect skipped');
286
+ firstReconnect = false;
287
+ return;
288
+ }
289
+
290
+ this._onRestartRequested();
291
+ }),
292
+ );
293
+
262
294
  await this._onRemoteConnected();
263
295
  }
264
296
 
@@ -327,6 +359,27 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
327
359
  this._processMessage(payload);
328
360
  }
329
361
 
362
+ get bundleSyncEnabled(): boolean {
363
+ return true;
364
+ }
365
+
366
+ async pushBundle(bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
367
+ const request: ImportBundleRequest = {
368
+ bundle: bundle.map(({ documentId, data, heads }) => ({
369
+ documentId,
370
+ mutation: DocumentCodec.encode(data),
371
+ heads,
372
+ })),
373
+ };
374
+ await this._edgeHttpClient.importBundle(this._spaceId, request);
375
+ }
376
+
377
+ async pullBundle(docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>> {
378
+ const request: ExportBundleRequest = { docHeads };
379
+ const response = await this._edgeHttpClient.exportBundle(this._spaceId, request);
380
+ return Object.fromEntries(response.bundle.map((doc) => [doc.documentId, DocumentCodec.decode(doc.mutation)]));
381
+ }
382
+
330
383
  private _processMessage(message: AutomergeProtocolMessage): void {
331
384
  // There's a race between the credentials being replicated that are needed for access control and the data replication.
332
385
  // AutomergeReplicator might return a Forbidden error if the credentials are not yet replicated.
@@ -353,16 +406,20 @@ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection
353
406
 
354
407
  const encoded = cbor.encode(message);
355
408
 
356
- await this._edgeConnection.send(
357
- buf.create(RouterMessageSchema, {
358
- serviceId: this._targetServiceId,
359
- source: {
360
- identityKey: this._edgeConnection.identityKey,
361
- peerKey: this._edgeConnection.peerKey,
362
- },
363
- payload: { value: bufferToArray(encoded) },
364
- }),
365
- );
409
+ try {
410
+ await this._edgeConnection.send(
411
+ buf.create(RouterMessageSchema, {
412
+ serviceId: this._targetServiceId,
413
+ source: {
414
+ identityKey: this._edgeConnection.identityKey,
415
+ peerKey: this._edgeConnection.peerKey,
416
+ },
417
+ payload: { value: bufferToArray(encoded) },
418
+ }),
419
+ );
420
+ } catch (err) {
421
+ log.error('failed to send message', { err });
422
+ }
366
423
  }
367
424
  }
368
425
 
@@ -6,10 +6,10 @@ import { describe, expect, test } from 'vitest';
6
6
 
7
7
  import { Filter } from '@dxos/echo';
8
8
  import { ObjectStructure } from '@dxos/echo-protocol';
9
- import { Expando, EXPANDO_TYPENAME, Ref } from '@dxos/echo-schema';
9
+ import { EXPANDO_TYPENAME, Expando, Ref } from '@dxos/echo-schema';
10
10
  import { DXN, ObjectId, SpaceId } from '@dxos/keys';
11
11
 
12
- import { filterMatchObject, type MatchedObject } from './filter-match';
12
+ import { type MatchedObject, filterMatchObject } from './filter-match';
13
13
 
14
14
  describe('filterMatch', () => {
15
15
  test('properties', () => {
@@ -17,6 +17,19 @@ describe('filterMatch', () => {
17
17
  expect(filterMatchObject(Filter.type(Expando, { value: 100 }).ast, OBJECT_1)).to.be.true;
18
18
  expect(filterMatchObject(Filter.type(Expando, { complete: false }).ast, OBJECT_1)).to.be.false;
19
19
  expect(filterMatchObject(Filter.type(Expando, { missing: undefined }).ast, OBJECT_1)).to.be.true;
20
+ expect(filterMatchObject(Filter.type(Expando, { properties: { subject: 'test' } }).ast, OBJECT_1)).to.be.true;
21
+ expect(filterMatchObject(Filter.type(Expando, { array: Filter.contains('two') }).ast, OBJECT_1)).to.be.true;
22
+ });
23
+
24
+ test('contains', () => {
25
+ expect(filterMatchObject(Filter.type(Expando, { properties: { label: Filter.contains('test') } }).ast, OBJECT_1)).to
26
+ .be.true;
27
+ expect(
28
+ filterMatchObject(
29
+ Filter.type(Expando, { fields: Filter.contains({ label: 'label', value: 'test' }) }).ast,
30
+ OBJECT_1,
31
+ ),
32
+ ).to.be.true;
20
33
  });
21
34
 
22
35
  test('and', () => {
@@ -78,7 +91,14 @@ const OBJECT_1: MatchedObject = {
78
91
  spaceId: SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO'),
79
92
  doc: ObjectStructure.makeObject({
80
93
  type: DXN.fromTypenameAndVersion(EXPANDO_TYPENAME, '0.1.0').toString(),
81
- data: { title: 'test', value: 100, complete: true },
94
+ data: {
95
+ title: 'test',
96
+ value: 100,
97
+ complete: true,
98
+ array: ['one', 'two', 'three'],
99
+ properties: { label: ['test'], subject: 'test' },
100
+ fields: [{ label: 'label', value: 'test' }],
101
+ },
82
102
  }),
83
103
  };
84
104
 
@@ -2,8 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { decodeReference, isEncodedReference, type QueryAST, type ObjectStructure } from '@dxos/echo-protocol';
6
- import { EXPANDO_TYPENAME } from '@dxos/echo-schema';
5
+ import { type ObjectStructure, type QueryAST, decodeReference, isEncodedReference } from '@dxos/echo-protocol';
6
+ import { EXPANDO_TYPENAME, type ObjectJSON } from '@dxos/echo-schema';
7
7
  import { DXN, type ObjectId, type SpaceId } from '@dxos/keys';
8
8
 
9
9
  export type MatchedObject = {
@@ -14,6 +14,7 @@ export type MatchedObject = {
14
14
 
15
15
  /**
16
16
  * Matches an object against a filter AST.
17
+ * @param obj object structure as stored in automerge.
17
18
  */
18
19
  export const filterMatchObject = (filter: QueryAST.Filter, obj: MatchedObject): boolean => {
19
20
  switch (filter.type) {
@@ -87,6 +88,118 @@ export const filterMatchObject = (filter: QueryAST.Filter, obj: MatchedObject):
87
88
  }
88
89
  };
89
90
 
91
+ export const filterMatchObjectJSON = (filter: QueryAST.Filter, obj: ObjectJSON): boolean => {
92
+ switch (filter.type) {
93
+ case 'object': {
94
+ // Check typename if specified
95
+ if (filter.typename !== null) {
96
+ // TODO(dmaretskyi): `system` is missing in some cases.
97
+ if (!obj['@type']) {
98
+ // Objects with no type are considered to be expando objects
99
+ const expectedDXN = DXN.parse(filter.typename).asTypeDXN();
100
+ if (expectedDXN?.type !== EXPANDO_TYPENAME) {
101
+ return false;
102
+ }
103
+ } else {
104
+ const actualDXN = DXN.parse(obj['@type']);
105
+ const expectedDXN = DXN.parse(filter.typename);
106
+
107
+ if (!compareTypename(expectedDXN, actualDXN)) {
108
+ return false;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Check IDs if specified
114
+ if (filter.id && filter.id.length > 0 && !filter.id.includes(obj.id)) {
115
+ return false;
116
+ }
117
+
118
+ // Check properties
119
+ if (filter.props) {
120
+ for (const [key, valueFilter] of Object.entries(filter.props)) {
121
+ if (key.startsWith('@')) {
122
+ // ignore meta properties
123
+ continue;
124
+ }
125
+ const value = (obj as any)[key];
126
+ if (!filterMatchValue(valueFilter, value)) {
127
+ return false;
128
+ }
129
+ }
130
+ }
131
+
132
+ // Check foreign keys if specified
133
+ if (filter.foreignKeys && filter.foreignKeys.length > 0) {
134
+ const hasMatchingKey = filter.foreignKeys.some((filterKey) =>
135
+ obj['@meta']?.keys?.some((objKey) => objKey.source === filterKey.source && objKey.id === filterKey.id),
136
+ );
137
+ if (!hasMatchingKey) {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ return true;
143
+ }
144
+
145
+ case 'text-search': {
146
+ // TODO: Implement text search
147
+ return false;
148
+ }
149
+
150
+ case 'not': {
151
+ return !filterMatchObjectJSON(filter.filter, obj);
152
+ }
153
+
154
+ case 'and': {
155
+ return filter.filters.every((f) => filterMatchObjectJSON(f, obj));
156
+ }
157
+
158
+ case 'or': {
159
+ return filter.filters.some((f) => filterMatchObjectJSON(f, obj));
160
+ }
161
+
162
+ default:
163
+ return false;
164
+ }
165
+ };
166
+
167
+ /**
168
+ * Performs structural matching between a filter object and a target object.
169
+ * This handles nested object comparison for array matching scenarios.
170
+ */
171
+ // TODO(wittjosiah): Add ast support for non-strict matching.
172
+ const structuralMatch = (filterObj: any, targetObj: any, strict = true): boolean => {
173
+ if (typeof filterObj !== 'object' || filterObj === null) {
174
+ return filterObj === targetObj;
175
+ }
176
+
177
+ if (typeof targetObj !== 'object' || targetObj === null) {
178
+ return false;
179
+ }
180
+
181
+ // Prohibit extra keys in targetObj.
182
+ const filterKeys = Object.keys(filterObj);
183
+ const targetKeys = Object.keys(targetObj);
184
+ if (strict && filterKeys.length !== targetKeys.length) {
185
+ return false;
186
+ }
187
+
188
+ return filterKeys.every((key) => {
189
+ if (!(key in targetObj)) {
190
+ return false;
191
+ }
192
+ const filterValue = filterObj[key];
193
+ const targetValue = targetObj[key];
194
+
195
+ if (typeof filterValue === 'object' && filterValue !== null) {
196
+ return structuralMatch(filterValue, targetValue);
197
+ }
198
+
199
+ return filterValue === targetValue;
200
+ });
201
+ };
202
+
90
203
  export const filterMatchValue = (filter: QueryAST.Filter, value: unknown): boolean => {
91
204
  switch (filter.type) {
92
205
  case 'compare': {
@@ -110,12 +223,44 @@ export const filterMatchValue = (filter: QueryAST.Filter, value: unknown): boole
110
223
  return (value as any) < compareValue;
111
224
  case 'lte':
112
225
  return (value as any) <= compareValue;
226
+ default:
227
+ return false;
113
228
  }
114
- break;
229
+ }
230
+ case 'object': {
231
+ // Handle nested object filters for property matching
232
+ if (typeof value !== 'object' || value === null) {
233
+ return false;
234
+ }
235
+
236
+ // Check properties
237
+ if (filter.props) {
238
+ for (const [key, valueFilter] of Object.entries(filter.props)) {
239
+ const nestedValue = (value as any)[key];
240
+ if (!filterMatchValue(valueFilter, nestedValue)) {
241
+ return false;
242
+ }
243
+ }
244
+ }
245
+
246
+ return true;
115
247
  }
116
248
  case 'in': {
117
249
  return filter.values.includes(value);
118
250
  }
251
+ case 'contains': {
252
+ if (!Array.isArray(value)) {
253
+ return false;
254
+ }
255
+
256
+ return value.some((element) => {
257
+ if (typeof filter.value === 'object' && filter.value !== null && !Array.isArray(filter.value)) {
258
+ return structuralMatch(filter.value, element);
259
+ }
260
+
261
+ return element === filter.value;
262
+ });
263
+ }
119
264
  case 'range': {
120
265
  return (value as any) >= filter.from && (value as any) <= filter.to;
121
266
  }
@@ -16,11 +16,11 @@ import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/servic
16
16
  import {
17
17
  type ControlPipelineSnapshot,
18
18
  type EchoMetadata,
19
- type SpaceMetadata,
19
+ type EdgeReplicationSetting,
20
20
  type IdentityRecord,
21
- type SpaceCache,
22
21
  type LargeSpaceMetadata,
23
- type EdgeReplicationSetting,
22
+ type SpaceCache,
23
+ type SpaceMetadata,
24
24
  } from '@dxos/protocols/proto/dxos/echo/metadata';
25
25
  import { type Directory, type File } from '@dxos/random-access-storage';
26
26
  import { type Timeframe } from '@dxos/timeframe';
@@ -2,8 +2,9 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import * as fc from 'fast-check';
6
5
  import { inspect } from 'node:util';
6
+
7
+ import * as fc from 'fast-check';
7
8
  import { describe, expect, test } from 'vitest';
8
9
 
9
10
  import { asyncTimeout } from '@dxos/async';
@@ -15,9 +16,10 @@ import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
16
  import { Timeframe } from '@dxos/timeframe';
16
17
  import { range } from '@dxos/util';
17
18
 
18
- import { Pipeline } from './pipeline';
19
19
  import { TestFeedBuilder } from '../testing';
20
20
 
21
+ import { Pipeline } from './pipeline';
22
+
21
23
  const NUM_AGENTS = 2;
22
24
  const NUM_MESSAGES = 10;
23
25
 
@@ -2,16 +2,17 @@
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 { Event, sleep } from '@dxos/async';
8
8
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
9
9
  import { Timeframe } from '@dxos/timeframe';
10
10
  import { range } from '@dxos/util';
11
11
 
12
- import { Pipeline } from './pipeline';
13
12
  import { TestFeedBuilder } from '../testing';
14
13
 
14
+ import { Pipeline } from './pipeline';
15
+
15
16
  const TEST_MESSAGE: FeedMessage = {
16
17
  timeframe: new Timeframe(),
17
18
  payload: {},
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, sleepWithContext, synchronized, Trigger } from '@dxos/async';
5
+ import { Event, Trigger, sleepWithContext, synchronized } from '@dxos/async';
6
6
  import { Context, rejectOnDispose } from '@dxos/context';
7
7
  import { failUndefined } from '@dxos/debug';
8
8
  import { FeedSetIterator, type FeedWrapper, type FeedWriter } from '@dxos/feed-store';
@@ -14,10 +14,11 @@ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
14
14
  import { Timeframe } from '@dxos/timeframe';
15
15
  import { ComplexMap } from '@dxos/util';
16
16
 
17
- import { createMessageSelector } from './message-selector';
18
- import { mapFeedIndexesToTimeframe, startAfter, TimeframeClock } from './timeframe-clock';
19
17
  import { createMappedFeedWriter } from '../common';
20
18
 
19
+ import { createMessageSelector } from './message-selector';
20
+ import { TimeframeClock, mapFeedIndexesToTimeframe, startAfter } from './timeframe-clock';
21
+
21
22
  export type WaitUntilReachedTargetParams = {
22
23
  /**
23
24
  * For cancellation.
@@ -40,8 +41,10 @@ export class PipelineState {
40
41
  */
41
42
  _ctx = new Context();
42
43
 
43
- // TODO(dmaretskyi): Remove?.
44
- public readonly timeframeUpdate = this._timeframeClock.update;
44
+ // TODO(dmaretskyi): Remove?. Avoid accessing `_timeframeClock` before constructor initialization.
45
+ public get timeframeUpdate() {
46
+ return this._timeframeClock.update;
47
+ }
45
48
 
46
49
  public readonly stalled = new Event();
47
50
 
@@ -5,3 +5,5 @@
5
5
  import { BaseError } from '@dxos/errors';
6
6
 
7
7
  export class QueryError extends BaseError.extend('QUERY_ERROR') {}
8
+
9
+ export class InvalidQueryError extends QueryError.extend('INVALID_QUERY') {}
package/src/query/plan.ts CHANGED
@@ -32,7 +32,8 @@ export namespace QueryPlan {
32
32
  | FilterDeletedStep
33
33
  | TraverseStep
34
34
  | UnionStep
35
- | SetDifferenceStep;
35
+ | SetDifferenceStep
36
+ | OrderStep;
36
37
 
37
38
  /**
38
39
  * Clear the current working set.
@@ -193,4 +194,14 @@ export namespace QueryPlan {
193
194
  source: Plan;
194
195
  exclude: Plan;
195
196
  };
197
+
198
+ /**
199
+ * Order the results of the plan.
200
+ */
201
+ export type OrderStep = {
202
+ _tag: 'OrderStep';
203
+
204
+ // Defaults to natural order if empty.
205
+ order: readonly QueryAST.Order[];
206
+ };
196
207
  }