jw-automator 4.0.0 โ 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/CHANGELOG.md +0 -0
- package/README.md +392 -99
- package/docs/11a-macro-fix.md +56 -0
- package/docs/ARCHITECTURE.md +88 -73
- package/docs/MIGRATION.md +218 -54
- package/docs/QUICKSTART.md +63 -48
- package/docs/defensive-defaults.md +9 -9
- package/examples/basic-example.js +24 -9
- package/examples/hello-world.js +2 -2
- package/examples/iot-sensor-example.js +6 -6
- package/examples/seed-example.js +6 -6
- package/index.js +0 -0
- package/package.json +2 -2
- package/src/Automator.js +558 -313
- package/src/core/CoreEngine.js +103 -159
- package/src/core/RecurrenceEngine.js +67 -6
- package/src/host/SchedulerHost.js +60 -14
- package/src/storage/FileStorage.js +0 -59
- package/src/storage/MemoryStorage.js +0 -27
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
-
##
|
|
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
|
|
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-
|
|
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
|
-
##
|
|
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('./
|
|
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
|
|
61
|
+
// Seed initial tasks (runs only on first use)
|
|
62
62
|
automator.seed((auto) => {
|
|
63
|
-
auto.
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
|
142
|
+
### 4. **Resilient Catch-Up with `catchUpMode`**
|
|
143
143
|
|
|
144
|
-
|
|
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
|
-
**`
|
|
146
|
+
**`catchUpMode` (The Easy Way)**
|
|
147
147
|
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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 (
|
|
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-
|
|
208
|
+
### 6. **Meta-Tasks (Tasks that Create Tasks)**
|
|
202
209
|
|
|
203
|
-
jw-automator treats
|
|
210
|
+
jw-automator treats tasks as **data**, enabling higher-order patterns:
|
|
204
211
|
|
|
205
|
-
* A daily 7:00 AM
|
|
206
|
-
* A monthly billing
|
|
207
|
-
* A multi-step animation (e.g., dimming a light) can create timed sub-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
239
|
+
Seed the automator with initial tasks. Runs only when the database is empty (first use).
|
|
232
240
|
|
|
233
|
-
**Returns:** `boolean`
|
|
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.
|
|
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
|
|
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
|
-
#### `
|
|
270
|
-
Add a new
|
|
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
|
|
274
|
-
name: 'My
|
|
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
|
-
|
|
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
|
-
#### `
|
|
289
|
-
Update an existing
|
|
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.
|
|
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
|
-
#### `
|
|
299
|
-
Update all
|
|
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.
|
|
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
|
-
#### `
|
|
308
|
-
Remove
|
|
347
|
+
#### `removeTaskByID(id)`
|
|
348
|
+
Remove a task by ID.
|
|
309
349
|
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
|
|
352
|
+
```js
|
|
353
|
+
const result = automator.removeTaskByID(1);
|
|
315
354
|
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
#### `
|
|
320
|
-
|
|
362
|
+
#### `removeTaskByName(name)`
|
|
363
|
+
Remove all tasks with the given name.
|
|
321
364
|
|
|
322
|
-
|
|
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
|
|
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
|
-
#### `
|
|
335
|
-
Get a human-readable description of
|
|
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
|
-
* `
|
|
343
|
-
* `update` -
|
|
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('
|
|
350
|
-
console.log('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
632
|
+
storageFile: './tasks.json',
|
|
633
|
+
autoSave: true, // default: true
|
|
634
|
+
saveInterval: 15000 // default: 15000ms (15 seconds)
|
|
371
635
|
});
|
|
372
636
|
```
|
|
373
637
|
|
|
374
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
##
|
|
679
|
+
## Example: Sensor Reading Every Second
|
|
392
680
|
|
|
393
681
|
```js
|
|
394
|
-
automator.
|
|
682
|
+
automator.addTask({
|
|
395
683
|
name: 'TempSensor',
|
|
396
684
|
cmd: 'readTemp',
|
|
397
685
|
date: null, // run immediately
|
|
398
686
|
payload: null,
|
|
399
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
739
|
+
## Task Specification
|
|
452
740
|
|
|
453
|
-
### Top-level
|
|
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
|
-
| `
|
|
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
|
-
##
|
|
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
|
-
##
|
|
783
|
+
## License
|
|
491
784
|
|
|
492
785
|
MIT
|
|
493
786
|
|
|
494
787
|
---
|
|
495
788
|
|
|
496
|
-
##
|
|
789
|
+
## Acknowledgments
|
|
497
790
|
|
|
498
|
-
jw-automator
|
|
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.**
|