jw-automator 3.2.0 → 4.0.1
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/CHANGELOG.md +23 -9
- package/README.md +27 -25
- package/docs/ARCHITECTURE.md +8 -6
- package/docs/MIGRATION.md +68 -160
- package/docs/QUICKSTART.md +29 -7
- package/docs/defensive-defaults.md +65 -0
- package/package.json +2 -2
- package/src/Automator.js +75 -42
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to jw-automator will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.0.0] - 2025-11-19
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
|
|
12
|
+
- **Smart Default `catchUpWindow` Behavior**: The default behavior for `catchUpWindow` has fundamentally changed. Instead of defaulting to `"unlimited"`, it now defaults based on action type:
|
|
13
|
+
- For recurring actions, it defaults to the recurrence interval duration.
|
|
14
|
+
- For one-time actions, it defaults to `0` (no catch-up).
|
|
15
|
+
- **Impact:** Users relying on the previous `"unlimited"` default for actions where `catchUpWindow` was not explicitly set will experience different behavior.
|
|
16
|
+
- **Invalid `repeat.type` is now a Fatal Error**: Providing a missing or invalid `repeat.type` (e.g., a typo) will now throw a hard `Error` immediately when the action is added or updated.
|
|
17
|
+
- **Impact:** Previously, this would be defensively coerced to `'day'` with an `error` event. Code relying on this coercion will now crash.
|
|
18
|
+
- **Coercion Events changed from `error` to `warning`**: All defensive coercions (e.g., invalid `repeat.interval`, negative `catchUpWindow`, invalid `repeat.limit`) now emit a `warning` event instead of an `error` event.
|
|
19
|
+
- **Impact:** Users listening for `automator.on('error', ...)` to catch these specific coercion notifications will need to update their code to listen for `automator.on('warning', ...)` instead.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`warning` Event Type**: A new event type, `warning`, has been introduced for non-fatal data coercions and corrections during action validation.
|
|
24
|
+
- **Smart `catchUpWindow` Defaults**: Actions now automatically infer a sensible `catchUpWindow` default based on whether they are one-time (default `0`) or recurring (default to recurrence interval duration).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- The "Defensive Validation" strategy has been refined to distinguish between fatal configuration errors and non-fatal coercions, using `Error` throws for the former and `warning` events for the latter.
|
|
29
|
+
|
|
8
30
|
## [3.2.0] - 2025-11-18
|
|
9
31
|
|
|
10
32
|
### Added
|
|
@@ -23,15 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
23
45
|
- Fast-forward optimization uses mathematical projection to instantly advance high-frequency tasks
|
|
24
46
|
- Uses `"unlimited"` string literal instead of `Infinity` for clean JSON serialization
|
|
25
47
|
|
|
26
|
-
- **Defensive Validation**: "Fail loudly, run defensively" philosophy
|
|
27
|
-
- Invalid `repeat.type` → defaults to `'day'` with ERROR event
|
|
28
|
-
- Invalid `repeat.interval` → coerced to `Math.max(1, Math.floor(value))` with ERROR event
|
|
29
|
-
- Invalid `repeat.limit` → defaults to `null` (unlimited) with ERROR event
|
|
30
|
-
- Invalid `repeat.endDate` → ignored with ERROR event
|
|
31
|
-
- Invalid `catchUpWindow` → defaults to `"unlimited"` with ERROR event
|
|
32
|
-
- Negative `catchUpWindow` → coerced to `0` with ERROR event
|
|
33
|
-
- Missing `date` → defaults to 5 seconds from now with DEBUG event
|
|
34
|
-
- Unregistered commands → emit DEBUG event, keep trying (never remove action)
|
|
48
|
+
- **Defensive Validation (Initial Implementation)**: Implemented "Fail loudly, run defensively" philosophy with various coercions and `error` events for invalid inputs. This initial strategy was further refined in v4.0.0, which introduced a dedicated `warning` event and stricter validation for `repeat.type`.
|
|
35
49
|
|
|
36
50
|
### Changed
|
|
37
51
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 📚 **jw-automator
|
|
1
|
+
# 📚 **jw-automator v4**
|
|
2
2
|
|
|
3
3
|
### A resilient, local-time, 1-second precision automation scheduler for Node.js
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## ⭐️ Overview
|
|
10
10
|
|
|
11
|
-
**jw-automator** is a robust automation engine designed for small devices, home automation hubs, personal servers, and Node.js environments where **correctness, resilience, and local-time behavior** matter more than millisecond precision.
|
|
11
|
+
**jw-automator** is a robust automation engine designed for small devices, home automation hubs, personal servers, and Node.js environments where **correctness, resilience, and local-time behavior** matter more than millisecond precision. Version 4 introduces enhanced defensive defaults and clearer error handling, making it even more predictable and robust.
|
|
12
12
|
|
|
13
13
|
Where traditional cron falls short — missed executions, poor DST handling, limited recurrence, lack of catch-up semantics — **jw-automator** provides a predictable, human-centric scheduling model:
|
|
14
14
|
|
|
@@ -139,35 +139,37 @@ This avoids cron's silent-but-surprising behaviors.
|
|
|
139
139
|
|
|
140
140
|
---
|
|
141
141
|
|
|
142
|
-
### 4. **Resilient Offline Catch-Up with
|
|
142
|
+
### 4. **Resilient Offline Catch-Up with Smart Defaults**
|
|
143
143
|
|
|
144
|
-
When the device is offline or delayed, you can control exactly how far back to catch up using the `catchUpWindow` property
|
|
144
|
+
When the device is offline or delayed, you can control exactly how far back to catch up using the `catchUpWindow` property. Automator v4 introduces **smart defaults** that infer the desired behavior based on your action's type, making the system more robust and predictable out-of-the-box.
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
catchUpWindow:
|
|
150
|
-
|
|
146
|
+
**`catchUpWindow` Behavior:**
|
|
147
|
+
|
|
148
|
+
* **Explicitly Set (milliseconds or `"unlimited"`):** Your explicit `catchUpWindow` value always takes precedence.
|
|
149
|
+
* `catchUpWindow: "unlimited"`: Catch up ALL missed executions.
|
|
150
|
+
* `catchUpWindow: 0`: Skip ALL missed executions (real-time only).
|
|
151
|
+
* `catchUpWindow: 5000`: Catch up if missed by ≤5 seconds, skip if older.
|
|
152
|
+
* **Smart Default (Recurring Actions):** If not specified, `catchUpWindow` defaults to the **duration of the action's recurrence interval**.
|
|
153
|
+
* Example: A `repeat: { type: 'hour', interval: 1 }` action will default to a 1-hour `catchUpWindow`. If missed by less than an hour, it runs. If missed by more, it fast-forwards to the next scheduled interval.
|
|
154
|
+
* **Smart Default (One-Time Actions):** If not specified and the action has no `repeat` property, `catchUpWindow` defaults to **`0`**.
|
|
155
|
+
* Example: A one-time task scheduled for 2:00 AM that's missed due to downtime will not run when the server comes back online later.
|
|
151
156
|
|
|
152
157
|
**How it works:**
|
|
153
158
|
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
159
|
+
* If an action is missed by less than its effective `catchUpWindow`, it executes (recovers from brief glitches or short offline periods).
|
|
160
|
+
* If missed by more, it's skipped and fast-forwarded to its next future scheduled time (prevents "thundering herds" after extended outages).
|
|
161
|
+
* The fast-forward optimization uses mathematical projection to instantly advance high-frequency tasks.
|
|
157
162
|
|
|
158
|
-
**
|
|
163
|
+
**Events for Coercion & Validation:**
|
|
159
164
|
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* **Sensor readings:** `catchUpWindow: 5000` - Tolerate 5s lag, skip if system was down for hours
|
|
165
|
+
* **`warning` event:** Emitted when `catchUpWindow` or `repeat` properties are syntactically invalid and have been defensively coerced to a sensible default (e.g., negative interval becomes `1`).
|
|
166
|
+
* **`Error` (thrown):** For fundamental issues like an invalid `repeat.type` (e.g., typo like `'horu'`). This is a fatal error, as the user's intent cannot be reliably determined.
|
|
163
167
|
|
|
164
168
|
**Backwards compatibility:**
|
|
165
169
|
|
|
166
|
-
The legacy `unBuffered` property is still
|
|
167
|
-
|
|
168
|
-
unBuffered:
|
|
169
|
-
unBuffered: true // equivalent to catchUpWindow: 0
|
|
170
|
-
```
|
|
170
|
+
The legacy `unBuffered` property is still supported and maps directly to `catchUpWindow` behavior:
|
|
171
|
+
* `unBuffered: false` is equivalent to `catchUpWindow: "unlimited"`
|
|
172
|
+
* `unBuffered: true` is equivalent to `catchUpWindow: 0`
|
|
171
173
|
|
|
172
174
|
---
|
|
173
175
|
|
|
@@ -340,6 +342,7 @@ Listen to events using `automator.on(event, callback)`:
|
|
|
340
342
|
* `action` - Action executed
|
|
341
343
|
* `update` - Action added/updated/removed
|
|
342
344
|
* `error` - Error occurred
|
|
345
|
+
* `warning` - Non-fatal data coercion or correction occurred
|
|
343
346
|
* `debug` - Debug information
|
|
344
347
|
|
|
345
348
|
```js
|
|
@@ -456,8 +459,7 @@ npm run test:coverage
|
|
|
456
459
|
| `cmd` | Name of registered function to execute |
|
|
457
460
|
| `payload` | Data passed to the command |
|
|
458
461
|
| `date` | Next scheduled run time (local `Date`) |
|
|
459
|
-
| `catchUpWindow` | Time window for catching up missed executions (default
|
|
460
|
-
| `unBuffered` | Legacy: Skip missed events (`true`) or catch up (`false`) |
|
|
462
|
+
| `catchUpWindow` | Time window for catching up missed executions (smart default based on action type, or milliseconds number) |
|
|
461
463
|
|
|
462
464
|
### Repeat block:
|
|
463
465
|
|
|
@@ -472,7 +474,7 @@ npm run test:coverage
|
|
|
472
474
|
|
|
473
475
|
---
|
|
474
476
|
|
|
475
|
-
## 🎯 Project Goals (
|
|
477
|
+
## 🎯 Project Goals (v4)
|
|
476
478
|
|
|
477
479
|
* Deterministic behavior
|
|
478
480
|
* Rock-solid DST handling
|
|
@@ -493,6 +495,6 @@ MIT
|
|
|
493
495
|
|
|
494
496
|
## ❤️ Acknowledgments
|
|
495
497
|
|
|
496
|
-
jw-automator
|
|
498
|
+
jw-automator v4 is a ground-up rethinking of the original jw-automator library, preserving the spirit while strengthening the foundations.
|
|
497
499
|
|
|
498
500
|
If you're building automation logic and want predictable, human-friendly scheduling that survives the real world — **welcome.**
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Architecture Overview
|
|
1
|
+
# Architecture Overview (v4)
|
|
2
2
|
|
|
3
|
-
## jw-automator
|
|
3
|
+
## jw-automator v4 Architecture
|
|
4
4
|
|
|
5
5
|
This document describes the internal architecture of jw-automator v3.
|
|
6
6
|
|
|
@@ -244,11 +244,13 @@ Returns event list (state unchanged)
|
|
|
244
244
|
- **Simulatable**: Preview future schedules
|
|
245
245
|
- **Catch-up**: Process offline gaps identically to real-time
|
|
246
246
|
|
|
247
|
-
### Why Buffered/UnBuffered?
|
|
247
|
+
### Why `catchUpWindow` (and Legacy Buffered/UnBuffered)?
|
|
248
248
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
-
|
|
249
|
+
The `catchUpWindow` property, supported by the CoreEngine, precisely defines the time window for recovering missed actions.
|
|
250
|
+
|
|
251
|
+
- **Smart Defaults**: For recurring actions, the `catchUpWindow` defaults to the action's interval (e.g., a 1-hour action has a 1-hour catch-up window). For one-time actions, it defaults to `0` (no catch-up). This prevents "thundering herd" issues by ensuring that actions too old are simply skipped.
|
|
252
|
+
- **Explicit Control**: Users can still set `catchUpWindow` to `0` (skip all missed), a specific millisecond value (tolerate N ms lag), or `"unlimited"` (catch up all, like old buffered behavior).
|
|
253
|
+
- **Legacy `unBuffered`**: The `unBuffered` flag (`true` or `false`) is now a legacy alias for `catchUpWindow: 0` and `catchUpWindow: "unlimited"` respectively. The system transparently maps it.
|
|
252
254
|
|
|
253
255
|
---
|
|
254
256
|
|
package/docs/MIGRATION.md
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
# Migration Guide:
|
|
1
|
+
# Migration Guide: v3 to v4
|
|
2
2
|
|
|
3
|
-
This guide helps you migrate from jw-automator
|
|
3
|
+
This guide helps you migrate from jw-automator v3 to v4.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
jw-automator
|
|
9
|
+
jw-automator v4 is a significant refinement of v3's architecture, focusing on making the scheduler's behavior even more predictable and robust out-of-the-box. While v3 represented a complete rewrite, v4 introduces breaking changes by refining default behaviors and error handling for action specifications.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
v3 was the widely-deployed production version. v4 builds upon that foundation with enhanced predictability.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## Breaking Changes
|
|
15
|
+
## Breaking Changes from v3 to v4
|
|
16
|
+
|
|
17
|
+
### 1. Default `catchUpWindow` Behavior Changed
|
|
18
|
+
|
|
19
|
+
**v3:** If `catchUpWindow` was not specified, it defaulted to `"unlimited"` (catch up all missed executions).
|
|
20
|
+
**v4:** If `catchUpWindow` is not specified, it now uses a **smart default** based on the action type:
|
|
21
|
+
- For **recurring actions**, it defaults to the **duration of the recurrence interval** (e.g., an hourly action gets a 1-hour `catchUpWindow`).
|
|
22
|
+
- For **one-time actions**, it defaults to **`0`** (skip all missed executions).
|
|
23
|
+
|
|
24
|
+
**Impact:** User applications that relied on the implicit "unlimited" catch-up for all actions in v3 might now see actions being skipped or fast-forwarded more aggressively. If you desire the old "unlimited" catch-up, you must explicitly set `catchUpWindow: "unlimited"`.
|
|
25
|
+
|
|
26
|
+
### 2. Invalid `repeat.type` Throws a Fatal Error
|
|
27
|
+
|
|
28
|
+
**v3:** If `repeat.type` was missing or invalid (e.g., a typo like `'horu'`), Automator would defensively coerce it to `'day'` and emit an `error` event.
|
|
29
|
+
**v4:** An invalid or missing `repeat.type` now throws a hard `Error` immediately.
|
|
30
|
+
|
|
31
|
+
**Impact:** Code that previously succeeded by silently allowing `repeat.type` coercions will now fail fast, forcing explicit correction. This ensures that the action's intent is never misinterpreted.
|
|
32
|
+
|
|
33
|
+
### 3. Coercion Events Changed from `error` to `warning`
|
|
34
|
+
|
|
35
|
+
**v3:** Non-fatal defensive coercions (e.g., an invalid `repeat.interval` being set to `1`, a negative `catchUpWindow` becoming `0`) would emit an `error` event.
|
|
36
|
+
**v4:** These same non-fatal coercions now emit a `warning` event. The `error` event is reserved for more critical issues (e.g., storage failures).
|
|
37
|
+
|
|
38
|
+
**Impact:** If your application was listening for `automator.on('error', ...)` to catch these coercion notifications, you must now update your event listener to `automator.on('warning', ...)` to continue receiving them.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Breaking Changes from v3 to v4
|
|
16
43
|
|
|
17
44
|
### 1. Constructor and Initialization
|
|
18
45
|
|
|
@@ -146,6 +173,20 @@ automator.updateActionByName('Old Name', {
|
|
|
146
173
|
|
|
147
174
|
## New Features in v3
|
|
148
175
|
|
|
176
|
+
## New Features in v4
|
|
177
|
+
|
|
178
|
+
### 1. Smart `catchUpWindow` Defaults
|
|
179
|
+
|
|
180
|
+
The new intelligent default system for `catchUpWindow` means that for most actions, you no longer need to explicitly define this property to get predictable, sensible behavior. It automatically adapts based on whether your action is one-time or recurring.
|
|
181
|
+
|
|
182
|
+
### 2. Dedicated `warning` Event
|
|
183
|
+
|
|
184
|
+
A new `warning` event (`automator.on('warning', ...)`) provides a clearer channel for non-fatal feedback about defensive coercions. This allows developers to distinguish between critical runtime errors and minor data corrections.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## New Features in v3
|
|
189
|
+
|
|
149
190
|
### 1. Simulation
|
|
150
191
|
|
|
151
192
|
Preview future schedules without running them:
|
|
@@ -232,180 +273,47 @@ const automator = new Automator({
|
|
|
232
273
|
|
|
233
274
|
---
|
|
234
275
|
|
|
235
|
-
## Migration Steps
|
|
236
|
-
|
|
237
|
-
### Step 1: Update Initialization
|
|
238
|
-
|
|
239
|
-
Replace your v2 initialization with v3 constructor:
|
|
276
|
+
## Migration Steps (v3 to v4)
|
|
240
277
|
|
|
241
|
-
|
|
242
|
-
// Before
|
|
243
|
-
const automator = require('jw-automator');
|
|
244
|
-
automator.init({ file: './actions.json' });
|
|
278
|
+
### Step 1: Understand Breaking Changes
|
|
245
279
|
|
|
246
|
-
|
|
247
|
-
const Automator = require('jw-automator');
|
|
248
|
-
const automator = new Automator({
|
|
249
|
-
storage: Automator.storage.file('./actions.json')
|
|
250
|
-
});
|
|
251
|
-
```
|
|
280
|
+
Thoroughly review the "Breaking Changes from v3 to v4" section above. Identify which changes affect your application.
|
|
252
281
|
|
|
253
|
-
### Step 2:
|
|
282
|
+
### Step 2: Review `catchUpWindow` Usage
|
|
254
283
|
|
|
255
|
-
|
|
284
|
+
If your v3 application relied on the implicit "unlimited" catch-up for actions where `catchUpWindow` was not explicitly set, you will need to:
|
|
256
285
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
automator.addAction({
|
|
260
|
-
cmd: 'myCmd',
|
|
261
|
-
date: new Date(),
|
|
262
|
-
repeat: { type: 'day', interval: 1 }
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// After
|
|
266
|
-
automator.addAction({
|
|
267
|
-
cmd: 'myCmd',
|
|
268
|
-
date: new Date(),
|
|
269
|
-
repeat: {
|
|
270
|
-
type: 'day',
|
|
271
|
-
interval: 1,
|
|
272
|
-
dstPolicy: 'once' // Explicitly choose DST behavior
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
```
|
|
286
|
+
- **Explicitly set `catchUpWindow: "unlimited"`** for those actions if you wish to maintain the old behavior.
|
|
287
|
+
- Otherwise, understand that these actions will now use the new smart defaults (recurrence interval for recurring, `0` for one-time actions).
|
|
276
288
|
|
|
277
289
|
### Step 3: Update Event Listeners
|
|
278
290
|
|
|
279
|
-
|
|
291
|
+
If your application was listening for `automator.on('error', ...)` to catch notifications about defensive coercions (e.g., invalid `repeat.interval`), you must now update your code to listen for `automator.on('warning', ...)` to receive these non-fatal messages.
|
|
280
292
|
|
|
281
|
-
|
|
282
|
-
// Before
|
|
283
|
-
automator.on('actionExecuted', (data) => { ... });
|
|
293
|
+
### Step 4: Correct Invalid `repeat.type` Definitions
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
automator.on('action', (event) => { ... });
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Step 4: Update API Calls
|
|
290
|
-
|
|
291
|
-
Use new method names:
|
|
292
|
-
|
|
293
|
-
```javascript
|
|
294
|
-
// Before
|
|
295
|
-
automator.removeAction(id);
|
|
296
|
-
|
|
297
|
-
// After
|
|
298
|
-
automator.removeActionByID(id);
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Step 5: Test DST Behavior
|
|
302
|
-
|
|
303
|
-
Review actions that run during DST transitions and set appropriate `dstPolicy`:
|
|
304
|
-
|
|
305
|
-
- `'once'` - Run only the first occurrence during fall-back (recommended default)
|
|
306
|
-
- `'twice'` - Run both occurrences during fall-back
|
|
307
|
-
|
|
308
|
-
### Step 6: Leverage New Features
|
|
309
|
-
|
|
310
|
-
Consider using:
|
|
311
|
-
- `getActionsInRange()` for calendar previews
|
|
312
|
-
- `describeAction()` for debugging
|
|
313
|
-
- Custom storage adapters for database persistence
|
|
314
|
-
- Update events for logging changes
|
|
295
|
+
If your application previously used actions with invalid or missing `repeat.type` values that were silently corrected in v3, you must now explicitly fix these `repeat.type` values. Failure to do so will result in a hard `Error` when the action is added or updated in v4.
|
|
315
296
|
|
|
316
297
|
---
|
|
317
298
|
|
|
318
299
|
## Compatibility Notes
|
|
319
300
|
|
|
320
|
-
### What's the Same
|
|
301
|
+
### What's the Same (v3 to v4)
|
|
321
302
|
|
|
322
|
-
-
|
|
323
|
-
-
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
303
|
+
- **Core concepts**: Schedule actions with recurrence
|
|
304
|
+
- **Recurrence types**: `second`, `minute`, `hour`, `day`, `week`, `month`, `year`
|
|
305
|
+
- **Local time**: Still operates in local time
|
|
306
|
+
- **API methods**: Names and signatures of `addAction()`, `updateActionByID()`, etc., remain the same.
|
|
307
|
+
- **Function registration**: Still use `addFunction()`
|
|
308
|
+
- **Legacy `unBuffered`**: Still supported as an alias, mapping to `catchUpWindow`.
|
|
327
309
|
|
|
328
|
-
### What's Different
|
|
310
|
+
### What's Different (v3 to v4)
|
|
329
311
|
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
-
|
|
333
|
-
-
|
|
334
|
-
- **API**: More consistent naming (`ByID`, `ByName`)
|
|
335
|
-
- **IDs**: Auto-generated, not user-provided
|
|
336
|
-
- **State**: Better separation of spec vs. state
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
## Example: Complete Migration
|
|
341
|
-
|
|
342
|
-
**v2 Code:**
|
|
343
|
-
```javascript
|
|
344
|
-
const automator = require('jw-automator');
|
|
345
|
-
automator.init({ file: './actions.json' });
|
|
346
|
-
|
|
347
|
-
automator.addFunction('turnLightOn', () => {
|
|
348
|
-
console.log('Light on');
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
automator.addAction({
|
|
352
|
-
cmd: 'turnLightOn',
|
|
353
|
-
date: new Date('2025-05-01T07:00:00'),
|
|
354
|
-
repeat: { type: 'day', interval: 1 }
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
automator.start();
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
**v3 Code:**
|
|
361
|
-
```javascript
|
|
362
|
-
const Automator = require('jw-automator');
|
|
363
|
-
|
|
364
|
-
const automator = new Automator({
|
|
365
|
-
storage: Automator.storage.file('./actions.json')
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
automator.addFunction('turnLightOn', () => {
|
|
369
|
-
console.log('Light on');
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
automator.addAction({
|
|
373
|
-
name: 'Morning Lights',
|
|
374
|
-
cmd: 'turnLightOn',
|
|
375
|
-
date: new Date('2025-05-01T07:00:00'),
|
|
376
|
-
unBuffered: false,
|
|
377
|
-
repeat: {
|
|
378
|
-
type: 'day',
|
|
379
|
-
interval: 1,
|
|
380
|
-
dstPolicy: 'once'
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
automator.start();
|
|
385
|
-
```
|
|
312
|
+
- **`catchUpWindow` Default Behavior**: Now smart and context-aware, instead of always `"unlimited"`.
|
|
313
|
+
- **`repeat.type` Validation**: Invalid types now throw a fatal `Error`.
|
|
314
|
+
- **Event Handling**: Coercions now emit `warning` events instead of `error` events.
|
|
315
|
+
- **Defensive Defaults Philosophy**: More refined and explicit.
|
|
386
316
|
|
|
387
317
|
---
|
|
388
318
|
|
|
389
319
|
## Getting Help
|
|
390
|
-
|
|
391
|
-
If you encounter issues during migration:
|
|
392
|
-
|
|
393
|
-
1. Check the [README](../README.md) for full API documentation
|
|
394
|
-
2. Review the [Architecture](./ARCHITECTURE.md) for design understanding
|
|
395
|
-
3. Run the examples in the `examples/` directory
|
|
396
|
-
4. File an issue on GitHub
|
|
397
|
-
|
|
398
|
-
---
|
|
399
|
-
|
|
400
|
-
## Why Rewrite?
|
|
401
|
-
|
|
402
|
-
v3 addresses several issues from v2:
|
|
403
|
-
|
|
404
|
-
- **Infinite loops**: Better safety guards
|
|
405
|
-
- **DST bugs**: Explicit, predictable handling
|
|
406
|
-
- **Catch-up logic**: More reliable offline behavior
|
|
407
|
-
- **Testability**: Deterministic core engine
|
|
408
|
-
- **Maintainability**: Cleaner architecture
|
|
409
|
-
- **Extensibility**: Pluggable storage, better API
|
|
410
|
-
|
|
411
|
-
The rewrite provides a solid foundation for long-term reliability.
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Quick Start Guide
|
|
2
2
|
|
|
3
|
-
Get up and running with jw-automator
|
|
3
|
+
Get up and running with jw-automator v4 in 5 minutes.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -137,36 +137,53 @@ automator.addAction({
|
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
140
|
-
##
|
|
140
|
+
## Offline Catch-Up with `catchUpWindow`
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Automator v4 manages offline catch-up using the `catchUpWindow` property, which has smart defaults for predictable behavior.
|
|
143
|
+
|
|
144
|
+
### Smart Defaults for `catchUpWindow`
|
|
145
|
+
|
|
146
|
+
- **Recurring Actions:** If `catchUpWindow` is not specified, it defaults to the **duration of the action's recurrence interval**. This ensures short delays are recovered, but long outages don't cause a "thundering herd."
|
|
147
|
+
- **One-Time Actions:** If `catchUpWindow` is not specified and the action has no `repeat` property, it defaults to **`0`** (skip if missed).
|
|
148
|
+
|
|
149
|
+
### Explicit Control
|
|
150
|
+
|
|
151
|
+
You can explicitly set `catchUpWindow`:
|
|
152
|
+
|
|
153
|
+
- `catchUpWindow: "unlimited"`: Catch up ALL missed executions.
|
|
154
|
+
- `catchUpWindow: 0`: Skip ALL missed executions (real-time only).
|
|
155
|
+
- `catchUpWindow: 5000`: Catch up if missed by ≤5 seconds, skip if older.
|
|
156
|
+
|
|
157
|
+
#### Example: Critical Task (unlimited catch-up)
|
|
143
158
|
|
|
144
159
|
```javascript
|
|
145
160
|
automator.addAction({
|
|
146
161
|
name: 'Critical Task',
|
|
147
162
|
cmd: 'criticalTask',
|
|
148
163
|
date: new Date('2025-05-01T10:00:00'),
|
|
149
|
-
|
|
164
|
+
catchUpWindow: "unlimited", // Execute all missed, even if offline for long
|
|
150
165
|
repeat: { type: 'hour', interval: 1 }
|
|
151
166
|
});
|
|
152
167
|
```
|
|
153
168
|
|
|
154
169
|
If the system is offline from 10:00 to 13:00, it will execute the 10:00, 11:00, and 12:00 occurrences when it comes back online.
|
|
155
170
|
|
|
156
|
-
|
|
171
|
+
#### Example: Animation Frame (skip missed)
|
|
157
172
|
|
|
158
173
|
```javascript
|
|
159
174
|
automator.addAction({
|
|
160
175
|
name: 'Animation Frame',
|
|
161
176
|
cmd: 'updateAnimation',
|
|
162
177
|
date: new Date(),
|
|
163
|
-
|
|
178
|
+
catchUpWindow: 0, // Only run if on time
|
|
164
179
|
repeat: { type: 'second', interval: 1 }
|
|
165
180
|
});
|
|
166
181
|
```
|
|
167
182
|
|
|
168
183
|
If the system is delayed, it won't execute missed animation frames.
|
|
169
184
|
|
|
185
|
+
**Legacy `unBuffered`**: The `unBuffered` property is still supported as a direct alias for `catchUpWindow` for backwards compatibility: `unBuffered: false` maps to `catchUpWindow: "unlimited"`, and `unBuffered: true` maps to `catchUpWindow: 0`.
|
|
186
|
+
|
|
170
187
|
---
|
|
171
188
|
|
|
172
189
|
## Simulation
|
|
@@ -249,6 +266,11 @@ automator.on('update', (event) => {
|
|
|
249
266
|
automator.on('error', (event) => {
|
|
250
267
|
console.error('Error:', event.message);
|
|
251
268
|
});
|
|
269
|
+
|
|
270
|
+
// Warnings (non-fatal coercions/corrections)
|
|
271
|
+
automator.on('warning', (event) => {
|
|
272
|
+
console.warn('Warning:', event.message);
|
|
273
|
+
});
|
|
252
274
|
```
|
|
253
275
|
|
|
254
276
|
---
|
|
@@ -349,7 +371,7 @@ process.on('SIGINT', () => {
|
|
|
349
371
|
- Read the full [README](../README.md)
|
|
350
372
|
- Check out [examples](../examples/)
|
|
351
373
|
- Review [Architecture](./ARCHITECTURE.md)
|
|
352
|
-
- See [Migration Guide](./MIGRATION.md) if upgrading from
|
|
374
|
+
- See [Migration Guide](./MIGRATION.md) if upgrading from v3
|
|
353
375
|
|
|
354
376
|
---
|
|
355
377
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Automator Defensive Defaults Strategy
|
|
2
|
+
|
|
3
|
+
The Automator's design philosophy for handling action specifications is **"Fail loudly, run defensively."**
|
|
4
|
+
|
|
5
|
+
The goal is to maximize system robustness. Rather than rejecting an action with minor errors or missing properties, the Automator will make reasonable, "defensive" assumptions to coerce the action into a valid, runnable state.
|
|
6
|
+
|
|
7
|
+
However, these corrections are never silent. Whenever a default is applied or a value is coerced due to invalid input, the Automator emits either a `warning` or `debug` event. This ensures that the developer is always aware of any assumptions the system has made on their behalf.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
### Property Default and Coercion Rules
|
|
12
|
+
|
|
13
|
+
The following rules are applied when an action is added via `addAction()` or updated via `updateAction...()`.
|
|
14
|
+
|
|
15
|
+
#### **`cmd`**
|
|
16
|
+
- **Rule:** An action without a command is not runnable.
|
|
17
|
+
- **Behavior:** Throws a hard `Error` if missing. This is the primary exception to the "run defensively" rule, as the action's intent cannot be determined.
|
|
18
|
+
|
|
19
|
+
#### **`date`** (Start Time)
|
|
20
|
+
- **Rule:** An action needs a starting time.
|
|
21
|
+
- **Default:** If `date` is not provided, it defaults to **5 seconds in the future** from the time it was added.
|
|
22
|
+
- **Event:** `debug`
|
|
23
|
+
|
|
24
|
+
#### **`catchUpWindow`** (Smart Default)
|
|
25
|
+
- **Philosophy:** The default catch-up behavior should match the user's likely intent for "server busy" vs. "server offline" scenarios. Explicit settings always take priority.
|
|
26
|
+
- **Priority Rules:**
|
|
27
|
+
1. **Explicit `catchUpWindow`:** If the property is present, it is used.
|
|
28
|
+
- `Infinity` is coerced to `"unlimited"`.
|
|
29
|
+
- Negative numbers are coerced to `0`.
|
|
30
|
+
- Invalid strings or other types are coerced to `"unlimited"`.
|
|
31
|
+
- A `warning` event is emitted for any coercion.
|
|
32
|
+
2. **Legacy `unBuffered`:** If `catchUpWindow` is absent but the legacy `unBuffered` property is present, it is mapped for backwards compatibility:
|
|
33
|
+
- `unBuffered: true` maps to `catchUpWindow: 0` (no catch-up).
|
|
34
|
+
- `unBuffered: false` maps to `catchUpWindow: "unlimited"`.
|
|
35
|
+
3. **Smart Default (Recurring):** If the action has a `repeat` property, `catchUpWindow` defaults to the **duration of the repeat interval** (e.g., an hourly action gets a 1-hour catch-up window).
|
|
36
|
+
4. **Smart Default (One-Time):** If the action does not have a `repeat` property, `catchUpWindow` defaults to **`0`** (no catch-up).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### Recurrence Rules (`repeat.*`)
|
|
41
|
+
|
|
42
|
+
#### **`repeat.type`**
|
|
43
|
+
- **Rule:** The recurrence `type` is fundamental to the action's behavior and must be a valid string (e.g., 'hour', 'day', 'week').
|
|
44
|
+
- **Behavior:** Throws a hard `Error` if missing or invalid. Unlike other properties, the ambiguity of an invalid type is considered a fatal error, as the user's intent cannot be safely determined.
|
|
45
|
+
|
|
46
|
+
#### **`repeat.interval`**
|
|
47
|
+
- **Rule:** The interval must be a positive integer.
|
|
48
|
+
- **Default:** If `undefined`, it is `1`. If defined but invalid (e.g., `0`, `-5`, `2.5`), it is coerced to a valid integer via `Math.max(1, Math.floor(value))`.
|
|
49
|
+
- **Event:** `warning` (if a value was changed).
|
|
50
|
+
|
|
51
|
+
#### **`repeat.limit`**
|
|
52
|
+
- **Rule:** The execution limit must be a number greater than or equal to 1.
|
|
53
|
+
- **Default:** If invalid (e.g., `0`, `-10`, `'foo'`), it is coerced to `null` (unlimited).
|
|
54
|
+
- **Event:** `warning`
|
|
55
|
+
|
|
56
|
+
#### **`repeat.endDate`**
|
|
57
|
+
- **Rule:** The end date must be a valid date representation.
|
|
58
|
+
-
|
|
59
|
+
- **Default:** If invalid, it is coerced to `null` (no end date).
|
|
60
|
+
- **Event:** `warning`
|
|
61
|
+
|
|
62
|
+
#### **`repeat.dstPolicy`**
|
|
63
|
+
- **Rule:** The DST "fall back" policy must be either `'once'` or `'twice'`.
|
|
64
|
+
- **Default:** If missing or invalid, it is coerced to `'once'`.
|
|
65
|
+
- **Event:** `warning` (if an invalid value was provided).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jw-automator",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A resilient, local-time, 1-second precision automation scheduler for Node.js",
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "A resilient, local-time, 1-second precision automation scheduler for Node.js with smart defensive defaults and clear error handling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"src/",
|
package/src/Automator.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Automator.js
|
|
3
3
|
*
|
|
4
|
-
* Main API class for jw-automator
|
|
4
|
+
* Main API class for jw-automator v4
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const SchedulerHost = require('./host/SchedulerHost');
|
|
@@ -135,7 +135,6 @@ class Automator {
|
|
|
135
135
|
cmd: actionSpec.cmd,
|
|
136
136
|
payload: actionSpec.payload !== undefined ? actionSpec.payload : null,
|
|
137
137
|
date: startDate,
|
|
138
|
-
unBuffered: actionSpec.unBuffered !== undefined ? actionSpec.unBuffered : false,
|
|
139
138
|
catchUpWindow: catchUpWindow,
|
|
140
139
|
repeat: actionSpec.repeat ? { ...actionSpec.repeat } : null,
|
|
141
140
|
count: 0
|
|
@@ -192,10 +191,8 @@ class Automator {
|
|
|
192
191
|
|
|
193
192
|
// Re-normalize catchUpWindow if unBuffered or catchUpWindow was updated
|
|
194
193
|
if ('unBuffered' in updates || 'catchUpWindow' in updates) {
|
|
195
|
-
action.catchUpWindow = this._normalizeCatchUpWindow(
|
|
196
|
-
|
|
197
|
-
unBuffered: action.unBuffered
|
|
198
|
-
});
|
|
194
|
+
action.catchUpWindow = this._normalizeCatchUpWindow(action);
|
|
195
|
+
delete action.unBuffered; // Remove legacy property
|
|
199
196
|
}
|
|
200
197
|
|
|
201
198
|
this._emit('update', {
|
|
@@ -244,10 +241,8 @@ class Automator {
|
|
|
244
241
|
|
|
245
242
|
// Re-normalize catchUpWindow if unBuffered or catchUpWindow was updated
|
|
246
243
|
if ('unBuffered' in updates || 'catchUpWindow' in updates) {
|
|
247
|
-
action.catchUpWindow = this._normalizeCatchUpWindow(
|
|
248
|
-
|
|
249
|
-
unBuffered: action.unBuffered
|
|
250
|
-
});
|
|
244
|
+
action.catchUpWindow = this._normalizeCatchUpWindow(action);
|
|
245
|
+
delete action.unBuffered; // Remove legacy property
|
|
251
246
|
}
|
|
252
247
|
|
|
253
248
|
this._emit('update', {
|
|
@@ -409,7 +404,7 @@ class Automator {
|
|
|
409
404
|
description += `\n Command: ${action.cmd}`;
|
|
410
405
|
description += `\n Next run: ${action.date ? action.date.toLocaleString() : 'None'}`;
|
|
411
406
|
description += `\n Executions: ${action.count}`;
|
|
412
|
-
description += `\n
|
|
407
|
+
description += `\n Catch-up Window: ${action.catchUpWindow === 'unlimited' ? 'unlimited' : `${action.catchUpWindow}ms`}`;
|
|
413
408
|
|
|
414
409
|
if (action.repeat) {
|
|
415
410
|
description += `\n Recurrence: ${action.repeat.type}`;
|
|
@@ -472,14 +467,15 @@ class Automator {
|
|
|
472
467
|
try {
|
|
473
468
|
const state = this.options.storage.load();
|
|
474
469
|
|
|
475
|
-
// Normalize catchUpWindow for existing actions (for backwards compatibility)
|
|
476
470
|
if (state.actions && state.actions.length > 0) {
|
|
477
|
-
state.actions
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
471
|
+
state.actions.forEach(action => {
|
|
472
|
+
// Set catchUpWindow if missing, using legacy unBuffered if present
|
|
473
|
+
if (action.catchUpWindow === undefined) {
|
|
474
|
+
action.catchUpWindow = this._normalizeCatchUpWindow(action);
|
|
475
|
+
}
|
|
476
|
+
// Remove legacy property after normalization
|
|
477
|
+
delete action.unBuffered;
|
|
478
|
+
});
|
|
483
479
|
|
|
484
480
|
const maxId = Math.max(...state.actions.map(a => a.id || 0));
|
|
485
481
|
this.nextId = maxId + 1;
|
|
@@ -524,6 +520,43 @@ class Automator {
|
|
|
524
520
|
}, this.options.saveInterval);
|
|
525
521
|
}
|
|
526
522
|
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Calculate interval in milliseconds from a repeat spec.
|
|
526
|
+
* Note: Uses approximations for month/year, as this is for a default
|
|
527
|
+
* catch-up window, not for precise scheduling.
|
|
528
|
+
*
|
|
529
|
+
* @param {object} repeat
|
|
530
|
+
* @returns {number}
|
|
531
|
+
* @private
|
|
532
|
+
*/
|
|
533
|
+
_getIntervalMilliseconds(repeat) {
|
|
534
|
+
const interval = repeat.interval || 1;
|
|
535
|
+
switch (repeat.type) {
|
|
536
|
+
case 'second':
|
|
537
|
+
return interval * 1000;
|
|
538
|
+
case 'minute':
|
|
539
|
+
return interval * 60 * 1000;
|
|
540
|
+
case 'hour':
|
|
541
|
+
return interval * 60 * 60 * 1000;
|
|
542
|
+
case 'day':
|
|
543
|
+
case 'weekday': // Approximated as 1 day for catch-up purposes
|
|
544
|
+
case 'weekend': // Approximated as 1 day for catch-up purposes
|
|
545
|
+
return interval * 24 * 60 * 60 * 1000;
|
|
546
|
+
case 'week':
|
|
547
|
+
return interval * 7 * 24 * 60 * 60 * 1000;
|
|
548
|
+
case 'month':
|
|
549
|
+
// A reasonable approximation for a default is 30 days
|
|
550
|
+
return interval * 30 * 24 * 60 * 60 * 1000;
|
|
551
|
+
case 'year':
|
|
552
|
+
// A reasonable approximation for a default is 365 days
|
|
553
|
+
return interval * 365 * 24 * 60 * 60 * 1000;
|
|
554
|
+
default:
|
|
555
|
+
// Fallback for an invalid type that slipped past validation
|
|
556
|
+
return 60000; // 1 minute
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
527
560
|
/**
|
|
528
561
|
* Normalize catchUpWindow property (handles backwards compatibility with unBuffered)
|
|
529
562
|
*
|
|
@@ -536,7 +569,7 @@ class Automator {
|
|
|
536
569
|
* @returns {string|number} - Normalized catchUpWindow value ("unlimited" or milliseconds)
|
|
537
570
|
*/
|
|
538
571
|
_normalizeCatchUpWindow(spec) {
|
|
539
|
-
// New property takes precedence
|
|
572
|
+
// 1. New property takes precedence
|
|
540
573
|
if (spec.catchUpWindow !== undefined) {
|
|
541
574
|
// Coerce Infinity to "unlimited" (backwards compatibility)
|
|
542
575
|
if (spec.catchUpWindow === Infinity) {
|
|
@@ -549,13 +582,18 @@ class Automator {
|
|
|
549
582
|
return spec.catchUpWindow;
|
|
550
583
|
}
|
|
551
584
|
|
|
552
|
-
// Backwards compatibility mapping
|
|
585
|
+
// 2. Backwards compatibility mapping for legacy 'unBuffered'
|
|
553
586
|
if (spec.unBuffered !== undefined) {
|
|
554
587
|
return spec.unBuffered ? 0 : "unlimited";
|
|
555
588
|
}
|
|
556
589
|
|
|
557
|
-
//
|
|
558
|
-
|
|
590
|
+
// 3. For recurring actions, default to the interval duration
|
|
591
|
+
if (spec.repeat) {
|
|
592
|
+
return this._getIntervalMilliseconds(spec.repeat);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 4. For one-time actions, default to 0 (no catch-up)
|
|
596
|
+
return 0;
|
|
559
597
|
}
|
|
560
598
|
|
|
561
599
|
/**
|
|
@@ -573,14 +611,9 @@ class Automator {
|
|
|
573
611
|
if (action.repeat) {
|
|
574
612
|
const validTypes = ['second', 'minute', 'hour', 'day', 'weekday', 'weekend', 'week', 'month', 'year'];
|
|
575
613
|
|
|
576
|
-
//
|
|
614
|
+
// CRITICAL: An invalid repeat.type is a fatal error, as intent is lost.
|
|
577
615
|
if (!action.repeat.type || !validTypes.includes(action.repeat.type)) {
|
|
578
|
-
|
|
579
|
-
type: 'error',
|
|
580
|
-
message: `Invalid repeat.type "${action.repeat.type}" - defaulting to "day"`,
|
|
581
|
-
actionSpec: action
|
|
582
|
-
});
|
|
583
|
-
action.repeat.type = 'day';
|
|
616
|
+
throw new Error(`Invalid repeat.type "${action.repeat.type}". Must be one of: ${validTypes.join(', ')}`);
|
|
584
617
|
}
|
|
585
618
|
|
|
586
619
|
// Defensive: Coerce invalid interval to Math.max(1, Math.floor(value))
|
|
@@ -588,8 +621,8 @@ class Automator {
|
|
|
588
621
|
const original = action.repeat.interval;
|
|
589
622
|
const coerced = Math.max(1, Math.floor(original));
|
|
590
623
|
if (original !== coerced) {
|
|
591
|
-
this._emit('
|
|
592
|
-
type: '
|
|
624
|
+
this._emit('warning', {
|
|
625
|
+
type: 'warning',
|
|
593
626
|
message: `Invalid repeat.interval ${original} - coerced to ${coerced}`,
|
|
594
627
|
actionSpec: action
|
|
595
628
|
});
|
|
@@ -599,19 +632,19 @@ class Automator {
|
|
|
599
632
|
|
|
600
633
|
// Defensive: Validate dstPolicy
|
|
601
634
|
if (action.repeat.dstPolicy && !['once', 'twice'].includes(action.repeat.dstPolicy)) {
|
|
602
|
-
this._emit('
|
|
603
|
-
type: '
|
|
635
|
+
this._emit('warning', {
|
|
636
|
+
type: 'warning',
|
|
604
637
|
message: `Invalid dstPolicy "${action.repeat.dstPolicy}" - defaulting to "once"`,
|
|
605
638
|
actionSpec: action
|
|
606
639
|
});
|
|
607
640
|
action.repeat.dstPolicy = 'once';
|
|
608
641
|
}
|
|
609
642
|
|
|
610
|
-
// Defensive: Coerce invalid repeat.limit to null (unlimited) with
|
|
643
|
+
// Defensive: Coerce invalid repeat.limit to null (unlimited) with WARNING event
|
|
611
644
|
if (action.repeat.limit !== undefined && action.repeat.limit !== null) {
|
|
612
645
|
if (typeof action.repeat.limit !== 'number' || action.repeat.limit < 1) {
|
|
613
|
-
this._emit('
|
|
614
|
-
type: '
|
|
646
|
+
this._emit('warning', {
|
|
647
|
+
type: 'warning',
|
|
615
648
|
message: `Invalid repeat.limit ${action.repeat.limit} - defaulting to null (unlimited)`,
|
|
616
649
|
actionSpec: action
|
|
617
650
|
});
|
|
@@ -624,8 +657,8 @@ class Automator {
|
|
|
624
657
|
try {
|
|
625
658
|
new Date(action.repeat.endDate);
|
|
626
659
|
} catch (e) {
|
|
627
|
-
this._emit('
|
|
628
|
-
type: '
|
|
660
|
+
this._emit('warning', {
|
|
661
|
+
type: 'warning',
|
|
629
662
|
message: `Invalid repeat.endDate - ignoring`,
|
|
630
663
|
actionSpec: action
|
|
631
664
|
});
|
|
@@ -642,8 +675,8 @@ class Automator {
|
|
|
642
675
|
|
|
643
676
|
// Coerce negative numbers to 0 FIRST
|
|
644
677
|
if (isNumber && action.catchUpWindow < 0) {
|
|
645
|
-
this._emit('
|
|
646
|
-
type: '
|
|
678
|
+
this._emit('warning', {
|
|
679
|
+
type: 'warning',
|
|
647
680
|
message: `Negative catchUpWindow ${action.catchUpWindow} - coerced to 0`,
|
|
648
681
|
actionSpec: action
|
|
649
682
|
});
|
|
@@ -651,8 +684,8 @@ class Automator {
|
|
|
651
684
|
}
|
|
652
685
|
// Then validate it's a valid value
|
|
653
686
|
else if (!isValidString && !isNumber && !isInfinity) {
|
|
654
|
-
this._emit('
|
|
655
|
-
type: '
|
|
687
|
+
this._emit('warning', {
|
|
688
|
+
type: 'warning',
|
|
656
689
|
message: `Invalid catchUpWindow "${action.catchUpWindow}" - defaulting to "unlimited"`,
|
|
657
690
|
actionSpec: action
|
|
658
691
|
});
|