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.
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/dist/api/audits/index.d.ts +8 -8
- package/dist/api/invitations/index.d.ts +790 -0
- package/dist/api/invitations/index.d.ts.map +1 -0
- package/dist/api/invitations/index.js +665 -0
- package/dist/api/invitations/index.js.map +1 -0
- package/dist/api/jobs/index.browser.js +8 -9
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +99 -43
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +257 -40
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +5 -5
- package/dist/api/notifications/index.browser.js +0 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +0 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +112 -1
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +90 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +79 -12
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/{billing → api/payments}/index.d.ts +67 -49
- package/dist/api/payments/index.d.ts.map +1 -0
- package/dist/{billing → api/payments}/index.js +108 -74
- package/dist/api/payments/index.js.map +1 -0
- package/dist/api/subscriptions/index.d.ts +1692 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1870 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +18 -2
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +167 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +13 -13
- package/dist/api/workflows/index.browser.js +246 -0
- package/dist/api/workflows/index.browser.js.map +1 -0
- package/dist/api/workflows/index.d.ts +1618 -0
- package/dist/api/workflows/index.d.ts.map +1 -0
- package/dist/api/workflows/index.js +1504 -0
- package/dist/api/workflows/index.js.map +1 -0
- package/dist/cli/core/index.d.ts +44 -28
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +16 -61
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +31 -8
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +79 -24
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +21 -2
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +21 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +21 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +21 -2
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js +24 -8
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +0 -18
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +0 -17
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +1 -13
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +0 -17
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +3 -3
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +3 -3
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/router/index.browser.js +25 -3
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +16 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +25 -3
- package/dist/react/router/index.js.map +1 -1
- package/dist/security/index.d.ts +28 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +28 -0
- package/dist/security/index.js.map +1 -1
- package/package.json +37 -20
- package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
- package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
- package/src/api/invitations/controllers/InvitationController.ts +84 -0
- package/src/api/invitations/entities/invitations.ts +33 -0
- package/src/api/invitations/index.ts +65 -0
- package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
- package/src/api/invitations/providers/InvitationProvider.ts +45 -0
- package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
- package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
- package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
- package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
- package/src/api/invitations/services/InvitationService.ts +556 -0
- package/src/api/jobs/__tests__/$job.spec.ts +876 -0
- package/src/api/jobs/controllers/AdminJobController.ts +44 -0
- package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
- package/src/api/jobs/index.ts +0 -3
- package/src/api/jobs/primitives/$job.ts +22 -11
- package/src/api/jobs/providers/JobProvider.ts +229 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
- package/src/api/jobs/services/JobService.ts +51 -12
- package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
- package/src/api/parameters/index.browser.ts +12 -0
- package/src/api/parameters/primitives/$parameter.ts +20 -3
- package/src/api/parameters/services/ParameterProvider.ts +48 -7
- package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
- package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
- package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
- package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
- package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
- package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
- package/src/{billing → api/payments}/index.ts +31 -25
- package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
- package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
- package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
- package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +144 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
- package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
- package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
- package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
- package/src/api/users/__tests__/SessionService.spec.ts +142 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
- package/src/api/users/controllers/UserController.ts +3 -8
- package/src/api/users/notifications/UserNotifications.ts +23 -0
- package/src/api/users/schemas/loginSchema.ts +1 -1
- package/src/api/users/services/CredentialService.ts +51 -4
- package/src/api/users/services/RegistrationService.ts +38 -9
- package/src/api/users/services/SessionService.ts +62 -9
- package/src/api/users/services/UserService.ts +21 -12
- package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
- package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
- package/src/api/workflows/entities/workflowExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
- package/src/api/workflows/index.browser.ts +22 -0
- package/src/api/workflows/index.ts +124 -0
- package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
- package/src/api/workflows/primitives/$workflow.ts +202 -0
- package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
- package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
- package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
- package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
- package/src/api/workflows/services/WorkflowService.ts +382 -0
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
- package/src/cli/vendor/services/VendorService.ts +126 -27
- package/src/core/__tests__/TypeProvider.spec.ts +4 -2
- package/src/core/providers/SchemaValidator.ts +1 -1
- package/src/core/providers/TypeProvider.ts +46 -3
- package/src/orm/__tests__/enums.spec.ts +22 -29
- package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
- package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
- package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
- package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
- package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
- package/src/security/primitives/$secure.ts +28 -0
- package/dist/billing/index.d.ts.map +0 -1
- package/dist/billing/index.js.map +0 -1
- package/src/billing/__tests__/BillingService.spec.ts +0 -136
- /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
- /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
- /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
- /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";
|
package/src/api/jobs/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
await this.executions.
|
|
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
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
|
@@ -14,12 +14,7 @@ export const jobRegistrationSchema = t.object({
|
|
|
14
14
|
hasBackoff: t.boolean(),
|
|
15
15
|
}),
|
|
16
16
|
),
|
|
17
|
-
|
|
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>;
|