@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc

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 (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -3,10 +3,10 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { r as isAlreadyExecutingSkip } from "./cron-scheduler-CYQgco86.js";
6
+ import { r as isAlreadyExecutingSkip } from "./cron-scheduler-idtLPoN-.js";
7
7
  import { r as logger } from "./logger-D-S-hO5e.js";
8
+ import { a as resolveListLimitParam } from "./query-parser-4WkMoA_K.js";
8
9
  import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
9
- import { a as resolveListLimitParam } from "./query-parser-BgiKJKvc.js";
10
10
  import { Hono } from "hono";
11
11
  //#region src/cron/cron-routes.ts
12
12
  var cron_routes_exports = /* @__PURE__ */ __exportAll({ createCronRoutes: () => createCronRoutes });
@@ -18,12 +18,24 @@ function actorOf(c) {
18
18
  return apiKey ? `api-key:${apiKey.id}` : void 0;
19
19
  }
20
20
  /**
21
+ * The manual trigger's `?async` switch. Absent means synchronous, as it always
22
+ * was; anything other than a plain yes or no is refused rather than read as
23
+ * one, so `?async=yes-please` does not quietly hold the request open.
24
+ */
25
+ function readAsyncFlag(raw) {
26
+ if (raw === void 0) return false;
27
+ const value = raw.trim().toLowerCase();
28
+ if (value === "1" || value === "true") return true;
29
+ if (value === "" || value === "0" || value === "false") return false;
30
+ throw ApiError.badRequest(`?async must be 1, true, 0 or false — got "${raw}"`);
31
+ }
32
+ /**
21
33
  * Create admin REST routes for managing cron jobs.
22
34
  *
23
35
  * Routes:
24
36
  * GET / → list all cron jobs
25
37
  * GET /:id → get a single job's status
26
- * POST /:id/trigger → manually trigger a job
38
+ * POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)
27
39
  * GET /:id/logs → get execution logs for a job
28
40
  * PUT /:id → update job (enable/disable, or `null` to follow the code)
29
41
  *
@@ -56,7 +68,18 @@ function createCronRoutes(scheduler, skipped = 0) {
56
68
  });
57
69
  router.post("/:id/trigger", async (c) => {
58
70
  const id = c.req.param("id");
71
+ const asyncRun = readAsyncFlag(c.req.query("async"));
59
72
  if (!scheduler.getJob(id)) throw ApiError.notFound(`Cron job "${id}" not found`);
73
+ if (asyncRun) {
74
+ const outcome = await scheduler.startJob(id);
75
+ if (!outcome) throw ApiError.notFound(`Cron job "${id}" not found`);
76
+ if (!outcome.started) throw ApiError.conflict(`Cron job "${id}" is already executing — try again when that run has finished`, "CRON_JOB_ALREADY_EXECUTING", { log: outcome.log });
77
+ return c.json({
78
+ accepted: true,
79
+ startedAt: outcome.startedAt,
80
+ job: await scheduler.fetchJob(id)
81
+ }, 202);
82
+ }
60
83
  const log = await scheduler.triggerJob(id);
61
84
  if (log && isAlreadyExecutingSkip(log)) throw ApiError.conflict(`Cron job "${id}" is already executing — try again when that run has finished`, "CRON_JOB_ALREADY_EXECUTING", { log });
62
85
  return c.json({
@@ -93,4 +116,4 @@ function createCronRoutes(scheduler, skipped = 0) {
93
116
  //#endregion
94
117
  export { cron_routes_exports as n, createCronRoutes as t };
95
118
 
96
- //# sourceMappingURL=cron-routes-Bc-SB0Se.js.map
119
+ //# sourceMappingURL=cron-routes-OD6BstUj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-routes-OD6BstUj.js","names":[],"sources":["../src/cron/cron-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Context } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { isAlreadyExecutingSkip, type CronScheduler } from \"./cron-scheduler\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { resolveListLimitParam } from \"../api/rest/query-parser\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Who made a change, as `cron_job_state.updated_by` records it. */\nfunction actorOf(c: Context<HonoEnv>): string | undefined {\n const user = c.get(\"user\");\n if (typeof user === \"object\" && user !== null && typeof user.uid === \"string\") return user.uid;\n const apiKey = c.get(\"apiKey\");\n return apiKey ? `api-key:${apiKey.id}` : undefined;\n}\n\n/**\n * The manual trigger's `?async` switch. Absent means synchronous, as it always\n * was; anything other than a plain yes or no is refused rather than read as\n * one, so `?async=yes-please` does not quietly hold the request open.\n */\nfunction readAsyncFlag(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const value = raw.trim().toLowerCase();\n if (value === \"1\" || value === \"true\") return true;\n if (value === \"\" || value === \"0\" || value === \"false\") return false;\n throw ApiError.badRequest(`?async must be 1, true, 0 or false — got \"${raw}\"`);\n}\n\n/**\n * Create admin REST routes for managing cron jobs.\n *\n * Routes:\n * GET / → list all cron jobs\n * GET /:id → get a single job's status\n * POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)\n * GET /:id/logs → get execution logs for a job\n * PUT /:id → update job (enable/disable, or `null` to follow the code)\n *\n * Every answer about a job's state is read from the store, not from this\n * process's memory: the process serving these routes is often not the one\n * running the jobs (the `api` role never starts its scheduler), and is only\n * ever one replica of several.\n */\nexport function createCronRoutes(scheduler: CronScheduler, skipped = 0): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Hono's onError does NOT propagate from parent to child routers, so this\n // child router registers its own handler to format thrown ApiErrors.\n router.onError(errorHandler);\n\n // List all jobs\n router.get(\"/\", async (c) => {\n const jobs = await scheduler.fetchJobs();\n // A file that failed to load is not a job, so it appears nowhere in\n // this list — and \"my job is missing\" and \"my job is not scheduled\"\n // look identical from here. Say how many were dropped, as the\n // functions listing does, so the Studio panel and anyone with curl can\n // see it without boot-log access.\n //\n // Two ways to be dropped, counted together and reported apart. A file\n // the loader could not read has only a count: the failure happened\n // before there was a job to name. A schedule the scheduler refused has a\n // name and a reason — most often \"Expected 5 fields, got 6\", from an\n // expression copied out of a tool that supports seconds — and quoting it\n // here turns a job that silently never fires into a one-line fix.\n const rejected = scheduler.listRejectedJobs();\n const total = skipped + rejected.length;\n return c.json({\n jobs,\n ...(total > 0 && {\n skipped: total,\n ...(rejected.length > 0 && { rejected }),\n note: [\n skipped > 0 ? `${skipped} cron file(s) failed to load` : undefined,\n // Not \"an invalid schedule\": a job is also refused for an\n // unknown timezone or a timeout that is not a positive\n // number, and `rejected` says which.\n rejected.length > 0 ? `${rejected.length} job(s) were refused` : undefined\n ].filter(Boolean).join(\" and \") +\n \" — NOT scheduled. \" +\n (rejected.length > 0\n ? \"See `rejected` for the reason; \"\n : \"\") +\n \"the server log has the rest.\"\n })\n });\n });\n\n // Get single job\n router.get(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const job = await scheduler.fetchJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n return c.json({ job });\n });\n\n // Trigger a job manually\n router.post(\"/:id/trigger\", async (c) => {\n const id = c.req.param(\"id\");\n const asyncRun = readAsyncFlag(c.req.query(\"async\"));\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n // `?async=1`: answer once the run has started rather than when it\n // ends. A long job's synchronous answer is cut by any proxy with a\n // shorter timeout than the job (60 s is common) while the run carries\n // on; this one is not. The outcome is in `GET /:id/logs`, as the entry\n // with `manual: true` and this `startedAt`.\n if (asyncRun) {\n const outcome = await scheduler.startJob(id);\n if (!outcome) throw ApiError.notFound(`Cron job \"${id}\" not found`);\n if (!outcome.started) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log: outcome.log }\n );\n }\n return c.json({\n accepted: true,\n startedAt: outcome.startedAt,\n job: await scheduler.fetchJob(id)\n }, 202);\n }\n\n const log = await scheduler.triggerJob(id);\n // Nothing ran, so this is not a success: the job is executing already —\n // on this process, or on whichever one holds its run lease (the worker,\n // seen from the api role). The skip is in the run history either way;\n // `details.log` carries it, and its line names the process.\n if (log && isAlreadyExecutingSkip(log)) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log }\n );\n }\n return c.json({ log,\njob: await scheduler.fetchJob(id) });\n });\n\n // Get job logs\n router.get(\"/:id/logs\", async (c) => {\n const id = c.req.param(\"id\");\n // Validated, not `parseInt`-ed. `?limit=abc` used to reach the store as\n // `NaN`, where Postgres refused `LIMIT NaN`, the store swallowed the\n // error and returned `[]` — a 200 with an empty list, which reads as\n // \"this job has never run\". The data plane answers 400 for the same\n // input; so does this now.\n const limit = resolveListLimitParam(c.req.query(\"limit\") ?? null, { defaultLimit: 50 });\n\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n const logs = await scheduler.getJobLogsFromDb(id, limit).catch((err: unknown) => {\n // Not an empty 200: Studio shows that as \"No executions yet\".\n logger.error(`[cron] Could not read the run history of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The run history of cron job \"${id}\" could not be read. The server log has the reason.`\n );\n });\n return c.json({ logs });\n });\n\n // Enable/disable a job — for every process, and across redeploys\n router.put(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const body: unknown = await c.req.json().catch(() => ({}));\n const enabled = typeof body === \"object\" && body !== null && \"enabled\" in body ? body.enabled : undefined;\n\n if (typeof enabled !== \"boolean\" && enabled !== null) {\n throw ApiError.badRequest(\n \"Missing 'enabled' in body: true or false to override the job's code, or null to follow it again\"\n );\n }\n\n if (!scheduler.getJob(id)) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n try {\n await scheduler.persistJobEnabled(id, enabled, actorOf(c));\n } catch (err) {\n logger.error(`[cron] Could not save the enabled state of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The change to cron job \"${id}\" could not be saved, so no process was changed. ` +\n \"The server log has the reason.\"\n );\n }\n\n return c.json({ job: await scheduler.fetchJob(id) });\n });\n\n return router;\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import "./src-CatHFUym.js";
6
+ import "./src-D5Y3N-S4.js";
7
7
  import { a as redactSensitiveText, r as logger, s as stringifyForLog } from "./logger-D-S-hO5e.js";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { hostname } from "node:os";
@@ -149,6 +149,8 @@ function buildScaleToZeroWarning(jobs, env = process.env) {
149
149
  //#region src/cron/cron-scheduler.ts
150
150
  var cron_scheduler_exports = /* @__PURE__ */ __exportAll({
151
151
  CronScheduler: () => CronScheduler,
152
+ MAX_CAPTURED_LOG_BYTES: () => MAX_CAPTURED_LOG_BYTES,
153
+ MAX_CAPTURED_LOG_LINES: () => 500,
152
154
  findMostRecentSlot: () => findMostRecentSlot,
153
155
  isAlreadyExecutingSkip: () => isAlreadyExecutingSkip,
154
156
  isValidTimeZone: () => isValidTimeZone,
@@ -295,6 +297,7 @@ function wallClockIn(candidate, zone) {
295
297
  timeZone: zone,
296
298
  hourCycle: "h23",
297
299
  weekday: "short",
300
+ year: "numeric",
298
301
  month: "numeric",
299
302
  day: "numeric",
300
303
  hour: "numeric",
@@ -305,6 +308,7 @@ function wallClockIn(candidate, zone) {
305
308
  const parts = {};
306
309
  for (const part of formatter.formatToParts(candidate)) parts[part.type] = part.value;
307
310
  return {
311
+ year: Number(parts.year),
308
312
  minute: Number(parts.minute),
309
313
  hour: Number(parts.hour),
310
314
  dom: Number(parts.day),
@@ -313,21 +317,69 @@ function wallClockIn(candidate, zone) {
313
317
  };
314
318
  }
315
319
  /**
316
- * Whether a minute-precision instant matches every field of the expression.
317
- *
318
- * Read in `zone` when one is named, else in the process's own zone — which is
319
- * whatever the host is set to, UTC in nearly every container. A schedule that
320
- * names its zone means the same wall-clock hour on every host it runs on.
320
+ * The wall clock an instant reads in `zone` when one is named, else in the
321
+ * process's own zone — which is whatever the host is set to, UTC in nearly
322
+ * every container. A schedule that names its zone means the same wall-clock
323
+ * hour on every host it runs on.
321
324
  */
322
- function matchesCronFields(candidate, fields, zone) {
323
- const wall = zone ? wallClockIn(candidate, zone) : {
325
+ function wallClockOf(instantMs, zone) {
326
+ const candidate = new Date(instantMs);
327
+ if (zone) return wallClockIn(candidate, zone);
328
+ return {
329
+ year: candidate.getFullYear(),
324
330
  minute: candidate.getMinutes(),
325
331
  hour: candidate.getHours(),
326
332
  dom: candidate.getDate(),
327
333
  month: candidate.getMonth() + 1,
328
334
  dow: candidate.getDay()
329
335
  };
330
- return fields.months.includes(wall.month) && fields.doms.includes(wall.dom) && fields.dows.includes(wall.dow) && fields.hours.includes(wall.hour) && fields.minutes.includes(wall.minute);
336
+ }
337
+ /**
338
+ * The zone's offset from UTC at an instant: its wall clock read back as UTC,
339
+ * minus the instant. Two instants with the same offset have wall clocks
340
+ * exactly as far apart as the instants themselves.
341
+ */
342
+ function offsetAt(instantMs, wall) {
343
+ return Date.UTC(wall.year, wall.month - 1, wall.dom, wall.hour, wall.minute) - instantMs;
344
+ }
345
+ /** Whether a wall-clock day — month, day of month, weekday — can hold a slot. */
346
+ function dayMatches(wall, fields) {
347
+ return fields.months.includes(wall.month) && fields.doms.includes(wall.dom) && fields.dows.includes(wall.dow);
348
+ }
349
+ /** Whether a wall-clock minute matches every field of the expression. */
350
+ function wallMatches(wall, fields) {
351
+ return dayMatches(wall, fields) && fields.hours.includes(wall.hour) && fields.minutes.includes(wall.minute);
352
+ }
353
+ var MINUTES_PER_DAY = 1440;
354
+ /**
355
+ * How many wall-clock minutes from a non-matching `wall` to the first later
356
+ * wall minute that could match. Never past the next wall midnight, so the
357
+ * minutes in between share `wall`'s day and every one of them is a miss.
358
+ */
359
+ function wallMinutesToNextCandidate(wall, fields) {
360
+ const minuteOfDay = wall.hour * 60 + wall.minute;
361
+ if (!dayMatches(wall, fields)) return MINUTES_PER_DAY - minuteOfDay;
362
+ if (!fields.hours.includes(wall.hour)) {
363
+ const nextHour = fields.hours.find((hour) => hour > wall.hour);
364
+ return nextHour === void 0 ? MINUTES_PER_DAY - minuteOfDay : nextHour * 60 - minuteOfDay;
365
+ }
366
+ const nextMinute = fields.minutes.find((minute) => minute > wall.minute);
367
+ return nextMinute === void 0 ? 60 - wall.minute : nextMinute - wall.minute;
368
+ }
369
+ /**
370
+ * The mirror of {@link wallMinutesToNextCandidate}: how many wall-clock
371
+ * minutes back from a non-matching `wall` to the latest earlier wall minute
372
+ * that could match. Never before 23:59 of the previous wall day.
373
+ */
374
+ function wallMinutesToPreviousCandidate(wall, fields) {
375
+ const minuteOfDay = wall.hour * 60 + wall.minute;
376
+ if (!dayMatches(wall, fields)) return minuteOfDay + 1;
377
+ if (!fields.hours.includes(wall.hour)) {
378
+ const previousHour = [...fields.hours].reverse().find((hour) => hour < wall.hour);
379
+ return previousHour === void 0 ? minuteOfDay + 1 : minuteOfDay - (previousHour * 60 + 59);
380
+ }
381
+ const previousMinute = [...fields.minutes].reverse().find((minute) => minute < wall.minute);
382
+ return previousMinute === void 0 ? wall.minute + 1 : wall.minute - previousMinute;
331
383
  }
332
384
  /**
333
385
  * How far forward to look for the next matching slot.
@@ -339,27 +391,86 @@ function matchesCronFields(candidate, fields, zone) {
339
391
  var MAX_SLOT_SEARCH_MINUTES = 2105280;
340
392
  var MINUTE_MS = 6e4;
341
393
  /**
394
+ * The first real minute in `[startMs, endMs)`, stepping in `direction`, whose
395
+ * wall clock matches `fields` — the same answer as testing every minute in
396
+ * turn, without testing every minute.
397
+ *
398
+ * The search moves through *real* minutes and only reads the wall clock, never
399
+ * steps it: across a daylight-saving change a wall-clock minute is not a real
400
+ * one. Falling back, a wall-clock step from 02:59 summer time lands on 03:00
401
+ * winter time and skips the repeated hour; inside it, the "next" minute can be
402
+ * an hour in the past. That is why this used to test one real minute at a
403
+ * time — and a yearly job named in a zone cost half a million `Intl` calls,
404
+ * blocking the event loop for a third of a second on every re-arm, and two
405
+ * seconds for 29 February.
406
+ *
407
+ * Now it hops. From a miss, {@link wallMinutesToNextCandidate} says how many
408
+ * wall minutes cannot match (the rest of a day that is the wrong date, the
409
+ * rest of an hour that is the wrong hour, the minutes before the next listed
410
+ * one). While the zone's offset holds, that many wall minutes are exactly
411
+ * that many real minutes, so the hop is taken when the offset at both ends is
412
+ * the same. When it differs, a transition lies inside the hop: the search
413
+ * bisects for the first minute on the new offset and resumes there — every
414
+ * minute before it is on the old offset, so its wall clock is inside the
415
+ * stretch already known to miss. A hop is at most a day, and the one thing
416
+ * this assumes is that no zone changes its offset and changes it back within
417
+ * a single day, which none in the tz database does. Each day costs one
418
+ * wall-clock read, plus about a dozen for each transition crossed.
419
+ *
420
+ * `direction: -1` walks back from `startMs` to `endMs` (inclusive) with the
421
+ * mirror of the same hop, for {@link findMostRecentSlot}.
422
+ */
423
+ function searchSlot(fields, startMs, endMs, direction, zone) {
424
+ const inWindow = (instantMs) => direction === 1 ? instantMs < endMs : instantMs >= endMs;
425
+ let current = startMs;
426
+ let wall = wallClockOf(current, zone);
427
+ while (inWindow(current)) {
428
+ if (wallMatches(wall, fields)) return current;
429
+ const skip = direction === 1 ? wallMinutesToNextCandidate(wall, fields) : wallMinutesToPreviousCandidate(wall, fields);
430
+ const target = current + direction * skip * MINUTE_MS;
431
+ const targetWall = wallClockOf(target, zone);
432
+ const offset = offsetAt(current, wall);
433
+ if (skip === 1 || offsetAt(target, targetWall) === offset) {
434
+ current = target;
435
+ wall = targetWall;
436
+ continue;
437
+ }
438
+ let held = current;
439
+ let changed = target;
440
+ let changedWall = targetWall;
441
+ while (Math.abs(changed - held) > MINUTE_MS) {
442
+ const middle = held + direction * Math.floor(Math.abs(changed - held) / MINUTE_MS / 2) * MINUTE_MS;
443
+ const middleWall = wallClockOf(middle, zone);
444
+ if (offsetAt(middle, middleWall) === offset) held = middle;
445
+ else {
446
+ changed = middle;
447
+ changedWall = middleWall;
448
+ }
449
+ }
450
+ current = changed;
451
+ wall = changedWall;
452
+ }
453
+ }
454
+ /**
342
455
  * Calculate the next Date after `after` that matches the cron expression.
343
456
  * Throws on invalid expressions.
344
457
  */
345
458
  function parseCronExpression(expression, after, timezone) {
346
459
  const fields = parseCronFields(expression);
347
- const candidate = new Date(after);
348
- candidate.setUTCSeconds(0, 0);
349
- candidate.setTime(candidate.getTime() + MINUTE_MS);
350
- for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES; i++) {
351
- if (matchesCronFields(candidate, fields, timezone)) return candidate;
352
- candidate.setTime(candidate.getTime() + MINUTE_MS);
353
- }
460
+ const start = new Date(after);
461
+ start.setUTCSeconds(0, 0);
462
+ const startMs = start.getTime() + MINUTE_MS;
463
+ const slot = searchSlot(fields, startMs, startMs + MAX_SLOT_SEARCH_MINUTES * MINUTE_MS, 1, timezone);
464
+ if (slot !== void 0) return new Date(slot);
354
465
  throw new Error(`Cron expression "${expression}" has no matching time within ${Math.round(MAX_SLOT_SEARCH_MINUTES / 525960)} years of ${after.toISOString()}. Check the day-of-month and month fields — a date such as 31 February never occurs.`);
355
466
  }
356
467
  /**
357
468
  * The latest slot matching `expression` within the inclusive window
358
469
  * `[from, to]`, or `undefined` when the expression has no slot in it.
359
470
  *
360
- * Walks backwards a minute at a time from `to`, so the first hit is already
361
- * the answer — in the common case (a job that ran normally moments ago) that
362
- * is a handful of iterations, not a scan of the whole window.
471
+ * Walks backwards from `to`, so the first hit is already the answer — in the
472
+ * common case (a job that ran normally moments ago) that is a handful of
473
+ * steps, not a scan of the whole window.
363
474
  *
364
475
  * `to`'s own minute is included: an instance booting at 06:00:30 has *not* run
365
476
  * the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —
@@ -371,14 +482,46 @@ function parseCronExpression(expression, after, timezone) {
371
482
  */
372
483
  function findMostRecentSlot(expression, from, to, timezone) {
373
484
  const fields = parseCronFields(expression);
374
- const candidate = new Date(to);
375
- candidate.setUTCSeconds(0, 0);
376
- for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES && candidate.getTime() >= from.getTime(); i++) {
377
- if (matchesCronFields(candidate, fields, timezone)) return candidate;
378
- candidate.setTime(candidate.getTime() - MINUTE_MS);
379
- }
485
+ const start = new Date(to);
486
+ start.setUTCSeconds(0, 0);
487
+ const startMs = start.getTime();
488
+ const slot = searchSlot(fields, startMs, Math.max(from.getTime(), startMs - (MAX_SLOT_SEARCH_MINUTES - 1) * MINUTE_MS), -1, timezone);
489
+ return slot === void 0 ? void 0 : new Date(slot);
380
490
  }
381
491
  var MAX_LOGS_PER_JOB = 50;
492
+ var MAX_CAPTURED_LOG_BYTES = 64 * 1024;
493
+ /**
494
+ * A run's `ctx.log` capture, capped at {@link MAX_CAPTURED_LOG_LINES} and
495
+ * {@link MAX_CAPTURED_LOG_BYTES}. The kept lines are the first ones: once one
496
+ * line is dropped, every later one is too, so the history reads as a prefix of
497
+ * the run's output and never as lines with gaps between them.
498
+ */
499
+ function createLogCapture(firstLine) {
500
+ const lines = [];
501
+ let bytes = 0;
502
+ let dropped = 0;
503
+ let closed = false;
504
+ const capture = {
505
+ add(line) {
506
+ if (closed) return;
507
+ const size = Buffer.byteLength(line, "utf8");
508
+ if (dropped === 0 && lines.length < 500 && bytes + size <= 65536) {
509
+ lines.push(line);
510
+ bytes += size;
511
+ return;
512
+ }
513
+ dropped++;
514
+ },
515
+ /** The lines kept, and the line that says what was not. Later `add`s are ignored. */
516
+ close() {
517
+ if (!closed && dropped > 0) lines.push(`… ${dropped} more line${dropped === 1 ? "" : "s"} not kept — see the server log`);
518
+ closed = true;
519
+ return lines;
520
+ }
521
+ };
522
+ if (firstLine !== void 0) capture.add(firstLine);
523
+ return capture;
524
+ }
382
525
  /**
383
526
  * Minimum milliseconds between scheduled executions of the same job.
384
527
  * Prevents tight re-execution loops caused by jitter or clock drift.
@@ -410,6 +553,12 @@ function setLongTimeout(callback, delayMs) {
410
553
  /** A run's timeout when its definition names none. */
411
554
  var DEFAULT_TIMEOUT_SECONDS = 300;
412
555
  /**
556
+ * How often a started scheduler deletes slot claims and run history past
557
+ * retention. Both accrue one row per job per fire, so a scheduler that lives
558
+ * for months needs to prune while it runs, not only when it next boots.
559
+ */
560
+ var CLAIM_SWEEP_INTERVAL_MS = 60 * 6e4;
561
+ /**
413
562
  * How long a run lease outlasts the run's own timeout.
414
563
  *
415
564
  * The lease is timed on the database's clock from the moment it is taken, the
@@ -469,6 +618,10 @@ var CronScheduler = class {
469
618
  * ever clear the lease that run took.
470
619
  */
471
620
  instanceLabel = `${hostname()}:${process.pid}`;
621
+ /** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */
622
+ claimSweepTimer;
623
+ sweepingClaims = false;
624
+ sweepingLogs = false;
472
625
  /**
473
626
  * Set the server singleton to make it available to cron job handlers.
474
627
  *
@@ -559,8 +712,48 @@ var CronScheduler = class {
559
712
  if (!this.store) logger.warn("[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job");
560
713
  this.warnIfScaleToZero();
561
714
  this.catchUpMissedSlots();
715
+ if (this.store?.sweepClaims || this.store?.sweepLogs) {
716
+ const sweep = () => {
717
+ this.sweepClaims();
718
+ this.sweepLogs();
719
+ };
720
+ sweep();
721
+ this.claimSweepTimer = setInterval(sweep, CLAIM_SWEEP_INTERVAL_MS);
722
+ this.claimSweepTimer.unref?.();
723
+ }
562
724
  logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
563
725
  }
726
+ /** Delete slot claims past retention. Failures are logged; the next hour tries again. */
727
+ async sweepClaims() {
728
+ const store = this.store;
729
+ if (!store?.sweepClaims || this.sweepingClaims) return;
730
+ this.sweepingClaims = true;
731
+ try {
732
+ await store.sweepClaims();
733
+ } catch (err) {
734
+ logger.warn("[cron] The claim retention sweep failed; it runs again within the hour", { error: err });
735
+ } finally {
736
+ this.sweepingClaims = false;
737
+ }
738
+ }
739
+ /**
740
+ * Delete run history past retention, and count the runs not counted yet.
741
+ * Failures are logged; the next hour tries again, from where this one
742
+ * stopped — each batch it finished is committed.
743
+ */
744
+ async sweepLogs() {
745
+ const store = this.store;
746
+ if (!store?.sweepLogs || this.sweepingLogs) return;
747
+ this.sweepingLogs = true;
748
+ try {
749
+ const { counted } = await store.sweepLogs();
750
+ if (counted > 0) logger.info(`[cron] Counted ${counted} earlier run(s) into the jobs' run totals`);
751
+ } catch (err) {
752
+ logger.warn("[cron] The run history sweep failed; it runs again within the hour", { error: err });
753
+ } finally {
754
+ this.sweepingLogs = false;
755
+ }
756
+ }
564
757
  /**
565
758
  * Stop the scheduler: clear every timer, then wait for the runs executing
566
759
  * right now — for at most `timeoutMs` when given.
@@ -576,6 +769,10 @@ var CronScheduler = class {
576
769
  async stop(timeoutMs) {
577
770
  this.started = false;
578
771
  for (const [id] of this.jobs) this.stopJob(id);
772
+ if (this.claimSweepTimer) {
773
+ clearInterval(this.claimSweepTimer);
774
+ this.claimSweepTimer = void 0;
775
+ }
579
776
  if (this.inFlight.size === 0) return;
580
777
  const settled = Promise.allSettled([...this.inFlight]).then(() => true);
581
778
  if (timeoutMs === void 0) {
@@ -747,6 +944,46 @@ var CronScheduler = class {
747
944
  return this.executeJob(job, true);
748
945
  }
749
946
  /**
947
+ * Start a job now and answer as soon as the run has begun, not when it
948
+ * ends — the manual trigger's `?async=1`.
949
+ *
950
+ * The synchronous {@link triggerJob} holds its HTTP request open for the
951
+ * whole run, and a proxy in front of the server (many stop at 60 s) cuts
952
+ * a long job's answer while the run carries on. This resolves once the
953
+ * run lease is held and the handler is about to be called, so a run that
954
+ * cannot start — already executing here or on the lease holder — is still
955
+ * answered as the skip it is. The run itself is tracked like any other:
956
+ * `stop()` waits for it, and its outcome lands in the run history, where
957
+ * the entry carries `manual: true` and the `startedAt` returned here.
958
+ *
959
+ * Returns `undefined` if the job doesn't exist.
960
+ */
961
+ async startJob(id) {
962
+ const job = this.jobs.get(id);
963
+ if (!job) return void 0;
964
+ if (job.executing) {
965
+ logger.warn(`[cron] Skipping manual trigger of "${id}" — already executing`);
966
+ return {
967
+ started: false,
968
+ log: this.recordSkip(job, "already_executing", true)
969
+ };
970
+ }
971
+ let onStart;
972
+ const begun = new Promise((resolve) => {
973
+ onStart = resolve;
974
+ });
975
+ const run = this.executeJob(job, true, void 0, onStart);
976
+ const first = await Promise.race([begun.then((startedAt) => ({ startedAt })), run.then((log) => ({ log }))]);
977
+ if ("startedAt" in first) return {
978
+ started: true,
979
+ startedAt: first.startedAt
980
+ };
981
+ return {
982
+ started: false,
983
+ log: first.log
984
+ };
985
+ }
986
+ /**
750
987
  * Record a run that did not happen because the previous one had not
751
988
  * finished.
752
989
  *
@@ -899,7 +1136,7 @@ var CronScheduler = class {
899
1136
  if (!this.started) {
900
1137
  const now = /* @__PURE__ */ new Date();
901
1138
  for (const job of jobs) try {
902
- viewOf(job.id).nextRunAt = parseCronExpression(job.definition.schedule, now, job.definition.timezone);
1139
+ viewOf(job.id).nextRunAt = this.listedNextRun(job, now);
903
1140
  } catch {}
904
1141
  }
905
1142
  if (store?.fetchRunSummaries && ids.length > 0) try {
@@ -911,6 +1148,34 @@ var CronScheduler = class {
911
1148
  return views;
912
1149
  }
913
1150
  /**
1151
+ * The next slot of `job` after `now`, for a listing on a process whose
1152
+ * scheduler is not started — the `api` role, which Studio's Cron panel
1153
+ * polls every 15 seconds.
1154
+ *
1155
+ * Cached on the job. The key is the job's schedule and zone (a re-register
1156
+ * replaces the job, and with it the cache); the entry holds while
1157
+ * `computedAt <= now < slot`. That is the same answer, not an
1158
+ * approximation: the search found no matching minute between
1159
+ * `computedAt`'s next minute and `slot`, so from any later instant before
1160
+ * `slot` it lands on `slot` again. A clock that moves back, or reaching the
1161
+ * slot, recomputes. The cache holds a pure function of the schedule, so
1162
+ * there is nothing user- or role-scoped in it.
1163
+ */
1164
+ listedNextRun(job, now) {
1165
+ const { schedule, timezone } = job.definition;
1166
+ const nowMs = now.getTime();
1167
+ const cached = job.listedSlot;
1168
+ if (cached && cached.schedule === schedule && cached.timezone === timezone && cached.computedAt <= nowMs && nowMs < cached.slot.getTime()) return cached.slot;
1169
+ const slot = parseCronExpression(schedule, now, timezone);
1170
+ job.listedSlot = {
1171
+ schedule,
1172
+ timezone,
1173
+ computedAt: nowMs,
1174
+ slot
1175
+ };
1176
+ return slot;
1177
+ }
1178
+ /**
914
1179
  * Warn once at start when the process looks like it is running on a
915
1180
  * platform that freezes or evicts instances between requests, where the
916
1181
  * in-process timers this scheduler relies on never fire.
@@ -1064,10 +1329,10 @@ var CronScheduler = class {
1064
1329
  * - Persists to store (non-blocking) if available
1065
1330
  * - Always restores state even on catastrophic errors
1066
1331
  */
1067
- executeJob(job, manual, seedLog) {
1332
+ executeJob(job, manual, seedLog, onStart) {
1068
1333
  const abort = new AbortController();
1069
1334
  this.runControllers.add(abort);
1070
- const run = this.runJob(job, manual, abort, seedLog);
1335
+ const run = this.runJob(job, manual, abort, seedLog, onStart);
1071
1336
  this.track(run);
1072
1337
  return run.finally(() => {
1073
1338
  this.runControllers.delete(abort);
@@ -1081,7 +1346,7 @@ var CronScheduler = class {
1081
1346
  };
1082
1347
  work.then(release, release);
1083
1348
  }
1084
- async runJob(job, manual, abort, seedLog) {
1349
+ async runJob(job, manual, abort, seedLog, onStart) {
1085
1350
  if (job.executing) return this.recordSkip(job, "already_executing", manual);
1086
1351
  job.executing = true;
1087
1352
  const lease = await this.acquireLease(job);
@@ -1091,14 +1356,14 @@ var CronScheduler = class {
1091
1356
  return this.recordSkip(job, "already_executing", manual, lease.holder ?? "another process");
1092
1357
  }
1093
1358
  const startedAt = /* @__PURE__ */ new Date();
1094
- const capturedLogs = seedLog ? [seedLog] : [];
1359
+ const capturedLogs = createLogCapture(seedLog || void 0);
1095
1360
  const ctx = {
1096
1361
  jobId: job.id,
1097
1362
  scheduledAt: startedAt,
1098
1363
  signal: abort.signal,
1099
1364
  log: (...args) => {
1100
1365
  const line = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
1101
- capturedLogs.push(line);
1366
+ capturedLogs.add(line);
1102
1367
  logger.info(args.map((a) => typeof a === "string" ? a : stringifyForLog(a)).join(" "), { cron: job.id });
1103
1368
  },
1104
1369
  rebase: this.client
@@ -1106,6 +1371,7 @@ var CronScheduler = class {
1106
1371
  job.state = "running";
1107
1372
  job.lastRunAt = startedAt;
1108
1373
  job.totalRuns++;
1374
+ onStart?.(startedAt.toISOString());
1109
1375
  let success = true;
1110
1376
  let error;
1111
1377
  let result;
@@ -1144,7 +1410,7 @@ var CronScheduler = class {
1144
1410
  success,
1145
1411
  error,
1146
1412
  result: result !== void 0 ? result : void 0,
1147
- logs: capturedLogs,
1413
+ logs: capturedLogs.close(),
1148
1414
  manual
1149
1415
  };
1150
1416
  job.logs.push(logEntry);
@@ -1188,4 +1454,4 @@ var CronScheduler = class {
1188
1454
  //#endregion
1189
1455
  export { parseEnvBoolean as a, validateCronExpression as i, cron_scheduler_exports as n, isAlreadyExecutingSkip as r, CronScheduler as t };
1190
1456
 
1191
- //# sourceMappingURL=cron-scheduler-CYQgco86.js.map
1457
+ //# sourceMappingURL=cron-scheduler-idtLPoN-.js.map