@velajs/cloudflare 0.1.1 → 1.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/CHANGELOG.md +29 -0
- package/README.md +52 -39
- package/dist/binding-ref.d.ts +2 -9
- package/dist/binding-ref.js +6 -10
- package/dist/cloudflare-application.d.ts +6 -6
- package/dist/cloudflare-application.js +35 -19
- package/dist/cloudflare-factory.d.ts +5 -8
- package/dist/cloudflare-factory.js +37 -26
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -5
- package/dist/modules/ai.module.d.ts +1 -6
- package/dist/modules/ai.module.js +7 -30
- package/dist/modules/create-binding-module.d.ts +14 -0
- package/dist/modules/create-binding-module.js +26 -0
- package/dist/modules/d1.module.d.ts +1 -6
- package/dist/modules/d1.module.js +7 -30
- package/dist/modules/durable-object.module.d.ts +1 -6
- package/dist/modules/durable-object.module.js +7 -30
- package/dist/modules/hyperdrive.module.d.ts +1 -6
- package/dist/modules/hyperdrive.module.js +7 -30
- package/dist/modules/kv.module.d.ts +1 -6
- package/dist/modules/kv.module.js +7 -30
- package/dist/modules/queue.module.d.ts +1 -6
- package/dist/modules/queue.module.js +7 -30
- package/dist/modules/r2.module.d.ts +1 -6
- package/dist/modules/r2.module.js +7 -30
- package/dist/modules/vectorize.module.d.ts +1 -6
- package/dist/modules/vectorize.module.js +7 -30
- package/dist/services/ai.service.d.ts +2 -12
- package/dist/services/ai.service.js +1 -4
- package/dist/services/d1.service.d.ts +2 -18
- package/dist/services/d1.service.js +1 -15
- package/dist/services/durable-object.service.d.ts +2 -20
- package/dist/services/durable-object.service.js +1 -16
- package/dist/services/hyperdrive.service.d.ts +2 -16
- package/dist/services/hyperdrive.service.js +1 -1
- package/dist/services/kv.service.d.ts +5 -18
- package/dist/services/kv.service.js +1 -16
- package/dist/services/queue.service.d.ts +3 -17
- package/dist/services/queue.service.js +1 -7
- package/dist/services/r2.service.d.ts +2 -14
- package/dist/services/r2.service.js +1 -16
- package/dist/services/vectorize.service.d.ts +2 -22
- package/dist/services/vectorize.service.js +1 -19
- package/dist/tokens.d.ts +0 -7
- package/dist/tokens.js +1 -9
- package/package.json +19 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (2026-04-28)
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- **`CloudflareFactory` renamed to `createCloudflareApp`.** The factory object exposed exactly one method (`.create`) and was a thin wrapper around `VelaFactory`. Replaced with a plain async function:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// before
|
|
11
|
+
import { CloudflareFactory } from '@velajs/cloudflare';
|
|
12
|
+
const app = await CloudflareFactory.create(AppModule);
|
|
13
|
+
|
|
14
|
+
// after
|
|
15
|
+
import { createCloudflareApp } from '@velajs/cloudflare';
|
|
16
|
+
const app = await createCloudflareApp(AppModule);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### New
|
|
20
|
+
|
|
21
|
+
- `CloudflareApplication.scheduled()` now also dispatches `@Cron('expr')` jobs from `@velajs/vela`. Use either the cloudflare-specific `@Scheduled()` decorator or the framework's `@Cron()` decorator — both are matched against the incoming cron event.
|
|
22
|
+
|
|
23
|
+
### Compatibility
|
|
24
|
+
|
|
25
|
+
- Requires `@velajs/vela` ≥ 1.0.0 for the `@Cron` integration. The schedule split in vela 0.10 makes its `ScheduleModule` metadata-only, which lets edge platforms drive cron via their native triggers.
|
|
26
|
+
|
|
27
|
+
## 0.1.0 (2026-04-13)
|
|
28
|
+
|
|
29
|
+
Initial release.
|
package/README.md
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
# @velajs/cloudflare
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@velajs/cloudflare)
|
|
4
|
+
[](https://github.com/velajs/cloudflare/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/velajs/cloudflare/blob/main/LICENSE)
|
|
6
|
+
|
|
3
7
|
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
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
|
-
|
|
12
|
+
pnpm add @velajs/cloudflare @velajs/vela hono
|
|
13
|
+
pnpm add -D @cloudflare/workers-types
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@cloudflare/workers-types` is a required peer (types-only — zero runtime cost). It's what gives `KVNamespace`, `D1Database`, `R2Bucket`, `Queue`, `DurableObjectNamespace`, `Ai`, `VectorizeIndex`, and `Hyperdrive` their proper types when you reach into the underlying binding.
|
|
17
|
+
|
|
18
|
+
## Service shape
|
|
19
|
+
|
|
20
|
+
Every service is a thin typed wrapper around its Cloudflare binding. Use the accessor (`.namespace`, `.database`, `.bucket`, `.queue`, `.binding`, `.index`) to call the binding's methods directly — full `@cloudflare/workers-types` autocomplete, no shim layer in between.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
class KVService { readonly namespace: KVNamespace; }
|
|
24
|
+
class D1Service { readonly database: D1Database; }
|
|
25
|
+
class R2Service { readonly bucket: R2Bucket; }
|
|
26
|
+
class QueueService<T> { readonly queue: Queue<T>; }
|
|
27
|
+
class DurableObjectService { readonly namespace: DurableObjectNamespace; }
|
|
28
|
+
class AIService { readonly binding: Ai; }
|
|
29
|
+
class VectorizeService { readonly index: VectorizeIndex; }
|
|
30
|
+
class HyperdriveService { readonly binding: Hyperdrive; }
|
|
9
31
|
```
|
|
10
32
|
|
|
11
33
|
## Quick Start
|
|
12
34
|
|
|
13
35
|
```ts
|
|
14
36
|
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';
|
|
37
|
+
import { CloudflareFactory, KVModule, KVService, D1Module, D1Service } from '@velajs/cloudflare';
|
|
17
38
|
|
|
18
39
|
@Injectable()
|
|
19
40
|
class UserService {
|
|
@@ -23,11 +44,11 @@ class UserService {
|
|
|
23
44
|
) {}
|
|
24
45
|
|
|
25
46
|
async findById(id: string) {
|
|
26
|
-
const cached = await this.kv.get(`user:${id}`);
|
|
47
|
+
const cached = await this.kv.namespace.get(`user:${id}`);
|
|
27
48
|
if (cached) return JSON.parse(cached as string);
|
|
28
49
|
|
|
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));
|
|
50
|
+
const user = await this.d1.database.prepare('SELECT * FROM users WHERE id = ?').bind(id).first();
|
|
51
|
+
if (user) await this.kv.namespace.put(`user:${id}`, JSON.stringify(user));
|
|
31
52
|
return user;
|
|
32
53
|
}
|
|
33
54
|
}
|
|
@@ -85,8 +106,7 @@ Each module follows the same pattern: `XModule.forRoot({ binding: 'NAME' })` ret
|
|
|
85
106
|
### KVModule
|
|
86
107
|
|
|
87
108
|
```ts
|
|
88
|
-
import { KVModule } from '@velajs/cloudflare';
|
|
89
|
-
import type { KVService } from '@velajs/cloudflare';
|
|
109
|
+
import { KVModule, KVService } from '@velajs/cloudflare';
|
|
90
110
|
|
|
91
111
|
@Module({ imports: [KVModule.forRoot({ binding: 'MY_KV' })] })
|
|
92
112
|
class AppModule {}
|
|
@@ -95,18 +115,17 @@ class AppModule {}
|
|
|
95
115
|
class CacheService {
|
|
96
116
|
constructor(private kv: KVService) {}
|
|
97
117
|
|
|
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(); }
|
|
118
|
+
async get(key: string) { return this.kv.namespace.get(key); }
|
|
119
|
+
async set(key: string, value: string) { return this.kv.namespace.put(key, value); }
|
|
120
|
+
async remove(key: string) { return this.kv.namespace.delete(key); }
|
|
121
|
+
async keys() { return this.kv.namespace.list(); }
|
|
102
122
|
}
|
|
103
123
|
```
|
|
104
124
|
|
|
105
125
|
### D1Module
|
|
106
126
|
|
|
107
127
|
```ts
|
|
108
|
-
import { D1Module } from '@velajs/cloudflare';
|
|
109
|
-
import type { D1Service } from '@velajs/cloudflare';
|
|
128
|
+
import { D1Module, D1Service } from '@velajs/cloudflare';
|
|
110
129
|
|
|
111
130
|
@Module({ imports: [D1Module.forRoot({ binding: 'DB' })] })
|
|
112
131
|
class AppModule {}
|
|
@@ -116,11 +135,11 @@ class PostService {
|
|
|
116
135
|
constructor(private d1: D1Service) {}
|
|
117
136
|
|
|
118
137
|
async findAll() {
|
|
119
|
-
return this.d1.prepare('SELECT * FROM posts').all();
|
|
138
|
+
return this.d1.database.prepare('SELECT * FROM posts').all();
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
async create(title: string) {
|
|
123
|
-
return this.d1.prepare('INSERT INTO posts (title) VALUES (?)').bind(title).run();
|
|
142
|
+
return this.d1.database.prepare('INSERT INTO posts (title) VALUES (?)').bind(title).run();
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
```
|
|
@@ -128,8 +147,7 @@ class PostService {
|
|
|
128
147
|
### R2Module
|
|
129
148
|
|
|
130
149
|
```ts
|
|
131
|
-
import { R2Module } from '@velajs/cloudflare';
|
|
132
|
-
import type { R2Service } from '@velajs/cloudflare';
|
|
150
|
+
import { R2Module, R2Service } from '@velajs/cloudflare';
|
|
133
151
|
|
|
134
152
|
@Module({ imports: [R2Module.forRoot({ binding: 'ASSETS' })] })
|
|
135
153
|
class AppModule {}
|
|
@@ -138,17 +156,16 @@ class AppModule {}
|
|
|
138
156
|
class StorageService {
|
|
139
157
|
constructor(private r2: R2Service) {}
|
|
140
158
|
|
|
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); }
|
|
159
|
+
async upload(key: string, data: string) { return this.r2.bucket.put(key, data); }
|
|
160
|
+
async download(key: string) { return this.r2.bucket.get(key); }
|
|
161
|
+
async remove(key: string) { return this.r2.bucket.delete(key); }
|
|
144
162
|
}
|
|
145
163
|
```
|
|
146
164
|
|
|
147
165
|
### QueueModule
|
|
148
166
|
|
|
149
167
|
```ts
|
|
150
|
-
import { QueueModule } from '@velajs/cloudflare';
|
|
151
|
-
import type { QueueService } from '@velajs/cloudflare';
|
|
168
|
+
import { QueueModule, QueueService } from '@velajs/cloudflare';
|
|
152
169
|
|
|
153
170
|
@Module({ imports: [QueueModule.forRoot({ binding: 'EMAIL_QUEUE' })] })
|
|
154
171
|
class AppModule {}
|
|
@@ -158,7 +175,7 @@ class NotificationService {
|
|
|
158
175
|
constructor(private queue: QueueService) {}
|
|
159
176
|
|
|
160
177
|
async sendEmail(to: string, subject: string) {
|
|
161
|
-
await this.queue.send({ to, subject });
|
|
178
|
+
await this.queue.queue.send({ to, subject });
|
|
162
179
|
}
|
|
163
180
|
}
|
|
164
181
|
```
|
|
@@ -166,8 +183,7 @@ class NotificationService {
|
|
|
166
183
|
### DurableObjectModule
|
|
167
184
|
|
|
168
185
|
```ts
|
|
169
|
-
import { DurableObjectModule } from '@velajs/cloudflare';
|
|
170
|
-
import type { DurableObjectService } from '@velajs/cloudflare';
|
|
186
|
+
import { DurableObjectModule, DurableObjectService } from '@velajs/cloudflare';
|
|
171
187
|
|
|
172
188
|
@Module({ imports: [DurableObjectModule.forRoot({ binding: 'COUNTER' })] })
|
|
173
189
|
class AppModule {}
|
|
@@ -177,8 +193,8 @@ class CounterService {
|
|
|
177
193
|
constructor(private doNs: DurableObjectService) {}
|
|
178
194
|
|
|
179
195
|
async increment(name: string) {
|
|
180
|
-
const id = this.doNs.idFromName(name);
|
|
181
|
-
const stub = this.doNs.get(id);
|
|
196
|
+
const id = this.doNs.namespace.idFromName(name);
|
|
197
|
+
const stub = this.doNs.namespace.get(id);
|
|
182
198
|
return (stub as any).fetch('/increment');
|
|
183
199
|
}
|
|
184
200
|
}
|
|
@@ -187,8 +203,7 @@ class CounterService {
|
|
|
187
203
|
### AIModule
|
|
188
204
|
|
|
189
205
|
```ts
|
|
190
|
-
import { AIModule } from '@velajs/cloudflare';
|
|
191
|
-
import type { AIService } from '@velajs/cloudflare';
|
|
206
|
+
import { AIModule, AIService } from '@velajs/cloudflare';
|
|
192
207
|
|
|
193
208
|
@Module({ imports: [AIModule.forRoot({ binding: 'AI' })] })
|
|
194
209
|
class AppModule {}
|
|
@@ -198,7 +213,7 @@ class ChatService {
|
|
|
198
213
|
constructor(private ai: AIService) {}
|
|
199
214
|
|
|
200
215
|
async chat(prompt: string) {
|
|
201
|
-
return this.ai.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
216
|
+
return this.ai.binding.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
202
217
|
messages: [{ role: 'user', content: prompt }],
|
|
203
218
|
});
|
|
204
219
|
}
|
|
@@ -208,8 +223,7 @@ class ChatService {
|
|
|
208
223
|
### VectorizeModule
|
|
209
224
|
|
|
210
225
|
```ts
|
|
211
|
-
import { VectorizeModule } from '@velajs/cloudflare';
|
|
212
|
-
import type { VectorizeService } from '@velajs/cloudflare';
|
|
226
|
+
import { VectorizeModule, VectorizeService } from '@velajs/cloudflare';
|
|
213
227
|
|
|
214
228
|
@Module({ imports: [VectorizeModule.forRoot({ binding: 'EMBEDDINGS' })] })
|
|
215
229
|
class AppModule {}
|
|
@@ -219,11 +233,11 @@ class SearchService {
|
|
|
219
233
|
constructor(private vectorize: VectorizeService) {}
|
|
220
234
|
|
|
221
235
|
async search(vector: number[]) {
|
|
222
|
-
return this.vectorize.query(vector, { topK: 10 });
|
|
236
|
+
return this.vectorize.index.query(vector, { topK: 10 });
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
async addVectors(vectors: unknown[]) {
|
|
226
|
-
return this.vectorize.upsert(vectors);
|
|
240
|
+
return this.vectorize.index.upsert(vectors);
|
|
227
241
|
}
|
|
228
242
|
}
|
|
229
243
|
```
|
|
@@ -231,8 +245,7 @@ class SearchService {
|
|
|
231
245
|
### HyperdriveModule
|
|
232
246
|
|
|
233
247
|
```ts
|
|
234
|
-
import { HyperdriveModule } from '@velajs/cloudflare';
|
|
235
|
-
import type { HyperdriveService } from '@velajs/cloudflare';
|
|
248
|
+
import { HyperdriveModule, HyperdriveService } from '@velajs/cloudflare';
|
|
236
249
|
|
|
237
250
|
@Module({ imports: [HyperdriveModule.forRoot({ binding: 'POSTGRES' })] })
|
|
238
251
|
class AppModule {}
|
|
@@ -242,11 +255,11 @@ class DbService {
|
|
|
242
255
|
constructor(private hd: HyperdriveService) {}
|
|
243
256
|
|
|
244
257
|
getConnectionString() {
|
|
245
|
-
return this.hd.connectionString;
|
|
258
|
+
return this.hd.binding.connectionString;
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
getConfig() {
|
|
249
|
-
return { host: this.hd.host, port: this.hd.port, database: this.hd.database };
|
|
262
|
+
return { host: this.hd.binding.host, port: this.hd.binding.port, database: this.hd.binding.database };
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
```
|
package/dist/binding-ref.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
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
1
|
export declare class BindingRef<T = unknown> {
|
|
9
2
|
readonly bindingName: string;
|
|
10
3
|
private _value;
|
|
11
4
|
constructor(bindingName: string);
|
|
12
5
|
get value(): T;
|
|
13
|
-
/** @internal — called by
|
|
14
|
-
_initialize(value:
|
|
6
|
+
/** @internal — called by createCloudflareApp middleware */
|
|
7
|
+
_initialize(value: T): void;
|
|
15
8
|
}
|
package/dist/binding-ref.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* CloudflareFactory's one-time middleware on the first request.
|
|
6
|
-
* Services access the binding lazily via `.value`.
|
|
7
|
-
*/ export class BindingRef {
|
|
1
|
+
// Mutable holder for a Cloudflare binding value. Created by each module's
|
|
2
|
+
// forRoot() and populated by createCloudflareApp's one-time middleware on
|
|
3
|
+
// the first request. Services access the binding lazily via .value.
|
|
4
|
+
export class BindingRef {
|
|
8
5
|
bindingName;
|
|
9
6
|
_value;
|
|
10
7
|
constructor(bindingName){
|
|
@@ -12,12 +9,11 @@
|
|
|
12
9
|
}
|
|
13
10
|
get value() {
|
|
14
11
|
if (this._value === undefined) {
|
|
15
|
-
throw new Error(`Cloudflare binding '${this.bindingName}' not initialized. ` + `Ensure
|
|
12
|
+
throw new Error(`Cloudflare binding '${this.bindingName}' not initialized. ` + `Ensure createCloudflareApp() is used and a request has been made.`);
|
|
16
13
|
}
|
|
17
14
|
return this._value;
|
|
18
15
|
}
|
|
19
|
-
/** @internal — called by
|
|
20
|
-
_initialize(value) {
|
|
16
|
+
/** @internal — called by createCloudflareApp middleware */ _initialize(value) {
|
|
21
17
|
this._value = value;
|
|
22
18
|
}
|
|
23
19
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import type
|
|
2
|
+
import { type VelaApplication } from '@velajs/vela';
|
|
3
3
|
import type { CloudflareEnv } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Wraps VelaApplication with Cloudflare-specific handlers:
|
|
6
6
|
* - `fetch` — HTTP request handler (from Hono)
|
|
7
|
-
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
7
|
+
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
8
|
+
* AND vela's own `@Cron()` jobs)
|
|
8
9
|
* - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
12
|
* ```ts
|
|
12
|
-
* const app = await
|
|
13
|
+
* const app = await createCloudflareApp(AppModule);
|
|
13
14
|
* export default {
|
|
14
15
|
* fetch: app.fetch,
|
|
15
16
|
* scheduled: app.scheduled.bind(app),
|
|
@@ -24,11 +25,11 @@ export declare class CloudflareApplication {
|
|
|
24
25
|
constructor(app: VelaApplication);
|
|
25
26
|
get fetch(): Hono['fetch'];
|
|
26
27
|
getHonoApp(): Hono;
|
|
27
|
-
/** @internal — scans instances for @Scheduled and @QueueConsumer metadata */
|
|
28
|
+
/** @internal — scans instances for @Scheduled, @Cron, and @QueueConsumer metadata */
|
|
28
29
|
scanInstances(instances: unknown[]): void;
|
|
29
30
|
/**
|
|
30
31
|
* Handle Cloudflare scheduled (cron) events.
|
|
31
|
-
* Matches the event's cron expression to `@Scheduled()` handlers.
|
|
32
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()` handlers.
|
|
32
33
|
*/
|
|
33
34
|
scheduled(event: {
|
|
34
35
|
cron: string;
|
|
@@ -46,6 +47,5 @@ export declare class CloudflareApplication {
|
|
|
46
47
|
}, env: CloudflareEnv, ctx: {
|
|
47
48
|
waitUntil: (promise: Promise<unknown>) => void;
|
|
48
49
|
}): Promise<void>;
|
|
49
|
-
/** Gracefully shut down the application. */
|
|
50
50
|
close(signal?: string): Promise<void>;
|
|
51
51
|
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import { CRON_METADATA, getMetadata } from "@velajs/vela";
|
|
1
2
|
import { getScheduledMetadata } from "./decorators/scheduled.js";
|
|
2
3
|
import { getQueueConsumerMetadata } from "./decorators/queue-consumer.js";
|
|
4
|
+
function invoke(instance, methodName, args) {
|
|
5
|
+
const method = instance[methodName];
|
|
6
|
+
if (typeof method !== 'function') {
|
|
7
|
+
throw new Error(`Method '${methodName}' is not a function on ${instance.constructor.name}`);
|
|
8
|
+
}
|
|
9
|
+
return method.apply(instance, args);
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Wraps VelaApplication with Cloudflare-specific handlers:
|
|
5
13
|
* - `fetch` — HTTP request handler (from Hono)
|
|
6
|
-
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
14
|
+
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
15
|
+
* AND vela's own `@Cron()` jobs)
|
|
7
16
|
* - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
|
|
8
17
|
*
|
|
9
18
|
* @example
|
|
10
19
|
* ```ts
|
|
11
|
-
* const app = await
|
|
20
|
+
* const app = await createCloudflareApp(AppModule);
|
|
12
21
|
* export default {
|
|
13
22
|
* fetch: app.fetch,
|
|
14
23
|
* scheduled: app.scheduled.bind(app),
|
|
@@ -28,19 +37,26 @@ import { getQueueConsumerMetadata } from "./decorators/queue-consumer.js";
|
|
|
28
37
|
getHonoApp() {
|
|
29
38
|
return this.app.getHonoApp();
|
|
30
39
|
}
|
|
31
|
-
/** @internal — scans instances for @Scheduled and @QueueConsumer metadata */ scanInstances(instances) {
|
|
40
|
+
/** @internal — scans instances for @Scheduled, @Cron, and @QueueConsumer metadata */ scanInstances(instances) {
|
|
32
41
|
for (const instance of instances){
|
|
33
42
|
if (!instance || typeof instance !== 'object') continue;
|
|
34
|
-
const
|
|
35
|
-
for (const meta of scheduledMeta){
|
|
43
|
+
for (const meta of getScheduledMetadata(instance)){
|
|
36
44
|
this.scheduledHandlers.push({
|
|
37
45
|
instance,
|
|
38
46
|
methodName: meta.methodName,
|
|
39
47
|
cron: meta.cron
|
|
40
48
|
});
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// vela's @Cron jobs run via the same Workers cron trigger.
|
|
51
|
+
const cronMeta = getMetadata(CRON_METADATA, instance.constructor) ?? [];
|
|
52
|
+
for (const meta of cronMeta){
|
|
53
|
+
this.scheduledHandlers.push({
|
|
54
|
+
instance,
|
|
55
|
+
methodName: meta.methodName,
|
|
56
|
+
cron: meta.expression
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const meta of getQueueConsumerMetadata(instance)){
|
|
44
60
|
this.queueConsumers.push({
|
|
45
61
|
instance,
|
|
46
62
|
methodName: meta.methodName,
|
|
@@ -51,27 +67,27 @@ import { getQueueConsumerMetadata } from "./decorators/queue-consumer.js";
|
|
|
51
67
|
}
|
|
52
68
|
/**
|
|
53
69
|
* Handle Cloudflare scheduled (cron) events.
|
|
54
|
-
* Matches the event's cron expression to `@Scheduled()` handlers.
|
|
70
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()` handlers.
|
|
55
71
|
*/ async scheduled(event, env, ctx) {
|
|
56
72
|
const matching = this.scheduledHandlers.filter((h)=>h.cron === event.cron);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
await Promise.all(matching.map((h)=>invoke(h.instance, h.methodName, [
|
|
74
|
+
event,
|
|
75
|
+
env,
|
|
76
|
+
ctx
|
|
77
|
+
])));
|
|
62
78
|
}
|
|
63
79
|
/**
|
|
64
80
|
* Handle Cloudflare Queue consumer events.
|
|
65
81
|
* Matches the batch queue name to `@QueueConsumer()` handlers.
|
|
66
82
|
*/ async queue(batch, env, ctx) {
|
|
67
83
|
const matching = this.queueConsumers.filter((h)=>h.queueName === batch.queue);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
await Promise.all(matching.map((h)=>invoke(h.instance, h.methodName, [
|
|
85
|
+
batch,
|
|
86
|
+
env,
|
|
87
|
+
ctx
|
|
88
|
+
])));
|
|
73
89
|
}
|
|
74
|
-
|
|
90
|
+
async close(signal) {
|
|
75
91
|
return this.app.close(signal);
|
|
76
92
|
}
|
|
77
93
|
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import type { Type } from '@velajs/vela';
|
|
2
2
|
import { CloudflareApplication } from './cloudflare-application';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Replaces `VelaFactory.create()` for Cloudflare apps.
|
|
4
|
+
* Create a Cloudflare Workers application.
|
|
6
5
|
*
|
|
7
|
-
* Sets up a one-time Hono middleware that captures `c.env` on the
|
|
8
|
-
*
|
|
6
|
+
* Sets up a one-time Hono middleware that captures `c.env` on the first
|
|
7
|
+
* request and initializes all configured binding refs.
|
|
9
8
|
*
|
|
10
9
|
* @example
|
|
11
10
|
* ```ts
|
|
12
|
-
* const app = await
|
|
11
|
+
* const app = await createCloudflareApp(AppModule);
|
|
13
12
|
* export default app; // has .fetch, .scheduled, .queue
|
|
14
13
|
* ```
|
|
15
14
|
*/
|
|
16
|
-
export declare
|
|
17
|
-
create(rootModule: Type): Promise<CloudflareApplication>;
|
|
18
|
-
};
|
|
15
|
+
export declare function createCloudflareApp(rootModule: Type): Promise<CloudflareApplication>;
|
|
@@ -1,42 +1,53 @@
|
|
|
1
1
|
import { VelaFactory } from "@velajs/vela";
|
|
2
|
+
import { BindingRef } from "./binding-ref.js";
|
|
2
3
|
import { CloudflareApplication } from "./cloudflare-application.js";
|
|
3
|
-
|
|
4
|
+
// Walks every provider registered in the container and pulls out the
|
|
5
|
+
// BindingRef instances. Replaces the old module-level `bindingsRegistry`
|
|
6
|
+
// global so two CloudflareApplication instances in one process don't
|
|
7
|
+
// share state.
|
|
8
|
+
function collectBindingRefs(container) {
|
|
9
|
+
const refs = [];
|
|
10
|
+
for (const token of container.getTokens()){
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = container.resolve(token);
|
|
14
|
+
} catch {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (value instanceof BindingRef) refs.push(value);
|
|
18
|
+
}
|
|
19
|
+
return refs;
|
|
20
|
+
}
|
|
4
21
|
/**
|
|
5
|
-
*
|
|
6
|
-
* Replaces `VelaFactory.create()` for Cloudflare apps.
|
|
22
|
+
* Create a Cloudflare Workers application.
|
|
7
23
|
*
|
|
8
|
-
* Sets up a one-time Hono middleware that captures `c.env` on the
|
|
9
|
-
*
|
|
24
|
+
* Sets up a one-time Hono middleware that captures `c.env` on the first
|
|
25
|
+
* request and initializes all configured binding refs.
|
|
10
26
|
*
|
|
11
27
|
* @example
|
|
12
28
|
* ```ts
|
|
13
|
-
* const app = await
|
|
29
|
+
* const app = await createCloudflareApp(AppModule);
|
|
14
30
|
* export default app; // has .fetch, .scheduled, .queue
|
|
15
31
|
* ```
|
|
16
|
-
*/ export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
let initialized = false;
|
|
23
|
-
velaApp.useGlobalMiddleware({
|
|
24
|
-
use: async (c, next)=>{
|
|
32
|
+
*/ export async function createCloudflareApp(rootModule) {
|
|
33
|
+
let initialized = false;
|
|
34
|
+
let refs;
|
|
35
|
+
const velaApp = await VelaFactory.create(rootModule, {
|
|
36
|
+
middleware: [
|
|
37
|
+
async (c, next)=>{
|
|
25
38
|
if (!initialized) {
|
|
26
39
|
initialized = true;
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
for (const ref of bindingsRegistry){
|
|
40
|
+
const env = c.env ?? {};
|
|
41
|
+
for (const ref of refs){
|
|
30
42
|
ref._initialize(env[ref.bindingName]);
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
await next();
|
|
34
46
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
refs = collectBindingRefs(velaApp.getContainer());
|
|
50
|
+
const cfApp = new CloudflareApplication(velaApp);
|
|
51
|
+
cfApp.scanInstances(velaApp.getInstances());
|
|
52
|
+
return cfApp;
|
|
53
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createCloudflareApp } from './cloudflare-factory';
|
|
2
2
|
export { CloudflareApplication } from './cloudflare-application';
|
|
3
3
|
export { KVModule } from './modules/kv.module';
|
|
4
4
|
export { D1Module } from './modules/d1.module';
|
|
@@ -19,9 +19,6 @@ export { HyperdriveService } from './services/hyperdrive.service';
|
|
|
19
19
|
export { Env } from './decorators/env';
|
|
20
20
|
export { Scheduled } from './decorators/scheduled';
|
|
21
21
|
export { QueueConsumer } from './decorators/queue-consumer';
|
|
22
|
-
export { BindingRef } from './binding-ref';
|
|
23
|
-
export { bindingsRegistry, clearBindingsRegistry } from './tokens';
|
|
24
|
-
export { KV_BINDING_REF, D1_BINDING_REF, R2_BINDING_REF, QUEUE_BINDING_REF, DO_BINDING_REF, AI_BINDING_REF, VECTORIZE_BINDING_REF, HYPERDRIVE_BINDING_REF, } from './tokens';
|
|
25
22
|
export type { CloudflareEnv, ScheduledRegistration, QueueRegistration } from './types';
|
|
26
23
|
export type { ScheduledMetadata } from './decorators/scheduled';
|
|
27
24
|
export type { QueueConsumerMetadata } from './decorators/queue-consumer';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Factory & Application
|
|
2
|
-
export {
|
|
2
|
+
export { 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";
|
|
@@ -23,7 +23,3 @@ export { HyperdriveService } from "./services/hyperdrive.service.js";
|
|
|
23
23
|
export { Env } from "./decorators/env.js";
|
|
24
24
|
export { Scheduled } from "./decorators/scheduled.js";
|
|
25
25
|
export { QueueConsumer } from "./decorators/queue-consumer.js";
|
|
26
|
-
// Internals (for advanced usage / testing)
|
|
27
|
-
export { BindingRef } from "./binding-ref.js";
|
|
28
|
-
export { bindingsRegistry, clearBindingsRegistry } from "./tokens.js";
|
|
29
|
-
export { KV_BINDING_REF, D1_BINDING_REF, R2_BINDING_REF, QUEUE_BINDING_REF, DO_BINDING_REF, AI_BINDING_REF, VECTORIZE_BINDING_REF, HYPERDRIVE_BINDING_REF } from "./tokens.js";
|
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
import { MetadataRegistry } from "@velajs/vela";
|
|
2
|
-
import { BindingRef } from "../binding-ref.js";
|
|
3
|
-
import { AI_BINDING_REF, bindingsRegistry } from "../tokens.js";
|
|
4
1
|
import { AIService } from "../services/ai.service.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
value: `AIModule_${options.binding}`
|
|
13
|
-
});
|
|
14
|
-
MetadataRegistry.setModuleOptions(moduleClass, {
|
|
15
|
-
exports: [
|
|
16
|
-
AIService,
|
|
17
|
-
AI_BINDING_REF
|
|
18
|
-
]
|
|
19
|
-
});
|
|
20
|
-
return {
|
|
21
|
-
module: moduleClass,
|
|
22
|
-
providers: [
|
|
23
|
-
{
|
|
24
|
-
token: AI_BINDING_REF,
|
|
25
|
-
useValue: ref
|
|
26
|
-
},
|
|
27
|
-
AIService
|
|
28
|
-
]
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
2
|
+
import { AI_BINDING_REF } from "../tokens.js";
|
|
3
|
+
import { createBindingModule } from "./create-binding-module.js";
|
|
4
|
+
export const AIModule = createBindingModule({
|
|
5
|
+
name: 'AI',
|
|
6
|
+
serviceClass: AIService,
|
|
7
|
+
bindingRefToken: AI_BINDING_REF
|
|
8
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DynamicModule, type InjectionToken, type Type } from '@velajs/vela';
|
|
2
|
+
import { BindingRef } from '../binding-ref';
|
|
3
|
+
interface CreateBindingModuleOptions<TService> {
|
|
4
|
+
name: string;
|
|
5
|
+
serviceClass: Type<TService>;
|
|
6
|
+
bindingRefToken: InjectionToken<BindingRef>;
|
|
7
|
+
}
|
|
8
|
+
export interface BindingModuleStatic {
|
|
9
|
+
forRoot(options: {
|
|
10
|
+
binding: string;
|
|
11
|
+
}): DynamicModule;
|
|
12
|
+
}
|
|
13
|
+
export declare function createBindingModule<TService>(opts: CreateBindingModuleOptions<TService>): BindingModuleStatic;
|
|
14
|
+
export {};
|