@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.
- package/dist/lib/browser/{chunk-TQJTKNMS.mjs → chunk-KQYT6ADL.mjs} +109 -3
- package/dist/lib/browser/chunk-KQYT6ADL.mjs.map +7 -0
- package/dist/lib/browser/{chunk-35I6ERLG.mjs → chunk-XGG76KKU.mjs} +513 -350
- package/dist/lib/browser/chunk-XGG76KKU.mjs.map +7 -0
- package/dist/lib/browser/filter/index.mjs +3 -1
- package/dist/lib/browser/index.mjs +1371 -601
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +119 -56
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/{chunk-5BHLPT24.mjs → chunk-CHMJJ4DG.mjs} +513 -350
- package/dist/lib/node-esm/chunk-CHMJJ4DG.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-RVK35BS7.mjs → chunk-W4ACY3YC.mjs} +109 -3
- package/dist/lib/node-esm/chunk-W4ACY3YC.mjs.map +7 -0
- package/dist/lib/node-esm/filter/index.mjs +3 -1
- package/dist/lib/node-esm/index.mjs +1371 -601
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +119 -56
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/automerge/automerge-host.d.ts +15 -28
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/collection-synchronizer.d.ts +1 -1
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +8 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +21 -2
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +1 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +1 -0
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/common/codec.d.ts +1 -1
- package/dist/types/src/common/codec.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +2 -2
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +2 -2
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +2 -2
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/query-service.d.ts +1 -1
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/space-state-manager.d.ts +1 -1
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-replicator.d.ts +4 -2
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/filter/filter-match.d.ts +4 -1
- package/dist/types/src/filter/filter-match.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +24 -8
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +8 -1
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +4 -1
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +2 -0
- package/dist/types/src/query/query-planner.d.ts.map +1 -1
- package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +1 -1
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +1 -1
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +1 -1
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +1 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +1 -0
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +42 -38
- package/src/automerge/automerge-host.test.ts +18 -8
- package/src/automerge/automerge-host.ts +251 -65
- package/src/automerge/automerge-repo.test.ts +67 -16
- package/src/automerge/collection-synchronizer.test.ts +2 -2
- package/src/automerge/collection-synchronizer.ts +4 -4
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +3 -3
- package/src/automerge/echo-network-adapter.ts +40 -7
- package/src/automerge/echo-replicator.ts +23 -2
- package/src/automerge/index.ts +1 -1
- package/src/automerge/leveldb-storage-adapter.ts +7 -7
- package/src/automerge/mesh-echo-replicator-connection.ts +4 -0
- package/src/automerge/mesh-echo-replicator.ts +2 -1
- package/src/automerge/storage-adapter.test.ts +1 -1
- package/src/common/space-id.ts +1 -1
- package/src/db-host/data-service.ts +9 -17
- package/src/db-host/database-root.ts +2 -2
- package/src/db-host/documents-synchronizer.test.ts +1 -1
- package/src/db-host/documents-synchronizer.ts +39 -26
- package/src/db-host/echo-host.ts +13 -14
- package/src/db-host/query-service.ts +8 -1
- package/src/db-host/space-state-manager.ts +2 -2
- package/src/edge/echo-edge-replicator.test.ts +5 -3
- package/src/edge/echo-edge-replicator.ts +75 -18
- package/src/filter/filter-match.test.ts +23 -3
- package/src/filter/filter-match.ts +148 -3
- package/src/metadata/metadata-store.ts +3 -3
- package/src/pipeline/pipeline-stress.test.ts +4 -2
- package/src/pipeline/pipeline.test.ts +3 -2
- package/src/pipeline/pipeline.ts +8 -5
- package/src/query/errors.ts +2 -0
- package/src/query/plan.ts +12 -1
- package/src/query/query-executor.ts +66 -11
- package/src/query/query-planner.test.ts +146 -2
- package/src/query/query-planner.ts +52 -8
- package/src/space/admission-discovery-extension.ts +2 -2
- package/src/space/control-pipeline.test.ts +4 -3
- package/src/space/control-pipeline.ts +9 -6
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +5 -4
- package/src/space/space-protocol.browser.test.ts +2 -2
- package/src/space/space-protocol.test.ts +3 -2
- package/src/space/space-protocol.ts +6 -3
- package/src/space/space.test.ts +1 -1
- package/src/space/space.ts +3 -2
- package/src/testing/test-agent-builder.ts +4 -3
- package/src/testing/test-replicator.ts +4 -0
- package/src/util.ts +1 -1
- package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
- package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
- package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
- package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
- package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
- package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
- package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
- package/dist/lib/node/filter/index.cjs +0 -32
- package/dist/lib/node/filter/index.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -4699
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -753
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
- 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,
|
|
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 {
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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 {
|
|
9
|
+
import { EXPANDO_TYPENAME, Expando, Ref } from '@dxos/echo-schema';
|
|
10
10
|
import { DXN, ObjectId, SpaceId } from '@dxos/keys';
|
|
11
11
|
|
|
12
|
-
import {
|
|
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: {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
19
|
+
type EdgeReplicationSetting,
|
|
20
20
|
type IdentityRecord,
|
|
21
|
-
type SpaceCache,
|
|
22
21
|
type LargeSpaceMetadata,
|
|
23
|
-
type
|
|
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,
|
|
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: {},
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Event, sleepWithContext, synchronized
|
|
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
|
|
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
|
|
package/src/query/errors.ts
CHANGED
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
|
}
|