alepha 0.19.2 → 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 (241) 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 +90 -34
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +267 -44
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/notifications/index.browser.js +0 -1
  14. package/dist/api/notifications/index.browser.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.d.ts.map +1 -1
  17. package/dist/api/notifications/index.js +0 -1
  18. package/dist/api/notifications/index.js.map +1 -1
  19. package/dist/api/parameters/index.browser.js +112 -1
  20. package/dist/api/parameters/index.browser.js.map +1 -1
  21. package/dist/api/parameters/index.d.ts +90 -3
  22. package/dist/api/parameters/index.d.ts.map +1 -1
  23. package/dist/api/parameters/index.js +79 -12
  24. package/dist/api/parameters/index.js.map +1 -1
  25. package/dist/{billing → api/payments}/index.d.ts +67 -49
  26. package/dist/api/payments/index.d.ts.map +1 -0
  27. package/dist/{billing → api/payments}/index.js +108 -74
  28. package/dist/api/payments/index.js.map +1 -0
  29. package/dist/api/subscriptions/index.d.ts +1692 -0
  30. package/dist/api/subscriptions/index.d.ts.map +1 -0
  31. package/dist/api/subscriptions/index.js +1870 -0
  32. package/dist/api/subscriptions/index.js.map +1 -0
  33. package/dist/api/users/index.d.ts +27 -21
  34. package/dist/api/users/index.d.ts.map +1 -1
  35. package/dist/api/users/index.js +167 -34
  36. package/dist/api/users/index.js.map +1 -1
  37. package/dist/api/workflows/index.browser.js +246 -0
  38. package/dist/api/workflows/index.browser.js.map +1 -0
  39. package/dist/api/workflows/index.d.ts +1618 -0
  40. package/dist/api/workflows/index.d.ts.map +1 -0
  41. package/dist/api/workflows/index.js +1504 -0
  42. package/dist/api/workflows/index.js.map +1 -0
  43. package/dist/cli/config/index.d.ts +6 -28
  44. package/dist/cli/config/index.d.ts.map +1 -1
  45. package/dist/cli/config/index.js +5 -10
  46. package/dist/cli/config/index.js.map +1 -1
  47. package/dist/cli/core/index.d.ts +11669 -208
  48. package/dist/cli/core/index.d.ts.map +1 -1
  49. package/dist/cli/core/index.js +60 -69
  50. package/dist/cli/core/index.js.map +1 -1
  51. package/dist/cli/devtools/index.d.ts +5 -0
  52. package/dist/cli/devtools/index.d.ts.map +1 -1
  53. package/dist/cli/devtools/index.js +4 -0
  54. package/dist/cli/devtools/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +69 -64
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +6 -2
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +38 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +85 -26
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/core/index.browser.js +21 -2
  64. package/dist/core/index.browser.js.map +1 -1
  65. package/dist/core/index.d.ts +33 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +25 -2
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/index.native.js +25 -2
  70. package/dist/core/index.native.js.map +1 -1
  71. package/dist/core/index.workerd.js +25 -2
  72. package/dist/core/index.workerd.js.map +1 -1
  73. package/dist/email/smtp/index.js +24 -8
  74. package/dist/email/smtp/index.js.map +1 -1
  75. package/dist/logger/index.d.ts.map +1 -1
  76. package/dist/logger/index.js +1 -1
  77. package/dist/logger/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +25 -73
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +10 -32
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +25 -73
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts +2 -1
  89. package/dist/orm/postgres/index.d.ts.map +1 -1
  90. package/dist/orm/postgres/index.js +3 -3
  91. package/dist/orm/postgres/index.js.map +1 -1
  92. package/dist/react/router/index.browser.js +25 -3
  93. package/dist/react/router/index.browser.js.map +1 -1
  94. package/dist/react/router/index.d.ts +16 -1
  95. package/dist/react/router/index.d.ts.map +1 -1
  96. package/dist/react/router/index.js +25 -3
  97. package/dist/react/router/index.js.map +1 -1
  98. package/dist/security/index.d.ts +28 -0
  99. package/dist/security/index.d.ts.map +1 -1
  100. package/dist/security/index.js +28 -0
  101. package/dist/security/index.js.map +1 -1
  102. package/package.json +37 -20
  103. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  104. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  105. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  106. package/src/api/invitations/entities/invitations.ts +33 -0
  107. package/src/api/invitations/index.ts +65 -0
  108. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  109. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  110. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  111. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  112. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  113. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  114. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  115. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  116. package/src/api/invitations/services/InvitationService.ts +556 -0
  117. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  118. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  119. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  120. package/src/api/jobs/index.ts +0 -3
  121. package/src/api/jobs/primitives/$job.ts +22 -11
  122. package/src/api/jobs/providers/JobProvider.ts +239 -25
  123. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  124. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  125. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  126. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  127. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  128. package/src/api/jobs/services/JobService.ts +51 -12
  129. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  130. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  131. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  132. package/src/api/parameters/index.browser.ts +12 -0
  133. package/src/api/parameters/primitives/$parameter.ts +20 -3
  134. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  135. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  136. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  137. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  138. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  139. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  140. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  141. package/src/{billing → api/payments}/index.ts +31 -25
  142. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  143. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  144. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  145. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  146. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  147. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  148. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  149. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  150. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  151. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  152. package/src/api/subscriptions/index.ts +144 -0
  153. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  154. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  155. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  156. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  157. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  158. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  159. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  160. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  161. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  162. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  163. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  164. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  165. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  166. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  167. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  168. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  169. package/src/api/subscriptions/services/BillingService.ts +437 -0
  170. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  171. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  172. package/src/api/subscriptions/services/UsageService.ts +118 -0
  173. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  174. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  175. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  176. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  177. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  178. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  179. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  180. package/src/api/users/controllers/UserController.ts +3 -8
  181. package/src/api/users/notifications/UserNotifications.ts +23 -0
  182. package/src/api/users/schemas/loginSchema.ts +1 -1
  183. package/src/api/users/services/CredentialService.ts +51 -4
  184. package/src/api/users/services/RegistrationService.ts +38 -9
  185. package/src/api/users/services/SessionService.ts +62 -9
  186. package/src/api/users/services/UserService.ts +21 -12
  187. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  188. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  189. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  190. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  191. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  192. package/src/api/workflows/index.browser.ts +22 -0
  193. package/src/api/workflows/index.ts +124 -0
  194. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  195. package/src/api/workflows/primitives/$workflow.ts +202 -0
  196. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  197. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  198. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  199. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  200. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  201. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  202. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  203. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  204. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  205. package/src/api/workflows/services/WorkflowService.ts +382 -0
  206. package/src/cli/config/defineConfig.ts +17 -46
  207. package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
  208. package/src/cli/core/services/PackageManagerUtils.ts +3 -1
  209. package/src/cli/core/services/ProjectScaffolder.ts +5 -5
  210. package/src/cli/core/templates/agentMd.ts +14 -5
  211. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  212. package/src/cli/devtools/index.ts +21 -1
  213. package/src/cli/platform/index.ts +23 -2
  214. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  215. package/src/cli/vendor/index.ts +20 -3
  216. package/src/cli/vendor/services/VendorService.ts +126 -27
  217. package/src/core/Alepha.ts +10 -0
  218. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  219. package/src/core/providers/SchemaValidator.ts +1 -1
  220. package/src/core/providers/TypeProvider.ts +46 -3
  221. package/src/logger/index.ts +6 -1
  222. package/src/orm/__tests__/enums.spec.ts +22 -29
  223. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  224. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  225. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  226. package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
  227. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  228. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  229. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  230. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  231. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  232. package/src/security/primitives/$secure.ts +28 -0
  233. package/tsconfig.base.json +0 -1
  234. package/dist/billing/index.d.ts.map +0 -1
  235. package/dist/billing/index.js.map +0 -1
  236. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  237. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  238. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  239. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  240. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  241. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -163,4 +163,48 @@ export class AdminJobController {
163
163
  },
