@dxos/echo-pipeline 0.6.2-main.8a232a5 → 0.6.2-main.d41f0d2

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 (40) hide show
  1. package/dist/lib/browser/{chunk-UJQ5VS5V.mjs → chunk-SJUDZ3CQ.mjs} +17 -7
  2. package/dist/lib/browser/chunk-SJUDZ3CQ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +562 -57
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-RH6TDRML.cjs → chunk-NLHNTXVQ.cjs} +20 -10
  8. package/dist/lib/node/chunk-NLHNTXVQ.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +582 -79
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +11 -11
  13. package/dist/types/src/automerge/automerge-doc-loader.d.ts +71 -0
  14. package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -0
  15. package/dist/types/src/automerge/automerge-doc-loader.test.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-doc-loader.test.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-host.d.ts +17 -2
  18. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-network-adapter.d.ts +1 -1
  20. package/dist/types/src/automerge/index.d.ts +2 -0
  21. package/dist/types/src/automerge/index.d.ts.map +1 -1
  22. package/dist/types/src/automerge/local-host-network-adapter.d.ts +30 -0
  23. package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -0
  24. package/dist/types/src/db-host/data-service.d.ts +5 -2
  25. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  26. package/dist/types/src/db-host/documents-synchronizer.d.ts +1 -1
  27. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  28. package/package.json +33 -33
  29. package/src/automerge/automerge-doc-loader.test.ts +103 -0
  30. package/src/automerge/automerge-doc-loader.ts +267 -0
  31. package/src/automerge/automerge-host.ts +56 -6
  32. package/src/automerge/automerge-repo.test.ts +1 -124
  33. package/src/automerge/echo-network-adapter.ts +1 -1
  34. package/src/automerge/index.ts +2 -0
  35. package/src/automerge/local-host-network-adapter.ts +115 -0
  36. package/src/db-host/data-service.ts +20 -3
  37. package/src/db-host/documents-synchronizer.test.ts +1 -1
  38. package/src/db-host/documents-synchronizer.ts +1 -1
  39. package/dist/lib/browser/chunk-UJQ5VS5V.mjs.map +0 -7
  40. package/dist/lib/node/chunk-RH6TDRML.cjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-pipeline",
3
- "version": "0.6.2-main.8a232a5",
3
+ "version": "0.6.2-main.d41f0d2",
4
4
  "description": "ECHO database.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -39,38 +39,38 @@
39
39
  "crc-32": "^1.2.2",
40
40
  "level": "^8.0.1",
41
41
  "level-transcoder": "^1.0.1",
