@dxos/echo-pipeline 0.4.9 → 0.4.10-main.068c3d8

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 (69) hide show
  1. package/dist/lib/browser/{chunk-RTEEJ723.mjs → chunk-SYE4EK33.mjs} +30 -35
  2. package/dist/lib/browser/chunk-SYE4EK33.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +593 -217
  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 +8 -2
  7. package/dist/lib/browser/testing/index.mjs.map +4 -4
  8. package/dist/lib/node/{chunk-7VZVCCNF.cjs → chunk-WCTX6RNS.cjs} +35 -40
  9. package/dist/lib/node/chunk-WCTX6RNS.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +611 -237
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +18 -13
  14. package/dist/lib/node/testing/index.cjs.map +4 -4
  15. package/dist/types/src/automerge/automerge-doc-loader.d.ts +66 -0
  16. package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-doc-loader.test.d.ts +2 -0
  18. package/dist/types/src/automerge/automerge-doc-loader.test.d.ts.map +1 -0
  19. package/dist/types/src/automerge/automerge-host.d.ts +21 -18
  20. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  21. package/dist/types/src/automerge/automerge-repo.test.d.ts +2 -0
  22. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/index.d.ts +4 -0
  24. package/dist/types/src/automerge/index.d.ts.map +1 -1
  25. package/dist/types/src/automerge/level.test.d.ts +2 -0
  26. package/dist/types/src/automerge/level.test.d.ts.map +1 -0
  27. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +30 -0
  28. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -0
  29. package/dist/types/src/automerge/local-host-network-adapter.d.ts +8 -1
  30. package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -1
  31. package/dist/types/src/automerge/migrations.d.ts +7 -0
  32. package/dist/types/src/automerge/migrations.d.ts.map +1 -0
  33. package/dist/types/src/automerge/reference.d.ts +15 -0
  34. package/dist/types/src/automerge/reference.d.ts.map +1 -0
  35. package/dist/types/src/automerge/storage-adapter.test.d.ts +2 -0
  36. package/dist/types/src/automerge/storage-adapter.test.d.ts.map +1 -0
  37. package/dist/types/src/automerge/types.d.ts +73 -0
  38. package/dist/types/src/automerge/types.d.ts.map +1 -0
  39. package/dist/types/src/metadata/metadata-store.d.ts +2 -1
  40. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  41. package/dist/types/src/space/space.d.ts +4 -8
  42. package/dist/types/src/space/space.d.ts.map +1 -1
  43. package/dist/types/src/testing/index.d.ts +1 -0
  44. package/dist/types/src/testing/index.d.ts.map +1 -1
  45. package/dist/types/src/testing/level.d.ts +3 -0
  46. package/dist/types/src/testing/level.d.ts.map +1 -0
  47. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  48. package/package.json +33 -30
  49. package/src/automerge/automerge-doc-loader.test.ts +97 -0
  50. package/src/automerge/automerge-doc-loader.ts +241 -0
  51. package/src/automerge/automerge-host.test.ts +22 -8
  52. package/src/automerge/automerge-host.ts +65 -118
  53. package/src/automerge/automerge-repo.test.ts +29 -0
  54. package/src/automerge/index.ts +4 -0
  55. package/src/automerge/level.test.ts +64 -0
  56. package/src/automerge/leveldb-storage-adapter.ts +117 -0
  57. package/src/automerge/local-host-network-adapter.ts +19 -13
  58. package/src/automerge/migrations.ts +41 -0
  59. package/src/automerge/reference.ts +31 -0
  60. package/src/automerge/storage-adapter.test.ts +90 -0
  61. package/src/automerge/types.ts +86 -0
  62. package/src/db-host/data-service.ts +1 -1
  63. package/src/metadata/metadata-store.ts +17 -8
  64. package/src/space/space.test.ts +7 -7
  65. package/src/space/space.ts +6 -21
  66. package/src/testing/index.ts +1 -0
  67. package/src/testing/level.ts +11 -0
  68. package/dist/lib/browser/chunk-RTEEJ723.mjs.map +0 -7
  69. package/dist/lib/node/chunk-7VZVCCNF.cjs.map +0 -7
