@bobfrankston/rmfudp 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/address.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface NetworkInfo {
2
+ name: string;
3
+ address: string;
4
+ netmask: string;
5
+ broadcast: string;
6
+ mac: string;
7
+ }
8
+ /** Get all IPv4 network interfaces with broadcast addresses */
9
+ export declare function getNetworkInterfaces(): NetworkInfo[];
10
+ /** Compute broadcast address from IP and netmask */
11
+ export declare function computeBroadcast(address: string, netmask: string): string;
12
+ /** Get all broadcast addresses for discovery */
13
+ export declare function getBroadcastAddresses(): string[];
14
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["address.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,wBAAgB,oBAAoB,IAAI,WAAW,EAAE,CAmBpD;AAED,oDAAoD;AACpD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKzE;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD"}
package/address.js ADDED
@@ -0,0 +1,34 @@
1
+ import * as os from 'node:os';
2
+ /** Get all IPv4 network interfaces with broadcast addresses */
3
+ export function getNetworkInterfaces() {
4
+ const result = [];
5
+ const netifs = os.networkInterfaces();
6
+ for (const name in netifs) {
7
+ for (const info of netifs[name]) {
8
+ if (info.family !== 'IPv4' || info.internal)
9
+ continue;
10
+ if (info.address.startsWith('169.254.'))
11
+ continue; /** link-local */
12
+ result.push({
13
+ name,
14
+ address: info.address,
15
+ netmask: info.netmask,
16
+ broadcast: computeBroadcast(info.address, info.netmask),
17
+ mac: info.mac
18
+ });
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ /** Compute broadcast address from IP and netmask */
24
+ export function computeBroadcast(address, netmask) {
25
+ const addrParts = address.split('.').map(n => parseInt(n, 10));
26
+ const maskParts = netmask.split('.').map(n => parseInt(n, 10));
27
+ const broadcastParts = addrParts.map((a, i) => (~maskParts[i] & 0xff) | a);
28
+ return broadcastParts.join('.');
29
+ }
30
+ /** Get all broadcast addresses for discovery */
31
+ export function getBroadcastAddresses() {
32
+ return getNetworkInterfaces().map(ni => ni.broadcast);
33
+ }
34
+ //# sourceMappingURL=address.js.map
package/address.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["address.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAU9B,+DAA+D;AAC/D,MAAM,UAAU,oBAAoB;IAChC,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC;IAEtC,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QACxB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ;gBAAE,SAAS;YACtD,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC;gBAAE,SAAS,CAAE,iBAAiB;YAErE,MAAM,CAAC,IAAI,CAAC;gBACR,IAAI;gBACJ,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC;gBACvD,GAAG,EAAE,IAAI,CAAC,GAAG;aAChB,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAAe;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/D,MAAM,cAAc,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3E,OAAO,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,qBAAqB;IACjC,OAAO,oBAAoB,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;AAC1D,CAAC"}
package/address.ts ADDED
@@ -0,0 +1,44 @@
1
+ import * as os from 'node:os';
2
+
3
+ export interface NetworkInfo {
4
+ name: string;
5
+ address: string;
6
+ netmask: string;
7
+ broadcast: string;
8
+ mac: string;
9
+ }
10
+
11
+ /** Get all IPv4 network interfaces with broadcast addresses */
12
+ export function getNetworkInterfaces(): NetworkInfo[] {
13
+ const result: NetworkInfo[] = [];
14
+ const netifs = os.networkInterfaces();
15
+
16
+ for (const name in netifs) {
17
+ for (const info of netifs[name]) {
18
+ if (info.family !== 'IPv4' || info.internal) continue;
19
+ if (info.address.startsWith('169.254.')) continue; /** link-local */
20
+
21
+ result.push({
22
+ name,
23
+ address: info.address,
24
+ netmask: info.netmask,
25
+ broadcast: computeBroadcast(info.address, info.netmask),
26
+ mac: info.mac
27
+ });
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+
33
+ /** Compute broadcast address from IP and netmask */
34
+ export function computeBroadcast(address: string, netmask: string): string {
35
+ const addrParts = address.split('.').map(n => parseInt(n, 10));
36
+ const maskParts = netmask.split('.').map(n => parseInt(n, 10));
37
+ const broadcastParts = addrParts.map((a, i) => (~maskParts[i] & 0xff) | a);
38
+ return broadcastParts.join('.');
39
+ }
40
+
41
+ /** Get all broadcast addresses for discovery */
42
+ export function getBroadcastAddresses(): string[] {
43
+ return getNetworkInterfaces().map(ni => ni.broadcast);
44
+ }
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export { UdpSocket } from './socket.js';
3
+ export { getNetworkInterfaces, getBroadcastAddresses, computeBroadcast, NetworkInfo } from './address.js';
4
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export { UdpSocket } from './socket.js';
3
+ export { getNetworkInterfaces, getBroadcastAddresses, computeBroadcast } from './address.js';
4
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,gBAAgB,EAAe,MAAM,cAAc,CAAC"}
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export { UdpSocket } from './socket.js';
3
+ export { getNetworkInterfaces, getBroadcastAddresses, computeBroadcast, NetworkInfo } from './address.js';
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@bobfrankston/rmfudp",
3
+ "version": "0.1.0",
4
+ "description": "Generic UDP transport with port sharing, retry logic, and configurable timeouts",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "import": "./index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "watch": "tsc -w"
17
+ },
18
+ "keywords": ["udp", "socket", "retry", "broadcast"],
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/BobFrankston/rmfudp.git"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "latest"
26
+ }
27
+ }
package/socket.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { UdpSocketOptions, UdpRetryOptions, RemoteInfo } from './types.js';
3
+ export declare class UdpSocket extends EventEmitter {
4
+ private socket;
5
+ private port;
6
+ private broadcastAddr;
7
+ private bound;
8
+ private retryCount;
9
+ private retryDelay;
10
+ private responseTimeout;
11
+ constructor(options?: UdpSocketOptions);
12
+ /** Bind to port and enable broadcast */
13
+ bind(): Promise<void>;
14
+ /** Close socket */
15
+ close(): void;
16
+ /** Get bound port */
17
+ getPort(): number;
18
+ /** Set retry options at runtime */
19
+ setRetryOptions(options: UdpRetryOptions): void;
20
+ /** Get current retry options */
21
+ getRetryOptions(): Required<UdpRetryOptions>;
22
+ /** Send data to specific address (fire-and-forget) */
23
+ send(ip: string, port: number, data: Buffer): void;
24
+ /** Broadcast data (fire-and-forget) */
25
+ broadcast(data: Buffer, port?: number): void;
26
+ /** Send with retry - resolves when ack received or retries exhausted */
27
+ sendWithRetry(ip: string, port: number, data: Buffer, isAck: (msg: Buffer, rinfo: RemoteInfo) => boolean): Promise<Buffer>;
28
+ }
29
+ //# sourceMappingURL=socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["socket.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ3E,qBAAa,SAAU,SAAQ,YAAY;IACvC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAkB;IAE/B,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAoC;gBAE/C,OAAO,GAAE,gBAAqB;IAwB1C,wCAAwC;IACxC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBrB,mBAAmB;IACnB,KAAK,IAAI,IAAI;IAMb,qBAAqB;IACrB,OAAO,IAAI,MAAM;IAIjB,mCAAmC;IACnC,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAM/C,gCAAgC;IAChC,eAAe,IAAI,QAAQ,CAAC,eAAe,CAAC;IAQ5C,sDAAsD;IACtD,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUlD,uCAAuC;IACvC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAI5C,wEAAwE;IACxE,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;CAoC7H"}
package/socket.js ADDED
@@ -0,0 +1,129 @@
1
+ import dgram from 'node:dgram';
2
+ import { EventEmitter } from 'node:events';
3
+ const DEFAULT_PORT = 0;
4
+ const DEFAULT_BROADCAST = '255.255.255.255';
5
+ const DEFAULT_RETRY_COUNT = 3;
6
+ const DEFAULT_RETRY_DELAY = 100;
7
+ const DEFAULT_RESPONSE_TIMEOUT = 1000;
8
+ export class UdpSocket extends EventEmitter {
9
+ socket;
10
+ port;
11
+ broadcastAddr;
12
+ bound = false;
13
+ retryCount = DEFAULT_RETRY_COUNT;
14
+ retryDelay = DEFAULT_RETRY_DELAY;
15
+ responseTimeout = DEFAULT_RESPONSE_TIMEOUT;
16
+ constructor(options = {}) {
17
+ super();
18
+ this.port = options.port ?? DEFAULT_PORT;
19
+ this.broadcastAddr = options.broadcastAddr ?? DEFAULT_BROADCAST;
20
+ this.socket = dgram.createSocket({
21
+ type: 'udp4',
22
+ reuseAddr: options.reuseAddr ?? true
23
+ });
24
+ this.socket.on('message', (msg, rinfo) => {
25
+ this.emit('message', msg, rinfo);
26
+ });
27
+ this.socket.on('error', (err) => {
28
+ this.emit('error', err);
29
+ });
30
+ this.socket.on('close', () => {
31
+ this.bound = false;
32
+ this.emit('close');
33
+ });
34
+ }
35
+ /** Bind to port and enable broadcast */
36
+ bind() {
37
+ return new Promise((resolve, reject) => {
38
+ if (this.bound) {
39
+ resolve();
40
+ return;
41
+ }
42
+ this.socket.once('error', reject);
43
+ this.socket.bind(this.port, () => {
44
+ this.socket.removeListener('error', reject);
45
+ this.socket.setBroadcast(true);
46
+ this.bound = true;
47
+ this.port = this.socket.address().port;
48
+ this.emit('bound');
49
+ resolve();
50
+ });
51
+ });
52
+ }
53
+ /** Close socket */
54
+ close() {
55
+ if (this.bound) {
56
+ this.socket.close();
57
+ }
58
+ }
59
+ /** Get bound port */
60
+ getPort() {
61
+ return this.port;
62
+ }
63
+ /** Set retry options at runtime */
64
+ setRetryOptions(options) {
65
+ if (options.retryCount !== undefined)
66
+ this.retryCount = options.retryCount;
67
+ if (options.retryDelay !== undefined)
68
+ this.retryDelay = options.retryDelay;
69
+ if (options.responseTimeout !== undefined)
70
+ this.responseTimeout = options.responseTimeout;
71
+ }
72
+ /** Get current retry options */
73
+ getRetryOptions() {
74
+ return {
75
+ retryCount: this.retryCount,
76
+ retryDelay: this.retryDelay,
77
+ responseTimeout: this.responseTimeout
78
+ };
79
+ }
80
+ /** Send data to specific address (fire-and-forget) */
81
+ send(ip, port, data) {
82
+ if (!this.bound) {
83
+ this.emit('error', new Error('Socket not bound'));
84
+ return;
85
+ }
86
+ this.socket.send(data, 0, data.length, port, ip, (err) => {
87
+ if (err)
88
+ this.emit('error', err);
89
+ });
90
+ }
91
+ /** Broadcast data (fire-and-forget) */
92
+ broadcast(data, port) {
93
+ this.send(this.broadcastAddr, port ?? this.port, data);
94
+ }
95
+ /** Send with retry - resolves when ack received or retries exhausted */
96
+ sendWithRetry(ip, port, data, isAck) {
97
+ return new Promise((resolve, reject) => {
98
+ let attempts = 0;
99
+ let timeoutHandle;
100
+ let resolved = false;
101
+ const onMessage = (msg, rinfo) => {
102
+ if (rinfo.address === ip && isAck(msg, rinfo)) {
103
+ resolved = true;
104
+ clearTimeout(timeoutHandle);
105
+ this.removeListener('message', onMessage);
106
+ resolve(msg);
107
+ }
108
+ };
109
+ const attempt = () => {
110
+ if (resolved)
111
+ return;
112
+ attempts++;
113
+ if (attempts > this.retryCount) {
114
+ this.removeListener('message', onMessage);
115
+ reject(new Error(`No response after ${this.retryCount} retries`));
116
+ return;
117
+ }
118
+ this.send(ip, port, data);
119
+ timeoutHandle = setTimeout(() => {
120
+ if (!resolved)
121
+ attempt();
122
+ }, attempts === 1 ? this.responseTimeout : this.retryDelay);
123
+ };
124
+ this.on('message', onMessage);
125
+ attempt();
126
+ });
127
+ }
128
+ }
129
+ //# sourceMappingURL=socket.js.map
package/socket.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["socket.ts"],"names":[],"mappings":"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"}
package/socket.ts ADDED
@@ -0,0 +1,147 @@
1
+ import dgram from 'node:dgram';
2
+ import { EventEmitter } from 'node:events';
3
+ import { UdpSocketOptions, UdpRetryOptions, RemoteInfo } from './types.js';
4
+
5
+ const DEFAULT_PORT = 0;
6
+ const DEFAULT_BROADCAST = '255.255.255.255';
7
+ const DEFAULT_RETRY_COUNT = 3;
8
+ const DEFAULT_RETRY_DELAY = 100;
9
+ const DEFAULT_RESPONSE_TIMEOUT = 1000;
10
+
11
+ export class UdpSocket extends EventEmitter {
12
+ private socket: dgram.Socket;
13
+ private port: number;
14
+ private broadcastAddr: string;
15
+ private bound: boolean = false;
16
+
17
+ private retryCount: number = DEFAULT_RETRY_COUNT;
18
+ private retryDelay: number = DEFAULT_RETRY_DELAY;
19
+ private responseTimeout: number = DEFAULT_RESPONSE_TIMEOUT;
20
+
21
+ constructor(options: UdpSocketOptions = {}) {
22
+ super();
23
+ this.port = options.port ?? DEFAULT_PORT;
24
+ this.broadcastAddr = options.broadcastAddr ?? DEFAULT_BROADCAST;
25
+
26
+ this.socket = dgram.createSocket({
27
+ type: 'udp4',
28
+ reuseAddr: options.reuseAddr ?? true
29
+ });
30
+
31
+ this.socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo) => {
32
+ this.emit('message', msg, rinfo as RemoteInfo);
33
+ });
34
+
35
+ this.socket.on('error', (err: Error) => {
36
+ this.emit('error', err);
37
+ });
38
+
39
+ this.socket.on('close', () => {
40
+ this.bound = false;
41
+ this.emit('close');
42
+ });
43
+ }
44
+
45
+ /** Bind to port and enable broadcast */
46
+ bind(): Promise<void> {
47
+ return new Promise((resolve, reject) => {
48
+ if (this.bound) {
49
+ resolve();
50
+ return;
51
+ }
52
+
53
+ this.socket.once('error', reject);
54
+
55
+ this.socket.bind(this.port, () => {
56
+ this.socket.removeListener('error', reject);
57
+ this.socket.setBroadcast(true);
58
+ this.bound = true;
59
+ this.port = this.socket.address().port;
60
+ this.emit('bound');
61
+ resolve();
62
+ });
63
+ });
64
+ }
65
+
66
+ /** Close socket */
67
+ close(): void {
68
+ if (this.bound) {
69
+ this.socket.close();
70
+ }
71
+ }
72
+
73
+ /** Get bound port */
74
+ getPort(): number {
75
+ return this.port;
76
+ }
77
+
78
+ /** Set retry options at runtime */
79
+ setRetryOptions(options: UdpRetryOptions): void {
80
+ if (options.retryCount !== undefined) this.retryCount = options.retryCount;
81
+ if (options.retryDelay !== undefined) this.retryDelay = options.retryDelay;
82
+ if (options.responseTimeout !== undefined) this.responseTimeout = options.responseTimeout;
83
+ }
84
+
85
+ /** Get current retry options */
86
+ getRetryOptions(): Required<UdpRetryOptions> {
87
+ return {
88
+ retryCount: this.retryCount,
89
+ retryDelay: this.retryDelay,
90
+ responseTimeout: this.responseTimeout
91
+ };
92
+ }
93
+
94
+ /** Send data to specific address (fire-and-forget) */
95
+ send(ip: string, port: number, data: Buffer): void {
96
+ if (!this.bound) {
97
+ this.emit('error', new Error('Socket not bound'));
98
+ return;
99
+ }
100
+ this.socket.send(data, 0, data.length, port, ip, (err) => {
101
+ if (err) this.emit('error', err);
102
+ });
103
+ }
104
+
105
+ /** Broadcast data (fire-and-forget) */
106
+ broadcast(data: Buffer, port?: number): void {
107
+ this.send(this.broadcastAddr, port ?? this.port, data);
108
+ }
109
+
110
+ /** Send with retry - resolves when ack received or retries exhausted */
111
+ sendWithRetry(ip: string, port: number, data: Buffer, isAck: (msg: Buffer, rinfo: RemoteInfo) => boolean): Promise<Buffer> {
112
+ return new Promise((resolve, reject) => {
113
+ let attempts = 0;
114
+ let timeoutHandle: NodeJS.Timeout;
115
+ let resolved = false;
116
+
117
+ const onMessage = (msg: Buffer, rinfo: RemoteInfo) => {
118
+ if (rinfo.address === ip && isAck(msg, rinfo)) {
119
+ resolved = true;
120
+ clearTimeout(timeoutHandle);
121
+ this.removeListener('message', onMessage);
122
+ resolve(msg);
123
+ }
124
+ };
125
+
126
+ const attempt = () => {
127
+ if (resolved) return;
128
+
129
+ attempts++;
130
+ if (attempts > this.retryCount) {
131
+ this.removeListener('message', onMessage);
132
+ reject(new Error(`No response after ${this.retryCount} retries`));
133
+ return;
134
+ }
135
+
136
+ this.send(ip, port, data);
137
+
138
+ timeoutHandle = setTimeout(() => {
139
+ if (!resolved) attempt();
140
+ }, attempts === 1 ? this.responseTimeout : this.retryDelay);
141
+ };
142
+
143
+ this.on('message', onMessage);
144
+ attempt();
145
+ });
146
+ }
147
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "strictNullChecks": false,
15
+ "noImplicitAny": true,
16
+ "noImplicitThis": true,
17
+ "newLine": "lf"
18
+ },
19
+ "exclude": ["node_modules", "cruft", ".git", "tests", "prev"]
20
+ }
package/types.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface UdpSocketOptions {
2
+ port?: number; /** default 0 (ephemeral) */
3
+ reuseAddr?: boolean; /** default true for port sharing */
4
+ broadcastAddr?: string; /** default "255.255.255.255" */
5
+ }
6
+ export interface UdpRetryOptions {
7
+ retryCount?: number; /** default 3 */
8
+ retryDelay?: number; /** ms between retries, default 100 */
9
+ responseTimeout?: number; /** ms to wait for response, default 1000 */
10
+ }
11
+ export interface RemoteInfo {
12
+ address: string;
13
+ port: number;
14
+ family: 'IPv4' | 'IPv6';
15
+ size: number;
16
+ }
17
+ export interface UdpSocketEvents {
18
+ message: (data: Buffer, rinfo: RemoteInfo) => void;
19
+ error: (err: Error) => void;
20
+ bound: () => void;
21
+ close: () => void;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map
package/types.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC,CAAE,4BAA4B;IAC5C,SAAS,CAAC,EAAE,OAAO,CAAC,CAAE,oCAAoC;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAC,CAAE,gCAAgC;CAC5D;AAED,MAAM,WAAW,eAAe;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC,CAAE,gBAAgB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC,CAAE,sCAAsC;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC,CAAE,4CAA4C;CAC1E;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACnD,KAAK,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CACrB"}
package/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/types.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
package/types.ts ADDED
@@ -0,0 +1,25 @@
1
+ export interface UdpSocketOptions {
2
+ port?: number; /** default 0 (ephemeral) */
3
+ reuseAddr?: boolean; /** default true for port sharing */
4
+ broadcastAddr?: string; /** default "255.255.255.255" */
5
+ }
6
+
7
+ export interface UdpRetryOptions {
8
+ retryCount?: number; /** default 3 */
9
+ retryDelay?: number; /** ms between retries, default 100 */
10
+ responseTimeout?: number; /** ms to wait for response, default 1000 */
11
+ }
12
+
13
+ export interface RemoteInfo {
14
+ address: string;
15
+ port: number;
16
+ family: 'IPv4' | 'IPv6';
17
+ size: number;
18
+ }
19
+
20
+ export interface UdpSocketEvents {
21
+ message: (data: Buffer, rinfo: RemoteInfo) => void;
22
+ error: (err: Error) => void;
23
+ bound: () => void;
24
+ close: () => void;
25
+ }