@velajs/cloudflare 2.0.0 → 3.0.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as DoPitrId, c as DoPitrUnavailableError, d as isDoPitrUnavailable, f as readDoPitrBookmark, i as DoPitrBookmarkRead, l as VelaDoPitrRpc, n as DoPitrArmOptions, o as DoPitrNamespace, p as CloudflareRoot, r as DoPitrArmResult, s as DoPitrStorage, t as VelaNonceDurableObject, u as armDoPitr } from "./nonce.durable-object-Df3_42Sy.js";
2
- import { AsyncCacheStore, DynamicModule, InjectionToken, NonceStore, RuntimeAdapter, ThrottlerStore, Type, VelaApplication, VelaMiddlewareHandler, VelaSecurityOptions } from "@velajs/vela";
3
- import { BroadcastCommand, ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketGatewayOptions, WebSocketServer, WsClient, WsDispatcher, WsException, WsMessage, WsResponse, WsServer } from "@velajs/vela/websocket";
1
+ import { a as DoPitrId, c as DoPitrUnavailableError, d as isDoPitrUnavailable, f as readDoPitrBookmark, i as DoPitrBookmarkRead, l as VelaDoPitrRpc, n as DoPitrArmOptions, o as DoPitrNamespace, p as CloudflareRoot, r as DoPitrArmResult, s as DoPitrStorage, t as VelaNonceDurableObject, u as armDoPitr } from "./nonce.durable-object-DffHMRnU.js";
2
+ import { AsyncCacheStore, CacheEntry, CacheEntryReader, CacheEntryWriter, CacheInvalidationStore, DynamicModule, InjectionToken, NonceStore, RuntimeAdapter, ThrottlerStore, Type, VelaApplication, VelaMiddlewareHandler, VelaSecurityOptions } from "@velajs/vela";
3
+ import { BroadcastCommand, ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketGatewayOptions, WebSocketSendGate, WebSocketServer, WsClient, WsDispatcher, WsException, WsMessage, WsResponse, WsServer } from "@velajs/vela/websocket";
4
4
  import { DownloadResult, PresignMethod, PresignedUrlResult, StorageBody, StorageDriver, UploadOptions, UploadResult } from "@velajs/vela/storage";
5
5
  import { CommitStamp, CursorLog, InvalidationCommand, LiveDriver, LiveEngine, LiveInvalidationSink, ResumeVerdict } from "@velajs/vela/live";
6
6
  import { Context, ExecutionContext } from "hono";
