@zintrust/core 2.4.5 → 2.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "2.4.5",
3
+ "version": "2.4.6",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @zintrust/core v2.4.5
2
+ * @zintrust/core v2.4.6
3
3
  *
4
4
  * ZinTrust Framework - Production-Grade TypeScript Backend
5
5
  * Built for performance, type safety, and exceptional developer experience
6
6
  *
7
7
  * Build Information:
8
- * Built: 2026-06-01T13:37:05.393Z
8
+ * Built: 2026-06-01T16:53:34.785Z
9
9
  * Node: >=20.0.0
10
10
  * License: MIT
11
11
  *
@@ -21,7 +21,7 @@
21
21
  * Available at runtime for debugging and health checks
22
22
  */
23
23
  export const ZINTRUST_VERSION = '0.1.41';
24
- export const ZINTRUST_BUILD_DATE = '2026-06-01T13:37:05.359Z'; // Replaced during build
24
+ export const ZINTRUST_BUILD_DATE = '2026-06-01T16:53:34.743Z'; // Replaced during build
25
25
  export { Application } from './boot/Application.js';
26
26
  export { AwsSigV4 } from './common/index.js';
27
27
  export { SignedRequest } from './security/SignedRequest.js';
@@ -2,23 +2,37 @@
2
2
  * ScheduleRunner
3
3
  * Lightweight, zero-dependency schedule runner for long-running runtimes
4
4
  */
5
- import { type ScheduleRunState } from './state/ScheduleStateStore';
5
+ import { type IScheduleStateStore, type ScheduleRunState } from './state/ScheduleStateStore';
6
6
  import type { ISchedule, IScheduleKernel } from './types';
7
+ /**
8
+ * Result of a `runIfDue` invocation.
9
+ * - `ran: true` → the handler was invoked this call.
10
+ * - `ran: false` → skipped; `reason` explains why and `nextRunAt` (when present)
11
+ * is the epoch-ms timestamp at which the schedule next becomes due.
12
+ */
13
+ export type RunIfDueResult = Readonly<{
14
+ ran: boolean;
15
+ reason?: 'disabled' | 'not-due' | 'no-cadence';
16
+ nextRunAt?: number;
17
+ outcome?: 'success' | 'failure';
18
+ }>;
7
19
  type ScheduleRunner = {
8
20
  register: (schedule: ISchedule) => void;
9
21
  start: (kernel?: IScheduleKernel) => void;
10
22
  stop: (timeoutMs?: number) => Promise<void>;
11
23
  list: () => ISchedule[];
12
24
  runOnce: (name: string, kernel?: IScheduleKernel) => Promise<void>;
25
+ runIfDue: (name: string, kernel?: IScheduleKernel) => Promise<RunIfDueResult>;
13
26
  getState: (name: string) => Promise<ScheduleRunState | null>;
14
27
  listStates: () => Promise<Array<{
15
28
  name: string;
16
29
  state: ScheduleRunState;
17
30
  }>>;
31
+ setStore: (store: IScheduleStateStore) => void;
18
32
  };
19
- export declare const create: () => Readonly<ScheduleRunner>;
33
+ export declare const create: (store?: IScheduleStateStore) => Readonly<ScheduleRunner>;
20
34
  declare const _default: {
21
- create: () => Readonly<ScheduleRunner>;
35
+ create: (store?: IScheduleStateStore) => Readonly<ScheduleRunner>;
22
36
  };
23
37
  export default _default;
24
38
  //# sourceMappingURL=ScheduleRunner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ScheduleRunner.d.ts","sourceRoot":"","sources":["../../../src/scheduler/ScheduleRunner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkBnE,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IAC1C,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,SAAS,EAAE,CAAC;IACxB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC7D,UAAU,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC,CAAC;CAC7E,CAAC;AA6VF,eAAO,MAAM,MAAM,QAAO,QAAQ,CAAC,cAAc,CA6BhD,CAAC;;kBA7BwB,QAAQ,CAAC,cAAc,CAAC;;AA+BlD,wBAA0B"}