164
164
  handler: ({ query }) => this.jobService.getTopFailures(query.days),
165
165
  });
166
+
167
+ public readonly pauseJob = $action({
168
+ method: "POST",
169
+ path: `${this.url}/pause`,
170
+ group: this.group,
171
+ use: [$secure({ permissions: ["admin:job:trigger"] })],
172
+ schema: {
173
+ body: t.object({ name: t.text() }),
174
+ response: okSchema,
175
+ },
176
+ handler: ({ body, user }) => {
177
+ return this.jobService.pauseJob(body.name, {
178
+ pausedBy: user?.id,
179
+ pausedByName: user?.name,
180
+ });
181
+ },
182
+ });
183
+
184
+ public readonly resumeJob = $action({
185
+ method: "POST",
186
+ path: `${this.url}/resume`,
187
+ group: this.group,
188
+ use: [$secure({ permissions: ["admin:job:trigger"] })],
189
+ schema: {
190
+ body: t.object({ name: t.text() }),
191
+ response: okSchema,
192
+ },
193
+ handler: async ({ body, user }) => {
194
+ return this.jobService.resumeJob(body.name, {
195
+ resumedBy: user?.id,
196
+ resumedByName: user?.name,
197
+ });
198
+ },
199
+ });
200
+
201
+ public readonly getPausedJobs = $action({
202
+ path: `${this.url}/paused`,
203
+ group: this.group,
204
+ use: [$secure({ permissions: ["admin:job:read"] })],
205
+ schema: {
206
+ response: t.array(t.text()),
207
+ },
208
+ handler: () => this.jobService.getPausedJobs(),
209
+ });
166
210
  }