@@ -55,9 +55,8 @@ type MountOpenApiOptions = Parameters<VelaApplication['mountOpenApi']>[0];
55
55
  * ```
56
56
  */
57
57
  export declare class CloudflareApplication<T extends object = object> {
58
- private app;
58
+ #private;
59
59
  readonly env: T;
60
- private wsGatewayRoutes;
61
60
  constructor(app: VelaApplication, env: T);
62
61
  readonly fetch: (request: Request, env: T, ctx?: ExecutionContext) => Promise<Response>;
63
62
  getHonoApp(): ReturnType<VelaApplication['getHonoApp']>;
@@ -97,13 +96,12 @@ export declare class CloudflareApplication<T extends object = object> {
97
96
  */
98
97
  mountOpenApi(options: MountOpenApiOptions): this;
99
98
  /**
100
- * @internal — scans instances for `@WebSocketGateway({ path, binding })`
101
- * upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
102
- * come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
103
- * kinds) at dispatch time.
99
+ * @internal Upgrade routes come from validated gateway entrypoints, including
100
+ * request-scoped gateways without a bootstrap instance. Retain the instance
101
+ * scan for legacy applications that only declare forwarding metadata.
104
102
  */
105
103
  scanInstances(instances: unknown[]): void;
106
- /** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */
104
+ /** @internal — upgrade routes discovered from the application's gateways. */
107
105
  getWsGatewayRoutes(): WsGatewayRoute[];
108
106
  /**
109
107
  * Handle Cloudflare scheduled (cron) events.
@@ -273,22 +271,36 @@ export declare const STORAGE_OPTIONS: InjectionToken<StorageModuleOptions>;
273
271
  //#endregion
274
272
  //#region src/services/kv-cache.store.d.ts
275
273
  /**
276
- * Cloudflare KV-backed {@link CacheStore}. Values are JSON-encoded. Intended as
277
- * the slow tier under a `TieredCacheStore` (memory L1 → KV L2), but usable
278
- * standalone from a typed environment factory: `new KVCacheStore(env.CACHE)`.
279
- * Reads return unknown JSON; validate values at the consuming boundary.
280
- *
281
- * Note: Cloudflare KV requires `expirationTtl >= 60s`, so sub-minute TTLs are
282
- * clamped up. Keep short TTLs on the memory tier; use KV for longer-lived entries.
274
+ * Native KV JSON value store. Metadata retains logical expiry even when KV's
275
+ * physical retention rounds up to its 60-second minimum. Legacy values without
276
+ * metadata remain readable, but cannot safely backfill another tier.
283
277
  */
284
- export declare class KVCacheStore implements AsyncCacheStore {
278
+ export declare class KVCacheStore implements AsyncCacheStore, CacheEntryReader, CacheEntryWriter {
285
279
  private readonly ns;
286
280
  constructor(ns: KVNamespace);
287
281
  get(key: string): Promise<unknown>;
282
+ getEntry(key: string): Promise<{
283
+ value: unknown;
284
+ expiresAt?: number;
285
+ } | undefined>;
288
286
  set(key: string, value: unknown, ttl?: number): Promise<void>;
287
+ setEntry(key: string, entry: CacheEntry): Promise<void>;
289
288
  del(key: string): Promise<void>;
289
+ /** Namespace-wide, best effort. Use a dedicated value namespace; never use for scoped invalidation. */
290
290
  clear(): Promise<void>;
291
291
  }
292
+ /**
293
+ * Optional, eventually consistent generation store. Use a dedicated KV namespace
294
+ * without TTL/lifecycle cleanup. Never delete/reset generations while entries can
295
+ * survive. Concurrent writes and cached/negative reads prevent strong invalidation;
296
+ * this is unsuitable for strict read-after-write or authorization revocation.
297
+ */
298
+ export declare class KVCacheInvalidationStore implements CacheInvalidationStore {
299
+ private readonly ns;
300
+ constructor(ns: KVNamespace);
301
+ getVersion(key: string): Promise<string>;
302
+ invalidate(key: string): Promise<void>;
303
+ }
292
304
  //#endregion
293
305
  //#region src/services/flagship-flag.driver.d.ts
294
306
  /**
@@ -414,6 +426,21 @@ interface ScheduledMetadata {
414
426
  cron: string;
415
427
  methodName: string;
416
428
  }
429
+ /** Compatible with the existing programmatic scheduled() entrypoint. */
430
+ interface ScheduledEvent {
431
+ readonly cron: string;
432
+ readonly scheduledTime?: number;
433
+ }
434
+ /** Native controller passed unchanged to a Worker handler. Call noRetry on its receiver. */
435
+ interface ScheduledController extends ScheduledEvent {
436
+ readonly scheduledTime: number;
437
+ noRetry(): void;
438
+ }
439
+ interface ScheduledContext {
440
+ waitUntil(promise: Promise<unknown>): void;
441
+ }
442
+ type ScheduledHandler<Env extends object = object> = (controller: ScheduledController, env: Env, context: ScheduledContext) => void | Promise<void>;
443
+ export declare function parseScheduledMetadata(value: unknown): ScheduledMetadata;
417
444
  /**
418
445
  * Marks a method as a scheduled (cron) handler.
419
446
  *
@@ -618,5 +645,5 @@ interface DurableObjectNonceStoreOptions {
618
645
  */
619
646
  export declare function durableObjectNonceStore(options: DurableObjectNonceStoreOptions): NonceStore;
620
647
  //#endregion
621
- export { type BroadcastNamespace, type CfLiveDriver, type CloudflareRateLimitBinding, type CloudflareRateLimitStoreOptions, type CloudflareRoot, type CloudflareWorkerOptions, ConnectedSocket, type CreateCloudflareAppOptions, type DiskConfig, type DoPitrArmOptions, type DoPitrArmResult, type DoPitrBookmarkRead, type DoPitrId, type DoPitrNamespace, type DoPitrStorage, DoPitrUnavailableError, type DurableObjectLiveOptions, type DurableObjectNonceNamespace, type DurableObjectNonceStoreOptions, type FlagshipBinding, type FlagshipFlagDriverOptions, type KvFlagDriverOptions, type LiveNamespace, MessageBody, type MountOpenApiOptions, type OnGatewayConnection, type OnGatewayDisconnect, type OnGatewayInit, type PresignedUrlConfig, type QueueConsumerMetadata, type ScheduledMetadata, type StorageModuleOptions, SubscribeMessage, type VelaDoPitrRpc, WebSocketGateway, WebSocketServer, type WsClient, WsException, type WsGatewayRoute, type WsMessage, type WsResponse, type WsServer, armDoPitr, isDoPitrUnavailable, readDoPitrBookmark };
648
+ export { type BroadcastNamespace, type CfLiveDriver, type CloudflareRateLimitBinding, type CloudflareRateLimitStoreOptions, type CloudflareRoot, type CloudflareWorkerOptions, ConnectedSocket, type CreateCloudflareAppOptions, type DiskConfig, type DoPitrArmOptions, type DoPitrArmResult, type DoPitrBookmarkRead, type DoPitrId, type DoPitrNamespace, type DoPitrStorage, DoPitrUnavailableError, type DurableObjectLiveOptions, type DurableObjectNonceNamespace, type DurableObjectNonceStoreOptions, type FlagshipBinding, type FlagshipFlagDriverOptions, type KvFlagDriverOptions, type LiveNamespace, MessageBody, type MountOpenApiOptions, type OnGatewayConnection, type OnGatewayDisconnect, type OnGatewayInit, type PresignedUrlConfig, type QueueConsumerMetadata, type ScheduledContext, type ScheduledController, type ScheduledEvent, type ScheduledHandler, type ScheduledMetadata, type StorageModuleOptions, SubscribeMessage, type VelaDoPitrRpc, WebSocketGateway, WebSocketServer, type WsClient, WsException, type WsGatewayRoute, type WsMessage, type WsResponse, type WsServer, armDoPitr, isDoPitrUnavailable, readDoPitrBookmark };
622
649
  //# sourceMappingURL=index.d.ts.map
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-CeVihShV.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.startsWith("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,12 @@ 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
+ const handlers = [...this.#app.entrypoints.ofKind("cf:queue"), ...this.#app.entrypoints.ofKind("cf:queue:module")].filter((ep) => entrypointString(ep.meta, "queueName") === batch.queue);
340
+ if (handlers.length === 0) throw new Error(`No consumer for queue '${batch.queue}'.`);
341
+ await settleEntrypoints(handlers.map((ep) => this.dispatchEntrypoint(ep, batch, env, ctx)));
307
342
  }
308
343
  async close(signal) {
309
- return this.app.close(signal);
344
+ return this.#app.close(signal);
310
345
  }
311
346
  };
312
347
  //#endregion
@@ -331,13 +366,27 @@ function cloudflareAdapter(options) {
331
366
  }
332
367
  /** Build an application for one native Workers environment. Call inside a platform event. */
333
368
  async function createCloudflareApp(rootModule, options) {
334
- const velaApp = await VelaFactory.create(resolveCloudflareRoot(rootModule, options.env), {
369
+ const velaApp = await VelaFactory.create(await resolveCloudflareRoot(rootModule, options.env), {
335
370
  globalPrefix: options.globalPrefix,
336
371
  security: options.security,
337
372
  middleware: options.middleware?.(options.env),
338
373
  adapters: [cloudflareAdapter(options)]
339
374
  });
340
375
  const app = new CloudflareApplication(velaApp, options.env);
376
+ const consumers = /* @__PURE__ */ new Map();
377
+ for (const entry of [...app.entrypoints.ofKind("cf:queue"), ...app.entrypoints.ofKind("cf:queue:module")]) {
378
+ const meta = entry.meta;
379
+ if (typeof meta !== "object" || meta === null || !("queueName" in meta) || typeof meta.queueName !== "string") {
380
+ await app.close();
381
+ throw new TypeError("Invalid queue consumer metadata.");
382
+ }
383
+ const previous = consumers.get(meta.queueName);
384
+ if (previous && (previous === "cf:queue:module" || entry.kind === "cf:queue:module")) {
385
+ await app.close();
386
+ throw new Error(`Ambiguous consumer ownership for queue '${meta.queueName}'.`);
387
+ }
388
+ consumers.set(meta.queueName, entry.kind);
389
+ }
341
390
  app.scanInstances(velaApp.getInstances());
342
391
  registerWebSocketRoutes(app.getHonoApp(), app.getWsGatewayRoutes());
343
392
  return app;
@@ -675,13 +724,9 @@ StorageModule = __decorate([Module({
675
724
  //#endregion
676
725
  //#region src/services/kv-cache.store.ts
677
726
  /**
678
- * Cloudflare KV-backed {@link CacheStore}. Values are JSON-encoded. Intended as
679
- * the slow tier under a `TieredCacheStore` (memory L1 → KV L2), but usable
680
- * standalone from a typed environment factory: `new KVCacheStore(env.CACHE)`.
681
- * Reads return unknown JSON; validate values at the consuming boundary.
682
- *
683
- * Note: Cloudflare KV requires `expirationTtl >= 60s`, so sub-minute TTLs are
684
- * clamped up. Keep short TTLs on the memory tier; use KV for longer-lived entries.
727
+ * Native KV JSON value store. Metadata retains logical expiry even when KV's
728
+ * physical retention rounds up to its 60-second minimum. Legacy values without
729
+ * metadata remain readable, but cannot safely backfill another tier.
685
730
  */
686
731
  var KVCacheStore = class {
687
732
  ns;
@@ -689,16 +734,42 @@ var KVCacheStore = class {
689
734
  this.ns = ns;
690
735
  }
691
736
  async get(key) {
692
- const value = await this.ns.get(key, "json");
693
- return value === null ? void 0 : value;
737
+ return (await this.getEntry(key))?.value;
738
+ }
739
+ async getEntry(key) {
740
+ const { value, metadata } = await this.ns.getWithMetadata(key, "json");
741
+ if (value === null) return void 0;
742
+ if (typeof metadata === "object" && metadata !== null && "velaCacheExpiresAt" in metadata) {
743
+ const expiresAt = metadata.velaCacheExpiresAt;
744
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= Date.now()) return void 0;
745
+ return {
746
+ value,
747
+ expiresAt
748
+ };
749
+ }
750
+ return { value };
694
751
  }
695
752
  async set(key, value, ttl) {
696
- const options = ttl !== void 0 ? { expirationTtl: Math.max(60, Math.floor(ttl)) } : void 0;
697
- await this.ns.put(key, JSON.stringify(value), options);
753
+ if (ttl !== void 0 && (!Number.isFinite(ttl) || ttl < 0)) throw new TypeError("Cache TTL must be finite and nonnegative.");
754
+ if (ttl !== void 0) return this.setEntry(key, {
755
+ value,
756
+ expiresAt: Date.now() + ttl * 1e3
757
+ });
758
+ await this.ns.put(key, JSON.stringify(value));
759
+ }
760
+ async setEntry(key, entry) {
761
+ if (!Number.isFinite(entry.expiresAt)) throw new TypeError("Cache expiry must be finite.");
762
+ const remaining = (entry.expiresAt - Date.now()) / 1e3;
763
+ if (remaining <= 0) return this.del(key);
764
+ await this.ns.put(key, JSON.stringify(entry.value), {
765
+ expirationTtl: Math.max(60, Math.ceil(remaining)),
766
+ metadata: { velaCacheExpiresAt: entry.expiresAt }
767
+ });
698
768
  }
699
769
  async del(key) {
700
770
  await this.ns.delete(key);
701
771
  }
772
+ /** Namespace-wide, best effort. Use a dedicated value namespace; never use for scoped invalidation. */
702
773
  async clear() {
703
774
  let cursor;
704
775
  do {
@@ -708,6 +779,27 @@ var KVCacheStore = class {
708
779
  } while (cursor);
709
780
  }
710
781
  };
782
+ /**
783
+ * Optional, eventually consistent generation store. Use a dedicated KV namespace
784
+ * without TTL/lifecycle cleanup. Never delete/reset generations while entries can
785
+ * survive. Concurrent writes and cached/negative reads prevent strong invalidation;
786
+ * this is unsuitable for strict read-after-write or authorization revocation.
787
+ */
788
+ var KVCacheInvalidationStore = class {
789
+ ns;
790
+ constructor(ns) {
791
+ this.ns = ns;
792
+ }
793
+ async getVersion(key) {
794
+ const value = await this.ns.get(key, "json");
795
+ if (value === null) return "initial";
796
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) throw new TypeError("Invalid cache generation.");
797
+ return value;
798
+ }
799
+ async invalidate(key) {
800
+ await this.ns.put(key, JSON.stringify(crypto.randomUUID()));
801
+ }
802
+ };
711
803
  //#endregion
712
804
  //#region src/services/flagship-flag.driver.ts
713
805
  /**
@@ -846,6 +938,13 @@ registerEntrypointKind({
846
938
  metaKey: SCHEDULED_METADATA_KEY,
847
939
  level: "method"
848
940
  });
941
+ function parseScheduledMetadata(value) {
942
+ 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");
943
+ return {
944
+ cron: value.cron,
945
+ methodName: value.methodName
946
+ };
947
+ }
849
948
  /**
850
949
  * Marks a method as a scheduled (cron) handler.
851
950
  *
@@ -861,8 +960,9 @@ registerEntrypointKind({
861
960
  * ```
