@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.
@@ -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 SublevelDB } from '@dxos/kv-store';
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: SublevelDB;
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 open() {
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 close() {
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 lastAvailableHash = getHeads(doc);
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, lastAvailableHash]));
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.repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
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
- 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();
@@ -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
+ }