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.
@@ -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.
@@ -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, storage abstraction
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
- │ - Action CRUD operations
27
- │ - Persistence coordination
26
+ │ - Task CRUD operations
27
+ │ - Moratorium-based persistence
28
28
  └──────────────┬──────────────────────────┘
29
29
 
30
- ┌──────┴──────┐
30
+
31
+
32
+ ┌──────▼──────┐
33
+ │SchedulerHost│
31
34
  │ │
32
- ┌───────▼──────┐ ┌──▼──────────────┐
33
- SchedulerHost│ │ Storage Adapter
34
- │ │
35
- │ - 1-sec tick │ │ - load()
36
- - Event emit │ │ - save() │
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
- └────────────────┘
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 action events
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 actions check against stable boundaries
122
+ - Ensures all tasks check against stable boundaries
123
123
  - Enables deterministic simulation
124
124
 
125
125
  ---
126
126
 
127
- ### 4. Storage Adapters
127
+ ### 4. Persistence (v5+)
128
128
 
129
- **Purpose**: Pluggable persistence
129
+ **Purpose**: State persistence with minimal disk wear
130
130
 
131
- **Interface**:
132
- ```javascript
133
- {
134
- load: () => state,
135
- save: (state) => void
136
- }
137
- ```
131
+ **Implementation**: Integrated directly into Automator class
138
132
 
139
- **Built-in Adapters**:
140
- - `FileStorage`: JSON file persistence
141
- - `MemoryStorage`: In-memory (no persistence)
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
- **Custom Adapters**: Users can provide their own (database, cloud, etc.)
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 action lifecycle
162
+ - Manage task lifecycle
155
163
  - Handle auto-save
156
164
  - Event emission
157
165
 
158
166
  **Key APIs**:
159
- - Action management: `addAction`, `updateActionByID`, `updateActionByName`, `removeActionByID`, etc.
167
+ - Task management: `addTask`, `updateTaskByID`, `updateTaskByName`, `removeTaskByID`, etc.
160
168
  - Function registration: `addFunction`, `removeFunction`
161
- - Introspection: `getActions`, `describeAction`, `getActionsInRange`
169
+ - Introspection: `getTasks`, `describeTask`, `getTasksInRange`
162
170
  - Lifecycle: `start`, `stop`
163
171
 
164
172
  ---
165
173
 
166
174
  ## Data Flow
167
175
 
168
- ### Adding an Action
176
+ ### Adding a Task
169
177
 
170
178
  ```
171
- User calls addAction()
179
+ User calls addTask()
172
180
 
173
181
  Automator validates spec
174
182
 
175
- Creates action with ID and state
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 action:
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 action events:
210
+ SchedulerHost executes task events:
203
211
  - Call registered function
204
- - Emit 'action' event
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 getActionsInRange(start, end)
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 action events
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 actions.
257
+ The `catchUpWindow` property, supported by the CoreEngine, precisely defines the time window for recovering missed tasks.
250
258
 
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.
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
- - Actions cloned before mutation
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 actions
296
+ - **Per-Tick Complexity**: O(n) where n = number of tasks
289
297
  - **Recurrence Calculation**: O(1) per step
290
- - **Memory**: O(n) for action storage
291
- - **Disk I/O**: Configurable auto-save interval (default 5s)
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 actions, not 100,000s
301
+ **Scalability**: Designed for 10-1000 tasks, not 100,000s
294
302
 
295
303
  ---
296
304
 
297
305
  ## Extension Points
298
306
 
299
- ### Custom Storage
307
+ ### Custom Persistence
308
+
309
+ Use `getTasks()` and event listeners for database/cloud storage:
300
310
 
301
311
  ```javascript
302
- new Automator({
303
- storage: {
304
- load: () => loadFromDatabase(),
305
- save: (state) => saveToDatabase(state)
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-Actions
329
+ ### Meta-Tasks
315
330
 
316
- Actions can call `automator.addAction()` to create dynamic schedules
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
- - Action priorities
353
+ - Task priorities
339
354
  - Conditional execution (predicates)
340
- - Action dependencies (chains)
355
+ - Task dependencies (chains)
341
356
 
342
357
  ---
343
358