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
|
@@ -2020,4 +2020,880 @@ describe("$job v2", () => {
|
|
|
2020
2020
|
expect(handler).toHaveBeenCalledTimes(3);
|
|
2021
2021
|
});
|
|
2022
2022
|
});
|
|
2023
|
+
|
|
2024
|
+
// ----- Concurrency enforcement -----
|
|
2025
|
+
|
|
2026
|
+
describe("concurrency enforcement", () => {
|
|
2027
|
+
it("should execute serially with concurrency: 1", async () => {
|
|
2028
|
+
let maxRunning = 0;
|
|
2029
|
+
let currentRunning = 0;
|
|
2030
|
+
|
|
2031
|
+
class App {
|
|
2032
|
+
repo = $repository(jobExecutionEntity);
|
|
2033
|
+
myJob = $job({
|
|
2034
|
+
schema: t.object({ id: t.text() }),
|
|
2035
|
+
concurrency: 1,
|
|
2036
|
+
handler: async () => {
|
|
2037
|
+
currentRunning++;
|
|
2038
|
+
maxRunning = Math.max(maxRunning, currentRunning);
|
|
2039
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2040
|
+
currentRunning--;
|
|
2041
|
+
},
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2046
|
+
const app = alepha.inject(App);
|
|
2047
|
+
await alepha.start();
|
|
2048
|
+
|
|
2049
|
+
await app.myJob.push([{ id: "1" }, { id: "2" }, { id: "3" }]);
|
|
2050
|
+
|
|
2051
|
+
await vi.waitFor(
|
|
2052
|
+
async () => {
|
|
2053
|
+
const completed = await app.repo.findMany({
|
|
2054
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2055
|
+
});
|
|
2056
|
+
expect(completed).toHaveLength(3);
|
|
2057
|
+
},
|
|
2058
|
+
{ timeout: 10_000 },
|
|
2059
|
+
);
|
|
2060
|
+
|
|
2061
|
+
expect(maxRunning).toBe(1);
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
it("should allow parallel execution up to concurrency limit", async () => {
|
|
2065
|
+
let maxRunning = 0;
|
|
2066
|
+
let currentRunning = 0;
|
|
2067
|
+
|
|
2068
|
+
class App {
|
|
2069
|
+
repo = $repository(jobExecutionEntity);
|
|
2070
|
+
myJob = $job({
|
|
2071
|
+
schema: t.object({ id: t.text() }),
|
|
2072
|
+
concurrency: 2,
|
|
2073
|
+
handler: async () => {
|
|
2074
|
+
currentRunning++;
|
|
2075
|
+
maxRunning = Math.max(maxRunning, currentRunning);
|
|
2076
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2077
|
+
currentRunning--;
|
|
2078
|
+
},
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2083
|
+
const app = alepha.inject(App);
|
|
2084
|
+
await alepha.start();
|
|
2085
|
+
|
|
2086
|
+
await app.myJob.push([
|
|
2087
|
+
{ id: "1" },
|
|
2088
|
+
{ id: "2" },
|
|
2089
|
+
{ id: "3" },
|
|
2090
|
+
{ id: "4" },
|
|
2091
|
+
]);
|
|
2092
|
+
|
|
2093
|
+
await vi.waitFor(
|
|
2094
|
+
async () => {
|
|
2095
|
+
const completed = await app.repo.findMany({
|
|
2096
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2097
|
+
});
|
|
2098
|
+
expect(completed).toHaveLength(4);
|
|
2099
|
+
},
|
|
2100
|
+
{ timeout: 10_000 },
|
|
2101
|
+
);
|
|
2102
|
+
|
|
2103
|
+
expect(maxRunning).toBeLessThanOrEqual(2);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
it("should dispatch next pending after completion", async () => {
|
|
2107
|
+
const order: string[] = [];
|
|
2108
|
+
|
|
2109
|
+
class App {
|
|
2110
|
+
repo = $repository(jobExecutionEntity);
|
|
2111
|
+
myJob = $job({
|
|
2112
|
+
schema: t.object({ id: t.text() }),
|
|
2113
|
+
concurrency: 1,
|
|
2114
|
+
handler: async ({ items }) => {
|
|
2115
|
+
order.push(items[0]?.payload.id ?? "cron");
|
|
2116
|
+
},
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2121
|
+
const app = alepha.inject(App);
|
|
2122
|
+
await alepha.start();
|
|
2123
|
+
|
|
2124
|
+
await app.myJob.push({ id: "a" });
|
|
2125
|
+
await app.myJob.push({ id: "b" });
|
|
2126
|
+
|
|
2127
|
+
await vi.waitFor(
|
|
2128
|
+
async () => {
|
|
2129
|
+
const completed = await app.repo.findMany({
|
|
2130
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2131
|
+
});
|
|
2132
|
+
expect(completed).toHaveLength(2);
|
|
2133
|
+
},
|
|
2134
|
+
{ timeout: 10_000 },
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
expect(order).toEqual(["a", "b"]);
|
|
2138
|
+
});
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// ----- Pause / Resume -----
|
|
2142
|
+
|
|
2143
|
+
describe("pause / resume", () => {
|
|
2144
|
+
it("should not dispatch while paused", async () => {
|
|
2145
|
+
const handler = vi.fn();
|
|
2146
|
+
|
|
2147
|
+
class App {
|
|
2148
|
+
repo = $repository(jobExecutionEntity);
|
|
2149
|
+
myJob = $job({
|
|
2150
|
+
schema: t.object({ value: t.text() }),
|
|
2151
|
+
handler,
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2156
|
+
const app = alepha.inject(App);
|
|
2157
|
+
await alepha.start();
|
|
2158
|
+
|
|
2159
|
+
app.myJob.pause();
|
|
2160
|
+
expect(app.myJob.paused).toBe(true);
|
|
2161
|
+
|
|
2162
|
+
await app.myJob.push({ value: "test" });
|
|
2163
|
+
|
|
2164
|
+
// Item should stay pending
|
|
2165
|
+
const executions = await app.repo.findMany({
|
|
2166
|
+
where: { jobName: "App.myJob" },
|
|
2167
|
+
});
|
|
2168
|
+
expect(executions).toHaveLength(1);
|
|
2169
|
+
expect(executions[0].status).toBe("pending");
|
|
2170
|
+
expect(handler).not.toHaveBeenCalled();
|
|
2171
|
+
|
|
2172
|
+
// Cleanup: resume so nothing leaks
|
|
2173
|
+
await app.myJob.resume();
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
it("should accept pushes while paused and process on resume", async () => {
|
|
2177
|
+
const handler = vi.fn();
|
|
2178
|
+
|
|
2179
|
+
class App {
|
|
2180
|
+
repo = $repository(jobExecutionEntity);
|
|
2181
|
+
myJob = $job({
|
|
2182
|
+
schema: t.object({ value: t.text() }),
|
|
2183
|
+
handler,
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2188
|
+
const app = alepha.inject(App);
|
|
2189
|
+
await alepha.start();
|
|
2190
|
+
|
|
2191
|
+
app.myJob.pause();
|
|
2192
|
+
|
|
2193
|
+
await app.myJob.push({ value: "a" });
|
|
2194
|
+
await app.myJob.push({ value: "b" });
|
|
2195
|
+
|
|
2196
|
+
expect(handler).not.toHaveBeenCalled();
|
|
2197
|
+
|
|
2198
|
+
await app.myJob.resume();
|
|
2199
|
+
expect(app.myJob.paused).toBe(false);
|
|
2200
|
+
|
|
2201
|
+
await vi.waitFor(
|
|
2202
|
+
async () => {
|
|
2203
|
+
const completed = await app.repo.findMany({
|
|
2204
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2205
|
+
});
|
|
2206
|
+
expect(completed).toHaveLength(2);
|
|
2207
|
+
},
|
|
2208
|
+
{ timeout: 10_000 },
|
|
2209
|
+
);
|
|
2210
|
+
|
|
2211
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
it("should report paused status via JobService registry", async () => {
|
|
2215
|
+
class App {
|
|
2216
|
+
jobService = $inject(JobService);
|
|
2217
|
+
myJob = $job({
|
|
2218
|
+
schema: t.object({ value: t.text() }),
|
|
2219
|
+
handler: async () => {},
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2224
|
+
const app = alepha.inject(App);
|
|
2225
|
+
await alepha.start();
|
|
2226
|
+
|
|
2227
|
+
const before = app.jobService.getRegistry();
|
|
2228
|
+
expect(before[0].paused).toBe(false);
|
|
2229
|
+
|
|
2230
|
+
app.myJob.pause();
|
|
2231
|
+
|
|
2232
|
+
const after = app.jobService.getRegistry();
|
|
2233
|
+
expect(after[0].paused).toBe(true);
|
|
2234
|
+
|
|
2235
|
+
await app.myJob.resume();
|
|
2236
|
+
});
|
|
2237
|
+
|
|
2238
|
+
it("should expose paused jobs via getPausedJobs", async () => {
|
|
2239
|
+
class App {
|
|
2240
|
+
jobService = $inject(JobService);
|
|
2241
|
+
jobA = $job({
|
|
2242
|
+
schema: t.object({ v: t.text() }),
|
|
2243
|
+
handler: async () => {},
|
|
2244
|
+
});
|
|
2245
|
+
jobB = $job({
|
|
2246
|
+
schema: t.object({ v: t.text() }),
|
|
2247
|
+
handler: async () => {},
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2252
|
+
const app = alepha.inject(App);
|
|
2253
|
+
await alepha.start();
|
|
2254
|
+
|
|
2255
|
+
app.jobA.pause();
|
|
2256
|
+
expect(app.jobService.getPausedJobs()).toEqual(["App.jobA"]);
|
|
2257
|
+
|
|
2258
|
+
app.jobB.pause();
|
|
2259
|
+
expect(app.jobService.getPausedJobs()).toHaveLength(2);
|
|
2260
|
+
|
|
2261
|
+
await app.jobA.resume();
|
|
2262
|
+
expect(app.jobService.getPausedJobs()).toEqual(["App.jobB"]);
|
|
2263
|
+
|
|
2264
|
+
await app.jobB.resume();
|
|
2265
|
+
});
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
// ----- Priority-ordered dispatch -----
|
|
2269
|
+
|
|
2270
|
+
describe("priority-ordered dispatch", () => {
|
|
2271
|
+
it("should dispatch higher priority jobs first", async () => {
|
|
2272
|
+
const order: string[] = [];
|
|
2273
|
+
|
|
2274
|
+
class App {
|
|
2275
|
+
repo = $repository(jobExecutionEntity);
|
|
2276
|
+
myJob = $job({
|
|
2277
|
+
schema: t.object({ label: t.text() }),
|
|
2278
|
+
concurrency: 1,
|
|
2279
|
+
handler: async ({ items }) => {
|
|
2280
|
+
order.push(items[0].payload.label);
|
|
2281
|
+
},
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2286
|
+
const app = alepha.inject(App);
|
|
2287
|
+
await alepha.start();
|
|
2288
|
+
|
|
2289
|
+
// Pause to queue items without dispatching
|
|
2290
|
+
app.myJob.pause();
|
|
2291
|
+
|
|
2292
|
+
await app.myJob.push({ label: "low" }, { priority: "low" });
|
|
2293
|
+
await app.myJob.push({ label: "critical" }, { priority: "critical" });
|
|
2294
|
+
await app.myJob.push({ label: "normal" });
|
|
2295
|
+
|
|
2296
|
+
// Resume triggers dispatch in priority order
|
|
2297
|
+
await app.myJob.resume();
|
|
2298
|
+
|
|
2299
|
+
await vi.waitFor(
|
|
2300
|
+
async () => {
|
|
2301
|
+
const completed = await app.repo.findMany({
|
|
2302
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2303
|
+
});
|
|
2304
|
+
expect(completed).toHaveLength(3);
|
|
2305
|
+
},
|
|
2306
|
+
{ timeout: 10_000 },
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
expect(order).toEqual(["critical", "normal", "low"]);
|
|
2310
|
+
});
|
|
2311
|
+
});
|
|
2312
|
+
|
|
2313
|
+
// ----- Bulk pushMany -----
|
|
2314
|
+
|
|
2315
|
+
describe("bulk pushMany", () => {
|
|
2316
|
+
it("should bulk-create non-keyed items", async () => {
|
|
2317
|
+
const handler = vi.fn();
|
|
2318
|
+
|
|
2319
|
+
class App {
|
|
2320
|
+
repo = $repository(jobExecutionEntity);
|
|
2321
|
+
myJob = $job({
|
|
2322
|
+
schema: t.object({ n: t.integer() }),
|
|
2323
|
+
handler,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2328
|
+
const app = alepha.inject(App);
|
|
2329
|
+
await alepha.start();
|
|
2330
|
+
|
|
2331
|
+
const ids = await app.myJob.pushMany([
|
|
2332
|
+
{ payload: { n: 1 } },
|
|
2333
|
+
{ payload: { n: 2 } },
|
|
2334
|
+
{ payload: { n: 3 } },
|
|
2335
|
+
]);
|
|
2336
|
+
|
|
2337
|
+
expect(ids).toHaveLength(3);
|
|
2338
|
+
|
|
2339
|
+
await vi.waitFor(
|
|
2340
|
+
async () => {
|
|
2341
|
+
const completed = await app.repo.findMany({
|
|
2342
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2343
|
+
});
|
|
2344
|
+
expect(completed).toHaveLength(3);
|
|
2345
|
+
},
|
|
2346
|
+
{ timeout: 10_000 },
|
|
2347
|
+
);
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
it("should handle mixed keyed and non-keyed items", async () => {
|
|
2351
|
+
class App {
|
|
2352
|
+
repo = $repository(jobExecutionEntity);
|
|
2353
|
+
myJob = $job({
|
|
2354
|
+
schema: t.object({ v: t.text() }),
|
|
2355
|
+
handler: async () => {},
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2360
|
+
const app = alepha.inject(App);
|
|
2361
|
+
await alepha.start();
|
|
2362
|
+
|
|
2363
|
+
const ids = await app.myJob.pushMany([
|
|
2364
|
+
{ payload: { v: "keyed" }, key: "k1", delay: [1, "hour"] },
|
|
2365
|
+
{ payload: { v: "bulk1" } },
|
|
2366
|
+
{ payload: { v: "bulk2" } },
|
|
2367
|
+
]);
|
|
2368
|
+
|
|
2369
|
+
expect(ids).toHaveLength(3);
|
|
2370
|
+
|
|
2371
|
+
const keyed = await app.repo.findById(ids[0]);
|
|
2372
|
+
expect(keyed?.key).toBe("k1");
|
|
2373
|
+
expect(keyed?.status).toBe("scheduled");
|
|
2374
|
+
|
|
2375
|
+
await vi.waitFor(async () => {
|
|
2376
|
+
const completed = await app.repo.findMany({
|
|
2377
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2378
|
+
});
|
|
2379
|
+
expect(completed).toHaveLength(2);
|
|
2380
|
+
});
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
it("should reject all items if one payload is invalid", async () => {
|
|
2384
|
+
class App {
|
|
2385
|
+
repo = $repository(jobExecutionEntity);
|
|
2386
|
+
myJob = $job({
|
|
2387
|
+
schema: t.object({ n: t.integer() }),
|
|
2388
|
+
handler: async () => {},
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2393
|
+
const app = alepha.inject(App);
|
|
2394
|
+
await alepha.start();
|
|
2395
|
+
|
|
2396
|
+
await expect(
|
|
2397
|
+
app.myJob.pushMany([
|
|
2398
|
+
{ payload: { n: 1 } },
|
|
2399
|
+
{ payload: { n: "not-a-number" as any } },
|
|
2400
|
+
]),
|
|
2401
|
+
).rejects.toThrow();
|
|
2402
|
+
|
|
2403
|
+
// First item should not have been created either (validation is upfront)
|
|
2404
|
+
const all = await app.repo.findMany({
|
|
2405
|
+
where: { jobName: "App.myJob" },
|
|
2406
|
+
});
|
|
2407
|
+
expect(all).toHaveLength(0);
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
it("should return empty array for empty input", async () => {
|
|
2411
|
+
class App {
|
|
2412
|
+
myJob = $job({
|
|
2413
|
+
schema: t.object({ v: t.text() }),
|
|
2414
|
+
handler: async () => {},
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2419
|
+
const app = alepha.inject(App);
|
|
2420
|
+
await alepha.start();
|
|
2421
|
+
|
|
2422
|
+
const ids = await app.myJob.pushMany([]);
|
|
2423
|
+
expect(ids).toEqual([]);
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
// ----- Graceful drain -----
|
|
2428
|
+
|
|
2429
|
+
describe("graceful drain", () => {
|
|
2430
|
+
it("should wait for in-flight jobs before aborting on stop", async () => {
|
|
2431
|
+
let completed = false;
|
|
2432
|
+
|
|
2433
|
+
class App {
|
|
2434
|
+
repo = $repository(jobExecutionEntity);
|
|
2435
|
+
myJob = $job({
|
|
2436
|
+
schema: t.object({ value: t.text() }),
|
|
2437
|
+
handler: async () => {
|
|
2438
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2439
|
+
completed = true;
|
|
2440
|
+
},
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2445
|
+
const app = alepha.inject(App);
|
|
2446
|
+
await alepha.start();
|
|
2447
|
+
|
|
2448
|
+
await app.myJob.push({ value: "test" });
|
|
2449
|
+
|
|
2450
|
+
await vi.waitFor(async () => {
|
|
2451
|
+
const running = await app.repo.findMany({
|
|
2452
|
+
where: { jobName: "App.myJob", status: "running" },
|
|
2453
|
+
});
|
|
2454
|
+
expect(running).toHaveLength(1);
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
// Stop should drain (wait for the in-flight job)
|
|
2458
|
+
await alepha.stop();
|
|
2459
|
+
|
|
2460
|
+
expect(completed).toBe(true);
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
it("should stop cleanly when no jobs are in-flight", async () => {
|
|
2464
|
+
class App {
|
|
2465
|
+
myJob = $job({
|
|
2466
|
+
schema: t.object({ value: t.text() }),
|
|
2467
|
+
handler: async () => {},
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2472
|
+
alepha.inject(App);
|
|
2473
|
+
await alepha.start();
|
|
2474
|
+
await alepha.stop();
|
|
2475
|
+
});
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
// ----- JobService reporting -----
|
|
2479
|
+
|
|
2480
|
+
describe("JobService reporting", () => {
|
|
2481
|
+
it("should find executions with status filter", async () => {
|
|
2482
|
+
class App {
|
|
2483
|
+
repo = $repository(jobExecutionEntity);
|
|
2484
|
+
jobService = $inject(JobService);
|
|
2485
|
+
myJob = $job({
|
|
2486
|
+
schema: t.object({ value: t.text() }),
|
|
2487
|
+
handler: async () => {},
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2492
|
+
const app = alepha.inject(App);
|
|
2493
|
+
await alepha.start();
|
|
2494
|
+
|
|
2495
|
+
await app.myJob.push({ value: "test" });
|
|
2496
|
+
|
|
2497
|
+
await vi.waitFor(async () => {
|
|
2498
|
+
const completed = await app.repo.findMany({
|
|
2499
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2500
|
+
});
|
|
2501
|
+
expect(completed).toHaveLength(1);
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
// Push a delayed one (stays scheduled)
|
|
2505
|
+
await app.myJob.push({ value: "later" }, { delay: [1, "hour"] });
|
|
2506
|
+
|
|
2507
|
+
const completedPage = await app.jobService.findExecutions({
|
|
2508
|
+
status: "completed",
|
|
2509
|
+
});
|
|
2510
|
+
expect(completedPage.content.length).toBe(1);
|
|
2511
|
+
expect(completedPage.content[0].status).toBe("completed");
|
|
2512
|
+
|
|
2513
|
+
const scheduledPage = await app.jobService.findExecutions({
|
|
2514
|
+
status: "scheduled",
|
|
2515
|
+
});
|
|
2516
|
+
expect(scheduledPage.content.length).toBe(1);
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
it("should find executions with priority filter", async () => {
|
|
2520
|
+
class App {
|
|
2521
|
+
repo = $repository(jobExecutionEntity);
|
|
2522
|
+
jobService = $inject(JobService);
|
|
2523
|
+
myJob = $job({
|
|
2524
|
+
schema: t.object({ value: t.text() }),
|
|
2525
|
+
handler: async () => {},
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2530
|
+
const app = alepha.inject(App);
|
|
2531
|
+
await alepha.start();
|
|
2532
|
+
|
|
2533
|
+
await app.myJob.push(
|
|
2534
|
+
{ value: "hi" },
|
|
2535
|
+
{ priority: "high", delay: [1, "hour"] },
|
|
2536
|
+
);
|
|
2537
|
+
await app.myJob.push(
|
|
2538
|
+
{ value: "lo" },
|
|
2539
|
+
{ priority: "low", delay: [1, "hour"] },
|
|
2540
|
+
);
|
|
2541
|
+
|
|
2542
|
+
const highPage = await app.jobService.findExecutions({
|
|
2543
|
+
priority: "high",
|
|
2544
|
+
});
|
|
2545
|
+
expect(highPage.content).toHaveLength(1);
|
|
2546
|
+
expect(highPage.content[0].priority).toBe(1);
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
it("should return execution detail with logs", async () => {
|
|
2550
|
+
class App {
|
|
2551
|
+
log = $logger();
|
|
2552
|
+
repo = $repository(jobExecutionEntity);
|
|
2553
|
+
logRepo = $repository(jobExecutionLogEntity);
|
|
2554
|
+
jobService = $inject(JobService);
|
|
2555
|
+
myJob = $job({
|
|
2556
|
+
schema: t.object({ value: t.text() }),
|
|
2557
|
+
handler: async () => {
|
|
2558
|
+
this.log.info("hello from handler");
|
|
2559
|
+
},
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2564
|
+
const app = alepha.inject(App);
|
|
2565
|
+
await alepha.start();
|
|
2566
|
+
|
|
2567
|
+
await app.myJob.push({ value: "test" });
|
|
2568
|
+
|
|
2569
|
+
await vi.waitFor(async () => {
|
|
2570
|
+
const completed = await app.repo.findMany({
|
|
2571
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2572
|
+
});
|
|
2573
|
+
expect(completed).toHaveLength(1);
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
const completed = await app.repo.findMany({
|
|
2577
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2578
|
+
});
|
|
2579
|
+
const detail = await app.jobService.getExecution(completed[0].id);
|
|
2580
|
+
|
|
2581
|
+
expect(detail.status).toBe("completed");
|
|
2582
|
+
expect(detail.can).toBeDefined();
|
|
2583
|
+
expect(detail.logs).toBeDefined();
|
|
2584
|
+
expect(detail.logs!.some((l) => l.message === "hello from handler")).toBe(
|
|
2585
|
+
true,
|
|
2586
|
+
);
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
it("should return cron jobs with last execution info", async () => {
|
|
2590
|
+
class App {
|
|
2591
|
+
repo = $repository(jobExecutionEntity);
|
|
2592
|
+
jobService = $inject(JobService);
|
|
2593
|
+
cronJob = $job({
|
|
2594
|
+
cron: "0 0 * * *",
|
|
2595
|
+
handler: async () => {},
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2600
|
+
const app = alepha.inject(App);
|
|
2601
|
+
await alepha.start();
|
|
2602
|
+
|
|
2603
|
+
await app.cronJob.trigger();
|
|
2604
|
+
|
|
2605
|
+
await vi.waitFor(async () => {
|
|
2606
|
+
const completed = await app.repo.findMany({
|
|
2607
|
+
where: { jobName: "App.cronJob", status: "completed" },
|
|
2608
|
+
});
|
|
2609
|
+
expect(completed).toHaveLength(1);
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
const cronJobs = await app.jobService.getCronJobs();
|
|
2613
|
+
expect(cronJobs).toHaveLength(1);
|
|
2614
|
+
expect(cronJobs[0].name).toBe("App.cronJob");
|
|
2615
|
+
expect(cronJobs[0].cron).toBe("0 0 * * *");
|
|
2616
|
+
expect(cronJobs[0].lastExecution).toBeDefined();
|
|
2617
|
+
expect(cronJobs[0].lastExecution!.status).toBe("completed");
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
it("should return queue depth per job", async () => {
|
|
2621
|
+
class App {
|
|
2622
|
+
repo = $repository(jobExecutionEntity);
|
|
2623
|
+
jobService = $inject(JobService);
|
|
2624
|
+
myJob = $job({
|
|
2625
|
+
schema: t.object({ value: t.text() }),
|
|
2626
|
+
handler: async () => {},
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2631
|
+
const app = alepha.inject(App);
|
|
2632
|
+
await alepha.start();
|
|
2633
|
+
|
|
2634
|
+
// Create a scheduled execution (stays in queue)
|
|
2635
|
+
await app.myJob.push({ value: "test" }, { delay: [1, "hour"] });
|
|
2636
|
+
|
|
2637
|
+
const depths = await app.jobService.getQueueDepth();
|
|
2638
|
+
const depth = depths.find((d) => d.jobName === "App.myJob");
|
|
2639
|
+
expect(depth).toBeDefined();
|
|
2640
|
+
expect(depth!.scheduled).toBe(1);
|
|
2641
|
+
expect(depth!.pending).toBe(0);
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
it("should return top failures", async () => {
|
|
2645
|
+
class App {
|
|
2646
|
+
repo = $repository(jobExecutionEntity);
|
|
2647
|
+
jobService = $inject(JobService);
|
|
2648
|
+
myJob = $job({
|
|
2649
|
+
schema: t.object({ value: t.text() }),
|
|
2650
|
+
handler: async () => {
|
|
2651
|
+
throw new Error("always fails");
|
|
2652
|
+
},
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2657
|
+
const app = alepha.inject(App);
|
|
2658
|
+
await alepha.start();
|
|
2659
|
+
|
|
2660
|
+
await app.myJob.push({ value: "test" });
|
|
2661
|
+
|
|
2662
|
+
await vi.waitFor(async () => {
|
|
2663
|
+
const dead = await app.repo.findMany({
|
|
2664
|
+
where: { jobName: "App.myJob", status: "dead" },
|
|
2665
|
+
});
|
|
2666
|
+
expect(dead).toHaveLength(1);
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
const failures = await app.jobService.getTopFailures();
|
|
2670
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
2671
|
+
const entry = failures.find((f) => f.jobName === "App.myJob");
|
|
2672
|
+
expect(entry).toBeDefined();
|
|
2673
|
+
expect(entry!.failures).toBe(1);
|
|
2674
|
+
expect(entry!.lastError).toBe("always fails");
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
it("should return activity over time", async () => {
|
|
2678
|
+
class App {
|
|
2679
|
+
repo = $repository(jobExecutionEntity);
|
|
2680
|
+
jobService = $inject(JobService);
|
|
2681
|
+
myJob = $job({
|
|
2682
|
+
schema: t.object({ value: t.text() }),
|
|
2683
|
+
handler: async () => {},
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2688
|
+
const app = alepha.inject(App);
|
|
2689
|
+
await alepha.start();
|
|
2690
|
+
|
|
2691
|
+
await app.myJob.push({ value: "test" });
|
|
2692
|
+
|
|
2693
|
+
await vi.waitFor(async () => {
|
|
2694
|
+
const completed = await app.repo.findMany({
|
|
2695
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2696
|
+
});
|
|
2697
|
+
expect(completed).toHaveLength(1);
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
const activity = await app.jobService.getActivity(7);
|
|
2701
|
+
expect(activity).toHaveLength(7);
|
|
2702
|
+
|
|
2703
|
+
// Today should have at least 1 completed
|
|
2704
|
+
const today = activity[activity.length - 1];
|
|
2705
|
+
expect(today.completed).toBeGreaterThanOrEqual(1);
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
it("should return job registry with type classification", async () => {
|
|
2709
|
+
class App {
|
|
2710
|
+
jobService = $inject(JobService);
|
|
2711
|
+
pushOnly = $job({
|
|
2712
|
+
schema: t.object({ v: t.text() }),
|
|
2713
|
+
handler: async () => {},
|
|
2714
|
+
});
|
|
2715
|
+
cronOnly = $job({
|
|
2716
|
+
cron: "0 0 * * *",
|
|
2717
|
+
handler: async () => {},
|
|
2718
|
+
});
|
|
2719
|
+
both = $job({
|
|
2720
|
+
schema: t.object({ v: t.text() }),
|
|
2721
|
+
cron: "0 0 * * *",
|
|
2722
|
+
handler: async () => {},
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2727
|
+
const app = alepha.inject(App);
|
|
2728
|
+
await alepha.start();
|
|
2729
|
+
|
|
2730
|
+
const registry = app.jobService.getRegistry();
|
|
2731
|
+
expect(registry).toHaveLength(3);
|
|
2732
|
+
|
|
2733
|
+
const push = registry.find((r) => r.name === "App.pushOnly");
|
|
2734
|
+
const cron = registry.find((r) => r.name === "App.cronOnly");
|
|
2735
|
+
const dual = registry.find((r) => r.name === "App.both");
|
|
2736
|
+
|
|
2737
|
+
expect(push!.type).toBe("push");
|
|
2738
|
+
expect(cron!.type).toBe("cron");
|
|
2739
|
+
expect(dual!.type).toBe("both");
|
|
2740
|
+
});
|
|
2741
|
+
});
|
|
2742
|
+
|
|
2743
|
+
// ----- Log purge -----
|
|
2744
|
+
|
|
2745
|
+
describe("log purge", () => {
|
|
2746
|
+
it("should delete old completed executions and their logs", async () => {
|
|
2747
|
+
class App {
|
|
2748
|
+
log = $logger();
|
|
2749
|
+
repo = $repository(jobExecutionEntity);
|
|
2750
|
+
logRepo = $repository(jobExecutionLogEntity);
|
|
2751
|
+
myJob = $job({
|
|
2752
|
+
schema: t.object({ value: t.text() }),
|
|
2753
|
+
handler: async () => {
|
|
2754
|
+
this.log.info("some log");
|
|
2755
|
+
},
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2760
|
+
const app = alepha.inject(App);
|
|
2761
|
+
await alepha.start();
|
|
2762
|
+
|
|
2763
|
+
await app.myJob.push({ value: "test" });
|
|
2764
|
+
|
|
2765
|
+
await vi.waitFor(async () => {
|
|
2766
|
+
const completed = await app.repo.findMany({
|
|
2767
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2768
|
+
});
|
|
2769
|
+
expect(completed).toHaveLength(1);
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
// Backdate completedAt to 60 days ago (retention is 30 days)
|
|
2773
|
+
const completed = await app.repo.findMany({
|
|
2774
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2775
|
+
});
|
|
2776
|
+
const sixtyDaysAgo = new Date(Date.now() - 60 * 86_400_000).toISOString();
|
|
2777
|
+
await app.repo.updateById(completed[0].id, {
|
|
2778
|
+
completedAt: sixtyDaysAgo,
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
// Verify log exists before purge
|
|
2782
|
+
const logBefore = await app.logRepo.findById(completed[0].id);
|
|
2783
|
+
expect(logBefore).toBeDefined();
|
|
2784
|
+
|
|
2785
|
+
// Run purge
|
|
2786
|
+
const provider = alepha.inject(JobProvider) as any;
|
|
2787
|
+
await provider.logPurge();
|
|
2788
|
+
|
|
2789
|
+
// Both execution and log should be deleted
|
|
2790
|
+
const execAfter = await app.repo.findById(completed[0].id);
|
|
2791
|
+
expect(execAfter).toBeUndefined();
|
|
2792
|
+
|
|
2793
|
+
const logAfter = await app.logRepo.findById(completed[0].id);
|
|
2794
|
+
expect(logAfter).toBeUndefined();
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
it("should preserve recent executions", async () => {
|
|
2798
|
+
class App {
|
|
2799
|
+
repo = $repository(jobExecutionEntity);
|
|
2800
|
+
myJob = $job({
|
|
2801
|
+
schema: t.object({ value: t.text() }),
|
|
2802
|
+
handler: async () => {},
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2807
|
+
const app = alepha.inject(App);
|
|
2808
|
+
await alepha.start();
|
|
2809
|
+
|
|
2810
|
+
await app.myJob.push({ value: "test" });
|
|
2811
|
+
|
|
2812
|
+
await vi.waitFor(async () => {
|
|
2813
|
+
const completed = await app.repo.findMany({
|
|
2814
|
+
where: { jobName: "App.myJob", status: "completed" },
|
|
2815
|
+
});
|
|
2816
|
+
expect(completed).toHaveLength(1);
|
|
2817
|
+
});
|
|
2818
|
+
|
|
2819
|
+
// Run purge — recent execution should survive
|
|
2820
|
+
const provider = alepha.inject(JobProvider) as any;
|
|
2821
|
+
await provider.logPurge();
|
|
2822
|
+
|
|
2823
|
+
const all = await app.repo.findMany({
|
|
2824
|
+
where: { jobName: "App.myJob" },
|
|
2825
|
+
});
|
|
2826
|
+
expect(all).toHaveLength(1);
|
|
2827
|
+
});
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
// ----- Locked sweeps -----
|
|
2831
|
+
|
|
2832
|
+
describe("locked sweeps", () => {
|
|
2833
|
+
it("should skip sweep when lock is held by another worker", async () => {
|
|
2834
|
+
class App {
|
|
2835
|
+
repo = $repository(jobExecutionEntity);
|
|
2836
|
+
myJob = $job({
|
|
2837
|
+
schema: t.object({ value: t.text() }),
|
|
2838
|
+
handler: async () => {},
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2843
|
+
const app = alepha.inject(App);
|
|
2844
|
+
await alepha.start();
|
|
2845
|
+
|
|
2846
|
+
// Insert a stale pending record
|
|
2847
|
+
const staleTime = new Date(Date.now() - 600_000).toISOString();
|
|
2848
|
+
await app.repo.create({
|
|
2849
|
+
jobName: "App.myJob",
|
|
2850
|
+
payload: { value: "stale" },
|
|
2851
|
+
status: "pending",
|
|
2852
|
+
priority: 2,
|
|
2853
|
+
maxAttempts: 1,
|
|
2854
|
+
createdAt: staleTime,
|
|
2855
|
+
updatedAt: staleTime,
|
|
2856
|
+
});
|
|
2857
|
+
|
|
2858
|
+
// Simulate another worker holding the lock
|
|
2859
|
+
const provider = alepha.inject(JobProvider) as any;
|
|
2860
|
+
await provider.lockProvider.set(
|
|
2861
|
+
"_alepha:jobs:recovery-lock",
|
|
2862
|
+
"other-worker,2026-01-01T00:00:00.000Z",
|
|
2863
|
+
false,
|
|
2864
|
+
300_000,
|
|
2865
|
+
);
|
|
2866
|
+
|
|
2867
|
+
// Recovery sweep should skip (lock held)
|
|
2868
|
+
await provider.recoverySweep();
|
|
2869
|
+
|
|
2870
|
+
// Job should still be pending (not dispatched)
|
|
2871
|
+
const pending = await app.repo.findMany({
|
|
2872
|
+
where: { jobName: "App.myJob", status: "pending" },
|
|
2873
|
+
});
|
|
2874
|
+
expect(pending).toHaveLength(1);
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
it("should release lock after sweep completes", async () => {
|
|
2878
|
+
class App {
|
|
2879
|
+
repo = $repository(jobExecutionEntity);
|
|
2880
|
+
myJob = $job({
|
|
2881
|
+
schema: t.object({ value: t.text() }),
|
|
2882
|
+
handler: async () => {},
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2887
|
+
const app = alepha.inject(App);
|
|
2888
|
+
await alepha.start();
|
|
2889
|
+
|
|
2890
|
+
const provider = alepha.inject(JobProvider) as any;
|
|
2891
|
+
|
|
2892
|
+
// Run sweep (should acquire and release lock)
|
|
2893
|
+
await provider.recoverySweep();
|
|
2894
|
+
|
|
2895
|
+
// Second sweep should also succeed (lock was released)
|
|
2896
|
+
await provider.recoverySweep();
|
|
2897
|
+
});
|
|
2898
|
+
});
|
|
2023
2899
|
});
|