@mandujs/core 0.21.0 → 0.22.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.
- package/package.json +101 -69
- package/src/auth/__tests__/login.test.ts +419 -0
- package/src/auth/__tests__/password.test.ts +122 -0
- package/src/auth/__tests__/reset.test.ts +296 -0
- package/src/auth/__tests__/tokens.test.ts +274 -0
- package/src/auth/__tests__/verification.test.ts +274 -0
- package/src/auth/index.ts +76 -0
- package/src/auth/login.ts +225 -0
- package/src/auth/password.ts +120 -0
- package/src/auth/reset.ts +243 -0
- package/src/auth/tokens.ts +612 -0
- package/src/auth/verification.ts +253 -0
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
- package/src/bundler/__tests__/cold-start.test.ts +504 -0
- package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
- package/src/bundler/__tests__/extended-watch.test.ts +710 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
- package/src/bundler/__tests__/hdr.test.ts +353 -0
- package/src/bundler/__tests__/hmr-client.test.ts +532 -0
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
- package/src/bundler/build.test.ts +8 -1
- package/src/bundler/build.ts +310 -18
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1611 -59
- package/src/bundler/fast-refresh-plugin.ts +307 -0
- package/src/bundler/hmr-types.ts +252 -0
- package/src/bundler/manifest-schema.ts +301 -0
- package/src/bundler/safe-build.test.ts +128 -0
- package/src/bundler/safe-build.ts +77 -0
- package/src/bundler/scenario-matrix.ts +229 -0
- package/src/bundler/types.ts +11 -0
- package/src/bundler/vendor-cache-types.ts +130 -0
- package/src/bundler/vendor-cache.ts +526 -0
- package/src/client/router.ts +214 -56
- package/src/db/__tests__/db.test.ts +485 -0
- package/src/db/index.ts +513 -0
- package/src/db/migrations/__tests__/runner.test.ts +661 -0
- package/src/db/migrations/history-table.ts +345 -0
- package/src/db/migrations/lock.ts +269 -0
- package/src/db/migrations/runner.ts +633 -0
- package/src/desktop/__tests__/smoke.test.ts +100 -0
- package/src/desktop/__tests__/window.test.ts +172 -0
- package/src/desktop/__tests__/worker.test.ts +266 -0
- package/src/desktop/index.ts +43 -0
- package/src/desktop/types.ts +158 -0
- package/src/desktop/window.ts +492 -0
- package/src/desktop/worker.ts +180 -0
- package/src/email/__tests__/email.test.ts +355 -0
- package/src/email/index.ts +282 -0
- package/src/email/resend.ts +163 -0
- package/src/email/smtp.ts +64 -0
- package/src/filling/__tests__/session-sqlite.test.ts +454 -0
- package/src/filling/context.ts +72 -78
- package/src/filling/cookie-codec.ts +299 -0
- package/src/filling/deps.ts +25 -1
- package/src/filling/filling.ts +28 -3
- package/src/filling/session-sqlite.ts +617 -0
- package/src/filling/session.ts +265 -216
- package/src/guard/decision-memory.test.ts +52 -22
- package/src/id/__tests__/id.test.ts +120 -0
- package/src/id/index.ts +105 -0
- package/src/kitchen/index.ts +2 -2
- package/src/kitchen/kitchen-handler.ts +86 -0
- package/src/kitchen/stream/activity-sse.ts +2 -1
- package/src/middleware/csrf.ts +328 -0
- package/src/middleware/index.ts +40 -0
- package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
- package/src/middleware/oauth/index.ts +505 -0
- package/src/middleware/oauth/providers.ts +115 -0
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
- package/src/middleware/rate-limit/index.ts +522 -0
- package/src/middleware/rate-limit/sqlite-store.ts +382 -0
- package/src/middleware/secure/__tests__/secure.test.ts +360 -0
- package/src/middleware/secure/csp.ts +193 -0
- package/src/middleware/secure/index.ts +417 -0
- package/src/middleware/session.ts +174 -0
- package/src/observability/event-bus.ts +81 -79
- package/src/paths.ts +37 -0
- package/src/perf/hmr-markers.ts +215 -0
- package/src/perf/index.ts +104 -0
- package/src/resource/__tests__/generator.test.ts +603 -2
- package/src/resource/ddl/__tests__/diff.test.ts +639 -0
- package/src/resource/ddl/__tests__/emit.test.ts +799 -0
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
- package/src/resource/ddl/diff.ts +392 -0
- package/src/resource/ddl/emit.ts +548 -0
- package/src/resource/ddl/persistence-types.ts +218 -0
- package/src/resource/ddl/snapshot.ts +447 -0
- package/src/resource/ddl/type-map.ts +223 -0
- package/src/resource/ddl/types.ts +232 -0
- package/src/resource/generator-repo.ts +610 -0
- package/src/resource/generator-schema.ts +476 -0
- package/src/resource/generator.ts +117 -1
- package/src/resource/index.ts +17 -1
- package/src/resource/schema.ts +30 -0
- package/src/router/fs-scanner.ts +3 -0
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
- package/src/runtime/__tests__/hdr-client.test.ts +223 -0
- package/src/runtime/__tests__/http-errors.test.ts +117 -0
- package/src/runtime/__tests__/not-found.test.ts +152 -0
- package/src/runtime/boundary.tsx +21 -1
- package/src/runtime/fast-refresh-runtime.ts +322 -0
- package/src/runtime/fast-refresh-types.ts +128 -0
- package/src/runtime/hmr-client.ts +409 -0
- package/src/runtime/http-errors.ts +113 -0
- package/src/runtime/index.ts +6 -0
- package/src/runtime/logger.ts +678 -677
- package/src/runtime/not-found.ts +93 -0
- package/src/runtime/redirect.ts +133 -0
- package/src/runtime/server.ts +518 -20
- package/src/runtime/ssr.ts +340 -10
- package/src/runtime/streaming-ssr.ts +222 -19
- package/src/scheduler/__tests__/scheduler.test.ts +514 -0
- package/src/scheduler/index.ts +343 -0
- package/src/storage/s3/__tests__/s3.test.ts +479 -0
- package/src/storage/s3/index.ts +412 -0
- package/src/testing/index.ts +58 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mandujs/core/scheduler
|
|
3
|
+
*
|
|
4
|
+
* Thin, production-minded wrapper around `Bun.cron` (Bun 1.3.12+). Adds four
|
|
5
|
+
* things the native API doesn't give on its own:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Overlap prevention** — a second tick that fires while the previous
|
|
8
|
+
* handler is still pending increments `skipCount` instead of running the
|
|
9
|
+
* body concurrently. Native `Bun.cron` documents the same guarantee but
|
|
10
|
+
* we also enforce it defensively and surface the skip count.
|
|
11
|
+
* 2. **Per-tick timeout (soft)** — `timeoutMs` logs a warning and clears the
|
|
12
|
+
* in-flight flag so the *next* scheduled tick can run; it does NOT abort
|
|
13
|
+
* the current handler (Bun.cron has no cancellation primitive). The
|
|
14
|
+
* original handler keeps running; we just stop blocking future ticks on
|
|
15
|
+
* it. This is the best you can do against a hung job without killing the
|
|
16
|
+
* process.
|
|
17
|
+
* 3. **Dev-mode skip** — jobs marked `skipInDev: true` are not registered
|
|
18
|
+
* when `NODE_ENV !== "production"`. They still appear in `status()` with
|
|
19
|
+
* zero counters so dashboards don't have to special-case them.
|
|
20
|
+
* 4. **Graceful shutdown** — `stop()` prevents new ticks immediately and
|
|
21
|
+
* resolves once all in-flight handlers settle.
|
|
22
|
+
*
|
|
23
|
+
* Single-process assumption: there is NO distributed lock, NO persistent queue,
|
|
24
|
+
* and NO cross-instance coordination. Running two processes with the same
|
|
25
|
+
* `defineCron` config will fire each job on every process. Use a queue
|
|
26
|
+
* (BullMQ, PG-boss, SQS) if you need exactly-once or multi-instance semantics.
|
|
27
|
+
*
|
|
28
|
+
* At-most-once on restart: if the process dies between ticks, the missed tick
|
|
29
|
+
* is lost — `Bun.cron` computes "next fire" from the moment it starts, not
|
|
30
|
+
* from a persisted schedule. Document this for any job whose absence matters.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { defineCron } from "@mandujs/core/scheduler";
|
|
35
|
+
*
|
|
36
|
+
* const jobs = defineCron({
|
|
37
|
+
* "clean:sessions": {
|
|
38
|
+
* schedule: "*\/15 * * * *",
|
|
39
|
+
* run: async () => { await db.exec("DELETE FROM sessions WHERE expires_at < now()"); },
|
|
40
|
+
* skipInDev: true,
|
|
41
|
+
* },
|
|
42
|
+
* "daily:report": {
|
|
43
|
+
* schedule: "0 3 * * *",
|
|
44
|
+
* run: async ({ scheduledAt }) => { await emailReport(scheduledAt); },
|
|
45
|
+
* timeoutMs: 5 * 60_000,
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* jobs.start();
|
|
50
|
+
* // ...later, on shutdown:
|
|
51
|
+
* await jobs.stop();
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @module scheduler
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** Context passed to each job handler. */
|
|
58
|
+
export interface CronContext {
|
|
59
|
+
/** Job name (the key under which the job was registered). */
|
|
60
|
+
name: string;
|
|
61
|
+
/** The scheduled firing time (close to, but not exactly, now). */
|
|
62
|
+
scheduledAt: Date;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Configuration for a single cron job. */
|
|
66
|
+
export interface CronJobConfig {
|
|
67
|
+
/** Crontab expression. Examples: "*\/15 * * * *", "0 3 * * *", "@daily". */
|
|
68
|
+
schedule: string;
|
|
69
|
+
/** Job handler. May be async; return value is ignored. */
|
|
70
|
+
run: (ctx: CronContext) => void | Promise<void>;
|
|
71
|
+
/** Skip registration in dev mode (NODE_ENV !== "production"). Default: false. */
|
|
72
|
+
skipInDev?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Soft timeout in ms. On timeout, a warning is logged and the in-flight flag
|
|
75
|
+
* clears so the next tick can run. The current handler is NOT aborted —
|
|
76
|
+
* Bun.cron has no cancellation primitive. Default: unlimited.
|
|
77
|
+
*/
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Observable status for a single job. */
|
|
82
|
+
export interface CronJobStatus {
|
|
83
|
+
/** Epoch ms of the last completed run, or null if never run. */
|
|
84
|
+
lastRunAt: number | null;
|
|
85
|
+
/** Duration of the last completed run in ms, or null if never run. */
|
|
86
|
+
lastDurationMs: number | null;
|
|
87
|
+
/** True while a handler is executing. */
|
|
88
|
+
inFlight: boolean;
|
|
89
|
+
/** Number of handler invocations that reached completion (including errors). */
|
|
90
|
+
runCount: number;
|
|
91
|
+
/** Number of ticks dropped because the previous run had not finished. */
|
|
92
|
+
skipCount: number;
|
|
93
|
+
/** Number of handler invocations that threw. */
|
|
94
|
+
errorCount: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Handle returned by {@link defineCron}. */
|
|
98
|
+
export interface CronRegistration {
|
|
99
|
+
/** Schedule all non-dev-skipped jobs. Idempotent — calling twice is a no-op. */
|
|
100
|
+
start(): void;
|
|
101
|
+
/** Stop accepting new ticks and wait for any in-flight handler to finish. */
|
|
102
|
+
stop(): Promise<void>;
|
|
103
|
+
/** Snapshot per-job statistics. */
|
|
104
|
+
status(): Record<string, CronJobStatus>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Minimal shape of the thing `Bun.cron` returns. */
|
|
108
|
+
interface CronJobHandle {
|
|
109
|
+
stop?: () => void | Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Function shape used to register a cron schedule. Matches `Bun.cron` but kept
|
|
114
|
+
* abstract so tests can inject a controllable fake.
|
|
115
|
+
*
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
export type CronScheduleFn = (
|
|
119
|
+
schedule: string,
|
|
120
|
+
handler: () => void | Promise<void>,
|
|
121
|
+
) => CronJobHandle | void;
|
|
122
|
+
|
|
123
|
+
interface BunCronGlobal {
|
|
124
|
+
cron?: CronScheduleFn;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolves `Bun.cron` at call time. Throws a clear, actionable error when the
|
|
129
|
+
* runtime doesn't provide it — matches the `auth/password.ts` style.
|
|
130
|
+
*/
|
|
131
|
+
function getBunCron(): CronScheduleFn {
|
|
132
|
+
const g = globalThis as unknown as { Bun?: BunCronGlobal };
|
|
133
|
+
if (!g.Bun || typeof g.Bun.cron !== "function") {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"[@mandujs/core/scheduler] Bun.cron is unavailable — this module requires the Bun runtime (>= 1.3.12).",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return g.Bun.cron;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Per-job mutable runtime state. */
|
|
142
|
+
interface JobState {
|
|
143
|
+
readonly name: string;
|
|
144
|
+
readonly config: CronJobConfig;
|
|
145
|
+
readonly skipped: boolean;
|
|
146
|
+
handle: CronJobHandle | null;
|
|
147
|
+
status: CronJobStatus;
|
|
148
|
+
/** Resolves when the in-flight handler (if any) finishes. */
|
|
149
|
+
inFlightSettle: Promise<void> | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Registers a set of cron jobs. Returns a handle; does NOT auto-start —
|
|
154
|
+
* call `.start()` from your server boot sequence.
|
|
155
|
+
*
|
|
156
|
+
* The public API. Internally dispatches to {@link _defineCronWith} passing
|
|
157
|
+
* `Bun.cron` as the scheduler.
|
|
158
|
+
*/
|
|
159
|
+
export function defineCron(jobs: Record<string, CronJobConfig>): CronRegistration {
|
|
160
|
+
// Probe lazily so `defineCron({})` with no entries can still be called in
|
|
161
|
+
// environments without `Bun.cron`. When the user actually goes to `start()`,
|
|
162
|
+
// the probe runs — matching `getBunPassword()` behaviour.
|
|
163
|
+
return _defineCronWith(jobs, (schedule, handler) => getBunCron()(schedule, handler));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Core constructor. Exposed for tests so they can inject a controllable fake
|
|
168
|
+
* scheduler and drive ticks deterministically without touching real cron.
|
|
169
|
+
*
|
|
170
|
+
* @internal
|
|
171
|
+
*/
|
|
172
|
+
export function _defineCronWith(
|
|
173
|
+
jobs: Record<string, CronJobConfig>,
|
|
174
|
+
scheduleFn: CronScheduleFn,
|
|
175
|
+
): CronRegistration {
|
|
176
|
+
const isProd =
|
|
177
|
+
typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
178
|
+
|
|
179
|
+
// Freeze the job set at definition time — no add/remove after construction.
|
|
180
|
+
const names = Object.keys(jobs);
|
|
181
|
+
const states: Map<string, JobState> = new Map();
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
const config = jobs[name];
|
|
184
|
+
const skipped = config.skipInDev === true && !isProd;
|
|
185
|
+
states.set(name, {
|
|
186
|
+
name,
|
|
187
|
+
config,
|
|
188
|
+
skipped,
|
|
189
|
+
handle: null,
|
|
190
|
+
status: {
|
|
191
|
+
lastRunAt: null,
|
|
192
|
+
lastDurationMs: null,
|
|
193
|
+
inFlight: false,
|
|
194
|
+
runCount: 0,
|
|
195
|
+
skipCount: 0,
|
|
196
|
+
errorCount: 0,
|
|
197
|
+
},
|
|
198
|
+
inFlightSettle: null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let started = false;
|
|
203
|
+
let stopping = false;
|
|
204
|
+
|
|
205
|
+
function makeTickHandler(state: JobState): () => Promise<void> {
|
|
206
|
+
return async () => {
|
|
207
|
+
// No new ticks once we've started stopping.
|
|
208
|
+
if (stopping) return;
|
|
209
|
+
|
|
210
|
+
// Overlap prevention: if the previous invocation is still running, skip.
|
|
211
|
+
if (state.status.inFlight) {
|
|
212
|
+
state.status.skipCount += 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
state.status.inFlight = true;
|
|
217
|
+
const startedAt = Date.now();
|
|
218
|
+
|
|
219
|
+
const ctx: CronContext = {
|
|
220
|
+
name: state.name,
|
|
221
|
+
scheduledAt: new Date(startedAt),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// The promise that future ticks (and `stop()`) wait on. We capture it
|
|
225
|
+
// in a variable so the `.finally()` can resolve the outer promise even
|
|
226
|
+
// if `run()` itself throws synchronously.
|
|
227
|
+
let settleResolve!: () => void;
|
|
228
|
+
const settle = new Promise<void>((r) => {
|
|
229
|
+
settleResolve = r;
|
|
230
|
+
});
|
|
231
|
+
state.inFlightSettle = settle;
|
|
232
|
+
|
|
233
|
+
const runAndCount = (async () => {
|
|
234
|
+
try {
|
|
235
|
+
await state.config.run(ctx);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
state.status.errorCount += 1;
|
|
238
|
+
// Error isolation — never let a handler crash the process.
|
|
239
|
+
console.error(
|
|
240
|
+
`[scheduler] job ${state.name} failed:`,
|
|
241
|
+
error,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
})();
|
|
245
|
+
|
|
246
|
+
// Decide whether to wait for the handler or give up after timeout.
|
|
247
|
+
const timeoutMs = state.config.timeoutMs;
|
|
248
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
249
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
250
|
+
const timeoutMarker = Symbol("timeout");
|
|
251
|
+
const timeoutPromise = new Promise<typeof timeoutMarker>((resolve) => {
|
|
252
|
+
timeoutHandle = setTimeout(() => resolve(timeoutMarker), timeoutMs);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const winner = await Promise.race([runAndCount.then(() => null), timeoutPromise]);
|
|
256
|
+
|
|
257
|
+
if (winner === timeoutMarker) {
|
|
258
|
+
// Handler is still running on its own. Log, clear inFlight so the
|
|
259
|
+
// next tick can fire, but do NOT attempt to cancel — Bun.cron has
|
|
260
|
+
// no cancellation and calling back into the handler would risk
|
|
261
|
+
// double-execution.
|
|
262
|
+
console.warn(
|
|
263
|
+
`[scheduler] job ${state.name} exceeded timeoutMs=${timeoutMs} — future ticks may run while the previous handler is still executing.`,
|
|
264
|
+
);
|
|
265
|
+
state.status.runCount += 1;
|
|
266
|
+
state.status.lastRunAt = Date.now();
|
|
267
|
+
state.status.lastDurationMs = Date.now() - startedAt;
|
|
268
|
+
state.status.inFlight = false;
|
|
269
|
+
settleResolve();
|
|
270
|
+
state.inFlightSettle = null;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handler finished first — clear the timeout to avoid a leaked timer.
|
|
275
|
+
if (timeoutHandle !== undefined) {
|
|
276
|
+
clearTimeout(timeoutHandle);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
await runAndCount;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
state.status.runCount += 1;
|
|
283
|
+
state.status.lastRunAt = Date.now();
|
|
284
|
+
state.status.lastDurationMs = Date.now() - startedAt;
|
|
285
|
+
state.status.inFlight = false;
|
|
286
|
+
settleResolve();
|
|
287
|
+
state.inFlightSettle = null;
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function start(): void {
|
|
292
|
+
if (started) return;
|
|
293
|
+
started = true;
|
|
294
|
+
stopping = false;
|
|
295
|
+
for (const state of states.values()) {
|
|
296
|
+
if (state.skipped) continue;
|
|
297
|
+
const tick = makeTickHandler(state);
|
|
298
|
+
const handle = scheduleFn(state.config.schedule, tick);
|
|
299
|
+
state.handle = handle ?? null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function stop(): Promise<void> {
|
|
304
|
+
if (!started) return;
|
|
305
|
+
stopping = true;
|
|
306
|
+
// Tell each underlying cron to stop firing new ticks. Handles returned
|
|
307
|
+
// from `Bun.cron` may be void (docs show `await Bun.cron.remove(name)` as
|
|
308
|
+
// the alternate shape), so we defensively handle both.
|
|
309
|
+
const stopPromises: Array<Promise<void>> = [];
|
|
310
|
+
for (const state of states.values()) {
|
|
311
|
+
if (state.handle && typeof state.handle.stop === "function") {
|
|
312
|
+
const r = state.handle.stop();
|
|
313
|
+
if (r && typeof (r as Promise<void>).then === "function") {
|
|
314
|
+
stopPromises.push(r as Promise<void>);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
state.handle = null;
|
|
318
|
+
}
|
|
319
|
+
if (stopPromises.length > 0) {
|
|
320
|
+
await Promise.allSettled(stopPromises);
|
|
321
|
+
}
|
|
322
|
+
// Wait for any in-flight handler to settle.
|
|
323
|
+
const inflight: Array<Promise<void>> = [];
|
|
324
|
+
for (const state of states.values()) {
|
|
325
|
+
if (state.inFlightSettle) inflight.push(state.inFlightSettle);
|
|
326
|
+
}
|
|
327
|
+
if (inflight.length > 0) {
|
|
328
|
+
await Promise.allSettled(inflight);
|
|
329
|
+
}
|
|
330
|
+
started = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function status(): Record<string, CronJobStatus> {
|
|
334
|
+
const out: Record<string, CronJobStatus> = {};
|
|
335
|
+
for (const [name, state] of states) {
|
|
336
|
+
// Snapshot (shallow clone) so callers can't mutate internal state.
|
|
337
|
+
out[name] = { ...state.status };
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { start, stop, status };
|
|
343
|
+
}
|