@velajs/cloudflare 1.22.1 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +39 -22
- package/dist/durable-objects.d.ts +1 -1
- package/dist/durable-objects.js +40 -21
- package/dist/durable-objects.js.map +1 -1
- package/dist/index.d.ts +22 -9
- package/dist/index.js +107 -59
- package/dist/index.js.map +1 -1
- package/dist/{nonce-validation-LE0vFk-7.js → nonce-validation-Bcqf3FvY.js} +106 -38
- package/dist/nonce-validation-Bcqf3FvY.js.map +1 -0
- package/dist/queue.d.ts +26 -0
- package/dist/queue.js +41 -0
- package/dist/queue.js.map +1 -0
- package/package.json +10 -6
- package/dist/nonce-validation-LE0vFk-7.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Injectable, defineProvider } from "@velajs/vela";
|
|
2
|
-
import { DEFAULT_WS_MAX_FRAME_BYTES, DEFAULT_WS_MAX_JOINED_ROOMS, assertWebSocketRoomId, resolveMaxFrameBytes
|
|
2
|
+
import { DEFAULT_WS_MAX_FRAME_BYTES, DEFAULT_WS_MAX_JOINED_ROOMS, WebSocketSendGate, assertWebSocketRoomId, resolveMaxFrameBytes } from "@velajs/vela/websocket";
|
|
3
3
|
import { LIVE_CURSOR_LOG, LIVE_DRIVER, LiveEngine, readPersistedLiveSubscriptions } from "@velajs/vela/live";
|
|
4
4
|
//#region src/websocket/room-id.ts
|
|
5
5
|
/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */
|
|
@@ -162,19 +162,87 @@ let WsServerHolder = class WsServerHolder {
|
|
|
162
162
|
};
|
|
163
163
|
WsServerHolder = __decorate([Injectable()], WsServerHolder);
|
|
164
164
|
//#endregion
|
|
165
|
+
//#region src/websocket/ws-attachment.ts
|
|
166
|
+
const record = (value) => {
|
|
167
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
168
|
+
const prototype = Object.getPrototypeOf(value);
|
|
169
|
+
return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).every((key) => {
|
|
170
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
171
|
+
return property !== void 0 && "value" in property;
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
const positive = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
175
|
+
const text = (value) => typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).byteLength <= 2048;
|
|
176
|
+
/** Versionless 1.x records remain readable; malformed/future versions fail closed. */
|
|
177
|
+
function readWsAttachment(value) {
|
|
178
|
+
if (!record(value) || value.version !== void 0 && value.version !== 1) return void 0;
|
|
179
|
+
if (!text(value.connId) || typeof value.path !== "string" || value.path.length > 8192 || value.state !== "pending" && value.state !== "active" && value.state !== "rejected" || !Array.isArray(value.rooms) || value.rooms.length > DEFAULT_WS_MAX_JOINED_ROOMS || !record(value.data)) return void 0;
|
|
180
|
+
const rooms = [];
|
|
181
|
+
for (let index = 0; index < value.rooms.length; index++) {
|
|
182
|
+
const descriptor = Object.getOwnPropertyDescriptor(value.rooms, index);
|
|
183
|
+
if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string") return void 0;
|
|
184
|
+
const room = descriptor.value;
|
|
185
|
+
try {
|
|
186
|
+
assertWebSocketRoomId(room);
|
|
187
|
+
} catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (rooms.includes(room)) return void 0;
|
|
191
|
+
rooms.push(room);
|
|
192
|
+
}
|
|
193
|
+
if (value.maxFrameBytes !== void 0 && !positive(value.maxFrameBytes)) return void 0;
|
|
194
|
+
if (value.expiresAtMs !== void 0 && !positive(value.expiresAtMs)) return void 0;
|
|
195
|
+
if (value.userId !== void 0 && !text(value.userId)) return void 0;
|
|
196
|
+
if (value.tenantId !== void 0 && !text(value.tenantId)) return void 0;
|
|
197
|
+
let principal;
|
|
198
|
+
if (value.principal !== void 0) {
|
|
199
|
+
const p = value.principal;
|
|
200
|
+
if (!record(p) || !text(p.issuer) || !text(p.subject) || p.principalType !== "user" && p.principalType !== "service") return void 0;
|
|
201
|
+
principal = {
|
|
202
|
+
issuer: p.issuer,
|
|
203
|
+
subject: p.subject,
|
|
204
|
+
principalType: p.principalType
|
|
205
|
+
};
|
|
206
|
+
if (value.userId !== void 0 && value.userId !== p.subject) return void 0;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
version: 1,
|
|
210
|
+
connId: value.connId,
|
|
211
|
+
state: value.state,
|
|
212
|
+
path: value.path,
|
|
213
|
+
rooms,
|
|
214
|
+
data: { ...value.data },
|
|
215
|
+
...principal ? { principal } : {},
|
|
216
|
+
...typeof value.userId === "string" ? { userId: value.userId } : {},
|
|
217
|
+
...typeof value.tenantId === "string" ? { tenantId: value.tenantId } : {},
|
|
218
|
+
...typeof value.expiresAtMs === "number" ? { expiresAtMs: value.expiresAtMs } : {},
|
|
219
|
+
...typeof value.maxFrameBytes === "number" ? { maxFrameBytes: value.maxFrameBytes } : {}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function socketAttachment(ws) {
|
|
223
|
+
try {
|
|
224
|
+
return readWsAttachment(ws.deserializeAttachment());
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function rejectedAttachment() {
|
|
230
|
+
return {
|
|
231
|
+
version: 1,
|
|
232
|
+
connId: "",
|
|
233
|
+
state: "rejected",
|
|
234
|
+
path: "",
|
|
235
|
+
rooms: [],
|
|
236
|
+
data: {}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
165
240
|
//#region src/websocket/do-state.ts
|
|
166
241
|
/** Cloudflare's serialized WebSocket hibernation attachment ceiling. */
|
|
167
242
|
const MAX_WS_ATTACHMENT_BYTES = 16384;
|
|
168
243
|
//#endregion
|
|
169
244
|
//#region src/websocket/cf-ws-client.ts
|
|
170
245
|
const encoder$1 = new TextEncoder();
|
|
171
|
-
const EMPTY = {
|
|
172
|
-
connId: "",
|
|
173
|
-
state: "rejected",
|
|
174
|
-
path: "",
|
|
175
|
-
rooms: [],
|
|
176
|
-
data: {}
|
|
177
|
-
};
|
|
178
246
|
/**
|
|
179
247
|
* Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.
|
|
180
248
|
* Per-connection state lives in the hibernation attachment (survives eviction),
|
|
@@ -183,37 +251,35 @@ const EMPTY = {
|
|
|
183
251
|
var CfWsClient = class {
|
|
184
252
|
ctx;
|
|
185
253
|
ws;
|
|
186
|
-
|
|
187
|
-
|
|
254
|
+
sendGate;
|
|
255
|
+
#attachment;
|
|
256
|
+
constructor(ctx, ws, sendGate = new WebSocketSendGate()) {
|
|
188
257
|
this.ctx = ctx;
|
|
189
258
|
this.ws = ws;
|
|
190
|
-
|
|
191
|
-
this
|
|
192
|
-
...EMPTY,
|
|
193
|
-
data: {}
|
|
194
|
-
};
|
|
259
|
+
this.sendGate = sendGate;
|
|
260
|
+
this.#attachment = socketAttachment(ws) ?? rejectedAttachment();
|
|
195
261
|
}
|
|
196
262
|
get id() {
|
|
197
|
-
return this
|
|
263
|
+
return this.#attachment.connId;
|
|
198
264
|
}
|
|
199
265
|
/** The gateway route path this socket connected on (used to route messages). */
|
|
200
266
|
get path() {
|
|
201
|
-
return this
|
|
267
|
+
return this.#attachment.path;
|
|
202
268
|
}
|
|
203
269
|
get rooms() {
|
|
204
|
-
return new Set(this
|
|
270
|
+
return new Set(this.#attachment.rooms);
|
|
205
271
|
}
|
|
206
272
|
get data() {
|
|
207
|
-
return this
|
|
273
|
+
return this.#attachment.data;
|
|
208
274
|
}
|
|
209
275
|
set data(value) {
|
|
210
|
-
this
|
|
276
|
+
this.#attachment.data = value;
|
|
211
277
|
}
|
|
212
278
|
get raw() {
|
|
213
279
|
return this.ws;
|
|
214
280
|
}
|
|
215
281
|
get maxFrameBytes() {
|
|
216
|
-
const value = this
|
|
282
|
+
const value = this.#attachment.maxFrameBytes;
|
|
217
283
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_WS_MAX_FRAME_BYTES;
|
|
218
284
|
}
|
|
219
285
|
send(event, data, id) {
|
|
@@ -227,24 +293,24 @@ var CfWsClient = class {
|
|
|
227
293
|
}));
|
|
228
294
|
}
|
|
229
295
|
sendRaw(payload) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
this.
|
|
296
|
+
this.trySendRaw(payload);
|
|
297
|
+
}
|
|
298
|
+
trySendRaw(payload) {
|
|
299
|
+
if (this.#attachment.state === "rejected") return "closed";
|
|
300
|
+
return this.sendGate.trySend(this.ws, payload, this.maxFrameBytes);
|
|
235
301
|
}
|
|
236
302
|
join(room) {
|
|
237
303
|
assertWebSocketRoomId(room);
|
|
238
|
-
if (!this
|
|
239
|
-
if (this
|
|
240
|
-
this
|
|
304
|
+
if (!this.#attachment.rooms.includes(room)) {
|
|
305
|
+
if (this.#attachment.rooms.length >= DEFAULT_WS_MAX_JOINED_ROOMS) throw new Error(`A WebSocket may join at most ${DEFAULT_WS_MAX_JOINED_ROOMS} rooms`);
|
|
306
|
+
this.#attachment.rooms.push(room);
|
|
241
307
|
this.persist();
|
|
242
308
|
}
|
|
243
309
|
}
|
|
244
310
|
leave(room) {
|
|
245
|
-
const next = this
|
|
246
|
-
if (next.length !== this
|
|
247
|
-
this
|
|
311
|
+
const next = this.#attachment.rooms.filter((r) => r !== room);
|
|
312
|
+
if (next.length !== this.#attachment.rooms.length) {
|
|
313
|
+
this.#attachment.rooms = next;
|
|
248
314
|
this.persist();
|
|
249
315
|
}
|
|
250
316
|
}
|
|
@@ -256,9 +322,10 @@ var CfWsClient = class {
|
|
|
256
322
|
this.ws.close(code, reason);
|
|
257
323
|
}
|
|
258
324
|
persist() {
|
|
259
|
-
|
|
325
|
+
if (this.#attachment.state === "rejected") throw new Error("Invalid WebSocket attachment");
|
|
326
|
+
const serialized = JSON.stringify(this.#attachment);
|
|
260
327
|
if (encoder$1.encode(serialized).length > 16384) throw new Error("WebSocket attachment exceeds the 16 KiB Cloudflare limit. Store large per-connection state in Durable Object storage keyed by connId instead.");
|
|
261
|
-
this.ws.serializeAttachment(this
|
|
328
|
+
this.ws.serializeAttachment(this.#attachment);
|
|
262
329
|
}
|
|
263
330
|
};
|
|
264
331
|
//#endregion
|
|
@@ -398,14 +465,15 @@ function initializeDoLiveResources(container, ctx) {
|
|
|
398
465
|
* subscribers. Returns the engine for the `invalidate` RPC, or undefined when
|
|
399
466
|
* the app doesn't use LiveModule.
|
|
400
467
|
*/
|
|
401
|
-
function initDoLive(app, ctx) {
|
|
468
|
+
function initDoLive(app, ctx, registry) {
|
|
402
469
|
const entry = app.entrypoints.ofKind("live")[0];
|
|
403
470
|
if (!entry) return void 0;
|
|
404
471
|
if (typeof entry.meta !== "object" || entry.meta === null || !("engine" in entry.meta)) throw new Error("Invalid live entrypoint metadata.");
|
|
405
472
|
const engine = entry.meta.engine;
|
|
406
473
|
if (!(engine instanceof LiveEngine)) throw new Error("Invalid live entrypoint engine.");
|
|
407
474
|
for (const ws of ctx.getWebSockets()) {
|
|
408
|
-
const client = new CfWsClient(ctx, ws);
|
|
475
|
+
const client = registry?.clientFor(ws) ?? new CfWsClient(ctx, ws);
|
|
476
|
+
if (!client.id) continue;
|
|
409
477
|
for (const record of readPersistedLiveSubscriptions(client)) engine.restoreSubscription(client.path, client, record);
|
|
410
478
|
}
|
|
411
479
|
return engine;
|
|
@@ -440,6 +508,6 @@ function isValidExpiry(expEpochSeconds, nowEpochSeconds) {
|
|
|
440
508
|
return typeof expEpochSeconds === "number" && Number.isSafeInteger(expEpochSeconds) && expEpochSeconds > 0 && expEpochSeconds >= nowEpochSeconds;
|
|
441
509
|
}
|
|
442
510
|
//#endregion
|
|
443
|
-
export {
|
|
511
|
+
export { durableObjectRoomName as C, connTag as S, roomToDurableId as T, readDoPitrBookmark as _, durableObjectLive as a, assertCloudflareEnvironment as b, liveInvalidateToRoom as c, rejectedAttachment as d, socketAttachment as f, isDoPitrUnavailable as g, armDoPitr as h, durableObjectCursorLog as i, CfWsClient as l, DoPitrUnavailableError as m, isValidExpiry as n, initDoLive as o, WsServerHolder as p, DoCursorLog as r, initializeDoLiveResources as s, isCanonicalBoundedText as t, MAX_WS_ATTACHMENT_BYTES as u, __decorate as v, roomTag as w, registerCloudflareEnvironment as x, resolveCloudflareRoot as y };
|
|
444
512
|
|
|
445
|
-
//# sourceMappingURL=nonce-validation-
|
|
513
|
+
//# sourceMappingURL=nonce-validation-Bcqf3FvY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce-validation-Bcqf3FvY.js","names":["encoder","encoder"],"sources":["../src/websocket/room-id.ts","../src/environment.ts","../src/root-module.ts","../src/websocket/do-pitr.ts","../src/websocket/ws-server-holder.ts","../src/websocket/ws-attachment.ts","../src/websocket/do-state.ts","../src/websocket/cf-ws-client.ts","../src/websocket/do-live.ts","../src/nonce/nonce-validation.ts"],"sourcesContent":["// Canonical room ↔ Durable Object mappings. The SAME functions are used by the\n// Worker upgrade route (to pick the DO) and by every server-initiated emit — so\n// a connection and a later broadcast always resolve to the same DO instance.\n\n/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */\nexport function roomTag(roomId: string): string {\n return `room:${roomId}`;\n}\n\n/** Hibernation tag addressing one connection directly. */\nexport function connTag(connId: string): string {\n return `conn:${connId}`;\n}\n\nconst DO_ROOM_NAME_PREFIX = 'vela:ws:v2:';\nconst MAX_DO_ROOM_NAME_BYTES = 1_024;\nconst encoder = new TextEncoder();\n\n/** Stable, collision-free Durable Object name for one gateway's room. */\nexport function durableObjectRoomName(gatewayPath: string, roomId: string): string {\n if (\n typeof gatewayPath !== 'string' ||\n gatewayPath.length === 0 ||\n typeof roomId !== 'string' ||\n roomId.length === 0 ||\n /[\\u0000-\\u001f\\u007f]/.test(gatewayPath) ||\n /[\\u0000-\\u001f\\u007f]/.test(roomId)\n ) {\n throw new Error('A non-empty, control-free gateway path and room id are required');\n }\n const name = `${DO_ROOM_NAME_PREFIX}${encodeURIComponent(gatewayPath)}:${encodeURIComponent(roomId)}`;\n if (encoder.encode(name).byteLength > MAX_DO_ROOM_NAME_BYTES) {\n throw new Error('The gateway/room Durable Object name exceeds 1024 bytes');\n }\n return name;\n}\n\n/** One Durable Object instance per gateway + room, addressed by name. */\nexport function roomToDurableId(\n ns: Pick<DurableObjectNamespace, 'idFromName'>,\n gatewayPath: string,\n roomId: string,\n): DurableObjectId {\n return ns.idFromName(durableObjectRoomName(gatewayPath, roomId));\n}\n","import { defineProvider, InjectionToken } from '@velajs/vela';\nimport type { Container } from '@velajs/vela/internal';\n\n/** An application's native Workers environment, including bindings and secrets. */\nexport interface CloudflareEnvironment<T extends object> {\n /** Typed token used by @Inject and provider factories. */\n readonly token: InjectionToken<T>;\n /** The environment supplied by the current platform event or DO constructor. */\n readonly env: T;\n}\n\n/**\n * Register native bindings before provider factories or lifecycle hooks run.\n * No platform I/O is performed here; callers create applications inside an event.\n */\nexport function registerCloudflareEnvironment<T extends object>(\n container: Container,\n environment: CloudflareEnvironment<T>,\n): void {\n container.register(defineProvider(environment.token, { useValue: environment.env }));\n container.markGlobalToken(environment.token);\n}\n\n/** Reject accidental reuse of an application with another event's environment. */\nexport function assertCloudflareEnvironment<T extends object>(expected: T, actual: T): void {\n if (actual !== expected) {\n throw new Error(\n 'This Cloudflare application belongs to a different environment. ' +\n 'Create an application with the current event environment.',\n );\n }\n}\n","import type { Type } from '@velajs/vela';\n\n/** A static module, or a module graph built from this Worker's native environment. */\nexport type CloudflareRoot<T extends object> = Type | { create(env: T): Type };\n\nexport function resolveCloudflareRoot<T extends object>(root: CloudflareRoot<T>, env: T): Type {\n return typeof root === 'function' ? root : root.create(env);\n}\n","/**\n * Durable Object point-in-time recovery (PITR) — thin, testable wrappers over a\n * SQLite-backed DO's native bookmark API. A SQLite Durable Object exposes three\n * storage methods (last-30-days PITR):\n *\n * - `getCurrentBookmark()` — an opaque bookmark for the storage's current state.\n * - `getBookmarkForTime(t)` — the bookmark closest to a wall-clock instant.\n * - `onNextSessionRestoreBookmark(b)` — arm a restore to bookmark `b`; the DO\n * restores to it the next time it starts a session, and the call RETURNS a\n * bookmark for the state JUST BEFORE the restore (the undo handle).\n *\n * These methods are ABSENT on a non-SQLite DO (key-value storage) and in some\n * local-dev runtimes, so this module models storage structurally with all three\n * methods OPTIONAL and degrades to a typed {@link DoPitrUnavailableError} (a\n * `code: 'PITR_UNAVAILABLE'`, HTTP 409 error) rather than an\n * `undefined is not a function` TypeError when a needed method is missing.\n *\n * Neither wrapper aborts the DO — `armDoPitr` only ARMS the restore and returns\n * the undo bookmark; the caller (the WS-DO RPC method) decides whether to\n * `ctx.abort()` to apply it immediately vs. on the next natural restart.\n *\n * This file is `cloudflare:workers`-free and pulls in NOTHING from `@velajs/vela`\n * or `@velajs/studio` — it is the raw capability the studio `TimeTravelPort`\n * wraps. Dependency direction is one-way: studio → cloudflare, never the reverse.\n */\n\n/**\n * The subset of `DurableObjectStorage` this module touches, with every method\n * OPTIONAL so it structurally models a non-SQLite DO whose storage has none of\n * them. A real `DurableObjectStorage` (whose methods are required) is assignable\n * to this shape.\n */\nexport interface DoPitrStorage {\n getCurrentBookmark?(): Promise<string>;\n getBookmarkForTime?(timestamp: number | Date): Promise<string>;\n onNextSessionRestoreBookmark?(bookmark: string): Promise<string>;\n}\n\n/** A read of a DO's current bookmark (+ the by-time bookmark when a time is given). */\nexport interface DoPitrBookmarkRead {\n /** The bookmark for the DO storage's current state. */\n current: string;\n /** The bookmark closest to the requested time (only when `time` was passed). */\n forTime?: string;\n}\n\n/** Arming input for {@link armDoPitr}: a target (bookmark WINS over time) + restart intent. */\nexport interface DoPitrArmOptions {\n /** An explicit target bookmark. Takes precedence over `time`. */\n bookmark?: string;\n /** A wall-clock target (epoch ms, ISO string, or Date), resolved to a bookmark. */\n time?: number | string | Date;\n /** Caller intent to restart-now; recorded on the result. `armDoPitr` never aborts. */\n restart?: boolean;\n}\n\n/** The result of arming a PITR restore (before any restart is applied). */\nexport interface DoPitrArmResult {\n /** The bookmark the restore is armed to. */\n restoredTo: string;\n /** The bookmark for the pre-restore state — restore to this to undo. */\n undoBookmark: string;\n /** Whether a restart-now was requested (the RPC layer performs the actual abort). */\n restarted: boolean;\n}\n\n/** The RPC surface a PITR-capable Vela WebSocket DO stub exposes to a Worker. */\nexport interface VelaDoPitrRpc {\n pitrCurrentBookmark(): Promise<DoPitrBookmarkRead>;\n pitrBookmarkForTime(time: number | string): Promise<DoPitrBookmarkRead>;\n pitrArmRestore(opts: DoPitrArmOptions): Promise<DoPitrArmResult>;\n}\n\n/** Structural view of a DO id (avoids depending on `@cloudflare/workers-types` downstream). */\nexport interface DoPitrId {\n toString(): string;\n readonly name?: string | null;\n}\n\n/**\n * Structural view of a DO namespace binding whose stubs speak the PITR RPC. A\n * downstream (the studio `@velajs/studio/cloudflare` port) types the app's\n * namespace binding as this shape to reach the PITR methods without importing\n * `@cloudflare/workers-types`.\n */\nexport interface DoPitrNamespace {\n idFromName(name: string): DoPitrId;\n get(id: DoPitrId): VelaDoPitrRpc;\n}\n\nconst PITR_UNAVAILABLE_CODE = 'PITR_UNAVAILABLE';\n\n/**\n * Thrown when a DO's storage lacks the SQLite bookmark API (non-SQLite DO, or a\n * local runtime without PITR). Carries a stable `code` + HTTP 409 `status`, and\n * a recognizable `name`/message so the studio port can map it to\n * `TIMETRAVEL_UNAVAILABLE` even after the error crosses the Worker→DO RPC hop\n * (which preserves `name` + `message`, not arbitrary own-properties).\n */\nexport class DoPitrUnavailableError extends Error {\n readonly code = PITR_UNAVAILABLE_CODE;\n readonly status = 409;\n\n constructor(message = 'Durable Object point-in-time recovery is unavailable on this storage') {\n super(`${PITR_UNAVAILABLE_CODE}: ${message}`);\n this.name = 'DoPitrUnavailableError';\n }\n}\n\n/**\n * True when `error` signals DO PITR unavailability. Robust across the Worker→DO\n * RPC hop: checks the `code` own-property (same process) AND the `name` / message\n * sentinel (survive RPC serialization) so a downstream can classify it either way.\n */\nexport function isDoPitrUnavailable(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const record = error as { code?: unknown; name?: unknown; message?: unknown };\n if (record.code === PITR_UNAVAILABLE_CODE) return true;\n if (record.name === 'DoPitrUnavailableError') return true;\n return (\n typeof record.message === 'string' && record.message.startsWith(`${PITR_UNAVAILABLE_CODE}:`)\n );\n}\n\n/** Normalize an epoch-ms number / ISO string / Date to the `number | Date` the DO API accepts. */\nfunction toStorageTime(time: number | string | Date): number | Date {\n if (typeof time === 'number') return time;\n if (time instanceof Date) return time;\n const asNumber = Number(time);\n return time.trim() !== '' && Number.isFinite(asNumber) ? asNumber : new Date(time);\n}\n\n/**\n * Read a DO's current bookmark, and — when `time` is given — the bookmark closest\n * to that instant. Throws {@link DoPitrUnavailableError} when a needed method is\n * absent, never `undefined is not a function`.\n */\nexport async function readDoPitrBookmark(\n storage: DoPitrStorage,\n time?: number | string | Date,\n): Promise<DoPitrBookmarkRead> {\n const getCurrent = storage.getCurrentBookmark;\n if (typeof getCurrent !== 'function') throw new DoPitrUnavailableError();\n const current = await getCurrent.call(storage);\n if (time === undefined) return { current };\n const getForTime = storage.getBookmarkForTime;\n if (typeof getForTime !== 'function') throw new DoPitrUnavailableError();\n const forTime = await getForTime.call(storage, toStorageTime(time));\n return { current, forTime };\n}\n\n/**\n * Arm a PITR restore. Resolves the target (an explicit `bookmark` WINS over\n * `time`), arms it via `onNextSessionRestoreBookmark`, and returns the undo\n * bookmark the DO reports for the pre-restore state. Does NOT abort — the caller\n * decides whether to restart now. Throws {@link DoPitrUnavailableError} when the\n * arming API (or the by-time resolver a `time` target needs) is absent.\n */\nexport async function armDoPitr(\n storage: DoPitrStorage,\n opts: DoPitrArmOptions,\n): Promise<DoPitrArmResult> {\n const armRestore = storage.onNextSessionRestoreBookmark;\n if (typeof armRestore !== 'function') throw new DoPitrUnavailableError();\n\n let target: string;\n if (opts.bookmark !== undefined) {\n target = opts.bookmark;\n } else if (opts.time !== undefined) {\n const getForTime = storage.getBookmarkForTime;\n if (typeof getForTime !== 'function') throw new DoPitrUnavailableError();\n target = await getForTime.call(storage, toStorageTime(opts.time));\n } else {\n throw new DoPitrUnavailableError('a target bookmark or time is required to arm a restore');\n }\n\n const undoBookmark = await armRestore.call(storage, target);\n return { restoredTo: target, undoBookmark, restarted: opts.restart === true };\n}\n","import { Injectable } from '@velajs/vela';\nimport { resolveMaxFrameBytes } from '@velajs/vela/websocket';\nimport type { BroadcastOperator, WsServer } from '@velajs/vela/websocket';\n\n/**\n * Late-bound `WsServer`. Provided as `WS_SERVER` (one per DI container, i.e. per\n * Durable Object instance), then pointed at the ctx-backed server once the DO\n * builds. Gateways inject it via `@WebSocketServer()`; it throws if used before\n * a runtime binds it (e.g. from the stateless Worker isolate).\n */\n@Injectable()\nexport class WsServerHolder implements WsServer {\n private target?: WsServer;\n private maxFrameBytes?: number;\n\n setTarget(server: WsServer): void {\n this.target = server;\n if (this.maxFrameBytes !== undefined) server.setOutboundFrameLimit?.(this.maxFrameBytes);\n }\n\n setOutboundFrameLimit(maxFrameBytes: number): void {\n const resolved = resolveMaxFrameBytes({ maxFrameBytes });\n this.maxFrameBytes =\n this.maxFrameBytes === undefined ? resolved : Math.max(this.maxFrameBytes, resolved);\n this.target?.setOutboundFrameLimit?.(this.maxFrameBytes);\n }\n\n private get resolved(): WsServer {\n if (!this.target) {\n throw new Error(\n 'WebSocket server is only available inside a WebSocket Durable Object. To ' +\n 'push from a Worker HTTP handler, use broadcastToRoom(namespace, gatewayPath, room, ...).',\n );\n }\n return this.target;\n }\n\n emit(event: string, data?: unknown): void | Promise<void> {\n return this.resolved.emit(event, data);\n }\n to(room: string): BroadcastOperator {\n return this.resolved.to(room);\n }\n in(room: string): BroadcastOperator {\n return this.resolved.in(room);\n }\n except(room: string): BroadcastOperator {\n return this.resolved.except(room);\n }\n}\n","import { assertWebSocketRoomId, DEFAULT_WS_MAX_JOINED_ROOMS } from '@velajs/vela/websocket';\nimport type { WsAttachment, WsLike } from './do-state';\n\nconst record = (value: unknown): value is Record<string, unknown> => {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return (\n (prototype === Object.prototype || prototype === null) &&\n Reflect.ownKeys(value).every((key) => {\n const property = Object.getOwnPropertyDescriptor(value, key);\n return property !== undefined && 'value' in property;\n })\n );\n};\nconst positive = (value: unknown): value is number =>\n typeof value === 'number' && Number.isSafeInteger(value) && value > 0;\nconst text = (value: unknown): value is string =>\n typeof value === 'string' &&\n value.length > 0 &&\n new TextEncoder().encode(value).byteLength <= 2048;\n\n/** Versionless 1.x records remain readable; malformed/future versions fail closed. */\nexport function readWsAttachment(value: unknown): WsAttachment | undefined {\n if (!record(value) || (value.version !== undefined && value.version !== 1)) return undefined;\n if (\n !text(value.connId) ||\n typeof value.path !== 'string' ||\n value.path.length > 8192 ||\n (value.state !== 'pending' && value.state !== 'active' && value.state !== 'rejected') ||\n !Array.isArray(value.rooms) ||\n value.rooms.length > DEFAULT_WS_MAX_JOINED_ROOMS ||\n !record(value.data)\n )\n return undefined;\n const rooms: string[] = [];\n for (let index = 0; index < value.rooms.length; index++) {\n const descriptor = Object.getOwnPropertyDescriptor(value.rooms, index);\n if (!descriptor || !('value' in descriptor) || typeof descriptor.value !== 'string')\n return undefined;\n const room: string = descriptor.value;\n try {\n assertWebSocketRoomId(room);\n } catch {\n return undefined;\n }\n if (rooms.includes(room)) return undefined;\n rooms.push(room);\n }\n if (value.maxFrameBytes !== undefined && !positive(value.maxFrameBytes)) return undefined;\n if (value.expiresAtMs !== undefined && !positive(value.expiresAtMs)) return undefined;\n if (value.userId !== undefined && !text(value.userId)) return undefined;\n if (value.tenantId !== undefined && !text(value.tenantId)) return undefined;\n let principal: WsAttachment['principal'];\n if (value.principal !== undefined) {\n const p = value.principal;\n if (\n !record(p) ||\n !text(p.issuer) ||\n !text(p.subject) ||\n (p.principalType !== 'user' && p.principalType !== 'service')\n )\n return undefined;\n principal = { issuer: p.issuer, subject: p.subject, principalType: p.principalType };\n if (value.userId !== undefined && value.userId !== p.subject) return undefined;\n }\n return {\n version: 1,\n connId: value.connId,\n state: value.state,\n path: value.path,\n rooms,\n data: { ...value.data },\n ...(principal ? { principal } : {}),\n ...(typeof value.userId === 'string' ? { userId: value.userId } : {}),\n ...(typeof value.tenantId === 'string' ? { tenantId: value.tenantId } : {}),\n ...(typeof value.expiresAtMs === 'number' ? { expiresAtMs: value.expiresAtMs } : {}),\n ...(typeof value.maxFrameBytes === 'number' ? { maxFrameBytes: value.maxFrameBytes } : {}),\n };\n}\n\nexport function socketAttachment(ws: WsLike): WsAttachment | undefined {\n try {\n return readWsAttachment(ws.deserializeAttachment());\n } catch {\n return undefined;\n }\n}\n\nexport function rejectedAttachment(): WsAttachment {\n return { version: 1, connId: '', state: 'rejected', path: '', rooms: [], data: {} };\n}\n","// Minimal structural views of the Durable Object runtime, so the transport\n// logic is unit-testable in Node with fakes. Real `DurableObjectState` and\n// `WebSocket` (from @cloudflare/workers-types) satisfy these structurally.\n\n/** Cloudflare's serialized WebSocket hibernation attachment ceiling. */\nexport const MAX_WS_ATTACHMENT_BYTES = 16_384;\n\nexport interface WsLike {\n readonly readyState?: number;\n readonly bufferedAmount?: number;\n send(message: string | ArrayBuffer): void;\n close(code?: number, reason?: string): void;\n serializeAttachment(value: unknown): void;\n deserializeAttachment(): unknown;\n}\n\n/** Structural view of the DO's SQLite handle (`ctx.storage.sql`, requires `new_sqlite_classes`). */\nexport interface SqlStorageLike {\n exec(query: string, ...bindings: unknown[]): { toArray(): Record<string, unknown>[] };\n}\n\nexport interface DoStateLike {\n readonly id: { toString(): string; readonly name?: string | null };\n acceptWebSocket(ws: WsLike, tags?: string[]): void;\n getWebSockets(tag?: string): WsLike[];\n setWebSocketAutoResponse?(pair: unknown): void;\n /** Present on SQLite-backed DOs — the live cursor log lives here. */\n readonly storage?: { sql?: SqlStorageLike };\n}\n\n/** Per-connection metadata persisted in the hibernation attachment (≤ 16 KiB). */\nexport interface WsAttachment {\n /** Absent on 1.x attachments created before versioned validation. */\n version?: 1;\n connId: string;\n /** Only active sockets may dispatch frames or receive fan-out. */\n state: 'pending' | 'active' | 'rejected';\n userId?: string;\n /** Verified, issuer-qualified connection principal. */\n principal?: {\n issuer: string;\n subject: string;\n principalType: 'user' | 'service';\n };\n /** Trusted server-derived tenant boundary for this connection. */\n tenantId?: string;\n /** Verified auth credential expiry in epoch milliseconds. */\n expiresAtMs?: number;\n /** The gateway route path this socket belongs to — used to route messages. */\n path: string;\n /** Validated inbound/outbound gateway frame ceiling, persisted across hibernation. */\n maxFrameBytes?: number;\n /** Dynamically-joined room names (the hub room is also a hibernation tag). */\n rooms: string[];\n data: Record<string, unknown>;\n}\n","import { socketAttachment, rejectedAttachment } from './ws-attachment';\nimport type { WebSocketSendResult, WsClient } from '@velajs/vela/websocket';\nimport {\n assertWebSocketRoomId,\n DEFAULT_WS_MAX_FRAME_BYTES,\n DEFAULT_WS_MAX_JOINED_ROOMS,\n WebSocketSendGate,\n} from '@velajs/vela/websocket';\nimport {\n MAX_WS_ATTACHMENT_BYTES,\n type DoStateLike,\n type WsAttachment,\n type WsLike,\n} from './do-state';\nconst encoder = new TextEncoder();\n\n/**\n * Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.\n * Per-connection state lives in the hibernation attachment (survives eviction),\n * so a fresh `CfWsClient` is reconstructed per message with no in-memory state.\n */\nexport class CfWsClient<\n TData extends Record<string, unknown> = Record<string, unknown>,\n> implements WsClient<TData> {\n readonly #attachment: WsAttachment;\n\n constructor(\n private readonly ctx: DoStateLike,\n private readonly ws: WsLike,\n private readonly sendGate = new WebSocketSendGate(),\n ) {\n this.#attachment = socketAttachment(ws) ?? rejectedAttachment();\n }\n\n get id(): string {\n return this.#attachment.connId;\n }\n\n /** The gateway route path this socket connected on (used to route messages). */\n get path(): string {\n return this.#attachment.path;\n }\n\n get rooms(): ReadonlySet<string> {\n return new Set(this.#attachment.rooms);\n }\n\n get data(): TData {\n return this.#attachment.data as TData;\n }\n\n set data(value: TData) {\n this.#attachment.data = value;\n }\n\n get raw(): unknown {\n return this.ws;\n }\n\n get maxFrameBytes(): number {\n const value = this.#attachment.maxFrameBytes;\n return typeof value === 'number' && Number.isSafeInteger(value) && value > 0\n ? value\n : DEFAULT_WS_MAX_FRAME_BYTES;\n }\n\n send(event: string, data?: unknown, id?: string): void {\n this.sendRaw(JSON.stringify(id !== undefined ? { id, event, data } : { event, data }));\n }\n\n sendRaw(payload: string): void {\n this.trySendRaw(payload);\n }\n\n trySendRaw(payload: string): WebSocketSendResult {\n if (this.#attachment.state === 'rejected') return 'closed';\n return this.sendGate.trySend(this.ws, payload, this.maxFrameBytes);\n }\n\n join(room: string): void {\n assertWebSocketRoomId(room);\n if (!this.#attachment.rooms.includes(room)) {\n if (this.#attachment.rooms.length >= DEFAULT_WS_MAX_JOINED_ROOMS) {\n throw new Error(`A WebSocket may join at most ${DEFAULT_WS_MAX_JOINED_ROOMS} rooms`);\n }\n this.#attachment.rooms.push(room);\n this.persist();\n }\n }\n\n leave(room: string): void {\n const next = this.#attachment.rooms.filter((r) => r !== room);\n if (next.length !== this.#attachment.rooms.length) {\n this.#attachment.rooms = next;\n this.persist();\n }\n }\n\n /** Persist `data`/room mutations to the hibernation attachment. */\n commit(): void {\n this.persist();\n }\n\n close(code?: number, reason?: string): void {\n this.ws.close(code, reason);\n }\n\n private persist(): void {\n if (this.#attachment.state === 'rejected') throw new Error('Invalid WebSocket attachment');\n const serialized = JSON.stringify(this.#attachment);\n if (encoder.encode(serialized).length > MAX_WS_ATTACHMENT_BYTES) {\n throw new Error(\n `WebSocket attachment exceeds the 16 KiB Cloudflare limit. Store large ` +\n `per-connection state in Durable Object storage keyed by connId instead.`,\n );\n }\n this.ws.serializeAttachment(this.#attachment);\n }\n}\n","import type { CfRoomRegistry } from './cf-room-registry';\nimport type { Container } from '@velajs/vela';\nimport {\n LIVE_CURSOR_LOG,\n LIVE_DRIVER,\n LiveEngine,\n readPersistedLiveSubscriptions,\n} from '@velajs/vela/live';\nimport type {\n CommitStamp,\n CursorLog,\n InvalidationCommand,\n LiveDriver,\n LiveInvalidationSink,\n ResumeVerdict,\n} from '@velajs/vela/live';\nimport { CfWsClient } from './cf-ws-client';\nimport type { DoStateLike, SqlStorageLike } from './do-state';\nimport { roomToDurableId } from './room-id';\n\nconst DEFAULT_ROOM = 'default';\nconst DEFAULT_MAX_LOG_ROWS = 4096;\n\n/**\n * The durable `CursorLog`: an append-only tag-invalidation log in the DO's\n * SQLite (`__vela_live_log`, AUTOINCREMENT seq = cursor) plus an epoch UUID in\n * `__vela_live_meta`. Because the cursor survives hibernation AND trims (it is\n * read from `sqlite_sequence`, lunora's `ctx-db-cdc.ts` trick), a reconnecting\n * client whose gap the log still covers gets a tiny `resume` instead of a\n * re-run — the real-resume half of the live protocol.\n *\n * Constructed un-initialized at module-composition time (the same app module\n * bootstraps in the Worker AND in each DO); `initDoLive` wires the SQLite\n * handle inside the DO. In the Worker isolate it stays un-initialized — and is\n * never consulted there, because `durableObjectLive()` routes every\n * invalidation to the room DO's log (one log scope per room, exactly the\n * protocol's model).\n */\nexport class DoCursorLog implements CursorLog {\n private sql?: SqlStorageLike;\n private epoch?: string;\n\n constructor(private readonly maxRows = DEFAULT_MAX_LOG_ROWS) {}\n\n /** @internal — called by `initDoLive` with the DO's `ctx.storage.sql`. */\n _initialize(sql: SqlStorageLike): void {\n this.sql = sql;\n sql.exec(\n 'CREATE TABLE IF NOT EXISTS __vela_live_log (seq INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, tags TEXT NOT NULL)',\n );\n sql.exec('CREATE TABLE IF NOT EXISTS __vela_live_meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)');\n const row = sql.exec(\"SELECT v FROM __vela_live_meta WHERE k = 'epoch'\").toArray()[0];\n if (row && typeof row.v === 'string') {\n this.epoch = row.v;\n } else {\n this.epoch = crypto.randomUUID();\n sql.exec(\"INSERT INTO __vela_live_meta (k, v) VALUES ('epoch', ?)\", this.epoch);\n }\n }\n\n append(tags: string[]): CommitStamp {\n const sql = this.assertReady();\n sql.exec(\n 'INSERT INTO __vela_live_log (ts, tags) VALUES (?, ?)',\n Date.now(),\n JSON.stringify(tags),\n );\n const stamp = this.current();\n // Bounded retention: trimmed gaps degrade to snapshot-on-reconnect.\n if (stamp.cursor > this.maxRows) {\n sql.exec('DELETE FROM __vela_live_log WHERE seq <= ?', stamp.cursor - this.maxRows);\n }\n return stamp;\n }\n\n current(): CommitStamp {\n const sql = this.assertReady();\n // sqlite_sequence survives DELETE-based trims, so the cursor never\n // rewinds. The table itself only materializes on the first AUTOINCREMENT\n // insert — before that the log is empty and the cursor is 0.\n let cursor = 0;\n try {\n const row = sql\n .exec(\"SELECT seq FROM sqlite_sequence WHERE name = '__vela_live_log'\")\n .toArray()[0];\n cursor = typeof row?.seq === 'number' ? row.seq : Number(row?.seq ?? 0);\n } catch {\n cursor = 0;\n }\n if (!this.epoch) throw new Error('DoCursorLog epoch is not initialized.');\n return { cursor, epoch: this.epoch };\n }\n\n evaluateResume(\n sinceCursor: number,\n sinceEpoch: string,\n subscriptionTags: string[],\n ): ResumeVerdict {\n const sql = this.assertReady();\n const { cursor, epoch } = this.current();\n if (sinceEpoch !== epoch) return 'snapshot'; // forked timeline (reset/recreated DO)\n if (sinceCursor > cursor) return 'snapshot'; // rollback guard\n if (sinceCursor === cursor) return 'resume';\n\n const minRow = sql.exec('SELECT MIN(seq) AS m FROM __vela_live_log').toArray()[0];\n const min = minRow?.m == null ? undefined : Number(minRow.m);\n // The log must still cover (sinceCursor, cursor] — a trimmed gap cannot be reasoned about.\n if (min === undefined || min > sinceCursor + 1) return 'snapshot';\n\n const subTags = new Set(subscriptionTags);\n for (const row of sql\n .exec('SELECT tags FROM __vela_live_log WHERE seq > ?', sinceCursor)\n .toArray()) {\n let tags: unknown;\n try {\n tags = JSON.parse(String(row.tags));\n } catch {\n return 'snapshot';\n }\n if (\n Array.isArray(tags) &&\n tags.some((tag: unknown) => typeof tag === 'string' && subTags.has(tag))\n )\n return 'rerun';\n }\n return 'resume';\n }\n\n private assertReady(): SqlStorageLike {\n if (!this.sql) {\n throw new Error(\n 'DoCursorLog is not initialized. It only runs inside a SQLite-backed Durable Object ' +\n '(wrangler: new_sqlite_classes) — Worker-side invalidations must go through durableObjectLive(), ' +\n \"which routes them to the room DO's log.\",\n );\n }\n return this.sql;\n }\n}\n\nexport interface DurableObjectLiveOptions {\n /** Native, RPC-typed namespace supplied by the application's environment. */\n namespace: LiveNamespace;\n /** Exact `@WebSocketGateway()` path sharing this room/log namespace. */\n gatewayPath: string;\n /** Room used when an invalidation names none. Matches the client default. */\n defaultRoom?: string;\n}\n\nexport interface LiveInvalidateStub {\n invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined>;\n}\n\n/** Only the native namespace operations required for live invalidation. */\nexport interface LiveNamespace {\n idFromName(name: string): DurableObjectId;\n get(id: DurableObjectId): LiveInvalidateStub;\n}\n\n/** One driver per application; construct from a LiveModule driver factory. */\nexport class CfLiveDriver implements LiveDriver {\n readonly kind = 'durable-object';\n private sink: LiveInvalidationSink | undefined;\n private localMode = false;\n\n constructor(private readonly options: DurableObjectLiveOptions) {}\n\n bind(sink: LiveInvalidationSink): void {\n this.sink = sink;\n }\n\n /** @internal — a DO dispatches to its own engine and SQLite log. */\n _setLocalMode(): void {\n this.localMode = true;\n }\n\n dispatch(cmd: InvalidationCommand): Promise<CommitStamp | undefined> | CommitStamp | undefined {\n if (this.localMode) return this.sink?.applyInvalidation(cmd);\n const { namespace, gatewayPath, defaultRoom } = this.options;\n const room = cmd.room ?? defaultRoom ?? DEFAULT_ROOM;\n return namespace\n .get(roomToDurableId(namespace, gatewayPath, room))\n .invalidate({ ...cmd, room });\n }\n}\n\n/** Use in LiveModule.forRootAsync: driver: () => durableObjectLive({ namespace: env.ROOMS, ... }). */\nexport function durableObjectLive(options: DurableObjectLiveOptions): CfLiveDriver {\n return new CfLiveDriver(options);\n}\n\n/** The app-facing surface of the engine reached through `app.entrypoints.ofKind('live')`. */\ninterface EntrypointsApp {\n entrypoints: { ofKind(kind: string): Array<{ meta: unknown }> };\n}\n\n/** @internal — prepare per-DO resources before user lifecycle hooks can invalidate. */\nexport function initializeDoLiveResources(container: Container, ctx: DoStateLike): void {\n if (container.has(LIVE_CURSOR_LOG)) {\n const log = container.resolve(LIVE_CURSOR_LOG);\n if (log instanceof DoCursorLog) {\n const sql = ctx.storage?.sql;\n if (!sql) {\n throw new Error(\n 'DoCursorLog requires a SQLite-backed Durable Object: add this class to ' +\n \"wrangler's `migrations[].new_sqlite_classes`. Falling back is not possible — \" +\n 'either enable SQLite or drop the `log: () => durableObjectCursorLog()` option ' +\n '(snapshot-on-reconnect semantics).',\n );\n }\n log._initialize(sql);\n }\n }\n\n if (container.has(LIVE_DRIVER)) {\n const driver = container.resolve(LIVE_DRIVER);\n if (driver instanceof CfLiveDriver) driver._setLocalMode();\n }\n}\n\n/**\n * DO-side wiring after lifecycle, called from `buildDoRuntime`: replay every hibernation-persisted\n * subscription into the (fresh) engine so an eviction is invisible to\n * subscribers. Returns the engine for the `invalidate` RPC, or undefined when\n * the app doesn't use LiveModule.\n */\nexport function initDoLive(\n app: EntrypointsApp,\n ctx: DoStateLike,\n registry?: CfRoomRegistry,\n): LiveEngine | undefined {\n const entry = app.entrypoints.ofKind('live')[0];\n if (!entry) return undefined;\n if (typeof entry.meta !== 'object' || entry.meta === null || !('engine' in entry.meta)) {\n throw new Error('Invalid live entrypoint metadata.');\n }\n const engine = entry.meta.engine;\n if (!(engine instanceof LiveEngine)) throw new Error('Invalid live entrypoint engine.');\n\n // Wake-time replay: subscriptions ride the hibernation attachments.\n for (const ws of ctx.getWebSockets()) {\n const client = registry?.clientFor(ws) ?? new CfWsClient(ctx, ws);\n if (!client.id) continue;\n for (const record of readPersistedLiveSubscriptions(client)) {\n engine.restoreSubscription(client.path, client, record);\n }\n }\n\n return engine;\n}\n\n/** Ergonomic alias: the log option for `LiveModule.forRoot` on Cloudflare. */\nexport function durableObjectCursorLog(maxRows?: number): DoCursorLog {\n return new DoCursorLog(maxRows);\n}\n\n/**\n * Invalidate live tags in a room from a Worker (controller / cron / queue\n * consumer) — the live sibling of `broadcastToRoom`. Returns the room log\n * scope's commit stamp for `Vela-Commit-Cursor` stamping.\n */\nexport async function liveInvalidateToRoom(\n ns: LiveNamespace,\n gatewayPath: string,\n room: string,\n tags: string[],\n): Promise<CommitStamp | undefined> {\n const stub = ns.get(roomToDurableId(ns, gatewayPath, room));\n return stub.invalidate({ room, tags });\n}\n","const encoder = new TextEncoder();\nexport const MAX_NONCE_BYTES = 512;\nexport function isCanonicalBoundedText(value: unknown, maxBytes: number): value is string {\n if (typeof value !== 'string') return false;\n for (const character of value) {\n const codePoint = character.codePointAt(0);\n if (\n codePoint !== undefined &&\n (codePoint <= 0x1f || codePoint === 0x7f || (codePoint >= 0xd800 && codePoint <= 0xdfff))\n ) {\n return false;\n }\n }\n return value.length > 0 && value === value.trim() && encoder.encode(value).byteLength <= maxBytes;\n}\n\nexport function isValidExpiry(\n expEpochSeconds: unknown,\n nowEpochSeconds: number,\n): expEpochSeconds is number {\n return (\n typeof expEpochSeconds === 'number' &&\n Number.isSafeInteger(expEpochSeconds) &&\n expEpochSeconds > 0 &&\n expEpochSeconds >= nowEpochSeconds\n );\n}\n"],"mappings":"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"}
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { QueueDispatchResult, QueueDriver, QueueJob, QueueMessageLike } from "@velajs/vela/queue";
|
|
2
|
+
//#region src/queue/cloudflare-queue.d.ts
|
|
3
|
+
/** Logical queue names mapped to native producer bindings from this application's environment. */
|
|
4
|
+
interface CloudflareQueueProducer {
|
|
5
|
+
send(message: QueueJob, options?: QueueSendOptions): Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
type CloudflareQueueBindings = Readonly<Record<string, CloudflareQueueProducer>>;
|
|
8
|
+
/** Native sends are awaited. No buffering, implicit retry, or deferred error handling. */
|
|
9
|
+
export declare function cloudflareQueueDriver(bindings: CloudflareQueueBindings): QueueDriver;
|
|
10
|
+
interface ConsumeQueueBatchOptions {
|
|
11
|
+
/** Logical queue name. Defaults to the native batch queue name. */
|
|
12
|
+
queue?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bridge native messages to portable dispatch. Attempts come from the host.
|
|
16
|
+
* Process every message; ack successful/unsettled deliveries, then rethrow any
|
|
17
|
+
* failures so the host retries the unsettled remainder. Explicit settlement wins.
|
|
18
|
+
* The callback must await all work that should determine delivery success.
|
|
19
|
+
*/
|
|
20
|
+
export declare function consumeQueueBatch(batch: {
|
|
21
|
+
readonly queue: string;
|
|
22
|
+
readonly messages: readonly QueueMessageLike[];
|
|
23
|
+
}, dispatch: (job: QueueJob, message: QueueMessageLike) => Promise<QueueDispatchResult | void>, options?: ConsumeQueueBatchOptions): Promise<void>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export type { CloudflareQueueBindings, CloudflareQueueProducer, ConsumeQueueBatchOptions };
|
|
26
|
+
//# sourceMappingURL=queue.d.ts.map
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { observeMessage, parseQueueJob } from "@velajs/vela/queue";
|
|
2
|
+
//#region src/queue/cloudflare-queue.ts
|
|
3
|
+
/** Native sends are awaited. No buffering, implicit retry, or deferred error handling. */
|
|
4
|
+
function cloudflareQueueDriver(bindings) {
|
|
5
|
+
const queues = new Map(Object.entries(bindings));
|
|
6
|
+
return {
|
|
7
|
+
kind: "cloudflare",
|
|
8
|
+
async enqueue(job, options) {
|
|
9
|
+
const queue = queues.get(job.queue);
|
|
10
|
+
if (!queue) throw new Error(`No Cloudflare producer binding for queue '${job.queue}'.`);
|
|
11
|
+
const delayMs = options?.delayMs;
|
|
12
|
+
if (delayMs !== void 0 && (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 864e5)) throw new RangeError("Queue delayMs must be between 0 and 86400000.");
|
|
13
|
+
await queue.send(parseQueueJob(job), delayMs === void 0 ? void 0 : { delaySeconds: Math.ceil(delayMs / 1e3) });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Bridge native messages to portable dispatch. Attempts come from the host.
|
|
19
|
+
* Process every message; ack successful/unsettled deliveries, then rethrow any
|
|
20
|
+
* failures so the host retries the unsettled remainder. Explicit settlement wins.
|
|
21
|
+
* The callback must await all work that should determine delivery success.
|
|
22
|
+
*/
|
|
23
|
+
async function consumeQueueBatch(batch, dispatch, options = {}) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
const queue = options.queue ?? batch.queue;
|
|
26
|
+
for (const message of batch.messages) try {
|
|
27
|
+
const job = parseQueueJob(message.body, message.attempts);
|
|
28
|
+
if (job.queue !== queue) throw new Error(`Queue envelope '${job.queue}' does not match '${queue}'.`);
|
|
29
|
+
const observed = observeMessage(message);
|
|
30
|
+
if ((await dispatch(job, observed.message))?.handled === 0) throw new Error(`Queue job '${job.name}' has no handler.`);
|
|
31
|
+
if (observed.disposition().outcome === "unsettled") observed.message.ack();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
errors.push(error);
|
|
34
|
+
}
|
|
35
|
+
if (errors.length === 1) throw errors[0];
|
|
36
|
+
if (errors.length > 1) throw new AggregateError(errors, "Queue batch deliveries failed.");
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { cloudflareQueueDriver, consumeQueueBatch };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.js","names":[],"sources":["../src/queue/cloudflare-queue.ts"],"sourcesContent":["import { observeMessage, parseQueueJob } from '@velajs/vela/queue';\nimport type {\n QueueDispatchResult,\n QueueDriver,\n QueueJob,\n QueueMessageLike,\n} from '@velajs/vela/queue';\n\n/** Logical queue names mapped to native producer bindings from this application's environment. */\nexport interface CloudflareQueueProducer {\n // Both older void-returning bindings and current metric-returning bindings work.\n send(message: QueueJob, options?: QueueSendOptions): Promise<unknown>;\n}\nexport type CloudflareQueueBindings = Readonly<Record<string, CloudflareQueueProducer>>;\n\n/** Native sends are awaited. No buffering, implicit retry, or deferred error handling. */\nexport function cloudflareQueueDriver(bindings: CloudflareQueueBindings): QueueDriver {\n const queues = new Map(Object.entries(bindings));\n return {\n kind: 'cloudflare',\n async enqueue(job, options) {\n const queue = queues.get(job.queue);\n if (!queue) throw new Error(`No Cloudflare producer binding for queue '${job.queue}'.`);\n const delayMs = options?.delayMs;\n if (\n delayMs !== undefined &&\n (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 86_400_000)\n ) {\n throw new RangeError('Queue delayMs must be between 0 and 86400000.');\n }\n await queue.send(\n parseQueueJob(job),\n delayMs === undefined ? undefined : { delaySeconds: Math.ceil(delayMs / 1000) },\n );\n },\n };\n}\n\nexport interface ConsumeQueueBatchOptions {\n /** Logical queue name. Defaults to the native batch queue name. */\n queue?: string;\n}\n\n/**\n * Bridge native messages to portable dispatch. Attempts come from the host.\n * Process every message; ack successful/unsettled deliveries, then rethrow any\n * failures so the host retries the unsettled remainder. Explicit settlement wins.\n * The callback must await all work that should determine delivery success.\n */\nexport async function consumeQueueBatch(\n batch: { readonly queue: string; readonly messages: readonly QueueMessageLike[] },\n dispatch: (job: QueueJob, message: QueueMessageLike) => Promise<QueueDispatchResult | void>,\n options: ConsumeQueueBatchOptions = {},\n): Promise<void> {\n const errors: unknown[] = [];\n const queue = options.queue ?? batch.queue;\n for (const message of batch.messages) {\n try {\n const job = parseQueueJob(message.body, message.attempts);\n if (job.queue !== queue)\n throw new Error(`Queue envelope '${job.queue}' does not match '${queue}'.`);\n const observed = observeMessage(message);\n const result = await dispatch(job, observed.message);\n if (result?.handled === 0) throw new Error(`Queue job '${job.name}' has no handler.`);\n if (observed.disposition().outcome === 'unsettled') observed.message.ack();\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) throw new AggregateError(errors, 'Queue batch deliveries failed.');\n}\n"],"mappings":";;;AAgBA,SAAgB,sBAAsB,UAAgD;CACpF,MAAM,SAAS,IAAI,IAAI,OAAO,QAAQ,QAAQ,CAAC;CAC/C,OAAO;EACL,MAAM;EACN,MAAM,QAAQ,KAAK,SAAS;GAC1B,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK;GAClC,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,6CAA6C,IAAI,MAAM,GAAG;GACtF,MAAM,UAAU,SAAS;GACzB,IACE,YAAY,KAAA,MACX,CAAC,OAAO,SAAS,OAAO,KAAK,UAAU,KAAK,UAAU,QAEvD,MAAM,IAAI,WAAW,+CAA+C;GAEtE,MAAM,MAAM,KACV,cAAc,GAAG,GACjB,YAAY,KAAA,IAAY,KAAA,IAAY,EAAE,cAAc,KAAK,KAAK,UAAU,GAAI,EAAE,CAChF;EACF;CACF;AACF;;;;;;;AAaA,eAAsB,kBACpB,OACA,UACA,UAAoC,CAAC,GACtB;CACf,MAAM,SAAoB,CAAC;CAC3B,MAAM,QAAQ,QAAQ,SAAS,MAAM;CACrC,KAAK,MAAM,WAAW,MAAM,UAC1B,IAAI;EACF,MAAM,MAAM,cAAc,QAAQ,MAAM,QAAQ,QAAQ;EACxD,IAAI,IAAI,UAAU,OAChB,MAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,oBAAoB,MAAM,GAAG;EAC5E,MAAM,WAAW,eAAe,OAAO;EAEvC,KAAI,MADiB,SAAS,KAAK,SAAS,OAAO,EAAA,EACvC,YAAY,GAAG,MAAM,IAAI,MAAM,cAAc,IAAI,KAAK,kBAAkB;EACpF,IAAI,SAAS,YAAY,CAAC,CAAC,YAAY,aAAa,SAAS,QAAQ,IAAI;CAC3E,SAAS,OAAO;EACd,OAAO,KAAK,KAAK;CACnB;CAEF,IAAI,OAAO,WAAW,GAAG,MAAM,OAAO;CACtC,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,eAAe,QAAQ,gCAAgC;AAC1F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/cloudflare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Cloudflare Workers integration for Vela framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -43,8 +43,15 @@
|
|
|
43
43
|
"./durable-objects": {
|
|
44
44
|
"types": "./dist/durable-objects.d.ts",
|
|
45
45
|
"import": "./dist/durable-objects.js"
|
|
46
|
+
},
|
|
47
|
+
"./queue": {
|
|
48
|
+
"types": "./dist/queue.d.ts",
|
|
49
|
+
"import": "./dist/queue.js"
|
|
46
50
|
}
|
|
47
51
|
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
48
55
|
"devDependencies": {
|
|
49
56
|
"@arethetypeswrong/cli": "0.18.5",
|
|
50
57
|
"@changesets/cli": "3.0.1",
|
|
@@ -61,13 +68,13 @@
|
|
|
61
68
|
"vitest": "4.1.10",
|
|
62
69
|
"zod": "^3.25.76",
|
|
63
70
|
"@velajs/feature-flags": "1.22.1",
|
|
64
|
-
"@velajs/vela": "1.
|
|
71
|
+
"@velajs/vela": "1.25.0"
|
|
65
72
|
},
|
|
66
73
|
"peerDependencies": {
|
|
67
74
|
"@cloudflare/workers-types": ">=4",
|
|
68
75
|
"hono": ">=4",
|
|
69
76
|
"@velajs/feature-flags": "^1.22.1",
|
|
70
|
-
"@velajs/vela": "^1.
|
|
77
|
+
"@velajs/vela": "^1.25.0"
|
|
71
78
|
},
|
|
72
79
|
"peerDependenciesMeta": {
|
|
73
80
|
"@velajs/feature-flags": {
|
|
@@ -77,9 +84,6 @@
|
|
|
77
84
|
"engines": {
|
|
78
85
|
"node": ">=24"
|
|
79
86
|
},
|
|
80
|
-
"publishConfig": {
|
|
81
|
-
"access": "public"
|
|
82
|
-
},
|
|
83
87
|
"scripts": {
|
|
84
88
|
"build": "tsdown",
|
|
85
89
|
"test": "vitest run",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"nonce-validation-LE0vFk-7.js","names":["encoder","encoder"],"sources":["../src/websocket/room-id.ts","../src/environment.ts","../src/root-module.ts","../src/websocket/do-pitr.ts","../src/websocket/ws-server-holder.ts","../src/websocket/do-state.ts","../src/websocket/cf-ws-client.ts","../src/websocket/do-live.ts","../src/nonce/nonce-validation.ts"],"sourcesContent":["// Canonical room ↔ Durable Object mappings. The SAME functions are used by the\n// Worker upgrade route (to pick the DO) and by every server-initiated emit — so\n// a connection and a later broadcast always resolve to the same DO instance.\n\n/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */\nexport function roomTag(roomId: string): string {\n return `room:${roomId}`;\n}\n\n/** Hibernation tag addressing one connection directly. */\nexport function connTag(connId: string): string {\n return `conn:${connId}`;\n}\n\nconst DO_ROOM_NAME_PREFIX = 'vela:ws:v2:';\nconst MAX_DO_ROOM_NAME_BYTES = 1_024;\nconst encoder = new TextEncoder();\n\n/** Stable, collision-free Durable Object name for one gateway's room. */\nexport function durableObjectRoomName(gatewayPath: string, roomId: string): string {\n if (\n typeof gatewayPath !== 'string' ||\n gatewayPath.length === 0 ||\n typeof roomId !== 'string' ||\n roomId.length === 0 ||\n /[\\u0000-\\u001f\\u007f]/.test(gatewayPath) ||\n /[\\u0000-\\u001f\\u007f]/.test(roomId)\n ) {\n throw new Error('A non-empty, control-free gateway path and room id are required');\n }\n const name = `${DO_ROOM_NAME_PREFIX}${encodeURIComponent(gatewayPath)}:${encodeURIComponent(roomId)}`;\n if (encoder.encode(name).byteLength > MAX_DO_ROOM_NAME_BYTES) {\n throw new Error('The gateway/room Durable Object name exceeds 1024 bytes');\n }\n return name;\n}\n\n/** One Durable Object instance per gateway + room, addressed by name. */\nexport function roomToDurableId(\n ns: Pick<DurableObjectNamespace, 'idFromName'>,\n gatewayPath: string,\n roomId: string,\n): DurableObjectId {\n return ns.idFromName(durableObjectRoomName(gatewayPath, roomId));\n}\n","import { defineProvider, InjectionToken } from '@velajs/vela';\nimport type { Container } from '@velajs/vela/internal';\n\n/** An application's native Workers environment, including bindings and secrets. */\nexport interface CloudflareEnvironment<T extends object> {\n /** Typed token used by @Inject and provider factories. */\n readonly token: InjectionToken<T>;\n /** The environment supplied by the current platform event or DO constructor. */\n readonly env: T;\n}\n\n/**\n * Register native bindings before provider factories or lifecycle hooks run.\n * No platform I/O is performed here; callers create applications inside an event.\n */\nexport function registerCloudflareEnvironment<T extends object>(\n container: Container,\n environment: CloudflareEnvironment<T>,\n): void {\n container.register(defineProvider(environment.token, { useValue: environment.env }));\n container.markGlobalToken(environment.token);\n}\n\n/** Reject accidental reuse of an application with another event's environment. */\nexport function assertCloudflareEnvironment<T extends object>(expected: T, actual: T): void {\n if (actual !== expected) {\n throw new Error(\n 'This Cloudflare application belongs to a different environment. ' +\n 'Create an application with the current event environment.',\n );\n }\n}\n","import type { Type } from '@velajs/vela';\n\n/** A static module, or a module graph built from this Worker's native environment. */\nexport type CloudflareRoot<T extends object> = Type | { create(env: T): Type };\n\nexport function resolveCloudflareRoot<T extends object>(root: CloudflareRoot<T>, env: T): Type {\n return typeof root === 'function' ? root : root.create(env);\n}\n","/**\n * Durable Object point-in-time recovery (PITR) — thin, testable wrappers over a\n * SQLite-backed DO's native bookmark API. A SQLite Durable Object exposes three\n * storage methods (last-30-days PITR):\n *\n * - `getCurrentBookmark()` — an opaque bookmark for the storage's current state.\n * - `getBookmarkForTime(t)` — the bookmark closest to a wall-clock instant.\n * - `onNextSessionRestoreBookmark(b)` — arm a restore to bookmark `b`; the DO\n * restores to it the next time it starts a session, and the call RETURNS a\n * bookmark for the state JUST BEFORE the restore (the undo handle).\n *\n * These methods are ABSENT on a non-SQLite DO (key-value storage) and in some\n * local-dev runtimes, so this module models storage structurally with all three\n * methods OPTIONAL and degrades to a typed {@link DoPitrUnavailableError} (a\n * `code: 'PITR_UNAVAILABLE'`, HTTP 409 error) rather than an\n * `undefined is not a function` TypeError when a needed method is missing.\n *\n * Neither wrapper aborts the DO — `armDoPitr` only ARMS the restore and returns\n * the undo bookmark; the caller (the WS-DO RPC method) decides whether to\n * `ctx.abort()` to apply it immediately vs. on the next natural restart.\n *\n * This file is `cloudflare:workers`-free and pulls in NOTHING from `@velajs/vela`\n * or `@velajs/studio` — it is the raw capability the studio `TimeTravelPort`\n * wraps. Dependency direction is one-way: studio → cloudflare, never the reverse.\n */\n\n/**\n * The subset of `DurableObjectStorage` this module touches, with every method\n * OPTIONAL so it structurally models a non-SQLite DO whose storage has none of\n * them. A real `DurableObjectStorage` (whose methods are required) is assignable\n * to this shape.\n */\nexport interface DoPitrStorage {\n getCurrentBookmark?(): Promise<string>;\n getBookmarkForTime?(timestamp: number | Date): Promise<string>;\n onNextSessionRestoreBookmark?(bookmark: string): Promise<string>;\n}\n\n/** A read of a DO's current bookmark (+ the by-time bookmark when a time is given). */\nexport interface DoPitrBookmarkRead {\n /** The bookmark for the DO storage's current state. */\n current: string;\n /** The bookmark closest to the requested time (only when `time` was passed). */\n forTime?: string;\n}\n\n/** Arming input for {@link armDoPitr}: a target (bookmark WINS over time) + restart intent. */\nexport interface DoPitrArmOptions {\n /** An explicit target bookmark. Takes precedence over `time`. */\n bookmark?: string;\n /** A wall-clock target (epoch ms, ISO string, or Date), resolved to a bookmark. */\n time?: number | string | Date;\n /** Caller intent to restart-now; recorded on the result. `armDoPitr` never aborts. */\n restart?: boolean;\n}\n\n/** The result of arming a PITR restore (before any restart is applied). */\nexport interface DoPitrArmResult {\n /** The bookmark the restore is armed to. */\n restoredTo: string;\n /** The bookmark for the pre-restore state — restore to this to undo. */\n undoBookmark: string;\n /** Whether a restart-now was requested (the RPC layer performs the actual abort). */\n restarted: boolean;\n}\n\n/** The RPC surface a PITR-capable Vela WebSocket DO stub exposes to a Worker. */\nexport interface VelaDoPitrRpc {\n pitrCurrentBookmark(): Promise<DoPitrBookmarkRead>;\n pitrBookmarkForTime(time: number | string): Promise<DoPitrBookmarkRead>;\n pitrArmRestore(opts: DoPitrArmOptions): Promise<DoPitrArmResult>;\n}\n\n/** Structural view of a DO id (avoids depending on `@cloudflare/workers-types` downstream). */\nexport interface DoPitrId {\n toString(): string;\n readonly name?: string | null;\n}\n\n/**\n * Structural view of a DO namespace binding whose stubs speak the PITR RPC. A\n * downstream (the studio `@velajs/studio/cloudflare` port) types the app's\n * namespace binding as this shape to reach the PITR methods without importing\n * `@cloudflare/workers-types`.\n */\nexport interface DoPitrNamespace {\n idFromName(name: string): DoPitrId;\n get(id: DoPitrId): VelaDoPitrRpc;\n}\n\nconst PITR_UNAVAILABLE_CODE = 'PITR_UNAVAILABLE';\n\n/**\n * Thrown when a DO's storage lacks the SQLite bookmark API (non-SQLite DO, or a\n * local runtime without PITR). Carries a stable `code` + HTTP 409 `status`, and\n * a recognizable `name`/message so the studio port can map it to\n * `TIMETRAVEL_UNAVAILABLE` even after the error crosses the Worker→DO RPC hop\n * (which preserves `name` + `message`, not arbitrary own-properties).\n */\nexport class DoPitrUnavailableError extends Error {\n readonly code = PITR_UNAVAILABLE_CODE;\n readonly status = 409;\n\n constructor(message = 'Durable Object point-in-time recovery is unavailable on this storage') {\n super(`${PITR_UNAVAILABLE_CODE}: ${message}`);\n this.name = 'DoPitrUnavailableError';\n }\n}\n\n/**\n * True when `error` signals DO PITR unavailability. Robust across the Worker→DO\n * RPC hop: checks the `code` own-property (same process) AND the `name` / message\n * sentinel (survive RPC serialization) so a downstream can classify it either way.\n */\nexport function isDoPitrUnavailable(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const record = error as { code?: unknown; name?: unknown; message?: unknown };\n if (record.code === PITR_UNAVAILABLE_CODE) return true;\n if (record.name === 'DoPitrUnavailableError') return true;\n return (\n typeof record.message === 'string' && record.message.startsWith(`${PITR_UNAVAILABLE_CODE}:`)\n );\n}\n\n/** Normalize an epoch-ms number / ISO string / Date to the `number | Date` the DO API accepts. */\nfunction toStorageTime(time: number | string | Date): number | Date {\n if (typeof time === 'number') return time;\n if (time instanceof Date) return time;\n const asNumber = Number(time);\n return time.trim() !== '' && Number.isFinite(asNumber) ? asNumber : new Date(time);\n}\n\n/**\n * Read a DO's current bookmark, and — when `time` is given — the bookmark closest\n * to that instant. Throws {@link DoPitrUnavailableError} when a needed method is\n * absent, never `undefined is not a function`.\n */\nexport async function readDoPitrBookmark(\n storage: DoPitrStorage,\n time?: number | string | Date,\n): Promise<DoPitrBookmarkRead> {\n const getCurrent = storage.getCurrentBookmark;\n if (typeof getCurrent !== 'function') throw new DoPitrUnavailableError();\n const current = await getCurrent.call(storage);\n if (time === undefined) return { current };\n const getForTime = storage.getBookmarkForTime;\n if (typeof getForTime !== 'function') throw new DoPitrUnavailableError();\n const forTime = await getForTime.call(storage, toStorageTime(time));\n return { current, forTime };\n}\n\n/**\n * Arm a PITR restore. Resolves the target (an explicit `bookmark` WINS over\n * `time`), arms it via `onNextSessionRestoreBookmark`, and returns the undo\n * bookmark the DO reports for the pre-restore state. Does NOT abort — the caller\n * decides whether to restart now. Throws {@link DoPitrUnavailableError} when the\n * arming API (or the by-time resolver a `time` target needs) is absent.\n */\nexport async function armDoPitr(\n storage: DoPitrStorage,\n opts: DoPitrArmOptions,\n): Promise<DoPitrArmResult> {\n const armRestore = storage.onNextSessionRestoreBookmark;\n if (typeof armRestore !== 'function') throw new DoPitrUnavailableError();\n\n let target: string;\n if (opts.bookmark !== undefined) {\n target = opts.bookmark;\n } else if (opts.time !== undefined) {\n const getForTime = storage.getBookmarkForTime;\n if (typeof getForTime !== 'function') throw new DoPitrUnavailableError();\n target = await getForTime.call(storage, toStorageTime(opts.time));\n } else {\n throw new DoPitrUnavailableError('a target bookmark or time is required to arm a restore');\n }\n\n const undoBookmark = await armRestore.call(storage, target);\n return { restoredTo: target, undoBookmark, restarted: opts.restart === true };\n}\n","import { Injectable } from '@velajs/vela';\nimport { resolveMaxFrameBytes } from '@velajs/vela/websocket';\nimport type { BroadcastOperator, WsServer } from '@velajs/vela/websocket';\n\n/**\n * Late-bound `WsServer`. Provided as `WS_SERVER` (one per DI container, i.e. per\n * Durable Object instance), then pointed at the ctx-backed server once the DO\n * builds. Gateways inject it via `@WebSocketServer()`; it throws if used before\n * a runtime binds it (e.g. from the stateless Worker isolate).\n */\n@Injectable()\nexport class WsServerHolder implements WsServer {\n private target?: WsServer;\n private maxFrameBytes?: number;\n\n setTarget(server: WsServer): void {\n this.target = server;\n if (this.maxFrameBytes !== undefined) server.setOutboundFrameLimit?.(this.maxFrameBytes);\n }\n\n setOutboundFrameLimit(maxFrameBytes: number): void {\n const resolved = resolveMaxFrameBytes({ maxFrameBytes });\n this.maxFrameBytes =\n this.maxFrameBytes === undefined ? resolved : Math.max(this.maxFrameBytes, resolved);\n this.target?.setOutboundFrameLimit?.(this.maxFrameBytes);\n }\n\n private get resolved(): WsServer {\n if (!this.target) {\n throw new Error(\n 'WebSocket server is only available inside a WebSocket Durable Object. To ' +\n 'push from a Worker HTTP handler, use broadcastToRoom(namespace, gatewayPath, room, ...).',\n );\n }\n return this.target;\n }\n\n emit(event: string, data?: unknown): void | Promise<void> {\n return this.resolved.emit(event, data);\n }\n to(room: string): BroadcastOperator {\n return this.resolved.to(room);\n }\n in(room: string): BroadcastOperator {\n return this.resolved.in(room);\n }\n except(room: string): BroadcastOperator {\n return this.resolved.except(room);\n }\n}\n","// Minimal structural views of the Durable Object runtime, so the transport\n// logic is unit-testable in Node with fakes. Real `DurableObjectState` and\n// `WebSocket` (from @cloudflare/workers-types) satisfy these structurally.\n\n/** Cloudflare's serialized WebSocket hibernation attachment ceiling. */\nexport const MAX_WS_ATTACHMENT_BYTES = 16_384;\n\nexport interface WsLike {\n send(message: string | ArrayBuffer): void;\n close(code?: number, reason?: string): void;\n serializeAttachment(value: unknown): void;\n deserializeAttachment(): unknown;\n}\n\n/** Structural view of the DO's SQLite handle (`ctx.storage.sql`, requires `new_sqlite_classes`). */\nexport interface SqlStorageLike {\n exec(query: string, ...bindings: unknown[]): { toArray(): Record<string, unknown>[] };\n}\n\nexport interface DoStateLike {\n readonly id: { toString(): string; readonly name?: string | null };\n acceptWebSocket(ws: WsLike, tags?: string[]): void;\n getWebSockets(tag?: string): WsLike[];\n setWebSocketAutoResponse?(pair: unknown): void;\n /** Present on SQLite-backed DOs — the live cursor log lives here. */\n readonly storage?: { sql?: SqlStorageLike };\n}\n\n/** Per-connection metadata persisted in the hibernation attachment (≤ 16 KiB). */\nexport interface WsAttachment {\n connId: string;\n /** Only active sockets may dispatch frames or receive fan-out. */\n state: 'pending' | 'active' | 'rejected';\n userId?: string;\n /** Verified, issuer-qualified connection principal. */\n principal?: {\n issuer: string;\n subject: string;\n principalType: 'user' | 'service';\n };\n /** Trusted server-derived tenant boundary for this connection. */\n tenantId?: string;\n /** Verified auth credential expiry in epoch milliseconds. */\n expiresAtMs?: number;\n /** The gateway route path this socket belongs to — used to route messages. */\n path: string;\n /** Validated inbound/outbound gateway frame ceiling, persisted across hibernation. */\n maxFrameBytes?: number;\n /** Dynamically-joined room names (the hub room is also a hibernation tag). */\n rooms: string[];\n data: Record<string, unknown>;\n}\n","import type { WsClient } from '@velajs/vela/websocket';\nimport {\n assertWebSocketRoomId,\n DEFAULT_WS_MAX_FRAME_BYTES,\n DEFAULT_WS_MAX_JOINED_ROOMS,\n webSocketFrameFits,\n} from '@velajs/vela/websocket';\nimport {\n MAX_WS_ATTACHMENT_BYTES,\n type DoStateLike,\n type WsAttachment,\n type WsLike,\n} from './do-state';\nconst encoder = new TextEncoder();\n\nconst EMPTY: WsAttachment = { connId: '', state: 'rejected', path: '', rooms: [], data: {} };\n\n/**\n * Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.\n * Per-connection state lives in the hibernation attachment (survives eviction),\n * so a fresh `CfWsClient` is reconstructed per message with no in-memory state.\n */\nexport class CfWsClient<\n TData extends Record<string, unknown> = Record<string, unknown>,\n> implements WsClient<TData> {\n private readonly attachment: WsAttachment;\n\n constructor(\n private readonly ctx: DoStateLike,\n private readonly ws: WsLike,\n ) {\n const raw = ws.deserializeAttachment() as WsAttachment | null;\n this.attachment = raw ?? { ...EMPTY, data: {} };\n }\n\n get id(): string {\n return this.attachment.connId;\n }\n\n /** The gateway route path this socket connected on (used to route messages). */\n get path(): string {\n return this.attachment.path;\n }\n\n get rooms(): ReadonlySet<string> {\n return new Set(this.attachment.rooms);\n }\n\n get data(): TData {\n return this.attachment.data as TData;\n }\n\n set data(value: TData) {\n this.attachment.data = value;\n }\n\n get raw(): unknown {\n return this.ws;\n }\n\n get maxFrameBytes(): number {\n const value = this.attachment.maxFrameBytes;\n return typeof value === 'number' && Number.isSafeInteger(value) && value > 0\n ? value\n : DEFAULT_WS_MAX_FRAME_BYTES;\n }\n\n send(event: string, data?: unknown, id?: string): void {\n this.sendRaw(JSON.stringify(id !== undefined ? { id, event, data } : { event, data }));\n }\n\n sendRaw(payload: string): void {\n if (!webSocketFrameFits(payload, this.maxFrameBytes)) {\n this.close(1009, 'Message too large');\n return;\n }\n this.ws.send(payload);\n }\n\n join(room: string): void {\n assertWebSocketRoomId(room);\n if (!this.attachment.rooms.includes(room)) {\n if (this.attachment.rooms.length >= DEFAULT_WS_MAX_JOINED_ROOMS) {\n throw new Error(`A WebSocket may join at most ${DEFAULT_WS_MAX_JOINED_ROOMS} rooms`);\n }\n this.attachment.rooms.push(room);\n this.persist();\n }\n }\n\n leave(room: string): void {\n const next = this.attachment.rooms.filter((r) => r !== room);\n if (next.length !== this.attachment.rooms.length) {\n this.attachment.rooms = next;\n this.persist();\n }\n }\n\n /** Persist `data`/room mutations to the hibernation attachment. */\n commit(): void {\n this.persist();\n }\n\n close(code?: number, reason?: string): void {\n this.ws.close(code, reason);\n }\n\n private persist(): void {\n const serialized = JSON.stringify(this.attachment);\n if (encoder.encode(serialized).length > MAX_WS_ATTACHMENT_BYTES) {\n throw new Error(\n `WebSocket attachment exceeds the 16 KiB Cloudflare limit. Store large ` +\n `per-connection state in Durable Object storage keyed by connId instead.`,\n );\n }\n this.ws.serializeAttachment(this.attachment);\n }\n}\n","import type { Container } from '@velajs/vela';\nimport {\n LIVE_CURSOR_LOG,\n LIVE_DRIVER,\n LiveEngine,\n readPersistedLiveSubscriptions,\n} from '@velajs/vela/live';\nimport type {\n CommitStamp,\n CursorLog,\n InvalidationCommand,\n LiveDriver,\n LiveInvalidationSink,\n ResumeVerdict,\n} from '@velajs/vela/live';\nimport { CfWsClient } from './cf-ws-client';\nimport type { DoStateLike, SqlStorageLike } from './do-state';\nimport { roomToDurableId } from './room-id';\n\nconst DEFAULT_ROOM = 'default';\nconst DEFAULT_MAX_LOG_ROWS = 4096;\n\n/**\n * The durable `CursorLog`: an append-only tag-invalidation log in the DO's\n * SQLite (`__vela_live_log`, AUTOINCREMENT seq = cursor) plus an epoch UUID in\n * `__vela_live_meta`. Because the cursor survives hibernation AND trims (it is\n * read from `sqlite_sequence`, lunora's `ctx-db-cdc.ts` trick), a reconnecting\n * client whose gap the log still covers gets a tiny `resume` instead of a\n * re-run — the real-resume half of the live protocol.\n *\n * Constructed un-initialized at module-composition time (the same app module\n * bootstraps in the Worker AND in each DO); `initDoLive` wires the SQLite\n * handle inside the DO. In the Worker isolate it stays un-initialized — and is\n * never consulted there, because `durableObjectLive()` routes every\n * invalidation to the room DO's log (one log scope per room, exactly the\n * protocol's model).\n */\nexport class DoCursorLog implements CursorLog {\n private sql?: SqlStorageLike;\n private epoch?: string;\n\n constructor(private readonly maxRows = DEFAULT_MAX_LOG_ROWS) {}\n\n /** @internal — called by `initDoLive` with the DO's `ctx.storage.sql`. */\n _initialize(sql: SqlStorageLike): void {\n this.sql = sql;\n sql.exec(\n 'CREATE TABLE IF NOT EXISTS __vela_live_log (seq INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, tags TEXT NOT NULL)',\n );\n sql.exec('CREATE TABLE IF NOT EXISTS __vela_live_meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)');\n const row = sql.exec(\"SELECT v FROM __vela_live_meta WHERE k = 'epoch'\").toArray()[0];\n if (row && typeof row.v === 'string') {\n this.epoch = row.v;\n } else {\n this.epoch = crypto.randomUUID();\n sql.exec(\"INSERT INTO __vela_live_meta (k, v) VALUES ('epoch', ?)\", this.epoch);\n }\n }\n\n append(tags: string[]): CommitStamp {\n const sql = this.assertReady();\n sql.exec(\n 'INSERT INTO __vela_live_log (ts, tags) VALUES (?, ?)',\n Date.now(),\n JSON.stringify(tags),\n );\n const stamp = this.current();\n // Bounded retention: trimmed gaps degrade to snapshot-on-reconnect.\n if (stamp.cursor > this.maxRows) {\n sql.exec('DELETE FROM __vela_live_log WHERE seq <= ?', stamp.cursor - this.maxRows);\n }\n return stamp;\n }\n\n current(): CommitStamp {\n const sql = this.assertReady();\n // sqlite_sequence survives DELETE-based trims, so the cursor never\n // rewinds. The table itself only materializes on the first AUTOINCREMENT\n // insert — before that the log is empty and the cursor is 0.\n let cursor = 0;\n try {\n const row = sql\n .exec(\"SELECT seq FROM sqlite_sequence WHERE name = '__vela_live_log'\")\n .toArray()[0];\n cursor = typeof row?.seq === 'number' ? row.seq : Number(row?.seq ?? 0);\n } catch {\n cursor = 0;\n }\n if (!this.epoch) throw new Error('DoCursorLog epoch is not initialized.');\n return { cursor, epoch: this.epoch };\n }\n\n evaluateResume(\n sinceCursor: number,\n sinceEpoch: string,\n subscriptionTags: string[],\n ): ResumeVerdict {\n const sql = this.assertReady();\n const { cursor, epoch } = this.current();\n if (sinceEpoch !== epoch) return 'snapshot'; // forked timeline (reset/recreated DO)\n if (sinceCursor > cursor) return 'snapshot'; // rollback guard\n if (sinceCursor === cursor) return 'resume';\n\n const minRow = sql.exec('SELECT MIN(seq) AS m FROM __vela_live_log').toArray()[0];\n const min = minRow?.m == null ? undefined : Number(minRow.m);\n // The log must still cover (sinceCursor, cursor] — a trimmed gap cannot be reasoned about.\n if (min === undefined || min > sinceCursor + 1) return 'snapshot';\n\n const subTags = new Set(subscriptionTags);\n for (const row of sql\n .exec('SELECT tags FROM __vela_live_log WHERE seq > ?', sinceCursor)\n .toArray()) {\n let tags: unknown;\n try {\n tags = JSON.parse(String(row.tags));\n } catch {\n return 'snapshot';\n }\n if (\n Array.isArray(tags) &&\n tags.some((tag: unknown) => typeof tag === 'string' && subTags.has(tag))\n )\n return 'rerun';\n }\n return 'resume';\n }\n\n private assertReady(): SqlStorageLike {\n if (!this.sql) {\n throw new Error(\n 'DoCursorLog is not initialized. It only runs inside a SQLite-backed Durable Object ' +\n '(wrangler: new_sqlite_classes) — Worker-side invalidations must go through durableObjectLive(), ' +\n \"which routes them to the room DO's log.\",\n );\n }\n return this.sql;\n }\n}\n\nexport interface DurableObjectLiveOptions {\n /** Native, RPC-typed namespace supplied by the application's environment. */\n namespace: LiveNamespace;\n /** Exact `@WebSocketGateway()` path sharing this room/log namespace. */\n gatewayPath: string;\n /** Room used when an invalidation names none. Matches the client default. */\n defaultRoom?: string;\n}\n\nexport interface LiveInvalidateStub {\n invalidate(cmd: InvalidationCommand): Promise<CommitStamp | undefined>;\n}\n\n/** Only the native namespace operations required for live invalidation. */\nexport interface LiveNamespace {\n idFromName(name: string): DurableObjectId;\n get(id: DurableObjectId): LiveInvalidateStub;\n}\n\n/** One driver per application; construct from a LiveModule driver factory. */\nexport class CfLiveDriver implements LiveDriver {\n readonly kind = 'durable-object';\n private sink: LiveInvalidationSink | undefined;\n private localMode = false;\n\n constructor(private readonly options: DurableObjectLiveOptions) {}\n\n bind(sink: LiveInvalidationSink): void {\n this.sink = sink;\n }\n\n /** @internal — a DO dispatches to its own engine and SQLite log. */\n _setLocalMode(): void {\n this.localMode = true;\n }\n\n dispatch(cmd: InvalidationCommand): Promise<CommitStamp | undefined> | CommitStamp | undefined {\n if (this.localMode) return this.sink?.applyInvalidation(cmd);\n const { namespace, gatewayPath, defaultRoom } = this.options;\n const room = cmd.room ?? defaultRoom ?? DEFAULT_ROOM;\n return namespace\n .get(roomToDurableId(namespace, gatewayPath, room))\n .invalidate({ ...cmd, room });\n }\n}\n\n/** Use in LiveModule.forRootAsync: driver: () => durableObjectLive({ namespace: env.ROOMS, ... }). */\nexport function durableObjectLive(options: DurableObjectLiveOptions): CfLiveDriver {\n return new CfLiveDriver(options);\n}\n\n/** The app-facing surface of the engine reached through `app.entrypoints.ofKind('live')`. */\ninterface EntrypointsApp {\n entrypoints: { ofKind(kind: string): Array<{ meta: unknown }> };\n}\n\n/** @internal — prepare per-DO resources before user lifecycle hooks can invalidate. */\nexport function initializeDoLiveResources(container: Container, ctx: DoStateLike): void {\n if (container.has(LIVE_CURSOR_LOG)) {\n const log = container.resolve(LIVE_CURSOR_LOG);\n if (log instanceof DoCursorLog) {\n const sql = ctx.storage?.sql;\n if (!sql) {\n throw new Error(\n 'DoCursorLog requires a SQLite-backed Durable Object: add this class to ' +\n \"wrangler's `migrations[].new_sqlite_classes`. Falling back is not possible — \" +\n 'either enable SQLite or drop the `log: () => durableObjectCursorLog()` option ' +\n '(snapshot-on-reconnect semantics).',\n );\n }\n log._initialize(sql);\n }\n }\n\n if (container.has(LIVE_DRIVER)) {\n const driver = container.resolve(LIVE_DRIVER);\n if (driver instanceof CfLiveDriver) driver._setLocalMode();\n }\n}\n\n/**\n * DO-side wiring after lifecycle, called from `buildDoRuntime`: replay every hibernation-persisted\n * subscription into the (fresh) engine so an eviction is invisible to\n * subscribers. Returns the engine for the `invalidate` RPC, or undefined when\n * the app doesn't use LiveModule.\n */\nexport function initDoLive(app: EntrypointsApp, ctx: DoStateLike): LiveEngine | undefined {\n const entry = app.entrypoints.ofKind('live')[0];\n if (!entry) return undefined;\n if (typeof entry.meta !== 'object' || entry.meta === null || !('engine' in entry.meta)) {\n throw new Error('Invalid live entrypoint metadata.');\n }\n const engine = entry.meta.engine;\n if (!(engine instanceof LiveEngine)) throw new Error('Invalid live entrypoint engine.');\n\n // Wake-time replay: subscriptions ride the hibernation attachments.\n for (const ws of ctx.getWebSockets()) {\n const client = new CfWsClient(ctx, ws);\n for (const record of readPersistedLiveSubscriptions(client)) {\n engine.restoreSubscription(client.path, client, record);\n }\n }\n\n return engine;\n}\n\n/** Ergonomic alias: the log option for `LiveModule.forRoot` on Cloudflare. */\nexport function durableObjectCursorLog(maxRows?: number): DoCursorLog {\n return new DoCursorLog(maxRows);\n}\n\n/**\n * Invalidate live tags in a room from a Worker (controller / cron / queue\n * consumer) — the live sibling of `broadcastToRoom`. Returns the room log\n * scope's commit stamp for `Vela-Commit-Cursor` stamping.\n */\nexport async function liveInvalidateToRoom(\n ns: LiveNamespace,\n gatewayPath: string,\n room: string,\n tags: string[],\n): Promise<CommitStamp | undefined> {\n const stub = ns.get(roomToDurableId(ns, gatewayPath, room));\n return stub.invalidate({ room, tags });\n}\n","const encoder = new TextEncoder();\nexport const MAX_NONCE_BYTES = 512;\nexport function isCanonicalBoundedText(value: unknown, maxBytes: number): value is string {\n if (typeof value !== 'string') return false;\n for (const character of value) {\n const codePoint = character.codePointAt(0);\n if (\n codePoint !== undefined &&\n (codePoint <= 0x1f || codePoint === 0x7f || (codePoint >= 0xd800 && codePoint <= 0xdfff))\n ) {\n return false;\n }\n }\n return value.length > 0 && value === value.trim() && encoder.encode(value).byteLength <= maxBytes;\n}\n\nexport function isValidExpiry(\n expEpochSeconds: unknown,\n nowEpochSeconds: number,\n): expEpochSeconds is number {\n return (\n typeof expEpochSeconds === 'number' &&\n Number.isSafeInteger(expEpochSeconds) &&\n expEpochSeconds > 0 &&\n expEpochSeconds >= nowEpochSeconds\n );\n}\n"],"mappings":"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"}
|