fakeswarm 0.1.2 → 0.2.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.
- package/README.md +12 -2
- package/package.json +1 -1
- package/src/index.js +69 -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,6 +70,14 @@ 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.
|
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,6 +25,13 @@ 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();
|
|
19
36
|
const joinedTopics = new Set(); // all topics we joined (regardless of mode)
|
|
20
37
|
const dialTopics = new Set(); // topics where we will attempt outbound dials (client mode)
|
|
@@ -89,10 +106,44 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
89
106
|
emitter.off(event, cb);
|
|
90
107
|
}
|
|
91
108
|
|
|
109
|
+
function retainStale(peer, socket) {
|
|
110
|
+
if (!reconnectRace.enabled) return;
|
|
111
|
+
const timer = setTimeout(async () => {
|
|
112
|
+
retentionTimers.delete(timer);
|
|
113
|
+
await destroySocket(socket);
|
|
114
|
+
const arr = staleSockets.get(peer);
|
|
115
|
+
if (arr) {
|
|
116
|
+
staleSockets.set(peer, arr.filter((s) => s !== socket));
|
|
117
|
+
if (staleSockets.get(peer).length === 0) staleSockets.delete(peer);
|
|
118
|
+
}
|
|
119
|
+
}, reconnectRace.staleRetentionMs);
|
|
120
|
+
retentionTimers.add(timer);
|
|
121
|
+
const arr = staleSockets.get(peer) ?? [];
|
|
122
|
+
arr.push(socket);
|
|
123
|
+
staleSockets.set(peer, arr);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scheduleReconnectTick() {
|
|
127
|
+
if (!reconnectRace.enabled) return;
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
reconnectTimers.delete(timer);
|
|
130
|
+
if (!closing && !closed) tick();
|
|
131
|
+
}, reconnectRace.reconnectDelayMs);
|
|
132
|
+
reconnectTimers.add(timer);
|
|
133
|
+
}
|
|
134
|
+
|
|
92
135
|
function trackConnection(peer, socket) {
|
|
93
136
|
const drop = () => {
|
|
94
|
-
connections.
|
|
95
|
-
|
|
137
|
+
const current = connections.get(peer)?.socket;
|
|
138
|
+
if (current === socket) {
|
|
139
|
+
connections.delete(peer);
|
|
140
|
+
emitter.emit("update");
|
|
141
|
+
retainStale(peer, socket);
|
|
142
|
+
scheduleReconnectTick();
|
|
143
|
+
} else if (reconnectRace.enabled) {
|
|
144
|
+
// stale socket closed after new one took over
|
|
145
|
+
retainStale(peer, socket);
|
|
146
|
+
}
|
|
96
147
|
};
|
|
97
148
|
socket.once("close", drop);
|
|
98
149
|
socket.once("error", drop);
|
|
@@ -102,7 +153,10 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
102
153
|
if (closing || closed) return;
|
|
103
154
|
if (!conn || !conn.id) return;
|
|
104
155
|
if (conn.id === peerId) return;
|
|
105
|
-
if (connections.has(conn.id))
|
|
156
|
+
if (connections.has(conn.id)) {
|
|
157
|
+
if (!reconnectRace.duplicateConnection) return;
|
|
158
|
+
// allow duplicate event later; still accept for overlap
|
|
159
|
+
}
|
|
106
160
|
|
|
107
161
|
// Guard: if we're already in-flight dialing this peer, don't also accept/construct.
|
|
108
162
|
// (This is conservative; the dial-election below in loop() is the main duplication killer.)
|
|
@@ -143,7 +197,7 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
143
197
|
async function dial(remotePeerId, makeConnection) {
|
|
144
198
|
if (closing || closed) return;
|
|
145
199
|
if (remotePeerId === peerId) return;
|
|
146
|
-
if (connections.has(remotePeerId)) return;
|
|
200
|
+
if (connections.has(remotePeerId) && !reconnectRace.duplicateConnection) return;
|
|
147
201
|
|
|
148
202
|
if (!shouldDial(remotePeerId)) return;
|
|
149
203
|
if (connecting.has(remotePeerId)) return;
|
|
@@ -161,7 +215,7 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
161
215
|
if (!socket) return;
|
|
162
216
|
|
|
163
217
|
// It's possible we raced and connected through the other side while awaiting.
|
|
164
|
-
if (connections.has(remotePeerId)) {
|
|
218
|
+
if (connections.has(remotePeerId) && !reconnectRace.duplicateConnection) {
|
|
165
219
|
if (socket?.destroy) await destroySocket(socket);
|
|
166
220
|
return;
|
|
167
221
|
}
|
|
@@ -224,7 +278,7 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
224
278
|
// Wait for any in-flight dial promises.
|
|
225
279
|
await Promise.allSettled(Array.from(connecting.values()));
|
|
226
280
|
|
|
227
|
-
if (connecting.size === 0) {
|
|
281
|
+
if (connecting.size === 0 && retentionTimers.size === 0 && reconnectTimers.size === 0) {
|
|
228
282
|
stableZero += 1;
|
|
229
283
|
if (stableZero >= 2) return; // two consecutive quiet checks
|
|
230
284
|
} else {
|
|
@@ -263,6 +317,10 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
263
317
|
closing = true;
|
|
264
318
|
|
|
265
319
|
if (tickTimer) clearTimeout(tickTimer);
|
|
320
|
+
for (const t of Array.from(retentionTimers)) clearTimeout(t);
|
|
321
|
+
for (const t of Array.from(reconnectTimers)) clearTimeout(t);
|
|
322
|
+
retentionTimers.clear();
|
|
323
|
+
reconnectTimers.clear();
|
|
266
324
|
|
|
267
325
|
// Stop publishing our topics (prevents new dials toward us)
|
|
268
326
|
for (const topicId of Array.from(publishedTopics)) {
|
|
@@ -279,6 +337,10 @@ function createFakeSwarm(seed = undefined, topics = defaultTopics) {
|
|
|
279
337
|
await Promise.allSettled(
|
|
280
338
|
Array.from(connections.values()).map(({ socket }) => destroySocket(socket))
|
|
281
339
|
);
|
|
340
|
+
await Promise.allSettled(
|
|
341
|
+
Array.from(staleSockets.values()).flat().map((socket) => destroySocket(socket))
|
|
342
|
+
);
|
|
343
|
+
staleSockets.clear();
|
|
282
344
|
|
|
283
345
|
connections.clear();
|
|
284
346
|
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
|
+
}
|