jw-automator 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 v3**
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 Time Windows**
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
- ```js
147
- catchUpWindow: "unlimited" // Catch up ALL missed executions (default)
148
- catchUpWindow: 0 // Skip ALL missed executions (real-time only)
149
- catchUpWindow: 5000 // Catch up if missed by ≤5 seconds, skip if older
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
- * If an action is missed by less than `catchUpWindow` milliseconds, it executes (recovers from brief glitches)
155
- * If missed by more than `catchUpWindow`, it's skipped and fast-forwarded (prevents thundering herd)
156
- * The fast-forward optimization uses mathematical projection to instantly advance high-frequency tasks
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
- **Example scenarios:**
163
+ **Events for Coercion & Validation:**
159
164
 
160
- * **Billing tasks:** `catchUpWindow: "unlimited"` - Never miss a charge
161
- * **Real-time alerts:** `catchUpWindow: 0` - Only relevant "now"
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 fully supported:
167
- ```js
168
- unBuffered: false // equivalent to catchUpWindow: "unlimited"
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: `"unlimited"`, or milliseconds number) |
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 (v3)
477
+ ## 🎯 Project Goals (v4)
476
478
 
477
479
  * Deterministic behavior
478
480
  * Rock-solid DST handling
@@ -1,6 +1,6 @@
1
- # Architecture Overview
1
+ # Architecture Overview (v4)
2
2
 
3
- ## jw-automator v3 Architecture
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
- - **Buffered**: "I want this to happen even if delayed" (e.g., turn heater off)
250
- - **UnBuffered**: "Only if on-time" (e.g., animations, rate limiting)
251
- - Both advance the recurrence chain correctly
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: v2 to v3
1
+ # Migration Guide: v3 to v4
2
2
 
