alepha 0.20.4 → 0.20.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/api/audits/index.d.ts +391 -359
  2. package/dist/api/audits/index.d.ts.map +1 -1
  3. package/dist/api/audits/index.js +23 -1
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +18 -0
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js +51 -0
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.browser.js +33 -14
  10. package/dist/api/jobs/index.browser.js.map +1 -1
  11. package/dist/api/jobs/index.d.ts +452 -155
  12. package/dist/api/jobs/index.d.ts.map +1 -1
  13. package/dist/api/jobs/index.js +474 -159
  14. package/dist/api/jobs/index.js.map +1 -1
  15. package/dist/api/keys/index.d.ts +32 -4
  16. package/dist/api/keys/index.d.ts.map +1 -1
  17. package/dist/api/keys/index.js +53 -0
  18. package/dist/api/keys/index.js.map +1 -1
  19. package/dist/api/notifications/index.d.ts +29 -1
  20. package/dist/api/notifications/index.d.ts.map +1 -1
  21. package/dist/api/notifications/index.js +55 -13
  22. package/dist/api/notifications/index.js.map +1 -1
  23. package/dist/api/organizations/index.js.map +1 -1
  24. package/dist/api/parameters/index.d.ts +15 -0
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +37 -0
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/api/payments/index.js.map +1 -1
  29. package/dist/api/users/index.d.ts +150 -9
  30. package/dist/api/users/index.d.ts.map +1 -1
  31. package/dist/api/users/index.js +237 -28
  32. package/dist/api/users/index.js.map +1 -1
  33. package/dist/api/verifications/index.d.ts +3 -3
  34. package/dist/api/verifications/index.js.map +1 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/bin/index.js +0 -0
  37. package/dist/bucket/index.d.ts +18 -0
  38. package/dist/bucket/index.d.ts.map +1 -1
  39. package/dist/bucket/index.js +47 -0
  40. package/dist/bucket/index.js.map +1 -1
  41. package/dist/bucket/index.workerd.js +24 -0
  42. package/dist/bucket/index.workerd.js.map +1 -1
  43. package/dist/cache/core/index.d.ts +20 -3
  44. package/dist/cache/core/index.d.ts.map +1 -1
  45. package/dist/cache/core/index.js.map +1 -1
  46. package/dist/cache/core/index.workerd.js.map +1 -1
  47. package/dist/cache/database/index.d.ts +155 -0
  48. package/dist/cache/database/index.d.ts.map +1 -0
  49. package/dist/cache/database/index.js +266 -0
  50. package/dist/cache/database/index.js.map +1 -0
  51. package/dist/cache/redis/index.js.map +1 -1
  52. package/dist/captcha/index.js.map +1 -1
  53. package/dist/cli/config/index.js.map +1 -1
  54. package/dist/cli/core/index.d.ts +35 -5
  55. package/dist/cli/core/index.d.ts.map +1 -1
  56. package/dist/cli/core/index.js +85 -6
  57. package/dist/cli/core/index.js.map +1 -1
  58. package/dist/cli/devtools/index.js.map +1 -1
  59. package/dist/cli/platform/index.js +1 -1
  60. package/dist/cli/platform/index.js.map +1 -1
  61. package/dist/cli/vendor/index.js.map +1 -1
  62. package/dist/command/index.js.map +1 -1
  63. package/dist/core/index.browser.js.map +1 -1
  64. package/dist/core/index.js.map +1 -1
  65. package/dist/core/index.native.js.map +1 -1
  66. package/dist/core/index.workerd.js.map +1 -1
  67. package/dist/crypto/index.browser.js.map +1 -1
  68. package/dist/crypto/index.js.map +1 -1
  69. package/dist/datetime/index.js.map +1 -1
  70. package/dist/email/brevo/index.js.map +1 -1
  71. package/dist/email/core/index.js.map +1 -1
  72. package/dist/email/core/index.workerd.js.map +1 -1
  73. package/dist/email/smtp/index.js.map +1 -1
  74. package/dist/fake/index.js.map +1 -1
  75. package/dist/lock/core/index.js.map +1 -1
  76. package/dist/lock/redis/index.js.map +1 -1
  77. package/dist/logger/index.js.map +1 -1
  78. package/dist/mcp/index.js.map +1 -1
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js.map +1 -1
  81. package/dist/orm/core/index.js.map +1 -1
  82. package/dist/orm/postgres/index.bun.js.map +1 -1
  83. package/dist/orm/postgres/index.js.map +1 -1
  84. package/dist/queue/core/index.js.map +1 -1
  85. package/dist/queue/core/index.workerd.js.map +1 -1
  86. package/dist/queue/redis/index.js.map +1 -1
  87. package/dist/react/auth/index.browser.js.map +1 -1
  88. package/dist/react/auth/index.js.map +1 -1
  89. package/dist/react/core/index.js.map +1 -1
  90. package/dist/react/form/index.js +2 -0
  91. package/dist/react/form/index.js.map +1 -1
  92. package/dist/react/head/index.browser.js.map +1 -1
  93. package/dist/react/head/index.js.map +1 -1
  94. package/dist/react/i18n/index.js.map +1 -1
  95. package/dist/react/intro/index.js.map +1 -1
  96. package/dist/react/router/index.browser.js.map +1 -1
  97. package/dist/react/router/index.js.map +1 -1
  98. package/dist/react/testing/index.js.map +1 -1
  99. package/dist/react/ui/index.js.map +1 -1
  100. package/dist/react/websocket/index.js.map +1 -1
  101. package/dist/redis/index.bun.js.map +1 -1
  102. package/dist/redis/index.js.map +1 -1
  103. package/dist/retry/index.js.map +1 -1
  104. package/dist/router/index.js.map +1 -1
  105. package/dist/scheduler/index.d.ts +22 -0
  106. package/dist/scheduler/index.d.ts.map +1 -1
  107. package/dist/scheduler/index.js +12 -0
  108. package/dist/scheduler/index.js.map +1 -1
  109. package/dist/scheduler/index.workerd.js +12 -0
  110. package/dist/scheduler/index.workerd.js.map +1 -1
  111. package/dist/security/index.browser.js.map +1 -1
  112. package/dist/security/index.js.map +1 -1
  113. package/dist/server/auth/index.js.map +1 -1
  114. package/dist/server/cookies/index.browser.js.map +1 -1
  115. package/dist/server/cookies/index.js.map +1 -1
  116. package/dist/server/core/index.browser.js.map +1 -1
  117. package/dist/server/core/index.js.map +1 -1
  118. package/dist/server/cors/index.js.map +1 -1
  119. package/dist/server/etag/index.js.map +1 -1
  120. package/dist/server/health/index.js.map +1 -1
  121. package/dist/server/links/index.browser.js.map +1 -1
  122. package/dist/server/links/index.js.map +1 -1
  123. package/dist/server/metrics/index.js.map +1 -1
  124. package/dist/server/proxy/index.js.map +1 -1
  125. package/dist/server/rate-limit/index.js.map +1 -1
  126. package/dist/server/static/index.js.map +1 -1
  127. package/dist/server/swagger/index.js.map +1 -1
  128. package/dist/sms/index.js.map +1 -1
  129. package/dist/system/index.browser.js.map +1 -1
  130. package/dist/system/index.js.map +1 -1
  131. package/dist/system/index.workerd.js.map +1 -1
  132. package/dist/topic/core/index.js.map +1 -1
  133. package/dist/topic/redis/index.js.map +1 -1
  134. package/dist/websocket/index.browser.js +4 -0
  135. package/dist/websocket/index.browser.js.map +1 -1
  136. package/dist/websocket/index.js +10 -0
  137. package/dist/websocket/index.js.map +1 -1
  138. package/package.json +282 -272
  139. package/src/api/audits/controllers/AdminAuditController.ts +29 -0
  140. package/src/api/files/controllers/FileController.ts +24 -0
  141. package/src/api/files/services/FileService.ts +41 -0
  142. package/src/api/jobs/__tests__/$job.spec.ts +427 -2
  143. package/src/api/jobs/entities/jobExecutionEntity.ts +3 -3
  144. package/src/api/jobs/index.ts +47 -10
  145. package/src/api/jobs/primitives/$job.ts +22 -9
  146. package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
  147. package/src/api/jobs/providers/JobDispatcher.ts +49 -0
  148. package/src/api/jobs/providers/JobProvider.ts +365 -142
  149. package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
  150. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -3
  151. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
  152. package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
  153. package/src/api/jobs/services/JobService.ts +21 -11
  154. package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
  155. package/src/api/keys/services/ApiKeyService.ts +42 -0
  156. package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
  157. package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
  158. package/src/api/notifications/index.ts +13 -3
  159. package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
  160. package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
  161. package/src/api/parameters/services/ParameterProvider.ts +18 -0
  162. package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
  163. package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
  164. package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
  165. package/src/api/users/controllers/AdminSessionController.ts +29 -0
  166. package/src/api/users/controllers/AdminUserController.ts +32 -0
  167. package/src/api/users/index.ts +3 -0
  168. package/src/api/users/services/CredentialService.ts +5 -0
  169. package/src/api/users/services/RegistrationService.ts +49 -1
  170. package/src/api/users/services/SessionCrudService.ts +16 -0
  171. package/src/api/users/services/SessionService.ts +17 -59
  172. package/src/api/users/services/UsernameSlugger.ts +195 -0
  173. package/src/bucket/primitives/$bucket.ts +21 -0
  174. package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
  175. package/src/bucket/providers/FileStorageProvider.ts +9 -0
  176. package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
  177. package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
  178. package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
  179. package/src/cache/core/primitives/$cache.ts +20 -3
  180. package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
  181. package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
  182. package/src/cache/database/entities/cacheEntries.ts +55 -0
  183. package/src/cache/database/index.ts +36 -0
  184. package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
  185. package/src/cli/core/services/ProjectScaffolder.ts +0 -2
  186. package/src/cli/core/tasks/BuildCloudflareTask.ts +17 -3
  187. package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
  188. package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
  189. package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
  190. package/src/cli/platform/commands/platform.ts +7 -1
  191. package/src/scheduler/index.ts +14 -0
  192. package/src/scheduler/providers/CronProvider.ts +13 -0
