@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
@@ -0,0 +1,330 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Repo, generateAutomergeUrl, initSubduction, parseAutomergeUrl } from '@automerge/automerge-repo';
6
+ import {
7
+ AuthenticatedTransport,
8
+ BlobMeta,
9
+ CommitId,
10
+ Digest,
11
+ MemorySigner,
12
+ MemoryStorage,
13
+ Nonce,
14
+ SedimentreeId,
15
+ Subduction,
16
+ commitIdOfBase58Id,
17
+ } from '@automerge/automerge-subduction';
18
+ import { beforeAll, describe, test } from 'vitest';
19
+
20
+ import { sleep } from '@dxos/async';
21
+
22
+ class AsyncQueue<T> {
23
+ private _items: T[] = [];
24
+ private _waiters: ((item: T) => void)[] = [];
25
+
26
+ push(item: T): void {
27
+ const waiter = this._waiters.shift();
28
+ if (waiter) {
29
+ waiter(item);
30
+ } else {
31
+ this._items.push(item);
32
+ }
33
+ }
34
+
35
+ async pull(): Promise<T> {
36
+ const item = this._items.shift();
37
+ if (item !== undefined) {
38
+ return item;
39
+ }
40
+ return new Promise<T>((resolve) => this._waiters.push(resolve));
41
+ }
42
+
43
+ clear(): void {
44
+ this._items.length = 0;
45
+ this._waiters.length = 0;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Pure-JS transport backed by async queues.
51
+ * Implements the subduction Transport interface for in-memory testing.
52
+ */
53
+ class MemoryTransport {
54
+ private _outBytes: AsyncQueue<Uint8Array>;
55
+ private _inBytes: AsyncQueue<Uint8Array>;
56
+ private _disconnectCallbacks: (() => void)[] = [];
57
+
58
+ constructor(outBytes: AsyncQueue<Uint8Array>, inBytes: AsyncQueue<Uint8Array>) {
59
+ this._outBytes = outBytes;
60
+ this._inBytes = inBytes;
61
+ }
62
+
63
+ async sendBytes(bytes: Uint8Array): Promise<void> {
64
+ this._outBytes.push(new Uint8Array(bytes));
65
+ }
66
+
67
+ async recvBytes(): Promise<Uint8Array> {
68
+ return this._inBytes.pull();
69
+ }
70
+
71
+ async disconnect(): Promise<void> {
72
+ for (const callback of this._disconnectCallbacks) {
73
+ callback();
74
+ }
75
+ this._inBytes.clear();
76
+ this._outBytes.clear();
77
+ }
78
+
79
+ onDisconnect(callback: () => void): void {
80
+ this._disconnectCallbacks.push(callback);
81
+ }
82
+ }
83
+
84
+ const createMemoryTransportPair = (): [MemoryTransport, MemoryTransport] => {
85
+ const bytesAB = new AsyncQueue<Uint8Array>();
86
+ const bytesBA = new AsyncQueue<Uint8Array>();
87
+ return [new MemoryTransport(bytesAB, bytesBA), new MemoryTransport(bytesBA, bytesAB)];
88
+ };
89
+
90
+ /**
91
+ * Build a deterministic {@link CommitId} seeded by a small integer. Values are arbitrary;
92
+ * used in tests only to satisfy the `head` parameter of `Subduction.addCommit`.
93
+ */
94
+ const commitIdOf = (seed: number): CommitId => CommitId.fromBytes(new Uint8Array(32).fill(seed));
95
+
96
+ describe('automerge-subduction', () => {
97
+ beforeAll(async () => {
98
+ await initSubduction();
99
+ });
100
+
101
+ test('creates signer and signs payloads', async ({ expect }) => {
102
+ const signer = MemorySigner.generate();
103
+ const signature = await signer.sign(new Uint8Array([1, 2, 3]));
104
+ const verifyingKey = signer.verifyingKey();
105
+ const peerId = signer.peerId();
106
+
107
+ expect(signature).toBeInstanceOf(Uint8Array);
108
+ expect(signature.length).toBe(64);
109
+ expect(verifyingKey).toBeInstanceOf(Uint8Array);
110
+ expect(verifyingKey.length).toBe(32);
111
+ expect(peerId.toString().length).toBeGreaterThan(0);
112
+ });
113
+
114
+ test('round-trips digest bytes and hex', ({ expect }) => {
115
+ const digestBytes = new Uint8Array(32).fill(2);
116
+ const digest = Digest.fromBytes(digestBytes);
117
+ const fromHex = Digest.fromHexString(digest.toHexString());
118
+
119
+ expect(digest.toBytes()).toEqual(digestBytes);
120
+ expect(digest.toHexString()).toHaveLength(64);
121
+ expect(fromHex.toBytes()).toEqual(digestBytes);
122
+ });
123
+
124
+ test('nonce random bytes are fixed-width', ({ expect }) => {
125
+ const nonce = Nonce.random();
126
+ expect(nonce.bytes.length).toBe(8);
127
+ });
128
+
129
+ test('blob meta reports digest and size', ({ expect }) => {
130
+ const blobMeta = new BlobMeta(new Uint8Array([5, 6, 7, 8]));
131
+ expect(blobMeta.sizeBytes).toBe(4n);
132
+ expect(blobMeta.digest().toBytes().length).toBe(32);
133
+ });
134
+
135
+ test('stores and loads sedimentree IDs in memory storage', async ({ expect }) => {
136
+ const storage = new MemoryStorage();
137
+ const id = SedimentreeId.fromBytes(new Uint8Array(32).fill(7));
138
+
139
+ await storage.saveSedimentreeId(id);
140
+ const ids = await storage.loadAllSedimentreeIds();
141
+
142
+ expect(ids).toHaveLength(1);
143
+ expect(ids[0].toBytes()).toEqual(id.toBytes());
144
+ });
145
+
146
+ test('adds a commit and exposes it through subduction read APIs', async ({ expect }) => {
147
+ const signer = MemorySigner.generate();
148
+ const storage = new MemoryStorage();
149
+ const subduction = new Subduction(signer, storage);
150
+ const id = SedimentreeId.fromBytes(new Uint8Array(32).fill(9));
151
+ const blob = new Uint8Array([10, 11, 12]);
152
+
153
+ const requestedFragment = await subduction.addCommit(id, commitIdOf(1), [], blob);
154
+ const commits = await subduction.getCommits(id);
155
+ const blobs = await subduction.getBlobs(id);
156
+
157
+ expect(requestedFragment).toBeUndefined();
158
+ expect(commits).toBeDefined();
159
+ expect(commits).toHaveLength(1);
160
+ expect(blobs).toHaveLength(1);
161
+ expect(blobs[0]).toEqual(blob);
162
+ });
163
+
164
+ test('throws on invalid base58 input', ({ expect }) => {
165
+ expect(() => commitIdOfBase58Id('contains-0')).toThrow();
166
+ });
167
+
168
+ test('automerge-repo URL helpers round-trip document IDs', ({ expect }) => {
169
+ const url = generateAutomergeUrl();
170
+ const parsed = parseAutomergeUrl(url);
171
+
172
+ expect(parsed.documentId.length).toBeGreaterThan(0);
173
+ expect(`automerge:${parsed.documentId}`).toBe(url);
174
+ });
175
+
176
+ test('automerge-repo creates documents with subduction signer', async ({ expect }) => {
177
+ const signer = MemorySigner.generate();
178
+ const repo = new Repo({
179
+ network: [],
180
+ signer,
181
+ });
182
+
183
+ const handle = repo.create<{ title?: string }>();
184
+ handle.change((doc) => {
185
+ doc.title = 'hello';
186
+ });
187
+
188
+ expect(handle.doc()?.title).toBe('hello');
189
+ await repo.shutdown();
190
+ });
191
+
192
+ test('establishes subduction connection and syncs via AuthenticatedTransport', async ({ expect }) => {
193
+ const signerA = MemorySigner.generate();
194
+ const signerB = MemorySigner.generate();
195
+ const subductionA = new Subduction(signerA, new MemoryStorage());
196
+ const subductionB = new Subduction(signerB, new MemoryStorage());
197
+
198
+ const [transportA, transportB] = createMemoryTransportPair();
199
+
200
+ const [authA, authB] = await Promise.all([
201
+ AuthenticatedTransport.setup(transportA, signerA, signerB.peerId()),
202
+ AuthenticatedTransport.accept(transportB, signerB),
203
+ ]);
204
+
205
+ await subductionA.addConnection(authA);
206
+ await subductionB.addConnection(authB);
207
+
208
+ const peersA = await subductionA.getConnectedPeerIds();
209
+ const peersB = await subductionB.getConnectedPeerIds();
210
+ expect(peersA).toHaveLength(1);
211
+ expect(peersB).toHaveLength(1);
212
+
213
+ const sid = SedimentreeId.fromBytes(new Uint8Array(32).fill(42));
214
+ await subductionA.addCommit(sid, commitIdOf(2), [], new Uint8Array([1, 2, 3]));
215
+
216
+ const result = await subductionA.syncWithAllPeers(sid, true);
217
+ expect(result.entries().length).toBeGreaterThan(0);
218
+
219
+ const blobsOnB = await subductionB.getBlobs(sid);
220
+ expect(blobsOnB).toHaveLength(1);
221
+ }, 10_000);
222
+
223
+ test('syncs between two subduction instances using connectTransport', async ({ expect }) => {
224
+ const signerA = MemorySigner.generate();
225
+ const signerB = MemorySigner.generate();
226
+ const subductionA = new Subduction(signerA, new MemoryStorage(), 'test-service');
227
+ const subductionB = new Subduction(signerB, new MemoryStorage(), 'test-service');
228
+
229
+ const [transportA, transportB] = createMemoryTransportPair();
230
+
231
+ const [peerIdB, peerIdA] = await Promise.all([
232
+ subductionA.connectTransport(transportA, 'test-service'),
233
+ subductionB.acceptTransport(transportB, 'test-service'),
234
+ ]);
235
+
236
+ expect(peerIdB.toString()).toBe(signerB.peerId().toString());
237
+ expect(peerIdA.toString()).toBe(signerA.peerId().toString());
238
+
239
+ const sid = SedimentreeId.fromBytes(new Uint8Array(32).fill(99));
240
+ await subductionA.addCommit(sid, commitIdOf(3), [], new Uint8Array([4, 5, 6]));
241
+
242
+ await subductionA.syncWithAllPeers(sid, false);
243
+
244
+ const blobsOnB = await subductionB.getBlobs(sid);
245
+ expect(blobsOnB).toHaveLength(1);
246
+ expect(blobsOnB[0]).toEqual(new Uint8Array([4, 5, 6]));
247
+ }, 10_000);
248
+
249
+ test('full sync exchanges all sedimentrees between peers', async ({ expect }) => {
250
+ const signerA = MemorySigner.generate();
251
+ const signerB = MemorySigner.generate();
252
+ const subductionA = new Subduction(signerA, new MemoryStorage());
253
+ const subductionB = new Subduction(signerB, new MemoryStorage());
254
+
255
+ // Add commits on both sides before connecting.
256
+ const sidA = SedimentreeId.fromBytes(new Uint8Array(32).fill(1));
257
+ const sidB = SedimentreeId.fromBytes(new Uint8Array(32).fill(2));
258
+ await subductionA.addCommit(sidA, commitIdOf(5), [], new Uint8Array([10, 20]));
259
+ await subductionB.addCommit(sidB, commitIdOf(6), [], new Uint8Array([30, 40]));
260
+
261
+ const [transportA, transportB] = createMemoryTransportPair();
262
+ const [authA, authB] = await Promise.all([
263
+ AuthenticatedTransport.setup(transportA, signerA, signerB.peerId()),
264
+ AuthenticatedTransport.accept(transportB, signerB),
265
+ ]);
266
+ await subductionA.addConnection(authA);
267
+ await subductionB.addConnection(authB);
268
+
269
+ // Full sync exchanges everything.
270
+ await subductionA.fullSyncWithPeer(signerB.peerId());
271
+ await subductionB.fullSyncWithPeer(signerA.peerId());
272
+
273
+ const blobsAonB = await subductionB.getBlobs(sidA);
274
+ const blobsBonA = await subductionA.getBlobs(sidB);
275
+ expect(blobsAonB).toHaveLength(1);
276
+ expect(blobsAonB[0]).toEqual(new Uint8Array([10, 20]));
277
+ expect(blobsBonA).toHaveLength(1);
278
+ expect(blobsBonA[0]).toEqual(new Uint8Array([30, 40]));
279
+ }, 10_000);
280
+
281
+ // After B drops handshake state and rehydrates from durable storage, does
282
+ // subduction re-handshake on the reused logical channel when A pushes?
283
+ // Empirical answer: no — A's auto-broadcast does not reach B'.
284
+ test(
285
+ 'does not auto-rehydrate handshake on a logical connection where one end dropped it',
286
+ { timeout: 15_000 },
287
+ async ({ expect }) => {
288
+ const signerA = MemorySigner.generate();
289
+ const signerB = MemorySigner.generate();
290
+ const storageA = new MemoryStorage();
291
+ const storageB = new MemoryStorage();
292
+ const [transportA, transportB] = createMemoryTransportPair();
293
+
294
+ const subA = new Subduction(signerA, storageA, 'svc');
295
+ let subB = new Subduction(signerB, storageB, 'svc');
296
+
297
+ const [authA, authB] = await Promise.all([
298
+ AuthenticatedTransport.setup(transportA, signerA, signerB.peerId()),
299
+ AuthenticatedTransport.accept(transportB, signerB),
300
+ ]);
301
+ await subA.addConnection(authA);
302
+ await subB.addConnection(authB);
303
+
304
+ const sid = SedimentreeId.fromBytes(new Uint8Array(32).fill(7));
305
+ await subA.addCommit(sid, commitIdOf(1), [], new Uint8Array([1, 2, 3]));
306
+ await expect.poll(() => subB.getBlobs(sid).then((bs) => bs.length), { timeout: 5_000 }).toEqual(1);
307
+
308
+ // Simulate B crash: clearing the queue's waiters halts the orphaned
309
+ // wasm pump that survives `free()`. Do NOT use `disconnectAll` /
310
+ // `disconnectFromPeer` — those send a graceful goodbye A would observe.
311
+ await transportB.disconnect();
312
+ subB.free();
313
+
314
+ subB = await Subduction.hydrate(signerB, storageB, 'svc');
315
+ expect(await subB.getBlobs(sid)).toHaveLength(1);
316
+ expect(await subB.getConnectedPeerIds()).toHaveLength(0);
317
+ expect(await subA.getConnectedPeerIds()).toHaveLength(1);
318
+
319
+ await subA.addCommit(sid, commitIdOf(2), [commitIdOf(1)], new Uint8Array([4, 5, 6]));
320
+ await sleep(500);
321
+
322
+ // TODO(mykola): When subduction-core grows channel-level handshake
323
+ // recovery, flip to `.toHaveLength(2)` and drop "does not" from name.
324
+ expect(await subB.getBlobs(sid)).toHaveLength(1);
325
+
326
+ subA.free();
327
+ subB.free();
328
+ },
329
+ );
330
+ });
@@ -2,15 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { PeerId } from '@automerge/automerge-repo';
6
- import { onTestFinished, describe, expect, test } from 'vitest';
5
+ import * as A from '@automerge/automerge';
6
+ import type { DocumentId, PeerId } from '@automerge/automerge-repo';
7
+ import { describe, onTestFinished, test } from 'vitest';
7
8
 
8
9
  import { sleep } from '@dxos/async';
10
+ import { range } from '@dxos/util';
9
11
 
10
- import { CollectionSynchronizer, diffCollectionState, type CollectionState } from './collection-synchronizer';
12
+ import { type CollectionState, CollectionSynchronizer, diffCollectionState } from './collection-synchronizer';
11
13
 
12
14
  describe('CollectionSynchronizer', () => {
13
- test('sync two peers', async () => {
15
+ test('sync two peers', async ({ expect }) => {
14
16
  const LATENCY = 10;
15
17
 
16
18
  const peerId1 = 'peer1' as PeerId;
@@ -53,22 +55,99 @@ describe('CollectionSynchronizer', () => {
53
55
  peer1.onConnectionOpen(peerId2);
54
56
  peer2.onConnectionOpen(peerId1);
55
57
 
58
+ const updated = Promise.all([
59
+ peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
60
+ peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
61
+ ]);
62
+
56
63
  peer1.setLocalCollectionState(collectionId, STATE_1);
57
64
  peer2.setLocalCollectionState(collectionId, STATE_2);
58
65
 
59
66
  peer1.refreshCollection(collectionId);
60
67
  peer2.refreshCollection(collectionId);
61
68
 
62
- await Promise.all([
63
- peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
64
- peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
65
- ]);
69
+ await updated;
66
70
 
67
71
  expect(peer1.getRemoteCollectionStates(collectionId).get(peerId2)).to.deep.equal(STATE_2);
68
72
  expect(peer2.getRemoteCollectionStates(collectionId).get(peerId1)).to.deep.equal(STATE_1);
69
73
  });
70
74
 
71
- test('diff collection state', () => {
75
+ test('pushes state to all interested peers on setLocalCollectionState', async ({ expect }) => {
76
+ // Push covers peers that queried us before we had state — `onCollectionStateQueried`
77
+ // silently drops that case; without this push the asker would wait up to POLL_INTERVAL
78
+ // for its polling loop to retry (further gated by MIN_QUERY_INTERVAL).
79
+ const peerId1 = 'peer1' as PeerId;
80
+ const peerId2 = 'peer2' as PeerId;
81
+ const collectionId = 'collection-test';
82
+
83
+ const sentStates: Array<{ peerId: PeerId; state: CollectionState }> = [];
84
+ const peer = await new CollectionSynchronizer({
85
+ queryCollectionState: () => {},
86
+ sendCollectionState: (_collectionId, peerId, state) => {
87
+ sentStates.push({ peerId, state });
88
+ },
89
+ shouldSyncCollection: () => true,
90
+ }).open();
91
+ onTestFinished(async () => {
92
+ await peer.close();
93
+ });
94
+
95
+ peer.onConnectionOpen(peerId1);
96
+ peer.onConnectionOpen(peerId2);
97
+
98
+ peer.setLocalCollectionState(collectionId, STATE_1);
99
+ await sleep(10);
100
+
101
+ expect(sentStates.map((m) => m.peerId).sort()).to.deep.equal([peerId1, peerId2].sort());
102
+ expect(sentStates.every((m) => m.state === STATE_1)).to.equal(true);
103
+ });
104
+
105
+ test('does not send to a peer after onConnectionClosed', async ({ expect }) => {
106
+ // Regression: `onConnectionClosed` must remove the peer from `interestedPeers`,
107
+ // otherwise the next `_broadcastLocalState` calls `sendCollectionState` for a
108
+ // now-stale peerId and the real network adapter throws "Connection not found.".
109
+ const peerId1 = 'peer1' as PeerId;
110
+ const peerId2 = 'peer2' as PeerId;
111
+ const collectionId = 'collection-test';
112
+
113
+ const connected = new Set<PeerId>([peerId1, peerId2]);
114
+ const sentTo: PeerId[] = [];
115
+ const peer = await new CollectionSynchronizer({
116
+ queryCollectionState: () => {},
117
+ sendCollectionState: (_collectionId, peerId) => {
118
+ if (!connected.has(peerId)) {
119
+ throw new Error('Connection not found.');
120
+ }
121
+ sentTo.push(peerId);
122
+ },
123
+ shouldSyncCollection: () => true,
124
+ }).open();
125
+ onTestFinished(async () => {
126
+ await peer.close();
127
+ });
128
+
129
+ // Activate the collection before any peer connects so the connect-time
130
+ // microtask adds peers to `interestedPeers` without ever calling
131
+ // `_broadcastLocalState` (so `lastBroadcast` stays unset for them).
132
+ peer.setLocalCollectionState(collectionId, STATE_1);
133
+ await sleep(10);
134
+
135
+ peer.onConnectionOpen(peerId1);
136
+ peer.onConnectionOpen(peerId2);
137
+ await sleep(10);
138
+
139
+ connected.delete(peerId2);
140
+ peer.onConnectionClosed(peerId2);
141
+
142
+ // Without the fix, peer2 is still in `interestedPeers` with no `lastBroadcast`
143
+ // entry, so the broadcast gate passes and `sendCollectionState` throws.
144
+ peer.setLocalCollectionState(collectionId, STATE_2);
145
+ await sleep(10);
146
+
147
+ expect(sentTo).to.deep.equal([peerId1]);
148
+ });
149
+
150
+ test('diff collection state', ({ expect }) => {
72
151
  const diff = diffCollectionState(STATE_1, STATE_2);
73
152
 
74
153
  expect(diff).to.deep.equal({
@@ -79,18 +158,20 @@ describe('CollectionSynchronizer', () => {
79
158
  });
80
159
  });
81
160
 
161
+ const TEST_HEADS = range(4).map((i) => A.getHeads(A.from({ i: i.toString() })));
162
+
82
163
  const STATE_1: CollectionState = {
83
164
  documents: {
84
- a: ['1'],
85
- b: ['2'],
86
- c: ['3'],
87
- },
165
+ a: TEST_HEADS[0],
166
+ b: TEST_HEADS[1],
167
+ c: TEST_HEADS[2],
168
+ } as Record<DocumentId, A.Heads>,
88
169
  };
89
170
 
90
171
  const STATE_2: CollectionState = {
91
172
  documents: {
92
- a: ['1'],
93
- b: ['4'],
94
- d: ['3'],
95
- },
173
+ a: TEST_HEADS[0],
174
+ b: TEST_HEADS[3],
175
+ d: TEST_HEADS[2],
176
+ } as Record<DocumentId, A.Heads>,
96
177
  };