3
- This guide helps you migrate from jw-automator v2 to v3.
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 v3 is a **complete clean-room rewrite** with improved semantics, better DST handling, and a cleaner API. While the core concepts remain the same, there are breaking changes from v2.
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
- v2 was the widely-deployed production version. v3 represents a ground-up reimplementation that preserves the philosophy while modernizing the architecture.
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
- ```javascript
242
- // Before
243
- const automator = require('jw-automator');
244
- automator.init({ file: './actions.json' });
278
+ ### Step 1: Understand Breaking Changes
245
279
 
246
- // After
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: Update Action Definitions
282
+ ### Step 2: Review `catchUpWindow` Usage
254
283
 
255
- Add `dstPolicy` to actions with repeat:
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
- ```javascript
258
- // Before
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
- Standardize event names:
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
- ```javascript
282
- // Before
283
- automator.on('actionExecuted', (data) => { ... });
293
+ ### Step 4: Correct Invalid `repeat.type` Definitions
284
294
 
285
- // After
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
- - **Core concept**: Schedule actions with recurrence
323
- - **Recurrence types**: second, minute, hour, day, week, month, year
324
- - **Local time**: Still operates in local time
325
- - **Buffered/unbuffered**: Still supported (as `unBuffered` flag)
326
- - **Function registration**: Still use `addFunction()`
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
- - **Constructor**: Now uses `new Automator()`
331
- - **Storage**: Explicitly configured
332
- - **DST**: Explicit policy required
333
- - **Events**: Standardized names and payloads
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.
@@ -1,6 +1,6 @@
1
1
  # Quick Start Guide
2
2
 
3
- Get up and running with jw-automator v3 in 5 minutes.
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
- ## Buffered vs UnBuffered
140
+ ## Offline Catch-Up with `catchUpWindow`
141
141
 
142
- ### Buffered (Default) - Catch Up Missed Events
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
- unBuffered: false, // Execute even if delayed
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
- ### UnBuffered - Skip Missed Events
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
- unBuffered: true, // Only run if on time
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 v1
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": "3.2.0",
4
- "description": "A resilient, local-time, 1-second precision automation scheduler for Node.js",
3
+ "version": "4.0.0",
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
@@ -192,10 +192,7 @@ class Automator {
192
192
 
193
193
  // Re-normalize catchUpWindow if unBuffered or catchUpWindow was updated
194
194
  if ('unBuffered' in updates || 'catchUpWindow' in updates) {
195
- action.catchUpWindow = this._normalizeCatchUpWindow({
196
- catchUpWindow: action.catchUpWindow,
197
- unBuffered: action.unBuffered
198
- });
195
+ action.catchUpWindow = this._normalizeCatchUpWindow(action);
199
196
  }
200
197
 
201
198
  this._emit('update', {
@@ -244,10 +241,7 @@ 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
- catchUpWindow: action.catchUpWindow,
249
- unBuffered: action.unBuffered
250
- });
244
+ action.catchUpWindow = this._normalizeCatchUpWindow(action);
251
245
  }
252
246
 
253
247
  this._emit('update', {
@@ -524,6 +518,43 @@ class Automator {
524
518
  }, this.options.saveInterval);
525
519
  }
526
520
 
521
+
522
+ /**
523
+ * Calculate interval in milliseconds from a repeat spec.
524
+ * Note: Uses approximations for month/year, as this is for a default
525
+ * catch-up window, not for precise scheduling.
526
+ *
527
+ * @param {object} repeat
528
+ * @returns {number}
529
+ * @private
530
+ */
531
+ _getIntervalMilliseconds(repeat) {
532
+ const interval = repeat.interval || 1;
533
+ switch (repeat.type) {
534
+ case 'second':
535
+ return interval * 1000;
536
+ case 'minute':
537
+ return interval * 60 * 1000;
538
+ case 'hour':
539
+ return interval * 60 * 60 * 1000;
540
+ case 'day':
541
+ case 'weekday': // Approximated as 1 day for catch-up purposes
542
+ case 'weekend': // Approximated as 1 day for catch-up purposes
543
+ return interval * 24 * 60 * 60 * 1000;
544
+ case 'week':
545
+ return interval * 7 * 24 * 60 * 60 * 1000;
546
+ case 'month':
547
+ // A reasonable approximation for a default is 30 days
548
+ return interval * 30 * 24 * 60 * 60 * 1000;
549
+ case 'year':
550
+ // A reasonable approximation for a default is 365 days
551
+ return interval * 365 * 24 * 60 * 60 * 1000;
552
+ default:
553
+ // Fallback for an invalid type that slipped past validation
554
+ return 60000; // 1 minute
555
+ }
556
+ }
557
+
527
558
  /**
528
559
  * Normalize catchUpWindow property (handles backwards compatibility with unBuffered)
529
560
  *
@@ -536,7 +567,7 @@ class Automator {
536
567
  * @returns {string|number} - Normalized catchUpWindow value ("unlimited" or milliseconds)
537
568
  */
538
569
  _normalizeCatchUpWindow(spec) {
539
- // New property takes precedence
570
+ // 1. New property takes precedence
540
571
  if (spec.catchUpWindow !== undefined) {
541
572
  // Coerce Infinity to "unlimited" (backwards compatibility)
542
573
  if (spec.catchUpWindow === Infinity) {
@@ -549,13 +580,18 @@ class Automator {
549
580
  return spec.catchUpWindow;
550
581
  }
551
582
 
552
- // Backwards compatibility mapping
583
+ // 2. Backwards compatibility mapping for legacy 'unBuffered'
553
584
  if (spec.unBuffered !== undefined) {
554
585
  return spec.unBuffered ? 0 : "unlimited";
555
586
  }
556
587
 
557
- // Default: catch up everything (current buffered behavior)
558
- return "unlimited";
588
+ // 3. For recurring actions, default to the interval duration
589
+ if (spec.repeat) {
590
+ return this._getIntervalMilliseconds(spec.repeat);
591
+ }
592
+
593
+ // 4. For one-time actions, default to 0 (no catch-up)
594
+ return 0;
559
595
  }
560
596
 
561
597
  /**
@@ -573,14 +609,9 @@ class Automator {
573
609
  if (action.repeat) {
574
610
  const validTypes = ['second', 'minute', 'hour', 'day', 'weekday', 'weekend', 'week', 'month', 'year'];
575
611
 
576
- // Defensive: Coerce invalid repeat.type to 'day' with ERROR event
612
+ // CRITICAL: An invalid repeat.type is a fatal error, as intent is lost.
577
613
  if (!action.repeat.type || !validTypes.includes(action.repeat.type)) {
578
- this._emit('error', {
579
- type: 'error',
580
- message: `Invalid repeat.type "${action.repeat.type}" - defaulting to "day"`,
581
- actionSpec: action
582
- });
583
- action.repeat.type = 'day';
614
+ throw new Error(`Invalid repeat.type "${action.repeat.type}". Must be one of: ${validTypes.join(', ')}`);
584
615
  }
585
616
 
586
617
  // Defensive: Coerce invalid interval to Math.max(1, Math.floor(value))
@@ -588,8 +619,8 @@ class Automator {
588
619
  const original = action.repeat.interval;
589
620
  const coerced = Math.max(1, Math.floor(original));
590
621
  if (original !== coerced) {
591
- this._emit('error', {
592
- type: 'error',
622
+ this._emit('warning', {
623
+ type: 'warning',
593
624
  message: `Invalid repeat.interval ${original} - coerced to ${coerced}`,
594
625
  actionSpec: action
595
626
  });
@@ -599,19 +630,19 @@ class Automator {
599
630
 
600
631
  // Defensive: Validate dstPolicy
601
632
  if (action.repeat.dstPolicy && !['once', 'twice'].includes(action.repeat.dstPolicy)) {
602
- this._emit('error', {
603
- type: 'error',
633
+ this._emit('warning', {
634
+ type: 'warning',
604
635
  message: `Invalid dstPolicy "${action.repeat.dstPolicy}" - defaulting to "once"`,
605
636
  actionSpec: action
606
637
  });
607
638
  action.repeat.dstPolicy = 'once';
608
639
  }
609
640
 
610
- // Defensive: Coerce invalid repeat.limit to null (unlimited) with ERROR event
641
+ // Defensive: Coerce invalid repeat.limit to null (unlimited) with WARNING event
611
642
  if (action.repeat.limit !== undefined && action.repeat.limit !== null) {
612
643
  if (typeof action.repeat.limit !== 'number' || action.repeat.limit < 1) {
613
- this._emit('error', {
614
- type: 'error',
644
+ this._emit('warning', {
645
+ type: 'warning',
615
646
  message: `Invalid repeat.limit ${action.repeat.limit} - defaulting to null (unlimited)`,
616
647
  actionSpec: action
617
648
  });
@@ -624,8 +655,8 @@ class Automator {
624
655
  try {
625
656
  new Date(action.repeat.endDate);
626
657
  } catch (e) {
627
- this._emit('error', {
628
- type: 'error',
658
+ this._emit('warning', {
659
+ type: 'warning',
629
660
  message: `Invalid repeat.endDate - ignoring`,
630
661
  actionSpec: action
631
662
  });
@@ -642,8 +673,8 @@ class Automator {
642
673
 
643
674
  // Coerce negative numbers to 0 FIRST
644
675
  if (isNumber && action.catchUpWindow < 0) {
645
- this._emit('error', {
646
- type: 'error',
676
+ this._emit('warning', {
677
+ type: 'warning',
647
678
  message: `Negative catchUpWindow ${action.catchUpWindow} - coerced to 0`,
648
679
  actionSpec: action
649
680
  });
@@ -651,8 +682,8 @@ class Automator {
651
682
  }
652
683
  // Then validate it's a valid value
653
684
  else if (!isValidString && !isNumber && !isInfinity) {
654
- this._emit('error', {
655
- type: 'error',
685
+ this._emit('warning', {
686
+ type: 'warning',
656
687
  message: `Invalid catchUpWindow "${action.catchUpWindow}" - defaulting to "unlimited"`,
657
688
  actionSpec: action
658
689
  });