@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff

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 (244) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
  4. package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
  5. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
  6. package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
  7. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  8. package/dist/lib/neutral/index.mjs +7332 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  14. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  15. package/dist/types/src/automerge/automerge-host.d.ts +119 -54
  16. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  18. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  19. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  20. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  21. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  22. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  25. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  28. package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
  29. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  30. package/dist/types/src/automerge/errors.d.ts +35 -0
  31. package/dist/types/src/automerge/errors.d.ts.map +1 -0
  32. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  33. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  34. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  35. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  36. package/dist/types/src/automerge/index.d.ts +3 -1
  37. package/dist/types/src/automerge/index.d.ts.map +1 -1
  38. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
  39. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  40. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  41. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  42. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  43. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  44. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  45. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  46. package/dist/types/src/common/codec.d.ts +1 -1
  47. package/dist/types/src/common/codec.d.ts.map +1 -1
  48. package/dist/types/src/common/feeds.d.ts.map +1 -1
  49. package/dist/types/src/common/space-id.d.ts.map +1 -1
  50. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  51. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  53. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  55. package/dist/types/src/db-host/data-service.d.ts +13 -7
  56. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  57. package/dist/types/src/db-host/database-root.d.ts +9 -3
  58. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  59. package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
  60. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  61. package/dist/types/src/db-host/echo-host.d.ts +74 -28
  62. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  63. package/dist/types/src/db-host/index.d.ts +2 -0
  64. package/dist/types/src/db-host/index.d.ts.map +1 -1
  65. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  66. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  67. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  68. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  69. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  70. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  71. package/dist/types/src/db-host/query-service.d.ts +19 -11
  72. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  73. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  74. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  75. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  76. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  77. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  78. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  79. package/dist/types/src/db-host/stub.d.ts +11 -0
  80. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  82. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  83. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  84. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  85. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  86. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  87. package/dist/types/src/edge/index.d.ts +1 -0
  88. package/dist/types/src/edge/index.d.ts.map +1 -1
  89. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  90. package/dist/types/src/filter/filter-match.d.ts +4 -1
  91. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  92. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  93. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  94. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  95. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  96. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  97. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  98. package/dist/types/src/query/errors.d.ts +22 -10
  99. package/dist/types/src/query/errors.d.ts.map +1 -1
  100. package/dist/types/src/query/plan.d.ts +66 -8
  101. package/dist/types/src/query/plan.d.ts.map +1 -1
  102. package/dist/types/src/query/query-executor.d.ts +66 -11
  103. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  104. package/dist/types/src/query/query-planner.d.ts +12 -1
  105. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  106. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  107. package/dist/types/src/space/auth.d.ts +3 -3
  108. package/dist/types/src/space/auth.d.ts.map +1 -1
  109. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  110. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  111. package/dist/types/src/space/space-manager.d.ts +8 -8
  112. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  113. package/dist/types/src/space/space-protocol.d.ts +10 -9
  114. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  115. package/dist/types/src/space/space.d.ts +6 -6
  116. package/dist/types/src/space/space.d.ts.map +1 -1
  117. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  118. package/dist/types/src/testing/index.d.ts +0 -2
  119. package/dist/types/src/testing/index.d.ts.map +1 -1
  120. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  121. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  122. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  123. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  124. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  125. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  126. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  127. package/dist/types/src/util.d.ts +1 -1
  128. package/dist/types/src/util.d.ts.map +1 -1
  129. package/dist/types/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +59 -55
  131. package/src/automerge/automerge-host-subduction.test.ts +263 -0
  132. package/src/automerge/automerge-host.test.ts +162 -39
  133. package/src/automerge/automerge-host.ts +679 -201
  134. package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
  135. package/src/automerge/automerge-repo.test.ts +349 -91
  136. package/src/automerge/automerge-subduction.test.ts +330 -0
  137. package/src/automerge/collection-synchronizer.test.ts +98 -17
  138. package/src/automerge/collection-synchronizer.ts +141 -37
  139. package/src/automerge/echo-data-monitor.ts +1 -1
  140. package/src/automerge/echo-network-adapter.test.ts +5 -4
  141. package/src/automerge/echo-network-adapter.ts +76 -31
  142. package/src/automerge/echo-replicator.ts +50 -15
  143. package/src/automerge/errors.ts +10 -0
  144. package/src/automerge/handle-state.ts +62 -0
  145. package/src/automerge/heads-store.ts +42 -3
  146. package/src/automerge/index.ts +3 -1
  147. package/src/automerge/leveldb-storage-adapter.ts +77 -11
  148. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  149. package/src/automerge/mesh-echo-replicator.ts +15 -11
  150. package/src/automerge/storage-adapter.test.ts +1 -1
  151. package/src/common/space-id.ts +1 -1
  152. package/src/db-host/automerge-data-source.test.ts +299 -0
  153. package/src/db-host/automerge-data-source.ts +187 -0
  154. package/src/db-host/data-service.ts +35 -26
  155. package/src/db-host/database-root.ts +20 -8
  156. package/src/db-host/documents-synchronizer.test.ts +145 -14
  157. package/src/db-host/documents-synchronizer.ts +163 -51
  158. package/src/db-host/echo-host.ts +384 -99
  159. package/src/db-host/index.ts +2 -0
  160. package/src/db-host/invalidation-hint.ts +50 -0
  161. package/src/db-host/local-queue-service.ts +127 -0
  162. package/src/db-host/query-invalidation.test.ts +270 -0
  163. package/src/db-host/query-service.ts +111 -106
  164. package/src/db-host/queue-data-source.ts +160 -0
  165. package/src/db-host/queue-service.test.ts +145 -0
  166. package/src/db-host/space-state-manager.ts +12 -10
  167. package/src/db-host/stub.ts +33 -0
  168. package/src/edge/echo-edge-replicator.test.ts +12 -10
  169. package/src/edge/echo-edge-replicator.ts +144 -58
  170. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  171. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  172. package/src/edge/index.ts +1 -0
  173. package/src/filter/filter-match.test.ts +68 -32
  174. package/src/filter/filter-match.ts +230 -21
  175. package/src/metadata/metadata-store.ts +11 -8
  176. package/src/pipeline/pipeline-stress.test.ts +1 -1
  177. package/src/pipeline/pipeline.test.ts +2 -2
  178. package/src/pipeline/pipeline.ts +9 -7
  179. package/src/query/errors.ts +3 -1
  180. package/src/query/plan.ts +84 -9
  181. package/src/query/query-executor.ts +1197 -165
  182. package/src/query/query-planner.test.ts +993 -81
  183. package/src/query/query-planner.ts +503 -27
  184. package/src/space/admission-discovery-extension.ts +3 -3
  185. package/src/space/auth.ts +6 -6
  186. package/src/space/control-pipeline.test.ts +5 -4
  187. package/src/space/control-pipeline.ts +16 -13
  188. package/src/space/space-manager.browser.test.ts +1 -1
  189. package/src/space/space-manager.ts +16 -20
  190. package/src/space/space-protocol.browser.test.ts +15 -14
  191. package/src/space/space-protocol.test.ts +15 -14
  192. package/src/space/space-protocol.ts +22 -18
  193. package/src/space/space.test.ts +3 -3
  194. package/src/space/space.ts +11 -11
  195. package/src/testing/index.ts +0 -2
  196. package/src/testing/test-agent-builder.ts +3 -3
  197. package/src/testing/test-network-adapter.ts +24 -3
  198. package/src/testing/test-replicator.ts +19 -20
  199. package/src/util.ts +7 -3
  200. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  201. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  202. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  203. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  204. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  205. package/dist/lib/browser/index.mjs +0 -4743
  206. package/dist/lib/browser/index.mjs.map +0 -7
  207. package/dist/lib/browser/meta.json +0 -1
  208. package/dist/lib/browser/testing/index.mjs.map +0 -7
  209. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  210. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  211. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  212. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  213. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  214. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  215. package/dist/lib/node/filter/index.cjs +0 -32
  216. package/dist/lib/node/filter/index.cjs.map +0 -7
  217. package/dist/lib/node/index.cjs +0 -4699
  218. package/dist/lib/node/index.cjs.map +0 -7
  219. package/dist/lib/node/meta.json +0 -1
  220. package/dist/lib/node/testing/index.cjs +0 -753
  221. package/dist/lib/node/testing/index.cjs.map +0 -7
  222. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  223. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  224. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  225. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  226. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  227. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  228. package/dist/lib/node-esm/filter/index.mjs +0 -11
  229. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  230. package/dist/lib/node-esm/index.mjs +0 -4743
  231. package/dist/lib/node-esm/index.mjs.map +0 -7
  232. package/dist/lib/node-esm/meta.json +0 -1
  233. package/dist/lib/node-esm/testing/index.mjs +0 -742
  234. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  235. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  236. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  237. package/dist/types/src/testing/test-data.d.ts +0 -18
  238. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  239. package/dist/types/src/testing/test-schema.d.ts +0 -37
  240. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  241. package/src/db-host/documents-iterator.ts +0 -74
  242. package/src/testing/test-data.ts +0 -127
  243. package/src/testing/test-schema.ts +0 -53
  244. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -2,14 +2,19 @@
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
+
8
+ import { type Context } from '@dxos/context';
5
9
  import { type PublicKey, type SpaceId } from '@dxos/keys';
