@velajs/cloudflare 1.24.0 → 1.29.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,133 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 416650e: Cron triggers run core `@Cron()` jobs through `invokeScheduledJob`, the same primitive as the Node executor: the adapter runs every `@Cron` job whose expression is exactly the trigger string, in a fresh invocation scope, and the trigger settles after every matching job and its `EXECUTION_LIFETIME` work settle. Closing the application aborts the invocation signal of running jobs and waits for them.
8
+
9
+ Add `CLOUDFLARE_SCHEDULED_EVENT`, a request-scoped token seeded into each job's invocation scope. Its `CloudflareScheduledEvent` value carries the trigger's `cron`, `scheduledTime` and a `noRetry()` already bound to the native controller. The token provides itself as request-scoped in every container, so a class that injects it is request-scoped wherever the module graph boots, including a `VelaWebSocketDurableObject`, `vela` CLI commands and `Test.createTestingModule()`, and is constructed per invocation instead of at bootstrap; resolving it outside a scheduled invocation throws. `ScheduledEvent` (the input of `scheduled()`) now also accepts the controller's optional `noRetry`.
10
+
11
+ Signed `ScheduleModule` dispatch now works on Workers: the adapter's invocation transport re-enters the signed route, so its global guards run.
12
+
13
+ The adapter reports schedule declarations a cron trigger cannot honor through the diagnostics policy: a `@Cron` without a dialect whose weekday field has digits or whose day fields are both restricted, `dialect: 'unix'`, `timeZone: 'local'`, `@Interval` jobs, which never run on Workers, and `@UseGuards`, `@UseInterceptors` or `@UseFilters` declared for a cron job. The default `'log'` mode warns once per declaration and never fails the first event; `'throw'` fails bootstrap. `vela deploy check` rejects the cron declarations and `@Interval` jobs before deployment (`ambiguous-cron-dialect`, `incompatible-cron-options`, `unsupported-interval`).
14
+
15
+ The adapter provides `SCHEDULE_INVOCATION_SEED`: a cron job fired outside a trigger, such as by Studio's run-now, receives a synthetic `CLOUDFLARE_SCHEDULED_EVENT` whose `cron` is the job's expression, whose `scheduledTime` is the invocation's, and whose `noRetry()` does nothing.
16
+
17
+ **Behavior change:** `@Scheduled` and `parseScheduledMetadata` are removed, along with the `ScheduledMetadata`, `ScheduledController`, `ScheduledContext` and `ScheduledHandler` types and the `cf:scheduled` and `cf:vela-cron` entrypoint kinds. Replace `@Scheduled(expr)` with `@Cron(expr, { dialect: 'cloudflare' })` from `@velajs/vela`. Cron jobs appear only as `schedule:cron` entrypoints.
18
+
19
+ **Behavior change:** scheduled handlers receive only a `ScheduleInvocation` (`kind`, `expression` equal to the trigger string, `scheduledTime`, `signal`), identical to Node, instead of `(controller, env, ctx)`. Inject `ENV` for bindings, `CLOUDFLARE_SCHEDULED_EVENT` for `noRetry()`, and `EXECUTION_LIFETIME` for `waitUntil()`.
20
+
21
+ **Behavior change:** scheduled jobs no longer run interceptors or filters declared with `@UseInterceptors` or `@UseFilters`, matching the Node executor. A job that declares `@UseGuards` on its class, method or module, whose guards the adapter used to run on each trigger, is now refused instead of running unguarded: the trigger fails, the job is never constructed, and the refusal is reported through the exception reporter (guards do not run for directly dispatched scheduled jobs — use `ScheduleModule.forRoot({ dispatch: { kind: 'signed', ... } })` or remove the guard). Other jobs on the same trigger still run. Queue consumers keep their guards, interceptors and filters. Use signed `ScheduleModule` dispatch to run a job through a route's request pipeline, or remove the guard.
22
+
23
+ **Behavior change:** a `@Cron` job that declares `@UseGuards`, `@UseInterceptors` or `@UseFilters` on its class, method or module is reported through the diagnostics policy, because those components never run for scheduled jobs: the default `'log'` mode warns once and `'throw'` fails bootstrap. Move them to a signed `ScheduleModule` dispatch route.
24
+ - a3e2b38: The Cloudflare runtime seeds the native environment as the framework `ENV`, in the Worker and in every `VelaWebSocketDurableObject`, and types it with the environment `wrangler types` generates: the package augments `VelaEnv` with `Cloudflare.Env`, so `@InjectEnv() env: VelaEnv`, `inject: [ENV]` factories and `registerAs` factories see your bindings, variables and secrets typed. Run `wrangler types` (for example with `--include-runtime=false` alongside `@cloudflare/workers-types`) so `Cloudflare.Env` declares them. The per-environment application cache and the environment identity assertion are unchanged.
25
+
26
+ `createCloudflareWorker` and `createCloudflareApp` accept `adapters: RuntimeAdapter[]`, composed after the Cloudflare adapter for each application, so a Worker entry can stay `export default createCloudflareWorker(AppModule, { adapters: [...] })` without a hand-written per-environment cache.
27
+
28
+ **Behavior change:** the `envToken` option is removed from `createCloudflareWorker`, `createCloudflareApp`, `cloudflareAdapter`, `VelaWebSocketDurableObject` and `buildDoRuntime`, with no alias. Delete the application's environment `InjectionToken` and inject `ENV` from `@velajs/vela` instead: `createCloudflareWorker(AppModule)`, `VelaWebSocketDurableObject(AppModule)`, `cloudflareAdapter({ env })`. `CloudflareApplication` and `CloudflareRoot` are no longer generic; their environment type is `VelaEnv`.
29
+
30
+ **Behavior change:** the `@Env()` parameter decorator is removed. Inject the environment with `@InjectEnv()` in a constructor, or read a binding in a factory with `inject: [ENV]`.
31
+
32
+ **Behavior change:** ENV now carries every binding, variable and secret of the Worker, so framework readers pick up values such as `URL_SIGNING_SECRET` (URL and invocation signing) and `VELA_STUDIO_TOKEN` (Studio) automatically once they are set as variables or secrets. Values come from outside the program: validate each value your code reads before relying on it.
33
+ - 2ae8505: Add the `@velajs/cloudflare/queues` subpath with `cloudflareQueues()`, the Cloudflare Queues driver for `QueueModule`. Configure it once with `QueueModule.forRoot({ driver: cloudflareQueues() })` and register each queue where it is used with `QueueModule.registerQueue({ name: 'email', binding: 'EMAIL_QUEUE' })`. Each application gets its own driver, which reads the registered binding from that application's `ENV` when a job is added, checks that it has `send()`, and awaits the native send. `QueueClient.addBulk` uses `sendBatch`, split into calls of at most 100 messages and an estimated 256 KB; a job estimated over 128 KB is rejected before anything is sent, and a partial failure rejects with a `QueueBatchError` listing the accepted job ids.
34
+
35
+ Native delivery needs no mapping: the Worker's `queue()` handler gives batches that no `@QueueConsumer` claims to `QueueModule`, which routes every job by its logical `queue`, so several registered queues can share one physical queue. Every job goes through the module's dispatch policy, so signed dispatch re-enters the signed route and runs its global guards. A message that is not a job envelope, belongs to an unregistered queue, or fails stays unacknowledged, so Cloudflare retries it and then dead-letters it. `registerQueue({ name, consumer })` pins the queue to that physical queue: its jobs are accepted only from it, and it carries only the queues pinned to it. Bootstrap rejects a physical queue claimed by both `@QueueConsumer` and a pinned registration. A raw `@QueueConsumer` owns its physical queue and must not carry jobs of queues registered with `QueueModule`, which `cloudflareQueues()` delivers: when it receives such job envelopes, which reach their `@Processor` only if the raw handler dispatches them itself, the adapter warns once per physical and logical queue unless diagnostics are silent; the raw consumer still receives and settles the batch.
36
+
37
+ **Behavior change:** `cloudflareQueueDriver(bindings, { consumers, producerBindings })` and the `@velajs/cloudflare/queue` subpath are removed, together with the `CloudflareQueueBindings` and `CloudflareQueueDriverOptions` types. Replace `driver: cloudflareQueueDriver({ email: env.EMAIL_QUEUE }, { producerBindings: { email: 'EMAIL_QUEUE' } })` with `driver: cloudflareQueues()` plus `QueueModule.registerQueue({ name: 'email', binding: 'EMAIL_QUEUE' })`, and replace a `consumers: { 'email-production': 'email' }` mapping with `QueueModule.registerQueue({ name: 'email', consumer: 'email-production' })`, or with a plain `registerQueue({ name: 'email' })` when the physical queue needs no pin.
38
+
39
+ **Behavior change:** `consumeQueueBatch` moves to `@velajs/cloudflare/queues`. It accepts every job envelope by default instead of requiring the job's queue to equal the batch's physical queue; its `queue` option is replaced by `queues`, the list of logical queues to accept.
40
+
41
+ **Behavior change:** the driver publishes one `cf:queue:module` entrypoint per application with `{ consumers }` (the pinned physical queues) instead of one `{ queueName, logicalQueue }` entrypoint per mapping, and the `cf:queue:producer` entrypoint kind is removed: registered queues are published as `queue:registration` entrypoints by `QueueModule`.
42
+
43
+ **Behavior change:** a failure on the native `QueueModule` path is reported once to the exception handler instead of once by its processor and again, with the whole batch rejection, by the adapter; a message that is not a job envelope or belongs to an unregistered queue is still reported once, individually.
44
+ - 8a3016c: **Behavior change:** Workers and Durable Objects are built from static roots only. `createCloudflareWorker`, `createCloudflareApp` and `VelaWebSocketDurableObject` take a module class or a `DynamicModule` declared at module scope; `CloudflareRoot` is now `Type | DynamicModule`. The `{ create(env) }` and async `{ create: async (env) => ... }` roots are removed, with no alias, together with the per-(root, environment) resolution cache. Read bindings where each application is built instead: `Module.forRootAsync({ inject: [ENV], useFactory: (env) => ({ ... }) })`, `useFactory` providers that inject `ENV`, or `@InjectEnv()` constructors. These run for each application, so nothing built from one environment is shared with another, and constructing another application or Durable Object instance declares no new classes in the isolate. The per-environment application cache of `createCloudflareWorker` is unchanged.
45
+
46
+ WebSocket upgrade routes authenticate with the gateway's `authenticator`, resolved once per application from the module that declares the gateway, and read an `(env) => origins` allowlist from the Worker's `ENV`. Authentication still completes before the Durable Object id is derived, and client-supplied `x-vela-*` headers are still stripped first. `UpgradeAuthenticator`, `WebSocketUpgradeIdentity` and `WebSocketUpgradeAuthenticationContext` are re-exported from the package root.
47
+
48
+ `WsGatewayRoute` gains an optional `moduleId`: the module that declares the gateway, from which its authenticator resolves.
49
+ - 864735d: **Behavior change:** a WebSocket Durable Object now refuses to start when its module registers the core `WebSocketModule` instead of `CloudflareWebSocketModule`. The core module's `WS_SERVER` broadcasts through its own sync driver, which never reaches the Durable Object's sockets, so `@WebSocketServer()` pushes were silently lost. Import `CloudflareWebSocketModule.forRoot()` in modules a `VelaWebSocketDurableObject` bootstraps.
50
+
51
+ The Worker adapter now warns once per isolate when `LiveModule` runs the default `localLive()` driver in the Worker, whose invalidations never reach subscriptions held by the Durable Object. Pass `driver: () => durableObjectLive({ namespace, gatewayPath })`. The warning respects the `'silent'` diagnostics mode.
52
+
53
+ ### Patch Changes
54
+
55
+ - a01273b: A queue batch that no consumer claims now rejects with guidance: the error names the physical queue, points to `@QueueConsumer(name)` or `QueueModule.forRoot({ driver: cloudflareQueues() })` with a `QueueModule.registerQueue()` for each queue the batch carries, and states that the unacknowledged batch is retried and then dead-lettered by Cloudflare.
56
+ - e4f2008: A Durable Object WebSocket whose `handleConnection` hook broadcasts to its room, for example `server.emit('system', { text: 'joined' })`, is now admitted. The broadcast reached the still-pending socket and rejected it, so every such upgrade failed with "Unable to persist authorized WebSocket state". Broadcasts now skip a socket while its connection hook runs and deliver to the room's active sockets; a pending socket that is not being admitted is still closed with 1008. When a socket is rejected while its hook runs, the error now says so.
57
+ - Updated dependencies [07d1713]
58
+ - Updated dependencies [db18d3a]
59
+ - Updated dependencies [07d1713]
60
+ - Updated dependencies [4071cb7]
61
+ - Updated dependencies [bacaacd]
62
+ - Updated dependencies [a814199]
63
+ - Updated dependencies [1838474]
64
+ - Updated dependencies [8a3016c]
65
+ - Updated dependencies [d803a49]
66
+ - Updated dependencies [b235935]
67
+ - Updated dependencies [08a81c8]
68
+ - Updated dependencies [5b5b81d]
69
+ - Updated dependencies [7daf4fc]
70
+ - Updated dependencies [35e8e0d]
71
+ - Updated dependencies [4420501]
72
+ - Updated dependencies [ff44b6a]
73
+ - Updated dependencies [6d4f0c0]
74
+ - Updated dependencies [e3bda2a]
75
+ - Updated dependencies [bd7e3c9]
76
+ - Updated dependencies [2b74880]
77
+ - Updated dependencies [5ba8635]
78
+ - Updated dependencies [db0c834]
79
+ - Updated dependencies [d6f6a65]
80
+ - Updated dependencies [8a3016c]
81
+ - Updated dependencies [d5a3ec8]
82
+ - Updated dependencies [0f7e8e7]
83
+ - Updated dependencies [41ec70d]
84
+ - Updated dependencies [b265297]
85
+ - Updated dependencies [bdfff47]
86
+ - Updated dependencies [28c7d07]
87
+ - Updated dependencies [8a3016c]
88
+ - Updated dependencies [44efdde]
89
+ - @velajs/vela@1.29.0
90
+ - @velajs/feature-flags@1.29.0
91
+
92
+ ## 1.28.0
93
+
94
+ ### Minor Changes
95
+
96
+ - Continue the module-based Workers APIs on the 1.x release line. Vela permits breaking changes in minor releases and does not retain compatibility layers. Upgrade the framework and integrations together for native queue dispatch, cron scheduling, RPC modules and asynchronous roots; see docs/module-workers.md.
97
+
98
+ ### Patch Changes
99
+
100
+ - Updated dependencies
101
+ - @velajs/feature-flags@1.28.0
102
+ - @velajs/vela@1.28.0
103
+
104
+ ## 3.0.0
105
+
106
+ ### Major Changes
107
+
108
+ - Publish module-based Workers on the unused 3.x stable release line. Earlier experimental 2.0.0 registry versions are immutable and do not contain this release. Upgrade the framework and integrations together; see docs/module-workers.md for queue bootstrap, native delivery, RPC modules and async root migration details.
109
+
110
+ ### Patch Changes
111
+
112
+ - Updated dependencies
113
+ - @velajs/feature-flags@3.0.0
114
+ - @velajs/vela@3.0.0
115
+
116
+ ## 2.0.0
117
+
118
+ ### Major Changes
119
+
120
+ - b99d71a: Compose native Worker applications through modules. QueueModule now initializes transport configuration at bootstrap and publishes driver-owned native routes, removing application-written consumer bridges. Duplicate queue ownership fails at startup. Cloudflare rejects deliveries without a consumer instead of silently accepting them; existing native decorators and envelopes remain supported.
121
+
122
+ Cloudflare roots accept dynamic modules and asynchronous factories. RPC server modules and injectable named clients reuse the existing schema-validated dispatcher. Deployment checks validate module queue mappings, producer declarations and RPC service bindings. A four-worker example and exact-archive runtime proof cover composition, native delivery and scheduling.
123
+
124
+ ### Patch Changes
125
+
126
+ - Updated dependencies [a2d2692]
127
+ - Updated dependencies [b99d71a]
128
+ - @velajs/feature-flags@2.0.0
129
+ - @velajs/vela@2.0.0
130
+
3
131
  ## 1.24.0
