@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
package/src/cadence.ts
CHANGED
|
@@ -1,52 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ScheduledCalendarFrequency,
|
|
3
2
|
ScheduledLocalTime,
|
|
4
3
|
ScheduledTask,
|
|
5
4
|
ScheduledTaskRecurrence,
|
|
6
5
|
} from "./types";
|
|
7
6
|
|
|
8
|
-
/** Parse an ISO timestamp into a finite Unix timestamp in milliseconds. */
|
|
9
|
-
export function parseScheduleTimestamp(value: string): number | undefined {
|
|
10
|
-
const trimmed = value.trim();
|
|
11
|
-
const match =
|
|
12
|
-
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/.exec(
|
|
13
|
-
trimmed,
|
|
14
|
-
);
|
|
15
|
-
if (!match) {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const year = Number(match[1]);
|
|
20
|
-
const month = Number(match[2]);
|
|
21
|
-
const day = Number(match[3]);
|
|
22
|
-
const hour = Number(match[4]);
|
|
23
|
-
const minute = Number(match[5]);
|
|
24
|
-
const second = match[6] ? Number(match[6]) : 0;
|
|
25
|
-
if (
|
|
26
|
-
!Number.isInteger(year) ||
|
|
27
|
-
!Number.isInteger(month) ||
|
|
28
|
-
!Number.isInteger(day) ||
|
|
29
|
-
!Number.isInteger(hour) ||
|
|
30
|
-
!Number.isInteger(minute) ||
|
|
31
|
-
!Number.isInteger(second) ||
|
|
32
|
-
month < 1 ||
|
|
33
|
-
month > 12 ||
|
|
34
|
-
day < 1 ||
|
|
35
|
-
day > daysInMonth(year, month) ||
|
|
36
|
-
hour < 0 ||
|
|
37
|
-
hour > 23 ||
|
|
38
|
-
minute < 0 ||
|
|
39
|
-
minute > 59 ||
|
|
40
|
-
second < 0 ||
|
|
41
|
-
second > 59
|
|
42
|
-
) {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const parsed = Date.parse(trimmed);
|
|
47
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
7
|
export interface ZonedDateTimeParts {
|
|
51
8
|
day: number;
|
|
52
9
|
hour: number;
|
|
@@ -93,6 +50,10 @@ function getLocalDateWeekday(date: LocalDate): number {
|
|
|
93
50
|
return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
|
|
94
51
|
}
|
|
95
52
|
|
|
53
|
+
function getWeekStart(date: LocalDate): LocalDate {
|
|
54
|
+
return addDays(date, -((getLocalDateWeekday(date) + 6) % 7));
|
|
55
|
+
}
|
|
56
|
+
|
|
96
57
|
/** Resolve a UTC timestamp into calendar parts for a named time zone. */
|
|
97
58
|
export function getZonedDateTimeParts(
|
|
98
59
|
timestampMs: number,
|
|
@@ -136,7 +97,7 @@ function localDateTimeToTimestampMs(args: {
|
|
|
136
97
|
date: LocalDate;
|
|
137
98
|
time: ScheduledLocalTime;
|
|
138
99
|
timezone: string;
|
|
139
|
-
}): number {
|
|
100
|
+
}): number | undefined {
|
|
140
101
|
const localAsUtcMs = Date.UTC(
|
|
141
102
|
args.date.year,
|
|
142
103
|
args.date.month - 1,
|
|
@@ -145,18 +106,222 @@ function localDateTimeToTimestampMs(args: {
|
|
|
145
106
|
args.time.minute,
|
|
146
107
|
0,
|
|
147
108
|
);
|
|
148
|
-
|
|
149
|
-
|
|
109
|
+
const offsets = new Set<number>();
|
|
110
|
+
for (const probeDeltaMs of [
|
|
111
|
+
-36 * 60 * 60 * 1000,
|
|
112
|
+
-12 * 60 * 60 * 1000,
|
|
113
|
+
0,
|
|
114
|
+
12 * 60 * 60 * 1000,
|
|
115
|
+
36 * 60 * 60 * 1000,
|
|
116
|
+
]) {
|
|
117
|
+
offsets.add(
|
|
118
|
+
getTimeZoneOffsetMs(localAsUtcMs + probeDeltaMs, args.timezone),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const matches = [...offsets]
|
|
123
|
+
.map((offsetMs) => localAsUtcMs - offsetMs)
|
|
124
|
+
.filter((timestampMs) => {
|
|
125
|
+
const parts = getZonedDateTimeParts(timestampMs, args.timezone);
|
|
126
|
+
return (
|
|
127
|
+
parts.year === args.date.year &&
|
|
128
|
+
parts.month === args.date.month &&
|
|
129
|
+
parts.day === args.date.day &&
|
|
130
|
+
parts.hour === args.time.hour &&
|
|
131
|
+
parts.minute === args.time.minute
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (matches.length === 0) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return Math.min(...matches);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Resolve local time, rejecting DST gaps and choosing the earlier instant in a fold. */
|
|
142
|
+
export function resolveLocalScheduleAtMs(args: {
|
|
143
|
+
date: string;
|
|
144
|
+
time: ScheduledLocalTime;
|
|
145
|
+
timezone: string;
|
|
146
|
+
}): number | undefined {
|
|
147
|
+
const date = parseLocalDate(args.date);
|
|
148
|
+
if (!date) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return localDateTimeToTimestampMs({
|
|
152
|
+
date,
|
|
153
|
+
time: args.time,
|
|
154
|
+
timezone: args.timezone,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function daysBetween(left: LocalDate, right: LocalDate): number {
|
|
159
|
+
return Math.floor(
|
|
160
|
+
(Date.UTC(right.year, right.month - 1, right.day) -
|
|
161
|
+
Date.UTC(left.year, left.month - 1, left.day)) /
|
|
162
|
+
(24 * 60 * 60 * 1000),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function monthsBetween(left: LocalDate, right: LocalDate): number {
|
|
167
|
+
return right.year * 12 + right.month - (left.year * 12 + left.month);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function weeklyRecurrenceMatchesDate(
|
|
171
|
+
date: LocalDate,
|
|
172
|
+
start: LocalDate,
|
|
173
|
+
recurrence: ScheduledTaskRecurrence,
|
|
174
|
+
): boolean {
|
|
175
|
+
if (compareDate(date, start) < 0) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return (
|
|
179
|
+
normalizeWeekdays(recurrence.weekdays).includes(
|
|
180
|
+
getLocalDateWeekday(date),
|
|
181
|
+
) &&
|
|
182
|
+
Math.floor(daysBetween(getWeekStart(start), getWeekStart(date)) / 7) %
|
|
183
|
+
recurrence.interval ===
|
|
184
|
+
0
|
|
185
|
+
);
|
|
186
|
+
}
|
|
150
187
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
function greatestCommonDivisor(left: number, right: number): number {
|
|
189
|
+
let a = Math.abs(left);
|
|
190
|
+
let b = Math.abs(right);
|
|
191
|
+
while (b !== 0) {
|
|
192
|
+
[a, b] = [b, a % b];
|
|
193
|
+
}
|
|
194
|
+
return a;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findNextRunAtMs(args: {
|
|
198
|
+
afterMs: number;
|
|
199
|
+
recurrence: ScheduledTaskRecurrence;
|
|
200
|
+
searchFrom: LocalDate;
|
|
201
|
+
timezone: string;
|
|
202
|
+
}): number | undefined {
|
|
203
|
+
const start = parseLocalDate(args.recurrence.startDate);
|
|
204
|
+
const interval = args.recurrence.interval;
|
|
205
|
+
if (!start || !Number.isInteger(interval) || interval <= 0) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const searchFrom =
|
|
210
|
+
compareDate(args.searchFrom, start) < 0 ? start : args.searchFrom;
|
|
211
|
+
if (args.recurrence.frequency === "daily") {
|
|
212
|
+
const offsetDays = daysBetween(start, searchFrom);
|
|
213
|
+
let candidateDate = addDays(
|
|
214
|
+
start,
|
|
215
|
+
Math.ceil(offsetDays / interval) * interval,
|
|
216
|
+
);
|
|
217
|
+
const gregorianCycleDays = 146_097;
|
|
218
|
+
const candidateCount =
|
|
219
|
+
gregorianCycleDays / greatestCommonDivisor(gregorianCycleDays, interval);
|
|
220
|
+
for (let attempts = 0; attempts < candidateCount; attempts += 1) {
|
|
221
|
+
const candidate = buildCandidate({
|
|
222
|
+
date: candidateDate,
|
|
223
|
+
recurrence: args.recurrence,
|
|
224
|
+
timezone: args.timezone,
|
|
225
|
+
});
|
|
226
|
+
if (candidate !== undefined && candidate > args.afterMs) {
|
|
227
|
+
return candidate;
|
|
228
|
+
}
|
|
229
|
+
candidateDate = addDays(candidateDate, interval);
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (args.recurrence.frequency === "weekly") {
|
|
235
|
+
if (normalizeWeekdays(args.recurrence.weekdays).length === 0) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
let candidateDate = searchFrom;
|
|
239
|
+
const gregorianCycleDays = 146_097;
|
|
240
|
+
const cadenceDays = interval * 7;
|
|
241
|
+
const searchDays =
|
|
242
|
+
(gregorianCycleDays * cadenceDays) /
|
|
243
|
+
greatestCommonDivisor(gregorianCycleDays, cadenceDays);
|
|
244
|
+
for (let attempts = 0; attempts < searchDays; attempts += 1) {
|
|
245
|
+
if (weeklyRecurrenceMatchesDate(candidateDate, start, args.recurrence)) {
|
|
246
|
+
const candidate = buildCandidate({
|
|
247
|
+
date: candidateDate,
|
|
248
|
+
recurrence: args.recurrence,
|
|
249
|
+
timezone: args.timezone,
|
|
250
|
+
});
|
|
251
|
+
if (candidate !== undefined && candidate > args.afterMs) {
|
|
252
|
+
return candidate;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
candidateDate = addDays(candidateDate, 1);
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (args.recurrence.frequency === "monthly") {
|
|
261
|
+
const startMonth = start.year * 12 + start.month - 1;
|
|
262
|
+
const searchMonth = searchFrom.year * 12 + searchFrom.month - 1;
|
|
263
|
+
let candidateMonth =
|
|
264
|
+
startMonth + Math.ceil((searchMonth - startMonth) / interval) * interval;
|
|
265
|
+
const gregorianCycleMonths = 4_800;
|
|
266
|
+
const candidateCount =
|
|
267
|
+
gregorianCycleMonths /
|
|
268
|
+
greatestCommonDivisor(gregorianCycleMonths, interval);
|
|
269
|
+
for (let attempts = 0; attempts < candidateCount; attempts += 1) {
|
|
270
|
+
const candidateDate = {
|
|
271
|
+
year: Math.floor(candidateMonth / 12),
|
|
272
|
+
month: (candidateMonth % 12) + 1,
|
|
273
|
+
day: args.recurrence.dayOfMonth ?? 0,
|
|
274
|
+
};
|
|
275
|
+
if (compareDate(candidateDate, searchFrom) >= 0) {
|
|
276
|
+
const candidate = buildCandidate({
|
|
277
|
+
date: candidateDate,
|
|
278
|
+
recurrence: args.recurrence,
|
|
279
|
+
timezone: args.timezone,
|
|
280
|
+
});
|
|
281
|
+
if (candidate !== undefined && candidate > args.afterMs) {
|
|
282
|
+
return candidate;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
candidateMonth += interval;
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let candidateYear =
|
|
291
|
+
start.year +
|
|
292
|
+
Math.ceil((searchFrom.year - start.year) / interval) * interval;
|
|
293
|
+
const candidateCount = 400 / greatestCommonDivisor(400, interval);
|
|
294
|
+
for (let attempts = 0; attempts < candidateCount; attempts += 1) {
|
|
295
|
+
const candidateDate = {
|
|
296
|
+
year: candidateYear,
|
|
297
|
+
month: args.recurrence.month ?? 0,
|
|
298
|
+
day: args.recurrence.dayOfMonth ?? 0,
|
|
299
|
+
};
|
|
300
|
+
if (compareDate(candidateDate, searchFrom) >= 0) {
|
|
301
|
+
const candidate = buildCandidate({
|
|
302
|
+
date: candidateDate,
|
|
303
|
+
recurrence: args.recurrence,
|
|
304
|
+
timezone: args.timezone,
|
|
305
|
+
});
|
|
306
|
+
if (candidate !== undefined && candidate > args.afterMs) {
|
|
307
|
+
return candidate;
|
|
308
|
+
}
|
|
155
309
|
}
|
|
156
|
-
|
|
310
|
+
candidateYear += interval;
|
|
157
311
|
}
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
158
314
|
|
|
159
|
-
|
|
315
|
+
/** Compute the first recurring calendar occurrence strictly after a timestamp. */
|
|
316
|
+
export function getFirstRunAtMs(args: {
|
|
317
|
+
afterMs: number;
|
|
318
|
+
recurrence: ScheduledTaskRecurrence;
|
|
319
|
+
timezone: string;
|
|
320
|
+
}): number | undefined {
|
|
321
|
+
return findNextRunAtMs({
|
|
322
|
+
...args,
|
|
323
|
+
searchFrom: getLocalDate(args.afterMs, args.timezone),
|
|
324
|
+
});
|
|
160
325
|
}
|
|
161
326
|
|
|
162
327
|
function compareDate(left: LocalDate, right: LocalDate): number {
|
|
@@ -203,14 +368,6 @@ function parseLocalDate(value: string): LocalDate | undefined {
|
|
|
203
368
|
return { year, month, day };
|
|
204
369
|
}
|
|
205
370
|
|
|
206
|
-
function formatLocalDate(date: LocalDate): string {
|
|
207
|
-
return [
|
|
208
|
-
String(date.year).padStart(4, "0"),
|
|
209
|
-
String(date.month).padStart(2, "0"),
|
|
210
|
-
String(date.day).padStart(2, "0"),
|
|
211
|
-
].join("-");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
371
|
function getLocalDate(timestampMs: number, timezone: string): LocalDate {
|
|
215
372
|
const parts = getZonedDateTimeParts(timestampMs, timezone);
|
|
216
373
|
return { year: parts.year, month: parts.month, day: parts.day };
|
|
@@ -226,7 +383,7 @@ function buildCandidate(args: {
|
|
|
226
383
|
date: LocalDate;
|
|
227
384
|
recurrence: ScheduledTaskRecurrence;
|
|
228
385
|
timezone: string;
|
|
229
|
-
}): number {
|
|
386
|
+
}): number | undefined {
|
|
230
387
|
return localDateTimeToTimestampMs({
|
|
231
388
|
date: args.date,
|
|
232
389
|
time: args.recurrence.time,
|
|
@@ -234,218 +391,6 @@ function buildCandidate(args: {
|
|
|
234
391
|
});
|
|
235
392
|
}
|
|
236
393
|
|
|
237
|
-
function getDailyNextRunAtMs(args: {
|
|
238
|
-
afterMs: number;
|
|
239
|
-
recurrence: ScheduledTaskRecurrence;
|
|
240
|
-
scheduledForMs: number;
|
|
241
|
-
timezone: string;
|
|
242
|
-
}): number | undefined {
|
|
243
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
244
|
-
if (!start) {
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
let candidateDate = addDays(
|
|
249
|
-
getLocalDate(args.scheduledForMs, args.timezone),
|
|
250
|
-
args.recurrence.interval,
|
|
251
|
-
);
|
|
252
|
-
if (compareDate(candidateDate, start) < 0) {
|
|
253
|
-
candidateDate = start;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
let candidate = buildCandidate({
|
|
257
|
-
date: candidateDate,
|
|
258
|
-
recurrence: args.recurrence,
|
|
259
|
-
timezone: args.timezone,
|
|
260
|
-
});
|
|
261
|
-
while (candidate <= args.afterMs) {
|
|
262
|
-
candidateDate = addDays(candidateDate, args.recurrence.interval);
|
|
263
|
-
candidate = buildCandidate({
|
|
264
|
-
date: candidateDate,
|
|
265
|
-
recurrence: args.recurrence,
|
|
266
|
-
timezone: args.timezone,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
return candidate;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function getWeeklyNextRunAtMs(args: {
|
|
273
|
-
afterMs: number;
|
|
274
|
-
recurrence: ScheduledTaskRecurrence;
|
|
275
|
-
scheduledForMs: number;
|
|
276
|
-
timezone: string;
|
|
277
|
-
}): number | undefined {
|
|
278
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
279
|
-
if (!start) {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const weekdays = normalizeWeekdays(args.recurrence.weekdays);
|
|
284
|
-
if (weekdays.length === 0) {
|
|
285
|
-
return undefined;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let candidateDate = addDays(
|
|
289
|
-
getLocalDate(args.scheduledForMs, args.timezone),
|
|
290
|
-
1,
|
|
291
|
-
);
|
|
292
|
-
for (let attempts = 0; attempts < 3660; attempts += 1) {
|
|
293
|
-
const weeksSinceStart = Math.floor(
|
|
294
|
-
(Date.UTC(
|
|
295
|
-
candidateDate.year,
|
|
296
|
-
candidateDate.month - 1,
|
|
297
|
-
candidateDate.day,
|
|
298
|
-
) -
|
|
299
|
-
Date.UTC(start.year, start.month - 1, start.day)) /
|
|
300
|
-
(7 * 24 * 60 * 60 * 1000),
|
|
301
|
-
);
|
|
302
|
-
const isInCycle =
|
|
303
|
-
weeksSinceStart >= 0 && weeksSinceStart % args.recurrence.interval === 0;
|
|
304
|
-
if (isInCycle && weekdays.includes(getLocalDateWeekday(candidateDate))) {
|
|
305
|
-
const candidate = buildCandidate({
|
|
306
|
-
date: candidateDate,
|
|
307
|
-
recurrence: args.recurrence,
|
|
308
|
-
timezone: args.timezone,
|
|
309
|
-
});
|
|
310
|
-
if (candidate > args.afterMs) {
|
|
311
|
-
return candidate;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
candidateDate = addDays(candidateDate, 1);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function getMonthlyNextRunAtMs(args: {
|
|
321
|
-
afterMs: number;
|
|
322
|
-
recurrence: ScheduledTaskRecurrence;
|
|
323
|
-
scheduledForMs: number;
|
|
324
|
-
timezone: string;
|
|
325
|
-
}): number | undefined {
|
|
326
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
327
|
-
const dayOfMonth = args.recurrence.dayOfMonth;
|
|
328
|
-
if (!start || !dayOfMonth) {
|
|
329
|
-
return undefined;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const scheduledDate = getLocalDate(args.scheduledForMs, args.timezone);
|
|
333
|
-
let monthIndex = scheduledDate.year * 12 + scheduledDate.month - 1;
|
|
334
|
-
const startMonthIndex = start.year * 12 + start.month - 1;
|
|
335
|
-
|
|
336
|
-
for (let attempts = 0; attempts < 1200; attempts += 1) {
|
|
337
|
-
monthIndex += args.recurrence.interval;
|
|
338
|
-
if (monthIndex < startMonthIndex) {
|
|
339
|
-
monthIndex = startMonthIndex;
|
|
340
|
-
}
|
|
341
|
-
const year = Math.floor(monthIndex / 12);
|
|
342
|
-
const month = (monthIndex % 12) + 1;
|
|
343
|
-
if (dayOfMonth > daysInMonth(year, month)) {
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
const candidate = buildCandidate({
|
|
347
|
-
date: { year, month, day: dayOfMonth },
|
|
348
|
-
recurrence: args.recurrence,
|
|
349
|
-
timezone: args.timezone,
|
|
350
|
-
});
|
|
351
|
-
if (candidate > args.afterMs) {
|
|
352
|
-
return candidate;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function getYearlyNextRunAtMs(args: {
|
|
360
|
-
afterMs: number;
|
|
361
|
-
recurrence: ScheduledTaskRecurrence;
|
|
362
|
-
scheduledForMs: number;
|
|
363
|
-
timezone: string;
|
|
364
|
-
}): number | undefined {
|
|
365
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
366
|
-
const month = args.recurrence.month;
|
|
367
|
-
const dayOfMonth = args.recurrence.dayOfMonth;
|
|
368
|
-
if (!start || !month || !dayOfMonth) {
|
|
369
|
-
return undefined;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const scheduledDate = getLocalDate(args.scheduledForMs, args.timezone);
|
|
373
|
-
let year = scheduledDate.year;
|
|
374
|
-
|
|
375
|
-
for (let attempts = 0; attempts < 100; attempts += 1) {
|
|
376
|
-
year += args.recurrence.interval;
|
|
377
|
-
if (year < start.year) {
|
|
378
|
-
year = start.year;
|
|
379
|
-
}
|
|
380
|
-
if (dayOfMonth > daysInMonth(year, month)) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const candidate = buildCandidate({
|
|
384
|
-
date: { year, month, day: dayOfMonth },
|
|
385
|
-
recurrence: args.recurrence,
|
|
386
|
-
timezone: args.timezone,
|
|
387
|
-
});
|
|
388
|
-
if (candidate > args.afterMs) {
|
|
389
|
-
return candidate;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return undefined;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** Build a calendar recurrence anchored to an exact first run timestamp. */
|
|
397
|
-
export function buildCalendarRecurrence(args: {
|
|
398
|
-
frequency: ScheduledCalendarFrequency;
|
|
399
|
-
interval?: number;
|
|
400
|
-
nextRunAtMs: number;
|
|
401
|
-
timezone: string;
|
|
402
|
-
weekdays?: number[];
|
|
403
|
-
}): ScheduledTaskRecurrence {
|
|
404
|
-
const interval = args.interval && args.interval > 0 ? args.interval : 1;
|
|
405
|
-
const parts = getZonedDateTimeParts(args.nextRunAtMs, args.timezone);
|
|
406
|
-
const time = { hour: parts.hour, minute: parts.minute };
|
|
407
|
-
const startDate = formatLocalDate(parts);
|
|
408
|
-
|
|
409
|
-
if (args.frequency === "weekly") {
|
|
410
|
-
const weekdays = normalizeWeekdays(args.weekdays);
|
|
411
|
-
return {
|
|
412
|
-
frequency: args.frequency,
|
|
413
|
-
interval,
|
|
414
|
-
startDate,
|
|
415
|
-
time,
|
|
416
|
-
weekdays: weekdays.length > 0 ? weekdays : [parts.weekday],
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (args.frequency === "monthly") {
|
|
421
|
-
return {
|
|
422
|
-
dayOfMonth: parts.day,
|
|
423
|
-
frequency: args.frequency,
|
|
424
|
-
interval,
|
|
425
|
-
startDate,
|
|
426
|
-
time,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (args.frequency === "yearly") {
|
|
431
|
-
return {
|
|
432
|
-
dayOfMonth: parts.day,
|
|
433
|
-
frequency: args.frequency,
|
|
434
|
-
interval,
|
|
435
|
-
month: parts.month,
|
|
436
|
-
startDate,
|
|
437
|
-
time,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
frequency: args.frequency,
|
|
443
|
-
interval,
|
|
444
|
-
startDate,
|
|
445
|
-
time,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
394
|
/** Return the next fire time after a completed run, when the task recurs. */
|
|
450
395
|
export function getNextRunAtMs(
|
|
451
396
|
task: ScheduledTask,
|
|
@@ -465,37 +410,16 @@ export function getNextRunAtMs(
|
|
|
465
410
|
return undefined;
|
|
466
411
|
}
|
|
467
412
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
scheduledForMs,
|
|
473
|
-
afterMs,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (recurrence.frequency === "weekly") {
|
|
478
|
-
return getWeeklyNextRunAtMs({
|
|
479
|
-
recurrence,
|
|
480
|
-
timezone: task.schedule.timezone,
|
|
481
|
-
scheduledForMs,
|
|
482
|
-
afterMs,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (recurrence.frequency === "monthly") {
|
|
487
|
-
return getMonthlyNextRunAtMs({
|
|
488
|
-
recurrence,
|
|
489
|
-
timezone: task.schedule.timezone,
|
|
490
|
-
scheduledForMs,
|
|
491
|
-
afterMs,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return getYearlyNextRunAtMs({
|
|
496
|
-
recurrence,
|
|
497
|
-
timezone: task.schedule.timezone,
|
|
498
|
-
scheduledForMs,
|
|
413
|
+
const timezone = task.schedule.timezone;
|
|
414
|
+
const afterDate = getLocalDate(afterMs, timezone);
|
|
415
|
+
const nextScheduledDate = addDays(getLocalDate(scheduledForMs, timezone), 1);
|
|
416
|
+
return findNextRunAtMs({
|
|
499
417
|
afterMs,
|
|
418
|
+
recurrence,
|
|
419
|
+
searchFrom:
|
|
420
|
+
compareDate(afterDate, nextScheduledDate) < 0
|
|
421
|
+
? nextScheduledDate
|
|
422
|
+
: afterDate,
|
|
423
|
+
timezone,
|
|
500
424
|
});
|
|
501
425
|
}
|
package/src/index.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
createSchedulerOperationalSqlStore,
|
|
15
15
|
createSchedulerSqlStore,
|
|
16
|
-
migrateSchedulerStateToSql,
|
|
17
16
|
type SchedulerDb,
|
|
18
17
|
type SchedulerOperationalStore,
|
|
19
18
|
type SchedulerStore,
|
|
@@ -582,12 +581,6 @@ export function createSchedulerPlugin() {
|
|
|
582
581
|
store: schedulerOperationalStore(ctx),
|
|
583
582
|
});
|
|
584
583
|
},
|
|
585
|
-
async migrateStorage(ctx) {
|
|
586
|
-
return await migrateSchedulerStateToSql({
|
|
587
|
-
db: ctx.db as SchedulerDb,
|
|
588
|
-
state: ctx.state,
|
|
589
|
-
});
|
|
590
|
-
},
|
|
591
584
|
},
|
|
592
585
|
});
|
|
593
586
|
}
|