@freqhole/reliquary 0.0.0

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 (226) hide show
  1. package/dist/automerge/acl-filtering.d.ts +87 -0
  2. package/dist/automerge/acl-filtering.d.ts.map +1 -0
  3. package/dist/automerge/acl-filtering.js +138 -0
  4. package/dist/automerge/acl-filtering.test.d.ts +2 -0
  5. package/dist/automerge/acl-filtering.test.d.ts.map +1 -0
  6. package/dist/automerge/acl-filtering.test.js +302 -0
  7. package/dist/automerge/index.d.ts +5 -0
  8. package/dist/automerge/index.d.ts.map +1 -0
  9. package/dist/automerge/index.js +7 -0
  10. package/dist/automerge/iroh-network-adapter.d.ts +178 -0
  11. package/dist/automerge/iroh-network-adapter.d.ts.map +1 -0
  12. package/dist/automerge/iroh-network-adapter.js +691 -0
  13. package/dist/automerge/iroh-network-adapter.test.d.ts +2 -0
  14. package/dist/automerge/iroh-network-adapter.test.d.ts.map +1 -0
  15. package/dist/automerge/iroh-network-adapter.test.js +818 -0
  16. package/dist/automerge/types.d.ts +66 -0
  17. package/dist/automerge/types.d.ts.map +1 -0
  18. package/dist/automerge/types.js +10 -0
  19. package/dist/blobs/bytes-backend.d.ts +41 -0
  20. package/dist/blobs/bytes-backend.d.ts.map +1 -0
  21. package/dist/blobs/bytes-backend.js +252 -0
  22. package/dist/blobs/bytes-backend.test.d.ts +2 -0
  23. package/dist/blobs/bytes-backend.test.d.ts.map +1 -0
  24. package/dist/blobs/bytes-backend.test.js +241 -0
  25. package/dist/blobs/db.d.ts +8 -0
  26. package/dist/blobs/db.d.ts.map +1 -0
  27. package/dist/blobs/db.js +141 -0
  28. package/dist/blobs/db.test.d.ts +2 -0
  29. package/dist/blobs/db.test.d.ts.map +1 -0
  30. package/dist/blobs/db.test.js +120 -0
  31. package/dist/blobs/index.d.ts +5 -0
  32. package/dist/blobs/index.d.ts.map +1 -0
  33. package/dist/blobs/index.js +2 -0
  34. package/dist/blobs/store.d.ts +47 -0
  35. package/dist/blobs/store.d.ts.map +1 -0
  36. package/dist/blobs/store.js +263 -0
  37. package/dist/blobs/store.test.d.ts +2 -0
  38. package/dist/blobs/store.test.d.ts.map +1 -0
  39. package/dist/blobs/store.test.js +377 -0
  40. package/dist/blobs/types.d.ts +55 -0
  41. package/dist/blobs/types.d.ts.map +1 -0
  42. package/dist/blobs/types.js +8 -0
  43. package/dist/ensure/client.d.ts +19 -0
  44. package/dist/ensure/client.d.ts.map +1 -0
  45. package/dist/ensure/client.js +74 -0
  46. package/dist/ensure/client.test.d.ts +2 -0
  47. package/dist/ensure/client.test.d.ts.map +1 -0
  48. package/dist/ensure/client.test.js +133 -0
  49. package/dist/ensure/index.d.ts +4 -0
  50. package/dist/ensure/index.d.ts.map +1 -0
  51. package/dist/ensure/index.js +4 -0
  52. package/dist/ensure/responder.d.ts +20 -0
  53. package/dist/ensure/responder.d.ts.map +1 -0
  54. package/dist/ensure/responder.js +100 -0
  55. package/dist/ensure/responder.test.d.ts +2 -0
  56. package/dist/ensure/responder.test.d.ts.map +1 -0
  57. package/dist/ensure/responder.test.js +191 -0
  58. package/dist/ensure/types.d.ts +19 -0
  59. package/dist/ensure/types.d.ts.map +1 -0
  60. package/dist/ensure/types.js +4 -0
  61. package/dist/ensure/types.test.d.ts +2 -0
  62. package/dist/ensure/types.test.d.ts.map +1 -0
  63. package/dist/ensure/types.test.js +81 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +3 -0
  67. package/dist/index.test.d.ts +2 -0
  68. package/dist/index.test.d.ts.map +1 -0
  69. package/dist/index.test.js +7 -0
  70. package/dist/solid/blob-url.d.ts +10 -0
  71. package/dist/solid/blob-url.d.ts.map +1 -0
  72. package/dist/solid/blob-url.js +34 -0
  73. package/dist/solid/blob-url.test.d.ts +2 -0
  74. package/dist/solid/blob-url.test.d.ts.map +1 -0
  75. package/dist/solid/blob-url.test.js +62 -0
  76. package/dist/solid/connection-summary.d.ts +25 -0
  77. package/dist/solid/connection-summary.d.ts.map +1 -0
  78. package/dist/solid/connection-summary.js +29 -0
  79. package/dist/solid/connection-summary.test.d.ts +2 -0
  80. package/dist/solid/connection-summary.test.d.ts.map +1 -0
  81. package/dist/solid/connection-summary.test.js +57 -0
  82. package/dist/solid/doc-store.d.ts +29 -0
  83. package/dist/solid/doc-store.d.ts.map +1 -0
  84. package/dist/solid/doc-store.js +79 -0
  85. package/dist/solid/doc-store.test.d.ts +2 -0
  86. package/dist/solid/doc-store.test.d.ts.map +1 -0
  87. package/dist/solid/doc-store.test.js +234 -0
  88. package/dist/solid/index.d.ts +4 -0
  89. package/dist/solid/index.d.ts.map +1 -0
  90. package/dist/solid/index.js +9 -0
  91. package/dist/solid/transfer-progress.d.ts +21 -0
  92. package/dist/solid/transfer-progress.d.ts.map +1 -0
  93. package/dist/solid/transfer-progress.js +32 -0
  94. package/dist/solid/transfer-progress.test.d.ts +2 -0
  95. package/dist/solid/transfer-progress.test.d.ts.map +1 -0
  96. package/dist/solid/transfer-progress.test.js +76 -0
  97. package/dist/testing/blob-fixtures.d.ts +19 -0
  98. package/dist/testing/blob-fixtures.d.ts.map +1 -0
  99. package/dist/testing/blob-fixtures.js +46 -0
  100. package/dist/testing/blob-fixtures.test.d.ts +2 -0
  101. package/dist/testing/blob-fixtures.test.d.ts.map +1 -0
  102. package/dist/testing/blob-fixtures.test.js +46 -0
  103. package/dist/testing/idb-harness.d.ts +10 -0
  104. package/dist/testing/idb-harness.d.ts.map +1 -0
  105. package/dist/testing/idb-harness.js +17 -0
  106. package/dist/testing/idb-harness.test.d.ts +2 -0
  107. package/dist/testing/idb-harness.test.d.ts.map +1 -0
  108. package/dist/testing/idb-harness.test.js +48 -0
  109. package/dist/testing/index.d.ts +7 -0
  110. package/dist/testing/index.d.ts.map +1 -0
  111. package/dist/testing/index.js +11 -0
  112. package/dist/testing/media.d.ts +15 -0
  113. package/dist/testing/media.d.ts.map +1 -0
  114. package/dist/testing/media.js +37 -0
  115. package/dist/testing/media.test.d.ts +2 -0
  116. package/dist/testing/media.test.d.ts.map +1 -0
  117. package/dist/testing/media.test.js +47 -0
  118. package/dist/testing/mock-blob-fetch.d.ts +33 -0
  119. package/dist/testing/mock-blob-fetch.d.ts.map +1 -0
  120. package/dist/testing/mock-blob-fetch.js +39 -0
  121. package/dist/testing/mock-blob-fetch.test.d.ts +2 -0
  122. package/dist/testing/mock-blob-fetch.test.d.ts.map +1 -0
  123. package/dist/testing/mock-blob-fetch.test.js +65 -0
  124. package/dist/testing/mock-node.d.ts +36 -0
  125. package/dist/testing/mock-node.d.ts.map +1 -0
  126. package/dist/testing/mock-node.js +97 -0
  127. package/dist/testing/mock-node.test.d.ts +2 -0
  128. package/dist/testing/mock-node.test.d.ts.map +1 -0
  129. package/dist/testing/mock-node.test.js +66 -0
  130. package/dist/testing/streams.d.ts +68 -0
  131. package/dist/testing/streams.d.ts.map +1 -0
  132. package/dist/testing/streams.js +99 -0
  133. package/dist/testing/streams.test.d.ts +2 -0
  134. package/dist/testing/streams.test.d.ts.map +1 -0
  135. package/dist/testing/streams.test.js +82 -0
  136. package/dist/transfer/cancellation.d.ts +8 -0
  137. package/dist/transfer/cancellation.d.ts.map +1 -0
  138. package/dist/transfer/cancellation.js +19 -0
  139. package/dist/transfer/index.d.ts +8 -0
  140. package/dist/transfer/index.d.ts.map +1 -0
  141. package/dist/transfer/index.js +4 -0
  142. package/dist/transfer/prefetch.d.ts +38 -0
  143. package/dist/transfer/prefetch.d.ts.map +1 -0
  144. package/dist/transfer/prefetch.js +61 -0
  145. package/dist/transfer/prefetch.test.d.ts +2 -0
  146. package/dist/transfer/prefetch.test.d.ts.map +1 -0
  147. package/dist/transfer/prefetch.test.js +136 -0
  148. package/dist/transfer/serve.d.ts +66 -0
  149. package/dist/transfer/serve.d.ts.map +1 -0
  150. package/dist/transfer/serve.js +126 -0
  151. package/dist/transfer/serve.test.d.ts +2 -0
  152. package/dist/transfer/serve.test.d.ts.map +1 -0
  153. package/dist/transfer/serve.test.js +101 -0
  154. package/dist/transfer/snatch.d.ts +39 -0
  155. package/dist/transfer/snatch.d.ts.map +1 -0
  156. package/dist/transfer/snatch.js +289 -0
  157. package/dist/transfer/snatch.test.d.ts +2 -0
  158. package/dist/transfer/snatch.test.d.ts.map +1 -0
  159. package/dist/transfer/snatch.test.js +337 -0
  160. package/dist/transfer/types.d.ts +85 -0
  161. package/dist/transfer/types.d.ts.map +1 -0
  162. package/dist/transfer/types.js +15 -0
  163. package/dist/utils/hash.d.ts +13 -0
  164. package/dist/utils/hash.d.ts.map +1 -0
  165. package/dist/utils/hash.js +31 -0
  166. package/dist/utils/hash.test.d.ts +2 -0
  167. package/dist/utils/hash.test.d.ts.map +1 -0
  168. package/dist/utils/hash.test.js +56 -0
  169. package/dist/utils/image-utils.d.ts +28 -0
  170. package/dist/utils/image-utils.d.ts.map +1 -0
  171. package/dist/utils/image-utils.js +113 -0
  172. package/dist/utils/image-utils.test.d.ts +2 -0
  173. package/dist/utils/image-utils.test.d.ts.map +1 -0
  174. package/dist/utils/image-utils.test.js +181 -0
  175. package/dist/utils/index.d.ts +4 -0
  176. package/dist/utils/index.d.ts.map +1 -0
  177. package/dist/utils/index.js +3 -0
  178. package/dist/utils/log.d.ts +22 -0
  179. package/dist/utils/log.d.ts.map +1 -0
  180. package/dist/utils/log.js +100 -0
  181. package/dist/utils/log.test.d.ts +2 -0
  182. package/dist/utils/log.test.d.ts.map +1 -0
  183. package/dist/utils/log.test.js +113 -0
  184. package/dist/worker/blob-worker-client.d.ts +118 -0
  185. package/dist/worker/blob-worker-client.d.ts.map +1 -0
  186. package/dist/worker/blob-worker-client.js +361 -0
  187. package/dist/worker/blob-worker-client.test.d.ts +2 -0
  188. package/dist/worker/blob-worker-client.test.d.ts.map +1 -0
  189. package/dist/worker/blob-worker-client.test.js +137 -0
  190. package/dist/worker/blob-worker-logic.d.ts +120 -0
  191. package/dist/worker/blob-worker-logic.d.ts.map +1 -0
  192. package/dist/worker/blob-worker-logic.js +368 -0
  193. package/dist/worker/blob-worker-logic.test.d.ts +2 -0
  194. package/dist/worker/blob-worker-logic.test.d.ts.map +1 -0
  195. package/dist/worker/blob-worker-logic.test.js +191 -0
  196. package/dist/worker/blob-worker.d.ts +21 -0
  197. package/dist/worker/blob-worker.d.ts.map +1 -0
  198. package/dist/worker/blob-worker.js +44 -0
  199. package/dist/worker/index.d.ts +12 -0
  200. package/dist/worker/index.d.ts.map +1 -0
  201. package/dist/worker/index.js +6 -0
  202. package/dist/worker/midden-blake3.d.ts +23 -0
  203. package/dist/worker/midden-blake3.d.ts.map +1 -0
  204. package/dist/worker/midden-blake3.js +47 -0
  205. package/dist/worker/midden-blake3.test.d.ts +2 -0
  206. package/dist/worker/midden-blake3.test.d.ts.map +1 -0
  207. package/dist/worker/midden-blake3.test.js +25 -0
  208. package/dist/worker/midden-not-bundled.stub.d.ts +1 -0
  209. package/dist/worker/midden-not-bundled.stub.d.ts.map +1 -0
  210. package/dist/worker/midden-not-bundled.stub.js +10 -0
  211. package/dist/worker/midden-stub.d.ts +10 -0
  212. package/dist/worker/midden-stub.d.ts.map +1 -0
  213. package/dist/worker/midden-stub.js +31 -0
  214. package/dist/worker/midden-stub.test.d.ts +2 -0
  215. package/dist/worker/midden-stub.test.d.ts.map +1 -0
  216. package/dist/worker/midden-stub.test.js +25 -0
  217. package/dist/worker/midden-worker-client.d.ts +103 -0
  218. package/dist/worker/midden-worker-client.d.ts.map +1 -0
  219. package/dist/worker/midden-worker-client.js +246 -0
  220. package/dist/worker/midden-worker-client.test.d.ts +2 -0
  221. package/dist/worker/midden-worker-client.test.d.ts.map +1 -0
  222. package/dist/worker/midden-worker-client.test.js +151 -0
  223. package/dist/worker/midden-worker-contract.d.ts +87 -0
  224. package/dist/worker/midden-worker-contract.d.ts.map +1 -0
  225. package/dist/worker/midden-worker-contract.js +30 -0
  226. package/package.json +90 -0