42
- "@dxos/async": "0.6.2-main.8a232a5",
43
- "@dxos/codec-protobuf": "0.6.2-main.8a232a5",
44
- "@dxos/automerge": "0.6.2-main.8a232a5",
45
- "@dxos/context": "0.6.2-main.8a232a5",
46
- "@dxos/credentials": "0.6.2-main.8a232a5",
47
- "@dxos/crypto": "0.6.2-main.8a232a5",
48
- "@dxos/echo-protocol": "0.6.2-main.8a232a5",
49
- "@dxos/debug": "0.6.2-main.8a232a5",
50
- "@dxos/echo-schema": "0.6.2-main.8a232a5",
51
- "@dxos/feed-store": "0.6.2-main.8a232a5",
52
- "@dxos/hypercore": "0.6.2-main.8a232a5",
53
- "@dxos/indexing": "0.6.2-main.8a232a5",
54
- "@dxos/invariant": "0.6.2-main.8a232a5",
55
- "@dxos/keyring": "0.6.2-main.8a232a5",
56
- "@dxos/keys": "0.6.2-main.8a232a5",
57
- "@dxos/kv-store": "0.6.2-main.8a232a5",
58
- "@dxos/messaging": "0.6.2-main.8a232a5",
59
- "@dxos/log": "0.6.2-main.8a232a5",
60
- "@dxos/network-manager": "0.6.2-main.8a232a5",
61
- "@dxos/protocols": "0.6.2-main.8a232a5",
62
- "@dxos/node-std": "0.6.2-main.8a232a5",
63
- "@dxos/rpc": "0.6.2-main.8a232a5",
64
- "@dxos/random-access-storage": "0.6.2-main.8a232a5",
65
- "@dxos/teleport": "0.6.2-main.8a232a5",
66
- "@dxos/teleport-extension-gossip": "0.6.2-main.8a232a5",
67
- "@dxos/teleport-extension-automerge-replicator": "0.6.2-main.8a232a5",
68
- "@dxos/teleport-extension-replicator": "0.6.2-main.8a232a5",
69
- "@dxos/teleport-extension-object-sync": "0.6.2-main.8a232a5",
70
- "@dxos/timeframe": "0.6.2-main.8a232a5",
71
- "@dxos/tracing": "0.6.2-main.8a232a5",
72
- "@dxos/typings": "0.6.2-main.8a232a5",
73
- "@dxos/util": "0.6.2-main.8a232a5"
42
+ "@dxos/automerge": "0.6.2-main.d41f0d2",
43
+ "@dxos/async": "0.6.2-main.d41f0d2",
44
+ "@dxos/codec-protobuf": "0.6.2-main.d41f0d2",
45
+ "@dxos/context": "0.6.2-main.d41f0d2",
46
+ "@dxos/debug": "0.6.2-main.d41f0d2",
47
+ "@dxos/credentials": "0.6.2-main.d41f0d2",
48
+ "@dxos/echo-protocol": "0.6.2-main.d41f0d2",
49
+ "@dxos/crypto": "0.6.2-main.d41f0d2",
50
+ "@dxos/echo-schema": "0.6.2-main.d41f0d2",
51
+ "@dxos/feed-store": "0.6.2-main.d41f0d2",
52
+ "@dxos/hypercore": "0.6.2-main.d41f0d2",
53
+ "@dxos/keyring": "0.6.2-main.d41f0d2",
54
+ "@dxos/indexing": "0.6.2-main.d41f0d2",
55
+ "@dxos/invariant": "0.6.2-main.d41f0d2",
56
+ "@dxos/keys": "0.6.2-main.d41f0d2",
57
+ "@dxos/log": "0.6.2-main.d41f0d2",
58
+ "@dxos/kv-store": "0.6.2-main.d41f0d2",
59
+ "@dxos/messaging": "0.6.2-main.d41f0d2",
60
+ "@dxos/network-manager": "0.6.2-main.d41f0d2",
61
+ "@dxos/node-std": "0.6.2-main.d41f0d2",
62
+ "@dxos/random-access-storage": "0.6.2-main.d41f0d2",
63
+ "@dxos/protocols": "0.6.2-main.d41f0d2",
64
+ "@dxos/rpc": "0.6.2-main.d41f0d2",
65
+ "@dxos/teleport": "0.6.2-main.d41f0d2",
66
+ "@dxos/teleport-extension-automerge-replicator": "0.6.2-main.d41f0d2",
67
+ "@dxos/teleport-extension-gossip": "0.6.2-main.d41f0d2",
68
+ "@dxos/teleport-extension-replicator": "0.6.2-main.d41f0d2",
69
+ "@dxos/teleport-extension-object-sync": "0.6.2-main.d41f0d2",
70
+ "@dxos/timeframe": "0.6.2-main.d41f0d2",
71
+ "@dxos/tracing": "0.6.2-main.d41f0d2",
72
+ "@dxos/util": "0.6.2-main.d41f0d2",
73
+ "@dxos/typings": "0.6.2-main.d41f0d2"
74
74
  },
