@dxos/echo-pipeline 0.5.9-main.1c1903d → 0.5.9-main.21b00b3

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 (31) hide show
  1. package/dist/lib/browser/index.mjs +207 -121
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +203 -122
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/automerge/automerge-host.d.ts +8 -1
  8. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  9. package/dist/types/src/automerge/automerge-storage-adapter.d.ts +16 -0
  10. package/dist/types/src/automerge/automerge-storage-adapter.d.ts.map +1 -0
  11. package/dist/types/src/automerge/automerge-storage/342/200/223wrapper.d.ts +25 -0
  12. package/dist/types/src/automerge/automerge-storage/342/200/223wrapper.d.ts.map +1 -0
  13. package/dist/types/src/automerge/echo-network-adapter.d.ts +0 -6
  14. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  15. package/dist/types/src/automerge/echo-replicator.d.ts +0 -1
  16. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  17. package/dist/types/src/automerge/index.d.ts +1 -0
  18. package/dist/types/src/automerge/index.d.ts.map +1 -1
  19. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  20. package/dist/types/src/automerge/migrations.d.ts +7 -0
  21. package/dist/types/src/automerge/migrations.d.ts.map +1 -0
  22. package/package.json +33 -33
  23. package/src/automerge/automerge-host.ts +21 -8
  24. package/src/automerge/automerge-storage-adapter.ts +103 -0
  25. package/src/automerge/automerge-storage/342/200/223wrapper.ts +59 -0
  26. package/src/automerge/echo-network-adapter.ts +8 -24
  27. package/src/automerge/echo-replicator.ts +0 -2
  28. package/src/automerge/index.ts +1 -0
  29. package/src/automerge/mesh-echo-replicator.ts +1 -3
  30. package/src/automerge/migrations.ts +42 -0
  31. package/src/automerge/storage-adapter.test.ts +139 -103
