@openlivesync/client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/dist/chunk-MINJGWLX.js +311 -0
- package/dist/chunk-MINJGWLX.js.map +1 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/react-entry.d.ts +63 -0
- package/dist/react-entry.js +151 -0
- package/dist/react-entry.js.map +1 -0
- package/package.json +32 -0
- package/src/client.ts +383 -0
- package/src/index.ts +43 -0
- package/src/protocol.ts +137 -0
- package/src/react-entry.tsx +230 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @openlivesync/client
|
|
2
|
+
|
|
3
|
+
Browser client for **OpenLiveSync**: connect to an `@openlivesync/server` over WebSocket for **presence**, **broadcast events**, and **chat**. Use the core API or the optional React hooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @openlivesync/client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For React hooks (optional):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @openlivesync/client react
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Core usage
|
|
18
|
+
|
|
19
|
+
Create a client, connect, and join a room. Subscribe to state updates:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createLiveSyncClient } from "@openlivesync/client";
|
|
23
|
+
|
|
24
|
+
const client = createLiveSyncClient({
|
|
25
|
+
url: "wss://localhost:3000/live",
|
|
26
|
+
reconnect: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
client.connect();
|
|
30
|
+
client.joinRoom("room1", { name: "Alice", color: "#00f" });
|
|
31
|
+
// Optional: pass access token so server can decode name/email (OAuth)
|
|
32
|
+
// client.joinRoom("room1", { name: "Alice" }, accessToken);
|
|
33
|
+
client.subscribe((state) => console.log(state.presence));
|
|
34
|
+
|
|
35
|
+
client.updatePresence({ cursor: { x: 10, y: 20 } });
|
|
36
|
+
client.broadcastEvent("cursor_move", { x: 10, y: 20 });
|
|
37
|
+
client.sendChat("Hello!");
|
|
38
|
+
client.leaveRoom();
|
|
39
|
+
client.disconnect();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## React usage
|
|
43
|
+
|
|
44
|
+
Wrap your app in `LiveSyncProvider` (pass a pre-created `client` or config like `url`), then use hooks:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { LiveSyncProvider, useRoom, useChat, useConnectionStatus } from "@openlivesync/client/react";
|
|
48
|
+
|
|
49
|
+
// With config (provider creates client and connects on mount)
|
|
50
|
+
function App() {
|
|
51
|
+
return (
|
|
52
|
+
<LiveSyncProvider url="wss://localhost:3000/live">
|
|
53
|
+
<RoomUI roomId="room1" />
|
|
54
|
+
</LiveSyncProvider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function RoomUI({ roomId }: { roomId: string }) {
|
|
59
|
+
const status = useConnectionStatus();
|
|
60
|
+
const { presence, join, leave, broadcastEvent } = useRoom(roomId);
|
|
61
|
+
const { messages, sendMessage } = useChat(roomId);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
<p>Status: {status}</p>
|
|
66
|
+
<button onClick={() => leave()}>Leave</button>
|
|
67
|
+
<ul>
|
|
68
|
+
{Object.entries(presence).map(([id, e]) => (
|
|
69
|
+
<li key={id}>{String(e.presence?.name ?? id)}</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
{messages.map((m) => (
|
|
73
|
+
<p key={m.id}>{m.message}</p>
|
|
74
|
+
))}
|
|
75
|
+
<button onClick={() => sendMessage("Hi!")}>Send</button>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or pass a pre-created client:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { createLiveSyncClient } from "@openlivesync/client";
|
|
85
|
+
import { LiveSyncProvider } from "@openlivesync/client/react";
|
|
86
|
+
|
|
87
|
+
const client = createLiveSyncClient({ url: "wss://localhost:3000/live" });
|
|
88
|
+
client.connect();
|
|
89
|
+
|
|
90
|
+
<LiveSyncProvider client={client}>
|
|
91
|
+
<App />
|
|
92
|
+
</LiveSyncProvider>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
### Core (`@openlivesync/client`)
|
|
98
|
+
|
|
99
|
+
- **`createLiveSyncClient(config)`** — Returns a client. Options: `url`, `reconnect?`, `reconnectIntervalMs?`, `maxReconnectIntervalMs?`, `getAuthToken?`, `presenceThrottleMs?`.
|
|
100
|
+
- **Client methods**: `connect()`, `disconnect()`, `joinRoom(roomId, presence?, accessToken?)`, `leaveRoom(roomId?)`, `updatePresence(presence)`, `broadcastEvent(event, payload?)`, `sendChat(message, metadata?)`, `getState()`, `subscribe(listener)`. If you pass `accessToken`, the server (when configured with `auth`) decodes it and attaches name/email to your connection; other clients see them in presence. **Authenticate once at connect:** 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
|
+
|
|
102
|
+
### React (`@openlivesync/client/react`)
|
|
103
|
+
|
|
104
|
+
- **`LiveSyncProvider`** — Props: `client?` or `url?` (+ optional reconnect/auth/presence options). If `url` is provided, the provider creates the client and connects on mount.
|
|
105
|
+
- **`useLiveSyncClient()`** — Returns the client from context.
|
|
106
|
+
- **`useConnectionStatus()`** — Returns `"connecting" | "open" | "closing" | "closed"`.
|
|
107
|
+
- **`useRoom(roomId, options?)`** — Returns `{ join, leave, updatePresence, broadcastEvent, presence, connectionId, isInRoom, currentRoomId }`. Options: `initialPresence?`, `autoJoin?`, `accessToken?`, `getAccessToken?`. With `autoJoin: true` (default), joins when `roomId` is set (using `accessToken` or `getAccessToken()` if provided) and leaves on unmount or when `roomId` changes. `join(roomId, presence?, accessToken?)` for manual join. For connect-only auth (token sent once at connect via provider's `getAuthToken`), omit `accessToken` and `getAccessToken` here.
|
|
108
|
+
- **`usePresence(roomId)`** — Returns the presence map for the current room.
|
|
109
|
+
- **`useChat(roomId)`** — Returns `{ messages, sendMessage }`.
|
|
110
|
+
|
|
111
|
+
Protocol types and `MSG_*` constants are exported from the main entry for typing or custom handling.
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
var MSG_JOIN_ROOM = "join_room";
|
|
3
|
+
var MSG_LEAVE_ROOM = "leave_room";
|
|
4
|
+
var MSG_UPDATE_PRESENCE = "update_presence";
|
|
5
|
+
var MSG_BROADCAST_EVENT = "broadcast_event";
|
|
6
|
+
var MSG_SEND_CHAT = "send_chat";
|
|
7
|
+
var MSG_ROOM_JOINED = "room_joined";
|
|
8
|
+
var MSG_PRESENCE_UPDATED = "presence_updated";
|
|
9
|
+
var MSG_BROADCAST_EVENT_RELAY = "broadcast_event";
|
|
10
|
+
var MSG_CHAT_MESSAGE = "chat_message";
|
|
11
|
+
var MSG_ERROR = "error";
|
|
12
|
+
|
|
13
|
+
// src/client.ts
|
|
14
|
+
var DEFAULT_RECONNECT_INTERVAL_MS = 1e3;
|
|
15
|
+
var DEFAULT_MAX_RECONNECT_INTERVAL_MS = 3e4;
|
|
16
|
+
var DEFAULT_PRESENCE_THROTTLE_MS = 100;
|
|
17
|
+
function isServerMessage(msg) {
|
|
18
|
+
if (msg === null || typeof msg !== "object" || !("type" in msg)) return false;
|
|
19
|
+
const t = msg.type;
|
|
20
|
+
return [
|
|
21
|
+
MSG_ROOM_JOINED,
|
|
22
|
+
MSG_PRESENCE_UPDATED,
|
|
23
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
24
|
+
MSG_CHAT_MESSAGE,
|
|
25
|
+
MSG_ERROR
|
|
26
|
+
].includes(t);
|
|
27
|
+
}
|
|
28
|
+
function createLiveSyncClient(config) {
|
|
29
|
+
const {
|
|
30
|
+
url: baseUrl,
|
|
31
|
+
reconnect: reconnectEnabled = true,
|
|
32
|
+
reconnectIntervalMs = DEFAULT_RECONNECT_INTERVAL_MS,
|
|
33
|
+
maxReconnectIntervalMs = DEFAULT_MAX_RECONNECT_INTERVAL_MS,
|
|
34
|
+
getAuthToken,
|
|
35
|
+
presenceThrottleMs = DEFAULT_PRESENCE_THROTTLE_MS
|
|
36
|
+
} = config;
|
|
37
|
+
let ws = null;
|
|
38
|
+
let reconnectTimeoutId = null;
|
|
39
|
+
let nextReconnectMs = reconnectIntervalMs;
|
|
40
|
+
let intentionalClose = false;
|
|
41
|
+
let lastPresenceUpdate = 0;
|
|
42
|
+
let pendingPresence = null;
|
|
43
|
+
const state = {
|
|
44
|
+
connectionStatus: "closed",
|
|
45
|
+
currentRoomId: null,
|
|
46
|
+
connectionId: null,
|
|
47
|
+
presence: {},
|
|
48
|
+
chatMessages: [],
|
|
49
|
+
lastError: null
|
|
50
|
+
};
|
|
51
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
52
|
+
function emit() {
|
|
53
|
+
const snapshot = {
|
|
54
|
+
connectionStatus: state.connectionStatus,
|
|
55
|
+
currentRoomId: state.currentRoomId,
|
|
56
|
+
connectionId: state.connectionId,
|
|
57
|
+
presence: { ...state.presence },
|
|
58
|
+
chatMessages: [...state.chatMessages],
|
|
59
|
+
lastError: state.lastError ? { ...state.lastError } : null
|
|
60
|
+
};
|
|
61
|
+
listeners.forEach((cb) => cb(snapshot));
|
|
62
|
+
}
|
|
63
|
+
function setStatus(status) {
|
|
64
|
+
state.connectionStatus = status;
|
|
65
|
+
emit();
|
|
66
|
+
}
|
|
67
|
+
function send(msg) {
|
|
68
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
69
|
+
ws.send(JSON.stringify(msg));
|
|
70
|
+
}
|
|
71
|
+
function clearReconnect() {
|
|
72
|
+
if (reconnectTimeoutId !== null) {
|
|
73
|
+
clearTimeout(reconnectTimeoutId);
|
|
74
|
+
reconnectTimeoutId = null;
|
|
75
|
+
}
|
|
76
|
+
nextReconnectMs = reconnectIntervalMs;
|
|
77
|
+
}
|
|
78
|
+
function scheduleReconnect() {
|
|
79
|
+
if (!reconnectEnabled || intentionalClose) return;
|
|
80
|
+
clearReconnect();
|
|
81
|
+
reconnectTimeoutId = setTimeout(() => {
|
|
82
|
+
reconnectTimeoutId = null;
|
|
83
|
+
nextReconnectMs = Math.min(
|
|
84
|
+
nextReconnectMs * 2,
|
|
85
|
+
maxReconnectIntervalMs
|
|
86
|
+
);
|
|
87
|
+
connect();
|
|
88
|
+
}, nextReconnectMs);
|
|
89
|
+
}
|
|
90
|
+
function applyPresenceUpdated(joined, left, updated) {
|
|
91
|
+
if (joined) {
|
|
92
|
+
for (const e of joined) state.presence[e.connectionId] = e;
|
|
93
|
+
}
|
|
94
|
+
if (left) {
|
|
95
|
+
for (const id of left) delete state.presence[id];
|
|
96
|
+
}
|
|
97
|
+
if (updated) {
|
|
98
|
+
for (const e of updated) state.presence[e.connectionId] = e;
|
|
99
|
+
}
|
|
100
|
+
emit();
|
|
101
|
+
}
|
|
102
|
+
function handleMessage(data) {
|
|
103
|
+
let msg;
|
|
104
|
+
try {
|
|
105
|
+
msg = JSON.parse(data);
|
|
106
|
+
} catch {
|
|
107
|
+
state.lastError = { code: "INVALID_JSON", message: "Invalid JSON from server" };
|
|
108
|
+
emit();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!isServerMessage(msg)) {
|
|
112
|
+
state.lastError = { code: "UNKNOWN_MESSAGE", message: "Unknown message type" };
|
|
113
|
+
emit();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
switch (msg.type) {
|
|
117
|
+
case MSG_ROOM_JOINED: {
|
|
118
|
+
const { roomId, connectionId, presence, chatHistory } = msg.payload;
|
|
119
|
+
state.currentRoomId = roomId;
|
|
120
|
+
state.connectionId = connectionId;
|
|
121
|
+
state.presence = presence ?? {};
|
|
122
|
+
state.chatMessages = chatHistory ?? [];
|
|
123
|
+
state.lastError = null;
|
|
124
|
+
emit();
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case MSG_PRESENCE_UPDATED: {
|
|
128
|
+
const { joined, left, updated } = msg.payload;
|
|
129
|
+
applyPresenceUpdated(joined, left, updated);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case MSG_BROADCAST_EVENT_RELAY:
|
|
133
|
+
break;
|
|
134
|
+
case MSG_CHAT_MESSAGE: {
|
|
135
|
+
const p = msg.payload;
|
|
136
|
+
const stored = {
|
|
137
|
+
id: p.id ?? `${p.connectionId}-${p.createdAt ?? Date.now()}`,
|
|
138
|
+
roomId: p.roomId,
|
|
139
|
+
connectionId: p.connectionId,
|
|
140
|
+
userId: p.userId,
|
|
141
|
+
message: p.message,
|
|
142
|
+
metadata: p.metadata,
|
|
143
|
+
createdAt: p.createdAt ?? Date.now()
|
|
144
|
+
};
|
|
145
|
+
if (state.currentRoomId === p.roomId) {
|
|
146
|
+
state.chatMessages = [...state.chatMessages, stored];
|
|
147
|
+
emit();
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case MSG_ERROR: {
|
|
152
|
+
state.lastError = msg.payload;
|
|
153
|
+
emit();
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function reconnectAndRejoin() {
|
|
159
|
+
const roomId = state.currentRoomId;
|
|
160
|
+
if (!roomId) return;
|
|
161
|
+
const presence = pendingPresence ?? (state.connectionId ? state.presence[state.connectionId]?.presence : void 0);
|
|
162
|
+
send({ type: MSG_JOIN_ROOM, payload: { roomId, presence } });
|
|
163
|
+
}
|
|
164
|
+
function connect() {
|
|
165
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
intentionalClose = false;
|
|
169
|
+
setStatus("connecting");
|
|
170
|
+
(async () => {
|
|
171
|
+
let url = baseUrl;
|
|
172
|
+
if (getAuthToken) {
|
|
173
|
+
try {
|
|
174
|
+
const token = await getAuthToken();
|
|
175
|
+
if (token) {
|
|
176
|
+
const sep = baseUrl.includes("?") ? "&" : "?";
|
|
177
|
+
url = `${baseUrl}${sep}access_token=${encodeURIComponent(token)}`;
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
state.lastError = {
|
|
181
|
+
code: "AUTH_ERROR",
|
|
182
|
+
message: e instanceof Error ? e.message : String(e)
|
|
183
|
+
};
|
|
184
|
+
setStatus("closed");
|
|
185
|
+
emit();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
ws = new WebSocket(url);
|
|
190
|
+
ws.onopen = () => {
|
|
191
|
+
setStatus("open");
|
|
192
|
+
nextReconnectMs = reconnectIntervalMs;
|
|
193
|
+
reconnectAndRejoin();
|
|
194
|
+
};
|
|
195
|
+
ws.onmessage = (event) => {
|
|
196
|
+
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
197
|
+
handleMessage(data);
|
|
198
|
+
};
|
|
199
|
+
ws.onclose = () => {
|
|
200
|
+
ws = null;
|
|
201
|
+
setStatus("closed");
|
|
202
|
+
if (!intentionalClose) {
|
|
203
|
+
scheduleReconnect();
|
|
204
|
+
} else {
|
|
205
|
+
clearReconnect();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
ws.onerror = () => {
|
|
209
|
+
state.lastError = { code: "WEBSOCKET_ERROR", message: "WebSocket error" };
|
|
210
|
+
emit();
|
|
211
|
+
};
|
|
212
|
+
})();
|
|
213
|
+
}
|
|
214
|
+
function disconnect() {
|
|
215
|
+
intentionalClose = true;
|
|
216
|
+
clearReconnect();
|
|
217
|
+
state.currentRoomId = null;
|
|
218
|
+
state.connectionId = null;
|
|
219
|
+
state.presence = {};
|
|
220
|
+
state.chatMessages = [];
|
|
221
|
+
pendingPresence = null;
|
|
222
|
+
if (ws) {
|
|
223
|
+
setStatus("closing");
|
|
224
|
+
ws.close();
|
|
225
|
+
ws = null;
|
|
226
|
+
}
|
|
227
|
+
setStatus("closed");
|
|
228
|
+
}
|
|
229
|
+
function joinRoom(roomId, presence, accessToken) {
|
|
230
|
+
if (state.currentRoomId) {
|
|
231
|
+
send({ type: MSG_LEAVE_ROOM, payload: { roomId: state.currentRoomId } });
|
|
232
|
+
}
|
|
233
|
+
state.currentRoomId = roomId;
|
|
234
|
+
state.presence = {};
|
|
235
|
+
state.chatMessages = [];
|
|
236
|
+
pendingPresence = presence ?? null;
|
|
237
|
+
const payload = { roomId };
|
|
238
|
+
if (presence !== void 0) payload.presence = presence;
|
|
239
|
+
if (accessToken !== void 0) payload.accessToken = accessToken;
|
|
240
|
+
send({ type: MSG_JOIN_ROOM, payload });
|
|
241
|
+
emit();
|
|
242
|
+
}
|
|
243
|
+
function leaveRoom(roomId) {
|
|
244
|
+
const target = roomId ?? state.currentRoomId;
|
|
245
|
+
if (target) {
|
|
246
|
+
send({ type: MSG_LEAVE_ROOM, payload: { roomId: target } });
|
|
247
|
+
if (target === state.currentRoomId) {
|
|
248
|
+
state.currentRoomId = null;
|
|
249
|
+
state.connectionId = null;
|
|
250
|
+
state.presence = {};
|
|
251
|
+
state.chatMessages = [];
|
|
252
|
+
pendingPresence = null;
|
|
253
|
+
}
|
|
254
|
+
emit();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function updatePresence(presence) {
|
|
258
|
+
pendingPresence = presence;
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
if (now - lastPresenceUpdate < presenceThrottleMs) return;
|
|
261
|
+
lastPresenceUpdate = now;
|
|
262
|
+
send({ type: MSG_UPDATE_PRESENCE, payload: { presence } });
|
|
263
|
+
}
|
|
264
|
+
function broadcastEvent(event, payload) {
|
|
265
|
+
send({ type: MSG_BROADCAST_EVENT, payload: { event, payload } });
|
|
266
|
+
}
|
|
267
|
+
function sendChat(message, metadata) {
|
|
268
|
+
send({ type: MSG_SEND_CHAT, payload: { message, metadata } });
|
|
269
|
+
}
|
|
270
|
+
function subscribe(listener) {
|
|
271
|
+
listeners.add(listener);
|
|
272
|
+
return () => listeners.delete(listener);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
connect,
|
|
276
|
+
disconnect,
|
|
277
|
+
joinRoom,
|
|
278
|
+
leaveRoom,
|
|
279
|
+
updatePresence,
|
|
280
|
+
broadcastEvent,
|
|
281
|
+
sendChat,
|
|
282
|
+
getConnectionStatus: () => state.connectionStatus,
|
|
283
|
+
getPresence: () => ({ ...state.presence }),
|
|
284
|
+
getChatMessages: () => [...state.chatMessages],
|
|
285
|
+
getCurrentRoomId: () => state.currentRoomId,
|
|
286
|
+
getState: () => ({
|
|
287
|
+
connectionStatus: state.connectionStatus,
|
|
288
|
+
currentRoomId: state.currentRoomId,
|
|
289
|
+
connectionId: state.connectionId,
|
|
290
|
+
presence: { ...state.presence },
|
|
291
|
+
chatMessages: [...state.chatMessages],
|
|
292
|
+
lastError: state.lastError ? { ...state.lastError } : null
|
|
293
|
+
}),
|
|
294
|
+
subscribe
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export {
|
|
299
|
+
MSG_JOIN_ROOM,
|
|
300
|
+
MSG_LEAVE_ROOM,
|
|
301
|
+
MSG_UPDATE_PRESENCE,
|
|
302
|
+
MSG_BROADCAST_EVENT,
|
|
303
|
+
MSG_SEND_CHAT,
|
|
304
|
+
MSG_ROOM_JOINED,
|
|
305
|
+
MSG_PRESENCE_UPDATED,
|
|
306
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
307
|
+
MSG_CHAT_MESSAGE,
|
|
308
|
+
MSG_ERROR,
|
|
309
|
+
createLiveSyncClient
|
|
310
|
+
};
|
|
311
|
+
//# sourceMappingURL=chunk-MINJGWLX.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}\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":"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","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MSG_BROADCAST_EVENT,
|
|
3
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
4
|
+
MSG_CHAT_MESSAGE,
|
|
5
|
+
MSG_ERROR,
|
|
6
|
+
MSG_JOIN_ROOM,
|
|
7
|
+
MSG_LEAVE_ROOM,
|
|
8
|
+
MSG_PRESENCE_UPDATED,
|
|
9
|
+
MSG_ROOM_JOINED,
|
|
10
|
+
MSG_SEND_CHAT,
|
|
11
|
+
MSG_UPDATE_PRESENCE,
|
|
12
|
+
createLiveSyncClient
|
|
13
|
+
} from "./chunk-MINJGWLX.js";
|
|
14
|
+
export {
|
|
15
|
+
MSG_BROADCAST_EVENT,
|
|
16
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
17
|
+
MSG_CHAT_MESSAGE,
|
|
18
|
+
MSG_ERROR,
|
|
19
|
+
MSG_JOIN_ROOM,
|
|
20
|
+
MSG_LEAVE_ROOM,
|
|
21
|
+
MSG_PRESENCE_UPDATED,
|
|
22
|
+
MSG_ROOM_JOINED,
|
|
23
|
+
MSG_SEND_CHAT,
|
|
24
|
+
MSG_UPDATE_PRESENCE,
|
|
25
|
+
createLiveSyncClient
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|