@gjsify/dgram 0.3.21 → 0.4.3
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/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/index.js +1 -1
- package/package.json +46 -43
- package/src/index.spec.ts +0 -1214
- package/src/index.ts +0 -577
- package/src/test.mts +0 -3
- package/tsconfig.json +0 -29
- package/tsconfig.tsbuildinfo +0 -1
package/src/index.ts
DELETED
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
// Node.js dgram module for GJS — UDP sockets via Gio.Socket
|
|
2
|
-
// Reference: Node.js lib/dgram.js
|
|
3
|
-
|
|
4
|
-
import Gio from '@girs/gio-2.0';
|
|
5
|
-
import GLib from '@girs/glib-2.0';
|
|
6
|
-
import { EventEmitter } from 'node:events';
|
|
7
|
-
import { Buffer } from 'node:buffer';
|
|
8
|
-
import { deferEmit, ensureMainLoop } from '@gjsify/utils';
|
|
9
|
-
|
|
10
|
-
export interface SocketOptions {
|
|
11
|
-
type: 'udp4' | 'udp6';
|
|
12
|
-
reuseAddr?: boolean;
|
|
13
|
-
reusePort?: boolean;
|
|
14
|
-
ipv6Only?: boolean;
|
|
15
|
-
recvBufferSize?: number;
|
|
16
|
-
sendBufferSize?: number;
|
|
17
|
-
signal?: AbortSignal;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface AddressInfo {
|
|
21
|
-
address: string;
|
|
22
|
-
family: string;
|
|
23
|
-
port: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* dgram.Socket — UDP socket wrapping Gio.Socket.
|
|
28
|
-
*/
|
|
29
|
-
interface RemoteAddressInfo {
|
|
30
|
-
address: string;
|
|
31
|
-
family: string;
|
|
32
|
-
port: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// GC guard — GJS garbage-collects objects with no JS references.
|
|
36
|
-
// Keep strong references to bound UDP sockets to prevent their
|
|
37
|
-
// Gio.Socket from being collected while receiving data.
|
|
38
|
-
const _activeSockets = new Set<Socket>();
|
|
39
|
-
|
|
40
|
-
export class Socket extends EventEmitter {
|
|
41
|
-
readonly type: 'udp4' | 'udp6';
|
|
42
|
-
|
|
43
|
-
private _socket: Gio.Socket | null = null;
|
|
44
|
-
private _bound = false;
|
|
45
|
-
private _closed = false;
|
|
46
|
-
private _receiving = false;
|
|
47
|
-
private _address: AddressInfo = { address: '0.0.0.0', family: 'IPv4', port: 0 };
|
|
48
|
-
private _cancellable: Gio.Cancellable = new Gio.Cancellable();
|
|
49
|
-
// Strong JS ref to the GSocketSource so SM GC cannot finalize the
|
|
50
|
-
// BoxedInstance while GLib's main context still holds it.
|
|
51
|
-
private _readSource: GLib.Source | null = null;
|
|
52
|
-
private _reuseAddr: boolean;
|
|
53
|
-
private _connected = false;
|
|
54
|
-
private _remoteAddress: RemoteAddressInfo | null = null;
|
|
55
|
-
|
|
56
|
-
constructor(options: SocketOptions | string) {
|
|
57
|
-
super();
|
|
58
|
-
|
|
59
|
-
if (typeof options === 'string') {
|
|
60
|
-
this.type = options as 'udp4' | 'udp6';
|
|
61
|
-
this._reuseAddr = false;
|
|
62
|
-
} else {
|
|
63
|
-
this.type = options.type;
|
|
64
|
-
this._reuseAddr = options.reuseAddr ?? false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const family = this.type === 'udp6' ? Gio.SocketFamily.IPV6 : Gio.SocketFamily.IPV4;
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
this._socket = Gio.Socket.new(family, Gio.SocketType.DATAGRAM, Gio.SocketProtocol.UDP);
|
|
71
|
-
this._socket.set_blocking(false);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
this._socket = null;
|
|
74
|
-
// Defer error emission
|
|
75
|
-
deferEmit(this, 'error', err);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Bind the socket to a port and optional address.
|
|
81
|
-
*/
|
|
82
|
-
bind(port?: number | { port?: number; address?: string; exclusive?: boolean }, address?: string | (() => void), callback?: () => void): this {
|
|
83
|
-
if (this._closed || !this._socket) return this;
|
|
84
|
-
|
|
85
|
-
let bindPort = 0;
|
|
86
|
-
let bindAddress = this.type === 'udp6' ? '::' : '0.0.0.0';
|
|
87
|
-
|
|
88
|
-
if (typeof port === 'object') {
|
|
89
|
-
const opts = port;
|
|
90
|
-
bindPort = opts.port || 0;
|
|
91
|
-
bindAddress = opts.address || bindAddress;
|
|
92
|
-
if (typeof address === 'function') callback = address;
|
|
93
|
-
} else if (typeof port === 'number') {
|
|
94
|
-
bindPort = port;
|
|
95
|
-
if (typeof address === 'string') bindAddress = address;
|
|
96
|
-
else if (typeof address === 'function') callback = address;
|
|
97
|
-
} else if (typeof port === 'function') {
|
|
98
|
-
callback = port;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (callback) this.once('listening', callback);
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const family = this.type === 'udp6' ? Gio.SocketFamily.IPV6 : Gio.SocketFamily.IPV4;
|
|
105
|
-
const inetAddr = Gio.InetAddress.new_from_string(bindAddress) ||
|
|
106
|
-
(family === Gio.SocketFamily.IPV6 ? Gio.InetAddress.new_any(Gio.SocketFamily.IPV6) : Gio.InetAddress.new_any(Gio.SocketFamily.IPV4));
|
|
107
|
-
const sockAddr = new Gio.InetSocketAddress({ address: inetAddr, port: bindPort });
|
|
108
|
-
|
|
109
|
-
this._socket.bind(sockAddr, this._reuseAddr);
|
|
110
|
-
this._bound = true;
|
|
111
|
-
_activeSockets.add(this);
|
|
112
|
-
ensureMainLoop();
|
|
113
|
-
|
|
114
|
-
// Get actual bound address
|
|
115
|
-
const localAddr = this._socket.get_local_address() as Gio.InetSocketAddress;
|
|
116
|
-
if (localAddr) {
|
|
117
|
-
this._address = {
|
|
118
|
-
address: localAddr.get_address().to_string(),
|
|
119
|
-
family: this.type === 'udp6' ? 'IPv6' : 'IPv4',
|
|
120
|
-
port: localAddr.get_port(),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
setTimeout(() => {
|
|
125
|
-
this.emit('listening');
|
|
126
|
-
this._startReceiving();
|
|
127
|
-
}, 0);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
deferEmit(this, 'error', err);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return this;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Send a message.
|
|
137
|
-
*/
|
|
138
|
-
send(
|
|
139
|
-
msg: Buffer | string | Uint8Array | (Buffer | string | Uint8Array)[],
|
|
140
|
-
offset?: number | ((err: Error | null, bytes: number) => void),
|
|
141
|
-
length?: number,
|
|
142
|
-
port?: number,
|
|
143
|
-
address?: string | ((err: Error | null, bytes: number) => void),
|
|
144
|
-
callback?: (err: Error | null, bytes: number) => void,
|
|
145
|
-
): void {
|
|
146
|
-
if (this._closed || !this._socket) return;
|
|
147
|
-
|
|
148
|
-
// Handle overloaded signatures:
|
|
149
|
-
// send(msg, port, address, callback)
|
|
150
|
-
// send(msg, offset, length, port, address, callback)
|
|
151
|
-
let buf: Buffer;
|
|
152
|
-
let destPort: number;
|
|
153
|
-
let destAddress: string;
|
|
154
|
-
let cb: ((err: Error | null, bytes: number) => void) | undefined;
|
|
155
|
-
|
|
156
|
-
if (typeof offset === 'function') {
|
|
157
|
-
// send(msg, callback)
|
|
158
|
-
cb = offset;
|
|
159
|
-
buf = this._toBuffer(msg);
|
|
160
|
-
destPort = this._address.port;
|
|
161
|
-
destAddress = this._address.address;
|
|
162
|
-
} else if (typeof offset === 'number' && typeof length === 'string') {
|
|
163
|
-
// send(msg, port, address, callback) — offset=port, length=address, port=callback
|
|
164
|
-
destPort = offset;
|
|
165
|
-
destAddress = length;
|
|
166
|
-
cb = port as unknown as ((err: Error | null, bytes: number) => void) | undefined;
|
|
167
|
-
buf = this._toBuffer(msg);
|
|
168
|
-
} else if (typeof offset === 'number' && typeof length === 'number' && typeof address === 'function') {
|
|
169
|
-
// send(msg, offset, length, port, callback)
|
|
170
|
-
cb = address;
|
|
171
|
-
destPort = port!;
|
|
172
|
-
destAddress = this.type === 'udp6' ? '::1' : '127.0.0.1';
|
|
173
|
-
buf = this._toBufferSlice(msg, offset, length);
|
|
174
|
-
} else if (typeof offset === 'number' && typeof length === 'number') {
|
|
175
|
-
// send(msg, offset, length, port, address, callback)
|
|
176
|
-
destPort = port!;
|
|
177
|
-
destAddress = (address as string) || (this.type === 'udp6' ? '::1' : '127.0.0.1');
|
|
178
|
-
cb = callback;
|
|
179
|
-
buf = this._toBufferSlice(msg, offset, length);
|
|
180
|
-
} else {
|
|
181
|
-
// send(msg, port) or similar — best effort
|
|
182
|
-
destPort = Number(offset) || 0;
|
|
183
|
-
destAddress = this.type === 'udp6' ? '::1' : '127.0.0.1';
|
|
184
|
-
buf = this._toBuffer(msg);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
this._resolveAndSend(destAddress, destPort, buf, cb);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private _autoBind(): void {
|
|
191
|
-
if (this._bound || !this._socket) return;
|
|
192
|
-
const anyAddr = this.type === 'udp6'
|
|
193
|
-
? Gio.InetAddress.new_any(Gio.SocketFamily.IPV6)
|
|
194
|
-
: Gio.InetAddress.new_any(Gio.SocketFamily.IPV4);
|
|
195
|
-
const anySockAddr = new Gio.InetSocketAddress({ address: anyAddr, port: 0 });
|
|
196
|
-
this._socket.bind(anySockAddr, false);
|
|
197
|
-
this._bound = true;
|
|
198
|
-
_activeSockets.add(this);
|
|
199
|
-
ensureMainLoop();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Socket family that matches `this.type` (udp4 → IPv4, udp6 → IPv6). */
|
|
203
|
-
private _socketFamily(): Gio.SocketFamily {
|
|
204
|
-
return this.type === 'udp6' ? Gio.SocketFamily.IPV6 : Gio.SocketFamily.IPV4;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** True when `addr` can be sent to from a socket bound with `this._socketFamily()`. */
|
|
208
|
-
private _isAddressCompatible(addr: Gio.InetAddress): boolean {
|
|
209
|
-
return addr.get_family() === this._socketFamily();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Build an EINVAL error for a destination whose family doesn't match the socket.
|
|
213
|
-
* Matches Node.js dgram behavior (node reports EINVAL from sendto(2) when the
|
|
214
|
-
* destination address family is incompatible with the bound socket). */
|
|
215
|
-
private _mismatchError(destAddress: string): NodeJS.ErrnoException {
|
|
216
|
-
const want = this.type === 'udp6' ? 'IPv6' : 'IPv4';
|
|
217
|
-
const err = new Error(
|
|
218
|
-
`EINVAL: cannot send to ${destAddress} from a ${want} socket (address-family mismatch)`,
|
|
219
|
-
) as NodeJS.ErrnoException;
|
|
220
|
-
err.code = 'EINVAL';
|
|
221
|
-
err.syscall = 'send';
|
|
222
|
-
return err;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private _resolveAndSend(destAddress: string, destPort: number, buf: Buffer, cb: ((err: Error | null, bytes: number) => void) | undefined): void {
|
|
226
|
-
if (this._closed || !this._socket) {
|
|
227
|
-
if (cb) cb(new Error('Socket is closed'), 0);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Try numeric IP first (fast path, no DNS)
|
|
232
|
-
let inetAddr = Gio.InetAddress.new_from_string(destAddress);
|
|
233
|
-
|
|
234
|
-
const doSend = (addr: Gio.InetAddress) => {
|
|
235
|
-
// Address-family mismatch — e.g. IPv6 destination, IPv4-bound socket.
|
|
236
|
-
// Gio.Socket.send_to would throw Gio.IOErrorEnum.NOT_SUPPORTED
|
|
237
|
-
// ("Die Adressfamilie wird von der Protokollfamilie nicht unterstützt").
|
|
238
|
-
// DHT/tracker code treats this as a recoverable warning and keeps going;
|
|
239
|
-
// surface it as ENETUNREACH so consumers see a familiar Node errno
|
|
240
|
-
// instead of a raw GLib German message.
|
|
241
|
-
if (!this._isAddressCompatible(addr)) {
|
|
242
|
-
const err = this._mismatchError(destAddress);
|
|
243
|
-
if (cb) cb(err, 0);
|
|
244
|
-
else this.emit('error', err);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
this._autoBind();
|
|
249
|
-
const sockAddr = new Gio.InetSocketAddress({ address: addr, port: destPort });
|
|
250
|
-
const bytesSent = this._socket!.send_to(sockAddr, buf, this._cancellable);
|
|
251
|
-
if (cb) cb(null, bytesSent);
|
|
252
|
-
} catch (err) {
|
|
253
|
-
if (cb) cb(err instanceof Error ? err : new Error(String(err)), 0);
|
|
254
|
-
else this.emit('error', err);
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
if (inetAddr) {
|
|
259
|
-
doSend(inetAddr);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Hostname — resolve asynchronously via Gio.Resolver
|
|
264
|
-
const resolver = Gio.Resolver.get_default();
|
|
265
|
-
resolver.lookup_by_name_async(destAddress, this._cancellable, (_obj, res) => {
|
|
266
|
-
try {
|
|
267
|
-
const addresses = resolver.lookup_by_name_finish(res);
|
|
268
|
-
if (!addresses || addresses.length === 0) {
|
|
269
|
-
const err = new Error(`ENOTFOUND ${destAddress}`) as NodeJS.ErrnoException;
|
|
270
|
-
err.code = 'ENOTFOUND';
|
|
271
|
-
if (cb) cb(err, 0);
|
|
272
|
-
else this.emit('error', err);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
// Prefer an address whose family matches this socket. Without this,
|
|
276
|
-
// picking addresses[0] (which can be IPv6) for an IPv4-bound socket
|
|
277
|
-
// hits EAFNOSUPPORT deep in Gio.Socket.send_to. DHT code queries many
|
|
278
|
-
// tracker/bootstrap hostnames that return mixed A / AAAA records.
|
|
279
|
-
const compatible = addresses.find(a => this._isAddressCompatible(a));
|
|
280
|
-
if (!compatible) {
|
|
281
|
-
const err = this._mismatchError(destAddress);
|
|
282
|
-
if (cb) cb(err, 0);
|
|
283
|
-
else this.emit('error', err);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
doSend(compatible);
|
|
287
|
-
} catch (err) {
|
|
288
|
-
if (cb) cb(err instanceof Error ? err : new Error(String(err)), 0);
|
|
289
|
-
else this.emit('error', err);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private _toBuffer(msg: Buffer | string | Uint8Array | (Buffer | string | Uint8Array)[]): Buffer {
|
|
295
|
-
if (Array.isArray(msg)) {
|
|
296
|
-
return Buffer.concat(msg.map(m => typeof m === 'string' ? Buffer.from(m) : Buffer.from(m)));
|
|
297
|
-
}
|
|
298
|
-
return typeof msg === 'string' ? Buffer.from(msg) : Buffer.from(msg);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private _toBufferSlice(msg: Buffer | string | Uint8Array | (Buffer | string | Uint8Array)[], offset: number, length: number): Buffer {
|
|
302
|
-
const buf = this._toBuffer(msg);
|
|
303
|
-
return Buffer.from(buf.buffer, buf.byteOffset + offset, length);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Close the socket.
|
|
308
|
-
*/
|
|
309
|
-
close(callback?: () => void): this {
|
|
310
|
-
if (this._closed) {
|
|
311
|
-
throw new Error('Not running');
|
|
312
|
-
}
|
|
313
|
-
this._closed = true;
|
|
314
|
-
_activeSockets.delete(this);
|
|
315
|
-
|
|
316
|
-
if (callback) this.once('close', callback);
|
|
317
|
-
|
|
318
|
-
// Destroy the read source before cancelling the shared cancellable — a
|
|
319
|
-
// pending callback could fire in between otherwise and see the cancellable
|
|
320
|
-
// already cancelled, emitting an unwanted 'error' on the closed socket.
|
|
321
|
-
if (this._readSource) {
|
|
322
|
-
try { this._readSource.destroy(); } catch (_e) { /* ignore */ }
|
|
323
|
-
this._readSource = null;
|
|
324
|
-
}
|
|
325
|
-
this._cancellable.cancel();
|
|
326
|
-
|
|
327
|
-
if (this._socket) {
|
|
328
|
-
try {
|
|
329
|
-
this._socket.close();
|
|
330
|
-
} catch (_e) {
|
|
331
|
-
// Ignore close errors
|
|
332
|
-
}
|
|
333
|
-
this._socket = null;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
deferEmit(this, 'close');
|
|
337
|
-
return this;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Associate the socket with a remote address/port (connected UDP).
|
|
342
|
-
* After connect(), send() can omit address and port.
|
|
343
|
-
*/
|
|
344
|
-
connect(port: number, address?: string | (() => void), callback?: () => void): void {
|
|
345
|
-
if (this._connected) {
|
|
346
|
-
const err = new Error('Already connected') as NodeJS.ErrnoException;
|
|
347
|
-
err.code = 'ERR_SOCKET_DGRAM_IS_CONNECTED';
|
|
348
|
-
throw err;
|
|
349
|
-
}
|
|
350
|
-
if (!port || port <= 0 || port >= 65536) {
|
|
351
|
-
const err = new RangeError(`Port should be > 0 and < 65536. Received ${port}.`) as NodeJS.ErrnoException;
|
|
352
|
-
err.code = 'ERR_SOCKET_BAD_PORT';
|
|
353
|
-
throw err;
|
|
354
|
-
}
|
|
355
|
-
let resolvedAddr: string;
|
|
356
|
-
let cb: (() => void) | undefined;
|
|
357
|
-
if (typeof address === 'function') {
|
|
358
|
-
cb = address;
|
|
359
|
-
resolvedAddr = this.type === 'udp6' ? '::1' : '127.0.0.1';
|
|
360
|
-
} else {
|
|
361
|
-
resolvedAddr = address || (this.type === 'udp6' ? '::1' : '127.0.0.1');
|
|
362
|
-
cb = callback;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
this._connected = true;
|
|
366
|
-
this._remoteAddress = {
|
|
367
|
-
address: resolvedAddr,
|
|
368
|
-
family: this.type === 'udp6' ? 'IPv6' : 'IPv4',
|
|
369
|
-
port,
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
if (cb) {
|
|
373
|
-
// Emit connect asynchronously (matches Node.js behaviour)
|
|
374
|
-
Promise.resolve().then(() => {
|
|
375
|
-
this.emit('connect');
|
|
376
|
-
cb!();
|
|
377
|
-
});
|
|
378
|
-
} else {
|
|
379
|
-
Promise.resolve().then(() => this.emit('connect'));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Dissociate a connected socket from its remote address.
|
|
385
|
-
*/
|
|
386
|
-
disconnect(): void {
|
|
387
|
-
if (!this._connected) {
|
|
388
|
-
const err = new Error('Not connected') as NodeJS.ErrnoException;
|
|
389
|
-
err.code = 'ERR_SOCKET_DGRAM_NOT_CONNECTED';
|
|
390
|
-
throw err;
|
|
391
|
-
}
|
|
392
|
-
this._connected = false;
|
|
393
|
-
this._remoteAddress = null;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Returns the remote address of a connected socket.
|
|
398
|
-
* Throws ERR_SOCKET_DGRAM_NOT_CONNECTED if not connected.
|
|
399
|
-
*/
|
|
400
|
-
remoteAddress(): RemoteAddressInfo {
|
|
401
|
-
if (!this._connected || !this._remoteAddress) {
|
|
402
|
-
const err = new Error('Not connected') as NodeJS.ErrnoException;
|
|
403
|
-
err.code = 'ERR_SOCKET_DGRAM_NOT_CONNECTED';
|
|
404
|
-
throw err;
|
|
405
|
-
}
|
|
406
|
-
return { ...this._remoteAddress };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Get the bound address info.
|
|
411
|
-
*/
|
|
412
|
-
address(): AddressInfo {
|
|
413
|
-
return { ...this._address };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Set the broadcast flag.
|
|
418
|
-
*/
|
|
419
|
-
setBroadcast(flag: boolean): void {
|
|
420
|
-
if (this._socket) {
|
|
421
|
-
this._socket.set_broadcast(flag);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Set the TTL.
|
|
427
|
-
*/
|
|
428
|
-
setTTL(ttl: number): number {
|
|
429
|
-
if (this._socket) {
|
|
430
|
-
this._socket.set_ttl(ttl);
|
|
431
|
-
}
|
|
432
|
-
return ttl;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Set multicast TTL.
|
|
437
|
-
*/
|
|
438
|
-
setMulticastTTL(ttl: number): number {
|
|
439
|
-
if (this._socket) {
|
|
440
|
-
this._socket.set_multicast_ttl(ttl);
|
|
441
|
-
}
|
|
442
|
-
return ttl;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Set multicast loopback.
|
|
447
|
-
*/
|
|
448
|
-
setMulticastLoopback(flag: boolean): boolean {
|
|
449
|
-
if (this._socket) {
|
|
450
|
-
this._socket.set_multicast_loopback(flag);
|
|
451
|
-
}
|
|
452
|
-
return flag;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Add multicast group membership.
|
|
457
|
-
*/
|
|
458
|
-
addMembership(multicastAddress: string, multicastInterface?: string): void {
|
|
459
|
-
if (!this._socket) return;
|
|
460
|
-
try {
|
|
461
|
-
const mcastAddr = Gio.InetAddress.new_from_string(multicastAddress);
|
|
462
|
-
this._socket.join_multicast_group(mcastAddr, false, multicastInterface || null);
|
|
463
|
-
} catch (err) {
|
|
464
|
-
this.emit('error', err);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Drop multicast group membership.
|
|
470
|
-
*/
|
|
471
|
-
dropMembership(multicastAddress: string, multicastInterface?: string): void {
|
|
472
|
-
if (!this._socket) return;
|
|
473
|
-
try {
|
|
474
|
-
const mcastAddr = Gio.InetAddress.new_from_string(multicastAddress);
|
|
475
|
-
this._socket.leave_multicast_group(mcastAddr, false, multicastInterface || null);
|
|
476
|
-
} catch (err) {
|
|
477
|
-
this.emit('error', err);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Set multicast interface.
|
|
483
|
-
*/
|
|
484
|
-
setMulticastInterface(_interfaceAddress: string): void {
|
|
485
|
-
// GLib handles this via join_multicast_group interface parameter
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Ref the socket (keep event loop alive). */
|
|
489
|
-
ref(): this {
|
|
490
|
-
return this;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/** Unref the socket (allow event loop to exit). */
|
|
494
|
-
unref(): this {
|
|
495
|
-
return this;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/** Get/Set receive buffer size. */
|
|
499
|
-
getRecvBufferSize(): number {
|
|
500
|
-
return 65536; // Default
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
setRecvBufferSize(_size: number): void {
|
|
504
|
-
// Gio.Socket doesn't expose SO_RCVBUF directly
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** Get/Set send buffer size. */
|
|
508
|
-
getSendBufferSize(): number {
|
|
509
|
-
return 65536; // Default
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
setSendBufferSize(_size: number): void {
|
|
513
|
-
// Gio.Socket doesn't expose SO_SNDBUF directly
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Start receiving messages in background.
|
|
518
|
-
*/
|
|
519
|
-
private _startReceiving(): void {
|
|
520
|
-
if (this._receiving || this._closed || !this._socket) return;
|
|
521
|
-
this._receiving = true;
|
|
522
|
-
this._receiveLoop();
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
private _receiveLoop(): void {
|
|
526
|
-
if (this._closed || !this._socket || this._readSource) return;
|
|
527
|
-
|
|
528
|
-
// Event-driven receive via GSocketSource: the source fires only when the
|
|
529
|
-
// kernel reports data available (IOCondition.IN). Previously a
|
|
530
|
-
// setTimeout(fn, 0) polling loop saturated the main loop with 0 ms timers
|
|
531
|
-
// whenever any UDP traffic was in flight — WebTorrent DHT under load
|
|
532
|
-
// starved GTK paint + other default-priority timers. receive_bytes_from is
|
|
533
|
-
// GLib 2.80+ and non-blocking (timeout_us=0); receive_from() isn't
|
|
534
|
-
// introspectable because its buffer arg is caller-allocated.
|
|
535
|
-
const source = this._socket.create_source(GLib.IOCondition.IN, this._cancellable);
|
|
536
|
-
this._readSource = source;
|
|
537
|
-
source.set_callback(() => {
|
|
538
|
-
if (this._closed || !this._socket) return GLib.SOURCE_REMOVE;
|
|
539
|
-
try {
|
|
540
|
-
const [bytes, srcAddr] = this._socket.receive_bytes_from(65536, 0, this._cancellable);
|
|
541
|
-
if (bytes && srcAddr) {
|
|
542
|
-
const data = Buffer.from(bytes.get_data()!);
|
|
543
|
-
const inetSockAddr = srcAddr as Gio.InetSocketAddress;
|
|
544
|
-
const rinfo: AddressInfo = {
|
|
545
|
-
address: inetSockAddr.get_address().to_string(),
|
|
546
|
-
family: this.type === 'udp6' ? 'IPv6' : 'IPv4',
|
|
547
|
-
port: inetSockAddr.get_port(),
|
|
548
|
-
};
|
|
549
|
-
if (data.length > 0) this.emit('message', data, rinfo);
|
|
550
|
-
}
|
|
551
|
-
} catch (err) {
|
|
552
|
-
const errObj = err as { code?: number };
|
|
553
|
-
if (!this._closed && errObj.code !== Gio.IOErrorEnum.CANCELLED) {
|
|
554
|
-
this.emit('error', err);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return GLib.SOURCE_CONTINUE;
|
|
558
|
-
});
|
|
559
|
-
source.attach(null);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Create a UDP socket.
|
|
565
|
-
*/
|
|
566
|
-
export function createSocket(type: 'udp4' | 'udp6' | SocketOptions, callback?: (msg: Buffer, rinfo: AddressInfo) => void): Socket {
|
|
567
|
-
const opts = typeof type === 'string' ? { type } : type;
|
|
568
|
-
const socket = new Socket(opts);
|
|
569
|
-
|
|
570
|
-
if (callback) {
|
|
571
|
-
socket.on('message', callback);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return socket;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
export default { Socket, createSocket };
|
package/src/test.mts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "ESNext",
|
|
4
|
-
"target": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"types": [
|
|
7
|
-
"node"
|
|
8
|
-
],
|
|
9
|
-
"experimentalDecorators": true,
|
|
10
|
-
"emitDeclarationOnly": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"outDir": "lib",
|
|
14
|
-
"rootDir": "src",
|
|
15
|
-
"declarationDir": "lib/types",
|
|
16
|
-
"composite": true,
|
|
17
|
-
"skipLibCheck": true,
|
|
18
|
-
"allowJs": true,
|
|
19
|
-
"checkJs": false,
|
|
20
|
-
"strict": false
|
|
21
|
-
},
|
|
22
|
-
"include": [
|
|
23
|
-
"src/**/*.ts"
|
|
24
|
-
],
|
|
25
|
-
"exclude": [
|
|
26
|
-
"src/test.ts",
|
|
27
|
-
"src/test.mts"
|
|
28
|
-
]
|
|
29
|
-
}
|