@@ -1,5 +1,6 @@
1
1
  import * as _$alepha from "alepha";
2
2
  import { Alepha, AlephaError, Async, KIND, PipelinePrimitive, PipelinePrimitiveOptions, Static, TNull, TObject, TOptional, TSchema, TUnion } from "alepha";
3
+ import { LockProvider } from "alepha/lock";
3
4
  import * as _$alepha_queue0 from "alepha/queue";
4
5
  import { CronProvider } from "alepha/scheduler";
5
6
  import * as _$alepha_server0 from "alepha/server";
@@ -313,6 +314,57 @@ declare module "alepha" {
313
314
  interface Env extends Partial<Static<typeof databaseEnvSchema>> {}
314
315
  } //# sourceMappingURL=databaseEnvSchema.d.ts.map
315
316
  //#endregion
317
+ //#region ../../src/api/jobs/entities/jobExecutionEntity.d.ts
318
+ /**
319
+ * Job execution record.
320
+ *
321
+ * Stores durable state for queue-mode jobs (outbox pattern) and error records
322
+ * for cron-mode jobs. Successful executions are trimmed by the sweep to keep
323
+ * the last N rows per job (configurable via `jobConfig.keepLastSuccess`).
324
+ *
325
+ * Status transitions:
326
+ * - queue push → pending (or `scheduled` if `delay`/`scheduledAt` was given)
327
+ * - worker claim → running
328
+ * - success → ok (or row deleted, depending on `record` and `keepLastSuccess`)
329
+ * - terminal failure → error
330
+ * - retryable failure → scheduled (with scheduledAt = now; sweep picks it up)
331
+ * - delay → scheduled (with scheduledAt = now + delay)
332
+ * - sweep picks due ones → pending
333
+ * - cancel → cancelled
334
+ */
335
+ declare const jobExecutionEntity: _$alepha_orm0.EntityPrimitive<_$alepha.TObject<{
336
+ id: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_PRIMARY_KEY>, typeof _$alepha_orm0.PG_DEFAULT>;
337
+ createdAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_CREATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
338
+ updatedAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_UPDATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
339
+ jobName: _$alepha.TString;
340
+ key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
341
+ status: _$alepha_orm0.PgAttr<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">, typeof _$alepha_orm0.PG_DEFAULT>;
342
+ priority: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
343
+ attempt: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
344
+ maxAttempts: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
345
+ payload: _$alepha.TOptional<_$alepha.TRecord<"^.*$", _$alepha.TAny>>;
346
+ scheduledAt: _$alepha.TOptional<_$alepha.TString>;
347
+ startedAt: _$alepha.TOptional<_$alepha.TString>;
348
+ completedAt: _$alepha.TOptional<_$alepha.TString>;
349
+ error: _$alepha.TOptional<_$alepha.TString>;
350
+ logs: _$alepha.TOptional<_$alepha.TArray<_$alepha.TObject<{
351
+ level: _$alepha.TUnsafe<"SILENT" | "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR">;
352
+ message: _$alepha.TString;
353
+ service: _$alepha.TString;
354
+ module: _$alepha.TString;
355
+ context: _$alepha.TOptional<_$alepha.TString>;
356
+ app: _$alepha.TOptional<_$alepha.TString>;
357
+ data: _$alepha.TOptional<_$alepha.TAny>;
358
+ timestamp: _$alepha.TNumber;
359
+ }>>>;
360
+ triggeredBy: _$alepha.TOptional<_$alepha.TString>;
361
+ triggeredByName: _$alepha.TOptional<_$alepha.TString>;
362
+ cancelledBy: _$alepha.TOptional<_$alepha.TString>;
363
+ cancelledByName: _$alepha.TOptional<_$alepha.TString>;
364
+ }>>;
365
+ type JobExecutionEntity = Static<typeof jobExecutionEntity.schema>;
366
+ type JobStatus = "pending" | "running" | "scheduled" | "ok" | "error" | "cancelled";
367
+ //#endregion
316
368
  //#region ../../src/api/jobs/primitives/$job.d.ts
317
369
  /**
318
370
  * Job primitive for defining scheduled (cron) or queued (push) tasks.
@@ -333,15 +385,8 @@ interface JobHandlerArgs<T extends TSchema = TSchema> {
333
385
  signal: AbortSignal;
334
386
  executionId: string;
335
387
  }
336
- interface JobRetryBackoff {
337
- initial: DurationLike;
338
- factor?: number;
339
- max?: DurationLike;
340
- jitter?: boolean;
341
- }
342
388
  interface JobRetryOptions {
343
389
  retries: number;
344
- backoff?: DurationLike | JobRetryBackoff;
345
390
  when?: (error: Error) => boolean;
346
391
  }
347
392
  type JobPriority = "critical" | "high" | "normal" | "low";
@@ -366,10 +411,30 @@ interface JobPrimitiveOptions<T extends TSchema = TSchema> extends PipelinePrimi
366
411
  */
