@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.
Files changed (45) hide show
  1. package/README.md +44 -1
  2. package/exports/browser/client-86Tnax66.js +2224 -0
  3. package/exports/browser/client-BFxJts6j.js +2224 -0
  4. package/exports/browser/client-BzvWzzLv.js +2224 -0
  5. package/exports/browser/client-C9GBz2Tf.js +2224 -0
  6. package/exports/browser/client-CQzuu6At.js +2224 -0
  7. package/exports/browser/client-CVkFejdx.js +2224 -0
  8. package/exports/browser/client-Cj_UUO8W.js +2224 -0
  9. package/exports/browser/client-CrGaeBjj.js +2224 -0
  10. package/exports/browser/client-ERocgKET.js +2224 -0
  11. package/exports/browser/client-a8rws3cu.js +2224 -0
  12. package/exports/browser/messages-BFCmg6Vb.js +212 -0
  13. package/exports/browser/messages-BHIC2mQu.js +212 -0
  14. package/exports/browser/messages-CimzJ4wr.js +210 -0
  15. package/exports/browser/messages-Cjoa_LGO.js +210 -0
  16. package/exports/browser/messages-D7JxkkDW.js +212 -0
  17. package/exports/browser/messages-DVHcNvs_.js +212 -0
  18. package/exports/browser/messages-DqL_g2qw.js +210 -0
  19. package/exports/browser/messages-XgZZUMma.js +212 -0
  20. package/exports/browser/messages-kfFyctPL.js +210 -0
  21. package/exports/browser/messages-psROnfRi.js +212 -0
  22. package/exports/browser/peernet-B9VL4Xwe.js +9522 -0
  23. package/exports/browser/peernet-C-HNdrcd.js +9528 -0
  24. package/exports/browser/peernet-C4rOvyyU.js +9661 -0
  25. package/exports/browser/peernet-CbI0-jFn.js +9651 -0
  26. package/exports/browser/peernet-CtHScDyh.js +9524 -0
  27. package/exports/browser/peernet-DfuxUK3u.js +9525 -0
  28. package/exports/browser/peernet-DiEcNDLD.js +9655 -0
  29. package/exports/browser/peernet-DtuRXOzP.js +9653 -0
  30. package/exports/browser/peernet-rcD5XL1y.js +9590 -0
  31. package/exports/browser/peernet-rw8HJGOz.js +9597 -0
  32. package/exports/browser/peernet.js +1 -1
  33. package/exports/messages-0uXHmQH7.js +209 -0
  34. package/exports/messages-DaUtFfBh.js +207 -0
  35. package/exports/peernet.js +358 -33
  36. package/exports/types/file-transfer.d.ts +34 -0
  37. package/exports/types/peernet.d.ts +20 -4
  38. package/exports/types/proto/file.proto.d.ts +5 -0
  39. package/package.json +2 -2
  40. package/rollup.config.js +2 -6
  41. package/src/file-transfer.ts +194 -0
  42. package/src/peernet.ts +170 -28
  43. package/src/proto/file.proto.js +6 -1
  44. package/test/file-transfer.test.js +81 -0
  45. package/test/peernet.test.js +31 -2
@@ -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-CHKVVTVE.js');
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
- protoInput = { path, content };
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: links || content
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
- const data = await blockStore.has(hash);
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
- let closestPeer = await this.dht.closestPeer(Object.values(providers));
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
- let data;
1055
- const has = await dataStore.has(hash);
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<any>;
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): 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<any>;
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 };
@@ -2,5 +2,10 @@ declare const _default: {
2
2
  path: string;
3
3
  'content?': Uint8Array<ArrayBuffer>;
4
4
  'links?': never[];
5
+ 'size?': number;
6
+ 'chunkSize?': number;
7
+ 'chunked?': boolean;
8
+ 'kind?': string;
9
+ 'blockHash?': string;
5
10
  };
6
11
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/peernet",
3
- "version": "1.2.29",
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'],