6
- import type { AutomergeProtocolMessage } from '@dxos/protocols';
10
+ import { type AutomergeProtocolMessage } from '@dxos/protocols';
7
11
 
8
- export interface EchoReplicator {
12
+ // TODO(burdon): Rename AutomergeReplicator?
13
+ export interface AutomergeReplicator {
9
14
  /**
10
15
  * Called on when replicator is added to EchoHost.
11
16
  */
12
- connect(context: EchoReplicatorContext): Promise<void>;
17
+ connect(ctx: Context, context: AutomergeReplicatorContext): Promise<void>;
13
18
 
14
19
  /**
15
20
  * Called on when replicator is removed from EchoHost.
@@ -17,7 +22,16 @@ export interface EchoReplicator {
17
22
  disconnect(): Promise<void>;
18
23
  }
19
24
 
20
- export interface EchoReplicatorContext {
25
+ /**
26
+ * Replicator with explicit per-space wiring (used by edge replicators that maintain one logical
27
+ * connection per `SpaceId`). `DataSpaceManager` drives these lifecycle hooks on space open/close.
28
+ */
29
+ export interface EdgeAutomergeReplicator extends AutomergeReplicator {
30
+ connectToSpace(ctx: Context, spaceId: SpaceId): Promise<void>;
31
+ disconnectFromSpace(spaceId: SpaceId): Promise<void>;
32
+ }
33
+
34
+ export interface AutomergeReplicatorContext {
21
35
  /**
22
36
  * Our own peer id.
23
37
  */
@@ -28,16 +42,20 @@ export interface EchoReplicatorContext {
28
42
  */
29
43
  getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
30
44
  getContainingSpaceIdForDocument(documentId: string): Promise<SpaceId | null>;
31
- isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckParams): Promise<boolean>;
32
45
 
33
- onConnectionOpen(connection: ReplicatorConnection): void;
34
- onConnectionClosed(connection: ReplicatorConnection): void;
35
- onConnectionAuthScopeChanged(connection: ReplicatorConnection): void;
46
+ /**
47
+ * Returns false if collection sync hasn't happened yet.
48
+ */
49
+ isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckProps): Promise<boolean>;
50
+
51
+ onConnectionOpen(connection: AutomergeReplicatorConnection): void;
52
+ onConnectionClosed(connection: AutomergeReplicatorConnection): void;
53
+ onConnectionAuthScopeChanged(connection: AutomergeReplicatorConnection): void;
36
54
  }
37
55
 
38
- export interface ReplicatorConnection {
56
+ export interface AutomergeReplicatorConnection {
39
57
  /**
40
- * Remove peer id.
58
+ * Remote peer id.
41
59
  */
42
60
  get peerId(): string;
43
61
 
@@ -55,23 +73,40 @@ export interface ReplicatorConnection {
55
73
  * @returns true if the document should be advertised to this peer.
56
74
  * The remote peer can still request the document by its id bypassing this check.
57
75
  */
58
- shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean>;
76
+ shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean>;
59
77
 
60
78
  /**
61
79
  * @returns true if the collection should be synced to this peer.
62
80
  */
63
- shouldSyncCollection(params: ShouldSyncCollectionParams): boolean;
81
+ shouldSyncCollection(params: ShouldSyncCollectionProps): boolean;
82
+
83
+ /**
84
+ * Batch syncing considered enabled if AutomergeReplicatorConnection implements `pushBatch` and `pullBatch` methods.
85
+ * @returns true if the batch syncing is enabled.
86
+ */
87
+ get bundleSyncEnabled(): boolean;
88
+
89
+ /**
90
+ * Pushes the batch of documents to the remote peer.
91
+ */
92
+ pushBundle?(ctx: Context, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]): Promise<void>;
93
+
94
+ /**
95
+ * Pulls the batch of documents from the remote peer.
96
+ */
97
+ // TODO(mykola): Use automerge-repo-bundles Bundle type here.
98
+ pullBundle?(ctx: Context, docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>>;
64
99
  }
65
100
 
66
- export type ShouldAdvertiseParams = {
101
+ export type ShouldAdvertiseProps = {
67
102
  documentId: string;
68
103
  };
69
104
 
70
- export type ShouldSyncCollectionParams = {
105
+ export type ShouldSyncCollectionProps = {
71
106
  collectionId: string;
72
107
  };
73
108
 
74
- export type RemoteDocumentExistenceCheckParams = {
109
+ export type RemoteDocumentExistenceCheckProps = {
75
110
  peerId: string;
76
111
  documentId: string;
77
112
  };
@@ -0,0 +1,10 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { BaseError } from '@dxos/errors';
6
+
7
+ /**
8
+ * Thrown when attempting to send a message to a peer that is not connected.
9
+ */
10
+ export class PeerNotFoundError extends BaseError.extend('PeerNotFoundError', 'Peer not found') {}
@@ -0,0 +1,62 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type DocumentId, type QueryState, type Repo } from '@automerge/automerge-repo';
6
+
7
+ /**
8
+ * Variants reported by {@link QueryState}: `'loading' | 'ready' | 'unavailable' | 'failed'`.
9
+ */
10
+ export type HandleQueryState = QueryState<unknown>['state'];
11
+
12
+ /**
13
+ * Returns the {@link QueryState} variant for a document already known to the
14
+ * repo (i.e. one for which a handle has been registered via `Repo.find` /
15
+ * `Repo.findWithProgress` / `Repo.create`), or `undefined` if no handle/query
16
+ * exists for this document yet.
17
+ *
18
+ * Use this anywhere you need to ask "is this document loaded?" / "did sync
19
+ * give up?" — read the result against the {@link HandleQueryState} variants
20
+ * (e.g. `=== 'ready'`).
21
+ *
22
+ * # Why we can't just call `DocHandle.isReady()` / read `DocHandle.state`
23
+ *
24
+ * The current subduction fork of `@automerge/automerge-repo`
25
+ * (`2.6.0-subduction.10`) **gutted** the per-handle state machine. Inside the
26
+ * fork, every `DocHandle` state predicate is a constant placeholder:
27
+ *
28
+ * ```
29
+ * isReady = () => true
30
+ * isUnloaded = () => false
31
+ * isDeleted = () => false
32
+ * isUnavailable = () => false
33
+ * inState = (states) => states.includes('ready')
34
+ * get state = () => 'ready'
35
+ * whenReady = async () => {} // resolves immediately, ignores argument
36
+ * ```
37
+ *
38
+ * That means `handle.isReady()` always lies "yes", `handle.state` always reads
39
+ * `'ready'`, and `await handle.whenReady()` never blocks — even for handles
40
+ * whose document has never been loaded from storage or synced from a peer.
41
+ * Reading `handle.doc()` on such a "ready" handle returns the empty initial
42
+ * doc (`A.init()` or `A.emptyChange(A.init())`) rather than throwing or
43
+ * returning undefined, which can silently corrupt downstream logic that
44
+ * trusted the handle's `isReady()` signal.
45
+ *
46
+ * Real liveness moved to the {@link DocumentQuery} returned by
47
+ * `Repo.findWithProgress`, whose state goes through
48
+ * `'loading' → 'ready' | 'unavailable' | 'failed'`. Production code that
49
+ * needs to know whether a document is actually loaded must consult the query
50
+ * state through this helper instead of any `DocHandle.*` predicate.
51
+ *
52
+ * TODO(mykola): Remove this helper once the fork either (a) restores the
53
+ * `DocHandle` state machine, or (b) every caller is migrated to query
54
+ * `DocumentProgress` directly with a richer API surface (await on state
55
+ * transitions, etc.).
56
+ */
57
+ export const getHandleState = (repo: Repo, documentId: DocumentId): HandleQueryState | undefined => {
58
+ if (!repo.handles[documentId]) {
59
+ return undefined;
60
+ }
61
+ return repo.findWithProgress(documentId).peek().state;
62
+ };
@@ -4,18 +4,45 @@
4
4
 
5
5
  import type { Heads } from '@automerge/automerge';
6
6
  import type { DocumentId } from '@automerge/automerge-repo';
7
+ import { type MixedEncoding } from 'level-transcoder';
7
8
 
8
- import { headsEncoding } from '@dxos/indexing';
9
+ import type { ProtoCodec } from '@dxos/codec-protobuf';
9
10
  import type { BatchLevel, SublevelDB } from '@dxos/kv-store';
11
+ import { log } from '@dxos/log';
12
+ import { schema } from '@dxos/protocols/proto';
13
+ import type { Heads as HeadsProto } from '@dxos/protocols/proto/dxos/echo/query';
10
14
 
11
- export type HeadsStoreParams = {
15
+ // NOTE: Lazy so that code that doesn't use indexing doesn't need to load the codec (breaks in workerd).
16
+ let headsCodec: ProtoCodec<HeadsProto>;
17
+ const getHeadsCodec = () => (headsCodec ??= schema.getCodecForType('dxos.echo.query.Heads'));
18
+
19
+ const headsEncoding: MixedEncoding<Heads, Uint8Array, Heads> = {
20
+ encode: (value: Heads): Uint8Array => getHeadsCodec().encode({ hashes: value }),
21
+ decode: (encodedValue: Uint8Array): Heads => {
22
+ try {
23
+ return getHeadsCodec().decode(encodedValue).hashes!;
24
+ } catch {
25
+ // Legacy encoding migration path.
26
+ log.warn('Detected legacy encoding of heads in storage.');
27
+ const concatenatedHeads = Buffer.from(encodedValue).toString('utf8').replace(/"/g, '');
28
+ const heads = [];
29
+ for (let i = 0; i < concatenatedHeads.length; i += 64) {
30
+ heads.push(concatenatedHeads.slice(i, i + 64));
31
+ }
32
+ return heads;
33
+ }
34
+ },
35
+ format: 'buffer',
36
+ };
37
+
38
+ export type HeadsStoreProps = {
12
39
  db: SublevelDB;
13
40
  };
14
41
 
15
42
  export class HeadsStore {
16
43
  private readonly _db: SublevelDB;
17
44
 
18
- constructor({ db }: HeadsStoreParams) {
45
+ constructor({ db }: HeadsStoreProps) {
19
46
  this._db = db;
20
47
  }
21
48
 
@@ -34,4 +61,16 @@ export class HeadsStore {
34
61
  valueEncoding: headsEncoding,
35
62
  });
36
63
  }
64
+
65
+ /**
66
+ * Iterate over all document IDs and their heads.
67
+ */
68
+ async *iterateAll(): AsyncGenerator<{ documentId: DocumentId; heads: Heads }> {
69
+ for await (const [documentId, heads] of this._db.iterator<DocumentId, Heads>({
70
+ keyEncoding: 'utf8',
71
+ valueEncoding: headsEncoding,
72
+ })) {
73
+ yield { documentId, heads };
74
+ }
75
+ }
37
76
  }
@@ -3,9 +3,11 @@
3
3
  //
4
4
 
5
5
  export * from './automerge-host';
6
+ export * from './handle-state';
6
7
  export * from './leveldb-storage-adapter';
7
8
  export * from './mesh-echo-replicator';
8
- export * from './echo-replicator';
9
+ export type * from './echo-replicator';
9
10
  export { diffCollectionState } from './collection-synchronizer';
10
11
  export * from './space-collection';
11
12
  export * from './echo-data-monitor';
13
+ export * from './errors';
@@ -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
 
@@ -16,27 +16,56 @@ export interface StorageAdapterDataMonitor {
16
16
  recordStoreDuration(durationMs: number): void;
17
17
  }
18
18
 
19
- export type LevelDBStorageAdapterParams = {
19
+ export type LevelDBStorageAdapterProps = {
20
20
  db: SublevelDB;
21
21
  callbacks?: StorageCallbacks;
22
22
  monitor?: StorageAdapterDataMonitor;
23
23
  };
24
24
 
25
- export type BeforeSaveParams = { path: StorageKey; batch: BatchLevel };
25
+ export type BeforeSaveProps = { path: StorageKey; batch: BatchLevel };
26
26
 
27
27
  export interface StorageCallbacks {
28
- beforeSave(params: BeforeSaveParams): MaybePromise<void>;
28
+ beforeSave(params: BeforeSaveProps): MaybePromise<void>;
29
29
  afterSave(path: StorageKey): MaybePromise<void>;
30
30
  }
31
31
 
32
32
  export class LevelDBStorageAdapter extends Resource implements StorageAdapterInterface {
33
- constructor(private readonly _params: LevelDBStorageAdapterParams) {
33
+ /**
34
+ * In-flight `loadRange` / `removeRange` iterations. Awaited in `_close` so
35
+ * the adapter doesn't return from close while a `for await` on the sublevel
36
+ * is still pending — otherwise the kv owner upstream would close the
37
+ * sublevel and the iterator's next `.next()` would reject with
38
+ * `LEVEL_ITERATOR_NOT_OPEN`.
39
+ *
40
+ * Promises stored here resolve regardless of outcome (the wrapper
41
+ * `.catch`es) so `Promise.all` won't reject.
42
+ */
43
+ readonly #inFlightIterations = new Set<Promise<unknown>>();
44
+
45
+ constructor(private readonly _params: LevelDBStorageAdapterProps) {
34
46
  super();
35
47
  }
36
48
 
49
+ protected override async _close(): Promise<void> {
50
+ // New `loadRange`/`removeRange` calls already short-circuit on `!isOpen`
51
+ // (the Resource base flips state before invoking `_close`). Wait for any
52
+ // iterations that started while we were still open to finish before
53
+ // returning, so the upstream `kv` owner can safely close the sublevel.
54
+ if (this.#inFlightIterations.size > 0) {
55
+ await Promise.all(this.#inFlightIterations);
56
+ }
57
+ }
58
+
59
+ #trackIteration<T>(work: Promise<T>): Promise<T> {
60
+ const settled = work.catch(() => undefined);
61
+ this.#inFlightIterations.add(settled);
62
+ void settled.finally(() => this.#inFlightIterations.delete(settled));
63
+ return work;
64
+ }
65
+
37
66
  async load(keyArray: StorageKey): Promise<Uint8Array | undefined> {
38
67
  try {
39
- if (this._lifecycleState !== LifecycleState.OPEN) {
68
+ if (!this.isOpen) {
40
69
  // TODO(mykola): this should be an error.
41
70
  return undefined;
42
71
  }
@@ -54,7 +83,7 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
54
83
  }
55
84
 
56
85
  async save(keyArray: StorageKey, binary: Uint8Array): Promise<void> {
57
- if (this._lifecycleState !== LifecycleState.OPEN) {
86
+ if (!this.isOpen) {
58
87
  return undefined;
59
88
  }
60
89
  const startMs = Date.now();
@@ -71,17 +100,50 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
71
100
  this._params.monitor?.recordStoreDuration(Date.now() - startMs);
72
101
  }
73
102
 
103
+ /**
104
+ * Atomically persist multiple key/value entries in a single LevelDB batch write.
105
+ */
106
+ async saveBatch(entries: Array<[StorageKey, Uint8Array]>): Promise<void> {
107
+ if (!this.isOpen || entries.length === 0) {
108
+ return undefined;
109
+ }
110
+ const startMs = Date.now();
111
+ const batch = this._params.db.batch();
112
+
113
+ for (const [keyArray] of entries) {
114
+ await this._params.callbacks?.beforeSave?.({ path: keyArray, batch });
115
+ }
116
+ for (const [keyArray, binary] of entries) {
117
+ batch.put<StorageKey, Uint8Array>(keyArray, Buffer.from(binary), {
118
+ ...encodingOptions,
119
+ });
120
+ }
121
+ await batch.write();
122
+
123
+ let bytesStored = 0;
124
+ for (const [keyArray, binary] of entries) {
125
+ bytesStored += binary.byteLength;
126
+ await this._params.callbacks?.afterSave?.(keyArray);
127
+ }
128
+ this._params.monitor?.recordBytesStored(bytesStored);
129
+ this._params.monitor?.recordStoreDuration(Date.now() - startMs);
130
+ }
131
+
74
132
  async remove(keyArray: StorageKey): Promise<void> {
75
- if (this._lifecycleState !== LifecycleState.OPEN) {
133
+ if (!this.isOpen) {
76
134
  return undefined;
77
135
  }
78
136
  await this._params.db.del<StorageKey>(keyArray, { ...encodingOptions });
79
137
  }
80
138
 
81
139
  async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
82
- if (this._lifecycleState !== LifecycleState.OPEN) {
140
+ if (!this.isOpen) {
83
141
  return [];
84
142
  }
143
+ return this.#trackIteration(this.#doLoadRange(keyPrefix));
144
+ }
145
+
146
+ async #doLoadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
85
147
  const startMs = Date.now();
86
148
  const result: Chunk[] = [];
87
149
  for await (const [key, value] of this._params.db.iterator<StorageKey, Uint8Array>({
@@ -100,9 +162,13 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
100
162
  }
101
163
 
102
164
  async removeRange(keyPrefix: StorageKey): Promise<void> {
103
- if (this._lifecycleState !== LifecycleState.OPEN) {
165
+ if (!this.isOpen) {
104
166
  return undefined;
105
167
  }
168
+ await this.#trackIteration(this.#doRemoveRange(keyPrefix));
169
+ }
170
+
171
+ async #doRemoveRange(keyPrefix: StorageKey): Promise<void> {
106
172
  const batch = this._params.db.batch();
107
173
 
108
174
  for await (const [key] of this._params.db.iterator<StorageKey, Uint8Array>({
@@ -12,20 +12,20 @@ import { log } from '@dxos/log';
12
12
  import type { AutomergeProtocolMessage } from '@dxos/protocols';
13
13
  import { AutomergeReplicator, type AutomergeReplicatorFactory } from '@dxos/teleport-extension-automerge-replicator';
14
14
 
15
- import type { ReplicatorConnection, ShouldAdvertiseParams, ShouldSyncCollectionParams } from './echo-replicator';
15
+ import type { AutomergeReplicatorConnection, ShouldAdvertiseProps, ShouldSyncCollectionProps } from './echo-replicator';
16
16
 
17
17
  const DEFAULT_FACTORY: AutomergeReplicatorFactory = (params) => new AutomergeReplicator(...params);
18
18
 
19
- export type MeshReplicatorConnectionParams = {
19
+ export type MeshReplicatorConnectionProps = {
20
20
  ownPeerId: string;
21
21
  onRemoteConnected: () => void;
22
22
  onRemoteDisconnected: () => void;
23
- shouldAdvertise: (params: ShouldAdvertiseParams) => Promise<boolean>;
24
- shouldSyncCollection: (params: ShouldSyncCollectionParams) => boolean;
23
+ shouldAdvertise: (params: ShouldAdvertiseProps) => Promise<boolean>;
24
+ shouldSyncCollection: (params: ShouldSyncCollectionProps) => boolean;
25
25
  replicatorFactory?: AutomergeReplicatorFactory;
26
26
  };
27
27
 
28
- export class MeshReplicatorConnection extends Resource implements ReplicatorConnection {
28
+ export class MeshReplicatorConnection extends Resource implements AutomergeReplicatorConnection {
29
29
  public readable: ReadableStream<AutomergeProtocolMessage>;
30
30
  public writable: WritableStream<AutomergeProtocolMessage>;
31
31
  public remoteDeviceKey: PublicKey | null = null;
@@ -35,7 +35,7 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
35
35
  private _remotePeerId: string | null = null;
36
36
  private _isEnabled = false;
37
37
 
38
- constructor(private readonly _params: MeshReplicatorConnectionParams) {
38
+ constructor(private readonly _params: MeshReplicatorConnectionProps) {
39
39
  super();
40
40
 
41
41
  let readableStreamController!: ReadableStreamDefaultController<AutomergeProtocolMessage>;
@@ -111,11 +111,15 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
111
111
  return this._isEnabled;
112
112
  }
113
113
 
114
- async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
114
+ get bundleSyncEnabled(): boolean {
115
+ return false;
116
+ }
117
+
118
+ async shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean> {
115
119
  return this._params.shouldAdvertise(params);
116
120
  }
117
121
 
118
- shouldSyncCollection(params: ShouldSyncCollectionParams): boolean {
122
+ shouldSyncCollection(params: ShouldSyncCollectionProps): boolean {
119
123
  return this._params.shouldSyncCollection(params);
120
124
  }
121
125
 
@@ -2,27 +2,29 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Context } from '@dxos/context';
5
6
  import type { CollectionId } from '@dxos/echo-protocol';
6
7
  import { invariant } from '@dxos/invariant';
7
8
  import { PublicKey, type SpaceId } from '@dxos/keys';
8
9
  import { log } from '@dxos/log';
9
- import {
10
- type AutomergeReplicator,
11
- type AutomergeReplicatorFactory,
12
- } from '@dxos/teleport-extension-automerge-replicator';
10
+ import type * as TeleportAutomergeReplicator from '@dxos/teleport-extension-automerge-replicator';
13
11
  import { ComplexSet, defaultMap } from '@dxos/util';
14
12
 
15
- import { type EchoReplicator, type EchoReplicatorContext, type ShouldAdvertiseParams } from './echo-replicator';
13
+ import { createIdFromSpaceKey } from '../common/space-id';
14
+ import {
15
+ type AutomergeReplicator,
16
+ type AutomergeReplicatorContext,
17
+ type ShouldAdvertiseProps,
18
+ } from './echo-replicator';
16
19
  import { MeshReplicatorConnection } from './mesh-echo-replicator-connection';
17
20
  import { getSpaceIdFromCollectionId } from './space-collection';
18
- import { createIdFromSpaceKey } from '../common/space-id';
19
21
 
20
22
  // TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
21
23
 
22
24
  /**
23
25
  * Used to replicate with other peers over the network.
24
26
  */
25
- export class MeshEchoReplicator implements EchoReplicator {
27
+ export class MeshEchoReplicator implements AutomergeReplicator {
26
28
  /**
27
29
  * We might have multiple connections open with a peer (one per space), but there'll be only one enabled
28
30
  * connection at any given moment, because there's a single repo for all the spaces.
@@ -40,9 +42,9 @@ export class MeshEchoReplicator implements EchoReplicator {
40
42
  */
41
43
  private readonly _authorizedDevices = new Map<SpaceId, ComplexSet<PublicKey>>();
42
44
 
43
- private _context: EchoReplicatorContext | null = null;
45
+ private _context: AutomergeReplicatorContext | null = null;
44
46
 
45
- async connect(context: EchoReplicatorContext): Promise<void> {
47
+ async connect(_ctx: Context, context: AutomergeReplicatorContext): Promise<void> {
46
48
  this._context = context;
47
49
  }
48
50
 
@@ -63,7 +65,9 @@ export class MeshEchoReplicator implements EchoReplicator {
63
65
  this._context = null;
64
66
  }
65
67
 
66
- createExtension(extensionFactory?: AutomergeReplicatorFactory): AutomergeReplicator {
68
+ createExtension(
69
+ extensionFactory?: TeleportAutomergeReplicator.AutomergeReplicatorFactory,
70
+ ): TeleportAutomergeReplicator.AutomergeReplicator {
67
71
  invariant(this._context);
68
72
 
69
73
  const connection: MeshReplicatorConnection = new MeshReplicatorConnection({
@@ -110,7 +114,7 @@ export class MeshEchoReplicator implements EchoReplicator {
110
114
  }
111
115
  }
112
116
  },
113
- shouldAdvertise: async (params: ShouldAdvertiseParams) => {
117
+ shouldAdvertise: async (params: ShouldAdvertiseProps) => {
114
118
  log('shouldAdvertise', { peerId: connection.peerId, documentId: params.documentId });
115
119
  invariant(this._context);
116
120
  try {
@@ -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);