@dxos/echo-pipeline 0.4.10-main.d4e372f → 0.4.10-main.d51f2c2

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 (60) 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 +279 -159
  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 +285 -168
  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 +2 -0
  16. package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-host.d.ts +20 -10
  18. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  19. package/dist/types/src/automerge/index.d.ts +1 -0
  20. package/dist/types/src/automerge/index.d.ts.map +1 -1
  21. package/dist/types/src/automerge/level.test.d.ts +2 -0
  22. package/dist/types/src/automerge/level.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +27 -0
  24. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -0
  25. package/dist/types/src/automerge/migrations.d.ts +7 -0
  26. package/dist/types/src/automerge/migrations.d.ts.map +1 -0
  27. package/dist/types/src/automerge/reference.d.ts +15 -0
  28. package/dist/types/src/automerge/reference.d.ts.map +1 -0
  29. package/dist/types/src/automerge/storage-adapter.test.d.ts +2 -0
  30. package/dist/types/src/automerge/storage-adapter.test.d.ts.map +1 -0
  31. package/dist/types/src/automerge/types.d.ts +7 -2
  32. package/dist/types/src/automerge/types.d.ts.map +1 -1
  33. package/dist/types/src/metadata/metadata-store.d.ts +2 -1
  34. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  35. package/dist/types/src/space/space.d.ts +4 -8
  36. package/dist/types/src/space/space.d.ts.map +1 -1
  37. package/dist/types/src/testing/index.d.ts +1 -0
  38. package/dist/types/src/testing/index.d.ts.map +1 -1
  39. package/dist/types/src/testing/level.d.ts +3 -0
  40. package/dist/types/src/testing/level.d.ts.map +1 -0
  41. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  42. package/package.json +33 -30
  43. package/src/automerge/automerge-doc-loader.ts +6 -0
  44. package/src/automerge/automerge-host.test.ts +19 -5
  45. package/src/automerge/automerge-host.ts +52 -33
  46. package/src/automerge/index.ts +1 -0
  47. package/src/automerge/level.test.ts +43 -0
  48. package/src/automerge/leveldb-storage-adapter.ts +105 -0
  49. package/src/automerge/migrations.ts +41 -0
  50. package/src/automerge/reference.ts +31 -0
  51. package/src/automerge/storage-adapter.test.ts +90 -0
  52. package/src/automerge/types.ts +7 -5
  53. package/src/db-host/data-service.ts +1 -1
  54. package/src/metadata/metadata-store.ts +17 -8
  55. package/src/space/space.test.ts +7 -7
  56. package/src/space/space.ts +6 -21
  57. package/src/testing/index.ts +1 -0
  58. package/src/testing/level.ts +11 -0
  59. package/dist/lib/browser/chunk-RTEEJ723.mjs.map +0 -7
  60. package/dist/lib/node/chunk-7VZVCCNF.cjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-pipeline",
3
- "version": "0.4.10-main.d4e372f",
3
+ "version": "0.4.10-main.d51f2c2",
4
4
  "description": "ECHO database.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -35,36 +35,39 @@
35
35
  "src"
36
36
  ],
