@sentry/junior-scheduler 0.107.1 → 0.109.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/README.md +10 -1
- package/dist/cadence.d.ts +12 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +553 -524
- package/dist/schedule-intent.d.ts +61 -0
- package/dist/store.d.ts +1 -10
- package/dist/tool-support.d.ts +8 -31
- package/dist/tools/create-task.d.ts +23 -5
- package/dist/tools/update-task.d.ts +23 -4
- package/package.json +2 -2
- package/src/cadence.ts +228 -304
- package/src/index.ts +0 -1
- package/src/plugin.ts +0 -7
- package/src/schedule-intent.ts +372 -0
- package/src/store.ts +35 -94
- package/src/tool-support.ts +25 -143
- package/src/tools/create-task.ts +66 -69
- package/src/tools/update-task.ts +59 -71
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns model-facing schedule intent and materializes it against the trusted
|
|
3
|
+
* server clock into the canonical schedule persisted by the scheduler.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
getFirstRunAtMs,
|
|
8
|
+
getZonedDateTimeParts,
|
|
9
|
+
resolveLocalScheduleAtMs,
|
|
10
|
+
} from "./cadence";
|
|
11
|
+
import type {
|
|
12
|
+
ScheduledLocalTime,
|
|
13
|
+
ScheduledTaskRecurrence,
|
|
14
|
+
ScheduledTaskSchedule,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
const localDateSchema = z
|
|
18
|
+
.string()
|
|
19
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Use a local date in YYYY-MM-DD format.");
|
|
20
|
+
const localTimeSchema = z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use local time in HH:MM format.");
|
|
23
|
+
const timezoneSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(80)
|
|
27
|
+
.describe(
|
|
28
|
+
"IANA timezone, for example America/Los_Angeles. Omit or use null for the scheduler default.",
|
|
29
|
+
)
|
|
30
|
+
.nullable()
|
|
31
|
+
.optional();
|
|
32
|
+
const weekdaySchema = z.enum([
|
|
33
|
+
"sunday",
|
|
34
|
+
"monday",
|
|
35
|
+
"tuesday",
|
|
36
|
+
"wednesday",
|
|
37
|
+
"thursday",
|
|
38
|
+
"friday",
|
|
39
|
+
"saturday",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const oneOffScheduleIntentSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
kind: z.literal("one_off").describe("A schedule that runs once."),
|
|
45
|
+
timezone: timezoneSchema,
|
|
46
|
+
timing: z
|
|
47
|
+
.discriminatedUnion("type", [
|
|
48
|
+
z
|
|
49
|
+
.object({
|
|
50
|
+
type: z
|
|
51
|
+
.literal("after")
|
|
52
|
+
.describe("Run after a relative delay from the server clock."),
|
|
53
|
+
value: z.number().int().positive().max(100_000),
|
|
54
|
+
unit: z.enum(["second", "minute", "hour", "day", "week"]),
|
|
55
|
+
})
|
|
56
|
+
.strict(),
|
|
57
|
+
z
|
|
58
|
+
.object({
|
|
59
|
+
type: z
|
|
60
|
+
.literal("at")
|
|
61
|
+
.describe("Run at an explicit local calendar date and time."),
|
|
62
|
+
date: localDateSchema.describe(
|
|
63
|
+
"Requested local calendar date in YYYY-MM-DD format.",
|
|
64
|
+
),
|
|
65
|
+
time: localTimeSchema.describe(
|
|
66
|
+
"Requested local wall-clock time in HH:MM format.",
|
|
67
|
+
),
|
|
68
|
+
})
|
|
69
|
+
.strict(),
|
|
70
|
+
])
|
|
71
|
+
.describe("Relative delay or explicit local time for the one-off run."),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
|
|
75
|
+
const recurringScheduleIntentSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
kind: z.literal("recurring").describe("A repeating calendar schedule."),
|
|
78
|
+
frequency: z
|
|
79
|
+
.enum(["daily", "weekly", "monthly", "yearly"])
|
|
80
|
+
.describe("Recurring tasks can run at most once per day."),
|
|
81
|
+
interval: z
|
|
82
|
+
.number()
|
|
83
|
+
.int()
|
|
84
|
+
.positive()
|
|
85
|
+
.max(365)
|
|
86
|
+
.describe("Repeat every N frequency units. Defaults to 1.")
|
|
87
|
+
.nullable()
|
|
88
|
+
.optional(),
|
|
89
|
+
time: localTimeSchema.describe(
|
|
90
|
+
"Local wall-clock time for each occurrence in HH:MM format.",
|
|
91
|
+
),
|
|
92
|
+
weekdays: z
|
|
93
|
+
.array(weekdaySchema)
|
|
94
|
+
.min(1)
|
|
95
|
+
.max(7)
|
|
96
|
+
.describe("Required for weekly schedules.")
|
|
97
|
+
.nullable()
|
|
98
|
+
.optional(),
|
|
99
|
+
day_of_month: z
|
|
100
|
+
.number()
|
|
101
|
+
.int()
|
|
102
|
+
.min(1)
|
|
103
|
+
.max(31)
|
|
104
|
+
.describe("Required for monthly and yearly schedules.")
|
|
105
|
+
.nullable()
|
|
106
|
+
.optional(),
|
|
107
|
+
month: z
|
|
108
|
+
.number()
|
|
109
|
+
.int()
|
|
110
|
+
.min(1)
|
|
111
|
+
.max(12)
|
|
112
|
+
.describe("Required for yearly schedules, where January is 1.")
|
|
113
|
+
.nullable()
|
|
114
|
+
.optional(),
|
|
115
|
+
start_date: localDateSchema
|
|
116
|
+
.describe(
|
|
117
|
+
"Optional local calendar date that anchors the recurrence. Omit to start with the next matching occurrence.",
|
|
118
|
+
)
|
|
119
|
+
.nullable()
|
|
120
|
+
.optional(),
|
|
121
|
+
timezone: timezoneSchema,
|
|
122
|
+
})
|
|
123
|
+
.strict()
|
|
124
|
+
.superRefine((schedule, context) => {
|
|
125
|
+
if (schedule.frequency === "weekly" && schedule.weekdays == null) {
|
|
126
|
+
context.addIssue({
|
|
127
|
+
code: "custom",
|
|
128
|
+
message: "Weekly schedules require weekdays.",
|
|
129
|
+
path: ["weekdays"],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (
|
|
133
|
+
(schedule.frequency === "monthly" || schedule.frequency === "yearly") &&
|
|
134
|
+
schedule.day_of_month == null
|
|
135
|
+
) {
|
|
136
|
+
context.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
message: `${schedule.frequency} schedules require day_of_month.`,
|
|
139
|
+
path: ["day_of_month"],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (schedule.frequency === "yearly" && schedule.month == null) {
|
|
143
|
+
context.addIssue({
|
|
144
|
+
code: "custom",
|
|
145
|
+
message: "Yearly schedules require month.",
|
|
146
|
+
path: ["month"],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (schedule.frequency !== "weekly" && schedule.weekdays != null) {
|
|
150
|
+
context.addIssue({
|
|
151
|
+
code: "custom",
|
|
152
|
+
message: "weekdays applies only to weekly schedules.",
|
|
153
|
+
path: ["weekdays"],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
schedule.frequency !== "monthly" &&
|
|
158
|
+
schedule.frequency !== "yearly" &&
|
|
159
|
+
schedule.day_of_month != null
|
|
160
|
+
) {
|
|
161
|
+
context.addIssue({
|
|
162
|
+
code: "custom",
|
|
163
|
+
message: "day_of_month applies only to monthly or yearly schedules.",
|
|
164
|
+
path: ["day_of_month"],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (schedule.frequency !== "yearly" && schedule.month != null) {
|
|
168
|
+
context.addIssue({
|
|
169
|
+
code: "custom",
|
|
170
|
+
message: "month applies only to yearly schedules.",
|
|
171
|
+
path: ["month"],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export const scheduleIntentSchema = z.union([
|
|
177
|
+
oneOffScheduleIntentSchema,
|
|
178
|
+
recurringScheduleIntentSchema,
|
|
179
|
+
]);
|
|
180
|
+
export type ScheduleIntent = z.infer<typeof scheduleIntentSchema>;
|
|
181
|
+
|
|
182
|
+
const WEEKDAY_INDEX = {
|
|
183
|
+
sunday: 0,
|
|
184
|
+
monday: 1,
|
|
185
|
+
tuesday: 2,
|
|
186
|
+
wednesday: 3,
|
|
187
|
+
thursday: 4,
|
|
188
|
+
friday: 5,
|
|
189
|
+
saturday: 6,
|
|
190
|
+
} as const;
|
|
191
|
+
|
|
192
|
+
const DURATION_MS = {
|
|
193
|
+
second: 1000,
|
|
194
|
+
minute: 60 * 1000,
|
|
195
|
+
hour: 60 * 60 * 1000,
|
|
196
|
+
day: 24 * 60 * 60 * 1000,
|
|
197
|
+
week: 7 * 24 * 60 * 60 * 1000,
|
|
198
|
+
} as const;
|
|
199
|
+
|
|
200
|
+
function parseLocalTime(value: string): ScheduledLocalTime {
|
|
201
|
+
const [hour, minute] = value.split(":").map(Number);
|
|
202
|
+
return { hour: hour!, minute: minute! };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function localDateAt(timestampMs: number, timezone: string): string {
|
|
206
|
+
const parts = getZonedDateTimeParts(timestampMs, timezone);
|
|
207
|
+
return [parts.year, parts.month, parts.day]
|
|
208
|
+
.map((value, index) =>
|
|
209
|
+
index === 0
|
|
210
|
+
? String(value).padStart(4, "0")
|
|
211
|
+
: String(value).padStart(2, "0"),
|
|
212
|
+
)
|
|
213
|
+
.join("-");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isValidTimeZone(timezone: string): boolean {
|
|
217
|
+
try {
|
|
218
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function plural(value: number, unit: string): string {
|
|
226
|
+
return value === 1 ? unit : `${unit}s`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatWeekdays(weekdays: readonly string[]): string {
|
|
230
|
+
const labels = weekdays.map(
|
|
231
|
+
(weekday) => `${weekday[0]!.toUpperCase()}${weekday.slice(1)}`,
|
|
232
|
+
);
|
|
233
|
+
if (labels.length < 2) {
|
|
234
|
+
return labels[0] ?? "";
|
|
235
|
+
}
|
|
236
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels.at(-1)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function recurringDescription(
|
|
240
|
+
schedule: z.infer<typeof recurringScheduleIntentSchema>,
|
|
241
|
+
timezone: string,
|
|
242
|
+
): string {
|
|
243
|
+
const interval = schedule.interval ?? 1;
|
|
244
|
+
const cadenceUnit = {
|
|
245
|
+
daily: "day",
|
|
246
|
+
weekly: "week",
|
|
247
|
+
monthly: "month",
|
|
248
|
+
yearly: "year",
|
|
249
|
+
}[schedule.frequency];
|
|
250
|
+
const cadence = interval === 1 ? cadenceUnit : `${interval} ${cadenceUnit}s`;
|
|
251
|
+
let detail = "";
|
|
252
|
+
if (schedule.frequency === "weekly") {
|
|
253
|
+
detail = ` on ${formatWeekdays(schedule.weekdays!)}`;
|
|
254
|
+
} else if (schedule.frequency === "monthly") {
|
|
255
|
+
detail = ` on day ${schedule.day_of_month}`;
|
|
256
|
+
} else if (schedule.frequency === "yearly") {
|
|
257
|
+
detail = ` on ${String(schedule.month).padStart(2, "0")}-${String(
|
|
258
|
+
schedule.day_of_month,
|
|
259
|
+
).padStart(2, "0")}`;
|
|
260
|
+
}
|
|
261
|
+
return `Every ${cadence}${detail} at ${schedule.time} (${timezone})`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface CompiledScheduleIntent {
|
|
265
|
+
nextRunAtMs: number;
|
|
266
|
+
schedule: ScheduledTaskSchedule;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export class ScheduleIntentError extends Error {}
|
|
270
|
+
|
|
271
|
+
/** Materialize model-interpreted schedule intent against the trusted server clock. */
|
|
272
|
+
export function compileScheduleIntent(args: {
|
|
273
|
+
defaultTimezone: string;
|
|
274
|
+
intent: ScheduleIntent;
|
|
275
|
+
nowMs: number;
|
|
276
|
+
}): CompiledScheduleIntent {
|
|
277
|
+
const timezone = args.intent.timezone ?? args.defaultTimezone;
|
|
278
|
+
if (!isValidTimeZone(timezone)) {
|
|
279
|
+
throw new ScheduleIntentError("timezone must be a valid IANA time zone.");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (args.intent.kind === "one_off") {
|
|
283
|
+
if (args.intent.timing.type === "after") {
|
|
284
|
+
const nextRunAtMs =
|
|
285
|
+
args.nowMs +
|
|
286
|
+
args.intent.timing.value * DURATION_MS[args.intent.timing.unit];
|
|
287
|
+
return {
|
|
288
|
+
nextRunAtMs,
|
|
289
|
+
schedule: {
|
|
290
|
+
description: `In ${args.intent.timing.value} ${plural(
|
|
291
|
+
args.intent.timing.value,
|
|
292
|
+
args.intent.timing.unit,
|
|
293
|
+
)}`,
|
|
294
|
+
kind: "one_off",
|
|
295
|
+
timezone,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const nextRunAtMs = resolveLocalScheduleAtMs({
|
|
301
|
+
date: args.intent.timing.date,
|
|
302
|
+
time: parseLocalTime(args.intent.timing.time),
|
|
303
|
+
timezone,
|
|
304
|
+
});
|
|
305
|
+
if (nextRunAtMs === undefined) {
|
|
306
|
+
throw new ScheduleIntentError(
|
|
307
|
+
"The requested local date or time does not exist in that timezone.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (nextRunAtMs <= args.nowMs) {
|
|
311
|
+
throw new ScheduleIntentError(
|
|
312
|
+
"The requested one-off schedule must be in the future.",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
nextRunAtMs,
|
|
317
|
+
schedule: {
|
|
318
|
+
description: `Once on ${args.intent.timing.date} at ${args.intent.timing.time} (${timezone})`,
|
|
319
|
+
kind: "one_off",
|
|
320
|
+
timezone,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const recurrence: ScheduledTaskRecurrence = {
|
|
326
|
+
frequency: args.intent.frequency,
|
|
327
|
+
interval: args.intent.interval ?? 1,
|
|
328
|
+
startDate: args.intent.start_date ?? localDateAt(args.nowMs, timezone),
|
|
329
|
+
time: parseLocalTime(args.intent.time),
|
|
330
|
+
...(args.intent.frequency === "weekly"
|
|
331
|
+
? {
|
|
332
|
+
weekdays: [
|
|
333
|
+
...new Set(
|
|
334
|
+
args.intent.weekdays!.map((weekday) => WEEKDAY_INDEX[weekday]),
|
|
335
|
+
),
|
|
336
|
+
].sort((left, right) => left - right),
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
...(args.intent.frequency === "monthly" ||
|
|
340
|
+
args.intent.frequency === "yearly"
|
|
341
|
+
? { dayOfMonth: args.intent.day_of_month ?? undefined }
|
|
342
|
+
: {}),
|
|
343
|
+
...(args.intent.frequency === "yearly"
|
|
344
|
+
? { month: args.intent.month ?? undefined }
|
|
345
|
+
: {}),
|
|
346
|
+
};
|
|
347
|
+
const searchRecurrence = args.intent.start_date
|
|
348
|
+
? recurrence
|
|
349
|
+
: { ...recurrence, interval: 1 };
|
|
350
|
+
const nextRunAtMs = getFirstRunAtMs({
|
|
351
|
+
afterMs: args.nowMs,
|
|
352
|
+
recurrence: searchRecurrence,
|
|
353
|
+
timezone,
|
|
354
|
+
});
|
|
355
|
+
if (nextRunAtMs === undefined) {
|
|
356
|
+
throw new ScheduleIntentError(
|
|
357
|
+
"The recurring schedule has no valid future occurrence.",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const materializedRecurrence = args.intent.start_date
|
|
361
|
+
? recurrence
|
|
362
|
+
: { ...recurrence, startDate: localDateAt(nextRunAtMs, timezone) };
|
|
363
|
+
return {
|
|
364
|
+
nextRunAtMs,
|
|
365
|
+
schedule: {
|
|
366
|
+
description: recurringDescription(args.intent, timezone),
|
|
367
|
+
kind: "recurring",
|
|
368
|
+
recurrence: materializedRecurrence,
|
|
369
|
+
timezone,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
pluginCredentialSubjectSchema,
|
|
3
2
|
destinationSchema,
|
|
4
3
|
isSlackDestination,
|
|
5
4
|
slackActorSchema,
|
|
@@ -114,13 +113,6 @@ const taskRecordSchema = z
|
|
|
114
113
|
credentialMode: scheduledTaskCredentialModeSchema,
|
|
115
114
|
})
|
|
116
115
|
.strict();
|
|
117
|
-
// TODO(v0.101.0): Remove parsing for scheduler task records without credentialMode.
|
|
118
|
-
const legacyTaskRecordSchema = z
|
|
119
|
-
.object({
|
|
120
|
-
...taskRecordFields,
|
|
121
|
-
credentialSubject: pluginCredentialSubjectSchema.optional(),
|
|
122
|
-
})
|
|
123
|
-
.strict();
|
|
124
116
|
const runRecordSchema = z
|
|
125
117
|
.object({
|
|
126
118
|
id: z.string(),
|
|
@@ -148,6 +140,7 @@ const runRecordSchema = z
|
|
|
148
140
|
|
|
149
141
|
export interface SchedulerStore {
|
|
150
142
|
claimDueRun(args: { nowMs: number }): Promise<ScheduledRun | undefined>;
|
|
143
|
+
createTask(task: ScheduledTask): Promise<ScheduledTask>;
|
|
151
144
|
getRun(runId: string): Promise<ScheduledRun | undefined>;
|
|
152
145
|
getTask(taskId: string): Promise<ScheduledTask | undefined>;
|
|
153
146
|
listIncompleteRuns(): Promise<ScheduledRun[]>;
|
|
@@ -236,6 +229,13 @@ function unique(values: string[]): string[] {
|
|
|
236
229
|
return [...new Set(values.filter(Boolean))];
|
|
237
230
|
}
|
|
238
231
|
|
|
232
|
+
const schedulerTaskIndexSchema = z.array(z.string().min(1));
|
|
233
|
+
|
|
234
|
+
/** Parse the persisted scheduler task index without repairing malformed state. */
|
|
235
|
+
function parseStringIndex(value: unknown): string[] {
|
|
236
|
+
return value === undefined ? [] : schedulerTaskIndexSchema.parse(value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
239
|
async function withLock<T>(
|
|
240
240
|
state: PluginState,
|
|
241
241
|
key: string,
|
|
@@ -250,9 +250,7 @@ async function addToIndex(
|
|
|
250
250
|
taskId: string,
|
|
251
251
|
): Promise<void> {
|
|
252
252
|
await withLock(state, indexLockKey(key), async () => {
|
|
253
|
-
const current = ((await state.get
|
|
254
|
-
(value): value is string => typeof value === "string",
|
|
255
|
-
);
|
|
253
|
+
const current = unique(parseStringIndex(await state.get(key)));
|
|
256
254
|
await state.set(key, unique([...current, taskId]), SCHEDULER_RECORD_TTL_MS);
|
|
257
255
|
});
|
|
258
256
|
}
|
|
@@ -263,11 +261,7 @@ async function removeFromIndex(
|
|
|
263
261
|
taskId: string,
|
|
264
262
|
): Promise<void> {
|
|
265
263
|
await withLock(state, indexLockKey(key), async () => {
|
|
266
|
-
const current = unique(
|
|
267
|
-
((await state.get<string[]>(key)) ?? []).filter(
|
|
268
|
-
(value): value is string => typeof value === "string",
|
|
269
|
-
),
|
|
270
|
-
);
|
|
264
|
+
const current = unique(parseStringIndex(await state.get(key)));
|
|
271
265
|
const next = current.filter((value) => value !== taskId);
|
|
272
266
|
if (next.length === current.length) {
|
|
273
267
|
return;
|
|
@@ -284,10 +278,7 @@ async function getIndex(
|
|
|
284
278
|
state: PluginReadState,
|
|
285
279
|
key: string,
|
|
286
280
|
): Promise<string[]> {
|
|
287
|
-
|
|
288
|
-
return unique(
|
|
289
|
-
values.filter((value): value is string => typeof value === "string"),
|
|
290
|
-
);
|
|
281
|
+
return unique(parseStringIndex(await state.get(key)));
|
|
291
282
|
}
|
|
292
283
|
|
|
293
284
|
async function clearActiveRun(
|
|
@@ -493,25 +484,6 @@ function parseStoredTask(value: unknown): ScheduledTask | undefined {
|
|
|
493
484
|
return parsed.success ? stripLegacyTaskFields(parsed.data) : undefined;
|
|
494
485
|
}
|
|
495
486
|
|
|
496
|
-
/** Decode pre-credential-mode tasks only for the state-to-SQL migration. */
|
|
497
|
-
function parseLegacyStoredTaskForMigration(
|
|
498
|
-
value: unknown,
|
|
499
|
-
): ScheduledTask | undefined {
|
|
500
|
-
const parsed = legacyTaskRecordSchema.safeParse(parseJsonRecord(value));
|
|
501
|
-
if (!parsed.success) {
|
|
502
|
-
return undefined;
|
|
503
|
-
}
|
|
504
|
-
const {
|
|
505
|
-
credentialSubject: _credentialSubject,
|
|
506
|
-
version: _version,
|
|
507
|
-
...task
|
|
508
|
-
} = parsed.data;
|
|
509
|
-
return {
|
|
510
|
-
...task,
|
|
511
|
-
credentialMode: "system",
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
487
|
/** Decode retained scheduler run state, skipping invalid legacy records. */
|
|
516
488
|
function parseStoredRun(value: unknown): ScheduledRun | undefined {
|
|
517
489
|
const parsed = runRecordSchema.safeParse(parseJsonRecord(value));
|
|
@@ -634,6 +606,18 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
634
606
|
this.state = state;
|
|
635
607
|
}
|
|
636
608
|
|
|
609
|
+
async createTask(task: ScheduledTask): Promise<ScheduledTask> {
|
|
610
|
+
const next = requireStoredTask(task);
|
|
611
|
+
return await withLock(this.state, taskLockKey(task.id), async () => {
|
|
612
|
+
const current = await getTaskFromState(this.state, task.id);
|
|
613
|
+
if (current) {
|
|
614
|
+
return current;
|
|
615
|
+
}
|
|
616
|
+
await this.saveTaskRecord(next, undefined);
|
|
617
|
+
return next;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
637
621
|
async saveTask(task: ScheduledTask): Promise<void> {
|
|
638
622
|
const next = requireStoredTask(task);
|
|
639
623
|
await withLock(this.state, taskLockKey(task.id), async () => {
|
|
@@ -1255,6 +1239,18 @@ async function listIncompleteRunsForTasksFromSql(
|
|
|
1255
1239
|
class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
|
|
1256
1240
|
constructor(private readonly db: SchedulerDb) {}
|
|
1257
1241
|
|
|
1242
|
+
async createTask(task: ScheduledTask): Promise<ScheduledTask> {
|
|
1243
|
+
const next = requireStoredTask(task);
|
|
1244
|
+
return await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
|
|
1245
|
+
const current = await getTaskFromSql(db, task.id);
|
|
1246
|
+
if (current) {
|
|
1247
|
+
return current;
|
|
1248
|
+
}
|
|
1249
|
+
await this.saveTaskRecord(db, next, undefined);
|
|
1250
|
+
return next;
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1258
1254
|
async saveTask(task: ScheduledTask): Promise<void> {
|
|
1259
1255
|
const next = requireStoredTask(task);
|
|
1260
1256
|
await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
|
|
@@ -1692,58 +1688,3 @@ export function createSchedulerOperationalSqlStore(
|
|
|
1692
1688
|
): SchedulerOperationalStore {
|
|
1693
1689
|
return new SqlSchedulerStore(db);
|
|
1694
1690
|
}
|
|
1695
|
-
|
|
1696
|
-
/** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
|
|
1697
|
-
export async function migrateSchedulerStateToSql(args: {
|
|
1698
|
-
db: SchedulerDb;
|
|
1699
|
-
state: PluginState;
|
|
1700
|
-
}): Promise<{
|
|
1701
|
-
existing: number;
|
|
1702
|
-
migrated: number;
|
|
1703
|
-
missing: number;
|
|
1704
|
-
scanned: number;
|
|
1705
|
-
}> {
|
|
1706
|
-
const store = createSchedulerSqlStore(args.db);
|
|
1707
|
-
const ids = await getIndex(args.state, globalTaskIndexKey());
|
|
1708
|
-
let existing = 0;
|
|
1709
|
-
let migrated = 0;
|
|
1710
|
-
let missing = 0;
|
|
1711
|
-
const migratedTasks: ScheduledTask[] = [];
|
|
1712
|
-
|
|
1713
|
-
for (const id of ids) {
|
|
1714
|
-
const rawTask = await args.state.get(taskKey(id));
|
|
1715
|
-
const task =
|
|
1716
|
-
parseStoredTask(rawTask) ?? parseLegacyStoredTaskForMigration(rawTask);
|
|
1717
|
-
if (!task) {
|
|
1718
|
-
missing += 1;
|
|
1719
|
-
continue;
|
|
1720
|
-
}
|
|
1721
|
-
migratedTasks.push(task);
|
|
1722
|
-
if (await store.getTask(task.id)) {
|
|
1723
|
-
existing += 1;
|
|
1724
|
-
continue;
|
|
1725
|
-
}
|
|
1726
|
-
await store.saveTask(task);
|
|
1727
|
-
migrated += 1;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
const runs = await listIncompleteRunsForTasksFromState(
|
|
1731
|
-
args.state,
|
|
1732
|
-
migratedTasks,
|
|
1733
|
-
);
|
|
1734
|
-
for (const run of runs) {
|
|
1735
|
-
if (await store.getRun(run.id)) {
|
|
1736
|
-
existing += 1;
|
|
1737
|
-
continue;
|
|
1738
|
-
}
|
|
1739
|
-
await upsertSqlRun(args.db, run);
|
|
1740
|
-
migrated += 1;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
return {
|
|
1744
|
-
existing,
|
|
1745
|
-
migrated,
|
|
1746
|
-
missing,
|
|
1747
|
-
scanned: ids.length + runs.length,
|
|
1748
|
-
};
|
|
1749
|
-
}
|