@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.
- package/README.md +372 -0
- package/dist/binding-ref.d.ts +15 -0
- package/dist/binding-ref.js +23 -0
- package/dist/cloudflare-application.d.ts +51 -0
- package/dist/cloudflare-application.js +77 -0
- package/dist/cloudflare-factory.d.ts +18 -0
- package/dist/cloudflare-factory.js +42 -0
- package/dist/decorators/env.d.ts +16 -0
- package/dist/decorators/env.js +22 -0
- package/dist/decorators/queue-consumer.d.ts +23 -0
- package/dist/decorators/queue-consumer.js +32 -0
- package/dist/decorators/scheduled.d.ts +20 -0
- package/dist/decorators/scheduled.js +29 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +29 -0
- package/dist/modules/ai.module.d.ts +6 -0
- package/dist/modules/ai.module.js +31 -0
- package/dist/modules/d1.module.d.ts +6 -0
- package/dist/modules/d1.module.js +31 -0
- package/dist/modules/durable-object.module.d.ts +6 -0
- package/dist/modules/durable-object.module.js +31 -0
- package/dist/modules/hyperdrive.module.d.ts +6 -0
- package/dist/modules/hyperdrive.module.js +31 -0
- package/dist/modules/kv.module.d.ts +6 -0
- package/dist/modules/kv.module.js +31 -0
- package/dist/modules/queue.module.d.ts +6 -0
- package/dist/modules/queue.module.js +31 -0
- package/dist/modules/r2.module.d.ts +6 -0
- package/dist/modules/r2.module.js +31 -0
- package/dist/modules/vectorize.module.d.ts +6 -0
- package/dist/modules/vectorize.module.js +31 -0
- package/dist/services/ai.service.d.ts +16 -0
- package/dist/services/ai.service.js +36 -0
- package/dist/services/d1.service.d.ts +22 -0
- package/dist/services/d1.service.js +47 -0
- package/dist/services/durable-object.service.d.ts +24 -0
- package/dist/services/durable-object.service.js +48 -0
- package/dist/services/hyperdrive.service.d.ts +26 -0
- package/dist/services/hyperdrive.service.js +51 -0
- package/dist/services/kv.service.d.ts +24 -0
- package/dist/services/kv.service.js +48 -0
- package/dist/services/queue.service.d.ts +20 -0
- package/dist/services/queue.service.js +39 -0
- package/dist/services/r2.service.d.ts +18 -0
- package/dist/services/r2.service.js +48 -0
- package/dist/services/vectorize.service.d.ts +26 -0
- package/dist/services/vectorize.service.js +51 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +18 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +1 -0
- 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
|
+
}
|