862
961
  */
863
962
  function Scheduled(cron) {
963
+ if (!parseCron(cron, { dialect: "cloudflare" })) throw new TypeError(`Invalid Cloudflare cron expression: ${cron}`);
864
964
  return (target, propertyKey, _descriptor) => {
865
- const existing = getMetadata(SCHEDULED_METADATA_KEY, target.constructor) ?? [];
965
+ const existing = getScheduledMetadata(target);
866
966
  existing.push({
867
967
  cron,
868
968
  methodName: String(propertyKey)
@@ -870,6 +970,13 @@ function Scheduled(cron) {
870
970
  defineMetadata(SCHEDULED_METADATA_KEY, existing, target.constructor);
871
971
  };
872
972
  }
973
+ function getScheduledMetadata(target) {
974
+ const ctor = typeof target === "function" ? target : target.constructor;
975
+ const value = getMetadata(SCHEDULED_METADATA_KEY, ctor);
976
+ if (value === void 0) return [];
977
+ if (!Array.isArray(value)) throw new TypeError("Invalid Cloudflare scheduled metadata list");
978
+ return value.map(parseScheduledMetadata);
979
+ }
873
980
  //#endregion
874
981
  //#region src/decorators/queue-consumer.ts
875
982
  const QUEUE_CONSUMER_METADATA_KEY = "cloudflare:queue-consumer";
@@ -1018,6 +1125,6 @@ function durableObjectNonceStore(options) {
1018
1125
  } };
1019
1126
  }
1020
1127
  //#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 };
1128
+ export { CloudflareApplication, CloudflareWebSocketModule, ConnectedSocket, DoCursorLog, DoPitrUnavailableError, Env, FlagshipFlagDriver, KVCacheInvalidationStore, 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
1129
 
1023
1130
  //# sourceMappingURL=index.js.map