@velajs/cloudflare 0.1.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 (52) hide show
  1. package/README.md +372 -0
  2. package/dist/binding-ref.d.ts +15 -0
  3. package/dist/binding-ref.js +23 -0
  4. package/dist/cloudflare-application.d.ts +51 -0
  5. package/dist/cloudflare-application.js +77 -0
  6. package/dist/cloudflare-factory.d.ts +18 -0
  7. package/dist/cloudflare-factory.js +42 -0
  8. package/dist/decorators/env.d.ts +16 -0
  9. package/dist/decorators/env.js +22 -0
  10. package/dist/decorators/queue-consumer.d.ts +23 -0
  11. package/dist/decorators/queue-consumer.js +32 -0
  12. package/dist/decorators/scheduled.d.ts +20 -0
  13. package/dist/decorators/scheduled.js +29 -0
  14. package/dist/index.d.ts +27 -0
  15. package/dist/index.js +29 -0
  16. package/dist/modules/ai.module.d.ts +6 -0
  17. package/dist/modules/ai.module.js +31 -0
  18. package/dist/modules/d1.module.d.ts +6 -0
  19. package/dist/modules/d1.module.js +31 -0
  20. package/dist/modules/durable-object.module.d.ts +6 -0
  21. package/dist/modules/durable-object.module.js +31 -0
  22. package/dist/modules/hyperdrive.module.d.ts +6 -0
  23. package/dist/modules/hyperdrive.module.js +31 -0
  24. package/dist/modules/kv.module.d.ts +6 -0
  25. package/dist/modules/kv.module.js +31 -0
  26. package/dist/modules/queue.module.d.ts +6 -0
  27. package/dist/modules/queue.module.js +31 -0
  28. package/dist/modules/r2.module.d.ts +6 -0
  29. package/dist/modules/r2.module.js +31 -0
  30. package/dist/modules/vectorize.module.d.ts +6 -0
  31. package/dist/modules/vectorize.module.js +31 -0
  32. package/dist/services/ai.service.d.ts +16 -0
  33. package/dist/services/ai.service.js +36 -0
  34. package/dist/services/d1.service.d.ts +22 -0
  35. package/dist/services/d1.service.js +47 -0
  36. package/dist/services/durable-object.service.d.ts +24 -0
  37. package/dist/services/durable-object.service.js +48 -0
  38. package/dist/services/hyperdrive.service.d.ts +26 -0
  39. package/dist/services/hyperdrive.service.js +51 -0
  40. package/dist/services/kv.service.d.ts +24 -0
  41. package/dist/services/kv.service.js +48 -0
  42. package/dist/services/queue.service.d.ts +20 -0
  43. package/dist/services/queue.service.js +39 -0
  44. package/dist/services/r2.service.d.ts +18 -0
  45. package/dist/services/r2.service.js +48 -0
  46. package/dist/services/vectorize.service.d.ts +26 -0
  47. package/dist/services/vectorize.service.js +51 -0
  48. package/dist/tokens.d.ts +17 -0
  49. package/dist/tokens.js +18 -0
  50. package/dist/types.d.ts +13 -0
  51. package/dist/types.js +1 -0
  52. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # @velajs/cloudflare
