@zintrust/core 0.1.41 → 0.1.42

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 (193) hide show
  1. package/package.json +17 -1
  2. package/src/boot/bootstrap.js +27 -11
  3. package/src/boot/registry/runtime.d.ts.map +1 -1
  4. package/src/boot/registry/runtime.js +11 -0
  5. package/src/cli/CLI.d.ts.map +1 -1
  6. package/src/cli/CLI.js +12 -0
  7. package/src/cli/commands/ConfigCommand.d.ts.map +1 -1
  8. package/src/cli/commands/ConfigCommand.js +3 -5
  9. package/src/cli/commands/D1LearnCommand.d.ts +9 -0
  10. package/src/cli/commands/D1LearnCommand.d.ts.map +1 -0
  11. package/src/cli/commands/D1LearnCommand.js +143 -0
  12. package/src/cli/commands/D1MigrateCommand.d.ts.map +1 -1
  13. package/src/cli/commands/D1MigrateCommand.js +55 -16
  14. package/src/cli/commands/InitContainerCommand.d.ts.map +1 -1
  15. package/src/cli/commands/InitContainerCommand.js +21 -6
  16. package/src/cli/commands/InitEcosystemCommand.d.ts +6 -0
  17. package/src/cli/commands/InitEcosystemCommand.d.ts.map +1 -0
  18. package/src/cli/commands/InitEcosystemCommand.js +51 -0
  19. package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
  20. package/src/cli/commands/MigrateCommand.js +78 -36
  21. package/src/cli/commands/MigrateWorkerCommand.d.ts.map +1 -1
  22. package/src/cli/commands/MigrateWorkerCommand.js +36 -2
  23. package/src/cli/commands/PutCommand.d.ts +6 -0
  24. package/src/cli/commands/PutCommand.d.ts.map +1 -0
  25. package/src/cli/commands/PutCommand.js +173 -0
  26. package/src/cli/commands/QueueRecoveryCommand.d.ts.map +1 -1
  27. package/src/cli/commands/QueueRecoveryCommand.js +113 -14
  28. package/src/cli/commands/ScheduleListCommand.d.ts +6 -0
  29. package/src/cli/commands/ScheduleListCommand.d.ts.map +1 -0
  30. package/src/cli/commands/ScheduleListCommand.js +62 -0
  31. package/src/cli/commands/ScheduleRunCommand.d.ts +6 -0
  32. package/src/cli/commands/ScheduleRunCommand.d.ts.map +1 -0
  33. package/src/cli/commands/ScheduleRunCommand.js +32 -0
  34. package/src/cli/commands/ScheduleStartCommand.d.ts +6 -0
  35. package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -0
  36. package/src/cli/commands/ScheduleStartCommand.js +40 -0
  37. package/src/cli/commands/SecretsCommand.d.ts.map +1 -1
  38. package/src/cli/commands/SecretsCommand.js +2 -2
  39. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts +6 -0
  40. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts.map +1 -0
  41. package/src/cli/commands/schedule/ScheduleCliSupport.js +55 -0
  42. package/src/cli/config/ConfigManager.d.ts.map +1 -1
  43. package/src/cli/config/ConfigManager.js +8 -1
  44. package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
  45. package/src/cli/d1/D1SqlMigrations.js +11 -1
  46. package/src/cli/d1/WranglerConfig.d.ts.map +1 -1
  47. package/src/cli/d1/WranglerConfig.js +34 -2
  48. package/src/cli/services/VersionChecker.d.ts.map +1 -1
  49. package/src/cli/services/VersionChecker.js +5 -1
  50. package/src/cli/utils/DatabaseCliUtils.d.ts.map +1 -1
  51. package/src/cli/utils/DatabaseCliUtils.js +6 -1
  52. package/src/cli/utils/EnvFileLoader.d.ts.map +1 -1
  53. package/src/cli/utils/EnvFileLoader.js +33 -14
  54. package/src/cli.d.ts +5 -0
  55. package/src/cli.d.ts.map +1 -0
  56. package/src/cli.js +4 -0
  57. package/src/collections/index.d.ts +2 -2
  58. package/src/collections/index.d.ts.map +1 -1
  59. package/src/collections/index.js +1 -1
  60. package/src/common/RemoteSignedJson.d.ts.map +1 -1
  61. package/src/common/RemoteSignedJson.js +49 -23
  62. package/src/common/utility.d.ts.map +1 -1
  63. package/src/common/utility.js +2 -6
  64. package/src/config/cloudflare.d.ts.map +1 -1
  65. package/src/config/cloudflare.js +19 -8
  66. package/src/config/env.js +2 -2
  67. package/src/helper/index.d.ts +225 -0
  68. package/src/helper/index.d.ts.map +1 -0
  69. package/src/helper/index.js +347 -0
  70. package/src/index.d.ts +3 -6
  71. package/src/index.d.ts.map +1 -1
  72. package/src/index.js +7 -9
  73. package/src/migrations/MigrationDiscovery.d.ts.map +1 -1
  74. package/src/migrations/MigrationDiscovery.js +2 -1
  75. package/src/orm/DatabaseAdapter.d.ts +1 -0
  76. package/src/orm/DatabaseAdapter.d.ts.map +1 -1
  77. package/src/orm/SchemaStatemenWriter.d.ts +15 -0
  78. package/src/orm/SchemaStatemenWriter.d.ts.map +1 -0
  79. package/src/orm/SchemaStatemenWriter.js +78 -0
  80. package/src/orm/adapters/D1Adapter.d.ts.map +1 -1
  81. package/src/orm/adapters/D1Adapter.js +52 -2
  82. package/src/orm/adapters/D1RemoteAdapter.d.ts.map +1 -1
  83. package/src/orm/adapters/D1RemoteAdapter.js +137 -89
  84. package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
  85. package/src/orm/adapters/MySQLProxyAdapter.js +100 -81
  86. package/src/orm/adapters/PostgreSQLProxyAdapter.d.ts.map +1 -1
  87. package/src/orm/adapters/PostgreSQLProxyAdapter.js +26 -10
  88. package/src/orm/adapters/SqlProxyAdapterUtils.d.ts.map +1 -1
  89. package/src/orm/adapters/SqlProxyAdapterUtils.js +2 -1
  90. package/src/orm/adapters/SqlProxyRegistryMode.d.ts +12 -0
  91. package/src/orm/adapters/SqlProxyRegistryMode.d.ts.map +1 -0
  92. package/src/orm/adapters/SqlProxyRegistryMode.js +24 -0
  93. package/src/orm/adapters/SqlServerProxyAdapter.d.ts +3 -0
  94. package/src/orm/adapters/SqlServerProxyAdapter.d.ts.map +1 -1
  95. package/src/orm/adapters/SqlServerProxyAdapter.js +125 -117
  96. package/src/orm/migrations/MigrationStore.js +1 -1
  97. package/src/proxy/ProxyRequestParsing.d.ts +9 -0
  98. package/src/proxy/ProxyRequestParsing.d.ts.map +1 -0
  99. package/src/proxy/ProxyRequestParsing.js +16 -0
  100. package/src/proxy/RequestValidator.d.ts.map +1 -1
  101. package/src/proxy/RequestValidator.js +2 -1
  102. package/src/proxy/SigningService.js +2 -2
  103. package/src/proxy/SqlProxyDbOverrides.d.ts +17 -0
  104. package/src/proxy/SqlProxyDbOverrides.d.ts.map +1 -0
  105. package/src/proxy/SqlProxyDbOverrides.js +1 -0
  106. package/src/proxy/SqlProxyServerDeps.d.ts +12 -0
  107. package/src/proxy/SqlProxyServerDeps.d.ts.map +1 -0
  108. package/src/proxy/SqlProxyServerDeps.js +9 -0
  109. package/src/proxy/StatementPayloadValidator.d.ts +13 -0
  110. package/src/proxy/StatementPayloadValidator.d.ts.map +1 -0
  111. package/src/proxy/StatementPayloadValidator.js +18 -0
  112. package/src/proxy/StatementRegistryLoader.d.ts +2 -0
  113. package/src/proxy/StatementRegistryLoader.d.ts.map +1 -0
  114. package/src/proxy/StatementRegistryLoader.js +36 -0
  115. package/src/proxy/StatementRegistryResolver.d.ts +15 -0
  116. package/src/proxy/StatementRegistryResolver.d.ts.map +1 -0
  117. package/src/proxy/StatementRegistryResolver.js +34 -0
  118. package/src/proxy/d1/ZintrustD1Proxy.d.ts +2 -1
  119. package/src/proxy/d1/ZintrustD1Proxy.d.ts.map +1 -1
  120. package/src/proxy/d1/ZintrustD1Proxy.js +2 -1
  121. package/src/proxy/isMutatingSql.d.ts +2 -0
  122. package/src/proxy/isMutatingSql.d.ts.map +1 -0
  123. package/src/proxy/isMutatingSql.js +12 -0
  124. package/src/proxy/kv/ZintrustKvProxy.d.ts +2 -1
  125. package/src/proxy/kv/ZintrustKvProxy.d.ts.map +1 -1
  126. package/src/proxy/kv/ZintrustKvProxy.js +2 -1
  127. package/src/proxy/mysql/MySqlProxyServer.d.ts +2 -8
  128. package/src/proxy/mysql/MySqlProxyServer.d.ts.map +1 -1
  129. package/src/proxy/mysql/MySqlProxyServer.js +84 -51
  130. package/src/proxy/postgres/PostgresProxyServer.d.ts +2 -8
  131. package/src/proxy/postgres/PostgresProxyServer.d.ts.map +1 -1
  132. package/src/proxy/postgres/PostgresProxyServer.js +86 -48
  133. package/src/proxy/smtp/SmtpProxyServer.d.ts.map +1 -1
  134. package/src/proxy/smtp/SmtpProxyServer.js +6 -5
  135. package/src/proxy/sqlserver/SqlServerProxyServer.d.ts +2 -8
  136. package/src/proxy/sqlserver/SqlServerProxyServer.d.ts.map +1 -1
  137. package/src/proxy/sqlserver/SqlServerProxyServer.js +84 -49
  138. package/src/proxy.d.ts +4 -0
  139. package/src/proxy.d.ts.map +1 -0
  140. package/src/proxy.js +3 -0
  141. package/src/scheduler/Schedule.d.ts +36 -0
  142. package/src/scheduler/Schedule.d.ts.map +1 -0
  143. package/src/scheduler/Schedule.js +197 -0
  144. package/src/scheduler/ScheduleHttpGateway.d.ts +8 -0
  145. package/src/scheduler/ScheduleHttpGateway.d.ts.map +1 -0
  146. package/src/scheduler/ScheduleHttpGateway.js +196 -0
  147. package/src/scheduler/ScheduleRunner.d.ts +6 -0
  148. package/src/scheduler/ScheduleRunner.d.ts.map +1 -1
  149. package/src/scheduler/ScheduleRunner.js +166 -29
  150. package/src/scheduler/SchedulerRuntime.d.ts +15 -0
  151. package/src/scheduler/SchedulerRuntime.d.ts.map +1 -0
  152. package/src/scheduler/SchedulerRuntime.js +79 -0
  153. package/src/scheduler/cron/Cron.d.ts +19 -0
  154. package/src/scheduler/cron/Cron.d.ts.map +1 -0
  155. package/src/scheduler/cron/Cron.js +200 -0
  156. package/src/scheduler/leader/SchedulerLeader.d.ts +14 -0
  157. package/src/scheduler/leader/SchedulerLeader.d.ts.map +1 -0
  158. package/src/scheduler/leader/SchedulerLeader.js +187 -0
  159. package/src/scheduler/state/ScheduleStateStore.d.ts +27 -0
  160. package/src/scheduler/state/ScheduleStateStore.d.ts.map +1 -0
  161. package/src/scheduler/state/ScheduleStateStore.js +27 -0
  162. package/src/scheduler/types.d.ts +10 -0
  163. package/src/scheduler/types.d.ts.map +1 -1
  164. package/src/schedules/index.d.ts +1 -0
  165. package/src/schedules/index.d.ts.map +1 -1
  166. package/src/schedules/index.js +1 -0
  167. package/src/schedules/job-tracking-cleanup.d.ts +4 -0
  168. package/src/schedules/job-tracking-cleanup.d.ts.map +1 -0
  169. package/src/schedules/job-tracking-cleanup.js +116 -0
  170. package/src/schedules/log-cleanup.d.ts +1 -2
  171. package/src/schedules/log-cleanup.d.ts.map +1 -1
  172. package/src/schedules/log-cleanup.js +12 -15
  173. package/src/security/Sanitizer.d.ts.map +1 -1
  174. package/src/security/Sanitizer.js +1 -9
  175. package/src/security/SignedRequest.d.ts.map +1 -1
  176. package/src/security/SignedRequest.js +2 -2
  177. package/src/templates/docker/docker-compose.ecosystem.yml.tpl +301 -0
  178. package/src/templates/docker/docker-compose.schedules.yml.tpl +84 -0
  179. package/src/templates/project/basic/app/Schedules/index.ts.tpl +0 -0
  180. package/src/templates/project/basic/config/database.ts.tpl +1 -1
  181. package/src/toolkit/Secrets/Manifest.d.ts.map +1 -1
  182. package/src/toolkit/Secrets/Manifest.js +5 -7
  183. package/src/tools/mail/drivers/Smtp.d.ts.map +1 -1
  184. package/src/tools/mail/drivers/Smtp.js +7 -1
  185. package/src/tools/queue/JobReconciliationRunner.d.ts.map +1 -1
  186. package/src/tools/queue/JobReconciliationRunner.js +7 -39
  187. package/src/tools/queue/JobRecoveryDaemon.d.ts.map +1 -1
  188. package/src/tools/queue/JobRecoveryDaemon.js +116 -18
  189. package/src/tools/queue/JobStateTracker.d.ts +10 -1
  190. package/src/tools/queue/JobStateTracker.d.ts.map +1 -1
  191. package/src/tools/queue/JobStateTracker.js +24 -2
  192. package/src/tools/queue/JobStateTrackerDbPersistence.d.ts.map +1 -1
  193. package/src/tools/queue/JobStateTrackerDbPersistence.js +93 -2
