@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.
- package/dist/lib/browser/{chunk-RTEEJ723.mjs → chunk-SYE4EK33.mjs} +30 -35
- package/dist/lib/browser/chunk-SYE4EK33.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +279 -159
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +8 -2
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/{chunk-7VZVCCNF.cjs → chunk-WCTX6RNS.cjs} +35 -40
- package/dist/lib/node/chunk-WCTX6RNS.cjs.map +7 -0
- package/dist/lib/node/index.cjs +285 -168
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +18 -13
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/types/src/automerge/automerge-doc-loader.d.ts +2 -0
- package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-host.d.ts +20 -10
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +1 -0
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/level.test.d.ts +2 -0
- package/dist/types/src/automerge/level.test.d.ts.map +1 -0
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +27 -0
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -0
- package/dist/types/src/automerge/migrations.d.ts +7 -0
- package/dist/types/src/automerge/migrations.d.ts.map +1 -0
- package/dist/types/src/automerge/reference.d.ts +15 -0
- package/dist/types/src/automerge/reference.d.ts.map +1 -0
- package/dist/types/src/automerge/storage-adapter.test.d.ts +2 -0
- package/dist/types/src/automerge/storage-adapter.test.d.ts.map +1 -0
- package/dist/types/src/automerge/types.d.ts +7 -2
- package/dist/types/src/automerge/types.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +2 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +4 -8
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/level.d.ts +3 -0
- package/dist/types/src/testing/level.d.ts.map +1 -0
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/package.json +33 -30
- package/src/automerge/automerge-doc-loader.ts +6 -0
- package/src/automerge/automerge-host.test.ts +19 -5
- package/src/automerge/automerge-host.ts +52 -33
- package/src/automerge/index.ts +1 -0
- package/src/automerge/level.test.ts +43 -0
- package/src/automerge/leveldb-storage-adapter.ts +105 -0
- package/src/automerge/migrations.ts +41 -0
- package/src/automerge/reference.ts +31 -0
- package/src/automerge/storage-adapter.test.ts +90 -0
- package/src/automerge/types.ts +7 -5
- package/src/db-host/data-service.ts +1 -1
- package/src/metadata/metadata-store.ts +17 -8
- package/src/space/space.test.ts +7 -7
- package/src/space/space.ts +6 -21
- package/src/testing/index.ts +1 -0
- package/src/testing/level.ts +11 -0
- package/dist/lib/browser/chunk-RTEEJ723.mjs.map +0 -7
- 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.
|
|
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
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/codec-protobuf": "0.4.10-main.
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/credentials": "0.4.10-main.
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/teleport
|
|
62
|
-
"@dxos/teleport-extension-
|
|
63
|
-
"@dxos/teleport-extension-
|
|
64
|
-
"@dxos/
|
|
65
|
-
"@dxos/
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
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
|
|
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
|
|
49
|
+
const level = createTestLevel();
|
|
50
|
+
await level.open();
|
|
51
|
+
afterTest(() => level.close());
|
|
42
52
|
|
|
43
|
-
const host = new AutomergeHost({
|
|
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({
|
|
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 {
|
|
21
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
46
|
-
private readonly
|
|
47
|
-
private readonly
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
80
|
+
}
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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()
|
|
201
|
-
mapValues(handle.docSync()
|
|
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
|
}
|
package/src/automerge/index.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/automerge/types.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
5
|
+
import { type AbstractSublevel } from 'abstract-level';
|
|
6
|
+
import { type Level } from 'level';
|
|
8
7
|
|
|
9
|
-
import { type
|
|
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?:
|
|
81
|
+
type?: EncodedReferenceObject;
|
|
83
82
|
};
|
|
83
|
+
|
|
84
|
+
export type MyLevel = Level<string, string>;
|
|
85
|
+
export type MySublevel = AbstractSublevel<any, string | Buffer | Uint8Array, string, string>;
|