@velajs/cloudflare 1.6.0 → 1.9.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 +5 -0
- package/dist/cloudflare-application.d.ts +21 -5
- package/dist/cloudflare-application.js +73 -35
- package/dist/cloudflare-factory.d.ts +15 -1
- package/dist/cloudflare-factory.js +36 -17
- package/dist/decorators/queue-consumer.js +9 -1
- package/dist/decorators/scheduled.js +8 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/services/flagship-flag.driver.d.ts +57 -0
- package/dist/services/flagship-flag.driver.js +49 -0
- package/dist/services/kv-flag.driver.d.ts +48 -0
- package/dist/services/kv-flag.driver.js +62 -0
- package/dist/websocket/do-bootstrap.d.ts +2 -0
- package/dist/websocket/do-bootstrap.js +8 -1
- package/dist/websocket/do-websocket-host.d.ts +2 -1
- package/dist/websocket/do-websocket-host.js +4 -2
- package/dist/websocket/websocket.durable-object.js +1 -1
- package/package.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 (2026-07-04)
|
|
4
|
+
|
|
5
|
+
- `cloudflareAdapter()` exported (createCloudflareApp composes vela RuntimeAdapter); `@QueueConsumer`/`@Scheduled` declare open entrypoint kinds; queue/scheduled dispatch runs per-event in a request scope through PipelineRunner (consumer-scoped guards/interceptors/filters; request-scoped deps rebuild per batch); DO WebSocket reads `app.entrypoints`. Requires `@velajs/vela >=1.11.0`.
|
|
6
|
+
|
|
7
|
+
|
|
3
8
|
## 1.6.0 (2026-07-01)
|
|
4
9
|
|
|
5
10
|
### Added
|
|
@@ -41,8 +41,6 @@ export type MountOpenApiOptions = Parameters<VelaApplication['mountOpenApi']>[0]
|
|
|
41
41
|
*/
|
|
42
42
|
export declare class CloudflareApplication {
|
|
43
43
|
private app;
|
|
44
|
-
private scheduledHandlers;
|
|
45
|
-
private queueConsumers;
|
|
46
44
|
private wsGatewayRoutes;
|
|
47
45
|
constructor(app: VelaApplication);
|
|
48
46
|
get fetch(): Hono['fetch'];
|
|
@@ -81,13 +79,20 @@ export declare class CloudflareApplication {
|
|
|
81
79
|
* ```
|
|
82
80
|
*/
|
|
83
81
|
mountOpenApi(options: MountOpenApiOptions): this;
|
|
84
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* @internal — scans instances for `@WebSocketGateway({ path, binding })`
|
|
84
|
+
* upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
|
|
85
|
+
* come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
|
|
86
|
+
* kinds) at dispatch time.
|
|
87
|
+
*/
|
|
85
88
|
scanInstances(instances: unknown[]): void;
|
|
86
89
|
/** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */
|
|
87
90
|
getWsGatewayRoutes(): WsGatewayRoute[];
|
|
88
91
|
/**
|
|
89
92
|
* Handle Cloudflare scheduled (cron) events.
|
|
90
|
-
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
93
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
94
|
+
* handlers read from `app.entrypoints`; each handler runs inside a fresh
|
|
95
|
+
* request-scoped child (request-scoped providers rebuild per tick).
|
|
91
96
|
*/
|
|
92
97
|
scheduled(event: {
|
|
93
98
|
cron: string;
|
|
@@ -95,9 +100,20 @@ export declare class CloudflareApplication {
|
|
|
95
100
|
}, env: CloudflareEnv, ctx: {
|
|
96
101
|
waitUntil: (promise: Promise<unknown>) => void;
|
|
97
102
|
}): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Run one entrypoint handler inside a fresh request scope, through the
|
|
105
|
+
* shared guard → interceptor pipeline (components declared with
|
|
106
|
+
* `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
|
|
107
|
+
* method). HTTP-global components deliberately do NOT apply — an HTTP auth
|
|
108
|
+
* guard has no business rejecting a queue batch. Unclaimed errors rethrow
|
|
109
|
+
* so the platform's retry semantics stay intact.
|
|
110
|
+
*/
|
|
111
|
+
private dispatchEntrypoint;
|
|
98
112
|
/**
|
|
99
113
|
* Handle Cloudflare Queue consumer events.
|
|
100
|
-
* Matches the batch queue name to `@QueueConsumer()` handlers
|
|
114
|
+
* Matches the batch queue name to `@QueueConsumer()` handlers read from
|
|
115
|
+
* `app.entrypoints`; each batch is processed inside a fresh request-scoped
|
|
116
|
+
* child (request-scoped providers rebuild per batch — no boot-time captives).
|
|
101
117
|
*/
|
|
102
118
|
queue(batch: {
|
|
103
119
|
queue: string;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { CRON_METADATA,
|
|
2
|
-
import {
|
|
3
|
-
import { getQueueConsumerMetadata } from "./decorators/queue-consumer.js";
|
|
1
|
+
import { CRON_METADATA, PipelineRunner, buildEntrypointExecutionContext, registerEntrypointKind, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela";
|
|
2
|
+
import { ComponentManager } from "@velajs/vela/internal";
|
|
4
3
|
import { collectWsGatewayRoutes } from "./websocket/websocket-routing.js";
|
|
4
|
+
// vela's own @Cron jobs run via the same Workers cron trigger — declare an
|
|
5
|
+
// entrypoint kind over vela's metadata key (the open-kind system makes
|
|
6
|
+
// cross-package declarations first-class).
|
|
7
|
+
registerEntrypointKind({
|
|
8
|
+
kind: 'cf:vela-cron',
|
|
9
|
+
metaKey: CRON_METADATA,
|
|
10
|
+
level: 'method'
|
|
11
|
+
});
|
|
5
12
|
function invoke(instance, methodName, args) {
|
|
6
13
|
const method = instance[methodName];
|
|
7
14
|
if (typeof method !== 'function') {
|
|
@@ -39,8 +46,6 @@ function invoke(instance, methodName, args) {
|
|
|
39
46
|
* ```
|
|
40
47
|
*/ export class CloudflareApplication {
|
|
41
48
|
app;
|
|
42
|
-
scheduledHandlers = [];
|
|
43
|
-
queueConsumers = [];
|
|
44
49
|
wsGatewayRoutes = [];
|
|
45
50
|
constructor(app){
|
|
46
51
|
this.app = app;
|
|
@@ -88,32 +93,14 @@ function invoke(instance, methodName, args) {
|
|
|
88
93
|
this.app.mountOpenApi(options);
|
|
89
94
|
return this;
|
|
90
95
|
}
|
|
91
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* @internal — scans instances for `@WebSocketGateway({ path, binding })`
|
|
98
|
+
* upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
|
|
99
|
+
* come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
|
|
100
|
+
* kinds) at dispatch time.
|
|
101
|
+
*/ scanInstances(instances) {
|
|
92
102
|
for (const instance of instances){
|
|
93
103
|
if (!instance || typeof instance !== 'object') continue;
|
|
94
|
-
for (const meta of getScheduledMetadata(instance)){
|
|
95
|
-
this.scheduledHandlers.push({
|
|
96
|
-
instance,
|
|
97
|
-
methodName: meta.methodName,
|
|
98
|
-
cron: meta.cron
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
// vela's @Cron jobs run via the same Workers cron trigger.
|
|
102
|
-
const cronMeta = getMetadata(CRON_METADATA, instance.constructor) ?? [];
|
|
103
|
-
for (const meta of cronMeta){
|
|
104
|
-
this.scheduledHandlers.push({
|
|
105
|
-
instance,
|
|
106
|
-
methodName: meta.methodName,
|
|
107
|
-
cron: meta.expression
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
for (const meta of getQueueConsumerMetadata(instance)){
|
|
111
|
-
this.queueConsumers.push({
|
|
112
|
-
instance,
|
|
113
|
-
methodName: meta.methodName,
|
|
114
|
-
queueName: meta.queueName
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
104
|
this.wsGatewayRoutes.push(...collectWsGatewayRoutes(instance));
|
|
118
105
|
}
|
|
119
106
|
}
|
|
@@ -122,21 +109,72 @@ function invoke(instance, methodName, args) {
|
|
|
122
109
|
}
|
|
123
110
|
/**
|
|
124
111
|
* Handle Cloudflare scheduled (cron) events.
|
|
125
|
-
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
112
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
113
|
+
* handlers read from `app.entrypoints`; each handler runs inside a fresh
|
|
114
|
+
* request-scoped child (request-scoped providers rebuild per tick).
|
|
126
115
|
*/ async scheduled(event, env, ctx) {
|
|
127
|
-
const
|
|
128
|
-
|
|
116
|
+
const handlers = [
|
|
117
|
+
...this.app.entrypoints.ofKind('cf:scheduled').map((ep)=>({
|
|
118
|
+
ep,
|
|
119
|
+
cron: ep.meta.cron
|
|
120
|
+
})),
|
|
121
|
+
...this.app.entrypoints.ofKind('cf:vela-cron').map((ep)=>({
|
|
122
|
+
ep,
|
|
123
|
+
cron: ep.meta.expression
|
|
124
|
+
}))
|
|
125
|
+
].filter((h)=>h.cron === event.cron);
|
|
126
|
+
await Promise.all(handlers.map(({ ep })=>this.dispatchEntrypoint(ep, [
|
|
129
127
|
event,
|
|
130
128
|
env,
|
|
131
129
|
ctx
|
|
132
130
|
])));
|
|
133
131
|
}
|
|
134
132
|
/**
|
|
133
|
+
* Run one entrypoint handler inside a fresh request scope, through the
|
|
134
|
+
* shared guard → interceptor pipeline (components declared with
|
|
135
|
+
* `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
|
|
136
|
+
* method). HTTP-global components deliberately do NOT apply — an HTTP auth
|
|
137
|
+
* guard has no business rejecting a queue batch. Unclaimed errors rethrow
|
|
138
|
+
* so the platform's retry semantics stay intact.
|
|
139
|
+
*/ async dispatchEntrypoint(ep, args) {
|
|
140
|
+
const targetClass = ep.token;
|
|
141
|
+
const methodName = String(ep.methodName);
|
|
142
|
+
const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, args[0]);
|
|
143
|
+
await runInEntrypointScope(this.app.getContainer(), async (scope)=>{
|
|
144
|
+
const instance = scope.resolve(ep.token);
|
|
145
|
+
const guards = ComponentManager.resolveGuards(ComponentManager.getScopedComponents('guard', targetClass, methodName), scope);
|
|
146
|
+
const interceptors = ComponentManager.resolveInterceptors(ComponentManager.getScopedComponents('interceptor', targetClass, methodName), scope);
|
|
147
|
+
// Closest-first, mirroring the HTTP/WS dispatchers.
|
|
148
|
+
const filters = ComponentManager.resolveFilters([
|
|
149
|
+
...ComponentManager.getScopedComponents('filter', targetClass, methodName)
|
|
150
|
+
].reverse(), scope);
|
|
151
|
+
try {
|
|
152
|
+
await PipelineRunner.run({
|
|
153
|
+
context,
|
|
154
|
+
guards,
|
|
155
|
+
interceptors,
|
|
156
|
+
resolveArgs: async ()=>args,
|
|
157
|
+
invoke: async (resolved)=>invoke(instance, methodName, resolved)
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
for (const filter of filters){
|
|
161
|
+
if (shouldFilterCatch(filter, error)) {
|
|
162
|
+
await filter.catch(error, context);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
135
171
|
* Handle Cloudflare Queue consumer events.
|
|
136
|
-
* Matches the batch queue name to `@QueueConsumer()` handlers
|
|
172
|
+
* Matches the batch queue name to `@QueueConsumer()` handlers read from
|
|
173
|
+
* `app.entrypoints`; each batch is processed inside a fresh request-scoped
|
|
174
|
+
* child (request-scoped providers rebuild per batch — no boot-time captives).
|
|
137
175
|
*/ async queue(batch, env, ctx) {
|
|
138
|
-
const
|
|
139
|
-
await Promise.all(
|
|
176
|
+
const handlers = this.app.entrypoints.ofKind('cf:queue').filter((ep)=>ep.meta.queueName === batch.queue);
|
|
177
|
+
await Promise.all(handlers.map((ep)=>this.dispatchEntrypoint(ep, [
|
|
140
178
|
batch,
|
|
141
179
|
env,
|
|
142
180
|
ctx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from 'hono';
|
|
2
|
-
import type { Type } from '@velajs/vela';
|
|
2
|
+
import type { RuntimeAdapter, Type } from '@velajs/vela';
|
|
3
3
|
import { CloudflareApplication } from './cloudflare-application';
|
|
4
4
|
/**
|
|
5
5
|
* Options for {@link createCloudflareApp}.
|
|
@@ -71,4 +71,18 @@ export interface CreateCloudflareAppOptions {
|
|
|
71
71
|
* });
|
|
72
72
|
* ```
|
|
73
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* The Cloudflare platform binding as a vela {@link RuntimeAdapter}: a one-time
|
|
76
|
+
* request middleware captures `c.env` on the first request and initializes
|
|
77
|
+
* every `BindingRef`/`EnvRef` (collected at `onBootstrap`, before any request
|
|
78
|
+
* can arrive). Adapter `requestMiddleware` is prepended to the global chain by
|
|
79
|
+
* `VelaFactory.create`, so user middleware can safely read binding refs.
|
|
80
|
+
*
|
|
81
|
+
* Exposed so consumers composing `VelaFactory.create` themselves can opt in:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const app = await VelaFactory.create(AppModule, { adapters: [cloudflareAdapter()] });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function cloudflareAdapter(): RuntimeAdapter;
|
|
74
88
|
export declare function createCloudflareApp(rootModule: Type, options?: CreateCloudflareAppOptions): Promise<CloudflareApplication>;
|
|
@@ -48,31 +48,50 @@ function collectBindingRefs(container) {
|
|
|
48
48
|
* ],
|
|
49
49
|
* });
|
|
50
50
|
* ```
|
|
51
|
-
*/
|
|
51
|
+
*/ /**
|
|
52
|
+
* The Cloudflare platform binding as a vela {@link RuntimeAdapter}: a one-time
|
|
53
|
+
* request middleware captures `c.env` on the first request and initializes
|
|
54
|
+
* every `BindingRef`/`EnvRef` (collected at `onBootstrap`, before any request
|
|
55
|
+
* can arrive). Adapter `requestMiddleware` is prepended to the global chain by
|
|
56
|
+
* `VelaFactory.create`, so user middleware can safely read binding refs.
|
|
57
|
+
*
|
|
58
|
+
* Exposed so consumers composing `VelaFactory.create` themselves can opt in:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* const app = await VelaFactory.create(AppModule, { adapters: [cloudflareAdapter()] });
|
|
62
|
+
* ```
|
|
63
|
+
*/ export function cloudflareAdapter() {
|
|
52
64
|
let initialized = false;
|
|
53
|
-
let refs;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
let refs = [];
|
|
66
|
+
return {
|
|
67
|
+
name: 'cloudflare',
|
|
68
|
+
requestMiddleware: [
|
|
69
|
+
async (c, next)=>{
|
|
70
|
+
if (!initialized) {
|
|
71
|
+
initialized = true;
|
|
72
|
+
const env = c.env ?? {};
|
|
73
|
+
for (const ref of refs){
|
|
74
|
+
// EnvRef holds the whole env; every other ref holds one binding.
|
|
75
|
+
if (ref instanceof EnvRef) ref._initialize(env);
|
|
76
|
+
else ref._initialize(env[ref.bindingName]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await next();
|
|
62
80
|
}
|
|
81
|
+
],
|
|
82
|
+
onBootstrap: ({ container })=>{
|
|
83
|
+
refs = collectBindingRefs(container);
|
|
63
84
|
}
|
|
64
|
-
await next();
|
|
65
85
|
};
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
}
|
|
87
|
+
export async function createCloudflareApp(rootModule, options = {}) {
|
|
68
88
|
const velaApp = await VelaFactory.create(rootModule, {
|
|
69
89
|
globalPrefix: options.globalPrefix,
|
|
70
|
-
middleware:
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
middleware: options.middleware,
|
|
91
|
+
adapters: [
|
|
92
|
+
cloudflareAdapter()
|
|
73
93
|
]
|
|
74
94
|
});
|
|
75
|
-
refs = collectBindingRefs(velaApp.getContainer());
|
|
76
95
|
const cfApp = new CloudflareApplication(velaApp);
|
|
77
96
|
cfApp.scanInstances(velaApp.getInstances());
|
|
78
97
|
// Register WebSocket upgrade routes for any @WebSocketGateway({ path, binding }).
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { defineMetadata, getMetadata } from "@velajs/vela";
|
|
1
|
+
import { defineMetadata, getMetadata, registerEntrypointKind } from "@velajs/vela";
|
|
2
2
|
const QUEUE_CONSUMER_METADATA_KEY = 'cloudflare:queue-consumer';
|
|
3
|
+
// Open entrypoint kind: any adapter can enumerate queue consumers via
|
|
4
|
+
// `app.entrypoints.ofKind('cf:queue')` — declared here, next to the decorator,
|
|
5
|
+
// with zero vela-core involvement.
|
|
6
|
+
registerEntrypointKind({
|
|
7
|
+
kind: 'cf:queue',
|
|
8
|
+
metaKey: QUEUE_CONSUMER_METADATA_KEY,
|
|
9
|
+
level: 'method'
|
|
10
|
+
});
|
|
3
11
|
/**
|
|
4
12
|
* Marks a method as a queue consumer handler.
|
|
5
13
|
*
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { defineMetadata, getMetadata } from "@velajs/vela";
|
|
1
|
+
import { defineMetadata, getMetadata, registerEntrypointKind } from "@velajs/vela";
|
|
2
2
|
const SCHEDULED_METADATA_KEY = 'cloudflare:scheduled';
|
|
3
|
+
// Open entrypoint kind: adapters enumerate cron handlers via
|
|
4
|
+
// `app.entrypoints.ofKind('cf:scheduled')` — declared next to the decorator.
|
|
5
|
+
registerEntrypointKind({
|
|
6
|
+
kind: 'cf:scheduled',
|
|
7
|
+
metaKey: SCHEDULED_METADATA_KEY,
|
|
8
|
+
level: 'method'
|
|
9
|
+
});
|
|
3
10
|
/**
|
|
4
11
|
* Marks a method as a scheduled (cron) handler.
|
|
5
12
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createCloudflareApp } from './cloudflare-factory';
|
|
1
|
+
export { cloudflareAdapter, createCloudflareApp } from './cloudflare-factory';
|
|
2
2
|
export type { CreateCloudflareAppOptions } from './cloudflare-factory';
|
|
3
3
|
export { CloudflareApplication } from './cloudflare-application';
|
|
4
4
|
export type { MountOpenApiOptions } from './cloudflare-application';
|
|
@@ -23,6 +23,10 @@ export { AIService } from './services/ai.service';
|
|
|
23
23
|
export { VectorizeService } from './services/vectorize.service';
|
|
24
24
|
export { HyperdriveService } from './services/hyperdrive.service';
|
|
25
25
|
export { EnvService } from './services/env.service';
|
|
26
|
+
export { FlagshipFlagDriver, flagshipFlagDriver } from './services/flagship-flag.driver';
|
|
27
|
+
export type { FlagshipBinding, FlagshipFlagDriverOptions } from './services/flagship-flag.driver';
|
|
28
|
+
export { KvFlagDriver, kvFlagDriver } from './services/kv-flag.driver';
|
|
29
|
+
export type { KvFlagDriverOptions } from './services/kv-flag.driver';
|
|
26
30
|
export { Env } from './decorators/env';
|
|
27
31
|
export { Scheduled } from './decorators/scheduled';
|
|
28
32
|
export { QueueConsumer } from './decorators/queue-consumer';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Factory & Application
|
|
2
|
-
export { createCloudflareApp } from "./cloudflare-factory.js";
|
|
2
|
+
export { cloudflareAdapter, createCloudflareApp } from "./cloudflare-factory.js";
|
|
3
3
|
export { CloudflareApplication } from "./cloudflare-application.js";
|
|
4
4
|
// Modules
|
|
5
5
|
export { KVModule } from "./modules/kv.module.js";
|
|
@@ -24,6 +24,9 @@ export { AIService } from "./services/ai.service.js";
|
|
|
24
24
|
export { VectorizeService } from "./services/vectorize.service.js";
|
|
25
25
|
export { HyperdriveService } from "./services/hyperdrive.service.js";
|
|
26
26
|
export { EnvService } from "./services/env.service.js";
|
|
27
|
+
// Feature-flag drivers (implement @velajs/feature-flags' FeatureFlagDriver contract)
|
|
28
|
+
export { FlagshipFlagDriver, flagshipFlagDriver } from "./services/flagship-flag.driver.js";
|
|
29
|
+
export { KvFlagDriver, kvFlagDriver } from "./services/kv-flag.driver.js";
|
|
27
30
|
// Decorators
|
|
28
31
|
export { Env } from "./decorators/env.js";
|
|
29
32
|
export { Scheduled } from "./decorators/scheduled.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { FeatureFlagDriver, FlagContext } from '@velajs/feature-flags';
|
|
2
|
+
/**
|
|
3
|
+
* The subset of a Cloudflare **Flagship** binding this driver evaluates against
|
|
4
|
+
* — the four typed value methods. The binding itself returns the supplied
|
|
5
|
+
* `defaultValue` on evaluation errors; transport-level failures reject and are
|
|
6
|
+
* left to propagate (never-throw is the service layer's job, not the driver's).
|
|
7
|
+
*
|
|
8
|
+
* @see https://developers.cloudflare.com/flagship/binding/
|
|
9
|
+
*/
|
|
10
|
+
export interface FlagshipBinding {
|
|
11
|
+
getBooleanValue(key: string, defaultValue: boolean, context?: FlagContext): Promise<boolean>;
|
|
12
|
+
getStringValue(key: string, defaultValue: string, context?: FlagContext): Promise<string>;
|
|
13
|
+
getNumberValue(key: string, defaultValue: number, context?: FlagContext): Promise<number>;
|
|
14
|
+
getObjectValue<T extends object>(key: string, defaultValue: T, context?: FlagContext): Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
export interface FlagshipFlagDriverOptions {
|
|
17
|
+
/** Driver name used for `use(name)` / default-driver selection. Default `"flagship"`. */
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* {@link FeatureFlagDriver} backed by a Cloudflare Flagship binding.
|
|
22
|
+
*
|
|
23
|
+
* A thin, honest wrapper: each contract method maps 1:1 onto the binding's
|
|
24
|
+
* corresponding value method, forwarding the caller's `fallback` (the binding's
|
|
25
|
+
* `defaultValue`) and evaluation context. The binding resolves the fallback on
|
|
26
|
+
* evaluation errors; anything the binding *rejects* with (e.g. a `remote: true`
|
|
27
|
+
* dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
|
|
28
|
+
* the never-throw guarantee.
|
|
29
|
+
*
|
|
30
|
+
* The binding only exists per request in a Worker, so pass a lazy accessor when
|
|
31
|
+
* wiring from a bootstrap factory (mirroring how {@link EnvService} reads are
|
|
32
|
+
* deferred); a resolved binding may be passed directly in tests.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* FeatureFlagsModule.forRootAsync({
|
|
36
|
+
* inject: [EnvService],
|
|
37
|
+
* useFactory: (env: EnvService) => ({
|
|
38
|
+
* drivers: [flagshipFlagDriver(() => env.get<FlagshipBinding>('FLAGS')!)],
|
|
39
|
+
* }),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Evaluation ergonomics (the 1:1 binding-method mapping) are ported from the
|
|
44
|
+
* Stratal feature-flags service (MIT, © Temitayo Fadojutimi), reshaped as a
|
|
45
|
+
* bare driver.
|
|
46
|
+
*/
|
|
47
|
+
export declare class FlagshipFlagDriver implements FeatureFlagDriver {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
private readonly resolve;
|
|
50
|
+
constructor(binding: FlagshipBinding | (() => FlagshipBinding), options?: FlagshipFlagDriverOptions);
|
|
51
|
+
getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
|
|
52
|
+
getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
|
|
53
|
+
getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
|
|
54
|
+
getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
|
|
55
|
+
}
|
|
56
|
+
/** Convenience factory for {@link FlagshipFlagDriver}. */
|
|
57
|
+
export declare function flagshipFlagDriver(binding: FlagshipBinding | (() => FlagshipBinding), options?: FlagshipFlagDriverOptions): FlagshipFlagDriver;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link FeatureFlagDriver} backed by a Cloudflare Flagship binding.
|
|
3
|
+
*
|
|
4
|
+
* A thin, honest wrapper: each contract method maps 1:1 onto the binding's
|
|
5
|
+
* corresponding value method, forwarding the caller's `fallback` (the binding's
|
|
6
|
+
* `defaultValue`) and evaluation context. The binding resolves the fallback on
|
|
7
|
+
* evaluation errors; anything the binding *rejects* with (e.g. a `remote: true`
|
|
8
|
+
* dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
|
|
9
|
+
* the never-throw guarantee.
|
|
10
|
+
*
|
|
11
|
+
* The binding only exists per request in a Worker, so pass a lazy accessor when
|
|
12
|
+
* wiring from a bootstrap factory (mirroring how {@link EnvService} reads are
|
|
13
|
+
* deferred); a resolved binding may be passed directly in tests.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* FeatureFlagsModule.forRootAsync({
|
|
17
|
+
* inject: [EnvService],
|
|
18
|
+
* useFactory: (env: EnvService) => ({
|
|
19
|
+
* drivers: [flagshipFlagDriver(() => env.get<FlagshipBinding>('FLAGS')!)],
|
|
20
|
+
* }),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Evaluation ergonomics (the 1:1 binding-method mapping) are ported from the
|
|
25
|
+
* Stratal feature-flags service (MIT, © Temitayo Fadojutimi), reshaped as a
|
|
26
|
+
* bare driver.
|
|
27
|
+
*/ export class FlagshipFlagDriver {
|
|
28
|
+
name;
|
|
29
|
+
resolve;
|
|
30
|
+
constructor(binding, options = {}){
|
|
31
|
+
this.resolve = typeof binding === 'function' ? binding : ()=>binding;
|
|
32
|
+
this.name = options.name ?? 'flagship';
|
|
33
|
+
}
|
|
34
|
+
getBoolean(key, fallback, ctx) {
|
|
35
|
+
return this.resolve().getBooleanValue(key, fallback, ctx);
|
|
36
|
+
}
|
|
37
|
+
getString(key, fallback, ctx) {
|
|
38
|
+
return this.resolve().getStringValue(key, fallback, ctx);
|
|
39
|
+
}
|
|
40
|
+
getNumber(key, fallback, ctx) {
|
|
41
|
+
return this.resolve().getNumberValue(key, fallback, ctx);
|
|
42
|
+
}
|
|
43
|
+
getObject(key, fallback, ctx) {
|
|
44
|
+
return this.resolve().getObjectValue(key, fallback, ctx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Convenience factory for {@link FlagshipFlagDriver}. */ export function flagshipFlagDriver(binding, options) {
|
|
48
|
+
return new FlagshipFlagDriver(binding, options);
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FeatureFlagDriver, FlagContext } from '@velajs/feature-flags';
|
|
2
|
+
import { KVService } from './kv.service';
|
|
3
|
+
export interface KvFlagDriverOptions {
|
|
4
|
+
/** Driver name used for `use(name)` / default-driver selection. Default `"kv"`. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Prefix prepended to every flag key before the KV read. Default `""` (none). */
|
|
7
|
+
prefix?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Cloudflare KV-backed {@link FeatureFlagDriver}. Flags are stored as JSON
|
|
11
|
+
* values under an optional key prefix and read with `get(key, 'json')`. Reads
|
|
12
|
+
* are type-checked against the requested type: a missing key or a value of the
|
|
13
|
+
* wrong JSON type returns the caller's `fallback`. KV has no targeting, so the
|
|
14
|
+
* evaluation context is ignored.
|
|
15
|
+
*
|
|
16
|
+
* The driver stays honest — it does **not** swallow errors. A KV failure (or a
|
|
17
|
+
* `SyntaxError` from a malformed stored value) propagates; the never-throw
|
|
18
|
+
* guarantee lives in `@velajs/feature-flags`'s service layer.
|
|
19
|
+
*
|
|
20
|
+
* Placed like {@link KVCacheStore}: construct it in a wiring factory over a
|
|
21
|
+
* resolved {@link KVService}.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* FeatureFlagsModule.forRootAsync({
|
|
25
|
+
* inject: [KVService],
|
|
26
|
+
* useFactory: (kv: KVService) => ({ drivers: [new KvFlagDriver(kv, { prefix: 'flag:' })] }),
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class KvFlagDriver implements FeatureFlagDriver {
|
|
31
|
+
private readonly kv;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
private readonly prefix;
|
|
34
|
+
constructor(kv: KVService, options?: KvFlagDriverOptions);
|
|
35
|
+
private get ns();
|
|
36
|
+
getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean>;
|
|
37
|
+
getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string>;
|
|
38
|
+
getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number>;
|
|
39
|
+
getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Reads and JSON-parses the (prefixed) key, returning the parsed value only
|
|
42
|
+
* when `matches` accepts its type; otherwise the caller's fallback. A missing
|
|
43
|
+
* key reads as `null` → fallback. Read/parse errors are left to propagate.
|
|
44
|
+
*/
|
|
45
|
+
private read;
|
|
46
|
+
}
|
|
47
|
+
/** Convenience factory for {@link KvFlagDriver}. */
|
|
48
|
+
export declare function kvFlagDriver(kv: KVService, options?: KvFlagDriverOptions): KvFlagDriver;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Inject, Injectable } from "@velajs/vela";
|
|
16
|
+
import { KVService } from "./kv.service.js";
|
|
17
|
+
export class KvFlagDriver {
|
|
18
|
+
kv;
|
|
19
|
+
name;
|
|
20
|
+
prefix;
|
|
21
|
+
constructor(kv, options = {}){
|
|
22
|
+
this.kv = kv;
|
|
23
|
+
this.name = options.name ?? 'kv';
|
|
24
|
+
this.prefix = options.prefix ?? '';
|
|
25
|
+
}
|
|
26
|
+
get ns() {
|
|
27
|
+
return this.kv.namespace;
|
|
28
|
+
}
|
|
29
|
+
getBoolean(key, fallback, _ctx) {
|
|
30
|
+
return this.read(key, fallback, (v)=>typeof v === 'boolean');
|
|
31
|
+
}
|
|
32
|
+
getString(key, fallback, _ctx) {
|
|
33
|
+
return this.read(key, fallback, (v)=>typeof v === 'string');
|
|
34
|
+
}
|
|
35
|
+
getNumber(key, fallback, _ctx) {
|
|
36
|
+
return this.read(key, fallback, (v)=>typeof v === 'number');
|
|
37
|
+
}
|
|
38
|
+
getObject(key, fallback, _ctx) {
|
|
39
|
+
return this.read(key, fallback, (v)=>typeof v === 'object' && v !== null);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads and JSON-parses the (prefixed) key, returning the parsed value only
|
|
43
|
+
* when `matches` accepts its type; otherwise the caller's fallback. A missing
|
|
44
|
+
* key reads as `null` → fallback. Read/parse errors are left to propagate.
|
|
45
|
+
*/ async read(key, fallback, matches) {
|
|
46
|
+
const value = await this.ns.get(this.prefix + key, 'json');
|
|
47
|
+
if (value === null || value === undefined) return fallback;
|
|
48
|
+
return matches(value) ? value : fallback;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
KvFlagDriver = _ts_decorate([
|
|
52
|
+
Injectable(),
|
|
53
|
+
_ts_param(0, Inject(KVService)),
|
|
54
|
+
_ts_metadata("design:type", Function),
|
|
55
|
+
_ts_metadata("design:paramtypes", [
|
|
56
|
+
typeof KVService === "undefined" ? Object : KVService,
|
|
57
|
+
typeof KvFlagDriverOptions === "undefined" ? Object : KvFlagDriverOptions
|
|
58
|
+
])
|
|
59
|
+
], KvFlagDriver);
|
|
60
|
+
/** Convenience factory for {@link KvFlagDriver}. */ export function kvFlagDriver(kv, options) {
|
|
61
|
+
return new KvFlagDriver(kv, options);
|
|
62
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface DoRuntime {
|
|
|
7
7
|
dispatcher: WsDispatcher;
|
|
8
8
|
registry: CfRoomRegistry;
|
|
9
9
|
server: WsServer;
|
|
10
|
+
/** Gateway paths from `app.entrypoints.ofKind('websocket')` (discovery order). */
|
|
11
|
+
gatewayPaths: string[];
|
|
10
12
|
close(signal?: string): Promise<void>;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
@@ -34,10 +34,17 @@ import { WsServerHolder } from "./ws-server-holder.js";
|
|
|
34
34
|
app.setInstances(await loader.resolveAllInstances());
|
|
35
35
|
await app.callOnModuleInit();
|
|
36
36
|
await app.callOnApplicationBootstrap();
|
|
37
|
+
// The entrypoint registry is the transport contract: one 'websocket' entry
|
|
38
|
+
// per discovered gateway ({ meta: { path, dispatcher } }). Built by
|
|
39
|
+
// callOnApplicationBootstrap(), so this slim no-routes path has it too.
|
|
40
|
+
const wsEntrypoints = app.entrypoints.ofKind('websocket');
|
|
37
41
|
return {
|
|
38
|
-
dispatcher
|
|
42
|
+
// Zero gateways still yields a live dispatcher (module imported, nothing
|
|
43
|
+
// decorated) — fall back to resolving it directly.
|
|
44
|
+
dispatcher: wsEntrypoints[0]?.meta.dispatcher ?? app.get(WsDispatcher),
|
|
39
45
|
registry,
|
|
40
46
|
server,
|
|
47
|
+
gatewayPaths: wsEntrypoints.map((ep)=>ep.meta.path),
|
|
41
48
|
close: (signal)=>app.close(signal)
|
|
42
49
|
};
|
|
43
50
|
}
|
|
@@ -10,7 +10,8 @@ export declare class DoWebSocketHost {
|
|
|
10
10
|
private readonly ctx;
|
|
11
11
|
private readonly dispatcher;
|
|
12
12
|
private readonly registry;
|
|
13
|
-
|
|
13
|
+
private readonly gatewayPaths;
|
|
14
|
+
constructor(ctx: DoStateLike, dispatcher: WsDispatcher, registry: CfRoomRegistry, gatewayPaths?: readonly string[]);
|
|
14
15
|
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */
|
|
15
16
|
defaultPath(): string | undefined;
|
|
16
17
|
/**
|
|
@@ -8,13 +8,15 @@ import { connTag, roomTag } from "./room-id.js";
|
|
|
8
8
|
ctx;
|
|
9
9
|
dispatcher;
|
|
10
10
|
registry;
|
|
11
|
-
|
|
11
|
+
gatewayPaths;
|
|
12
|
+
constructor(ctx, dispatcher, registry, gatewayPaths = []){
|
|
12
13
|
this.ctx = ctx;
|
|
13
14
|
this.dispatcher = dispatcher;
|
|
14
15
|
this.registry = registry;
|
|
16
|
+
this.gatewayPaths = gatewayPaths;
|
|
15
17
|
}
|
|
16
18
|
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */ defaultPath() {
|
|
17
|
-
return this.
|
|
19
|
+
return this.gatewayPaths[0];
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
22
|
* Accept a hibernatable socket: tag it with its hub room + connection id,
|
|
@@ -28,7 +28,7 @@ const PONG = '{"event":"pong"}';
|
|
|
28
28
|
}
|
|
29
29
|
this.ready = ctx.blockConcurrencyWhile(async ()=>{
|
|
30
30
|
const runtime = await buildDoRuntime(rootModule, ctx, env);
|
|
31
|
-
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry);
|
|
31
|
+
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
async fetch(request) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/cloudflare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Cloudflare Workers integration for Vela framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,14 +45,21 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@cloudflare/workers-types": ">=4",
|
|
48
|
-
"@velajs/
|
|
48
|
+
"@velajs/feature-flags": "^0.1.0",
|
|
49
|
+
"@velajs/vela": ">=1.11.0",
|
|
49
50
|
"hono": ">=4"
|
|
50
51
|
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@velajs/feature-flags": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
51
57
|
"devDependencies": {
|
|
52
58
|
"@cloudflare/workers-types": "^4.20260624.1",
|
|
53
59
|
"@swc/cli": "^0.8.1",
|
|
54
60
|
"@swc/core": "^1.15.43",
|
|
55
|
-
"@velajs/
|
|
61
|
+
"@velajs/feature-flags": "^0.1.0",
|
|
62
|
+
"@velajs/vela": "^1.12.0",
|
|
56
63
|
"hono": "^4.12.27",
|
|
57
64
|
"typescript": "^6.0.3",
|
|
58
65
|
"unplugin-swc": "^1.5.9",
|