@dxos/echo-pipeline 0.6.0 → 0.6.1-main.04e8aa0
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-HS77A4I4.mjs → chunk-A2LCXJVD.mjs} +16 -1
- package/dist/lib/browser/chunk-A2LCXJVD.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +170 -90
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node/{chunk-Y5U7UXEL.cjs → chunk-GHBIMYZK.cjs} +19 -4
- package/dist/lib/node/chunk-GHBIMYZK.cjs.map +7 -0
- package/dist/lib/node/index.cjs +214 -136
- 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 -11
- package/dist/types/src/automerge/automerge-host.d.ts +12 -9
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +2 -2
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +5 -6
- package/dist/types/src/automerge/echo-replicator.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/dist/types/src/db-host/data-service.d.ts +4 -2
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-doc-loader.ts +1 -1
- package/src/automerge/automerge-host.test.ts +34 -17
- package/src/automerge/automerge-host.ts +56 -16
- package/src/automerge/automerge-repo.test.ts +76 -1
- package/src/automerge/echo-network-adapter.ts +10 -6
- package/src/automerge/echo-replicator.ts +6 -9
- package/src/automerge/heads-store.ts +39 -0
- package/src/automerge/mesh-echo-replicator.ts +5 -5
- package/src/db-host/data-service.ts +22 -2
- package/dist/lib/browser/chunk-HS77A4I4.mjs.map +0 -7
- package/dist/lib/node/chunk-Y5U7UXEL.cjs.map +0 -7
|
@@ -225,7 +225,7 @@ export class AutomergeDocumentLoaderImpl implements AutomergeDocumentLoader {
|
|
|
225
225
|
|
|
226
226
|
private async _createObjectOnDocumentLoad(handle: DocHandle<SpaceDoc>, objectId: string) {
|
|
227
227
|
try {
|
|
228
|
-
await handle.
|
|
228
|
+
await handle.whenReady();
|
|
229
229
|
const logMeta = { objectId, docUrl: handle.url };
|
|
230
230
|
if (this.onObjectDocumentLoaded.listenerCount() === 0) {
|
|
231
231
|
log.info('document loaded after all listeners were removed', logMeta);
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import expect from 'expect';
|
|
6
6
|
|
|
7
|
+
import { getHeads } from '@dxos/automerge/automerge';
|
|
7
8
|
import { IndexMetadataStore } from '@dxos/indexing';
|
|
9
|
+
import type { LevelDB } from '@dxos/kv-store';
|
|
8
10
|
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
9
11
|
import { afterTest, describe, test } from '@dxos/test';
|
|
10
12
|
|
|
@@ -15,13 +17,8 @@ describe('AutomergeHost', () => {
|
|
|
15
17
|
const level = createTestLevel();
|
|
16
18
|
await level.open();
|
|
17
19
|
afterTest(() => level.close());
|
|
18
|
-
const host = new AutomergeHost({
|
|
19
|
-
db: level.sublevel('automerge'),
|
|
20
|
-
indexMetadataStore: new IndexMetadataStore({ db: level.sublevel('index-metadata') }),
|
|
21
|
-
});
|
|
22
|
-
await host.open();
|
|
23
|
-
afterTest(() => host.close());
|
|
24
20
|
|
|
21
|
+
const host = await setupAutomergeHost({ level });
|
|
25
22
|
const handle = host.repo.create();
|
|
26
23
|
handle.change((doc: any) => {
|
|
27
24
|
doc.text = 'Hello world';
|
|
@@ -35,11 +32,7 @@ describe('AutomergeHost', () => {
|
|
|
35
32
|
await level.open();
|
|
36
33
|
afterTest(() => level.close());
|
|
37
34
|
|
|
38
|
-
const host =
|
|
39
|
-
db: level.sublevel('automerge'),
|
|
40
|
-
indexMetadataStore: new IndexMetadataStore({ db: level.sublevel('index-metadata') }),
|
|
41
|
-
});
|
|
42
|
-
await host.open();
|
|
35
|
+
const host = await setupAutomergeHost({ level });
|
|
43
36
|
const handle = host.repo.create();
|
|
44
37
|
handle.change((doc: any) => {
|
|
45
38
|
doc.text = 'Hello world';
|
|
@@ -49,15 +42,39 @@ describe('AutomergeHost', () => {
|
|
|
49
42
|
await host.repo.flush();
|
|
50
43
|
await host.close();
|
|
51
44
|
|
|
52
|
-
const host2 =
|
|
53
|
-
db: level.sublevel('automerge'),
|
|
54
|
-
indexMetadataStore: new IndexMetadataStore({ db: level.sublevel('index-metadata') }),
|
|
55
|
-
});
|
|
56
|
-
await host2.open();
|
|
57
|
-
afterTest(() => host2.close());
|
|
45
|
+
const host2 = await setupAutomergeHost({ level });
|
|
58
46
|
const handle2 = host2.repo.find(url);
|
|
59
47
|
await handle2.whenReady();
|
|
60
48
|
expect(handle2.docSync().text).toEqual('Hello world');
|
|
61
49
|
await host2.repo.flush();
|
|
62
50
|
});
|
|
51
|
+
|
|
52
|
+
test('query document heads', async () => {
|
|
53
|
+
const level = createTestLevel();
|
|
54
|
+
await level.open();
|
|
55
|
+
afterTest(() => level.close());
|
|
56
|
+
|
|
57
|
+
const host = await setupAutomergeHost({ level });
|
|
58
|
+
const handle = host.createDoc({ text: 'Hello world' });
|
|
59
|
+
const expectedHeads = getHeads(handle.docSync());
|
|
60
|
+
await host.flush();
|
|
61
|
+
|
|
62
|
+
expect(await host.getHeads(handle.documentId)).toEqual(expectedHeads);
|
|
63
|
+
|
|
64
|
+
// Simulate a restart.
|
|
65
|
+
{
|
|
66
|
+
const host = await setupAutomergeHost({ level });
|
|
67
|
+
expect(await host.getHeads(handle.documentId)).toEqual(expectedHeads);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
63
70
|
});
|
|
71
|
+
|
|
72
|
+
const setupAutomergeHost = async ({ level }: { level: LevelDB }) => {
|
|
73
|
+
const host = new AutomergeHost({
|
|
74
|
+
db: level,
|
|
75
|
+
indexMetadataStore: new IndexMetadataStore({ db: level.sublevel('index-metadata') }),
|
|
76
|
+
});
|
|
77
|
+
await host.open();
|
|
78
|
+
afterTest(() => host.close());
|
|
79
|
+
return host;
|
|
80
|
+
};
|
|
@@ -22,11 +22,13 @@ 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
|
+
import { invariant } from '@dxos/invariant';
|
|
28
29
|
import { PublicKey } from '@dxos/keys';
|
|
29
|
-
import { type
|
|
30
|
+
import { type LevelDB } from '@dxos/kv-store';
|
|
31
|
+
import { log } from '@dxos/log';
|
|
30
32
|
import { objectPointerCodec } from '@dxos/protocols';
|
|
31
33
|
import {
|
|
32
34
|
type FlushRequest,
|
|
@@ -39,6 +41,7 @@ import { mapValues } from '@dxos/util';
|
|
|
39
41
|
|
|
40
42
|
import { EchoNetworkAdapter, isEchoPeerMetadata } from './echo-network-adapter';
|
|
41
43
|
import { type EchoReplicator } from './echo-replicator';
|
|
44
|
+
import { HeadsStore } from './heads-store';
|
|
42
45
|
import { LevelDBStorageAdapter, type BeforeSaveParams } from './leveldb-storage-adapter';
|
|
43
46
|
import { LocalHostNetworkAdapter } from './local-host-network-adapter';
|
|
44
47
|
|
|
@@ -46,7 +49,7 @@ import { LocalHostNetworkAdapter } from './local-host-network-adapter';
|
|
|
46
49
|
export type { DocumentId };
|
|
47
50
|
|
|
48
51
|
export type AutomergeHostParams = {
|
|
49
|
-
db:
|
|
52
|
+
db: LevelDB;
|
|
50
53
|
|
|
51
54
|
indexMetadataStore: IndexMetadataStore;
|
|
52
55
|
};
|
|
@@ -66,9 +69,9 @@ export type CreateDocOptions = {
|
|
|
66
69
|
* Abstracts over the AutomergeRepo.
|
|
67
70
|
*/
|
|
68
71
|
@trace.resource()
|
|
69
|
-
export class AutomergeHost {
|
|
72
|
+
export class AutomergeHost extends Resource {
|
|
73
|
+
private readonly _db: LevelDB;
|
|
70
74
|
private readonly _indexMetadataStore: IndexMetadataStore;
|
|
71
|
-
private readonly _ctx = new Context();
|
|
72
75
|
private readonly _echoNetworkAdapter = new EchoNetworkAdapter({
|
|
73
76
|
getContainingSpaceForDocument: this._getContainingSpaceForDocument.bind(this),
|
|
74
77
|
});
|
|
@@ -76,22 +79,26 @@ export class AutomergeHost {
|
|
|
76
79
|
private _repo!: Repo;
|
|
77
80
|
private _clientNetwork!: LocalHostNetworkAdapter;
|
|
78
81
|
private _storage!: StorageAdapterInterface & Lifecycle;
|
|
82
|
+
private readonly _headsStore: HeadsStore;
|
|
79
83
|
|
|
80
84
|
@trace.info()
|
|
81
85
|
private _peerId!: string;
|
|
82
86
|
|
|
83
87
|
constructor({ db, indexMetadataStore }: AutomergeHostParams) {
|
|
88
|
+
super();
|
|
89
|
+
this._db = db;
|
|
84
90
|
this._storage = new LevelDBStorageAdapter({
|
|
85
|
-
db,
|
|
91
|
+
db: db.sublevel('automerge'),
|
|
86
92
|
callbacks: {
|
|
87
93
|
beforeSave: async (params) => this._beforeSave(params),
|
|
88
94
|
afterSave: async () => this._afterSave(),
|
|
89
95
|
},
|
|
90
96
|
});
|
|
97
|
+
this._headsStore = new HeadsStore({ db: db.sublevel('heads') });
|
|
91
98
|
this._indexMetadataStore = indexMetadataStore;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
async
|
|
101
|
+
protected override async _open() {
|
|
95
102
|
// TODO(burdon): Should this be stable?
|
|
96
103
|
this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
|
|
97
104
|
|
|
@@ -117,7 +124,7 @@ export class AutomergeHost {
|
|
|
117
124
|
await this._echoNetworkAdapter.whenConnected();
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
async
|
|
127
|
+
protected override async _close() {
|
|
121
128
|
await this._storage.close?.();
|
|
122
129
|
await this._clientNetwork.close();
|
|
123
130
|
await this._echoNetworkAdapter.close();
|
|
@@ -179,14 +186,32 @@ export class AutomergeHost {
|
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
async reIndexHeads(documentIds: DocumentId[]) {
|
|
190
|
+
for (const documentId of documentIds) {
|
|
191
|
+
log.info('reindexing heads for document', { documentId });
|
|
192
|
+
const handle = this._repo.find(documentId);
|
|
193
|
+
await handle.whenReady(['ready', 'requesting']);
|
|
194
|
+
if (handle.inState(['requesting'])) {
|
|
195
|
+
log.warn('document is not available locally, skipping', { documentId });
|
|
196
|
+
continue; // Handle not available locally.
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const doc = handle.docSync();
|
|
200
|
+
invariant(doc);
|
|
201
|
+
|
|
202
|
+
const heads = getHeads(doc);
|
|
203
|
+
const batch = this._db.batch();
|
|
204
|
+
this._headsStore.setHeads(documentId, heads, batch);
|
|
205
|
+
await batch.write();
|
|
206
|
+
}
|
|
207
|
+
log.info('done reindexing heads');
|
|
208
|
+
}
|
|
209
|
+
|
|
182
210
|
// TODO(dmaretskyi): Share based on HALO permissions and space affinity.
|
|
183
211
|
// Hosts, running in the worker, don't share documents unless requested by other peers.
|
|
184
212
|
// NOTE: If both peers return sharePolicy=false the replication will not happen
|
|
185
213
|
// https://github.com/automerge/automerge-repo/pull/292
|
|
186
|
-
private async _sharePolicy(
|
|
187
|
-
peerId: PeerId /* device key */,
|
|
188
|
-
documentId?: DocumentId /* space key */,
|
|
189
|
-
): Promise<boolean> {
|
|
214
|
+
private async _sharePolicy(peerId: PeerId, documentId?: DocumentId): Promise<boolean> {
|
|
190
215
|
if (peerId.startsWith('client-')) {
|
|
191
216
|
return false; // Only send docs to clients if they are requested.
|
|
192
217
|
}
|
|
@@ -215,13 +240,15 @@ export class AutomergeHost {
|
|
|
215
240
|
|
|
216
241
|
const spaceKey = getSpaceKeyFromDoc(doc) ?? undefined;
|
|
217
242
|
|
|
218
|
-
const
|
|
243
|
+
const heads = getHeads(doc);
|
|
244
|
+
|
|
245
|
+
this._headsStore.setHeads(handle.documentId, heads, batch);
|
|
219
246
|
|
|
220
247
|
const objectIds = Object.keys(doc.objects ?? {});
|
|
221
248
|
const encodedIds = objectIds.map((objectId) =>
|
|
222
249
|
objectPointerCodec.encode({ documentId: handle.documentId, objectId, spaceKey }),
|
|
223
250
|
);
|
|
224
|
-
const idToLastHash = new Map(encodedIds.map((id) => [id,
|
|
251
|
+
const idToLastHash = new Map(encodedIds.map((id) => [id, heads]));
|
|
225
252
|
this._indexMetadataStore.markDirty(idToLastHash, batch);
|
|
226
253
|
}
|
|
227
254
|
|
|
@@ -281,7 +308,7 @@ export class AutomergeHost {
|
|
|
281
308
|
* Flush documents to disk.
|
|
282
309
|
*/
|
|
283
310
|
@trace.span({ showInBrowserTimeline: true })
|
|
284
|
-
async flush({ states }: FlushRequest): Promise<void> {
|
|
311
|
+
async flush({ states }: FlushRequest = {}): Promise<void> {
|
|
285
312
|
// Note: Wait for all requested documents to be loaded/synced from thin-client.
|
|
286
313
|
if (states) {
|
|
287
314
|
await Promise.all(
|
|
@@ -289,7 +316,7 @@ export class AutomergeHost {
|
|
|
289
316
|
if (!heads) {
|
|
290
317
|
return;
|
|
291
318
|
}
|
|
292
|
-
const handle = this.
|
|
319
|
+
const handle = this._repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
|
|
293
320
|
await waitForHeads(handle, heads);
|
|
294
321
|
}) ?? [],
|
|
295
322
|
);
|
|
@@ -298,6 +325,19 @@ export class AutomergeHost {
|
|
|
298
325
|
await this._repo.flush(states?.map(({ documentId }) => documentId as DocumentId));
|
|
299
326
|
}
|
|
300
327
|
|
|
328
|
+
async getHeads(documentId: DocumentId): Promise<Heads | undefined> {
|
|
329
|
+
const handle = this._repo.handles[documentId];
|
|
330
|
+
if (handle) {
|
|
331
|
+
const doc = handle.docSync();
|
|
332
|
+
if (!doc) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return getHeads(doc);
|
|
336
|
+
} else {
|
|
337
|
+
return this._headsStore.getHeads(documentId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
301
341
|
/**
|
|
302
342
|
* Host <-> Client sync.
|
|
303
343
|
*/
|
|
@@ -7,7 +7,14 @@ import waitForExpect from 'wait-for-expect';
|
|
|
7
7
|
|
|
8
8
|
import { asyncTimeout, sleep } from '@dxos/async';
|
|
9
9
|
import { type Heads, change, clone, equals, from, getBackend, getHeads } from '@dxos/automerge/automerge';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Message,
|
|
12
|
+
Repo,
|
|
13
|
+
type PeerId,
|
|
14
|
+
type DocumentId,
|
|
15
|
+
type HandleState,
|
|
16
|
+
type AutomergeUrl,
|
|
17
|
+
} from '@dxos/automerge/automerge-repo';
|
|
11
18
|
import { randomBytes } from '@dxos/crypto';
|
|
12
19
|
import { PublicKey } from '@dxos/keys';
|
|
13
20
|
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
@@ -92,6 +99,74 @@ describe('AutomergeRepo', () => {
|
|
|
92
99
|
expect(equals(a, c)).to.be.true;
|
|
93
100
|
});
|
|
94
101
|
|
|
102
|
+
test('documents missing from local storage go to requesting state', async () => {
|
|
103
|
+
const hostAdapter: TestAdapter = new TestAdapter({
|
|
104
|
+
send: (message: Message) => {
|
|
105
|
+
console.log('hostAdapter.send', message);
|
|
106
|
+
clientAdapter.receive(message);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const clientAdapter: TestAdapter = new TestAdapter({
|
|
110
|
+
send: (message: Message) => {
|
|
111
|
+
console.log('clientAdapter.send', message);
|
|
112
|
+
if (message.type !== 'doc-unavailable' && message.type !== 'sync') {
|
|
113
|
+
hostAdapter.receive(message);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const host = new Repo({
|
|
119
|
+
network: [hostAdapter],
|
|
120
|
+
});
|
|
121
|
+
const _client = new Repo({
|
|
122
|
+
network: [clientAdapter],
|
|
123
|
+
});
|
|
124
|
+
hostAdapter.ready();
|
|
125
|
+
clientAdapter.ready();
|
|
126
|
+
await hostAdapter.onConnect.wait();
|
|
127
|
+
await clientAdapter.onConnect.wait();
|
|
128
|
+
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
129
|
+
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
130
|
+
|
|
131
|
+
const url = 'automerge:3JN8F3Z4dUWEEKKFN7WE9gEGvVUT';
|
|
132
|
+
const handle = host.find(url as AutomergeUrl);
|
|
133
|
+
await handle.whenReady(['requesting']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('documents on disk go to ready state', async () => {
|
|
137
|
+
const level = createTestLevel();
|
|
138
|
+
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
139
|
+
await openAndClose(level, storage);
|
|
140
|
+
|
|
141
|
+
let url: AutomergeUrl | undefined;
|
|
142
|
+
{
|
|
143
|
+
const repo = new Repo({
|
|
144
|
+
network: [],
|
|
145
|
+
storage,
|
|
146
|
+
});
|
|
147
|
+
const handle = repo.create<{ field?: string }>();
|
|
148
|
+
url = handle.url;
|
|
149
|
+
await repo.flush();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
const repo = new Repo({
|
|
154
|
+
network: [],
|
|
155
|
+
storage,
|
|
156
|
+
});
|
|
157
|
+
const handle = repo.find(url as AutomergeUrl);
|
|
158
|
+
|
|
159
|
+
let requestingState = false;
|
|
160
|
+
queueMicrotask(async () => {
|
|
161
|
+
await handle.whenReady(['requesting']);
|
|
162
|
+
requestingState = true;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await handle.whenReady(['ready']);
|
|
166
|
+
expect(requestingState).to.be.false;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
95
170
|
describe('network', () => {
|
|
96
171
|
test('basic networking', async () => {
|
|
97
172
|
const hostAdapter: TestAdapter = new TestAdapter({
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { synchronized, Trigger } from '@dxos/async';
|
|
6
6
|
import { type Message, NetworkAdapter, type PeerId, type PeerMetadata } from '@dxos/automerge/automerge-repo';
|
|
7
7
|
import { LifecycleState } from '@dxos/context';
|
|
8
8
|
import { invariant } from '@dxos/invariant';
|
|
9
9
|
import { type PublicKey } from '@dxos/keys';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
11
|
|
|
12
|
-
import { type EchoReplicator, type ReplicatorConnection, type
|
|
12
|
+
import { type EchoReplicator, type ReplicatorConnection, type ShouldAdvertiseParams } from './echo-replicator';
|
|
13
13
|
|
|
14
14
|
export type EchoNetworkAdapterParams = {
|
|
15
15
|
getContainingSpaceForDocument: (documentId: string) => Promise<PublicKey | null>;
|
|
@@ -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();
|
|
@@ -106,13 +110,13 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
106
110
|
this._replicators.delete(replicator);
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
async shouldAdvertize(peerId: PeerId, params:
|
|
113
|
+
async shouldAdvertize(peerId: PeerId, params: ShouldAdvertiseParams): Promise<boolean> {
|
|
110
114
|
const connection = this._connections.get(peerId);
|
|
111
115
|
if (!connection) {
|
|
112
116
|
return false;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
return connection.connection.
|
|
119
|
+
return connection.connection.shouldAdvertise(params);
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
private _onConnectionOpen(connection: ReplicatorConnection) {
|
|
@@ -23,13 +23,11 @@ export interface EchoReplicatorContext {
|
|
|
23
23
|
*/
|
|
24
24
|
get peerId(): string;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
|
|
27
27
|
|
|
28
|
+
onConnectionOpen(connection: ReplicatorConnection): void;
|
|
28
29
|
onConnectionClosed(connection: ReplicatorConnection): void;
|
|
29
|
-
|
|
30
30
|
onConnectionAuthScopeChanged(connection: ReplicatorConnection): void;
|
|
31
|
-
|
|
32
|
-
getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
export interface ReplicatorConnection {
|
|
@@ -49,13 +47,12 @@ export interface ReplicatorConnection {
|
|
|
49
47
|
writable: WritableStream<Message>;
|
|
50
48
|
|
|
51
49
|
/**
|
|
52
|
-
* @returns true if the document should be
|
|
53
|
-
*
|
|
54
|
-
* The remote peer can still request the document by it's id bypassing this check.
|
|
50
|
+
* @returns true if the document should be advertised to this peer.
|
|
51
|
+
* The remote peer can still request the document by its id bypassing this check.
|
|
55
52
|
*/
|
|
56
|
-
|
|
53
|
+
shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean>;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
export type
|
|
56
|
+
export type ShouldAdvertiseParams = {
|
|
60
57
|
documentId: string;
|
|
61
58
|
};
|
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Message
|
|
5
|
+
import { cbor, type Message } from '@dxos/automerge/automerge-repo';
|
|
6
6
|
import { Resource } from '@dxos/context';
|
|
7
7
|
import { invariant } from '@dxos/invariant';
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type EchoReplicator,
|
|
15
15
|
type EchoReplicatorContext,
|
|
16
16
|
type ReplicatorConnection,
|
|
17
|
-
type
|
|
17
|
+
type ShouldAdvertiseParams,
|
|
18
18
|
} from './echo-replicator';
|
|
19
19
|
|
|
20
20
|
// TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
|
|
@@ -74,7 +74,7 @@ export class MeshEchoReplicator implements EchoReplicator {
|
|
|
74
74
|
await connection.disable();
|
|
75
75
|
this._connections.delete(connection);
|
|
76
76
|
},
|
|
77
|
-
shouldAdvertize: async (params:
|
|
77
|
+
shouldAdvertize: async (params: ShouldAdvertiseParams) => {
|
|
78
78
|
log('shouldAdvertize', { peerId: connection.peerId, documentId: params.documentId });
|
|
79
79
|
invariant(this._context);
|
|
80
80
|
try {
|
|
@@ -133,7 +133,7 @@ type MeshReplicatorConnectionParams = {
|
|
|
133
133
|
ownPeerId: string;
|
|
134
134
|
onRemoteConnected: () => Promise<void>;
|
|
135
135
|
onRemoteDisconnected: () => Promise<void>;
|
|
136
|
-
shouldAdvertize: (params:
|
|
136
|
+
shouldAdvertize: (params: ShouldAdvertiseParams) => Promise<boolean>;
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
class MeshReplicatorConnection extends Resource implements ReplicatorConnection {
|
|
@@ -216,7 +216,7 @@ class MeshReplicatorConnection extends Resource implements ReplicatorConnection
|
|
|
216
216
|
return this._remotePeerId;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
async
|
|
219
|
+
async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
|
|
220
220
|
return this._params.shouldAdvertize(params);
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
// Copyright 2021 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Stream } from '@dxos/codec-protobuf';
|
|
5
|
+
import { type RequestOptions, type Stream } from '@dxos/codec-protobuf';
|
|
6
6
|
import {
|
|
7
7
|
type DataService,
|
|
8
8
|
type EchoEvent,
|
|
9
9
|
type FlushRequest,
|
|
10
|
+
type GetDocumentHeadsRequest,
|
|
11
|
+
type GetDocumentHeadsResponse,
|
|
10
12
|
type HostInfo,
|
|
11
13
|
type MutationReceipt,
|
|
14
|
+
type ReIndexHeadsRequest,
|
|
12
15
|
type SubscribeRequest,
|
|
13
16
|
type SyncRepoRequest,
|
|
14
17
|
type SyncRepoResponse,
|
|
15
18
|
type WriteRequest,
|
|
16
19
|
} from '@dxos/protocols/proto/dxos/echo/service';
|
|
17
20
|
|
|
18
|
-
import { type AutomergeHost } from '../automerge';
|
|
21
|
+
import { type AutomergeHost, type DocumentId } from '../automerge';
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Data sync between client and services.
|
|
@@ -49,4 +52,21 @@ export class DataServiceImpl implements DataService {
|
|
|
49
52
|
sendSyncMessage(request: SyncRepoRequest): Promise<void> {
|
|
50
53
|
return this._automergeHost.sendSyncMessage(request);
|
|
51
54
|
}
|
|
55
|
+
|
|
56
|
+
async getDocumentHeads(request: GetDocumentHeadsRequest): Promise<GetDocumentHeadsResponse> {
|
|
57
|
+
const states = await Promise.all(
|
|
58
|
+
request.documentIds?.map(async (documentId): Promise<GetDocumentHeadsResponse.DocState> => {
|
|
59
|
+
const heads = await this._automergeHost.getHeads(documentId as DocumentId);
|
|
60
|
+
return {
|
|
61
|
+
documentId,
|
|
62
|
+
heads,
|
|
63
|
+
};
|
|
64
|
+
}) ?? [],
|
|
65
|
+
);
|
|
66
|
+
return { states };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async reIndexHeads(request: ReIndexHeadsRequest, options?: RequestOptions): Promise<void> {
|
|
70
|
+
await this._automergeHost.reIndexHeads((request.documentIds ?? []) as DocumentId[]);
|
|
71
|
+
}
|
|
52
72
|
}
|