@velajs/cloudflare 1.29.0 → 1.30.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 (89) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  3. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/cloudflare-application.js +309 -0
  7. package/dist/cloudflare-application.js.map +1 -0
  8. package/dist/cloudflare-factory.js +109 -0
  9. package/dist/cloudflare-factory.js.map +1 -0
  10. package/dist/decorators/queue-consumer.js +39 -0
  11. package/dist/decorators/queue-consumer.js.map +1 -0
  12. package/dist/durable-objects.d.ts +3 -4
  13. package/dist/durable-objects.js +2 -524
  14. package/dist/environment.js +19 -0
  15. package/dist/environment.js.map +1 -0
  16. package/dist/index.d.ts +11 -9
  17. package/dist/index.js +21 -1276
  18. package/dist/nonce/durable-object-nonce.store.js +33 -0
  19. package/dist/nonce/durable-object-nonce.store.js.map +1 -0
  20. package/dist/nonce/nonce-validation.js +18 -0
  21. package/dist/nonce/nonce-validation.js.map +1 -0
  22. package/dist/nonce/nonce.durable-object.js +54 -0
  23. package/dist/nonce/nonce.durable-object.js.map +1 -0
  24. package/dist/{nonce.durable-object-Df4CZi-0.d.ts → nonce.durable-object-Ci9AGssY.d.ts} +2 -3
  25. package/dist/queue/cloudflare-queues.js +215 -0
  26. package/dist/queue/cloudflare-queues.js.map +1 -0
  27. package/dist/queues.d.ts +1 -2
  28. package/dist/queues.js +1 -214
  29. package/dist/rate-limit/cloudflare-rate-limit.store.js +40 -0
  30. package/dist/rate-limit/cloudflare-rate-limit.store.js.map +1 -0
  31. package/dist/schedule-diagnostics.js +51 -0
  32. package/dist/schedule-diagnostics.js.map +1 -0
  33. package/dist/scheduled-event.js +68 -0
  34. package/dist/scheduled-event.js.map +1 -0
  35. package/dist/services/flagship-flag.driver.js +52 -0
  36. package/dist/services/flagship-flag.driver.js.map +1 -0
  37. package/dist/services/kv-cache.store.js +82 -0
  38. package/dist/services/kv-cache.store.js.map +1 -0
  39. package/dist/services/kv-flag.driver.js +62 -0
  40. package/dist/services/kv-flag.driver.js.map +1 -0
  41. package/dist/storage/r2-storage.driver.js +67 -0
  42. package/dist/storage/r2-storage.driver.js.map +1 -0
  43. package/dist/storage/storage-key-claim.js +74 -0
  44. package/dist/storage/storage-key-claim.js.map +1 -0
  45. package/dist/storage/storage-manager.service.js +38 -0
  46. package/dist/storage/storage-manager.service.js.map +1 -0
  47. package/dist/storage/storage.controller.js +64 -0
  48. package/dist/storage/storage.controller.js.map +1 -0
  49. package/dist/storage/storage.module.js +25 -0
  50. package/dist/storage/storage.module.js.map +1 -0
  51. package/dist/storage/storage.service.js +64 -0
  52. package/dist/storage/storage.service.js.map +1 -0
  53. package/dist/storage/storage.tokens.js +7 -0
  54. package/dist/storage/storage.tokens.js.map +1 -0
  55. package/dist/vela-env-DFvyoNT3.d.ts +1 -2
  56. package/dist/websocket/broadcast.js +32 -0
  57. package/dist/websocket/broadcast.js.map +1 -0
  58. package/dist/websocket/cf-room-registry.js +137 -0
  59. package/dist/websocket/cf-room-registry.js.map +1 -0
  60. package/dist/websocket/cf-ws-client.js +94 -0
  61. package/dist/websocket/cf-ws-client.js.map +1 -0
  62. package/dist/websocket/cloudflare-websocket.module.js +25 -0
  63. package/dist/websocket/cloudflare-websocket.module.js.map +1 -0
  64. package/dist/websocket/do-bootstrap.js +56 -0
  65. package/dist/websocket/do-bootstrap.js.map +1 -0
  66. package/dist/websocket/do-live.js +184 -0
  67. package/dist/websocket/do-live.js.map +1 -0
  68. package/dist/websocket/do-pitr.js +81 -0
  69. package/dist/websocket/do-pitr.js.map +1 -0
  70. package/dist/websocket/do-state.js +7 -0
  71. package/dist/websocket/do-state.js.map +1 -0
  72. package/dist/websocket/do-websocket-host.js +169 -0
  73. package/dist/websocket/do-websocket-host.js.map +1 -0
  74. package/dist/websocket/room-id.js +27 -0
  75. package/dist/websocket/room-id.js.map +1 -0
  76. package/dist/websocket/websocket-routing.js +133 -0
  77. package/dist/websocket/websocket-routing.js.map +1 -0
  78. package/dist/websocket/websocket.durable-object.js +138 -0
  79. package/dist/websocket/websocket.durable-object.js.map +1 -0
  80. package/dist/websocket/ws-attachment.js +79 -0
  81. package/dist/websocket/ws-attachment.js.map +1 -0
  82. package/dist/websocket/ws-server-holder.js +38 -0
  83. package/dist/websocket/ws-server-holder.js.map +1 -0
  84. package/package.json +9 -7
  85. package/dist/durable-objects.js.map +0 -1
  86. package/dist/index.js.map +0 -1
  87. package/dist/nonce-validation-Dy8z05A9.js +0 -522
  88. package/dist/nonce-validation-Dy8z05A9.js.map +0 -1
  89. package/dist/queues.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 44023fc: `@velajs/cloudflare` exports only the Cloudflare adapter and its platform pieces, so each framework API has one import path. It requires `@velajs/vela` with the tiered entry points and imports its seams from `@velajs/vela/module-kit`, `@velajs/vela/schedule` and the other feature subpaths.
8
+
9
+ The package now publishes one JavaScript module per source file instead of shared chunks, so a bundler drops the features a Worker never imports. A shared chunk kept every decorated class it held, so a Worker that only calls `createCloudflareWorker()` shipped the storage module with the signed-URL crypto its controller uses, the KV and feature-flag drivers and the Durable Object host; it no longer does, which takes 6,975 bytes gzipped off that minimal Worker. Entry points, exports and type declarations are unchanged.
10
+
11
+ **Behavior change:** the WebSocket gateway API is no longer re-exported from `@velajs/cloudflare`. Import it from `@velajs/vela/websocket`:
12
+
13
+ | Old import | New import | Names |
14
+ |---|---|---|
15
+ | `@velajs/cloudflare` | `@velajs/vela/websocket` | `ConnectedSocket`, `MessageBody`, `OnGatewayConnection`, `OnGatewayDisconnect`, `OnGatewayInit`, `SubscribeMessage`, `UpgradeAuthenticator`, `WebSocketGateway`, `WebSocketServer`, `WebSocketUpgradeAuthenticationContext`, `WebSocketUpgradeIdentity`, `WsClient`, `WsException`, `WsMessage`, `WsResponse`, `WsServer` |
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [4d0342b]
20
+ - Updated dependencies [4467619]
21
+ - Updated dependencies [7372d90]
22
+ - Updated dependencies [c101033]
23
+ - Updated dependencies [4d0342b]
24
+ - @velajs/feature-flags@1.30.0
25
+ - @velajs/vela@1.30.0
26
+
3
27
  ## 1.29.0
4
28
 
5
29
  ### Minor Changes
package/README.md CHANGED
@@ -254,7 +254,8 @@ queue, cron and Durable Object code.
254
254
  ## Queues and cron
