@dropgate/core 2.0.0-beta.2 → 2.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/README.md +76 -15
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +385 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +182 -120
- package/dist/index.d.ts +182 -120
- package/dist/index.js +383 -138
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +268 -62
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +154 -92
- package/dist/p2p/index.d.ts +154 -92
- package/dist/p2p/index.js +267 -62
- package/dist/p2p/index.js.map +1 -1
- package/package.json +88 -88
package/dist/index.cjs
CHANGED
|
@@ -49,6 +49,7 @@ __export(index_exports, {
|
|
|
49
49
|
getDefaultBase64: () => getDefaultBase64,
|
|
50
50
|
getDefaultCrypto: () => getDefaultCrypto,
|
|
51
51
|
getDefaultFetch: () => getDefaultFetch,
|
|
52
|
+
getServerInfo: () => getServerInfo,
|
|
52
53
|
importKeyFromBase64: () => importKeyFromBase64,
|
|
53
54
|
isLocalhostHostname: () => isLocalhostHostname,
|
|
54
55
|
isP2PCodeLike: () => isP2PCodeLike,
|
|
@@ -57,6 +58,7 @@ __export(index_exports, {
|
|
|
57
58
|
makeAbortSignal: () => makeAbortSignal,
|
|
58
59
|
parseSemverMajorMinor: () => parseSemverMajorMinor,
|
|
59
60
|
parseServerUrl: () => parseServerUrl,
|
|
61
|
+
resolvePeerConfig: () => resolvePeerConfig,
|
|
60
62
|
sha256Hex: () => sha256Hex,
|
|
61
63
|
sleep: () => sleep,
|
|
62
64
|
startP2PReceive: () => startP2PReceive,
|
|
@@ -376,6 +378,37 @@ function estimateTotalUploadSizeBytes(fileSizeBytes, totalChunks, isEncrypted) {
|
|
|
376
378
|
if (!isEncrypted) return base;
|
|
377
379
|
return base + (Number(totalChunks) || 0) * ENCRYPTION_OVERHEAD_PER_CHUNK;
|
|
378
380
|
}
|
|
381
|
+
async function getServerInfo(opts) {
|
|
382
|
+
const { host, port, secure, timeoutMs = 5e3, signal, fetchFn: customFetch } = opts;
|
|
383
|
+
const fetchFn = customFetch || getDefaultFetch();
|
|
384
|
+
if (!fetchFn) {
|
|
385
|
+
throw new DropgateValidationError("No fetch() implementation found.");
|
|
386
|
+
}
|
|
387
|
+
const baseUrl = buildBaseUrl({ host, port, secure });
|
|
388
|
+
try {
|
|
389
|
+
const { res, json } = await fetchJson(
|
|
390
|
+
fetchFn,
|
|
391
|
+
`${baseUrl}/api/info`,
|
|
392
|
+
{
|
|
393
|
+
method: "GET",
|
|
394
|
+
timeoutMs,
|
|
395
|
+
signal,
|
|
396
|
+
headers: { Accept: "application/json" }
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
if (res.ok && json && typeof json === "object" && "version" in json) {
|
|
400
|
+
return { baseUrl, serverInfo: json };
|
|
401
|
+
}
|
|
402
|
+
throw new DropgateProtocolError(
|
|
403
|
+
`Server info request failed (status ${res.status}).`
|
|
404
|
+
);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (err instanceof DropgateError) throw err;
|
|
407
|
+
throw new DropgateNetworkError("Could not reach server /api/info.", {
|
|
408
|
+
cause: err
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
379
412
|
var DropgateClient = class {
|
|
380
413
|
/**
|
|
381
414
|
* Create a new DropgateClient instance.
|
|
@@ -403,40 +436,6 @@ var DropgateClient = class {
|
|
|
403
436
|
this.base64 = opts.base64 || getDefaultBase64();
|
|
404
437
|
this.logger = opts.logger || null;
|
|
405
438
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Fetch server information from the /api/info endpoint.
|
|
408
|
-
* @param opts - Server target and request options.
|
|
409
|
-
* @returns The server base URL and server info object.
|
|
410
|
-
* @throws {DropgateNetworkError} If the server cannot be reached.
|
|
411
|
-
* @throws {DropgateProtocolError} If the server returns an invalid response.
|
|
412
|
-
*/
|
|
413
|
-
async getServerInfo(opts) {
|
|
414
|
-
const { host, port, secure, timeoutMs = 5e3, signal } = opts;
|
|
415
|
-
const baseUrl = buildBaseUrl({ host, port, secure });
|
|
416
|
-
try {
|
|
417
|
-
const { res, json } = await fetchJson(
|
|
418
|
-
this.fetchFn,
|
|
419
|
-
`${baseUrl}/api/info`,
|
|
420
|
-
{
|
|
421
|
-
method: "GET",
|
|
422
|
-
timeoutMs,
|
|
423
|
-
signal,
|
|
424
|
-
headers: { Accept: "application/json" }
|
|
425
|
-
}
|
|
426
|
-
);
|
|
427
|
-
if (res.ok && json && typeof json === "object" && "version" in json) {
|
|
428
|
-
return { baseUrl, serverInfo: json };
|
|
429
|
-
}
|
|
430
|
-
throw new DropgateProtocolError(
|
|
431
|
-
`Server info request failed (status ${res.status}).`
|
|
432
|
-
);
|
|
433
|
-
} catch (err) {
|
|
434
|
-
if (err instanceof DropgateError) throw err;
|
|
435
|
-
throw new DropgateNetworkError("Could not reach server /api/info.", {
|
|
436
|
-
cause: err
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
439
|
/**
|
|
441
440
|
* Resolve a user-entered sharing code or URL via the server.
|
|
442
441
|
* @param value - The sharing code or URL to resolve.
|
|
@@ -445,8 +444,12 @@ var DropgateClient = class {
|
|
|
445
444
|
* @throws {DropgateProtocolError} If the share lookup fails.
|
|
446
445
|
*/
|
|
447
446
|
async resolveShareTarget(value, opts) {
|
|
448
|
-
const {
|
|
449
|
-
const
|
|
447
|
+
const { timeoutMs = 5e3, signal } = opts;
|
|
448
|
+
const compat = await this.checkCompatibility(opts);
|
|
449
|
+
if (!compat.compatible) {
|
|
450
|
+
throw new DropgateValidationError(compat.message);
|
|
451
|
+
}
|
|
452
|
+
const { baseUrl } = compat;
|
|
450
453
|
const { res, json } = await fetchJson(
|
|
451
454
|
this.fetchFn,
|
|
452
455
|
`${baseUrl}/api/resolve`,
|
|
@@ -469,10 +472,25 @@ var DropgateClient = class {
|
|
|
469
472
|
}
|
|
470
473
|
/**
|
|
471
474
|
* Check version compatibility between this client and a server.
|
|
472
|
-
*
|
|
473
|
-
* @
|
|
475
|
+
* Fetches server info internally using getServerInfo.
|
|
476
|
+
* @param opts - Server target and request options.
|
|
477
|
+
* @returns Compatibility result with status, message, and server info.
|
|
478
|
+
* @throws {DropgateNetworkError} If the server cannot be reached.
|
|
479
|
+
* @throws {DropgateProtocolError} If the server returns an invalid response.
|
|
474
480
|
*/
|
|
475
|
-
checkCompatibility(
|
|
481
|
+
async checkCompatibility(opts) {
|
|
482
|
+
let baseUrl;
|
|
483
|
+
let serverInfo;
|
|
484
|
+
try {
|
|
485
|
+
const result = await getServerInfo({ ...opts, fetchFn: this.fetchFn });
|
|
486
|
+
baseUrl = result.baseUrl;
|
|
487
|
+
serverInfo = result.serverInfo;
|
|
488
|
+
} catch (err) {
|
|
489
|
+
if (err instanceof DropgateError) throw err;
|
|
490
|
+
throw new DropgateNetworkError("Could not connect to the server.", {
|
|
491
|
+
cause: err
|
|
492
|
+
});
|
|
493
|
+
}
|
|
476
494
|
const serverVersion = String(serverInfo?.version || "0.0.0");
|
|
477
495
|
const clientVersion = String(this.clientVersion || "0.0.0");
|
|
478
496
|
const c = parseSemverMajorMinor(clientVersion);
|
|
@@ -482,7 +500,9 @@ var DropgateClient = class {
|
|
|
482
500
|
compatible: false,
|
|
483
501
|
clientVersion,
|
|
484
502
|
serverVersion,
|
|
485
|
-
message: `Incompatible versions. Client v${clientVersion}, Server v${serverVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}
|
|
503
|
+
message: `Incompatible versions. Client v${clientVersion}, Server v${serverVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}.`,
|
|
504
|
+
serverInfo,
|
|
505
|
+
baseUrl
|
|
486
506
|
};
|
|
487
507
|
}
|
|
488
508
|
if (c.minor > s.minor) {
|
|
@@ -490,14 +510,18 @@ var DropgateClient = class {
|
|
|
490
510
|
compatible: true,
|
|
491
511
|
clientVersion,
|
|
492
512
|
serverVersion,
|
|
493
|
-
message: `Client (v${clientVersion}) is newer than Server (v${serverVersion})${serverInfo?.name ? ` (${serverInfo.name})` : ""}. Some features may not work
|
|
513
|
+
message: `Client (v${clientVersion}) is newer than Server (v${serverVersion})${serverInfo?.name ? ` (${serverInfo.name})` : ""}. Some features may not work.`,
|
|
514
|
+
serverInfo,
|
|
515
|
+
baseUrl
|
|
494
516
|
};
|
|
495
517
|
}
|
|
496
518
|
return {
|
|
497
519
|
compatible: true,
|
|
498
520
|
clientVersion,
|
|
499
521
|
serverVersion,
|
|
500
|
-
message: `Server: v${serverVersion}, Client: v${clientVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}
|
|
522
|
+
message: `Server: v${serverVersion}, Client: v${clientVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}.`,
|
|
523
|
+
serverInfo,
|
|
524
|
+
baseUrl
|
|
501
525
|
};
|
|
502
526
|
}
|
|
503
527
|
/**
|
|
@@ -591,27 +615,17 @@ var DropgateClient = class {
|
|
|
591
615
|
"Web Crypto API not available (crypto.subtle)."
|
|
592
616
|
);
|
|
593
617
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
baseUrl = res.baseUrl;
|
|
606
|
-
serverInfo = res.serverInfo;
|
|
607
|
-
} catch (err) {
|
|
608
|
-
if (err instanceof DropgateError) throw err;
|
|
609
|
-
throw new DropgateNetworkError("Could not connect to the server.", {
|
|
610
|
-
cause: err
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
const compat = this.checkCompatibility(serverInfo);
|
|
614
|
-
progress({ phase: "server-compat", text: compat.message });
|
|
618
|
+
const fileSizeBytes = file.size;
|
|
619
|
+
progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
620
|
+
const compat = await this.checkCompatibility({
|
|
621
|
+
host,
|
|
622
|
+
port,
|
|
623
|
+
secure,
|
|
624
|
+
timeoutMs: timeouts.serverInfoMs ?? 5e3,
|
|
625
|
+
signal
|
|
626
|
+
});
|
|
627
|
+
const { baseUrl, serverInfo } = compat;
|
|
628
|
+
progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
615
629
|
if (!compat.compatible) {
|
|
616
630
|
throw new DropgateValidationError(compat.message);
|
|
617
631
|
}
|
|
@@ -624,7 +638,7 @@ var DropgateClient = class {
|
|
|
624
638
|
let keyB64 = null;
|
|
625
639
|
let transmittedFilename = filename;
|
|
626
640
|
if (encrypt) {
|
|
627
|
-
progress({ phase: "crypto", text: "Generating encryption key..." });
|
|
641
|
+
progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
628
642
|
try {
|
|
629
643
|
cryptoKey = await generateAesGcmKey(this.cryptoObj);
|
|
630
644
|
keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
|
|
@@ -646,7 +660,7 @@ var DropgateClient = class {
|
|
|
646
660
|
totalChunks,
|
|
647
661
|
encrypt
|
|
648
662
|
);
|
|
649
|
-
progress({ phase: "init", text: "Reserving server storage..." });
|
|
663
|
+
progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
650
664
|
const initPayload = {
|
|
651
665
|
filename: transmittedFilename,
|
|
652
666
|
lifetime: lifetimeMs,
|
|
@@ -689,10 +703,13 @@ var DropgateClient = class {
|
|
|
689
703
|
const end = Math.min(start + this.chunkSize, file.size);
|
|
690
704
|
let chunkBlob = file.slice(start, end);
|
|
691
705
|
const percentComplete = i / totalChunks * 100;
|
|
706
|
+
const processedBytes = i * this.chunkSize;
|
|
692
707
|
progress({
|
|
693
708
|
phase: "chunk",
|
|
694
709
|
text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
|
|
695
710
|
percent: percentComplete,
|
|
711
|
+
processedBytes,
|
|
712
|
+
totalBytes: fileSizeBytes,
|
|
696
713
|
chunkIndex: i,
|
|
697
714
|
totalChunks
|
|
698
715
|
});
|
|
@@ -732,11 +749,13 @@ var DropgateClient = class {
|
|
|
732
749
|
signal,
|
|
733
750
|
progress,
|
|
734
751
|
chunkIndex: i,
|
|
735
|
-
totalChunks
|
|
752
|
+
totalChunks,
|
|
753
|
+
chunkSize: this.chunkSize,
|
|
754
|
+
fileSizeBytes
|
|
736
755
|
}
|
|
737
756
|
);
|
|
738
757
|
}
|
|
739
|
-
progress({ phase: "complete", text: "Finalising upload...", percent: 100 });
|
|
758
|
+
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
740
759
|
const completeRes = await fetchJson(
|
|
741
760
|
this.fetchFn,
|
|
742
761
|
`${baseUrl}/upload/complete`,
|
|
@@ -769,7 +788,7 @@ var DropgateClient = class {
|
|
|
769
788
|
if (encrypt && keyB64) {
|
|
770
789
|
downloadUrl += `#${keyB64}`;
|
|
771
790
|
}
|
|
772
|
-
progress({ phase: "done", text: "Upload successful!", percent: 100 });
|
|
791
|
+
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
773
792
|
return {
|
|
774
793
|
downloadUrl,
|
|
775
794
|
fileId,
|
|
@@ -815,8 +834,20 @@ var DropgateClient = class {
|
|
|
815
834
|
if (!fileId || typeof fileId !== "string") {
|
|
816
835
|
throw new DropgateValidationError("File ID is required.");
|
|
817
836
|
}
|
|
818
|
-
|
|
819
|
-
|
|
837
|
+
progress({ phase: "server-info", text: "Checking server...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
838
|
+
const compat = await this.checkCompatibility({
|
|
839
|
+
host,
|
|
840
|
+
port,
|
|
841
|
+
secure,
|
|
842
|
+
timeoutMs,
|
|
843
|
+
signal
|
|
844
|
+
});
|
|
845
|
+
const { baseUrl } = compat;
|
|
846
|
+
progress({ phase: "server-compat", text: compat.message, processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
847
|
+
if (!compat.compatible) {
|
|
848
|
+
throw new DropgateValidationError(compat.message);
|
|
849
|
+
}
|
|
850
|
+
progress({ phase: "metadata", text: "Fetching file info...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
820
851
|
const { signal: metaSignal, cleanup: metaCleanup } = makeAbortSignal(signal, timeoutMs);
|
|
821
852
|
let metadata;
|
|
822
853
|
try {
|
|
@@ -859,7 +890,7 @@ var DropgateClient = class {
|
|
|
859
890
|
if (!this.cryptoObj?.subtle) {
|
|
860
891
|
throw new DropgateValidationError("Web Crypto API not available for decryption.");
|
|
861
892
|
}
|
|
862
|
-
progress({ phase: "decrypting", text: "Preparing decryption...",
|
|
893
|
+
progress({ phase: "decrypting", text: "Preparing decryption...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
863
894
|
try {
|
|
864
895
|
cryptoKey = await importKeyFromBase64(this.cryptoObj, keyB64, this.base64);
|
|
865
896
|
filename = await decryptFilenameFromBase64(
|
|
@@ -877,7 +908,7 @@ var DropgateClient = class {
|
|
|
877
908
|
} else {
|
|
878
909
|
filename = metadata.filename || "file";
|
|
879
910
|
}
|
|
880
|
-
progress({ phase: "downloading", text: "Starting download...", percent: 0,
|
|
911
|
+
progress({ phase: "downloading", text: "Starting download...", percent: 0, processedBytes: 0, totalBytes });
|
|
881
912
|
const { signal: downloadSignal, cleanup: downloadCleanup } = makeAbortSignal(signal, timeoutMs);
|
|
882
913
|
let receivedBytes = 0;
|
|
883
914
|
const dataChunks = [];
|
|
@@ -946,7 +977,7 @@ var DropgateClient = class {
|
|
|
946
977
|
phase: "decrypting",
|
|
947
978
|
text: `Downloading & decrypting... (${percent}%)`,
|
|
948
979
|
percent,
|
|
949
|
-
receivedBytes,
|
|
980
|
+
processedBytes: receivedBytes,
|
|
950
981
|
totalBytes
|
|
951
982
|
});
|
|
952
983
|
}
|
|
@@ -978,7 +1009,7 @@ var DropgateClient = class {
|
|
|
978
1009
|
phase: "downloading",
|
|
979
1010
|
text: `Downloading... (${percent}%)`,
|
|
980
1011
|
percent,
|
|
981
|
-
receivedBytes,
|
|
1012
|
+
processedBytes: receivedBytes,
|
|
982
1013
|
totalBytes
|
|
983
1014
|
});
|
|
984
1015
|
}
|
|
@@ -992,7 +1023,7 @@ var DropgateClient = class {
|
|
|
992
1023
|
} finally {
|
|
993
1024
|
downloadCleanup();
|
|
994
1025
|
}
|
|
995
|
-
progress({ phase: "complete", text: "Download complete!", percent: 100, receivedBytes, totalBytes });
|
|
1026
|
+
progress({ phase: "complete", text: "Download complete!", percent: 100, processedBytes: receivedBytes, totalBytes });
|
|
996
1027
|
let data;
|
|
997
1028
|
if (collectData && dataChunks.length > 0) {
|
|
998
1029
|
const totalLength = dataChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
@@ -1019,7 +1050,9 @@ var DropgateClient = class {
|
|
|
1019
1050
|
signal,
|
|
1020
1051
|
progress,
|
|
1021
1052
|
chunkIndex,
|
|
1022
|
-
totalChunks
|
|
1053
|
+
totalChunks,
|
|
1054
|
+
chunkSize,
|
|
1055
|
+
fileSizeBytes
|
|
1023
1056
|
} = opts;
|
|
1024
1057
|
let attemptsLeft = retries;
|
|
1025
1058
|
let currentBackoff = backoffMs;
|
|
@@ -1052,6 +1085,8 @@ var DropgateClient = class {
|
|
|
1052
1085
|
throw err instanceof DropgateError ? err : new DropgateNetworkError("Chunk upload failed.", { cause: err });
|
|
1053
1086
|
}
|
|
1054
1087
|
const attemptNumber = maxRetries - attemptsLeft + 1;
|
|
1088
|
+
const processedBytes = chunkIndex * chunkSize;
|
|
1089
|
+
const percent = chunkIndex / totalChunks * 100;
|
|
1055
1090
|
let remaining = currentBackoff;
|
|
1056
1091
|
const tick = 100;
|
|
1057
1092
|
while (remaining > 0) {
|
|
@@ -1059,6 +1094,9 @@ var DropgateClient = class {
|
|
|
1059
1094
|
progress({
|
|
1060
1095
|
phase: "retry-wait",
|
|
1061
1096
|
text: `Chunk upload failed. Retrying in ${secondsLeft}s... (${attemptNumber}/${maxRetries})`,
|
|
1097
|
+
percent,
|
|
1098
|
+
processedBytes,
|
|
1099
|
+
totalBytes: fileSizeBytes,
|
|
1062
1100
|
chunkIndex,
|
|
1063
1101
|
totalChunks
|
|
1064
1102
|
});
|
|
@@ -1068,6 +1106,9 @@ var DropgateClient = class {
|
|
|
1068
1106
|
progress({
|
|
1069
1107
|
phase: "retry",
|
|
1070
1108
|
text: `Chunk upload failed. Retrying now... (${attemptNumber}/${maxRetries})`,
|
|
1109
|
+
percent,
|
|
1110
|
+
processedBytes,
|
|
1111
|
+
totalBytes: fileSizeBytes,
|
|
1071
1112
|
chunkIndex,
|
|
1072
1113
|
totalChunks
|
|
1073
1114
|
});
|
|
@@ -1090,11 +1131,11 @@ function isSecureContextForP2P(hostname, isSecureContext) {
|
|
|
1090
1131
|
return Boolean(isSecureContext) || isLocalhostHostname(hostname || "");
|
|
1091
1132
|
}
|
|
1092
1133
|
function generateP2PCode(cryptoObj) {
|
|
1093
|
-
const
|
|
1134
|
+
const crypto2 = cryptoObj || getDefaultCrypto();
|
|
1094
1135
|
const letters = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
1095
|
-
if (
|
|
1136
|
+
if (crypto2) {
|
|
1096
1137
|
const randomBytes = new Uint8Array(8);
|
|
1097
|
-
|
|
1138
|
+
crypto2.getRandomValues(randomBytes);
|
|
1098
1139
|
let letterPart = "";
|
|
1099
1140
|
for (let i = 0; i < 4; i++) {
|
|
1100
1141
|
letterPart += letters[randomBytes[i] % letters.length];
|
|
@@ -1120,8 +1161,14 @@ function isP2PCodeLike(code) {
|
|
|
1120
1161
|
}
|
|
1121
1162
|
|
|
1122
1163
|
// src/p2p/helpers.ts
|
|
1123
|
-
function
|
|
1124
|
-
|
|
1164
|
+
function resolvePeerConfig(userConfig, serverCaps) {
|
|
1165
|
+
return {
|
|
1166
|
+
path: userConfig.peerjsPath ?? serverCaps?.peerjsPath ?? "/peerjs",
|
|
1167
|
+
iceServers: userConfig.iceServers ?? serverCaps?.iceServers ?? []
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function buildPeerOptions(config = {}) {
|
|
1171
|
+
const { host, port, peerjsPath = "/peerjs", secure = false, iceServers = [] } = config;
|
|
1125
1172
|
const peerOpts = {
|
|
1126
1173
|
host,
|
|
1127
1174
|
path: peerjsPath,
|
|
@@ -1163,6 +1210,12 @@ async function createPeerWithRetries(opts) {
|
|
|
1163
1210
|
}
|
|
1164
1211
|
|
|
1165
1212
|
// src/p2p/send.ts
|
|
1213
|
+
function generateSessionId() {
|
|
1214
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1215
|
+
return crypto.randomUUID();
|
|
1216
|
+
}
|
|
1217
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
1218
|
+
}
|
|
1166
1219
|
async function startP2PSend(opts) {
|
|
1167
1220
|
const {
|
|
1168
1221
|
file,
|
|
@@ -1177,15 +1230,16 @@ async function startP2PSend(opts) {
|
|
|
1177
1230
|
cryptoObj,
|
|
1178
1231
|
maxAttempts = 4,
|
|
1179
1232
|
chunkSize = 256 * 1024,
|
|
1180
|
-
readyTimeoutMs = 8e3,
|
|
1181
1233
|
endAckTimeoutMs = 15e3,
|
|
1182
1234
|
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
1183
1235
|
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
1236
|
+
heartbeatIntervalMs = 5e3,
|
|
1184
1237
|
onCode,
|
|
1185
1238
|
onStatus,
|
|
1186
1239
|
onProgress,
|
|
1187
1240
|
onComplete,
|
|
1188
|
-
onError
|
|
1241
|
+
onError,
|
|
1242
|
+
onDisconnect
|
|
1189
1243
|
} = opts;
|
|
1190
1244
|
if (!file) {
|
|
1191
1245
|
throw new DropgateValidationError("File is missing.");
|
|
@@ -1199,8 +1253,10 @@ async function startP2PSend(opts) {
|
|
|
1199
1253
|
if (serverInfo && !p2pCaps?.enabled) {
|
|
1200
1254
|
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
1201
1255
|
}
|
|
1202
|
-
const finalPath
|
|
1203
|
-
|
|
1256
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
1257
|
+
{ peerjsPath, iceServers },
|
|
1258
|
+
p2pCaps
|
|
1259
|
+
);
|
|
1204
1260
|
const peerOpts = buildPeerOptions({
|
|
1205
1261
|
host,
|
|
1206
1262
|
port,
|
|
@@ -1217,18 +1273,37 @@ async function startP2PSend(opts) {
|
|
|
1217
1273
|
buildPeer,
|
|
1218
1274
|
onCode
|
|
1219
1275
|
});
|
|
1220
|
-
|
|
1276
|
+
const sessionId = generateSessionId();
|
|
1277
|
+
let state = "listening";
|
|
1221
1278
|
let activeConn = null;
|
|
1222
|
-
let
|
|
1223
|
-
let
|
|
1279
|
+
let sentBytes = 0;
|
|
1280
|
+
let heartbeatTimer = null;
|
|
1224
1281
|
const reportProgress = (data) => {
|
|
1225
1282
|
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : file.size;
|
|
1226
1283
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
1227
1284
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
1228
|
-
onProgress?.({
|
|
1285
|
+
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
1229
1286
|
};
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1287
|
+
const safeError = (err) => {
|
|
1288
|
+
if (state === "closed" || state === "completed") return;
|
|
1289
|
+
state = "closed";
|
|
1290
|
+
onError?.(err);
|
|
1291
|
+
cleanup();
|
|
1292
|
+
};
|
|
1293
|
+
const safeComplete = () => {
|
|
1294
|
+
if (state !== "finishing") return;
|
|
1295
|
+
state = "completed";
|
|
1296
|
+
onComplete?.();
|
|
1297
|
+
cleanup();
|
|
1298
|
+
};
|
|
1299
|
+
const cleanup = () => {
|
|
1300
|
+
if (heartbeatTimer) {
|
|
1301
|
+
clearInterval(heartbeatTimer);
|
|
1302
|
+
heartbeatTimer = null;
|
|
1303
|
+
}
|
|
1304
|
+
if (typeof window !== "undefined") {
|
|
1305
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
1306
|
+
}
|
|
1232
1307
|
try {
|
|
1233
1308
|
activeConn?.close();
|
|
1234
1309
|
} catch {
|
|
@@ -1238,21 +1313,59 @@ async function startP2PSend(opts) {
|
|
|
1238
1313
|
} catch {
|
|
1239
1314
|
}
|
|
1240
1315
|
};
|
|
1316
|
+
const handleUnload = () => {
|
|
1317
|
+
try {
|
|
1318
|
+
activeConn?.send({ t: "error", message: "Sender closed the connection." });
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
stop();
|
|
1322
|
+
};
|
|
1323
|
+
if (typeof window !== "undefined") {
|
|
1324
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
1325
|
+
}
|
|
1326
|
+
const stop = () => {
|
|
1327
|
+
if (state === "closed") return;
|
|
1328
|
+
state = "closed";
|
|
1329
|
+
cleanup();
|
|
1330
|
+
};
|
|
1331
|
+
const isStopped = () => state === "closed";
|
|
1241
1332
|
peer.on("connection", (conn) => {
|
|
1242
|
-
if (
|
|
1333
|
+
if (state === "closed") return;
|
|
1243
1334
|
if (activeConn) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1335
|
+
const isOldConnOpen = activeConn.open !== false;
|
|
1336
|
+
if (isOldConnOpen && state === "transferring") {
|
|
1337
|
+
try {
|
|
1338
|
+
conn.send({ t: "error", message: "Transfer already in progress." });
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
conn.close();
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
return;
|
|
1346
|
+
} else if (!isOldConnOpen) {
|
|
1347
|
+
try {
|
|
1348
|
+
activeConn.close();
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
activeConn = null;
|
|
1352
|
+
state = "listening";
|
|
1353
|
+
sentBytes = 0;
|
|
1354
|
+
} else {
|
|
1355
|
+
try {
|
|
1356
|
+
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
conn.close();
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
return;
|
|
1251
1364
|
}
|
|
1252
|
-
return;
|
|
1253
1365
|
}
|
|
1254
1366
|
activeConn = conn;
|
|
1255
|
-
|
|
1367
|
+
state = "negotiating";
|
|
1368
|
+
onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
1256
1369
|
let readyResolve = null;
|
|
1257
1370
|
let ackResolve = null;
|
|
1258
1371
|
const readyPromise = new Promise((resolve) => {
|
|
@@ -1268,6 +1381,7 @@ async function startP2PSend(opts) {
|
|
|
1268
1381
|
const msg = data;
|
|
1269
1382
|
if (!msg.t) return;
|
|
1270
1383
|
if (msg.t === "ready") {
|
|
1384
|
+
onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
1271
1385
|
readyResolve?.();
|
|
1272
1386
|
return;
|
|
1273
1387
|
}
|
|
@@ -1279,22 +1393,23 @@ async function startP2PSend(opts) {
|
|
|
1279
1393
|
ackResolve?.(msg);
|
|
1280
1394
|
return;
|
|
1281
1395
|
}
|
|
1396
|
+
if (msg.t === "pong") {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1282
1399
|
if (msg.t === "error") {
|
|
1283
|
-
|
|
1284
|
-
stop();
|
|
1400
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
1285
1401
|
}
|
|
1286
1402
|
});
|
|
1287
1403
|
conn.on("open", async () => {
|
|
1288
1404
|
try {
|
|
1289
|
-
|
|
1290
|
-
if (stopped) return;
|
|
1405
|
+
if (isStopped()) return;
|
|
1291
1406
|
conn.send({
|
|
1292
1407
|
t: "meta",
|
|
1408
|
+
sessionId,
|
|
1293
1409
|
name: file.name,
|
|
1294
1410
|
size: file.size,
|
|
1295
1411
|
mime: file.type || "application/octet-stream"
|
|
1296
1412
|
});
|
|
1297
|
-
let sent = 0;
|
|
1298
1413
|
const total = file.size;
|
|
1299
1414
|
const dc = conn._dc;
|
|
1300
1415
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
@@ -1303,13 +1418,25 @@ async function startP2PSend(opts) {
|
|
|
1303
1418
|
} catch {
|
|
1304
1419
|
}
|
|
1305
1420
|
}
|
|
1306
|
-
await
|
|
1421
|
+
await readyPromise;
|
|
1422
|
+
if (isStopped()) return;
|
|
1423
|
+
if (heartbeatIntervalMs > 0) {
|
|
1424
|
+
heartbeatTimer = setInterval(() => {
|
|
1425
|
+
if (state === "transferring" || state === "finishing") {
|
|
1426
|
+
try {
|
|
1427
|
+
conn.send({ t: "ping" });
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}, heartbeatIntervalMs);
|
|
1432
|
+
}
|
|
1433
|
+
state = "transferring";
|
|
1307
1434
|
for (let offset = 0; offset < total; offset += chunkSize) {
|
|
1308
|
-
if (
|
|
1435
|
+
if (isStopped()) return;
|
|
1309
1436
|
const slice = file.slice(offset, offset + chunkSize);
|
|
1310
1437
|
const buf = await slice.arrayBuffer();
|
|
1311
1438
|
conn.send(buf);
|
|
1312
|
-
|
|
1439
|
+
sentBytes += buf.byteLength;
|
|
1313
1440
|
if (dc) {
|
|
1314
1441
|
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
1315
1442
|
await new Promise((resolve) => {
|
|
@@ -1329,13 +1456,15 @@ async function startP2PSend(opts) {
|
|
|
1329
1456
|
}
|
|
1330
1457
|
}
|
|
1331
1458
|
}
|
|
1332
|
-
if (
|
|
1459
|
+
if (isStopped()) return;
|
|
1460
|
+
state = "finishing";
|
|
1333
1461
|
conn.send({ t: "end" });
|
|
1334
1462
|
const ackTimeoutMs = Number.isFinite(endAckTimeoutMs) ? Math.max(endAckTimeoutMs, Math.ceil(file.size / (1024 * 1024)) * 1e3) : null;
|
|
1335
1463
|
const ackResult = await Promise.race([
|
|
1336
1464
|
ackPromise,
|
|
1337
1465
|
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
1338
1466
|
]);
|
|
1467
|
+
if (isStopped()) return;
|
|
1339
1468
|
if (!ackResult || typeof ackResult !== "object") {
|
|
1340
1469
|
throw new DropgateNetworkError("Receiver did not confirm completion.");
|
|
1341
1470
|
}
|
|
@@ -1346,29 +1475,43 @@ async function startP2PSend(opts) {
|
|
|
1346
1475
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
1347
1476
|
}
|
|
1348
1477
|
reportProgress({ received: ackReceived || ackTotal, total: ackTotal });
|
|
1349
|
-
|
|
1350
|
-
transferActive = false;
|
|
1351
|
-
onComplete?.();
|
|
1352
|
-
stop();
|
|
1478
|
+
safeComplete();
|
|
1353
1479
|
} catch (err) {
|
|
1354
|
-
|
|
1355
|
-
stop();
|
|
1480
|
+
safeError(err);
|
|
1356
1481
|
}
|
|
1357
1482
|
});
|
|
1358
1483
|
conn.on("error", (err) => {
|
|
1359
|
-
|
|
1360
|
-
stop();
|
|
1484
|
+
safeError(err);
|
|
1361
1485
|
});
|
|
1362
1486
|
conn.on("close", () => {
|
|
1363
|
-
if (
|
|
1364
|
-
|
|
1487
|
+
if (state === "closed" || state === "completed") {
|
|
1488
|
+
cleanup();
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (state === "transferring" || state === "finishing") {
|
|
1492
|
+
safeError(
|
|
1365
1493
|
new DropgateNetworkError("Receiver disconnected before transfer completed.")
|
|
1366
1494
|
);
|
|
1495
|
+
} else {
|
|
1496
|
+
activeConn = null;
|
|
1497
|
+
state = "listening";
|
|
1498
|
+
sentBytes = 0;
|
|
1499
|
+
onDisconnect?.();
|
|
1367
1500
|
}
|
|
1368
|
-
stop();
|
|
1369
1501
|
});
|
|
1370
1502
|
});
|
|
1371
|
-
return {
|
|
1503
|
+
return {
|
|
1504
|
+
peer,
|
|
1505
|
+
code,
|
|
1506
|
+
sessionId,
|
|
1507
|
+
stop,
|
|
1508
|
+
getStatus: () => state,
|
|
1509
|
+
getBytesSent: () => sentBytes,
|
|
1510
|
+
getConnectedPeerId: () => {
|
|
1511
|
+
if (!activeConn) return null;
|
|
1512
|
+
return activeConn.peer || null;
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1372
1515
|
}
|
|
1373
1516
|
|
|
1374
1517
|
// src/p2p/receive.ts
|
|
@@ -1382,6 +1525,8 @@ async function startP2PReceive(opts) {
|
|
|
1382
1525
|
peerjsPath,
|
|
1383
1526
|
secure = false,
|
|
1384
1527
|
iceServers,
|
|
1528
|
+
autoReady = true,
|
|
1529
|
+
watchdogTimeoutMs = 15e3,
|
|
1385
1530
|
onStatus,
|
|
1386
1531
|
onMeta,
|
|
1387
1532
|
onData,
|
|
@@ -1406,8 +1551,10 @@ async function startP2PReceive(opts) {
|
|
|
1406
1551
|
if (!isP2PCodeLike(normalizedCode)) {
|
|
1407
1552
|
throw new DropgateValidationError("Invalid direct transfer code.");
|
|
1408
1553
|
}
|
|
1409
|
-
const finalPath
|
|
1410
|
-
|
|
1554
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
1555
|
+
{ peerjsPath, iceServers },
|
|
1556
|
+
p2pCaps
|
|
1557
|
+
);
|
|
1411
1558
|
const peerOpts = buildPeerOptions({
|
|
1412
1559
|
host,
|
|
1413
1560
|
port,
|
|
@@ -1416,44 +1563,127 @@ async function startP2PReceive(opts) {
|
|
|
1416
1563
|
iceServers: finalIceServers
|
|
1417
1564
|
});
|
|
1418
1565
|
const peer = new Peer(void 0, peerOpts);
|
|
1566
|
+
let state = "initializing";
|
|
1419
1567
|
let total = 0;
|
|
1420
1568
|
let received = 0;
|
|
1569
|
+
let currentSessionId = null;
|
|
1421
1570
|
let lastProgressSentAt = 0;
|
|
1422
1571
|
const progressIntervalMs = 120;
|
|
1423
1572
|
let writeQueue = Promise.resolve();
|
|
1424
|
-
|
|
1573
|
+
let watchdogTimer = null;
|
|
1574
|
+
let activeConn = null;
|
|
1575
|
+
const resetWatchdog = () => {
|
|
1576
|
+
if (watchdogTimeoutMs <= 0) return;
|
|
1577
|
+
if (watchdogTimer) {
|
|
1578
|
+
clearTimeout(watchdogTimer);
|
|
1579
|
+
}
|
|
1580
|
+
watchdogTimer = setTimeout(() => {
|
|
1581
|
+
if (state === "transferring") {
|
|
1582
|
+
safeError(new DropgateNetworkError("Connection timed out (no data received)."));
|
|
1583
|
+
}
|
|
1584
|
+
}, watchdogTimeoutMs);
|
|
1585
|
+
};
|
|
1586
|
+
const clearWatchdog = () => {
|
|
1587
|
+
if (watchdogTimer) {
|
|
1588
|
+
clearTimeout(watchdogTimer);
|
|
1589
|
+
watchdogTimer = null;
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const safeError = (err) => {
|
|
1593
|
+
if (state === "closed" || state === "completed") return;
|
|
1594
|
+
state = "closed";
|
|
1595
|
+
onError?.(err);
|
|
1596
|
+
cleanup();
|
|
1597
|
+
};
|
|
1598
|
+
const safeComplete = (completeData) => {
|
|
1599
|
+
if (state !== "transferring") return;
|
|
1600
|
+
state = "completed";
|
|
1601
|
+
onComplete?.(completeData);
|
|
1602
|
+
cleanup();
|
|
1603
|
+
};
|
|
1604
|
+
const cleanup = () => {
|
|
1605
|
+
clearWatchdog();
|
|
1606
|
+
if (typeof window !== "undefined") {
|
|
1607
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
1608
|
+
}
|
|
1425
1609
|
try {
|
|
1426
1610
|
peer.destroy();
|
|
1427
1611
|
} catch {
|
|
1428
1612
|
}
|
|
1429
1613
|
};
|
|
1430
|
-
|
|
1431
|
-
|
|
1614
|
+
const handleUnload = () => {
|
|
1615
|
+
try {
|
|
1616
|
+
activeConn?.send({ t: "error", message: "Receiver closed the connection." });
|
|
1617
|
+
} catch {
|
|
1618
|
+
}
|
|
1432
1619
|
stop();
|
|
1620
|
+
};
|
|
1621
|
+
if (typeof window !== "undefined") {
|
|
1622
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
1623
|
+
}
|
|
1624
|
+
const stop = () => {
|
|
1625
|
+
if (state === "closed") return;
|
|
1626
|
+
state = "closed";
|
|
1627
|
+
cleanup();
|
|
1628
|
+
};
|
|
1629
|
+
peer.on("error", (err) => {
|
|
1630
|
+
safeError(err);
|
|
1433
1631
|
});
|
|
1434
1632
|
peer.on("open", () => {
|
|
1633
|
+
state = "connecting";
|
|
1435
1634
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
1635
|
+
activeConn = conn;
|
|
1436
1636
|
conn.on("open", () => {
|
|
1637
|
+
state = "negotiating";
|
|
1437
1638
|
onStatus?.({ phase: "connected", message: "Waiting for file details..." });
|
|
1438
1639
|
});
|
|
1439
1640
|
conn.on("data", async (data) => {
|
|
1440
1641
|
try {
|
|
1642
|
+
resetWatchdog();
|
|
1441
1643
|
if (data && typeof data === "object" && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
|
|
1442
1644
|
const msg = data;
|
|
1443
1645
|
if (msg.t === "meta") {
|
|
1646
|
+
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
1647
|
+
try {
|
|
1648
|
+
conn.send({ t: "error", message: "Busy with another session." });
|
|
1649
|
+
} catch {
|
|
1650
|
+
}
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (msg.sessionId) {
|
|
1654
|
+
currentSessionId = msg.sessionId;
|
|
1655
|
+
}
|
|
1444
1656
|
const name = String(msg.name || "file");
|
|
1445
1657
|
total = Number(msg.size) || 0;
|
|
1446
1658
|
received = 0;
|
|
1447
1659
|
writeQueue = Promise.resolve();
|
|
1448
|
-
|
|
1449
|
-
|
|
1660
|
+
const sendReady = () => {
|
|
1661
|
+
state = "transferring";
|
|
1662
|
+
resetWatchdog();
|
|
1663
|
+
try {
|
|
1664
|
+
conn.send({ t: "ready" });
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
if (autoReady) {
|
|
1669
|
+
onMeta?.({ name, total });
|
|
1670
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1671
|
+
sendReady();
|
|
1672
|
+
} else {
|
|
1673
|
+
onMeta?.({ name, total, sendReady });
|
|
1674
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1675
|
+
}
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
if (msg.t === "ping") {
|
|
1450
1679
|
try {
|
|
1451
|
-
conn.send({ t: "
|
|
1680
|
+
conn.send({ t: "pong" });
|
|
1452
1681
|
} catch {
|
|
1453
1682
|
}
|
|
1454
1683
|
return;
|
|
1455
1684
|
}
|
|
1456
1685
|
if (msg.t === "end") {
|
|
1686
|
+
clearWatchdog();
|
|
1457
1687
|
await writeQueue;
|
|
1458
1688
|
if (total && received < total) {
|
|
1459
1689
|
const err = new DropgateNetworkError(
|
|
@@ -1465,11 +1695,11 @@ async function startP2PReceive(opts) {
|
|
|
1465
1695
|
}
|
|
1466
1696
|
throw err;
|
|
1467
1697
|
}
|
|
1468
|
-
onComplete?.({ received, total });
|
|
1469
1698
|
try {
|
|
1470
1699
|
conn.send({ t: "ack", phase: "end", received, total });
|
|
1471
1700
|
} catch {
|
|
1472
1701
|
}
|
|
1702
|
+
safeComplete({ received, total });
|
|
1473
1703
|
return;
|
|
1474
1704
|
}
|
|
1475
1705
|
if (msg.t === "error") {
|
|
@@ -1496,7 +1726,7 @@ async function startP2PReceive(opts) {
|
|
|
1496
1726
|
}
|
|
1497
1727
|
received += buf.byteLength;
|
|
1498
1728
|
const percent = total ? Math.min(100, received / total * 100) : 0;
|
|
1499
|
-
onProgress?.({ received, total, percent });
|
|
1729
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent });
|
|
1500
1730
|
const now = Date.now();
|
|
1501
1731
|
if (received === total || now - lastProgressSentAt >= progressIntervalMs) {
|
|
1502
1732
|
lastProgressSentAt = now;
|
|
@@ -1513,21 +1743,36 @@ async function startP2PReceive(opts) {
|
|
|
1513
1743
|
});
|
|
1514
1744
|
} catch {
|
|
1515
1745
|
}
|
|
1516
|
-
|
|
1517
|
-
stop();
|
|
1746
|
+
safeError(err);
|
|
1518
1747
|
});
|
|
1519
1748
|
} catch (err) {
|
|
1520
|
-
|
|
1521
|
-
stop();
|
|
1749
|
+
safeError(err);
|
|
1522
1750
|
}
|
|
1523
1751
|
});
|
|
1524
1752
|
conn.on("close", () => {
|
|
1525
|
-
if (
|
|
1753
|
+
if (state === "closed" || state === "completed") {
|
|
1754
|
+
cleanup();
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (state === "transferring") {
|
|
1758
|
+
safeError(new DropgateNetworkError("Sender disconnected during transfer."));
|
|
1759
|
+
} else if (state === "negotiating") {
|
|
1760
|
+
state = "closed";
|
|
1761
|
+
cleanup();
|
|
1526
1762
|
onDisconnect?.();
|
|
1763
|
+
} else {
|
|
1764
|
+
safeError(new DropgateNetworkError("Sender disconnected before file details were received."));
|
|
1527
1765
|
}
|
|
1528
1766
|
});
|
|
1529
1767
|
});
|
|
1530
|
-
return {
|
|
1768
|
+
return {
|
|
1769
|
+
peer,
|
|
1770
|
+
stop,
|
|
1771
|
+
getStatus: () => state,
|
|
1772
|
+
getBytesReceived: () => received,
|
|
1773
|
+
getTotalBytes: () => total,
|
|
1774
|
+
getSessionId: () => currentSessionId
|
|
1775
|
+
};
|
|
1531
1776
|
}
|
|
1532
1777
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1533
1778
|
0 && (module.exports = {
|
|
@@ -1560,6 +1805,7 @@ async function startP2PReceive(opts) {
|
|
|
1560
1805
|
getDefaultBase64,
|
|
1561
1806
|
getDefaultCrypto,
|
|
1562
1807
|
getDefaultFetch,
|
|
1808
|
+
getServerInfo,
|
|
1563
1809
|
importKeyFromBase64,
|
|
1564
1810
|
isLocalhostHostname,
|
|
1565
1811
|
isP2PCodeLike,
|
|
@@ -1568,6 +1814,7 @@ async function startP2PReceive(opts) {
|
|
|
1568
1814
|
makeAbortSignal,
|
|
1569
1815
|
parseSemverMajorMinor,
|
|
1570
1816
|
parseServerUrl,
|
|
1817
|
+
resolvePeerConfig,
|
|
1571
1818
|
sha256Hex,
|
|
1572
1819
|
sleep,
|
|
1573
1820
|
startP2PReceive,
|