@hashxltd/liveframe-vue 0.0.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/README.md +2 -0
- package/bun.lock +78 -0
- package/package.json +23 -0
- package/src/client/index.ts +17 -0
- package/src/errors/index.ts +152 -0
- package/src/fabric/index.ts +172 -0
- package/src/fabric/systembus.ts +93 -0
- package/src/index.ts +1 -0
- package/src/protocol/codec.ts +177 -0
- package/src/protocol/constants.ts +125 -0
- package/src/protocol/index.ts +3 -0
- package/src/protocol/types.ts +255 -0
- package/src/transport/index.ts +212 -0
- package/src/utils/index.ts +157 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Encodes outbound envelopes and decodes + validates inbound frames.
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
PROTOCOL_VERSION,
|
|
7
|
+
MAX_FRAME_BYTES,
|
|
8
|
+
MAX_EVENT_NAME_LENGTH,
|
|
9
|
+
} from "./constants";
|
|
10
|
+
import type { Envelope } from "./types";
|
|
11
|
+
import { FrameError, FrameErrorCode } from "../errors";
|
|
12
|
+
|
|
13
|
+
const EVENT_RE = /^[a-z][a-z0-9._-]{0,127}$/;
|
|
14
|
+
|
|
15
|
+
// ─── Encode ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Serialises an outbound envelope to a JSON string.
|
|
19
|
+
* Throws FrameError.PAYLOAD_TOO_LARGE if the result exceeds MAX_FRAME_BYTES.
|
|
20
|
+
*/
|
|
21
|
+
export function encode<P>(env: Envelope<P>): string {
|
|
22
|
+
const raw = JSON.stringify(env);
|
|
23
|
+
if (raw.length > MAX_FRAME_BYTES) {
|
|
24
|
+
throw new FrameError(
|
|
25
|
+
FrameErrorCode.PAYLOAD_TOO_LARGE,
|
|
26
|
+
`Frame size ${raw.length} B exceeds limit of ${MAX_FRAME_BYTES} B (event: ${env.event})`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Decode ───────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses and structurally validates an inbound frame.
|
|
36
|
+
* Returns the typed envelope or throws FrameError.
|
|
37
|
+
*/
|
|
38
|
+
export function decode(raw: string): Envelope {
|
|
39
|
+
let parsed: unknown;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new FrameError(
|
|
44
|
+
FrameErrorCode.MALFORMED_JSON,
|
|
45
|
+
"Frame is not valid JSON",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
50
|
+
throw new FrameError(
|
|
51
|
+
FrameErrorCode.INVALID_SHAPE,
|
|
52
|
+
"Frame must be a JSON object",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const obj = parsed as Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
// ── Version ──────────────────────────────────────────────────────────────
|
|
59
|
+
if (obj["v"] !== PROTOCOL_VERSION) {
|
|
60
|
+
throw new FrameError(
|
|
61
|
+
FrameErrorCode.VERSION_MISMATCH,
|
|
62
|
+
`Unsupported protocol version: ${obj["v"]} (expected ${PROTOCOL_VERSION})`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Required fields ───────────────────────────────────────────────────────
|
|
67
|
+
assertString(obj, "id", FrameErrorCode.MISSING_FIELD);
|
|
68
|
+
assertString(obj, "event", FrameErrorCode.MISSING_FIELD);
|
|
69
|
+
assertNumber(obj, "ts", FrameErrorCode.MISSING_FIELD);
|
|
70
|
+
|
|
71
|
+
// ── Event name format ─────────────────────────────────────────────────────
|
|
72
|
+
const event = obj["event"] as string;
|
|
73
|
+
if (event.length > MAX_EVENT_NAME_LENGTH || !EVENT_RE.test(event)) {
|
|
74
|
+
throw new FrameError(
|
|
75
|
+
FrameErrorCode.INVALID_EVENT,
|
|
76
|
+
`Invalid event name: "${event}"`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Payload type ──────────────────────────────────────────────────────────
|
|
81
|
+
if ("payload" in obj && obj["payload"] !== undefined) {
|
|
82
|
+
const p = obj["payload"];
|
|
83
|
+
if (typeof p !== "object" || p === null || Array.isArray(p)) {
|
|
84
|
+
throw new FrameError(
|
|
85
|
+
FrameErrorCode.INVALID_SHAPE,
|
|
86
|
+
`payload must be a JSON object (event: ${event})`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return obj as unknown as Envelope;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parses and structurally validates an inbound frame.
|
|
96
|
+
* Returns the typed envelope.
|
|
97
|
+
*/
|
|
98
|
+
export function decode_as<T = unknown>(raw: string): Envelope<T> {
|
|
99
|
+
let parsed: unknown;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new FrameError(
|
|
105
|
+
FrameErrorCode.MALFORMED_JSON,
|
|
106
|
+
"Frame is not valid JSON",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
111
|
+
throw new FrameError(
|
|
112
|
+
FrameErrorCode.INVALID_SHAPE,
|
|
113
|
+
"Frame must be a JSON object",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const obj = parsed as Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
// ── Version ─────────────────────────────────────────────
|
|
120
|
+
if (obj["v"] !== PROTOCOL_VERSION) {
|
|
121
|
+
throw new FrameError(
|
|
122
|
+
FrameErrorCode.VERSION_MISMATCH,
|
|
123
|
+
`Unsupported protocol version: ${obj["v"]} (expected ${PROTOCOL_VERSION})`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Required fields ──────────────────────────────────────
|
|
128
|
+
assertString(obj, "id", FrameErrorCode.MISSING_FIELD);
|
|
129
|
+
assertString(obj, "event", FrameErrorCode.MISSING_FIELD);
|
|
130
|
+
assertNumber(obj, "ts", FrameErrorCode.MISSING_FIELD);
|
|
131
|
+
|
|
132
|
+
// ── Event validation ─────────────────────────────────────
|
|
133
|
+
const event = obj["event"] as string;
|
|
134
|
+
|
|
135
|
+
if (event.length > MAX_EVENT_NAME_LENGTH || !EVENT_RE.test(event)) {
|
|
136
|
+
throw new FrameError(
|
|
137
|
+
FrameErrorCode.INVALID_EVENT,
|
|
138
|
+
`Invalid event name: "${event}"`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Payload validation ───────────────────────────────────
|
|
143
|
+
if ("payload" in obj && obj["payload"] !== undefined) {
|
|
144
|
+
const p = obj["payload"];
|
|
145
|
+
|
|
146
|
+
if (typeof p !== "object" || p === null || Array.isArray(p)) {
|
|
147
|
+
throw new FrameError(
|
|
148
|
+
FrameErrorCode.INVALID_SHAPE,
|
|
149
|
+
`payload must be a JSON object (event: ${event})`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return obj as unknown as Envelope<T>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function assertString(
|
|
160
|
+
obj: Record<string, unknown>,
|
|
161
|
+
key: string,
|
|
162
|
+
code: FrameErrorCode,
|
|
163
|
+
): void {
|
|
164
|
+
if (typeof obj[key] !== "string" || (obj[key] as string).length === 0) {
|
|
165
|
+
throw new FrameError(code, `Missing or invalid field: "${key}"`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function assertNumber(
|
|
170
|
+
obj: Record<string, unknown>,
|
|
171
|
+
key: string,
|
|
172
|
+
code: FrameErrorCode,
|
|
173
|
+
): void {
|
|
174
|
+
if (typeof obj[key] !== "number" || !Number.isFinite(obj[key] as number)) {
|
|
175
|
+
throw new FrameError(code, `Missing or invalid field: "${key}"`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Single source of truth for every magic number in the protocol.
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export const PROTOCOL_VERSION = 1 as const
|
|
6
|
+
export const MAX_FRAME_BYTES = 64 * 1024 // 64 KB
|
|
7
|
+
export const MAX_EVENT_NAME_LENGTH = 128
|
|
8
|
+
export const MAX_PENDING_ACKS = 512
|
|
9
|
+
export const DEFAULT_ACK_TIMEOUT_MS = 5_000
|
|
10
|
+
export const DEFAULT_AUTH_TIMEOUT_MS = 10_000
|
|
11
|
+
export const DEFAULT_PING_INTERVAL_MS = 25_000
|
|
12
|
+
export const DEFAULT_PING_TIMEOUT_MS = 10_000
|
|
13
|
+
export const DEFAULT_RECONNECT_BASE_MS = 250
|
|
14
|
+
export const DEFAULT_RECONNECT_CAP_MS = 30_000
|
|
15
|
+
export const TOKEN_REFRESH_LEAD_MS = 60_000 // refresh 60 s before expiry
|
|
16
|
+
export const RATE_LIMIT_CAPACITY = 100
|
|
17
|
+
export const RATE_LIMIT_WINDOW_MS = 10_000
|
|
18
|
+
|
|
19
|
+
// ─── WebSocket sub-protocol ───────────────────────────────────────────────────
|
|
20
|
+
export const WS_SUBPROTOCOL = 'liveframe.v1'
|
|
21
|
+
|
|
22
|
+
// ─── WebSocket close codes ────────────────────────────────────────────────────
|
|
23
|
+
export const CloseCodes = {
|
|
24
|
+
NORMAL: 1000,
|
|
25
|
+
GOING_AWAY: 1001,
|
|
26
|
+
POLICY_VIOLATION: 1008,
|
|
27
|
+
PROTOCOL_ERROR: 4000,
|
|
28
|
+
HEARTBEAT_TIMEOUT: 4001,
|
|
29
|
+
AUTH_TIMEOUT: 4002,
|
|
30
|
+
AUTH_FAILED: 4003,
|
|
31
|
+
RATE_LIMITED: 4004,
|
|
32
|
+
MESSAGE_TOO_LARGE: 4005,
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export type CloseCode = (typeof CloseCodes)[keyof typeof CloseCodes]
|
|
36
|
+
|
|
37
|
+
/** Close codes that MUST NOT trigger automatic reconnection. */
|
|
38
|
+
export const NO_RECONNECT_CODES = new Set<number>([
|
|
39
|
+
CloseCodes.AUTH_FAILED,
|
|
40
|
+
CloseCodes.POLICY_VIOLATION,
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
// ─── System event names ───────────────────────────────────────────────────────
|
|
44
|
+
export const SystemEvents = {
|
|
45
|
+
// Server → Client
|
|
46
|
+
CONNECTED: 'sys.connected',
|
|
47
|
+
PING: 'sys.ping',
|
|
48
|
+
PONG: 'sys.pong',
|
|
49
|
+
DISCONNECT: 'sys.disconnect',
|
|
50
|
+
RATE_LIMITED: 'sys.rate_limited',
|
|
51
|
+
|
|
52
|
+
// Auth
|
|
53
|
+
AUTH_LOGIN: 'auth.login',
|
|
54
|
+
AUTH_OK: 'auth.ok',
|
|
55
|
+
AUTH_REFRESH: 'auth.refresh',
|
|
56
|
+
AUTH_REVOKED: 'auth.revoked',
|
|
57
|
+
|
|
58
|
+
// Ack
|
|
59
|
+
ACK_OK: 'ack.ok',
|
|
60
|
+
ACK_ERROR: 'ack.error',
|
|
61
|
+
|
|
62
|
+
// Presence
|
|
63
|
+
PRESENCE_JOINED: 'presence.joined',
|
|
64
|
+
PRESENCE_LEFT: 'presence.left',
|
|
65
|
+
PRESENCE_UPDATE: 'presence.update',
|
|
66
|
+
PRESENCE_LIST: 'presence.list',
|
|
67
|
+
PRESENCE_SNAPSHOT: 'presence.snapshot',
|
|
68
|
+
|
|
69
|
+
// Errors
|
|
70
|
+
ERROR_AUTH: 'error.auth',
|
|
71
|
+
ERROR_VALIDATION: 'error.validation',
|
|
72
|
+
ERROR_ROUTING: 'error.routing',
|
|
73
|
+
ERROR_RATE_LIMIT: 'error.rate_limit',
|
|
74
|
+
ERROR_INTERNAL: 'error.internal',
|
|
75
|
+
} as const
|
|
76
|
+
|
|
77
|
+
export type SystemEvent = (typeof SystemEvents)[keyof typeof SystemEvents]
|
|
78
|
+
|
|
79
|
+
// Create a runtime set
|
|
80
|
+
const systemEventSet = new Set<string>(Object.values(SystemEvents))
|
|
81
|
+
|
|
82
|
+
export function isSystemEvent(event: string): event is SystemEvent {
|
|
83
|
+
return systemEventSet.has(event)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Application-level error codes ───────────────────────────────────────────
|
|
87
|
+
export const ErrorCodes = {
|
|
88
|
+
// Auth
|
|
89
|
+
TOKEN_EXPIRED: 'token_expired',
|
|
90
|
+
TOKEN_INVALID: 'token_invalid',
|
|
91
|
+
TOKEN_REVOKED: 'token_revoked',
|
|
92
|
+
NOT_AUTHENTICATED: 'not_authenticated',
|
|
93
|
+
SESSION_EXPIRED: 'session_expired',
|
|
94
|
+
|
|
95
|
+
// Validation
|
|
96
|
+
VALIDATION_FAILED: 'validation_failed',
|
|
97
|
+
PAYLOAD_TOO_LARGE: 'payload_too_large',
|
|
98
|
+
INVALID_EVENT: 'invalid_event',
|
|
99
|
+
MISSING_FIELD: 'missing_field',
|
|
100
|
+
|
|
101
|
+
// Routing
|
|
102
|
+
NOT_FOUND: 'not_found',
|
|
103
|
+
NOT_ALLOWED: 'not_allowed',
|
|
104
|
+
|
|
105
|
+
// Rate limiting
|
|
106
|
+
RATE_EXCEEDED: 'rate_exceeded',
|
|
107
|
+
|
|
108
|
+
// Server
|
|
109
|
+
INTERNAL: 'internal',
|
|
110
|
+
SERVICE_UNAVAILABLE: 'service_unavailable',
|
|
111
|
+
} as const
|
|
112
|
+
|
|
113
|
+
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
|
114
|
+
|
|
115
|
+
// ─── Connection states ────────────────────────────────────────────────────────
|
|
116
|
+
export const ConnectionStates = {
|
|
117
|
+
DISCONNECTED: 'DISCONNECTED',
|
|
118
|
+
CONNECTING: 'CONNECTING',
|
|
119
|
+
CONNECTED: 'CONNECTED', // WS open; sys.connected received
|
|
120
|
+
AUTHENTICATING: 'AUTHENTICATING', // auth.login sent
|
|
121
|
+
READY: 'READY', // auth.ok received; app events may flow
|
|
122
|
+
RECONNECTING: 'RECONNECTING', // backing off before retry
|
|
123
|
+
} as const
|
|
124
|
+
|
|
125
|
+
export type ConnectionState = (typeof ConnectionStates)[keyof typeof ConnectionStates]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// protocol/types.ts
|
|
3
|
+
// Every message shape the protocol defines, in one place.
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import type { PROTOCOL_VERSION } from "./constants";
|
|
7
|
+
|
|
8
|
+
// ─── Base envelope ────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Every WebSocket frame — in both directions — is wrapped in this envelope.
|
|
12
|
+
*
|
|
13
|
+
* ```json
|
|
14
|
+
* { "v":1, "id":"uuid-v4", "event":"chat.message", "payload":{...}, "ts":1710000000000, "ack":true }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface Envelope<P = unknown> {
|
|
18
|
+
/** Protocol version. MUST be 1. */
|
|
19
|
+
readonly v: typeof PROTOCOL_VERSION;
|
|
20
|
+
/** UUID v4 — unique per frame. Used for ack correlation and idempotency. */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** Dot-separated event name. Max 128 chars. Pattern: ^[a-z][a-z0-9._-]{0,127}$ */
|
|
23
|
+
readonly event: string;
|
|
24
|
+
/** Application payload. MUST be an object or omitted — never a primitive. */
|
|
25
|
+
readonly payload?: P;
|
|
26
|
+
/** Unix milliseconds (UTC) at time of send. */
|
|
27
|
+
readonly ts: number;
|
|
28
|
+
/** When true, sender requests ack.ok / ack.error in return. */
|
|
29
|
+
readonly ack?: true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Status types ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** High-level status of a delivered operation (used by ack.ok). */
|
|
35
|
+
export type OperationStatus = "delivered" | "queued" | "processed";
|
|
36
|
+
|
|
37
|
+
/** Current health status of the connection. */
|
|
38
|
+
export type HealthStatus = "healthy" | "degraded" | "reconnecting" | "offline";
|
|
39
|
+
|
|
40
|
+
// ─── System payloads ──────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface SysConnectedPayload {
|
|
43
|
+
/** Server-assigned socket identifier. */
|
|
44
|
+
readonly socket_id: string;
|
|
45
|
+
/** Informational SemVer of the server binary. */
|
|
46
|
+
readonly server_version: string;
|
|
47
|
+
/** Always 1 in this version. */
|
|
48
|
+
readonly protocol_version: number;
|
|
49
|
+
/** Client MUST send auth.login within this window (ms). */
|
|
50
|
+
readonly auth_timeout_ms: number;
|
|
51
|
+
/** How often client should send sys.ping (ms). */
|
|
52
|
+
readonly ping_interval_ms: number;
|
|
53
|
+
/** How long server will wait for pong (ms). */
|
|
54
|
+
readonly ping_timeout_ms: number;
|
|
55
|
+
/** Server node that accepted this connection. */
|
|
56
|
+
readonly node_id?: string;
|
|
57
|
+
/** Supported features the server advertises. */
|
|
58
|
+
readonly features?: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SysPingPayload {
|
|
62
|
+
/** Timestamp from the sender for RTT calculation. */
|
|
63
|
+
readonly sent_ts: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SysPongPayload {
|
|
67
|
+
/** Mirrors the ping's sent_ts. */
|
|
68
|
+
readonly sent_ts: number;
|
|
69
|
+
/** Server-side processing time in ms. */
|
|
70
|
+
readonly server_ms?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SysDisconnectPayload {
|
|
74
|
+
/** Machine-readable reason code. */
|
|
75
|
+
readonly code: string;
|
|
76
|
+
/** Human-readable message. */
|
|
77
|
+
readonly message?: string;
|
|
78
|
+
/** Minimum ms to wait before reconnecting. */
|
|
79
|
+
readonly retry_after_ms?: number;
|
|
80
|
+
/** Whether the client should attempt to reconnect at all. */
|
|
81
|
+
readonly permanent?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SysRateLimitedPayload {
|
|
85
|
+
/** How long to wait before resuming sends (ms). */
|
|
86
|
+
readonly retry_after_ms: number;
|
|
87
|
+
/** The limit that was breached, e.g. "100/10s". */
|
|
88
|
+
readonly limit: string;
|
|
89
|
+
/** How many tokens remain. */
|
|
90
|
+
readonly remaining?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Auth payloads ────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export interface AuthLoginPayload {
|
|
96
|
+
readonly token: string;
|
|
97
|
+
readonly token_type: "bearer";
|
|
98
|
+
/** Optional device fingerprint for audit logs. */
|
|
99
|
+
readonly device_id?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AuthOkPayload {
|
|
103
|
+
readonly user_id: string;
|
|
104
|
+
readonly socket_id: string;
|
|
105
|
+
/** Unix ms when the session token expires. */
|
|
106
|
+
readonly expires_at: number;
|
|
107
|
+
/** Granted scopes. */
|
|
108
|
+
readonly scopes?: readonly string[];
|
|
109
|
+
/** Arbitrary per-user metadata the server attaches. */
|
|
110
|
+
readonly meta?: Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface AuthRefreshPayload {
|
|
114
|
+
readonly token: string;
|
|
115
|
+
readonly token_type: "bearer";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AuthRevokedPayload {
|
|
119
|
+
/** Why the session was revoked. */
|
|
120
|
+
readonly reason: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Ack payloads ─────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface AckOkPayload {
|
|
126
|
+
/** The id of the frame being acknowledged. */
|
|
127
|
+
readonly ref_id: string;
|
|
128
|
+
/** Server-side processing latency (ms). */
|
|
129
|
+
readonly latency_ms: number;
|
|
130
|
+
/** High-level delivery status. */
|
|
131
|
+
readonly status: OperationStatus;
|
|
132
|
+
/** Optional data the server attaches to the ack (e.g. persisted record ID). */
|
|
133
|
+
readonly data?: Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface AckErrorPayload {
|
|
137
|
+
readonly ref_id: string;
|
|
138
|
+
readonly code: string;
|
|
139
|
+
readonly message: string;
|
|
140
|
+
/** Field-level validation details. */
|
|
141
|
+
readonly details?: ReadonlyArray<{ field: string; message: string }>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Error payloads ───────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export interface ErrorPayload {
|
|
147
|
+
readonly code: string;
|
|
148
|
+
readonly message: string;
|
|
149
|
+
/** The id of the frame that caused this error, if applicable. */
|
|
150
|
+
readonly ref_id?: string;
|
|
151
|
+
/** Field-level validation errors. */
|
|
152
|
+
readonly details?: ReadonlyArray<{ field: string; message: string }>;
|
|
153
|
+
/** Machine-readable hint for the client. */
|
|
154
|
+
readonly hint?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Presence payloads ────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export interface PresenceInfo {
|
|
160
|
+
readonly socket_id: string;
|
|
161
|
+
readonly user_id: string;
|
|
162
|
+
readonly node_id: string;
|
|
163
|
+
readonly connect_at: number;
|
|
164
|
+
/** Custom attributes set by the server (display_name, avatar, role, …). */
|
|
165
|
+
readonly meta?: Record<string, unknown>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface PresenceLeftPayload {
|
|
169
|
+
readonly socket_id: string;
|
|
170
|
+
readonly user_id: string;
|
|
171
|
+
readonly disconnect_at: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PresenceUpdatePayload extends PresenceInfo {
|
|
175
|
+
readonly changed_fields: readonly string[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface PresenceListPayload {
|
|
179
|
+
/** Optional scope (room, channel) to filter by. */
|
|
180
|
+
readonly scope?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface PresenceSnapshotPayload {
|
|
184
|
+
/** Mirrors the id of the presence.list request. */
|
|
185
|
+
readonly request_id: string;
|
|
186
|
+
readonly count: number;
|
|
187
|
+
readonly users: readonly PresenceInfo[];
|
|
188
|
+
readonly scope?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Typed envelope aliases ───────────────────────────────────────────────────
|
|
192
|
+
// Convenient aliases used internally and re-exported for consumers.
|
|
193
|
+
|
|
194
|
+
export type SysConnectedEnvelope = Envelope<SysConnectedPayload>;
|
|
195
|
+
export type SysPingEnvelope = Envelope<SysPingPayload>;
|
|
196
|
+
export type SysPongEnvelope = Envelope<SysPongPayload>;
|
|
197
|
+
export type SysDisconnectEnvelope = Envelope<SysDisconnectPayload>;
|
|
198
|
+
export type SysRateLimitedEnvelope = Envelope<SysRateLimitedPayload>;
|
|
199
|
+
export type AuthOkEnvelope = Envelope<AuthOkPayload>;
|
|
200
|
+
export type AuthRevokedEnvelope = Envelope<AuthRevokedPayload>;
|
|
201
|
+
export type AckOkEnvelope = Envelope<AckOkPayload>;
|
|
202
|
+
export type AckErrorEnvelope = Envelope<AckErrorPayload>;
|
|
203
|
+
export type ErrorEnvelope = Envelope<ErrorPayload>;
|
|
204
|
+
|
|
205
|
+
// ─── Client config ────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export interface LiveFrameClientConfig {
|
|
208
|
+
/** WebSocket URL. MUST use wss:// in production. */
|
|
209
|
+
readonly url: string;
|
|
210
|
+
/** Called to supply the initial Bearer token. */
|
|
211
|
+
// ? readonly getToken: () => Promise<string>
|
|
212
|
+
/** Called before token expiry. Defaults to getToken. */
|
|
213
|
+
// ? readonly refreshToken?: () => Promise<string>
|
|
214
|
+
/** Device fingerprint included in auth.login (optional). */
|
|
215
|
+
readonly deviceId?: string;
|
|
216
|
+
/** ms to wait for auth.ok. Default: 10 000. */
|
|
217
|
+
// ? readonly authTimeoutMs?: number
|
|
218
|
+
/** ms to wait for ack.ok. Default: 5 000. */
|
|
219
|
+
// ? readonly ackTimeoutMs?: number
|
|
220
|
+
/** 0 = unlimited reconnect attempts. Default: 0. */
|
|
221
|
+
readonly maxReconnectAttempts?: number;
|
|
222
|
+
/** Base delay for exponential backoff. Default: 250. */
|
|
223
|
+
readonly reconnectBaseMs?: number;
|
|
224
|
+
/** Maximum backoff delay. Default: 30 000. */
|
|
225
|
+
readonly reconnectCapMs?: number;
|
|
226
|
+
/** Called on every state transition. */
|
|
227
|
+
readonly onStateChange?: (
|
|
228
|
+
state: import("./constants").ConnectionState,
|
|
229
|
+
prev: import("./constants").ConnectionState,
|
|
230
|
+
) => void;
|
|
231
|
+
/** Called on every received envelope after internal handling. */
|
|
232
|
+
readonly onMessage?: (env: Envelope) => void;
|
|
233
|
+
/** Called on unrecoverable errors (bad auth, max retries). */
|
|
234
|
+
readonly onFatalError?: (reason: string, code?: number) => void;
|
|
235
|
+
/** Custom logger. Defaults to console. */
|
|
236
|
+
readonly logger?: Logger;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface Logger {
|
|
240
|
+
debug(message: string, ...args: unknown[]): void;
|
|
241
|
+
info(message: string, ...args: unknown[]): void;
|
|
242
|
+
warn(message: string, ...args: unknown[]): void;
|
|
243
|
+
error(message: string, ...args: unknown[]): void;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── Send options ─────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
export interface SendOptions {
|
|
249
|
+
/** Request server-side acknowledgement. Default: false. */
|
|
250
|
+
readonly ack?: boolean;
|
|
251
|
+
/** Override the default ack timeout (ms). */
|
|
252
|
+
readonly ackTimeoutMs?: number;
|
|
253
|
+
/** Idempotency key. If set, retries with the same key are deduplicated. */
|
|
254
|
+
readonly idempotencyKey?: string;
|
|
255
|
+
}
|