@gjsify/dgram 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @gjsify/dgram
2
+
3
+ GJS implementation of the Node.js `dgram` module using Gio.Socket for UDP networking.
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @gjsify/dgram
11
+ # or
12
+ yarn add @gjsify/dgram
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { createSocket } from '@gjsify/dgram';
19
+
20
+ const socket = createSocket('udp4');
21
+ socket.bind(41234);
22
+ socket.on('message', (msg, rinfo) => {
23
+ console.log(`Received: ${msg} from ${rinfo.address}:${rinfo.port}`);
24
+ });
25
+ ```
26
+
27
+ ## License
28
+
29
+ MIT
@@ -0,0 +1,370 @@
1
+ import Gio from "@girs/gio-2.0";
2
+ import GLib from "@girs/glib-2.0";
3
+ import { EventEmitter } from "node:events";
4
+ import { Buffer } from "node:buffer";
5
+ import { deferEmit, ensureMainLoop } from "@gjsify/utils";
6
+ const _activeSockets = /* @__PURE__ */ new Set();
7
+ class Socket extends EventEmitter {
8
+ type;
9
+ _socket = null;
10
+ _bound = false;
11
+ _closed = false;
12
+ _receiving = false;
13
+ _address = { address: "0.0.0.0", family: "IPv4", port: 0 };
14
+ _cancellable = new Gio.Cancellable();
15
+ _reuseAddr;
16
+ _connected = false;
17
+ _remoteAddress = null;
18
+ constructor(options) {
19
+ super();
20
+ if (typeof options === "string") {
21
+ this.type = options;
22
+ this._reuseAddr = false;
23
+ } else {
24
+ this.type = options.type;
25
+ this._reuseAddr = options.reuseAddr ?? false;
26
+ }
27
+ const family = this.type === "udp6" ? Gio.SocketFamily.IPV6 : Gio.SocketFamily.IPV4;
28
+ try {
29
+ this._socket = Gio.Socket.new(family, Gio.SocketType.DATAGRAM, Gio.SocketProtocol.UDP);
30
+ this._socket.set_blocking(false);
31
+ } catch (err) {
32
+ this._socket = null;
33
+ deferEmit(this, "error", err);
34
+ }
35
+ }
36
+ /**
37
+ * Bind the socket to a port and optional address.
38
+ */
39
+ bind(port, address, callback) {
40
+ if (this._closed || !this._socket) return this;
41
+ let bindPort = 0;
42
+ let bindAddress = this.type === "udp6" ? "::" : "0.0.0.0";
43
+ if (typeof port === "object") {
44
+ const opts = port;
45
+ bindPort = opts.port || 0;
46
+ bindAddress = opts.address || bindAddress;
47
+ if (typeof address === "function") callback = address;
48
+ } else if (typeof port === "number") {
49
+ bindPort = port;
50
+ if (typeof address === "string") bindAddress = address;
51
+ else if (typeof address === "function") callback = address;
52
+ } else if (typeof port === "function") {
53
+ callback = port;
54
+ }
55
+ if (callback) this.once("listening", callback);
56
+ try {
57
+ const family = this.type === "udp6" ? Gio.SocketFamily.IPV6 : Gio.SocketFamily.IPV4;
58
+ const inetAddr = Gio.InetAddress.new_from_string(bindAddress) || (family === Gio.SocketFamily.IPV6 ? Gio.InetAddress.new_any(Gio.SocketFamily.IPV6) : Gio.InetAddress.new_any(Gio.SocketFamily.IPV4));
59
+ const sockAddr = new Gio.InetSocketAddress({ address: inetAddr, port: bindPort });
60
+ this._socket.bind(sockAddr, this._reuseAddr);
61
+ this._bound = true;
62
+ _activeSockets.add(this);
63
+ ensureMainLoop();
64
+ const localAddr = this._socket.get_local_address();
65
+ if (localAddr) {
66
+ this._address = {
67
+ address: localAddr.get_address().to_string(),
68
+ family: this.type === "udp6" ? "IPv6" : "IPv4",
69
+ port: localAddr.get_port()
70
+ };
71
+ }
72
+ setTimeout(() => {
73
+ this.emit("listening");
74
+ this._startReceiving();
75
+ }, 0);
76
+ } catch (err) {
77
+ deferEmit(this, "error", err);
78
+ }
79
+ return this;
80
+ }
81
+ /**
82
+ * Send a message.
83
+ */
84
+ send(msg, offset, length, port, address, callback) {
85
+ if (this._closed || !this._socket) return;
86
+ let buf;
87
+ let destPort;
88
+ let destAddress;
89
+ let cb;
90
+ if (typeof offset === "function") {
91
+ cb = offset;
92
+ buf = this._toBuffer(msg);
93
+ destPort = this._address.port;
94
+ destAddress = this._address.address;
95
+ } else if (typeof offset === "number" && typeof length === "string") {
96
+ destPort = offset;
97
+ destAddress = length;
98
+ cb = port;
99
+ buf = this._toBuffer(msg);
100
+ } else if (typeof offset === "number" && typeof length === "number" && typeof address === "function") {
101
+ cb = address;
102
+ destPort = port;
103
+ destAddress = this.type === "udp6" ? "::1" : "127.0.0.1";
104
+ buf = this._toBufferSlice(msg, offset, length);
105
+ } else if (typeof offset === "number" && typeof length === "number") {
106
+ destPort = port;
107
+ destAddress = address || (this.type === "udp6" ? "::1" : "127.0.0.1");
108
+ cb = callback;
109
+ buf = this._toBufferSlice(msg, offset, length);
110
+ } else {
111
+ destPort = Number(offset) || 0;
112
+ destAddress = this.type === "udp6" ? "::1" : "127.0.0.1";
113
+ buf = this._toBuffer(msg);
114
+ }
115
+ try {
116
+ const inetAddr = Gio.InetAddress.new_from_string(destAddress);
117
+ const sockAddr = new Gio.InetSocketAddress({ address: inetAddr, port: destPort });
118
+ if (!this._bound) {
119
+ const anyAddr = this.type === "udp6" ? Gio.InetAddress.new_any(Gio.SocketFamily.IPV6) : Gio.InetAddress.new_any(Gio.SocketFamily.IPV4);
120
+ const anySockAddr = new Gio.InetSocketAddress({ address: anyAddr, port: 0 });
121
+ this._socket.bind(anySockAddr, false);
122
+ this._bound = true;
123
+ }
124
+ const bytesSent = this._socket.send_to(sockAddr, buf, this._cancellable);
125
+ if (cb) cb(null, bytesSent);
126
+ } catch (err) {
127
+ if (cb) cb(err instanceof Error ? err : new Error(String(err)), 0);
128
+ else this.emit("error", err);
129
+ }
130
+ }
131
+ _toBuffer(msg) {
132
+ if (Array.isArray(msg)) {
133
+ return Buffer.concat(msg.map((m) => typeof m === "string" ? Buffer.from(m) : Buffer.from(m)));
134
+ }
135
+ return typeof msg === "string" ? Buffer.from(msg) : Buffer.from(msg);
136
+ }
137
+ _toBufferSlice(msg, offset, length) {
138
+ const buf = this._toBuffer(msg);
139
+ return Buffer.from(buf.buffer, buf.byteOffset + offset, length);
140
+ }
141
+ /**
142
+ * Close the socket.
143
+ */
144
+ close(callback) {
145
+ if (this._closed) {
146
+ throw new Error("Not running");
147
+ }
148
+ this._closed = true;
149
+ _activeSockets.delete(this);
150
+ if (callback) this.once("close", callback);
151
+ this._cancellable.cancel();
152
+ if (this._socket) {
153
+ try {
154
+ this._socket.close();
155
+ } catch (_e) {
156
+ }
157
+ this._socket = null;
158
+ }
159
+ deferEmit(this, "close");
160
+ return this;
161
+ }
162
+ /**
163
+ * Associate the socket with a remote address/port (connected UDP).
164
+ * After connect(), send() can omit address and port.
165
+ */
166
+ connect(port, address, callback) {
167
+ if (this._connected) {
168
+ const err = new Error("Already connected");
169
+ err.code = "ERR_SOCKET_DGRAM_IS_CONNECTED";
170
+ throw err;
171
+ }
172
+ if (!port || port <= 0 || port >= 65536) {
173
+ const err = new RangeError(`Port should be > 0 and < 65536. Received ${port}.`);
174
+ err.code = "ERR_SOCKET_BAD_PORT";
175
+ throw err;
176
+ }
177
+ let resolvedAddr;
178
+ let cb;
179
+ if (typeof address === "function") {
180
+ cb = address;
181
+ resolvedAddr = this.type === "udp6" ? "::1" : "127.0.0.1";
182
+ } else {
183
+ resolvedAddr = address || (this.type === "udp6" ? "::1" : "127.0.0.1");
184
+ cb = callback;
185
+ }
186
+ this._connected = true;
187
+ this._remoteAddress = {
188
+ address: resolvedAddr,
189
+ family: this.type === "udp6" ? "IPv6" : "IPv4",
190
+ port
191
+ };
192
+ if (cb) {
193
+ Promise.resolve().then(() => {
194
+ this.emit("connect");
195
+ cb();
196
+ });
197
+ } else {
198
+ Promise.resolve().then(() => this.emit("connect"));
199
+ }
200
+ }
201
+ /**
202
+ * Dissociate a connected socket from its remote address.
203
+ */
204
+ disconnect() {
205
+ if (!this._connected) {
206
+ const err = new Error("Not connected");
207
+ err.code = "ERR_SOCKET_DGRAM_NOT_CONNECTED";
208
+ throw err;
209
+ }
210
+ this._connected = false;
211
+ this._remoteAddress = null;
212
+ }
213
+ /**
214
+ * Returns the remote address of a connected socket.
215
+ * Throws ERR_SOCKET_DGRAM_NOT_CONNECTED if not connected.
216
+ */
217
+ remoteAddress() {
218
+ if (!this._connected || !this._remoteAddress) {
219
+ const err = new Error("Not connected");
220
+ err.code = "ERR_SOCKET_DGRAM_NOT_CONNECTED";
221
+ throw err;
222
+ }
223
+ return { ...this._remoteAddress };
224
+ }
225
+ /**
226
+ * Get the bound address info.
227
+ */
228
+ address() {
229
+ return { ...this._address };
230
+ }
231
+ /**
232
+ * Set the broadcast flag.
233
+ */
234
+ setBroadcast(flag) {
235
+ if (this._socket) {
236
+ this._socket.set_broadcast(flag);
237
+ }
238
+ }
239
+ /**
240
+ * Set the TTL.
241
+ */
242
+ setTTL(ttl) {
243
+ if (this._socket) {
244
+ this._socket.set_ttl(ttl);
245
+ }
246
+ return ttl;
247
+ }
248
+ /**
249
+ * Set multicast TTL.
250
+ */
251
+ setMulticastTTL(ttl) {
252
+ if (this._socket) {
253
+ this._socket.set_multicast_ttl(ttl);
254
+ }
255
+ return ttl;
256
+ }
257
+ /**
258
+ * Set multicast loopback.
259
+ */
260
+ setMulticastLoopback(flag) {
261
+ if (this._socket) {
262
+ this._socket.set_multicast_loopback(flag);
263
+ }
264
+ return flag;
265
+ }
266
+ /**
267
+ * Add multicast group membership.
268
+ */
269
+ addMembership(multicastAddress, multicastInterface) {
270
+ if (!this._socket) return;
271
+ try {
272
+ const mcastAddr = Gio.InetAddress.new_from_string(multicastAddress);
273
+ this._socket.join_multicast_group(mcastAddr, false, multicastInterface || null);
274
+ } catch (err) {
275
+ this.emit("error", err);
276
+ }
277
+ }
278
+ /**
279
+ * Drop multicast group membership.
280
+ */
281
+ dropMembership(multicastAddress, multicastInterface) {
282
+ if (!this._socket) return;
283
+ try {
284
+ const mcastAddr = Gio.InetAddress.new_from_string(multicastAddress);
285
+ this._socket.leave_multicast_group(mcastAddr, false, multicastInterface || null);
286
+ } catch (err) {
287
+ this.emit("error", err);
288
+ }
289
+ }
290
+ /**
291
+ * Set multicast interface.
292
+ */
293
+ setMulticastInterface(_interfaceAddress) {
294
+ }
295
+ /** Ref the socket (keep event loop alive). */
296
+ ref() {
297
+ return this;
298
+ }
299
+ /** Unref the socket (allow event loop to exit). */
300
+ unref() {
301
+ return this;
302
+ }
303
+ /** Get/Set receive buffer size. */
304
+ getRecvBufferSize() {
305
+ return 65536;
306
+ }
307
+ setRecvBufferSize(_size) {
308
+ }
309
+ /** Get/Set send buffer size. */
310
+ getSendBufferSize() {
311
+ return 65536;
312
+ }
313
+ setSendBufferSize(_size) {
314
+ }
315
+ /**
316
+ * Start receiving messages in background.
317
+ */
318
+ _startReceiving() {
319
+ if (this._receiving || this._closed || !this._socket) return;
320
+ this._receiving = true;
321
+ this._receiveLoop();
322
+ }
323
+ _receiveLoop() {
324
+ if (this._closed || !this._socket) return;
325
+ try {
326
+ if (!this._socket.condition_check(GLib.IOCondition.IN)) {
327
+ setTimeout(() => this._receiveLoop(), 50);
328
+ return;
329
+ }
330
+ const buf = new Uint8Array(65536);
331
+ const result = this._socket.receive_from(buf, this._cancellable);
332
+ const bytesRead = Array.isArray(result) ? result[0] : result;
333
+ const srcAddr = Array.isArray(result) ? result[1] : null;
334
+ if (bytesRead > 0 && srcAddr) {
335
+ const data = Buffer.from(buf.subarray(0, bytesRead));
336
+ const inetSockAddr = srcAddr;
337
+ const rinfo = {
338
+ address: inetSockAddr.get_address().to_string(),
339
+ family: this.type === "udp6" ? "IPv6" : "IPv4",
340
+ port: inetSockAddr.get_port()
341
+ };
342
+ this.emit("message", data, rinfo);
343
+ }
344
+ if (!this._closed) {
345
+ setTimeout(() => this._receiveLoop(), 0);
346
+ }
347
+ } catch (err) {
348
+ if (!this._closed) {
349
+ const errObj = err;
350
+ if (errObj.code !== Gio.IOErrorEnum.CANCELLED) {
351
+ this.emit("error", err);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ function createSocket(type, callback) {
358
+ const opts = typeof type === "string" ? { type } : type;
359
+ const socket = new Socket(opts);
360
+ if (callback) {
361
+ socket.on("message", callback);
362
+ }
363
+ return socket;
364
+ }
365
+ var index_default = { Socket, createSocket };
366
+ export {
367
+ Socket,
368
+ createSocket,
369
+ index_default as default
370
+ };
@@ -0,0 +1,125 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Buffer } from 'node:buffer';
3
+ export interface SocketOptions {
4
+ type: 'udp4' | 'udp6';
5
+ reuseAddr?: boolean;
6
+ reusePort?: boolean;
7
+ ipv6Only?: boolean;
8
+ recvBufferSize?: number;
9
+ sendBufferSize?: number;
10
+ signal?: AbortSignal;
11
+ }
12
+ export interface AddressInfo {
13
+ address: string;
14
+ family: string;
15
+ port: number;
16
+ }
17
+ /**
18
+ * dgram.Socket — UDP socket wrapping Gio.Socket.
19
+ */
20
+ interface RemoteAddressInfo {
21
+ address: string;
22
+ family: string;
23
+ port: number;
24
+ }
25
+ export declare class Socket extends EventEmitter {
26
+ readonly type: 'udp4' | 'udp6';
27
+ private _socket;
28
+ private _bound;
29
+ private _closed;
30
+ private _receiving;
31
+ private _address;
32
+ private _cancellable;
33
+ private _reuseAddr;
34
+ private _connected;
35
+ private _remoteAddress;
36
+ constructor(options: SocketOptions | string);
37
+ /**
38
+ * Bind the socket to a port and optional address.
39
+ */
40
+ bind(port?: number | {
41
+ port?: number;
42
+ address?: string;
43
+ exclusive?: boolean;
44
+ }, address?: string | (() => void), callback?: () => void): this;
45
+ /**
46
+ * Send a message.
47
+ */
48
+ send(msg: Buffer | string | Uint8Array | (Buffer | string | Uint8Array)[], offset?: number | ((err: Error | null, bytes: number) => void), length?: number, port?: number, address?: string | ((err: Error | null, bytes: number) => void), callback?: (err: Error | null, bytes: number) => void): void;
49
+ private _toBuffer;
50
+ private _toBufferSlice;
51
+ /**
52
+ * Close the socket.
53
+ */
54
+ close(callback?: () => void): this;
55
+ /**
56
+ * Associate the socket with a remote address/port (connected UDP).
57
+ * After connect(), send() can omit address and port.
58
+ */
59
+ connect(port: number, address?: string | (() => void), callback?: () => void): void;
60
+ /**
61
+ * Dissociate a connected socket from its remote address.
62
+ */
63
+ disconnect(): void;
64
+ /**
65
+ * Returns the remote address of a connected socket.
66
+ * Throws ERR_SOCKET_DGRAM_NOT_CONNECTED if not connected.
67
+ */
68
+ remoteAddress(): RemoteAddressInfo;
69
+ /**
70
+ * Get the bound address info.
71
+ */
72
+ address(): AddressInfo;
73
+ /**
74
+ * Set the broadcast flag.
75
+ */
76
+ setBroadcast(flag: boolean): void;
77
+ /**
78
+ * Set the TTL.
79
+ */
80
+ setTTL(ttl: number): number;
81
+ /**
82
+ * Set multicast TTL.
83
+ */
84
+ setMulticastTTL(ttl: number): number;
85
+ /**
86
+ * Set multicast loopback.
87
+ */
88
+ setMulticastLoopback(flag: boolean): boolean;
89
+ /**
90
+ * Add multicast group membership.
91
+ */
92
+ addMembership(multicastAddress: string, multicastInterface?: string): void;
93
+ /**
94
+ * Drop multicast group membership.
95
+ */
96
+ dropMembership(multicastAddress: string, multicastInterface?: string): void;
97
+ /**
98
+ * Set multicast interface.
99
+ */
100
+ setMulticastInterface(_interfaceAddress: string): void;
101
+ /** Ref the socket (keep event loop alive). */
102
+ ref(): this;
103
+ /** Unref the socket (allow event loop to exit). */
104
+ unref(): this;
105
+ /** Get/Set receive buffer size. */
106
+ getRecvBufferSize(): number;
107
+ setRecvBufferSize(_size: number): void;
108
+ /** Get/Set send buffer size. */
109
+ getSendBufferSize(): number;
110
+ setSendBufferSize(_size: number): void;
111
+ /**
112
+ * Start receiving messages in background.
113
+ */
114
+ private _startReceiving;
115
+ private _receiveLoop;
116
+ }
117
+ /**
118
+ * Create a UDP socket.
119
+ */
120
+ export declare function createSocket(type: 'udp4' | 'udp6' | SocketOptions, callback?: (msg: Buffer, rinfo: AddressInfo) => void): Socket;
121
+ declare const _default: {
122
+ Socket: typeof Socket;
123
+ createSocket: typeof createSocket;
124
+ };
125
+ export default _default;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@gjsify/dgram",
3
+ "version": "0.1.0",
4
+ "description": "Node.js dgram module for Gjs",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
16
+ "check": "tsc --noEmit",
17
+ "build": "yarn build:gjsify && yarn build:types",
18
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
19
+ "build:types": "tsc",
20
+ "build:test": "yarn build:test:gjs && yarn build:test:node",
21
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
22
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
23
+ "test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
24
+ "test:gjs": "gjs -m test.gjs.mjs",
25
+ "test:node": "node test.node.mjs"
26
+ },
27
+ "keywords": [
28
+ "gjs",
29
+ "node",
30
+ "dgram"
31
+ ],
32
+ "dependencies": {
33
+ "@girs/gio-2.0": "^2.88.0-4.0.0-beta.42",
34
+ "@girs/glib-2.0": "^2.88.0-4.0.0-beta.42",
35
+ "@gjsify/buffer": "^0.1.0",
36
+ "@gjsify/events": "^0.1.0",
37
+ "@gjsify/utils": "^0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@gjsify/cli": "^0.1.0",
41
+ "@gjsify/unit": "^0.1.0",
42
+ "@types/node": "^25.5.0",
43
+ "typescript": "^6.0.2"
44
+ }
45
+ }