alepha 0.19.3 → 0.19.4

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 (215) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -1,6 +1,5 @@
1
1
  import { $atom, $hook, $inject, $module, $state, Alepha, AlephaError, KIND, PipelinePrimitive, createPrimitive, t } from "alepha";
2
- import { AlephaBatch } from "alepha/batch";
3
- import { AlephaLock } from "alepha/lock";
2
+ import { AlephaLock, LockProvider } from "alepha/lock";
4
3
  import { $queue, AlephaQueue } from "alepha/queue";
5
4
  import { AlephaScheduler, CronProvider } from "alepha/scheduler";
6
5
  import { $secure } from "alepha/security";
@@ -32,6 +31,7 @@ const jobCronInfoSchema = t.object({
32
31
  ]),
33
32
  concurrency: t.integer(),
34
33
  hasSchema: t.boolean(),
34
+ paused: t.boolean(),
35
35
  lastExecution: t.optional(t.object({
36
36
  id: t.uuid(),
37
37
  status: t.text(),
@@ -57,7 +57,6 @@ const jobExecutionEntity = $entity({
57
57
  "retrying",
58
58
  "running",
59
59
  "completed",
60
- "failed",
61
60
  "dead",
62
61
  "cancelled"
63
62
  ]), "pending"),
@@ -126,7 +125,6 @@ const jobExecutionQuerySchema = t.extend(pageQuerySchema, {
126
125
  "retrying",
127
126
  "running",
128
127
  "completed",
129
- "failed",
130
128
  "dead",
131
129
  "cancelled"
132
130
  ])),
@@ -155,7 +153,8 @@ const jobQueueDepthSchema = t.object({
155
153
  scheduled: t.integer(),
156
154
  retrying: t.integer(),
157
155
  dead: t.integer(),
158
- concurrency: t.integer()
156
+ concurrency: t.integer(),
157
+ paused: t.boolean()
159
158
  });
160
159
  //#endregion
161
160
  //#region ../../src/api/jobs/schemas/jobRegistrationSchema.ts
@@ -180,10 +179,7 @@ const jobRegistrationSchema = t.object({
180
179
  retries: t.integer(),
181
180
  hasBackoff: t.boolean()
182
181
  })),
183
- batch: t.optional(t.object({
184
- size: t.integer(),
185
- window: t.text()
186
- }))
182
+ paused: t.boolean()
187
183
  });
188
184
  //#endregion
189
185
  //#region ../../src/api/jobs/schemas/jobStatsSchema.ts
@@ -225,7 +221,8 @@ const jobConfig = $atom({
225
221
  }),
226
222
  delayed: t.object({ interval: t.integer({ description: "Sweep interval (ms)." }) }),
227
223
  logRetentionDays: t.integer({ description: "Days to keep completed/dead executions." }),
228
- logMaxEntries: t.integer({ description: "Max log entries captured per execution." })
224
+ logMaxEntries: t.integer({ description: "Max log entries captured per execution." }),
225
+ drainTimeout: t.integer({ description: "Max time (ms) to wait for in-flight jobs during shutdown." })
229
226
  }),
230
227
  default: {
231
228
  recovery: {
@@ -235,7 +232,8 @@ const jobConfig = $atom({
235
232
  },
236
233
  delayed: { interval: 3e5 },
237
234
  logRetentionDays: 30,
238
- logMaxEntries: 100
235
+ logMaxEntries: 100,
236
+ drainTimeout: 3e4
239
237
  }
240
238
  });
241
239
  //#endregion