367
412
  cron?: string;
368
413
  /**
369
- * Retry policy for queue-mode jobs.
414
+ * Retry policy for queue-mode and direct-mode jobs.
370
415
  * Cron-mode jobs do not retry — the next tick re-runs.
416
+ *
417
+ * Retries are picked up by the reconciliation sweep, so retry granularity
418
+ * is bounded by `sweepCron` (default 5 minutes). The first retry may run
419
+ * earlier than 5 minutes if the sweep tick happens sooner.
371
420
  */
372
421
  retry?: JobRetryOptions;
422
+ /**
423
+ * **Cron-mode only.** Whether to acquire a distributed lock around the
424
+ * cron tick so that only one instance of a multi-replica deployment runs
425
+ * the handler per tick.
426
+ *
427
+ * Has **no effect** on queue-mode and direct-mode jobs — those rely on
428
+ * the outbox `claim()` UPDATE-guard to serialize work instead, which is
429
+ * always on.
430
+ *
431
+ * To get cross-instance coordination on Docker / Node deployments,
432
+ * register a real `LockProvider` (e.g. `alepha/lock/redis`). The default
433
+ * `MemoryLockProvider` is per-process only.
434
+ *
435
+ * @default true
436
+ */
437
+ lock?: boolean;
373
438
  /**
374
439
  * Max execution time per attempt. Handler receives an `AbortSignal`.
375
440
  */
@@ -433,6 +498,52 @@ declare class JobPrimitive<T extends TSchema = TSchema> extends PipelinePrimitiv
433
498
  trigger(context?: JobTriggerContext<T>): Promise<void>;
434
499
  }
435
500
  //#endregion
501
+ //#region ../../src/api/jobs/providers/JobDispatcher.d.ts
502
+ /**
503
+ * Abstract dispatcher for queued/direct job executions.
504
+ *
505
+ * The default implementation, {@link DirectJobDispatcher}, runs the handler
506
+ * in-process after the caller's `push()` returns — fast and dependency-free.
507
+ *
508
+ * `AlephaApiJobsQueue` substitutes this with `JobQueueProvider`, which
509
+ * publishes the executionId to `AlephaQueue` so a worker pool can consume
510
+ * the work asynchronously.
511
+ *
512
+ * Substitute via DI:
513
+ * ```ts
514
+ * Alepha.create()
515
+ * .with({ provide: JobDispatcher, use: MyCustomDispatcher })
516
+ * .with(AlephaApiJobs);
517
+ * ```
518
+ *
519
+ * The `kind` getter is read by the `JobProvider.effectiveMode` accessor
520
+ * and by the admin UI so users can see which dispatcher is currently active.
521
+ */
522
+ declare abstract class JobDispatcher {
523
+ /**
524
+ * Identifier for this dispatcher's effective mode. Reported to the admin
525
+ * UI so operators can see whether `$job` is running in `queue` or
526
+ * `direct` mode.
527
+ */
528
+ abstract readonly kind: "queue" | "direct";
529
+ /**
530
+ * Hand off a single execution. The caller's `push()` awaits this so the
531
+ * caller can be sure the dispatch has at least been initiated. Long-running
532
+ * work must NOT be awaited here (use background scheduling instead) — this
533
+ * call should return as quickly as possible.
534
+ */
535
+ abstract dispatch(jobName: string, executionId: string): Promise<void>;
536
+ /**
537
+ * Optional batch dispatch. The default implementation loops, but
538
+ * dispatchers backed by a real queue should override this to use the
539
+ * provider's batch send (e.g. Cloudflare Queues `sendBatch`).
540
+ */
541
+ dispatchMany(items: Array<{
542
+ jobName: string;
543
+ executionId: string;
544
+ }>): Promise<void>;
545
+ }
546
+ //#endregion
436
547
  //#region ../../src/api/jobs/providers/JobProvider.d.ts
437
548
  declare const PRIORITY_MAP: Record<JobPriority, number>;
438
549
  declare const PRIORITY_REVERSE: Record<number, JobPriority>;
@@ -460,34 +571,54 @@ interface CancelContext {
460
571
  cancelledBy?: string;
461
572
  cancelledByName?: string;
462
573
  }
574
+ /**
575
+ * The declared shape of the job (set at registration time).
576
+ *
577
+ * **Important** — this `kind` is the *declared* form. The *effective*
578
+ * runtime mode (cron / queue / direct) is exposed by
579
+ * `JobProvider.effectiveMode(name)` and the `JobRegistration.type` field on
580
+ * the admin schema. Don't conflate the two: a `queue` kind can run as
581
+ * `direct` at runtime when no queue dispatcher is loaded.
582
+ */
463
583
  interface JobRuntimeRegistration {
464
584
  name: string;
465
585
  options: JobPrimitiveOptions;
466
- type: "cron" | "queue";
586
+ kind: "cron" | "queue";
467
587
  }
588
+ type JobEffectiveMode = "cron" | "queue" | "direct";
468
589
  /**
469
- * Coordinates cron (scheduler) and queue (push) jobs with a durable outbox
470
- * table and a single reconciliation sweep.
590
+ * Coordinates cron and push jobs with a durable outbox table and a single
591
+ * reconciliation sweep. The actual delivery channel (queue / direct) is
592
+ * abstracted behind {@link JobDispatcher}, substituted by DI:
471
593
  *
472
- * Queue-mode flow:
473
- * push() → INSERT row (pending) + queue.send({ executionId })
474
- * worker → SELECT row UPDATE running → handler → DELETE (ok) / UPDATE (error)
594
+ * - **DirectJobDispatcher** (default, registered by `AlephaApiJobs`) —
595
+ * runs the handler in-process right after `push()` returns.
596
+ * - **QueueJobDispatcher** (registered by `AlephaApiJobsQueue`) sends
597
+ * the executionId through `AlephaQueue` so a pool of workers can pick
598
+ * it up.
475
599
  *
476
- * Cron-mode flow:
477
- * scheduler tick handler runs inlineINSERT row only on error
600
+ * Push flow:
601
+ * push() INSERT row (pending)dispatcher.dispatch(jobName, id)
602
+ * worker → claim → UPDATE running → handler → DELETE/UPDATE on success
603
+ * → UPDATE error / scheduled (retry) on failure
478
604
  *
479
- * Sweep responsibilities (every `sweepInterval`):
605
+ * Cron flow:
606
+ * scheduler tick → acquire lock → executeInline (no retry)
607
+ * → enqueue + dispatch (retry declared)
608
+ *
609
+ * Sweep responsibilities (every `sweepCron`):
480
610
  * - re-enqueue pending rows older than `staleThreshold`
481
- * - fail running rows older than `max(timeout*2, runTimeout)`
482
- * - move `scheduled` rows with `scheduledAt <= now` to pending + enqueue
611
+ * - mark crashed running rows as failed and apply retry policy
612
+ * - move `scheduled` rows with `scheduledAt <= now` to pending + dispatch
483
613
  * - trim per-job history beyond `keepLastSuccess` / `keepLastError`
484
614
  */
