swarmwire-lib 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,8 +55,9 @@ The command-line tool built on this library is moving to its own package,
55
55
 
56
56
  > Status: **pre-alpha.** Transfers work end to end, over a real swarm, with resume,
57
57
  > verification, request timeouts, partial seeding, rarest-first selection and an adaptive
58
- > request window. Behaviour with very many peers is not measured yet. See
59
- > [docs/roadmap.md](docs/roadmap.md) for what is built and what is next.
58
+ > request window. Behaviour with very many peers is only modelled, not measured. It is enough
59
+ > for a command-line tool; it is not yet the content-addressing layer SwarmFS needs (admission,
60
+ > peer hints, no public discovery). [docs/roadmap.md](docs/roadmap.md) says where it stands.
60
61
 
61
62
  ## What it is
62
63
 
@@ -110,7 +111,9 @@ Anything that needs a name, a directory or a user belongs upstairs. See
110
111
  |------------------------------------|-----------------------------------------------------------------------------------------------|
111
112
  | [docs/format.md](docs/format.md) | Addressing, verification, links, topic derivation — the parts that must never change casually |
112
113
  | [docs/contract.md](docs/contract.md) | What applications can rely on, supply and must not expect |
113
- | [docs/roadmap.md](docs/roadmap.md) | Vision and milestones |
114
+ | [docs/roadmap.md](docs/roadmap.md) | Vision, milestones, and where things stand |
115
+ | [docs/scaling.md](docs/scaling.md) | Piece selection in large swarms, simulated |
116
+ | [docs/research/p2p-landscape.md](docs/research/p2p-landscape.md) | What other P2P systems do, and what is worth taking |
114
117
  | [AGENTS.md](AGENTS.md) | How to work in this repo |
115
118
 
116
119
  ## License
package/lib/fs.js CHANGED
@@ -8,12 +8,25 @@
8
8
  import fs from 'fs'
9
9
  import b4a from 'b4a'
10
10
 
11
+ /**
12
+ * `fd:N` names an already-open descriptor instead of a path (Android hands out descriptors
13
+ * for picked files, with no path). Read-only, files only; the caller keeps it open.
14
+ * shortcut: sync fstat/read on the fd, upgrade if the blocking reads ever show up in profiles.
15
+ */
16
+ const fdOf = (path) => (/^fd:\d+$/.test(path) ? Number(path.slice(3)) : null)
17
+
11
18
  /**
12
19
  * @param {string} path
13
20
  * @returns {Promise<{size: number, mtimeMs: number, directory: boolean}|null>} null if it
14
21
  * does not exist
15
22
  */