37
37
  "dependencies": {
38
+ "abstract-level": "^1.0.2",
38
39
  "crc-32": "^1.2.2",
39
- "@dxos/automerge": "0.4.10-main.d4e372f",
40
- "@dxos/async": "0.4.10-main.d4e372f",
41
- "@dxos/context": "0.4.10-main.d4e372f",
42
- "@dxos/codec-protobuf": "0.4.10-main.d4e372f",
43
- "@dxos/crypto": "0.4.10-main.d4e372f",
44
- "@dxos/credentials": "0.4.10-main.d4e372f",
45
- "@dxos/debug": "0.4.10-main.d4e372f",
46
- "@dxos/echo-db": "0.4.10-main.d4e372f",
47
- "@dxos/feed-store": "0.4.10-main.d4e372f",
48
- "@dxos/hypercore": "0.4.10-main.d4e372f",
49
- "@dxos/invariant": "0.4.10-main.d4e372f",
50
- "@dxos/keyring": "0.4.10-main.d4e372f",
51
- "@dxos/keys": "0.4.10-main.d4e372f",
52
- "@dxos/messaging": "0.4.10-main.d4e372f",
53
- "@dxos/log": "0.4.10-main.d4e372f",
54
- "@dxos/network-manager": "0.4.10-main.d4e372f",
55
- "@dxos/node-std": "0.4.10-main.d4e372f",
56
- "@dxos/protocols": "0.4.10-main.d4e372f",
57
- "@dxos/random-access-storage": "0.4.10-main.d4e372f",
58
- "@dxos/teleport": "0.4.10-main.d4e372f",
59
- "@dxos/rpc": "0.4.10-main.d4e372f",
60
- "@dxos/teleport-extension-automerge-replicator": "0.4.10-main.d4e372f",
61
- "@dxos/teleport-extension-gossip": "0.4.10-main.d4e372f",
62
- "@dxos/teleport-extension-object-sync": "0.4.10-main.d4e372f",
63
- "@dxos/teleport-extension-replicator": "0.4.10-main.d4e372f",
64
- "@dxos/tracing": "0.4.10-main.d4e372f",
65
- "@dxos/typings": "0.4.10-main.d4e372f",
66
- "@dxos/timeframe": "0.4.10-main.d4e372f",
67
- "@dxos/util": "0.4.10-main.d4e372f"
40
+ "level": "^8.0.1",
41
+ "level-transcoder": "^1.0.1",
42
+ "@dxos/async": "0.4.10-main.d51f2c2",
43
+ "@dxos/codec-protobuf": "0.4.10-main.d51f2c2",
44
+ "@dxos/context": "0.4.10-main.d51f2c2",
45
+ "@dxos/credentials": "0.4.10-main.d51f2c2",
46
+ "@dxos/automerge": "0.4.10-main.d51f2c2",
47
+ "@dxos/crypto": "0.4.10-main.d51f2c2",
48
+ "@dxos/debug": "0.4.10-main.d51f2c2",
49
+ "@dxos/feed-store": "0.4.10-main.d51f2c2",
50
+ "@dxos/echo-db": "0.4.10-main.d51f2c2",
51
+ "@dxos/hypercore": "0.4.10-main.d51f2c2",
52
+ "@dxos/keyring": "0.4.10-main.d51f2c2",
53
+ "@dxos/keys": "0.4.10-main.d51f2c2",
54
+ "@dxos/invariant": "0.4.10-main.d51f2c2",
55
+ "@dxos/log": "0.4.10-main.d51f2c2",
56
+ "@dxos/messaging": "0.4.10-main.d51f2c2",
57
+ "@dxos/node-std": "0.4.10-main.d51f2c2",
58
+ "@dxos/protocols": "0.4.10-main.d51f2c2",
59
+ "@dxos/random-access-storage": "0.4.10-main.d51f2c2",
60
+ "@dxos/network-manager": "0.4.10-main.d51f2c2",
61
+ "@dxos/rpc": "0.4.10-main.d51f2c2",
62
+ "@dxos/teleport": "0.4.10-main.d51f2c2",
63
+ "@dxos/teleport-extension-automerge-replicator": "0.4.10-main.d51f2c2",
64
+ "@dxos/teleport-extension-gossip": "0.4.10-main.d51f2c2",
65
+ "@dxos/teleport-extension-object-sync": "0.4.10-main.d51f2c2",
66
+ "@dxos/timeframe": "0.4.10-main.d51f2c2",
67
+ "@dxos/typings": "0.4.10-main.d51f2c2",
68
+ "@dxos/teleport-extension-replicator": "0.4.10-main.d51f2c2",
69
+ "@dxos/tracing": "0.4.10-main.d51f2c2",
70
+ "@dxos/util": "0.4.10-main.d51f2c2"
68
71
  },
