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/batch/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/batch/providers/BatchProvider.ts","../../src/batch/primitives/$batch.ts","../../src/batch/index.ts"],"sourcesContent":["import { $hook, $inject, type Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { type RetryBackoffOptions, RetryProvider } from \"alepha/retry\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchOptions<TItem, TResponse = any> {\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: TItem[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n *\n * @default 10\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n *\n * @default [1, \"second\"]\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: TItem) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n *\n * @default 1\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: {\n /**\n * The maximum number of attempts.\n *\n * @default 3\n */\n max?: number;\n\n /**\n * The backoff strategy for delays between retries.\n * Can be a fixed number (in ms) or a configuration object for exponential backoff.\n *\n * @default { initial: 200, factor: 2, jitter: true }\n */\n backoff?: number | RetryBackoffOptions;\n\n /**\n * An overall time limit for all retry attempts combined.\n *\n * e.g., `[5, 'seconds']`\n */\n maxDuration?: DurationLike;\n\n /**\n * A function that determines if a retry should be attempted based on the error.\n *\n * @default (error) => true (retries on any error)\n */\n when?: (error: Error) => boolean;\n\n /**\n * A custom callback for when a retry attempt fails.\n * This is called before the delay.\n */\n onError?: (error: Error, attempt: number) => void;\n };\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type BatchItemStatus = \"pending\" | \"processing\" | \"completed\" | \"failed\";\n\nexport interface BatchItemState<TItem, TResponse> {\n id: string;\n item: TItem;\n partitionKey: string;\n status: BatchItemStatus;\n result?: TResponse;\n error?: Error;\n promise?: Promise<TResponse>;\n resolve?: (value: TResponse) => void;\n reject?: (error: Error) => void;\n}\n\nexport interface PartitionState {\n itemIds: string[];\n timeout?: { clear: () => void };\n flushing: boolean;\n}\n\n/**\n * Context object that holds all state for a batch processor instance.\n */\nexport interface BatchContext<TItem, TResponse> {\n options: BatchOptions<TItem, TResponse>;\n itemStates: Map<string, BatchItemState<TItem, TResponse>>;\n partitions: Map<string, PartitionState>;\n activeHandlers: PromiseWithResolvers<void>[];\n isShuttingDown: boolean;\n isReady: boolean;\n alepha: Alepha;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Service for batch processing operations.\n * Provides methods to manage batches of items with automatic flushing based on size or time.\n */\nexport class BatchProvider {\n protected readonly log = $logger();\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly retryProvider = $inject(RetryProvider);\n\n /**\n * All active batch contexts managed by this provider.\n */\n protected readonly contexts = new Set<BatchContext<any, any>>();\n\n /**\n * Creates a new batch context with the given options.\n */\n createContext<TItem, TResponse>(\n alepha: Alepha,\n options: BatchOptions<TItem, TResponse>,\n ): BatchContext<TItem, TResponse> {\n const context: BatchContext<TItem, TResponse> = {\n options,\n itemStates: new Map(),\n partitions: new Map(),\n activeHandlers: [],\n isShuttingDown: false,\n isReady: false,\n alepha,\n };\n this.contexts.add(context);\n return context;\n }\n\n /**\n * Shutdown hook - flushes all batch contexts on application stop.\n */\n protected readonly onStop = $hook({\n on: \"stop\",\n priority: \"first\",\n handler: async () => {\n if (this.contexts.size === 0) {\n return;\n }\n this.log.debug(`Shutting down ${this.contexts.size} batch context(s)...`);\n const promises: Promise<void>[] = [];\n for (const context of this.contexts) {\n promises.push(this.shutdown(context));\n }\n await Promise.all(promises);\n this.log.debug(\"All batch contexts shut down\");\n },\n });\n\n /**\n * Get the effective maxSize for a context.\n */\n protected getMaxSize<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.maxSize ?? 10;\n }\n\n /**\n * Get the effective concurrency for a context.\n */\n protected getConcurrency<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.concurrency ?? 1;\n }\n\n /**\n * Get the effective maxDuration for a context.\n */\n protected getMaxDuration<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): DurationLike {\n return context.options.maxDuration ?? [1, \"second\"];\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n *\n * @throws Error if maxQueueSize is exceeded\n */\n push<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n item: TItem,\n ): string {\n // 1. Generate unique ID\n const id = crypto.randomUUID();\n\n // 2. Determine the partition key (with error handling)\n let partitionKey: string;\n try {\n partitionKey = context.options.partitionBy\n ? context.options.partitionBy(item)\n : \"default\";\n } catch (error) {\n this.log.warn(\n \"partitionBy function threw an error, using 'default' partition\",\n { error },\n );\n partitionKey = \"default\";\n }\n\n // 3. Create item state\n const itemState: BatchItemState<TItem, TResponse> = {\n id,\n item,\n partitionKey,\n status: \"pending\",\n };\n\n // CAUTION: Do not log.debug/info here as it may cause infinite loops if logging is batched\n\n context.itemStates.set(id, itemState);\n\n // 4. Get or create the partition state\n if (!context.partitions.has(partitionKey)) {\n context.partitions.set(partitionKey, {\n itemIds: [],\n flushing: false,\n });\n }\n const partition = context.partitions.get(partitionKey)!;\n\n // 5. Check maxQueueSize before adding\n if (\n context.options.maxQueueSize !== undefined &&\n partition.itemIds.length >= context.options.maxQueueSize\n ) {\n throw new AlephaError(\n `Batch queue size exceeded for partition '${partitionKey}' (max: ${context.options.maxQueueSize})`,\n );\n }\n\n // 6. Add item ID to partition\n partition.itemIds.push(id);\n\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n // 7. Only start processing if the app is ready (after \"ready\" hook)\n // During startup, items are just buffered in memory\n if (context.isReady) {\n // Check if the batch is full\n if (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Batch partition '${partitionKey}' is full, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on max size`,\n error,\n ),\n );\n } else if (!partition.timeout && !partition.flushing) {\n // 8. Start the timeout if it's not already running for this partition and not currently flushing\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n } else {\n // Not ready yet - just buffer items, no size checks or timeouts\n this.log.trace(\n `Buffering item in partition '${partitionKey}' (app not ready yet, ${partition.itemIds.length} items buffered)`,\n );\n }\n\n // 9. Return ID immediately\n return id;\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n async wait<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ): Promise<TResponse> {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n throw new AlephaError(`Item with id '${id}' not found`);\n }\n\n // If already completed or failed, return immediately\n if (itemState.status === \"completed\") {\n return itemState.result!;\n }\n if (itemState.status === \"failed\") {\n throw itemState.error!;\n }\n\n // Create promise on-demand if not already created\n if (!itemState.promise) {\n itemState.promise = new Promise<TResponse>((resolve, reject) => {\n itemState.resolve = resolve;\n itemState.reject = reject;\n });\n }\n\n return itemState.promise;\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n status<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n return undefined;\n }\n\n if (itemState.status === \"completed\") {\n return { status: \"completed\", result: itemState.result! };\n }\n if (itemState.status === \"failed\") {\n return { status: \"failed\", error: itemState.error! };\n }\n return { status: itemState.status };\n }\n\n /**\n * Clears completed and failed items from the context to free memory.\n * Returns the number of items cleared.\n *\n * @param context The batch context\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n clearCompleted<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n status?: \"completed\" | \"failed\",\n ): number {\n let count = 0;\n for (const [id, state] of context.itemStates) {\n if (status) {\n if (state.status === status) {\n context.itemStates.delete(id);\n count++;\n }\n } else if (state.status === \"completed\" || state.status === \"failed\") {\n context.itemStates.delete(id);\n count++;\n }\n }\n return count;\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n async flush<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey?: string,\n ): Promise<void> {\n const promises: Promise<void>[] = [];\n if (partitionKey) {\n if (context.partitions.has(partitionKey)) {\n promises.push(this.flushPartition(context, partitionKey));\n }\n } else {\n for (const key of context.partitions.keys()) {\n promises.push(this.flushPartition(context, key));\n }\n }\n await Promise.all(promises);\n }\n\n /**\n * Flush a specific partition.\n */\n protected async flushPartition<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey: string,\n limit?: number,\n ): Promise<void> {\n const partition = context.partitions.get(partitionKey);\n if (!partition || partition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n return;\n }\n\n // Clear the timeout and grab the item IDs (up to limit if specified)\n partition.timeout?.clear();\n partition.timeout = undefined;\n const itemsToTake =\n limit !== undefined\n ? Math.min(limit, partition.itemIds.length)\n : partition.itemIds.length;\n const itemIdsToProcess = partition.itemIds.splice(0, itemsToTake);\n\n // Mark partition as flushing to prevent race conditions\n partition.flushing = true;\n\n // Get the items and mark them as processing\n const itemsToProcess: TItem[] = [];\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"processing\";\n itemsToProcess.push(itemState.item);\n }\n }\n\n const concurrency = this.getConcurrency(context);\n const maxDuration = this.getMaxDuration(context);\n\n // Wait until there's a free slot (if at concurrency limit)\n while (context.activeHandlers.length >= concurrency) {\n this.log.trace(\n `Batch handler is at concurrency limit, waiting for a slot...`,\n );\n // Wait for any single handler to complete, not all of them\n await Promise.race(context.activeHandlers.map((it) => it.promise));\n }\n\n const promise = Promise.withResolvers<void>();\n context.activeHandlers.push(promise);\n let result: any;\n try {\n result = await context.alepha.context.run(() =>\n // during shutdown, call handler directly to avoid retry cancellation\n context.isShuttingDown\n ? context.options.handler(itemsToProcess)\n : this.retryProvider.retry(\n {\n ...context.options.retry,\n handler: context.options.handler,\n },\n itemsToProcess,\n ),\n );\n\n // Mark all items as completed and resolve their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"completed\";\n itemState.result = result;\n // Only resolve if someone is waiting\n itemState.resolve?.(result);\n }\n }\n } catch (error) {\n this.log.error(`Batch handler failed`, error);\n\n // Mark all items as failed and reject their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"failed\";\n itemState.error = error as Error;\n // Only reject if someone is waiting (promise was created)\n itemState.reject?.(error as Error);\n }\n }\n } finally {\n promise.resolve();\n context.activeHandlers = context.activeHandlers.filter(\n (it) => it !== promise,\n );\n\n // Only delete partition if no new items arrived during processing\n const currentPartition = context.partitions.get(partitionKey);\n if (currentPartition?.flushing && currentPartition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n } else if (currentPartition) {\n // Reset flushing flag if partition still exists with items\n currentPartition.flushing = false;\n\n // Restart timeout for items that arrived during flush\n if (currentPartition.itemIds.length > 0 && !currentPartition.timeout) {\n currentPartition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n }\n\n /**\n * Mark the context as ready and start processing buffered items.\n * Called after the \"ready\" hook.\n */\n async markReady<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\n \"Batch processor is now ready, starting to process buffered items...\",\n );\n context.isReady = true;\n await this.startProcessing(context);\n }\n\n /**\n * Mark the context as shutting down and flush all remaining items.\n */\n async shutdown<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\"Flushing all remaining batch partitions on shutdown...\");\n context.isShuttingDown = true;\n await this.flush(context);\n this.log.debug(\"All batch partitions flushed\");\n }\n\n /**\n * Called after the \"ready\" hook to start processing buffered items that were\n * pushed during startup. This checks all partitions and starts timeouts/flushes\n * for items that were accumulated before the app was ready.\n */\n protected async startProcessing<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n for (const [partitionKey, partition] of context.partitions.entries()) {\n if (partition.itemIds.length === 0) {\n continue;\n }\n\n this.log.trace(\n `Starting processing for partition '${partitionKey}' with ${partition.itemIds.length} buffered items`,\n );\n\n // Flush batches of maxSize while we have items >= maxSize\n while (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Partition '${partitionKey}' has ${partition.itemIds.length} items, flushing batch of ${maxSize}...`,\n );\n await this.flushPartition(context, partitionKey, maxSize);\n }\n\n // After flushing full batches, start timeout for any remaining items\n if (\n partition.itemIds.length > 0 &&\n !partition.timeout &&\n !partition.flushing\n ) {\n this.log.trace(\n `Starting timeout for partition '${partitionKey}' with ${partition.itemIds.length} remaining items`,\n );\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush partition '${partitionKey}' on timeout after startup`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n}\n","import {\n $hook,\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport type { RetryPrimitiveOptions } from \"alepha/retry\";\nimport {\n type BatchContext,\n type BatchItemState,\n type BatchItemStatus,\n BatchProvider,\n} from \"../providers/BatchProvider.ts\";\n\n/**\n * Creates a batch processing primitive for efficient grouping and processing of multiple operations.\n */\nexport const $batch = <TItem extends TSchema, TResponse>(\n options: BatchPrimitiveOptions<TItem, TResponse>,\n): BatchPrimitive<TItem, TResponse> =>\n createPrimitive(BatchPrimitive<TItem, TResponse>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchPrimitiveOptions<TItem extends TSchema, TResponse = any> {\n /**\n * TypeBox schema for validating each item added to the batch.\n */\n schema: TItem;\n\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: Static<TItem>[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: Static<TItem>) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: Omit<RetryPrimitiveOptions<() => Array<Static<TItem>>>, \"handler\">;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type { BatchItemState, BatchItemStatus };\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BatchPrimitive<\n TItem extends TSchema,\n TResponse = any,\n> extends Primitive<BatchPrimitiveOptions<TItem, TResponse>> {\n protected readonly batchProvider = $inject(BatchProvider);\n protected readonly context: BatchContext<Static<TItem>, TResponse>;\n\n constructor(\n ...args: ConstructorParameters<\n typeof Primitive<BatchPrimitiveOptions<TItem, TResponse>>\n >\n ) {\n super(...args);\n this.context = this.batchProvider.createContext(this.alepha, {\n handler: this.options.handler,\n maxSize: this.options.maxSize,\n maxQueueSize: this.options.maxQueueSize,\n maxDuration: this.options.maxDuration,\n partitionBy: this.options.partitionBy,\n concurrency: this.options.concurrency,\n retry: this.options.retry,\n });\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n */\n public async push(item: Static<TItem>): Promise<string> {\n // Validate the item against the schema\n const validatedItem = this.alepha.codec.validate(this.options.schema, item);\n return this.batchProvider.push(this.context, validatedItem);\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n public async wait(id: string): Promise<TResponse> {\n return this.batchProvider.wait(this.context, id);\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n public status(\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n return this.batchProvider.status(this.context, id);\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n public async flush(partitionKey?: string): Promise<void> {\n return this.batchProvider.flush(this.context, partitionKey);\n }\n\n /**\n * Clears completed and failed items from memory.\n * Call this periodically in long-running applications to prevent memory leaks.\n *\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n public clearCompleted(status?: \"completed\" | \"failed\"): number {\n return this.batchProvider.clearCompleted(this.context, status);\n }\n\n protected readonly onReady = $hook({\n on: \"ready\",\n handler: async () => {\n await this.batchProvider.markReady(this.context);\n },\n });\n}\n\n$batch[KIND] = BatchPrimitive;\n","import { $module } from \"alepha\";\nimport { $batch } from \"./primitives/$batch.ts\";\nimport { BatchProvider } from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$batch.ts\";\nexport * from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Batch accumulation and processing.\n *\n * **Features:**\n * - Batch accumulator with handler\n * - Configurable batch size\n * - Time-based triggers\n * - Status tracking\n *\n * @module alepha.batch\n */\nexport const AlephaBatch = $module({\n name: \"alepha.batch\",\n primitives: [$batch],\n services: [BatchProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/batch/providers/BatchProvider.ts","../../src/batch/primitives/$batch.ts","../../src/batch/index.ts"],"sourcesContent":["import { $hook, $inject, type Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { type RetryBackoffOptions, RetryProvider } from \"alepha/retry\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchOptions<TItem, TResponse = any> {\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: TItem[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n *\n * @default 10\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n *\n * @default [1, \"second\"]\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: TItem) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n *\n * @default 1\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: {\n /**\n * The maximum number of attempts.\n *\n * @default 3\n */\n max?: number;\n\n /**\n * The backoff strategy for delays between retries.\n * Can be a fixed number (in ms) or a configuration object for exponential backoff.\n *\n * @default { initial: 200, factor: 2, jitter: true }\n */\n backoff?: number | RetryBackoffOptions;\n\n /**\n * An overall time limit for all retry attempts combined.\n *\n * e.g., `[5, 'seconds']`\n */\n maxDuration?: DurationLike;\n\n /**\n * A function that determines if a retry should be attempted based on the error.\n *\n * @default (error) => true (retries on any error)\n */\n when?: (error: Error) => boolean;\n\n /**\n * A custom callback for when a retry attempt fails.\n * This is called before the delay.\n */\n onError?: (error: Error, attempt: number) => void;\n };\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type BatchItemStatus = \"pending\" | \"processing\" | \"completed\" | \"failed\";\n\nexport interface BatchItemState<TItem, TResponse> {\n id: string;\n item: TItem;\n partitionKey: string;\n status: BatchItemStatus;\n result?: TResponse;\n error?: Error;\n promise?: Promise<TResponse>;\n resolve?: (value: TResponse) => void;\n reject?: (error: Error) => void;\n}\n\nexport interface PartitionState {\n itemIds: string[];\n timeout?: { clear: () => void };\n flushing: boolean;\n}\n\n/**\n * Context object that holds all state for a batch processor instance.\n */\nexport interface BatchContext<TItem, TResponse> {\n options: BatchOptions<TItem, TResponse>;\n itemStates: Map<string, BatchItemState<TItem, TResponse>>;\n partitions: Map<string, PartitionState>;\n activeHandlers: PromiseWithResolvers<void>[];\n isShuttingDown: boolean;\n isReady: boolean;\n alepha: Alepha;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Service for batch processing operations.\n * Provides methods to manage batches of items with automatic flushing based on size or time.\n */\nexport class BatchProvider {\n protected readonly log = $logger();\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly retryProvider = $inject(RetryProvider);\n\n /**\n * All active batch contexts managed by this provider.\n */\n protected readonly contexts = new Set<BatchContext<any, any>>();\n\n /**\n * Creates a new batch context with the given options.\n */\n createContext<TItem, TResponse>(\n alepha: Alepha,\n options: BatchOptions<TItem, TResponse>,\n ): BatchContext<TItem, TResponse> {\n const context: BatchContext<TItem, TResponse> = {\n options,\n itemStates: new Map(),\n partitions: new Map(),\n activeHandlers: [],\n isShuttingDown: false,\n isReady: false,\n alepha,\n };\n this.contexts.add(context);\n return context;\n }\n\n /**\n * Shutdown hook - flushes all batch contexts on application stop.\n */\n protected readonly onStop = $hook({\n on: \"stop\",\n priority: \"first\",\n handler: async () => {\n if (this.contexts.size === 0) {\n return;\n }\n this.log.debug(`Shutting down ${this.contexts.size} batch context(s)...`);\n const promises: Promise<void>[] = [];\n for (const context of this.contexts) {\n promises.push(this.shutdown(context));\n }\n await Promise.all(promises);\n this.log.debug(\"All batch contexts shut down\");\n },\n });\n\n /**\n * Get the effective maxSize for a context.\n */\n protected getMaxSize<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.maxSize ?? 10;\n }\n\n /**\n * Get the effective concurrency for a context.\n */\n protected getConcurrency<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.concurrency ?? 1;\n }\n\n /**\n * Get the effective maxDuration for a context.\n */\n protected getMaxDuration<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): DurationLike {\n return context.options.maxDuration ?? [1, \"second\"];\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n *\n * @throws Error if maxQueueSize is exceeded\n */\n push<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n item: TItem,\n ): string {\n // 1. Generate unique ID\n const id = crypto.randomUUID();\n\n // 2. Determine the partition key (with error handling)\n let partitionKey: string;\n try {\n partitionKey = context.options.partitionBy\n ? context.options.partitionBy(item)\n : \"default\";\n } catch (error) {\n this.log.warn(\n \"partitionBy function threw an error, using 'default' partition\",\n { error },\n );\n partitionKey = \"default\";\n }\n\n // 3. Create item state\n const itemState: BatchItemState<TItem, TResponse> = {\n id,\n item,\n partitionKey,\n status: \"pending\",\n };\n\n // CAUTION: Do not log.debug/info here as it may cause infinite loops if logging is batched\n\n context.itemStates.set(id, itemState);\n\n // 4. Get or create the partition state\n if (!context.partitions.has(partitionKey)) {\n context.partitions.set(partitionKey, {\n itemIds: [],\n flushing: false,\n });\n }\n const partition = context.partitions.get(partitionKey)!;\n\n // 5. Check maxQueueSize before adding\n if (\n context.options.maxQueueSize !== undefined &&\n partition.itemIds.length >= context.options.maxQueueSize\n ) {\n throw new AlephaError(\n `Batch queue size exceeded for partition '${partitionKey}' (max: ${context.options.maxQueueSize})`,\n );\n }\n\n // 6. Add item ID to partition\n partition.itemIds.push(id);\n\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n // 7. Only start processing if the app is ready (after \"ready\" hook)\n // During startup, items are just buffered in memory\n if (context.isReady) {\n // Check if the batch is full\n if (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Batch partition '${partitionKey}' is full, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on max size`,\n error,\n ),\n );\n } else if (!partition.timeout && !partition.flushing) {\n // 8. Start the timeout if it's not already running for this partition and not currently flushing\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n } else {\n // Not ready yet - just buffer items, no size checks or timeouts\n this.log.trace(\n `Buffering item in partition '${partitionKey}' (app not ready yet, ${partition.itemIds.length} items buffered)`,\n );\n }\n\n // 9. Return ID immediately\n return id;\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n async wait<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ): Promise<TResponse> {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n throw new AlephaError(`Item with id '${id}' not found`);\n }\n\n // If already completed or failed, return immediately\n if (itemState.status === \"completed\") {\n return itemState.result!;\n }\n if (itemState.status === \"failed\") {\n throw itemState.error!;\n }\n\n // Create promise on-demand if not already created\n if (!itemState.promise) {\n itemState.promise = new Promise<TResponse>((resolve, reject) => {\n itemState.resolve = resolve;\n itemState.reject = reject;\n });\n }\n\n return itemState.promise;\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n status<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n return undefined;\n }\n\n if (itemState.status === \"completed\") {\n return { status: \"completed\", result: itemState.result! };\n }\n if (itemState.status === \"failed\") {\n return { status: \"failed\", error: itemState.error! };\n }\n return { status: itemState.status };\n }\n\n /**\n * Clears completed and failed items from the context to free memory.\n * Returns the number of items cleared.\n *\n * @param context The batch context\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n clearCompleted<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n status?: \"completed\" | \"failed\",\n ): number {\n let count = 0;\n for (const [id, state] of context.itemStates) {\n if (status) {\n if (state.status === status) {\n context.itemStates.delete(id);\n count++;\n }\n } else if (state.status === \"completed\" || state.status === \"failed\") {\n context.itemStates.delete(id);\n count++;\n }\n }\n return count;\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n async flush<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey?: string,\n ): Promise<void> {\n const promises: Promise<void>[] = [];\n if (partitionKey) {\n if (context.partitions.has(partitionKey)) {\n promises.push(this.flushPartition(context, partitionKey));\n }\n } else {\n for (const key of context.partitions.keys()) {\n promises.push(this.flushPartition(context, key));\n }\n }\n await Promise.all(promises);\n }\n\n /**\n * Flush a specific partition.\n */\n protected async flushPartition<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey: string,\n limit?: number,\n ): Promise<void> {\n const partition = context.partitions.get(partitionKey);\n if (!partition || partition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n return;\n }\n\n // Clear the timeout and grab the item IDs (up to limit if specified)\n partition.timeout?.clear();\n partition.timeout = undefined;\n const itemsToTake =\n limit !== undefined\n ? Math.min(limit, partition.itemIds.length)\n : partition.itemIds.length;\n const itemIdsToProcess = partition.itemIds.splice(0, itemsToTake);\n\n // Mark partition as flushing to prevent race conditions\n partition.flushing = true;\n\n // Get the items and mark them as processing\n const itemsToProcess: TItem[] = [];\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"processing\";\n itemsToProcess.push(itemState.item);\n }\n }\n\n const concurrency = this.getConcurrency(context);\n const maxDuration = this.getMaxDuration(context);\n\n // Wait until there's a free slot (if at concurrency limit)\n while (context.activeHandlers.length >= concurrency) {\n this.log.trace(\n `Batch handler is at concurrency limit, waiting for a slot...`,\n );\n // Wait for any single handler to complete, not all of them\n await Promise.race(context.activeHandlers.map((it) => it.promise));\n }\n\n const promise = Promise.withResolvers<void>();\n context.activeHandlers.push(promise);\n let result: any;\n try {\n result = await context.alepha.context.run(() =>\n // during shutdown, call handler directly to avoid retry cancellation\n context.isShuttingDown\n ? context.options.handler(itemsToProcess)\n : this.retryProvider.retry(\n {\n ...context.options.retry,\n handler: context.options.handler,\n },\n itemsToProcess,\n ),\n );\n\n // Mark all items as completed and resolve their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"completed\";\n itemState.result = result;\n // Only resolve if someone is waiting\n itemState.resolve?.(result);\n }\n }\n } catch (error) {\n this.log.error(`Batch handler failed`, error);\n\n // Mark all items as failed and reject their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"failed\";\n itemState.error = error as Error;\n // Only reject if someone is waiting (promise was created)\n itemState.reject?.(error as Error);\n }\n }\n } finally {\n promise.resolve();\n context.activeHandlers = context.activeHandlers.filter(\n (it) => it !== promise,\n );\n\n // Only delete partition if no new items arrived during processing\n const currentPartition = context.partitions.get(partitionKey);\n if (currentPartition?.flushing && currentPartition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n } else if (currentPartition) {\n // Reset flushing flag if partition still exists with items\n currentPartition.flushing = false;\n\n // Restart timeout for items that arrived during flush\n if (currentPartition.itemIds.length > 0 && !currentPartition.timeout) {\n currentPartition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n }\n\n /**\n * Mark the context as ready and start processing buffered items.\n * Called after the \"ready\" hook.\n */\n async markReady<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\n \"Batch processor is now ready, starting to process buffered items...\",\n );\n context.isReady = true;\n await this.startProcessing(context);\n }\n\n /**\n * Mark the context as shutting down and flush all remaining items.\n */\n async shutdown<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\"Flushing all remaining batch partitions on shutdown...\");\n context.isShuttingDown = true;\n await this.flush(context);\n this.log.debug(\"All batch partitions flushed\");\n }\n\n /**\n * Called after the \"ready\" hook to start processing buffered items that were\n * pushed during startup. This checks all partitions and starts timeouts/flushes\n * for items that were accumulated before the app was ready.\n */\n protected async startProcessing<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n for (const [partitionKey, partition] of context.partitions.entries()) {\n if (partition.itemIds.length === 0) {\n continue;\n }\n\n this.log.trace(\n `Starting processing for partition '${partitionKey}' with ${partition.itemIds.length} buffered items`,\n );\n\n // Flush batches of maxSize while we have items >= maxSize\n while (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Partition '${partitionKey}' has ${partition.itemIds.length} items, flushing batch of ${maxSize}...`,\n );\n await this.flushPartition(context, partitionKey, maxSize);\n }\n\n // After flushing full batches, start timeout for any remaining items\n if (\n partition.itemIds.length > 0 &&\n !partition.timeout &&\n !partition.flushing\n ) {\n this.log.trace(\n `Starting timeout for partition '${partitionKey}' with ${partition.itemIds.length} remaining items`,\n );\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush partition '${partitionKey}' on timeout after startup`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n}\n","import {\n $hook,\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport type { RetryPrimitiveOptions } from \"alepha/retry\";\nimport {\n type BatchContext,\n type BatchItemState,\n type BatchItemStatus,\n BatchProvider,\n} from \"../providers/BatchProvider.ts\";\n\n/**\n * Creates a batch processing primitive for efficient grouping and processing of multiple operations.\n */\nexport const $batch = <TItem extends TSchema, TResponse>(\n options: BatchPrimitiveOptions<TItem, TResponse>,\n): BatchPrimitive<TItem, TResponse> =>\n createPrimitive(BatchPrimitive<TItem, TResponse>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchPrimitiveOptions<TItem extends TSchema, TResponse = any> {\n /**\n * TypeBox schema for validating each item added to the batch.\n */\n schema: TItem;\n\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: Static<TItem>[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: Static<TItem>) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: Omit<RetryPrimitiveOptions<() => Array<Static<TItem>>>, \"handler\">;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type { BatchItemState, BatchItemStatus };\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BatchPrimitive<\n TItem extends TSchema,\n TResponse = any,\n> extends Primitive<BatchPrimitiveOptions<TItem, TResponse>> {\n protected readonly batchProvider = $inject(BatchProvider);\n protected readonly context: BatchContext<Static<TItem>, TResponse>;\n\n constructor(\n ...args: ConstructorParameters<\n typeof Primitive<BatchPrimitiveOptions<TItem, TResponse>>\n >\n ) {\n super(...args);\n this.context = this.batchProvider.createContext(this.alepha, {\n handler: this.options.handler,\n maxSize: this.options.maxSize,\n maxQueueSize: this.options.maxQueueSize,\n maxDuration: this.options.maxDuration,\n partitionBy: this.options.partitionBy,\n concurrency: this.options.concurrency,\n retry: this.options.retry,\n });\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n */\n public async push(item: Static<TItem>): Promise<string> {\n // Validate the item against the schema\n const validatedItem = this.alepha.codec.validate(this.options.schema, item);\n return this.batchProvider.push(this.context, validatedItem);\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n public async wait(id: string): Promise<TResponse> {\n return this.batchProvider.wait(this.context, id);\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n public status(\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n return this.batchProvider.status(this.context, id);\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n public async flush(partitionKey?: string): Promise<void> {\n return this.batchProvider.flush(this.context, partitionKey);\n }\n\n /**\n * Clears completed and failed items from memory.\n * Call this periodically in long-running applications to prevent memory leaks.\n *\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n public clearCompleted(status?: \"completed\" | \"failed\"): number {\n return this.batchProvider.clearCompleted(this.context, status);\n }\n\n protected readonly onReady = $hook({\n on: \"ready\",\n handler: async () => {\n await this.batchProvider.markReady(this.context);\n },\n });\n}\n\n$batch[KIND] = BatchPrimitive;\n","import { $module } from \"alepha\";\nimport { $batch } from \"./primitives/$batch.ts\";\nimport { BatchProvider } from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$batch.ts\";\nexport * from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Batch accumulation and processing.\n *\n * **Features:**\n * - Batch accumulator with handler\n * - Configurable batch size\n * - Time-based triggers\n * - Status tracking\n *\n * @module alepha.batch\n */\nexport const AlephaBatch = $module({\n name: \"alepha.batch\",\n primitives: [$batch],\n services: [BatchProvider],\n});\n"],"mappings":"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"}
|
package/dist/bucket/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as _$alepha_logger0 from "alepha/logger";
|
|
6
6
|
import { S3mini } from "s3mini";
|
|
7
7
|
import { R2Bucket } from "@cloudflare/workers-types";
|
|
8
|
+
import * as _$typebox from "typebox";
|
|
8
9
|
|
|
9
10
|
//#region ../../src/bucket/providers/FileStorageProvider.d.ts
|
|
10
11
|
declare abstract class FileStorageProvider {
|
|
@@ -40,6 +41,14 @@ declare abstract class FileStorageProvider {
|
|
|
40
41
|
* @param fileId - Identifier of the file to delete
|
|
41
42
|
*/
|
|
42
43
|
abstract delete(bucketName: string, fileId: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Delete multiple files in one round-trip where the provider supports
|
|
46
|
+
* batch (R2/S3, up to 1000 per call). Memory/Local fall back to a loop.
|
|
47
|
+
*
|
|
48
|
+
* @param bucketName - Container name
|
|
49
|
+
* @param fileIds - Identifiers of the files to delete
|
|
50
|
+
*/
|
|
51
|
+
abstract deleteMany(bucketName: string, fileIds: string[]): Promise<void>;
|
|
43
52
|
}
|
|
44
53
|
//#endregion
|
|
45
54
|
//#region ../../src/bucket/providers/MemoryFileStorageProvider.d.ts
|
|
@@ -57,6 +66,7 @@ declare class MemoryFileStorageProvider implements FileStorageProvider {
|
|
|
57
66
|
download(bucketName: string, fileId: string): Promise<FileLike>;
|
|
58
67
|
exists(bucketName: string, fileId: string): Promise<boolean>;
|
|
59
68
|
delete(bucketName: string, fileId: string): Promise<void>;
|
|
69
|
+
deleteMany(bucketName: string, fileIds: string[]): Promise<void>;
|
|
60
70
|
protected createId(): string;
|
|
61
71
|
}
|
|
62
72
|
//#endregion
|
|
@@ -258,6 +268,12 @@ declare class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {
|
|
|
258
268
|
* Delete permanently a file from the bucket.
|
|
259
269
|
*/
|
|
260
270
|
delete(fileId: string, skipHook?: boolean): Promise<void>;
|
|
271
|
+
/**
|
|
272
|
+
* Delete many files in one round-trip when the underlying provider supports
|
|
273
|
+
* batch (R2/S3 up to 1000 keys per call). Emits one `bucket:file:deleted`
|
|
274
|
+
* event per id unless `skipHook` is set.
|
|
275
|
+
*/
|
|
276
|
+
deleteMany(fileIds: string[], skipHook?: boolean): Promise<void>;
|
|
261
277
|
/**
|
|
262
278
|
* Checks if a file exists in the bucket.
|
|
263
279
|
*/
|
|
@@ -357,6 +373,7 @@ declare class CloudflareR2Provider implements FileStorageProvider {
|
|
|
357
373
|
download(bucketName: string, fileId: string): Promise<FileLike>;
|
|
358
374
|
exists(bucketName: string, fileId: string): Promise<boolean>;
|
|
359
375
|
delete(bucketName: string, fileId: string): Promise<void>;
|
|
376
|
+
deleteMany(bucketName: string, fileIds: string[]): Promise<void>;
|
|
360
377
|
/**
|
|
361
378
|
* Build the full R2 key: {prefix}/{bucketName}/{fileId}
|
|
362
379
|
*/
|
|
@@ -369,8 +386,8 @@ declare class CloudflareR2Provider implements FileStorageProvider {
|
|
|
369
386
|
/**
|
|
370
387
|
* Local file storage configuration atom
|
|
371
388
|
*/
|
|
372
|
-
declare const localFileStorageOptions: _$alepha.Atom<_$
|
|
373
|
-
storagePath: _$
|
|
389
|
+
declare const localFileStorageOptions: _$alepha.Atom<_$typebox.TObject<{
|
|
390
|
+
storagePath: _$typebox.TString;
|
|
374
391
|
}>, "alepha.bucket.local.options">;
|
|
375
392
|
type LocalFileStorageProviderOptions = Static<typeof localFileStorageOptions.schema>;
|
|
376
393
|
declare module "alepha" {
|
|
@@ -393,6 +410,7 @@ declare class LocalFileStorageProvider implements FileStorageProvider {
|
|
|
393
410
|
download(bucketName: string, fileId: string): Promise<FileLike>;
|
|
394
411
|
exists(bucketName: string, fileId: string): Promise<boolean>;
|
|
395
412
|
delete(bucketName: string, fileId: string): Promise<void>;
|
|
413
|
+
deleteMany(bucketName: string, fileIds: string[]): Promise<void>;
|
|
396
414
|
protected stat(bucket: string, fileId: string): Promise<fs.Stats>;
|
|
397
415
|
protected createId(mimeType: string): string;
|
|
398
416
|
protected path(bucket: string, fileId?: string): string;
|
|
@@ -461,6 +479,7 @@ declare class NodeS3BucketProvider implements FileStorageProvider {
|
|
|
461
479
|
download(bucketName: string, fileId: string): Promise<FileLike>;
|
|
462
480
|
exists(bucketName: string, fileId: string): Promise<boolean>;
|
|
463
481
|
delete(bucketName: string, fileId: string): Promise<void>;
|
|
482
|
+
deleteMany(bucketName: string, fileIds: string[]): Promise<void>;
|
|
464
483
|
}
|
|
465
484
|
//#endregion
|
|
466
485
|
//#region ../../src/bucket/index.d.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/providers/LocalFileStorageProvider.ts","../../src/bucket/providers/NodeS3BucketProvider.ts","../../src/bucket/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/providers/LocalFileStorageProvider.ts","../../src/bucket/providers/NodeS3BucketProvider.ts","../../src/bucket/index.ts"],"mappings":"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"}
|
package/dist/bucket/index.js
CHANGED
|
@@ -58,6 +58,9 @@ var MemoryFileStorageProvider = class {
|
|
|
58
58
|
if (!(fileKey in this.files)) throw new FileNotFoundError(`File with ID ${fileId} not found.`);
|
|
59
59
|
delete this.files[fileKey];
|
|
60
60
|
}
|
|
61
|
+
async deleteMany(bucketName, fileIds) {
|
|
62
|
+
for (const id of fileIds) delete this.files[`${bucketName}/${id}`];
|
|
63
|
+
}
|
|
61
64
|
createId() {
|
|
62
65
|
return randomUUID();
|
|
63
66
|
}
|
|
@@ -157,6 +160,20 @@ var BucketPrimitive = class extends Primitive {
|
|
|
157
160
|
});
|
|
158
161
|
}
|
|
159
162
|
/**
|
|
163
|
+
* Delete many files in one round-trip when the underlying provider supports
|
|
164
|
+
* batch (R2/S3 up to 1000 keys per call). Emits one `bucket:file:deleted`
|
|
165
|
+
* event per id unless `skipHook` is set.
|
|
166
|
+
*/
|
|
167
|
+
async deleteMany(fileIds, skipHook = false) {
|
|
168
|
+
if (fileIds.length === 0) return;
|
|
169
|
+
await this.provider.deleteMany(this.name, fileIds);
|
|
170
|
+
if (skipHook) return;
|
|
171
|
+
for (const id of fileIds) await this.alepha.events.emit("bucket:file:deleted", {
|
|
172
|
+
id,
|
|
173
|
+
bucket: this
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
160
177
|
* Checks if a file exists in the bucket.
|
|
161
178
|
*/
|
|
162
179
|
async exists(fileId) {
|
|
@@ -258,6 +275,12 @@ var LocalFileStorageProvider = class {
|
|
|
258
275
|
throw new AlephaError("Error deleting file", { cause: error });
|
|
259
276
|
}
|
|
260
277
|
}
|
|
278
|
+
async deleteMany(bucketName, fileIds) {
|
|
279
|
+
await Promise.all(fileIds.map((id) => unlink(this.path(bucketName, id)).catch((error) => {
|
|
280
|
+
if (this.isErrorNoEntry(error)) return;
|
|
281
|
+
throw new AlephaError("Error deleting file", { cause: error });
|
|
282
|
+
})));
|
|
283
|
+
}
|
|
261
284
|
stat(bucket, fileId) {
|
|
262
285
|
return stat(this.path(bucket, fileId));
|
|
263
286
|
}
|
|
@@ -402,6 +425,23 @@ var NodeS3BucketProvider = class {
|
|
|
402
425
|
throw error;
|
|
403
426
|
}
|
|
404
427
|
}
|
|
428
|
+
async deleteMany(bucketName, fileIds) {
|
|
429
|
+
if (fileIds.length === 0) return;
|
|
430
|
+
this.log.trace(`Deleting ${fileIds.length} files from bucket '${bucketName}'...`);
|
|
431
|
+
const client = this.getClient(bucketName);
|
|
432
|
+
for (let i = 0; i < fileIds.length; i += 1e3) {
|
|
433
|
+
const chunk = fileIds.slice(i, i + 1e3);
|
|
434
|
+
try {
|
|
435
|
+
const batch = client.deleteObjects;
|
|
436
|
+
if (typeof batch === "function") await batch.call(client, chunk);
|
|
437
|
+
else await Promise.all(chunk.map((id) => client.deleteObject(id)));
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this.log.error(`Failed to delete files: ${error}`);
|
|
440
|
+
if (error instanceof Error) throw new FileNotFoundError("Error deleting files", { cause: error });
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
405
445
|
};
|
|
406
446
|
//#endregion
|
|
407
447
|
//#region ../../src/bucket/providers/CloudflareR2Provider.ts
|
|
@@ -532,6 +572,13 @@ R2_BUCKET_NAME: t.string({ description: "R2 bucket name in Cloudflare" }) }));
|
|
|
532
572
|
if (!await this.exists(bucketName, fileId)) throw new FileNotFoundError(`File '${fileId}' not found in bucket '${bucketName}'.`);
|
|
533
573
|
await r2.delete(key);
|
|
534
574
|
}
|
|
575
|
+
async deleteMany(bucketName, fileIds) {
|
|
576
|
+
if (fileIds.length === 0) return;
|
|
577
|
+
const r2 = this.getR2();
|
|
578
|
+
const keys = fileIds.map((id) => this.key(bucketName, id));
|
|
579
|
+
this.log.trace(`Deleting ${keys.length} keys from '${bucketName}'`);
|
|
580
|
+
for (let i = 0; i < keys.length; i += 1e3) await r2.delete(keys.slice(i, i + 1e3));
|
|
581
|
+
}
|
|
535
582
|
/**
|
|
536
583
|
* Build the full R2 key: {prefix}/{bucketName}/{fileId}
|
|
537
584
|
*/
|
package/dist/bucket/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["Buffer"],"sources":["../../src/bucket/errors/InvalidFileError.ts","../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/providers/LocalFileStorageProvider.ts","../../src/bucket/providers/NodeS3BucketProvider.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/index.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport class InvalidFileError extends AlephaError {\n public readonly status = 400;\n}\n","import type { FileLike } from \"alepha\";\n\nexport abstract class FileStorageProvider {\n /**\n * Uploads a file to the storage.\n *\n * @param bucketName - Container name\n * @param file - File to upload\n * @param fileId - Optional file identifier. If not provided, a unique ID will be generated.\n * @return The identifier of the uploaded file.\n */\n abstract upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string>;\n\n /**\n * Downloads a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to download\n * @return The downloaded file as a FileLike object.\n */\n abstract download(bucketName: string, fileId: string): Promise<FileLike>;\n\n /**\n * Check if fileId exists in the storage bucket.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to stream\n * @return True is the file exists, false otherwise.\n */\n abstract exists(bucketName: string, fileId: string): Promise<boolean>;\n\n /**\n * Delete permanently a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to delete\n */\n abstract delete(bucketName: string, fileId: string): Promise<void>;\n}\n","import { AlephaError } from \"alepha\";\n\nexport class FileNotFoundError extends AlephaError {\n public readonly status = 404;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { $inject, type FileLike } from \"alepha\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\ninterface StoredFile {\n buffer: Buffer;\n name: string;\n type: string;\n size: number;\n}\n\nexport class MemoryFileStorageProvider implements FileStorageProvider {\n public readonly files: Record<string, StoredFile> = {};\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n // Consume the stream and store as buffer so downloads are repeatable\n const chunks: Uint8Array[] = [];\n for await (const chunk of file.stream() as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n this.files[`${bucketName}/${fileId}`] = {\n buffer,\n name: file.name,\n type: file.type,\n size: file.size,\n };\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const fileKey = `${bucketName}/${fileId}`;\n const stored = this.files[fileKey];\n\n if (!stored) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n // Create a fresh FileLike with a new stream from the stored buffer\n return this.fileSystem.createFile({\n stream: new Blob([new Uint8Array(stored.buffer)]).stream(),\n name: stored.name,\n type: stored.type,\n size: stored.size,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n return `${bucketName}/${fileId}` in this.files;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const fileKey = `${bucketName}/${fileId}`;\n if (!(fileKey in this.files)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n delete this.files[fileKey];\n }\n\n protected createId(): string {\n return randomUUID();\n }\n}\n","import {\n $inject,\n createPrimitive,\n type FileLike,\n KIND,\n Primitive,\n type Service,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { InvalidFileError } from \"../errors/InvalidFileError.ts\";\nimport { FileStorageProvider } from \"../providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"../providers/MemoryFileStorageProvider.ts\";\n\n/**\n * Creates a bucket primitive for file storage and management with configurable validation.\n *\n * Provides a comprehensive file storage system that handles uploads, downloads, validation,\n * and management across multiple storage backends with MIME type and size limit controls.\n *\n * **Key Features**\n * - Multi-provider support (filesystem, cloud storage, in-memory)\n * - Automatic MIME type and file size validation\n * - Event integration for file operations monitoring\n * - Flexible per-bucket and per-operation configuration\n * - Smart file type and size detection\n *\n * **Common Use Cases**\n * - User profile pictures and document uploads\n * - Product images and media management\n * - Document storage and retrieval systems\n *\n * @example\n * ```ts\n * class MediaService {\n * images = $bucket({\n * name: \"user-images\",\n * mimeTypes: [\"image/jpeg\", \"image/png\", \"image/gif\"],\n * maxSize: 5 // 5MB limit\n * });\n *\n * documents = $bucket({\n * name: \"documents\",\n * mimeTypes: [\"application/pdf\", \"text/plain\"],\n * maxSize: 50 // 50MB limit\n * });\n *\n * async uploadProfileImage(file: FileLike, userId: string): Promise<string> {\n * const fileId = await this.images.upload(file);\n * await this.userService.updateProfileImage(userId, fileId);\n * return fileId;\n * }\n *\n * async downloadDocument(documentId: string): Promise<FileLike> {\n * return await this.documents.download(documentId);\n * }\n *\n * async deleteDocument(documentId: string): Promise<void> {\n * await this.documents.delete(documentId);\n * }\n * }\n * ```\n */\nexport const $bucket = (options: BucketPrimitiveOptions) =>\n createPrimitive(BucketPrimitive, options);\n\nexport interface BucketPrimitiveOptions extends BucketFileOptions {\n /**\n * File storage provider configuration for the bucket.\n *\n * Options:\n * - **\"memory\"**: In-memory storage (default for development, lost on restart)\n * - **Service<FileStorageProvider>**: Custom provider class (e.g., S3FileStorageProvider, AzureBlobProvider)\n * - **undefined**: Uses the default file storage provider from dependency injection\n *\n * **Provider Selection Guidelines**:\n * - **Development**: Use \"memory\" for fast, simple testing without external dependencies\n * - **Production**: Use cloud providers (S3, Azure Blob, Google Cloud Storage) for scalability\n * - **Local deployment**: Use filesystem providers for on-premise installations\n * - **Hybrid**: Use different providers for different bucket types (temp files vs permanent storage)\n *\n * **Provider Capabilities**:\n * - File persistence and durability guarantees\n * - Scalability and performance characteristics\n * - Geographic distribution and CDN integration\n * - Cost implications for storage and bandwidth\n * - Backup and disaster recovery features\n *\n * @default Uses injected FileStorageProvider\n * @example \"memory\"\n * @example S3FileStorageProvider\n * @example AzureBlobStorageProvider\n */\n provider?: Service<FileStorageProvider> | \"memory\";\n\n /**\n * Unique name identifier for the bucket.\n *\n * This name is used for:\n * - Storage backend organization and partitioning\n * - File path generation and URL construction\n * - Logging, monitoring, and debugging\n * - Access control and permissions management\n * - Backup and replication configuration\n *\n * **Naming Conventions**:\n * - Use lowercase with hyphens for consistency\n * - Include purpose or content type in the name\n * - Avoid spaces and special characters\n * - Consider environment prefixes for deployment isolation\n *\n * If not provided, defaults to the property key where the bucket is declared.\n *\n * @example \"user-avatars\"\n * @example \"product-images\"\n * @example \"legal-documents\"\n * @example \"temp-processing-files\"\n */\n name?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Human-readable description of the bucket's purpose and contents.\n *\n * Used for:\n * - Documentation generation and API references\n * - Developer onboarding and system understanding\n * - Monitoring dashboards and admin interfaces\n * - Compliance and audit documentation\n *\n * **Description Best Practices**:\n * - Explain what types of files this bucket stores\n * - Mention any special handling or processing requirements\n * - Include information about retention policies if applicable\n * - Note any compliance or security considerations\n *\n * @example \"User profile pictures and avatar images\"\n * @example \"Product catalog images with automated thumbnail generation\"\n * @example \"Legal documents requiring long-term retention\"\n * @example \"Temporary files for data processing workflows\"\n */\n description?: string;\n\n /**\n * Array of allowed MIME types for files uploaded to this bucket.\n *\n * When specified, only files with these exact MIME types will be accepted.\n * Files with disallowed MIME types will be rejected with an InvalidFileError.\n *\n * **MIME Type Categories**:\n * - Images: \"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/svg+xml\"\n * - Documents: \"application/pdf\", \"text/plain\", \"text/csv\"\n * - Office: \"application/msword\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n * - Archives: \"application/zip\", \"application/x-tar\", \"application/gzip\"\n * - Media: \"video/mp4\", \"audio/mpeg\", \"audio/wav\"\n *\n * **Security Considerations**:\n * - Always validate MIME types for user uploads\n * - Be cautious with executable file types\n * - Consider using allow-lists rather than deny-lists\n * - Remember that MIME types can be spoofed by malicious users\n *\n * If not specified, all MIME types are allowed (not recommended for user uploads).\n *\n * @example [\"image/jpeg\", \"image/png\"] // Only JPEG and PNG images\n * @example [\"application/pdf\", \"text/plain\"] // Documents only\n * @example [\"video/mp4\", \"video/webm\"] // Video files\n */\n mimeTypes?: string[];\n\n /**\n * Maximum file size allowed in megabytes (MB).\n *\n * Files larger than this limit will be rejected with an InvalidFileError.\n * This helps prevent:\n * - Storage quota exhaustion\n * - Memory issues during file processing\n * - Long upload times and timeouts\n * - Abuse of storage resources\n *\n * **Size Guidelines by File Type**:\n * - Profile images: 1-5 MB\n * - Product photos: 5-10 MB\n * - Documents: 10-50 MB\n * - Video files: 50-500 MB\n * - Data files: 100-1000 MB\n *\n * **Considerations**:\n * - Consider your storage costs and limits\n * - Factor in network upload speeds for users\n * - Account for processing requirements (thumbnails, compression)\n * - Set reasonable limits based on actual use cases\n *\n * @default 10 MB\n *\n * @example 1 // 1MB for small images\n * @example 25 // 25MB for documents\n * @example 100 // 100MB for media files\n */\n maxSize?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {\n public readonly provider = this.$provider();\n protected readonly fileSystem = $inject(FileSystemProvider);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Uploads a file to the bucket.\n */\n public async upload(\n file: FileLike,\n options?: BucketFileOptions,\n ): Promise<string> {\n if (file instanceof File) {\n // our createFile is smarter than the browser's File constructor\n // by doing this, we can guess the MIME type and size!\n file = this.fileSystem.createFile({ file });\n }\n\n options = {\n ...this.options,\n ...options,\n };\n\n const mimeTypes = options.mimeTypes ?? undefined;\n const maxSize = options.maxSize ?? 10; // Default to 10 MB if not specified\n\n if (mimeTypes) {\n const mimeType = file.type || \"application/octet-stream\";\n if (!mimeTypes.includes(mimeType)) {\n throw new InvalidFileError(\n `MIME type ${mimeType} is not allowed in bucket ${this.name}`,\n );\n }\n }\n\n // check size in bytes, convert MB to bytes\n if (file.size > maxSize * 1024 * 1024) {\n throw new InvalidFileError(\n `File size ${file.size} exceeds the maximum size of ${maxSize} MB in bucket ${this.name}`,\n );\n }\n\n const id = await this.provider.upload(this.name, file);\n\n await this.alepha.events.emit(\"bucket:file:uploaded\", {\n id,\n bucket: this,\n file,\n options,\n });\n\n return id;\n }\n\n /**\n * Delete permanently a file from the bucket.\n */\n public async delete(fileId: string, skipHook = false): Promise<void> {\n await this.provider.delete(this.name, fileId);\n\n if (skipHook) {\n return;\n }\n\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id: fileId,\n bucket: this,\n });\n }\n\n /**\n * Checks if a file exists in the bucket.\n */\n public async exists(fileId: string): Promise<boolean> {\n return this.provider.exists(this.name, fileId);\n }\n\n /**\n * Downloads a file from the bucket.\n */\n public async download(fileId: string): Promise<FileLike> {\n const file = await this.provider.download(this.name, fileId);\n\n await this.alepha.events.emit(\"bucket:file:downloaded\", {\n id: fileId,\n bucket: this,\n file,\n });\n\n return file;\n }\n\n protected $provider() {\n if (!this.options.provider) {\n return this.alepha.inject(FileStorageProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemoryFileStorageProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n$bucket[KIND] = BucketPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Optional description of the bucket.\n */\n description?: string;\n\n /**\n * Allowed MIME types.\n */\n mimeTypes?: string[];\n\n /**\n * Maximum size of the files in the bucket.\n *\n * @default 10\n */\n maxSize?: number;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type * as fs from \"node:fs\";\nimport { createReadStream } from \"node:fs\";\nimport { mkdir, stat, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n $atom,\n $hook,\n $inject,\n $state,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Local file storage configuration atom\n */\nexport const localFileStorageOptions = $atom({\n name: \"alepha.bucket.local.options\",\n schema: t.object({\n storagePath: t.string({\n description: \"Directory path where files will be stored\",\n }),\n }),\n default: {\n storagePath: \"node_modules/.alepha/buckets\",\n },\n});\n\nexport type LocalFileStorageProviderOptions = Static<\n typeof localFileStorageOptions.schema\n>;\n\ndeclare module \"alepha\" {\n interface State {\n [localFileStorageOptions.key]: LocalFileStorageProviderOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class LocalFileStorageProvider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly fileSystemProvider = $inject(FileSystemProvider);\n protected readonly options = $state(localFileStorageOptions);\n\n protected get storagePath(): string {\n return this.options.storagePath;\n }\n\n protected readonly onConfigure = $hook({\n on: \"configure\",\n handler: async () => {\n if (\n this.alepha.isTest() &&\n this.storagePath === localFileStorageOptions.options.default.storagePath\n ) {\n this.alepha.store.set(localFileStorageOptions, {\n storagePath: join(tmpdir(), `alepha-test-${Date.now()}`),\n });\n }\n },\n });\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n try {\n await mkdir(this.storagePath, { recursive: true });\n } catch {}\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n await mkdir(join(this.storagePath, bucket.name), {\n recursive: true,\n });\n\n this.log.debug(`Bucket '${bucket.name}' at ${this.storagePath} OK`);\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\n\n this.log.trace(`Uploading file to ${bucketName}`);\n\n await this.fileSystemProvider.writeFile(\n this.path(bucketName, fileId),\n file,\n );\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const filePath = this.path(bucketName, fileId);\n\n try {\n const stats = await stat(filePath);\n const mimeType = this.fileDetector.getContentType(fileId);\n\n return this.fileSystemProvider.createFile({\n stream: createReadStream(filePath),\n name: fileId,\n type: mimeType,\n size: stats.size,\n });\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Invalid file operation\", { cause: error });\n }\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n try {\n await stat(this.path(bucketName, fileId));\n return true;\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n return false;\n }\n throw new AlephaError(\"Error checking file existence\", { cause: error });\n }\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n try {\n return await unlink(this.path(bucketName, fileId));\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Error deleting file\", { cause: error });\n }\n }\n\n protected stat(bucket: string, fileId: string): Promise<fs.Stats> {\n return stat(this.path(bucket, fileId));\n }\n\n protected createId(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${randomUUID()}.${ext}`;\n }\n\n protected path(bucket: string, fileId = \"\"): string {\n return join(this.storagePath, bucket, fileId);\n }\n\n protected isErrorNoEntry(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n }\n}\n","import { Buffer } from \"node:buffer\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { S3mini } from \"s3mini\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\nconst envSchema = t.object({\n /**\n * S3 endpoint URL. The bucket name is appended (path-style) per request.\n *\n * Examples:\n * - AWS S3: `https://s3.us-east-1.amazonaws.com`\n * - Cloudflare R2: `https://<account-id>.r2.cloudflarestorage.com`\n * - MinIO: `http://localhost:9000`\n * - DigitalOcean Spaces: `https://<region>.digitaloceanspaces.com`\n */\n S3_ENDPOINT: t.string(),\n\n /**\n * AWS region or \"auto\" for R2.\n *\n * @default \"auto\"\n */\n S3_REGION: t.optional(t.string()),\n\n /**\n * Access key ID for S3 authentication.\n */\n S3_ACCESS_KEY_ID: t.string(),\n\n /**\n * Secret access key for S3 authentication.\n */\n S3_SECRET_ACCESS_KEY: t.string(),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof envSchema>> {}\n}\n\n/**\n * S3-compatible file storage provider for Node.js.\n *\n * Backed by `s3mini` (zero-dep, ~20 KB). Works with AWS S3, Cloudflare R2,\n * MinIO, DigitalOcean Spaces, Backblaze B2, and any other S3-compatible service.\n *\n * Uses path-style addressing (`<endpoint>/<bucket>`).\n */\nexport class NodeS3BucketProvider implements FileStorageProvider {\n protected readonly log = $logger();\n protected readonly env = $env(envSchema);\n protected readonly alepha = $inject(Alepha);\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly clients: Map<string, S3mini> = new Map();\n\n /**\n * Convert bucket name to S3-compatible format.\n * S3 bucket names must be lowercase, 3-63 characters, no underscores.\n */\n public convertName(name: string): string {\n return name.replaceAll(\"/\", \"-\").replaceAll(\"_\", \"-\").toLowerCase();\n }\n\n protected getClient(bucketName: string): S3mini {\n const name = this.convertName(bucketName);\n let client = this.clients.get(name);\n if (!client) {\n const endpoint = this.env.S3_ENDPOINT.replace(/\\/+$/, \"\");\n client = new S3mini({\n accessKeyId: this.env.S3_ACCESS_KEY_ID,\n secretAccessKey: this.env.S3_SECRET_ACCESS_KEY,\n region: this.env.S3_REGION || \"auto\",\n endpoint: `${endpoint}/${name}`,\n });\n this.clients.set(name, client);\n }\n return client;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n const name = this.convertName(bucket.name);\n const client = this.getClient(bucket.name);\n\n this.log.debug(`Preparing S3 bucket '${name}'...`);\n\n const exists = await client.bucketExists();\n if (!exists) {\n this.log.debug(`Creating S3 bucket '${name}'...`);\n const created = await client.createBucket();\n if (!created) {\n throw new AlephaError(`Failed to create S3 bucket '${name}'`);\n }\n }\n\n this.log.info(`S3 bucket '${bucket.name}' OK`);\n }\n },\n });\n\n protected createId(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${crypto.randomUUID()}.${ext}`;\n }\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\n\n this.log.trace(\n `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,\n );\n\n const client = this.getClient(bucketName);\n\n try {\n const buffer = new Uint8Array(await file.arrayBuffer());\n await client.putObject(\n fileId,\n buffer,\n file.type || \"application/octet-stream\",\n undefined,\n { \"x-amz-meta-name\": encodeURIComponent(file.name) },\n file.size,\n );\n\n this.log.trace(`File uploaded successfully: ${fileId}`);\n return fileId;\n } catch (error) {\n this.log.error(`Failed to upload file: ${error}`);\n if (error instanceof Error) {\n throw new AlephaError(`Upload failed: ${error.message}`, {\n cause: error,\n });\n }\n throw error;\n }\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n this.log.trace(\n `Downloading file '${fileId}' from bucket '${bucketName}'...`,\n );\n\n const client = this.getClient(bucketName);\n const response = await client.getObjectResponse(fileId);\n\n if (!response) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'`,\n );\n }\n\n const buffer = Buffer.from(await response.arrayBuffer());\n\n const mimeType =\n response.headers.get(\"content-type\") ||\n this.fileDetector.getContentType(fileId);\n\n const metaName = response.headers.get(\"x-amz-meta-name\");\n const name = metaName ? decodeURIComponent(metaName) : fileId;\n\n return this.fileSystem.createFile({\n buffer,\n name,\n type: mimeType,\n size: buffer.length,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n this.log.trace(\n `Checking existence of file '${fileId}' in bucket '${bucketName}'...`,\n );\n\n const client = this.getClient(bucketName);\n const result = await client.objectExists(fileId);\n return result === true;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);\n\n const client = this.getClient(bucketName);\n\n try {\n await client.deleteObject(fileId);\n } catch (error) {\n this.log.error(`Failed to delete file: ${error}`);\n if (error instanceof Error) {\n throw new FileNotFoundError(\"Error deleting file\", { cause: error });\n }\n throw error;\n }\n }\n}\n","import type { R2Bucket } from \"@cloudflare/workers-types\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cloudflare R2 storage provider.\n *\n * Uses a single R2 bucket binding for all $bucket primitives.\n * Files are organized as: {APP_NAME}/{bucketName}/{fileId}\n *\n * **Required environment variables:**\n * - `R2_BUCKET_NAME` - The actual R2 bucket name in Cloudflare\n *\n * **Optional (uses core Alepha env):**\n * - `APP_NAME` - Prefix for all files (for multi-app setups sharing one R2 bucket)\n *\n * @example\n * ```bash\n * # .env\n * APP_NAME=myapp # optional, used as prefix\n * R2_BUCKET_NAME=storage\n * ```\n *\n * @example\n * ```toml\n * # wrangler.toml - automatically generated by alepha build\n * [[r2_buckets]]\n * binding = \"R2\"\n * bucket_name = \"storage\"\n * ```\n *\n * @example\n * ```ts\n * // Define buckets with validation rules\n * const avatars = $bucket({\n * name: \"avatars\",\n * maxFileSize: 5 * 1024 * 1024,\n * allowedMimeTypes: [\"image/*\"],\n * });\n *\n * const documents = $bucket({\n * name: \"documents\",\n * maxFileSize: 50 * 1024 * 1024,\n * allowedMimeTypes: [\"application/pdf\"],\n * });\n *\n * // Files stored at: myapp/avatars/uuid.png, myapp/documents/uuid.pdf\n * ```\n */\nexport class CloudflareR2Provider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n /**\n * The actual R2 bucket name in Cloudflare.\n */\n R2_BUCKET_NAME: t.string({\n description: \"R2 bucket name in Cloudflare\",\n }),\n }),\n );\n\n protected r2?: R2Bucket;\n\n /**\n * Get the R2 bucket name from environment.\n */\n public get bucketName(): string {\n return this.env.R2_BUCKET_NAME;\n }\n\n /**\n * Get the optional prefix from APP_NAME environment variable.\n * Used for multi-app setups sharing the same R2 bucket.\n */\n public get prefix(): string | undefined {\n return this.alepha.env.APP_NAME;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n const cloudflareEnv = this.alepha.get(\"cloudflare.env\") as\n | Record<string, unknown>\n | undefined;\n if (!cloudflareEnv) {\n throw new AlephaError(\n \"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.\",\n );\n }\n\n const binding = cloudflareEnv[this.bucketName] as R2Bucket | undefined;\n if (!binding) {\n throw new AlephaError(\n `R2 binding '${this.bucketName}' not found in Cloudflare Workers environment.`,\n );\n }\n\n this.r2 = binding;\n\n const prefixStr = this.prefix ? `${this.prefix}/` : \"\";\n this.log.info(\n `R2 storage ready (bucket: ${this.bucketName}, prefix: ${prefixStr || \"(none)\"})`,\n );\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n this.log.debug(\n `Bucket '${bucket.name}' -> ${prefixStr}${bucket.name}/`,\n );\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n const r2 = this.getR2();\n fileId ??= this.createId(file.name);\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Uploading '${key}'`);\n\n const arrayBuffer = await file.arrayBuffer();\n\n await r2.put(key, arrayBuffer, {\n httpMetadata: {\n contentType: file.type,\n },\n customMetadata: {\n originalName: file.name,\n bucket: bucketName,\n },\n });\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Downloading '${key}'`);\n\n const object = await r2.get(key);\n if (!object) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n const originalName = object.customMetadata?.originalName ?? fileId;\n const contentType =\n object.httpMetadata?.contentType ?? \"application/octet-stream\";\n\n return {\n name: originalName,\n type: contentType,\n size: object.size,\n lastModified: object.uploaded.getTime(),\n stream: () => object.body as unknown as ReadableStream,\n arrayBuffer: () => object.arrayBuffer(),\n text: () => object.text(),\n };\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Checking '${key}'`);\n\n const object = await r2.head(key);\n return object !== null;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Deleting '${key}'`);\n\n // R2's delete doesn't throw if the file doesn't exist,\n // so we check existence first for consistency with other providers\n const exists = await this.exists(bucketName, fileId);\n if (!exists) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n await r2.delete(key);\n }\n\n /**\n * Build the full R2 key: {prefix}/{bucketName}/{fileId}\n */\n protected key(bucketName: string, fileId: string): string {\n const parts = [bucketName, fileId];\n if (this.prefix) {\n parts.unshift(this.prefix);\n }\n return parts.join(\"/\");\n }\n\n protected getR2(): R2Bucket {\n if (!this.r2) {\n throw new AlephaError(\"R2 storage not initialized. Call start() first.\");\n }\n return this.r2;\n }\n\n protected createId(filename: string): string {\n const ext = filename.includes(\".\") ? filename.split(\".\").pop() : \"\";\n const id = crypto.randomUUID();\n return ext ? `${id}.${ext}` : id;\n }\n}\n","import { $module, type FileLike } from \"alepha\";\nimport {\n $bucket,\n type BucketFileOptions,\n type BucketPrimitive,\n} from \"./primitives/$bucket.ts\";\nimport { FileStorageProvider } from \"./providers/FileStorageProvider.ts\";\nimport { LocalFileStorageProvider } from \"./providers/LocalFileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"./providers/MemoryFileStorageProvider.ts\";\nimport { NodeS3BucketProvider } from \"./providers/NodeS3BucketProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/FileNotFoundError.ts\";\nexport * from \"./primitives/$bucket.ts\";\nexport * from \"./providers/CloudflareR2Provider.ts\";\nexport * from \"./providers/FileStorageProvider.ts\";\nexport * from \"./providers/LocalFileStorageProvider.ts\";\nexport * from \"./providers/MemoryFileStorageProvider.ts\";\nexport * from \"./providers/NodeS3BucketProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Hooks {\n /**\n * Triggered when a file is uploaded to a bucket.\n * Can be used to perform actions after a file is uploaded, like creating a database record!\n */\n \"bucket:file:uploaded\": {\n id: string;\n file: FileLike;\n bucket: BucketPrimitive;\n options: BucketFileOptions;\n };\n /**\n * Triggered when a file is deleted from a bucket.\n */\n \"bucket:file:deleted\": {\n id: string;\n bucket: BucketPrimitive;\n };\n /**\n * Triggered when a file is downloaded from a bucket.\n */\n \"bucket:file:downloaded\": {\n id: string;\n file: FileLike;\n bucket: BucketPrimitive;\n };\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Unified file storage abstraction across multiple backends.\n *\n * **Features:**\n * - File storage buckets with constraints\n * - Unified API across all storage backends\n * - MIME type validation\n * - File size limits\n * - Upload/download/delete operations\n * - TTL-based file expiration\n * - Providers: Memory (testing), Local filesystem, AWS S3 / Cloudflare R2 / MinIO, Azure Blob Storage, Vercel Blob\n *\n * @module alepha.bucket\n */\nexport const AlephaBucket = $module({\n name: \"alepha.bucket\",\n primitives: [$bucket],\n services: [FileStorageProvider],\n variants: [\n MemoryFileStorageProvider,\n LocalFileStorageProvider,\n NodeS3BucketProvider,\n ],\n register: (alepha) => {\n const useS3 = !!alepha.env.S3_ENDPOINT;\n alepha.with({\n optional: true,\n provide: FileStorageProvider,\n use:\n alepha.isTest() || alepha.isServerless()\n ? MemoryFileStorageProvider\n : useS3\n ? NodeS3BucketProvider\n : LocalFileStorageProvider,\n });\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["Buffer"],"sources":["../../src/bucket/errors/InvalidFileError.ts","../../src/bucket/providers/FileStorageProvider.ts","../../src/bucket/errors/FileNotFoundError.ts","../../src/bucket/providers/MemoryFileStorageProvider.ts","../../src/bucket/primitives/$bucket.ts","../../src/bucket/providers/LocalFileStorageProvider.ts","../../src/bucket/providers/NodeS3BucketProvider.ts","../../src/bucket/providers/CloudflareR2Provider.ts","../../src/bucket/index.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport class InvalidFileError extends AlephaError {\n public readonly status = 400;\n}\n","import type { FileLike } from \"alepha\";\n\nexport abstract class FileStorageProvider {\n /**\n * Uploads a file to the storage.\n *\n * @param bucketName - Container name\n * @param file - File to upload\n * @param fileId - Optional file identifier. If not provided, a unique ID will be generated.\n * @return The identifier of the uploaded file.\n */\n abstract upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string>;\n\n /**\n * Downloads a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to download\n * @return The downloaded file as a FileLike object.\n */\n abstract download(bucketName: string, fileId: string): Promise<FileLike>;\n\n /**\n * Check if fileId exists in the storage bucket.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to stream\n * @return True is the file exists, false otherwise.\n */\n abstract exists(bucketName: string, fileId: string): Promise<boolean>;\n\n /**\n * Delete permanently a file from the storage.\n *\n * @param bucketName - Container name\n * @param fileId - Identifier of the file to delete\n */\n abstract delete(bucketName: string, fileId: string): Promise<void>;\n\n /**\n * Delete multiple files in one round-trip where the provider supports\n * batch (R2/S3, up to 1000 per call). Memory/Local fall back to a loop.\n *\n * @param bucketName - Container name\n * @param fileIds - Identifiers of the files to delete\n */\n abstract deleteMany(bucketName: string, fileIds: string[]): Promise<void>;\n}\n","import { AlephaError } from \"alepha\";\n\nexport class FileNotFoundError extends AlephaError {\n public readonly status = 404;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { $inject, type FileLike } from \"alepha\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\ninterface StoredFile {\n buffer: Buffer;\n name: string;\n type: string;\n size: number;\n}\n\nexport class MemoryFileStorageProvider implements FileStorageProvider {\n public readonly files: Record<string, StoredFile> = {};\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId();\n\n // Consume the stream and store as buffer so downloads are repeatable\n const chunks: Uint8Array[] = [];\n for await (const chunk of file.stream() as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n this.files[`${bucketName}/${fileId}`] = {\n buffer,\n name: file.name,\n type: file.type,\n size: file.size,\n };\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const fileKey = `${bucketName}/${fileId}`;\n const stored = this.files[fileKey];\n\n if (!stored) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n // Create a fresh FileLike with a new stream from the stored buffer\n return this.fileSystem.createFile({\n stream: new Blob([new Uint8Array(stored.buffer)]).stream(),\n name: stored.name,\n type: stored.type,\n size: stored.size,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n return `${bucketName}/${fileId}` in this.files;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const fileKey = `${bucketName}/${fileId}`;\n if (!(fileKey in this.files)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n\n delete this.files[fileKey];\n }\n\n public async deleteMany(\n bucketName: string,\n fileIds: string[],\n ): Promise<void> {\n for (const id of fileIds) {\n delete this.files[`${bucketName}/${id}`];\n }\n }\n\n protected createId(): string {\n return randomUUID();\n }\n}\n","import {\n $inject,\n createPrimitive,\n type FileLike,\n KIND,\n Primitive,\n type Service,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { InvalidFileError } from \"../errors/InvalidFileError.ts\";\nimport { FileStorageProvider } from \"../providers/FileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"../providers/MemoryFileStorageProvider.ts\";\n\n/**\n * Creates a bucket primitive for file storage and management with configurable validation.\n *\n * Provides a comprehensive file storage system that handles uploads, downloads, validation,\n * and management across multiple storage backends with MIME type and size limit controls.\n *\n * **Key Features**\n * - Multi-provider support (filesystem, cloud storage, in-memory)\n * - Automatic MIME type and file size validation\n * - Event integration for file operations monitoring\n * - Flexible per-bucket and per-operation configuration\n * - Smart file type and size detection\n *\n * **Common Use Cases**\n * - User profile pictures and document uploads\n * - Product images and media management\n * - Document storage and retrieval systems\n *\n * @example\n * ```ts\n * class MediaService {\n * images = $bucket({\n * name: \"user-images\",\n * mimeTypes: [\"image/jpeg\", \"image/png\", \"image/gif\"],\n * maxSize: 5 // 5MB limit\n * });\n *\n * documents = $bucket({\n * name: \"documents\",\n * mimeTypes: [\"application/pdf\", \"text/plain\"],\n * maxSize: 50 // 50MB limit\n * });\n *\n * async uploadProfileImage(file: FileLike, userId: string): Promise<string> {\n * const fileId = await this.images.upload(file);\n * await this.userService.updateProfileImage(userId, fileId);\n * return fileId;\n * }\n *\n * async downloadDocument(documentId: string): Promise<FileLike> {\n * return await this.documents.download(documentId);\n * }\n *\n * async deleteDocument(documentId: string): Promise<void> {\n * await this.documents.delete(documentId);\n * }\n * }\n * ```\n */\nexport const $bucket = (options: BucketPrimitiveOptions) =>\n createPrimitive(BucketPrimitive, options);\n\nexport interface BucketPrimitiveOptions extends BucketFileOptions {\n /**\n * File storage provider configuration for the bucket.\n *\n * Options:\n * - **\"memory\"**: In-memory storage (default for development, lost on restart)\n * - **Service<FileStorageProvider>**: Custom provider class (e.g., S3FileStorageProvider, AzureBlobProvider)\n * - **undefined**: Uses the default file storage provider from dependency injection\n *\n * **Provider Selection Guidelines**:\n * - **Development**: Use \"memory\" for fast, simple testing without external dependencies\n * - **Production**: Use cloud providers (S3, Azure Blob, Google Cloud Storage) for scalability\n * - **Local deployment**: Use filesystem providers for on-premise installations\n * - **Hybrid**: Use different providers for different bucket types (temp files vs permanent storage)\n *\n * **Provider Capabilities**:\n * - File persistence and durability guarantees\n * - Scalability and performance characteristics\n * - Geographic distribution and CDN integration\n * - Cost implications for storage and bandwidth\n * - Backup and disaster recovery features\n *\n * @default Uses injected FileStorageProvider\n * @example \"memory\"\n * @example S3FileStorageProvider\n * @example AzureBlobStorageProvider\n */\n provider?: Service<FileStorageProvider> | \"memory\";\n\n /**\n * Unique name identifier for the bucket.\n *\n * This name is used for:\n * - Storage backend organization and partitioning\n * - File path generation and URL construction\n * - Logging, monitoring, and debugging\n * - Access control and permissions management\n * - Backup and replication configuration\n *\n * **Naming Conventions**:\n * - Use lowercase with hyphens for consistency\n * - Include purpose or content type in the name\n * - Avoid spaces and special characters\n * - Consider environment prefixes for deployment isolation\n *\n * If not provided, defaults to the property key where the bucket is declared.\n *\n * @example \"user-avatars\"\n * @example \"product-images\"\n * @example \"legal-documents\"\n * @example \"temp-processing-files\"\n */\n name?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Human-readable description of the bucket's purpose and contents.\n *\n * Used for:\n * - Documentation generation and API references\n * - Developer onboarding and system understanding\n * - Monitoring dashboards and admin interfaces\n * - Compliance and audit documentation\n *\n * **Description Best Practices**:\n * - Explain what types of files this bucket stores\n * - Mention any special handling or processing requirements\n * - Include information about retention policies if applicable\n * - Note any compliance or security considerations\n *\n * @example \"User profile pictures and avatar images\"\n * @example \"Product catalog images with automated thumbnail generation\"\n * @example \"Legal documents requiring long-term retention\"\n * @example \"Temporary files for data processing workflows\"\n */\n description?: string;\n\n /**\n * Array of allowed MIME types for files uploaded to this bucket.\n *\n * When specified, only files with these exact MIME types will be accepted.\n * Files with disallowed MIME types will be rejected with an InvalidFileError.\n *\n * **MIME Type Categories**:\n * - Images: \"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/svg+xml\"\n * - Documents: \"application/pdf\", \"text/plain\", \"text/csv\"\n * - Office: \"application/msword\", \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n * - Archives: \"application/zip\", \"application/x-tar\", \"application/gzip\"\n * - Media: \"video/mp4\", \"audio/mpeg\", \"audio/wav\"\n *\n * **Security Considerations**:\n * - Always validate MIME types for user uploads\n * - Be cautious with executable file types\n * - Consider using allow-lists rather than deny-lists\n * - Remember that MIME types can be spoofed by malicious users\n *\n * If not specified, all MIME types are allowed (not recommended for user uploads).\n *\n * @example [\"image/jpeg\", \"image/png\"] // Only JPEG and PNG images\n * @example [\"application/pdf\", \"text/plain\"] // Documents only\n * @example [\"video/mp4\", \"video/webm\"] // Video files\n */\n mimeTypes?: string[];\n\n /**\n * Maximum file size allowed in megabytes (MB).\n *\n * Files larger than this limit will be rejected with an InvalidFileError.\n * This helps prevent:\n * - Storage quota exhaustion\n * - Memory issues during file processing\n * - Long upload times and timeouts\n * - Abuse of storage resources\n *\n * **Size Guidelines by File Type**:\n * - Profile images: 1-5 MB\n * - Product photos: 5-10 MB\n * - Documents: 10-50 MB\n * - Video files: 50-500 MB\n * - Data files: 100-1000 MB\n *\n * **Considerations**:\n * - Consider your storage costs and limits\n * - Factor in network upload speeds for users\n * - Account for processing requirements (thumbnails, compression)\n * - Set reasonable limits based on actual use cases\n *\n * @default 10 MB\n *\n * @example 1 // 1MB for small images\n * @example 25 // 25MB for documents\n * @example 100 // 100MB for media files\n */\n maxSize?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BucketPrimitive extends Primitive<BucketPrimitiveOptions> {\n public readonly provider = this.$provider();\n protected readonly fileSystem = $inject(FileSystemProvider);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Uploads a file to the bucket.\n */\n public async upload(\n file: FileLike,\n options?: BucketFileOptions,\n ): Promise<string> {\n if (file instanceof File) {\n // our createFile is smarter than the browser's File constructor\n // by doing this, we can guess the MIME type and size!\n file = this.fileSystem.createFile({ file });\n }\n\n options = {\n ...this.options,\n ...options,\n };\n\n const mimeTypes = options.mimeTypes ?? undefined;\n const maxSize = options.maxSize ?? 10; // Default to 10 MB if not specified\n\n if (mimeTypes) {\n const mimeType = file.type || \"application/octet-stream\";\n if (!mimeTypes.includes(mimeType)) {\n throw new InvalidFileError(\n `MIME type ${mimeType} is not allowed in bucket ${this.name}`,\n );\n }\n }\n\n // check size in bytes, convert MB to bytes\n if (file.size > maxSize * 1024 * 1024) {\n throw new InvalidFileError(\n `File size ${file.size} exceeds the maximum size of ${maxSize} MB in bucket ${this.name}`,\n );\n }\n\n const id = await this.provider.upload(this.name, file);\n\n await this.alepha.events.emit(\"bucket:file:uploaded\", {\n id,\n bucket: this,\n file,\n options,\n });\n\n return id;\n }\n\n /**\n * Delete permanently a file from the bucket.\n */\n public async delete(fileId: string, skipHook = false): Promise<void> {\n await this.provider.delete(this.name, fileId);\n\n if (skipHook) {\n return;\n }\n\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id: fileId,\n bucket: this,\n });\n }\n\n /**\n * Delete many files in one round-trip when the underlying provider supports\n * batch (R2/S3 up to 1000 keys per call). Emits one `bucket:file:deleted`\n * event per id unless `skipHook` is set.\n */\n public async deleteMany(fileIds: string[], skipHook = false): Promise<void> {\n if (fileIds.length === 0) return;\n await this.provider.deleteMany(this.name, fileIds);\n\n if (skipHook) {\n return;\n }\n\n for (const id of fileIds) {\n await this.alepha.events.emit(\"bucket:file:deleted\", {\n id,\n bucket: this,\n });\n }\n }\n\n /**\n * Checks if a file exists in the bucket.\n */\n public async exists(fileId: string): Promise<boolean> {\n return this.provider.exists(this.name, fileId);\n }\n\n /**\n * Downloads a file from the bucket.\n */\n public async download(fileId: string): Promise<FileLike> {\n const file = await this.provider.download(this.name, fileId);\n\n await this.alepha.events.emit(\"bucket:file:downloaded\", {\n id: fileId,\n bucket: this,\n file,\n });\n\n return file;\n }\n\n protected $provider() {\n if (!this.options.provider) {\n return this.alepha.inject(FileStorageProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemoryFileStorageProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n$bucket[KIND] = BucketPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BucketFileOptions {\n /**\n * Optional description of the bucket.\n */\n description?: string;\n\n /**\n * Allowed MIME types.\n */\n mimeTypes?: string[];\n\n /**\n * Maximum size of the files in the bucket.\n *\n * @default 10\n */\n maxSize?: number;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type * as fs from \"node:fs\";\nimport { createReadStream } from \"node:fs\";\nimport { mkdir, stat, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n $atom,\n $hook,\n $inject,\n $state,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Local file storage configuration atom\n */\nexport const localFileStorageOptions = $atom({\n name: \"alepha.bucket.local.options\",\n schema: t.object({\n storagePath: t.string({\n description: \"Directory path where files will be stored\",\n }),\n }),\n default: {\n storagePath: \"node_modules/.alepha/buckets\",\n },\n});\n\nexport type LocalFileStorageProviderOptions = Static<\n typeof localFileStorageOptions.schema\n>;\n\ndeclare module \"alepha\" {\n interface State {\n [localFileStorageOptions.key]: LocalFileStorageProviderOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class LocalFileStorageProvider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly fileSystemProvider = $inject(FileSystemProvider);\n protected readonly options = $state(localFileStorageOptions);\n\n protected get storagePath(): string {\n return this.options.storagePath;\n }\n\n protected readonly onConfigure = $hook({\n on: \"configure\",\n handler: async () => {\n if (\n this.alepha.isTest() &&\n this.storagePath === localFileStorageOptions.options.default.storagePath\n ) {\n this.alepha.store.set(localFileStorageOptions, {\n storagePath: join(tmpdir(), `alepha-test-${Date.now()}`),\n });\n }\n },\n });\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n try {\n await mkdir(this.storagePath, { recursive: true });\n } catch {}\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n await mkdir(join(this.storagePath, bucket.name), {\n recursive: true,\n });\n\n this.log.debug(`Bucket '${bucket.name}' at ${this.storagePath} OK`);\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\n\n this.log.trace(`Uploading file to ${bucketName}`);\n\n await this.fileSystemProvider.writeFile(\n this.path(bucketName, fileId),\n file,\n );\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const filePath = this.path(bucketName, fileId);\n\n try {\n const stats = await stat(filePath);\n const mimeType = this.fileDetector.getContentType(fileId);\n\n return this.fileSystemProvider.createFile({\n stream: createReadStream(filePath),\n name: fileId,\n type: mimeType,\n size: stats.size,\n });\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Invalid file operation\", { cause: error });\n }\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n try {\n await stat(this.path(bucketName, fileId));\n return true;\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n return false;\n }\n throw new AlephaError(\"Error checking file existence\", { cause: error });\n }\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n try {\n return await unlink(this.path(bucketName, fileId));\n } catch (error) {\n if (this.isErrorNoEntry(error)) {\n throw new FileNotFoundError(`File with ID ${fileId} not found.`);\n }\n throw new AlephaError(\"Error deleting file\", { cause: error });\n }\n }\n\n public async deleteMany(\n bucketName: string,\n fileIds: string[],\n ): Promise<void> {\n await Promise.all(\n fileIds.map((id) =>\n unlink(this.path(bucketName, id)).catch((error) => {\n if (this.isErrorNoEntry(error)) return;\n throw new AlephaError(\"Error deleting file\", { cause: error });\n }),\n ),\n );\n }\n\n protected stat(bucket: string, fileId: string): Promise<fs.Stats> {\n return stat(this.path(bucket, fileId));\n }\n\n protected createId(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${randomUUID()}.${ext}`;\n }\n\n protected path(bucket: string, fileId = \"\"): string {\n return join(this.storagePath, bucket, fileId);\n }\n\n protected isErrorNoEntry(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n }\n}\n","import { Buffer } from \"node:buffer\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n type Static,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileDetector, FileSystemProvider } from \"alepha/system\";\nimport { S3mini } from \"s3mini\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\nconst envSchema = t.object({\n /**\n * S3 endpoint URL. The bucket name is appended (path-style) per request.\n *\n * Examples:\n * - AWS S3: `https://s3.us-east-1.amazonaws.com`\n * - Cloudflare R2: `https://<account-id>.r2.cloudflarestorage.com`\n * - MinIO: `http://localhost:9000`\n * - DigitalOcean Spaces: `https://<region>.digitaloceanspaces.com`\n */\n S3_ENDPOINT: t.string(),\n\n /**\n * AWS region or \"auto\" for R2.\n *\n * @default \"auto\"\n */\n S3_REGION: t.optional(t.string()),\n\n /**\n * Access key ID for S3 authentication.\n */\n S3_ACCESS_KEY_ID: t.string(),\n\n /**\n * Secret access key for S3 authentication.\n */\n S3_SECRET_ACCESS_KEY: t.string(),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof envSchema>> {}\n}\n\n/**\n * S3-compatible file storage provider for Node.js.\n *\n * Backed by `s3mini` (zero-dep, ~20 KB). Works with AWS S3, Cloudflare R2,\n * MinIO, DigitalOcean Spaces, Backblaze B2, and any other S3-compatible service.\n *\n * Uses path-style addressing (`<endpoint>/<bucket>`).\n */\nexport class NodeS3BucketProvider implements FileStorageProvider {\n protected readonly log = $logger();\n protected readonly env = $env(envSchema);\n protected readonly alepha = $inject(Alepha);\n protected readonly fileSystem = $inject(FileSystemProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly clients: Map<string, S3mini> = new Map();\n\n /**\n * Convert bucket name to S3-compatible format.\n * S3 bucket names must be lowercase, 3-63 characters, no underscores.\n */\n public convertName(name: string): string {\n return name.replaceAll(\"/\", \"-\").replaceAll(\"_\", \"-\").toLowerCase();\n }\n\n protected getClient(bucketName: string): S3mini {\n const name = this.convertName(bucketName);\n let client = this.clients.get(name);\n if (!client) {\n const endpoint = this.env.S3_ENDPOINT.replace(/\\/+$/, \"\");\n client = new S3mini({\n accessKeyId: this.env.S3_ACCESS_KEY_ID,\n secretAccessKey: this.env.S3_SECRET_ACCESS_KEY,\n region: this.env.S3_REGION || \"auto\",\n endpoint: `${endpoint}/${name}`,\n });\n this.clients.set(name, client);\n }\n return client;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n\n const name = this.convertName(bucket.name);\n const client = this.getClient(bucket.name);\n\n this.log.debug(`Preparing S3 bucket '${name}'...`);\n\n const exists = await client.bucketExists();\n if (!exists) {\n this.log.debug(`Creating S3 bucket '${name}'...`);\n const created = await client.createBucket();\n if (!created) {\n throw new AlephaError(`Failed to create S3 bucket '${name}'`);\n }\n }\n\n this.log.info(`S3 bucket '${bucket.name}' OK`);\n }\n },\n });\n\n protected createId(mimeType: string): string {\n const ext = this.fileDetector.getExtensionFromMimeType(mimeType);\n return `${crypto.randomUUID()}.${ext}`;\n }\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n fileId ??= this.createId(file.type);\n\n this.log.trace(\n `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,\n );\n\n const client = this.getClient(bucketName);\n\n try {\n const buffer = new Uint8Array(await file.arrayBuffer());\n await client.putObject(\n fileId,\n buffer,\n file.type || \"application/octet-stream\",\n undefined,\n { \"x-amz-meta-name\": encodeURIComponent(file.name) },\n file.size,\n );\n\n this.log.trace(`File uploaded successfully: ${fileId}`);\n return fileId;\n } catch (error) {\n this.log.error(`Failed to upload file: ${error}`);\n if (error instanceof Error) {\n throw new AlephaError(`Upload failed: ${error.message}`, {\n cause: error,\n });\n }\n throw error;\n }\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n this.log.trace(\n `Downloading file '${fileId}' from bucket '${bucketName}'...`,\n );\n\n const client = this.getClient(bucketName);\n const response = await client.getObjectResponse(fileId);\n\n if (!response) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'`,\n );\n }\n\n const buffer = Buffer.from(await response.arrayBuffer());\n\n const mimeType =\n response.headers.get(\"content-type\") ||\n this.fileDetector.getContentType(fileId);\n\n const metaName = response.headers.get(\"x-amz-meta-name\");\n const name = metaName ? decodeURIComponent(metaName) : fileId;\n\n return this.fileSystem.createFile({\n buffer,\n name,\n type: mimeType,\n size: buffer.length,\n });\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n this.log.trace(\n `Checking existence of file '${fileId}' in bucket '${bucketName}'...`,\n );\n\n const client = this.getClient(bucketName);\n const result = await client.objectExists(fileId);\n return result === true;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);\n\n const client = this.getClient(bucketName);\n\n try {\n await client.deleteObject(fileId);\n } catch (error) {\n this.log.error(`Failed to delete file: ${error}`);\n if (error instanceof Error) {\n throw new FileNotFoundError(\"Error deleting file\", { cause: error });\n }\n throw error;\n }\n }\n\n public async deleteMany(\n bucketName: string,\n fileIds: string[],\n ): Promise<void> {\n if (fileIds.length === 0) return;\n this.log.trace(\n `Deleting ${fileIds.length} files from bucket '${bucketName}'...`,\n );\n const client = this.getClient(bucketName);\n // S3 DeleteObjects caps at 1000 keys per request.\n for (let i = 0; i < fileIds.length; i += 1000) {\n const chunk = fileIds.slice(i, i + 1000);\n try {\n // bun:s3 client exposes a per-key deleteObject; some SDKs also expose\n // deleteObjects(keys: string[]). Prefer batch when available.\n const batch = (\n client as unknown as {\n deleteObjects?: (keys: string[]) => Promise<unknown>;\n }\n ).deleteObjects;\n if (typeof batch === \"function\") {\n await batch.call(client, chunk);\n } else {\n await Promise.all(chunk.map((id) => client.deleteObject(id)));\n }\n } catch (error) {\n this.log.error(`Failed to delete files: ${error}`);\n if (error instanceof Error) {\n throw new FileNotFoundError(\"Error deleting files\", { cause: error });\n }\n throw error;\n }\n }\n }\n}\n","import type { R2Bucket } from \"@cloudflare/workers-types\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n AlephaError,\n type FileLike,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileNotFoundError } from \"../errors/FileNotFoundError.ts\";\nimport { $bucket } from \"../primitives/$bucket.ts\";\nimport type { FileStorageProvider } from \"./FileStorageProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cloudflare R2 storage provider.\n *\n * Uses a single R2 bucket binding for all $bucket primitives.\n * Files are organized as: {APP_NAME}/{bucketName}/{fileId}\n *\n * **Required environment variables:**\n * - `R2_BUCKET_NAME` - The actual R2 bucket name in Cloudflare\n *\n * **Optional (uses core Alepha env):**\n * - `APP_NAME` - Prefix for all files (for multi-app setups sharing one R2 bucket)\n *\n * @example\n * ```bash\n * # .env\n * APP_NAME=myapp # optional, used as prefix\n * R2_BUCKET_NAME=storage\n * ```\n *\n * @example\n * ```toml\n * # wrangler.toml - automatically generated by alepha build\n * [[r2_buckets]]\n * binding = \"R2\"\n * bucket_name = \"storage\"\n * ```\n *\n * @example\n * ```ts\n * // Define buckets with validation rules\n * const avatars = $bucket({\n * name: \"avatars\",\n * maxFileSize: 5 * 1024 * 1024,\n * allowedMimeTypes: [\"image/*\"],\n * });\n *\n * const documents = $bucket({\n * name: \"documents\",\n * maxFileSize: 50 * 1024 * 1024,\n * allowedMimeTypes: [\"application/pdf\"],\n * });\n *\n * // Files stored at: myapp/avatars/uuid.png, myapp/documents/uuid.pdf\n * ```\n */\nexport class CloudflareR2Provider implements FileStorageProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n /**\n * The actual R2 bucket name in Cloudflare.\n */\n R2_BUCKET_NAME: t.string({\n description: \"R2 bucket name in Cloudflare\",\n }),\n }),\n );\n\n protected r2?: R2Bucket;\n\n /**\n * Get the R2 bucket name from environment.\n */\n public get bucketName(): string {\n return this.env.R2_BUCKET_NAME;\n }\n\n /**\n * Get the optional prefix from APP_NAME environment variable.\n * Used for multi-app setups sharing the same R2 bucket.\n */\n public get prefix(): string | undefined {\n return this.alepha.env.APP_NAME;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n const cloudflareEnv = this.alepha.get(\"cloudflare.env\") as\n | Record<string, unknown>\n | undefined;\n if (!cloudflareEnv) {\n throw new AlephaError(\n \"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.\",\n );\n }\n\n const binding = cloudflareEnv[this.bucketName] as R2Bucket | undefined;\n if (!binding) {\n throw new AlephaError(\n `R2 binding '${this.bucketName}' not found in Cloudflare Workers environment.`,\n );\n }\n\n this.r2 = binding;\n\n const prefixStr = this.prefix ? `${this.prefix}/` : \"\";\n this.log.info(\n `R2 storage ready (bucket: ${this.bucketName}, prefix: ${prefixStr || \"(none)\"})`,\n );\n\n for (const bucket of this.alepha.primitives($bucket)) {\n if (bucket.provider !== this) {\n continue;\n }\n this.log.debug(\n `Bucket '${bucket.name}' -> ${prefixStr}${bucket.name}/`,\n );\n }\n },\n });\n\n public async upload(\n bucketName: string,\n file: FileLike,\n fileId?: string,\n ): Promise<string> {\n const r2 = this.getR2();\n fileId ??= this.createId(file.name);\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Uploading '${key}'`);\n\n const arrayBuffer = await file.arrayBuffer();\n\n await r2.put(key, arrayBuffer, {\n httpMetadata: {\n contentType: file.type,\n },\n customMetadata: {\n originalName: file.name,\n bucket: bucketName,\n },\n });\n\n return fileId;\n }\n\n public async download(bucketName: string, fileId: string): Promise<FileLike> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Downloading '${key}'`);\n\n const object = await r2.get(key);\n if (!object) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n const originalName = object.customMetadata?.originalName ?? fileId;\n const contentType =\n object.httpMetadata?.contentType ?? \"application/octet-stream\";\n\n return {\n name: originalName,\n type: contentType,\n size: object.size,\n lastModified: object.uploaded.getTime(),\n stream: () => object.body as unknown as ReadableStream,\n arrayBuffer: () => object.arrayBuffer(),\n text: () => object.text(),\n };\n }\n\n public async exists(bucketName: string, fileId: string): Promise<boolean> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Checking '${key}'`);\n\n const object = await r2.head(key);\n return object !== null;\n }\n\n public async delete(bucketName: string, fileId: string): Promise<void> {\n const r2 = this.getR2();\n const key = this.key(bucketName, fileId);\n\n this.log.trace(`Deleting '${key}'`);\n\n // R2's delete doesn't throw if the file doesn't exist,\n // so we check existence first for consistency with other providers\n const exists = await this.exists(bucketName, fileId);\n if (!exists) {\n throw new FileNotFoundError(\n `File '${fileId}' not found in bucket '${bucketName}'.`,\n );\n }\n\n await r2.delete(key);\n }\n\n public async deleteMany(\n bucketName: string,\n fileIds: string[],\n ): Promise<void> {\n if (fileIds.length === 0) return;\n const r2 = this.getR2();\n const keys = fileIds.map((id) => this.key(bucketName, id));\n this.log.trace(`Deleting ${keys.length} keys from '${bucketName}'`);\n // R2 binding accepts a string[] and silently ignores missing keys.\n // Chunk at 1000 to stay within the documented batch limit.\n for (let i = 0; i < keys.length; i += 1000) {\n await r2.delete(keys.slice(i, i + 1000));\n }\n }\n\n /**\n * Build the full R2 key: {prefix}/{bucketName}/{fileId}\n */\n protected key(bucketName: string, fileId: string): string {\n const parts = [bucketName, fileId];\n if (this.prefix) {\n parts.unshift(this.prefix);\n }\n return parts.join(\"/\");\n }\n\n protected getR2(): R2Bucket {\n if (!this.r2) {\n throw new AlephaError(\"R2 storage not initialized. Call start() first.\");\n }\n return this.r2;\n }\n\n protected createId(filename: string): string {\n const ext = filename.includes(\".\") ? filename.split(\".\").pop() : \"\";\n const id = crypto.randomUUID();\n return ext ? `${id}.${ext}` : id;\n }\n}\n","import { $module, type FileLike } from \"alepha\";\nimport {\n $bucket,\n type BucketFileOptions,\n type BucketPrimitive,\n} from \"./primitives/$bucket.ts\";\nimport { FileStorageProvider } from \"./providers/FileStorageProvider.ts\";\nimport { LocalFileStorageProvider } from \"./providers/LocalFileStorageProvider.ts\";\nimport { MemoryFileStorageProvider } from \"./providers/MemoryFileStorageProvider.ts\";\nimport { NodeS3BucketProvider } from \"./providers/NodeS3BucketProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/FileNotFoundError.ts\";\nexport * from \"./primitives/$bucket.ts\";\nexport * from \"./providers/CloudflareR2Provider.ts\";\nexport * from \"./providers/FileStorageProvider.ts\";\nexport * from \"./providers/LocalFileStorageProvider.ts\";\nexport * from \"./providers/MemoryFileStorageProvider.ts\";\nexport * from \"./providers/NodeS3BucketProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Hooks {\n /**\n * Triggered when a file is uploaded to a bucket.\n * Can be used to perform actions after a file is uploaded, like creating a database record!\n */\n \"bucket:file:uploaded\": {\n id: string;\n file: FileLike;\n bucket: BucketPrimitive;\n options: BucketFileOptions;\n };\n /**\n * Triggered when a file is deleted from a bucket.\n */\n \"bucket:file:deleted\": {\n id: string;\n bucket: BucketPrimitive;\n };\n /**\n * Triggered when a file is downloaded from a bucket.\n */\n \"bucket:file:downloaded\": {\n id: string;\n file: FileLike;\n bucket: BucketPrimitive;\n };\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Unified file storage abstraction across multiple backends.\n *\n * **Features:**\n * - File storage buckets with constraints\n * - Unified API across all storage backends\n * - MIME type validation\n * - File size limits\n * - Upload/download/delete operations\n * - TTL-based file expiration\n * - Providers: Memory (testing), Local filesystem, AWS S3 / Cloudflare R2 / MinIO, Azure Blob Storage, Vercel Blob\n *\n * @module alepha.bucket\n */\nexport const AlephaBucket = $module({\n name: \"alepha.bucket\",\n primitives: [$bucket],\n services: [FileStorageProvider],\n variants: [\n MemoryFileStorageProvider,\n LocalFileStorageProvider,\n NodeS3BucketProvider,\n ],\n register: (alepha) => {\n const useS3 = !!alepha.env.S3_ENDPOINT;\n alepha.with({\n optional: true,\n provide: FileStorageProvider,\n use:\n alepha.isTest() || alepha.isServerless()\n ? MemoryFileStorageProvider\n : useS3\n ? NodeS3BucketProvider\n : LocalFileStorageProvider,\n });\n },\n});\n"],"mappings":"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"}
|