75
75
  "devDependencies": {
76
76
  "fast-check": "^3.19.0",
@@ -0,0 +1,103 @@
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 { type SpaceDoc, SpaceDocVersion } from '@dxos/echo-protocol';
11
+ import { generateEchoId } from '@dxos/echo-schema';
12
+ import { PublicKey } from '@dxos/keys';
13
+ import { describe, test } from '@dxos/test';
14
+
15
+ import {
16
+ type AutomergeDocumentLoader,
17
+ AutomergeDocumentLoaderImpl,
18
+ type ObjectDocumentLoaded,
19
+ } from './automerge-doc-loader';
20
+ import { createIdFromSpaceKey } from '../space';
21
+
22
+ const ctx = new Context();
23
+ const SPACE_KEY = PublicKey.random();
24
+ const randomId = () => generateEchoId();
25
+
26
+ describe('AutomergeDocumentLoader', () => {
27
+ test('space access is set on root doc handle and it is accessible', async () => {
28
+ const { loader, spaceRootDocHandle } = await setupTest();
29
+ expect(loader.getSpaceRootDocHandle()).not.to.throw;
30
+ expect(spaceRootDocHandle.docSync()?.access?.spaceKey).to.eq(SPACE_KEY.toHex());
31
+ });
32
+
33
+ test('new object document is linked with space and root document', async () => {
34
+ const objectId = randomId();
35
+ const { loader, spaceRootDocHandle } = await setupTest();
36
+ const objectDocHandle = loader.createDocumentForObject(objectId);
37
+ const handle = spaceRootDocHandle.docSync();
38
+ expect(objectDocHandle.docSync()?.access?.spaceKey).to.eq(SPACE_KEY.toHex());
39
+ expect(handle?.links[objectId]).to.eq(objectDocHandle.url);
40
+ });
41
+
42
+ test('listener is invoked after a document is loaded', async () => {
43
+ const objectId = randomId();
44
+ const { loader, repo } = await setupTest();
45
+ const handle = repo.create<SpaceDoc>();
46
+ const docLoadInfo = waitForDocumentLoad(loader, { objectId, handle });
47
+ loadLinkedObjects(loader, { [objectId]: handle.url });
48
+ await sleep(10);
49
+ expect(docLoadInfo.loaded).to.be.true;
50
+ });
51
+
52
+ test('listener is not invoked if an object was rebound during document loading', async () => {
53
+ const objectId = randomId();
54
+ const { loader, repo } = await setupTest();
55
+ const oldDocHandle = repo.create<SpaceDoc>();
56
+ const newDocHandle = repo.create<SpaceDoc>();
57
+ const docLoadInfo = waitForDocumentLoad(loader, { objectId, handle: oldDocHandle });
58
+ loadLinkedObjects(loader, { [objectId]: oldDocHandle.url });
59
+ loader.onObjectBoundToDocument(newDocHandle, objectId);
60
+ await sleep(10);
61
+ expect(docLoadInfo.loaded).to.be.false;
62
+ });
63
+
64
+ test('document link is not loaded if object exists as inline object', async () => {
65
+ const objectId = randomId();
66
+ const { loader, repo } = await setupTest();
67
+ const existingHandle = repo.create<SpaceDoc>();
68
+ loader.onObjectBoundToDocument(existingHandle, objectId);
69
+ const newDocHandle = repo.create<SpaceDoc>();
70
+ const docLoadInfo = waitForDocumentLoad(loader, { objectId, handle: newDocHandle });
71
+ loadLinkedObjects(loader, { [objectId]: existingHandle.url });
72
+ await sleep(10);
73
+ expect(docLoadInfo.loaded).to.be.false;
74
+ });
75
+
76
+ const setupTest = async () => {
77
+ const spaceId = await createIdFromSpaceKey(SPACE_KEY);
78
+ const repo = new Repo({ network: [] });
79
+ const loader = new AutomergeDocumentLoaderImpl(spaceId, repo, SPACE_KEY);
80
+ const spaceRootDocHandle = createRootDoc(repo);
81
+ await loader.loadSpaceRootDocHandle(ctx, { rootUrl: spaceRootDocHandle.url });
82
+ return { loader, spaceRootDocHandle, repo };
83
+ };
84
+
85
+ const createRootDoc = (repo: Repo) => {
86
+ return repo.create<SpaceDoc>({ version: SpaceDocVersion.CURRENT });
87
+ };
88
+
89
+ const loadLinkedObjects = (loader: AutomergeDocumentLoader, links: SpaceDoc['links']) => {
90
+ Object.keys(links ?? {}).forEach((objectId) => loader.loadObjectDocument(objectId));
91
+ loader.onObjectLinksUpdated(links);
92
+ };
93
+
94
+ const waitForDocumentLoad = (loader: AutomergeDocumentLoader, expected: ObjectDocumentLoaded) => {
95
+ const docLoadInfo = { loaded: false };
96
+ loader.onObjectDocumentLoaded.on((data) => {
97
+ expect(data.objectId).to.eq(expected.objectId);
98
+ expect(data.handle.url).to.eq(expected.handle.url);
99
+ docLoadInfo.loaded = true;
100
+ });
101
+ return docLoadInfo;
102
+ };
103
+ });
@@ -0,0 +1,267 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Event } from '@dxos/async';
6
+ import {
7
+ type DocHandle,
8
+ type AutomergeUrl,
9
+ type DocumentId,
10
+ type Repo,
11
+ interpretAsDocumentId,
12
+ } from '@dxos/automerge/automerge-repo';
13
+ import { cancelWithContext, type Context } from '@dxos/context';
14
+ import { warnAfterTimeout } from '@dxos/debug';
15
+ import { type SpaceState, type SpaceDoc, SpaceDocVersion } from '@dxos/echo-protocol';
16
+ import { invariant } from '@dxos/invariant';
17
+ import { type PublicKey, type SpaceId } from '@dxos/keys';
18
+ import { log } from '@dxos/log';
19
+ import { trace } from '@dxos/tracing';
20
+
21
+ type SpaceDocumentLinks = SpaceDoc['links'];
22
+
23
+ export interface AutomergeDocumentLoader {
24
+ onObjectDocumentLoaded: Event<ObjectDocumentLoaded>;
25
+
26
+ getAllHandles(): DocHandle<SpaceDoc>[];
27
+
28
+ loadSpaceRootDocHandle(ctx: Context, spaceState: SpaceState): Promise<void>;
29
+ loadObjectDocument(objectId: string | string[]): void;
30
+ getObjectDocumentId(objectId: string): string | undefined;
31
+ getSpaceRootDocHandle(): DocHandle<SpaceDoc>;
32
+ createDocumentForObject(objectId: string): DocHandle<SpaceDoc>;
33
+ onObjectLinksUpdated(links: SpaceDocumentLinks): void;
34
+ onObjectBoundToDocument(handle: DocHandle<SpaceDoc>, objectId: string): void;
35
+
36
+ /**
37
+ * @returns objectIds for which we had document handles or were loading one.
38
+ */
39
+ clearHandleReferences(): string[];
40
+ }
41
+
42
+ /**
43
+ * Manages object <-> docHandle binding and automerge document loading.
44
+ */
45
+ @trace.resource()
46
+ export class AutomergeDocumentLoaderImpl implements AutomergeDocumentLoader {
47
+ private _spaceRootDocHandle: DocHandle<SpaceDoc> | null = null;
48
+ /**
49
+ * An object id pointer to a handle of the document where the object is stored inline.
50
+ */
51
+ private readonly _objectDocumentHandles = new Map<string, DocHandle<SpaceDoc>>();
52
+ /**
53
+ * If object was requested via loadObjectDocument but root document links weren't updated yet
54
+ * loading will be triggered in onObjectLinksUpdated callback.
55
+ */
56
+ private readonly _objectsPendingDocumentLoad = new Set<string>();
57
+
58
+ public readonly onObjectDocumentLoaded = new Event<ObjectDocumentLoaded>();
59
+
60
+ constructor(
61
+ private readonly _spaceId: SpaceId,
62
+ private readonly _repo: Repo,
63
+ /** Legacy Id */
64
+ private readonly _spaceKey: PublicKey,
65
+ ) {}
66
+
67
+ getAllHandles(): DocHandle<SpaceDoc>[] {
68
+ return this._spaceRootDocHandle != null
69
+ ? [this._spaceRootDocHandle, ...new Set(this._objectDocumentHandles.values())]
70
+ : [];
71
+ }
72
+
73
+ @trace.span({ showInBrowserTimeline: true })
74
+ public async loadSpaceRootDocHandle(ctx: Context, spaceState: SpaceState): Promise<void> {
75
+ if (this._spaceRootDocHandle != null) {
76
+ return;
77
+ }
78
+ if (!spaceState.rootUrl) {
79
+ throw new Error('Database opened with no rootUrl');
80
+ }
81
+
82
+ const existingDocHandle = await this._initDocHandle(ctx, spaceState.rootUrl);
83
+ const doc = existingDocHandle.docSync();
84
+ invariant(doc);
85
+ invariant(doc.version === SpaceDocVersion.CURRENT);
86
+ if (doc.access == null) {
87
+ this._initDocAccess(existingDocHandle);
88
+ }
89
+ this._spaceRootDocHandle = existingDocHandle;
90
+ }
91
+
92
+ public loadObjectDocument(objectIdOrMany: string | string[]) {
93
+ const objectIds = Array.isArray(objectIdOrMany) ? objectIdOrMany : [objectIdOrMany];
94
+ let hasUrlsToLoad = false;
95
+ const urlsToLoad: SpaceDoc['links'] = {};
96
+ for (const objectId of objectIds) {
97
+ invariant(this._spaceRootDocHandle);
98
+ if (this._objectDocumentHandles.has(objectId) || this._objectsPendingDocumentLoad.has(objectId)) {
99
+ continue;
100
+ }
101
+ const spaceRootDoc = this._spaceRootDocHandle.docSync();
102
+ invariant(spaceRootDoc);
103
+ const documentUrl = (spaceRootDoc.links ?? {})[objectId];
104
+ if (documentUrl == null) {
105
+ this._objectsPendingDocumentLoad.add(objectId);
106
+ log.info('loading delayed until object links are initialized', { objectId });
107
+ } else {
108
+ urlsToLoad[objectId] = documentUrl;
109
+ hasUrlsToLoad = true;
110
+ }
111
+ }
112
+ if (hasUrlsToLoad) {
113
+ this._loadLinkedObjects(urlsToLoad);
114
+ }
115
+ }
116
+
117
+ public getObjectDocumentId(objectId: string): string | undefined {
118
+ invariant(this._spaceRootDocHandle);
119
+ const spaceRootDoc = this._spaceRootDocHandle.docSync();
120
+ invariant(spaceRootDoc);
121
+ if (spaceRootDoc.objects?.[objectId]) {
122
+ return this._spaceRootDocHandle.documentId;
123
+ }
124
+ const documentUrl = (spaceRootDoc.links ?? {})[objectId];
125
+ return documentUrl && interpretAsDocumentId(documentUrl);
126
+ }
127
+
128
+ public onObjectLinksUpdated(links: SpaceDocumentLinks) {
129
+ if (!links) {
130
+ return;
131
+ }
132
+ const linksAwaitingLoad = Object.entries(links).filter(([objectId]) =>
133
+ this._objectsPendingDocumentLoad.has(objectId),
134
+ );
135
+ this._loadLinkedObjects(Object.fromEntries(linksAwaitingLoad));
136
+ linksAwaitingLoad.forEach(([objectId]) => this._objectsPendingDocumentLoad.delete(objectId));
137
+ }
138
+
139
+ public getSpaceRootDocHandle(): DocHandle<SpaceDoc> {
140
+ invariant(this._spaceRootDocHandle);
141
+ return this._spaceRootDocHandle;
142
+ }
143
+
144
+ public createDocumentForObject(objectId: string): DocHandle<SpaceDoc> {
145
+ invariant(this._spaceRootDocHandle);
146
+ const spaceDocHandle = this._repo.create<SpaceDoc>({
147
+ version: SpaceDocVersion.CURRENT,
148
+ });
149
+ this._initDocAccess(spaceDocHandle);
150
+ this.onObjectBoundToDocument(spaceDocHandle, objectId);
151
+ this._spaceRootDocHandle.change((newDoc: SpaceDoc) => {
152
+ newDoc.links ??= {};
153
+ newDoc.links[objectId] = spaceDocHandle.url;
154
+ });
155
+ return spaceDocHandle;
156
+ }
157
+
158
+ public onObjectBoundToDocument(handle: DocHandle<SpaceDoc>, objectId: string) {
159
+ this._objectDocumentHandles.set(objectId, handle);
160
+ }
161
+
162
+ public clearHandleReferences(): string[] {
163
+ const objectsWithHandles = [...this._objectDocumentHandles.keys()];
164
+ this._objectDocumentHandles.clear();
165
+ this._spaceRootDocHandle = null;
166
+ return objectsWithHandles;
167
+ }
168
+
169
+ private _loadLinkedObjects(links: SpaceDocumentLinks) {
170
+ if (!links) {
171
+ return;
172
+ }
173
+ for (const [objectId, automergeUrl] of Object.entries(links)) {
174
+ const logMeta = { objectId, automergeUrl };
175
+ const objectDocumentHandle = this._objectDocumentHandles.get(objectId);
176
+ if (objectDocumentHandle != null && objectDocumentHandle.url !== automergeUrl) {
177
+ log.warn('object already inlined in a different document, ignoring the link', {
178
+ ...logMeta,
179
+ actualDocumentUrl: objectDocumentHandle.url,
180
+ });
181
+ continue;
182
+ }
183
+ if (objectDocumentHandle?.url === automergeUrl) {
184
+ log.warn('object document was already loaded', logMeta);
185
+ continue;
186
+ }
187
+ const handle = this._repo.find<SpaceDoc>(automergeUrl as DocumentId);
188
+ log.debug('document loading triggered', logMeta);
189
+ this._objectDocumentHandles.set(objectId, handle);
190
+ void this._createObjectOnDocumentLoad(handle, objectId);
191
+ }
192
+ }
193
+
194
+ private async _initDocHandle(ctx: Context, url: string) {
195
+ const docHandle = this._repo.find<SpaceDoc>(url as DocumentId);
196
+ while (true) {
197
+ try {
198
+ await warnAfterTimeout(5_000, 'Automerge root doc load timeout (CoreDatabase)', async () => {
199
+ await cancelWithContext(ctx, docHandle.whenReady()); // TODO(dmaretskyi): Temporary 5s timeout for debugging.
200
+ });
201
+ break;
202
+ } catch (err) {
203
+ if (`${err}`.includes('Timeout')) {
204
+ log.info('wraparound', { id: docHandle.documentId, state: docHandle.state });
205
+ continue;
206
+ }
207
+
208
+ throw err;
209
+ }
210
+ }
211
+
212
+ if (docHandle.state === 'unavailable') {
213
+ throw new Error('Automerge document is unavailable');
214
+ }
215
+
216
+ return docHandle;
217
+ }
218
+
219
+ private _initDocAccess(handle: DocHandle<SpaceDoc>) {
220
+ handle.change((newDoc: SpaceDoc) => {
221
+ newDoc.access ??= { spaceKey: this._spaceKey.toHex() };
222
+ newDoc.access.spaceKey = this._spaceKey.toHex();
223
+ });
224
+ }
225
+
226
+ private async _createObjectOnDocumentLoad(handle: DocHandle<SpaceDoc>, objectId: string) {
227
+ try {
228
+ await handle.whenReady();
229
+ const logMeta = { objectId, docUrl: handle.url };
230
+ if (this.onObjectDocumentLoaded.listenerCount() === 0) {
231
+ log.info('document loaded after all listeners were removed', logMeta);
232
+ return;
233
+ }
234
+ const objectDocHandle = this._objectDocumentHandles.get(objectId);
235
+ if (objectDocHandle?.url !== handle.url) {
236
+ log.warn('object was rebound while a document was loading, discarding handle', logMeta);
237
+ return;
238
+ }
239
+ this.onObjectDocumentLoaded.emit({ handle, objectId });
240
+ } catch (err) {
241
+ const shouldRetryLoading = this.onObjectDocumentLoaded.listenerCount() > 0;
242
+ log.warn('failed to load a document', {
243
+ objectId,
244
+ automergeUrl: handle.url,
245
+ retryLoading: shouldRetryLoading,
246
+ err,
247
+ });
248
+ if (shouldRetryLoading) {
249
+ await this._createObjectOnDocumentLoad(handle, objectId);
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ export interface ObjectDocumentLoaded {
256
+ handle: DocHandle<SpaceDoc>;
257
+ objectId: string;
258
+ }
259
+
260
+ export interface DocumentChanges {
261
+ createdObjectIds: string[];
262
+ updatedObjectIds: string[];
263
+ objectsToRebind: string[];
264
+ linkedDocuments: {
265
+ [echoId: string]: AutomergeUrl;
266
+ };
267
+ }
@@ -8,20 +8,21 @@ import {
8
8
  getBackend,
9
9
  getHeads,
10
10
  isAutomerge,
11
- equals as headsEquals,
12
11
  save,
12
+ equals as headsEquals,
13
13
  type Doc,
14
14
  type Heads,
15
15
  } from '@dxos/automerge/automerge';
16
16
  import {
17
- type DocHandleChangePayload,
18
17
  Repo,
19
18
  type AnyDocumentId,
20
19
  type DocHandle,
20
+ type DocHandleChangePayload,
21
21
  type DocumentId,
22
22
  type PeerId,
23
23
  type StorageAdapterInterface,
24
24
  } from '@dxos/automerge/automerge-repo';
25
+ import { type Stream } from '@dxos/codec-protobuf';
25
26
  import { Context, Resource, cancelWithContext, type Lifecycle } from '@dxos/context';
26
27
  import { type SpaceDoc } from '@dxos/echo-protocol';
27
28
  import { type IndexMetadataStore } from '@dxos/indexing';
@@ -30,7 +31,13 @@ import { PublicKey } from '@dxos/keys';
30
31
  import { type LevelDB } from '@dxos/kv-store';
31
32
  import { log } from '@dxos/log';
32
33
  import { objectPointerCodec } from '@dxos/protocols';
33
- import { type DocHeadsList, type FlushRequest } from '@dxos/protocols/proto/dxos/echo/service';
34
+ import {
35
+ type DocHeadsList,
36
+ type FlushRequest,
37
+ type HostInfo,
38
+ type SyncRepoRequest,
39
+ type SyncRepoResponse,
40
+ } from '@dxos/protocols/proto/dxos/echo/service';
34
41
  import { trace } from '@dxos/tracing';
35
42
  import { mapValues } from '@dxos/util';
36
43
 
@@ -38,6 +45,10 @@ import { EchoNetworkAdapter, isEchoPeerMetadata } from './echo-network-adapter';
38
45
  import { type EchoReplicator } from './echo-replicator';
39
46
  import { HeadsStore } from './heads-store';
40
47
  import { LevelDBStorageAdapter, type BeforeSaveParams } from './leveldb-storage-adapter';
48
+ import { LocalHostNetworkAdapter } from './local-host-network-adapter';
49
+
50
+ // TODO: Remove
51
+ export type { DocumentId };
41
52
 
42
53
  export type AutomergeHostParams = {
43
54
  db: LevelDB;
@@ -68,6 +79,7 @@ export class AutomergeHost extends Resource {
68
79
  });
69
80
 
70
81
  private _repo!: Repo;
82
+ private _clientNetwork!: LocalHostNetworkAdapter;
71
83
  private _storage!: StorageAdapterInterface & Lifecycle;
72
84
  private readonly _headsStore: HeadsStore;
73
85
 
@@ -93,6 +105,7 @@ export class AutomergeHost extends Resource {
93
105
  this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
94
106
 
95
107
  await this._storage.open?.();
108
+ this._clientNetwork = new LocalHostNetworkAdapter();
96
109
 
97
110
  // Construct the automerge repo.
98
111
  this._repo = new Repo({
@@ -100,17 +113,22 @@ export class AutomergeHost extends Resource {
100
113
  sharePolicy: this._sharePolicy.bind(this),
101
114
  storage: this._storage,
102
115
  network: [
116
+ // Downstream client.
117
+ this._clientNetwork,
103
118
  // Upstream swarm.
104
119
  this._echoNetworkAdapter,
105
120
  ],
106
121
  });
107
122
 
123
+ this._clientNetwork.ready();
108
124
  await this._echoNetworkAdapter.open();
125
+ await this._clientNetwork.whenConnected();
109
126
  await this._echoNetworkAdapter.whenConnected();
110
127
  }
111
128
 
112
129
  protected override async _close() {
113
130
  await this._storage.close?.();
131
+ await this._clientNetwork.close();
114
132
  await this._echoNetworkAdapter.close();
115
133
  await this._ctx.dispose();
116
134
  }
@@ -317,10 +335,21 @@ export class AutomergeHost extends Resource {
317
335
  * Flush documents to disk.
318
336
  */
319
337
  @trace.span({ showInBrowserTimeline: true })
320
- async flush({ documentIds }: FlushRequest = {}): Promise<void> {
321
- // Note: Sync protocol for client and services ensures that all handles should have all changes.
338
+ async flush({ states }: FlushRequest = {}): Promise<void> {
339
+ // Note: Wait for all requested documents to be loaded/synced from thin-client.
340
+ if (states) {
341
+ await Promise.all(
342
+ states.map(async ({ heads, documentId }) => {
343
+ if (!heads) {
344
+ return;
345
+ }
346
+ const handle = this._repo.handles[documentId as DocumentId] ?? this._repo.find(documentId as DocumentId);
347
+ await waitForHeads(handle, heads);
348
+ }) ?? [],
349
+ );
350
+ }
322
351
 
323
- await this._repo.flush(documentIds as DocumentId[] | undefined);
352
+ await this._repo.flush(states?.map(({ documentId }) => documentId as DocumentId));
324
353
  }
325
354
 
326
355
  async getHeads(documentId: DocumentId): Promise<Heads | undefined> {
@@ -335,6 +364,27 @@ export class AutomergeHost extends Resource {
335
364
  return this._headsStore.getHeads(documentId);
336
365
  }
337
366
  }
367
+
368
+ /**
369
+ * Host <-> Client sync.
370
+ */
371
+ syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
372
+ return this._clientNetwork.syncRepo(request);
373
+ }
374
+
375
+ /**
376
+ * Host <-> Client sync.
377
+ */
378
+ sendSyncMessage(request: SyncRepoRequest): Promise<void> {
379
+ return this._clientNetwork.sendSyncMessage(request);
380
+ }
381
+
382
+ /**
383
+ * Host <-> Client sync.
384
+ */
385
+ async getHostInfo(): Promise<HostInfo> {
386
+ return this._clientNetwork.getHostInfo();
387
+ }
338
388
  }
339
389
 
340
390
  export const getSpaceKeyFromDoc = (doc: Doc<SpaceDoc>): string | null => {