@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,11 +3,11 @@ 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 { N as REBASE_USER_ROLE, P as revokeInternalTableSql } from "./src-I3aG1PcY.js";
7
- import "./src-CatHFUym.js";
6
+ import "./src-D5Y3N-S4.js";
8
7
  import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
9
- import { n as sqlRows, t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
10
8
  import { r as logger } from "./logger-D-S-hO5e.js";
9
+ import { n as revokeInternalTableSql, t as REBASE_USER_ROLE } from "./internal-tables-L2t39RdT.js";
10
+ import { n as sqlRows, t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
11
11
  import { n as createDdlBootstrapper, r as hasInCauseChain } from "./ddl-bootstrap-BaqMSa4Y.js";
12
12
  import { randomUUID } from "crypto";
13
13
  //#region src/db/ambient-transaction.ts
@@ -30,6 +30,25 @@ var DEFAULT_CONCURRENCY = 5;
30
30
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
31
31
  var DEFAULT_VISIBILITY_TIMEOUT_MS = 5 * 6e4;
32
32
  var DEFAULT_MAX_ATTEMPTS = 3;
33
+ /**
34
+ * How long one attempt may run when `jobs.timeoutMs` is left out. Without a
35
+ * bound, a handler awaiting a socket that never answers held its worker slot
36
+ * and its claim for the life of the process, with `/health` green. Fifteen
37
+ * minutes is far past any job that is merely slow, and a job meant to run
38
+ * longer says so with its own `timeoutMs` — or `0`, for no limit.
39
+ */
40
+ var DEFAULT_JOB_TIMEOUT_MS = 15 * 6e4;
41
+ /**
42
+ * The longest an idle worker goes without looking for work.
43
+ *
44
+ * An idle worker used to claim every 2 seconds, forever: about 45,000 round
45
+ * trips a day per pod, each with a pool checkout, and a database that could
46
+ * never suspend. Now it is woken instead — by `NOTIFY` when another process
47
+ * enqueues, by its own enqueues, retries and delayed jobs — and this is the
48
+ * safety poll behind that. Where the database cannot notify it, it backs off
49
+ * from `pollIntervalMs` to this, doubling after each look that found nothing.
50
+ */
51
+ var IDLE_POLL_MAX_MS = 3e4;
33
52
  /** 1s, 5s, 25s, 125s … capped at an hour. */
34
53
  function defaultBackoff(attempt) {
35
54
  return Math.min(1e3 * Math.pow(5, Math.max(0, attempt - 1)), 60 * 6e4);
@@ -43,6 +62,12 @@ function defaultBackoff(attempt) {
43
62
  */
44
63
  var REAP_INTERVAL_FACTOR = .25;
45
64
  /**
65
+ * How often a worker deletes finished jobs past retention. Hourly keeps the
66
+ * table near its retention size on a pod that is never redeployed; each sweep
67
+ * reads only the finished rows, through `idx_jobs_finished`.
68
+ */
69
+ var RETENTION_SWEEP_INTERVAL_MS = 60 * 6e4;
70
+ /**
46
71
  * Thrown by a handler for a failure no retry can change — a destination that
47
72
  * is refused, a receiver that redirects. The job is dead-lettered on this
48
73
  * attempt, with the message as its `last_error`, instead of spending its
@@ -61,11 +86,31 @@ function createJobQueue(store, options = {}) {
61
86
  const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;
62
87
  const defaultMaxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
63
88
  const backoff = options.backoff ?? defaultBackoff;
89
+ const configuredTimeoutMs = options.timeoutMs;
90
+ if (configuredTimeoutMs !== void 0 && !(Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs >= 0)) throw new Error(`jobs.timeoutMs must be a number of milliseconds, or 0 for no limit (got ${String(configuredTimeoutMs)}).`);
91
+ /** `undefined` means no limit: only an explicit `0` asks for that. */
92
+ const timeoutMs = configuredTimeoutMs === void 0 ? DEFAULT_JOB_TIMEOUT_MS : configuredTimeoutMs === 0 ? void 0 : configuredTimeoutMs;
64
93
  const handlers = /* @__PURE__ */ new Map();
65
94
  for (const [task, handler] of Object.entries(options.tasks ?? {})) handlers.set(task, handler);
66
95
  const workerId = `${process.pid}-${randomUUID().slice(0, 8)}`;
67
96
  let timer = null;
97
+ /** When {@link timer} fires (epoch ms), so a later wake never postpones an earlier one. */
98
+ let timerDue = Infinity;
99
+ /**
100
+ * The wait after a look that found nothing. Doubles to {@link idleCeilingMs}
101
+ * while the worker polls on its own; back to `pollIntervalMs` whenever
102
+ * there is work or a wake. Fixed at the ceiling while notifications wake it.
103
+ */
104
+ const idleCeilingMs = Math.max(IDLE_POLL_MAX_MS, pollIntervalMs);
105
+ let idleIntervalMs = pollIntervalMs;
106
+ /** Notifications from the database wake this worker — see `JobStore.subscribe`. */
107
+ let listening = false;
108
+ let unsubscribe = null;
109
+ /** A wake arrived while a claim was in flight: claim again once it is done. */
110
+ let rerun = false;
68
111
  let reapTimer = null;
112
+ let sweepTimer = null;
113
+ let sweeping = false;
69
114
  let running = false;
70
115
  /**
71
116
  * Every job this worker is running, tracked one by one rather than per
@@ -112,20 +157,38 @@ function createJobQueue(store, options = {}) {
112
157
  };
113
158
  if (!handler) {
114
159
  logger.warn(`[jobs] No handler registered for task "${job.task}" — returning the job to the queue`);
115
- await store.fail(job.id, `No handler registered for task "${job.task}"`, job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null, claim);
160
+ const retryAt = job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null;
161
+ await store.fail(job.id, `No handler registered for task "${job.task}"`, retryAt, claim);
162
+ if (retryAt) wake(retryAt.getTime());
116
163
  return;
117
164
  }
118
165
  const releaseClaim = keepClaim(job, claim);
166
+ const controller = new AbortController();
167
+ let deadline;
119
168
  try {
120
169
  try {
121
- await handler({
170
+ const run = Promise.resolve().then(() => handler({
122
171
  id: job.id,
123
172
  task: job.task,
124
173
  payload: job.payload,
125
174
  attempt: job.attempts,
126
- maxAttempts: job.maxAttempts
127
- });
175
+ maxAttempts: job.maxAttempts,
176
+ signal: controller.signal
177
+ }));
178
+ if (timeoutMs === void 0) await run;
179
+ else {
180
+ run.catch(() => void 0);
181
+ await Promise.race([run, new Promise((_, reject) => {
182
+ deadline = setTimeout(() => {
183
+ const error = /* @__PURE__ */ new Error(`"${job.task}" timed out after ${timeoutMs}ms (jobs.timeoutMs)`);
184
+ controller.abort(error);
185
+ reject(error);
186
+ }, timeoutMs);
187
+ deadline.unref?.();
188
+ })]);
189
+ }
128
190
  } finally {
191
+ clearTimeout(deadline);
129
192
  releaseClaim();
130
193
  }
131
194
  await store.complete(job.id, claim);
