OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / 829553c

test: a JS control, and our connect-failure rate turns out to be the network's
Two valid session-daemon runs gave 19/20 and 16/20 on the public DHT — about a 15% connect-failure rate — and nothing in the harness could distinguish an ordinary flaky DHT lookup from a defect of ours. I was one step from investigating it. daemon/test_daemon_longlived.py now takes --js and runs the IDENTICAL accounting against hyperswarm 4.17.1 via daemon/js_peer.js, which derives the topic as BLAKE2b(seed) exactly as daemon/main.cpp does. One driver, one set of thresholds, one clock; only the peer implementation changes. Two separate harnesses would have let the numbers drift for reasons unrelated to the code. hyperswarm 4.17.1 18/20 90% session-daemon 19/20 95% session-daemon 16/20 80% ours combined 35/40 87.5% vs JS 90% Indistinguishable. The public DHT drops roughly one connection in ten at this cadence and we match the reference. The 15% was never ours. ⚠ n=20 per run: 90% vs 87.5% is well inside binomial noise. This rules out a gross defect and nothing smaller. The --js path returns PASS unconditionally with a CONTROL note — a control measures the network, and letting it gate our build would be a category error. Also installs hyperswarm 4.17.1 (upstream exact; the KB clone is 4.17.0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
829553caa4a6b10943bb1394e8d828746bf9b1ab
by plankatron <fleeky@gmail.com> · 2026-09-10T16:33:22.000Z

Changed files (28) +2539 -6

daemon/js_peer.js +70 -0
……@@ -0,0 +1,70 @@
1// The JS CONTROL for test_daemon_longlived.py.
2//
3// Same shape as session-daemon: join one topic on the public DHT and report
4// `peers=N` on a pulse, so the SAME Python driver and the SAME accounting can
5// measure both implementations. The point is not to test hyperswarm — it is to
6// find out what the public DHT's own connect-failure rate looks like, so our
7// number has something to be compared against.
8//
9// node js_peer.js --topic <seed> [--seconds N]
10//
11// The topic is BLAKE2b(seed), exactly as daemon/main.cpp derives it, so a JS
12// peer and a C++ daemon on the same seed land on the same topic.
13
14const Hyperswarm = require('hyperswarm')
15const sodium = require('sodium-universal')
16const b4a = require('b4a')
17
18const argv = process.argv.slice(2)
19function arg (name, dflt) {
20 const i = argv.indexOf(name)
21 return i === -1 ? dflt : argv[i + 1]
22}
23
24const seed = arg('--topic', null)
25const seconds = Number(arg('--seconds', '0'))
26if (!seed) {
27 console.error('usage: node js_peer.js --topic <seed> [--seconds N]')
28 process.exit(1)
29}
30
31const topic = b4a.alloc(32)
32sodium.crypto_generichash(topic, b4a.from(seed))
33
34const swarm = new Hyperswarm()
35let peers = 0
36
37swarm.on('connection', (conn) => {
38 peers++
39 pulse()
40 conn.on('error', () => {})
41 conn.on('close', () => { peers--; pulse() })
42 // Say something, so the connection is real rather than merely opened.
43 conn.write(b4a.from('hello'))
44 conn.on('data', () => {})
45})
46
47function pulse () {
48 const dht = swarm.dht
49 const up = dht && dht.bootstrapped ? 'up' : 'connecting'
50 console.log(`[js] dht=${up} peers=${peers}`)
51}
52
53async function main () {
54 await swarm.dht.ready()
55 pulse()
56 swarm.join(topic, { server: true, client: true })
57 await swarm.flush()
58 pulse()
59
60 const timer = setInterval(pulse, 5000)
61 if (seconds > 0) {
62 setTimeout(async () => {
63 clearInterval(timer)
64 await swarm.destroy()
65 process.exit(0)
66 }, seconds * 1000)
67 }
68}
69
70main().catch((err) => { console.error(err); process.exit(1) })
daemon/test_daemon_longlived.py +29 -6
……@@ -36,6 +36,15 @@ import time
3636HERE = os.path.dirname(os.path.realpath(__file__))
3737DAEMON = os.path.join(HERE, "session-daemon")
3838
39# ⛔ THE JS CONTROL. Our own pass rate means nothing on its own: the public DHT
40# has a failure rate of its own, and without measuring it we cannot tell an
41# ordinary flaky lookup from a defect of ours. `--js` runs the IDENTICAL driver
42# and the IDENTICAL accounting against hyperswarm 4.17.1 instead of
43# session-daemon, so the two numbers are comparable by construction.
44USE_JS = "--js" in sys.argv
45JS_PEER = os.path.join(HERE, "js_peer.js")
46NODE_MODULES = os.path.normpath(os.path.join(HERE, "..", "test", "node_modules"))
47
3948PEERS = 20 # must exceed the 16-slot pool
4049BASELINE = 5 # of the first 8, at least this many must connect
4150CONNECT_TIMEOUT = 45.0
……@@ -45,11 +54,16 @@ EXIT_PASS, EXIT_FAIL, EXIT_INCONCLUSIVE = 0, 1, 2
4554
4655
4756def spawn(topic, tmp, tag):
48 sock = os.path.join(tmp, "%s.sock" % tag)
4957 logpath = os.path.join(tmp, "%s.log" % tag)
5058 log = open(logpath, "wb")
51 p = subprocess.Popen([DAEMON, "--topic", topic, "--socket", sock],
52 stdout=log, stderr=subprocess.STDOUT)
59 if USE_JS:
60 env = dict(os.environ, NODE_PATH=NODE_MODULES)
61 cmd = ["node", JS_PEER, "--topic", topic]
62 p = subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT, env=env)
63 else:
64 sock = os.path.join(tmp, "%s.sock" % tag)
65 cmd = [DAEMON, "--topic", topic, "--socket", sock]
66 p = subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT)
5367 return p, log, logpath
5468
5569
……@@ -82,15 +96,17 @@ def stop(proc, log):
8296
8397
8498def main():
85 if not os.path.isfile(DAEMON):
86 print("missing %s — run daemon/build.sh" % DAEMON)
99 target = JS_PEER if USE_JS else DAEMON
100 if not os.path.isfile(target):
101 print("missing %s%s" % (target, "" if USE_JS else " — run daemon/build.sh"))
87102 return EXIT_INCONCLUSIVE
88103
89104 topic = "pool-drain-%d" % os.getpid()
90105 tmp = tempfile.mkdtemp(prefix="longlived-")
91106
92107 print("=" * 78)
93 print("a long-lived session-daemon on the PUBLIC DHT, %d sequential peers" % PEERS)
108 print("a long-lived %s on the PUBLIC DHT, %d sequential peers"
109 % ("hyperswarm 4.17.1 (JS CONTROL)" if USE_JS else "session-daemon", PEERS))
94110 print("=" * 78)
95111 print("topic: %s" % topic)
96112 print("logs: %s" % tmp)
……@@ -133,6 +149,13 @@ def main():
133149 % (sum(early), len(early)))
134150 return EXIT_INCONCLUSIVE
135151
152 if USE_JS:
153 # The control measures the network, it does not gate our code.
154 print("\nCONTROL: this is hyperswarm's own rate on the public DHT "
155 "today. Compare it with the session-daemon run before reading "
156 "anything into ours.")
157 return EXIT_PASS
158
136159 if pool_full:
137160 print("\nFAIL: the host exhausted its handshake pool — #72's wall, "
138161 "unfixed.")
docs/PARITY-hyperswarm.md +34 -0
……@@ -895,3 +895,37 @@ avoiding interference with the older `join_topic`/`connect_to_peer` API. A
895895deliberate deviation, not a defect.
896896
897897Suite: 13 binaries green.
898
899## 14. Slice 10 — the JS CONTROL: our connect-failure rate is the network's
900
901A pass rate means nothing on its own. Two valid session-daemon runs gave 19/20
902and 16/20 — a ~15% failure rate — and there was no way to tell an ordinary flaky
903public-DHT lookup from a defect of ours.
904
905So the driver gained a `--js` flag that runs the IDENTICAL accounting against
906hyperswarm 4.17.1 (`daemon/js_peer.js`), which derives the topic as
907`BLAKE2b(seed)` exactly as `daemon/main.cpp` does. Same code path, same
908thresholds, same clock — only the peer implementation differs.
909
910 hyperswarm 4.17.1 18/20 90% failures at 19, 20
911 session-daemon 19/20 95% failure at 20
912 session-daemon 16/20 80% failures at 13, 16, 18, 20
913
914 ours combined 35/40 87.5% vs JS 18/20 90%
915
916**Indistinguishable.** The public DHT drops roughly one connection in ten at this
917cadence and we match the reference. The ~15% was never ours to fix.
918
919⚠ **n = 20 per run.** 90% vs 87.5% is well inside binomial noise. This rules out
920a GROSS defect — we are not at 50% while JS is at 95% — and rules out nothing
921smaller. A few percent would need hundreds of trials and a different harness.
922
923⚠ JS's two failures were the LAST two peers; ours were scattered. Recorded, not
924interpreted: at one JS run that is not evidence of anything.
925
926⛔ The `--js` path returns PASS unconditionally with a CONTROL note. A control
927measures the NETWORK; letting it gate our build would be a category error.
928
929**Why this belongs in a parity audit:** it is the only measurement here that
930could have said "stop looking". Every other slice found something. This one
931found that the thing I was about to chase was the DHT behaving normally.
test/node_modules/.package-lock.json +30 -0
……@@ -377,6 +377,21 @@
377377 "b4a": "^1.3.0"
378378 }
379379 },
380 "node_modules/hyperswarm": {
381 "version": "4.17.1",
382 "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.1.tgz",
383 "integrity": "sha512-bFy89nulBSY1qQZMZMGWb6wwBHftthONZr5IpX4o7hcSmMQI5IHyfA7Id3KU03SDflWfPvEm1YJy0AL5BFFblA==",
384 "license": "MIT",
385 "dependencies": {
386 "b4a": "^1.3.1",
387 "bare-events": "^2.2.0",
388 "hyperdht": "^6.21.0",
389 "safety-catch": "^1.0.2",
390 "shuffled-priority-queue": "^2.1.0",
391 "streamx": "^2.22.1",
392 "unslab": "^1.3.0"
393 }
394 },
380395 "node_modules/is-property": {
381396 "version": "1.0.2",
382397 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
……@@ -472,6 +487,15 @@
472487 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
473488 "license": "MIT"
474489 },
490 "node_modules/shuffled-priority-queue": {
491 "version": "2.1.0",
492 "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
493 "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
494 "license": "MIT",
495 "dependencies": {
496 "unordered-set": "^2.0.1"
497 }
498 },
475499 "node_modules/signal-promise": {
476500 "version": "1.0.3",
477501 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
……@@ -574,6 +598,12 @@
574598 "bare": ">=1.17.4"
575599 }
576600 },
601 "node_modules/unordered-set": {
602 "version": "2.0.1",
603 "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
604 "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
605 "license": "MIT"
606 },
577607 "node_modules/unslab": {
578608 "version": "1.3.0",
579609 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
test/node_modules/hyperswarm/LICENSE +21 -0
……@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) 2019 Mathias Buus, Paul Frazee, David Mark Clements and contributors
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
test/node_modules/hyperswarm/README.md +199 -0
……@@ -0,0 +1,199 @@
1# hyperswarm
2
3### [See the full API docs at docs.pears.com](https://docs.pears.com/building-blocks/hyperswarm)
4
5A high-level API for finding and connecting to peers who are interested in a "topic."
6
7## Installation
8
9```
10npm install hyperswarm
11```
12
13## Usage
14
15```js
16const Hyperswarm = require('hyperswarm')
17
18const swarm = new Hyperswarm()
19
20swarm1.on('connection', (conn) => {
21 // swarm1 will receive server connections
22 conn.write('this is a server connection')
23 conn.end()
24})
25
26const discoveryKey = Buffer.alloc(32).fill('hello world') // must be 32 bytes
27
28// join the swarm, others will find you
29swarm2.join(discoveryKey, { server: true, client: true })
30```
31
32## Hyperswarm API
33
34#### `const swarm = new Hyperswarm(opts = {})`
35
36Construct a new Hyperswarm instance.
37
38`opts` can include:
39
40- `keyPair`: A Noise keypair that will be used to listen/connect on the DHT. Defaults to a new key pair.
41- `seed`: A unique, 32-byte, random seed that can be used to deterministically generate the key pair.
42- `maxPeers`: The maximum number of peer connections to allow.
43- `firewall`: A sync function of the form `remotePublicKey => (true|false)`. If true, the connection will be rejected. Defaults to allowing all connections.
44- `dht`: A DHT instance. Defaults to a new instance.
45
46#### `swarm.connecting`
47
48Number that indicates connections in progress.
49
50#### `swarm.connections`
51
52A set of all active client/server connections.
53
54#### `swarm.peers`
55
56A Map containing all connected peers, of the form: `(Noise public key hex string) -> PeerInfo object`
57
58See the [`PeerInfo`](#peerinfo-api) API for more details.
59
60#### `swarm.dht`
61
62A [`hyperdht`](https://github.com/holepunchto/hyperdht) instance. Useful if you want lower-level control over Hyperswarm's networking.
63
64#### `swarm.on('connection', (socket, peerInfo) => {})`
65
66Emitted whenever the swarm connects to a new peer.
67
68`socket` is an end-to-end (Noise) encrypted Duplex stream
69
70`peerInfo` is a [`PeerInfo`](#peerinfo-api) instance
71
72#### `swarm.on('update', () => {})`
73
74Emitted when internal values are changed, useful for user interfaces.
75
76For example: emitted when `swarm.connecting` or `swarm.connections` changes.
77
78#### `const discovery = swarm.join(topic, opts = {})`
79
80Start discovering and connecting to peers sharing a common topic. As new peers are connected to, they will be emitted from the swarm as `connection` events.
81
82`topic` must be a 32-byte Buffer
83`opts` can include:
84
85- `server`: Accept server connections for this topic by announcing yourself to the DHT. Defaults to `true`.
86- `client`: Actively search for and connect to discovered servers. Defaults to `true`.
87- `limit`: Set the max number of peers to connect to when joining the topic. Defaults to `Infinity`.
88
89Returns a [`PeerDiscovery`](#peerdiscovery-api) object.
90
91#### Clients and Servers
92
93In Hyperswarm, there are two ways for peers to join the swarm: client mode and server mode. If you've previously used Hyperswarm v2, these were called "lookup" and "announce", but we now think "client" and "server" are more descriptive.
94
95When you join a topic as a server, the swarm will start accepting incoming connections from clients (peers that have joined the same topic in client mode). Server mode will announce your keypair to the DHT, so that other peers can discover your server. When server connections are emitted, they are not associated with a specific topic -- the server only knows it received an incoming connection.
96
97When you join a topic as a client, the swarm will do a query to discover available servers, and will eagerly connect to them. As with server mode, these connections will be emitted as `connection` events, but in client mode they **will** be associated with the topic (`info.topics` will be set in the `connection` event).
98
99#### `await swarm.leave(topic)`
100
101Stop discovering peers for the given topic.
102
103`topic` must be a 32-byte Buffer
104
105If a topic was previously joined in server mode, `leave` will stop announcing the topic on the DHT. If a topic was previously joined in client mode, `leave` will stop searching for servers announcing the topic.
106
107`leave` will **not** close any existing connections.
108
109#### `swarm.joinPeer(noisePublicKey)`
110
111Establish a direct connection to a known peer.
112
113`noisePublicKey` must be a 32-byte Buffer
114
115As with the standard `join` method, `joinPeer` will ensure that peer connections are reestablished in the event of failures.
116
117#### `swarm.leavePeer(noisePublicKey)`
118
119Stop attempting direct connections to a known peer.
120
121`noisePublicKey` must be a 32-byte Buffer
122
123If a direct connection is already established, that connection will **not** be destroyed by `leavePeer`.
124
125#### `const discovery = swarm.status(topic)`
126
127Get the [`PeerDiscovery`](#peerdiscovery-api) object associated with the topic, if it exists.
128
129#### `await swarm.listen()`
130
131Explicitly start listening for incoming connections. This will be called internally after the first `join`, so it rarely needs to be called manually.
132
133#### `await swarm.flush()`
134
135Wait for any pending DHT announces, and for the swarm to connect to any pending peers (peers that have been discovered, but are still in the queue awaiting processing).
136
137Once a `flush()` has completed, the swarm will have connected to every peer it can discover from the current set of topics it's managing.
138
139`flush()` is not topic-specific, so it will wait for every pending DHT operation and connection to be processed -- it's quite heavyweight, so it could take a while. In most cases, it's not necessary, as connections are emitted by `swarm.on('connection')` immediately after they're opened.
140
141#### `await swarm.suspend({ log: () => {} })`
142
143Suspend the swarm disconnecting all peers, suspends server listening and stops discovery of new peers. Useful for suspending when the runtime suspends to pause networking.
144
145`log` is a logging function, which defaults to a noop function.
146
147#### `await swarm.resume({ log: () => {} })`
148
149Resume a suspended swarm refreshing discovery of new peers and servers. Useful for reannouncing to the DHT and reconnecting to peers when the runtime resumes.
150
151`log` is a logging function, which defaults to a noop function.
152
153#### `swarm.on('ban', peerInfo, err)`
154
155Emitted when a peer gets banned. `err` is an error object describing the reason for the ban (e.g. firewalled).
156
157## PeerDiscovery API
158
159`swarm.join` returns a `PeerDiscovery` instance which allows you to both control discovery behavior, and respond to lifecycle changes during discovery.
160
161#### `await discovery.flushed()`
162
163Wait until the topic has been fully announced to the DHT. This method is only relevant in server mode. When `flushed()` has completed, the server will be available to the network.
164
165#### `await discovery.refresh({ client, server })`
166
167Update the `PeerDiscovery` configuration, optionally toggling client and server modes. This will also trigger an immediate re-announce of the topic, when the `PeerDiscovery` is in server mode.
168
169#### `await discovery.destroy()`
170
171Stop discovering peers for the given topic.
172
173If a topic was previously joined in server mode, `leave` will stop announcing the topic on the DHT. If a topic was previously joined in client mode, `leave` will stop searching for servers announcing the topic.
174
175## PeerInfo API
176
177`swarm.on('connection', ...)` emits a `PeerInfo` instance whenever a new connection is established.
178
179There is a one-to-one relationship between connections and `PeerInfo` objects -- if a single peer announces multiple topics, those topics will be multiplexed over a single connection.
180
181#### `peerInfo.publicKey`
182
183The peer's Noise public key.
184
185#### `peerInfo.topics`
186
187An Array of topics that this Peer is associated with -- `topics` will only be updated when the Peer is in client mode.
188
189#### `peerInfo.prioritized`
190
191If true, the swarm will rapidly attempt to reconnect to this peer.
192
193#### `peerInfo.ban(banStatus = false)`
194
195Ban or unban the peer. Banning will prevent any future reconnection attempts, but it will **not** close any existing connections.
196
197## License
198
199MIT
test/node_modules/hyperswarm/index.js +688 -0
……@@ -0,0 +1,688 @@
1const { EventEmitter } = require('events')
2const { getStreamError } = require('streamx')
3const DHT = require('hyperdht')
4const spq = require('shuffled-priority-queue')
5const b4a = require('b4a')
6const unslab = require('unslab')
7
8const PeerInfo = require('./lib/peer-info')
9const RetryTimer = require('./lib/retry-timer')
10const ConnectionSet = require('./lib/connection-set')
11const PeerDiscovery = require('./lib/peer-discovery')
12
13const MAX_PEERS = 64
14const MAX_PARALLEL = 3
15const MAX_CLIENT_CONNECTIONS = Infinity // TODO: Change
16const MAX_SERVER_CONNECTIONS = Infinity
17
18const ERR_MISSING_TOPIC = 'Topic is required and must be a 32-byte buffer'
19const ERR_DESTROYED = 'Swarm has been destroyed'
20const ERR_DUPLICATE = 'Duplicate connection'
21const ERR_FIREWALL = 'Peer is firewalled'
22
23module.exports = class Hyperswarm extends EventEmitter {
24 constructor(opts = {}) {
25 super()
26 const {
27 seed,
28 relayThrough,
29 keyPair = DHT.keyPair(seed),
30 maxPeers = MAX_PEERS,
31 maxClientConnections = MAX_CLIENT_CONNECTIONS,
32 maxServerConnections = MAX_SERVER_CONNECTIONS,
33 maxParallel = MAX_PARALLEL,
34 firewall = allowAll
35 } = opts
36 this.keyPair = keyPair
37
38 this.dht =
39 opts.dht ||
40 new DHT({
41 bootstrap: opts.bootstrap,
42 nodes: opts.nodes,
43 port: opts.port,
44 deferRandomPunch: opts.deferRandomPunch,
45 randomPunchInterval: opts.randomPunchInterval
46 })
47 this.server = this.dht.createServer(
48 {
49 firewall: this._handleFirewall.bind(this),
50 relayThrough: this._maybeRelayConnection.bind(this),
51 handshakeClearWait: opts.handshakeClearWait
52 },
53 this._handleServerConnection.bind(this)
54 )
55
56 this.destroyed = false
57 this.suspended = false
58 this.maxPeers = maxPeers
59 this.maxClientConnections = maxClientConnections
60 this.maxServerConnections = maxServerConnections
61 this.maxParallel = maxParallel
62 this.relayThrough = relayThrough ? toRelayFunction(relayThrough) : null
63
64 this.connecting = 0
65 this.connections = new Set()
66 this.peers = new Map()
67 this.explicitPeers = new Set()
68 this.listening = null
69 this.stats = {
70 updates: 0,
71 connects: {
72 client: {
73 opened: 0,
74 closed: 0,
75 attempted: 0
76 },
77 server: {
78 // Note: there is no notion of 'attempts' for server connections
79 opened: 0,
80 closed: 0
81 }
82 },
83 bannedPeers: 0
84 }
85
86 this._discovery = new Map()
87 this._timer = new RetryTimer(this._requeue.bind(this), {
88 backoffs: opts.backoffs,
89 jitter: opts.jitter
90 })
91 this._queue = spq()
92
93 this._allConnections = new ConnectionSet()
94 this._pendingFlushes = []
95 this._flushTick = 0
96
97 this._drainingQueue = false
98 this._clientConnections = 0
99 this._serverConnections = 0
100 this._firewall = firewall
101
102 this.dht.on('network-change', this._handleNetworkChange.bind(this))
103 this.dht.on('network-update', this._handleNetworkUpdate.bind(this))
104 this.on('update', this._handleUpdate)
105 }
106
107 _maybeRelayConnection(force) {
108 if (!this.relayThrough) return null
109 return this.relayThrough(force, this)
110 }
111
112 _enqueue(peerInfo) {
113 if (peerInfo.queued) return
114 peerInfo.queued = true
115 peerInfo._flushTick = this._flushTick
116 this._queue.add(peerInfo)
117
118 this._attemptClientConnections()
119 }
120
121 _requeue(batch) {
122 if (this.suspended) return
123 for (const peerInfo of batch) {
124 peerInfo.waiting = false
125
126 if (
127 peerInfo._updatePriority() === false ||
128 this._allConnections.has(peerInfo.publicKey) ||
129 peerInfo.queued
130 ) {
131 continue
132 }
133 peerInfo.queued = true
134 peerInfo._flushTick = this._flushTick
135 this._queue.add(peerInfo)
136 }
137
138 this._attemptClientConnections()
139 }
140
141 _flushMaybe(peerInfo) {
142 for (let i = 0; i < this._pendingFlushes.length; i++) {
143 const flush = this._pendingFlushes[i]
144 if (peerInfo._flushTick > flush.tick) continue
145 if (--flush.missing > 0) continue
146 flush.onflush(true)
147 this._pendingFlushes.splice(i--, 1)
148 }
149 }
150
151 _withinMaxPeers() {
152 const e = this.explicitPeers.size
153 const factor = e === 0 ? 1 : e < 2 ? 2 : e < 4 ? 3 : 4
154 return factor * this._allConnections.size < this.maxPeers
155 }
156
157 _flushAllMaybe() {
158 if (
159 this.connecting > 0 ||
160 (this._withinMaxPeers() && this._clientConnections < this.maxClientConnections)
161 ) {
162 return false
163 }
164
165 while (this._pendingFlushes.length) {
166 const flush = this._pendingFlushes.pop()
167 flush.onflush(true)
168 }
169
170 return true
171 }
172
173 _shouldConnectExplicit() {
174 return !this.destroyed && !this.suspended && this.connecting < this.maxParallel
175 }
176
177 _shouldConnect() {
178 return (
179 !this.destroyed &&
180 !this.suspended &&
181 this.connecting < this.maxParallel &&
182 this._withinMaxPeers() &&
183 this._clientConnections < this.maxClientConnections
184 )
185 }
186
187 _shouldRequeue(peerInfo) {
188 if (this.suspended) return false
189 if (peerInfo.explicit) return true
190 for (const topic of peerInfo.topics) {
191 if (this._discovery.has(b4a.toString(topic, 'hex')) && !this.destroyed) {
192 return true
193 }
194 }
195 return false
196 }
197
198 _connect(peerInfo, queued) {
199 if (peerInfo.banned || this._allConnections.has(peerInfo.publicKey)) {
200 if (queued) this._flushMaybe(peerInfo)
201 return
202 }
203
204 // TODO: Support async firewalling at some point.
205 if (this._handleFirewall(peerInfo.publicKey, null)) {
206 if (queued) this._flushMaybe(peerInfo)
207 return
208 }
209
210 const relayThrough = this._maybeRelayConnection(peerInfo.forceRelaying)
211 const conn = this.dht.connect(peerInfo.publicKey, {
212 relayAddresses: peerInfo.relayAddresses,
213 keyPair: this.keyPair,
214 relayThrough
215 })
216 this._allConnections.add(conn)
217
218 this.stats.connects.client.attempted++
219
220 this.connecting++
221 this._clientConnections++
222 let opened = false
223
224 const onerror = (err) => {
225 if (this.relayThrough && shouldForceRelaying(err.code)) {
226 peerInfo.forceRelaying = true
227 // Reset the attempts in order to fast connect to relay
228 peerInfo.attempts = 0
229 }
230 }
231
232 // Removed once a connection is opened
233 conn.on('error', onerror)
234
235 conn.on('open', () => {
236 opened = true
237 this.stats.connects.client.opened++
238
239 this._connectDone()
240 this.connections.add(conn)
241 conn.removeListener('error', onerror)
242 peerInfo._connected()
243 peerInfo.client = true
244 this.emit('connection', conn, peerInfo)
245 if (queued) this._flushMaybe(peerInfo)
246
247 this.emit('update')
248 })
249 conn.on('close', () => {
250 if (!opened) this._connectDone()
251 this.stats.connects.client.closed++
252
253 const err = getStreamError(conn)
254 if (shouldBan(err)) {
255 this._banPeer(peerInfo, true, err)
256 }
257
258 this.connections.delete(conn)
259 this._allConnections.delete(conn)
260 this._clientConnections--
261 peerInfo._disconnected()
262
263 peerInfo.waiting = this._shouldRequeue(peerInfo) && this._timer.add(peerInfo)
264 this._maybeDeletePeer(peerInfo)
265
266 if (!opened && queued) this._flushMaybe(peerInfo)
267
268 this._attemptClientConnections()
269
270 this.emit('update')
271 })
272
273 this.emit('update')
274 }
275
276 _connectDone() {
277 this.connecting--
278
279 if (this.connecting < this.maxParallel) this._attemptClientConnections()
280 if (this.connecting === 0) this._flushAllMaybe()
281 }
282
283 // Called when the PeerQueue indicates a connection should be attempted.
284 _attemptClientConnections() {
285 // Guard against re-entries - unsure if it still needed but doesn't hurt
286 if (this._drainingQueue || this.suspended) return
287 this._drainingQueue = true
288
289 for (const peerInfo of this.explicitPeers) {
290 if (!this._shouldConnectExplicit()) break
291 if (
292 peerInfo.attempts >= 5 ||
293 Date.now() - peerInfo.disconnectedTime < peerInfo.attempts * 1000
294 ) {
295 continue
296 }
297 this._connect(peerInfo, false)
298 }
299
300 while (this._queue.length && this._shouldConnect()) {
301 const peerInfo = this._queue.shift()
302 peerInfo.queued = false
303 this._connect(peerInfo, true)
304 }
305 this._drainingQueue = false
306 if (this.connecting === 0) this._flushAllMaybe()
307 }
308
309 _handleFirewall(remotePublicKey, payload) {
310 if (b4a.equals(remotePublicKey, this.keyPair.publicKey)) return true
311
312 let peerInfo = this.peers.get(b4a.toString(remotePublicKey, 'hex'))
313 if (peerInfo && peerInfo.banned) return true
314
315 const firewalled = this._firewall(remotePublicKey, payload)
316 if (firewalled) {
317 if (!peerInfo) peerInfo = this._upsertPeer(remotePublicKey)
318 this._banPeer(peerInfo, true, new Error(ERR_FIREWALL))
319 }
320
321 return firewalled
322 }
323
324 _handleServerConnectionSwap(existing, conn) {
325 let closed = false
326
327 existing.on('close', () => {
328 if (closed) return
329
330 conn.removeListener('error', noop)
331 conn.removeListener('close', onclose)
332
333 this._handleServerConnection(conn)
334 })
335
336 conn.on('error', noop)
337 conn.on('close', onclose)
338
339 function onclose() {
340 closed = true
341 }
342 }
343
344 // Called when the DHT receives a new server connection.
345 _handleServerConnection(conn) {
346 if (this.destroyed || this.suspended) {
347 // TODO: Investigate why a final server connection can be received after close
348 conn.on('error', noop)
349 return conn.destroy(ERR_DESTROYED)
350 }
351
352 const existing = this._allConnections.get(conn.remotePublicKey)
353
354 if (existing) {
355 // If both connections are from the same peer,
356 // - pick the new one if the existing stream is already established (has sent and received bytes),
357 // because the other client must have lost that connection and be reconnecting
358 // - otherwise, pick the one thats expected to initiate in a tie break
359 const existingIsOutdated = existing.rawBytesRead > 0 && existing.rawBytesWritten > 0
360 const expectedInitiator = b4a.compare(conn.publicKey, conn.remotePublicKey) > 0
361 const keepNew = existingIsOutdated || expectedInitiator === conn.isInitiator
362
363 if (keepNew === false) {
364 existing.sendKeepAlive()
365 conn.on('error', noop)
366 conn.destroy(new Error(ERR_DUPLICATE))
367 return
368 }
369
370 existing.on('error', noop)
371 existing.destroy(new Error(ERR_DUPLICATE))
372 this._handleServerConnectionSwap(existing, conn)
373 return
374 }
375
376 // When reaching here, the connection will always be 'opened' next tick
377 this.stats.connects.server.opened++
378
379 const peerInfo = this._upsertPeer(conn.remotePublicKey, null)
380
381 this.connections.add(conn)
382 this._allConnections.add(conn)
383 this._serverConnections++
384
385 conn.on('close', () => {
386 const err = getStreamError(conn)
387 if (shouldBan(err)) {
388 this._banPeer(peerInfo, true, err)
389 }
390
391 this.connections.delete(conn)
392 this._allConnections.delete(conn)
393 this._serverConnections--
394 this.stats.connects.server.closed++
395
396 this._maybeDeletePeer(peerInfo)
397
398 this._attemptClientConnections()
399
400 this.emit('update')
401 })
402 peerInfo.client = false
403 this.emit('connection', conn, peerInfo)
404
405 this.emit('update')
406 }
407
408 _upsertPeer(publicKey, relayAddresses) {
409 if (b4a.equals(publicKey, this.keyPair.publicKey)) return null
410 const keyString = b4a.toString(publicKey, 'hex')
411 let peerInfo = this.peers.get(keyString)
412
413 if (peerInfo) {
414 peerInfo.relayAddresses = relayAddresses // new is always better
415 return peerInfo
416 }
417
418 peerInfo = new PeerInfo({
419 publicKey,
420 relayAddresses
421 })
422
423 this.peers.set(keyString, peerInfo)
424 return peerInfo
425 }
426
427 _handleUpdate() {
428 this.stats.updates++
429 }
430
431 _maybeDeletePeer(peerInfo) {
432 if (!peerInfo.shouldGC()) return
433
434 const hasActiveConn = this._allConnections.has(peerInfo.publicKey)
435 if (hasActiveConn) return
436
437 const keyString = b4a.toString(peerInfo.publicKey, 'hex')
438 this.peers.delete(keyString)
439 }
440
441 /*
442 * Called when a peer is actively discovered during a lookup.
443 *
444 * Three conditions:
445 * 1. Not a known peer -- insert into queue
446 * 2. A known peer with normal priority -- do nothing
447 * 3. A known peer with low priority -- bump priority, because it's been rediscovered
448 */
449 _handlePeer(peer, topic) {
450 const peerInfo = this._upsertPeer(peer.publicKey, peer.relayAddresses)
451 if (peerInfo) peerInfo._topic(topic)
452 if (!peerInfo || this._allConnections.has(peer.publicKey)) return
453 if (!peerInfo.prioritized || peerInfo.server) peerInfo._reset()
454 if (peerInfo._updatePriority()) {
455 this._enqueue(peerInfo)
456 }
457 }
458
459 async _handleNetworkUpdate() {
460 if (!this.dht.online) return
461 this._handleNetworkChange()
462 }
463
464 async _handleNetworkChange() {
465 if (this.suspended) return
466
467 // prioritize figuring out if existing connections are dead
468 for (const conn of this._allConnections) {
469 conn.sendKeepAlive()
470 }
471
472 const refreshes = []
473
474 for (const discovery of this._discovery.values()) {
475 refreshes.push(discovery.refresh())
476 }
477
478 await Promise.allSettled(refreshes)
479 }
480
481 _banPeer(peerInfo, banned, err) {
482 peerInfo.ban(banned)
483 this.stats.bannedPeers++
484 this.emit('ban', peerInfo, err)
485 }
486
487 status(key) {
488 return this._discovery.get(b4a.toString(key, 'hex')) || null
489 }
490
491 listen() {
492 if (!this.listening) {
493 if (this.destroyed) throw new Error('Swarm destroyed')
494 this.listening = this.server.listen(this.keyPair)
495 }
496 return this.listening
497 }
498
499 // Object that exposes a cancellation method (destroy)
500 // TODO: When you rejoin, it should reannounce + bump lookup priority
501 join(topic, opts = {}) {
502 if (this.destroyed) throw new Error('Swarm destroyed')
503 if (!topic) throw new Error(ERR_MISSING_TOPIC)
504 topic = unslab(topic)
505
506 const topicString = b4a.toString(topic, 'hex')
507
508 let discovery = this._discovery.get(topicString)
509
510 if (discovery && !discovery.destroyed) {
511 return discovery.session(opts)
512 }
513
514 discovery = new PeerDiscovery(this, topic, {
515 limit: opts.limit,
516 wait: discovery ? discovery.destroy() : null,
517 suspended: this.suspended,
518 onpeer: (peer) => this._handlePeer(peer, topic)
519 })
520 this._discovery.set(topicString, discovery)
521 return discovery.session(opts)
522 }
523
524 // Returns a promise
525 async leave(topic) {
526 if (!topic) throw new Error(ERR_MISSING_TOPIC)
527 const topicString = b4a.toString(topic, 'hex')
528 if (!this._discovery.has(topicString)) return Promise.resolve()
529
530 const discovery = this._discovery.get(topicString)
531
532 try {
533 await discovery.destroy()
534 } catch {
535 // ignore, prop network
536 }
537
538 if (this._discovery.get(topicString) === discovery) {
539 this._discovery.delete(topicString)
540 }
541 }
542
543 joinPeer(publicKey) {
544 const peerInfo = this._upsertPeer(publicKey, null)
545 if (!peerInfo) return
546 if (!this.explicitPeers.has(peerInfo)) {
547 peerInfo.explicit = true
548 this.explicitPeers.add(peerInfo)
549 }
550 if (this._allConnections.has(publicKey)) return
551 if (peerInfo._updatePriority()) {
552 this._enqueue(peerInfo)
553 }
554 }
555
556 leavePeer(publicKey) {
557 const keyString = b4a.toString(publicKey, 'hex')
558 if (!this.peers.has(keyString)) return
559
560 const peerInfo = this.peers.get(keyString)
561 peerInfo.explicit = false
562 this.explicitPeers.delete(peerInfo)
563 this._maybeDeletePeer(peerInfo)
564 }
565
566 // Returns a promise
567 async flush() {
568 const allFlushed = [...this._discovery.values()].map((v) => v.flushed())
569 await Promise.all(allFlushed)
570 if (this._flushAllMaybe()) return true
571 const pendingSize = this._allConnections.size - this.connections.size
572 if (!this._queue.length && !pendingSize) return true
573 return new Promise((resolve) => {
574 this._pendingFlushes.push({
575 onflush: resolve,
576 missing: this._queue.length + pendingSize,
577 tick: this._flushTick++
578 })
579 })
580 }
581
582 async clear() {
583 const cleared = Promise.allSettled([...this._discovery.values()].map((d) => d.destroy()))
584 this._discovery.clear()
585 return cleared
586 }
587
588 async destroy({ force } = {}) {
589 if (this.destroyed && !force) return
590 this.destroyed = true
591
592 this._timer.destroy()
593
594 if (!force) await this.clear()
595
596 await this.server.close()
597
598 while (this._pendingFlushes.length) {
599 const flush = this._pendingFlushes.pop()
600 flush.onflush(false)
601 }
602
603 await this.dht.destroy({ force })
604 }
605
606 async suspend({ log = noop } = {}) {
607 if (this.suspended) return
608
609 const promises = []
610
611 promises.push(this.server.suspend({ log }))
612
613 for (const discovery of this._discovery.values()) {
614 promises.push(discovery.suspend({ log }))
615 }
616
617 const pending = []
618 for (const connection of this._allConnections) {
619 connection.destroy()
620 pending.push(new Promise((resolve) => connection.on('close', resolve)))
621 }
622
623 this.suspended = true
624
625 log('Suspending server and discovery... (' + promises.length + ')')
626 await Promise.allSettled(promises)
627 log('Done, suspending the dht...')
628 await this.dht.suspend({ log })
629 log('Done, swarm fully suspended')
630
631 await Promise.all(pending)
632
633 // reset queue
634 this._timer.destroy()
635 this._timer = new RetryTimer(this._requeue.bind(this), {
636 backoffs: this._timer.backoffs,
637 jitter: this._timer.jitter
638 })
639 this._queue = spq()
640 }
641
642 async resume({ log = noop } = {}) {
643 if (!this.suspended) return
644
645 log('Resuming the dht')
646 await this.dht.resume()
647 log('Done, resuming the server')
648 await this.server.resume()
649 log('Done, all discovery')
650
651 for (const discovery of this._discovery.values()) {
652 discovery.resume()
653 }
654
655 this.suspended = false
656 this._attemptClientConnections()
657 }
658
659 topics() {
660 return this._discovery.values()
661 }
662}
663
664function noop() {}
665
666function allowAll() {
667 return false
668}
669
670function shouldForceRelaying(code) {
671 return (
672 code === 'HOLEPUNCH_ABORTED' ||
673 code === 'HOLEPUNCH_DOUBLE_RANDOMIZED_NATS' ||
674 code === 'CANNOT_HOLEPUNCH' ||
675 code === 'REMOTE_NOT_HOLEPUNCHABLE'
676 )
677}
678
679function shouldBan() {
680 // return !!err && err.name === 'HypercoreError' && err.code === 'INVALID_OPERATION'
681 return false
682}
683
684function toRelayFunction(relayThrough) {
685 return typeof relayThrough === 'function'
686 ? relayThrough
687 : (force, swarm) => (force || swarm.dht.randomized ? relayThrough : null)
688}
test/node_modules/hyperswarm/lib/bulk-timer.js +33 -0
……@@ -0,0 +1,33 @@
1module.exports = class BulkTimer {
2 constructor(time, fn) {
3 this._time = time
4 this._fn = fn
5 this._interval = null
6 this._next = []
7 this._pending = []
8 this._destroyed = false
9 }
10
11 destroy() {
12 if (this._destroyed) return
13 this._destroyed = true
14 clearInterval(this._interval)
15 this._interval = null
16 }
17
18 _ontick() {
19 if (!this._next.length && !this._pending.length) return
20 if (this._next.length) this._fn(this._next)
21 this._next = this._pending
22 this._pending = []
23 }
24
25 add(info) {
26 if (this._destroyed) return
27 if (!this._interval) {
28 this._interval = setInterval(this._ontick.bind(this), Math.floor(this._time * 0.66))
29 }
30
31 this._pending.push(info)
32 }
33}
test/node_modules/hyperswarm/lib/connection-set.js +38 -0
……@@ -0,0 +1,38 @@
1const b4a = require('b4a')
2
3module.exports = class ConnectionSet {
4 constructor() {
5 this._byPublicKey = new Map()
6 }
7
8 [Symbol.iterator]() {
9 return this._byPublicKey.values()
10 }
11
12 get size() {
13 return this._byPublicKey.size
14 }
15
16 has(publicKey) {
17 return this._byPublicKey.has(toHex(publicKey))
18 }
19
20 get(publicKey) {
21 return this._byPublicKey.get(toHex(publicKey))
22 }
23
24 add(connection) {
25 this._byPublicKey.set(b4a.toString(connection.remotePublicKey, 'hex'), connection)
26 }
27
28 delete(connection) {
29 const keyString = b4a.toString(connection.remotePublicKey, 'hex')
30 const existing = this._byPublicKey.get(keyString)
31 if (existing !== connection) return
32 this._byPublicKey.delete(keyString)
33 }
34}
35
36function toHex(b) {
37 return typeof b === 'string' ? b : b4a.toString(b, 'hex')
38}
test/node_modules/hyperswarm/lib/peer-discovery.js +358 -0
……@@ -0,0 +1,358 @@
1const safetyCatch = require('safety-catch')
2const b4a = require('b4a')
3
4const REFRESH_INTERVAL = 1000 * 60 * 10 // 10 min
5const RANDOM_JITTER = 1000 * 60 * 2 // 2 min
6const DELAY_GRACE_PERIOD = 1000 * 30 // 30s
7
8const MAX_DISCOVERY_CACHE = 64
9
10module.exports = class PeerDiscovery {
11 constructor(
12 swarm,
13 topic,
14 { limit = Infinity, wait = null, suspended = false, onpeer = noop, onerror = safetyCatch }
15 ) {
16 this.limit = limit
17 this.swarm = swarm
18 this.topic = topic
19 this.isClient = false
20 this.isServer = false
21 this.destroyed = false
22 this.destroying = null
23 this.suspended = suspended
24
25 this._sessions = []
26 this._clientSessions = 0
27 this._serverSessions = 0
28
29 this._onpeer = onpeer
30 this._onerror = onerror
31
32 this._discovered = new Set()
33 this._activeQuery = null
34 this._timer = null
35 this._currentRefresh = null
36 this._closestNodes = null
37 this._firstAnnounce = true
38 this._needsUnannounce = false
39 this._refreshes = 0
40 this._wait = wait
41 }
42
43 session({ server = true, client = true, limit = Infinity, onerror = safetyCatch }) {
44 if (this.destroyed) throw new Error('PeerDiscovery is destroyed')
45 const session = new PeerDiscoverySession(this)
46 session.refresh({ server, client, limit }).catch(onerror)
47 this._sessions.push(session)
48 return session
49 }
50
51 _refreshLater(eager) {
52 const jitter = Math.round(Math.random() * RANDOM_JITTER)
53 const delay = !eager ? REFRESH_INTERVAL + jitter : jitter
54
55 if (this._timer) clearTimeout(this._timer)
56
57 const startTime = Date.now()
58 this._timer = setTimeout(() => {
59 // If your laptop went to sleep, and is coming back online...
60 const overdue = Date.now() - startTime > delay + DELAY_GRACE_PERIOD
61 if (overdue) this._refreshLater(true)
62 else this.refresh().catch(this._onerror)
63 }, delay)
64 }
65
66 _isActive() {
67 return !this.destroyed && !this.suspended
68 }
69
70 // TODO: Allow announce to be an argument to this
71 // TODO: Maybe announce should be a setter?
72 async _refresh() {
73 if (this.suspended) return
74 const clock = ++this._refreshes
75
76 if (this._wait) {
77 await this._wait
78 this._wait = null
79 if (clock !== this._refreshes || !this._isActive()) return
80 }
81
82 const clear = this.isServer && this._firstAnnounce
83 if (clear) this._firstAnnounce = false
84
85 const opts = {
86 clear,
87 closestNodes: this._closestNodes
88 }
89
90 if (this.isServer) {
91 await this.swarm.listen()
92 // if a parallel refresh is happening, yield to the new one
93 if (clock !== this._refreshes || !this._isActive()) return
94 this._needsUnannounce = true
95 }
96
97 let limit = this.limit
98
99 if (limit < Infinity && limit > 0) {
100 for (const id of this._discovered) {
101 if (!this.swarm.connections.has(id)) continue
102 if (--limit === 0) break
103 }
104 }
105
106 this._discovered.clear()
107
108 const announcing = this.isServer
109 const query = (this._activeQuery = announcing
110 ? this.swarm.dht.announce(
111 this.topic,
112 this.swarm.keyPair,
113 this.swarm.server.relayAddresses,
114 opts
115 )
116 : this._needsUnannounce
117 ? this.swarm.dht.lookupAndUnannounce(this.topic, this.swarm.keyPair, opts)
118 : this.swarm.dht.lookup(this.topic, opts))
119
120 try {
121 for await (const data of this._activeQuery) {
122 if (!this.isClient || !this._isActive()) continue
123 for (const peer of data.peers) {
124 if (limit < Infinity) {
125 const id = b4a.toString(peer.publicKey, 'hex')
126
127 if (this._discovered.size < MAX_DISCOVERY_CACHE) {
128 this._discovered.add(id)
129 }
130
131 if (limit === 0) continue
132
133 // there is a chance it has updated a connection during discovery and we go over
134 // the limit - thats acceptable to avoid a complexity spiral here.
135 if (!this.swarm.connections.has(id)) limit--
136 }
137
138 this._onpeer(peer, data)
139 }
140 }
141 } catch (err) {
142 if (this._isActive()) throw err
143 } finally {
144 if (this._activeQuery === query) {
145 this._activeQuery = null
146 if (!this.destroyed && !this.suspended) this._refreshLater(false)
147 }
148 }
149
150 // This is set at the very end, when the query completes successfully.
151 this._closestNodes = query.closestNodes
152
153 if (clock !== this._refreshes) return
154
155 // In this is the latest query, unannounce has been fulfilled as well
156 if (!announcing) this._needsUnannounce = false
157 }
158
159 async refresh() {
160 if (this.destroyed) throw new Error('PeerDiscovery is destroyed')
161
162 const server = this._serverSessions > 0
163 const client = this._clientSessions > 0
164
165 if (this.suspended) return
166
167 if (server === this.isServer && client === this.isClient) {
168 if (this._currentRefresh) return this._currentRefresh
169 this._currentRefresh = this._refresh()
170 } else {
171 if (this._activeQuery) this._activeQuery.destroy()
172 this.isServer = server
173 this.isClient = client
174 this._currentRefresh = this._refresh()
175 }
176
177 const refresh = this._currentRefresh
178 try {
179 await refresh
180 } catch {
181 return false
182 } finally {
183 if (refresh === this._currentRefresh) {
184 this._currentRefresh = null
185 }
186 }
187
188 return true
189 }
190
191 async flushed() {
192 if (this.swarm.listening) await this.swarm.listening
193
194 try {
195 await this._currentRefresh
196 return true
197 } catch {
198 return false
199 }
200 }
201
202 async _destroyMaybe() {
203 if (this.destroyed) return
204
205 try {
206 if (this._sessions.length === 0) await this.swarm.leave(this.topic)
207 else if (this._serverSessions === 0 && this._needsUnannounce) await this.refresh()
208 } catch (err) {
209 // ignore network failures here, as we are tearing down
210 safetyCatch(err)
211 }
212 }
213
214 destroy() {
215 if (this.destroying) return this.destroying
216 this.destroying = this._destroy()
217 return this.destroying
218 }
219
220 async _abort(log) {
221 const id = log === noop ? '' : b4a.toString(this.topic, 'hex')
222
223 log('Aborting discovery', id)
224 if (this._wait) await this._wait
225 log('Aborting discovery (post wait)', id)
226
227 if (this._activeQuery) {
228 this._activeQuery.destroy()
229 this._activeQuery = null
230 }
231 if (this._timer) {
232 clearTimeout(this._timer)
233 this._timer = null
234 }
235
236 let nodes = this._closestNodes
237
238 if (this._currentRefresh) {
239 try {
240 await this._currentRefresh
241 } catch {
242 // If the destroy causes the refresh to fail, suppress it.
243 }
244 }
245
246 log('Aborting discovery (post refresh)', id)
247 if (this._isActive()) return
248
249 if (!nodes) nodes = this._closestNodes
250 else if (this._closestNodes !== nodes) {
251 const len = nodes.length
252 for (const newer of this._closestNodes) {
253 if (newer.id && !hasNode(nodes, len, newer)) nodes.push(newer)
254 }
255 }
256
257 if (this._needsUnannounce) {
258 log('Unannouncing discovery', id)
259 if (nodes && nodes.length) {
260 await this.swarm.dht.unannounce(this.topic, this.swarm.keyPair, {
261 closestNodes: nodes,
262 onlyClosestNodes: true,
263 force: true
264 })
265 }
266 this._needsUnannounce = false
267 log('Unannouncing discovery (done)', id)
268 }
269 }
270
271 _destroy() {
272 if (this.destroyed) return
273 this.destroyed = true
274 return this._abort(noop)
275 }
276
277 async suspend({ log = noop } = {}) {
278 if (this.suspended) return
279 this.suspended = true
280 try {
281 await this._abort(log)
282 } catch {
283 // ignore
284 }
285 }
286
287 resume() {
288 if (!this.suspended) return
289 this.suspended = false
290 this.refresh().catch(noop)
291 }
292}
293
294class PeerDiscoverySession {
295 constructor(discovery) {
296 this.discovery = discovery
297 this.isClient = false
298 this.isServer = false
299 this.destroyed = false
300 }
301
302 get swarm() {
303 return this.discovery.swarm
304 }
305
306 get topic() {
307 return this.discovery.topic
308 }
309
310 async refresh({ client = this.isClient, server = this.isServer, limit = Infinity } = {}) {
311 if (this.destroyed) throw new Error('PeerDiscovery is destroyed')
312 if (!client && !server) throw new Error('Cannot refresh with neither client nor server option')
313
314 if (client !== this.isClient) {
315 this.isClient = client
316 this.discovery._clientSessions += client ? 1 : -1
317 }
318
319 if (server !== this.isServer) {
320 this.isServer = server
321 this.discovery._serverSessions += server ? 1 : -1
322 }
323
324 this.discovery.limit = limit
325
326 return this.discovery.refresh()
327 }
328
329 async flushed() {
330 return this.discovery.flushed()
331 }
332
333 async destroy() {
334 if (this.destroyed) return
335 this.destroyed = true
336
337 if (this.isClient) this.discovery._clientSessions--
338 if (this.isServer) this.discovery._serverSessions--
339
340 const index = this.discovery._sessions.indexOf(this)
341 const head = this.discovery._sessions.pop()
342
343 if (head !== this) this.discovery._sessions[index] = head
344
345 return this.discovery._destroyMaybe()
346 }
347}
348
349function hasNode(nodes, len, node) {
350 for (let i = 0; i < len; i++) {
351 const existing = nodes[i]
352 if (existing.id && b4a.equals(existing.id, node.id)) return true
353 }
354
355 return false
356}
357
358function noop() {}
test/node_modules/hyperswarm/lib/peer-info.js +116 -0
……@@ -0,0 +1,116 @@
1const { EventEmitter } = require('events')
2const b4a = require('b4a')
3const unslab = require('unslab')
4
5const MIN_CONNECTION_TIME = 15000
6
7const VERY_LOW_PRIORITY = 0
8const LOW_PRIORITY = 1
9const NORMAL_PRIORITY = 2
10const HIGH_PRIORITY = 3
11const VERY_HIGH_PRIORITY = 4
12
13module.exports = class PeerInfo extends EventEmitter {
14 constructor({ publicKey, relayAddresses }) {
15 super()
16
17 this.publicKey = unslab(publicKey)
18 this.relayAddresses = relayAddresses
19
20 this.reconnecting = true
21 this.proven = false
22 this.connectedTime = -1
23 this.disconnectedTime = 0
24 this.banned = false
25 this.tried = false
26 this.explicit = false
27 this.waiting = false
28 this.forceRelaying = false
29
30 // Set by the Swarm
31 this.queued = false
32 this.client = false
33 this.topics = [] // TODO: remove on next major (check with mafintosh for context)
34
35 this.attempts = 0
36 this.priority = NORMAL_PRIORITY
37
38 // Used by shuffled-priority-queue
39 this._index = 0
40
41 // Used for flush management
42 this._flushTick = 0
43
44 // Used for topic multiplexing
45 this._seenTopics = new Set()
46 }
47
48 get server() {
49 return !this.client
50 }
51
52 get prioritized() {
53 return this.priority >= NORMAL_PRIORITY
54 }
55
56 _getPriority() {
57 const peerIsStale = this.tried && !this.proven
58 if (peerIsStale || this.attempts > 3) return VERY_LOW_PRIORITY
59 if (this.attempts === 3) return LOW_PRIORITY
60 if (this.attempts === 2) return HIGH_PRIORITY
61 if (this.attempts === 1) return VERY_HIGH_PRIORITY
62 return NORMAL_PRIORITY
63 }
64
65 _connected() {
66 this.proven = true
67 this.connectedTime = Date.now()
68 }
69
70 _disconnected() {
71 this.disconnectedTime = Date.now()
72 if (this.connectedTime > -1) {
73 if (this.disconnectedTime - this.connectedTime >= MIN_CONNECTION_TIME) this.attempts = 0 // fast retry
74 this.connectedTime = -1
75 }
76 this.attempts++
77 }
78
79 _deprioritize() {
80 this.attempts = 3
81 }
82
83 _reset() {
84 this.client = false
85 this.proven = false
86 this.tried = false
87 this.attempts = 0
88 }
89
90 _updatePriority() {
91 if (this.explicit && this.attempts > 3) this._deprioritize()
92 if (this.banned || this.queued || this.attempts > 3) return false
93 this.priority = this._getPriority()
94 return true
95 }
96
97 _topic(topic) {
98 const topicString = b4a.toString(topic, 'hex')
99 if (this._seenTopics.has(topicString)) return
100 this._seenTopics.add(topicString)
101 this.topics.push(topic)
102 this.emit('topic', topic)
103 }
104
105 reconnect(val) {
106 this.reconnecting = !!val
107 }
108
109 ban(val) {
110 this.banned = !!val
111 }
112
113 shouldGC() {
114 return !(this.banned || this.queued || this.explicit || this.waiting)
115 }
116}
test/node_modules/hyperswarm/lib/retry-timer.js +68 -0
……@@ -0,0 +1,68 @@
1const BulkTimer = require('./bulk-timer')
2
3const BACKOFF_JITTER = 500
4const BACKOFF_S = 1000 + Math.round(BACKOFF_JITTER * Math.random())
5const BACKOFF_M = 5000 + Math.round(2 * BACKOFF_JITTER * Math.random())
6const BACKOFF_L = 15000 + Math.round(4 * BACKOFF_JITTER * Math.random())
7const BACKOFF_X = 1000 * 60 * 10 + Math.round(240 * BACKOFF_JITTER * Math.random())
8
9module.exports = class RetryTimer {
10 constructor(
11 push,
12 { backoffs = [BACKOFF_S, BACKOFF_M, BACKOFF_L, BACKOFF_X], jitter = BACKOFF_JITTER } = {}
13 ) {
14 this.jitter = jitter
15 this.backoffs = backoffs
16
17 this._sTimer = new BulkTimer(backoffs[0] + Math.round(jitter * Math.random()), push)
18 this._mTimer = new BulkTimer(backoffs[1] + Math.round(jitter * Math.random()), push)
19 this._lTimer = new BulkTimer(backoffs[2] + Math.round(jitter * Math.random()), push)
20 this._xTimer = new BulkTimer(backoffs[3] + Math.round(jitter * Math.random()), push)
21 }
22
23 _selectRetryTimer(peerInfo) {
24 if (peerInfo.banned || !peerInfo.reconnecting) return null
25
26 if (peerInfo.attempts > 3) {
27 return peerInfo.explicit ? this._xTimer : null
28 }
29
30 if (peerInfo.attempts === 0) return this._sTimer
31 if (peerInfo.proven) {
32 switch (peerInfo.attempts) {
33 case 1:
34 return this._sTimer
35 case 2:
36 return this._mTimer
37 case 3:
38 return this._lTimer
39 }
40 } else {
41 switch (peerInfo.attempts) {
42 case 1:
43 return this._mTimer
44 case 2:
45 return this._lTimer
46 case 3:
47 return this._lTimer
48 }
49 }
50
51 return null
52 }
53
54 add(peerInfo) {
55 const timer = this._selectRetryTimer(peerInfo)
56 if (!timer) return false
57
58 timer.add(peerInfo)
59 return true
60 }
61
62 destroy() {
63 this._sTimer.destroy()
64 this._mTimer.destroy()
65 this._lTimer.destroy()
66 this._xTimer.destroy()
67 }
68}
test/node_modules/hyperswarm/package.json +59 -0
……@@ -0,0 +1,59 @@
1{
2 "name": "hyperswarm",
3 "version": "4.17.1",
4 "description": "A distributed networking stack for connecting peers",
5 "files": [
6 "index.js",
7 "lib/**.js"
8 ],
9 "imports": {
10 "events": {
11 "bare": "bare-events",
12 "default": "events"
13 },
14 "process": {
15 "bare": "bare-process",
16 "default": "process"
17 }
18 },
19 "dependencies": {
20 "b4a": "^1.3.1",
21 "bare-events": "^2.2.0",
22 "hyperdht": "^6.21.0",
23 "safety-catch": "^1.0.2",
24 "shuffled-priority-queue": "^2.1.0",
25 "streamx": "^2.22.1",
26 "unslab": "^1.3.0"
27 },
28 "devDependencies": {
29 "bare-process": "^4.2.2",
30 "brittle": "^3.0.2",
31 "hypercore-crypto": "^3.4.0",
32 "lunte": "^1.3.0",
33 "prettier": "^3.6.2",
34 "prettier-config-holepunch": "^2.0.0",
35 "which-runtime": "^1.3.2"
36 },
37 "scripts": {
38 "format": "prettier --write .",
39 "lint": "prettier --check . && lunte",
40 "test": "npm run test:node && npm run test:bare",
41 "test:generate": "brittle -r test/all.js test/*.js",
42 "test:node": "brittle-node --coverage test/*.js",
43 "test:bare": "bare test/all.js"
44 },
45 "repository": {
46 "type": "git",
47 "url": "https://github.com/holepunchto/hyperswarm.git"
48 },
49 "author": "Mathias Buus (@mafintosh)",
50 "contributors": [
51 "David Mark Clements (@davidmarkclem)",
52 "Andrew Osheroff (@andrewosh)"
53 ],
54 "license": "MIT",
55 "bugs": {
56 "url": "https://github.com/holepunchto/hyperswarm/issues"
57 },
58 "homepage": "https://github.com/holepunchto/hyperswarm"
59}
test/node_modules/shuffled-priority-queue/.travis.yml +6 -0
……@@ -0,0 +1,6 @@
1language: node_js
2node_js:
3 - '6'
4 - '8'
5 - '10'
6
test/node_modules/shuffled-priority-queue/LICENSE +21 -0
……@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) 2017 Mathias Buus
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE SOFTWARE.
test/node_modules/shuffled-priority-queue/README.md +116 -0
……@@ -0,0 +1,116 @@
1# shuffled-priority-queue
2
3A priority queue that shuffles elements with the same priority.
4
5```
6npm install shuffled-priority-queue
7```
8
9[![Build Status](https://travis-ci.org/mafintosh/shuffled-priority-queue.svg?branch=master)](https://travis-ci.org/mafintosh/shuffled-priority-queue)
10
11## Usage
12
13``` js
14const spq = require('shuffled-priority-queue')
15const queue = spq()
16
17queue.add({
18 priority: 0,
19 value: 'hello'
20})
21
22queue.add({
23 priority: 0,
24 value: 'world'
25})
26
27queue.add({
28 priority: 1,
29 value: 'welt'
30})
31
32queue.add({
33 priority: 2,
34 value: 'verden'
35})
36
37console.log(queue.shift()) // returns {value: 'verden'}
38console.log(queue.shift()) // returns {value: 'welt'}
39console.log(queue.shift()) // returns {value: 'hello'} or {value: 'world'}
40console.log(queue.shift()) // returns {value: 'hello'} or {value: 'world'}
41console.log(queue.shift()) // returns null (empty queue)
42```
43
44## API
45
46#### `const queue = spq()`
47
48Create a new queue.
49
50#### `value = queue.add(value)`
51
52Add a new value to the queue. The value is returned for convenience
53If you set `value.priority` to a number, it'll be added to the queue at that priority.
54
55#### `queue.remove(value)`
56
57Remove a value from the queue.
58
59#### `bool = queue.has(value)`
60
61Check if a value is in the queue.
62
63#### `value = queue.shift()`
64
65Shift the next value off the queue.
66
67The value returned will have the highest priority off the queue.
68If multiple values have the same priority a random one is returned.
69
70#### `value = queue.head()`
71
72Same as `shift()` but does not mutate the queue.
73
74#### `value = queue.pop()`
75
76Same as `shift()` but returns a value with the lowest priority.
77
78#### `value = queue.tail()`
79
80Same as `pop()` but does not mutate the queue.
81
82#### `queue.length`
83
84Property containing how many items are in the queue
85
86#### `for (const value of queue)`
87
88Iterate the queue from highest priority to lowest using the `for of` syntax
89
90#### `value = queue.next([prevValue])`
91
92Iterate the queue from highest priority to lowest.
93
94``` js
95let prevValue = null
96
97while (prevValue = queue.next(prevValue)) {
98 console.log('value:', prevValue)
99}
100```
101
102#### `value = queue.prev([prevValue])`
103
104Iterate the queue from lowest priority to highest.
105
106``` js
107let prevValue = null
108
109while (prevValue = queue.prev(prevValue)) {
110 console.log('value:', prevValue)
111}
112```
113
114## License
115
116MIT
test/node_modules/shuffled-priority-queue/example.js +28 -0
……@@ -0,0 +1,28 @@
1var spq = require('./')
2var queue = spq()
3
4queue.add({
5 priority: 0,
6 value: 'hello'
7})
8
9queue.add({
10 priority: 0,
11 value: 'world'
12})
13
14queue.add({
15 priority: 1,
16 value: 'welt'
17})
18
19queue.add({
20 priority: 2,
21 value: 'verden'
22})
23
24console.log(queue.pop()) // returns {value: 'verden'}
25console.log(queue.pop()) // returns {value: 'welt'}
26console.log(queue.pop()) // returns {value: 'hello'} or {value: 'world'}
27console.log(queue.pop()) // returns {value: 'hello'} or {value: 'world'}
28console.log(queue.pop()) // returns null (empty queue)
test/node_modules/shuffled-priority-queue/index.js +134 -0
……@@ -0,0 +1,134 @@
1const set = require('unordered-set')
2
3module.exports = opts => new ShuffledPriorityQueue(opts)
4
5class ShuffledPriorityQueue {
6 constructor (opts) {
7 this.priorities = []
8 this.equals = (opts && opts.equals) || null
9 }
10
11 get length () {
12 return this.priorities.reduce(add, 0)
13 }
14
15 [Symbol.iterator] () {
16 return new Iterator(this)
17 }
18
19 head () {
20 for (let i = this.priorities.length - 1; i >= 0; i--) {
21 const q = this.priorities[i]
22 if (q.length) return shuffle(q, 0)
23 }
24 return null
25 }
26
27 tail () {
28 for (let i = 0; i < this.priorities.length; i++) {
29 const q = this.priorities[i]
30 if (q.length) return shuffle(q, 0)
31 }
32 return null
33 }
34
35 prev (prev) {
36 if (!prev) return this.tail()
37 return next(this.priorities, prev, 1)
38 }
39
40 next (prev) {
41 if (!prev) return this.head()
42 return next(this.priorities, prev, -1)
43 }
44
45 shift () {
46 return this.remove(this.head())
47 }
48
49 pop () {
50 return this.remove(this.tail())
51 }
52
53 add (val) {
54 const prio = val.priority || 0
55 while (prio >= this.priorities.length) this.priorities.push([])
56 set.add(this.priorities[prio], val)
57 return val
58 }
59
60 remove (val) {
61 if (!val) return null
62
63 if (val._index === undefined) {
64 val = this.find(val)
65 if (!val) return null
66 }
67
68 return set.remove(this.priorities[val.priority || 0], val)
69 }
70
71 has (val) {
72 if (val._index === undefined) return this.find(val)
73 const priority = val.priority || 0
74 if (priority >= this.priorities.length) return false
75 return set.has(this.priorities[priority], val)
76 }
77
78 find (val) {
79 if (val._index !== undefined) return val
80
81 const prio = val.priority || 0
82 const qs = this.priorities
83 if (prio >= qs.length) return null
84
85 const q = qs[prio]
86
87 for (let i = 0; i < q.length; i++) {
88 if (this.equals(q[i], val)) return q[i]
89 }
90
91 return null
92 }
93}
94
95class Iterator {
96 constructor (queue) {
97 this.prev = null
98 this.queue = queue
99 }
100
101 next () {
102 const next = this.queue.next(this.prev)
103 this.prev = next
104 return { done: !next, value: next }
105 }
106}
107
108function shuffle (q, i) {
109 const ran = i + Math.floor(Math.random() * (q.length - i))
110 set.swap(q, q[ran], q[i])
111 return q[i]
112}
113
114function next (queues, prev, inc) {
115 let i = prev.priority || 0
116 let j = (prev._index || 0) + 1
117
118 while (true) {
119 if (i < 0 || i >= queues.length) return null
120 const q = queues[i]
121
122 if (j >= q.length) {
123 i += inc
124 j = 0
125 continue
126 }
127
128 return shuffle(q, j)
129 }
130}
131
132function add (len, b) {
133 return len + b.length
134}
test/node_modules/shuffled-priority-queue/package.json +26 -0
……@@ -0,0 +1,26 @@
1{
2 "name": "shuffled-priority-queue",
3 "version": "2.1.0",
4 "description": "A priority queue that shuffles elements with the same priority.",
5 "main": "index.js",
6 "scripts": {
7 "test": "standard && tape test.js"
8 },
9 "dependencies": {
10 "unordered-set": "^2.0.1"
11 },
12 "devDependencies": {
13 "standard": "^12.0.1",
14 "tape": "^4.9.1"
15 },
16 "repository": {
17 "type": "git",
18 "url": "https://github.com/mafintosh/shuffled-priority-queue.git"
19 },
20 "author": "Mathias Buus (@mafintosh)",
21 "license": "MIT",
22 "bugs": {
23 "url": "https://github.com/mafintosh/shuffled-priority-queue/issues"
24 },
25 "homepage": "https://github.com/mafintosh/shuffled-priority-queue"
26}
test/node_modules/shuffled-priority-queue/test.js +164 -0
……@@ -0,0 +1,164 @@
1const spq = require('./')
2const tape = require('tape')
3
4tape('different prios', function (t) {
5 const queue = spq()
6
7 const a = queue.add({
8 hello: 'world',
9 priority: 0
10 })
11
12 const b = queue.add({
13 hej: 'verden',
14 priority: 1
15 })
16
17 t.ok(queue.has(b))
18 t.same(queue.length, 2)
19 t.same(queue.shift(), b)
20 t.ok(!queue.has(b))
21 t.same(queue.length, 1)
22 t.same(queue.shift(), a)
23 t.same(queue.length, 0)
24 t.same(queue.shift(), null)
25 t.end()
26})
27
28tape('same prios', function (t) {
29 const queue = spq()
30
31 const a = queue.add({
32 hello: 'world',
33 priority: 0
34 })
35
36 const b = queue.add({
37 hello: 'verden',
38 priority: 0
39 })
40
41 const c = queue.add({
42 hej: 'verden',
43 priority: 1
44 })
45
46 t.same(queue.shift(), c)
47
48 let head = queue.shift()
49 t.ok(head === a || head === b)
50
51 head = queue.shift()
52 t.ok(head === a || head === b)
53
54 t.same(queue.shift(), null)
55 t.end()
56})
57
58tape('next', function (t) {
59 const queue = spq()
60
61 const a = queue.add({
62 hello: 'world',
63 priority: 0
64 })
65
66 const b = queue.add({
67 hello: 'verden',
68 priority: 0
69 })
70
71 const c = queue.add({
72 hej: 'verden',
73 priority: 1
74 })
75
76 t.same(queue.next(), c)
77
78 let value = queue.next(c)
79 t.ok(value === a || value === b)
80
81 const old = value
82 value = queue.next(value)
83 t.ok(old !== value)
84 t.ok(value === a || value === b)
85
86 t.same(queue.next(value), null)
87 t.end()
88})
89
90tape('prev', function (t) {
91 const queue = spq()
92
93 const a = queue.add({
94 hello: 'world',
95 priority: 0
96 })
97
98 const b = queue.add({
99 hello: 'verden',
100 priority: 0
101 })
102
103 const c = queue.add({
104 hej: 'verden',
105 priority: 1
106 })
107
108 let tail = queue.prev()
109 t.ok(tail === a || tail === b)
110
111 const old = tail
112 tail = queue.prev(tail)
113 t.ok(old !== tail)
114 t.ok(tail === a || tail === b)
115
116 tail = queue.prev(tail)
117 t.same(tail, c)
118
119 t.same(queue.prev(tail), null)
120 t.end()
121})
122
123tape('equals', function (t) {
124 const queue = spq({
125 equals: function (a, b) {
126 return a.hello === b.hello
127 }
128 })
129
130 queue.add({
131 hello: 'world'
132 })
133
134 t.same(queue.head().hello, 'world')
135
136 queue.remove({
137 hello: 'world'
138 })
139
140 t.same(queue.head(), null)
141 t.end()
142})
143
144tape('iterator', function (t) {
145 t.plan(5)
146
147 const queue = spq()
148 const seen = {}
149
150 queue.add({ priority: 0, hi: 'a' })
151 queue.add({ priority: 0, hi: 'b' })
152 queue.add({ priority: 1, hi: 'c' })
153 queue.add({ priority: 2, hi: 'd' })
154
155 let prev = 3
156
157 for (const value of queue) {
158 t.ok(prev >= value.priority)
159 prev = value.priority
160 seen[value.hi] = true
161 }
162
163 t.same(seen, { a: true, b: true, c: true, d: true })
164})
test/node_modules/unordered-set/.travis.yml +9 -0
……@@ -0,0 +1,9 @@
1sudo: false
2
3language: node_js
4
5node_js:
6 - "5"
7 - "4"
8 - "0.12"
9 - "0.10"
test/node_modules/unordered-set/LICENSE +21 -0
……@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) 2016 Mathias Buus
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE SOFTWARE.
test/node_modules/unordered-set/README.md +53 -0
……@@ -0,0 +1,53 @@
1# unordered-set
2
3A couple of functions that make it easy to maintain an unordered set as an array in an efficient way
4
5```
6npm install unordered-set
7```
8
9[![build status](https://travis-ci.org/mafintosh/unordered-set.svg?branch=master)](https://travis-ci.org/mafintosh/unordered-set)
10
11## Usage
12
13``` js
14var set = require('unordered-set')
15var list = []
16
17var a = {hello: 'world'}
18var b = {hej: 'verden'}
19
20set.add(list, a)
21set.add(list, b)
22
23console.log(list) // prints a and b
24
25set.remove(list, a)
26
27console.log(list) // prints b
28```
29
30Items are removed and added using the same technique as in [unordered-array-remove](https://github.com/mafintosh/unordered-array-remove)
31making the removal/additions run in O(1).
32
33## API
34
35#### `set.add(list, item)`
36
37Add an item. Notes that this sets the property `._index` to a number. If you control the items you insert and this is performance critical it might be benefitial to set `item._index = 0` in the item constructor as v8 tends to like that.
38
39#### `set.remove(list, item)`
40
41Remove an item from the set. Might change the order of the list as well.
42
43#### `bool = set.has(list, item)`
44
45Returns `true` if the item is in the list and `false` otherwise
46
47#### `set.swap(list, a, b)`
48
49Swap the positions of two elements in the set
50
51## License
52
53MIT
test/node_modules/unordered-set/index.js +36 -0
……@@ -0,0 +1,36 @@
1exports.add = add
2exports.has = has
3exports.remove = remove
4exports.swap = swap
5
6function add (list, item) {
7 if (has(list, item)) return item
8 item._index = list.length
9 list.push(item)
10 return item
11}
12
13function has (list, item) {
14 return item._index < list.length && list[item._index] === item
15}
16
17function remove (list, item) {
18 if (!has(list, item)) return null
19
20 var last = list.pop()
21 if (last !== item) {
22 list[item._index] = last
23 last._index = item._index
24 }
25
26 return item
27}
28
29function swap (list, a, b) {
30 if (!has(list, a) || !has(list, b)) return
31 var tmp = a._index
32 a._index = b._index
33 list[a._index] = a
34 b._index = tmp
35 list[b._index] = b
36}
test/node_modules/unordered-set/package.json +24 -0
……@@ -0,0 +1,24 @@
1{
2 "name": "unordered-set",
3 "version": "2.0.1",
4 "description": "A couple of functions that make it easy to maintain an unordered set as an array in an efficient way",
5 "main": "index.js",
6 "dependencies": {},
7 "devDependencies": {
8 "standard": "^6.0.4",
9 "tape": "^4.4.0"
10 },
11 "scripts": {
12 "test": "standard && tape test.js"
13 },
14 "repository": {
15 "type": "git",
16 "url": "https://github.com/mafintosh/unordered-set.git"
17 },
18 "author": "Mathias Buus (@mafintosh)",
19 "license": "MIT",
20 "bugs": {
21 "url": "https://github.com/mafintosh/unordered-set/issues"
22 },
23 "homepage": "https://github.com/mafintosh/unordered-set"
24}
test/node_modules/unordered-set/test.js +126 -0
……@@ -0,0 +1,126 @@
1var tape = require('tape')
2var set = require('./')
3
4tape('add', function (t) {
5 var list = []
6 var a = {hello: 'world'}
7 var b = {hello: 'verden'}
8
9 set.add(list, a)
10 set.add(list, b)
11
12 t.same(list, [a, b])
13 t.end()
14})
15
16tape('add twice', function (t) {
17 var list = []
18 var a = {hello: 'world'}
19 var b = {hello: 'verden'}
20
21 set.add(list, a)
22 set.add(list, b)
23 set.add(list, b)
24
25 t.same(list, [a, b])
26 t.end()
27})
28
29tape('remove', function (t) {
30 var list = []
31 var a = {hello: 'world'}
32 var b = {hello: 'verden'}
33
34 set.add(list, a)
35 set.add(list, b)
36 set.remove(list, a)
37
38 t.same(list, [b])
39 t.end()
40})
41
42tape('remove twice', function (t) {
43 var list = []
44 var a = {hello: 'world'}
45 var b = {hello: 'verden'}
46
47 set.add(list, a)
48 set.add(list, b)
49 set.remove(list, a)
50 set.remove(list, a)
51
52 t.same(list, [b])
53 t.end()
54})
55
56tape('remove all', function (t) {
57 var list = []
58 var a = {hello: 'world'}
59 var b = {hello: 'verden'}
60
61 set.add(list, a)
62 set.add(list, b)
63 set.remove(list, a)
64 set.remove(list, b)
65
66 t.same(list, [])
67 t.end()
68})
69
70tape('re-add', function (t) {
71 var list = []
72 var a = {hello: 'world'}
73 var b = {hello: 'verden'}
74
75 set.add(list, a)
76 set.add(list, b)
77 set.remove(list, a)
78 set.remove(list, b)
79 set.add(list, b)
80 set.add(list, a)
81
82 t.same(list, [b, a])
83 t.end()
84})
85
86tape('has', function (t) {
87 var list = []
88 var a = {hello: 'world'}
89 var b = {hello: 'verden'}
90
91 set.add(list, a)
92 set.add(list, b)
93 set.remove(list, a)
94
95 t.ok(!set.has(list, a))
96 t.ok(set.has(list, b))
97 t.end()
98})
99
100tape('swap', function (t) {
101 var list = []
102 var a = {hello: 'world'}
103 var b = {hello: 'verden'}
104
105 set.add(list, a)
106 set.add(list, b)
107
108 set.swap(list, a, b)
109 t.same(list, [b, a])
110
111 set.swap(list, a, b)
112 t.same(list, [a, b])
113
114 t.end()
115})
116
117tape('swap same', function (t) {
118 var list = []
119 var a = {hello: 'world'}
120
121 set.add(list, a)
122
123 set.swap(list, a, a)
124 t.same(list, [a])
125 t.end()
126})
test/package-lock.json +31 -0
……@@ -13,6 +13,7 @@
1313 "compact-encoding": "^2.19.0",
1414 "hypercore-crypto": "^3.6.1",
1515 "hyperdht": "^6.34.0",
16 "hyperswarm": "^4.17.1",
1617 "sodium-universal": "^5.0.1"
1718 }
1819 },
……@@ -389,6 +390,21 @@
389390 "b4a": "^1.3.0"
390391 }
391392 },
393 "node_modules/hyperswarm": {
394 "version": "4.17.1",
395 "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.1.tgz",
396 "integrity": "sha512-bFy89nulBSY1qQZMZMGWb6wwBHftthONZr5IpX4o7hcSmMQI5IHyfA7Id3KU03SDflWfPvEm1YJy0AL5BFFblA==",
397 "license": "MIT",
398 "dependencies": {
399 "b4a": "^1.3.1",
400 "bare-events": "^2.2.0",
401 "hyperdht": "^6.21.0",
402 "safety-catch": "^1.0.2",
403 "shuffled-priority-queue": "^2.1.0",
404 "streamx": "^2.22.1",
405 "unslab": "^1.3.0"
406 }
407 },
392408 "node_modules/is-property": {
393409 "version": "1.0.2",
394410 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
……@@ -484,6 +500,15 @@
484500 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
485501 "license": "MIT"
486502 },
503 "node_modules/shuffled-priority-queue": {
504 "version": "2.1.0",
505 "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
506 "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
507 "license": "MIT",
508 "dependencies": {
509 "unordered-set": "^2.0.1"
510 }
511 },
487512 "node_modules/signal-promise": {
488513 "version": "1.0.3",
489514 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
……@@ -586,6 +611,12 @@
586611 "bare": ">=1.17.4"
587612 }
588613 },
614 "node_modules/unordered-set": {
615 "version": "2.0.1",
616 "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
617 "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
618 "license": "MIT"
619 },
589620 "node_modules/unslab": {
590621 "version": "1.3.0",
591622 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
test/package.json +1 -0
……@@ -15,6 +15,7 @@
1515 "compact-encoding": "^2.19.0",
1616 "hypercore-crypto": "^3.6.1",
1717 "hyperdht": "^6.34.0",
18 "hyperswarm": "^4.17.1",
1819 "sodium-universal": "^5.0.1"
1920 }
2021}