@dxos/echo-pipeline 0.4.10-main.c32f430 → 0.4.10-main.c5e8686
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 +344 -246
- 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 +367 -270
- 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 +21 -18
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-repo.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -0
- package/dist/types/src/automerge/index.d.ts +2 -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 +30 -0
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -0
- package/dist/types/src/automerge/local-host-network-adapter.d.ts +8 -1
- package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -1
- 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 +8 -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 +22 -8
- package/src/automerge/automerge-host.ts +65 -118
- package/src/automerge/automerge-repo.test.ts +29 -0
- package/src/automerge/index.ts +2 -0
- package/src/automerge/level.test.ts +64 -0
- package/src/automerge/leveldb-storage-adapter.ts +117 -0
- package/src/automerge/local-host-network-adapter.ts +19 -13
- 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 +8 -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.c5e8686",
|
|
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/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/keys": "0.4.10-main.
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/teleport": "0.4.10-main.
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/teleport-extension-replicator": "0.4.10-main.
|
|
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.c5e8686",
|
|
43
|
+
"@dxos/codec-protobuf": "0.4.10-main.c5e8686",
|
|
44
|
+
"@dxos/context": "0.4.10-main.c5e8686",
|
|
45
|
+
"@dxos/echo-db": "0.4.10-main.c5e8686",
|
|
46
|
+
"@dxos/crypto": "0.4.10-main.c5e8686",
|
|
47
|
+
"@dxos/credentials": "0.4.10-main.c5e8686",
|
|
48
|
+
"@dxos/debug": "0.4.10-main.c5e8686",
|
|
49
|
+
"@dxos/automerge": "0.4.10-main.c5e8686",
|
|
50
|
+
"@dxos/feed-store": "0.4.10-main.c5e8686",
|
|
51
|
+
"@dxos/hypercore": "0.4.10-main.c5e8686",
|
|
52
|
+
"@dxos/keys": "0.4.10-main.c5e8686",
|
|
53
|
+
"@dxos/invariant": "0.4.10-main.c5e8686",
|
|
54
|
+
"@dxos/log": "0.4.10-main.c5e8686",
|
|
55
|
+
"@dxos/keyring": "0.4.10-main.c5e8686",
|
|
56
|
+
"@dxos/messaging": "0.4.10-main.c5e8686",
|
|
57
|
+
"@dxos/network-manager": "0.4.10-main.c5e8686",
|
|
58
|
+
"@dxos/node-std": "0.4.10-main.c5e8686",
|
|
59
|
+
"@dxos/protocols": "0.4.10-main.c5e8686",
|
|
60
|
+
"@dxos/teleport": "0.4.10-main.c5e8686",
|
|
61
|
+
"@dxos/random-access-storage": "0.4.10-main.c5e8686",
|
|
62
|
+
"@dxos/rpc": "0.4.10-main.c5e8686",
|
|
63
|
+
"@dxos/teleport-extension-automerge-replicator": "0.4.10-main.c5e8686",
|
|
64
|
+
"@dxos/teleport-extension-gossip": "0.4.10-main.c5e8686",
|
|
65
|
+
"@dxos/teleport-extension-object-sync": "0.4.10-main.c5e8686",
|
|
66
|
+
"@dxos/teleport-extension-replicator": "0.4.10-main.c5e8686",
|
|
67
|
+
"@dxos/tracing": "0.4.10-main.c5e8686",
|
|
68
|
+
"@dxos/timeframe": "0.4.10-main.c5e8686",
|
|
69
|
+
"@dxos/util": "0.4.10-main.c5e8686",
|
|
70
|
+
"@dxos/typings": "0.4.10-main.c5e8686"
|
|
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,35 +25,50 @@ 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) => {
|
|
35
43
|
doc.text = 'Hello world';
|
|
36
44
|
});
|
|
45
|
+
await host.repo.flush();
|
|
37
46
|
expect(handle.docSync().text).toEqual('Hello world');
|
|
38
47
|
});
|
|
39
48
|
|
|
40
49
|
test('changes are preserved in storage', async () => {
|
|
41
|
-
const
|
|
50
|
+
const level = createTestLevel();
|
|
51
|
+
await level.open();
|
|
52
|
+
afterTest(() => level.close());
|
|
42
53
|
|
|
43
|
-
const host = new AutomergeHost({
|
|
54
|
+
const host = new AutomergeHost({ db: level.sublevel('automerge') });
|
|
55
|
+
await host.open();
|
|
44
56
|
const handle = host.repo.create();
|
|
45
57
|
handle.change((doc: any) => {
|
|
46
58
|
doc.text = 'Hello world';
|
|
47
59
|
});
|
|
48
60
|
const url = handle.url;
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
await
|
|
62
|
+
await host.repo.flush();
|
|
63
|
+
await host.close();
|
|
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');
|
|
71
|
+
await host2.repo.flush();
|
|
57
72
|
});
|
|
58
73
|
|
|
59
74
|
test('basic networking', async () => {
|
|
@@ -168,7 +183,6 @@ describe('AutomergeHost', () => {
|
|
|
168
183
|
const secondHandle = host.create();
|
|
169
184
|
secondHandle.change((doc: any) => (doc.text = 'Hello world'));
|
|
170
185
|
await host.find(secondHandle.url).whenReady();
|
|
171
|
-
// await sleep(100);
|
|
172
186
|
allowedDocs.push(secondHandle.documentId);
|
|
173
187
|
|
|
174
188
|
{
|
|
@@ -2,78 +2,81 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
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 {
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
46
|
-
private readonly
|
|
47
|
-
private readonly
|
|
48
|
-
|
|
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
|
|
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,
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
62
|
+
constructor({ directory, db, storageCallbacks }: AutomergeHostParams) {
|
|
63
|
+
this._directory = directory;
|
|
64
|
+
this._db = db;
|
|
65
|
+
this._storageCallbacks = storageCallbacks;
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this._ctx.
|
|
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
|
-
|
|
165
|
-
|
|
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()
|
|
201
|
-
mapValues(handle.docSync()
|
|
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
|
+
});
|
package/src/automerge/index.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export * from './automerge-host';
|
|
6
6
|
export * from './automerge-storage-adapter';
|
|
7
7
|
export * from './automerge-doc-loader';
|
|
8
|
+
export * from './leveldb-storage-adapter';
|
|
8
9
|
export * from './local-host-network-adapter';
|
|
9
10
|
export * from './mesh-network-adapter';
|
|
10
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';
|