jw-automator 4.0.1 โ†’ 6.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ๐Ÿ“š **jw-automator v4**
1
+ # **jw-automator v6**
2
2
 
3
3
  ### A resilient, local-time, 1-second precision automation scheduler for Node.js
4
4
 
@@ -6,9 +6,9 @@
6
6
 
7
7
  ---
8
8
 
9
- ## โญ๏ธ Overview
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. Version 4 introduces enhanced defensive defaults and clearer error handling, making it even more predictable and robust.
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 6 introduces result-based error handling with structured error codes, making it ideal for web interfaces and ensuring the scheduler never crashes your application.
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
 
@@ -18,7 +18,7 @@ Where traditional cron falls short โ€” missed executions, poor DST handling, lim
18
18
  * **Offline resiliency & catch-up logic**
19
19
  * **Buffered/unBuffered execution policies**
20
20
  * **Rich introspection and event lifecycle**
21
- * **Meta-actions** that can dynamically create/update other actions
21
+ * **Meta-tasks** that can dynamically create/update other tasks
22
22
  * **Pluggable persistence** (file, memory, custom storage)
23
23
  * **Deterministic step engine** suitable for simulation/testing
24
24
 
@@ -35,7 +35,7 @@ jw-automator v3 is a **clean-room re-architecture** of the original library, kee
35
35
 
36
36
  ---
37
37
 
38
- ## ๐Ÿš€ Quick Start
38
+ ## Quick Start
39
39
 
40
40
  ### Installation
41
41
 
@@ -50,7 +50,7 @@ const Automator = require('jw-automator');
50
50
 
51
51
  // Create an automator with file-based persistence
52
52
  const automator = new Automator({
53
- storage: Automator.storage.file('./actions.json')
53
+ storage: Automator.storage.file('./tasks.json')
54
54
  });
55
55
 
56
56
  // Register a command function
@@ -58,14 +58,14 @@ automator.addFunction('turnLightOn', function(payload) {
58
58
  console.log('Turning light on');
59
59
  });
60
60
 
