@dropgate/core 2.2.1 → 3.0.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 +259 -128
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +2900 -1431
- 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 +2902 -1384
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +587 -191
- 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 +589 -179
- 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,57 @@ 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
|
|
219
|
+
var P2P_UNACKED_CHUNK_TIMEOUT_MS = 3e4;
|
|
182
220
|
function generateSessionId() {
|
|
183
|
-
|
|
184
|
-
return crypto.randomUUID();
|
|
185
|
-
}
|
|
186
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
221
|
+
return crypto.randomUUID();
|
|
187
222
|
}
|
|
223
|
+
var ALLOWED_TRANSITIONS = {
|
|
224
|
+
initializing: ["listening", "closed"],
|
|
225
|
+
listening: ["handshaking", "closed", "cancelled"],
|
|
226
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
227
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
228
|
+
transferring: ["finishing", "closed", "cancelled"],
|
|
229
|
+
finishing: ["awaiting_ack", "closed", "cancelled"],
|
|
230
|
+
awaiting_ack: ["completed", "closed", "cancelled"],
|
|
231
|
+
completed: ["closed"],
|
|
232
|
+
cancelled: ["closed"],
|
|
233
|
+
closed: []
|
|
234
|
+
};
|
|
188
235
|
async function startP2PSend(opts) {
|
|
189
236
|
const {
|
|
190
237
|
file,
|
|
@@ -198,21 +245,27 @@ async function startP2PSend(opts) {
|
|
|
198
245
|
codeGenerator,
|
|
199
246
|
cryptoObj,
|
|
200
247
|
maxAttempts = 4,
|
|
201
|
-
chunkSize =
|
|
202
|
-
endAckTimeoutMs =
|
|
248
|
+
chunkSize = P2P_CHUNK_SIZE,
|
|
249
|
+
endAckTimeoutMs = P2P_END_ACK_TIMEOUT_MS,
|
|
203
250
|
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
204
251
|
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
205
252
|
heartbeatIntervalMs = 5e3,
|
|
253
|
+
chunkAcknowledgments = true,
|
|
254
|
+
maxUnackedChunks = P2P_MAX_UNACKED_CHUNKS,
|
|
206
255
|
onCode,
|
|
207
256
|
onStatus,
|
|
208
257
|
onProgress,
|
|
209
258
|
onComplete,
|
|
210
259
|
onError,
|
|
211
260
|
onDisconnect,
|
|
212
|
-
onCancel
|
|
261
|
+
onCancel,
|
|
262
|
+
onConnectionHealth
|
|
213
263
|
} = opts;
|
|
214
|
-
|
|
215
|
-
|
|
264
|
+
const files = Array.isArray(file) ? file : [file];
|
|
265
|
+
const isMultiFile = files.length > 1;
|
|
266
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
267
|
+
if (!files.length) {
|
|
268
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
216
269
|
}
|
|
217
270
|
if (!Peer) {
|
|
218
271
|
throw new DropgateValidationError(
|
|
@@ -248,21 +301,39 @@ async function startP2PSend(opts) {
|
|
|
248
301
|
let activeConn = null;
|
|
249
302
|
let sentBytes = 0;
|
|
250
303
|
let heartbeatTimer = null;
|
|
304
|
+
let healthCheckTimer = null;
|
|
305
|
+
let lastActivityTime = Date.now();
|
|
306
|
+
const unackedChunks = /* @__PURE__ */ new Map();
|
|
307
|
+
let nextSeq = 0;
|
|
308
|
+
let ackResolvers = [];
|
|
309
|
+
let transferEverStarted = false;
|
|
310
|
+
const connectionAttempts = [];
|
|
311
|
+
const MAX_CONNECTION_ATTEMPTS = 10;
|
|
312
|
+
const CONNECTION_RATE_WINDOW_MS = 1e4;
|
|
313
|
+
const transitionTo = (newState) => {
|
|
314
|
+
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
315
|
+
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
state = newState;
|
|
319
|
+
return true;
|
|
320
|
+
};
|
|
251
321
|
const reportProgress = (data) => {
|
|
252
|
-
|
|
322
|
+
if (isStopped()) return;
|
|
323
|
+
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : totalSize;
|
|
253
324
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
254
325
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
255
326
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
256
327
|
};
|
|
257
328
|
const safeError = (err) => {
|
|
258
329
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
259
|
-
|
|
330
|
+
transitionTo("closed");
|
|
260
331
|
onError?.(err);
|
|
261
332
|
cleanup();
|
|
262
333
|
};
|
|
263
334
|
const safeComplete = () => {
|
|
264
|
-
if (state !== "finishing") return;
|
|
265
|
-
|
|
335
|
+
if (state !== "awaiting_ack" && state !== "finishing") return;
|
|
336
|
+
transitionTo("completed");
|
|
266
337
|
onComplete?.();
|
|
267
338
|
cleanup();
|
|
268
339
|
};
|
|
@@ -271,6 +342,13 @@ async function startP2PSend(opts) {
|
|
|
271
342
|
clearInterval(heartbeatTimer);
|
|
272
343
|
heartbeatTimer = null;
|
|
273
344
|
}
|
|
345
|
+
if (healthCheckTimer) {
|
|
346
|
+
clearInterval(healthCheckTimer);
|
|
347
|
+
healthCheckTimer = null;
|
|
348
|
+
}
|
|
349
|
+
ackResolvers.forEach((resolve) => resolve());
|
|
350
|
+
ackResolvers = [];
|
|
351
|
+
unackedChunks.clear();
|
|
274
352
|
if (typeof window !== "undefined") {
|
|
275
353
|
window.removeEventListener("beforeunload", handleUnload);
|
|
276
354
|
}
|
|
@@ -295,8 +373,12 @@ async function startP2PSend(opts) {
|
|
|
295
373
|
}
|
|
296
374
|
const stop = () => {
|
|
297
375
|
if (state === "closed" || state === "cancelled") return;
|
|
298
|
-
|
|
299
|
-
|
|
376
|
+
if (state === "completed") {
|
|
377
|
+
cleanup();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const wasActive = state === "transferring" || state === "finishing" || state === "awaiting_ack";
|
|
381
|
+
transitionTo("cancelled");
|
|
300
382
|
try {
|
|
301
383
|
if (activeConn && activeConn.open) {
|
|
302
384
|
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
@@ -309,8 +391,114 @@ async function startP2PSend(opts) {
|
|
|
309
391
|
cleanup();
|
|
310
392
|
};
|
|
311
393
|
const isStopped = () => state === "closed" || state === "cancelled";
|
|
394
|
+
const startHealthMonitoring = (conn) => {
|
|
395
|
+
if (!onConnectionHealth) return;
|
|
396
|
+
healthCheckTimer = setInterval(() => {
|
|
397
|
+
if (isStopped()) return;
|
|
398
|
+
const dc = conn._dc;
|
|
399
|
+
if (!dc) return;
|
|
400
|
+
const health = {
|
|
401
|
+
iceConnectionState: dc.readyState === "open" ? "connected" : "disconnected",
|
|
402
|
+
bufferedAmount: dc.bufferedAmount,
|
|
403
|
+
lastActivityMs: Date.now() - lastActivityTime
|
|
404
|
+
};
|
|
405
|
+
onConnectionHealth(health);
|
|
406
|
+
}, 2e3);
|
|
407
|
+
};
|
|
408
|
+
const handleChunkAck = (msg) => {
|
|
409
|
+
lastActivityTime = Date.now();
|
|
410
|
+
unackedChunks.delete(msg.seq);
|
|
411
|
+
reportProgress({ received: msg.received, total: totalSize });
|
|
412
|
+
const resolver = ackResolvers.shift();
|
|
413
|
+
if (resolver) resolver();
|
|
414
|
+
};
|
|
415
|
+
const waitForAck = () => {
|
|
416
|
+
return new Promise((resolve) => {
|
|
417
|
+
ackResolvers.push(resolve);
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
421
|
+
if (chunkAcknowledgments) {
|
|
422
|
+
while (unackedChunks.size >= maxUnackedChunks) {
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
for (const [_seq, chunk] of unackedChunks) {
|
|
425
|
+
if (now - chunk.sentAt > P2P_UNACKED_CHUNK_TIMEOUT_MS) {
|
|
426
|
+
throw new DropgateNetworkError("Receiver stopped acknowledging chunks");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
await Promise.race([
|
|
430
|
+
waitForAck(),
|
|
431
|
+
sleep(1e3)
|
|
432
|
+
// Timeout to prevent deadlock
|
|
433
|
+
]);
|
|
434
|
+
if (isStopped()) return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const seq = nextSeq++;
|
|
438
|
+
if (chunkAcknowledgments) {
|
|
439
|
+
unackedChunks.set(seq, { offset, size: data.byteLength, sentAt: Date.now() });
|
|
440
|
+
}
|
|
441
|
+
conn.send({ t: "chunk", seq, offset, size: data.byteLength, total: fileTotal ?? totalSize });
|
|
442
|
+
conn.send(data);
|
|
443
|
+
sentBytes += data.byteLength;
|
|
444
|
+
const dc = conn._dc;
|
|
445
|
+
if (dc && bufferHighWaterMark > 0) {
|
|
446
|
+
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
447
|
+
await new Promise((resolve) => {
|
|
448
|
+
const fallback = setTimeout(resolve, 60);
|
|
449
|
+
try {
|
|
450
|
+
dc.addEventListener(
|
|
451
|
+
"bufferedamountlow",
|
|
452
|
+
() => {
|
|
453
|
+
clearTimeout(fallback);
|
|
454
|
+
resolve();
|
|
455
|
+
},
|
|
456
|
+
{ once: true }
|
|
457
|
+
);
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
if (isStopped()) return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const waitForEndAck = async (conn, ackPromise) => {
|
|
466
|
+
const baseTimeout = endAckTimeoutMs;
|
|
467
|
+
for (let attempt = 0; attempt < P2P_END_ACK_RETRIES; attempt++) {
|
|
468
|
+
conn.send({ t: "end", attempt });
|
|
469
|
+
const timeout = baseTimeout * Math.pow(1.5, attempt);
|
|
470
|
+
const result = await Promise.race([
|
|
471
|
+
ackPromise,
|
|
472
|
+
sleep(timeout).then(() => null)
|
|
473
|
+
]);
|
|
474
|
+
if (result && result.t === "end_ack") {
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
if (isStopped()) {
|
|
478
|
+
throw new DropgateNetworkError("Connection closed during completion.");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
throw new DropgateNetworkError("Receiver did not confirm completion after retries.");
|
|
482
|
+
};
|
|
312
483
|
peer.on("connection", (conn) => {
|
|
313
|
-
if (
|
|
484
|
+
if (isStopped()) return;
|
|
485
|
+
const now = Date.now();
|
|
486
|
+
while (connectionAttempts.length > 0 && connectionAttempts[0] < now - CONNECTION_RATE_WINDOW_MS) {
|
|
487
|
+
connectionAttempts.shift();
|
|
488
|
+
}
|
|
489
|
+
if (connectionAttempts.length >= MAX_CONNECTION_ATTEMPTS) {
|
|
490
|
+
console.warn("[P2P Send] Connection rate limit exceeded, rejecting connection");
|
|
491
|
+
try {
|
|
492
|
+
conn.send({ t: "error", message: "Too many connection attempts. Please wait." });
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
conn.close();
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
connectionAttempts.push(now);
|
|
314
502
|
if (activeConn) {
|
|
315
503
|
const isOldConnOpen = activeConn.open !== false;
|
|
316
504
|
if (isOldConnOpen && state === "transferring") {
|
|
@@ -329,8 +517,21 @@ async function startP2PSend(opts) {
|
|
|
329
517
|
} catch {
|
|
330
518
|
}
|
|
331
519
|
activeConn = null;
|
|
520
|
+
if (transferEverStarted) {
|
|
521
|
+
try {
|
|
522
|
+
conn.send({ t: "error", message: "Transfer already started with another receiver. Cannot reconnect." });
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
conn.close();
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
332
531
|
state = "listening";
|
|
333
532
|
sentBytes = 0;
|
|
533
|
+
nextSeq = 0;
|
|
534
|
+
unackedChunks.clear();
|
|
334
535
|
} else {
|
|
335
536
|
try {
|
|
336
537
|
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
@@ -344,60 +545,98 @@ async function startP2PSend(opts) {
|
|
|
344
545
|
}
|
|
345
546
|
}
|
|
346
547
|
activeConn = conn;
|
|
347
|
-
|
|
348
|
-
onStatus?.({ phase: "
|
|
548
|
+
transitionTo("handshaking");
|
|
549
|
+
if (!isStopped()) onStatus?.({ phase: "connected", message: "Receiver connected." });
|
|
550
|
+
lastActivityTime = Date.now();
|
|
551
|
+
let helloResolve = null;
|
|
349
552
|
let readyResolve = null;
|
|
350
|
-
let
|
|
553
|
+
let endAckResolve = null;
|
|
554
|
+
let fileEndAckResolve = null;
|
|
555
|
+
const helloPromise = new Promise((resolve) => {
|
|
556
|
+
helloResolve = resolve;
|
|
557
|
+
});
|
|
351
558
|
const readyPromise = new Promise((resolve) => {
|
|
352
559
|
readyResolve = resolve;
|
|
353
560
|
});
|
|
354
|
-
const
|
|
355
|
-
|
|
561
|
+
const endAckPromise = new Promise((resolve) => {
|
|
562
|
+
endAckResolve = resolve;
|
|
356
563
|
});
|
|
357
564
|
conn.on("data", (data) => {
|
|
358
|
-
|
|
565
|
+
lastActivityTime = Date.now();
|
|
566
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
359
567
|
return;
|
|
360
568
|
}
|
|
569
|
+
if (!isP2PMessage(data)) return;
|
|
361
570
|
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
|
-
|
|
571
|
+
switch (msg.t) {
|
|
572
|
+
case "hello":
|
|
573
|
+
helloResolve?.(msg.protocolVersion);
|
|
574
|
+
break;
|
|
575
|
+
case "ready":
|
|
576
|
+
if (!isStopped()) onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
577
|
+
readyResolve?.();
|
|
578
|
+
break;
|
|
579
|
+
case "chunk_ack":
|
|
580
|
+
handleChunkAck(msg);
|
|
581
|
+
break;
|
|
582
|
+
case "file_end_ack":
|
|
583
|
+
fileEndAckResolve?.(msg);
|
|
584
|
+
break;
|
|
585
|
+
case "end_ack":
|
|
586
|
+
endAckResolve?.(msg);
|
|
587
|
+
break;
|
|
588
|
+
case "pong":
|
|
589
|
+
break;
|
|
590
|
+
case "error":
|
|
591
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
592
|
+
break;
|
|
593
|
+
case "cancelled":
|
|
594
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
595
|
+
transitionTo("cancelled");
|
|
596
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.reason });
|
|
597
|
+
cleanup();
|
|
598
|
+
break;
|
|
388
599
|
}
|
|
389
600
|
});
|
|
390
601
|
conn.on("open", async () => {
|
|
391
602
|
try {
|
|
392
603
|
if (isStopped()) return;
|
|
604
|
+
startHealthMonitoring(conn);
|
|
605
|
+
conn.send({
|
|
606
|
+
t: "hello",
|
|
607
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
608
|
+
sessionId
|
|
609
|
+
});
|
|
610
|
+
const receiverVersion = await Promise.race([
|
|
611
|
+
helloPromise,
|
|
612
|
+
sleep(1e4).then(() => null)
|
|
613
|
+
]);
|
|
614
|
+
if (isStopped()) return;
|
|
615
|
+
if (receiverVersion === null) {
|
|
616
|
+
throw new DropgateNetworkError("Receiver did not respond to handshake.");
|
|
617
|
+
} else if (receiverVersion !== P2P_PROTOCOL_VERSION) {
|
|
618
|
+
throw new DropgateNetworkError(
|
|
619
|
+
`Protocol version mismatch: sender v${P2P_PROTOCOL_VERSION}, receiver v${receiverVersion}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
transitionTo("negotiating");
|
|
623
|
+
if (!isStopped()) onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
624
|
+
if (isMultiFile) {
|
|
625
|
+
conn.send({
|
|
626
|
+
t: "file_list",
|
|
627
|
+
fileCount: files.length,
|
|
628
|
+
files: files.map((f) => ({ name: f.name, size: f.size, mime: f.type || "application/octet-stream" })),
|
|
629
|
+
totalSize
|
|
630
|
+
});
|
|
631
|
+
}
|
|
393
632
|
conn.send({
|
|
394
633
|
t: "meta",
|
|
395
634
|
sessionId,
|
|
396
|
-
name:
|
|
397
|
-
size:
|
|
398
|
-
mime:
|
|
635
|
+
name: files[0].name,
|
|
636
|
+
size: files[0].size,
|
|
637
|
+
mime: files[0].type || "application/octet-stream",
|
|
638
|
+
...isMultiFile ? { fileIndex: 0 } : {}
|
|
399
639
|
});
|
|
400
|
-
const total = file.size;
|
|
401
640
|
const dc = conn._dc;
|
|
402
641
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
403
642
|
try {
|
|
@@ -409,56 +648,61 @@ async function startP2PSend(opts) {
|
|
|
409
648
|
if (isStopped()) return;
|
|
410
649
|
if (heartbeatIntervalMs > 0) {
|
|
411
650
|
heartbeatTimer = setInterval(() => {
|
|
412
|
-
if (state === "transferring" || state === "finishing") {
|
|
651
|
+
if (state === "transferring" || state === "finishing" || state === "awaiting_ack") {
|
|
413
652
|
try {
|
|
414
|
-
conn.send({ t: "ping" });
|
|
653
|
+
conn.send({ t: "ping", timestamp: Date.now() });
|
|
415
654
|
} catch {
|
|
416
655
|
}
|
|
417
656
|
}
|
|
418
657
|
}, heartbeatIntervalMs);
|
|
419
658
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
659
|
+
transitionTo("transferring");
|
|
660
|
+
transferEverStarted = true;
|
|
661
|
+
let overallSentBytes = 0;
|
|
662
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
663
|
+
const currentFile = files[fi];
|
|
664
|
+
if (isMultiFile && fi > 0) {
|
|
665
|
+
conn.send({
|
|
666
|
+
t: "meta",
|
|
667
|
+
sessionId,
|
|
668
|
+
name: currentFile.name,
|
|
669
|
+
size: currentFile.size,
|
|
670
|
+
mime: currentFile.type || "application/octet-stream",
|
|
671
|
+
fileIndex: fi
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
for (let offset = 0; offset < currentFile.size; offset += chunkSize) {
|
|
675
|
+
if (isStopped()) return;
|
|
676
|
+
const slice = currentFile.slice(offset, offset + chunkSize);
|
|
677
|
+
const buf = await slice.arrayBuffer();
|
|
678
|
+
if (isStopped()) return;
|
|
679
|
+
await sendChunk(conn, buf, offset, currentFile.size);
|
|
680
|
+
overallSentBytes += buf.byteLength;
|
|
681
|
+
reportProgress({ received: overallSentBytes, total: totalSize });
|
|
682
|
+
}
|
|
425
683
|
if (isStopped()) return;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
},
|
|
439
|
-
{ once: true }
|
|
440
|
-
);
|
|
441
|
-
} catch {
|
|
442
|
-
}
|
|
443
|
-
});
|
|
684
|
+
if (isMultiFile) {
|
|
685
|
+
const fileEndAckPromise = new Promise((resolve) => {
|
|
686
|
+
fileEndAckResolve = resolve;
|
|
687
|
+
});
|
|
688
|
+
conn.send({ t: "file_end", fileIndex: fi });
|
|
689
|
+
const feAck = await Promise.race([
|
|
690
|
+
fileEndAckPromise,
|
|
691
|
+
sleep(endAckTimeoutMs).then(() => null)
|
|
692
|
+
]);
|
|
693
|
+
if (isStopped()) return;
|
|
694
|
+
if (!feAck) {
|
|
695
|
+
throw new DropgateNetworkError(`Receiver did not confirm receipt of file ${fi + 1}/${files.length}.`);
|
|
444
696
|
}
|
|
445
697
|
}
|
|
446
698
|
}
|
|
447
699
|
if (isStopped()) return;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
const ackResult = await Promise.race([
|
|
452
|
-
ackPromise,
|
|
453
|
-
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
454
|
-
]);
|
|
700
|
+
transitionTo("finishing");
|
|
701
|
+
transitionTo("awaiting_ack");
|
|
702
|
+
const ackResult = await waitForEndAck(conn, endAckPromise);
|
|
455
703
|
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;
|
|
704
|
+
const ackTotal = Number(ackResult.total) || totalSize;
|
|
705
|
+
const ackReceived = Number(ackResult.received) || 0;
|
|
462
706
|
if (ackTotal && ackReceived < ackTotal) {
|
|
463
707
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
464
708
|
}
|
|
@@ -476,14 +720,24 @@ async function startP2PSend(opts) {
|
|
|
476
720
|
cleanup();
|
|
477
721
|
return;
|
|
478
722
|
}
|
|
723
|
+
if (state === "awaiting_ack") {
|
|
724
|
+
setTimeout(() => {
|
|
725
|
+
if (state === "awaiting_ack") {
|
|
726
|
+
safeError(new DropgateNetworkError("Connection closed while awaiting confirmation."));
|
|
727
|
+
}
|
|
728
|
+
}, P2P_CLOSE_GRACE_PERIOD_MS);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
479
731
|
if (state === "transferring" || state === "finishing") {
|
|
480
|
-
|
|
732
|
+
transitionTo("cancelled");
|
|
481
733
|
onCancel?.({ cancelledBy: "receiver" });
|
|
482
734
|
cleanup();
|
|
483
735
|
} else {
|
|
484
736
|
activeConn = null;
|
|
485
737
|
state = "listening";
|
|
486
738
|
sentBytes = 0;
|
|
739
|
+
nextSeq = 0;
|
|
740
|
+
unackedChunks.clear();
|
|
487
741
|
onDisconnect?.();
|
|
488
742
|
}
|
|
489
743
|
});
|
|
@@ -503,6 +757,16 @@ async function startP2PSend(opts) {
|
|
|
503
757
|
}
|
|
504
758
|
|
|
505
759
|
// src/p2p/receive.ts
|
|
760
|
+
var ALLOWED_TRANSITIONS2 = {
|
|
761
|
+
initializing: ["connecting", "closed"],
|
|
762
|
+
connecting: ["handshaking", "closed", "cancelled"],
|
|
763
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
764
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
765
|
+
transferring: ["completed", "closed", "cancelled"],
|
|
766
|
+
completed: ["closed"],
|
|
767
|
+
cancelled: ["closed"],
|
|
768
|
+
closed: []
|
|
769
|
+
};
|
|
506
770
|
async function startP2PReceive(opts) {
|
|
507
771
|
const {
|
|
508
772
|
code,
|
|
@@ -519,6 +783,8 @@ async function startP2PReceive(opts) {
|
|
|
519
783
|
onMeta,
|
|
520
784
|
onData,
|
|
521
785
|
onProgress,
|
|
786
|
+
onFileStart,
|
|
787
|
+
onFileEnd,
|
|
522
788
|
onComplete,
|
|
523
789
|
onError,
|
|
524
790
|
onDisconnect,
|
|
@@ -556,11 +822,26 @@ async function startP2PReceive(opts) {
|
|
|
556
822
|
let total = 0;
|
|
557
823
|
let received = 0;
|
|
558
824
|
let currentSessionId = null;
|
|
559
|
-
let lastProgressSentAt = 0;
|
|
560
|
-
const progressIntervalMs = 120;
|
|
561
825
|
let writeQueue = Promise.resolve();
|
|
562
826
|
let watchdogTimer = null;
|
|
563
827
|
let activeConn = null;
|
|
828
|
+
let pendingChunk = null;
|
|
829
|
+
let fileList = null;
|
|
830
|
+
let currentFileReceived = 0;
|
|
831
|
+
let totalReceivedAllFiles = 0;
|
|
832
|
+
let expectedChunkSeq = 0;
|
|
833
|
+
let writeQueueDepth = 0;
|
|
834
|
+
const MAX_WRITE_QUEUE_DEPTH = 100;
|
|
835
|
+
const MAX_FILE_COUNT = 1e4;
|
|
836
|
+
const transitionTo = (newState) => {
|
|
837
|
+
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
838
|
+
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
state = newState;
|
|
842
|
+
return true;
|
|
843
|
+
};
|
|
844
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
564
845
|
const resetWatchdog = () => {
|
|
565
846
|
if (watchdogTimeoutMs <= 0) return;
|
|
566
847
|
if (watchdogTimer) {
|
|
@@ -580,15 +861,14 @@ async function startP2PReceive(opts) {
|
|
|
580
861
|
};
|
|
581
862
|
const safeError = (err) => {
|
|
582
863
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
583
|
-
|
|
864
|
+
transitionTo("closed");
|
|
584
865
|
onError?.(err);
|
|
585
866
|
cleanup();
|
|
586
867
|
};
|
|
587
868
|
const safeComplete = (completeData) => {
|
|
588
869
|
if (state !== "transferring") return;
|
|
589
|
-
|
|
870
|
+
transitionTo("completed");
|
|
590
871
|
onComplete?.(completeData);
|
|
591
|
-
cleanup();
|
|
592
872
|
};
|
|
593
873
|
const cleanup = () => {
|
|
594
874
|
clearWatchdog();
|
|
@@ -612,11 +892,15 @@ async function startP2PReceive(opts) {
|
|
|
612
892
|
}
|
|
613
893
|
const stop = () => {
|
|
614
894
|
if (state === "closed" || state === "cancelled") return;
|
|
895
|
+
if (state === "completed") {
|
|
896
|
+
cleanup();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
615
899
|
const wasActive = state === "transferring";
|
|
616
|
-
|
|
900
|
+
transitionTo("cancelled");
|
|
617
901
|
try {
|
|
618
902
|
if (activeConn && activeConn.open) {
|
|
619
|
-
activeConn.send({ t: "cancelled",
|
|
903
|
+
activeConn.send({ t: "cancelled", reason: "Receiver cancelled the transfer." });
|
|
620
904
|
}
|
|
621
905
|
} catch {
|
|
622
906
|
}
|
|
@@ -625,23 +909,122 @@ async function startP2PReceive(opts) {
|
|
|
625
909
|
}
|
|
626
910
|
cleanup();
|
|
627
911
|
};
|
|
912
|
+
const sendChunkAck = (conn, seq) => {
|
|
913
|
+
try {
|
|
914
|
+
conn.send({ t: "chunk_ack", seq, received });
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
};
|
|
628
918
|
peer.on("error", (err) => {
|
|
629
919
|
safeError(err);
|
|
630
920
|
});
|
|
631
921
|
peer.on("open", () => {
|
|
632
|
-
|
|
922
|
+
transitionTo("connecting");
|
|
633
923
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
634
924
|
activeConn = conn;
|
|
635
925
|
conn.on("open", () => {
|
|
636
|
-
|
|
637
|
-
onStatus?.({ phase: "connected", message: "
|
|
926
|
+
transitionTo("handshaking");
|
|
927
|
+
onStatus?.({ phase: "connected", message: "Connected." });
|
|
928
|
+
conn.send({
|
|
929
|
+
t: "hello",
|
|
930
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
931
|
+
sessionId: ""
|
|
932
|
+
});
|
|
638
933
|
});
|
|
639
934
|
conn.on("data", async (data) => {
|
|
640
935
|
try {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
936
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
937
|
+
if (state !== "transferring") {
|
|
938
|
+
throw new DropgateValidationError(
|
|
939
|
+
"Received binary data before transfer was accepted. Possible malicious sender."
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
resetWatchdog();
|
|
943
|
+
if (writeQueueDepth >= MAX_WRITE_QUEUE_DEPTH) {
|
|
944
|
+
throw new DropgateNetworkError("Write queue overflow - receiver cannot keep up");
|
|
945
|
+
}
|
|
946
|
+
let bufPromise;
|
|
947
|
+
if (data instanceof ArrayBuffer) {
|
|
948
|
+
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
949
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
950
|
+
bufPromise = Promise.resolve(
|
|
951
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
952
|
+
);
|
|
953
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
954
|
+
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
955
|
+
} else {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
959
|
+
const expectedSize = pendingChunk?.size;
|
|
960
|
+
pendingChunk = null;
|
|
961
|
+
writeQueueDepth++;
|
|
962
|
+
writeQueue = writeQueue.then(async () => {
|
|
963
|
+
const buf = await bufPromise;
|
|
964
|
+
if (expectedSize !== void 0 && buf.byteLength !== expectedSize) {
|
|
965
|
+
throw new DropgateValidationError(
|
|
966
|
+
`Chunk size mismatch: expected ${expectedSize}, got ${buf.byteLength}`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const newReceived = received + buf.byteLength;
|
|
970
|
+
if (total > 0 && newReceived > total) {
|
|
971
|
+
throw new DropgateValidationError(
|
|
972
|
+
`Received more data than expected: ${newReceived} > ${total}`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (onData) {
|
|
976
|
+
await onData(buf);
|
|
977
|
+
}
|
|
978
|
+
received += buf.byteLength;
|
|
979
|
+
currentFileReceived += buf.byteLength;
|
|
980
|
+
const progressReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
981
|
+
const progressTotal = fileList ? fileList.totalSize : total;
|
|
982
|
+
const percent = progressTotal ? Math.min(100, progressReceived / progressTotal * 100) : 0;
|
|
983
|
+
if (!isStopped()) onProgress?.({ processedBytes: progressReceived, totalBytes: progressTotal, percent });
|
|
984
|
+
if (chunkSeq >= 0) {
|
|
985
|
+
sendChunkAck(conn, chunkSeq);
|
|
986
|
+
}
|
|
987
|
+
}).catch((err) => {
|
|
988
|
+
try {
|
|
989
|
+
conn.send({
|
|
990
|
+
t: "error",
|
|
991
|
+
message: err?.message || "Receiver write failed."
|
|
992
|
+
});
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
safeError(err);
|
|
996
|
+
}).finally(() => {
|
|
997
|
+
writeQueueDepth--;
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (!isP2PMessage(data)) return;
|
|
1002
|
+
const msg = data;
|
|
1003
|
+
switch (msg.t) {
|
|
1004
|
+
case "hello":
|
|
1005
|
+
currentSessionId = msg.sessionId || null;
|
|
1006
|
+
transitionTo("negotiating");
|
|
1007
|
+
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
1008
|
+
break;
|
|
1009
|
+
case "file_list": {
|
|
1010
|
+
const fileListMsg = msg;
|
|
1011
|
+
if (fileListMsg.fileCount > MAX_FILE_COUNT) {
|
|
1012
|
+
throw new DropgateValidationError(`Too many files: ${fileListMsg.fileCount}`);
|
|
1013
|
+
}
|
|
1014
|
+
const sumSize = fileListMsg.files.reduce((sum, f) => sum + f.size, 0);
|
|
1015
|
+
if (sumSize !== fileListMsg.totalSize) {
|
|
1016
|
+
throw new DropgateValidationError(
|
|
1017
|
+
`File list size mismatch: declared ${fileListMsg.totalSize}, actual sum ${sumSize}`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
fileList = fileListMsg;
|
|
1021
|
+
total = fileListMsg.totalSize;
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
case "meta": {
|
|
1025
|
+
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
645
1028
|
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
646
1029
|
try {
|
|
647
1030
|
conn.send({ t: "error", message: "Busy with another session." });
|
|
@@ -653,40 +1036,96 @@ async function startP2PReceive(opts) {
|
|
|
653
1036
|
currentSessionId = msg.sessionId;
|
|
654
1037
|
}
|
|
655
1038
|
const name = String(msg.name || "file");
|
|
656
|
-
|
|
1039
|
+
const fileSize = Number(msg.size) || 0;
|
|
1040
|
+
const fi = msg.fileIndex;
|
|
1041
|
+
if (fileList && typeof fi === "number" && fi > 0) {
|
|
1042
|
+
currentFileReceived = 0;
|
|
1043
|
+
onFileStart?.({ fileIndex: fi, name, size: fileSize });
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
657
1046
|
received = 0;
|
|
1047
|
+
currentFileReceived = 0;
|
|
1048
|
+
totalReceivedAllFiles = 0;
|
|
1049
|
+
if (!fileList) {
|
|
1050
|
+
total = fileSize;
|
|
1051
|
+
}
|
|
658
1052
|
writeQueue = Promise.resolve();
|
|
659
1053
|
const sendReady = () => {
|
|
660
|
-
|
|
1054
|
+
transitionTo("transferring");
|
|
661
1055
|
resetWatchdog();
|
|
1056
|
+
if (fileList) {
|
|
1057
|
+
onFileStart?.({ fileIndex: 0, name, size: fileSize });
|
|
1058
|
+
}
|
|
662
1059
|
try {
|
|
663
1060
|
conn.send({ t: "ready" });
|
|
664
1061
|
} catch {
|
|
665
1062
|
}
|
|
666
1063
|
};
|
|
1064
|
+
const metaEvt = { name, total };
|
|
1065
|
+
if (fileList) {
|
|
1066
|
+
metaEvt.fileCount = fileList.fileCount;
|
|
1067
|
+
metaEvt.files = fileList.files.map((f) => ({ name: f.name, size: f.size }));
|
|
1068
|
+
metaEvt.totalSize = fileList.totalSize;
|
|
1069
|
+
}
|
|
667
1070
|
if (autoReady) {
|
|
668
|
-
|
|
669
|
-
|
|
1071
|
+
if (!isStopped()) {
|
|
1072
|
+
onMeta?.(metaEvt);
|
|
1073
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1074
|
+
}
|
|
670
1075
|
sendReady();
|
|
671
1076
|
} else {
|
|
672
|
-
|
|
673
|
-
|
|
1077
|
+
metaEvt.sendReady = sendReady;
|
|
1078
|
+
if (!isStopped()) {
|
|
1079
|
+
onMeta?.(metaEvt);
|
|
1080
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1081
|
+
}
|
|
674
1082
|
}
|
|
675
|
-
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
case "chunk": {
|
|
1086
|
+
const chunkMsg = msg;
|
|
1087
|
+
if (state !== "transferring") {
|
|
1088
|
+
throw new DropgateValidationError(
|
|
1089
|
+
"Received chunk message before transfer was accepted."
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (chunkMsg.seq !== expectedChunkSeq) {
|
|
1093
|
+
throw new DropgateValidationError(
|
|
1094
|
+
`Chunk sequence error: expected ${expectedChunkSeq}, got ${chunkMsg.seq}`
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
expectedChunkSeq++;
|
|
1098
|
+
pendingChunk = chunkMsg;
|
|
1099
|
+
break;
|
|
676
1100
|
}
|
|
677
|
-
|
|
1101
|
+
case "ping":
|
|
678
1102
|
try {
|
|
679
|
-
conn.send({ t: "pong" });
|
|
1103
|
+
conn.send({ t: "pong", timestamp: Date.now() });
|
|
680
1104
|
} catch {
|
|
681
1105
|
}
|
|
682
|
-
|
|
1106
|
+
break;
|
|
1107
|
+
case "file_end": {
|
|
1108
|
+
clearWatchdog();
|
|
1109
|
+
await writeQueue;
|
|
1110
|
+
const feIdx = msg.fileIndex;
|
|
1111
|
+
onFileEnd?.({ fileIndex: feIdx, receivedBytes: currentFileReceived });
|
|
1112
|
+
try {
|
|
1113
|
+
conn.send({ t: "file_end_ack", fileIndex: feIdx, received: currentFileReceived, size: currentFileReceived });
|
|
1114
|
+
} catch {
|
|
1115
|
+
}
|
|
1116
|
+
totalReceivedAllFiles += currentFileReceived;
|
|
1117
|
+
currentFileReceived = 0;
|
|
1118
|
+
resetWatchdog();
|
|
1119
|
+
break;
|
|
683
1120
|
}
|
|
684
|
-
|
|
1121
|
+
case "end":
|
|
685
1122
|
clearWatchdog();
|
|
686
1123
|
await writeQueue;
|
|
687
|
-
|
|
1124
|
+
const finalReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
1125
|
+
const finalTotal = fileList ? fileList.totalSize : total;
|
|
1126
|
+
if (finalTotal && finalReceived < finalTotal) {
|
|
688
1127
|
const err = new DropgateNetworkError(
|
|
689
|
-
"Transfer ended before
|
|
1128
|
+
"Transfer ended before all data was received."
|
|
690
1129
|
);
|
|
691
1130
|
try {
|
|
692
1131
|
conn.send({ t: "error", message: err.message });
|
|
@@ -695,62 +1134,31 @@ async function startP2PReceive(opts) {
|
|
|
695
1134
|
throw err;
|
|
696
1135
|
}
|
|
697
1136
|
try {
|
|
698
|
-
conn.send({ t: "
|
|
1137
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
699
1138
|
} catch {
|
|
700
1139
|
}
|
|
701
|
-
safeComplete({ received, total });
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1140
|
+
safeComplete({ received: finalReceived, total: finalTotal });
|
|
1141
|
+
(async () => {
|
|
1142
|
+
for (let i = 0; i < 2; i++) {
|
|
1143
|
+
await sleep(P2P_END_ACK_RETRY_DELAY_MS);
|
|
1144
|
+
try {
|
|
1145
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
1146
|
+
} catch {
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
})().catch(() => {
|
|
1151
|
+
});
|
|
1152
|
+
break;
|
|
1153
|
+
case "error":
|
|
705
1154
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
706
|
-
|
|
707
|
-
if (msg.t === "cancelled") {
|
|
1155
|
+
case "cancelled":
|
|
708
1156
|
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
709
|
-
|
|
710
|
-
onCancel?.({ cancelledBy: "sender", message: msg.
|
|
1157
|
+
transitionTo("cancelled");
|
|
1158
|
+
onCancel?.({ cancelledBy: "sender", message: msg.reason });
|
|
711
1159
|
cleanup();
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
return;
|
|
1160
|
+
break;
|
|
715
1161
|
}
|
|
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;
|
|
727
|
-
}
|
|
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
1162
|
} catch (err) {
|
|
755
1163
|
safeError(err);
|
|
756
1164
|
}
|
|
@@ -761,11 +1169,11 @@ async function startP2PReceive(opts) {
|
|
|
761
1169
|
return;
|
|
762
1170
|
}
|
|
763
1171
|
if (state === "transferring") {
|
|
764
|
-
|
|
1172
|
+
transitionTo("cancelled");
|
|
765
1173
|
onCancel?.({ cancelledBy: "sender" });
|
|
766
1174
|
cleanup();
|
|
767
1175
|
} else if (state === "negotiating") {
|
|
768
|
-
|
|
1176
|
+
transitionTo("closed");
|
|
769
1177
|
cleanup();
|
|
770
1178
|
onDisconnect?.();
|
|
771
1179
|
} else {
|
|
@@ -782,16 +1190,4 @@ async function startP2PReceive(opts) {
|
|
|
782
1190
|
getSessionId: () => currentSessionId
|
|
783
1191
|
};
|
|
784
1192
|
}
|
|
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
1193
|
//# sourceMappingURL=index.cjs.map
|