@@ -4,45 +4,184 @@
4
4
  */
5
5
  import { Logger } from '../config/logger.js';
6
6
  import { ErrorFactory } from '../exceptions/ZintrustError.js';
7
+ import { Cron } from './cron/Cron.js';
8
+ import { InMemoryScheduleStateStore, } from './state/ScheduleStateStore.js';
9
+ const nowMs = () => Date.now();
10
+ const randomInt = (min, maxInclusive) => {
11
+ if (!Number.isFinite(min) || !Number.isFinite(maxInclusive))
12
+ return 0;
13
+ if (maxInclusive <= min)
14
+ return Math.floor(min);
15
+ return Math.floor(min + Math.random() * (maxInclusive - min + 1)); //NOSONAR
16
+ };
17
+ const resolveBackoffDelayMs = (state) => {
18
+ const policy = state.schedule.backoff;
19
+ if (!policy)
20
+ return 0;
21
+ const initialMs = Number.isFinite(policy.initialMs)
22
+ ? Math.max(0, Math.floor(policy.initialMs))
23
+ : 0;
24
+ const maxMs = Number.isFinite(policy.maxMs) ? Math.max(0, Math.floor(policy.maxMs)) : 0;
25
+ if (initialMs <= 0 || maxMs <= 0)
26
+ return 0;
27
+ const factor = policy.factor === undefined || !Number.isFinite(policy.factor) || policy.factor <= 1
28
+ ? 2
29
+ : policy.factor;
30
+ const power = Math.max(0, state.consecutiveFailures - 1);
31
+ const raw = initialMs * Math.pow(factor, power);
32
+ return Math.min(maxMs, Math.floor(raw));
33
+ };
34
+ const resolveJitterMs = (jitterMs) => {
35
+ return typeof jitterMs === 'number' && jitterMs > 0 ? randomInt(0, Math.floor(jitterMs)) : 0;
36
+ };
37
+ const computeBackoffDelay = (state) => {
38
+ if (state.schedule.enabled === false)
39
+ return null;
40
+ const backoffDelayMs = resolveBackoffDelayMs(state);
41
+ if (backoffDelayMs > 0) {
42
+ const jitter = resolveJitterMs(state.schedule.jitterMs);
43
+ return backoffDelayMs + jitter;
44
+ }
45
+ return null;
46
+ };
47
+ const computeCronDelay = (schedule) => {
48
+ if (typeof schedule.cron !== 'string' || schedule.cron.trim().length === 0) {
49
+ return null;
50
+ }
51
+ const tz = typeof schedule.timezone === 'string' && schedule.timezone.trim().length > 0
52
+ ? schedule.timezone
53
+ : 'UTC';
54
+ const nextAt = Cron.nextRunAtMs(nowMs(), schedule.cron, tz);
55
+ const baseDelay = Math.max(0, nextAt - nowMs());
56
+ const jitter = resolveJitterMs(schedule.jitterMs);
57
+ return baseDelay + jitter;
58
+ };
59
+ const computeIntervalDelay = (schedule) => {
60
+ if (typeof schedule.intervalMs !== 'number' || schedule.intervalMs <= 0) {
61
+ return null;
62
+ }
63
+ const base = Math.floor(schedule.intervalMs);
64
+ const jitter = resolveJitterMs(schedule.jitterMs);
65
+ return base + jitter;
66
+ };
67
+ const computeNextDelayMs = (state, outcome) => {
68
+ const schedule = state.schedule;
69
+ if (schedule.enabled === false)
70
+ return null;
71
+ if (outcome === 'failure') {
72
+ const backoffDelay = computeBackoffDelay(state);
73
+ if (backoffDelay !== null)
74
+ return backoffDelay;
75
+ }
76
+ const cronDelay = computeCronDelay(schedule);
77
+ if (cronDelay !== null)
78
+ return cronDelay;
79
+ const intervalDelay = computeIntervalDelay(schedule);
80
+ if (intervalDelay !== null)
81
+ return intervalDelay;
82
+ return null;
83
+ };
84
+ const clearTimer = (state) => {
85
+ if (state.timeoutId !== undefined) {
86
+ globalThis.clearTimeout(state.timeoutId);
87
+ state.timeoutId = undefined;
88
+ }
89
+ };
90
+ const scheduleNext = (state, kernel, invoke, outcome, store) => {
91
+ clearTimer(state);
92
+ const delay = computeNextDelayMs(state, outcome);
93
+ if (delay === null)
94
+ return;
95
+ const nextRunAt = nowMs() + delay;
96
+ void store.set(state.schedule.name, {
97
+ nextRunAt,
98
+ consecutiveFailures: state.consecutiveFailures,
99
+ });
100
+ state.timeoutId = globalThis.setTimeout(() => {
101
+ void runOnceAndReschedule(state, kernel, invoke, store);
102
+ }, delay);
103
+ };
104
+ const runOnceAndReschedule = async (state, kernel, invoke, store) => {
105
+ const outcome = await invoke(state, kernel);
106
+ scheduleNext(state, kernel, invoke, outcome, store);
107
+ };
7
108
  const createRegister = (runner, invokeHandler) => (schedule) => {
8
- if (runner.schedules.has(schedule.name)) {
109
+ const existing = runner.schedules.get(schedule.name);
110
+ if (existing !== undefined) {
9
111
  Logger.warn(`Schedule replaced: ${schedule.name}`);
112
+ // Reuse the same internal state to avoid leaving old timers/reschedule loops behind.
113
+ existing.schedule = schedule;
114
+ // If disabled, ensure any pending timer is cleared.
115
+ if (schedule.enabled === false) {
116
+ clearTimer(existing);
117
+ return;
118
+ }
119
+ // If a run is currently in progress, let it finish and reschedule naturally.
120
+ if (existing.isRunning) {
121
+ return;
122
+ }
123
+ // If schedules are already started, apply the new schedule immediately.
124
+ if (runner.started) {
125
+ if (schedule.runOnStart === true) {
126
+ void runOnceAndReschedule(existing, runner.kernel, invokeHandler, runner.store);
127
+ return;
128
+ }
129
+ scheduleNext(existing, runner.kernel, invokeHandler, 'success', runner.store);
130
+ }
131
+ return;
10
132
  }
11
133
  const state = {
12
134
  schedule,
13
135
  isRunning: false,
136
+ consecutiveFailures: 0,
14
137
  };
15
138
  runner.schedules.set(schedule.name, state);
16
139
  // If schedules are already started, register should take effect immediately.
17
140
  if (runner.started && schedule.enabled !== false) {
18
141
  if (schedule.runOnStart === true) {
19
- void invokeHandler(state, runner.kernel);
20
- }
21
- if (typeof schedule.intervalMs === 'number' && schedule.intervalMs > 0) {
22
- state.intervalId = globalThis.setInterval(() => {
23
- void invokeHandler(state, runner.kernel);
24
- }, schedule.intervalMs);
142
+ void runOnceAndReschedule(state, runner.kernel, invokeHandler, runner.store);
143
+ return;
25
144
  }
145
+ // Auto scheduling
146
+ scheduleNext(state, runner.kernel, invokeHandler, 'success', runner.store);
26
147
  }
27
148
  };
28
- const createInvokeHandler = () => async (state, kernel) => {
149
+ const createInvokeHandler = (store) => async (state, kernel) => {
29
150
  if (state.isRunning) {
30
151
  Logger.info(`Skipping overlapping run for schedule: ${state.schedule.name}`);
31
- return;
152
+ return 'failure';
32
153
  }
33
154
  state.isRunning = true;
34
155
  try {
35
156
  const handlerPromise = Promise.resolve()
36
157
  .then(async () => state.schedule.handler(kernel))
37
158
  .then(() => {
38
- state.lastRunAt = Date.now();
159
+ state.lastRunAt = nowMs();
160
+ state.consecutiveFailures = 0;
161
+ void store.set(state.schedule.name, {
162
+ lastRunAt: state.lastRunAt,
163
+ lastSuccessAt: state.lastRunAt,
164
+ lastErrorAt: undefined,
165
+ lastErrorMessage: undefined,
166
+ consecutiveFailures: state.consecutiveFailures,
167
+ });
168
+ return 'success';
39
169
  })
40
170
  .catch((error) => {
171
+ state.consecutiveFailures = Math.min(1_000_000, state.consecutiveFailures + 1);
172
+ const errMsg = error instanceof Error ? error.message : String(error);
173
+ const at = nowMs();
174
+ void store.set(state.schedule.name, {
175
+ lastRunAt: at,
176
+ lastErrorAt: at,
177
+ lastErrorMessage: errMsg,
178
+ consecutiveFailures: state.consecutiveFailures,
179
+ });
41
180
  Logger.error(`Schedule '${state.schedule.name}' failed:`, error);
42
- })
43
- .then(() => undefined);
44
- state.runningPromise = handlerPromise;
45
- await handlerPromise;
181
+ return 'failure';
182
+ });
183
+ state.runningPromise = handlerPromise.then(() => undefined);
184
+ return await handlerPromise;
46
185
  }
47
186
  finally {
48
187
  state.isRunning = false;
@@ -59,16 +198,12 @@ const createStart = (runner, invokeHandler) => (kernel) => {
59
198
  if (schedule.enabled === false)
60
199
  continue;
61
200
  if (schedule.runOnStart === true) {
62
- // fire-and-forget (handled by invokeHandler which logs errors)
63
- void invokeHandler(state, kernel);
64
- }
65
- if (typeof schedule.intervalMs === 'number' && schedule.intervalMs > 0) {
66
- const id = globalThis.setInterval(() => {
67
- // fire and forget invocation; overlapping runs are protected inside
68
- void invokeHandler(state, kernel);
69
- }, schedule.intervalMs);
70
- state.intervalId = id;
201
+ // fire-and-forget; next scheduling happens after the handler completes
202
+ void runOnceAndReschedule(state, kernel, invokeHandler, runner.store);
203
+ continue;
71
204
  }
205
+ // Auto scheduling
206
+ scheduleNext(state, kernel, invokeHandler, 'success', runner.store);
72
207
  }
73
208
  };
74
209
  const createStop = (runner) => async () => {
@@ -76,12 +211,9 @@ const createStop = (runner) => async () => {
76
211
  return;
77
212
  runner.started = false;
78
213
  runner.kernel = undefined;
79
- // Clear intervals
214
+ // Clear timers
80
215
  for (const [, state] of runner.schedules) {
81
- if (state.intervalId !== undefined) {
82
- globalThis.clearInterval(state.intervalId);
83
- state.intervalId = undefined;
84
- }
216
+ clearTimer(state);
85
217
  }
86
218
  // Await running handlers (runningPromise is guaranteed not to reject)
87
219
  const running = [];
@@ -136,20 +268,25 @@ export const create = () => {
136
268
  schedules: new Map(),
137
269
  started: false,
138
270
  kernel: undefined,
271
+ store: InMemoryScheduleStateStore.create(),
139
272
  };
140
- const invokeHandler = createInvokeHandler();
273
+ const invokeHandler = createInvokeHandler(runner.store);
141
274
  const register = createRegister(runner, invokeHandler);
142
275
  const start = createStart(runner, invokeHandler);
143
276
  const stopRaw = createStop(runner);
144
277
  const stop = createStopWithTimeout(stopRaw);
145
278
  const list = createList(runner);
146
279
  const runOnce = createRunOnce(runner, invokeHandler);
280
+ const getState = async (name) => runner.store.get(name);
281
+ const listStates = async () => runner.store.list();
147
282
  return Object.freeze({
148
283
  register,
149
284
  start,
150
285
  stop,
151
286
  list,
152
287
  runOnce,
288
+ getState,
289
+ listStates,
153
290
  });
154
291
  };
155
292
  export default { create };
@@ -0,0 +1,15 @@
1
+ import type { ScheduleRunState } from './state/ScheduleStateStore';
2
+ import type { ISchedule, IScheduleKernel } from './types';
3
+ export declare const SchedulerRuntime: Readonly<{
4
+ registerMany: (schedules: ReadonlyArray<ISchedule>, source?: "core" | "app") => void;
5
+ start(kernel?: IScheduleKernel): void;
6
+ stop(timeoutMs?: number): Promise<void>;
7
+ list(): ISchedule[];
8
+ listWithState(): Promise<Array<{
9
+ schedule: ISchedule;
10
+ state: ScheduleRunState | null;
11
+ }>>;
12
+ runOnce(name: string, kernel?: IScheduleKernel): Promise<void>;
13
+ }>;
14
+ export default SchedulerRuntime;
15
+ //# sourceMappingURL=SchedulerRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchedulerRuntime.d.ts","sourceRoot":"","sources":["../../../src/scheduler/SchedulerRuntime.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAmDnE,eAAO,MAAM,gBAAgB;8BAzBhB,aAAa,CAAC,SAAS,CAAC,WAC3B,MAAM,GAAG,KAAK,KACrB,IAAI;mBAyBU,eAAe,GAAG,IAAI;qBAqBd,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAOrC,SAAS,EAAE;qBAGI,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;kBAkB1E,MAAM,WAAW,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;EAGtC,CAAC;AAEjC,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { Logger } from '../config/logger.js';
2
+ import { createScheduleRunner } from './index.js';
3
+ import { SchedulerLeader } from './leader/SchedulerLeader.js';
4
+ const state = {
5
+ runner: createScheduleRunner(),
6
+ registered: new Map(),
7
+ leader: SchedulerLeader.create(),
8
+ leaderStarted: false,
9
+ };
10
+ const registerMany = (schedules, source = 'core') => {
11
+ for (const schedule of schedules) {
12
+ if (schedule === undefined || schedule === null || typeof schedule.name !== 'string')
13
+ continue;
14
+ const name = schedule.name.trim();
15
+ if (name.length === 0)
16
+ continue;
17
+ const existing = state.registered.get(name);
18
+ // If app already registered, it always wins.
19
+ if (existing === 'app')
20
+ continue;
21
+ // Prevent repeated registrations from the same source.
22
+ if (existing === source)
23
+ continue;
24
+ if (existing === 'core' && source === 'app') {
25
+ Logger.info('Schedule overridden by app/Schedules', { name });
26
+ }
27
+ state.registered.set(name, source);
28
+ state.runner.register(schedule);
29
+ }
30
+ };
31
+ export const SchedulerRuntime = Object.freeze({
32
+ registerMany,
33
+ start(kernel) {
34
+ // If leader mode is enabled, only the leader instance starts timers.
35
+ if (state.leader.isEnabled()) {
36
+ if (state.leaderStarted)
37
+ return;
38
+ state.leaderStarted = true;
39
+ state.leader.start({
40
+ onBecameLeader: () => {
41
+ state.runner.start(kernel);
42
+ },
43
+ onLostLeadership: () => {
44
+ // Best-effort stop; leadership transitions should not crash the process.
45
+ void state.runner.stop();
46
+ },
47
+ });
48
+ return;
49
+ }
50
+ state.runner.start(kernel);
51
+ },
52
+ async stop(timeoutMs) {
53
+ if (state.leaderStarted) {
54
+ state.leaderStarted = false;
55
+ await state.leader.stop();
56
+ }
57
+ await state.runner.stop(timeoutMs);
58
+ },
59
+ list() {
60
+ return state.runner.list();
61
+ },
62
+ async listWithState() {
63
+ const schedules = state.runner.list();
64
+ const getState = state.runner
65
+ .getState;
66
+ if (typeof getState !== 'function') {
67
+ return schedules.map((schedule) => ({ schedule, state: null }));
68
+ }
69
+ const rows = await Promise.all(schedules.map(async (schedule) => {
70
+ const stateRow = (await getState(schedule.name));
71
+ return { schedule, state: stateRow };
72
+ }));
73
+ return rows;
74
+ },
75
+ async runOnce(name, kernel) {
76
+ await state.runner.runOnce(name, kernel);
77
+ },
78
+ });
79
+ export default SchedulerRuntime;
@@ -0,0 +1,19 @@
1
+ type CronAllowed = Readonly<{
2
+ any: true;
3
+ }> | Readonly<{
4
+ any: false;
5
+ values: ReadonlySet<number>;
6
+ }>;
7
+ export type CronSpec = Readonly<{
8
+ minute: CronAllowed;
9
+ hour: CronAllowed;
10
+ dayOfMonth: CronAllowed;
11
+ month: CronAllowed;
12
+ dayOfWeek: CronAllowed;
13
+ }>;
14
+ export declare const Cron: Readonly<{
15
+ parse(expr: string): CronSpec;
16
+ nextRunAtMs(nowMs: number, expr: string, timeZone?: string): number;
17
+ }>;
18
+ export default Cron;
19
+ //# sourceMappingURL=Cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cron.d.ts","sourceRoot":"","sources":["../../../../src/scheduler/cron/Cron.ts"],"names":[],"mappings":"AAEA,KAAK,WAAW,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,CAAC,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAEnG,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;IAC9B,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,WAAW,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,WAAW,CAAC;CACxB,CAAC,CAAC;AA0LH,eAAO,MAAM,IAAI;gBACH,MAAM,GAAG,QAAQ;uBA+BV,MAAM,QAAQ,MAAM,aAAY,MAAM,GAAW,MAAM;EAoB1E,CAAC;AAEH,eAAe,IAAI,CAAC"}
@@ -0,0 +1,200 @@
1
+ const ANY = Object.freeze({ any: true });
2
+ const toInt = (value) => {
3
+ const n = Number.parseInt(value, 10);
4
+ return Number.isFinite(n) ? n : null;
5
+ };
6
+ const clamp = (n, range) => Math.min(range.max, Math.max(range.min, n));
7
+ const expandStep = (step, range, out) => {
8
+ for (let i = range.min; i <= range.max; i += step)
9
+ out.add(i);
10
+ };
11
+ const expandRange = (trimmed, range, out) => {
12
+ const left = trimmed.slice(0, trimmed.indexOf('-'));
13
+ const rest = trimmed.slice(trimmed.indexOf('-') + 1);
14
+ const slashIdx = rest.indexOf('/');
15
+ const right = slashIdx === -1 ? rest : rest.slice(0, slashIdx);
16
+ const stepRaw = slashIdx === -1 ? null : rest.slice(slashIdx + 1);
17
+ const start = toInt(left);
18
+ const end = toInt(right);
19
+ const step = stepRaw === null ? 1 : toInt(stepRaw);
20
+ if (start === null || end === null || step === null || step <= 0)
21
+ return;
22
+ const a = clamp(start, range);
23
+ const b = clamp(end, range);
24
+ const lo = Math.min(a, b);
25
+ const hi = Math.max(a, b);
26
+ for (let i = lo; i <= hi; i += step)
27
+ out.add(i);
28
+ };
29
+ const expandPart = (part, range, out) => {
30
+ const trimmed = part.trim();
31
+ if (trimmed.length === 0)
32
+ return;
33
+ if (trimmed.startsWith('*/')) {
34
+ const step = toInt(trimmed.slice(2));
35
+ if (step !== null && step > 0)
36
+ expandStep(step, range, out);
37
+ return;
38
+ }
39
+ if (trimmed.includes('-')) {
40
+ expandRange(trimmed, range, out);
41
+ return;
42
+ }
43
+ const num = toInt(trimmed);
44
+ if (num !== null)
45
+ out.add(clamp(num, range));
46
+ };
47
+ const parseField = (raw, range, normalize) => {
48
+ const value = raw.trim();
49
+ if (value === '*' || value.length === 0)
50
+ return ANY;
51
+ const set = new Set();
52
+ for (const part of value.split(',')) {
53
+ expandPart(part, range, set);
54
+ }
55
+ if (normalize !== undefined) {
56
+ const normalized = new Set();
57
+ for (const n of set)
58
+ normalized.add(normalize(n));
59
+ return Object.freeze({ any: false, values: normalized });
60
+ }
61
+ return Object.freeze({ any: false, values: set });
62
+ };
63
+ const matches = (allowed, value) => {
64
+ if (allowed.any)
65
+ return true;
66
+ return allowed.values.has(value);
67
+ };
68
+ const weekdayToDow = (weekdayShort) => {
69
+ switch (weekdayShort) {
70
+ case 'Sun':
71
+ return 0;
72
+ case 'Mon':
73
+ return 1;
74
+ case 'Tue':
75
+ return 2;
76
+ case 'Wed':
77
+ return 3;
78
+ case 'Thu':
79
+ return 4;
80
+ case 'Fri':
81
+ return 5;
82
+ case 'Sat':
83
+ return 6;
84
+ default:
85
+ return 0;
86
+ }
87
+ };
88
+ const dtfCache = new Map();
89
+ const getFormatter = (timeZone) => {
90
+ const key = `en-US|${timeZone}`;
91
+ const cached = dtfCache.get(key);
92
+ if (cached)
93
+ return cached;
94
+ const fmt = new Intl.DateTimeFormat('en-US', {
95
+ timeZone,
96
+ weekday: 'short',
97
+ year: 'numeric',
98
+ month: '2-digit',
99
+ day: '2-digit',
100
+ hour: '2-digit',
101
+ minute: '2-digit',
102
+ hourCycle: 'h23',
103
+ });
104
+ dtfCache.set(key, fmt);
105
+ return fmt;
106
+ };
107
+ const getZonedParts = (date, timeZone) => {
108
+ // Fallback to UTC if Intl timeZone support is unavailable.
109
+ try {
110
+ const fmt = getFormatter(timeZone);
111
+ const parts = fmt.formatToParts(date);
112
+ const get = (type) => parts.find((p) => p.type === type)?.value;
113
+ const minute = Number.parseInt(get('minute') ?? '0', 10);
114
+ const hour = Number.parseInt(get('hour') ?? '0', 10);
115
+ const day = Number.parseInt(get('day') ?? '1', 10);
116
+ const month = Number.parseInt(get('month') ?? '1', 10);
117
+ const weekday = get('weekday') ?? 'Sun';
118
+ const dow = weekdayToDow(weekday);
119
+ return { minute, hour, day, month, dow };
120
+ }
121
+ catch {
122
+ return {
123
+ minute: date.getUTCMinutes(),
124
+ hour: date.getUTCHours(),
125
+ day: date.getUTCDate(),
126
+ month: date.getUTCMonth() + 1,
127
+ dow: date.getUTCDay(),
128
+ };
129
+ }
130
+ };
131
+ const domDowMatches = (spec, parts) => {
132
+ const domAny = spec.dayOfMonth.any;
133
+ const dowAny = spec.dayOfWeek.any;
134
+ const domOk = matches(spec.dayOfMonth, parts.day);
135
+ const dowOk = matches(spec.dayOfWeek, parts.dow);
136
+ // Vixie cron semantics:
137
+ // - if either DOM or DOW is '*', require the other field to match
138
+ // - if both are restricted, match if either matches
139
+ if (domAny && dowAny)
140
+ return true;
141
+ if (domAny)
142
+ return dowOk;
143
+ if (dowAny)
144
+ return domOk;
145
+ return domOk || dowOk;
146
+ };
147
+ const matchesSpec = (spec, parts) => {
148
+ return (matches(spec.minute, parts.minute) &&
149
+ matches(spec.hour, parts.hour) &&
150
+ matches(spec.month, parts.month) &&
151
+ domDowMatches(spec, parts));
152
+ };
153
+ export const Cron = Object.freeze({
154
+ parse(expr) {
155
+ const raw = String(expr ?? '').trim();
156
+ const parts = raw.split(/\s+/).filter(Boolean);
157
+ if (parts.length !== 5) {
158
+ // Return an "any" spec for invalid inputs; runner will treat it as "every minute".
159
+ return Object.freeze({
160
+ minute: ANY,
161
+ hour: ANY,
162
+ dayOfMonth: ANY,
163
+ month: ANY,
164
+ dayOfWeek: ANY,
165
+ });
166
+ }
167
+ const [min, hour, dom, month, dow] = parts;
168
+ const normalizeDow = (n) => {
169
+ // allow 7 as Sunday
170
+ if (n === 7)
171
+ return 0;
172
+ return clamp(n, { min: 0, max: 6 });
173
+ };
174
+ return Object.freeze({
175
+ minute: parseField(min, { min: 0, max: 59 }),
176
+ hour: parseField(hour, { min: 0, max: 23 }),
177
+ dayOfMonth: parseField(dom, { min: 1, max: 31 }),
178
+ month: parseField(month, { min: 1, max: 12 }),
179
+ dayOfWeek: parseField(dow, { min: 0, max: 7 }, normalizeDow),
180
+ });
181
+ },
182
+ nextRunAtMs(nowMs, expr, timeZone = 'UTC') {
183
+ const spec = this.parse(expr);
184
+ const base = new Date(nowMs);
185
+ // Cron is minute-resolution; start from next minute boundary.
186
+ base.setUTCSeconds(0, 0);
187
+ base.setTime(base.getTime() + 60_000);
188
+ // Bound search to 366 days (minute granularity). This is defensive; typical crons resolve quickly.
189
+ const maxIterations = 366 * 24 * 60;
190
+ for (let i = 0; i < maxIterations; i++) {
191
+ const parts = getZonedParts(base, timeZone);
192
+ if (matchesSpec(spec, parts))
193
+ return base.getTime();
194
+ base.setTime(base.getTime() + 60_000);
195
+ }
196
+ // Fallback: run in 60s.
197
+ return nowMs + 60_000;
198
+ },
199
+ });
200
+ export default Cron;
@@ -0,0 +1,14 @@
1
+ export type SchedulerLeaderHooks = Readonly<{
2
+ onBecameLeader: () => void;
3
+ onLostLeadership: () => void;
4
+ }>;
5
+ export type SchedulerLeaderApi = Readonly<{
6
+ isEnabled: () => boolean;
7
+ start: (hooks: SchedulerLeaderHooks) => void;
8
+ stop: () => Promise<void>;
9
+ }>;
10
+ export declare const SchedulerLeader: Readonly<{
11
+ create(): SchedulerLeaderApi;
12
+ }>;
13
+ export default SchedulerLeader;
14
+ //# sourceMappingURL=SchedulerLeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchedulerLeader.d.ts","sourceRoot":"","sources":["../../../../src/scheduler/leader/SchedulerLeader.ts"],"names":[],"mappings":"AAoFA,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,IAAI,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC,CAAC;AAgGH,eAAO,MAAM,eAAe;cAChB,kBAAkB;EAkD5B,CAAC;AAEH,eAAe,eAAe,CAAC"}