@dropgate/core 2.0.0-beta.2 → 2.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -15
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +639 -303
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -121
- package/dist/index.d.ts +208 -121
- package/dist/index.js +637 -303
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +310 -65
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +165 -92
- package/dist/p2p/index.d.ts +165 -92
- package/dist/p2p/index.js +309 -65
- 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,17 @@ 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,
|
|
178
|
+
onCancel
|
|
165
179
|
} = opts;
|
|
166
180
|
if (!file) {
|
|
167
181
|
throw new DropgateValidationError("File is missing.");
|
|
@@ -175,8 +189,10 @@ async function startP2PSend(opts) {
|
|
|
175
189
|
if (serverInfo && !p2pCaps?.enabled) {
|
|
176
190
|
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
177
191
|
}
|
|
178
|
-
const finalPath
|
|
179
|
-
|
|
192
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
193
|
+
{ peerjsPath, iceServers },
|
|
194
|
+
p2pCaps
|
|
195
|
+
);
|
|
180
196
|
const peerOpts = buildPeerOptions({
|
|
181
197
|
host,
|
|
182
198
|
port,
|
|
@@ -193,18 +209,37 @@ async function startP2PSend(opts) {
|
|
|
193
209
|
buildPeer,
|
|
194
210
|
onCode
|
|
195
211
|
});
|
|
196
|
-
|
|
212
|
+
const sessionId = generateSessionId();
|
|
213
|
+
let state = "listening";
|
|
197
214
|
let activeConn = null;
|
|
198
|
-
let
|
|
199
|
-
let
|
|
215
|
+
let sentBytes = 0;
|
|
216
|
+
let heartbeatTimer = null;
|
|
200
217
|
const reportProgress = (data) => {
|
|
201
218
|
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : file.size;
|
|
202
219
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
203
220
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
204
|
-
onProgress?.({
|
|
221
|
+
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
205
222
|
};
|
|
206
|
-
const
|
|
207
|
-
|
|
223
|
+
const safeError = (err) => {
|
|
224
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
225
|
+
state = "closed";
|
|
226
|
+
onError?.(err);
|
|
227
|
+
cleanup();
|
|
228
|
+
};
|
|
229
|
+
const safeComplete = () => {
|
|
230
|
+
if (state !== "finishing") return;
|
|
231
|
+
state = "completed";
|
|
232
|
+
onComplete?.();
|
|
233
|
+
cleanup();
|
|
234
|
+
};
|
|
235
|
+
const cleanup = () => {
|
|
236
|
+
if (heartbeatTimer) {
|
|
237
|
+
clearInterval(heartbeatTimer);
|
|
238
|
+
heartbeatTimer = null;
|
|
239
|
+
}
|
|
240
|
+
if (typeof window !== "undefined") {
|
|
241
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
242
|
+
}
|
|
208
243
|
try {
|
|
209
244
|
activeConn?.close();
|
|
210
245
|
} catch {
|
|
@@ -214,21 +249,69 @@ async function startP2PSend(opts) {
|
|
|
214
249
|
} catch {
|
|
215
250
|
}
|
|
216
251
|
};
|
|
252
|
+
const handleUnload = () => {
|
|
253
|
+
try {
|
|
254
|
+
activeConn?.send({ t: "error", message: "Sender closed the connection." });
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
stop();
|
|
258
|
+
};
|
|
259
|
+
if (typeof window !== "undefined") {
|
|
260
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
261
|
+
}
|
|
262
|
+
const stop = () => {
|
|
263
|
+
if (state === "closed" || state === "cancelled") return;
|
|
264
|
+
const wasActive = state === "transferring" || state === "finishing";
|
|
265
|
+
state = "cancelled";
|
|
266
|
+
try {
|
|
267
|
+
if (activeConn && activeConn.open) {
|
|
268
|
+
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
if (wasActive && onCancel) {
|
|
273
|
+
onCancel({ cancelledBy: "sender" });
|
|
274
|
+
}
|
|
275
|
+
cleanup();
|
|
276
|
+
};
|
|
277
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
217
278
|
peer.on("connection", (conn) => {
|
|
218
|
-
if (
|
|
279
|
+
if (state === "closed") return;
|
|
219
280
|
if (activeConn) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
281
|
+
const isOldConnOpen = activeConn.open !== false;
|
|
282
|
+
if (isOldConnOpen && state === "transferring") {
|
|
283
|
+
try {
|
|
284
|
+
conn.send({ t: "error", message: "Transfer already in progress." });
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
conn.close();
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
} else if (!isOldConnOpen) {
|
|
293
|
+
try {
|
|
294
|
+
activeConn.close();
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
activeConn = null;
|
|
298
|
+
state = "listening";
|
|
299
|
+
sentBytes = 0;
|
|
300
|
+
} else {
|
|
301
|
+
try {
|
|
302
|
+
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
conn.close();
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
227
310
|
}
|
|
228
|
-
return;
|
|
229
311
|
}
|
|
230
312
|
activeConn = conn;
|
|
231
|
-
|
|
313
|
+
state = "negotiating";
|
|
314
|
+
onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
232
315
|
let readyResolve = null;
|
|
233
316
|
let ackResolve = null;
|
|
234
317
|
const readyPromise = new Promise((resolve) => {
|
|
@@ -244,6 +327,7 @@ async function startP2PSend(opts) {
|
|
|
244
327
|
const msg = data;
|
|
245
328
|
if (!msg.t) return;
|
|
246
329
|
if (msg.t === "ready") {
|
|
330
|
+
onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
247
331
|
readyResolve?.();
|
|
248
332
|
return;
|
|
249
333
|
}
|
|
@@ -255,22 +339,30 @@ async function startP2PSend(opts) {
|
|
|
255
339
|
ackResolve?.(msg);
|
|
256
340
|
return;
|
|
257
341
|
}
|
|
342
|
+
if (msg.t === "pong") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
258
345
|
if (msg.t === "error") {
|
|
259
|
-
|
|
260
|
-
|
|
346
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (msg.t === "cancelled") {
|
|
350
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
351
|
+
state = "cancelled";
|
|
352
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.message });
|
|
353
|
+
cleanup();
|
|
261
354
|
}
|
|
262
355
|
});
|
|
263
356
|
conn.on("open", async () => {
|
|
264
357
|
try {
|
|
265
|
-
|
|
266
|
-
if (stopped) return;
|
|
358
|
+
if (isStopped()) return;
|
|
267
359
|
conn.send({
|
|
268
360
|
t: "meta",
|
|
361
|
+
sessionId,
|
|
269
362
|
name: file.name,
|
|
270
363
|
size: file.size,
|
|
271
364
|
mime: file.type || "application/octet-stream"
|
|
272
365
|
});
|
|
273
|
-
let sent = 0;
|
|
274
366
|
const total = file.size;
|
|
275
367
|
const dc = conn._dc;
|
|
276
368
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
@@ -279,13 +371,26 @@ async function startP2PSend(opts) {
|
|
|
279
371
|
} catch {
|
|
280
372
|
}
|
|
281
373
|
}
|
|
282
|
-
await
|
|
374
|
+
await readyPromise;
|
|
375
|
+
if (isStopped()) return;
|
|
376
|
+
if (heartbeatIntervalMs > 0) {
|
|
377
|
+
heartbeatTimer = setInterval(() => {
|
|
378
|
+
if (state === "transferring" || state === "finishing") {
|
|
379
|
+
try {
|
|
380
|
+
conn.send({ t: "ping" });
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}, heartbeatIntervalMs);
|
|
385
|
+
}
|
|
386
|
+
state = "transferring";
|
|
283
387
|
for (let offset = 0; offset < total; offset += chunkSize) {
|
|
284
|
-
if (
|
|
388
|
+
if (isStopped()) return;
|
|
285
389
|
const slice = file.slice(offset, offset + chunkSize);
|
|
286
390
|
const buf = await slice.arrayBuffer();
|
|
391
|
+
if (isStopped()) return;
|
|
287
392
|
conn.send(buf);
|
|
288
|
-
|
|
393
|
+
sentBytes += buf.byteLength;
|
|
289
394
|
if (dc) {
|
|
290
395
|
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
291
396
|
await new Promise((resolve) => {
|
|
@@ -305,13 +410,15 @@ async function startP2PSend(opts) {
|
|
|
305
410
|
}
|
|
306
411
|
}
|
|
307
412
|
}
|
|
308
|
-
if (
|
|
413
|
+
if (isStopped()) return;
|
|
414
|
+
state = "finishing";
|
|
309
415
|
conn.send({ t: "end" });
|
|
310
416
|
const ackTimeoutMs = Number.isFinite(endAckTimeoutMs) ? Math.max(endAckTimeoutMs, Math.ceil(file.size / (1024 * 1024)) * 1e3) : null;
|
|
311
417
|
const ackResult = await Promise.race([
|
|
312
418
|
ackPromise,
|
|
313
419
|
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
314
420
|
]);
|
|
421
|
+
if (isStopped()) return;
|
|
315
422
|
if (!ackResult || typeof ackResult !== "object") {
|
|
316
423
|
throw new DropgateNetworkError("Receiver did not confirm completion.");
|
|
317
424
|
}
|
|
@@ -322,29 +429,43 @@ async function startP2PSend(opts) {
|
|
|
322
429
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
323
430
|
}
|
|
324
431
|
reportProgress({ received: ackReceived || ackTotal, total: ackTotal });
|
|
325
|
-
|
|
326
|
-
transferActive = false;
|
|
327
|
-
onComplete?.();
|
|
328
|
-
stop();
|
|
432
|
+
safeComplete();
|
|
329
433
|
} catch (err) {
|
|
330
|
-
|
|
331
|
-
stop();
|
|
434
|
+
safeError(err);
|
|
332
435
|
}
|
|
333
436
|
});
|
|
334
437
|
conn.on("error", (err) => {
|
|
335
|
-
|
|
336
|
-
stop();
|
|
438
|
+
safeError(err);
|
|
337
439
|
});
|
|
338
440
|
conn.on("close", () => {
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
441
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
442
|
+
cleanup();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (state === "transferring" || state === "finishing") {
|
|
446
|
+
state = "cancelled";
|
|
447
|
+
onCancel?.({ cancelledBy: "receiver" });
|
|
448
|
+
cleanup();
|
|
449
|
+
} else {
|
|
450
|
+
activeConn = null;
|
|
451
|
+
state = "listening";
|
|
452
|
+
sentBytes = 0;
|
|
453
|
+
onDisconnect?.();
|
|
343
454
|
}
|
|
344
|
-
stop();
|
|
345
455
|
});
|
|
346
456
|
});
|
|
347
|
-
return {
|
|
457
|
+
return {
|
|
458
|
+
peer,
|
|
459
|
+
code,
|
|
460
|
+
sessionId,
|
|
461
|
+
stop,
|
|
462
|
+
getStatus: () => state,
|
|
463
|
+
getBytesSent: () => sentBytes,
|
|
464
|
+
getConnectedPeerId: () => {
|
|
465
|
+
if (!activeConn) return null;
|
|
466
|
+
return activeConn.peer || null;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
348
469
|
}
|
|
349
470
|
|
|
350
471
|
// src/p2p/receive.ts
|
|
@@ -358,13 +479,16 @@ async function startP2PReceive(opts) {
|
|
|
358
479
|
peerjsPath,
|
|
359
480
|
secure = false,
|
|
360
481
|
iceServers,
|
|
482
|
+
autoReady = true,
|
|
483
|
+
watchdogTimeoutMs = 15e3,
|
|
361
484
|
onStatus,
|
|
362
485
|
onMeta,
|
|
363
486
|
onData,
|
|
364
487
|
onProgress,
|
|
365
488
|
onComplete,
|
|
366
489
|
onError,
|
|
367
|
-
onDisconnect
|
|
490
|
+
onDisconnect,
|
|
491
|
+
onCancel
|
|
368
492
|
} = opts;
|
|
369
493
|
if (!code) {
|
|
370
494
|
throw new DropgateValidationError("No sharing code was provided.");
|
|
@@ -382,8 +506,10 @@ async function startP2PReceive(opts) {
|
|
|
382
506
|
if (!isP2PCodeLike(normalizedCode)) {
|
|
383
507
|
throw new DropgateValidationError("Invalid direct transfer code.");
|
|
384
508
|
}
|
|
385
|
-
const finalPath
|
|
386
|
-
|
|
509
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
510
|
+
{ peerjsPath, iceServers },
|
|
511
|
+
p2pCaps
|
|
512
|
+
);
|
|
387
513
|
const peerOpts = buildPeerOptions({
|
|
388
514
|
host,
|
|
389
515
|
port,
|
|
@@ -392,44 +518,137 @@ async function startP2PReceive(opts) {
|
|
|
392
518
|
iceServers: finalIceServers
|
|
393
519
|
});
|
|
394
520
|
const peer = new Peer(void 0, peerOpts);
|
|
521
|
+
let state = "initializing";
|
|
395
522
|
let total = 0;
|
|
396
523
|
let received = 0;
|
|
524
|
+
let currentSessionId = null;
|
|
397
525
|
let lastProgressSentAt = 0;
|
|
398
526
|
const progressIntervalMs = 120;
|
|
399
527
|
let writeQueue = Promise.resolve();
|
|
400
|
-
|
|
528
|
+
let watchdogTimer = null;
|
|
529
|
+
let activeConn = null;
|
|
530
|
+
const resetWatchdog = () => {
|
|
531
|
+
if (watchdogTimeoutMs <= 0) return;
|
|
532
|
+
if (watchdogTimer) {
|
|
533
|
+
clearTimeout(watchdogTimer);
|
|
534
|
+
}
|
|
535
|
+
watchdogTimer = setTimeout(() => {
|
|
536
|
+
if (state === "transferring") {
|
|
537
|
+
safeError(new DropgateNetworkError("Connection timed out (no data received)."));
|
|
538
|
+
}
|
|
539
|
+
}, watchdogTimeoutMs);
|
|
540
|
+
};
|
|
541
|
+
const clearWatchdog = () => {
|
|
542
|
+
if (watchdogTimer) {
|
|
543
|
+
clearTimeout(watchdogTimer);
|
|
544
|
+
watchdogTimer = null;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const safeError = (err) => {
|
|
548
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
549
|
+
state = "closed";
|
|
550
|
+
onError?.(err);
|
|
551
|
+
cleanup();
|
|
552
|
+
};
|
|
553
|
+
const safeComplete = (completeData) => {
|
|
554
|
+
if (state !== "transferring") return;
|
|
555
|
+
state = "completed";
|
|
556
|
+
onComplete?.(completeData);
|
|
557
|
+
cleanup();
|
|
558
|
+
};
|
|
559
|
+
const cleanup = () => {
|
|
560
|
+
clearWatchdog();
|
|
561
|
+
if (typeof window !== "undefined") {
|
|
562
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
563
|
+
}
|
|
401
564
|
try {
|
|
402
565
|
peer.destroy();
|
|
403
566
|
} catch {
|
|
404
567
|
}
|
|
405
568
|
};
|
|
406
|
-
|
|
407
|
-
|
|
569
|
+
const handleUnload = () => {
|
|
570
|
+
try {
|
|
571
|
+
activeConn?.send({ t: "error", message: "Receiver closed the connection." });
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
408
574
|
stop();
|
|
575
|
+
};
|
|
576
|
+
if (typeof window !== "undefined") {
|
|
577
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
578
|
+
}
|
|
579
|
+
const stop = () => {
|
|
580
|
+
if (state === "closed" || state === "cancelled") return;
|
|
581
|
+
const wasActive = state === "transferring";
|
|
582
|
+
state = "cancelled";
|
|
583
|
+
try {
|
|
584
|
+
if (activeConn && activeConn.open) {
|
|
585
|
+
activeConn.send({ t: "cancelled", message: "Receiver cancelled the transfer." });
|
|
586
|
+
}
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
if (wasActive && onCancel) {
|
|
590
|
+
onCancel({ cancelledBy: "receiver" });
|
|
591
|
+
}
|
|
592
|
+
cleanup();
|
|
593
|
+
};
|
|
594
|
+
peer.on("error", (err) => {
|
|
595
|
+
safeError(err);
|
|
409
596
|
});
|
|
410
597
|
peer.on("open", () => {
|
|
598
|
+
state = "connecting";
|
|
411
599
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
600
|
+
activeConn = conn;
|
|
412
601
|
conn.on("open", () => {
|
|
602
|
+
state = "negotiating";
|
|
413
603
|
onStatus?.({ phase: "connected", message: "Waiting for file details..." });
|
|
414
604
|
});
|
|
415
605
|
conn.on("data", async (data) => {
|
|
416
606
|
try {
|
|
607
|
+
resetWatchdog();
|
|
417
608
|
if (data && typeof data === "object" && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
|
|
418
609
|
const msg = data;
|
|
419
610
|
if (msg.t === "meta") {
|
|
611
|
+
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
612
|
+
try {
|
|
613
|
+
conn.send({ t: "error", message: "Busy with another session." });
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (msg.sessionId) {
|
|
619
|
+
currentSessionId = msg.sessionId;
|
|
620
|
+
}
|
|
420
621
|
const name = String(msg.name || "file");
|
|
421
622
|
total = Number(msg.size) || 0;
|
|
422
623
|
received = 0;
|
|
423
624
|
writeQueue = Promise.resolve();
|
|
424
|
-
|
|
425
|
-
|
|
625
|
+
const sendReady = () => {
|
|
626
|
+
state = "transferring";
|
|
627
|
+
resetWatchdog();
|
|
628
|
+
try {
|
|
629
|
+
conn.send({ t: "ready" });
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
if (autoReady) {
|
|
634
|
+
onMeta?.({ name, total });
|
|
635
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
636
|
+
sendReady();
|
|
637
|
+
} else {
|
|
638
|
+
onMeta?.({ name, total, sendReady });
|
|
639
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (msg.t === "ping") {
|
|
426
644
|
try {
|
|
427
|
-
conn.send({ t: "
|
|
645
|
+
conn.send({ t: "pong" });
|
|
428
646
|
} catch {
|
|
429
647
|
}
|
|
430
648
|
return;
|
|
431
649
|
}
|
|
432
650
|
if (msg.t === "end") {
|
|
651
|
+
clearWatchdog();
|
|
433
652
|
await writeQueue;
|
|
434
653
|
if (total && received < total) {
|
|
435
654
|
const err = new DropgateNetworkError(
|
|
@@ -441,16 +660,23 @@ async function startP2PReceive(opts) {
|
|
|
441
660
|
}
|
|
442
661
|
throw err;
|
|
443
662
|
}
|
|
444
|
-
onComplete?.({ received, total });
|
|
445
663
|
try {
|
|
446
664
|
conn.send({ t: "ack", phase: "end", received, total });
|
|
447
665
|
} catch {
|
|
448
666
|
}
|
|
667
|
+
safeComplete({ received, total });
|
|
449
668
|
return;
|
|
450
669
|
}
|
|
451
670
|
if (msg.t === "error") {
|
|
452
671
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
453
672
|
}
|
|
673
|
+
if (msg.t === "cancelled") {
|
|
674
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
675
|
+
state = "cancelled";
|
|
676
|
+
onCancel?.({ cancelledBy: "sender", message: msg.message });
|
|
677
|
+
cleanup();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
454
680
|
return;
|
|
455
681
|
}
|
|
456
682
|
let bufPromise;
|
|
@@ -472,7 +698,7 @@ async function startP2PReceive(opts) {
|
|
|
472
698
|
}
|
|
473
699
|
received += buf.byteLength;
|
|
474
700
|
const percent = total ? Math.min(100, received / total * 100) : 0;
|
|
475
|
-
onProgress?.({ received, total, percent });
|
|
701
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent });
|
|
476
702
|
const now = Date.now();
|
|
477
703
|
if (received === total || now - lastProgressSentAt >= progressIntervalMs) {
|
|
478
704
|
lastProgressSentAt = now;
|
|
@@ -489,21 +715,38 @@ async function startP2PReceive(opts) {
|
|
|
489
715
|
});
|
|
490
716
|
} catch {
|
|
491
717
|
}
|
|
492
|
-
|
|
493
|
-
stop();
|
|
718
|
+
safeError(err);
|
|
494
719
|
});
|
|
495
720
|
} catch (err) {
|
|
496
|
-
|
|
497
|
-
stop();
|
|
721
|
+
safeError(err);
|
|
498
722
|
}
|
|
499
723
|
});
|
|
500
724
|
conn.on("close", () => {
|
|
501
|
-
if (
|
|
725
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
726
|
+
cleanup();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (state === "transferring") {
|
|
730
|
+
state = "cancelled";
|
|
731
|
+
onCancel?.({ cancelledBy: "sender" });
|
|
732
|
+
cleanup();
|
|
733
|
+
} else if (state === "negotiating") {
|
|
734
|
+
state = "closed";
|
|
735
|
+
cleanup();
|
|
502
736
|
onDisconnect?.();
|
|
737
|
+
} else {
|
|
738
|
+
safeError(new DropgateNetworkError("Sender disconnected before file details were received."));
|
|
503
739
|
}
|
|
504
740
|
});
|
|
505
741
|
});
|
|
506
|
-
return {
|
|
742
|
+
return {
|
|
743
|
+
peer,
|
|
744
|
+
stop,
|
|
745
|
+
getStatus: () => state,
|
|
746
|
+
getBytesReceived: () => received,
|
|
747
|
+
getTotalBytes: () => total,
|
|
748
|
+
getSessionId: () => currentSessionId
|
|
749
|
+
};
|
|
507
750
|
}
|
|
508
751
|
export {
|
|
509
752
|
buildPeerOptions,
|
|
@@ -512,6 +755,7 @@ export {
|
|
|
512
755
|
isLocalhostHostname,
|
|
513
756
|
isP2PCodeLike,
|
|
514
757
|
isSecureContextForP2P,
|
|
758
|
+
resolvePeerConfig,
|
|
515
759
|
startP2PReceive,
|
|
516
760
|
startP2PSend
|
|
517
761
|
};
|