@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/neutral/index.mjs +7332 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts +119 -54
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
- package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/errors.d.ts +35 -0
- package/dist/types/src/automerge/errors.d.ts.map +1 -0
- package/dist/types/src/automerge/handle-state.d.ts +52 -0
- package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
- package/dist/types/src/automerge/heads-store.d.ts +9 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +3 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.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/common/feeds.d.ts.map +1 -1
- package/dist/types/src/common/space-id.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
- package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +13 -7
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +9 -3
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +74 -28
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +2 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
- package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
- package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +19 -11
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
- package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
- package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +11 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +1 -0
- package/dist/types/src/edge/index.d.ts.map +1 -1
- package/dist/types/src/edge/inflight-request-limiter.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/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +22 -10
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +66 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +66 -11
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +12 -1
- 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/auth.d.ts +3 -3
- package/dist/types/src/space/auth.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +5 -4
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +8 -8
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +10 -9
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +6 -6
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.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-feed-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +14 -13
- 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 +59 -55
- package/src/automerge/automerge-host-subduction.test.ts +263 -0
- package/src/automerge/automerge-host.test.ts +162 -39
- package/src/automerge/automerge-host.ts +679 -201
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +349 -91
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +98 -17
- package/src/automerge/collection-synchronizer.ts +141 -37
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +5 -4
- package/src/automerge/echo-network-adapter.ts +76 -31
- package/src/automerge/echo-replicator.ts +50 -15
- package/src/automerge/errors.ts +10 -0
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +3 -1
- package/src/automerge/leveldb-storage-adapter.ts +77 -11
- package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
- package/src/automerge/mesh-echo-replicator.ts +15 -11
- package/src/automerge/storage-adapter.test.ts +1 -1
- package/src/common/space-id.ts +1 -1
- package/src/db-host/automerge-data-source.test.ts +299 -0
- package/src/db-host/automerge-data-source.ts +187 -0
- package/src/db-host/data-service.ts +35 -26
- package/src/db-host/database-root.ts +20 -8
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +163 -51
- package/src/db-host/echo-host.ts +384 -99
- package/src/db-host/index.ts +2 -0
- package/src/db-host/invalidation-hint.ts +50 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-invalidation.test.ts +270 -0
- package/src/db-host/query-service.ts +111 -106
- package/src/db-host/queue-data-source.ts +160 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +12 -10
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +12 -10
- package/src/edge/echo-edge-replicator.ts +144 -58
- package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
- package/src/edge/echo-edge-subduction-replicator.ts +377 -0
- package/src/edge/index.ts +1 -0
- package/src/filter/filter-match.test.ts +68 -32
- package/src/filter/filter-match.ts +230 -21
- package/src/metadata/metadata-store.ts +11 -8
- package/src/pipeline/pipeline-stress.test.ts +1 -1
- package/src/pipeline/pipeline.test.ts +2 -2
- package/src/pipeline/pipeline.ts +9 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +84 -9
- package/src/query/query-executor.ts +1197 -165
- package/src/query/query-planner.test.ts +993 -81
- package/src/query/query-planner.ts +503 -27
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +5 -4
- package/src/space/control-pipeline.ts +16 -13
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +16 -20
- package/src/space/space-protocol.browser.test.ts +15 -14
- package/src/space/space-protocol.test.ts +15 -14
- package/src/space/space-protocol.ts +22 -18
- package/src/space/space.test.ts +3 -3
- package/src/space/space.ts +11 -11
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +3 -3
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +19 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -4743
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.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 +0 -2067
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -11
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4743
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -742
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
- package/dist/types/src/testing/test-data.d.ts +0 -18
- package/dist/types/src/testing/test-data.d.ts.map +0 -1
- package/dist/types/src/testing/test-schema.d.ts +0 -37
- package/dist/types/src/testing/test-schema.d.ts.map +0 -1
- package/src/db-host/documents-iterator.ts +0 -74
- package/src/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
- /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { IndexingResult } from '@dxos/index-core';
|
|
6
|
+
import type { ObjectId, SpaceId } from '@dxos/keys';
|
|
7
|
+
|
|
8
|
+
export type InvalidationHint = {
|
|
9
|
+
spaceIds?: ReadonlySet<SpaceId>;
|
|
10
|
+
queueIds?: ReadonlySet<ObjectId>;
|
|
11
|
+
typenames?: ReadonlySet<string>;
|
|
12
|
+
objectIds?: ReadonlySet<ObjectId>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts an IndexingResult to an InvalidationHint.
|
|
17
|
+
* Returns undefined when nothing was indexed (no need to invalidate).
|
|
18
|
+
*/
|
|
19
|
+
export const hintFromIndexingResult = (r: IndexingResult): InvalidationHint | undefined => {
|
|
20
|
+
if (r.updated === 0) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
spaceIds: r.spaces.size > 0 ? r.spaces : undefined,
|
|
25
|
+
queueIds: r.queues.size > 0 ? r.queues : undefined,
|
|
26
|
+
typenames: r.types.size > 0 ? r.types : undefined,
|
|
27
|
+
objectIds: r.objects.size > 0 ? r.objects : undefined,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Merges two hints for the same tick.
|
|
33
|
+
* If either side leaves a dimension unconstrained (undefined), the merged result is also unconstrained on that dimension.
|
|
34
|
+
*/
|
|
35
|
+
export const mergeHints = (a: InvalidationHint, b: InvalidationHint): InvalidationHint => ({
|
|
36
|
+
spaceIds: unionOrUndefined(a.spaceIds, b.spaceIds),
|
|
37
|
+
queueIds: unionOrUndefined(a.queueIds, b.queueIds),
|
|
38
|
+
typenames: unionOrUndefined(a.typenames, b.typenames),
|
|
39
|
+
objectIds: unionOrUndefined(a.objectIds, b.objectIds),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const unionOrUndefined = <T>(
|
|
43
|
+
a: ReadonlySet<T> | undefined,
|
|
44
|
+
b: ReadonlySet<T> | undefined,
|
|
45
|
+
): ReadonlySet<T> | undefined => {
|
|
46
|
+
if (a === undefined || b === undefined) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return new Set<T>([...a, ...b]);
|
|
50
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Function from 'effect/Function';
|
|
8
|
+
|
|
9
|
+
import { type RequestOptions } from '@dxos/codec-protobuf';
|
|
10
|
+
import { Context } from '@dxos/context';
|
|
11
|
+
import { EchoFeedCodec } from '@dxos/echo-protocol';
|
|
12
|
+
import { type ObjectJSON } from '@dxos/echo/internal';
|
|
13
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
14
|
+
import { type FeedStore } from '@dxos/feed';
|
|
15
|
+
import { assertArgument, invariant } from '@dxos/invariant';
|
|
16
|
+
import { type SpaceId } from '@dxos/keys';
|
|
17
|
+
import { FeedProtocol } from '@dxos/protocols';
|
|
18
|
+
import {
|
|
19
|
+
type DeleteFromQueueRequest,
|
|
20
|
+
type InsertIntoQueueRequest,
|
|
21
|
+
type QueryQueueRequest,
|
|
22
|
+
type QueueQueryResult,
|
|
23
|
+
type QueueService,
|
|
24
|
+
type SyncQueueRequest,
|
|
25
|
+
} from '@dxos/protocols/proto/dxos/client/services';
|
|
26
|
+
import type { SqlTransaction } from '@dxos/sql-sqlite';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Writes queue data to a local FeedStore.
|
|
30
|
+
*/
|
|
31
|
+
export class LocalQueueServiceImpl implements QueueService {
|
|
32
|
+
#runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
|
|
33
|
+
#feedStore: FeedStore;
|
|
34
|
+
#syncQueue?: (ctx: Context, request: SyncQueueRequest) => Promise<void>;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>,
|
|
38
|
+
feedStore: FeedStore,
|
|
39
|
+
syncQueue?: (ctx: Context, request: SyncQueueRequest) => Promise<void>,
|
|
40
|
+
) {
|
|
41
|
+
this.#runtime = runtime;
|
|
42
|
+
this.#feedStore = feedStore;
|
|
43
|
+
this.#syncQueue = syncQueue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
queryQueue(request: QueryQueueRequest): Promise<QueueQueryResult> {
|
|
47
|
+
const { query } = request;
|
|
48
|
+
invariant(query, 'query is required');
|
|
49
|
+
const { spaceId, queueIds } = query;
|
|
50
|
+
return RuntimeProvider.runPromise(this.#runtime)(
|
|
51
|
+
Effect.gen(this, function* () {
|
|
52
|
+
const cursor = query.after ? parseInt(query.after) : -1;
|
|
53
|
+
const result = yield* this.#feedStore.query({
|
|
54
|
+
requestId: crypto.randomUUID(),
|
|
55
|
+
feedNamespace: request.query.queuesNamespace || FeedProtocol.WellKnownNamespaces.data,
|
|
56
|
+
spaceId: spaceId! as SpaceId,
|
|
57
|
+
query: { feedIds: queueIds ?? [] },
|
|
58
|
+
position: cursor,
|
|
59
|
+
limit: query.limit,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const objects = result.blocks.map((block: FeedProtocol.Block) =>
|
|
63
|
+
JSON.stringify(EchoFeedCodec.decode(block.data, block.position ?? undefined) as ObjectJSON),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const lastBlock = result.blocks[result.blocks.length - 1];
|
|
67
|
+
const nextCursor = lastBlock && lastBlock.position != null ? String(lastBlock.position) : null;
|
|
68
|
+
|
|
69
|
+
return Function.identity<QueueQueryResult>({
|
|
70
|
+
objects,
|
|
71
|
+
|
|
72
|
+
// TODO(dmaretskyi): This is wrong, fix later - cursors should come directly from the feed.
|
|
73
|
+
nextCursor: nextCursor?.toString() ?? '',
|
|
74
|
+
prevCursor: '',
|
|
75
|
+
});
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
insertIntoQueue(request: InsertIntoQueueRequest): Promise<void> {
|
|
81
|
+
const { subspaceTag, spaceId, queueId, objects } = request;
|
|
82
|
+
const feedNamespace = subspaceTag ?? FeedProtocol.WellKnownNamespaces.data;
|
|
83
|
+
assertArgument(
|
|
84
|
+
FeedProtocol.isWellKnownNamespace(feedNamespace),
|
|
85
|
+
'request.subspaceTag',
|
|
86
|
+
'expected a well-known queue namespace',
|
|
87
|
+
);
|
|
88
|
+
return RuntimeProvider.runPromise(this.#runtime)(
|
|
89
|
+
Effect.gen(this, function* () {
|
|
90
|
+
const messages = (objects ?? []).map((encoded) => ({
|
|
91
|
+
spaceId: spaceId,
|
|
92
|
+
feedId: queueId!,
|
|
93
|
+
feedNamespace,
|
|
94
|
+
data: EchoFeedCodec.encode(JSON.parse(encoded) as ObjectJSON),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
yield* this.#feedStore.appendLocal(messages);
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
deleteFromQueue(request: DeleteFromQueueRequest): Promise<void> {
|
|
103
|
+
const { subspaceTag, spaceId, queueId, objectIds } = request;
|
|
104
|
+
const feedNamespace = subspaceTag ?? FeedProtocol.WellKnownNamespaces.data;
|
|
105
|
+
assertArgument(
|
|
106
|
+
FeedProtocol.isWellKnownNamespace(feedNamespace),
|
|
107
|
+
'request.subspaceTag',
|
|
108
|
+
'expected a well-known queue namespace',
|
|
109
|
+
);
|
|
110
|
+
return RuntimeProvider.runPromise(this.#runtime)(
|
|
111
|
+
Effect.gen(this, function* () {
|
|
112
|
+
const messages = objectIds!.map((id) => ({
|
|
113
|
+
spaceId: spaceId,
|
|
114
|
+
feedId: queueId!,
|
|
115
|
+
feedNamespace,
|
|
116
|
+
data: EchoFeedCodec.encode({ id, '@deleted': true }),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
yield* this.#feedStore.appendLocal(messages);
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async syncQueue(request: SyncQueueRequest, options?: RequestOptions): Promise<void> {
|
|
125
|
+
await this.#syncQueue?.(options?.ctx ?? Context.default(), request);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Filter, Query } from '@dxos/echo';
|
|
8
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
9
|
+
import { DXN, ObjectId, SpaceId } from '@dxos/keys';
|
|
10
|
+
|
|
11
|
+
import { QueryExecutor } from '../query/query-executor';
|
|
12
|
+
import { type InvalidationHint, hintFromIndexingResult, mergeHints } from './invalidation-hint';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const makeHint = (partial: Partial<InvalidationHint>): InvalidationHint => partial;
|
|
19
|
+
|
|
20
|
+
const makeSpaceSet = (...ids: SpaceId[]) => new Set<SpaceId>(ids);
|
|
21
|
+
const makeTypeSet = (...types: string[]) => new Set<string>(types);
|
|
22
|
+
const makeObjectSet = (...ids: ObjectId[]) => new Set<ObjectId>(ids);
|
|
23
|
+
|
|
24
|
+
const SPACE_ID = SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO');
|
|
25
|
+
|
|
26
|
+
const PERSON_DXN = 'dxn:type:com.example.type.person:0.1.0';
|
|
27
|
+
const ORG_DXN = 'dxn:type:com.example.type.organization:0.1.0';
|
|
28
|
+
|
|
29
|
+
// Stable queue DXN mirroring query-planner.test.ts.
|
|
30
|
+
const QUEUE_DXN = DXN.parse('dxn:queue:data:B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO:01JJRA86VK4H1TEB6QQVSWXP0E').toString();
|
|
31
|
+
const QUEUE_SPACE_ID = SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO');
|
|
32
|
+
const QUEUE_ID = ObjectId.make('01JJRA86VK4H1TEB6QQVSWXP0E');
|
|
33
|
+
|
|
34
|
+
const withSpace = (q: any): any => (q as any).from({ spaceIds: [SPACE_ID] });
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a QueryExecutor with no real dependencies — only the query is used
|
|
38
|
+
* to build the plan and cached scopes via extractScopes().
|
|
39
|
+
*/
|
|
40
|
+
const makeExecutor = (query: { ast: any }): QueryExecutor =>
|
|
41
|
+
new QueryExecutor({
|
|
42
|
+
indexEngine: {} as any,
|
|
43
|
+
runtime: {} as any,
|
|
44
|
+
automergeHost: {} as any,
|
|
45
|
+
spaceStateManager: {} as any,
|
|
46
|
+
queryId: 'test',
|
|
47
|
+
query: query.ast,
|
|
48
|
+
reactivity: 'reactive' as any,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// hintFromIndexingResult
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe('hintFromIndexingResult', () => {
|
|
56
|
+
test('returns undefined when nothing was indexed', ({ expect }) => {
|
|
57
|
+
const result = hintFromIndexingResult({
|
|
58
|
+
updated: 0,
|
|
59
|
+
done: true,
|
|
60
|
+
spaces: new Set(),
|
|
61
|
+
queues: new Set(),
|
|
62
|
+
documents: new Set(),
|
|
63
|
+
types: new Set(),
|
|
64
|
+
objects: new Set(),
|
|
65
|
+
});
|
|
66
|
+
expect(result).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('converts populated result to hint with non-empty dimensions', ({ expect }) => {
|
|
70
|
+
const spaceId = SpaceId.random();
|
|
71
|
+
const objectId = ObjectId.random();
|
|
72
|
+
const result = hintFromIndexingResult({
|
|
73
|
+
updated: 1,
|
|
74
|
+
done: true,
|
|
75
|
+
spaces: new Set([spaceId]),
|
|
76
|
+
queues: new Set(),
|
|
77
|
+
documents: new Set(['doc-1']),
|
|
78
|
+
types: new Set([PERSON_DXN]),
|
|
79
|
+
objects: new Set([objectId]),
|
|
80
|
+
});
|
|
81
|
+
expect(result).toBeDefined();
|
|
82
|
+
expect(result!.spaceIds?.has(spaceId)).toBe(true);
|
|
83
|
+
expect(result!.typenames?.has(PERSON_DXN)).toBe(true);
|
|
84
|
+
expect(result!.objectIds?.has(objectId)).toBe(true);
|
|
85
|
+
// Empty queues → undefined (no queue constraint)
|
|
86
|
+
expect(result!.queueIds).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// mergeHints — per-tick merger
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe('mergeHints', () => {
|
|
95
|
+
test('unions constrained dimensions from both hints', ({ expect }) => {
|
|
96
|
+
const s1 = SpaceId.random();
|
|
97
|
+
const s2 = SpaceId.random();
|
|
98
|
+
const merged = mergeHints(
|
|
99
|
+
makeHint({ spaceIds: makeSpaceSet(s1), typenames: makeTypeSet('TypeA') }),
|
|
100
|
+
makeHint({ spaceIds: makeSpaceSet(s2), typenames: makeTypeSet('TypeB') }),
|
|
101
|
+
);
|
|
102
|
+
expect(merged.spaceIds?.has(s1)).toBe(true);
|
|
103
|
+
expect(merged.spaceIds?.has(s2)).toBe(true);
|
|
104
|
+
expect(merged.typenames?.has('TypeA')).toBe(true);
|
|
105
|
+
expect(merged.typenames?.has('TypeB')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('drops a dimension to unconstrained when either contributor is unconstrained', ({ expect }) => {
|
|
109
|
+
const s = SpaceId.random();
|
|
110
|
+
// b has no spaceIds → unconstrained on that dimension
|
|
111
|
+
const merged = mergeHints(
|
|
112
|
+
makeHint({ spaceIds: makeSpaceSet(s), typenames: makeTypeSet('TypeA') }),
|
|
113
|
+
makeHint({ typenames: makeTypeSet('TypeB') }),
|
|
114
|
+
);
|
|
115
|
+
// Space is dropped because b is unconstrained on it.
|
|
116
|
+
expect(merged.spaceIds).toBeUndefined();
|
|
117
|
+
// Types are still unioned.
|
|
118
|
+
expect(merged.typenames?.has('TypeA')).toBe(true);
|
|
119
|
+
expect(merged.typenames?.has('TypeB')).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('fully unconstrained result when one side has no constraints at all', ({ expect }) => {
|
|
123
|
+
const merged = mergeHints(
|
|
124
|
+
makeHint({}), // fully unconstrained
|
|
125
|
+
makeHint({ spaceIds: makeSpaceSet(SpaceId.random()), typenames: makeTypeSet('TypeA') }),
|
|
126
|
+
);
|
|
127
|
+
expect(merged.spaceIds).toBeUndefined();
|
|
128
|
+
expect(merged.typenames).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// QueryExecutor.matchesHint — typed queries
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('QueryExecutor.matchesHint — typed query', () => {
|
|
137
|
+
test('matches when hint typenames overlaps', ({ expect }) => {
|
|
138
|
+
const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
|
|
139
|
+
expect(executor.matchesHint(makeHint({ typenames: makeTypeSet(PERSON_DXN) }))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('does NOT match when hint typenames are disjoint', ({ expect }) => {
|
|
143
|
+
const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
|
|
144
|
+
const hint = makeHint({
|
|
145
|
+
spaceIds: makeSpaceSet(SPACE_ID),
|
|
146
|
+
typenames: makeTypeSet(ORG_DXN), // different type
|
|
147
|
+
});
|
|
148
|
+
expect(executor.matchesHint(hint)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('matches when hint has no typenames constraint (unconstrained → always match)', ({ expect }) => {
|
|
152
|
+
const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
|
|
153
|
+
expect(executor.matchesHint(makeHint({ spaceIds: makeSpaceSet(SPACE_ID) }))).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('does NOT match when hint spaceIds exclude this query space', ({ expect }) => {
|
|
157
|
+
const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
|
|
158
|
+
const hint = makeHint({
|
|
159
|
+
spaceIds: makeSpaceSet(SpaceId.random()),
|
|
160
|
+
typenames: makeTypeSet(PERSON_DXN),
|
|
161
|
+
});
|
|
162
|
+
expect(executor.matchesHint(hint)).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('Filter.or(typeA, typeB) matches hint with only typeA', ({ expect }) => {
|
|
166
|
+
const executor = makeExecutor(
|
|
167
|
+
withSpace(Query.select(Filter.or(Filter.type(TestSchema.Organization), Filter.type(TestSchema.Person)))),
|
|
168
|
+
);
|
|
169
|
+
expect(
|
|
170
|
+
executor.matchesHint(makeHint({ spaceIds: makeSpaceSet(SPACE_ID), typenames: makeTypeSet(PERSON_DXN) })),
|
|
171
|
+
).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Filter.or(typeA, typeB) does NOT match hint with unrelated type', ({ expect }) => {
|
|
175
|
+
const executor = makeExecutor(
|
|
176
|
+
withSpace(Query.select(Filter.or(Filter.type(TestSchema.Organization), Filter.type(TestSchema.Person)))),
|
|
177
|
+
);
|
|
178
|
+
expect(
|
|
179
|
+
executor.matchesHint(
|
|
180
|
+
makeHint({ spaceIds: makeSpaceSet(SPACE_ID), typenames: makeTypeSet('dxn:type:com.example.unrelated:0.1.0') }),
|
|
181
|
+
),
|
|
182
|
+
).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// QueryExecutor.matchesHint — complex / non-simple queries
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
describe('QueryExecutor.matchesHint — non-simple queries always match', () => {
|
|
191
|
+
test('Filter.not(Filter.or(typeA, typeB)) query always matches (inverted TypeSelector → isSimple=false)', ({
|
|
192
|
+
expect,
|
|
193
|
+
}) => {
|
|
194
|
+
const executor = makeExecutor(
|
|
195
|
+
withSpace(
|
|
196
|
+
Query.select(Filter.not(Filter.or(Filter.type(TestSchema.Organization), Filter.type(TestSchema.Person)))),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
// Even a completely disjoint hint should match for non-simple queries.
|
|
200
|
+
const hint = makeHint({
|
|
201
|
+
spaceIds: makeSpaceSet(SpaceId.random()),
|
|
202
|
+
typenames: makeTypeSet('dxn:type:totally.unrelated:0.1.0'),
|
|
203
|
+
});
|
|
204
|
+
expect(executor.matchesHint(hint)).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('Union query (Query.all) always matches (UnionStep → isSimple=false)', ({ expect }) => {
|
|
208
|
+
const executor = new QueryExecutor({
|
|
209
|
+
indexEngine: {} as any,
|
|
210
|
+
runtime: {} as any,
|
|
211
|
+
automergeHost: {} as any,
|
|
212
|
+
spaceStateManager: {} as any,
|
|
213
|
+
queryId: 'test',
|
|
214
|
+
query: Query.all(
|
|
215
|
+
withSpace(Query.select(Filter.type(TestSchema.Person))),
|
|
216
|
+
withSpace(Query.select(Filter.type(TestSchema.Organization))),
|
|
217
|
+
).ast,
|
|
218
|
+
reactivity: 'reactive' as any,
|
|
219
|
+
});
|
|
220
|
+
const hint = makeHint({
|
|
221
|
+
spaceIds: makeSpaceSet(SpaceId.random()),
|
|
222
|
+
typenames: makeTypeSet('dxn:type:unrelated:0.1.0'),
|
|
223
|
+
});
|
|
224
|
+
expect(executor.matchesHint(hint)).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// QueryExecutor.matchesHint — queue-scope derives spaceId
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
describe('QueryExecutor.matchesHint — queue scope derives spaceId', () => {
|
|
233
|
+
test('queue-only scope derives spaceId and matches space-scoped hint', ({ expect }) => {
|
|
234
|
+
const executor = new QueryExecutor({
|
|
235
|
+
indexEngine: {} as any,
|
|
236
|
+
runtime: {} as any,
|
|
237
|
+
automergeHost: {} as any,
|
|
238
|
+
spaceStateManager: {} as any,
|
|
239
|
+
queryId: 'test',
|
|
240
|
+
query: Query.select(Filter.type(TestSchema.Task)).from({ feeds: [QUEUE_DXN] }).ast,
|
|
241
|
+
reactivity: 'reactive' as any,
|
|
242
|
+
});
|
|
243
|
+
// Hint carries the space derived from the queue DXN → should match.
|
|
244
|
+
const matchingHint = makeHint({ spaceIds: makeSpaceSet(QUEUE_SPACE_ID) });
|
|
245
|
+
expect(executor.matchesHint(matchingHint)).toBe(true);
|
|
246
|
+
|
|
247
|
+
// Hint from a different space → should not match.
|
|
248
|
+
const nonMatchingHint = makeHint({ spaceIds: makeSpaceSet(SpaceId.random()) });
|
|
249
|
+
expect(executor.matchesHint(nonMatchingHint)).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('queue-only scope matches queue-scoped hint with the right queueId', ({ expect }) => {
|
|
253
|
+
const executor = new QueryExecutor({
|
|
254
|
+
indexEngine: {} as any,
|
|
255
|
+
runtime: {} as any,
|
|
256
|
+
automergeHost: {} as any,
|
|
257
|
+
spaceStateManager: {} as any,
|
|
258
|
+
queryId: 'test',
|
|
259
|
+
query: Query.select(Filter.type(TestSchema.Task)).from({ feeds: [QUEUE_DXN] }).ast,
|
|
260
|
+
reactivity: 'reactive' as any,
|
|
261
|
+
});
|
|
262
|
+
// Hint constrained to the exact queue → match.
|
|
263
|
+
const matchingHint = makeHint({ queueIds: makeObjectSet(QUEUE_ID) });
|
|
264
|
+
expect(executor.matchesHint(matchingHint)).toBe(true);
|
|
265
|
+
|
|
266
|
+
// Hint with a different queue → no match.
|
|
267
|
+
const nonMatchingHint = makeHint({ queueIds: makeObjectSet(ObjectId.random()) });
|
|
268
|
+
expect(executor.matchesHint(nonMatchingHint)).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
});
|