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/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 +556 -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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Proposed Solution: Consistent Cascading Catch-up Macros
|
|
2
|
+
|
|
3
|
+
This document outlines a plan to address inconsistencies and enhance the cascading behavior of `catchUpMode` macros within the `jw-automator` library.
|
|
4
|
+
|
|
5
|
+
## Current State Analysis
|
|
6
|
+
|
|
7
|
+
### Strengths
|
|
8
|
+
* **Existing Cascading Logic:** A basic cascading system is present where a task's `catchUpMode` can override the `Automator` instance's `defaultCatchUpMode`.
|
|
9
|
+
* **Pure Core Engine:** The `CoreEngine` correctly consumes only normalized `catchUpWindow` and `catchUpLimit` values, remaining isolated from macro interpretation.
|
|
10
|
+
* **Normalization-then-Deletion:** The `catchUpMode` is processed into concrete `catchUpWindow`/`catchUpLimit` values and then deleted from the task object before persistence. This ensures that the persisted state reflects the "ground truth" and prevents unintended changes if macro definitions evolve.
|
|
11
|
+
|
|
12
|
+
### Weaknesses / Inconsistencies
|
|
13
|
+
* **Inconsistent Update Behavior:** The `updateTaskByID` and `updateTaskByName` methods do not fully apply the cascading logic. They only process `catchUpMode` if it's explicitly provided in the `updates` object, failing to fall back to the `Automator`'s `defaultCatchUpMode` if the task being updated previously used the default. This can lead to tasks losing their intended catch-up behavior upon update.
|
|
14
|
+
* **Limited Macro Support:** The current implementation only supports `'default'` and `'realtime'` `catchUpMode` values. The `'auto'` mode, which was discussed as calculating values based on recurrence intervals, is not yet implemented.
|
|
15
|
+
|
|
16
|
+
## Proposed Solution
|
|
17
|
+
|
|
18
|
+
The goal is to reinforce and consistently apply the existing "one-way normalization" philosophy across all task management operations (`addTask`, `updateTaskByID`, `updateTaskByName`), while also extending its capabilities.
|
|
19
|
+
|
|
20
|
+
### 1. Centralize Normalization Logic
|
|
21
|
+
|
|
22
|
+
* **Action:** Create a new private helper method in `src/Automator.js`, e.g., `_normalizeCatchUpSettings(taskSpec, existingTask)`.
|
|
23
|
+
* **Purpose:** This method will be responsible for resolving the final `catchUpWindow` and `catchUpLimit` values based on a clear cascading order:
|
|
24
|
+
1. **Explicit values on input:** `taskSpec.catchUpWindow` or `taskSpec.catchUpLimit` take highest precedence.
|
|
25
|
+
2. **Input `catchUpMode`:** If `taskSpec.catchUpMode` is present, it will be used to derive values.
|
|
26
|
+
3. **Existing task values:** If no `catchUpWindow`, `catchUpLimit`, or `catchUpMode` is provided in `taskSpec`, the values from `existingTask` (if provided for an update operation) will be retained.
|
|
27
|
+
4. **Automator `defaultCatchUpMode`:** If none of the above are present, the `this.options.defaultCatchUpMode` will be used to derive the values.
|
|
28
|
+
5. **Hardcoded System Default:** If all else fails, a sensible default (e.g., `realtime` equivalent) will be used.
|
|
29
|
+
* **Macro Deletion:** This helper will ensure `taskSpec.catchUpMode` is always deleted after normalization.
|
|
30
|
+
|
|
31
|
+
### 2. Refactor `addTask`
|
|
32
|
+
|
|
33
|
+
* **Action:** Modify `addTask` to call `_normalizeCatchUpSettings` to process the incoming `taskSpec` before assigning task properties.
|
|
34
|
+
* **Benefit:** Simplifies `addTask` and ensures consistent application of the normalization rules.
|
|
35
|
+
|
|
36
|
+
### 3. Refactor `updateTaskByID` and `updateTaskByName`
|
|
37
|
+
|
|
38
|
+
* **Action:**
|
|
39
|
+
* For both methods, first retrieve the `existingTask` before applying any updates.
|
|
40
|
+
* Pass the `updates` object (which acts as the `taskSpec` for normalization) and the `existingTask` to `_normalizeCatchUpSettings`.
|
|
41
|
+
* Apply the resulting `catchUpWindow` and `catchUpLimit` to the task being updated.
|
|
42
|
+
* **Benefit:** Resolves the current inconsistency. Tasks will now correctly retain their existing `catchUp` settings during an update if no new `catchUpMode` or explicit values are provided, or correctly apply the `Automator`'s `defaultCatchUpMode` if applicable.
|
|
43
|
+
|
|
44
|
+
### 4. Implement `'auto'` Macro (Recommended Enhancement)
|
|
45
|
+
|
|
46
|
+
* **Action:** Integrate the logic for an `'auto'` `catchUpMode` within the `_normalizeCatchUpSettings` helper.
|
|
47
|
+
* **Logic:** When `catchUpMode` is `'auto'`, calculate `catchUpWindow` and `catchUpLimit` dynamically, potentially based on the task's `repeat.interval` (e.g., `catchUpWindow = 25% of interval`, `catchUpLimit = 1`).
|
|
48
|
+
* **Benefit:** Aligns the implementation with the ideas in `dev/11-catchup_macros.md`, providing a more intelligent default based on task recurrence.
|
|
49
|
+
|
|
50
|
+
## Summary of Benefits
|
|
51
|
+
|
|
52
|
+
This plan will:
|
|
53
|
+
* Ensure **consistent and predictable** catch-up behavior across all task creation and modification operations.
|
|
54
|
+
* Maintain the project's philosophy of **pure core engine** and **persisted ground truth**.
|
|
55
|
+
* Improve **developer ergonomics** by centralizing complex logic.
|
|
56
|
+
* Provide a clear path for **future macro enhancements** like the `'auto'` mode.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -12,7 +12,7 @@ This document describes the internal architecture of jw-automator v3.
|
|
|
12
12
|
2. **Local Time First**: Human-centric time semantics, not UTC-based
|
|
13
13
|
3. **Deterministic Core**: Pure step function enables testing and simulation
|
|
14
14
|
4. **Resilience**: Survive offline gaps, DST transitions, and event loop stalls
|
|
15
|
-
5. **Separation of Concerns**: Spec vs. state, core vs. host,
|
|
15
|
+
5. **Separation of Concerns**: Spec vs. state, core vs. host, persistence strategy
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -23,38 +23,38 @@ This document describes the internal architecture of jw-automator v3.
|
|
|
23
23
|
│ Automator (API) │
|
|
24
24
|
│ - User-facing API │
|
|
25
25
|
│ - Event management │
|
|
26
|
-
│ -
|
|
27
|
-
│ -
|
|
26
|
+
│ - Task CRUD operations │
|
|
27
|
+
│ - Moratorium-based persistence │
|
|
28
28
|
└──────────────┬──────────────────────────┘
|
|
29
29
|
│
|
|
30
|
-
|
|
30
|
+
│
|
|
31
|
+
│
|
|
32
|
+
┌──────▼──────┐
|
|
33
|
+
│SchedulerHost│
|
|
31
34
|
│ │
|
|
32
|
-
|
|
33
|
-
│
|
|
34
|
-
│
|
|
35
|
-
|
|
36
|
-
│
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
│
|
|
42
|
-
│
|
|
43
|
-
│
|
|
44
|
-
│
|
|
45
|
-
│
|
|
46
|
-
|
|
47
|
-
│
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
│ - DST handling │
|
|
56
|
-
│ - Local-time logic │
|
|
57
|
-
└────────────────┘
|
|
35
|
+
│ - 1-sec tick│
|
|
36
|
+
│ - Event emit│
|
|
37
|
+
│ - Functions │
|
|
38
|
+
└──────┬──────┘
|
|
39
|
+
│
|
|
40
|
+
┌──────────────▼──────────────────────┐
|
|
41
|
+
│ CoreEngine │
|
|
42
|
+
│ │
|
|
43
|
+
│ step(state, lastTick, now) │
|
|
44
|
+
│ → { newState, events } │
|
|
45
|
+
│ │
|
|
46
|
+
│ - Deterministic │
|
|
47
|
+
│ - Pure function │
|
|
48
|
+
│ - Testable │
|
|
49
|
+
└──────────────┬──────────────────────┘
|
|
50
|
+
│
|
|
51
|
+
┌──────▼────────┐
|
|
52
|
+
│RecurrenceEngine│
|
|
53
|
+
│ │
|
|
54
|
+
│ - Next occurrence calc │
|
|
55
|
+
│ - DST handling │
|
|
56
|
+
│ - Local-time logic │
|
|
57
|
+
└────────────────┘
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
---
|
|
@@ -68,7 +68,7 @@ This document describes the internal architecture of jw-automator v3.
|
|
|
68
68
|
**Responsibilities**:
|
|
69
69
|
- Process scheduling steps
|
|
70
70
|
- Manage catch-up logic
|
|
71
|
-
- Emit
|
|
71
|
+
- Emit task events
|
|
72
72
|
- Enforce safety invariants (max iterations, monotonic time)
|
|
73
73
|
|
|
74
74
|
**Key Method**: `step(state, lastTick, now)`
|
|
@@ -119,28 +119,36 @@ setTimeout(() => tick(), wait);
|
|
|
119
119
|
|
|
120
120
|
**Why?**
|
|
121
121
|
- Prevents cumulative drift
|
|
122
|
-
- Ensures all
|
|
122
|
+
- Ensures all tasks check against stable boundaries
|
|
123
123
|
- Enables deterministic simulation
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
127
|
-
### 4.
|
|
127
|
+
### 4. Persistence (v5+)
|
|
128
128
|
|
|
129
|
-
**Purpose**:
|
|
129
|
+
**Purpose**: State persistence with minimal disk wear
|
|
130
130
|
|
|
131
|
-
**
|
|
132
|
-
```javascript
|
|
133
|
-
{
|
|
134
|
-
load: () => state,
|
|
135
|
-
save: (state) => void
|
|
136
|
-
}
|
|
137
|
-
```
|
|
131
|
+
**Implementation**: Integrated directly into Automator class
|
|
138
132
|
|
|
139
|
-
**
|
|
140
|
-
-
|
|
141
|
-
-
|
|
133
|
+
**Strategy**: Moratorium-based state machine
|
|
134
|
+
- CRUD operations (add/update/remove) save immediately and start moratorium period
|
|
135
|
+
- Task execution marks state dirty and saves if moratorium has expired
|
|
136
|
+
- If moratorium is active, dirty state waits until moratorium ends, then saves automatically
|
|
137
|
+
- Single entry point: `_requestSave(force)` with "tell vs ask" semantics
|
|
138
|
+
- `saveInterval` defines the moratorium period (minimum cooling time, default: 15 seconds)
|
|
139
|
+
- `stop()` forces save if dirty, ignoring any active moratorium
|
|
140
|
+
- No periodic polling - one-shot timer only fires when state is dirty
|
|
142
141
|
|
|
143
|
-
**
|
|
142
|
+
**State Machine**:
|
|
143
|
+
- `stateDirty`: Boolean flag indicating unsaved changes
|
|
144
|
+
- `moratoriumActive`: Boolean flag indicating cooling period is active
|
|
145
|
+
- `moratoriumTimer`: One-shot setTimeout handle (not setInterval)
|
|
146
|
+
|
|
147
|
+
**Files**:
|
|
148
|
+
- File-based: `storageFile` option specifies JSON file path
|
|
149
|
+
- Memory-only: No `storageFile` option
|
|
150
|
+
|
|
151
|
+
**Custom Persistence**: Use `getTasks()` and event listeners for database/cloud storage
|
|
144
152
|
|
|
145
153
|
---
|
|
146
154
|
|
|
@@ -151,28 +159,28 @@ setTimeout(() => tick(), wait);
|
|
|
151
159
|
**Responsibilities**:
|
|
152
160
|
- Coordinate all components
|
|
153
161
|
- Provide user-facing API
|
|
154
|
-
- Manage
|
|
162
|
+
- Manage task lifecycle
|
|
155
163
|
- Handle auto-save
|
|
156
164
|
- Event emission
|
|
157
165
|
|
|
158
166
|
**Key APIs**:
|
|
159
|
-
-
|
|
167
|
+
- Task management: `addTask`, `updateTaskByID`, `updateTaskByName`, `removeTaskByID`, etc.
|
|
160
168
|
- Function registration: `addFunction`, `removeFunction`
|
|
161
|
-
- Introspection: `
|
|
169
|
+
- Introspection: `getTasks`, `describeTask`, `getTasksInRange`
|
|
162
170
|
- Lifecycle: `start`, `stop`
|
|
163
171
|
|
|
164
172
|
---
|
|
165
173
|
|
|
166
174
|
## Data Flow
|
|
167
175
|
|
|
168
|
-
### Adding
|
|
176
|
+
### Adding a Task
|
|
169
177
|
|
|
170
178
|
```
|
|
171
|
-
User calls
|
|
179
|
+
User calls addTask()
|
|
172
180
|
↓
|
|
173
181
|
Automator validates spec
|
|
174
182
|
↓
|
|
175
|
-
Creates
|
|
183
|
+
Creates task with ID and state
|
|
176
184
|
↓
|
|
177
185
|
Adds to SchedulerHost state
|
|
178
186
|
↓
|
|
@@ -188,7 +196,7 @@ SchedulerHost timer fires
|
|
|
188
196
|
↓
|
|
189
197
|
Calls CoreEngine.step(state, lastTick, now)
|
|
190
198
|
↓
|
|
191
|
-
CoreEngine processes each
|
|
199
|
+
CoreEngine processes each task:
|
|
192
200
|
- Is nextRun <= now?
|
|
193
201
|
- Buffered/unbuffered logic
|
|
194
202
|
- Execute or skip
|
|
@@ -199,9 +207,9 @@ Returns { newState, events }
|
|
|
199
207
|
↓
|
|
200
208
|
SchedulerHost updates state
|
|
201
209
|
↓
|
|
202
|
-
SchedulerHost executes
|
|
210
|
+
SchedulerHost executes task events:
|
|
203
211
|
- Call registered function
|
|
204
|
-
- Emit '
|
|
212
|
+
- Emit 'task' event
|
|
205
213
|
↓
|
|
206
214
|
Schedule next tick
|
|
207
215
|
```
|
|
@@ -209,13 +217,13 @@ Schedule next tick
|
|
|
209
217
|
### Simulation
|
|
210
218
|
|
|
211
219
|
```
|
|
212
|
-
User calls
|
|
220
|
+
User calls getTasksInRange(start, end)
|
|
213
221
|
↓
|
|
214
222
|
CoreEngine.simulate() clones state
|
|
215
223
|
↓
|
|
216
224
|
Steps through time second-by-second
|
|
217
225
|
↓
|
|
218
|
-
Collects all
|
|
226
|
+
Collects all task events
|
|
219
227
|
↓
|
|
220
228
|
Returns event list (state unchanged)
|
|
221
229
|
```
|
|
@@ -246,9 +254,9 @@ Returns event list (state unchanged)
|
|
|
246
254
|
|
|
247
255
|
### Why `catchUpWindow` (and Legacy Buffered/UnBuffered)?
|
|
248
256
|
|
|
249
|
-
The `catchUpWindow` property, supported by the CoreEngine, precisely defines the time window for recovering missed
|
|
257
|
+
The `catchUpWindow` property, supported by the CoreEngine, precisely defines the time window for recovering missed tasks.
|
|
250
258
|
|
|
251
|
-
- **Smart Defaults**: For recurring
|
|
259
|
+
- **Smart Defaults**: For recurring tasks, the `catchUpWindow` defaults to the task's interval (e.g., a 1-hour task has a 1-hour catch-up window). For one-time tasks, it defaults to `0` (no catch-up). This prevents "thundering herd" issues by ensuring that tasks too old are simply skipped.
|
|
252
260
|
- **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
261
|
- **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.
|
|
254
262
|
|
|
@@ -276,7 +284,7 @@ The `catchUpWindow` property, supported by the CoreEngine, precisely defines the
|
|
|
276
284
|
|
|
277
285
|
### 3. State Integrity
|
|
278
286
|
|
|
279
|
-
-
|
|
287
|
+
- Tasks cloned before mutation
|
|
280
288
|
- Spec vs. state separation
|
|
281
289
|
- Deep copies returned from getters
|
|
282
290
|
- Auto-save with configurable intervals
|
|
@@ -285,25 +293,32 @@ The `catchUpWindow` property, supported by the CoreEngine, precisely defines the
|
|
|
285
293
|
|
|
286
294
|
## Performance Characteristics
|
|
287
295
|
|
|
288
|
-
- **Per-Tick Complexity**: O(n) where n = number of
|
|
296
|
+
- **Per-Tick Complexity**: O(n) where n = number of tasks
|
|
289
297
|
- **Recurrence Calculation**: O(1) per step
|
|
290
|
-
- **Memory**: O(n) for
|
|
291
|
-
- **Disk I/O**:
|
|
298
|
+
- **Memory**: O(n) for task storage
|
|
299
|
+
- **Disk I/O**: Dirty-flag with cooling period (default: 15s minimum between saves)
|
|
292
300
|
|
|
293
|
-
**Scalability**: Designed for 10-1000
|
|
301
|
+
**Scalability**: Designed for 10-1000 tasks, not 100,000s
|
|
294
302
|
|
|
295
303
|
---
|
|
296
304
|
|
|
297
305
|
## Extension Points
|
|
298
306
|
|
|
299
|
-
### Custom
|
|
307
|
+
### Custom Persistence
|
|
308
|
+
|
|
309
|
+
Use `getTasks()` and event listeners for database/cloud storage:
|
|
300
310
|
|
|
301
311
|
```javascript
|
|
302
|
-
new Automator(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
312
|
+
const automator = new Automator(); // Memory-only
|
|
313
|
+
|
|
314
|
+
automator.seed(async (auto) => {
|
|
315
|
+
const tasks = await loadFromDatabase();
|
|
316
|
+
tasks.forEach(task => auto.addTask(task));
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
automator.on('update', async () => {
|
|
320
|
+
const tasks = automator.getTasks();
|
|
321
|
+
await saveToDatabase(tasks);
|
|
307
322
|
});
|
|
308
323
|
```
|
|
309
324
|
|
|
@@ -311,9 +326,9 @@ new Automator({
|
|
|
311
326
|
|
|
312
327
|
Extend `RecurrenceEngine` with new types (requires code modification)
|
|
313
328
|
|
|
314
|
-
### Meta-
|
|
329
|
+
### Meta-Tasks
|
|
315
330
|
|
|
316
|
-
|
|
331
|
+
Tasks can call `automator.addTask()` to create dynamic schedules
|
|
317
332
|
|
|
318
333
|
---
|
|
319
334
|
|
|
@@ -335,9 +350,9 @@ Potential improvements for future versions:
|
|
|
335
350
|
- Timezone support (explicit tz parameter)
|
|
336
351
|
- Cron expression compatibility layer
|
|
337
352
|
- Web-based dashboard
|
|
338
|
-
-
|
|
353
|
+
- Task priorities
|
|
339
354
|
- Conditional execution (predicates)
|
|
340
|
-
-
|
|
355
|
+
- Task dependencies (chains)
|
|
341
356
|
|
|
342
357
|
---
|
|
343
358
|
|