@vdoninja/ninja-p2p 0.1.4 → 0.2.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/.agents/skills/ninja-p2p/SKILL.md +102 -2
- package/.claude/skills/ninja-p2p/SKILL.md +209 -130
- package/.codex/skills/ninja-p2p/SKILL.md +102 -2
- package/CHANGELOG.md +113 -0
- package/README.md +1077 -775
- package/dashboard.html +370 -50
- package/dist/agent-state.js +9 -0
- package/dist/cli-lib.d.ts +40 -0
- package/dist/cli-lib.js +165 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +501 -10
- package/dist/demo.d.ts +37 -0
- package/dist/demo.js +186 -0
- package/dist/doctor.d.ts +52 -0
- package/dist/doctor.js +219 -0
- package/dist/file-transfer.d.ts +15 -2
- package/dist/file-transfer.js +249 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/message-bus.d.ts +15 -0
- package/dist/message-bus.js +32 -3
- package/dist/peer-registry.js +7 -0
- package/dist/protocol.d.ts +78 -1
- package/dist/protocol.js +42 -6
- package/dist/shared-folders.js +20 -1
- package/dist/social-stream.d.ts +100 -0
- package/dist/social-stream.js +254 -0
- package/dist/swarm-manager.d.ts +164 -0
- package/dist/swarm-manager.js +957 -0
- package/dist/swarm-session.d.ts +197 -0
- package/dist/swarm-session.js +465 -0
- package/dist/swarm-wire.d.ts +48 -0
- package/dist/swarm-wire.js +86 -0
- package/dist/swarm.d.ts +258 -0
- package/dist/swarm.js +694 -0
- package/dist/vdo-bridge.d.ts +62 -1
- package/dist/vdo-bridge.js +273 -23
- package/dist/vdoninja-sdk-types.d.ts +75 -0
- package/dist/vdoninja-sdk-types.js +10 -0
- package/dist/wake.d.ts +83 -0
- package/dist/wake.js +206 -0
- package/docs/protocol-and-reliability.md +231 -38
- package/docs/sdk-wishlist.md +236 -0
- package/docs/security.md +197 -0
- package/docs/social-stream-bridge.md +390 -0
- package/package.json +125 -116
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm binary wire format
|
|
3
|
+
*
|
|
4
|
+
* Chunks used to travel as base64 inside a JSON envelope, because the SDK
|
|
5
|
+
* stringified everything before it reached the data channel. `sendBinary` gives
|
|
6
|
+
* us a real byte lane, so a chunk now goes out as a 40-byte header followed by
|
|
7
|
+
* the payload untouched — 0.06% overhead on a 64 KB chunk instead of 33%.
|
|
8
|
+
*
|
|
9
|
+
* The lane carries no routing of its own: `binaryReceived` tells us which peer
|
|
10
|
+
* sent bytes and nothing else. So the frame has to identify the file and the
|
|
11
|
+
* chunk itself, and has to be self-describing enough that a future frame type
|
|
12
|
+
* can share the lane without ambiguity.
|
|
13
|
+
*
|
|
14
|
+
* 0 magic 'N'
|
|
15
|
+
* 1 magic 'J'
|
|
16
|
+
* 2 version
|
|
17
|
+
* 3 frame type
|
|
18
|
+
* 4..35 fileId, 32 raw bytes (the sha256 the hex id encodes)
|
|
19
|
+
* 36..39 chunk index, uint32 big-endian
|
|
20
|
+
* 40.. payload
|
|
21
|
+
*/
|
|
22
|
+
export declare const SWARM_WIRE_MAGIC_0 = 78;
|
|
23
|
+
export declare const SWARM_WIRE_MAGIC_1 = 74;
|
|
24
|
+
export declare const SWARM_WIRE_VERSION = 1;
|
|
25
|
+
export declare const SWARM_FRAME_CHUNK = 1;
|
|
26
|
+
export declare const SWARM_WIRE_HEADER_BYTES = 40;
|
|
27
|
+
export type SwarmChunkFrame = {
|
|
28
|
+
fileId: string;
|
|
29
|
+
index: number;
|
|
30
|
+
data: Uint8Array;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether a content id can ride the binary lane.
|
|
34
|
+
*
|
|
35
|
+
* Ids are sha256 hex, so this is true for anything we produce. A peer that
|
|
36
|
+
* offers something else is not lied to or dropped — it just gets the base64
|
|
37
|
+
* path, which imposes no constraints on the id at all.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isBinaryFileId(fileId: string): boolean;
|
|
40
|
+
export declare function encodeChunkFrame(fileId: string, index: number, data: Uint8Array): Uint8Array;
|
|
41
|
+
/**
|
|
42
|
+
* Parse a frame, or return null if it is not one of ours.
|
|
43
|
+
*
|
|
44
|
+
* Null rather than throwing: the binary lane is shared with anything else the
|
|
45
|
+
* application chooses to put on it, and a frame we do not recognise is somebody
|
|
46
|
+
* else's business, not an error.
|
|
47
|
+
*/
|
|
48
|
+
export declare function decodeChunkFrame(bytes: Uint8Array): SwarmChunkFrame | null;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm binary wire format
|
|
3
|
+
*
|
|
4
|
+
* Chunks used to travel as base64 inside a JSON envelope, because the SDK
|
|
5
|
+
* stringified everything before it reached the data channel. `sendBinary` gives
|
|
6
|
+
* us a real byte lane, so a chunk now goes out as a 40-byte header followed by
|
|
7
|
+
* the payload untouched — 0.06% overhead on a 64 KB chunk instead of 33%.
|
|
8
|
+
*
|
|
9
|
+
* The lane carries no routing of its own: `binaryReceived` tells us which peer
|
|
10
|
+
* sent bytes and nothing else. So the frame has to identify the file and the
|
|
11
|
+
* chunk itself, and has to be self-describing enough that a future frame type
|
|
12
|
+
* can share the lane without ambiguity.
|
|
13
|
+
*
|
|
14
|
+
* 0 magic 'N'
|
|
15
|
+
* 1 magic 'J'
|
|
16
|
+
* 2 version
|
|
17
|
+
* 3 frame type
|
|
18
|
+
* 4..35 fileId, 32 raw bytes (the sha256 the hex id encodes)
|
|
19
|
+
* 36..39 chunk index, uint32 big-endian
|
|
20
|
+
* 40.. payload
|
|
21
|
+
*/
|
|
22
|
+
export const SWARM_WIRE_MAGIC_0 = 0x4e; // 'N'
|
|
23
|
+
export const SWARM_WIRE_MAGIC_1 = 0x4a; // 'J'
|
|
24
|
+
export const SWARM_WIRE_VERSION = 1;
|
|
25
|
+
export const SWARM_FRAME_CHUNK = 1;
|
|
26
|
+
export const SWARM_WIRE_HEADER_BYTES = 40;
|
|
27
|
+
const FILE_ID_BYTES = 32;
|
|
28
|
+
const HEX_FILE_ID = /^[0-9a-f]{64}$/;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a content id can ride the binary lane.
|
|
31
|
+
*
|
|
32
|
+
* Ids are sha256 hex, so this is true for anything we produce. A peer that
|
|
33
|
+
* offers something else is not lied to or dropped — it just gets the base64
|
|
34
|
+
* path, which imposes no constraints on the id at all.
|
|
35
|
+
*/
|
|
36
|
+
export function isBinaryFileId(fileId) {
|
|
37
|
+
return HEX_FILE_ID.test(fileId);
|
|
38
|
+
}
|
|
39
|
+
export function encodeChunkFrame(fileId, index, data) {
|
|
40
|
+
if (!isBinaryFileId(fileId)) {
|
|
41
|
+
throw new Error(`fileId is not a 64-character hex id: ${fileId}`);
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isInteger(index) || index < 0 || index > 0xffffffff) {
|
|
44
|
+
throw new Error(`chunk index out of range: ${index}`);
|
|
45
|
+
}
|
|
46
|
+
const frame = new Uint8Array(SWARM_WIRE_HEADER_BYTES + data.length);
|
|
47
|
+
frame[0] = SWARM_WIRE_MAGIC_0;
|
|
48
|
+
frame[1] = SWARM_WIRE_MAGIC_1;
|
|
49
|
+
frame[2] = SWARM_WIRE_VERSION;
|
|
50
|
+
frame[3] = SWARM_FRAME_CHUNK;
|
|
51
|
+
for (let i = 0; i < FILE_ID_BYTES; i += 1) {
|
|
52
|
+
frame[4 + i] = Number.parseInt(fileId.slice(i * 2, i * 2 + 2), 16);
|
|
53
|
+
}
|
|
54
|
+
const view = new DataView(frame.buffer, frame.byteOffset);
|
|
55
|
+
view.setUint32(36, index, false);
|
|
56
|
+
frame.set(data, SWARM_WIRE_HEADER_BYTES);
|
|
57
|
+
return frame;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a frame, or return null if it is not one of ours.
|
|
61
|
+
*
|
|
62
|
+
* Null rather than throwing: the binary lane is shared with anything else the
|
|
63
|
+
* application chooses to put on it, and a frame we do not recognise is somebody
|
|
64
|
+
* else's business, not an error.
|
|
65
|
+
*/
|
|
66
|
+
export function decodeChunkFrame(bytes) {
|
|
67
|
+
if (bytes.length < SWARM_WIRE_HEADER_BYTES)
|
|
68
|
+
return null;
|
|
69
|
+
if (bytes[0] !== SWARM_WIRE_MAGIC_0 || bytes[1] !== SWARM_WIRE_MAGIC_1)
|
|
70
|
+
return null;
|
|
71
|
+
if (bytes[2] !== SWARM_WIRE_VERSION)
|
|
72
|
+
return null;
|
|
73
|
+
if (bytes[3] !== SWARM_FRAME_CHUNK)
|
|
74
|
+
return null;
|
|
75
|
+
let fileId = "";
|
|
76
|
+
for (let i = 0; i < FILE_ID_BYTES; i += 1) {
|
|
77
|
+
fileId += bytes[4 + i].toString(16).padStart(2, "0");
|
|
78
|
+
}
|
|
79
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
80
|
+
const index = view.getUint32(36, false);
|
|
81
|
+
// Copy rather than subarray: the payload outlives the frame — it is hashed,
|
|
82
|
+
// written to disk, and may be re-served — and holding a view would pin the
|
|
83
|
+
// whole received buffer alive for as long as any chunk of it is referenced.
|
|
84
|
+
const data = bytes.slice(SWARM_WIRE_HEADER_BYTES);
|
|
85
|
+
return { fileId, index, data };
|
|
86
|
+
}
|
package/dist/swarm.d.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Transfer
|
|
3
|
+
*
|
|
4
|
+
* The original file transfer was one sender pushing base64 chunks in strict
|
|
5
|
+
* order to one receiver. That caps throughput at a single peer's upload, breaks
|
|
6
|
+
* if a chunk arrives out of order, and cannot resume from anyone else.
|
|
7
|
+
*
|
|
8
|
+
* This is the torrent-shaped replacement. Files are content-addressed by their
|
|
9
|
+
* sha256, so every peer holding the same bytes is interchangeable. Peers publish
|
|
10
|
+
* a bitfield of the chunks they hold, chunks are pulled rather than pushed, and
|
|
11
|
+
* a peer can start serving a chunk the moment it has verified it — while still
|
|
12
|
+
* downloading the rest.
|
|
13
|
+
*
|
|
14
|
+
* The SDK gives one data channel per peer, so parallelism comes from two places:
|
|
15
|
+
* requesting from several peers at once, and keeping several requests in flight
|
|
16
|
+
* on each channel rather than waiting for each to land.
|
|
17
|
+
*
|
|
18
|
+
* This module is deliberately pure and I/O-light so the hard parts — bitfields,
|
|
19
|
+
* piece selection, verification — are testable without a network.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_SWARM_CHUNK_SIZE = 64000;
|
|
22
|
+
export declare const DEFAULT_MAX_IN_FLIGHT_PER_PEER = 4;
|
|
23
|
+
export declare const DEFAULT_MAX_IN_FLIGHT_TOTAL = 24;
|
|
24
|
+
/** Keep each paged manifest message comfortably below any SCTP limit. */
|
|
25
|
+
export declare const SWARM_MANIFEST_PAGE_HASHES = 256;
|
|
26
|
+
/** Small files stay one-message simple; large files request hash pages on demand. */
|
|
27
|
+
export declare const SWARM_INLINE_MANIFEST_HASHES = 256;
|
|
28
|
+
/** Bounds allocations from a peer-controlled manifest while still allowing ~64 GB at 64 KB chunks. */
|
|
29
|
+
export declare const MAX_SWARM_TOTAL_CHUNKS = 1000000;
|
|
30
|
+
/** A chunk must fit in memory and, in practice, one data-channel message. */
|
|
31
|
+
export declare const MAX_SWARM_CHUNK_SIZE: number;
|
|
32
|
+
export declare const SHA256_HEX_PATTERN: RegExp;
|
|
33
|
+
/** Room left for the JSON envelope wrapped around a base64 chunk. */
|
|
34
|
+
export declare const SWARM_ENVELOPE_HEADROOM = 4096;
|
|
35
|
+
/** After this long with no progress, a lock is treated as abandoned. */
|
|
36
|
+
export declare const SWARM_LOCK_STALE_MS: number;
|
|
37
|
+
/**
|
|
38
|
+
* Where an in-progress download is written.
|
|
39
|
+
*
|
|
40
|
+
* Keyed by content **and destination**, not content alone. Two `fetch` runs of
|
|
41
|
+
* the same file into different folders are a perfectly ordinary thing to do,
|
|
42
|
+
* and sharing one `.part` between them meant they wrote over each other and the
|
|
43
|
+
* first to finish renamed the file out from under the rest — which surfaced as
|
|
44
|
+
* an ENOENT crash, having already corrupted whatever the others had written.
|
|
45
|
+
*
|
|
46
|
+
* Keeping the destination in the key also preserves resume: the same fetch, run
|
|
47
|
+
* again, lands on the same part file and picks up where it left off.
|
|
48
|
+
*/
|
|
49
|
+
export declare function partPathFor(workDir: string, fileId: string, savedPath: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Largest chunk that still fits one SCTP message once base64-encoded.
|
|
52
|
+
*
|
|
53
|
+
* Only the fallback path needs this. Binary chunks add a 40-byte header, so a
|
|
54
|
+
* 64 KB chunk fits inside even the 65536 every WebRTC implementation must
|
|
55
|
+
* support. Base64 inflates by 4/3, so the same chunk becomes 85 KB and would be
|
|
56
|
+
* refused by a peer that negotiated only that minimum — a failure that would
|
|
57
|
+
* only ever show up against an implementation we had not tested with.
|
|
58
|
+
*
|
|
59
|
+
* Every implementation measured here negotiates 262144, which is why this has
|
|
60
|
+
* never bitten. It is a guard, not a tuning knob: chunk size was measured
|
|
61
|
+
* against throughput at 64 KB, 128 KB and 192 KB and moved it by under 2%.
|
|
62
|
+
*/
|
|
63
|
+
export declare function maxSafeChunkSize(messageLimit: number): number;
|
|
64
|
+
/** Immutable description of a file, shared by every peer in its swarm. */
|
|
65
|
+
export type SwarmManifest = {
|
|
66
|
+
/** sha256 of the whole file. Also the swarm identity. */
|
|
67
|
+
fileId: string;
|
|
68
|
+
name: string;
|
|
69
|
+
mimeType: string;
|
|
70
|
+
size: number;
|
|
71
|
+
chunkSize: number;
|
|
72
|
+
totalChunks: number;
|
|
73
|
+
/** sha256 per chunk, so a bad chunk can be blamed on the peer that sent it. */
|
|
74
|
+
chunkHashes: string[];
|
|
75
|
+
/** sha256 over the raw chunk hashes, used to authenticate paged manifests. */
|
|
76
|
+
chunkHashesHash: string;
|
|
77
|
+
};
|
|
78
|
+
/** Metadata that is safe to advertise before the full chunk-hash manifest arrives. */
|
|
79
|
+
export type SwarmManifestSummary = Omit<SwarmManifest, "chunkHashes">;
|
|
80
|
+
/**
|
|
81
|
+
* Which chunks a peer holds.
|
|
82
|
+
*
|
|
83
|
+
* A bitfield rather than a list: a 4 GB file at 64 KB chunks is ~65k chunks,
|
|
84
|
+
* which is 8 KB as bits and would be megabytes as JSON indices. Peers exchange
|
|
85
|
+
* these constantly, so the compact form matters.
|
|
86
|
+
*/
|
|
87
|
+
export declare class ChunkMap {
|
|
88
|
+
readonly totalChunks: number;
|
|
89
|
+
private readonly bits;
|
|
90
|
+
private present;
|
|
91
|
+
constructor(totalChunks: number, bits?: Uint8Array);
|
|
92
|
+
static full(totalChunks: number): ChunkMap;
|
|
93
|
+
static fromBase64(value: string, totalChunks: number): ChunkMap;
|
|
94
|
+
toBase64(): string;
|
|
95
|
+
has(index: number): boolean;
|
|
96
|
+
set(index: number): boolean;
|
|
97
|
+
count(): number;
|
|
98
|
+
isComplete(): boolean;
|
|
99
|
+
missing(): number[];
|
|
100
|
+
clone(): ChunkMap;
|
|
101
|
+
private recount;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* What we have actually observed about a peer.
|
|
105
|
+
*
|
|
106
|
+
* Every field here is measured from real traffic — no declared capacity, no
|
|
107
|
+
* trust tiers, nothing a peer can assert about itself. A peer that claims to be
|
|
108
|
+
* fast and then is not will simply score worse after a few chunks.
|
|
109
|
+
*/
|
|
110
|
+
export type SwarmPeerStats = {
|
|
111
|
+
/** Exponentially weighted round-trip time for chunk requests, or null until measured. */
|
|
112
|
+
rttMs: number | null;
|
|
113
|
+
/** Requests that timed out or returned a chunk failing its hash. */
|
|
114
|
+
failures: number;
|
|
115
|
+
/** Chunks delivered and verified. */
|
|
116
|
+
delivered: number;
|
|
117
|
+
};
|
|
118
|
+
export type SwarmPeerState = {
|
|
119
|
+
peerId: string;
|
|
120
|
+
chunks: ChunkMap;
|
|
121
|
+
/** How many requests are already outstanding with this peer. */
|
|
122
|
+
inFlight: number;
|
|
123
|
+
stats?: SwarmPeerStats;
|
|
124
|
+
};
|
|
125
|
+
/** RTT assumed for a peer we have not measured yet, so new peers still get tried. */
|
|
126
|
+
export declare const UNKNOWN_PEER_RTT_MS = 250;
|
|
127
|
+
export declare function createPeerStats(): SwarmPeerStats;
|
|
128
|
+
/** Fold a new RTT sample in. Weighted toward history so one spike cannot swing it. */
|
|
129
|
+
export declare function recordPeerRtt(stats: SwarmPeerStats, sampleMs: number): SwarmPeerStats;
|
|
130
|
+
/**
|
|
131
|
+
* Rank a peer for serving the next chunk. Lower is better, and the units are
|
|
132
|
+
* milliseconds throughout so the number stays explainable: it is roughly
|
|
133
|
+
* "expected delay if we ask this peer".
|
|
134
|
+
*/
|
|
135
|
+
export declare function scorePeer(peer: SwarmPeerState): number;
|
|
136
|
+
export type ChunkRequestPlan = {
|
|
137
|
+
peerId: string;
|
|
138
|
+
index: number;
|
|
139
|
+
};
|
|
140
|
+
export type PlanOptions = {
|
|
141
|
+
maxInFlightPerPeer?: number;
|
|
142
|
+
maxInFlightTotal?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Tie-break among equally rare chunks. Injectable so tests stay deterministic;
|
|
145
|
+
* everything else about planning already is.
|
|
146
|
+
*/
|
|
147
|
+
random?: () => number;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Decide what to ask for next.
|
|
151
|
+
*
|
|
152
|
+
* Rarest-first: chunks held by the fewest peers are fetched first, so a swarm
|
|
153
|
+
* does not lose the only copy of a chunk when the peer holding it leaves. Among
|
|
154
|
+
* peers that can serve a chunk, the best-scoring one wins — measured latency,
|
|
155
|
+
* observed failures, and current queue depth — which spreads load and routes
|
|
156
|
+
* around bad peers without any central coordination.
|
|
157
|
+
*
|
|
158
|
+
* Ties are broken randomly, and that is load-bearing rather than a detail. Ties
|
|
159
|
+
* are the normal case at the start of a download, when every chunk has exactly
|
|
160
|
+
* the same rarity. Breaking them by index meant every downloader asked for the
|
|
161
|
+
* same chunks in the same order, so downloaders never held anything the others
|
|
162
|
+
* lacked and could never serve each other — three leechers against one seeder
|
|
163
|
+
* measured at exactly one third the speed of one, with no peer-to-peer traffic
|
|
164
|
+
* at all. Random selection makes them diverge immediately.
|
|
165
|
+
*/
|
|
166
|
+
export declare function planChunkRequests(have: ChunkMap, peers: SwarmPeerState[], inFlightIndices: ReadonlySet<number>, options?: PlanOptions): ChunkRequestPlan[];
|
|
167
|
+
export declare function chunkOffset(index: number, chunkSize: number): number;
|
|
168
|
+
export declare function chunkLength(index: number, manifest: Pick<SwarmManifest, "size" | "chunkSize">): number;
|
|
169
|
+
/**
|
|
170
|
+
* A partially downloaded file backed by a sparse `.part` on disk.
|
|
171
|
+
*
|
|
172
|
+
* Chunks are written at their byte offset rather than appended, which is what
|
|
173
|
+
* makes out-of-order and multi-source delivery possible at all.
|
|
174
|
+
*/
|
|
175
|
+
export declare class ChunkFile {
|
|
176
|
+
readonly filePath: string;
|
|
177
|
+
readonly manifest: SwarmManifest;
|
|
178
|
+
private fd;
|
|
179
|
+
private lockPath;
|
|
180
|
+
private lockToken;
|
|
181
|
+
/**
|
|
182
|
+
* @param exclusive Refuse to open if another live process holds this file.
|
|
183
|
+
* Only a download needs it — a seed session opens the user's own file
|
|
184
|
+
* read-mostly and several of those coexisting is fine.
|
|
185
|
+
*/
|
|
186
|
+
constructor(filePath: string, manifest: SwarmManifest, exclusive?: boolean);
|
|
187
|
+
open(): void;
|
|
188
|
+
/**
|
|
189
|
+
* Work out which chunks this file already holds, by hashing them.
|
|
190
|
+
*
|
|
191
|
+
* Keeping the bytes across a restart is only half of resuming; without this
|
|
192
|
+
* the bitfield starts empty and every chunk on disk is fetched again. That is
|
|
193
|
+
* what the code did, so "resume after an interruption" was true of the file
|
|
194
|
+
* and false of the transfer.
|
|
195
|
+
*
|
|
196
|
+
* Every chunk is verified rather than inferred from the file's length. A part
|
|
197
|
+
* file is written at byte offsets, so a gap reads back as zeros and a
|
|
198
|
+
* truncated write leaves a partial chunk — neither is distinguishable from
|
|
199
|
+
* real data by size alone, and trusting either would silently corrupt the
|
|
200
|
+
* result. Hashing costs one pass over the file, which is only paid when a
|
|
201
|
+
* part file already exists.
|
|
202
|
+
*/
|
|
203
|
+
scanVerified(): ChunkMap;
|
|
204
|
+
close(): void;
|
|
205
|
+
/** Close the data handle while retaining ownership through final verification. */
|
|
206
|
+
closeHandle(): void;
|
|
207
|
+
/** Release a download lock after its part file has been finalized or abandoned. */
|
|
208
|
+
releaseExclusiveLock(): void;
|
|
209
|
+
/**
|
|
210
|
+
* Claim the part file, or explain who has it.
|
|
211
|
+
*
|
|
212
|
+
* Two downloads of the same file to the same folder is the one case the
|
|
213
|
+
* destination-keyed path cannot separate, and it is a genuine conflict rather
|
|
214
|
+
* than something to paper over — so fail loudly instead of interleaving
|
|
215
|
+
* writes. A lock left behind by a killed process goes stale rather than
|
|
216
|
+
* blocking that destination forever.
|
|
217
|
+
*/
|
|
218
|
+
private acquireLock;
|
|
219
|
+
/** Refresh the lock so a long, healthy transfer is never mistaken for stale. */
|
|
220
|
+
touchLock(): void;
|
|
221
|
+
/**
|
|
222
|
+
* Verify and store one chunk. Returns false when the bytes do not match the
|
|
223
|
+
* manifest hash, which means the peer that sent them is wrong or hostile —
|
|
224
|
+
* the chunk is dropped and can be re-fetched from someone else.
|
|
225
|
+
*/
|
|
226
|
+
writeChunk(index: number, bytes: Uint8Array): boolean;
|
|
227
|
+
readChunk(index: number): Uint8Array | null;
|
|
228
|
+
}
|
|
229
|
+
/** Build a manifest by hashing a file's bytes chunk by chunk. */
|
|
230
|
+
export declare function buildManifest(bytes: Uint8Array, name: string, mimeType: string, chunkSize?: number): SwarmManifest;
|
|
231
|
+
/**
|
|
232
|
+
* Build a manifest without loading the file into memory.
|
|
233
|
+
*
|
|
234
|
+
* The old path copied the whole file into a Buffer and then a Uint8Array before
|
|
235
|
+
* hashing it, and completion did the same again. That made the "large file"
|
|
236
|
+
* command consume roughly twice the file size in memory. This keeps the peak at
|
|
237
|
+
* one chunk while producing the same content and chunk digests.
|
|
238
|
+
*/
|
|
239
|
+
export declare function buildManifestFromFile(filePath: string, name: string, mimeType: string, chunkSize?: number): SwarmManifest;
|
|
240
|
+
/** sha256 a file with bounded memory. */
|
|
241
|
+
export declare function sha256FileHex(filePath: string): {
|
|
242
|
+
sha256: string;
|
|
243
|
+
size: number;
|
|
244
|
+
};
|
|
245
|
+
/** Stable digest for a paged chunk-hash manifest. */
|
|
246
|
+
export declare function hashChunkHashes(chunkHashes: readonly string[]): string;
|
|
247
|
+
export declare function toManifestSummary(manifest: SwarmManifest): SwarmManifestSummary;
|
|
248
|
+
/** Return a useful reason rather than letting peer-controlled values reach I/O. */
|
|
249
|
+
export declare function validateManifestSummary(manifest: SwarmManifestSummary): string | null;
|
|
250
|
+
export declare function validateSwarmManifest(manifest: SwarmManifest): string | null;
|
|
251
|
+
export declare function isSha256Hex(value: unknown): value is string;
|
|
252
|
+
/**
|
|
253
|
+
* Confirm two peers are describing the same bytes before trading chunks.
|
|
254
|
+
* Without this a peer could advertise a familiar fileId while serving a
|
|
255
|
+
* different file's chunk hashes.
|
|
256
|
+
*/
|
|
257
|
+
export declare function manifestsAgree(a: SwarmManifest, b: SwarmManifest): boolean;
|
|
258
|
+
export declare function sha256Hex(bytes: Uint8Array): string;
|