@dxos/echo-pipeline 0.5.9-main.f099efe → 0.5.9-next.73dcc17

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.
@@ -2,10 +2,19 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Event } from '@dxos/async';
6
- import { next as automerge, getBackend, getHeads, type Doc, type Heads } from '@dxos/automerge/automerge';
5
+ import { Event, asyncTimeout } from '@dxos/async';
6
+ import {
7
+ next as automerge,
8
+ getBackend,
9
+ getHeads,
10
+ isAutomerge,
11
+ save,
12
+ type Doc,
13
+ type Heads,
14
+ } from '@dxos/automerge/automerge';
7
15
  import {
8
16
  Repo,
17
+ type AnyDocumentId,
9
18
  type DocHandle,
10
19
  type DocHandleChangePayload,
11
20
  type DocumentId,
@@ -13,10 +22,9 @@ import {
13
22
  type StorageAdapterInterface,
14
23
  } from '@dxos/automerge/automerge-repo';
15
24
  import { type Stream } from '@dxos/codec-protobuf';
16
- import { Context, type Lifecycle } from '@dxos/context';
25
+ import { Context, cancelWithContext, type Lifecycle } from '@dxos/context';
17
26
  import { type SpaceDoc } from '@dxos/echo-protocol';
18
27
  import { type IndexMetadataStore } from '@dxos/indexing';
19
- import { invariant } from '@dxos/invariant';
20
28
  import { PublicKey } from '@dxos/keys';
21
29
  import { type SublevelDB } from '@dxos/kv-store';
22
30
  import { objectPointerCodec } from '@dxos/protocols';
@@ -29,7 +37,7 @@ import {
29
37
  import { trace } from '@dxos/tracing';
30
38
  import { mapValues } from '@dxos/util';
31
39
 
32
- import { EchoNetworkAdapter } from './echo-network-adapter';
40
+ import { EchoNetworkAdapter, isEchoPeerMetadata } from './echo-network-adapter';
33
41
  import { type EchoReplicator } from './echo-replicator';
34
42
  import { LevelDBStorageAdapter, type BeforeSaveParams } from './leveldb-storage-adapter';
35
43
  import { LocalHostNetworkAdapter } from './local-host-network-adapter';
@@ -43,6 +51,20 @@ export type AutomergeHostParams = {
43
51
  indexMetadataStore: IndexMetadataStore;
44
52
  };
45
53
 
54
+ export type LoadDocOptions = {
55
+ timeout?: number;
56
+ };
57
+
58
+ export type CreateDocOptions = {
59
+ /**
60
+ * Import the document together with its history.
61
+ */
62
+ preserveHistory?: boolean;
63
+ };
64
+
65
+ /**
66
+ * Abstracts over the AutomergeRepo.
67
+ */
46
68
  @trace.resource()
47
69
  export class AutomergeHost {
48
70
  private readonly _indexMetadataStore: IndexMetadataStore;
@@ -102,6 +124,9 @@ export class AutomergeHost {
102
124
  await this._ctx.dispose();
103
125
  }
104
126
 
127
+ /**
128
+ * @deprecated To be abstracted away.
129
+ */
105
130
  get repo(): Repo {
106
131
  return this._repo;
107
132
  }
@@ -114,6 +139,46 @@ export class AutomergeHost {
114
139
  await this._echoNetworkAdapter.removeReplicator(replicator);
115
140
  }
116
141
 
142
+ /**
143
+ * Loads the document handle from the repo and waits for it to be ready.
144
+ */
145
+ async loadDoc<T>(ctx: Context, documentId: AnyDocumentId, opts?: LoadDocOptions): Promise<DocHandle<T>> {
146
+ let handle: DocHandle<T> | undefined;
147
+ if (typeof documentId === 'string') {
148
+ // NOTE: documentId might also be a URL, in which case this lookup will fail.
149
+ handle = this._repo.handles[documentId as DocumentId];
150
+ }
151
+ if (!handle) {
152
+ handle = this._repo.find(documentId as DocumentId);
153
+ }
154
+
155
+ // `whenReady` creates a timeout so we guard it with an if to skip it if the handle is already ready.
156
+ if (!handle.isReady()) {
157
+ if (!opts?.timeout) {
158
+ await cancelWithContext(ctx, handle.whenReady());
159
+ } else {
160
+ await cancelWithContext(ctx, asyncTimeout(handle.whenReady(), opts.timeout));
161
+ }
162
+ }
163
+
164
+ return handle;
165
+ }
166
+
167
+ /**
168
+ * Create new persisted document.
169
+ */
170
+ createDoc<T>(initialValue?: T | Doc<T>, opts?: CreateDocOptions): DocHandle<T> {
171
+ if (opts?.preserveHistory) {
172
+ if (!isAutomerge(initialValue)) {
173
+ throw new TypeError('Initial value must be an Automerge document');
174
+ }
175
+ // TODO(dmaretskyi): There's a more efficient way.
176
+ return this._repo.import(save(initialValue as Doc<T>));
177
+ } else {
178
+ return this._repo.create(initialValue);
179
+ }
180
+ }
181
+
117
182
  // TODO(dmaretskyi): Share based on HALO permissions and space affinity.
118
183
  // Hosts, running in the worker, don't share documents unless requested by other peers.
119
184
  // NOTE: If both peers return sharePolicy=false the replication will not happen
@@ -131,7 +196,7 @@ export class AutomergeHost {
131
196
  }
132
197
 
133
198
  const peerMetadata = this.repo.peerMetadataByPeerId[peerId];
134
- if ((peerMetadata as any)?.dxos_peerSource === 'EchoNetworkAdapter') {
199
+ if (isEchoPeerMetadata(peerMetadata)) {
135
200
  return this._echoNetworkAdapter.shouldAdvertize(peerId, { documentId });
136
201
  }
137
202
 
@@ -212,39 +277,52 @@ export class AutomergeHost {
212
277
  return PublicKey.from(spaceKeyHex);
213
278
  }
214
279
 
215
- //
216
- // Methods for client-services.
217
- //
280
+ /**
281
+ * Flush documents to disk.
282
+ */
218
283
  @trace.span({ showInBrowserTimeline: true })
219
284
  async flush({ states }: FlushRequest): Promise<void> {
220
285
  // Note: Wait for all requested documents to be loaded/synced from thin-client.
221
- await Promise.all(
222
- states?.map(async ({ heads, documentId }) => {
223
- invariant(heads, 'heads are required for flush');
224
- const handle = this.repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
225
- await waitForHeads(handle, heads);
226
- }) ?? [],
227
- );
286
+ if (states) {
287
+ await Promise.all(
288
+ states.map(async ({ heads, documentId }) => {
289
+ if (!heads) {
290
+ return;
291
+ }
292
+ const handle = this.repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
293
+ await waitForHeads(handle, heads);
294
+ }) ?? [],
295
+ );
296
+ }
228
297
 
229
298
  await this._repo.flush(states?.map(({ documentId }) => documentId as DocumentId));
230
299
  }
231
300
 
301
+ /**
302
+ * Host <-> Client sync.
303
+ */
232
304
  syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
233
305
  return this._clientNetwork.syncRepo(request);
234
306
  }
235
307
 
308
+ /**
309
+ * Host <-> Client sync.
310
+ */
236
311
  sendSyncMessage(request: SyncRepoRequest): Promise<void> {
237
312
  return this._clientNetwork.sendSyncMessage(request);
238
313
  }
239
314
 
315
+ /**
316
+ * Host <-> Client sync.
317
+ */
240
318
  async getHostInfo(): Promise<HostInfo> {
241
319
  return this._clientNetwork.getHostInfo();
242
320
  }
243
321
  }
244
322
 
245
- export const getSpaceKeyFromDoc = (doc: any): string | null => {
323
+ export const getSpaceKeyFromDoc = (doc: Doc<SpaceDoc>): string | null => {
246
324
  // experimental_spaceKey is set on old documents, new ones are created with doc.access.spaceKey
247
- const rawSpaceKey = doc.access?.spaceKey ?? doc.experimental_spaceKey;
325
+ const rawSpaceKey = doc.access?.spaceKey ?? (doc as any).experimental_spaceKey;
248
326
  if (rawSpaceKey == null) {
249
327
  return null;
250
328
  }
@@ -174,10 +174,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
174
174
  private _emitPeerCandidate(connection: ReplicatorConnection) {
175
175
  this.emit('peer-candidate', {
176
176
  peerId: connection.peerId as PeerId,
177
- peerMetadata: {
178
- // TODO(dmaretskyi): Refactor this.
179
- dxos_peerSource: 'EchoNetworkAdapter',
180
- } as any,
177
+ peerMetadata: createEchoPeerMetadata(),
181
178
  });
182
179
  }
183
180
  }
@@ -188,3 +185,12 @@ type ConnectionEntry = {
188
185
  writer: WritableStreamDefaultWriter<Message>;
189
186
  isOpen: boolean;
190
187
  };
188
+
189
+ export const createEchoPeerMetadata = (): PeerMetadata =>
190
+ ({
191
+ // TODO(dmaretskyi): Refactor this.
192
+ dxos_peerSource: 'EchoNetworkAdapter',
193
+ }) as any;
194
+
195
+ export const isEchoPeerMetadata = (metadata: PeerMetadata): boolean =>
196
+ (metadata as any)?.dxos_peerSource === 'EchoNetworkAdapter';
@@ -63,8 +63,8 @@ export class MeshEchoReplicator implements EchoReplicator {
63
63
  this._context.onConnectionAuthScopeChanged(connection);
64
64
  } else {
65
65
  this._connectionsPerPeer.set(connection.peerId, connection);
66
- await connection.enable();
67
66
  this._context.onConnectionOpen(connection);
67
+ await connection.enable();
68
68
  }
69
69
  },
70
70
  onRemoteDisconnected: async () => {