@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +8 -8
  3. package/dist/cloudflare-application.d.ts +25 -5
  4. package/dist/cloudflare-application.js +79 -35
  5. package/dist/cloudflare-factory.d.ts +15 -1
  6. package/dist/cloudflare-factory.js +45 -24
  7. package/dist/decorators/queue-consumer.js +9 -1
  8. package/dist/decorators/scheduled.js +8 -1
  9. package/dist/index.d.ts +8 -1
  10. package/dist/index.js +8 -1
  11. package/dist/modules/create-binding-module.js +19 -22
  12. package/dist/services/kv-cache.store.d.ts +19 -0
  13. package/dist/services/kv-cache.store.js +58 -0
  14. package/dist/storage/index.d.ts +7 -0
  15. package/dist/storage/index.js +6 -0
  16. package/dist/storage/r2-storage.driver.d.ts +19 -0
  17. package/dist/storage/r2-storage.driver.js +61 -0
  18. package/dist/storage/storage-manager.service.d.ts +16 -0
  19. package/dist/storage/storage-manager.service.js +56 -0
  20. package/dist/storage/storage.controller.d.ts +16 -0
  21. package/dist/storage/storage.controller.js +88 -0
  22. package/dist/storage/storage.module.d.ts +7 -0
  23. package/dist/storage/storage.module.js +37 -0
  24. package/dist/storage/storage.service.d.ts +20 -0
  25. package/dist/storage/storage.service.js +78 -0
  26. package/dist/storage/storage.tokens.d.ts +3 -0
  27. package/dist/storage/storage.tokens.js +2 -0
  28. package/dist/storage/storage.types.d.ts +17 -0
  29. package/dist/storage/storage.types.js +1 -0
  30. package/dist/websocket/broadcast.d.ts +15 -0
  31. package/dist/websocket/broadcast.js +26 -0
  32. package/dist/websocket/cf-room-registry.d.ts +23 -0
  33. package/dist/websocket/cf-room-registry.js +65 -0
  34. package/dist/websocket/cf-ws-client.d.ts +28 -0
  35. package/dist/websocket/cf-ws-client.js +83 -0
  36. package/dist/websocket/cloudflare-websocket.module.d.ts +11 -0
  37. package/dist/websocket/cloudflare-websocket.module.js +27 -0
  38. package/dist/websocket/do-bootstrap.d.ts +21 -0
  39. package/dist/websocket/do-bootstrap.js +50 -0
  40. package/dist/websocket/do-state.d.ts +25 -0
  41. package/dist/websocket/do-state.js +4 -0
  42. package/dist/websocket/do-websocket-host.d.ts +28 -0
  43. package/dist/websocket/do-websocket-host.js +69 -0
  44. package/dist/websocket/index.d.ts +13 -0
  45. package/dist/websocket/index.js +13 -0
  46. package/dist/websocket/room-id.d.ts +6 -0
  47. package/dist/websocket/room-id.js +12 -0
  48. package/dist/websocket/websocket-routing.d.ts +14 -0
  49. package/dist/websocket/websocket-routing.js +45 -0
  50. package/dist/websocket/websocket.durable-object.d.ts +15 -0
  51. package/dist/websocket/websocket.durable-object.js +72 -0
  52. package/dist/websocket/ws-server-holder.d.ts +16 -0
  53. package/dist/websocket/ws-server-holder.js +34 -0
  54. 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 { CloudflareFactory, KVModule, KVService, D1Module, D1Service } from '@velajs/cloudflare';
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 CloudflareFactory.create(AppModule);
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
- ## CloudflareFactory
330
+ ## createCloudflareApp
331
331
 
332
- Use `CloudflareFactory.create()` 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.
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 { CloudflareFactory } from '@velajs/cloudflare';
335
+ import { createCloudflareApp } from '@velajs/cloudflare';
336
336
 
337
- const app = await CloudflareFactory.create(AppModule);
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 CloudflareFactory.create(AppModule);
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
- `CloudflareFactory` handles this by:
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 scheduledHandlers;
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
- /** @internal — scans instances for @Scheduled, @Cron, and @QueueConsumer metadata */
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()` handlers.
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, getMetadata } from "@velajs/vela";
2
- import { getScheduledMetadata } from "./decorators/scheduled.js";
3
- import { getQueueConsumerMetadata } from "./decorators/queue-consumer.js";
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
- scheduledHandlers = [];
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
- /** @internal — scans instances for @Scheduled, @Cron, and @QueueConsumer metadata */ scanInstances(instances) {
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
- for (const meta of getScheduledMetadata(instance)){
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()` handlers.
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 matching = this.scheduledHandlers.filter((h)=>h.cron === event.cron);
122
- await Promise.all(matching.map((h)=>invoke(h.instance, h.methodName, [
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 matching = this.queueConsumers.filter((h)=>h.queueName === batch.queue);
133
- await Promise.all(matching.map((h)=>invoke(h.instance, h.methodName, [
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 token of container.getTokens()){
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
- */ export async function createCloudflareApp(rootModule, options = {}) {
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
- const bindingInit = async (c, next)=>{
56
- if (!initialized) {
57
- initialized = true;
58
- const env = c.env ?? {};
59
- for (const ref of refs){
60
- // EnvRef holds the whole env; every other ref holds one binding.
61
- if (ref instanceof EnvRef) ref._initialize(env);
62
- else ref._initialize(env[ref.bindingName]);
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
- // IMPORTANT: keep the binding-init middleware FIRST so user middleware
68
- // can safely read binding refs / `c.env` derivatives on the first request.
86
+ }
87
+ export async function createCloudflareApp(rootModule, options = {}) {
69
88
  const velaApp = await VelaFactory.create(rootModule, {
70
89
  globalPrefix: options.globalPrefix,
71
- middleware: [
72
- bindingInit,
73
- ...options.middleware ?? []
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
- // the canonical forRoot() shape for them.
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
- forRoot ({ binding }) {
27
- const ref = new BindingRef(binding);
28
- return {
29
- module: moduleClass,
30
- key: binding,
31
- providers: [
32
- {
33
- provide: opts.bindingRefToken,
34
- useValue: ref
35
- },
36
- opts.serviceClass
37
- ],
38
- exports: [
39
- opts.serviceClass,
40
- opts.bindingRefToken
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";