@geckom/opencode-queue 0.1.2
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/block-watcher.js +137 -0
- package/dist/constants.js +25 -0
- package/dist/file-lock.js +106 -0
- package/dist/formatters.js +108 -0
- package/dist/idle-detector.js +49 -0
- package/dist/index.js +1 -0
- package/dist/opencode-queue.js +1 -0
- package/dist/plugin.js +370 -0
- package/dist/queue-manager.js +438 -0
- package/dist/queue-processor.js +273 -0
- package/dist/schedule-manager.js +126 -0
- package/dist/session-greeter.js +81 -0
- package/dist/shared-state.js +105 -0
- package/dist/toast.js +11 -0
- package/dist/types.js +5 -0
- package/dist/utils.js +29 -0
- package/package.json +58 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { CronJob } from "cron";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { DEFAULT_CONFIG, LOCK_FILE, OPENCODE_DIR, PROCESSING_LOCK_STALE_MS, QUEUE_CORRUPTION_MARKER_FILE, QUEUE_FILE, STORE_LOCK_FILE, STORE_LOCK_RETRY_MS, STORE_LOCK_STALE_MS, STORE_LOCK_WAIT_MS, } from "./constants.js";
|
|
6
|
+
import { FileLock } from "./file-lock.js";
|
|
7
|
+
import { createQueueItemFromSchedule } from "./utils.js";
|
|
8
|
+
/**
|
|
9
|
+
* QueueManager owns queue.json persistence, normalization, and all state
|
|
10
|
+
* transitions that must be serialized across processes.
|
|
11
|
+
*/
|
|
12
|
+
export class QueueManager {
|
|
13
|
+
clearCorruptionMarker() {
|
|
14
|
+
try {
|
|
15
|
+
if (existsSync(QUEUE_CORRUPTION_MARKER_FILE)) {
|
|
16
|
+
unlinkSync(QUEUE_CORRUPTION_MARKER_FILE);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
handleCorruptedStore(error) {
|
|
22
|
+
if (!existsSync(OPENCODE_DIR)) {
|
|
23
|
+
mkdirSync(OPENCODE_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
let backupPath = null;
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(QUEUE_CORRUPTION_MARKER_FILE)) {
|
|
28
|
+
const marker = JSON.parse(readFileSync(QUEUE_CORRUPTION_MARKER_FILE, "utf-8"));
|
|
29
|
+
if (typeof marker.backupPath === "string") {
|
|
30
|
+
backupPath = marker.backupPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
if (!backupPath) {
|
|
36
|
+
backupPath = `${QUEUE_FILE}.corrupt-${Date.now()}`;
|
|
37
|
+
copyFileSync(QUEUE_FILE, backupPath);
|
|
38
|
+
writeFileSync(QUEUE_CORRUPTION_MARKER_FILE, JSON.stringify({
|
|
39
|
+
detectedAt: new Date().toISOString(),
|
|
40
|
+
backupPath,
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
}, null, 2), "utf-8");
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Queue store is corrupted: ${QUEUE_FILE}. Backup preserved at ${backupPath}. Repair or replace queue.json before continuing.`);
|
|
45
|
+
}
|
|
46
|
+
normalizeItem(item) {
|
|
47
|
+
return {
|
|
48
|
+
id: String(item.id || randomUUID()),
|
|
49
|
+
workspace: String(item.workspace || ""),
|
|
50
|
+
goal: String(item.goal || ""),
|
|
51
|
+
status: item.status || "pending",
|
|
52
|
+
parentItemId: item.parentItemId ?? null,
|
|
53
|
+
dependencyMode: item.dependencyMode === "completed" ? "completed" : "review_pending",
|
|
54
|
+
dependencySatisfiedAt: item.dependencySatisfiedAt ?? null,
|
|
55
|
+
dependencySourceStatus: item.dependencySourceStatus === "completed" || item.dependencySourceStatus === "review_pending"
|
|
56
|
+
? item.dependencySourceStatus
|
|
57
|
+
: null,
|
|
58
|
+
dependencyBlockedReason: item.dependencyBlockedReason ?? null,
|
|
59
|
+
staleDependency: Boolean(item.staleDependency),
|
|
60
|
+
sessionId: item.sessionId ?? null,
|
|
61
|
+
createdAt: String(item.createdAt || new Date().toISOString()),
|
|
62
|
+
startedAt: item.startedAt ?? null,
|
|
63
|
+
completedAt: item.completedAt ?? null,
|
|
64
|
+
reviewedAt: item.reviewedAt ?? null,
|
|
65
|
+
blockedReason: item.blockedReason ?? null,
|
|
66
|
+
error: item.error ?? null,
|
|
67
|
+
result: item.result ?? null,
|
|
68
|
+
sessionUrl: item.sessionUrl ?? null,
|
|
69
|
+
retryCount: typeof item.retryCount === "number" ? item.retryCount : 0,
|
|
70
|
+
nextRetryAt: item.nextRetryAt ?? null,
|
|
71
|
+
followupMessage: item.followupMessage ?? null,
|
|
72
|
+
sourceScheduleId: item.sourceScheduleId ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
normalizeSchedule(schedule) {
|
|
76
|
+
return {
|
|
77
|
+
id: String(schedule.id || randomUUID()),
|
|
78
|
+
workspace: String(schedule.workspace || ""),
|
|
79
|
+
goal: String(schedule.goal || ""),
|
|
80
|
+
scheduledFor: schedule.scheduledFor ?? null,
|
|
81
|
+
cronExpression: schedule.cronExpression ?? null,
|
|
82
|
+
timezone: String(schedule.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
83
|
+
enabled: Boolean(schedule.enabled),
|
|
84
|
+
lastTriggeredAt: schedule.lastTriggeredAt ?? null,
|
|
85
|
+
nextTriggerAt: schedule.nextTriggerAt ?? null,
|
|
86
|
+
occurrenceCount: typeof schedule.occurrenceCount === "number" ? schedule.occurrenceCount : 0,
|
|
87
|
+
maxOccurrences: typeof schedule.maxOccurrences === "number" ? schedule.maxOccurrences : null,
|
|
88
|
+
parentItemId: schedule.parentItemId ?? null,
|
|
89
|
+
dependencyMode: schedule.dependencyMode === "completed" ? "completed" : "review_pending",
|
|
90
|
+
createdAt: String(schedule.createdAt || new Date().toISOString()),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
normalizeConfig(config) {
|
|
94
|
+
return {
|
|
95
|
+
...DEFAULT_CONFIG,
|
|
96
|
+
...(config ?? {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
readStore() {
|
|
100
|
+
if (!existsSync(QUEUE_FILE)) {
|
|
101
|
+
this.clearCorruptionMarker();
|
|
102
|
+
return { config: { ...DEFAULT_CONFIG }, items: [], schedules: [] };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const raw = readFileSync(QUEUE_FILE, "utf-8");
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
this.clearCorruptionMarker();
|
|
108
|
+
return {
|
|
109
|
+
config: this.normalizeConfig(parsed.config),
|
|
110
|
+
items: Array.isArray(parsed.items) ? parsed.items.map((item) => this.normalizeItem(item)) : [],
|
|
111
|
+
schedules: Array.isArray(parsed.schedules) ? parsed.schedules.map((schedule) => this.normalizeSchedule(schedule)) : [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.handleCorruptedStore(error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
writeStore(store) {
|
|
119
|
+
if (!existsSync(OPENCODE_DIR)) {
|
|
120
|
+
mkdirSync(OPENCODE_DIR, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
const tmp = QUEUE_FILE + ".tmp";
|
|
123
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
124
|
+
renameSync(tmp, QUEUE_FILE);
|
|
125
|
+
}
|
|
126
|
+
getNextCronTrigger(schedule) {
|
|
127
|
+
if (!schedule.cronExpression)
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
const job = new CronJob(schedule.cronExpression, () => { }, undefined, false, schedule.timezone);
|
|
131
|
+
const nextDate = job.nextDate();
|
|
132
|
+
job.stop();
|
|
133
|
+
return nextDate ? nextDate.toISO() : null;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
wouldCreateDependencyCycle(itemId, parentItemId, items) {
|
|
140
|
+
if (!parentItemId)
|
|
141
|
+
return false;
|
|
142
|
+
let currentId = parentItemId;
|
|
143
|
+
const visited = new Set();
|
|
144
|
+
while (currentId) {
|
|
145
|
+
if (currentId === itemId)
|
|
146
|
+
return true;
|
|
147
|
+
if (visited.has(currentId))
|
|
148
|
+
return true;
|
|
149
|
+
visited.add(currentId);
|
|
150
|
+
const parent = items.find((candidate) => candidate.id === currentId);
|
|
151
|
+
currentId = parent?.parentItemId ?? null;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
evaluateDependency(item, items) {
|
|
156
|
+
if (!item.parentItemId) {
|
|
157
|
+
return { eligible: true, blockedReason: null, sourceStatus: null };
|
|
158
|
+
}
|
|
159
|
+
const parent = items.find((candidate) => candidate.id === item.parentItemId);
|
|
160
|
+
if (!parent) {
|
|
161
|
+
return { eligible: false, blockedReason: `Parent item ${item.parentItemId} not found.`, sourceStatus: null };
|
|
162
|
+
}
|
|
163
|
+
if (parent.status === "completed") {
|
|
164
|
+
return { eligible: true, blockedReason: null, sourceStatus: "completed" };
|
|
165
|
+
}
|
|
166
|
+
if (parent.status === "review_pending" && item.dependencyMode === "review_pending") {
|
|
167
|
+
return { eligible: true, blockedReason: null, sourceStatus: "review_pending" };
|
|
168
|
+
}
|
|
169
|
+
if (parent.status === "review_pending") {
|
|
170
|
+
return { eligible: false, blockedReason: `Waiting for parent ${parent.id} to be completed.`, sourceStatus: null };
|
|
171
|
+
}
|
|
172
|
+
if (parent.status === "failed") {
|
|
173
|
+
return { eligible: false, blockedReason: `Parent ${parent.id} failed.`, sourceStatus: null };
|
|
174
|
+
}
|
|
175
|
+
if (parent.status === "blocked") {
|
|
176
|
+
return { eligible: false, blockedReason: `Parent ${parent.id} is blocked.`, sourceStatus: null };
|
|
177
|
+
}
|
|
178
|
+
if (parent.status === "running") {
|
|
179
|
+
return { eligible: false, blockedReason: `Parent ${parent.id} is running.`, sourceStatus: null };
|
|
180
|
+
}
|
|
181
|
+
return { eligible: false, blockedReason: `Waiting for parent ${parent.id} to start.`, sourceStatus: null };
|
|
182
|
+
}
|
|
183
|
+
async mutateStore(work) {
|
|
184
|
+
return FileLock.withLock(STORE_LOCK_FILE, STORE_LOCK_STALE_MS, STORE_LOCK_RETRY_MS, STORE_LOCK_WAIT_MS, async () => {
|
|
185
|
+
const store = this.readStore();
|
|
186
|
+
const result = work(store);
|
|
187
|
+
this.writeStore(store);
|
|
188
|
+
return result;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
getConfig() {
|
|
192
|
+
return this.readStore().config;
|
|
193
|
+
}
|
|
194
|
+
async updateConfig(updates) {
|
|
195
|
+
return this.mutateStore((store) => {
|
|
196
|
+
store.config = this.normalizeConfig({
|
|
197
|
+
...store.config,
|
|
198
|
+
...updates,
|
|
199
|
+
});
|
|
200
|
+
return store.config;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
listItems(status) {
|
|
204
|
+
const store = this.readStore();
|
|
205
|
+
return status ? store.items.filter((item) => item.status === status) : store.items;
|
|
206
|
+
}
|
|
207
|
+
getItem(id) {
|
|
208
|
+
return this.readStore().items.find((item) => item.id === id);
|
|
209
|
+
}
|
|
210
|
+
async addItem(workspace, goal, options) {
|
|
211
|
+
const absWorkspace = resolve(workspace);
|
|
212
|
+
if (!existsSync(absWorkspace)) {
|
|
213
|
+
return { error: `Directory not found: ${absWorkspace}` };
|
|
214
|
+
}
|
|
215
|
+
if (!statSync(absWorkspace).isDirectory()) {
|
|
216
|
+
return { error: `Path is not a directory: ${absWorkspace}` };
|
|
217
|
+
}
|
|
218
|
+
return this.mutateStore((store) => {
|
|
219
|
+
const parentItemId = options?.parentItemId ?? null;
|
|
220
|
+
if (parentItemId && !store.items.some((item) => item.id === parentItemId)) {
|
|
221
|
+
return { error: `Parent item not found: ${parentItemId}` };
|
|
222
|
+
}
|
|
223
|
+
const item = {
|
|
224
|
+
id: randomUUID(),
|
|
225
|
+
workspace: absWorkspace,
|
|
226
|
+
goal,
|
|
227
|
+
status: "pending",
|
|
228
|
+
parentItemId,
|
|
229
|
+
dependencyMode: options?.dependencyMode === "completed" ? "completed" : "review_pending",
|
|
230
|
+
dependencySatisfiedAt: null,
|
|
231
|
+
dependencySourceStatus: null,
|
|
232
|
+
dependencyBlockedReason: parentItemId ? `Waiting for parent ${parentItemId} to start.` : null,
|
|
233
|
+
staleDependency: false,
|
|
234
|
+
sessionId: null,
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
startedAt: null,
|
|
237
|
+
completedAt: null,
|
|
238
|
+
reviewedAt: null,
|
|
239
|
+
blockedReason: null,
|
|
240
|
+
error: null,
|
|
241
|
+
result: null,
|
|
242
|
+
sessionUrl: null,
|
|
243
|
+
retryCount: 0,
|
|
244
|
+
nextRetryAt: null,
|
|
245
|
+
followupMessage: null,
|
|
246
|
+
sourceScheduleId: options?.sourceScheduleId ?? null,
|
|
247
|
+
};
|
|
248
|
+
if (this.wouldCreateDependencyCycle(item.id, item.parentItemId, store.items)) {
|
|
249
|
+
return { error: `Dependency cycle detected for parent ${item.parentItemId}.` };
|
|
250
|
+
}
|
|
251
|
+
if (options?.prepend)
|
|
252
|
+
store.items.unshift(item);
|
|
253
|
+
else
|
|
254
|
+
store.items.push(item);
|
|
255
|
+
return item;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async updateItem(id, updates) {
|
|
259
|
+
return this.mutateStore((store) => {
|
|
260
|
+
const idx = store.items.findIndex((item) => item.id === id);
|
|
261
|
+
if (idx === -1)
|
|
262
|
+
return undefined;
|
|
263
|
+
Object.assign(store.items[idx], updates);
|
|
264
|
+
return store.items[idx];
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async updateItemAndMoveToFront(id, updates) {
|
|
268
|
+
return this.mutateStore((store) => {
|
|
269
|
+
const idx = store.items.findIndex((item) => item.id === id);
|
|
270
|
+
if (idx === -1)
|
|
271
|
+
return undefined;
|
|
272
|
+
Object.assign(store.items[idx], updates);
|
|
273
|
+
const [item] = store.items.splice(idx, 1);
|
|
274
|
+
store.items.unshift(item);
|
|
275
|
+
return item;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async removeItem(id) {
|
|
279
|
+
return this.mutateStore((store) => {
|
|
280
|
+
const idx = store.items.findIndex((item) => item.id === id);
|
|
281
|
+
if (idx === -1)
|
|
282
|
+
return false;
|
|
283
|
+
store.items.splice(idx, 1);
|
|
284
|
+
return true;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async markDescendantsStale(parentItemId) {
|
|
288
|
+
await this.mutateStore((store) => {
|
|
289
|
+
const descendants = new Set();
|
|
290
|
+
const queue = [parentItemId];
|
|
291
|
+
while (queue.length > 0) {
|
|
292
|
+
const current = queue.shift();
|
|
293
|
+
for (const item of store.items) {
|
|
294
|
+
if (item.parentItemId === current && !descendants.has(item.id)) {
|
|
295
|
+
descendants.add(item.id);
|
|
296
|
+
queue.push(item.id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const item of store.items) {
|
|
301
|
+
if (!descendants.has(item.id))
|
|
302
|
+
continue;
|
|
303
|
+
if (!["running", "review_pending", "completed", "blocked"].includes(item.status))
|
|
304
|
+
continue;
|
|
305
|
+
item.staleDependency = true;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async getNextPending() {
|
|
310
|
+
return FileLock.withLock(STORE_LOCK_FILE, STORE_LOCK_STALE_MS, STORE_LOCK_RETRY_MS, STORE_LOCK_WAIT_MS, async () => {
|
|
311
|
+
const store = this.readStore();
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
let changed = false;
|
|
314
|
+
let nextItem;
|
|
315
|
+
for (const item of store.items) {
|
|
316
|
+
const nextRetryAt = item.nextRetryAt ? new Date(item.nextRetryAt).getTime() : null;
|
|
317
|
+
const isReadyPending = item.status === "pending" && (nextRetryAt === null || nextRetryAt <= now);
|
|
318
|
+
const isLegacyReadyRetry = item.status === "running" && nextRetryAt !== null && nextRetryAt <= now;
|
|
319
|
+
if (!isReadyPending && !isLegacyReadyRetry)
|
|
320
|
+
continue;
|
|
321
|
+
const dependency = this.evaluateDependency(item, store.items);
|
|
322
|
+
if (item.dependencyBlockedReason !== dependency.blockedReason) {
|
|
323
|
+
item.dependencyBlockedReason = dependency.blockedReason;
|
|
324
|
+
changed = true;
|
|
325
|
+
}
|
|
326
|
+
if (!dependency.eligible)
|
|
327
|
+
continue;
|
|
328
|
+
if (item.dependencySourceStatus !== dependency.sourceStatus) {
|
|
329
|
+
item.dependencySourceStatus = dependency.sourceStatus;
|
|
330
|
+
changed = true;
|
|
331
|
+
}
|
|
332
|
+
if (dependency.sourceStatus && !item.dependencySatisfiedAt) {
|
|
333
|
+
item.dependencySatisfiedAt = new Date().toISOString();
|
|
334
|
+
changed = true;
|
|
335
|
+
}
|
|
336
|
+
if (item.dependencyBlockedReason !== null) {
|
|
337
|
+
item.dependencyBlockedReason = null;
|
|
338
|
+
changed = true;
|
|
339
|
+
}
|
|
340
|
+
nextItem = { ...item };
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
if (changed)
|
|
344
|
+
this.writeStore(store);
|
|
345
|
+
return nextItem;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
countByStatus() {
|
|
349
|
+
const store = this.readStore();
|
|
350
|
+
const counts = {};
|
|
351
|
+
for (const item of store.items) {
|
|
352
|
+
counts[item.status] = (counts[item.status] || 0) + 1;
|
|
353
|
+
}
|
|
354
|
+
return counts;
|
|
355
|
+
}
|
|
356
|
+
async resetRunningToPending() {
|
|
357
|
+
if (FileLock.isFresh(LOCK_FILE, PROCESSING_LOCK_STALE_MS))
|
|
358
|
+
return;
|
|
359
|
+
await this.mutateStore((store) => {
|
|
360
|
+
for (const item of store.items) {
|
|
361
|
+
if (item.status === "running") {
|
|
362
|
+
item.status = "pending";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
listSchedules() {
|
|
368
|
+
return this.readStore().schedules;
|
|
369
|
+
}
|
|
370
|
+
getSchedule(id) {
|
|
371
|
+
return this.readStore().schedules.find((schedule) => schedule.id === id);
|
|
372
|
+
}
|
|
373
|
+
async addSchedule(schedule) {
|
|
374
|
+
const task = {
|
|
375
|
+
...schedule,
|
|
376
|
+
id: randomUUID(),
|
|
377
|
+
lastTriggeredAt: null,
|
|
378
|
+
nextTriggerAt: null,
|
|
379
|
+
occurrenceCount: 0,
|
|
380
|
+
createdAt: new Date().toISOString(),
|
|
381
|
+
};
|
|
382
|
+
await this.mutateStore((store) => {
|
|
383
|
+
store.schedules.push(task);
|
|
384
|
+
});
|
|
385
|
+
return task;
|
|
386
|
+
}
|
|
387
|
+
async updateSchedule(id, updates) {
|
|
388
|
+
return this.mutateStore((store) => {
|
|
389
|
+
const idx = store.schedules.findIndex((schedule) => schedule.id === id);
|
|
390
|
+
if (idx === -1)
|
|
391
|
+
return undefined;
|
|
392
|
+
Object.assign(store.schedules[idx], updates);
|
|
393
|
+
return store.schedules[idx];
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async removeSchedule(id) {
|
|
397
|
+
return this.mutateStore((store) => {
|
|
398
|
+
const idx = store.schedules.findIndex((schedule) => schedule.id === id);
|
|
399
|
+
if (idx === -1)
|
|
400
|
+
return false;
|
|
401
|
+
store.schedules.splice(idx, 1);
|
|
402
|
+
return true;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async triggerSchedule(scheduleId) {
|
|
406
|
+
return this.mutateStore((store) => {
|
|
407
|
+
const schedule = store.schedules.find((candidate) => candidate.id === scheduleId);
|
|
408
|
+
if (!schedule || !schedule.enabled)
|
|
409
|
+
return null;
|
|
410
|
+
const now = new Date();
|
|
411
|
+
const triggerAt = schedule.nextTriggerAt || schedule.scheduledFor;
|
|
412
|
+
if (triggerAt) {
|
|
413
|
+
const triggerMs = new Date(triggerAt).getTime();
|
|
414
|
+
if (!Number.isNaN(triggerMs) && triggerMs > now.getTime()) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
schedule.lastTriggeredAt = now.toISOString();
|
|
419
|
+
schedule.occurrenceCount += 1;
|
|
420
|
+
if (schedule.maxOccurrences !== null && schedule.occurrenceCount >= schedule.maxOccurrences) {
|
|
421
|
+
schedule.enabled = false;
|
|
422
|
+
}
|
|
423
|
+
if (schedule.scheduledFor) {
|
|
424
|
+
schedule.enabled = false;
|
|
425
|
+
schedule.nextTriggerAt = null;
|
|
426
|
+
}
|
|
427
|
+
else if (schedule.cronExpression && schedule.enabled) {
|
|
428
|
+
schedule.nextTriggerAt = this.getNextCronTrigger(schedule);
|
|
429
|
+
}
|
|
430
|
+
else if (!schedule.enabled) {
|
|
431
|
+
schedule.nextTriggerAt = null;
|
|
432
|
+
}
|
|
433
|
+
const item = createQueueItemFromSchedule(schedule, scheduleId);
|
|
434
|
+
store.items.unshift(item);
|
|
435
|
+
return { schedule: { ...schedule }, itemId: item.id };
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|