@dxos/echo-pipeline 0.6.0 → 0.6.1-main.7c5f65a
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/index.mjs +110 -82
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +105 -79
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/automerge/automerge-host.d.ts +10 -9
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/heads-store.d.ts +13 -0
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -0
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +34 -17
- package/src/automerge/automerge-host.ts +30 -12
- package/src/automerge/echo-network-adapter.ts +6 -2
- package/src/automerge/heads-store.ts +39 -0
|
@@ -22,11 +22,11 @@ import {
|
|
|
22
22
|
type StorageAdapterInterface,
|
|
23
23
|
} from '@dxos/automerge/automerge-repo';
|
|
24
24
|
import { type Stream } from '@dxos/codec-protobuf';
|
|
25
|
-
import { Context, cancelWithContext, type Lifecycle } from '@dxos/context';
|
|
25
|
+
import { type Context, Resource, cancelWithContext, type Lifecycle } from '@dxos/context';
|
|
26
26
|
import { type SpaceDoc } from '@dxos/echo-protocol';
|
|
27
27
|
import { type IndexMetadataStore } from '@dxos/indexing';
|
|
28
28
|
import { PublicKey } from '@dxos/keys';
|
|
29
|
-
import { type
|
|
29
|
+
import { type LevelDB } from '@dxos/kv-store';
|
|
30
30
|
import { objectPointerCodec } from '@dxos/protocols';
|
|
31
31
|
import {
|
|
32
32
|
type FlushRequest,
|
|
@@ -39,6 +39,7 @@ import { mapValues } from '@dxos/util';
|
|
|
39
39
|
|
|
40
40
|
import { EchoNetworkAdapter, isEchoPeerMetadata } from './echo-network-adapter';
|
|
41
41
|
import { type EchoReplicator } from './echo-replicator';
|
|
42
|
+
import { HeadsStore } from './heads-store';
|
|
42
43
|
import { LevelDBStorageAdapter, type BeforeSaveParams } from './leveldb-storage-adapter';
|
|
43
44
|
import { LocalHostNetworkAdapter } from './local-host-network-adapter';
|
|
44
45
|
|
|
@@ -46,7 +47,7 @@ import { LocalHostNetworkAdapter } from './local-host-network-adapter';
|
|
|
46
47
|
export type { DocumentId };
|
|
47
48
|
|
|
48
49
|
export type AutomergeHostParams = {
|
|
49
|
-
db:
|
|
50
|
+
db: LevelDB;
|
|
50
51
|
|
|
51
52
|
indexMetadataStore: IndexMetadataStore;
|
|
52
53
|
};
|
|
@@ -66,9 +67,8 @@ export type CreateDocOptions = {
|
|
|
66
67
|
* Abstracts over the AutomergeRepo.
|
|
67
68
|
*/
|
|
68
69
|
@trace.resource()
|
|
69
|
-
export class AutomergeHost {
|
|
70
|
+
export class AutomergeHost extends Resource {
|
|
70
71
|
private readonly _indexMetadataStore: IndexMetadataStore;
|
|
71
|
-
private readonly _ctx = new Context();
|
|
72
72
|
private readonly _echoNetworkAdapter = new EchoNetworkAdapter({
|
|
73
73
|
getContainingSpaceForDocument: this._getContainingSpaceForDocument.bind(this),
|
|
74
74
|
});
|
|
@@ -76,22 +76,25 @@ export class AutomergeHost {
|
|
|
76
76
|
private _repo!: Repo;
|
|
77
77
|
private _clientNetwork!: LocalHostNetworkAdapter;
|
|
78
78
|
private _storage!: StorageAdapterInterface & Lifecycle;
|
|
79
|
+
private readonly _headsStore: HeadsStore;
|
|
79
80
|
|
|
80
81
|
@trace.info()
|
|
81
82
|
private _peerId!: string;
|
|
82
83
|
|
|
83
84
|
constructor({ db, indexMetadataStore }: AutomergeHostParams) {
|
|
85
|
+
super();
|
|
84
86
|
this._storage = new LevelDBStorageAdapter({
|
|
85
|
-
db,
|
|
87
|
+
db: db.sublevel('automerge'),
|
|
86
88
|
callbacks: {
|
|
87
89
|
beforeSave: async (params) => this._beforeSave(params),
|
|
88
90
|
afterSave: async () => this._afterSave(),
|
|
89
91
|
},
|
|
90
92
|
});
|
|
93
|
+
this._headsStore = new HeadsStore({ db: db.sublevel('heads') });
|
|
91
94
|
this._indexMetadataStore = indexMetadataStore;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
async
|
|
97
|
+
protected override async _open() {
|
|
95
98
|
// TODO(burdon): Should this be stable?
|
|
96
99
|
this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
|
|
97
100
|
|
|
@@ -117,7 +120,7 @@ export class AutomergeHost {
|
|
|
117
120
|
await this._echoNetworkAdapter.whenConnected();
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
async
|
|
123
|
+
protected override async _close() {
|
|
121
124
|
await this._storage.close?.();
|
|
122
125
|
await this._clientNetwork.close();
|
|
123
126
|
await this._echoNetworkAdapter.close();
|
|
@@ -215,13 +218,15 @@ export class AutomergeHost {
|
|
|
215
218
|
|
|
216
219
|
const spaceKey = getSpaceKeyFromDoc(doc) ?? undefined;
|
|
217
220
|
|
|
218
|
-
const
|
|
221
|
+
const heads = getHeads(doc);
|
|
222
|
+
|
|
223
|
+
this._headsStore.setHeads(handle.documentId, heads, batch);
|
|
219
224
|
|
|
220
225
|
const objectIds = Object.keys(doc.objects ?? {});
|
|
221
226
|
const encodedIds = objectIds.map((objectId) =>
|
|
222
227
|
objectPointerCodec.encode({ documentId: handle.documentId, objectId, spaceKey }),
|
|
223
228
|
);
|
|
224
|
-
const idToLastHash = new Map(encodedIds.map((id) => [id,
|
|
229
|
+
const idToLastHash = new Map(encodedIds.map((id) => [id, heads]));
|
|
225
230
|
this._indexMetadataStore.markDirty(idToLastHash, batch);
|
|
226
231
|
}
|
|
227
232
|
|
|
@@ -281,7 +286,7 @@ export class AutomergeHost {
|
|
|
281
286
|
* Flush documents to disk.
|
|
282
287
|
*/
|
|
283
288
|
@trace.span({ showInBrowserTimeline: true })
|
|
284
|
-
async flush({ states }: FlushRequest): Promise<void> {
|
|
289
|
+
async flush({ states }: FlushRequest = {}): Promise<void> {
|
|
285
290
|
// Note: Wait for all requested documents to be loaded/synced from thin-client.
|
|
286
291
|
if (states) {
|
|
287
292
|
await Promise.all(
|
|
@@ -289,7 +294,7 @@ export class AutomergeHost {
|
|
|
289
294
|
if (!heads) {
|
|
290
295
|
return;
|
|
291
296
|
}
|
|
292
|
-
const handle = this.
|
|
297
|
+
const handle = this._repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
|
|
293
298
|
await waitForHeads(handle, heads);
|
|
294
299
|
}) ?? [],
|
|
295
300
|
);
|
|
@@ -298,6 +303,19 @@ export class AutomergeHost {
|
|
|
298
303
|
await this._repo.flush(states?.map(({ documentId }) => documentId as DocumentId));
|
|
299
304
|
}
|
|
300
305
|
|
|
306
|
+
async getHeads(documentId: DocumentId): Promise<Heads | undefined> {
|
|
307
|
+
const handle = this._repo.handles[documentId];
|
|
308
|
+
if (handle) {
|
|
309
|
+
const doc = handle.docSync();
|
|
310
|
+
if (!doc) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
return getHeads(doc);
|
|
314
|
+
} else {
|
|
315
|
+
return this._headsStore.getHeads(documentId);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
301
319
|
/**
|
|
302
320
|
* Host <-> Client sync.
|
|
303
321
|
*/
|
|
@@ -57,7 +57,9 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
57
57
|
|
|
58
58
|
@synchronized
|
|
59
59
|
async open() {
|
|
60
|
-
|
|
60
|
+
if (this._lifecycleState === LifecycleState.OPEN) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
61
63
|
this._lifecycleState = LifecycleState.OPEN;
|
|
62
64
|
|
|
63
65
|
log('emit ready');
|
|
@@ -68,7 +70,9 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
68
70
|
|
|
69
71
|
@synchronized
|
|
70
72
|
async close() {
|
|
71
|
-
|
|
73
|
+
if (this._lifecycleState === LifecycleState.CLOSED) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
72
76
|
|
|
73
77
|
for (const replicator of this._replicators) {
|
|
74
78
|
await replicator.disconnect();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { Heads } from '@dxos/automerge/automerge';
|
|
6
|
+
import type { DocumentId } from '@dxos/automerge/automerge-repo';
|
|
7
|
+
import { headsEncoding } from '@dxos/indexing';
|
|
8
|
+
import type { BatchLevel, SublevelDB } from '@dxos/kv-store';
|
|
9
|
+
|
|
10
|
+
export type HeadsStoreParams = {
|
|
11
|
+
db: SublevelDB;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class HeadsStore {
|
|
15
|
+
private readonly _db: SublevelDB;
|
|
16
|
+
|
|
17
|
+
constructor({ db }: HeadsStoreParams) {
|
|
18
|
+
this._db = db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setHeads(documentId: DocumentId, heads: Heads, batch: BatchLevel) {
|
|
22
|
+
batch.put<DocumentId, Heads>(documentId, heads, {
|
|
23
|
+
sublevel: this._db,
|
|
24
|
+
keyEncoding: 'utf8',
|
|
25
|
+
valueEncoding: headsEncoding,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getHeads(documentId: DocumentId): Promise<Heads | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
return await this._db.get<DocumentId, Heads>(documentId, { keyEncoding: 'utf8', valueEncoding: headsEncoding });
|
|
32
|
+
} catch (err: any) {
|
|
33
|
+
if (err.notFound) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|