@rebasepro/server 0.23.0 → 0.24.1-canary.g56af4a4

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 (261) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/GCSStorageController-DivE9bAx.js +402 -0
  4. package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
  5. package/dist/S3StorageController-D_vKs4kP.js +480 -0
  6. package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +97 -11
  13. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  14. package/dist/api/rest/field-access-query.d.ts +28 -8
  15. package/dist/api/rest/field-ops.d.ts +19 -7
  16. package/dist/api/rest/query-body.d.ts +10 -0
  17. package/dist/api/rest/write-validation.d.ts +9 -7
  18. package/dist/api/schema-editor-routes.d.ts +10 -1
  19. package/dist/api/sql-export-routes.d.ts +8 -0
  20. package/dist/api/types.d.ts +28 -1
  21. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
  22. package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
  23. package/dist/auth/access.d.ts +121 -0
  24. package/dist/auth/adapter-middleware.d.ts +2 -1
  25. package/dist/auth/address-ownership.d.ts +16 -1
  26. package/dist/auth/admin-roles-route.d.ts +4 -2
  27. package/dist/auth/admin-roles.d.ts +17 -20
  28. package/dist/auth/admin-user-ops.d.ts +24 -2
  29. package/dist/auth/admin-users-route.d.ts +2 -0
  30. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  31. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  32. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  33. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  34. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  35. package/dist/auth/api-keys/index.d.ts +11 -11
  36. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  37. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  38. package/dist/auth/auth-hooks.d.ts +46 -7
  39. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  40. package/dist/auth/cookie-utils.d.ts +7 -0
  41. package/dist/auth/deliverable-address.d.ts +6 -0
  42. package/dist/auth/email-change-routes.d.ts +41 -0
  43. package/dist/auth/expired-token-sweep.d.ts +71 -0
  44. package/dist/auth/impersonation.d.ts +110 -0
  45. package/dist/auth/index.d.ts +4 -2
  46. package/dist/auth/interfaces.d.ts +120 -59
  47. package/dist/auth/jwt-crypto.d.ts +7 -0
  48. package/dist/auth/jwt-keys.d.ts +4 -0
  49. package/dist/auth/jwt.d.ts +54 -3
  50. package/dist/auth/magic-link-routes.d.ts +2 -6
  51. package/dist/auth/mfa-routes.d.ts +2 -9
  52. package/dist/auth/middleware.d.ts +17 -5
  53. package/dist/auth/otp-routes.d.ts +2 -6
  54. package/dist/auth/passwordless-signup.d.ts +27 -0
  55. package/dist/auth/platform-token.d.ts +122 -0
  56. package/dist/auth/rate-limiter.d.ts +54 -1
  57. package/dist/auth/registration-policy.d.ts +17 -0
  58. package/dist/auth/routes.d.ts +45 -0
  59. package/dist/auth/scope-routes.d.ts +22 -0
  60. package/dist/auth/session-routes.d.ts +11 -6
  61. package/dist/auth/token-revocation.d.ts +50 -1
  62. package/dist/auth/user-picking.d.ts +35 -0
  63. package/dist/auth/verify-credential.d.ts +28 -0
  64. package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
  65. package/dist/auth-OPq_FeMs.js.map +1 -0
  66. package/dist/auth_adapter-CIItCTc8.js +21 -0
  67. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  68. package/dist/backend-DTAOsLQc.js.map +1 -1
  69. package/dist/backup/backup-common.d.ts +24 -3
  70. package/dist/backup/backup-routes.d.ts +46 -5
  71. package/dist/backup/backup-schedule.d.ts +33 -0
  72. package/dist/backup/backup-storage.d.ts +16 -0
  73. package/dist/backup/index.d.ts +5 -1
  74. package/dist/backup-5d7XhLX3.js +581 -0
  75. package/dist/backup-5d7XhLX3.js.map +1 -0
  76. package/dist/boot/bundle.d.ts +19 -0
  77. package/dist/boot/driver.d.ts +3 -0
  78. package/dist/boot/env.d.ts +56 -4
  79. package/dist/boot/http-server.d.ts +22 -0
  80. package/dist/boot/options.d.ts +16 -1
  81. package/dist/boot/runtime-cors.d.ts +17 -0
  82. package/dist/boot/security-headers.d.ts +26 -0
  83. package/dist/boot/static-routing.d.ts +65 -0
  84. package/dist/collection_patch-BRu-BvDv.js +472 -0
  85. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  86. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
  87. package/dist/contract-routes-02lASyJI.js.map +1 -0
  88. package/dist/cron/cron-routes.d.ts +1 -1
  89. package/dist/cron/cron-scheduler.d.ts +94 -23
  90. package/dist/cron/cron-store.d.ts +31 -4
  91. package/dist/cron/define-cron.d.ts +6 -2
  92. package/dist/cron/index.d.ts +1 -1
  93. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
  94. package/dist/cron-loader-D7OmleAb.js.map +1 -0
  95. package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
  96. package/dist/cron-routes-CBfiaqEL.js.map +1 -0
  97. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
  98. package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
  99. package/dist/cron-store-B2Ih0gCN.js +540 -0
  100. package/dist/cron-store-B2Ih0gCN.js.map +1 -0
  101. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  102. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  103. package/dist/email/index.d.ts +2 -2
  104. package/dist/email/smtp-email-service.d.ts +23 -0
  105. package/dist/email/templates.d.ts +22 -0
  106. package/dist/email/types.d.ts +44 -0
  107. package/dist/env.d.ts +1 -39
  108. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  109. package/dist/errors-D6_y86c5.js.map +1 -0
  110. package/dist/filter-dialect-REl-TjZr.js +1628 -0
  111. package/dist/filter-dialect-REl-TjZr.js.map +1 -0
  112. package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
  113. package/dist/function-loader-BDKKiBmy.js.map +1 -0
  114. package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
  115. package/dist/function-routes-DWB7BBsi.js.map +1 -0
  116. package/dist/functions/context.d.ts +26 -6
  117. package/dist/functions/define-function.d.ts +7 -4
  118. package/dist/functions/guards.d.ts +22 -5
  119. package/dist/functions/index.d.ts +2 -2
  120. package/dist/functions/index.js +114 -37
  121. package/dist/functions/index.js.map +1 -1
  122. package/dist/functions/request-timeout.d.ts +7 -5
  123. package/dist/history/history-routes.d.ts +9 -0
  124. package/dist/history/retention-sweep.d.ts +81 -0
  125. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
  126. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
  127. package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
  128. package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
  129. package/dist/index.d.ts +14 -7
  130. package/dist/index.es.js +23363 -17552
  131. package/dist/index.es.js.map +1 -1
  132. package/dist/init/health.d.ts +17 -2
  133. package/dist/init/shutdown.d.ts +20 -0
  134. package/dist/init.d.ts +70 -6
  135. package/dist/internal-tables-L2t39RdT.js +136 -0
  136. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  137. package/dist/jobs/job-queue.d.ts +8 -0
  138. package/dist/jobs/job-store.d.ts +24 -0
  139. package/dist/jobs/types.d.ts +25 -4
  140. package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
  141. package/dist/jobs-BpD4Q0qR.js.map +1 -0
  142. package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
  143. package/dist/jwt-Bqcu7cGt.js.map +1 -0
  144. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  145. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  146. package/dist/load-failure-UvzZgGXT.js +48 -0
  147. package/dist/load-failure-UvzZgGXT.js.map +1 -0
  148. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  149. package/dist/logger-D-S-hO5e.js.map +1 -0
  150. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  151. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  152. package/dist/mcp/consent-page.d.ts +1 -1
  153. package/dist/mcp/mcp-routes.d.ts +7 -0
  154. package/dist/mcp/mcp-tools.d.ts +23 -9
  155. package/dist/mcp/oauth-metadata.d.ts +21 -16
  156. package/dist/mcp/oauth-routes.d.ts +7 -1
  157. package/dist/metrics/index.d.ts +18 -0
  158. package/dist/metrics/pool-wait.d.ts +50 -0
  159. package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
  160. package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
  161. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
  162. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
  163. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
  164. package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
  165. package/dist/policy-PdzX-Opd.js +121 -0
  166. package/dist/policy-PdzX-Opd.js.map +1 -0
  167. package/dist/project_manifest-R6y_KHms.js +59 -0
  168. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  169. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  170. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  171. package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
  172. package/dist/query-parser-DK9eMkb9.js.map +1 -0
  173. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
  174. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  175. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  176. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  177. package/dist/schema-edit/apply-schema-change.d.ts +73 -3
  178. package/dist/schema-edit/local-source.d.ts +29 -0
  179. package/dist/schema-edit/project-root.d.ts +3 -2
  180. package/dist/schema-edit/remote-source.d.ts +20 -5
  181. package/dist/schema-edit/source-editor.d.ts +28 -0
  182. package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
  183. package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
  184. package/dist/serve-spa.d.ts +58 -0
  185. package/dist/services/outbound-post.d.ts +50 -0
  186. package/dist/services/outbound-url-guard.d.ts +34 -5
  187. package/dist/services/routed-realtime-service.d.ts +16 -0
  188. package/dist/services/webhook-service.d.ts +66 -14
  189. package/dist/soft-delete-params-47yBGs3_.js +668 -0
  190. package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
  191. package/dist/sql-export-routes-BNNuGGLP.js +240 -0
  192. package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
  193. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  194. package/dist/src-CatHFUym.js.map +1 -0
  195. package/dist/storage/GCSStorageController.d.ts +48 -2
  196. package/dist/storage/LocalStorageController.d.ts +69 -2
  197. package/dist/storage/S3StorageController.d.ts +39 -3
  198. package/dist/storage/image-transform.d.ts +5 -0
  199. package/dist/storage/index.d.ts +2 -2
  200. package/dist/storage/maintenance-routes.d.ts +23 -0
  201. package/dist/storage/maintenance.d.ts +126 -0
  202. package/dist/storage/property-limits.d.ts +41 -6
  203. package/dist/storage/rendition-cache.d.ts +9 -11
  204. package/dist/storage/request-keys.d.ts +15 -0
  205. package/dist/storage/request-principals.d.ts +30 -0
  206. package/dist/storage/requested-object.d.ts +74 -0
  207. package/dist/storage/routes.d.ts +52 -14
  208. package/dist/storage/source-variables.d.ts +10 -0
  209. package/dist/storage/streams.d.ts +42 -0
  210. package/dist/storage/tus-handler.d.ts +76 -7
  211. package/dist/storage/types.d.ts +101 -7
  212. package/dist/storage/upload-validation.d.ts +13 -0
  213. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  214. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  215. package/dist/strings.es-BeZWAodn.js +256 -0
  216. package/dist/strings.es-BeZWAodn.js.map +1 -0
  217. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  218. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  219. package/dist/utils/byte-size.d.ts +10 -0
  220. package/dist/utils/compression.d.ts +6 -0
  221. package/dist/utils/load-failure.d.ts +14 -0
  222. package/dist/utils/logger.d.ts +12 -0
  223. package/package.json +5 -5
  224. package/dist/GCSStorageController-CjrA4PMo.js +0 -242
  225. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  226. package/dist/S3StorageController-B6pKDNVj.js +0 -264
  227. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  228. package/dist/admin-roles-vYdp_Pil.js +0 -36
  229. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  230. package/dist/admin_block-DxKLmdiv.js +0 -206
  231. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  232. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  233. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  234. package/dist/auth-B-GIMpDG.js.map +0 -1
  235. package/dist/backup-D7YR94N3.js +0 -253
  236. package/dist/backup-D7YR94N3.js.map +0 -1
  237. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  238. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  239. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  240. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  241. package/dist/cron-store-TcoGz-xS.js +0 -305
  242. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  243. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  244. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  245. package/dist/errors-DWsX4yTd.js.map +0 -1
  246. package/dist/function-loader-xnbDAPfa.js.map +0 -1
  247. package/dist/function-routes-Chet4-lB.js.map +0 -1
  248. package/dist/jobs-CazMYhyy.js.map +0 -1
  249. package/dist/jwt-DnQHNFCl.js.map +0 -1
  250. package/dist/logger-DO2PZc4i.js.map +0 -1
  251. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  252. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  253. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  254. package/dist/request-timeout-C_4C2BeR.js.map +0 -1
  255. package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
  256. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  257. package/dist/src-pmvW7BFx.js +0 -6688
  258. package/dist/src-pmvW7BFx.js.map +0 -1
  259. package/dist/src-vkcwKXbT.js.map +0 -1
  260. package/dist/types-BfKcm9do.js +0 -39
  261. package/dist/types-BfKcm9do.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import type { CronJobStatus, CronJobLogEntry } from "@rebasepro/types";
