@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1da679c

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 (146) hide show
  1. package/dist/lib/browser/{chunk-TQJTKNMS.mjs → chunk-KQYT6ADL.mjs} +109 -3
  2. package/dist/lib/browser/chunk-KQYT6ADL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-35I6ERLG.mjs → chunk-XGG76KKU.mjs} +513 -350
  4. package/dist/lib/browser/chunk-XGG76KKU.mjs.map +7 -0
  5. package/dist/lib/browser/filter/index.mjs +3 -1
  6. package/dist/lib/browser/index.mjs +1371 -601
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +119 -56
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/lib/node-esm/{chunk-5BHLPT24.mjs → chunk-CHMJJ4DG.mjs} +513 -350
  12. package/dist/lib/node-esm/chunk-CHMJJ4DG.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-RVK35BS7.mjs → chunk-W4ACY3YC.mjs} +109 -3
  14. package/dist/lib/node-esm/chunk-W4ACY3YC.mjs.map +7 -0
  15. package/dist/lib/node-esm/filter/index.mjs +3 -1
  16. package/dist/lib/node-esm/index.mjs +1371 -601
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/testing/index.mjs +119 -56
  20. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  21. package/dist/types/src/automerge/automerge-host.d.ts +15 -28
  22. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  23. package/dist/types/src/automerge/collection-synchronizer.d.ts +1 -1
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  25. package/dist/types/src/automerge/echo-network-adapter.d.ts +8 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/echo-replicator.d.ts +21 -2
  28. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  29. package/dist/types/src/automerge/index.d.ts +1 -1
  30. package/dist/types/src/automerge/index.d.ts.map +1 -1
  31. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +1 -1
  32. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  33. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +1 -0
  34. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  35. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  36. package/dist/types/src/common/codec.d.ts +1 -1
  37. package/dist/types/src/common/codec.d.ts.map +1 -1
  38. package/dist/types/src/db-host/data-service.d.ts +2 -2
  39. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  40. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  41. package/dist/types/src/db-host/documents-synchronizer.d.ts +2 -2
  42. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  43. package/dist/types/src/db-host/echo-host.d.ts +2 -2
  44. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  45. package/dist/types/src/db-host/query-service.d.ts +1 -1
  46. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  47. package/dist/types/src/db-host/space-state-manager.d.ts +1 -1
  48. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  49. package/dist/types/src/edge/echo-edge-replicator.d.ts +4 -2
  50. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  51. package/dist/types/src/filter/filter-match.d.ts +4 -1
  52. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  53. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  54. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  55. package/dist/types/src/pipeline/pipeline.d.ts +1 -1
  56. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  57. package/dist/types/src/query/errors.d.ts +24 -8
  58. package/dist/types/src/query/errors.d.ts.map +1 -1
  59. package/dist/types/src/query/plan.d.ts +8 -1
  60. package/dist/types/src/query/plan.d.ts.map +1 -1
  61. package/dist/types/src/query/query-executor.d.ts +4 -1
  62. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  63. package/dist/types/src/query/query-planner.d.ts +2 -0
  64. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  65. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  66. package/dist/types/src/space/control-pipeline.d.ts +1 -1
  67. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  68. package/dist/types/src/space/space-manager.d.ts +1 -1
  69. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  70. package/dist/types/src/space/space-protocol.d.ts +1 -1
  71. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  72. package/dist/types/src/space/space.d.ts +1 -1
  73. package/dist/types/src/space/space.d.ts.map +1 -1
  74. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  75. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  76. package/dist/types/src/testing/test-replicator.d.ts +1 -0
  77. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  78. package/dist/types/src/util.d.ts +1 -1
  79. package/dist/types/src/util.d.ts.map +1 -1
  80. package/dist/types/tsconfig.tsbuildinfo +1 -1
  81. package/package.json +42 -38
  82. package/src/automerge/automerge-host.test.ts +18 -8
  83. package/src/automerge/automerge-host.ts +251 -65
  84. package/src/automerge/automerge-repo.test.ts +67 -16
  85. package/src/automerge/collection-synchronizer.test.ts +2 -2
  86. package/src/automerge/collection-synchronizer.ts +4 -4
  87. package/src/automerge/echo-data-monitor.ts +1 -1
  88. package/src/automerge/echo-network-adapter.test.ts +3 -3
  89. package/src/automerge/echo-network-adapter.ts +40 -7
  90. package/src/automerge/echo-replicator.ts +23 -2
  91. package/src/automerge/index.ts +1 -1
  92. package/src/automerge/leveldb-storage-adapter.ts +7 -7
  93. package/src/automerge/mesh-echo-replicator-connection.ts +4 -0
  94. package/src/automerge/mesh-echo-replicator.ts +2 -1
  95. package/src/automerge/storage-adapter.test.ts +1 -1
  96. package/src/common/space-id.ts +1 -1
  97. package/src/db-host/data-service.ts +9 -17
  98. package/src/db-host/database-root.ts +2 -2
  99. package/src/db-host/documents-synchronizer.test.ts +1 -1
  100. package/src/db-host/documents-synchronizer.ts +39 -26
  101. package/src/db-host/echo-host.ts +13 -14
  102. package/src/db-host/query-service.ts +8 -1
  103. package/src/db-host/space-state-manager.ts +2 -2
  104. package/src/edge/echo-edge-replicator.test.ts +5 -3
  105. package/src/edge/echo-edge-replicator.ts +75 -18
  106. package/src/filter/filter-match.test.ts +23 -3
  107. package/src/filter/filter-match.ts +148 -3
  108. package/src/metadata/metadata-store.ts +3 -3
  109. package/src/pipeline/pipeline-stress.test.ts +4 -2
  110. package/src/pipeline/pipeline.test.ts +3 -2
  111. package/src/pipeline/pipeline.ts +8 -5
  112. package/src/query/errors.ts +2 -0
  113. package/src/query/plan.ts +12 -1
  114. package/src/query/query-executor.ts +66 -11
  115. package/src/query/query-planner.test.ts +146 -2
  116. package/src/query/query-planner.ts +52 -8
  117. package/src/space/admission-discovery-extension.ts +2 -2
  118. package/src/space/control-pipeline.test.ts +4 -3
  119. package/src/space/control-pipeline.ts +9 -6
  120. package/src/space/space-manager.browser.test.ts +1 -1
  121. package/src/space/space-manager.ts +5 -4
  122. package/src/space/space-protocol.browser.test.ts +2 -2
  123. package/src/space/space-protocol.test.ts +3 -2
  124. package/src/space/space-protocol.ts +6 -3
  125. package/src/space/space.test.ts +1 -1
  126. package/src/space/space.ts +3 -2
  127. package/src/testing/test-agent-builder.ts +4 -3
  128. package/src/testing/test-replicator.ts +4 -0
  129. package/src/util.ts +1 -1
  130. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  131. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  132. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  133. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  134. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  135. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  136. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  137. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  138. package/dist/lib/node/filter/index.cjs +0 -32
  139. package/dist/lib/node/filter/index.cjs.map +0 -7
  140. package/dist/lib/node/index.cjs +0 -4699
  141. package/dist/lib/node/index.cjs.map +0 -7
  142. package/dist/lib/node/meta.json +0 -1
  143. package/dist/lib/node/testing/index.cjs +0 -753
  144. package/dist/lib/node/testing/index.cjs.map +0 -7
  145. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  146. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