4
132
 
5
133
  ### Minor Changes
package/README.md CHANGED
@@ -6,26 +6,27 @@ platform's native types.
6
6
 
7
7
  ## Native environment and application lifetime
8
8
 
9
- Define one typed token for your generated Workers environment. Inject that token
10
- wherever bindings or secrets are needed, including async provider factories.
9
+ The Worker's native environment is the framework `ENV` from `@velajs/vela`.
10
+ `createCloudflareWorker` seeds it for each environment before any provider is
11
+ constructed, so the Worker entry only exports. Inject it wherever bindings or
12
+ secrets are needed, including async provider factories (`inject: [ENV]`).
13
+
14
+ Types come from Wrangler. Run `wrangler types --include-runtime=false` (runtime
15
+ types stay with `@cloudflare/workers-types`) and include the generated
16
+ `worker-configuration.d.ts` in your tsconfig. It declares `Cloudflare.Env` from
17
+ the bindings and variables in your Wrangler file and the secret names in
18
+ `.dev.vars`; this package extends `VelaEnv` with it, so `ENV`, `forRootAsync`
19
+ factories and `registerAs` factories are typed without a hand-written interface.
20
+ Regenerate it whenever the Wrangler file changes.
11
21
 
12
22
  ```ts
13
- import { Controller, Get, Inject, InjectionToken, Module } from '@velajs/vela';
23
+ import { Controller, Get, InjectEnv, Module, type VelaEnv } from '@velajs/vela';
14
24
  import { createCloudflareWorker } from '@velajs/cloudflare';
15
25
 
16
- interface WorkerEnv {
17
- CACHE: KVNamespace;
18
- DB: D1Database;
19
- FILES: R2Bucket;
20
- JOBS: Queue<{ taskId: string }>;
21
- SERVICE_NAME: string;
22
- APP_SECRET: string;
23
- }
24
- export const ENV = new InjectionToken<WorkerEnv>('Worker environment');
25
-
26
26
  @Controller('/status')
27
27
  class StatusController {
28
- constructor(@Inject(ENV) private readonly env: WorkerEnv) {}
28
+ // CACHE is a KVNamespace in worker-configuration.d.ts.
29
+ constructor(@InjectEnv() private readonly env: VelaEnv) {}
29
30
 
30
31
  @Get()
31
32
  async status() {
@@ -36,7 +37,7 @@ class StatusController {
36
37
  @Module({ controllers: [StatusController] })
37
38
  class AppModule {}
38
39
 
39
- export default createCloudflareWorker(AppModule, { envToken: ENV });
40
+ export default createCloudflareWorker(AppModule);
40
41
  ```
