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 +6 -3
- package/lib/fs.js +26 -0
- package/lib/swarmwire.js +14 -4
- package/lib/tree.js +19 -8
- package/package.json +1 -1
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
|
|
59
|
-
>
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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