@dropgate/core 2.2.1 → 3.0.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 +259 -128
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +2814 -1436
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +424 -283
- package/dist/index.d.ts +424 -283
- package/dist/index.js +2816 -1389
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +495 -190
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +366 -5
- package/dist/p2p/index.d.ts +366 -5
- package/dist/p2p/index.js +497 -178
- package/dist/p2p/index.js.map +1 -1
- package/package.json +15 -12
package/dist/p2p/index.cjs
CHANGED
|
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
7
|
var __export = (target, all) => {
|
|
7
8
|
for (var name in all)
|
|
8
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -16,15 +17,23 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
17
|
return to;
|
|
17
18
|
};
|
|
18
19
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
19
21
|
|
|
20
22
|
// src/p2p/index.ts
|
|
21
23
|
var p2p_exports = {};
|
|
22
24
|
__export(p2p_exports, {
|
|
25
|
+
P2P_CHUNK_SIZE: () => P2P_CHUNK_SIZE,
|
|
26
|
+
P2P_END_ACK_RETRIES: () => P2P_END_ACK_RETRIES,
|
|
27
|
+
P2P_END_ACK_TIMEOUT_MS: () => P2P_END_ACK_TIMEOUT_MS,
|
|
28
|
+
P2P_MAX_UNACKED_CHUNKS: () => P2P_MAX_UNACKED_CHUNKS,
|
|
29
|
+
P2P_PROTOCOL_VERSION: () => P2P_PROTOCOL_VERSION,
|
|
23
30
|
buildPeerOptions: () => buildPeerOptions,
|
|
24
31
|
createPeerWithRetries: () => createPeerWithRetries,
|
|
25
32
|
generateP2PCode: () => generateP2PCode,
|
|
26
33
|
isLocalhostHostname: () => isLocalhostHostname,
|
|
27
34
|
isP2PCodeLike: () => isP2PCodeLike,
|
|
35
|
+
isP2PMessage: () => isP2PMessage,
|
|
36
|
+
isProtocolCompatible: () => isProtocolCompatible,
|
|
28
37
|
isSecureContextForP2P: () => isSecureContextForP2P,
|
|
29
38
|
resolvePeerConfig: () => resolvePeerConfig,
|
|
30
39
|
startP2PReceive: () => startP2PReceive,
|
|
@@ -35,18 +44,12 @@ module.exports = __toCommonJS(p2p_exports);
|
|
|
35
44
|
// src/errors.ts
|
|
36
45
|
var DropgateError = class extends Error {
|
|
37
46
|
constructor(message, opts = {}) {
|
|
38
|
-
super(message);
|
|
47
|
+
super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
48
|
+
__publicField(this, "code");
|
|
49
|
+
__publicField(this, "details");
|
|
39
50
|
this.name = this.constructor.name;
|
|
40
51
|
this.code = opts.code || "DROPGATE_ERROR";
|
|
41
52
|
this.details = opts.details;
|
|
42
|
-
if (opts.cause !== void 0) {
|
|
43
|
-
Object.defineProperty(this, "cause", {
|
|
44
|
-
value: opts.cause,
|
|
45
|
-
writable: false,
|
|
46
|
-
enumerable: false,
|
|
47
|
-
configurable: true
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
53
|
}
|
|
51
54
|
};
|
|
52
55
|
var DropgateValidationError = class extends DropgateError {
|
|
@@ -178,13 +181,56 @@ async function createPeerWithRetries(opts) {
|
|
|
178
181
|
throw lastError || new DropgateNetworkError("Could not establish PeerJS connection.");
|
|
179
182
|
}
|
|
180
183
|
|
|
184
|
+
// src/p2p/protocol.ts
|
|
185
|
+
var P2P_PROTOCOL_VERSION = 3;
|
|
186
|
+
function isP2PMessage(value) {
|
|
187
|
+
if (!value || typeof value !== "object") return false;
|
|
188
|
+
const msg = value;
|
|
189
|
+
return typeof msg.t === "string" && [
|
|
190
|
+
"hello",
|
|
191
|
+
"file_list",
|
|
192
|
+
"meta",
|
|
193
|
+
"ready",
|
|
194
|
+
"chunk",
|
|
195
|
+
"chunk_ack",
|
|
196
|
+
"file_end",
|
|
197
|
+
"file_end_ack",
|
|
198
|
+
"end",
|
|
199
|
+
"end_ack",
|
|
200
|
+
"ping",
|
|
201
|
+
"pong",
|
|
202
|
+
"error",
|
|
203
|
+
"cancelled",
|
|
204
|
+
"resume",
|
|
205
|
+
"resume_ack"
|
|
206
|
+
].includes(msg.t);
|
|
207
|
+
}
|
|
208
|
+
function isProtocolCompatible(senderVersion, receiverVersion) {
|
|
209
|
+
return senderVersion === receiverVersion;
|
|
210
|
+
}
|
|
211
|
+
var P2P_CHUNK_SIZE = 64 * 1024;
|
|
212
|
+
var P2P_MAX_UNACKED_CHUNKS = 32;
|
|
213
|
+
var P2P_END_ACK_TIMEOUT_MS = 15e3;
|
|
214
|
+
var P2P_END_ACK_RETRIES = 3;
|
|
215
|
+
var P2P_END_ACK_RETRY_DELAY_MS = 100;
|
|
216
|
+
var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
|
|
217
|
+
|
|
181
218
|
// src/p2p/send.ts
|
|
182
219
|
function generateSessionId() {
|
|
183
|
-
|
|
184
|
-
return crypto.randomUUID();
|
|
185
|
-
}
|
|
186
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
220
|
+
return crypto.randomUUID();
|
|
187
221
|
}
|
|
222
|
+
var ALLOWED_TRANSITIONS = {
|
|
223
|
+
initializing: ["listening", "closed"],
|
|
224
|
+
listening: ["handshaking", "closed", "cancelled"],
|
|
225
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
226
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
227
|
+
transferring: ["finishing", "closed", "cancelled"],
|
|
228
|
+
finishing: ["awaiting_ack", "closed", "cancelled"],
|
|
229
|
+
awaiting_ack: ["completed", "closed", "cancelled"],
|
|
230
|
+
completed: ["closed"],
|
|
231
|
+
cancelled: ["closed"],
|
|
232
|
+
closed: []
|
|
233
|
+
};
|
|
188
234
|
async function startP2PSend(opts) {
|
|
189
235
|
const {
|
|
190
236
|
file,
|
|
@@ -198,21 +244,27 @@ async function startP2PSend(opts) {
|
|
|
198
244
|
codeGenerator,
|
|
199
245
|
cryptoObj,
|
|
200
246
|
maxAttempts = 4,
|
|
201
|
-
chunkSize =
|
|
202
|
-
endAckTimeoutMs =
|
|
247
|
+
chunkSize = P2P_CHUNK_SIZE,
|
|
248
|
+
endAckTimeoutMs = P2P_END_ACK_TIMEOUT_MS,
|
|
203
249
|
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
204
250
|
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
205
251
|
heartbeatIntervalMs = 5e3,
|
|
252
|
+
chunkAcknowledgments = true,
|
|
253
|
+
maxUnackedChunks = P2P_MAX_UNACKED_CHUNKS,
|
|
206
254
|
onCode,
|
|
207
255
|
onStatus,
|
|
208
256
|
onProgress,
|
|
209
257
|
onComplete,
|
|
210
258
|
onError,
|
|
211
259
|
onDisconnect,
|
|
212
|
-
onCancel
|
|
260
|
+
onCancel,
|
|
261
|
+
onConnectionHealth
|
|
213
262
|
} = opts;
|
|
214
|
-
|
|
215
|
-
|
|
263
|
+
const files = Array.isArray(file) ? file : [file];
|
|
264
|
+
const isMultiFile = files.length > 1;
|
|
265
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
266
|
+
if (!files.length) {
|
|
267
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
216
268
|
}
|
|
217
269
|
if (!Peer) {
|
|
218
270
|
throw new DropgateValidationError(
|
|
@@ -248,21 +300,35 @@ async function startP2PSend(opts) {
|
|
|
248
300
|
let activeConn = null;
|
|
249
301
|
let sentBytes = 0;
|
|
250
302
|
let heartbeatTimer = null;
|
|
303
|
+
let healthCheckTimer = null;
|
|
304
|
+
let lastActivityTime = Date.now();
|
|
305
|
+
const unackedChunks = /* @__PURE__ */ new Map();
|
|
306
|
+
let nextSeq = 0;
|
|
307
|
+
let ackResolvers = [];
|
|
308
|
+
const transitionTo = (newState) => {
|
|
309
|
+
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
310
|
+
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
state = newState;
|
|
314
|
+
return true;
|
|
315
|
+
};
|
|
251
316
|
const reportProgress = (data) => {
|
|
252
|
-
|
|
317
|
+
if (isStopped()) return;
|
|
318
|
+
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : totalSize;
|
|
253
319
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
254
320
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
255
321
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
256
322
|
};
|
|
257
323
|
const safeError = (err) => {
|
|
258
324
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
259
|
-
|
|
325
|
+
transitionTo("closed");
|
|
260
326
|
onError?.(err);
|
|
261
327
|
cleanup();
|
|
262
328
|
};
|
|
263
329
|
const safeComplete = () => {
|
|
264
|
-
if (state !== "finishing") return;
|
|
265
|
-
|
|
330
|
+
if (state !== "awaiting_ack" && state !== "finishing") return;
|
|
331
|
+
transitionTo("completed");
|
|
266
332
|
onComplete?.();
|
|
267
333
|
cleanup();
|
|
268
334
|
};
|
|
@@ -271,6 +337,13 @@ async function startP2PSend(opts) {
|
|
|
271
337
|
clearInterval(heartbeatTimer);
|
|
272
338
|
heartbeatTimer = null;
|
|
273
339
|
}
|
|
340
|
+
if (healthCheckTimer) {
|
|
341
|
+
clearInterval(healthCheckTimer);
|
|
342
|
+
healthCheckTimer = null;
|
|
343
|
+
}
|
|
344
|
+
ackResolvers.forEach((resolve) => resolve());
|
|
345
|
+
ackResolvers = [];
|
|
346
|
+
unackedChunks.clear();
|
|
274
347
|
if (typeof window !== "undefined") {
|
|
275
348
|
window.removeEventListener("beforeunload", handleUnload);
|
|
276
349
|
}
|
|
@@ -295,8 +368,12 @@ async function startP2PSend(opts) {
|
|
|
295
368
|
}
|
|
296
369
|
const stop = () => {
|
|
297
370
|
if (state === "closed" || state === "cancelled") return;
|
|
298
|
-
|
|
299
|
-
|
|
371
|
+
if (state === "completed") {
|
|
372
|
+
cleanup();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const wasActive = state === "transferring" || state === "finishing" || state === "awaiting_ack";
|
|
376
|
+
transitionTo("cancelled");
|
|
300
377
|
try {
|
|
301
378
|
if (activeConn && activeConn.open) {
|
|
302
379
|
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
@@ -309,8 +386,91 @@ async function startP2PSend(opts) {
|
|
|
309
386
|
cleanup();
|
|
310
387
|
};
|
|
311
388
|
const isStopped = () => state === "closed" || state === "cancelled";
|
|
389
|
+
const startHealthMonitoring = (conn) => {
|
|
390
|
+
if (!onConnectionHealth) return;
|
|
391
|
+
healthCheckTimer = setInterval(() => {
|
|
392
|
+
if (isStopped()) return;
|
|
393
|
+
const dc = conn._dc;
|
|
394
|
+
if (!dc) return;
|
|
395
|
+
const health = {
|
|
396
|
+
iceConnectionState: dc.readyState === "open" ? "connected" : "disconnected",
|
|
397
|
+
bufferedAmount: dc.bufferedAmount,
|
|
398
|
+
lastActivityMs: Date.now() - lastActivityTime
|
|
399
|
+
};
|
|
400
|
+
onConnectionHealth(health);
|
|
401
|
+
}, 2e3);
|
|
402
|
+
};
|
|
403
|
+
const handleChunkAck = (msg) => {
|
|
404
|
+
lastActivityTime = Date.now();
|
|
405
|
+
unackedChunks.delete(msg.seq);
|
|
406
|
+
reportProgress({ received: msg.received, total: totalSize });
|
|
407
|
+
const resolver = ackResolvers.shift();
|
|
408
|
+
if (resolver) resolver();
|
|
409
|
+
};
|
|
410
|
+
const waitForAck = () => {
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
ackResolvers.push(resolve);
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
416
|
+
if (chunkAcknowledgments) {
|
|
417
|
+
while (unackedChunks.size >= maxUnackedChunks) {
|
|
418
|
+
await Promise.race([
|
|
419
|
+
waitForAck(),
|
|
420
|
+
sleep(1e3)
|
|
421
|
+
// Timeout to prevent deadlock
|
|
422
|
+
]);
|
|
423
|
+
if (isStopped()) return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const seq = nextSeq++;
|
|
427
|
+
if (chunkAcknowledgments) {
|
|
428
|
+
unackedChunks.set(seq, { offset, size: data.byteLength, sentAt: Date.now() });
|
|
429
|
+
}
|
|
430
|
+
conn.send({ t: "chunk", seq, offset, size: data.byteLength, total: fileTotal ?? totalSize });
|
|
431
|
+
conn.send(data);
|
|
432
|
+
sentBytes += data.byteLength;
|
|
433
|
+
const dc = conn._dc;
|
|
434
|
+
if (dc && bufferHighWaterMark > 0) {
|
|
435
|
+
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
436
|
+
await new Promise((resolve) => {
|
|
437
|
+
const fallback = setTimeout(resolve, 60);
|
|
438
|
+
try {
|
|
439
|
+
dc.addEventListener(
|
|
440
|
+
"bufferedamountlow",
|
|
441
|
+
() => {
|
|
442
|
+
clearTimeout(fallback);
|
|
443
|
+
resolve();
|
|
444
|
+
},
|
|
445
|
+
{ once: true }
|
|
446
|
+
);
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
if (isStopped()) return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const waitForEndAck = async (conn, ackPromise) => {
|
|
455
|
+
const baseTimeout = endAckTimeoutMs;
|
|
456
|
+
for (let attempt = 0; attempt < P2P_END_ACK_RETRIES; attempt++) {
|
|
457
|
+
conn.send({ t: "end", attempt });
|
|
458
|
+
const timeout = baseTimeout * Math.pow(1.5, attempt);
|
|
459
|
+
const result = await Promise.race([
|
|
460
|
+
ackPromise,
|
|
461
|
+
sleep(timeout).then(() => null)
|
|
462
|
+
]);
|
|
463
|
+
if (result && result.t === "end_ack") {
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
if (isStopped()) {
|
|
467
|
+
throw new DropgateNetworkError("Connection closed during completion.");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
throw new DropgateNetworkError("Receiver did not confirm completion after retries.");
|
|
471
|
+
};
|
|
312
472
|
peer.on("connection", (conn) => {
|
|
313
|
-
if (
|
|
473
|
+
if (isStopped()) return;
|
|
314
474
|
if (activeConn) {
|
|
315
475
|
const isOldConnOpen = activeConn.open !== false;
|
|
316
476
|
if (isOldConnOpen && state === "transferring") {
|
|
@@ -331,6 +491,8 @@ async function startP2PSend(opts) {
|
|
|
331
491
|
activeConn = null;
|
|
332
492
|
state = "listening";
|
|
333
493
|
sentBytes = 0;
|
|
494
|
+
nextSeq = 0;
|
|
495
|
+
unackedChunks.clear();
|
|
334
496
|
} else {
|
|
335
497
|
try {
|
|
336
498
|
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
@@ -344,60 +506,98 @@ async function startP2PSend(opts) {
|
|
|
344
506
|
}
|
|
345
507
|
}
|
|
346
508
|
activeConn = conn;
|
|
347
|
-
|
|
348
|
-
onStatus?.({ phase: "
|
|
509
|
+
transitionTo("handshaking");
|
|
510
|
+
if (!isStopped()) onStatus?.({ phase: "connected", message: "Receiver connected." });
|
|
511
|
+
lastActivityTime = Date.now();
|
|
512
|
+
let helloResolve = null;
|
|
349
513
|
let readyResolve = null;
|
|
350
|
-
let
|
|
514
|
+
let endAckResolve = null;
|
|
515
|
+
let fileEndAckResolve = null;
|
|
516
|
+
const helloPromise = new Promise((resolve) => {
|
|
517
|
+
helloResolve = resolve;
|
|
518
|
+
});
|
|
351
519
|
const readyPromise = new Promise((resolve) => {
|
|
352
520
|
readyResolve = resolve;
|
|
353
521
|
});
|
|
354
|
-
const
|
|
355
|
-
|
|
522
|
+
const endAckPromise = new Promise((resolve) => {
|
|
523
|
+
endAckResolve = resolve;
|
|
356
524
|
});
|
|
357
525
|
conn.on("data", (data) => {
|
|
358
|
-
|
|
526
|
+
lastActivityTime = Date.now();
|
|
527
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
359
528
|
return;
|
|
360
529
|
}
|
|
530
|
+
if (!isP2PMessage(data)) return;
|
|
361
531
|
const msg = data;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
532
|
+
switch (msg.t) {
|
|
533
|
+
case "hello":
|
|
534
|
+
helloResolve?.(msg.protocolVersion);
|
|
535
|
+
break;
|
|
536
|
+
case "ready":
|
|
537
|
+
if (!isStopped()) onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
538
|
+
readyResolve?.();
|
|
539
|
+
break;
|
|
540
|
+
case "chunk_ack":
|
|
541
|
+
handleChunkAck(msg);
|
|
542
|
+
break;
|
|
543
|
+
case "file_end_ack":
|
|
544
|
+
fileEndAckResolve?.(msg);
|
|
545
|
+
break;
|
|
546
|
+
case "end_ack":
|
|
547
|
+
endAckResolve?.(msg);
|
|
548
|
+
break;
|
|
549
|
+
case "pong":
|
|
550
|
+
break;
|
|
551
|
+
case "error":
|
|
552
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
553
|
+
break;
|
|
554
|
+
case "cancelled":
|
|
555
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
556
|
+
transitionTo("cancelled");
|
|
557
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.reason });
|
|
558
|
+
cleanup();
|
|
559
|
+
break;
|
|
388
560
|
}
|
|
389
561
|
});
|
|
390
562
|
conn.on("open", async () => {
|
|
391
563
|
try {
|
|
392
564
|
if (isStopped()) return;
|
|
565
|
+
startHealthMonitoring(conn);
|
|
566
|
+
conn.send({
|
|
567
|
+
t: "hello",
|
|
568
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
569
|
+
sessionId
|
|
570
|
+
});
|
|
571
|
+
const receiverVersion = await Promise.race([
|
|
572
|
+
helloPromise,
|
|
573
|
+
sleep(1e4).then(() => null)
|
|
574
|
+
]);
|
|
575
|
+
if (isStopped()) return;
|
|
576
|
+
if (receiverVersion === null) {
|
|
577
|
+
throw new DropgateNetworkError("Receiver did not respond to handshake.");
|
|
578
|
+
} else if (receiverVersion !== P2P_PROTOCOL_VERSION) {
|
|
579
|
+
throw new DropgateNetworkError(
|
|
580
|
+
`Protocol version mismatch: sender v${P2P_PROTOCOL_VERSION}, receiver v${receiverVersion}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
transitionTo("negotiating");
|
|
584
|
+
if (!isStopped()) onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
585
|
+
if (isMultiFile) {
|
|
586
|
+
conn.send({
|
|
587
|
+
t: "file_list",
|
|
588
|
+
fileCount: files.length,
|
|
589
|
+
files: files.map((f) => ({ name: f.name, size: f.size, mime: f.type || "application/octet-stream" })),
|
|
590
|
+
totalSize
|
|
591
|
+
});
|
|
592
|
+
}
|
|
393
593
|
conn.send({
|
|
394
594
|
t: "meta",
|
|
395
595
|
sessionId,
|
|
396
|
-
name:
|
|
397
|
-
size:
|
|
398
|
-
mime:
|
|
596
|
+
name: files[0].name,
|
|
597
|
+
size: files[0].size,
|
|
598
|
+
mime: files[0].type || "application/octet-stream",
|
|
599
|
+
...isMultiFile ? { fileIndex: 0 } : {}
|
|
399
600
|
});
|
|
400
|
-
const total = file.size;
|
|
401
601
|
const dc = conn._dc;
|
|
402
602
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
403
603
|
try {
|
|
@@ -409,56 +609,60 @@ async function startP2PSend(opts) {
|
|
|
409
609
|
if (isStopped()) return;
|
|
410
610
|
if (heartbeatIntervalMs > 0) {
|
|
411
611
|
heartbeatTimer = setInterval(() => {
|
|
412
|
-
if (state === "transferring" || state === "finishing") {
|
|
612
|
+
if (state === "transferring" || state === "finishing" || state === "awaiting_ack") {
|
|
413
613
|
try {
|
|
414
|
-
conn.send({ t: "ping" });
|
|
614
|
+
conn.send({ t: "ping", timestamp: Date.now() });
|
|
415
615
|
} catch {
|
|
416
616
|
}
|
|
417
617
|
}
|
|
418
618
|
}, heartbeatIntervalMs);
|
|
419
619
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
620
|
+
transitionTo("transferring");
|
|
621
|
+
let overallSentBytes = 0;
|
|
622
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
623
|
+
const currentFile = files[fi];
|
|
624
|
+
if (isMultiFile && fi > 0) {
|
|
625
|
+
conn.send({
|
|
626
|
+
t: "meta",
|
|
627
|
+
sessionId,
|
|
628
|
+
name: currentFile.name,
|
|
629
|
+
size: currentFile.size,
|
|
630
|
+
mime: currentFile.type || "application/octet-stream",
|
|
631
|
+
fileIndex: fi
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
for (let offset = 0; offset < currentFile.size; offset += chunkSize) {
|
|
635
|
+
if (isStopped()) return;
|
|
636
|
+
const slice = currentFile.slice(offset, offset + chunkSize);
|
|
637
|
+
const buf = await slice.arrayBuffer();
|
|
638
|
+
if (isStopped()) return;
|
|
639
|
+
await sendChunk(conn, buf, offset, currentFile.size);
|
|
640
|
+
overallSentBytes += buf.byteLength;
|
|
641
|
+
reportProgress({ received: overallSentBytes, total: totalSize });
|
|
642
|
+
}
|
|
425
643
|
if (isStopped()) return;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
},
|
|
439
|
-
{ once: true }
|
|
440
|
-
);
|
|
441
|
-
} catch {
|
|
442
|
-
}
|
|
443
|
-
});
|
|
644
|
+
if (isMultiFile) {
|
|
645
|
+
const fileEndAckPromise = new Promise((resolve) => {
|
|
646
|
+
fileEndAckResolve = resolve;
|
|
647
|
+
});
|
|
648
|
+
conn.send({ t: "file_end", fileIndex: fi });
|
|
649
|
+
const feAck = await Promise.race([
|
|
650
|
+
fileEndAckPromise,
|
|
651
|
+
sleep(endAckTimeoutMs).then(() => null)
|
|
652
|
+
]);
|
|
653
|
+
if (isStopped()) return;
|
|
654
|
+
if (!feAck) {
|
|
655
|
+
throw new DropgateNetworkError(`Receiver did not confirm receipt of file ${fi + 1}/${files.length}.`);
|
|
444
656
|
}
|
|
445
657
|
}
|
|
446
658
|
}
|
|
447
659
|
if (isStopped()) return;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
const ackResult = await Promise.race([
|
|
452
|
-
ackPromise,
|
|
453
|
-
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
454
|
-
]);
|
|
660
|
+
transitionTo("finishing");
|
|
661
|
+
transitionTo("awaiting_ack");
|
|
662
|
+
const ackResult = await waitForEndAck(conn, endAckPromise);
|
|
455
663
|
if (isStopped()) return;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
const ackData = ackResult;
|
|
460
|
-
const ackTotal = Number(ackData.total) || file.size;
|
|
461
|
-
const ackReceived = Number(ackData.received) || 0;
|
|
664
|
+
const ackTotal = Number(ackResult.total) || totalSize;
|
|
665
|
+
const ackReceived = Number(ackResult.received) || 0;
|
|
462
666
|
if (ackTotal && ackReceived < ackTotal) {
|
|
463
667
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
464
668
|
}
|
|
@@ -476,14 +680,24 @@ async function startP2PSend(opts) {
|
|
|
476
680
|
cleanup();
|
|
477
681
|
return;
|
|
478
682
|
}
|
|
683
|
+
if (state === "awaiting_ack") {
|
|
684
|
+
setTimeout(() => {
|
|
685
|
+
if (state === "awaiting_ack") {
|
|
686
|
+
safeError(new DropgateNetworkError("Connection closed while awaiting confirmation."));
|
|
687
|
+
}
|
|
688
|
+
}, P2P_CLOSE_GRACE_PERIOD_MS);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
479
691
|
if (state === "transferring" || state === "finishing") {
|
|
480
|
-
|
|
692
|
+
transitionTo("cancelled");
|
|
481
693
|
onCancel?.({ cancelledBy: "receiver" });
|
|
482
694
|
cleanup();
|
|
483
695
|
} else {
|
|
484
696
|
activeConn = null;
|
|
485
697
|
state = "listening";
|
|
486
698
|
sentBytes = 0;
|
|
699
|
+
nextSeq = 0;
|
|
700
|
+
unackedChunks.clear();
|
|
487
701
|
onDisconnect?.();
|
|
488
702
|
}
|
|
489
703
|
});
|
|
@@ -503,6 +717,16 @@ async function startP2PSend(opts) {
|
|
|
503
717
|
}
|
|
504
718
|
|
|
505
719
|
// src/p2p/receive.ts
|
|
720
|
+
var ALLOWED_TRANSITIONS2 = {
|
|
721
|
+
initializing: ["connecting", "closed"],
|
|
722
|
+
connecting: ["handshaking", "closed", "cancelled"],
|
|
723
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
724
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
725
|
+
transferring: ["completed", "closed", "cancelled"],
|
|
726
|
+
completed: ["closed"],
|
|
727
|
+
cancelled: ["closed"],
|
|
728
|
+
closed: []
|
|
729
|
+
};
|
|
506
730
|
async function startP2PReceive(opts) {
|
|
507
731
|
const {
|
|
508
732
|
code,
|
|
@@ -519,6 +743,8 @@ async function startP2PReceive(opts) {
|
|
|
519
743
|
onMeta,
|
|
520
744
|
onData,
|
|
521
745
|
onProgress,
|
|
746
|
+
onFileStart,
|
|
747
|
+
onFileEnd,
|
|
522
748
|
onComplete,
|
|
523
749
|
onError,
|
|
524
750
|
onDisconnect,
|
|
@@ -556,11 +782,22 @@ async function startP2PReceive(opts) {
|
|
|
556
782
|
let total = 0;
|
|
557
783
|
let received = 0;
|
|
558
784
|
let currentSessionId = null;
|
|
559
|
-
let lastProgressSentAt = 0;
|
|
560
|
-
const progressIntervalMs = 120;
|
|
561
785
|
let writeQueue = Promise.resolve();
|
|
562
786
|
let watchdogTimer = null;
|
|
563
787
|
let activeConn = null;
|
|
788
|
+
let pendingChunk = null;
|
|
789
|
+
let fileList = null;
|
|
790
|
+
let currentFileReceived = 0;
|
|
791
|
+
let totalReceivedAllFiles = 0;
|
|
792
|
+
const transitionTo = (newState) => {
|
|
793
|
+
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
794
|
+
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
state = newState;
|
|
798
|
+
return true;
|
|
799
|
+
};
|
|
800
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
564
801
|
const resetWatchdog = () => {
|
|
565
802
|
if (watchdogTimeoutMs <= 0) return;
|
|
566
803
|
if (watchdogTimer) {
|
|
@@ -580,15 +817,14 @@ async function startP2PReceive(opts) {
|
|
|
580
817
|
};
|
|
581
818
|
const safeError = (err) => {
|
|
582
819
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
583
|
-
|
|
820
|
+
transitionTo("closed");
|
|
584
821
|
onError?.(err);
|
|
585
822
|
cleanup();
|
|
586
823
|
};
|
|
587
824
|
const safeComplete = (completeData) => {
|
|
588
825
|
if (state !== "transferring") return;
|
|
589
|
-
|
|
826
|
+
transitionTo("completed");
|
|
590
827
|
onComplete?.(completeData);
|
|
591
|
-
cleanup();
|
|
592
828
|
};
|
|
593
829
|
const cleanup = () => {
|
|
594
830
|
clearWatchdog();
|
|
@@ -612,11 +848,15 @@ async function startP2PReceive(opts) {
|
|
|
612
848
|
}
|
|
613
849
|
const stop = () => {
|
|
614
850
|
if (state === "closed" || state === "cancelled") return;
|
|
851
|
+
if (state === "completed") {
|
|
852
|
+
cleanup();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
615
855
|
const wasActive = state === "transferring";
|
|
616
|
-
|
|
856
|
+
transitionTo("cancelled");
|
|
617
857
|
try {
|
|
618
858
|
if (activeConn && activeConn.open) {
|
|
619
|
-
activeConn.send({ t: "cancelled",
|
|
859
|
+
activeConn.send({ t: "cancelled", reason: "Receiver cancelled the transfer." });
|
|
620
860
|
}
|
|
621
861
|
} catch {
|
|
622
862
|
}
|
|
@@ -625,23 +865,88 @@ async function startP2PReceive(opts) {
|
|
|
625
865
|
}
|
|
626
866
|
cleanup();
|
|
627
867
|
};
|
|
868
|
+
const sendChunkAck = (conn, seq) => {
|
|
869
|
+
try {
|
|
870
|
+
conn.send({ t: "chunk_ack", seq, received });
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
};
|
|
628
874
|
peer.on("error", (err) => {
|
|
629
875
|
safeError(err);
|
|
630
876
|
});
|
|
631
877
|
peer.on("open", () => {
|
|
632
|
-
|
|
878
|
+
transitionTo("connecting");
|
|
633
879
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
634
880
|
activeConn = conn;
|
|
635
881
|
conn.on("open", () => {
|
|
636
|
-
|
|
637
|
-
onStatus?.({ phase: "connected", message: "
|
|
882
|
+
transitionTo("handshaking");
|
|
883
|
+
onStatus?.({ phase: "connected", message: "Connected." });
|
|
884
|
+
conn.send({
|
|
885
|
+
t: "hello",
|
|
886
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
887
|
+
sessionId: ""
|
|
888
|
+
});
|
|
638
889
|
});
|
|
639
890
|
conn.on("data", async (data) => {
|
|
640
891
|
try {
|
|
641
892
|
resetWatchdog();
|
|
642
|
-
if (data
|
|
643
|
-
|
|
644
|
-
if (
|
|
893
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
894
|
+
let bufPromise;
|
|
895
|
+
if (data instanceof ArrayBuffer) {
|
|
896
|
+
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
897
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
898
|
+
bufPromise = Promise.resolve(
|
|
899
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
900
|
+
);
|
|
901
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
902
|
+
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
903
|
+
} else {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
907
|
+
pendingChunk = null;
|
|
908
|
+
writeQueue = writeQueue.then(async () => {
|
|
909
|
+
const buf = await bufPromise;
|
|
910
|
+
if (onData) {
|
|
911
|
+
await onData(buf);
|
|
912
|
+
}
|
|
913
|
+
received += buf.byteLength;
|
|
914
|
+
currentFileReceived += buf.byteLength;
|
|
915
|
+
const progressReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
916
|
+
const progressTotal = fileList ? fileList.totalSize : total;
|
|
917
|
+
const percent = progressTotal ? Math.min(100, progressReceived / progressTotal * 100) : 0;
|
|
918
|
+
if (!isStopped()) onProgress?.({ processedBytes: progressReceived, totalBytes: progressTotal, percent });
|
|
919
|
+
if (chunkSeq >= 0) {
|
|
920
|
+
sendChunkAck(conn, chunkSeq);
|
|
921
|
+
}
|
|
922
|
+
}).catch((err) => {
|
|
923
|
+
try {
|
|
924
|
+
conn.send({
|
|
925
|
+
t: "error",
|
|
926
|
+
message: err?.message || "Receiver write failed."
|
|
927
|
+
});
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
safeError(err);
|
|
931
|
+
});
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (!isP2PMessage(data)) return;
|
|
935
|
+
const msg = data;
|
|
936
|
+
switch (msg.t) {
|
|
937
|
+
case "hello":
|
|
938
|
+
currentSessionId = msg.sessionId || null;
|
|
939
|
+
transitionTo("negotiating");
|
|
940
|
+
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
941
|
+
break;
|
|
942
|
+
case "file_list":
|
|
943
|
+
fileList = msg;
|
|
944
|
+
total = fileList.totalSize;
|
|
945
|
+
break;
|
|
946
|
+
case "meta": {
|
|
947
|
+
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
645
950
|
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
646
951
|
try {
|
|
647
952
|
conn.send({ t: "error", message: "Busy with another session." });
|
|
@@ -653,40 +958,83 @@ async function startP2PReceive(opts) {
|
|
|
653
958
|
currentSessionId = msg.sessionId;
|
|
654
959
|
}
|
|
655
960
|
const name = String(msg.name || "file");
|
|
656
|
-
|
|
961
|
+
const fileSize = Number(msg.size) || 0;
|
|
962
|
+
const fi = msg.fileIndex;
|
|
963
|
+
if (fileList && typeof fi === "number" && fi > 0) {
|
|
964
|
+
currentFileReceived = 0;
|
|
965
|
+
onFileStart?.({ fileIndex: fi, name, size: fileSize });
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
657
968
|
received = 0;
|
|
969
|
+
currentFileReceived = 0;
|
|
970
|
+
totalReceivedAllFiles = 0;
|
|
971
|
+
if (!fileList) {
|
|
972
|
+
total = fileSize;
|
|
973
|
+
}
|
|
658
974
|
writeQueue = Promise.resolve();
|
|
659
975
|
const sendReady = () => {
|
|
660
|
-
|
|
976
|
+
transitionTo("transferring");
|
|
661
977
|
resetWatchdog();
|
|
978
|
+
if (fileList) {
|
|
979
|
+
onFileStart?.({ fileIndex: 0, name, size: fileSize });
|
|
980
|
+
}
|
|
662
981
|
try {
|
|
663
982
|
conn.send({ t: "ready" });
|
|
664
983
|
} catch {
|
|
665
984
|
}
|
|
666
985
|
};
|
|
986
|
+
const metaEvt = { name, total };
|
|
987
|
+
if (fileList) {
|
|
988
|
+
metaEvt.fileCount = fileList.fileCount;
|
|
989
|
+
metaEvt.files = fileList.files.map((f) => ({ name: f.name, size: f.size }));
|
|
990
|
+
metaEvt.totalSize = fileList.totalSize;
|
|
991
|
+
}
|
|
667
992
|
if (autoReady) {
|
|
668
|
-
|
|
669
|
-
|
|
993
|
+
if (!isStopped()) {
|
|
994
|
+
onMeta?.(metaEvt);
|
|
995
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
996
|
+
}
|
|
670
997
|
sendReady();
|
|
671
998
|
} else {
|
|
672
|
-
|
|
673
|
-
|
|
999
|
+
metaEvt.sendReady = sendReady;
|
|
1000
|
+
if (!isStopped()) {
|
|
1001
|
+
onMeta?.(metaEvt);
|
|
1002
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1003
|
+
}
|
|
674
1004
|
}
|
|
675
|
-
|
|
1005
|
+
break;
|
|
676
1006
|
}
|
|
677
|
-
|
|
1007
|
+
case "chunk":
|
|
1008
|
+
pendingChunk = msg;
|
|
1009
|
+
break;
|
|
1010
|
+
case "ping":
|
|
678
1011
|
try {
|
|
679
|
-
conn.send({ t: "pong" });
|
|
1012
|
+
conn.send({ t: "pong", timestamp: Date.now() });
|
|
680
1013
|
} catch {
|
|
681
1014
|
}
|
|
682
|
-
|
|
1015
|
+
break;
|
|
1016
|
+
case "file_end": {
|
|
1017
|
+
clearWatchdog();
|
|
1018
|
+
await writeQueue;
|
|
1019
|
+
const feIdx = msg.fileIndex;
|
|
1020
|
+
onFileEnd?.({ fileIndex: feIdx, receivedBytes: currentFileReceived });
|
|
1021
|
+
try {
|
|
1022
|
+
conn.send({ t: "file_end_ack", fileIndex: feIdx, received: currentFileReceived, size: currentFileReceived });
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
totalReceivedAllFiles += currentFileReceived;
|
|
1026
|
+
currentFileReceived = 0;
|
|
1027
|
+
resetWatchdog();
|
|
1028
|
+
break;
|
|
683
1029
|
}
|
|
684
|
-
|
|
1030
|
+
case "end":
|
|
685
1031
|
clearWatchdog();
|
|
686
1032
|
await writeQueue;
|
|
687
|
-
|
|
1033
|
+
const finalReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
1034
|
+
const finalTotal = fileList ? fileList.totalSize : total;
|
|
1035
|
+
if (finalTotal && finalReceived < finalTotal) {
|
|
688
1036
|
const err = new DropgateNetworkError(
|
|
689
|
-
"Transfer ended before
|
|
1037
|
+
"Transfer ended before all data was received."
|
|
690
1038
|
);
|
|
691
1039
|
try {
|
|
692
1040
|
conn.send({ t: "error", message: err.message });
|
|
@@ -695,62 +1043,31 @@ async function startP2PReceive(opts) {
|
|
|
695
1043
|
throw err;
|
|
696
1044
|
}
|
|
697
1045
|
try {
|
|
698
|
-
conn.send({ t: "
|
|
1046
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
699
1047
|
} catch {
|
|
700
1048
|
}
|
|
701
|
-
safeComplete({ received, total });
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1049
|
+
safeComplete({ received: finalReceived, total: finalTotal });
|
|
1050
|
+
(async () => {
|
|
1051
|
+
for (let i = 0; i < 2; i++) {
|
|
1052
|
+
await sleep(P2P_END_ACK_RETRY_DELAY_MS);
|
|
1053
|
+
try {
|
|
1054
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
1055
|
+
} catch {
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
})().catch(() => {
|
|
1060
|
+
});
|
|
1061
|
+
break;
|
|
1062
|
+
case "error":
|
|
705
1063
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
706
|
-
|
|
707
|
-
if (msg.t === "cancelled") {
|
|
1064
|
+
case "cancelled":
|
|
708
1065
|
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
709
|
-
|
|
710
|
-
onCancel?.({ cancelledBy: "sender", message: msg.
|
|
1066
|
+
transitionTo("cancelled");
|
|
1067
|
+
onCancel?.({ cancelledBy: "sender", message: msg.reason });
|
|
711
1068
|
cleanup();
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
let bufPromise;
|
|
717
|
-
if (data instanceof ArrayBuffer) {
|
|
718
|
-
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
719
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
720
|
-
bufPromise = Promise.resolve(
|
|
721
|
-
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
722
|
-
);
|
|
723
|
-
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
724
|
-
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
725
|
-
} else {
|
|
726
|
-
return;
|
|
1069
|
+
break;
|
|
727
1070
|
}
|
|
728
|
-
writeQueue = writeQueue.then(async () => {
|
|
729
|
-
const buf = await bufPromise;
|
|
730
|
-
if (onData) {
|
|
731
|
-
await onData(buf);
|
|
732
|
-
}
|
|
733
|
-
received += buf.byteLength;
|
|
734
|
-
const percent = total ? Math.min(100, received / total * 100) : 0;
|
|
735
|
-
onProgress?.({ processedBytes: received, totalBytes: total, percent });
|
|
736
|
-
const now = Date.now();
|
|
737
|
-
if (received === total || now - lastProgressSentAt >= progressIntervalMs) {
|
|
738
|
-
lastProgressSentAt = now;
|
|
739
|
-
try {
|
|
740
|
-
conn.send({ t: "progress", received, total });
|
|
741
|
-
} catch {
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}).catch((err) => {
|
|
745
|
-
try {
|
|
746
|
-
conn.send({
|
|
747
|
-
t: "error",
|
|
748
|
-
message: err?.message || "Receiver write failed."
|
|
749
|
-
});
|
|
750
|
-
} catch {
|
|
751
|
-
}
|
|
752
|
-
safeError(err);
|
|
753
|
-
});
|
|
754
1071
|
} catch (err) {
|
|
755
1072
|
safeError(err);
|
|
756
1073
|
}
|
|
@@ -761,11 +1078,11 @@ async function startP2PReceive(opts) {
|
|
|
761
1078
|
return;
|
|
762
1079
|
}
|
|
763
1080
|
if (state === "transferring") {
|
|
764
|
-
|
|
1081
|
+
transitionTo("cancelled");
|
|
765
1082
|
onCancel?.({ cancelledBy: "sender" });
|
|
766
1083
|
cleanup();
|
|
767
1084
|
} else if (state === "negotiating") {
|
|
768
|
-
|
|
1085
|
+
transitionTo("closed");
|
|
769
1086
|
cleanup();
|
|
770
1087
|
onDisconnect?.();
|
|
771
1088
|
} else {
|
|
@@ -782,16 +1099,4 @@ async function startP2PReceive(opts) {
|
|
|
782
1099
|
getSessionId: () => currentSessionId
|
|
783
1100
|
};
|
|
784
1101
|
}
|
|
785
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
786
|
-
0 && (module.exports = {
|
|
787
|
-
buildPeerOptions,
|
|
788
|
-
createPeerWithRetries,
|
|
789
|
-
generateP2PCode,
|
|
790
|
-
isLocalhostHostname,
|
|
791
|
-
isP2PCodeLike,
|
|
792
|
-
isSecureContextForP2P,
|
|
793
|
-
resolvePeerConfig,
|
|
794
|
-
startP2PReceive,
|
|
795
|
-
startP2PSend
|
|
796
|
-
});
|
|
797
1102
|
//# sourceMappingURL=index.cjs.map
|