41
42
 
42
43
  The worker exposes `fetch`, `queue`, and `scheduled`. Its first event builds an
@@ -45,36 +46,57 @@ environment object share construction. Different environment objects receive
45
46
  separate applications, including separate providers, lifecycle state, and live
46
47
  drivers. A failed construction is evicted and the next event retries.
47
48
 
48
- When module configuration itself needs bindings, pass `{ create: (env) => AppModule }`
49
- instead of a static class. The callback receives the native environment inferred
50
- from `envToken` and runs once per successful environment bootstrap. The same form
51
- works with `VelaWebSocketDurableObject` for authenticated live gateways. See the
49
+ The root is static: a module class, or a `DynamicModule` such as
50
+ `AppModule.forRoot(...)`, declared once at module scope. `createCloudflareWorker`,
51
+ `createCloudflareApp` and `VelaWebSocketDurableObject` all take the same root.
52
+ When module configuration needs bindings, read them where each application is
53
+ built, from its own `ENV`:
54
+
55
+ ```ts
56
+ import { ENV, Module } from '@velajs/vela';
57
+
58
+ @Module({
59
+ imports: [
60
+ DatabaseModule.forRootAsync({
61
+ inject: [ENV],
62
+ useFactory: (env) => ({ database: env.DB }),
63
+ }),
64
+ ],
65
+ })
66
+ class AppModule {}
67
+ ```
68
+
69
+ `forRootAsync` factories, `useFactory` providers, `@InjectEnv()` constructors and
70
+ queue driver factories such as `cloudflareQueues()` run for each application, so
71
+ nothing built from one environment is shared with another. Because the root never
72
+ changes, building another application or Durable Object instance declares no new
73
+ classes in the isolate. See the
52
74
  [complete API starter](../../apps/api-starter/README.md) for D1, Better Auth, CRUD,
