rosinterface 1.3.0 → 1.3.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 (78) hide show
  1. package/dist/cli/Generate.js +20 -1
  2. package/dist/cli/Generate.js.map +1 -1
  3. package/dist/cli/SchemaInferrer.d.ts +9 -0
  4. package/dist/cli/SchemaInferrer.js +22 -3
  5. package/dist/cli/SchemaInferrer.js.map +1 -1
  6. package/dist/client/CommandBuilder.d.ts +337 -2
  7. package/dist/client/CommandBuilder.js +483 -15
  8. package/dist/client/CommandBuilder.js.map +1 -1
  9. package/dist/client/MikrotikClient.d.ts +349 -1
  10. package/dist/client/MikrotikClient.js +364 -1
  11. package/dist/client/MikrotikClient.js.map +1 -1
  12. package/dist/client/MikrotikPool.d.ts +30 -0
  13. package/dist/client/MikrotikPool.js +31 -1
  14. package/dist/client/MikrotikPool.js.map +1 -1
  15. package/dist/client/MikrotikSwarm.d.ts +167 -0
  16. package/dist/client/MikrotikSwarm.js +178 -1
  17. package/dist/client/MikrotikSwarm.js.map +1 -1
  18. package/dist/client/MikrotikTransaction.d.ts +27 -0
  19. package/dist/client/MikrotikTransaction.js +28 -0
  20. package/dist/client/MikrotikTransaction.js.map +1 -1
  21. package/dist/client/ResultParser.d.ts +19 -0
  22. package/dist/client/ResultParser.js +31 -0
  23. package/dist/client/ResultParser.js.map +1 -1
  24. package/dist/client/SnapshotSubscription.d.ts +139 -0
  25. package/dist/client/SnapshotSubscription.js +169 -0
  26. package/dist/client/SnapshotSubscription.js.map +1 -1
  27. package/dist/core/Auth.d.ts +31 -0
  28. package/dist/core/Auth.js +46 -1
  29. package/dist/core/Auth.js.map +1 -1
  30. package/dist/core/CircuitBreaker.d.ts +26 -0
  31. package/dist/core/CircuitBreaker.js +26 -0
  32. package/dist/core/CircuitBreaker.js.map +1 -1
  33. package/dist/core/HttpConstants.d.ts +29 -16
  34. package/dist/core/HttpConstants.js +23 -7
  35. package/dist/core/HttpConstants.js.map +1 -1
  36. package/dist/core/OfflineQueue.d.ts +16 -0
  37. package/dist/core/OfflineQueue.js +10 -0
  38. package/dist/core/OfflineQueue.js.map +1 -1
  39. package/dist/core/RateLimiter.d.ts +24 -0
  40. package/dist/core/RateLimiter.js +43 -7
  41. package/dist/core/RateLimiter.js.map +1 -1
  42. package/dist/core/RestProtocol.d.ts +9 -0
  43. package/dist/core/RestProtocol.js +16 -1
  44. package/dist/core/RestProtocol.js.map +1 -1
  45. package/dist/core/RosError.d.ts +15 -0
  46. package/dist/core/RosError.js +36 -1
  47. package/dist/core/RosError.js.map +1 -1
  48. package/dist/core/RosProtocol.d.ts +21 -0
  49. package/dist/core/RosProtocol.js +40 -1
  50. package/dist/core/RosProtocol.js.map +1 -1
  51. package/dist/core/SchemaMapper.d.ts +41 -0
  52. package/dist/core/SchemaMapper.js +57 -2
  53. package/dist/core/SchemaMapper.js.map +1 -1
  54. package/dist/core/SocketClient.d.ts +34 -0
  55. package/dist/core/SocketClient.js +51 -3
  56. package/dist/core/SocketClient.js.map +1 -1
  57. package/dist/features/FileManager.d.ts +50 -0
  58. package/dist/features/FileManager.js +72 -6
  59. package/dist/features/FileManager.js.map +1 -1
  60. package/dist/features/LiveCollection.d.ts +51 -0
  61. package/dist/features/LiveCollection.js +69 -0
  62. package/dist/features/LiveCollection.js.map +1 -1
  63. package/dist/features/PrometheusExporter.d.ts +18 -1
  64. package/dist/features/PrometheusExporter.js +21 -1
  65. package/dist/features/PrometheusExporter.js.map +1 -1
  66. package/dist/index.d.ts +66 -0
  67. package/dist/index.js +78 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/types/index.d.ts +4 -0
  70. package/dist/types/index.js +24 -0
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/Helpers.d.ts +16 -0
  73. package/dist/utils/Helpers.js +17 -1
  74. package/dist/utils/Helpers.js.map +1 -1
  75. package/dist/utils/MikrotikCollection.d.ts +85 -0
  76. package/dist/utils/MikrotikCollection.js +97 -1
  77. package/dist/utils/MikrotikCollection.js.map +1 -1
  78. package/package.json +2 -2
