@ecobridge.xyz/devicemanager 3.0.2

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.
Files changed (137) hide show
  1. package/.gitea/workflows/default_nottags.yaml +66 -0
  2. package/.gitea/workflows/default_tags.yaml +124 -0
  3. package/.vscode/launch.json +11 -0
  4. package/.vscode/settings.json +26 -0
  5. package/changelog.md +91 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/device/device.classes.device.d.ts +205 -0
  9. package/dist_ts/device/device.classes.device.js +344 -0
  10. package/dist_ts/devicemanager.classes.devicemanager.d.ts +210 -0
  11. package/dist_ts/devicemanager.classes.devicemanager.js +835 -0
  12. package/dist_ts/discovery/discovery.classes.homeassistant.d.ts +101 -0
  13. package/dist_ts/discovery/discovery.classes.homeassistant.js +313 -0
  14. package/dist_ts/discovery/discovery.classes.mdns.d.ts +90 -0
  15. package/dist_ts/discovery/discovery.classes.mdns.js +291 -0
  16. package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +64 -0
  17. package/dist_ts/discovery/discovery.classes.networkscanner.js +535 -0
  18. package/dist_ts/discovery/discovery.classes.ssdp.d.ts +112 -0
  19. package/dist_ts/discovery/discovery.classes.ssdp.js +255 -0
  20. package/dist_ts/factories/index.d.ts +190 -0
  21. package/dist_ts/factories/index.js +350 -0
  22. package/dist_ts/features/feature.abstract.d.ts +118 -0
  23. package/dist_ts/features/feature.abstract.js +172 -0
  24. package/dist_ts/features/feature.camera.d.ts +95 -0
  25. package/dist_ts/features/feature.camera.js +162 -0
  26. package/dist_ts/features/feature.climate.d.ts +174 -0
  27. package/dist_ts/features/feature.climate.js +328 -0
  28. package/dist_ts/features/feature.cover.d.ts +121 -0
  29. package/dist_ts/features/feature.cover.js +217 -0
  30. package/dist_ts/features/feature.fan.d.ts +127 -0
  31. package/dist_ts/features/feature.fan.js +235 -0
  32. package/dist_ts/features/feature.light.d.ts +156 -0
  33. package/dist_ts/features/feature.light.js +296 -0
  34. package/dist_ts/features/feature.lock.d.ts +103 -0
  35. package/dist_ts/features/feature.lock.js +170 -0
  36. package/dist_ts/features/feature.playback.d.ts +98 -0
  37. package/dist_ts/features/feature.playback.js +191 -0
  38. package/dist_ts/features/feature.power.d.ts +79 -0
  39. package/dist_ts/features/feature.power.js +173 -0
  40. package/dist_ts/features/feature.print.d.ts +78 -0
  41. package/dist_ts/features/feature.print.js +226 -0
  42. package/dist_ts/features/feature.scan.d.ts +79 -0
  43. package/dist_ts/features/feature.scan.js +315 -0
  44. package/dist_ts/features/feature.sensor.d.ts +95 -0
  45. package/dist_ts/features/feature.sensor.js +147 -0
  46. package/dist_ts/features/feature.snmp.d.ts +88 -0
  47. package/dist_ts/features/feature.snmp.js +175 -0
  48. package/dist_ts/features/feature.switch.d.ts +75 -0
  49. package/dist_ts/features/feature.switch.js +126 -0
  50. package/dist_ts/features/feature.volume.d.ts +104 -0
  51. package/dist_ts/features/feature.volume.js +186 -0
  52. package/dist_ts/features/index.d.ts +19 -0
  53. package/dist_ts/features/index.js +23 -0
  54. package/dist_ts/helpers/helpers.iprange.d.ts +35 -0
  55. package/dist_ts/helpers/helpers.iprange.js +151 -0
  56. package/dist_ts/helpers/helpers.retry.d.ts +25 -0
  57. package/dist_ts/helpers/helpers.retry.js +69 -0
  58. package/dist_ts/index.d.ts +15 -0
  59. package/dist_ts/index.js +40 -0
  60. package/dist_ts/interfaces/feature.interfaces.d.ts +255 -0
  61. package/dist_ts/interfaces/feature.interfaces.js +6 -0
  62. package/dist_ts/interfaces/homeassistant.interfaces.d.ts +550 -0
  63. package/dist_ts/interfaces/homeassistant.interfaces.js +111 -0
  64. package/dist_ts/interfaces/index.d.ts +330 -0
  65. package/dist_ts/interfaces/index.js +16 -0
  66. package/dist_ts/interfaces/smarthome.interfaces.d.ts +282 -0
  67. package/dist_ts/interfaces/smarthome.interfaces.js +7 -0
  68. package/dist_ts/paths.d.ts +1 -0
  69. package/dist_ts/paths.js +3 -0
  70. package/dist_ts/plugins.d.ts +23 -0
  71. package/dist_ts/plugins.js +30 -0
  72. package/dist_ts/protocols/index.d.ts +12 -0
  73. package/dist_ts/protocols/index.js +21 -0
  74. package/dist_ts/protocols/protocol.escl.d.ts +58 -0
  75. package/dist_ts/protocols/protocol.escl.js +364 -0
  76. package/dist_ts/protocols/protocol.homeassistant.d.ts +248 -0
  77. package/dist_ts/protocols/protocol.homeassistant.js +611 -0
  78. package/dist_ts/protocols/protocol.ipp.d.ts +45 -0
  79. package/dist_ts/protocols/protocol.ipp.js +284 -0
  80. package/dist_ts/protocols/protocol.nut.d.ts +160 -0
  81. package/dist_ts/protocols/protocol.nut.js +364 -0
  82. package/dist_ts/protocols/protocol.sane.d.ts +104 -0
  83. package/dist_ts/protocols/protocol.sane.js +597 -0
  84. package/dist_ts/protocols/protocol.snmp.d.ts +133 -0
  85. package/dist_ts/protocols/protocol.snmp.js +333 -0
  86. package/dist_ts/protocols/protocol.upnp.d.ts +207 -0
  87. package/dist_ts/protocols/protocol.upnp.js +442 -0
  88. package/dist_ts/protocols/protocol.upssnmp.d.ts +160 -0
  89. package/dist_ts/protocols/protocol.upssnmp.js +263 -0
  90. package/npmextra.json +24 -0
  91. package/package.json +39 -0
  92. package/readme.hints.md +115 -0
  93. package/readme.md +624 -0
  94. package/test/test.ts +261 -0
  95. package/ts/00_commitinfo_data.ts +8 -0
  96. package/ts/device/device.classes.device.ts +454 -0
  97. package/ts/devicemanager.classes.devicemanager.ts +1219 -0
  98. package/ts/discovery/discovery.classes.homeassistant.ts +376 -0
  99. package/ts/discovery/discovery.classes.mdns.ts +347 -0
  100. package/ts/discovery/discovery.classes.networkscanner.ts +648 -0
  101. package/ts/discovery/discovery.classes.ssdp.ts +357 -0
  102. package/ts/factories/index.ts +696 -0
  103. package/ts/features/feature.abstract.ts +251 -0
  104. package/ts/features/feature.camera.ts +214 -0
  105. package/ts/features/feature.climate.ts +407 -0
  106. package/ts/features/feature.cover.ts +278 -0
  107. package/ts/features/feature.fan.ts +296 -0
  108. package/ts/features/feature.light.ts +369 -0
  109. package/ts/features/feature.lock.ts +223 -0
  110. package/ts/features/feature.playback.ts +246 -0
  111. package/ts/features/feature.power.ts +231 -0
  112. package/ts/features/feature.print.ts +297 -0
  113. package/ts/features/feature.scan.ts +370 -0
  114. package/ts/features/feature.sensor.ts +202 -0
  115. package/ts/features/feature.snmp.ts +235 -0
  116. package/ts/features/feature.switch.ts +170 -0
  117. package/ts/features/feature.volume.ts +256 -0
  118. package/ts/features/index.ts +25 -0
  119. package/ts/helpers/helpers.iprange.ts +172 -0
  120. package/ts/helpers/helpers.retry.ts +100 -0
  121. package/ts/index.ts +165 -0
  122. package/ts/interfaces/feature.interfaces.ts +359 -0
  123. package/ts/interfaces/homeassistant.interfaces.ts +666 -0
  124. package/ts/interfaces/index.ts +421 -0
  125. package/ts/interfaces/smarthome.interfaces.ts +421 -0
  126. package/ts/paths.ts +5 -0
  127. package/ts/plugins.ts +41 -0
  128. package/ts/protocols/index.ts +59 -0
  129. package/ts/protocols/protocol.escl.ts +439 -0
  130. package/ts/protocols/protocol.homeassistant.ts +737 -0
  131. package/ts/protocols/protocol.ipp.ts +329 -0
  132. package/ts/protocols/protocol.nut.ts +471 -0
  133. package/ts/protocols/protocol.sane.ts +694 -0
  134. package/ts/protocols/protocol.snmp.ts +439 -0
  135. package/ts/protocols/protocol.upnp.ts +627 -0
  136. package/ts/protocols/protocol.upssnmp.ts +377 -0
  137. package/tsconfig.json +12 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Volume Feature
