@velajs/cloudflare 1.9.0 → 1.10.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.
@@ -2,6 +2,7 @@ import { VelaFactory } from "@velajs/vela";
2
2
  import { BindingRef } from "./binding-ref.js";
3
3
  import { CloudflareApplication } from "./cloudflare-application.js";
4
4
  import { EnvRef } from "./env-ref.js";
5
+ import { initializeWorkerLive } from "./websocket/do-live.js";
5
6
  import { registerWebSocketRoutes } from "./websocket/websocket-routing.js";
6
7
  // Walks every provider registered in the container and pulls out the
7
8
  // BindingRef instances. Replaces the old module-level `bindingsRegistry`
@@ -63,6 +64,7 @@ function collectBindingRefs(container) {
63
64
  */ export function cloudflareAdapter() {
64
65
  let initialized = false;
65
66
  let refs = [];
67
+ let liveContainer;
66
68
  return {
67
69
  name: 'cloudflare',
68
70
  requestMiddleware: [
@@ -75,11 +77,15 @@ function collectBindingRefs(container) {
75
77
  if (ref instanceof EnvRef) ref._initialize(env);
76
78
  else ref._initialize(env[ref.bindingName]);
77
79
  }
80
+ // Live queries: hand the durableObjectLive() driver its env so
81
+ // Worker-side invalidations can resolve the room DO namespace.
82
+ if (liveContainer) initializeWorkerLive(liveContainer, env);
78
83
  }
79
84
  await next();
80
85
  }
81
86
  ],
82
87
  onBootstrap: ({ container })=>{
88
+ liveContainer = container;
83
89
  refs = collectBindingRefs(container);
84
90
  }
85
91
  };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export { Scheduled } from './decorators/scheduled';
32
32
  export { QueueConsumer } from './decorators/queue-consumer';
33
33
  export { VelaWebSocketDurableObject, CloudflareWebSocketModule, broadcastToRoom, } from './websocket/index';
34
34
  export type { WsGatewayRoute } from './websocket/index';
35
+ export { DoCursorLog, durableObjectCursorLog, durableObjectLive, liveInvalidateToRoom, } from './websocket/index';
36
+ export type { CfLiveDriver, DurableObjectLiveOptions } from './websocket/index';
35
37
  export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException, } from '@velajs/vela/websocket';
36
38
  export type { WsClient, WsServer, WsResponse, WsMessage, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from '@velajs/vela/websocket';
37
39
  export type { CloudflareEnv, ScheduledRegistration, QueueRegistration } from './types';
package/dist/index.js CHANGED
@@ -33,5 +33,7 @@ export { Scheduled } from "./decorators/scheduled.js";
33
33
  export { QueueConsumer } from "./decorators/queue-consumer.js";
34
34
  // WebSocket (Durable Object transport for the Vela WebSocketModule)
35
35
  export { VelaWebSocketDurableObject, CloudflareWebSocketModule, broadcastToRoom } from "./websocket/index.js";
36
+ // Live queries (Durable Object transport for @velajs/vela/live)
37
+ export { DoCursorLog, durableObjectCursorLog, durableObjectLive, liveInvalidateToRoom } from "./websocket/index.js";
36
38
  // Re-export the core gateway API so a Cloudflare app can import it from one place.
37
39
  export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException } from "@velajs/vela/websocket";
@@ -1,6 +1,7 @@
1
1
  import type { Type } from '@velajs/vela';
2
2
  import { WsDispatcher } from '@velajs/vela/websocket';
3
3
  import type { WsServer } from '@velajs/vela/websocket';
4
+ import type { LiveEngine } from '@velajs/vela/live';
4
5
  import { CfRoomRegistry } from './cf-room-registry';
5
6
  import type { DoStateLike } from './do-state';