53
75
  the generated Hono client, live updates, and Studio inspection in one application.
54
76
 
55
- The cache uses weak object keys: it does not permanently retain replaced
56
- environments or secrets. Providers with request scope still rebuild per HTTP
57
- request or queue/cron dispatch. Do not retain request objects or authentication
58
- state in singleton providers.
77
+ The application cache uses weak object keys, so the cache itself does not keep a
78
+ replaced environment alive. Build secret-bearing values in `forRootAsync`
79
+ factories that inject `ENV` rather than capturing them in module options.
80
+ Providers with request scope still rebuild per HTTP request or queue/cron dispatch.
81
+ Do not retain request objects or authentication state in singleton providers.
59
82
 
60
83
  For explicit construction inside a platform event:
61
84
 
62
85
  ```ts
63
86
  const app = await createCloudflareApp(AppModule, {
64
87
  env,
65
- envToken: ENV,
66
88
  globalPrefix: '/api',
67
89
  middleware: (bindings) => [async (context, next) => {
68
90
  context.header('x-service', bindings.SERVICE_NAME);
69
91
  await next();
70
92
  }],
71
93
  });
72
- const bindings = app.get(ENV); // WorkerEnv, inferred from ENV
94
+ const bindings = app.get(ENV); // VelaEnv
73
95
  return app.fetch(request, env, executionContext);
74
96
  ```
