jw-automator 3.1.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 +61 -0
- package/README.md +77 -38
- package/docs/ARCHITECTURE.md +8 -6
- package/docs/MIGRATION.md +68 -160
- package/docs/QUICKSTART.md +29 -7
- package/docs/defensive-defaults.md +65 -0
- package/examples/basic-example.js +48 -45
- package/examples/hello-world.js +0 -0
- package/examples/iot-sensor-example.js +0 -0
- package/examples/seed-example.js +147 -0
- package/index.js +0 -0
- package/package.json +2 -2
- package/src/Automator.js +246 -16
- package/src/core/CoreEngine.js +108 -4
- package/src/core/RecurrenceEngine.js +0 -0
- package/src/host/SchedulerHost.js +0 -0
- package/src/storage/FileStorage.js +0 -0
- package/src/storage/MemoryStorage.js +0 -0
|
@@ -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).
|
|
@@ -50,51 +50,54 @@ automator.on('error', (event) => {
|
|
|
50
50
|
console.error('Error:', event.message);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
nextSunday
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
53
|
+
// Seed initial actions (runs only on first use)
|
|
54
|
+
automator.seed((auto) => {
|
|
55
|
+
console.log('Seeding initial actions...\n');
|
|
56
|
+
|
|
57
|
+
// 1. Every 10 seconds - demo message
|
|
58
|
+
auto.addAction({
|
|
59
|
+
name: 'Demo Message',
|
|
60
|
+
cmd: 'logMessage',
|
|
61
|
+
date: new Date(Date.now() + 5000), // Start in 5 seconds
|
|
62
|
+
payload: { message: 'This is a recurring message every 10 seconds' },
|
|
63
|
+
catchUpWindow: 30000, // Tolerate 30 seconds of lag
|
|
64
|
+
repeat: {
|
|
65
|
+
type: 'second',
|
|
66
|
+
interval: 10,
|
|
67
|
+
limit: 6, // Run 6 times then stop
|
|
68
|
+
dstPolicy: 'once'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 2. Daily morning routine at 7:00 AM
|
|
73
|
+
auto.addAction({
|
|
74
|
+
name: 'Morning Routine',
|
|
75
|
+
cmd: 'morningRoutine',
|
|
76
|
+
date: new Date(new Date().setHours(7, 0, 0, 0)),
|
|
77
|
+
catchUpWindow: "unlimited", // Never miss a morning routine
|
|
78
|
+
repeat: {
|
|
79
|
+
type: 'day',
|
|
80
|
+
interval: 1,
|
|
81
|
+
dstPolicy: 'once'
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 3. Weekly backup every Sunday at 2:00 AM
|
|
86
|
+
const nextSunday = new Date();
|
|
87
|
+
nextSunday.setDate(nextSunday.getDate() + (7 - nextSunday.getDay()) % 7);
|
|
88
|
+
nextSunday.setHours(2, 0, 0, 0);
|
|
89
|
+
|
|
90
|
+
auto.addAction({
|
|
91
|
+
name: 'Weekly Backup',
|
|
92
|
+
cmd: 'weeklyBackup',
|
|
93
|
+
date: nextSunday,
|
|
94
|
+
catchUpWindow: "unlimited", // Always run backups, even if delayed
|
|
95
|
+
repeat: {
|
|
96
|
+
type: 'week',
|
|
97
|
+
interval: 1,
|
|
98
|
+
dstPolicy: 'once'
|
|
99
|
+
}
|
|
100
|
+
});
|
|
98
101
|
});
|
|
99
102
|
|
|
100
103
|
// Demonstrate simulation - what will happen in the next 24 hours?
|
package/examples/hello-world.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed Example - jw-automator v3
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the seed() method and catchUpWindow property.
|
|
5
|
+
* This example shows how to bootstrap a persistent scheduler that:
|
|
6
|
+
* - Initializes actions only on first run
|
|
7
|
+
* - Preserves user-modified schedules on subsequent runs
|
|
8
|
+
* - Uses catchUpWindow for smart catch-up behavior
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const Automator = require('../index');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
// Storage file path
|
|
15
|
+
const STORAGE_FILE = './seed-example-actions.json';
|
|
16
|
+
|
|
17
|
+
// Clean up old storage file for demo purposes
|
|
18
|
+
if (fs.existsSync(STORAGE_FILE)) {
|
|
19
|
+
console.log('Note: Removing existing storage file for clean demo');
|
|
20
|
+
fs.unlinkSync(STORAGE_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create automator with file-based persistence
|
|
24
|
+
const automator = new Automator({
|
|
25
|
+
storage: Automator.storage.file(STORAGE_FILE),
|
|
26
|
+
autoSave: true,
|
|
27
|
+
saveInterval: 3000
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Register command functions
|
|
31
|
+
automator.addFunction('criticalTask', function(payload, event) {
|
|
32
|
+
console.log(`[CRITICAL] ${payload.message}`);
|
|
33
|
+
console.log(` Scheduled: ${event.scheduledTime.toLocaleTimeString()}`);
|
|
34
|
+
console.log(` Executed: ${event.actualTime.toLocaleTimeString()}`);
|
|
35
|
+
console.log(` Execution #${event.count + 1}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
automator.addFunction('realtimeAlert', function(payload, event) {
|
|
39
|
+
console.log(`[REALTIME] ${payload.message}`);
|
|
40
|
+
console.log(` Only executes if "now" - skips if missed`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
automator.addFunction('flexibleTask', function(payload, event) {
|
|
44
|
+
const lag = event.actualTime - event.scheduledTime;
|
|
45
|
+
console.log(`[FLEXIBLE] ${payload.message}`);
|
|
46
|
+
console.log(` Lag: ${lag}ms (tolerates up to 5 seconds)`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Listen to events
|
|
50
|
+
automator.on('ready', () => {
|
|
51
|
+
console.log('\n=== Automator Ready ===');
|
|
52
|
+
console.log(`Actions loaded: ${automator.getActions().length}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// SEED: Initialize actions (runs only on first use)
|
|
56
|
+
const didSeed = automator.seed((auto) => {
|
|
57
|
+
console.log('\n=== SEEDING: First Run Detected ===');
|
|
58
|
+
console.log('Creating initial system tasks...\n');
|
|
59
|
+
|
|
60
|
+
// Task 1: Critical billing task - NEVER miss an execution
|
|
61
|
+
auto.addAction({
|
|
62
|
+
name: 'Billing Task',
|
|
63
|
+
cmd: 'criticalTask',
|
|
64
|
+
date: new Date(Date.now() + 2000),
|
|
65
|
+
payload: { message: 'Process billing (catchUpWindow: "unlimited")' },
|
|
66
|
+
catchUpWindow: "unlimited", // Catch up ALL missed executions
|
|
67
|
+
repeat: {
|
|
68
|
+
type: 'second',
|
|
69
|
+
interval: 10,
|
|
70
|
+
limit: 3
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Task 2: Real-time alert - only relevant "now"
|
|
75
|
+
auto.addAction({
|
|
76
|
+
name: 'Realtime Alert',
|
|
77
|
+
cmd: 'realtimeAlert',
|
|
78
|
+
date: new Date(Date.now() + 5000),
|
|
79
|
+
payload: { message: 'Temperature spike detected!' },
|
|
80
|
+
catchUpWindow: 0, // Skip ALL missed executions
|
|
81
|
+
repeat: {
|
|
82
|
+
type: 'second',
|
|
83
|
+
interval: 15,
|
|
84
|
+
limit: 3
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Task 3: Flexible sensor reading - tolerate brief lag
|
|
89
|
+
auto.addAction({
|
|
90
|
+
name: 'Sensor Reading',
|
|
91
|
+
cmd: 'flexibleTask',
|
|
92
|
+
date: new Date(Date.now() + 8000),
|
|
93
|
+
payload: { message: 'Read temperature sensor' },
|
|
94
|
+
catchUpWindow: 5000, // Tolerate up to 5 seconds of lag
|
|
95
|
+
repeat: {
|
|
96
|
+
type: 'second',
|
|
97
|
+
interval: 12,
|
|
98
|
+
limit: 3
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log('✅ Initial tasks created!\n');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (didSeed) {
|
|
106
|
+
console.log('✨ Seeding completed - tasks initialized for the first time');
|
|
107
|
+
} else {
|
|
108
|
+
console.log('⏭️ Seeding skipped - existing schedule preserved');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('\n=== Current Schedule ===');
|
|
112
|
+
automator.getActions().forEach((action) => {
|
|
113
|
+
console.log(`\n${action.name}:`);
|
|
114
|
+
console.log(` catchUpWindow: ${action.catchUpWindow === "unlimited" ? '"unlimited"' : action.catchUpWindow + 'ms'}`);
|
|
115
|
+
console.log(` Next run: ${action.date.toLocaleTimeString()}`);
|
|
116
|
+
console.log(` Executions so far: ${action.count}`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log('\n=== Starting Scheduler ===\n');
|
|
120
|
+
automator.start();
|
|
121
|
+
|
|
122
|
+
// Simulate a brief delay after 15 seconds to demonstrate catch-up behavior
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
console.log('\n⏸️ Simulating 3-second system pause...\n');
|
|
125
|
+
const start = Date.now();
|
|
126
|
+
while (Date.now() - start < 3000) {
|
|
127
|
+
// Block the event loop
|
|
128
|
+
}
|
|
129
|
+
console.log('▶️ Resumed! Watch how different catchUpWindow values behave:\n');
|
|
130
|
+
}, 15000);
|
|
131
|
+
|
|
132
|
+
// Auto-stop after 40 seconds
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
console.log('\n=== Shutting Down ===');
|
|
135
|
+
automator.stop();
|
|
136
|
+
console.log('\n💡 Tip: Run this example again to see seeding skipped!');
|
|
137
|
+
console.log(`Storage file: ${STORAGE_FILE}`);
|
|
138
|
+
console.log('Goodbye!\n');
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}, 40000);
|
|
141
|
+
|
|
142
|
+
// Graceful shutdown on Ctrl+C
|
|
143
|
+
process.on('SIGINT', () => {
|
|
144
|
+
console.log('\n\nShutting down...');
|
|
145
|
+
automator.stop();
|
|
146
|
+
process.exit(0);
|
|
147
|
+
});
|
package/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jw-automator",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A resilient, local-time, 1-second precision automation scheduler for Node.js",
|
|
3
|
+
"version": "4.0.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/",
|