@dxos/echo-pipeline 0.4.9 → 0.4.10-main.0be5154
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/index.mjs +278 -0
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +277 -0
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/automerge/automerge-doc-loader.d.ts +64 -0
- package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-doc-loader.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-doc-loader.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/types.d.ts +67 -0
- package/dist/types/src/automerge/types.d.ts.map +1 -0
- package/package.json +30 -30
- package/src/automerge/automerge-doc-loader.test.ts +97 -0
- package/src/automerge/automerge-doc-loader.ts +235 -0
- package/src/automerge/index.ts +2 -0
- package/src/automerge/types.ts +83 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type Reference } from '@dxos/echo-db';
|
|
2
|
+
export type SpaceState = {
|
|
3
|
+
rootUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
export interface SpaceDoc {
|
|
6
|
+
access?: {
|
|
7
|
+
spaceKey: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Objects inlined in the current document.
|
|
11
|
+
*/
|
|
12
|
+
objects?: {
|
|
13
|
+
[key: string]: ObjectStructure;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Object id points to an automerge doc url where the object is embedded.
|
|
17
|
+
*/
|
|
18
|
+
links?: {
|
|
19
|
+
[echoId: string]: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Representation of an ECHO object in an AM document.
|
|
24
|
+
*/
|
|
25
|
+
export type ObjectStructure = {
|
|
26
|
+
data: Record<string, any>;
|
|
27
|
+
meta: ObjectMeta;
|
|
28
|
+
system: ObjectSystem;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Echo object metadata.
|
|
32
|
+
*/
|
|
33
|
+
export type ObjectMeta = {
|
|
34
|
+
/**
|
|
35
|
+
* Foreign keys.
|
|
36
|
+
*/
|
|
37
|
+
keys: ForeignKey[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Reference to an object in a foreign database.
|
|
41
|
+
*/
|
|
42
|
+
export type ForeignKey = {
|
|
43
|
+
/**
|
|
44
|
+
* Name of the foreign database/system.
|
|
45
|
+
* E.g. `github.com`.
|
|
46
|
+
*/
|
|
47
|
+
source?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Id within the foreign database.
|
|
50
|
+
*/
|
|
51
|
+
id?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Automerge object system properties.
|
|
55
|
+
* (Is automerge specific.)
|
|
56
|
+
*/
|
|
57
|
+
export type ObjectSystem = {
|
|
58
|
+
/**
|
|
59
|
+
* Deletion marker.
|
|
60
|
+
*/
|
|
61
|
+
deleted?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Object reference ('protobuf' protocol) type.
|
|
64
|
+
*/
|
|
65
|
+
type?: Reference;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/automerge/types.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG;IAEvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;KAChC,CAAC;IACF;;OAEG;IACH,KAAK,CAAC,EAAE;QACN,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;KAC1B,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;OAEG;IACH,IAAI,EAAE,UAAU,EAAE,CAAC;CACpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-pipeline",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10-main.0be5154",
|
|
4
4
|
"description": "ECHO database.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -36,35 +36,35 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"crc-32": "^1.2.2",
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/codec-protobuf": "0.4.
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/crypto": "0.4.
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/feed-store": "0.4.
|
|
48
|
-
"@dxos/hypercore": "0.4.
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/network-manager": "0.4.
|
|
55
|
-
"@dxos/node-std": "0.4.
|
|
56
|
-
"@dxos/protocols": "0.4.
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/teleport-extension-automerge-replicator": "0.4.
|
|
61
|
-
"@dxos/teleport-extension-gossip": "0.4.
|
|
62
|
-
"@dxos/teleport-extension-
|
|
63
|
-
"@dxos/
|
|
64
|
-
"@dxos/
|
|
65
|
-
"@dxos/tracing": "0.4.
|
|
66
|
-
"@dxos/typings": "0.4.
|
|
67
|
-
"@dxos/util": "0.4.
|
|
39
|
+
"@dxos/async": "0.4.10-main.0be5154",
|
|
40
|
+
"@dxos/codec-protobuf": "0.4.10-main.0be5154",
|
|
41
|
+
"@dxos/context": "0.4.10-main.0be5154",
|
|
42
|
+
"@dxos/credentials": "0.4.10-main.0be5154",
|
|
43
|
+
"@dxos/automerge": "0.4.10-main.0be5154",
|
|
44
|
+
"@dxos/echo-db": "0.4.10-main.0be5154",
|
|
45
|
+
"@dxos/crypto": "0.4.10-main.0be5154",
|
|
46
|
+
"@dxos/debug": "0.4.10-main.0be5154",
|
|
47
|
+
"@dxos/feed-store": "0.4.10-main.0be5154",
|
|
48
|
+
"@dxos/hypercore": "0.4.10-main.0be5154",
|
|
49
|
+
"@dxos/invariant": "0.4.10-main.0be5154",
|
|
50
|
+
"@dxos/keys": "0.4.10-main.0be5154",
|
|
51
|
+
"@dxos/keyring": "0.4.10-main.0be5154",
|
|
52
|
+
"@dxos/messaging": "0.4.10-main.0be5154",
|
|
53
|
+
"@dxos/log": "0.4.10-main.0be5154",
|
|
54
|
+
"@dxos/network-manager": "0.4.10-main.0be5154",
|
|
55
|
+
"@dxos/node-std": "0.4.10-main.0be5154",
|
|
56
|
+
"@dxos/protocols": "0.4.10-main.0be5154",
|
|
57
|
+
"@dxos/teleport": "0.4.10-main.0be5154",
|
|
58
|
+
"@dxos/rpc": "0.4.10-main.0be5154",
|
|
59
|
+
"@dxos/random-access-storage": "0.4.10-main.0be5154",
|
|
60
|
+
"@dxos/teleport-extension-automerge-replicator": "0.4.10-main.0be5154",
|
|
61
|
+
"@dxos/teleport-extension-gossip": "0.4.10-main.0be5154",
|
|
62
|
+
"@dxos/teleport-extension-replicator": "0.4.10-main.0be5154",
|
|
63
|
+
"@dxos/teleport-extension-object-sync": "0.4.10-main.0be5154",
|
|
64
|
+
"@dxos/timeframe": "0.4.10-main.0be5154",
|
|
65
|
+
"@dxos/tracing": "0.4.10-main.0be5154",
|
|
66
|
+
"@dxos/typings": "0.4.10-main.0be5154",
|
|
67
|
+
"@dxos/util": "0.4.10-main.0be5154"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"fast-check": "^3.15.1",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { sleep } from '@dxos/async';
|
|
8
|
+
import { Repo } from '@dxos/automerge/automerge-repo';
|
|
9
|
+
import { Context } from '@dxos/context';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { describe, test } from '@dxos/test';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type AutomergeDocumentLoader,
|
|
15
|
+
AutomergeDocumentLoaderImpl,
|
|
16
|
+
type ObjectDocumentLoaded,
|
|
17
|
+
} from './automerge-doc-loader';
|
|
18
|
+
import { type SpaceDoc } from './types';
|
|
19
|
+
|
|
20
|
+
const ctx = new Context();
|
|
21
|
+
const SPACE_KEY = PublicKey.random();
|
|
22
|
+
const randomId = () => PublicKey.random().toHex();
|
|
23
|
+
describe('AutomergeDocumentLoader', () => {
|
|
24
|
+
test('space access is set on root doc handle and it is accessible', async () => {
|
|
25
|
+
const { loader, spaceRootDocHandle } = await setupTest();
|
|
26
|
+
expect(loader.getSpaceRootDocHandle()).not.to.throw;
|
|
27
|
+
expect(spaceRootDocHandle.docSync()?.access?.spaceKey).to.eq(SPACE_KEY.toHex());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('new object document is linked with space and root document', async () => {
|
|
31
|
+
const objectId = randomId();
|
|
32
|
+
const { loader, spaceRootDocHandle } = await setupTest();
|
|
33
|
+
const objectDocHandle = loader.createDocumentForObject(objectId);
|
|
34
|
+
const handle = spaceRootDocHandle.docSync();
|
|
35
|
+
expect(objectDocHandle.docSync()?.access?.spaceKey).to.eq(SPACE_KEY.toHex());
|
|
36
|
+
expect(handle?.links[objectId]).to.eq(objectDocHandle.url);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('listener is invoked after a document is loaded', async () => {
|
|
40
|
+
const objectId = randomId();
|
|
41
|
+
const { loader, repo } = await setupTest();
|
|
42
|
+
const handle = repo.create<SpaceDoc>();
|
|
43
|
+
const docLoadInfo = waitForDocumentLoad(loader, { objectId, handle });
|
|
44
|
+
loadLinkedObjects(loader, { [objectId]: handle.url });
|
|
45
|
+
await sleep(10);
|
|
46
|
+
expect(docLoadInfo.loaded).to.be.true;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('listener is not invoked if an object was rebound during document loading', async () => {
|
|
50
|
+
const objectId = randomId();
|
|
51
|
+
const { loader, repo } = await setupTest();
|
|
52
|
+
const oldDocHandle = repo.create<SpaceDoc>();
|
|
53
|
+
const newDocHandle = repo.create<SpaceDoc>();
|
|
54
|
+
const docLoadInfo = waitForDocumentLoad(loader, { objectId, handle: oldDocHandle });
|
|
55
|
+
loadLinkedObjects(loader, { [objectId]: oldDocHandle.url });
|
|
56
|
+
loader.onObjectBoundToDocument(newDocHandle, objectId);
|
|
57
|
+
await sleep(10);
|
|
58
|
+
expect(docLoadInfo.loaded).to.be.false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('document link is not loaded if object exists as inline object', async () => {
|
|
62
|
+
const objectId = randomId();
|
|
63
|
+
const { loader, repo } = await setupTest();
|
|
64
|
+
const existingHandle = repo.create<SpaceDoc>();
|
|
65
|
+
loader.onObjectBoundToDocument(existingHandle, objectId);
|
|
66
|
+
const newDocHandle = repo.create<SpaceDoc>();
|
|
67
|
+
const docLoadInfo = waitForDocumentLoad(loader, { objectId, handle: newDocHandle });
|
|
68
|
+
loadLinkedObjects(loader, { [objectId]: existingHandle.url });
|
|
69
|
+
await sleep(10);
|
|
70
|
+
expect(docLoadInfo.loaded).to.be.false;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const setupTest = async () => {
|
|
74
|
+
const repo = new Repo({ network: [] });
|
|
75
|
+
const loader = new AutomergeDocumentLoaderImpl(SPACE_KEY, repo);
|
|
76
|
+
const spaceRootDocHandle = repo.create<SpaceDoc>();
|
|
77
|
+
await loader.loadSpaceRootDocHandle(ctx, {
|
|
78
|
+
rootUrl: spaceRootDocHandle.url,
|
|
79
|
+
});
|
|
80
|
+
return { loader, spaceRootDocHandle, repo };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const loadLinkedObjects = (loader: AutomergeDocumentLoader, links: SpaceDoc['links']) => {
|
|
84
|
+
Object.keys(links ?? {}).forEach((objectId) => loader.loadObjectDocument(objectId));
|
|
85
|
+
loader.onObjectLinksUpdated(links);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const waitForDocumentLoad = (loader: AutomergeDocumentLoader, expected: ObjectDocumentLoaded) => {
|
|
89
|
+
const docLoadInfo = { loaded: false };
|
|
90
|
+
loader.onObjectDocumentLoaded.on((data) => {
|
|
91
|
+
expect(data.objectId).to.eq(expected.objectId);
|
|
92
|
+
expect(data.handle.url).to.eq(expected.handle.url);
|
|
93
|
+
docLoadInfo.loaded = true;
|
|
94
|
+
});
|
|
95
|
+
return docLoadInfo;
|
|
96
|
+
};
|
|
97
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Event } from '@dxos/async';
|
|
6
|
+
import { type DocHandle, type AutomergeUrl, type DocumentId, type Repo } from '@dxos/automerge/automerge-repo';
|
|
7
|
+
import { cancelWithContext, type Context } from '@dxos/context';
|
|
8
|
+
import { warnAfterTimeout } from '@dxos/debug';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { type PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
|
|
13
|
+
import { type SpaceState, type SpaceDoc } from './types';
|
|
14
|
+
|
|
15
|
+
type SpaceDocumentLinks = SpaceDoc['links'];
|
|
16
|
+
|
|
17
|
+
export interface AutomergeDocumentLoader {
|
|
18
|
+
onObjectDocumentLoaded: Event<ObjectDocumentLoaded>;
|
|
19
|
+
|
|
20
|
+
loadSpaceRootDocHandle(ctx: Context, spaceState: SpaceState): Promise<void>;
|
|
21
|
+
loadObjectDocument(objectId: string): void;
|
|
22
|
+
getSpaceRootDocHandle(): DocHandle<SpaceDoc>;
|
|
23
|
+
createDocumentForObject(objectId: string): DocHandle<SpaceDoc>;
|
|
24
|
+
onObjectLinksUpdated(links: SpaceDocumentLinks): void;
|
|
25
|
+
onObjectBoundToDocument(handle: DocHandle<SpaceDoc>, objectId: string): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns objectIds for which we had document handles or were loading one.
|
|
29
|
+
*/
|
|
30
|
+
clearHandleReferences(): string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Manages object <-> docHandle binding and automerge document loading.
|
|
35
|
+
*/
|
|
36
|
+
export class AutomergeDocumentLoaderImpl implements AutomergeDocumentLoader {
|
|
37
|
+
private _spaceRootDocHandle: DocHandle<SpaceDoc> | null = null;
|
|
38
|
+
/**
|
|
39
|
+
* An object id pointer to a handle of the document where the object is stored inline.
|
|
40
|
+
*/
|
|
41
|
+
private readonly _objectDocumentHandles = new Map<string, DocHandle<SpaceDoc>>();
|
|
42
|
+
/**
|
|
43
|
+
* If object was requested via loadObjectDocument but root document links weren't updated yet
|
|
44
|
+
* loading will be triggered in onObjectLinksUpdated callback.
|
|
45
|
+
*/
|
|
46
|
+
private readonly _objectsPendingDocumentLoad = new Set<string>();
|
|
47
|
+
|
|
48
|
+
public readonly onObjectDocumentLoaded = new Event<ObjectDocumentLoaded>();
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly _spaceKey: PublicKey,
|
|
52
|
+
private readonly _repo: Repo,
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
public async loadSpaceRootDocHandle(ctx: Context, spaceState: SpaceState): Promise<void> {
|
|
56
|
+
if (this._spaceRootDocHandle != null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!spaceState.rootUrl) {
|
|
60
|
+
log.error('Database opened with no rootUrl', { spaceKey: this._spaceKey });
|
|
61
|
+
this._createContextBoundSpaceRootDocument(ctx);
|
|
62
|
+
} else {
|
|
63
|
+
const existingDocHandle = await this._initDocHandle(ctx, spaceState.rootUrl);
|
|
64
|
+
const doc = existingDocHandle.docSync();
|
|
65
|
+
invariant(doc);
|
|
66
|
+
if (doc.access == null) {
|
|
67
|
+
this._initDocAccess(existingDocHandle);
|
|
68
|
+
}
|
|
69
|
+
this._spaceRootDocHandle = existingDocHandle;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public loadObjectDocument(objectId: string) {
|
|
74
|
+
invariant(this._spaceRootDocHandle);
|
|
75
|
+
if (this._objectDocumentHandles.has(objectId) || this._objectsPendingDocumentLoad.has(objectId)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const spaceRootDoc = this._spaceRootDocHandle.docSync();
|
|
79
|
+
invariant(spaceRootDoc);
|
|
80
|
+
const documentUrl = (spaceRootDoc.links ?? {})[objectId];
|
|
81
|
+
if (documentUrl == null) {
|
|
82
|
+
this._objectsPendingDocumentLoad.add(objectId);
|
|
83
|
+
log.info('loading delayed until object links are initialized', { objectId });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this._loadLinkedObjects({ [objectId]: documentUrl });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public onObjectLinksUpdated(links: SpaceDocumentLinks) {
|
|
90
|
+
if (!links) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const linksAwaitingLoad = Object.entries(links).filter(([objectId]) =>
|
|
94
|
+
this._objectsPendingDocumentLoad.has(objectId),
|
|
95
|
+
);
|
|
96
|
+
this._loadLinkedObjects(Object.fromEntries(linksAwaitingLoad));
|
|
97
|
+
linksAwaitingLoad.forEach(([objectId]) => this._objectsPendingDocumentLoad.delete(objectId));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public getSpaceRootDocHandle(): DocHandle<SpaceDoc> {
|
|
101
|
+
invariant(this._spaceRootDocHandle);
|
|
102
|
+
return this._spaceRootDocHandle;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public createDocumentForObject(objectId: string): DocHandle<SpaceDoc> {
|
|
106
|
+
invariant(this._spaceRootDocHandle);
|
|
107
|
+
const spaceDocHandle = this._repo.create<SpaceDoc>();
|
|
108
|
+
this._initDocAccess(spaceDocHandle);
|
|
109
|
+
this.onObjectBoundToDocument(spaceDocHandle, objectId);
|
|
110
|
+
this._spaceRootDocHandle.change((newDoc: SpaceDoc) => {
|
|
111
|
+
newDoc.links ??= {};
|
|
112
|
+
newDoc.links[objectId] = spaceDocHandle.url;
|
|
113
|
+
});
|
|
114
|
+
return spaceDocHandle;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public onObjectBoundToDocument(handle: DocHandle<SpaceDoc>, objectId: string) {
|
|
118
|
+
this._objectDocumentHandles.set(objectId, handle);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public clearHandleReferences(): string[] {
|
|
122
|
+
const objectsWithHandles = [...this._objectDocumentHandles.keys()];
|
|
123
|
+
this._objectDocumentHandles.clear();
|
|
124
|
+
this._spaceRootDocHandle = null;
|
|
125
|
+
return objectsWithHandles;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private _loadLinkedObjects(links: SpaceDocumentLinks) {
|
|
129
|
+
if (!links) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const [objectId, automergeUrl] of Object.entries(links)) {
|
|
133
|
+
const logMeta = { objectId, automergeUrl };
|
|
134
|
+
const objectDocumentHandle = this._objectDocumentHandles.get(objectId);
|
|
135
|
+
if (objectDocumentHandle != null && objectDocumentHandle.url !== automergeUrl) {
|
|
136
|
+
log.warn('object already inlined in a different document, ignoring the link', {
|
|
137
|
+
...logMeta,
|
|
138
|
+
actualDocumentUrl: objectDocumentHandle.url,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (objectDocumentHandle?.url === automergeUrl) {
|
|
143
|
+
log.warn('object document was already loaded', logMeta);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const handle = this._repo.find<SpaceDoc>(automergeUrl as DocumentId);
|
|
147
|
+
log.debug('document loading triggered', logMeta);
|
|
148
|
+
this._objectDocumentHandles.set(objectId, handle);
|
|
149
|
+
void this._createObjectOnDocumentLoad(handle, objectId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async _initDocHandle(ctx: Context, url: string) {
|
|
154
|
+
const docHandle = this._repo.find<SpaceDoc>(url as DocumentId);
|
|
155
|
+
while (true) {
|
|
156
|
+
try {
|
|
157
|
+
await warnAfterTimeout(5_000, 'Automerge root doc load timeout (AutomergeDb)', async () => {
|
|
158
|
+
await cancelWithContext(ctx, docHandle.whenReady()); // TODO(dmaretskyi): Temporary 5s timeout for debugging.
|
|
159
|
+
});
|
|
160
|
+
break;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (`${err}`.includes('Timeout')) {
|
|
163
|
+
log.info('wraparound', { id: docHandle.documentId, state: docHandle.state });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (docHandle.state === 'unavailable') {
|
|
172
|
+
throw new Error('Automerge document is unavailable');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return docHandle;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private _createContextBoundSpaceRootDocument(ctx: Context) {
|
|
179
|
+
const docHandle = this._repo.create<SpaceDoc>();
|
|
180
|
+
this._spaceRootDocHandle = docHandle;
|
|
181
|
+
ctx.onDispose(() => {
|
|
182
|
+
docHandle.delete();
|
|
183
|
+
this._spaceRootDocHandle = null;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _initDocAccess(handle: DocHandle<SpaceDoc>) {
|
|
188
|
+
handle.change((newDoc: SpaceDoc) => {
|
|
189
|
+
newDoc.access ??= { spaceKey: this._spaceKey.toHex() };
|
|
190
|
+
newDoc.access.spaceKey = this._spaceKey.toHex();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async _createObjectOnDocumentLoad(handle: DocHandle<SpaceDoc>, objectId: string) {
|
|
195
|
+
try {
|
|
196
|
+
await handle.doc(['ready']);
|
|
197
|
+
const logMeta = { objectId, docUrl: handle.url };
|
|
198
|
+
if (this.onObjectDocumentLoaded.listenerCount() === 0) {
|
|
199
|
+
log.info('document loaded after all listeners were removed', logMeta);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const objectDocHandle = this._objectDocumentHandles.get(objectId);
|
|
203
|
+
if (objectDocHandle?.url !== handle.url) {
|
|
204
|
+
log.warn('object was rebound while a document was loading, discarding handle', logMeta);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.onObjectDocumentLoaded.emit({ handle, objectId });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const shouldRetryLoading = this.onObjectDocumentLoaded.listenerCount() > 0;
|
|
210
|
+
log.warn('failed to load a document', {
|
|
211
|
+
objectId,
|
|
212
|
+
automergeUrl: handle.url,
|
|
213
|
+
retryLoading: shouldRetryLoading,
|
|
214
|
+
err,
|
|
215
|
+
});
|
|
216
|
+
if (shouldRetryLoading) {
|
|
217
|
+
await this._createObjectOnDocumentLoad(handle, objectId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface ObjectDocumentLoaded {
|
|
224
|
+
handle: DocHandle<SpaceDoc>;
|
|
225
|
+
objectId: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface DocumentChanges {
|
|
229
|
+
createdObjectIds: string[];
|
|
230
|
+
updatedObjectIds: string[];
|
|
231
|
+
objectsToRebind: string[];
|
|
232
|
+
linkedDocuments: {
|
|
233
|
+
[echoId: string]: AutomergeUrl;
|
|
234
|
+
};
|
|
235
|
+
}
|
package/src/automerge/index.ts
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Copyright 2023 DXOS.org
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import { type Reference } from '@dxos/echo-db';
|
|
10
|
+
|
|
11
|
+
export type SpaceState = {
|
|
12
|
+
// Url of the root automerge document.
|
|
13
|
+
rootUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface SpaceDoc {
|
|
17
|
+
access?: {
|
|
18
|
+
spaceKey: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Objects inlined in the current document.
|
|
22
|
+
*/
|
|
23
|
+
objects?: {
|
|
24
|
+
[key: string]: ObjectStructure;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Object id points to an automerge doc url where the object is embedded.
|
|
28
|
+
*/
|
|
29
|
+
links?: {
|
|
30
|
+
[echoId: string]: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Representation of an ECHO object in an AM document.
|
|
36
|
+
*/
|
|
37
|
+
export type ObjectStructure = {
|
|
38
|
+
data: Record<string, any>;
|
|
39
|
+
meta: ObjectMeta;
|
|
40
|
+
system: ObjectSystem;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Echo object metadata.
|
|
45
|
+
*/
|
|
46
|
+
export type ObjectMeta = {
|
|
47
|
+
/**
|
|
48
|
+
* Foreign keys.
|
|
49
|
+
*/
|
|
50
|
+
keys: ForeignKey[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reference to an object in a foreign database.
|
|
55
|
+
*/
|
|
56
|
+
export type ForeignKey = {
|
|
57
|
+
/**
|
|
58
|
+
* Name of the foreign database/system.
|
|
59
|
+
* E.g. `github.com`.
|
|
60
|
+
*/
|
|
61
|
+
source?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Id within the foreign database.
|
|
65
|
+
*/
|
|
66
|
+
id?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Automerge object system properties.
|
|
71
|
+
* (Is automerge specific.)
|
|
72
|
+
*/
|
|
73
|
+
export type ObjectSystem = {
|
|
74
|
+
/**
|
|
75
|
+
* Deletion marker.
|
|
76
|
+
*/
|
|
77
|
+
deleted?: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Object reference ('protobuf' protocol) type.
|
|
81
|
+
*/
|
|
82
|
+
type?: Reference;
|
|
83
|
+
};
|