@velajs/cloudflare 1.22.1 → 1.23.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/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
- import { C as roomToDurableId, _ as resolveCloudflareRoot, a as durableObjectLive, c as liveInvalidateToRoom, d as WsServerHolder, f as DoPitrUnavailableError, g as __decorate, h as readDoPitrBookmark, i as durableObjectCursorLog, m as isDoPitrUnavailable, n as isValidExpiry, p as armDoPitr, r as DoCursorLog, t as isCanonicalBoundedText, v as assertCloudflareEnvironment, x as durableObjectRoomName, y as registerCloudflareEnvironment } from "./nonce-validation-LE0vFk-7.js";
1
+ import { C as durableObjectRoomName, T as roomToDurableId, _ as readDoPitrBookmark, a as durableObjectLive, b as assertCloudflareEnvironment, c as liveInvalidateToRoom, g as isDoPitrUnavailable, h as armDoPitr, i as durableObjectCursorLog, m as DoPitrUnavailableError, n as isValidExpiry, p as WsServerHolder, r as DoCursorLog, t as isCanonicalBoundedText, v as __decorate, x as registerCloudflareEnvironment, y as resolveCloudflareRoot } from "./nonce-validation-Bcqf3FvY.js";
2
2
  import { getConnInfo } from "hono/cloudflare-workers";