@@ -0,0 +1,103 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+ //
5
+ // Copyright 2023 DXOS.org
6
+ //
7
+
8
+ import { type Chunk, type StorageKey, type StorageAdapterInterface } from '@dxos/automerge/automerge-repo';
9
+ import { type Directory } from '@dxos/random-access-storage';
10
+ import { arrayToBuffer, bufferToArray } from '@dxos/util';
11
+
12
+ export class AutomergeStorageAdapter implements StorageAdapterInterface {
13
+ // TODO(mykola): Hack for restricting automerge Repo to access storage if Host is `closed`.
14
+ // Automerge Repo do not have any lifetime management.
15
+ private _state: 'opened' | 'closed' = 'opened';
16
+
17
+ constructor(private readonly _directory: Directory) {}
18
+
19
+ async load(key: StorageKey): Promise<Uint8Array | undefined> {
20
+ if (this._state !== 'opened') {
21
+ return undefined;
22
+ }
23
+ const filename = this._getFilename(key);
24
+ const file = this._directory.getOrCreateFile(filename);
25
+ const { size } = await file.stat();
26
+ if (!size || size === 0) {
27
+ return undefined;
28
+ }
29
+ const buffer = await file.read(0, size);
30
+ return bufferToArray(buffer);
31
+ }
32
+
33
+ async save(key: StorageKey, data: Uint8Array): Promise<void> {
34
+ if (this._state !== 'opened') {
35
+ return undefined;
36
+ }
37
+ const filename = this._getFilename(key);
38
+ const file = this._directory.getOrCreateFile(filename);
39
+ await file.write(0, arrayToBuffer(data));
40
+ await file.truncate?.(data.length);
41
+
42
+ await file.flush?.();
43
+ }
44
+
45
+ async remove(key: StorageKey): Promise<void> {
46
+ if (this._state !== 'opened') {
47
+ return undefined;
48
+ }
49
+ // TODO(dmaretskyi): Better deletion.
50
+ const filename = this._getFilename(key);
51
+ const file = this._directory.getOrCreateFile(filename);
52
+ await file.destroy();
53
+ }
54
+
55
+ async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
56
+ if (this._state !== 'opened') {
57
+ return [];
58
+ }
59
+ const filename = this._getFilename(keyPrefix);
60
+ const entries = await this._directory.list();
61
+ return Promise.all(
62
+ entries
63
+ .filter((entry) => entry.startsWith(filename))
64
+ .map(async (entry): Promise<Chunk> => {
65
+ const file = this._directory.getOrCreateFile(entry);
66
+ const { size } = await file.stat();
67
+ const buffer = await file.read(0, size);
68
+ return {
69
+ key: this._getKeyFromFilename(entry),
70
+ data: bufferToArray(buffer),
71
+ };
72
+ }),
73
+ );
74
+ }
75
+
76
+ async removeRange(keyPrefix: StorageKey): Promise<void> {
77
+ if (this._state !== 'opened') {
78
+ return undefined;
79
+ }
80
+ const filename = this._getFilename(keyPrefix);
81
+ const entries = await this._directory.list();
82
+ await Promise.all(
83
+ entries
84
+ .filter((entry) => entry.startsWith(filename))
85
+ .map(async (entry): Promise<void> => {
86
+ const file = this._directory.getOrCreateFile(entry);
87
+ await file.destroy();
88
+ }),
89
+ );
90
+ }
91
+
92
+ async close(): Promise<void> {
93
+ this._state = 'closed';
94
+ }
95
+
96
+ private _getFilename(key: StorageKey): string {
97
+ return key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-');
98
+ }
99
+
100
+ private _getKeyFromFilename(filename: string): StorageKey {
101
+ return filename.split('-').map((k) => k.replaceAll('%2D', '-').replaceAll('%25', '%'));
102
+ }
103
+ }
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type StorageKey, type Chunk, type StorageAdapterInterface } from '@dxos/automerge/automerge-repo';
6
+ import { type MaybePromise } from '@dxos/util';
7
+
8
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
9
+
10
+ export type StorageCallbacks = {
11
+ beforeSave?: (path: string[]) => MaybePromise<void>;
12
+ afterSave?: (path: string[]) => MaybePromise<void>;
13
+ };
14
+
15
+ export type AutomergeStorageWrapperParams = {
16
+ storage: StorageAdapterInterface;
17
+ callbacks: StorageCallbacks;
18
+ };
19
+
20
+ /**
21
+ * Wrapper for automerge storage that adds callback on save.
22
+ */
23
+ export class AutomergeStorageWrapper implements StorageAdapterInterface {
24
+ private readonly _storage: StorageAdapterInterface;
25
+ private readonly _callbacks: StorageCallbacks;
26
+
27
+ constructor({ storage, callbacks }: AutomergeStorageWrapperParams) {
28
+ this._storage = storage;
29
+ this._callbacks = callbacks;
30
+ }
31
+
32
+ async load(key: StorageKey): Promise<Uint8Array | undefined> {
33
+ return this._storage.load(key);
34
+ }
35
+
36
+ async save(key: StorageKey, value: Uint8Array): Promise<void> {
37
+ await this._callbacks.beforeSave?.(key);
38
+ await this._storage.save(key, value);
39
+ await this._callbacks.afterSave?.(key);
40
+ }
41
+
42
+ async remove(key: StorageKey): Promise<void> {
43
+ return this._storage.remove(key);
44
+ }
45
+
46
+ async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
47
+ return this._storage.loadRange(keyPrefix);
48
+ }
49
+
50
+ async removeRange(keyPrefix: StorageKey): Promise<void> {
51
+ return this._storage.removeRange(keyPrefix);
52
+ }
53
+
54
+ async close() {
55
+ if (this._storage instanceof AutomergeStorageAdapter) {
56
+ return this._storage.close();
57
+ }
58
+ }
59
+ }
@@ -93,7 +93,6 @@ export class EchoNetworkAdapter extends NetworkAdapter {
93
93
  peerId: this.peerId,
94
94
  onConnectionOpen: this._onConnectionOpen.bind(this),
95
95
  onConnectionClosed: this._onConnectionClosed.bind(this),
96
- onConnectionAuthScopeChanged: this._onConnectionAuthScopeChanged.bind(this),
97
96
  getContainingSpaceForDocument: this._params.getContainingSpaceForDocument,
98
97
  });
99
98
  }
@@ -102,6 +101,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
102
101
  async removeReplicator(replicator: EchoReplicator) {
103
102
  invariant(this._lifecycleState === LifecycleState.OPEN);
104
103
  invariant(this._replicators.has(replicator));
104
+ ('');
105
105
  await replicator.disconnect();
106
106
  this._replicators.delete(replicator);
107
107
  }
@@ -142,19 +142,13 @@ export class EchoNetworkAdapter extends NetworkAdapter {
142
142
  });
143
143
 
144
144
  log('emit peer-candidate', { peerId: connection.peerId });