75
97
 
76
- `env` is registered before provider factories and lifecycle hooks. Referencing a
77
- binding inside `middleware(env)` is typed from that same token; request callbacks
98
+ `env` is registered as `ENV` before provider factories and lifecycle hooks.
99
+ Bindings inside `middleware(env)` are typed as `VelaEnv` too; request callbacks
78
100
  capture the native environment without retyping Hono's context. Referencing a
79
101
  binding is safe during construction; platform I/O must still happen inside a
80
102
  Workers event or Durable Object context. An explicitly built application rejects
@@ -82,20 +104,114 @@ requests or events carrying another environment object, including calls through
82
104
  the underlying Hono app. Internal `ctx.run` reentry retains the application's
83
105
  environment.
84
106
 
85
- `cloudflareAdapter({ env, envToken })` provides the same bootstrap and request
86
- contract when composing `VelaFactory.create` directly.
107
+ `cloudflareAdapter({ env })` provides the same bootstrap and request contract
108
+ when composing `VelaFactory.create` directly. `createCloudflareWorker` and
109
+ `createCloudflareApp` accept `adapters: RuntimeAdapter[]`, composed after the
110
+ Cloudflare adapter for each application, so the Worker entry needs no
111
+ hand-written per-environment cache for them.
87
112
 
88
- ## Managed queue and cron work
113
+ Because `ENV` carries every binding, variable and secret, framework features
114
+ read their secrets from it without extra wiring: a string `URL_SIGNING_SECRET`
115
+ signs URLs and invocations when no explicit secret is configured, and Studio
116
+ reads `VELA_STUDIO_TOKEN` and its `VELA_STUDIO_*_EDITABLE` flags. Set them with
117
+ `wrangler secret put`. Values come from outside the program, so validate each
118
+ value your own code reads before relying on it.
119
+
120
+ ## Module-based queues, cron and RPC
121
+
122
+ `QueueModule` from `@velajs/vela/queue` is the Workers queue API. Configure the
123
+ driver once in the root module and register each queue where it is used:
124
+
125
+ ```ts
126
+ import { Injectable, Module } from '@velajs/vela';
127
+ import {
128
+ InjectQueue,
129
+ Process,
130
+ Processor,
131
+ QueueModule,
132
+ defineQueueJob,
133
+ type QueueClient,
134
+ type QueueJob,
135
+ } from '@velajs/vela/queue';
136
+ import { cloudflareQueues } from '@velajs/cloudflare/queues';
137
+ import { z } from 'zod';
138
+
139
+ const welcome = defineQueueJob('welcome', z.object({ userId: z.string() }));
89
140
 
90
- Each matching queue/cron handler receives its original event and environment,
91
- plus a context whose `waitUntil(promise)` delegates to the platform and retains
92
- that handler's DI scope until the promise settles. Class/method guards,
93
- interceptors and filters resolve asynchronously from the handler's declaring
94
- module. The execution context exposes that same child via `getContainer()` and
95
- its owner via `getModuleId()`; `REQUEST_CONTEXT` remains HTTP-only.
141
+ @Injectable()
142
+ class Signup {
143
+ constructor(@InjectQueue('email') private readonly email: QueueClient) {}
144
+ invite(userId: string) {
145
+ return this.email.add(welcome, { userId });
146
+ }
147
+ }
96
148
 
97
- Inject `EXECUTION_LIFETIME` from `@velajs/vela` to schedule deferred callbacks
98
- with `lifetime.defer(work)` or register already-started work with
149
+ @Processor('email')
150
+ class EmailProcessor {
151
+ @Process(welcome)
152
+ send(job: QueueJob<{ userId: string }>) {}
153
+ }
154
+
155
+ @Module({
156
+ imports: [QueueModule.registerQueue({ name: 'email', binding: 'EMAIL_QUEUE' })],
157
+ providers: [Signup, EmailProcessor],
158
+ })
159
+ class EmailModule {}
160
+
161
+ @Module({ imports: [QueueModule.forRoot({ driver: cloudflareQueues() }), EmailModule] })
162
+ class AppModule {}
163
+ ```
164
+
165
+ `binding` names a Wrangler `queues.producers[].binding`. The driver reads it
166
+ from the application's `ENV` when a job is added and awaits the native send.
167
+ `addBulk` uses `sendBatch`, split into calls of at most 100 messages and an
168
+ estimated 256 KB, and rejects a job estimated over 128 KB before sending
169
+ anything. A partial failure rejects with a `QueueBatchError` whose `accepted`
170
+ lists the job ids already sent.
171
+
172
+ The Worker's `queue()` handler gives each batch to the `@QueueConsumer` handlers
173
+ of its physical queue. Batches no `@QueueConsumer` claims go to `QueueModule`,
174
+ which routes every job by its logical queue, so several registered queues may
175
+ share one physical queue. Each job runs through the module's dispatch policy,
176
+ including signed dispatch and its global guards. A message is acknowledged
177
+ after its processors succeed; a message that is not a job envelope, belongs to
178
+ an unregistered queue, or fails stays unacknowledged, so Cloudflare retries it
179
+ and then dead-letters it. `registerQueue({ name, consumer: 'email-production' })`
180
+ pins the queue to that physical queue: its jobs are accepted only from it, and
181
+ it carries only the queues pinned to it. A physical queue cannot be both a
182
+ `@QueueConsumer` queue and a pinned consumer. A `@QueueConsumer` owns its
183
+ physical queue and must not carry jobs of registered queues: those reach their
184
+ `@Processor` only if the raw handler dispatches them itself, so the adapter
185
+ warns once when it sees them. Registered queues are delivered by
186
+ `cloudflareQueues()`. `dispatchQueueJob` is for tests and for transports other
187
+ than Cloudflare Queues; it applies the module's dispatch policy, signed dispatch
188
+ included.
189
+
190
+ Use `ScheduleModule.forRoot()` and `@Cron()` for native scheduled work. The
191
+ [queue guide](../../docs/queues.md) and [module guide](../../docs/module-workers.md)
192
+ cover producer-only and consumer-only Workers, RPC modules and deployment
193
+ checks. `@QueueConsumer` remains available for raw batches.
194
+
195
+ ## Managed queue and cron work
196
+
197
+ Each matching `@QueueConsumer` handler receives its batch and environment, plus
198
+ a context whose `waitUntil(promise)` delegates to the platform and retains that
199
+ handler's DI scope until the promise settles. Class/method guards, interceptors
200
+ and filters resolve asynchronously from the handler's declaring module. The
201
+ execution context exposes that same child via `getContainer()` and its owner via
202
+ `getModuleId()`; `REQUEST_CONTEXT` remains HTTP-only.
203
+
204
+ A `@Cron` job receives only its `CronInvocation`, with no environment or
205
+ context argument, and runs no guards, interceptors or filters: the adapter warns
206
+ once (fails bootstrap in `diagnostics: 'throw'`) when a job declares
207
+ `@UseGuards`, `@UseInterceptors` or `@UseFilters`, and a job that declares
208
+ guards is refused on every trigger instead of running unguarded. Use signed
209
+ `ScheduleModule` dispatch to run a job through a route's request pipeline, and
210
+ inject `ENV`, `CLOUDFLARE_SCHEDULED_EVENT` and `EXECUTION_LIFETIME` for what the
211
+ native handler arguments used to carry.
212
+
213
+ In both, inject `EXECUTION_LIFETIME` from `@velajs/vela` to schedule deferred
214
+ callbacks with `lifetime.defer(work)` or register already-started work with
99
215
  `lifetime.waitUntil(promise)`. The handler, managed work and asynchronous provider