3
- import { CRON_METADATA, ConfigurableModuleBuilder, Controller, Get, Inject, Injectable, InjectionToken, Module, PipelineRunner, Req, VelaFactory, buildEntrypointExecutionContext, createParamDecorator, defineMetadata, defineProvider, getMetadata, getTrustedRequestIdentity, registerEntrypointKind, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela";
4
- import { ComponentManager } from "@velajs/vela/internal";
5
- import { ConnectedSocket, DEFAULT_WS_MAX_FRAME_BYTES, MessageBody, SubscribeMessage, WS_GATEWAY_METADATA, WS_SERVER, WebSocketGateway, WebSocketServer, WsDispatcher, WsException, assertBroadcastCommandFits, authenticateWebSocketUpgrade, resolveGatewayRoomId, resolveGatewayRoomParam, resolveMaxFrameBytes } from "@velajs/vela/websocket";
3
+ import { CRON_METADATA, ConfigurableModuleBuilder, Controller, Get, Inject, Injectable, InjectionToken, Module, PipelineRunner, Req, VelaFactory, buildEntrypointExecutionContext, createParamDecorator, defineMetadata, defineProvider, getEntrypointModuleId, getMetadata, getTrustedRequestIdentity, parseCron, registerEntrypointKind, resolveEntrypoint, resolveErrorReporter, resolveScopedComponentsAsync, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela";
4
+ import { ConnectedSocket, DEFAULT_WS_MAX_FRAME_BYTES, MessageBody, SubscribeMessage, WS_GATEWAY_METADATA, WS_SERVER, WebSocketGateway, WebSocketServer, WsDispatcher, WsException, assertBroadcastCommandFits, authenticateWebSocketUpgrade, readWsEntrypointMeta, resolveGatewayRoomId, resolveGatewayRoomParam, resolveMaxFrameBytes } from "@velajs/vela/websocket";
6
5
  import { STORAGE_SIGNED_URL_PURPOSE, joinStoragePath, signUrl, verifySignedUrl } from "@velajs/vela/storage";
7
6
  //#region src/websocket/websocket-routing.ts
8
7
  const MAX_IDENTITY_FIELD_BYTES = 2048;
@@ -123,7 +122,7 @@ registerEntrypointKind({
123
122
  });
124
123
  function invoke(instance, methodName, args) {
125
124
  const method = Reflect.get(instance, methodName);
126
- if (typeof method !== "function") throw new Error(`Method '${methodName}' is not a function on ${instance.constructor.name}`);
125
+ if (typeof method !== "function") throw new Error(`Method '${String(methodName)}' is not a function on ${instance.constructor.name}`);
127
126
  return Reflect.apply(method, instance, args);
128
127
  }
129
128
  function entrypointString(meta, property) {
@@ -132,6 +131,12 @@ function entrypointString(meta, property) {
132
131
  if (typeof value !== "string") throw new Error(`Invalid entrypoint metadata: ${property} must be a string.`);
133
132
  return value;
134
133
  }
134
+ /** Wait for every matching handler, even when one fails before its siblings. */
135
+ async function settleEntrypoints(work) {
136
+ const errors = (await Promise.allSettled(work)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
137
+ if (errors.length === 1) throw errors[0];
138
+ if (errors.length > 1) throw new AggregateError(errors, "Multiple entrypoint handlers failed.");
139
+ }
135
140
  /**
136
141
  * Wraps VelaApplication with Cloudflare-specific handlers:
137
142
  * - `fetch` — HTTP request handler (from Hono)
@@ -162,20 +167,20 @@ function entrypointString(meta, property) {
162
167
  * ```
163
168
  */
164
169
  var CloudflareApplication = class {
165
- app;
166
170
  env;
167
- wsGatewayRoutes = [];
171
+ #wsGatewayRoutes = [];
172
+ #app;
168
173
  constructor(app, env) {
169
- this.app = app;
170
174
  this.env = env;
175
+ this.#app = app;
171
176
  this.get = app.get.bind(app);
172
177
  }
173
178
  fetch = async (request, env, ctx) => {
174
179
  assertCloudflareEnvironment(this.env, env);
175
- return this.app.fetch(request, env, ctx);
180
+ return this.#app.fetch(request, env, ctx);
176
181
  };
177
182
  getHonoApp() {
178
- return this.app.getHonoApp();
183
+ return this.#app.getHonoApp();
179
184
  }
180
185
  /**
181
186
  * Resolve a provider from the application's DI container (delegates to
@@ -191,7 +196,7 @@ var CloudflareApplication = class {
191
196
  */
192
197
  get;
193
198
  get entrypoints() {
194
- return this.app.entrypoints;
199
+ return this.#app.entrypoints;
195
200
  }
196
201
  /**
197
202
  * Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
@@ -214,24 +219,34 @@ var CloudflareApplication = class {
214
219
  * ```
215
220
  */
216
221
  mountOpenApi(options) {
217
- this.app.mountOpenApi(options);
222
+ this.#app.mountOpenApi(options);
218
223
  return this;
219
224
  }
220
225
  /**
221
- * @internal — scans instances for `@WebSocketGateway({ path, binding })`
222
- * upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
223
- * come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
224
- * kinds) at dispatch time.
226
+ * @internal Upgrade routes come from validated gateway entrypoints, including
227
+ * request-scoped gateways without a bootstrap instance. Retain the instance
228
+ * scan for legacy applications that only declare forwarding metadata.
225
229
  */
226
230
  scanInstances(instances) {
231
+ const routes = /* @__PURE__ */ new Map();
232
+ for (const ep of this.#app.entrypoints.ofKind("websocket")) {
233
+ if (typeof ep.meta !== "object" || ep.meta === null || !("dispatcher" in ep.meta)) continue;
234
+ const meta = readWsEntrypointMeta(ep.meta);
235
+ if (meta.options.binding) routes.set(meta.path, {
236
+ path: meta.path,
237
+ binding: meta.options.binding,
238
+ options: { ...meta.options }
239
+ });
240
+ }
227
241
  for (const instance of instances) {
228
242
  if (!instance || typeof instance !== "object") continue;
229
- this.wsGatewayRoutes.push(...collectWsGatewayRoutes(instance));
243
+ for (const route of collectWsGatewayRoutes(instance)) if (!routes.has(route.path)) routes.set(route.path, route);
230
244
  }
245
+ this.#wsGatewayRoutes.splice(0, this.#wsGatewayRoutes.length, ...routes.values());
231
246
  }
232
- /** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */
247
+ /** @internal — upgrade routes discovered from the application's gateways. */
233
248
  getWsGatewayRoutes() {
234
- return this.wsGatewayRoutes;
249
+ return [...this.#wsGatewayRoutes];
235
250
  }
236
251
  /**
237
252
  * Handle Cloudflare scheduled (cron) events.
@@ -241,18 +256,13 @@ var CloudflareApplication = class {
241
256
  */
242
257
  async scheduled(event, env, ctx) {
243
258
  assertCloudflareEnvironment(this.env, env);
244
- const handlers = [...this.app.entrypoints.ofKind("cf:scheduled").map((ep) => ({
259
+ await settleEntrypoints([...this.#app.entrypoints.ofKind("cf:scheduled").map((ep) => ({
245
260
  ep,
246
261
  cron: entrypointString(ep.meta, "cron")
247
- })), ...this.app.entrypoints.ofKind("cf:vela-cron").map((ep) => ({
262
+ })), ...this.#app.entrypoints.ofKind("cf:vela-cron").map((ep) => ({
248
263
  ep,
249
264
  cron: entrypointString(ep.meta, "expression")
250
- }))].filter((h) => h.cron === event.cron);
251
- await Promise.all(handlers.map(({ ep }) => this.dispatchEntrypoint(ep, [
252
- event,
253
- env,
254
- ctx
255
- ])));
265
+ }))].filter((h) => h.cron === event.cron).map(({ ep }) => this.dispatchEntrypoint(ep, event, env, ctx)));
256
266
  }