@@ -133,7 +196,9 @@ function createJobQueue(store, options = {}) {
133
196
  const message = error instanceof Error ? error.stack ?? error.message : String(error);
134
197
  const permanent = error instanceof PermanentJobError;
135
198
  const willRetry = !permanent && job.attempts < job.maxAttempts;
136
- await store.fail(job.id, message.slice(0, 4e3), willRetry ? new Date(Date.now() + backoff(job.attempts)) : null, claim);
199
+ const retryAt = willRetry ? new Date(Date.now() + backoff(job.attempts)) : null;
200
+ await store.fail(job.id, message.slice(0, 4e3), retryAt, claim);
201
+ if (retryAt) wake(retryAt.getTime());
137
202
  if (willRetry) logger.warn(`[jobs] "${job.task}" failed on attempt ${job.attempts}/${job.maxAttempts}; retrying`, { jobId: job.id });
138
203
  else if (permanent) logger.error(`[jobs] "${job.task}" failed permanently on attempt ${job.attempts}; not retrying`, {
139
204
  jobId: job.id,
@@ -154,13 +219,31 @@ function createJobQueue(store, options = {}) {
154
219
  if (reaping) return;
155
220
  reaping = true;
156
221
  try {
157
- await store.reapExpired(visibilityTimeoutMs);
222
+ if (await store.reapExpired(visibilityTimeoutMs) > 0) wake();
158
223
  } catch (error) {
159
224
  logger.error("[jobs] Failed to reclaim expired jobs", { error });
160
225
  } finally {
161
226
  reaping = false;
162
227
  }
163
228
  }
229
+ /**
230
+ * Delete finished jobs past retention. On the worker, on its own timer:
231
+ * the sweep used to run only in the boot path, awaited before the server
232
+ * served, so a pod that lived for weeks never pruned and the next boot
233
+ * paid for all of it at once.
234
+ */
235
+ async function sweep() {
236
+ if (sweeping || !store.sweepExpired) return;
237
+ sweeping = true;
238
+ try {
239
+ const deleted = await store.sweepExpired();
240
+ if (deleted > 0) logger.info(`[jobs] Removed ${deleted} finished job(s) past retention`);
241
+ } catch (error) {
242
+ logger.warn("[jobs] The retention sweep failed; it runs again within the hour", { error });
243
+ } finally {
244
+ sweeping = false;
245
+ }
246
+ }
164
247
  /** Claim as many jobs as there are free slots and start each one on its own. */
165
248
  async function claimAndStart() {
166
249
  const free = concurrency - active.size;
@@ -168,6 +251,7 @@ function createJobQueue(store, options = {}) {
168
251
  if (handlers.size === 0) return [];
169
252
  const jobs = await store.claim(free, workerId, [...handlers.keys()]);
170
253
  backlog = jobs.length >= free;
254
+ if (jobs.length > 0) idleIntervalMs = pollIntervalMs;
171
255
  return jobs.map((job) => {
172
256
  const run = runJob(job).catch((error) => {
173
257
  logger.error(`[jobs] Could not record the outcome of "${job.task}"`, {
@@ -182,17 +266,41 @@ function createJobQueue(store, options = {}) {
182
266
  return run;
183
267
  });
184
268
  }
269
+ /** Look for work in `delayMs` — unless a look is already due sooner. */
185
270
  function schedule(delayMs) {
186
271
  if (!running) return;
272
+ const due = Date.now() + delayMs;
273
+ if (timer && timerDue <= due) return;
187
274
  if (timer) clearTimeout(timer);
275
+ timerDue = due;
188
276
  timer = setTimeout(() => {
189
277
  timer = null;
278
+ timerDue = Infinity;
190
279
  tick();
191
280
  }, delayMs);
192
281
  timer.unref?.();
193
282
  }
283
+ /**
284
+ * Work may be runnable at `runAtMs` (now, when absent): a job was enqueued
285
+ * — here, or in another process that notified — or a retry or a reclaimed
286
+ * job came due. Resets the back-off.
287
+ */
288
+ function wake(runAtMs) {
289
+ if (!running) return;
290
+ idleIntervalMs = pollIntervalMs;
291
+ const delay = runAtMs === void 0 ? 0 : Math.max(0, runAtMs - Date.now());
292
+ if (delay === 0 && active.size >= concurrency) {
293
+ backlog = true;
294
+ return;
295
+ }
296
+ schedule(delay);
297
+ }
194
298
  async function tick() {
195
- if (!running || claiming) return;
299
+ if (!running) return;
300
+ if (claiming) {
301
+ rerun = true;
302
+ return;
303
+ }
196
304
  const work = claimAndStart().catch((error) => {
197
305
  logger.error("[jobs] Poll failed", { error });
198
306
  backlog = false;
@@ -200,7 +308,32 @@ function createJobQueue(store, options = {}) {
200
308
  claiming = work;
201
309
  await work;
202
310
  claiming = null;
203
- schedule(backlog && active.size < concurrency ? 0 : pollIntervalMs);
311
+ if (backlog && active.size < concurrency || rerun) {
312
+ rerun = false;
313
+ schedule(0);
314
+ return;
315
+ }
316
+ if (listening) {
317
+ schedule(idleCeilingMs);
318
+ return;
319
+ }
320
+ schedule(idleIntervalMs);
321
+ idleIntervalMs = Math.min(idleIntervalMs * 2, idleCeilingMs);
322
+ }
323
+ /** Be woken by the database when another process enqueues — where it can. */
324
+ function listen() {
325
+ if (!store.subscribe) return;
326
+ store.subscribe((runAtMs) => wake(runAtMs), () => wake()).then((stop) => {
327
+ if (!running) {
328
+ stop().catch(() => void 0);
329
+ return;
330
+ }
331
+ unsubscribe = stop;
332
+ listening = true;
333
+ logger.info(`[jobs] Woken by the database on enqueue; looking every ${idleCeilingMs / 1e3}s besides`);
334
+ }).catch((error) => {
335
+ logger.info(`[jobs] Not woken by the database (${error instanceof Error ? error.message : String(error)}); polling instead, from every ${pollIntervalMs}ms up to every ${idleCeilingMs / 1e3}s while idle`);
336
+ });
204
337
  }
205
338
  async function drain() {
206
339
  await claiming;
@@ -210,12 +343,20 @@ function createJobQueue(store, options = {}) {
210
343
  start() {
211
344
  if (running) return;
212
345
  running = true;
213
- logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms)`);
346
+ idleIntervalMs = pollIntervalMs;
347
+ logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms, up to ${idleCeilingMs}ms while idle)`);
348
+ if (configuredTimeoutMs === void 0) logger.info(`[jobs] Each attempt is limited to ${DEFAULT_JOB_TIMEOUT_MS / 6e4} minutes, the default jobs.timeoutMs. Set jobs.timeoutMs to change it, or 0 for no limit.`);
214
349
  reap();
215
350
  reapTimer = setInterval(() => {
216
351
  reap();
217
352
  }, visibilityTimeoutMs * REAP_INTERVAL_FACTOR);
218
353
  reapTimer.unref?.();
354
+ sweep();
355
+ sweepTimer = setInterval(() => {
356
+ sweep();
357
+ }, RETENTION_SWEEP_INTERVAL_MS);
358
+ sweepTimer.unref?.();
359
+ listen();
219
360
  schedule(0);
220
361
  },
221
362
  async stop(timeoutMs) {
@@ -223,11 +364,22 @@ function createJobQueue(store, options = {}) {
223
364
  if (timer) {
224
365
  clearTimeout(timer);
225
366
  timer = null;
367
+ timerDue = Infinity;
226
368
  }
369
+ listening = false;
370
+ const stopListening = unsubscribe;
371
+ unsubscribe = null;
372
+ if (stopListening) await stopListening().catch((error) => {
373
+ logger.warn("[jobs] Could not close the notification connection", { error });
374
+ });
227
375
  if (reapTimer) {
228
376
  clearInterval(reapTimer);
229
377
  reapTimer = null;
230
378
  }
379
+ if (sweepTimer) {
380
+ clearInterval(sweepTimer);
381
+ sweepTimer = null;
382
+ }
231
383
  draining ??= drain().finally(() => {
232
384
  draining = null;
233
385
  });
@@ -256,13 +408,18 @@ function createJobQueue(store, options = {}) {
256
408
  return running;
257
409
  },
258
410
  async enqueue(task, payload, enqueueOptions = {}) {
259
- return store.insert({
411
+ const runAt = new Date(Date.now() + (enqueueOptions.delayMs ?? 0));
412
+ const ambient = currentAmbientTransaction();
413
+ const id = await store.insert({
260
414
  task,
261
415
  payload: payload ?? null,
262
- runAt: new Date(Date.now() + (enqueueOptions.delayMs ?? 0)),
416
+ runAt,
263
417
  maxAttempts: enqueueOptions.maxAttempts ?? defaultMaxAttempts,
264
418
  idempotencyKey: enqueueOptions.idempotencyKey
265
419
  });
420
+ if (id !== null && running) if (ambient) ambient.afterCommit(() => wake(runAt.getTime()));
421
+ else wake(runAt.getTime());
422
+ return id;
266
423
  }
267
424
  };
268
425
  }
@@ -277,7 +434,7 @@ function createJobQueue(store, options = {}) {
277
434
  * about — and to test — away from a polling loop.
278
435
  */
279
436
  var TABLE = "rebase.jobs";
280
- /** How long finished jobs are kept before the boot sweep removes them. */
437
+ /** How long finished jobs are kept before the retention sweep removes them. */
281
438
  var SUCCEEDED_RETENTION_DAYS = 3;
282
439
  /**
283
440
  * Failures outlive successes by a lot. A dead-lettered job is evidence, and the
@@ -285,6 +442,13 @@ var SUCCEEDED_RETENTION_DAYS = 3;
285
442
  * on Friday night.
286
443
  */
287
444
  var FAILED_RETENTION_DAYS = 30;
445
+ /**
446
+ * Rows one retention statement deletes. A sweep repeats it until a batch
447
+ * comes back short, so a backlog — the first sweep on a table a long-lived
448
+ * pod let grow to millions of rows — is many short transactions rather than
449
+ * one that holds locks and writes a burst of WAL for seconds.
450
+ */
451
+ var RETENTION_BATCH = 5e3;
288
452
  function toRecord(row) {
289
453
  return {
290
454
  id: row.id,
@@ -327,6 +491,16 @@ function isUniqueViolation(err) {
327
491
  */
328
492
  var ENQUEUE_FUNCTION = "rebase.enqueue_job";
329
493
  var ENQUEUE_SIGNATURE = `${ENQUEUE_FUNCTION}(text, jsonb, timestamptz, integer, text)`;
494
+ /**
495
+ * The `NOTIFY` channel an insert announces itself on, with the job's `run_at`
496
+ * in epoch milliseconds as the payload. Workers listening on it are woken
497
+ * instead of polling; Postgres delivers it when the insert commits, and not at
498
+ * all if it rolls back. Frozen: workers of the previous release listen on it
499
+ * during a rolling deploy.
500
+ */
501
+ var JOBS_NOTIFY_CHANNEL = "rebase_jobs";
502
+ /** `run_at` as the notification's payload: epoch milliseconds, as text. */
503
+ var NOTIFY_PAYLOAD = (runAt) => `(extract(epoch FROM ${runAt}) * 1000)::bigint::text`;
330
504
  /** The rows a claim's writes may touch: `$1` the job, `$2` the worker, `$3` the attempt. */
331
505
  var HELD_BY_CLAIM = "id = $1 AND status = 'running' AND locked_by = $2 AND attempts = $3";
332
506
  function warnStaleOutcome(id, claim, outcome) {
@@ -382,13 +556,13 @@ function createJobStore(driver) {
382
556
  CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_idempotency
383
557
  ON ${TABLE}(idempotency_key)
384
558
  WHERE idempotency_key IS NOT NULL AND status IN ('pending', 'running')
559
+ `);
560
+ await ddl.ensureObject("Creating idx_jobs_finished", `
561
+ CREATE INDEX IF NOT EXISTS idx_jobs_finished
562
+ ON ${TABLE}(status, updated_at)
563
+ WHERE status IN ('succeeded', 'failed')
385
564
  `);
386
565
  if (await ddl.isReadable(TABLE)) {
387
- await ddl.step("Job retention sweep", async () => {
388
- await exec(`DELETE FROM ${TABLE}
389
- WHERE (status = 'succeeded' AND updated_at < now() - make_interval(days => $1))
390
- OR (status = 'failed' AND updated_at < now() - make_interval(days => $2))`, [SUCCEEDED_RETENTION_DAYS, FAILED_RETENTION_DAYS]);
391
- });
392
566
  await ddl.step("Revoking end-user access to jobs", () => exec(revokeInternalTableSql("rebase", "jobs")));
393
567
  await ddl.ensureObject(`Creating ${ENQUEUE_FUNCTION}`, `
394
568
  CREATE OR REPLACE FUNCTION ${ENQUEUE_FUNCTION}(
@@ -399,6 +573,7 @@ function createJobStore(driver) {
399
573
  SECURITY DEFINER
400
574
  SET search_path = pg_catalog, pg_temp
401
575
  AS $fn$
576
+ SELECT pg_notify('${JOBS_NOTIFY_CHANNEL}', ${NOTIFY_PAYLOAD("p_run_at")});
402
577
  INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)
403
578
  VALUES (p_task, p_payload, p_run_at, p_max_attempts, p_idempotency_key)
404
579
  ON CONFLICT DO NOTHING
@@ -431,9 +606,13 @@ function createJobStore(driver) {
431
606
  const ambient = enqueueInTransaction ? currentAmbientTransaction() : void 0;
432
607
  if (ambient) return (await ambient.exec(`SELECT ${ENQUEUE_FUNCTION}($1::text, $2::jsonb, $3::timestamptz, $4::integer, $5::text) AS id`, params))?.[0]?.id ?? null;
433
608
  try {
434
- return (await exec(`INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)
435
- VALUES ($1, $2::jsonb, $3, $4, $5)
436
- RETURNING id`, params))?.[0]?.id ?? null;
609
+ return (await exec(`WITH inserted AS (
610
+ INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)
611
+ VALUES ($1, $2::jsonb, $3, $4, $5)
612
+ RETURNING id, run_at
613
+ )
614
+ SELECT id, pg_notify('rebase_jobs', ${NOTIFY_PAYLOAD("run_at")}) AS notified
615
+ FROM inserted`, params))?.[0]?.id ?? null;
437
616
  } catch (error) {
438
617
  if (isUniqueViolation(error)) return null;
439
618
  throw error;
@@ -518,6 +697,37 @@ function createJobStore(driver) {
518
697
  if (count > 0) logger.warn(`[jobs] Reclaimed ${count} job(s) from a worker that stopped responding (${revived?.length ?? 0} retryable, ${buried?.length ?? 0} dead-lettered)`);
519
698
  return count;
520
699
  },
700
+ async sweepExpired() {
701
+ let total = 0;
702
+ for (;;) {
703
+ const rows = await exec(`WITH swept AS (
704
+ DELETE FROM ${TABLE}
705
+ WHERE id IN (
706
+ SELECT id FROM ${TABLE}
707
+ WHERE (status = 'succeeded' AND updated_at < now() - make_interval(days => $1))
708
+ OR (status = 'failed' AND updated_at < now() - make_interval(days => $2))
709
+ LIMIT $3
710
+ FOR UPDATE SKIP LOCKED
711
+ )
712
+ RETURNING 1
713
+ )
714
+ SELECT count(*)::int AS deleted FROM swept`, [
715
+ SUCCEEDED_RETENTION_DAYS,
716
+ FAILED_RETENTION_DAYS,
717
+ RETENTION_BATCH
718
+ ]);
719
+ const deleted = Number(firstSqlRow(rows)?.deleted ?? 0);
720
+ total += deleted;
721
+ if (deleted < RETENTION_BATCH) return total;
722
+ }
723
+ },
724
+ async subscribe(onWake, onReconnect) {
725
+ if (!admin.listen) throw new Error("this database driver has no notifications");
726
+ return admin.listen(JOBS_NOTIFY_CHANNEL, (payload) => {
727
+ const runAtMs = Number(payload);
728
+ onWake(Number.isFinite(runAtMs) && runAtMs > 0 ? runAtMs : void 0);
729
+ }, { onReconnect });
730
+ },
521
731
  async fetch(id) {
522
732
  const row = firstSqlRow(await exec(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]));
523
733
  return row ? toRecord(row) : null;
@@ -533,4 +743,4 @@ var jobs_exports = /* @__PURE__ */ __exportAll({
533
743
  //#endregion
534
744
  export { defaultBackoff as a, createJobQueue as i, createJobStore as n, currentAmbientTransaction as o, PermanentJobError as r, jobs_exports as t };
535
745
 
536
- //# sourceMappingURL=jobs-DqYNfquG.js.map
746
+ //# sourceMappingURL=jobs-BCbflZj2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs-BCbflZj2.js","names":[],"sources":["../src/db/ambient-transaction.ts","../src/jobs/job-queue.ts","../src/jobs/job-store.ts","../src/jobs/index.ts"],"sourcesContent":["/**\n * The write transaction the current code is running inside, when there is one.\n *\n * A collection callback runs inside the transaction that carries its write.\n * Work it hands off — a job, a queue message, a topic event, a webhook\n * delivery — has to share that write's fate: queued when it commits, never\n * queued when it rolls back, and invisible to a worker until the commit. The\n * code that queues it lives in this package and knows no driver; the\n * transaction belongs to the driver. So the driver publishes the transaction it\n * opened for a write, and the queueing code asks for it here.\n *\n * Until this existed the job store wrote through the default driver's own\n * connection, in autocommit, whatever called it. A job enqueued from an\n * `afterSave` that then threw stayed queued for a row that was never written,\n * and a worker could claim it before the row it was about committed.\n *\n * The resolver lives on `globalThis` under a registered symbol, like the\n * `rebase` singleton: a managed runtime can load two copies of this package,\n * and the copy a driver registers with must be the one the job store reads.\n * No Node imports — the store is reachable from code that has to stay portable.\n *\n * **The symbol's key is a contract between packages, and frozen.** The\n * Postgres driver writes the slot itself rather than importing a setter from\n * here: the managed runtime supplies this package from the image while a\n * bundle brings its own driver, and a driver importing a function an older\n * image does not export would fail to link and take the boot down. Written\n * through the slot, any pairing of versions degrades to the old behaviour —\n * jobs committing on their own — instead.\n */\nexport interface AmbientTransaction {\n /**\n * Run one statement on the transaction, `$1…` placeholders as\n * `executeSql` takes them. It runs as the transaction's current role — on\n * Postgres the request's restricted one — so a statement that needs more\n * goes through a function granted to that role.\n */\n exec(sqlText: string, params?: unknown[]): Promise<Record<string, unknown>[]>;\n /** Run `fn` after the transaction commits. Never runs if it rolls back. */\n afterCommit(fn: () => void): void;\n}\n\n/** Frozen — see above. `server-postgres/src/services/write-transaction-scope.ts` writes it. */\nconst RESOLVER_SLOT = Symbol.for(\"rebase.server.ambientTransactionResolver\");\n\ntype ResolverSlot = { [RESOLVER_SLOT]?: () => AmbientTransaction | undefined };\n\n/**\n * Register how to find the write transaction the caller is inside; pass\n * `undefined` to unregister. For tests and for drivers in this repository's\n * lockstep — a driver shipped separately writes the slot, as above.\n */\nexport function setAmbientTransactionResolver(resolve: (() => AmbientTransaction | undefined) | undefined): void {\n (globalThis as ResolverSlot)[RESOLVER_SLOT] = resolve;\n}\n\n/** The write transaction the caller is inside, or `undefined` outside one. */\nexport function currentAmbientTransaction(): AmbientTransaction | undefined {\n return (globalThis as ResolverSlot)[RESOLVER_SLOT]?.();\n}\n","import { randomUUID } from \"crypto\";\nimport { logger } from \"../utils/logger.js\";\nimport { currentAmbientTransaction } from \"../db/ambient-transaction.js\";\nimport type { JobClaim, JobStore } from \"./job-store.js\";\nimport type { EnqueueOptions, JobHandler, JobQueueClient, JobQueueOptions, JobRecord } from \"./types.js\";\n\n/**\n * The worker: claim, run, record, repeat.\n *\n * Everything difficult about running jobs concurrently is in `job-store.ts`,\n * where one `UPDATE … FOR UPDATE SKIP LOCKED` does the arbitration. What is\n * left here is a loop and the decisions around a handler that throws.\n */\n\nconst DEFAULT_CONCURRENCY = 5;\nconst DEFAULT_POLL_INTERVAL_MS = 2_000;\nconst DEFAULT_VISIBILITY_TIMEOUT_MS = 5 * 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\n\n/**\n * How long one attempt may run when `jobs.timeoutMs` is left out. Without a\n * bound, a handler awaiting a socket that never answers held its worker slot\n * and its claim for the life of the process, with `/health` green. Fifteen\n * minutes is far past any job that is merely slow, and a job meant to run\n * longer says so with its own `timeoutMs` — or `0`, for no limit.\n */\nexport const DEFAULT_JOB_TIMEOUT_MS = 15 * 60_000;\n\n/**\n * The longest an idle worker goes without looking for work.\n *\n * An idle worker used to claim every 2 seconds, forever: about 45,000 round\n * trips a day per pod, each with a pool checkout, and a database that could\n * never suspend. Now it is woken instead — by `NOTIFY` when another process\n * enqueues, by its own enqueues, retries and delayed jobs — and this is the\n * safety poll behind that. Where the database cannot notify it, it backs off\n * from `pollIntervalMs` to this, doubling after each look that found nothing.\n */\nconst IDLE_POLL_MAX_MS = 30_000;\n\n/** 1s, 5s, 25s, 125s … capped at an hour. */\nexport function defaultBackoff(attempt: number): number {\n return Math.min(1_000 * Math.pow(5, Math.max(0, attempt - 1)), 60 * 60_000);\n}\n\n/**\n * How often the reaper runs, relative to the visibility timeout.\n *\n * A quarter of it, so a stranded job waits at most 1.25× the timeout rather\n * than 2× — and so the sweep is not itself a per-poll query against a table\n * whose interesting rows are, almost always, none.\n */\nconst REAP_INTERVAL_FACTOR = 0.25;\n\n/**\n * How often a worker deletes finished jobs past retention. Hourly keeps the\n * table near its retention size on a pod that is never redeployed; each sweep\n * reads only the finished rows, through `idx_jobs_finished`.\n */\nconst RETENTION_SWEEP_INTERVAL_MS = 60 * 60_000;\n\n/**\n * Thrown by a handler for a failure no retry can change — a destination that\n * is refused, a receiver that redirects. The job is dead-lettered on this\n * attempt, with the message as its `last_error`, instead of spending its\n * remaining attempts proving the same thing. Returning normally is not the\n * alternative: that records the job as succeeded.\n */\nexport class PermanentJobError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"PermanentJobError\";\n }\n}\n\nexport interface JobQueue extends JobQueueClient {\n start(): void;\n /**\n * Stop claiming, and wait for the jobs in flight — for at most `timeoutMs`\n * when given. A job still running when the budget runs out keeps its claim\n * and is recovered by the visibility timeout; waiting on it without a bound\n * would let one handler that never settles hold the whole shutdown.\n */\n stop(timeoutMs?: number): Promise<void>;\n /**\n * Claim what fits in the free slots, run it, and resolve with how many jobs\n * ran. For tests and for `/jobs/drain`; not meant to run beside `start()`.\n */\n runOnce(): Promise<number>;\n /** Registered after construction — how `tasks` from config and internal producers meet. */\n register<P = unknown>(task: string, handler: JobHandler<P>): void;\n isRunning(): boolean;\n}\n\nexport function createJobQueue(store: JobStore, options: JobQueueOptions = {}): JobQueue {\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;\n const defaultMaxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const backoff = options.backoff ?? defaultBackoff;\n const configuredTimeoutMs = options.timeoutMs;\n if (configuredTimeoutMs !== undefined && !(Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs >= 0)) {\n // NaN is what a mistyped env var parses to; it would fail every\n // attempt at once.\n throw new Error(`jobs.timeoutMs must be a number of milliseconds, or 0 for no limit (got ${String(configuredTimeoutMs)}).`);\n }\n /** `undefined` means no limit: only an explicit `0` asks for that. */\n const timeoutMs = configuredTimeoutMs === undefined\n ? DEFAULT_JOB_TIMEOUT_MS\n : configuredTimeoutMs === 0 ? undefined : configuredTimeoutMs;\n\n const handlers = new Map<string, JobHandler<never>>();\n for (const [task, handler] of Object.entries(options.tasks ?? {})) {\n handlers.set(task, handler);\n }\n\n // Identifies this process in `locked_by`: \"which pod had it when it\n // stopped\" is the first question anyone asks of a stuck job, and with the\n // attempt it is the claim an outcome is written under (see `JobClaim`).\n const workerId = `${process.pid}-${randomUUID().slice(0, 8)}`;\n\n let timer: NodeJS.Timeout | null = null;\n /** When {@link timer} fires (epoch ms), so a later wake never postpones an earlier one. */\n let timerDue = Infinity;\n /**\n * The wait after a look that found nothing. Doubles to {@link idleCeilingMs}\n * while the worker polls on its own; back to `pollIntervalMs` whenever\n * there is work or a wake. Fixed at the ceiling while notifications wake it.\n */\n const idleCeilingMs = Math.max(IDLE_POLL_MAX_MS, pollIntervalMs);\n let idleIntervalMs = pollIntervalMs;\n /** Notifications from the database wake this worker — see `JobStore.subscribe`. */\n let listening = false;\n let unsubscribe: (() => Promise<void>) | null = null;\n /** A wake arrived while a claim was in flight: claim again once it is done. */\n let rerun = false;\n let reapTimer: NodeJS.Timeout | null = null;\n let sweepTimer: NodeJS.Timeout | null = null;\n let sweeping = false;\n let running = false;\n /**\n * Every job this worker is running, tracked one by one rather than per\n * claimed batch: a batch would let its slowest job decide when the next\n * claim happens, so one handler that never settled would idle every other\n * slot — and hold `stop()` with it.\n */\n const active = new Set<Promise<void>>();\n /** The claim in progress, so `stop()` also waits for jobs it is about to hand out. */\n let claiming: Promise<unknown> | null = null;\n /** The last claim filled every free slot, so more work is probably waiting. */\n let backlog = false;\n let reaping = false;\n let draining: Promise<void> | null = null;\n\n /**\n * Renew `claim` while its handler runs, at a third of the visibility\n * timeout, so the reaper only ever takes a job from a worker that stopped\n * renewing — a dead one — and never from one that is merely slow. Returns\n * the stop.\n */\n function keepClaim(job: JobRecord, claim: JobClaim): () => void {\n if (!store.heartbeat) return () => undefined;\n const renew = store.heartbeat.bind(store);\n const interval = setInterval(() => {\n renew(job.id, claim).then((held) => {\n if (held) return;\n clearInterval(interval);\n logger.warn(\n `[jobs] \"${job.task}\" lost its claim on attempt ${claim.attempt} while running; ` +\n \"its outcome will not be recorded\", { jobId: job.id }\n );\n }).catch((error: unknown) => {\n logger.warn(`[jobs] Could not renew the claim on \"${job.task}\"`, { jobId: job.id, error });\n });\n }, Math.max(1, Math.floor(visibilityTimeoutMs / 3)));\n interval.unref?.();\n return () => clearInterval(interval);\n }\n\n async function runJob(job: JobRecord): Promise<void> {\n const handler = handlers.get(job.task);\n const claim: JobClaim = { workerId, attempt: job.attempts };\n\n if (!handler) {\n // Only reachable through a store that ignores the `tasks` filter on\n // `claim` — the one that ships never hands this worker a task it\n // has no handler for. Give the row back for a peer that has one.\n // The attempt the claim spent is not undone, so a task nobody in\n // the fleet implements dead-letters with an error naming it rather\n // than cycling through such a store forever.\n logger.warn(`[jobs] No handler registered for task \"${job.task}\" — returning the job to the queue`);\n const retryAt = job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null;\n await store.fail(job.id, `No handler registered for task \"${job.task}\"`, retryAt, claim);\n if (retryAt) wake(retryAt.getTime());\n return;\n }\n\n const releaseClaim = keepClaim(job, claim);\n const controller = new AbortController();\n let deadline: NodeJS.Timeout | undefined;\n try {\n try {\n const run = Promise.resolve().then(() => handler({\n id: job.id,\n task: job.task,\n payload: job.payload as never,\n attempt: job.attempts,\n maxAttempts: job.maxAttempts,\n signal: controller.signal\n } as never));\n if (timeoutMs === undefined) {\n await run;\n } else {\n // A handler that ignores the signal outlives its attempt;\n // what it does after is not this attempt's outcome.\n run.catch(() => undefined);\n await Promise.race([run, new Promise<never>((_, reject) => {\n deadline = setTimeout(() => {\n const error = new Error(`\"${job.task}\" timed out after ${timeoutMs}ms (jobs.timeoutMs)`);\n controller.abort(error);\n reject(error);\n }, timeoutMs);\n // A deadline is not work: it must not hold a process\n // open that is otherwise done.\n deadline.unref?.();\n })]);\n }\n } finally {\n clearTimeout(deadline);\n releaseClaim();\n }\n await store.complete(job.id, claim);\n } catch (error) {\n const message = error instanceof Error ? (error.stack ?? error.message) : String(error);\n const permanent = error instanceof PermanentJobError;\n const willRetry = !permanent && job.attempts < job.maxAttempts;\n\n // Truncated, because `last_error` holds a stack and a queue that\n // accumulates megabytes of them is its own outage.\n const retryAt = willRetry ? new Date(Date.now() + backoff(job.attempts)) : null;\n await store.fail(job.id, message.slice(0, 4_000), retryAt, claim);\n // This worker knows when the retry is due; nobody will notify it.\n if (retryAt) wake(retryAt.getTime());\n\n if (willRetry) {\n logger.warn(`[jobs] \"${job.task}\" failed on attempt ${job.attempts}/${job.maxAttempts}; retrying`, { jobId: job.id });\n } else if (permanent) {\n logger.error(`[jobs] \"${job.task}\" failed permanently on attempt ${job.attempts}; not retrying`, { jobId: job.id, error: message });\n } else {\n // The last attempt is an error, not a warning: nothing else will\n // touch this job, and if nobody looks at the table it is simply\n // lost work.\n logger.error(`[jobs] \"${job.task}\" failed permanently after ${job.attempts} attempts`, { jobId: job.id, error: message });\n }\n }\n }\n\n /**\n * Return jobs stranded by a dead worker. On its own timer, not inside the\n * poll: a poll can be skipped for as long as every slot is busy, and that\n * is exactly when a stranded job most needs a peer to take it back.\n */\n async function reap(): Promise<void> {\n if (reaping) return;\n reaping = true;\n try {\n // Revived jobs are runnable now, and nothing notified anyone.\n if (await store.reapExpired(visibilityTimeoutMs) > 0) wake();\n } catch (error) {\n logger.error(\"[jobs] Failed to reclaim expired jobs\", { error });\n } finally {\n reaping = false;\n }\n }\n\n /**\n * Delete finished jobs past retention. On the worker, on its own timer:\n * the sweep used to run only in the boot path, awaited before the server\n * served, so a pod that lived for weeks never pruned and the next boot\n * paid for all of it at once.\n */\n async function sweep(): Promise<void> {\n if (sweeping || !store.sweepExpired) return;\n sweeping = true;\n try {\n const deleted = await store.sweepExpired();\n if (deleted > 0) logger.info(`[jobs] Removed ${deleted} finished job(s) past retention`);\n } catch (error) {\n logger.warn(\"[jobs] The retention sweep failed; it runs again within the hour\", { error });\n } finally {\n sweeping = false;\n }\n }\n\n /** Claim as many jobs as there are free slots and start each one on its own. */\n async function claimAndStart(): Promise<Promise<void>[]> {\n const free = concurrency - active.size;\n if (free <= 0) return [];\n\n // Only the tasks this worker can run. During a rolling deploy an\n // instance on older code shares the table with jobs whose task only\n // the newer code implements, and a claim spends an attempt: claiming\n // them here would dead-letter them before an updated peer got to them.\n // A handler registered after `start()` joins the next claim.\n if (handlers.size === 0) return [];\n const jobs = await store.claim(free, workerId, [...handlers.keys()]);\n backlog = jobs.length >= free;\n // Found work: look again soon if it runs out. Found none: wait longer.\n if (jobs.length > 0) idleIntervalMs = pollIntervalMs;\n\n return jobs.map((job) => {\n const run: Promise<void> = runJob(job)\n .catch((error) => {\n // `runJob` handles a throwing handler; this is a store\n // write failing after it. The job keeps its claim, so the\n // visibility timeout recovers it.\n logger.error(`[jobs] Could not record the outcome of \"${job.task}\"`, { jobId: job.id, error });\n })\n .finally(() => {\n active.delete(run);\n // A slot just came free and there is work waiting: take the\n // next job now, not a poll interval from now.\n if (backlog) schedule(0);\n });\n active.add(run);\n return run;\n });\n }\n\n /** Look for work in `delayMs` — unless a look is already due sooner. */\n function schedule(delayMs: number): void {\n if (!running) return;\n const due = Date.now() + delayMs;\n if (timer && timerDue <= due) return;\n if (timer) clearTimeout(timer);\n timerDue = due;\n timer = setTimeout(() => {\n timer = null;\n timerDue = Infinity;\n void tick();\n }, delayMs);\n // Never hold the process open. A queue with nothing to do should not be\n // the reason `rebase dev` will not exit.\n timer.unref?.();\n }\n\n /**\n * Work may be runnable at `runAtMs` (now, when absent): a job was enqueued\n * — here, or in another process that notified — or a retry or a reclaimed\n * job came due. Resets the back-off.\n */\n function wake(runAtMs?: number): void {\n if (!running) return;\n idleIntervalMs = pollIntervalMs;\n const delay = runAtMs === undefined ? 0 : Math.max(0, runAtMs - Date.now());\n if (delay === 0 && active.size >= concurrency) {\n // Every slot is busy: the next one to free claims at once.\n backlog = true;\n return;\n }\n schedule(delay);\n }\n\n async function tick(): Promise<void> {\n if (!running) return;\n // A claim already in progress looks again when it finishes — the wake\n // may be for a row its snapshot did not see.\n if (claiming) {\n rerun = true;\n return;\n }\n const work = claimAndStart().catch((error) => {\n logger.error(\"[jobs] Poll failed\", { error });\n backlog = false;\n });\n claiming = work;\n await work;\n claiming = null;\n\n // A full claim with a slot still free — a job already finished — means\n // go straight back, and so does a wake that came in mid-claim.\n // Otherwise sleep; a job finishing during a backlog wakes the loop\n // early, and so does anything that calls `wake`.\n if ((backlog && active.size < concurrency) || rerun) {\n rerun = false;\n schedule(0);\n return;\n }\n if (listening) {\n schedule(idleCeilingMs);\n return;\n }\n schedule(idleIntervalMs);\n idleIntervalMs = Math.min(idleIntervalMs * 2, idleCeilingMs);\n }\n\n /** Be woken by the database when another process enqueues — where it can. */\n function listen(): void {\n if (!store.subscribe) return;\n store.subscribe(\n (runAtMs) => wake(runAtMs),\n // Notifications sent while the connection was down are gone.\n () => wake()\n ).then((stop) => {\n if (!running) {\n void stop().catch(() => undefined);\n return;\n }\n unsubscribe = stop;\n listening = true;\n logger.info(`[jobs] Woken by the database on enqueue; looking every ${idleCeilingMs / 1000}s besides`);\n }).catch((error: unknown) => {\n logger.info(\n `[jobs] Not woken by the database (${error instanceof Error ? error.message : String(error)}); ` +\n `polling instead, from every ${pollIntervalMs}ms up to every ${idleCeilingMs / 1000}s while idle`\n );\n });\n }\n\n async function drain(): Promise<void> {\n await claiming;\n await Promise.allSettled([...active]);\n }\n\n return {\n start(): void {\n if (running) return;\n running = true;\n idleIntervalMs = pollIntervalMs;\n logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms, up to ${idleCeilingMs}ms while idle)`);\n if (configuredTimeoutMs === undefined) {\n logger.info(\n `[jobs] Each attempt is limited to ${DEFAULT_JOB_TIMEOUT_MS / 60_000} minutes, the default jobs.timeoutMs. ` +\n \"Set jobs.timeoutMs to change it, or 0 for no limit.\"\n );\n }\n void reap();\n reapTimer = setInterval(() => { void reap(); }, visibilityTimeoutMs * REAP_INTERVAL_FACTOR);\n reapTimer.unref?.();\n void sweep();\n sweepTimer = setInterval(() => { void sweep(); }, RETENTION_SWEEP_INTERVAL_MS);\n sweepTimer.unref?.();\n listen();\n schedule(0);\n },\n\n async stop(timeoutMs?: number): Promise<void> {\n running = false;\n if (timer) {\n clearTimeout(timer);\n timer = null;\n timerDue = Infinity;\n }\n listening = false;\n const stopListening = unsubscribe;\n unsubscribe = null;\n if (stopListening) {\n await stopListening().catch((error: unknown) => {\n logger.warn(\"[jobs] Could not close the notification connection\", { error });\n });\n }\n if (reapTimer) {\n clearInterval(reapTimer);\n reapTimer = null;\n }\n if (sweepTimer) {\n clearInterval(sweepTimer);\n sweepTimer = null;\n }\n // Jobs in flight keep their claim until they finish or the\n // visibility timeout expires, so waiting here is what turns a\n // graceful shutdown into \"no job runs twice\".\n draining ??= drain().finally(() => { draining = null; });\n if (timeoutMs === undefined) {\n await draining;\n return;\n }\n let budget: NodeJS.Timeout | undefined;\n const finished = await Promise.race([\n draining.then(() => true),\n new Promise<false>((resolve) => {\n budget = setTimeout(() => resolve(false), timeoutMs);\n budget.unref?.();\n })\n ]);\n clearTimeout(budget);\n if (!finished) {\n logger.warn(\n `[jobs] ${active.size} job(s) still running after ${timeoutMs}ms; stopping without them. ` +\n \"They keep their claim and are retried once the visibility timeout reclaims it.\"\n );\n }\n },\n\n async runOnce(): Promise<number> {\n const started = await claimAndStart();\n await Promise.allSettled(started);\n return started.length;\n },\n\n register<P = unknown>(task: string, handler: JobHandler<P>): void {\n if (handlers.has(task)) {\n logger.warn(`[jobs] Task \"${task}\" was already registered; the later handler wins`);\n }\n handlers.set(task, handler as JobHandler<never>);\n },\n\n isRunning(): boolean {\n return running;\n },\n\n async enqueue<P = unknown>(task: string, payload?: P, enqueueOptions: EnqueueOptions = {}): Promise<string | null> {\n const runAt = new Date(Date.now() + (enqueueOptions.delayMs ?? 0));\n // Read before the insert, which may await: inside a write the row\n // is invisible until that write commits, so that is when to look.\n const ambient = currentAmbientTransaction();\n const id = await store.insert({\n task,\n payload: payload ?? null,\n runAt,\n maxAttempts: enqueueOptions.maxAttempts ?? defaultMaxAttempts,\n idempotencyKey: enqueueOptions.idempotencyKey\n });\n // This process's own worker needs no notification to hear about it.\n if (id !== null && running) {\n if (ambient) ambient.afterCommit(() => wake(runAt.getTime()));\n else wake(runAt.getTime());\n }\n return id;\n }\n };\n}\n","import type { DataDriver } from \"@rebasepro/types\";\nimport { isSQLAdmin } from \"@rebasepro/types\";\nimport { REBASE_USER_ROLE, revokeInternalTableSql, sqlRows, firstSqlRow } from \"@rebasepro/common\";\nimport { logger } from \"../utils/logger.js\";\nimport { createDdlBootstrapper, hasInCauseChain, type SqlExec } from \"../boot/ddl-bootstrap.js\";\nimport { currentAmbientTransaction } from \"../db/ambient-transaction.js\";\nimport type { JobRecord } from \"./types.js\";\n\n/**\n * The queue's storage, as SQL against `rebase.jobs`.\n *\n * Split from the worker for the same reason `cron-store` is split from\n * `cron-scheduler`: the interesting parts here are three statements that have\n * to be exactly right under concurrency, and they are much easier to reason\n * about — and to test — away from a polling loop.\n */\n\nconst TABLE = \"rebase.jobs\";\n\n/** How long finished jobs are kept before the retention sweep removes them. */\nconst SUCCEEDED_RETENTION_DAYS = 3;\n/**\n * Failures outlive successes by a lot. A dead-lettered job is evidence, and the\n * person who needs it is usually looking on Monday for something that happened\n * on Friday night.\n */\nconst FAILED_RETENTION_DAYS = 30;\n\n/**\n * Rows one retention statement deletes. A sweep repeats it until a batch\n * comes back short, so a backlog — the first sweep on a table a long-lived\n * pod let grow to millions of rows — is many short transactions rather than\n * one that holds locks and writes a burst of WAL for seconds.\n */\nconst RETENTION_BATCH = 5_000;\n\n/** A row as Postgres returns it. */\ninterface JobRow {\n id: string;\n task: string;\n payload: unknown;\n status: string;\n run_at: string;\n attempts: number;\n max_attempts: number;\n last_error: string | null;\n created_at: string;\n updated_at: string;\n}\n\nfunction toRecord(row: JobRow): JobRecord {\n return {\n id: row.id,\n task: row.task,\n payload: row.payload,\n status: row.status as JobRecord[\"status\"],\n runAt: new Date(row.run_at).toISOString(),\n attempts: Number(row.attempts),\n maxAttempts: Number(row.max_attempts),\n lastError: row.last_error,\n createdAt: new Date(row.created_at).toISOString(),\n updatedAt: new Date(row.updated_at).toISOString()\n };\n}\n\n/** Same rule as `cron-store`: match the SQLSTATE, never the message. */\nfunction isUniqueViolation(err: unknown): boolean {\n return hasInCauseChain(err, (e) => e.code === \"23505\");\n}\n\n/**\n * How a job gets into `rebase.jobs` from inside somebody else's transaction.\n *\n * An enqueue from a collection callback has to ride the write's transaction —\n * committed with it, discarded with it, invisible to workers until then — and\n * that transaction runs as the request's restricted role, which has no access\n * to `rebase.jobs` at all (see the revoke in `ensureTable`). This function is\n * the one door: `SECURITY DEFINER`, so the insert runs with the owner's rights\n * for that one statement and the transaction's role never changes. Switching\n * the role for the insert instead would elevate the whole connection for as\n * long as the insert took, and a callback's un-awaited statement would run\n * in that window as the owner.\n *\n * The door is narrow on purpose. It takes the five columns an enqueue sets and\n * nothing else, so it cannot claim, complete or rewrite a job — and only the\n * request role is granted it, not PUBLIC. That role runs statements the server\n * writes; nobody outside gets SQL as it.\n *\n * `ON CONFLICT DO NOTHING` rather than a caught unique violation: an error\n * inside a transaction aborts the whole transaction, so an idempotency clash\n * would have failed the customer's write. A clash returns no row, hence NULL.\n */\nconst ENQUEUE_FUNCTION = \"rebase.enqueue_job\";\nconst ENQUEUE_SIGNATURE = `${ENQUEUE_FUNCTION}(text, jsonb, timestamptz, integer, text)`;\n\n/**\n * The `NOTIFY` channel an insert announces itself on, with the job's `run_at`\n * in epoch milliseconds as the payload. Workers listening on it are woken\n * instead of polling; Postgres delivers it when the insert commits, and not at\n * all if it rolls back. Frozen: workers of the previous release listen on it\n * during a rolling deploy.\n */\nexport const JOBS_NOTIFY_CHANNEL = \"rebase_jobs\";\n\n/** `run_at` as the notification's payload: epoch milliseconds, as text. */\nconst NOTIFY_PAYLOAD = (runAt: string) => `(extract(epoch FROM ${runAt}) * 1000)::bigint::text`;\n\n/**\n * Which claim an outcome belongs to: the worker holding the job, and the\n * attempt that claim spent.\n *\n * The visibility timeout hands a job to a second worker when the first stops\n * renewing its claim, and the first may still be running — slow, not dead. Its\n * outcome is then about an attempt the table has moved past, and writing it\n * would mark a running job `succeeded`, or send a finished one back to\n * `pending` for another run. Both writes therefore apply only while the claim\n * they name still holds the row. The attempt is part of the identity because a\n * worker's own reaper can hand a job back to the same worker in another slot.\n */\nexport interface JobClaim {\n workerId: string;\n attempt: number;\n}\n\nexport interface JobStore {\n ensureTable(): Promise<void>;\n /** Returns the new job's id, or `null` if an idempotency key matched unfinished work. */\n insert(job: {\n task: string;\n payload: unknown;\n runAt: Date;\n maxAttempts: number;\n idempotencyKey?: string;\n }): Promise<string | null>;\n /**\n * Atomically take up to `limit` runnable jobs for this worker — only jobs\n * whose task is in `tasks`, when it is given. A worker never claims work it\n * cannot run: a claim spends an attempt, so an instance running older code\n * during a rollout would otherwise dead-letter the newer code's jobs.\n */\n claim(limit: number, workerId: string, tasks?: readonly string[]): Promise<JobRecord[]>;\n /** Mark the job `succeeded` — a no-op once `claim` no longer holds it. */\n complete(id: string, claim: JobClaim): Promise<void>;\n /**\n * Back to `pending` with a later `runAt`, or `failed` when out of attempts —\n * a no-op once `claim` no longer holds it.\n */\n fail(id: string, error: string, retryAt: Date | null, claim: JobClaim): Promise<void>;\n /**\n * Renew `claim` on a job whose handler is still running, so the visibility\n * timeout only ever reclaims a job from a worker that stopped renewing.\n * Resolves with whether the claim still held. A store without it leaves a\n * handler that outlives the timeout to be run a second time beside itself.\n */\n heartbeat?(id: string, claim: JobClaim): Promise<boolean>;\n /** Return jobs stranded by a worker that died holding them. Resolves with how many. */\n reapExpired(visibilityTimeoutMs: number): Promise<number>;\n /**\n * Call `onWake` whenever any process inserts a job, with the job's `run_at`\n * in epoch milliseconds. Resolves, once listening, with the stop; rejects\n * when this database cannot notify this process — no session connection\n * to hold a `LISTEN` on — and the worker polls instead. `onReconnect` is\n * called after a dropped connection is back: what was sent meanwhile is\n * lost. A store without it leaves the worker polling.\n */\n subscribe?(onWake: (runAtMs?: number) => void, onReconnect: () => void): Promise<() => Promise<void>>;\n /**\n * Delete finished jobs past retention — succeeded ones after\n * {@link SUCCEEDED_RETENTION_DAYS} days, failed ones after\n * {@link FAILED_RETENTION_DAYS} — in batches. Resolves with how many went.\n * The worker runs it on a timer; a store without it keeps every row.\n */\n sweepExpired?(): Promise<number>;\n fetch(id: string): Promise<JobRecord | null>;\n}\n\n/** The rows a claim's writes may touch: `$1` the job, `$2` the worker, `$3` the attempt. */\nconst HELD_BY_CLAIM = \"id = $1 AND status = 'running' AND locked_by = $2 AND attempts = $3\";\n\nfunction warnStaleOutcome(id: string, claim: JobClaim, outcome: \"success\" | \"failure\"): void {\n logger.warn(\n `[jobs] Ignored the ${outcome} of attempt ${claim.attempt} of job ${id}: the job was reclaimed ` +\n \"from this worker while it ran, so that attempt's outcome is no longer the job's.\"\n );\n}\n\nexport function createJobStore(driver: DataDriver): JobStore | undefined {\n const admin = driver.admin;\n if (!isSQLAdmin(admin)) {\n logger.warn(\n \"⚠️ [jobs] DataDriver does not support SQL admin — the durable job queue is unavailable. \" +\n \"Work that would have been queued runs inline instead.\"\n );\n return undefined;\n }\n\n // Two shapes of the same call: the bootstrapper's `SqlExec` takes an\n // options object, while everything below reads better with a positional\n // parameter array.\n const execRaw: SqlExec = (sqlText, options) =>\n admin.executeSql(sqlText, options?.params ? { params: options.params } : undefined);\n const exec = (sqlText: string, params?: unknown[]) =>\n execRaw(sqlText, params ? { params } : undefined);\n\n const ddl = createDdlBootstrapper(execRaw, \"jobs\");\n\n /**\n * Whether {@link ENQUEUE_FUNCTION} exists, so an enqueue inside a write can\n * ride its transaction. Decided at boot rather than assumed: a database\n * that refused the function would otherwise fail every write whose callback\n * enqueues, where falling back to the old behaviour only loses atomicity.\n */\n let enqueueInTransaction = false;\n\n return {\n async ensureTable(): Promise<void> {\n await ddl.ensureObject(\"Creating schema rebase\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n await ddl.ensureObject(`Creating ${TABLE}`, `\n CREATE TABLE IF NOT EXISTS ${TABLE} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n task TEXT NOT NULL,\n payload JSONB,\n status TEXT NOT NULL DEFAULT 'pending',\n run_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n locked_at TIMESTAMPTZ,\n locked_by TEXT,\n idempotency_key TEXT,\n last_error TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )\n `);\n\n // The claim query's index. Partial, because the rows it has to find\n // fast are a shrinking minority of a table that also holds every\n // success and every dead letter.\n await ddl.ensureObject(\"Creating idx_jobs_runnable\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_runnable\n ON ${TABLE}(run_at, created_at)\n WHERE status = 'pending'\n `);\n\n // Finds jobs stranded by a dead worker.\n await ddl.ensureObject(\"Creating idx_jobs_running\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_running\n ON ${TABLE}(locked_at)\n WHERE status = 'running'\n `);\n\n // What makes `idempotencyKey` a guarantee rather than a\n // check-then-act race: two instances reacting to one event both\n // reach the INSERT, and the index decides. Partial on unfinished\n // work — see `EnqueueOptions.idempotencyKey` for why a key must be\n // reusable once its job is done.\n await ddl.ensureObject(\"Creating idx_jobs_idempotency\", `\n CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_idempotency\n ON ${TABLE}(idempotency_key)\n WHERE idempotency_key IS NOT NULL AND status IN ('pending', 'running')\n `);\n\n // The retention sweep's index. Partial on the finished rows — the\n // only ones it deletes — so a sweep reads what it removes instead\n // of the whole table; without it every sweep was a sequential scan,\n // six seconds over two million rows.\n await ddl.ensureObject(\"Creating idx_jobs_finished\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_finished\n ON ${TABLE}(status, updated_at)\n WHERE status IN ('succeeded', 'failed')\n `);\n\n const ready = await ddl.isReadable(TABLE);\n\n if (ready) {\n // No retention sweep here. It used to run at boot and only at\n // boot: awaited before the server served, a sequential scan of\n // the whole table, and never again for as long as the pod lived\n // — a worker up for three weeks held seven times the rows\n // retention meant to keep. The worker runs `sweepExpired` on a\n // timer instead.\n\n // Payloads are arbitrary application data — a webhook body, a\n // user id, whatever was passed — and a writable queue lets any\n // signed-in user schedule work of their choosing under the\n // server's own authority. Same reasoning, and the same\n // unconditional re-application, as `cron_claims`.\n await ddl.step(\"Revoking end-user access to jobs\", () =>\n exec(revokeInternalTableSql(\"rebase\", \"jobs\")));\n\n await ddl.ensureObject(`Creating ${ENQUEUE_FUNCTION}`, `\n CREATE OR REPLACE FUNCTION ${ENQUEUE_FUNCTION}(\n p_task text, p_payload jsonb, p_run_at timestamptz,\n p_max_attempts integer, p_idempotency_key text\n ) RETURNS text\n LANGUAGE sql\n SECURITY DEFINER\n SET search_path = pg_catalog, pg_temp\n AS $fn$\n SELECT pg_notify('${JOBS_NOTIFY_CHANNEL}', ${NOTIFY_PAYLOAD(\"p_run_at\")});\n INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)\n VALUES (p_task, p_payload, p_run_at, p_max_attempts, p_idempotency_key)\n ON CONFLICT DO NOTHING\n RETURNING id\n $fn$\n `);\n // Re-applied every boot: a new function is executable by PUBLIC\n // until told otherwise, and the request role may be provisioned\n // after the function was first made.\n await ddl.step(`Granting ${ENQUEUE_FUNCTION} to the request role`, async () => {\n await exec(`REVOKE ALL ON FUNCTION ${ENQUEUE_SIGNATURE} FROM PUBLIC`);\n await exec(`\n DO $grant$ BEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}') THEN\n GRANT EXECUTE ON FUNCTION ${ENQUEUE_SIGNATURE} TO ${REBASE_USER_ROLE};\n END IF;\n END $grant$\n `);\n });\n enqueueInTransaction = firstSqlRow<{ present: boolean }>(await exec(\n `SELECT to_regprocedure('${ENQUEUE_SIGNATURE}') IS NOT NULL AS present`\n ))?.present === true;\n if (!enqueueInTransaction) {\n logger.warn(\n `⚠️ [jobs] ${ENQUEUE_FUNCTION} could not be created, so a job enqueued inside a write ` +\n \"commits on its own: it stays queued if the write rolls back.\"\n );\n }\n\n logger.info(\"✅ Job queue table ready\");\n } else {\n logger.error(\n `❌ [jobs] ${TABLE} is unavailable — nothing can be queued and nothing queued earlier ` +\n \"will run. Callers fall back to running the work inline.\"\n );\n }\n },\n\n async insert(job): Promise<string | null> {\n const params = [\n job.task,\n JSON.stringify(job.payload ?? null),\n job.runAt.toISOString(),\n job.maxAttempts,\n job.idempotencyKey ?? null\n ];\n // Inside a write: on its transaction, through the definer function\n // — see `ENQUEUE_FUNCTION`. Checked and used with no `await`\n // between, so the transaction cannot close in the gap.\n const ambient = enqueueInTransaction ? currentAmbientTransaction() : undefined;\n if (ambient) {\n const rows = await ambient.exec(\n `SELECT ${ENQUEUE_FUNCTION}($1::text, $2::jsonb, $3::timestamptz, $4::integer, $5::text) AS id`,\n params\n );\n return (rows?.[0]?.id as string | null | undefined) ?? null;\n }\n try {\n // The notification rides the insert's own statement, so it\n // is sent exactly when the row commits.\n const rows = await exec(\n `WITH inserted AS (\n INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)\n VALUES ($1, $2::jsonb, $3, $4, $5)\n RETURNING id, run_at\n )\n SELECT id, pg_notify('${JOBS_NOTIFY_CHANNEL}', ${NOTIFY_PAYLOAD(\"run_at\")}) AS notified\n FROM inserted`,\n params\n );\n return (rows?.[0]?.id as string) ?? null;\n } catch (error) {\n // The losing side of an idempotency race. Not an error: the\n // work the caller wanted is already queued, which is the\n // outcome they asked for.\n if (isUniqueViolation(error)) return null;\n throw error;\n }\n },\n\n async claim(limit: number, workerId: string, tasks?: readonly string[]): Promise<JobRecord[]> {\n if (tasks && tasks.length === 0) return [];\n // One placeholder per task rather than an array parameter: every\n // driver binds a string, and not every driver binds an array.\n const taskFilter = tasks\n ? `AND task IN (${tasks.map((_, i) => `$${i + 3}`).join(\", \")})`\n : \"\";\n // `FOR UPDATE SKIP LOCKED` is the whole design. The inner select\n // takes row locks on the jobs it picks and *skips* any a concurrent\n // worker already holds, so N workers polling the same table divide\n // the work instead of contending for the head of it — and no job is\n // ever handed to two of them.\n //\n // `attempts` is incremented here, on claim, rather than on failure.\n // A worker that is killed mid-job never reports anything, so\n // counting on failure would let a job that crashes the process be\n // retried forever, once per restart, taking the process down each\n // time.\n const rows = await exec(\n `UPDATE ${TABLE} SET\n status = 'running',\n attempts = attempts + 1,\n locked_at = now(),\n locked_by = $2,\n updated_at = now()\n WHERE id IN (\n SELECT id FROM ${TABLE}\n WHERE status = 'pending' AND run_at <= now() ${taskFilter}\n ORDER BY run_at, created_at\n LIMIT $1\n FOR UPDATE SKIP LOCKED\n )\n RETURNING *`,\n [limit, workerId, ...(tasks ?? [])]\n );\n return sqlRows<JobRow>(rows).map(toRecord);\n },\n\n async complete(id: string, claim: JobClaim): Promise<void> {\n const rows = await exec(\n `UPDATE ${TABLE} SET status = 'succeeded', locked_at = NULL, locked_by = NULL,\n last_error = NULL, updated_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt]\n );\n if (sqlRows(rows).length === 0) warnStaleOutcome(id, claim, \"success\");\n },\n\n async fail(id: string, error: string, retryAt: Date | null, claim: JobClaim): Promise<void> {\n const rows = retryAt\n ? await exec(\n `UPDATE ${TABLE} SET status = 'pending', run_at = $4, locked_at = NULL,\n locked_by = NULL, last_error = $5, updated_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt, retryAt.toISOString(), error]\n )\n : await exec(\n `UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,\n last_error = $4, updated_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt, error]\n );\n if (sqlRows(rows).length === 0) warnStaleOutcome(id, claim, \"failure\");\n },\n\n async heartbeat(id: string, claim: JobClaim): Promise<boolean> {\n const rows = await exec(\n `UPDATE ${TABLE} SET locked_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt]\n );\n return sqlRows(rows).length > 0;\n },\n\n async reapExpired(visibilityTimeoutMs: number): Promise<number> {\n // A worker killed while holding a job cannot release it, so nothing\n // but a timeout will ever free the row. Jobs that still have\n // attempts left go back to `pending`; the rest are dead-lettered\n // with an error that says what happened, because \"attempts: 3,\n // lastError: null\" is otherwise a genuinely baffling row to find.\n const seconds = Math.max(1, Math.round(visibilityTimeoutMs / 1000));\n\n const revived = await exec(\n `UPDATE ${TABLE} SET status = 'pending', locked_at = NULL, locked_by = NULL,\n last_error = 'Worker stopped responding; the job was reclaimed', updated_at = now()\n WHERE status = 'running'\n AND locked_at < now() - make_interval(secs => $1)\n AND attempts < max_attempts\n RETURNING id`,\n [seconds]\n );\n\n const buried = await exec(\n `UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,\n last_error = 'Worker stopped responding on the final attempt', updated_at = now()\n WHERE status = 'running'\n AND locked_at < now() - make_interval(secs => $1)\n AND attempts >= max_attempts\n RETURNING id`,\n [seconds]\n );\n\n const count = (revived?.length ?? 0) + (buried?.length ?? 0);\n if (count > 0) {\n logger.warn(\n `[jobs] Reclaimed ${count} job(s) from a worker that stopped responding ` +\n `(${revived?.length ?? 0} retryable, ${buried?.length ?? 0} dead-lettered)`\n );\n }\n return count;\n },\n\n async sweepExpired(): Promise<number> {\n // `SKIP LOCKED`, so replicas sweeping at once divide the rows\n // rather than queue behind each other's batch. The count comes back\n // as one row, not one id per deleted job.\n let total = 0;\n for (;;) {\n const rows = await exec(\n `WITH swept AS (\n DELETE FROM ${TABLE}\n WHERE id IN (\n SELECT id FROM ${TABLE}\n WHERE (status = 'succeeded' AND updated_at < now() - make_interval(days => $1))\n OR (status = 'failed' AND updated_at < now() - make_interval(days => $2))\n LIMIT $3\n FOR UPDATE SKIP LOCKED\n )\n RETURNING 1\n )\n SELECT count(*)::int AS deleted FROM swept`,\n [SUCCEEDED_RETENTION_DAYS, FAILED_RETENTION_DAYS, RETENTION_BATCH]\n );\n const deleted = Number(firstSqlRow<{ deleted: number }>(rows)?.deleted ?? 0);\n total += deleted;\n if (deleted < RETENTION_BATCH) return total;\n }\n },\n\n async subscribe(onWake: (runAtMs?: number) => void, onReconnect: () => void): Promise<() => Promise<void>> {\n if (!admin.listen) {\n throw new Error(\"this database driver has no notifications\");\n }\n return admin.listen(JOBS_NOTIFY_CHANNEL, (payload) => {\n const runAtMs = Number(payload);\n onWake(Number.isFinite(runAtMs) && runAtMs > 0 ? runAtMs : undefined);\n }, { onReconnect });\n },\n\n async fetch(id: string): Promise<JobRecord | null> {\n const rows = await exec(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]);\n const row = firstSqlRow<JobRow>(rows);\n return row ? toRecord(row) : null;\n }\n };\n}\n","export { createJobStore } from \"./job-store\";\nexport type { JobClaim, JobStore } from \"./job-store\";\nexport { createJobQueue, defaultBackoff, PermanentJobError } from \"./job-queue\";\nexport type { JobQueue } from \"./job-queue\";\nexport type {\n EnqueueOptions,\n JobContext,\n JobHandler,\n JobQueueClient,\n JobQueueOptions,\n JobRecord,\n JobStatus\n} from \"./types\";\n"],"mappings":"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"}