@dropgate/core 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -128
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +2814 -1436
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +424 -283
- package/dist/index.d.ts +424 -283
- package/dist/index.js +2816 -1389
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +495 -190
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +366 -5
- package/dist/p2p/index.d.ts +366 -5
- package/dist/p2p/index.js +497 -178
- package/dist/p2p/index.js.map +1 -1
- package/package.json +15 -12
package/dist/p2p/index.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,56 @@ 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
|
|
148
180
|
function generateSessionId() {
|
|
149
|
-
|
|
150
|
-
return crypto.randomUUID();
|
|
151
|
-
}
|
|
152
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
181
|
+
return crypto.randomUUID();
|
|
153
182
|
}
|
|
183
|
+
var ALLOWED_TRANSITIONS = {
|
|
184
|
+
initializing: ["listening", "closed"],
|
|
185
|
+
listening: ["handshaking", "closed", "cancelled"],
|
|
186
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
187
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
188
|
+
transferring: ["finishing", "closed", "cancelled"],
|
|
189
|
+
finishing: ["awaiting_ack", "closed", "cancelled"],
|
|
190
|
+
awaiting_ack: ["completed", "closed", "cancelled"],
|
|
191
|
+
completed: ["closed"],
|
|
192
|
+
cancelled: ["closed"],
|
|
193
|
+
closed: []
|
|
194
|
+
};
|
|
154
195
|
async function startP2PSend(opts) {
|
|
155
196
|
const {
|
|
156
197
|
file,
|
|
@@ -164,21 +205,27 @@ async function startP2PSend(opts) {
|
|
|
164
205
|
codeGenerator,
|
|
165
206
|
cryptoObj,
|
|
166
207
|
maxAttempts = 4,
|
|
167
|
-
chunkSize =
|
|
168
|
-
endAckTimeoutMs =
|
|
208
|
+
chunkSize = P2P_CHUNK_SIZE,
|
|
209
|
+
endAckTimeoutMs = P2P_END_ACK_TIMEOUT_MS,
|
|
169
210
|
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
170
211
|
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
171
212
|
heartbeatIntervalMs = 5e3,
|
|
213
|
+
chunkAcknowledgments = true,
|
|
214
|
+
maxUnackedChunks = P2P_MAX_UNACKED_CHUNKS,
|
|
172
215
|
onCode,
|
|
173
216
|
onStatus,
|
|
174
217
|
onProgress,
|
|
175
218
|
onComplete,
|
|
176
219
|
onError,
|
|
177
220
|
onDisconnect,
|
|
178
|
-
onCancel
|
|
221
|
+
onCancel,
|
|
222
|
+
onConnectionHealth
|
|
179
223
|
} = opts;
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
const files = Array.isArray(file) ? file : [file];
|
|
225
|
+
const isMultiFile = files.length > 1;
|
|
226
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
227
|
+
if (!files.length) {
|
|
228
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
182
229
|
}
|
|
183
230
|
if (!Peer) {
|
|
184
231
|
throw new DropgateValidationError(
|
|
@@ -214,21 +261,35 @@ async function startP2PSend(opts) {
|
|
|
214
261
|
let activeConn = null;
|
|
215
262
|
let sentBytes = 0;
|
|
216
263
|
let heartbeatTimer = null;
|
|
264
|
+
let healthCheckTimer = null;
|
|
265
|
+
let lastActivityTime = Date.now();
|
|
266
|
+
const unackedChunks = /* @__PURE__ */ new Map();
|
|
267
|
+
let nextSeq = 0;
|
|
268
|
+
let ackResolvers = [];
|
|
269
|
+
const transitionTo = (newState) => {
|
|
270
|
+
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
271
|
+
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
state = newState;
|
|
275
|
+
return true;
|
|
276
|
+
};
|
|
217
277
|
const reportProgress = (data) => {
|
|
218
|
-
|
|
278
|
+
if (isStopped()) return;
|
|
279
|
+
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : totalSize;
|
|
219
280
|
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
220
281
|
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
221
282
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
222
283
|
};
|
|
223
284
|
const safeError = (err) => {
|
|
224
285
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
225
|
-
|
|
286
|
+
transitionTo("closed");
|
|
226
287
|
onError?.(err);
|
|
227
288
|
cleanup();
|
|
228
289
|
};
|
|
229
290
|
const safeComplete = () => {
|
|
230
|
-
if (state !== "finishing") return;
|
|
231
|
-
|
|
291
|
+
if (state !== "awaiting_ack" && state !== "finishing") return;
|
|
292
|
+
transitionTo("completed");
|
|
232
293
|
onComplete?.();
|
|
233
294
|
cleanup();
|
|
234
295
|
};
|
|
@@ -237,6 +298,13 @@ async function startP2PSend(opts) {
|
|
|
237
298
|
clearInterval(heartbeatTimer);
|
|
238
299
|
heartbeatTimer = null;
|
|
239
300
|
}
|
|
301
|
+
if (healthCheckTimer) {
|
|
302
|
+
clearInterval(healthCheckTimer);
|
|
303
|
+
healthCheckTimer = null;
|
|
304
|
+
}
|
|
305
|
+
ackResolvers.forEach((resolve) => resolve());
|
|
306
|
+
ackResolvers = [];
|
|
307
|
+
unackedChunks.clear();
|
|
240
308
|
if (typeof window !== "undefined") {
|
|
241
309
|
window.removeEventListener("beforeunload", handleUnload);
|
|
242
310
|
}
|
|
@@ -261,8 +329,12 @@ async function startP2PSend(opts) {
|
|
|
261
329
|
}
|
|
262
330
|
const stop = () => {
|
|
263
331
|
if (state === "closed" || state === "cancelled") return;
|
|
264
|
-
|
|
265
|
-
|
|
332
|
+
if (state === "completed") {
|
|
333
|
+
cleanup();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const wasActive = state === "transferring" || state === "finishing" || state === "awaiting_ack";
|
|
337
|
+
transitionTo("cancelled");
|
|
266
338
|
try {
|
|
267
339
|
if (activeConn && activeConn.open) {
|
|
268
340
|
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
@@ -275,8 +347,91 @@ async function startP2PSend(opts) {
|
|
|
275
347
|
cleanup();
|
|
276
348
|
};
|
|
277
349
|
const isStopped = () => state === "closed" || state === "cancelled";
|
|
350
|
+
const startHealthMonitoring = (conn) => {
|
|
351
|
+
if (!onConnectionHealth) return;
|
|
352
|
+
healthCheckTimer = setInterval(() => {
|
|
353
|
+
if (isStopped()) return;
|
|
354
|
+
const dc = conn._dc;
|
|
355
|
+
if (!dc) return;
|
|
356
|
+
const health = {
|
|
357
|
+
iceConnectionState: dc.readyState === "open" ? "connected" : "disconnected",
|
|
358
|
+
bufferedAmount: dc.bufferedAmount,
|
|
359
|
+
lastActivityMs: Date.now() - lastActivityTime
|
|
360
|
+
};
|
|
361
|
+
onConnectionHealth(health);
|
|
362
|
+
}, 2e3);
|
|
363
|
+
};
|
|
364
|
+
const handleChunkAck = (msg) => {
|
|
365
|
+
lastActivityTime = Date.now();
|
|
366
|
+
unackedChunks.delete(msg.seq);
|
|
367
|
+
reportProgress({ received: msg.received, total: totalSize });
|
|
368
|
+
const resolver = ackResolvers.shift();
|
|
369
|
+
if (resolver) resolver();
|
|
370
|
+
};
|
|
371
|
+
const waitForAck = () => {
|
|
372
|
+
return new Promise((resolve) => {
|
|
373
|
+
ackResolvers.push(resolve);
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
377
|
+
if (chunkAcknowledgments) {
|
|
378
|
+
while (unackedChunks.size >= maxUnackedChunks) {
|
|
379
|
+
await Promise.race([
|
|
380
|
+
waitForAck(),
|
|
381
|
+
sleep(1e3)
|
|
382
|
+
// Timeout to prevent deadlock
|
|
383
|
+
]);
|
|
384
|
+
if (isStopped()) return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const seq = nextSeq++;
|
|
388
|
+
if (chunkAcknowledgments) {
|
|
389
|
+
unackedChunks.set(seq, { offset, size: data.byteLength, sentAt: Date.now() });
|
|
390
|
+
}
|
|
391
|
+
conn.send({ t: "chunk", seq, offset, size: data.byteLength, total: fileTotal ?? totalSize });
|
|
392
|
+
conn.send(data);
|
|
393
|
+
sentBytes += data.byteLength;
|
|
394
|
+
const dc = conn._dc;
|
|
395
|
+
if (dc && bufferHighWaterMark > 0) {
|
|
396
|
+
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
397
|
+
await new Promise((resolve) => {
|
|
398
|
+
const fallback = setTimeout(resolve, 60);
|
|
399
|
+
try {
|
|
400
|
+
dc.addEventListener(
|
|
401
|
+
"bufferedamountlow",
|
|
402
|
+
() => {
|
|
403
|
+
clearTimeout(fallback);
|
|
404
|
+
resolve();
|
|
405
|
+
},
|
|
406
|
+
{ once: true }
|
|
407
|
+
);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
if (isStopped()) return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const waitForEndAck = async (conn, ackPromise) => {
|
|
416
|
+
const baseTimeout = endAckTimeoutMs;
|
|
417
|
+
for (let attempt = 0; attempt < P2P_END_ACK_RETRIES; attempt++) {
|
|
418
|
+
conn.send({ t: "end", attempt });
|
|
419
|
+
const timeout = baseTimeout * Math.pow(1.5, attempt);
|
|
420
|
+
const result = await Promise.race([
|
|
421
|
+
ackPromise,
|
|
422
|
+
sleep(timeout).then(() => null)
|
|
423
|
+
]);
|
|
424
|
+
if (result && result.t === "end_ack") {
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
if (isStopped()) {
|
|
428
|
+
throw new DropgateNetworkError("Connection closed during completion.");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
throw new DropgateNetworkError("Receiver did not confirm completion after retries.");
|
|
432
|
+
};
|
|
278
433
|
peer.on("connection", (conn) => {
|
|
279
|
-
if (
|
|
434
|
+
if (isStopped()) return;
|
|
280
435
|
if (activeConn) {
|
|
281
436
|
const isOldConnOpen = activeConn.open !== false;
|
|
282
437
|
if (isOldConnOpen && state === "transferring") {
|
|
@@ -297,6 +452,8 @@ async function startP2PSend(opts) {
|
|
|
297
452
|
activeConn = null;
|
|
298
453
|
state = "listening";
|
|
299
454
|
sentBytes = 0;
|
|
455
|
+
nextSeq = 0;
|
|
456
|
+
unackedChunks.clear();
|
|
300
457
|
} else {
|
|
301
458
|
try {
|
|
302
459
|
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
@@ -310,60 +467,98 @@ async function startP2PSend(opts) {
|
|
|
310
467
|
}
|
|
311
468
|
}
|
|
312
469
|
activeConn = conn;
|
|
313
|
-
|
|
314
|
-
onStatus?.({ phase: "
|
|
470
|
+
transitionTo("handshaking");
|
|
471
|
+
if (!isStopped()) onStatus?.({ phase: "connected", message: "Receiver connected." });
|
|
472
|
+
lastActivityTime = Date.now();
|
|
473
|
+
let helloResolve = null;
|
|
315
474
|
let readyResolve = null;
|
|
316
|
-
let
|
|
475
|
+
let endAckResolve = null;
|
|
476
|
+
let fileEndAckResolve = null;
|
|
477
|
+
const helloPromise = new Promise((resolve) => {
|
|
478
|
+
helloResolve = resolve;
|
|
479
|
+
});
|
|
317
480
|
const readyPromise = new Promise((resolve) => {
|
|
318
481
|
readyResolve = resolve;
|
|
319
482
|
});
|
|
320
|
-
const
|
|
321
|
-
|
|
483
|
+
const endAckPromise = new Promise((resolve) => {
|
|
484
|
+
endAckResolve = resolve;
|
|
322
485
|
});
|
|
323
486
|
conn.on("data", (data) => {
|
|
324
|
-
|
|
487
|
+
lastActivityTime = Date.now();
|
|
488
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
325
489
|
return;
|
|
326
490
|
}
|
|
491
|
+
if (!isP2PMessage(data)) return;
|
|
327
492
|
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
|
-
|
|
493
|
+
switch (msg.t) {
|
|
494
|
+
case "hello":
|
|
495
|
+
helloResolve?.(msg.protocolVersion);
|
|
496
|
+
break;
|
|
497
|
+
case "ready":
|
|
498
|
+
if (!isStopped()) onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
499
|
+
readyResolve?.();
|
|
500
|
+
break;
|
|
501
|
+
case "chunk_ack":
|
|
502
|
+
handleChunkAck(msg);
|
|
503
|
+
break;
|
|
504
|
+
case "file_end_ack":
|
|
505
|
+
fileEndAckResolve?.(msg);
|
|
506
|
+
break;
|
|
507
|
+
case "end_ack":
|
|
508
|
+
endAckResolve?.(msg);
|
|
509
|
+
break;
|
|
510
|
+
case "pong":
|
|
511
|
+
break;
|
|
512
|
+
case "error":
|
|
513
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
514
|
+
break;
|
|
515
|
+
case "cancelled":
|
|
516
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
517
|
+
transitionTo("cancelled");
|
|
518
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.reason });
|
|
519
|
+
cleanup();
|
|
520
|
+
break;
|
|
354
521
|
}
|
|
355
522
|
});
|
|
356
523
|
conn.on("open", async () => {
|
|
357
524
|
try {
|
|
358
525
|
if (isStopped()) return;
|
|
526
|
+
startHealthMonitoring(conn);
|
|
527
|
+
conn.send({
|
|
528
|
+
t: "hello",
|
|
529
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
530
|
+
sessionId
|
|
531
|
+
});
|
|
532
|
+
const receiverVersion = await Promise.race([
|
|
533
|
+
helloPromise,
|
|
534
|
+
sleep(1e4).then(() => null)
|
|
535
|
+
]);
|
|
536
|
+
if (isStopped()) return;
|
|
537
|
+
if (receiverVersion === null) {
|
|
538
|
+
throw new DropgateNetworkError("Receiver did not respond to handshake.");
|
|
539
|
+
} else if (receiverVersion !== P2P_PROTOCOL_VERSION) {
|
|
540
|
+
throw new DropgateNetworkError(
|
|
541
|
+
`Protocol version mismatch: sender v${P2P_PROTOCOL_VERSION}, receiver v${receiverVersion}`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
transitionTo("negotiating");
|
|
545
|
+
if (!isStopped()) onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
546
|
+
if (isMultiFile) {
|
|
547
|
+
conn.send({
|
|
548
|
+
t: "file_list",
|
|
549
|
+
fileCount: files.length,
|
|
550
|
+
files: files.map((f) => ({ name: f.name, size: f.size, mime: f.type || "application/octet-stream" })),
|
|
551
|
+
totalSize
|
|
552
|
+
});
|
|
553
|
+
}
|
|
359
554
|
conn.send({
|
|
360
555
|
t: "meta",
|
|
361
556
|
sessionId,
|
|
362
|
-
name:
|
|
363
|
-
size:
|
|
364
|
-
mime:
|
|
557
|
+
name: files[0].name,
|
|
558
|
+
size: files[0].size,
|
|
559
|
+
mime: files[0].type || "application/octet-stream",
|
|
560
|
+
...isMultiFile ? { fileIndex: 0 } : {}
|
|
365
561
|
});
|
|
366
|
-
const total = file.size;
|
|
367
562
|
const dc = conn._dc;
|
|
368
563
|
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
369
564
|
try {
|
|
@@ -375,56 +570,60 @@ async function startP2PSend(opts) {
|
|
|
375
570
|
if (isStopped()) return;
|
|
376
571
|
if (heartbeatIntervalMs > 0) {
|
|
377
572
|
heartbeatTimer = setInterval(() => {
|
|
378
|
-
if (state === "transferring" || state === "finishing") {
|
|
573
|
+
if (state === "transferring" || state === "finishing" || state === "awaiting_ack") {
|
|
379
574
|
try {
|
|
380
|
-
conn.send({ t: "ping" });
|
|
575
|
+
conn.send({ t: "ping", timestamp: Date.now() });
|
|
381
576
|
} catch {
|
|
382
577
|
}
|
|
383
578
|
}
|
|
384
579
|
}, heartbeatIntervalMs);
|
|
385
580
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
581
|
+
transitionTo("transferring");
|
|
582
|
+
let overallSentBytes = 0;
|
|
583
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
584
|
+
const currentFile = files[fi];
|
|
585
|
+
if (isMultiFile && fi > 0) {
|
|
586
|
+
conn.send({
|
|
587
|
+
t: "meta",
|
|
588
|
+
sessionId,
|
|
589
|
+
name: currentFile.name,
|
|
590
|
+
size: currentFile.size,
|
|
591
|
+
mime: currentFile.type || "application/octet-stream",
|
|
592
|
+
fileIndex: fi
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
for (let offset = 0; offset < currentFile.size; offset += chunkSize) {
|
|
596
|
+
if (isStopped()) return;
|
|
597
|
+
const slice = currentFile.slice(offset, offset + chunkSize);
|
|
598
|
+
const buf = await slice.arrayBuffer();
|
|
599
|
+
if (isStopped()) return;
|
|
600
|
+
await sendChunk(conn, buf, offset, currentFile.size);
|
|
601
|
+
overallSentBytes += buf.byteLength;
|
|
602
|
+
reportProgress({ received: overallSentBytes, total: totalSize });
|
|
603
|
+
}
|
|
391
604
|
if (isStopped()) return;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
},
|
|
405
|
-
{ once: true }
|
|
406
|
-
);
|
|
407
|
-
} catch {
|
|
408
|
-
}
|
|
409
|
-
});
|
|
605
|
+
if (isMultiFile) {
|
|
606
|
+
const fileEndAckPromise = new Promise((resolve) => {
|
|
607
|
+
fileEndAckResolve = resolve;
|
|
608
|
+
});
|
|
609
|
+
conn.send({ t: "file_end", fileIndex: fi });
|
|
610
|
+
const feAck = await Promise.race([
|
|
611
|
+
fileEndAckPromise,
|
|
612
|
+
sleep(endAckTimeoutMs).then(() => null)
|
|
613
|
+
]);
|
|
614
|
+
if (isStopped()) return;
|
|
615
|
+
if (!feAck) {
|
|
616
|
+
throw new DropgateNetworkError(`Receiver did not confirm receipt of file ${fi + 1}/${files.length}.`);
|
|
410
617
|
}
|
|
411
618
|
}
|
|
412
619
|
}
|
|
413
620
|
if (isStopped()) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
const ackResult = await Promise.race([
|
|
418
|
-
ackPromise,
|
|
419
|
-
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
420
|
-
]);
|
|
621
|
+
transitionTo("finishing");
|
|
622
|
+
transitionTo("awaiting_ack");
|
|
623
|
+
const ackResult = await waitForEndAck(conn, endAckPromise);
|
|
421
624
|
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;
|
|
625
|
+
const ackTotal = Number(ackResult.total) || totalSize;
|
|
626
|
+
const ackReceived = Number(ackResult.received) || 0;
|
|
428
627
|
if (ackTotal && ackReceived < ackTotal) {
|
|
429
628
|
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
430
629
|
}
|
|
@@ -442,14 +641,24 @@ async function startP2PSend(opts) {
|
|
|
442
641
|
cleanup();
|
|
443
642
|
return;
|
|
444
643
|
}
|
|
644
|
+
if (state === "awaiting_ack") {
|
|
645
|
+
setTimeout(() => {
|
|
646
|
+
if (state === "awaiting_ack") {
|
|
647
|
+
safeError(new DropgateNetworkError("Connection closed while awaiting confirmation."));
|
|
648
|
+
}
|
|
649
|
+
}, P2P_CLOSE_GRACE_PERIOD_MS);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
445
652
|
if (state === "transferring" || state === "finishing") {
|
|
446
|
-
|
|
653
|
+
transitionTo("cancelled");
|
|
447
654
|
onCancel?.({ cancelledBy: "receiver" });
|
|
448
655
|
cleanup();
|
|
449
656
|
} else {
|
|
450
657
|
activeConn = null;
|
|
451
658
|
state = "listening";
|
|
452
659
|
sentBytes = 0;
|
|
660
|
+
nextSeq = 0;
|
|
661
|
+
unackedChunks.clear();
|
|
453
662
|
onDisconnect?.();
|
|
454
663
|
}
|
|
455
664
|
});
|
|
@@ -469,6 +678,16 @@ async function startP2PSend(opts) {
|
|
|
469
678
|
}
|
|
470
679
|
|
|
471
680
|
// src/p2p/receive.ts
|
|
681
|
+
var ALLOWED_TRANSITIONS2 = {
|
|
682
|
+
initializing: ["connecting", "closed"],
|
|
683
|
+
connecting: ["handshaking", "closed", "cancelled"],
|
|
684
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
685
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
686
|
+
transferring: ["completed", "closed", "cancelled"],
|
|
687
|
+
completed: ["closed"],
|
|
688
|
+
cancelled: ["closed"],
|
|
689
|
+
closed: []
|
|
690
|
+
};
|
|
472
691
|
async function startP2PReceive(opts) {
|
|
473
692
|
const {
|
|
474
693
|
code,
|
|
@@ -485,6 +704,8 @@ async function startP2PReceive(opts) {
|
|
|
485
704
|
onMeta,
|
|
486
705
|
onData,
|
|
487
706
|
onProgress,
|
|
707
|
+
onFileStart,
|
|
708
|
+
onFileEnd,
|
|
488
709
|
onComplete,
|
|
489
710
|
onError,
|
|
490
711
|
onDisconnect,
|
|
@@ -522,11 +743,22 @@ async function startP2PReceive(opts) {
|
|
|
522
743
|
let total = 0;
|
|
523
744
|
let received = 0;
|
|
524
745
|
let currentSessionId = null;
|
|
525
|
-
let lastProgressSentAt = 0;
|
|
526
|
-
const progressIntervalMs = 120;
|
|
527
746
|
let writeQueue = Promise.resolve();
|
|
528
747
|
let watchdogTimer = null;
|
|
529
748
|
let activeConn = null;
|
|
749
|
+
let pendingChunk = null;
|
|
750
|
+
let fileList = null;
|
|
751
|
+
let currentFileReceived = 0;
|
|
752
|
+
let totalReceivedAllFiles = 0;
|
|
753
|
+
const transitionTo = (newState) => {
|
|
754
|
+
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
755
|
+
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
state = newState;
|
|
759
|
+
return true;
|
|
760
|
+
};
|
|
761
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
530
762
|
const resetWatchdog = () => {
|
|
531
763
|
if (watchdogTimeoutMs <= 0) return;
|
|
532
764
|
if (watchdogTimer) {
|
|
@@ -546,15 +778,14 @@ async function startP2PReceive(opts) {
|
|
|
546
778
|
};
|
|
547
779
|
const safeError = (err) => {
|
|
548
780
|
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
549
|
-
|
|
781
|
+
transitionTo("closed");
|
|
550
782
|
onError?.(err);
|
|
551
783
|
cleanup();
|
|
552
784
|
};
|
|
553
785
|
const safeComplete = (completeData) => {
|
|
554
786
|
if (state !== "transferring") return;
|
|
555
|
-
|
|
787
|
+
transitionTo("completed");
|
|
556
788
|
onComplete?.(completeData);
|
|
557
|
-
cleanup();
|
|
558
789
|
};
|
|
559
790
|
const cleanup = () => {
|
|
560
791
|
clearWatchdog();
|
|
@@ -578,11 +809,15 @@ async function startP2PReceive(opts) {
|
|
|
578
809
|
}
|
|
579
810
|
const stop = () => {
|
|
580
811
|
if (state === "closed" || state === "cancelled") return;
|
|
812
|
+
if (state === "completed") {
|
|
813
|
+
cleanup();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
581
816
|
const wasActive = state === "transferring";
|
|
582
|
-
|
|
817
|
+
transitionTo("cancelled");
|
|
583
818
|
try {
|
|
584
819
|
if (activeConn && activeConn.open) {
|
|
585
|
-
activeConn.send({ t: "cancelled",
|
|
820
|
+
activeConn.send({ t: "cancelled", reason: "Receiver cancelled the transfer." });
|
|
586
821
|
}
|
|
587
822
|
} catch {
|
|
588
823
|
}
|
|
@@ -591,23 +826,88 @@ async function startP2PReceive(opts) {
|
|
|
591
826
|
}
|
|
592
827
|
cleanup();
|
|
593
828
|
};
|
|
829
|
+
const sendChunkAck = (conn, seq) => {
|
|
830
|
+
try {
|
|
831
|
+
conn.send({ t: "chunk_ack", seq, received });
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
};
|
|
594
835
|
peer.on("error", (err) => {
|
|
595
836
|
safeError(err);
|
|
596
837
|
});
|
|
597
838
|
peer.on("open", () => {
|
|
598
|
-
|
|
839
|
+
transitionTo("connecting");
|
|
599
840
|
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
600
841
|
activeConn = conn;
|
|
601
842
|
conn.on("open", () => {
|
|
602
|
-
|
|
603
|
-
onStatus?.({ phase: "connected", message: "
|
|
843
|
+
transitionTo("handshaking");
|
|
844
|
+
onStatus?.({ phase: "connected", message: "Connected." });
|
|
845
|
+
conn.send({
|
|
846
|
+
t: "hello",
|
|
847
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
848
|
+
sessionId: ""
|
|
849
|
+
});
|
|
604
850
|
});
|
|
605
851
|
conn.on("data", async (data) => {
|
|
606
852
|
try {
|
|
607
853
|
resetWatchdog();
|
|
608
|
-
if (data
|
|
609
|
-
|
|
610
|
-
if (
|
|
854
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
855
|
+
let bufPromise;
|
|
856
|
+
if (data instanceof ArrayBuffer) {
|
|
857
|
+
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
858
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
859
|
+
bufPromise = Promise.resolve(
|
|
860
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
861
|
+
);
|
|
862
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
863
|
+
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
864
|
+
} else {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
868
|
+
pendingChunk = null;
|
|
869
|
+
writeQueue = writeQueue.then(async () => {
|
|
870
|
+
const buf = await bufPromise;
|
|
871
|
+
if (onData) {
|
|
872
|
+
await onData(buf);
|
|
873
|
+
}
|
|
874
|
+
received += buf.byteLength;
|
|
875
|
+
currentFileReceived += buf.byteLength;
|
|
876
|
+
const progressReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
877
|
+
const progressTotal = fileList ? fileList.totalSize : total;
|
|
878
|
+
const percent = progressTotal ? Math.min(100, progressReceived / progressTotal * 100) : 0;
|
|
879
|
+
if (!isStopped()) onProgress?.({ processedBytes: progressReceived, totalBytes: progressTotal, percent });
|
|
880
|
+
if (chunkSeq >= 0) {
|
|
881
|
+
sendChunkAck(conn, chunkSeq);
|
|
882
|
+
}
|
|
883
|
+
}).catch((err) => {
|
|
884
|
+
try {
|
|
885
|
+
conn.send({
|
|
886
|
+
t: "error",
|
|
887
|
+
message: err?.message || "Receiver write failed."
|
|
888
|
+
});
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
safeError(err);
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (!isP2PMessage(data)) return;
|
|
896
|
+
const msg = data;
|
|
897
|
+
switch (msg.t) {
|
|
898
|
+
case "hello":
|
|
899
|
+
currentSessionId = msg.sessionId || null;
|
|
900
|
+
transitionTo("negotiating");
|
|
901
|
+
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
902
|
+
break;
|
|
903
|
+
case "file_list":
|
|
904
|
+
fileList = msg;
|
|
905
|
+
total = fileList.totalSize;
|
|
906
|
+
break;
|
|
907
|
+
case "meta": {
|
|
908
|
+
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
611
911
|
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
612
912
|
try {
|
|
613
913
|
conn.send({ t: "error", message: "Busy with another session." });
|
|
@@ -619,40 +919,83 @@ async function startP2PReceive(opts) {
|
|
|
619
919
|
currentSessionId = msg.sessionId;
|
|
620
920
|
}
|
|
621
921
|
const name = String(msg.name || "file");
|
|
622
|
-
|
|
922
|
+
const fileSize = Number(msg.size) || 0;
|
|
923
|
+
const fi = msg.fileIndex;
|
|
924
|
+
if (fileList && typeof fi === "number" && fi > 0) {
|
|
925
|
+
currentFileReceived = 0;
|
|
926
|
+
onFileStart?.({ fileIndex: fi, name, size: fileSize });
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
623
929
|
received = 0;
|
|
930
|
+
currentFileReceived = 0;
|
|
931
|
+
totalReceivedAllFiles = 0;
|
|
932
|
+
if (!fileList) {
|
|
933
|
+
total = fileSize;
|
|
934
|
+
}
|
|
624
935
|
writeQueue = Promise.resolve();
|
|
625
936
|
const sendReady = () => {
|
|
626
|
-
|
|
937
|
+
transitionTo("transferring");
|
|
627
938
|
resetWatchdog();
|
|
939
|
+
if (fileList) {
|
|
940
|
+
onFileStart?.({ fileIndex: 0, name, size: fileSize });
|
|
941
|
+
}
|
|
628
942
|
try {
|
|
629
943
|
conn.send({ t: "ready" });
|
|
630
944
|
} catch {
|
|
631
945
|
}
|
|
632
946
|
};
|
|
947
|
+
const metaEvt = { name, total };
|
|
948
|
+
if (fileList) {
|
|
949
|
+
metaEvt.fileCount = fileList.fileCount;
|
|
950
|
+
metaEvt.files = fileList.files.map((f) => ({ name: f.name, size: f.size }));
|
|
951
|
+
metaEvt.totalSize = fileList.totalSize;
|
|
952
|
+
}
|
|
633
953
|
if (autoReady) {
|
|
634
|
-
|
|
635
|
-
|
|
954
|
+
if (!isStopped()) {
|
|
955
|
+
onMeta?.(metaEvt);
|
|
956
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
957
|
+
}
|
|
636
958
|
sendReady();
|
|
637
959
|
} else {
|
|
638
|
-
|
|
639
|
-
|
|
960
|
+
metaEvt.sendReady = sendReady;
|
|
961
|
+
if (!isStopped()) {
|
|
962
|
+
onMeta?.(metaEvt);
|
|
963
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
964
|
+
}
|
|
640
965
|
}
|
|
641
|
-
|
|
966
|
+
break;
|
|
642
967
|
}
|
|
643
|
-
|
|
968
|
+
case "chunk":
|
|
969
|
+
pendingChunk = msg;
|
|
970
|
+
break;
|
|
971
|
+
case "ping":
|
|
644
972
|
try {
|
|
645
|
-
conn.send({ t: "pong" });
|
|
973
|
+
conn.send({ t: "pong", timestamp: Date.now() });
|
|
646
974
|
} catch {
|
|
647
975
|
}
|
|
648
|
-
|
|
976
|
+
break;
|
|
977
|
+
case "file_end": {
|
|
978
|
+
clearWatchdog();
|
|
979
|
+
await writeQueue;
|
|
980
|
+
const feIdx = msg.fileIndex;
|
|
981
|
+
onFileEnd?.({ fileIndex: feIdx, receivedBytes: currentFileReceived });
|
|
982
|
+
try {
|
|
983
|
+
conn.send({ t: "file_end_ack", fileIndex: feIdx, received: currentFileReceived, size: currentFileReceived });
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
totalReceivedAllFiles += currentFileReceived;
|
|
987
|
+
currentFileReceived = 0;
|
|
988
|
+
resetWatchdog();
|
|
989
|
+
break;
|
|
649
990
|
}
|
|
650
|
-
|
|
991
|
+
case "end":
|
|
651
992
|
clearWatchdog();
|
|
652
993
|
await writeQueue;
|
|
653
|
-
|
|
994
|
+
const finalReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
995
|
+
const finalTotal = fileList ? fileList.totalSize : total;
|
|
996
|
+
if (finalTotal && finalReceived < finalTotal) {
|
|
654
997
|
const err = new DropgateNetworkError(
|
|
655
|
-
"Transfer ended before
|
|
998
|
+
"Transfer ended before all data was received."
|
|
656
999
|
);
|
|
657
1000
|
try {
|
|
658
1001
|
conn.send({ t: "error", message: err.message });
|
|
@@ -661,62 +1004,31 @@ async function startP2PReceive(opts) {
|
|
|
661
1004
|
throw err;
|
|
662
1005
|
}
|
|
663
1006
|
try {
|
|
664
|
-
conn.send({ t: "
|
|
1007
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
665
1008
|
} catch {
|
|
666
1009
|
}
|
|
667
|
-
safeComplete({ received, total });
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1010
|
+
safeComplete({ received: finalReceived, total: finalTotal });
|
|
1011
|
+
(async () => {
|
|
1012
|
+
for (let i = 0; i < 2; i++) {
|
|
1013
|
+
await sleep(P2P_END_ACK_RETRY_DELAY_MS);
|
|
1014
|
+
try {
|
|
1015
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
1016
|
+
} catch {
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
})().catch(() => {
|
|
1021
|
+
});
|
|
1022
|
+
break;
|
|
1023
|
+
case "error":
|
|
671
1024
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
672
|
-
|
|
673
|
-
if (msg.t === "cancelled") {
|
|
1025
|
+
case "cancelled":
|
|
674
1026
|
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
675
|
-
|
|
676
|
-
onCancel?.({ cancelledBy: "sender", message: msg.
|
|
1027
|
+
transitionTo("cancelled");
|
|
1028
|
+
onCancel?.({ cancelledBy: "sender", message: msg.reason });
|
|
677
1029
|
cleanup();
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
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;
|
|
1030
|
+
break;
|
|
693
1031
|
}
|
|
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
1032
|
} catch (err) {
|
|
721
1033
|
safeError(err);
|
|
722
1034
|
}
|
|
@@ -727,11 +1039,11 @@ async function startP2PReceive(opts) {
|
|
|
727
1039
|
return;
|
|
728
1040
|
}
|
|
729
1041
|
if (state === "transferring") {
|
|
730
|
-
|
|
1042
|
+
transitionTo("cancelled");
|
|
731
1043
|
onCancel?.({ cancelledBy: "sender" });
|
|
732
1044
|
cleanup();
|
|
733
1045
|
} else if (state === "negotiating") {
|
|
734
|
-
|
|
1046
|
+
transitionTo("closed");
|
|
735
1047
|
cleanup();
|
|
736
1048
|
onDisconnect?.();
|
|
737
1049
|
} else {
|
|
@@ -749,11 +1061,18 @@ async function startP2PReceive(opts) {
|
|
|
749
1061
|
};
|
|
750
1062
|
}
|
|
751
1063
|
export {
|
|
1064
|
+
P2P_CHUNK_SIZE,
|
|
1065
|
+
P2P_END_ACK_RETRIES,
|
|
1066
|
+
P2P_END_ACK_TIMEOUT_MS,
|
|
1067
|
+
P2P_MAX_UNACKED_CHUNKS,
|
|
1068
|
+
P2P_PROTOCOL_VERSION,
|
|
752
1069
|
buildPeerOptions,
|
|
753
1070
|
createPeerWithRetries,
|
|
754
1071
|
generateP2PCode,
|
|
755
1072
|
isLocalhostHostname,
|
|
756
1073
|
isP2PCodeLike,
|
|
1074
|
+
isP2PMessage,
|
|
1075
|
+
isProtocolCompatible,
|
|
757
1076
|
isSecureContextForP2P,
|
|
758
1077
|
resolvePeerConfig,
|
|
759
1078
|
startP2PReceive,
|