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.
- 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 +90 -34
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +267 -44
- package/dist/api/jobs/index.js.map +1 -1
- 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 +27 -21
- 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/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/config/index.d.ts +6 -28
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +5 -10
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +11669 -208
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +60 -69
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +5 -0
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +4 -0
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +69 -64
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +6 -2
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +38 -10
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +85 -26
- 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 +25 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +25 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +25 -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/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/logger/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 +25 -73
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +10 -32
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +25 -73
- 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 +2 -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 +239 -25
- 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/config/defineConfig.ts +17 -46
- package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
- package/src/cli/core/services/PackageManagerUtils.ts +3 -1
- package/src/cli/core/services/ProjectScaffolder.ts +5 -5
- package/src/cli/core/templates/agentMd.ts +14 -5
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/devtools/index.ts +21 -1
- package/src/cli/platform/index.ts +23 -2
- package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
- package/src/cli/vendor/index.ts +20 -3
- package/src/cli/vendor/services/VendorService.ts +126 -27
- package/src/core/Alepha.ts +10 -0
- 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/logger/index.ts +6 -1
- 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/core/providers/DrizzleKitProvider.ts +56 -105
- 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/tsconfig.base.json +0 -1
- 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`).
|
|
@@ -116,12 +120,16 @@ export class JobProvider {
|
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
if (options.cron) {
|
|
119
|
-
this.cronProvider.createCronJob(name, options.cron, () =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
await this.executions.
|
|
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
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
|
@@ -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>;
|