swarmwire-lib 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +137 -2
- package/docs/contract.md +121 -0
- package/docs/format.md +180 -0
- package/index.js +12 -0
- package/lib/address.js +166 -0
- package/lib/blake3.cjs +15 -0
- package/lib/blake3.js +12 -0
- package/lib/collection.js +133 -0
- package/lib/destination.js +237 -0
- package/lib/fs.js +176 -0
- package/lib/nodes.js +118 -0
- package/lib/partfile.js +169 -0
- package/lib/select.js +33 -0
- package/lib/source.js +171 -0
- package/lib/swarmwire.js +841 -0
- package/lib/timers.js +21 -0
- package/lib/tree.js +326 -0
- package/lib/wire.js +95 -0
- package/package.json +94 -4
package/lib/timers.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time, injected.
|
|
3
|
+
*
|
|
4
|
+
* The transfer engine needs to give up on a peer that goes quiet, which means it needs a
|
|
5
|
+
* clock. Taking the clock as a dependency keeps `setTimeout` out of the engine, so the test
|
|
6
|
+
* harness can advance time by hand instead of tests sleeping and hoping.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const systemTimers = {
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} ms
|
|
12
|
+
* @param {Function} fn
|
|
13
|
+
* @returns {Function} cancel
|
|
14
|
+
*/
|
|
15
|
+
setTimer (ms, fn) {
|
|
16
|
+
const timer = setTimeout(fn, ms)
|
|
17
|
+
if (typeof timer.unref === 'function') timer.unref() // never hold a process open
|
|
18
|
+
|
|
19
|
+
return () => clearTimeout(timer)
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/tree.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The group tree: BLAKE3's own hash tree, read at the granularity of chunk groups instead of
|
|
3
|
+
* single 1 KiB chunks.
|
|
4
|
+
*
|
|
5
|
+
* BLAKE3 already hashes by building a tree over 1 KiB chunks. Taking the tree seriously one
|
|
6
|
+
* level up — treating a group of chunks as a leaf — gives exactly what a transfer needs:
|
|
7
|
+
*
|
|
8
|
+
* - the root is still the plain BLAKE3 hash of the content (`b3sum` agrees)
|
|
9
|
+
* - any single group can be proved against that root with ~log2(groups) hashes
|
|
10
|
+
* - a sender keeps only the group hashes, never the content
|
|
11
|
+
* - a receiver can verify a group the moment it arrives, holding nothing but the root
|
|
12
|
+
*
|
|
13
|
+
* The shape must match BLAKE3's exactly or nothing agrees with anything: the tree splits at
|
|
14
|
+
* the largest power of two *below* the number of leaves, which makes it lopsided for counts
|
|
15
|
+
* that are not powers of two. `test/unit/tree.test.js` checks the root against the plain hash
|
|
16
|
+
* at every size that straddles a boundary.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { bao } from './blake3.js'
|
|
20
|
+
import b4a from 'b4a'
|
|
21
|
+
|
|
22
|
+
/** BLAKE3's chunk size. Not ours to choose. */
|
|
23
|
+
export const CHUNK_LEN = 1024
|
|
24
|
+
|
|
25
|
+
/** log2 of the chunks per group: 4 → 16 KiB. See docs/format.md §2. */
|
|
26
|
+
export const DEFAULT_GROUP_LOG = 4
|
|
27
|
+
|
|
28
|
+
/** Bytes per group. */
|
|
29
|
+
export function groupLength (groupLog = DEFAULT_GROUP_LOG) {
|
|
30
|
+
return CHUNK_LEN << groupLog
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** How many groups content of this size has. Empty content is one empty group. */
|
|
34
|
+
export function groupCount (size, groupLog = DEFAULT_GROUP_LOG) {
|
|
35
|
+
if (size === 0) return 1
|
|
36
|
+
return Math.ceil(size / groupLength(groupLog))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Where a group lives in the content.
|
|
41
|
+
*
|
|
42
|
+
* @returns {{offset: number, length: number}}
|
|
43
|
+
*/
|
|
44
|
+
export function groupRange (index, size, groupLog = DEFAULT_GROUP_LOG) {
|
|
45
|
+
const len = groupLength(groupLog)
|
|
46
|
+
const offset = index * len
|
|
47
|
+
if (offset > size) throw new RangeError('group index is past the end of the content')
|
|
48
|
+
return { offset, length: Math.min(len, size - offset) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds a tree one group at a time, so hashing a file never means holding it in memory.
|
|
53
|
+
*/
|
|
54
|
+
export class GroupTreeBuilder {
|
|
55
|
+
constructor ({ groupLog = DEFAULT_GROUP_LOG } = {}) {
|
|
56
|
+
this.groupLog = groupLog
|
|
57
|
+
this.size = 0
|
|
58
|
+
|
|
59
|
+
this._hashes = []
|
|
60
|
+
this._full = groupLength(groupLog)
|
|
61
|
+
this._short = false
|
|
62
|
+
this._firstGroup = null // kept only until we know whether it is the only one
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add the next group. Every group but the last must be exactly `groupLength()` bytes.
|
|
67
|
+
*
|
|
68
|
+
* @param {Uint8Array} data
|
|
69
|
+
*/
|
|
70
|
+
push (data) {
|
|
71
|
+
if (this._short) throw new Error('a short group must be the last one: previous group was not full')
|
|
72
|
+
if (data.length > this._full) throw new Error('group is longer than the group length')
|
|
73
|
+
if (data.length < this._full) this._short = true
|
|
74
|
+
|
|
75
|
+
const index = this._hashes.length
|
|
76
|
+
|
|
77
|
+
// A lone group is the root of the tree, which changes how BLAKE3 finalises it. We do not
|
|
78
|
+
// know yet whether another group is coming, so keep the first one until we do.
|
|
79
|
+
if (index === 0) this._firstGroup = b4a.from(data)
|
|
80
|
+
else this._firstGroup = null
|
|
81
|
+
|
|
82
|
+
this._hashes.push(bao.chunkGroupCV(data, index << this.groupLog, false))
|
|
83
|
+
this.size += data.length
|
|
84
|
+
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @returns {GroupTree} */
|
|
89
|
+
finalize () {
|
|
90
|
+
if (this._hashes.length === 0) this.push(b4a.alloc(0))
|
|
91
|
+
|
|
92
|
+
const hashes = this._hashes
|
|
93
|
+
if (hashes.length === 1) {
|
|
94
|
+
// Recompute the only group as the root.
|
|
95
|
+
hashes[0] = bao.chunkGroupCV(this._firstGroup, 0, true)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return new GroupTree(hashes, { size: this.size, groupLog: this.groupLog })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The hash of one group: its chaining value in the tree.
|
|
104
|
+
*
|
|
105
|
+
* A peer that holds a group can always recompute this from the bytes, which is what lets a
|
|
106
|
+
* half-finished download prove the parts it does have.
|
|
107
|
+
*
|
|
108
|
+
* @param {Uint8Array} data
|
|
109
|
+
* @param {number} index
|
|
110
|
+
* @param {{groupLog?: number, only?: boolean}} [opts] - `only` when the group is the whole
|
|
111
|
+
* content, which BLAKE3 finalises differently
|
|
112
|
+
* @returns {Uint8Array} 32 bytes
|
|
113
|
+
*/
|
|
114
|
+
export function groupHash (data, index, { groupLog = DEFAULT_GROUP_LOG, only = false } = {}) {
|
|
115
|
+
return bao.chunkGroupCV(data, index << groupLog, only)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class GroupTree {
|
|
119
|
+
/**
|
|
120
|
+
* @param {(Uint8Array|null)[]} groupHashes - holes are allowed: a partial holder knows only
|
|
121
|
+
* the groups it has, and can prove a group only when the siblings it needs are present
|
|
122
|
+
* @param {{size: number, groupLog?: number}} opts
|
|
123
|
+
*/
|
|
124
|
+
constructor (groupHashes, { size, groupLog = DEFAULT_GROUP_LOG }) {
|
|
125
|
+
if (groupHashes.length !== groupCount(size, groupLog)) {
|
|
126
|
+
throw new Error('group hashes do not match the content size')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.groupHashes = groupHashes
|
|
130
|
+
this.size = size
|
|
131
|
+
this.groupLog = groupLog
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Hash content held in memory. For files, stream it through GroupTreeBuilder instead. */
|
|
135
|
+
static fromContent (content, { groupLog = DEFAULT_GROUP_LOG } = {}) {
|
|
136
|
+
const builder = new GroupTreeBuilder({ groupLog })
|
|
137
|
+
const len = groupLength(groupLog)
|
|
138
|
+
|
|
139
|
+
for (let offset = 0; offset < content.length; offset += len) {
|
|
140
|
+
builder.push(content.subarray(offset, Math.min(offset + len, content.length)))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return builder.finalize()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Rebuild from cached group hashes — what a sender keeps next to a file. */
|
|
147
|
+
static fromGroupHashes (groupHashes, { size, groupLog = DEFAULT_GROUP_LOG } = {}) {
|
|
148
|
+
return new GroupTree(groupHashes.map((h) => b4a.from(h)), { size, groupLog })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get groupCount () {
|
|
152
|
+
return this.groupHashes.length
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** True when every group's hash is known, so the root can be computed. */
|
|
156
|
+
get complete () {
|
|
157
|
+
return this.groupHashes.every((hash) => hash !== null && hash !== undefined)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The content address: the plain BLAKE3 hash, hex encoded. Null if any group is missing. */
|
|
161
|
+
get root () {
|
|
162
|
+
if (!this.complete) return null
|
|
163
|
+
if (this.groupCount === 1) return b4a.toString(this.groupHashes[0], 'hex')
|
|
164
|
+
|
|
165
|
+
return b4a.toString(subtree(this.groupHashes, 0, this.groupCount, true), 'hex')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The sibling hashes proving `index` belongs to the root, nearest sibling first.
|
|
170
|
+
*
|
|
171
|
+
* Returns null when a sibling subtree has a hole in it: a partial holder can prove some of
|
|
172
|
+
* what it has, but not all of it, and must not pretend otherwise.
|
|
173
|
+
*
|
|
174
|
+
* @param {number} index
|
|
175
|
+
* @returns {Uint8Array[]|null}
|
|
176
|
+
*/
|
|
177
|
+
proof (index) {
|
|
178
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.groupCount) {
|
|
179
|
+
throw new RangeError('group index out of range')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const siblings = []
|
|
183
|
+
let start = 0
|
|
184
|
+
let end = this.groupCount
|
|
185
|
+
|
|
186
|
+
while (end - start > 1) {
|
|
187
|
+
const mid = start + splitPoint(end - start)
|
|
188
|
+
const sibling = index < mid
|
|
189
|
+
? subtree(this.groupHashes, mid, end, false)
|
|
190
|
+
: subtree(this.groupHashes, start, mid, false)
|
|
191
|
+
|
|
192
|
+
if (sibling === null) return null
|
|
193
|
+
siblings.push(sibling)
|
|
194
|
+
|
|
195
|
+
if (index < mid) end = mid
|
|
196
|
+
else start = mid
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return siblings.reverse()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Can this group be proved with what is known? */
|
|
203
|
+
canProve (index) {
|
|
204
|
+
return this.groupHashes[index] != null && this.proof(index) !== null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check that `data` really is group `index` of the content addressed by `root`.
|
|
210
|
+
*
|
|
211
|
+
* Everything a peer sends goes through here before it reaches a destination file. Returns
|
|
212
|
+
* false rather than throwing: a lying peer is an expected condition, not an exception.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} opts
|
|
215
|
+
* @param {string} opts.root - 64 hex characters
|
|
216
|
+
* @param {number} opts.size - content size in bytes
|
|
217
|
+
* @param {number} opts.index - group index
|
|
218
|
+
* @param {Uint8Array} opts.data - the group's bytes
|
|
219
|
+
* @param {Uint8Array[]} opts.proof - sibling hashes, nearest first
|
|
220
|
+
* @param {number} [opts.groupLog]
|
|
221
|
+
* @returns {boolean}
|
|
222
|
+
*/
|
|
223
|
+
export function verifyGroup ({ root, size, index, data, proof, groupLog = DEFAULT_GROUP_LOG }) {
|
|
224
|
+
const count = groupCount(size, groupLog)
|
|
225
|
+
if (!Number.isInteger(index) || index < 0 || index >= count) return false
|
|
226
|
+
|
|
227
|
+
// The size a group must have is fixed by the content size: a peer cannot pad or truncate.
|
|
228
|
+
const expected = groupRange(index, size, groupLog)
|
|
229
|
+
if (data.length !== expected.length) return false
|
|
230
|
+
|
|
231
|
+
const path = descend(count, index)
|
|
232
|
+
if (!Array.isArray(proof) || proof.length !== path.length) return false
|
|
233
|
+
|
|
234
|
+
let cv = bao.chunkGroupCV(data, index << groupLog, count === 1)
|
|
235
|
+
|
|
236
|
+
// Fold upwards, nearest sibling first; the last parent is the root.
|
|
237
|
+
for (let i = 0; i < proof.length; i++) {
|
|
238
|
+
const sibling = proof[i]
|
|
239
|
+
if (!sibling || sibling.length !== 32) return false
|
|
240
|
+
|
|
241
|
+
const isRoot = i === proof.length - 1
|
|
242
|
+
const goesLeft = path[path.length - 1 - i] === LEFT
|
|
243
|
+
|
|
244
|
+
cv = goesLeft ? bao.parentCV(cv, sibling, isRoot) : bao.parentCV(sibling, cv, isRoot)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return bao.constantTimeEqual(cv, b4a.from(root, 'hex'))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// -- tree shape --------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
const LEFT = 0
|
|
253
|
+
const RIGHT = 1
|
|
254
|
+
|
|
255
|
+
/** BLAKE3 splits at the largest power of two strictly below the leaf count. */
|
|
256
|
+
export function splitPoint (count) {
|
|
257
|
+
let p = 1
|
|
258
|
+
while (p * 2 < count) p *= 2
|
|
259
|
+
return p
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Null when any leaf under this subtree is unknown. */
|
|
263
|
+
function subtree (hashes, start, end, isRoot) {
|
|
264
|
+
if (end - start === 1) return hashes[start] ?? null
|
|
265
|
+
|
|
266
|
+
const mid = start + splitPoint(end - start)
|
|
267
|
+
const left = subtree(hashes, start, mid, false)
|
|
268
|
+
if (left === null) return null
|
|
269
|
+
|
|
270
|
+
const right = subtree(hashes, mid, end, false)
|
|
271
|
+
if (right === null) return null
|
|
272
|
+
|
|
273
|
+
return bao.parentCV(left, right, isRoot)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The subtree ranges a proof for `index` is made of, nearest sibling first — the same order
|
|
278
|
+
* the proof itself uses.
|
|
279
|
+
*
|
|
280
|
+
* Knowing *which* ranges a proof's hashes describe is what lets a receiver keep them: the
|
|
281
|
+
* hashes it was given to check one group are exactly the ones it needs to prove that group to
|
|
282
|
+
* somebody else later.
|
|
283
|
+
*
|
|
284
|
+
* @param {number} count - total groups
|
|
285
|
+
* @param {number} index
|
|
286
|
+
* @returns {{start: number, end: number}[]}
|
|
287
|
+
*/
|
|
288
|
+
export function siblingRanges (count, index) {
|
|
289
|
+
const ranges = []
|
|
290
|
+
let start = 0
|
|
291
|
+
let end = count
|
|
292
|
+
|
|
293
|
+
while (end - start > 1) {
|
|
294
|
+
const mid = start + splitPoint(end - start)
|
|
295
|
+
|
|
296
|
+
if (index < mid) {
|
|
297
|
+
ranges.push({ start: mid, end })
|
|
298
|
+
end = mid
|
|
299
|
+
} else {
|
|
300
|
+
ranges.push({ start, end: mid })
|
|
301
|
+
start = mid
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return ranges.reverse()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The path from the root down to `index`: which side it is on at each level. */
|
|
309
|
+
function descend (count, index) {
|
|
310
|
+
const path = []
|
|
311
|
+
let start = 0
|
|
312
|
+
let end = count
|
|
313
|
+
|
|
314
|
+
while (end - start > 1) {
|
|
315
|
+
const mid = start + splitPoint(end - start)
|
|
316
|
+
if (index < mid) {
|
|
317
|
+
path.push(LEFT)
|
|
318
|
+
end = mid
|
|
319
|
+
} else {
|
|
320
|
+
path.push(RIGHT)
|
|
321
|
+
start = mid
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return path
|
|
326
|
+
}
|
package/lib/wire.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire: what peers say to each other, and nothing more.
|
|
3
|
+
*
|
|
4
|
+
* Six messages. A peer asks who has some content, holders say so, the asker requests groups
|
|
5
|
+
* one at a time, and each answer carries the proof that makes it checkable on arrival.
|
|
6
|
+
* Scheduling, windows and peer choice live in swarmwire.js — none of that is on the wire.
|
|
7
|
+
*
|
|
8
|
+
* Unknown message types are ignored rather than treated as errors, so adding a message later
|
|
9
|
+
* never breaks a peer running today's version.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import c from 'compact-encoding'
|
|
13
|
+
import Protomux from 'protomux'
|
|
14
|
+
|
|
15
|
+
export const PROTOCOL = 'swarmwire/0'
|
|
16
|
+
export const VERSION = 0
|
|
17
|
+
|
|
18
|
+
/** Field order is the wire format: append, never reorder. */
|
|
19
|
+
const encodings = {
|
|
20
|
+
handshake: record({ version: c.uint, groupLog: c.uint }),
|
|
21
|
+
want: record({ root: c.fixed32, size: c.uint }),
|
|
22
|
+
// `have` carries a bitfield because a peer may hold only part of the content: a download
|
|
23
|
+
// still in progress serves what it has, and asking it for the rest wastes a request.
|
|
24
|
+
have: record({ root: c.fixed32, size: c.uint, groupLog: c.uint, bitfield: c.buffer }),
|
|
25
|
+
request: record({ root: c.fixed32, index: c.uint }),
|
|
26
|
+
data: record({ root: c.fixed32, index: c.uint, proof: c.array(c.fixed32), data: c.buffer }),
|
|
27
|
+
unavailable: record({ root: c.fixed32 }),
|
|
28
|
+
// One new group, announced to whoever is interested. Sending a whole bitfield per group
|
|
29
|
+
// would cost kilobytes each time on a large file.
|
|
30
|
+
holds: record({ root: c.fixed32, index: c.uint }),
|
|
31
|
+
// "Not that one, not now." Silence would work too, but only after a timeout, and a group
|
|
32
|
+
// reserved for fifteen seconds that nobody is fetching is a stalled download.
|
|
33
|
+
reject: record({ root: c.fixed32, index: c.uint })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Message order is the wire format too: protomux identifies messages by position. */
|
|
37
|
+
const ORDER = ['handshake', 'want', 'have', 'request', 'data', 'unavailable', 'holds', 'reject']
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Open the swarmwire channel on a connection.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} stream - a duplex connection from the swarm
|
|
43
|
+
* @param {Record<string, Function>} handlers - onhandshake, onwant, onhave, onrequest,
|
|
44
|
+
* ondata, onunavailable, onclose; all optional
|
|
45
|
+
* @returns {{channel: object, send: Record<string, Function>}|null} null if the channel is
|
|
46
|
+
* already open on this connection
|
|
47
|
+
*/
|
|
48
|
+
export function openChannel (stream, handlers = {}) {
|
|
49
|
+
const mux = Protomux.from(stream)
|
|
50
|
+
|
|
51
|
+
const channel = mux.createChannel({
|
|
52
|
+
protocol: PROTOCOL,
|
|
53
|
+
onclose: handlers.onclose
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (channel === null) return null // someone else already opened it on this stream
|
|
57
|
+
|
|
58
|
+
const messages = {}
|
|
59
|
+
for (const name of ORDER) {
|
|
60
|
+
messages[name] = channel.addMessage({
|
|
61
|
+
encoding: encodings[name],
|
|
62
|
+
onmessage: handlers['on' + name] ?? noop
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
channel.open()
|
|
67
|
+
|
|
68
|
+
const send = {}
|
|
69
|
+
for (const name of ORDER) {
|
|
70
|
+
send[name] = (payload) => messages[name].send(payload)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { channel, send }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Build an encoding from an ordered set of fields. */
|
|
77
|
+
function record (fields) {
|
|
78
|
+
const keys = Object.keys(fields)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
preencode (state, message) {
|
|
82
|
+
for (const key of keys) fields[key].preencode(state, message[key])
|
|
83
|
+
},
|
|
84
|
+
encode (state, message) {
|
|
85
|
+
for (const key of keys) fields[key].encode(state, message[key])
|
|
86
|
+
},
|
|
87
|
+
decode (state) {
|
|
88
|
+
const message = {}
|
|
89
|
+
for (const key of keys) message[key] = fields[key].decode(state)
|
|
90
|
+
return message
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function noop () {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,96 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmwire-lib",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Content-addressed, verified file transfer between filesystems over Hyperswarm. No copies, no cores, no trackers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/tibocub/swarmwire.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/tibocub/swarmwire/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/tibocub/swarmwire#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"hyperswarm",
|
|
18
|
+
"p2p",
|
|
19
|
+
"file-transfer",
|
|
20
|
+
"content-addressed",
|
|
21
|
+
"bare"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"lib/",
|
|
26
|
+
"docs/format.md",
|
|
27
|
+
"docs/contract.md"
|
|
28
|
+
],
|
|
29
|
+
"imports": {
|
|
30
|
+
"fs": {
|
|
31
|
+
"bare": "bare-fs",
|
|
32
|
+
"default": "fs"
|
|
33
|
+
},
|
|
34
|
+
"events": {
|
|
35
|
+
"bare": "bare-events",
|
|
36
|
+
"default": "events"
|
|
37
|
+
},
|
|
38
|
+
"process": {
|
|
39
|
+
"bare": "bare-process",
|
|
40
|
+
"default": "process"
|
|
41
|
+
},
|
|
42
|
+
"path": {
|
|
43
|
+
"bare": "bare-path",
|
|
44
|
+
"default": "path"
|
|
45
|
+
},
|
|
46
|
+
"os": {
|
|
47
|
+
"bare": "bare-os",
|
|
48
|
+
"default": "os"
|
|
49
|
+
},
|
|
50
|
+
"url": {
|
|
51
|
+
"bare": "bare-url",
|
|
52
|
+
"default": "url"
|
|
53
|
+
},
|
|
54
|
+
"child_process": {
|
|
55
|
+
"bare": "bare-subprocess",
|
|
56
|
+
"default": "child_process"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "npm run test:unit && npm run test:fs && npm run test:swarm",
|
|
61
|
+
"test:unit": "brittle test/unit/*.test.js",
|
|
62
|
+
"test:fs": "brittle test/fs/*.test.js",
|
|
63
|
+
"test:swarm": "brittle test/swarm/*.test.js",
|
|
64
|
+
"test:net": "brittle test/net/*.test.js",
|
|
65
|
+
"test:all": "npm test && npm run test:net && npm run test:bare && npm run test:bare:net",
|
|
66
|
+
"test:bare": "bare node_modules/brittle/bin/bare.js test/unit/*.test.js test/fs/*.test.js test/swarm/*.test.js",
|
|
67
|
+
"test:bare:net": "bare node_modules/brittle/bin/bare.js test/net/*.test.js",
|
|
68
|
+
"bench": "node bench/swarm.js",
|
|
69
|
+
"bench:net": "node bench/throughput.js",
|
|
70
|
+
"check:pack": "node scripts/check-pack.js"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"b4a": "^1.9.0",
|
|
74
|
+
"bare-events": "^2.9.0",
|
|
75
|
+
"bare-fs": "^4.8.2",
|
|
76
|
+
"blake3-bao": "^1.3.4",
|
|
77
|
+
"compact-encoding": "^3.5.2",
|
|
78
|
+
"hyperswarm": "^4.17.2",
|
|
79
|
+
"protomux": "^3.12.1",
|
|
80
|
+
"streamx": "^2.28.1"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@hyperswarm/testnet": "^3.1.4",
|
|
84
|
+
"bare": "^1.34.1",
|
|
85
|
+
"bare-os": "^3.6.1",
|
|
86
|
+
"bare-path": "^3.0.0",
|
|
87
|
+
"bare-process": "^4.2.4",
|
|
88
|
+
"bare-subprocess": "^5.0.0",
|
|
89
|
+
"bare-url": "^2.1.7",
|
|
90
|
+
"brittle": "^3.7.0"
|
|
91
|
+
},
|
|
92
|
+
"engines": {
|
|
93
|
+
"bare": ">=1.16",
|
|
94
|
+
"node": ">=20"
|
|
95
|
+
}
|
|
96
|
+
}
|