1
+ {"version":3,"file":"ScheduleRunner.d.ts","sourceRoot":"","sources":["../../../src/scheduler/ScheduleRunner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACtB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkBnE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CACjC,CAAC,CAAC;AAEH,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IAC1C,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,SAAS,EAAE,CAAC;IACxB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC9E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC7D,UAAU,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC,CAAC;IAC5E,QAAQ,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAChD,CAAC;AAwZF,eAAO,MAAM,MAAM,GAAI,QAAQ,mBAAmB,KAAG,QAAQ,CAAC,cAAc,CAmC3E,CAAC;;qBAnC6B,mBAAmB,KAAG,QAAQ,CAAC,cAAc,CAAC;;AAqC7E,wBAA0B"}
@@ -146,7 +146,8 @@ const createRegister = (runner, invokeHandler) => (schedule) => {
146
146
  scheduleNext(state, runner.kernel, invokeHandler, 'success', runner.store);
147
147
  }
148
148
  };
149
- const createInvokeHandler = (store) => async (state, kernel) => {
149
+ const createInvokeHandler = (runner) => async (state, kernel) => {
150
+ const store = runner.store;
150
151
  if (state.isRunning) {
151
152
  Logger.info(`Skipping overlapping run for schedule: ${state.schedule.name}`);
152
153
  return 'failure';
@@ -263,30 +264,79 @@ const createRunOnce = (runner, invokeHandler) => async (name, kernel) => {
263
264
  return;
264
265
  await invokeHandler(state, kernel ?? runner.kernel);
265
266
  };
266
- export const create = () => {
267
+ /**
268
+ * Computes the epoch-ms timestamp at which a schedule next becomes due,
269
+ * measured from its last successful run. Cron takes precedence over interval.
270
+ * Returns `null` when the schedule has neither a cron nor a positive interval
271
+ * (i.e. it has no cadence of its own and is always considered due).
272
+ */
273
+ const computeDueAtMs = (schedule, lastRunAt) => {
274
+ if (typeof schedule.cron === 'string' && schedule.cron.trim().length > 0) {
275
+ const tz = typeof schedule.timezone === 'string' && schedule.timezone.trim().length > 0
276
+ ? schedule.timezone
277
+ : undefined;
278
+ return Cron.nextRunAtMs(lastRunAt, schedule.cron, tz);
279
+ }
280
+ if (typeof schedule.intervalMs === 'number' && schedule.intervalMs > 0) {
281
+ return lastRunAt + Math.floor(schedule.intervalMs);
282
+ }
283
+ return null;
284
+ };
285
+ const createRunIfDue = (runner, invokeHandler) => async (name, kernel) => {
286
+ const state = runner.schedules.get(name);
287
+ if (state === undefined)
288
+ throw ErrorFactory.createNotFoundError(`Schedule not found: ${name}`);
289
+ if (state.schedule.enabled === false)
290
+ return { ran: false, reason: 'disabled' };
291
+ // Prefer the durable, persisted last-run time over the in-memory mirror so
292
+ // this works across stateless invocations (e.g. Cloudflare Worker cron ticks)
293
+ // when a persistent store has been wired via setStore().
294
+ const persisted = await runner.store.get(name);
295
+ const lastRunAt = persisted?.lastSuccessAt ?? persisted?.lastRunAt ?? state.lastRunAt ?? undefined;
296
+ if (typeof lastRunAt === 'number') {
297
+ const dueAt = computeDueAtMs(state.schedule, lastRunAt);
298
+ if (dueAt === null) {
299
+ // No cadence configured: nothing to gate on, run it.
300
+ const outcome = await invokeHandler(state, kernel ?? runner.kernel);
301
+ return { ran: true, outcome, reason: 'no-cadence' };
302
+ }
303
+ if (nowMs() < dueAt) {
304
+ return { ran: false, reason: 'not-due', nextRunAt: dueAt };
305
+ }
306
+ }
307
+ const outcome = await invokeHandler(state, kernel ?? runner.kernel);
308
+ return { ran: true, outcome };
309
+ };
310
+ export const create = (store) => {
267
311
  const runner = {
268
312
  schedules: new Map(),
269
313
  started: false,
270
314
  kernel: undefined,
271
- store: InMemoryScheduleStateStore.create(),
315
+ store: store ?? InMemoryScheduleStateStore.create(),
272
316
  };
273
- const invokeHandler = createInvokeHandler(runner.store);
317
+ const invokeHandler = createInvokeHandler(runner);
274
318
  const register = createRegister(runner, invokeHandler);
275
319
  const start = createStart(runner, invokeHandler);
276
320
  const stopRaw = createStop(runner);
277
321
  const stop = createStopWithTimeout(stopRaw);
278
322
  const list = createList(runner);
279
323
  const runOnce = createRunOnce(runner, invokeHandler);
324
+ const runIfDue = createRunIfDue(runner, invokeHandler);
280
325
  const getState = async (name) => runner.store.get(name);
281
326
  const listStates = async () => runner.store.list();
327
+ const setStore = (next) => {
328
+ runner.store = next;
329
+ };
282
330
  return Object.freeze({
283
331
  register,
284
332
  start,
285
333
  stop,
286
334
  list,
287
335
  runOnce,
336
+ runIfDue,
288
337
  getState,
289
338
  listStates,
339
+ setStore,
290
340
  });
291
341
  };
292
342
  export default { create };
@@ -1,4 +1,5 @@
1
- import type { ScheduleRunState } from './state/ScheduleStateStore';
1
+ import type { RunIfDueResult } from './ScheduleRunner';
2
+ import type { IScheduleStateStore, ScheduleRunState } from './state/ScheduleStateStore';
2
3
  import type { ISchedule, IScheduleKernel } from './types';
3
4
  export declare const SchedulerRuntime: Readonly<{
4
5
  registerMany: (schedules: ReadonlyArray<ISchedule>, source?: "core" | "app") => void;
@@ -10,6 +11,20 @@ export declare const SchedulerRuntime: Readonly<{
10
11
  state: ScheduleRunState | null;
11
12
  }>>;
12
13
  runOnce(name: string, kernel?: IScheduleKernel): Promise<void>;
14
+ /**
15
+ * Runs a schedule only if it is due, based on its persisted last-run time and
16
+ * its configured cron/interval cadence. Intended for stateless cron triggers
17
+ * (e.g. a Cloudflare Worker `scheduled` handler) where the timer-based
18
+ * `start()` loop cannot run. For the due check to survive cold starts, wire a
19
+ * persistent state store via {@link useStateStore} first.
20
+ */
21
+ runIfDue(name: string, kernel?: IScheduleKernel): Promise<RunIfDueResult>;
22
+ /**
23
+ * Swaps the schedule state store (default is in-memory). Provide a durable
24
+ * implementation (Redis/KV/D1-backed) so `runIfDue` can read `lastRunAt`
25
+ * across stateless invocations. Call this before `registerMany`/`runIfDue`.
26
+ */
27
+ useStateStore(store: IScheduleStateStore): void;
13
28
  }>;
14
29
  export default SchedulerRuntime;
15
30
  //# sourceMappingURL=SchedulerRuntime.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"SchedulerRuntime.d.ts","sourceRoot":"","sources":["../../../src/scheduler/SchedulerRuntime.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACjG,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAgEnE,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;IAGpE;;;;;;OAMG;mBACkB,MAAM,WAAW,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IAG/E;;;;OAIG;yBACkB,mBAAmB,GAAG,IAAI;EAGjB,CAAC;AAEjC,eAAe,gBAAgB,CAAC"}
@@ -75,5 +75,23 @@ export const SchedulerRuntime = Object.freeze({
75
75
  async runOnce(name, kernel) {
76
76
  await state.runner.runOnce(name, kernel);
77
77
  },
78
+ /**
79
+ * Runs a schedule only if it is due, based on its persisted last-run time and
80
+ * its configured cron/interval cadence. Intended for stateless cron triggers
81
+ * (e.g. a Cloudflare Worker `scheduled` handler) where the timer-based
82
+ * `start()` loop cannot run. For the due check to survive cold starts, wire a
83
+ * persistent state store via {@link useStateStore} first.
84
+ */
85
+ async runIfDue(name, kernel) {
86
+ return state.runner.runIfDue(name, kernel);
87
+ },
88
+ /**
89
+ * Swaps the schedule state store (default is in-memory). Provide a durable
90
+ * implementation (Redis/KV/D1-backed) so `runIfDue` can read `lastRunAt`
91
+ * across stateless invocations. Call this before `registerMany`/`runIfDue`.
92
+ */
93
+ useStateStore(store) {
94
+ state.runner.setStore(store);
95
+ },
78
96
  });
79
97
  export default SchedulerRuntime;