2
+
3
+ Cloudflare Workers integration for the [Vela](https://github.com/velajs/vela) framework. NestJS-style per-service modules for KV, D1, R2, Queues, Durable Objects, Workers AI, Vectorize, and Hyperdrive.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @velajs/cloudflare @velajs/vela hono
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { Controller, Get, Module, Injectable, Param } from '@velajs/vela';
15
+ import { CloudflareFactory, KVModule, D1Module } from '@velajs/cloudflare';
16
+ import type { KVService, D1Service } from '@velajs/cloudflare';
17
+
18
+ @Injectable()
19
+ class UserService {
20
+ constructor(
21
+ private kv: KVService,
22
+ private d1: D1Service,
23
+ ) {}
24
+
25
+ async findById(id: string) {
26
+ const cached = await this.kv.get(`user:${id}`);
27
+ if (cached) return JSON.parse(cached as string);
28
+
29
+ const user = await this.d1.prepare('SELECT * FROM users WHERE id = ?').bind(id).first();
30
+ if (user) await this.kv.put(`user:${id}`, JSON.stringify(user));
31
+ return user;
32
+ }
33
+ }
34
+
35
+ @Controller('/users')
36
+ class UserController {
37
+ constructor(private users: UserService) {}
38
+
39
+ @Get('/:id')
40
+ async getUser(@Param('id') id: string) {
41
+ return this.users.findById(id);
42
+ }
43
+ }
44
+
45
+ @Module({
46
+ imports: [
47
+ KVModule.forRoot({ binding: 'CACHE' }),
48
+ D1Module.forRoot({ binding: 'DB' }),
49
+ ],
50
+ providers: [UserService],
51
+ controllers: [UserController],
52
+ })
53
+ class AppModule {}
54
+
55
+ export default await CloudflareFactory.create(AppModule);
56
+ ```
57
+
58
+ The `binding` string matches the binding name in your `wrangler.toml`:
59
+
60
+ ```toml
61
+ [[kv_namespaces]]
62
+ binding = "CACHE"
63
+ id = "abc123"
64
+
65
+ [[d1_databases]]
66
+ binding = "DB"
67
+ database_id = "def456"
68
+ ```
69
+
70
+ ## Modules
71
+
72
+ Each module follows the same pattern: `XModule.forRoot({ binding: 'NAME' })` returns a dynamic module that provides a service wrapping the Cloudflare binding.
73
+
74
+ | Module | Service | Cloudflare Binding |
75
+ |--------|---------|-------------------|
76
+ | `KVModule` | `KVService` | KV Namespace |
77
+ | `D1Module` | `D1Service` | D1 Database |
78
+ | `R2Module` | `R2Service` | R2 Bucket |
79
+ | `QueueModule` | `QueueService` | Queue (producer) |
80
+ | `DurableObjectModule` | `DurableObjectService` | Durable Object Namespace |
81
+ | `AIModule` | `AIService` | Workers AI |
82
+ | `VectorizeModule` | `VectorizeService` | Vectorize Index |
83
+ | `HyperdriveModule` | `HyperdriveService` | Hyperdrive |
84
+
85
+ ### KVModule
86
+
87
+ ```ts
88
+ import { KVModule } from '@velajs/cloudflare';
89
+ import type { KVService } from '@velajs/cloudflare';
90
+
91
+ @Module({ imports: [KVModule.forRoot({ binding: 'MY_KV' })] })
92
+ class AppModule {}
93
+
94
+ @Injectable()
95
+ class CacheService {
96
+ constructor(private kv: KVService) {}
97
+
98
+ async get(key: string) { return this.kv.get(key); }
99
+ async set(key: string, value: string) { return this.kv.put(key, value); }
100
+ async remove(key: string) { return this.kv.delete(key); }
101
+ async keys() { return this.kv.list(); }
102
+ }
103
+ ```
104
+
105
+ ### D1Module
106
+
107
+ ```ts
108
+ import { D1Module } from '@velajs/cloudflare';
109
+ import type { D1Service } from '@velajs/cloudflare';
110
+
111
+ @Module({ imports: [D1Module.forRoot({ binding: 'DB' })] })
112
+ class AppModule {}
113
+
114
+ @Injectable()
115
+ class PostService {
116
+ constructor(private d1: D1Service) {}
117
+
118
+ async findAll() {
119
+ return this.d1.prepare('SELECT * FROM posts').all();
120
+ }
121
+
122
+ async create(title: string) {
123
+ return this.d1.prepare('INSERT INTO posts (title) VALUES (?)').bind(title).run();
124
+ }
125
+ }
126
+ ```
127
+
128
+ ### R2Module
129
+
130
+ ```ts
131
+ import { R2Module } from '@velajs/cloudflare';
132
+ import type { R2Service } from '@velajs/cloudflare';
133
+
134
+ @Module({ imports: [R2Module.forRoot({ binding: 'ASSETS' })] })
135
+ class AppModule {}
136
+
137
+ @Injectable()
138
+ class StorageService {
139
+ constructor(private r2: R2Service) {}
140
+
141
+ async upload(key: string, data: string) { return this.r2.put(key, data); }
142
+ async download(key: string) { return this.r2.get(key); }
143
+ async remove(key: string) { return this.r2.delete(key); }
144
+ }
145
+ ```
146
+
147
+ ### QueueModule
148
+
149
+ ```ts
150
+ import { QueueModule } from '@velajs/cloudflare';
151
+ import type { QueueService } from '@velajs/cloudflare';
152
+
153
+ @Module({ imports: [QueueModule.forRoot({ binding: 'EMAIL_QUEUE' })] })
154
+ class AppModule {}
155
+
156
+ @Injectable()
157
+ class NotificationService {
158
+ constructor(private queue: QueueService) {}
159
+
160
+ async sendEmail(to: string, subject: string) {
161
+ await this.queue.send({ to, subject });
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### DurableObjectModule
167
+
168
+ ```ts
169
+ import { DurableObjectModule } from '@velajs/cloudflare';
170
+ import type { DurableObjectService } from '@velajs/cloudflare';
171
+
172
+ @Module({ imports: [DurableObjectModule.forRoot({ binding: 'COUNTER' })] })
173
+ class AppModule {}
174
+
175
+ @Injectable()
176
+ class CounterService {
177
+ constructor(private doNs: DurableObjectService) {}
178
+
179
+ async increment(name: string) {
180
+ const id = this.doNs.idFromName(name);
181
+ const stub = this.doNs.get(id);
182
+ return (stub as any).fetch('/increment');
183
+ }
184
+ }
185
+ ```
186
+
187
+ ### AIModule
188
+
189
+ ```ts
190
+ import { AIModule } from '@velajs/cloudflare';
191
+ import type { AIService } from '@velajs/cloudflare';
192
+
193
+ @Module({ imports: [AIModule.forRoot({ binding: 'AI' })] })
194
+ class AppModule {}
195
+
196
+ @Injectable()
197
+ class ChatService {
198
+ constructor(private ai: AIService) {}
199
+
200
+ async chat(prompt: string) {
201
+ return this.ai.run('@cf/meta/llama-3.1-8b-instruct', {
202
+ messages: [{ role: 'user', content: prompt }],
203
+ });
204
+ }
205
+ }
206
+ ```
207
+
208
+ ### VectorizeModule
209
+
210
+ ```ts
211
+ import { VectorizeModule } from '@velajs/cloudflare';
212
+ import type { VectorizeService } from '@velajs/cloudflare';
213
+
214
+ @Module({ imports: [VectorizeModule.forRoot({ binding: 'EMBEDDINGS' })] })
215
+ class AppModule {}
216
+
217
+ @Injectable()
218
+ class SearchService {
219
+ constructor(private vectorize: VectorizeService) {}
220
+
221
+ async search(vector: number[]) {
222
+ return this.vectorize.query(vector, { topK: 10 });
223
+ }
224
+
225
+ async addVectors(vectors: unknown[]) {
226
+ return this.vectorize.upsert(vectors);
227
+ }
228
+ }
229
+ ```
230
+
231
+ ### HyperdriveModule
232
+
233
+ ```ts
234
+ import { HyperdriveModule } from '@velajs/cloudflare';
235
+ import type { HyperdriveService } from '@velajs/cloudflare';
236
+
237
+ @Module({ imports: [HyperdriveModule.forRoot({ binding: 'POSTGRES' })] })
238
+ class AppModule {}
239
+
240
+ @Injectable()
241
+ class DbService {
242
+ constructor(private hd: HyperdriveService) {}
243
+
244
+ getConnectionString() {
245
+ return this.hd.connectionString;
246
+ }
247
+
248
+ getConfig() {
249
+ return { host: this.hd.host, port: this.hd.port, database: this.hd.database };
250
+ }
251
+ }
252
+ ```
253
+
254
+ ## Decorators
255
+
256
+ ### @Env()
257
+
258
+ Parameter decorator for direct access to Cloudflare bindings in controllers. Useful as an escape hatch when you don't need a full module.
259
+
260
+ ```ts
261
+ import { Env } from '@velajs/cloudflare';
262
+
263
+ @Controller('/debug')
264
+ class DebugController {
265
+ @Get('/env')
266
+ handle(@Env() env: Record<string, unknown>) {
267
+ return { bindings: Object.keys(env) };
268
+ }
269
+
270
+ @Get('/kv')
271
+ handleKV(@Env('MY_KV') kv: KVNamespace) {
272
+ return kv.get('some-key');
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### @Scheduled()
278
+
279
+ Method decorator for cron trigger handlers.
280
+
281
+ ```ts
282
+ import { Scheduled } from '@velajs/cloudflare';
283
+
284
+ @Injectable()
285
+ class CleanupService {
286
+ @Scheduled('0 * * * *') // every hour
287
+ async hourlyCleanup() {
288
+ console.log('Running hourly cleanup');
289
+ }
290
+
291
+ @Scheduled('0 0 * * *') // every day at midnight
292
+ async dailyReport() {
293
+ console.log('Generating daily report');
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### @QueueConsumer()
299
+
300
+ Method decorator for queue consumer handlers.
301
+
302
+ ```ts
303
+ import { QueueConsumer } from '@velajs/cloudflare';
304
+
305
+ @Injectable()
306
+ class EmailWorker {
307
+ @QueueConsumer('email-queue')
308
+ async process(batch: MessageBatch) {
309
+ for (const msg of batch.messages) {
310
+ console.log('Sending email:', msg.body);
311
+ msg.ack();
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ ## CloudflareFactory
318
+
319
+ 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.
320
+
321
+ ```ts
322
+ import { CloudflareFactory } from '@velajs/cloudflare';
323
+
324
+ const app = await CloudflareFactory.create(AppModule);
325
+ export default app;
326
+ ```
327
+
328
+ ## Full Worker Export
329
+
330
+ To use scheduled triggers and queue consumers, export the handlers explicitly:
331
+
332
+ ```ts
333
+ const app = await CloudflareFactory.create(AppModule);
334
+
335
+ export default {
336
+ fetch: app.fetch,
337
+ scheduled: app.scheduled.bind(app),
338
+ queue: app.queue.bind(app),
339
+ };
340
+ ```
341
+
342
+ ## Raw Binding Access
343
+
344
+ Each service exposes the underlying Cloudflare binding via a getter:
345
+
346
+ ```ts
347
+ const rawKV = kvService.namespace; // KVNamespace
348
+ const rawD1 = d1Service.database; // D1Database
349
+ const rawR2 = r2Service.bucket; // R2Bucket
350
+ const rawQueue = queueService.queue; // Queue
351
+ const rawDO = doService.namespace; // DurableObjectNamespace
352
+ const rawAI = aiService.binding; // Ai
353
+ const rawVec = vecService.index; // VectorizeIndex
354
+ const rawHD = hdService.binding; // Hyperdrive
355
+ ```
356
+
357
+ ## How It Works
358
+
359
+ 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.
360
+
361
+ `CloudflareFactory` handles this by:
362
+
363
+ 1. Each `XModule.forRoot()` creates a `BindingRef` (mutable holder) and registers it in the DI container
364
+ 2. Services are constructed at boot time with the `BindingRef` — no binding access yet
365
+ 3. On the first HTTP request, a one-time middleware reads `c.env` and initializes all `BindingRef` instances
366
+ 4. From that point on, services access bindings lazily through the ref
367
+
368
+ This means binding-dependent services work as plain singletons with no per-request overhead.
369
+
370
+ ## License
371
+
372
+ MIT
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Mutable holder for a Cloudflare binding value.
3
+ *
4
+ * Created by each module's `forRoot()` and populated by
5
+ * CloudflareFactory's one-time middleware on the first request.
6
+ * Services access the binding lazily via `.value`.
7
+ */
8
+ export declare class BindingRef<T = unknown> {
9
+ readonly bindingName: string;
10
+ private _value;
11
+ constructor(bindingName: string);
12
+ get value(): T;
13
+ /** @internal — called by CloudflareFactory middleware */
14
+ _initialize(value: any): void;
15
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Mutable holder for a Cloudflare binding value.
3
+ *
4
+ * Created by each module's `forRoot()` and populated by
5
+ * CloudflareFactory's one-time middleware on the first request.
6
+ * Services access the binding lazily via `.value`.
7
+ */ export class BindingRef {
8
+ bindingName;
9
+ _value;
10
+ constructor(bindingName){
11
+ this.bindingName = bindingName;
12
+ }
13
+ get value() {
14
+ if (this._value === undefined) {
15
+ throw new Error(`Cloudflare binding '${this.bindingName}' not initialized. ` + `Ensure CloudflareFactory.create() is used and a request has been made.`);
16
+ }
17
+ return this._value;
18
+ }
19
+ /** @internal — called by CloudflareFactory middleware */ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ _initialize(value) {
21
+ this._value = value;
22
+ }
23
+ }
@@ -0,0 +1,51 @@
1
+ import type { Hono } from 'hono';
2
+ import type { VelaApplication } from '@velajs/vela';
3
+ import type { CloudflareEnv } from './types';
4
+ /**
5
+ * Wraps VelaApplication with Cloudflare-specific handlers:
6
+ * - `fetch` — HTTP request handler (from Hono)
7
+ * - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators)
8
+ * - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const app = await CloudflareFactory.create(AppModule);
13
+ * export default {
14
+ * fetch: app.fetch,
15
+ * scheduled: app.scheduled.bind(app),
16
+ * queue: app.queue.bind(app),
17
+ * };
18
+ * ```
19
+ */
20
+ export declare class CloudflareApplication {
21
+ private app;
22
+ private scheduledHandlers;
23
+ private queueConsumers;
24
+ constructor(app: VelaApplication);
25
+ get fetch(): Hono['fetch'];
26
+ getHonoApp(): Hono;
27
+ /** @internal — scans instances for @Scheduled and @QueueConsumer metadata */
28
+ scanInstances(instances: unknown[]): void;
29
+ /**
30
+ * Handle Cloudflare scheduled (cron) events.
31
+ * Matches the event's cron expression to `@Scheduled()` handlers.
32
+ */
33
+ scheduled(event: {
34
+ cron: string;
35
+ scheduledTime?: number;
36
+ }, env: CloudflareEnv, ctx: {
37
+ waitUntil: (promise: Promise<unknown>) => void;
38
+ }): Promise<void>;
39
+ /**
40
+ * Handle Cloudflare Queue consumer events.
41
+ * Matches the batch queue name to `@QueueConsumer()` handlers.
42
+ */
43
+ queue(batch: {
44
+ queue: string;
45
+ messages: unknown[];
46
+ }, env: CloudflareEnv, ctx: {
47
+ waitUntil: (promise: Promise<unknown>) => void;
48
+ }): Promise<void>;
49
+ /** Gracefully shut down the application. */
50
+ close(signal?: string): Promise<void>;
51
+ }
@@ -0,0 +1,77 @@
1
+ import { getScheduledMetadata } from "./decorators/scheduled.js";
2
+ import { getQueueConsumerMetadata } from "./decorators/queue-consumer.js";
3
+ /**
4
+ * Wraps VelaApplication with Cloudflare-specific handlers:
5
+ * - `fetch` — HTTP request handler (from Hono)
6
+ * - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators)
7
+ * - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const app = await CloudflareFactory.create(AppModule);
12
+ * export default {
13
+ * fetch: app.fetch,
14
+ * scheduled: app.scheduled.bind(app),
15
+ * queue: app.queue.bind(app),
16
+ * };
17
+ * ```
18
+ */ export class CloudflareApplication {
19
+ app;
20
+ scheduledHandlers = [];
21
+ queueConsumers = [];
22
+ constructor(app){
23
+ this.app = app;
24
+ }
25
+ get fetch() {
26
+ return this.app.fetch;
27
+ }
28
+ getHonoApp() {
29
+ return this.app.getHonoApp();
30
+ }
31
+ /** @internal — scans instances for @Scheduled and @QueueConsumer metadata */ scanInstances(instances) {
32
+ for (const instance of instances){
33
+ if (!instance || typeof instance !== 'object') continue;
34
+ const scheduledMeta = getScheduledMetadata(instance);
35
+ for (const meta of scheduledMeta){
36
+ this.scheduledHandlers.push({
37
+ instance,
38
+ methodName: meta.methodName,
39
+ cron: meta.cron
40
+ });
41
+ }
42
+ const queueMeta = getQueueConsumerMetadata(instance);
43
+ for (const meta of queueMeta){
44
+ this.queueConsumers.push({
45
+ instance,
46
+ methodName: meta.methodName,
47
+ queueName: meta.queueName
48
+ });
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Handle Cloudflare scheduled (cron) events.
54
+ * Matches the event's cron expression to `@Scheduled()` handlers.
55
+ */ async scheduled(event, env, ctx) {
56
+ const matching = this.scheduledHandlers.filter((h)=>h.cron === event.cron);
57
+ const promises = matching.map((handler)=>{
58
+ const method = handler.instance[handler.methodName];
59
+ return method.call(handler.instance, event, env, ctx);
60
+ });
61
+ await Promise.all(promises);
62
+ }
63
+ /**
64
+ * Handle Cloudflare Queue consumer events.
65
+ * Matches the batch queue name to `@QueueConsumer()` handlers.
66
+ */ async queue(batch, env, ctx) {
67
+ const matching = this.queueConsumers.filter((h)=>h.queueName === batch.queue);
68
+ const promises = matching.map((handler)=>{
69
+ const method = handler.instance[handler.methodName];
70
+ return method.call(handler.instance, batch, env, ctx);
71
+ });
72
+ await Promise.all(promises);
73
+ }
74
+ /** Gracefully shut down the application. */ async close(signal) {
75
+ return this.app.close(signal);
76
+ }
77
+ }
@@ -0,0 +1,18 @@
1
+ import type { Type } from '@velajs/vela';
2
+ import { CloudflareApplication } from './cloudflare-application';
3
+ /**
4
+ * Factory for creating Cloudflare Worker applications.
5
+ * Replaces `VelaFactory.create()` for Cloudflare apps.
6
+ *
7
+ * Sets up a one-time Hono middleware that captures `c.env` on the
8
+ * first request and initializes all configured binding refs.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const app = await CloudflareFactory.create(AppModule);
13
+ * export default app; // has .fetch, .scheduled, .queue
14
+ * ```
15
+ */
16
+ export declare const CloudflareFactory: {
17
+ create(rootModule: Type): Promise<CloudflareApplication>;
18
+ };
@@ -0,0 +1,42 @@
1
+ import { VelaFactory } from "@velajs/vela";
2
+ import { CloudflareApplication } from "./cloudflare-application.js";
3
+ import { bindingsRegistry } from "./tokens.js";
4
+ /**
5
+ * Factory for creating Cloudflare Worker applications.
6
+ * Replaces `VelaFactory.create()` for Cloudflare apps.
7
+ *
8
+ * Sets up a one-time Hono middleware that captures `c.env` on the
9
+ * first request and initializes all configured binding refs.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const app = await CloudflareFactory.create(AppModule);
14
+ * export default app; // has .fetch, .scheduled, .queue
15
+ * ```
16
+ */ export const CloudflareFactory = {
17
+ async create (rootModule) {
18
+ const velaApp = await VelaFactory.create(rootModule);
19
+ // One-time middleware: captures c.env on first request
20
+ // and initializes all BindingRef instances from bindingsRegistry.
21
+ // Registered via useGlobalMiddleware so it persists across rebuilds.
22
+ let initialized = false;
23
+ velaApp.useGlobalMiddleware({
24
+ use: async (c, next)=>{
25
+ if (!initialized) {
26
+ initialized = true;
27
+ const ctx = c;
28
+ const env = ctx.env ?? {};
29
+ for (const ref of bindingsRegistry){
30
+ ref._initialize(env[ref.bindingName]);
31
+ }
32
+ }
33
+ await next();
34
+ }
35
+ });
36
+ // Rebuild so the init middleware is placed before route handlers
37
+ await velaApp.rebuild();
38
+ const cfApp = new CloudflareApplication(velaApp);
39
+ cfApp.scanInstances(velaApp.getInstances());
40
+ return cfApp;
41
+ }
42
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Parameter decorator to inject Cloudflare environment bindings.
3
+ *
4
+ * Without arguments, returns the entire `env` object.
5
+ * With a binding name, returns that specific binding.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * @Get()
10
+ * handle(@Env() env: CloudflareEnv) { ... }
11
+ *
12
+ * @Get()
13
+ * handle(@Env('MY_KV') kv: KVNamespace) { ... }
14
+ * ```
15
+ */
16
+ export declare const Env: (data?: string | undefined, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
@@ -0,0 +1,22 @@
1
+ import { createParamDecorator } from "@velajs/vela";
2
+ /**
3
+ * Parameter decorator to inject Cloudflare environment bindings.
4
+ *
5
+ * Without arguments, returns the entire `env` object.
6
+ * With a binding name, returns that specific binding.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * @Get()
11
+ * handle(@Env() env: CloudflareEnv) { ... }
12
+ *
13
+ * @Get()
14
+ * handle(@Env('MY_KV') kv: KVNamespace) { ... }
15
+ * ```
16
+ */ export const Env = createParamDecorator((bindingName, ctx)=>{
17
+ // Hono Context has .env on Cloudflare Workers
18
+ const c = ctx.getContext();
19
+ const env = c.env;
20
+ if (!env) return undefined;
21
+ return bindingName ? env[bindingName] : env;
22
+ });
@@ -0,0 +1,23 @@
1
+ export interface QueueConsumerMetadata {
2
+ queueName: string;
3
+ methodName: string;
4
+ }
5
+ /**
6
+ * Marks a method as a queue consumer handler.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * @Injectable()
11
+ * class WorkerService {
12
+ * @QueueConsumer('email-queue')
13
+ * async processEmails(batch: MessageBatch) {
14
+ * for (const msg of batch.messages) {
15
+ * console.log('Processing:', msg.body);
16
+ * msg.ack();
17
+ * }
18
+ * }
19
+ * }
20
+ * ```
21
+ */
22
+ export declare function QueueConsumer(queueName: string): MethodDecorator;
23
+ export declare function getQueueConsumerMetadata(target: object): QueueConsumerMetadata[];
@@ -0,0 +1,32 @@
1
+ import { defineMetadata, getMetadata } from "@velajs/vela";
2
+ const QUEUE_CONSUMER_METADATA_KEY = 'cloudflare:queue-consumer';
3
+ /**
4
+ * Marks a method as a queue consumer handler.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * @Injectable()
9
+ * class WorkerService {
10
+ * @QueueConsumer('email-queue')
11
+ * async processEmails(batch: MessageBatch) {
12
+ * for (const msg of batch.messages) {
13
+ * console.log('Processing:', msg.body);
14
+ * msg.ack();
15
+ * }
16
+ * }
17
+ * }
18
+ * ```
19
+ */ export function QueueConsumer(queueName) {
20
+ return (target, propertyKey, _descriptor)=>{
21
+ const existing = getMetadata(QUEUE_CONSUMER_METADATA_KEY, target.constructor) ?? [];
22
+ existing.push({
23
+ queueName,
24
+ methodName: String(propertyKey)
25
+ });
26
+ defineMetadata(QUEUE_CONSUMER_METADATA_KEY, existing, target.constructor);
27
+ };
28
+ }
29
+ export function getQueueConsumerMetadata(target) {
30
+ const ctor = target.constructor ?? target;
31
+ return getMetadata(QUEUE_CONSUMER_METADATA_KEY, ctor) ?? [];
32
+ }