@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +52 -39
  3. package/dist/binding-ref.d.ts +2 -9
  4. package/dist/binding-ref.js +6 -10
  5. package/dist/cloudflare-application.d.ts +6 -6
  6. package/dist/cloudflare-application.js +35 -19
  7. package/dist/cloudflare-factory.d.ts +5 -8
  8. package/dist/cloudflare-factory.js +37 -26
  9. package/dist/index.d.ts +1 -4
  10. package/dist/index.js +1 -5
  11. package/dist/modules/ai.module.d.ts +1 -6
  12. package/dist/modules/ai.module.js +7 -30
  13. package/dist/modules/create-binding-module.d.ts +14 -0
  14. package/dist/modules/create-binding-module.js +26 -0
  15. package/dist/modules/d1.module.d.ts +1 -6
  16. package/dist/modules/d1.module.js +7 -30
  17. package/dist/modules/durable-object.module.d.ts +1 -6
  18. package/dist/modules/durable-object.module.js +7 -30
  19. package/dist/modules/hyperdrive.module.d.ts +1 -6
  20. package/dist/modules/hyperdrive.module.js +7 -30
  21. package/dist/modules/kv.module.d.ts +1 -6
  22. package/dist/modules/kv.module.js +7 -30
  23. package/dist/modules/queue.module.d.ts +1 -6
  24. package/dist/modules/queue.module.js +7 -30
  25. package/dist/modules/r2.module.d.ts +1 -6
  26. package/dist/modules/r2.module.js +7 -30
  27. package/dist/modules/vectorize.module.d.ts +1 -6
  28. package/dist/modules/vectorize.module.js +7 -30
  29. package/dist/services/ai.service.d.ts +2 -12
  30. package/dist/services/ai.service.js +1 -4
  31. package/dist/services/d1.service.d.ts +2 -18
  32. package/dist/services/d1.service.js +1 -15
  33. package/dist/services/durable-object.service.d.ts +2 -20
  34. package/dist/services/durable-object.service.js +1 -16
  35. package/dist/services/hyperdrive.service.d.ts +2 -16
  36. package/dist/services/hyperdrive.service.js +1 -1
  37. package/dist/services/kv.service.d.ts +5 -18
  38. package/dist/services/kv.service.js +1 -16
  39. package/dist/services/queue.service.d.ts +3 -17
  40. package/dist/services/queue.service.js +1 -7
  41. package/dist/services/r2.service.d.ts +2 -14
  42. package/dist/services/r2.service.js +1 -16
  43. package/dist/services/vectorize.service.d.ts +2 -22
  44. package/dist/services/vectorize.service.js +1 -19
  45. package/dist/tokens.d.ts +0 -7
  46. package/dist/tokens.js +1 -9
  47. 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
+ [![npm version](https://img.shields.io/npm/v/@velajs/cloudflare)](https://www.npmjs.com/package/@velajs/cloudflare)
4
+ [![CI](https://github.com/velajs/cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/velajs/cloudflare/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/npm/l/@velajs/cloudflare)](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
- bun add @velajs/cloudflare @velajs/vela hono
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
  ```
@@ -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 CloudflareFactory middleware */
14
- _initialize(value: any): void;
6
+ /** @internal — called by createCloudflareApp middleware */
7
+ _initialize(value: T): void;
15
8
  }
@@ -1,10 +1,7 @@
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 {
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 CloudflareFactory.create() is used and a request has been made.`);
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 CloudflareFactory middleware */ // eslint-disable-next-line @typescript-eslint/no-explicit-any
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 { VelaApplication } from '@velajs/vela';
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 CloudflareFactory.create(AppModule);
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 CloudflareFactory.create(AppModule);
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 scheduledMeta = getScheduledMetadata(instance);
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
- const queueMeta = getQueueConsumerMetadata(instance);
43
- for (const meta of queueMeta){
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
- 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);
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
- 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);
84
+ await Promise.all(matching.map((h)=>invoke(h.instance, h.methodName, [
85
+ batch,
86
+ env,
87
+ ctx
88
+ ])));
73
89
  }
74
- /** Gracefully shut down the application. */ async close(signal) {
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
- * Factory for creating Cloudflare Worker applications.
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
- * first request and initializes all configured binding refs.
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 CloudflareFactory.create(AppModule);
11
+ * const app = await createCloudflareApp(AppModule);
13
12
  * export default app; // has .fetch, .scheduled, .queue
14
13
  * ```
15
14
  */
16
- export declare const CloudflareFactory: {
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
- import { bindingsRegistry } from "./tokens.js";
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
- * Factory for creating Cloudflare Worker applications.
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
- * first request and initializes all configured binding refs.
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 CloudflareFactory.create(AppModule);
29
+ * const app = await createCloudflareApp(AppModule);
14
30
  * export default app; // has .fetch, .scheduled, .queue
15
31
  * ```
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)=>{
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 ctx = c;
28
- const env = ctx.env ?? {};
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
- // 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
- };
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 { CloudflareFactory } from './cloudflare-factory';
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 { CloudflareFactory } from "./cloudflare-factory.js";
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,6 +1 @@
1
- import type { DynamicModule } from '@velajs/vela';
2
- export declare class AIModule {
3
- static forRoot(options: {
4
- binding: string;
5
- }): DynamicModule;
6
- }
1
+ export declare const AIModule: import("./create-binding-module").BindingModuleStatic;
@@ -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
- export class AIModule {
6
- static forRoot(options) {
7
- const ref = new BindingRef(options.binding);
8
- bindingsRegistry.push(ref);
9
- const moduleClass = class AIDynamicModule {
10
- };
11
- Object.defineProperty(moduleClass, 'name', {
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 {};