@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.
Files changed (34) hide show
  1. package/dist/lib/browser/{chunk-HS77A4I4.mjs → chunk-A2LCXJVD.mjs} +16 -1
  2. package/dist/lib/browser/chunk-A2LCXJVD.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +170 -90
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-Y5U7UXEL.cjs → chunk-GHBIMYZK.cjs} +19 -4
  8. package/dist/lib/node/chunk-GHBIMYZK.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +214 -136
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +11 -11
  13. package/dist/types/src/automerge/automerge-host.d.ts +12 -9
  14. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  15. package/dist/types/src/automerge/echo-network-adapter.d.ts +2 -2
  16. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  17. package/dist/types/src/automerge/echo-replicator.d.ts +5 -6
  18. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  19. package/dist/types/src/automerge/heads-store.d.ts +13 -0
  20. package/dist/types/src/automerge/heads-store.d.ts.map +1 -0
  21. package/dist/types/src/db-host/data-service.d.ts +4 -2
  22. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  23. package/package.json +33 -33
  24. package/src/automerge/automerge-doc-loader.ts +1 -1
  25. package/src/automerge/automerge-host.test.ts +34 -17
  26. package/src/automerge/automerge-host.ts +56 -16
  27. package/src/automerge/automerge-repo.test.ts +76 -1
  28. package/src/automerge/echo-network-adapter.ts +10 -6
  29. package/src/automerge/echo-replicator.ts +6 -9
  30. package/src/automerge/heads-store.ts +39 -0
  31. package/src/automerge/mesh-echo-replicator.ts +5 -5
  32. package/src/db-host/data-service.ts +22 -2
  33. package/dist/lib/browser/chunk-HS77A4I4.mjs.map +0 -7
  34. 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.doc(['ready']);
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 = new AutomergeHost({
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 = new AutomergeHost({
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 SublevelDB } from '@dxos/kv-store';
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: SublevelDB;
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 open() {
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 close() {
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 lastAvailableHash = getHeads(doc);
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, lastAvailableHash]));
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.repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
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 { type Message, Repo, type PeerId, type DocumentId, type HandleState } from '@dxos/automerge/automerge-repo';
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 { Trigger, synchronized } from '@dxos/async';
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 ShouldAdvertizeParams } from './echo-replicator';
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
- invariant(this._lifecycleState === LifecycleState.CLOSED);
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
- invariant(this._lifecycleState === LifecycleState.OPEN);
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: ShouldAdvertizeParams): Promise<boolean> {
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.shouldAdvertize(params);
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
- onConnectionOpen(connection: ReplicatorConnection): void;
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 advertized to this peer.
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
- shouldAdvertize(params: ShouldAdvertizeParams): Promise<boolean>;
53
+ shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean>;
57
54
  }
58
55
 
59
- export type ShouldAdvertizeParams = {
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, cbor } from '@dxos/automerge/automerge-repo';
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 ShouldAdvertizeParams,
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: ShouldAdvertizeParams) => {
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: ShouldAdvertizeParams) => Promise<boolean>;
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 shouldAdvertize(params: ShouldAdvertizeParams): Promise<boolean> {
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
  }