@j13b/scheduler 0.0.0-stage → 0.1.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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -2
  3. package/dist/alterations.d.ts +6 -0
  4. package/dist/alterations.d.ts.map +1 -0
  5. package/dist/alterations.js +61 -0
  6. package/dist/alterations.js.map +1 -0
  7. package/dist/cjs/alterations.d.ts +5 -0
  8. package/dist/cjs/alterations.js +96 -0
  9. package/dist/cjs/dispatcher.d.ts +38 -0
  10. package/dist/cjs/dispatcher.js +165 -0
  11. package/dist/cjs/dispatcher_store.d.ts +9 -0
  12. package/dist/cjs/dispatcher_store.js +15 -0
  13. package/dist/cjs/generators/anchored.d.ts +3 -0
  14. package/dist/cjs/generators/anchored.js +57 -0
  15. package/dist/cjs/generators/days_of_month.d.ts +3 -0
  16. package/dist/cjs/generators/days_of_month.js +76 -0
  17. package/dist/cjs/generators/days_of_week.d.ts +5 -0
  18. package/dist/cjs/generators/days_of_week.js +65 -0
  19. package/dist/cjs/generators/days_of_year.d.ts +3 -0
  20. package/dist/cjs/generators/days_of_year.js +70 -0
  21. package/dist/cjs/generators/instant.d.ts +3 -0
  22. package/dist/cjs/generators/instant.js +15 -0
  23. package/dist/cjs/generators/local_date_time.d.ts +3 -0
  24. package/dist/cjs/generators/local_date_time.js +31 -0
  25. package/dist/cjs/generators/months_of_year.d.ts +3 -0
  26. package/dist/cjs/generators/months_of_year.js +109 -0
  27. package/dist/cjs/generators/types.d.ts +9 -0
  28. package/dist/cjs/generators/types.js +19 -0
  29. package/dist/cjs/generators/weeks_of_month.d.ts +3 -0
  30. package/dist/cjs/generators/weeks_of_month.js +99 -0
  31. package/dist/cjs/generators/yearly.d.ts +3 -0
  32. package/dist/cjs/generators/yearly.js +52 -0
  33. package/dist/cjs/in_memory_port.d.ts +18 -0
  34. package/dist/cjs/in_memory_port.js +59 -0
  35. package/dist/cjs/index.d.ts +6 -0
  36. package/dist/cjs/index.js +14 -0
  37. package/dist/cjs/occurrence_key.d.ts +5 -0
  38. package/dist/cjs/occurrence_key.js +21 -0
  39. package/dist/cjs/package.json +3 -0
  40. package/dist/cjs/port.d.ts +15 -0
  41. package/dist/cjs/port.js +2 -0
  42. package/dist/cjs/scheduler.d.ts +24 -0
  43. package/dist/cjs/scheduler.js +460 -0
  44. package/dist/cjs/types.d.ts +149 -0
  45. package/dist/cjs/types.js +58 -0
  46. package/dist/cjs/tz.d.ts +22 -0
  47. package/dist/cjs/tz.js +197 -0
  48. package/dist/dispatcher.d.ts +39 -0
  49. package/dist/dispatcher.d.ts.map +1 -0
  50. package/dist/dispatcher.js +111 -0
  51. package/dist/dispatcher.js.map +1 -0
  52. package/dist/dispatcher_store.d.ts +10 -0
  53. package/dist/dispatcher_store.d.ts.map +1 -0
  54. package/dist/dispatcher_store.js +13 -0
  55. package/dist/dispatcher_store.js.map +1 -0
  56. package/dist/generators/anchored.d.ts +4 -0
  57. package/dist/generators/anchored.d.ts.map +1 -0
  58. package/dist/generators/anchored.js +42 -0
  59. package/dist/generators/anchored.js.map +1 -0
  60. package/dist/generators/days_of_month.d.ts +4 -0
  61. package/dist/generators/days_of_month.d.ts.map +1 -0
  62. package/dist/generators/days_of_month.js +53 -0
  63. package/dist/generators/days_of_month.js.map +1 -0
  64. package/dist/generators/days_of_week.d.ts +6 -0
  65. package/dist/generators/days_of_week.d.ts.map +1 -0
  66. package/dist/generators/days_of_week.js +43 -0
  67. package/dist/generators/days_of_week.js.map +1 -0
  68. package/dist/generators/days_of_year.d.ts +4 -0
  69. package/dist/generators/days_of_year.d.ts.map +1 -0
  70. package/dist/generators/days_of_year.js +50 -0
  71. package/dist/generators/days_of_year.js.map +1 -0
  72. package/dist/generators/instant.d.ts +4 -0
  73. package/dist/generators/instant.d.ts.map +1 -0
  74. package/dist/generators/instant.js +15 -0
  75. package/dist/generators/instant.js.map +1 -0
  76. package/dist/generators/local_date_time.d.ts +4 -0
  77. package/dist/generators/local_date_time.d.ts.map +1 -0
  78. package/dist/generators/local_date_time.js +20 -0
  79. package/dist/generators/local_date_time.js.map +1 -0
  80. package/dist/generators/months_of_year.d.ts +4 -0
  81. package/dist/generators/months_of_year.d.ts.map +1 -0
  82. package/dist/generators/months_of_year.js +64 -0
  83. package/dist/generators/months_of_year.js.map +1 -0
  84. package/dist/generators/types.d.ts +10 -0
  85. package/dist/generators/types.d.ts.map +1 -0
  86. package/dist/generators/types.js +14 -0
  87. package/dist/generators/types.js.map +1 -0
  88. package/dist/generators/weeks_of_month.d.ts +4 -0
  89. package/dist/generators/weeks_of_month.d.ts.map +1 -0
  90. package/dist/generators/weeks_of_month.js +61 -0
  91. package/dist/generators/weeks_of_month.js.map +1 -0
  92. package/dist/generators/yearly.d.ts +4 -0
  93. package/dist/generators/yearly.d.ts.map +1 -0
  94. package/dist/generators/yearly.js +37 -0
  95. package/dist/generators/yearly.js.map +1 -0
  96. package/dist/in_memory_port.d.ts +19 -0
  97. package/dist/in_memory_port.d.ts.map +1 -0
  98. package/dist/in_memory_port.js +45 -0
  99. package/dist/in_memory_port.js.map +1 -0
  100. package/dist/index.d.ts +7 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +6 -0
  103. package/dist/occurrence_key.d.ts +6 -0
  104. package/dist/occurrence_key.d.ts.map +1 -0
  105. package/dist/occurrence_key.js +25 -0
  106. package/dist/occurrence_key.js.map +1 -0
  107. package/dist/port.d.ts +16 -0
  108. package/dist/port.d.ts.map +1 -0
  109. package/dist/scheduler.d.ts +25 -0
  110. package/dist/scheduler.d.ts.map +1 -0
  111. package/dist/scheduler.js +332 -0
  112. package/dist/scheduler.js.map +1 -0
  113. package/dist/types.d.ts +150 -0
  114. package/dist/types.d.ts.map +1 -0
  115. package/dist/types.js +29 -0
  116. package/dist/types.js.map +1 -0
  117. package/dist/tz.d.ts +23 -0
  118. package/dist/tz.d.ts.map +1 -0
  119. package/dist/tz.js +131 -0
  120. package/dist/tz.js.map +1 -0
  121. package/package.json +70 -4
  122. package/src/__tests__/alterations.test.ts +177 -0
  123. package/src/__tests__/anchored.test.ts +107 -0
  124. package/src/__tests__/days_of_month.test.ts +162 -0
  125. package/src/__tests__/days_of_week.test.ts +134 -0
  126. package/src/__tests__/days_of_year.test.ts +158 -0
  127. package/src/__tests__/dispatcher.test.ts +317 -0
  128. package/src/__tests__/instant.test.ts +50 -0
  129. package/src/__tests__/local_date_time.test.ts +139 -0
  130. package/src/__tests__/months_of_year.test.ts +229 -0
  131. package/src/__tests__/scheduler.test.ts +888 -0
  132. package/src/__tests__/tz.test.ts +201 -0
  133. package/src/__tests__/weeks_of_month.test.ts +195 -0
  134. package/src/__tests__/yearly.test.ts +108 -0
  135. package/src/alterations.ts +108 -0
  136. package/src/dispatcher.ts +184 -0
  137. package/src/dispatcher_store.ts +26 -0
  138. package/src/generators/anchored.ts +69 -0
  139. package/src/generators/days_of_month.ts +100 -0
  140. package/src/generators/days_of_week.ts +101 -0
  141. package/src/generators/days_of_year.ts +88 -0
  142. package/src/generators/instant.ts +18 -0
  143. package/src/generators/local_date_time.ts +38 -0
  144. package/src/generators/months_of_year.ts +143 -0
  145. package/src/generators/types.ts +26 -0
  146. package/src/generators/weeks_of_month.ts +133 -0
  147. package/src/generators/yearly.ts +73 -0
  148. package/src/in_memory_port.ts +70 -0
  149. package/src/index.ts +24 -0
  150. package/src/occurrence_key.ts +42 -0
  151. package/src/port.ts +31 -0
  152. package/src/scheduler.ts +499 -0
  153. package/src/types.ts +369 -0
  154. package/src/tz.ts +246 -0
  155. package/tsconfig.json +25 -0
