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 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
- | Peer hints (preferred peers, priorities) | planned | 003 | - |
27
- | Admission decisions | planned | 003 | - |
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
- /** How many missing groups to consider at once. */
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
- * Order candidate groups by how few peers hold them, keeping file order as the tie-break so
15
- * a transfer still roughly streams.
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 rarestFirst (missing, holders, { limit = SCAN_LIMIT } = {}) {
27
- const candidates = missing.length > limit ? missing.slice(0, limit) : missing.slice()
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
- return candidates
30
- .map((index) => ({ index, holders: holders(index) }))
31
- .sort((a, b) => a.holders - b.holders || a.index - b.index)
32
- .map((candidate) => candidate.index)
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 { rarestFirst } from './select.js'
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._bitfields.set(peer.id, b4a.from(bitfield ?? []))
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._bitfields.delete(peer.id)
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 wanted = rarestFirst(this._destination.missing(), (index) => this._holders(index))
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
- /** How many connected peers hold this group. Drives rarest-first selection. */
823
- _holders (index) {
824
- let n = 0
825
- for (const peerId of this._sources) {
826
- if (this._peerHas(peerId, index)) n++
827
- }
828
- return n
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) bao.clearBit(bitfield, index)
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.setBit(bitfield, index)
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._bitfields.delete(peer.id)
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.2.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": {