485
615
  declare class JobProvider {
486
616
  protected readonly alepha: Alepha;
487
617
  protected readonly dt: DateTimeProvider;
488
618
  protected readonly cronProvider: CronProvider;
619
+ protected readonly lockProvider: LockProvider;
489
620
  protected readonly config: Readonly<{
490
- sweepInterval: number;
621
+ sweepCron: string;
491
622
  staleThreshold: number;
492
623
  runTimeout: number;
493
624
  keepLastSuccess: number;
@@ -495,6 +626,15 @@ declare class JobProvider {
495
626
  logMaxEntries: number;
496
627
  drainTimeout: number;
497
628
  }>;
629
+ /**
630
+ * Resolved at first use (after the container is fully wired) — picks
631
+ * the queue dispatcher when `AlephaApiJobsQueue` was loaded, otherwise
632
+ * the direct dispatcher. Lazy because both dispatchers inject
633
+ * `JobProvider` themselves; resolving them at field-init time would
634
+ * create a circular construction.
635
+ */
636
+ protected dispatcherRef?: JobDispatcher;
637
+ get dispatcher(): JobDispatcher;
498
638
  protected readonly log: _$alepha_logger0.Logger;
499
639
  protected readonly executions: _$alepha_orm0.Repository<_$alepha.TObject<{
500
640
  id: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_PRIMARY_KEY>, typeof _$alepha_orm0.PG_DEFAULT>;
@@ -502,7 +642,7 @@ declare class JobProvider {
502
642
  updatedAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_UPDATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
503
643
  jobName: _$alepha.TString;
504
644
  key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
505
- status: _$alepha_orm0.PgAttr<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">, typeof _$alepha_orm0.PG_DEFAULT>;
645
+ status: _$alepha_orm0.PgAttr<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">, typeof _$alepha_orm0.PG_DEFAULT>;
506
646
  priority: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
507
647
  attempt: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
508
648
  maxAttempts: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
@@ -540,14 +680,75 @@ declare class JobProvider {
540
680
  timestamp: number;
541
681
  }[]>;
542
682
  protected stopping: boolean;
543
- /**
544
- * Set by `JobQueueProvider` when `AlephaApiJobsQueue` is loaded.
545
- * When null, queue-mode jobs cannot be pushed.
546
- */
547
- queueDispatch: ((jobName: string, executionId: string) => Promise<void>) | null;
548
683
  registerJob(name: string, options: JobPrimitiveOptions): void;
549
684
  getRegisteredJobs(): Map<string, JobRuntimeRegistration>;
685
+ /**
686
+ * Resolves what *actually* runs at dispatch time. Cron jobs are always
687
+ * "cron"; non-cron jobs delegate to the active `JobDispatcher` (queue
688
+ * vs. direct), which is determined by which modules the app loaded.
689
+ */
690
+ effectiveMode(name: string): JobEffectiveMode;
550
691
  protected runCron(name: string): Promise<void>;
692
+ /**
693
+ * Cron-mode runner that respects the per-job distributed lock.
694
+ * Used by both the scheduled tick and manual `trigger()` calls so that an
695
+ * admin-triggered run on one instance can't race a scheduled run on another.
696
+ *
697
+ * **Two paths depending on `retry`:**
698
+ *
699
+ * - **No `retry`** — runs the handler inline. No DB row on success;
700
+ * error row only on failure. The "next tick" is the implicit retry.
701
+ * - **`retry` declared** — enqueues a synthetic execution row and hands
702
+ * it to the dispatcher. The handler then runs through the same path
703
+ * as a queue/direct push (claim, retry-on-fail, sweep recovery). Use
704
+ * this when a single failed tick must not block work for the whole
705
+ * `cron` interval (e.g. once-daily jobs).
706
+ */
707
+ protected runCronLocked(registration: JobRuntimeRegistration, ctx: {
708
+ triggeredBy?: string;
709
+ triggeredByName?: string;
710
+ }): Promise<void>;
711
+ /**
712
+ * Materialize a cron tick into the outbox so it goes through the normal
713
+ * retry/sweep path. Used when the user opts into `retry` on a cron job —
714
+ * a transient failure no longer means "wait for the next cron tick", it
715
+ * means "the sweep will retry within `sweepCron`".
716
+ */
717
+ protected enqueueCronExecution(registration: JobRuntimeRegistration, ctx: {
718
+ triggeredBy?: string;
719
+ triggeredByName?: string;
720
+ }): Promise<void>;
721
+ /**
722
+ * Acquire a per-job NX lock keyed by `cron-job:<name>` so that a single
723
+ * tick across all replicas runs exactly one execution. Auto-expires after
724
+ * `2 * timeout` (or 5 minutes if no per-job timeout) so a crashed worker
725
+ * cannot permanently block the cron from firing.
726
+ *
727
+ * **Caveat — same-process double-fire is not prevented.** The lock value
728
+ * is a per-process holder id, so two concurrent ticks on the same process
729
+ * (e.g. a scheduled tick overlapping an admin `trigger()` call) will both
730
+ * see "we own it". This is acceptable for the multi-replica use case the
731
+ * lock targets; a process that overlaps its own cron handler should set a
732
+ * smaller `timeout` or use idempotent handler logic. A future fix can add
733
+ * a per-process Set guard before reaching the LockProvider.
734
+ */
735
+ protected acquireCronLock(registration: JobRuntimeRegistration): Promise<boolean>;
736
+ /**
737
+ * Update only when the row is still in one of the expected statuses.
738
+ * Logs and returns silently when the guard rejects — this happens when a
739
+ * concurrent operation (most often `cancel()`) has already moved the row
740
+ * into a terminal state. We must not overwrite that.
741
+ */
742
+ protected guardedUpdate(executionId: string, expectedStatuses: JobStatus[], patch: Parameters<typeof this.executions.updateById>[1], label: string): Promise<void>;
743
+ protected releaseCronLock(registration: JobRuntimeRegistration): Promise<void>;
744
+ protected cronLockKey(jobName: string): string;
745
+ /**
746
+ * Stable per-process id used as the lock value — survives multiple ticks.
747
+ * Lazy so that Cloudflare Workers (which forbid random in global scope)
748
+ * stay happy.
749
+ */
750
+ protected lockHolderIdValue?: string;
751
+ protected get lockHolderId(): string;
551
752
  /**
552
753
  * Execute a cron handler inline. Records a row only on error (or always,
553
754
  * when `record: 'all'`). No DB writes on the happy path by default.
@@ -576,17 +777,65 @@ declare class JobProvider {
576
777
  */
577
778
  protected scheduleOptimisticDispatch(jobName: string, executionId: string, scheduledAt: string): void;
578
779
  pushMany(name: string, items: Array<PushManyItem>): Promise<string[]>;
579
- protected dispatchToQueue(jobName: string, executionId: string): Promise<void>;
780
+ /**
781
+ * Hand a single execution to the active `JobDispatcher`. Whether that
782
+ * results in a queue send or in-process execution depends on which
783
+ * dispatcher is wired (see {@link JobDispatcher}).
784
+ */
785
+ protected dispatch(jobName: string, executionId: string): Promise<void>;
786
+ /**
787
+ * Batched variant. Used by `pushMany` so a backing queue can do a single
788
+ * batch network call (e.g. Cloudflare Queues `sendBatch`).
789
+ */
790
+ protected dispatchMany(items: Array<{
791
+ jobName: string;
792
+ executionId: string;
793
+ }>): Promise<void>;
580
794
  trigger(name: string, context?: JobTriggerContext): Promise<void>;
581
795
  cancel(executionId: string, context?: CancelContext): Promise<void>;
582
796
  processExecution(jobName: string, executionId: string): Promise<void>;
583
797
  protected processQueueExecution(registration: JobRuntimeRegistration, executionId: string): Promise<void>;
584
- protected claim(executionId: string): Promise<boolean>;
798
+ /**
799
+ * Transition pending → running and return the post-update row.
800
+ * Two round-trips: read current attempt, then guarded UPDATE … RETURNING.
801
+ * Returns null when the row is gone or already claimed by another worker.
802
+ * The returned row replaces a separate post-claim findById, so the dispatch
803
+ * path is 2 queries instead of 3.
804
+ */
805
+ protected claim(executionId: string): Promise<{
806
+ error?: string | undefined;
807
+ key?: string | null | undefined;
808
+ payload?: Record<string, any> | undefined;
809
+ scheduledAt?: string | undefined;
810
+ startedAt?: string | undefined;
811
+ completedAt?: string | undefined;
812
+ logs?: {
813
+ context?: string | undefined;
814
+ app?: string | undefined;
815
+ data?: any;
816
+ level: "SILENT" | "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR";
817
+ message: string;
818
+ service: string;
819
+ module: string;
820
+ timestamp: number;
821
+ }[] | undefined;
822
+ triggeredBy?: string | undefined;
823
+ triggeredByName?: string | undefined;
824
+ cancelledBy?: string | undefined;
825
+ cancelledByName?: string | undefined;
826
+ priority: number;
827
+ status: "ok" | "error" | "pending" | "running" | "scheduled" | "cancelled";
828
+ id: string;
829
+ createdAt: string;
830
+ updatedAt: string;
831
+ jobName: string;
832
+ attempt: number;
833
+ maxAttempts: number;
834
+ } | null>;
585
835
  protected handleFailure(executionId: string, registration: JobRuntimeRegistration, currentAttempt: number, error: Error, contextId: string): Promise<void>;
586
- protected computeBackoff(retry: JobRetryOptions, attempt: number): string;
587
836
  protected snapshotLogs(contextId: string): LogEntry[] | undefined;
588
837
  protected sweep(): Promise<void>;
589
- protected dispatchToQueueSafe(jobName: string, executionId: string): Promise<void>;
838
+ protected dispatchSafe(jobName: string, executionId: string): Promise<void>;
590
839
  /**
591
840
  * Move a row from `scheduled` → `pending` and dispatch it.
592
841
  * Used by the optimistic retry/delay timer. If the sweep has already moved
@@ -602,22 +851,68 @@ declare class JobProvider {
602
851
  //#endregion
603
852
  //#region ../../src/api/jobs/schemas/jobExecutionQuerySchema.d.ts
604
853
  declare const jobExecutionQuerySchema: _$alepha.TObject<{
605
- status: _$alepha.TOptional<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">>;
854
+ status: _$alepha.TOptional<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">>;
606
855
  limit: _$alepha.TOptional<_$alepha.TInteger>;
607
856
  }>;
608
857
  type JobExecutionQuery = Static<typeof jobExecutionQuerySchema>;
609
858
  //#endregion
859
+ //#region ../../src/api/jobs/schemas/jobExecutionResourceSchema.d.ts
860
+ /**
861
+ * Public-facing schema for a job execution row.
862
+ *
863
+ * Diverges from the raw entity in two places, both for API ergonomics:
864
+ *
865
+ * - `priority` is exposed as the **string enum** (`critical`/`high`/...)
866
+ * instead of the numeric value used internally for SQL ordering. The
867
+ * `JobService` is responsible for the int → string transform.
868
+ * - `can` derives the available admin actions from the row's status.
869
+ */
870
+ declare const jobExecutionResourceSchema: _$alepha.TObject<{
871
+ id: PgAttr<PgAttr<_$alepha.TString, typeof PG_PRIMARY_KEY>, typeof PG_DEFAULT>;
872
+ createdAt: PgAttr<PgAttr<_$alepha.TString, typeof PG_CREATED_AT>, typeof PG_DEFAULT>;
873
+ updatedAt: PgAttr<PgAttr<_$alepha.TString, typeof PG_UPDATED_AT>, typeof PG_DEFAULT>;
874
+ jobName: _$alepha.TString;
875
+ key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
876
+ status: PgAttr<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">, typeof PG_DEFAULT>;
877
+ priority: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
878
+ attempt: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
879
+ maxAttempts: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
880
+ payload: _$alepha.TOptional<_$alepha.TRecord<"^.*$", _$alepha.TAny>>;
881
+ scheduledAt: _$alepha.TOptional<_$alepha.TString>;
882
+ startedAt: _$alepha.TOptional<_$alepha.TString>;
883
+ completedAt: _$alepha.TOptional<_$alepha.TString>;
884
+ error: _$alepha.TOptional<_$alepha.TString>;
885
+ logs: _$alepha.TOptional<_$alepha.TArray<_$alepha.TObject<{
886
+ level: _$alepha.TUnsafe<"SILENT" | "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR">;
887
+ message: _$alepha.TString;
888
+ service: _$alepha.TString;
889
+ module: _$alepha.TString;
890
+ context: _$alepha.TOptional<_$alepha.TString>;
891
+ app: _$alepha.TOptional<_$alepha.TString>;
892
+ data: _$alepha.TOptional<_$alepha.TAny>;
893
+ timestamp: _$alepha.TNumber;
894
+ }>>>;
895
+ triggeredBy: _$alepha.TOptional<_$alepha.TString>;
896
+ triggeredByName: _$alepha.TOptional<_$alepha.TString>;
897
+ cancelledBy: _$alepha.TOptional<_$alepha.TString>;
898
+ cancelledByName: _$alepha.TOptional<_$alepha.TString>;
899
+ can: _$alepha.TObject<{
900
+ retry: _$alepha.TBoolean;
901
+ cancel: _$alepha.TBoolean;
902
+ }>;
903
+ }>;
904
+ type JobExecutionResource = Static<typeof jobExecutionResourceSchema>;
905
+ //#endregion
610
906
  //#region ../../src/api/jobs/schemas/jobRegistrationSchema.d.ts
611
907
  declare const jobRegistrationSchema: _$alepha.TObject<{
612
908
  name: _$alepha.TString;
613
909
  description: _$alepha.TOptional<_$alepha.TString>;
614
- type: _$alepha.TUnsafe<"cron" | "queue">;
910
+ type: _$alepha.TUnsafe<"cron" | "queue" | "direct">;
615
911
  priority: _$alepha.TUnsafe<"critical" | "high" | "normal" | "low">;
616
912
  cron: _$alepha.TOptional<_$alepha.TString>;
617
913
  timeout: _$alepha.TOptional<_$alepha.TString>;
618
914
  retry: _$alepha.TOptional<_$alepha.TObject<{
619
915
  retries: _$alepha.TInteger;
620
- hasBackoff: _$alepha.TBoolean;
621
916
  }>>;
622
917
  recent: _$alepha.TObject<{
623
918
  ok: _$alepha.TInteger;
@@ -646,7 +941,7 @@ declare class JobService {
646
941
  updatedAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_UPDATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
647
942
  jobName: _$alepha.TString;
648
943
  key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
649
- status: _$alepha_orm0.PgAttr<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">, typeof _$alepha_orm0.PG_DEFAULT>;
944
+ status: _$alepha_orm0.PgAttr<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">, typeof _$alepha_orm0.PG_DEFAULT>;
650
945
  priority: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
651
946
  attempt: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
652
947
  maxAttempts: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
@@ -674,6 +969,16 @@ declare class JobService {
674
969
  retry: boolean;
675
970
  cancel: boolean;
676
971
  };
972
+ /**
973
+ * Convert the int-priority storage column into the public enum string.
974
+ * The cast through `unknown` skips TypeScript's structural check between
975
+ * the entity-level row (`priority: number`) and the resource schema
976
+ * (`priority: enum`); the runtime values are correct.
977
+ */
978
+ protected toResource<T extends {
979
+ priority: number;
980
+ status: string;
981
+ }>(row: T): JobExecutionResource;
677
982
  /**
678
983
  * List every registered job with recent ok/error counts and lastRun.
679
984
  * One aggregate query covers all jobs.
@@ -683,12 +988,8 @@ declare class JobService {
683
988
  * Recent executions for a single job, ORDER BY startedAt DESC.
684
989
  */
685
990
  getExecutions(jobName: string, query?: JobExecutionQuery): Promise<{
686
- can: {
687
- retry: boolean;
688
- cancel: boolean;
689
- };
690
- key?: string | null | undefined;
691
991
  error?: string | undefined;
992
+ key?: string | null | undefined;
692
993
  payload?: Record<string, any> | undefined;
693
994
  scheduledAt?: string | undefined;
694
995
  startedAt?: string | undefined;
@@ -707,25 +1008,25 @@ declare class JobService {
707
1008
  triggeredByName?: string | undefined;
708
1009
  cancelledBy?: string | undefined;
709
1010
  cancelledByName?: string | undefined;
1011
+ priority: number;
1012
+ status: "ok" | "error" | "pending" | "running" | "scheduled" | "cancelled";
710
1013
  id: string;
711
1014
  createdAt: string;
712
1015
  updatedAt: string;
713
1016
  jobName: string;
714
- status: "pending" | "running" | "scheduled" | "ok" | "error" | "cancelled";
715
- priority: number;
716
1017
  attempt: number;
717
1018
  maxAttempts: number;
1019
+ can: {
1020
+ retry: boolean;
1021
+ cancel: boolean;
1022
+ };
718
1023
  }[]>;
719
1024
  /**
720
1025
  * Full execution detail (includes captured logs).
721
1026
  */
722
1027
  getExecution(id: string): Promise<{
723
- can: {
724
- retry: boolean;
725
- cancel: boolean;
726
- };
727
- key?: string | null | undefined;
728
1028
  error?: string | undefined;
1029
+ key?: string | null | undefined;
729
1030
  payload?: Record<string, any> | undefined;
730
1031
  scheduledAt?: string | undefined;
731
1032
  startedAt?: string | undefined;
@@ -744,14 +1045,18 @@ declare class JobService {
744
1045
  triggeredByName?: string | undefined;
745
1046
  cancelledBy?: string | undefined;
746
1047
  cancelledByName?: string | undefined;
1048
+ priority: number;
1049
+ status: "ok" | "error" | "pending" | "running" | "scheduled" | "cancelled";
747
1050
  id: string;
748
1051
  createdAt: string;
749
1052
  updatedAt: string;
750
1053
  jobName: string;
751
- status: "pending" | "running" | "scheduled" | "ok" | "error" | "cancelled";
752
- priority: number;
753
1054
  attempt: number;
754
1055
  maxAttempts: number;
1056
+ can: {
1057
+ retry: boolean;
1058
+ cancel: boolean;
1059
+ };
755
1060
  }>;
756
1061
  /**
757
1062
  * Manual trigger (cron jobs) or push-with-payload (queue jobs).
@@ -788,13 +1093,12 @@ declare class AdminJobController {
788
1093
  response: _$alepha.TArray<_$alepha.TObject<{
789
1094
  name: _$alepha.TString;
790
1095
  description: _$alepha.TOptional<_$alepha.TString>;
791
- type: _$alepha.TUnsafe<"cron" | "queue">;
1096
+ type: _$alepha.TUnsafe<"cron" | "queue" | "direct">;
792
1097
  priority: _$alepha.TUnsafe<"critical" | "high" | "normal" | "low">;
793
1098
  cron: _$alepha.TOptional<_$alepha.TString>;
794
1099
  timeout: _$alepha.TOptional<_$alepha.TString>;
795
1100
  retry: _$alepha.TOptional<_$alepha.TObject<{
796
1101
  retries: _$alepha.TInteger;
797
- hasBackoff: _$alepha.TBoolean;
798
1102
  }>>;
799
1103
  recent: _$alepha.TObject<{
800
1104
  ok: _$alepha.TInteger;
@@ -808,7 +1112,7 @@ declare class AdminJobController {
808
1112
  name: _$alepha.TString;
809
1113
  }>;
810
1114
  query: _$alepha.TObject<{
811
- status: _$alepha.TOptional<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">>;
1115
+ status: _$alepha.TOptional<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">>;
812
1116
  limit: _$alepha.TOptional<_$alepha.TInteger>;
813
1117
  }>;
814
1118
  response: _$alepha.TArray<_$alepha.TObject<{
@@ -817,7 +1121,7 @@ declare class AdminJobController {
817
1121
  updatedAt: PgAttr<PgAttr<_$alepha.TString, typeof PG_UPDATED_AT>, typeof PG_DEFAULT>;
818
1122
  jobName: _$alepha.TString;
819
1123
  key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
820
- status: PgAttr<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">, typeof PG_DEFAULT>;
1124
+ status: PgAttr<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">, typeof PG_DEFAULT>;
821
1125
  priority: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
822
1126
  attempt: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
823
1127
  maxAttempts: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
@@ -856,7 +1160,7 @@ declare class AdminJobController {
856
1160
  updatedAt: PgAttr<PgAttr<_$alepha.TString, typeof PG_UPDATED_AT>, typeof PG_DEFAULT>;
857
1161
  jobName: _$alepha.TString;
858
1162
  key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
859
- status: PgAttr<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">, typeof PG_DEFAULT>;
1163
+ status: PgAttr<_$alepha.TUnsafe<"ok" | "error" | "pending" | "running" | "scheduled" | "cancelled">, typeof PG_DEFAULT>;
860
1164
  priority: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
861
1165
  attempt: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
862
1166
  maxAttempts: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
@@ -920,79 +1224,77 @@ declare class AdminJobController {
920
1224
  }>;
921
1225
  }
922
1226
  //#endregion
923
- //#region ../../src/api/jobs/entities/jobExecutionEntity.d.ts
1227
+ //#region ../../src/api/jobs/providers/DirectJobDispatcher.d.ts
924
1228
  /**
925
- * Job execution record.
1229
+ * Default `JobDispatcher` for environments without `AlephaApiJobsQueue`.
926
1230
  *
927
- * Stores durable state for queue-mode jobs (outbox pattern) and error records
928
- * for cron-mode jobs. Successful executions are trimmed by the sweep to keep
929
- * the last N rows per job (configurable via `jobConfig.keepLastSuccess`).
1231
+ * Runs `JobProvider.processExecution` in the background the caller's
1232
+ * `push()` returns immediately while the handler continues to completion
1233
+ * in the same process. The DB outbox row is the durability guarantee:
1234
+ * if the process dies before the handler finishes, the next sweep tick
1235
+ * picks the row up and re-dispatches.
930
1236
  *
931
- * Status transitions:
932
- * - queue push → pending
933
- * - worker claim → running
934
- * - success → ok
935
- * - terminal failure → error
936
- * - retry → scheduled (with scheduledAt = now + backoff)
937
- * - delay → scheduled (with scheduledAt = now + delay)
938
- * - sweep picks due ones → pending
939
- * - cancel → cancelled
1237
+ * **Cloudflare Workers** — when an `executionCtx.waitUntil` is available
1238
+ * in the alepha store at `cloudflare.waitUntil`, the dispatch wraps the
1239
+ * background promise with `waitUntil` so the runtime keeps the isolate
1240
+ * alive past the HTTP response. Without this, the handler would be
1241
+ * terminated when the response is returned and only the next sweep
1242
+ * (every 5 min by default) would re-dispatch.
1243
+ *
1244
+ * **Vercel / single-Node** on long-running runtimes the event loop
1245
+ * keeps the promise alive naturally; no special wiring is required.
940
1246
  */
941
- declare const jobExecutionEntity: _$alepha_orm0.EntityPrimitive<_$alepha.TObject<{
942
- id: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_PRIMARY_KEY>, typeof _$alepha_orm0.PG_DEFAULT>;
943
- createdAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_CREATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
944
- updatedAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$alepha.TString, typeof _$alepha_orm0.PG_UPDATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
945
- jobName: _$alepha.TString;
946
- key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
947
- status: _$alepha_orm0.PgAttr<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">, typeof _$alepha_orm0.PG_DEFAULT>;
948
- priority: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
949
- attempt: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
950
- maxAttempts: _$alepha_orm0.PgAttr<_$alepha.TInteger, typeof _$alepha_orm0.PG_DEFAULT>;
951
- payload: _$alepha.TOptional<_$alepha.TRecord<"^.*$", _$alepha.TAny>>;
952
- scheduledAt: _$alepha.TOptional<_$alepha.TString>;
953
- startedAt: _$alepha.TOptional<_$alepha.TString>;
954
- completedAt: _$alepha.TOptional<_$alepha.TString>;
955
- error: _$alepha.TOptional<_$alepha.TString>;
956
- logs: _$alepha.TOptional<_$alepha.TArray<_$alepha.TObject<{
957
- level: _$alepha.TUnsafe<"SILENT" | "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR">;
958
- message: _$alepha.TString;
959
- service: _$alepha.TString;
960
- module: _$alepha.TString;
961
- context: _$alepha.TOptional<_$alepha.TString>;
962
- app: _$alepha.TOptional<_$alepha.TString>;
963
- data: _$alepha.TOptional<_$alepha.TAny>;
964
- timestamp: _$alepha.TNumber;
965
- }>>>;
966
- triggeredBy: _$alepha.TOptional<_$alepha.TString>;
967
- triggeredByName: _$alepha.TOptional<_$alepha.TString>;
968
- cancelledBy: _$alepha.TOptional<_$alepha.TString>;
969
- cancelledByName: _$alepha.TOptional<_$alepha.TString>;
970
- }>>;
971
- type JobExecutionEntity = Static<typeof jobExecutionEntity.schema>;
972
- type JobStatus = "pending" | "running" | "scheduled" | "ok" | "error" | "cancelled";
1247
+ declare class DirectJobDispatcher extends JobDispatcher {
1248
+ readonly kind: "direct";
1249
+ protected readonly alepha: Alepha;
1250
+ protected readonly log: _$alepha_logger0.Logger;
1251
+ protected jobProviderRef?: JobProvider;
1252
+ protected getJobProvider(): JobProvider;
1253
+ dispatch(jobName: string, executionId: string): Promise<void>;
1254
+ }
973
1255
  //#endregion
974
1256
  //#region ../../src/api/jobs/providers/JobQueueProvider.d.ts
975
1257
  /**
976
- * Plumbs outbox-style dispatch through `AlephaQueue`.
1258
+ * Queue-backed `JobDispatcher` registered by `AlephaApiJobsQueue`.
1259
+ *
1260
+ * Extends {@link JobDispatcher} and substitutes the default
1261
+ * `DirectJobDispatcher` so that `$job.push()` is delivered through
1262
+ * `AlephaQueue` (e.g. Cloudflare Queues, Redis, in-memory) instead of
1263
+ * being processed in-process.
977
1264
  *
978
- * Registered only when the app imports `AlephaApiJobsQueue`. Sets
979
- * `JobProvider.queueDispatch` eagerly at instantiation so queue-mode jobs
980
- * can dispatch regardless of start-hook ordering.
1265
+ * The class is also kept as a `JobQueueProvider` export name for backwards
1266
+ * compatibility it has always been the queue path's entry point.
981
1267
  */
982
- declare class JobQueueProvider {
983
- protected readonly jobProvider: JobProvider;
1268
+ declare class JobQueueProvider extends JobDispatcher {
1269
+ readonly kind: "queue";
1270
+ protected readonly alepha: Alepha;
1271
+ protected jobProviderRef?: JobProvider;
1272
+ protected getJobProvider(): JobProvider;
984
1273
  protected readonly queue: _$alepha_queue0.QueuePrimitive<_$alepha.TObject<{
985
1274
  jobName: _$alepha.TString;
986
1275
  executionId: _$alepha.TString;
987
1276
  }>>;
988
- constructor();
989
- protected wireDispatcher(): void;
1277
+ dispatch(jobName: string, executionId: string): Promise<void>;
1278
+ /**
1279
+ * Fan-out to a single variadic `queue.push(...payloads)` call so the
1280
+ * underlying queue provider can batch the network round-trips when it
1281
+ * supports it (Cloudflare Queues, Redis pipelines).
1282
+ */
1283
+ dispatchMany(items: Array<{
1284
+ jobName: string;
1285
+ executionId: string;
1286
+ }>): Promise<void>;
1287
+ /**
1288
+ * Backwards-compatible alias for {@link dispatch}. Older code paths called
1289
+ * `JobQueueProvider.push(jobName, executionId)` directly; new code should
1290
+ * go through the `JobDispatcher.dispatch` API.
1291
+ */
990
1292
  push(jobName: string, executionId: string): Promise<void>;
991
1293
  }
992
1294
  //#endregion
993
1295
  //#region ../../src/api/jobs/schemas/jobConfigAtom.d.ts
994
1296
  declare const jobConfig: _$alepha.Atom<_$alepha.TObject<{
995
- sweepInterval: _$alepha.TInteger;
1297
+ sweepCron: _$alepha.TString;
996
1298
  staleThreshold: _$alepha.TInteger;
997
1299
  runTimeout: _$alepha.TInteger;
998
1300
  keepLastSuccess: _$alepha.TInteger;
@@ -1007,43 +1309,6 @@ declare module "alepha" {
1007
1309
  }
1008
1310
  } //# sourceMappingURL=jobConfigAtom.d.ts.map
1009
1311
  //#endregion
1010
- //#region ../../src/api/jobs/schemas/jobExecutionResourceSchema.d.ts
1011
- declare const jobExecutionResourceSchema: _$alepha.TObject<{
1012
- id: PgAttr<PgAttr<_$alepha.TString, typeof PG_PRIMARY_KEY>, typeof PG_DEFAULT>;
1013
- createdAt: PgAttr<PgAttr<_$alepha.TString, typeof PG_CREATED_AT>, typeof PG_DEFAULT>;
1014
- updatedAt: PgAttr<PgAttr<_$alepha.TString, typeof PG_UPDATED_AT>, typeof PG_DEFAULT>;
1015
- jobName: _$alepha.TString;
1016
- key: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TNull, _$alepha.TString]>>;
1017
- status: PgAttr<_$alepha.TUnsafe<"pending" | "running" | "scheduled" | "ok" | "error" | "cancelled">, typeof PG_DEFAULT>;
1018
- priority: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
1019
- attempt: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
1020
- maxAttempts: PgAttr<_$alepha.TInteger, typeof PG_DEFAULT>;
1021
- payload: _$alepha.TOptional<_$alepha.TRecord<"^.*$", _$alepha.TAny>>;
1022
- scheduledAt: _$alepha.TOptional<_$alepha.TString>;
1023
- startedAt: _$alepha.TOptional<_$alepha.TString>;
1024
- completedAt: _$alepha.TOptional<_$alepha.TString>;
1025
- error: _$alepha.TOptional<_$alepha.TString>;
1026
- logs: _$alepha.TOptional<_$alepha.TArray<_$alepha.TObject<{
1027
- level: _$alepha.TUnsafe<"SILENT" | "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR">;
1028
- message: _$alepha.TString;
1029
- service: _$alepha.TString;
1030
- module: _$alepha.TString;
1031
- context: _$alepha.TOptional<_$alepha.TString>;
1032
- app: _$alepha.TOptional<_$alepha.TString>;
1033
- data: _$alepha.TOptional<_$alepha.TAny>;
1034
- timestamp: _$alepha.TNumber;
1035
- }>>>;
1036
- triggeredBy: _$alepha.TOptional<_$alepha.TString>;
1037
- triggeredByName: _$alepha.TOptional<_$alepha.TString>;
1038
- cancelledBy: _$alepha.TOptional<_$alepha.TString>;
1039
- cancelledByName: _$alepha.TOptional<_$alepha.TString>;
1040
- can: _$alepha.TObject<{
1041
- retry: _$alepha.TBoolean;
1042
- cancel: _$alepha.TBoolean;
1043
- }>;
1044
- }>;
1045
- type JobExecutionResource = Static<typeof jobExecutionResourceSchema>;
1046
- //#endregion
1047
1312
  //#region ../../src/api/jobs/schemas/triggerJobSchema.d.ts
1048
1313
  declare const triggerJobSchema: _$alepha.TObject<{
1049
1314
  payload: _$alepha.TOptional<_$alepha.TRecord<"^.*$", _$alepha.TAny>>;
@@ -1080,27 +1345,59 @@ declare module "alepha" {
1080
1345
  /**
1081
1346
  * Job execution framework — cron and durable queue work with a single primitive.
1082
1347
  *
1083
- * A `$job` is either **cron-only** (declares `cron`) or **queue-only** (declares `schema`).
1084
- * Cron jobs run inline on their schedule and only record errors by default.
1085
- * Queue jobs use the outbox pattern: push commits to DB first, then notifies via queue.
1348
+ * A `$job` is either **cron-only** (declares `cron`) or **payload-only** (declares `schema`).
1349
+ *
1350
+ * **Three runtime modes:**
1351
+ *
1352
+ * - **cron** — fires on a schedule. Cron-mode jobs are protected by a
1353
+ * distributed lock by default (`lock: true`), so multi-replica Docker
1354
+ * deployments only run the handler once per tick. Override with
1355
+ * `lock: false` if you genuinely want every replica to fire.
1356
+ * - **queue** — push-driven, dispatched through the queue infrastructure
1357
+ * (`AlephaQueue`, e.g. Cloudflare Queues, Redis). Real-time delivery,
1358
+ * ideal for high-volume systems. Requires `AlephaApiJobsQueue`.
1359
+ * - **direct** — push-driven, processed in-process right after the caller
1360
+ * awaits the push. The DB outbox row is the durability guarantee — if
1361
+ * the process dies, the reconciliation sweep re-dispatches. Default
1362
+ * when `AlephaApiJobsQueue` is *not* loaded. Best for cheap deployments
1363
+ * (Cloudflare Workers, single-instance Node) where standing up a queue
1364
+ * is overkill.
1365
+ *
1366
+ * **Retries** are sweep-driven across all modes (no exponential backoff).
1367
+ * Granularity is bounded by `sweepCron` (default 5 min). The first retry
1368
+ * may land anywhere from a few seconds to ~5 min later depending on when
1369
+ * the next sweep tick fires. Cron jobs that declare `retry` go through
1370
+ * the same sweep path — a transient failure no longer means waiting for
1371
+ * the next cron tick (useful for once-daily jobs).
1372
+ *
1373
+ * **Runtime support for cron triggers**
1086
1374
  *
1087
- * **This module provides cron support only.** To enable queue-mode jobs, also
1088
- * import {@link AlephaApiJobsQueue} it brings in the queue layer and infrastructure
1089
- * binding (e.g. Cloudflare Queues). Cron-only deployments (Vercel, CF-without-Queues)
1090
- * do not need `AlephaApiJobsQueue`.
1375
+ * - **Long-running Node / Docker** `CronProvider` runs an in-process
1376
+ * timer loop. Multi-replica deployments serialize ticks via the cron
1377
+ * lock (see `$job.lock`).
1378
+ * - **Cloudflare Workers** — the build emits cron expressions into
1379
+ * `wrangler.jsonc`; Cloudflare invokes the worker on schedule and the
1380
+ * `cloudflare:scheduled` hook routes the event to the matching jobs.
1381
+ * - **Vercel** — the build emits cron entries into
1382
+ * `.vercel/output/config.json` mapped to `/_alepha/cron/:name`; the
1383
+ * serverless handler emits `serverless:cron` and `CronProvider` runs
1384
+ * the matching job. Set `CRON_SECRET` to require authenticated calls.
1091
1385
  *
1092
1386
  * @module alepha.api.jobs
1093
1387
  */
1094
1388
  declare const AlephaApiJobs: _$alepha.Service<_$alepha.Module>;
1095
1389
  /**
1096
1390
  * Queue support for `$job`. Import alongside {@link AlephaApiJobs} when your
1097
- * app declares queue-mode jobs (any `$job` with a `schema`).
1391
+ * app declares queue-mode jobs (any `$job` with a `schema`) and you want a
1392
+ * real queue (e.g. Cloudflare Queues, Redis) instead of in-process direct
1393
+ * execution.
1098
1394
  *
1099
- * Adds `JobQueueProvider` which plumbs the outbox dispatch through `AlephaQueue`.
1395
+ * Adds `JobQueueProvider` to the container. `JobProvider` detects its
1396
+ * presence at start-up and routes dispatches through it.
1100
1397
  *
1101
1398
  * @module alepha.api.jobs.queue
1102
1399
  */
1103
1400
  declare const AlephaApiJobsQueue: _$alepha.Service<_$alepha.Module>;
1104
1401
  //#endregion
1105
- export { $job, AdminJobController, AlephaApiJobs, AlephaApiJobsQueue, CancelContext, JobConfig, JobExecutionEntity, JobExecutionQuery, JobExecutionResource, JobHandlerArgs, JobPrimitive, JobPrimitiveOptions, JobPriority, JobProvider, JobQueueProvider, JobRegistration, JobRetryBackoff, JobRetryOptions, JobService, JobStatus, JobTriggerContext, PRIORITY_MAP, PRIORITY_REVERSE, PushManyItem, PushOptions, TriggerJob, jobConfig, jobExecutionEntity, jobExecutionQuerySchema, jobExecutionResourceSchema, jobRegistrationSchema, triggerJobSchema };
1402
+ export { $job, AdminJobController, AlephaApiJobs, AlephaApiJobsQueue, CancelContext, DirectJobDispatcher, JobConfig, JobDispatcher, JobEffectiveMode, JobExecutionEntity, JobExecutionQuery, JobExecutionResource, JobHandlerArgs, JobPrimitive, JobPrimitiveOptions, JobPriority, JobProvider, JobQueueProvider, JobRegistration, JobRetryOptions, JobService, JobStatus, JobTriggerContext, PRIORITY_MAP, PRIORITY_REVERSE, PushManyItem, PushOptions, TriggerJob, jobConfig, jobExecutionEntity, jobExecutionQuerySchema, jobExecutionResourceSchema, jobRegistrationSchema, triggerJobSchema };
1106
1403
  //# sourceMappingURL=index.d.ts.map