@dxos/echo-pipeline 0.6.12 → 0.6.13-main.548ca8d
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-PESZVYAN.mjs +2050 -0
- package/dist/lib/browser/chunk-PESZVYAN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +3463 -17
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +3 -4
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-7HHYCGUR.cjs → chunk-6EZVIJNE.cjs} +89 -47
- package/dist/lib/node/chunk-6EZVIJNE.cjs.map +7 -0
- package/dist/lib/node/index.cjs +3440 -35
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +11 -12
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/{browser/chunk-UKXIJW43.mjs → node-esm/chunk-4LW7MDPZ.mjs} +76 -36
- package/dist/lib/node-esm/chunk-4LW7MDPZ.mjs.map +7 -0
- package/dist/lib/{browser/chunk-MPWFDDQK.mjs → node-esm/index.mjs} +1702 -335
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +551 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host.d.ts +24 -1
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/collection-synchronizer.d.ts +2 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +3 -3
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +3 -3
- 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/automerge/space-collection.d.ts +3 -2
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-metrics.d.ts +11 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -0
- package/dist/types/src/db-host/data-service.d.ts +3 -2
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +20 -0
- package/dist/types/src/db-host/database-root.d.ts.map +1 -0
- package/dist/types/src/db-host/documents-iterator.d.ts +7 -0
- package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -0
- package/dist/types/src/db-host/echo-host.d.ts +73 -0
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -0
- package/dist/types/src/db-host/index.d.ts +5 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/migration.d.ts +8 -0
- package/dist/types/src/db-host/migration.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +25 -0
- package/dist/types/src/db-host/query-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-state.d.ts +41 -0
- package/dist/types/src/db-host/query-state.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +23 -0
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +23 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +2 -0
- package/dist/types/src/edge/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +4 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +4 -4
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/package.json +40 -50
- package/src/automerge/automerge-host.test.ts +8 -9
- package/src/automerge/automerge-host.ts +46 -7
- package/src/automerge/automerge-repo.test.ts +18 -16
- package/src/automerge/collection-synchronizer.test.ts +10 -5
- package/src/automerge/collection-synchronizer.ts +17 -6
- package/src/automerge/echo-data-monitor.test.ts +1 -3
- package/src/automerge/echo-network-adapter.test.ts +4 -3
- package/src/automerge/echo-network-adapter.ts +5 -4
- package/src/automerge/echo-replicator.ts +3 -3
- package/src/automerge/mesh-echo-replicator-connection.ts +10 -9
- package/src/automerge/mesh-echo-replicator.ts +2 -1
- package/src/automerge/space-collection.ts +3 -2
- package/src/automerge/storage-adapter.test.ts +2 -3
- package/src/db-host/automerge-metrics.ts +38 -0
- package/src/db-host/data-service.ts +29 -14
- package/src/db-host/database-root.ts +86 -0
- package/src/db-host/documents-iterator.ts +73 -0
- package/src/db-host/documents-synchronizer.test.ts +2 -2
- package/src/db-host/echo-host.ts +257 -0
- package/src/db-host/index.ts +6 -1
- package/src/db-host/migration.ts +57 -0
- package/src/db-host/query-service.ts +208 -0
- package/src/db-host/query-state.ts +200 -0
- package/src/db-host/space-state-manager.ts +90 -0
- package/src/edge/echo-edge-replicator.test.ts +96 -0
- package/src/edge/echo-edge-replicator.ts +337 -0
- package/src/edge/index.ts +5 -0
- package/src/index.ts +1 -0
- package/src/metadata/metadata-store.ts +20 -0
- package/src/pipeline/pipeline-stress.test.ts +44 -47
- package/src/pipeline/pipeline.test.ts +3 -4
- package/src/space/control-pipeline.test.ts +2 -3
- package/src/space/control-pipeline.ts +10 -1
- package/src/space/replication.browser.test.ts +2 -8
- package/src/space/space-manager.browser.test.ts +6 -5
- package/src/space/space-protocol.browser.test.ts +29 -34
- package/src/space/space-protocol.test.ts +29 -27
- package/src/space/space.test.ts +28 -11
- package/src/testing/test-agent-builder.ts +2 -2
- package/src/testing/test-replicator.ts +3 -3
- package/dist/lib/browser/chunk-MPWFDDQK.mjs.map +0 -7
- package/dist/lib/browser/chunk-UKXIJW43.mjs.map +0 -7
- package/dist/lib/browser/chunk-XPCF2V5U.mjs +0 -31
- package/dist/lib/browser/chunk-XPCF2V5U.mjs.map +0 -7
- package/dist/lib/browser/light.mjs +0 -32
- package/dist/lib/browser/light.mjs.map +0 -7
- package/dist/lib/node/chunk-5DH4KR2S.cjs +0 -2148
- package/dist/lib/node/chunk-5DH4KR2S.cjs.map +0 -7
- package/dist/lib/node/chunk-7HHYCGUR.cjs.map +0 -7
- package/dist/lib/node/chunk-DZVH7HDD.cjs +0 -43
- package/dist/lib/node/chunk-DZVH7HDD.cjs.map +0 -7
- package/dist/lib/node/light.cjs +0 -52
- package/dist/lib/node/light.cjs.map +0 -7
- package/dist/types/src/light.d.ts +0 -4
- package/dist/types/src/light.d.ts.map +0 -1
- package/src/light.ts +0 -7
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import type { CollectionId } from '@dxos/echo-protocol';
|
|
5
6
|
import { invariant } from '@dxos/invariant';
|
|
6
7
|
import { PublicKey, type SpaceId } from '@dxos/keys';
|
|
7
8
|
import { log } from '@dxos/log';
|
|
@@ -126,7 +127,7 @@ export class MeshEchoReplicator implements EchoReplicator {
|
|
|
126
127
|
}
|
|
127
128
|
},
|
|
128
129
|
shouldSyncCollection: ({ collectionId }) => {
|
|
129
|
-
const spaceId = getSpaceIdFromCollectionId(collectionId);
|
|
130
|
+
const spaceId = getSpaceIdFromCollectionId(collectionId as CollectionId);
|
|
130
131
|
|
|
131
132
|
const authorizedDevices = this._authorizedDevices.get(spaceId);
|
|
132
133
|
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import type { CollectionId } from '@dxos/echo-protocol';
|
|
5
6
|
import { invariant } from '@dxos/invariant';
|
|
6
7
|
import { SpaceId } from '@dxos/keys';
|
|
7
8
|
|
|
8
|
-
export const deriveCollectionIdFromSpaceId = (spaceId: SpaceId):
|
|
9
|
+
export const deriveCollectionIdFromSpaceId = (spaceId: SpaceId): CollectionId => `space:${spaceId}` as CollectionId;
|
|
9
10
|
|
|
10
|
-
export const getSpaceIdFromCollectionId = (collectionId:
|
|
11
|
+
export const getSpaceIdFromCollectionId = (collectionId: CollectionId): SpaceId => {
|
|
11
12
|
const spaceId = collectionId.replace(/^space:/, '');
|
|
12
13
|
invariant(SpaceId.isValid(spaceId));
|
|
13
14
|
return spaceId;
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { onTestFinished, describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { randomBytes } from '@dxos/crypto';
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
9
9
|
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
10
|
-
import { afterTest, describe, test } from '@dxos/test';
|
|
11
10
|
import { arrayToBuffer, bufferToArray } from '@dxos/util';
|
|
12
11
|
|
|
13
12
|
import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
|
|
@@ -23,7 +22,7 @@ describe('LevelDBStorageAdapter', () => {
|
|
|
23
22
|
await adapter.close();
|
|
24
23
|
await level.close();
|
|
25
24
|
};
|
|
26
|
-
|
|
25
|
+
onTestFinished(close);
|
|
27
26
|
return {
|
|
28
27
|
adapter,
|
|
29
28
|
close,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as A from '@dxos/automerge/automerge';
|
|
6
|
+
import { log } from '@dxos/log';
|
|
7
|
+
|
|
8
|
+
export type DocMetrics = {
|
|
9
|
+
compressedByteSize: number;
|
|
10
|
+
loadTime: number;
|
|
11
|
+
mutationCount: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* WARN: Slow to run on large docs.
|
|
16
|
+
*/
|
|
17
|
+
export const measureDocMetrics = (doc: A.Doc<any>): DocMetrics => {
|
|
18
|
+
const snapshot = A.save(doc);
|
|
19
|
+
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
const temp = A.load(snapshot);
|
|
22
|
+
const end = Date.now();
|
|
23
|
+
A.free(temp);
|
|
24
|
+
|
|
25
|
+
const getAllChangesStart = Date.now();
|
|
26
|
+
const mutationCount = A.getAllChanges(doc).length;
|
|
27
|
+
const getAllChangesEnd = Date.now();
|
|
28
|
+
|
|
29
|
+
if (getAllChangesEnd - getAllChangesStart > 300) {
|
|
30
|
+
log.warn('getAllChanges took too long', { elapsed: getAllChangesEnd - getAllChangesStart });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
compressedByteSize: snapshot.byteLength,
|
|
35
|
+
loadTime: end - start,
|
|
36
|
+
mutationCount,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Copyright 2021 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { UpdateScheduler } from '@dxos/async';
|
|
5
6
|
import { type DocumentId } from '@dxos/automerge/automerge-repo';
|
|
6
|
-
import { type RequestOptions
|
|
7
|
+
import { type RequestOptions } from '@dxos/codec-protobuf';
|
|
8
|
+
import { Stream } from '@dxos/codec-protobuf/stream';
|
|
7
9
|
import { invariant } from '@dxos/invariant';
|
|
8
10
|
import { SpaceId } from '@dxos/keys';
|
|
9
11
|
import { log } from '@dxos/log';
|
|
@@ -120,19 +122,32 @@ export class DataServiceImpl implements DataService {
|
|
|
120
122
|
await this._updateIndexes();
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
subscribeSpaceSyncState(request: GetSpaceSyncStateRequest): Stream<SpaceSyncState> {
|
|
126
|
+
return new Stream<SpaceSyncState>(({ ctx, next, ready }) => {
|
|
127
|
+
invariant(SpaceId.isValid(request.spaceId));
|
|
128
|
+
const collectionId = deriveCollectionIdFromSpaceId(request.spaceId);
|
|
129
|
+
|
|
130
|
+
const scheduler = new UpdateScheduler(ctx, async () => {
|
|
131
|
+
const state = await this._automergeHost.getCollectionSyncState(collectionId);
|
|
132
|
+
|
|
133
|
+
next({
|
|
134
|
+
peers: state.peers.map((peer) => ({
|
|
135
|
+
peerId: peer.peerId,
|
|
136
|
+
missingOnRemote: peer.missingOnRemote,
|
|
137
|
+
missingOnLocal: peer.missingOnLocal,
|
|
138
|
+
differentDocuments: peer.differentDocuments,
|
|
139
|
+
localDocumentCount: peer.localDocumentCount,
|
|
140
|
+
remoteDocumentCount: peer.remoteDocumentCount,
|
|
141
|
+
})),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
})
|
|
136
|
-
|
|
145
|
+
this._automergeHost.collectionStateUpdated.on(ctx, (e) => {
|
|
146
|
+
if (e.collectionId === collectionId) {
|
|
147
|
+
scheduler.trigger();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
scheduler.trigger();
|
|
151
|
+
});
|
|
137
152
|
}
|
|
138
153
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as A from '@dxos/automerge/automerge';
|
|
6
|
+
import type { AutomergeUrl, DocHandle, DocumentId } from '@dxos/automerge/automerge-repo';
|
|
7
|
+
import { type SpaceDoc, SpaceDocVersion } from '@dxos/echo-protocol';
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
|
|
10
|
+
import { measureDocMetrics, type DocMetrics } from './automerge-metrics';
|
|
11
|
+
import { getSpaceKeyFromDoc } from '../automerge';
|
|
12
|
+
|
|
13
|
+
export class DatabaseRoot {
|
|
14
|
+
constructor(private readonly _rootHandle: DocHandle<SpaceDoc>) {}
|
|
15
|
+
|
|
16
|
+
get documentId(): DocumentId {
|
|
17
|
+
return this._rootHandle.documentId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get url() {
|
|
21
|
+
return this._rootHandle.url;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get isLoaded(): boolean {
|
|
25
|
+
return !!this._rootHandle.docSync();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get handle(): DocHandle<SpaceDoc> {
|
|
29
|
+
return this._rootHandle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
docSync(): A.Doc<SpaceDoc> | null {
|
|
33
|
+
return this._rootHandle.docSync();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getVersion(): SpaceDocVersion | null {
|
|
37
|
+
const doc = this.docSync();
|
|
38
|
+
if (!doc) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return doc.version ?? SpaceDocVersion.LEGACY;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getSpaceKey(): string | null {
|
|
46
|
+
const doc = this.docSync();
|
|
47
|
+
if (!doc) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return getSpaceKeyFromDoc(doc);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getInlineObjectCount(): number | null {
|
|
55
|
+
const doc = this.docSync();
|
|
56
|
+
if (!doc) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.keys(doc.objects ?? {}).length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getLinkedObjectCount(): number | null {
|
|
64
|
+
const doc = this.docSync();
|
|
65
|
+
if (!doc) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Object.keys(doc.links ?? {}).length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAllLinkedDocuments(): AutomergeUrl[] {
|
|
73
|
+
const doc = this.docSync();
|
|
74
|
+
invariant(doc);
|
|
75
|
+
|
|
76
|
+
return Object.values(doc.links ?? {}) as AutomergeUrl[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
measureMetrics(): DocMetrics | null {
|
|
80
|
+
const doc = this.docSync();
|
|
81
|
+
if (!doc) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return measureDocMetrics(doc);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as A from '@dxos/automerge/automerge';
|
|
6
|
+
import { type DocumentId } from '@dxos/automerge/automerge-repo';
|
|
7
|
+
import { Context } from '@dxos/context';
|
|
8
|
+
import { SpaceDocVersion, type SpaceDoc } from '@dxos/echo-protocol';
|
|
9
|
+
import { type ObjectSnapshot, type IdToHeads } from '@dxos/indexing';
|
|
10
|
+
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { ObjectPointerVersion, objectPointerCodec } from '@dxos/protocols';
|
|
13
|
+
|
|
14
|
+
import { type AutomergeHost, getSpaceKeyFromDoc } from '../automerge';
|
|
15
|
+
|
|
16
|
+
const LOG_VIEW_OPERATION_THRESHOLD = 300;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Factory for `loadDocuments` iterator.
|
|
20
|
+
*/
|
|
21
|
+
export const createSelectedDocumentsIterator = (automergeHost: AutomergeHost) =>
|
|
22
|
+
/**
|
|
23
|
+
* Get object data blobs from Automerge Repo by ids.
|
|
24
|
+
*/
|
|
25
|
+
// TODO(mykola): Unload automerge handles after usage.
|
|
26
|
+
async function* loadDocuments(objects: IdToHeads): AsyncGenerator<ObjectSnapshot[], void, void> {
|
|
27
|
+
for (const [id, heads] of objects.entries()) {
|
|
28
|
+
try {
|
|
29
|
+
const { documentId, objectId } = objectPointerCodec.decode(id);
|
|
30
|
+
const handle = await automergeHost.loadDoc(Context.default(), documentId as DocumentId);
|
|
31
|
+
|
|
32
|
+
let doc: A.Doc<SpaceDoc> = handle.docSync();
|
|
33
|
+
invariant(doc);
|
|
34
|
+
|
|
35
|
+
const currentHeads = A.getHeads(doc);
|
|
36
|
+
|
|
37
|
+
// Checkout the requested version of the document.
|
|
38
|
+
if (!A.equals(currentHeads, heads)) {
|
|
39
|
+
const begin = Date.now();
|
|
40
|
+
// `view` can take a long time even if the document is already at the right version.
|
|
41
|
+
doc = A.view(doc, heads);
|
|
42
|
+
const end = Date.now();
|
|
43
|
+
if (end - begin > LOG_VIEW_OPERATION_THRESHOLD) {
|
|
44
|
+
log.info('Checking out document version is taking too long', {
|
|
45
|
+
duration: end - begin,
|
|
46
|
+
requestedHeads: heads,
|
|
47
|
+
originalHeads: currentHeads,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skip outdated docs.
|
|
53
|
+
if (doc.version !== SpaceDocVersion.CURRENT) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!doc.objects?.[objectId]) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Upgrade V0 object pointers to V1.
|
|
62
|
+
let newId = id;
|
|
63
|
+
if (objectPointerCodec.getVersion(id) === ObjectPointerVersion.V0) {
|
|
64
|
+
const spaceKey = getSpaceKeyFromDoc(doc) ?? undefined;
|
|
65
|
+
newId = objectPointerCodec.encode({ documentId, objectId, spaceKey });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
yield [{ id: newId, object: doc.objects![objectId], heads }];
|
|
69
|
+
} catch (error) {
|
|
70
|
+
log.error('Error loading document', { heads, id, error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { sleep } from '@dxos/async';
|
|
8
8
|
import { next as A } from '@dxos/automerge/automerge';
|
|
9
9
|
import { generateAutomergeUrl, parseAutomergeUrl, Repo } from '@dxos/automerge/automerge-repo';
|
|
10
|
-
import {
|
|
10
|
+
import { openAndClose } from '@dxos/test-utils';
|
|
11
11
|
|
|
12
12
|
import { DocumentsSynchronizer } from './documents-synchronizer';
|
|
13
13
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type AnyDocumentId,
|
|
7
|
+
type AutomergeUrl,
|
|
8
|
+
type DocHandle,
|
|
9
|
+
type DocumentId,
|
|
10
|
+
type Repo,
|
|
11
|
+
} from '@dxos/automerge/automerge-repo';
|
|
12
|
+
import { LifecycleState, Resource, type Context } from '@dxos/context';
|
|
13
|
+
import { todo } from '@dxos/debug';
|
|
14
|
+
import { createIdFromSpaceKey, SpaceDocVersion, type SpaceDoc } from '@dxos/echo-protocol';
|
|
15
|
+
import { IndexMetadataStore, IndexStore, Indexer } from '@dxos/indexing';
|
|
16
|
+
import { invariant } from '@dxos/invariant';
|
|
17
|
+
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
18
|
+
import { type LevelDB } from '@dxos/kv-store';
|
|
19
|
+
import { IndexKind, type IndexConfig } from '@dxos/protocols/proto/dxos/echo/indexing';
|
|
20
|
+
import { trace } from '@dxos/tracing';
|
|
21
|
+
|
|
22
|
+
import { DataServiceImpl } from './data-service';
|
|
23
|
+
import { type DatabaseRoot } from './database-root';
|
|
24
|
+
import { createSelectedDocumentsIterator } from './documents-iterator';
|
|
25
|
+
import { QueryServiceImpl } from './query-service';
|
|
26
|
+
import { SpaceStateManager } from './space-state-manager';
|
|
27
|
+
import {
|
|
28
|
+
AutomergeHost,
|
|
29
|
+
EchoDataMonitor,
|
|
30
|
+
deriveCollectionIdFromSpaceId,
|
|
31
|
+
type LoadDocOptions,
|
|
32
|
+
type CreateDocOptions,
|
|
33
|
+
type EchoReplicator,
|
|
34
|
+
type CollectionSyncState,
|
|
35
|
+
type EchoDataStats,
|
|
36
|
+
} from '../automerge';
|
|
37
|
+
|
|
38
|
+
const INDEXER_CONFIG: IndexConfig = {
|
|
39
|
+
enabled: true,
|
|
40
|
+
indexes: [{ kind: IndexKind.Kind.SCHEMA_MATCH }],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type EchoHostParams = {
|
|
44
|
+
kv: LevelDB;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Host for the Echo database.
|
|
49
|
+
* Manages multiple spaces.
|
|
50
|
+
* Stores data to disk.
|
|
51
|
+
* Can sync with pluggable data replicators.
|
|
52
|
+
*/
|
|
53
|
+
export class EchoHost extends Resource {
|
|
54
|
+
private readonly _indexMetadataStore: IndexMetadataStore;
|
|
55
|
+
private readonly _indexer: Indexer;
|
|
56
|
+
private readonly _automergeHost: AutomergeHost;
|
|
57
|
+
private readonly _queryService: QueryServiceImpl;
|
|
58
|
+
private readonly _dataService: DataServiceImpl;
|
|
59
|
+
private readonly _spaceStateManager = new SpaceStateManager();
|
|
60
|
+
private readonly _echoDataMonitor: EchoDataMonitor;
|
|
61
|
+
|
|
62
|
+
constructor({ kv }: EchoHostParams) {
|
|
63
|
+
super();
|
|
64
|
+
|
|
65
|
+
this._indexMetadataStore = new IndexMetadataStore({ db: kv.sublevel('index-metadata') });
|
|
66
|
+
|
|
67
|
+
this._echoDataMonitor = new EchoDataMonitor();
|
|
68
|
+
|
|
69
|
+
this._automergeHost = new AutomergeHost({
|
|
70
|
+
db: kv,
|
|
71
|
+
dataMonitor: this._echoDataMonitor,
|
|
72
|
+
indexMetadataStore: this._indexMetadataStore,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this._indexer = new Indexer({
|
|
76
|
+
db: kv,
|
|
77
|
+
indexStore: new IndexStore({ db: kv.sublevel('index-storage') }),
|
|
78
|
+
metadataStore: this._indexMetadataStore,
|
|
79
|
+
loadDocuments: createSelectedDocumentsIterator(this._automergeHost),
|
|
80
|
+
indexCooldownTime: process.env.NODE_ENV === 'test' ? 0 : undefined,
|
|
81
|
+
});
|
|
82
|
+
this._indexer.setConfig(INDEXER_CONFIG);
|
|
83
|
+
|
|
84
|
+
this._queryService = new QueryServiceImpl({
|
|
85
|
+
automergeHost: this._automergeHost,
|
|
86
|
+
indexer: this._indexer,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this._dataService = new DataServiceImpl({
|
|
90
|
+
automergeHost: this._automergeHost,
|
|
91
|
+
updateIndexes: async () => {
|
|
92
|
+
await this._indexer.updateIndexes();
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
trace.diagnostic<EchoStatsDiagnostic>({
|
|
97
|
+
id: 'echo-stats',
|
|
98
|
+
name: 'Echo Stats',
|
|
99
|
+
fetch: async () => {
|
|
100
|
+
return {
|
|
101
|
+
dataStats: this._echoDataMonitor.computeStats(),
|
|
102
|
+
loadedDocsCount: this._automergeHost.loadedDocsCount,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
trace.diagnostic({
|
|
108
|
+
id: 'database-roots',
|
|
109
|
+
name: 'Database Roots',
|
|
110
|
+
fetch: async () => {
|
|
111
|
+
return Array.from(this._spaceStateManager.roots.values()).map((root) => ({
|
|
112
|
+
url: root.url,
|
|
113
|
+
isLoaded: root.isLoaded,
|
|
114
|
+
spaceKey: root.getSpaceKey(),
|
|
115
|
+
inlineObjects: root.getInlineObjectCount(),
|
|
116
|
+
linkedObjects: root.getLinkedObjectCount(),
|
|
117
|
+
}));
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
trace.diagnostic({
|
|
122
|
+
id: 'database-root-metrics',
|
|
123
|
+
name: 'Database Roots (with metrics)',
|
|
124
|
+
fetch: async () => {
|
|
125
|
+
return Array.from(this._spaceStateManager.roots.values()).map((root) => ({
|
|
126
|
+
url: root.url,
|
|
127
|
+
isLoaded: root.isLoaded,
|
|
128
|
+
spaceKey: root.getSpaceKey(),
|
|
129
|
+
inlineObjects: root.getInlineObjectCount(),
|
|
130
|
+
linkedObjects: root.getLinkedObjectCount(),
|
|
131
|
+
...(root.measureMetrics() ?? {}),
|
|
132
|
+
}));
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get queryService(): QueryServiceImpl {
|
|
138
|
+
return this._queryService;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get dataService(): DataServiceImpl {
|
|
142
|
+
return this._dataService;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @deprecated To be abstracted away.
|
|
147
|
+
*/
|
|
148
|
+
get automergeRepo(): Repo {
|
|
149
|
+
return this._automergeHost.repo;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get roots(): ReadonlyMap<DocumentId, DatabaseRoot> {
|
|
153
|
+
return this._spaceStateManager.roots;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected override async _open(ctx: Context): Promise<void> {
|
|
157
|
+
await this._automergeHost.open();
|
|
158
|
+
await this._indexer.open(ctx);
|
|
159
|
+
await this._queryService.open(ctx);
|
|
160
|
+
await this._spaceStateManager.open(ctx);
|
|
161
|
+
|
|
162
|
+
this._spaceStateManager.spaceDocumentListUpdated.on(this._ctx, (e) => {
|
|
163
|
+
void this._automergeHost.updateLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId), e.documentIds);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
protected override async _close(ctx: Context): Promise<void> {
|
|
168
|
+
await this._spaceStateManager.close();
|
|
169
|
+
await this._queryService.close(ctx);
|
|
170
|
+
await this._indexer.close(ctx);
|
|
171
|
+
await this._automergeHost.close();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Flush all pending writes to the underlying storage.
|
|
176
|
+
*/
|
|
177
|
+
async flush() {
|
|
178
|
+
await this._automergeHost.repo.flush();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Perform any pending index updates.
|
|
183
|
+
*/
|
|
184
|
+
async updateIndexes() {
|
|
185
|
+
await this._indexer.updateIndexes();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Loads the document handle from the repo and waits for it to be ready.
|
|
190
|
+
*/
|
|
191
|
+
async loadDoc<T>(ctx: Context, documentId: AnyDocumentId, opts?: LoadDocOptions): Promise<DocHandle<T>> {
|
|
192
|
+
return await this._automergeHost.loadDoc(ctx, documentId, opts);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create new persisted document.
|
|
197
|
+
*/
|
|
198
|
+
createDoc<T>(initialValue?: T, opts?: CreateDocOptions): DocHandle<T> {
|
|
199
|
+
return this._automergeHost.createDoc(initialValue, opts);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create new space root.
|
|
204
|
+
*/
|
|
205
|
+
async createSpaceRoot(spaceKey: PublicKey): Promise<DatabaseRoot> {
|
|
206
|
+
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
207
|
+
const spaceId = await createIdFromSpaceKey(spaceKey);
|
|
208
|
+
|
|
209
|
+
const automergeRoot = this._automergeHost.createDoc<SpaceDoc>({
|
|
210
|
+
version: SpaceDocVersion.CURRENT,
|
|
211
|
+
access: { spaceKey: spaceKey.toHex() },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await this._automergeHost.flush({ documentIds: [automergeRoot.documentId] });
|
|
215
|
+
|
|
216
|
+
return await this.openSpaceRoot(spaceId, automergeRoot.url);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// TODO(dmaretskyi): Change to document id.
|
|
220
|
+
async openSpaceRoot(spaceId: SpaceId, automergeUrl: AutomergeUrl): Promise<DatabaseRoot> {
|
|
221
|
+
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
222
|
+
const handle = this._automergeHost.repo.find(automergeUrl);
|
|
223
|
+
|
|
224
|
+
return this._spaceStateManager.assignRootToSpace(spaceId, handle);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// TODO(dmaretskyi): Change to document id.
|
|
228
|
+
async closeSpaceRoot(automergeUrl: AutomergeUrl): Promise<void> {
|
|
229
|
+
todo();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Install data replicator.
|
|
234
|
+
*/
|
|
235
|
+
async addReplicator(replicator: EchoReplicator): Promise<void> {
|
|
236
|
+
await this._automergeHost.addReplicator(replicator);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Remove data replicator.
|
|
241
|
+
*/
|
|
242
|
+
async removeReplicator(replicator: EchoReplicator): Promise<void> {
|
|
243
|
+
await this._automergeHost.removeReplicator(replicator);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getSpaceSyncState(spaceId: SpaceId): Promise<CollectionSyncState> {
|
|
247
|
+
const collectionId = deriveCollectionIdFromSpaceId(spaceId);
|
|
248
|
+
return this._automergeHost.getCollectionSyncState(collectionId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export type { EchoDataStats };
|
|
253
|
+
|
|
254
|
+
export type EchoStatsDiagnostic = {
|
|
255
|
+
loadedDocsCount: number;
|
|
256
|
+
dataStats: EchoDataStats;
|
|
257
|
+
};
|
package/src/db-host/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './data-service';
|
|
6
6
|
export * from './documents-synchronizer';
|
|
7
|
+
export * from './echo-host';
|
|
8
|
+
export * from './migration';
|
|
9
|
+
export * from './database-root';
|
|
10
|
+
export * from './query-state';
|
|
11
|
+
export * from './query-service';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { convertLegacyReference } from '@dxos/echo-protocol';
|
|
6
|
+
import {
|
|
7
|
+
decodeReference,
|
|
8
|
+
encodeReference,
|
|
9
|
+
isLegacyReference,
|
|
10
|
+
LEGACY_TYPE_PROPERTIES,
|
|
11
|
+
type ObjectStructure,
|
|
12
|
+
Reference,
|
|
13
|
+
type SpaceDoc,
|
|
14
|
+
SpaceDocVersion,
|
|
15
|
+
} from '@dxos/echo-protocol';
|
|
16
|
+
import { TYPE_PROPERTIES } from '@dxos/echo-schema';
|
|
17
|
+
import { deepMapValuesAsync } from '@dxos/util';
|
|
18
|
+
|
|
19
|
+
export const convertLegacyReferences = async (doc: SpaceDoc): Promise<SpaceDoc> => {
|
|
20
|
+
const newDoc = await deepMapValuesAsync(doc, async (value, recurse) => {
|
|
21
|
+
if (isLegacyReference(value)) {
|
|
22
|
+
return convertLegacyReference(value);
|
|
23
|
+
}
|
|
24
|
+
return recurse(value);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
newDoc.version = SpaceDocVersion.CURRENT;
|
|
28
|
+
return newDoc;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const convertLegacySpaceRootDoc = async (root: SpaceDoc): Promise<SpaceDoc> => {
|
|
32
|
+
// Convert references.
|
|
33
|
+
const newDoc: SpaceDoc = await convertLegacyReferences(root);
|
|
34
|
+
|
|
35
|
+
// Update properties type.
|
|
36
|
+
const properties = findInlineObjectOfType(newDoc, LEGACY_TYPE_PROPERTIES);
|
|
37
|
+
if (properties) {
|
|
38
|
+
const [_, obj] = properties;
|
|
39
|
+
obj.system.type = encodeReference(Reference.fromLegacyTypename(TYPE_PROPERTIES));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return newDoc;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Assumes properties are at root.
|
|
47
|
+
*/
|
|
48
|
+
export const findInlineObjectOfType = (spaceDoc: SpaceDoc, typename: string): [string, ObjectStructure] | undefined => {
|
|
49
|
+
for (const id in spaceDoc.objects ?? {}) {
|
|
50
|
+
const obj = spaceDoc.objects![id];
|
|
51
|
+
if (obj.system.type && decodeReference(obj.system.type).objectId === typename) {
|
|
52
|
+
return [id, obj];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return undefined;
|
|
57
|
+
};
|