@@ -0,0 +1,5 @@
1
+ import type { DaysOfWeekRule, Disambiguation } from "../types.js";
2
+ import { dayOfWeekInTz } from "../tz.js";
3
+ import { type Occurrence } from "./types.js";
4
+ export declare function generateDaysOfWeek(rule: DaysOfWeekRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
5
+ export { dayOfWeekInTz };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dayOfWeekInTz = void 0;
4
+ exports.generateDaysOfWeek = generateDaysOfWeek;
5
+ const tz_js_1 = require("../tz.js");
6
+ Object.defineProperty(exports, "dayOfWeekInTz", { enumerable: true, get: function () { return tz_js_1.dayOfWeekInTz; } });
7
+ const types_js_1 = require("./types.js");
8
+ // Iterate week-by-week (in wall time, in tz) starting from seriesStart's
9
+ // week. For each scheduled week, emit the occurrences that fall on the
10
+ // rule's days.
11
+ function generateDaysOfWeek(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
12
+ if (rule.weekInterval <= 0) {
13
+ throw new Error(`DaysOfWeekRule.weekInterval must be > 0 (got ${rule.weekInterval})`);
14
+ }
15
+ if (rule.days.length === 0) {
16
+ throw new Error("DaysOfWeekRule.days must be non-empty");
17
+ }
18
+ if (rule.count != null && rule.count < 0) {
19
+ throw new Error(`DaysOfWeekRule.count must be >= 0 (got ${rule.count})`);
20
+ }
21
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
22
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
23
+ if (rule.seriesStart >= effectiveEnd)
24
+ return [];
25
+ // Sort days for deterministic emit order within a week.
26
+ const days = [...rule.days].sort((a, b) => a - b);
27
+ // Anchor: snap seriesStart to the start of its week (Sunday) in tz.
28
+ const seriesStartParts = (0, tz_js_1.utcToWallParts)(rule.seriesStart, tz);
29
+ const daysSinceSunday = seriesStartParts.dayOfWeek;
30
+ const weekAnchor = (0, tz_js_1.wallTimeToUtc)(seriesStartParts.year, seriesStartParts.month, seriesStartParts.day - daysSinceSunday, 0, tz);
31
+ const out = [];
32
+ let count = 0;
33
+ // Step by `weekInterval` weeks at a time. Use addDaysInTz to handle DST
34
+ // correctly (a week is 7 calendar days, not 7*86400000 ms).
35
+ let weekStart = weekAnchor;
36
+ while (weekStart < effectiveEnd) {
37
+ for (const dow of days) {
38
+ // Compute the day-of-week within this week in tz wall time.
39
+ const dayUtc = (0, tz_js_1.addDaysInTz)(weekStart, dow, tz);
40
+ const dayParts = (0, tz_js_1.utcToWallParts)(dayUtc, tz);
41
+ if (disambiguation === "skip" &&
42
+ (0, tz_js_1.isNonexistentWallTime)(dayParts.year, dayParts.month, dayParts.day, rule.timeOfDay, tz)) {
43
+ continue;
44
+ }
45
+ const occStart = (0, tz_js_1.wallTimeToUtc)(dayParts.year, dayParts.month, dayParts.day, rule.timeOfDay, tz, disambiguation);
46
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occStart, rule.timeSpan, tz);
47
+ // Must be on/after seriesStart and before seriesEnd.
48
+ if (occStart < rule.seriesStart)
49
+ continue;
50
+ if (occStart >= seriesEnd)
51
+ continue;
52
+ if (rule.count != null && count >= rule.count)
53
+ return out;
54
+ count++;
55
+ if ((0, types_js_1.intersects)(occStart, occEnd, windowStart, windowEnd)) {
56
+ out.push({ startTimestamp: occStart, endTimestamp: occEnd });
57
+ if (out.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
58
+ throw new types_js_1.TooManyOccurrencesError("daysOfWeek");
59
+ }
60
+ }
61
+ }
62
+ weekStart = (0, tz_js_1.addDaysInTz)(weekStart, 7 * rule.weekInterval, tz);
63
+ }
64
+ return out;
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { DaysOfYearRule, Disambiguation } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateDaysOfYear(rule: DaysOfYearRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateDaysOfYear = generateDaysOfYear;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // Day-of-year recurrence. Day 1 = Jan 1. Day 366 only exists in leap
7
+ // years and is SKIPPED in non-leap years (no clamp — "366th day" means
8
+ // literally that).
9
+ function generateDaysOfYear(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
10
+ if (rule.yearInterval <= 0) {
11
+ throw new Error(`DaysOfYearRule.yearInterval must be > 0 (got ${rule.yearInterval})`);
12
+ }
13
+ if (rule.daysOfYear.length === 0) {
14
+ throw new Error("DaysOfYearRule.daysOfYear must be non-empty");
15
+ }
16
+ for (const d of rule.daysOfYear) {
17
+ if (d < 1 || d > 366) {
18
+ throw new Error(`DaysOfYearRule.daysOfYear entries must be in 1..366 (got ${d})`);
19
+ }
20
+ }
21
+ if (rule.count != null && rule.count < 0) {
22
+ throw new Error(`DaysOfYearRule.count must be >= 0 (got ${rule.count})`);
23
+ }
24
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
25
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
26
+ if (rule.seriesStart >= effectiveEnd)
27
+ return [];
28
+ const days = [...new Set(rule.daysOfYear)].sort((a, b) => a - b);
29
+ const startYear = (0, tz_js_1.utcToWallParts)(rule.seriesStart, tz).year;
30
+ const endYear = (0, tz_js_1.utcToWallParts)(Math.min(effectiveEnd, Number.MAX_SAFE_INTEGER), tz).year;
31
+ const out = [];
32
+ let count = 0;
33
+ for (let year = startYear; year <= endYear; year += rule.yearInterval) {
34
+ const yearEmits = [];
35
+ for (const dayOfYear of days) {
36
+ // Date.UTC(year, 0, N) treats N as "Nth day from Jan 1". If N is 366
37
+ // in a non-leap year it rolls into year+1; detect and skip.
38
+ const probe = new Date(Date.UTC(year, 0, dayOfYear));
39
+ if (probe.getUTCFullYear() !== year)
40
+ continue; // day 366 in non-leap year
41
+ const month = probe.getUTCMonth();
42
+ const day = probe.getUTCDate();
43
+ if (disambiguation === "skip" &&
44
+ (0, tz_js_1.isNonexistentWallTime)(year, month, day, rule.timeOfDay, tz)) {
45
+ continue;
46
+ }
47
+ const occStart = (0, tz_js_1.wallTimeToUtc)(year, month, day, rule.timeOfDay, tz, disambiguation);
48
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occStart, rule.timeSpan, tz);
49
+ if (occStart < rule.seriesStart)
50
+ continue;
51
+ if (occStart >= seriesEnd)
52
+ continue;
53
+ if (rule.count != null && count >= rule.count) {
54
+ yearEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
55
+ out.push(...yearEmits);
56
+ return out;
57
+ }
58
+ count++;
59
+ if ((0, types_js_1.intersects)(occStart, occEnd, windowStart, windowEnd)) {
60
+ yearEmits.push({ startTimestamp: occStart, endTimestamp: occEnd });
61
+ if (out.length + yearEmits.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
62
+ throw new types_js_1.TooManyOccurrencesError("daysOfYear");
63
+ }
64
+ }
65
+ }
66
+ yearEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
67
+ out.push(...yearEmits);
68
+ }
69
+ return out;
70
+ }
@@ -0,0 +1,3 @@
1
+ import type { InstantRule } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateInstant(rule: InstantRule, tz: string | undefined, windowStart: number, windowEnd: number): Occurrence[];
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateInstant = generateInstant;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // A single occurrence at an absolute UTC instant. The template timezone does
7
+ // not affect the start (the instant is already a fixed point on the timeline);
8
+ // it only flexes an hourSpan end, via computeSpanEnd.
9
+ function generateInstant(rule, tz, windowStart, windowEnd) {
10
+ const start = rule.startOn;
11
+ const end = (0, tz_js_1.computeSpanEnd)(start, rule.timeSpan, tz);
12
+ if (!(0, types_js_1.intersects)(start, end, windowStart, windowEnd))
13
+ return [];
14
+ return [{ startTimestamp: start, endTimestamp: end }];
15
+ }
@@ -0,0 +1,3 @@
1
+ import type { Disambiguation, LocalDateTimeRule } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateLocalDateTime(rule: LocalDateTimeRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateLocalDateTime = generateLocalDateTime;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // A single occurrence expressed in wall-clock terms, resolved through the
7
+ // template timezone (DST-aware). `day` clamps to the month length. When tz is
8
+ // undefined the wall-clock fields are interpreted as UTC. A spring-forward gap
9
+ // or fall-back overlap on the start is resolved per `disambiguation` (see
10
+ // tz.ts) — "skip" drops the occurrence entirely if its wall time doesn't exist.
11
+ function generateLocalDateTime(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
12
+ if (!Number.isInteger(rule.year)) {
13
+ throw new Error(`LocalDateTimeRule.year must be an integer (got ${rule.year})`);
14
+ }
15
+ if (rule.month < 0 || rule.month > 11) {
16
+ throw new Error(`LocalDateTimeRule.month must be in 0..11 (got ${rule.month})`);
17
+ }
18
+ if (rule.day < 1 || rule.day > 31) {
19
+ throw new Error(`LocalDateTimeRule.day must be in 1..31 (got ${rule.day})`);
20
+ }
21
+ const day = Math.min(rule.day, (0, tz_js_1.monthLength)(rule.year, rule.month));
22
+ if (disambiguation === "skip" &&
23
+ (0, tz_js_1.isNonexistentWallTime)(rule.year, rule.month, day, rule.timeOfDay, tz)) {
24
+ return [];
25
+ }
26
+ const start = (0, tz_js_1.wallTimeToUtc)(rule.year, rule.month, day, rule.timeOfDay, tz, disambiguation);
27
+ const end = (0, tz_js_1.computeSpanEnd)(start, rule.timeSpan, tz);
28
+ if (!(0, types_js_1.intersects)(start, end, windowStart, windowEnd))
29
+ return [];
30
+ return [{ startTimestamp: start, endTimestamp: end }];
31
+ }
@@ -0,0 +1,3 @@
1
+ import type { Disambiguation, MonthsOfYearRule } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateMonthsOfYear(rule: MonthsOfYearRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateMonthsOfYear = generateMonthsOfYear;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // Positional yearly recurrence — top of the positional cascade.
7
+ // (months × weeks-of-month × weekdays, with yearInterval as cadence.)
8
+ //
9
+ // For each year in cadence, walk the selected months; in each month, find
10
+ // the Nth weekday matches (e.g. "2nd Tuesday") for each (week, day) pair.
11
+ // If a position doesn't exist that month (e.g. 5th Sunday in a 4-Sunday
12
+ // month), silently skip — same policy as WeeksOfMonthRule.
13
+ function generateMonthsOfYear(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
14
+ if (rule.yearInterval <= 0) {
15
+ throw new Error(`MonthsOfYearRule.yearInterval must be > 0 (got ${rule.yearInterval})`);
16
+ }
17
+ if (rule.months.length === 0) {
18
+ throw new Error("MonthsOfYearRule.months must be non-empty");
19
+ }
20
+ for (const m of rule.months) {
21
+ if (m < 0 || m > 11) {
22
+ throw new Error(`MonthsOfYearRule.months entries must be in 0..11 (got ${m})`);
23
+ }
24
+ }
25
+ if (rule.weeks.length === 0) {
26
+ throw new Error("MonthsOfYearRule.weeks must be non-empty");
27
+ }
28
+ for (const w of rule.weeks) {
29
+ if (w === 0 || w < -5 || w > 5) {
30
+ throw new Error(`MonthsOfYearRule.weeks entries must be in [-5..-1] or [1..5] (got ${w})`);
31
+ }
32
+ }
33
+ if (rule.days.length === 0) {
34
+ throw new Error("MonthsOfYearRule.days must be non-empty");
35
+ }
36
+ if (rule.count != null && rule.count < 0) {
37
+ throw new Error(`MonthsOfYearRule.count must be >= 0 (got ${rule.count})`);
38
+ }
39
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
40
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
41
+ if (rule.seriesStart >= effectiveEnd)
42
+ return [];
43
+ const months = [...new Set(rule.months)].sort((a, b) => a - b);
44
+ const weeks = [...new Set(rule.weeks)];
45
+ const weekdays = [...new Set(rule.days)].sort((a, b) => a - b);
46
+ // Anchor at the start of seriesStart's year (in tz), but actually anchor
47
+ // at its first selected month within that year. We iterate year-by-year.
48
+ const seriesStartParts = (0, tz_js_1.utcToWallParts)(rule.seriesStart, tz);
49
+ let yearAnchor = (0, tz_js_1.wallTimeToUtc)(seriesStartParts.year, 0, 1, 0, tz);
50
+ const out = [];
51
+ let count = 0;
52
+ while (yearAnchor < effectiveEnd) {
53
+ const yp = (0, tz_js_1.utcToWallParts)(yearAnchor, tz);
54
+ const yearEmits = [];
55
+ for (const month of months) {
56
+ for (const week of weeks) {
57
+ for (const dow of weekdays) {
58
+ const targetDay = _nthWeekdayOfMonth(yp.year, month, dow, week);
59
+ if (targetDay == null)
60
+ continue; // Nth doesn't exist this month
61
+ if (disambiguation === "skip" &&
62
+ (0, tz_js_1.isNonexistentWallTime)(yp.year, month, targetDay, rule.timeOfDay, tz)) {
63
+ continue;
64
+ }
65
+ const occStart = (0, tz_js_1.wallTimeToUtc)(yp.year, month, targetDay, rule.timeOfDay, tz, disambiguation);
66
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occStart, rule.timeSpan, tz);
67
+ if (occStart < rule.seriesStart)
68
+ continue;
69
+ if (occStart >= seriesEnd)
70
+ continue;
71
+ if (rule.count != null && count >= rule.count) {
72
+ yearEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
73
+ out.push(...yearEmits);
74
+ return out;
75
+ }
76
+ count++;
77
+ if ((0, types_js_1.intersects)(occStart, occEnd, windowStart, windowEnd)) {
78
+ yearEmits.push({ startTimestamp: occStart, endTimestamp: occEnd });
79
+ if (out.length + yearEmits.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
80
+ throw new types_js_1.TooManyOccurrencesError("monthsOfYear");
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ yearEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
87
+ out.push(...yearEmits);
88
+ // Step yearInterval years. Use addMonthsInTz(12) so leap-year wall-clock
89
+ // arithmetic is correct (it isn't strictly necessary at year granularity,
90
+ // but matches the rest of the codebase).
91
+ yearAnchor = (0, tz_js_1.addMonthsInTz)(yearAnchor, 12 * rule.yearInterval, tz);
92
+ }
93
+ return out;
94
+ }
95
+ // Same helper as WeeksOfMonth uses. Returns day-of-month (1..31) for the
96
+ // Nth occurrence of `dow` in the given month, or null if it doesn't exist.
97
+ function _nthWeekdayOfMonth(year, month, dow, n) {
98
+ const len = (0, tz_js_1.monthLength)(year, month);
99
+ if (n > 0) {
100
+ const firstDow = new Date(Date.UTC(year, month, 1)).getUTCDay();
101
+ const offset = (dow - firstDow + 7) % 7;
102
+ const day = 1 + offset + (n - 1) * 7;
103
+ return day <= len ? day : null;
104
+ }
105
+ const lastDow = new Date(Date.UTC(year, month, len)).getUTCDay();
106
+ const offset = (lastDow - dow + 7) % 7;
107
+ const day = len - offset + (n + 1) * 7;
108
+ return day >= 1 ? day : null;
109
+ }
@@ -0,0 +1,9 @@
1
+ export interface Occurrence {
2
+ startTimestamp: number;
3
+ endTimestamp: number;
4
+ }
5
+ export declare const MAX_OCCURRENCES_PER_GENERATE = 10000;
6
+ export declare class TooManyOccurrencesError extends Error {
7
+ constructor(kind: string);
8
+ }
9
+ export declare function intersects(startA: number, endA: number, startB: number, endB: number): boolean;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TooManyOccurrencesError = exports.MAX_OCCURRENCES_PER_GENERATE = void 0;
4
+ exports.intersects = intersects;
5
+ // Hard ceiling per generate() call. A misconfigured rule (e.g. 1ms anchored
6
+ // interval over a year-long window) would otherwise produce billions of
7
+ // occurrences; we'd rather throw a clear error than blow heap.
8
+ exports.MAX_OCCURRENCES_PER_GENERATE = 10_000;
9
+ class TooManyOccurrencesError extends Error {
10
+ constructor(kind) {
11
+ super(`Generator '${kind}' would emit more than ${exports.MAX_OCCURRENCES_PER_GENERATE} occurrences for the requested window. Narrow the window, increase the interval, or set 'count' on the rule.`);
12
+ this.name = "TooManyOccurrencesError";
13
+ }
14
+ }
15
+ exports.TooManyOccurrencesError = TooManyOccurrencesError;
16
+ // Half-open intersection: [startA, endA) overlaps [startB, endB)?
17
+ function intersects(startA, endA, startB, endB) {
18
+ return Math.max(startA, startB) < Math.min(endA, endB);
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { Disambiguation, WeeksOfMonthRule } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateWeeksOfMonth(rule: WeeksOfMonthRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateWeeksOfMonth = generateWeeksOfMonth;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // "Nth Tuesday of every month", "last Friday of every other month", etc.
7
+ // Iterate month-by-month at monthInterval cadence; for each repeatable day,
8
+ // resolve the target wall date.
9
+ function generateWeeksOfMonth(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
10
+ if (rule.monthInterval <= 0) {
11
+ throw new Error(`WeeksOfMonthRule.monthInterval must be > 0 (got ${rule.monthInterval})`);
12
+ }
13
+ if (rule.weeks.length === 0) {
14
+ throw new Error("WeeksOfMonthRule.weeks must be non-empty");
15
+ }
16
+ for (const w of rule.weeks) {
17
+ if (w === 0 || w < -5 || w > 5) {
18
+ throw new Error(`WeeksOfMonthRule.weeks entries must be in [-5..-1] or [1..5] (got ${w})`);
19
+ }
20
+ }
21
+ if (rule.days.length === 0) {
22
+ throw new Error("WeeksOfMonthRule.days must be non-empty");
23
+ }
24
+ if (rule.count != null && rule.count < 0) {
25
+ throw new Error(`WeeksOfMonthRule.count must be >= 0 (got ${rule.count})`);
26
+ }
27
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
28
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
29
+ if (rule.seriesStart >= effectiveEnd)
30
+ return [];
31
+ const days = [...rule.days].sort((a, b) => a - b);
32
+ // Dedup `weeks` so a duplicated entry doesn't double-emit. Don't sort —
33
+ // we sort the per-month emits at the end of each month iteration anyway.
34
+ const weeks = [...new Set(rule.weeks)];
35
+ // Anchor at seriesStart's month (in tz).
36
+ const seriesStartParts = (0, tz_js_1.utcToWallParts)(rule.seriesStart, tz);
37
+ let monthAnchor = (0, tz_js_1.wallTimeToUtc)(seriesStartParts.year, seriesStartParts.month, 1, 0, tz);
38
+ const out = [];
39
+ let count = 0;
40
+ while (monthAnchor < effectiveEnd) {
41
+ const mp = (0, tz_js_1.utcToWallParts)(monthAnchor, tz);
42
+ const monthEmits = [];
43
+ for (const week of weeks) {
44
+ for (const dow of days) {
45
+ const targetDay = _nthWeekdayOfMonth(mp.year, mp.month, dow, week);
46
+ if (targetDay == null)
47
+ continue; // e.g. 5th Friday in a 4-Friday month
48
+ if (disambiguation === "skip" &&
49
+ (0, tz_js_1.isNonexistentWallTime)(mp.year, mp.month, targetDay, rule.timeOfDay, tz)) {
50
+ continue;
51
+ }
52
+ const occStart = (0, tz_js_1.wallTimeToUtc)(mp.year, mp.month, targetDay, rule.timeOfDay, tz, disambiguation);
53
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occStart, rule.timeSpan, tz);
54
+ if (occStart < rule.seriesStart)
55
+ continue;
56
+ if (occStart >= seriesEnd)
57
+ continue;
58
+ if (rule.count != null && count >= rule.count) {
59
+ // Sort and emit what we collected this month before returning.
60
+ monthEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
61
+ out.push(...monthEmits);
62
+ return out;
63
+ }
64
+ count++;
65
+ if ((0, types_js_1.intersects)(occStart, occEnd, windowStart, windowEnd)) {
66
+ monthEmits.push({ startTimestamp: occStart, endTimestamp: occEnd });
67
+ if (out.length + monthEmits.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
68
+ throw new types_js_1.TooManyOccurrencesError("weeksOfMonth");
69
+ }
70
+ }
71
+ }
72
+ }
73
+ // Sort within-month emits before appending. With multiple weeks (and
74
+ // negative weeks mixed with positive), per-week iteration order doesn't
75
+ // produce ascending startTimestamps.
76
+ monthEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
77
+ out.push(...monthEmits);
78
+ monthAnchor = (0, tz_js_1.addMonthsInTz)(monthAnchor, rule.monthInterval, tz);
79
+ }
80
+ return out;
81
+ }
82
+ // Returns the day-of-month (1..31) for the Nth weekday of the given month,
83
+ // or null if it doesn't exist (e.g. asking for the 5th Tuesday of a 4-Tuesday
84
+ // month). Negative N counts from the end: -1 = last, -2 = second-to-last.
85
+ function _nthWeekdayOfMonth(year, month, dow, n) {
86
+ const len = (0, tz_js_1.monthLength)(year, month);
87
+ if (n > 0) {
88
+ // Find the first occurrence of `dow` in the month, then step by 7s.
89
+ const firstDow = new Date(Date.UTC(year, month, 1)).getUTCDay();
90
+ const offset = (dow - firstDow + 7) % 7;
91
+ const day = 1 + offset + (n - 1) * 7;
92
+ return day <= len ? day : null;
93
+ }
94
+ // n < 0: count from end of month.
95
+ const lastDow = new Date(Date.UTC(year, month, len)).getUTCDay();
96
+ const offset = (lastDow - dow + 7) % 7;
97
+ const day = len - offset + (n + 1) * 7;
98
+ return day >= 1 ? day : null;
99
+ }
@@ -0,0 +1,3 @@
1
+ import type { Disambiguation, YearlyRule } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateYearly(rule: YearlyRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateYearly = generateYearly;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // Simple yearly recurrence: a single month + day. Day clamps to month
7
+ // length (Feb 29 → Feb 28 in non-leap years, Apr 31 → Apr 30, etc.).
8
+ function generateYearly(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
9
+ if (rule.yearInterval <= 0) {
10
+ throw new Error(`YearlyRule.yearInterval must be > 0 (got ${rule.yearInterval})`);
11
+ }
12
+ if (rule.month < 0 || rule.month > 11) {
13
+ throw new Error(`YearlyRule.month must be in 0..11 (got ${rule.month})`);
14
+ }
15
+ if (rule.day < 1 || rule.day > 31) {
16
+ throw new Error(`YearlyRule.day must be in 1..31 (got ${rule.day})`);
17
+ }
18
+ if (rule.count != null && rule.count < 0) {
19
+ throw new Error(`YearlyRule.count must be >= 0 (got ${rule.count})`);
20
+ }
21
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
22
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
23
+ if (rule.seriesStart >= effectiveEnd)
24
+ return [];
25
+ const startYear = (0, tz_js_1.utcToWallParts)(rule.seriesStart, tz).year;
26
+ const endYear = (0, tz_js_1.utcToWallParts)(Math.min(effectiveEnd, Number.MAX_SAFE_INTEGER), tz).year;
27
+ const out = [];
28
+ let count = 0;
29
+ for (let year = startYear; year <= endYear; year += rule.yearInterval) {
30
+ const day = Math.min(rule.day, (0, tz_js_1.monthLength)(year, rule.month));
31
+ if (disambiguation === "skip" &&
32
+ (0, tz_js_1.isNonexistentWallTime)(year, rule.month, day, rule.timeOfDay, tz)) {
33
+ continue;
34
+ }
35
+ const occStart = (0, tz_js_1.wallTimeToUtc)(year, rule.month, day, rule.timeOfDay, tz, disambiguation);
36
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occStart, rule.timeSpan, tz);
37
+ if (occStart < rule.seriesStart)
38
+ continue;
39
+ if (occStart >= seriesEnd)
40
+ continue;
41
+ if (rule.count != null && count >= rule.count)
42
+ break;
43
+ count++;
44
+ if ((0, types_js_1.intersects)(occStart, occEnd, windowStart, windowEnd)) {
45
+ out.push({ startTimestamp: occStart, endTimestamp: occEnd });
46
+ if (out.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
47
+ throw new types_js_1.TooManyOccurrencesError("yearly");
48
+ }
49
+ }
50
+ }
51
+ return out;
52
+ }
@@ -0,0 +1,18 @@
1
+ import type { SchedulerPort } from "./port.js";
2
+ import type { Alteration, ScheduleTemplate } from "./types.js";
3
+ export declare class InMemorySchedulerPort<TPayload extends {
4
+ type: string;
5
+ }> implements SchedulerPort<TPayload> {
6
+ private _templates;
7
+ private _alterations;
8
+ listTemplates(_start: number, _end: number): Promise<ScheduleTemplate<TPayload>[]>;
9
+ getTemplate(id: string): Promise<ScheduleTemplate<TPayload> | null>;
10
+ addTemplate(template: ScheduleTemplate<TPayload>): Promise<void>;
11
+ updateTemplate(template: ScheduleTemplate<TPayload>): Promise<void>;
12
+ removeTemplate(id: string): Promise<void>;
13
+ listAlterations(_start: number, _end: number): Promise<Alteration<TPayload>[]>;
14
+ listAlterationsByTemplate(templateId: string): Promise<Alteration<TPayload>[]>;
15
+ addAlteration(alteration: Alteration<TPayload>): Promise<void>;
16
+ removeAlteration(id: string): Promise<void>;
17
+ removeAlterationsForTemplate(templateId: string): Promise<void>;
18
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemorySchedulerPort = void 0;
4
+ // Reference port. Stores templates and alterations in plain Maps. Returns
5
+ // deep-cloned copies so callers can't mutate internal state.
6
+ //
7
+ // Range queries return everything and let the Scheduler filter — correct,
8
+ // but O(n) per query. A real backend would index on seriesStart/seriesEnd.
9
+ class InMemorySchedulerPort {
10
+ _templates = new Map();
11
+ _alterations = new Map();
12
+ async listTemplates(_start, _end) {
13
+ return [...this._templates.values()].map(clone);
14
+ }
15
+ async getTemplate(id) {
16
+ const t = this._templates.get(id);
17
+ return t == null ? null : clone(t);
18
+ }
19
+ async addTemplate(template) {
20
+ if (this._templates.has(template.id)) {
21
+ throw new Error(`Template already exists: ${template.id}`);
22
+ }
23
+ this._templates.set(template.id, clone(template));
24
+ }
25
+ async updateTemplate(template) {
26
+ if (!this._templates.has(template.id)) {
27
+ throw new Error(`No template with id ${template.id}`);
28
+ }
29
+ this._templates.set(template.id, clone(template));
30
+ }
31
+ async removeTemplate(id) {
32
+ this._templates.delete(id);
33
+ }
34
+ async listAlterations(_start, _end) {
35
+ return [...this._alterations.values()].map(clone);
36
+ }
37
+ async listAlterationsByTemplate(templateId) {
38
+ return [...this._alterations.values()].filter((a) => a.templateId === templateId).map(clone);
39
+ }
40
+ async addAlteration(alteration) {
41
+ if (this._alterations.has(alteration.id)) {
42
+ throw new Error(`Alteration already exists: ${alteration.id}`);
43
+ }
44
+ this._alterations.set(alteration.id, clone(alteration));
45
+ }
46
+ async removeAlteration(id) {
47
+ this._alterations.delete(id);
48
+ }
49
+ async removeAlterationsForTemplate(templateId) {
50
+ for (const [id, a] of this._alterations) {
51
+ if (a.templateId === templateId)
52
+ this._alterations.delete(id);
53
+ }
54
+ }
55
+ }
56
+ exports.InMemorySchedulerPort = InMemorySchedulerPort;
57
+ function clone(value) {
58
+ return JSON.parse(JSON.stringify(value));
59
+ }
@@ -0,0 +1,6 @@
1
+ export { Scheduler } from "./scheduler.js";
2
+ export { InMemorySchedulerPort } from "./in_memory_port.js";
3
+ export type { SchedulerPort } from "./port.js";
4
+ export { Dispatcher, type DispatcherConfig } from "./dispatcher.js";
5
+ export { type DispatcherStore, InMemoryDispatcherStore } from "./dispatcher_store.js";
6
+ export { Day, Month, type RecurrenceRule, type InstantRule, type LocalDateTimeRule, type AnchoredRule, type DaysOfWeekRule, type WeeksOfMonthRule, type MonthsOfYearRule, type YearlyRule, type DaysOfMonthRule, type DaysOfYearRule, type ScheduleTemplate, type Alteration, type ScheduledEvent, type TimeSpan, type Disambiguation, } from "./types.js";
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Month = exports.Day = exports.InMemoryDispatcherStore = exports.Dispatcher = exports.InMemorySchedulerPort = exports.Scheduler = void 0;
4
+ var scheduler_js_1 = require("./scheduler.js");
5
+ Object.defineProperty(exports, "Scheduler", { enumerable: true, get: function () { return scheduler_js_1.Scheduler; } });
6
+ var in_memory_port_js_1 = require("./in_memory_port.js");
7
+ Object.defineProperty(exports, "InMemorySchedulerPort", { enumerable: true, get: function () { return in_memory_port_js_1.InMemorySchedulerPort; } });
8
+ var dispatcher_js_1 = require("./dispatcher.js");
9
+ Object.defineProperty(exports, "Dispatcher", { enumerable: true, get: function () { return dispatcher_js_1.Dispatcher; } });
10
+ var dispatcher_store_js_1 = require("./dispatcher_store.js");
11
+ Object.defineProperty(exports, "InMemoryDispatcherStore", { enumerable: true, get: function () { return dispatcher_store_js_1.InMemoryDispatcherStore; } });
12
+ var types_js_1 = require("./types.js");
13
+ Object.defineProperty(exports, "Day", { enumerable: true, get: function () { return types_js_1.Day; } });
14
+ Object.defineProperty(exports, "Month", { enumerable: true, get: function () { return types_js_1.Month; } });
@@ -0,0 +1,5 @@
1
+ import type { Occurrence } from "./generators/types.js";
2
+ export interface KeyedOccurrence extends Occurrence {
3
+ occurrenceKey: string;
4
+ }
5
+ export declare function assignOccurrenceKeys(occurrences: readonly Occurrence[], tz: string | undefined): KeyedOccurrence[];
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assignOccurrenceKeys = assignOccurrenceKeys;
4
+ const tz_js_1 = require("./tz.js");
5
+ function assignOccurrenceKeys(occurrences, tz) {
6
+ const counts = new Map();
7
+ return occurrences.map((occ) => {
8
+ const w = (0, tz_js_1.utcToWallParts)(occ.startTimestamp, tz);
9
+ const date = `${_pad4(w.year)}-${_pad2(w.month + 1)}-${_pad2(w.day)}`;
10
+ const idx = counts.get(date) ?? 0;
11
+ counts.set(date, idx + 1);
12
+ const occurrenceKey = idx === 0 ? date : `${date}#${idx}`;
13
+ return { ...occ, occurrenceKey };
14
+ });
15
+ }
16
+ function _pad2(n) {
17
+ return n < 10 ? `0${n}` : String(n);
18
+ }
19
+ function _pad4(n) {
20
+ return n < 10 ? `000${n}` : n < 100 ? `00${n}` : n < 1000 ? `0${n}` : String(n);
21
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }