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
@@ -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`).
@@ -227,8 +231,61 @@ export class JobProvider {
227
231
  name: string,
228
232
  items: Array<PushManyItem>,
229
233
  ): Promise<string[]> {
230
- 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
+
231
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) {
232
289
  const id = await this.push(name, item.payload, {
233
290
  key: item.key,
234
291
  delay: item.delay,
@@ -237,6 +294,23 @@ export class JobProvider {
237
294
  });
238
295
  ids.push(id);
239
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
+
240
314
  return ids;
241
315
  }
242
316
 
@@ -330,6 +404,25 @@ export class JobProvider {
330
404
  jobName: string,
331
405
  executionId: string,
332
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
+
333
426
  if (this.queueDispatch) {
334
427
  this.log.debug(`Dispatching job '${jobName}' via queue`, { executionId });
335
428
  await this.queueDispatch(jobName, executionId);
@@ -342,6 +435,19 @@ export class JobProvider {
342
435
  public async processExecution(
343
436
  jobName: string,
344
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,
345
451
  ): Promise<void> {
346
452
  const registration = this.getRegistration(jobName);
347
453
 
@@ -455,6 +561,9 @@ export class JobProvider {
455
561
  { name: jobName, executionId },
456
562
  { catch: true },
457
563
  );
564
+
565
+ // A slot just opened — dispatch next pending job if any
566
+ await this.dispatchNextPending(jobName);
458
567
  }
459
568
  },
460
569
  { context },
@@ -464,6 +573,36 @@ export class JobProvider {
464
573
  }
465
574
  }
466
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
+
467
606
  protected async claim(executionId: string): Promise<boolean> {
468
607
  const execution = await this.executions.findById(executionId);
469
608
  if (!execution) return false;
@@ -650,10 +789,14 @@ export class JobProvider {
650
789
  protected async recoverySweep(): Promise<void> {
651
790
  this.log.trace("Starting recovery sweep");
652
791
  if (this.stopping) return;
792
+
793
+ const acquired = await this.tryLock("_alepha:jobs:recovery-lock", 300_000);
794
+ if (!acquired) return;
795
+
653
796
  try {
654
797
  const now = this.dt.now();
655
798
 
656
- // 1. Stale pending jobs
799
+ // 1. Stale pending jobs (priority-ordered)
657
800
  const staleThreshold = now
658
801
  .subtract(this.config.recovery.staleThreshold, "millisecond")
659
802
  .toISOString();
@@ -664,6 +807,7 @@ export class JobProvider {
664
807
 
665
808
  const stalePending = await this.executions.findMany({
666
809
  where: pendingWhere,
810
+ orderBy: { column: "priority", direction: "asc" },
667
811
  });
668
812
 
669
813
  for (const exec of stalePending) {
@@ -712,6 +856,8 @@ export class JobProvider {
712
856
  }
713
857
  } catch (e) {
714
858
  this.log.error("Recovery sweep failed", { error: e });
859
+ } finally {
860
+ await this.releaseLock("_alepha:jobs:recovery-lock");
715
861
  }
716
862
  }
717
863
 
@@ -725,6 +871,10 @@ export class JobProvider {
725
871
  protected async delayedDispatchSweep(): Promise<void> {
726
872
  this.log.trace("Starting delayed dispatch sweep");
727
873
  if (this.stopping) return;
874
+
875
+ const acquired = await this.tryLock("_alepha:jobs:dispatch-lock", 60_000);
876
+ if (!acquired) return;
877
+
728
878
  try {
729
879
  const now = this.dt.nowISOString();
730
880
 
@@ -732,7 +882,10 @@ export class JobProvider {
732
882
  where.status = { inArray: ["scheduled", "retrying"] };
733
883
  where.scheduledAt = { lte: now };
734
884
 
735
- const ready = await this.executions.findMany({ where });
885
+ const ready = await this.executions.findMany({
886
+ where,
887
+ orderBy: { column: "priority", direction: "asc" },
888
+ });
736
889
 
737
890
  for (const exec of ready) {
738
891
  if (!this.jobs.has(exec.jobName)) continue;
@@ -741,6 +894,8 @@ export class JobProvider {
741
894
  }
742
895
  } catch (e) {
743
896
  this.log.error("Delayed dispatch sweep failed", { error: e });
897
+ } finally {
898
+ await this.releaseLock("_alepha:jobs:dispatch-lock");
744
899
  }
745
900
  }
746
901
 
@@ -762,25 +917,66 @@ export class JobProvider {
762
917
  where.status = { inArray: ["completed", "dead", "cancelled"] };
763
918
  where.completedAt = { lte: cutoff };
764
919
 
765
- const old = await this.executions.findMany({ where });
766
-
767
- for (const exec of old) {
768
- try {
769
- await this.executionLogs.deleteById(exec.id);
770
- } catch {
771
- // Log record may not exist
772
- }
773
- await this.executions.deleteById(exec.id);
774
- }
775
-
776
- if (old.length > 0) {
777
- 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`);
778
930
  }
779
931
  } catch (e) {
780
932
  this.log.error("Log purge failed", { error: e });
781
933
  }
782
934
  }
783
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
+
784
980
  // --- Lifecycle hooks ---
785
981
 
786
982
  protected readonly onStart = $hook({
@@ -843,9 +1039,23 @@ export class JobProvider {
843
1039
  handler: async () => {
844
1040
  this.stopping = true;
845
1041
 
846
- // Abort any running executions
847
- for (const controller of this.abortControllers.values()) {
848
- 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
+ }
849
1059
  }
850
1060
  },
851
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>;