@openlivesync/client 1.0.2 → 1.0.3
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 +15 -5
- package/dist/{chunk-MINJGWLX.js → chunk-TAMQSVU2.js} +14 -4
- package/dist/chunk-TAMQSVU2.js.map +1 -0
- package/dist/index-HY9A86VV.d.ts +196 -0
- package/dist/index.d.ts +1 -184
- package/dist/index.js +1 -1
- package/dist/react-entry.d.ts +6 -2
- package/dist/react-entry.js +23 -7
- package/dist/react-entry.js.map +1 -1
- package/package.json +8 -3
- package/src/client.test.js +147 -0
- package/src/client.ts +25 -5
- package/src/protocol.ts +4 -0
- package/src/react-entry.test.tsx +39 -0
- package/src/react-entry.tsx +28 -7
- package/dist/chunk-MINJGWLX.js.map +0 -1
package/README.md
CHANGED
|
@@ -27,9 +27,10 @@ const client = createLiveSyncClient({
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
client.connect();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
30
|
+
// Join with manual name/email (no token)
|
|
31
|
+
client.joinRoom("room1", { color: "#00f" }, { name: "Alice", email: "alice@example.com" });
|
|
32
|
+
// Or join with an access token so the server decodes name/email/provider (OAuth / OpenID)
|
|
33
|
+
// client.joinRoom("room1", { color: "#00f" }, { accessToken });
|
|
33
34
|
client.subscribe((state) => console.log(state.presence));
|
|
34
35
|
|
|
35
36
|
client.updatePresence({ cursor: { x: 10, y: 20 } });
|
|
@@ -97,14 +98,23 @@ client.connect();
|
|
|
97
98
|
### Core (`@openlivesync/client`)
|
|
98
99
|
|
|
99
100
|
- **`createLiveSyncClient(config)`** — Returns a client. Options: `url`, `reconnect?`, `reconnectIntervalMs?`, `maxReconnectIntervalMs?`, `getAuthToken?`, `presenceThrottleMs?`.
|
|
100
|
-
- **Client methods**: `connect()`, `disconnect()`, `joinRoom(roomId, presence?,
|
|
101
|
+
- **Client methods**: `connect()`, `disconnect()`, `joinRoom(roomId, presence?, identity?)`, `leaveRoom(roomId?)`, `updatePresence(presence)`, `broadcastEvent(event, payload?)`, `sendChat(message, metadata?)`, `getState()`, `subscribe(listener)`.
|
|
102
|
+
- **`identity`** is `{ accessToken?, name?, email? }`.
|
|
103
|
+
- If you pass an **`accessToken`**, the server (when configured with `auth`) decodes it and attaches `name`, `email`, and `provider` to your connection; other clients see them in presence.
|
|
104
|
+
- If you pass only **`name`/`email`** (no token), the server uses those values directly and shares them with other participants via `PresenceEntry`.
|
|
105
|
+
- If you pass both, the token takes priority (decoded claims win); if decoding fails, the server falls back to the provided `name`/`email`.
|
|
106
|
+
- **Authenticate once at connect (recommended):** use only `getAuthToken` in config (token is sent in the URL at connect) and do **not** pass `accessToken` to `joinRoom` or `useRoom`; the server will recognize you for the connection lifetime.
|
|
101
107
|
|
|
102
108
|
### React (`@openlivesync/client/react`)
|
|
103
109
|
|
|
104
110
|
- **`LiveSyncProvider`** — Props: `client?` or `url?` (+ optional reconnect/auth/presence options). If `url` is provided, the provider creates the client and connects on mount.
|
|
105
111
|
- **`useLiveSyncClient()`** — Returns the client from context.
|
|
106
112
|
- **`useConnectionStatus()`** — Returns `"connecting" | "open" | "closing" | "closed"`.
|
|
107
|
-
- **`useRoom(roomId, options?)`** — Returns `{ join, leave, updatePresence, broadcastEvent, presence, connectionId, isInRoom, currentRoomId }`.
|
|
113
|
+
- **`useRoom(roomId, options?)`** — Returns `{ join, leave, updatePresence, broadcastEvent, presence, connectionId, isInRoom, currentRoomId }`.
|
|
114
|
+
- **Options**: `initialPresence?`, `autoJoin?`, `accessToken?`, `getAccessToken?`, `name?`, `email?`.
|
|
115
|
+
- With `autoJoin: true` (default), joins when `roomId` is set using an identity built from `{ accessToken (or getAccessToken()), name, email }` and leaves on unmount or when `roomId` changes.
|
|
116
|
+
- `join(roomId, presence?, identity?)` for manual join, where `identity` is `{ accessToken?, name?, email? }`.
|
|
117
|
+
- For connect-only auth (token sent once at connect via provider's `getAuthToken`), omit `accessToken`, `getAccessToken`, and `identity.accessToken` here and rely on the connection identity established at upgrade.
|
|
108
118
|
- **`usePresence(roomId)`** — Returns the presence map for the current room.
|
|
109
119
|
- **`useChat(roomId)`** — Returns `{ messages, sendMessage }`.
|
|
110
120
|
|
|
@@ -40,6 +40,7 @@ function createLiveSyncClient(config) {
|
|
|
40
40
|
let intentionalClose = false;
|
|
41
41
|
let lastPresenceUpdate = 0;
|
|
42
42
|
let pendingPresence = null;
|
|
43
|
+
let lastJoinIdentity = null;
|
|
43
44
|
const state = {
|
|
44
45
|
connectionStatus: "closed",
|
|
45
46
|
currentRoomId: null,
|
|
@@ -159,7 +160,13 @@ function createLiveSyncClient(config) {
|
|
|
159
160
|
const roomId = state.currentRoomId;
|
|
160
161
|
if (!roomId) return;
|
|
161
162
|
const presence = pendingPresence ?? (state.connectionId ? state.presence[state.connectionId]?.presence : void 0);
|
|
162
|
-
|
|
163
|
+
const identity = lastJoinIdentity;
|
|
164
|
+
const payload = { roomId };
|
|
165
|
+
if (presence !== void 0) payload.presence = presence;
|
|
166
|
+
if (identity?.accessToken !== void 0) payload.accessToken = identity.accessToken;
|
|
167
|
+
if (identity?.name !== void 0) payload.name = identity.name;
|
|
168
|
+
if (identity?.email !== void 0) payload.email = identity.email;
|
|
169
|
+
send({ type: MSG_JOIN_ROOM, payload });
|
|
163
170
|
}
|
|
164
171
|
function connect() {
|
|
165
172
|
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
|
|
@@ -226,7 +233,7 @@ function createLiveSyncClient(config) {
|
|
|
226
233
|
}
|
|
227
234
|
setStatus("closed");
|
|
228
235
|
}
|
|
229
|
-
function joinRoom(roomId, presence,
|
|
236
|
+
function joinRoom(roomId, presence, identity) {
|
|
230
237
|
if (state.currentRoomId) {
|
|
231
238
|
send({ type: MSG_LEAVE_ROOM, payload: { roomId: state.currentRoomId } });
|
|
232
239
|
}
|
|
@@ -234,9 +241,12 @@ function createLiveSyncClient(config) {
|
|
|
234
241
|
state.presence = {};
|
|
235
242
|
state.chatMessages = [];
|
|
236
243
|
pendingPresence = presence ?? null;
|
|
244
|
+
lastJoinIdentity = identity ?? null;
|
|
237
245
|
const payload = { roomId };
|
|
238
246
|
if (presence !== void 0) payload.presence = presence;
|
|
239
|
-
if (accessToken !== void 0) payload.accessToken = accessToken;
|
|
247
|
+
if (identity?.accessToken !== void 0) payload.accessToken = identity.accessToken;
|
|
248
|
+
if (identity?.name !== void 0) payload.name = identity.name;
|
|
249
|
+
if (identity?.email !== void 0) payload.email = identity.email;
|
|
240
250
|
send({ type: MSG_JOIN_ROOM, payload });
|
|
241
251
|
emit();
|
|
242
252
|
}
|
|
@@ -308,4 +318,4 @@ export {
|
|
|
308
318
|
MSG_ERROR,
|
|
309
319
|
createLiveSyncClient
|
|
310
320
|
};
|
|
311
|
-
//# sourceMappingURL=chunk-
|
|
321
|
+
//# sourceMappingURL=chunk-TAMQSVU2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol.ts","../src/client.ts"],"sourcesContent":["/**\n * Wire protocol types for @openlivesync/client.\n * Must stay in sync with @openlivesync/server protocol (same message types and payload shapes).\n */\n\n/** Generic presence payload (cursor, name, color, etc.). Server does not interpret. */\nexport type Presence = Record<string, unknown>;\n\n/** User/session info attached by server from auth (optional). */\nexport interface UserInfo {\n userId?: string;\n name?: string;\n email?: string;\n provider?: string;\n [key: string]: unknown;\n}\n\n// ----- Client → Server message types -----\n\nexport const MSG_JOIN_ROOM = \"join_room\";\nexport const MSG_LEAVE_ROOM = \"leave_room\";\nexport const MSG_UPDATE_PRESENCE = \"update_presence\";\nexport const MSG_BROADCAST_EVENT = \"broadcast_event\";\nexport const MSG_SEND_CHAT = \"send_chat\";\n\nexport interface JoinRoomPayload {\n roomId: string;\n presence?: Presence;\n /** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */\n accessToken?: string;\n /** Optional display name if not using accessToken. */\n name?: string;\n /** Optional email if not using accessToken. */\n email?: string;\n}\n\nexport interface LeaveRoomPayload {\n roomId?: string;\n}\n\nexport interface UpdatePresencePayload {\n presence: Presence;\n}\n\nexport interface BroadcastEventPayload {\n event: string;\n payload?: unknown;\n}\n\nexport interface SendChatPayload {\n message: string;\n /** Optional application-defined metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport type ClientMessage =\n | { type: typeof MSG_JOIN_ROOM; payload: JoinRoomPayload }\n | { type: typeof MSG_LEAVE_ROOM; payload?: LeaveRoomPayload }\n | { type: typeof MSG_UPDATE_PRESENCE; payload: UpdatePresencePayload }\n | { type: typeof MSG_BROADCAST_EVENT; payload: BroadcastEventPayload }\n | { type: typeof MSG_SEND_CHAT; payload: SendChatPayload };\n\n// ----- Server → Client message types -----\n\nexport const MSG_ROOM_JOINED = \"room_joined\";\nexport const MSG_PRESENCE_UPDATED = \"presence_updated\";\nexport const MSG_BROADCAST_EVENT_RELAY = \"broadcast_event\";\nexport const MSG_CHAT_MESSAGE = \"chat_message\";\nexport const MSG_ERROR = \"error\";\n\nexport interface PresenceEntry {\n connectionId: string;\n userId?: string;\n name?: string;\n email?: string;\n provider?: string;\n presence: Presence;\n}\n\nexport interface RoomJoinedPayload {\n roomId: string;\n connectionId: string;\n presence: Record<string, PresenceEntry>;\n chatHistory?: StoredChatMessage[];\n}\n\nexport interface PresenceUpdatedPayload {\n roomId: string;\n joined?: PresenceEntry[];\n left?: string[];\n updated?: PresenceEntry[];\n}\n\nexport interface BroadcastEventRelayPayload {\n roomId: string;\n connectionId: string;\n userId?: string;\n event: string;\n payload?: unknown;\n}\n\nexport interface ChatMessagePayload {\n roomId: string;\n connectionId: string;\n userId?: string;\n message: string;\n metadata?: Record<string, unknown>;\n id?: string;\n createdAt?: number;\n}\n\nexport interface StoredChatMessage {\n id: string;\n roomId: string;\n connectionId: string;\n userId?: string;\n message: string;\n metadata?: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface ErrorPayload {\n code: string;\n message: string;\n}\n\nexport type ServerMessage =\n | { type: typeof MSG_ROOM_JOINED; payload: RoomJoinedPayload }\n | { type: typeof MSG_PRESENCE_UPDATED; payload: PresenceUpdatedPayload }\n | { type: typeof MSG_BROADCAST_EVENT_RELAY; payload: BroadcastEventRelayPayload }\n | { type: typeof MSG_CHAT_MESSAGE; payload: ChatMessagePayload }\n | { type: typeof MSG_ERROR; payload: ErrorPayload };\n\n/** Chat message as provided when appending (before storage adds id/createdAt). */\nexport interface ChatMessageInput {\n roomId: string;\n connectionId: string;\n userId?: string;\n message: string;\n metadata?: Record<string, unknown>;\n}\n","/**\n * Core WebSocket client for @openlivesync.\n * Connects to @openlivesync/server, manages room/presence/chat state, and notifies subscribers.\n */\n\nimport type {\n ClientMessage,\n ServerMessage,\n Presence,\n PresenceEntry,\n StoredChatMessage,\n JoinRoomPayload,\n} from \"./protocol.js\";\nimport {\n MSG_JOIN_ROOM,\n MSG_LEAVE_ROOM,\n MSG_UPDATE_PRESENCE,\n MSG_BROADCAST_EVENT,\n MSG_SEND_CHAT,\n MSG_ROOM_JOINED,\n MSG_PRESENCE_UPDATED,\n MSG_BROADCAST_EVENT_RELAY,\n MSG_CHAT_MESSAGE,\n MSG_ERROR,\n} from \"./protocol.js\";\n\nexport type ConnectionStatus = \"connecting\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface LiveSyncClientState {\n connectionStatus: ConnectionStatus;\n currentRoomId: string | null;\n connectionId: string | null;\n presence: Record<string, PresenceEntry>;\n chatMessages: StoredChatMessage[];\n lastError: { code: string; message: string } | null;\n}\n\nexport interface JoinRoomIdentity {\n /** Optional display name if not using accessToken. */\n name?: string;\n /** Optional email if not using accessToken. */\n email?: string;\n /** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */\n accessToken?: string;\n}\n\nexport interface LiveSyncClientConfig {\n /** WebSocket URL (e.g. wss://host/live). */\n url: string;\n /** Auto-reconnect on close (default true). */\n reconnect?: boolean;\n /** Initial reconnect delay in ms (default 1000). */\n reconnectIntervalMs?: number;\n /** Max reconnect delay in ms (default 30000). */\n maxReconnectIntervalMs?: number;\n /** Optional: return token for auth; appended as query param (e.g. ?access_token=). */\n getAuthToken?: () => string | Promise<string>;\n /** Throttle presence updates in ms (default 100, match server). */\n presenceThrottleMs?: number;\n}\n\nconst DEFAULT_RECONNECT_INTERVAL_MS = 1000;\nconst DEFAULT_MAX_RECONNECT_INTERVAL_MS = 30000;\nconst DEFAULT_PRESENCE_THROTTLE_MS = 100;\n\nfunction isServerMessage(msg: unknown): msg is ServerMessage {\n if (msg === null || typeof msg !== \"object\" || !(\"type\" in msg)) return false;\n const t = (msg as { type: string }).type;\n return [\n MSG_ROOM_JOINED,\n MSG_PRESENCE_UPDATED,\n MSG_BROADCAST_EVENT_RELAY,\n MSG_CHAT_MESSAGE,\n MSG_ERROR,\n ].includes(t);\n}\n\nexport interface LiveSyncClient {\n connect(): void;\n disconnect(): void;\n joinRoom(roomId: string, presence?: Presence, identity?: JoinRoomIdentity): void;\n leaveRoom(roomId?: string): void;\n updatePresence(presence: Presence): void;\n broadcastEvent(event: string, payload?: unknown): void;\n sendChat(message: string, metadata?: Record<string, unknown>): void;\n getConnectionStatus(): ConnectionStatus;\n getPresence(): Record<string, PresenceEntry>;\n getChatMessages(): StoredChatMessage[];\n getCurrentRoomId(): string | null;\n getState(): LiveSyncClientState;\n subscribe(listener: (state: LiveSyncClientState) => void): () => void;\n}\n\nexport function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClient {\n const {\n url: baseUrl,\n reconnect: reconnectEnabled = true,\n reconnectIntervalMs = DEFAULT_RECONNECT_INTERVAL_MS,\n maxReconnectIntervalMs = DEFAULT_MAX_RECONNECT_INTERVAL_MS,\n getAuthToken,\n presenceThrottleMs = DEFAULT_PRESENCE_THROTTLE_MS,\n } = config;\n\n let ws: WebSocket | null = null;\n let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null;\n let nextReconnectMs = reconnectIntervalMs;\n let intentionalClose = false;\n let lastPresenceUpdate = 0;\n let pendingPresence: Presence | null = null;\n let lastJoinIdentity: JoinRoomIdentity | null = null;\n\n const state: LiveSyncClientState = {\n connectionStatus: \"closed\",\n currentRoomId: null,\n connectionId: null,\n presence: {},\n chatMessages: [],\n lastError: null,\n };\n\n const listeners = new Set<(s: LiveSyncClientState) => void>();\n\n function emit() {\n const snapshot: LiveSyncClientState = {\n connectionStatus: state.connectionStatus,\n currentRoomId: state.currentRoomId,\n connectionId: state.connectionId,\n presence: { ...state.presence },\n chatMessages: [...state.chatMessages],\n lastError: state.lastError ? { ...state.lastError } : null,\n };\n listeners.forEach((cb) => cb(snapshot));\n }\n\n function setStatus(status: ConnectionStatus) {\n state.connectionStatus = status;\n emit();\n }\n\n function send(msg: ClientMessage) {\n if (!ws || ws.readyState !== WebSocket.OPEN) return;\n ws.send(JSON.stringify(msg));\n }\n\n function clearReconnect() {\n if (reconnectTimeoutId !== null) {\n clearTimeout(reconnectTimeoutId);\n reconnectTimeoutId = null;\n }\n nextReconnectMs = reconnectIntervalMs;\n }\n\n function scheduleReconnect() {\n if (!reconnectEnabled || intentionalClose) return;\n clearReconnect();\n reconnectTimeoutId = setTimeout(() => {\n reconnectTimeoutId = null;\n nextReconnectMs = Math.min(\n nextReconnectMs * 2,\n maxReconnectIntervalMs\n );\n connect();\n }, nextReconnectMs);\n }\n\n function applyPresenceUpdated(\n joined?: PresenceEntry[],\n left?: string[],\n updated?: PresenceEntry[]\n ) {\n if (joined) {\n for (const e of joined) state.presence[e.connectionId] = e;\n }\n if (left) {\n for (const id of left) delete state.presence[id];\n }\n if (updated) {\n for (const e of updated) state.presence[e.connectionId] = e;\n }\n emit();\n }\n\n function handleMessage(data: string) {\n let msg: unknown;\n try {\n msg = JSON.parse(data) as unknown;\n } catch {\n state.lastError = { code: \"INVALID_JSON\", message: \"Invalid JSON from server\" };\n emit();\n return;\n }\n if (!isServerMessage(msg)) {\n state.lastError = { code: \"UNKNOWN_MESSAGE\", message: \"Unknown message type\" };\n emit();\n return;\n }\n switch (msg.type) {\n case MSG_ROOM_JOINED: {\n const { roomId, connectionId, presence, chatHistory } = msg.payload;\n state.currentRoomId = roomId;\n state.connectionId = connectionId;\n state.presence = presence ?? {};\n state.chatMessages = chatHistory ?? [];\n state.lastError = null;\n emit();\n break;\n }\n case MSG_PRESENCE_UPDATED: {\n const { joined, left, updated } = msg.payload;\n applyPresenceUpdated(joined, left, updated);\n break;\n }\n case MSG_BROADCAST_EVENT_RELAY:\n // Application can subscribe to custom events if we add an event emitter; for now we only update state for presence/chat.\n break;\n case MSG_CHAT_MESSAGE: {\n const p = msg.payload;\n const stored: StoredChatMessage = {\n id: p.id ?? `${p.connectionId}-${p.createdAt ?? Date.now()}`,\n roomId: p.roomId,\n connectionId: p.connectionId,\n userId: p.userId,\n message: p.message,\n metadata: p.metadata,\n createdAt: p.createdAt ?? Date.now(),\n };\n if (state.currentRoomId === p.roomId) {\n state.chatMessages = [...state.chatMessages, stored];\n emit();\n }\n break;\n }\n case MSG_ERROR: {\n state.lastError = msg.payload;\n emit();\n break;\n }\n }\n }\n\n function reconnectAndRejoin() {\n const roomId = state.currentRoomId;\n if (!roomId) return;\n const presence = pendingPresence ?? (state.connectionId ? state.presence[state.connectionId]?.presence : undefined);\n const identity = lastJoinIdentity;\n const payload: JoinRoomPayload = { roomId };\n if (presence !== undefined) payload.presence = presence;\n if (identity?.accessToken !== undefined) payload.accessToken = identity.accessToken;\n if (identity?.name !== undefined) payload.name = identity.name;\n if (identity?.email !== undefined) payload.email = identity.email;\n send({ type: MSG_JOIN_ROOM, payload });\n }\n\n function connect() {\n if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {\n return;\n }\n intentionalClose = false;\n setStatus(\"connecting\");\n\n (async () => {\n let url = baseUrl;\n if (getAuthToken) {\n try {\n const token = await getAuthToken();\n if (token) {\n const sep = baseUrl.includes(\"?\") ? \"&\" : \"?\";\n url = `${baseUrl}${sep}access_token=${encodeURIComponent(token)}`;\n }\n } catch (e) {\n state.lastError = {\n code: \"AUTH_ERROR\",\n message: e instanceof Error ? e.message : String(e),\n };\n setStatus(\"closed\");\n emit();\n return;\n }\n }\n\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n setStatus(\"open\");\n nextReconnectMs = reconnectIntervalMs;\n reconnectAndRejoin();\n };\n\n ws.onmessage = (event) => {\n const data = typeof event.data === \"string\" ? event.data : event.data.toString();\n handleMessage(data);\n };\n\n ws.onclose = () => {\n ws = null;\n setStatus(\"closed\");\n if (!intentionalClose) {\n scheduleReconnect();\n } else {\n clearReconnect();\n }\n };\n\n ws.onerror = () => {\n state.lastError = { code: \"WEBSOCKET_ERROR\", message: \"WebSocket error\" };\n emit();\n };\n })();\n }\n\n function disconnect() {\n intentionalClose = true;\n clearReconnect();\n state.currentRoomId = null;\n state.connectionId = null;\n state.presence = {};\n state.chatMessages = [];\n pendingPresence = null;\n if (ws) {\n setStatus(\"closing\");\n ws.close();\n ws = null;\n }\n setStatus(\"closed\");\n }\n\n function joinRoom(roomId: string, presence?: Presence, identity?: JoinRoomIdentity) {\n if (state.currentRoomId) {\n send({ type: MSG_LEAVE_ROOM, payload: { roomId: state.currentRoomId } });\n }\n state.currentRoomId = roomId;\n state.presence = {};\n state.chatMessages = [];\n pendingPresence = presence ?? null;\n lastJoinIdentity = identity ?? null;\n const payload: JoinRoomPayload = { roomId };\n if (presence !== undefined) payload.presence = presence;\n if (identity?.accessToken !== undefined) payload.accessToken = identity.accessToken;\n if (identity?.name !== undefined) payload.name = identity.name;\n if (identity?.email !== undefined) payload.email = identity.email;\n send({ type: MSG_JOIN_ROOM, payload });\n emit();\n }\n\n function leaveRoom(roomId?: string) {\n const target = roomId ?? state.currentRoomId;\n if (target) {\n send({ type: MSG_LEAVE_ROOM, payload: { roomId: target } });\n if (target === state.currentRoomId) {\n state.currentRoomId = null;\n state.connectionId = null;\n state.presence = {};\n state.chatMessages = [];\n pendingPresence = null;\n }\n emit();\n }\n }\n\n function updatePresence(presence: Presence) {\n pendingPresence = presence;\n const now = Date.now();\n if (now - lastPresenceUpdate < presenceThrottleMs) return;\n lastPresenceUpdate = now;\n send({ type: MSG_UPDATE_PRESENCE, payload: { presence } });\n }\n\n function broadcastEvent(event: string, payload?: unknown) {\n send({ type: MSG_BROADCAST_EVENT, payload: { event, payload } });\n }\n\n function sendChat(message: string, metadata?: Record<string, unknown>) {\n send({ type: MSG_SEND_CHAT, payload: { message, metadata } });\n }\n\n function subscribe(listener: (state: LiveSyncClientState) => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n }\n\n return {\n connect,\n disconnect,\n joinRoom,\n leaveRoom,\n updatePresence,\n broadcastEvent,\n sendChat,\n getConnectionStatus: () => state.connectionStatus,\n getPresence: () => ({ ...state.presence }),\n getChatMessages: () => [...state.chatMessages],\n getCurrentRoomId: () => state.currentRoomId,\n getState: () => ({\n connectionStatus: state.connectionStatus,\n currentRoomId: state.currentRoomId,\n connectionId: state.connectionId,\n presence: { ...state.presence },\n chatMessages: [...state.chatMessages],\n lastError: state.lastError ? { ...state.lastError } : null,\n }),\n subscribe,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol types for @openlivesync/client.
|
|
3
|
+
* Must stay in sync with @openlivesync/server protocol (same message types and payload shapes).
|
|
4
|
+
*/
|
|
5
|
+
/** Generic presence payload (cursor, name, color, etc.). Server does not interpret. */
|
|
6
|
+
type Presence = Record<string, unknown>;
|
|
7
|
+
/** User/session info attached by server from auth (optional). */
|
|
8
|
+
interface UserInfo {
|
|
9
|
+
userId?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
provider?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
declare const MSG_JOIN_ROOM = "join_room";
|
|
16
|
+
declare const MSG_LEAVE_ROOM = "leave_room";
|
|
17
|
+
declare const MSG_UPDATE_PRESENCE = "update_presence";
|
|
18
|
+
declare const MSG_BROADCAST_EVENT = "broadcast_event";
|
|
19
|
+
declare const MSG_SEND_CHAT = "send_chat";
|
|
20
|
+
interface JoinRoomPayload {
|
|
21
|
+
roomId: string;
|
|
22
|
+
presence?: Presence;
|
|
23
|
+
/** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */
|
|
24
|
+
accessToken?: string;
|
|
25
|
+
/** Optional display name if not using accessToken. */
|
|
26
|
+
name?: string;
|
|
27
|
+
/** Optional email if not using accessToken. */
|
|
28
|
+
email?: string;
|
|
29
|
+
}
|
|
30
|
+
interface LeaveRoomPayload {
|
|
31
|
+
roomId?: string;
|
|
32
|
+
}
|
|
33
|
+
interface UpdatePresencePayload {
|
|
34
|
+
presence: Presence;
|
|
35
|
+
}
|
|
36
|
+
interface BroadcastEventPayload {
|
|
37
|
+
event: string;
|
|
38
|
+
payload?: unknown;
|
|
39
|
+
}
|
|
40
|
+
interface SendChatPayload {
|
|
41
|
+
message: string;
|
|
42
|
+
/** Optional application-defined metadata */
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
type ClientMessage = {
|
|
46
|
+
type: typeof MSG_JOIN_ROOM;
|
|
47
|
+
payload: JoinRoomPayload;
|
|
48
|
+
} | {
|
|
49
|
+
type: typeof MSG_LEAVE_ROOM;
|
|
50
|
+
payload?: LeaveRoomPayload;
|
|
51
|
+
} | {
|
|
52
|
+
type: typeof MSG_UPDATE_PRESENCE;
|
|
53
|
+
payload: UpdatePresencePayload;
|
|
54
|
+
} | {
|
|
55
|
+
type: typeof MSG_BROADCAST_EVENT;
|
|
56
|
+
payload: BroadcastEventPayload;
|
|
57
|
+
} | {
|
|
58
|
+
type: typeof MSG_SEND_CHAT;
|
|
59
|
+
payload: SendChatPayload;
|
|
60
|
+
};
|
|
61
|
+
declare const MSG_ROOM_JOINED = "room_joined";
|
|
62
|
+
declare const MSG_PRESENCE_UPDATED = "presence_updated";
|
|
63
|
+
declare const MSG_BROADCAST_EVENT_RELAY = "broadcast_event";
|
|
64
|
+
declare const MSG_CHAT_MESSAGE = "chat_message";
|
|
65
|
+
declare const MSG_ERROR = "error";
|
|
66
|
+
interface PresenceEntry {
|
|
67
|
+
connectionId: string;
|
|
68
|
+
userId?: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
email?: string;
|
|
71
|
+
provider?: string;
|
|
72
|
+
presence: Presence;
|
|
73
|
+
}
|
|
74
|
+
interface RoomJoinedPayload {
|
|
75
|
+
roomId: string;
|
|
76
|
+
connectionId: string;
|
|
77
|
+
presence: Record<string, PresenceEntry>;
|
|
78
|
+
chatHistory?: StoredChatMessage[];
|
|
79
|
+
}
|
|
80
|
+
interface PresenceUpdatedPayload {
|
|
81
|
+
roomId: string;
|
|
82
|
+
joined?: PresenceEntry[];
|
|
83
|
+
left?: string[];
|
|
84
|
+
updated?: PresenceEntry[];
|
|
85
|
+
}
|
|
86
|
+
interface BroadcastEventRelayPayload {
|
|
87
|
+
roomId: string;
|
|
88
|
+
connectionId: string;
|
|
89
|
+
userId?: string;
|
|
90
|
+
event: string;
|
|
91
|
+
payload?: unknown;
|
|
92
|
+
}
|
|
93
|
+
interface ChatMessagePayload {
|
|
94
|
+
roomId: string;
|
|
95
|
+
connectionId: string;
|
|
96
|
+
userId?: string;
|
|
97
|
+
message: string;
|
|
98
|
+
metadata?: Record<string, unknown>;
|
|
99
|
+
id?: string;
|
|
100
|
+
createdAt?: number;
|
|
101
|
+
}
|
|
102
|
+
interface StoredChatMessage {
|
|
103
|
+
id: string;
|
|
104
|
+
roomId: string;
|
|
105
|
+
connectionId: string;
|
|
106
|
+
userId?: string;
|
|
107
|
+
message: string;
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
createdAt: number;
|
|
110
|
+
}
|
|
111
|
+
interface ErrorPayload {
|
|
112
|
+
code: string;
|
|
113
|
+
message: string;
|
|
114
|
+
}
|
|
115
|
+
type ServerMessage = {
|
|
116
|
+
type: typeof MSG_ROOM_JOINED;
|
|
117
|
+
payload: RoomJoinedPayload;
|
|
118
|
+
} | {
|
|
119
|
+
type: typeof MSG_PRESENCE_UPDATED;
|
|
120
|
+
payload: PresenceUpdatedPayload;
|
|
121
|
+
} | {
|
|
122
|
+
type: typeof MSG_BROADCAST_EVENT_RELAY;
|
|
123
|
+
payload: BroadcastEventRelayPayload;
|
|
124
|
+
} | {
|
|
125
|
+
type: typeof MSG_CHAT_MESSAGE;
|
|
126
|
+
payload: ChatMessagePayload;
|
|
127
|
+
} | {
|
|
128
|
+
type: typeof MSG_ERROR;
|
|
129
|
+
payload: ErrorPayload;
|
|
130
|
+
};
|
|
131
|
+
/** Chat message as provided when appending (before storage adds id/createdAt). */
|
|
132
|
+
interface ChatMessageInput {
|
|
133
|
+
roomId: string;
|
|
134
|
+
connectionId: string;
|
|
135
|
+
userId?: string;
|
|
136
|
+
message: string;
|
|
137
|
+
metadata?: Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Core WebSocket client for @openlivesync.
|
|
142
|
+
* Connects to @openlivesync/server, manages room/presence/chat state, and notifies subscribers.
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
type ConnectionStatus = "connecting" | "open" | "closing" | "closed";
|
|
146
|
+
interface LiveSyncClientState {
|
|
147
|
+
connectionStatus: ConnectionStatus;
|
|
148
|
+
currentRoomId: string | null;
|
|
149
|
+
connectionId: string | null;
|
|
150
|
+
presence: Record<string, PresenceEntry>;
|
|
151
|
+
chatMessages: StoredChatMessage[];
|
|
152
|
+
lastError: {
|
|
153
|
+
code: string;
|
|
154
|
+
message: string;
|
|
155
|
+
} | null;
|
|
156
|
+
}
|
|
157
|
+
interface JoinRoomIdentity {
|
|
158
|
+
/** Optional display name if not using accessToken. */
|
|
159
|
+
name?: string;
|
|
160
|
+
/** Optional email if not using accessToken. */
|
|
161
|
+
email?: string;
|
|
162
|
+
/** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */
|
|
163
|
+
accessToken?: string;
|
|
164
|
+
}
|
|
165
|
+
interface LiveSyncClientConfig {
|
|
166
|
+
/** WebSocket URL (e.g. wss://host/live). */
|
|
167
|
+
url: string;
|
|
168
|
+
/** Auto-reconnect on close (default true). */
|
|
169
|
+
reconnect?: boolean;
|
|
170
|
+
/** Initial reconnect delay in ms (default 1000). */
|
|
171
|
+
reconnectIntervalMs?: number;
|
|
172
|
+
/** Max reconnect delay in ms (default 30000). */
|
|
173
|
+
maxReconnectIntervalMs?: number;
|
|
174
|
+
/** Optional: return token for auth; appended as query param (e.g. ?access_token=). */
|
|
175
|
+
getAuthToken?: () => string | Promise<string>;
|
|
176
|
+
/** Throttle presence updates in ms (default 100, match server). */
|
|
177
|
+
presenceThrottleMs?: number;
|
|
178
|
+
}
|
|
179
|
+
interface LiveSyncClient {
|
|
180
|
+
connect(): void;
|
|
181
|
+
disconnect(): void;
|
|
182
|
+
joinRoom(roomId: string, presence?: Presence, identity?: JoinRoomIdentity): void;
|
|
183
|
+
leaveRoom(roomId?: string): void;
|
|
184
|
+
updatePresence(presence: Presence): void;
|
|
185
|
+
broadcastEvent(event: string, payload?: unknown): void;
|
|
186
|
+
sendChat(message: string, metadata?: Record<string, unknown>): void;
|
|
187
|
+
getConnectionStatus(): ConnectionStatus;
|
|
188
|
+
getPresence(): Record<string, PresenceEntry>;
|
|
189
|
+
getChatMessages(): StoredChatMessage[];
|
|
190
|
+
getCurrentRoomId(): string | null;
|
|
191
|
+
getState(): LiveSyncClientState;
|
|
192
|
+
subscribe(listener: (state: LiveSyncClientState) => void): () => void;
|
|
193
|
+
}
|
|
194
|
+
declare function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClient;
|
|
195
|
+
|
|
196
|
+
export { type BroadcastEventPayload as B, type ChatMessageInput as C, type ErrorPayload as E, type JoinRoomIdentity as J, type LiveSyncClient as L, MSG_BROADCAST_EVENT as M, type Presence as P, type RoomJoinedPayload as R, type StoredChatMessage as S, type UpdatePresencePayload as U, type PresenceEntry as a, type LiveSyncClientState as b, type BroadcastEventRelayPayload as c, type ChatMessagePayload as d, type ClientMessage as e, type ConnectionStatus as f, type JoinRoomPayload as g, type LeaveRoomPayload as h, type LiveSyncClientConfig as i, MSG_BROADCAST_EVENT_RELAY as j, MSG_CHAT_MESSAGE as k, MSG_ERROR as l, MSG_JOIN_ROOM as m, MSG_LEAVE_ROOM as n, MSG_PRESENCE_UPDATED as o, MSG_ROOM_JOINED as p, MSG_SEND_CHAT as q, MSG_UPDATE_PRESENCE as r, type PresenceUpdatedPayload as s, type SendChatPayload as t, type ServerMessage as u, type UserInfo as v, createLiveSyncClient as w };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,184 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Wire protocol types for @openlivesync/client.
|
|
3
|
-
* Must stay in sync with @openlivesync/server protocol (same message types and payload shapes).
|
|
4
|
-
*/
|
|
5
|
-
/** Generic presence payload (cursor, name, color, etc.). Server does not interpret. */
|
|
6
|
-
type Presence = Record<string, unknown>;
|
|
7
|
-
/** User/session info attached by server from auth (optional). */
|
|
8
|
-
interface UserInfo {
|
|
9
|
-
userId?: string;
|
|
10
|
-
name?: string;
|
|
11
|
-
email?: string;
|
|
12
|
-
provider?: string;
|
|
13
|
-
[key: string]: unknown;
|
|
14
|
-
}
|
|
15
|
-
declare const MSG_JOIN_ROOM = "join_room";
|
|
16
|
-
declare const MSG_LEAVE_ROOM = "leave_room";
|
|
17
|
-
declare const MSG_UPDATE_PRESENCE = "update_presence";
|
|
18
|
-
declare const MSG_BROADCAST_EVENT = "broadcast_event";
|
|
19
|
-
declare const MSG_SEND_CHAT = "send_chat";
|
|
20
|
-
interface JoinRoomPayload {
|
|
21
|
-
roomId: string;
|
|
22
|
-
presence?: Presence;
|
|
23
|
-
/** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */
|
|
24
|
-
accessToken?: string;
|
|
25
|
-
}
|
|
26
|
-
interface LeaveRoomPayload {
|
|
27
|
-
roomId?: string;
|
|
28
|
-
}
|
|
29
|
-
interface UpdatePresencePayload {
|
|
30
|
-
presence: Presence;
|
|
31
|
-
}
|
|
32
|
-
interface BroadcastEventPayload {
|
|
33
|
-
event: string;
|
|
34
|
-
payload?: unknown;
|
|
35
|
-
}
|
|
36
|
-
interface SendChatPayload {
|
|
37
|
-
message: string;
|
|
38
|
-
/** Optional application-defined metadata */
|
|
39
|
-
metadata?: Record<string, unknown>;
|
|
40
|
-
}
|
|
41
|
-
type ClientMessage = {
|
|
42
|
-
type: typeof MSG_JOIN_ROOM;
|
|
43
|
-
payload: JoinRoomPayload;
|
|
44
|
-
} | {
|
|
45
|
-
type: typeof MSG_LEAVE_ROOM;
|
|
46
|
-
payload?: LeaveRoomPayload;
|
|
47
|
-
} | {
|
|
48
|
-
type: typeof MSG_UPDATE_PRESENCE;
|
|
49
|
-
payload: UpdatePresencePayload;
|
|
50
|
-
} | {
|
|
51
|
-
type: typeof MSG_BROADCAST_EVENT;
|
|
52
|
-
payload: BroadcastEventPayload;
|
|
53
|
-
} | {
|
|
54
|
-
type: typeof MSG_SEND_CHAT;
|
|
55
|
-
payload: SendChatPayload;
|
|
56
|
-
};
|
|
57
|
-
declare const MSG_ROOM_JOINED = "room_joined";
|
|
58
|
-
declare const MSG_PRESENCE_UPDATED = "presence_updated";
|
|
59
|
-
declare const MSG_BROADCAST_EVENT_RELAY = "broadcast_event";
|
|
60
|
-
declare const MSG_CHAT_MESSAGE = "chat_message";
|
|
61
|
-
declare const MSG_ERROR = "error";
|
|
62
|
-
interface PresenceEntry {
|
|
63
|
-
connectionId: string;
|
|
64
|
-
userId?: string;
|
|
65
|
-
name?: string;
|
|
66
|
-
email?: string;
|
|
67
|
-
provider?: string;
|
|
68
|
-
presence: Presence;
|
|
69
|
-
}
|
|
70
|
-
interface RoomJoinedPayload {
|
|
71
|
-
roomId: string;
|
|
72
|
-
connectionId: string;
|
|
73
|
-
presence: Record<string, PresenceEntry>;
|
|
74
|
-
chatHistory?: StoredChatMessage[];
|
|
75
|
-
}
|
|
76
|
-
interface PresenceUpdatedPayload {
|
|
77
|
-
roomId: string;
|
|
78
|
-
joined?: PresenceEntry[];
|
|
79
|
-
left?: string[];
|
|
80
|
-
updated?: PresenceEntry[];
|
|
81
|
-
}
|
|
82
|
-
interface BroadcastEventRelayPayload {
|
|
83
|
-
roomId: string;
|
|
84
|
-
connectionId: string;
|
|
85
|
-
userId?: string;
|
|
86
|
-
event: string;
|
|
87
|
-
payload?: unknown;
|
|
88
|
-
}
|
|
89
|
-
interface ChatMessagePayload {
|
|
90
|
-
roomId: string;
|
|
91
|
-
connectionId: string;
|
|
92
|
-
userId?: string;
|
|
93
|
-
message: string;
|
|
94
|
-
metadata?: Record<string, unknown>;
|
|
95
|
-
id?: string;
|
|
96
|
-
createdAt?: number;
|
|
97
|
-
}
|
|
98
|
-
interface StoredChatMessage {
|
|
99
|
-
id: string;
|
|
100
|
-
roomId: string;
|
|
101
|
-
connectionId: string;
|
|
102
|
-
userId?: string;
|
|
103
|
-
message: string;
|
|
104
|
-
metadata?: Record<string, unknown>;
|
|
105
|
-
createdAt: number;
|
|
106
|
-
}
|
|
107
|
-
interface ErrorPayload {
|
|
108
|
-
code: string;
|
|
109
|
-
message: string;
|
|
110
|
-
}
|
|
111
|
-
type ServerMessage = {
|
|
112
|
-
type: typeof MSG_ROOM_JOINED;
|
|
113
|
-
payload: RoomJoinedPayload;
|
|
114
|
-
} | {
|
|
115
|
-
type: typeof MSG_PRESENCE_UPDATED;
|
|
116
|
-
payload: PresenceUpdatedPayload;
|
|
117
|
-
} | {
|
|
118
|
-
type: typeof MSG_BROADCAST_EVENT_RELAY;
|
|
119
|
-
payload: BroadcastEventRelayPayload;
|
|
120
|
-
} | {
|
|
121
|
-
type: typeof MSG_CHAT_MESSAGE;
|
|
122
|
-
payload: ChatMessagePayload;
|
|
123
|
-
} | {
|
|
124
|
-
type: typeof MSG_ERROR;
|
|
125
|
-
payload: ErrorPayload;
|
|
126
|
-
};
|
|
127
|
-
/** Chat message as provided when appending (before storage adds id/createdAt). */
|
|
128
|
-
interface ChatMessageInput {
|
|
129
|
-
roomId: string;
|
|
130
|
-
connectionId: string;
|
|
131
|
-
userId?: string;
|
|
132
|
-
message: string;
|
|
133
|
-
metadata?: Record<string, unknown>;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Core WebSocket client for @openlivesync.
|
|
138
|
-
* Connects to @openlivesync/server, manages room/presence/chat state, and notifies subscribers.
|
|
139
|
-
*/
|
|
140
|
-
|
|
141
|
-
type ConnectionStatus = "connecting" | "open" | "closing" | "closed";
|
|
142
|
-
interface LiveSyncClientState {
|
|
143
|
-
connectionStatus: ConnectionStatus;
|
|
144
|
-
currentRoomId: string | null;
|
|
145
|
-
connectionId: string | null;
|
|
146
|
-
presence: Record<string, PresenceEntry>;
|
|
147
|
-
chatMessages: StoredChatMessage[];
|
|
148
|
-
lastError: {
|
|
149
|
-
code: string;
|
|
150
|
-
message: string;
|
|
151
|
-
} | null;
|
|
152
|
-
}
|
|
153
|
-
interface LiveSyncClientConfig {
|
|
154
|
-
/** WebSocket URL (e.g. wss://host/live). */
|
|
155
|
-
url: string;
|
|
156
|
-
/** Auto-reconnect on close (default true). */
|
|
157
|
-
reconnect?: boolean;
|
|
158
|
-
/** Initial reconnect delay in ms (default 1000). */
|
|
159
|
-
reconnectIntervalMs?: number;
|
|
160
|
-
/** Max reconnect delay in ms (default 30000). */
|
|
161
|
-
maxReconnectIntervalMs?: number;
|
|
162
|
-
/** Optional: return token for auth; appended as query param (e.g. ?access_token=). */
|
|
163
|
-
getAuthToken?: () => string | Promise<string>;
|
|
164
|
-
/** Throttle presence updates in ms (default 100, match server). */
|
|
165
|
-
presenceThrottleMs?: number;
|
|
166
|
-
}
|
|
167
|
-
interface LiveSyncClient {
|
|
168
|
-
connect(): void;
|
|
169
|
-
disconnect(): void;
|
|
170
|
-
joinRoom(roomId: string, presence?: Presence, accessToken?: string): void;
|
|
171
|
-
leaveRoom(roomId?: string): void;
|
|
172
|
-
updatePresence(presence: Presence): void;
|
|
173
|
-
broadcastEvent(event: string, payload?: unknown): void;
|
|
174
|
-
sendChat(message: string, metadata?: Record<string, unknown>): void;
|
|
175
|
-
getConnectionStatus(): ConnectionStatus;
|
|
176
|
-
getPresence(): Record<string, PresenceEntry>;
|
|
177
|
-
getChatMessages(): StoredChatMessage[];
|
|
178
|
-
getCurrentRoomId(): string | null;
|
|
179
|
-
getState(): LiveSyncClientState;
|
|
180
|
-
subscribe(listener: (state: LiveSyncClientState) => void): () => void;
|
|
181
|
-
}
|
|
182
|
-
declare function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClient;
|
|
183
|
-
|
|
184
|
-
export { type BroadcastEventPayload, type BroadcastEventRelayPayload, type ChatMessageInput, type ChatMessagePayload, type ClientMessage, type ConnectionStatus, type ErrorPayload, type JoinRoomPayload, type LeaveRoomPayload, type LiveSyncClient, type LiveSyncClientConfig, type LiveSyncClientState, MSG_BROADCAST_EVENT, MSG_BROADCAST_EVENT_RELAY, MSG_CHAT_MESSAGE, MSG_ERROR, MSG_JOIN_ROOM, MSG_LEAVE_ROOM, MSG_PRESENCE_UPDATED, MSG_ROOM_JOINED, MSG_SEND_CHAT, MSG_UPDATE_PRESENCE, type Presence, type PresenceEntry, type PresenceUpdatedPayload, type RoomJoinedPayload, type SendChatPayload, type ServerMessage, type StoredChatMessage, type UpdatePresencePayload, type UserInfo, createLiveSyncClient };
|
|
1
|
+
export { B as BroadcastEventPayload, c as BroadcastEventRelayPayload, C as ChatMessageInput, d as ChatMessagePayload, e as ClientMessage, f as ConnectionStatus, E as ErrorPayload, g as JoinRoomPayload, h as LeaveRoomPayload, L as LiveSyncClient, i as LiveSyncClientConfig, b as LiveSyncClientState, M as MSG_BROADCAST_EVENT, j as MSG_BROADCAST_EVENT_RELAY, k as MSG_CHAT_MESSAGE, l as MSG_ERROR, m as MSG_JOIN_ROOM, n as MSG_LEAVE_ROOM, o as MSG_PRESENCE_UPDATED, p as MSG_ROOM_JOINED, q as MSG_SEND_CHAT, r as MSG_UPDATE_PRESENCE, P as Presence, a as PresenceEntry, s as PresenceUpdatedPayload, R as RoomJoinedPayload, t as SendChatPayload, u as ServerMessage, S as StoredChatMessage, U as UpdatePresencePayload, v as UserInfo, w as createLiveSyncClient } from './index-HY9A86VV.js';
|
package/dist/index.js
CHANGED
package/dist/react-entry.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
|
-
import { LiveSyncClient, StoredChatMessage, Presence, PresenceEntry, LiveSyncClientState } from './index.js';
|
|
2
|
+
import { L as LiveSyncClient, S as StoredChatMessage, P as Presence, J as JoinRoomIdentity, a as PresenceEntry, b as LiveSyncClientState } from './index-HY9A86VV.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* React bindings for @openlivesync/client.
|
|
@@ -35,9 +35,13 @@ interface UseRoomOptions {
|
|
|
35
35
|
accessToken?: string;
|
|
36
36
|
/** Optional getter for access token (e.g. refreshed token); used when auto-joining. */
|
|
37
37
|
getAccessToken?: () => string | Promise<string>;
|
|
38
|
+
/** Optional display name if not using accessToken. */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** Optional email if not using accessToken. */
|
|
41
|
+
email?: string;
|
|
38
42
|
}
|
|
39
43
|
interface UseRoomReturn {
|
|
40
|
-
join: (roomId: string, presence?: Presence,
|
|
44
|
+
join: (roomId: string, presence?: Presence, identity?: JoinRoomIdentity) => void;
|
|
41
45
|
leave: (roomId?: string) => void;
|
|
42
46
|
updatePresence: (presence: Presence) => void;
|
|
43
47
|
broadcastEvent: (event: string, payload?: unknown) => void;
|
package/dist/react-entry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createLiveSyncClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TAMQSVU2.js";
|
|
4
4
|
|
|
5
5
|
// src/react-entry.tsx
|
|
6
6
|
import React, {
|
|
@@ -67,17 +67,28 @@ function useConnectionStatus() {
|
|
|
67
67
|
return useClientState().connectionStatus;
|
|
68
68
|
}
|
|
69
69
|
function useRoom(roomId, options = {}) {
|
|
70
|
-
const {
|
|
70
|
+
const {
|
|
71
|
+
initialPresence,
|
|
72
|
+
autoJoin = true,
|
|
73
|
+
accessToken,
|
|
74
|
+
getAccessToken,
|
|
75
|
+
name,
|
|
76
|
+
email
|
|
77
|
+
} = options;
|
|
71
78
|
const client = useLiveSyncClient();
|
|
72
79
|
const state = useClientState();
|
|
73
80
|
const joinedRef = useRef(null);
|
|
74
81
|
const join = useCallback(
|
|
75
|
-
(id, presence,
|
|
76
|
-
const
|
|
77
|
-
|
|
82
|
+
(id, presence, identity) => {
|
|
83
|
+
const effectiveIdentity = identity ?? {
|
|
84
|
+
accessToken,
|
|
85
|
+
name,
|
|
86
|
+
email
|
|
87
|
+
};
|
|
88
|
+
client.joinRoom(id, presence ?? initialPresence, effectiveIdentity);
|
|
78
89
|
joinedRef.current = id;
|
|
79
90
|
},
|
|
80
|
-
[client, initialPresence, accessToken]
|
|
91
|
+
[client, initialPresence, accessToken, name, email]
|
|
81
92
|
);
|
|
82
93
|
const leave = useCallback(
|
|
83
94
|
(id) => {
|
|
@@ -92,7 +103,12 @@ function useRoom(roomId, options = {}) {
|
|
|
92
103
|
(async () => {
|
|
93
104
|
const token = accessToken ?? (getAccessToken ? await getAccessToken() : void 0);
|
|
94
105
|
if (!cancelled) {
|
|
95
|
-
|
|
106
|
+
const identity = {
|
|
107
|
+
accessToken: token,
|
|
108
|
+
name,
|
|
109
|
+
email
|
|
110
|
+
};
|
|
111
|
+
client.joinRoom(roomId, initialPresence, identity);
|
|
96
112
|
joinedRef.current = roomId;
|
|
97
113
|
}
|
|
98
114
|
})();
|
package/dist/react-entry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react-entry.tsx"],"sourcesContent":["/**\n * React bindings for @openlivesync/client.\n * Provider + hooks; import from \"@openlivesync/client/react\".\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n createLiveSyncClient,\n type LiveSyncClient,\n type LiveSyncClientConfig,\n type LiveSyncClientState,\n} from \"./client.js\";\nimport type { Presence, PresenceEntry, StoredChatMessage } from \"./protocol.js\";\n\nconst LiveSyncContext = createContext<LiveSyncClient | null>(null);\n\nexport interface LiveSyncProviderProps {\n children: ReactNode;\n /** Pre-created client (call createLiveSyncClient yourself). */\n client?: LiveSyncClient;\n /** Or pass config and the provider will create the client and connect on mount. */\n url?: string;\n reconnect?: boolean;\n reconnectIntervalMs?: number;\n maxReconnectIntervalMs?: number;\n getAuthToken?: () => string | Promise<string>;\n presenceThrottleMs?: number;\n}\n\n/**\n * Provides the LiveSync client to the tree. Pass either `client` or config (`url`, etc.).\n * If config is passed, the client is created on mount and connect() is called; disconnect on unmount.\n */\nexport function LiveSyncProvider(props: LiveSyncProviderProps) {\n const { children, client: clientProp } = props;\n const configRef = useRef<LiveSyncClientConfig | null>(null);\n if (!configRef.current && !clientProp && props.url) {\n configRef.current = {\n url: props.url,\n reconnect: props.reconnect,\n reconnectIntervalMs: props.reconnectIntervalMs,\n maxReconnectIntervalMs: props.maxReconnectIntervalMs,\n getAuthToken: props.getAuthToken,\n presenceThrottleMs: props.presenceThrottleMs,\n };\n }\n const config = configRef.current;\n\n const clientFromConfig = useMemo(() => {\n if (clientProp || !config) return null;\n return createLiveSyncClient(config);\n }, [clientProp, config?.url]);\n\n const client = clientProp ?? clientFromConfig;\n\n useEffect(() => {\n if (!client) return;\n if (!clientProp && config) {\n client.connect();\n return () => client.disconnect();\n }\n }, [client, clientProp, config]);\n\n if (!client) {\n throw new Error(\n \"LiveSyncProvider: pass either `client` or config (e.g. `url`) to create the client.\"\n );\n }\n\n return React.createElement(LiveSyncContext.Provider, { value: client }, children);\n}\n\nexport function useLiveSyncClient(): LiveSyncClient {\n const client = useContext(LiveSyncContext);\n if (!client) {\n throw new Error(\"useLiveSyncClient must be used within LiveSyncProvider\");\n }\n return client;\n}\n\nfunction useClientState(): LiveSyncClientState {\n const client = useLiveSyncClient();\n const [state, setState] = useState<LiveSyncClientState>(() =>\n client.getState()\n );\n useEffect(() => {\n return client.subscribe(setState);\n }, [client]);\n return state;\n}\n\n/** Returns current connection status and triggers re-renders when it changes. */\nexport function useConnectionStatus(): LiveSyncClientState[\"connectionStatus\"] {\n return useClientState().connectionStatus;\n}\n\nexport interface UseRoomOptions {\n /** Initial presence when joining (optional). */\n initialPresence?: Presence;\n /** If true, join the room when roomId is set and leave on cleanup or when roomId changes. */\n autoJoin?: boolean;\n /** Optional access token sent with join_room (server can decode for name/email). */\n accessToken?: string;\n /** Optional getter for access token (e.g. refreshed token); used when auto-joining. */\n getAccessToken?: () => string | Promise<string>;\n}\n\nexport interface UseRoomReturn {\n join: (roomId: string, presence?: Presence, accessToken?: string) => void;\n leave: (roomId?: string) => void;\n updatePresence: (presence: Presence) => void;\n broadcastEvent: (event: string, payload?: unknown) => void;\n presence: Record<string, PresenceEntry>;\n connectionId: string | null;\n isInRoom: boolean;\n currentRoomId: string | null;\n}\n\n/**\n * Subscribe to room state and get methods to join/leave/update presence/broadcast.\n * If autoJoin is true (default), joining happens when roomId is set and leaving on unmount or roomId change.\n */\nexport function useRoom(\n roomId: string | null,\n options: UseRoomOptions = {}\n): UseRoomReturn {\n const { initialPresence, autoJoin = true, accessToken, getAccessToken } = options;\n const client = useLiveSyncClient();\n const state = useClientState();\n const joinedRef = useRef<string | null>(null);\n\n const join = useCallback(\n (id: string, presence?: Presence, token?: string) => {\n const t = token ?? accessToken;\n client.joinRoom(id, presence ?? initialPresence, t);\n joinedRef.current = id;\n },\n [client, initialPresence, accessToken]\n );\n\n const leave = useCallback(\n (id?: string) => {\n client.leaveRoom(id);\n if (!id || id === joinedRef.current) joinedRef.current = null;\n },\n [client]\n );\n\n useEffect(() => {\n if (!autoJoin || roomId === null) return;\n let cancelled = false;\n (async () => {\n const token = accessToken ?? (getAccessToken ? await getAccessToken() : undefined);\n if (!cancelled) {\n client.joinRoom(roomId, initialPresence, token);\n joinedRef.current = roomId;\n }\n })();\n return () => {\n cancelled = true;\n leave(roomId);\n };\n }, [autoJoin, roomId, initialPresence, accessToken, getAccessToken, client, leave]);\n\n const updatePresence = useCallback(\n (presence: Presence) => client.updatePresence(presence),\n [client]\n );\n\n const broadcastEvent = useCallback(\n (event: string, payload?: unknown) => client.broadcastEvent(event, payload),\n [client]\n );\n\n const isInRoom =\n state.currentRoomId !== null && state.currentRoomId === roomId;\n\n return {\n join,\n leave,\n updatePresence,\n broadcastEvent,\n presence: roomId && isInRoom ? state.presence : {},\n connectionId: isInRoom ? state.connectionId : null,\n isInRoom,\n currentRoomId: state.currentRoomId,\n };\n}\n\n/** Returns presence map for the current room (or empty if not in room). */\nexport function usePresence(roomId: string | null): Record<string, PresenceEntry> {\n useLiveSyncClient(); // ensure we're inside provider\n const state = useClientState();\n const isInRoom =\n roomId !== null &&\n state.currentRoomId === roomId;\n return isInRoom ? state.presence : {};\n}\n\nexport interface UseChatReturn {\n messages: StoredChatMessage[];\n sendMessage: (message: string, metadata?: Record<string, unknown>) => void;\n}\n\n/** Returns chat messages for the given room and sendMessage. Ensure the room is joined (e.g. via useRoom). */\nexport function useChat(roomId: string | null): UseChatReturn {\n const client = useLiveSyncClient();\n const state = useClientState();\n const isInRoom =\n roomId !== null && state.currentRoomId === roomId;\n const messages = isInRoom ? state.chatMessages : [];\n\n const sendMessage = useCallback(\n (message: string, metadata?: Record<string, unknown>) => {\n client.sendChat(message, metadata);\n },\n [client]\n );\n\n return { messages, sendMessage };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/react-entry.tsx"],"sourcesContent":["/**\n * React bindings for @openlivesync/client.\n * Provider + hooks; import from \"@openlivesync/client/react\".\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n createLiveSyncClient,\n type LiveSyncClient,\n type LiveSyncClientConfig,\n type LiveSyncClientState,\n type JoinRoomIdentity,\n} from \"./client.js\";\nimport type { Presence, PresenceEntry, StoredChatMessage } from \"./protocol.js\";\n\nconst LiveSyncContext = createContext<LiveSyncClient | null>(null);\n\nexport interface LiveSyncProviderProps {\n children: ReactNode;\n /** Pre-created client (call createLiveSyncClient yourself). */\n client?: LiveSyncClient;\n /** Or pass config and the provider will create the client and connect on mount. */\n url?: string;\n reconnect?: boolean;\n reconnectIntervalMs?: number;\n maxReconnectIntervalMs?: number;\n getAuthToken?: () => string | Promise<string>;\n presenceThrottleMs?: number;\n}\n\n/**\n * Provides the LiveSync client to the tree. Pass either `client` or config (`url`, etc.).\n * If config is passed, the client is created on mount and connect() is called; disconnect on unmount.\n */\nexport function LiveSyncProvider(props: LiveSyncProviderProps) {\n const { children, client: clientProp } = props;\n const configRef = useRef<LiveSyncClientConfig | null>(null);\n if (!configRef.current && !clientProp && props.url) {\n configRef.current = {\n url: props.url,\n reconnect: props.reconnect,\n reconnectIntervalMs: props.reconnectIntervalMs,\n maxReconnectIntervalMs: props.maxReconnectIntervalMs,\n getAuthToken: props.getAuthToken,\n presenceThrottleMs: props.presenceThrottleMs,\n };\n }\n const config = configRef.current;\n\n const clientFromConfig = useMemo(() => {\n if (clientProp || !config) return null;\n return createLiveSyncClient(config);\n }, [clientProp, config?.url]);\n\n const client = clientProp ?? clientFromConfig;\n\n useEffect(() => {\n if (!client) return;\n if (!clientProp && config) {\n client.connect();\n return () => client.disconnect();\n }\n }, [client, clientProp, config]);\n\n if (!client) {\n throw new Error(\n \"LiveSyncProvider: pass either `client` or config (e.g. `url`) to create the client.\"\n );\n }\n\n return React.createElement(LiveSyncContext.Provider, { value: client }, children);\n}\n\nexport function useLiveSyncClient(): LiveSyncClient {\n const client = useContext(LiveSyncContext);\n if (!client) {\n throw new Error(\"useLiveSyncClient must be used within LiveSyncProvider\");\n }\n return client;\n}\n\nfunction useClientState(): LiveSyncClientState {\n const client = useLiveSyncClient();\n const [state, setState] = useState<LiveSyncClientState>(() =>\n client.getState()\n );\n useEffect(() => {\n return client.subscribe(setState);\n }, [client]);\n return state;\n}\n\n/** Returns current connection status and triggers re-renders when it changes. */\nexport function useConnectionStatus(): LiveSyncClientState[\"connectionStatus\"] {\n return useClientState().connectionStatus;\n}\n\nexport interface UseRoomOptions {\n /** Initial presence when joining (optional). */\n initialPresence?: Presence;\n /** If true, join the room when roomId is set and leave on cleanup or when roomId changes. */\n autoJoin?: boolean;\n /** Optional access token sent with join_room (server can decode for name/email). */\n accessToken?: string;\n /** Optional getter for access token (e.g. refreshed token); used when auto-joining. */\n getAccessToken?: () => string | Promise<string>;\n /** Optional display name if not using accessToken. */\n name?: string;\n /** Optional email if not using accessToken. */\n email?: string;\n}\n\nexport interface UseRoomReturn {\n join: (roomId: string, presence?: Presence, identity?: JoinRoomIdentity) => void;\n leave: (roomId?: string) => void;\n updatePresence: (presence: Presence) => void;\n broadcastEvent: (event: string, payload?: unknown) => void;\n presence: Record<string, PresenceEntry>;\n connectionId: string | null;\n isInRoom: boolean;\n currentRoomId: string | null;\n}\n\n/**\n * Subscribe to room state and get methods to join/leave/update presence/broadcast.\n * If autoJoin is true (default), joining happens when roomId is set and leaving on unmount or roomId change.\n */\nexport function useRoom(\n roomId: string | null,\n options: UseRoomOptions = {}\n): UseRoomReturn {\n const {\n initialPresence,\n autoJoin = true,\n accessToken,\n getAccessToken,\n name,\n email,\n } = options;\n const client = useLiveSyncClient();\n const state = useClientState();\n const joinedRef = useRef<string | null>(null);\n\n const join = useCallback(\n (id: string, presence?: Presence, identity?: JoinRoomIdentity) => {\n const effectiveIdentity: JoinRoomIdentity = identity ?? {\n accessToken,\n name,\n email,\n };\n client.joinRoom(id, presence ?? initialPresence, effectiveIdentity);\n joinedRef.current = id;\n },\n [client, initialPresence, accessToken, name, email]\n );\n\n const leave = useCallback(\n (id?: string) => {\n client.leaveRoom(id);\n if (!id || id === joinedRef.current) joinedRef.current = null;\n },\n [client]\n );\n\n useEffect(() => {\n if (!autoJoin || roomId === null) return;\n let cancelled = false;\n (async () => {\n const token = accessToken ?? (getAccessToken ? await getAccessToken() : undefined);\n if (!cancelled) {\n const identity: JoinRoomIdentity = {\n accessToken: token,\n name,\n email,\n };\n client.joinRoom(roomId, initialPresence, identity);\n joinedRef.current = roomId;\n }\n })();\n return () => {\n cancelled = true;\n leave(roomId);\n };\n }, [autoJoin, roomId, initialPresence, accessToken, getAccessToken, client, leave]);\n\n const updatePresence = useCallback(\n (presence: Presence) => client.updatePresence(presence),\n [client]\n );\n\n const broadcastEvent = useCallback(\n (event: string, payload?: unknown) => client.broadcastEvent(event, payload),\n [client]\n );\n\n const isInRoom =\n state.currentRoomId !== null && state.currentRoomId === roomId;\n\n return {\n join,\n leave,\n updatePresence,\n broadcastEvent,\n presence: roomId && isInRoom ? state.presence : {},\n connectionId: isInRoom ? state.connectionId : null,\n isInRoom,\n currentRoomId: state.currentRoomId,\n };\n}\n\n/** Returns presence map for the current room (or empty if not in room). */\nexport function usePresence(roomId: string | null): Record<string, PresenceEntry> {\n useLiveSyncClient(); // ensure we're inside provider\n const state = useClientState();\n const isInRoom =\n roomId !== null &&\n state.currentRoomId === roomId;\n return isInRoom ? state.presence : {};\n}\n\nexport interface UseChatReturn {\n messages: StoredChatMessage[];\n sendMessage: (message: string, metadata?: Record<string, unknown>) => void;\n}\n\n/** Returns chat messages for the given room and sendMessage. Ensure the room is joined (e.g. via useRoom). */\nexport function useChat(roomId: string | null): UseChatReturn {\n const client = useLiveSyncClient();\n const state = useClientState();\n const isInRoom =\n roomId !== null && state.currentRoomId === roomId;\n const messages = isInRoom ? state.chatMessages : [];\n\n const sendMessage = useCallback(\n (message: string, metadata?: Record<string, unknown>) => {\n client.sendChat(message, metadata);\n },\n [client]\n );\n\n return { messages, sendMessage };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlivesync/client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,16 +18,21 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup",
|
|
21
|
-
"clean": "rm -rf dist"
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:watch": "vitest --watch"
|
|
22
24
|
},
|
|
23
25
|
"peerDependencies": {
|
|
24
26
|
"react": ">=18.0.0"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
29
|
+
"@testing-library/react": "^16.0.0",
|
|
27
30
|
"@types/react": "^18.3.0",
|
|
28
31
|
"react": "^18.3.0",
|
|
32
|
+
"react-dom": "^18.3.0",
|
|
29
33
|
"tsup": "^8.3.5",
|
|
30
|
-
"typescript": "^5.6.3"
|
|
34
|
+
"typescript": "^5.6.3",
|
|
35
|
+
"vitest": "^2.1.4"
|
|
31
36
|
},
|
|
32
37
|
"repository": {
|
|
33
38
|
"type": "git",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { createLiveSyncClient } from "./client.js";
|
|
3
|
+
|
|
4
|
+
function createMockWebSocket() {
|
|
5
|
+
const listeners = {};
|
|
6
|
+
const sent = [];
|
|
7
|
+
|
|
8
|
+
const ws = {
|
|
9
|
+
readyState: 1,
|
|
10
|
+
OPEN: 1,
|
|
11
|
+
url: "",
|
|
12
|
+
onopen: null,
|
|
13
|
+
onmessage: null,
|
|
14
|
+
onclose: null,
|
|
15
|
+
onerror: null,
|
|
16
|
+
addEventListener(type, listener) {
|
|
17
|
+
listeners[type] = listeners[type] ?? [];
|
|
18
|
+
listeners[type].push(listener);
|
|
19
|
+
},
|
|
20
|
+
send(data) {
|
|
21
|
+
sent.push(data);
|
|
22
|
+
},
|
|
23
|
+
close() {
|
|
24
|
+
if (typeof ws.onclose === "function") {
|
|
25
|
+
ws.onclose({});
|
|
26
|
+
}
|
|
27
|
+
(listeners["close"] ?? []).forEach((l) => l({ data: "" }));
|
|
28
|
+
},
|
|
29
|
+
sent,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function emitOpen() {
|
|
33
|
+
if (typeof ws.onopen === "function") {
|
|
34
|
+
ws.onopen({});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emitMessage(msg) {
|
|
39
|
+
const data = JSON.stringify(msg);
|
|
40
|
+
if (typeof ws.onmessage === "function") {
|
|
41
|
+
ws.onmessage({ data });
|
|
42
|
+
}
|
|
43
|
+
(listeners["message"] ?? []).forEach((l) => l({ data }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { ws, sent, emitOpen, emitMessage };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("createLiveSyncClient joinRoom identity", () => {
|
|
50
|
+
const OriginalWebSocket = globalThis.WebSocket;
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
globalThis.WebSocket = OriginalWebSocket;
|
|
54
|
+
vi.restoreAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("sends join_room with manual name/email when identity has no accessToken", () => {
|
|
58
|
+
const mock = createMockWebSocket();
|
|
59
|
+
globalThis.WebSocket = vi.fn(() => mock.ws);
|
|
60
|
+
|
|
61
|
+
const client = createLiveSyncClient({ url: "ws://localhost/live", reconnect: false });
|
|
62
|
+
client.connect();
|
|
63
|
+
mock.emitOpen();
|
|
64
|
+
|
|
65
|
+
const presence = { cursor: { x: 1, y: 2 } };
|
|
66
|
+
const identity = { name: "Manual User", email: "manual@example.com" };
|
|
67
|
+
|
|
68
|
+
client.joinRoom("room1", presence, identity);
|
|
69
|
+
|
|
70
|
+
const raw = mock.sent[mock.sent.length - 1];
|
|
71
|
+
expect(raw).toBeTruthy();
|
|
72
|
+
const msg = JSON.parse(raw);
|
|
73
|
+
expect(msg.type).toBe("join_room");
|
|
74
|
+
expect(msg.payload).toMatchObject({
|
|
75
|
+
roomId: "room1",
|
|
76
|
+
presence,
|
|
77
|
+
name: "Manual User",
|
|
78
|
+
email: "manual@example.com",
|
|
79
|
+
});
|
|
80
|
+
expect(msg.payload.accessToken).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sends join_room with accessToken when identity has token only", () => {
|
|
84
|
+
const mock = createMockWebSocket();
|
|
85
|
+
globalThis.WebSocket = vi.fn(() => mock.ws);
|
|
86
|
+
|
|
87
|
+
const client = createLiveSyncClient({ url: "ws://localhost/live", reconnect: false });
|
|
88
|
+
client.connect();
|
|
89
|
+
mock.emitOpen();
|
|
90
|
+
|
|
91
|
+
const identity = { accessToken: "token-123" };
|
|
92
|
+
client.joinRoom("room1", undefined, identity);
|
|
93
|
+
|
|
94
|
+
const raw = mock.sent[mock.sent.length - 1];
|
|
95
|
+
expect(raw).toBeTruthy();
|
|
96
|
+
const msg = JSON.parse(raw);
|
|
97
|
+
expect(msg.type).toBe("join_room");
|
|
98
|
+
expect(msg.payload).toMatchObject({
|
|
99
|
+
roomId: "room1",
|
|
100
|
+
accessToken: "token-123",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reconnectAndRejoin reuses last identity", () => {
|
|
105
|
+
const mock = createMockWebSocket();
|
|
106
|
+
globalThis.WebSocket = vi.fn(() => mock.ws);
|
|
107
|
+
|
|
108
|
+
const client = createLiveSyncClient({ url: "ws://localhost/live", reconnect: false });
|
|
109
|
+
client.connect();
|
|
110
|
+
mock.emitOpen();
|
|
111
|
+
|
|
112
|
+
const identity = {
|
|
113
|
+
accessToken: "token-abc",
|
|
114
|
+
name: "Reconnect User",
|
|
115
|
+
email: "reconnect@example.com",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
client.joinRoom("room-reconnect", { cursor: { x: 0 } }, identity);
|
|
119
|
+
|
|
120
|
+
// Simulate server confirming join so client state has currentRoomId set
|
|
121
|
+
mock.emitMessage({
|
|
122
|
+
type: "room_joined",
|
|
123
|
+
payload: {
|
|
124
|
+
roomId: "room-reconnect",
|
|
125
|
+
connectionId: "c1",
|
|
126
|
+
presence: {},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Clear previous sends and trigger reconnectAndRejoin via connect() call again
|
|
131
|
+
mock.sent.length = 0;
|
|
132
|
+
client.connect();
|
|
133
|
+
mock.emitOpen();
|
|
134
|
+
|
|
135
|
+
const raw = mock.sent[mock.sent.length - 1];
|
|
136
|
+
expect(raw).toBeTruthy();
|
|
137
|
+
const msg = JSON.parse(raw);
|
|
138
|
+
expect(msg.type).toBe("join_room");
|
|
139
|
+
expect(msg.payload).toMatchObject({
|
|
140
|
+
roomId: "room-reconnect",
|
|
141
|
+
accessToken: "token-abc",
|
|
142
|
+
name: "Reconnect User",
|
|
143
|
+
email: "reconnect@example.com",
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
package/src/client.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
Presence,
|
|
10
10
|
PresenceEntry,
|
|
11
11
|
StoredChatMessage,
|
|
12
|
+
JoinRoomPayload,
|
|
12
13
|
} from "./protocol.js";
|
|
13
14
|
import {
|
|
14
15
|
MSG_JOIN_ROOM,
|
|
@@ -34,6 +35,15 @@ export interface LiveSyncClientState {
|
|
|
34
35
|
lastError: { code: string; message: string } | null;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export interface JoinRoomIdentity {
|
|
39
|
+
/** Optional display name if not using accessToken. */
|
|
40
|
+
name?: string;
|
|
41
|
+
/** Optional email if not using accessToken. */
|
|
42
|
+
email?: string;
|
|
43
|
+
/** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */
|
|
44
|
+
accessToken?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
export interface LiveSyncClientConfig {
|
|
38
48
|
/** WebSocket URL (e.g. wss://host/live). */
|
|
39
49
|
url: string;
|
|
@@ -68,7 +78,7 @@ function isServerMessage(msg: unknown): msg is ServerMessage {
|
|
|
68
78
|
export interface LiveSyncClient {
|
|
69
79
|
connect(): void;
|
|
70
80
|
disconnect(): void;
|
|
71
|
-
joinRoom(roomId: string, presence?: Presence,
|
|
81
|
+
joinRoom(roomId: string, presence?: Presence, identity?: JoinRoomIdentity): void;
|
|
72
82
|
leaveRoom(roomId?: string): void;
|
|
73
83
|
updatePresence(presence: Presence): void;
|
|
74
84
|
broadcastEvent(event: string, payload?: unknown): void;
|
|
@@ -97,6 +107,7 @@ export function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClie
|
|
|
97
107
|
let intentionalClose = false;
|
|
98
108
|
let lastPresenceUpdate = 0;
|
|
99
109
|
let pendingPresence: Presence | null = null;
|
|
110
|
+
let lastJoinIdentity: JoinRoomIdentity | null = null;
|
|
100
111
|
|
|
101
112
|
const state: LiveSyncClientState = {
|
|
102
113
|
connectionStatus: "closed",
|
|
@@ -231,7 +242,13 @@ export function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClie
|
|
|
231
242
|
const roomId = state.currentRoomId;
|
|
232
243
|
if (!roomId) return;
|
|
233
244
|
const presence = pendingPresence ?? (state.connectionId ? state.presence[state.connectionId]?.presence : undefined);
|
|
234
|
-
|
|
245
|
+
const identity = lastJoinIdentity;
|
|
246
|
+
const payload: JoinRoomPayload = { roomId };
|
|
247
|
+
if (presence !== undefined) payload.presence = presence;
|
|
248
|
+
if (identity?.accessToken !== undefined) payload.accessToken = identity.accessToken;
|
|
249
|
+
if (identity?.name !== undefined) payload.name = identity.name;
|
|
250
|
+
if (identity?.email !== undefined) payload.email = identity.email;
|
|
251
|
+
send({ type: MSG_JOIN_ROOM, payload });
|
|
235
252
|
}
|
|
236
253
|
|
|
237
254
|
function connect() {
|
|
@@ -307,7 +324,7 @@ export function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClie
|
|
|
307
324
|
setStatus("closed");
|
|
308
325
|
}
|
|
309
326
|
|
|
310
|
-
function joinRoom(roomId: string, presence?: Presence,
|
|
327
|
+
function joinRoom(roomId: string, presence?: Presence, identity?: JoinRoomIdentity) {
|
|
311
328
|
if (state.currentRoomId) {
|
|
312
329
|
send({ type: MSG_LEAVE_ROOM, payload: { roomId: state.currentRoomId } });
|
|
313
330
|
}
|
|
@@ -315,9 +332,12 @@ export function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClie
|
|
|
315
332
|
state.presence = {};
|
|
316
333
|
state.chatMessages = [];
|
|
317
334
|
pendingPresence = presence ?? null;
|
|
318
|
-
|
|
335
|
+
lastJoinIdentity = identity ?? null;
|
|
336
|
+
const payload: JoinRoomPayload = { roomId };
|
|
319
337
|
if (presence !== undefined) payload.presence = presence;
|
|
320
|
-
if (accessToken !== undefined) payload.accessToken = accessToken;
|
|
338
|
+
if (identity?.accessToken !== undefined) payload.accessToken = identity.accessToken;
|
|
339
|
+
if (identity?.name !== undefined) payload.name = identity.name;
|
|
340
|
+
if (identity?.email !== undefined) payload.email = identity.email;
|
|
321
341
|
send({ type: MSG_JOIN_ROOM, payload });
|
|
322
342
|
emit();
|
|
323
343
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface JoinRoomPayload {
|
|
|
28
28
|
presence?: Presence;
|
|
29
29
|
/** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */
|
|
30
30
|
accessToken?: string;
|
|
31
|
+
/** Optional display name if not using accessToken. */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Optional email if not using accessToken. */
|
|
34
|
+
email?: string;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface LeaveRoomPayload {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import React, { type ReactNode } from "react";
|
|
3
|
+
import { renderHook, act } from "@testing-library/react";
|
|
4
|
+
import { LiveSyncProvider, useRoom } from "./react-entry.js";
|
|
5
|
+
import { createLiveSyncClient, type JoinRoomIdentity } from "./client.js";
|
|
6
|
+
|
|
7
|
+
describe("useRoom identity options", () => {
|
|
8
|
+
it("join uses identity from options when no explicit identity is passed", () => {
|
|
9
|
+
const joinSpy: { lastArgs?: [string, unknown, JoinRoomIdentity?] } = {};
|
|
10
|
+
const client = createLiveSyncClient({ url: "ws://localhost/live", reconnect: false });
|
|
11
|
+
// Monkey-patch joinRoom to capture calls
|
|
12
|
+
(client as unknown as {
|
|
13
|
+
joinRoom: (...args: [string, unknown, JoinRoomIdentity?]) => void;
|
|
14
|
+
}).joinRoom = (...args: [string, unknown, JoinRoomIdentity?]) => {
|
|
15
|
+
joinSpy.lastArgs = args;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const wrapper: React.FC<{ children: ReactNode }> = ({ children }) => (
|
|
19
|
+
<LiveSyncProvider client={client}>{children}</LiveSyncProvider>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const { result } = renderHook(
|
|
23
|
+
() => useRoom("room-hook", { name: "Hook User", email: "hook@example.com" }),
|
|
24
|
+
{ wrapper }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
act(() => {
|
|
28
|
+
result.current.join("room-hook");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(joinSpy.lastArgs).toBeDefined();
|
|
32
|
+
const [, , identity] = joinSpy.lastArgs!;
|
|
33
|
+
expect(identity).toMatchObject({
|
|
34
|
+
name: "Hook User",
|
|
35
|
+
email: "hook@example.com",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
package/src/react-entry.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type LiveSyncClient,
|
|
19
19
|
type LiveSyncClientConfig,
|
|
20
20
|
type LiveSyncClientState,
|
|
21
|
+
type JoinRoomIdentity,
|
|
21
22
|
} from "./client.js";
|
|
22
23
|
import type { Presence, PresenceEntry, StoredChatMessage } from "./protocol.js";
|
|
23
24
|
|
|
@@ -112,10 +113,14 @@ export interface UseRoomOptions {
|
|
|
112
113
|
accessToken?: string;
|
|
113
114
|
/** Optional getter for access token (e.g. refreshed token); used when auto-joining. */
|
|
114
115
|
getAccessToken?: () => string | Promise<string>;
|
|
116
|
+
/** Optional display name if not using accessToken. */
|
|
117
|
+
name?: string;
|
|
118
|
+
/** Optional email if not using accessToken. */
|
|
119
|
+
email?: string;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
export interface UseRoomReturn {
|
|
118
|
-
join: (roomId: string, presence?: Presence,
|
|
123
|
+
join: (roomId: string, presence?: Presence, identity?: JoinRoomIdentity) => void;
|
|
119
124
|
leave: (roomId?: string) => void;
|
|
120
125
|
updatePresence: (presence: Presence) => void;
|
|
121
126
|
broadcastEvent: (event: string, payload?: unknown) => void;
|
|
@@ -133,18 +138,29 @@ export function useRoom(
|
|
|
133
138
|
roomId: string | null,
|
|
134
139
|
options: UseRoomOptions = {}
|
|
135
140
|
): UseRoomReturn {
|
|
136
|
-
const {
|
|
141
|
+
const {
|
|
142
|
+
initialPresence,
|
|
143
|
+
autoJoin = true,
|
|
144
|
+
accessToken,
|
|
145
|
+
getAccessToken,
|
|
146
|
+
name,
|
|
147
|
+
email,
|
|
148
|
+
} = options;
|
|
137
149
|
const client = useLiveSyncClient();
|
|
138
150
|
const state = useClientState();
|
|
139
151
|
const joinedRef = useRef<string | null>(null);
|
|
140
152
|
|
|
141
153
|
const join = useCallback(
|
|
142
|
-
(id: string, presence?: Presence,
|
|
143
|
-
const
|
|
144
|
-
|
|
154
|
+
(id: string, presence?: Presence, identity?: JoinRoomIdentity) => {
|
|
155
|
+
const effectiveIdentity: JoinRoomIdentity = identity ?? {
|
|
156
|
+
accessToken,
|
|
157
|
+
name,
|
|
158
|
+
email,
|
|
159
|
+
};
|
|
160
|
+
client.joinRoom(id, presence ?? initialPresence, effectiveIdentity);
|
|
145
161
|
joinedRef.current = id;
|
|
146
162
|
},
|
|
147
|
-
[client, initialPresence, accessToken]
|
|
163
|
+
[client, initialPresence, accessToken, name, email]
|
|
148
164
|
);
|
|
149
165
|
|
|
150
166
|
const leave = useCallback(
|
|
@@ -161,7 +177,12 @@ export function useRoom(
|
|
|
161
177
|
(async () => {
|
|
162
178
|
const token = accessToken ?? (getAccessToken ? await getAccessToken() : undefined);
|
|
163
179
|
if (!cancelled) {
|
|
164
|
-
|
|
180
|
+
const identity: JoinRoomIdentity = {
|
|
181
|
+
accessToken: token,
|
|
182
|
+
name,
|
|
183
|
+
email,
|
|
184
|
+
};
|
|
185
|
+
client.joinRoom(roomId, initialPresence, identity);
|
|
165
186
|
joinedRef.current = roomId;
|
|
166
187
|
}
|
|
167
188
|
})();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/protocol.ts","../src/client.ts"],"sourcesContent":["/**\n * Wire protocol types for @openlivesync/client.\n * Must stay in sync with @openlivesync/server protocol (same message types and payload shapes).\n */\n\n/** Generic presence payload (cursor, name, color, etc.). Server does not interpret. */\nexport type Presence = Record<string, unknown>;\n\n/** User/session info attached by server from auth (optional). */\nexport interface UserInfo {\n userId?: string;\n name?: string;\n email?: string;\n provider?: string;\n [key: string]: unknown;\n}\n\n// ----- Client → Server message types -----\n\nexport const MSG_JOIN_ROOM = \"join_room\";\nexport const MSG_LEAVE_ROOM = \"leave_room\";\nexport const MSG_UPDATE_PRESENCE = \"update_presence\";\nexport const MSG_BROADCAST_EVENT = \"broadcast_event\";\nexport const MSG_SEND_CHAT = \"send_chat\";\n\nexport interface JoinRoomPayload {\n roomId: string;\n presence?: Presence;\n /** Optional OAuth/OpenID access token; server decodes to get name, email, provider. */\n accessToken?: string;\n}\n\nexport interface LeaveRoomPayload {\n roomId?: string;\n}\n\nexport interface UpdatePresencePayload {\n presence: Presence;\n}\n\nexport interface BroadcastEventPayload {\n event: string;\n payload?: unknown;\n}\n\nexport interface SendChatPayload {\n message: string;\n /** Optional application-defined metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport type ClientMessage =\n | { type: typeof MSG_JOIN_ROOM; payload: JoinRoomPayload }\n | { type: typeof MSG_LEAVE_ROOM; payload?: LeaveRoomPayload }\n | { type: typeof MSG_UPDATE_PRESENCE; payload: UpdatePresencePayload }\n | { type: typeof MSG_BROADCAST_EVENT; payload: BroadcastEventPayload }\n | { type: typeof MSG_SEND_CHAT; payload: SendChatPayload };\n\n// ----- Server → Client message types -----\n\nexport const MSG_ROOM_JOINED = \"room_joined\";\nexport const MSG_PRESENCE_UPDATED = \"presence_updated\";\nexport const MSG_BROADCAST_EVENT_RELAY = \"broadcast_event\";\nexport const MSG_CHAT_MESSAGE = \"chat_message\";\nexport const MSG_ERROR = \"error\";\n\nexport interface PresenceEntry {\n connectionId: string;\n userId?: string;\n name?: string;\n email?: string;\n provider?: string;\n presence: Presence;\n}\n\nexport interface RoomJoinedPayload {\n roomId: string;\n connectionId: string;\n presence: Record<string, PresenceEntry>;\n chatHistory?: StoredChatMessage[];\n}\n\nexport interface PresenceUpdatedPayload {\n roomId: string;\n joined?: PresenceEntry[];\n left?: string[];\n updated?: PresenceEntry[];\n}\n\nexport interface BroadcastEventRelayPayload {\n roomId: string;\n connectionId: string;\n userId?: string;\n event: string;\n payload?: unknown;\n}\n\nexport interface ChatMessagePayload {\n roomId: string;\n connectionId: string;\n userId?: string;\n message: string;\n metadata?: Record<string, unknown>;\n id?: string;\n createdAt?: number;\n}\n\nexport interface StoredChatMessage {\n id: string;\n roomId: string;\n connectionId: string;\n userId?: string;\n message: string;\n metadata?: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface ErrorPayload {\n code: string;\n message: string;\n}\n\nexport type ServerMessage =\n | { type: typeof MSG_ROOM_JOINED; payload: RoomJoinedPayload }\n | { type: typeof MSG_PRESENCE_UPDATED; payload: PresenceUpdatedPayload }\n | { type: typeof MSG_BROADCAST_EVENT_RELAY; payload: BroadcastEventRelayPayload }\n | { type: typeof MSG_CHAT_MESSAGE; payload: ChatMessagePayload }\n | { type: typeof MSG_ERROR; payload: ErrorPayload };\n\n/** Chat message as provided when appending (before storage adds id/createdAt). */\nexport interface ChatMessageInput {\n roomId: string;\n connectionId: string;\n userId?: string;\n message: string;\n metadata?: Record<string, unknown>;\n}\n","/**\n * Core WebSocket client for @openlivesync.\n * Connects to @openlivesync/server, manages room/presence/chat state, and notifies subscribers.\n */\n\nimport type {\n ClientMessage,\n ServerMessage,\n Presence,\n PresenceEntry,\n StoredChatMessage,\n} from \"./protocol.js\";\nimport {\n MSG_JOIN_ROOM,\n MSG_LEAVE_ROOM,\n MSG_UPDATE_PRESENCE,\n MSG_BROADCAST_EVENT,\n MSG_SEND_CHAT,\n MSG_ROOM_JOINED,\n MSG_PRESENCE_UPDATED,\n MSG_BROADCAST_EVENT_RELAY,\n MSG_CHAT_MESSAGE,\n MSG_ERROR,\n} from \"./protocol.js\";\n\nexport type ConnectionStatus = \"connecting\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface LiveSyncClientState {\n connectionStatus: ConnectionStatus;\n currentRoomId: string | null;\n connectionId: string | null;\n presence: Record<string, PresenceEntry>;\n chatMessages: StoredChatMessage[];\n lastError: { code: string; message: string } | null;\n}\n\nexport interface LiveSyncClientConfig {\n /** WebSocket URL (e.g. wss://host/live). */\n url: string;\n /** Auto-reconnect on close (default true). */\n reconnect?: boolean;\n /** Initial reconnect delay in ms (default 1000). */\n reconnectIntervalMs?: number;\n /** Max reconnect delay in ms (default 30000). */\n maxReconnectIntervalMs?: number;\n /** Optional: return token for auth; appended as query param (e.g. ?access_token=). */\n getAuthToken?: () => string | Promise<string>;\n /** Throttle presence updates in ms (default 100, match server). */\n presenceThrottleMs?: number;\n}\n\nconst DEFAULT_RECONNECT_INTERVAL_MS = 1000;\nconst DEFAULT_MAX_RECONNECT_INTERVAL_MS = 30000;\nconst DEFAULT_PRESENCE_THROTTLE_MS = 100;\n\nfunction isServerMessage(msg: unknown): msg is ServerMessage {\n if (msg === null || typeof msg !== \"object\" || !(\"type\" in msg)) return false;\n const t = (msg as { type: string }).type;\n return [\n MSG_ROOM_JOINED,\n MSG_PRESENCE_UPDATED,\n MSG_BROADCAST_EVENT_RELAY,\n MSG_CHAT_MESSAGE,\n MSG_ERROR,\n ].includes(t);\n}\n\nexport interface LiveSyncClient {\n connect(): void;\n disconnect(): void;\n joinRoom(roomId: string, presence?: Presence, accessToken?: string): void;\n leaveRoom(roomId?: string): void;\n updatePresence(presence: Presence): void;\n broadcastEvent(event: string, payload?: unknown): void;\n sendChat(message: string, metadata?: Record<string, unknown>): void;\n getConnectionStatus(): ConnectionStatus;\n getPresence(): Record<string, PresenceEntry>;\n getChatMessages(): StoredChatMessage[];\n getCurrentRoomId(): string | null;\n getState(): LiveSyncClientState;\n subscribe(listener: (state: LiveSyncClientState) => void): () => void;\n}\n\nexport function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClient {\n const {\n url: baseUrl,\n reconnect: reconnectEnabled = true,\n reconnectIntervalMs = DEFAULT_RECONNECT_INTERVAL_MS,\n maxReconnectIntervalMs = DEFAULT_MAX_RECONNECT_INTERVAL_MS,\n getAuthToken,\n presenceThrottleMs = DEFAULT_PRESENCE_THROTTLE_MS,\n } = config;\n\n let ws: WebSocket | null = null;\n let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null;\n let nextReconnectMs = reconnectIntervalMs;\n let intentionalClose = false;\n let lastPresenceUpdate = 0;\n let pendingPresence: Presence | null = null;\n\n const state: LiveSyncClientState = {\n connectionStatus: \"closed\",\n currentRoomId: null,\n connectionId: null,\n presence: {},\n chatMessages: [],\n lastError: null,\n };\n\n const listeners = new Set<(s: LiveSyncClientState) => void>();\n\n function emit() {\n const snapshot: LiveSyncClientState = {\n connectionStatus: state.connectionStatus,\n currentRoomId: state.currentRoomId,\n connectionId: state.connectionId,\n presence: { ...state.presence },\n chatMessages: [...state.chatMessages],\n lastError: state.lastError ? { ...state.lastError } : null,\n };\n listeners.forEach((cb) => cb(snapshot));\n }\n\n function setStatus(status: ConnectionStatus) {\n state.connectionStatus = status;\n emit();\n }\n\n function send(msg: ClientMessage) {\n if (!ws || ws.readyState !== WebSocket.OPEN) return;\n ws.send(JSON.stringify(msg));\n }\n\n function clearReconnect() {\n if (reconnectTimeoutId !== null) {\n clearTimeout(reconnectTimeoutId);\n reconnectTimeoutId = null;\n }\n nextReconnectMs = reconnectIntervalMs;\n }\n\n function scheduleReconnect() {\n if (!reconnectEnabled || intentionalClose) return;\n clearReconnect();\n reconnectTimeoutId = setTimeout(() => {\n reconnectTimeoutId = null;\n nextReconnectMs = Math.min(\n nextReconnectMs * 2,\n maxReconnectIntervalMs\n );\n connect();\n }, nextReconnectMs);\n }\n\n function applyPresenceUpdated(\n joined?: PresenceEntry[],\n left?: string[],\n updated?: PresenceEntry[]\n ) {\n if (joined) {\n for (const e of joined) state.presence[e.connectionId] = e;\n }\n if (left) {\n for (const id of left) delete state.presence[id];\n }\n if (updated) {\n for (const e of updated) state.presence[e.connectionId] = e;\n }\n emit();\n }\n\n function handleMessage(data: string) {\n let msg: unknown;\n try {\n msg = JSON.parse(data) as unknown;\n } catch {\n state.lastError = { code: \"INVALID_JSON\", message: \"Invalid JSON from server\" };\n emit();\n return;\n }\n if (!isServerMessage(msg)) {\n state.lastError = { code: \"UNKNOWN_MESSAGE\", message: \"Unknown message type\" };\n emit();\n return;\n }\n switch (msg.type) {\n case MSG_ROOM_JOINED: {\n const { roomId, connectionId, presence, chatHistory } = msg.payload;\n state.currentRoomId = roomId;\n state.connectionId = connectionId;\n state.presence = presence ?? {};\n state.chatMessages = chatHistory ?? [];\n state.lastError = null;\n emit();\n break;\n }\n case MSG_PRESENCE_UPDATED: {\n const { joined, left, updated } = msg.payload;\n applyPresenceUpdated(joined, left, updated);\n break;\n }\n case MSG_BROADCAST_EVENT_RELAY:\n // Application can subscribe to custom events if we add an event emitter; for now we only update state for presence/chat.\n break;\n case MSG_CHAT_MESSAGE: {\n const p = msg.payload;\n const stored: StoredChatMessage = {\n id: p.id ?? `${p.connectionId}-${p.createdAt ?? Date.now()}`,\n roomId: p.roomId,\n connectionId: p.connectionId,\n userId: p.userId,\n message: p.message,\n metadata: p.metadata,\n createdAt: p.createdAt ?? Date.now(),\n };\n if (state.currentRoomId === p.roomId) {\n state.chatMessages = [...state.chatMessages, stored];\n emit();\n }\n break;\n }\n case MSG_ERROR: {\n state.lastError = msg.payload;\n emit();\n break;\n }\n }\n }\n\n function reconnectAndRejoin() {\n const roomId = state.currentRoomId;\n if (!roomId) return;\n const presence = pendingPresence ?? (state.connectionId ? state.presence[state.connectionId]?.presence : undefined);\n send({ type: MSG_JOIN_ROOM, payload: { roomId, presence } });\n }\n\n function connect() {\n if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {\n return;\n }\n intentionalClose = false;\n setStatus(\"connecting\");\n\n (async () => {\n let url = baseUrl;\n if (getAuthToken) {\n try {\n const token = await getAuthToken();\n if (token) {\n const sep = baseUrl.includes(\"?\") ? \"&\" : \"?\";\n url = `${baseUrl}${sep}access_token=${encodeURIComponent(token)}`;\n }\n } catch (e) {\n state.lastError = {\n code: \"AUTH_ERROR\",\n message: e instanceof Error ? e.message : String(e),\n };\n setStatus(\"closed\");\n emit();\n return;\n }\n }\n\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n setStatus(\"open\");\n nextReconnectMs = reconnectIntervalMs;\n reconnectAndRejoin();\n };\n\n ws.onmessage = (event) => {\n const data = typeof event.data === \"string\" ? event.data : event.data.toString();\n handleMessage(data);\n };\n\n ws.onclose = () => {\n ws = null;\n setStatus(\"closed\");\n if (!intentionalClose) {\n scheduleReconnect();\n } else {\n clearReconnect();\n }\n };\n\n ws.onerror = () => {\n state.lastError = { code: \"WEBSOCKET_ERROR\", message: \"WebSocket error\" };\n emit();\n };\n })();\n }\n\n function disconnect() {\n intentionalClose = true;\n clearReconnect();\n state.currentRoomId = null;\n state.connectionId = null;\n state.presence = {};\n state.chatMessages = [];\n pendingPresence = null;\n if (ws) {\n setStatus(\"closing\");\n ws.close();\n ws = null;\n }\n setStatus(\"closed\");\n }\n\n function joinRoom(roomId: string, presence?: Presence, accessToken?: string) {\n if (state.currentRoomId) {\n send({ type: MSG_LEAVE_ROOM, payload: { roomId: state.currentRoomId } });\n }\n state.currentRoomId = roomId;\n state.presence = {};\n state.chatMessages = [];\n pendingPresence = presence ?? null;\n const payload: { roomId: string; presence?: Presence; accessToken?: string } = { roomId };\n if (presence !== undefined) payload.presence = presence;\n if (accessToken !== undefined) payload.accessToken = accessToken;\n send({ type: MSG_JOIN_ROOM, payload });\n emit();\n }\n\n function leaveRoom(roomId?: string) {\n const target = roomId ?? state.currentRoomId;\n if (target) {\n send({ type: MSG_LEAVE_ROOM, payload: { roomId: target } });\n if (target === state.currentRoomId) {\n state.currentRoomId = null;\n state.connectionId = null;\n state.presence = {};\n state.chatMessages = [];\n pendingPresence = null;\n }\n emit();\n }\n }\n\n function updatePresence(presence: Presence) {\n pendingPresence = presence;\n const now = Date.now();\n if (now - lastPresenceUpdate < presenceThrottleMs) return;\n lastPresenceUpdate = now;\n send({ type: MSG_UPDATE_PRESENCE, payload: { presence } });\n }\n\n function broadcastEvent(event: string, payload?: unknown) {\n send({ type: MSG_BROADCAST_EVENT, payload: { event, payload } });\n }\n\n function sendChat(message: string, metadata?: Record<string, unknown>) {\n send({ type: MSG_SEND_CHAT, payload: { message, metadata } });\n }\n\n function subscribe(listener: (state: LiveSyncClientState) => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n }\n\n return {\n connect,\n disconnect,\n joinRoom,\n leaveRoom,\n updatePresence,\n broadcastEvent,\n sendChat,\n getConnectionStatus: () => state.connectionStatus,\n getPresence: () => ({ ...state.presence }),\n getChatMessages: () => [...state.chatMessages],\n getCurrentRoomId: () => state.currentRoomId,\n getState: () => ({\n connectionStatus: state.connectionStatus,\n currentRoomId: state.currentRoomId,\n connectionId: state.connectionId,\n presence: { ...state.presence },\n chatMessages: [...state.chatMessages],\n lastError: state.lastError ? { ...state.lastError } : null,\n }),\n subscribe,\n };\n}\n"],"mappings":";AAmBO,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AACvB,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB;AAC5B,IAAM,gBAAgB;AAqCtB,IAAM,kBAAkB;AACxB,IAAM,uBAAuB;AAC7B,IAAM,4BAA4B;AAClC,IAAM,mBAAmB;AACzB,IAAM,YAAY;;;ACbzB,IAAM,gCAAgC;AACtC,IAAM,oCAAoC;AAC1C,IAAM,+BAA+B;AAErC,SAAS,gBAAgB,KAAoC;AAC3D,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,EAAE,UAAU,KAAM,QAAO;AACxE,QAAM,IAAK,IAAyB;AACpC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,SAAS,CAAC;AACd;AAkBO,SAAS,qBAAqB,QAA8C;AACjF,QAAM;AAAA,IACJ,KAAK;AAAA,IACL,WAAW,mBAAmB;AAAA,IAC9B,sBAAsB;AAAA,IACtB,yBAAyB;AAAA,IACzB;AAAA,IACA,qBAAqB;AAAA,EACvB,IAAI;AAEJ,MAAI,KAAuB;AAC3B,MAAI,qBAA2D;AAC/D,MAAI,kBAAkB;AACtB,MAAI,mBAAmB;AACvB,MAAI,qBAAqB;AACzB,MAAI,kBAAmC;AAEvC,QAAM,QAA6B;AAAA,IACjC,kBAAkB;AAAA,IAClB,eAAe;AAAA,IACf,cAAc;AAAA,IACd,UAAU,CAAC;AAAA,IACX,cAAc,CAAC;AAAA,IACf,WAAW;AAAA,EACb;AAEA,QAAM,YAAY,oBAAI,IAAsC;AAE5D,WAAS,OAAO;AACd,UAAM,WAAgC;AAAA,MACpC,kBAAkB,MAAM;AAAA,MACxB,eAAe,MAAM;AAAA,MACrB,cAAc,MAAM;AAAA,MACpB,UAAU,EAAE,GAAG,MAAM,SAAS;AAAA,MAC9B,cAAc,CAAC,GAAG,MAAM,YAAY;AAAA,MACpC,WAAW,MAAM,YAAY,EAAE,GAAG,MAAM,UAAU,IAAI;AAAA,IACxD;AACA,cAAU,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC;AAAA,EACxC;AAEA,WAAS,UAAU,QAA0B;AAC3C,UAAM,mBAAmB;AACzB,SAAK;AAAA,EACP;AAEA,WAAS,KAAK,KAAoB;AAChC,QAAI,CAAC,MAAM,GAAG,eAAe,UAAU,KAAM;AAC7C,OAAG,KAAK,KAAK,UAAU,GAAG,CAAC;AAAA,EAC7B;AAEA,WAAS,iBAAiB;AACxB,QAAI,uBAAuB,MAAM;AAC/B,mBAAa,kBAAkB;AAC/B,2BAAqB;AAAA,IACvB;AACA,sBAAkB;AAAA,EACpB;AAEA,WAAS,oBAAoB;AAC3B,QAAI,CAAC,oBAAoB,iBAAkB;AAC3C,mBAAe;AACf,yBAAqB,WAAW,MAAM;AACpC,2BAAqB;AACrB,wBAAkB,KAAK;AAAA,QACrB,kBAAkB;AAAA,QAClB;AAAA,MACF;AACA,cAAQ;AAAA,IACV,GAAG,eAAe;AAAA,EACpB;AAEA,WAAS,qBACP,QACA,MACA,SACA;AACA,QAAI,QAAQ;AACV,iBAAW,KAAK,OAAQ,OAAM,SAAS,EAAE,YAAY,IAAI;AAAA,IAC3D;AACA,QAAI,MAAM;AACR,iBAAW,MAAM,KAAM,QAAO,MAAM,SAAS,EAAE;AAAA,IACjD;AACA,QAAI,SAAS;AACX,iBAAW,KAAK,QAAS,OAAM,SAAS,EAAE,YAAY,IAAI;AAAA,IAC5D;AACA,SAAK;AAAA,EACP;AAEA,WAAS,cAAc,MAAc;AACnC,QAAI;AACJ,QAAI;AACF,YAAM,KAAK,MAAM,IAAI;AAAA,IACvB,QAAQ;AACN,YAAM,YAAY,EAAE,MAAM,gBAAgB,SAAS,2BAA2B;AAC9E,WAAK;AACL;AAAA,IACF;AACA,QAAI,CAAC,gBAAgB,GAAG,GAAG;AACzB,YAAM,YAAY,EAAE,MAAM,mBAAmB,SAAS,uBAAuB;AAC7E,WAAK;AACL;AAAA,IACF;AACA,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK,iBAAiB;AACpB,cAAM,EAAE,QAAQ,cAAc,UAAU,YAAY,IAAI,IAAI;AAC5D,cAAM,gBAAgB;AACtB,cAAM,eAAe;AACrB,cAAM,WAAW,YAAY,CAAC;AAC9B,cAAM,eAAe,eAAe,CAAC;AACrC,cAAM,YAAY;AAClB,aAAK;AACL;AAAA,MACF;AAAA,MACA,KAAK,sBAAsB;AACzB,cAAM,EAAE,QAAQ,MAAM,QAAQ,IAAI,IAAI;AACtC,6BAAqB,QAAQ,MAAM,OAAO;AAC1C;AAAA,MACF;AAAA,MACA,KAAK;AAEH;AAAA,MACF,KAAK,kBAAkB;AACrB,cAAM,IAAI,IAAI;AACd,cAAM,SAA4B;AAAA,UAChC,IAAI,EAAE,MAAM,GAAG,EAAE,YAAY,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;AAAA,UAC1D,QAAQ,EAAE;AAAA,UACV,cAAc,EAAE;AAAA,UAChB,QAAQ,EAAE;AAAA,UACV,SAAS,EAAE;AAAA,UACX,UAAU,EAAE;AAAA,UACZ,WAAW,EAAE,aAAa,KAAK,IAAI;AAAA,QACrC;AACA,YAAI,MAAM,kBAAkB,EAAE,QAAQ;AACpC,gBAAM,eAAe,CAAC,GAAG,MAAM,cAAc,MAAM;AACnD,eAAK;AAAA,QACP;AACA;AAAA,MACF;AAAA,MACA,KAAK,WAAW;AACd,cAAM,YAAY,IAAI;AACtB,aAAK;AACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,WAAS,qBAAqB;AAC5B,UAAM,SAAS,MAAM;AACrB,QAAI,CAAC,OAAQ;AACb,UAAM,WAAW,oBAAoB,MAAM,eAAe,MAAM,SAAS,MAAM,YAAY,GAAG,WAAW;AACzG,SAAK,EAAE,MAAM,eAAe,SAAS,EAAE,QAAQ,SAAS,EAAE,CAAC;AAAA,EAC7D;AAEA,WAAS,UAAU;AACjB,QAAI,IAAI,eAAe,UAAU,QAAQ,IAAI,eAAe,UAAU,YAAY;AAChF;AAAA,IACF;AACA,uBAAmB;AACnB,cAAU,YAAY;AAEtB,KAAC,YAAY;AACX,UAAI,MAAM;AACV,UAAI,cAAc;AAChB,YAAI;AACF,gBAAM,QAAQ,MAAM,aAAa;AACjC,cAAI,OAAO;AACT,kBAAM,MAAM,QAAQ,SAAS,GAAG,IAAI,MAAM;AAC1C,kBAAM,GAAG,OAAO,GAAG,GAAG,gBAAgB,mBAAmB,KAAK,CAAC;AAAA,UACjE;AAAA,QACF,SAAS,GAAG;AACV,gBAAM,YAAY;AAAA,YAChB,MAAM;AAAA,YACN,SAAS,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,UACpD;AACA,oBAAU,QAAQ;AAClB,eAAK;AACL;AAAA,QACF;AAAA,MACF;AAEA,WAAK,IAAI,UAAU,GAAG;AAEtB,SAAG,SAAS,MAAM;AAChB,kBAAU,MAAM;AAChB,0BAAkB;AAClB,2BAAmB;AAAA,MACrB;AAEA,SAAG,YAAY,CAAC,UAAU;AACxB,cAAM,OAAO,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,MAAM,KAAK,SAAS;AAC/E,sBAAc,IAAI;AAAA,MACpB;AAEA,SAAG,UAAU,MAAM;AACjB,aAAK;AACL,kBAAU,QAAQ;AAClB,YAAI,CAAC,kBAAkB;AACrB,4BAAkB;AAAA,QACpB,OAAO;AACL,yBAAe;AAAA,QACjB;AAAA,MACF;AAEA,SAAG,UAAU,MAAM;AACjB,cAAM,YAAY,EAAE,MAAM,mBAAmB,SAAS,kBAAkB;AACxE,aAAK;AAAA,MACP;AAAA,IACF,GAAG;AAAA,EACL;AAEA,WAAS,aAAa;AACpB,uBAAmB;AACnB,mBAAe;AACf,UAAM,gBAAgB;AACtB,UAAM,eAAe;AACrB,UAAM,WAAW,CAAC;AAClB,UAAM,eAAe,CAAC;AACtB,sBAAkB;AAClB,QAAI,IAAI;AACN,gBAAU,SAAS;AACnB,SAAG,MAAM;AACT,WAAK;AAAA,IACP;AACA,cAAU,QAAQ;AAAA,EACpB;AAEA,WAAS,SAAS,QAAgB,UAAqB,aAAsB;AAC3E,QAAI,MAAM,eAAe;AACvB,WAAK,EAAE,MAAM,gBAAgB,SAAS,EAAE,QAAQ,MAAM,cAAc,EAAE,CAAC;AAAA,IACzE;AACA,UAAM,gBAAgB;AACtB,UAAM,WAAW,CAAC;AAClB,UAAM,eAAe,CAAC;AACtB,sBAAkB,YAAY;AAC9B,UAAM,UAAyE,EAAE,OAAO;AACxF,QAAI,aAAa,OAAW,SAAQ,WAAW;AAC/C,QAAI,gBAAgB,OAAW,SAAQ,cAAc;AACrD,SAAK,EAAE,MAAM,eAAe,QAAQ,CAAC;AACrC,SAAK;AAAA,EACP;AAEA,WAAS,UAAU,QAAiB;AAClC,UAAM,SAAS,UAAU,MAAM;AAC/B,QAAI,QAAQ;AACV,WAAK,EAAE,MAAM,gBAAgB,SAAS,EAAE,QAAQ,OAAO,EAAE,CAAC;AAC1D,UAAI,WAAW,MAAM,eAAe;AAClC,cAAM,gBAAgB;AACtB,cAAM,eAAe;AACrB,cAAM,WAAW,CAAC;AAClB,cAAM,eAAe,CAAC;AACtB,0BAAkB;AAAA,MACpB;AACA,WAAK;AAAA,IACP;AAAA,EACF;AAEA,WAAS,eAAe,UAAoB;AAC1C,sBAAkB;AAClB,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,qBAAqB,mBAAoB;AACnD,yBAAqB;AACrB,SAAK,EAAE,MAAM,qBAAqB,SAAS,EAAE,SAAS,EAAE,CAAC;AAAA,EAC3D;AAEA,WAAS,eAAe,OAAe,SAAmB;AACxD,SAAK,EAAE,MAAM,qBAAqB,SAAS,EAAE,OAAO,QAAQ,EAAE,CAAC;AAAA,EACjE;AAEA,WAAS,SAAS,SAAiB,UAAoC;AACrE,SAAK,EAAE,MAAM,eAAe,SAAS,EAAE,SAAS,SAAS,EAAE,CAAC;AAAA,EAC9D;AAEA,WAAS,UAAU,UAA4D;AAC7E,cAAU,IAAI,QAAQ;AACtB,WAAO,MAAM,UAAU,OAAO,QAAQ;AAAA,EACxC;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB,MAAM,MAAM;AAAA,IACjC,aAAa,OAAO,EAAE,GAAG,MAAM,SAAS;AAAA,IACxC,iBAAiB,MAAM,CAAC,GAAG,MAAM,YAAY;AAAA,IAC7C,kBAAkB,MAAM,MAAM;AAAA,IAC9B,UAAU,OAAO;AAAA,MACf,kBAAkB,MAAM;AAAA,MACxB,eAAe,MAAM;AAAA,MACrB,cAAc,MAAM;AAAA,MACpB,UAAU,EAAE,GAAG,MAAM,SAAS;AAAA,MAC9B,cAAc,CAAC,GAAG,MAAM,YAAY;AAAA,MACpC,WAAW,MAAM,YAAY,EAAE,GAAG,MAAM,UAAU,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|