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

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 (239) hide show
  1. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-VIHYDCCC.mjs} +313 -709
  2. package/dist/lib/neutral/chunk-VIHYDCCC.mjs.map +7 -0
  3. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs +247 -0
  4. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/neutral/index.mjs +7065 -0
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  12. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  13. package/dist/types/src/automerge/automerge-host.d.ts +99 -54
  14. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  15. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  18. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  19. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  20. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  21. package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
  22. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  23. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  24. package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
  25. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-replicator.d.ts +38 -15
  27. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  28. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  29. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  30. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  31. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  32. package/dist/types/src/automerge/index.d.ts +2 -1
  33. package/dist/types/src/automerge/index.d.ts.map +1 -1
  34. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +9 -5
  35. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  36. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  37. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  38. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  39. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  40. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  41. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  42. package/dist/types/src/common/codec.d.ts +1 -1
  43. package/dist/types/src/common/codec.d.ts.map +1 -1
  44. package/dist/types/src/common/feeds.d.ts.map +1 -1
  45. package/dist/types/src/common/space-id.d.ts.map +1 -1
  46. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  47. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  48. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  49. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  50. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  51. package/dist/types/src/db-host/data-service.d.ts +8 -7
  52. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  53. package/dist/types/src/db-host/database-root.d.ts +9 -3
  54. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  55. package/dist/types/src/db-host/documents-synchronizer.d.ts +9 -8
  56. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  57. package/dist/types/src/db-host/echo-host.d.ts +62 -27
  58. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  59. package/dist/types/src/db-host/index.d.ts +2 -0
  60. package/dist/types/src/db-host/index.d.ts.map +1 -1
  61. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  62. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  63. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  64. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  65. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  66. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  67. package/dist/types/src/db-host/query-service.d.ts +19 -11
  68. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  69. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  70. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  71. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  72. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  73. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  74. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  75. package/dist/types/src/db-host/stub.d.ts +11 -0
  76. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  77. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  78. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  79. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  80. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  82. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  83. package/dist/types/src/edge/index.d.ts +1 -0
  84. package/dist/types/src/edge/index.d.ts.map +1 -1
  85. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  86. package/dist/types/src/filter/filter-match.d.ts +4 -1
  87. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  88. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  89. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  90. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  91. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  92. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  93. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  94. package/dist/types/src/query/errors.d.ts +22 -10
  95. package/dist/types/src/query/errors.d.ts.map +1 -1
  96. package/dist/types/src/query/plan.d.ts +66 -8
  97. package/dist/types/src/query/plan.d.ts.map +1 -1
  98. package/dist/types/src/query/query-executor.d.ts +66 -11
  99. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  100. package/dist/types/src/query/query-planner.d.ts +12 -1
  101. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  102. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  103. package/dist/types/src/space/auth.d.ts +3 -3
  104. package/dist/types/src/space/auth.d.ts.map +1 -1
  105. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  106. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  107. package/dist/types/src/space/space-manager.d.ts +8 -8
  108. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  109. package/dist/types/src/space/space-protocol.d.ts +10 -9
  110. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  111. package/dist/types/src/space/space.d.ts +6 -6
  112. package/dist/types/src/space/space.d.ts.map +1 -1
  113. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  114. package/dist/types/src/testing/index.d.ts +0 -2
  115. package/dist/types/src/testing/index.d.ts.map +1 -1
  116. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  117. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  118. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  119. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  120. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  121. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  122. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  123. package/dist/types/src/util.d.ts +1 -1
  124. package/dist/types/src/util.d.ts.map +1 -1
  125. package/dist/types/tsconfig.tsbuildinfo +1 -1
  126. package/package.json +56 -54
  127. package/src/automerge/automerge-host-subduction.test.ts +247 -0
  128. package/src/automerge/automerge-host.test.ts +65 -39
  129. package/src/automerge/automerge-host.ts +589 -206
  130. package/src/automerge/automerge-repo-subduction.test.ts +1049 -0
  131. package/src/automerge/automerge-repo.test.ts +349 -91
  132. package/src/automerge/automerge-subduction.test.ts +330 -0
  133. package/src/automerge/collection-synchronizer.test.ts +53 -17
  134. package/src/automerge/collection-synchronizer.ts +119 -36
  135. package/src/automerge/echo-data-monitor.ts +1 -1
  136. package/src/automerge/echo-network-adapter.test.ts +5 -4
  137. package/src/automerge/echo-network-adapter.ts +74 -30
  138. package/src/automerge/echo-replicator.ts +41 -15
  139. package/src/automerge/handle-state.ts +62 -0
  140. package/src/automerge/heads-store.ts +42 -3
  141. package/src/automerge/index.ts +2 -1
  142. package/src/automerge/leveldb-storage-adapter.ts +40 -11
  143. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  144. package/src/automerge/mesh-echo-replicator.ts +15 -11
  145. package/src/automerge/storage-adapter.test.ts +1 -1
  146. package/src/common/space-id.ts +1 -1
  147. package/src/db-host/automerge-data-source.test.ts +299 -0
  148. package/src/db-host/automerge-data-source.ts +187 -0
  149. package/src/db-host/data-service.ts +25 -26
  150. package/src/db-host/database-root.ts +20 -8
  151. package/src/db-host/documents-synchronizer.test.ts +143 -14
  152. package/src/db-host/documents-synchronizer.ts +72 -55
  153. package/src/db-host/echo-host.ts +348 -96
  154. package/src/db-host/index.ts +2 -0
  155. package/src/db-host/invalidation-hint.ts +50 -0
  156. package/src/db-host/local-queue-service.ts +127 -0
  157. package/src/db-host/query-invalidation.test.ts +270 -0
  158. package/src/db-host/query-service.ts +111 -106
  159. package/src/db-host/queue-data-source.ts +160 -0
  160. package/src/db-host/queue-service.test.ts +145 -0
  161. package/src/db-host/space-state-manager.ts +12 -10
  162. package/src/db-host/stub.ts +33 -0
  163. package/src/edge/echo-edge-replicator.test.ts +12 -10
  164. package/src/edge/echo-edge-replicator.ts +144 -58
  165. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  166. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  167. package/src/edge/index.ts +1 -0
  168. package/src/filter/filter-match.test.ts +68 -32
  169. package/src/filter/filter-match.ts +184 -20
  170. package/src/metadata/metadata-store.ts +11 -8
  171. package/src/pipeline/pipeline-stress.test.ts +1 -1
  172. package/src/pipeline/pipeline.test.ts +2 -2
  173. package/src/pipeline/pipeline.ts +9 -7
  174. package/src/query/errors.ts +3 -1
  175. package/src/query/plan.ts +82 -8
  176. package/src/query/query-executor.ts +1199 -164
  177. package/src/query/query-planner.test.ts +993 -81
  178. package/src/query/query-planner.ts +467 -27
  179. package/src/space/admission-discovery-extension.ts +3 -3
  180. package/src/space/auth.ts +6 -6
  181. package/src/space/control-pipeline.test.ts +5 -4
  182. package/src/space/control-pipeline.ts +16 -13
  183. package/src/space/space-manager.browser.test.ts +1 -1
  184. package/src/space/space-manager.ts +16 -20
  185. package/src/space/space-protocol.browser.test.ts +15 -14
  186. package/src/space/space-protocol.test.ts +15 -14
  187. package/src/space/space-protocol.ts +22 -18
  188. package/src/space/space.test.ts +3 -3
  189. package/src/space/space.ts +11 -11
  190. package/src/testing/index.ts +0 -2
  191. package/src/testing/test-agent-builder.ts +3 -3
  192. package/src/testing/test-network-adapter.ts +24 -3
  193. package/src/testing/test-replicator.ts +19 -20
  194. package/src/util.ts +7 -3
  195. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  196. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  197. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  198. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  199. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  200. package/dist/lib/browser/index.mjs +0 -4743
  201. package/dist/lib/browser/index.mjs.map +0 -7
  202. package/dist/lib/browser/meta.json +0 -1
  203. package/dist/lib/browser/testing/index.mjs.map +0 -7
  204. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  205. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  206. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  207. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  208. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  209. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  210. package/dist/lib/node/filter/index.cjs +0 -32
  211. package/dist/lib/node/filter/index.cjs.map +0 -7
  212. package/dist/lib/node/index.cjs +0 -4699
  213. package/dist/lib/node/index.cjs.map +0 -7
  214. package/dist/lib/node/meta.json +0 -1
  215. package/dist/lib/node/testing/index.cjs +0 -753
  216. package/dist/lib/node/testing/index.cjs.map +0 -7
  217. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  218. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  219. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  220. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  221. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  222. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  223. package/dist/lib/node-esm/filter/index.mjs +0 -11
  224. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  225. package/dist/lib/node-esm/index.mjs +0 -4743
  226. package/dist/lib/node-esm/index.mjs.map +0 -7
  227. package/dist/lib/node-esm/meta.json +0 -1
  228. package/dist/lib/node-esm/testing/index.mjs +0 -742
  229. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  230. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  231. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  232. package/dist/types/src/testing/test-data.d.ts +0 -18
  233. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  234. package/dist/types/src/testing/test-schema.d.ts +0 -37
  235. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  236. package/src/db-host/documents-iterator.ts +0 -74
  237. package/src/testing/test-data.ts +0 -127
  238. package/src/testing/test-schema.ts +0 -53
  239. /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,54 @@ 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('diff collection state', ({ expect }) => {
72
106
  const diff = diffCollectionState(STATE_1, STATE_2);
73
107
 
74
108
  expect(diff).to.deep.equal({
@@ -79,18 +113,20 @@ describe('CollectionSynchronizer', () => {
79
113
  });
80
114
  });
81
115
 
116
+ const TEST_HEADS = range(4).map((i) => A.getHeads(A.from({ i: i.toString() })));
117
+
82
118
  const STATE_1: CollectionState = {
83
119
  documents: {
84
- a: ['1'],
85
- b: ['2'],
86
- c: ['3'],
87
- },
120
+ a: TEST_HEADS[0],
121
+ b: TEST_HEADS[1],
122
+ c: TEST_HEADS[2],
123
+ } as Record<DocumentId, A.Heads>,
88
124
  };
89
125
 
90
126
  const STATE_2: CollectionState = {
91
127
  documents: {
92
- a: ['1'],
93
- b: ['4'],
94
- d: ['3'],
95
- },
128
+ a: TEST_HEADS[0],
129
+ b: TEST_HEADS[3],
130
+ d: TEST_HEADS[2],
131
+ } as Record<DocumentId, A.Heads>,
96
132
  };