@velajs/cloudflare 1.5.0 → 1.8.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 +17 -0
- package/README.md +8 -8
- package/dist/cloudflare-application.d.ts +25 -5
- package/dist/cloudflare-application.js +79 -35
- package/dist/cloudflare-factory.d.ts +15 -1
- package/dist/cloudflare-factory.js +45 -24
- package/dist/decorators/queue-consumer.js +9 -1
- package/dist/decorators/scheduled.js +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -1
- package/dist/modules/create-binding-module.js +19 -22
- package/dist/services/kv-cache.store.d.ts +19 -0
- package/dist/services/kv-cache.store.js +58 -0
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/r2-storage.driver.d.ts +19 -0
- package/dist/storage/r2-storage.driver.js +61 -0
- package/dist/storage/storage-manager.service.d.ts +16 -0
- package/dist/storage/storage-manager.service.js +56 -0
- package/dist/storage/storage.controller.d.ts +16 -0
- package/dist/storage/storage.controller.js +88 -0
- package/dist/storage/storage.module.d.ts +7 -0
- package/dist/storage/storage.module.js +37 -0
- package/dist/storage/storage.service.d.ts +20 -0
- package/dist/storage/storage.service.js +78 -0
- package/dist/storage/storage.tokens.d.ts +3 -0
- package/dist/storage/storage.tokens.js +2 -0
- package/dist/storage/storage.types.d.ts +17 -0
- package/dist/storage/storage.types.js +1 -0
- package/dist/websocket/broadcast.d.ts +15 -0
- package/dist/websocket/broadcast.js +26 -0
- package/dist/websocket/cf-room-registry.d.ts +23 -0
- package/dist/websocket/cf-room-registry.js +65 -0
- package/dist/websocket/cf-ws-client.d.ts +28 -0
- package/dist/websocket/cf-ws-client.js +83 -0
- package/dist/websocket/cloudflare-websocket.module.d.ts +11 -0
- package/dist/websocket/cloudflare-websocket.module.js +27 -0
- package/dist/websocket/do-bootstrap.d.ts +21 -0
- package/dist/websocket/do-bootstrap.js +50 -0
- package/dist/websocket/do-state.d.ts +25 -0
- package/dist/websocket/do-state.js +4 -0
- package/dist/websocket/do-websocket-host.d.ts +28 -0
- package/dist/websocket/do-websocket-host.js +69 -0
- package/dist/websocket/index.d.ts +13 -0
- package/dist/websocket/index.js +13 -0
- package/dist/websocket/room-id.d.ts +6 -0
- package/dist/websocket/room-id.js +12 -0
- package/dist/websocket/websocket-routing.d.ts +14 -0
- package/dist/websocket/websocket-routing.js +45 -0
- package/dist/websocket/websocket.durable-object.d.ts +15 -0
- package/dist/websocket/websocket.durable-object.js +72 -0
- package/dist/websocket/ws-server-holder.d.ts +16 -0
- package/dist/websocket/ws-server-holder.js +34 -0
- package/package.json +8 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 (2026-07-04)
|
|
4
|
+
|
|
5
|
+
- `cloudflareAdapter()` exported (createCloudflareApp composes vela RuntimeAdapter); `@QueueConsumer`/`@Scheduled` declare open entrypoint kinds; queue/scheduled dispatch runs per-event in a request scope through PipelineRunner (consumer-scoped guards/interceptors/filters; request-scoped deps rebuild per batch); DO WebSocket reads `app.entrypoints`. Requires `@velajs/vela >=1.11.0`.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## 1.6.0 (2026-07-01)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Multi-disk `StorageModule` over R2** (`StorageService.put/get/delete/exists/url`, per-disk templated roots, bucket-by-name via `EnvService`, `R2StorageDriver`) + a signature-gated `StorageController` presign-proxy.
|
|
13
|
+
- **`KVCacheStore`** implementing vela's `AsyncCacheStore` — pair with `TieredCacheStore` for a memory→KV cache.
|
|
14
|
+
- WebSocket transport for vela's WebSocket gateways (Durable Object backed).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Multiple same-type bindings** (e.g. two `KVModule.forRoot` with different bindings) now all initialize — `collectBindingRefs` enumerates every binding ref across module buckets instead of resolving each token once.
|
|
19
|
+
|
|
3
20
|
## 0.2.0 (2026-04-28)
|
|
4
21
|
|
|
5
22
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ class HyperdriveService { readonly binding: Hyperdrive; }
|
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
import { Controller, Get, Module, Injectable, Param } from '@velajs/vela';
|
|
37
|
-
import {
|
|
37
|
+
import { createCloudflareApp, KVModule, KVService, D1Module, D1Service } from '@velajs/cloudflare';
|
|
38
38
|
|
|
39
39
|
@Injectable()
|
|
40
40
|
class UserService {
|
|
@@ -73,7 +73,7 @@ class UserController {
|
|
|
73
73
|
})
|
|
74
74
|
class AppModule {}
|
|
75
75
|
|
|
76
|
-
export default await
|
|
76
|
+
export default await createCloudflareApp(AppModule);
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
The `binding` string matches the binding name in your `wrangler.toml`:
|
|
@@ -327,14 +327,14 @@ class EmailWorker {
|
|
|
327
327
|
}
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
##
|
|
330
|
+
## createCloudflareApp
|
|
331
331
|
|
|
332
|
-
Use `
|
|
332
|
+
Use `createCloudflareApp()` instead of `VelaFactory.create()` for Cloudflare apps. It sets up a one-time middleware that captures `c.env` on the first request and initializes all binding modules.
|
|
333
333
|
|
|
334
334
|
```ts
|
|
335
|
-
import {
|
|
335
|
+
import { createCloudflareApp } from '@velajs/cloudflare';
|
|
336
336
|
|
|
337
|
-
const app = await
|
|
337
|
+
const app = await createCloudflareApp(AppModule);
|
|
338
338
|
export default app;
|
|
339
339
|
```
|
|
340
340
|
|
|
@@ -343,7 +343,7 @@ export default app;
|
|
|
343
343
|
To use scheduled triggers and queue consumers, export the handlers explicitly:
|
|
344
344
|
|
|
345
345
|
```ts
|
|
346
|
-
const app = await
|
|
346
|
+
const app = await createCloudflareApp(AppModule);
|
|
347
347
|
|
|
348
348
|
export default {
|
|
349
349
|
fetch: app.fetch,
|
|
@@ -371,7 +371,7 @@ const rawHD = hdService.binding; // Hyperdrive
|
|
|
371
371
|
|
|
372
372
|
Cloudflare Workers only provide bindings (`env.DB`, `env.MY_KV`, etc.) at request time via the `env` parameter. They are stable across requests within an isolate.
|
|
373
373
|
|
|
374
|
-
`
|
|
374
|
+
`createCloudflareApp` handles this by:
|
|
375
375
|
|
|
376
376
|
1. Each `XModule.forRoot()` creates a `BindingRef` (mutable holder) and registers it in the DI container
|
|
377
377
|
2. Services are constructed at boot time with the `BindingRef` — no binding access yet
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { type VelaApplication } from '@velajs/vela';
|
|
3
|
+
import { type WsGatewayRoute } from './websocket/websocket-routing';
|
|
3
4
|
import type { CloudflareEnv } from './types';
|
|
4
5
|
/**
|
|
5
6
|
* Options accepted by {@link CloudflareApplication.mountOpenApi}.
|
|
@@ -40,8 +41,7 @@ export type MountOpenApiOptions = Parameters<VelaApplication['mountOpenApi']>[0]
|
|
|
40
41
|
*/
|
|
41
42
|
export declare class CloudflareApplication {
|
|
42
43
|
private app;
|
|
43
|
-
private
|
|
44
|
-
private queueConsumers;
|
|
44
|
+
private wsGatewayRoutes;
|
|
45
45
|
constructor(app: VelaApplication);
|
|
46
46
|
get fetch(): Hono['fetch'];
|
|
47
47
|
getHonoApp(): Hono;
|
|
@@ -79,11 +79,20 @@ export declare class CloudflareApplication {
|
|
|
79
79
|
* ```
|
|
80
80
|
*/
|
|
81
81
|
mountOpenApi(options: MountOpenApiOptions): this;
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* @internal — scans instances for `@WebSocketGateway({ path, binding })`
|
|
84
|
+
* upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
|
|
85
|
+
* come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
|
|
86
|
+
* kinds) at dispatch time.
|
|
87
|
+
*/
|
|
83
88
|
scanInstances(instances: unknown[]): void;
|
|
89
|
+
/** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */
|
|
90
|
+
getWsGatewayRoutes(): WsGatewayRoute[];
|
|
84
91
|
/**
|
|
85
92
|
* Handle Cloudflare scheduled (cron) events.
|
|
86
|
-
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
93
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
94
|
+
* handlers read from `app.entrypoints`; each handler runs inside a fresh
|
|
95
|
+
* request-scoped child (request-scoped providers rebuild per tick).
|
|
87
96
|
*/
|
|
88
97
|
scheduled(event: {
|
|
89
98
|
cron: string;
|
|
@@ -91,9 +100,20 @@ export declare class CloudflareApplication {
|
|
|
91
100
|
}, env: CloudflareEnv, ctx: {
|
|
92
101
|
waitUntil: (promise: Promise<unknown>) => void;
|
|
93
102
|
}): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Run one entrypoint handler inside a fresh request scope, through the
|
|
105
|
+
* shared guard → interceptor pipeline (components declared with
|
|
106
|
+
* `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
|
|
107
|
+
* method). HTTP-global components deliberately do NOT apply — an HTTP auth
|
|
108
|
+
* guard has no business rejecting a queue batch. Unclaimed errors rethrow
|
|
109
|
+
* so the platform's retry semantics stay intact.
|
|
110
|
+
*/
|
|
111
|
+
private dispatchEntrypoint;
|
|
94
112
|
/**
|
|
95
113
|
* Handle Cloudflare Queue consumer events.
|
|
96
|
-
* Matches the batch queue name to `@QueueConsumer()` handlers
|
|
114
|
+
* Matches the batch queue name to `@QueueConsumer()` handlers read from
|
|
115
|
+
* `app.entrypoints`; each batch is processed inside a fresh request-scoped
|
|
116
|
+
* child (request-scoped providers rebuild per batch — no boot-time captives).
|
|
97
117
|
*/
|
|
98
118
|
queue(batch: {
|
|
99
119
|
queue: string;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { CRON_METADATA,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { CRON_METADATA, PipelineRunner, buildEntrypointExecutionContext, registerEntrypointKind, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela";
|
|
2
|
+
import { ComponentManager } from "@velajs/vela/internal";
|
|
3
|
+
import { collectWsGatewayRoutes } from "./websocket/websocket-routing.js";
|
|
4
|
+
// vela's own @Cron jobs run via the same Workers cron trigger — declare an
|
|
5
|
+
// entrypoint kind over vela's metadata key (the open-kind system makes
|
|
6
|
+
// cross-package declarations first-class).
|
|
7
|
+
registerEntrypointKind({
|
|
8
|
+
kind: 'cf:vela-cron',
|
|
9
|
+
metaKey: CRON_METADATA,
|
|
10
|
+
level: 'method'
|
|
11
|
+
});
|
|
4
12
|
function invoke(instance, methodName, args) {
|
|
5
13
|
const method = instance[methodName];
|
|
6
14
|
if (typeof method !== 'function') {
|
|
@@ -38,8 +46,7 @@ function invoke(instance, methodName, args) {
|
|
|
38
46
|
* ```
|
|
39
47
|
*/ export class CloudflareApplication {
|
|
40
48
|
app;
|
|
41
|
-
|
|
42
|
-
queueConsumers = [];
|
|
49
|
+
wsGatewayRoutes = [];
|
|
43
50
|
constructor(app){
|
|
44
51
|
this.app = app;
|
|
45
52
|
}
|
|
@@ -86,51 +93,88 @@ function invoke(instance, methodName, args) {
|
|
|
86
93
|
this.app.mountOpenApi(options);
|
|
87
94
|
return this;
|
|
88
95
|
}
|
|
89
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* @internal — scans instances for `@WebSocketGateway({ path, binding })`
|
|
98
|
+
* upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
|
|
99
|
+
* come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
|
|
100
|
+
* kinds) at dispatch time.
|
|
101
|
+
*/ scanInstances(instances) {
|
|
90
102
|
for (const instance of instances){
|
|
91
103
|
if (!instance || typeof instance !== 'object') continue;
|
|
92
|
-
|
|
93
|
-
this.scheduledHandlers.push({
|
|
94
|
-
instance,
|
|
95
|
-
methodName: meta.methodName,
|
|
96
|
-
cron: meta.cron
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
// vela's @Cron jobs run via the same Workers cron trigger.
|
|
100
|
-
const cronMeta = getMetadata(CRON_METADATA, instance.constructor) ?? [];
|
|
101
|
-
for (const meta of cronMeta){
|
|
102
|
-
this.scheduledHandlers.push({
|
|
103
|
-
instance,
|
|
104
|
-
methodName: meta.methodName,
|
|
105
|
-
cron: meta.expression
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
for (const meta of getQueueConsumerMetadata(instance)){
|
|
109
|
-
this.queueConsumers.push({
|
|
110
|
-
instance,
|
|
111
|
-
methodName: meta.methodName,
|
|
112
|
-
queueName: meta.queueName
|
|
113
|
-
});
|
|
114
|
-
}
|
|
104
|
+
this.wsGatewayRoutes.push(...collectWsGatewayRoutes(instance));
|
|
115
105
|
}
|
|
116
106
|
}
|
|
107
|
+
/** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */ getWsGatewayRoutes() {
|
|
108
|
+
return this.wsGatewayRoutes;
|
|
109
|
+
}
|
|
117
110
|
/**
|
|
118
111
|
* Handle Cloudflare scheduled (cron) events.
|
|
119
|
-
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
112
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
113
|
+
* handlers read from `app.entrypoints`; each handler runs inside a fresh
|
|
114
|
+
* request-scoped child (request-scoped providers rebuild per tick).
|
|
120
115
|
*/ async scheduled(event, env, ctx) {
|
|
121
|
-
const
|
|
122
|
-
|
|
116
|
+
const handlers = [
|
|
117
|
+
...this.app.entrypoints.ofKind('cf:scheduled').map((ep)=>({
|
|
118
|
+
ep,
|
|
119
|
+
cron: ep.meta.cron
|
|
120
|
+
})),
|
|
121
|
+
...this.app.entrypoints.ofKind('cf:vela-cron').map((ep)=>({
|
|
122
|
+
ep,
|
|
123
|
+
cron: ep.meta.expression
|
|
124
|
+
}))
|
|
125
|
+
].filter((h)=>h.cron === event.cron);
|
|
126
|
+
await Promise.all(handlers.map(({ ep })=>this.dispatchEntrypoint(ep, [
|
|
123
127
|
event,
|
|
124
128
|
env,
|
|
125
129
|
ctx
|
|
126
130
|
])));
|
|
127
131
|
}
|
|
128
132
|
/**
|
|
133
|
+
* Run one entrypoint handler inside a fresh request scope, through the
|
|
134
|
+
* shared guard → interceptor pipeline (components declared with
|
|
135
|
+
* `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
|
|
136
|
+
* method). HTTP-global components deliberately do NOT apply — an HTTP auth
|
|
137
|
+
* guard has no business rejecting a queue batch. Unclaimed errors rethrow
|
|
138
|
+
* so the platform's retry semantics stay intact.
|
|
139
|
+
*/ async dispatchEntrypoint(ep, args) {
|
|
140
|
+
const targetClass = ep.token;
|
|
141
|
+
const methodName = String(ep.methodName);
|
|
142
|
+
const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, args[0]);
|
|
143
|
+
await runInEntrypointScope(this.app.getContainer(), async (scope)=>{
|
|
144
|
+
const instance = scope.resolve(ep.token);
|
|
145
|
+
const guards = ComponentManager.resolveGuards(ComponentManager.getScopedComponents('guard', targetClass, methodName), scope);
|
|
146
|
+
const interceptors = ComponentManager.resolveInterceptors(ComponentManager.getScopedComponents('interceptor', targetClass, methodName), scope);
|
|
147
|
+
// Closest-first, mirroring the HTTP/WS dispatchers.
|
|
148
|
+
const filters = ComponentManager.resolveFilters([
|
|
149
|
+
...ComponentManager.getScopedComponents('filter', targetClass, methodName)
|
|
150
|
+
].reverse(), scope);
|
|
151
|
+
try {
|
|
152
|
+
await PipelineRunner.run({
|
|
153
|
+
context,
|
|
154
|
+
guards,
|
|
155
|
+
interceptors,
|
|
156
|
+
resolveArgs: async ()=>args,
|
|
157
|
+
invoke: async (resolved)=>invoke(instance, methodName, resolved)
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
for (const filter of filters){
|
|
161
|
+
if (shouldFilterCatch(filter, error)) {
|
|
162
|
+
await filter.catch(error, context);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
129
171
|
* Handle Cloudflare Queue consumer events.
|
|
130
|
-
* Matches the batch queue name to `@QueueConsumer()` handlers
|
|
172
|
+
* Matches the batch queue name to `@QueueConsumer()` handlers read from
|
|
173
|
+
* `app.entrypoints`; each batch is processed inside a fresh request-scoped
|
|
174
|
+
* child (request-scoped providers rebuild per batch — no boot-time captives).
|
|
131
175
|
*/ async queue(batch, env, ctx) {
|
|
132
|
-
const
|
|
133
|
-
await Promise.all(
|
|
176
|
+
const handlers = this.app.entrypoints.ofKind('cf:queue').filter((ep)=>ep.meta.queueName === batch.queue);
|
|
177
|
+
await Promise.all(handlers.map((ep)=>this.dispatchEntrypoint(ep, [
|
|
134
178
|
batch,
|
|
135
179
|
env,
|
|
136
180
|
ctx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from 'hono';
|
|
2
|
-
import type { Type } from '@velajs/vela';
|
|
2
|
+
import type { RuntimeAdapter, Type } from '@velajs/vela';
|
|
3
3
|
import { CloudflareApplication } from './cloudflare-application';
|
|
4
4
|
/**
|
|
5
5
|
* Options for {@link createCloudflareApp}.
|
|
@@ -71,4 +71,18 @@ export interface CreateCloudflareAppOptions {
|
|
|
71
71
|
* });
|
|
72
72
|
* ```
|
|
73
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* The Cloudflare platform binding as a vela {@link RuntimeAdapter}: a one-time
|
|
76
|
+
* request middleware captures `c.env` on the first request and initializes
|
|
77
|
+
* every `BindingRef`/`EnvRef` (collected at `onBootstrap`, before any request
|
|
78
|
+
* can arrive). Adapter `requestMiddleware` is prepended to the global chain by
|
|
79
|
+
* `VelaFactory.create`, so user middleware can safely read binding refs.
|
|
80
|
+
*
|
|
81
|
+
* Exposed so consumers composing `VelaFactory.create` themselves can opt in:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const app = await VelaFactory.create(AppModule, { adapters: [cloudflareAdapter()] });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function cloudflareAdapter(): RuntimeAdapter;
|
|
74
88
|
export declare function createCloudflareApp(rootModule: Type, options?: CreateCloudflareAppOptions): Promise<CloudflareApplication>;
|
|
@@ -2,19 +2,18 @@ import { VelaFactory } from "@velajs/vela";
|
|
|
2
2
|
import { BindingRef } from "./binding-ref.js";
|
|
3
3
|
import { CloudflareApplication } from "./cloudflare-application.js";
|
|
4
4
|
import { EnvRef } from "./env-ref.js";
|
|
5
|
+
import { registerWebSocketRoutes } from "./websocket/websocket-routing.js";
|
|
5
6
|
// Walks every provider registered in the container and pulls out the
|
|
6
7
|
// BindingRef instances. Replaces the old module-level `bindingsRegistry`
|
|
7
8
|
// global so two CloudflareApplication instances in one process don't
|
|
8
9
|
// share state.
|
|
9
10
|
function collectBindingRefs(container) {
|
|
11
|
+
// Enumerate per-instance useValue providers across ALL module buckets. Two
|
|
12
|
+
// same-type binding modules (e.g. KVModule.forRoot for CACHE and SESSIONS)
|
|
13
|
+
// share one token but live in distinct buckets — resolving the token would
|
|
14
|
+
// return only the first, leaving the others uninitialized.
|
|
10
15
|
const refs = [];
|
|
11
|
-
for (const
|
|
12
|
-
let value;
|
|
13
|
-
try {
|
|
14
|
-
value = container.resolve(token);
|
|
15
|
-
} catch {
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
16
|
+
for (const value of container.getUseValues()){
|
|
18
17
|
if (value instanceof BindingRef) refs.push(value);
|
|
19
18
|
}
|
|
20
19
|
return refs;
|
|
@@ -49,32 +48,54 @@ function collectBindingRefs(container) {
|
|
|
49
48
|
* ],
|
|
50
49
|
* });
|
|
51
50
|
* ```
|
|
52
|
-
*/
|
|
51
|
+
*/ /**
|
|
52
|
+
* The Cloudflare platform binding as a vela {@link RuntimeAdapter}: a one-time
|
|
53
|
+
* request middleware captures `c.env` on the first request and initializes
|
|
54
|
+
* every `BindingRef`/`EnvRef` (collected at `onBootstrap`, before any request
|
|
55
|
+
* can arrive). Adapter `requestMiddleware` is prepended to the global chain by
|
|
56
|
+
* `VelaFactory.create`, so user middleware can safely read binding refs.
|
|
57
|
+
*
|
|
58
|
+
* Exposed so consumers composing `VelaFactory.create` themselves can opt in:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* const app = await VelaFactory.create(AppModule, { adapters: [cloudflareAdapter()] });
|
|
62
|
+
* ```
|
|
63
|
+
*/ export function cloudflareAdapter() {
|
|
53
64
|
let initialized = false;
|
|
54
|
-
let refs;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
let refs = [];
|
|
66
|
+
return {
|
|
67
|
+
name: 'cloudflare',
|
|
68
|
+
requestMiddleware: [
|
|
69
|
+
async (c, next)=>{
|
|
70
|
+
if (!initialized) {
|
|
71
|
+
initialized = true;
|
|
72
|
+
const env = c.env ?? {};
|
|
73
|
+
for (const ref of refs){
|
|
74
|
+
// EnvRef holds the whole env; every other ref holds one binding.
|
|
75
|
+
if (ref instanceof EnvRef) ref._initialize(env);
|
|
76
|
+
else ref._initialize(env[ref.bindingName]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await next();
|
|
63
80
|
}
|
|
81
|
+
],
|
|
82
|
+
onBootstrap: ({ container })=>{
|
|
83
|
+
refs = collectBindingRefs(container);
|
|
64
84
|
}
|
|
65
|
-
await next();
|
|
66
85
|
};
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
}
|
|
87
|
+
export async function createCloudflareApp(rootModule, options = {}) {
|
|
69
88
|
const velaApp = await VelaFactory.create(rootModule, {
|
|
70
89
|
globalPrefix: options.globalPrefix,
|
|
71
|
-
middleware:
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
middleware: options.middleware,
|
|
91
|
+
adapters: [
|
|
92
|
+
cloudflareAdapter()
|
|
74
93
|
]
|
|
75
94
|
});
|
|
76
|
-
refs = collectBindingRefs(velaApp.getContainer());
|
|
77
95
|
const cfApp = new CloudflareApplication(velaApp);
|
|
78
96
|
cfApp.scanInstances(velaApp.getInstances());
|
|
97
|
+
// Register WebSocket upgrade routes for any @WebSocketGateway({ path, binding }).
|
|
98
|
+
// Each forwards the upgrade to the room's Durable Object (which owns the socket).
|
|
99
|
+
registerWebSocketRoutes(cfApp.getHonoApp(), cfApp.getWsGatewayRoutes());
|
|
79
100
|
return cfApp;
|
|
80
101
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { defineMetadata, getMetadata } from "@velajs/vela";
|
|
1
|
+
import { defineMetadata, getMetadata, registerEntrypointKind } from "@velajs/vela";
|
|
2
2
|
const QUEUE_CONSUMER_METADATA_KEY = 'cloudflare:queue-consumer';
|
|
3
|
+
// Open entrypoint kind: any adapter can enumerate queue consumers via
|
|
4
|
+
// `app.entrypoints.ofKind('cf:queue')` — declared here, next to the decorator,
|
|
5
|
+
// with zero vela-core involvement.
|
|
6
|
+
registerEntrypointKind({
|
|
7
|
+
kind: 'cf:queue',
|
|
8
|
+
metaKey: QUEUE_CONSUMER_METADATA_KEY,
|
|
9
|
+
level: 'method'
|
|
10
|
+
});
|
|
3
11
|
/**
|
|
4
12
|
* Marks a method as a queue consumer handler.
|
|
5
13
|
*
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { defineMetadata, getMetadata } from "@velajs/vela";
|
|
1
|
+
import { defineMetadata, getMetadata, registerEntrypointKind } from "@velajs/vela";
|
|
2
2
|
const SCHEDULED_METADATA_KEY = 'cloudflare:scheduled';
|
|
3
|
+
// Open entrypoint kind: adapters enumerate cron handlers via
|
|
4
|
+
// `app.entrypoints.ofKind('cf:scheduled')` — declared next to the decorator.
|
|
5
|
+
registerEntrypointKind({
|
|
6
|
+
kind: 'cf:scheduled',
|
|
7
|
+
metaKey: SCHEDULED_METADATA_KEY,
|
|
8
|
+
level: 'method'
|
|
9
|
+
});
|
|
3
10
|
/**
|
|
4
11
|
* Marks a method as a scheduled (cron) handler.
|
|
5
12
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createCloudflareApp } from './cloudflare-factory';
|
|
1
|
+
export { cloudflareAdapter, createCloudflareApp } from './cloudflare-factory';
|
|
2
2
|
export type { CreateCloudflareAppOptions } from './cloudflare-factory';
|
|
3
3
|
export { CloudflareApplication } from './cloudflare-application';
|
|
4
4
|
export type { MountOpenApiOptions } from './cloudflare-application';
|
|
@@ -11,7 +11,10 @@ export { AIModule } from './modules/ai.module';
|
|
|
11
11
|
export { VectorizeModule } from './modules/vectorize.module';
|
|
12
12
|
export { HyperdriveModule } from './modules/hyperdrive.module';
|
|
13
13
|
export { EnvModule } from './modules/env.module';
|
|
14
|
+
export { StorageModule, StorageService, StorageManagerService, StorageController, R2StorageDriver, STORAGE_OPTIONS, } from './storage/index';
|
|
15
|
+
export type { StorageModuleOptions, DiskConfig, PresignedUrlConfig } from './storage/index';
|
|
14
16
|
export { KVService } from './services/kv.service';
|
|
17
|
+
export { KVCacheStore } from './services/kv-cache.store';
|
|
15
18
|
export { D1Service } from './services/d1.service';
|
|
16
19
|
export { R2Service } from './services/r2.service';
|
|
17
20
|
export { QueueService } from './services/queue.service';
|
|
@@ -23,6 +26,10 @@ export { EnvService } from './services/env.service';
|
|
|
23
26
|
export { Env } from './decorators/env';
|
|
24
27
|
export { Scheduled } from './decorators/scheduled';
|
|
25
28
|
export { QueueConsumer } from './decorators/queue-consumer';
|
|
29
|
+
export { VelaWebSocketDurableObject, CloudflareWebSocketModule, broadcastToRoom, } from './websocket/index';
|
|
30
|
+
export type { WsGatewayRoute } from './websocket/index';
|
|
31
|
+
export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException, } from '@velajs/vela/websocket';
|
|
32
|
+
export type { WsClient, WsServer, WsResponse, WsMessage, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from '@velajs/vela/websocket';
|
|
26
33
|
export type { CloudflareEnv, ScheduledRegistration, QueueRegistration } from './types';
|
|
27
34
|
export type { ScheduledMetadata } from './decorators/scheduled';
|
|
28
35
|
export type { QueueConsumerMetadata } from './decorators/queue-consumer';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Factory & Application
|
|
2
|
-
export { createCloudflareApp } from "./cloudflare-factory.js";
|
|
2
|
+
export { cloudflareAdapter, createCloudflareApp } from "./cloudflare-factory.js";
|
|
3
3
|
export { CloudflareApplication } from "./cloudflare-application.js";
|
|
4
4
|
// Modules
|
|
5
5
|
export { KVModule } from "./modules/kv.module.js";
|
|
@@ -11,8 +11,11 @@ export { AIModule } from "./modules/ai.module.js";
|
|
|
11
11
|
export { VectorizeModule } from "./modules/vectorize.module.js";
|
|
12
12
|
export { HyperdriveModule } from "./modules/hyperdrive.module.js";
|
|
13
13
|
export { EnvModule } from "./modules/env.module.js";
|
|
14
|
+
// Storage (multi-disk over R2 + presign proxy)
|
|
15
|
+
export { StorageModule, StorageService, StorageManagerService, StorageController, R2StorageDriver, STORAGE_OPTIONS } from "./storage/index.js";
|
|
14
16
|
// Services
|
|
15
17
|
export { KVService } from "./services/kv.service.js";
|
|
18
|
+
export { KVCacheStore } from "./services/kv-cache.store.js";
|
|
16
19
|
export { D1Service } from "./services/d1.service.js";
|
|
17
20
|
export { R2Service } from "./services/r2.service.js";
|
|
18
21
|
export { QueueService } from "./services/queue.service.js";
|
|
@@ -25,3 +28,7 @@ export { EnvService } from "./services/env.service.js";
|
|
|
25
28
|
export { Env } from "./decorators/env.js";
|
|
26
29
|
export { Scheduled } from "./decorators/scheduled.js";
|
|
27
30
|
export { QueueConsumer } from "./decorators/queue-consumer.js";
|
|
31
|
+
// WebSocket (Durable Object transport for the Vela WebSocketModule)
|
|
32
|
+
export { VelaWebSocketDurableObject, CloudflareWebSocketModule, broadcastToRoom } from "./websocket/index.js";
|
|
33
|
+
// Re-export the core gateway API so a Cloudflare app can import it from one place.
|
|
34
|
+
export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException } from "@velajs/vela/websocket";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { defineConfigurableModule } from "@velajs/vela";
|
|
1
2
|
import { BindingRef } from "../binding-ref.js";
|
|
2
3
|
// Single factory for every Cloudflare binding module (KV, D1, R2, ...).
|
|
3
|
-
// Each binding module is just a bag of (token, service); this generates
|
|
4
|
-
//
|
|
4
|
+
// Each binding module is just a bag of (token, service); this generates the
|
|
5
|
+
// canonical forRoot() shape via vela's `defineConfigurableModule` engine.
|
|
5
6
|
//
|
|
6
7
|
// Identity model (post vela audit #2):
|
|
7
8
|
// - One real module class is declared per binding TYPE (KV, D1, R2, ...).
|
|
@@ -22,24 +23,20 @@ export function createBindingModule(opts) {
|
|
|
22
23
|
[className]: class {
|
|
23
24
|
}
|
|
24
25
|
}[className];
|
|
25
|
-
return {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
]
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
};
|
|
26
|
+
return defineConfigurableModule({
|
|
27
|
+
module: moduleClass,
|
|
28
|
+
methodName: 'forRoot',
|
|
29
|
+
keyFrom: ({ binding })=>binding,
|
|
30
|
+
providers: ({ binding })=>[
|
|
31
|
+
{
|
|
32
|
+
provide: opts.bindingRefToken,
|
|
33
|
+
useValue: new BindingRef(binding)
|
|
34
|
+
},
|
|
35
|
+
opts.serviceClass
|
|
36
|
+
],
|
|
37
|
+
exports: [
|
|
38
|
+
opts.serviceClass,
|
|
39
|
+
opts.bindingRefToken
|
|
40
|
+
]
|
|
41
|
+
});
|
|
45
42
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AsyncCacheStore } from '@velajs/vela';
|
|
2
|
+
import { KVService } from './kv.service';
|
|
3
|
+
/**
|
|
4
|
+
* Cloudflare KV-backed {@link CacheStore}. Values are JSON-encoded. Intended as
|
|
5
|
+
* the slow tier under a `TieredCacheStore` (memory L1 → KV L2), but usable
|
|
6
|
+
* standalone as `CacheModule.forRootAsync({ inject: [KVService], useFactory: (kv) => ({ store: new KVCacheStore(kv) }) })`.
|
|
7
|
+
*
|
|
8
|
+
* Note: Cloudflare KV requires `expirationTtl >= 60s`, so sub-minute TTLs are
|
|
9
|
+
* clamped up. Keep short TTLs on the memory tier; use KV for longer-lived entries.
|
|
10
|
+
*/
|
|
11
|
+
export declare class KVCacheStore implements AsyncCacheStore {
|
|
12
|
+
private readonly kv;
|
|
13
|
+
constructor(kv: KVService);
|
|
14
|
+
private get ns();
|
|
15
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
16
|
+
set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
|
|
17
|
+
del(key: string): Promise<void>;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Inject, Injectable } from "@velajs/vela";
|
|
16
|
+
import { KVService } from "./kv.service.js";
|
|
17
|
+
export class KVCacheStore {
|
|
18
|
+
kv;
|
|
19
|
+
constructor(kv){
|
|
20
|
+
this.kv = kv;
|
|
21
|
+
}
|
|
22
|
+
get ns() {
|
|
23
|
+
return this.kv.namespace;
|
|
24
|
+
}
|
|
25
|
+
async get(key) {
|
|
26
|
+
const value = await this.ns.get(key, 'json');
|
|
27
|
+
return value === null ? undefined : value;
|
|
28
|
+
}
|
|
29
|
+
async set(key, value, ttl) {
|
|
30
|
+
// KV enforces a 60s minimum expirationTtl; clamp up. Omit for no-TTL.
|
|
31
|
+
const options = ttl !== undefined ? {
|
|
32
|
+
expirationTtl: Math.max(60, Math.floor(ttl))
|
|
33
|
+
} : undefined;
|
|
34
|
+
await this.ns.put(key, JSON.stringify(value), options);
|
|
35
|
+
}
|
|
36
|
+
async del(key) {
|
|
37
|
+
await this.ns.delete(key);
|
|
38
|
+
}
|
|
39
|
+
async clear() {
|
|
40
|
+
// KV has no native clear — page through and delete. Best-effort.
|
|
41
|
+
let cursor;
|
|
42
|
+
do {
|
|
43
|
+
const list = await this.ns.list(cursor ? {
|
|
44
|
+
cursor
|
|
45
|
+
} : undefined);
|
|
46
|
+
await Promise.all(list.keys.map((entry)=>this.ns.delete(entry.name)));
|
|
47
|
+
cursor = list.list_complete ? undefined : list.cursor;
|
|
48
|
+
}while (cursor)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
KVCacheStore = _ts_decorate([
|
|
52
|
+
Injectable(),
|
|
53
|
+
_ts_param(0, Inject(KVService)),
|
|
54
|
+
_ts_metadata("design:type", Function),
|
|
55
|
+
_ts_metadata("design:paramtypes", [
|
|
56
|
+
typeof KVService === "undefined" ? Object : KVService
|
|
57
|
+
])
|
|
58
|
+
], KVCacheStore);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { StorageModule } from './storage.module';
|
|
2
|
+
export { StorageService } from './storage.service';
|
|
3
|
+
export { StorageManagerService } from './storage-manager.service';
|
|
4
|
+
export { StorageController } from './storage.controller';
|
|
5
|
+
export { R2StorageDriver } from './r2-storage.driver';
|
|
6
|
+
export { STORAGE_OPTIONS } from './storage.tokens';
|
|
7
|
+
export type { StorageModuleOptions, DiskConfig, PresignedUrlConfig } from './storage.types';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { StorageModule } from "./storage.module.js";
|
|
2
|
+
export { StorageService } from "./storage.service.js";
|
|
3
|
+
export { StorageManagerService } from "./storage-manager.service.js";
|
|
4
|
+
export { StorageController } from "./storage.controller.js";
|
|
5
|
+
export { R2StorageDriver } from "./r2-storage.driver.js";
|
|
6
|
+
export { STORAGE_OPTIONS } from "./storage.tokens.js";
|