@leofcoin/peernet 1.2.29 → 1.2.30
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 +44 -1
- package/exports/browser/client-86Tnax66.js +2224 -0
- package/exports/browser/client-BFxJts6j.js +2224 -0
- package/exports/browser/client-BzvWzzLv.js +2224 -0
- package/exports/browser/client-C9GBz2Tf.js +2224 -0
- package/exports/browser/client-CQzuu6At.js +2224 -0
- package/exports/browser/client-CVkFejdx.js +2224 -0
- package/exports/browser/client-Cj_UUO8W.js +2224 -0
- package/exports/browser/client-CrGaeBjj.js +2224 -0
- package/exports/browser/client-ERocgKET.js +2224 -0
- package/exports/browser/client-a8rws3cu.js +2224 -0
- package/exports/browser/messages-BFCmg6Vb.js +212 -0
- package/exports/browser/messages-BHIC2mQu.js +212 -0
- package/exports/browser/messages-CimzJ4wr.js +210 -0
- package/exports/browser/messages-Cjoa_LGO.js +210 -0
- package/exports/browser/messages-D7JxkkDW.js +212 -0
- package/exports/browser/messages-DVHcNvs_.js +212 -0
- package/exports/browser/messages-DqL_g2qw.js +210 -0
- package/exports/browser/messages-XgZZUMma.js +212 -0
- package/exports/browser/messages-kfFyctPL.js +210 -0
- package/exports/browser/messages-psROnfRi.js +212 -0
- package/exports/browser/peernet-B9VL4Xwe.js +9522 -0
- package/exports/browser/peernet-C-HNdrcd.js +9528 -0
- package/exports/browser/peernet-C4rOvyyU.js +9661 -0
- package/exports/browser/peernet-CbI0-jFn.js +9651 -0
- package/exports/browser/peernet-CtHScDyh.js +9524 -0
- package/exports/browser/peernet-DfuxUK3u.js +9525 -0
- package/exports/browser/peernet-DiEcNDLD.js +9655 -0
- package/exports/browser/peernet-DtuRXOzP.js +9653 -0
- package/exports/browser/peernet-rcD5XL1y.js +9590 -0
- package/exports/browser/peernet-rw8HJGOz.js +9597 -0
- package/exports/browser/peernet.js +1 -1
- package/exports/messages-0uXHmQH7.js +209 -0
- package/exports/messages-DaUtFfBh.js +207 -0
- package/exports/peernet.js +358 -33
- package/exports/types/file-transfer.d.ts +34 -0
- package/exports/types/peernet.d.ts +20 -4
- package/exports/types/proto/file.proto.d.ts +5 -0
- package/package.json +2 -2
- package/rollup.config.js +2 -6
- package/src/file-transfer.ts +194 -0
- package/src/peernet.ts +170 -28
- package/src/proto/file.proto.js +6 -1
- package/test/file-transfer.test.js +81 -0
- package/test/peernet.test.js +31 -2
package/exports/peernet.js
CHANGED
|
@@ -301,6 +301,196 @@ const nothingFoundError = (hash) => {
|
|
|
301
301
|
return new Error(`nothing found for ${hash}`);
|
|
302
302
|
};
|
|
303
303
|
|
|
304
|
+
/** A resumable download. Completed chunks remain cached on the transfer instance. */
|
|
305
|
+
class FileTransfer {
|
|
306
|
+
hash;
|
|
307
|
+
state = 'idle';
|
|
308
|
+
result;
|
|
309
|
+
error;
|
|
310
|
+
#fetch;
|
|
311
|
+
#decode;
|
|
312
|
+
#verify;
|
|
313
|
+
#pin;
|
|
314
|
+
#verifyManifest;
|
|
315
|
+
#concurrency;
|
|
316
|
+
#stopped = false;
|
|
317
|
+
#listeners = new Set();
|
|
318
|
+
#chunks = new Map();
|
|
319
|
+
#resumeWaiters = [];
|
|
320
|
+
#resolve;
|
|
321
|
+
#reject;
|
|
322
|
+
#totalChunks = 0;
|
|
323
|
+
#totalBytes = 0;
|
|
324
|
+
#started = false;
|
|
325
|
+
constructor(options) {
|
|
326
|
+
this.hash = options.hash;
|
|
327
|
+
this.#fetch = options.fetch;
|
|
328
|
+
this.#decode = options.decode;
|
|
329
|
+
this.#verify = options.verify;
|
|
330
|
+
this.#pin = options.pin;
|
|
331
|
+
this.#verifyManifest = options.verifyManifest !== false;
|
|
332
|
+
this.#concurrency = options.concurrency ?? 4;
|
|
333
|
+
if (!Number.isSafeInteger(this.#concurrency) || this.#concurrency <= 0)
|
|
334
|
+
throw new TypeError('concurrency must be a positive integer');
|
|
335
|
+
this.result = new Promise((resolve, reject) => {
|
|
336
|
+
this.#resolve = resolve;
|
|
337
|
+
this.#reject = reject;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
get progress() {
|
|
341
|
+
let transferredBytes = 0;
|
|
342
|
+
for (const chunk of this.#chunks.values())
|
|
343
|
+
transferredBytes += chunk.length;
|
|
344
|
+
return {
|
|
345
|
+
hash: this.hash,
|
|
346
|
+
state: this.state,
|
|
347
|
+
completedChunks: this.#chunks.size,
|
|
348
|
+
totalChunks: this.#totalChunks,
|
|
349
|
+
transferredBytes,
|
|
350
|
+
totalBytes: this.#totalBytes
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
onProgress(listener) {
|
|
354
|
+
this.#listeners.add(listener);
|
|
355
|
+
listener(this.progress);
|
|
356
|
+
return () => this.#listeners.delete(listener);
|
|
357
|
+
}
|
|
358
|
+
start() {
|
|
359
|
+
if (!this.#started) {
|
|
360
|
+
this.#started = true;
|
|
361
|
+
this.state = 'running';
|
|
362
|
+
this.#emit();
|
|
363
|
+
void this.#run();
|
|
364
|
+
}
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
pause() {
|
|
368
|
+
if (this.state === 'running') {
|
|
369
|
+
this.state = 'paused';
|
|
370
|
+
this.#emit();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
resume() {
|
|
374
|
+
if (this.state !== 'paused')
|
|
375
|
+
return;
|
|
376
|
+
this.state = 'running';
|
|
377
|
+
const waiters = this.#resumeWaiters.splice(0);
|
|
378
|
+
for (const resolve of waiters)
|
|
379
|
+
resolve();
|
|
380
|
+
this.#emit();
|
|
381
|
+
}
|
|
382
|
+
cancel() {
|
|
383
|
+
if (this.state === 'completed' || this.state === 'failed' || this.state === 'cancelled')
|
|
384
|
+
return;
|
|
385
|
+
this.state = 'cancelled';
|
|
386
|
+
this.#stopped = true;
|
|
387
|
+
const error = new Error(`Transfer ${this.hash} was cancelled`);
|
|
388
|
+
this.error = error;
|
|
389
|
+
const waiters = this.#resumeWaiters.splice(0);
|
|
390
|
+
for (const resolve of waiters)
|
|
391
|
+
resolve();
|
|
392
|
+
this.#reject(error);
|
|
393
|
+
this.#emit();
|
|
394
|
+
}
|
|
395
|
+
async #waitUntilRunning() {
|
|
396
|
+
if (this.state === 'paused')
|
|
397
|
+
await new Promise((resolve) => this.#resumeWaiters.push(resolve));
|
|
398
|
+
if (this.state === 'cancelled')
|
|
399
|
+
throw this.error;
|
|
400
|
+
}
|
|
401
|
+
async #run() {
|
|
402
|
+
try {
|
|
403
|
+
const manifestData = await this.#fetch(this.hash);
|
|
404
|
+
if (!manifestData)
|
|
405
|
+
throw new Error(`Unable to download ${this.hash}`);
|
|
406
|
+
if (this.#verifyManifest && !(await this.#verify(manifestData, this.hash)))
|
|
407
|
+
throw new Error(`Manifest hash mismatch for ${this.hash}`);
|
|
408
|
+
const manifest = await this.#decode(manifestData);
|
|
409
|
+
if (this.#pin)
|
|
410
|
+
await this.#pin(this.hash, manifestData);
|
|
411
|
+
if (!manifest.chunked) {
|
|
412
|
+
if (manifest.links?.length)
|
|
413
|
+
throw new Error(`${this.hash} is a directory`);
|
|
414
|
+
const content = manifest.content || new Uint8Array();
|
|
415
|
+
this.#totalChunks = 1;
|
|
416
|
+
this.#totalBytes = content.length;
|
|
417
|
+
this.#chunks.set(0, content);
|
|
418
|
+
this.state = 'completed';
|
|
419
|
+
this.#resolve(content);
|
|
420
|
+
this.#emit();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const links = manifest.links || [];
|
|
424
|
+
this.#totalChunks = links.length;
|
|
425
|
+
this.#totalBytes =
|
|
426
|
+
Number(manifest.size) || links.reduce((sum, link) => sum + (Number(link.size) || 0), 0);
|
|
427
|
+
this.#emit();
|
|
428
|
+
let nextIndex = 0;
|
|
429
|
+
const worker = async () => {
|
|
430
|
+
while (!this.#stopped) {
|
|
431
|
+
await this.#waitUntilRunning();
|
|
432
|
+
const index = nextIndex++;
|
|
433
|
+
if (index >= links.length)
|
|
434
|
+
return;
|
|
435
|
+
if (this.#chunks.has(index))
|
|
436
|
+
continue;
|
|
437
|
+
const encoded = await this.#fetch(links[index].hash, index);
|
|
438
|
+
if (this.#stopped)
|
|
439
|
+
return;
|
|
440
|
+
if (!encoded)
|
|
441
|
+
throw new Error(`Missing chunk ${index + 1}/${links.length}`);
|
|
442
|
+
if (!(await this.#verify(encoded, links[index].hash)))
|
|
443
|
+
throw new Error(`Hash mismatch for chunk ${index + 1}`);
|
|
444
|
+
const chunkNode = await this.#decode(encoded);
|
|
445
|
+
const content = chunkNode.content || new Uint8Array();
|
|
446
|
+
this.#chunks.set(index, content);
|
|
447
|
+
if (this.#pin)
|
|
448
|
+
await this.#pin(links[index].hash, encoded);
|
|
449
|
+
this.#emit();
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
await Promise.all(Array.from({ length: Math.min(this.#concurrency, links.length) }, () => worker()));
|
|
453
|
+
const output = new Uint8Array(this.#totalBytes);
|
|
454
|
+
let offset = 0;
|
|
455
|
+
for (let index = 0; index < this.#totalChunks; index++) {
|
|
456
|
+
const chunk = this.#chunks.get(index);
|
|
457
|
+
if (!chunk)
|
|
458
|
+
throw new Error(`Missing downloaded chunk ${index + 1}`);
|
|
459
|
+
output.set(chunk, offset);
|
|
460
|
+
offset += chunk.length;
|
|
461
|
+
}
|
|
462
|
+
this.state = 'completed';
|
|
463
|
+
this.#resolve(offset === output.length ? output : output.slice(0, offset));
|
|
464
|
+
this.#emit();
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (this.state === 'cancelled')
|
|
468
|
+
return;
|
|
469
|
+
this.state = 'failed';
|
|
470
|
+
this.#stopped = true;
|
|
471
|
+
this.error = error;
|
|
472
|
+
this.#reject(error);
|
|
473
|
+
this.#emit();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
#emit() {
|
|
477
|
+
const progress = this.progress;
|
|
478
|
+
for (const listener of this.#listeners)
|
|
479
|
+
listener(progress);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
484
|
+
const DEFAULT_BLOCK_CHUNK_SIZE = 256 * 1024;
|
|
485
|
+
const DEFAULT_BLOCK_CHUNK_THRESHOLD = 1024 * 1024;
|
|
486
|
+
const DEFAULT_TRANSFER_CONCURRENCY = 4;
|
|
487
|
+
const adaptiveChunkSize = (size, minimum = DEFAULT_CHUNK_SIZE) => {
|
|
488
|
+
if (size >= 256 * 1024 * 1024)
|
|
489
|
+
return Math.max(minimum, 4 * 1024 * 1024);
|
|
490
|
+
if (size >= 32 * 1024 * 1024)
|
|
491
|
+
return Math.max(minimum, 1024 * 1024);
|
|
492
|
+
return minimum;
|
|
493
|
+
};
|
|
304
494
|
globalThis.LeofcoinStorage = Storage;
|
|
305
495
|
globalThis.leofcoin = globalThis.leofcoin || {};
|
|
306
496
|
globalThis.pubsub = globalThis.pubsub || new PubSub();
|
|
@@ -338,6 +528,9 @@ class Peernet {
|
|
|
338
528
|
_peerHandler;
|
|
339
529
|
protos;
|
|
340
530
|
version;
|
|
531
|
+
blockChunkSize;
|
|
532
|
+
blockChunkThreshold;
|
|
533
|
+
transferConcurrency;
|
|
341
534
|
#peerAttempts = {};
|
|
342
535
|
_inMemoryBroadcasts;
|
|
343
536
|
/**
|
|
@@ -364,6 +557,15 @@ class Peernet {
|
|
|
364
557
|
this.stars = options.stars;
|
|
365
558
|
this.transport = options.transport;
|
|
366
559
|
this.version = options.version;
|
|
560
|
+
this.blockChunkSize = options.blockChunkSize ?? DEFAULT_BLOCK_CHUNK_SIZE;
|
|
561
|
+
this.blockChunkThreshold = options.blockChunkThreshold ?? DEFAULT_BLOCK_CHUNK_THRESHOLD;
|
|
562
|
+
this.transferConcurrency = options.transferConcurrency ?? DEFAULT_TRANSFER_CONCURRENCY;
|
|
563
|
+
if (!Number.isSafeInteger(this.blockChunkSize) || this.blockChunkSize <= 0)
|
|
564
|
+
throw new TypeError('blockChunkSize must be a positive integer');
|
|
565
|
+
if (!Number.isSafeInteger(this.blockChunkThreshold) || this.blockChunkThreshold < 0)
|
|
566
|
+
throw new TypeError('blockChunkThreshold must be a non-negative integer');
|
|
567
|
+
if (!Number.isSafeInteger(this.transferConcurrency) || this.transferConcurrency <= 0)
|
|
568
|
+
throw new TypeError('transferConcurrency must be a positive integer');
|
|
367
569
|
const parts = this.network.split(':');
|
|
368
570
|
this.networkVersion = options.networkVersion || parts.length > 1 ? parts[1] : 'mainnet';
|
|
369
571
|
if (!options.storePrefix)
|
|
@@ -456,7 +658,7 @@ class Peernet {
|
|
|
456
658
|
await getAddress();
|
|
457
659
|
this.storePrefix = options.storePrefix;
|
|
458
660
|
this.root = options.root;
|
|
459
|
-
const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-
|
|
661
|
+
const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-0uXHmQH7.js');
|
|
460
662
|
/**
|
|
461
663
|
* proto Object containing protos
|
|
462
664
|
* @type {Object}
|
|
@@ -592,12 +794,34 @@ class Peernet {
|
|
|
592
794
|
|
|
593
795
|
* @returns {Promise<string>} The hash that can be shared for direct download
|
|
594
796
|
*/
|
|
595
|
-
async broadcast(path, { content, links }) {
|
|
797
|
+
async broadcast(path, { content, links, chunkSize }) {
|
|
798
|
+
chunkSize = chunkSize ?? (content ? adaptiveChunkSize(content.length) : DEFAULT_CHUNK_SIZE);
|
|
799
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize <= 0)
|
|
800
|
+
throw new TypeError('chunkSize must be a positive integer');
|
|
596
801
|
let protoInput;
|
|
597
|
-
if (content)
|
|
598
|
-
|
|
802
|
+
if (content && content.length > chunkSize) {
|
|
803
|
+
const chunkLinks = [];
|
|
804
|
+
for (let offset = 0, index = 0; offset < content.length; offset += chunkSize, index++) {
|
|
805
|
+
const chunkNode = await new globalThis.peernet.protos['peernet-file']({
|
|
806
|
+
path: `${path}.part-${index}`,
|
|
807
|
+
content: content.slice(offset, Math.min(offset + chunkSize, content.length))
|
|
808
|
+
});
|
|
809
|
+
const chunkHash = await chunkNode.hash();
|
|
810
|
+
const chunkEncoded = await chunkNode.encoded;
|
|
811
|
+
if (!this._inMemoryBroadcasts)
|
|
812
|
+
this._inMemoryBroadcasts = new Map();
|
|
813
|
+
this._inMemoryBroadcasts.set(chunkHash, chunkEncoded);
|
|
814
|
+
await shareStore.put(chunkHash, chunkEncoded);
|
|
815
|
+
chunkLinks.push({ hash: chunkHash, path: String(index), size: chunkNode.decoded.content.length });
|
|
816
|
+
}
|
|
817
|
+
protoInput = { path, links: chunkLinks, size: content.length, chunkSize, chunked: true };
|
|
818
|
+
}
|
|
819
|
+
else if (content)
|
|
820
|
+
protoInput = { path, content, size: content.length };
|
|
599
821
|
else if (links)
|
|
600
822
|
protoInput = { path, links };
|
|
823
|
+
else
|
|
824
|
+
throw new TypeError('broadcast requires content or links');
|
|
601
825
|
const protoNode = await new globalThis.peernet.protos['peernet-file'](protoInput);
|
|
602
826
|
const hash = await protoNode.hash();
|
|
603
827
|
const encoded = await protoNode.encoded;
|
|
@@ -609,6 +833,46 @@ class Peernet {
|
|
|
609
833
|
await this.publish('broadcast', { hash, from: this.id });
|
|
610
834
|
return hash;
|
|
611
835
|
}
|
|
836
|
+
/** Create and immediately start a resumable, integrity-checked file download. */
|
|
837
|
+
download(hash, options = {}) {
|
|
838
|
+
const FileProto = globalThis.peernet.protos['peernet-file'];
|
|
839
|
+
const transfer = new FileTransfer({
|
|
840
|
+
hash,
|
|
841
|
+
fetch: async (wantedHash, index) => {
|
|
842
|
+
if (wantedHash !== hash) {
|
|
843
|
+
const providers = Object.values(this.dht.providersFor(hash) || {});
|
|
844
|
+
for (const provider of providers)
|
|
845
|
+
this.dht.addProvider(provider, wantedHash);
|
|
846
|
+
}
|
|
847
|
+
const store = await this.whichStore([...this.stores], wantedHash);
|
|
848
|
+
if (store && (await store.has(wantedHash)))
|
|
849
|
+
return store.get(wantedHash);
|
|
850
|
+
const result = await this.requestData(wantedHash, undefined, { providerIndex: index });
|
|
851
|
+
return result;
|
|
852
|
+
},
|
|
853
|
+
decode: async (encoded) => {
|
|
854
|
+
const node = await new FileProto(encoded);
|
|
855
|
+
await node.decode();
|
|
856
|
+
if (node.decoded?.chunked) {
|
|
857
|
+
const providers = Object.values(this.dht.providersFor(hash) || {});
|
|
858
|
+
for (const link of node.decoded.links || []) {
|
|
859
|
+
for (const provider of providers)
|
|
860
|
+
this.dht.addProvider(provider, link.hash);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return node.decoded;
|
|
864
|
+
},
|
|
865
|
+
concurrency: this.transferConcurrency,
|
|
866
|
+
verify: async (encoded, expectedHash) => {
|
|
867
|
+
const node = await new FileProto(encoded);
|
|
868
|
+
return (await node.hash()) === expectedHash;
|
|
869
|
+
},
|
|
870
|
+
pin: options.pin ? (chunkHash, encoded) => dataStore.put(chunkHash, encoded) : undefined
|
|
871
|
+
});
|
|
872
|
+
if (options.autoStart !== false)
|
|
873
|
+
transfer.start();
|
|
874
|
+
return transfer;
|
|
875
|
+
}
|
|
612
876
|
async handleData(peer, id, proto) {
|
|
613
877
|
let { hash, store } = proto.decoded;
|
|
614
878
|
let data;
|
|
@@ -619,15 +883,9 @@ class Peernet {
|
|
|
619
883
|
if (typeof hash === 'function') {
|
|
620
884
|
resolvedHash = await hash();
|
|
621
885
|
}
|
|
622
|
-
// Decode the stored proto to extract the content or links
|
|
623
|
-
const FileProto = globalThis.peernet.protos['peernet-file'];
|
|
624
|
-
const fileProto = await new FileProto(data);
|
|
625
|
-
await fileProto.decode();
|
|
626
|
-
const { content, links } = fileProto.decoded;
|
|
627
|
-
console.log(links);
|
|
628
886
|
data = await new globalThis.peernet.protos['peernet-data-response']({
|
|
629
887
|
hash: resolvedHash,
|
|
630
|
-
data
|
|
888
|
+
data
|
|
631
889
|
});
|
|
632
890
|
const node = await this.prepareMessage(data);
|
|
633
891
|
await this.sendMessage(peer, id, node.encoded);
|
|
@@ -783,18 +1041,92 @@ class Peernet {
|
|
|
783
1041
|
get block() {
|
|
784
1042
|
return {
|
|
785
1043
|
get: async (hash) => {
|
|
786
|
-
|
|
787
|
-
if (data)
|
|
788
|
-
return blockStore.get(hash);
|
|
789
|
-
return this.requestData(hash, 'block');
|
|
1044
|
+
return this.#createBlockTransfer(hash).result;
|
|
790
1045
|
},
|
|
791
1046
|
put: async (hash, data) => {
|
|
792
1047
|
if (await blockStore.has(hash))
|
|
793
1048
|
return;
|
|
1049
|
+
if (data.length > this.blockChunkThreshold)
|
|
1050
|
+
return this.#putChunkedBlock(hash, data);
|
|
794
1051
|
return await blockStore.put(hash, data);
|
|
795
1052
|
},
|
|
796
|
-
has: async (hash) => await blockStore.has(hash)
|
|
1053
|
+
has: async (hash) => await blockStore.has(hash),
|
|
1054
|
+
download: (hash, options = {}) => this.#createBlockTransfer(hash, options)
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
async #putChunkedBlock(hash, data) {
|
|
1058
|
+
const links = [];
|
|
1059
|
+
const FileProto = globalThis.peernet.protos['peernet-file'];
|
|
1060
|
+
const chunkSize = adaptiveChunkSize(data.length, this.blockChunkSize);
|
|
1061
|
+
for (let offset = 0, index = 0; offset < data.length; offset += chunkSize, index++) {
|
|
1062
|
+
const content = data.slice(offset, Math.min(offset + chunkSize, data.length));
|
|
1063
|
+
const chunk = new FileProto({ path: `block-${hash}.part-${index}`, content });
|
|
1064
|
+
const chunkHash = await chunk.hash();
|
|
1065
|
+
await blockStore.put(chunkHash, chunk.encoded);
|
|
1066
|
+
links.push({ hash: chunkHash, path: String(index).padStart(12, '0'), size: content.length });
|
|
1067
|
+
}
|
|
1068
|
+
const manifest = new FileProto({
|
|
1069
|
+
path: `block-${hash}`,
|
|
1070
|
+
links,
|
|
1071
|
+
size: data.length,
|
|
1072
|
+
chunkSize,
|
|
1073
|
+
chunked: true,
|
|
1074
|
+
kind: 'block',
|
|
1075
|
+
blockHash: hash
|
|
1076
|
+
});
|
|
1077
|
+
return blockStore.put(hash, manifest.encoded);
|
|
1078
|
+
}
|
|
1079
|
+
#createBlockTransfer(hash, options = {}) {
|
|
1080
|
+
const FileProto = globalThis.peernet.protos['peernet-file'];
|
|
1081
|
+
const fetch = async (wantedHash, index) => {
|
|
1082
|
+
if (wantedHash !== hash) {
|
|
1083
|
+
const providers = Object.values(this.dht.providersFor(hash) || {});
|
|
1084
|
+
for (const provider of providers)
|
|
1085
|
+
this.dht.addProvider(provider, wantedHash);
|
|
1086
|
+
}
|
|
1087
|
+
if (await blockStore.has(wantedHash))
|
|
1088
|
+
return blockStore.get(wantedHash);
|
|
1089
|
+
const result = await this.requestData(wantedHash, 'block', { providerIndex: index });
|
|
1090
|
+
return result;
|
|
797
1091
|
};
|
|
1092
|
+
const transfer = new FileTransfer({
|
|
1093
|
+
hash,
|
|
1094
|
+
fetch,
|
|
1095
|
+
decode: async (encoded) => {
|
|
1096
|
+
let node;
|
|
1097
|
+
try {
|
|
1098
|
+
node = new FileProto(encoded);
|
|
1099
|
+
await node.decode();
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
// A normal block is intentionally not a peernet-file envelope.
|
|
1103
|
+
return { content: encoded };
|
|
1104
|
+
}
|
|
1105
|
+
if (node.decoded?.kind === 'block') {
|
|
1106
|
+
if (node.decoded.blockHash !== hash)
|
|
1107
|
+
throw new Error(`Block manifest hash mismatch for ${hash}`);
|
|
1108
|
+
const providers = Object.values(this.dht.providersFor(hash) || {});
|
|
1109
|
+
for (const link of node.decoded.links || []) {
|
|
1110
|
+
for (const provider of providers)
|
|
1111
|
+
this.dht.addProvider(provider, link.hash);
|
|
1112
|
+
}
|
|
1113
|
+
return node.decoded;
|
|
1114
|
+
}
|
|
1115
|
+
if (node.decoded?.path?.startsWith(`block-${hash}.part-`))
|
|
1116
|
+
return node.decoded;
|
|
1117
|
+
return { content: encoded };
|
|
1118
|
+
},
|
|
1119
|
+
verify: async (encoded, expectedHash) => {
|
|
1120
|
+
const node = new FileProto(encoded);
|
|
1121
|
+
return (await node.hash()) === expectedHash;
|
|
1122
|
+
},
|
|
1123
|
+
verifyManifest: false,
|
|
1124
|
+
concurrency: this.transferConcurrency,
|
|
1125
|
+
pin: (wantedHash, encoded) => blockStore.put(wantedHash, encoded)
|
|
1126
|
+
});
|
|
1127
|
+
if (options.autoStart !== false)
|
|
1128
|
+
transfer.start();
|
|
1129
|
+
return transfer;
|
|
798
1130
|
}
|
|
799
1131
|
get transaction() {
|
|
800
1132
|
return {
|
|
@@ -812,14 +1144,17 @@ class Peernet {
|
|
|
812
1144
|
has: async (hash) => await transactionStore.has(hash)
|
|
813
1145
|
};
|
|
814
1146
|
}
|
|
815
|
-
async requestData(hash, store) {
|
|
1147
|
+
async requestData(hash, store, options = {}) {
|
|
816
1148
|
try {
|
|
817
1149
|
const providers = await this.providersFor(hash);
|
|
818
1150
|
if (!providers || (providers && Object.keys(providers).length === 0))
|
|
819
1151
|
throw nothingFoundError(hash);
|
|
820
1152
|
debug(`found ${Object.keys(providers).length} provider(s) for ${hash}`);
|
|
821
1153
|
// get closest peer on earth
|
|
822
|
-
|
|
1154
|
+
const providerValues = Object.values(providers);
|
|
1155
|
+
let closestPeer = options.providerIndex === undefined
|
|
1156
|
+
? await this.dht.closestPeer(providerValues)
|
|
1157
|
+
: providerValues[options.providerIndex % providerValues.length];
|
|
823
1158
|
// fallback to first provider if no closest peer found
|
|
824
1159
|
if (!closestPeer || !closestPeer.id)
|
|
825
1160
|
closestPeer = Object.values(providers)[0];
|
|
@@ -831,7 +1166,7 @@ class Peernet {
|
|
|
831
1166
|
const peer = this.connections[id];
|
|
832
1167
|
if (!peer || !peer?.connected) {
|
|
833
1168
|
this.dht.removeProvider(id, hash);
|
|
834
|
-
return this.requestData(hash, store?.name || store);
|
|
1169
|
+
return this.requestData(hash, store?.name || store, options);
|
|
835
1170
|
}
|
|
836
1171
|
let data = await new globalThis.peernet.protos['peernet-data']({
|
|
837
1172
|
hash,
|
|
@@ -874,7 +1209,7 @@ class Peernet {
|
|
|
874
1209
|
if (this.#peerAttempts[id] === undefined)
|
|
875
1210
|
this.#peerAttempts[id] = 0;
|
|
876
1211
|
this.#peerAttempts[id]++;
|
|
877
|
-
return this.requestData(hash, store?.name || store);
|
|
1212
|
+
return this.requestData(hash, store?.name || store, options);
|
|
878
1213
|
}
|
|
879
1214
|
// this.put(hash, proto.decoded.data)
|
|
880
1215
|
}
|
|
@@ -1051,18 +1386,8 @@ class Peernet {
|
|
|
1051
1386
|
return paths;
|
|
1052
1387
|
}
|
|
1053
1388
|
async cat(hash, options) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
data = has ? await dataStore.get(hash) : await this.requestData(hash, 'data');
|
|
1057
|
-
if (!data)
|
|
1058
|
-
throw nothingFoundError(hash);
|
|
1059
|
-
const node = await new globalThis.peernet.protos['peernet-file'](data);
|
|
1060
|
-
await node.decode();
|
|
1061
|
-
if (node.decoded?.links.length > 0)
|
|
1062
|
-
throw new Error(`${hash} is a directory`);
|
|
1063
|
-
if (options?.pin)
|
|
1064
|
-
await dataStore.put(hash, node.encoded);
|
|
1065
|
-
return node.decoded.content;
|
|
1389
|
+
const transfer = this.download(hash, { pin: options?.pin });
|
|
1390
|
+
return transfer.result;
|
|
1066
1391
|
}
|
|
1067
1392
|
/**
|
|
1068
1393
|
* goes trough given stores and tries to find data for given hash
|
|
@@ -1174,4 +1499,4 @@ class Peernet {
|
|
|
1174
1499
|
}
|
|
1175
1500
|
globalThis.Peernet = Peernet;
|
|
1176
1501
|
|
|
1177
|
-
export { Peernet as default };
|
|
1502
|
+
export { FileTransfer, Peernet as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type FileTransferState = 'idle' | 'running' | 'paused' | 'completed' | 'cancelled' | 'failed';
|
|
2
|
+
export type FileTransferProgress = {
|
|
3
|
+
hash: string;
|
|
4
|
+
state: FileTransferState;
|
|
5
|
+
completedChunks: number;
|
|
6
|
+
totalChunks: number;
|
|
7
|
+
transferredBytes: number;
|
|
8
|
+
totalBytes: number;
|
|
9
|
+
};
|
|
10
|
+
type ProgressListener = (progress: FileTransferProgress) => void;
|
|
11
|
+
/** A resumable download. Completed chunks remain cached on the transfer instance. */
|
|
12
|
+
export default class FileTransfer {
|
|
13
|
+
#private;
|
|
14
|
+
readonly hash: string;
|
|
15
|
+
state: FileTransferState;
|
|
16
|
+
result: Promise<Uint8Array>;
|
|
17
|
+
error?: unknown;
|
|
18
|
+
constructor(options: {
|
|
19
|
+
hash: string;
|
|
20
|
+
fetch: (hash: string, index?: number) => Promise<Uint8Array | undefined>;
|
|
21
|
+
decode: (data: Uint8Array) => Promise<any>;
|
|
22
|
+
verify: (data: Uint8Array, hash: string) => Promise<boolean>;
|
|
23
|
+
pin?: (hash: string, data: Uint8Array) => Promise<any>;
|
|
24
|
+
verifyManifest?: boolean;
|
|
25
|
+
concurrency?: number;
|
|
26
|
+
});
|
|
27
|
+
get progress(): FileTransferProgress;
|
|
28
|
+
onProgress(listener: ProgressListener): () => void;
|
|
29
|
+
start(): this;
|
|
30
|
+
pause(): void;
|
|
31
|
+
resume(): void;
|
|
32
|
+
cancel(): void;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -5,6 +5,7 @@ import MessageHandler from './handlers/message.js';
|
|
|
5
5
|
import { Storage as LeofcoinStorageClass } from '@leofcoin/storage';
|
|
6
6
|
import Identity from './identity.js';
|
|
7
7
|
import swarm from '@netpeer/swarm/client';
|
|
8
|
+
import FileTransfer from './file-transfer.js';
|
|
8
9
|
declare global {
|
|
9
10
|
var LeofcoinStorage: typeof LeofcoinStorageClass;
|
|
10
11
|
var peernet: Peernet;
|
|
@@ -52,6 +53,9 @@ export default class Peernet {
|
|
|
52
53
|
_peerHandler: PeerDiscovery;
|
|
53
54
|
protos: {};
|
|
54
55
|
version: any;
|
|
56
|
+
blockChunkSize: number;
|
|
57
|
+
blockChunkThreshold: number;
|
|
58
|
+
transferConcurrency: number;
|
|
55
59
|
private _inMemoryBroadcasts;
|
|
56
60
|
/**
|
|
57
61
|
* @access public
|
|
@@ -161,10 +165,16 @@ export default class Peernet {
|
|
|
161
165
|
|
|
162
166
|
* @returns {Promise<string>} The hash that can be shared for direct download
|
|
163
167
|
*/
|
|
164
|
-
broadcast(path: string, { content, links }: {
|
|
168
|
+
broadcast(path: string, { content, links, chunkSize }: {
|
|
165
169
|
content?: Uint8Array;
|
|
166
170
|
links?: any[];
|
|
171
|
+
chunkSize?: number;
|
|
167
172
|
}): Promise<string>;
|
|
173
|
+
/** Create and immediately start a resumable, integrity-checked file download. */
|
|
174
|
+
download(hash: string, options?: {
|
|
175
|
+
pin?: boolean;
|
|
176
|
+
autoStart?: boolean;
|
|
177
|
+
}): FileTransfer;
|
|
168
178
|
handleData(peer: any, id: any, proto: any): Promise<any>;
|
|
169
179
|
handleRequest(peer: any, id: any, proto: any): Promise<void>;
|
|
170
180
|
/**
|
|
@@ -195,16 +205,21 @@ export default class Peernet {
|
|
|
195
205
|
*/
|
|
196
206
|
providersFor(hash: string, store?: undefined): Promise<import("./dht/dht.js").DHTProviderMapValue>;
|
|
197
207
|
get block(): {
|
|
198
|
-
get: (hash: string) => Promise<
|
|
208
|
+
get: (hash: string) => Promise<Uint8Array<ArrayBufferLike>>;
|
|
199
209
|
put: (hash: string, data: Uint8Array) => Promise<unknown>;
|
|
200
210
|
has: (hash: string) => Promise<boolean | any[]>;
|
|
211
|
+
download: (hash: string, options?: {
|
|
212
|
+
autoStart?: boolean;
|
|
213
|
+
}) => FileTransfer;
|
|
201
214
|
};
|
|
202
215
|
get transaction(): {
|
|
203
216
|
get: (hash: string) => Promise<any>;
|
|
204
217
|
put: (hash: string, data: Uint8Array) => Promise<unknown>;
|
|
205
218
|
has: (hash: string) => Promise<boolean | any[]>;
|
|
206
219
|
};
|
|
207
|
-
requestData(hash: any, store: any
|
|
220
|
+
requestData(hash: any, store: any, options?: {
|
|
221
|
+
providerIndex?: number;
|
|
222
|
+
}): any;
|
|
208
223
|
get message(): {
|
|
209
224
|
/**
|
|
210
225
|
* Get content for given message hash
|
|
@@ -304,7 +319,7 @@ export default class Peernet {
|
|
|
304
319
|
path: any;
|
|
305
320
|
hash: any;
|
|
306
321
|
}[]>;
|
|
307
|
-
cat(hash: any, options: any): Promise<
|
|
322
|
+
cat(hash: any, options: any): Promise<Uint8Array<ArrayBufferLike>>;
|
|
308
323
|
/**
|
|
309
324
|
* goes trough given stores and tries to find data for given hash
|
|
310
325
|
* @param {Array} stores
|
|
@@ -346,3 +361,4 @@ export default class Peernet {
|
|
|
346
361
|
removePeer(peer: any): Promise<void>;
|
|
347
362
|
get Buffer(): BufferConstructor;
|
|
348
363
|
}
|
|
364
|
+
export { FileTransfer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/peernet",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.30",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"scripts": {
|
|
97
97
|
"build": "rollup -c",
|
|
98
98
|
"watch": "rollup -c -w",
|
|
99
|
-
"test": "node --test test/peernet.test.js",
|
|
99
|
+
"test": "node --test test/peernet.test.js test/file-transfer.test.js",
|
|
100
100
|
"server": "discovery-swarm-webrtc --port=4000",
|
|
101
101
|
"demo": "jsproject --serve ./ --port 6868"
|
|
102
102
|
},
|
package/rollup.config.js
CHANGED
|
@@ -28,9 +28,7 @@ const walk = async (dir) => {
|
|
|
28
28
|
const isHashedChunk = (runtimePath) => /-[A-Za-z0-9_-]{8,}\.js$/.test(runtimePath)
|
|
29
29
|
|
|
30
30
|
const isPromptImport = (id) =>
|
|
31
|
-
id === './prompts/password.js' ||
|
|
32
|
-
id === './src/prompts/password.js' ||
|
|
33
|
-
id.endsWith('/src/prompts/password.js')
|
|
31
|
+
id === './prompts/password.js' || id === './src/prompts/password.js' || id.endsWith('/src/prompts/password.js')
|
|
34
32
|
|
|
35
33
|
const runtimeFirstExports = ({ exportsDir = 'exports', declarationsDir = 'exports/types' } = {}) => ({
|
|
36
34
|
name: 'runtime-first-exports',
|
|
@@ -125,9 +123,7 @@ export default [
|
|
|
125
123
|
})
|
|
126
124
|
],
|
|
127
125
|
external: (id) =>
|
|
128
|
-
isPromptImport(id) ||
|
|
129
|
-
id === './prompts/password/browser.js' ||
|
|
130
|
-
id === './prompts/password/node.js'
|
|
126
|
+
isPromptImport(id) || id === './prompts/password/browser.js' || id === './prompts/password/node.js'
|
|
131
127
|
},
|
|
132
128
|
{
|
|
133
129
|
input: ['./src/prompts/password/browser.js'],
|