255
255
 
256
256
  ```ts
257
- import { Cron, InjectEnv, Injectable, type CronInvocation, type VelaEnv } from '@velajs/vela';
257
+ import { InjectEnv, Injectable, type VelaEnv } from '@velajs/vela';
258
+ import { Cron, type CronInvocation } from '@velajs/vela/schedule';
258
259
  import { QueueConsumer } from '@velajs/cloudflare';
259
260
 
260
261
  @Injectable()
@@ -0,0 +1,9 @@
1
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+ //#endregion
9
+ export { __decorate as default };
@@ -0,0 +1,6 @@
1
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
2
+ function __decorateMetadata(k, v) {
3
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
+ }
5
+ //#endregion
6
+ export { __decorateMetadata as default };
@@ -0,0 +1,8 @@
1
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
2
+ function __decorateParam(paramIndex, decorator) {
3
+ return function(target, key) {
4
+ decorator(target, key, paramIndex);
5
+ };
6
+ }
7
+ //#endregion
8
+ export { __decorateParam as default };
@@ -0,0 +1,309 @@
1
+ import { collectWsGatewayRoutes } from "./websocket/websocket-routing.js";
2
+ import { assertCloudflareEnvironment } from "./environment.js";
3
+ import { CLOUDFLARE_SCHEDULED_EVENT, cloudflareScheduledEvent } from "./scheduled-event.js";
4
+ import { PipelineRunner, buildEntrypointExecutionContext, getEntrypointModuleId, invokeScheduledJob, parseCronMetadata, resolveEntrypoint, resolveErrorReporter, resolveScopedComponentsAsync, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela/module-kit";
5
+ import { readWsEntrypointMeta } from "@velajs/vela/websocket";
6
+ //#region src/cloudflare-application.ts
7
+ function invoke(instance, methodName, args) {
8
+ const method = Reflect.get(instance, methodName);
9
+ if (typeof method !== "function") throw new Error(`Method '${String(methodName)}' is not a function on ${instance.constructor.name}`);
10
+ return Reflect.apply(method, instance, args);
11
+ }
12
+ function entrypointString(meta, property) {
13
+ if (typeof meta !== "object" || meta === null) throw new Error("Invalid entrypoint metadata.");
14
+ const value = Reflect.get(meta, property);
15
+ if (typeof value !== "string") throw new Error(`Invalid entrypoint metadata: ${property} must be a string.`);
16
+ return value;
17
+ }
18
+ /**
19
+ * `QueueModule`'s native consumer reports each failure of a batch once itself
20
+ * (processor failures where they ran, transport failures on settlement), so
21
+ * its rejection is not reported again here.
22
+ */
23
+ const SELF_REPORTING_KINDS = /* @__PURE__ */ new Set(["cf:queue:module"]);
24
+ const warnedRawJobs = /* @__PURE__ */ new Set();
25
+ /** The logical queue of a Vela job envelope, without importing the queue subsystem. */
26
+ function envelopeQueue(body) {
27
+ if (typeof body !== "object" || body === null) return void 0;
28
+ const queue = Reflect.get(body, "queue");
29
+ if (typeof queue !== "string" || typeof Reflect.get(body, "id") !== "string" || typeof Reflect.get(body, "name") !== "string" || !("data" in body)) return;
30
+ return queue;
31
+ }
32
+ /** Wait for every matching handler, even when one fails before its siblings. */
33
+ async function settleEntrypoints(work) {
34
+ const errors = (await Promise.allSettled(work)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
35
+ if (errors.length === 1) throw errors[0];
36
+ if (errors.length > 1) throw new AggregateError(errors, "Multiple entrypoint handlers failed.");
37
+ }
38
+ /**
39
+ * Wraps VelaApplication with Cloudflare-specific handlers:
40
+ * - `fetch` — HTTP request handler (from Hono)
41
+ * - `scheduled` — Cron trigger handler (runs the `@Cron()` jobs whose
42
+ * expression is the trigger's exact string)
43
+ * - `queue` — Queue consumer handler (`@QueueConsumer()` by physical queue,
44
+ * then `QueueModule`'s native consumer)
45
+ * - `mountOpenApi` — Serve an OpenAPI document (and optional Scalar UI) on
46
+ * the underlying Hono app
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const app = await createCloudflareApp(AppModule, { env });
51
+ * export default {
52
+ * fetch: app.fetch,
53
+ * scheduled: app.scheduled.bind(app),
54
+ * queue: app.queue.bind(app),
55
+ * };
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * // Serve OpenAPI docs alongside your routes
61
+ * const app = await createCloudflareApp(AppModule, { env });
62
+ * const document = createOpenApiDocument(AppModule);
63
+ * app.mountOpenApi({ document, ui: 'scalar' });
64
+ * // GET /openapi.json -> JSON document
65
+ * // GET /scalar -> Scalar UI (loads from CDN)
66
+ * ```
67
+ */
68
+ var CloudflareApplication = class {
69
+ env;
70
+ #wsGatewayRoutes = [];
71
+ #app;
72
+ /** Scheduled triggers in flight; close() aborts their signals and awaits them. */
73
+ #scheduled = /* @__PURE__ */ new Map();
74
+ constructor(app, env) {
75
+ this.env = env;
76
+ this.#app = app;
77
+ this.get = app.get.bind(app);
78
+ }
79
+ fetch = async (request, env, ctx) => {
80
+ assertCloudflareEnvironment(this.env, env);
81
+ return this.#app.fetch(request, env, ctx);
82
+ };
83
+ getHonoApp() {
84
+ return this.#app.getHonoApp();
85
+ }
86
+ /**
87
+ * Resolve a provider from the application's DI container (delegates to
88
+ * `VelaApplication.get`). Handy for grabbing a service — e.g. an auth service —
89
+ * to use inside `createCloudflareApp({ middleware: env => [...] })` request middleware,
90
+ * which runs outside the DI request pipeline.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const app = await createCloudflareApp(AppModule, { env });
95
+ * const auth = app.get(BetterAuthService);
96
+ * ```
97
+ */
98
+ get;
99
+ get entrypoints() {
100
+ return this.#app.entrypoints;
101
+ }
102
+ /**
103
+ * Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
104
+ * underlying Hono app. Delegates verbatim to `VelaApplication.mountOpenApi`,
105
+ * so the JSON endpoint defaults to `/openapi.json` and the Scalar UI (when
106
+ * opted in) defaults to `/scalar`. Edge-safe — the UI HTML loads Scalar from
107
+ * a CDN at runtime, nothing is bundled server-side.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { createOpenApiDocument } from '@velajs/vela/openapi';
112
+ *
113
+ * const app = await createCloudflareApp(AppModule, { env });
114
+ * const document = createOpenApiDocument(AppModule, {
115
+ * info: { title: 'My API', version: '1.0.0' },
116
+ * });
117
+ * app.mountOpenApi({ document, ui: 'scalar' });
118
+ * // GET /openapi.json -> { openapi: '3.1.0', ... }
119
+ * // GET /scalar -> Scalar UI HTML
120
+ * ```
121
+ */
122
+ mountOpenApi(options) {
123
+ this.#app.mountOpenApi(options);
124
+ return this;
125
+ }
126
+ /**
127
+ * @internal Upgrade routes come from validated gateway entrypoints, including
128
+ * request-scoped gateways without a bootstrap instance. Retain the instance
129
+ * scan for legacy applications that only declare forwarding metadata.
130
+ */
131
+ scanInstances(instances) {
132
+ const routes = /* @__PURE__ */ new Map();
133
+ for (const ep of this.#app.entrypoints.ofKind("websocket")) {
134
+ if (typeof ep.meta !== "object" || ep.meta === null || !("dispatcher" in ep.meta)) continue;
135
+ const meta = readWsEntrypointMeta(ep.meta);
136
+ if (meta.options.binding) routes.set(meta.path, {
137
+ path: meta.path,
138
+ binding: meta.options.binding,
139
+ options: { ...meta.options },
140
+ moduleId: meta.moduleId
141
+ });
142
+ }
143
+ const container = this.#app.getContainer();
144
+ for (const instance of instances) {
145
+ if (!instance || typeof instance !== "object") continue;
146
+ for (const route of collectWsGatewayRoutes(instance, container)) if (!routes.has(route.path)) routes.set(route.path, route);
147
+ }
148
+ this.#wsGatewayRoutes.splice(0, this.#wsGatewayRoutes.length, ...routes.values());
149
+ }
150
+ /** @internal — upgrade routes discovered from the application's gateways. */
151
+ getWsGatewayRoutes() {
152
+ return [...this.#wsGatewayRoutes];
153
+ }
154
+ /**
155
+ * Handle a Cloudflare cron trigger. Runs every core `@Cron()` job whose
156
+ * expression is exactly `event.cron` (the trigger string is compared as
157
+ * delivered, never re-evaluated) through `invokeScheduledJob`, the dispatch
158
+ * primitive every runtime shares. Each job receives only its
159
+ * `ScheduleInvocation`, runs in a fresh invocation scope seeded with
160
+ * {@link CLOUDFLARE_SCHEDULED_EVENT}, and honors signed `ScheduleModule`
161
+ * dispatch. The trigger settles after every matching job and its managed
162
+ * work (`EXECUTION_LIFETIME.waitUntil`/`defer`) settle; failures reject it.
163
+ */
164
+ async scheduled(event, env, _ctx) {
165
+ assertCloudflareEnvironment(this.env, env);
166
+ const jobs = this.#app.entrypoints.ofKind("schedule:cron", parseCronMetadata).filter((entry) => entry.meta.expression === event.cron);
167
+ if (jobs.length === 0) return;
168
+ const controller = new AbortController();
169
+ const scheduledTime = event.scheduledTime ?? Date.now();
170
+ const invocation = Object.freeze({
171
+ kind: "cron",
172
+ expression: event.cron,
173
+ scheduledTime,
174
+ signal: controller.signal
175
+ });
176
+ const trigger = cloudflareScheduledEvent(event, scheduledTime);
177
+ const container = this.#app.getContainer();
178
+ const running = settleEntrypoints(jobs.map((entry) => invokeScheduledJob(container, entry, invocation, { seed: (scope) => scope.setRequestInstance(CLOUDFLARE_SCHEDULED_EVENT, trigger) })));
179
+ this.#scheduled.set(running, controller);
180
+ try {
181
+ await running;
182
+ } finally {
183
+ this.#scheduled.delete(running);
184
+ }
185
+ }
186
+ /**
187
+ * Run one queue consumer inside a fresh request scope, through the shared
188
+ * guard → interceptor pipeline (components declared with
189
+ * `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
190
+ * method). HTTP-global components deliberately do NOT apply — an HTTP auth
191
+ * guard has no business rejecting a queue batch. Unclaimed errors are
192
+ * reported once (`QueueModule`'s consumer reports its own) and rethrow so
193
+ * the platform's retry semantics stay intact.
194
+ */
195
+ async dispatchEntrypoint(ep, payload, env, platformContext) {
196
+ const targetClass = ep.token;
197
+ if (typeof targetClass !== "function") throw new Error("Entrypoint token must be a class.");
198
+ if (ep.methodName === void 0) throw new Error("Entrypoint must declare a handler method.");
199
+ const methodName = ep.methodName;
200
+ const reportContext = {
201
+ edge: "queue",
202
+ source: `${targetClass.name}.${String(methodName)}`
203
+ };
204
+ let reported;
205
+ let delegated;
206
+ try {
207
+ await runInEntrypointScope(this.#app.getContainer(), async (scope, lifetime) => {
208
+ const moduleId = getEntrypointModuleId(scope, ep);
209
+ const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, payload, moduleId, scope);
210
+ const invocationContext = { waitUntil(promise) {
211
+ lifetime.waitUntil(promise);
212
+ platformContext.waitUntil(promise);
213
+ } };
214
+ let filters = [];
215
+ try {
216
+ filters = (await resolveScopedComponentsAsync("filter", targetClass, methodName, scope, moduleId)).toReversed();
217
+ const guards = await resolveScopedComponentsAsync("guard", targetClass, methodName, scope, moduleId);
218
+ const interceptors = await resolveScopedComponentsAsync("interceptor", targetClass, methodName, scope, moduleId);
219
+ await PipelineRunner.run({
220
+ context,
221
+ guards,
222
+ interceptors,
223
+ resolveArgs: async () => [
224
+ payload,
225
+ env,
226
+ invocationContext
227
+ ],
228
+ invoke: async (args) => {
229
+ const instance = await resolveEntrypoint(scope, ep);
230
+ if (typeof instance !== "object" || instance === null) throw new Error("Entrypoint must resolve to an object.");
231
+ try {
232
+ return await invoke(instance, methodName, args);
233
+ } catch (error) {
234
+ if (SELF_REPORTING_KINDS.has(ep.kind)) delegated = { error };
235
+ throw error;
236
+ }
237
+ }
238
+ });
239
+ } catch (error) {
240
+ if (delegated?.error !== error) resolveErrorReporter(scope).report(error, reportContext);
241
+ for (const filter of filters) if (shouldFilterCatch(filter, error)) {
242
+ await filter.catch(error, context);
243
+ return;
244
+ }
245
+ reported = { error };
246
+ throw error;
247
+ }
248
+ });
249
+ } catch (error) {
250
+ if (!reported || reported.error !== error) {
251
+ const completionError = reported && error instanceof AggregateError && error.errors[0] === reported.error ? error.errors[1] : error;
252
+ resolveErrorReporter(this.#app.getContainer()).report(completionError, reportContext);
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+ /**
258
+ * Handle Cloudflare Queue consumer events. `@QueueConsumer()` handlers read
259
+ * from `app.entrypoints` claim a batch by its physical queue name and
260
+ * receive it whole. A batch no handler claims goes to `QueueModule`'s native
261
+ * consumer (`cloudflareQueues()` from `@velajs/cloudflare/queues`), which
262
+ * routes each job by its logical queue. Each dispatch runs inside a fresh
263
+ * request-scoped child (request-scoped providers rebuild per batch — no
264
+ * boot-time captives). A batch nothing claims rejects: resolving would let
265
+ * Cloudflare acknowledge every message implicitly.
266
+ */
267
+ async queue(batch, env, ctx) {
268
+ assertCloudflareEnvironment(this.env, env);
269
+ const raw = this.#app.entrypoints.ofKind("cf:queue").filter((ep) => entrypointString(ep.meta, "queueName") === batch.queue);
270
+ if (raw.length > 0) this.warnRawJobs(batch);
271
+ const handlers = raw.length > 0 ? raw : this.#app.entrypoints.ofKind("cf:queue:module");
272
+ if (handlers.length === 0) throw new Error(`No consumer claims queue '${batch.queue}'. Add @QueueConsumer('${batch.queue}') to a provider, or deliver it through QueueModule.forRoot({ driver: cloudflareQueues() }) with a QueueModule.registerQueue() for each queue it carries. The batch is rejected unacknowledged, so Cloudflare retries it and then routes it to the configured dead-letter queue.`);
273
+ await settleEntrypoints(handlers.map((ep) => this.dispatchEntrypoint(ep, batch, env, ctx)));
274
+ }
275
+ /**
276
+ * A raw `@QueueConsumer` owns its physical queue's batches and must not carry
277
+ * jobs of a queue registered with `QueueModule`: those reach their
278
+ * `@Processor` only if the raw handler dispatches them itself, while
279
+ * `cloudflareQueues()` delivers registered queues. Warn once per physical and
280
+ * logical queue (unless diagnostics are silent); the raw consumer still
281
+ * receives and settles the batch.
282
+ */
283
+ warnRawJobs(batch) {
284
+ if (this.#app.getContainer().getDiagnostics() === "silent") return;
285
+ const registered = new Set(this.#app.entrypoints.ofKind("queue:registration").map((entry) => typeof entry.meta === "object" && entry.meta !== null ? Reflect.get(entry.meta, "name") : void 0));
286
+ if (registered.size === 0) return;
287
+ for (const message of batch.messages) {
288
+ const queue = envelopeQueue(typeof message === "object" && message !== null ? Reflect.get(message, "body") : void 0);
289
+ if (queue === void 0 || !registered.has(queue)) continue;
290
+ const warning = `[vela] @QueueConsumer('${batch.queue}') received jobs of registered queue '${queue}'. The raw consumer owns '${batch.queue}' and must not carry jobs of registered queues: they reach @Processor('${queue}') only if the raw handler dispatches them itself. Deliver '${queue}' through cloudflareQueues() instead: send it to a physical queue that no @QueueConsumer claims.`;
291
+ if (warnedRawJobs.has(warning)) continue;
292
+ warnedRawJobs.add(warning);
293
+ console.warn(warning);
294
+ }
295
+ }
296
+ /**
297
+ * Abort the signals of scheduled jobs still running, wait for them and their
298
+ * managed work to settle, then close the application.
299
+ */
300
+ async close(signal) {
301
+ for (const controller of this.#scheduled.values()) controller.abort();
302
+ await Promise.allSettled(this.#scheduled.keys());
303
+ return this.#app.close(signal);
304
+ }
305
+ };
306
+ //#endregion
307
+ export { CloudflareApplication };
308
+
309
+ //# sourceMappingURL=cloudflare-application.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-application.js","names":[],"sources":["../src/cloudflare-application.ts"],"sourcesContent":["import type { ExecutionContext } from 'hono';\nimport type { VelaApplication, ExceptionFilter, VelaEnv } from '@velajs/vela';\nimport {\n PipelineRunner,\n buildEntrypointExecutionContext,\n getEntrypointModuleId,\n invokeScheduledJob,\n parseCronMetadata,\n resolveEntrypoint,\n resolveScopedComponentsAsync,\n resolveErrorReporter,\n runInEntrypointScope,\n shouldFilterCatch,\n} from '@velajs/vela/module-kit';\nimport type { ScheduleInvocation } from '@velajs/vela/schedule';\nimport type { Entrypoint } from '@velajs/vela/module-kit';\nimport { readWsEntrypointMeta } from '@velajs/vela/websocket';\nimport { collectWsGatewayRoutes, type WsGatewayRoute } from './websocket/websocket-routing';\nimport { assertCloudflareEnvironment } from './environment';\nimport {\n CLOUDFLARE_SCHEDULED_EVENT,\n cloudflareScheduledEvent,\n type ScheduledEvent,\n} from './scheduled-event';\n\n/**\n * Options accepted by {@link CloudflareApplication.mountOpenApi}.\n *\n * Re-exposes vela's `MountOpenApiOptions` type — derived structurally from\n * the underlying `VelaApplication.mountOpenApi` signature so consumers don't\n * have to reach into vela's internal subpaths to type the argument.\n */\nexport type MountOpenApiOptions = Parameters<VelaApplication['mountOpenApi']>[0];\n\nfunction invoke(instance: object, methodName: string | symbol, args: unknown[]): unknown {\n // Decorator metadata names an instance method; inspect it before invoking.\n const method: unknown = Reflect.get(instance, methodName);\n if (typeof method !== 'function') {\n throw new Error(\n `Method '${String(methodName)}' is not a function on ${instance.constructor.name}`,\n );\n }\n return Reflect.apply(method, instance, args);\n}\n\nfunction entrypointString(meta: unknown, property: string): string {\n if (typeof meta !== 'object' || meta === null) throw new Error('Invalid entrypoint metadata.');\n const value: unknown = Reflect.get(meta, property);\n if (typeof value !== 'string')\n throw new Error(`Invalid entrypoint metadata: ${property} must be a string.`);\n return value;\n}\n\n/**\n * `QueueModule`'s native consumer reports each failure of a batch once itself\n * (processor failures where they ran, transport failures on settlement), so\n * its rejection is not reported again here.\n */\nconst SELF_REPORTING_KINDS = new Set(['cf:queue:module']);\n\nconst warnedRawJobs = new Set<string>();\n\n/** The logical queue of a Vela job envelope, without importing the queue subsystem. */\nfunction envelopeQueue(body: unknown): string | undefined {\n if (typeof body !== 'object' || body === null) return undefined;\n const queue: unknown = Reflect.get(body, 'queue');\n if (\n typeof queue !== 'string' ||\n typeof Reflect.get(body, 'id') !== 'string' ||\n typeof Reflect.get(body, 'name') !== 'string' ||\n !('data' in body)\n ) {\n return undefined;\n }\n return queue;\n}\n\n/** Wait for every matching handler, even when one fails before its siblings. */\nasync function settleEntrypoints(work: readonly Promise<void>[]): Promise<void> {\n const outcomes = await Promise.allSettled(work);\n const errors = outcomes.flatMap((outcome) =>\n outcome.status === 'rejected' ? [outcome.reason] : [],\n );\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) throw new AggregateError(errors, 'Multiple entrypoint handlers failed.');\n}\n\n/**\n * Wraps VelaApplication with Cloudflare-specific handlers:\n * - `fetch` — HTTP request handler (from Hono)\n * - `scheduled` — Cron trigger handler (runs the `@Cron()` jobs whose\n * expression is the trigger's exact string)\n * - `queue` — Queue consumer handler (`@QueueConsumer()` by physical queue,\n * then `QueueModule`'s native consumer)\n * - `mountOpenApi` — Serve an OpenAPI document (and optional Scalar UI) on\n * the underlying Hono app\n *\n * @example\n * ```ts\n * const app = await createCloudflareApp(AppModule, { env });\n * export default {\n * fetch: app.fetch,\n * scheduled: app.scheduled.bind(app),\n * queue: app.queue.bind(app),\n * };\n * ```\n *\n * @example\n * ```ts\n * // Serve OpenAPI docs alongside your routes\n * const app = await createCloudflareApp(AppModule, { env });\n * const document = createOpenApiDocument(AppModule);\n * app.mountOpenApi({ document, ui: 'scalar' });\n * // GET /openapi.json -> JSON document\n * // GET /scalar -> Scalar UI (loads from CDN)\n * ```\n */\nexport class CloudflareApplication {\n readonly #wsGatewayRoutes: WsGatewayRoute[] = [];\n readonly #app: VelaApplication;\n /** Scheduled triggers in flight; close() aborts their signals and awaits them. */\n readonly #scheduled = new Map<Promise<void>, AbortController>();\n\n constructor(\n app: VelaApplication,\n readonly env: VelaEnv,\n ) {\n this.#app = app;\n this.get = app.get.bind(app);\n }\n\n readonly fetch = async (\n request: Request,\n env: VelaEnv,\n ctx?: ExecutionContext,\n ): Promise<Response> => {\n assertCloudflareEnvironment(this.env, env);\n return this.#app.fetch(request, env, ctx);\n };\n\n getHonoApp(): ReturnType<VelaApplication['getHonoApp']> {\n return this.#app.getHonoApp();\n }\n\n /**\n * Resolve a provider from the application's DI container (delegates to\n * `VelaApplication.get`). Handy for grabbing a service — e.g. an auth service —\n * to use inside `createCloudflareApp({ middleware: env => [...] })` request middleware,\n * which runs outside the DI request pipeline.\n *\n * @example\n * ```ts\n * const app = await createCloudflareApp(AppModule, { env });\n * const auth = app.get(BetterAuthService);\n * ```\n */\n readonly get: VelaApplication['get'];\n\n get entrypoints(): VelaApplication['entrypoints'] {\n return this.#app.entrypoints;\n }\n\n /**\n * Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the\n * underlying Hono app. Delegates verbatim to `VelaApplication.mountOpenApi`,\n * so the JSON endpoint defaults to `/openapi.json` and the Scalar UI (when\n * opted in) defaults to `/scalar`. Edge-safe — the UI HTML loads Scalar from\n * a CDN at runtime, nothing is bundled server-side.\n *\n * @example\n * ```ts\n * import { createOpenApiDocument } from '@velajs/vela/openapi';\n *\n * const app = await createCloudflareApp(AppModule, { env });\n * const document = createOpenApiDocument(AppModule, {\n * info: { title: 'My API', version: '1.0.0' },\n * });\n * app.mountOpenApi({ document, ui: 'scalar' });\n * // GET /openapi.json -> { openapi: '3.1.0', ... }\n * // GET /scalar -> Scalar UI HTML\n * ```\n */\n mountOpenApi(options: MountOpenApiOptions): this {\n this.#app.mountOpenApi(options);\n return this;\n }\n\n /**\n * @internal Upgrade routes come from validated gateway entrypoints, including\n * request-scoped gateways without a bootstrap instance. Retain the instance\n * scan for legacy applications that only declare forwarding metadata.\n */\n scanInstances(instances: unknown[]): void {\n const routes = new Map<string, WsGatewayRoute>();\n for (const ep of this.#app.entrypoints.ofKind('websocket')) {\n if (typeof ep.meta !== 'object' || ep.meta === null || !('dispatcher' in ep.meta)) continue;\n const meta = readWsEntrypointMeta(ep.meta);\n if (meta.options.binding) {\n routes.set(meta.path, {\n path: meta.path,\n binding: meta.options.binding,\n options: { ...meta.options },\n moduleId: meta.moduleId,\n });\n }\n }\n const container = this.#app.getContainer();\n for (const instance of instances) {\n if (!instance || typeof instance !== 'object') continue;\n for (const route of collectWsGatewayRoutes(instance, container)) {\n if (!routes.has(route.path)) routes.set(route.path, route);\n }\n }\n this.#wsGatewayRoutes.splice(0, this.#wsGatewayRoutes.length, ...routes.values());\n }\n\n /** @internal — upgrade routes discovered from the application's gateways. */\n getWsGatewayRoutes(): WsGatewayRoute[] {\n return [...this.#wsGatewayRoutes];\n }\n\n /**\n * Handle a Cloudflare cron trigger. Runs every core `@Cron()` job whose\n * expression is exactly `event.cron` (the trigger string is compared as\n * delivered, never re-evaluated) through `invokeScheduledJob`, the dispatch\n * primitive every runtime shares. Each job receives only its\n * `ScheduleInvocation`, runs in a fresh invocation scope seeded with\n * {@link CLOUDFLARE_SCHEDULED_EVENT}, and honors signed `ScheduleModule`\n * dispatch. The trigger settles after every matching job and its managed\n * work (`EXECUTION_LIFETIME.waitUntil`/`defer`) settle; failures reject it.\n */\n async scheduled(\n event: ScheduledEvent,\n env: VelaEnv,\n _ctx?: { waitUntil: (promise: Promise<unknown>) => void },\n ): Promise<void> {\n assertCloudflareEnvironment(this.env, env);\n const jobs = this.#app.entrypoints\n .ofKind('schedule:cron', parseCronMetadata)\n .filter((entry) => entry.meta.expression === event.cron);\n if (jobs.length === 0) return;\n\n const controller = new AbortController();\n const scheduledTime = event.scheduledTime ?? Date.now();\n // Shared by every matching job, so none of them can alter what another sees.\n const invocation: ScheduleInvocation = Object.freeze({\n kind: 'cron' as const,\n expression: event.cron,\n scheduledTime,\n signal: controller.signal,\n });\n const trigger = cloudflareScheduledEvent(event, scheduledTime);\n const container = this.#app.getContainer();\n const running = settleEntrypoints(\n jobs.map((entry) =>\n invokeScheduledJob(container, entry, invocation, {\n seed: (scope) => scope.setRequestInstance(CLOUDFLARE_SCHEDULED_EVENT, trigger),\n }),\n ),\n );\n this.#scheduled.set(running, controller);\n try {\n await running;\n } finally {\n this.#scheduled.delete(running);\n }\n }\n\n /**\n * Run one queue consumer inside a fresh request scope, through the shared\n * guard → interceptor pipeline (components declared with\n * `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or\n * method). HTTP-global components deliberately do NOT apply — an HTTP auth\n * guard has no business rejecting a queue batch. Unclaimed errors are\n * reported once (`QueueModule`'s consumer reports its own) and rethrow so\n * the platform's retry semantics stay intact.\n */\n private async dispatchEntrypoint(\n ep: Entrypoint,\n payload: unknown,\n env: VelaEnv,\n platformContext: { waitUntil: (promise: Promise<unknown>) => void },\n ): Promise<void> {\n const targetClass = ep.token;\n if (typeof targetClass !== 'function') throw new Error('Entrypoint token must be a class.');\n if (ep.methodName === undefined) throw new Error('Entrypoint must declare a handler method.');\n const methodName = ep.methodName;\n const reportContext = {\n edge: 'queue' as const,\n source: `${targetClass.name}.${String(methodName)}`,\n };\n let reported: { error: unknown } | undefined;\n // A self-reporting handler's own rejection was already reported.\n let delegated: { error: unknown } | undefined;\n try {\n await runInEntrypointScope(this.#app.getContainer(), async (scope, lifetime) => {\n const moduleId = getEntrypointModuleId(scope, ep);\n const context = buildEntrypointExecutionContext(\n ep.kind,\n targetClass,\n methodName,\n payload,\n moduleId,\n scope,\n );\n const invocationContext = {\n waitUntil(promise: Promise<unknown>): void {\n lifetime.waitUntil(promise);\n platformContext.waitUntil(promise);\n },\n };\n let filters: ExceptionFilter[] = [];\n try {\n filters = (\n await resolveScopedComponentsAsync('filter', targetClass, methodName, scope, moduleId)\n ).toReversed();\n const guards = await resolveScopedComponentsAsync(\n 'guard',\n targetClass,\n methodName,\n scope,\n moduleId,\n );\n const interceptors = await resolveScopedComponentsAsync(\n 'interceptor',\n targetClass,\n methodName,\n scope,\n moduleId,\n );\n await PipelineRunner.run({\n context,\n guards,\n interceptors,\n resolveArgs: async () => [payload, env, invocationContext],\n invoke: async (args) => {\n const instance = await resolveEntrypoint(scope, ep);\n if (typeof instance !== 'object' || instance === null) {\n throw new Error('Entrypoint must resolve to an object.');\n }\n try {\n return await invoke(instance, methodName, args);\n } catch (error) {\n if (SELF_REPORTING_KINDS.has(ep.kind)) delegated = { error };\n throw error;\n }\n },\n });\n } catch (error) {\n if (delegated?.error !== error) {\n resolveErrorReporter(scope).report(error, reportContext);\n }\n for (const filter of filters) {\n if (shouldFilterCatch(filter, error)) {\n // Filters run closest-first; this is the framework catch hook.\n // eslint-disable-next-line no-await-in-loop, promise/valid-params\n await filter.catch(error, context);\n return;\n }\n }\n reported = { error };\n throw error;\n }\n });\n } catch (error) {\n // Managed completion happens after the handler's filter boundary. Report\n // a new completion failure without reporting an already-observed handler\n // failure twice; preserve both errors in the rejected invocation.\n if (!reported || reported.error !== error) {\n const completionError =\n reported && error instanceof AggregateError && error.errors[0] === reported.error\n ? error.errors[1]\n : error;\n resolveErrorReporter(this.#app.getContainer()).report(completionError, reportContext);\n }\n throw error;\n }\n }\n\n /**\n * Handle Cloudflare Queue consumer events. `@QueueConsumer()` handlers read\n * from `app.entrypoints` claim a batch by its physical queue name and\n * receive it whole. A batch no handler claims goes to `QueueModule`'s native\n * consumer (`cloudflareQueues()` from `@velajs/cloudflare/queues`), which\n * routes each job by its logical queue. Each dispatch runs inside a fresh\n * request-scoped child (request-scoped providers rebuild per batch — no\n * boot-time captives). A batch nothing claims rejects: resolving would let\n * Cloudflare acknowledge every message implicitly.\n */\n async queue(\n batch: { queue: string; messages: readonly unknown[] },\n env: VelaEnv,\n ctx: { waitUntil: (promise: Promise<unknown>) => void },\n ): Promise<void> {\n assertCloudflareEnvironment(this.env, env);\n const raw = this.#app.entrypoints\n .ofKind('cf:queue')\n .filter((ep) => entrypointString(ep.meta, 'queueName') === batch.queue);\n if (raw.length > 0) this.warnRawJobs(batch);\n const handlers = raw.length > 0 ? raw : this.#app.entrypoints.ofKind('cf:queue:module');\n\n if (handlers.length === 0) {\n throw new Error(\n `No consumer claims queue '${batch.queue}'. Add @QueueConsumer('${batch.queue}') to a ` +\n `provider, or deliver it through QueueModule.forRoot({ driver: cloudflareQueues() }) ` +\n `with a QueueModule.registerQueue() for each queue it carries. The batch is rejected ` +\n `unacknowledged, so Cloudflare retries it and then routes it to the configured ` +\n `dead-letter queue.`,\n );\n }\n\n await settleEntrypoints(handlers.map((ep) => this.dispatchEntrypoint(ep, batch, env, ctx)));\n }\n\n /**\n * A raw `@QueueConsumer` owns its physical queue's batches and must not carry\n * jobs of a queue registered with `QueueModule`: those reach their\n * `@Processor` only if the raw handler dispatches them itself, while\n * `cloudflareQueues()` delivers registered queues. Warn once per physical and\n * logical queue (unless diagnostics are silent); the raw consumer still\n * receives and settles the batch.\n */\n private warnRawJobs(batch: { queue: string; messages: readonly unknown[] }): void {\n if (this.#app.getContainer().getDiagnostics() === 'silent') return;\n const registered = new Set(\n this.#app.entrypoints\n .ofKind('queue:registration')\n .map((entry) =>\n typeof entry.meta === 'object' && entry.meta !== null\n ? Reflect.get(entry.meta, 'name')\n : undefined,\n ),\n );\n if (registered.size === 0) return;\n for (const message of batch.messages) {\n const body =\n typeof message === 'object' && message !== null ? Reflect.get(message, 'body') : undefined;\n const queue = envelopeQueue(body);\n if (queue === undefined || !registered.has(queue)) continue;\n const warning =\n `[vela] @QueueConsumer('${batch.queue}') received jobs of registered queue '${queue}'. ` +\n `The raw consumer owns '${batch.queue}' and must not carry jobs of registered queues: ` +\n `they reach @Processor('${queue}') only if the raw handler dispatches them itself. ` +\n `Deliver '${queue}' through cloudflareQueues() instead: send it to a physical queue ` +\n `that no @QueueConsumer claims.`;\n if (warnedRawJobs.has(warning)) continue;\n warnedRawJobs.add(warning);\n console.warn(warning);\n }\n }\n\n /**\n * Abort the signals of scheduled jobs still running, wait for them and their\n * managed work to settle, then close the application.\n */\n async close(signal?: string): Promise<void> {\n for (const controller of this.#scheduled.values()) controller.abort();\n await Promise.allSettled(this.#scheduled.keys());\n return this.#app.close(signal);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ import { registerWebSocketRoutes } from "./websocket/websocket-routing.js";
2
+ import { assertCloudflareEnvironment, registerCloudflareEnvironment } from "./environment.js";
3
+ import { registerScheduledEventSeed } from "./scheduled-event.js";
4
+ import { CloudflareApplication } from "./cloudflare-application.js";
5
+ import { reportCloudflareScheduleDiagnostics } from "./schedule-diagnostics.js";
6
+ import { warnWorkerLocalLive } from "./websocket/do-live.js";
7
+ import { getConnInfo } from "hono/cloudflare-workers";
8
+ import { VelaFactory } from "@velajs/vela";
9
+ //#region src/cloudflare-factory.ts
10
+ function readStrings(meta, property) {
11
+ const value = typeof meta === "object" && meta !== null ? Reflect.get(meta, property) : void 0;
12
+ if (typeof value === "string") return [value];
13
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) return value;
14
+ throw new TypeError(`Invalid queue consumer metadata: ${property}.`);
15
+ }
16
+ /**
17
+ * `@QueueConsumer` handlers own their physical queue. A `QueueModule`
18
+ * registration that pins the same physical queue with `consumer` would never
19
+ * see its batches, so bootstrap rejects the overlap.
20
+ */
21
+ function assertQueueConsumerOwnership(entrypoints) {
22
+ const pinned = new Set(entrypoints.ofKind("cf:queue:module").flatMap((entry) => readStrings(entry.meta, "consumers")));
23
+ for (const entry of entrypoints.ofKind("cf:queue")) {
24
+ const [queue] = readStrings(entry.meta, "queueName");
25
+ if (queue !== void 0 && pinned.has(queue)) throw new Error(`Ambiguous consumer ownership for queue '${queue}': @QueueConsumer('${queue}') and a QueueModule.registerQueue({ consumer: '${queue}' }) both claim it. Keep one owner.`);
26
+ }
27
+ }
28
+ /**
29
+ * Bind an application to one environment: seeded as the global ENV before
30
+ * provider factories and lifecycle hooks, and asserted on every request. The
31
+ * adapter also supplies the `InternalDispatcher` transport, so signed queue and
32
+ * schedule dispatch re-enter this application's routes, and reports schedule
33
+ * declarations a cron trigger cannot honor through the diagnostics policy.
34
+ */
35
+ function cloudflareAdapter(options) {
36
+ const { env } = options;
37
+ return {
38
+ name: "cloudflare",
39
+ requestMiddleware: [async (context, next) => {
40
+ assertCloudflareEnvironment(env, context.env);
41
+ await next();
42
+ }],
43
+ invocationTransport: ({ app }) => (request) => Promise.resolve(app.fetch(request, env)),
44
+ getClientIp: (c) => getConnInfo(c).remote.address ?? null,
45
+ configureContainer: (container) => {
46
+ registerCloudflareEnvironment(container, env);
47
+ registerScheduledEventSeed(container);
48
+ },
49
+ onBootstrap: async ({ app, container }) => {
50
+ assertQueueConsumerOwnership(app.entrypoints);
51
+ reportCloudflareScheduleDiagnostics(container, app.entrypoints);
52
+ await warnWorkerLocalLive(container);
53
+ }
54
+ };
55
+ }
56
+ /**
57
+ * Build an application for one native Workers environment. Call inside a platform event.
58
+ * The root is static: a module class or a `DynamicModule` declared at module scope.
59
+ * Read bindings in providers (`@InjectEnv()`) and module factories
60
+ * (`forRootAsync({ inject: [ENV], useFactory })`), which run for each application.
61
+ */
62
+ async function createCloudflareApp(rootModule, options) {
63
+ const velaApp = await VelaFactory.create(rootModule, {
64
+ globalPrefix: options.globalPrefix,
65
+ security: options.security,
66
+ middleware: options.middleware?.(options.env),
67
+ adapters: [cloudflareAdapter(options), ...options.adapters ?? []]
68
+ });
69
+ const app = new CloudflareApplication(velaApp, options.env);
70
+ app.scanInstances(velaApp.getInstances());
71
+ registerWebSocketRoutes(app.getHonoApp(), app.getWsGatewayRoutes(), velaApp.getContainer());
72
+ return app;
73
+ }
74
+ /**
75
+ * Worker entrypoint with one bootstrap per environment identity. Concurrent cold
76
+ * events share construction; failed construction is evicted so the next event
77
+ * can retry. Weak keys stop this cache from retaining a replaced environment.
78
+ */
79
+ function createCloudflareWorker(rootModule, options = {}) {
80
+ const applications = /* @__PURE__ */ new WeakMap();
81
+ const application = (env) => {
82
+ const existing = applications.get(env);
83
+ if (existing) return existing;
84
+ const pending = createCloudflareApp(rootModule, {
85
+ ...options,
86
+ env
87
+ });
88
+ applications.set(env, pending);
89
+ pending.catch(() => {
90
+ if (applications.get(env) === pending) applications.delete(env);
91
+ });
92
+ return pending;
93
+ };
94
+ return {
95
+ async fetch(request, env, ctx) {
96
+ return (await application(env)).fetch(request, env, ctx);
97
+ },
98
+ async scheduled(event, env, ctx) {
99
+ return (await application(env)).scheduled(event, env, ctx);
100
+ },
101
+ async queue(batch, env, ctx) {
102
+ return (await application(env)).queue(batch, env, ctx);
103
+ }
104
+ };
105
+ }
106
+ //#endregion
107
+ export { cloudflareAdapter, createCloudflareApp, createCloudflareWorker };
108
+
109
+ //# sourceMappingURL=cloudflare-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-factory.js","names":[],"sources":["../src/cloudflare-factory.ts"],"sourcesContent":["import type { ExecutionContext } from 'hono';\nimport { getConnInfo } from 'hono/cloudflare-workers';\nimport { VelaFactory } from '@velajs/vela';\nimport type {\n VelaApplication,\n VelaEnv,\n VelaMiddlewareHandler,\n VelaSecurityOptions,\n} from '@velajs/vela';\nimport type { RuntimeAdapter } from '@velajs/vela/module-kit';\nimport { CloudflareApplication } from './cloudflare-application';\nimport { assertCloudflareEnvironment, registerCloudflareEnvironment } from './environment';\nimport { reportCloudflareScheduleDiagnostics } from './schedule-diagnostics';\nimport { registerScheduledEventSeed, type ScheduledEvent } from './scheduled-event';\nimport { warnWorkerLocalLive } from './websocket/do-live';\nimport { registerWebSocketRoutes } from './websocket/websocket-routing';\nimport type { CloudflareRoot } from './root-module';\n\nexport interface CloudflareWorkerOptions {\n globalPrefix?: string;\n security?: VelaSecurityOptions;\n /** Build request middleware from the same native environment DI receives as ENV. */\n middleware?: (env: VelaEnv) => VelaMiddlewareHandler[];\n /** Further runtime adapters, composed after the Cloudflare adapter for each application. */\n adapters?: RuntimeAdapter[];\n}\n\nexport interface CreateCloudflareAppOptions extends CloudflareWorkerOptions {\n /** Supply the platform environment inside fetch/queue/scheduled or a DO constructor. */\n env: VelaEnv;\n}\n\nfunction readStrings(meta: unknown, property: string): string[] {\n const value: unknown =\n typeof meta === 'object' && meta !== null ? Reflect.get(meta, property) : undefined;\n if (typeof value === 'string') return [value];\n if (Array.isArray(value) && value.every((item) => typeof item === 'string')) return value;\n throw new TypeError(`Invalid queue consumer metadata: ${property}.`);\n}\n\n/**\n * `@QueueConsumer` handlers own their physical queue. A `QueueModule`\n * registration that pins the same physical queue with `consumer` would never\n * see its batches, so bootstrap rejects the overlap.\n */\nfunction assertQueueConsumerOwnership(entrypoints: VelaApplication['entrypoints']): void {\n const pinned = new Set(\n entrypoints.ofKind('cf:queue:module').flatMap((entry) => readStrings(entry.meta, 'consumers')),\n );\n for (const entry of entrypoints.ofKind('cf:queue')) {\n const [queue] = readStrings(entry.meta, 'queueName');\n if (queue !== undefined && pinned.has(queue)) {\n throw new Error(\n `Ambiguous consumer ownership for queue '${queue}': @QueueConsumer('${queue}') and a ` +\n `QueueModule.registerQueue({ consumer: '${queue}' }) both claim it. Keep one owner.`,\n );\n }\n }\n}\n\n/**\n * Bind an application to one environment: seeded as the global ENV before\n * provider factories and lifecycle hooks, and asserted on every request. The\n * adapter also supplies the `InternalDispatcher` transport, so signed queue and\n * schedule dispatch re-enter this application's routes, and reports schedule\n * declarations a cron trigger cannot honor through the diagnostics policy.\n */\nexport function cloudflareAdapter(options: { env: VelaEnv }): RuntimeAdapter {\n const { env } = options;\n return {\n name: 'cloudflare',\n requestMiddleware: [\n async (context, next) => {\n assertCloudflareEnvironment(env, context.env);\n await next();\n },\n ],\n invocationTransport:\n ({ app }) =>\n (request) =>\n Promise.resolve(app.fetch(request, env)),\n getClientIp: (c) => getConnInfo(c).remote.address ?? null,\n configureContainer: (container) => {\n registerCloudflareEnvironment(container, env);\n registerScheduledEventSeed(container);\n },\n onBootstrap: async ({ app, container }) => {\n assertQueueConsumerOwnership(app.entrypoints);\n reportCloudflareScheduleDiagnostics(container, app.entrypoints);\n await warnWorkerLocalLive(container);\n },\n };\n}\n\n/**\n * Build an application for one native Workers environment. Call inside a platform event.\n * The root is static: a module class or a `DynamicModule` declared at module scope.\n * Read bindings in providers (`@InjectEnv()`) and module factories\n * (`forRootAsync({ inject: [ENV], useFactory })`), which run for each application.\n */\nexport async function createCloudflareApp(\n rootModule: CloudflareRoot,\n options: CreateCloudflareAppOptions,\n): Promise<CloudflareApplication> {\n const velaApp = await VelaFactory.create(rootModule, {\n globalPrefix: options.globalPrefix,\n security: options.security,\n middleware: options.middleware?.(options.env),\n adapters: [cloudflareAdapter(options), ...(options.adapters ?? [])],\n });\n const app = new CloudflareApplication(velaApp, options.env);\n app.scanInstances(velaApp.getInstances());\n registerWebSocketRoutes(app.getHonoApp(), app.getWsGatewayRoutes(), velaApp.getContainer());\n return app;\n}\n\n/**\n * Worker entrypoint with one bootstrap per environment identity. Concurrent cold\n * events share construction; failed construction is evicted so the next event\n * can retry. Weak keys stop this cache from retaining a replaced environment.\n */\nexport function createCloudflareWorker(\n rootModule: CloudflareRoot,\n options: CloudflareWorkerOptions = {},\n) {\n const applications = new WeakMap<VelaEnv, Promise<CloudflareApplication>>();\n const application = (env: VelaEnv): Promise<CloudflareApplication> => {\n const existing = applications.get(env);\n if (existing) return existing;\n const pending = createCloudflareApp(rootModule, { ...options, env });\n applications.set(env, pending);\n void pending.catch(() => {\n if (applications.get(env) === pending) applications.delete(env);\n });\n return pending;\n };\n return {\n async fetch(request: Request, env: VelaEnv, ctx: ExecutionContext): Promise<Response> {\n return (await application(env)).fetch(request, env, ctx);\n },\n async scheduled(\n event: ScheduledEvent,\n env: VelaEnv,\n ctx?: { waitUntil: (promise: Promise<unknown>) => void },\n ): Promise<void> {\n return (await application(env)).scheduled(event, env, ctx);\n },\n async queue(\n batch: { queue: string; messages: readonly unknown[] },\n env: VelaEnv,\n ctx: { waitUntil: (promise: Promise<unknown>) => void },\n ): Promise<void> {\n return (await application(env)).queue(batch, env, ctx);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { defineMetadata, getMetadata, registerEntrypointKind } from "@velajs/vela/module-kit";
2
+ //#region src/decorators/queue-consumer.ts
3
+ const QUEUE_CONSUMER_METADATA_KEY = "cloudflare:queue-consumer";
4
+ registerEntrypointKind({
5
+ kind: "cf:queue",
6
+ metaKey: QUEUE_CONSUMER_METADATA_KEY,
7
+ level: "method"
8
+ });
9
+ /**
10
+ * Marks a method as a queue consumer handler.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * @Injectable()
15
+ * class WorkerService {
16
+ * @QueueConsumer('email-queue')
17
+ * async processEmails(batch: MessageBatch) {
18
+ * for (const msg of batch.messages) {
19
+ * console.log('Processing:', msg.body);
20
+ * msg.ack();
21
+ * }
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ function QueueConsumer(queueName) {
27
+ return (target, propertyKey, _descriptor) => {
28
+ const existing = getMetadata(QUEUE_CONSUMER_METADATA_KEY, target.constructor) ?? [];
29
+ existing.push({
30
+ queueName,
31
+ methodName: String(propertyKey)
32
+ });
33
+ defineMetadata(QUEUE_CONSUMER_METADATA_KEY, existing, target.constructor);
34
+ };
35
+ }
36
+ //#endregion
37
+ export { QueueConsumer };
38
+
39
+ //# sourceMappingURL=queue-consumer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-consumer.js","names":[],"sources":["../../src/decorators/queue-consumer.ts"],"sourcesContent":["import { defineMetadata, getMetadata, registerEntrypointKind } from '@velajs/vela/module-kit';\n\nconst QUEUE_CONSUMER_METADATA_KEY = 'cloudflare:queue-consumer';\n\n// Open entrypoint kind: any adapter can enumerate queue consumers via\n// `app.entrypoints.ofKind('cf:queue')` — declared here, next to the decorator,\n// with zero vela-core involvement.\nregisterEntrypointKind({ kind: 'cf:queue', metaKey: QUEUE_CONSUMER_METADATA_KEY, level: 'method' });\n\nexport interface QueueConsumerMetadata {\n queueName: string;\n methodName: string;\n}\n\n/**\n * Marks a method as a queue consumer handler.\n *\n * @example\n * ```ts\n * @Injectable()\n * class WorkerService {\n * @QueueConsumer('email-queue')\n * async processEmails(batch: MessageBatch) {\n * for (const msg of batch.messages) {\n * console.log('Processing:', msg.body);\n * msg.ack();\n * }\n * }\n * }\n * ```\n */\nexport function QueueConsumer(queueName: string): MethodDecorator {\n return (target: object, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {\n const existing: QueueConsumerMetadata[] =\n (getMetadata(QUEUE_CONSUMER_METADATA_KEY, target.constructor) as QueueConsumerMetadata[]) ??\n [];\n existing.push({ queueName, methodName: String(propertyKey) });\n defineMetadata(QUEUE_CONSUMER_METADATA_KEY, existing, target.constructor);\n };\n}\n\nexport function getQueueConsumerMetadata(target: object): QueueConsumerMetadata[] {\n const ctor = target.constructor ?? target;\n return (getMetadata(QUEUE_CONSUMER_METADATA_KEY, ctor) as QueueConsumerMetadata[]) ?? [];\n}\n"],"mappings":";;AAEA,MAAM,8BAA8B;AAKpC,uBAAuB;CAAE,MAAM;CAAY,SAAS;CAA6B,OAAO;AAAS,CAAC;;;;;;;;;;;;;;;;;;AAwBlG,SAAgB,cAAc,WAAoC;CAChE,QAAQ,QAAgB,aAA8B,gBAAoC;EACxF,MAAM,WACH,YAAY,6BAA6B,OAAO,WAAW,KAC5D,CAAC;EACH,SAAS,KAAK;GAAE;GAAW,YAAY,OAAO,WAAW;EAAE,CAAC;EAC5D,eAAe,6BAA6B,UAAU,OAAO,WAAW;CAC1E;AACF"}
@@ -1,9 +1,9 @@
1
1
  import "./vela-env-DFvyoNT3.js";
2
- import { l as VelaDoPitrRpc, p as CloudflareRoot, t as VelaNonceDurableObject } from "./nonce.durable-object-Df4CZi-0.js";
2
+ import { l as VelaDoPitrRpc, p as CloudflareRoot, t as VelaNonceDurableObject } from "./nonce.durable-object-Ci9AGssY.js";
3
+ import { DurableObject } from "cloudflare:workers";
3
4
  import { VelaEnv } from "@velajs/vela";
4
5
  import { BroadcastCommand } from "@velajs/vela/websocket";
5
6
  import { CommitStamp, InvalidationCommand, LiveInspection } from "@velajs/vela/live";
6
- import { DurableObject } from "cloudflare:workers";
7
7
  //#region src/websocket/websocket.durable-object.d.ts
8
8
  /**
9
9
  * Base class for the WebSocket Durable Object. The user exports a named subclass
@@ -25,5 +25,4 @@ export declare function VelaWebSocketDurableObject(rootModule: CloudflareRoot):
25
25
  inspectLive(): Promise<LiveInspection>;
26
26
  };
27
27
  //#endregion
28
- export { VelaNonceDurableObject };
29
- //# sourceMappingURL=durable-objects.d.ts.map
28
+ export { VelaNonceDurableObject };