@@ -5,8 +5,8 @@
5
5
  import { next as am } from '@automerge/automerge';
6
6
  import type { DocumentId, PeerId } from '@automerge/automerge-repo';
7
7
 
8
- import { asyncReturn, Event, scheduleTask, scheduleTaskInterval } from '@dxos/async';
9
- import { Resource, type Context } from '@dxos/context';
8
+ import { Event, asyncReturn, scheduleTask, scheduleTaskInterval } from '@dxos/async';
9
+ import { type Context, Resource } from '@dxos/context';
10
10
  import { log } from '@dxos/log';
11
11
  import { trace } from '@dxos/tracing';
12
12
  import { defaultMap } from '@dxos/util';
@@ -239,9 +239,9 @@ export const diffCollectionState = (local: CollectionState, remote: CollectionSt
239
239
  const missingOnLocal: DocumentId[] = [];
240
240
  const different: DocumentId[] = [];
241
241
  for (const documentId of allDocuments) {
242
- if (!local.documents[documentId]) {
242
+ if (!local.documents[documentId] || local.documents[documentId].length === 0) {
243
243
  missingOnLocal.push(documentId as DocumentId);
244
- } else if (!remote.documents[documentId]) {
244
+ } else if (!remote.documents[documentId] || remote.documents[documentId].length === 0) {
245
245
  missingOnRemote.push(documentId as DocumentId);
246
246
  } else if (!am.equals(local.documents[documentId], remote.documents[documentId])) {
247
247
  different.push(documentId as DocumentId);
@@ -5,7 +5,7 @@
5
5
  import { type Message } from '@automerge/automerge-repo';
6
6
 
7
7
  import { type TimeAware, trace } from '@dxos/tracing';
8
- import { CircularBuffer, mapValues, SlidingWindowSummary, type SlidingWindowSummaryConfig } from '@dxos/util';
8
+ import { CircularBuffer, SlidingWindowSummary, type SlidingWindowSummaryConfig, mapValues } from '@dxos/util';
9
9
 
10
10
  import { type NetworkDataMonitor } from './echo-network-adapter';
11
11
  import { type StorageAdapterDataMonitor } from './leveldb-storage-adapter';
@@ -2,10 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { cbor, type PeerId } from '@automerge/automerge-repo';
6
- import { onTestFinished, describe, expect, test } from 'vitest';
5
+ import { type PeerId, cbor } from '@automerge/automerge-repo';
6
+ import { describe, expect, onTestFinished, test } from 'vitest';
7
7
 
8
- import { sleep, Trigger, waitForCondition } from '@dxos/async';
8
+ import { Trigger, sleep, waitForCondition } from '@dxos/async';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { type SyncMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
@@ -2,9 +2,16 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { NetworkAdapter, type Message, type PeerId, type PeerMetadata } from '@automerge/automerge-repo';
6
-
7
- import { synchronized, Trigger } from '@dxos/async';
5
+ import {
6
+ type DocumentId,
7
+ type Heads,
8
+ type Message,
9
+ NetworkAdapter,
10
+ type PeerId,
11
+ type PeerMetadata,
12
+ } from '@automerge/automerge-repo';
13
+
14
+ import { Trigger, synchronized } from '@dxos/async';
8
15
  import { LifecycleState } from '@dxos/context';
9
16
  import { invariant } from '@dxos/invariant';
10
17
  import { type PublicKey } from '@dxos/keys';
@@ -12,6 +19,8 @@ import { log } from '@dxos/log';
12
19
  import type { AutomergeProtocolMessage } from '@dxos/protocols';
13
20
  import { isNonNullable } from '@dxos/util';
14
21
 
22
+ import { createIdFromSpaceKey } from '../common/space-id';
23
+
15
24
  import {
16
25
  type EchoReplicator,
17
26
  type RemoteDocumentExistenceCheckParams,
@@ -20,12 +29,11 @@ import {
20
29
  type ShouldSyncCollectionParams,
21
30
  } from './echo-replicator';
22
31
  import {
23
- isCollectionQueryMessage,
24
- isCollectionStateMessage,
25
32
  type CollectionQueryMessage,
26
33
  type CollectionStateMessage,
34
+ isCollectionQueryMessage,
35
+ isCollectionStateMessage,
27
36
  } from './network-protocol';
28
- import { createIdFromSpaceKey } from '../common/space-id';
29
37
 
30
38
  export interface NetworkDataMonitor {
31
39
  recordPeerConnected(peerId: string): void;
@@ -203,6 +211,30 @@ export class EchoNetworkAdapter extends NetworkAdapter {
203
211
  .filter(isNonNullable);
204
212
  }
205
213
 
214
+ bundleSyncEnabledForPeer(peerId: PeerId): boolean {
215
+ const connection = this._connections.get(peerId);
216
+ if (!connection) {
217
+ return false;
218
+ }
219
+ return connection.connection.bundleSyncEnabled;
220
+ }
221
+
222
+ async pushBundle(peerId: PeerId, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
223
+ const connection = this._connections.get(peerId);
224
+ if (!connection) {
225
+ throw new Error('Connection not found.');
226
+ }
227
+ return connection.connection.pushBundle!(bundle);
228
+ }
229
+
230
+ async pullBundle(peerId: PeerId, docHeads: Record<DocumentId, Heads>) {
231
+ const connection = this._connections.get(peerId);
232
+ if (!connection) {
233
+ throw new Error('Connection not found.');
234
+ }
235
+ return connection.connection.pullBundle!(docHeads);
236
+ }
237
+
206
238
  private _send(message: Message): void {
207
239
  const connectionEntry = this._connections.get(message.targetId);
208
240
  if (!connectionEntry) {
@@ -281,8 +313,9 @@ export class EchoNetworkAdapter extends NetworkAdapter {
281
313
  this.emit('peer-disconnected', { peerId: connection.peerId as PeerId });
282
314
  this._params.monitor?.recordPeerDisconnected(connection.peerId);
283
315
 
284
- void entry.reader.cancel().catch((err) => log.catch(err));
285
316
  void entry.writer.abort().catch((err) => log.catch(err));
317
+ void entry.reader.cancel().catch((err) => log.catch(err));
318
+
286
319
  this._connections.delete(connection.peerId as PeerId);
287
320
  }
288
321
 
@@ -2,9 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Heads } from '@automerge/automerge';
6
+ import { type DocumentId } from '@automerge/automerge-repo';
7
+
5
8
  import { type PublicKey, type SpaceId } from '@dxos/keys';
6
- import type { AutomergeProtocolMessage } from '@dxos/protocols';
9
+ import { type AutomergeProtocolMessage } from '@dxos/protocols';
7
10
 
11
+ // TODO(burdon): Rename AutomergeReplicator?
8
12
  export interface EchoReplicator {
9
13
  /**
10
14
  * Called on when replicator is added to EchoHost.
@@ -37,7 +41,7 @@ export interface EchoReplicatorContext {
37
41
 
38
42
  export interface ReplicatorConnection {
39
43
  /**
40
- * Remove peer id.
44
+ * Remote peer id.
41
45
  */
42
46
  get peerId(): string;
43
47
 
@@ -61,6 +65,23 @@ export interface ReplicatorConnection {
61
65
  * @returns true if the collection should be synced to this peer.
62
66
  */
63
67
  shouldSyncCollection(params: ShouldSyncCollectionParams): boolean;
68
+
69
+ /**
70
+ * Batch syncing considered enabled if ReplicatorConnection implements `pushBatch` and `pullBatch` methods.
71
+ * @returns true if the batch syncing is enabled.
72
+ */
73
+ get bundleSyncEnabled(): boolean;
74
+
75
+ /**
76
+ * Pushes the batch of documents to the remote peer.
77
+ */
78
+ pushBundle?(bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]): Promise<void>;
79
+
80
+ /**
81
+ * Pulls the batch of documents from the remote peer.
82
+ */
83
+ // TODO(mykola): Use automerge-repo-bundles Bundle type here.
84
+ pullBundle?(docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>>;
64
85
  }
65
86
 
66
87
  export type ShouldAdvertiseParams = {
@@ -5,7 +5,7 @@
5
5
  export * from './automerge-host';
6
6
  export * from './leveldb-storage-adapter';
7
7
  export * from './mesh-echo-replicator';
8
- export * from './echo-replicator';
8
+ export type * from './echo-replicator';
9
9
  export { diffCollectionState } from './collection-synchronizer';
10
10
  export * from './space-collection';
11
11
  export * from './echo-data-monitor';
@@ -2,10 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  // s
4
4
 
5
- import { type StorageAdapterInterface, type Chunk, type StorageKey } from '@automerge/automerge-repo';
5
+ import { type Chunk, type StorageAdapterInterface, type StorageKey } from '@automerge/automerge-repo';
6
6
  import { type MixedEncoding } from 'level-transcoder';
7
7
 
8
- import { LifecycleState, Resource } from '@dxos/context';
8
+ import { Resource } from '@dxos/context';
9
9
  import { type BatchLevel, type SublevelDB } from '@dxos/kv-store';
10
10
  import { type MaybePromise } from '@dxos/util';
11
11
 
@@ -36,7 +36,7 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
36
36
 
37
37
  async load(keyArray: StorageKey): Promise<Uint8Array | undefined> {
38
38
  try {
39
- if (this._lifecycleState !== LifecycleState.OPEN) {
39
+ if (!this.isOpen) {
40
40
  // TODO(mykola): this should be an error.
41
41
  return undefined;
42
42
  }
@@ -54,7 +54,7 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
54
54
  }
55
55
 
56
56
  async save(keyArray: StorageKey, binary: Uint8Array): Promise<void> {
57
- if (this._lifecycleState !== LifecycleState.OPEN) {
57
+ if (!this.isOpen) {
58
58
  return undefined;
59
59
  }
60
60
  const startMs = Date.now();
@@ -72,14 +72,14 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
72
72
  }
73
73
 
74
74
  async remove(keyArray: StorageKey): Promise<void> {
75
- if (this._lifecycleState !== LifecycleState.OPEN) {
75
+ if (!this.isOpen) {
76
76
  return undefined;
77
77
  }
78
78
  await this._params.db.del<StorageKey>(keyArray, { ...encodingOptions });
79
79
  }
80
80
 
81
81
  async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
82
- if (this._lifecycleState !== LifecycleState.OPEN) {
82
+ if (!this.isOpen) {
83
83
  return [];
84
84
  }
85
85
  const startMs = Date.now();
@@ -100,7 +100,7 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
100
100
  }
101
101
 
102
102
  async removeRange(keyPrefix: StorageKey): Promise<void> {
103
- if (this._lifecycleState !== LifecycleState.OPEN) {
103
+ if (!this.isOpen) {
104
104
  return undefined;
105
105
  }
106
106
  const batch = this._params.db.batch();
@@ -111,6 +111,10 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
111
111
  return this._isEnabled;
112
112
  }
113
113
 
114
+ get bundleSyncEnabled(): boolean {
115
+ return false;
116
+ }
117
+
114
118
  async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
115
119
  return this._params.shouldAdvertise(params);
116
120
  }
@@ -12,10 +12,11 @@ import {
12
12
  } from '@dxos/teleport-extension-automerge-replicator';
13
13
  import { ComplexSet, defaultMap } from '@dxos/util';
14
14
 
15
+ import { createIdFromSpaceKey } from '../common/space-id';
16
+
15
17
  import { type EchoReplicator, type EchoReplicatorContext, type ShouldAdvertiseParams } from './echo-replicator';
16
18
  import { MeshReplicatorConnection } from './mesh-echo-replicator-connection';
17
19
  import { getSpaceIdFromCollectionId } from './space-collection';
18
- import { createIdFromSpaceKey } from '../common/space-id';
19
20
 
20
21
  // TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
21
22
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { onTestFinished, describe, expect, test } from 'vitest';
5
+ import { describe, expect, onTestFinished, test } from 'vitest';
6
6
 
7
7
  import { randomBytes } from '@dxos/crypto';
8
8
  import { PublicKey } from '@dxos/keys';
@@ -18,7 +18,7 @@ export const createIdFromSpaceKey = async (spaceKey: PublicKey): Promise<SpaceId
18
18
  return cachedValue;
19
19
  }
20
20
 
21
- const digest = await subtleCrypto.digest('SHA-256', spaceKey.asUint8Array());
21
+ const digest = await subtleCrypto.digest('SHA-256', spaceKey.asUint8Array() as Uint8Array<ArrayBuffer>);
22
22
 
23
23
  const bytes = new Uint8Array(digest).slice(0, SpaceId.byteLength);
24
24
  const spaceId = SpaceId.encode(bytes);
@@ -11,23 +11,24 @@ import { invariant } from '@dxos/invariant';
11
11
  import { SpaceId } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
13
  import {
14
+ type BatchedDocumentUpdates,
14
15
  type DataService,
15
16
  type FlushRequest,
16
- type SubscribeRequest,
17
- type BatchedDocumentUpdates,
18
- type UpdateSubscriptionRequest,
19
17
  type GetDocumentHeadsRequest,
20
18
  type GetDocumentHeadsResponse,
21
- type ReIndexHeadsRequest,
22
- type WaitUntilHeadsReplicatedRequest,
23
- type UpdateRequest,
24
19
  type GetSpaceSyncStateRequest,
20
+ type ReIndexHeadsRequest,
25
21
  type SpaceSyncState,
22
+ type SubscribeRequest,
23
+ type UpdateRequest,
24
+ type UpdateSubscriptionRequest,
25
+ type WaitUntilHeadsReplicatedRequest,
26
26
  } from '@dxos/protocols/proto/dxos/echo/service';
27
27
 
28
+ import { type AutomergeHost, deriveCollectionIdFromSpaceId } from '../automerge';
29
+
28
30
  import { DocumentsSynchronizer } from './documents-synchronizer';
29
31
  import { type SpaceStateManager } from './space-state-manager';
30
- import { deriveCollectionIdFromSpaceId, type AutomergeHost } from '../automerge';
31
32
 
32
33
  export type DataServiceParams = {
33
34
  automergeHost: AutomergeHost;
@@ -145,16 +146,7 @@ export class DataServiceImpl implements DataService {
145
146
  const scheduler = new UpdateScheduler(ctx, async () => {
146
147
  const state = collectionId ? await this._automergeHost.getCollectionSyncState(collectionId) : { peers: [] };
147
148
 
148
- next({
149
- peers: state.peers.map((peer) => ({
150
- peerId: peer.peerId,
151
- missingOnRemote: peer.missingOnRemote,
152
- missingOnLocal: peer.missingOnLocal,
153
- differentDocuments: peer.differentDocuments,
154
- localDocumentCount: peer.localDocumentCount,
155
- remoteDocumentCount: peer.remoteDocumentCount,
156
- })),
157
- });
149
+ next({ peers: state.peers });
158
150
  });
159
151
 
160
152
  this._automergeHost.collectionStateUpdated.on(ctx, (e) => {
@@ -3,12 +3,12 @@
3
3
  //
4
4
 
5
5
  import type * as A from '@automerge/automerge';
6
- import { interpretAsDocumentId, type AutomergeUrl, type DocHandle, type DocumentId } from '@automerge/automerge-repo';
6
+ import { type AutomergeUrl, type DocHandle, type DocumentId, interpretAsDocumentId } from '@automerge/automerge-repo';
7
7
 
8
8
  import { DatabaseDirectory, SpaceDocVersion } from '@dxos/echo-protocol';
9
9
  import { invariant } from '@dxos/invariant';
10
10
 
11
- import { measureDocMetrics, type DocMetrics } from './automerge-metrics';
11
+ import { type DocMetrics, measureDocMetrics } from './automerge-metrics';
12
12
 
13
13
  export class DatabaseRoot {
14
14
  static mapLinks(doc: DocHandle<DatabaseDirectory>, mapping: Record<DocumentId, DocumentId>): void {
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { next as A } from '@automerge/automerge';
6
- import { generateAutomergeUrl, parseAutomergeUrl, Repo } from '@automerge/automerge-repo';
6
+ import { Repo, generateAutomergeUrl, parseAutomergeUrl } from '@automerge/automerge-repo';
7
7
  import { describe, expect, test } from 'vitest';
8
8
 
9
9
  import { sleep } from '@dxos/async';
@@ -3,17 +3,15 @@
3
3
  //
4
4
 
5
5
  import { next as A, type Heads } from '@automerge/automerge';
6
- import { type Repo, type DocHandle, type DocumentId } from '@automerge/automerge-repo';
6
+ import { type DocHandle, type DocumentId, type Repo } from '@automerge/automerge-repo';
7
7
 
8
- import { UpdateScheduler } from '@dxos/async';
9
- import { Resource } from '@dxos/context';
8
+ import { UpdateScheduler, sleep } from '@dxos/async';
9
+ import { LifecycleState, Resource, cancelWithContext } from '@dxos/context';
10
10
  import { type DatabaseDirectory } from '@dxos/echo-protocol';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { log } from '@dxos/log';
13
13
  import { type BatchedDocumentUpdates, type DocumentUpdate } from '@dxos/protocols/proto/dxos/echo/service';
14
14
 
15
- import { FIND_PARAMS } from '../automerge';
16
-
17
15
  const MAX_UPDATE_FREQ = 10; // [updates/sec]
18
16
 
19
17
  export type DocumentsSynchronizerParams = {
@@ -27,6 +25,9 @@ interface DocSyncState {
27
25
  clearSubscriptions?: () => void;
28
26
  }
29
27
 
28
+ const WRAP_AROUND_RETRY_LIMIT = 3;
29
+ const WRAP_AROUND_RETRY_INITIAL_DELAY = 100; // [ms]
30
+
30
31
  /**
31
32
  * Manages a connection and replication between worker's Automerge Repo and the client's Repo.
32
33
  */
@@ -47,8 +48,12 @@ export class DocumentsSynchronizer extends Resource {
47
48
  super();
48
49
  }
49
50
 
50
- addDocuments(documentIds: DocumentId[], retryCounter = 0): void {
51
- if (retryCounter > 3) {
51
+ addDocuments(
52
+ documentIds: DocumentId[],
53
+ retryCounter = 0,
54
+ wrapAroundRetryDelay = WRAP_AROUND_RETRY_INITIAL_DELAY,
55
+ ): void {
56
+ if (retryCounter > WRAP_AROUND_RETRY_LIMIT) {
52
57
  log.warn('Failed to load document, retry limit reached', { documentIds });
53
58
  return;
54
59
  }
@@ -64,7 +69,9 @@ export class DocumentsSynchronizer extends Resource {
64
69
  })
65
70
  .catch((error) => {
66
71
  log.warn('Failed to load document, wraparound', { documentId, error });
67
- this.addDocuments([documentId], retryCounter + 1);
72
+ void cancelWithContext(this._ctx, sleep(wrapAroundRetryDelay)).then(() =>
73
+ this.addDocuments([documentId], retryCounter + 1, wrapAroundRetryDelay * 2),
74
+ );
68
75
  });
69
76
  }
70
77
  }
@@ -90,17 +97,13 @@ export class DocumentsSynchronizer extends Resource {
90
97
 
91
98
  async update(updates: DocumentUpdate[]): Promise<void> {
92
99
  for (const { documentId, mutation, isNew } of updates) {
93
- if (isNew) {
94
- const doc = await this._params.repo.find<DatabaseDirectory>(documentId as DocumentId, FIND_PARAMS);
95
- doc.update((doc) => A.loadIncremental(doc, mutation));
96
- this._startSync(doc);
97
- } else {
98
- this._writeMutation(documentId as DocumentId, mutation);
99
- }
100
+ this._writeMutation(documentId as DocumentId, mutation, isNew);
100
101
  }
102
+ // TODO(mykola): This should not be required.
103
+ await this._params.repo.flush(updates.map(({ documentId }) => documentId as DocumentId));
101
104
  }
102
105
 
103
- private _startSync(doc: DocHandle<DatabaseDirectory>): void {
106
+ private _startSync(doc: DocHandle<DatabaseDirectory>) {
104
107
  if (this._syncStates.has(doc.documentId)) {
105
108
  log('Document already being synced', { documentId: doc.documentId });
106
109
  return;
@@ -109,6 +112,7 @@ export class DocumentsSynchronizer extends Resource {
109
112
  const syncState: DocSyncState = { handle: doc };
110
113
  this._subscribeForChanges(syncState);
111
114
  this._syncStates.set(doc.documentId, syncState);
115
+ return syncState;
112
116
  }
113
117
 
114
118
  _subscribeForChanges(syncState: DocSyncState): void {
@@ -157,16 +161,25 @@ export class DocumentsSynchronizer extends Resource {
157
161
  return mutation;
158
162
  }
159
163
 
160
- private _writeMutation(documentId: DocumentId, mutation: Uint8Array): void {
161
- const syncState = this._syncStates.get(documentId);
162
- invariant(syncState, 'Sync state for document not found');
163
- syncState.handle.update((doc) => {
164
- const headsBefore = A.getHeads(doc);
165
- const newDoc = A.loadIncremental(doc, mutation);
166
- if (A.equals(headsBefore, syncState.lastSentHead)) {
167
- syncState.lastSentHead = A.getHeads(newDoc);
164
+ private _writeMutation(documentId: DocumentId, mutation: Uint8Array, isNew?: boolean): void {
165
+ if (this._lifecycleState === LifecycleState.CLOSED) {
166
+ return;
167
+ }
168
+ if (isNew) {
169
+ const newHandle = this._params.repo.import<DatabaseDirectory>(mutation, { docId: documentId });
170
+ const syncState = this._startSync(newHandle);
171
+ syncState!.lastSentHead = A.getHeads(newHandle.doc());
172
+ } else {
173
+ const syncState = this._syncStates.get(documentId);
174
+ invariant(syncState, 'Sync state for document not found');
175
+ const headsBefore = A.getHeads(syncState.handle.doc());
176
+ // This will update corresponding handle in the repo.
177
+ this._params.repo.import(mutation, { docId: documentId });
178
+
179
+ if (A.equals(headsBefore, syncState!.lastSentHead)) {
180
+ // No new mutations were discovered on network, so we do not need to send updates from worker to client.
181
+ syncState!.lastSentHead = A.getHeads(syncState.handle.doc());
168
182
  }
169
- return newDoc;
170
- });
183
+ }
171
184
  }
172
185
  }
@@ -10,9 +10,9 @@ import {
10
10
  type Repo,
11
11
  } from '@automerge/automerge-repo';
12
12
 
13
- import { LifecycleState, Resource, type Context } from '@dxos/context';
13
+ import { type Context, LifecycleState, Resource } from '@dxos/context';
14
14
  import { todo } from '@dxos/debug';
15
- import { createIdFromSpaceKey, SpaceDocVersion, type DatabaseDirectory } from '@dxos/echo-protocol';
15
+ import { type DatabaseDirectory, SpaceDocVersion, createIdFromSpaceKey } from '@dxos/echo-protocol';
16
16
  import { IndexMetadataStore, IndexStore, Indexer } from '@dxos/indexing';
17
17
  import { invariant } from '@dxos/invariant';
18
18
  import { type PublicKey, type SpaceId } from '@dxos/keys';
@@ -20,24 +20,25 @@ import { type LevelDB } from '@dxos/kv-store';
20
20
  import { IndexKind } from '@dxos/protocols/proto/dxos/echo/indexing';
21
21
  import { trace } from '@dxos/tracing';
22
22
 
23
- import { DataServiceImpl } from './data-service';
24
- import { type DatabaseRoot } from './database-root';
25
- import { createSelectedDocumentsIterator } from './documents-iterator';
26
- import { QueryServiceImpl } from './query-service';
27
- import { SpaceStateManager } from './space-state-manager';
28
23
  import {
29
24
  AutomergeHost,
30
- FIND_PARAMS,
31
- EchoDataMonitor,
32
- deriveCollectionIdFromSpaceId,
33
- type LoadDocOptions,
34
25
  type CreateDocOptions,
35
- type EchoReplicator,
26
+ EchoDataMonitor,
36
27
  type EchoDataStats,
28
+ type EchoReplicator,
29
+ FIND_PARAMS,
30
+ type LoadDocOptions,
37
31
  type PeerIdProvider,
38
32
  type RootDocumentSpaceKeyProvider,
33
+ deriveCollectionIdFromSpaceId,
39
34
  } from '../automerge';
40
35
 
36
+ import { DataServiceImpl } from './data-service';
37
+ import { type DatabaseRoot } from './database-root';
38
+ import { createSelectedDocumentsIterator } from './documents-iterator';
39
+ import { QueryServiceImpl } from './query-service';
40
+ import { SpaceStateManager } from './space-state-manager';
41
+
41
42
  export interface EchoHostIndexingConfig {
42
43
  /**
43
44
  * @default true
@@ -197,8 +198,6 @@ export class EchoHost extends Resource {
197
198
  if (e.previousRootId) {
198
199
  void this._automergeHost.clearLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId, e.previousRootId));
199
200
  }
200
- // TODO(yaroslav): remove collection without spaceRootId after release (production<->staging interop)
201
- void this._automergeHost.updateLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId), e.documentIds);
202
201
  void this._automergeHost.updateLocalCollectionState(
203
202
  deriveCollectionIdFromSpaceId(e.spaceId, e.spaceRootId),
204
203
  e.documentIds,
@@ -23,10 +23,11 @@ import {
23
23
  } from '@dxos/protocols/proto/dxos/echo/query';
24
24
  import { trace } from '@dxos/tracing';
25
25
 
26
- import type { SpaceStateManager } from './space-state-manager';
27
26
  import { type AutomergeHost } from '../automerge';
28
27
  import { QueryExecutor } from '../query';
29
28
 
29
+ import type { SpaceStateManager } from './space-state-manager';
30
+
30
31
  export type QueryServiceParams = {
31
32
  indexer: Indexer;
32
33
  automergeHost: AutomergeHost;
@@ -96,6 +97,12 @@ export class QueryServiceImpl extends Resource implements QueryService {
96
97
 
97
98
  execQuery(request: QueryRequest): Stream<QueryResponse> {
98
99
  return new Stream<QueryResponse>(({ next, close, ctx }) => {
100
+ if (this._params.indexer.config?.enabled !== true) {
101
+ log.error('indexer is disabled', { config: this._params.indexer.config });
102
+ close();
103
+ return;
104
+ }
105
+
99
106
  const queryEntry = this._createQuery(ctx, request, next, close, close);
100
107
  scheduleMicroTask(ctx, async () => {
101
108
  await queryEntry.executor.open();
@@ -2,11 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { interpretAsDocumentId, type DocHandle, type DocumentId } from '@automerge/automerge-repo';
5
+ import { type DocHandle, type DocumentId, interpretAsDocumentId } from '@automerge/automerge-repo';
6
6
  import isEqual from 'lodash.isequal';
7
7
 
8
8
  import { Event, UpdateScheduler } from '@dxos/async';
9
- import { Resource, Context, LifecycleState } from '@dxos/context';
9
+ import { Context, LifecycleState, Resource } from '@dxos/context';
10
10
  import { type DatabaseDirectory } from '@dxos/echo-protocol';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { type SpaceId } from '@dxos/keys';
@@ -7,7 +7,7 @@ import { getRandomPort } from 'get-port-please';
7
7
  import { describe, expect, onTestFinished, test } from 'vitest';
8
8
 
9
9
  import { Event } from '@dxos/async';
10
- import { createEphemeralEdgeIdentity, EdgeClient, MessageSchema } from '@dxos/edge-client';
10
+ import { EdgeClient, type EdgeHttpClient, MessageSchema, createEphemeralEdgeIdentity } from '@dxos/edge-client';
11
11
  import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
12
12
  import { PublicKey, SpaceId } from '@dxos/keys';
13
13
  import { EdgeService } from '@dxos/protocols';
@@ -16,9 +16,10 @@ import { createBuf } from '@dxos/protocols/buf';
16
16
  import type { Peer } from '@dxos/protocols/proto/dxos/edge/messenger';
17
17
  import { openAndClose } from '@dxos/test-utils';
18
18
 
19
- import { EchoEdgeReplicator } from './echo-edge-replicator';
20
19
  import type { EchoReplicatorContext, ReplicatorConnection } from '../automerge';
21
20
 
21
+ import { EchoEdgeReplicator } from './echo-edge-replicator';
22
+
22
23
  describe('EchoEdgeReplicator', () => {
23
24
  test('reconnects', async () => {
24
25
  const { client, server } = await createClientServer();
@@ -79,7 +80,8 @@ describe('EchoEdgeReplicator', () => {
79
80
  });
80
81
 
81
82
  const connectReplicator = async (client: EdgeClient, context: EchoReplicatorContext) => {
82
- const replicator = new EchoEdgeReplicator({ edgeConnection: client });
83
+ // EdgeHttpClient functionality is not tested here.
84
+ const replicator = new EchoEdgeReplicator({ edgeConnection: client, edgeHttpClient: {} as EdgeHttpClient });
83
85
  await replicator.connect(context);
84
86
  onTestFinished(() => replicator.disconnect());
85
87
  return replicator;