@velajs/client 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +148 -14
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/dist/live-client-CAb9TctR.d.ts +230 -0
- package/dist/presence.d.ts +24 -21
- package/dist/presence.js +44 -42
- package/dist/presence.js.map +1 -0
- package/package.json +35 -35
- package/dist/connection.d.ts +0 -43
- package/dist/connection.js +0 -215
- package/dist/errors.d.ts +0 -18
- package/dist/errors.js +0 -36
- package/dist/frame-reducer.d.ts +0 -17
- package/dist/frame-reducer.js +0 -66
- package/dist/live-client.d.ts +0 -44
- package/dist/live-client.js +0 -210
- package/dist/optimistic.d.ts +0 -54
- package/dist/optimistic.js +0 -67
- package/dist/reconnect.d.ts +0 -13
- package/dist/reconnect.js +0 -16
- package/dist/stable-key.d.ts +0 -7
- package/dist/stable-key.js +0 -20
- package/dist/subscription.d.ts +0 -42
- package/dist/subscription.js +0 -0
- package/dist/types.d.ts +0 -126
- package/dist/types.js +0 -1
package/CHANGELOG.md
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { C as dropConfirmedLayers, S as applyOptimisticLayer, _ as WebSocketLike, a as LiveClientOptions, b as OptimisticHost, c as MutateOptions, d as ReadCacheAdapter, f as ReconnectOptions, g as WebSocketFactory, h as Unsubscribe, i as HydrationEntry, l as OptimisticTarget, m as SubscribeOptions, n as ArgsOf, o as LiveContract, p as ResultOf, r as ConnectionStatus, s as LiveStore, t as LiveClient, u as OutboxSink, v as CommitStamp, w as foldOptimistic, x as OptimisticLayer, y as LayerHandle } from "./live-client-CAb9TctR.js";
|
|
2
|
+
import { COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER, ClientLiveFrame, LIVE_EVENT, LIVE_PROTOCOL, RowOp, ServerLiveFrame, ServerLiveFrame as ServerLiveFrame$1, applyListDelta, encodeListDelta } from "@velajs/live-protocol";
|
|
3
|
+
//#region src/subscription.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* One deduped live subscription: every `subscribe()` for the same
|
|
6
|
+
* (query, args, room) joins a single state (one wire registration, N
|
|
7
|
+
* callbacks), lunora `subscription.ts` style.
|
|
8
|
+
*/
|
|
9
|
+
interface SubscriptionState {
|
|
10
|
+
/** Wire subscription id — client-chosen, unique per client (thus per socket). */
|
|
11
|
+
sub: string;
|
|
12
|
+
query: string;
|
|
13
|
+
args: unknown;
|
|
14
|
+
argsKey: string;
|
|
15
|
+
room: string;
|
|
16
|
+
key?: string;
|
|
17
|
+
/** Server acknowledged the registration. */
|
|
18
|
+
acked: boolean;
|
|
19
|
+
callbacks: Set<(value: unknown) => void>;
|
|
20
|
+
errorCallbacks: Set<(error: {
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
fatal: boolean;
|
|
24
|
+
}) => void>;
|
|
25
|
+
layers: OptimisticLayer[];
|
|
26
|
+
/** Authoritative value with NO optimistic overlay. */
|
|
27
|
+
serverBase: unknown;
|
|
28
|
+
/** Distinguishes "no value yet" from an authoritative `undefined`-shaped value. */
|
|
29
|
+
hasBase: boolean;
|
|
30
|
+
serverCursor?: number;
|
|
31
|
+
serverEpoch?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Displayed value = serverBase folded through layers. Reassigned only when
|
|
34
|
+
* something actually changed, so snapshot consumers (React's
|
|
35
|
+
* useSyncExternalStore) get referentially stable reads between notifies.
|
|
36
|
+
*/
|
|
37
|
+
lastValue: unknown;
|
|
38
|
+
}
|
|
39
|
+
declare const subscriptionKey: (query: string, argsKey: string, room: string) => string;
|
|
40
|
+
declare const argsKeyOf: (args: unknown) => string;
|
|
41
|
+
declare function createSubscriptionState(query: string, args: unknown, room: string, key?: string): SubscriptionState;
|
|
42
|
+
/** Re-fold the displayed value; true when it changed (notify). */
|
|
43
|
+
declare function refold(state: SubscriptionState): boolean;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/connection.d.ts
|
|
46
|
+
interface ConnectionDeps {
|
|
47
|
+
makeSocket: WebSocketFactory;
|
|
48
|
+
authToken?: () => string | undefined | Promise<string | undefined>;
|
|
49
|
+
heartbeatIntervalMs: number;
|
|
50
|
+
reconnect?: ReconnectOptions;
|
|
51
|
+
onStatusChange: () => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One socket per room (vela's node transport auto-joins the `:id` route
|
|
55
|
+
* param's room; Cloudflare is one-DO≈one-room), multiplexing every live
|
|
56
|
+
* subscription for that room. Owns: connect/reconnect (decorrelated jitter),
|
|
57
|
+
* resubscribe-with-cursor on open, app-level ping keepalive (self-rescheduling
|
|
58
|
+
* setTimeout — never setInterval), and inbound `$live` frame routing into the
|
|
59
|
+
* pure frame reducer.
|
|
60
|
+
*/
|
|
61
|
+
declare class RoomConnection {
|
|
62
|
+
private readonly url;
|
|
63
|
+
private readonly deps;
|
|
64
|
+
status: ConnectionStatus;
|
|
65
|
+
private socket?;
|
|
66
|
+
private readonly bySub;
|
|
67
|
+
private readonly reconnectState;
|
|
68
|
+
private reconnectTimer?;
|
|
69
|
+
private heartbeatTimer?;
|
|
70
|
+
private closedByUser;
|
|
71
|
+
private generation;
|
|
72
|
+
constructor(url: string, deps: ConnectionDeps);
|
|
73
|
+
register(state: SubscriptionState): void;
|
|
74
|
+
unregister(state: SubscriptionState): void;
|
|
75
|
+
sendPresence(room: string, meta?: unknown): void;
|
|
76
|
+
close(): void;
|
|
77
|
+
private ensureConnected;
|
|
78
|
+
private connect;
|
|
79
|
+
private handleMessage;
|
|
80
|
+
private sendSub;
|
|
81
|
+
private sendFrame;
|
|
82
|
+
private scheduleReconnect;
|
|
83
|
+
private scheduleHeartbeat;
|
|
84
|
+
private clearTimers;
|
|
85
|
+
private setStatus;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/frame-reducer.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* What the connection layer must do after a frame was applied:
|
|
91
|
+
* - `notify` — the displayed value changed, fire callbacks;
|
|
92
|
+
* - `resubscribe` — the cache is unusable (epoch fork mid-delta, unmergeable
|
|
93
|
+
* delta); send a fresh cold `sub` for this state;
|
|
94
|
+
* - `error` — an error frame; route to error callbacks;
|
|
95
|
+
* - `none` — bookkeeping only.
|
|
96
|
+
*/
|
|
97
|
+
type FrameEffect = 'none' | 'notify' | 'resubscribe' | 'error';
|
|
98
|
+
/**
|
|
99
|
+
* The pure per-subscription frame state machine (adapts lunora's
|
|
100
|
+
* `handleDataMessage`/`handleResumeMessage`/`handleSettledMessage`).
|
|
101
|
+
* Deliberately socket-free so the protocol semantics are unit-testable.
|
|
102
|
+
*/
|
|
103
|
+
declare function applyServerFrame(state: SubscriptionState, frame: ServerLiveFrame$1): FrameEffect;
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/errors.d.ts
|
|
106
|
+
/**
|
|
107
|
+
* The client's error surface — three tiers:
|
|
108
|
+
* - connection-level: surfaced through `onConnectionStatus('offline')`;
|
|
109
|
+
* - subscription-level: `error` frames → `SubscribeOptions.onError`;
|
|
110
|
+
* - mutation-level: `mutate()` rejects with a {@link VelaLiveError}.
|
|
111
|
+
*
|
|
112
|
+
* Follows lunora's zero-dep catalog pattern (one error class, a small code
|
|
113
|
+
* union, structural guards) inlined rather than published separately.
|
|
114
|
+
*/
|
|
115
|
+
declare class VelaLiveError extends Error {
|
|
116
|
+
readonly code: string;
|
|
117
|
+
readonly status?: number | undefined;
|
|
118
|
+
constructor(code: string, message: string, status?: number | undefined);
|
|
119
|
+
}
|
|
120
|
+
declare const isVelaLiveError: (value: unknown) => value is VelaLiveError;
|
|
121
|
+
declare const getErrorCode: (value: unknown) => string | undefined;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/reconnect.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Decorrelated exponential jitter (lunora `reconnect.ts` port): each retry
|
|
126
|
+
* waits a random duration between the base and 3× the previous wait, capped.
|
|
127
|
+
* Decorrelation avoids the reconnect stampede a fixed exponential schedule
|
|
128
|
+
* produces when a fleet of clients drops at once.
|
|
129
|
+
*/
|
|
130
|
+
interface ReconnectState {
|
|
131
|
+
previousMs?: number;
|
|
132
|
+
}
|
|
133
|
+
declare const DEFAULT_RECONNECT_BASE_MS = 250;
|
|
134
|
+
declare const DEFAULT_RECONNECT_CAP_MS = 30000;
|
|
135
|
+
declare function nextReconnectDelay(state: ReconnectState, baseMs?: number, capMs?: number, random?: () => number): number;
|
|
136
|
+
declare function resetReconnect(state: ReconnectState): void;
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/stable-key.d.ts
|
|
139
|
+
/**
|
|
140
|
+
* Deterministic JSON encoding for cache/dedup keys: object keys are sorted at
|
|
141
|
+
* every depth so `{a,b}` and `{b,a}` produce the same key (lunora
|
|
142
|
+
* `shared/stable-key.ts` pattern). NOT a wire format — wire frames use plain
|
|
143
|
+
* `JSON.stringify` via @velajs/live-protocol's canonical encoders.
|
|
144
|
+
*/
|
|
145
|
+
declare function stableStringify(value: unknown): string;
|
|
146
|
+
//#endregion
|
|
147
|
+
export { type ArgsOf, COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER, type ClientLiveFrame, type CommitStamp, type ConnectionStatus, DEFAULT_RECONNECT_BASE_MS, DEFAULT_RECONNECT_CAP_MS, type FrameEffect, type HydrationEntry, LIVE_EVENT, LIVE_PROTOCOL, type LayerHandle, LiveClient, type LiveClientOptions, type LiveContract, type LiveStore, type MutateOptions, type OptimisticHost, type OptimisticLayer, type OptimisticTarget, type OutboxSink, type ReadCacheAdapter, type ReconnectOptions, type ResultOf, RoomConnection, type RowOp, type ServerLiveFrame, type SubscribeOptions, type SubscriptionState, type Unsubscribe, VelaLiveError, type WebSocketFactory, type WebSocketLike, applyListDelta, applyOptimisticLayer, applyServerFrame, argsKeyOf, createSubscriptionState, dropConfirmedLayers, encodeListDelta, foldOptimistic, getErrorCode, isVelaLiveError, nextReconnectDelay, refold, resetReconnect, stableStringify, subscriptionKey };
|
|
148
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["applyListDelta","LIVE_PROTOCOL","COMMIT_CURSOR_HEADER","COMMIT_EPOCH_HEADER"],"sources":["../src/optimistic.ts","../src/stable-key.ts","../src/subscription.ts","../src/frame-reducer.ts","../src/reconnect.ts","../src/connection.ts","../src/errors.ts","../src/live-client.ts"],"sourcesContent":["/**\n * Rebaseable, cursor-gated optimistic layers (lunora `optimistic-layers.ts`\n * port). The displayed value is ALWAYS the authoritative server base folded\n * through the pending layers, so:\n *\n * - an unrelated server frame re-folds the layers onto the new base instead of\n * clobbering the optimistic state (\"rebasing\");\n * - a layer drops only when a subscription frame arrives whose cursor passes\n * the mutation's commit cursor (same epoch) — NEVER on HTTP-response\n * timing, which races the broadcast;\n * - confirm without a stamp (a non-live-aware endpoint) degrades to one-shot\n * optimism: the layer drops silently and the next authoritative frame\n * reconciles;\n * - rollback removes the layer and re-folds so the failed write's effect\n * disappears immediately.\n */\n\nexport interface CommitStamp {\n cursor: number;\n epoch: string;\n}\n\nexport interface OptimisticLayer {\n readonly id: symbol;\n readonly transform: (current: unknown) => unknown;\n /** Set by confirm(); the layer is dropped by the first frame whose cursor passes it. */\n commitCursor?: number;\n commitEpoch?: string;\n}\n\n/** The slice of subscription state the optimistic engine operates on. */\nexport interface OptimisticHost {\n layers: OptimisticLayer[];\n serverBase: unknown;\n hasBase: boolean;\n serverCursor?: number;\n serverEpoch?: string;\n}\n\nexport const foldOptimistic = (host: OptimisticHost): unknown =>\n host.layers.reduce(\n (value, layer) => layer.transform(value),\n host.hasBase ? host.serverBase : undefined,\n );\n\nexport interface LayerHandle {\n /**\n * Record the mutation's commit stamp. With a stamp, the layer survives\n * until a frame's cursor passes it (dropping immediately when the current\n * frame already has). Without one, the layer is removed silently — no\n * re-fold, the incoming authoritative frame reconciles.\n */\n confirm(stamp?: CommitStamp): boolean;\n /** Remove the layer and report whether the host must re-fold + notify. */\n rollback(): boolean;\n}\n\nexport function applyOptimisticLayer(\n host: OptimisticHost,\n transform: (current: unknown) => unknown,\n): LayerHandle {\n const layer: OptimisticLayer = { id: Symbol('optimistic'), transform };\n host.layers.push(layer);\n\n const remove = (): boolean => {\n const index = host.layers.indexOf(layer);\n if (index === -1) return false;\n host.layers.splice(index, 1);\n return true;\n };\n\n return {\n confirm(stamp) {\n if (!stamp) {\n remove();\n return false;\n }\n // A stamp from a foreign epoch can never be gated — treat as unstamped.\n if (host.serverEpoch !== undefined && stamp.epoch !== host.serverEpoch) {\n remove();\n return false;\n }\n layer.commitCursor = stamp.cursor;\n layer.commitEpoch = stamp.epoch;\n // The confirming frame may already have passed while the HTTP response\n // was in flight — drop now and tell the caller to re-fold.\n if (host.serverCursor !== undefined && host.serverCursor >= stamp.cursor) {\n return remove();\n }\n return false;\n },\n rollback() {\n return remove();\n },\n };\n}\n\n/**\n * Drop every confirmed layer the frame at (cursor, epoch) covers. A layer\n * confirmed under a DIFFERENT epoch is also dropped — its gate can never fire\n * on this timeline. Returns whether any layer was removed (host re-folds).\n */\nexport function dropConfirmedLayers(\n host: OptimisticHost,\n cursor?: number,\n epoch?: string,\n): boolean {\n if (cursor === undefined) return false;\n const before = host.layers.length;\n host.layers = host.layers.filter((layer) => {\n if (layer.commitCursor === undefined) return true; // still pending\n if (epoch !== undefined && layer.commitEpoch !== epoch) return false;\n return cursor < layer.commitCursor;\n });\n return host.layers.length !== before;\n}\n","/**\n * Deterministic JSON encoding for cache/dedup keys: object keys are sorted at\n * every depth so `{a,b}` and `{b,a}` produce the same key (lunora\n * `shared/stable-key.ts` pattern). NOT a wire format — wire frames use plain\n * `JSON.stringify` via @velajs/live-protocol's canonical encoders.\n */\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(sortDeep(value));\n}\n\nfunction sortDeep(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortDeep);\n if (typeof value === 'object' && value !== null) {\n const source = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(source).sort()) {\n out[key] = sortDeep(source[key]);\n }\n return out;\n }\n return value;\n}\n","import { foldOptimistic } from './optimistic';\nimport type { OptimisticLayer } from './optimistic';\nimport { stableStringify } from './stable-key';\n\n/**\n * One deduped live subscription: every `subscribe()` for the same\n * (query, args, room) joins a single state (one wire registration, N\n * callbacks), lunora `subscription.ts` style.\n */\nexport interface SubscriptionState {\n /** Wire subscription id — client-chosen, unique per client (thus per socket). */\n sub: string;\n query: string;\n args: unknown;\n argsKey: string;\n room: string;\n key?: string;\n /** Server acknowledged the registration. */\n acked: boolean;\n callbacks: Set<(value: unknown) => void>;\n errorCallbacks: Set<(error: { code: string; message: string; fatal: boolean }) => void>;\n layers: OptimisticLayer[];\n /** Authoritative value with NO optimistic overlay. */\n serverBase: unknown;\n /** Distinguishes \"no value yet\" from an authoritative `undefined`-shaped value. */\n hasBase: boolean;\n serverCursor?: number;\n serverEpoch?: string;\n /**\n * Displayed value = serverBase folded through layers. Reassigned only when\n * something actually changed, so snapshot consumers (React's\n * useSyncExternalStore) get referentially stable reads between notifies.\n */\n lastValue: unknown;\n}\n\nexport const subscriptionKey = (query: string, argsKey: string, room: string): string =>\n `${room}\u0000${query}\u0000${argsKey}`;\n\nexport const argsKeyOf = (args: unknown): string => stableStringify(args ?? null);\n\nlet wireSeq = 0;\n\nexport function createSubscriptionState(\n query: string,\n args: unknown,\n room: string,\n key?: string,\n): SubscriptionState {\n wireSeq += 1;\n return {\n sub: `s${wireSeq}`,\n query,\n args,\n argsKey: argsKeyOf(args),\n room,\n key,\n acked: false,\n callbacks: new Set(),\n errorCallbacks: new Set(),\n layers: [],\n serverBase: undefined,\n hasBase: false,\n serverCursor: undefined,\n serverEpoch: undefined,\n lastValue: undefined,\n };\n}\n\n/** Re-fold the displayed value; true when it changed (notify). */\nexport function refold(state: SubscriptionState): boolean {\n const next =\n state.layers.length === 0 && state.hasBase ? state.serverBase : foldOptimistic(state);\n if (next === state.lastValue) return false;\n state.lastValue = next;\n return true;\n}\n\nexport function notify(state: SubscriptionState): void {\n for (const callback of state.callbacks) {\n callback(state.lastValue);\n }\n}\n","import { DEFAULT_KEY_FIELD, applyListDelta } from '@velajs/live-protocol';\nimport type { ServerLiveFrame } from '@velajs/live-protocol';\nimport { dropConfirmedLayers } from './optimistic';\nimport { refold } from './subscription';\nimport type { SubscriptionState } from './subscription';\n\n/**\n * What the connection layer must do after a frame was applied:\n * - `notify` — the displayed value changed, fire callbacks;\n * - `resubscribe` — the cache is unusable (epoch fork mid-delta, unmergeable\n * delta); send a fresh cold `sub` for this state;\n * - `error` — an error frame; route to error callbacks;\n * - `none` — bookkeeping only.\n */\nexport type FrameEffect = 'none' | 'notify' | 'resubscribe' | 'error';\n\n/**\n * The pure per-subscription frame state machine (adapts lunora's\n * `handleDataMessage`/`handleResumeMessage`/`handleSettledMessage`).\n * Deliberately socket-free so the protocol semantics are unit-testable.\n */\nexport function applyServerFrame(state: SubscriptionState, frame: ServerLiveFrame): FrameEffect {\n switch (frame.t) {\n case 'ack':\n state.acked = true;\n return 'none';\n\n case 'error':\n return 'error';\n\n case 'resume': {\n // Nothing relevant changed while away: keep the cached value, advance\n // the watermark. Confirmed layers the new cursor covers still drop.\n state.serverCursor = frame.cursor;\n state.serverEpoch = frame.epoch;\n const dropped = dropConfirmedLayers(state, frame.cursor, frame.epoch);\n return dropped && refold(state) ? 'notify' : 'none';\n }\n\n case 'settled': {\n // Byte-identical re-run: no payload, but the cursor advance is what\n // drops optimistic layers for writes that didn't change this query.\n advanceWatermark(state, frame.cursor, frame.epoch);\n const dropped = dropConfirmedLayers(state, frame.cursor, frame.epoch);\n return dropped && refold(state) ? 'notify' : 'none';\n }\n\n case 'data': {\n if (epochForked(state, frame.epoch)) {\n // New timeline: every optimistic gate is void. The snapshot itself is\n // authoritative, so apply it as a cold first frame.\n state.layers = [];\n }\n state.serverBase = frame.snapshot;\n state.hasBase = true;\n advanceWatermark(state, frame.cursor, frame.epoch);\n dropConfirmedLayers(state, frame.cursor, frame.epoch);\n refold(state);\n return 'notify';\n }\n\n case 'delta': {\n // A delta diffs against the baseline the server believes we confirmed —\n // across an epoch fork or without a base that belief is wrong by\n // construction: start over.\n if (epochForked(state, frame.epoch) || !state.hasBase) return 'resubscribe';\n const merged = applyListDelta(state.serverBase, frame.ops, state.key ?? DEFAULT_KEY_FIELD);\n if (merged === undefined) return 'resubscribe';\n state.serverBase = merged;\n advanceWatermark(state, frame.cursor, frame.epoch);\n dropConfirmedLayers(state, frame.cursor, frame.epoch);\n refold(state);\n return 'notify';\n }\n }\n}\n\nconst epochForked = (state: SubscriptionState, epoch?: string): boolean =>\n epoch !== undefined && state.serverEpoch !== undefined && epoch !== state.serverEpoch;\n\nfunction advanceWatermark(state: SubscriptionState, cursor?: number, epoch?: string): void {\n if (cursor !== undefined) state.serverCursor = cursor;\n if (epoch !== undefined) state.serverEpoch = epoch;\n}\n","/**\n * Decorrelated exponential jitter (lunora `reconnect.ts` port): each retry\n * waits a random duration between the base and 3× the previous wait, capped.\n * Decorrelation avoids the reconnect stampede a fixed exponential schedule\n * produces when a fleet of clients drops at once.\n */\nexport interface ReconnectState {\n previousMs?: number;\n}\n\nexport const DEFAULT_RECONNECT_BASE_MS = 250;\nexport const DEFAULT_RECONNECT_CAP_MS = 30_000;\n\nexport function nextReconnectDelay(\n state: ReconnectState,\n baseMs = DEFAULT_RECONNECT_BASE_MS,\n capMs = DEFAULT_RECONNECT_CAP_MS,\n random: () => number = Math.random,\n): number {\n const high = Math.max(baseMs, (state.previousMs ?? baseMs) * 3);\n const delay = Math.min(capMs, baseMs + random() * (high - baseMs));\n state.previousMs = delay;\n return delay;\n}\n\nexport function resetReconnect(state: ReconnectState): void {\n state.previousMs = undefined;\n}\n","import {\n LIVE_PROTOCOL,\n encodeLiveEnvelope,\n isServerLiveFrame,\n readLiveEnvelope,\n} from '@velajs/live-protocol';\nimport type { ClientLiveFrame } from '@velajs/live-protocol';\nimport { applyServerFrame } from './frame-reducer';\nimport { nextReconnectDelay, resetReconnect } from './reconnect';\nimport type { ReconnectState } from './reconnect';\nimport { notify } from './subscription';\nimport type { SubscriptionState } from './subscription';\nimport type { ConnectionStatus, ReconnectOptions, WebSocketFactory } from './types';\n\nconst OPEN = 1;\n\nexport interface ConnectionDeps {\n makeSocket: WebSocketFactory;\n authToken?: () => string | undefined | Promise<string | undefined>;\n heartbeatIntervalMs: number;\n reconnect?: ReconnectOptions;\n onStatusChange: () => void;\n}\n\n/**\n * One socket per room (vela's node transport auto-joins the `:id` route\n * param's room; Cloudflare is one-DO≈one-room), multiplexing every live\n * subscription for that room. Owns: connect/reconnect (decorrelated jitter),\n * resubscribe-with-cursor on open, app-level ping keepalive (self-rescheduling\n * setTimeout — never setInterval), and inbound `$live` frame routing into the\n * pure frame reducer.\n */\nexport class RoomConnection {\n status: ConnectionStatus = 'idle';\n private socket?: ReturnType<WebSocketFactory>;\n private readonly bySub = new Map<string, SubscriptionState>();\n private readonly reconnectState: ReconnectState = {};\n private reconnectTimer?: ReturnType<typeof setTimeout>;\n private heartbeatTimer?: ReturnType<typeof setTimeout>;\n private closedByUser = false;\n private generation = 0;\n\n constructor(\n private readonly url: string,\n private readonly deps: ConnectionDeps,\n ) {}\n\n register(state: SubscriptionState): void {\n this.bySub.set(state.sub, state);\n if (this.socket?.readyState === OPEN) {\n this.sendSub(state);\n } else {\n this.ensureConnected();\n }\n }\n\n unregister(state: SubscriptionState): void {\n this.bySub.delete(state.sub);\n this.sendFrame({ t: 'unsub', sub: state.sub });\n }\n\n sendPresence(room: string, meta?: unknown): void {\n this.ensureConnected();\n this.sendFrame({ t: 'presence', room, meta });\n }\n\n close(): void {\n this.closedByUser = true;\n this.clearTimers();\n this.socket?.close(1000, 'client closed');\n this.socket = undefined;\n this.setStatus('closed');\n }\n\n private ensureConnected(): void {\n if (this.closedByUser) return;\n if (this.socket && this.socket.readyState <= OPEN) return; // CONNECTING or OPEN\n void this.connect();\n }\n\n private async connect(): Promise<void> {\n this.setStatus('connecting');\n const generation = ++this.generation;\n\n let token: string | undefined;\n try {\n token = await this.deps.authToken?.();\n } catch {\n // A failing token provider is treated as a connection failure: back off\n // and retry — the next attempt re-invokes it (rotation-friendly).\n if (generation === this.generation) this.scheduleReconnect();\n return;\n }\n if (generation !== this.generation || this.closedByUser) return;\n\n const url =\n token === undefined\n ? this.url\n : `${this.url}${this.url.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`;\n let socket: ReturnType<WebSocketFactory>;\n try {\n socket = this.deps.makeSocket(url);\n } catch {\n this.scheduleReconnect();\n return;\n }\n this.socket = socket;\n\n socket.onopen = () => {\n if (generation !== this.generation) return;\n resetReconnect(this.reconnectState);\n this.setStatus('connected');\n // Resubscribe everything with resume watermarks — the server answers\n // each with data (re-run), resume (untouched), or a cold snapshot.\n for (const state of this.bySub.values()) this.sendSub(state);\n this.scheduleHeartbeat();\n };\n\n socket.onmessage = (event) => {\n if (typeof event.data !== 'string') return;\n this.handleMessage(event.data);\n };\n\n socket.onclose = () => {\n if (generation !== this.generation) return;\n this.clearTimers();\n this.socket = undefined;\n if (this.closedByUser) return;\n this.setStatus('offline');\n this.scheduleReconnect();\n };\n\n socket.onerror = () => {\n // onclose follows; nothing to do here.\n };\n }\n\n private handleMessage(raw: string): void {\n let envelope: unknown;\n try {\n envelope = JSON.parse(raw);\n } catch {\n return;\n }\n const frame = readLiveEnvelope(envelope);\n if (frame === undefined || !isServerLiveFrame(frame)) return; // classic gateway events / pong / unknown frames\n\n const sub = 'sub' in frame ? frame.sub : undefined;\n const state = sub === undefined ? undefined : this.bySub.get(sub);\n if (!state) return;\n\n const effect = applyServerFrame(state, frame);\n switch (effect) {\n case 'notify':\n notify(state);\n return;\n case 'error':\n if (frame.t === 'error') {\n for (const callback of state.errorCallbacks) {\n callback({ code: frame.code, message: frame.message, fatal: frame.fatal });\n }\n }\n return;\n case 'resubscribe':\n // Unusable cache (epoch fork mid-delta / unmergeable delta): start\n // cold. Same wire id ⇒ unsub first so the server replaces the record.\n state.hasBase = false;\n state.serverBase = undefined;\n state.serverCursor = undefined;\n state.serverEpoch = undefined;\n this.sendFrame({ t: 'unsub', sub: state.sub });\n this.sendSub(state);\n return;\n case 'none':\n return;\n }\n }\n\n private sendSub(state: SubscriptionState): void {\n const frame: ClientLiveFrame = {\n t: 'sub',\n sub: state.sub,\n query: state.query,\n ...(state.args === undefined ? {} : { args: state.args }),\n ...(state.serverCursor !== undefined && state.serverEpoch !== undefined\n ? { sinceCursor: state.serverCursor, sinceEpoch: state.serverEpoch }\n : {}),\n ...(state.key === undefined ? {} : { key: state.key }),\n v: LIVE_PROTOCOL,\n };\n this.sendFrame(frame);\n }\n\n private sendFrame(frame: ClientLiveFrame): void {\n if (this.socket?.readyState !== OPEN) return; // onopen resends subscriptions\n try {\n this.socket.send(encodeLiveEnvelope(frame));\n } catch {\n // socket died between the readyState check and send — onclose recovers\n }\n }\n\n private scheduleReconnect(): void {\n if (this.closedByUser || this.reconnectTimer) return;\n const delay = nextReconnectDelay(\n this.reconnectState,\n this.deps.reconnect?.baseMs,\n this.deps.reconnect?.capMs,\n );\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = undefined;\n void this.connect();\n }, delay);\n }\n\n private scheduleHeartbeat(): void {\n if (this.heartbeatTimer) return;\n const tick = () => {\n this.heartbeatTimer = undefined;\n if (this.socket?.readyState !== OPEN) return;\n try {\n this.socket.send('{\"event\":\"ping\"}');\n } catch {\n return;\n }\n this.heartbeatTimer = setTimeout(tick, this.deps.heartbeatIntervalMs);\n };\n this.heartbeatTimer = setTimeout(tick, this.deps.heartbeatIntervalMs);\n }\n\n private clearTimers(): void {\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n if (this.heartbeatTimer) clearTimeout(this.heartbeatTimer);\n this.reconnectTimer = undefined;\n this.heartbeatTimer = undefined;\n }\n\n private setStatus(status: ConnectionStatus): void {\n if (this.status === status) return;\n this.status = status;\n this.deps.onStatusChange();\n }\n}\n","/**\n * The client's error surface — three tiers:\n * - connection-level: surfaced through `onConnectionStatus('offline')`;\n * - subscription-level: `error` frames → `SubscribeOptions.onError`;\n * - mutation-level: `mutate()` rejects with a {@link VelaLiveError}.\n *\n * Follows lunora's zero-dep catalog pattern (one error class, a small code\n * union, structural guards) inlined rather than published separately.\n */\nexport class VelaLiveError extends Error {\n constructor(\n readonly code: string,\n message: string,\n readonly status?: number,\n ) {\n super(message);\n this.name = 'VelaLiveError';\n }\n}\n\nexport const isVelaLiveError = (value: unknown): value is VelaLiveError =>\n value instanceof VelaLiveError ||\n (typeof value === 'object' &&\n value !== null &&\n (value as { name?: unknown }).name === 'VelaLiveError' &&\n typeof (value as { code?: unknown }).code === 'string');\n\nexport const getErrorCode = (value: unknown): string | undefined =>\n isVelaLiveError(value) ? value.code : undefined;\n\n/** Decode an HTTP error response into a VelaLiveError (vela's `{ error | message }` shapes tolerated). */\nexport const toMutationError = async (response: Response): Promise<VelaLiveError> => {\n let code = `http_${response.status}`;\n let message = response.statusText || `mutation failed with status ${response.status}`;\n try {\n const body = (await response.json()) as {\n error?: { code?: string; message?: string } | string;\n message?: string;\n };\n if (typeof body.error === 'object' && body.error !== null) {\n code = body.error.code ?? code;\n message = body.error.message ?? message;\n } else if (typeof body.error === 'string') {\n message = body.error;\n } else if (typeof body.message === 'string') {\n message = body.message;\n }\n } catch {\n // non-JSON body — keep the status-derived error\n }\n return new VelaLiveError(code, message, response.status);\n};\n","import { COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER } from '@velajs/live-protocol';\nimport { RoomConnection } from './connection';\nimport { toMutationError } from './errors';\nimport { applyOptimisticLayer } from './optimistic';\nimport type { CommitStamp, LayerHandle } from './optimistic';\nimport {\n argsKeyOf,\n createSubscriptionState,\n notify,\n refold,\n subscriptionKey,\n} from './subscription';\nimport type { SubscriptionState } from './subscription';\nimport type {\n ArgsOf,\n ConnectionStatus,\n HydrationEntry,\n LiveClientOptions,\n LiveContract,\n LiveStore,\n MutateOptions,\n ResultOf,\n SubscribeOptions,\n Unsubscribe,\n} from './types';\n\nconst DEFAULT_WS_PATH = '/rooms/:room/ws';\nconst DEFAULT_ROOM = 'default';\nconst DEFAULT_HEARTBEAT_MS = 30_000;\n\n/**\n * The framework-neutral Vela live client.\n *\n * ```ts\n * const client = new LiveClient<AppLive>({ url: 'https://api.example.com' });\n * const stop = client.subscribe('todos.list', { listId: 'l1' }, (todos) => render(todos));\n * await client.mutate('/todos', { text: 'hi' }, {\n * optimistic: { query: 'todos.list', args: { listId: 'l1' }, apply: (t = []) => [...t, temp] },\n * });\n * ```\n *\n * Identical `(query, args, room)` subscriptions share one wire registration;\n * reconnects resubscribe with the last cursor+epoch so untouched\n * subscriptions resume without a re-run; optimistic updates are rebaseable\n * layers gated on the mutation's `Vela-Commit-Cursor` response header.\n */\nexport class LiveClient<C extends LiveContract = LiveContract> {\n private readonly registry = new Map<string, SubscriptionState>();\n private readonly connections = new Map<string, RoomConnection>();\n private readonly statusListeners = new Set<(status: ConnectionStatus) => void>();\n private lastStatus: ConnectionStatus = 'idle';\n\n constructor(private readonly options: LiveClientOptions) {}\n\n subscribe<Q extends keyof C & string>(\n query: Q,\n args: ArgsOf<C, Q>,\n callback: (value: ResultOf<C, Q> | undefined) => void,\n subscribeOptions?: SubscribeOptions,\n ): Unsubscribe {\n const room = subscribeOptions?.room ?? this.options.defaultRoom ?? DEFAULT_ROOM;\n const key = subscriptionKey(query, argsKeyOf(args), room);\n\n let state = this.registry.get(key);\n let fresh = false;\n if (!state) {\n state = createSubscriptionState(query, args, room, subscribeOptions?.key);\n this.registry.set(key, state);\n fresh = true;\n }\n\n const cb = callback as (value: unknown) => void;\n state.callbacks.add(cb);\n if (subscribeOptions?.onError) state.errorCallbacks.add(subscribeOptions.onError);\n\n // Late joiners get the cached value synchronously (lunora parity).\n if (state.hasBase || state.layers.length > 0) cb(state.lastValue);\n\n if (fresh) this.connectionFor(room).register(state);\n\n return () => {\n state.callbacks.delete(cb);\n if (subscribeOptions?.onError) state.errorCallbacks.delete(subscribeOptions.onError);\n if (state.callbacks.size === 0) {\n this.registry.delete(key);\n this.connectionFor(room).unregister(state);\n }\n };\n }\n\n /** The current cached (folded) value — referentially stable between notifications. */\n peek<Q extends keyof C & string>(\n query: Q,\n args: ArgsOf<C, Q>,\n room?: string,\n ): ResultOf<C, Q> | undefined {\n const state = this.registry.get(\n subscriptionKey(query, argsKeyOf(args), room ?? this.options.defaultRoom ?? DEFAULT_ROOM),\n );\n return state?.lastValue as ResultOf<C, Q> | undefined;\n }\n\n /**\n * Fire an HTTP mutation. Optimistic targets paint immediately; the layer\n * drops when a subscription frame's cursor passes the response's\n * `Vela-Commit-Cursor` (missing header ⇒ one-shot optimism); a rejection\n * rolls back. Resolves with the parsed JSON body (undefined when empty).\n */\n async mutate<R = unknown>(\n path: string,\n body?: unknown,\n mutateOptions?: MutateOptions,\n ): Promise<R> {\n const handles: Array<{ state: SubscriptionState; handle: LayerHandle }> = [];\n const paint = (state: SubscriptionState, transform: (current: unknown) => unknown): void => {\n const handle = applyOptimisticLayer(state, transform);\n handles.push({ state, handle });\n if (refold(state)) notify(state);\n };\n\n if (mutateOptions?.optimistic) {\n const target = mutateOptions.optimistic;\n const state = this.registry.get(\n subscriptionKey(\n target.query,\n argsKeyOf(target.args),\n target.room ?? mutateOptions.room ?? this.options.defaultRoom ?? DEFAULT_ROOM,\n ),\n );\n // No live subscription for the target ⇒ nothing displays it: no-op.\n if (state) paint(state, target.apply as (current: unknown) => unknown);\n }\n if (mutateOptions?.optimisticUpdate) {\n mutateOptions.optimisticUpdate(this.makeStore(mutateOptions, paint));\n }\n\n try {\n const doFetch = this.options.fetch ?? fetch;\n const token = await this.options.authToken?.();\n const headers: Record<string, string> = {\n ...(body === undefined ? {} : { 'content-type': 'application/json' }),\n ...(token === undefined ? {} : { authorization: `Bearer ${token}` }),\n ...mutateOptions?.headers,\n };\n const response = await doFetch(new URL(path, this.options.url).toString(), {\n method: mutateOptions?.method ?? 'POST',\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) throw await toMutationError(response);\n\n const stamp = readCommitStamp(response);\n for (const { state, handle } of handles) {\n // confirm() reports true when the covering frame already arrived —\n // drop is immediate, re-fold now.\n if (handle.confirm(stamp) && refold(state)) notify(state);\n }\n\n if (response.status === 204 || response.status === 205) return undefined as R;\n try {\n return (await response.json()) as R;\n } catch {\n return undefined as R;\n }\n } catch (error) {\n for (const { state, handle } of handles) {\n if (handle.rollback() && refold(state)) notify(state);\n }\n throw error;\n }\n }\n\n /** Presence heartbeat for a room (see `@velajs/client/presence` for the managed preset). */\n presenceBeat(room: string, meta?: unknown): void {\n this.connectionFor(room).sendPresence(room, meta);\n }\n\n /** Seed subscription state before connecting (SSR hydration). Subscribes then resume from the seeded cursor. */\n hydrate(entries: HydrationEntry[]): void {\n for (const entry of entries) {\n const room = entry.room ?? this.options.defaultRoom ?? DEFAULT_ROOM;\n const key = subscriptionKey(entry.query, argsKeyOf(entry.args), room);\n if (this.registry.has(key)) continue;\n const state = createSubscriptionState(entry.query, entry.args, room);\n state.serverBase = entry.value;\n state.hasBase = true;\n state.lastValue = entry.value;\n state.serverCursor = entry.cursor;\n state.serverEpoch = entry.epoch;\n this.registry.set(key, state);\n }\n }\n\n connectionStatus(): ConnectionStatus {\n const statuses = [...this.connections.values()].map((connection) => connection.status);\n if (statuses.includes('connected')) return 'connected';\n if (statuses.includes('connecting')) return 'connecting';\n if (statuses.includes('offline')) return 'offline';\n if (statuses.length > 0 && statuses.every((status) => status === 'closed')) return 'closed';\n return 'idle';\n }\n\n onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe {\n this.statusListeners.add(listener);\n return () => this.statusListeners.delete(listener);\n }\n\n close(): void {\n for (const connection of this.connections.values()) connection.close();\n }\n\n private makeStore(\n mutateOptions: MutateOptions,\n paint: (state: SubscriptionState, transform: (current: unknown) => unknown) => void,\n ): LiveStore {\n const resolve = (query: string, args?: unknown, room?: string) =>\n this.registry.get(\n subscriptionKey(\n query,\n argsKeyOf(args),\n room ?? mutateOptions.room ?? this.options.defaultRoom ?? DEFAULT_ROOM,\n ),\n );\n return {\n get: (query, args, room) => resolve(query, args, room)?.lastValue,\n set: (query, args, next, room) => {\n const state = resolve(query, args, room);\n if (!state) return;\n const transform =\n typeof next === 'function' ? (next as (current: unknown) => unknown) : () => next;\n paint(state, transform);\n },\n };\n }\n\n private connectionFor(room: string): RoomConnection {\n const wsBase = (this.options.wsUrl ?? this.options.url).replace(/^http/, 'ws');\n const template = this.options.wsPath ?? DEFAULT_WS_PATH;\n const path = template.includes(':room')\n ? template.replace(':room', encodeURIComponent(room))\n : template;\n const url = new URL(path, wsBase).toString();\n\n let connection = this.connections.get(url);\n if (!connection) {\n connection = new RoomConnection(url, {\n makeSocket:\n this.options.WebSocket ??\n ((socketUrl: string) => {\n const WS = (globalThis as { WebSocket?: new (u: string) => unknown }).WebSocket;\n if (!WS) {\n throw new Error(\n 'No WebSocket implementation available — pass one via LiveClientOptions.WebSocket',\n );\n }\n return new WS(socketUrl) as never;\n }),\n authToken: this.options.authToken,\n heartbeatIntervalMs: this.options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS,\n reconnect: this.options.reconnect,\n onStatusChange: () => {\n const status = this.connectionStatus();\n if (status === this.lastStatus) return;\n this.lastStatus = status;\n for (const listener of this.statusListeners) listener(status);\n },\n });\n this.connections.set(url, connection);\n }\n return connection;\n }\n}\n\nfunction readCommitStamp(response: Response): CommitStamp | undefined {\n const cursor = response.headers.get(COMMIT_CURSOR_HEADER);\n const epoch = response.headers.get(COMMIT_EPOCH_HEADER);\n if (cursor === null || epoch === null) return undefined;\n const parsed = Number(cursor);\n return Number.isFinite(parsed) ? { cursor: parsed, epoch } : undefined;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
//#region src/optimistic.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Rebaseable, cursor-gated optimistic layers (lunora `optimistic-layers.ts`
|
|
4
|
+
* port). The displayed value is ALWAYS the authoritative server base folded
|
|
5
|
+
* through the pending layers, so:
|
|
6
|
+
*
|
|
7
|
+
* - an unrelated server frame re-folds the layers onto the new base instead of
|
|
8
|
+
* clobbering the optimistic state ("rebasing");
|
|
9
|
+
* - a layer drops only when a subscription frame arrives whose cursor passes
|
|
10
|
+
* the mutation's commit cursor (same epoch) — NEVER on HTTP-response
|
|
11
|
+
* timing, which races the broadcast;
|
|
12
|
+
* - confirm without a stamp (a non-live-aware endpoint) degrades to one-shot
|
|
13
|
+
* optimism: the layer drops silently and the next authoritative frame
|
|
14
|
+
* reconciles;
|
|
15
|
+
* - rollback removes the layer and re-folds so the failed write's effect
|
|
16
|
+
* disappears immediately.
|
|
17
|
+
*/
|
|
18
|
+
interface CommitStamp {
|
|
19
|
+
cursor: number;
|
|
20
|
+
epoch: string;
|
|
21
|
+
}
|
|
22
|
+
interface OptimisticLayer {
|
|
23
|
+
readonly id: symbol;
|
|
24
|
+
readonly transform: (current: unknown) => unknown;
|
|
25
|
+
/** Set by confirm(); the layer is dropped by the first frame whose cursor passes it. */
|
|
26
|
+
commitCursor?: number;
|
|
27
|
+
commitEpoch?: string;
|
|
28
|
+
}
|
|
29
|
+
/** The slice of subscription state the optimistic engine operates on. */
|
|
30
|
+
interface OptimisticHost {
|
|
31
|
+
layers: OptimisticLayer[];
|
|
32
|
+
serverBase: unknown;
|
|
33
|
+
hasBase: boolean;
|
|
34
|
+
serverCursor?: number;
|
|
35
|
+
serverEpoch?: string;
|
|
36
|
+
}
|
|
37
|
+
declare const foldOptimistic: (host: OptimisticHost) => unknown;
|
|
38
|
+
interface LayerHandle {
|
|
39
|
+
/**
|
|
40
|
+
* Record the mutation's commit stamp. With a stamp, the layer survives
|
|
41
|
+
* until a frame's cursor passes it (dropping immediately when the current
|
|
42
|
+
* frame already has). Without one, the layer is removed silently — no
|
|
43
|
+
* re-fold, the incoming authoritative frame reconciles.
|
|
44
|
+
*/
|
|
45
|
+
confirm(stamp?: CommitStamp): boolean;
|
|
46
|
+
/** Remove the layer and report whether the host must re-fold + notify. */
|
|
47
|
+
rollback(): boolean;
|
|
48
|
+
}
|
|
49
|
+
declare function applyOptimisticLayer(host: OptimisticHost, transform: (current: unknown) => unknown): LayerHandle;
|
|
50
|
+
/**
|
|
51
|
+
* Drop every confirmed layer the frame at (cursor, epoch) covers. A layer
|
|
52
|
+
* confirmed under a DIFFERENT epoch is also dropped — its gate can never fire
|
|
53
|
+
* on this timeline. Returns whether any layer was removed (host re-folds).
|
|
54
|
+
*/
|
|
55
|
+
declare function dropConfirmedLayers(host: OptimisticHost, cursor?: number, epoch?: string): boolean;
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/types.d.ts
|
|
58
|
+
/**
|
|
59
|
+
* The app-supplied typing contract: one entry per live query, keyed by the
|
|
60
|
+
* server's `@LiveQuery(name)`. Hand-written in v1; shaped so a future
|
|
61
|
+
* `vela codegen` emission (from OpenAPI operationIds + live metadata) is a
|
|
62
|
+
* drop-in replacement.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* interface AppLive {
|
|
66
|
+
* 'todos.list': { args: { listId: string }; result: Todo[] };
|
|
67
|
+
* }
|
|
68
|
+
* const client = new LiveClient<AppLive>({ url });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
interface LiveContract {
|
|
72
|
+
[query: string]: {
|
|
73
|
+
args: unknown;
|
|
74
|
+
result: unknown;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
type ArgsOf<C, Q extends keyof C> = C[Q] extends {
|
|
78
|
+
args: infer A;
|
|
79
|
+
} ? A : never;
|
|
80
|
+
type ResultOf<C, Q extends keyof C> = C[Q] extends {
|
|
81
|
+
result: infer R;
|
|
82
|
+
} ? R : never;
|
|
83
|
+
type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'offline' | 'closed';
|
|
84
|
+
/** The WebSocket surface the client uses — injectable for SSR/tests/non-browser runtimes. */
|
|
85
|
+
interface WebSocketLike {
|
|
86
|
+
onopen: ((event?: unknown) => void) | null;
|
|
87
|
+
onmessage: ((event: {
|
|
88
|
+
data: unknown;
|
|
89
|
+
}) => void) | null;
|
|
90
|
+
onclose: ((event?: unknown) => void) | null;
|
|
91
|
+
onerror: ((event?: unknown) => void) | null;
|
|
92
|
+
send(data: string): void;
|
|
93
|
+
close(code?: number, reason?: string): void;
|
|
94
|
+
readyState: number;
|
|
95
|
+
}
|
|
96
|
+
type WebSocketFactory = (url: string) => WebSocketLike;
|
|
97
|
+
interface ReconnectOptions {
|
|
98
|
+
/** First-retry floor (default 250 ms). */
|
|
99
|
+
baseMs?: number;
|
|
100
|
+
/** Backoff ceiling (default 30 000 ms). */
|
|
101
|
+
capMs?: number;
|
|
102
|
+
}
|
|
103
|
+
interface LiveClientOptions {
|
|
104
|
+
/** HTTP(S) base of the Vela app, e.g. `https://api.example.com`. */
|
|
105
|
+
url: string;
|
|
106
|
+
/** WS(S) base override; derived from `url` (http→ws) when omitted. */
|
|
107
|
+
wsUrl?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Gateway path template the live socket connects to. A `:room` placeholder
|
|
110
|
+
* is substituted per subscription room (matching vela's node transport,
|
|
111
|
+
* which auto-joins the `:id` route param, and Cloudflare's one-DO≈one-room
|
|
112
|
+
* model). Default `/rooms/:room/ws`.
|
|
113
|
+
*/
|
|
114
|
+
wsPath?: string;
|
|
115
|
+
/** Room used when a subscribe/mutate call names none. Default `'default'`. */
|
|
116
|
+
defaultRoom?: string;
|
|
117
|
+
/** Injectables — the core never touches globals directly (SSR/edge safety). */
|
|
118
|
+
WebSocket?: WebSocketFactory;
|
|
119
|
+
fetch?: typeof fetch;
|
|
120
|
+
/**
|
|
121
|
+
* Async token provider, re-invoked per socket connect and per mutation.
|
|
122
|
+
* Sockets carry it as a `?token=` query param (browsers cannot set WS
|
|
123
|
+
* headers — use short-lived rotating tokens); mutations as a Bearer header.
|
|
124
|
+
*/
|
|
125
|
+
authToken?: () => string | undefined | Promise<string | undefined>;
|
|
126
|
+
/** App-level `{event:'ping'}` keepalive cadence (default 30 000 ms; CF answers without waking the DO). */
|
|
127
|
+
heartbeatIntervalMs?: number;
|
|
128
|
+
reconnect?: ReconnectOptions;
|
|
129
|
+
}
|
|
130
|
+
interface SubscribeOptions {
|
|
131
|
+
room?: string;
|
|
132
|
+
/** Delta key-field override forwarded to the server (default `'id'`). */
|
|
133
|
+
key?: string;
|
|
134
|
+
onError?: (error: {
|
|
135
|
+
code: string;
|
|
136
|
+
message: string;
|
|
137
|
+
fatal: boolean;
|
|
138
|
+
}) => void;
|
|
139
|
+
}
|
|
140
|
+
/** Single-subscription optimistic target for `mutate()`. */
|
|
141
|
+
interface OptimisticTarget<T = unknown> {
|
|
142
|
+
query: string;
|
|
143
|
+
args?: unknown;
|
|
144
|
+
room?: string;
|
|
145
|
+
apply: (current: T | undefined) => T;
|
|
146
|
+
}
|
|
147
|
+
/** Multi-subscription optimistic store (the `optimisticUpdate` callback's argument). */
|
|
148
|
+
interface LiveStore {
|
|
149
|
+
get(query: string, args?: unknown, room?: string): unknown;
|
|
150
|
+
set(query: string, args: unknown, next: unknown | ((current: unknown) => unknown), room?: string): void;
|
|
151
|
+
}
|
|
152
|
+
interface MutateOptions {
|
|
153
|
+
method?: string;
|
|
154
|
+
headers?: Record<string, string>;
|
|
155
|
+
room?: string;
|
|
156
|
+
optimistic?: OptimisticTarget;
|
|
157
|
+
optimisticUpdate?: (store: LiveStore) => void;
|
|
158
|
+
}
|
|
159
|
+
type Unsubscribe = () => void;
|
|
160
|
+
/** Seed states before the socket connects (SSR hydration). */
|
|
161
|
+
interface HydrationEntry {
|
|
162
|
+
query: string;
|
|
163
|
+
args?: unknown;
|
|
164
|
+
room?: string;
|
|
165
|
+
value: unknown;
|
|
166
|
+
cursor?: number;
|
|
167
|
+
epoch?: string;
|
|
168
|
+
}
|
|
169
|
+
/** Durable offline write outbox seam — not implemented in v1 (mutations are plain HTTP). */
|
|
170
|
+
interface OutboxSink {
|
|
171
|
+
enqueue(path: string, body: unknown, options?: MutateOptions): Promise<void>;
|
|
172
|
+
onCommit?(stamp: CommitStamp): void;
|
|
173
|
+
}
|
|
174
|
+
/** Durable read-cache seam (IndexedDB, …) — in-memory only in v1. */
|
|
175
|
+
interface ReadCacheAdapter {
|
|
176
|
+
load(key: string): Promise<{
|
|
177
|
+
value: unknown;
|
|
178
|
+
cursor?: number;
|
|
179
|
+
epoch?: string;
|
|
180
|
+
} | undefined>;
|
|
181
|
+
persist(key: string, value: unknown, cursor?: number, epoch?: string): Promise<void>;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/live-client.d.ts
|
|
185
|
+
/**
|
|
186
|
+
* The framework-neutral Vela live client.
|
|
187
|
+
*
|
|
188
|
+
* ```ts
|
|
189
|
+
* const client = new LiveClient<AppLive>({ url: 'https://api.example.com' });
|
|
190
|
+
* const stop = client.subscribe('todos.list', { listId: 'l1' }, (todos) => render(todos));
|
|
191
|
+
* await client.mutate('/todos', { text: 'hi' }, {
|
|
192
|
+
* optimistic: { query: 'todos.list', args: { listId: 'l1' }, apply: (t = []) => [...t, temp] },
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* Identical `(query, args, room)` subscriptions share one wire registration;
|
|
197
|
+
* reconnects resubscribe with the last cursor+epoch so untouched
|
|
198
|
+
* subscriptions resume without a re-run; optimistic updates are rebaseable
|
|
199
|
+
* layers gated on the mutation's `Vela-Commit-Cursor` response header.
|
|
200
|
+
*/
|
|
201
|
+
declare class LiveClient<C extends LiveContract = LiveContract> {
|
|
202
|
+
private readonly options;
|
|
203
|
+
private readonly registry;
|
|
204
|
+
private readonly connections;
|
|
205
|
+
private readonly statusListeners;
|
|
206
|
+
private lastStatus;
|
|
207
|
+
constructor(options: LiveClientOptions);
|
|
208
|
+
subscribe<Q extends keyof C & string>(query: Q, args: ArgsOf<C, Q>, callback: (value: ResultOf<C, Q> | undefined) => void, subscribeOptions?: SubscribeOptions): Unsubscribe;
|
|
209
|
+
/** The current cached (folded) value — referentially stable between notifications. */
|
|
210
|
+
peek<Q extends keyof C & string>(query: Q, args: ArgsOf<C, Q>, room?: string): ResultOf<C, Q> | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Fire an HTTP mutation. Optimistic targets paint immediately; the layer
|
|
213
|
+
* drops when a subscription frame's cursor passes the response's
|
|
214
|
+
* `Vela-Commit-Cursor` (missing header ⇒ one-shot optimism); a rejection
|
|
215
|
+
* rolls back. Resolves with the parsed JSON body (undefined when empty).
|
|
216
|
+
*/
|
|
217
|
+
mutate<R = unknown>(path: string, body?: unknown, mutateOptions?: MutateOptions): Promise<R>;
|
|
218
|
+
/** Presence heartbeat for a room (see `@velajs/client/presence` for the managed preset). */
|
|
219
|
+
presenceBeat(room: string, meta?: unknown): void;
|
|
220
|
+
/** Seed subscription state before connecting (SSR hydration). Subscribes then resume from the seeded cursor. */
|
|
221
|
+
hydrate(entries: HydrationEntry[]): void;
|
|
222
|
+
connectionStatus(): ConnectionStatus;
|
|
223
|
+
onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
|
|
224
|
+
close(): void;
|
|
225
|
+
private makeStore;
|
|
226
|
+
private connectionFor;
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export { dropConfirmedLayers as C, applyOptimisticLayer as S, WebSocketLike as _, LiveClientOptions as a, OptimisticHost as b, MutateOptions as c, ReadCacheAdapter as d, ReconnectOptions as f, WebSocketFactory as g, Unsubscribe as h, HydrationEntry as i, OptimisticTarget as l, SubscribeOptions as m, ArgsOf as n, LiveContract as o, ResultOf as p, ConnectionStatus as r, LiveStore as s, LiveClient as t, OutboxSink as u, CommitStamp as v, foldOptimistic as w, OptimisticLayer as x, LayerHandle as y };
|
|
230
|
+
//# sourceMappingURL=live-client-CAb9TctR.d.ts.map
|
package/dist/presence.d.ts
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { o as LiveContract, t as LiveClient } from "./live-client-CAb9TctR.js";
|
|
2
|
+
//#region src/presence.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The built-in roster query name served by `@velajs/vela/live`'s presence
|
|
5
5
|
* preset (`PRESENCE_ROSTER_QUERY` on the server side).
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
declare const PRESENCE_ROSTER_QUERY = "$presence.roster";
|
|
8
8
|
/** One present connection, as the server's roster query returns it. */
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
interface PresenceMember {
|
|
10
|
+
id: string;
|
|
11
|
+
meta?: unknown;
|
|
12
|
+
lastSeen: number;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
interface PresenceOptions {
|
|
15
|
+
room: string;
|
|
16
|
+
/** Payload attached to this connection's roster entry (function form re-evaluated per beat). */
|
|
17
|
+
meta?: unknown | (() => unknown);
|
|
18
|
+
/** Keep WELL under the server's TTL (default beat 10 000 ms vs 30 000 ms TTL). */
|
|
19
|
+
heartbeatIntervalMs?: number;
|
|
20
|
+
onRoster?: (members: PresenceMember[]) => void;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
interface PresenceHandle {
|
|
23
|
+
/** The last received roster (undefined until the first push). */
|
|
24
|
+
roster(): PresenceMember[] | undefined;
|
|
25
|
+
/** Send an immediate heartbeat (e.g. on visibility regain — the React binding wires this). */
|
|
26
|
+
beat(): void;
|
|
27
|
+
stop(): void;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Framework-neutral presence: joins a room's roster (heartbeats over the
|
|
31
31
|
* room's live socket) and subscribes to it. Departure is immediate on socket
|
|
32
32
|
* close — the heartbeat/TTL pair only covers ungraceful drops.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
declare function createPresence(client: LiveClient<LiveContract>, options: PresenceOptions): PresenceHandle;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { PRESENCE_ROSTER_QUERY, PresenceHandle, PresenceMember, PresenceOptions, createPresence };
|
|
37
|
+
//# sourceMappingURL=presence.d.ts.map
|