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
|
@@ -8,21 +8,20 @@ import {
|
|
|
8
8
|
type TSchema,
|
|
9
9
|
} from "alepha";
|
|
10
10
|
import { DateTimeProvider, type DurationLike } from "alepha/datetime";
|
|
11
|
+
import { LockProvider } from "alepha/lock";
|
|
11
12
|
import type { LogEntry } from "alepha/logger";
|
|
12
13
|
import { $logger } from "alepha/logger";
|
|
13
|
-
import { $repository } from "alepha/orm";
|
|
14
|
+
import { $repository, DbEntityNotFoundError } from "alepha/orm";
|
|
14
15
|
import { CronProvider } from "alepha/scheduler";
|
|
15
16
|
import {
|
|
16
17
|
type JobStatus,
|
|
17
18
|
jobExecutionEntity,
|
|
18
19
|
} from "../entities/jobExecutionEntity.ts";
|
|
19
|
-
import type {
|
|
20
|
-
JobPrimitiveOptions,
|
|
21
|
-
JobPriority,
|
|
22
|
-
JobRetryBackoff,
|
|
23
|
-
JobRetryOptions,
|
|
24
|
-
} from "../primitives/$job.ts";
|
|
20
|
+
import type { JobPrimitiveOptions, JobPriority } from "../primitives/$job.ts";
|
|
25
21
|
import { jobConfig } from "../schemas/jobConfigAtom.ts";
|
|
22
|
+
import { DirectJobDispatcher } from "./DirectJobDispatcher.ts";
|
|
23
|
+
import type { JobDispatcher } from "./JobDispatcher.ts";
|
|
24
|
+
import { JobQueueProvider } from "./JobQueueProvider.ts";
|
|
26
25
|
|
|
27
26
|
// -----------------------------------------------------------------------------------------------------------------
|
|
28
27
|
|
|
@@ -40,8 +39,6 @@ const PRIORITY_REVERSE: Record<number, JobPriority> = {
|
|
|
40
39
|
3: "low",
|
|
41
40
|
};
|
|
42
41
|
|
|
43
|
-
const SWEEP_CRON = "*/5 * * * *";
|
|
44
|
-
|
|
45
42
|
// -----------------------------------------------------------------------------------------------------------------
|
|
46
43
|
|
|
47
44
|
export interface PushOptions {
|
|
@@ -72,36 +69,78 @@ export interface CancelContext {
|
|
|
72
69
|
cancelledByName?: string;
|
|
73
70
|
}
|
|
74
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The declared shape of the job (set at registration time).
|
|
74
|
+
*
|
|
75
|
+
* **Important** — this `kind` is the *declared* form. The *effective*
|
|
76
|
+
* runtime mode (cron / queue / direct) is exposed by
|
|
77
|
+
* `JobProvider.effectiveMode(name)` and the `JobRegistration.type` field on
|
|
78
|
+
* the admin schema. Don't conflate the two: a `queue` kind can run as
|
|
79
|
+
* `direct` at runtime when no queue dispatcher is loaded.
|
|
80
|
+
*/
|
|
75
81
|
interface JobRuntimeRegistration {
|
|
76
82
|
name: string;
|
|
77
83
|
options: JobPrimitiveOptions;
|
|
78
|
-
|
|
84
|
+
kind: "cron" | "queue";
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
export type JobEffectiveMode = "cron" | "queue" | "direct";
|
|
88
|
+
|
|
81
89
|
// -----------------------------------------------------------------------------------------------------------------
|
|
82
90
|
|
|
83
91
|
/**
|
|
84
|
-
* Coordinates cron
|
|
85
|
-
*
|
|
92
|
+
* Coordinates cron and push jobs with a durable outbox table and a single
|
|
93
|
+
* reconciliation sweep. The actual delivery channel (queue / direct) is
|
|
94
|
+
* abstracted behind {@link JobDispatcher}, substituted by DI:
|
|
86
95
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
96
|
+
* - **DirectJobDispatcher** (default, registered by `AlephaApiJobs`) —
|
|
97
|
+
* runs the handler in-process right after `push()` returns.
|
|
98
|
+
* - **QueueJobDispatcher** (registered by `AlephaApiJobsQueue`) — sends
|
|
99
|
+
* the executionId through `AlephaQueue` so a pool of workers can pick
|
|
100
|
+
* it up.
|
|
90
101
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
102
|
+
* Push flow:
|
|
103
|
+
* push() → INSERT row (pending) → dispatcher.dispatch(jobName, id)
|
|
104
|
+
* worker → claim → UPDATE running → handler → DELETE/UPDATE on success
|
|
105
|
+
* → UPDATE error / scheduled (retry) on failure
|
|
93
106
|
*
|
|
94
|
-
*
|
|
107
|
+
* Cron flow:
|
|
108
|
+
* scheduler tick → acquire lock → executeInline (no retry)
|
|
109
|
+
* → enqueue + dispatch (retry declared)
|
|
110
|
+
*
|
|
111
|
+
* Sweep responsibilities (every `sweepCron`):
|
|
95
112
|
* - re-enqueue pending rows older than `staleThreshold`
|
|
96
|
-
* -
|
|
97
|
-
* - move `scheduled` rows with `scheduledAt <= now` to pending +
|
|
98
|
-
*
|
|
113
|
+
* - mark crashed running rows as failed and apply retry policy
|
|
114
|
+
* - move `scheduled` rows with `scheduledAt <= now` to pending + dispatch
|
|
115
|
+
*
|
|
116
|
+
* Trim runs on its own cron (`trimCron`, default hourly):
|
|
117
|
+
* - per-job history trimmed beyond `keepLastSuccess` / `keepLastError`
|
|
118
|
+
* - decoupled from sweep because trim cost scales with job count, not
|
|
119
|
+
* retry latency — running it every sweep is wasted work for most apps.
|
|
99
120
|
*/
|
|
100
121
|
export class JobProvider {
|
|
101
122
|
protected readonly alepha = $inject(Alepha);
|
|
102
123
|
protected readonly dt = $inject(DateTimeProvider);
|
|
103
124
|
protected readonly cronProvider = $inject(CronProvider);
|
|
125
|
+
protected readonly lockProvider = $inject(LockProvider);
|
|
104
126
|
protected readonly config = $state(jobConfig);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolved at first use (after the container is fully wired) — picks
|
|
130
|
+
* the queue dispatcher when `AlephaApiJobsQueue` was loaded, otherwise
|
|
131
|
+
* the direct dispatcher. Lazy because both dispatchers inject
|
|
132
|
+
* `JobProvider` themselves; resolving them at field-init time would
|
|
133
|
+
* create a circular construction.
|
|
134
|
+
*/
|
|
135
|
+
protected dispatcherRef?: JobDispatcher;
|
|
136
|
+
public get dispatcher(): JobDispatcher {
|
|
137
|
+
if (!this.dispatcherRef) {
|
|
138
|
+
this.dispatcherRef = this.alepha.has(JobQueueProvider)
|
|
139
|
+
? this.alepha.inject(JobQueueProvider)
|
|
140
|
+
: this.alepha.inject(DirectJobDispatcher);
|
|
141
|
+
}
|
|
142
|
+
return this.dispatcherRef;
|
|
143
|
+
}
|
|
105
144
|
protected readonly log = $logger();
|
|
106
145
|
protected readonly executions = $repository(jobExecutionEntity);
|
|
107
146
|
|
|
@@ -111,14 +150,6 @@ export class JobProvider {
|
|
|
111
150
|
protected readonly perExecutionLogs = new Map<string, LogEntry[]>();
|
|
112
151
|
protected stopping = false;
|
|
113
152
|
|
|
114
|
-
/**
|
|
115
|
-
* Set by `JobQueueProvider` when `AlephaApiJobsQueue` is loaded.
|
|
116
|
-
* When null, queue-mode jobs cannot be pushed.
|
|
117
|
-
*/
|
|
118
|
-
public queueDispatch:
|
|
119
|
-
| ((jobName: string, executionId: string) => Promise<void>)
|
|
120
|
-
| null = null;
|
|
121
|
-
|
|
122
153
|
// --- Registration -----------------------------------------------------------------------------------------------
|
|
123
154
|
|
|
124
155
|
public registerJob(name: string, options: JobPrimitiveOptions): void {
|
|
@@ -136,9 +167,9 @@ export class JobProvider {
|
|
|
136
167
|
);
|
|
137
168
|
}
|
|
138
169
|
|
|
139
|
-
const
|
|
140
|
-
this.jobs.set(name, { name, options,
|
|
141
|
-
this.log.debug(`Registered ${
|
|
170
|
+
const kind: "cron" | "queue" = options.cron ? "cron" : "queue";
|
|
171
|
+
this.jobs.set(name, { name, options, kind });
|
|
172
|
+
this.log.debug(`Registered ${kind} job '${name}'`, {
|
|
142
173
|
cron: options.cron,
|
|
143
174
|
priority: options.priority ?? "normal",
|
|
144
175
|
retries: options.retry?.retries ?? 0,
|
|
@@ -159,28 +190,200 @@ export class JobProvider {
|
|
|
159
190
|
return this.jobs;
|
|
160
191
|
}
|
|
161
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Resolves what *actually* runs at dispatch time. Cron jobs are always
|
|
195
|
+
* "cron"; non-cron jobs delegate to the active `JobDispatcher` (queue
|
|
196
|
+
* vs. direct), which is determined by which modules the app loaded.
|
|
197
|
+
*/
|
|
198
|
+
public effectiveMode(name: string): JobEffectiveMode {
|
|
199
|
+
const reg = this.getRegistration(name);
|
|
200
|
+
if (reg.kind === "cron") return "cron";
|
|
201
|
+
return this.dispatcher.kind;
|
|
202
|
+
}
|
|
203
|
+
|
|
162
204
|
// --- Cron execution (inline, no queue) --------------------------------------------------------------------------
|
|
163
205
|
|
|
164
206
|
protected async runCron(name: string): Promise<void> {
|
|
165
207
|
const registration = this.getRegistration(name);
|
|
166
|
-
if (registration.
|
|
208
|
+
if (registration.kind !== "cron") {
|
|
167
209
|
throw new AlephaError(`Job '${name}' is not cron-mode`);
|
|
168
210
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const executionId = crypto.randomUUID();
|
|
172
|
-
const promise = this.executeInline(registration, executionId, {
|
|
173
|
-
payload: undefined,
|
|
174
|
-
attempt: 1,
|
|
211
|
+
await this.runCronLocked(registration, {
|
|
175
212
|
triggeredBy: "system",
|
|
176
213
|
triggeredByName: "system (cron)",
|
|
177
214
|
});
|
|
178
|
-
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Cron-mode runner that respects the per-job distributed lock.
|
|
219
|
+
* Used by both the scheduled tick and manual `trigger()` calls so that an
|
|
220
|
+
* admin-triggered run on one instance can't race a scheduled run on another.
|
|
221
|
+
*
|
|
222
|
+
* **Two paths depending on `retry`:**
|
|
223
|
+
*
|
|
224
|
+
* - **No `retry`** — runs the handler inline. No DB row on success;
|
|
225
|
+
* error row only on failure. The "next tick" is the implicit retry.
|
|
226
|
+
* - **`retry` declared** — enqueues a synthetic execution row and hands
|
|
227
|
+
* it to the dispatcher. The handler then runs through the same path
|
|
228
|
+
* as a queue/direct push (claim, retry-on-fail, sweep recovery). Use
|
|
229
|
+
* this when a single failed tick must not block work for the whole
|
|
230
|
+
* `cron` interval (e.g. once-daily jobs).
|
|
231
|
+
*/
|
|
232
|
+
protected async runCronLocked(
|
|
233
|
+
registration: JobRuntimeRegistration,
|
|
234
|
+
ctx: { triggeredBy?: string; triggeredByName?: string },
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
if (this.stopping) return;
|
|
237
|
+
|
|
238
|
+
const useLock = registration.options.lock !== false;
|
|
239
|
+
if (useLock) {
|
|
240
|
+
const acquired = await this.acquireCronLock(registration);
|
|
241
|
+
if (!acquired) {
|
|
242
|
+
this.log.debug(
|
|
243
|
+
`Cron '${registration.name}' skipped — another instance holds the lock`,
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
179
249
|
try {
|
|
180
|
-
|
|
250
|
+
if (registration.options.retry) {
|
|
251
|
+
await this.enqueueCronExecution(registration, ctx);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const executionId = crypto.randomUUID();
|
|
256
|
+
const promise = this.executeInline(registration, executionId, {
|
|
257
|
+
payload: undefined,
|
|
258
|
+
attempt: 1,
|
|
259
|
+
triggeredBy: ctx.triggeredBy,
|
|
260
|
+
triggeredByName: ctx.triggeredByName,
|
|
261
|
+
});
|
|
262
|
+
this.inFlight.add(promise);
|
|
263
|
+
try {
|
|
264
|
+
await promise;
|
|
265
|
+
} finally {
|
|
266
|
+
this.inFlight.delete(promise);
|
|
267
|
+
}
|
|
181
268
|
} finally {
|
|
182
|
-
|
|
269
|
+
if (useLock) {
|
|
270
|
+
await this.releaseCronLock(registration);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Materialize a cron tick into the outbox so it goes through the normal
|
|
277
|
+
* retry/sweep path. Used when the user opts into `retry` on a cron job —
|
|
278
|
+
* a transient failure no longer means "wait for the next cron tick", it
|
|
279
|
+
* means "the sweep will retry within `sweepCron`".
|
|
280
|
+
*/
|
|
281
|
+
protected async enqueueCronExecution(
|
|
282
|
+
registration: JobRuntimeRegistration,
|
|
283
|
+
ctx: { triggeredBy?: string; triggeredByName?: string },
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const opts = registration.options;
|
|
286
|
+
const maxAttempts = (opts.retry?.retries ?? 0) + 1;
|
|
287
|
+
const execution = await this.executions.create({
|
|
288
|
+
jobName: registration.name,
|
|
289
|
+
payload: undefined,
|
|
290
|
+
status: "pending",
|
|
291
|
+
priority: PRIORITY_MAP[opts.priority ?? "normal"],
|
|
292
|
+
maxAttempts,
|
|
293
|
+
triggeredBy: ctx.triggeredBy,
|
|
294
|
+
triggeredByName: ctx.triggeredByName,
|
|
295
|
+
});
|
|
296
|
+
await this.dispatch(registration.name, execution.id);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Acquire a per-job NX lock keyed by `cron-job:<name>` so that a single
|
|
301
|
+
* tick across all replicas runs exactly one execution. Auto-expires after
|
|
302
|
+
* `2 * timeout` (or 5 minutes if no per-job timeout) so a crashed worker
|
|
303
|
+
* cannot permanently block the cron from firing.
|
|
304
|
+
*
|
|
305
|
+
* **Caveat — same-process double-fire is not prevented.** The lock value
|
|
306
|
+
* is a per-process holder id, so two concurrent ticks on the same process
|
|
307
|
+
* (e.g. a scheduled tick overlapping an admin `trigger()` call) will both
|
|
308
|
+
* see "we own it". This is acceptable for the multi-replica use case the
|
|
309
|
+
* lock targets; a process that overlaps its own cron handler should set a
|
|
310
|
+
* smaller `timeout` or use idempotent handler logic. A future fix can add
|
|
311
|
+
* a per-process Set guard before reaching the LockProvider.
|
|
312
|
+
*/
|
|
313
|
+
protected async acquireCronLock(
|
|
314
|
+
registration: JobRuntimeRegistration,
|
|
315
|
+
): Promise<boolean> {
|
|
316
|
+
const lockKey = this.cronLockKey(registration.name);
|
|
317
|
+
const ttlMs = registration.options.timeout
|
|
318
|
+
? this.dt.duration(registration.options.timeout).as("milliseconds") * 2
|
|
319
|
+
: 5 * 60 * 1000;
|
|
320
|
+
const value = `${this.lockHolderId},${this.dt.nowISOString()}`;
|
|
321
|
+
try {
|
|
322
|
+
const stored = await this.lockProvider.set(lockKey, value, true, ttlMs);
|
|
323
|
+
const [holderId] = stored.split(",");
|
|
324
|
+
return holderId === this.lockHolderId;
|
|
325
|
+
} catch (e) {
|
|
326
|
+
this.log.warn(`Cron lock acquire failed for '${registration.name}'`, e);
|
|
327
|
+
return true; // Fail-open: better to risk a duplicate than to silently miss a tick.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Update only when the row is still in one of the expected statuses.
|
|
333
|
+
* Logs and returns silently when the guard rejects — this happens when a
|
|
334
|
+
* concurrent operation (most often `cancel()`) has already moved the row
|
|
335
|
+
* into a terminal state. We must not overwrite that.
|
|
336
|
+
*/
|
|
337
|
+
protected async guardedUpdate(
|
|
338
|
+
executionId: string,
|
|
339
|
+
expectedStatuses: JobStatus[],
|
|
340
|
+
patch: Parameters<typeof this.executions.updateById>[1],
|
|
341
|
+
label: string,
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
try {
|
|
344
|
+
await this.executions.updateOne(
|
|
345
|
+
{ id: { eq: executionId }, status: { inArray: expectedStatuses } },
|
|
346
|
+
patch,
|
|
347
|
+
);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
if (e instanceof DbEntityNotFoundError) {
|
|
350
|
+
this.log.debug(
|
|
351
|
+
`${label}: row ${executionId} not in expected status — skipping write`,
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
throw e;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
protected async releaseCronLock(
|
|
360
|
+
registration: JobRuntimeRegistration,
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
try {
|
|
363
|
+
await this.lockProvider.del(this.cronLockKey(registration.name));
|
|
364
|
+
} catch (e) {
|
|
365
|
+
this.log.debug(
|
|
366
|
+
`Cron lock release failed for '${registration.name}' (will expire by TTL)`,
|
|
367
|
+
e,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
protected cronLockKey(jobName: string): string {
|
|
373
|
+
return `alepha.api.jobs.cron:${jobName}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Stable per-process id used as the lock value — survives multiple ticks.
|
|
378
|
+
* Lazy so that Cloudflare Workers (which forbid random in global scope)
|
|
379
|
+
* stay happy.
|
|
380
|
+
*/
|
|
381
|
+
protected lockHolderIdValue?: string;
|
|
382
|
+
protected get lockHolderId(): string {
|
|
383
|
+
if (!this.lockHolderIdValue) {
|
|
384
|
+
this.lockHolderIdValue = crypto.randomUUID();
|
|
183
385
|
}
|
|
386
|
+
return this.lockHolderIdValue;
|
|
184
387
|
}
|
|
185
388
|
|
|
186
389
|
/**
|
|
@@ -328,7 +531,7 @@ export class JobProvider {
|
|
|
328
531
|
options?: PushOptions,
|
|
329
532
|
): Promise<string> {
|
|
330
533
|
const registration = this.getRegistration(name);
|
|
331
|
-
if (registration.
|
|
534
|
+
if (registration.kind !== "queue") {
|
|
332
535
|
throw new AlephaError(
|
|
333
536
|
`Job '${name}' is not queue-mode (no schema declared). Use trigger() instead.`,
|
|
334
537
|
);
|
|
@@ -375,7 +578,7 @@ export class JobProvider {
|
|
|
375
578
|
triggeredByName: options.triggeredByName,
|
|
376
579
|
});
|
|
377
580
|
if (status === "pending") {
|
|
378
|
-
await this.
|
|
581
|
+
await this.dispatch(name, execution.id);
|
|
379
582
|
} else if (status === "scheduled" && scheduledAt) {
|
|
380
583
|
this.scheduleOptimisticDispatch(name, execution.id, scheduledAt);
|
|
381
584
|
}
|
|
@@ -394,7 +597,7 @@ export class JobProvider {
|
|
|
394
597
|
});
|
|
395
598
|
|
|
396
599
|
if (status === "pending") {
|
|
397
|
-
await this.
|
|
600
|
+
await this.dispatch(name, execution.id);
|
|
398
601
|
} else if (status === "scheduled" && scheduledAt) {
|
|
399
602
|
this.scheduleOptimisticDispatch(name, execution.id, scheduledAt);
|
|
400
603
|
}
|
|
@@ -428,7 +631,7 @@ export class JobProvider {
|
|
|
428
631
|
if (items.length === 0) return [];
|
|
429
632
|
|
|
430
633
|
const registration = this.getRegistration(name);
|
|
431
|
-
if (registration.
|
|
634
|
+
if (registration.kind !== "queue") {
|
|
432
635
|
throw new AlephaError(
|
|
433
636
|
`Job '${name}' is not queue-mode (no schema declared).`,
|
|
434
637
|
);
|
|
@@ -487,10 +690,13 @@ export class JobProvider {
|
|
|
487
690
|
|
|
488
691
|
if (bulk.length > 0) {
|
|
489
692
|
const created = await this.executions.createMany(bulk);
|
|
693
|
+
// Collect pending rows so we hand them to the dispatcher in a single
|
|
694
|
+
// batch — the queue dispatcher can fan them out in one network call.
|
|
695
|
+
const toDispatch: Array<{ jobName: string; executionId: string }> = [];
|
|
490
696
|
for (const exec of created) {
|
|
491
697
|
ids.push(exec.id);
|
|
492
698
|
if (exec.status === "pending" && !this.stopping) {
|
|
493
|
-
|
|
699
|
+
toDispatch.push({ jobName: name, executionId: exec.id });
|
|
494
700
|
} else if (
|
|
495
701
|
exec.status === "scheduled" &&
|
|
496
702
|
exec.scheduledAt &&
|
|
@@ -499,6 +705,9 @@ export class JobProvider {
|
|
|
499
705
|
this.scheduleOptimisticDispatch(name, exec.id, exec.scheduledAt);
|
|
500
706
|
}
|
|
501
707
|
}
|
|
708
|
+
if (toDispatch.length > 0) {
|
|
709
|
+
await this.dispatchMany(toDispatch);
|
|
710
|
+
}
|
|
502
711
|
}
|
|
503
712
|
|
|
504
713
|
this.log.debug(`pushMany '${name}': ${ids.length} jobs created`, {
|
|
@@ -509,17 +718,28 @@ export class JobProvider {
|
|
|
509
718
|
return ids;
|
|
510
719
|
}
|
|
511
720
|
|
|
512
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Hand a single execution to the active `JobDispatcher`. Whether that
|
|
723
|
+
* results in a queue send or in-process execution depends on which
|
|
724
|
+
* dispatcher is wired (see {@link JobDispatcher}).
|
|
725
|
+
*/
|
|
726
|
+
protected async dispatch(
|
|
513
727
|
jobName: string,
|
|
514
728
|
executionId: string,
|
|
515
729
|
): Promise<void> {
|
|
516
730
|
if (this.stopping) return;
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
731
|
+
await this.dispatcher.dispatch(jobName, executionId);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Batched variant. Used by `pushMany` so a backing queue can do a single
|
|
736
|
+
* batch network call (e.g. Cloudflare Queues `sendBatch`).
|
|
737
|
+
*/
|
|
738
|
+
protected async dispatchMany(
|
|
739
|
+
items: Array<{ jobName: string; executionId: string }>,
|
|
740
|
+
): Promise<void> {
|
|
741
|
+
if (this.stopping || items.length === 0) return;
|
|
742
|
+
await this.dispatcher.dispatchMany(items);
|
|
523
743
|
}
|
|
524
744
|
|
|
525
745
|
// --- Manual trigger (admin / CLI) ------------------------------------------------------------------------------
|
|
@@ -530,11 +750,8 @@ export class JobProvider {
|
|
|
530
750
|
): Promise<void> {
|
|
531
751
|
const registration = this.getRegistration(name);
|
|
532
752
|
|
|
533
|
-
if (registration.
|
|
534
|
-
|
|
535
|
-
await this.executeInline(registration, executionId, {
|
|
536
|
-
payload: undefined,
|
|
537
|
-
attempt: 1,
|
|
753
|
+
if (registration.kind === "cron") {
|
|
754
|
+
await this.runCronLocked(registration, {
|
|
538
755
|
triggeredBy: context?.triggeredBy,
|
|
539
756
|
triggeredByName: context?.triggeredByName,
|
|
540
757
|
});
|
|
@@ -603,10 +820,13 @@ export class JobProvider {
|
|
|
603
820
|
});
|
|
604
821
|
return;
|
|
605
822
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
823
|
+
// Both `queue` and cron-with-retry execute through the outbox path —
|
|
824
|
+
// the DB-level `claim()` is the actual concurrency guard.
|
|
825
|
+
if (registration.kind !== "queue" && !registration.options.retry) {
|
|
826
|
+
this.log.warn(
|
|
827
|
+
`Job '${jobName}' has no outbox path (no schema and no retry) — skipping`,
|
|
828
|
+
{ executionId },
|
|
829
|
+
);
|
|
610
830
|
return;
|
|
611
831
|
}
|
|
612
832
|
|
|
@@ -627,15 +847,12 @@ export class JobProvider {
|
|
|
627
847
|
const opts = registration.options;
|
|
628
848
|
const record = opts.record ?? "error";
|
|
629
849
|
|
|
630
|
-
const
|
|
631
|
-
if (!
|
|
850
|
+
const execution = await this.claim(executionId);
|
|
851
|
+
if (!execution) {
|
|
632
852
|
this.log.debug(`Execution ${executionId} already claimed, skipping`);
|
|
633
853
|
return;
|
|
634
854
|
}
|
|
635
855
|
|
|
636
|
-
const execution = await this.executions.findById(executionId);
|
|
637
|
-
if (!execution) return;
|
|
638
|
-
|
|
639
856
|
const contextId = this.alepha.context.createContextId();
|
|
640
857
|
this.perExecutionLogs.set(contextId, []);
|
|
641
858
|
|
|
@@ -726,21 +943,28 @@ export class JobProvider {
|
|
|
726
943
|
}
|
|
727
944
|
}
|
|
728
945
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
946
|
+
/**
|
|
947
|
+
* Transition pending → running and return the post-update row.
|
|
948
|
+
* Two round-trips: read current attempt, then guarded UPDATE … RETURNING.
|
|
949
|
+
* Returns null when the row is gone or already claimed by another worker.
|
|
950
|
+
* The returned row replaces a separate post-claim findById, so the dispatch
|
|
951
|
+
* path is 2 queries instead of 3.
|
|
952
|
+
*/
|
|
953
|
+
protected async claim(executionId: string) {
|
|
954
|
+
const current = await this.executions.findById(executionId);
|
|
955
|
+
if (!current) return null;
|
|
732
956
|
try {
|
|
733
|
-
await this.executions.updateOne(
|
|
957
|
+
return await this.executions.updateOne(
|
|
734
958
|
{ id: { eq: executionId }, status: { eq: "pending" } },
|
|
735
959
|
{
|
|
736
960
|
status: "running",
|
|
737
|
-
attempt:
|
|
961
|
+
attempt: current.attempt + 1,
|
|
738
962
|
startedAt: this.dt.nowISOString(),
|
|
739
963
|
},
|
|
740
964
|
);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
965
|
+
} catch (e) {
|
|
966
|
+
if (e instanceof DbEntityNotFoundError) return null;
|
|
967
|
+
throw e;
|
|
744
968
|
}
|
|
745
969
|
}
|
|
746
970
|
|
|
@@ -756,46 +980,67 @@ export class JobProvider {
|
|
|
756
980
|
const retry = opts.retry;
|
|
757
981
|
const maxAttempts = (retry?.retries ?? 0) + 1;
|
|
758
982
|
|
|
983
|
+
// `retries: 2` means "1 initial + 2 retries = 3 total attempts". Retry
|
|
984
|
+
// while we have not yet *executed* the maxAttempts'th attempt — i.e.
|
|
985
|
+
// currentAttempt (the one that just failed) is strictly less than
|
|
986
|
+
// maxAttempts. The off-by-one fix: previously this was
|
|
987
|
+
// `currentAttempt + 1 < maxAttempts`, which only ran 2 attempts for
|
|
988
|
+
// `retries: 2`.
|
|
759
989
|
const canRetry =
|
|
760
990
|
retry &&
|
|
761
|
-
currentAttempt
|
|
991
|
+
currentAttempt < maxAttempts &&
|
|
762
992
|
(retry.when ? retry.when(error) : true);
|
|
763
993
|
|
|
764
994
|
if (canRetry) {
|
|
765
|
-
|
|
995
|
+
// Retries are sweep-driven: write the row as `scheduled` with
|
|
996
|
+
// `scheduledAt = now`. The next sweep tick (every `sweepCron`,
|
|
997
|
+
// default 5 minutes) re-dispatches it. This means the first retry
|
|
998
|
+
// can land anywhere from a few seconds to ~5 minutes later — the
|
|
999
|
+
// exact moment depends on when the next sweep tick fires.
|
|
1000
|
+
//
|
|
1001
|
+
// We deliberately do NOT use exponential backoff or a local timer.
|
|
1002
|
+
// - Exponential backoff was platform-dependent (precise on Node
|
|
1003
|
+
// via setTimeout, but degraded to "next sweep" on Cloudflare
|
|
1004
|
+
// Workers where timers don't survive invocations). The behavior
|
|
1005
|
+
// is now identical everywhere.
|
|
1006
|
+
// - Sweep-only retry keeps the retry path observable from a single
|
|
1007
|
+
// place (the DB) and removes a class of races between the timer
|
|
1008
|
+
// and the sweep.
|
|
1009
|
+
const nextScheduledAt = this.dt.nowISOString();
|
|
766
1010
|
this.log.info(
|
|
767
|
-
`Job '${jobName}' failed, scheduling retry ${currentAttempt + 1}/${maxAttempts}`,
|
|
768
|
-
{ executionId, error: error.message
|
|
1011
|
+
`Job '${jobName}' failed, scheduling retry ${currentAttempt + 1}/${maxAttempts} (sweep will pick up)`,
|
|
1012
|
+
{ executionId, error: error.message },
|
|
769
1013
|
);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
new Date(nextScheduledAt).getTime() - this.dt.nowMillis(),
|
|
1014
|
+
// Guard with `status: running` so a concurrent cancel that has already
|
|
1015
|
+
// flipped the row to 'cancelled' is not overwritten by the retry write.
|
|
1016
|
+
await this.guardedUpdate(
|
|
1017
|
+
executionId,
|
|
1018
|
+
["running"],
|
|
1019
|
+
{
|
|
1020
|
+
status: "scheduled",
|
|
1021
|
+
error: error.message,
|
|
1022
|
+
scheduledAt: nextScheduledAt,
|
|
1023
|
+
logs: this.snapshotLogs(contextId),
|
|
1024
|
+
},
|
|
1025
|
+
"retry-after-failure",
|
|
783
1026
|
);
|
|
784
|
-
this.dt.createTimeout(() => {
|
|
785
|
-
void this.dispatchScheduled(jobName, executionId);
|
|
786
|
-
}, delayMs);
|
|
787
1027
|
} else {
|
|
788
1028
|
this.log.info(
|
|
789
1029
|
`Job '${jobName}' dead after ${currentAttempt} attempt(s)`,
|
|
790
1030
|
{ executionId, error: error.message },
|
|
791
1031
|
);
|
|
792
|
-
await this.
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1032
|
+
await this.guardedUpdate(
|
|
1033
|
+
executionId,
|
|
1034
|
+
["running"],
|
|
1035
|
+
{
|
|
1036
|
+
status: "error",
|
|
1037
|
+
error: error.message,
|
|
1038
|
+
completedAt: this.dt.nowISOString(),
|
|
1039
|
+
key: null,
|
|
1040
|
+
logs: this.snapshotLogs(contextId),
|
|
1041
|
+
},
|
|
1042
|
+
"terminal-failure",
|
|
1043
|
+
);
|
|
799
1044
|
}
|
|
800
1045
|
|
|
801
1046
|
await this.alepha.events.emit(
|
|
@@ -805,30 +1050,6 @@ export class JobProvider {
|
|
|
805
1050
|
);
|
|
806
1051
|
}
|
|
807
1052
|
|
|
808
|
-
protected computeBackoff(retry: JobRetryOptions, attempt: number): string {
|
|
809
|
-
const now = this.dt.now();
|
|
810
|
-
if (!retry.backoff) {
|
|
811
|
-
return now.add(1, "second").toISOString();
|
|
812
|
-
}
|
|
813
|
-
if (Array.isArray(retry.backoff)) {
|
|
814
|
-
return now.add(this.dt.duration(retry.backoff)).toISOString();
|
|
815
|
-
}
|
|
816
|
-
const backoff = retry.backoff as JobRetryBackoff;
|
|
817
|
-
const initial = this.dt.duration(backoff.initial).as("milliseconds");
|
|
818
|
-
const factor = backoff.factor ?? 2;
|
|
819
|
-
let delayMs = initial * factor ** (attempt - 1);
|
|
820
|
-
if (backoff.max) {
|
|
821
|
-
delayMs = Math.min(
|
|
822
|
-
delayMs,
|
|
823
|
-
this.dt.duration(backoff.max).as("milliseconds"),
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
if (backoff.jitter) {
|
|
827
|
-
delayMs = delayMs * (0.75 + Math.random() * 0.5);
|
|
828
|
-
}
|
|
829
|
-
return now.add(delayMs, "millisecond").toISOString();
|
|
830
|
-
}
|
|
831
|
-
|
|
832
1053
|
protected snapshotLogs(contextId: string): LogEntry[] | undefined {
|
|
833
1054
|
const entries = this.perExecutionLogs.get(contextId);
|
|
834
1055
|
if (!entries || entries.length === 0) return undefined;
|
|
@@ -866,7 +1087,7 @@ export class JobProvider {
|
|
|
866
1087
|
for (const exec of due) {
|
|
867
1088
|
if (!this.jobs.has(exec.jobName)) continue;
|
|
868
1089
|
await this.executions.updateById(exec.id, { status: "pending" });
|
|
869
|
-
await this.
|
|
1090
|
+
await this.dispatchSafe(exec.jobName, exec.id);
|
|
870
1091
|
}
|
|
871
1092
|
|
|
872
1093
|
// 2. Stale pending rows → re-dispatch
|
|
@@ -882,7 +1103,7 @@ export class JobProvider {
|
|
|
882
1103
|
});
|
|
883
1104
|
for (const exec of stale) {
|
|
884
1105
|
if (!this.jobs.has(exec.jobName)) continue;
|
|
885
|
-
await this.
|
|
1106
|
+
await this.dispatchSafe(exec.jobName, exec.id);
|
|
886
1107
|
}
|
|
887
1108
|
|
|
888
1109
|
// 3. Crashed running rows → mark as failed + apply retry
|
|
@@ -910,20 +1131,17 @@ export class JobProvider {
|
|
|
910
1131
|
await this.handleFailure(exec.id, reg, exec.attempt, err, "");
|
|
911
1132
|
}
|
|
912
1133
|
}
|
|
913
|
-
|
|
914
|
-
// 4. Trim ring buffer per job
|
|
915
|
-
await this.trimRingBuffers();
|
|
916
1134
|
} catch (e) {
|
|
917
1135
|
this.log.error("Sweep failed", { error: e });
|
|
918
1136
|
}
|
|
919
1137
|
}
|
|
920
1138
|
|
|
921
|
-
protected async
|
|
1139
|
+
protected async dispatchSafe(
|
|
922
1140
|
jobName: string,
|
|
923
1141
|
executionId: string,
|
|
924
1142
|
): Promise<void> {
|
|
925
1143
|
try {
|
|
926
|
-
await this.
|
|
1144
|
+
await this.dispatch(jobName, executionId);
|
|
927
1145
|
} catch (e) {
|
|
928
1146
|
this.log.warn(`Sweep failed to dispatch ${jobName} (${executionId})`, e);
|
|
929
1147
|
}
|
|
@@ -944,7 +1162,7 @@ export class JobProvider {
|
|
|
944
1162
|
{ id: { eq: executionId }, status: { eq: "scheduled" } },
|
|
945
1163
|
{ status: "pending" },
|
|
946
1164
|
);
|
|
947
|
-
await this.
|
|
1165
|
+
await this.dispatchSafe(jobName, executionId);
|
|
948
1166
|
} catch {
|
|
949
1167
|
// Row already transitioned (sweep ran, another worker claimed, etc.)
|
|
950
1168
|
}
|
|
@@ -971,7 +1189,7 @@ export class JobProvider {
|
|
|
971
1189
|
try {
|
|
972
1190
|
const rows = await this.executions.findMany({
|
|
973
1191
|
where: { jobName: { eq: jobName }, status: { eq: status } },
|
|
974
|
-
orderBy: { column: "
|
|
1192
|
+
orderBy: { column: "createdAt", direction: "desc" },
|
|
975
1193
|
limit: keep + 50,
|
|
976
1194
|
});
|
|
977
1195
|
if (rows.length <= keep) return;
|
|
@@ -992,19 +1210,25 @@ export class JobProvider {
|
|
|
992
1210
|
protected readonly onStart = $hook({
|
|
993
1211
|
on: "start",
|
|
994
1212
|
handler: async () => {
|
|
995
|
-
//
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1213
|
+
// Summarize effective modes once at start so operators can see at a
|
|
1214
|
+
// glance what each job will do. No validation is needed here: queue
|
|
1215
|
+
// jobs gracefully fall back to direct mode when AlephaApiJobsQueue
|
|
1216
|
+
// isn't loaded.
|
|
1217
|
+
const modes: Record<JobEffectiveMode, number> = {
|
|
1218
|
+
cron: 0,
|
|
1219
|
+
queue: 0,
|
|
1220
|
+
direct: 0,
|
|
1221
|
+
};
|
|
1222
|
+
const perJob: Record<string, JobEffectiveMode> = {};
|
|
1223
|
+
for (const [name] of this.jobs) {
|
|
1224
|
+
const m = this.effectiveMode(name);
|
|
1225
|
+
modes[m]++;
|
|
1226
|
+
perJob[name] = m;
|
|
1003
1227
|
}
|
|
1004
|
-
|
|
1005
1228
|
this.log.info(`Job system OK`, {
|
|
1006
|
-
|
|
1229
|
+
modes,
|
|
1007
1230
|
jobs: this.jobs.size,
|
|
1231
|
+
perJob,
|
|
1008
1232
|
});
|
|
1009
1233
|
|
|
1010
1234
|
// Capture logs per execution context.
|
|
@@ -1022,12 +1246,26 @@ export class JobProvider {
|
|
|
1022
1246
|
|
|
1023
1247
|
this.cronProvider.createCronJob(
|
|
1024
1248
|
"api:jobs:sweep",
|
|
1025
|
-
|
|
1249
|
+
this.config.sweepCron,
|
|
1026
1250
|
async () => {
|
|
1027
1251
|
await this.sweep();
|
|
1028
1252
|
},
|
|
1029
1253
|
true,
|
|
1030
1254
|
);
|
|
1255
|
+
|
|
1256
|
+
this.cronProvider.createCronJob(
|
|
1257
|
+
"api:jobs:trim",
|
|
1258
|
+
this.config.trimCron,
|
|
1259
|
+
async () => {
|
|
1260
|
+
if (this.stopping) return;
|
|
1261
|
+
try {
|
|
1262
|
+
await this.trimRingBuffers();
|
|
1263
|
+
} catch (e) {
|
|
1264
|
+
this.log.error("Trim failed", { error: e });
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
true,
|
|
1268
|
+
);
|
|
1031
1269
|
},
|
|
1032
1270
|
});
|
|
1033
1271
|
|