@@ -0,0 +1,818 @@
1
+ // ---------------------------------------------------------------------------
2
+ // unit tests for IrohNetworkAdapter
3
+ // ---------------------------------------------------------------------------
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { createMockBiStream, createMockMidden } from "../testing/index.js";
6
+ // ---------------------------------------------------------------------------
7
+ // helper: flush microtasks + a small real delay
8
+ // ---------------------------------------------------------------------------
9
+ function flush(ms = 20) {
10
+ return new Promise((r) => setTimeout(r, ms));
11
+ }
12
+ import { cbor } from "@automerge/automerge-repo";
13
+ import { IrohNetworkAdapter, SYNC_ALPN } from "./iroh-network-adapter.js";
14
+ // ---------------------------------------------------------------------------
15
+ // identity fixture + a fresh injectable identity double per test
16
+ // ---------------------------------------------------------------------------
17
+ function makeIdentity(nodeId = "a".repeat(64)) {
18
+ return {
19
+ node_id: nodeId,
20
+ secret_key: new Uint8Array(32),
21
+ created_at: Date.now(),
22
+ };
23
+ }
24
+ /** builds the identity-related half of IrohNetworkAdapterOptions with hooks
25
+ * the test can drive by hand, mirroring what a real getIdentity/
26
+ * onIdentityChange pair backed by stored state would do. */
27
+ function createIdentityDouble() {
28
+ let stored = null;
29
+ let callback = null;
30
+ return {
31
+ getIdentity: vi.fn(async () => stored),
32
+ onIdentityChange: vi.fn((cb) => {
33
+ callback = cb;
34
+ return () => {
35
+ callback = null;
36
+ };
37
+ }),
38
+ setStoredIdentity(identity) {
39
+ stored = identity;
40
+ },
41
+ triggerChange(identity) {
42
+ callback?.(identity);
43
+ },
44
+ get hasCallback() {
45
+ return callback !== null;
46
+ },
47
+ };
48
+ }
49
+ function makeOpts(factory, identity) {
50
+ return {
51
+ getNode: factory,
52
+ getIdentity: identity.getIdentity,
53
+ onIdentityChange: identity.onIdentityChange,
54
+ };
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // tests
58
+ // ---------------------------------------------------------------------------
59
+ describe("IrohNetworkAdapter", () => {
60
+ function setup() {
61
+ const mockMidden = createMockMidden();
62
+ const factory = vi.fn(async () => mockMidden);
63
+ const identity = createIdentityDouble();
64
+ const adapter = new IrohNetworkAdapter(makeOpts(factory, identity));
65
+ return { mockMidden, factory, identity, adapter };
66
+ }
67
+ // -----------------------------------------------------------------------
68
+ // SYNC_ALPN export
69
+ // -----------------------------------------------------------------------
70
+ describe("SYNC_ALPN", () => {
71
+ it("exports the correct ALPN string", () => {
72
+ expect(SYNC_ALPN).toBe("iroh/automerge-repo/1");
73
+ });
74
+ });
75
+ // -----------------------------------------------------------------------
76
+ // construction and readiness
77
+ // -----------------------------------------------------------------------
78
+ describe("construction and readiness", () => {
79
+ it("starts not ready", () => {
80
+ const { adapter } = setup();
81
+ expect(adapter.isReady()).toBe(false);
82
+ });
83
+ it("whenReady() resolves after connect()", async () => {
84
+ const { adapter } = setup();
85
+ adapter.connect("our-peer-id");
86
+ await adapter.whenReady();
87
+ expect(adapter.isReady()).toBe(true);
88
+ });
89
+ it("stores the peerId passed to connect()", () => {
90
+ const { adapter } = setup();
91
+ adapter.connect("some-peer");
92
+ expect(adapter.peerId).toBe("some-peer");
93
+ });
94
+ it("becomes ready after connect() even without identity", async () => {
95
+ const { adapter } = setup();
96
+ adapter.connect("our-peer-id");
97
+ await flush();
98
+ expect(adapter.isReady()).toBe(true);
99
+ });
100
+ });
101
+ // -----------------------------------------------------------------------
102
+ // deferred initialization (identity-getter parameterization)
103
+ // -----------------------------------------------------------------------
104
+ describe("deferred initialization", () => {
105
+ it("does not call getNode when no identity exists", async () => {
106
+ const { adapter, factory } = setup();
107
+ adapter.connect("our-id");
108
+ await flush(50);
109
+ expect(factory).not.toHaveBeenCalled();
110
+ expect(adapter.isReady()).toBe(true);
111
+ });
112
+ it("calls getNode immediately when identity exists at connect time", async () => {
113
+ const { adapter, factory, identity } = setup();
114
+ identity.setStoredIdentity(makeIdentity());
115
+ adapter.connect("our-id");
116
+ await flush(50);
117
+ expect(factory).toHaveBeenCalledTimes(1);
118
+ });
119
+ it("starts the node when identity is created later via onIdentityChange", async () => {
120
+ const { adapter, factory, identity } = setup();
121
+ adapter.connect("our-id");
122
+ await flush(50);
123
+ expect(factory).not.toHaveBeenCalled();
124
+ identity.triggerChange(makeIdentity());
125
+ await flush(50);
126
+ expect(factory).toHaveBeenCalledTimes(1);
127
+ });
128
+ it("ignores identity change if already disconnected", async () => {
129
+ const { adapter, factory, identity } = setup();
130
+ adapter.connect("our-id");
131
+ await flush(50);
132
+ adapter.disconnect();
133
+ identity.triggerChange(makeIdentity());
134
+ await flush(50);
135
+ expect(factory).not.toHaveBeenCalled();
136
+ });
137
+ it("ignores identity change if the node is already initialized", async () => {
138
+ const { adapter, factory, identity } = setup();
139
+ identity.setStoredIdentity(makeIdentity());
140
+ adapter.connect("our-id");
141
+ await flush(50);
142
+ expect(factory).toHaveBeenCalledTimes(1);
143
+ identity.triggerChange(makeIdentity());
144
+ await flush(50);
145
+ expect(factory).toHaveBeenCalledTimes(1);
146
+ });
147
+ it("stays passive with no onIdentityChange and no identity", async () => {
148
+ const mockMidden = createMockMidden();
149
+ const factory = vi.fn(async () => mockMidden);
150
+ const adapter = new IrohNetworkAdapter({
151
+ getNode: factory,
152
+ getIdentity: vi.fn(async () => null),
153
+ });
154
+ adapter.connect("our-id");
155
+ await flush(50);
156
+ expect(factory).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+ // -----------------------------------------------------------------------
160
+ // addPeer()
161
+ // -----------------------------------------------------------------------
162
+ describe("addPeer()", () => {
163
+ async function setupConnected() {
164
+ const s = setup();
165
+ s.identity.setStoredIdentity(makeIdentity());
166
+ s.adapter.connect("our-id");
167
+ await flush(50);
168
+ return s;
169
+ }
170
+ it("calls open_bi with the correct nodeId and ALPN", async () => {
171
+ const { adapter, mockMidden } = await setupConnected();
172
+ const peerId = "b".repeat(64);
173
+ await adapter.addPeer(peerId);
174
+ expect(mockMidden.open_bi).toHaveBeenCalledWith(peerId, SYNC_ALPN);
175
+ });
176
+ it("emits peer-candidate after connecting", async () => {
177
+ const { adapter } = await setupConnected();
178
+ const peerCandidates = [];
179
+ adapter.on("peer-candidate", (payload) => peerCandidates.push(payload));
180
+ const peerId = "b".repeat(64);
181
+ await adapter.addPeer(peerId);
182
+ expect(peerCandidates).toHaveLength(1);
183
+ expect(peerCandidates[0].peerId).toBe(peerId);
184
+ expect(peerCandidates[0].peerMetadata).toEqual({ isEphemeral: false });
185
+ });
186
+ it("skips if already connected to that peer", async () => {
187
+ const { adapter, mockMidden } = await setupConnected();
188
+ const peerId = "b".repeat(64);
189
+ await adapter.addPeer(peerId);
190
+ await adapter.addPeer(peerId);
191
+ expect(mockMidden.open_bi).toHaveBeenCalledTimes(1);
192
+ });
193
+ it("throws if adapter is disconnected", async () => {
194
+ const { adapter } = await setupConnected();
195
+ adapter.disconnect();
196
+ await expect(adapter.addPeer("b".repeat(64))).rejects.toThrow("adapter is disconnected");
197
+ });
198
+ it("initializes the node lazily if not yet started", async () => {
199
+ const s = setup();
200
+ s.adapter.connect("our-id");
201
+ await flush(50);
202
+ expect(s.factory).not.toHaveBeenCalled();
203
+ await s.adapter.addPeer("c".repeat(64));
204
+ expect(s.factory).toHaveBeenCalledTimes(1);
205
+ });
206
+ it("retries the initial dial a few times before giving up", async () => {
207
+ const { adapter, mockMidden } = await setupConnected();
208
+ const peerId = "b".repeat(64);
209
+ const goodStream = createMockBiStream(peerId);
210
+ mockMidden.open_bi
211
+ .mockRejectedValueOnce(new Error("no addressing information available"))
212
+ .mockRejectedValueOnce(new Error("no addressing information available"))
213
+ .mockResolvedValueOnce(goodStream);
214
+ const addPeerPromise = adapter.addPeer(peerId);
215
+ // openBiWithRetry sleeps 750ms between attempts - advance real time via flush
216
+ await flush(2000);
217
+ await addPeerPromise;
218
+ expect(mockMidden.open_bi).toHaveBeenCalledTimes(3);
219
+ expect(adapter.isConnected(peerId)).toBe(true);
220
+ });
221
+ });
222
+ // -----------------------------------------------------------------------
223
+ // send()
224
+ // -----------------------------------------------------------------------
225
+ describe("send()", () => {
226
+ async function setupConnected() {
227
+ const s = setup();
228
+ s.identity.setStoredIdentity(makeIdentity());
229
+ s.adapter.connect("our-id");
230
+ await flush(50);
231
+ return s;
232
+ }
233
+ it("CBOR-encodes message and writes to the correct peer stream", async () => {
234
+ const { adapter, mockMidden } = await setupConnected();
235
+ const peerId = "b".repeat(64);
236
+ await adapter.addPeer(peerId);
237
+ const stream = await mockMidden.open_bi.mock.results[0].value;
238
+ const message = {
239
+ type: "sync",
240
+ senderId: "our-id",
241
+ targetId: peerId,
242
+ data: new Uint8Array([1, 2, 3]),
243
+ documentId: "doc-1",
244
+ };
245
+ adapter.send(message);
246
+ await flush();
247
+ expect(stream.write_message).toHaveBeenCalledTimes(1);
248
+ const written = stream.write_message.mock.calls[0][0];
249
+ const decoded = cbor.decode(written);
250
+ expect(decoded.type).toBe("sync");
251
+ expect(decoded.targetId).toBe(peerId);
252
+ expect(decoded.documentId).toBe("doc-1");
253
+ });
254
+ it("does not throw when no stream exists for the target", async () => {
255
+ const { adapter } = await setupConnected();
256
+ const message = {
257
+ type: "sync",
258
+ senderId: "our-id",
259
+ targetId: "unknown-peer",
260
+ };
261
+ expect(() => adapter.send(message)).not.toThrow();
262
+ });
263
+ it("removes peer on write failure", async () => {
264
+ const { adapter, mockMidden } = await setupConnected();
265
+ const peerId = "b".repeat(64);
266
+ const failingStream = createMockBiStream(peerId);
267
+ failingStream.write_message.mockRejectedValueOnce(new Error("write error"));
268
+ mockMidden.open_bi.mockResolvedValueOnce(failingStream);
269
+ await adapter.addPeer(peerId);
270
+ const disconnected = [];
271
+ adapter.on("peer-disconnected", (payload) => disconnected.push(payload.peerId));
272
+ adapter.send({
273
+ type: "sync",
274
+ senderId: "our-id",
275
+ targetId: peerId,
276
+ });
277
+ await flush(50);
278
+ expect(disconnected).toContain(peerId);
279
+ });
280
+ });
281
+ // -----------------------------------------------------------------------
282
+ // read loop
283
+ // -----------------------------------------------------------------------
284
+ describe("read loop", () => {
285
+ async function setupConnected() {
286
+ const s = setup();
287
+ s.identity.setStoredIdentity(makeIdentity());
288
+ s.adapter.connect("our-id");
289
+ await flush(50);
290
+ return s;
291
+ }
292
+ it("incoming CBOR messages are decoded and emitted as 'message' events", async () => {
293
+ const { adapter, mockMidden } = await setupConnected();
294
+ const peerId = "b".repeat(64);
295
+ const mockStream = createMockBiStream(peerId);
296
+ mockMidden.open_bi.mockResolvedValueOnce(mockStream);
297
+ await adapter.addPeer(peerId);
298
+ await flush();
299
+ const receivedMessages = [];
300
+ adapter.on("message", (msg) => receivedMessages.push(msg));
301
+ const outgoing = {
302
+ type: "sync",
303
+ senderId: peerId,
304
+ targetId: "our-id",
305
+ documentId: "doc-1",
306
+ data: new Uint8Array([10, 20, 30]),
307
+ };
308
+ const encoded = cbor.encode(outgoing);
309
+ const bytes = new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength);
310
+ mockStream.pushMessage(bytes);
311
+ await flush(50);
312
+ expect(receivedMessages).toHaveLength(1);
313
+ expect(receivedMessages[0].senderId).toBe(peerId);
314
+ expect(receivedMessages[0].type).toBe("sync");
315
+ expect(receivedMessages[0].documentId).toBe("doc-1");
316
+ });
317
+ it("emits peer-disconnected when read_message returns null (stream close)", async () => {
318
+ const { adapter, mockMidden } = await setupConnected();
319
+ const peerId = "b".repeat(64);
320
+ const mockStream = createMockBiStream(peerId);
321
+ mockMidden.open_bi.mockResolvedValueOnce(mockStream);
322
+ await adapter.addPeer(peerId);
323
+ await flush();
324
+ const disconnected = [];
325
+ adapter.on("peer-disconnected", (payload) => disconnected.push(payload.peerId));
326
+ mockStream.pushMessage(null);
327
+ await flush(50);
328
+ expect(disconnected).toContain(peerId);
329
+ });
330
+ it("emits peer-disconnected on read error", async () => {
331
+ const { adapter, mockMidden } = await setupConnected();
332
+ const peerId = "b".repeat(64);
333
+ const mockStream = createMockBiStream(peerId);
334
+ mockStream.read_message.mockRejectedValueOnce(new Error("read failed"));
335
+ mockMidden.open_bi.mockResolvedValueOnce(mockStream);
336
+ const disconnected = [];
337
+ adapter.on("peer-disconnected", (payload) => disconnected.push(payload.peerId));
338
+ await adapter.addPeer(peerId);
339
+ await flush(50);
340
+ expect(disconnected).toContain(peerId);
341
+ });
342
+ });
343
+ // -----------------------------------------------------------------------
344
+ // accept loop
345
+ // -----------------------------------------------------------------------
346
+ describe("accept loop", () => {
347
+ async function setupConnected() {
348
+ const s = setup();
349
+ s.identity.setStoredIdentity(makeIdentity());
350
+ s.adapter.connect("our-id");
351
+ await flush(50);
352
+ return s;
353
+ }
354
+ it("registers incoming streams with correct ALPN", async () => {
355
+ const { adapter, mockMidden } = await setupConnected();
356
+ const peerId = "c".repeat(64);
357
+ const incomingStream = createMockBiStream(peerId, SYNC_ALPN);
358
+ const peerCandidates = [];
359
+ adapter.on("peer-candidate", (payload) => peerCandidates.push(payload));
360
+ mockMidden.pushIncoming(incomingStream);
361
+ await flush(50);
362
+ expect(peerCandidates.some((p) => p.peerId === peerId)).toBe(true);
363
+ });
364
+ it("closes streams with wrong ALPN", async () => {
365
+ const { mockMidden } = await setupConnected();
366
+ const peerId = "d".repeat(64);
367
+ const wrongAlpnStream = createMockBiStream(peerId, "some/other/protocol");
368
+ mockMidden.pushIncoming(wrongAlpnStream);
369
+ await flush(50);
370
+ expect(wrongAlpnStream.close).toHaveBeenCalled();
371
+ expect(wrongAlpnStream._closed).toBe(true);
372
+ });
373
+ it("does not emit peer-candidate for wrong ALPN", async () => {
374
+ const { adapter, mockMidden } = await setupConnected();
375
+ const wrongAlpnStream = createMockBiStream("d".repeat(64), "wrong/alpn");
376
+ const peerCandidates = [];
377
+ adapter.on("peer-candidate", (payload) => peerCandidates.push(payload));
378
+ mockMidden.pushIncoming(wrongAlpnStream);
379
+ await flush(50);
380
+ const wrongPeer = peerCandidates.find((p) => p.peerId === "d".repeat(64));
381
+ expect(wrongPeer).toBeUndefined();
382
+ });
383
+ it("dispatches a registered ALPN handler instead of closing the stream", async () => {
384
+ const { adapter, mockMidden } = await setupConnected();
385
+ const alpn = "app-specific/1";
386
+ const handler = vi.fn();
387
+ adapter.registerAlpnHandler(alpn, handler);
388
+ const stream = createMockBiStream("e".repeat(64), alpn);
389
+ mockMidden.pushIncoming(stream);
390
+ await flush(50);
391
+ expect(handler).toHaveBeenCalledWith(stream);
392
+ expect(stream.close).not.toHaveBeenCalled();
393
+ });
394
+ it("stops the accept loop when endpoint returns null", async () => {
395
+ const { adapter, mockMidden } = await setupConnected();
396
+ mockMidden.pushIncoming(null);
397
+ await flush(50);
398
+ const peerCandidates = [];
399
+ adapter.on("peer-candidate", (payload) => peerCandidates.push(payload));
400
+ mockMidden.pushIncoming(createMockBiStream("e".repeat(64)));
401
+ await flush(50);
402
+ expect(peerCandidates).toHaveLength(0);
403
+ });
404
+ it("reads messages from accepted streams", async () => {
405
+ const { adapter, mockMidden } = await setupConnected();
406
+ const peerId = "f".repeat(64);
407
+ const incomingStream = createMockBiStream(peerId, SYNC_ALPN);
408
+ mockMidden.pushIncoming(incomingStream);
409
+ await flush(50);
410
+ const receivedMessages = [];
411
+ adapter.on("message", (msg) => receivedMessages.push(msg));
412
+ const outgoing = {
413
+ type: "sync",
414
+ senderId: peerId,
415
+ targetId: "our-id",
416
+ documentId: "doc-accepted",
417
+ };
418
+ const encoded = cbor.encode(outgoing);
419
+ incomingStream.pushMessage(new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength));
420
+ await flush(50);
421
+ expect(receivedMessages).toHaveLength(1);
422
+ expect(receivedMessages[0].senderId).toBe(peerId);
423
+ expect(receivedMessages[0].documentId).toBe("doc-accepted");
424
+ });
425
+ });
426
+ // -----------------------------------------------------------------------
427
+ // disconnect()
428
+ // -----------------------------------------------------------------------
429
+ describe("disconnect()", () => {
430
+ async function setupConnected() {
431
+ const s = setup();
432
+ s.identity.setStoredIdentity(makeIdentity());
433
+ s.adapter.connect("our-id");
434
+ await flush(50);
435
+ return s;
436
+ }
437
+ it("emits peer-disconnected for each connected peer", async () => {
438
+ const { adapter } = await setupConnected();
439
+ const peerB = "b".repeat(64);
440
+ const peerC = "c".repeat(64);
441
+ await adapter.addPeer(peerB);
442
+ await adapter.addPeer(peerC);
443
+ const disconnected = [];
444
+ adapter.on("peer-disconnected", (payload) => disconnected.push(payload.peerId));
445
+ adapter.disconnect();
446
+ expect(disconnected).toHaveLength(2);
447
+ expect(disconnected).toContain(peerB);
448
+ expect(disconnected).toContain(peerC);
449
+ });
450
+ it("emits close event", async () => {
451
+ const { adapter } = await setupConnected();
452
+ let closed = false;
453
+ adapter.on("close", () => {
454
+ closed = true;
455
+ });
456
+ adapter.disconnect();
457
+ expect(closed).toBe(true);
458
+ });
459
+ it("closes all streams", async () => {
460
+ const { adapter, mockMidden } = await setupConnected();
461
+ const peerB = "b".repeat(64);
462
+ const streamB = createMockBiStream(peerB);
463
+ mockMidden.open_bi.mockResolvedValueOnce(streamB);
464
+ await adapter.addPeer(peerB);
465
+ adapter.disconnect();
466
+ expect(streamB.close).toHaveBeenCalled();
467
+ });
468
+ it("is idempotent (second call does not throw)", async () => {
469
+ const { adapter } = await setupConnected();
470
+ await adapter.addPeer("b".repeat(64));
471
+ adapter.disconnect();
472
+ expect(() => adapter.disconnect()).not.toThrow();
473
+ });
474
+ it("unsubscribes from identity changes on disconnect", async () => {
475
+ const s = setup();
476
+ s.adapter.connect("our-id");
477
+ await flush(50);
478
+ expect(s.identity.hasCallback).toBe(true);
479
+ s.adapter.disconnect();
480
+ expect(s.identity.hasCallback).toBe(false);
481
+ });
482
+ it("sets endpoint state back to off", async () => {
483
+ const { adapter } = await setupConnected();
484
+ expect(adapter.getEndpointState()).toBe("online");
485
+ adapter.disconnect();
486
+ expect(adapter.getEndpointState()).toBe("off");
487
+ });
488
+ });
489
+ // -----------------------------------------------------------------------
490
+ // registerStream replacing an existing stream (peer supersession rule)
491
+ // -----------------------------------------------------------------------
492
+ describe("stream supersession", () => {
493
+ async function setupConnected() {
494
+ const s = setup();
495
+ s.identity.setStoredIdentity(makeIdentity());
496
+ s.adapter.connect("our-id");
497
+ await flush(50);
498
+ return s;
499
+ }
500
+ it("simultaneous connect: the newest stream takes writes and NEITHER live stream is closed", async () => {
501
+ const { adapter, mockMidden } = await setupConnected();
502
+ const peerId = "b".repeat(64);
503
+ const outbound = createMockBiStream(peerId);
504
+ const inbound = createMockBiStream(peerId);
505
+ mockMidden.open_bi.mockResolvedValueOnce(outbound);
506
+ await adapter.addPeer(peerId);
507
+ // peer dialed us at the same time - inbound arrives while our
508
+ // outbound is registered. closing either stream is wrong: with a
509
+ // "close the replaced one" policy, both sides of a simultaneous
510
+ // connect close the stream the OTHER side kept, killing both
511
+ // connections.
512
+ mockMidden.pushIncoming(inbound);
513
+ await flush(50);
514
+ expect(outbound.close).not.toHaveBeenCalled();
515
+ expect(inbound.close).not.toHaveBeenCalled();
516
+ // both streams are being drained
517
+ expect(outbound.read_message).toHaveBeenCalled();
518
+ expect(inbound.read_message).toHaveBeenCalled();
519
+ });
520
+ it("a fresh inbound stream takes over from a zombie incumbent without the zombie's death blocking it (peer restart recovery)", async () => {
521
+ const { adapter, mockMidden } = await setupConnected();
522
+ const peerId = "b".repeat(64);
523
+ const zombie = createMockBiStream(peerId);
524
+ const fresh = createMockBiStream(peerId);
525
+ mockMidden.open_bi.mockResolvedValueOnce(zombie);
526
+ await adapter.addPeer(peerId);
527
+ // the peer process restarted and redialed - its fresh inbound must
528
+ // become the write target immediately (writes to the zombie would
529
+ // silently vanish and deadlock recovery)
530
+ mockMidden.pushIncoming(fresh);
531
+ await flush(50);
532
+ expect(fresh.close).not.toHaveBeenCalled();
533
+ expect(fresh.read_message).toHaveBeenCalled();
534
+ });
535
+ it("supersession re-announces peer-candidate after cycling peer-disconnected", async () => {
536
+ const { adapter, mockMidden } = await setupConnected();
537
+ const peerId = "b".repeat(64);
538
+ const outbound = createMockBiStream(peerId);
539
+ const inbound = createMockBiStream(peerId);
540
+ mockMidden.open_bi.mockResolvedValueOnce(outbound);
541
+ await adapter.addPeer(peerId);
542
+ const events = [];
543
+ adapter.on("peer-disconnected", () => events.push("disconnected"));
544
+ adapter.on("peer-candidate", () => events.push("candidate"));
545
+ mockMidden.pushIncoming(inbound);
546
+ await flush(200);
547
+ expect(events).toEqual(["disconnected", "candidate"]);
548
+ });
549
+ it("the superseded stream's own natural death does not remove the peer", async () => {
550
+ const { adapter, mockMidden } = await setupConnected();
551
+ const peerId = "b".repeat(64);
552
+ const outbound = createMockBiStream(peerId);
553
+ const inbound = createMockBiStream(peerId);
554
+ mockMidden.open_bi.mockResolvedValueOnce(outbound);
555
+ await adapter.addPeer(peerId);
556
+ mockMidden.pushIncoming(inbound);
557
+ await flush(200);
558
+ const disconnectedAfterSupersede = [];
559
+ adapter.on("peer-disconnected", (payload) => disconnectedAfterSupersede.push(payload.peerId));
560
+ // the OLD (superseded) outbound stream finally dies on its own
561
+ outbound.pushMessage(null);
562
+ await flush(50);
563
+ // the peer is still considered connected via the newer inbound stream
564
+ expect(adapter.isConnected(peerId)).toBe(true);
565
+ expect(disconnectedAfterSupersede).toHaveLength(0);
566
+ });
567
+ });
568
+ // -----------------------------------------------------------------------
569
+ // stop() / restart()
570
+ // -----------------------------------------------------------------------
571
+ describe("stop() / restart()", () => {
572
+ async function setupConnected() {
573
+ const s = setup();
574
+ s.identity.setStoredIdentity(makeIdentity());
575
+ s.adapter.connect("our-id");
576
+ await flush(50);
577
+ return s;
578
+ }
579
+ it("stop() closes streams, sets endpoint state to off, keeps intended peers", async () => {
580
+ const { adapter, mockMidden } = await setupConnected();
581
+ const peerId = "b".repeat(64);
582
+ const stream = createMockBiStream(peerId);
583
+ mockMidden.open_bi.mockResolvedValueOnce(stream);
584
+ await adapter.addPeer(peerId);
585
+ adapter.stop();
586
+ expect(stream.close).toHaveBeenCalled();
587
+ expect(adapter.getEndpointState()).toBe("off");
588
+ expect(adapter.isConnected(peerId)).toBe(false);
589
+ });
590
+ it("addPeer() while stopped remembers the peer but does not dial", async () => {
591
+ const { adapter, mockMidden } = await setupConnected();
592
+ adapter.stop();
593
+ await adapter.addPeer("b".repeat(64));
594
+ expect(mockMidden.open_bi).not.toHaveBeenCalled();
595
+ });
596
+ it("restart() re-initializes the node and reconnects remembered peers", async () => {
597
+ const { adapter, mockMidden, factory } = await setupConnected();
598
+ const peerId = "b".repeat(64);
599
+ const firstStream = createMockBiStream(peerId);
600
+ mockMidden.open_bi.mockResolvedValueOnce(firstStream);
601
+ await adapter.addPeer(peerId);
602
+ adapter.stop();
603
+ const callsBeforeRestart = factory.mock.calls.length;
604
+ const reconnectedStream = createMockBiStream(peerId);
605
+ mockMidden.open_bi.mockResolvedValueOnce(reconnectedStream);
606
+ await adapter.restart();
607
+ await flush(50);
608
+ expect(factory.mock.calls.length).toBeGreaterThan(callsBeforeRestart);
609
+ expect(adapter.getEndpointState()).toBe("online");
610
+ expect(adapter.isConnected(peerId)).toBe(true);
611
+ });
612
+ it("restart() is a no-op if never stopped", async () => {
613
+ const { adapter, factory } = await setupConnected();
614
+ const callsBefore = factory.mock.calls.length;
615
+ await adapter.restart();
616
+ expect(factory.mock.calls.length).toBe(callsBefore);
617
+ });
618
+ });
619
+ // -----------------------------------------------------------------------
620
+ // reconnection
621
+ // -----------------------------------------------------------------------
622
+ describe("reconnection", () => {
623
+ async function setupConnected() {
624
+ const s = setup();
625
+ s.identity.setStoredIdentity(makeIdentity());
626
+ s.adapter.connect("our-id");
627
+ await flush(50);
628
+ return s;
629
+ }
630
+ it("schedules reconnection when an intended peer disconnects", async () => {
631
+ const { adapter, mockMidden } = await setupConnected();
632
+ const peerId = "b".repeat(64);
633
+ const stream = createMockBiStream(peerId);
634
+ mockMidden.open_bi.mockResolvedValueOnce(stream);
635
+ await adapter.addPeer(peerId);
636
+ await flush(50);
637
+ mockMidden.open_bi.mockResolvedValue(createMockBiStream(peerId));
638
+ vi.useFakeTimers();
639
+ try {
640
+ stream.pushMessage(null); // close the stream -> removePeer -> scheduleReconnect
641
+ await vi.advanceTimersByTimeAsync(2100);
642
+ expect(mockMidden.open_bi).toHaveBeenCalledTimes(2);
643
+ }
644
+ finally {
645
+ vi.useRealTimers();
646
+ }
647
+ });
648
+ it("does not schedule reconnection for peers connected via accept loop only", async () => {
649
+ const { adapter, mockMidden } = await setupConnected();
650
+ const peerId = "c".repeat(64);
651
+ const stream = createMockBiStream(peerId);
652
+ mockMidden.pushIncoming(stream);
653
+ await flush(50);
654
+ const openBiCallsBefore = mockMidden.open_bi.mock.calls.length;
655
+ vi.useFakeTimers();
656
+ try {
657
+ stream.pushMessage(null);
658
+ await vi.advanceTimersByTimeAsync(10_000);
659
+ expect(mockMidden.open_bi.mock.calls.length).toBe(openBiCallsBefore);
660
+ }
661
+ finally {
662
+ vi.useRealTimers();
663
+ }
664
+ });
665
+ it("reconnects successfully after a transient connection drop", async () => {
666
+ const { adapter, mockMidden } = await setupConnected();
667
+ const peerId = "b".repeat(64);
668
+ const firstStream = createMockBiStream(peerId);
669
+ const reconnectedStream = createMockBiStream(peerId);
670
+ mockMidden.open_bi.mockResolvedValueOnce(firstStream);
671
+ await adapter.addPeer(peerId);
672
+ await flush(50);
673
+ mockMidden.open_bi.mockResolvedValueOnce(reconnectedStream);
674
+ const peerCandidates = [];
675
+ adapter.on("peer-candidate", (ev) => {
676
+ peerCandidates.push(ev.peerId);
677
+ });
678
+ vi.useFakeTimers();
679
+ try {
680
+ firstStream.pushMessage(null);
681
+ await vi.advanceTimersByTimeAsync(2100);
682
+ expect(mockMidden.open_bi).toHaveBeenCalledTimes(2);
683
+ expect(peerCandidates).toContain(peerId);
684
+ }
685
+ finally {
686
+ vi.useRealTimers();
687
+ }
688
+ });
689
+ it("gives up after maximum reconnection attempts", async () => {
690
+ const { adapter, mockMidden } = await setupConnected();
691
+ const peerId = "b".repeat(64);
692
+ const initialStream = createMockBiStream(peerId);
693
+ mockMidden.open_bi.mockResolvedValueOnce(initialStream);
694
+ await adapter.addPeer(peerId);
695
+ await flush(50);
696
+ for (let i = 0; i < 10; i++) {
697
+ mockMidden.open_bi.mockRejectedValueOnce(new Error(`fail ${i}`));
698
+ }
699
+ vi.useFakeTimers();
700
+ try {
701
+ initialStream.pushMessage(null);
702
+ await vi.advanceTimersByTimeAsync(200_000);
703
+ const summary = adapter.getConnectionSummary();
704
+ expect(summary.failed).toBe(1);
705
+ }
706
+ finally {
707
+ vi.useRealTimers();
708
+ }
709
+ });
710
+ it("clears reconnect state when peer reconnects via accept loop", async () => {
711
+ const { adapter, mockMidden } = await setupConnected();
712
+ const peerId = "b".repeat(64);
713
+ const firstStream = createMockBiStream(peerId);
714
+ mockMidden.open_bi.mockResolvedValueOnce(firstStream);
715
+ await adapter.addPeer(peerId);
716
+ await flush(50);
717
+ mockMidden.open_bi.mockRejectedValue(new Error("still failing"));
718
+ vi.useFakeTimers();
719
+ try {
720
+ firstStream.pushMessage(null);
721
+ await vi.advanceTimersByTimeAsync(0);
722
+ const incomingStream = createMockBiStream(peerId);
723
+ mockMidden.pushIncoming(incomingStream);
724
+ await vi.advanceTimersByTimeAsync(0);
725
+ const openBiCallsBefore = mockMidden.open_bi.mock.calls.length;
726
+ await vi.advanceTimersByTimeAsync(10_000);
727
+ const openBiCallsAfter = mockMidden.open_bi.mock.calls.length;
728
+ expect(openBiCallsAfter).toBe(openBiCallsBefore);
729
+ }
730
+ finally {
731
+ vi.useRealTimers();
732
+ }
733
+ });
734
+ it("disconnect() cancels all pending reconnection timers", async () => {
735
+ const { adapter, mockMidden } = await setupConnected();
736
+ const peerId = "b".repeat(64);
737
+ const stream = createMockBiStream(peerId);
738
+ mockMidden.open_bi.mockResolvedValueOnce(stream);
739
+ await adapter.addPeer(peerId);
740
+ await flush(50);
741
+ mockMidden.open_bi.mockResolvedValue(createMockBiStream(peerId));
742
+ vi.useFakeTimers();
743
+ try {
744
+ stream.pushMessage(null);
745
+ await vi.advanceTimersByTimeAsync(0);
746
+ adapter.disconnect();
747
+ const openBiCallsBefore = mockMidden.open_bi.mock.calls.length;
748
+ await vi.advanceTimersByTimeAsync(10_000);
749
+ const openBiCallsAfter = mockMidden.open_bi.mock.calls.length;
750
+ expect(openBiCallsAfter).toBe(openBiCallsBefore);
751
+ }
752
+ finally {
753
+ vi.useRealTimers();
754
+ }
755
+ });
756
+ });
757
+ // -----------------------------------------------------------------------
758
+ // forgetPeer()
759
+ // -----------------------------------------------------------------------
760
+ describe("forgetPeer()", () => {
761
+ async function setupConnected() {
762
+ const s = setup();
763
+ s.identity.setStoredIdentity(makeIdentity());
764
+ s.adapter.connect("our-id");
765
+ await flush(50);
766
+ return s;
767
+ }
768
+ it("closes the stream and emits peer-disconnected", async () => {
769
+ const { adapter, mockMidden } = await setupConnected();
770
+ const peerId = "b".repeat(64);
771
+ const stream = createMockBiStream(peerId);
772
+ mockMidden.open_bi.mockResolvedValueOnce(stream);
773
+ await adapter.addPeer(peerId);
774
+ await flush(50);
775
+ const disconnected = [];
776
+ adapter.on("peer-disconnected", (ev) => {
777
+ disconnected.push(ev.peerId);
778
+ });
779
+ adapter.forgetPeer(peerId);
780
+ expect(stream.close).toHaveBeenCalled();
781
+ expect(disconnected).toContain(peerId);
782
+ });
783
+ it("does not schedule reconnection for a forgotten peer", async () => {
784
+ const { adapter, mockMidden } = await setupConnected();
785
+ const peerId = "b".repeat(64);
786
+ const stream = createMockBiStream(peerId);
787
+ mockMidden.open_bi.mockResolvedValueOnce(stream);
788
+ await adapter.addPeer(peerId);
789
+ await flush(50);
790
+ const openBiCallsBefore = mockMidden.open_bi.mock.calls.length;
791
+ adapter.forgetPeer(peerId);
792
+ await flush(50);
793
+ expect(mockMidden.open_bi.mock.calls.length).toBe(openBiCallsBefore);
794
+ });
795
+ it("cancels a pending reconnection timer", async () => {
796
+ const { adapter, mockMidden } = await setupConnected();
797
+ const peerId = "b".repeat(64);
798
+ const stream = createMockBiStream(peerId);
799
+ mockMidden.open_bi.mockResolvedValueOnce(stream);
800
+ await adapter.addPeer(peerId);
801
+ await flush(50);
802
+ mockMidden.open_bi.mockResolvedValue(createMockBiStream(peerId));
803
+ vi.useFakeTimers();
804
+ try {
805
+ stream.pushMessage(null); // disconnect -> scheduleReconnect
806
+ await vi.advanceTimersByTimeAsync(0);
807
+ adapter.forgetPeer(peerId);
808
+ const openBiCallsBefore = mockMidden.open_bi.mock.calls.length;
809
+ await vi.advanceTimersByTimeAsync(10_000);
810
+ const openBiCallsAfter = mockMidden.open_bi.mock.calls.length;
811
+ expect(openBiCallsAfter).toBe(openBiCallsBefore);
812
+ }
813
+ finally {
814
+ vi.useRealTimers();
815
+ }
816
+ });
817
+ });
818
+ });