@dropgate/core 2.1.0 → 2.2.0-beta.1
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 +4 -4
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +287 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +287 -198
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +54 -15
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +13 -2
- package/dist/p2p/index.d.ts +13 -2
- package/dist/p2p/index.js +54 -15
- package/dist/p2p/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -531,201 +531,251 @@ var DropgateClient = class {
|
|
|
531
531
|
encrypt,
|
|
532
532
|
filenameOverride,
|
|
533
533
|
onProgress,
|
|
534
|
+
onCancel,
|
|
534
535
|
signal,
|
|
535
536
|
timeouts = {},
|
|
536
537
|
retry = {}
|
|
537
538
|
} = opts;
|
|
538
|
-
const
|
|
539
|
+
const internalController = signal ? null : new AbortController();
|
|
540
|
+
const effectiveSignal = signal || internalController?.signal;
|
|
541
|
+
let uploadState = "initializing";
|
|
542
|
+
let currentUploadId = null;
|
|
543
|
+
let currentBaseUrl = null;
|
|
544
|
+
const uploadPromise = (async () => {
|
|
539
545
|
try {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
546
|
+
const progress = (evt) => {
|
|
547
|
+
try {
|
|
548
|
+
if (onProgress) onProgress(evt);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
if (!this.cryptoObj?.subtle) {
|
|
553
|
+
throw new DropgateValidationError(
|
|
554
|
+
"Web Crypto API not available (crypto.subtle)."
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const fileSizeBytes = file.size;
|
|
558
|
+
progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
559
|
+
const compat = await this.checkCompatibility({
|
|
560
|
+
host,
|
|
561
|
+
port,
|
|
562
|
+
secure,
|
|
563
|
+
timeoutMs: timeouts.serverInfoMs ?? 5e3,
|
|
564
|
+
signal: effectiveSignal
|
|
565
|
+
});
|
|
566
|
+
const { baseUrl, serverInfo } = compat;
|
|
567
|
+
progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
568
|
+
if (!compat.compatible) {
|
|
569
|
+
throw new DropgateValidationError(compat.message);
|
|
570
|
+
}
|
|
571
|
+
const filename = filenameOverride ?? file.name ?? "file";
|
|
572
|
+
if (!encrypt) {
|
|
573
|
+
validatePlainFilename(filename);
|
|
574
|
+
}
|
|
575
|
+
this.validateUploadInputs({ file, lifetimeMs, encrypt, serverInfo });
|
|
576
|
+
let cryptoKey = null;
|
|
577
|
+
let keyB64 = null;
|
|
578
|
+
let transmittedFilename = filename;
|
|
579
|
+
if (encrypt) {
|
|
580
|
+
progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
581
|
+
try {
|
|
582
|
+
cryptoKey = await generateAesGcmKey(this.cryptoObj);
|
|
583
|
+
keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
|
|
584
|
+
transmittedFilename = await encryptFilenameToBase64(
|
|
585
|
+
this.cryptoObj,
|
|
586
|
+
filename,
|
|
587
|
+
cryptoKey
|
|
588
|
+
);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
throw new DropgateError("Failed to prepare encryption.", {
|
|
591
|
+
code: "CRYPTO_PREP_FAILED",
|
|
592
|
+
cause: err
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
|
597
|
+
const totalUploadSize = estimateTotalUploadSizeBytes(
|
|
598
|
+
file.size,
|
|
599
|
+
totalChunks,
|
|
600
|
+
encrypt
|
|
580
601
|
);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
602
|
+
progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
603
|
+
const initPayload = {
|
|
604
|
+
filename: transmittedFilename,
|
|
605
|
+
lifetime: lifetimeMs,
|
|
606
|
+
isEncrypted: Boolean(encrypt),
|
|
607
|
+
totalSize: totalUploadSize,
|
|
608
|
+
totalChunks
|
|
609
|
+
};
|
|
610
|
+
const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
|
|
611
|
+
method: "POST",
|
|
612
|
+
timeoutMs: timeouts.initMs ?? 15e3,
|
|
613
|
+
signal: effectiveSignal,
|
|
614
|
+
headers: {
|
|
615
|
+
"Content-Type": "application/json",
|
|
616
|
+
Accept: "application/json"
|
|
617
|
+
},
|
|
618
|
+
body: JSON.stringify(initPayload)
|
|
585
619
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
620
|
+
if (!initRes.res.ok) {
|
|
621
|
+
const errorJson = initRes.json;
|
|
622
|
+
const msg = errorJson?.error || `Server initialisation failed: ${initRes.res.status}`;
|
|
623
|
+
throw new DropgateProtocolError(msg, {
|
|
624
|
+
details: initRes.json || initRes.text
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const initJson = initRes.json;
|
|
628
|
+
const uploadId = initJson?.uploadId;
|
|
629
|
+
if (!uploadId || typeof uploadId !== "string") {
|
|
630
|
+
throw new DropgateProtocolError(
|
|
631
|
+
"Server did not return a valid uploadId."
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
currentUploadId = uploadId;
|
|
635
|
+
currentBaseUrl = baseUrl;
|
|
636
|
+
uploadState = "uploading";
|
|
637
|
+
const retries = Number.isFinite(retry.retries) ? retry.retries : 5;
|
|
638
|
+
const baseBackoffMs = Number.isFinite(retry.backoffMs) ? retry.backoffMs : 1e3;
|
|
639
|
+
const maxBackoffMs = Number.isFinite(retry.maxBackoffMs) ? retry.maxBackoffMs : 3e4;
|
|
640
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
641
|
+
if (effectiveSignal?.aborted) {
|
|
642
|
+
throw effectiveSignal.reason || new DropgateAbortError();
|
|
643
|
+
}
|
|
644
|
+
const start = i * this.chunkSize;
|
|
645
|
+
const end = Math.min(start + this.chunkSize, file.size);
|
|
646
|
+
let chunkBlob = file.slice(start, end);
|
|
647
|
+
const percentComplete = i / totalChunks * 100;
|
|
648
|
+
const processedBytes = i * this.chunkSize;
|
|
649
|
+
progress({
|
|
650
|
+
phase: "chunk",
|
|
651
|
+
text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
|
|
652
|
+
percent: percentComplete,
|
|
653
|
+
processedBytes,
|
|
654
|
+
totalBytes: fileSizeBytes,
|
|
655
|
+
chunkIndex: i,
|
|
656
|
+
totalChunks
|
|
657
|
+
});
|
|
658
|
+
const chunkBuffer = await chunkBlob.arrayBuffer();
|
|
659
|
+
let uploadBlob;
|
|
660
|
+
if (encrypt && cryptoKey) {
|
|
661
|
+
uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
|
|
662
|
+
} else {
|
|
663
|
+
uploadBlob = new Blob([chunkBuffer]);
|
|
664
|
+
}
|
|
665
|
+
if (uploadBlob.size > DEFAULT_CHUNK_SIZE + 1024) {
|
|
666
|
+
throw new DropgateValidationError(
|
|
667
|
+
"Chunk too large (client-side). Check chunk size settings."
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
const toHash = await uploadBlob.arrayBuffer();
|
|
671
|
+
const hashHex = await sha256Hex(this.cryptoObj, toHash);
|
|
672
|
+
const headers = {
|
|
673
|
+
"Content-Type": "application/octet-stream",
|
|
674
|
+
"X-Upload-ID": uploadId,
|
|
675
|
+
"X-Chunk-Index": String(i),
|
|
676
|
+
"X-Chunk-Hash": hashHex
|
|
677
|
+
};
|
|
678
|
+
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
679
|
+
await this.attemptChunkUpload(
|
|
680
|
+
chunkUrl,
|
|
681
|
+
{
|
|
682
|
+
method: "POST",
|
|
683
|
+
headers,
|
|
684
|
+
body: uploadBlob
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
retries,
|
|
688
|
+
backoffMs: baseBackoffMs,
|
|
689
|
+
maxBackoffMs,
|
|
690
|
+
timeoutMs: timeouts.chunkMs ?? 6e4,
|
|
691
|
+
signal: effectiveSignal,
|
|
692
|
+
progress,
|
|
693
|
+
chunkIndex: i,
|
|
694
|
+
totalChunks,
|
|
695
|
+
chunkSize: this.chunkSize,
|
|
696
|
+
fileSizeBytes
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
701
|
+
uploadState = "completing";
|
|
702
|
+
const completeRes = await fetchJson(
|
|
703
|
+
this.fetchFn,
|
|
704
|
+
`${baseUrl}/upload/complete`,
|
|
705
|
+
{
|
|
706
|
+
method: "POST",
|
|
707
|
+
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
708
|
+
signal: effectiveSignal,
|
|
709
|
+
headers: {
|
|
710
|
+
"Content-Type": "application/json",
|
|
711
|
+
Accept: "application/json"
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify({ uploadId })
|
|
714
|
+
}
|
|
657
715
|
);
|
|
716
|
+
if (!completeRes.res.ok) {
|
|
717
|
+
const errorJson = completeRes.json;
|
|
718
|
+
const msg = errorJson?.error || "Finalisation failed.";
|
|
719
|
+
throw new DropgateProtocolError(msg, {
|
|
720
|
+
details: completeRes.json || completeRes.text
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
const completeJson = completeRes.json;
|
|
724
|
+
const fileId = completeJson?.id;
|
|
725
|
+
if (!fileId || typeof fileId !== "string") {
|
|
726
|
+
throw new DropgateProtocolError(
|
|
727
|
+
"Server did not return a valid file id."
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
let downloadUrl = `${baseUrl}/${fileId}`;
|
|
731
|
+
if (encrypt && keyB64) {
|
|
732
|
+
downloadUrl += `#${keyB64}`;
|
|
733
|
+
}
|
|
734
|
+
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
735
|
+
uploadState = "completed";
|
|
736
|
+
return {
|
|
737
|
+
downloadUrl,
|
|
738
|
+
fileId,
|
|
739
|
+
uploadId,
|
|
740
|
+
baseUrl,
|
|
741
|
+
...encrypt && keyB64 ? { keyB64 } : {}
|
|
742
|
+
};
|
|
743
|
+
} catch (err) {
|
|
744
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort"))) {
|
|
745
|
+
uploadState = "cancelled";
|
|
746
|
+
onCancel?.();
|
|
747
|
+
} else {
|
|
748
|
+
uploadState = "error";
|
|
749
|
+
}
|
|
750
|
+
throw err;
|
|
658
751
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
"X-Upload-ID": uploadId,
|
|
664
|
-
"X-Chunk-Index": String(i),
|
|
665
|
-
"X-Chunk-Hash": hashHex
|
|
666
|
-
};
|
|
667
|
-
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
668
|
-
await this.attemptChunkUpload(
|
|
669
|
-
chunkUrl,
|
|
670
|
-
{
|
|
752
|
+
})();
|
|
753
|
+
const callCancelEndpoint = async (uploadId, baseUrl) => {
|
|
754
|
+
try {
|
|
755
|
+
await fetchJson(this.fetchFn, `${baseUrl}/upload/cancel`, {
|
|
671
756
|
method: "POST",
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
signal,
|
|
681
|
-
progress,
|
|
682
|
-
chunkIndex: i,
|
|
683
|
-
totalChunks,
|
|
684
|
-
chunkSize: this.chunkSize,
|
|
685
|
-
fileSizeBytes
|
|
686
|
-
}
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
690
|
-
const completeRes = await fetchJson(
|
|
691
|
-
this.fetchFn,
|
|
692
|
-
`${baseUrl}/upload/complete`,
|
|
693
|
-
{
|
|
694
|
-
method: "POST",
|
|
695
|
-
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
696
|
-
signal,
|
|
697
|
-
headers: {
|
|
698
|
-
"Content-Type": "application/json",
|
|
699
|
-
Accept: "application/json"
|
|
700
|
-
},
|
|
701
|
-
body: JSON.stringify({ uploadId })
|
|
757
|
+
timeoutMs: 5e3,
|
|
758
|
+
headers: {
|
|
759
|
+
"Content-Type": "application/json",
|
|
760
|
+
Accept: "application/json"
|
|
761
|
+
},
|
|
762
|
+
body: JSON.stringify({ uploadId })
|
|
763
|
+
});
|
|
764
|
+
} catch {
|
|
702
765
|
}
|
|
703
|
-
|
|
704
|
-
if (!completeRes.res.ok) {
|
|
705
|
-
const errorJson = completeRes.json;
|
|
706
|
-
const msg = errorJson?.error || "Finalisation failed.";
|
|
707
|
-
throw new DropgateProtocolError(msg, {
|
|
708
|
-
details: completeRes.json || completeRes.text
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
const completeJson = completeRes.json;
|
|
712
|
-
const fileId = completeJson?.id;
|
|
713
|
-
if (!fileId || typeof fileId !== "string") {
|
|
714
|
-
throw new DropgateProtocolError(
|
|
715
|
-
"Server did not return a valid file id."
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
let downloadUrl = `${baseUrl}/${fileId}`;
|
|
719
|
-
if (encrypt && keyB64) {
|
|
720
|
-
downloadUrl += `#${keyB64}`;
|
|
721
|
-
}
|
|
722
|
-
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
766
|
+
};
|
|
723
767
|
return {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
768
|
+
result: uploadPromise,
|
|
769
|
+
cancel: (reason) => {
|
|
770
|
+
if (uploadState === "completed" || uploadState === "cancelled") return;
|
|
771
|
+
uploadState = "cancelled";
|
|
772
|
+
if (currentUploadId && currentBaseUrl) {
|
|
773
|
+
callCancelEndpoint(currentUploadId, currentBaseUrl).catch(() => {
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
internalController?.abort(new DropgateAbortError(reason || "Upload cancelled by user."));
|
|
777
|
+
},
|
|
778
|
+
getStatus: () => uploadState
|
|
729
779
|
};
|
|
730
780
|
}
|
|
731
781
|
/**
|
|
@@ -1170,7 +1220,8 @@ async function startP2PSend(opts) {
|
|
|
1170
1220
|
onProgress,
|
|
1171
1221
|
onComplete,
|
|
1172
1222
|
onError,
|
|
1173
|
-
onDisconnect
|
|
1223
|
+
onDisconnect,
|
|
1224
|
+
onCancel
|
|
1174
1225
|
} = opts;
|
|
1175
1226
|
if (!file) {
|
|
1176
1227
|
throw new DropgateValidationError("File is missing.");
|
|
@@ -1216,7 +1267,7 @@ async function startP2PSend(opts) {
|
|
|
1216
1267
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
1217
1268
|
};
|
|
1218
1269
|
const safeError = (err) => {
|
|
1219
|
-
if (state === "closed" || state === "completed") return;
|
|
1270
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1220
1271
|
state = "closed";
|
|
1221
1272
|
onError?.(err);
|
|
1222
1273
|
cleanup();
|
|
@@ -1255,11 +1306,21 @@ async function startP2PSend(opts) {
|
|
|
1255
1306
|
window.addEventListener("beforeunload", handleUnload);
|
|
1256
1307
|
}
|
|
1257
1308
|
const stop = () => {
|
|
1258
|
-
if (state === "closed") return;
|
|
1259
|
-
|
|
1309
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1310
|
+
const wasActive = state === "transferring" || state === "finishing";
|
|
1311
|
+
state = "cancelled";
|
|
1312
|
+
try {
|
|
1313
|
+
if (activeConn && activeConn.open) {
|
|
1314
|
+
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
1315
|
+
}
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
if (wasActive && onCancel) {
|
|
1319
|
+
onCancel({ cancelledBy: "sender" });
|
|
1320
|
+
}
|
|
1260
1321
|
cleanup();
|
|
1261
1322
|
};
|
|
1262
|
-
const isStopped = () => state === "closed";
|
|
1323
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
1263
1324
|
peer.on("connection", (conn) => {
|
|
1264
1325
|
if (state === "closed") return;
|
|
1265
1326
|
if (activeConn) {
|
|
@@ -1329,6 +1390,13 @@ async function startP2PSend(opts) {
|
|
|
1329
1390
|
}
|
|
1330
1391
|
if (msg.t === "error") {
|
|
1331
1392
|
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (msg.t === "cancelled") {
|
|
1396
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1397
|
+
state = "cancelled";
|
|
1398
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.message });
|
|
1399
|
+
cleanup();
|
|
1332
1400
|
}
|
|
1333
1401
|
});
|
|
1334
1402
|
conn.on("open", async () => {
|
|
@@ -1366,6 +1434,7 @@ async function startP2PSend(opts) {
|
|
|
1366
1434
|
if (isStopped()) return;
|
|
1367
1435
|
const slice = file.slice(offset, offset + chunkSize);
|
|
1368
1436
|
const buf = await slice.arrayBuffer();
|
|
1437
|
+
if (isStopped()) return;
|
|
1369
1438
|
conn.send(buf);
|
|
1370
1439
|
sentBytes += buf.byteLength;
|
|
1371
1440
|
if (dc) {
|
|
@@ -1415,14 +1484,14 @@ async function startP2PSend(opts) {
|
|
|
1415
1484
|
safeError(err);
|
|
1416
1485
|
});
|
|
1417
1486
|
conn.on("close", () => {
|
|
1418
|
-
if (state === "closed" || state === "completed") {
|
|
1487
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1419
1488
|
cleanup();
|
|
1420
1489
|
return;
|
|
1421
1490
|
}
|
|
1422
1491
|
if (state === "transferring" || state === "finishing") {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
);
|
|
1492
|
+
state = "cancelled";
|
|
1493
|
+
onCancel?.({ cancelledBy: "receiver" });
|
|
1494
|
+
cleanup();
|
|
1426
1495
|
} else {
|
|
1427
1496
|
activeConn = null;
|
|
1428
1497
|
state = "listening";
|
|
@@ -1464,7 +1533,8 @@ async function startP2PReceive(opts) {
|
|
|
1464
1533
|
onProgress,
|
|
1465
1534
|
onComplete,
|
|
1466
1535
|
onError,
|
|
1467
|
-
onDisconnect
|
|
1536
|
+
onDisconnect,
|
|
1537
|
+
onCancel
|
|
1468
1538
|
} = opts;
|
|
1469
1539
|
if (!code) {
|
|
1470
1540
|
throw new DropgateValidationError("No sharing code was provided.");
|
|
@@ -1521,7 +1591,7 @@ async function startP2PReceive(opts) {
|
|
|
1521
1591
|
}
|
|
1522
1592
|
};
|
|
1523
1593
|
const safeError = (err) => {
|
|
1524
|
-
if (state === "closed" || state === "completed") return;
|
|
1594
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1525
1595
|
state = "closed";
|
|
1526
1596
|
onError?.(err);
|
|
1527
1597
|
cleanup();
|
|
@@ -1553,8 +1623,18 @@ async function startP2PReceive(opts) {
|
|
|
1553
1623
|
window.addEventListener("beforeunload", handleUnload);
|
|
1554
1624
|
}
|
|
1555
1625
|
const stop = () => {
|
|
1556
|
-
if (state === "closed") return;
|
|
1557
|
-
|
|
1626
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1627
|
+
const wasActive = state === "transferring";
|
|
1628
|
+
state = "cancelled";
|
|
1629
|
+
try {
|
|
1630
|
+
if (activeConn && activeConn.open) {
|
|
1631
|
+
activeConn.send({ t: "cancelled", message: "Receiver cancelled the transfer." });
|
|
1632
|
+
}
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1635
|
+
if (wasActive && onCancel) {
|
|
1636
|
+
onCancel({ cancelledBy: "receiver" });
|
|
1637
|
+
}
|
|
1558
1638
|
cleanup();
|
|
1559
1639
|
};
|
|
1560
1640
|
peer.on("error", (err) => {
|
|
@@ -1636,6 +1716,13 @@ async function startP2PReceive(opts) {
|
|
|
1636
1716
|
if (msg.t === "error") {
|
|
1637
1717
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
1638
1718
|
}
|
|
1719
|
+
if (msg.t === "cancelled") {
|
|
1720
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1721
|
+
state = "cancelled";
|
|
1722
|
+
onCancel?.({ cancelledBy: "sender", message: msg.message });
|
|
1723
|
+
cleanup();
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1639
1726
|
return;
|
|
1640
1727
|
}
|
|
1641
1728
|
let bufPromise;
|
|
@@ -1681,12 +1768,14 @@ async function startP2PReceive(opts) {
|
|
|
1681
1768
|
}
|
|
1682
1769
|
});
|
|
1683
1770
|
conn.on("close", () => {
|
|
1684
|
-
if (state === "closed" || state === "completed") {
|
|
1771
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1685
1772
|
cleanup();
|
|
1686
1773
|
return;
|
|
1687
1774
|
}
|
|
1688
1775
|
if (state === "transferring") {
|
|
1689
|
-
|
|
1776
|
+
state = "cancelled";
|
|
1777
|
+
onCancel?.({ cancelledBy: "sender" });
|
|
1778
|
+
cleanup();
|
|
1690
1779
|
} else if (state === "negotiating") {
|
|
1691
1780
|
state = "closed";
|
|
1692
1781
|
cleanup();
|