1
+ import type { CronJobDefinition, CronJobStatus, CronJobLogEntry, RejectedCronJob } from "@rebasepro/types";
2
2
  import type { RebaseServerClient } from "@rebasepro/types";
3
3
  import type { LoadedCronJob } from "./cron-loader.js";
4
4
  import type { CronStore } from "./cron-store.js";
@@ -30,9 +30,9 @@ export declare function parseCronExpression(expression: string, after: Date, tim
30
30
  * The latest slot matching `expression` within the inclusive window
31
31
  * `[from, to]`, or `undefined` when the expression has no slot in it.
32
32
  *
33
- * Walks backwards a minute at a time from `to`, so the first hit is already
34
- * the answer — in the common case (a job that ran normally moments ago) that
35
- * is a handful of iterations, not a scan of the whole window.
33
+ * Walks backwards from `to`, so the first hit is already the answer — in the
34
+ * common case (a job that ran normally moments ago) that is a handful of
35
+ * steps, not a scan of the whole window.
36
36
  *
37
37
  * `to`'s own minute is included: an instance booting at 06:00:30 has *not* run
38
38
  * the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —
@@ -43,6 +43,29 @@ export declare function parseCronExpression(expression: string, after: Date, tim
43
43
  * ISO string down either path. The claim key depends on that.
44
44
  */
45
45
  export declare function findMostRecentSlot(expression: string, from: Date, to: Date, timezone?: string): Date | undefined;
46
+ /**
47
+ * What one run keeps of its `ctx.log` lines: up to 500 lines or 64 KB,
48
+ * whichever comes first. The lines past that are counted, and one line at the
49
+ * end says how many there were; every line still reaches the server log.
50
+ *
51
+ * Uncapped, a job logging one line per item kept them all — in the 50-run ring
52
+ * per job, in a `cron_logs` row, and in every history read: a per-minute sync
53
+ * of 1,000 items held 4 MB per job in memory and wrote 115 MB a day.
54
+ */
55
+ export declare const MAX_CAPTURED_LOG_LINES = 500;
56
+ export declare const MAX_CAPTURED_LOG_BYTES: number;
57
+ /**
58
+ * What {@link CronScheduler.startJob} answers: the run began at `startedAt`
59
+ * (its history entry will carry the same timestamp), or it did not start and
60
+ * `log` is the skip that says why.
61
+ */
62
+ export type StartedCronRun = {
63
+ started: true;
64
+ startedAt: string;
65
+ } | {
66
+ started: false;
67
+ log: CronJobLogEntry;
68
+ };
46
69
  /**
47
70
  * Whether a log entry records a run that did not happen because one was
48
71
  * already executing — on this process, or on another that holds the job's run
@@ -50,19 +73,6 @@ export declare function findMostRecentSlot(expression: string, from: Date, to: D
50
73
  * panel reads.
51
74
  */
52
75
  export declare function isAlreadyExecutingSkip(entry: CronJobLogEntry): boolean;
53
- /**
54
- * A job the scheduler refused, and why.
55
- *
56
- * It is not a `CronJobStatus`: it has no state, no next run and no counters,
57
- * because it was never registered. Reporting it as a job with `state: "error"`
58
- * would be a lie in the other direction — nothing is going to run it.
59
- */
60
- export interface RejectedCronJob {
61
- id: string;
62
- name: string;
63
- schedule: string;
64
- reason: string;
65
- }
66
76
  export declare class CronScheduler {
67
77
  private jobs;
68
78
  private rejected;
@@ -82,6 +92,10 @@ export declare class CronScheduler {
82
92
  * ever clear the lease that run took.
83
93
  */
84
94
  private readonly instanceLabel;
95
+ /** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */
96
+ private claimSweepTimer?;
97
+ private sweepingClaims;
98
+ private sweepingLogs;
85
99
  /**
86
100
  * Set the server singleton to make it available to cron job handlers.
87
101
  *
@@ -111,6 +125,14 @@ export declare class CronScheduler {
111
125
  * Start the scheduler — begins ticking all enabled jobs.
112
126
  */
113
127
  start(): void;
128
+ /** Delete slot claims past retention. Failures are logged; the next hour tries again. */
129
+ private sweepClaims;
130
+ /**
131
+ * Delete run history past retention, and count the runs not counted yet.
132
+ * Failures are logged; the next hour tries again, from where this one
133
+ * stopped — each batch it finished is committed.
134
+ */
135
+ private sweepLogs;
114
136
  /**
115
137
  * Stop the scheduler: clear every timer, then wait for the runs executing
116
138
  * right now — for at most `timeoutMs` when given.
@@ -128,6 +150,8 @@ export declare class CronScheduler {
128
150
  * List all registered jobs with their current status.
129
151
  */
130
152
  listJobs(): CronJobStatus[];
153
+ /** Keep a refused job, so the admin surface can say why it never runs. */
154
+ private refuse;
131
155
  /**
132
156
  * Jobs that loaded but whose schedule the scheduler refused.
133
157
  *
@@ -136,6 +160,17 @@ export declare class CronScheduler {
136
160
  * cron will never fire" look identical from the admin panel.
137
161
  */
138
162
  listRejectedJobs(): RejectedCronJob[];
163
+ /**
164
+ * {@link listRejectedJobs}, those whose definition matches — how a surface
165
+ * finds a refused job by what it is, as {@link jobIdsWhere} does for the
166
+ * registered ones.
167
+ */
168
+ rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[];
169
+ /**
170
+ * The ids of the registered jobs whose definition matches — how a surface
171
+ * finds a job by what it is rather than by the file it happens to live in.
172
+ */
173
+ jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[];
139
174
  /**
140
175
  * Get a single job status by ID.
141
176
  */
@@ -144,11 +179,11 @@ export declare class CronScheduler {
144
179
  * {@link listJobs}, as the fleet sees it rather than this process alone.
145
180
  *
146
181
  * Reads the store: every job's enabled state — a pause another replica
147
- * made — and whether another process holds its run lease. On a process
148
- * whose scheduler is not started (the `api` role) it also reads each job's
149
- * run count and last run from `cron_logs`, since that process runs nothing
150
- * and its own counters would say 0 runs for a job the worker has run a
151
- * thousand times.
182
+ * made — whether another process holds its run lease, and each job's run
183
+ * count and last run from `cron_logs`. The counters in this process's
184
+ * memory count only the runs it made: the `api` role runs nothing and
185
+ * would say 0 runs for a job the worker has run a thousand times, and one
186
+ * of two scheduling replicas never sees the slots the other one claimed.
152
187
  *
153
188
  * A store that cannot answer leaves this process's own view, with a
154
189
  * warning: a listing is not worth failing.
@@ -162,7 +197,12 @@ export declare class CronScheduler {
162
197
  getJobLogs(id: string, limit?: number): CronJobLogEntry[];
163
198
  /**
164
199
  * Get log entries for a job from the database (if store is available).
165
- * Falls back to in-memory logs if no store is configured.
200
+ * Falls back to in-memory logs if no store is configured, or if the store
201
+ * has none — a run whose row is still being written.
202
+ *
203
+ * Throws when the store cannot read the history. This process's memory is
204
+ * not a stand-in for it: it is empty on the `api` role and after a
205
+ * restart, and holds one replica's runs at most.
166
206
  */
167
207
  getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]>;
168
208
  /**
@@ -199,6 +239,22 @@ export declare class CronScheduler {
199
239
  * recognises it.
200
240
  */
201
241
  triggerJob(id: string): Promise<CronJobLogEntry | undefined>;
242
+ /**
243
+ * Start a job now and answer as soon as the run has begun, not when it
244
+ * ends — the manual trigger's `?async=1`.
245
+ *
246
+ * The synchronous {@link triggerJob} holds its HTTP request open for the
247
+ * whole run, and a proxy in front of the server (many stop at 60 s) cuts
248
+ * a long job's answer while the run carries on. This resolves once the
249
+ * run lease is held and the handler is about to be called, so a run that
250
+ * cannot start — already executing here or on the lease holder — is still
251
+ * answered as the skip it is. The run itself is tracked like any other:
252
+ * `stop()` waits for it, and its outcome lands in the run history, where
253
+ * the entry carries `manual: true` and the `startedAt` returned here.
254
+ *
255
+ * Returns `undefined` if the job doesn't exist.
256
+ */
257
+ startJob(id: string): Promise<StartedCronRun | undefined>;
202
258
  /**
203
259
  * Record a run that did not happen because the previous one had not
204
260
  * finished.
@@ -262,6 +318,21 @@ export declare class CronScheduler {
262
318
  * rest as a view for {@link toStatus}. Never throws.
263
319
  */
264
320
  private readFleet;
321
+ /**
322
+ * The next slot of `job` after `now`, for a listing on a process whose
323
+ * scheduler is not started — the `api` role, which Studio's Cron panel
324
+ * polls every 15 seconds.
325
+ *
326
+ * Cached on the job. The key is the job's schedule and zone (a re-register
327
+ * replaces the job, and with it the cache); the entry holds while
328
+ * `computedAt <= now < slot`. That is the same answer, not an
329
+ * approximation: the search found no matching minute between
330
+ * `computedAt`'s next minute and `slot`, so from any later instant before
331
+ * `slot` it lands on `slot` again. A clock that moves back, or reaching the
332
+ * slot, recomputes. The cache holds a pure function of the schedule, so
333
+ * there is nothing user- or role-scoped in it.
334
+ */
335
+ private listedNextRun;
265
336
  /**
266
337
  * Warn once at start when the process looks like it is running on a
267
338
  * platform that freezes or evicts instances between requests, where the
@@ -17,6 +17,10 @@ export interface CronStore {
17
17
  insertLog(entry: CronJobLogEntry): Promise<void>;
18
18
  /**
19
19
  * Fetch the most recent logs for a job.
20
+ *
21
+ * Throws when the history cannot be read. An empty array is a claim — the
22
+ * job has no recorded runs — and a caller shows it as "never ran".
23
+ *
20
24
  * @param jobId The job identifier
21
25
  * @param limit Max entries to return (default 50)
22
26
  * @returns Logs sorted newest-first
@@ -24,7 +28,8 @@ export interface CronStore {
24
28
  fetchLogs(jobId: string, limit?: number): Promise<CronJobLogEntry[]>;
25
29
  /**
26
30
  * Fetch aggregate stats for all jobs (totalRuns, totalFailures, lastRunAt).
27
- * Used to seed in-memory counters on startup.
31
+ * Used to seed in-memory counters on startup. The SQL store reads them from
32
+ * the all-time counters on `cron_job_state`, not from the history.
28
33
  */
29
34
  fetchJobStats(): Promise<Map<string, {
30
35
  totalRuns: number;
@@ -82,11 +87,33 @@ export interface CronStore {
82
87
  /** Release a lease taken with {@link tryAcquireRunLease} — only if `holder` still holds it. */
83
88
  releaseRunLease?(jobId: string, holder: string): Promise<void>;
84
89
  /**
85
- * Run count, failure count and last run of each job, read from
86
- * `cron_logs` — for a process whose scheduler is not started (the `api`
87
- * role), which runs nothing and so counts nothing of its own.
90
+ * Run count, failure count and last run of each job — every process's
91
+ * runs, where the scheduler's own counters hold only the ones it made. The
92
+ * SQL store keeps them as all-time counters on `cron_job_state`, so they
93
+ * outlive the history's retention.
88
94
  */
89
95
  fetchRunSummaries?(jobIds: readonly string[]): Promise<Map<string, CronJobRunSummary>>;
96
+ /**
97
+ * Delete slot claims past retention — all but each job's most recent one —
98
+ * in batches. Resolves with how many went. The scheduler runs it on a
99
+ * timer while it is started; a store without it keeps every claim.
100
+ */
101
+ sweepClaims?(): Promise<number>;
102
+ /**
103
+ * Delete run history past retention, and count into the job counters any
104
+ * run that is not counted yet — a history written before the counters
105
+ * existed, or by an older server during a rolling deploy. Both in batches.
106
+ * The scheduler runs it with {@link sweepClaims}; a store without it keeps
107
+ * every run.
108
+ */
109
+ sweepLogs?(): Promise<CronLogSweep>;
110
+ }
111
+ /** What one {@link CronStore.sweepLogs} did. */
112
+ export interface CronLogSweep {
113
+ /** History rows deleted past retention. */
114
+ deleted: number;
115
+ /** Runs counted into the job counters that were not counted before. */
116
+ counted: number;
90
117
  }
91
118
  /** One job's row in `rebase.cron_job_state`, as the scheduler reads it. */
92
119
  export interface CronJobPersistedState {
@@ -1,4 +1,4 @@
1
- import type { CronJobDefinition } from "@rebasepro/types";
1
+ import type { CronJobDefinition, RegisteredDatabase } from "@rebasepro/types";
2
2
  /**
3
3
  * Typed authoring helper for a cron job file. Identity at runtime —
4
4
  * a plain default-exported {@link CronJobDefinition} works identically;
@@ -38,5 +38,9 @@ import type { CronJobDefinition } from "@rebasepro/types";
38
38
  * },
39
39
  * });
40
40
  * ```
41
+ *
42
+ * `ctx.rebase` is typed from the `Database` the project registered on
43
+ * `Register` (see the typed server data guide), and untyped when it registered
44
+ * none. `defineCron<Database>({…})` types one job without the registration.
41
45
  */
42
- export declare function defineCron(definition: CronJobDefinition): CronJobDefinition;
46
+ export declare function defineCron<DB = RegisteredDatabase>(definition: CronJobDefinition<DB>): CronJobDefinition<DB>;
@@ -5,4 +5,4 @@ export { CronScheduler, validateCronExpression } from "./cron-scheduler.js";
5
5
  export { createCronRoutes } from "./cron-routes.js";
6
6
  export { createCronStore } from "./cron-store.js";
7
7
  export { detectFreezableRuntime, buildScaleToZeroWarning, CRON_ALWAYS_ON_ENV } from "./scale-to-zero.js";
8
- export type { CronStore, CronJobPersistedState, CronRunLease, CronJobRunSummary } from "./cron-store.js";
8
+ export type { CronStore, CronJobPersistedState, CronRunLease, CronJobRunSummary, CronLogSweep } from "./cron-store.js";
@@ -3,9 +3,9 @@ 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 { t as declareCron } from "./src-vkcwKXbT.js";
7
- import { r as logger } from "./logger-DO2PZc4i.js";
8
- import { t as nativeDynamicImport } from "./dynamic-import-X40pTZUQ.js";
6
+ import { x as declareCron } from "./src-CatHFUym.js";
7
+ import { r as logger } from "./logger-D-S-hO5e.js";
8
+ import { n as describeLoadFailure, r as nativeDynamicImport, t as SYNTAX_ERROR_ADVICE } from "./load-failure-UvzZgGXT.js";
9
9
  import * as fs$1 from "fs";
10
10
  import * as path$1 from "path";
11
11
  import { pathToFileURL } from "url";
@@ -75,7 +75,7 @@ async function loadCronJobsWithDiagnostics(directory, importModule = nativeDynam
75
75
  schedule: def.schedule,
76
76
  name: def.name || id,
77
77
  enabled: def.enabled !== false,
78
- timeoutSeconds: def.timeoutSeconds || 300,
78
+ timeoutSeconds: def.timeoutSeconds ?? 300,
79
79
  handler: def.handler
80
80
  };
81
81
  declareCron(id, cronResourceOptions(def));
@@ -86,11 +86,15 @@ async function loadCronJobsWithDiagnostics(directory, importModule = nativeDynam
86
86
  logger.info(`⏰ Loaded cron job: ${id} (${definition.schedule}${definition.timezone ? ` ${definition.timezone}` : " in the process's local time"})`);
87
87
  } catch (err) {
88
88
  const message = err instanceof Error ? err.message : String(err);
89
- problems.push(`${file} (threw: ${message})`);
89
+ problems.push(describeLoadFailure(file, err));
90
90
  logger.debug(`[cron] Failed to load ${file}: ${message}`);
91
91
  }
92
92
  }
93
- if (problems.length > 0) logger.warn(`[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\n` + problems.map((p) => ` - ${p}`).join("\n") + "\n Fix these or author them with `defineCron(...)` for a typed, compile-checked contract.");
93
+ if (problems.length > 0) {
94
+ const syntax = problems.some((p) => p.includes("(syntax error"));
95
+ const other = problems.some((p) => !p.includes("(syntax error"));
96
+ logger.warn(`[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\n` + problems.map((p) => ` - ${p}`).join("\n") + "\n" + [...syntax ? [SYNTAX_ERROR_ADVICE] : [], ...other ? [" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract."] : []].join("\n"));
97
+ }
94
98
  return {
95
99
  jobs,
96
100
  problems
@@ -115,4 +119,4 @@ function cronResourceOptions(definition) {
115
119
  //#endregion
116
120
  export { loadCronJobsFromDirectory as n, loadCronJobsWithDiagnostics as r, cron_loader_exports as t };
117
121
 
118
- //# sourceMappingURL=cron-loader-DfTj2Hbi.js.map
122
+ //# sourceMappingURL=cron-loader-D7OmleAb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-loader-D7OmleAb.js","names":[],"sources":["../src/cron/cron-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { declareCron, type CronJobDefinition, type CronResourceOptions } from \"@rebasepro/types\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\nimport { describeLoadFailure, SYNTAX_ERROR_ADVICE } from \"../utils/load-failure.js\";\n\nexport interface LoadedCronJob {\n /** Job ID derived from filename (e.g. \"cleanup-sessions\"). */\n id: string;\n /** The full definition. */\n definition: CronJobDefinition;\n}\n\n/** {@link loadCronJobsWithDiagnostics}' answer: what loaded, and what did not. */\nexport interface LoadedCronJobs {\n jobs: LoadedCronJob[];\n /** One entry per file that will NOT be scheduled, with the reason. */\n problems: string[];\n}\n\n/**\n * Auto-discover cron job files from a directory.\n *\n * Each file should default-export a `CronJobDefinition`.\n * The filename (without extension) becomes the job ID:\n * `crons/cleanup-sessions.ts` → id = \"cleanup-sessions\"\n *\n * Follows the same discovery pattern as `loadFunctionsFromDirectory` — see\n * {@link loadCronJobsWithDiagnostics} for what happens to the files it cannot\n * load.\n */\nexport async function loadCronJobsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJob[]> {\n return (await loadCronJobsWithDiagnostics(directory, importModule)).jobs;\n}\n\n/**\n * {@link loadCronJobsFromDirectory}, plus the list of files that were skipped.\n *\n * A job that does not load does not run, and until now the only trace was one\n * `warn` per file in the boot log — next to nothing at 3am when the report that\n * should have been mailed was not. A cron file is written once and then trusted\n * for months, so \"it silently never ran\" is the failure mode this surface has,\n * and a log line is not a surface.\n *\n * Never throws, for the same reason `loadFunctionsWithDiagnostics` does not: one\n * malformed file must not take the server down, and the other jobs in the\n * directory are still worth scheduling. The difference is that the skips are now\n * counted, summarised in one place, and answered by `GET /api/cron` — so the\n * question \"why did nothing run?\" has an answer that does not require log\n * access.\n */\nexport async function loadCronJobsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJobs> {\n const jobs: LoadedCronJob[] = [];\n // Aggregated so a broken job surfaces as one loud summary rather than a\n // warning buried per-file among the boot noise.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { jobs, problems };\n }\n\n const files = fs.readdirSync(directory);\n for (const file of files) {\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported: unknown = mod.default;\n\n if (!exported || typeof exported !== \"object\") {\n problems.push(`${file} (no default export)`);\n logger.warn(`[cron] ${file}: no valid default export. Skipping.`);\n continue;\n }\n\n const def = exported as Record<string, unknown>;\n if (typeof def.schedule !== \"string\" || typeof def.handler !== \"function\") {\n problems.push(`${file} (default export has no 'schedule' or no 'handler')`);\n logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);\n continue;\n }\n\n const id = path.basename(file, path.extname(file));\n // Spread first, then normalise. Rebuilding this object field by\n // field dropped `catchUpWindowSeconds` — and since this loader\n // is the only production caller of `registerJobs`, catch-up\n // never ran for any job authored the documented way: 64 lines\n // of docblock, a docs section, a unit suite and a Postgres e2e\n // all describing a feature that was switched off in the one\n // path that matters. Every existing test built `LoadedCronJob`\n // literals directly, so none of them went through here.\n //\n // The shape is the bug, not the missing line: a field added to\n // `CronJobDefinition` tomorrow would be dropped the same way.\n const definition: CronJobDefinition = {\n ...(def as Partial<CronJobDefinition>),\n schedule: def.schedule as string,\n name: (def.name as string) || id,\n enabled: def.enabled !== false,\n // `??`, not `||`: zero and NaN are values the author\n // wrote, and the scheduler refuses both by name (they end\n // up under `rejected`). `|| 300` swallowed them, so a job\n // written with `0` meaning \"no limit\" ran with five\n // minutes and was listed as healthy. `Infinity` is \"none\".\n timeoutSeconds: (def.timeoutSeconds as number | undefined) ?? 300,\n handler: def.handler as CronJobDefinition[\"handler\"]\n };\n\n // The declaration, keyed by the same id the scheduler, the\n // routes and the Studio use — the filename. `defineCron` cannot\n // do this: it never sees the filename, and `name` is a label.\n // The derive step runs this same loader, so the graph a host\n // reads and the schedule this process runs come from one place.\n // From the raw export, not the normalised definition: the\n // defaults filled in above (`enabled: true`, `timeoutSeconds:\n // 300`) are this process's, and a graph that records them for\n // every cron would say something the author never wrote.\n declareCron(id, cronResourceOptions(def as unknown as CronJobDefinition));\n\n jobs.push({ id,\ndefinition });\n logger.info(\n `⏰ Loaded cron job: ${id} (${definition.schedule}` +\n `${definition.timezone ? ` ${definition.timezone}` : \" in the process's local time\"})`\n );\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n problems.push(describeLoadFailure(file, err));\n // At `debug`: the aggregate warning below lists every problem\n // by file, so this line was the same text a second time — and\n // when the throw is a Zod error, that text is ten lines of\n // serialised issues. The reader met it twice before reaching\n // the sentence that says what to do.\n logger.debug(`[cron] Failed to load ${file}: ${message}`);\n }\n }\n }\n\n if (problems.length > 0) {\n // Advice keyed to what went wrong, as the functions loader does:\n // `defineCron` is the remedy for a wrong export shape, not for a typo.\n const syntax = problems.some(p => p.includes(\"(syntax error\"));\n const other = problems.some(p => !p.includes(\"(syntax error\"));\n logger.warn(\n `[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n [\n ...(syntax ? [SYNTAX_ERROR_ADVICE] : []),\n ...(other ? [\" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract.\"] : [])\n ].join(\"\\n\")\n );\n }\n\n return { jobs, problems };\n}\n\n/**\n * The declaration a definition makes: every field a host can read without the\n * handler. Spelled once here so the graph and the scheduler cannot disagree\n * about what a cron is.\n */\nexport function cronResourceOptions(definition: CronJobDefinition): CronResourceOptions {\n return {\n schedule: definition.schedule,\n ...(definition.timezone !== undefined ? { timezone: definition.timezone } : {}),\n ...(definition.description !== undefined ? { description: definition.description } : {}),\n ...(definition.enabled !== undefined ? { enabled: definition.enabled } : {}),\n ...(definition.timeoutSeconds !== undefined ? { timeoutSeconds: definition.timeoutSeconds } : {}),\n ...(definition.catchUpWindowSeconds !== undefined ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {}),\n ...(definition.name ? { label: definition.name } : {})\n };\n}\n"],"mappings":"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"}
@@ -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-B0pLfAix.js";
7
- import { r as logger } from "./logger-DO2PZc4i.js";
8
- import { r as errorHandler, t as ApiError } from "./errors-DWsX4yTd.js";
9
- import { a as resolveListLimitParam } from "./query-parser-DGRVFNM3.js";
6
+ import { r as isAlreadyExecutingSkip } from "./cron-scheduler-C2vbETMo.js";
7
+ import { a as resolveListLimitParam } from "./query-parser-DK9eMkb9.js";
8
+ import { r as logger } from "./logger-D-S-hO5e.js";
9
+ import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.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
  *
@@ -44,7 +56,7 @@ function createCronRoutes(scheduler, skipped = 0) {
44
56
  ...total > 0 && {
45
57
  skipped: total,
46
58
  ...rejected.length > 0 && { rejected },
47
- note: [skipped > 0 ? `${skipped} cron file(s) failed to load` : void 0, rejected.length > 0 ? `${rejected.length} job(s) have an invalid schedule` : void 0].filter(Boolean).join(" and ") + " — NOT scheduled. " + (rejected.length > 0 ? "See `rejected` for the reason; " : "") + "the server log has the rest."
59
+ note: [skipped > 0 ? `${skipped} cron file(s) failed to load` : void 0, rejected.length > 0 ? `${rejected.length} job(s) were refused` : void 0].filter(Boolean).join(" and ") + " — NOT scheduled. " + (rejected.length > 0 ? "See `rejected` for the reason; " : "") + "the server log has the rest."
48
60
  }
49
61
  });
50
62
  });
@@ -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({
@@ -68,7 +91,10 @@ function createCronRoutes(scheduler, skipped = 0) {
68
91
  const id = c.req.param("id");
69
92
  const limit = resolveListLimitParam(c.req.query("limit") ?? null, { defaultLimit: 50 });
70
93
  if (!scheduler.getJob(id)) throw ApiError.notFound(`Cron job "${id}" not found`);
71
- const logs = await scheduler.getJobLogsFromDb(id, limit);
94
+ const logs = await scheduler.getJobLogsFromDb(id, limit).catch((err) => {
95
+ logger.error(`[cron] Could not read the run history of "${id}"`, { error: err });
96
+ throw ApiError.serviceUnavailable(`The run history of cron job "${id}" could not be read. The server log has the reason.`);
97
+ });
72
98
  return c.json({ logs });
73
99
  });
74
100
  router.put("/:id", async (c) => {
@@ -90,4 +116,4 @@ function createCronRoutes(scheduler, skipped = 0) {
90
116
  //#endregion
91
117
  export { cron_routes_exports as n, createCronRoutes as t };
92
118
 
93
- //# sourceMappingURL=cron-routes-eE8nif_b.js.map
119
+ //# sourceMappingURL=cron-routes-CBfiaqEL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-routes-CBfiaqEL.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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"}