16
23
  export async function stat (path) {
24
+ const fd = fdOf(path)
25
+ if (fd !== null) {
26
+ const st = fs.fstatSync(fd)
27
+ return { size: st.size, mtimeMs: Math.floor(st.mtimeMs), directory: false }
28
+ }
29
+
17
30
  try {
18
31
  const st = await fs.promises.stat(path)
19
32
  return { size: st.size, mtimeMs: Math.floor(st.mtimeMs), directory: st.isDirectory() }
@@ -25,6 +38,19 @@ export async function stat (path) {
25
38
 
26
39
  /** Open a file for reading byte ranges out of it, in place. */
27
40
  export async function openRead (path) {
41
+ const fd = fdOf(path)
42
+ if (fd !== null) {
43
+ return {
44
+ async read (offset, length) {
45
+ const buffer = b4a.alloc(length)
46
+ const bytesRead = length === 0 ? 0 : fs.readSync(fd, buffer, 0, length, offset)
47
+ return bytesRead === length ? buffer : buffer.subarray(0, bytesRead)
48
+ },
49
+
50
+ close () {} // not ours to close
51
+ }
52
+ }
53
+
28
54
  const handle = await fs.promises.open(path, 'r')
29
55
 
30
56
  return {
package/lib/swarmwire.js CHANGED
@@ -18,7 +18,7 @@ import { ContentSource, MemorySource } from './source.js'
18
18
  import { encodeManifest, decodeManifest, totalSize } from './collection.js'
19
19
  import { Destination } from './destination.js'
20
20
  import { discoveryTopic, formatLink, parseLink } from './address.js'
21
- import { DEFAULT_GROUP_LOG, groupCount } from './tree.js'
21
+ import { DEFAULT_GROUP_LOG, groupCount, groupLength } from './tree.js'
22
22
  import { openChannel, VERSION } from './wire.js'
23
23
  import { Availability, candidates, rank } from './select.js'
24
24
  import { Holdings } from './holdings.js'
@@ -222,10 +222,13 @@ export class Swarmwire extends EventEmitter {
222
222
  *
223
223
  * @param {string|object} link
224
224
  * @param {string} dir
225
- * @param {{onfile?: Function}} [opts]
225
+ * `onfile({ path, size })` runs as each file completes; `onprogress({ bytes, total })` as bytes of
226
+ * the whole folder arrive, first with `bytes: 0` once the list is known.
227
+ *
228
+ * @param {{onfile?: Function, onprogress?: Function}} [opts]
226
229
  * @returns {Promise<{files: number, bytes: number}>}
227
230
  */
228
- async downloadCollection (link, dir, { onfile = null } = {}) {
231
+ async downloadCollection (link, dir, { onfile = null, onprogress = null } = {}) {
229
232
  const parsed = typeof link === 'string' ? parseLink(link) : link
230
233
 
231
234
  await fsx.mkdir(dir)
@@ -238,14 +241,21 @@ export class Swarmwire extends EventEmitter {
238
241
  if (!list) throw new Error('the folder list read was stopped')
239
242
 
240
243
  let bytes = 0
244
+ const total = list.size
245
+ onprogress?.({ bytes, total })
241
246
 
242
247
  for (const file of list.files) {
243
248
  const target = join(dir, file.path)
244
249
  await fsx.mkdir(parentOf(target))
245
250
 
246
- await this.downloadFile(parsed, file, target)
251
+ const download = this.downloadFile(parsed, file, target)
252
+ download.on('progress', ({ have }) => {
253
+ onprogress?.({ bytes: bytes + Math.min(have * groupLength(this.groupLog), file.size), total })
254
+ })
255
+ await download
247
256
 
248
257
  bytes += file.size
258
+ onprogress?.({ bytes, total })
249
259
  if (onfile) onfile({ path: file.path, size: file.size })
250
260
  }
251
261
 
package/lib/tree.js CHANGED
@@ -129,6 +129,7 @@ export class GroupTree {
129
129
  this.groupHashes = groupHashes
130
130
  this.size = size
131
131
  this.groupLog = groupLog
132
+ this._known = new Map() // 'start:end' → hash of a larger subtree; a hash once known never changes
132
133
  }
133
134
 
134
135
  /** Hash content held in memory. For files, stream it through GroupTreeBuilder instead. */
@@ -162,7 +163,7 @@ export class GroupTree {
162
163
  if (!this.complete) return null
163
164
  if (this.groupCount === 1) return b4a.toString(this.groupHashes[0], 'hex')
164
165
 
165
- return b4a.toString(subtree(this.groupHashes, 0, this.groupCount, true), 'hex')
166
+ return b4a.toString(subtree(this.groupHashes, 0, this.groupCount, true, this._known), 'hex')
166
167
  }
167
168
 
168
169
  /**
@@ -186,8 +187,8 @@ export class GroupTree {
186
187
  while (end - start > 1) {
187
188
  const mid = start + splitPoint(end - start)
188
189
  const sibling = index < mid
189
- ? subtree(this.groupHashes, mid, end, false)
190
- : subtree(this.groupHashes, start, mid, false)
190
+ ? subtree(this.groupHashes, mid, end, false, this._known)
191
+ : subtree(this.groupHashes, start, mid, false, this._known)
191
192
 
192
193
  if (sibling === null) return null
193
194
  siblings.push(sibling)
@@ -259,18 +260,28 @@ export function splitPoint (count) {
259
260
  return p
260
261
  }
261
262
 
262
- /** Null when any leaf under this subtree is unknown. */
263
- function subtree (hashes, start, end, isRoot) {
263
+ // shortcut: only subtrees of at least this many groups are remembered, so the memory is about n/16
264
+ // hashes and a proof recomputes at most ~15 per level; lower it if proofs ever show up in a profile.
265
+ const REMEMBER = 16
266
+
267
+ /** Null when any leaf under this subtree is unknown. `known` remembers the bigger ones. */
268
+ function subtree (hashes, start, end, isRoot, known) {
264
269
  if (end - start === 1) return hashes[start] ?? null
265
270
 
271
+ const key = !isRoot && end - start >= REMEMBER ? start + ':' + end : null
272
+ const remembered = key && known.get(key)
273
+ if (remembered) return remembered
274
+
266
275
  const mid = start + splitPoint(end - start)
267
- const left = subtree(hashes, start, mid, false)
276
+ const left = subtree(hashes, start, mid, false, known)
268
277
  if (left === null) return null
269
278
 
270
- const right = subtree(hashes, mid, end, false)
279
+ const right = subtree(hashes, mid, end, false, known)
271
280
  if (right === null) return null
272
281
 
273
- return bao.parentCV(left, right, isRoot)
282
+ const hash = bao.parentCV(left, right, isRoot)
283
+ if (key) known.set(key, hash)
284
+ return hash
274
285
  }
275
286
 
276
287
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmwire-lib",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",