@j13b/scheduler 0.0.0-stage → 0.1.1

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -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 +7 -0
  36. package/dist/cjs/index.js +18 -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 +23 -0
  43. package/dist/cjs/scheduler.js +302 -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/cjs/validate_rule.d.ts +4 -0
  49. package/dist/cjs/validate_rule.js +267 -0
  50. package/dist/dispatcher.d.ts +39 -0
  51. package/dist/dispatcher.d.ts.map +1 -0
  52. package/dist/dispatcher.js +111 -0
  53. package/dist/dispatcher.js.map +1 -0
  54. package/dist/dispatcher_store.d.ts +10 -0
  55. package/dist/dispatcher_store.d.ts.map +1 -0
  56. package/dist/dispatcher_store.js +13 -0
  57. package/dist/dispatcher_store.js.map +1 -0
  58. package/dist/generators/anchored.d.ts +4 -0
  59. package/dist/generators/anchored.d.ts.map +1 -0
  60. package/dist/generators/anchored.js +42 -0
  61. package/dist/generators/anchored.js.map +1 -0
  62. package/dist/generators/days_of_month.d.ts +4 -0
  63. package/dist/generators/days_of_month.d.ts.map +1 -0
  64. package/dist/generators/days_of_month.js +53 -0
  65. package/dist/generators/days_of_month.js.map +1 -0
  66. package/dist/generators/days_of_week.d.ts +6 -0
  67. package/dist/generators/days_of_week.d.ts.map +1 -0
  68. package/dist/generators/days_of_week.js +43 -0
  69. package/dist/generators/days_of_week.js.map +1 -0
  70. package/dist/generators/days_of_year.d.ts +4 -0
  71. package/dist/generators/days_of_year.d.ts.map +1 -0
  72. package/dist/generators/days_of_year.js +50 -0
  73. package/dist/generators/days_of_year.js.map +1 -0
  74. package/dist/generators/instant.d.ts +4 -0
  75. package/dist/generators/instant.d.ts.map +1 -0
  76. package/dist/generators/instant.js +15 -0
  77. package/dist/generators/instant.js.map +1 -0
  78. package/dist/generators/local_date_time.d.ts +4 -0
  79. package/dist/generators/local_date_time.d.ts.map +1 -0
  80. package/dist/generators/local_date_time.js +20 -0
  81. package/dist/generators/local_date_time.js.map +1 -0
  82. package/dist/generators/months_of_year.d.ts +4 -0
  83. package/dist/generators/months_of_year.d.ts.map +1 -0
  84. package/dist/generators/months_of_year.js +64 -0
  85. package/dist/generators/months_of_year.js.map +1 -0
  86. package/dist/generators/types.d.ts +10 -0
  87. package/dist/generators/types.d.ts.map +1 -0
  88. package/dist/generators/types.js +14 -0
  89. package/dist/generators/types.js.map +1 -0
  90. package/dist/generators/weeks_of_month.d.ts +4 -0
  91. package/dist/generators/weeks_of_month.d.ts.map +1 -0
  92. package/dist/generators/weeks_of_month.js +61 -0
  93. package/dist/generators/weeks_of_month.js.map +1 -0
  94. package/dist/generators/yearly.d.ts +4 -0
  95. package/dist/generators/yearly.d.ts.map +1 -0
  96. package/dist/generators/yearly.js +37 -0
  97. package/dist/generators/yearly.js.map +1 -0
  98. package/dist/in_memory_port.d.ts +19 -0
  99. package/dist/in_memory_port.d.ts.map +1 -0
  100. package/dist/in_memory_port.js +45 -0
  101. package/dist/in_memory_port.js.map +1 -0
  102. package/dist/index.d.ts +8 -0
  103. package/dist/index.d.ts.map +1 -0
  104. package/dist/index.js +7 -0
  105. package/dist/occurrence_key.d.ts +6 -0
  106. package/dist/occurrence_key.d.ts.map +1 -0
  107. package/dist/occurrence_key.js +25 -0
  108. package/dist/occurrence_key.js.map +1 -0
  109. package/dist/port.d.ts +16 -0
  110. package/dist/port.d.ts.map +1 -0
  111. package/dist/scheduler.d.ts +24 -0
  112. package/dist/scheduler.d.ts.map +1 -0
  113. package/dist/scheduler.js +241 -0
  114. package/dist/scheduler.js.map +1 -0
  115. package/dist/types.d.ts +150 -0
  116. package/dist/types.d.ts.map +1 -0
  117. package/dist/types.js +29 -0
  118. package/dist/types.js.map +1 -0
  119. package/dist/tz.d.ts +23 -0
  120. package/dist/tz.d.ts.map +1 -0
  121. package/dist/tz.js +131 -0
  122. package/dist/tz.js.map +1 -0
  123. package/dist/validate_rule.d.ts +5 -0
  124. package/dist/validate_rule.d.ts.map +1 -0
  125. package/dist/validate_rule.js +280 -0
  126. package/dist/validate_rule.js.map +1 -0
  127. package/package.json +76 -4
  128. package/src/__tests__/alterations.test.ts +177 -0
  129. package/src/__tests__/anchored.test.ts +107 -0
  130. package/src/__tests__/days_of_month.test.ts +162 -0
  131. package/src/__tests__/days_of_week.test.ts +134 -0
  132. package/src/__tests__/days_of_year.test.ts +158 -0
  133. package/src/__tests__/dispatcher.test.ts +317 -0
  134. package/src/__tests__/instant.test.ts +50 -0
  135. package/src/__tests__/local_date_time.test.ts +139 -0
  136. package/src/__tests__/months_of_year.test.ts +229 -0
  137. package/src/__tests__/scheduler.test.ts +888 -0
  138. package/src/__tests__/tz.test.ts +201 -0
  139. package/src/__tests__/validate_rule.test.ts +190 -0
  140. package/src/__tests__/weeks_of_month.test.ts +195 -0
  141. package/src/__tests__/yearly.test.ts +108 -0
  142. package/src/alterations.ts +108 -0
  143. package/src/dispatcher.ts +184 -0
  144. package/src/dispatcher_store.ts +26 -0
  145. package/src/generators/anchored.ts +69 -0
  146. package/src/generators/days_of_month.ts +100 -0
  147. package/src/generators/days_of_week.ts +101 -0
  148. package/src/generators/days_of_year.ts +88 -0
  149. package/src/generators/instant.ts +18 -0
  150. package/src/generators/local_date_time.ts +38 -0
  151. package/src/generators/months_of_year.ts +143 -0
  152. package/src/generators/types.ts +26 -0
  153. package/src/generators/weeks_of_month.ts +133 -0
  154. package/src/generators/yearly.ts +73 -0
  155. package/src/in_memory_port.ts +70 -0
  156. package/src/index.ts +25 -0
  157. package/src/occurrence_key.ts +42 -0
  158. package/src/port.ts +31 -0
  159. package/src/scheduler.ts +353 -0
  160. package/src/types.ts +369 -0
  161. package/src/tz.ts +246 -0
  162. package/src/validate_rule.ts +311 -0
  163. package/tsconfig.json +25 -0
