@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.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js +8 -0
- package/dist/cloudflare-application.js +309 -0
- package/dist/cloudflare-application.js.map +1 -0
- package/dist/cloudflare-factory.js +109 -0
- package/dist/cloudflare-factory.js.map +1 -0
- package/dist/decorators/queue-consumer.js +39 -0
- package/dist/decorators/queue-consumer.js.map +1 -0
- package/dist/durable-objects.d.ts +3 -4
- package/dist/durable-objects.js +2 -524
- package/dist/environment.js +19 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +21 -1276
- package/dist/nonce/durable-object-nonce.store.js +33 -0
- package/dist/nonce/durable-object-nonce.store.js.map +1 -0
- package/dist/nonce/nonce-validation.js +18 -0
- package/dist/nonce/nonce-validation.js.map +1 -0
- package/dist/nonce/nonce.durable-object.js +54 -0
- package/dist/nonce/nonce.durable-object.js.map +1 -0
- package/dist/{nonce.durable-object-Df4CZi-0.d.ts → nonce.durable-object-Ci9AGssY.d.ts} +2 -3
- package/dist/queue/cloudflare-queues.js +215 -0
- package/dist/queue/cloudflare-queues.js.map +1 -0
- package/dist/queues.d.ts +1 -2
- package/dist/queues.js +1 -214
- package/dist/rate-limit/cloudflare-rate-limit.store.js +40 -0
- package/dist/rate-limit/cloudflare-rate-limit.store.js.map +1 -0
- package/dist/schedule-diagnostics.js +51 -0
- package/dist/schedule-diagnostics.js.map +1 -0
- package/dist/scheduled-event.js +68 -0
- package/dist/scheduled-event.js.map +1 -0
- package/dist/services/flagship-flag.driver.js +52 -0
- package/dist/services/flagship-flag.driver.js.map +1 -0
- package/dist/services/kv-cache.store.js +82 -0
- package/dist/services/kv-cache.store.js.map +1 -0
- package/dist/services/kv-flag.driver.js +62 -0
- package/dist/services/kv-flag.driver.js.map +1 -0
- package/dist/storage/r2-storage.driver.js +67 -0
- package/dist/storage/r2-storage.driver.js.map +1 -0
- package/dist/storage/storage-key-claim.js +74 -0
- package/dist/storage/storage-key-claim.js.map +1 -0
- package/dist/storage/storage-manager.service.js +38 -0
- package/dist/storage/storage-manager.service.js.map +1 -0
- package/dist/storage/storage.controller.js +64 -0
- package/dist/storage/storage.controller.js.map +1 -0
- package/dist/storage/storage.module.js +25 -0
- package/dist/storage/storage.module.js.map +1 -0
- package/dist/storage/storage.service.js +64 -0
- package/dist/storage/storage.service.js.map +1 -0
- package/dist/storage/storage.tokens.js +7 -0
- package/dist/storage/storage.tokens.js.map +1 -0
- package/dist/vela-env-DFvyoNT3.d.ts +1 -2
- package/dist/websocket/broadcast.js +32 -0
- package/dist/websocket/broadcast.js.map +1 -0
- package/dist/websocket/cf-room-registry.js +137 -0
- package/dist/websocket/cf-room-registry.js.map +1 -0
- package/dist/websocket/cf-ws-client.js +94 -0
- package/dist/websocket/cf-ws-client.js.map +1 -0
- package/dist/websocket/cloudflare-websocket.module.js +25 -0
- package/dist/websocket/cloudflare-websocket.module.js.map +1 -0
- package/dist/websocket/do-bootstrap.js +56 -0
- package/dist/websocket/do-bootstrap.js.map +1 -0
- package/dist/websocket/do-live.js +184 -0
- package/dist/websocket/do-live.js.map +1 -0
- package/dist/websocket/do-pitr.js +81 -0
- package/dist/websocket/do-pitr.js.map +1 -0
- package/dist/websocket/do-state.js +7 -0
- package/dist/websocket/do-state.js.map +1 -0
- package/dist/websocket/do-websocket-host.js +169 -0
- package/dist/websocket/do-websocket-host.js.map +1 -0
- package/dist/websocket/room-id.js +27 -0
- package/dist/websocket/room-id.js.map +1 -0
- package/dist/websocket/websocket-routing.js +133 -0
- package/dist/websocket/websocket-routing.js.map +1 -0
- package/dist/websocket/websocket.durable-object.js +138 -0
- package/dist/websocket/websocket.durable-object.js.map +1 -0
- package/dist/websocket/ws-attachment.js +79 -0
- package/dist/websocket/ws-attachment.js.map +1 -0
- package/dist/websocket/ws-server-holder.js +38 -0
- package/dist/websocket/ws-server-holder.js.map +1 -0
- package/package.json +9 -7
- package/dist/durable-objects.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/nonce-validation-Dy8z05A9.js +0 -522
- package/dist/nonce-validation-Dy8z05A9.js.map +0 -1
- 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 {
|
|
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,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-
|
|
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 };
|