alepha 0.20.5 → 0.20.7
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +701 -654
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +24 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +193 -166
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +52 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +40 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +639 -333
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +495 -162
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +222 -188
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +54 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +265 -236
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +332 -314
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +1001 -844
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +123 -122
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +21 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +134 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +156 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +142 -128
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +160 -13
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +106 -7
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -5
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +8 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +25 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/package.json +33 -39
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +501 -24
- package/src/api/jobs/entities/jobExecutionEntity.ts +4 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +385 -147
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +9 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- 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 +133 -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__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +367 -24
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +33 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -844
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1175
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
package/dist/api/jobs/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { $atom, $hook, $inject, $module, $state, Alepha, AlephaError, KIND, PipelinePrimitive, createPrimitive, t } from "alepha";
|
|
2
|
-
import { AlephaLock } from "alepha/lock";
|
|
2
|
+
import { AlephaLock, LockProvider } from "alepha/lock";
|
|
3
3
|
import { $queue, AlephaQueue } from "alepha/queue";
|
|
4
4
|
import { AlephaScheduler, CronProvider } from "alepha/scheduler";
|
|
5
5
|
import { $secure } from "alepha/security";
|
|
6
6
|
import { $action, NotFoundError, okSchema } from "alepha/server";
|
|
7
7
|
import { $logger, logEntrySchema } from "alepha/logger";
|
|
8
|
-
import { $entity, $repository, db, sql } from "alepha/orm";
|
|
8
|
+
import { $entity, $repository, DbEntityNotFoundError, db, sql } from "alepha/orm";
|
|
9
9
|
import { DateTimeProvider } from "alepha/datetime";
|
|
10
10
|
//#region ../../src/api/jobs/schemas/jobExecutionQuerySchema.ts
|
|
11
11
|
const jobExecutionQuerySchema = t.object({
|
|
@@ -33,11 +33,11 @@ const jobExecutionQuerySchema = t.object({
|
|
|
33
33
|
* the last N rows per job (configurable via `jobConfig.keepLastSuccess`).
|
|
34
34
|
*
|
|
35
35
|
* Status transitions:
|
|
36
|
-
* - queue push → pending
|
|
36
|
+
* - queue push → pending (or `scheduled` if `delay`/`scheduledAt` was given)
|
|
37
37
|
* - worker claim → running
|
|
38
|
-
* - success → ok
|
|
38
|
+
* - success → ok (or row deleted, depending on `record` and `keepLastSuccess`)
|
|
39
39
|
* - terminal failure → error
|
|
40
|
-
* -
|
|
40
|
+
* - retryable failure → scheduled (with scheduledAt = now; sweep picks it up)
|
|
41
41
|
* - delay → scheduled (with scheduledAt = now + delay)
|
|
42
42
|
* - sweep picks due ones → pending
|
|
43
43
|
* - cancel → cancelled
|
|
@@ -81,6 +81,11 @@ const jobExecutionEntity = $entity({
|
|
|
81
81
|
"status",
|
|
82
82
|
"scheduledAt"
|
|
83
83
|
] },
|
|
84
|
+
{ columns: [
|
|
85
|
+
"jobName",
|
|
86
|
+
"status",
|
|
87
|
+
"createdAt"
|
|
88
|
+
] },
|
|
84
89
|
{ columns: ["jobName", "startedAt"] },
|
|
85
90
|
{
|
|
86
91
|
columns: ["jobName", "key"],
|
|
@@ -90,10 +95,28 @@ const jobExecutionEntity = $entity({
|
|
|
90
95
|
});
|
|
91
96
|
//#endregion
|
|
92
97
|
//#region ../../src/api/jobs/schemas/jobExecutionResourceSchema.ts
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Public-facing schema for a job execution row.
|
|
100
|
+
*
|
|
101
|
+
* Diverges from the raw entity in two places, both for API ergonomics:
|
|
102
|
+
*
|
|
103
|
+
* - `priority` is exposed as the **string enum** (`critical`/`high`/...)
|
|
104
|
+
* instead of the numeric value used internally for SQL ordering. The
|
|
105
|
+
* `JobService` is responsible for the int → string transform.
|
|
106
|
+
* - `can` derives the available admin actions from the row's status.
|
|
107
|
+
*/
|
|
108
|
+
const jobExecutionResourceSchema = t.extend(jobExecutionEntity.schema, {
|
|
109
|
+
priority: t.enum([
|
|
110
|
+
"critical",
|
|
111
|
+
"high",
|
|
112
|
+
"normal",
|
|
113
|
+
"low"
|
|
114
|
+
]),
|
|
115
|
+
can: t.object({
|
|
116
|
+
retry: t.boolean(),
|
|
117
|
+
cancel: t.boolean()
|
|
118
|
+
})
|
|
119
|
+
}, {
|
|
97
120
|
title: "JobExecutionResource",
|
|
98
121
|
description: "A job execution row with derived actions."
|
|
99
122
|
});
|
|
@@ -102,7 +125,11 @@ const jobExecutionResourceSchema = t.extend(jobExecutionEntity.schema, { can: t.
|
|
|
102
125
|
const jobRegistrationSchema = t.object({
|
|
103
126
|
name: t.text(),
|
|
104
127
|
description: t.optional(t.text()),
|
|
105
|
-
type: t.enum([
|
|
128
|
+
type: t.enum([
|
|
129
|
+
"cron",
|
|
130
|
+
"queue",
|
|
131
|
+
"direct"
|
|
132
|
+
], { description: "Effective runtime mode. 'cron' = scheduled. 'queue' = push-driven, dispatched via AlephaApiJobsQueue. 'direct' = push-driven, processed in-process (no queue infrastructure loaded), with the sweep as the safety net." }),
|
|
106
133
|
priority: t.enum([
|
|
107
134
|
"critical",
|
|
108
135
|
"high",
|
|
@@ -111,10 +138,7 @@ const jobRegistrationSchema = t.object({
|
|
|
111
138
|
]),
|
|
112
139
|
cron: t.optional(t.text()),
|
|
113
140
|
timeout: t.optional(t.text()),
|
|
114
|
-
retry: t.optional(t.object({
|
|
115
|
-
retries: t.integer(),
|
|
116
|
-
hasBackoff: t.boolean()
|
|
117
|
-
})),
|
|
141
|
+
retry: t.optional(t.object({ retries: t.integer() })),
|
|
118
142
|
recent: t.object({
|
|
119
143
|
ok: t.integer(),
|
|
120
144
|
error: t.integer(),
|
|
@@ -130,7 +154,8 @@ const jobConfig = $atom({
|
|
|
130
154
|
name: "alepha.jobs",
|
|
131
155
|
description: "Configuration for the $job primitive.",
|
|
132
156
|
schema: t.object({
|
|
133
|
-
|
|
157
|
+
sweepCron: t.text({ description: "Cron expression for the sweep tick. Must be minute-granular at minimum (cron resolution). On Cloudflare Workers this expression is emitted into wrangler.jsonc by the build." }),
|
|
158
|
+
trimCron: t.text({ description: "Cron expression for the ring-buffer trim tick (per-job keepLastSuccess/keepLastError enforcement). Decoupled from `sweepCron` because trim is bounded by job execution rate, not retry latency — running it every sweep is wasted work for most apps." }),
|
|
134
159
|
staleThreshold: t.integer({ description: "Pending age (ms) before the sweep re-dispatches it." }),
|
|
135
160
|
runTimeout: t.integer({ description: "Running age (ms) before assumed crash (fallback when no per-job timeout)." }),
|
|
136
161
|
keepLastSuccess: t.integer({ description: "Max successful rows to keep per job. Set 0 to disable and delete on success." }),
|
|
@@ -139,7 +164,8 @@ const jobConfig = $atom({
|
|
|
139
164
|
drainTimeout: t.integer({ description: "Max time (ms) to wait for in-flight jobs during shutdown." })
|
|
140
165
|
}),
|
|
141
166
|
default: {
|
|
142
|
-
|
|
167
|
+
sweepCron: "*/5 * * * *",
|
|
168
|
+
trimCron: "0 * * * *",
|
|
143
169
|
staleThreshold: 3e5,
|
|
144
170
|
runTimeout: 18e5,
|
|
145
171
|
keepLastSuccess: 10,
|
|
@@ -149,6 +175,135 @@ const jobConfig = $atom({
|
|
|
149
175
|
}
|
|
150
176
|
});
|
|
151
177
|
//#endregion
|
|
178
|
+
//#region ../../src/api/jobs/providers/JobDispatcher.ts
|
|
179
|
+
/**
|
|
180
|
+
* Abstract dispatcher for queued/direct job executions.
|
|
181
|
+
*
|
|
182
|
+
* The default implementation, {@link DirectJobDispatcher}, runs the handler
|
|
183
|
+
* in-process after the caller's `push()` returns — fast and dependency-free.
|
|
184
|
+
*
|
|
185
|
+
* `AlephaApiJobsQueue` substitutes this with `JobQueueProvider`, which
|
|
186
|
+
* publishes the executionId to `AlephaQueue` so a worker pool can consume
|
|
187
|
+
* the work asynchronously.
|
|
188
|
+
*
|
|
189
|
+
* Substitute via DI:
|
|
190
|
+
* ```ts
|
|
191
|
+
* Alepha.create()
|
|
192
|
+
* .with({ provide: JobDispatcher, use: MyCustomDispatcher })
|
|
193
|
+
* .with(AlephaApiJobs);
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* The `kind` getter is read by the `JobProvider.effectiveMode` accessor
|
|
197
|
+
* and by the admin UI so users can see which dispatcher is currently active.
|
|
198
|
+
*/
|
|
199
|
+
var JobDispatcher = class {
|
|
200
|
+
/**
|
|
201
|
+
* Optional batch dispatch. The default implementation loops, but
|
|
202
|
+
* dispatchers backed by a real queue should override this to use the
|
|
203
|
+
* provider's batch send (e.g. Cloudflare Queues `sendBatch`).
|
|
204
|
+
*/
|
|
205
|
+
async dispatchMany(items) {
|
|
206
|
+
for (const item of items) await this.dispatch(item.jobName, item.executionId);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region ../../src/api/jobs/providers/DirectJobDispatcher.ts
|
|
211
|
+
/**
|
|
212
|
+
* Default `JobDispatcher` for environments without `AlephaApiJobsQueue`.
|
|
213
|
+
*
|
|
214
|
+
* Runs `JobProvider.processExecution` in the background — the caller's
|
|
215
|
+
* `push()` returns immediately while the handler continues to completion
|
|
216
|
+
* in the same process. The DB outbox row is the durability guarantee:
|
|
217
|
+
* if the process dies before the handler finishes, the next sweep tick
|
|
218
|
+
* picks the row up and re-dispatches.
|
|
219
|
+
*
|
|
220
|
+
* **Cloudflare Workers** — when an `executionCtx.waitUntil` is available
|
|
221
|
+
* in the alepha store at `cloudflare.waitUntil`, the dispatch wraps the
|
|
222
|
+
* background promise with `waitUntil` so the runtime keeps the isolate
|
|
223
|
+
* alive past the HTTP response. Without this, the handler would be
|
|
224
|
+
* terminated when the response is returned and only the next sweep
|
|
225
|
+
* (every 5 min by default) would re-dispatch.
|
|
226
|
+
*
|
|
227
|
+
* **Vercel / single-Node** — on long-running runtimes the event loop
|
|
228
|
+
* keeps the promise alive naturally; no special wiring is required.
|
|
229
|
+
*/
|
|
230
|
+
var DirectJobDispatcher = class extends JobDispatcher {
|
|
231
|
+
kind = "direct";
|
|
232
|
+
alepha = $inject(Alepha);
|
|
233
|
+
log = $logger();
|
|
234
|
+
jobProviderRef;
|
|
235
|
+
getJobProvider() {
|
|
236
|
+
if (!this.jobProviderRef) this.jobProviderRef = this.alepha.inject(JobProvider);
|
|
237
|
+
return this.jobProviderRef;
|
|
238
|
+
}
|
|
239
|
+
async dispatch(jobName, executionId) {
|
|
240
|
+
const promise = this.getJobProvider().processExecution(jobName, executionId).catch((err) => {
|
|
241
|
+
this.log.warn(`Direct execution failed for '${jobName}' (sweep will retry)`, err);
|
|
242
|
+
});
|
|
243
|
+
const waitUntil = this.alepha.store.get("cloudflare.waitUntil");
|
|
244
|
+
if (typeof waitUntil === "function") try {
|
|
245
|
+
waitUntil(promise);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
this.log.debug("waitUntil rejected — falling back to fire-and-track", e);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region ../../src/api/jobs/providers/JobQueueProvider.ts
|
|
253
|
+
/**
|
|
254
|
+
* Queue-backed `JobDispatcher` registered by `AlephaApiJobsQueue`.
|
|
255
|
+
*
|
|
256
|
+
* Extends {@link JobDispatcher} and substitutes the default
|
|
257
|
+
* `DirectJobDispatcher` so that `$job.push()` is delivered through
|
|
258
|
+
* `AlephaQueue` (e.g. Cloudflare Queues, Redis, in-memory) instead of
|
|
259
|
+
* being processed in-process.
|
|
260
|
+
*
|
|
261
|
+
* The class is also kept as a `JobQueueProvider` export name for backwards
|
|
262
|
+
* compatibility — it has always been the queue path's entry point.
|
|
263
|
+
*/
|
|
264
|
+
var JobQueueProvider = class extends JobDispatcher {
|
|
265
|
+
kind = "queue";
|
|
266
|
+
alepha = $inject(Alepha);
|
|
267
|
+
jobProviderRef;
|
|
268
|
+
getJobProvider() {
|
|
269
|
+
if (!this.jobProviderRef) this.jobProviderRef = this.alepha.inject(JobProvider);
|
|
270
|
+
return this.jobProviderRef;
|
|
271
|
+
}
|
|
272
|
+
queue = $queue({
|
|
273
|
+
name: "api:jobs:dispatch",
|
|
274
|
+
schema: t.object({
|
|
275
|
+
jobName: t.text(),
|
|
276
|
+
executionId: t.text()
|
|
277
|
+
}),
|
|
278
|
+
handler: async (msg) => {
|
|
279
|
+
await this.getJobProvider().processExecution(msg.payload.jobName, msg.payload.executionId);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
async dispatch(jobName, executionId) {
|
|
283
|
+
await this.queue.push({
|
|
284
|
+
jobName,
|
|
285
|
+
executionId
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Fan-out to a single variadic `queue.push(...payloads)` call so the
|
|
290
|
+
* underlying queue provider can batch the network round-trips when it
|
|
291
|
+
* supports it (Cloudflare Queues, Redis pipelines).
|
|
292
|
+
*/
|
|
293
|
+
async dispatchMany(items) {
|
|
294
|
+
if (items.length === 0) return;
|
|
295
|
+
await this.queue.push(...items);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Backwards-compatible alias for {@link dispatch}. Older code paths called
|
|
299
|
+
* `JobQueueProvider.push(jobName, executionId)` directly; new code should
|
|
300
|
+
* go through the `JobDispatcher.dispatch` API.
|
|
301
|
+
*/
|
|
302
|
+
async push(jobName, executionId) {
|
|
303
|
+
return this.dispatch(jobName, executionId);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
//#endregion
|
|
152
307
|
//#region ../../src/api/jobs/providers/JobProvider.ts
|
|
153
308
|
const PRIORITY_MAP = {
|
|
154
309
|
critical: 0,
|
|
@@ -162,29 +317,54 @@ const PRIORITY_REVERSE = {
|
|
|
162
317
|
2: "normal",
|
|
163
318
|
3: "low"
|
|
164
319
|
};
|
|
165
|
-
const SWEEP_CRON = "*/5 * * * *";
|
|
166
320
|
/**
|
|
167
|
-
* Coordinates cron
|
|
168
|
-
*
|
|
321
|
+
* Coordinates cron and push jobs with a durable outbox table and a single
|
|
322
|
+
* reconciliation sweep. The actual delivery channel (queue / direct) is
|
|
323
|
+
* abstracted behind {@link JobDispatcher}, substituted by DI:
|
|
324
|
+
*
|
|
325
|
+
* - **DirectJobDispatcher** (default, registered by `AlephaApiJobs`) —
|
|
326
|
+
* runs the handler in-process right after `push()` returns.
|
|
327
|
+
* - **QueueJobDispatcher** (registered by `AlephaApiJobsQueue`) — sends
|
|
328
|
+
* the executionId through `AlephaQueue` so a pool of workers can pick
|
|
329
|
+
* it up.
|
|
169
330
|
*
|
|
170
|
-
*
|
|
171
|
-
* push() → INSERT row (pending)
|
|
172
|
-
* worker →
|
|
331
|
+
* Push flow:
|
|
332
|
+
* push() → INSERT row (pending) → dispatcher.dispatch(jobName, id)
|
|
333
|
+
* worker → claim → UPDATE running → handler → DELETE/UPDATE on success
|
|
334
|
+
* → UPDATE error / scheduled (retry) on failure
|
|
173
335
|
*
|
|
174
|
-
* Cron
|
|
175
|
-
* scheduler tick →
|
|
336
|
+
* Cron flow:
|
|
337
|
+
* scheduler tick → acquire lock → executeInline (no retry)
|
|
338
|
+
* → enqueue + dispatch (retry declared)
|
|
176
339
|
*
|
|
177
|
-
* Sweep responsibilities (every `
|
|
340
|
+
* Sweep responsibilities (every `sweepCron`):
|
|
178
341
|
* - re-enqueue pending rows older than `staleThreshold`
|
|
179
|
-
* -
|
|
180
|
-
* - move `scheduled` rows with `scheduledAt <= now` to pending +
|
|
181
|
-
*
|
|
342
|
+
* - mark crashed running rows as failed and apply retry policy
|
|
343
|
+
* - move `scheduled` rows with `scheduledAt <= now` to pending + dispatch
|
|
344
|
+
*
|
|
345
|
+
* Trim runs on its own cron (`trimCron`, default hourly):
|
|
346
|
+
* - per-job history trimmed beyond `keepLastSuccess` / `keepLastError`
|
|
347
|
+
* - decoupled from sweep because trim cost scales with job count, not
|
|
348
|
+
* retry latency — running it every sweep is wasted work for most apps.
|
|
182
349
|
*/
|
|
183
350
|
var JobProvider = class {
|
|
184
351
|
alepha = $inject(Alepha);
|
|
185
352
|
dt = $inject(DateTimeProvider);
|
|
186
353
|
cronProvider = $inject(CronProvider);
|
|
354
|
+
lockProvider = $inject(LockProvider);
|
|
187
355
|
config = $state(jobConfig);
|
|
356
|
+
/**
|
|
357
|
+
* Resolved at first use (after the container is fully wired) — picks
|
|
358
|
+
* the queue dispatcher when `AlephaApiJobsQueue` was loaded, otherwise
|
|
359
|
+
* the direct dispatcher. Lazy because both dispatchers inject
|
|
360
|
+
* `JobProvider` themselves; resolving them at field-init time would
|
|
361
|
+
* create a circular construction.
|
|
362
|
+
*/
|
|
363
|
+
dispatcherRef;
|
|
364
|
+
get dispatcher() {
|
|
365
|
+
if (!this.dispatcherRef) this.dispatcherRef = this.alepha.has(JobQueueProvider) ? this.alepha.inject(JobQueueProvider) : this.alepha.inject(DirectJobDispatcher);
|
|
366
|
+
return this.dispatcherRef;
|
|
367
|
+
}
|
|
188
368
|
log = $logger();
|
|
189
369
|
executions = $repository(jobExecutionEntity);
|
|
190
370
|
jobs = /* @__PURE__ */ new Map();
|
|
@@ -192,22 +372,17 @@ var JobProvider = class {
|
|
|
192
372
|
abortControllers = /* @__PURE__ */ new Map();
|
|
193
373
|
perExecutionLogs = /* @__PURE__ */ new Map();
|
|
194
374
|
stopping = false;
|
|
195
|
-
/**
|
|
196
|
-
* Set by `JobQueueProvider` when `AlephaApiJobsQueue` is loaded.
|
|
197
|
-
* When null, queue-mode jobs cannot be pushed.
|
|
198
|
-
*/
|
|
199
|
-
queueDispatch = null;
|
|
200
375
|
registerJob(name, options) {
|
|
201
376
|
if (this.jobs.has(name)) throw new AlephaError(`Job already registered: ${name}`);
|
|
202
377
|
if (options.cron && options.schema) throw new AlephaError(`Job '${name}' declares both 'cron' and 'schema'. A job must be either cron-only (recurring) or queue-only (push-based). Split into two jobs.`);
|
|
203
378
|
if (!options.cron && !options.schema) throw new AlephaError(`Job '${name}' must declare either 'cron' (for recurring tasks) or 'schema' (for queue-mode tasks).`);
|
|
204
|
-
const
|
|
379
|
+
const kind = options.cron ? "cron" : "queue";
|
|
205
380
|
this.jobs.set(name, {
|
|
206
381
|
name,
|
|
207
382
|
options,
|
|
208
|
-
|
|
383
|
+
kind
|
|
209
384
|
});
|
|
210
|
-
this.log.debug(`Registered ${
|
|
385
|
+
this.log.debug(`Registered ${kind} job '${name}'`, {
|
|
211
386
|
cron: options.cron,
|
|
212
387
|
priority: options.priority ?? "normal",
|
|
213
388
|
retries: options.retry?.retries ?? 0
|
|
@@ -223,24 +398,155 @@ var JobProvider = class {
|
|
|
223
398
|
getRegisteredJobs() {
|
|
224
399
|
return this.jobs;
|
|
225
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Resolves what *actually* runs at dispatch time. Cron jobs are always
|
|
403
|
+
* "cron"; non-cron jobs delegate to the active `JobDispatcher` (queue
|
|
404
|
+
* vs. direct), which is determined by which modules the app loaded.
|
|
405
|
+
*/
|
|
406
|
+
effectiveMode(name) {
|
|
407
|
+
if (this.getRegistration(name).kind === "cron") return "cron";
|
|
408
|
+
return this.dispatcher.kind;
|
|
409
|
+
}
|
|
226
410
|
async runCron(name) {
|
|
227
411
|
const registration = this.getRegistration(name);
|
|
228
|
-
if (registration.
|
|
229
|
-
|
|
230
|
-
const executionId = crypto.randomUUID();
|
|
231
|
-
const promise = this.executeInline(registration, executionId, {
|
|
232
|
-
payload: void 0,
|
|
233
|
-
attempt: 1,
|
|
412
|
+
if (registration.kind !== "cron") throw new AlephaError(`Job '${name}' is not cron-mode`);
|
|
413
|
+
await this.runCronLocked(registration, {
|
|
234
414
|
triggeredBy: "system",
|
|
235
415
|
triggeredByName: "system (cron)"
|
|
236
416
|
});
|
|
237
|
-
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Cron-mode runner that respects the per-job distributed lock.
|
|
420
|
+
* Used by both the scheduled tick and manual `trigger()` calls so that an
|
|
421
|
+
* admin-triggered run on one instance can't race a scheduled run on another.
|
|
422
|
+
*
|
|
423
|
+
* **Two paths depending on `retry`:**
|
|
424
|
+
*
|
|
425
|
+
* - **No `retry`** — runs the handler inline. No DB row on success;
|
|
426
|
+
* error row only on failure. The "next tick" is the implicit retry.
|
|
427
|
+
* - **`retry` declared** — enqueues a synthetic execution row and hands
|
|
428
|
+
* it to the dispatcher. The handler then runs through the same path
|
|
429
|
+
* as a queue/direct push (claim, retry-on-fail, sweep recovery). Use
|
|
430
|
+
* this when a single failed tick must not block work for the whole
|
|
431
|
+
* `cron` interval (e.g. once-daily jobs).
|
|
432
|
+
*/
|
|
433
|
+
async runCronLocked(registration, ctx) {
|
|
434
|
+
if (this.stopping) return;
|
|
435
|
+
const useLock = registration.options.lock !== false;
|
|
436
|
+
if (useLock) {
|
|
437
|
+
if (!await this.acquireCronLock(registration)) {
|
|
438
|
+
this.log.debug(`Cron '${registration.name}' skipped — another instance holds the lock`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
238
442
|
try {
|
|
239
|
-
|
|
443
|
+
if (registration.options.retry) {
|
|
444
|
+
await this.enqueueCronExecution(registration, ctx);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const executionId = crypto.randomUUID();
|
|
448
|
+
const promise = this.executeInline(registration, executionId, {
|
|
449
|
+
payload: void 0,
|
|
450
|
+
attempt: 1,
|
|
451
|
+
triggeredBy: ctx.triggeredBy,
|
|
452
|
+
triggeredByName: ctx.triggeredByName
|
|
453
|
+
});
|
|
454
|
+
this.inFlight.add(promise);
|
|
455
|
+
try {
|
|
456
|
+
await promise;
|
|
457
|
+
} finally {
|
|
458
|
+
this.inFlight.delete(promise);
|
|
459
|
+
}
|
|
240
460
|
} finally {
|
|
241
|
-
this.
|
|
461
|
+
if (useLock) await this.releaseCronLock(registration);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Materialize a cron tick into the outbox so it goes through the normal
|
|
466
|
+
* retry/sweep path. Used when the user opts into `retry` on a cron job —
|
|
467
|
+
* a transient failure no longer means "wait for the next cron tick", it
|
|
468
|
+
* means "the sweep will retry within `sweepCron`".
|
|
469
|
+
*/
|
|
470
|
+
async enqueueCronExecution(registration, ctx) {
|
|
471
|
+
const opts = registration.options;
|
|
472
|
+
const maxAttempts = (opts.retry?.retries ?? 0) + 1;
|
|
473
|
+
const execution = await this.executions.create({
|
|
474
|
+
jobName: registration.name,
|
|
475
|
+
payload: void 0,
|
|
476
|
+
status: "pending",
|
|
477
|
+
priority: PRIORITY_MAP[opts.priority ?? "normal"],
|
|
478
|
+
maxAttempts,
|
|
479
|
+
triggeredBy: ctx.triggeredBy,
|
|
480
|
+
triggeredByName: ctx.triggeredByName
|
|
481
|
+
});
|
|
482
|
+
await this.dispatch(registration.name, execution.id);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Acquire a per-job NX lock keyed by `cron-job:<name>` so that a single
|
|
486
|
+
* tick across all replicas runs exactly one execution. Auto-expires after
|
|
487
|
+
* `2 * timeout` (or 5 minutes if no per-job timeout) so a crashed worker
|
|
488
|
+
* cannot permanently block the cron from firing.
|
|
489
|
+
*
|
|
490
|
+
* **Caveat — same-process double-fire is not prevented.** The lock value
|
|
491
|
+
* is a per-process holder id, so two concurrent ticks on the same process
|
|
492
|
+
* (e.g. a scheduled tick overlapping an admin `trigger()` call) will both
|
|
493
|
+
* see "we own it". This is acceptable for the multi-replica use case the
|
|
494
|
+
* lock targets; a process that overlaps its own cron handler should set a
|
|
495
|
+
* smaller `timeout` or use idempotent handler logic. A future fix can add
|
|
496
|
+
* a per-process Set guard before reaching the LockProvider.
|
|
497
|
+
*/
|
|
498
|
+
async acquireCronLock(registration) {
|
|
499
|
+
const lockKey = this.cronLockKey(registration.name);
|
|
500
|
+
const ttlMs = registration.options.timeout ? this.dt.duration(registration.options.timeout).as("milliseconds") * 2 : 300 * 1e3;
|
|
501
|
+
const value = `${this.lockHolderId},${this.dt.nowISOString()}`;
|
|
502
|
+
try {
|
|
503
|
+
const [holderId] = (await this.lockProvider.set(lockKey, value, true, ttlMs)).split(",");
|
|
504
|
+
return holderId === this.lockHolderId;
|
|
505
|
+
} catch (e) {
|
|
506
|
+
this.log.warn(`Cron lock acquire failed for '${registration.name}'`, e);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Update only when the row is still in one of the expected statuses.
|
|
512
|
+
* Logs and returns silently when the guard rejects — this happens when a
|
|
513
|
+
* concurrent operation (most often `cancel()`) has already moved the row
|
|
514
|
+
* into a terminal state. We must not overwrite that.
|
|
515
|
+
*/
|
|
516
|
+
async guardedUpdate(executionId, expectedStatuses, patch, label) {
|
|
517
|
+
try {
|
|
518
|
+
await this.executions.updateOne({
|
|
519
|
+
id: { eq: executionId },
|
|
520
|
+
status: { inArray: expectedStatuses }
|
|
521
|
+
}, patch);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
if (e instanceof DbEntityNotFoundError) {
|
|
524
|
+
this.log.debug(`${label}: row ${executionId} not in expected status — skipping write`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
throw e;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async releaseCronLock(registration) {
|
|
531
|
+
try {
|
|
532
|
+
await this.lockProvider.del(this.cronLockKey(registration.name));
|
|
533
|
+
} catch (e) {
|
|
534
|
+
this.log.debug(`Cron lock release failed for '${registration.name}' (will expire by TTL)`, e);
|
|
242
535
|
}
|
|
243
536
|
}
|
|
537
|
+
cronLockKey(jobName) {
|
|
538
|
+
return `alepha.api.jobs.cron:${jobName}`;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Stable per-process id used as the lock value — survives multiple ticks.
|
|
542
|
+
* Lazy so that Cloudflare Workers (which forbid random in global scope)
|
|
543
|
+
* stay happy.
|
|
544
|
+
*/
|
|
545
|
+
lockHolderIdValue;
|
|
546
|
+
get lockHolderId() {
|
|
547
|
+
if (!this.lockHolderIdValue) this.lockHolderIdValue = crypto.randomUUID();
|
|
548
|
+
return this.lockHolderIdValue;
|
|
549
|
+
}
|
|
244
550
|
/**
|
|
245
551
|
* Execute a cron handler inline. Records a row only on error (or always,
|
|
246
552
|
* when `record: 'all'`). No DB writes on the happy path by default.
|
|
@@ -339,7 +645,7 @@ var JobProvider = class {
|
|
|
339
645
|
}
|
|
340
646
|
async push(name, payload, options) {
|
|
341
647
|
const registration = this.getRegistration(name);
|
|
342
|
-
if (registration.
|
|
648
|
+
if (registration.kind !== "queue") throw new AlephaError(`Job '${name}' is not queue-mode (no schema declared). Use trigger() instead.`);
|
|
343
649
|
const opts = registration.options;
|
|
344
650
|
const validated = this.alepha.codec.validate(opts.schema, payload);
|
|
345
651
|
const priority = PRIORITY_MAP[options?.priority ?? opts.priority ?? "normal"];
|
|
@@ -368,7 +674,7 @@ var JobProvider = class {
|
|
|
368
674
|
triggeredBy: options.triggeredBy,
|
|
369
675
|
triggeredByName: options.triggeredByName
|
|
370
676
|
});
|
|
371
|
-
if (status === "pending") await this.
|
|
677
|
+
if (status === "pending") await this.dispatch(name, execution.id);
|
|
372
678
|
else if (status === "scheduled" && scheduledAt) this.scheduleOptimisticDispatch(name, execution.id, scheduledAt);
|
|
373
679
|
return execution.id;
|
|
374
680
|
}
|
|
@@ -382,7 +688,7 @@ var JobProvider = class {
|
|
|
382
688
|
triggeredBy: options?.triggeredBy,
|
|
383
689
|
triggeredByName: options?.triggeredByName
|
|
384
690
|
});
|
|
385
|
-
if (status === "pending") await this.
|
|
691
|
+
if (status === "pending") await this.dispatch(name, execution.id);
|
|
386
692
|
else if (status === "scheduled" && scheduledAt) this.scheduleOptimisticDispatch(name, execution.id, scheduledAt);
|
|
387
693
|
return execution.id;
|
|
388
694
|
}
|
|
@@ -401,7 +707,7 @@ var JobProvider = class {
|
|
|
401
707
|
async pushMany(name, items) {
|
|
402
708
|
if (items.length === 0) return [];
|
|
403
709
|
const registration = this.getRegistration(name);
|
|
404
|
-
if (registration.
|
|
710
|
+
if (registration.kind !== "queue") throw new AlephaError(`Job '${name}' is not queue-mode (no schema declared).`);
|
|
405
711
|
const opts = registration.options;
|
|
406
712
|
const maxAttempts = (opts.retry?.retries ?? 0) + 1;
|
|
407
713
|
const keyed = [];
|
|
@@ -440,11 +746,16 @@ var JobProvider = class {
|
|
|
440
746
|
}
|
|
441
747
|
if (bulk.length > 0) {
|
|
442
748
|
const created = await this.executions.createMany(bulk);
|
|
749
|
+
const toDispatch = [];
|
|
443
750
|
for (const exec of created) {
|
|
444
751
|
ids.push(exec.id);
|
|
445
|
-
if (exec.status === "pending" && !this.stopping)
|
|
752
|
+
if (exec.status === "pending" && !this.stopping) toDispatch.push({
|
|
753
|
+
jobName: name,
|
|
754
|
+
executionId: exec.id
|
|
755
|
+
});
|
|
446
756
|
else if (exec.status === "scheduled" && exec.scheduledAt && !this.stopping) this.scheduleOptimisticDispatch(name, exec.id, exec.scheduledAt);
|
|
447
757
|
}
|
|
758
|
+
if (toDispatch.length > 0) await this.dispatchMany(toDispatch);
|
|
448
759
|
}
|
|
449
760
|
this.log.debug(`pushMany '${name}': ${ids.length} jobs created`, {
|
|
450
761
|
bulk: bulk.length,
|
|
@@ -452,18 +763,27 @@ var JobProvider = class {
|
|
|
452
763
|
});
|
|
453
764
|
return ids;
|
|
454
765
|
}
|
|
455
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Hand a single execution to the active `JobDispatcher`. Whether that
|
|
768
|
+
* results in a queue send or in-process execution depends on which
|
|
769
|
+
* dispatcher is wired (see {@link JobDispatcher}).
|
|
770
|
+
*/
|
|
771
|
+
async dispatch(jobName, executionId) {
|
|
456
772
|
if (this.stopping) return;
|
|
457
|
-
|
|
458
|
-
|
|
773
|
+
await this.dispatcher.dispatch(jobName, executionId);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Batched variant. Used by `pushMany` so a backing queue can do a single
|
|
777
|
+
* batch network call (e.g. Cloudflare Queues `sendBatch`).
|
|
778
|
+
*/
|
|
779
|
+
async dispatchMany(items) {
|
|
780
|
+
if (this.stopping || items.length === 0) return;
|
|
781
|
+
await this.dispatcher.dispatchMany(items);
|
|
459
782
|
}
|
|
460
783
|
async trigger(name, context) {
|
|
461
784
|
const registration = this.getRegistration(name);
|
|
462
|
-
if (registration.
|
|
463
|
-
|
|
464
|
-
await this.executeInline(registration, executionId, {
|
|
465
|
-
payload: void 0,
|
|
466
|
-
attempt: 1,
|
|
785
|
+
if (registration.kind === "cron") {
|
|
786
|
+
await this.runCronLocked(registration, {
|
|
467
787
|
triggeredBy: context?.triggeredBy,
|
|
468
788
|
triggeredByName: context?.triggeredByName
|
|
469
789
|
});
|
|
@@ -499,8 +819,8 @@ var JobProvider = class {
|
|
|
499
819
|
this.log.warn(`Unknown job '${jobName}' — skipping execution`, { executionId });
|
|
500
820
|
return;
|
|
501
821
|
}
|
|
502
|
-
if (registration.
|
|
503
|
-
this.log.warn(`Job '${jobName}'
|
|
822
|
+
if (registration.kind !== "queue" && !registration.options.retry) {
|
|
823
|
+
this.log.warn(`Job '${jobName}' has no outbox path (no schema and no retry) — skipping`, { executionId });
|
|
504
824
|
return;
|
|
505
825
|
}
|
|
506
826
|
const promise = this.processQueueExecution(registration, executionId);
|
|
@@ -515,12 +835,11 @@ var JobProvider = class {
|
|
|
515
835
|
const jobName = registration.name;
|
|
516
836
|
const opts = registration.options;
|
|
517
837
|
const record = opts.record ?? "error";
|
|
518
|
-
|
|
838
|
+
const execution = await this.claim(executionId);
|
|
839
|
+
if (!execution) {
|
|
519
840
|
this.log.debug(`Execution ${executionId} already claimed, skipping`);
|
|
520
841
|
return;
|
|
521
842
|
}
|
|
522
|
-
const execution = await this.executions.findById(executionId);
|
|
523
|
-
if (!execution) return;
|
|
524
843
|
const contextId = this.alepha.context.createContextId();
|
|
525
844
|
this.perExecutionLogs.set(contextId, []);
|
|
526
845
|
const abortController = new AbortController();
|
|
@@ -581,56 +900,58 @@ var JobProvider = class {
|
|
|
581
900
|
this.perExecutionLogs.delete(contextId);
|
|
582
901
|
}
|
|
583
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Transition pending → running and return the post-update row.
|
|
905
|
+
* Two round-trips: read current attempt, then guarded UPDATE … RETURNING.
|
|
906
|
+
* Returns null when the row is gone or already claimed by another worker.
|
|
907
|
+
* The returned row replaces a separate post-claim findById, so the dispatch
|
|
908
|
+
* path is 2 queries instead of 3.
|
|
909
|
+
*/
|
|
584
910
|
async claim(executionId) {
|
|
585
|
-
const
|
|
586
|
-
if (!
|
|
911
|
+
const current = await this.executions.findById(executionId);
|
|
912
|
+
if (!current) return null;
|
|
587
913
|
try {
|
|
588
|
-
await this.executions.updateOne({
|
|
914
|
+
return await this.executions.updateOne({
|
|
589
915
|
id: { eq: executionId },
|
|
590
916
|
status: { eq: "pending" }
|
|
591
917
|
}, {
|
|
592
918
|
status: "running",
|
|
593
|
-
attempt:
|
|
919
|
+
attempt: current.attempt + 1,
|
|
594
920
|
startedAt: this.dt.nowISOString()
|
|
595
921
|
});
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
922
|
+
} catch (e) {
|
|
923
|
+
if (e instanceof DbEntityNotFoundError) return null;
|
|
924
|
+
throw e;
|
|
599
925
|
}
|
|
600
926
|
}
|
|
601
927
|
async handleFailure(executionId, registration, currentAttempt, error, contextId) {
|
|
602
928
|
const jobName = registration.name;
|
|
603
929
|
const retry = registration.options.retry;
|
|
604
930
|
const maxAttempts = (retry?.retries ?? 0) + 1;
|
|
605
|
-
if (retry && currentAttempt
|
|
606
|
-
const nextScheduledAt = this.
|
|
607
|
-
this.log.info(`Job '${jobName}' failed, scheduling retry ${currentAttempt + 1}/${maxAttempts}`, {
|
|
931
|
+
if (retry && currentAttempt < maxAttempts && (retry.when ? retry.when(error) : true)) {
|
|
932
|
+
const nextScheduledAt = this.dt.nowISOString();
|
|
933
|
+
this.log.info(`Job '${jobName}' failed, scheduling retry ${currentAttempt + 1}/${maxAttempts} (sweep will pick up)`, {
|
|
608
934
|
executionId,
|
|
609
|
-
error: error.message
|
|
610
|
-
nextScheduledAt
|
|
935
|
+
error: error.message
|
|
611
936
|
});
|
|
612
|
-
await this.
|
|
937
|
+
await this.guardedUpdate(executionId, ["running"], {
|
|
613
938
|
status: "scheduled",
|
|
614
939
|
error: error.message,
|
|
615
940
|
scheduledAt: nextScheduledAt,
|
|
616
941
|
logs: this.snapshotLogs(contextId)
|
|
617
|
-
});
|
|
618
|
-
const delayMs = Math.max(0, new Date(nextScheduledAt).getTime() - this.dt.nowMillis());
|
|
619
|
-
this.dt.createTimeout(() => {
|
|
620
|
-
this.dispatchScheduled(jobName, executionId);
|
|
621
|
-
}, delayMs);
|
|
942
|
+
}, "retry-after-failure");
|
|
622
943
|
} else {
|
|
623
944
|
this.log.info(`Job '${jobName}' dead after ${currentAttempt} attempt(s)`, {
|
|
624
945
|
executionId,
|
|
625
946
|
error: error.message
|
|
626
947
|
});
|
|
627
|
-
await this.
|
|
948
|
+
await this.guardedUpdate(executionId, ["running"], {
|
|
628
949
|
status: "error",
|
|
629
950
|
error: error.message,
|
|
630
951
|
completedAt: this.dt.nowISOString(),
|
|
631
952
|
key: null,
|
|
632
953
|
logs: this.snapshotLogs(contextId)
|
|
633
|
-
});
|
|
954
|
+
}, "terminal-failure");
|
|
634
955
|
}
|
|
635
956
|
await this.alepha.events.emit("job:error", {
|
|
636
957
|
name: jobName,
|
|
@@ -638,16 +959,6 @@ var JobProvider = class {
|
|
|
638
959
|
executionId
|
|
639
960
|
}, { catch: true });
|
|
640
961
|
}
|
|
641
|
-
computeBackoff(retry, attempt) {
|
|
642
|
-
const now = this.dt.now();
|
|
643
|
-
if (!retry.backoff) return now.add(1, "second").toISOString();
|
|
644
|
-
if (Array.isArray(retry.backoff)) return now.add(this.dt.duration(retry.backoff)).toISOString();
|
|
645
|
-
const backoff = retry.backoff;
|
|
646
|
-
let delayMs = this.dt.duration(backoff.initial).as("milliseconds") * (backoff.factor ?? 2) ** (attempt - 1);
|
|
647
|
-
if (backoff.max) delayMs = Math.min(delayMs, this.dt.duration(backoff.max).as("milliseconds"));
|
|
648
|
-
if (backoff.jitter) delayMs = delayMs * (.75 + Math.random() * .5);
|
|
649
|
-
return now.add(delayMs, "millisecond").toISOString();
|
|
650
|
-
}
|
|
651
962
|
snapshotLogs(contextId) {
|
|
652
963
|
const entries = this.perExecutionLogs.get(contextId);
|
|
653
964
|
if (!entries || entries.length === 0) return void 0;
|
|
@@ -683,7 +994,7 @@ var JobProvider = class {
|
|
|
683
994
|
for (const exec of due) {
|
|
684
995
|
if (!this.jobs.has(exec.jobName)) continue;
|
|
685
996
|
await this.executions.updateById(exec.id, { status: "pending" });
|
|
686
|
-
await this.
|
|
997
|
+
await this.dispatchSafe(exec.jobName, exec.id);
|
|
687
998
|
}
|
|
688
999
|
const staleIso = now.subtract(this.config.staleThreshold, "millisecond").toISOString();
|
|
689
1000
|
const staleWhere = this.executions.createQueryWhere();
|
|
@@ -698,7 +1009,7 @@ var JobProvider = class {
|
|
|
698
1009
|
});
|
|
699
1010
|
for (const exec of stale) {
|
|
700
1011
|
if (!this.jobs.has(exec.jobName)) continue;
|
|
701
|
-
await this.
|
|
1012
|
+
await this.dispatchSafe(exec.jobName, exec.id);
|
|
702
1013
|
}
|
|
703
1014
|
const runningWhere = this.executions.createQueryWhere();
|
|
704
1015
|
runningWhere.status = { eq: "running" };
|
|
@@ -716,14 +1027,13 @@ var JobProvider = class {
|
|
|
716
1027
|
await this.handleFailure(exec.id, reg, exec.attempt, err, "");
|
|
717
1028
|
}
|
|
718
1029
|
}
|
|
719
|
-
await this.trimRingBuffers();
|
|
720
1030
|
} catch (e) {
|
|
721
1031
|
this.log.error("Sweep failed", { error: e });
|
|
722
1032
|
}
|
|
723
1033
|
}
|
|
724
|
-
async
|
|
1034
|
+
async dispatchSafe(jobName, executionId) {
|
|
725
1035
|
try {
|
|
726
|
-
await this.
|
|
1036
|
+
await this.dispatch(jobName, executionId);
|
|
727
1037
|
} catch (e) {
|
|
728
1038
|
this.log.warn(`Sweep failed to dispatch ${jobName} (${executionId})`, e);
|
|
729
1039
|
}
|
|
@@ -740,7 +1050,7 @@ var JobProvider = class {
|
|
|
740
1050
|
id: { eq: executionId },
|
|
741
1051
|
status: { eq: "scheduled" }
|
|
742
1052
|
}, { status: "pending" });
|
|
743
|
-
await this.
|
|
1053
|
+
await this.dispatchSafe(jobName, executionId);
|
|
744
1054
|
} catch {}
|
|
745
1055
|
}
|
|
746
1056
|
async trimRingBuffers() {
|
|
@@ -759,7 +1069,7 @@ var JobProvider = class {
|
|
|
759
1069
|
status: { eq: status }
|
|
760
1070
|
},
|
|
761
1071
|
orderBy: {
|
|
762
|
-
column: "
|
|
1072
|
+
column: "createdAt",
|
|
763
1073
|
direction: "desc"
|
|
764
1074
|
},
|
|
765
1075
|
limit: keep + 50
|
|
@@ -777,10 +1087,21 @@ var JobProvider = class {
|
|
|
777
1087
|
onStart = $hook({
|
|
778
1088
|
on: "start",
|
|
779
1089
|
handler: async () => {
|
|
780
|
-
|
|
1090
|
+
const modes = {
|
|
1091
|
+
cron: 0,
|
|
1092
|
+
queue: 0,
|
|
1093
|
+
direct: 0
|
|
1094
|
+
};
|
|
1095
|
+
const perJob = {};
|
|
1096
|
+
for (const [name] of this.jobs) {
|
|
1097
|
+
const m = this.effectiveMode(name);
|
|
1098
|
+
modes[m]++;
|
|
1099
|
+
perJob[name] = m;
|
|
1100
|
+
}
|
|
781
1101
|
this.log.info(`Job system OK`, {
|
|
782
|
-
|
|
783
|
-
jobs: this.jobs.size
|
|
1102
|
+
modes,
|
|
1103
|
+
jobs: this.jobs.size,
|
|
1104
|
+
perJob
|
|
784
1105
|
});
|
|
785
1106
|
this.alepha.events.on("log", ({ entry }) => {
|
|
786
1107
|
const ctx = entry.context;
|
|
@@ -790,9 +1111,17 @@ var JobProvider = class {
|
|
|
790
1111
|
entries.push(entry);
|
|
791
1112
|
});
|
|
792
1113
|
if (!this.alepha.isServerless()) await this.sweep();
|
|
793
|
-
this.cronProvider.createCronJob("api:jobs:sweep",
|
|
1114
|
+
this.cronProvider.createCronJob("api:jobs:sweep", this.config.sweepCron, async () => {
|
|
794
1115
|
await this.sweep();
|
|
795
1116
|
}, true);
|
|
1117
|
+
this.cronProvider.createCronJob("api:jobs:trim", this.config.trimCron, async () => {
|
|
1118
|
+
if (this.stopping) return;
|
|
1119
|
+
try {
|
|
1120
|
+
await this.trimRingBuffers();
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
this.log.error("Trim failed", { error: e });
|
|
1123
|
+
}
|
|
1124
|
+
}, true);
|
|
796
1125
|
}
|
|
797
1126
|
});
|
|
798
1127
|
onStop = $hook({
|
|
@@ -889,6 +1218,19 @@ var JobService = class {
|
|
|
889
1218
|
};
|
|
890
1219
|
}
|
|
891
1220
|
/**
|
|
1221
|
+
* Convert the int-priority storage column into the public enum string.
|
|
1222
|
+
* The cast through `unknown` skips TypeScript's structural check between
|
|
1223
|
+
* the entity-level row (`priority: number`) and the resource schema
|
|
1224
|
+
* (`priority: enum`); the runtime values are correct.
|
|
1225
|
+
*/
|
|
1226
|
+
toResource(row) {
|
|
1227
|
+
return {
|
|
1228
|
+
...row,
|
|
1229
|
+
priority: PRIORITY_REVERSE[row.priority] ?? "normal",
|
|
1230
|
+
can: this.computeCan(row.status)
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
892
1234
|
* List every registered job with recent ok/error counts and lastRun.
|
|
893
1235
|
* One aggregate query covers all jobs.
|
|
894
1236
|
*/
|
|
@@ -936,14 +1278,11 @@ var JobService = class {
|
|
|
936
1278
|
result.push({
|
|
937
1279
|
name,
|
|
938
1280
|
description: opts.description,
|
|
939
|
-
type:
|
|
1281
|
+
type: this.jobProvider.effectiveMode(name),
|
|
940
1282
|
cron: opts.cron,
|
|
941
1283
|
priority: opts.priority ?? "normal",
|
|
942
1284
|
timeout: opts.timeout ? String(opts.timeout) : void 0,
|
|
943
|
-
retry: opts.retry ? {
|
|
944
|
-
retries: opts.retry.retries,
|
|
945
|
-
hasBackoff: Boolean(opts.retry.backoff)
|
|
946
|
-
} : void 0,
|
|
1285
|
+
retry: opts.retry ? { retries: opts.retry.retries } : void 0,
|
|
947
1286
|
recent: counts
|
|
948
1287
|
});
|
|
949
1288
|
}
|
|
@@ -964,10 +1303,7 @@ var JobService = class {
|
|
|
964
1303
|
direction: "desc"
|
|
965
1304
|
},
|
|
966
1305
|
limit: query.limit ?? 20
|
|
967
|
-
})).map((row) => (
|
|
968
|
-
...row,
|
|
969
|
-
can: this.computeCan(row.status)
|
|
970
|
-
}));
|
|
1306
|
+
})).map((row) => this.toResource(row));
|
|
971
1307
|
}
|
|
972
1308
|
/**
|
|
973
1309
|
* Full execution detail (includes captured logs).
|
|
@@ -975,10 +1311,7 @@ var JobService = class {
|
|
|
975
1311
|
async getExecution(id) {
|
|
976
1312
|
const execution = await this.executions.findById(id);
|
|
977
1313
|
if (!execution) throw new NotFoundError(`Execution not found: ${id}`);
|
|
978
|
-
return
|
|
979
|
-
...execution,
|
|
980
|
-
can: this.computeCan(execution.status)
|
|
981
|
-
};
|
|
1314
|
+
return this.toResource(execution);
|
|
982
1315
|
}
|
|
983
1316
|
/**
|
|
984
1317
|
* Manual trigger (cron jobs) or push-with-payload (queue jobs).
|
|
@@ -1103,69 +1436,69 @@ var AdminJobController = class {
|
|
|
1103
1436
|
});
|
|
1104
1437
|
};
|
|
1105
1438
|
//#endregion
|
|
1106
|
-
//#region ../../src/api/jobs/providers/JobQueueProvider.ts
|
|
1107
|
-
/**
|
|
1108
|
-
* Plumbs outbox-style dispatch through `AlephaQueue`.
|
|
1109
|
-
*
|
|
1110
|
-
* Registered only when the app imports `AlephaApiJobsQueue`. Sets
|
|
1111
|
-
* `JobProvider.queueDispatch` eagerly at instantiation so queue-mode jobs
|
|
1112
|
-
* can dispatch regardless of start-hook ordering.
|
|
1113
|
-
*/
|
|
1114
|
-
var JobQueueProvider = class {
|
|
1115
|
-
jobProvider = $inject(JobProvider);
|
|
1116
|
-
queue = $queue({
|
|
1117
|
-
name: "api:jobs:dispatch",
|
|
1118
|
-
schema: t.object({
|
|
1119
|
-
jobName: t.text(),
|
|
1120
|
-
executionId: t.text()
|
|
1121
|
-
}),
|
|
1122
|
-
handler: async (msg) => {
|
|
1123
|
-
await this.jobProvider.processExecution(msg.payload.jobName, msg.payload.executionId);
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
constructor() {
|
|
1127
|
-
this.wireDispatcher();
|
|
1128
|
-
}
|
|
1129
|
-
wireDispatcher() {
|
|
1130
|
-
this.jobProvider.queueDispatch = (jobName, executionId) => this.push(jobName, executionId);
|
|
1131
|
-
}
|
|
1132
|
-
async push(jobName, executionId) {
|
|
1133
|
-
await this.queue.push({
|
|
1134
|
-
jobName,
|
|
1135
|
-
executionId
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
//#endregion
|
|
1140
1439
|
//#region ../../src/api/jobs/index.ts
|
|
1141
1440
|
/**
|
|
1142
1441
|
* Job execution framework — cron and durable queue work with a single primitive.
|
|
1143
1442
|
*
|
|
1144
|
-
* A `$job` is either **cron-only** (declares `cron`) or **
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1443
|
+
* A `$job` is either **cron-only** (declares `cron`) or **payload-only** (declares `schema`).
|
|
1444
|
+
*
|
|
1445
|
+
* **Three runtime modes:**
|
|
1446
|
+
*
|
|
1447
|
+
* - **cron** — fires on a schedule. Cron-mode jobs are protected by a
|
|
1448
|
+
* distributed lock by default (`lock: true`), so multi-replica Docker
|
|
1449
|
+
* deployments only run the handler once per tick. Override with
|
|
1450
|
+
* `lock: false` if you genuinely want every replica to fire.
|
|
1451
|
+
* - **queue** — push-driven, dispatched through the queue infrastructure
|
|
1452
|
+
* (`AlephaQueue`, e.g. Cloudflare Queues, Redis). Real-time delivery,
|
|
1453
|
+
* ideal for high-volume systems. Requires `AlephaApiJobsQueue`.
|
|
1454
|
+
* - **direct** — push-driven, processed in-process right after the caller
|
|
1455
|
+
* awaits the push. The DB outbox row is the durability guarantee — if
|
|
1456
|
+
* the process dies, the reconciliation sweep re-dispatches. Default
|
|
1457
|
+
* when `AlephaApiJobsQueue` is *not* loaded. Best for cheap deployments
|
|
1458
|
+
* (Cloudflare Workers, single-instance Node) where standing up a queue
|
|
1459
|
+
* is overkill.
|
|
1460
|
+
*
|
|
1461
|
+
* **Retries** are sweep-driven across all modes (no exponential backoff).
|
|
1462
|
+
* Granularity is bounded by `sweepCron` (default 5 min). The first retry
|
|
1463
|
+
* may land anywhere from a few seconds to ~5 min later depending on when
|
|
1464
|
+
* the next sweep tick fires. Cron jobs that declare `retry` go through
|
|
1465
|
+
* the same sweep path — a transient failure no longer means waiting for
|
|
1466
|
+
* the next cron tick (useful for once-daily jobs).
|
|
1467
|
+
*
|
|
1468
|
+
* **Runtime support for cron triggers**
|
|
1147
1469
|
*
|
|
1148
|
-
* **
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1470
|
+
* - **Long-running Node / Docker** — `CronProvider` runs an in-process
|
|
1471
|
+
* timer loop. Multi-replica deployments serialize ticks via the cron
|
|
1472
|
+
* lock (see `$job.lock`).
|
|
1473
|
+
* - **Cloudflare Workers** — the build emits cron expressions into
|
|
1474
|
+
* `wrangler.jsonc`; Cloudflare invokes the worker on schedule and the
|
|
1475
|
+
* `cloudflare:scheduled` hook routes the event to the matching jobs.
|
|
1476
|
+
* - **Vercel** — the build emits cron entries into
|
|
1477
|
+
* `.vercel/output/config.json` mapped to `/_alepha/cron/:name`; the
|
|
1478
|
+
* serverless handler emits `serverless:cron` and `CronProvider` runs
|
|
1479
|
+
* the matching job. Set `CRON_SECRET` to require authenticated calls.
|
|
1152
1480
|
*
|
|
1153
1481
|
* @module alepha.api.jobs
|
|
1154
1482
|
*/
|
|
1155
1483
|
const AlephaApiJobs = $module({
|
|
1156
1484
|
name: "alepha.api.jobs",
|
|
1485
|
+
primitives: [$job],
|
|
1157
1486
|
imports: [AlephaScheduler, AlephaLock],
|
|
1158
1487
|
services: [
|
|
1159
1488
|
JobProvider,
|
|
1160
1489
|
JobService,
|
|
1161
|
-
AdminJobController
|
|
1490
|
+
AdminJobController,
|
|
1491
|
+
DirectJobDispatcher
|
|
1162
1492
|
]
|
|
1163
1493
|
});
|
|
1164
1494
|
/**
|
|
1165
1495
|
* Queue support for `$job`. Import alongside {@link AlephaApiJobs} when your
|
|
1166
|
-
* app declares queue-mode jobs (any `$job` with a `schema`)
|
|
1496
|
+
* app declares queue-mode jobs (any `$job` with a `schema`) and you want a
|
|
1497
|
+
* real queue (e.g. Cloudflare Queues, Redis) instead of in-process direct
|
|
1498
|
+
* execution.
|
|
1167
1499
|
*
|
|
1168
|
-
* Adds `JobQueueProvider`
|
|
1500
|
+
* Adds `JobQueueProvider` to the container. `JobProvider` detects its
|
|
1501
|
+
* presence at start-up and routes dispatches through it.
|
|
1169
1502
|
*
|
|
1170
1503
|
* @module alepha.api.jobs.queue
|
|
1171
1504
|
*/
|
|
@@ -1175,6 +1508,6 @@ const AlephaApiJobsQueue = $module({
|
|
|
1175
1508
|
services: [JobQueueProvider]
|
|
1176
1509
|
});
|
|
1177
1510
|
//#endregion
|
|
1178
|
-
export { $job, AdminJobController, AlephaApiJobs, AlephaApiJobsQueue, JobPrimitive, JobProvider, JobQueueProvider, JobService, PRIORITY_MAP, PRIORITY_REVERSE, jobConfig, jobExecutionEntity, jobExecutionQuerySchema, jobExecutionResourceSchema, jobRegistrationSchema, triggerJobSchema };
|
|
1511
|
+
export { $job, AdminJobController, AlephaApiJobs, AlephaApiJobsQueue, DirectJobDispatcher, JobDispatcher, JobPrimitive, JobProvider, JobQueueProvider, JobService, PRIORITY_MAP, PRIORITY_REVERSE, jobConfig, jobExecutionEntity, jobExecutionQuerySchema, jobExecutionResourceSchema, jobRegistrationSchema, triggerJobSchema };
|
|
1179
1512
|
|
|
1180
1513
|
//# sourceMappingURL=index.js.map
|