@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.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
1
5
|
// src/errors.ts
|
|
2
6
|
var DropgateError = class extends Error {
|
|
3
7
|
constructor(message, opts = {}) {
|
|
4
|
-
super(message);
|
|
8
|
+
super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
9
|
+
__publicField(this, "code");
|
|
10
|
+
__publicField(this, "details");
|
|
5
11
|
this.name = this.constructor.name;
|
|
6
12
|
this.code = opts.code || "DROPGATE_ERROR";
|
|
7
13
|
this.details = opts.details;
|
|
8
|
-
if (opts.cause !== void 0) {
|
|
9
|
-
Object.defineProperty(this, "cause", {
|
|
10
|
-
value: opts.cause,
|
|
11
|
-
writable: false,
|
|
12
|
-
enumerable: false,
|
|
13
|
-
configurable: true
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
14
|
}
|
|
17
15
|
};
|
|
18
16
|
var DropgateValidationError = class extends DropgateError {
|
|
@@ -144,13 +142,57 @@ async function createPeerWithRetries(opts) {
|
|
|
144
142
|
throw lastError || new DropgateNetworkError("Could not establish PeerJS connection.");
|
|
145
143
|
}
|
|
146
144
|
|
|
145
|
+
// src/p2p/protocol.ts
|
|
146
|
+
var P2P_PROTOCOL_VERSION = 3;
|
|
147
|
+
function isP2PMessage(value) {
|
|
148
|
+
if (!value || typeof value !== "object") return false;
|
|
149
|
+
const msg = value;
|
|
150
|
+
return typeof msg.t === "string" && [
|
|
151
|
+
"hello",
|
|
152
|
+
"file_list",
|
|
153
|
+
"meta",
|
|
154
|
+
"ready",
|
|
155
|
+
"chunk",
|
|
156
|
+
"chunk_ack",
|
|
157
|
+
"file_end",
|
|
158
|
+
"file_end_ack",
|
|
159
|
+
"end",
|
|
160
|
+
"end_ack",
|
|
161
|
+
"ping",
|
|
162
|
+
"pong",
|
|
163
|
+
"error",
|
|
164
|
+
"cancelled",
|
|
165
|
+
"resume",
|
|
166
|
+
"resume_ack"
|
|
167
|
+
].includes(msg.t);
|
|
168
|
+
}
|
|
169
|
+
function isProtocolCompatible(senderVersion, receiverVersion) {
|
|
170
|
+
return senderVersion === receiverVersion;
|
|
171
|
+
}
|
|
172
|
+
var P2P_CHUNK_SIZE = 64 * 1024;
|
|
173
|
+
var P2P_MAX_UNACKED_CHUNKS = 32;
|
|
174
|
+
var P2P_END_ACK_TIMEOUT_MS = 15e3;
|
|
175
|
+
var P2P_END_ACK_RETRIES = 3;
|
|
176
|
+
var P2P_END_ACK_RETRY_DELAY_MS = 100;
|
|
177
|
+
var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
|
|
178
|
+
|
|
147
179
|
// src/p2p/send.ts
|
|
180
|
+
var P2P_UNACKED_CHUNK_TIMEOUT_MS = 3e4;
|
|
148
181
|
function generateSessionId() {
|
|
149
|
-
|
|
150
|
-
return crypto.randomUUID();
|
|
151
|
-
}
|
|
152
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
182
|
+
return crypto.randomUUID();
|
|
153
183
|
}
|
|
184
|
+
var ALLOWED_TRANSITIONS = {
|
|
185
|
+
initializing: ["listening", "closed"],
|
|
186
|
+
listening: ["handshaking", "closed", "cancelled"],
|
|
187
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
188
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
189
|
+
transferring: ["finishing", "closed", "cancelled"],
|
|
190
|
+
finishing: ["awaiting_ack", "closed", "cancelled"],
|
|
191
|
+
awaiting_ack: ["completed", "closed", "cancelled"],
|
|
192
|
+
completed: ["closed"],
|
|
193
|
+
cancelled: ["closed"],
|
|
194
|
+
closed: []
|
|
195
|
+
};
|
|
154
196
|
async function startP2PSend(opts) {
|
|
155
197
|
const {
|
|
156
198
|
file,
|
|
@@ -164,21 +206,27 @@ async function startP2PSend(opts) {
|
|
|
164
206
|
codeGenerator,
|
|
165
207
|
cryptoObj,
|
|
166
208
|
maxAttempts = 4,
|
|
167
|
-
chunkSize =
|
|
168
|
-
endAckTimeoutMs =
|
|
209
|
+
chunkSize = P2P_CHUNK_SIZE,
|
|
210
|
+
endAckTimeoutMs = P2P_END_ACK_TIMEOUT_MS,
|
|
169
211
|
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
170
212
|
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
171
213
|
heartbeatIntervalMs = 5e3,
|
|
214
|
+
chunkAcknowledgments = true,
|
|
215
|
+
maxUnackedChunks = P2P_MAX_UNACKED_CHUNKS,
|
|
172
216
|
onCode,
|
|
173
217
|
onStatus,
|
|
174
218
|
onProgress,
|
|
175
219
|
onComplete,
|
|
176
220
|
onError,
|
|
177
221
|
onDisconnect,
|
|
178
|
-
onCancel
|
|
222
|
+
onCancel,
|
|
223
|
+
onConnectionHealth
|
|
179
224
|
} = opts;
|
|
180
|
-
|
|
181
|
-
|
|
225
|
+
const files = Array.isArray(file) ? file : [file];
|
|
226
|
+
const isMultiFile = files.length > 1;
|
|
227
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
228
|
+
if (!files.length) {
|
|
229
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
182
230
|
}
|
|
183
231
|
if (!Peer) {
|
|
184
232
|
throw new DropgateValidationError(
|
|
@@ -214,21 +262,39 @@ async function startP2PSend(opts) {
|
|
|
214
262
|
let activeConn = null;
|
|
215
263
|
let sentBytes = 0;
|
|
216
264
|
let heartbeatTimer = null;
|
|
265
|
+
let healthCheckTimer = null;
|
|
266
|
+
let lastActivityTime = Date.now();
|
|
267
|
+
const unackedChunks = /* @__PURE__ */ new Map();
|
|
268
|
+
let nextSeq = 0;
|
|
269
|
+
let ackResolvers = [];
|
|
270
|
+
let transferEverStarted = false;
|
|
271
|
+
const connectionAttempts = [];
|
|
272
|
+
const MAX_CONNECTION_ATTEMPTS = 10;
|
|
273
|
+
const CONNECTION_RATE_WINDOW_MS = 1e4;
|
|
274
|
+
const transitionTo = (newState) => {
|
|
275
|
+
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
276
|
+
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
state = newState;
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
217
282
|
const reportProgress = (data) => {
|
|
218
|
-
|
|
283
|
+
if (isStopped()) return;
|
|
284
|
+
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : totalSize;
|
|
219
285
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
220
286
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
221
287
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
222
288
|
};
|
|
223
289
|
const safeError = (err) => {
|
|
224
290
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
225
|
-
|
|
291
|
+
transitionTo("closed");
|
|
226
292
|
onError?.(err);
|
|
227
293
|
cleanup();
|
|
228
294
|
};
|
|
229
295
|
const safeComplete = () => {
|
|
230
|
-
if (state !== "finishing") return;
|
|
231
|
-
|
|
296
|
+
if (state !== "awaiting_ack" && state !== "finishing") return;
|
|
297
|
+
transitionTo("completed");
|
|
232
298
|
onComplete?.();
|
|
233
299
|
cleanup();
|
|
234
300
|
};
|
|
@@ -237,6 +303,13 @@ async function startP2PSend(opts) {
|
|
|
237
303
|
clearInterval(heartbeatTimer);
|
|
238
304
|
heartbeatTimer = null;
|
|
239
305
|
}
|
|
306
|
+
if (healthCheckTimer) {
|
|
307
|
+
clearInterval(healthCheckTimer);
|
|
308
|
+
healthCheckTimer = null;
|
|
309
|
+
}
|
|
310
|
+
ackResolvers.forEach((resolve) => resolve());
|
|
311
|
+
ackResolvers = [];
|
|
312
|
+
unackedChunks.clear();
|
|
240
313
|
if (typeof window !== "undefined") {
|
|
241
314
|
window.removeEventListener("beforeunload", handleUnload);
|
|
242
315
|
}
|
|
@@ -261,8 +334,12 @@ async function startP2PSend(opts) {
|
|
|
261
334
|
}
|
|
262
335
|
const stop = () => {
|
|
263
336
|
if (state === "closed" || state === "cancelled") return;
|
|
264
|
-
|
|
265
|
-
|
|
337
|
+
if (state === "completed") {
|
|
338
|
+
cleanup();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const wasActive = state === "transferring" || state === "finishing" || state === "awaiting_ack";
|
|
342
|
+
transitionTo("cancelled");
|
|
266
343
|
try {
|
|
267
344
|
if (activeConn && activeConn.open) {
|
|
268
345
|
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
@@ -275,8 +352,114 @@ async function startP2PSend(opts) {
|
|
|
275
352
|
cleanup();
|
|
276
353
|
};
|
|
277
354
|
const isStopped = () => state === "closed" || state === "cancelled";
|
|
355
|
+
const startHealthMonitoring = (conn) => {
|
|
356
|
+
if (!onConnectionHealth) return;
|
|
357
|
+
healthCheckTimer = setInterval(() => {
|
|
358
|
+
if (isStopped()) return;
|
|
359
|
+
const dc = conn._dc;
|
|
360
|
+
if (!dc) return;
|
|
361
|
+
const health = {
|
|
362
|
+
iceConnectionState: dc.readyState === "open" ? "connected" : "disconnected",
|
|
363
|
+
bufferedAmount: dc.bufferedAmount,
|
|
364
|
+
lastActivityMs: Date.now() - lastActivityTime
|
|
365
|
+
};
|
|
366
|
+
onConnectionHealth(health);
|
|
367
|
+
}, 2e3);
|
|
368
|
+
};
|
|
369
|
+
const handleChunkAck = (msg) => {
|
|
370
|
+
lastActivityTime = Date.now();
|
|
371
|
+
unackedChunks.delete(msg.seq);
|
|
372
|
+
reportProgress({ received: msg.received, total: totalSize });
|
|
373
|
+
const resolver = ackResolvers.shift();
|
|
374
|
+
if (resolver) resolver();
|
|
375
|
+
};
|
|
376
|
+
const waitForAck = () => {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
ackResolvers.push(resolve);
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
382
|
+
if (chunkAcknowledgments) {
|
|
383
|
+
while (unackedChunks.size >= maxUnackedChunks) {
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
for (const [_seq, chunk] of unackedChunks) {
|
|
386
|
+
if (now - chunk.sentAt > P2P_UNACKED_CHUNK_TIMEOUT_MS) {
|
|
387
|
+
throw new DropgateNetworkError("Receiver stopped acknowledging chunks");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
await Promise.race([
|
|
391
|
+
waitForAck(),
|
|
392
|
+
sleep(1e3)
|
|
393
|
+
// Timeout to prevent deadlock
|
|
394
|
+
]);
|
|
395
|
+
if (isStopped()) return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const seq = nextSeq++;
|
|
399
|
+
if (chunkAcknowledgments) {
|
|
400
|
+
unackedChunks.set(seq, { offset, size: data.byteLength, sentAt: Date.now() });
|
|
401
|
+
}
|
|
402
|
+
conn.send({ t: "chunk", seq, offset, size: data.byteLength, total: fileTotal ?? totalSize });
|
|
403
|
+
conn.send(data);
|
|
404
|
+
sentBytes += data.byteLength;
|
|
405
|
+
const dc = conn._dc;
|
|
406
|
+
if (dc && bufferHighWaterMark > 0) {
|
|
407
|
+
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
408
|
+
await new Promise((resolve) => {
|
|
409
|
+
const fallback = setTimeout(resolve, 60);
|
|
410
|
+
try {
|
|
411
|
+
dc.addEventListener(
|
|
412
|
+
"bufferedamountlow",
|
|
413
|
+
() => {
|
|
414
|
+
clearTimeout(fallback);
|
|
415
|
+
resolve();
|
|
416
|
+
},
|
|
417
|
+
{ once: true }
|
|
418
|
+
);
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
if (isStopped()) return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const waitForEndAck = async (conn, ackPromise) => {
|
|
427
|
+
const baseTimeout = endAckTimeoutMs;
|
|
428
|
+
for (let attempt = 0; attempt < P2P_END_ACK_RETRIES; attempt++) {
|
|
429
|
+
conn.send({ t: "end", attempt });
|
|
430
|
+
const timeout = baseTimeout * Math.pow(1.5, attempt);
|
|
431
|
+
const result = await Promise.race([
|
|
432
|
+
ackPromise,
|
|
433
|
+
sleep(timeout).then(() => null)
|
|
434
|
+
]);
|
|
435
|
+
if (result && result.t === "end_ack") {
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
if (isStopped()) {
|
|
439
|
+
throw new DropgateNetworkError("Connection closed during completion.");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw new DropgateNetworkError("Receiver did not confirm completion after retries.");
|
|
443
|
+
};
|
|
278
444
|
peer.on("connection", (conn) => {
|
|
279
|
-
if (
|
|
445
|
+
if (isStopped()) return;
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
while (connectionAttempts.length > 0 && connectionAttempts[0] < now - CONNECTION_RATE_WINDOW_MS) {
|
|
448
|
+
connectionAttempts.shift();
|
|
449
|
+
}
|
|
450
|
+
if (connectionAttempts.length >= MAX_CONNECTION_ATTEMPTS) {
|
|
451
|
+
console.warn("[P2P Send] Connection rate limit exceeded, rejecting connection");
|
|
452
|
+
try {
|
|
453
|
+
conn.send({ t: "error", message: "Too many connection attempts. Please wait." });
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
conn.close();
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
connectionAttempts.push(now);
|
|
280
463
|
if (activeConn) {
|
|
281
464
|
const isOldConnOpen = activeConn.open !== false;
|
|
282
465
|
if (isOldConnOpen && state === "transferring") {
|
|
@@ -295,8 +478,21 @@ async function startP2PSend(opts) {
|
|
|
295
478
|
} catch {
|
|
296
479
|
}
|
|
297
480
|
activeConn = null;
|
|
481
|
+
if (transferEverStarted) {
|
|
482
|
+
try {
|
|
483
|
+
conn.send({ t: "error", message: "Transfer already started with another receiver. Cannot reconnect." });
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
conn.close();
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
298
492
|
state = "listening";
|
|
299
493
|
sentBytes = 0;
|
|
494
|
+
nextSeq = 0;
|
|
495
|
+
unackedChunks.clear();
|
|
300
496
|
} else {
|
|
301
497
|
try {
|
|
302
498
|
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
@@ -310,60 +506,98 @@ async function startP2PSend(opts) {
|
|
|
310
506
|
}
|
|
311
507
|
}
|
|
312
508
|
activeConn = conn;
|
|
313
|
-
|
|
314
|
-
onStatus?.({ phase: "
|
|
509
|
+
transitionTo("handshaking");
|
|
510
|
+
if (!isStopped()) onStatus?.({ phase: "connected", message: "Receiver connected." });
|
|
511
|
+
lastActivityTime = Date.now();
|
|
512
|
+
let helloResolve = null;
|
|
315
513
|
let readyResolve = null;
|
|
316
|
-
let
|
|
514
|
+
let endAckResolve = null;
|
|
515
|
+
let fileEndAckResolve = null;
|
|
516
|
+
const helloPromise = new Promise((resolve) => {
|
|
517
|
+
helloResolve = resolve;
|
|
518
|
+
});
|
|
317
519
|
const readyPromise = new Promise((resolve) => {
|
|
318
520
|
readyResolve = resolve;
|
|
319
521
|
});
|
|
320
|
-
const
|
|
321
|
-
|
|
522
|
+
const endAckPromise = new Promise((resolve) => {
|
|
523
|
+
endAckResolve = resolve;
|
|
322
524
|
});
|
|
323
525
|
conn.on("data", (data) => {
|
|
324
|
-
|
|
526
|
+
lastActivityTime = Date.now();
|
|
527
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
325
528
|
return;
|
|
326
529
|
}
|
|
530
|
+
if (!isP2PMessage(data)) return;
|
|
327
531
|
const msg = data;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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;
|
|
354
560
|
}
|
|
355
561
|
});
|
|
356
562
|
conn.on("open", async () => {
|
|
357
563
|
try {
|
|
358
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
|
+
}
|
|
359
593
|
conn.send({
|
|
360
594
|
t: "meta",
|
|
361
595
|
sessionId,
|
|
362
|
-
name:
|
|
363
|
-
size:
|
|
364
|
-
mime:
|
|
596
|
+
name: files[0].name,
|
|
597
|
+
size: files[0].size,
|
|
598
|
+
mime: files[0].type || "application/octet-stream",
|
|
599
|
+
...isMultiFile ? { fileIndex: 0 } : {}
|
|
365
600
|
});
|
|
366
|
-
const total = file.size;
|
|
367
601
|
const dc = conn._dc;
|
|
368
602
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
369
603
|
try {
|
|
@@ -375,56 +609,61 @@ async function startP2PSend(opts) {
|
|
|
375
609
|
if (isStopped()) return;
|
|
376
610
|
if (heartbeatIntervalMs > 0) {
|
|
377
611
|
heartbeatTimer = setInterval(() => {
|
|
378
|
-
if (state === "transferring" || state === "finishing") {
|
|
612
|
+
if (state === "transferring" || state === "finishing" || state === "awaiting_ack") {
|
|
379
613
|
try {
|
|
380
|
-
conn.send({ t: "ping" });
|
|
614
|
+
conn.send({ t: "ping", timestamp: Date.now() });
|
|
381
615
|
} catch {
|
|
382
616
|
}
|
|
383
617
|
}
|
|
384
618
|
}, heartbeatIntervalMs);
|
|
385
619
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
620
|
+
transitionTo("transferring");
|
|
621
|
+
transferEverStarted = true;
|
|
622
|
+
let overallSentBytes = 0;
|
|
623
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
624
|
+
const currentFile = files[fi];
|
|
625
|
+
if (isMultiFile && fi > 0) {
|
|
626
|
+
conn.send({
|
|
627
|
+
t: "meta",
|
|
628
|
+
sessionId,
|
|
629
|
+
name: currentFile.name,
|
|
630
|
+
size: currentFile.size,
|
|
631
|
+
mime: currentFile.type || "application/octet-stream",
|
|
632
|
+
fileIndex: fi
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
for (let offset = 0; offset < currentFile.size; offset += chunkSize) {
|
|
636
|
+
if (isStopped()) return;
|
|
637
|
+
const slice = currentFile.slice(offset, offset + chunkSize);
|
|
638
|
+
const buf = await slice.arrayBuffer();
|
|
639
|
+
if (isStopped()) return;
|
|
640
|
+
await sendChunk(conn, buf, offset, currentFile.size);
|
|
641
|
+
overallSentBytes += buf.byteLength;
|
|
642
|
+
reportProgress({ received: overallSentBytes, total: totalSize });
|
|
643
|
+
}
|
|
391
644
|
if (isStopped()) return;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
},
|
|
405
|
-
{ once: true }
|
|
406
|
-
);
|
|
407
|
-
} catch {
|
|
408
|
-
}
|
|
409
|
-
});
|
|
645
|
+
if (isMultiFile) {
|
|
646
|
+
const fileEndAckPromise = new Promise((resolve) => {
|
|
647
|
+
fileEndAckResolve = resolve;
|
|
648
|
+
});
|
|
649
|
+
conn.send({ t: "file_end", fileIndex: fi });
|
|
650
|
+
const feAck = await Promise.race([
|
|
651
|
+
fileEndAckPromise,
|
|
652
|
+
sleep(endAckTimeoutMs).then(() => null)
|
|
653
|
+
]);
|
|
654
|
+
if (isStopped()) return;
|
|
655
|
+
if (!feAck) {
|
|
656
|
+
throw new DropgateNetworkError(`Receiver did not confirm receipt of file ${fi + 1}/${files.length}.`);
|
|
410
657
|
}
|
|
411
658
|
}
|
|
412
659
|
}
|
|
413
660
|
if (isStopped()) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
const ackResult = await Promise.race([
|
|
418
|
-
ackPromise,
|
|
419
|
-
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
420
|
-
]);
|
|
661
|
+
transitionTo("finishing");
|
|
662
|
+
transitionTo("awaiting_ack");
|
|
663
|
+
const ackResult = await waitForEndAck(conn, endAckPromise);
|
|
421
664
|
if (isStopped()) return;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
const ackData = ackResult;
|
|
426
|
-
const ackTotal = Number(ackData.total) || file.size;
|
|
427
|
-
const ackReceived = Number(ackData.received) || 0;
|
|
665
|
+
const ackTotal = Number(ackResult.total) || totalSize;
|
|
666
|
+
const ackReceived = Number(ackResult.received) || 0;
|
|
428
667
|
if (ackTotal && ackReceived < ackTotal) {
|
|
429
668
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
430
669
|
}
|
|
@@ -442,14 +681,24 @@ async function startP2PSend(opts) {
|
|
|
442
681
|
cleanup();
|
|
443
682
|
return;
|
|
444
683
|
}
|
|
684
|
+
if (state === "awaiting_ack") {
|
|
685
|
+
setTimeout(() => {
|
|
686
|
+
if (state === "awaiting_ack") {
|
|
687
|
+
safeError(new DropgateNetworkError("Connection closed while awaiting confirmation."));
|
|
688
|
+
}
|
|
689
|
+
}, P2P_CLOSE_GRACE_PERIOD_MS);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
445
692
|
if (state === "transferring" || state === "finishing") {
|
|
446
|
-
|
|
693
|
+
transitionTo("cancelled");
|
|
447
694
|
onCancel?.({ cancelledBy: "receiver" });
|
|
448
695
|
cleanup();
|
|
449
696
|
} else {
|
|
450
697
|
activeConn = null;
|
|
451
698
|
state = "listening";
|
|
452
699
|
sentBytes = 0;
|
|
700
|
+
nextSeq = 0;
|
|
701
|
+
unackedChunks.clear();
|
|
453
702
|
onDisconnect?.();
|
|
454
703
|
}
|
|
455
704
|
});
|
|
@@ -469,6 +718,16 @@ async function startP2PSend(opts) {
|
|
|
469
718
|
}
|
|
470
719
|
|
|
471
720
|
// src/p2p/receive.ts
|
|
721
|
+
var ALLOWED_TRANSITIONS2 = {
|
|
722
|
+
initializing: ["connecting", "closed"],
|
|
723
|
+
connecting: ["handshaking", "closed", "cancelled"],
|
|
724
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
725
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
726
|
+
transferring: ["completed", "closed", "cancelled"],
|
|
727
|
+
completed: ["closed"],
|
|
728
|
+
cancelled: ["closed"],
|
|
729
|
+
closed: []
|
|
730
|
+
};
|
|
472
731
|
async function startP2PReceive(opts) {
|
|
473
732
|
const {
|
|
474
733
|
code,
|
|
@@ -485,6 +744,8 @@ async function startP2PReceive(opts) {
|
|
|
485
744
|
onMeta,
|
|
486
745
|
onData,
|
|
487
746
|
onProgress,
|
|
747
|
+
onFileStart,
|
|
748
|
+
onFileEnd,
|
|
488
749
|
onComplete,
|
|
489
750
|
onError,
|
|
490
751
|
onDisconnect,
|
|
@@ -522,11 +783,26 @@ async function startP2PReceive(opts) {
|
|
|
522
783
|
let total = 0;
|
|
523
784
|
let received = 0;
|
|
524
785
|
let currentSessionId = null;
|
|
525
|
-
let lastProgressSentAt = 0;
|
|
526
|
-
const progressIntervalMs = 120;
|
|
527
786
|
let writeQueue = Promise.resolve();
|
|
528
787
|
let watchdogTimer = null;
|
|
529
788
|
let activeConn = null;
|
|
789
|
+
let pendingChunk = null;
|
|
790
|
+
let fileList = null;
|
|
791
|
+
let currentFileReceived = 0;
|
|
792
|
+
let totalReceivedAllFiles = 0;
|
|
793
|
+
let expectedChunkSeq = 0;
|
|
794
|
+
let writeQueueDepth = 0;
|
|
795
|
+
const MAX_WRITE_QUEUE_DEPTH = 100;
|
|
796
|
+
const MAX_FILE_COUNT = 1e4;
|
|
797
|
+
const transitionTo = (newState) => {
|
|
798
|
+
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
799
|
+
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
state = newState;
|
|
803
|
+
return true;
|
|
804
|
+
};
|
|
805
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
530
806
|
const resetWatchdog = () => {
|
|
531
807
|
if (watchdogTimeoutMs <= 0) return;
|
|
532
808
|
if (watchdogTimer) {
|
|
@@ -546,15 +822,14 @@ async function startP2PReceive(opts) {
|
|
|
546
822
|
};
|
|
547
823
|
const safeError = (err) => {
|
|
548
824
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
549
|
-
|
|
825
|
+
transitionTo("closed");
|
|
550
826
|
onError?.(err);
|
|
551
827
|
cleanup();
|
|
552
828
|
};
|
|
553
829
|
const safeComplete = (completeData) => {
|
|
554
830
|
if (state !== "transferring") return;
|
|
555
|
-
|
|
831
|
+
transitionTo("completed");
|
|
556
832
|
onComplete?.(completeData);
|
|
557
|
-
cleanup();
|
|
558
833
|
};
|
|
559
834
|
const cleanup = () => {
|
|
560
835
|
clearWatchdog();
|
|
@@ -578,11 +853,15 @@ async function startP2PReceive(opts) {
|
|
|
578
853
|
}
|
|
579
854
|
const stop = () => {
|
|
580
855
|
if (state === "closed" || state === "cancelled") return;
|
|
856
|
+
if (state === "completed") {
|
|
857
|
+
cleanup();
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
581
860
|
const wasActive = state === "transferring";
|
|
582
|
-
|
|
861
|
+
transitionTo("cancelled");
|
|
583
862
|
try {
|
|
584
863
|
if (activeConn && activeConn.open) {
|
|
585
|
-
activeConn.send({ t: "cancelled",
|
|
864
|
+
activeConn.send({ t: "cancelled", reason: "Receiver cancelled the transfer." });
|
|
586
865
|
}
|
|
587
866
|
} catch {
|
|
588
867
|
}
|
|
@@ -591,23 +870,122 @@ async function startP2PReceive(opts) {
|
|
|
591
870
|
}
|
|
592
871
|
cleanup();
|
|
593
872
|
};
|
|
873
|
+
const sendChunkAck = (conn, seq) => {
|
|
874
|
+
try {
|
|
875
|
+
conn.send({ t: "chunk_ack", seq, received });
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
};
|
|
594
879
|
peer.on("error", (err) => {
|
|
595
880
|
safeError(err);
|
|
596
881
|
});
|
|
597
882
|
peer.on("open", () => {
|
|
598
|
-
|
|
883
|
+
transitionTo("connecting");
|
|
599
884
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
600
885
|
activeConn = conn;
|
|
601
886
|
conn.on("open", () => {
|
|
602
|
-
|
|
603
|
-
onStatus?.({ phase: "connected", message: "
|
|
887
|
+
transitionTo("handshaking");
|
|
888
|
+
onStatus?.({ phase: "connected", message: "Connected." });
|
|
889
|
+
conn.send({
|
|
890
|
+
t: "hello",
|
|
891
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
892
|
+
sessionId: ""
|
|
893
|
+
});
|
|
604
894
|
});
|
|
605
895
|
conn.on("data", async (data) => {
|
|
606
896
|
try {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
897
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
898
|
+
if (state !== "transferring") {
|
|
899
|
+
throw new DropgateValidationError(
|
|
900
|
+
"Received binary data before transfer was accepted. Possible malicious sender."
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
resetWatchdog();
|
|
904
|
+
if (writeQueueDepth >= MAX_WRITE_QUEUE_DEPTH) {
|
|
905
|
+
throw new DropgateNetworkError("Write queue overflow - receiver cannot keep up");
|
|
906
|
+
}
|
|
907
|
+
let bufPromise;
|
|
908
|
+
if (data instanceof ArrayBuffer) {
|
|
909
|
+
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
910
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
911
|
+
bufPromise = Promise.resolve(
|
|
912
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
913
|
+
);
|
|
914
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
915
|
+
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
916
|
+
} else {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
920
|
+
const expectedSize = pendingChunk?.size;
|
|
921
|
+
pendingChunk = null;
|
|
922
|
+
writeQueueDepth++;
|
|
923
|
+
writeQueue = writeQueue.then(async () => {
|
|
924
|
+
const buf = await bufPromise;
|
|
925
|
+
if (expectedSize !== void 0 && buf.byteLength !== expectedSize) {
|
|
926
|
+
throw new DropgateValidationError(
|
|
927
|
+
`Chunk size mismatch: expected ${expectedSize}, got ${buf.byteLength}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const newReceived = received + buf.byteLength;
|
|
931
|
+
if (total > 0 && newReceived > total) {
|
|
932
|
+
throw new DropgateValidationError(
|
|
933
|
+
`Received more data than expected: ${newReceived} > ${total}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
if (onData) {
|
|
937
|
+
await onData(buf);
|
|
938
|
+
}
|
|
939
|
+
received += buf.byteLength;
|
|
940
|
+
currentFileReceived += buf.byteLength;
|
|
941
|
+
const progressReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
942
|
+
const progressTotal = fileList ? fileList.totalSize : total;
|
|
943
|
+
const percent = progressTotal ? Math.min(100, progressReceived / progressTotal * 100) : 0;
|
|
944
|
+
if (!isStopped()) onProgress?.({ processedBytes: progressReceived, totalBytes: progressTotal, percent });
|
|
945
|
+
if (chunkSeq >= 0) {
|
|
946
|
+
sendChunkAck(conn, chunkSeq);
|
|
947
|
+
}
|
|
948
|
+
}).catch((err) => {
|
|
949
|
+
try {
|
|
950
|
+
conn.send({
|
|
951
|
+
t: "error",
|
|
952
|
+
message: err?.message || "Receiver write failed."
|
|
953
|
+
});
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
safeError(err);
|
|
957
|
+
}).finally(() => {
|
|
958
|
+
writeQueueDepth--;
|
|
959
|
+
});
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (!isP2PMessage(data)) return;
|
|
963
|
+
const msg = data;
|
|
964
|
+
switch (msg.t) {
|
|
965
|
+
case "hello":
|
|
966
|
+
currentSessionId = msg.sessionId || null;
|
|
967
|
+
transitionTo("negotiating");
|
|
968
|
+
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
969
|
+
break;
|
|
970
|
+
case "file_list": {
|
|
971
|
+
const fileListMsg = msg;
|
|
972
|
+
if (fileListMsg.fileCount > MAX_FILE_COUNT) {
|
|
973
|
+
throw new DropgateValidationError(`Too many files: ${fileListMsg.fileCount}`);
|
|
974
|
+
}
|
|
975
|
+
const sumSize = fileListMsg.files.reduce((sum, f) => sum + f.size, 0);
|
|
976
|
+
if (sumSize !== fileListMsg.totalSize) {
|
|
977
|
+
throw new DropgateValidationError(
|
|
978
|
+
`File list size mismatch: declared ${fileListMsg.totalSize}, actual sum ${sumSize}`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
fileList = fileListMsg;
|
|
982
|
+
total = fileListMsg.totalSize;
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
case "meta": {
|
|
986
|
+
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
611
989
|
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
612
990
|
try {
|
|
613
991
|
conn.send({ t: "error", message: "Busy with another session." });
|
|
@@ -619,40 +997,96 @@ async function startP2PReceive(opts) {
|
|
|
619
997
|
currentSessionId = msg.sessionId;
|
|
620
998
|
}
|
|
621
999
|
const name = String(msg.name || "file");
|
|
622
|
-
|
|
1000
|
+
const fileSize = Number(msg.size) || 0;
|
|
1001
|
+
const fi = msg.fileIndex;
|
|
1002
|
+
if (fileList && typeof fi === "number" && fi > 0) {
|
|
1003
|
+
currentFileReceived = 0;
|
|
1004
|
+
onFileStart?.({ fileIndex: fi, name, size: fileSize });
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
623
1007
|
received = 0;
|
|
1008
|
+
currentFileReceived = 0;
|
|
1009
|
+
totalReceivedAllFiles = 0;
|
|
1010
|
+
if (!fileList) {
|
|
1011
|
+
total = fileSize;
|
|
1012
|
+
}
|
|
624
1013
|
writeQueue = Promise.resolve();
|
|
625
1014
|
const sendReady = () => {
|
|
626
|
-
|
|
1015
|
+
transitionTo("transferring");
|
|
627
1016
|
resetWatchdog();
|
|
1017
|
+
if (fileList) {
|
|
1018
|
+
onFileStart?.({ fileIndex: 0, name, size: fileSize });
|
|
1019
|
+
}
|
|
628
1020
|
try {
|
|
629
1021
|
conn.send({ t: "ready" });
|
|
630
1022
|
} catch {
|
|
631
1023
|
}
|
|
632
1024
|
};
|
|
1025
|
+
const metaEvt = { name, total };
|
|
1026
|
+
if (fileList) {
|
|
1027
|
+
metaEvt.fileCount = fileList.fileCount;
|
|
1028
|
+
metaEvt.files = fileList.files.map((f) => ({ name: f.name, size: f.size }));
|
|
1029
|
+
metaEvt.totalSize = fileList.totalSize;
|
|
1030
|
+
}
|
|
633
1031
|
if (autoReady) {
|
|
634
|
-
|
|
635
|
-
|
|
1032
|
+
if (!isStopped()) {
|
|
1033
|
+
onMeta?.(metaEvt);
|
|
1034
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1035
|
+
}
|
|
636
1036
|
sendReady();
|
|
637
1037
|
} else {
|
|
638
|
-
|
|
639
|
-
|
|
1038
|
+
metaEvt.sendReady = sendReady;
|
|
1039
|
+
if (!isStopped()) {
|
|
1040
|
+
onMeta?.(metaEvt);
|
|
1041
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1042
|
+
}
|
|
640
1043
|
}
|
|
641
|
-
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
case "chunk": {
|
|
1047
|
+
const chunkMsg = msg;
|
|
1048
|
+
if (state !== "transferring") {
|
|
1049
|
+
throw new DropgateValidationError(
|
|
1050
|
+
"Received chunk message before transfer was accepted."
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
if (chunkMsg.seq !== expectedChunkSeq) {
|
|
1054
|
+
throw new DropgateValidationError(
|
|
1055
|
+
`Chunk sequence error: expected ${expectedChunkSeq}, got ${chunkMsg.seq}`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
expectedChunkSeq++;
|
|
1059
|
+
pendingChunk = chunkMsg;
|
|
1060
|
+
break;
|
|
642
1061
|
}
|
|
643
|
-
|
|
1062
|
+
case "ping":
|
|
644
1063
|
try {
|
|
645
|
-
conn.send({ t: "pong" });
|
|
1064
|
+
conn.send({ t: "pong", timestamp: Date.now() });
|
|
646
1065
|
} catch {
|
|
647
1066
|
}
|
|
648
|
-
|
|
1067
|
+
break;
|
|
1068
|
+
case "file_end": {
|
|
1069
|
+
clearWatchdog();
|
|
1070
|
+
await writeQueue;
|
|
1071
|
+
const feIdx = msg.fileIndex;
|
|
1072
|
+
onFileEnd?.({ fileIndex: feIdx, receivedBytes: currentFileReceived });
|
|
1073
|
+
try {
|
|
1074
|
+
conn.send({ t: "file_end_ack", fileIndex: feIdx, received: currentFileReceived, size: currentFileReceived });
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
totalReceivedAllFiles += currentFileReceived;
|
|
1078
|
+
currentFileReceived = 0;
|
|
1079
|
+
resetWatchdog();
|
|
1080
|
+
break;
|
|
649
1081
|
}
|
|
650
|
-
|
|
1082
|
+
case "end":
|
|
651
1083
|
clearWatchdog();
|
|
652
1084
|
await writeQueue;
|
|
653
|
-
|
|
1085
|
+
const finalReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
1086
|
+
const finalTotal = fileList ? fileList.totalSize : total;
|
|
1087
|
+
if (finalTotal && finalReceived < finalTotal) {
|
|
654
1088
|
const err = new DropgateNetworkError(
|
|
655
|
-
"Transfer ended before
|
|
1089
|
+
"Transfer ended before all data was received."
|
|
656
1090
|
);
|
|
657
1091
|
try {
|
|
658
1092
|
conn.send({ t: "error", message: err.message });
|
|
@@ -661,62 +1095,31 @@ async function startP2PReceive(opts) {
|
|
|
661
1095
|
throw err;
|
|
662
1096
|
}
|
|
663
1097
|
try {
|
|
664
|
-
conn.send({ t: "
|
|
1098
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
665
1099
|
} catch {
|
|
666
1100
|
}
|
|
667
|
-
safeComplete({ received, total });
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1101
|
+
safeComplete({ received: finalReceived, total: finalTotal });
|
|
1102
|
+
(async () => {
|
|
1103
|
+
for (let i = 0; i < 2; i++) {
|
|
1104
|
+
await sleep(P2P_END_ACK_RETRY_DELAY_MS);
|
|
1105
|
+
try {
|
|
1106
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
1107
|
+
} catch {
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
})().catch(() => {
|
|
1112
|
+
});
|
|
1113
|
+
break;
|
|
1114
|
+
case "error":
|
|
671
1115
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
672
|
-
|
|
673
|
-
if (msg.t === "cancelled") {
|
|
1116
|
+
case "cancelled":
|
|
674
1117
|
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
675
|
-
|
|
676
|
-
onCancel?.({ cancelledBy: "sender", message: msg.
|
|
1118
|
+
transitionTo("cancelled");
|
|
1119
|
+
onCancel?.({ cancelledBy: "sender", message: msg.reason });
|
|
677
1120
|
cleanup();
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
return;
|
|
1121
|
+
break;
|
|
681
1122
|
}
|
|
682
|
-
let bufPromise;
|
|
683
|
-
if (data instanceof ArrayBuffer) {
|
|
684
|
-
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
685
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
686
|
-
bufPromise = Promise.resolve(
|
|
687
|
-
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
688
|
-
);
|
|
689
|
-
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
690
|
-
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
691
|
-
} else {
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
writeQueue = writeQueue.then(async () => {
|
|
695
|
-
const buf = await bufPromise;
|
|
696
|
-
if (onData) {
|
|
697
|
-
await onData(buf);
|
|
698
|
-
}
|
|
699
|
-
received += buf.byteLength;
|
|
700
|
-
const percent = total ? Math.min(100, received / total * 100) : 0;
|
|
701
|
-
onProgress?.({ processedBytes: received, totalBytes: total, percent });
|
|
702
|
-
const now = Date.now();
|
|
703
|
-
if (received === total || now - lastProgressSentAt >= progressIntervalMs) {
|
|
704
|
-
lastProgressSentAt = now;
|
|
705
|
-
try {
|
|
706
|
-
conn.send({ t: "progress", received, total });
|
|
707
|
-
} catch {
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}).catch((err) => {
|
|
711
|
-
try {
|
|
712
|
-
conn.send({
|
|
713
|
-
t: "error",
|
|
714
|
-
message: err?.message || "Receiver write failed."
|
|
715
|
-
});
|
|
716
|
-
} catch {
|
|
717
|
-
}
|
|
718
|
-
safeError(err);
|
|
719
|
-
});
|
|
720
1123
|
} catch (err) {
|
|
721
1124
|
safeError(err);
|
|
722
1125
|
}
|
|
@@ -727,11 +1130,11 @@ async function startP2PReceive(opts) {
|
|
|
727
1130
|
return;
|
|
728
1131
|
}
|
|
729
1132
|
if (state === "transferring") {
|
|
730
|
-
|
|
1133
|
+
transitionTo("cancelled");
|
|
731
1134
|
onCancel?.({ cancelledBy: "sender" });
|
|
732
1135
|
cleanup();
|
|
733
1136
|
} else if (state === "negotiating") {
|
|
734
|
-
|
|
1137
|
+
transitionTo("closed");
|
|
735
1138
|
cleanup();
|
|
736
1139
|
onDisconnect?.();
|
|
737
1140
|
} else {
|
|
@@ -749,11 +1152,18 @@ async function startP2PReceive(opts) {
|
|
|
749
1152
|
};
|
|
750
1153
|
}
|
|
751
1154
|
export {
|
|
1155
|
+
P2P_CHUNK_SIZE,
|
|
1156
|
+
P2P_END_ACK_RETRIES,
|
|
1157
|
+
P2P_END_ACK_TIMEOUT_MS,
|
|
1158
|
+
P2P_MAX_UNACKED_CHUNKS,
|
|
1159
|
+
P2P_PROTOCOL_VERSION,
|
|
752
1160
|
buildPeerOptions,
|
|
753
1161
|
createPeerWithRetries,
|
|
754
1162
|
generateP2PCode,
|
|
755
1163
|
isLocalhostHostname,
|
|
756
1164
|
isP2PCodeLike,
|
|
1165
|
+
isP2PMessage,
|
|
1166
|
+
isProtocolCompatible,
|
|
757
1167
|
isSecureContextForP2P,
|
|
758
1168
|
resolvePeerConfig,
|
|
759
1169
|
startP2PReceive,
|