@@ -19,7 +19,6 @@ export const jobExecutionEntity = $entity({
19
19
  "retrying",
20
20
  "running",
21
21
  "completed",
22
- "failed",
23
22
  "dead",
24
23
  "cancelled",
25
24
  ]),
@@ -59,6 +58,5 @@ export type JobStatus =
59
58
  | "retrying"
60
59
  | "running"
61
60
  | "completed"
62
- | "failed"
63
61
  | "dead"
64
62
  | "cancelled";
@@ -1,5 +1,4 @@
1
1
  import { $module, type Alepha, type Static, t } from "alepha";
2
- import { AlephaBatch } from "alepha/batch";
3
2
  import type { DateTime } from "alepha/datetime";
4
3
  import { AlephaLock } from "alepha/lock";
5
4
  import { AlephaQueue } from "alepha/queue";
@@ -83,7 +82,6 @@ export const AlephaApiJobs = $module({
83
82
  AlephaQueue,
84
83
  AlephaScheduler,
85
84
  AlephaLock,
86
- AlephaBatch,
87
85
  JobProvider,
88
86
  JobQueueProvider,
89
87
  JobService,
@@ -100,7 +98,6 @@ export const AlephaApiJobs = $module({
100
98
 
101
99
  alepha.with(AlephaScheduler);
102
100
  alepha.with(AlephaLock);
103
- alepha.with(AlephaBatch);
104
101
  alepha.with(JobProvider);
105
102
  alepha.with(JobService);
106
103
  alepha.with(AdminJobController);
@@ -17,7 +17,7 @@ import {
17
17
  } from "../providers/JobProvider.ts";
18
18
 
19
19
  /**
20
- * Job primitive for defining scheduled and on-demand tasks with payload validation, retry policies, and batching.
20
+ * Job primitive for defining scheduled and on-demand tasks with payload validation and retry policies.
21
21
  */
22
22
  export const $job = <T extends TSchema = TSchema>(
23
23
  options: JobPrimitiveOptions<T>,
@@ -52,11 +52,6 @@ export interface JobRetryOptions {
52
52
  when?: (error: Error) => boolean;
53
53
  }
54
54
 
55
- export interface JobBatchOptions {
56
- size: number;
57
- window: DurationLike;
58
- }
59
-
60
55
  export type JobPriority = "critical" | "high" | "normal" | "low";
61
56
 
62
57
  export interface JobPrimitiveOptions<T extends TSchema = TSchema>
@@ -93,11 +88,6 @@ export interface JobPrimitiveOptions<T extends TSchema = TSchema>
93
88
  */
94
89
  concurrency?: number;
95
90
 
96
- /**
97
- * Consumer batching configuration.
98
- */
99
- batch?: JobBatchOptions;
100
-
101
91
  /**
102
92
  * Default priority for pushed jobs.
103
93
  * @default "normal"
@@ -162,6 +152,27 @@ export class JobPrimitive<
162
152
  public async trigger(context?: JobTriggerContext): Promise<void> {
163
153
  return this.jobProvider.trigger(this.name, context);
164
154
  }
155
+
156
+ /**
157
+ * Pause this job. Pushed items are still accepted but processing is held.
158
+ */
159
+ public pause(): void {
160
+ this.jobProvider.pauseJob(this.name);
161
+ }
162
+
163
+ /**
164
+ * Resume a paused job and dispatch any pending items.
165
+ */
166
+ public async resume(): Promise<void> {
167
+ return this.jobProvider.resumeJob(this.name);
168
+ }
169
+
170
+ /**
171
+ * Whether this job is currently paused.
172
+ */
173
+ public get paused(): boolean {
174
+ return this.jobProvider.isJobPaused(this.name);
175
+ }
165
176
  }
166
177
 
167
178
  $job[KIND] = JobPrimitive;
@@ -8,6 +8,7 @@ import {
8
8
  type TSchema,
9
9
  } from "alepha";
10
10
  import { DateTimeProvider, type DurationLike } from "alepha/datetime";
11
+ import { LockProvider } from "alepha/lock";
11
12
  import type { LogEntry } from "alepha/logger";
12
13
  import { $logger } from "alepha/logger";
13
14
  import { $repository } from "alepha/orm";
@@ -81,12 +82,15 @@ export class JobProvider {
81
82
  protected readonly alepha = $inject(Alepha);
82
83
  protected readonly dt = $inject(DateTimeProvider);
83
84
  protected readonly cronProvider = $inject(CronProvider);
85
+ protected readonly lockProvider = $inject(LockProvider);
84
86
  protected readonly config = $state(jobConfig);
85
87
  protected readonly log = $logger();
86
88
  protected readonly executions = $repository(jobExecutionEntity);
87
89
  protected readonly executionLogs = $repository(jobExecutionLogEntity);
88
90
 
89
91
  protected readonly jobs = new Map<string, JobRegistration>();
92
+ protected readonly pausedJobs = new Set<string>();
93
+ protected readonly inFlight = new Set<Promise<void>>();
90
94
 
91
95
  /**
92
96
  * When set, job executions are dispatched through a queue (e.g. `JobQueueProvider`).
@@ -116,12 +120,16 @@ export class JobProvider {
116
120
  });
117
121
 
118
122
  if (options.cron) {
119
- this.cronProvider.createCronJob(name, options.cron, () =>
120
- this.trigger(name, {
121
- triggeredBy: "system",
122
- triggeredByName: "system (cron)",
123
- }),
124
- );
123
+ this.cronProvider.createCronJob(name, options.cron, async () => {
124
+ try {
125
+ await this.trigger(name, {
126
+ triggeredBy: "system",
127
+ triggeredByName: "system (cron)",
128
+ });
129
+ } catch (error) {
130
+ this.log.error(`Cron trigger failed for job '${name}'`, error);
131
+ }
132
+ });
125
133
  }
126
134
  }
127
135
 
@@ -223,8 +231,61 @@ export class JobProvider {
223
231
  name: string,
224
232
  items: Array<PushManyItem>,
225
233
  ): Promise<string[]> {
226
- const ids: string[] = [];
234
+ if (items.length === 0) return [];
235
+
236
+ const registration = this.getRegistration(name);
237
+ const opts = registration.options;
238
+
239
+ if (!opts.schema) {
240
+ throw new AlephaError(
241
+ `Cannot push to job '${name}': no schema defined. Use trigger() for cron-only jobs.`,
242
+ );
243
+ }
244
+
245
+ const maxAttempts = (opts.retry?.retries ?? 0) + 1;
246
+
247
+ // Keyed items need upsert logic — fall back to individual push
248
+ const keyed: PushManyItem[] = [];
249
+ const bulkRows: Array<{
250
+ jobName: string;
251
+ payload: Record<string, unknown>;
252
+ status: JobStatus;
253
+ priority: number;
254
+ maxAttempts: number;
255
+ scheduledAt?: string;
256
+ }> = [];
257
+
227
258
  for (const item of items) {
259
+ const validated = this.alepha.codec.validate(opts.schema, item.payload);
260
+ if (item.key) {
261
+ keyed.push({ ...item, payload: validated as Static<TSchema> });
262
+ } else {
263
+ const isDelayed = item.delay || item.scheduledAt;
264
+ const status: JobStatus = isDelayed ? "scheduled" : "pending";
265
+ let scheduledAt: string | undefined;
266
+ if (item.scheduledAt) {
267
+ scheduledAt = item.scheduledAt.toISOString();
268
+ } else if (item.delay) {
269
+ scheduledAt = this.dt
270
+ .now()
271
+ .add(this.dt.duration(item.delay))
272
+ .toISOString();
273
+ }
274
+ bulkRows.push({
275
+ jobName: name,
276
+ payload: validated as Record<string, unknown>,
277
+ status,
278
+ priority: PRIORITY_MAP[item.priority ?? opts.priority ?? "normal"],
279
+ maxAttempts,
280
+ scheduledAt,
281
+ });
282
+ }
283
+ }
284
+
285
+ const ids: string[] = [];
286
+
287
+ // Keyed: sequential upserts
288
+ for (const item of keyed) {
228
289
  const id = await this.push(name, item.payload, {
229
290
  key: item.key,
230
291
  delay: item.delay,
@@ -233,6 +294,23 @@ export class JobProvider {
233
294
  });
234
295
  ids.push(id);
235
296
  }
297
+
298
+ // Non-keyed: single bulk insert
299
+ if (bulkRows.length > 0) {
300
+ const created = await this.executions.createMany(bulkRows);
301
+ for (const exec of created) {
302
+ ids.push(exec.id);
303
+ if (exec.status === "pending" && !this.stopping) {
304
+ await this.scheduleProcessing(name, exec.id);
305
+ }
306
+ }
307
+ }
308
+
309
+ this.log.debug(`pushMany '${name}': ${ids.length} jobs created`, {
310
+ bulk: bulkRows.length,
311
+ keyed: keyed.length,
312
+ });
313
+
236
314
  return ids;
237
315
  }
238
316
 
@@ -326,6 +404,25 @@ export class JobProvider {
326
404
  jobName: string,
327
405
  executionId: string,
328
406
  ): Promise<void> {
407
+ if (this.pausedJobs.has(jobName)) {
408
+ this.log.debug(`Job '${jobName}' is paused, deferring`, { executionId });
409
+ return;
410
+ }
411
+
412
+ const registration = this.getRegistration(jobName);
413
+ const maxConcurrency = registration.options.concurrency ?? 1;
414
+ const runningCount = await this.executions.count({
415
+ jobName: { eq: jobName },
416
+ status: { eq: "running" },
417
+ });
418
+ if (runningCount >= maxConcurrency) {
419
+ this.log.debug(
420
+ `Job '${jobName}' at concurrency limit (${runningCount}/${maxConcurrency}), deferring`,
421
+ { executionId },
422
+ );
423
+ return;
424
+ }
425
+
329
426
  if (this.queueDispatch) {
330
427
  this.log.debug(`Dispatching job '${jobName}' via queue`, { executionId });
331
428
  await this.queueDispatch(jobName, executionId);
@@ -338,6 +435,19 @@ export class JobProvider {
338
435
  public async processExecution(
339
436
  jobName: string,
340
437
  executionId: string,
438
+ ): Promise<void> {
439
+ const promise = this.processExecutionInner(jobName, executionId);
440
+ this.inFlight.add(promise);
441
+ try {
442
+ await promise;
443
+ } finally {
444
+ this.inFlight.delete(promise);
445
+ }
446
+ }
447
+
448
+ protected async processExecutionInner(
449
+ jobName: string,
450
+ executionId: string,
341
451
  ): Promise<void> {
342
452
  const registration = this.getRegistration(jobName);
343
453
 
@@ -451,6 +561,9 @@ export class JobProvider {
451
561
  { name: jobName, executionId },
452
562
  { catch: true },
453
563
  );
564
+
565
+ // A slot just opened — dispatch next pending job if any
566
+ await this.dispatchNextPending(jobName);
454
567
  }
455
568
  },
456
569
  { context },
@@ -460,6 +573,36 @@ export class JobProvider {
460
573
  }
461
574
  }
462
575
 
576
+ /**
577
+ * After a job finishes (success, failure, or cancel), dispatch any pending
578
+ * jobs that were deferred due to the concurrency limit.
579
+ */
580
+ protected async dispatchNextPending(jobName: string): Promise<void> {
581
+ if (this.stopping || this.pausedJobs.has(jobName)) return;
582
+
583
+ const registration = this.jobs.get(jobName);
584
+ if (!registration) return;
585
+
586
+ const maxConcurrency = registration.options.concurrency ?? 1;
587
+ const runningCount = await this.executions.count({
588
+ jobName: { eq: jobName },
589
+ status: { eq: "running" },
590
+ });
591
+
592
+ const available = maxConcurrency - runningCount;
593
+ if (available <= 0) return;
594
+
595
+ const pending = await this.executions.findMany({
596
+ where: { jobName: { eq: jobName }, status: { eq: "pending" } },
597
+ orderBy: { column: "priority", direction: "asc" },
598
+ limit: available,
599
+ });
600
+
601
+ for (const exec of pending) {
602
+ await this.scheduleProcessing(jobName, exec.id);
603
+ }
604
+ }
605
+
463
606
  protected async claim(executionId: string): Promise<boolean> {
464
607
  const execution = await this.executions.findById(executionId);
465
608
  if (!execution) return false;
@@ -646,10 +789,14 @@ export class JobProvider {
646
789
  protected async recoverySweep(): Promise<void> {
647
790
  this.log.trace("Starting recovery sweep");
648
791
  if (this.stopping) return;
792
+
793
+ const acquired = await this.tryLock("_alepha:jobs:recovery-lock", 300_000);
794
+ if (!acquired) return;
795
+
649
796
  try {
650
797
  const now = this.dt.now();
651
798
 
652
- // 1. Stale pending jobs
799
+ // 1. Stale pending jobs (priority-ordered)
653
800
  const staleThreshold = now
654
801
  .subtract(this.config.recovery.staleThreshold, "millisecond")
655
802
  .toISOString();
@@ -660,6 +807,7 @@ export class JobProvider {
660
807
 
661
808
  const stalePending = await this.executions.findMany({
662
809
  where: pendingWhere,
810
+ orderBy: { column: "priority", direction: "asc" },
663
811
  });
664
812
 
665
813
  for (const exec of stalePending) {
@@ -708,6 +856,8 @@ export class JobProvider {
708
856
  }
709
857
  } catch (e) {
710
858
  this.log.error("Recovery sweep failed", { error: e });
859
+ } finally {
860
+ await this.releaseLock("_alepha:jobs:recovery-lock");
711
861
  }
712
862
  }
713
863
 
@@ -721,6 +871,10 @@ export class JobProvider {
721
871
  protected async delayedDispatchSweep(): Promise<void> {
722
872
  this.log.trace("Starting delayed dispatch sweep");
723
873
  if (this.stopping) return;
874
+
875
+ const acquired = await this.tryLock("_alepha:jobs:dispatch-lock", 60_000);
876
+ if (!acquired) return;
877
+
724
878
  try {
725
879
  const now = this.dt.nowISOString();
726
880
 
@@ -728,7 +882,10 @@ export class JobProvider {
728
882
  where.status = { inArray: ["scheduled", "retrying"] };
729
883
  where.scheduledAt = { lte: now };
730
884
 
731
- const ready = await this.executions.findMany({ where });
885
+ const ready = await this.executions.findMany({
886
+ where,
887
+ orderBy: { column: "priority", direction: "asc" },
888
+ });
732
889
 
733
890
  for (const exec of ready) {
734
891
  if (!this.jobs.has(exec.jobName)) continue;
@@ -737,6 +894,8 @@ export class JobProvider {
737
894
  }
738
895
  } catch (e) {
739
896
  this.log.error("Delayed dispatch sweep failed", { error: e });
897
+ } finally {
898
+ await this.releaseLock("_alepha:jobs:dispatch-lock");
740
899
  }
741
900
  }
742
901
 
@@ -758,25 +917,66 @@ export class JobProvider {
758
917
  where.status = { inArray: ["completed", "dead", "cancelled"] };
759
918
  where.completedAt = { lte: cutoff };
760
919
 
761
- const old = await this.executions.findMany({ where });
762
-
763
- for (const exec of old) {
764
- try {
765
- await this.executionLogs.deleteById(exec.id);
766
- } catch {
767
- // Log record may not exist
768
- }
769
- await this.executions.deleteById(exec.id);
770
- }
771
-
772
- if (old.length > 0) {
773
- this.log.info(`Log purge: deleted ${old.length} old execution records`);
920
+ // Bulk-delete logs first (FK-safe), then executions
921
+ const expiredIds = await this.executions.findMany({
922
+ where,
923
+ columns: ["id"] as any,
924
+ });
925
+ if (expiredIds.length > 0) {
926
+ const ids = expiredIds.map((e) => e.id);
927
+ await this.executionLogs.deleteMany({ id: { inArray: ids } });
928
+ await this.executions.deleteMany({ id: { inArray: ids } });
929
+ this.log.info(`Log purge: deleted ${ids.length} old execution records`);
774
930
  }
775
931
  } catch (e) {
776
932
  this.log.error("Log purge failed", { error: e });
777
933
  }
778
934
  }
779
935
 
936
+ // --- Pause / Resume ---
937
+
938
+ public pauseJob(name: string): void {
939
+ this.getRegistration(name);
940
+ this.pausedJobs.add(name);
941
+ this.log.info(`Paused job '${name}'`);
942
+ }
943
+
944
+ public async resumeJob(name: string): Promise<void> {
945
+ this.getRegistration(name);
946
+ this.pausedJobs.delete(name);
947
+ this.log.info(`Resumed job '${name}'`);
948
+
949
+ // Dispatch any pending items for this job
950
+ const pending = await this.executions.findMany({
951
+ where: { jobName: { eq: name }, status: { eq: "pending" } },
952
+ orderBy: { column: "priority", direction: "asc" },
953
+ });
954
+ for (const exec of pending) {
955
+ await this.scheduleProcessing(name, exec.id);
956
+ }
957
+ }
958
+
959
+ public isJobPaused(name: string): boolean {
960
+ return this.pausedJobs.has(name);
961
+ }
962
+
963
+ public getPausedJobs(): string[] {
964
+ return [...this.pausedJobs];
965
+ }
966
+
967
+ // --- Lock helpers ---
968
+
969
+ protected async tryLock(key: string, ttlMs: number): Promise<boolean> {
970
+ const lockValue = `${this.workerId},${this.dt.nowISOString()}`;
971
+ const result = await this.lockProvider.set(key, lockValue, true, ttlMs);
972
+ const [lockId] = result.split(",");
973
+ return lockId === this.workerId;
974
+ }
975
+
976
+ protected async releaseLock(key: string): Promise<void> {
977
+ await this.lockProvider.del(key);
978
+ }
979
+
780
980
  // --- Lifecycle hooks ---
781
981
 
782
982
  protected readonly onStart = $hook({
@@ -839,9 +1039,23 @@ export class JobProvider {
839
1039
  handler: async () => {
840
1040
  this.stopping = true;
841
1041
 
842
- // Abort any running executions
843
- for (const controller of this.abortControllers.values()) {
844
- controller.abort();
1042
+ // Drain: wait for in-flight jobs to finish before aborting
1043
+ if (this.inFlight.size > 0) {
1044
+ this.log.info(`Draining ${this.inFlight.size} in-flight job(s)...`);
1045
+ await Promise.race([
1046
+ Promise.allSettled([...this.inFlight]),
1047
+ this.dt.wait([this.config.drainTimeout, "millisecond"]),
1048
+ ]);
1049
+ }
1050
+
1051
+ // Abort any still-running executions after drain timeout
1052
+ if (this.abortControllers.size > 0) {
1053
+ this.log.warn(
1054
+ `Aborting ${this.abortControllers.size} remaining job(s) after drain timeout`,
1055
+ );
1056
+ for (const controller of this.abortControllers.values()) {
1057
+ controller.abort();
1058
+ }
845
1059
  }
846
1060
  },
847
1061
  });
@@ -23,6 +23,9 @@ export const jobConfig = $atom({
23
23
  logMaxEntries: t.integer({
24
24
  description: "Max log entries captured per execution.",
25
25
  }),
26
+ drainTimeout: t.integer({
27
+ description: "Max time (ms) to wait for in-flight jobs during shutdown.",
28
+ }),
26
29
  }),
27
30
  default: {
28
31
  recovery: {
@@ -35,6 +38,7 @@ export const jobConfig = $atom({
35
38
  },
36
39
  logRetentionDays: 30,
37
40
  logMaxEntries: 100,
41
+ drainTimeout: 30_000,
38
42
  },
39
43
  });
40
44
 
@@ -7,6 +7,7 @@ export const jobCronInfoSchema = t.object({
7
7
  priority: t.enum(["critical", "high", "normal", "low"]),
8
8
  concurrency: t.integer(),
9
9
  hasSchema: t.boolean(),
10
+ paused: t.boolean(),
10
11
  lastExecution: t.optional(
11
12
  t.object({
12
13
  id: t.uuid(),
@@ -14,7 +14,6 @@ export const jobExecutionQuerySchema = t.extend(pageQuerySchema, {
14
14
  "retrying",
15
15
  "running",
16
16
  "completed",
17
- "failed",
18
17
  "dead",
19
18
  "cancelled",
20
19
  ]),
@@ -8,6 +8,7 @@ export const jobQueueDepthSchema = t.object({
8
8
  retrying: t.integer(),
9
9
  dead: t.integer(),
10
10
  concurrency: t.integer(),
11
+ paused: t.boolean(),
11
12
  });
12
13
 
13
14
  export type JobQueueDepth = Static<typeof jobQueueDepthSchema>;
@@ -14,12 +14,7 @@ export const jobRegistrationSchema = t.object({
14
14
  hasBackoff: t.boolean(),
15
15
  }),
16
16
  ),
17
- batch: t.optional(
18
- t.object({
19
- size: t.integer(),
20
- window: t.text(),
21
- }),
22
- ),
17
+ paused: t.boolean(),
23
18
  });
24
19
 
25
20
  export type JobRegistration = Static<typeof jobRegistrationSchema>;