@@ -256,11 +254,14 @@ var JobProvider = class JobProvider {
256
254
  alepha = $inject(Alepha);
257
255
  dt = $inject(DateTimeProvider);
258
256
  cronProvider = $inject(CronProvider);
257
+ lockProvider = $inject(LockProvider);
259
258
  config = $state(jobConfig);
260
259
  log = $logger();
261
260
  executions = $repository(jobExecutionEntity);
262
261
  executionLogs = $repository(jobExecutionLogEntity);
263
262
  jobs = /* @__PURE__ */ new Map();
263
+ pausedJobs = /* @__PURE__ */ new Set();
264
+ inFlight = /* @__PURE__ */ new Set();
264
265
  /**
265
266
  * When set, job executions are dispatched through a queue (e.g. `JobQueueProvider`).
266
267
  * When null, jobs execute inline (fire-and-forget). Useful for serverless environments.
@@ -346,8 +347,35 @@ var JobProvider = class JobProvider {
346
347
  return execution.id;
347
348
  }
348
349
  async pushMany(name, items) {
349
- const ids = [];
350
+ if (items.length === 0) return [];
351
+ const opts = this.getRegistration(name).options;
352
+ if (!opts.schema) throw new AlephaError(`Cannot push to job '${name}': no schema defined. Use trigger() for cron-only jobs.`);
353
+ const maxAttempts = (opts.retry?.retries ?? 0) + 1;
354
+ const keyed = [];
355
+ const bulkRows = [];
350
356
  for (const item of items) {
357
+ const validated = this.alepha.codec.validate(opts.schema, item.payload);
358
+ if (item.key) keyed.push({
359
+ ...item,
360
+ payload: validated
361
+ });
362
+ else {
363
+ const status = item.delay || item.scheduledAt ? "scheduled" : "pending";
364
+ let scheduledAt;
365
+ if (item.scheduledAt) scheduledAt = item.scheduledAt.toISOString();
366
+ else if (item.delay) scheduledAt = this.dt.now().add(this.dt.duration(item.delay)).toISOString();
367
+ bulkRows.push({
368
+ jobName: name,
369
+ payload: validated,
370
+ status,
371
+ priority: PRIORITY_MAP[item.priority ?? opts.priority ?? "normal"],
372
+ maxAttempts,
373
+ scheduledAt
374
+ });
375
+ }
376
+ }
377
+ const ids = [];
378
+ for (const item of keyed) {
351
379
  const id = await this.push(name, item.payload, {
352
380
  key: item.key,
353
381
  delay: item.delay,
@@ -356,6 +384,17 @@ var JobProvider = class JobProvider {
356
384
  });
357
385
  ids.push(id);
358
386
  }
387
+ if (bulkRows.length > 0) {
388
+ const created = await this.executions.createMany(bulkRows);
389
+ for (const exec of created) {
390
+ ids.push(exec.id);
391
+ if (exec.status === "pending" && !this.stopping) await this.scheduleProcessing(name, exec.id);
392
+ }
393
+ }
394
+ this.log.debug(`pushMany '${name}': ${ids.length} jobs created`, {
395
+ bulk: bulkRows.length,
396
+ keyed: keyed.length
397
+ });
359
398
  return ids;
360
399
  }
361
400
  async trigger(name, context) {
@@ -403,6 +442,19 @@ var JobProvider = class JobProvider {
403
442
  });
404
443
  }
405
444
  async scheduleProcessing(jobName, executionId) {
445
+ if (this.pausedJobs.has(jobName)) {
446
+ this.log.debug(`Job '${jobName}' is paused, deferring`, { executionId });
447
+ return;
448
+ }
449
+ const maxConcurrency = this.getRegistration(jobName).options.concurrency ?? 1;
450
+ const runningCount = await this.executions.count({
451
+ jobName: { eq: jobName },
452
+ status: { eq: "running" }
453
+ });
454
+ if (runningCount >= maxConcurrency) {
455
+ this.log.debug(`Job '${jobName}' at concurrency limit (${runningCount}/${maxConcurrency}), deferring`, { executionId });
456
+ return;
457
+ }
406
458
  if (this.queueDispatch) {
407
459
  this.log.debug(`Dispatching job '${jobName}' via queue`, { executionId });
408
460
  await this.queueDispatch(jobName, executionId);
@@ -412,6 +464,15 @@ var JobProvider = class JobProvider {
412
464
  }
413
465
  }
414
466
  async processExecution(jobName, executionId) {
467
+ const promise = this.processExecutionInner(jobName, executionId);
468
+ this.inFlight.add(promise);
469
+ try {
470
+ await promise;
471
+ } finally {
472
+ this.inFlight.delete(promise);
473
+ }
474
+ }
475
+ async processExecutionInner(jobName, executionId) {
415
476
  const registration = this.getRegistration(jobName);
416
477
  if (!await this.claim(executionId)) {
417
478
  this.log.debug(`Execution ${executionId} already claimed, skipping`);
@@ -483,12 +544,39 @@ var JobProvider = class JobProvider {
483
544
  name: jobName,
484
545
  executionId
485
546
  }, { catch: true });
547
+ await this.dispatchNextPending(jobName);
486
548
  }
487
549
  }, { context });
488
550
  } finally {
489
551
  this.logs.delete(context);
490
552
  }
491
553
  }
554
+ /**
555
+ * After a job finishes (success, failure, or cancel), dispatch any pending
556
+ * jobs that were deferred due to the concurrency limit.
557
+ */
558
+ async dispatchNextPending(jobName) {
559
+ if (this.stopping || this.pausedJobs.has(jobName)) return;
560
+ const registration = this.jobs.get(jobName);
561
+ if (!registration) return;
562
+ const available = (registration.options.concurrency ?? 1) - await this.executions.count({
563
+ jobName: { eq: jobName },
564
+ status: { eq: "running" }
565
+ });
566
+ if (available <= 0) return;
567
+ const pending = await this.executions.findMany({
568
+ where: {
569
+ jobName: { eq: jobName },
570
+ status: { eq: "pending" }
571
+ },
572
+ orderBy: {
573
+ column: "priority",
574
+ direction: "asc"
575
+ },
576
+ limit: available
577
+ });
578
+ for (const exec of pending) await this.scheduleProcessing(jobName, exec.id);
579
+ }
492
580
  async claim(executionId) {
493
581
  const execution = await this.executions.findById(executionId);
494
582
  if (!execution) return false;
@@ -606,13 +694,20 @@ var JobProvider = class JobProvider {
606
694
  async recoverySweep() {
607
695
  this.log.trace("Starting recovery sweep");
608
696
  if (this.stopping) return;
697
+ if (!await this.tryLock("_alepha:jobs:recovery-lock", 3e5)) return;
609
698
  try {
610
699
  const now = this.dt.now();
611
700
  const staleThreshold = now.subtract(this.config.recovery.staleThreshold, "millisecond").toISOString();
612
701
  const pendingWhere = this.executions.createQueryWhere();
613
702
  pendingWhere.status = { eq: "pending" };
614
703
  pendingWhere.createdAt = { lte: staleThreshold };
615
- const stalePending = await this.executions.findMany({ where: pendingWhere });
704
+ const stalePending = await this.executions.findMany({
705
+ where: pendingWhere,
706
+ orderBy: {
707
+ column: "priority",
708
+ direction: "asc"
709
+ }
710
+ });
616
711
  for (const exec of stalePending) {
617
712
  if (!this.jobs.has(exec.jobName)) continue;
618
713
  this.log.debug(`Recovery sweep: re-dispatching stale pending job ${exec.jobName} (${exec.id})`);
@@ -639,6 +734,8 @@ var JobProvider = class JobProvider {
639
734
  }
640
735
  } catch (e) {
641
736
  this.log.error("Recovery sweep failed", { error: e });
737
+ } finally {
738
+ await this.releaseLock("_alepha:jobs:recovery-lock");
642
739
  }
643
740
  }
644
741
  /**
@@ -651,12 +748,19 @@ var JobProvider = class JobProvider {
651
748
  async delayedDispatchSweep() {
652
749
  this.log.trace("Starting delayed dispatch sweep");
653
750
  if (this.stopping) return;
751
+ if (!await this.tryLock("_alepha:jobs:dispatch-lock", 6e4)) return;
654
752
  try {
655
753
  const now = this.dt.nowISOString();
656
754
  const where = this.executions.createQueryWhere();
657
755
  where.status = { inArray: ["scheduled", "retrying"] };
658
756
  where.scheduledAt = { lte: now };
659
- const ready = await this.executions.findMany({ where });
757
+ const ready = await this.executions.findMany({
758
+ where,
759
+ orderBy: {
760
+ column: "priority",
761
+ direction: "asc"
762
+ }
763
+ });
660
764
  for (const exec of ready) {
661
765
  if (!this.jobs.has(exec.jobName)) continue;
662
766
  await this.executions.updateById(exec.id, { status: "pending" });
@@ -664,6 +768,8 @@ var JobProvider = class JobProvider {
664
768
  }
665
769
  } catch (e) {
666
770
  this.log.error("Delayed dispatch sweep failed", { error: e });
771
+ } finally {
772
+ await this.releaseLock("_alepha:jobs:dispatch-lock");
667
773
  }
668
774
  }
669
775
  /**
@@ -683,18 +789,55 @@ var JobProvider = class JobProvider {
683
789
  "cancelled"
684
790
  ] };
685
791
  where.completedAt = { lte: cutoff };
686
- const old = await this.executions.findMany({ where });
687
- for (const exec of old) {
688
- try {
689
- await this.executionLogs.deleteById(exec.id);
690
- } catch {}
691
- await this.executions.deleteById(exec.id);
792
+ const expiredIds = await this.executions.findMany({
793
+ where,
794
+ columns: ["id"]
795
+ });
796
+ if (expiredIds.length > 0) {
797
+ const ids = expiredIds.map((e) => e.id);
798
+ await this.executionLogs.deleteMany({ id: { inArray: ids } });
799
+ await this.executions.deleteMany({ id: { inArray: ids } });
800
+ this.log.info(`Log purge: deleted ${ids.length} old execution records`);
692
801
  }
693
- if (old.length > 0) this.log.info(`Log purge: deleted ${old.length} old execution records`);
694
802
  } catch (e) {
695
803
  this.log.error("Log purge failed", { error: e });
696
804
  }
697
805
  }
806
+ pauseJob(name) {
807
+ this.getRegistration(name);
808
+ this.pausedJobs.add(name);
809
+ this.log.info(`Paused job '${name}'`);
810
+ }
811
+ async resumeJob(name) {
812
+ this.getRegistration(name);
813
+ this.pausedJobs.delete(name);
814
+ this.log.info(`Resumed job '${name}'`);
815
+ const pending = await this.executions.findMany({
816
+ where: {
817
+ jobName: { eq: name },
818
+ status: { eq: "pending" }
819
+ },
820
+ orderBy: {
821
+ column: "priority",
822
+ direction: "asc"
823
+ }
824
+ });
825
+ for (const exec of pending) await this.scheduleProcessing(name, exec.id);
826
+ }
827
+ isJobPaused(name) {
828
+ return this.pausedJobs.has(name);
829
+ }
830
+ getPausedJobs() {
831
+ return [...this.pausedJobs];
832
+ }
833
+ async tryLock(key, ttlMs) {
834
+ const lockValue = `${this.workerId},${this.dt.nowISOString()}`;
835
+ const [lockId] = (await this.lockProvider.set(key, lockValue, true, ttlMs)).split(",");
836
+ return lockId === this.workerId;
837
+ }
838
+ async releaseLock(key) {
839
+ await this.lockProvider.del(key);
840
+ }
698
841
  onStart = $hook({
699
842
  on: "start",
700
843
  handler: async () => {
@@ -729,7 +872,14 @@ var JobProvider = class JobProvider {
729
872
  on: "stop",
730
873
  handler: async () => {
731
874
  this.stopping = true;
732
- for (const controller of this.abortControllers.values()) controller.abort();
875
+ if (this.inFlight.size > 0) {
876
+ this.log.info(`Draining ${this.inFlight.size} in-flight job(s)...`);
877
+ await Promise.race([Promise.allSettled([...this.inFlight]), this.dt.wait([this.config.drainTimeout, "millisecond"])]);
878
+ }
879
+ if (this.abortControllers.size > 0) {
880
+ this.log.warn(`Aborting ${this.abortControllers.size} remaining job(s) after drain timeout`);
881
+ for (const controller of this.abortControllers.values()) controller.abort();
882
+ }
733
883
  }
734
884
  });
735
885
  getRegistration(name) {
@@ -741,7 +891,7 @@ var JobProvider = class JobProvider {
741
891
  //#endregion
742
892
  //#region ../../src/api/jobs/primitives/$job.ts
743
893
  /**
744
- * Job primitive for defining scheduled and on-demand tasks with payload validation, retry policies, and batching.
894
+ * Job primitive for defining scheduled and on-demand tasks with payload validation and retry policies.
745
895
  */
746
896
  const $job = (options) => {
747
897
  return createPrimitive(JobPrimitive, options);
@@ -783,6 +933,24 @@ var JobPrimitive = class extends PipelinePrimitive {
783
933
  async trigger(context) {
784
934
  return this.jobProvider.trigger(this.name, context);
785
935
  }
936
+ /**
937
+ * Pause this job. Pushed items are still accepted but processing is held.
938
+ */
939
+ pause() {
940
+ this.jobProvider.pauseJob(this.name);
941
+ }
942
+ /**
943
+ * Resume a paused job and dispatch any pending items.
944
+ */
945
+ async resume() {
946
+ return this.jobProvider.resumeJob(this.name);
947
+ }
948
+ /**
949
+ * Whether this job is currently paused.
950
+ */
951
+ get paused() {
952
+ return this.jobProvider.isJobPaused(this.name);
953
+ }
786
954
  };
787
955
  $job[KIND] = JobPrimitive;
788
956
  //#endregion
@@ -822,7 +990,7 @@ var JobService = class {
822
990
  COUNT(*) FILTER (WHERE ${e.status} = 'retrying') AS retrying,
823
991
  COUNT(*) FILTER (WHERE ${e.status} = 'dead') AS dead,
824
992
  COUNT(*) FILTER (WHERE ${e.status} = 'completed' AND ${e.completedAt} >= ${periodAgo}) AS completed_24h,
825
- COUNT(*) FILTER (WHERE ${e.status} IN ('dead', 'failed') AND ${e.completedAt} >= ${periodAgo}) AS failed_24h
993
+ COUNT(*) FILTER (WHERE ${e.status} = 'dead' AND ${e.completedAt} >= ${periodAgo}) AS failed_24h
826
994
  FROM ${e}
827
995
  `, t.object({
828
996
  running: t.string(),
@@ -867,10 +1035,7 @@ var JobService = class {
867
1035
  retries: opts.retry.retries,
868
1036
  hasBackoff: Boolean(opts.retry.backoff)
869
1037
  } : void 0,
870
- batch: opts.batch ? {
871
- size: opts.batch.size,
872
- window: String(opts.batch.window)
873
- } : void 0
1038
+ paused: this.jobProvider.isJobPaused(name)
874
1039
  };
875
1040
  result.push(registration);
876
1041
  }
@@ -944,6 +1109,23 @@ var JobService = class {
944
1109
  });
945
1110
  return { ok: true };
946
1111
  }
1112
+ pauseJob(name, context) {
1113
+ const job = this.alepha.primitives($job).find((j) => j.name === name);
1114
+ if (!job) throw new NotFoundError(`Job not found: ${name}`);
1115
+ this.log.info(`Pausing job '${name}'`, { pausedBy: context?.pausedByName ?? context?.pausedBy });
1116
+ job.pause();
1117
+ return { ok: true };
1118
+ }
1119
+ async resumeJob(name, context) {
1120
+ const job = this.alepha.primitives($job).find((j) => j.name === name);
1121
+ if (!job) throw new NotFoundError(`Job not found: ${name}`);
1122
+ this.log.info(`Resuming job '${name}'`, { resumedBy: context?.resumedByName ?? context?.resumedBy });
1123
+ await job.resume();
1124
+ return { ok: true };
1125
+ }
1126
+ getPausedJobs() {
1127
+ return this.jobProvider.getPausedJobs();
1128
+ }
947
1129
  async getCronJobs() {
948
1130
  const jobs = this.jobProvider.getRegisteredJobs();
949
1131
  const cronJobNames = [];
@@ -960,6 +1142,7 @@ var JobService = class {
960
1142
  priority: opts.priority ?? "normal",
961
1143
  concurrency: opts.concurrency ?? 1,
962
1144
  hasSchema: Boolean(opts.schema),
1145
+ paused: this.jobProvider.isJobPaused(name),
963
1146
  lastExecution: last ? {
964
1147
  id: last.id,
965
1148
  status: last.status,
@@ -1003,7 +1186,8 @@ var JobService = class {
1003
1186
  scheduled: Number(row?.scheduled ?? 0),
1004
1187
  retrying: Number(row?.retrying ?? 0),
1005
1188
  dead: Number(row?.dead ?? 0),
1006
- concurrency: reg.options.concurrency ?? 1
1189
+ concurrency: reg.options.concurrency ?? 1,
1190
+ paused: this.jobProvider.isJobPaused(name)
1007
1191
  });
1008
1192
  }
1009
1193
  return result;
@@ -1021,10 +1205,10 @@ var JobService = class {
1021
1205
  SELECT
1022
1206
  ds.date::text AS date,
1023
1207
  COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'completed'), 0) AS completed,
1024
- COALESCE(COUNT(*) FILTER (WHERE ${e.status} IN ('dead', 'failed')), 0) AS failed
1208
+ COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'dead'), 0) AS failed
1025
1209
  FROM date_series ds
1026
1210
  LEFT JOIN ${e} ON DATE(${e.completedAt}) = ds.date
1027
- AND ${e.status} IN ('completed', 'dead', 'failed')
1211
+ AND ${e.status} IN ('completed', 'dead')
1028
1212
  GROUP BY ds.date
1029
1213
  ORDER BY ds.date ASC
1030
1214
  `, t.object({
@@ -1040,11 +1224,7 @@ var JobService = class {
1040
1224
  async getActivitySqlite(days = 14) {
1041
1225
  const startDate = this.dt.now().subtract(days - 1, "day");
1042
1226
  const where = this.executions.createQueryWhere();
1043
- where.status = { inArray: [
1044
- "completed",
1045
- "dead",
1046
- "failed"
1047
- ] };
1227
+ where.status = { inArray: ["completed", "dead"] };
1048
1228
  where.completedAt = { gte: startDate.startOf("day").toISOString() };
1049
1229
  const executions = await this.executions.findMany({ where });
1050
1230
  const byDate = /* @__PURE__ */ new Map();
@@ -1077,7 +1257,7 @@ var JobService = class {
1077
1257
  COUNT(*) AS failures,
1078
1258
  (ARRAY_AGG(${e.error} ORDER BY ${e.completedAt} DESC))[1] AS last_error
1079
1259
  FROM ${e}
1080
- WHERE ${e.status} IN ('dead', 'failed')
1260
+ WHERE ${e.status} = 'dead'
1081
1261
  AND ${e.completedAt} >= ${periodAgoIso}
1082
1262
  GROUP BY ${e.jobName}
1083
1263
  ORDER BY failures DESC
@@ -1093,7 +1273,7 @@ var JobService = class {
1093
1273
  }
1094
1274
  async getTopFailuresSqlite(periodAgoIso) {
1095
1275
  const where = this.executions.createQueryWhere();
1096
- where.status = { inArray: ["dead", "failed"] };
1276
+ where.status = { eq: "dead" };
1097
1277
  where.completedAt = { gte: periodAgoIso };
1098
1278
  const failures = await this.executions.findMany({
1099
1279
  where,
@@ -1290,6 +1470,45 @@ var AdminJobController = class {
1290
1470
  },
1291
1471
  handler: ({ query }) => this.jobService.getTopFailures(query.days)
1292
1472
  });
1473
+ pauseJob = $action({
1474
+ method: "POST",
1475
+ path: `${this.url}/pause`,
1476
+ group: this.group,
1477
+ use: [$secure({ permissions: ["admin:job:trigger"] })],
1478
+ schema: {
1479
+ body: t.object({ name: t.text() }),
1480
+ response: okSchema
1481
+ },
1482
+ handler: ({ body, user }) => {
1483
+ return this.jobService.pauseJob(body.name, {
1484
+ pausedBy: user?.id,
1485
+ pausedByName: user?.name
1486
+ });
1487
+ }
1488
+ });
1489
+ resumeJob = $action({
1490
+ method: "POST",
1491
+ path: `${this.url}/resume`,
1492
+ group: this.group,
1493
+ use: [$secure({ permissions: ["admin:job:trigger"] })],
1494
+ schema: {
1495
+ body: t.object({ name: t.text() }),
1496
+ response: okSchema
1497
+ },
1498
+ handler: async ({ body, user }) => {
1499
+ return this.jobService.resumeJob(body.name, {
1500
+ resumedBy: user?.id,
1501
+ resumedByName: user?.name
1502
+ });
1503
+ }
1504
+ });
1505
+ getPausedJobs = $action({
1506
+ path: `${this.url}/paused`,
1507
+ group: this.group,
1508
+ use: [$secure({ permissions: ["admin:job:read"] })],
1509
+ schema: { response: t.array(t.text()) },
1510
+ handler: () => this.jobService.getPausedJobs()
1511
+ });
1293
1512
  };
1294
1513
  //#endregion
1295
1514
  //#region ../../src/api/jobs/providers/JobQueueProvider.ts
@@ -1351,7 +1570,6 @@ const AlephaApiJobs = $module({
1351
1570
  AlephaQueue,
1352
1571
  AlephaScheduler,
1353
1572
  AlephaLock,
1354
- AlephaBatch,
1355
1573
  JobProvider,
1356
1574
  JobQueueProvider,
1357
1575
  JobService,
@@ -1362,7 +1580,6 @@ const AlephaApiJobs = $module({
1362
1580
  const useQueue = env.ALEPHA_JOBS_QUEUE === 1 ? true : env.ALEPHA_JOBS_QUEUE === 0 ? false : !alepha.isServerless();
1363
1581
  alepha.with(AlephaScheduler);
1364
1582
  alepha.with(AlephaLock);
1365
- alepha.with(AlephaBatch);
1366
1583
  alepha.with(JobProvider);
1367
1584
  alepha.with(JobService);
1368
1585
  alepha.with(AdminJobController);