@velajs/cloudflare 1.9.0 → 1.10.1
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 +8 -3
- package/dist/index.d.ts +805 -39
- package/dist/index.js +1645 -37
- package/dist/index.js.map +1 -0
- package/package.json +55 -41
- package/dist/binding-ref.d.ts +0 -8
- package/dist/binding-ref.js +0 -19
- package/dist/cloudflare-application.d.ts +0 -125
- package/dist/cloudflare-application.js +0 -186
- package/dist/cloudflare-factory.d.ts +0 -88
- package/dist/cloudflare-factory.js +0 -101
- package/dist/decorators/env.d.ts +0 -16
- package/dist/decorators/env.js +0 -22
- package/dist/decorators/queue-consumer.d.ts +0 -23
- package/dist/decorators/queue-consumer.js +0 -40
- package/dist/decorators/scheduled.d.ts +0 -20
- package/dist/decorators/scheduled.js +0 -36
- package/dist/env-ref.d.ts +0 -10
- package/dist/env-ref.js +0 -13
- package/dist/modules/ai.module.d.ts +0 -1
- package/dist/modules/ai.module.js +0 -8
- package/dist/modules/create-binding-module.d.ts +0 -14
- package/dist/modules/create-binding-module.js +0 -42
- package/dist/modules/d1.module.d.ts +0 -1
- package/dist/modules/d1.module.js +0 -8
- package/dist/modules/durable-object.module.d.ts +0 -1
- package/dist/modules/durable-object.module.js +0 -8
- package/dist/modules/env.module.d.ts +0 -17
- package/dist/modules/env.module.js +0 -35
- package/dist/modules/hyperdrive.module.d.ts +0 -1
- package/dist/modules/hyperdrive.module.js +0 -8
- package/dist/modules/kv.module.d.ts +0 -1
- package/dist/modules/kv.module.js +0 -8
- package/dist/modules/queue.module.d.ts +0 -1
- package/dist/modules/queue.module.js +0 -8
- package/dist/modules/r2.module.d.ts +0 -1
- package/dist/modules/r2.module.js +0 -8
- package/dist/modules/vectorize.module.d.ts +0 -1
- package/dist/modules/vectorize.module.js +0 -8
- package/dist/services/ai.service.d.ts +0 -6
- package/dist/services/ai.service.js +0 -33
- package/dist/services/d1.service.d.ts +0 -6
- package/dist/services/d1.service.js +0 -33
- package/dist/services/durable-object.service.d.ts +0 -6
- package/dist/services/durable-object.service.js +0 -33
- package/dist/services/env.service.d.ts +0 -31
- package/dist/services/env.service.js +0 -36
- package/dist/services/flagship-flag.driver.d.ts +0 -57
- package/dist/services/flagship-flag.driver.js +0 -49
- package/dist/services/hyperdrive.service.d.ts +0 -12
- package/dist/services/hyperdrive.service.js +0 -51
- package/dist/services/kv-cache.store.d.ts +0 -19
- package/dist/services/kv-cache.store.js +0 -58
- package/dist/services/kv-flag.driver.d.ts +0 -48
- package/dist/services/kv-flag.driver.js +0 -62
- package/dist/services/kv.service.d.ts +0 -11
- package/dist/services/kv.service.js +0 -33
- package/dist/services/queue.service.d.ts +0 -6
- package/dist/services/queue.service.js +0 -33
- package/dist/services/r2.service.d.ts +0 -6
- package/dist/services/r2.service.js +0 -33
- package/dist/services/vectorize.service.d.ts +0 -6
- package/dist/services/vectorize.service.js +0 -33
- package/dist/storage/index.d.ts +0 -7
- package/dist/storage/index.js +0 -6
- package/dist/storage/r2-storage.driver.d.ts +0 -19
- package/dist/storage/r2-storage.driver.js +0 -61
- package/dist/storage/storage-manager.service.d.ts +0 -16
- package/dist/storage/storage-manager.service.js +0 -56
- package/dist/storage/storage.controller.d.ts +0 -16
- package/dist/storage/storage.controller.js +0 -88
- package/dist/storage/storage.module.d.ts +0 -7
- package/dist/storage/storage.module.js +0 -37
- package/dist/storage/storage.service.d.ts +0 -20
- package/dist/storage/storage.service.js +0 -78
- package/dist/storage/storage.tokens.d.ts +0 -3
- package/dist/storage/storage.tokens.js +0 -2
- package/dist/storage/storage.types.d.ts +0 -17
- package/dist/storage/storage.types.js +0 -1
- package/dist/tokens.d.ts +0 -12
- package/dist/tokens.js +0 -12
- package/dist/types.d.ts +0 -13
- package/dist/types.js +0 -1
- package/dist/websocket/broadcast.d.ts +0 -15
- package/dist/websocket/broadcast.js +0 -26
- package/dist/websocket/cf-room-registry.d.ts +0 -23
- package/dist/websocket/cf-room-registry.js +0 -65
- package/dist/websocket/cf-ws-client.d.ts +0 -28
- package/dist/websocket/cf-ws-client.js +0 -83
- package/dist/websocket/cloudflare-websocket.module.d.ts +0 -11
- package/dist/websocket/cloudflare-websocket.module.js +0 -27
- package/dist/websocket/do-bootstrap.d.ts +0 -21
- package/dist/websocket/do-bootstrap.js +0 -50
- package/dist/websocket/do-state.d.ts +0 -25
- package/dist/websocket/do-state.js +0 -4
- package/dist/websocket/do-websocket-host.d.ts +0 -28
- package/dist/websocket/do-websocket-host.js +0 -69
- package/dist/websocket/index.d.ts +0 -13
- package/dist/websocket/index.js +0 -13
- package/dist/websocket/room-id.d.ts +0 -6
- package/dist/websocket/room-id.js +0 -12
- package/dist/websocket/websocket-routing.d.ts +0 -14
- package/dist/websocket/websocket-routing.js +0 -45
- package/dist/websocket/websocket.durable-object.d.ts +0 -15
- package/dist/websocket/websocket.durable-object.js +0 -72
- package/dist/websocket/ws-server-holder.d.ts +0 -16
- package/dist/websocket/ws-server-holder.js +0 -34
package/dist/index.js
CHANGED
|
@@ -1,37 +1,1645 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
import { CRON_METADATA, ConfigurableModuleBuilder, Controller, Get, Inject, Injectable, InjectionToken, Module, PipelineRunner, Req, VelaApplication, VelaFactory, bootstrap, buildEntrypointExecutionContext, createParamDecorator, defineConfigurableModule, defineMetadata, getMetadata, registerEntrypointKind, runInEntrypointScope, shouldFilterCatch } from "@velajs/vela";
|
|
2
|
+
import { ComponentManager } from "@velajs/vela/internal";
|
|
3
|
+
import { ConnectedSocket, MessageBody, SubscribeMessage, WS_GATEWAY_METADATA, WS_SERVER, WebSocketGateway, WebSocketServer, WsDispatcher, WsException, WsServerImpl, local } from "@velajs/vela/websocket";
|
|
4
|
+
import { LIVE_CURSOR_LOG, LIVE_DRIVER, readPersistedLiveSubscriptions } from "@velajs/vela/live";
|
|
5
|
+
import { joinStoragePath, signUrl, verifySignedUrl } from "@velajs/vela/storage";
|
|
6
|
+
import { DurableObject } from "cloudflare:workers";
|
|
7
|
+
//#region src/binding-ref.ts
|
|
8
|
+
var BindingRef = class {
|
|
9
|
+
bindingName;
|
|
10
|
+
_value;
|
|
11
|
+
constructor(bindingName) {
|
|
12
|
+
this.bindingName = bindingName;
|
|
13
|
+
}
|
|
14
|
+
get value() {
|
|
15
|
+
if (this._value === void 0) throw new Error(`Cloudflare binding '${this.bindingName}' not initialized. Ensure createCloudflareApp() is used and a request has been made.`);
|
|
16
|
+
return this._value;
|
|
17
|
+
}
|
|
18
|
+
/** @internal — called by createCloudflareApp middleware */
|
|
19
|
+
_initialize(value) {
|
|
20
|
+
this._value = value;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/websocket/room-id.ts
|
|
25
|
+
/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */
|
|
26
|
+
function roomTag(roomId) {
|
|
27
|
+
return `room:${roomId}`;
|
|
28
|
+
}
|
|
29
|
+
/** Hibernation tag addressing one connection directly. */
|
|
30
|
+
function connTag(connId) {
|
|
31
|
+
return `conn:${connId}`;
|
|
32
|
+
}
|
|
33
|
+
/** One Durable Object instance per room, addressed by name. */
|
|
34
|
+
function roomToDurableId(ns, roomId) {
|
|
35
|
+
return ns.idFromName(roomId);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/websocket/websocket-routing.ts
|
|
39
|
+
/** Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted gateways only). */
|
|
40
|
+
function collectWsGatewayRoutes(instance) {
|
|
41
|
+
const options = getMetadata(WS_GATEWAY_METADATA, instance.constructor);
|
|
42
|
+
if (!options?.path || !options?.binding) return [];
|
|
43
|
+
return [{
|
|
44
|
+
path: options.path,
|
|
45
|
+
binding: options.binding
|
|
46
|
+
}];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Registers the upgrade routes on the Worker's Hono app. Each route validates
|
|
50
|
+
* the `Upgrade` header, resolves the room's Durable Object, and forwards the raw
|
|
51
|
+
* request — injecting spoof-safe `x-vela-*` headers the DO reads. The DO returns
|
|
52
|
+
* the `101` with the client socket.
|
|
53
|
+
*/
|
|
54
|
+
function registerWebSocketRoutes(hono, routes) {
|
|
55
|
+
for (const route of routes) hono.get(route.path, async (c) => {
|
|
56
|
+
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return c.text("Expected WebSocket upgrade", 426);
|
|
57
|
+
const ns = c.env[route.binding];
|
|
58
|
+
if (!ns) return c.text(`Durable Object binding '${route.binding}' is not configured`, 500);
|
|
59
|
+
const roomId = c.req.param("id") ?? route.path;
|
|
60
|
+
const stub = ns.get(roomToDurableId(ns, roomId));
|
|
61
|
+
const headers = new Headers(c.req.raw.headers);
|
|
62
|
+
headers.delete("x-vela-room");
|
|
63
|
+
headers.delete("x-vela-path");
|
|
64
|
+
headers.delete("x-vela-user");
|
|
65
|
+
headers.set("x-vela-room", roomId);
|
|
66
|
+
headers.set("x-vela-path", route.path);
|
|
67
|
+
const userId = c.get("userId");
|
|
68
|
+
if (userId) headers.set("x-vela-user", String(userId));
|
|
69
|
+
return stub.fetch(new Request(c.req.raw, { headers }));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/cloudflare-application.ts
|
|
74
|
+
registerEntrypointKind({
|
|
75
|
+
kind: "cf:vela-cron",
|
|
76
|
+
metaKey: CRON_METADATA,
|
|
77
|
+
level: "method"
|
|
78
|
+
});
|
|
79
|
+
function invoke(instance, methodName, args) {
|
|
80
|
+
const method = instance[methodName];
|
|
81
|
+
if (typeof method !== "function") throw new Error(`Method '${methodName}' is not a function on ${instance.constructor.name}`);
|
|
82
|
+
return method.apply(instance, args);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Wraps VelaApplication with Cloudflare-specific handlers:
|
|
86
|
+
* - `fetch` — HTTP request handler (from Hono)
|
|
87
|
+
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
88
|
+
* AND vela's own `@Cron()` jobs)
|
|
89
|
+
* - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
|
|
90
|
+
* - `mountOpenApi` — Serve an OpenAPI document (and optional Scalar UI) on
|
|
91
|
+
* the underlying Hono app
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const app = await createCloudflareApp(AppModule);
|
|
96
|
+
* export default {
|
|
97
|
+
* fetch: app.fetch,
|
|
98
|
+
* scheduled: app.scheduled.bind(app),
|
|
99
|
+
* queue: app.queue.bind(app),
|
|
100
|
+
* };
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* // Serve OpenAPI docs alongside your routes
|
|
106
|
+
* const app = await createCloudflareApp(AppModule);
|
|
107
|
+
* const document = createOpenApiDocument(AppModule);
|
|
108
|
+
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
109
|
+
* // GET /openapi.json -> JSON document
|
|
110
|
+
* // GET /scalar -> Scalar UI (loads from CDN)
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
var CloudflareApplication = class {
|
|
114
|
+
app;
|
|
115
|
+
wsGatewayRoutes = [];
|
|
116
|
+
constructor(app) {
|
|
117
|
+
this.app = app;
|
|
118
|
+
}
|
|
119
|
+
get fetch() {
|
|
120
|
+
return this.app.fetch;
|
|
121
|
+
}
|
|
122
|
+
getHonoApp() {
|
|
123
|
+
return this.app.getHonoApp();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a provider from the application's DI container (delegates to
|
|
127
|
+
* `VelaApplication.get`). Handy for grabbing a service — e.g. an auth service —
|
|
128
|
+
* to use inside `createCloudflareApp({ middleware: [...] })` request middleware,
|
|
129
|
+
* which runs outside the DI request pipeline.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const app = await createCloudflareApp(AppModule);
|
|
134
|
+
* const auth = app.get<BetterAuthService>(BetterAuthService);
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
get(token) {
|
|
138
|
+
return this.app.get(token);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
|
|
142
|
+
* underlying Hono app. Delegates verbatim to `VelaApplication.mountOpenApi`,
|
|
143
|
+
* so the JSON endpoint defaults to `/openapi.json` and the Scalar UI (when
|
|
144
|
+
* opted in) defaults to `/scalar`. Edge-safe — the UI HTML loads Scalar from
|
|
145
|
+
* a CDN at runtime, nothing is bundled server-side.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* import { createOpenApiDocument } from '@velajs/vela';
|
|
150
|
+
*
|
|
151
|
+
* const app = await createCloudflareApp(AppModule);
|
|
152
|
+
* const document = createOpenApiDocument(AppModule, {
|
|
153
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
154
|
+
* });
|
|
155
|
+
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
156
|
+
* // GET /openapi.json -> { openapi: '3.1.0', ... }
|
|
157
|
+
* // GET /scalar -> Scalar UI HTML
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
mountOpenApi(options) {
|
|
161
|
+
this.app.mountOpenApi(options);
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @internal — scans instances for `@WebSocketGateway({ path, binding })`
|
|
166
|
+
* upgrade routes. Queue/scheduled handlers are NOT scanned anymore: they
|
|
167
|
+
* come from `app.entrypoints` (`cf:queue` / `cf:scheduled` / `cf:vela-cron`
|
|
168
|
+
* kinds) at dispatch time.
|
|
169
|
+
*/
|
|
170
|
+
scanInstances(instances) {
|
|
171
|
+
for (const instance of instances) {
|
|
172
|
+
if (!instance || typeof instance !== "object") continue;
|
|
173
|
+
this.wsGatewayRoutes.push(...collectWsGatewayRoutes(instance));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** @internal — upgrade routes discovered from `@WebSocketGateway({ path, binding })`. */
|
|
177
|
+
getWsGatewayRoutes() {
|
|
178
|
+
return this.wsGatewayRoutes;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Handle Cloudflare scheduled (cron) events.
|
|
182
|
+
* Matches the event's cron expression to `@Scheduled()` and vela `@Cron()`
|
|
183
|
+
* handlers read from `app.entrypoints`; each handler runs inside a fresh
|
|
184
|
+
* request-scoped child (request-scoped providers rebuild per tick).
|
|
185
|
+
*/
|
|
186
|
+
async scheduled(event, env, ctx) {
|
|
187
|
+
const handlers = [...this.app.entrypoints.ofKind("cf:scheduled").map((ep) => ({
|
|
188
|
+
ep,
|
|
189
|
+
cron: ep.meta.cron
|
|
190
|
+
})), ...this.app.entrypoints.ofKind("cf:vela-cron").map((ep) => ({
|
|
191
|
+
ep,
|
|
192
|
+
cron: ep.meta.expression
|
|
193
|
+
}))].filter((h) => h.cron === event.cron);
|
|
194
|
+
await Promise.all(handlers.map(({ ep }) => this.dispatchEntrypoint(ep, [
|
|
195
|
+
event,
|
|
196
|
+
env,
|
|
197
|
+
ctx
|
|
198
|
+
])));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Run one entrypoint handler inside a fresh request scope, through the
|
|
202
|
+
* shared guard → interceptor pipeline (components declared with
|
|
203
|
+
* `@UseGuards`/`@UseInterceptors`/`@UseFilters` on the consumer class or
|
|
204
|
+
* method). HTTP-global components deliberately do NOT apply — an HTTP auth
|
|
205
|
+
* guard has no business rejecting a queue batch. Unclaimed errors rethrow
|
|
206
|
+
* so the platform's retry semantics stay intact.
|
|
207
|
+
*/
|
|
208
|
+
async dispatchEntrypoint(ep, args) {
|
|
209
|
+
const targetClass = ep.token;
|
|
210
|
+
const methodName = String(ep.methodName);
|
|
211
|
+
const context = buildEntrypointExecutionContext(ep.kind, targetClass, methodName, args[0]);
|
|
212
|
+
await runInEntrypointScope(this.app.getContainer(), async (scope) => {
|
|
213
|
+
const instance = scope.resolve(ep.token);
|
|
214
|
+
const guards = ComponentManager.resolveGuards(ComponentManager.getScopedComponents("guard", targetClass, methodName), scope);
|
|
215
|
+
const interceptors = ComponentManager.resolveInterceptors(ComponentManager.getScopedComponents("interceptor", targetClass, methodName), scope);
|
|
216
|
+
const filters = ComponentManager.resolveFilters([...ComponentManager.getScopedComponents("filter", targetClass, methodName)].reverse(), scope);
|
|
217
|
+
try {
|
|
218
|
+
await PipelineRunner.run({
|
|
219
|
+
context,
|
|
220
|
+
guards,
|
|
221
|
+
interceptors,
|
|
222
|
+
resolveArgs: async () => args,
|
|
223
|
+
invoke: async (resolved) => invoke(instance, methodName, resolved)
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
for (const filter of filters) if (shouldFilterCatch(filter, error)) {
|
|
227
|
+
await filter.catch(error, context);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Handle Cloudflare Queue consumer events.
|
|
236
|
+
* Matches the batch queue name to `@QueueConsumer()` handlers read from
|
|
237
|
+
* `app.entrypoints`; each batch is processed inside a fresh request-scoped
|
|
238
|
+
* child (request-scoped providers rebuild per batch — no boot-time captives).
|
|
239
|
+
*/
|
|
240
|
+
async queue(batch, env, ctx) {
|
|
241
|
+
const handlers = this.app.entrypoints.ofKind("cf:queue").filter((ep) => ep.meta.queueName === batch.queue);
|
|
242
|
+
await Promise.all(handlers.map((ep) => this.dispatchEntrypoint(ep, [
|
|
243
|
+
batch,
|
|
244
|
+
env,
|
|
245
|
+
ctx
|
|
246
|
+
])));
|
|
247
|
+
}
|
|
248
|
+
async close(signal) {
|
|
249
|
+
return this.app.close(signal);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/env-ref.ts
|
|
254
|
+
const ENV_SENTINEL = "__cf_env__";
|
|
255
|
+
/**
|
|
256
|
+
* Holds the entire Cloudflare Worker `env` record (bindings + vars/secrets),
|
|
257
|
+
* not a single binding. Subclasses {@link BindingRef} so createCloudflareApp's
|
|
258
|
+
* binding-init middleware collects and initializes it through the same path;
|
|
259
|
+
* the factory special-cases it to pass the full `env` instead of one binding.
|
|
260
|
+
*/
|
|
261
|
+
var EnvRef = class extends BindingRef {
|
|
262
|
+
constructor() {
|
|
263
|
+
super(ENV_SENTINEL);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/websocket/cf-ws-client.ts
|
|
268
|
+
const MAX_ATTACHMENT_BYTES = 16384;
|
|
269
|
+
const encoder = new TextEncoder();
|
|
270
|
+
const EMPTY = {
|
|
271
|
+
connId: "",
|
|
272
|
+
path: "",
|
|
273
|
+
rooms: [],
|
|
274
|
+
data: {}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.
|
|
278
|
+
* Per-connection state lives in the hibernation attachment (survives eviction),
|
|
279
|
+
* so a fresh `CfWsClient` is reconstructed per message with no in-memory state.
|
|
280
|
+
*/
|
|
281
|
+
var CfWsClient = class {
|
|
282
|
+
ctx;
|
|
283
|
+
ws;
|
|
284
|
+
attachment;
|
|
285
|
+
constructor(ctx, ws) {
|
|
286
|
+
this.ctx = ctx;
|
|
287
|
+
this.ws = ws;
|
|
288
|
+
const raw = ws.deserializeAttachment();
|
|
289
|
+
this.attachment = raw ?? {
|
|
290
|
+
...EMPTY,
|
|
291
|
+
data: {}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
get id() {
|
|
295
|
+
return this.attachment.connId;
|
|
296
|
+
}
|
|
297
|
+
/** The gateway route path this socket connected on (used to route messages). */
|
|
298
|
+
get path() {
|
|
299
|
+
return this.attachment.path;
|
|
300
|
+
}
|
|
301
|
+
get rooms() {
|
|
302
|
+
return new Set(this.attachment.rooms);
|
|
303
|
+
}
|
|
304
|
+
get data() {
|
|
305
|
+
return this.attachment.data;
|
|
306
|
+
}
|
|
307
|
+
set data(value) {
|
|
308
|
+
this.attachment.data = value;
|
|
309
|
+
}
|
|
310
|
+
get raw() {
|
|
311
|
+
return this.ws;
|
|
312
|
+
}
|
|
313
|
+
send(event, data, id) {
|
|
314
|
+
this.ws.send(JSON.stringify(id !== void 0 ? {
|
|
315
|
+
id,
|
|
316
|
+
event,
|
|
317
|
+
data
|
|
318
|
+
} : {
|
|
319
|
+
event,
|
|
320
|
+
data
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
sendRaw(payload) {
|
|
324
|
+
this.ws.send(payload);
|
|
325
|
+
}
|
|
326
|
+
join(room) {
|
|
327
|
+
if (!this.attachment.rooms.includes(room)) {
|
|
328
|
+
this.attachment.rooms.push(room);
|
|
329
|
+
this.persist();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
leave(room) {
|
|
333
|
+
const next = this.attachment.rooms.filter((r) => r !== room);
|
|
334
|
+
if (next.length !== this.attachment.rooms.length) {
|
|
335
|
+
this.attachment.rooms = next;
|
|
336
|
+
this.persist();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Persist `data`/room mutations to the hibernation attachment. */
|
|
340
|
+
commit() {
|
|
341
|
+
this.persist();
|
|
342
|
+
}
|
|
343
|
+
close(code, reason) {
|
|
344
|
+
this.ws.close(code, reason);
|
|
345
|
+
}
|
|
346
|
+
persist() {
|
|
347
|
+
const serialized = JSON.stringify(this.attachment);
|
|
348
|
+
if (encoder.encode(serialized).length > MAX_ATTACHMENT_BYTES) throw new Error("WebSocket attachment exceeds the 16 KiB Cloudflare limit. Store large per-connection state in Durable Object storage keyed by connId instead.");
|
|
349
|
+
this.ws.serializeAttachment(this.attachment);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/websocket/do-live.ts
|
|
354
|
+
const DEFAULT_ROOM = "default";
|
|
355
|
+
const DEFAULT_MAX_LOG_ROWS = 4096;
|
|
356
|
+
/**
|
|
357
|
+
* The durable `CursorLog`: an append-only tag-invalidation log in the DO's
|
|
358
|
+
* SQLite (`__vela_live_log`, AUTOINCREMENT seq = cursor) plus an epoch UUID in
|
|
359
|
+
* `__vela_live_meta`. Because the cursor survives hibernation AND trims (it is
|
|
360
|
+
* read from `sqlite_sequence`, lunora's `ctx-db-cdc.ts` trick), a reconnecting
|
|
361
|
+
* client whose gap the log still covers gets a tiny `resume` instead of a
|
|
362
|
+
* re-run — the real-resume half of the live protocol.
|
|
363
|
+
*
|
|
364
|
+
* Constructed un-initialized at module-composition time (the same app module
|
|
365
|
+
* bootstraps in the Worker AND in each DO); `initDoLive` wires the SQLite
|
|
366
|
+
* handle inside the DO. In the Worker isolate it stays un-initialized — and is
|
|
367
|
+
* never consulted there, because `durableObjectLive()` routes every
|
|
368
|
+
* invalidation to the room DO's log (one log scope per room, exactly the
|
|
369
|
+
* protocol's model).
|
|
370
|
+
*/
|
|
371
|
+
var DoCursorLog = class {
|
|
372
|
+
maxRows;
|
|
373
|
+
sql;
|
|
374
|
+
epoch;
|
|
375
|
+
constructor(maxRows = DEFAULT_MAX_LOG_ROWS) {
|
|
376
|
+
this.maxRows = maxRows;
|
|
377
|
+
}
|
|
378
|
+
/** @internal — called by `initDoLive` with the DO's `ctx.storage.sql`. */
|
|
379
|
+
_initialize(sql) {
|
|
380
|
+
this.sql = sql;
|
|
381
|
+
sql.exec("CREATE TABLE IF NOT EXISTS __vela_live_log (seq INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, tags TEXT NOT NULL)");
|
|
382
|
+
sql.exec("CREATE TABLE IF NOT EXISTS __vela_live_meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)");
|
|
383
|
+
const row = sql.exec("SELECT v FROM __vela_live_meta WHERE k = 'epoch'").toArray()[0];
|
|
384
|
+
if (row && typeof row.v === "string") this.epoch = row.v;
|
|
385
|
+
else {
|
|
386
|
+
this.epoch = crypto.randomUUID();
|
|
387
|
+
sql.exec("INSERT INTO __vela_live_meta (k, v) VALUES ('epoch', ?)", this.epoch);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
append(tags) {
|
|
391
|
+
const sql = this.assertReady();
|
|
392
|
+
sql.exec("INSERT INTO __vela_live_log (ts, tags) VALUES (?, ?)", Date.now(), JSON.stringify(tags));
|
|
393
|
+
const stamp = this.current();
|
|
394
|
+
if (stamp.cursor > this.maxRows) sql.exec("DELETE FROM __vela_live_log WHERE seq <= ?", stamp.cursor - this.maxRows);
|
|
395
|
+
return stamp;
|
|
396
|
+
}
|
|
397
|
+
current() {
|
|
398
|
+
const sql = this.assertReady();
|
|
399
|
+
let cursor = 0;
|
|
400
|
+
try {
|
|
401
|
+
const row = sql.exec("SELECT seq FROM sqlite_sequence WHERE name = '__vela_live_log'").toArray()[0];
|
|
402
|
+
cursor = typeof row?.seq === "number" ? row.seq : Number(row?.seq ?? 0);
|
|
403
|
+
} catch {
|
|
404
|
+
cursor = 0;
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
cursor,
|
|
408
|
+
epoch: this.epoch
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
evaluateResume(sinceCursor, sinceEpoch, subscriptionTags) {
|
|
412
|
+
const sql = this.assertReady();
|
|
413
|
+
const { cursor, epoch } = this.current();
|
|
414
|
+
if (sinceEpoch !== epoch) return "snapshot";
|
|
415
|
+
if (sinceCursor > cursor) return "snapshot";
|
|
416
|
+
if (sinceCursor === cursor) return "resume";
|
|
417
|
+
const minRow = sql.exec("SELECT MIN(seq) AS m FROM __vela_live_log").toArray()[0];
|
|
418
|
+
const min = minRow?.m == null ? void 0 : Number(minRow.m);
|
|
419
|
+
if (min === void 0 || min > sinceCursor + 1) return "snapshot";
|
|
420
|
+
const subTags = new Set(subscriptionTags);
|
|
421
|
+
for (const row of sql.exec("SELECT tags FROM __vela_live_log WHERE seq > ?", sinceCursor).toArray()) {
|
|
422
|
+
let tags;
|
|
423
|
+
try {
|
|
424
|
+
tags = JSON.parse(String(row.tags));
|
|
425
|
+
} catch {
|
|
426
|
+
return "snapshot";
|
|
427
|
+
}
|
|
428
|
+
if (Array.isArray(tags) && tags.some((tag) => subTags.has(tag))) return "rerun";
|
|
429
|
+
}
|
|
430
|
+
return "resume";
|
|
431
|
+
}
|
|
432
|
+
assertReady() {
|
|
433
|
+
if (!this.sql) throw new Error("DoCursorLog is not initialized. It only runs inside a SQLite-backed Durable Object (wrangler: new_sqlite_classes) — Worker-side invalidations must go through durableObjectLive(), which routes them to the room DO's log.");
|
|
434
|
+
return this.sql;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* The Cloudflare `LiveDriver`. Dual-mode, because the SAME app module
|
|
439
|
+
* bootstraps in both isolates:
|
|
440
|
+
*
|
|
441
|
+
* - **Worker** (HTTP mutations, queue consumers, crons): route the command to
|
|
442
|
+
* the room's Durable Object over the `invalidate` RPC — the same canonical
|
|
443
|
+
* `roomToDurableId` mapping the upgrade route and `broadcastToRoom` use —
|
|
444
|
+
* and return THAT log scope's commit stamp (what `Vela-Commit-Cursor`
|
|
445
|
+
* must carry).
|
|
446
|
+
* - **DO** (writes issued from inside the object): apply to the local engine.
|
|
447
|
+
*/
|
|
448
|
+
function durableObjectLive(options) {
|
|
449
|
+
let sink;
|
|
450
|
+
let env;
|
|
451
|
+
let localMode = false;
|
|
452
|
+
return {
|
|
453
|
+
kind: "durable-object",
|
|
454
|
+
bind(boundSink) {
|
|
455
|
+
sink = boundSink;
|
|
456
|
+
},
|
|
457
|
+
_initializeEnv(capturedEnv) {
|
|
458
|
+
env = capturedEnv;
|
|
459
|
+
},
|
|
460
|
+
_setLocalMode() {
|
|
461
|
+
localMode = true;
|
|
462
|
+
},
|
|
463
|
+
dispatch(cmd) {
|
|
464
|
+
if (localMode) return sink?.applyInvalidation(cmd);
|
|
465
|
+
const namespace = env?.[options.binding];
|
|
466
|
+
if (!namespace) throw new Error(`durableObjectLive: binding '${options.binding}' is not available. In a Worker, createCloudflareApp() captures env on the first request; check the wrangler binding name.`);
|
|
467
|
+
const room = cmd.room ?? options.defaultRoom ?? DEFAULT_ROOM;
|
|
468
|
+
return namespace.get(roomToDurableId(namespace, room)).invalidate({
|
|
469
|
+
...cmd,
|
|
470
|
+
room
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const isCfLiveDriver = (value) => typeof value === "object" && value !== null && typeof value._setLocalMode === "function" && typeof value._initializeEnv === "function";
|
|
476
|
+
/** Worker-side wiring, called from `cloudflareAdapter`'s first-request middleware. */
|
|
477
|
+
function initializeWorkerLive(container, env) {
|
|
478
|
+
let driver;
|
|
479
|
+
try {
|
|
480
|
+
driver = container.resolve(LIVE_DRIVER);
|
|
481
|
+
} catch {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (isCfLiveDriver(driver)) driver._initializeEnv(env);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* DO-side wiring, called from `buildDoRuntime`: initialize the SQLite cursor
|
|
488
|
+
* log, flip the driver to local mode, and replay every hibernation-persisted
|
|
489
|
+
* subscription into the (fresh) engine so an eviction is invisible to
|
|
490
|
+
* subscribers. Returns the engine for the `invalidate` RPC, or undefined when
|
|
491
|
+
* the app doesn't use LiveModule.
|
|
492
|
+
*/
|
|
493
|
+
function initDoLive(app, container, ctx) {
|
|
494
|
+
const entry = app.entrypoints.ofKind("live")[0];
|
|
495
|
+
if (!entry) return void 0;
|
|
496
|
+
const engine = entry.meta.engine;
|
|
497
|
+
try {
|
|
498
|
+
const log = container.resolve(LIVE_CURSOR_LOG);
|
|
499
|
+
if (log instanceof DoCursorLog) {
|
|
500
|
+
const sql = ctx.storage?.sql;
|
|
501
|
+
if (!sql) throw new Error("DoCursorLog requires a SQLite-backed Durable Object: add this class to wrangler's `migrations[].new_sqlite_classes`. Falling back is not possible — either enable SQLite or drop the `log: durableObjectCursorLog()` option (snapshot-on-reconnect semantics).");
|
|
502
|
+
log._initialize(sql);
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
if (err instanceof Error && err.message.includes("new_sqlite_classes")) throw err;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const driver = container.resolve(LIVE_DRIVER);
|
|
509
|
+
if (isCfLiveDriver(driver)) driver._setLocalMode();
|
|
510
|
+
} catch {}
|
|
511
|
+
for (const ws of ctx.getWebSockets()) {
|
|
512
|
+
const client = new CfWsClient(ctx, ws);
|
|
513
|
+
for (const record of readPersistedLiveSubscriptions(client)) engine.restoreSubscription(client.path, client, record);
|
|
514
|
+
}
|
|
515
|
+
return engine;
|
|
516
|
+
}
|
|
517
|
+
/** Ergonomic alias: the log option for `LiveModule.forRoot` on Cloudflare. */
|
|
518
|
+
function durableObjectCursorLog(maxRows) {
|
|
519
|
+
return new DoCursorLog(maxRows);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Invalidate live tags in a room from a Worker (controller / cron / queue
|
|
523
|
+
* consumer) — the live sibling of `broadcastToRoom`. Returns the room log
|
|
524
|
+
* scope's commit stamp for `Vela-Commit-Cursor` stamping.
|
|
525
|
+
*/
|
|
526
|
+
async function liveInvalidateToRoom(ns, room, tags) {
|
|
527
|
+
return ns.get(roomToDurableId(ns, room)).invalidate({
|
|
528
|
+
room,
|
|
529
|
+
tags
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/cloudflare-factory.ts
|
|
534
|
+
function collectBindingRefs(container) {
|
|
535
|
+
const refs = [];
|
|
536
|
+
for (const value of container.getUseValues()) if (value instanceof BindingRef) refs.push(value);
|
|
537
|
+
return refs;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Create a Cloudflare Workers application.
|
|
541
|
+
*
|
|
542
|
+
* Sets up a one-time Hono middleware that captures `c.env` on the first
|
|
543
|
+
* request and initializes all configured binding refs. Optional
|
|
544
|
+
* {@link CreateCloudflareAppOptions} are forwarded to the underlying
|
|
545
|
+
* `VelaFactory.create` so consumers don't have to wrap the resulting
|
|
546
|
+
* application in an outer Hono just to set a `globalPrefix` or attach
|
|
547
|
+
* extra request middleware.
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```ts
|
|
551
|
+
* // Minimal — backwards compatible
|
|
552
|
+
* const app = await createCloudflareApp(AppModule);
|
|
553
|
+
* export default app; // has .fetch, .scheduled, .queue
|
|
554
|
+
* ```
|
|
555
|
+
*
|
|
556
|
+
* @example
|
|
557
|
+
* ```ts
|
|
558
|
+
* // With a global prefix and outer middleware
|
|
559
|
+
* const app = await createCloudflareApp(AppModule, {
|
|
560
|
+
* globalPrefix: '/v1',
|
|
561
|
+
* middleware: [
|
|
562
|
+
* async (c, next) => {
|
|
563
|
+
* c.set('tenantId', c.req.header('x-tenant-id') ?? 'public');
|
|
564
|
+
* await next();
|
|
565
|
+
* },
|
|
566
|
+
* ],
|
|
567
|
+
* });
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
/**
|
|
571
|
+
* The Cloudflare platform binding as a vela {@link RuntimeAdapter}: a one-time
|
|
572
|
+
* request middleware captures `c.env` on the first request and initializes
|
|
573
|
+
* every `BindingRef`/`EnvRef` (collected at `onBootstrap`, before any request
|
|
574
|
+
* can arrive). Adapter `requestMiddleware` is prepended to the global chain by
|
|
575
|
+
* `VelaFactory.create`, so user middleware can safely read binding refs.
|
|
576
|
+
*
|
|
577
|
+
* Exposed so consumers composing `VelaFactory.create` themselves can opt in:
|
|
578
|
+
*
|
|
579
|
+
* ```ts
|
|
580
|
+
* const app = await VelaFactory.create(AppModule, { adapters: [cloudflareAdapter()] });
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
function cloudflareAdapter() {
|
|
584
|
+
let initialized = false;
|
|
585
|
+
let refs = [];
|
|
586
|
+
let liveContainer;
|
|
587
|
+
return {
|
|
588
|
+
name: "cloudflare",
|
|
589
|
+
requestMiddleware: [async (c, next) => {
|
|
590
|
+
if (!initialized) {
|
|
591
|
+
initialized = true;
|
|
592
|
+
const env = c.env ?? {};
|
|
593
|
+
for (const ref of refs) if (ref instanceof EnvRef) ref._initialize(env);
|
|
594
|
+
else ref._initialize(env[ref.bindingName]);
|
|
595
|
+
if (liveContainer) initializeWorkerLive(liveContainer, env);
|
|
596
|
+
}
|
|
597
|
+
await next();
|
|
598
|
+
}],
|
|
599
|
+
onBootstrap: ({ container }) => {
|
|
600
|
+
liveContainer = container;
|
|
601
|
+
refs = collectBindingRefs(container);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
async function createCloudflareApp(rootModule, options = {}) {
|
|
606
|
+
const velaApp = await VelaFactory.create(rootModule, {
|
|
607
|
+
globalPrefix: options.globalPrefix,
|
|
608
|
+
middleware: options.middleware,
|
|
609
|
+
adapters: [cloudflareAdapter()]
|
|
610
|
+
});
|
|
611
|
+
const cfApp = new CloudflareApplication(velaApp);
|
|
612
|
+
cfApp.scanInstances(velaApp.getInstances());
|
|
613
|
+
registerWebSocketRoutes(cfApp.getHonoApp(), cfApp.getWsGatewayRoutes());
|
|
614
|
+
return cfApp;
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/tokens.ts
|
|
618
|
+
const KV_BINDING_REF = new InjectionToken("CF_KV_BINDING_REF");
|
|
619
|
+
const D1_BINDING_REF = new InjectionToken("CF_D1_BINDING_REF");
|
|
620
|
+
const R2_BINDING_REF = new InjectionToken("CF_R2_BINDING_REF");
|
|
621
|
+
const QUEUE_BINDING_REF = new InjectionToken("CF_QUEUE_BINDING_REF");
|
|
622
|
+
const DO_BINDING_REF = new InjectionToken("CF_DO_BINDING_REF");
|
|
623
|
+
const AI_BINDING_REF = new InjectionToken("CF_AI_BINDING_REF");
|
|
624
|
+
const VECTORIZE_BINDING_REF = new InjectionToken("CF_VECTORIZE_BINDING_REF");
|
|
625
|
+
const HYPERDRIVE_BINDING_REF = new InjectionToken("CF_HYPERDRIVE_BINDING_REF");
|
|
626
|
+
const ENV_REF = new InjectionToken("CF_ENV_REF");
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateMetadata.js
|
|
629
|
+
function __decorateMetadata(k, v) {
|
|
630
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateParam.js
|
|
634
|
+
function __decorateParam(paramIndex, decorator) {
|
|
635
|
+
return function(target, key) {
|
|
636
|
+
decorator(target, key, paramIndex);
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
|
|
641
|
+
function __decorate(decorators, target, key, desc) {
|
|
642
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
643
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
644
|
+
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;
|
|
645
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/services/kv.service.ts
|
|
649
|
+
let KVService = class KVService {
|
|
650
|
+
ref;
|
|
651
|
+
constructor(ref) {
|
|
652
|
+
this.ref = ref;
|
|
653
|
+
}
|
|
654
|
+
get namespace() {
|
|
655
|
+
return this.ref.value;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
KVService = __decorate([
|
|
659
|
+
Injectable(),
|
|
660
|
+
__decorateParam(0, Inject(KV_BINDING_REF)),
|
|
661
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
662
|
+
], KVService);
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/modules/create-binding-module.ts
|
|
665
|
+
function createBindingModule(opts) {
|
|
666
|
+
const className = `${opts.name}Module`;
|
|
667
|
+
const moduleClass = { [className]: class {} }[className];
|
|
668
|
+
return defineConfigurableModule({
|
|
669
|
+
module: moduleClass,
|
|
670
|
+
methodName: "forRoot",
|
|
671
|
+
keyFrom: ({ binding }) => binding,
|
|
672
|
+
providers: ({ binding }) => [{
|
|
673
|
+
provide: opts.bindingRefToken,
|
|
674
|
+
useValue: new BindingRef(binding)
|
|
675
|
+
}, opts.serviceClass],
|
|
676
|
+
exports: [opts.serviceClass, opts.bindingRefToken]
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/modules/kv.module.ts
|
|
681
|
+
const KVModule = createBindingModule({
|
|
682
|
+
name: "KV",
|
|
683
|
+
serviceClass: KVService,
|
|
684
|
+
bindingRefToken: KV_BINDING_REF
|
|
685
|
+
});
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/services/d1.service.ts
|
|
688
|
+
let D1Service = class D1Service {
|
|
689
|
+
ref;
|
|
690
|
+
constructor(ref) {
|
|
691
|
+
this.ref = ref;
|
|
692
|
+
}
|
|
693
|
+
get database() {
|
|
694
|
+
return this.ref.value;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
D1Service = __decorate([
|
|
698
|
+
Injectable(),
|
|
699
|
+
__decorateParam(0, Inject(D1_BINDING_REF)),
|
|
700
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
701
|
+
], D1Service);
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/modules/d1.module.ts
|
|
704
|
+
const D1Module = createBindingModule({
|
|
705
|
+
name: "D1",
|
|
706
|
+
serviceClass: D1Service,
|
|
707
|
+
bindingRefToken: D1_BINDING_REF
|
|
708
|
+
});
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/services/r2.service.ts
|
|
711
|
+
let R2Service = class R2Service {
|
|
712
|
+
ref;
|
|
713
|
+
constructor(ref) {
|
|
714
|
+
this.ref = ref;
|
|
715
|
+
}
|
|
716
|
+
get bucket() {
|
|
717
|
+
return this.ref.value;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
R2Service = __decorate([
|
|
721
|
+
Injectable(),
|
|
722
|
+
__decorateParam(0, Inject(R2_BINDING_REF)),
|
|
723
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
724
|
+
], R2Service);
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/modules/r2.module.ts
|
|
727
|
+
const R2Module = createBindingModule({
|
|
728
|
+
name: "R2",
|
|
729
|
+
serviceClass: R2Service,
|
|
730
|
+
bindingRefToken: R2_BINDING_REF
|
|
731
|
+
});
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/services/queue.service.ts
|
|
734
|
+
let QueueService = class QueueService {
|
|
735
|
+
ref;
|
|
736
|
+
constructor(ref) {
|
|
737
|
+
this.ref = ref;
|
|
738
|
+
}
|
|
739
|
+
get queue() {
|
|
740
|
+
return this.ref.value;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
QueueService = __decorate([
|
|
744
|
+
Injectable(),
|
|
745
|
+
__decorateParam(0, Inject(QUEUE_BINDING_REF)),
|
|
746
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
747
|
+
], QueueService);
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/modules/queue.module.ts
|
|
750
|
+
const QueueModule = createBindingModule({
|
|
751
|
+
name: "Queue",
|
|
752
|
+
serviceClass: QueueService,
|
|
753
|
+
bindingRefToken: QUEUE_BINDING_REF
|
|
754
|
+
});
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/services/durable-object.service.ts
|
|
757
|
+
let DurableObjectService = class DurableObjectService {
|
|
758
|
+
ref;
|
|
759
|
+
constructor(ref) {
|
|
760
|
+
this.ref = ref;
|
|
761
|
+
}
|
|
762
|
+
get namespace() {
|
|
763
|
+
return this.ref.value;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
DurableObjectService = __decorate([
|
|
767
|
+
Injectable(),
|
|
768
|
+
__decorateParam(0, Inject(DO_BINDING_REF)),
|
|
769
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
770
|
+
], DurableObjectService);
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/modules/durable-object.module.ts
|
|
773
|
+
const DurableObjectModule = createBindingModule({
|
|
774
|
+
name: "DurableObject",
|
|
775
|
+
serviceClass: DurableObjectService,
|
|
776
|
+
bindingRefToken: DO_BINDING_REF
|
|
777
|
+
});
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/services/ai.service.ts
|
|
780
|
+
let AIService = class AIService {
|
|
781
|
+
ref;
|
|
782
|
+
constructor(ref) {
|
|
783
|
+
this.ref = ref;
|
|
784
|
+
}
|
|
785
|
+
get binding() {
|
|
786
|
+
return this.ref.value;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
AIService = __decorate([
|
|
790
|
+
Injectable(),
|
|
791
|
+
__decorateParam(0, Inject(AI_BINDING_REF)),
|
|
792
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
793
|
+
], AIService);
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/modules/ai.module.ts
|
|
796
|
+
const AIModule = createBindingModule({
|
|
797
|
+
name: "AI",
|
|
798
|
+
serviceClass: AIService,
|
|
799
|
+
bindingRefToken: AI_BINDING_REF
|
|
800
|
+
});
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/services/vectorize.service.ts
|
|
803
|
+
let VectorizeService = class VectorizeService {
|
|
804
|
+
ref;
|
|
805
|
+
constructor(ref) {
|
|
806
|
+
this.ref = ref;
|
|
807
|
+
}
|
|
808
|
+
get index() {
|
|
809
|
+
return this.ref.value;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
VectorizeService = __decorate([
|
|
813
|
+
Injectable(),
|
|
814
|
+
__decorateParam(0, Inject(VECTORIZE_BINDING_REF)),
|
|
815
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
816
|
+
], VectorizeService);
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/modules/vectorize.module.ts
|
|
819
|
+
const VectorizeModule = createBindingModule({
|
|
820
|
+
name: "Vectorize",
|
|
821
|
+
serviceClass: VectorizeService,
|
|
822
|
+
bindingRefToken: VECTORIZE_BINDING_REF
|
|
823
|
+
});
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/services/hyperdrive.service.ts
|
|
826
|
+
let HyperdriveService = class HyperdriveService {
|
|
827
|
+
ref;
|
|
828
|
+
constructor(ref) {
|
|
829
|
+
this.ref = ref;
|
|
830
|
+
}
|
|
831
|
+
get binding() {
|
|
832
|
+
return this.ref.value;
|
|
833
|
+
}
|
|
834
|
+
get connectionString() {
|
|
835
|
+
return this.binding.connectionString;
|
|
836
|
+
}
|
|
837
|
+
get host() {
|
|
838
|
+
return this.binding.host;
|
|
839
|
+
}
|
|
840
|
+
get port() {
|
|
841
|
+
return this.binding.port;
|
|
842
|
+
}
|
|
843
|
+
get user() {
|
|
844
|
+
return this.binding.user;
|
|
845
|
+
}
|
|
846
|
+
get password() {
|
|
847
|
+
return this.binding.password;
|
|
848
|
+
}
|
|
849
|
+
get database() {
|
|
850
|
+
return this.binding.database;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
HyperdriveService = __decorate([
|
|
854
|
+
Injectable(),
|
|
855
|
+
__decorateParam(0, Inject(HYPERDRIVE_BINDING_REF)),
|
|
856
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
857
|
+
], HyperdriveService);
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/modules/hyperdrive.module.ts
|
|
860
|
+
const HyperdriveModule = createBindingModule({
|
|
861
|
+
name: "Hyperdrive",
|
|
862
|
+
serviceClass: HyperdriveService,
|
|
863
|
+
bindingRefToken: HYPERDRIVE_BINDING_REF
|
|
864
|
+
});
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/services/env.service.ts
|
|
867
|
+
let EnvService = class EnvService {
|
|
868
|
+
ref;
|
|
869
|
+
constructor(ref) {
|
|
870
|
+
this.ref = ref;
|
|
871
|
+
}
|
|
872
|
+
/** The full env record. Throws if read before the first request. */
|
|
873
|
+
get env() {
|
|
874
|
+
return this.ref.value;
|
|
875
|
+
}
|
|
876
|
+
/** Read a single env entry (binding, var, or secret) by name. */
|
|
877
|
+
get(key) {
|
|
878
|
+
return this.ref.value[key];
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
EnvService = __decorate([
|
|
882
|
+
Injectable(),
|
|
883
|
+
__decorateParam(0, Inject(ENV_REF)),
|
|
884
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
885
|
+
], EnvService);
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/modules/env.module.ts
|
|
888
|
+
/**
|
|
889
|
+
* Provides {@link EnvService} GLOBALLY so any module's provider factories can
|
|
890
|
+
* `inject: [EnvService]`. Register once on the root module:
|
|
891
|
+
*
|
|
892
|
+
* ```ts
|
|
893
|
+
* @Module({ imports: [EnvModule.forRoot(), AuthModule.forRootAsync({ ... })] })
|
|
894
|
+
* class AppModule {}
|
|
895
|
+
* ```
|
|
896
|
+
*
|
|
897
|
+
* The {@link EnvRef} it provides is collected and initialized by
|
|
898
|
+
* createCloudflareApp's binding-init middleware on the first request (the
|
|
899
|
+
* factory passes it the full `env`, not a single binding).
|
|
900
|
+
*/
|
|
901
|
+
var EnvModule = class EnvModule {
|
|
902
|
+
static forRoot() {
|
|
903
|
+
const ref = new EnvRef();
|
|
904
|
+
return {
|
|
905
|
+
module: EnvModule,
|
|
906
|
+
global: true,
|
|
907
|
+
providers: [{
|
|
908
|
+
provide: ENV_REF,
|
|
909
|
+
useValue: ref
|
|
910
|
+
}, EnvService],
|
|
911
|
+
exports: [EnvService, ENV_REF]
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/storage/r2-storage.driver.ts
|
|
917
|
+
/** Base path of the StorageController presign-proxy route. */
|
|
918
|
+
const STORAGE_ROUTE_BASE = "/storage";
|
|
919
|
+
/** {@link StorageDriver} over a Cloudflare R2 bucket. */
|
|
920
|
+
var R2StorageDriver = class {
|
|
921
|
+
config;
|
|
922
|
+
constructor(config) {
|
|
923
|
+
this.config = config;
|
|
924
|
+
}
|
|
925
|
+
async upload(body, path, options) {
|
|
926
|
+
await this.config.bucket.put(path, body, {
|
|
927
|
+
httpMetadata: options.mimeType ? { contentType: options.mimeType } : void 0,
|
|
928
|
+
customMetadata: options.metadata
|
|
929
|
+
});
|
|
930
|
+
return {
|
|
931
|
+
path,
|
|
932
|
+
disk: this.config.disk,
|
|
933
|
+
size: options.size,
|
|
934
|
+
mimeType: options.mimeType ?? "application/octet-stream",
|
|
935
|
+
uploadedAt: /* @__PURE__ */ new Date()
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
async download(path) {
|
|
939
|
+
const obj = await this.config.bucket.get(path);
|
|
940
|
+
if (!obj) throw new Error(`Storage object not found at "${path}".`);
|
|
941
|
+
return {
|
|
942
|
+
toStream: () => obj.body,
|
|
943
|
+
toArrayBuffer: () => obj.arrayBuffer(),
|
|
944
|
+
toText: () => obj.text(),
|
|
945
|
+
contentType: obj.httpMetadata?.contentType ?? "application/octet-stream",
|
|
946
|
+
size: obj.size,
|
|
947
|
+
metadata: obj.customMetadata
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
async delete(path) {
|
|
951
|
+
await this.config.bucket.delete(path);
|
|
952
|
+
}
|
|
953
|
+
async exists(path) {
|
|
954
|
+
return await this.config.bucket.head(path) !== null;
|
|
955
|
+
}
|
|
956
|
+
async getPresignedUrl(path, method, expiresIn) {
|
|
957
|
+
if (!this.config.secret) throw new Error("A signing secret is required for presigned URLs (set APP_SECRET).");
|
|
958
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) throw new Error(`Invalid presigned URL expiry: ${expiresIn}s (must be a positive number).`);
|
|
959
|
+
return {
|
|
960
|
+
url: await signUrl(`${`${STORAGE_ROUTE_BASE}/${this.config.disk}/${path}`}?method=${method}`, this.config.secret, { expiresIn }),
|
|
961
|
+
method,
|
|
962
|
+
expiresIn,
|
|
963
|
+
expiresAt: new Date(Date.now() + expiresIn * 1e3)
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region src/storage/storage.tokens.ts
|
|
969
|
+
const STORAGE_OPTIONS = new InjectionToken("STORAGE_OPTIONS");
|
|
970
|
+
//#endregion
|
|
971
|
+
//#region src/storage/storage-manager.service.ts
|
|
972
|
+
let StorageManagerService = class StorageManagerService {
|
|
973
|
+
options;
|
|
974
|
+
env;
|
|
975
|
+
constructor(options, env) {
|
|
976
|
+
this.options = options;
|
|
977
|
+
this.env = env;
|
|
978
|
+
}
|
|
979
|
+
hasDisk(disk) {
|
|
980
|
+
return this.options.disks.some((d) => d.disk === disk);
|
|
981
|
+
}
|
|
982
|
+
getDiskConfig(disk) {
|
|
983
|
+
const config = this.options.disks.find((d) => d.disk === disk);
|
|
984
|
+
if (!config) throw new Error(`Storage disk "${disk}" is not configured.`);
|
|
985
|
+
return config;
|
|
986
|
+
}
|
|
987
|
+
getDriver(disk) {
|
|
988
|
+
const config = this.getDiskConfig(disk);
|
|
989
|
+
const bucket = this.env.get(config.binding);
|
|
990
|
+
if (!bucket) throw new Error(`R2 binding "${config.binding}" for disk "${disk}" was not found in env.`);
|
|
991
|
+
return new R2StorageDriver({
|
|
992
|
+
disk,
|
|
993
|
+
bucket,
|
|
994
|
+
secret: this.env.get("APP_SECRET")
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
StorageManagerService = __decorate([
|
|
999
|
+
Injectable(),
|
|
1000
|
+
__decorateParam(0, Inject(STORAGE_OPTIONS)),
|
|
1001
|
+
__decorateParam(1, Inject(EnvService)),
|
|
1002
|
+
__decorateMetadata("design:paramtypes", [Object, typeof EnvService === "undefined" ? Object : EnvService])
|
|
1003
|
+
], StorageManagerService);
|
|
1004
|
+
//#endregion
|
|
1005
|
+
//#region src/storage/storage.controller.ts
|
|
1006
|
+
let StorageController = class StorageController {
|
|
1007
|
+
manager;
|
|
1008
|
+
env;
|
|
1009
|
+
constructor(manager, env) {
|
|
1010
|
+
this.manager = manager;
|
|
1011
|
+
this.env = env;
|
|
1012
|
+
}
|
|
1013
|
+
async download(c) {
|
|
1014
|
+
const secret = this.env.get("APP_SECRET");
|
|
1015
|
+
if (!secret) return new Response("Storage signing is not configured", { status: 500 });
|
|
1016
|
+
const disk = c.req.param("disk");
|
|
1017
|
+
if (!disk || !this.manager.hasDisk(disk)) return new Response("Unknown disk", { status: 404 });
|
|
1018
|
+
if (!await verifySignedUrl(c.req.url, secret)) return new Response("Invalid or expired URL", { status: 403 });
|
|
1019
|
+
const url = new URL(c.req.url);
|
|
1020
|
+
if ((url.searchParams.get("method") ?? "GET") !== "GET") return new Response("URL is not scoped for reads", { status: 403 });
|
|
1021
|
+
const { pathname } = url;
|
|
1022
|
+
const prefix = `/storage/${disk}/`;
|
|
1023
|
+
const idx = pathname.indexOf(prefix);
|
|
1024
|
+
const fullPath = idx >= 0 ? decodeURIComponent(pathname.slice(idx + prefix.length)) : "";
|
|
1025
|
+
try {
|
|
1026
|
+
const result = await this.manager.getDriver(disk).download(fullPath);
|
|
1027
|
+
return new Response(result.toStream(), { headers: { "content-type": result.contentType } });
|
|
1028
|
+
} catch {
|
|
1029
|
+
return new Response("Not found", { status: 404 });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
__decorate([
|
|
1034
|
+
Get("/:disk/*"),
|
|
1035
|
+
__decorateParam(0, Req()),
|
|
1036
|
+
__decorateMetadata("design:type", Function),
|
|
1037
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
1038
|
+
__decorateMetadata("design:returntype", Promise)
|
|
1039
|
+
], StorageController.prototype, "download", null);
|
|
1040
|
+
StorageController = __decorate([
|
|
1041
|
+
Controller("storage"),
|
|
1042
|
+
__decorateParam(0, Inject(StorageManagerService)),
|
|
1043
|
+
__decorateParam(1, Inject(EnvService)),
|
|
1044
|
+
__decorateMetadata("design:paramtypes", [typeof StorageManagerService === "undefined" ? Object : StorageManagerService, typeof EnvService === "undefined" ? Object : EnvService])
|
|
1045
|
+
], StorageController);
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region src/storage/storage.service.ts
|
|
1048
|
+
const DEFAULT_PRESIGN = {
|
|
1049
|
+
defaultExpiry: 3600,
|
|
1050
|
+
maxExpiry: 86400
|
|
1051
|
+
};
|
|
1052
|
+
let StorageService = class StorageService {
|
|
1053
|
+
options;
|
|
1054
|
+
manager;
|
|
1055
|
+
constructor(options, manager) {
|
|
1056
|
+
this.options = options;
|
|
1057
|
+
this.manager = manager;
|
|
1058
|
+
}
|
|
1059
|
+
put(relativePath, body, options = {}, disk) {
|
|
1060
|
+
const name = this.resolveDisk(disk);
|
|
1061
|
+
return this.manager.getDriver(name).upload(body, this.fullPath(relativePath, name), options);
|
|
1062
|
+
}
|
|
1063
|
+
get(relativePath, disk) {
|
|
1064
|
+
const name = this.resolveDisk(disk);
|
|
1065
|
+
return this.manager.getDriver(name).download(this.fullPath(relativePath, name));
|
|
1066
|
+
}
|
|
1067
|
+
delete(relativePath, disk) {
|
|
1068
|
+
const name = this.resolveDisk(disk);
|
|
1069
|
+
return this.manager.getDriver(name).delete(this.fullPath(relativePath, name));
|
|
1070
|
+
}
|
|
1071
|
+
exists(relativePath, disk) {
|
|
1072
|
+
const name = this.resolveDisk(disk);
|
|
1073
|
+
return this.manager.getDriver(name).exists(this.fullPath(relativePath, name));
|
|
1074
|
+
}
|
|
1075
|
+
url(relativePath, method = "GET", expiresIn, disk) {
|
|
1076
|
+
const name = this.resolveDisk(disk);
|
|
1077
|
+
return this.manager.getDriver(name).getPresignedUrl(this.fullPath(relativePath, name), method, this.validateExpiry(expiresIn));
|
|
1078
|
+
}
|
|
1079
|
+
resolveDisk(disk) {
|
|
1080
|
+
const name = disk ?? this.options.defaultDisk;
|
|
1081
|
+
if (!this.manager.hasDisk(name)) throw new Error(`Storage disk "${name}" is not configured.`);
|
|
1082
|
+
return name;
|
|
1083
|
+
}
|
|
1084
|
+
fullPath(relativePath, disk) {
|
|
1085
|
+
return joinStoragePath(this.manager.getDiskConfig(disk).root, relativePath);
|
|
1086
|
+
}
|
|
1087
|
+
validateExpiry(expiresIn) {
|
|
1088
|
+
const cfg = this.options.presignedUrl ?? DEFAULT_PRESIGN;
|
|
1089
|
+
const value = expiresIn ?? cfg.defaultExpiry;
|
|
1090
|
+
if (!Number.isFinite(value) || value < 1 || value > cfg.maxExpiry) throw new Error(`Presigned URL expiry ${value}s is out of range (1–${cfg.maxExpiry}s).`);
|
|
1091
|
+
return value;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
StorageService = __decorate([
|
|
1095
|
+
Injectable(),
|
|
1096
|
+
__decorateParam(0, Inject(STORAGE_OPTIONS)),
|
|
1097
|
+
__decorateParam(1, Inject(StorageManagerService)),
|
|
1098
|
+
__decorateMetadata("design:paramtypes", [Object, typeof StorageManagerService === "undefined" ? Object : StorageManagerService])
|
|
1099
|
+
], StorageService);
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region src/storage/storage.module.ts
|
|
1102
|
+
const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
|
|
1103
|
+
moduleName: "Storage",
|
|
1104
|
+
optionsInjectionToken: STORAGE_OPTIONS
|
|
1105
|
+
}).build();
|
|
1106
|
+
let StorageModule = class StorageModule extends ConfigurableModuleClass {};
|
|
1107
|
+
StorageModule = __decorate([Module({
|
|
1108
|
+
imports: [EnvModule.forRoot()],
|
|
1109
|
+
providers: [StorageManagerService, StorageService],
|
|
1110
|
+
controllers: [StorageController],
|
|
1111
|
+
exports: [
|
|
1112
|
+
StorageService,
|
|
1113
|
+
StorageManagerService,
|
|
1114
|
+
STORAGE_OPTIONS
|
|
1115
|
+
]
|
|
1116
|
+
})], StorageModule);
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region src/services/kv-cache.store.ts
|
|
1119
|
+
let KVCacheStore = class KVCacheStore {
|
|
1120
|
+
kv;
|
|
1121
|
+
constructor(kv) {
|
|
1122
|
+
this.kv = kv;
|
|
1123
|
+
}
|
|
1124
|
+
get ns() {
|
|
1125
|
+
return this.kv.namespace;
|
|
1126
|
+
}
|
|
1127
|
+
async get(key) {
|
|
1128
|
+
const value = await this.ns.get(key, "json");
|
|
1129
|
+
return value === null ? void 0 : value;
|
|
1130
|
+
}
|
|
1131
|
+
async set(key, value, ttl) {
|
|
1132
|
+
const options = ttl !== void 0 ? { expirationTtl: Math.max(60, Math.floor(ttl)) } : void 0;
|
|
1133
|
+
await this.ns.put(key, JSON.stringify(value), options);
|
|
1134
|
+
}
|
|
1135
|
+
async del(key) {
|
|
1136
|
+
await this.ns.delete(key);
|
|
1137
|
+
}
|
|
1138
|
+
async clear() {
|
|
1139
|
+
let cursor;
|
|
1140
|
+
do {
|
|
1141
|
+
const list = await this.ns.list(cursor ? { cursor } : void 0);
|
|
1142
|
+
await Promise.all(list.keys.map((entry) => this.ns.delete(entry.name)));
|
|
1143
|
+
cursor = list.list_complete ? void 0 : list.cursor;
|
|
1144
|
+
} while (cursor);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
KVCacheStore = __decorate([
|
|
1148
|
+
Injectable(),
|
|
1149
|
+
__decorateParam(0, Inject(KVService)),
|
|
1150
|
+
__decorateMetadata("design:paramtypes", [typeof KVService === "undefined" ? Object : KVService])
|
|
1151
|
+
], KVCacheStore);
|
|
1152
|
+
//#endregion
|
|
1153
|
+
//#region src/services/flagship-flag.driver.ts
|
|
1154
|
+
/**
|
|
1155
|
+
* {@link FeatureFlagDriver} backed by a Cloudflare Flagship binding.
|
|
1156
|
+
*
|
|
1157
|
+
* A thin, honest wrapper: each contract method maps 1:1 onto the binding's
|
|
1158
|
+
* corresponding value method, forwarding the caller's `fallback` (the binding's
|
|
1159
|
+
* `defaultValue`) and evaluation context. The binding resolves the fallback on
|
|
1160
|
+
* evaluation errors; anything the binding *rejects* with (e.g. a `remote: true`
|
|
1161
|
+
* dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
|
|
1162
|
+
* the never-throw guarantee.
|
|
1163
|
+
*
|
|
1164
|
+
* The binding only exists per request in a Worker, so pass a lazy accessor when
|
|
1165
|
+
* wiring from a bootstrap factory (mirroring how {@link EnvService} reads are
|
|
1166
|
+
* deferred); a resolved binding may be passed directly in tests.
|
|
1167
|
+
*
|
|
1168
|
+
* ```ts
|
|
1169
|
+
* FeatureFlagsModule.forRootAsync({
|
|
1170
|
+
* inject: [EnvService],
|
|
1171
|
+
* useFactory: (env: EnvService) => ({
|
|
1172
|
+
* drivers: [flagshipFlagDriver(() => env.get<FlagshipBinding>('FLAGS')!)],
|
|
1173
|
+
* }),
|
|
1174
|
+
* });
|
|
1175
|
+
* ```
|
|
1176
|
+
*
|
|
1177
|
+
* Evaluation ergonomics (the 1:1 binding-method mapping) are ported from the
|
|
1178
|
+
* Stratal feature-flags service (MIT, © Temitayo Fadojutimi), reshaped as a
|
|
1179
|
+
* bare driver.
|
|
1180
|
+
*/
|
|
1181
|
+
var FlagshipFlagDriver = class {
|
|
1182
|
+
name;
|
|
1183
|
+
resolve;
|
|
1184
|
+
constructor(binding, options = {}) {
|
|
1185
|
+
this.resolve = typeof binding === "function" ? binding : () => binding;
|
|
1186
|
+
this.name = options.name ?? "flagship";
|
|
1187
|
+
}
|
|
1188
|
+
getBoolean(key, fallback, ctx) {
|
|
1189
|
+
return this.resolve().getBooleanValue(key, fallback, ctx);
|
|
1190
|
+
}
|
|
1191
|
+
getString(key, fallback, ctx) {
|
|
1192
|
+
return this.resolve().getStringValue(key, fallback, ctx);
|
|
1193
|
+
}
|
|
1194
|
+
getNumber(key, fallback, ctx) {
|
|
1195
|
+
return this.resolve().getNumberValue(key, fallback, ctx);
|
|
1196
|
+
}
|
|
1197
|
+
getObject(key, fallback, ctx) {
|
|
1198
|
+
return this.resolve().getObjectValue(key, fallback, ctx);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
/** Convenience factory for {@link FlagshipFlagDriver}. */
|
|
1202
|
+
function flagshipFlagDriver(binding, options) {
|
|
1203
|
+
return new FlagshipFlagDriver(binding, options);
|
|
1204
|
+
}
|
|
1205
|
+
//#endregion
|
|
1206
|
+
//#region src/services/kv-flag.driver.ts
|
|
1207
|
+
let KvFlagDriver = class KvFlagDriver {
|
|
1208
|
+
kv;
|
|
1209
|
+
name;
|
|
1210
|
+
prefix;
|
|
1211
|
+
constructor(kv, options = {}) {
|
|
1212
|
+
this.kv = kv;
|
|
1213
|
+
this.name = options.name ?? "kv";
|
|
1214
|
+
this.prefix = options.prefix ?? "";
|
|
1215
|
+
}
|
|
1216
|
+
get ns() {
|
|
1217
|
+
return this.kv.namespace;
|
|
1218
|
+
}
|
|
1219
|
+
getBoolean(key, fallback, _ctx) {
|
|
1220
|
+
return this.read(key, fallback, (v) => typeof v === "boolean");
|
|
1221
|
+
}
|
|
1222
|
+
getString(key, fallback, _ctx) {
|
|
1223
|
+
return this.read(key, fallback, (v) => typeof v === "string");
|
|
1224
|
+
}
|
|
1225
|
+
getNumber(key, fallback, _ctx) {
|
|
1226
|
+
return this.read(key, fallback, (v) => typeof v === "number");
|
|
1227
|
+
}
|
|
1228
|
+
getObject(key, fallback, _ctx) {
|
|
1229
|
+
return this.read(key, fallback, (v) => typeof v === "object" && v !== null);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Reads and JSON-parses the (prefixed) key, returning the parsed value only
|
|
1233
|
+
* when `matches` accepts its type; otherwise the caller's fallback. A missing
|
|
1234
|
+
* key reads as `null` → fallback. Read/parse errors are left to propagate.
|
|
1235
|
+
*/
|
|
1236
|
+
async read(key, fallback, matches) {
|
|
1237
|
+
const value = await this.ns.get(this.prefix + key, "json");
|
|
1238
|
+
if (value === null || value === void 0) return fallback;
|
|
1239
|
+
return matches(value) ? value : fallback;
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
KvFlagDriver = __decorate([
|
|
1243
|
+
Injectable(),
|
|
1244
|
+
__decorateParam(0, Inject(KVService)),
|
|
1245
|
+
__decorateMetadata("design:paramtypes", [typeof KVService === "undefined" ? Object : KVService, Object])
|
|
1246
|
+
], KvFlagDriver);
|
|
1247
|
+
/** Convenience factory for {@link KvFlagDriver}. */
|
|
1248
|
+
function kvFlagDriver(kv, options) {
|
|
1249
|
+
return new KvFlagDriver(kv, options);
|
|
1250
|
+
}
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/decorators/env.ts
|
|
1253
|
+
/**
|
|
1254
|
+
* Parameter decorator to inject Cloudflare environment bindings.
|
|
1255
|
+
*
|
|
1256
|
+
* Without arguments, returns the entire `env` object.
|
|
1257
|
+
* With a binding name, returns that specific binding.
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* ```ts
|
|
1261
|
+
* @Get()
|
|
1262
|
+
* handle(@Env() env: CloudflareEnv) { ... }
|
|
1263
|
+
*
|
|
1264
|
+
* @Get()
|
|
1265
|
+
* handle(@Env('MY_KV') kv: KVNamespace) { ... }
|
|
1266
|
+
* ```
|
|
1267
|
+
*/
|
|
1268
|
+
const Env = createParamDecorator((bindingName, ctx) => {
|
|
1269
|
+
const env = ctx.getContext().env;
|
|
1270
|
+
if (!env) return void 0;
|
|
1271
|
+
return bindingName ? env[bindingName] : env;
|
|
1272
|
+
});
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/decorators/scheduled.ts
|
|
1275
|
+
const SCHEDULED_METADATA_KEY = "cloudflare:scheduled";
|
|
1276
|
+
registerEntrypointKind({
|
|
1277
|
+
kind: "cf:scheduled",
|
|
1278
|
+
metaKey: SCHEDULED_METADATA_KEY,
|
|
1279
|
+
level: "method"
|
|
1280
|
+
});
|
|
1281
|
+
/**
|
|
1282
|
+
* Marks a method as a scheduled (cron) handler.
|
|
1283
|
+
*
|
|
1284
|
+
* @example
|
|
1285
|
+
* ```ts
|
|
1286
|
+
* @Injectable()
|
|
1287
|
+
* class WorkerService {
|
|
1288
|
+
* @Scheduled('0 * * * *')
|
|
1289
|
+
* async hourlyCron() {
|
|
1290
|
+
* console.log('Running hourly');
|
|
1291
|
+
* }
|
|
1292
|
+
* }
|
|
1293
|
+
* ```
|
|
1294
|
+
*/
|
|
1295
|
+
function Scheduled(cron) {
|
|
1296
|
+
return (target, propertyKey, _descriptor) => {
|
|
1297
|
+
const existing = getMetadata(SCHEDULED_METADATA_KEY, target.constructor) ?? [];
|
|
1298
|
+
existing.push({
|
|
1299
|
+
cron,
|
|
1300
|
+
methodName: String(propertyKey)
|
|
1301
|
+
});
|
|
1302
|
+
defineMetadata(SCHEDULED_METADATA_KEY, existing, target.constructor);
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
//#endregion
|
|
1306
|
+
//#region src/decorators/queue-consumer.ts
|
|
1307
|
+
const QUEUE_CONSUMER_METADATA_KEY = "cloudflare:queue-consumer";
|
|
1308
|
+
registerEntrypointKind({
|
|
1309
|
+
kind: "cf:queue",
|
|
1310
|
+
metaKey: QUEUE_CONSUMER_METADATA_KEY,
|
|
1311
|
+
level: "method"
|
|
1312
|
+
});
|
|
1313
|
+
/**
|
|
1314
|
+
* Marks a method as a queue consumer handler.
|
|
1315
|
+
*
|
|
1316
|
+
* @example
|
|
1317
|
+
* ```ts
|
|
1318
|
+
* @Injectable()
|
|
1319
|
+
* class WorkerService {
|
|
1320
|
+
* @QueueConsumer('email-queue')
|
|
1321
|
+
* async processEmails(batch: MessageBatch) {
|
|
1322
|
+
* for (const msg of batch.messages) {
|
|
1323
|
+
* console.log('Processing:', msg.body);
|
|
1324
|
+
* msg.ack();
|
|
1325
|
+
* }
|
|
1326
|
+
* }
|
|
1327
|
+
* }
|
|
1328
|
+
* ```
|
|
1329
|
+
*/
|
|
1330
|
+
function QueueConsumer(queueName) {
|
|
1331
|
+
return (target, propertyKey, _descriptor) => {
|
|
1332
|
+
const existing = getMetadata(QUEUE_CONSUMER_METADATA_KEY, target.constructor) ?? [];
|
|
1333
|
+
existing.push({
|
|
1334
|
+
queueName,
|
|
1335
|
+
methodName: String(propertyKey)
|
|
1336
|
+
});
|
|
1337
|
+
defineMetadata(QUEUE_CONSUMER_METADATA_KEY, existing, target.constructor);
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/websocket/cf-room-registry.ts
|
|
1342
|
+
/**
|
|
1343
|
+
* Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room
|
|
1344
|
+
* membership lives in tags (hub room, set at accept) + the attachment (dynamic
|
|
1345
|
+
* `join()`), never in DO instance fields — so it survives hibernation with zero
|
|
1346
|
+
* rehydration.
|
|
1347
|
+
*/
|
|
1348
|
+
var CfRoomRegistry = class {
|
|
1349
|
+
ctx;
|
|
1350
|
+
constructor(ctx) {
|
|
1351
|
+
this.ctx = ctx;
|
|
1352
|
+
}
|
|
1353
|
+
register(_client) {}
|
|
1354
|
+
leaveAll(_client) {}
|
|
1355
|
+
join(client, room) {
|
|
1356
|
+
return client.join(room);
|
|
1357
|
+
}
|
|
1358
|
+
leave(client, room) {
|
|
1359
|
+
return client.leave(room);
|
|
1360
|
+
}
|
|
1361
|
+
localIdsInRoom(room) {
|
|
1362
|
+
return this.socketsForRoom(room).map((ws) => this.attachmentOf(ws).connId);
|
|
1363
|
+
}
|
|
1364
|
+
deliverLocal(cmd) {
|
|
1365
|
+
const excludeIds = new Set(cmd.exceptIds ?? []);
|
|
1366
|
+
const excludeRooms = cmd.exceptRooms ?? [];
|
|
1367
|
+
const targets = cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);
|
|
1368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1369
|
+
for (const ws of targets) {
|
|
1370
|
+
const att = this.attachmentOf(ws);
|
|
1371
|
+
if (seen.has(att.connId)) continue;
|
|
1372
|
+
seen.add(att.connId);
|
|
1373
|
+
if (excludeIds.has(att.connId)) continue;
|
|
1374
|
+
if (excludeRooms.some((r) => att.rooms.includes(r))) continue;
|
|
1375
|
+
ws.send(cmd.frame);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
collectRooms(rooms) {
|
|
1379
|
+
const set = /* @__PURE__ */ new Set();
|
|
1380
|
+
for (const room of rooms) for (const ws of this.socketsForRoom(room)) set.add(ws);
|
|
1381
|
+
return [...set];
|
|
1382
|
+
}
|
|
1383
|
+
socketsForRoom(room) {
|
|
1384
|
+
return this.ctx.getWebSockets().filter((ws) => this.attachmentOf(ws).rooms.includes(room));
|
|
1385
|
+
}
|
|
1386
|
+
attachmentOf(ws) {
|
|
1387
|
+
return ws.deserializeAttachment() ?? {
|
|
1388
|
+
connId: "",
|
|
1389
|
+
path: "",
|
|
1390
|
+
rooms: [],
|
|
1391
|
+
data: {}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
/** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */
|
|
1395
|
+
clientFor(ws) {
|
|
1396
|
+
return new CfWsClient(this.ctx, ws);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
//#endregion
|
|
1400
|
+
//#region src/websocket/ws-server-holder.ts
|
|
1401
|
+
let WsServerHolder = class WsServerHolder {
|
|
1402
|
+
target;
|
|
1403
|
+
setTarget(server) {
|
|
1404
|
+
this.target = server;
|
|
1405
|
+
}
|
|
1406
|
+
get resolved() {
|
|
1407
|
+
if (!this.target) throw new Error("WebSocket server is only available inside a WebSocket Durable Object. To push from a Worker HTTP handler, use broadcastToRoom(namespace, room, ...).");
|
|
1408
|
+
return this.target;
|
|
1409
|
+
}
|
|
1410
|
+
emit(event, data) {
|
|
1411
|
+
return this.resolved.emit(event, data);
|
|
1412
|
+
}
|
|
1413
|
+
to(room) {
|
|
1414
|
+
return this.resolved.to(room);
|
|
1415
|
+
}
|
|
1416
|
+
in(room) {
|
|
1417
|
+
return this.resolved.in(room);
|
|
1418
|
+
}
|
|
1419
|
+
except(room) {
|
|
1420
|
+
return this.resolved.except(room);
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
WsServerHolder = __decorate([Injectable()], WsServerHolder);
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/websocket/do-bootstrap.ts
|
|
1426
|
+
/**
|
|
1427
|
+
* Slim DI bootstrap for the Durable Object isolate: wires the container and runs
|
|
1428
|
+
* `OnModuleInit`/`OnApplicationBootstrap` (so `WsDispatcher` discovers gateways)
|
|
1429
|
+
* WITHOUT building the Hono app/routes the DO never serves. Cloudflare binding
|
|
1430
|
+
* refs are initialized straight from the DO's `env`, and the ctx-backed server
|
|
1431
|
+
* is bound before bootstrap lifecycle so gateway `afterInit`/handlers see it.
|
|
1432
|
+
*/
|
|
1433
|
+
async function buildDoRuntime(rootModule, ctx, env) {
|
|
1434
|
+
const { container, routeManager, loader } = await bootstrap(rootModule);
|
|
1435
|
+
for (const value of container.getUseValues()) if (value instanceof EnvRef) value._initialize(env);
|
|
1436
|
+
else if (value instanceof BindingRef) value._initialize(env[value.bindingName]);
|
|
1437
|
+
const registry = new CfRoomRegistry(ctx);
|
|
1438
|
+
const driver = local();
|
|
1439
|
+
driver.bind(registry);
|
|
1440
|
+
const server = new WsServerImpl(driver);
|
|
1441
|
+
try {
|
|
1442
|
+
const holder = container.resolve(WS_SERVER);
|
|
1443
|
+
if (holder instanceof WsServerHolder) holder.setTarget(server);
|
|
1444
|
+
} catch {}
|
|
1445
|
+
const app = new VelaApplication(container, routeManager);
|
|
1446
|
+
app.setInstances(await loader.resolveAllInstances());
|
|
1447
|
+
await app.callOnModuleInit();
|
|
1448
|
+
await app.callOnApplicationBootstrap();
|
|
1449
|
+
const wsEntrypoints = app.entrypoints.ofKind("websocket");
|
|
1450
|
+
const live = initDoLive(app, container, ctx);
|
|
1451
|
+
return {
|
|
1452
|
+
dispatcher: wsEntrypoints[0]?.meta.dispatcher ?? app.get(WsDispatcher),
|
|
1453
|
+
registry,
|
|
1454
|
+
server,
|
|
1455
|
+
gatewayPaths: wsEntrypoints.map((ep) => ep.meta.path),
|
|
1456
|
+
live,
|
|
1457
|
+
close: (signal) => app.close(signal)
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/websocket/do-websocket-host.ts
|
|
1462
|
+
/**
|
|
1463
|
+
* The socket lifecycle inside a WebSocket Durable Object, decoupled from the
|
|
1464
|
+
* `cloudflare:workers` base class so it is unit-testable with fakes. The thin
|
|
1465
|
+
* `VelaWebSocketDurableObject` shell forwards its hibernation callbacks here.
|
|
1466
|
+
*/
|
|
1467
|
+
var DoWebSocketHost = class {
|
|
1468
|
+
ctx;
|
|
1469
|
+
dispatcher;
|
|
1470
|
+
registry;
|
|
1471
|
+
gatewayPaths;
|
|
1472
|
+
constructor(ctx, dispatcher, registry, gatewayPaths = []) {
|
|
1473
|
+
this.ctx = ctx;
|
|
1474
|
+
this.dispatcher = dispatcher;
|
|
1475
|
+
this.registry = registry;
|
|
1476
|
+
this.gatewayPaths = gatewayPaths;
|
|
1477
|
+
}
|
|
1478
|
+
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */
|
|
1479
|
+
defaultPath() {
|
|
1480
|
+
return this.gatewayPaths[0];
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Accept a hibernatable socket: tag it with its hub room + connection id,
|
|
1484
|
+
* persist the attachment, then fire `OnGatewayConnection` WITHOUT blocking the
|
|
1485
|
+
* 101 response (the caller returns it immediately).
|
|
1486
|
+
*/
|
|
1487
|
+
accept(ws, path, roomId, userId) {
|
|
1488
|
+
const connId = crypto.randomUUID();
|
|
1489
|
+
this.ctx.acceptWebSocket(ws, [roomTag(roomId), connTag(connId)]);
|
|
1490
|
+
const attachment = {
|
|
1491
|
+
connId,
|
|
1492
|
+
userId,
|
|
1493
|
+
path,
|
|
1494
|
+
rooms: [roomId],
|
|
1495
|
+
data: userId ? { userId } : {}
|
|
1496
|
+
};
|
|
1497
|
+
ws.serializeAttachment(attachment);
|
|
1498
|
+
Promise.resolve(this.dispatcher.handleOpen(path, new CfWsClient(this.ctx, ws))).catch((err) => console.warn("[vela] websocket handleConnection failed:", err));
|
|
1499
|
+
}
|
|
1500
|
+
async onMessage(ws, message) {
|
|
1501
|
+
const client = new CfWsClient(this.ctx, ws);
|
|
1502
|
+
await this.dispatcher.dispatchMessage(client.path, client, message);
|
|
1503
|
+
}
|
|
1504
|
+
async onClose(ws, code, reason) {
|
|
1505
|
+
const client = new CfWsClient(this.ctx, ws);
|
|
1506
|
+
await this.dispatcher.handleClose(client.path, client, code, reason);
|
|
1507
|
+
try {
|
|
1508
|
+
if (code === 1e3 || code >= 3e3 && code <= 4999) ws.close(code, reason);
|
|
1509
|
+
else ws.close();
|
|
1510
|
+
} catch {}
|
|
1511
|
+
}
|
|
1512
|
+
async onError(ws, err) {
|
|
1513
|
+
const client = new CfWsClient(this.ctx, ws);
|
|
1514
|
+
await this.dispatcher.handleError(client.path, client, err);
|
|
1515
|
+
}
|
|
1516
|
+
/** Deliver a broadcast command to this DO's local sockets (RPC entry point). */
|
|
1517
|
+
broadcast(cmd) {
|
|
1518
|
+
this.registry.deliverLocal(cmd);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
//#endregion
|
|
1522
|
+
//#region src/websocket/websocket.durable-object.ts
|
|
1523
|
+
const PING = "{\"event\":\"ping\"}";
|
|
1524
|
+
const PONG = "{\"event\":\"pong\"}";
|
|
1525
|
+
/**
|
|
1526
|
+
* Base class for the WebSocket Durable Object. The user exports a named subclass
|
|
1527
|
+
* (matching their `wrangler.toml` `class_name`) built from their `AppModule`:
|
|
1528
|
+
*
|
|
1529
|
+
* ```ts
|
|
1530
|
+
* export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
|
|
1531
|
+
* ```
|
|
1532
|
+
*
|
|
1533
|
+
* It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
|
|
1534
|
+
* bridge DO hibernation) and forwards every event into the runtime-agnostic
|
|
1535
|
+
* `WsDispatcher` via {@link DoWebSocketHost}.
|
|
1536
|
+
*/
|
|
1537
|
+
function VelaWebSocketDurableObject(rootModule) {
|
|
1538
|
+
return class VelaWsDurableObject extends DurableObject {
|
|
1539
|
+
host;
|
|
1540
|
+
liveEngine;
|
|
1541
|
+
ready;
|
|
1542
|
+
constructor(ctx, env) {
|
|
1543
|
+
super(ctx, env);
|
|
1544
|
+
try {
|
|
1545
|
+
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));
|
|
1546
|
+
} catch {}
|
|
1547
|
+
this.ready = ctx.blockConcurrencyWhile(async () => {
|
|
1548
|
+
const runtime = await buildDoRuntime(rootModule, ctx, env);
|
|
1549
|
+
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
|
|
1550
|
+
this.liveEngine = runtime.live;
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
async fetch(request) {
|
|
1554
|
+
await this.ready;
|
|
1555
|
+
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
1556
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
1557
|
+
const url = new URL(request.url);
|
|
1558
|
+
const roomId = request.headers.get("x-vela-room") ?? this.ctx.id.name ?? url.pathname;
|
|
1559
|
+
const path = request.headers.get("x-vela-path") ?? this.host.defaultPath() ?? url.pathname;
|
|
1560
|
+
const userId = request.headers.get("x-vela-user") || void 0;
|
|
1561
|
+
this.host.accept(server, path, roomId, userId);
|
|
1562
|
+
return new Response(null, {
|
|
1563
|
+
status: 101,
|
|
1564
|
+
webSocket: client
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
async webSocketMessage(ws, message) {
|
|
1568
|
+
await this.ready;
|
|
1569
|
+
await this.host.onMessage(ws, message);
|
|
1570
|
+
}
|
|
1571
|
+
async webSocketClose(ws, code, reason) {
|
|
1572
|
+
await this.ready;
|
|
1573
|
+
await this.host.onClose(ws, code, reason);
|
|
1574
|
+
}
|
|
1575
|
+
async webSocketError(ws, error) {
|
|
1576
|
+
await this.ready;
|
|
1577
|
+
await this.host.onError(ws, error);
|
|
1578
|
+
}
|
|
1579
|
+
/** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */
|
|
1580
|
+
async broadcast(cmd) {
|
|
1581
|
+
await this.ready;
|
|
1582
|
+
this.host.broadcast(cmd);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /
|
|
1586
|
+
* liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns
|
|
1587
|
+
* the commit stamp (what `Vela-Commit-Cursor` carries); the subscription
|
|
1588
|
+
* refreshes fan out asynchronously.
|
|
1589
|
+
*/
|
|
1590
|
+
async invalidate(cmd) {
|
|
1591
|
+
await this.ready;
|
|
1592
|
+
return this.liveEngine?.applyInvalidation(cmd);
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
//#endregion
|
|
1597
|
+
//#region src/websocket/cloudflare-websocket.module.ts
|
|
1598
|
+
/**
|
|
1599
|
+
* Cloudflare counterpart to the core `WebSocketModule.forRoot()`. Import this in
|
|
1600
|
+
* your `AppModule` instead: it provides the gateway dispatcher plus a late-bound
|
|
1601
|
+
* `WS_SERVER` (`WsServerHolder`) that the WebSocket Durable Object wires to a
|
|
1602
|
+
* ctx-backed server per instance. `useClass` ensures a fresh holder per DI
|
|
1603
|
+
* container so colocated DO instances never share a server.
|
|
1604
|
+
*/
|
|
1605
|
+
var CloudflareWebSocketModule = class CloudflareWebSocketModule {
|
|
1606
|
+
static forRoot() {
|
|
1607
|
+
return {
|
|
1608
|
+
module: CloudflareWebSocketModule,
|
|
1609
|
+
providers: [{
|
|
1610
|
+
provide: WS_SERVER,
|
|
1611
|
+
useClass: WsServerHolder
|
|
1612
|
+
}, WsDispatcher],
|
|
1613
|
+
exports: [WS_SERVER, WsDispatcher]
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/websocket/broadcast.ts
|
|
1619
|
+
/**
|
|
1620
|
+
* Push to a room from a Worker HTTP handler / cron / queue consumer (server-
|
|
1621
|
+
* initiated emit). Resolves the room's Durable Object and calls its `broadcast`
|
|
1622
|
+
* RPC method — the same canonical room→DO mapping the upgrade route uses, so it
|
|
1623
|
+
* always reaches the DO holding those sockets.
|
|
1624
|
+
*
|
|
1625
|
+
* @example
|
|
1626
|
+
* ```ts
|
|
1627
|
+
* // In a controller — ns from DurableObjectService.namespace
|
|
1628
|
+
* await broadcastToRoom(ns, `org:${id}`, 'order.created', order);
|
|
1629
|
+
* ```
|
|
1630
|
+
*/
|
|
1631
|
+
async function broadcastToRoom(ns, room, event, data, options) {
|
|
1632
|
+
const cmd = {
|
|
1633
|
+
rooms: [room],
|
|
1634
|
+
exceptIds: options?.exceptIds,
|
|
1635
|
+
frame: JSON.stringify({
|
|
1636
|
+
event,
|
|
1637
|
+
data
|
|
1638
|
+
})
|
|
1639
|
+
};
|
|
1640
|
+
await ns.get(roomToDurableId(ns, room)).broadcast(cmd);
|
|
1641
|
+
}
|
|
1642
|
+
//#endregion
|
|
1643
|
+
export { AIModule, AIService, CloudflareApplication, CloudflareWebSocketModule, ConnectedSocket, D1Module, D1Service, DoCursorLog, DurableObjectModule, DurableObjectService, Env, EnvModule, EnvService, FlagshipFlagDriver, HyperdriveModule, HyperdriveService, KVCacheStore, KVModule, KVService, KvFlagDriver, MessageBody, QueueConsumer, QueueModule, QueueService, R2Module, R2Service, R2StorageDriver, STORAGE_OPTIONS, Scheduled, StorageController, StorageManagerService, StorageModule, StorageService, SubscribeMessage, VectorizeModule, VectorizeService, VelaWebSocketDurableObject, WebSocketGateway, WebSocketServer, WsException, broadcastToRoom, cloudflareAdapter, createCloudflareApp, durableObjectCursorLog, durableObjectLive, flagshipFlagDriver, kvFlagDriver, liveInvalidateToRoom };
|
|
1644
|
+
|
|
1645
|
+
//# sourceMappingURL=index.js.map
|