61
- // Seed initial actions (runs only on first use)
61
+ // Seed initial tasks (runs only on first use)
62
62
  automator.seed((auto) => {
63
- auto.addAction({
63
+ auto.addTask({
64
64
  name: 'Morning Lights',
65
65
  cmd: 'turnLightOn',
66
66
  date: new Date('2025-05-01T07:00:00'),
67
67
  payload: null,
68
- catchUpWindow: 60000, // Tolerate 1 minute of lag
68
+ catchUpMode: 'default', // Use default catch-up behavior
69
69
  repeat: {
70
70
  type: 'day',
71
71
  interval: 1,
@@ -82,7 +82,7 @@ automator.start();
82
82
 
83
83
  ---
84
84
 
85
- ## ๐Ÿ”ฅ Features
85
+ ## Features
86
86
 
87
87
  ### 1. **True 1-Second Precision**
88
88
 
@@ -98,7 +98,7 @@ automator.start();
98
98
 
99
99
  ### 2. **Human-Friendly Recurrence Rules**
100
100
 
101
- Each action can specify a recurrence like:
101
+ Each task can specify a recurrence like:
102
102
 
103
103
  ```js
104
104
  repeat: {
@@ -139,31 +139,38 @@ This avoids cron's silent-but-surprising behaviors.
139
139
 
140
140
  ---
141
141
 
142
- ### 4. **Resilient Offline Catch-Up with Smart Defaults**
142
+ ### 4. **Resilient Catch-Up with `catchUpMode`**
143
143
 
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.
144
+ By default, `jw-automator` is resilient to minor event loop delays and jitter. This is managed through a simple `catchUpMode` property on each task, which makes behavior predictable without needing to configure complex settings.
145
145
 
146
- **`catchUpWindow` Behavior:**
146
+ **`catchUpMode` (The Easy Way)**
147
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.
148
+ This is the recommended way to control catch-up behavior.
156
149
 
157
- **How it works:**
150
+ * `catchUpMode: 'default'` (System-wide default)
151
+ * **Behavior:** Provides a small buffer for tasks to recover from brief delays. If a task is missed by a few moments, it will run. If it's missed by a long time (e.g., the system was off), it will be skipped.
152
+ * **Implementation:** Sets `catchUpWindow: 500` (milliseconds) and `catchUpLimit: 1`.
153
+ * **Use Case:** The best setting for most tasks. It prevents tasks from being skipped due to normal system fluctuations.
158
154
 
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.
155
+ * `catchUpMode: 'realtime'`
156
+ * **Behavior:** The task will only run if the scheduler ticks at its exact scheduled second. If the event loop is busy and the moment is missed, the task is skipped.
157
+ * **Implementation:** Sets `catchUpWindow: 0` and `catchUpLimit: 0`.
158
+ * **Use Case:** For tasks where executing late is worse than not executing at all.
162
159
 
163
- **Events for Coercion & Validation:**
160
+ You can also set a system-wide default in the constructor:
161
+ ```js
162
+ const automator = new Automator({
163
+ defaultCatchUpMode: 'realtime' // Make all tasks realtime by default
164
+ });
165
+ ```
164
166
 
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.
167
+ **`catchUpWindow` (The Advanced Way)**
168
+
169
+ For more advanced control, you can bypass `catchUpMode` and set `catchUpWindow` directly. An explicit `catchUpWindow` value on a task will always take precedence.
170
+
171
+ * `catchUpWindow: 0`: Skip ALL missed executions (same as `catchUpMode: 'realtime'`).
172
+ * `catchUpWindow: 5000`: Catch up if missed by โ‰ค5 seconds, skip if older.
173
+ * `catchUpWindow: "unlimited"`: Catch up ALL missed executions.
167
174
 
168
175
  **Backwards compatibility:**
169
176
 
@@ -187,7 +194,7 @@ This powers:
187
194
  * Offline catch-up
188
195
  * Future schedule simulation
189
196
  * Testing
190
- * Meta-scheduling (actions that schedule other actions)
197
+ * Meta-scheduling (tasks that schedule other tasks)
191
198
 
192
199
  Because `step` is deterministic, you can:
193
200
 
@@ -198,21 +205,21 @@ Because `step` is deterministic, you can:
198
205
 
199
206
  ---
200
207
 
201
- ### 6. **Meta-Actions (Actions that Create Actions)**
208
+ ### 6. **Meta-Tasks (Tasks that Create Tasks)**
202
209
 
203
- jw-automator treats actions as **data**, enabling higher-order patterns:
210
+ jw-automator treats tasks as **data**, enabling higher-order patterns:
204
211
 
205
- * A daily 7:00 AM action can spawn a sequence of 60 one-per-second actions.
206
- * A monthly billing action can create daily reminder actions.
207
- * A multi-step animation (e.g., dimming a light) can create timed sub-actions.
212
+ * A daily 7:00 AM task can spawn a sequence of 60 one-per-second tasks.
213
+ * A monthly billing task can create daily reminder tasks.
214
+ * A multi-step animation (e.g., dimming a light) can create timed sub-tasks.
208
215
 
209
- Actions have a `repeat.count` that can be pre-set or manipulated intentionally.
216
+ Tasks have a `repeat.count` that can be pre-set or manipulated intentionally.
210
217
 
211
218
  This makes jw-automator more like a *mini automation runtime* than just a cron clone.
212
219
 
213
220
  ---
214
221
 
215
- ## ๐Ÿ“ API Reference
222
+ ## API Reference
216
223
 
217
224
  ### Constructor
218
225
 
@@ -224,17 +231,18 @@ Options:
224
231
  * `storage` - Storage adapter (default: memory)
225
232
  * `autoSave` - Auto-save state (default: true)
226
233
  * `saveInterval` - Save interval in ms (default: 5000)
234
+ * `defaultCatchUpMode` - The default catch-up behavior for all tasks (`'default'` or `'realtime'`). Defaults to `'default'`.
227
235
 
228
236
  ### Methods
229
237
 
230
238
  #### `seed(callback)`
231
- Seed the automator with initial actions. Runs only when the database is empty (first use).
239
+ Seed the automator with initial tasks. Runs only when the database is empty (first use).
232
240
 
233
- **Returns:** `boolean` - `true` if seeding ran, `false` if skipped
241
+ **Returns:** Result object with `{ success: true, seeded: boolean }` or `{ success: false, error: string, code: string }`
234
242
 
235
243
  ```js
236
- automator.seed((auto) => {
237
- auto.addAction({
244
+ const result = automator.seed((auto) => {
245
+ auto.addTask({
238
246
  name: 'Daily Report',
239
247
  cmd: 'generateReport',
240
248
  date: new Date('2025-01-01T09:00:00'),
@@ -242,11 +250,19 @@ automator.seed((auto) => {
242
250
  repeat: { type: 'day', interval: 1 }
243
251
  });
244
252
  });
253
+
254
+ if (result.success && result.seeded) {
255
+ console.log('Database seeded successfully');
256
+ } else if (result.success) {
257
+ console.log('Database already populated - seeding skipped');
258
+ } else {
259
+ console.error('Seed failed:', result.error);
260
+ }
245
261
  ```
246
262
 
247
263
  **Why use seed()?**
248
264
 
249
- * Solves the bootstrapping problem: safely initialize actions without resetting the schedule on every restart
265
+ * Solves the bootstrapping problem: safely initialize tasks without resetting the schedule on every restart
250
266
  * Preserves user-modified schedules perfectly
251
267
  * Runs initialization logic only once in the application lifecycle
252
268
  * Automatically saves state after seeding
@@ -266,16 +282,18 @@ automator.addFunction('myCommand', function(payload, event) {
266
282
  });
267
283
  ```
268
284
 
269
- #### `addAction(actionSpec)`
270
- Add a new action. Returns the action ID.
285
+ #### `addTask(taskSpec)`
286
+ Add a new task.
287
+
288
+ **Returns:** Result object with `{ success: true, id: number }` or `{ success: false, error: string, code: string }`
271
289
 
272
290
  ```js
273
- const id = automator.addAction({
274
- name: 'My Action',
291
+ const result = automator.addTask({
292
+ name: 'My Task',
275
293
  cmd: 'myCommand',
276
294
  date: new Date('2025-05-01T10:00:00'),
277
295
  payload: { data: 'value' },
278
- unBuffered: false,
296
+ catchUpMode: 'default',
279
297
  repeat: {
280
298
  type: 'hour',
281
299
  interval: 2,
@@ -283,47 +301,93 @@ const id = automator.addAction({
283
301
  dstPolicy: 'once'
284
302
  }
285
303
  });
304
+
305
+ if (result.success) {
306
+ console.log('Task added with ID:', result.id);
307
+ } else {
308
+ console.error('Failed to add task:', result.error);
309
+ }
286
310
  ```
287
311
 
288
- #### `updateActionByID(id, updates)`
289
- Update an existing action.
312
+ #### `updateTaskByID(id, updates)`
313
+ Update an existing task.
314
+
315
+ **Returns:** Result object with `{ success: true, id: number, task: object }` or `{ success: false, error: string, code: string }`
290
316
 
291
317
  ```js
292
- automator.updateActionByID(1, {
318
+ const result = automator.updateTaskByID(1, {
293
319
  name: 'Updated Name',
294
320
  repeat: { type: 'day', interval: 1 }
295
321
  });
322
+
323
+ if (result.success) {
324
+ console.log('Task updated:', result.id);
325
+ } else {
326
+ console.error('Failed to update task:', result.error);
327
+ }
296
328
  ```
297
329
 
298
- #### `updateActionByName(name, updates)`
299
- Update all actions with the given name. Returns the number of actions updated.
330
+ #### `updateTaskByName(name, updates)`
331
+ Update all tasks with the given name.
332
+
333
+ **Returns:** Result object with `{ success: true, count: number }` or `{ success: false, error: string, code: string }`
300
334
 
301
335
  ```js
302
- automator.updateActionByName('My Action', {
336
+ const result = automator.updateTaskByName('My Task', {
303
337
  payload: { newData: 'newValue' }
304
338
  });
339
+
340
+ if (result.success) {
341
+ console.log(`Updated ${result.count} task(s)`);
342
+ } else {
343
+ console.error('Failed to update tasks:', result.error);
344
+ }
305
345
  ```
306
346
 
307
- #### `removeActionByID(id)`
308
- Remove an action by ID.
347
+ #### `removeTaskByID(id)`
348
+ Remove a task by ID.
309
349
 
310
- #### `removeActionByName(name)`
311
- Remove all actions with the given name.
350
+ **Returns:** Result object with `{ success: true, id: number, task: object }` or `{ success: false, error: string, code: string }`
312
351
 
313
- #### `getActions()`
314
- Get all actions (deep copy).
352
+ ```js
353
+ const result = automator.removeTaskByID(1);
315
354
 
316
- #### `getActionsByName(name)`
317
- Get actions by name.
355
+ if (result.success) {
356
+ console.log('Task removed:', result.id);
357
+ } else {
358
+ console.error('Failed to remove task:', result.error);
359
+ }
360
+ ```
318
361
 
319
- #### `getActionByID(id)`
320
- Get a specific action by ID.
362
+ #### `removeTaskByName(name)`
363
+ Remove all tasks with the given name.
321
364
 
322
- #### `getActionsInRange(startDate, endDate, callback)`
323
- Simulate actions in a time range.
365
+ **Returns:** Result object with `{ success: true, count: number }` or `{ success: false, error: string, code: string }`
324
366
 
325
367
  ```js
326
- const events = automator.getActionsInRange(
368
+ const result = automator.removeTaskByName('My Task');
369
+
370
+ if (result.success) {
371
+ console.log(`Removed ${result.count} task(s)`);
372
+ } else {
373
+ console.error('Failed to remove tasks:', result.error);
374
+ }
375
+ ```
376
+
377
+ #### `getTasks()`
378
+ Get all tasks (deep copy).
379
+
380
+ #### `getTasksByName(name)`
381
+ Get tasks by name.
382
+
383
+ #### `getTaskByID(id)`
384
+ Get a specific task by ID.
385
+
386
+ #### `getTasksInRange(startDate, endDate, callback)`
387
+ Simulate tasks in a time range.
388
+
389
+ ```js
390
+ const events = automator.getTasksInRange(
327
391
  new Date('2025-05-01'),
328
392
  new Date('2025-05-07')
329
393
  );
@@ -331,72 +395,296 @@ const events = automator.getActionsInRange(
331
395
  console.log(events); // Array of scheduled events
332
396
  ```
333
397
 
334
- #### `describeAction(id)`
335
- Get a human-readable description of an action.
398
+ #### `describeTask(id)`
399
+ Get a human-readable description of a task.
336
400
 
337
401
  ### Events
338
402
 
339
403
  Listen to events using `automator.on(event, callback)`:
340
404
 
341
405
  * `ready` - Scheduler started
342
- * `action` - Action executed
343
- * `update` - Action added/updated/removed
406
+ * `task` - Task executed
407
+ * `update` - Task added/updated/removed
344
408
  * `error` - Error occurred
345
409
  * `warning` - Non-fatal data coercion or correction occurred
346
410
  * `debug` - Debug information
347
411
 
348
412
  ```js
349
- automator.on('action', (event) => {
350
- console.log('Action executed:', event.name);
413
+ automator.on('task', (event) => {
414
+ console.log('Task executed:', event.name);
351
415
  console.log('Scheduled:', event.scheduledTime);
352
416
  console.log('Actual:', event.actualTime);
353
417
  });
354
418
  ```
355
419
 
356
- ### Storage Adapters
420
+ ---
421
+
422
+ ## Error Handling (v6.0+)
423
+
424
+ Starting with v6.0, all CRUD methods return **result objects** instead of throwing errors. This design ensures:
425
+
426
+ 1. **Never crashes your application** - No exceptions thrown for validation errors
427
+ 2. **Synchronous error reporting** - Get immediate feedback for web interface integration
428
+ 3. **Structured error codes** - Enable programmatic error handling
429
+ 4. **Predictable behavior** - All methods follow the same pattern
357
430
 
358
- #### File Storage
431
+ ### Result Object Pattern
432
+
433
+ All mutation methods (`addTask`, `updateTaskByID`, `updateTaskByName`, `removeTaskByID`, `removeTaskByName`, `seed`) return a result object:
434
+
435
+ #### Success Response
359
436
 
360
437
  ```js
361
- const automator = new Automator({
362
- storage: Automator.storage.file('./actions.json')
438
+ const result = automator.addTask({
439
+ cmd: 'myCommand',
440
+ date: new Date()
441
+ });
442
+
443
+ // Success structure:
444
+ // {
445
+ // success: true,
446
+ // id: 1 // for addTask, updateTaskByID, removeTaskByID
447
+ // count: 2, // for updateTaskByName, removeTaskByName
448
+ // seeded: true, // for seed()
449
+ // task: {...} // optional - the task object
450
+ // }
451
+
452
+ if (result.success) {
453
+ console.log('Task added with ID:', result.id);
454
+ }
455
+ ```
456
+
457
+ #### Error Response
458
+
459
+ ```js
460
+ const result = automator.addTask({
461
+ name: 'Invalid Task'
462
+ // Missing required 'cmd' property
363
463
  });
464
+
465
+ // Error structure:
466
+ // {
467
+ // success: false,
468
+ // error: "Task must have a cmd property",
469
+ // code: "MISSING_CMD",
470
+ // field: "cmd" // optional - which field caused the error
471
+ // }
472
+
473
+ if (!result.success) {
474
+ console.error(`Error: ${result.error} (${result.code})`);
475
+ }
364
476
  ```
365
477
 
366
- #### Memory Storage
478
+ ### Error Codes Reference
479
+
480
+ | Code | Description | Affected Methods |
481
+ |------|-------------|------------------|
482
+ | `MISSING_CMD` | Required `cmd` property missing | `addTask` |
483
+ | `INVALID_REPEAT_TYPE` | Invalid or missing `repeat.type` | `addTask`, `updateTaskByID`, `updateTaskByName` |
484
+ | `INVALID_CATCHUP_WINDOW` | Invalid `catchUpWindow` value | `addTask`, `updateTaskByID`, `updateTaskByName` |
485
+ | `INVALID_CATCHUP_LIMIT` | Invalid `catchUpLimit` value | `addTask`, `updateTaskByID`, `updateTaskByName` |
486
+ | `TASK_NOT_FOUND` | Task ID not found | `updateTaskByID`, `removeTaskByID` |
487
+ | `NO_TASKS_FOUND` | No tasks with given name | `removeTaskByName` |
488
+ | `INVALID_CALLBACK` | Callback is not a function | `seed` |
489
+
490
+ ### Web Interface Integration Example
491
+
492
+ The result object pattern makes it easy to integrate with web APIs:
493
+
494
+ ```js
495
+ const express = require('express');
496
+ const app = express();
497
+
498
+ // Add task endpoint
499
+ app.post('/api/tasks', (req, res) => {
500
+ const result = automator.addTask(req.body);
501
+
502
+ if (result.success) {
503
+ res.json({
504
+ message: 'Task created successfully',
505
+ taskId: result.id
506
+ });
507
+ } else {
508
+ res.status(400).json({
509
+ error: result.error,
510
+ code: result.code,
511
+ field: result.field // helpful for form validation
512
+ });
513
+ }
514
+ });
515
+
516
+ // Update task endpoint
517
+ app.put('/api/tasks/:id', (req, res) => {
518
+ const result = automator.updateTaskByID(
519
+ parseInt(req.params.id),
520
+ req.body
521
+ );
522
+
523
+ if (result.success) {
524
+ res.json({
525
+ message: 'Task updated successfully',
526
+ task: result.task
527
+ });
528
+ } else {
529
+ const status = result.code === 'TASK_NOT_FOUND' ? 404 : 400;
530
+ res.status(status).json({
531
+ error: result.error,
532
+ code: result.code
533
+ });
534
+ }
535
+ });
536
+
537
+ // Delete task endpoint
538
+ app.delete('/api/tasks/:id', (req, res) => {
539
+ const result = automator.removeTaskByID(parseInt(req.params.id));
540
+
541
+ if (result.success) {
542
+ res.json({
543
+ message: 'Task deleted successfully',
544
+ taskId: result.id
545
+ });
546
+ } else {
547
+ res.status(404).json({
548
+ error: result.error,
549
+ code: result.code
550
+ });
551
+ }
552
+ });
553
+ ```
554
+
555
+ ### Validation Rules
556
+
557
+ The following validation rules apply:
558
+
559
+ **Fatal Errors** (return error result):
560
+ - Missing `cmd` property
561
+ - Invalid `repeat.type` (must be: second, minute, hour, day, weekday, weekend, week, month, year)
562
+ - Invalid `catchUpWindow` (must be "unlimited" or non-negative number)
563
+ - Invalid `catchUpLimit` (must be "all" or non-negative integer)
564
+ - Task not found (for update/remove by ID)
565
+
566
+ **Defensive Coercions** (emit warnings but allow task):
567
+ - Invalid `repeat.interval` โ†’ coerced to valid integer (minimum 1)
568
+ - Invalid `repeat.limit` โ†’ coerced to `null` (unlimited)
569
+ - Invalid `repeat.endDate` โ†’ coerced to `null`
570
+ - Invalid `repeat.dstPolicy` โ†’ coerced to `'once'`
571
+ - Missing `date` โ†’ defaults to 5 seconds in future
572
+
573
+ **Silent Defaults** (emit debug):
574
+ - Missing `catchUpWindow` โ†’ smart default based on task type
575
+ - Missing `repeat.interval` โ†’ defaults to 1
576
+
577
+ ### Event-Based Error Monitoring
578
+
579
+ In addition to returning result objects, the automator still emits error events for logging and monitoring:
580
+
581
+ ```js
582
+ automator.on('error', (event) => {
583
+ console.error('Validation error:', event.message);
584
+ console.error('Error code:', event.code);
585
+
586
+ // Log to external monitoring service
587
+ if (event.type === 'validation_error') {
588
+ logToMonitoring({
589
+ level: 'error',
590
+ code: event.code,
591
+ message: event.message
592
+ });
593
+ }
594
+ });
595
+
596
+ automator.on('warning', (event) => {
597
+ console.warn('Data coercion:', event.message);
598
+ });
599
+ ```
600
+
601
+ ### Migration from v5.x
602
+
603
+ **Before (v5.x - throwing exceptions):**
604
+ ```js
605
+ try {
606
+ const id = automator.addTask({ cmd: 'test' });
607
+ console.log('Task added:', id);
608
+ } catch (error) {
609
+ console.error('Failed:', error.message);
610
+ }
611
+ ```
612
+
613
+ **After (v6.0 - result objects):**
614
+ ```js
615
+ const result = automator.addTask({ cmd: 'test' });
616
+
617
+ if (result.success) {
618
+ console.log('Task added:', result.id);
619
+ } else {
620
+ console.error('Failed:', result.error);
621
+ }
622
+ ```
623
+
624
+ ---
625
+
626
+ ### Storage Options
627
+
628
+ #### File-Based Persistence
367
629
 
368
630
  ```js
369
631
  const automator = new Automator({
370
- storage: Automator.storage.memory()
632
+ storageFile: './tasks.json',
633
+ autoSave: true, // default: true
634
+ saveInterval: 15000 // default: 15000ms (15 seconds)
371
635
  });
372
636
  ```
373
637
 
374
- #### Custom Storage
638
+ **Moratorium-Based Persistence:**
639
+ - **CRUD operations** (add/update/remove tasks) save immediately and start a moratorium period
640
+ - **Task execution** (state progression) marks state as dirty and saves if moratorium has expired
641
+ - If moratorium is active, dirty state waits until moratorium ends, then saves automatically
642
+ - `saveInterval` sets the moratorium period - the minimum cooling time between saves (default: 15s)
643
+ - `stop()` always saves immediately if dirty, ignoring any active moratorium
644
+
645
+ This moratorium-based approach minimizes disk writes from frequent task execution (important for SD cards/flash media) while ensuring CRUD changes are always persisted immediately.
646
+
647
+ #### Memory-Only Mode
375
648
 
376
649
  ```js
377
650
  const automator = new Automator({
378
- storage: {
379
- load: function() {
380
- // Return { actions: [...] }
381
- },
382
- save: function(state) {
383
- // Save state
384
- }
385
- }
651
+ // No storageFile = memory-only mode (no persistence)
652
+ });
653
+ ```
654
+
655
+ State exists only in memory and is lost when the process ends.
656
+
657
+ #### Custom Storage (Database, Cloud, etc.)
658
+
659
+ For custom persistence needs, use `getTasks()` and event listeners:
660
+
661
+ ```js
662
+ const automator = new Automator(); // Memory-only, no file
663
+
664
+ // Load from custom source on initialization
665
+ automator.seed(async (auto) => {
666
+ const tasks = await loadFromDatabase();
667
+ tasks.forEach(task => auto.addTask(task));
668
+ });
669
+
670
+ // Save on updates
671
+ automator.on('update', async () => {
672
+ const tasks = automator.getTasks();
673
+ await saveToDatabase(tasks);
386
674
  });
387
675
  ```
388
676
 
389
677
  ---
390
678
 
391
- ## ๐Ÿ“Š Example: Sensor Reading Every Second
679
+ ## Example: Sensor Reading Every Second
392
680
 
393
681
  ```js
394
- automator.addAction({
682
+ automator.addTask({
395
683
  name: 'TempSensor',
396
684
  cmd: 'readTemp',
397
685
  date: null, // run immediately
398
686
  payload: null,
399
- unBuffered: false, // catch up if delayed
687
+ catchUpMode: 'default',
400
688
  repeat: {
401
689
  type: 'second',
402
690
  interval: 1
@@ -414,7 +702,7 @@ Your "60 readings per minute" pattern is preserved logically.
414
702
 
415
703
  ---
416
704
 
417
- ## ๐Ÿ•ฐ DST Behavior Examples
705
+ ## DST Behavior Examples
418
706
 
419
707
  ### Fall Back (Repeated Hour)
420
708
 
@@ -439,7 +727,7 @@ User chooses:
439
727
 
440
728
  ---
441
729
 
442
- ## ๐Ÿงช Testing
730
+ ## Testing
443
731
 
444
732
  ```bash
445
733
  npm test
@@ -448,9 +736,9 @@ npm run test:coverage
448
736
 
449
737
  ---
450
738
 
451
- ## ๐Ÿ“ฆ Action Specification
739
+ ## Task Specification
452
740
 
453
- ### Top-level action fields:
741
+ ### Top-level task fields:
454
742
 
455
743
  | Field | Description |
456
744
  | --------------- | --------------------------------------------------------------------- |
@@ -459,7 +747,10 @@ npm run test:coverage
459
747
  | `cmd` | Name of registered function to execute |
460
748
  | `payload` | Data passed to the command |
461
749
  | `date` | Next scheduled run time (local `Date`) |
462
- | `catchUpWindow` | Time window for catching up missed executions (smart default based on action type, or milliseconds number) |
750
+ | `catchUpMode` | Sets catch-up behavior ('default', 'realtime'). Overridden by explicit `catchUpWindow`. |
751
+ | `catchUpWindow` | Time window for catching up missed executions (in milliseconds). |
752
+ | `catchUpLimit` | Max number of missed executions to run (e.g., 1, or 'all'). |
753
+
463
754
 
464
755
  ### Repeat block:
465
756
 
@@ -474,7 +765,7 @@ npm run test:coverage
474
765
 
475
766
  ---
476
767
 
477
- ## ๐ŸŽฏ Project Goals (v4)
768
+ ## Project Goals (v6)
478
769
 
479
770
  * Deterministic behavior
480
771
  * Rock-solid DST handling
@@ -484,17 +775,19 @@ npm run test:coverage
484
775
  * Suitable for small devices
485
776
  * Approachable but powerful API
486
777
  * Long-term maintainability
778
+ * Never crash applications (result-based error handling)
779
+ * Web interface friendly (synchronous, structured error feedback)
487
780
 
488
781
  ---
489
782
 
490
- ## ๐Ÿ“ License
783
+ ## License
491
784
 
492
785
  MIT
493
786
 
494
787
  ---
495
788
 
496
- ## โค๏ธ Acknowledgments
789
+ ## Acknowledgments
497
790
 
498
- jw-automator v4 is a ground-up rethinking of the original jw-automator library, preserving the spirit while strengthening the foundations.
791
+ jw-automator v6 builds on the solid foundations of previous versions, adding result-based error handling to ensure stability and web interface compatibility while preserving the spirit of predictable, human-friendly scheduling.
499
792
 
500
793
  If you're building automation logic and want predictable, human-friendly scheduling that survives the real world โ€” **welcome.**