6
7
  export interface DoRuntime {
@@ -9,6 +10,8 @@ export interface DoRuntime {
9
10
  server: WsServer;
10
11
  /** Gateway paths from `app.entrypoints.ofKind('websocket')` (discovery order). */
11
12
  gatewayPaths: string[];
13
+ /** The live-query engine (undefined when the app doesn't import LiveModule). */
14
+ live?: LiveEngine;
12
15
  close(signal?: string): Promise<void>;
13
16
  }
14
17
  /**
@@ -3,6 +3,7 @@ import { local, WsDispatcher, WsServerImpl, WS_SERVER } from "@velajs/vela/webso
3
3
  import { BindingRef } from "../binding-ref.js";
4
4
  import { EnvRef } from "../env-ref.js";
5
5
  import { CfRoomRegistry } from "./cf-room-registry.js";
6
+ import { initDoLive } from "./do-live.js";
6
7
  import { WsServerHolder } from "./ws-server-holder.js";
7
8
  /**
8
9
  * Slim DI bootstrap for the Durable Object isolate: wires the container and runs
@@ -38,6 +39,9 @@ import { WsServerHolder } from "./ws-server-holder.js";
38
39
  // per discovered gateway ({ meta: { path, dispatcher } }). Built by
39
40
  // callOnApplicationBootstrap(), so this slim no-routes path has it too.
40
41
  const wsEntrypoints = app.entrypoints.ofKind('websocket');
42
+ // Live queries: wire the SQLite cursor log + local driver mode and replay
43
+ // hibernation-persisted subscriptions into the fresh engine.
44
+ const live = initDoLive(app, container, ctx);
41
45
  return {
42
46
  // Zero gateways still yields a live dispatcher (module imported, nothing
43
47
  // decorated) — fall back to resolving it directly.
@@ -45,6 +49,7 @@ import { WsServerHolder } from "./ws-server-holder.js";
45
49
  registry,
46
50
  server,
47
51
  gatewayPaths: wsEntrypoints.map((ep)=>ep.meta.path),
52
+ live,
48
53
  close: (signal)=>app.close(signal)
49
54
  };
50
55
  }
@@ -0,0 +1,81 @@
1
+ import type { Container } from '@velajs/vela';
2
+ import type { CommitStamp, CursorLog, LiveDriver, LiveEngine, ResumeVerdict } from '@velajs/vela/live';
3
+ import type { DoStateLike, SqlStorageLike } from './do-state';
4
+ /**
5
+ * The durable `CursorLog`: an append-only tag-invalidation log in the DO's
6
+ * SQLite (`__vela_live_log`, AUTOINCREMENT seq = cursor) plus an epoch UUID in
7
+ * `__vela_live_meta`. Because the cursor survives hibernation AND trims (it is
8
+ * read from `sqlite_sequence`, lunora's `ctx-db-cdc.ts` trick), a reconnecting
9
+ * client whose gap the log still covers gets a tiny `resume` instead of a
10
+ * re-run — the real-resume half of the live protocol.
11
+ *
12
+ * Constructed un-initialized at module-composition time (the same app module
13
+ * bootstraps in the Worker AND in each DO); `initDoLive` wires the SQLite
14
+ * handle inside the DO. In the Worker isolate it stays un-initialized — and is
15
+ * never consulted there, because `durableObjectLive()` routes every
16
+ * invalidation to the room DO's log (one log scope per room, exactly the
17
+ * protocol's model).
18
+ */
19
+ export declare class DoCursorLog implements CursorLog {
20
+ private readonly maxRows;
21
+ private sql?;
22
+ private epoch?;
23
+ constructor(maxRows?: number);
24
+ /** @internal — called by `initDoLive` with the DO's `ctx.storage.sql`. */
25
+ _initialize(sql: SqlStorageLike): void;
26
+ append(tags: string[]): CommitStamp;
27
+ current(): CommitStamp;
28
+ evaluateResume(sinceCursor: number, sinceEpoch: string, subscriptionTags: string[]): ResumeVerdict;
29
+ private assertReady;
30
+ }
31
+ export interface DurableObjectLiveOptions {
32
+ /** The wrangler binding name of the WebSocket DO namespace (e.g. `'CHAT_ROOM'`). */
33
+ binding: string;
34
+ /** Room used when an invalidation names none. Matches the client default. */
35
+ defaultRoom?: string;
36
+ }
37
+ export interface CfLiveDriver extends LiveDriver {
38
+ /** @internal — Worker isolate: capture `env` so the namespace binding resolves per dispatch. */
39
+ _initializeEnv(env: Record<string, unknown>): void;
40
+ /** @internal — DO isolate: deliver invalidations straight to this DO's engine. */
41
+ _setLocalMode(): void;
42
+ }
43
+ /**
44
+ * The Cloudflare `LiveDriver`. Dual-mode, because the SAME app module
45
+ * bootstraps in both isolates:
46
+ *
47
+ * - **Worker** (HTTP mutations, queue consumers, crons): route the command to
48
+ * the room's Durable Object over the `invalidate` RPC — the same canonical
49
+ * `roomToDurableId` mapping the upgrade route and `broadcastToRoom` use —
50
+ * and return THAT log scope's commit stamp (what `Vela-Commit-Cursor`
51
+ * must carry).
52
+ * - **DO** (writes issued from inside the object): apply to the local engine.
53
+ */
54
+ export declare function durableObjectLive(options: DurableObjectLiveOptions): CfLiveDriver;
55
+ /** Worker-side wiring, called from `cloudflareAdapter`'s first-request middleware. */
56
+ export declare function initializeWorkerLive(container: Container, env: Record<string, unknown>): void;
57
+ /** The app-facing surface of the engine reached through `app.entrypoints.ofKind('live')`. */
58
+ interface EntrypointsApp {
59
+ entrypoints: {
60
+ ofKind<M>(kind: string): Array<{
61
+ meta: M;
62
+ }>;
63
+ };
64
+ }
65
+ /**
66
+ * DO-side wiring, called from `buildDoRuntime`: initialize the SQLite cursor
67
+ * log, flip the driver to local mode, and replay every hibernation-persisted
68
+ * subscription into the (fresh) engine so an eviction is invisible to
69
+ * subscribers. Returns the engine for the `invalidate` RPC, or undefined when
70
+ * the app doesn't use LiveModule.
71
+ */
72
+ export declare function initDoLive(app: EntrypointsApp, container: Container, ctx: DoStateLike): LiveEngine | undefined;
73
+ /** Ergonomic alias: the log option for `LiveModule.forRoot` on Cloudflare. */
74
+ export declare function durableObjectCursorLog(maxRows?: number): DoCursorLog;
75
+ /**
76
+ * Invalidate live tags in a room from a Worker (controller / cron / queue
77
+ * consumer) — the live sibling of `broadcastToRoom`. Returns the room log
78
+ * scope's commit stamp for `Vela-Commit-Cursor` stamping.
79
+ */
80
+ export declare function liveInvalidateToRoom(ns: DurableObjectNamespace, room: string, tags: string[]): Promise<CommitStamp | undefined>;
81
+ export {};
@@ -0,0 +1,197 @@
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
+ }
@@ -4,6 +4,12 @@ export interface WsLike {
4
4
  serializeAttachment(value: unknown): void;
5
5
  deserializeAttachment(): unknown;
6
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
+ }
7
13
  export interface DoStateLike {
8
14
  readonly id: {
9
15
  toString(): string;
@@ -12,6 +18,10 @@ export interface DoStateLike {
12
18
  acceptWebSocket(ws: WsLike, tags?: string[]): void;
13
19
  getWebSockets(tag?: string): WsLike[];
14
20
  setWebSocketAutoResponse?(pair: unknown): void;
21
+ /** Present on SQLite-backed DOs — the live cursor log lives here. */
22
+ readonly storage?: {
23
+ sql?: SqlStorageLike;
24
+ };
15
25
  }
16
26
  /** Per-connection metadata persisted in the hibernation attachment (≤ 16 KiB). */
17
27
  export interface WsAttachment {
@@ -1,6 +1,8 @@
1
1
  export { VelaWebSocketDurableObject } from './websocket.durable-object';
2
2
  export { CloudflareWebSocketModule } from './cloudflare-websocket.module';
3
3
  export { broadcastToRoom } from './broadcast';
4
+ export { DoCursorLog, durableObjectCursorLog, durableObjectLive, initDoLive, initializeWorkerLive, liveInvalidateToRoom, } from './do-live';
5
+ export type { CfLiveDriver, DurableObjectLiveOptions } from './do-live';
4
6
  export { CfWsClient } from './cf-ws-client';
5
7
  export { CfRoomRegistry } from './cf-room-registry';
6
8
  export { DoWebSocketHost } from './do-websocket-host';
@@ -10,4 +12,4 @@ export type { DoRuntime } from './do-bootstrap';
10
12
  export { registerWebSocketRoutes, collectWsGatewayRoutes } from './websocket-routing';
11
13
  export type { WsGatewayRoute } from './websocket-routing';
12
14
  export { roomTag, connTag, roomToDurableId } from './room-id';
13
- export type { DoStateLike, WsLike, WsAttachment } from './do-state';
15
+ export type { DoStateLike, SqlStorageLike, WsLike, WsAttachment } from './do-state';
@@ -3,6 +3,8 @@ export { VelaWebSocketDurableObject } from "./websocket.durable-object.js";
3
3
  // Module + server-initiated emit helper
4
4
  export { CloudflareWebSocketModule } from "./cloudflare-websocket.module.js";
5
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";
6
8
  // Transport internals (advanced use / testing)
7
9
  export { CfWsClient } from "./cf-ws-client.js";
8
10
  export { CfRoomRegistry } from "./cf-room-registry.js";
@@ -17,6 +17,7 @@ const PONG = '{"event":"pong"}';
17
17
  */ export function VelaWebSocketDurableObject(rootModule) {
18
18
  return class VelaWsDurableObject extends DurableObject {
19
19
  host;
20
+ liveEngine;
20
21
  ready;
21
22
  constructor(ctx, env){
22
23
  super(ctx, env);
@@ -29,6 +30,7 @@ const PONG = '{"event":"pong"}';
29
30
  this.ready = ctx.blockConcurrencyWhile(async ()=>{
30
31
  const runtime = await buildDoRuntime(rootModule, ctx, env);
31
32
  this.host = new DoWebSocketHost(ctx, runtime.dispatcher, runtime.registry, runtime.gatewayPaths);
33
+ this.liveEngine = runtime.live;
32
34
  });
33
35
  }
34
36
  async fetch(request) {
@@ -68,5 +70,14 @@ const PONG = '{"event":"pong"}';
68
70
  await this.ready;
69
71
  this.host.broadcast(cmd);
70
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
+ }
71
82
  };
72
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/cloudflare",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Cloudflare Workers integration for Vela framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,7 +46,7 @@
46
46
  "peerDependencies": {
47
47
  "@cloudflare/workers-types": ">=4",
48
48
  "@velajs/feature-flags": "^0.1.0",
49
- "@velajs/vela": ">=1.11.0",
49
+ "@velajs/vela": ">=1.17.0",
50
50
  "hono": ">=4"
51
51
  },
52
52
  "peerDependenciesMeta": {
@@ -59,7 +59,7 @@
59
59
  "@swc/cli": "^0.8.1",
60
60
  "@swc/core": "^1.15.43",
61
61
  "@velajs/feature-flags": "^0.1.0",
62
- "@velajs/vela": "^1.12.0",
62
+ "@velajs/vela": "^1.17.0",
63
63
  "hono": "^4.12.27",
64
64
  "typescript": "^6.0.3",
65
65
  "unplugin-swc": "^1.5.9",