@@ -4,71 +4,292 @@ exports.CommandBuilder = void 0;
4
4
  const Helpers_1 = require("../utils/Helpers");
5
5
  const MikrotikCollection_1 = require("../utils/MikrotikCollection");
6
6
  const OfflineQueue_1 = require("../core/OfflineQueue");
7
+ /**
8
+ * CommandBuilder.ts
9
+ * * The Fluent Interface Engine with Offline-First Capabilities.
10
+ * * Provides syntax sugar for constructing MikroTik commands.
11
+ * * Supports Real-Time Streaming, Persistent Queueing, Smart Caching,
12
+ * * and Enterprise REST Features (Idempotency, Projections).
13
+ */
7
14
  class CommandBuilder {
8
15
  constructor(client, menuPath) {
16
+ // Internal storage for query parts
9
17
  this.queryParams = {};
10
18
  this.propList = [];
19
+ // Internal state for execution options
11
20
  this._idempotent = false;
21
+ // Persistence Flag
12
22
  this.isPersistentRequest = false;
13
23
  this.client = client;
24
+ // Normalize path: Ensure it starts with '/' and doesn't end with '/'
14
25
  this.menuPath = menuPath.startsWith('/') ? menuPath : '/' + menuPath;
15
26
  if (this.menuPath.endsWith('/') && this.menuPath.length > 1) {
16
27
  this.menuPath = this.menuPath.slice(0, -1);
17
28
  }
18
29
  }
30
+ // ========================================================
31
+ // FLUENT FILTERS
32
+ // ========================================================
33
+ /**
34
+ * Adds a **Query Filter** (Equal Match) to the command.
35
+ *
36
+ * This method appends a filter parameter (`?key=value`) to the API request.
37
+ * Only items matching this condition will be returned or affected.
38
+ *
39
+ * **Feature: Auto-Kebab Case**
40
+ * You can use standard JavaScript camelCase keys. They are automatically converted
41
+ * to MikroTik's kebab-case format.
42
+ * - Input: `macAddress` -> Output: `?mac-address=...`
43
+ * - Input: `rxByte` -> Output: `?rx-byte=...`
44
+ *
45
+ * @param key The field name to filter by (e.g., 'name', 'disabled', 'macAddress').
46
+ * @param value The value to match. Booleans are converted to 'true'/'false' strings.
47
+ * @returns The current builder instance for chaining.
48
+ *
49
+ * @example
50
+ * // Find a specific user by name
51
+ * client.command('/ppp/secret')
52
+ * .where('name', 'john_doe')
53
+ * .print();
54
+ *
55
+ * @example
56
+ * // Find all disabled interfaces (camelCase supported)
57
+ * client.command('/interface')
58
+ * .where('disabled', true) // Sends ?disabled=true
59
+ * .print();
60
+ */
19
61
  where(key, value) {
20
62
  const kebabKey = (0, Helpers_1.camelToKebab)(key);
21
63
  this.queryParams[`?${kebabKey}`] = this.formatValue(value);
22
64
  return this;
23
65
  }
66
+ /**
67
+ * Adds an **Existence Filter** to the command.
68
+ *
69
+ * Matches items where the specified key exists (is defined), regardless of its value.
70
+ * This is useful for finding items that have optional properties set.
71
+ * Corresponds to the MikroTik API syntax `?key=`.
72
+ *
73
+ * @param key The field name to check for existence.
74
+ * @returns The current builder instance for chaining.
75
+ *
76
+ * @example
77
+ * // Find all firewall rules that have a comment
78
+ * client.command('/ip/firewall/filter')
79
+ * .whereExists('comment')
80
+ * .print();
81
+ */
24
82
  whereExists(key) {
25
83
  const kebabKey = (0, Helpers_1.camelToKebab)(key);
84
+ // In MikroTik API, existence is queried by sending the key without value (Socket)
85
+ // or properly mapped in REST via your translateToRest logic.
26
86
  this.queryParams[`?${kebabKey}`] = '';
27
87
  return this;
28
88
  }
89
+ /**
90
+ * **Field Projection (.select)**
91
+ *
92
+ * Restricts the fields returned by the router to a specific list.
93
+ * Using this method significantly reduces CPU load on the router and network bandwidth,
94
+ * especially when querying large tables like the routing table or logs.
95
+ * Corresponds to the MikroTik API argument `.proplist`.
96
+ *
97
+ * **Feature: Auto-Kebab Case**
98
+ * Inputs like `rxByte` are automatically converted to `rx-byte`.
99
+ *
100
+ * **Usage Levels:**
101
+ * * 1. **Novice:** Simple array of strings. `['name', 'address']`
102
+ * * 2. **Advanced (Surgical):** Type-safe list of keys ensuring they exist in interface T.
103
+ *
104
+ * @param fields An array of field names to retrieve (can be type-safe keys).
105
+ * @returns The current builder instance for chaining.
106
+ *
107
+ * @example
108
+ * // Get only the name and uptime of active users (ignoring traffic stats)
109
+ * const users = await client.command<PPPActive>('/ppp/active')
110
+ * .select(['name', 'uptime']) // optimized request
111
+ * .print();
112
+ */
29
113
  select(fields) {
30
114
  const fieldStrings = fields.map(String);
115
+ // Convert camelCase (JS) to kebab-case (MikroTik)
116
+ // e.g., 'callerId' -> 'caller-id'
31
117
  const kebabFields = fieldStrings.map(f => (0, Helpers_1.camelToKebab)(f));
32
- this.propList.push(...kebabFields);
118
+ // Use a Set to avoid duplicate fields in the request
119
+ const uniqueFields = new Set([...this.propList, ...kebabFields]);
120
+ this.propList = Array.from(uniqueFields);
33
121
  return this;
34
122
  }
123
+ /**
124
+ * **Enable Idempotency (.idempotent)**
125
+ *
126
+ * Flags the next operation to be **"Safe from Duplicates"**.
127
+ *
128
+ * * **Effect:** If you call `.add()` and the item already exists (based on a unique key like 'name'),
129
+ * the library will NOT throw an error. Instead, it will gracefully fetch and return the existing item.
130
+ * * **Requirement:** This is primarily supported in REST mode (v7+). In Socket mode, behavior depends on driver support.
131
+ *
132
+ * @returns The current builder instance for chaining.
133
+ * @example
134
+ * // Safe Create: Won't fail if 'vlan10' exists
135
+ * client.command('/interface/vlan')
136
+ * .idempotent()
137
+ * .add({ name: 'vlan10', 'vlan-id': 10 });
138
+ */
35
139
  idempotent(keyField) {
36
140
  this._idempotent = true;
37
141
  this._idempotencyKey = keyField;
38
142
  return this;
39
143
  }
144
+ // ========================================================
145
+ // PERSISTENCE MODIFIER
146
+ // ========================================================
147
+ /**
148
+ * **Offline Tolerance Strategy**
149
+ *
150
+ * Marks the current command as **Persistent**.
151
+ * Normally, if the router is disconnected, a command fails immediately.
152
+ * With `.persistent()`, the command is added to an internal retry queue
153
+ * and will be executed automatically as soon as the connection is restored.
154
+ *
155
+ * **Use Case:** Critical background tasks (e.g., scheduled billing cuts) that
156
+ * must eventually run, even if the network is currently unstable.
157
+ *
158
+ * @returns The current builder instance.
159
+ * @example
160
+ * // Even if the router is down, this will run when it comes back up.
161
+ * client.command('/system/reboot').persistent().send();
162
+ */
40
163
  persistent() {
41
164
  this.isPersistentRequest = true;
42
165
  return this;
43
166
  }
167
+ // ========================================================
168
+ // READ TERMINATORS (With Caching & Source Filtering)
169
+ // ========================================================
170
+ /**
171
+ * **Server-Side Search (Multi-Item)**
172
+ *
173
+ * Executes a search directly on the RouterOS CPU.
174
+ * Unlike client-side filtering, this method instructs the router to filter
175
+ * the data *before* sending it over the network.
176
+ *
177
+ * **Performance Note:**
178
+ * extremely efficient for large tables (like thousands of PPPoE secrets),
179
+ * as only the matching rows travel over the wire.
180
+ *
181
+ * @param criteria An object with key-value pairs to match (e.g., `{ disabled: 'true', profile: 'default' }`).
182
+ * @returns A `MikrotikCollection` containing only the matching items.
183
+ *
184
+ * @example
185
+ * // Fetch all users in the 'default' profile
186
+ * const users = await client.command('/ppp/secret').findBy({ profile: 'default' });
187
+ */
44
188
  async findBy(criteria) {
189
+ // Iterate over criteria and use the main .where() method.
190
+ // This ensures formatValue() and camelToKebab() are applied consistently.
45
191
  for (const [key, value] of Object.entries(criteria)) {
192
+ // Force casting to handle string|number|boolean properly
46
193
  this.where(key, value);
47
194
  }
195
+ // Execute the print command (which handles the REST/Socket translation)
48
196
  return this.print();
49
197
  }
198
+ /**
199
+ * **Server-Side Search (Single-Item)**
200
+ *
201
+ * The most efficient way to retrieve a single specific resource.
202
+ * It applies the filters, executes the query, and returns the first result.
203
+ *
204
+ * @param criteria Unique identifiers (e.g., `{ name: 'admin' }` or `{ macAddress: '00:...' }`).
205
+ * @returns The found item object, or `null` if no match was found.
206
+ *
207
+ * @example
208
+ * // Find a specific interface by name
209
+ * const ether1 = await client.command('/interface').findOne({ name: 'ether1' });
210
+ * if (ether1) console.log(ether1.mac_address);
211
+ */
50
212
  async findOne(criteria) {
213
+ // We reuse the central 'findBy' logic to ensure consistent query parsing
51
214
  const collection = await this.findBy(criteria);
52
- return collection.first();
215
+ return collection.count() > 0 ? collection.first() : null;
53
216
  }
217
+ /**
218
+ * **Execution Terminator: Print with Caching**
219
+ *
220
+ * Finalizes the builder chain and sends the `print` command to the router.
221
+ *
222
+ * **Feature: Smart Caching (TTL 5s)**
223
+ * To prevent flooding the router with redundant read requests (e.g., multiple UI components
224
+ * asking for the same data), this method implements a **Read-Through Cache**.
225
+ * 1. **Cache Hit:** If the exact same query was made < 5 seconds ago, returns local data immediately.
226
+ * 2. **Cache Miss:** Fetches from the router, stores the result, and returns it.
227
+ *
228
+ * **Feature: Garbage Collection**
229
+ * Includes a probabilistic strategy (5% chance) to prune expired cache entries
230
+ * on every call to keep memory footprint low.
231
+ *
232
+ * @param extraParams Optional explicit parameters (e.g., `{ 'count-only': 'true' }`).
233
+ * @returns A `MikrotikCollection` (v1.2.0) equipped with pagination and transformation tools.
234
+ *
235
+ * @example
236
+ * // EXAMPLE 1: Standard Fetch (Basic Array)
237
+ * // Get all active PPPoE users as a simple array
238
+ * const users = await client.command('/ppp/active').print().then(c => c.toArray());
239
+ *
240
+ * @example
241
+ * // EXAMPLE 2: Pagination (Frontend Tables)
242
+ * // Get Page 2 of secrets, 25 items per page
243
+ * const page2 = await client.command('/ppp/secret')
244
+ * .where('disabled', 'false')
245
+ * .print()
246
+ * .then(c => c.toPages(2, 25));
247
+ *
248
+ * @example
249
+ * // EXAMPLE 3: High-Performance Lookup (O(1) Map)
250
+ * // Index users by name for instant access without looping
251
+ * const userMap = await client.command('/ppp/secret')
252
+ * .print()
253
+ * .then(c => c.toMap('name'));
254
+ *
255
+ * console.log(userMap['juan_perez']?.password); // Instant access!
256
+ *
257
+ * @example
258
+ * // EXAMPLE 4: Reporting (Grouping)
259
+ * // Group active connections by Service Type (pppoe vs ovpn)
260
+ * const report = await client.command('/ppp/active')
261
+ * .print()
262
+ * .then(c => c.toGrouped('service'));
263
+ *
264
+ * console.log(`PPPoE Users: ${report['pppoe']?.length || 0}`);
265
+ */
54
266
  async print(extraParams) {
55
267
  const fluentParams = this.getParams();
56
268
  const finalParams = { ...fluentParams, ...extraParams };
269
+ // GENERATE CACHE KEY
270
+ // Unique key based on: Router Host + Menu Path + Query Parameters
57
271
  const host = this.client.options?.host || 'default';
58
272
  const cacheKey = `${host}:${this.menuPath}:${JSON.stringify(finalParams)}`;
273
+ // CHECK CACHE
59
274
  const cached = CommandBuilder.queryCache.get(cacheKey);
60
275
  if (cached && Date.now() < cached.expires) {
61
276
  return new MikrotikCollection_1.MikrotikCollection(cached.data);
62
277
  }
278
+ // NETWORK REQUEST (Cache Miss)
63
279
  const rawData = await this.client.write(`${this.menuPath}/print`, finalParams);
280
+ // SAVE TO CACHE
64
281
  CommandBuilder.queryCache.set(cacheKey, {
65
282
  data: rawData,
66
283
  expires: Date.now() + CommandBuilder.CACHE_TTL_MS
67
284
  });
285
+ // Garbage Collection: Clean up old keys randomly (simple strategy)
68
286
  if (Math.random() > 0.95)
69
287
  this.pruneCache();
70
288
  return new MikrotikCollection_1.MikrotikCollection(rawData);
71
289
  }
290
+ /**
291
+ * Helper to clean expired cache entries to prevent memory leaks.
292
+ */
72
293
  pruneCache() {
73
294
  const now = Date.now();
74
295
  for (const [key, val] of CommandBuilder.queryCache.entries()) {
@@ -76,79 +297,306 @@ class CommandBuilder {
76
297
  CommandBuilder.queryCache.delete(key);
77
298
  }
78
299
  }
300
+ /**
301
+ * **Execution Terminator: First Result**
302
+ *
303
+ * Syntactic sugar for fetching a list and retrieving only the first item.
304
+ * Useful when you know the query will return a single result or you only care about the top record.
305
+ *
306
+ * @returns The first item of type `T`, or `null` if the collection is empty.
307
+ * @example
308
+ * // Get the first active admin user
309
+ * const admin = await client.command('/user').where('group', 'full').first();
310
+ */
79
311
  async first() {
312
+ // We execute print(). Note: MikroTik API does not natively support 'LIMIT 1'
313
+ // effectively without scripting, so this fetches the filtered list.
80
314
  const collection = await this.print();
81
- return collection.first();
315
+ // Ensure we return null if the collection is empty, not undefined.
316
+ return collection.count() > 0 ? collection.first() : null;
82
317
  }
318
+ // ========================================================
319
+ // WRITE TERMINATORS (Add/Set/Remove)
320
+ // ========================================================
321
+ /**
322
+ * **Execution Terminator: Create Resource (ADD)**
323
+ *
324
+ * Sends an `/add` command to the router to create a new item.
325
+ *
326
+ * **Feature: Automatic Cache Invalidation**
327
+ * Upon success, this method automatically invalidates the local cache for this menu path.
328
+ * This guarantees that the next `.print()` call will fetch fresh data from the router,
329
+ * including the item you just created.
330
+ *
331
+ * **Feature: Offline Queueing**
332
+ * If the router is unreachable and `.persistent()` was used (or global offline mode is on),
333
+ * the command is saved to the `OfflineQueue` for later execution.
334
+ *
335
+ * **Feature: Idempotency**
336
+ * If `.idempotent()` was called, passes the flag to the client to safely handle duplicates.
337
+ *
338
+ * @param data The object containing the properties for the new item.
339
+ * @returns The MikroTik internal ID (e.g., `*1A`) of the created item, or `'QUEUED_OFFLINE'`.
340
+ *
341
+ * @example
342
+ * // Add a new firewall address list entry
343
+ * const id = await client.command('/ip/firewall/address-list').add({
344
+ * list: 'allowed_users',
345
+ * address: '192.168.88.50',
346
+ * comment: 'Added via API'
347
+ * });
348
+ */
83
349
  async add(data) {
84
350
  const params = this.prepareParams(data);
351
+ // OFFLINE CHECK
85
352
  if (this.shouldDefer()) {
86
353
  OfflineQueue_1.OfflineQueue.enqueue({ action: 'add', path: this.menuPath, params: params });
87
354
  return 'QUEUED_OFFLINE';
88
355
  }
356
+ // REAL EXECUTION
89
357
  const response = await this.client.write(`${this.menuPath}/add`, params, {
90
358
  idempotent: this._idempotent,
91
359
  idempotencyKey: this._idempotencyKey
92
360
  });
93
361
  this.invalidatePathCache();
362
+ let responseObj = null;
94
363
  if (Array.isArray(response) && response.length > 0) {
95
- if (this._idempotent && response[0]['_idempotent_recovery']) {
96
- return response[0];
364
+ responseObj = response[0]; // (Socket)
365
+ }
366
+ else if (typeof response === 'object' && response !== null) {
367
+ responseObj = response; // (REST)
368
+ }
369
+ // Idempotency Recovery
370
+ if (this._idempotent && responseObj && responseObj['_idempotent_recovery']) {
371
+ return responseObj;
372
+ }
373
+ let newId = responseObj ? (responseObj['ret'] || responseObj['.id']) : null;
374
+ if (!newId && params['name']) {
375
+ try {
376
+ const search = await this.client.write(`${this.menuPath}/print`, {
377
+ '?name': params['name']
378
+ });
379
+ if (Array.isArray(search) && search.length > 0) {
380
+ return search[0];
381
+ }
382
+ }
383
+ catch (e) {
384
+ console.warn("Fallback search failed", e);
385
+ }
386
+ }
387
+ if (newId) {
388
+ try {
389
+ const fetchResponse = await this.client.write(`${this.menuPath}/print`, { '.id': newId });
390
+ if (Array.isArray(fetchResponse) && fetchResponse.length > 0) {
391
+ return fetchResponse[0];
392
+ }
393
+ // (REST direct ID fetch)
394
+ else if (typeof fetchResponse === 'object' && fetchResponse !== null && !Array.isArray(fetchResponse)) {
395
+ return fetchResponse;
396
+ }
97
397
  }
98
- if (response[0]['ret']) {
99
- return response[0]['ret'];
398
+ catch (error) {
399
+ console.warn(`Auto-fetch failed for ${newId}`, error);
100
400
  }
401
+ return newId;
101
402
  }
102
403
  return '';
103
404
  }
405
+ /**
406
+ * **Get or Create (Syntactic Sugar)**
407
+ * * A shorthand method that enables idempotency and executes the add.
408
+ * * Semantically clearer for business logic "Ensure this exists".
409
+ * * @param data The data to ensure exists.
410
+ * @returns The Single item created or recovered.
411
+ */
104
412
  async getOrCreate(data) {
105
- this.idempotent();
413
+ this.idempotent(); // Enable flag
106
414
  const result = await this.add(data);
415
+ // If result is string (Socket ID), we might need to fetch it (not implemented here for speed)
416
+ // If result is object (REST recovery), return it.
107
417
  if (typeof result === 'object')
108
418
  return result;
419
+ // Fallback for ID return
109
420
  return { '.id': result };
110
421
  }
422
+ /**
423
+ * **Execution Terminator: Update Resource (SET)**
424
+ *
425
+ * Sends a `/set` command to modify an existing item.
426
+ *
427
+ * **Feature: Idempotency & Cache**
428
+ * Like `.add()`, this triggers cache invalidation. It also handles the tricky
429
+ * `.id` parameter requirement of MikroTik automatically.
430
+ *
431
+ * @param id The internal ID of the item (e.g., `*14`) or a unique name if supported by the menu.
432
+ * @param data An object containing ONLY the fields you want to change (Partial update).
433
+ *
434
+ * @example
435
+ * // Update a PPPoE secret's password
436
+ * await client.command('/ppp/secret').set('*1F', {
437
+ * password: 'new_secure_password',
438
+ * comment: 'Password changed on ' + new Date().toISOString()
439
+ * });
440
+ */
111
441
  async set(id, data) {
112
442
  const params = this.prepareParams(data);
113
443
  params['.id'] = id;
444
+ // OFFLINE CHECK
114
445
  if (this.shouldDefer()) {
115
446
  OfflineQueue_1.OfflineQueue.enqueue({ action: 'set', path: this.menuPath, params: params });
116
- return;
447
+ throw new Error("OFFLINE_QUEUED");
117
448
  }
118
449
  await this.client.write(`${this.menuPath}/set`, params);
119
450
  this.invalidatePathCache();
451
+ // AUTO-FETCH
452
+ try {
453
+ const fetchResponse = await this.client.write(`${this.menuPath}/print`, { '.id': id });
454
+ if (Array.isArray(fetchResponse) && fetchResponse.length > 0) {
455
+ return fetchResponse[0];
456
+ }
457
+ else if (typeof fetchResponse === 'object' && fetchResponse !== null) {
458
+ if (!Array.isArray(fetchResponse)) {
459
+ return fetchResponse;
460
+ }
461
+ }
462
+ }
463
+ catch (error) {
464
+ console.warn(`Set successful, but auto-fetch failed for ${id}`, error);
465
+ }
466
+ return { '.id': id, ...data };
120
467
  }
468
+ /**
469
+ * **Execution Terminator: Delete Resource (REMOVE)**
470
+ *
471
+ * Sends a `/remove` command to delete one or more items.
472
+ *
473
+ * **Feature: Batch Deletion**
474
+ * You can pass an array of IDs to delete multiple items in a single API call,
475
+ * which is significantly faster than a loop of delete calls.
476
+ *
477
+ * @param id A single ID string (e.g., `*1A`) or an array of IDs.
478
+ *
479
+ * @example
480
+ * // Remove a single item
481
+ * await client.command('/queue/simple').remove('*A1');
482
+ *
483
+ * @example
484
+ * // Batch remove (Kick multiple active connections)
485
+ * const idsToKick = ['*8001', '*8002', '*8003'];
486
+ * await client.command('/ppp/active').remove(idsToKick);
487
+ */
121
488
  async remove(id) {
122
- const ids = Array.isArray(id) ? id.join(',') : id;
123
- const params = { '.id': ids };
489
+ // Always normalize input to an Array
490
+ const ids = Array.isArray(id) ? id : [id];
491
+ // OFFLINE CHECK
124
492
  if (this.shouldDefer()) {
125
- OfflineQueue_1.OfflineQueue.enqueue({ action: 'remove', path: this.menuPath, params: params });
126
- return;
493
+ // For offline queue, store the "joined" version (comma-separated) for compactness
494
+ OfflineQueue_1.OfflineQueue.enqueue({
495
+ action: 'remove',
496
+ path: this.menuPath,
497
+ params: { '.id': ids.join(',') }
498
+ });
499
+ // Return empty array indicating online deletion was not confirmed
500
+ return [];
501
+ }
502
+ try {
503
+ // Execute parallel requests.
504
+ // This ensures compatibility with REST API, which typically requires individual
505
+ // DELETE requests per ID rather than a comma-separated list in the URL path.
506
+ await Promise.all(ids.map(singleId => {
507
+ return this.client.write(`${this.menuPath}/remove`, { '.id': singleId });
508
+ }));
509
+ }
510
+ catch (error) {
511
+ console.error("Error during bulk removal:", error);
512
+ throw error;
127
513
  }
128
- await this.client.write(`${this.menuPath}/remove`, params);
514
+ // Invalidate Cache for this path to ensure next read is fresh
129
515
  this.invalidatePathCache();
516
+ return ids;
130
517
  }
518
+ // ========================================================
519
+ // STREAMING TERMINATORS (UPDATED)
520
+ // ========================================================
521
+ /**
522
+ * **Streaming Terminator: Data Watcher**
523
+ *
524
+ * Initiates a standard real-time stream using the RouterOS `=follow=` protocol.
525
+ * Used for monitoring **changes in configuration or state** (e.g., "Tell me when a new log appears"
526
+ * or "Notify me when a user connects").
527
+ *
528
+ * **Protocol Internals:**
529
+ * This method automatically constructs the complex packet structure required by RouterOS:
530
+ * - Filters are sent as Queries (`?name=...`).
531
+ * - Properties are sent as Attributes (`=.proplist=...`).
532
+ * - The Streaming Flag (`=follow=`) is appended at the end.
533
+ *
534
+ * @param callback Function to execute whenever a new data packet arrives.
535
+ * @returns A `Subscription` object with a `.stop()` method to cancel the stream.
536
+ *
537
+ * @example
538
+ * // Monitor the System Log in real-time
539
+ * const logStream = client.command('/log')
540
+ * .where('topics', 'error') // Only listen for errors
541
+ * .listen((entry) => {
542
+ * console.log(`NEW ERROR: ${entry.message}`);
543
+ * });
544
+ *
545
+ * // Stop after 1 minute
546
+ * setTimeout(() => logStream.stop(), 60000);
547
+ */
131
548
  listen(callback) {
549
+ // Build Base Command
132
550
  const lines = [`${this.menuPath}/print`];
551
+ // Get All Parameters (Filters + Selects)
133
552
  const params = this.getParams();
553
+ // Smart Parsing: Distinguish between Queries (?) and Attributes (=)
134
554
  for (const [key, value] of Object.entries(params)) {
135
555
  let cleanKey = key;
556
+ // Remove existing '?' prefix if present (from .where())
136
557
  if (cleanKey.startsWith('?')) {
137
558
  cleanKey = cleanKey.substring(1);
138
559
  }
139
560
  if (cleanKey === '.proplist') {
561
+ // Attributes MUST start with '='
140
562
  lines.push(`=${cleanKey}=${value}`);
141
563
  }
142
564
  else {
565
+ // Filters MUST start with '?'
143
566
  lines.push(`?${cleanKey}=${value}`);
144
567
  }
145
568
  }
569
+ // Streaming Argument
146
570
  lines.push('=follow=');
571
+ // Send Raw Array to Client (Polymorphic Stream)
147
572
  return this.client.stream(lines, undefined, callback);
148
573
  }
574
+ /**
575
+ * **Streaming Terminator: Traffic & Torch**
576
+ *
577
+ * A specialized listener designed for commands that stream data but **do not** support
578
+ * the standard `/print` syntax, specifically `/interface/monitor-traffic` and `/tool/torch`.
579
+ *
580
+ * **Difference from .listen():**
581
+ * Standard listeners use Query parameters (`?key=value`). Monitor commands require
582
+ * Action parameters (`=key=value`). This method automatically converts your `.where()`
583
+ * filters into the correct format for these tools.
584
+ *
585
+ * @param callback Function to execute with the live metric data.
586
+ * @returns A `Subscription` object.
587
+ *
588
+ * @example
589
+ * // Monitor Bandwidth on Ether1
590
+ * client.command('/interface') // or '/interface/monitor-traffic'
591
+ * .where('interface', 'ether1')
592
+ * .listenMonitor((stats) => {
593
+ * console.log(`RX: ${stats['rx-bits-per-second']} bps`);
594
+ * });
595
+ */
149
596
  listenMonitor(callback) {
150
597
  const rawParams = this.getParams();
151
598
  const actionParams = {};
599
+ // Convert standard query params to action params (remove '?')
152
600
  for (const [key, value] of Object.entries(rawParams)) {
153
601
  const cleanKey = key.startsWith('?') ? key.substring(1) : key;
154
602
  actionParams[cleanKey] = value;
@@ -159,6 +607,12 @@ class CommandBuilder {
159
607
  }
160
608
  return this.client.stream(cmd, actionParams, callback);
161
609
  }
610
+ // ========================================================
611
+ // INTERNAL HELPERS
612
+ // ========================================================
613
+ /**
614
+ * Clears cache entries related to the current menu path.
615
+ */
162
616
  invalidatePathCache() {
163
617
  const host = this.client.options?.host || 'default';
164
618
  const prefix = `${host}:${this.menuPath}`;
@@ -174,14 +628,21 @@ class CommandBuilder {
174
628
  return !this.isClientConnected();
175
629
  }
176
630
  isClientConnected() {
631
+ // Accessing private socket property via 'any' casting (safe within library context)
177
632
  const socket = this.client['socket'];
633
+ // If socket is null (pure REST mode), assume connected via HTTP or let fetch fail naturally
178
634
  if (!socket)
179
635
  return true;
180
636
  return socket['connected'] === true;
181
637
  }
638
+ /**
639
+ * Merges query params (filters) and property list (selects).
640
+ */
182
641
  getParams() {
183
642
  const params = { ...this.queryParams };
643
+ // Add .proplist if select() was used
184
644
  if (this.propList.length > 0) {
645
+ // Note: We add it as a plain key here, listen() handles the '=' prefix
185
646
  params['.proplist'] = this.propList.join(',');
186
647
  }
187
648
  return params;
@@ -202,6 +663,13 @@ class CommandBuilder {
202
663
  }
203
664
  }
204
665
  exports.CommandBuilder = CommandBuilder;
666
+ // ========================================================
667
+ // STATIC CACHE (Shared across builders)
668
+ // ========================================================
669
+ /**
670
+ * Short-lived cache to prevent read-spamming the router.
671
+ * TTL: 5 Seconds.
672
+ */
205
673
  CommandBuilder.queryCache = new Map();
206
- CommandBuilder.CACHE_TTL_MS = 5000;
674
+ CommandBuilder.CACHE_TTL_MS = 5000; // 5s
207
675
  //# sourceMappingURL=CommandBuilder.js.map