@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 +14 -0
- package/address.d.ts.map +1 -0
- package/address.js +34 -0
- package/address.js.map +1 -0
- package/address.ts +44 -0
- package/index.d.ts +4 -0
- package/index.d.ts.map +1 -0
- package/index.js +4 -0
- package/index.js.map +1 -0
- package/index.ts +3 -0
- package/package.json +27 -0
- package/socket.d.ts +29 -0
- package/socket.d.ts.map +1 -0
- package/socket.js +129 -0
- package/socket.js.map +1 -0
- package/socket.ts +147 -0
- package/tsconfig.json +20 -0
- package/types.d.ts +23 -0
- package/types.d.ts.map +1 -0
- package/types.js +2 -0
- package/types.js.map +1 -0
- package/types.ts +25 -0
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
|
package/address.d.ts.map
ADDED
|
@@ -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
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
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
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
|
package/socket.d.ts.map
ADDED
|
@@ -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
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
|
+
}
|