@@ -2,78 +2,81 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { next as automerge, getHeads } from '@dxos/automerge/automerge';
6
- import {
7
- Repo,
8
- type PeerId,
9
- type DocumentId,
10
- type StorageKey,
11
- type DocHandle,
12
- type DocHandleChangePayload,
13
- } from '@dxos/automerge/automerge-repo';
14
- import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
5
+ import { asyncTimeout } from '@dxos/async';
6
+ import { next as automerge } from '@dxos/automerge/automerge';
7
+ import { Repo, type DocumentId, type PeerId, type StorageAdapterInterface } from '@dxos/automerge/automerge-repo';
15
8
  import { type Stream } from '@dxos/codec-protobuf';
16
- import { Context } from '@dxos/context';
9
+ import { Context, type Lifecycle } from '@dxos/context';
17
10
  import { PublicKey } from '@dxos/keys';
18
11
  import { log } from '@dxos/log';
19
- import { idCodec } from '@dxos/protocols';
20
- import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
21
- import { StorageType, type Directory } from '@dxos/random-access-storage';
12
+ import {
13
+ type FlushRequest,
14
+ type HostInfo,
15
+ type SyncRepoRequest,
16
+ type SyncRepoResponse,
17
+ } from '@dxos/protocols/proto/dxos/echo/service';
18
+ import { type Directory } from '@dxos/random-access-storage';
22
19
  import { type AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
23
20
  import { trace } from '@dxos/tracing';
24
21
  import { ComplexMap, ComplexSet, defaultMap, mapValues } from '@dxos/util';
25
22
 
26
- import { AutomergeStorageAdapter } from './automerge-storage-adapter';
27
- import { AutomergeStorageWrapper } from './automerge-storage–wrapper';
23
+ import { LevelDBStorageAdapter, type StorageCallbacks } from './leveldb-storage-adapter';
28
24
  import { LocalHostNetworkAdapter } from './local-host-network-adapter';
29
25
  import { MeshNetworkAdapter } from './mesh-network-adapter';
26
+ import { levelMigration } from './migrations';
27
+ import { type SubLevelDB } from './types';
30
28
 
31
29
  export type { DocumentId };
32
30
 
33
- export interface MetadataMethods {
34
- markDirty(idToLastHash: Map<string, string>): Promise<void>;
35
- }
36
-
37
31
  export type AutomergeHostParams = {
38
- directory: Directory;
39
- metadata?: MetadataMethods;
32
+ db: SubLevelDB;
33
+ /**
34
+ * For migration purposes.
35
+ */
36
+ directory?: Directory;
37
+ storageCallbacks?: StorageCallbacks;
40
38
  };
41
39
 
42
40
  @trace.resource()
43
41
  export class AutomergeHost {
44
42
  private readonly _ctx = new Context();
45
- private readonly _repo: Repo;
46
- private readonly _meshNetwork: MeshNetworkAdapter;
47
- private readonly _clientNetwork: LocalHostNetworkAdapter;
48
- private readonly _storage: AutomergeStorageWrapper;
43
+ private readonly _directory?: Directory;
44
+ private readonly _db: SubLevelDB;
45
+ private readonly _storageCallbacks?: StorageCallbacks;
46
+
47
+ private _repo!: Repo;
48
+ private _meshNetwork!: MeshNetworkAdapter;
49
+ private _clientNetwork!: LocalHostNetworkAdapter;
50
+ private _storage!: StorageAdapterInterface & Lifecycle;
49
51
 
50
52
  @trace.info()
51
- private readonly _peerId: string;
53
+ private _peerId!: string;
52
54
 
53
55
  /**
54
56
  * spaceKey -> deviceKey[]
55
57
  */
56
58
  private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
57
59
 
58
- private readonly _updatingMetadata = new Map<string, Promise<void>>();
59
- private readonly _metadata?: MetadataMethods;
60
-
61
60
  public _requestedDocs = new Set<string>();
62
61
 
63
- constructor({ directory, metadata }: AutomergeHostParams) {
64
- this._metadata = metadata;
65
- this._meshNetwork = new MeshNetworkAdapter();
66
- this._clientNetwork = new LocalHostNetworkAdapter();
62
+ constructor({ directory, db, storageCallbacks }: AutomergeHostParams) {
63
+ this._directory = directory;
64
+ this._db = db;
65
+ this._storageCallbacks = storageCallbacks;
66
+ }
67
67
 
68
- this._storage = new AutomergeStorageWrapper({
69
- storage:
70
- // TODO(mykola): Delete specific handling of IDB storage.
71
- directory.type === StorageType.IDB
72
- ? new IndexedDBStorageAdapter(directory.path, 'data')
73
- : new AutomergeStorageAdapter(directory),
74
- callbacks: { beforeSave: (params) => this._beforeSave(params) },
68
+ async open() {
69
+ // TODO(mykola): remove this before 0.6 release.
70
+ this._directory && (await levelMigration({ db: this._db, directory: this._directory }));
71
+ this._storage = new LevelDBStorageAdapter({
72
+ db: this._db,
73
+ callbacks: this._storageCallbacks,
75
74
  });
75
+ await this._storage.open?.();
76
76
  this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
77
+
78
+ this._meshNetwork = new MeshNetworkAdapter();
79
+ this._clientNetwork = new LocalHostNetworkAdapter();
77
80
  this._repo = new Repo({
78
81
  peerId: this._peerId as PeerId,
79
82
  network: [this._clientNetwork, this._meshNetwork],
@@ -133,61 +136,17 @@ export class AutomergeHost {
133
136
  this._clientNetwork.ready();
134
137
  this._meshNetwork.ready();
135
138
 
136
- {
137
- const listener = ({ handle }: { handle: DocHandle<any> }) => this._onDocument(handle);
138
- this._repo.on('document', listener);
139
- this._ctx.onDispose(() => {
140
- this._repo.off('document', listener);
141
- });
142
- }
143
- }
144
-
145
- get repo(): Repo {
146
- return this._repo;
147
- }
148
-
149
- private async _beforeSave(path: StorageKey) {
150
- const id = path[0];
151
- if (this._updatingMetadata.has(id)) {
152
- return this._updatingMetadata.get(id);
153
- }
139
+ await this._clientNetwork.whenConnected();
154
140
  }
155
141
 
156
- private _onDocument(handle: DocHandle<any>) {
157
- const listener = (event: DocHandleChangePayload<any>) => this._onUpdate(event);
158
- handle.on('change', listener);
159
- this._ctx.onDispose(() => {
160
- handle.off('change', listener);
161
- });
142
+ async close() {
143
+ await this._storage.close?.();
144
+ await this._clientNetwork.close();
145
+ await this._ctx.dispose();
162
146
  }
163
147
 
164
- private _onUpdate(event: DocHandleChangePayload<any>) {
165
- if (this._metadata == null) {
166
- return;
167
- }
168
-
169
- const objectIds = getInlineChanges(event);
170
- if (objectIds.length === 0) {
171
- return;
172
- }
173
-
174
- const heads = getHeads(event.doc);
175
- const lastAvailableHash = heads.join('');
176
- if (!lastAvailableHash) {
177
- return;
178
- }
179
-
180
- const encodedIds = objectIds.map((objectId) => idCodec.encode({ documentId: event.handle.documentId, objectId }));
181
- const idToLastHash = new Map(encodedIds.map((id) => [id, lastAvailableHash]));
182
- const markingDirtyPromise = this._metadata
183
- .markDirty(idToLastHash)
184
- .then(() => {
185
- this._updatingMetadata.delete(event.handle.documentId);
186
- })
187
- .catch((err: Error) => {
188
- this._ctx.disposed && log.catch(err);
189
- });
190
- this._updatingMetadata.set(event.handle.documentId, markingDirtyPromise);
148
+ get repo(): Repo {
149
+ return this._repo;
191
150
  }
192
151
 
193
152
  @trace.info({ depth: null })
@@ -197,8 +156,8 @@ export class AutomergeHost {
197
156
  hasDoc: !!handle.docSync(),
198
157
  heads: handle.docSync() ? automerge.getHeads(handle.docSync()) : null,
199
158
  data:
200
- handle.docSync()?.doc &&
201
- mapValues(handle.docSync()?.doc, (value, key) => {
159
+ handle.docSync() &&
160
+ mapValues(handle.docSync(), (value, key) => {
202
161
  try {
203
162
  switch (key) {
204
163
  case 'access':
@@ -221,16 +180,22 @@ export class AutomergeHost {
221
180
  return this._repo.peers;
222
181
  }
223
182
 
224
- async close() {
225
- await this._storage.close();
226
- await this._clientNetwork.close();
227
- await this._ctx.dispose();
228
- }
229
-
230
183
  //
231
184
  // Methods for client-services.
232
185
  //
233
186
 
187
+ async flush({ documentIds }: FlushRequest): Promise<void> {
188
+ // Note: Wait for all requested documents to be loaded/synced from thin-client.
189
+ await Promise.all(documentIds?.map((id) => this._repo.find(id as DocumentId).whenReady()) ?? []);
190
+
191
+ // TODO(dmaretskyi): Workaround until the flush issue gets resolved.
192
+ try {
193
+ await asyncTimeout(this._repo.flush(documentIds as DocumentId[]), 500);
194
+ } catch (err) {
195
+ log.warn('flush error', { documentIds, err });
196
+ }
197
+ }
198
+
234
199
  syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
235
200
  return this._clientNetwork.syncRepo(request);
236
201
  }
@@ -257,24 +222,6 @@ export class AutomergeHost {
257
222
  }
258
223
  }
259
224
 
260
- // TODO(mykola): Reconcile with `getInlineAndLinkChanges` in AutomergeDB.
261
- const getInlineChanges = (event: DocHandleChangePayload<any>) => {
262
- const inlineChangedObjectIds = new Set<string>();
263
- for (const { path } of event.patches) {
264
- if (path.length < 2) {
265
- continue;
266
- }
267
- switch (path[0]) {
268
- case 'objects':
269
- if (path.length >= 2) {
270
- inlineChangedObjectIds.add(path[1]);
271
- }
272
- break;
273
- }
274
- }
275
- return [...inlineChangedObjectIds];
276
- };
277
-
278
225
  export const getSpaceKeyFromDoc = (doc: any): string | null => {
279
226
  // experimental_spaceKey is set on old documents, new ones are created with doc.access.spaceKey
280
227
  const rawSpaceKey = doc.access?.spaceKey ?? doc.experimental_spaceKey;
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Repo } from '@dxos/automerge/automerge-repo';
6
+ import { randomBytes } from '@dxos/crypto';
7
+ import { StorageType, createStorage } from '@dxos/random-access-storage';
8
+ import { describe, test } from '@dxos/test';
9
+
10
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
11
+
12
+ describe('AutomergeRepo', () => {
13
+ // Currently failing
14
+ test.skip('flush', async () => {
15
+ const repo = new Repo({
16
+ network: [],
17
+ storage: new AutomergeStorageAdapter(createStorage({ type: StorageType.NODE }).createDirectory()),
18
+ });
19
+ const handle = repo.create<{ field?: string }>();
20
+
21
+ for (let i = 0; i < 10; i++) {
22
+ const p = repo.flush([handle.documentId]);
23
+ handle.change((doc: any) => {
24
+ doc.field += randomBytes(1024).toString('hex');
25
+ });
26
+ await p;
27
+ }
28
+ });
29
+ });
@@ -4,5 +4,9 @@
4
4
 
5
5
  export * from './automerge-host';
6
6
  export * from './automerge-storage-adapter';
7
+ export * from './automerge-doc-loader';
8
+ export * from './leveldb-storage-adapter';
7
9
  export * from './local-host-network-adapter';
8
10
  export * from './mesh-network-adapter';
11
+ export * from './types';
12
+ export * from './reference';
@@ -0,0 +1,64 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import { Level } from 'level';
7
+
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { describe, openAndClose, test } from '@dxos/test';
10
+
11
+ import { type SubLevelDB } from './types';
12
+ import { createTestLevel } from '../testing';
13
+
14
+ describe('Level', () => {
15
+ test('missing keys', async () => {
16
+ const level = createTestLevel();
17
+ await openAndClose(level);
18
+
19
+ expect(() => level.get('missing')).to.throw;
20
+ });
21
+
22
+ test('data persistance after reload', async () => {
23
+ const path = `/tmp/dxos-${PublicKey.random().toHex()}`;
24
+ const level = new Level<string, string>(path);
25
+ await level.open();
26
+
27
+ const key = 'name';
28
+ const value = 'Rich';
29
+ {
30
+ await level.put(key, value);
31
+ expect(await level.get(key)).to.equal(value);
32
+ }
33
+
34
+ await level.close();
35
+
36
+ {
37
+ const level = new Level<string, string>(path);
38
+ await level.open();
39
+ expect(await level.get(key)).to.equal(value);
40
+ await level.clear();
41
+ expect(() => level.get(key)).to.throw;
42
+ }
43
+ });
44
+
45
+ test('batch different sublevels', async () => {
46
+ const level = createTestLevel();
47
+ await openAndClose(level);
48
+
49
+ const first: SubLevelDB = level.sublevel('first');
50
+ const second: SubLevelDB = level.sublevel('second');
51
+
52
+ const batch = first.batch();
53
+
54
+ const key = 'key';
55
+ const value = 'first-level-value';
56
+ batch.put(key, value, { sublevel: second });
57
+ await batch.write();
58
+
59
+ expect(() => first.get(key)).to.throw;
60
+ expect(await level.sublevel('second').get(key)).to.equal(value);
61
+ expect(await second.get(key)).to.equal(value);
62
+ expect(await second.iterator().all()).to.deep.equal([[key, value]]);
63
+ });
64
+ });
@@ -0,0 +1,117 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ // s
4
+
5
+ import { type MixedEncoding } from 'level-transcoder';
6
+
7
+ import { type StorageAdapterInterface, type Chunk, type StorageKey } from '@dxos/automerge/automerge-repo';
8
+ import { LifecycleState, Resource } from '@dxos/context';
9
+ import { type MaybePromise } from '@dxos/util';
10
+
11
+ import { type BatchLevel, type SubLevelDB } from './types';
12
+
13
+ export type LevelDBStorageAdapterParams = {
14
+ db: SubLevelDB;
15
+ callbacks?: StorageCallbacks;
16
+ };
17
+
18
+ export type BeforeSaveParams = { path: StorageKey; batch: BatchLevel };
19
+
20
+ export interface StorageCallbacks {
21
+ beforeSave(params: BeforeSaveParams): MaybePromise<void>;
22
+ afterSave(path: StorageKey): MaybePromise<void>;
23
+ }
24
+
25
+ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInterface {
26
+ constructor(private readonly _params: LevelDBStorageAdapterParams) {
27
+ super();
28
+ }
29
+
30
+ async load(keyArray: StorageKey): Promise<Uint8Array | undefined> {
31
+ try {
32
+ if (this._lifecycleState !== LifecycleState.OPEN) {
33
+ // TODO(mykola): this should be an error.
34
+ return undefined;
35
+ }
36
+ return await this._params.db.get<StorageKey, Uint8Array>(keyArray, { ...encodingOptions });
37
+ } catch (err: any) {
38
+ if (isLevelDbNotFoundError(err)) {
39
+ return undefined;
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ async save(keyArray: StorageKey, binary: Uint8Array): Promise<void> {
46
+ if (this._lifecycleState !== LifecycleState.OPEN) {
47
+ return undefined;
48
+ }
49
+ const batch = this._params.db.batch();
50
+
51
+ await this._params.callbacks?.beforeSave?.({ path: keyArray, batch });
52
+ batch.put<StorageKey, Uint8Array>(keyArray, Buffer.from(binary), {
53
+ ...encodingOptions,
54
+ });
55
+ await batch.write();
56
+
57
+ await this._params.callbacks?.afterSave?.(keyArray);
58
+ }
59
+
60
+ async remove(keyArray: StorageKey): Promise<void> {
61
+ if (this._lifecycleState !== LifecycleState.OPEN) {
62
+ return undefined;
63
+ }
64
+ await this._params.db.del<StorageKey>(keyArray, { ...encodingOptions });
65
+ }
66
+
67
+ async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
68
+ if (this._lifecycleState !== LifecycleState.OPEN) {
69
+ return [];
70
+ }
71
+ const result: Chunk[] = [];
72
+ for await (const [key, value] of this._params.db.iterator<StorageKey, Uint8Array>({
73
+ gte: keyPrefix,
74
+ lte: [...keyPrefix, '\uffff'],
75
+ ...encodingOptions,
76
+ })) {
77
+ result.push({
78
+ key,
79
+ data: value,
80
+ });
81
+ }
82
+ return result;
83
+ }
84
+
85
+ async removeRange(keyPrefix: StorageKey): Promise<void> {
86
+ if (this._lifecycleState !== LifecycleState.OPEN) {
87
+ return undefined;
88
+ }
89
+ const batch = this._params.db.batch();
90
+
91
+ for await (const [key] of this._params.db.iterator<StorageKey, Uint8Array>({
92
+ gte: keyPrefix,
93
+ lte: [...keyPrefix, '\uffff'],
94
+ ...encodingOptions,
95
+ })) {
96
+ batch.del<StorageKey>(key, { ...encodingOptions });
97
+ }
98
+ await batch.write();
99
+ }
100
+ }
101
+
102
+ const keyEncoder: MixedEncoding<StorageKey, Uint8Array, StorageKey> = {
103
+ encode: (key: StorageKey): Uint8Array =>
104
+ Buffer.from(key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-')),
105
+ decode: (key: Uint8Array): StorageKey =>
106
+ Buffer.from(key)
107
+ .toString()
108
+ .split('-')
109
+ .map((k) => k.replaceAll('%2D', '-').replaceAll('%25', '%')),
110
+ };
111
+
112
+ export const encodingOptions = {
113
+ keyEncoding: keyEncoder,
114
+ valueEncoding: 'buffer',
115
+ };
116
+
117
+ const isLevelDbNotFoundError = (err: any): boolean => err.code === 'LEVEL_NOT_FOUND';
@@ -6,7 +6,6 @@ import { Trigger } from '@dxos/async';
6
6
  import { NetworkAdapter, type Message, type PeerId, cbor } from '@dxos/automerge/automerge-repo';
7
7
  import { Stream } from '@dxos/codec-protobuf';
8
8
  import { invariant } from '@dxos/invariant';
9
- import { log } from '@dxos/log';
10
9
  import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
11
10
 
12
11
  type ClientSyncState = {
@@ -32,10 +31,17 @@ export class LocalHostNetworkAdapter extends NetworkAdapter {
32
31
  });
33
32
  }
34
33
 
35
- private _connected = new Trigger();
34
+ private readonly _connected = new Trigger();
35
+ private _isConnected: boolean = false;
36
36
 
37
+ /**
38
+ * Called by `Repo` to connect to the network.
39
+ *
40
+ * @param peerId Our peer Id.
41
+ */
37
42
  override connect(peerId: PeerId): void {
38
43
  this.peerId = peerId;
44
+ this._isConnected = true;
39
45
  this._connected.wake();
40
46
  // No-op. Client always connects first
41
47
  }
@@ -56,6 +62,10 @@ export class LocalHostNetworkAdapter extends NetworkAdapter {
56
62
  // No-op
57
63
  }
58
64
 
65
+ async whenConnected(): Promise<void> {
66
+ await this._connected.wait({ timeout: 10_000 });
67
+ }
68
+
59
69
  syncRepo({ id, syncMessage }: SyncRepoRequest): Stream<SyncRepoResponse> {
60
70
  const peerId = this._getPeerId(id);
61
71
 
@@ -77,26 +87,22 @@ export class LocalHostNetworkAdapter extends NetworkAdapter {
77
87
  },
78
88
  });
79
89
 
80
- this._connected
81
- .wait({ timeout: 1_000 })
82
- .then(() => {
83
- this.emit('peer-candidate', {
84
- peerMetadata: {},
85
- peerId,
86
- });
87
- })
88
- .catch((err) => log.catch(err));
90
+ invariant(this._isConnected);
91
+ this.emit('peer-candidate', {
92
+ peerMetadata: {},
93
+ peerId,
94
+ });
89
95
  });
90
96
  }
91
97
 
92
98
  async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
93
- await this._connected.wait({ timeout: 1_000 });
99
+ invariant(this._isConnected);
94
100
  const message = cbor.decode(syncMessage!) as Message;
95
101
  this.emit('message', message);
96
102
  }
97
103
 
98
104
  async getHostInfo(): Promise<HostInfo> {
99
- await this._connected.wait({ timeout: 1_000 });
105
+ invariant(this._isConnected);
100
106
  invariant(this.peerId, 'Peer id not set.');
101
107
  return {
102
108
  peerId: this.peerId,
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type StorageKey } from '@dxos/automerge/automerge-repo';
6
+ import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
7
+ import { log } from '@dxos/log';
8
+ import { StorageType, type Directory } from '@dxos/random-access-storage';
9
+
10
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
11
+ import { encodingOptions } from './leveldb-storage-adapter';
12
+ import { type SubLevelDB } from './types';
13
+
14
+ export const levelMigration = async ({ db, directory }: { db: SubLevelDB; directory: Directory }) => {
15
+ // Note: Make automigration from previous storage to leveldb here.
16
+ const isNewLevel = !(await db
17
+ .iterator<StorageKey, Uint8Array>({
18
+ ...encodingOptions,
19
+ })
20
+ .next());
21
+
22
+ if (!isNewLevel) {
23
+ return;
24
+ }
25
+
26
+ const oldStorageAdapter =
27
+ directory.type === StorageType.IDB
28
+ ? new IndexedDBStorageAdapter(directory.path, 'data')
29
+ : new AutomergeStorageAdapter(directory);
30
+
31
+ const chunks = await oldStorageAdapter.loadRange([]);
32
+ if (chunks.length === 0) {
33
+ return;
34
+ }
35
+ const batch = db.batch();
36
+ log.info('found chunks on old storage adapter', { chunks: chunks.length });
37
+ for (const { key, data } of await oldStorageAdapter.loadRange([])) {
38
+ data && batch.put<StorageKey, Uint8Array>(key, data, { ...encodingOptions });
39
+ }
40
+ await batch.write();
41
+ };
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Reference } from '@dxos/echo-db';
6
+
7
+ export const REFERENCE_TYPE_TAG = 'dxos.echo.model.document.Reference';
8
+
9
+ /**
10
+ * Reference as it is stored in Automerge document.
11
+ */
12
+ export type EncodedReferenceObject = {
13
+ '@type': typeof REFERENCE_TYPE_TAG;
14
+ itemId: string | null;
15
+ protocol: string | null;
16
+ host: string | null;
17
+ };
18
+
19
+ export const encodeReference = (reference: Reference): EncodedReferenceObject => ({
20
+ '@type': REFERENCE_TYPE_TAG,
21
+ // NOTE: Automerge do not support undefined values, so we need to use null instead.
22
+ itemId: reference.itemId ?? null,
23
+ protocol: reference.protocol ?? null,
24
+ host: reference.host ?? null,
25
+ });
26
+
27
+ export const decodeReference = (value: any) =>
28
+ new Reference(value.itemId, value.protocol ?? undefined, value.host ?? undefined);
29
+
30
+ export const isEncodedReferenceObject = (value: any): value is EncodedReferenceObject =>
31
+ typeof value === 'object' && value !== null && value['@type'] === REFERENCE_TYPE_TAG;