100
216
  disposal finish before queue/cron dispatch returns. Unclaimed failures reject
101
217
  for the platform to observe; they are not silently converted into success. When
@@ -112,7 +228,7 @@ Bindings retain their full native API and generic parameters. There are no
112
228
  binding-name wrappers to initialize or cast.
113
229
 
114
230
  ```ts
115
- import { defineProvider, InjectionToken, Module } from '@velajs/vela';
231
+ import { defineProvider, ENV, InjectionToken, Module } from '@velajs/vela';
116
232
 
117
233
  const TASK_QUEUE = new InjectionToken<Queue<{ taskId: string }>>('task queue');
118
234
 
@@ -126,27 +242,29 @@ const TASK_QUEUE = new InjectionToken<Queue<{ taskId: string }>>('task queue');
126
242
  class JobsModule {}
127
243
  ```
128
244
 
129
- Every `useFactory` strategy declares its dependencies with `inject`, including
130
- `inject: []` for factories without dependencies. This also applies to
131
- `lazyProvider` and `forRootAsync` factory options.
245
+ A `useFactory` strategy declares its dependencies with `inject`; a factory
246
+ without parameters may omit it. This also applies to `lazyProvider` and
247
+ `forRootAsync` factory options.
132
248
 
133
249
  Use native `env.DB`, `env.CACHE`, `env.FILES`, `env.JOBS`, `env.AI`,
