@dropgate/core 2.0.0-beta.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -15
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +385 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +182 -120
- package/dist/index.d.ts +182 -120
- package/dist/index.js +383 -138
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +268 -62
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +154 -92
- package/dist/p2p/index.d.ts +154 -92
- package/dist/p2p/index.js +267 -62
- package/dist/p2p/index.js.map +1 -1
- package/package.json +88 -88
package/dist/p2p/index.js
CHANGED
|
@@ -66,11 +66,11 @@ function isSecureContextForP2P(hostname, isSecureContext) {
|
|
|
66
66
|
return Boolean(isSecureContext) || isLocalhostHostname(hostname || "");
|
|
67
67
|
}
|
|
68
68
|
function generateP2PCode(cryptoObj) {
|
|
69
|
-
const
|
|
69
|
+
const crypto2 = cryptoObj || getDefaultCrypto();
|
|
70
70
|
const letters = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
71
|
-
if (
|
|
71
|
+
if (crypto2) {
|
|
72
72
|
const randomBytes = new Uint8Array(8);
|
|
73
|
-
|
|
73
|
+
crypto2.getRandomValues(randomBytes);
|
|
74
74
|
let letterPart = "";
|
|
75
75
|
for (let i = 0; i < 4; i++) {
|
|
76
76
|
letterPart += letters[randomBytes[i] % letters.length];
|
|
@@ -96,8 +96,14 @@ function isP2PCodeLike(code) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// src/p2p/helpers.ts
|
|
99
|
-
function
|
|
100
|
-
|
|
99
|
+
function resolvePeerConfig(userConfig, serverCaps) {
|
|
100
|
+
return {
|
|
101
|
+
path: userConfig.peerjsPath ?? serverCaps?.peerjsPath ?? "/peerjs",
|
|
102
|
+
iceServers: userConfig.iceServers ?? serverCaps?.iceServers ?? []
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function buildPeerOptions(config = {}) {
|
|
106
|
+
const { host, port, peerjsPath = "/peerjs", secure = false, iceServers = [] } = config;
|
|
101
107
|
const peerOpts = {
|
|
102
108
|
host,
|
|
103
109
|
path: peerjsPath,
|
|
@@ -139,6 +145,12 @@ async function createPeerWithRetries(opts) {
|
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
// src/p2p/send.ts
|
|
148
|
+
function generateSessionId() {
|
|
149
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
150
|
+
return crypto.randomUUID();
|
|
151
|
+
}
|
|
152
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
153
|
+
}
|
|
142
154
|
async function startP2PSend(opts) {
|
|
143
155
|
const {
|
|
144
156
|
file,
|
|
@@ -153,15 +165,16 @@ async function startP2PSend(opts) {
|
|
|
153
165
|
cryptoObj,
|
|
154
166
|
maxAttempts = 4,
|
|
155
167
|
chunkSize = 256 * 1024,
|
|
156
|
-
readyTimeoutMs = 8e3,
|
|
157
168
|
endAckTimeoutMs = 15e3,
|
|
158
169
|
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
159
170
|
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
171
|
+
heartbeatIntervalMs = 5e3,
|
|
160
172
|
onCode,
|
|
161
173
|
onStatus,
|
|
162
174
|
onProgress,
|
|
163
175
|
onComplete,
|
|
164
|
-
onError
|
|
176
|
+
onError,
|
|
177
|
+
onDisconnect
|
|
165
178
|
} = opts;
|
|
166
179
|
if (!file) {
|
|
167
180
|
throw new DropgateValidationError("File is missing.");
|
|
@@ -175,8 +188,10 @@ async function startP2PSend(opts) {
|
|
|
175
188
|
if (serverInfo && !p2pCaps?.enabled) {
|
|
176
189
|
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
177
190
|
}
|
|
178
|
-
const finalPath
|
|
179
|
-
|
|
191
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
192
|
+
{ peerjsPath, iceServers },
|
|
193
|
+
p2pCaps
|
|
194
|
+
);
|
|
180
195
|
const peerOpts = buildPeerOptions({
|
|
181
196
|
host,
|
|
182
197
|
port,
|
|
@@ -193,18 +208,37 @@ async function startP2PSend(opts) {
|
|
|
193
208
|
buildPeer,
|
|
194
209
|
onCode
|
|
195
210
|
});
|
|
196
|
-
|
|
211
|
+
const sessionId = generateSessionId();
|
|
212
|
+
let state = "listening";
|
|
197
213
|
let activeConn = null;
|
|
198
|
-
let
|
|
199
|
-
let
|
|
214
|
+
let sentBytes = 0;
|
|
215
|
+
let heartbeatTimer = null;
|
|
200
216
|
const reportProgress = (data) => {
|
|
201
217
|
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : file.size;
|
|
202
218
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
203
219
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
204
|
-
onProgress?.({
|
|
220
|
+
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
205
221
|
};
|
|
206
|
-
const
|
|
207
|
-
|
|
222
|
+
const safeError = (err) => {
|
|
223
|
+
if (state === "closed" || state === "completed") return;
|
|
224
|
+
state = "closed";
|
|
225
|
+
onError?.(err);
|
|
226
|
+
cleanup();
|
|
227
|
+
};
|
|
228
|
+
const safeComplete = () => {
|
|
229
|
+
if (state !== "finishing") return;
|
|
230
|
+
state = "completed";
|
|
231
|
+
onComplete?.();
|
|
232
|
+
cleanup();
|
|
233
|
+
};
|
|
234
|
+
const cleanup = () => {
|
|
235
|
+
if (heartbeatTimer) {
|
|
236
|
+
clearInterval(heartbeatTimer);
|
|
237
|
+
heartbeatTimer = null;
|
|
238
|
+
}
|
|
239
|
+
if (typeof window !== "undefined") {
|
|
240
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
241
|
+
}
|
|
208
242
|
try {
|
|
209
243
|
activeConn?.close();
|
|
210
244
|
} catch {
|
|
@@ -214,21 +248,59 @@ async function startP2PSend(opts) {
|
|
|
214
248
|
} catch {
|
|
215
249
|
}
|
|
216
250
|
};
|
|
251
|
+
const handleUnload = () => {
|
|
252
|
+
try {
|
|
253
|
+
activeConn?.send({ t: "error", message: "Sender closed the connection." });
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
stop();
|
|
257
|
+
};
|
|
258
|
+
if (typeof window !== "undefined") {
|
|
259
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
260
|
+
}
|
|
261
|
+
const stop = () => {
|
|
262
|
+
if (state === "closed") return;
|
|
263
|
+
state = "closed";
|
|
264
|
+
cleanup();
|
|
265
|
+
};
|
|
266
|
+
const isStopped = () => state === "closed";
|
|
217
267
|
peer.on("connection", (conn) => {
|
|
218
|
-
if (
|
|
268
|
+
if (state === "closed") return;
|
|
219
269
|
if (activeConn) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
const isOldConnOpen = activeConn.open !== false;
|
|
271
|
+
if (isOldConnOpen && state === "transferring") {
|
|
272
|
+
try {
|
|
273
|
+
conn.send({ t: "error", message: "Transfer already in progress." });
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
conn.close();
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
} else if (!isOldConnOpen) {
|
|
282
|
+
try {
|
|
283
|
+
activeConn.close();
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
activeConn = null;
|
|
287
|
+
state = "listening";
|
|
288
|
+
sentBytes = 0;
|
|
289
|
+
} else {
|
|
290
|
+
try {
|
|
291
|
+
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
conn.close();
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
227
299
|
}
|
|
228
|
-
return;
|
|
229
300
|
}
|
|
230
301
|
activeConn = conn;
|
|
231
|
-
|
|
302
|
+
state = "negotiating";
|
|
303
|
+
onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
232
304
|
let readyResolve = null;
|
|
233
305
|
let ackResolve = null;
|
|
234
306
|
const readyPromise = new Promise((resolve) => {
|
|
@@ -244,6 +316,7 @@ async function startP2PSend(opts) {
|
|
|
244
316
|
const msg = data;
|
|
245
317
|
if (!msg.t) return;
|
|
246
318
|
if (msg.t === "ready") {
|
|
319
|
+
onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
247
320
|
readyResolve?.();
|
|
248
321
|
return;
|
|
249
322
|
}
|
|
@@ -255,22 +328,23 @@ async function startP2PSend(opts) {
|
|
|
255
328
|
ackResolve?.(msg);
|
|
256
329
|
return;
|
|
257
330
|
}
|
|
331
|
+
if (msg.t === "pong") {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
258
334
|
if (msg.t === "error") {
|
|
259
|
-
|
|
260
|
-
stop();
|
|
335
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
261
336
|
}
|
|
262
337
|
});
|
|
263
338
|
conn.on("open", async () => {
|
|
264
339
|
try {
|
|
265
|
-
|
|
266
|
-
if (stopped) return;
|
|
340
|
+
if (isStopped()) return;
|
|
267
341
|
conn.send({
|
|
268
342
|
t: "meta",
|
|
343
|
+
sessionId,
|
|
269
344
|
name: file.name,
|
|
270
345
|
size: file.size,
|
|
271
346
|
mime: file.type || "application/octet-stream"
|
|
272
347
|
});
|
|
273
|
-
let sent = 0;
|
|
274
348
|
const total = file.size;
|
|
275
349
|
const dc = conn._dc;
|
|
276
350
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
@@ -279,13 +353,25 @@ async function startP2PSend(opts) {
|
|
|
279
353
|
} catch {
|
|
280
354
|
}
|
|
281
355
|
}
|
|
282
|
-
await
|
|
356
|
+
await readyPromise;
|
|
357
|
+
if (isStopped()) return;
|
|
358
|
+
if (heartbeatIntervalMs > 0) {
|
|
359
|
+
heartbeatTimer = setInterval(() => {
|
|
360
|
+
if (state === "transferring" || state === "finishing") {
|
|
361
|
+
try {
|
|
362
|
+
conn.send({ t: "ping" });
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}, heartbeatIntervalMs);
|
|
367
|
+
}
|
|
368
|
+
state = "transferring";
|
|
283
369
|
for (let offset = 0; offset < total; offset += chunkSize) {
|
|
284
|
-
if (
|
|
370
|
+
if (isStopped()) return;
|
|
285
371
|
const slice = file.slice(offset, offset + chunkSize);
|
|
286
372
|
const buf = await slice.arrayBuffer();
|
|
287
373
|
conn.send(buf);
|
|
288
|
-
|
|
374
|
+
sentBytes += buf.byteLength;
|
|
289
375
|
if (dc) {
|
|
290
376
|
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
291
377
|
await new Promise((resolve) => {
|
|
@@ -305,13 +391,15 @@ async function startP2PSend(opts) {
|
|
|
305
391
|
}
|
|
306
392
|
}
|
|
307
393
|
}
|
|
308
|
-
if (
|
|
394
|
+
if (isStopped()) return;
|
|
395
|
+
state = "finishing";
|
|
309
396
|
conn.send({ t: "end" });
|
|
310
397
|
const ackTimeoutMs = Number.isFinite(endAckTimeoutMs) ? Math.max(endAckTimeoutMs, Math.ceil(file.size / (1024 * 1024)) * 1e3) : null;
|
|
311
398
|
const ackResult = await Promise.race([
|
|
312
399
|
ackPromise,
|
|
313
400
|
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
314
401
|
]);
|
|
402
|
+
if (isStopped()) return;
|
|
315
403
|
if (!ackResult || typeof ackResult !== "object") {
|
|
316
404
|
throw new DropgateNetworkError("Receiver did not confirm completion.");
|
|
317
405
|
}
|
|
@@ -322,29 +410,43 @@ async function startP2PSend(opts) {
|
|
|
322
410
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
323
411
|
}
|
|
324
412
|
reportProgress({ received: ackReceived || ackTotal, total: ackTotal });
|
|
325
|
-
|
|
326
|
-
transferActive = false;
|
|
327
|
-
onComplete?.();
|
|
328
|
-
stop();
|
|
413
|
+
safeComplete();
|
|
329
414
|
} catch (err) {
|
|
330
|
-
|
|
331
|
-
stop();
|
|
415
|
+
safeError(err);
|
|
332
416
|
}
|
|
333
417
|
});
|
|
334
418
|
conn.on("error", (err) => {
|
|
335
|
-
|
|
336
|
-
stop();
|
|
419
|
+
safeError(err);
|
|
337
420
|
});
|
|
338
421
|
conn.on("close", () => {
|
|
339
|
-
if (
|
|
340
|
-
|
|
422
|
+
if (state === "closed" || state === "completed") {
|
|
423
|
+
cleanup();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (state === "transferring" || state === "finishing") {
|
|
427
|
+
safeError(
|
|
341
428
|
new DropgateNetworkError("Receiver disconnected before transfer completed.")
|
|
342
429
|
);
|
|
430
|
+
} else {
|
|
431
|
+
activeConn = null;
|
|
432
|
+
state = "listening";
|
|
433
|
+
sentBytes = 0;
|
|
434
|
+
onDisconnect?.();
|
|
343
435
|
}
|
|
344
|
-
stop();
|
|
345
436
|
});
|
|
346
437
|
});
|
|
347
|
-
return {
|
|
438
|
+
return {
|
|
439
|
+
peer,
|
|
440
|
+
code,
|
|
441
|
+
sessionId,
|
|
442
|
+
stop,
|
|
443
|
+
getStatus: () => state,
|
|
444
|
+
getBytesSent: () => sentBytes,
|
|
445
|
+
getConnectedPeerId: () => {
|
|
446
|
+
if (!activeConn) return null;
|
|
447
|
+
return activeConn.peer || null;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
348
450
|
}
|
|
349
451
|
|
|
350
452
|
// src/p2p/receive.ts
|
|
@@ -358,6 +460,8 @@ async function startP2PReceive(opts) {
|
|
|
358
460
|
peerjsPath,
|
|
359
461
|
secure = false,
|
|
360
462
|
iceServers,
|
|
463
|
+
autoReady = true,
|
|
464
|
+
watchdogTimeoutMs = 15e3,
|
|
361
465
|
onStatus,
|
|
362
466
|
onMeta,
|
|
363
467
|
onData,
|
|
@@ -382,8 +486,10 @@ async function startP2PReceive(opts) {
|
|
|
382
486
|
if (!isP2PCodeLike(normalizedCode)) {
|
|
383
487
|
throw new DropgateValidationError("Invalid direct transfer code.");
|
|
384
488
|
}
|
|
385
|
-
const finalPath
|
|
386
|
-
|
|
489
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
490
|
+
{ peerjsPath, iceServers },
|
|
491
|
+
p2pCaps
|
|
492
|
+
);
|
|
387
493
|
const peerOpts = buildPeerOptions({
|
|
388
494
|
host,
|
|
389
495
|
port,
|
|
@@ -392,44 +498,127 @@ async function startP2PReceive(opts) {
|
|
|
392
498
|
iceServers: finalIceServers
|
|
393
499
|
});
|
|
394
500
|
const peer = new Peer(void 0, peerOpts);
|
|
501
|
+
let state = "initializing";
|
|
395
502
|
let total = 0;
|
|
396
503
|
let received = 0;
|
|
504
|
+
let currentSessionId = null;
|
|
397
505
|
let lastProgressSentAt = 0;
|
|
398
506
|
const progressIntervalMs = 120;
|
|
399
507
|
let writeQueue = Promise.resolve();
|
|
400
|
-
|
|
508
|
+
let watchdogTimer = null;
|
|
509
|
+
let activeConn = null;
|
|
510
|
+
const resetWatchdog = () => {
|
|
511
|
+
if (watchdogTimeoutMs <= 0) return;
|
|
512
|
+
if (watchdogTimer) {
|
|
513
|
+
clearTimeout(watchdogTimer);
|
|
514
|
+
}
|
|
515
|
+
watchdogTimer = setTimeout(() => {
|
|
516
|
+
if (state === "transferring") {
|
|
517
|
+
safeError(new DropgateNetworkError("Connection timed out (no data received)."));
|
|
518
|
+
}
|
|
519
|
+
}, watchdogTimeoutMs);
|
|
520
|
+
};
|
|
521
|
+
const clearWatchdog = () => {
|
|
522
|
+
if (watchdogTimer) {
|
|
523
|
+
clearTimeout(watchdogTimer);
|
|
524
|
+
watchdogTimer = null;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const safeError = (err) => {
|
|
528
|
+
if (state === "closed" || state === "completed") return;
|
|
529
|
+
state = "closed";
|
|
530
|
+
onError?.(err);
|
|
531
|
+
cleanup();
|
|
532
|
+
};
|
|
533
|
+
const safeComplete = (completeData) => {
|
|
534
|
+
if (state !== "transferring") return;
|
|
535
|
+
state = "completed";
|
|
536
|
+
onComplete?.(completeData);
|
|
537
|
+
cleanup();
|
|
538
|
+
};
|
|
539
|
+
const cleanup = () => {
|
|
540
|
+
clearWatchdog();
|
|
541
|
+
if (typeof window !== "undefined") {
|
|
542
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
543
|
+
}
|
|
401
544
|
try {
|
|
402
545
|
peer.destroy();
|
|
403
546
|
} catch {
|
|
404
547
|
}
|
|
405
548
|
};
|
|
406
|
-
|
|
407
|
-
|
|
549
|
+
const handleUnload = () => {
|
|
550
|
+
try {
|
|
551
|
+
activeConn?.send({ t: "error", message: "Receiver closed the connection." });
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
408
554
|
stop();
|
|
555
|
+
};
|
|
556
|
+
if (typeof window !== "undefined") {
|
|
557
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
558
|
+
}
|
|
559
|
+
const stop = () => {
|
|
560
|
+
if (state === "closed") return;
|
|
561
|
+
state = "closed";
|
|
562
|
+
cleanup();
|
|
563
|
+
};
|
|
564
|
+
peer.on("error", (err) => {
|
|
565
|
+
safeError(err);
|
|
409
566
|
});
|
|
410
567
|
peer.on("open", () => {
|
|
568
|
+
state = "connecting";
|
|
411
569
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
570
|
+
activeConn = conn;
|
|
412
571
|
conn.on("open", () => {
|
|
572
|
+
state = "negotiating";
|
|
413
573
|
onStatus?.({ phase: "connected", message: "Waiting for file details..." });
|
|
414
574
|
});
|
|
415
575
|
conn.on("data", async (data) => {
|
|
416
576
|
try {
|
|
577
|
+
resetWatchdog();
|
|
417
578
|
if (data && typeof data === "object" && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
|
|
418
579
|
const msg = data;
|
|
419
580
|
if (msg.t === "meta") {
|
|
581
|
+
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
582
|
+
try {
|
|
583
|
+
conn.send({ t: "error", message: "Busy with another session." });
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (msg.sessionId) {
|
|
589
|
+
currentSessionId = msg.sessionId;
|
|
590
|
+
}
|
|
420
591
|
const name = String(msg.name || "file");
|
|
421
592
|
total = Number(msg.size) || 0;
|
|
422
593
|
received = 0;
|
|
423
594
|
writeQueue = Promise.resolve();
|
|
424
|
-
|
|
425
|
-
|
|
595
|
+
const sendReady = () => {
|
|
596
|
+
state = "transferring";
|
|
597
|
+
resetWatchdog();
|
|
598
|
+
try {
|
|
599
|
+
conn.send({ t: "ready" });
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
if (autoReady) {
|
|
604
|
+
onMeta?.({ name, total });
|
|
605
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
606
|
+
sendReady();
|
|
607
|
+
} else {
|
|
608
|
+
onMeta?.({ name, total, sendReady });
|
|
609
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (msg.t === "ping") {
|
|
426
614
|
try {
|
|
427
|
-
conn.send({ t: "
|
|
615
|
+
conn.send({ t: "pong" });
|
|
428
616
|
} catch {
|
|
429
617
|
}
|
|
430
618
|
return;
|
|
431
619
|
}
|
|
432
620
|
if (msg.t === "end") {
|
|
621
|
+
clearWatchdog();
|
|
433
622
|
await writeQueue;
|
|
434
623
|
if (total && received < total) {
|
|
435
624
|
const err = new DropgateNetworkError(
|
|
@@ -441,11 +630,11 @@ async function startP2PReceive(opts) {
|
|
|
441
630
|
}
|
|
442
631
|
throw err;
|
|
443
632
|
}
|
|
444
|
-
onComplete?.({ received, total });
|
|
445
633
|
try {
|
|
446
634
|
conn.send({ t: "ack", phase: "end", received, total });
|
|
447
635
|
} catch {
|
|
448
636
|
}
|
|
637
|
+
safeComplete({ received, total });
|
|
449
638
|
return;
|
|
450
639
|
}
|
|
451
640
|
if (msg.t === "error") {
|
|
@@ -472,7 +661,7 @@ async function startP2PReceive(opts) {
|
|
|
472
661
|
}
|
|
473
662
|
received += buf.byteLength;
|
|
474
663
|
const percent = total ? Math.min(100, received / total * 100) : 0;
|
|
475
|
-
onProgress?.({ received, total, percent });
|
|
664
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent });
|
|
476
665
|
const now = Date.now();
|
|
477
666
|
if (received === total || now - lastProgressSentAt >= progressIntervalMs) {
|
|
478
667
|
lastProgressSentAt = now;
|
|
@@ -489,21 +678,36 @@ async function startP2PReceive(opts) {
|
|
|
489
678
|
});
|
|
490
679
|
} catch {
|
|
491
680
|
}
|
|
492
|
-
|
|
493
|
-
stop();
|
|
681
|
+
safeError(err);
|
|
494
682
|
});
|
|
495
683
|
} catch (err) {
|
|
496
|
-
|
|
497
|
-
stop();
|
|
684
|
+
safeError(err);
|
|
498
685
|
}
|
|
499
686
|
});
|
|
500
687
|
conn.on("close", () => {
|
|
501
|
-
if (
|
|
688
|
+
if (state === "closed" || state === "completed") {
|
|
689
|
+
cleanup();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (state === "transferring") {
|
|
693
|
+
safeError(new DropgateNetworkError("Sender disconnected during transfer."));
|
|
694
|
+
} else if (state === "negotiating") {
|
|
695
|
+
state = "closed";
|
|
696
|
+
cleanup();
|
|
502
697
|
onDisconnect?.();
|
|
698
|
+
} else {
|
|
699
|
+
safeError(new DropgateNetworkError("Sender disconnected before file details were received."));
|
|
503
700
|
}
|
|
504
701
|
});
|
|
505
702
|
});
|
|
506
|
-
return {
|
|
703
|
+
return {
|
|
704
|
+
peer,
|
|
705
|
+
stop,
|
|
706
|
+
getStatus: () => state,
|
|
707
|
+
getBytesReceived: () => received,
|
|
708
|
+
getTotalBytes: () => total,
|
|
709
|
+
getSessionId: () => currentSessionId
|
|
710
|
+
};
|
|
507
711
|
}
|
|
508
712
|
export {
|
|
509
713
|
buildPeerOptions,
|
|
@@ -512,6 +716,7 @@ export {
|
|
|
512
716
|
isLocalhostHostname,
|
|
513
717
|
isP2PCodeLike,
|
|
514
718
|
isSecureContextForP2P,
|
|
719
|
+
resolvePeerConfig,
|
|
515
720
|
startP2PReceive,
|
|
516
721
|
startP2PSend
|
|
517
722
|
};
|