@velajs/vela 1.16.0 → 1.17.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/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/live/index.d.ts +12 -0
- package/dist/live/index.js +16 -0
- package/dist/live/live.cursor.d.ts +22 -0
- package/dist/live/live.cursor.js +56 -0
- package/dist/live/live.decorators.d.ts +32 -0
- package/dist/live/live.decorators.js +46 -0
- package/dist/live/live.delta.d.ts +15 -0
- package/dist/live/live.delta.js +51 -0
- package/dist/live/live.engine.d.ts +81 -0
- package/dist/live/live.engine.js +448 -0
- package/dist/live/live.invalidation.d.ts +45 -0
- package/dist/live/live.invalidation.js +99 -0
- package/dist/live/live.module.d.ts +28 -0
- package/dist/live/live.module.js +87 -0
- package/dist/live/live.tokens.d.ts +9 -0
- package/dist/live/live.tokens.js +8 -0
- package/dist/live/live.types.d.ts +138 -0
- package/dist/live/live.types.js +1 -0
- package/dist/live/presence.d.ts +44 -0
- package/dist/live/presence.js +130 -0
- package/dist/websocket/index.d.ts +3 -3
- package/dist/websocket/index.js +2 -2
- package/dist/websocket/websocket.decorators.d.ts +14 -0
- package/dist/websocket/websocket.decorators.js +23 -1
- package/dist/websocket/websocket.tokens.d.ts +7 -0
- package/dist/websocket/websocket.tokens.js +6 -0
- package/dist/websocket/websocket.types.d.ts +16 -0
- package/dist/websocket/ws-dispatcher.d.ts +9 -0
- package/dist/websocket/ws-dispatcher.js +64 -1
- package/dist/websocket-node/index.d.ts +2 -0
- package/dist/websocket-node/index.js +1 -0
- package/dist/websocket-node/redis-live.d.ts +24 -0
- package/dist/websocket-node/redis-live.js +57 -0
- package/package.json +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_E
|
|
|
31
31
|
export type { EventHandler, OnEventMetadata } from './event-emitter/index';
|
|
32
32
|
export { ScheduleModule, ScheduleRegistry, Cron, Interval, parseCron, CRON_METADATA, INTERVAL_METADATA, } from './schedule/index';
|
|
33
33
|
export type { RegisteredCronJob, RegisteredIntervalJob, CronMetadata, IntervalMetadata, CronMatcher, } from './schedule/index';
|
|
34
|
-
export { WebSocketModule, WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsDispatcher, WsException, WS_SERVER, } from './websocket/index';
|
|
35
|
-
export type { WebSocketModuleOptions, WsClient, WsServer, WsMessage, WsResponse, WsExecutionContext, WsArgumentsHost, BroadcastCommand, SyncDriver, RoomRegistry, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from './websocket/index';
|
|
34
|
+
export { WebSocketModule, WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, ReservedWsEvent, WsDispatcher, WsException, WS_SERVER, RESERVED_WS_EVENT_PREFIX, } from './websocket/index';
|
|
35
|
+
export type { WebSocketModuleOptions, WsClient, WsServer, WsMessage, WsResponse, ReservedWsEventMetadata, ReservedWsEventHandler, WsExecutionContext, WsArgumentsHost, BroadcastCommand, SyncDriver, RoomRegistry, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, } from './websocket/index';
|
|
36
36
|
export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator, } from './health/index';
|
|
37
37
|
export type { HealthCheckResult, HealthCheckStatus, HealthIndicatorResult, HealthIndicatorFunction, ResponseCheckCallback, HttpPingOptions, } from './health/index';
|
|
38
38
|
export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA, } from './throttler/index';
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_E
|
|
|
33
33
|
// Schedule
|
|
34
34
|
export { ScheduleModule, ScheduleRegistry, Cron, Interval, parseCron, CRON_METADATA, INTERVAL_METADATA } from "./schedule/index.js";
|
|
35
35
|
// WebSocket (edge-safe core; transport-facing internals live at @velajs/vela/websocket)
|
|
36
|
-
export { WebSocketModule, WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, WsDispatcher, WsException, WS_SERVER } from "./websocket/index.js";
|
|
36
|
+
export { WebSocketModule, WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, WebSocketServer, ReservedWsEvent, WsDispatcher, WsException, WS_SERVER, RESERVED_WS_EVENT_PREFIX } from "./websocket/index.js";
|
|
37
37
|
// Health
|
|
38
38
|
export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator } from "./health/index.js";
|
|
39
39
|
// Throttler
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { LiveModule } from './live.module';
|
|
2
|
+
export { LiveResolver, LiveQuery, getLiveQueries } from './live.decorators';
|
|
3
|
+
export { LiveEngine, LIVE_SUBS_DATA_KEY, readPersistedLiveSubscriptions } from './live.engine';
|
|
4
|
+
export { LiveInvalidation, localLive, perAppLiveDriver, stampCommitHeaders } from './live.invalidation';
|
|
5
|
+
export { InMemoryCursorLog } from './live.cursor';
|
|
6
|
+
export { encodeSubscriptionUpdate } from './live.delta';
|
|
7
|
+
export { PresenceService, PresenceResolver, presenceTag, PRESENCE_ROSTER_QUERY } from './presence';
|
|
8
|
+
export type { PresenceMember } from './presence';
|
|
9
|
+
export { LIVE_DRIVER, LIVE_CURSOR_LOG, LIVE_MODULE_OPTIONS, LIVE_RESOLVER_METADATA, LIVE_QUERY_METADATA, } from './live.tokens';
|
|
10
|
+
export type { CommitStamp, CursorLog, InvalidationCommand, LiveDriver, LiveEntrypointMeta, LiveIdentity, LiveInvalidationSink, LiveModuleOptions, LivePresenceOptions, LiveQueryContext, LiveQueryMetadata, LiveQueryOptions, LiveResolverMetadata, ResumeVerdict, SubscriptionRecord, } from './live.types';
|
|
11
|
+
export { LIVE_PROTOCOL, LIVE_EVENT, LIVE_ERROR_CODES, COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER, DEFAULT_KEY_FIELD, encodeLiveEnvelope, encodeLiveFrame, readLiveEnvelope, isClientLiveFrame, isServerLiveFrame, encodeListDelta, applyListDelta, } from '@velajs/live-protocol';
|
|
12
|
+
export type { ClientLiveFrame, LiveFrame, RowOp, ServerLiveFrame } from '@velajs/live-protocol';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @velajs/vela/live — first-party live-query subsystem (tag-based realtime
|
|
2
|
+
// reactivity), authored entirely on the public API plus the shared wire
|
|
3
|
+
// package @velajs/live-protocol (the live-openness audit test enforces it).
|
|
4
|
+
// Subpath-only, like @velajs/vela/queue: the main barrel stays lean and the
|
|
5
|
+
// name stays free for platform packages.
|
|
6
|
+
export { LiveModule } from "./live.module.js";
|
|
7
|
+
export { LiveResolver, LiveQuery, getLiveQueries } from "./live.decorators.js";
|
|
8
|
+
export { LiveEngine, LIVE_SUBS_DATA_KEY, readPersistedLiveSubscriptions } from "./live.engine.js";
|
|
9
|
+
export { LiveInvalidation, localLive, perAppLiveDriver, stampCommitHeaders } from "./live.invalidation.js";
|
|
10
|
+
export { InMemoryCursorLog } from "./live.cursor.js";
|
|
11
|
+
export { encodeSubscriptionUpdate } from "./live.delta.js";
|
|
12
|
+
export { PresenceService, PresenceResolver, presenceTag, PRESENCE_ROSTER_QUERY } from "./presence.js";
|
|
13
|
+
export { LIVE_DRIVER, LIVE_CURSOR_LOG, LIVE_MODULE_OPTIONS, LIVE_RESOLVER_METADATA, LIVE_QUERY_METADATA } from "./live.tokens.js";
|
|
14
|
+
// The wire contract (frames, delta codec, headers) is re-exported so app code
|
|
15
|
+
// and transports need a single import surface.
|
|
16
|
+
export { LIVE_PROTOCOL, LIVE_EVENT, LIVE_ERROR_CODES, COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER, DEFAULT_KEY_FIELD, encodeLiveEnvelope, encodeLiveFrame, readLiveEnvelope, isClientLiveFrame, isServerLiveFrame, encodeListDelta, applyListDelta } from "@velajs/live-protocol";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CommitStamp, CursorLog, ResumeVerdict } from './live.types';
|
|
2
|
+
/**
|
|
3
|
+
* The in-core `CursorLog`: a bounded in-memory ring, epoch minted per process
|
|
4
|
+
* (Web Crypto). A restart rolls the epoch, so every reconnecting client falls
|
|
5
|
+
* back to snapshot — exactly the documented node/dev semantics. Within one
|
|
6
|
+
* process lifetime resume works for gaps the ring still covers; a trimmed gap
|
|
7
|
+
* degrades to snapshot (mirrors lunora's CDC retention rule,
|
|
8
|
+
* `ctx-db-cdc.ts` — retention gap → snapshot).
|
|
9
|
+
*
|
|
10
|
+
* The Cloudflare transport replaces this with a Durable-Object SQLite log so
|
|
11
|
+
* resume survives hibernation and eviction.
|
|
12
|
+
*/
|
|
13
|
+
export declare class InMemoryCursorLog implements CursorLog {
|
|
14
|
+
private readonly maxEntries;
|
|
15
|
+
private readonly epoch;
|
|
16
|
+
private readonly entries;
|
|
17
|
+
private seq;
|
|
18
|
+
constructor(maxEntries?: number);
|
|
19
|
+
append(tags: string[]): CommitStamp;
|
|
20
|
+
current(): CommitStamp;
|
|
21
|
+
evaluateResume(sinceCursor: number, sinceEpoch: string, subscriptionTags: string[]): ResumeVerdict;
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const intersects = (a, b)=>a.some((tag)=>b.has(tag));
|
|
2
|
+
/**
|
|
3
|
+
* The in-core `CursorLog`: a bounded in-memory ring, epoch minted per process
|
|
4
|
+
* (Web Crypto). A restart rolls the epoch, so every reconnecting client falls
|
|
5
|
+
* back to snapshot — exactly the documented node/dev semantics. Within one
|
|
6
|
+
* process lifetime resume works for gaps the ring still covers; a trimmed gap
|
|
7
|
+
* degrades to snapshot (mirrors lunora's CDC retention rule,
|
|
8
|
+
* `ctx-db-cdc.ts` — retention gap → snapshot).
|
|
9
|
+
*
|
|
10
|
+
* The Cloudflare transport replaces this with a Durable-Object SQLite log so
|
|
11
|
+
* resume survives hibernation and eviction.
|
|
12
|
+
*/ export class InMemoryCursorLog {
|
|
13
|
+
maxEntries;
|
|
14
|
+
epoch = crypto.randomUUID();
|
|
15
|
+
entries = [];
|
|
16
|
+
seq = 0;
|
|
17
|
+
constructor(maxEntries = 1024){
|
|
18
|
+
this.maxEntries = maxEntries;
|
|
19
|
+
}
|
|
20
|
+
append(tags) {
|
|
21
|
+
this.seq += 1;
|
|
22
|
+
this.entries.push({
|
|
23
|
+
seq: this.seq,
|
|
24
|
+
tags
|
|
25
|
+
});
|
|
26
|
+
if (this.entries.length > this.maxEntries) {
|
|
27
|
+
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
cursor: this.seq,
|
|
31
|
+
epoch: this.epoch
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
current() {
|
|
35
|
+
return {
|
|
36
|
+
cursor: this.seq,
|
|
37
|
+
epoch: this.epoch
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
evaluateResume(sinceCursor, sinceEpoch, subscriptionTags) {
|
|
41
|
+
// Forked timeline (restart/reset) — the client's cursor means nothing here.
|
|
42
|
+
if (sinceEpoch !== this.epoch) return 'snapshot';
|
|
43
|
+
// Rollback guard: a cursor from the future is unexplainable.
|
|
44
|
+
if (sinceCursor > this.seq) return 'snapshot';
|
|
45
|
+
if (sinceCursor === this.seq) return 'resume';
|
|
46
|
+
// Retention: the ring must still cover (sinceCursor, seq].
|
|
47
|
+
const oldest = this.entries[0];
|
|
48
|
+
if (oldest === undefined || oldest.seq > sinceCursor + 1) return 'snapshot';
|
|
49
|
+
const subTags = new Set(subscriptionTags);
|
|
50
|
+
for (const entry of this.entries){
|
|
51
|
+
if (entry.seq <= sinceCursor) continue;
|
|
52
|
+
if (intersects(entry.tags, subTags)) return 'rerun';
|
|
53
|
+
}
|
|
54
|
+
return 'resume';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LiveQueryMetadata, LiveQueryOptions } from './live.types';
|
|
2
|
+
/**
|
|
3
|
+
* Marks a provider class as a live-query resolver:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* @LiveResolver()
|
|
7
|
+
* @Injectable()
|
|
8
|
+
* class TodoLive {
|
|
9
|
+
* constructor(private readonly todos: TodoService) {}
|
|
10
|
+
*
|
|
11
|
+
* @LiveQuery('todos.list', { tags: (a: { listId: string }) => [`todos:${a.listId}`] })
|
|
12
|
+
* list(args: { listId: string }, ctx: LiveQueryContext) {
|
|
13
|
+
* return this.todos.byList(args.listId, ctx.identity?.userId);
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Clients subscribe by query name over the `$live` reserved event; the engine
|
|
19
|
+
* re-runs a handler whenever one of its tags is invalidated and pushes the
|
|
20
|
+
* result (as a keyed delta when possible). Stack with `@Injectable()`, exactly
|
|
21
|
+
* like `@Processor`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function LiveResolver(): ClassDecorator;
|
|
24
|
+
/**
|
|
25
|
+
* Declares a live query on a resolver method. The handler receives
|
|
26
|
+
* `(args, ctx: LiveQueryContext)` positionally — args are validated once at
|
|
27
|
+
* subscribe (via `options.parse`), then replayed verbatim into every re-run
|
|
28
|
+
* under the subscriber's captured identity.
|
|
29
|
+
*/
|
|
30
|
+
export declare function LiveQuery<A = unknown>(name: string, options: LiveQueryOptions<A>): MethodDecorator;
|
|
31
|
+
/** `@LiveQuery` entries declared on a resolver class (declaration order). */
|
|
32
|
+
export declare function getLiveQueries(resolverClass: object): LiveQueryMetadata[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createDiscoverableDecorator, defineMetadata, getMetadata } from "../index.js";
|
|
2
|
+
import { LIVE_QUERY_METADATA, LIVE_RESOLVER_METADATA } from "./live.tokens.js";
|
|
3
|
+
const LiveResolverMeta = createDiscoverableDecorator(LIVE_RESOLVER_METADATA);
|
|
4
|
+
/**
|
|
5
|
+
* Marks a provider class as a live-query resolver:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* @LiveResolver()
|
|
9
|
+
* @Injectable()
|
|
10
|
+
* class TodoLive {
|
|
11
|
+
* constructor(private readonly todos: TodoService) {}
|
|
12
|
+
*
|
|
13
|
+
* @LiveQuery('todos.list', { tags: (a: { listId: string }) => [`todos:${a.listId}`] })
|
|
14
|
+
* list(args: { listId: string }, ctx: LiveQueryContext) {
|
|
15
|
+
* return this.todos.byList(args.listId, ctx.identity?.userId);
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Clients subscribe by query name over the `$live` reserved event; the engine
|
|
21
|
+
* re-runs a handler whenever one of its tags is invalidated and pushes the
|
|
22
|
+
* result (as a keyed delta when possible). Stack with `@Injectable()`, exactly
|
|
23
|
+
* like `@Processor`.
|
|
24
|
+
*/ export function LiveResolver() {
|
|
25
|
+
return LiveResolverMeta({});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Declares a live query on a resolver method. The handler receives
|
|
29
|
+
* `(args, ctx: LiveQueryContext)` positionally — args are validated once at
|
|
30
|
+
* subscribe (via `options.parse`), then replayed verbatim into every re-run
|
|
31
|
+
* under the subscriber's captured identity.
|
|
32
|
+
*/ export function LiveQuery(name, options) {
|
|
33
|
+
return (target, propertyKey)=>{
|
|
34
|
+
const ctor = target.constructor;
|
|
35
|
+
const existing = getMetadata(LIVE_QUERY_METADATA, ctor) ?? [];
|
|
36
|
+
existing.push({
|
|
37
|
+
name,
|
|
38
|
+
methodName: propertyKey,
|
|
39
|
+
options: options
|
|
40
|
+
});
|
|
41
|
+
defineMetadata(LIVE_QUERY_METADATA, existing, ctor);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** `@LiveQuery` entries declared on a resolver class (declaration order). */ export function getLiveQueries(resolverClass) {
|
|
45
|
+
return getMetadata(LIVE_QUERY_METADATA, resolverClass) ?? [];
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ServerLiveFrame } from '@velajs/live-protocol';
|
|
2
|
+
import type { CommitStamp, SubscriptionRecord } from './live.types';
|
|
3
|
+
/**
|
|
4
|
+
* Decide what one re-run puts on the wire — pure, so the contract is
|
|
5
|
+
* unit-testable without sockets:
|
|
6
|
+
*
|
|
7
|
+
* - byte-identical to the baseline → `settled` (cursor still advances; this
|
|
8
|
+
* is what drops client optimistic layers for writes that didn't change the
|
|
9
|
+
* result);
|
|
10
|
+
* - baseline exists and the shared codec can diff → `delta` (batched keyed
|
|
11
|
+
* row ops);
|
|
12
|
+
* - otherwise (first send after subscribe/resume, codec bailed, unparseable
|
|
13
|
+
* baseline) → full `data` snapshot.
|
|
14
|
+
*/
|
|
15
|
+
export declare function encodeSubscriptionUpdate(record: SubscriptionRecord, json: string, result: unknown, stamp: CommitStamp, initial: boolean): ServerLiveFrame;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DEFAULT_KEY_FIELD, encodeListDelta } from "@velajs/live-protocol";
|
|
2
|
+
/**
|
|
3
|
+
* Decide what one re-run puts on the wire — pure, so the contract is
|
|
4
|
+
* unit-testable without sockets:
|
|
5
|
+
*
|
|
6
|
+
* - byte-identical to the baseline → `settled` (cursor still advances; this
|
|
7
|
+
* is what drops client optimistic layers for writes that didn't change the
|
|
8
|
+
* result);
|
|
9
|
+
* - baseline exists and the shared codec can diff → `delta` (batched keyed
|
|
10
|
+
* row ops);
|
|
11
|
+
* - otherwise (first send after subscribe/resume, codec bailed, unparseable
|
|
12
|
+
* baseline) → full `data` snapshot.
|
|
13
|
+
*/ export function encodeSubscriptionUpdate(record, json, result, stamp, initial) {
|
|
14
|
+
const settled = {
|
|
15
|
+
t: 'settled',
|
|
16
|
+
sub: record.sub,
|
|
17
|
+
cursor: stamp.cursor,
|
|
18
|
+
epoch: stamp.epoch
|
|
19
|
+
};
|
|
20
|
+
if (!initial && record.lastJson !== undefined) {
|
|
21
|
+
if (record.lastJson === json) return settled;
|
|
22
|
+
let previous;
|
|
23
|
+
let parsed = false;
|
|
24
|
+
try {
|
|
25
|
+
previous = JSON.parse(record.lastJson);
|
|
26
|
+
parsed = true;
|
|
27
|
+
} catch {
|
|
28
|
+
// Unparseable baseline — fall through to a snapshot.
|
|
29
|
+
}
|
|
30
|
+
if (parsed) {
|
|
31
|
+
const ops = encodeListDelta(previous, result, record.key ?? DEFAULT_KEY_FIELD);
|
|
32
|
+
if (ops !== undefined && ops.length > 0) {
|
|
33
|
+
return {
|
|
34
|
+
t: 'delta',
|
|
35
|
+
sub: record.sub,
|
|
36
|
+
ops,
|
|
37
|
+
cursor: stamp.cursor,
|
|
38
|
+
epoch: stamp.epoch
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (ops !== undefined) return settled;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
t: 'data',
|
|
46
|
+
sub: record.sub,
|
|
47
|
+
snapshot: result,
|
|
48
|
+
cursor: stamp.cursor,
|
|
49
|
+
epoch: stamp.epoch
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Container, DiscoveryService } from '../index';
|
|
2
|
+
import type { ContributesEntrypoints, Entrypoint, OnApplicationBootstrap, ReservedWsEventHandler, WsClient, WsMessage } from '../index';
|
|
3
|
+
import type { CommitStamp, CursorLog, InvalidationCommand, LiveDriver, LiveEntrypointMeta, LiveInvalidationSink, LiveModuleOptions, SubscriptionRecord } from './live.types';
|
|
4
|
+
import { PresenceService } from './presence';
|
|
5
|
+
/**
|
|
6
|
+
* Where the engine persists a connection's subscription records inside
|
|
7
|
+
* `client.data` (committed through `WsClient.commit()`). On Cloudflare that
|
|
8
|
+
* lands in the hibernation attachment, so subscriptions survive DO eviction;
|
|
9
|
+
* the transport replays them via `restoreSubscription` on wake. On node,
|
|
10
|
+
* `commit()` is a no-op and this is just in-memory bookkeeping.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LIVE_SUBS_DATA_KEY = "__velaLiveSubs";
|
|
13
|
+
/** Read the subscription records a transport persisted for a connection (wake/restore path). */
|
|
14
|
+
export declare function readPersistedLiveSubscriptions(client: WsClient): SubscriptionRecord[];
|
|
15
|
+
/**
|
|
16
|
+
* The live-query engine of ONE log scope: routes `$live` frames (subscribe /
|
|
17
|
+
* unsubscribe / presence), owns the subscription registry, and turns tag
|
|
18
|
+
* invalidations into re-run → diff → push cycles.
|
|
19
|
+
*
|
|
20
|
+
* Delivery semantics (ported from lunora's `shard-do.ts` refresh loop):
|
|
21
|
+
* - **Coalescing**: a burst of invalidations collapses into one flush; every
|
|
22
|
+
* committed invalidation is observed by a flush that STARTS after it
|
|
23
|
+
* committed (`applyInvalidation` appends to the log before arming the
|
|
24
|
+
* drain, and the drain re-loops while tags are pending).
|
|
25
|
+
* - **At-least-once**: a subscription's diff baseline (`lastJson`) advances
|
|
26
|
+
* only when the frame left the socket; keyed deltas are idempotent on
|
|
27
|
+
* replay.
|
|
28
|
+
* - **Frame suppression**: a byte-identical re-run sends `settled` — no
|
|
29
|
+
* payload, but the cursor still advances so client optimistic layers drop.
|
|
30
|
+
* - **Outbound expiry**: a subscription whose captured identity lapsed is
|
|
31
|
+
* dropped (socket closed) before it receives another scoped push.
|
|
32
|
+
* - **Cursor-before-run**: the stamp a frame carries is read BEFORE the
|
|
33
|
+
* re-run, so a result can only be NEWER than its cursor claims — an
|
|
34
|
+
* invalidation racing the run is re-observed by the next flush, never lost.
|
|
35
|
+
*
|
|
36
|
+
* Transports reach the engine via `app.entrypoints.ofKind('live')` (it
|
|
37
|
+
* contributes itself) — the same contract the WebSocket triad uses.
|
|
38
|
+
*/
|
|
39
|
+
export declare class LiveEngine implements OnApplicationBootstrap, ContributesEntrypoints, ReservedWsEventHandler, LiveInvalidationSink {
|
|
40
|
+
private readonly container;
|
|
41
|
+
private readonly discovery;
|
|
42
|
+
private readonly log;
|
|
43
|
+
private readonly options;
|
|
44
|
+
private readonly presence?;
|
|
45
|
+
private readonly queries;
|
|
46
|
+
private readonly connections;
|
|
47
|
+
private readonly pendingTags;
|
|
48
|
+
private drainChain;
|
|
49
|
+
private draining;
|
|
50
|
+
constructor(container: Container, discovery: DiscoveryService, log: CursorLog, driver: LiveDriver, options: LiveModuleOptions, presence?: PresenceService | undefined);
|
|
51
|
+
onApplicationBootstrap(): Promise<void>;
|
|
52
|
+
/** The `'live'` entrypoint — how transports (node registrar, CF DO bootstrap) find the engine. */
|
|
53
|
+
collectEntrypoints(): Entrypoint<LiveEntrypointMeta>[];
|
|
54
|
+
handleReservedEvent(path: string, client: WsClient, message: WsMessage): Promise<void>;
|
|
55
|
+
handleSocketClose(_path: string, client: WsClient): Promise<void>;
|
|
56
|
+
applyInvalidation(cmd: InvalidationCommand): Promise<CommitStamp>;
|
|
57
|
+
/** Settles when every pending invalidation has been flushed (tests, graceful transports). */
|
|
58
|
+
whenIdle(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Re-attach a persisted subscription without the subscribe handshake — the
|
|
61
|
+
* Cloudflare transport replays hibernation-attachment records through this
|
|
62
|
+
* on wake. No frames are sent; the next relevant invalidation pushes.
|
|
63
|
+
*/
|
|
64
|
+
restoreSubscription(path: string, client: WsClient, record: SubscriptionRecord): void;
|
|
65
|
+
private onSubscribe;
|
|
66
|
+
private runSubscribeGuards;
|
|
67
|
+
private scheduleDrain;
|
|
68
|
+
private drainLoop;
|
|
69
|
+
private push;
|
|
70
|
+
private runQuery;
|
|
71
|
+
/**
|
|
72
|
+
* Mirror the connection's records into `client.data` + `commit()` so
|
|
73
|
+
* hibernating transports can replay them on wake. The volatile diff
|
|
74
|
+
* baseline (`lastJson`/`lastCursor`) is deliberately stripped: it can be
|
|
75
|
+
* large (the whole last result vs the 16 KiB attachment cap) and a lost
|
|
76
|
+
* baseline just means the next relevant change sends a full snapshot.
|
|
77
|
+
*/
|
|
78
|
+
private persistSubscriptions;
|
|
79
|
+
private ensureConnection;
|
|
80
|
+
private sendFrame;
|
|
81
|
+
}
|