145
- this._emitPeerCandidate(connection);
146
- }
147
-
148
- /**
149
- * Trigger doc-synchronizer shared documents set recalculation. Happens on peer-candidate.
150
- * TODO(y): replace with a proper API call when sharePolicy update becomes supported by automerge-repo
151
- */
152
- private _onConnectionAuthScopeChanged(connection: ReplicatorConnection) {
153
- log('Connection auth scope changed', { peerId: connection.peerId });
154
- const entry = this._connections.get(connection.peerId as PeerId);
155
- invariant(entry);
156
- this.emit('peer-disconnected', { peerId: connection.peerId as PeerId });
157
- this._emitPeerCandidate(connection);
145
+ this.emit('peer-candidate', {
146
+ peerId: connection.peerId as PeerId,
147
+ peerMetadata: {
148
+ // TODO(dmaretskyi): Refactor this.
149
+ dxos_peerSource: 'EchoNetworkAdapter',
150
+ } as any,
151
+ });
158
152
  }
159
153
 
160
154
  private _onConnectionClosed(connection: ReplicatorConnection) {
@@ -170,16 +164,6 @@ export class EchoNetworkAdapter extends NetworkAdapter {
170
164
 
171
165
  this._connections.delete(connection.peerId as PeerId);
172
166
  }
173
-
174
- private _emitPeerCandidate(connection: ReplicatorConnection) {
175
- this.emit('peer-candidate', {
176
- peerId: connection.peerId as PeerId,
177
- peerMetadata: {
178
- // TODO(dmaretskyi): Refactor this.
179
- dxos_peerSource: 'EchoNetworkAdapter',
180
- } as any,
181
- });
182
- }
183
167
  }
184
168
 
