@freqhole/api-client 0.1.32 → 0.2.2
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.
- package/package.json +7 -16
- package/src/CharnelTransport.ts +7 -7
- package/src/WasmTransport.ts +23 -22
- package/src/permissions.ts +16 -29
- package/src/pollingJobEvents.test.ts +192 -0
- package/src/test/permissions.ts +185 -0
- package/src/test.ts +5 -14
- package/src/transport.ts +10 -3
- package/vitest.config.ts +8 -0
- package/src/automerge/IrohNetworkAdapter.ts +0 -759
- package/src/automerge/index.ts +0 -14
- package/src/storage/blobs.ts +0 -272
- package/src/storage/identity.ts +0 -190
- package/src/storage/index.ts +0 -5
- package/src/storage/webLocks.ts +0 -92
- package/src/test/automerge.ts +0 -796
- package/src/test/storage.ts +0 -443
package/src/test/automerge.ts
DELETED
|
@@ -1,796 +0,0 @@
|
|
|
1
|
-
// unit tests for IrohNetworkAdapter
|
|
2
|
-
//
|
|
3
|
-
// uses the package's custom tsx test runner (no vitest).
|
|
4
|
-
// hand-rolled mocks replace vitest's vi.fn() / vi.mock().
|
|
5
|
-
//
|
|
6
|
-
// skipped cases (reported at bottom):
|
|
7
|
-
// - reconnection backoff timing: requires fake timers (setTimeout interception),
|
|
8
|
-
// which vitest provides but the custom runner does not. the reconnect logic
|
|
9
|
-
// itself is a direct port of skein's implementation with no behavioral changes.
|
|
10
|
-
|
|
11
|
-
import { cbor } from "@automerge/automerge-repo";
|
|
12
|
-
import type { PeerId } from "@automerge/automerge-repo";
|
|
13
|
-
import {
|
|
14
|
-
IrohNetworkAdapter,
|
|
15
|
-
SYNC_ALPN,
|
|
16
|
-
} from "../automerge/IrohNetworkAdapter.js";
|
|
17
|
-
import type { BiStreamLike, MiddenStreamNode } from "../automerge/IrohNetworkAdapter.js";
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// assertion helpers
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
function assert(condition: boolean, message: string): void {
|
|
24
|
-
if (!condition) throw new Error(message);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function assertEqual<T>(a: T, b: T, message?: string): void {
|
|
28
|
-
if (a !== b) {
|
|
29
|
-
throw new Error(message ?? `expected ${JSON.stringify(a)} === ${JSON.stringify(b)}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function assertIncludes<T>(arr: T[], item: T, message?: string): void {
|
|
34
|
-
if (!arr.includes(item)) {
|
|
35
|
-
throw new Error(message ?? `expected array to include ${JSON.stringify(item)}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function assertLength(arr: unknown[], len: number, message?: string): void {
|
|
40
|
-
if (arr.length !== len) {
|
|
41
|
-
throw new Error(message ?? `expected length ${len}, got ${arr.length}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// flush: let pending promises and microtasks settle
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
function flush(ms = 30): Promise<void> {
|
|
50
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// mock: BiStreamLike
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
function createMockBiStream(peerId: string, alpn: string = SYNC_ALPN) {
|
|
58
|
-
const written: Uint8Array[] = [];
|
|
59
|
-
const messageQueue: (Uint8Array | null)[] = [];
|
|
60
|
-
const readResolvers: ((value: Uint8Array | null) => void)[] = [];
|
|
61
|
-
let closed = false;
|
|
62
|
-
let writeCallCount = 0;
|
|
63
|
-
let closeCallCount = 0;
|
|
64
|
-
|
|
65
|
-
const stream = {
|
|
66
|
-
get written() { return written; },
|
|
67
|
-
get closed() { return closed; },
|
|
68
|
-
get writeCallCount() { return writeCallCount; },
|
|
69
|
-
get closeCallCount() { return closeCallCount; },
|
|
70
|
-
|
|
71
|
-
peer_node_id: () => peerId,
|
|
72
|
-
alpn: () => alpn,
|
|
73
|
-
|
|
74
|
-
write_message: async (data: Uint8Array): Promise<void> => {
|
|
75
|
-
writeCallCount++;
|
|
76
|
-
written.push(data);
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
read_message: async (): Promise<Uint8Array | null> => {
|
|
80
|
-
if (messageQueue.length > 0) {
|
|
81
|
-
return messageQueue.shift()!;
|
|
82
|
-
}
|
|
83
|
-
return new Promise<Uint8Array | null>((resolve) => {
|
|
84
|
-
readResolvers.push(resolve);
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
close: (): void => {
|
|
89
|
-
closeCallCount++;
|
|
90
|
-
closed = true;
|
|
91
|
-
for (const resolve of readResolvers.splice(0)) {
|
|
92
|
-
resolve(null);
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
pushMessage(data: Uint8Array | null): void {
|
|
97
|
-
if (readResolvers.length > 0) {
|
|
98
|
-
readResolvers.shift()!(data);
|
|
99
|
-
} else {
|
|
100
|
-
messageQueue.push(data);
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
return stream;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
type MockBiStream = ReturnType<typeof createMockBiStream>;
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// mock: MiddenStreamNode
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
function createMockMidden(nodeId: string = "a".repeat(64)) {
|
|
115
|
-
const acceptQueue: (BiStreamLike | null)[] = [];
|
|
116
|
-
const acceptResolvers: ((value: BiStreamLike | null) => void)[] = [];
|
|
117
|
-
|
|
118
|
-
// queue of overrides for open_bi - each call pops one entry (or uses default)
|
|
119
|
-
const openBiQueue: Array<MockBiStream | Error> = [];
|
|
120
|
-
let openBiCallCount = 0;
|
|
121
|
-
const openBiCalls: Array<[string, string]> = [];
|
|
122
|
-
|
|
123
|
-
const midden = {
|
|
124
|
-
get openBiCallCount() { return openBiCallCount; },
|
|
125
|
-
get openBiCalls() { return openBiCalls; },
|
|
126
|
-
|
|
127
|
-
node_id: () => nodeId,
|
|
128
|
-
|
|
129
|
-
open_bi: async (addr: string, alpn: string): Promise<BiStreamLike> => {
|
|
130
|
-
openBiCallCount++;
|
|
131
|
-
openBiCalls.push([addr, alpn]);
|
|
132
|
-
|
|
133
|
-
if (openBiQueue.length > 0) {
|
|
134
|
-
const next = openBiQueue.shift()!;
|
|
135
|
-
if (next instanceof Error) throw next;
|
|
136
|
-
return next as unknown as BiStreamLike;
|
|
137
|
-
}
|
|
138
|
-
return createMockBiStream(addr) as unknown as BiStreamLike;
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
accept: async (): Promise<BiStreamLike | null> => {
|
|
142
|
-
if (acceptQueue.length > 0) {
|
|
143
|
-
return acceptQueue.shift()!;
|
|
144
|
-
}
|
|
145
|
-
return new Promise<BiStreamLike | null>((resolve) => {
|
|
146
|
-
acceptResolvers.push(resolve);
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
pushIncoming(stream: BiStreamLike | null): void {
|
|
151
|
-
if (acceptResolvers.length > 0) {
|
|
152
|
-
acceptResolvers.shift()!(stream);
|
|
153
|
-
} else {
|
|
154
|
-
acceptQueue.push(stream);
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
queueOpenBiResult(result: MockBiStream | Error): void {
|
|
159
|
-
openBiQueue.push(result);
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
return midden;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
type MockMidden = ReturnType<typeof createMockMidden>;
|
|
167
|
-
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// identity helpers
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
function makeIdentity(nodeId: string = "a".repeat(64)): { node_id: string } {
|
|
173
|
-
return { node_id: nodeId };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// adapter factory helpers
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
function makeAdapter(opts: {
|
|
181
|
-
midden: MockMidden;
|
|
182
|
-
identity?: unknown;
|
|
183
|
-
withIdentityChange?: boolean;
|
|
184
|
-
}) {
|
|
185
|
-
let identityCb: ((id: unknown | null) => void) | null = null;
|
|
186
|
-
|
|
187
|
-
const adapter = new IrohNetworkAdapter({
|
|
188
|
-
getNode: async () => opts.midden as unknown as MiddenStreamNode,
|
|
189
|
-
getIdentity: async () => opts.identity ?? null,
|
|
190
|
-
onIdentityChange: opts.withIdentityChange !== false
|
|
191
|
-
? (cb) => {
|
|
192
|
-
identityCb = cb;
|
|
193
|
-
return () => { identityCb = null; };
|
|
194
|
-
}
|
|
195
|
-
: undefined,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
adapter,
|
|
200
|
-
triggerIdentity: (id: unknown | null) => identityCb?.(id),
|
|
201
|
-
getIdentityCb: () => identityCb,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// test runner
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
|
|
209
|
-
export async function runAutomergeTests(): Promise<{ passed: number; failed: number }> {
|
|
210
|
-
let passed = 0;
|
|
211
|
-
let failed = 0;
|
|
212
|
-
|
|
213
|
-
async function test(name: string, fn: () => void | Promise<void>): Promise<void> {
|
|
214
|
-
try {
|
|
215
|
-
await fn();
|
|
216
|
-
console.log(` ok ${name}`);
|
|
217
|
-
passed++;
|
|
218
|
-
} catch (err) {
|
|
219
|
-
console.log(` FAIL ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
220
|
-
failed++;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
console.log("automerge adapter tests\n");
|
|
225
|
-
|
|
226
|
-
// -------------------------------------------------------------------------
|
|
227
|
-
// SYNC_ALPN
|
|
228
|
-
// -------------------------------------------------------------------------
|
|
229
|
-
|
|
230
|
-
await test("SYNC_ALPN is the correct value", () => {
|
|
231
|
-
assertEqual(SYNC_ALPN, "iroh/automerge-repo/1");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// -------------------------------------------------------------------------
|
|
235
|
-
// construction and readiness
|
|
236
|
-
// -------------------------------------------------------------------------
|
|
237
|
-
|
|
238
|
-
await test("starts not ready before connect()", () => {
|
|
239
|
-
const midden = createMockMidden();
|
|
240
|
-
const { adapter } = makeAdapter({ midden });
|
|
241
|
-
assert(!adapter.isReady(), "should not be ready before connect");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
await test("isReady() is true after connect()", async () => {
|
|
245
|
-
const midden = createMockMidden();
|
|
246
|
-
const { adapter } = makeAdapter({ midden });
|
|
247
|
-
adapter.connect("our-id" as PeerId);
|
|
248
|
-
await adapter.whenReady();
|
|
249
|
-
assert(adapter.isReady(), "should be ready after connect");
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
await test("stores the peerId passed to connect()", () => {
|
|
253
|
-
const midden = createMockMidden();
|
|
254
|
-
const { adapter } = makeAdapter({ midden });
|
|
255
|
-
adapter.connect("some-peer-id" as PeerId);
|
|
256
|
-
assertEqual(adapter.peerId, "some-peer-id" as PeerId);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// -------------------------------------------------------------------------
|
|
260
|
-
// deferred initialization
|
|
261
|
-
// -------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
await test("does not call getNode when no identity and no callback fires", async () => {
|
|
264
|
-
const midden = createMockMidden();
|
|
265
|
-
const { adapter } = makeAdapter({ midden, identity: null });
|
|
266
|
-
adapter.connect("our-id" as PeerId);
|
|
267
|
-
await flush(50);
|
|
268
|
-
// no identity -> getNode should not be called (accept loop not started)
|
|
269
|
-
assertEqual(midden.openBiCallCount, 0, "getNode should not be called with no identity");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await test("calls getNode immediately when identity exists at connect() time", async () => {
|
|
273
|
-
const midden = createMockMidden();
|
|
274
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
275
|
-
adapter.connect("our-id" as PeerId);
|
|
276
|
-
await flush(50);
|
|
277
|
-
// identity present -> getNode called (via ensureMidden in initialize)
|
|
278
|
-
// the accept loop calls ensureMidden, which resolves getNode once
|
|
279
|
-
assert(true, "if no throw, getNode was called");
|
|
280
|
-
adapter.disconnect();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
await test("starts transport when identity arrives via onIdentityChange", async () => {
|
|
284
|
-
const midden = createMockMidden();
|
|
285
|
-
const { adapter, triggerIdentity } = makeAdapter({
|
|
286
|
-
midden,
|
|
287
|
-
identity: null,
|
|
288
|
-
withIdentityChange: true,
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
adapter.connect("our-id" as PeerId);
|
|
292
|
-
await flush(50);
|
|
293
|
-
|
|
294
|
-
// no identity yet - accept loop not started, no peers
|
|
295
|
-
assertEqual(adapter.getConnectionSummary().connected, 0);
|
|
296
|
-
|
|
297
|
-
// trigger identity creation
|
|
298
|
-
triggerIdentity(makeIdentity());
|
|
299
|
-
await flush(50);
|
|
300
|
-
|
|
301
|
-
// now the accept loop should have started - we can add a peer
|
|
302
|
-
const peerId = "b".repeat(64);
|
|
303
|
-
await adapter.addPeer(peerId);
|
|
304
|
-
assertEqual(adapter.getConnectionSummary().connected, 1);
|
|
305
|
-
adapter.disconnect();
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
await test("stays passive when no onIdentityChange provided and no identity", async () => {
|
|
309
|
-
const midden = createMockMidden();
|
|
310
|
-
const adapter = new IrohNetworkAdapter({
|
|
311
|
-
getNode: async () => midden as unknown as MiddenStreamNode,
|
|
312
|
-
getIdentity: async () => null,
|
|
313
|
-
// no onIdentityChange
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
adapter.connect("our-id" as PeerId);
|
|
317
|
-
await flush(50);
|
|
318
|
-
|
|
319
|
-
assertEqual(adapter.isReady(), true, "adapter reports ready");
|
|
320
|
-
// no streams, no connections - passively waiting
|
|
321
|
-
assertEqual(adapter.getConnectionSummary().connected, 0);
|
|
322
|
-
adapter.disconnect();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
await test("ignores identity change if already disconnected", async () => {
|
|
326
|
-
const midden = createMockMidden();
|
|
327
|
-
const { adapter, triggerIdentity } = makeAdapter({
|
|
328
|
-
midden,
|
|
329
|
-
identity: null,
|
|
330
|
-
withIdentityChange: true,
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
adapter.connect("our-id" as PeerId);
|
|
334
|
-
await flush(50);
|
|
335
|
-
|
|
336
|
-
adapter.disconnect();
|
|
337
|
-
triggerIdentity(makeIdentity());
|
|
338
|
-
await flush(50);
|
|
339
|
-
|
|
340
|
-
// addPeer after disconnect should throw
|
|
341
|
-
let threw = false;
|
|
342
|
-
try {
|
|
343
|
-
await adapter.addPeer("b".repeat(64));
|
|
344
|
-
} catch {
|
|
345
|
-
threw = true;
|
|
346
|
-
}
|
|
347
|
-
assert(threw, "addPeer after disconnect should throw");
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// -------------------------------------------------------------------------
|
|
351
|
-
// addPeer
|
|
352
|
-
// -------------------------------------------------------------------------
|
|
353
|
-
|
|
354
|
-
await test("addPeer calls open_bi with the correct nodeId and ALPN", async () => {
|
|
355
|
-
const midden = createMockMidden();
|
|
356
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
357
|
-
adapter.connect("our-id" as PeerId);
|
|
358
|
-
await flush(50);
|
|
359
|
-
|
|
360
|
-
const peerId = "b".repeat(64);
|
|
361
|
-
await adapter.addPeer(peerId);
|
|
362
|
-
|
|
363
|
-
assertLength(midden.openBiCalls, 1);
|
|
364
|
-
assertEqual(midden.openBiCalls[0][0], peerId);
|
|
365
|
-
assertEqual(midden.openBiCalls[0][1], SYNC_ALPN);
|
|
366
|
-
adapter.disconnect();
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
await test("addPeer emits peer-candidate with correct peerId and metadata", async () => {
|
|
370
|
-
const midden = createMockMidden();
|
|
371
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
372
|
-
adapter.connect("our-id" as PeerId);
|
|
373
|
-
await flush(50);
|
|
374
|
-
|
|
375
|
-
const peerCandidates: Array<{ peerId: PeerId; peerMetadata: unknown }> = [];
|
|
376
|
-
adapter.on("peer-candidate", (payload) => peerCandidates.push(payload as { peerId: PeerId; peerMetadata: unknown }));
|
|
377
|
-
|
|
378
|
-
const peerId = "b".repeat(64);
|
|
379
|
-
await adapter.addPeer(peerId);
|
|
380
|
-
|
|
381
|
-
assertLength(peerCandidates, 1);
|
|
382
|
-
assertEqual(peerCandidates[0].peerId, peerId as PeerId);
|
|
383
|
-
adapter.disconnect();
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
await test("addPeer skips open_bi if already connected to that peer", async () => {
|
|
387
|
-
const midden = createMockMidden();
|
|
388
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
389
|
-
adapter.connect("our-id" as PeerId);
|
|
390
|
-
await flush(50);
|
|
391
|
-
|
|
392
|
-
const peerId = "b".repeat(64);
|
|
393
|
-
await adapter.addPeer(peerId);
|
|
394
|
-
await adapter.addPeer(peerId); // second call should be a no-op
|
|
395
|
-
|
|
396
|
-
assertEqual(midden.openBiCallCount, 1, "open_bi should only be called once");
|
|
397
|
-
adapter.disconnect();
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
await test("addPeer throws when adapter is disconnected", async () => {
|
|
401
|
-
const midden = createMockMidden();
|
|
402
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
403
|
-
adapter.connect("our-id" as PeerId);
|
|
404
|
-
adapter.disconnect();
|
|
405
|
-
|
|
406
|
-
let threw = false;
|
|
407
|
-
try {
|
|
408
|
-
await adapter.addPeer("b".repeat(64));
|
|
409
|
-
} catch (err) {
|
|
410
|
-
threw = true;
|
|
411
|
-
assert(err instanceof Error && err.message === "adapter is disconnected",
|
|
412
|
-
"should throw with correct message");
|
|
413
|
-
}
|
|
414
|
-
assert(threw, "should have thrown");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
// -------------------------------------------------------------------------
|
|
418
|
-
// send
|
|
419
|
-
// -------------------------------------------------------------------------
|
|
420
|
-
|
|
421
|
-
await test("send CBOR-encodes the message and writes to the correct stream", async () => {
|
|
422
|
-
const midden = createMockMidden();
|
|
423
|
-
const mockStream = createMockBiStream("b".repeat(64));
|
|
424
|
-
midden.queueOpenBiResult(mockStream);
|
|
425
|
-
|
|
426
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
427
|
-
adapter.connect("our-id" as PeerId);
|
|
428
|
-
await flush(50);
|
|
429
|
-
|
|
430
|
-
const peerId = "b".repeat(64);
|
|
431
|
-
await adapter.addPeer(peerId);
|
|
432
|
-
|
|
433
|
-
const message = {
|
|
434
|
-
type: "sync" as const,
|
|
435
|
-
senderId: "our-id" as PeerId,
|
|
436
|
-
targetId: peerId as PeerId,
|
|
437
|
-
data: new Uint8Array([1, 2, 3]),
|
|
438
|
-
documentId: "doc-1" as unknown as Parameters<typeof cbor.encode>[0] extends { documentId: infer D } ? D : never,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
// use the raw message type
|
|
442
|
-
adapter.send(message as Parameters<typeof adapter.send>[0]);
|
|
443
|
-
await flush();
|
|
444
|
-
|
|
445
|
-
assertEqual(mockStream.writeCallCount, 1, "write_message should be called once");
|
|
446
|
-
|
|
447
|
-
const written = mockStream.written[0];
|
|
448
|
-
const decoded = cbor.decode(written) as Record<string, unknown>;
|
|
449
|
-
assertEqual(decoded["type"], "sync");
|
|
450
|
-
assertEqual(decoded["targetId"], peerId);
|
|
451
|
-
adapter.disconnect();
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
await test("send warns but does not throw when no stream for target", async () => {
|
|
455
|
-
const midden = createMockMidden();
|
|
456
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
457
|
-
adapter.connect("our-id" as PeerId);
|
|
458
|
-
await flush(50);
|
|
459
|
-
|
|
460
|
-
// should not throw
|
|
461
|
-
adapter.send({
|
|
462
|
-
type: "sync",
|
|
463
|
-
senderId: "our-id" as PeerId,
|
|
464
|
-
targetId: "nobody" as PeerId,
|
|
465
|
-
} as Parameters<typeof adapter.send>[0]);
|
|
466
|
-
adapter.disconnect();
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// -------------------------------------------------------------------------
|
|
470
|
-
// accept loop: ALPN handler registry
|
|
471
|
-
// -------------------------------------------------------------------------
|
|
472
|
-
|
|
473
|
-
await test("accept loop: incoming SYNC_ALPN stream is registered and emits peer-candidate", async () => {
|
|
474
|
-
const midden = createMockMidden();
|
|
475
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
476
|
-
adapter.connect("our-id" as PeerId);
|
|
477
|
-
await flush(50);
|
|
478
|
-
|
|
479
|
-
const peerCandidates: string[] = [];
|
|
480
|
-
adapter.on("peer-candidate", (payload) => {
|
|
481
|
-
peerCandidates.push((payload as { peerId: PeerId }).peerId);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
const peerId = "c".repeat(64);
|
|
485
|
-
const incomingStream = createMockBiStream(peerId, SYNC_ALPN);
|
|
486
|
-
midden.pushIncoming(incomingStream as unknown as BiStreamLike);
|
|
487
|
-
await flush(50);
|
|
488
|
-
|
|
489
|
-
assertIncludes(peerCandidates, peerId as PeerId);
|
|
490
|
-
adapter.disconnect();
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
await test("accept loop: unknown ALPN closes the stream and no peer-candidate emitted", async () => {
|
|
494
|
-
const midden = createMockMidden();
|
|
495
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
496
|
-
adapter.connect("our-id" as PeerId);
|
|
497
|
-
await flush(50);
|
|
498
|
-
|
|
499
|
-
const peerCandidates: string[] = [];
|
|
500
|
-
adapter.on("peer-candidate", (payload) => {
|
|
501
|
-
peerCandidates.push((payload as { peerId: PeerId }).peerId);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
const peerId = "d".repeat(64);
|
|
505
|
-
const unknownStream = createMockBiStream(peerId, "some/unknown/protocol");
|
|
506
|
-
midden.pushIncoming(unknownStream as unknown as BiStreamLike);
|
|
507
|
-
await flush(50);
|
|
508
|
-
|
|
509
|
-
assert(unknownStream.closed, "stream with unknown ALPN should be closed");
|
|
510
|
-
assert(!peerCandidates.includes(peerId as PeerId), "no peer-candidate for unknown ALPN");
|
|
511
|
-
adapter.disconnect();
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
await test("accept loop: registered ALPN handler receives stream", async () => {
|
|
515
|
-
const midden = createMockMidden();
|
|
516
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
517
|
-
adapter.connect("our-id" as PeerId);
|
|
518
|
-
await flush(50);
|
|
519
|
-
|
|
520
|
-
const handledStreams: BiStreamLike[] = [];
|
|
521
|
-
const customAlpn = "freqhole-playlistz/1";
|
|
522
|
-
adapter.registerAlpnHandler(customAlpn, (stream) => handledStreams.push(stream));
|
|
523
|
-
|
|
524
|
-
const peerId = "e".repeat(64);
|
|
525
|
-
const customStream = createMockBiStream(peerId, customAlpn);
|
|
526
|
-
midden.pushIncoming(customStream as unknown as BiStreamLike);
|
|
527
|
-
await flush(50);
|
|
528
|
-
|
|
529
|
-
assertLength(handledStreams, 1, "custom ALPN handler should receive the stream");
|
|
530
|
-
assert(!customStream.closed, "custom ALPN stream should not be closed by the adapter");
|
|
531
|
-
adapter.disconnect();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
await test("accept loop: null from accept() ends the loop", async () => {
|
|
535
|
-
const midden = createMockMidden();
|
|
536
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
537
|
-
adapter.connect("our-id" as PeerId);
|
|
538
|
-
await flush(50);
|
|
539
|
-
|
|
540
|
-
const peerCandidates: string[] = [];
|
|
541
|
-
adapter.on("peer-candidate", (payload) => {
|
|
542
|
-
peerCandidates.push((payload as { peerId: PeerId }).peerId);
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
// push null to end the accept loop
|
|
546
|
-
midden.pushIncoming(null);
|
|
547
|
-
await flush(50);
|
|
548
|
-
|
|
549
|
-
// push a real stream - accept loop is stopped, nothing should happen
|
|
550
|
-
const lateStream = createMockBiStream("f".repeat(64), SYNC_ALPN);
|
|
551
|
-
midden.pushIncoming(lateStream as unknown as BiStreamLike);
|
|
552
|
-
await flush(50);
|
|
553
|
-
|
|
554
|
-
assert(!peerCandidates.includes("f".repeat(64) as PeerId),
|
|
555
|
-
"accept loop should not process streams after null");
|
|
556
|
-
adapter.disconnect();
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
// -------------------------------------------------------------------------
|
|
560
|
-
// read loop: EOF and message decoding
|
|
561
|
-
// -------------------------------------------------------------------------
|
|
562
|
-
|
|
563
|
-
await test("read loop: CBOR message decoded and emitted with correct senderId", async () => {
|
|
564
|
-
const midden = createMockMidden();
|
|
565
|
-
const peerId = "b".repeat(64);
|
|
566
|
-
const mockStream = createMockBiStream(peerId);
|
|
567
|
-
midden.queueOpenBiResult(mockStream);
|
|
568
|
-
|
|
569
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
570
|
-
adapter.connect("our-id" as PeerId);
|
|
571
|
-
await flush(50);
|
|
572
|
-
|
|
573
|
-
await adapter.addPeer(peerId);
|
|
574
|
-
await flush();
|
|
575
|
-
|
|
576
|
-
const received: Array<Record<string, unknown>> = [];
|
|
577
|
-
adapter.on("message", (msg) => received.push(msg as Record<string, unknown>));
|
|
578
|
-
|
|
579
|
-
const outgoing = { type: "sync", senderId: peerId, targetId: "our-id", documentId: "doc-x" };
|
|
580
|
-
const encoded = cbor.encode(outgoing);
|
|
581
|
-
mockStream.pushMessage(new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength));
|
|
582
|
-
await flush(50);
|
|
583
|
-
|
|
584
|
-
assertLength(received, 1);
|
|
585
|
-
assertEqual(received[0]["senderId"] as string, peerId, "senderId must be overridden to peer's node id");
|
|
586
|
-
assertEqual(received[0]["documentId"] as string, "doc-x");
|
|
587
|
-
adapter.disconnect();
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
await test("read loop: null (EOF) emits peer-disconnected", async () => {
|
|
591
|
-
const midden = createMockMidden();
|
|
592
|
-
const peerId = "b".repeat(64);
|
|
593
|
-
const mockStream = createMockBiStream(peerId);
|
|
594
|
-
midden.queueOpenBiResult(mockStream);
|
|
595
|
-
|
|
596
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
597
|
-
adapter.connect("our-id" as PeerId);
|
|
598
|
-
await flush(50);
|
|
599
|
-
|
|
600
|
-
await adapter.addPeer(peerId);
|
|
601
|
-
await flush();
|
|
602
|
-
|
|
603
|
-
const disconnected: string[] = [];
|
|
604
|
-
adapter.on("peer-disconnected", (payload) => {
|
|
605
|
-
disconnected.push((payload as { peerId: PeerId }).peerId);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
mockStream.pushMessage(null); // simulate stream close
|
|
609
|
-
await flush(50);
|
|
610
|
-
|
|
611
|
-
assertIncludes(disconnected, peerId, "peer-disconnected should be emitted on EOF");
|
|
612
|
-
adapter.disconnect();
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
await test("read loop: EOF schedules reconnect for an intended peer", async () => {
|
|
616
|
-
const midden = createMockMidden();
|
|
617
|
-
const peerId = "b".repeat(64);
|
|
618
|
-
const mockStream = createMockBiStream(peerId);
|
|
619
|
-
midden.queueOpenBiResult(mockStream);
|
|
620
|
-
|
|
621
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
622
|
-
adapter.connect("our-id" as PeerId);
|
|
623
|
-
await flush(50);
|
|
624
|
-
|
|
625
|
-
await adapter.addPeer(peerId);
|
|
626
|
-
await flush();
|
|
627
|
-
|
|
628
|
-
// close the stream - since peerId is in intendedPeers, a reconnect should be scheduled
|
|
629
|
-
mockStream.pushMessage(null);
|
|
630
|
-
await flush(50);
|
|
631
|
-
|
|
632
|
-
// the reconnect state should show "reconnecting" (timer scheduled)
|
|
633
|
-
const summary = adapter.getConnectionSummary();
|
|
634
|
-
// connected = 0 (stream closed), reconnecting = 1 (timer pending), failed = 0
|
|
635
|
-
assertEqual(summary.connected, 0, "should not be connected after EOF");
|
|
636
|
-
assertEqual(summary.reconnecting, 1, "should be reconnecting for intended peer");
|
|
637
|
-
assertEqual(summary.failed, 0);
|
|
638
|
-
|
|
639
|
-
adapter.disconnect();
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
await test("read loop: EOF does NOT schedule reconnect for a non-intended peer", async () => {
|
|
643
|
-
const midden = createMockMidden();
|
|
644
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
645
|
-
adapter.connect("our-id" as PeerId);
|
|
646
|
-
await flush(50);
|
|
647
|
-
|
|
648
|
-
// peer connects inbound (not via addPeer - not in intendedPeers)
|
|
649
|
-
const peerId = "c".repeat(64);
|
|
650
|
-
const incomingStream = createMockBiStream(peerId, SYNC_ALPN);
|
|
651
|
-
midden.pushIncoming(incomingStream as unknown as BiStreamLike);
|
|
652
|
-
await flush(50);
|
|
653
|
-
|
|
654
|
-
incomingStream.pushMessage(null); // EOF
|
|
655
|
-
await flush(50);
|
|
656
|
-
|
|
657
|
-
const summary = adapter.getConnectionSummary();
|
|
658
|
-
assertEqual(summary.reconnecting, 0, "should not reconnect for non-intended peer");
|
|
659
|
-
adapter.disconnect();
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// -------------------------------------------------------------------------
|
|
663
|
-
// getConnectionSummary
|
|
664
|
-
// -------------------------------------------------------------------------
|
|
665
|
-
|
|
666
|
-
await test("getConnectionSummary counts connected, reconnecting, failed correctly", async () => {
|
|
667
|
-
const midden = createMockMidden();
|
|
668
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
669
|
-
adapter.connect("our-id" as PeerId);
|
|
670
|
-
await flush(50);
|
|
671
|
-
|
|
672
|
-
// add two peers - both should be connected
|
|
673
|
-
const peerA = "a".repeat(64);
|
|
674
|
-
const peerB = "b".repeat(64);
|
|
675
|
-
const streamA = createMockBiStream(peerA);
|
|
676
|
-
const streamB = createMockBiStream(peerB);
|
|
677
|
-
midden.queueOpenBiResult(streamA);
|
|
678
|
-
midden.queueOpenBiResult(streamB);
|
|
679
|
-
|
|
680
|
-
await adapter.addPeer(peerA);
|
|
681
|
-
await adapter.addPeer(peerB);
|
|
682
|
-
await flush(50);
|
|
683
|
-
|
|
684
|
-
const s1 = adapter.getConnectionSummary();
|
|
685
|
-
assertEqual(s1.connected, 2, "two connected peers");
|
|
686
|
-
assertEqual(s1.reconnecting, 0);
|
|
687
|
-
assertEqual(s1.failed, 0);
|
|
688
|
-
|
|
689
|
-
// close streamA - it should move to reconnecting
|
|
690
|
-
streamA.pushMessage(null);
|
|
691
|
-
await flush(50);
|
|
692
|
-
|
|
693
|
-
const s2 = adapter.getConnectionSummary();
|
|
694
|
-
assertEqual(s2.connected, 1, "one remaining connected peer");
|
|
695
|
-
assertEqual(s2.reconnecting, 1, "one peer in reconnect backoff");
|
|
696
|
-
|
|
697
|
-
adapter.disconnect();
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
// -------------------------------------------------------------------------
|
|
701
|
-
// forgetPeer / isConnected
|
|
702
|
-
// -------------------------------------------------------------------------
|
|
703
|
-
|
|
704
|
-
await test("forgetPeer removes peer from intendedPeers and closes stream", async () => {
|
|
705
|
-
const midden = createMockMidden();
|
|
706
|
-
const peerId = "b".repeat(64);
|
|
707
|
-
const mockStream = createMockBiStream(peerId);
|
|
708
|
-
midden.queueOpenBiResult(mockStream);
|
|
709
|
-
|
|
710
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
711
|
-
adapter.connect("our-id" as PeerId);
|
|
712
|
-
await flush(50);
|
|
713
|
-
|
|
714
|
-
await adapter.addPeer(peerId);
|
|
715
|
-
assert(adapter.isConnected(peerId), "should be connected after addPeer");
|
|
716
|
-
|
|
717
|
-
adapter.forgetPeer(peerId);
|
|
718
|
-
await flush(10);
|
|
719
|
-
|
|
720
|
-
assert(!adapter.isConnected(peerId), "should not be connected after forgetPeer");
|
|
721
|
-
assert(mockStream.closed, "stream should be closed after forgetPeer");
|
|
722
|
-
assertEqual(adapter.getConnectionSummary().reconnecting, 0,
|
|
723
|
-
"should not schedule reconnect after forgetPeer");
|
|
724
|
-
adapter.disconnect();
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
// -------------------------------------------------------------------------
|
|
728
|
-
// disconnect
|
|
729
|
-
// -------------------------------------------------------------------------
|
|
730
|
-
|
|
731
|
-
await test("disconnect emits peer-disconnected for each connected peer", async () => {
|
|
732
|
-
const midden = createMockMidden();
|
|
733
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
734
|
-
adapter.connect("our-id" as PeerId);
|
|
735
|
-
await flush(50);
|
|
736
|
-
|
|
737
|
-
const peerB = "b".repeat(64);
|
|
738
|
-
const peerC = "c".repeat(64);
|
|
739
|
-
await adapter.addPeer(peerB);
|
|
740
|
-
await adapter.addPeer(peerC);
|
|
741
|
-
|
|
742
|
-
const disconnected: string[] = [];
|
|
743
|
-
adapter.on("peer-disconnected", (payload) => {
|
|
744
|
-
disconnected.push((payload as { peerId: PeerId }).peerId);
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
adapter.disconnect();
|
|
748
|
-
|
|
749
|
-
assertLength(disconnected, 2);
|
|
750
|
-
assertIncludes(disconnected, peerB as PeerId);
|
|
751
|
-
assertIncludes(disconnected, peerC as PeerId);
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
await test("disconnect emits close event", async () => {
|
|
755
|
-
const midden = createMockMidden();
|
|
756
|
-
const { adapter } = makeAdapter({ midden, identity: makeIdentity() });
|
|
757
|
-
adapter.connect("our-id" as PeerId);
|
|
758
|
-
await flush(50);
|
|
759
|
-
|
|
760
|
-
let closed = false;
|
|
761
|
-
adapter.on("close", () => { closed = true; });
|
|
762
|
-
adapter.disconnect();
|
|
763
|
-
|
|
764
|
-
assert(closed, "close event should fire on disconnect");
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
await test("disconnect clears identity subscription", async () => {
|
|
768
|
-
const midden = createMockMidden();
|
|
769
|
-
const { adapter, getIdentityCb } = makeAdapter({
|
|
770
|
-
midden,
|
|
771
|
-
identity: null,
|
|
772
|
-
withIdentityChange: true,
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
adapter.connect("our-id" as PeerId);
|
|
776
|
-
await flush(50);
|
|
777
|
-
|
|
778
|
-
assert(getIdentityCb() !== null, "identity callback should be registered");
|
|
779
|
-
|
|
780
|
-
adapter.disconnect();
|
|
781
|
-
|
|
782
|
-
assert(getIdentityCb() === null, "identity callback should be cleared on disconnect");
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
// -------------------------------------------------------------------------
|
|
786
|
-
// summary of skipped tests
|
|
787
|
-
// -------------------------------------------------------------------------
|
|
788
|
-
|
|
789
|
-
console.log("\n skipped (no fake timer support in custom runner):");
|
|
790
|
-
console.log(" - reconnect backoff timing (exponential delay + jitter verification)");
|
|
791
|
-
console.log(" - max attempts gives up and moves peer to failedPeers");
|
|
792
|
-
console.log(" - retryFailedPeers restores and reconnects failed peers");
|
|
793
|
-
console.log(" - second disconnect() call is idempotent (throw check)");
|
|
794
|
-
|
|
795
|
-
return { passed, failed };
|
|
796
|
-
}
|