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
package/src/core/CoreEngine.js
CHANGED
|
@@ -11,7 +11,7 @@ class CoreEngine {
|
|
|
11
11
|
/**
|
|
12
12
|
* Process one scheduling step
|
|
13
13
|
*
|
|
14
|
-
* @param {Object} state - Current scheduler state (
|
|
14
|
+
* @param {Object} state - Current scheduler state (tasks array)
|
|
15
15
|
* @param {Date} lastTick - Previous tick time
|
|
16
16
|
* @param {Date} now - Current tick time
|
|
17
17
|
* @param {number} maxIterations - Safety limit for catch-up loops
|
|
@@ -21,29 +21,29 @@ class CoreEngine {
|
|
|
21
21
|
const events = [];
|
|
22
22
|
const newState = this._cloneState(state);
|
|
23
23
|
|
|
24
|
-
// Process each
|
|
25
|
-
for (let i = 0; i < newState.
|
|
26
|
-
const
|
|
24
|
+
// Process each task
|
|
25
|
+
for (let i = 0; i < newState.tasks.length; i++) {
|
|
26
|
+
const task = newState.tasks[i];
|
|
27
27
|
|
|
28
|
-
if (!
|
|
28
|
+
if (!task.date) {
|
|
29
29
|
// No scheduled time - run immediately
|
|
30
|
-
const event = this.
|
|
30
|
+
const event = this._executeTask(task, now);
|
|
31
31
|
events.push(event);
|
|
32
32
|
|
|
33
33
|
// Advance to next occurrence
|
|
34
|
-
this.
|
|
34
|
+
this._advanceTask(task);
|
|
35
35
|
|
|
36
|
-
// Check if
|
|
37
|
-
if (RecurrenceEngine.shouldStop(
|
|
38
|
-
newState.
|
|
36
|
+
// Check if task should be removed
|
|
37
|
+
if (RecurrenceEngine.shouldStop(task)) {
|
|
38
|
+
newState.tasks.splice(i, 1);
|
|
39
39
|
i--;
|
|
40
40
|
}
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const nextRun = new Date(
|
|
44
|
+
const nextRun = new Date(task.date);
|
|
45
45
|
|
|
46
|
-
// Safety: prevent processing
|
|
46
|
+
// Safety: prevent processing tasks in the future
|
|
47
47
|
if (nextRun > now) {
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
@@ -52,78 +52,81 @@ class CoreEngine {
|
|
|
52
52
|
let iterationCount = 0;
|
|
53
53
|
let currentNextRun = nextRun;
|
|
54
54
|
|
|
55
|
-
// Get catchUpWindow (defaults to
|
|
56
|
-
const catchUpWindow =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
currentNextRun =
|
|
69
|
-
action.date = currentNextRun;
|
|
70
|
-
action.count = fastForwardResult.count;
|
|
71
|
-
|
|
72
|
-
// After fast-forward, if still outside window, just advance without executing
|
|
73
|
-
if (currentNextRun <= now) {
|
|
74
|
-
const finalLag = now.getTime() - currentNextRun.getTime();
|
|
75
|
-
if (finalLag > catchUpWindow) {
|
|
76
|
-
// Still outside window after fast-forward, skip to next viable occurrence
|
|
77
|
-
this._advanceAction(action);
|
|
78
|
-
currentNextRun = action.date ? new Date(action.date) : null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
55
|
+
// Get catchUpWindow and catchUpLimit (defaults to 0 - real-time mode, no catch-up)
|
|
56
|
+
const catchUpWindow = task.catchUpWindow !== undefined ? task.catchUpWindow : 0;
|
|
57
|
+
const catchUpLimit = task.catchUpLimit !== undefined ? task.catchUpLimit : 0;
|
|
58
|
+
|
|
59
|
+
// JUMP: Fast-forward to the beginning of the catch-up window.
|
|
60
|
+
// This avoids iterating through a large number of occurrences outside the window.
|
|
61
|
+
if (catchUpWindow !== "unlimited" && catchUpWindow > 0 && task.repeat) {
|
|
62
|
+
const windowStart = new Date(now.getTime() - catchUpWindow);
|
|
63
|
+
if (currentNextRun < windowStart) {
|
|
64
|
+
const ffResult = RecurrenceEngine.fastForward(currentNextRun, task.repeat, windowStart);
|
|
65
|
+
if (ffResult && ffResult.skippedOccurrences > 0) {
|
|
66
|
+
task.date = ffResult.nextRun;
|
|
67
|
+
task.count = (task.count || 0) + ffResult.skippedOccurrences;
|
|
68
|
+
currentNextRun = ffResult.nextRun;
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
}
|
|
84
72
|
|
|
85
|
-
|
|
73
|
+
// Buffer for collecting eligible slots when catchUpLimit is set
|
|
74
|
+
// This allows us to keep only the most recent N slots
|
|
75
|
+
const eligibleBuffer = [];
|
|
76
|
+
const needsBuffering = catchUpLimit !== "all" && catchUpLimit > 0;
|
|
77
|
+
|
|
78
|
+
while (currentNextRun && currentNextRun <= now && iterationCount < maxIterations) {
|
|
86
79
|
iterationCount++;
|
|
87
80
|
|
|
88
81
|
const lag = now.getTime() - currentNextRun.getTime();
|
|
89
82
|
|
|
90
|
-
// Determine if
|
|
91
|
-
const isInCurrentTick = currentNextRun > lastTick && currentNextRun <= now;
|
|
83
|
+
// Determine if this occurrence is eligible based on catchUpWindow
|
|
92
84
|
const isWithinWindow = catchUpWindow === "unlimited" || lag <= catchUpWindow;
|
|
93
85
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
if (isWithinWindow) {
|
|
87
|
+
if (needsBuffering) {
|
|
88
|
+
// Buffer this eligible slot
|
|
89
|
+
eligibleBuffer.push({
|
|
90
|
+
scheduledTime: new Date(currentNextRun),
|
|
91
|
+
count: task.count || 0
|
|
92
|
+
});
|
|
93
|
+
// Keep only the last N slots
|
|
94
|
+
if (eligibleBuffer.length > catchUpLimit) {
|
|
95
|
+
eligibleBuffer.shift();
|
|
96
|
+
}
|
|
97
|
+
} else if (catchUpLimit === "all") {
|
|
98
|
+
// Execute immediately - no limit
|
|
99
|
+
const event = this._executeTask(task, currentNextRun);
|
|
100
|
+
events.push(event);
|
|
101
|
+
}
|
|
102
|
+
// If catchUpLimit is 0, don't execute (real-time mode)
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
// Advance to next occurrence
|
|
103
106
|
const prevTime = currentNextRun.getTime();
|
|
104
|
-
this.
|
|
107
|
+
this._advanceTask(task);
|
|
105
108
|
|
|
106
|
-
// Check if
|
|
107
|
-
if (RecurrenceEngine.shouldStop(
|
|
108
|
-
newState.
|
|
109
|
+
// Check if task should stop
|
|
110
|
+
if (RecurrenceEngine.shouldStop(task)) {
|
|
111
|
+
newState.tasks.splice(i, 1);
|
|
109
112
|
i--;
|
|
110
113
|
break;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
// Update currentNextRun for next iteration
|
|
114
|
-
if (
|
|
115
|
-
currentNextRun = new Date(
|
|
117
|
+
if (task.date) {
|
|
118
|
+
currentNextRun = new Date(task.date);
|
|
116
119
|
|
|
117
120
|
// SAFETY: Ensure monotonic progression
|
|
118
121
|
if (currentNextRun.getTime() <= prevTime) {
|
|
119
122
|
events.push({
|
|
120
123
|
type: 'error',
|
|
121
|
-
message: `
|
|
122
|
-
|
|
124
|
+
message: `Task ${task.id} failed to advance time monotonically`,
|
|
125
|
+
taskId: task.id
|
|
123
126
|
});
|
|
124
127
|
// Force advancement
|
|
125
128
|
currentNextRun = new Date(prevTime + 1000);
|
|
126
|
-
|
|
129
|
+
task.date = currentNextRun;
|
|
127
130
|
}
|
|
128
131
|
} else {
|
|
129
132
|
// No more occurrences
|
|
@@ -131,12 +134,29 @@ class CoreEngine {
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// Execute buffered slots (if we were buffering)
|
|
138
|
+
if (needsBuffering && eligibleBuffer.length > 0) {
|
|
139
|
+
for (const slot of eligibleBuffer) {
|
|
140
|
+
const event = {
|
|
141
|
+
type: 'task',
|
|
142
|
+
taskId: task.id,
|
|
143
|
+
name: task.name,
|
|
144
|
+
cmd: task.cmd,
|
|
145
|
+
payload: task.payload,
|
|
146
|
+
scheduledTime: slot.scheduledTime,
|
|
147
|
+
actualTime: new Date(),
|
|
148
|
+
count: slot.count
|
|
149
|
+
};
|
|
150
|
+
events.push(event);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
// Safety check: iteration limit reached
|
|
135
155
|
if (iterationCount >= maxIterations) {
|
|
136
156
|
events.push({
|
|
137
157
|
type: 'error',
|
|
138
|
-
message: `
|
|
139
|
-
|
|
158
|
+
message: `Task ${task.id} exceeded max iterations (${maxIterations}) - possible infinite loop`,
|
|
159
|
+
taskId: task.id
|
|
140
160
|
});
|
|
141
161
|
}
|
|
142
162
|
}
|
|
@@ -149,137 +169,61 @@ class CoreEngine {
|
|
|
149
169
|
*/
|
|
150
170
|
static _cloneState(state) {
|
|
151
171
|
return {
|
|
152
|
-
|
|
172
|
+
tasks: state.tasks.map(task => ({ ...task }))
|
|
153
173
|
};
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
/**
|
|
157
|
-
*
|
|
158
|
-
*/
|
|
159
|
-
static _isCurrentTick(time, lastTick, now) {
|
|
160
|
-
return time > lastTick && time <= now;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Create an action execution event
|
|
177
|
+
* Create a task execution event
|
|
165
178
|
*/
|
|
166
|
-
static
|
|
179
|
+
static _executeTask(task, scheduledTime) {
|
|
167
180
|
return {
|
|
168
|
-
type: '
|
|
169
|
-
|
|
170
|
-
name:
|
|
171
|
-
cmd:
|
|
172
|
-
payload:
|
|
181
|
+
type: 'task',
|
|
182
|
+
taskId: task.id,
|
|
183
|
+
name: task.name,
|
|
184
|
+
cmd: task.cmd,
|
|
185
|
+
payload: task.payload,
|
|
173
186
|
scheduledTime: new Date(scheduledTime),
|
|
174
187
|
actualTime: new Date(),
|
|
175
|
-
count:
|
|
188
|
+
count: task.count || 0
|
|
176
189
|
};
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
/**
|
|
180
|
-
* Advance
|
|
193
|
+
* Advance a task to its next occurrence
|
|
181
194
|
*/
|
|
182
|
-
static
|
|
195
|
+
static _advanceTask(task) {
|
|
183
196
|
// Increment count
|
|
184
|
-
|
|
197
|
+
task.count = (task.count || 0) + 1;
|
|
185
198
|
|
|
186
199
|
// Calculate next run time
|
|
187
|
-
if (
|
|
188
|
-
const currentTime =
|
|
189
|
-
const dstPolicy =
|
|
200
|
+
if (task.repeat) {
|
|
201
|
+
const currentTime = task.date ? new Date(task.date) : new Date();
|
|
202
|
+
const dstPolicy = task.repeat.dstPolicy || 'once';
|
|
190
203
|
|
|
191
204
|
const nextTime = RecurrenceEngine.getNextOccurrence(
|
|
192
205
|
currentTime,
|
|
193
|
-
|
|
206
|
+
task.repeat,
|
|
194
207
|
dstPolicy
|
|
195
208
|
);
|
|
196
209
|
|
|
197
|
-
|
|
210
|
+
task.date = nextTime;
|
|
198
211
|
} else {
|
|
199
|
-
// One-time
|
|
200
|
-
|
|
212
|
+
// One-time task - clear date
|
|
213
|
+
task.date = null;
|
|
201
214
|
}
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
/**
|
|
205
|
-
* Fast-forward an action to near the current time using mathematical projection
|
|
206
|
-
* This avoids iterating through thousands/millions of occurrences for high-frequency tasks
|
|
207
|
-
*
|
|
208
|
-
* @param {Object} action - The action to fast-forward
|
|
209
|
-
* @param {Date} now - Current time
|
|
210
|
-
* @param {number} catchUpWindow - The catch-up window in milliseconds
|
|
211
|
-
* @returns {Object|null} - { nextRun, count } or null if not applicable
|
|
212
|
-
*/
|
|
213
|
-
static _fastForwardAction(action, now, catchUpWindow) {
|
|
214
|
-
if (!action.repeat || !action.date) {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const { type, interval = 1 } = action.repeat;
|
|
219
|
-
const currentTime = new Date(action.date);
|
|
220
|
-
const lag = now.getTime() - currentTime.getTime();
|
|
221
|
-
|
|
222
|
-
// Only applicable for simple time-based recurrence (not weekday/weekend)
|
|
223
|
-
let stepMilliseconds = 0;
|
|
224
|
-
|
|
225
|
-
switch (type) {
|
|
226
|
-
case 'second':
|
|
227
|
-
stepMilliseconds = interval * 1000;
|
|
228
|
-
break;
|
|
229
|
-
case 'minute':
|
|
230
|
-
stepMilliseconds = interval * 60 * 1000;
|
|
231
|
-
break;
|
|
232
|
-
case 'hour':
|
|
233
|
-
stepMilliseconds = interval * 60 * 60 * 1000;
|
|
234
|
-
break;
|
|
235
|
-
case 'day':
|
|
236
|
-
stepMilliseconds = interval * 24 * 60 * 60 * 1000;
|
|
237
|
-
break;
|
|
238
|
-
case 'week':
|
|
239
|
-
stepMilliseconds = interval * 7 * 24 * 60 * 60 * 1000;
|
|
240
|
-
break;
|
|
241
|
-
default:
|
|
242
|
-
// Complex recurrence (weekday, weekend, month, year) - can't fast-forward easily
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (stepMilliseconds === 0) {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Calculate how many steps we can skip
|
|
251
|
-
// We want to jump to just before the catch-up window starts
|
|
252
|
-
const targetLag = catchUpWindow;
|
|
253
|
-
const timeToSkip = lag - targetLag;
|
|
254
217
|
|
|
255
|
-
if (timeToSkip <= 0) {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const stepsToSkip = Math.floor(timeToSkip / stepMilliseconds);
|
|
260
|
-
|
|
261
|
-
if (stepsToSkip <= 0) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Project forward
|
|
266
|
-
const newTime = new Date(currentTime.getTime() + (stepsToSkip * stepMilliseconds));
|
|
267
|
-
const newCount = (action.count || 0) + stepsToSkip;
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
nextRun: newTime,
|
|
271
|
-
count: newCount
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
218
|
|
|
275
219
|
/**
|
|
276
|
-
* Simulate
|
|
220
|
+
* Simulate tasks in a time range without mutating state
|
|
277
221
|
*/
|
|
278
222
|
static simulate(state, startDate, endDate, maxIterations = 100000) {
|
|
279
223
|
const simulatedEvents = [];
|
|
280
224
|
const simulatedState = this._cloneState(state);
|
|
281
225
|
|
|
282
|
-
// Start simulation from the earliest
|
|
226
|
+
// Start simulation from the earliest task time or startDate
|
|
283
227
|
let currentTime = new Date(startDate);
|
|
284
228
|
|
|
285
229
|
// Align to second boundary
|
|
@@ -296,11 +240,11 @@ class CoreEngine {
|
|
|
296
240
|
iterationCount++;
|
|
297
241
|
|
|
298
242
|
const { newState, events } = this.step(simulatedState, lastTick, currentTime, maxIterations);
|
|
299
|
-
simulatedState.
|
|
243
|
+
simulatedState.tasks = newState.tasks;
|
|
300
244
|
|
|
301
|
-
// Collect
|
|
302
|
-
const
|
|
303
|
-
simulatedEvents.push(...
|
|
245
|
+
// Collect task events (filter out errors for cleaner simulation output)
|
|
246
|
+
const taskEvents = events.filter(e => e.type === 'task');
|
|
247
|
+
simulatedEvents.push(...taskEvents);
|
|
304
248
|
|
|
305
249
|
// Advance time by 1 second
|
|
306
250
|
lastTick = currentTime;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
class RecurrenceEngine {
|
|
9
9
|
/**
|
|
10
|
-
* Calculate the next occurrence time for
|
|
10
|
+
* Calculate the next occurrence time for a task based on its recurrence rule.
|
|
11
11
|
*
|
|
12
12
|
* CRITICAL INVARIANT: nextTime.getTime() > currentTime.getTime()
|
|
13
13
|
*
|
|
@@ -69,6 +69,67 @@ class RecurrenceEngine {
|
|
|
69
69
|
return next;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Fast-forward a task to a target time using mathematical projection.
|
|
74
|
+
* This is only applicable for simple, fixed-duration recurrence types.
|
|
75
|
+
*
|
|
76
|
+
* @param {Date} currentTime - The current scheduled time.
|
|
77
|
+
* @param {Object} repeat - The repeat configuration.
|
|
78
|
+
* @param {Date} targetTime - The time to project towards.
|
|
79
|
+
* @returns {Object|null} - { nextRun, skippedOccurrences } or null if not applicable.
|
|
80
|
+
*/
|
|
81
|
+
static fastForward(currentTime, repeat, targetTime) {
|
|
82
|
+
if (!repeat || !repeat.type) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { type, interval = 1 } = repeat;
|
|
87
|
+
const timeToSkip = targetTime.getTime() - currentTime.getTime();
|
|
88
|
+
|
|
89
|
+
if (timeToSkip <= 0) {
|
|
90
|
+
return { nextRun: currentTime, skippedOccurrences: 0 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let stepMilliseconds = 0;
|
|
94
|
+
switch (type) {
|
|
95
|
+
case 'second':
|
|
96
|
+
stepMilliseconds = interval * 1000;
|
|
97
|
+
break;
|
|
98
|
+
case 'minute':
|
|
99
|
+
stepMilliseconds = interval * 60 * 1000;
|
|
100
|
+
break;
|
|
101
|
+
case 'hour':
|
|
102
|
+
stepMilliseconds = interval * 60 * 60 * 1000;
|
|
103
|
+
break;
|
|
104
|
+
case 'day':
|
|
105
|
+
stepMilliseconds = interval * 24 * 60 * 60 * 1000;
|
|
106
|
+
break;
|
|
107
|
+
case 'week':
|
|
108
|
+
stepMilliseconds = interval * 7 * 24 * 60 * 60 * 1000;
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
// Complex recurrence types (weekday, weekend, month, year) cannot be fast-forwarded.
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (stepMilliseconds === 0) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stepsToSkip = Math.floor(timeToSkip / stepMilliseconds);
|
|
120
|
+
|
|
121
|
+
if (stepsToSkip <= 0) {
|
|
122
|
+
return { nextRun: currentTime, skippedOccurrences: 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const newTime = new Date(currentTime.getTime() + (stepsToSkip * stepMilliseconds));
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
nextRun: newTime,
|
|
129
|
+
skippedOccurrences: stepsToSkip,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
72
133
|
/**
|
|
73
134
|
* Add seconds to a date
|
|
74
135
|
*/
|
|
@@ -206,16 +267,16 @@ class RecurrenceEngine {
|
|
|
206
267
|
}
|
|
207
268
|
|
|
208
269
|
/**
|
|
209
|
-
* Check if
|
|
270
|
+
* Check if a task should stop based on limit or endDate
|
|
210
271
|
*/
|
|
211
|
-
static shouldStop(
|
|
212
|
-
const { repeat } =
|
|
272
|
+
static shouldStop(task) {
|
|
273
|
+
const { repeat } = task;
|
|
213
274
|
|
|
214
275
|
if (!repeat) return true;
|
|
215
276
|
|
|
216
277
|
// Check count limit
|
|
217
278
|
if (repeat.limit !== null && repeat.limit !== undefined) {
|
|
218
|
-
const count =
|
|
279
|
+
const count = task.count || 0;
|
|
219
280
|
if (count >= repeat.limit) {
|
|
220
281
|
return true;
|
|
221
282
|
}
|
|
@@ -224,7 +285,7 @@ class RecurrenceEngine {
|
|
|
224
285
|
// Check end date
|
|
225
286
|
if (repeat.endDate) {
|
|
226
287
|
const endDate = new Date(repeat.endDate);
|
|
227
|
-
const nextRun = new Date(
|
|
288
|
+
const nextRun = new Date(task.date);
|
|
228
289
|
if (nextRun > endDate) {
|
|
229
290
|
return true;
|
|
230
291
|
}
|
|
@@ -11,11 +11,12 @@ const CoreEngine = require('../core/CoreEngine');
|
|
|
11
11
|
class SchedulerHost extends EventEmitter {
|
|
12
12
|
constructor() {
|
|
13
13
|
super();
|
|
14
|
-
this.state = {
|
|
14
|
+
this.state = { tasks: [] };
|
|
15
15
|
this.running = false;
|
|
16
16
|
this.lastTick = null;
|
|
17
17
|
this.timer = null;
|
|
18
18
|
this.functions = new Map();
|
|
19
|
+
this.bootMode = true;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -27,8 +28,48 @@ class SchedulerHost extends EventEmitter {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
this.running = true;
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const now = new Date();
|
|
32
|
+
now.setMilliseconds(0);
|
|
33
|
+
|
|
34
|
+
// Boot sweep - advance state to current time without executing tasks
|
|
35
|
+
if (this.bootMode) {
|
|
36
|
+
try {
|
|
37
|
+
// Boot sweep: advance state from lastTick to now without executing tasks
|
|
38
|
+
// If lastTick is null (first start), use now (no catch-up needed)
|
|
39
|
+
const { newState, events } = CoreEngine.step(this.state, this.lastTick || now, now);
|
|
40
|
+
|
|
41
|
+
// Update state (tasks are now advanced to current time)
|
|
42
|
+
this.state = newState;
|
|
43
|
+
|
|
44
|
+
// Process events to ensure state consistency, but don't execute callbacks
|
|
45
|
+
// The bootMode flag will prevent _executeTaskEvent from calling functions
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
if (event.type === 'task') {
|
|
48
|
+
this._executeTaskEvent(event);
|
|
49
|
+
} else if (event.type === 'error') {
|
|
50
|
+
this.emit('error', event);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exit boot mode
|
|
55
|
+
this.bootMode = false;
|
|
56
|
+
|
|
57
|
+
// Signal to Automator that state should be saved
|
|
58
|
+
this.emit('boot-complete');
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.emit('error', {
|
|
62
|
+
type: 'error',
|
|
63
|
+
message: `Boot sweep error: ${error.message}`,
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
// Exit boot mode even on error to allow normal operation
|
|
67
|
+
this.bootMode = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Set lastTick after boot sweep completes
|
|
72
|
+
this.lastTick = now;
|
|
32
73
|
|
|
33
74
|
this._scheduleTick();
|
|
34
75
|
this.emit('ready');
|
|
@@ -102,8 +143,8 @@ class SchedulerHost extends EventEmitter {
|
|
|
102
143
|
|
|
103
144
|
// Process events
|
|
104
145
|
for (const event of events) {
|
|
105
|
-
if (event.type === '
|
|
106
|
-
this.
|
|
146
|
+
if (event.type === 'task') {
|
|
147
|
+
this._executeTaskEvent(event);
|
|
107
148
|
} else if (event.type === 'error') {
|
|
108
149
|
this.emit('error', event);
|
|
109
150
|
}
|
|
@@ -121,11 +162,16 @@ class SchedulerHost extends EventEmitter {
|
|
|
121
162
|
}
|
|
122
163
|
|
|
123
164
|
/**
|
|
124
|
-
* Execute
|
|
165
|
+
* Execute a task event by calling its registered function
|
|
125
166
|
*/
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
this.
|
|
167
|
+
_executeTaskEvent(event) {
|
|
168
|
+
// Skip execution during boot mode
|
|
169
|
+
if (this.bootMode) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Emit the task event
|
|
174
|
+
this.emit('task', event);
|
|
129
175
|
|
|
130
176
|
// Execute the command function if registered
|
|
131
177
|
const fn = this.functions.get(event.cmd);
|
|
@@ -136,7 +182,7 @@ class SchedulerHost extends EventEmitter {
|
|
|
136
182
|
this.emit('error', {
|
|
137
183
|
type: 'error',
|
|
138
184
|
message: `Error executing command ${event.cmd}: ${error.message}`,
|
|
139
|
-
|
|
185
|
+
taskId: event.taskId,
|
|
140
186
|
error
|
|
141
187
|
});
|
|
142
188
|
}
|
|
@@ -144,16 +190,16 @@ class SchedulerHost extends EventEmitter {
|
|
|
144
190
|
this.emit('debug', {
|
|
145
191
|
type: 'debug',
|
|
146
192
|
message: `No function registered for command: ${event.cmd}`,
|
|
147
|
-
|
|
193
|
+
taskId: event.taskId
|
|
148
194
|
});
|
|
149
195
|
}
|
|
150
196
|
}
|
|
151
197
|
|
|
152
198
|
/**
|
|
153
|
-
* Add
|
|
199
|
+
* Add a task to the state
|
|
154
200
|
*/
|
|
155
|
-
|
|
156
|
-
this.state.
|
|
201
|
+
addTask(task) {
|
|
202
|
+
this.state.tasks.push(task);
|
|
157
203
|
}
|
|
158
204
|
|
|
159
205
|
/**
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileStorage.js
|
|
3
|
-
*
|
|
4
|
-
* Pluggable file-based storage adapter
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
class FileStorage {
|
|
11
|
-
constructor(filePath) {
|
|
12
|
-
this.filePath = path.resolve(filePath);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Load state from file
|
|
17
|
-
*/
|
|
18
|
-
load() {
|
|
19
|
-
try {
|
|
20
|
-
if (fs.existsSync(this.filePath)) {
|
|
21
|
-
const data = fs.readFileSync(this.filePath, 'utf8');
|
|
22
|
-
const state = JSON.parse(data);
|
|
23
|
-
|
|
24
|
-
// Convert date strings back to Date objects
|
|
25
|
-
if (state.actions) {
|
|
26
|
-
state.actions = state.actions.map(action => {
|
|
27
|
-
if (action.date) {
|
|
28
|
-
action.date = new Date(action.date);
|
|
29
|
-
}
|
|
30
|
-
if (action.repeat && action.repeat.endDate) {
|
|
31
|
-
action.repeat.endDate = new Date(action.repeat.endDate);
|
|
32
|
-
}
|
|
33
|
-
return action;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return state;
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
throw new Error(`Failed to load from ${this.filePath}: ${error.message}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { actions: [] };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Save state to file
|
|
48
|
-
*/
|
|
49
|
-
save(state) {
|
|
50
|
-
try {
|
|
51
|
-
const data = JSON.stringify(state, null, 2);
|
|
52
|
-
fs.writeFileSync(this.filePath, data, 'utf8');
|
|
53
|
-
} catch (error) {
|
|
54
|
-
throw new Error(`Failed to save to ${this.filePath}: ${error.message}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
module.exports = FileStorage;
|