3
+ * Provides volume control capability
4
+ */
5
+
6
+ import { Feature, type TDeviceReference } from './feature.abstract.js';
7
+ import type {
8
+ IVolumeFeatureInfo,
9
+ IFeatureOptions,
10
+ } from '../interfaces/feature.interfaces.js';
11
+
12
+ /**
13
+ * Options for creating a VolumeFeature
14
+ */
15
+ export interface IVolumeFeatureOptions extends IFeatureOptions {
16
+ /** Volume control protocol (usually same as device protocol) */
17
+ volumeProtocol?: string;
18
+ /** Minimum volume level */
19
+ minVolume?: number;
20
+ /** Maximum volume level */
21
+ maxVolume?: number;
22
+ /** Volume step increment */
23
+ volumeStep?: number;
24
+ /** Whether mute is supported */
25
+ supportsMute?: boolean;
26
+ /** Protocol-specific volume controller */
27
+ volumeController?: IVolumeController;
28
+ }
29
+
30
+ /**
31
+ * Interface for protocol-specific volume control
32
+ */
33
+ export interface IVolumeController {
34
+ getVolume(): Promise<number>;
35
+ setVolume(level: number): Promise<void>;
36
+ getMute(): Promise<boolean>;
37
+ setMute(muted: boolean): Promise<void>;
38
+ }
39
+
40
+ /**
41
+ * Volume Feature - provides volume control capability
42
+ *
43
+ * Separated from PlaybackFeature because some devices have volume control
44
+ * without playback capability (e.g., amplifiers, HDMI matrix switches).
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const volume = device.getFeature<VolumeFeature>('volume');
49
+ * if (volume) {
50
+ * const current = await volume.getVolume();
51
+ * await volume.setVolume(current + 10);
52
+ * await volume.toggleMute();
53
+ * }
54
+ * ```
55
+ */
56
+ export class VolumeFeature extends Feature {
57
+ public readonly type = 'volume' as const;
58
+ public readonly protocol: string;
59
+
60
+ // Capabilities
61
+ public readonly minVolume: number;
62
+ public readonly maxVolume: number;
63
+ public readonly volumeStep: number;
64
+ public readonly supportsMute: boolean;
65
+
66
+ // Current state
67
+ protected _volume: number = 0;
68
+ protected _muted: boolean = false;
69
+
70
+ // Volume controller (protocol-specific)
71
+ protected volumeController: IVolumeController | null = null;
72
+
73
+ constructor(
74
+ device: TDeviceReference,
75
+ port: number,
76
+ options?: IVolumeFeatureOptions
77
+ ) {
78
+ super(device, port, options);
79
+ this.protocol = options?.volumeProtocol ?? 'generic';
80
+ this.minVolume = options?.minVolume ?? 0;
81
+ this.maxVolume = options?.maxVolume ?? 100;
82
+ this.volumeStep = options?.volumeStep ?? 5;
83
+ this.supportsMute = options?.supportsMute ?? true;
84
+
85
+ if (options?.volumeController) {
86
+ this.volumeController = options.volumeController;
87
+ }
88
+ }
89
+
90
+ // ============================================================================
91
+ // Properties
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Get current volume level (cached)
96
+ */
97
+ public get volume(): number {
98
+ return this._volume;
99
+ }
100
+
101
+ /**
102
+ * Get mute state (cached)
103
+ */
104
+ public get muted(): boolean {
105
+ return this._muted;
106
+ }
107
+
108
+ // ============================================================================
109
+ // Connection
110
+ // ============================================================================
111
+
112
+ protected async doConnect(): Promise<void> {
113
+ // Fetch initial state if we have a controller
114
+ if (this.volumeController) {
115
+ try {
116
+ this._volume = await this.volumeController.getVolume();
117
+ this._muted = await this.volumeController.getMute();
118
+ } catch {
119
+ // Ignore errors fetching initial state
120
+ }
121
+ }
122
+ }
123
+
124
+ protected async doDisconnect(): Promise<void> {
125
+ // Nothing to disconnect for volume control
126
+ }
127
+
128
+ // ============================================================================
129
+ // Volume Control
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Get current volume level
134
+ */
135
+ public async getVolume(): Promise<number> {
136
+ if (this.volumeController) {
137
+ this._volume = await this.volumeController.getVolume();
138
+ }
139
+ return this._volume;
140
+ }
141
+
142
+ /**
143
+ * Set volume level
144
+ * @param level Volume level (clamped to min/max)
145
+ */
146
+ public async setVolume(level: number): Promise<void> {
147
+ const clampedLevel = Math.max(this.minVolume, Math.min(this.maxVolume, level));
148
+
149
+ if (this.volumeController) {
150
+ await this.volumeController.setVolume(clampedLevel);
151
+ }
152
+
153
+ const oldVolume = this._volume;
154
+ this._volume = clampedLevel;
155
+
156
+ if (oldVolume !== clampedLevel) {
157
+ this.emit('volume:changed', { oldVolume, newVolume: clampedLevel });
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Increase volume by step
163
+ */
164
+ public async volumeUp(step?: number): Promise<number> {
165
+ const increment = step ?? this.volumeStep;
166
+ const newVolume = Math.min(this.maxVolume, this._volume + increment);
167
+ await this.setVolume(newVolume);
168
+ return this._volume;
169
+ }
170
+
171
+ /**
172
+ * Decrease volume by step
173
+ */
174
+ public async volumeDown(step?: number): Promise<number> {
175
+ const decrement = step ?? this.volumeStep;
176
+ const newVolume = Math.max(this.minVolume, this._volume - decrement);
177
+ await this.setVolume(newVolume);
178
+ return this._volume;
179
+ }
180
+
181
+ /**
182
+ * Get mute state
183
+ */
184
+ public async getMute(): Promise<boolean> {
185
+ if (this.volumeController && this.supportsMute) {
186
+ this._muted = await this.volumeController.getMute();
187
+ }
188
+ return this._muted;
189
+ }
190
+
191
+ /**
192
+ * Set mute state
193
+ */
194
+ public async setMute(muted: boolean): Promise<void> {
195
+ if (!this.supportsMute) {
196
+ throw new Error('Mute not supported');
197
+ }
198
+
199
+ if (this.volumeController) {
200
+ await this.volumeController.setMute(muted);
201
+ }
202
+
203
+ const oldMuted = this._muted;
204
+ this._muted = muted;
205
+
206
+ if (oldMuted !== muted) {
207
+ this.emit('mute:changed', { oldMuted, newMuted: muted });
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Toggle mute state
213
+ */
214
+ public async toggleMute(): Promise<boolean> {
215
+ const currentMuted = await this.getMute();
216
+ await this.setMute(!currentMuted);
217
+ return this._muted;
218
+ }
219
+
220
+ // ============================================================================
221
+ // Serialization
222
+ // ============================================================================
223
+
224
+ public getFeatureInfo(): IVolumeFeatureInfo {
225
+ return {
226
+ ...this.getBaseFeatureInfo(),
227
+ type: 'volume',
228
+ minVolume: this.minVolume,
229
+ maxVolume: this.maxVolume,
230
+ volumeStep: this.volumeStep,
231
+ supportsMute: this.supportsMute,
232
+ };
233
+ }
234
+
235
+ // ============================================================================
236
+ // Static Factory
237
+ // ============================================================================
238
+
239
+ /**
240
+ * Create from discovery metadata
241
+ */
242
+ public static fromDiscovery(
243
+ device: TDeviceReference,
244
+ port: number,
245
+ protocol: string,
246
+ metadata: Record<string, unknown>
247
+ ): VolumeFeature {
248
+ return new VolumeFeature(device, port, {
249
+ volumeProtocol: protocol,
250
+ minVolume: metadata.minVolume as number ?? 0,
251
+ maxVolume: metadata.maxVolume as number ?? 100,
252
+ volumeStep: metadata.volumeStep as number ?? 5,
253
+ supportsMute: metadata.supportsMute as boolean ?? true,
254
+ });
255
+ }
256
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Features Module
3
+ * Exports all feature classes and types
4
+ */
5
+
6
+ // Abstract base
7
+ export { Feature, type TDeviceReference } from './feature.abstract.js';
8
+
9
+ // Concrete features - Document/Infrastructure
10
+ export { ScanFeature, type IScanFeatureOptions } from './feature.scan.js';
11
+ export { PrintFeature, type IPrintFeatureOptions } from './feature.print.js';
12
+ export { PlaybackFeature, type IPlaybackFeatureOptions } from './feature.playback.js';
13
+ export { VolumeFeature, type IVolumeFeatureOptions, type IVolumeController } from './feature.volume.js';
14
+ export { PowerFeature, type IPowerFeatureOptions } from './feature.power.js';
15
+ export { SnmpFeature, type ISnmpFeatureOptions } from './feature.snmp.js';
16
+
17
+ // Smart Home Features (protocol-agnostic: home-assistant, hue, mqtt, etc.)
18
+ export { SwitchFeature, type ISwitchFeatureOptions } from './feature.switch.js';
19
+ export { SensorFeature, type ISensorFeatureOptions } from './feature.sensor.js';
20
+ export { LightFeature, type ILightFeatureOptions } from './feature.light.js';
21
+ export { CoverFeature, type ICoverFeatureOptions } from './feature.cover.js';
22
+ export { LockFeature, type ILockFeatureOptions } from './feature.lock.js';
23
+ export { FanFeature, type IFanFeatureOptions } from './feature.fan.js';
24
+ export { ClimateFeature, type IClimateFeatureOptions } from './feature.climate.js';
25
+ export { CameraFeature, type ICameraFeatureOptions } from './feature.camera.js';
@@ -0,0 +1,172 @@
1
+ /**
2
+ * IP Range utility functions for network scanning
3
+ */
4
+
5
+ import * as os from 'os';
6
+
7
+ /**
8
+ * Validates an IPv4 address
9
+ */
10
+ export function isValidIp(ip: string): boolean {
11
+ const parts = ip.split('.');
12
+ if (parts.length !== 4) return false;
13
+
14
+ return parts.every((part) => {
15
+ const num = parseInt(part, 10);
16
+ return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Converts an IPv4 address to a 32-bit number
22
+ */
23
+ export function ipToNumber(ip: string): number {
24
+ const parts = ip.split('.').map((p) => parseInt(p, 10));
25
+ return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
26
+ }
27
+
28
+ /**
29
+ * Converts a 32-bit number to an IPv4 address
30
+ */
31
+ export function numberToIp(num: number): string {
32
+ return [
33
+ (num >>> 24) & 255,
34
+ (num >>> 16) & 255,
35
+ (num >>> 8) & 255,
36
+ num & 255,
37
+ ].join('.');
38
+ }
39
+
40
+ /**
41
+ * Generates an array of IP addresses from a start to end range
42
+ * Excludes network address (.0) and broadcast address (.255) for /24 ranges
43
+ */
44
+ export function ipRangeToIps(startIp: string, endIp: string): string[] {
45
+ if (!isValidIp(startIp) || !isValidIp(endIp)) {
46
+ throw new Error(`Invalid IP address: ${!isValidIp(startIp) ? startIp : endIp}`);
47
+ }
48
+
49
+ const start = ipToNumber(startIp);
50
+ const end = ipToNumber(endIp);
51
+
52
+ if (start > end) {
53
+ throw new Error(`Start IP (${startIp}) must be less than or equal to end IP (${endIp})`);
54
+ }
55
+
56
+ const ips: string[] = [];
57
+ for (let i = start; i <= end; i++) {
58
+ ips.push(numberToIp(i));
59
+ }
60
+
61
+ return ips;
62
+ }
63
+
64
+ /**
65
+ * Parses CIDR notation and returns an array of usable host IPs
66
+ * Excludes network address and broadcast address
67
+ * Example: "192.168.1.0/24" returns 192.168.1.1 through 192.168.1.254
68
+ */
69
+ export function cidrToIps(cidr: string): string[] {
70
+ const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
71
+ if (!match) {
72
+ throw new Error(`Invalid CIDR notation: ${cidr}`);
73
+ }
74
+
75
+ const [, networkIp, prefixStr] = match;
76
+ const prefix = parseInt(prefixStr, 10);
77
+
78
+ if (!isValidIp(networkIp)) {
79
+ throw new Error(`Invalid network address: ${networkIp}`);
80
+ }
81
+
82
+ if (prefix < 0 || prefix > 32) {
83
+ throw new Error(`Invalid prefix length: ${prefix}`);
84
+ }
85
+
86
+ // For /32, just return the single IP
87
+ if (prefix === 32) {
88
+ return [networkIp];
89
+ }
90
+
91
+ // For /31 (point-to-point), return both IPs
92
+ if (prefix === 31) {
93
+ const networkNum = ipToNumber(networkIp);
94
+ const mask = (0xffffffff << (32 - prefix)) >>> 0;
95
+ const network = (networkNum & mask) >>> 0;
96
+ return [numberToIp(network), numberToIp(network + 1)];
97
+ }
98
+
99
+ // Calculate network and broadcast addresses
100
+ const networkNum = ipToNumber(networkIp);
101
+ const mask = (0xffffffff << (32 - prefix)) >>> 0;
102
+ const network = (networkNum & mask) >>> 0;
103
+ const broadcast = (network | (~mask >>> 0)) >>> 0;
104
+
105
+ // Generate usable host IPs (exclude network and broadcast)
106
+ const ips: string[] = [];
107
+ for (let i = network + 1; i < broadcast; i++) {
108
+ ips.push(numberToIp(i));
109
+ }
110
+
111
+ return ips;
112
+ }
113
+
114
+ /**
115
+ * Gets the local network interfaces and returns the first non-loopback IPv4 subnet
116
+ * Returns CIDR notation (e.g., "192.168.1.0/24")
117
+ */
118
+ export function getLocalSubnet(): string | null {
119
+ try {
120
+ const interfaces = os.networkInterfaces();
121
+
122
+ for (const name of Object.keys(interfaces)) {
123
+ const iface = interfaces[name];
124
+ if (!iface) continue;
125
+
126
+ for (const info of iface) {
127
+ // Skip loopback and non-IPv4
128
+ if (info.family !== 'IPv4' || info.internal) continue;
129
+
130
+ // Calculate the network address from IP and netmask
131
+ const ip = ipToNumber(info.address);
132
+ const mask = ipToNumber(info.netmask);
133
+ const network = (ip & mask) >>> 0;
134
+
135
+ // Calculate prefix length from netmask
136
+ const maskBits = info.netmask.split('.').reduce((acc: number, octet: string) => {
137
+ const byte = parseInt(octet, 10);
138
+ let bits = 0;
139
+ for (let i = 7; i >= 0; i--) {
140
+ if ((byte >> i) & 1) bits++;
141
+ else break;
142
+ }
143
+ return acc + bits;
144
+ }, 0);
145
+
146
+ return `${numberToIp(network)}/${maskBits}`;
147
+ }
148
+ }
149
+ } catch {
150
+ // Failed to get network interfaces
151
+ }
152
+
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * Counts the number of IPs in a CIDR range (excluding network and broadcast)
158
+ */
159
+ export function countIpsInCidr(cidr: string): number {
160
+ const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
161
+ if (!match) {
162
+ throw new Error(`Invalid CIDR notation: ${cidr}`);
163
+ }
164
+
165
+ const prefix = parseInt(match[2], 10);
166
+
167
+ if (prefix === 32) return 1;
168
+ if (prefix === 31) return 2;
169
+
170
+ // 2^(32-prefix) - 2 (minus network and broadcast)
171
+ return Math.pow(2, 32 - prefix) - 2;
172
+ }
@@ -0,0 +1,100 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IRetryOptions } from '../interfaces/index.js';
3
+
4
+ const defaultRetryOptions: Required<IRetryOptions> = {
5
+ maxRetries: 5,
6
+ baseDelay: 1000,
7
+ maxDelay: 16000,
8
+ multiplier: 2,
9
+ jitter: true,
10
+ };
11
+
12
+ /**
13
+ * Calculates the delay for a retry attempt using exponential backoff
14
+ */
15
+ function calculateDelay(
16
+ attempt: number,
17
+ options: Required<IRetryOptions>
18
+ ): number {
19
+ const delay = Math.min(
20
+ options.baseDelay * Math.pow(options.multiplier, attempt),
21
+ options.maxDelay
22
+ );
23
+
24
+ if (options.jitter) {
25
+ // Add random jitter of +/- 25%
26
+ const jitterRange = delay * 0.25;
27
+ return delay + (Math.random() * jitterRange * 2 - jitterRange);
28
+ }
29
+
30
+ return delay;
31
+ }
32
+
33
+ /**
34
+ * Executes a function with retry logic using exponential backoff
35
+ *
36
+ * @param fn - The async function to execute
37
+ * @param options - Retry configuration options
38
+ * @returns The result of the function
39
+ * @throws The last error if all retries fail
40
+ */
41
+ export async function withRetry<T>(
42
+ fn: () => Promise<T>,
43
+ options?: IRetryOptions
44
+ ): Promise<T> {
45
+ const opts: Required<IRetryOptions> = { ...defaultRetryOptions, ...options };
46
+ let lastError: Error | undefined;
47
+
48
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
49
+ try {
50
+ return await fn();
51
+ } catch (error) {
52
+ lastError = error instanceof Error ? error : new Error(String(error));
53
+
54
+ if (attempt === opts.maxRetries) {
55
+ break;
56
+ }
57
+
58
+ const delay = calculateDelay(attempt, opts);
59
+ await plugins.smartdelay.delayFor(delay);
60
+ }
61
+ }
62
+
63
+ throw lastError ?? new Error('Retry failed with unknown error');
64
+ }
65
+
66
+ /**
67
+ * Creates a retryable version of an async function
68
+ *
69
+ * @param fn - The async function to wrap
70
+ * @param options - Retry configuration options
71
+ * @returns A wrapped function that retries on failure
72
+ */
73
+ export function createRetryable<TArgs extends unknown[], TResult>(
74
+ fn: (...args: TArgs) => Promise<TResult>,
75
+ options?: IRetryOptions
76
+ ): (...args: TArgs) => Promise<TResult> {
77
+ return (...args: TArgs) => withRetry(() => fn(...args), options);
78
+ }
79
+
80
+ /**
81
+ * Retry decorator for class methods
82
+ * Note: Use as a wrapper function since TC39 decorators have different semantics
83
+ */
84
+ export function retryMethod<T extends Record<string, unknown>>(
85
+ target: T,
86
+ methodName: keyof T & string,
87
+ options?: IRetryOptions
88
+ ): void {
89
+ const originalMethod = target[methodName];
90
+ if (typeof originalMethod !== 'function') {
91
+ throw new Error(`${methodName} is not a function`);
92
+ }
93
+
94
+ target[methodName] = createRetryable(
95
+ originalMethod.bind(target) as (...args: unknown[]) => Promise<unknown>,
96
+ options
97
+ ) as T[typeof methodName];
98
+ }
99
+
100
+ export { defaultRetryOptions };