@decentnetwork/peer 0.1.122 → 0.1.124
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/dist/peer.d.ts +23 -0
- package/dist/peer.js +143 -26
- package/dist/transport/dgram-lifecycle.d.ts +14 -0
- package/dist/transport/dgram-lifecycle.js +64 -0
- package/package.json +1 -1
package/dist/peer.d.ts
CHANGED
|
@@ -307,4 +307,27 @@ export declare class Peer {
|
|
|
307
307
|
name: string;
|
|
308
308
|
description: string;
|
|
309
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* Tell a friend our public UDP endpoint over the (already-working,
|
|
312
|
+
* possibly tcp-relay) messenger channel, so they can hole-punch to us.
|
|
313
|
+
* This is the out-of-band substitute for the broken DHT/onion-announce
|
|
314
|
+
* endpoint discovery. Both peers do this symmetrically; on receipt each
|
|
315
|
+
* feeds the other's endpoint into endpointCandidates and punches.
|
|
316
|
+
*/
|
|
317
|
+
/**
|
|
318
|
+
* Lazily allocate our node-level TURN relay on its own dedicated UDP
|
|
319
|
+
* socket. Returns our public relay address (on the TURN server) which we
|
|
320
|
+
* advertise to peers so they can reach us via the relay even when direct
|
|
321
|
+
* UDP can't be punched. Relayed data is injected into #onDatagram tagged
|
|
322
|
+
* `viaRelay` so it updates the relay freshness, not the direct endpoint.
|
|
323
|
+
*/
|
|
324
|
+
/**
|
|
325
|
+
* Test-only seam for scripts/turn-socket-leak-selftest.mjs: drive one TURN
|
|
326
|
+
* allocation attempt directly instead of waiting out the 120s-per-friend
|
|
327
|
+
* relay keepalive that reaches this path in production. Not public API.
|
|
328
|
+
*/
|
|
329
|
+
__testForceTurnAllocation(): Promise<{
|
|
330
|
+
host: string;
|
|
331
|
+
port: number;
|
|
332
|
+
} | undefined>;
|
|
310
333
|
}
|
package/dist/peer.js
CHANGED
|
@@ -23,14 +23,35 @@ import { UdpTransport } from "./transport/udp.js";
|
|
|
23
23
|
import { bytesToHex, concatBytes, randomBytes } from "./utils/bytes.js";
|
|
24
24
|
import { buildBindingRequest, decodeStun, decodeXorMappedAddress, findAttr, STUN_ATTR_XOR_MAPPED_ADDRESS, STUN_BINDING_SUCCESS } from "./stun.js";
|
|
25
25
|
import { TurnClient } from "./turn.js";
|
|
26
|
-
import {
|
|
26
|
+
import { createBoundUdp4Socket, closeDgramSocket } from "./transport/dgram-lifecycle.js";
|
|
27
27
|
// Dedicated TURN relay servers — fixed public relays used as a stable
|
|
28
28
|
// fallback path when direct UDP hole-punch fails or flaps (symmetric NAT,
|
|
29
29
|
// NAT remaps, lossy direct path). A relay endpoint never NAT-flaps, so the
|
|
30
30
|
// path stays put even when the peers' NATs churn. Static long-term creds.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// DECENT_TURN_SERVERS overrides the list ("host:port,host:port"). Used by the
|
|
32
|
+
// socket-leak selftest to point allocation at an unroutable address so every
|
|
33
|
+
// attempt fails deterministically; also lets ops repoint TURN without a build.
|
|
34
|
+
const TURN_RELAY_SERVERS = (() => {
|
|
35
|
+
const raw = process.env.DECENT_TURN_SERVERS?.trim();
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return [{ host: "tokyo.fi.chat", port: 3478, username: "allcom", password: "allcompass" }];
|
|
38
|
+
}
|
|
39
|
+
return raw
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((entry) => entry.trim())
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.map((entry) => {
|
|
44
|
+
const idx = entry.lastIndexOf(":");
|
|
45
|
+
const host = idx === -1 ? entry : entry.slice(0, idx);
|
|
46
|
+
const port = idx === -1 ? 3478 : Number.parseInt(entry.slice(idx + 1), 10);
|
|
47
|
+
return {
|
|
48
|
+
host,
|
|
49
|
+
port: Number.isFinite(port) && port > 0 ? port : 3478,
|
|
50
|
+
username: "allcom",
|
|
51
|
+
password: "allcompass"
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
})();
|
|
34
55
|
const ANNOUNCE_WAIT_TIMEOUT_MS = readEnvInt("DECENT_ANNOUNCE_WAIT_TIMEOUT_MS", 4000);
|
|
35
56
|
// 24 (was 12): with distance-ordered walking a lookup needs headroom to
|
|
36
57
|
// recurse past a first wave of dead close-in-XOR-space entries.
|
|
@@ -88,6 +109,15 @@ const FRIEND_PING_INTERVAL_MS = readEnvInt("DECENT_FRIEND_PING_INTERVAL_MS", 400
|
|
|
88
109
|
// middle of toxcore's reactive range — fast enough to keep responsiveness,
|
|
89
110
|
// slow enough not to burn CPU on idle peers. Tunable via env.
|
|
90
111
|
const FRIEND_CONNECTION_LOOP_MS = readEnvInt("DECENT_FRIEND_CONNECTION_LOOP_MS", 250);
|
|
112
|
+
/** Minimum gap between onion DHT-PK announces to the SAME friend.
|
|
113
|
+
*
|
|
114
|
+
* One announce fans out to several nodes and costs three X25519 scalar mults
|
|
115
|
+
* per node (one per onion layer), so this budget is the difference between an
|
|
116
|
+
* idle node and a busy core. Two call sites in #doFriendConnections need it —
|
|
117
|
+
* they previously each spelled the interval out, drifted apart (25s vs the
|
|
118
|
+
* enclosing 3s re-punch cadence), and the cheap one silently set the pace for
|
|
119
|
+
* the expensive one. Shared here so they cannot diverge again. */
|
|
120
|
+
const DHT_PK_ANNOUNCE_COOLDOWN_MS = readEnvInt("DECENT_DHT_PK_ANNOUNCE_COOLDOWN_MS", 25_000);
|
|
91
121
|
const FRIEND_TIMEOUT_MS = readEnvInt("DECENT_FRIEND_TIMEOUT_MS", 32000);
|
|
92
122
|
// A session whose keys we've PROVEN (decrypted ≥1 real packet from the peer) is
|
|
93
123
|
// NOT torn down at FRIEND_TIMEOUT_MS. A transient all-transport blackout — e.g.
|
|
@@ -358,6 +388,14 @@ export class Peer {
|
|
|
358
388
|
// session entry exists yet so the connection loop does not flood DHT-PK
|
|
359
389
|
// requests when route discovery keeps failing.
|
|
360
390
|
#dhtPkSendCooldown = new Map();
|
|
391
|
+
/** Last ONION-routed endpoint lookup per friend. Separate from
|
|
392
|
+
* #dhtPkSendCooldown: they are two different expensive operations for the
|
|
393
|
+
* same friend, and sharing one timestamp would let whichever ran first
|
|
394
|
+
* suppress the other for a full budget. */
|
|
395
|
+
#onionLookupCooldown = new Map();
|
|
396
|
+
/** Consecutive failed endpoint lookups per (friend,target) — drives how wide
|
|
397
|
+
* the next fan-out goes. Cleared on success. */
|
|
398
|
+
#onionLookupMisses = new Map();
|
|
361
399
|
// Per-friend cooldown for re-asserting the TCP-relay route toward an
|
|
362
400
|
// unconnected friend (the "accepted friend that never connects" wedge —
|
|
363
401
|
// requestRoute only fired once at startup and was never retried).
|
|
@@ -736,13 +774,8 @@ export class Peer {
|
|
|
736
774
|
this.#udp.off("datagram", this.#onDatagram);
|
|
737
775
|
await this.#udp.stop();
|
|
738
776
|
// Release the TURN allocation + its dedicated socket.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
this.#turnSocket?.close();
|
|
742
|
-
}
|
|
743
|
-
catch {
|
|
744
|
-
// best-effort
|
|
745
|
-
}
|
|
777
|
+
this.#turnClient?.close();
|
|
778
|
+
await closeDgramSocket(this.#turnSocket);
|
|
746
779
|
this.#turnClient = undefined;
|
|
747
780
|
this.#turnSocket = undefined;
|
|
748
781
|
this.#ourRelayAddr = undefined;
|
|
@@ -3044,12 +3077,53 @@ export class Peer {
|
|
|
3044
3077
|
return false;
|
|
3045
3078
|
}
|
|
3046
3079
|
const targetId = carrierIdFromPublicKey(searchPublicKey);
|
|
3080
|
+
// Per-(friend, target) budget, enforced HERE rather than at the call sites.
|
|
3081
|
+
//
|
|
3082
|
+
// One run of this fans out to up to 24 nodes and each onion request costs
|
|
3083
|
+
// three X25519 scalar mults (one per layer) — up to ~72 per call, tens of
|
|
3084
|
+
// ms of pure CPU. There are four call sites and gating them one at a time
|
|
3085
|
+
// just moves the cost to whichever one was missed: after gating only the
|
|
3086
|
+
// re-punch path, this function was STILL the top crypto entry on
|
|
3087
|
+
// smtp.gfax.cn. A choke point inside the function is the only version that
|
|
3088
|
+
// cannot be bypassed by a call site added later.
|
|
3089
|
+
//
|
|
3090
|
+
// Keyed by target, not just friend, so the dhtPk lookup and the
|
|
3091
|
+
// onion-routed fallback for the same friend keep independent budgets —
|
|
3092
|
+
// throttling repeats of the SAME search, never the first try of a
|
|
3093
|
+
// different one.
|
|
3094
|
+
const budgetKey = `${friendId}:${targetId}`;
|
|
3095
|
+
const lastLookup = this.#onionLookupCooldown.get(budgetKey) ?? 0;
|
|
3096
|
+
const sinceLast = Date.now() - lastLookup;
|
|
3097
|
+
if (sinceLast < DHT_PK_ANNOUNCE_COOLDOWN_MS) {
|
|
3098
|
+
this.#debugLog(`endpoint lookup for ${friendId} skipped: ${Math.round(sinceLast / 1000)}s since last ` +
|
|
3099
|
+
`(budget ${DHT_PK_ANNOUNCE_COOLDOWN_MS / 1000}s)`);
|
|
3100
|
+
return false;
|
|
3101
|
+
}
|
|
3102
|
+
this.#onionLookupCooldown.set(budgetKey, Date.now());
|
|
3103
|
+
// Adaptive fan-out. Each node in this queue costs TWO announce round trips
|
|
3104
|
+
// (ping id, then the real query) and every onion request is three X25519
|
|
3105
|
+
// scalar mults — so a full 24-node sweep is ~144 scalar mults, and the loop
|
|
3106
|
+
// only exits early on SUCCESS. In steady state the lookups that repeat are
|
|
3107
|
+
// exactly the ones that keep failing, so the old fixed width paid the
|
|
3108
|
+
// maximum price precisely when it was least likely to pay off: measured
|
|
3109
|
+
// ~10-23 announce sends/sec on smtp.gfax.cn, ~28% of a core.
|
|
3110
|
+
//
|
|
3111
|
+
// First attempt keeps the full width — that is the one most likely to find
|
|
3112
|
+
// the peer, and discovery reliability is not what we are trading away.
|
|
3113
|
+
// Repeats narrow to a probe, and every 8th attempt goes wide again so a
|
|
3114
|
+
// peer that only just came back is still found without waiting for a
|
|
3115
|
+
// restart. Widening beats a permanently narrow sweep: the failure we must
|
|
3116
|
+
// not introduce is "never discovers", not "discovers a bit later".
|
|
3117
|
+
const misses = this.#onionLookupMisses.get(budgetKey) ?? 0;
|
|
3118
|
+
const fullWidth = Math.max(8, Math.min(24, MAX_FRIEND_ROUTE_ATTEMPTS));
|
|
3119
|
+
const width = misses === 0 || misses % 8 === 0 ? fullWidth : 4;
|
|
3047
3120
|
const queue = dedupeNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes)
|
|
3048
3121
|
.filter((n) => !this.#isNodeBlacklisted(`${n.host}:${n.port}`))
|
|
3049
3122
|
.sort((a, b) => this.#nodeScore(`${b.host}:${b.port}`) - this.#nodeScore(`${a.host}:${a.port}`))
|
|
3050
|
-
.slice(0,
|
|
3123
|
+
.slice(0, width);
|
|
3051
3124
|
const directKnown = queue.find((node) => node.pk === targetId);
|
|
3052
3125
|
if (directKnown) {
|
|
3126
|
+
this.#onionLookupMisses.delete(budgetKey);
|
|
3053
3127
|
this.#cacheFriendRemote(friendId, directKnown.host, directKnown.port);
|
|
3054
3128
|
this.#debugLog(`friend endpoint matched known node for ${friendId} at ${directKnown.host}:${directKnown.port}`);
|
|
3055
3129
|
return true;
|
|
@@ -3106,11 +3180,15 @@ export class Peer {
|
|
|
3106
3180
|
}
|
|
3107
3181
|
const exact = discovered.find((n) => n.pk === targetId);
|
|
3108
3182
|
if (exact) {
|
|
3183
|
+
this.#onionLookupMisses.delete(budgetKey);
|
|
3109
3184
|
this.#cacheFriendRemote(friendId, exact.host, exact.port);
|
|
3110
3185
|
this.#debugLog(`friend endpoint discovered for ${friendId} at ${exact.host}:${exact.port}`);
|
|
3111
3186
|
return true;
|
|
3112
3187
|
}
|
|
3113
3188
|
}
|
|
3189
|
+
// Nothing found: remember, so the next sweep for this target probes
|
|
3190
|
+
// narrowly instead of paying the full 24-node price again.
|
|
3191
|
+
this.#onionLookupMisses.set(budgetKey, misses + 1);
|
|
3114
3192
|
return false;
|
|
3115
3193
|
}
|
|
3116
3194
|
async #announceSelfBestEffort(force = false, deadlineMs = Number.POSITIVE_INFINITY) {
|
|
@@ -3531,9 +3609,29 @@ export class Peer {
|
|
|
3531
3609
|
if (friendRealPk && friendRealPk.length === 32) {
|
|
3532
3610
|
// 1. Re-announce our DHT-PK so peer learns *our* UDP endpoint
|
|
3533
3611
|
// even if their own DHT lookup against us was stale.
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3612
|
+
//
|
|
3613
|
+
// This shares #dhtPkSendCooldown with the periodic branch
|
|
3614
|
+
// below ON PURPOSE. The two operations in this `if` have very
|
|
3615
|
+
// different costs and must not share the 3s cadence: the
|
|
3616
|
+
// endpoint offer above is one UDP packet, while an onion
|
|
3617
|
+
// DHT-PK announce fans out to several nodes and does THREE
|
|
3618
|
+
// X25519 scalar mults per node (one per onion layer). At 3s
|
|
3619
|
+
// per friend that dominated the CPU of any node with a few
|
|
3620
|
+
// idle relay friends — measured on smtp.gfax.cn: 82% of wall
|
|
3621
|
+
// clock inside nacl, 83% of it under createOnionRequest0,
|
|
3622
|
+
// against 2% on a node with none. Note the trigger is NOT a
|
|
3623
|
+
// broken friend: udpConfirmed needs UDP traffic within 4s, so
|
|
3624
|
+
// a perfectly healthy but idle relay friend qualifies forever.
|
|
3625
|
+
//
|
|
3626
|
+
// Keep the offer at 3s (cheap, and it is what actually
|
|
3627
|
+
// re-punches); let the expensive announce keep its 25s budget.
|
|
3628
|
+
const lastAnnounce = this.#dhtPkSendCooldown.get(friendId) ?? 0;
|
|
3629
|
+
if (now - lastAnnounce > DHT_PK_ANNOUNCE_COOLDOWN_MS) {
|
|
3630
|
+
this.#dhtPkSendCooldown.set(friendId, now);
|
|
3631
|
+
void this.#sendOnionDhtPk(friendRealPk).catch((error) => {
|
|
3632
|
+
this.#debugLog(`UDP retry: dhtpk_send for ${friendId} failed: ${error.message}`);
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3537
3635
|
// 2. Try to discover the peer's UDP endpoint and kick a
|
|
3538
3636
|
// fresh cookie request via UDP. discoverAndCacheFriend
|
|
3539
3637
|
// Endpoint accepts EITHER the DHT-PK (preferred —
|
|
@@ -3544,6 +3642,19 @@ export class Peer {
|
|
|
3544
3642
|
// session.friendDhtPublicKey is null. Falling back
|
|
3545
3643
|
// to friendRealPk keeps the retry from being a no-op
|
|
3546
3644
|
// in exactly the case it was designed to fix.
|
|
3645
|
+
// COST NOTE: those two lookups are not the same price. With a
|
|
3646
|
+
// dhtPk it is a direct DHT query. Without one it is an
|
|
3647
|
+
// ONION-routed lookup — several nodes, three X25519 scalar
|
|
3648
|
+
// mults each — and the fallback fires precisely on
|
|
3649
|
+
// relay-only sessions, which are the long-lived ones. At the
|
|
3650
|
+
// 3s re-punch cadence that made it the single biggest CPU
|
|
3651
|
+
// consumer on a node with idle relay friends (measured on
|
|
3652
|
+
// smtp.gfax.cn: 31% of all crypto time entered here). So the
|
|
3653
|
+
// cheap direct lookup keeps the 3s cadence and only the onion
|
|
3654
|
+
// fallback is charged against the announce budget.
|
|
3655
|
+
// The per-(friend, target) budget now lives INSIDE
|
|
3656
|
+
// #discoverAndCacheFriendEndpoint, so this call site stays
|
|
3657
|
+
// simple and every other caller is covered by the same gate.
|
|
3547
3658
|
const dhtPk = session.friendDhtPublicKey;
|
|
3548
3659
|
const searchKey = dhtPk ?? friendRealPk;
|
|
3549
3660
|
if (searchKey && searchKey.length === 32) {
|
|
@@ -3577,8 +3688,7 @@ export class Peer {
|
|
|
3577
3688
|
// bootstrap nodes). Per-friend cooldown via #dhtPkSendCooldown so we
|
|
3578
3689
|
// don't flood onion data requests every 5s loop tick.
|
|
3579
3690
|
const lastDhtPkSent = this.#dhtPkSendCooldown.get(friendId) ?? 0;
|
|
3580
|
-
|
|
3581
|
-
if (now - lastDhtPkSent > dhtPkInterval) {
|
|
3691
|
+
if (now - lastDhtPkSent > DHT_PK_ANNOUNCE_COOLDOWN_MS) {
|
|
3582
3692
|
let friendRealPk = session?.friendRealPublicKey;
|
|
3583
3693
|
if (!friendRealPk && friend.address) {
|
|
3584
3694
|
try {
|
|
@@ -4765,6 +4875,14 @@ export class Peer {
|
|
|
4765
4875
|
* UDP can't be punched. Relayed data is injected into #onDatagram tagged
|
|
4766
4876
|
* `viaRelay` so it updates the relay freshness, not the direct endpoint.
|
|
4767
4877
|
*/
|
|
4878
|
+
/**
|
|
4879
|
+
* Test-only seam for scripts/turn-socket-leak-selftest.mjs: drive one TURN
|
|
4880
|
+
* allocation attempt directly instead of waiting out the 120s-per-friend
|
|
4881
|
+
* relay keepalive that reaches this path in production. Not public API.
|
|
4882
|
+
*/
|
|
4883
|
+
async __testForceTurnAllocation() {
|
|
4884
|
+
return this.#ensureTurnRelay().catch(() => undefined);
|
|
4885
|
+
}
|
|
4768
4886
|
async #ensureTurnRelay() {
|
|
4769
4887
|
if (this.#ourRelayAddr)
|
|
4770
4888
|
return this.#ourRelayAddr;
|
|
@@ -4772,15 +4890,10 @@ export class Peer {
|
|
|
4772
4890
|
return this.#turnAllocating;
|
|
4773
4891
|
this.#turnAllocating = (async () => {
|
|
4774
4892
|
for (const srv of TURN_RELAY_SERVERS) {
|
|
4893
|
+
let sock;
|
|
4894
|
+
let client;
|
|
4775
4895
|
try {
|
|
4776
|
-
|
|
4777
|
-
await new Promise((resolve, reject) => {
|
|
4778
|
-
sock.once("error", reject);
|
|
4779
|
-
sock.bind(0, () => {
|
|
4780
|
-
sock.off("error", reject);
|
|
4781
|
-
resolve();
|
|
4782
|
-
});
|
|
4783
|
-
});
|
|
4896
|
+
sock = await createBoundUdp4Socket();
|
|
4784
4897
|
// CRITICAL: a persistent 'error' listener. A TURN send to an
|
|
4785
4898
|
// unresolvable host (DNS NXDOMAIN — e.g. tokyo.fi.chat on a box whose
|
|
4786
4899
|
// resolver doesn't know it) emits an 'error' on this dgram socket;
|
|
@@ -4789,7 +4902,7 @@ export class Peer {
|
|
|
4789
4902
|
// "getaddrinfo ENOTFOUND tokyo.fi.chat", which churned every session).
|
|
4790
4903
|
// Swallow it — the relay simply won't allocate/relay over this server.
|
|
4791
4904
|
sock.on("error", (e) => this.#debugLog(`turn socket error (${srv.host}): ${e.message}`));
|
|
4792
|
-
|
|
4905
|
+
client = new TurnClient({
|
|
4793
4906
|
sock,
|
|
4794
4907
|
creds: { host: srv.host, port: srv.port, realm: "", username: srv.username, password: srv.password }
|
|
4795
4908
|
});
|
|
@@ -4805,12 +4918,16 @@ export class Peer {
|
|
|
4805
4918
|
});
|
|
4806
4919
|
this.#turnSocket = sock;
|
|
4807
4920
|
this.#turnClient = client;
|
|
4921
|
+
sock = undefined;
|
|
4922
|
+
client = undefined;
|
|
4808
4923
|
this.#ourRelayAddr = { host: alloc.relayedAddress.address, port: alloc.relayedAddress.port };
|
|
4809
4924
|
this.#debugLog(`turn relay allocated on ${srv.host}: ${this.#ourRelayAddr.host}:${this.#ourRelayAddr.port}`);
|
|
4810
4925
|
return this.#ourRelayAddr;
|
|
4811
4926
|
}
|
|
4812
4927
|
catch (error) {
|
|
4813
4928
|
this.#debugLog(`turn relay allocate failed on ${srv.host}: ${error.message}`);
|
|
4929
|
+
client?.close();
|
|
4930
|
+
await closeDgramSocket(sock);
|
|
4814
4931
|
}
|
|
4815
4932
|
}
|
|
4816
4933
|
return undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Socket as DgramSocket } from "dgram";
|
|
2
|
+
export type DgramSocketFactory = () => DgramSocket;
|
|
3
|
+
export declare function createBoundUdp4Socket(createSocket?: DgramSocketFactory): Promise<DgramSocket>;
|
|
4
|
+
/**
|
|
5
|
+
* Close a dgram socket and wait for the fd to actually be released.
|
|
6
|
+
*
|
|
7
|
+
* Never rejects and never hangs: `close()` throws ERR_SOCKET_DGRAM_NOT_RUNNING
|
|
8
|
+
* on an already-closed socket, and a socket caught mid-teardown may never run
|
|
9
|
+
* the close callback at all. Both must settle, because callers await this on
|
|
10
|
+
* the TURN failure path — a hang there would leave `#turnAllocating` pending
|
|
11
|
+
* forever and wedge every later caller awaiting that same promise, and would
|
|
12
|
+
* also block `Peer.stop()`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function closeDgramSocket(sock: DgramSocket | undefined, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createSocket as createDgramSocket } from "dgram";
|
|
2
|
+
export async function createBoundUdp4Socket(createSocket = () => createDgramSocket("udp4")) {
|
|
3
|
+
const sock = createSocket();
|
|
4
|
+
try {
|
|
5
|
+
await new Promise((resolve, reject) => {
|
|
6
|
+
const cleanup = () => {
|
|
7
|
+
sock.off("error", onError);
|
|
8
|
+
};
|
|
9
|
+
const onError = (error) => {
|
|
10
|
+
cleanup();
|
|
11
|
+
reject(error);
|
|
12
|
+
};
|
|
13
|
+
sock.once("error", onError);
|
|
14
|
+
sock.bind(0, () => {
|
|
15
|
+
cleanup();
|
|
16
|
+
resolve();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
return sock;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
await closeDgramSocket(sock);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Close a dgram socket and wait for the fd to actually be released.
|
|
28
|
+
*
|
|
29
|
+
* Never rejects and never hangs: `close()` throws ERR_SOCKET_DGRAM_NOT_RUNNING
|
|
30
|
+
* on an already-closed socket, and a socket caught mid-teardown may never run
|
|
31
|
+
* the close callback at all. Both must settle, because callers await this on
|
|
32
|
+
* the TURN failure path — a hang there would leave `#turnAllocating` pending
|
|
33
|
+
* forever and wedge every later caller awaiting that same promise, and would
|
|
34
|
+
* also block `Peer.stop()`.
|
|
35
|
+
*/
|
|
36
|
+
export async function closeDgramSocket(sock, timeoutMs = 2000) {
|
|
37
|
+
if (!sock)
|
|
38
|
+
return;
|
|
39
|
+
await new Promise((resolve) => {
|
|
40
|
+
let done = false;
|
|
41
|
+
let timer;
|
|
42
|
+
const finish = () => {
|
|
43
|
+
if (done)
|
|
44
|
+
return;
|
|
45
|
+
done = true;
|
|
46
|
+
if (timer)
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
sock.off("close", finish);
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
// Belt and braces: the callback, the 'close' event, or the timer —
|
|
52
|
+
// whichever comes first releases the caller.
|
|
53
|
+
timer = setTimeout(finish, timeoutMs);
|
|
54
|
+
// Don't keep the event loop alive just to observe a close.
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
try {
|
|
57
|
+
sock.once("close", finish);
|
|
58
|
+
sock.close(finish);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
finish();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.124",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|