swarmwire-lib 0.2.0 → 0.3.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/docs/contract.md +10 -2
- package/lib/select.js +44 -18
- package/lib/swarmwire.js +38 -16
- package/package.json +2 -1
package/docs/contract.md
CHANGED
|
@@ -23,8 +23,9 @@ code (`serveCollection`, `downloadCollection`). See `AGENTS.md` for the invarian
|
|
|
23
23
|
| Peer identity: the remote public key of the connection | exists | - | `test/swarm/transfer.test.js` |
|
|
24
24
|
| Peer activity events, peer count, seeder and leecher counts | exists | - | `test/swarm/peers.test.js` |
|
|
25
25
|
| Folder file-list reader (look-only, in memory), and downloading one listed file | exists | - | `test/swarm/list.test.js` |
|
|
26
|
-
|
|
|
27
|
-
|
|
|
26
|
+
| Download order spread by random choice (no two newcomers ask for the same thing first) | exists | - | `test/swarm/spread.test.js` |
|
|
27
|
+
| Peer hints (preferred peers, priorities) | planned | 004 | - |
|
|
28
|
+
| Admission decisions | planned | 004 | - |
|
|
28
29
|
|
|
29
30
|
## 3. Rules the application can rely on
|
|
30
31
|
|
|
@@ -73,6 +74,13 @@ The application can decide, per peer and per content, whether content is served.
|
|
|
73
74
|
**Direct connections only**: a snapshot that may lag, not a count of the swarm. Global counts and
|
|
74
75
|
chunk rarity across the network are a tracker's job, not swarmwire's.
|
|
75
76
|
|
|
77
|
+
### 3.3a Download order
|
|
78
|
+
|
|
79
|
+
Groups are fetched rarest first, equal rarity broken at random, starting from a random place in
|
|
80
|
+
the file. **A download does not proceed in file order**: a file is complete when every group is
|
|
81
|
+
held, and nothing is promised about reading it while it downloads. How the choice was made, and
|
|
82
|
+
what was measured, is in `docs/scaling.md` in the repository (not part of the package).
|
|
83
|
+
|
|
76
84
|
### 3.4 Identity
|
|
77
85
|
|
|
78
86
|
swarmwire knows the remote public key of each connection and nothing else about a peer. Tying a
|
package/lib/select.js
CHANGED
|
@@ -5,29 +5,55 @@
|
|
|
5
5
|
* The reason is not speed on this download but survival of the swarm — a group held by one
|
|
6
6
|
* peer disappears when that peer leaves, and a swarm where every copy of some group lives on
|
|
7
7
|
* one machine is one disconnection away from nobody being able to finish.
|
|
8
|
+
*
|
|
9
|
+
* Two things keep a crowd from asking for the same thing. Equally rare groups are ordered at
|
|
10
|
+
* random rather than by position, and the scan for candidates starts at a random group rather
|
|
11
|
+
* than the start of the file — otherwise a thousand fresh downloaders of a large file would
|
|
12
|
+
* all draw from its first few hundred groups.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
import { bao } from './blake3.js'
|
|
16
|
+
|
|
17
|
+
/** How many candidate groups to weigh at once. */
|
|
11
18
|
export const SCAN_LIMIT = 256
|
|
12
19
|
|
|
20
|
+
/** How many source peers hold each group. Kept up to date by the caller, read in O(1). */
|
|
21
|
+
export class Availability {
|
|
22
|
+
constructor (groupCount) {
|
|
23
|
+
this.counts = new Uint16Array(groupCount)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
at (index) { return this.counts[index] }
|
|
27
|
+
set (index) { this.counts[index]++ }
|
|
28
|
+
unset (index) { if (this.counts[index] > 0) this.counts[index]-- }
|
|
29
|
+
|
|
30
|
+
add (bitfield) { this._each(bitfield, (i) => this.set(i)) }
|
|
31
|
+
remove (bitfield) { this._each(bitfield, (i) => this.unset(i)) }
|
|
32
|
+
|
|
33
|
+
_each (bitfield, fn) {
|
|
34
|
+
const n = Math.min(this.counts.length, bitfield.length * 8)
|
|
35
|
+
for (let i = 0; i < n; i++) if (bao.getBit(bitfield, i)) fn(i)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
/**
|
|
14
|
-
*
|
|
15
|
-
* a
|
|
16
|
-
*
|
|
17
|
-
* Only the first `limit` missing groups are considered: on a large file, sorting every
|
|
18
|
-
* outstanding group on every request would cost more than it saves, and rarity among the next
|
|
19
|
-
* few hundred is what actually decides the next request.
|
|
20
|
-
*
|
|
21
|
-
* @param {number[]} missing - group indices still wanted, in ascending order
|
|
22
|
-
* @param {(index: number) => number} holders - how many peers hold this group
|
|
23
|
-
* @param {{limit?: number}} [opts]
|
|
24
|
-
* @returns {number[]}
|
|
40
|
+
* The groups worth asking this peer for: wanted, offered, and not already being fetched.
|
|
41
|
+
* Scanning starts at `from` and wraps, so the window is a random stretch of the file.
|
|
25
42
|
*/
|
|
26
|
-
export function
|
|
27
|
-
const
|
|
43
|
+
export function candidates ({ count, has, peerHas, claimed, from = 0, limit = SCAN_LIMIT }) {
|
|
44
|
+
const out = []
|
|
45
|
+
|
|
46
|
+
for (let k = 0; k < count && out.length < limit; k++) {
|
|
47
|
+
const i = (from + k) % count
|
|
48
|
+
if (!has(i) && peerHas(i) && !claimed(i)) out.push(i)
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
28
52
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.map((
|
|
53
|
+
/** Fewest holders first; groups equally rare come in the order `random` gives them. */
|
|
54
|
+
export function rank (pool, availability, random) {
|
|
55
|
+
return pool
|
|
56
|
+
.map((index) => ({ index, rarity: availability.at(index), tie: random() }))
|
|
57
|
+
.sort((a, b) => a.rarity - b.rarity || a.tie - b.tie)
|
|
58
|
+
.map((entry) => entry.index)
|
|
33
59
|
}
|
package/lib/swarmwire.js
CHANGED
|
@@ -20,7 +20,7 @@ import { Destination } from './destination.js'
|
|
|
20
20
|
import { discoveryTopic, formatLink, parseLink } from './address.js'
|
|
21
21
|
import { DEFAULT_GROUP_LOG, groupCount } from './tree.js'
|
|
22
22
|
import { openChannel, VERSION } from './wire.js'
|
|
23
|
-
import {
|
|
23
|
+
import { Availability, candidates, rank } from './select.js'
|
|
24
24
|
import { Holdings } from './holdings.js'
|
|
25
25
|
import { Listing } from './listing.js'
|
|
26
26
|
import { systemTimers } from './timers.js'
|
|
@@ -79,6 +79,7 @@ export class Swarmwire extends EventEmitter {
|
|
|
79
79
|
this.timers = timers
|
|
80
80
|
this.requestTimeout = requestTimeout
|
|
81
81
|
this.window = window
|
|
82
|
+
this.random = Math.random // decides between equally rare groups; tests replace it with a seeded one
|
|
82
83
|
this.serving = new Map() // root → ContentSource
|
|
83
84
|
this.downloads = new Map() // root → Download
|
|
84
85
|
this.peers = new Map() // peer id → Peer
|
|
@@ -553,6 +554,7 @@ export class Download extends EventEmitter {
|
|
|
553
554
|
|
|
554
555
|
this._destination = null
|
|
555
556
|
this._bitfields = new Map() // peer id -> what that peer says it holds
|
|
557
|
+
this._availability = null // how many of those hold each group; built once the size is known
|
|
556
558
|
this._sources = new Set() // peer ids worth asking right now
|
|
557
559
|
this._liars = new Set() // peer ids that sent something that did not verify: never again
|
|
558
560
|
this._asked = new Set() // peer ids already told what we want
|
|
@@ -675,6 +677,8 @@ export class Download extends EventEmitter {
|
|
|
675
677
|
if (this.stopped) return destination.close()
|
|
676
678
|
|
|
677
679
|
this._destination = destination
|
|
680
|
+
this._availability = new Availability(destination.groupCount)
|
|
681
|
+
for (const bitfield of this._bitfields.values()) this._availability.add(bitfield)
|
|
678
682
|
if (this._destination.complete) return this._finish()
|
|
679
683
|
|
|
680
684
|
for (const peer of this.node.peers.values()) this._askPeer(peer)
|
|
@@ -693,7 +697,7 @@ export class Download extends EventEmitter {
|
|
|
693
697
|
|
|
694
698
|
if (this._liars.has(peer.id)) return // nothing it says is worth storing
|
|
695
699
|
|
|
696
|
-
this.
|
|
700
|
+
this._setBitfield(peer.id, b4a.from(bitfield ?? []))
|
|
697
701
|
this._sources.add(peer.id)
|
|
698
702
|
|
|
699
703
|
if (this.size === null) {
|
|
@@ -737,7 +741,7 @@ export class Download extends EventEmitter {
|
|
|
737
741
|
this.emit('rejected', { index, from: peer.id })
|
|
738
742
|
this._liars.add(peer.id) // a peer that sends rubbish is never asked again
|
|
739
743
|
this._sources.delete(peer.id)
|
|
740
|
-
this.
|
|
744
|
+
this._dropBitfield(peer.id)
|
|
741
745
|
}
|
|
742
746
|
|
|
743
747
|
if (this.complete) return this._finish()
|
|
@@ -770,12 +774,20 @@ export class Download extends EventEmitter {
|
|
|
770
774
|
|
|
771
775
|
const requests = this._requests(peer)
|
|
772
776
|
const window = this._windowFor(peer)
|
|
773
|
-
const
|
|
777
|
+
const destination = this._destination
|
|
778
|
+
const random = this.node.random
|
|
779
|
+
|
|
780
|
+
const pool = candidates({
|
|
781
|
+
count: destination.groupCount,
|
|
782
|
+
has: (index) => destination.has(index),
|
|
783
|
+
peerHas: (index) => this._peerHas(peer.id, index),
|
|
784
|
+
claimed: (index) => this._claimed(index),
|
|
785
|
+
from: Math.floor(random() * destination.groupCount)
|
|
786
|
+
})
|
|
787
|
+
const wanted = rank(pool, this._availability, random)
|
|
774
788
|
|
|
775
789
|
for (const index of wanted) {
|
|
776
790
|
if (requests.size >= window) break
|
|
777
|
-
if (this._claimed(index)) continue
|
|
778
|
-
if (!this._peerHas(peer.id, index)) continue // it has not got that one yet
|
|
779
791
|
|
|
780
792
|
this._claim(peer, index)
|
|
781
793
|
peer.send.request({ root: b4a.from(this.root, 'hex'), index })
|
|
@@ -819,13 +831,17 @@ export class Download extends EventEmitter {
|
|
|
819
831
|
this._windows.set(peer.id, next)
|
|
820
832
|
}
|
|
821
833
|
|
|
822
|
-
/**
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
834
|
+
/** Remember what a peer holds, keeping the per-group counts in step. */
|
|
835
|
+
_setBitfield (peerId, bitfield) {
|
|
836
|
+
this._dropBitfield(peerId)
|
|
837
|
+
this._bitfields.set(peerId, bitfield)
|
|
838
|
+
this._availability?.add(bitfield)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
_dropBitfield (peerId) {
|
|
842
|
+
const old = this._bitfields.get(peerId)
|
|
843
|
+
if (old) this._availability?.remove(old)
|
|
844
|
+
this._bitfields.delete(peerId)
|
|
829
845
|
}
|
|
830
846
|
|
|
831
847
|
/** Does this peer hold that group? An empty bitfield means we were told nothing useful. */
|
|
@@ -848,7 +864,10 @@ export class Download extends EventEmitter {
|
|
|
848
864
|
this._narrow(peer)
|
|
849
865
|
|
|
850
866
|
const bitfield = this._bitfields.get(peer.id)
|
|
851
|
-
if (bitfield && index < bitfield.length * 8
|
|
867
|
+
if (bitfield && index < bitfield.length * 8 && bao.getBit(bitfield, index)) {
|
|
868
|
+
bao.clearBit(bitfield, index)
|
|
869
|
+
this._availability?.unset(index)
|
|
870
|
+
}
|
|
852
871
|
|
|
853
872
|
this._pumpAll()
|
|
854
873
|
}
|
|
@@ -858,7 +877,10 @@ export class Download extends EventEmitter {
|
|
|
858
877
|
const bitfield = this._bitfields.get(peer.id)
|
|
859
878
|
if (!bitfield || index >= bitfield.length * 8) return
|
|
860
879
|
|
|
861
|
-
bao.
|
|
880
|
+
if (!bao.getBit(bitfield, index)) {
|
|
881
|
+
bao.setBit(bitfield, index)
|
|
882
|
+
this._availability?.set(index)
|
|
883
|
+
}
|
|
862
884
|
this._pump(peer)
|
|
863
885
|
}
|
|
864
886
|
|
|
@@ -906,7 +928,7 @@ export class Download extends EventEmitter {
|
|
|
906
928
|
|
|
907
929
|
_peerGone (peer) {
|
|
908
930
|
this._sources.delete(peer.id)
|
|
909
|
-
this.
|
|
931
|
+
this._dropBitfield(peer.id)
|
|
910
932
|
this._asked.delete(peer.id)
|
|
911
933
|
this._releaseAll(peer.id) // whatever it owed us is up for grabs again
|
|
912
934
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmwire-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Content-addressed, verified file transfer between filesystems over Hyperswarm. No copies, no cores, no trackers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"test:bare:net": "bare node_modules/brittle/bin/bare.js test/net/*.test.js",
|
|
68
68
|
"bench": "node bench/swarm.js",
|
|
69
69
|
"bench:net": "node bench/throughput.js",
|
|
70
|
+
"bench:sim": "node bench/swarm-sim.js",
|
|
70
71
|
"check:pack": "node scripts/check-pack.js"
|
|
71
72
|
},
|
|
72
73
|
"dependencies": {
|