69
72
  "devDependencies": {
70
73
  "fast-check": "^3.15.1",
@@ -17,6 +17,8 @@ type SpaceDocumentLinks = SpaceDoc['links'];
17
17
  export interface AutomergeDocumentLoader {
18
18
  onObjectDocumentLoaded: Event<ObjectDocumentLoaded>;
19
19
 
20
+ getAllHandles(): DocHandle<SpaceDoc>[];
21
+
20
22
  loadSpaceRootDocHandle(ctx: Context, spaceState: SpaceState): Promise<void>;
21
23
  loadObjectDocument(objectId: string): void;
22
24
  getSpaceRootDocHandle(): DocHandle<SpaceDoc>;
@@ -52,6 +54,10 @@ export class AutomergeDocumentLoaderImpl implements AutomergeDocumentLoader {
52
54
  private readonly _repo: Repo,
53
55
  ) {}
54
56
 
57
+ getAllHandles(): DocHandle<SpaceDoc>[] {
58
+ return [...new Set(this._objectDocumentHandles.values())];
59
+ }
60
+
55
61
  public async loadSpaceRootDocHandle(ctx: Context, spaceState: SpaceState): Promise<void> {
56
62
  if (this._spaceRootDocHandle != null) {
57
63
  return;
@@ -25,10 +25,18 @@ import { arrayToBuffer, bufferToArray } from '@dxos/util';
25
25
  import { AutomergeHost } from './automerge-host';
26
26
  import { AutomergeStorageAdapter } from './automerge-storage-adapter';
27
27
  import { MeshNetworkAdapter } from './mesh-network-adapter';
28
+ import { createTestLevel } from '../testing';
28
29
 
29
30
  describe('AutomergeHost', () => {
30
- test('can create documents', () => {
31
- const host = new AutomergeHost({ directory: createStorage({ type: StorageType.RAM }).createDirectory() });
31
+ test('can create documents', async () => {
32
+ const level = createTestLevel();
33
+ await level.open();
34
+ afterTest(() => level.close());
35
+ const host = new AutomergeHost({
36
+ db: level.sublevel('automerge'),
37
+ });
38
+ await host.open();
39
+ afterTest(() => host.close());
32
40
 
33
41
  const handle = host.repo.create();
34
42
  handle.change((doc: any) => {
@@ -38,9 +46,13 @@ describe('AutomergeHost', () => {
38
46
  });
39
47
 
40
48
  test('changes are preserved in storage', async () => {
41
- const storageDirectory = createStorage({ type: StorageType.RAM }).createDirectory();
49
+ const level = createTestLevel();
50
+ await level.open();
51
+ afterTest(() => level.close());
42
52
 
43
- const host = new AutomergeHost({ directory: storageDirectory });
53
+ const host = new AutomergeHost({ db: level.sublevel('automerge') });
54
+ await host.open();
55
+ afterTest(() => host.close());
44
56
  const handle = host.repo.create();
45
57
  handle.change((doc: any) => {
46
58
  doc.text = 'Hello world';
@@ -50,7 +62,9 @@ describe('AutomergeHost', () => {
50
62
  // TODO(dmaretskyi): Is there a way to know when automerge has finished saving?
51
63
  await sleep(100);
52
64
 
53
- const host2 = new AutomergeHost({ directory: storageDirectory });
65
+ const host2 = new AutomergeHost({ db: level.sublevel('automerge') });
66
+ await host2.open();
67
+ afterTest(() => host2.close());
54
68
  const handle2 = host2.repo.find(url);
55
69
  await handle2.whenReady();
56
70
  expect(handle2.docSync().text).toEqual('Hello world');
@@ -10,23 +10,29 @@ import {
10
10
  type StorageKey,
11
11
  type DocHandle,
12
12
  type DocHandleChangePayload,
13
+ type StorageAdapterInterface,
13
14
  } from '@dxos/automerge/automerge-repo';
14
- import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
15
15
  import { type Stream } from '@dxos/codec-protobuf';
16
16
  import { Context } from '@dxos/context';
17
17
  import { PublicKey } from '@dxos/keys';
18
18
  import { log } from '@dxos/log';
19
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';
20
+ import {
21
+ type FlushRequest,
22
+ type HostInfo,
23
+ type SyncRepoRequest,
24
+ type SyncRepoResponse,
25
+ } from '@dxos/protocols/proto/dxos/echo/service';
26
+ import { type Directory } from '@dxos/random-access-storage';
22
27
  import { type AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
23
28
  import { trace } from '@dxos/tracing';
24
- import { ComplexMap, ComplexSet, defaultMap, mapValues } from '@dxos/util';
29
+ import { ComplexMap, ComplexSet, type MaybePromise, defaultMap, mapValues } from '@dxos/util';
25
30
 
26
- import { AutomergeStorageAdapter } from './automerge-storage-adapter';
27
- import { AutomergeStorageWrapper } from './automerge-storage–wrapper';
31
+ import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
28
32
  import { LocalHostNetworkAdapter } from './local-host-network-adapter';
29
33
  import { MeshNetworkAdapter } from './mesh-network-adapter';
34
+ import { levelMigration } from './migrations';
35
+ import { type MySublevel } from './types';
30
36
 
31
37
  export type { DocumentId };
32
38
 
@@ -35,20 +41,28 @@ export interface MetadataMethods {
35
41
  }
36
42
 
37
43
  export type AutomergeHostParams = {
38
- directory: Directory;
44
+ db: MaybePromise<MySublevel>;
45
+ /**
46
+ * For migration purposes.
47
+ */
48
+ directory?: Directory;
39
49
  metadata?: MetadataMethods;
40
50
  };
41
51
 
42
52
  @trace.resource()
43
53
  export class AutomergeHost {
44
54
  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;
55
+ private readonly _directory?: Directory;
56
+ private readonly _db: MaybePromise<MySublevel>;
57
+ private readonly _metadata?: MetadataMethods;
58
+
59
+ private _repo!: Repo;
60
+ private _meshNetwork!: MeshNetworkAdapter;
61
+ private _clientNetwork!: LocalHostNetworkAdapter;
62
+ private _storage!: StorageAdapterInterface & { close?: () => void };
49
63
 
50
64
  @trace.info()
51
- private readonly _peerId: string;
65
+ private _peerId!: string;
52
66
 
53
67
  /**
54
68
  * spaceKey -> deviceKey[]
@@ -56,24 +70,25 @@ export class AutomergeHost {
56
70
  private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
57
71
 
58
72
  private readonly _updatingMetadata = new Map<string, Promise<void>>();
59
- private readonly _metadata?: MetadataMethods;
60
73
 
61
74
  public _requestedDocs = new Set<string>();
62
75
 
63
- constructor({ directory, metadata }: AutomergeHostParams) {
76
+ constructor({ directory, db, metadata }: AutomergeHostParams) {
77
+ this._directory = directory;
78
+ this._db = db;
64
79
  this._metadata = metadata;
65
- this._meshNetwork = new MeshNetworkAdapter();
66
- this._clientNetwork = new LocalHostNetworkAdapter();
80
+ }
67
81
 
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),
82
+ async open() {
83
+ this._directory && (await levelMigration({ db: await this._db, directory: this._directory }));
84
+ this._storage = new LevelDBStorageAdapter({
85
+ db: await this._db,
74
86
  callbacks: { beforeSave: (params) => this._beforeSave(params) },
75
87
  });
76
88
  this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
89
+
90
+ this._meshNetwork = new MeshNetworkAdapter();
91
+ this._clientNetwork = new LocalHostNetworkAdapter();
77
92
  this._repo = new Repo({
78
93
  peerId: this._peerId as PeerId,
79
94
  network: [this._clientNetwork, this._meshNetwork],
@@ -138,10 +153,17 @@ export class AutomergeHost {
138
153
  this._repo.on('document', listener);
139
154
  this._ctx.onDispose(() => {
140
155
  this._repo.off('document', listener);
156
+ Object.values(this._repo.handles).forEach((handle) => handle.off('change'));
141
157
  });
142
158
  }
143
159
  }
144
160
 
161
+ async close() {
162
+ this._storage.close?.();
163
+ await this._clientNetwork.close();
164
+ await this._ctx.dispose();
165
+ }
166
+
145
167
  get repo(): Repo {
146
168
  return this._repo;
147
169
  }
@@ -156,9 +178,6 @@ export class AutomergeHost {
156
178
  private _onDocument(handle: DocHandle<any>) {
157
179
  const listener = (event: DocHandleChangePayload<any>) => this._onUpdate(event);
158
180
  handle.on('change', listener);
159
- this._ctx.onDispose(() => {
160
- handle.off('change', listener);
161
- });
162
181
  }
163
182
 
164
183
  private _onUpdate(event: DocHandleChangePayload<any>) {
@@ -197,8 +216,8 @@ export class AutomergeHost {
197
216
  hasDoc: !!handle.docSync(),
198
217
  heads: handle.docSync() ? automerge.getHeads(handle.docSync()) : null,
199
218
  data:
200
- handle.docSync()?.doc &&
201
- mapValues(handle.docSync()?.doc, (value, key) => {
219
+ handle.docSync() &&
220
+ mapValues(handle.docSync(), (value, key) => {
202
221
  try {
203
222
  switch (key) {
204
223
  case 'access':
@@ -221,16 +240,16 @@ export class AutomergeHost {
221
240
  return this._repo.peers;
222
241
  }
223
242
 
224
- async close() {
225
- await this._storage.close();
226
- await this._clientNetwork.close();
227
- await this._ctx.dispose();
228
- }
229
-
230
243
  //
231
244
  // Methods for client-services.
232
245
  //
233
246
 
247
+ async flush({ documentIds }: FlushRequest): Promise<void> {
248
+ // Note: Wait for all requested documents to be loaded/synced from thin-client.
249
+ await Promise.all(documentIds?.map((id) => this._repo.find(id as DocumentId).whenReady()) ?? []);
250
+ await this._repo.flush(documentIds as DocumentId[]);
251
+ }
252
+
234
253
  syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
235
254
  return this._clientNetwork.syncRepo(request);
236
255
  }
@@ -8,3 +8,4 @@ export * from './automerge-doc-loader';
8
8
  export * from './local-host-network-adapter';
9
9
  export * from './mesh-network-adapter';
10
10
  export * from './types';
11
+ export * from './reference';
@@ -0,0 +1,43 @@
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, test } from '@dxos/test';
10
+
11
+ import { createTestLevel } from '../testing';
12
+
13
+ describe('Level', () => {
14
+ test('missing keys', async () => {
15
+ const level = createTestLevel();
16
+ await level.open();
17
+
18
+ expect(() => level.get('missing')).to.throw;
19
+ });
20
+
21
+ test('data persistance after reload', async () => {
22
+ const path = `/tmp/dxos-${PublicKey.random().toHex()}`;
23
+ const level = new Level<string, string>(path);
24
+ await level.open();
25
+
26
+ const key = 'name';
27
+ const value = 'Rich';
28
+ {
29
+ await level.put(key, value);
30
+ expect(await level.get(key)).to.equal(value);
31
+ }
32
+
33
+ await level.close();
34
+
35
+ {
36
+ const level = new Level<string, string>(path);
37
+ await level.open();
38
+ expect(await level.get(key)).to.equal(value);
39
+ await level.clear();
40
+ expect(() => level.get(key)).to.throw;
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,105 @@
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 { type MaybePromise } from '@dxos/util';
9
+
10
+ import { type MySublevel } from './types';
11
+
12
+ export type LevelDBStorageAdapterParams = {
13
+ db: MySublevel;
14
+ callbacks?: StorageCallbacks;
15
+ };
16
+
17
+ export type StorageCallbacks = {
18
+ beforeSave?: (path: StorageKey) => MaybePromise<void>;
19
+ afterSave?: (path: StorageKey) => MaybePromise<void>;
20
+ };
21
+
22
+ export class LevelDBStorageAdapter implements StorageAdapterInterface {
23
+ private _state: 'opened' | 'closed' = 'opened';
24
+ constructor(private readonly _params: LevelDBStorageAdapterParams) {}
25
+
26
+ async load(keyArray: StorageKey): Promise<Uint8Array | undefined> {
27
+ if (this._state !== 'opened') {
28
+ return undefined;
29
+ }
30
+ return this._params.db
31
+ .get<StorageKey, Uint8Array>(keyArray, { ...encodingOptions })
32
+ .catch((err) => (err.code === 'LEVEL_NOT_FOUND' ? undefined : Promise.reject(err)));
33
+ }
34
+
35
+ async save(keyArray: StorageKey, binary: Uint8Array): Promise<void> {
36
+ if (this._state !== 'opened') {
37
+ return undefined;
38
+ }
39
+ await this._params.callbacks?.beforeSave?.(keyArray);
40
+ await this._params.db.put<StorageKey, Uint8Array>(keyArray, Buffer.from(binary), {
41
+ ...encodingOptions,
42
+ });
43
+ await this._params.callbacks?.afterSave?.(keyArray);
44
+ }
45
+
46
+ async remove(keyArray: StorageKey): Promise<void> {
47
+ if (this._state !== 'opened') {
48
+ return undefined;
49
+ }
50
+ await this._params.db.del<StorageKey>(keyArray, { ...encodingOptions });
51
+ }
52
+
53
+ async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
54
+ if (this._state !== 'opened') {
55
+ return [];
56
+ }
57
+ const result: Chunk[] = [];
58
+ for await (const [key, value] of this._params.db.iterator<StorageKey, Uint8Array>({
59
+ gte: keyPrefix,
60
+ lte: [...keyPrefix, '\uffff'],
61
+ ...encodingOptions,
62
+ })) {
63
+ result.push({
64
+ key,
65
+ data: value,
66
+ });
67
+ }
68
+ return result;
69
+ }
70
+
71
+ async removeRange(keyPrefix: StorageKey): Promise<void> {
72
+ if (this._state !== 'opened') {
73
+ return undefined;
74
+ }
75
+ const batch = this._params.db.batch();
76
+
77
+ for await (const [key] of this._params.db.iterator<StorageKey, Uint8Array>({
78
+ gte: keyPrefix,
79
+ lte: [...keyPrefix, '\uffff'],
80
+ ...encodingOptions,
81
+ })) {
82
+ batch.del<StorageKey>(key, { ...encodingOptions });
83
+ }
84
+ await batch.write();
85
+ }
86
+
87
+ close() {
88
+ this._state = 'closed';
89
+ }
90
+ }
91
+
92
+ const keyEncoder: MixedEncoding<StorageKey, Uint8Array, StorageKey> = {
93
+ encode: (key: StorageKey): Uint8Array =>
94
+ Buffer.from(key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-')),
95
+ decode: (key: Uint8Array): StorageKey =>
96
+ Buffer.from(key)
97
+ .toString()
98
+ .split('-')
99
+ .map((k) => k.replaceAll('%2D', '-').replaceAll('%25', '%')),
100
+ };
101
+
102
+ export const encodingOptions = {
103
+ keyEncoding: keyEncoder,
104
+ valueEncoding: 'buffer',
105
+ };
@@ -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 MySublevel } from './types';
13
+
14
+ export const levelMigration = async ({ db, directory }: { db: MySublevel; 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;
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { type StorageAdapterInterface } from '@dxos/automerge/automerge-repo';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { StorageType, createStorage } from '@dxos/random-access-storage';
10
+ import { afterTest, describe, test } from '@dxos/test';
11
+ import { type MaybePromise } from '@dxos/util';
12
+
13
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
14
+ import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
15
+ import { createTestLevel } from '../testing';
16
+
17
+ const runTests = (
18
+ testNamespace: string,
19
+ /** Run per test. Expects automatic clean-up with `afterTest`. */ createAdapter: () => MaybePromise<StorageAdapterInterface>,
20
+ ) => {
21
+ describe(testNamespace, () => {
22
+ const chunks = [
23
+ { key: ['a', 'b', 'c', '1'], data: PublicKey.random().asUint8Array() },
24
+ { key: ['a', 'b', 'c', '2'], data: PublicKey.random().asUint8Array() },
25
+ { key: ['a', 'b', 'd', '3'], data: PublicKey.random().asUint8Array() },
26
+ { key: ['a', 'b', 'd', '4'], data: PublicKey.random().asUint8Array() },
27
+ ];
28
+
29
+ test('should store and retrieve data', async () => {
30
+ const adapter = await createAdapter();
31
+
32
+ await adapter.save(chunks[0].key, chunks[0].data);
33
+ expect(await adapter.load(chunks[0].key)).to.deep.equal(chunks[0].data);
34
+ });
35
+
36
+ test('loadRange return inputs with correct prefixes', async () => {
37
+ const adapter = await createAdapter();
38
+
39
+ for (const chunk of chunks) {
40
+ await adapter.save(chunk.key, chunk.data);
41
+ }
42
+
43
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(4);
44
+ expect((await adapter.loadRange(['a', 'b', 'c']))[0]).to.deep.equal(chunks[0]);
45
+ expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(2);
46
+ });
47
+
48
+ test('deletion works', async () => {
49
+ const adapter = await createAdapter();
50
+
51
+ for (const chunk of chunks) {
52
+ await adapter.save(chunk.key, chunk.data);
53
+ }
54
+
55
+ await adapter.remove(['a', 'b', 'c', '1']);
56
+
57
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(3);
58
+ expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(1);
59
+
60
+ await adapter.removeRange(['a', 'b', 'd']);
61
+
62
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(1);
63
+ expect((await adapter.loadRange(['a', 'b']))[0]).to.deep.equal(chunks[1]);
64
+ expect(await adapter.load(['a', 'b', 'c', '2'])).to.deep.equal(chunks[1].data);
65
+ expect(await adapter.load(['a', 'b', 'd', '3'])).to.be.undefined;
66
+ });
67
+ });
68
+ };
69
+
70
+ /**
71
+ * Run tests for AutomergeStorageAdapter.
72
+ */
73
+ runTests('AutomergeStorageAdapter', () => {
74
+ const storage = createStorage({ type: StorageType.RAM });
75
+ afterTest(() => storage.close());
76
+ const dir = storage.createDirectory('automerge');
77
+ const adapter = new AutomergeStorageAdapter(dir);
78
+ afterTest(() => adapter.close());
79
+ return adapter;
80
+ });
81
+
82
+ /**
83
+ * Run tests for LevelDBStorageAdapter.
84
+ */
85
+ runTests('LevelDBStorageAdapter', async () => {
86
+ const level = createTestLevel();
87
+ await level.open();
88
+ afterTest(() => level.close());
89
+ return new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
90
+ });
@@ -2,11 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- //
6
- // Copyright 2023 DXOS.org
7
- //
5
+ import { type AbstractSublevel } from 'abstract-level';
6
+ import { type Level } from 'level';
8
7
 
9
- import { type Reference } from '@dxos/echo-db';
8
+ import { type EncodedReferenceObject } from './reference';
10
9
 
11
10
  export type SpaceState = {
12
11
  // Url of the root automerge document.
@@ -79,5 +78,8 @@ export type ObjectSystem = {
79
78
  /**
80
79
  * Object reference ('protobuf' protocol) type.
81
80
  */
82
- type?: Reference;
81
+ type?: EncodedReferenceObject;
83
82
  };
83
+
84
+ export type MyLevel = Level<string, string>;
85
+ export type MySublevel = AbstractSublevel<any, string | Buffer | Uint8Array, string, string>;
@@ -33,7 +33,7 @@ export class DataServiceImpl implements DataService {
33
33
  }
34
34
 
35
35
  async flush(request: FlushRequest): Promise<void> {
36
- // TODO(dmaretskyi): Implement with automerge.
36
+ await this._automergeHost.flush(request);
37
37
  }
38
38
 
39
39
  // Automerge specific.