185
169
  type ConnectionEntry = {
@@ -27,8 +27,6 @@ export interface EchoReplicatorContext {
27
27
 
28
28
  onConnectionClosed(connection: ReplicatorConnection): void;
29
29
 
30
- onConnectionAuthScopeChanged(connection: ReplicatorConnection): void;
31
-
32
30
  getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
33
31
  }
34
32
 
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './automerge-host';
6
+ export * from './automerge-storage-adapter';
6
7
  export * from './automerge-doc-loader';
7
8
  export * from './leveldb-storage-adapter';
8
9
  export * from './local-host-network-adapter';
@@ -59,9 +59,7 @@ export class MeshEchoReplicator implements EchoReplicator {
59
59
  log('onRemoteConnected', { peerId: connection.peerId });
60
60
  invariant(this._context);
61
61
 
62
- if (this._connectionsPerPeer.has(connection.peerId)) {
63
- this._context.onConnectionAuthScopeChanged(connection);
64
- } else {
62
+ if (!this._connectionsPerPeer.has(connection.peerId)) {
65
63
  this._connectionsPerPeer.set(connection.peerId, connection);
66
64
  await connection.enable();
67
65
  this._context.onConnectionOpen(connection);
@@ -0,0 +1,42 @@
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 { type SublevelDB } from '@dxos/kv-store';
8
+ import { log } from '@dxos/log';
9
+ import { StorageType, type Directory } from '@dxos/random-access-storage';
10
+
11
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
12
+ import { encodingOptions } from './leveldb-storage-adapter';
13
+
14
+ export const levelMigration = async ({ db, directory }: { db: SublevelDB; directory: Directory }) => {
15
+ // Note: Make auto-migration 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
+
36
+ const batch = db.batch();
37
+ log.info('found chunks on old storage adapter', { chunks: chunks.length });
38
+ for (const { key, data } of await oldStorageAdapter.loadRange([])) {
39
+ data && batch.put<StorageKey, Uint8Array>(key, data, { ...encodingOptions });
40
+ }
41
+ await batch.write();
42
+ };
@@ -4,117 +4,153 @@
4
4
 
5
5
  import { expect } from 'chai';
6
6
 
7
+ import { type StorageAdapterInterface } from '@dxos/automerge/automerge-repo';
7
8
  import { randomBytes } from '@dxos/crypto';
8
9
  import { PublicKey } from '@dxos/keys';
9
10
  import { createTestLevel } from '@dxos/kv-store/testing';
11
+ import { StorageType, createStorage } from '@dxos/random-access-storage';
10
12
  import { afterTest, describe, test } from '@dxos/test';
11
- import { arrayToBuffer, bufferToArray } from '@dxos/util';
13
+ import { arrayToBuffer, bufferToArray, type MaybePromise } from '@dxos/util';
12
14
 
15
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
13
16
  import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
14
17
 
15
- describe('LevelDBStorageAdapter', () => {
16
- const createAdapter = async (root?: string) => {
17
- const level = createTestLevel(root);
18
- await level.open();
19
- const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
20
- await adapter.open?.();
21
-
22
- const close = async () => {
23
- await adapter.close();
24
- await level.close();
25
- };
26
- afterTest(close);
27
- return {
28
- adapter,
29
- close,
30
- };
31
- };
32
-
33
- const chunks = [
34
- { key: ['a', 'b', 'c', '1'], data: PublicKey.random().asUint8Array() },
35
- { key: ['a', 'b', 'c', '2'], data: PublicKey.random().asUint8Array() },
36
- { key: ['a', 'b', 'd', '3'], data: PublicKey.random().asUint8Array() },
37
- { key: ['a', 'b', 'd', '4'], data: PublicKey.random().asUint8Array() },
38
- ];
39
-
40
- test('should store and retrieve data', async () => {
41
- const { adapter } = await createAdapter();
42
-
43
- await adapter.save(chunks[0].key, chunks[0].data);
44
- expect(await adapter.load(chunks[0].key)).to.deep.equal(chunks[0].data);
45
- });
46
-
47
- test('loadRange return inputs with correct prefixes', async () => {
48
- const { adapter } = await createAdapter();
49
-
50
- for (const chunk of chunks) {
51
- await adapter.save(chunk.key, chunk.data);
52
- }
53
-
54
- expect((await adapter.loadRange(['a', 'b'])).length).to.equal(4);
55
- expect((await adapter.loadRange(['a', 'b', 'c']))[0]).to.deep.equal(chunks[0]);
56
- expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(2);
57
- });
58
-
59
- test('deletion works', async () => {
60
- const { adapter } = await createAdapter();
61
-
62
- for (const chunk of chunks) {
63
- await adapter.save(chunk.key, chunk.data);
64
- }
65
-
66
- await adapter.remove(['a', 'b', 'c', '1']);
67
-
68
- expect((await adapter.loadRange(['a', 'b'])).length).to.equal(3);
69
- expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(1);
70
-
71
- await adapter.removeRange(['a', 'b', 'd']);
72
-
73
- expect((await adapter.loadRange(['a', 'b'])).length).to.equal(1);
74
- expect((await adapter.loadRange(['a', 'b']))[0]).to.deep.equal(chunks[1]);
75
- expect(await adapter.load(['a', 'b', 'c', '2'])).to.deep.equal(chunks[1].data);
76
- expect(await adapter.load(['a', 'b', 'd', '3'])).to.be.undefined;
18
+ const runTests = (
19
+ testNamespace: string,
20
+ /** Run per test. Expects automatic clean-up with `afterTest`. */
21
+ createAdapter: (root?: string) => MaybePromise<{
22
+ adapter: StorageAdapterInterface;
23
+ /** Would be called automatically with `afterTest`. Exposed for mid-test clean-up. */
24
+ close: () => MaybePromise<void>;
25
+ }>,
26
+ ) => {
27
+ describe(testNamespace, () => {
28
+ const chunks = [
29
+ { key: ['a', 'b', 'c', '1'], data: PublicKey.random().asUint8Array() },
30
+ { key: ['a', 'b', 'c', '2'], data: PublicKey.random().asUint8Array() },
31
+ { key: ['a', 'b', 'd', '3'], data: PublicKey.random().asUint8Array() },
32
+ { key: ['a', 'b', 'd', '4'], data: PublicKey.random().asUint8Array() },
33
+ ];
34
+
35
+ test('should store and retrieve data', async () => {
36
+ const { adapter } = await createAdapter();
37
+
38
+ await adapter.save(chunks[0].key, chunks[0].data);
39
+ expect(await adapter.load(chunks[0].key)).to.deep.equal(chunks[0].data);
40
+ });
41
+
42
+ test('loadRange return inputs with correct prefixes', async () => {
43
+ const { adapter } = await createAdapter();
44
+
45
+ for (const chunk of chunks) {
46
+ await adapter.save(chunk.key, chunk.data);
47
+ }
48
+
49
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(4);
50
+ expect((await adapter.loadRange(['a', 'b', 'c']))[0]).to.deep.equal(chunks[0]);
51
+ expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(2);
52
+ });
53
+
54
+ test('deletion works', async () => {
55
+ const { adapter } = await createAdapter();
56
+
57
+ for (const chunk of chunks) {
58
+ await adapter.save(chunk.key, chunk.data);
59
+ }
60
+
61
+ await adapter.remove(['a', 'b', 'c', '1']);
62
+
63
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(3);
64
+ expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(1);
65
+
66
+ await adapter.removeRange(['a', 'b', 'd']);
67
+
68
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(1);
69
+ expect((await adapter.loadRange(['a', 'b']))[0]).to.deep.equal(chunks[1]);
70
+ expect(await adapter.load(['a', 'b', 'c', '2'])).to.deep.equal(chunks[1].data);
71
+ expect(await adapter.load(['a', 'b', 'd', '3'])).to.be.undefined;
72
+ });
73
+
74
+ test('loadRange', async () => {
75
+ const root = `/tmp/${randomBytes(16).toString('hex')}`;
76
+ {
77
+ const { adapter, close } = await createAdapter(root);
78
+ await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
79
+ await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
80
+ await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
81
+ await close();
82
+ }
83
+
84
+ {
85
+ const { adapter } = await createAdapter(root);
86
+ const range = await adapter.loadRange(['test']);
87
+ expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).to.deep.eq(['one', 'two']);
88
+ expect(range.map((chunk) => chunk.key)).to.deep.eq([
89
+ ['test', '1'],
90
+ ['test', '2'],
91
+ ]);
92
+ }
93
+ });
94
+
95
+ test('removeRange', async () => {
96
+ const root = `/tmp/${randomBytes(16).toString('hex')}`;
97
+ {
98
+ const { adapter, close } = await createAdapter(root);
99
+ await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
100
+ await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
101
+ await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
102
+ await close();
103
+ }
104
+
105
+ {
106
+ const { adapter } = await createAdapter(root);
107
+ await adapter.removeRange(['test']);
108
+ const range = await adapter.loadRange(['test']);
109
+ expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).to.deep.eq([]);
110
+ const range2 = await adapter.loadRange(['bar']);
111
+ expect(range2.map((chunk) => arrayToBuffer(chunk.data!).toString())).to.deep.eq(['bar']);
112
+ expect(range2.map((chunk) => chunk.key)).to.deep.eq([['bar', '1']]);
113
+ }
114
+ });
77
115
  });
116
+ };
117
+
118
+ /**
119
+ * Run tests for AutomergeStorageAdapter.
120
+ */
121
+ runTests('AutomergeStorageAdapter', (root?: string) => {
122
+ const storage = createStorage({ type: root ? StorageType.NODE : StorageType.RAM, root });
123
+ const dir = storage.createDirectory('automerge');
124
+ const adapter = new AutomergeStorageAdapter(dir);
125
+
126
+ const close = async () => {
127
+ await adapter.close();
128
+ await storage.close();
129
+ };
130
+ afterTest(close);
78
131
 
79
- test('loadRange', async () => {
80
- const root = `/tmp/${randomBytes(16).toString('hex')}`;
81
- {
82
- const { adapter, close } = await createAdapter(root);
83
- await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
84
- await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
85
- await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
86
- await close();
87
- }
88
-
89
- {
90
- const { adapter } = await createAdapter(root);
91
- const range = await adapter.loadRange(['test']);
92
- expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).to.deep.eq(['one', 'two']);
93
- expect(range.map((chunk) => chunk.key)).to.deep.eq([
94
- ['test', '1'],
95
- ['test', '2'],
96
- ]);
97
- }
98
- });
132
+ return {
133
+ adapter,
134
+ close,
135
+ };
136
+ });
99
137
 
100
- test('removeRange', async () => {
101
- const root = `/tmp/${randomBytes(16).toString('hex')}`;
102
- {
103
- const { adapter, close } = await createAdapter(root);
104
- await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
105
- await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
106
- await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
107
- await close();
108
- }
109
-
110
- {
111
- const { adapter } = await createAdapter(root);
112
- await adapter.removeRange(['test']);
113
- const range = await adapter.loadRange(['test']);
114
- expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).to.deep.eq([]);
115
- const range2 = await adapter.loadRange(['bar']);
116
- expect(range2.map((chunk) => arrayToBuffer(chunk.data!).toString())).to.deep.eq(['bar']);
117
- expect(range2.map((chunk) => chunk.key)).to.deep.eq([['bar', '1']]);
118
- }
119
- });
138
+ /**
139
+ * Run tests for LevelDBStorageAdapter.
140
+ */
141
+ runTests('LevelDBStorageAdapter', async (root?: string) => {
142
+ const level = createTestLevel(root);
143
+ await level.open();
144
+ const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
145
+ await adapter.open?.();
146
+
147
+ const close = async () => {
148
+ await adapter.close();
149
+ await level.close();
150
+ };
151
+ afterTest(close);
152
+ return {
153
+ adapter,
154
+ close,
155
+ };
120
156
  });