@velajs/cloudflare 1.8.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.
- package/dist/cloudflare-factory.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/services/flagship-flag.driver.d.ts +57 -0
- package/dist/services/flagship-flag.driver.js +49 -0
- package/dist/services/kv-flag.driver.d.ts +48 -0
- package/dist/services/kv-flag.driver.js +62 -0
- package/dist/websocket/do-bootstrap.d.ts +3 -0
- package/dist/websocket/do-bootstrap.js +5 -0
- package/dist/websocket/do-live.d.ts +81 -0
- package/dist/websocket/do-live.js +197 -0
- package/dist/websocket/do-state.d.ts +10 -0
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.js +2 -0
- package/dist/websocket/websocket.durable-object.js +11 -0
- package/package.json +10 -3
|
@@ -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
|
@@ -23,11 +23,17 @@ export { AIService } from './services/ai.service';
|
|
|
23
23
|
export { VectorizeService } from './services/vectorize.service';
|
|
24
24
|
export { HyperdriveService } from './services/hyperdrive.service';
|
|
25
25
|
export { EnvService } from './services/env.service';
|
|
26
|
+
export { FlagshipFlagDriver, flagshipFlagDriver } from './services/flagship-flag.driver';
|
|
27
|
+
export type { FlagshipBinding, FlagshipFlagDriverOptions } from './services/flagship-flag.driver';
|
|
28
|
+
export { KvFlagDriver, kvFlagDriver } from './services/kv-flag.driver';
|
|
29
|
+
export type { KvFlagDriverOptions } from './services/kv-flag.driver';
|
|
26
30
|
export { Env } from './decorators/env';
|
|
27
31
|
export { Scheduled } from './decorators/scheduled';
|
|
28
32
|
export { QueueConsumer } from './decorators/queue-consumer';
|
|
29
33
|
export { VelaWebSocketDurableObject, CloudflareWebSocketModule, broadcastToRoom, } from './websocket/index';
|
|
30
34
|
export type { WsGatewayRoute } from './websocket/index';
|
|
35
|
+
export { DoCursorLog, durableObjectCursorLog, durableObjectLive, liveInvalidateToRoom, } from './websocket/index';
|
|
36
|
+
export type { CfLiveDriver, DurableObjectLiveOptions } from './websocket/index';
|
|
31
37
|
export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException, } from '@velajs/vela/websocket';
|
|
32
38
|
export type { WsClient, WsServer, WsResponse, WsMessage, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from '@velajs/vela/websocket';
|
|
33
39
|
export type { CloudflareEnv, ScheduledRegistration, QueueRegistration } from './types';
|
package/dist/index.js
CHANGED
|
@@ -24,11 +24,16 @@ export { AIService } from "./services/ai.service.js";
|
|
|
24
24
|
export { VectorizeService } from "./services/vectorize.service.js";
|
|
25
25
|
export { HyperdriveService } from "./services/hyperdrive.service.js";
|
|
26
26
|
export { EnvService } from "./services/env.service.js";
|
|
27
|
+
// Feature-flag drivers (implement @velajs/feature-flags' FeatureFlagDriver contract)
|
|
28
|
+
export { FlagshipFlagDriver, flagshipFlagDriver } from "./services/flagship-flag.driver.js";
|
|
29
|
+
export { KvFlagDriver, kvFlagDriver } from "./services/kv-flag.driver.js";
|
|
27
30
|
// Decorators
|
|
28
31
|
export { Env } from "./decorators/env.js";
|
|
29
32
|
export { Scheduled } from "./decorators/scheduled.js";
|
|
30
33
|
export { QueueConsumer } from "./decorators/queue-consumer.js";
|
|
31
34
|
// WebSocket (Durable Object transport for the Vela WebSocketModule)
|
|
32
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";
|
|
33
38
|
// Re-export the core gateway API so a Cloudflare app can import it from one place.
|
|
34
39
|
export { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsException } from "@velajs/vela/websocket";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { FeatureFlagDriver, FlagContext } from '@velajs/feature-flags';
|
|
2
|
+
/**
|
|
3
|
+
* The subset of a Cloudflare **Flagship** binding this driver evaluates against
|
|
4
|
+
* — the four typed value methods. The binding itself returns the supplied
|
|
5
|
+
* `defaultValue` on evaluation errors; transport-level failures reject and are
|
|
6
|
+
* left to propagate (never-throw is the service layer's job, not the driver's).
|
|
7
|
+
*
|
|
8
|
+
* @see https://developers.cloudflare.com/flagship/binding/
|
|
9
|
+
*/
|
|
10
|
+
export interface FlagshipBinding {
|
|
11
|
+
getBooleanValue(key: string, defaultValue: boolean, context?: FlagContext): Promise<boolean>;
|
|
12
|
+
getStringValue(key: string, defaultValue: string, context?: FlagContext): Promise<string>;
|
|
13
|
+
getNumberValue(key: string, defaultValue: number, context?: FlagContext): Promise<number>;
|
|
14
|
+
getObjectValue<T extends object>(key: string, defaultValue: T, context?: FlagContext): Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
export interface FlagshipFlagDriverOptions {
|
|
17
|
+
/** Driver name used for `use(name)` / default-driver selection. Default `"flagship"`. */
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* {@link FeatureFlagDriver} backed by a Cloudflare Flagship binding.
|
|
22
|
+
*
|
|
23
|
+
* A thin, honest wrapper: each contract method maps 1:1 onto the binding's
|
|
24
|
+
* corresponding value method, forwarding the caller's `fallback` (the binding's
|
|
25
|
+
* `defaultValue`) and evaluation context. The binding resolves the fallback on
|
|
26
|
+
* evaluation errors; anything the binding *rejects* with (e.g. a `remote: true`
|
|
27
|
+
* dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
|
|
28
|
+
* the never-throw guarantee.
|
|
29
|
+
*
|
|
30
|
+
* The binding only exists per request in a Worker, so pass a lazy accessor when
|
|
31
|
+
* wiring from a bootstrap factory (mirroring how {@link EnvService} reads are
|
|
32
|
+
* deferred); a resolved binding may be passed directly in tests.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* FeatureFlagsModule.forRootAsync({
|
|
36
|
+
* inject: [EnvService],
|
|
37
|
+
* useFactory: (env: EnvService) => ({
|
|
38
|
+
* drivers: [flagshipFlagDriver(() => env.get<FlagshipBinding>('FLAGS')!)],
|
|
39
|
+
* }),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Evaluation ergonomics (the 1:1 binding-method mapping) are ported from the
|
|
44
|
+
* Stratal feature-flags service (MIT, © Temitayo Fadojutimi), reshaped as a
|
|
45
|
+
* bare driver.
|
|
46
|
+
*/
|
|
47
|
+
export declare class FlagshipFlagDriver implements FeatureFlagDriver {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
private readonly resolve;
|
|
50
|
+
constructor(binding: FlagshipBinding | (() => FlagshipBinding), options?: FlagshipFlagDriverOptions);
|
|
51
|
+
getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
|
|
52
|
+
getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
|
|
53
|
+
getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
|
|
54
|
+
getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
|
|
55
|
+
}
|
|
56
|
+
/** Convenience factory for {@link FlagshipFlagDriver}. */
|
|
57
|
+
export declare function flagshipFlagDriver(binding: FlagshipBinding | (() => FlagshipBinding), options?: FlagshipFlagDriverOptions): FlagshipFlagDriver;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link FeatureFlagDriver} backed by a Cloudflare Flagship binding.
|
|
3
|
+
*
|
|
4
|
+
* A thin, honest wrapper: each contract method maps 1:1 onto the binding's
|
|
5
|
+
* corresponding value method, forwarding the caller's `fallback` (the binding's
|
|
6
|
+
* `defaultValue`) and evaluation context. The binding resolves the fallback on
|
|
7
|
+
* evaluation errors; anything the binding *rejects* with (e.g. a `remote: true`
|
|
8
|
+
* dev-proxy tunnel dropping) propagates — `@velajs/feature-flags`'s service owns
|
|
9
|
+
* the never-throw guarantee.
|
|
10
|
+
*
|
|
11
|
+
* The binding only exists per request in a Worker, so pass a lazy accessor when
|
|
12
|
+
* wiring from a bootstrap factory (mirroring how {@link EnvService} reads are
|
|
13
|
+
* deferred); a resolved binding may be passed directly in tests.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* FeatureFlagsModule.forRootAsync({
|
|
17
|
+
* inject: [EnvService],
|
|
18
|
+
* useFactory: (env: EnvService) => ({
|
|
19
|
+
* drivers: [flagshipFlagDriver(() => env.get<FlagshipBinding>('FLAGS')!)],
|
|
20
|
+
* }),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Evaluation ergonomics (the 1:1 binding-method mapping) are ported from the
|
|
25
|
+
* Stratal feature-flags service (MIT, © Temitayo Fadojutimi), reshaped as a
|
|
26
|
+
* bare driver.
|
|
27
|
+
*/ export class FlagshipFlagDriver {
|
|
28
|
+
name;
|
|
29
|
+
resolve;
|
|
30
|
+
constructor(binding, options = {}){
|
|
31
|
+
this.resolve = typeof binding === 'function' ? binding : ()=>binding;
|
|
32
|
+
this.name = options.name ?? 'flagship';
|
|
33
|
+
}
|
|
34
|
+
getBoolean(key, fallback, ctx) {
|
|
35
|
+
return this.resolve().getBooleanValue(key, fallback, ctx);
|
|
36
|
+
}
|
|
37
|
+
getString(key, fallback, ctx) {
|
|
38
|
+
return this.resolve().getStringValue(key, fallback, ctx);
|
|
39
|
+
}
|
|
40
|
+
getNumber(key, fallback, ctx) {
|
|
41
|
+
return this.resolve().getNumberValue(key, fallback, ctx);
|
|
42
|
+
}
|
|
43
|
+
getObject(key, fallback, ctx) {
|
|
44
|
+
return this.resolve().getObjectValue(key, fallback, ctx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Convenience factory for {@link FlagshipFlagDriver}. */ export function flagshipFlagDriver(binding, options) {
|
|
48
|
+
return new FlagshipFlagDriver(binding, options);
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FeatureFlagDriver, FlagContext } from '@velajs/feature-flags';
|
|
2
|
+
import { KVService } from './kv.service';
|
|
3
|
+
export interface KvFlagDriverOptions {
|
|
4
|
+
/** Driver name used for `use(name)` / default-driver selection. Default `"kv"`. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Prefix prepended to every flag key before the KV read. Default `""` (none). */
|
|
7
|
+
prefix?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Cloudflare KV-backed {@link FeatureFlagDriver}. Flags are stored as JSON
|
|
11
|
+
* values under an optional key prefix and read with `get(key, 'json')`. Reads
|
|
12
|
+
* are type-checked against the requested type: a missing key or a value of the
|
|
13
|
+
* wrong JSON type returns the caller's `fallback`. KV has no targeting, so the
|
|
14
|
+
* evaluation context is ignored.
|
|
15
|
+
*
|
|
16
|
+
* The driver stays honest — it does **not** swallow errors. A KV failure (or a
|
|
17
|
+
* `SyntaxError` from a malformed stored value) propagates; the never-throw
|
|
18
|
+
* guarantee lives in `@velajs/feature-flags`'s service layer.
|
|
19
|
+
*
|
|
20
|
+
* Placed like {@link KVCacheStore}: construct it in a wiring factory over a
|
|
21
|
+
* resolved {@link KVService}.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* FeatureFlagsModule.forRootAsync({
|
|
25
|
+
* inject: [KVService],
|
|
26
|
+
* useFactory: (kv: KVService) => ({ drivers: [new KvFlagDriver(kv, { prefix: 'flag:' })] }),
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class KvFlagDriver implements FeatureFlagDriver {
|
|
31
|
+
private readonly kv;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
private readonly prefix;
|
|
34
|
+
constructor(kv: KVService, options?: KvFlagDriverOptions);
|
|
35
|
+
private get ns();
|
|
36
|
+
getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean>;
|
|
37
|
+
getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string>;
|
|
38
|
+
getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number>;
|
|
39
|
+
getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Reads and JSON-parses the (prefixed) key, returning the parsed value only
|
|
42
|
+
* when `matches` accepts its type; otherwise the caller's fallback. A missing
|
|
43
|
+
* key reads as `null` → fallback. Read/parse errors are left to propagate.
|
|
44
|
+
*/
|
|
45
|
+
private read;
|
|
46
|
+
}
|
|
47
|
+
/** Convenience factory for {@link KvFlagDriver}. */
|
|
48
|
+
export declare function kvFlagDriver(kv: KVService, options?: KvFlagDriverOptions): KvFlagDriver;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Inject, Injectable } from "@velajs/vela";
|
|
16
|
+
import { KVService } from "./kv.service.js";
|
|
17
|
+
export class KvFlagDriver {
|
|
18
|
+
kv;
|
|
19
|
+
name;
|
|
20
|
+
prefix;
|
|
21
|
+
constructor(kv, options = {}){
|
|
22
|
+
this.kv = kv;
|
|
23
|
+
this.name = options.name ?? 'kv';
|
|
24
|
+
this.prefix = options.prefix ?? '';
|
|
25
|
+
}
|
|
26
|
+
get ns() {
|
|
27
|
+
return this.kv.namespace;
|
|
28
|
+
}
|
|
29
|
+
getBoolean(key, fallback, _ctx) {
|
|
30
|
+
return this.read(key, fallback, (v)=>typeof v === 'boolean');
|
|
31
|
+
}
|
|
32
|
+
getString(key, fallback, _ctx) {
|
|
33
|
+
return this.read(key, fallback, (v)=>typeof v === 'string');
|
|
34
|
+
}
|
|
35
|
+
getNumber(key, fallback, _ctx) {
|
|
36
|
+
return this.read(key, fallback, (v)=>typeof v === 'number');
|
|
37
|
+
}
|
|
38
|
+
getObject(key, fallback, _ctx) {
|
|
39
|
+
return this.read(key, fallback, (v)=>typeof v === 'object' && v !== null);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads and JSON-parses the (prefixed) key, returning the parsed value only
|
|
43
|
+
* when `matches` accepts its type; otherwise the caller's fallback. A missing
|
|
44
|
+
* key reads as `null` → fallback. Read/parse errors are left to propagate.
|
|
45
|
+
*/ async read(key, fallback, matches) {
|
|
46
|
+
const value = await this.ns.get(this.prefix + key, 'json');
|
|
47
|
+
if (value === null || value === undefined) return fallback;
|
|
48
|
+
return matches(value) ? value : fallback;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
KvFlagDriver = _ts_decorate([
|
|
52
|
+
Injectable(),
|
|
53
|
+
_ts_param(0, Inject(KVService)),
|
|
54
|
+
_ts_metadata("design:type", Function),
|
|
55
|
+
_ts_metadata("design:paramtypes", [
|
|
56
|
+
typeof KVService === "undefined" ? Object : KVService,
|
|
57
|
+
typeof KvFlagDriverOptions === "undefined" ? Object : KvFlagDriverOptions
|
|
58
|
+
])
|
|
59
|
+
], KvFlagDriver);
|
|
60
|
+
/** Convenience factory for {@link KvFlagDriver}. */ export function kvFlagDriver(kv, options) {
|
|
61
|
+
return new KvFlagDriver(kv, options);
|
|
62
|
+
}
|
|
@@ -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';
|
package/dist/websocket/index.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Cloudflare Workers integration for Vela framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,14 +45,21 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@cloudflare/workers-types": ">=4",
|
|
48
|
-
"@velajs/
|
|
48
|
+
"@velajs/feature-flags": "^0.1.0",
|
|
49
|
+
"@velajs/vela": ">=1.17.0",
|
|
49
50
|
"hono": ">=4"
|
|
50
51
|
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@velajs/feature-flags": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
51
57
|
"devDependencies": {
|
|
52
58
|
"@cloudflare/workers-types": "^4.20260624.1",
|
|
53
59
|
"@swc/cli": "^0.8.1",
|
|
54
60
|
"@swc/core": "^1.15.43",
|
|
55
|
-
"@velajs/
|
|
61
|
+
"@velajs/feature-flags": "^0.1.0",
|
|
62
|
+
"@velajs/vela": "^1.17.0",
|
|
56
63
|
"hono": "^4.12.27",
|
|
57
64
|
"typescript": "^6.0.3",
|
|
58
65
|
"unplugin-swc": "^1.5.9",
|