package/dist/cjs/tz.js ADDED
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isValidTimeZone = isValidTimeZone;
4
+ exports.utcToWallParts = utcToWallParts;
5
+ exports.isNonexistentWallTime = isNonexistentWallTime;
6
+ exports.wallTimeToUtc = wallTimeToUtc;
7
+ exports.dayOfWeekInTz = dayOfWeekInTz;
8
+ exports.addDaysInTz = addDaysInTz;
9
+ exports.addMonthsInTz = addMonthsInTz;
10
+ exports.monthLength = monthLength;
11
+ exports.computeSpanEnd = computeSpanEnd;
12
+ const DAY_MS = 86_400_000;
13
+ const _formatterCache = new Map();
14
+ function _formatter(tz) {
15
+ let f = _formatterCache.get(tz);
16
+ if (f == null) {
17
+ f = new Intl.DateTimeFormat("en-US", {
18
+ timeZone: tz,
19
+ year: "numeric",
20
+ month: "2-digit",
21
+ day: "2-digit",
22
+ hour: "2-digit",
23
+ minute: "2-digit",
24
+ second: "2-digit",
25
+ hour12: false,
26
+ });
27
+ _formatterCache.set(tz, f);
28
+ }
29
+ return f;
30
+ }
31
+ // Is `tz` an IANA zone name (or "UTC") the runtime recognizes? Constructing an
32
+ // Intl.DateTimeFormat with an unknown zone throws RangeError; we probe for that.
33
+ function isValidTimeZone(tz) {
34
+ try {
35
+ new Intl.DateTimeFormat("en-US", { timeZone: tz }).format();
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ function utcToWallParts(utcEpochMs, tz = "UTC") {
43
+ if (tz === "UTC") {
44
+ const d = new Date(utcEpochMs);
45
+ return {
46
+ year: d.getUTCFullYear(),
47
+ month: d.getUTCMonth(),
48
+ day: d.getUTCDate(),
49
+ hour: d.getUTCHours(),
50
+ minute: d.getUTCMinutes(),
51
+ second: d.getUTCSeconds(),
52
+ ms: d.getUTCMilliseconds(),
53
+ dayOfWeek: d.getUTCDay(),
54
+ };
55
+ }
56
+ const parts = _formatter(tz).formatToParts(new Date(utcEpochMs));
57
+ const get = (type) => {
58
+ const p = parts.find((x) => x.type === type);
59
+ if (p == null)
60
+ throw new Error(`Missing date-time part '${type}' for tz '${tz}'`);
61
+ return Number(p.value);
62
+ };
63
+ let hour = get("hour");
64
+ if (hour === 24)
65
+ hour = 0; // some Node versions report midnight as "24"
66
+ const year = get("year");
67
+ const month = get("month") - 1;
68
+ const day = get("day");
69
+ // Day-of-week derived from the wall date via UTC math (the math itself
70
+ // is tz-independent — Date.UTC just gives us a zero-anchored instant).
71
+ const dayOfWeek = new Date(Date.UTC(year, month, day)).getUTCDay();
72
+ // Intl doesn't give us ms — preserve the original ms from utcEpochMs.
73
+ // Sub-second precision is the same in any tz, since offsets are minute-
74
+ // aligned (or, for a few historical zones, second-aligned).
75
+ const ms = ((utcEpochMs % 1000) + 1000) % 1000;
76
+ return {
77
+ year,
78
+ month,
79
+ day,
80
+ hour,
81
+ minute: get("minute"),
82
+ second: get("second"),
83
+ ms,
84
+ dayOfWeek,
85
+ };
86
+ }
87
+ // Offset from UTC to local wall time at the given UTC instant, in ms.
88
+ // Positive for zones east of UTC. (wallAsUtc - utcInstant)
89
+ function _offsetMsAt(utcEpochMs, tz) {
90
+ if (tz === "UTC")
91
+ return 0;
92
+ const w = utcToWallParts(utcEpochMs, tz);
93
+ const wallAsUtc = Date.UTC(w.year, w.month, w.day, w.hour, w.minute, w.second) + w.ms;
94
+ return wallAsUtc - utcEpochMs;
95
+ }
96
+ // The instants that a wall time could map to in `tz`, plus the bracketing
97
+ // offsets. `valid` has length 1 normally, 0 in a spring-forward gap, or 2 in a
98
+ // fall-back overlap (sorted ascending).
99
+ function _possibleInstants(localUtc, tz) {
100
+ const offBefore = _offsetMsAt(localUtc - DAY_MS, tz);
101
+ const offAfter = _offsetMsAt(localUtc + DAY_MS, tz);
102
+ const valid = [];
103
+ const offsets = offBefore === offAfter ? [offBefore] : [offBefore, offAfter];
104
+ for (const off of offsets) {
105
+ const epoch = localUtc - off;
106
+ // Keep this candidate only if it reads back as the wall time we asked for
107
+ // (a gap's candidates won't; an overlap's both will).
108
+ const w = utcToWallParts(epoch, tz);
109
+ const back = Date.UTC(w.year, w.month, w.day, w.hour, w.minute, w.second) + w.ms;
110
+ if (back === localUtc && !valid.includes(epoch))
111
+ valid.push(epoch);
112
+ }
113
+ valid.sort((a, b) => a - b);
114
+ return { valid, offBefore, offAfter };
115
+ }
116
+ // Is this wall time nonexistent (in a spring-forward gap) in `tz`? Used by the
117
+ // "skip" disambiguation to drop occurrences whose wall time doesn't exist.
118
+ function isNonexistentWallTime(year, month, day, msOfDay, tz) {
119
+ if (tz == null || tz === "UTC")
120
+ return false;
121
+ const localUtc = Date.UTC(year, month, day) + msOfDay;
122
+ return _possibleInstants(localUtc, tz).valid.length === 0;
123
+ }
124
+ function wallTimeToUtc(year, month, // 0-indexed
125
+ day, msOfDay, // 0..86_399_999, ms since wall-clock midnight
126
+ tz = "UTC", disambiguation = "compatible") {
127
+ if (tz === "UTC") {
128
+ return Date.UTC(year, month, day) + msOfDay;
129
+ }
130
+ const localUtc = Date.UTC(year, month, day) + msOfDay;
131
+ const { valid, offBefore, offAfter } = _possibleInstants(localUtc, tz);
132
+ if (valid.length === 1)
133
+ return valid[0];
134
+ if (valid.length === 2) {
135
+ // Fall-back overlap. compatible/earlier/skip → earlier; later → later.
136
+ if (disambiguation === "reject") {
137
+ throw new Error(`Ambiguous wall time ${new Date(localUtc).toISOString().slice(0, 19)} in '${tz}' (fall-back overlap) with disambiguation "reject"`);
138
+ }
139
+ return disambiguation === "later" ? valid[1] : valid[0];
140
+ }
141
+ // Spring-forward gap (valid.length === 0). earlier → the earlier instant
142
+ // (post-transition offset); compatible/later/skip → the later instant.
143
+ // ("skip" only reaches here defensively — callers pre-check via
144
+ // isNonexistentWallTime; we resolve it like "compatible".)
145
+ if (disambiguation === "reject") {
146
+ throw new Error(`Nonexistent wall time ${new Date(localUtc).toISOString().slice(0, 19)} in '${tz}' (spring-forward gap) with disambiguation "reject"`);
147
+ }
148
+ return disambiguation === "earlier" ? localUtc - offAfter : localUtc - offBefore;
149
+ }
150
+ function dayOfWeekInTz(utcEpochMs, tz = "UTC") {
151
+ return utcToWallParts(utcEpochMs, tz).dayOfWeek;
152
+ }
153
+ // Add N calendar days in the wall-clock sense — important across DST
154
+ // boundaries, where one civil day is 23 or 25 hours, not 24.
155
+ function addDaysInTz(utcEpochMs, days, tz = "UTC") {
156
+ const w = utcToWallParts(utcEpochMs, tz);
157
+ return wallTimeToUtc(w.year, w.month, w.day + days, _hmsToMs(w), tz);
158
+ }
159
+ function addMonthsInTz(utcEpochMs, months, tz = "UTC") {
160
+ const w = utcToWallParts(utcEpochMs, tz);
161
+ return wallTimeToUtc(w.year, w.month + months, w.day, _hmsToMs(w), tz);
162
+ }
163
+ // Civil-calendar month length — tz-independent.
164
+ function monthLength(year, month) {
165
+ // Date.UTC(y, m+1, 0) gives the last day of month `m` (0-indexed).
166
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
167
+ }
168
+ // Returns endTimestamp for an occurrence given its start and a TimeSpan.
169
+ //
170
+ // For `duration`: pure UTC ms addition. The wall clock may show ±1h different
171
+ // elapsed time on DST nights, but real elapsed time is exactly `ms`.
172
+ //
173
+ // For `hourSpan`: add hours in wall-clock space (handles midnight crossing
174
+ // and DST flex). On a tz-less template, hourSpan collapses to hours*3_600_000
175
+ // since there's no wall clock to flex against.
176
+ //
177
+ // When span is undefined, defaults to 1ms (so the half-open intersection
178
+ // emits cron-fire occurrences correctly).
179
+ function computeSpanEnd(startUtc, span, tz) {
180
+ if (span == null)
181
+ return startUtc + 1;
182
+ if (span.type === "duration")
183
+ return startUtc + span.ms;
184
+ // hourSpan
185
+ if (tz == null) {
186
+ // No tz → treat hours as a fixed real-time interval.
187
+ return startUtc + span.hours * 3_600_000;
188
+ }
189
+ const w = utcToWallParts(startUtc, tz);
190
+ const totalMs = _hmsToMs(w) + span.hours * 3_600_000;
191
+ const daysAhead = Math.floor(totalMs / 86_400_000);
192
+ const endMsInDay = totalMs - daysAhead * 86_400_000;
193
+ return wallTimeToUtc(w.year, w.month, w.day + daysAhead, endMsInDay, tz);
194
+ }
195
+ function _hmsToMs(w) {
196
+ return w.hour * 3_600_000 + w.minute * 60_000 + w.second * 1000 + w.ms;
197
+ }
@@ -0,0 +1,4 @@
1
+ import type { RecurrenceRule, TimeSpan } from "./types.js";
2
+ export declare const RULE_KINDS: RecurrenceRule["kind"][];
3
+ export declare function validateRule(rule: RecurrenceRule): void;
4
+ export declare function validateTimeSpan(span: unknown): asserts span is TimeSpan | undefined;
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RULE_KINDS = void 0;
4
+ exports.validateRule = validateRule;
5
+ exports.validateTimeSpan = validateTimeSpan;
6
+ // ─── Rule validation ──────────────────────────────────────────
7
+ //
8
+ // A rule often comes from outside the type system: JSON in a database, a
9
+ // request body, or an LLM tool call. So validation checks the WHOLE shape —
10
+ // every required field present, of the right type, in range, and no fields
11
+ // the kind does not have — and throws an Error that names the field and the
12
+ // form it expects. A bad rule never reaches a generator, where it would crash
13
+ // with "Cannot read properties of undefined" or "Invalid time value".
14
+ const DAY_MS = 24 * 60 * 60 * 1000;
15
+ const DAYS_HINT = "Day numbers 0..6 (0=Sunday, 1=Monday … 6=Saturday)";
16
+ const MONTHS_HINT = "Month numbers 0..11 (0=January … 11=December)";
17
+ const WEEKS_HINT = "week-of-month numbers 1..5 (1=first) or -1..-5 (-1=last)";
18
+ const TIME_OF_DAY_HINT = "ms after local midnight, 0..86_399_999 (7:00 = 25_200_000)";
19
+ const TIMESTAMP_HINT = "a UTC timestamp in ms";
20
+ // The common series fields every repeating kind has.
21
+ const SERIES = {
22
+ timeSpan: { required: false, check: (v) => validateTimeSpan(v) },
23
+ seriesStart: { required: true, check: timestamp },
24
+ seriesEnd: { required: true, check: nullable(timestamp, "null (no end)") },
25
+ count: {
26
+ required: false,
27
+ check: nullable(int(0, Number.MAX_SAFE_INTEGER, "a whole number >= 0"), "null"),
28
+ },
29
+ };
30
+ const TIME_OF_DAY = { required: true, check: int(0, DAY_MS - 1, TIME_OF_DAY_HINT) };
31
+ const KINDS = {
32
+ instant: {
33
+ name: "InstantRule",
34
+ fields: {
35
+ startOn: { required: true, check: timestamp },
36
+ timeSpan: { required: false, check: (v) => validateTimeSpan(v) },
37
+ },
38
+ },
39
+ localDateTime: {
40
+ name: "LocalDateTimeRule",
41
+ fields: {
42
+ year: { required: true, check: int(1, 9999, "a year such as 2026") },
43
+ month: { required: true, check: int(0, 11, MONTHS_HINT) },
44
+ day: { required: true, check: int(1, 31, "a day of the month 1..31") },
45
+ timeOfDay: TIME_OF_DAY,
46
+ timeSpan: { required: false, check: (v) => validateTimeSpan(v) },
47
+ },
48
+ },
49
+ anchored: {
50
+ name: "AnchoredRule",
51
+ fields: {
52
+ anchorDate: { required: true, check: timestamp },
53
+ intervalMs: { required: true, check: positive("an interval in ms > 0") },
54
+ ...SERIES,
55
+ },
56
+ },
57
+ daysOfWeek: {
58
+ name: "DaysOfWeekRule",
59
+ fields: {
60
+ days: { required: true, check: intList(0, 6, DAYS_HINT) },
61
+ weekInterval: {
62
+ required: true,
63
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every week)"),
64
+ },
65
+ timeOfDay: TIME_OF_DAY,
66
+ ...SERIES,
67
+ },
68
+ },
69
+ weeksOfMonth: {
70
+ name: "WeeksOfMonthRule",
71
+ fields: {
72
+ days: { required: true, check: intList(0, 6, DAYS_HINT) },
73
+ weeks: { required: true, check: weekList },
74
+ monthInterval: {
75
+ required: true,
76
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every month)"),
77
+ },
78
+ timeOfDay: TIME_OF_DAY,
79
+ ...SERIES,
80
+ },
81
+ },
82
+ monthsOfYear: {
83
+ name: "MonthsOfYearRule",
84
+ fields: {
85
+ months: { required: true, check: intList(0, 11, MONTHS_HINT) },
86
+ weeks: { required: true, check: weekList },
87
+ days: { required: true, check: intList(0, 6, DAYS_HINT) },
88
+ yearInterval: {
89
+ required: true,
90
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
91
+ },
92
+ timeOfDay: TIME_OF_DAY,
93
+ ...SERIES,
94
+ },
95
+ },
96
+ yearly: {
97
+ name: "YearlyRule",
98
+ fields: {
99
+ month: { required: true, check: int(0, 11, MONTHS_HINT) },
100
+ day: { required: true, check: int(1, 31, "a day of the month 1..31") },
101
+ yearInterval: {
102
+ required: true,
103
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
104
+ },
105
+ timeOfDay: TIME_OF_DAY,
106
+ ...SERIES,
107
+ },
108
+ },
109
+ daysOfMonth: {
110
+ name: "DaysOfMonthRule",
111
+ fields: {
112
+ months: { required: true, check: intList(0, 11, MONTHS_HINT) },
113
+ daysOfMonth: { required: true, check: intList(1, 31, "days of the month 1..31") },
114
+ yearInterval: {
115
+ required: true,
116
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
117
+ },
118
+ timeOfDay: TIME_OF_DAY,
119
+ ...SERIES,
120
+ },
121
+ },
122
+ daysOfYear: {
123
+ name: "DaysOfYearRule",
124
+ fields: {
125
+ daysOfYear: { required: true, check: intList(1, 366, "days of the year 1..366") },
126
+ yearInterval: {
127
+ required: true,
128
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
129
+ },
130
+ timeOfDay: TIME_OF_DAY,
131
+ ...SERIES,
132
+ },
133
+ },
134
+ };
135
+ // The rule kinds, in the order the types declare them.
136
+ exports.RULE_KINDS = Object.keys(KINDS);
137
+ // Throws when `rule` is not a valid RecurrenceRule. The message names the
138
+ // first wrong field and the form it expects.
139
+ function validateRule(rule) {
140
+ const raw = rule;
141
+ if (typeof raw !== "object" || raw == null || Array.isArray(raw)) {
142
+ throw new Error(`A rule must be an object with a "kind" (one of ${exports.RULE_KINDS.join(", ")})`);
143
+ }
144
+ const obj = raw;
145
+ const kind = KINDS[obj.kind];
146
+ if (kind == null) {
147
+ throw new Error(`Unknown rule kind ${JSON.stringify(obj.kind)} (expected one of ${exports.RULE_KINDS.join(", ")})`);
148
+ }
149
+ const known = Object.keys(kind.fields);
150
+ for (const key of Object.keys(obj)) {
151
+ if (key !== "kind" && !known.includes(key)) {
152
+ throw new Error(`${kind.name} has no field "${key}" (its fields are: ${known.join(", ")})`);
153
+ }
154
+ }
155
+ for (const [field, spec] of Object.entries(kind.fields)) {
156
+ const label = `${kind.name}.${field}`;
157
+ if (obj[field] === undefined) {
158
+ if (spec.required)
159
+ throw new Error(`${label} is required: ${describe(spec, label)}`);
160
+ continue;
161
+ }
162
+ spec.check(obj[field], label);
163
+ }
164
+ const seriesStart = obj.seriesStart;
165
+ const seriesEnd = obj.seriesEnd;
166
+ if (seriesStart != null && seriesEnd != null && seriesEnd <= seriesStart) {
167
+ throw new Error(`${kind.name}.seriesEnd must be > seriesStart`);
168
+ }
169
+ }
170
+ // Throws when `span` is not a TimeSpan. Undefined is valid (the rule's
171
+ // default span applies).
172
+ function validateTimeSpan(span) {
173
+ if (span === undefined)
174
+ return;
175
+ const form = '{ "type": "duration", "ms": <ms >= 1> } or { "type": "hourSpan", "hours": <hours > 0> }';
176
+ if (typeof span !== "object" || span == null) {
177
+ throw new Error(`timeSpan must be ${form}`);
178
+ }
179
+ const s = span;
180
+ if (s.type === "duration") {
181
+ if (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 1) {
182
+ throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${String(s.ms)})`);
183
+ }
184
+ return;
185
+ }
186
+ if (s.type === "hourSpan") {
187
+ if (typeof s.hours !== "number" || !Number.isFinite(s.hours) || s.hours <= 0) {
188
+ throw new Error(`timeSpan.hours must be > 0 (got ${String(s.hours)})`);
189
+ }
190
+ return;
191
+ }
192
+ throw new Error(`timeSpan must be ${form} (got type ${JSON.stringify(s.type)})`);
193
+ }
194
+ // ─── Field checks ─────────────────────────────────────────────
195
+ // What the field should hold, for "is required" messages: each check is
196
+ // called on undefined and its message reused.
197
+ function describe(spec, label) {
198
+ try {
199
+ spec.check(undefined, label);
200
+ }
201
+ catch (err) {
202
+ const msg = err instanceof Error ? err.message : String(err);
203
+ const at = msg.indexOf(" must be ");
204
+ if (at >= 0)
205
+ return msg.slice(at + " must be ".length).replace(/ \(got [^)]*\)$/, "");
206
+ }
207
+ return "see the rule's type";
208
+ }
209
+ function show(v) {
210
+ return v === undefined ? "nothing" : JSON.stringify(v);
211
+ }
212
+ function isInt(v) {
213
+ return typeof v === "number" && Number.isInteger(v);
214
+ }
215
+ function int(min, max, hint) {
216
+ return (v, field) => {
217
+ if (!isInt(v) || v < min || v > max)
218
+ throw new Error(`${field} must be ${hint} (got ${show(v)})`);
219
+ };
220
+ }
221
+ function positive(hint) {
222
+ return (v, field) => {
223
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
224
+ throw new Error(`${field} must be ${hint} (got ${show(v)})`);
225
+ }
226
+ };
227
+ }
228
+ function timestamp(v, field) {
229
+ if (typeof v !== "number" || !Number.isFinite(v)) {
230
+ throw new Error(`${field} must be ${TIMESTAMP_HINT} (got ${show(v)})`);
231
+ }
232
+ }
233
+ function nullable(check, nullHint) {
234
+ return (v, field) => {
235
+ if (v === null)
236
+ return;
237
+ try {
238
+ check(v, field);
239
+ }
240
+ catch (err) {
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ throw new Error(msg.replace(/ \(got /, ` or ${nullHint} (got `));
243
+ }
244
+ };
245
+ }
246
+ function intList(min, max, hint) {
247
+ return (v, field) => {
248
+ const form = `a non-empty array of ${hint}`;
249
+ if (!Array.isArray(v) || v.length === 0)
250
+ throw new Error(`${field} must be ${form} (got ${show(v)})`);
251
+ for (const x of v) {
252
+ if (!isInt(x) || x < min || x > max) {
253
+ throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(x)} is out of range)`);
254
+ }
255
+ }
256
+ };
257
+ }
258
+ function weekList(v, field) {
259
+ const form = `a non-empty array of ${WEEKS_HINT}`;
260
+ if (!Array.isArray(v) || v.length === 0)
261
+ throw new Error(`${field} must be ${form} (got ${show(v)})`);
262
+ for (const w of v) {
263
+ if (!isInt(w) || w === 0 || w < -5 || w > 5) {
264
+ throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(w)} is out of range)`);
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,39 @@
1
+ import { DispatcherStore } from './dispatcher_store.js';
2
+ import { Scheduler } from './scheduler.js';
3
+ import { ScheduledEvent } from './types.js';
4
+ export interface DispatcherConfig<TPayload extends {
5
+ type: string;
6
+ }> {
7
+ scheduler: Scheduler<TPayload>;
8
+ fire: (event: ScheduledEvent<TPayload>) => Promise<void> | void;
9
+ store?: DispatcherStore;
10
+ onError?: (err: unknown, event?: ScheduledEvent<TPayload>) => void;
11
+ idleTimeoutMs?: number;
12
+ now?: () => number;
13
+ setTimeout?: (fn: () => void, ms: number) => unknown;
14
+ clearTimeout?: (handle: unknown) => void;
15
+ }
16
+ export declare class Dispatcher<TPayload extends {
17
+ type: string;
18
+ }> {
19
+ private readonly _scheduler;
20
+ private readonly _fire;
21
+ private readonly _store;
22
+ private readonly _onError;
23
+ private readonly _idleTimeoutMs;
24
+ private readonly _now;
25
+ private readonly _setTimeout;
26
+ private readonly _clearTimeout;
27
+ private _running;
28
+ private _wakeResolve;
29
+ private _changeSubscription;
30
+ constructor(config: DispatcherConfig<TPayload>);
31
+ start(): Promise<void>;
32
+ stop(): void;
33
+ private _loop;
34
+ private _findNextTarget;
35
+ private _tick;
36
+ private _sleepUntil;
37
+ private _wake;
38
+ }
39
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAA2B,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAOjD,MAAM,WAAW,gBAAgB,CAAC,QAAQ,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;IACjE,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAI/B,IAAI,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAEhE,KAAK,CAAC,EAAE,eAAe,CAAC;IAExB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;IAEnE,aAAa,CAAC,EAAE,MAAM,CAAC;IAGvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AASD,qBAAa,UAAU,CAAC,QAAQ,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;IAClF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2D;IACpF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0C;IACtE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAE1D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,mBAAmB,CAAyC;gBAExD,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC;IAexC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B,IAAI,IAAI,IAAI;YAYE,KAAK;YAYL,eAAe;YAiBf,KAAK;YAgCL,WAAW;IAuBzB,OAAO,CAAC,KAAK;CAGd"}
@@ -0,0 +1,111 @@
1
+ import { InMemoryDispatcherStore } from "./dispatcher_store.js";
2
+ // src/dispatcher.ts
3
+ var DEFAULT_IDLE_TIMEOUT_MS = 36e5;
4
+ var Dispatcher = class {
5
+ _scheduler;
6
+ _fire;
7
+ _store;
8
+ _onError;
9
+ _idleTimeoutMs;
10
+ _now;
11
+ _setTimeout;
12
+ _clearTimeout;
13
+ _running = false;
14
+ _wakeResolve = null;
15
+ _changeSubscription = null;
16
+ constructor(config) {
17
+ this._scheduler = config.scheduler;
18
+ this._fire = config.fire;
19
+ this._store = config.store ?? new InMemoryDispatcherStore();
20
+ this._onError = config.onError ?? defaultOnError;
21
+ this._idleTimeoutMs = config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
22
+ this._now = config.now ?? Date.now;
23
+ this._setTimeout = config.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));
24
+ this._clearTimeout = config.clearTimeout ?? ((h) => globalThis.clearTimeout(h));
25
+ }
26
+ async start() {
27
+ if (this._running) return;
28
+ this._running = true;
29
+ this._changeSubscription = this._scheduler.changed.subscribe(() => this._wake());
30
+ await this._tick();
31
+ if (this._running) this._loop();
32
+ }
33
+ stop() {
34
+ if (!this._running) return;
35
+ this._running = false;
36
+ if (this._changeSubscription != null) {
37
+ this._changeSubscription.unsubscribe();
38
+ this._changeSubscription = null;
39
+ }
40
+ this._wake();
41
+ }
42
+ async _loop() {
43
+ while (this._running) {
44
+ const target = await this._findNextTarget();
45
+ if (!this._running) return;
46
+ await this._sleepUntil(target);
47
+ if (!this._running) return;
48
+ await this._tick();
49
+ }
50
+ }
51
+ async _findNextTarget() {
52
+ try {
53
+ const next = (await this._scheduler.nextDue(this._now(), 1))[0];
54
+ return next != null ? next.startTimestamp : this._now() + this._idleTimeoutMs;
55
+ } catch (err) {
56
+ this._onError(err);
57
+ return this._now() + this._idleTimeoutMs;
58
+ }
59
+ }
60
+ async _tick() {
61
+ const cursor = await this._store.loadCursor() ?? this._now();
62
+ const windowEnd = this._now() + 1;
63
+ if (windowEnd <= cursor) return;
64
+ let events;
65
+ try {
66
+ events = await this._scheduler.list(cursor, windowEnd);
67
+ } catch (err) {
68
+ this._onError(err);
69
+ return;
70
+ }
71
+ for (const event of events) {
72
+ if (!this._running) return;
73
+ try {
74
+ await this._fire(event);
75
+ } catch (err) {
76
+ this._onError(err, event);
77
+ }
78
+ }
79
+ await this._store.saveCursor(windowEnd);
80
+ }
81
+ async _sleepUntil(targetMs) {
82
+ const remaining = targetMs - this._now();
83
+ if (remaining <= 0) return;
84
+ await new Promise((resolve) => {
85
+ let done = false;
86
+ const timer = this._setTimeout(() => {
87
+ if (done) return;
88
+ done = true;
89
+ this._wakeResolve = null;
90
+ resolve();
91
+ }, remaining);
92
+ this._wakeResolve = () => {
93
+ if (done) return;
94
+ done = true;
95
+ this._clearTimeout(timer);
96
+ this._wakeResolve = null;
97
+ resolve();
98
+ };
99
+ });
100
+ }
101
+ _wake() {
102
+ if (this._wakeResolve != null) this._wakeResolve();
103
+ }
104
+ };
105
+ function defaultOnError(err, event) {
106
+ if (event != null) console.error(`[Dispatcher] fire failed for ${event.templateId}/${event.occurrenceKey}:`, err);
107
+ else console.error("[Dispatcher] error:", err);
108
+ }
109
+ export { Dispatcher };
110
+
111
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.js","names":[],"sources":["../src/dispatcher.ts"],"sourcesContent":["import type { IEventSubscription } from \"@j13b/state\";\nimport { type DispatcherStore, InMemoryDispatcherStore } from \"./dispatcher_store.js\";\nimport type { Scheduler } from \"./scheduler.js\";\nimport type { ScheduledEvent } from \"./types.js\";\n\n// Default safety-net wait when nothing is scheduled. The Scheduler.changed\n// event normally wakes us as soon as something gets added, so this is just\n// a backstop in case a notification is missed.\nconst DEFAULT_IDLE_TIMEOUT_MS = 60 * 60 * 1000;\n\nexport interface DispatcherConfig<TPayload extends { type: string }> {\n scheduler: Scheduler<TPayload>;\n // Called for each due event in cursor → now order. Errors thrown from here\n // are passed to onError; the cursor advances regardless (cron-style: a\n // permanently-failing fire shouldn't block the queue forever).\n fire: (event: ScheduledEvent<TPayload>) => Promise<void> | void;\n // Persists the cursor across restarts. Defaults to in-memory (no recovery).\n store?: DispatcherStore;\n // Called when fire() throws or list() fails. Defaults to console.error.\n onError?: (err: unknown, event?: ScheduledEvent<TPayload>) => void;\n // Backstop wait when no event is scheduled. Default 1 hour. Lower for tests.\n idleTimeoutMs?: number;\n // Clock injection (Date.now) and timer injection (setTimeout/clearTimeout).\n // Defaults to wall clock. Inject for deterministic tests.\n now?: () => number;\n setTimeout?: (fn: () => void, ms: number) => unknown;\n clearTimeout?: (handle: unknown) => void;\n}\n\n// Drives a Scheduler in real time. The loop:\n// 1. Catch up: list(cursor, now) → fire each → advance cursor → save.\n// 2. Find next: nextDue(now, 1) → wait until its startTimestamp.\n// 3. Wait may end early via Scheduler.changed (a write happened) or stop().\n// Catch-up handles crashes AND in-process adds we missed. The change event\n// handles in-process adds while we're sleeping — the dispatcher wakes\n// immediately and re-queries instead of firing the stale \"next\" event late.\nexport class Dispatcher<TPayload extends { type: string }> {\n private readonly _scheduler: Scheduler<TPayload>;\n private readonly _fire: (event: ScheduledEvent<TPayload>) => Promise<void> | void;\n private readonly _store: DispatcherStore;\n private readonly _onError: (err: unknown, event?: ScheduledEvent<TPayload>) => void;\n private readonly _idleTimeoutMs: number;\n private readonly _now: () => number;\n private readonly _setTimeout: (fn: () => void, ms: number) => unknown;\n private readonly _clearTimeout: (handle: unknown) => void;\n\n private _running = false;\n private _wakeResolve: (() => void) | null = null;\n private _changeSubscription: IEventSubscription<void> | null = null;\n\n constructor(config: DispatcherConfig<TPayload>) {\n this._scheduler = config.scheduler;\n this._fire = config.fire;\n this._store = config.store ?? new InMemoryDispatcherStore();\n this._onError = config.onError ?? defaultOnError;\n this._idleTimeoutMs = config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n this._now = config.now ?? Date.now;\n this._setTimeout = config.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));\n this._clearTimeout =\n config.clearTimeout ?? ((h) => globalThis.clearTimeout(h as ReturnType<typeof setTimeout>));\n }\n\n // Starts the loop. Resolves after the first catch-up pass completes (so\n // tests can await an initial sync). The loop keeps running in the\n // background until stop() is called.\n async start(): Promise<void> {\n if (this._running) return;\n this._running = true;\n this._changeSubscription = this._scheduler.changed.subscribe(() => this._wake());\n // First iteration in foreground so callers can await initial catch-up.\n await this._tick();\n if (this._running) void this._loop();\n }\n\n // Halts the loop. Any in-flight fire() completes; subsequent fires are\n // skipped. The Scheduler subscription is removed.\n stop(): void {\n if (!this._running) return;\n this._running = false;\n if (this._changeSubscription != null) {\n this._changeSubscription.unsubscribe();\n this._changeSubscription = null;\n }\n this._wake();\n }\n\n // ─── Internals ──────────────────────────────────────────────\n\n private async _loop(): Promise<void> {\n while (this._running) {\n const target = await this._findNextTarget();\n if (!this._running) return;\n await this._sleepUntil(target);\n if (!this._running) return;\n await this._tick();\n }\n }\n\n // Returns the epoch-ms instant the dispatcher should next wake. Either\n // the next due event's startTimestamp, or now + idleTimeoutMs.\n private async _findNextTarget(): Promise<number> {\n try {\n const upcoming = await this._scheduler.nextDue(this._now(), 1);\n const next = upcoming[0];\n return next != null ? next.startTimestamp : this._now() + this._idleTimeoutMs;\n } catch (err) {\n this._onError(err);\n return this._now() + this._idleTimeoutMs;\n }\n }\n\n // Catch-up pass: fire everything with start in [cursor, now]. We use\n // `now + 1` as the half-open windowEnd so an event scheduled exactly at\n // the wake instant is included (the wake from setTimeout lands on the\n // event's startTimestamp, and we don't want to miss it). Advancing the\n // cursor to `now + 1` likewise avoids re-listing the same event on the\n // next tick.\n private async _tick(): Promise<void> {\n const cursor = (await this._store.loadCursor()) ?? this._now();\n const now = this._now();\n const windowEnd = now + 1;\n if (windowEnd <= cursor) {\n // Cursor is already at or past `now + 1` — happens on the very first\n // tick when we just-saved-and-rewoke. Nothing to do.\n return;\n }\n\n let events: ScheduledEvent<TPayload>[];\n try {\n events = await this._scheduler.list(cursor, windowEnd);\n } catch (err) {\n this._onError(err);\n // Don't advance cursor on a list failure — try again next tick.\n return;\n }\n\n for (const event of events) {\n if (!this._running) return;\n try {\n await this._fire(event);\n } catch (err) {\n this._onError(err, event);\n }\n }\n await this._store.saveCursor(windowEnd);\n }\n\n // Sleeps until either the target epoch-ms instant, or wake() is called\n // (via Scheduler.changed or stop()). No-op if target is already past.\n private async _sleepUntil(targetMs: number): Promise<void> {\n const remaining = targetMs - this._now();\n if (remaining <= 0) return;\n await new Promise<void>((resolve) => {\n let done = false;\n const timer = this._setTimeout(() => {\n if (done) return;\n done = true;\n this._wakeResolve = null;\n resolve();\n }, remaining);\n this._wakeResolve = () => {\n if (done) return;\n done = true;\n this._clearTimeout(timer);\n this._wakeResolve = null;\n resolve();\n };\n });\n }\n\n // Resolves any in-flight _sleepUntil. Called by stop() and by the\n // Scheduler.changed subscription.\n private _wake(): void {\n if (this._wakeResolve != null) this._wakeResolve();\n }\n}\n\nfunction defaultOnError(err: unknown, event?: ScheduledEvent<{ type: string }>): void {\n if (event != null) {\n console.error(`[Dispatcher] fire failed for ${event.templateId}/${event.occurrenceKey}:`, err);\n } else {\n console.error(\"[Dispatcher] error:\", err);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export interface DispatcherStore {
2
+ loadCursor(): Promise<number | null>;
3
+ saveCursor(cursor: number): Promise<void>;
4
+ }
5
+ export declare class InMemoryDispatcherStore implements DispatcherStore {
6
+ private _cursor;
7
+ loadCursor(): Promise<number | null>;
8
+ saveCursor(cursor: number): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=dispatcher_store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher_store.d.ts","sourceRoot":"","sources":["../src/dispatcher_store.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,eAAe;IAC9B,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAID,qBAAa,uBAAwB,YAAW,eAAe;IAC7D,OAAO,CAAC,OAAO,CAAuB;IAEhC,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIpC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
@@ -0,0 +1,13 @@
1
+ // src/dispatcher_store.ts
2
+ var InMemoryDispatcherStore = class {
3
+ _cursor = null;
4
+ async loadCursor() {
5
+ return this._cursor;
6
+ }
7
+ async saveCursor(cursor) {
8
+ this._cursor = cursor;
9
+ }
10
+ };
11
+ export { InMemoryDispatcherStore };
12
+
13
+ //# sourceMappingURL=dispatcher_store.js.map