@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
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/{browser/chunk-WJJXJTNS.mjs → neutral/chunk-46DCRDA7.mjs} +44 -13
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-FR6GJ4S4.mjs → neutral/chunk-LR7UFVQX.mjs} +145 -618
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +1 -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 +49 -292
- 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 +105 -27
- 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 +22 -2
- 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 +17 -11
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +28 -16
- 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 +2 -0
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +10 -4
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +8 -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/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 +12 -6
- 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 +18 -10
- 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 +3 -2
- 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 +7 -6
- 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 +1 -1
- package/dist/types/src/filter/filter-match.d.ts.map +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 +2 -2
- 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 +16 -20
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +59 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +55 -12
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +10 -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 +4 -3
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +7 -7
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +9 -8
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +5 -5
- 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.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 +13 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +56 -54
- package/src/automerge/automerge-host-subduction.test.ts +263 -0
- package/src/automerge/automerge-host.test.ts +145 -32
- package/src/automerge/automerge-host.ts +442 -163
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +284 -77
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +96 -16
- package/src/automerge/collection-synchronizer.ts +125 -26
- package/src/automerge/echo-network-adapter.test.ts +2 -1
- package/src/automerge/echo-network-adapter.ts +44 -32
- package/src/automerge/echo-replicator.ts +30 -16
- 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 +2 -0
- package/src/automerge/leveldb-storage-adapter.ts +70 -4
- package/src/automerge/mesh-echo-replicator-connection.ts +8 -8
- package/src/automerge/mesh-echo-replicator.ts +14 -11
- 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 +27 -10
- package/src/db-host/database-root.ts +19 -7
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +158 -59
- package/src/db-host/echo-host.ts +376 -90
- 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 +108 -111
- 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 +11 -9
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +9 -9
- package/src/edge/echo-edge-replicator.ts +50 -42
- 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 +41 -25
- package/src/filter/filter-match.ts +75 -20
- package/src/metadata/metadata-store.ts +8 -5
- package/src/pipeline/pipeline-stress.test.ts +1 -3
- package/src/pipeline/pipeline.test.ts +0 -1
- package/src/pipeline/pipeline.ts +2 -3
- package/src/query/errors.ts +2 -2
- package/src/query/plan.ts +73 -9
- package/src/query/query-executor.ts +1064 -172
- package/src/query/query-planner.test.ts +854 -86
- package/src/query/query-planner.ts +444 -38
- package/src/space/admission-discovery-extension.ts +1 -1
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +2 -2
- package/src/space/control-pipeline.ts +7 -7
- package/src/space/space-manager.ts +12 -17
- package/src/space/space-protocol.browser.test.ts +13 -12
- package/src/space/space-protocol.test.ts +13 -13
- package/src/space/space-protocol.ts +16 -15
- package/src/space/space.test.ts +2 -2
- package/src/space/space.ts +9 -10
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +0 -1
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +15 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
- package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -5492
- 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-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JQCCVDLT.mjs +0 -2144
- package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs +0 -240
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -13
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -5492
- 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 -764
- 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
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { type DocHandle, type DocumentId } from '@automerge/automerge-repo';
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
7
6
|
import * as Schema from 'effect/Schema';
|
|
8
7
|
|
|
9
8
|
import { DeferredTask, scheduleMicroTask, synchronized } from '@dxos/async';
|
|
10
9
|
import { Stream } from '@dxos/codec-protobuf/stream';
|
|
11
|
-
import { Context, Resource } from '@dxos/context';
|
|
10
|
+
import { type Context, Resource } from '@dxos/context';
|
|
12
11
|
import { raise } from '@dxos/debug';
|
|
13
|
-
import {
|
|
14
|
-
import { type
|
|
12
|
+
import { QueryAST } from '@dxos/echo-protocol';
|
|
13
|
+
import { type RuntimeProvider } from '@dxos/effect';
|
|
14
|
+
import { type IndexEngine } from '@dxos/index-core';
|
|
15
15
|
import { log } from '@dxos/log';
|
|
16
|
-
import { objectPointerCodec } from '@dxos/protocols';
|
|
17
|
-
import { type IndexConfig } from '@dxos/protocols/proto/dxos/echo/indexing';
|
|
18
16
|
import {
|
|
19
17
|
type QueryRequest,
|
|
20
18
|
type QueryResponse,
|
|
@@ -25,11 +23,12 @@ import { trace } from '@dxos/tracing';
|
|
|
25
23
|
|
|
26
24
|
import { type AutomergeHost } from '../automerge';
|
|
27
25
|
import { QueryExecutor } from '../query';
|
|
28
|
-
|
|
26
|
+
import { type InvalidationHint, mergeHints } from './invalidation-hint';
|
|
29
27
|
import type { SpaceStateManager } from './space-state-manager';
|
|
30
28
|
|
|
31
|
-
export type
|
|
32
|
-
|
|
29
|
+
export type QueryServiceProps = {
|
|
30
|
+
indexEngine: IndexEngine;
|
|
31
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
33
32
|
automergeHost: AutomergeHost;
|
|
34
33
|
spaceStateManager: SpaceStateManager;
|
|
35
34
|
};
|
|
@@ -54,6 +53,16 @@ type ActiveQuery = {
|
|
|
54
53
|
close: () => Promise<void>;
|
|
55
54
|
};
|
|
56
55
|
|
|
56
|
+
type QueryInvalidationStats = {
|
|
57
|
+
totalInvalidations: number;
|
|
58
|
+
catchAllInvalidations: number;
|
|
59
|
+
hintedInvalidations: number;
|
|
60
|
+
totalDirtyQueriesExecuted: number;
|
|
61
|
+
totalExecutionBatches: number;
|
|
62
|
+
averageDirtyPerBatch: number;
|
|
63
|
+
averageQueriesActive: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
57
66
|
@trace.resource()
|
|
58
67
|
export class QueryServiceImpl extends Resource implements QueryService {
|
|
59
68
|
// TODO(dmaretskyi): We need to implement query deduping. Idle composer has 80 queries with only 10 being unique.
|
|
@@ -61,8 +70,22 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
61
70
|
|
|
62
71
|
private _updateQueries!: DeferredTask;
|
|
63
72
|
|
|
73
|
+
// 'all' = catch-all; null = no pending hint.
|
|
74
|
+
#pendingHint: InvalidationHint | 'all' | null = null;
|
|
75
|
+
|
|
76
|
+
// Diagnostic counters.
|
|
77
|
+
#stats: QueryInvalidationStats = {
|
|
78
|
+
totalInvalidations: 0,
|
|
79
|
+
catchAllInvalidations: 0,
|
|
80
|
+
hintedInvalidations: 0,
|
|
81
|
+
totalDirtyQueriesExecuted: 0,
|
|
82
|
+
totalExecutionBatches: 0,
|
|
83
|
+
averageDirtyPerBatch: 0,
|
|
84
|
+
averageQueriesActive: 0,
|
|
85
|
+
};
|
|
86
|
+
|
|
64
87
|
// TODO(burdon): OK for options, but not params. Pass separately and type readonly here.
|
|
65
|
-
constructor(private readonly _params:
|
|
88
|
+
constructor(private readonly _params: QueryServiceProps) {
|
|
66
89
|
super();
|
|
67
90
|
|
|
68
91
|
trace.diagnostic({
|
|
@@ -78,12 +101,16 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
78
101
|
});
|
|
79
102
|
},
|
|
80
103
|
});
|
|
104
|
+
|
|
105
|
+
trace.diagnostic<QueryInvalidationStats>({
|
|
106
|
+
id: 'query-invalidation',
|
|
107
|
+
name: 'Query Invalidation',
|
|
108
|
+
fetch: async () => ({ ...this.#stats }),
|
|
109
|
+
});
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
override async _open(): Promise<void> {
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
this._updateQueries = new DeferredTask(this._ctx, this._executeQueries.bind(this));
|
|
113
|
+
this._updateQueries = new DeferredTask(this._ctx, () => this._executeQueries(this._ctx));
|
|
87
114
|
}
|
|
88
115
|
|
|
89
116
|
@synchronized
|
|
@@ -92,18 +119,23 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
92
119
|
await Promise.all(Array.from(this._queries).map((query) => query.close()));
|
|
93
120
|
}
|
|
94
121
|
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
/**
|
|
123
|
+
* @deprecated No longer needed with SQL-based indexing.
|
|
124
|
+
*/
|
|
125
|
+
async setConfig(): Promise<void> {
|
|
126
|
+
// No-op: SQL indexer doesn't need explicit configuration.
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @deprecated No longer needed with SQL-based indexing.
|
|
131
|
+
*/
|
|
132
|
+
async reindex(): Promise<void> {
|
|
133
|
+
// No-op: SQL indexer handles re-indexing automatically.
|
|
134
|
+
log.warn('reindex() is deprecated and no longer has any effect');
|
|
97
135
|
}
|
|
98
136
|
|
|
99
137
|
execQuery(request: QueryRequest): Stream<QueryResponse> {
|
|
100
138
|
return new Stream<QueryResponse>(({ next, close, ctx }) => {
|
|
101
|
-
if (this._params.indexer.config?.enabled !== true) {
|
|
102
|
-
log.error('indexer is disabled', { config: this._params.indexer.config });
|
|
103
|
-
close();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
139
|
const queryEntry = this._createQuery(ctx, request, next, close, close);
|
|
108
140
|
scheduleMicroTask(ctx, async () => {
|
|
109
141
|
await queryEntry.executor.open();
|
|
@@ -115,32 +147,20 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
115
147
|
}
|
|
116
148
|
|
|
117
149
|
/**
|
|
118
|
-
*
|
|
150
|
+
* Schedule re-execution of queries, optionally guided by a targeted hint.
|
|
151
|
+
* When called without a hint, all queries are marked dirty (catch-all invalidation).
|
|
119
152
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
log('Collected documents...', { count: ids.size });
|
|
153
|
+
invalidateQueries(hint?: InvalidationHint): void {
|
|
154
|
+
this.#stats.totalInvalidations++;
|
|
155
|
+
if (!hint) {
|
|
156
|
+
this.#pendingHint = 'all';
|
|
157
|
+
this.#stats.catchAllInvalidations++;
|
|
158
|
+
} else {
|
|
159
|
+
this.#stats.hintedInvalidations++;
|
|
160
|
+
if (this.#pendingHint !== 'all') {
|
|
161
|
+
this.#pendingHint = this.#pendingHint ? mergeHints(this.#pendingHint, hint) : hint;
|
|
130
162
|
}
|
|
131
163
|
}
|
|
132
|
-
|
|
133
|
-
log('Marking all documents as dirty...', { count: ids.size });
|
|
134
|
-
await this._params.indexer.reindex(ids);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Schedule re-execution of all queries.
|
|
139
|
-
*/
|
|
140
|
-
invalidateQueries() {
|
|
141
|
-
for (const query of this._queries) {
|
|
142
|
-
query.dirty = true;
|
|
143
|
-
}
|
|
144
164
|
this._updateQueries.schedule();
|
|
145
165
|
}
|
|
146
166
|
|
|
@@ -154,7 +174,8 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
154
174
|
const parsedQuery = QueryAST.Query.pipe(Schema.decodeUnknownSync)(JSON.parse(request.query));
|
|
155
175
|
const queryEntry: ActiveQuery = {
|
|
156
176
|
executor: new QueryExecutor({
|
|
157
|
-
|
|
177
|
+
indexEngine: this._params.indexEngine,
|
|
178
|
+
runtime: this._params.runtime,
|
|
158
179
|
automergeHost: this._params.automergeHost,
|
|
159
180
|
queryId: request.queryId ?? raise(new Error('query id required')),
|
|
160
181
|
query: parsedQuery,
|
|
@@ -181,17 +202,39 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
181
202
|
return queryEntry;
|
|
182
203
|
}
|
|
183
204
|
|
|
184
|
-
@trace.span({ showInBrowserTimeline: true })
|
|
185
|
-
private async _executeQueries() {
|
|
186
|
-
|
|
205
|
+
@trace.span({ showInBrowserTimeline: true, showInRemoteTracing: false })
|
|
206
|
+
private async _executeQueries(_ctx: Context) {
|
|
207
|
+
const hint = this.#pendingHint;
|
|
208
|
+
this.#pendingHint = null;
|
|
209
|
+
|
|
210
|
+
// Apply hint to determine which queries need re-execution.
|
|
211
|
+
for (const query of this._queries) {
|
|
212
|
+
if (!query.open) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (query.firstResult) {
|
|
216
|
+
// First run is always executed regardless of hint.
|
|
217
|
+
query.dirty = true;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (hint === 'all') {
|
|
221
|
+
query.dirty = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (hint && query.executor.matchesHint(hint)) {
|
|
225
|
+
query.dirty = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
187
229
|
const begin = performance.now();
|
|
188
|
-
let
|
|
230
|
+
let dirtyCount = 0;
|
|
231
|
+
const activeCount = this._queries.size;
|
|
189
232
|
await Promise.all(
|
|
190
233
|
Array.from(this._queries).map(async (query) => {
|
|
191
234
|
if (!query.dirty || !query.open) {
|
|
192
235
|
return;
|
|
193
236
|
}
|
|
194
|
-
|
|
237
|
+
dirtyCount++;
|
|
195
238
|
|
|
196
239
|
try {
|
|
197
240
|
const { changed } = await query.executor.execQuery();
|
|
@@ -201,68 +244,22 @@ export class QueryServiceImpl extends Resource implements QueryService {
|
|
|
201
244
|
query.sendResults(query.executor.getResults());
|
|
202
245
|
}
|
|
203
246
|
} catch (err) {
|
|
204
|
-
log.catch(err
|
|
247
|
+
log.catch(err, {
|
|
248
|
+
queryId: query.executor.queryId,
|
|
249
|
+
query: JSON.stringify(query.executor.query),
|
|
250
|
+
});
|
|
251
|
+
query.onError(err as Error);
|
|
205
252
|
}
|
|
206
253
|
}),
|
|
207
254
|
);
|
|
208
|
-
log.verbose('executed queries', { count, duration: performance.now() - begin });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Factory for `getAllDocuments` iterator.
|
|
214
|
-
*/
|
|
215
|
-
// TODO(dmaretskyi): Get roots from echo-host.
|
|
216
|
-
const createDocumentsIterator = (automergeHost: AutomergeHost) =>
|
|
217
|
-
/**
|
|
218
|
-
* Recursively get all object data blobs from loaded documents from Automerge Repo.
|
|
219
|
-
*/
|
|
220
|
-
// TODO(mykola): Unload automerge handles after usage.
|
|
221
|
-
async function* getAllDocuments(): AsyncGenerator<ObjectSnapshot[], void, void> {
|
|
222
|
-
/** visited automerge handles */
|
|
223
|
-
const visited = new Set<string>();
|
|
224
|
-
|
|
225
|
-
async function* getObjectsFromHandle(handle: DocHandle<DatabaseDirectory>): AsyncGenerator<ObjectSnapshot[]> {
|
|
226
|
-
if (visited.has(handle.documentId) || !handle.isReady()) {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const doc = handle.doc()!;
|
|
231
|
-
const spaceKey = DatabaseDirectory.getSpaceKey(doc) ?? undefined;
|
|
232
|
-
if (doc.objects) {
|
|
233
|
-
yield Object.entries(doc.objects as { [key: string]: any }).map(([objectId, object]) => {
|
|
234
|
-
return {
|
|
235
|
-
id: objectPointerCodec.encode({ documentId: handle.documentId, objectId, spaceKey }),
|
|
236
|
-
object,
|
|
237
|
-
heads: getHeads(doc),
|
|
238
|
-
};
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (doc.links) {
|
|
243
|
-
for (const id of Object.values(doc.links as { [echoId: string]: string })) {
|
|
244
|
-
const urlString = id.toString();
|
|
245
|
-
if (visited.has(urlString)) {
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const linkHandle = await automergeHost.loadDoc<DatabaseDirectory>(Context.default(), urlString as DocumentId);
|
|
249
|
-
for await (const result of getObjectsFromHandle(linkHandle)) {
|
|
250
|
-
yield result;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
this.#stats.totalExecutionBatches++;
|
|
257
|
+
this.#stats.totalDirtyQueriesExecuted += dirtyCount;
|
|
258
|
+
this.#stats.averageDirtyPerBatch = this.#stats.totalDirtyQueriesExecuted / this.#stats.totalExecutionBatches;
|
|
259
|
+
this.#stats.averageQueriesActive =
|
|
260
|
+
(this.#stats.averageQueriesActive * (this.#stats.totalExecutionBatches - 1) + activeCount) /
|
|
261
|
+
this.#stats.totalExecutionBatches;
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
for await (const result of getObjectsFromHandle(handle)) {
|
|
264
|
-
yield result;
|
|
265
|
-
}
|
|
266
|
-
visited.add(handle.documentId);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
263
|
+
log.verbose('executed queries', { dirty: dirtyCount, active: activeCount, duration: performance.now() - begin });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
|
|
8
|
+
import { type Context } from '@dxos/context';
|
|
9
|
+
import { EchoFeedCodec } from '@dxos/echo-protocol';
|
|
10
|
+
import { type ObjectJSON } from '@dxos/echo/internal';
|
|
11
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
12
|
+
import { type FeedStore } from '@dxos/feed';
|
|
13
|
+
import { type DataSourceCursor, type IndexDataSource, type IndexerObject } from '@dxos/index-core';
|
|
14
|
+
import { failedInvariant } from '@dxos/invariant';
|
|
15
|
+
import type { SpaceId } from '@dxos/keys';
|
|
16
|
+
import { log } from '@dxos/log';
|
|
17
|
+
import { FeedProtocol } from '@dxos/protocols';
|
|
18
|
+
|
|
19
|
+
export type QueueDataSourceOptions = {
|
|
20
|
+
feedStore: FeedStore;
|
|
21
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
22
|
+
/**
|
|
23
|
+
* Callback to get the list of space IDs that should be indexed.
|
|
24
|
+
*/
|
|
25
|
+
getSpaceIds: () => SpaceId[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Feed namespaces to index.
|
|
29
|
+
* @default [WellKnownNamespaces.data, WellKnownNamespaces.trace]
|
|
30
|
+
*/
|
|
31
|
+
feedNamespaces?: string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class QueueDataSource implements IndexDataSource {
|
|
35
|
+
readonly sourceName = 'queue';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Feed namespaces to index.
|
|
39
|
+
*/
|
|
40
|
+
private readonly _feedNamespaces: string[];
|
|
41
|
+
private readonly _feedStore: FeedStore;
|
|
42
|
+
private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
43
|
+
private readonly _getSpaceIds: () => SpaceId[];
|
|
44
|
+
|
|
45
|
+
constructor(options: QueueDataSourceOptions) {
|
|
46
|
+
this._feedStore = options.feedStore;
|
|
47
|
+
this._runtime = options.runtime;
|
|
48
|
+
this._getSpaceIds = options.getSpaceIds;
|
|
49
|
+
this._feedNamespaces = options.feedNamespaces ?? [
|
|
50
|
+
FeedProtocol.WellKnownNamespaces.data,
|
|
51
|
+
FeedProtocol.WellKnownNamespaces.trace,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getChangedObjects(
|
|
56
|
+
_ctx: Context,
|
|
57
|
+
cursors: DataSourceCursor[],
|
|
58
|
+
opts?: { limit?: number },
|
|
59
|
+
): Effect.Effect<{ objects: IndexerObject[]; cursors: DataSourceCursor[] }> {
|
|
60
|
+
// For queue, the cursor is assumed to have:
|
|
61
|
+
// spaceId = set
|
|
62
|
+
// resourceId = null
|
|
63
|
+
// cursor = feed cursor
|
|
64
|
+
|
|
65
|
+
// We also add new cursors from all previously unindexed spaces.
|
|
66
|
+
|
|
67
|
+
return Effect.gen(this, function* () {
|
|
68
|
+
const objects: IndexerObject[] = [];
|
|
69
|
+
const updatedCursors: DataSourceCursor[] = [];
|
|
70
|
+
|
|
71
|
+
// Delete old cursors that did not have feed namespace set.
|
|
72
|
+
let augmentedCursors = [...cursors].filter((c) => c.resourceId !== null);
|
|
73
|
+
for (const spaceId of this._getSpaceIds()) {
|
|
74
|
+
for (const feedNamespace of this._feedNamespaces) {
|
|
75
|
+
if (!augmentedCursors.some((c) => c.spaceId === spaceId && c.resourceId === feedNamespace)) {
|
|
76
|
+
// Add a new cursor for this space starting from the beginning.
|
|
77
|
+
// Empty string cursor means "start from beginning".
|
|
78
|
+
augmentedCursors.push({
|
|
79
|
+
spaceId,
|
|
80
|
+
resourceId: feedNamespace,
|
|
81
|
+
cursor: '',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Limit per call, but we might have multiple spaces.
|
|
88
|
+
// We should distribute limit or just fill up to limit.
|
|
89
|
+
let remainingLimit = opts?.limit ?? Infinity;
|
|
90
|
+
|
|
91
|
+
for (const cursor of augmentedCursors) {
|
|
92
|
+
if (remainingLimit <= 0) {
|
|
93
|
+
// Remaining cursors are not processed in this call.
|
|
94
|
+
updatedCursors.push(cursor);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!cursor.spaceId) {
|
|
98
|
+
// Ignore cursors without spaceId.
|
|
99
|
+
updatedCursors.push(cursor);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
FeedProtocol.WellKnownNamespaces.data !== cursor.resourceId &&
|
|
104
|
+
FeedProtocol.WellKnownNamespaces.trace !== cursor.resourceId
|
|
105
|
+
) {
|
|
106
|
+
// TODO(dmaretskyi): Update/remove this check when adding new feed namespaces.
|
|
107
|
+
log.warn('Ignoring cursor with invaliding feed namespace', { namespace: cursor.resourceId });
|
|
108
|
+
// Ignore cursors with unknown resourceId.
|
|
109
|
+
updatedCursors.push(cursor);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Empty string cursor means "start from beginning".
|
|
114
|
+
const currentCursor =
|
|
115
|
+
typeof cursor.cursor === 'string' && cursor.cursor !== ''
|
|
116
|
+
? FeedProtocol.FeedCursor.make(cursor.cursor)
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = yield* this._feedStore.query({
|
|
121
|
+
spaceId: cursor.spaceId,
|
|
122
|
+
feedNamespace: cursor.resourceId,
|
|
123
|
+
cursor: currentCursor,
|
|
124
|
+
limit: remainingLimit,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Process blocks
|
|
128
|
+
for (const block of result.blocks) {
|
|
129
|
+
try {
|
|
130
|
+
const data = EchoFeedCodec.decode(block.data) as ObjectJSON;
|
|
131
|
+
|
|
132
|
+
objects.push({
|
|
133
|
+
spaceId: cursor.spaceId,
|
|
134
|
+
queueId: block.feedId ?? failedInvariant(),
|
|
135
|
+
queueNamespace: cursor.resourceId,
|
|
136
|
+
documentId: null,
|
|
137
|
+
recordId: null,
|
|
138
|
+
data,
|
|
139
|
+
updatedAt: block.timestamp,
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.warn('Failed to parse block data for indexing', { spaceId: cursor.spaceId, err });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
remainingLimit -= result.blocks.length;
|
|
147
|
+
updatedCursors.push({
|
|
148
|
+
spaceId: cursor.spaceId,
|
|
149
|
+
resourceId: cursor.resourceId,
|
|
150
|
+
cursor: result.nextCursor,
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
log.error('Error querying queue for indexing', { spaceId: cursor.spaceId, error });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { objects, cursors: updatedCursors };
|
|
158
|
+
}).pipe(RuntimeProvider.provide(this._runtime), Effect.withSpan('QueueDataSource.getChangedObjects'), Effect.orDie);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as SqliteClient from '@effect/sql-sqlite-node/SqliteClient';
|
|
6
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
import { describe, expect, it } from '@effect/vitest';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Layer from 'effect/Layer';
|
|
10
|
+
|
|
11
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
12
|
+
import { FeedStore } from '@dxos/feed';
|
|
13
|
+
import { ObjectId, SpaceId } from '@dxos/keys';
|
|
14
|
+
import { FeedProtocol } from '@dxos/protocols';
|
|
15
|
+
import { SqlTransaction } from '@dxos/sql-sqlite';
|
|
16
|
+
|
|
17
|
+
import { LocalQueueServiceImpl } from './local-queue-service';
|
|
18
|
+
|
|
19
|
+
const TestLayer = SqlTransaction.layer.pipe(
|
|
20
|
+
Layer.provideMerge(
|
|
21
|
+
SqliteClient.layer({
|
|
22
|
+
filename: ':memory:',
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
describe('LocalQueueServiceImpl', () => {
|
|
28
|
+
it.effect('should insert and query items', () =>
|
|
29
|
+
Effect.gen(function* () {
|
|
30
|
+
const feedStore = new FeedStore({ localActorId: 'actor-id', assignPositions: true });
|
|
31
|
+
const runtime = yield* RuntimeProvider.currentRuntime<SqlClient.SqlClient | SqlTransaction.SqlTransaction>();
|
|
32
|
+
const service = new LocalQueueServiceImpl(runtime, feedStore);
|
|
33
|
+
yield* feedStore.migrate();
|
|
34
|
+
|
|
35
|
+
const spaceId = SpaceId.random();
|
|
36
|
+
const queueId = ObjectId.random();
|
|
37
|
+
const object1 = { id: 'obj1', data: 'test1' };
|
|
38
|
+
const object2 = { id: 'obj2', data: 'test2' };
|
|
39
|
+
|
|
40
|
+
yield* Effect.promise(() =>
|
|
41
|
+
service.insertIntoQueue({
|
|
42
|
+
subspaceTag: FeedProtocol.WellKnownNamespaces.data,
|
|
43
|
+
spaceId,
|
|
44
|
+
queueId,
|
|
45
|
+
objects: [object1, object2].map((obj) => JSON.stringify(obj)),
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const result = yield* Effect.promise(() =>
|
|
50
|
+
service.queryQueue({
|
|
51
|
+
query: { spaceId, queueIds: [queueId] },
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
expect(JSON.parse(result.objects![0])).toMatchObject(object1);
|
|
55
|
+
expect(JSON.parse(result.objects![1])).toMatchObject(object2);
|
|
56
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
it.effect('should delete items', () =>
|
|
60
|
+
Effect.gen(function* () {
|
|
61
|
+
const runtime = Effect.succeed(yield* Effect.runtime<any>());
|
|
62
|
+
const feedStore = new FeedStore({ localActorId: 'actor-id', assignPositions: true });
|
|
63
|
+
const service = new LocalQueueServiceImpl(runtime, feedStore);
|
|
64
|
+
yield* feedStore.migrate();
|
|
65
|
+
|
|
66
|
+
const spaceId = SpaceId.random();
|
|
67
|
+
const queueId = ObjectId.random();
|
|
68
|
+
const object1Id = ObjectId.random();
|
|
69
|
+
const object1 = { id: object1Id, data: 'test1' };
|
|
70
|
+
|
|
71
|
+
yield* Effect.promise(() =>
|
|
72
|
+
service.insertIntoQueue({
|
|
73
|
+
subspaceTag: FeedProtocol.WellKnownNamespaces.data,
|
|
74
|
+
spaceId,
|
|
75
|
+
queueId,
|
|
76
|
+
objects: [JSON.stringify(object1)],
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
yield* Effect.promise(() =>
|
|
80
|
+
service.deleteFromQueue({
|
|
81
|
+
subspaceTag: FeedProtocol.WellKnownNamespaces.data,
|
|
82
|
+
spaceId,
|
|
83
|
+
queueId,
|
|
84
|
+
objectIds: [object1Id],
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const result = yield* Effect.promise(() =>
|
|
89
|
+
service.queryQueue({
|
|
90
|
+
query: { spaceId, queueIds: [queueId] },
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
expect(result.objects).toHaveLength(2);
|
|
94
|
+
expect(JSON.parse(result.objects![1])).toMatchObject({ id: object1Id, '@deleted': true });
|
|
95
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
it.effect('should support pagination', () =>
|
|
99
|
+
Effect.gen(function* () {
|
|
100
|
+
const runtime = Effect.succeed(yield* Effect.runtime<any>());
|
|
101
|
+
const feedStore = new FeedStore({ localActorId: 'actor-id', assignPositions: true });
|
|
102
|
+
yield* feedStore.migrate();
|
|
103
|
+
const service = new LocalQueueServiceImpl(runtime, feedStore);
|
|
104
|
+
const spaceId = 'space-1' as SpaceId;
|
|
105
|
+
const queueId = ObjectId.random();
|
|
106
|
+
|
|
107
|
+
// Insert 10 items
|
|
108
|
+
const items = Array.from({ length: 10 }, (_, i) => ({ id: `obj${i}`, data: `test${i}` }));
|
|
109
|
+
yield* Effect.promise(() =>
|
|
110
|
+
service.insertIntoQueue({
|
|
111
|
+
subspaceTag: FeedProtocol.WellKnownNamespaces.data,
|
|
112
|
+
spaceId,
|
|
113
|
+
queueId,
|
|
114
|
+
objects: items.map((item) => JSON.stringify(item)),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Query first 5
|
|
119
|
+
const page1 = yield* Effect.promise(() =>
|
|
120
|
+
service.queryQueue({
|
|
121
|
+
query: { spaceId, queueIds: [queueId], limit: 5 },
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
expect(page1.objects).toHaveLength(5);
|
|
125
|
+
expect(JSON.parse(page1.objects![0])).toMatchObject(items[0]);
|
|
126
|
+
expect(JSON.parse(page1.objects![4])).toMatchObject(items[4]);
|
|
127
|
+
expect(page1.nextCursor).toBeDefined();
|
|
128
|
+
|
|
129
|
+
// Query next 5
|
|
130
|
+
const page2 = yield* Effect.promise(() =>
|
|
131
|
+
service.queryQueue({
|
|
132
|
+
query: {
|
|
133
|
+
spaceId,
|
|
134
|
+
queueIds: [queueId],
|
|
135
|
+
limit: 5,
|
|
136
|
+
after: page1.nextCursor!,
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
expect(page2.objects).toHaveLength(5);
|
|
141
|
+
expect(JSON.parse(page2.objects![0])).toMatchObject(items[5]);
|
|
142
|
+
expect(JSON.parse(page2.objects![4])).toMatchObject(items[9]);
|
|
143
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
6
|
-
import isEqual from '
|
|
5
|
+
import { type DocumentId, type DocumentQuery, interpretAsDocumentId } from '@automerge/automerge-repo';
|
|
6
|
+
import isEqual from 'fast-deep-equal';
|
|
7
7
|
|
|
8
8
|
import { Event, UpdateScheduler } from '@dxos/async';
|
|
9
9
|
import { Context, LifecycleState, Resource } from '@dxos/context';
|
|
@@ -32,6 +32,10 @@ export class SpaceStateManager extends Resource {
|
|
|
32
32
|
return this._roots;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
get spaceIds(): SpaceId[] {
|
|
36
|
+
return Array.from(this._rootBySpace.keys());
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
getRootByDocumentId(documentId: DocumentId): DatabaseRoot | undefined {
|
|
36
40
|
return this._roots.get(documentId);
|
|
37
41
|
}
|
|
@@ -49,13 +53,13 @@ export class SpaceStateManager extends Resource {
|
|
|
49
53
|
return this._roots.get(documentId);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
async assignRootToSpace(spaceId: SpaceId,
|
|
56
|
+
async assignRootToSpace(spaceId: SpaceId, query: DocumentQuery<DatabaseDirectory>): Promise<DatabaseRoot> {
|
|
53
57
|
let root: DatabaseRoot;
|
|
54
|
-
if (this._roots.has(
|
|
55
|
-
root = this._roots.get(
|
|
58
|
+
if (this._roots.has(query.documentId)) {
|
|
59
|
+
root = this._roots.get(query.documentId)!;
|
|
56
60
|
} else {
|
|
57
|
-
root = new DatabaseRoot(
|
|
58
|
-
this._roots.set(
|
|
61
|
+
root = new DatabaseRoot(query);
|
|
62
|
+
this._roots.set(query.documentId, root);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
if (this._rootBySpace.get(spaceId) === root.handle.documentId) {
|
|
@@ -73,8 +77,6 @@ export class SpaceStateManager extends Resource {
|
|
|
73
77
|
|
|
74
78
|
this._perRootContext.set(root.handle.documentId, ctx);
|
|
75
79
|
|
|
76
|
-
await root.handle.whenReady();
|
|
77
|
-
|
|
78
80
|
const documentListCheckScheduler = new UpdateScheduler(
|
|
79
81
|
ctx,
|
|
80
82
|
async () => {
|