134
- `env.VECTORIZE`, or `env.HYPERDRIVE` directly. `@Env()` remains available for HTTP
135
- handler parameters; typed token injection also works outside HTTP.
250
+ `env.VECTORIZE`, or `env.HYPERDRIVE` directly. Inject `ENV` in constructors
251
+ (`@InjectEnv()`) and factories (`inject: [ENV]`); it works the same in HTTP,
252
+ queue, cron and Durable Object code.
136
253
 
137
254
  ## Queues and cron
138
255
 
139
256
  ```ts
140
- import { Inject, Injectable } from '@velajs/vela';
141
- import { QueueConsumer, Scheduled } from '@velajs/cloudflare';
257
+ import { Cron, InjectEnv, Injectable, type CronInvocation, type VelaEnv } from '@velajs/vela';
258
+ import { QueueConsumer } from '@velajs/cloudflare';
142
259
 
143
260
  @Injectable()
144
261
  class Jobs {
145
- constructor(@Inject(ENV) private readonly env: WorkerEnv) {}
262
+ constructor(@InjectEnv() private readonly env: VelaEnv) {}
146
263
 
147
- @Scheduled('0 * * * *')
148
- async refresh() {
149
- await this.env.CACHE.put('last-refresh', new Date().toISOString());
264
+ // Declare the same string under Wrangler `triggers.crons`.
265
+ @Cron('0 * * * *', { dialect: 'cloudflare' })
266
+ async refresh(tick: CronInvocation) {
267
+ await this.env.CACHE.put('last-refresh', new Date(tick.scheduledTime).toISOString());
150
268
  }
151
269
 
152
270
  @QueueConsumer('jobs')
@@ -158,17 +276,24 @@ class Jobs {
158
276
  }
159
277
  ```
160
278
 
161
- Core `@Cron()` also runs on Workers scheduled triggers. Consumers use fresh
162
- request scopes and their declared guards, interceptors, and filters. Unclaimed
163
- errors propagate to the platform for retry. Cold queue and cron events have the
164
- same native bindings and live invalidation capabilities as HTTP.
279
+ A cron trigger runs every core `@Cron()` job whose expression is exactly the
280
+ trigger string. Jobs receive only their `CronInvocation`, as on Node, in a fresh
281
+ request scope and without guards, interceptors or filters; inject
282
+ `CLOUDFLARE_SCHEDULED_EVENT` for the trigger's bound `noRetry()` and
283
+ `EXECUTION_LIFETIME` for background work. A job run outside a trigger (Studio's
284
+ run-now) receives a synthetic event whose `noRetry()` does nothing. Signed `ScheduleModule` dispatch runs
285
+ the signed route with its global guards. Queue consumers use fresh request
286
+ scopes and their declared guards, interceptors, and filters. Unclaimed errors
287
+ propagate to the platform for retry. Cold queue and cron events have the same
288
+ native bindings and live invalidation capabilities as HTTP. See
289
+ [scheduling](../../docs/scheduling.md).
165
290
 
166
291
  ## WebSockets, live queries, and Durable Objects
167
292
 
168
293
  Use the native Durable Object entrypoint only in your Worker entry file:
169
294
 
