@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc674ce
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-WJJXJTNS.mjs → chunk-FJPXA75J.mjs} +7 -12
- package/dist/lib/browser/chunk-FJPXA75J.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FR6GJ4S4.mjs → chunk-WIQUYO7H.mjs} +39 -29
- package/dist/lib/browser/chunk-WIQUYO7H.mjs.map +7 -0
- package/dist/lib/browser/filter/index.mjs +1 -2
- package/dist/lib/browser/index.mjs +1933 -620
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +28 -201
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/{chunk-JQCCVDLT.mjs → chunk-7PL6UK4B.mjs} +39 -29
- package/dist/lib/node-esm/chunk-7PL6UK4B.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-LEQ77KAT.mjs → chunk-S5KGVQFZ.mjs} +7 -12
- package/dist/lib/node-esm/chunk-S5KGVQFZ.mjs.map +7 -0
- package/dist/lib/node-esm/filter/index.mjs +1 -2
- package/dist/lib/node-esm/index.mjs +1933 -620
- 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 +28 -201
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/automerge/automerge-host.d.ts +36 -9
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- 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/collection-synchronizer.d.ts +3 -2
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +11 -6
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +9 -6
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- 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/leveldb-storage-adapter.d.ts +4 -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 +7 -7
- 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/db-host/automerge-data-source.d.ts +34 -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/data-service.d.ts +4 -3
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +43 -11
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +1 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/local-queue-service.d.ts +15 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +7 -2
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +28 -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 +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +10 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +2 -2
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +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/pipeline.d.ts +2 -2
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +11 -15
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +37 -3
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +39 -3
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +6 -0
- package/dist/types/src/query/query-planner.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 +2 -2
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +6 -6
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +5 -5
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +3 -3
- package/dist/types/src/space/space.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-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 +6 -6
- 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 +48 -38
- package/src/automerge/automerge-host.test.ts +37 -14
- package/src/automerge/automerge-host.ts +130 -36
- package/src/automerge/automerge-repo.test.ts +147 -30
- package/src/automerge/collection-synchronizer.test.ts +16 -10
- package/src/automerge/collection-synchronizer.ts +41 -11
- package/src/automerge/echo-network-adapter.ts +23 -11
- package/src/automerge/echo-replicator.ts +10 -6
- package/src/automerge/heads-store.ts +14 -2
- package/src/automerge/leveldb-storage-adapter.ts +4 -4
- package/src/automerge/mesh-echo-replicator-connection.ts +7 -7
- package/src/automerge/mesh-echo-replicator.ts +2 -2
- package/src/db-host/automerge-data-source.test.ts +294 -0
- package/src/db-host/automerge-data-source.ts +144 -0
- package/src/db-host/data-service.ts +10 -3
- package/src/db-host/documents-iterator.ts +3 -1
- package/src/db-host/documents-synchronizer.test.ts +77 -13
- package/src/db-host/documents-synchronizer.ts +51 -50
- package/src/db-host/echo-host.ts +191 -28
- package/src/db-host/index.ts +1 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-service.ts +17 -4
- package/src/db-host/queue-data-source.ts +132 -0
- package/src/db-host/queue-service.test.ts +141 -0
- package/src/db-host/space-state-manager.ts +4 -0
- package/src/db-host/stub.ts +28 -0
- package/src/edge/echo-edge-replicator.ts +12 -8
- package/src/filter/filter-match.test.ts +31 -25
- package/src/filter/filter-match.ts +8 -14
- package/src/metadata/metadata-store.ts +5 -2
- package/src/pipeline/pipeline.ts +2 -2
- package/src/query/errors.ts +2 -2
- package/src/query/plan.ts +48 -3
- package/src/query/query-executor.ts +612 -131
- package/src/query/query-planner.test.ts +509 -35
- package/src/query/query-planner.ts +187 -13
- package/src/space/admission-discovery-extension.ts +1 -1
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.ts +2 -2
- package/src/space/space-manager.ts +6 -6
- package/src/space/space-protocol.ts +11 -11
- package/src/space/space.ts +3 -3
- package/src/testing/index.ts +0 -2
- package/src/testing/test-network-adapter.ts +16 -3
- package/src/testing/test-replicator.ts +8 -7
- 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/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.map +0 -7
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
- 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/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { next as A, type Heads } from '@automerge/automerge';
|
|
6
|
-
import { type DocHandle, type DocumentId
|
|
6
|
+
import { type DocHandle, type DocumentId } from '@automerge/automerge-repo';
|
|
7
7
|
|
|
8
|
-
import { UpdateScheduler
|
|
9
|
-
import { LifecycleState, Resource
|
|
8
|
+
import { UpdateScheduler } from '@dxos/async';
|
|
9
|
+
import { Context, LifecycleState, Resource } from '@dxos/context';
|
|
10
10
|
import { type DatabaseDirectory } from '@dxos/echo-protocol';
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
13
|
import { type BatchedDocumentUpdates, type DocumentUpdate } from '@dxos/protocols/proto/dxos/echo/service';
|
|
14
|
+
import { retry } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
import type { AutomergeHost } from '../automerge';
|
|
14
17
|
|
|
15
18
|
const MAX_UPDATE_FREQ = 10; // [updates/sec]
|
|
16
19
|
|
|
17
|
-
export type
|
|
18
|
-
|
|
20
|
+
export type DocumentsSynchronizerProps = {
|
|
21
|
+
automergeHost: AutomergeHost;
|
|
19
22
|
sendUpdates: (updates: BatchedDocumentUpdates) => void;
|
|
20
23
|
};
|
|
21
24
|
|
|
@@ -44,36 +47,38 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
44
47
|
*/
|
|
45
48
|
private _sendUpdatesJob?: UpdateScheduler = undefined;
|
|
46
49
|
|
|
47
|
-
constructor(private readonly _params:
|
|
50
|
+
constructor(private readonly _params: DocumentsSynchronizerProps) {
|
|
48
51
|
super();
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
addDocuments(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.addDocuments([documentId], retryCounter + 1, wrapAroundRetryDelay * 2),
|
|
54
|
+
async addDocuments(documentIds: DocumentId[]): Promise<void> {
|
|
55
|
+
await Promise.all(
|
|
56
|
+
documentIds.map(async (documentId) => {
|
|
57
|
+
try {
|
|
58
|
+
await retry(
|
|
59
|
+
{ count: WRAP_AROUND_RETRY_LIMIT, delayMs: WRAP_AROUND_RETRY_INITIAL_DELAY, exponent: 2 },
|
|
60
|
+
async () => {
|
|
61
|
+
try {
|
|
62
|
+
log('loading document', { documentId });
|
|
63
|
+
const doc = await this._params.automergeHost.loadDoc<DatabaseDirectory>(
|
|
64
|
+
Context.default(),
|
|
65
|
+
documentId as DocumentId,
|
|
66
|
+
{ fetchFromNetwork: true },
|
|
67
|
+
);
|
|
68
|
+
this._startSync(doc);
|
|
69
|
+
this._pendingUpdates.add(doc.documentId);
|
|
70
|
+
this._sendUpdatesJob!.trigger();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log.warn('failed to load document', { err });
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
74
76
|
);
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
+
} catch (err) {
|
|
78
|
+
log.catch(err);
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
removeDocuments(documentIds: DocumentId[]): void {
|
|
@@ -96,11 +101,11 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
async update(updates: DocumentUpdate[]): Promise<void> {
|
|
99
|
-
for (const { documentId, mutation
|
|
100
|
-
this._writeMutation(documentId as DocumentId, mutation
|
|
104
|
+
for (const { documentId, mutation } of updates) {
|
|
105
|
+
await this._writeMutation(documentId as DocumentId, mutation);
|
|
101
106
|
}
|
|
102
107
|
// TODO(mykola): This should not be required.
|
|
103
|
-
await this._params.
|
|
108
|
+
await this._params.automergeHost.flush({ documentIds: updates.map(({ documentId }) => documentId as DocumentId) });
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
private _startSync(doc: DocHandle<DatabaseDirectory>) {
|
|
@@ -161,25 +166,21 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
161
166
|
return mutation;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
private _writeMutation(documentId: DocumentId, mutation: Uint8Array
|
|
169
|
+
private async _writeMutation(documentId: DocumentId, mutation: Uint8Array): Promise<void> {
|
|
165
170
|
if (this._lifecycleState === LifecycleState.CLOSED) {
|
|
166
171
|
return;
|
|
167
172
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (A.equals(headsBefore, syncState!.lastSentHead)) {
|
|
180
|
-
// No new mutations were discovered on network, so we do not need to send updates from worker to client.
|
|
181
|
-
syncState!.lastSentHead = A.getHeads(syncState.handle.doc());
|
|
182
|
-
}
|
|
173
|
+
log('write mutation', { documentId });
|
|
174
|
+
|
|
175
|
+
const syncState = this._syncStates.get(documentId);
|
|
176
|
+
invariant(syncState, 'Sync state for document not found');
|
|
177
|
+
const headsBefore = A.getHeads(syncState.handle.doc());
|
|
178
|
+
// This will update corresponding handle in the repo.
|
|
179
|
+
await this._params.automergeHost.createDoc(mutation, { documentId, preserveHistory: true });
|
|
180
|
+
|
|
181
|
+
if (A.equals(headsBefore, syncState.lastSentHead)) {
|
|
182
|
+
// No new mutations were discovered on network, so we do not need to send updates from worker to client.
|
|
183
|
+
syncState.lastSentHead = A.getHeads(syncState.handle.doc());
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
}
|
package/src/db-host/echo-host.ts
CHANGED
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type Repo,
|
|
11
|
-
} from '@automerge/automerge-repo';
|
|
12
|
-
|
|
13
|
-
import { type Context, LifecycleState, Resource } from '@dxos/context';
|
|
5
|
+
import { type AnyDocumentId, type AutomergeUrl, type DocHandle, type DocumentId } from '@automerge/automerge-repo';
|
|
6
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
|
|
8
|
+
import { DeferredTask, sleep } from '@dxos/async';
|
|
9
|
+
import { Context, LifecycleState, Resource } from '@dxos/context';
|
|
14
10
|
import { todo } from '@dxos/debug';
|
|
15
11
|
import { type DatabaseDirectory, SpaceDocVersion, createIdFromSpaceKey } from '@dxos/echo-protocol';
|
|
12
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
13
|
+
import { FeedStore } from '@dxos/feed';
|
|
14
|
+
import { IndexEngine } from '@dxos/index-core';
|
|
16
15
|
import { IndexMetadataStore, IndexStore, Indexer } from '@dxos/indexing';
|
|
17
16
|
import { invariant } from '@dxos/invariant';
|
|
18
17
|
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
19
18
|
import { type LevelDB } from '@dxos/kv-store';
|
|
19
|
+
import { log } from '@dxos/log';
|
|
20
|
+
import type { QueueService } from '@dxos/protocols';
|
|
20
21
|
import { IndexKind } from '@dxos/protocols/proto/dxos/echo/indexing';
|
|
22
|
+
import type * as SqlTransaction from '@dxos/sql-sqlite/SqlTransaction';
|
|
21
23
|
import { trace } from '@dxos/tracing';
|
|
22
24
|
|
|
23
25
|
import {
|
|
@@ -26,24 +28,27 @@ import {
|
|
|
26
28
|
EchoDataMonitor,
|
|
27
29
|
type EchoDataStats,
|
|
28
30
|
type EchoReplicator,
|
|
29
|
-
FIND_PARAMS,
|
|
30
31
|
type LoadDocOptions,
|
|
31
32
|
type PeerIdProvider,
|
|
32
33
|
type RootDocumentSpaceKeyProvider,
|
|
33
34
|
deriveCollectionIdFromSpaceId,
|
|
34
35
|
} from '../automerge';
|
|
35
36
|
|
|
37
|
+
import { AutomergeDataSource } from './automerge-data-source';
|
|
36
38
|
import { DataServiceImpl } from './data-service';
|
|
37
39
|
import { type DatabaseRoot } from './database-root';
|
|
38
40
|
import { createSelectedDocumentsIterator } from './documents-iterator';
|
|
41
|
+
import { LocalQueueServiceImpl } from './local-queue-service';
|
|
39
42
|
import { QueryServiceImpl } from './query-service';
|
|
43
|
+
import { QueueDataSource } from './queue-data-source';
|
|
40
44
|
import { SpaceStateManager } from './space-state-manager';
|
|
45
|
+
import { QueueServiceStub } from './stub';
|
|
41
46
|
|
|
42
47
|
export interface EchoHostIndexingConfig {
|
|
43
48
|
/**
|
|
44
|
-
* @default
|
|
49
|
+
* @default false
|
|
45
50
|
*/
|
|
46
|
-
|
|
51
|
+
sqlIndex: boolean;
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* @default false
|
|
@@ -52,17 +57,30 @@ export interface EchoHostIndexingConfig {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
const DEFAULT_INDEXING_CONFIG: EchoHostIndexingConfig = {
|
|
55
|
-
// TODO(
|
|
56
|
-
|
|
60
|
+
// TODO(mykola): Enable by default when SQLite indexer is stable.
|
|
61
|
+
sqlIndex: false,
|
|
57
62
|
vector: false,
|
|
58
63
|
};
|
|
59
64
|
|
|
60
|
-
export type
|
|
65
|
+
export type EchoHostProps = {
|
|
61
66
|
kv: LevelDB;
|
|
62
67
|
peerIdProvider?: PeerIdProvider;
|
|
63
68
|
getSpaceKeyByRootDocumentId?: RootDocumentSpaceKeyProvider;
|
|
64
69
|
|
|
65
70
|
indexing?: Partial<EchoHostIndexingConfig>;
|
|
71
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Use SQLite-backed local-first durable queues.
|
|
75
|
+
* Otherwise defaults to in-memory impl.
|
|
76
|
+
*/
|
|
77
|
+
localQueues?: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* This peer is allowed to assign positions (global-order) to items appended to the queue.
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
assignQueuePositions?: boolean;
|
|
66
84
|
};
|
|
67
85
|
|
|
68
86
|
/**
|
|
@@ -80,10 +98,31 @@ export class EchoHost extends Resource {
|
|
|
80
98
|
private readonly _spaceStateManager = new SpaceStateManager();
|
|
81
99
|
private readonly _echoDataMonitor: EchoDataMonitor;
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
private readonly _automergeDataSource: AutomergeDataSource;
|
|
102
|
+
private readonly _indexer2: IndexEngine;
|
|
103
|
+
private readonly _indexConfig: EchoHostIndexingConfig;
|
|
104
|
+
private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
|
|
105
|
+
private readonly _feedStore?: FeedStore;
|
|
106
|
+
private readonly _queueDataSource?: QueueDataSource;
|
|
107
|
+
|
|
108
|
+
private _updateIndexes!: DeferredTask;
|
|
109
|
+
|
|
110
|
+
private _queuesService: QueueService;
|
|
111
|
+
|
|
112
|
+
private _indexesUpToDate = false;
|
|
113
|
+
|
|
114
|
+
constructor({
|
|
115
|
+
kv,
|
|
116
|
+
localQueues,
|
|
117
|
+
indexing = {},
|
|
118
|
+
peerIdProvider,
|
|
119
|
+
getSpaceKeyByRootDocumentId,
|
|
120
|
+
runtime,
|
|
121
|
+
assignQueuePositions = false,
|
|
122
|
+
}: EchoHostProps) {
|
|
84
123
|
super();
|
|
85
124
|
|
|
86
|
-
|
|
125
|
+
this._indexConfig = { ...DEFAULT_INDEXING_CONFIG, ...indexing };
|
|
87
126
|
|
|
88
127
|
this._indexMetadataStore = new IndexMetadataStore({ db: kv.sublevel('index-metadata') });
|
|
89
128
|
this._echoDataMonitor = new EchoDataMonitor();
|
|
@@ -95,6 +134,21 @@ export class EchoHost extends Resource {
|
|
|
95
134
|
getSpaceKeyByRootDocumentId,
|
|
96
135
|
});
|
|
97
136
|
|
|
137
|
+
this._runtime = runtime;
|
|
138
|
+
this._automergeDataSource = new AutomergeDataSource(this._automergeHost);
|
|
139
|
+
|
|
140
|
+
if (localQueues) {
|
|
141
|
+
this._feedStore = new FeedStore({ assignPositions: assignQueuePositions, localActorId: crypto.randomUUID() });
|
|
142
|
+
this._queueDataSource = new QueueDataSource({
|
|
143
|
+
feedStore: this._feedStore,
|
|
144
|
+
runtime: this._runtime,
|
|
145
|
+
getSpaceIds: () => this._spaceStateManager.spaceIds,
|
|
146
|
+
});
|
|
147
|
+
this._queuesService = new LocalQueueServiceImpl(runtime, this._feedStore);
|
|
148
|
+
} else {
|
|
149
|
+
this._queuesService = new QueueServiceStub();
|
|
150
|
+
}
|
|
151
|
+
|
|
98
152
|
this._indexer = new Indexer({
|
|
99
153
|
db: kv,
|
|
100
154
|
indexStore: new IndexStore({ db: kv.sublevel('index-storage') }),
|
|
@@ -109,14 +163,18 @@ export class EchoHost extends Resource {
|
|
|
109
163
|
{ kind: IndexKind.Kind.SCHEMA_MATCH },
|
|
110
164
|
{ kind: IndexKind.Kind.GRAPH },
|
|
111
165
|
|
|
112
|
-
...(
|
|
113
|
-
...(indexingConfig.vector ? [{ kind: IndexKind.Kind.VECTOR }] : []),
|
|
166
|
+
...(this._indexConfig.vector ? [{ kind: IndexKind.Kind.VECTOR }] : []),
|
|
114
167
|
],
|
|
115
168
|
});
|
|
116
169
|
|
|
170
|
+
// New index-core Indexer for text queries.
|
|
171
|
+
this._indexer2 = new IndexEngine();
|
|
172
|
+
|
|
117
173
|
this._queryService = new QueryServiceImpl({
|
|
118
174
|
automergeHost: this._automergeHost,
|
|
119
175
|
indexer: this._indexer,
|
|
176
|
+
indexer2: this._indexConfig.sqlIndex ? this._indexer2 : undefined,
|
|
177
|
+
runtime: this._indexConfig.sqlIndex ? this._runtime : undefined,
|
|
120
178
|
spaceStateManager: this._spaceStateManager,
|
|
121
179
|
});
|
|
122
180
|
|
|
@@ -125,6 +183,11 @@ export class EchoHost extends Resource {
|
|
|
125
183
|
spaceStateManager: this._spaceStateManager,
|
|
126
184
|
updateIndexes: async () => {
|
|
127
185
|
await this._indexer.updateIndexes();
|
|
186
|
+
if (this._indexConfig.sqlIndex) {
|
|
187
|
+
do {
|
|
188
|
+
await this._updateIndexes.runBlocking();
|
|
189
|
+
} while (!this._indexesUpToDate);
|
|
190
|
+
}
|
|
128
191
|
},
|
|
129
192
|
});
|
|
130
193
|
|
|
@@ -177,23 +240,42 @@ export class EchoHost extends Resource {
|
|
|
177
240
|
return this._dataService;
|
|
178
241
|
}
|
|
179
242
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
*/
|
|
183
|
-
get automergeRepo(): Repo {
|
|
184
|
-
return this._automergeHost.repo;
|
|
243
|
+
get queuesService(): QueueService {
|
|
244
|
+
return this._queuesService;
|
|
185
245
|
}
|
|
186
246
|
|
|
187
247
|
get roots(): ReadonlyMap<DocumentId, DatabaseRoot> {
|
|
188
248
|
return this._spaceStateManager.roots;
|
|
189
249
|
}
|
|
190
250
|
|
|
251
|
+
/**
|
|
252
|
+
* New index-core indexer for text and other queries.
|
|
253
|
+
*/
|
|
254
|
+
get indexer2(): IndexEngine {
|
|
255
|
+
return this._indexer2;
|
|
256
|
+
}
|
|
257
|
+
|
|
191
258
|
protected override async _open(ctx: Context): Promise<void> {
|
|
192
259
|
await this._automergeHost.open();
|
|
193
260
|
await this._indexer.open(ctx);
|
|
194
261
|
await this._queryService.open(ctx);
|
|
195
262
|
await this._spaceStateManager.open(ctx);
|
|
196
263
|
|
|
264
|
+
if (this._feedStore) {
|
|
265
|
+
await RuntimeProvider.runPromise(this._runtime)(this._feedStore.migrate());
|
|
266
|
+
this._feedStore.onNewBlocks.on(this._ctx, () => {
|
|
267
|
+
this._queryService.invalidateQueries();
|
|
268
|
+
if (this._indexConfig.sqlIndex) {
|
|
269
|
+
this._updateIndexes.schedule();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (this._indexConfig.sqlIndex) {
|
|
275
|
+
await RuntimeProvider.runPromise(this._runtime)(this._indexer2.migrate());
|
|
276
|
+
this._updateIndexes = new DeferredTask(this._ctx, this._runUpdateIndexes);
|
|
277
|
+
}
|
|
278
|
+
|
|
197
279
|
this._spaceStateManager.spaceDocumentListUpdated.on(this._ctx, (e) => {
|
|
198
280
|
if (e.previousRootId) {
|
|
199
281
|
void this._automergeHost.clearLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId, e.previousRootId));
|
|
@@ -205,7 +287,13 @@ export class EchoHost extends Resource {
|
|
|
205
287
|
});
|
|
206
288
|
this._automergeHost.documentsSaved.on(this._ctx, () => {
|
|
207
289
|
this._queryService.invalidateQueries();
|
|
290
|
+
if (this._indexConfig.sqlIndex) {
|
|
291
|
+
this._updateIndexes.schedule();
|
|
292
|
+
}
|
|
208
293
|
});
|
|
294
|
+
if (this._indexConfig.sqlIndex) {
|
|
295
|
+
this._updateIndexes.schedule();
|
|
296
|
+
}
|
|
209
297
|
}
|
|
210
298
|
|
|
211
299
|
protected override async _close(ctx: Context): Promise<void> {
|
|
@@ -243,7 +331,7 @@ export class EchoHost extends Resource {
|
|
|
243
331
|
/**
|
|
244
332
|
* Create new persisted document.
|
|
245
333
|
*/
|
|
246
|
-
createDoc<T>(initialValue?: T, opts?: CreateDocOptions): DocHandle<T
|
|
334
|
+
async createDoc<T>(initialValue?: T, opts?: CreateDocOptions): Promise<DocHandle<T>> {
|
|
247
335
|
return this._automergeHost.createDoc(initialValue, opts);
|
|
248
336
|
}
|
|
249
337
|
|
|
@@ -254,7 +342,7 @@ export class EchoHost extends Resource {
|
|
|
254
342
|
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
255
343
|
const spaceId = await createIdFromSpaceKey(spaceKey);
|
|
256
344
|
|
|
257
|
-
const automergeRoot = this._automergeHost.createDoc<DatabaseDirectory>({
|
|
345
|
+
const automergeRoot = await this._automergeHost.createDoc<DatabaseDirectory>({
|
|
258
346
|
version: SpaceDocVersion.CURRENT,
|
|
259
347
|
access: { spaceKey: spaceKey.toHex() },
|
|
260
348
|
|
|
@@ -271,14 +359,16 @@ export class EchoHost extends Resource {
|
|
|
271
359
|
// TODO(dmaretskyi): Change to document id.
|
|
272
360
|
async openSpaceRoot(spaceId: SpaceId, automergeUrl: AutomergeUrl): Promise<DatabaseRoot> {
|
|
273
361
|
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
274
|
-
const handle = await this._automergeHost.
|
|
362
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), automergeUrl, {
|
|
363
|
+
fetchFromNetwork: true,
|
|
364
|
+
});
|
|
275
365
|
await handle.whenReady();
|
|
276
366
|
|
|
277
367
|
return this._spaceStateManager.assignRootToSpace(spaceId, handle);
|
|
278
368
|
}
|
|
279
369
|
|
|
280
370
|
// TODO(dmaretskyi): Change to document id.
|
|
281
|
-
async closeSpaceRoot(
|
|
371
|
+
async closeSpaceRoot(_automergeUrl: AutomergeUrl): Promise<void> {
|
|
282
372
|
todo();
|
|
283
373
|
}
|
|
284
374
|
|
|
@@ -295,6 +385,79 @@ export class EchoHost extends Resource {
|
|
|
295
385
|
async removeReplicator(replicator: EchoReplicator): Promise<void> {
|
|
296
386
|
await this._automergeHost.removeReplicator(replicator);
|
|
297
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Run collection sync for the given space.
|
|
391
|
+
* Does not wait for the sync to complete.
|
|
392
|
+
*/
|
|
393
|
+
async runCollectionSync(spaceId: SpaceId) {
|
|
394
|
+
const root = this._spaceStateManager.getRootBySpaceId(spaceId);
|
|
395
|
+
if (!root) {
|
|
396
|
+
throw new Error(`Space not found: ${spaceId}`);
|
|
397
|
+
}
|
|
398
|
+
this._automergeHost.refreshCollection(deriveCollectionIdFromSpaceId(spaceId, root.documentId));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private _runUpdateIndexes = async (): Promise<void> => {
|
|
402
|
+
if (!this._indexer2) {
|
|
403
|
+
// Indexer not initialized yet, skip this update cycle.
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let totalUpdated = 0;
|
|
408
|
+
let totalDone = true;
|
|
409
|
+
|
|
410
|
+
{
|
|
411
|
+
performance.mark('indexer2.update.automerge:start');
|
|
412
|
+
const { updated, done } = await this._indexer2
|
|
413
|
+
.update(this._automergeDataSource, { spaceId: null, limit: 50 })
|
|
414
|
+
.pipe(RuntimeProvider.runPromise(this._runtime));
|
|
415
|
+
totalUpdated += updated;
|
|
416
|
+
totalDone &&= done;
|
|
417
|
+
performance.measure('Index Automerge', {
|
|
418
|
+
start: 'indexer2.update.automerge:start',
|
|
419
|
+
detail: {
|
|
420
|
+
devtools: {
|
|
421
|
+
dataType: 'track-entry',
|
|
422
|
+
track: 'Indexing',
|
|
423
|
+
trackGroup: 'ECHO', // Group related tracks together
|
|
424
|
+
color: 'tertiary-dark',
|
|
425
|
+
properties: [['count', updated]],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (this._queueDataSource) {
|
|
432
|
+
performance.mark('indexer2.update.queue:start');
|
|
433
|
+
const { updated, done } = await this._indexer2
|
|
434
|
+
.update(this._queueDataSource, { spaceId: null, limit: 50 })
|
|
435
|
+
.pipe(RuntimeProvider.runPromise(this._runtime));
|
|
436
|
+
totalUpdated += updated;
|
|
437
|
+
totalDone &&= done;
|
|
438
|
+
performance.measure('Index Queues', {
|
|
439
|
+
start: 'indexer2.update.queue:start',
|
|
440
|
+
detail: {
|
|
441
|
+
devtools: {
|
|
442
|
+
dataType: 'track-entry',
|
|
443
|
+
track: 'Indexing',
|
|
444
|
+
trackGroup: 'ECHO',
|
|
445
|
+
color: 'tertiary-dark',
|
|
446
|
+
properties: [['count', updated]],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
log.verbose('indexer2 update completed', { updated: totalUpdated, done: totalDone });
|
|
453
|
+
await sleep(1);
|
|
454
|
+
if (!totalDone) {
|
|
455
|
+
this._indexesUpToDate = false;
|
|
456
|
+
this._updateIndexes!.schedule();
|
|
457
|
+
} else {
|
|
458
|
+
this._indexesUpToDate = true;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
298
461
|
}
|
|
299
462
|
|
|
300
463
|
export type { EchoDataStats };
|
package/src/db-host/index.ts
CHANGED
|
@@ -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 { ATTR_META, type ObjectJSON } from '@dxos/echo/internal';
|
|
10
|
+
import type { ForeignKey } from '@dxos/echo-protocol';
|
|
11
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
12
|
+
import { type Block, type FeedStore } from '@dxos/feed';
|
|
13
|
+
import { invariant } from '@dxos/invariant';
|
|
14
|
+
import { KEY_QUEUE_POSITION } from '@dxos/protocols';
|
|
15
|
+
import {
|
|
16
|
+
type DeleteFromQueueRequest,
|
|
17
|
+
type InsertIntoQueueRequest,
|
|
18
|
+
type QueryQueueRequest,
|
|
19
|
+
type QueueQueryResult,
|
|
20
|
+
type QueueService,
|
|
21
|
+
} from '@dxos/protocols/proto/dxos/client/services';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Writes queue data to a local FeedStore.
|
|
25
|
+
*/
|
|
26
|
+
export class LocalQueueServiceImpl implements QueueService {
|
|
27
|
+
#runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
28
|
+
#feedStore: FeedStore;
|
|
29
|
+
|
|
30
|
+
constructor(runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>, feedStore: FeedStore) {
|
|
31
|
+
this.#runtime = runtime;
|
|
32
|
+
this.#feedStore = feedStore;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
queryQueue(request: QueryQueueRequest): Promise<QueueQueryResult> {
|
|
36
|
+
const { query } = request;
|
|
37
|
+
invariant(query, 'query is required');
|
|
38
|
+
const { spaceId, queueIds } = query;
|
|
39
|
+
return RuntimeProvider.runPromise(this.#runtime)(
|
|
40
|
+
Effect.gen(this, function* () {
|
|
41
|
+
const cursor = query.after ? parseInt(query.after) : -1;
|
|
42
|
+
const result = yield* this.#feedStore.query({
|
|
43
|
+
requestId: crypto.randomUUID(),
|
|
44
|
+
spaceId: spaceId!,
|
|
45
|
+
query: { feedIds: queueIds ?? [] },
|
|
46
|
+
position: cursor,
|
|
47
|
+
limit: query.limit,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const objects = result.blocks.map((block: Block) => {
|
|
51
|
+
const data = JSON.parse(new TextDecoder().decode(block.data));
|
|
52
|
+
if (block.position !== null) {
|
|
53
|
+
setQueuePosition(data, block.position);
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const lastBlock = result.blocks[result.blocks.length - 1];
|
|
59
|
+
const nextCursor = lastBlock && lastBlock.position != null ? String(lastBlock.position) : null;
|
|
60
|
+
|
|
61
|
+
return Function.identity<QueueQueryResult>({
|
|
62
|
+
objects,
|
|
63
|
+
|
|
64
|
+
// TODO(dmaretskyi): This is wrong, fix later - cursors should come directly from the feed.
|
|
65
|
+
nextCursor: nextCursor?.toString() ?? '',
|
|
66
|
+
prevCursor: '',
|
|
67
|
+
});
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
insertIntoQueue(request: InsertIntoQueueRequest): Promise<void> {
|
|
73
|
+
const { subspaceTag, spaceId, queueId, objects } = request;
|
|
74
|
+
return RuntimeProvider.runPromise(this.#runtime)(
|
|
75
|
+
Effect.gen(this, function* () {
|
|
76
|
+
const messages = objects!.map((obj) => {
|
|
77
|
+
const data = structuredClone(obj);
|
|
78
|
+
if (data[ATTR_META]?.keys?.find((key: ForeignKey) => key.source === KEY_QUEUE_POSITION)) {
|
|
79
|
+
data[ATTR_META].keys = data[ATTR_META].keys.filter((key: ForeignKey) => key.source !== KEY_QUEUE_POSITION);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
spaceId: spaceId,
|
|
84
|
+
feedId: queueId!,
|
|
85
|
+
feedNamespace: subspaceTag,
|
|
86
|
+
data: new TextEncoder().encode(JSON.stringify(obj)),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
yield* this.#feedStore.appendLocal(messages);
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
deleteFromQueue(request: DeleteFromQueueRequest): Promise<void> {
|
|
96
|
+
const { subspaceTag, spaceId, queueId, objectIds } = request;
|
|
97
|
+
return RuntimeProvider.runPromise(this.#runtime)(
|
|
98
|
+
Effect.gen(this, function* () {
|
|
99
|
+
const messages = objectIds!.map((id) => ({
|
|
100
|
+
spaceId: spaceId,
|
|
101
|
+
feedId: queueId!,
|
|
102
|
+
feedNamespace: subspaceTag,
|
|
103
|
+
data: new TextEncoder().encode(JSON.stringify({ id, '@deleted': true })),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
yield* this.#feedStore.appendLocal(messages);
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// TODO(dmaretskyi): Duplicated code.
|
|
113
|
+
const setQueuePosition = (obj: ObjectJSON, position: number) => {
|
|
114
|
+
obj[ATTR_META] ??= { keys: [] };
|
|
115
|
+
obj[ATTR_META].keys ??= [];
|
|
116
|
+
for (let i = 0; i < obj[ATTR_META].keys.length; i++) {
|
|
117
|
+
const key = obj[ATTR_META].keys[i];
|
|
118
|
+
if (key.source === KEY_QUEUE_POSITION) {
|
|
119
|
+
obj[ATTR_META].keys.splice(i, 1);
|
|
120
|
+
i--;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
obj[ATTR_META].keys.push({
|
|
124
|
+
source: KEY_QUEUE_POSITION,
|
|
125
|
+
id: position.toString(),
|
|
126
|
+
});
|
|
127
|
+
};
|