257
267
  /**
258
268
  * Run one entrypoint handler inside a fresh request scope, through the
@@ -262,33 +272,61 @@ var CloudflareApplication = class {
262
272
  * guard has no business rejecting a queue batch. Unclaimed errors rethrow
263
273
  * so the platform's retry semantics stay intact.
264
274
  */
265
- async dispatchEntrypoint(ep, args) {
275
+ async dispatchEntrypoint(ep, payload, env, platformContext) {
266
276
  const targetClass = ep.token;
267
277
  if (typeof targetClass !== "function") throw new Error("Entrypoint token must be a class.");
268
- const methodName = String(ep.methodName);
269
- const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, args[0]);
270
- await runInEntrypointScope(this.app.getContainer(), async (scope) => {
271
- const instance = scope.resolve(ep.token);
272
- if (typeof instance !== "object" || instance === null) throw new Error("Entrypoint must resolve to an object.");
273
- const guards = ComponentManager.resolveGuards(ComponentManager.getScopedComponents("guard", targetClass, methodName), scope);
274
- const interceptors = ComponentManager.resolveInterceptors(ComponentManager.getScopedComponents("interceptor", targetClass, methodName), scope);
275
- const filters = ComponentManager.resolveFilters([...ComponentManager.getScopedComponents("filter", targetClass, methodName)].reverse(), scope);
276
- try {
277
- await PipelineRunner.run({
278
- context,
279
- guards,
280
- interceptors,
281
- resolveArgs: async () => args,
282
- invoke: async (resolved) => invoke(instance, methodName, resolved)
283
- });
284
- } catch (error) {
285
- for (const filter of filters) if (shouldFilterCatch(filter, error)) {
286
- await filter.catch(error, context);
287
- return;
278
+ if (ep.methodName === void 0) throw new Error("Entrypoint must declare a handler method.");
279
+ const methodName = ep.methodName;
280
+ const reportContext = {
281
+ edge: ep.kind === "cf:queue" ? "queue" : "schedule",
282
+ source: `${targetClass.name}.${String(methodName)}`
283
+ };
284
+ let reported;
285
+ try {
286
+ await runInEntrypointScope(this.#app.getContainer(), async (scope, lifetime) => {
287
+ const moduleId = getEntrypointModuleId(scope, ep);
288
+ const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, payload, moduleId, scope);
289
+ const invocationContext = { waitUntil(promise) {
290
+ lifetime.waitUntil(promise);
291
+ platformContext.waitUntil(promise);
292
+ } };
293
+ let filters = [];
294
+ try {
295
+ filters = (await resolveScopedComponentsAsync("filter", targetClass, methodName, scope, moduleId)).toReversed();
296
+ const guards = await resolveScopedComponentsAsync("guard", targetClass, methodName, scope, moduleId);
297
+ const interceptors = await resolveScopedComponentsAsync("interceptor", targetClass, methodName, scope, moduleId);
298
+ await PipelineRunner.run({
299
+ context,
300
+ guards,
301
+ interceptors,
302
+ resolveArgs: async () => [
303
+ payload,
304
+ env,
305
+ invocationContext
306
+ ],
307
+ invoke: async (args) => {
308
+ const instance = await resolveEntrypoint(scope, ep);
309
+ if (typeof instance !== "object" || instance === null) throw new Error("Entrypoint must resolve to an object.");
310
+ return invoke(instance, methodName, args);
311
+ }
312
+ });
313
+ } catch (error) {
314
+ resolveErrorReporter(scope).report(error, reportContext);
315
+ for (const filter of filters) if (shouldFilterCatch(filter, error)) {
316
+ await filter.catch(error, context);
317
+ return;
318
+ }
319
+ reported = { error };
320
+ throw error;
288
321
  }
289
- throw error;
322
+ });
323
+ } catch (error) {
324
+ if (!reported || reported.error !== error) {
325
+ const completionError = reported && error instanceof AggregateError && error.errors[0] === reported.error ? error.errors[1] : error;
326
+ resolveErrorReporter(this.#app.getContainer()).report(completionError, reportContext);
290
327
  }
291
- });
328
+ throw error;
329
+ }
292
330
  }
293
331
  /**
294
332
  * Handle Cloudflare Queue consumer events.
@@ -298,15 +336,10 @@ var CloudflareApplication = class {
298
336
  */
299
337
  async queue(batch, env, ctx) {
300
338
  assertCloudflareEnvironment(this.env, env);
301
- const handlers = this.app.entrypoints.ofKind("cf:queue").filter((ep) => entrypointString(ep.meta, "queueName") === batch.queue);
302
- await Promise.all(handlers.map((ep) => this.dispatchEntrypoint(ep, [
303
- batch,
304
- env,
305
- ctx
306
- ])));
339
+ await settleEntrypoints(this.#app.entrypoints.ofKind("cf:queue").filter((ep) => entrypointString(ep.meta, "queueName") === batch.queue).map((ep) => this.dispatchEntrypoint(ep, batch, env, ctx)));
307
340
  }
308
341
  async close(signal) {
309
- return this.app.close(signal);
342
+ return this.#app.close(signal);
310
343
  }
311
344
  };
312
345
  //#endregion
@@ -846,6 +879,13 @@ registerEntrypointKind({
846
879
  metaKey: SCHEDULED_METADATA_KEY,
847
880
  level: "method"
848
881
  });
882
+ function parseScheduledMetadata(value) {
883
+ if (typeof value !== "object" || value === null || !("cron" in value) || typeof value.cron !== "string" || !("methodName" in value) || typeof value.methodName !== "string" || !parseCron(value.cron, { dialect: "cloudflare" })) throw new TypeError("Invalid Cloudflare scheduled metadata");
884
+ return {
885
+ cron: value.cron,
886
+ methodName: value.methodName
887
+ };
888
+ }
849
889
  /**
850
890
  * Marks a method as a scheduled (cron) handler.
851
891
  *
@@ -861,8 +901,9 @@ registerEntrypointKind({
861
901
  * ```
862
902
  */
863
903
  function Scheduled(cron) {
904
+ if (!parseCron(cron, { dialect: "cloudflare" })) throw new TypeError(`Invalid Cloudflare cron expression: ${cron}`);
864
905
  return (target, propertyKey, _descriptor) => {
865
- const existing = getMetadata(SCHEDULED_METADATA_KEY, target.constructor) ?? [];
906
+ const existing = getScheduledMetadata(target);
866
907
  existing.push({
867
908
  cron,
868
909
  methodName: String(propertyKey)
@@ -870,6 +911,13 @@ function Scheduled(cron) {
870
911
  defineMetadata(SCHEDULED_METADATA_KEY, existing, target.constructor);
871
912
  };
872
913
  }
914
+ function getScheduledMetadata(target) {
915
+ const ctor = typeof target === "function" ? target : target.constructor;
916
+ const value = getMetadata(SCHEDULED_METADATA_KEY, ctor);
917
+ if (value === void 0) return [];
918
+ if (!Array.isArray(value)) throw new TypeError("Invalid Cloudflare scheduled metadata list");
919
+ return value.map(parseScheduledMetadata);
920
+ }
873
921
  //#endregion
874
922
  //#region src/decorators/queue-consumer.ts
875
923
  const QUEUE_CONSUMER_METADATA_KEY = "cloudflare:queue-consumer";
@@ -1018,6 +1066,6 @@ function durableObjectNonceStore(options) {
1018
1066
  } };
1019
1067
  }
1020
1068
  //#endregion
1021
- export { CloudflareApplication, CloudflareWebSocketModule, ConnectedSocket, DoCursorLog, DoPitrUnavailableError, Env, FlagshipFlagDriver, KVCacheStore, KvFlagDriver, MessageBody, QueueConsumer, R2StorageDriver, STORAGE_OPTIONS, Scheduled, StorageController, StorageManagerService, StorageModule, StorageService, SubscribeMessage, WebSocketGateway, WebSocketServer, WsException, armDoPitr, broadcastToRoom, cloudflareAdapter, cloudflareRateLimitStore, createCloudflareApp, createCloudflareWorker, durableObjectCursorLog, durableObjectLive, durableObjectNonceStore, durableObjectRoomName, flagshipFlagDriver, isDoPitrUnavailable, kvFlagDriver, liveInvalidateToRoom, readDoPitrBookmark };
1069
+ export { CloudflareApplication, CloudflareWebSocketModule, ConnectedSocket, DoCursorLog, DoPitrUnavailableError, Env, FlagshipFlagDriver, KVCacheStore, KvFlagDriver, MessageBody, QueueConsumer, R2StorageDriver, STORAGE_OPTIONS, Scheduled, StorageController, StorageManagerService, StorageModule, StorageService, SubscribeMessage, WebSocketGateway, WebSocketServer, WsException, armDoPitr, broadcastToRoom, cloudflareAdapter, cloudflareRateLimitStore, createCloudflareApp, createCloudflareWorker, durableObjectCursorLog, durableObjectLive, durableObjectNonceStore, durableObjectRoomName, flagshipFlagDriver, isDoPitrUnavailable, kvFlagDriver, liveInvalidateToRoom, parseScheduledMetadata, readDoPitrBookmark };
1022
1070
 
1023
1071
  //# sourceMappingURL=index.js.map