@velajs/vela 1.16.0 → 1.17.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.
Files changed (36) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +1 -1
  3. package/dist/live/index.d.ts +12 -0
  4. package/dist/live/index.js +16 -0
  5. package/dist/live/live.cursor.d.ts +22 -0
  6. package/dist/live/live.cursor.js +56 -0
  7. package/dist/live/live.decorators.d.ts +32 -0
  8. package/dist/live/live.decorators.js +46 -0
  9. package/dist/live/live.delta.d.ts +15 -0
  10. package/dist/live/live.delta.js +51 -0
  11. package/dist/live/live.engine.d.ts +81 -0
  12. package/dist/live/live.engine.js +448 -0
  13. package/dist/live/live.invalidation.d.ts +27 -0
  14. package/dist/live/live.invalidation.js +56 -0
  15. package/dist/live/live.module.d.ts +28 -0
  16. package/dist/live/live.module.js +83 -0
  17. package/dist/live/live.tokens.d.ts +9 -0
  18. package/dist/live/live.tokens.js +8 -0
  19. package/dist/live/live.types.d.ts +138 -0
  20. package/dist/live/live.types.js +1 -0
  21. package/dist/live/presence.d.ts +44 -0
  22. package/dist/live/presence.js +130 -0
  23. package/dist/websocket/index.d.ts +3 -3
  24. package/dist/websocket/index.js +2 -2
  25. package/dist/websocket/websocket.decorators.d.ts +14 -0
  26. package/dist/websocket/websocket.decorators.js +23 -1
  27. package/dist/websocket/websocket.tokens.d.ts +7 -0
  28. package/dist/websocket/websocket.tokens.js +6 -0
  29. package/dist/websocket/websocket.types.d.ts +16 -0
  30. package/dist/websocket/ws-dispatcher.d.ts +9 -0
  31. package/dist/websocket/ws-dispatcher.js +64 -1
  32. package/dist/websocket-node/index.d.ts +2 -0
  33. package/dist/websocket-node/index.js +1 -0
  34. package/dist/websocket-node/redis-live.d.ts +24 -0
  35. package/dist/websocket-node/redis-live.js +57 -0
  36. 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, 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, 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
+ }