fakeswarm 0.1.2 → 0.2.1
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/README.md +13 -2
- package/package.json +1 -1
- package/src/index.js +70 -7
- package/src/normalize.js +17 -0
package/README.md
CHANGED
|
@@ -47,8 +47,10 @@ Always call `close()` when done to unpublish topics and destroy sockets.
|
|
|
47
47
|
|
|
48
48
|
## API
|
|
49
49
|
|
|
50
|
-
### createFakeSwarm(
|
|
51
|
-
- `
|
|
50
|
+
### createFakeSwarm(seedOrOpts?, topics?)
|
|
51
|
+
- `seedOrOpts` can be:
|
|
52
|
+
- `Buffer | Uint8Array | null` (legacy seed) or
|
|
53
|
+
- `{ seed?, net? }`
|
|
52
54
|
- `topics` (Map) optional shared topics map. By default a module-level Map is used so multiple swarms share the same space.
|
|
53
55
|
|
|
54
56
|
Returns an object with:
|
|
@@ -68,9 +70,18 @@ Returns an object with:
|
|
|
68
70
|
- `keyPair`: the generated keypair.
|
|
69
71
|
- `id`: `idEncoding.encode(publicKey)` convenience string.
|
|
70
72
|
|
|
73
|
+
#### Network profile (opt-in, default off)
|
|
74
|
+
`net.reconnectRace` lets you simulate a quick disconnect/reconnect overlap:
|
|
75
|
+
- `enabled` (boolean): default false. When false, behavior is unchanged.
|
|
76
|
+
- `staleRetentionMs` (default 25): how long a closed socket is retained internally before destroy.
|
|
77
|
+
- `reconnectDelayMs` (default 10): how soon a reconnect attempt is triggered.
|
|
78
|
+
- `duplicateConnection` (boolean): if true, emit a second `connection` event for the new socket even while the stale one is retained.
|
|
79
|
+
`connections` still exposes only the current socket per peer; stale sockets are kept internally and cleaned up after the retention window.
|
|
80
|
+
|
|
71
81
|
### Join / leave semantics
|
|
72
82
|
- Joining registers this swarm in the shared topics map; peers that share a topic will connect.
|
|
73
83
|
- Leaving (or closing) removes the entry from the shared map to avoid ghost peers.
|
|
84
|
+
- **Testing tip:** pass a fresh `topics = new Map()` per test to avoid cross-test bleed; the default module-level map is shared across all swarms in the process.
|
|
74
85
|
|
|
75
86
|
### Determinism notes
|
|
76
87
|
- Dial election: only the lexicographically smaller peerId initiates (`peerId < remotePeerId`), preventing double connects.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,10 +2,20 @@ import EventEmitter from "events";
|
|
|
2
2
|
import idEncoding from "hypercore-id-encoding";
|
|
3
3
|
import Krypto from "hypercore-crypto";
|
|
4
4
|
import NoiseStream from "@hyperswarm/secret-stream";
|
|
5
|
+
import { normalizeArgs } from "./normalize.js";
|
|
5
6
|
|
|
6
7
|
const defaultTopics = new Map();
|
|
7
8
|
|
|
8
|
-
function createFakeSwarm(
|
|
9
|
+
function createFakeSwarm(seedOrOpts = undefined, topicsArg = defaultTopics) {
|
|
10
|
+
const { seed, net, topics } = normalizeArgs(seedOrOpts, topicsArg);
|
|
11
|
+
|
|
12
|
+
const reconnectRace = {
|
|
13
|
+
enabled: !!net?.reconnectRace?.enabled,
|
|
14
|
+
staleRetentionMs: net?.reconnectRace?.staleRetentionMs ?? 25,
|
|
15
|
+
reconnectDelayMs: net?.reconnectRace?.reconnectDelayMs ?? 10,
|
|
16
|
+
duplicateConnection: !!net?.reconnectRace?.duplicateConnection
|
|
17
|
+
};
|
|
18
|
+
|
|
9
19
|
const keyPair = Krypto.keyPair(seed);
|
|
10
20
|
const peerId = idEncoding.encode(keyPair.publicKey);
|
|
11
21
|
|
|
@@ -15,7 +25,15 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
15
25
|
// remotePeerId -> Promise<void> (prevents duplicate concurrent dials)
|
|
16
26
|
const connecting = new Map();
|
|
17
27
|
|
|
28
|
+
// remotePeerId -> array of stale sockets kept temporarily
|
|
29
|
+
const staleSockets = new Map();
|
|
30
|
+
|
|
31
|
+
// timers we await during flush/close for determinism
|
|
32
|
+
const retentionTimers = new Set();
|
|
33
|
+
const reconnectTimers = new Set();
|
|
34
|
+
|
|
18
35
|
const emitter = new EventEmitter();
|
|
36
|
+
emitter.setMaxListeners(0);
|
|
19
37
|
const joinedTopics = new Set(); // all topics we joined (regardless of mode)
|
|
20
38
|
const dialTopics = new Set(); // topics where we will attempt outbound dials (client mode)
|
|
21
39
|
const publishedTopics = new Set(); // topics we publish into the shared map (server mode)
|
|
@@ -89,10 +107,44 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
89
107
|
emitter.off(event, cb);
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
function retainStale(peer, socket) {
|
|
111
|
+
if (!reconnectRace.enabled) return;
|
|
112
|
+
const timer = setTimeout(async () => {
|
|
113
|
+
retentionTimers.delete(timer);
|
|
114
|
+
await destroySocket(socket);
|
|
115
|
+
const arr = staleSockets.get(peer);
|
|
116
|
+
if (arr) {
|
|
117
|
+
staleSockets.set(peer, arr.filter((s) => s !== socket));
|
|
118
|
+
if (staleSockets.get(peer).length === 0) staleSockets.delete(peer);
|
|
119
|
+
}
|
|
120
|
+
}, reconnectRace.staleRetentionMs);
|
|
121
|
+
retentionTimers.add(timer);
|
|
122
|
+
const arr = staleSockets.get(peer) ?? [];
|
|
123
|
+
arr.push(socket);
|
|
124
|
+
staleSockets.set(peer, arr);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function scheduleReconnectTick() {
|
|
128
|
+
if (!reconnectRace.enabled) return;
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
reconnectTimers.delete(timer);
|
|
131
|
+
if (!closing && !closed) tick();
|
|
132
|
+
}, reconnectRace.reconnectDelayMs);
|
|
133
|
+
reconnectTimers.add(timer);
|
|
134
|
+
}
|
|
135
|
+
|
|
92
136
|
function trackConnection(peer, socket) {
|
|
93
137
|
const drop = () => {
|
|
94
|
-
connections.
|
|
95
|
-
|
|
138
|
+
const current = connections.get(peer)?.socket;
|
|
139
|
+
if (current === socket) {
|
|
140
|
+
connections.delete(peer);
|
|
141
|
+
emitter.emit("update");
|
|
142
|
+
retainStale(peer, socket);
|
|
143
|
+
scheduleReconnectTick();
|
|
144
|
+
} else if (reconnectRace.enabled) {
|
|
145
|
+
// stale socket closed after new one took over
|
|
146
|
+
retainStale(peer, socket);
|
|
147
|
+
}
|
|
96
148
|
};
|
|
97
149
|
socket.once("close", drop);
|
|
98
150
|
socket.once("error", drop);
|
|
@@ -102,7 +154,10 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
102
154
|
if (closing || closed) return;
|
|
103
155
|
if (!conn || !conn.id) return;
|
|
104
156
|
if (conn.id === peerId) return;
|
|
105
|
-
if (connections.has(conn.id))
|
|
157
|
+
if (connections.has(conn.id)) {
|
|
158
|
+
if (!reconnectRace.duplicateConnection) return;
|
|
159
|
+
// allow duplicate event later; still accept for overlap
|
|
160
|
+
}
|
|
106
161
|
|
|
107
162
|
// Guard: if we're already in-flight dialing this peer, don't also accept/construct.
|
|
108
163
|
// (This is conservative; the dial-election below in loop() is the main duplication killer.)
|
|
@@ -143,7 +198,7 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
143
198
|
async function dial(remotePeerId, makeConnection) {
|
|
144
199
|
if (closing || closed) return;
|
|
145
200
|
if (remotePeerId === peerId) return;
|
|
146
|
-
if (connections.has(remotePeerId)) return;
|
|
201
|
+
if (connections.has(remotePeerId) && !reconnectRace.duplicateConnection) return;
|
|
147
202
|
|
|
148
203
|
if (!shouldDial(remotePeerId)) return;
|
|
149
204
|
if (connecting.has(remotePeerId)) return;
|
|
@@ -161,7 +216,7 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
161
216
|
if (!socket) return;
|
|
162
217
|
|
|
163
218
|
// It's possible we raced and connected through the other side while awaiting.
|
|
164
|
-
if (connections.has(remotePeerId)) {
|
|
219
|
+
if (connections.has(remotePeerId) && !reconnectRace.duplicateConnection) {
|
|
165
220
|
if (socket?.destroy) await destroySocket(socket);
|
|
166
221
|
return;
|
|
167
222
|
}
|
|
@@ -224,7 +279,7 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
224
279
|
// Wait for any in-flight dial promises.
|
|
225
280
|
await Promise.allSettled(Array.from(connecting.values()));
|
|
226
281
|
|
|
227
|
-
if (connecting.size === 0) {
|
|
282
|
+
if (connecting.size === 0 && retentionTimers.size === 0 && reconnectTimers.size === 0) {
|
|
228
283
|
stableZero += 1;
|
|
229
284
|
if (stableZero >= 2) return; // two consecutive quiet checks
|
|
230
285
|
} else {
|
|
@@ -263,6 +318,10 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
263
318
|
closing = true;
|
|
264
319
|
|
|
265
320
|
if (tickTimer) clearTimeout(tickTimer);
|
|
321
|
+
for (const t of Array.from(retentionTimers)) clearTimeout(t);
|
|
322
|
+
for (const t of Array.from(reconnectTimers)) clearTimeout(t);
|
|
323
|
+
retentionTimers.clear();
|
|
324
|
+
reconnectTimers.clear();
|
|
266
325
|
|
|
267
326
|
// Stop publishing our topics (prevents new dials toward us)
|
|
268
327
|
for (const topicId of Array.from(publishedTopics)) {
|
|
@@ -279,6 +338,10 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
279
338
|
await Promise.allSettled(
|
|
280
339
|
Array.from(connections.values()).map(({ socket }) => destroySocket(socket))
|
|
281
340
|
);
|
|
341
|
+
await Promise.allSettled(
|
|
342
|
+
Array.from(staleSockets.values()).flat().map((socket) => destroySocket(socket))
|
|
343
|
+
);
|
|
344
|
+
staleSockets.clear();
|
|
282
345
|
|
|
283
346
|
connections.clear();
|
|
284
347
|
emitter.emit("update");
|
package/src/normalize.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizeArgs(seedOrOpts, topicsArg) {
|
|
2
|
+
const isBufferLike =
|
|
3
|
+
seedOrOpts &&
|
|
4
|
+
(Buffer.isBuffer(seedOrOpts) ||
|
|
5
|
+
seedOrOpts instanceof Uint8Array);
|
|
6
|
+
|
|
7
|
+
if (isBufferLike || seedOrOpts === null || seedOrOpts === undefined) {
|
|
8
|
+
return { seed: seedOrOpts, net: undefined, topics: topicsArg };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof seedOrOpts === "object") {
|
|
12
|
+
const { seed = undefined, net = undefined } = seedOrOpts;
|
|
13
|
+
return { seed, net, topics: topicsArg };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { seed: undefined, net: undefined, topics: topicsArg };
|
|
17
|
+
}
|