@velajs/cloudflare 1.10.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 -41
- package/dist/index.js +1645 -39
- package/dist/index.js.map +1 -0
- package/package.json +54 -40
- 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 -107
- 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 -24
- package/dist/websocket/do-bootstrap.js +0 -55
- package/dist/websocket/do-live.d.ts +0 -81
- package/dist/websocket/do-live.js +0 -197
- package/dist/websocket/do-state.d.ts +0 -35
- 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 -15
- package/dist/websocket/index.js +0 -15
- 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 -83
- package/dist/websocket/ws-server-holder.d.ts +0 -16
- package/dist/websocket/ws-server-holder.js +0 -34
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { LIVE_CURSOR_LOG, LIVE_DRIVER, readPersistedLiveSubscriptions } from "@velajs/vela/live";
|
|
2
|
-
import { CfWsClient } from "./cf-ws-client.js";
|
|
3
|
-
import { roomToDurableId } from "./room-id.js";
|
|
4
|
-
const DEFAULT_ROOM = 'default';
|
|
5
|
-
const DEFAULT_MAX_LOG_ROWS = 4096;
|
|
6
|
-
/**
|
|
7
|
-
* The durable `CursorLog`: an append-only tag-invalidation log in the DO's
|
|
8
|
-
* SQLite (`__vela_live_log`, AUTOINCREMENT seq = cursor) plus an epoch UUID in
|
|
9
|
-
* `__vela_live_meta`. Because the cursor survives hibernation AND trims (it is
|
|
10
|
-
* read from `sqlite_sequence`, lunora's `ctx-db-cdc.ts` trick), a reconnecting
|
|
11
|
-
* client whose gap the log still covers gets a tiny `resume` instead of a
|
|
12
|
-
* re-run — the real-resume half of the live protocol.
|
|
13
|
-
*
|
|
14
|
-
* Constructed un-initialized at module-composition time (the same app module
|
|
15
|
-
* bootstraps in the Worker AND in each DO); `initDoLive` wires the SQLite
|
|
16
|
-
* handle inside the DO. In the Worker isolate it stays un-initialized — and is
|
|
17
|
-
* never consulted there, because `durableObjectLive()` routes every
|
|
18
|
-
* invalidation to the room DO's log (one log scope per room, exactly the
|
|
19
|
-
* protocol's model).
|
|
20
|
-
*/ export class DoCursorLog {
|
|
21
|
-
maxRows;
|
|
22
|
-
sql;
|
|
23
|
-
epoch;
|
|
24
|
-
constructor(maxRows = DEFAULT_MAX_LOG_ROWS){
|
|
25
|
-
this.maxRows = maxRows;
|
|
26
|
-
}
|
|
27
|
-
/** @internal — called by `initDoLive` with the DO's `ctx.storage.sql`. */ _initialize(sql) {
|
|
28
|
-
this.sql = sql;
|
|
29
|
-
sql.exec('CREATE TABLE IF NOT EXISTS __vela_live_log (seq INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, tags TEXT NOT NULL)');
|
|
30
|
-
sql.exec('CREATE TABLE IF NOT EXISTS __vela_live_meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)');
|
|
31
|
-
const row = sql.exec("SELECT v FROM __vela_live_meta WHERE k = 'epoch'").toArray()[0];
|
|
32
|
-
if (row && typeof row.v === 'string') {
|
|
33
|
-
this.epoch = row.v;
|
|
34
|
-
} else {
|
|
35
|
-
this.epoch = crypto.randomUUID();
|
|
36
|
-
sql.exec("INSERT INTO __vela_live_meta (k, v) VALUES ('epoch', ?)", this.epoch);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
append(tags) {
|
|
40
|
-
const sql = this.assertReady();
|
|
41
|
-
sql.exec('INSERT INTO __vela_live_log (ts, tags) VALUES (?, ?)', Date.now(), JSON.stringify(tags));
|
|
42
|
-
const stamp = this.current();
|
|
43
|
-
// Bounded retention: trimmed gaps degrade to snapshot-on-reconnect.
|
|
44
|
-
if (stamp.cursor > this.maxRows) {
|
|
45
|
-
sql.exec('DELETE FROM __vela_live_log WHERE seq <= ?', stamp.cursor - this.maxRows);
|
|
46
|
-
}
|
|
47
|
-
return stamp;
|
|
48
|
-
}
|
|
49
|
-
current() {
|
|
50
|
-
const sql = this.assertReady();
|
|
51
|
-
// sqlite_sequence survives DELETE-based trims, so the cursor never
|
|
52
|
-
// rewinds. The table itself only materializes on the first AUTOINCREMENT
|
|
53
|
-
// insert — before that the log is empty and the cursor is 0.
|
|
54
|
-
let cursor = 0;
|
|
55
|
-
try {
|
|
56
|
-
const row = sql.exec("SELECT seq FROM sqlite_sequence WHERE name = '__vela_live_log'").toArray()[0];
|
|
57
|
-
cursor = typeof row?.seq === 'number' ? row.seq : Number(row?.seq ?? 0);
|
|
58
|
-
} catch {
|
|
59
|
-
cursor = 0;
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
cursor,
|
|
63
|
-
epoch: this.epoch
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
evaluateResume(sinceCursor, sinceEpoch, subscriptionTags) {
|
|
67
|
-
const sql = this.assertReady();
|
|
68
|
-
const { cursor, epoch } = this.current();
|
|
69
|
-
if (sinceEpoch !== epoch) return 'snapshot'; // forked timeline (reset/recreated DO)
|
|
70
|
-
if (sinceCursor > cursor) return 'snapshot'; // rollback guard
|
|
71
|
-
if (sinceCursor === cursor) return 'resume';
|
|
72
|
-
const minRow = sql.exec('SELECT MIN(seq) AS m FROM __vela_live_log').toArray()[0];
|
|
73
|
-
const min = minRow?.m == null ? undefined : Number(minRow.m);
|
|
74
|
-
// The log must still cover (sinceCursor, cursor] — a trimmed gap cannot be reasoned about.
|
|
75
|
-
if (min === undefined || min > sinceCursor + 1) return 'snapshot';
|
|
76
|
-
const subTags = new Set(subscriptionTags);
|
|
77
|
-
for (const row of sql.exec('SELECT tags FROM __vela_live_log WHERE seq > ?', sinceCursor).toArray()){
|
|
78
|
-
let tags;
|
|
79
|
-
try {
|
|
80
|
-
tags = JSON.parse(String(row.tags));
|
|
81
|
-
} catch {
|
|
82
|
-
return 'snapshot';
|
|
83
|
-
}
|
|
84
|
-
if (Array.isArray(tags) && tags.some((tag)=>subTags.has(tag))) return 'rerun';
|
|
85
|
-
}
|
|
86
|
-
return 'resume';
|
|
87
|
-
}
|
|
88
|
-
assertReady() {
|
|
89
|
-
if (!this.sql) {
|
|
90
|
-
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.");
|
|
91
|
-
}
|
|
92
|
-
return this.sql;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* The Cloudflare `LiveDriver`. Dual-mode, because the SAME app module
|
|
97
|
-
* bootstraps in both isolates:
|
|
98
|
-
*
|
|
99
|
-
* - **Worker** (HTTP mutations, queue consumers, crons): route the command to
|
|
100
|
-
* the room's Durable Object over the `invalidate` RPC — the same canonical
|
|
101
|
-
* `roomToDurableId` mapping the upgrade route and `broadcastToRoom` use —
|
|
102
|
-
* and return THAT log scope's commit stamp (what `Vela-Commit-Cursor`
|
|
103
|
-
* must carry).
|
|
104
|
-
* - **DO** (writes issued from inside the object): apply to the local engine.
|
|
105
|
-
*/ export function durableObjectLive(options) {
|
|
106
|
-
let sink;
|
|
107
|
-
let env;
|
|
108
|
-
let localMode = false;
|
|
109
|
-
return {
|
|
110
|
-
kind: 'durable-object',
|
|
111
|
-
bind (boundSink) {
|
|
112
|
-
sink = boundSink;
|
|
113
|
-
},
|
|
114
|
-
_initializeEnv (capturedEnv) {
|
|
115
|
-
env = capturedEnv;
|
|
116
|
-
},
|
|
117
|
-
_setLocalMode () {
|
|
118
|
-
localMode = true;
|
|
119
|
-
},
|
|
120
|
-
dispatch (cmd) {
|
|
121
|
-
if (localMode) return sink?.applyInvalidation(cmd);
|
|
122
|
-
const namespace = env?.[options.binding];
|
|
123
|
-
if (!namespace) {
|
|
124
|
-
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.');
|
|
125
|
-
}
|
|
126
|
-
const room = cmd.room ?? options.defaultRoom ?? DEFAULT_ROOM;
|
|
127
|
-
const stub = namespace.get(roomToDurableId(namespace, room));
|
|
128
|
-
return stub.invalidate({
|
|
129
|
-
...cmd,
|
|
130
|
-
room
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
const isCfLiveDriver = (value)=>typeof value === 'object' && value !== null && typeof value._setLocalMode === 'function' && typeof value._initializeEnv === 'function';
|
|
136
|
-
/** Worker-side wiring, called from `cloudflareAdapter`'s first-request middleware. */ export function initializeWorkerLive(container, env) {
|
|
137
|
-
let driver;
|
|
138
|
-
try {
|
|
139
|
-
driver = container.resolve(LIVE_DRIVER);
|
|
140
|
-
} catch {
|
|
141
|
-
return; // LiveModule not imported
|
|
142
|
-
}
|
|
143
|
-
if (isCfLiveDriver(driver)) driver._initializeEnv(env);
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* DO-side wiring, called from `buildDoRuntime`: initialize the SQLite cursor
|
|
147
|
-
* log, flip the driver to local mode, and replay every hibernation-persisted
|
|
148
|
-
* subscription into the (fresh) engine so an eviction is invisible to
|
|
149
|
-
* subscribers. Returns the engine for the `invalidate` RPC, or undefined when
|
|
150
|
-
* the app doesn't use LiveModule.
|
|
151
|
-
*/ export function initDoLive(app, container, ctx) {
|
|
152
|
-
const entry = app.entrypoints.ofKind('live')[0];
|
|
153
|
-
if (!entry) return undefined;
|
|
154
|
-
const engine = entry.meta.engine;
|
|
155
|
-
try {
|
|
156
|
-
const log = container.resolve(LIVE_CURSOR_LOG);
|
|
157
|
-
if (log instanceof DoCursorLog) {
|
|
158
|
-
const sql = ctx.storage?.sql;
|
|
159
|
-
if (!sql) {
|
|
160
|
-
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).');
|
|
161
|
-
}
|
|
162
|
-
log._initialize(sql);
|
|
163
|
-
}
|
|
164
|
-
} catch (err) {
|
|
165
|
-
// Surface misconfiguration loudly — a silently un-initialized log would
|
|
166
|
-
// throw on the first subscribe instead.
|
|
167
|
-
if (err instanceof Error && err.message.includes('new_sqlite_classes')) throw err;
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
const driver = container.resolve(LIVE_DRIVER);
|
|
171
|
-
if (isCfLiveDriver(driver)) driver._setLocalMode();
|
|
172
|
-
} catch {
|
|
173
|
-
// LiveModule always provides LIVE_DRIVER when the engine exists; defensive only.
|
|
174
|
-
}
|
|
175
|
-
// Wake-time replay: subscriptions ride the hibernation attachments.
|
|
176
|
-
for (const ws of ctx.getWebSockets()){
|
|
177
|
-
const client = new CfWsClient(ctx, ws);
|
|
178
|
-
for (const record of readPersistedLiveSubscriptions(client)){
|
|
179
|
-
engine.restoreSubscription(client.path, client, record);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return engine;
|
|
183
|
-
}
|
|
184
|
-
/** Ergonomic alias: the log option for `LiveModule.forRoot` on Cloudflare. */ export function durableObjectCursorLog(maxRows) {
|
|
185
|
-
return new DoCursorLog(maxRows);
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Invalidate live tags in a room from a Worker (controller / cron / queue
|
|
189
|
-
* consumer) — the live sibling of `broadcastToRoom`. Returns the room log
|
|
190
|
-
* scope's commit stamp for `Vela-Commit-Cursor` stamping.
|
|
191
|
-
*/ export async function liveInvalidateToRoom(ns, room, tags) {
|
|
192
|
-
const stub = ns.get(roomToDurableId(ns, room));
|
|
193
|
-
return stub.invalidate({
|
|
194
|
-
room,
|
|
195
|
-
tags
|
|
196
|
-
});
|
|
197
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export interface WsLike {
|
|
2
|
-
send(message: string | ArrayBuffer): void;
|
|
3
|
-
close(code?: number, reason?: string): void;
|
|
4
|
-
serializeAttachment(value: unknown): void;
|
|
5
|
-
deserializeAttachment(): unknown;
|
|
6
|
-
}
|
|
7
|
-
/** Structural view of the DO's SQLite handle (`ctx.storage.sql`, requires `new_sqlite_classes`). */
|
|
8
|
-
export interface SqlStorageLike {
|
|
9
|
-
exec(query: string, ...bindings: unknown[]): {
|
|
10
|
-
toArray(): Record<string, unknown>[];
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
export interface DoStateLike {
|
|
14
|
-
readonly id: {
|
|
15
|
-
toString(): string;
|
|
16
|
-
readonly name?: string | null;
|
|
17
|
-
};
|
|
18
|
-
acceptWebSocket(ws: WsLike, tags?: string[]): void;
|
|
19
|
-
getWebSockets(tag?: string): WsLike[];
|
|
20
|
-
setWebSocketAutoResponse?(pair: unknown): void;
|
|
21
|
-
/** Present on SQLite-backed DOs — the live cursor log lives here. */
|
|
22
|
-
readonly storage?: {
|
|
23
|
-
sql?: SqlStorageLike;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/** Per-connection metadata persisted in the hibernation attachment (≤ 16 KiB). */
|
|
27
|
-
export interface WsAttachment {
|
|
28
|
-
connId: string;
|
|
29
|
-
userId?: string;
|
|
30
|
-
/** The gateway route path this socket belongs to — used to route messages. */
|
|
31
|
-
path: string;
|
|
32
|
-
/** Dynamically-joined room names (the hub room is also a hibernation tag). */
|
|
33
|
-
rooms: string[];
|
|
34
|
-
data: Record<string, unknown>;
|
|
35
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
// Minimal structural views of the Durable Object runtime, so the transport
|
|
2
|
-
// logic is unit-testable in Node with fakes. Real `DurableObjectState` and
|
|
3
|
-
// `WebSocket` (from @cloudflare/workers-types) satisfy these structurally.
|
|
4
|
-
/** Per-connection metadata persisted in the hibernation attachment (≤ 16 KiB). */ export { };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { BroadcastCommand, WsDispatcher } from '@velajs/vela/websocket';
|
|
2
|
-
import type { CfRoomRegistry } from './cf-room-registry';
|
|
3
|
-
import type { DoStateLike, WsLike } from './do-state';
|
|
4
|
-
/**
|
|
5
|
-
* The socket lifecycle inside a WebSocket Durable Object, decoupled from the
|
|
6
|
-
* `cloudflare:workers` base class so it is unit-testable with fakes. The thin
|
|
7
|
-
* `VelaWebSocketDurableObject` shell forwards its hibernation callbacks here.
|
|
8
|
-
*/
|
|
9
|
-
export declare class DoWebSocketHost {
|
|
10
|
-
private readonly ctx;
|
|
11
|
-
private readonly dispatcher;
|
|
12
|
-
private readonly registry;
|
|
13
|
-
private readonly gatewayPaths;
|
|
14
|
-
constructor(ctx: DoStateLike, dispatcher: WsDispatcher, registry: CfRoomRegistry, gatewayPaths?: readonly string[]);
|
|
15
|
-
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */
|
|
16
|
-
defaultPath(): string | undefined;
|
|
17
|
-
/**
|
|
18
|
-
* Accept a hibernatable socket: tag it with its hub room + connection id,
|
|
19
|
-
* persist the attachment, then fire `OnGatewayConnection` WITHOUT blocking the
|
|
20
|
-
* 101 response (the caller returns it immediately).
|
|
21
|
-
*/
|
|
22
|
-
accept(ws: WsLike, path: string, roomId: string, userId?: string): void;
|
|
23
|
-
onMessage(ws: WsLike, message: string | ArrayBuffer): Promise<void>;
|
|
24
|
-
onClose(ws: WsLike, code: number, reason: string): Promise<void>;
|
|
25
|
-
onError(ws: WsLike, err: unknown): Promise<void>;
|
|
26
|
-
/** Deliver a broadcast command to this DO's local sockets (RPC entry point). */
|
|
27
|
-
broadcast(cmd: BroadcastCommand): void;
|
|
28
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { CfWsClient } from "./cf-ws-client.js";
|
|
2
|
-
import { connTag, roomTag } from "./room-id.js";
|
|
3
|
-
/**
|
|
4
|
-
* The socket lifecycle inside a WebSocket Durable Object, decoupled from the
|
|
5
|
-
* `cloudflare:workers` base class so it is unit-testable with fakes. The thin
|
|
6
|
-
* `VelaWebSocketDurableObject` shell forwards its hibernation callbacks here.
|
|
7
|
-
*/ export class DoWebSocketHost {
|
|
8
|
-
ctx;
|
|
9
|
-
dispatcher;
|
|
10
|
-
registry;
|
|
11
|
-
gatewayPaths;
|
|
12
|
-
constructor(ctx, dispatcher, registry, gatewayPaths = []){
|
|
13
|
-
this.ctx = ctx;
|
|
14
|
-
this.dispatcher = dispatcher;
|
|
15
|
-
this.registry = registry;
|
|
16
|
-
this.gatewayPaths = gatewayPaths;
|
|
17
|
-
}
|
|
18
|
-
/** The single registered gateway's path — a fallback when the Worker didn't forward `x-vela-path`. */ defaultPath() {
|
|
19
|
-
return this.gatewayPaths[0];
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Accept a hibernatable socket: tag it with its hub room + connection id,
|
|
23
|
-
* persist the attachment, then fire `OnGatewayConnection` WITHOUT blocking the
|
|
24
|
-
* 101 response (the caller returns it immediately).
|
|
25
|
-
*/ accept(ws, path, roomId, userId) {
|
|
26
|
-
const connId = crypto.randomUUID();
|
|
27
|
-
this.ctx.acceptWebSocket(ws, [
|
|
28
|
-
roomTag(roomId),
|
|
29
|
-
connTag(connId)
|
|
30
|
-
]);
|
|
31
|
-
const attachment = {
|
|
32
|
-
connId,
|
|
33
|
-
userId,
|
|
34
|
-
path,
|
|
35
|
-
rooms: [
|
|
36
|
-
roomId
|
|
37
|
-
],
|
|
38
|
-
data: userId ? {
|
|
39
|
-
userId
|
|
40
|
-
} : {}
|
|
41
|
-
};
|
|
42
|
-
ws.serializeAttachment(attachment);
|
|
43
|
-
void Promise.resolve(this.dispatcher.handleOpen(path, new CfWsClient(this.ctx, ws))).catch((err)=>console.warn('[vela] websocket handleConnection failed:', err));
|
|
44
|
-
}
|
|
45
|
-
async onMessage(ws, message) {
|
|
46
|
-
const client = new CfWsClient(this.ctx, ws);
|
|
47
|
-
await this.dispatcher.dispatchMessage(client.path, client, message);
|
|
48
|
-
}
|
|
49
|
-
async onClose(ws, code, reason) {
|
|
50
|
-
const client = new CfWsClient(this.ctx, ws);
|
|
51
|
-
await this.dispatcher.handleClose(client.path, client, code, reason);
|
|
52
|
-
// Complete the closing handshake. Only 1000 and 3000-4999 are valid for
|
|
53
|
-
// close(); reserved/abnormal codes (1005/1006/1015, etc.) throw a RangeError,
|
|
54
|
-
// so fall back to a codeless close on those.
|
|
55
|
-
try {
|
|
56
|
-
if (code === 1000 || code >= 3000 && code <= 4999) ws.close(code, reason);
|
|
57
|
-
else ws.close();
|
|
58
|
-
} catch {
|
|
59
|
-
// socket already closed
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async onError(ws, err) {
|
|
63
|
-
const client = new CfWsClient(this.ctx, ws);
|
|
64
|
-
await this.dispatcher.handleError(client.path, client, err);
|
|
65
|
-
}
|
|
66
|
-
/** Deliver a broadcast command to this DO's local sockets (RPC entry point). */ broadcast(cmd) {
|
|
67
|
-
this.registry.deliverLocal(cmd);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export { VelaWebSocketDurableObject } from './websocket.durable-object';
|
|
2
|
-
export { CloudflareWebSocketModule } from './cloudflare-websocket.module';
|
|
3
|
-
export { broadcastToRoom } from './broadcast';
|
|
4
|
-
export { DoCursorLog, durableObjectCursorLog, durableObjectLive, initDoLive, initializeWorkerLive, liveInvalidateToRoom, } from './do-live';
|
|
5
|
-
export type { CfLiveDriver, DurableObjectLiveOptions } from './do-live';
|
|
6
|
-
export { CfWsClient } from './cf-ws-client';
|
|
7
|
-
export { CfRoomRegistry } from './cf-room-registry';
|
|
8
|
-
export { DoWebSocketHost } from './do-websocket-host';
|
|
9
|
-
export { WsServerHolder } from './ws-server-holder';
|
|
10
|
-
export { buildDoRuntime } from './do-bootstrap';
|
|
11
|
-
export type { DoRuntime } from './do-bootstrap';
|
|
12
|
-
export { registerWebSocketRoutes, collectWsGatewayRoutes } from './websocket-routing';
|
|
13
|
-
export type { WsGatewayRoute } from './websocket-routing';
|
|
14
|
-
export { roomTag, connTag, roomToDurableId } from './room-id';
|
|
15
|
-
export type { DoStateLike, SqlStorageLike, WsLike, WsAttachment } from './do-state';
|
package/dist/websocket/index.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// DO base class (imports `cloudflare:workers` — only runs in workerd).
|
|
2
|
-
export { VelaWebSocketDurableObject } from "./websocket.durable-object.js";
|
|
3
|
-
// Module + server-initiated emit helper
|
|
4
|
-
export { CloudflareWebSocketModule } from "./cloudflare-websocket.module.js";
|
|
5
|
-
export { broadcastToRoom } from "./broadcast.js";
|
|
6
|
-
// Live queries: durable cursor log + DO-routed invalidation driver
|
|
7
|
-
export { DoCursorLog, durableObjectCursorLog, durableObjectLive, initDoLive, initializeWorkerLive, liveInvalidateToRoom } from "./do-live.js";
|
|
8
|
-
// Transport internals (advanced use / testing)
|
|
9
|
-
export { CfWsClient } from "./cf-ws-client.js";
|
|
10
|
-
export { CfRoomRegistry } from "./cf-room-registry.js";
|
|
11
|
-
export { DoWebSocketHost } from "./do-websocket-host.js";
|
|
12
|
-
export { WsServerHolder } from "./ws-server-holder.js";
|
|
13
|
-
export { buildDoRuntime } from "./do-bootstrap.js";
|
|
14
|
-
export { registerWebSocketRoutes, collectWsGatewayRoutes } from "./websocket-routing.js";
|
|
15
|
-
export { roomTag, connTag, roomToDurableId } from "./room-id.js";
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */
|
|
2
|
-
export declare function roomTag(roomId: string): string;
|
|
3
|
-
/** Hibernation tag addressing one connection directly. */
|
|
4
|
-
export declare function connTag(connId: string): string;
|
|
5
|
-
/** One Durable Object instance per room, addressed by name. */
|
|
6
|
-
export declare function roomToDurableId(ns: DurableObjectNamespace, roomId: string): DurableObjectId;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Canonical room ↔ Durable Object mappings. The SAME functions are used by the
|
|
2
|
-
// Worker upgrade route (to pick the DO) and by every server-initiated emit — so
|
|
3
|
-
// a connection and a later broadcast always resolve to the same DO instance.
|
|
4
|
-
/** Hibernation tag marking a socket's hub room (set at accept; immutable after). */ export function roomTag(roomId) {
|
|
5
|
-
return `room:${roomId}`;
|
|
6
|
-
}
|
|
7
|
-
/** Hibernation tag addressing one connection directly. */ export function connTag(connId) {
|
|
8
|
-
return `conn:${connId}`;
|
|
9
|
-
}
|
|
10
|
-
/** One Durable Object instance per room, addressed by name. */ export function roomToDurableId(ns, roomId) {
|
|
11
|
-
return ns.idFromName(roomId);
|
|
12
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Hono } from 'hono';
|
|
2
|
-
export interface WsGatewayRoute {
|
|
3
|
-
path: string;
|
|
4
|
-
binding: string;
|
|
5
|
-
}
|
|
6
|
-
/** Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted gateways only). */
|
|
7
|
-
export declare function collectWsGatewayRoutes(instance: object): WsGatewayRoute[];
|
|
8
|
-
/**
|
|
9
|
-
* Registers the upgrade routes on the Worker's Hono app. Each route validates
|
|
10
|
-
* the `Upgrade` header, resolves the room's Durable Object, and forwards the raw
|
|
11
|
-
* request — injecting spoof-safe `x-vela-*` headers the DO reads. The DO returns
|
|
12
|
-
* the `101` with the client socket.
|
|
13
|
-
*/
|
|
14
|
-
export declare function registerWebSocketRoutes(hono: Hono, routes: WsGatewayRoute[]): void;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { getMetadata } from "@velajs/vela";
|
|
2
|
-
import { WS_GATEWAY_METADATA } from "@velajs/vela/websocket";
|
|
3
|
-
import { roomToDurableId } from "./room-id.js";
|
|
4
|
-
/** Read `@WebSocketGateway({ path, binding })` off a resolved instance (CF-hosted gateways only). */ export function collectWsGatewayRoutes(instance) {
|
|
5
|
-
const options = getMetadata(WS_GATEWAY_METADATA, instance.constructor);
|
|
6
|
-
if (!options?.path || !options?.binding) return [];
|
|
7
|
-
return [
|
|
8
|
-
{
|
|
9
|
-
path: options.path,
|
|
10
|
-
binding: options.binding
|
|
11
|
-
}
|
|
12
|
-
];
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Registers the upgrade routes on the Worker's Hono app. Each route validates
|
|
16
|
-
* the `Upgrade` header, resolves the room's Durable Object, and forwards the raw
|
|
17
|
-
* request — injecting spoof-safe `x-vela-*` headers the DO reads. The DO returns
|
|
18
|
-
* the `101` with the client socket.
|
|
19
|
-
*/ export function registerWebSocketRoutes(hono, routes) {
|
|
20
|
-
for (const route of routes){
|
|
21
|
-
hono.get(route.path, async (c)=>{
|
|
22
|
-
if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {
|
|
23
|
-
return c.text('Expected WebSocket upgrade', 426);
|
|
24
|
-
}
|
|
25
|
-
const ns = c.env[route.binding];
|
|
26
|
-
if (!ns) {
|
|
27
|
-
return c.text(`Durable Object binding '${route.binding}' is not configured`, 500);
|
|
28
|
-
}
|
|
29
|
-
const roomId = c.req.param('id') ?? route.path;
|
|
30
|
-
const stub = ns.get(roomToDurableId(ns, roomId));
|
|
31
|
-
// Strip any client-supplied x-vela-* (anti-spoof), then set server values.
|
|
32
|
-
const headers = new Headers(c.req.raw.headers);
|
|
33
|
-
headers.delete('x-vela-room');
|
|
34
|
-
headers.delete('x-vela-path');
|
|
35
|
-
headers.delete('x-vela-user');
|
|
36
|
-
headers.set('x-vela-room', roomId);
|
|
37
|
-
headers.set('x-vela-path', route.path);
|
|
38
|
-
const userId = c.get('userId');
|
|
39
|
-
if (userId) headers.set('x-vela-user', String(userId));
|
|
40
|
-
return stub.fetch(new Request(c.req.raw, {
|
|
41
|
-
headers
|
|
42
|
-
}));
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
-
import type { Type } from '@velajs/vela';
|
|
3
|
-
/**
|
|
4
|
-
* Base class for the WebSocket Durable Object. The user exports a named subclass
|
|
5
|
-
* (matching their `wrangler.toml` `class_name`) built from their `AppModule`:
|
|
6
|
-
*
|
|
7
|
-
* ```ts
|
|
8
|
-
* export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
|
|
9
|
-
* ```
|
|
10
|
-
*
|
|
11
|
-
* It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
|
|
12
|
-
* bridge DO hibernation) and forwards every event into the runtime-agnostic
|
|
13
|
-
* `WsDispatcher` via {@link DoWebSocketHost}.
|
|
14
|
-
*/
|
|
15
|
-
export declare function VelaWebSocketDurableObject(rootModule: Type): new (ctx: DurableObjectState, env: Record<string, unknown>) => DurableObject<Record<string, unknown>>;
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { DurableObject } from "cloudflare:workers";
|
|
2
|
-
import { buildDoRuntime } from "./do-bootstrap.js";
|
|
3
|
-
import { DoWebSocketHost } from "./do-websocket-host.js";
|
|
4
|
-
const PING = '{"event":"ping"}';
|
|
5
|
-
const PONG = '{"event":"pong"}';
|
|
6
|
-
/**
|
|
7
|
-
* Base class for the WebSocket Durable Object. The user exports a named subclass
|
|
8
|
-
* (matching their `wrangler.toml` `class_name`) built from their `AppModule`:
|
|
9
|
-
*
|
|
10
|
-
* ```ts
|
|
11
|
-
* export class ChatRoom extends VelaWebSocketDurableObject(AppModule) {}
|
|
12
|
-
* ```
|
|
13
|
-
*
|
|
14
|
-
* It owns the raw hibernation socket lifecycle (Hono's `upgradeWebSocket` cannot
|
|
15
|
-
* bridge DO hibernation) and forwards every event into the runtime-agnostic
|
|
16
|
-
* `WsDispatcher` via {@link DoWebSocketHost}.
|
|
17
|
-
*/ export function VelaWebSocketDurableObject(rootModule) {
|
|
18
|
-
return class VelaWsDurableObject extends DurableObject {
|
|
19
|
-
host;
|
|
20
|
-
liveEngine;
|
|
21
|
-
ready;
|
|
22
|
-
constructor(ctx, env){
|
|
23
|
-
super(ctx, env);
|
|
24
|
-
// Application-level ping/pong answered WITHOUT waking a hibernated DO.
|
|
25
|
-
try {
|
|
26
|
-
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));
|
|
27
|
-
} catch {
|
|
28
|
-
// Older runtimes without auto-response — fine, protocol pings still work.
|
|
29
|
-
}
|
|
30
|
-
this.ready = ctx.blockConcurrencyWhile(async ()=>{
|
|
31
|
-
const runtime = await buildDoRuntime(rootModule, ctx, env);
|
|
32
|
-
this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
|
|
33
|
-
this.liveEngine = runtime.live;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
async fetch(request) {
|
|
37
|
-
await this.ready;
|
|
38
|
-
if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
|
|
39
|
-
return new Response('Expected WebSocket upgrade', {
|
|
40
|
-
status: 426
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
const { 0: client, 1: server } = new WebSocketPair();
|
|
44
|
-
const url = new URL(request.url);
|
|
45
|
-
// Headers are primary (carry auth + multi-gateway routing); fall back to the
|
|
46
|
-
// DO's own name (set via idFromName(room)) and the single gateway path so a
|
|
47
|
-
// plain `stub.fetch(request)` forward still works.
|
|
48
|
-
const roomId = request.headers.get('x-vela-room') ?? this.ctx.id.name ?? url.pathname;
|
|
49
|
-
const path = request.headers.get('x-vela-path') ?? this.host.defaultPath() ?? url.pathname;
|
|
50
|
-
const userId = request.headers.get('x-vela-user') || undefined;
|
|
51
|
-
this.host.accept(server, path, roomId, userId);
|
|
52
|
-
return new Response(null, {
|
|
53
|
-
status: 101,
|
|
54
|
-
webSocket: client
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
async webSocketMessage(ws, message) {
|
|
58
|
-
await this.ready;
|
|
59
|
-
await this.host.onMessage(ws, message);
|
|
60
|
-
}
|
|
61
|
-
async webSocketClose(ws, code, reason) {
|
|
62
|
-
await this.ready;
|
|
63
|
-
await this.host.onClose(ws, code, reason);
|
|
64
|
-
}
|
|
65
|
-
async webSocketError(ws, error) {
|
|
66
|
-
await this.ready;
|
|
67
|
-
await this.host.onError(ws, error);
|
|
68
|
-
}
|
|
69
|
-
/** DO RPC — server-initiated broadcast forwarded from a Worker (see `broadcastToRoom`). */ async broadcast(cmd) {
|
|
70
|
-
await this.ready;
|
|
71
|
-
this.host.broadcast(cmd);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* DO RPC — live tag invalidation forwarded from a Worker (durableObjectLive /
|
|
75
|
-
* liveInvalidateToRoom). Appends to THIS log scope's cursor log and returns
|
|
76
|
-
* the commit stamp (what `Vela-Commit-Cursor` carries); the subscription
|
|
77
|
-
* refreshes fan out asynchronously.
|
|
78
|
-
*/ async invalidate(cmd) {
|
|
79
|
-
await this.ready;
|
|
80
|
-
return this.liveEngine?.applyInvalidation(cmd);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { BroadcastOperator, WsServer } from '@velajs/vela/websocket';
|
|
2
|
-
/**
|
|
3
|
-
* Late-bound `WsServer`. Provided as `WS_SERVER` (one per DI container, i.e. per
|
|
4
|
-
* Durable Object instance), then pointed at the ctx-backed server once the DO
|
|
5
|
-
* builds. Gateways inject it via `@WebSocketServer()`; it throws if used before
|
|
6
|
-
* a runtime binds it (e.g. from the stateless Worker isolate).
|
|
7
|
-
*/
|
|
8
|
-
export declare class WsServerHolder implements WsServer {
|
|
9
|
-
private target?;
|
|
10
|
-
setTarget(server: WsServer): void;
|
|
11
|
-
private get resolved();
|
|
12
|
-
emit(event: string, data?: unknown): void | Promise<void>;
|
|
13
|
-
to(room: string): BroadcastOperator;
|
|
14
|
-
in(room: string): BroadcastOperator;
|
|
15
|
-
except(room: string): BroadcastOperator;
|
|
16
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
}
|
|
7
|
-
import { Injectable } from "@velajs/vela";
|
|
8
|
-
export class WsServerHolder {
|
|
9
|
-
target;
|
|
10
|
-
setTarget(server) {
|
|
11
|
-
this.target = server;
|
|
12
|
-
}
|
|
13
|
-
get resolved() {
|
|
14
|
-
if (!this.target) {
|
|
15
|
-
throw new Error('WebSocket server is only available inside a WebSocket Durable Object. To ' + 'push from a Worker HTTP handler, use broadcastToRoom(namespace, room, ...).');
|
|
16
|
-
}
|
|
17
|
-
return this.target;
|
|
18
|
-
}
|
|
19
|
-
emit(event, data) {
|
|
20
|
-
return this.resolved.emit(event, data);
|
|
21
|
-
}
|
|
22
|
-
to(room) {
|
|
23
|
-
return this.resolved.to(room);
|
|
24
|
-
}
|
|
25
|
-
in(room) {
|
|
26
|
-
return this.resolved.in(room);
|
|
27
|
-
}
|
|
28
|
-
except(room) {
|
|
29
|
-
return this.resolved.except(room);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
WsServerHolder = _ts_decorate([
|
|
33
|
-
Injectable()
|
|
34
|
-
], WsServerHolder);
|