170
295
  ```ts
171
- import { InjectionToken, Module } from '@velajs/vela';
296
+ import { ENV, Module } from '@velajs/vela';
172
297
  import { LiveModule } from '@velajs/vela/live';
173
298
  import {
174
299
  CloudflareWebSocketModule,
@@ -178,14 +303,12 @@ import {
178
303
  } from '@velajs/cloudflare';
179
304
  import { VelaWebSocketDurableObject } from '@velajs/cloudflare/durable-objects';
180
305
 
181
- interface RoomEnv { ROOMS: DurableObjectNamespace<Room> }
182
- const ROOM_ENV = new InjectionToken<RoomEnv>('room environment');
183
-
184
306
  @Module({
185
307
  imports: [
186
308
  CloudflareWebSocketModule.forRoot(),
187
309
  LiveModule.forRootAsync({
188
- inject: [ROOM_ENV],
310
+ // ROOMS is typed DurableObjectNamespace<Room> by `wrangler types`.
311
+ inject: [ENV],
189
312
  useFactory: (env) => ({
190
313
  driver: () => durableObjectLive({
191
314
  namespace: env.ROOMS,
@@ -200,12 +323,19 @@ const ROOM_ENV = new InjectionToken<RoomEnv>('room environment');
200
323
  })
201
324
  class RoomModule {}
202
325
 
203
- export class Room extends VelaWebSocketDurableObject(RoomModule, { envToken: ROOM_ENV }) {}
204
- export default createCloudflareWorker(RoomModule, { envToken: ROOM_ENV });
326
+ export class Room extends VelaWebSocketDurableObject(RoomModule) {}
327
+ export default createCloudflareWorker(RoomModule);
205
328
  ```
206
329
 
207
330
  Declare gateways with `@WebSocketGateway({ path, roomParam, binding, ... })` and
208
- configure origins and upgrade authentication for your application. Upgrade
331
+ configure origins and upgrade authentication for your application.
332
+ `authenticator` names an `UpgradeAuthenticator` class that the Worker resolves
333
+ once per application from the module declaring the gateway, and
334
+ `allowedOrigins` may read the environment: `(env) => [env.APP_ORIGIN]`.
335
+ `BetterAuthUpgradeAuthenticator` (`@velajs/better-auth`) and
336
+ `CloudflareAccessUpgradeAuthenticator` (`@velajs/cloudflare-access/vela`) are
337
+ ready-made authenticators. A gateway without an authenticator refuses every
338
+ upgrade. Authentication finishes before the Durable Object id is derived. Upgrade
209
339
  routing consumes the core trusted request identity, checks conflicts with the
210
340
  upgrade credential, and forwards issuer, subject, tenant, and expiry to the DO.
211
341
  Client-supplied internal identity headers are stripped before authorization.
@@ -1,24 +1,25 @@
1
- import { l as VelaDoPitrRpc, p as CloudflareRoot, t as VelaNonceDurableObject } from "./nonce.durable-object-Df3_42Sy.js";
2
- import { InjectionToken } from "@velajs/vela";
1
+ import "./vela-env-DFvyoNT3.js";
2
+ import { l as VelaDoPitrRpc, p as CloudflareRoot, t as VelaNonceDurableObject } from "./nonce.durable-object-Df4CZi-0.js";
3
+ import { VelaEnv } from "@velajs/vela";
3
4
  import { BroadcastCommand } from "@velajs/vela/websocket";
4
5
  import { CommitStamp, InvalidationCommand, LiveInspection } from "@velajs/vela/live";
5
6
  import { DurableObject } from "cloudflare:workers";
6
7
  //#region src/websocket/websocket.durable-object.d.ts
7
8
  /**
8
9
  * Base class for the WebSocket Durable Object. The user exports a named subclass
9
- * (matching their `wrangler.toml` `class_name`) built from their `AppModule`:
10
+ * (matching their `wrangler.toml` `class_name`) built from their `AppModule`, or
11
+ * from a `DynamicModule` declared at module scope:
10
12
  *
11
13
  * ```ts
12
- * export class ChatRoom extends VelaWebSocketDurableObject(AppModule, { envToken: ENV }) {}
14
+ * export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
13
15
  * ```
14
16
  *
15
17
  * It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
16
18
  * bridge DO hibernation) and forwards every event into the runtime-agnostic
17
- * `WsDispatcher` via {@link DoWebSocketHost}.
19
+ * `WsDispatcher` via {@link DoWebSocketHost}. The DO's `env` is the
20
+ * application's ENV, as in the Worker.
18
21
  */
19
- export declare function VelaWebSocketDurableObject<T extends object>(rootModule: CloudflareRoot<NoInfer<T>>, options: {
20
- envToken: InjectionToken<T>;
21
- }): new (ctx: DurableObjectState, env: T) => DurableObject<T> & VelaDoPitrRpc & {
22
+ export declare function VelaWebSocketDurableObject(rootModule: CloudflareRoot): new (ctx: DurableObjectState, env: VelaEnv) => DurableObject<VelaEnv> & VelaDoPitrRpc & {
22
23
  broadcast(cmd: BroadcastCommand): Promise<void>;
23
24
  invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined>;
24
25
  inspectLive(): Promise<LiveInspection>;