@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 +128 -0
- package/README.md +192 -62
- package/dist/durable-objects.d.ts +9 -8
- package/dist/durable-objects.js +30 -18
- package/dist/durable-objects.js.map +1 -1
- package/dist/index.d.ts +141 -108
- package/dist/index.js +334 -172
- package/dist/index.js.map +1 -1
- package/dist/{nonce-validation-Bcqf3FvY.js → nonce-validation-Dy8z05A9.js} +140 -131
- package/dist/nonce-validation-Dy8z05A9.js.map +1 -0
- package/dist/{nonce.durable-object-Df3_42Sy.d.ts → nonce.durable-object-Df4CZi-0.d.ts} +9 -6
- package/dist/queues.d.ts +49 -0
- package/dist/queues.js +215 -0
- package/dist/queues.js.map +1 -0
- package/dist/vela-env-DFvyoNT3.d.ts +10 -0
- package/package.json +11 -10
- package/dist/nonce-validation-Bcqf3FvY.js.map +0 -1
- package/dist/queue.d.ts +0 -26
- package/dist/queue.js +0 -41
- package/dist/queue.js.map +0 -1
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
|
-
|
|
10
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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); //
|
|
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.
|
|
77
|
-
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
`
|
|
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.
|
|
135
|
-
|
|
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 {
|
|
141
|
-
import { QueueConsumer
|
|
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(@
|
|
262
|
+
constructor(@InjectEnv() private readonly env: VelaEnv) {}
|
|
146
263
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
204
|
-
export default createCloudflareWorker(RoomModule
|
|
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.
|
|
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
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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>;
|