@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { LiveSyncClient, StoredChatMessage, Presence, PresenceEntry, LiveSyncClientState } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React bindings for @openlivesync/client.
|
|
6
|
+
* Provider + hooks; import from "@openlivesync/client/react".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface LiveSyncProviderProps {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** Pre-created client (call createLiveSyncClient yourself). */
|
|
12
|
+
client?: LiveSyncClient;
|
|
13
|
+
/** Or pass config and the provider will create the client and connect on mount. */
|
|
14
|
+
url?: string;
|
|
15
|
+
reconnect?: boolean;
|
|
16
|
+
reconnectIntervalMs?: number;
|
|
17
|
+
maxReconnectIntervalMs?: number;
|
|
18
|
+
getAuthToken?: () => string | Promise<string>;
|
|
19
|
+
presenceThrottleMs?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Provides the LiveSync client to the tree. Pass either `client` or config (`url`, etc.).
|
|
23
|
+
* If config is passed, the client is created on mount and connect() is called; disconnect on unmount.
|
|
24
|
+
*/
|
|
25
|
+
declare function LiveSyncProvider(props: LiveSyncProviderProps): React.FunctionComponentElement<React.ProviderProps<LiveSyncClient | null>>;
|
|
26
|
+
declare function useLiveSyncClient(): LiveSyncClient;
|
|
27
|
+
/** Returns current connection status and triggers re-renders when it changes. */
|
|
28
|
+
declare function useConnectionStatus(): LiveSyncClientState["connectionStatus"];
|
|
29
|
+
interface UseRoomOptions {
|
|
30
|
+
/** Initial presence when joining (optional). */
|
|
31
|
+
initialPresence?: Presence;
|
|
32
|
+
/** If true, join the room when roomId is set and leave on cleanup or when roomId changes. */
|
|
33
|
+
autoJoin?: boolean;
|
|
34
|
+
/** Optional access token sent with join_room (server can decode for name/email). */
|
|
35
|
+
accessToken?: string;
|
|
36
|
+
/** Optional getter for access token (e.g. refreshed token); used when auto-joining. */
|
|
37
|
+
getAccessToken?: () => string | Promise<string>;
|
|
38
|
+
}
|
|
39
|
+
interface UseRoomReturn {
|
|
40
|
+
join: (roomId: string, presence?: Presence, accessToken?: string) => void;
|
|
41
|
+
leave: (roomId?: string) => void;
|
|
42
|
+
updatePresence: (presence: Presence) => void;
|
|
43
|
+
broadcastEvent: (event: string, payload?: unknown) => void;
|
|
44
|
+
presence: Record<string, PresenceEntry>;
|
|
45
|
+
connectionId: string | null;
|
|
46
|
+
isInRoom: boolean;
|
|
47
|
+
currentRoomId: string | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Subscribe to room state and get methods to join/leave/update presence/broadcast.
|
|
51
|
+
* If autoJoin is true (default), joining happens when roomId is set and leaving on unmount or roomId change.
|
|
52
|
+
*/
|
|
53
|
+
declare function useRoom(roomId: string | null, options?: UseRoomOptions): UseRoomReturn;
|
|
54
|
+
/** Returns presence map for the current room (or empty if not in room). */
|
|
55
|
+
declare function usePresence(roomId: string | null): Record<string, PresenceEntry>;
|
|
56
|
+
interface UseChatReturn {
|
|
57
|
+
messages: StoredChatMessage[];
|
|
58
|
+
sendMessage: (message: string, metadata?: Record<string, unknown>) => void;
|
|
59
|
+
}
|
|
60
|
+
/** Returns chat messages for the given room and sendMessage. Ensure the room is joined (e.g. via useRoom). */
|
|
61
|
+
declare function useChat(roomId: string | null): UseChatReturn;
|
|
62
|
+
|
|
63
|
+
export { LiveSyncProvider, type LiveSyncProviderProps, type UseChatReturn, type UseRoomOptions, type UseRoomReturn, useChat, useConnectionStatus, useLiveSyncClient, usePresence, useRoom };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLiveSyncClient
|
|
3
|
+
} from "./chunk-MINJGWLX.js";
|
|
4
|
+
|
|
5
|
+
// src/react-entry.tsx
|
|
6
|
+
import React, {
|
|
7
|
+
createContext,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState
|
|
14
|
+
} from "react";
|
|
15
|
+
var LiveSyncContext = createContext(null);
|
|
16
|
+
function LiveSyncProvider(props) {
|
|
17
|
+
const { children, client: clientProp } = props;
|
|
18
|
+
const configRef = useRef(null);
|
|
19
|
+
if (!configRef.current && !clientProp && props.url) {
|
|
20
|
+
configRef.current = {
|
|
21
|
+
url: props.url,
|
|
22
|
+
reconnect: props.reconnect,
|
|
23
|
+
reconnectIntervalMs: props.reconnectIntervalMs,
|
|
24
|
+
maxReconnectIntervalMs: props.maxReconnectIntervalMs,
|
|
25
|
+
getAuthToken: props.getAuthToken,
|
|
26
|
+
presenceThrottleMs: props.presenceThrottleMs
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const config = configRef.current;
|
|
30
|
+
const clientFromConfig = useMemo(() => {
|
|
31
|
+
if (clientProp || !config) return null;
|
|
32
|
+
return createLiveSyncClient(config);
|
|
33
|
+
}, [clientProp, config?.url]);
|
|
34
|
+
const client = clientProp ?? clientFromConfig;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!client) return;
|
|
37
|
+
if (!clientProp && config) {
|
|
38
|
+
client.connect();
|
|
39
|
+
return () => client.disconnect();
|
|
40
|
+
}
|
|
41
|
+
}, [client, clientProp, config]);
|
|
42
|
+
if (!client) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"LiveSyncProvider: pass either `client` or config (e.g. `url`) to create the client."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return React.createElement(LiveSyncContext.Provider, { value: client }, children);
|
|
48
|
+
}
|
|
49
|
+
function useLiveSyncClient() {
|
|
50
|
+
const client = useContext(LiveSyncContext);
|
|
51
|
+
if (!client) {
|
|
52
|
+
throw new Error("useLiveSyncClient must be used within LiveSyncProvider");
|
|
53
|
+
}
|
|
54
|
+
return client;
|
|
55
|
+
}
|
|
56
|
+
function useClientState() {
|
|
57
|
+
const client = useLiveSyncClient();
|
|
58
|
+
const [state, setState] = useState(
|
|
59
|
+
() => client.getState()
|
|
60
|
+
);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
return client.subscribe(setState);
|
|
63
|
+
}, [client]);
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
function useConnectionStatus() {
|
|
67
|
+
return useClientState().connectionStatus;
|
|
68
|
+
}
|
|
69
|
+
function useRoom(roomId, options = {}) {
|
|
70
|
+
const { initialPresence, autoJoin = true, accessToken, getAccessToken } = options;
|
|
71
|
+
const client = useLiveSyncClient();
|
|
72
|
+
const state = useClientState();
|
|
73
|
+
const joinedRef = useRef(null);
|
|
74
|
+
const join = useCallback(
|
|
75
|
+
(id, presence, token) => {
|
|
76
|
+
const t = token ?? accessToken;
|
|
77
|
+
client.joinRoom(id, presence ?? initialPresence, t);
|
|
78
|
+
joinedRef.current = id;
|
|
79
|
+
},
|
|
80
|
+
[client, initialPresence, accessToken]
|
|
81
|
+
);
|
|
82
|
+
const leave = useCallback(
|
|
83
|
+
(id) => {
|
|
84
|
+
client.leaveRoom(id);
|
|
85
|
+
if (!id || id === joinedRef.current) joinedRef.current = null;
|
|
86
|
+
},
|
|
87
|
+
[client]
|
|
88
|
+
);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!autoJoin || roomId === null) return;
|
|
91
|
+
let cancelled = false;
|
|
92
|
+
(async () => {
|
|
93
|
+
const token = accessToken ?? (getAccessToken ? await getAccessToken() : void 0);
|
|
94
|
+
if (!cancelled) {
|
|
95
|
+
client.joinRoom(roomId, initialPresence, token);
|
|
96
|
+
joinedRef.current = roomId;
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
return () => {
|
|
100
|
+
cancelled = true;
|
|
101
|
+
leave(roomId);
|
|
102
|
+
};
|
|
103
|
+
}, [autoJoin, roomId, initialPresence, accessToken, getAccessToken, client, leave]);
|
|
104
|
+
const updatePresence = useCallback(
|
|
105
|
+
(presence) => client.updatePresence(presence),
|
|
106
|
+
[client]
|
|
107
|
+
);
|
|
108
|
+
const broadcastEvent = useCallback(
|
|
109
|
+
(event, payload) => client.broadcastEvent(event, payload),
|
|
110
|
+
[client]
|
|
111
|
+
);
|
|
112
|
+
const isInRoom = state.currentRoomId !== null && state.currentRoomId === roomId;
|
|
113
|
+
return {
|
|
114
|
+
join,
|
|
115
|
+
leave,
|
|
116
|
+
updatePresence,
|
|
117
|
+
broadcastEvent,
|
|
118
|
+
presence: roomId && isInRoom ? state.presence : {},
|
|
119
|
+
connectionId: isInRoom ? state.connectionId : null,
|
|
120
|
+
isInRoom,
|
|
121
|
+
currentRoomId: state.currentRoomId
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function usePresence(roomId) {
|
|
125
|
+
useLiveSyncClient();
|
|
126
|
+
const state = useClientState();
|
|
127
|
+
const isInRoom = roomId !== null && state.currentRoomId === roomId;
|
|
128
|
+
return isInRoom ? state.presence : {};
|
|
129
|
+
}
|
|
130
|
+
function useChat(roomId) {
|
|
131
|
+
const client = useLiveSyncClient();
|
|
132
|
+
const state = useClientState();
|
|
133
|
+
const isInRoom = roomId !== null && state.currentRoomId === roomId;
|
|
134
|
+
const messages = isInRoom ? state.chatMessages : [];
|
|
135
|
+
const sendMessage = useCallback(
|
|
136
|
+
(message, metadata) => {
|
|
137
|
+
client.sendChat(message, metadata);
|
|
138
|
+
},
|
|
139
|
+
[client]
|
|
140
|
+
);
|
|
141
|
+
return { messages, sendMessage };
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
LiveSyncProvider,
|
|
145
|
+
useChat,
|
|
146
|
+
useConnectionStatus,
|
|
147
|
+
useLiveSyncClient,
|
|
148
|
+
usePresence,
|
|
149
|
+
useRoom
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=react-entry.js.map
|
|
@@ -0,0 +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":";;;;;AAKA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AASP,IAAM,kBAAkB,cAAqC,IAAI;AAmB1D,SAAS,iBAAiB,OAA8B;AAC7D,QAAM,EAAE,UAAU,QAAQ,WAAW,IAAI;AACzC,QAAM,YAAY,OAAoC,IAAI;AAC1D,MAAI,CAAC,UAAU,WAAW,CAAC,cAAc,MAAM,KAAK;AAClD,cAAU,UAAU;AAAA,MAClB,KAAK,MAAM;AAAA,MACX,WAAW,MAAM;AAAA,MACjB,qBAAqB,MAAM;AAAA,MAC3B,wBAAwB,MAAM;AAAA,MAC9B,cAAc,MAAM;AAAA,MACpB,oBAAoB,MAAM;AAAA,IAC5B;AAAA,EACF;AACA,QAAM,SAAS,UAAU;AAEzB,QAAM,mBAAmB,QAAQ,MAAM;AACrC,QAAI,cAAc,CAAC,OAAQ,QAAO;AAClC,WAAO,qBAAqB,MAAM;AAAA,EACpC,GAAG,CAAC,YAAY,QAAQ,GAAG,CAAC;AAE5B,QAAM,SAAS,cAAc;AAE7B,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,QAAI,CAAC,cAAc,QAAQ;AACzB,aAAO,QAAQ;AACf,aAAO,MAAM,OAAO,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,QAAQ,YAAY,MAAM,CAAC;AAE/B,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,cAAc,gBAAgB,UAAU,EAAE,OAAO,OAAO,GAAG,QAAQ;AAClF;AAEO,SAAS,oBAAoC;AAClD,QAAM,SAAS,WAAW,eAAe;AACzC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,SAAO;AACT;AAEA,SAAS,iBAAsC;AAC7C,QAAM,SAAS,kBAAkB;AACjC,QAAM,CAAC,OAAO,QAAQ,IAAI;AAAA,IAA8B,MACtD,OAAO,SAAS;AAAA,EAClB;AACA,YAAU,MAAM;AACd,WAAO,OAAO,UAAU,QAAQ;AAAA,EAClC,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACT;AAGO,SAAS,sBAA+D;AAC7E,SAAO,eAAe,EAAE;AAC1B;AA4BO,SAAS,QACd,QACA,UAA0B,CAAC,GACZ;AACf,QAAM,EAAE,iBAAiB,WAAW,MAAM,aAAa,eAAe,IAAI;AAC1E,QAAM,SAAS,kBAAkB;AACjC,QAAM,QAAQ,eAAe;AAC7B,QAAM,YAAY,OAAsB,IAAI;AAE5C,QAAM,OAAO;AAAA,IACX,CAAC,IAAY,UAAqB,UAAmB;AACnD,YAAM,IAAI,SAAS;AACnB,aAAO,SAAS,IAAI,YAAY,iBAAiB,CAAC;AAClD,gBAAU,UAAU;AAAA,IACtB;AAAA,IACA,CAAC,QAAQ,iBAAiB,WAAW;AAAA,EACvC;AAEA,QAAM,QAAQ;AAAA,IACZ,CAAC,OAAgB;AACf,aAAO,UAAU,EAAE;AACnB,UAAI,CAAC,MAAM,OAAO,UAAU,QAAS,WAAU,UAAU;AAAA,IAC3D;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,YAAU,MAAM;AACd,QAAI,CAAC,YAAY,WAAW,KAAM;AAClC,QAAI,YAAY;AAChB,KAAC,YAAY;AACX,YAAM,QAAQ,gBAAgB,iBAAiB,MAAM,eAAe,IAAI;AACxE,UAAI,CAAC,WAAW;AACd,eAAO,SAAS,QAAQ,iBAAiB,KAAK;AAC9C,kBAAU,UAAU;AAAA,MACtB;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,kBAAY;AACZ,YAAM,MAAM;AAAA,IACd;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,iBAAiB,aAAa,gBAAgB,QAAQ,KAAK,CAAC;AAElF,QAAM,iBAAiB;AAAA,IACrB,CAAC,aAAuB,OAAO,eAAe,QAAQ;AAAA,IACtD,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,iBAAiB;AAAA,IACrB,CAAC,OAAe,YAAsB,OAAO,eAAe,OAAO,OAAO;AAAA,IAC1E,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,WACJ,MAAM,kBAAkB,QAAQ,MAAM,kBAAkB;AAE1D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,UAAU,WAAW,MAAM,WAAW,CAAC;AAAA,IACjD,cAAc,WAAW,MAAM,eAAe;AAAA,IAC9C;AAAA,IACA,eAAe,MAAM;AAAA,EACvB;AACF;AAGO,SAAS,YAAY,QAAsD;AAChF,oBAAkB;AAClB,QAAM,QAAQ,eAAe;AAC7B,QAAM,WACJ,WAAW,QACX,MAAM,kBAAkB;AAC1B,SAAO,WAAW,MAAM,WAAW,CAAC;AACtC;AAQO,SAAS,QAAQ,QAAsC;AAC5D,QAAM,SAAS,kBAAkB;AACjC,QAAM,QAAQ,eAAe;AAC7B,QAAM,WACJ,WAAW,QAAQ,MAAM,kBAAkB;AAC7C,QAAM,WAAW,WAAW,MAAM,eAAe,CAAC;AAElD,QAAM,cAAc;AAAA,IAClB,CAAC,SAAiB,aAAuC;AACvD,aAAO,SAAS,SAAS,QAAQ;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO,EAAE,UAAU,YAAY;AACjC;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openlivesync/client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./dist/react-entry.d.ts",
|
|
15
|
+
"import": "./dist/react-entry.js",
|
|
16
|
+
"default": "./dist/react-entry.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^18.3.0",
|
|
28
|
+
"react": "^18.3.0",
|
|
29
|
+
"tsup": "^8.3.5",
|
|
30
|
+
"typescript": "^5.6.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core WebSocket client for @openlivesync.
|
|
3
|
+
* Connects to @openlivesync/server, manages room/presence/chat state, and notifies subscribers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ClientMessage,
|
|
8
|
+
ServerMessage,
|
|
9
|
+
Presence,
|
|
10
|
+
PresenceEntry,
|
|
11
|
+
StoredChatMessage,
|
|
12
|
+
} from "./protocol.js";
|
|
13
|
+
import {
|
|
14
|
+
MSG_JOIN_ROOM,
|
|
15
|
+
MSG_LEAVE_ROOM,
|
|
16
|
+
MSG_UPDATE_PRESENCE,
|
|
17
|
+
MSG_BROADCAST_EVENT,
|
|
18
|
+
MSG_SEND_CHAT,
|
|
19
|
+
MSG_ROOM_JOINED,
|
|
20
|
+
MSG_PRESENCE_UPDATED,
|
|
21
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
22
|
+
MSG_CHAT_MESSAGE,
|
|
23
|
+
MSG_ERROR,
|
|
24
|
+
} from "./protocol.js";
|
|
25
|
+
|
|
26
|
+
export type ConnectionStatus = "connecting" | "open" | "closing" | "closed";
|
|
27
|
+
|
|
28
|
+
export interface LiveSyncClientState {
|
|
29
|
+
connectionStatus: ConnectionStatus;
|
|
30
|
+
currentRoomId: string | null;
|
|
31
|
+
connectionId: string | null;
|
|
32
|
+
presence: Record<string, PresenceEntry>;
|
|
33
|
+
chatMessages: StoredChatMessage[];
|
|
34
|
+
lastError: { code: string; message: string } | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LiveSyncClientConfig {
|
|
38
|
+
/** WebSocket URL (e.g. wss://host/live). */
|
|
39
|
+
url: string;
|
|
40
|
+
/** Auto-reconnect on close (default true). */
|
|
41
|
+
reconnect?: boolean;
|
|
42
|
+
/** Initial reconnect delay in ms (default 1000). */
|
|
43
|
+
reconnectIntervalMs?: number;
|
|
44
|
+
/** Max reconnect delay in ms (default 30000). */
|
|
45
|
+
maxReconnectIntervalMs?: number;
|
|
46
|
+
/** Optional: return token for auth; appended as query param (e.g. ?access_token=). */
|
|
47
|
+
getAuthToken?: () => string | Promise<string>;
|
|
48
|
+
/** Throttle presence updates in ms (default 100, match server). */
|
|
49
|
+
presenceThrottleMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_RECONNECT_INTERVAL_MS = 1000;
|
|
53
|
+
const DEFAULT_MAX_RECONNECT_INTERVAL_MS = 30000;
|
|
54
|
+
const DEFAULT_PRESENCE_THROTTLE_MS = 100;
|
|
55
|
+
|
|
56
|
+
function isServerMessage(msg: unknown): msg is ServerMessage {
|
|
57
|
+
if (msg === null || typeof msg !== "object" || !("type" in msg)) return false;
|
|
58
|
+
const t = (msg as { type: string }).type;
|
|
59
|
+
return [
|
|
60
|
+
MSG_ROOM_JOINED,
|
|
61
|
+
MSG_PRESENCE_UPDATED,
|
|
62
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
63
|
+
MSG_CHAT_MESSAGE,
|
|
64
|
+
MSG_ERROR,
|
|
65
|
+
].includes(t);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LiveSyncClient {
|
|
69
|
+
connect(): void;
|
|
70
|
+
disconnect(): void;
|
|
71
|
+
joinRoom(roomId: string, presence?: Presence, accessToken?: string): void;
|
|
72
|
+
leaveRoom(roomId?: string): void;
|
|
73
|
+
updatePresence(presence: Presence): void;
|
|
74
|
+
broadcastEvent(event: string, payload?: unknown): void;
|
|
75
|
+
sendChat(message: string, metadata?: Record<string, unknown>): void;
|
|
76
|
+
getConnectionStatus(): ConnectionStatus;
|
|
77
|
+
getPresence(): Record<string, PresenceEntry>;
|
|
78
|
+
getChatMessages(): StoredChatMessage[];
|
|
79
|
+
getCurrentRoomId(): string | null;
|
|
80
|
+
getState(): LiveSyncClientState;
|
|
81
|
+
subscribe(listener: (state: LiveSyncClientState) => void): () => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createLiveSyncClient(config: LiveSyncClientConfig): LiveSyncClient {
|
|
85
|
+
const {
|
|
86
|
+
url: baseUrl,
|
|
87
|
+
reconnect: reconnectEnabled = true,
|
|
88
|
+
reconnectIntervalMs = DEFAULT_RECONNECT_INTERVAL_MS,
|
|
89
|
+
maxReconnectIntervalMs = DEFAULT_MAX_RECONNECT_INTERVAL_MS,
|
|
90
|
+
getAuthToken,
|
|
91
|
+
presenceThrottleMs = DEFAULT_PRESENCE_THROTTLE_MS,
|
|
92
|
+
} = config;
|
|
93
|
+
|
|
94
|
+
let ws: WebSocket | null = null;
|
|
95
|
+
let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
96
|
+
let nextReconnectMs = reconnectIntervalMs;
|
|
97
|
+
let intentionalClose = false;
|
|
98
|
+
let lastPresenceUpdate = 0;
|
|
99
|
+
let pendingPresence: Presence | null = null;
|
|
100
|
+
|
|
101
|
+
const state: LiveSyncClientState = {
|
|
102
|
+
connectionStatus: "closed",
|
|
103
|
+
currentRoomId: null,
|
|
104
|
+
connectionId: null,
|
|
105
|
+
presence: {},
|
|
106
|
+
chatMessages: [],
|
|
107
|
+
lastError: null,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const listeners = new Set<(s: LiveSyncClientState) => void>();
|
|
111
|
+
|
|
112
|
+
function emit() {
|
|
113
|
+
const snapshot: LiveSyncClientState = {
|
|
114
|
+
connectionStatus: state.connectionStatus,
|
|
115
|
+
currentRoomId: state.currentRoomId,
|
|
116
|
+
connectionId: state.connectionId,
|
|
117
|
+
presence: { ...state.presence },
|
|
118
|
+
chatMessages: [...state.chatMessages],
|
|
119
|
+
lastError: state.lastError ? { ...state.lastError } : null,
|
|
120
|
+
};
|
|
121
|
+
listeners.forEach((cb) => cb(snapshot));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function setStatus(status: ConnectionStatus) {
|
|
125
|
+
state.connectionStatus = status;
|
|
126
|
+
emit();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function send(msg: ClientMessage) {
|
|
130
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
131
|
+
ws.send(JSON.stringify(msg));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function clearReconnect() {
|
|
135
|
+
if (reconnectTimeoutId !== null) {
|
|
136
|
+
clearTimeout(reconnectTimeoutId);
|
|
137
|
+
reconnectTimeoutId = null;
|
|
138
|
+
}
|
|
139
|
+
nextReconnectMs = reconnectIntervalMs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function scheduleReconnect() {
|
|
143
|
+
if (!reconnectEnabled || intentionalClose) return;
|
|
144
|
+
clearReconnect();
|
|
145
|
+
reconnectTimeoutId = setTimeout(() => {
|
|
146
|
+
reconnectTimeoutId = null;
|
|
147
|
+
nextReconnectMs = Math.min(
|
|
148
|
+
nextReconnectMs * 2,
|
|
149
|
+
maxReconnectIntervalMs
|
|
150
|
+
);
|
|
151
|
+
connect();
|
|
152
|
+
}, nextReconnectMs);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applyPresenceUpdated(
|
|
156
|
+
joined?: PresenceEntry[],
|
|
157
|
+
left?: string[],
|
|
158
|
+
updated?: PresenceEntry[]
|
|
159
|
+
) {
|
|
160
|
+
if (joined) {
|
|
161
|
+
for (const e of joined) state.presence[e.connectionId] = e;
|
|
162
|
+
}
|
|
163
|
+
if (left) {
|
|
164
|
+
for (const id of left) delete state.presence[id];
|
|
165
|
+
}
|
|
166
|
+
if (updated) {
|
|
167
|
+
for (const e of updated) state.presence[e.connectionId] = e;
|
|
168
|
+
}
|
|
169
|
+
emit();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handleMessage(data: string) {
|
|
173
|
+
let msg: unknown;
|
|
174
|
+
try {
|
|
175
|
+
msg = JSON.parse(data) as unknown;
|
|
176
|
+
} catch {
|
|
177
|
+
state.lastError = { code: "INVALID_JSON", message: "Invalid JSON from server" };
|
|
178
|
+
emit();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isServerMessage(msg)) {
|
|
182
|
+
state.lastError = { code: "UNKNOWN_MESSAGE", message: "Unknown message type" };
|
|
183
|
+
emit();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
switch (msg.type) {
|
|
187
|
+
case MSG_ROOM_JOINED: {
|
|
188
|
+
const { roomId, connectionId, presence, chatHistory } = msg.payload;
|
|
189
|
+
state.currentRoomId = roomId;
|
|
190
|
+
state.connectionId = connectionId;
|
|
191
|
+
state.presence = presence ?? {};
|
|
192
|
+
state.chatMessages = chatHistory ?? [];
|
|
193
|
+
state.lastError = null;
|
|
194
|
+
emit();
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case MSG_PRESENCE_UPDATED: {
|
|
198
|
+
const { joined, left, updated } = msg.payload;
|
|
199
|
+
applyPresenceUpdated(joined, left, updated);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case MSG_BROADCAST_EVENT_RELAY:
|
|
203
|
+
// Application can subscribe to custom events if we add an event emitter; for now we only update state for presence/chat.
|
|
204
|
+
break;
|
|
205
|
+
case MSG_CHAT_MESSAGE: {
|
|
206
|
+
const p = msg.payload;
|
|
207
|
+
const stored: StoredChatMessage = {
|
|
208
|
+
id: p.id ?? `${p.connectionId}-${p.createdAt ?? Date.now()}`,
|
|
209
|
+
roomId: p.roomId,
|
|
210
|
+
connectionId: p.connectionId,
|
|
211
|
+
userId: p.userId,
|
|
212
|
+
message: p.message,
|
|
213
|
+
metadata: p.metadata,
|
|
214
|
+
createdAt: p.createdAt ?? Date.now(),
|
|
215
|
+
};
|
|
216
|
+
if (state.currentRoomId === p.roomId) {
|
|
217
|
+
state.chatMessages = [...state.chatMessages, stored];
|
|
218
|
+
emit();
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case MSG_ERROR: {
|
|
223
|
+
state.lastError = msg.payload;
|
|
224
|
+
emit();
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function reconnectAndRejoin() {
|
|
231
|
+
const roomId = state.currentRoomId;
|
|
232
|
+
if (!roomId) return;
|
|
233
|
+
const presence = pendingPresence ?? (state.connectionId ? state.presence[state.connectionId]?.presence : undefined);
|
|
234
|
+
send({ type: MSG_JOIN_ROOM, payload: { roomId, presence } });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function connect() {
|
|
238
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
intentionalClose = false;
|
|
242
|
+
setStatus("connecting");
|
|
243
|
+
|
|
244
|
+
(async () => {
|
|
245
|
+
let url = baseUrl;
|
|
246
|
+
if (getAuthToken) {
|
|
247
|
+
try {
|
|
248
|
+
const token = await getAuthToken();
|
|
249
|
+
if (token) {
|
|
250
|
+
const sep = baseUrl.includes("?") ? "&" : "?";
|
|
251
|
+
url = `${baseUrl}${sep}access_token=${encodeURIComponent(token)}`;
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
state.lastError = {
|
|
255
|
+
code: "AUTH_ERROR",
|
|
256
|
+
message: e instanceof Error ? e.message : String(e),
|
|
257
|
+
};
|
|
258
|
+
setStatus("closed");
|
|
259
|
+
emit();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
ws = new WebSocket(url);
|
|
265
|
+
|
|
266
|
+
ws.onopen = () => {
|
|
267
|
+
setStatus("open");
|
|
268
|
+
nextReconnectMs = reconnectIntervalMs;
|
|
269
|
+
reconnectAndRejoin();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
ws.onmessage = (event) => {
|
|
273
|
+
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
274
|
+
handleMessage(data);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
ws.onclose = () => {
|
|
278
|
+
ws = null;
|
|
279
|
+
setStatus("closed");
|
|
280
|
+
if (!intentionalClose) {
|
|
281
|
+
scheduleReconnect();
|
|
282
|
+
} else {
|
|
283
|
+
clearReconnect();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
ws.onerror = () => {
|
|
288
|
+
state.lastError = { code: "WEBSOCKET_ERROR", message: "WebSocket error" };
|
|
289
|
+
emit();
|
|
290
|
+
};
|
|
291
|
+
})();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function disconnect() {
|
|
295
|
+
intentionalClose = true;
|
|
296
|
+
clearReconnect();
|
|
297
|
+
state.currentRoomId = null;
|
|
298
|
+
state.connectionId = null;
|
|
299
|
+
state.presence = {};
|
|
300
|
+
state.chatMessages = [];
|
|
301
|
+
pendingPresence = null;
|
|
302
|
+
if (ws) {
|
|
303
|
+
setStatus("closing");
|
|
304
|
+
ws.close();
|
|
305
|
+
ws = null;
|
|
306
|
+
}
|
|
307
|
+
setStatus("closed");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function joinRoom(roomId: string, presence?: Presence, accessToken?: string) {
|
|
311
|
+
if (state.currentRoomId) {
|
|
312
|
+
send({ type: MSG_LEAVE_ROOM, payload: { roomId: state.currentRoomId } });
|
|
313
|
+
}
|
|
314
|
+
state.currentRoomId = roomId;
|
|
315
|
+
state.presence = {};
|
|
316
|
+
state.chatMessages = [];
|
|
317
|
+
pendingPresence = presence ?? null;
|
|
318
|
+
const payload: { roomId: string; presence?: Presence; accessToken?: string } = { roomId };
|
|
319
|
+
if (presence !== undefined) payload.presence = presence;
|
|
320
|
+
if (accessToken !== undefined) payload.accessToken = accessToken;
|
|
321
|
+
send({ type: MSG_JOIN_ROOM, payload });
|
|
322
|
+
emit();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function leaveRoom(roomId?: string) {
|
|
326
|
+
const target = roomId ?? state.currentRoomId;
|
|
327
|
+
if (target) {
|
|
328
|
+
send({ type: MSG_LEAVE_ROOM, payload: { roomId: target } });
|
|
329
|
+
if (target === state.currentRoomId) {
|
|
330
|
+
state.currentRoomId = null;
|
|
331
|
+
state.connectionId = null;
|
|
332
|
+
state.presence = {};
|
|
333
|
+
state.chatMessages = [];
|
|
334
|
+
pendingPresence = null;
|
|
335
|
+
}
|
|
336
|
+
emit();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function updatePresence(presence: Presence) {
|
|
341
|
+
pendingPresence = presence;
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
if (now - lastPresenceUpdate < presenceThrottleMs) return;
|
|
344
|
+
lastPresenceUpdate = now;
|
|
345
|
+
send({ type: MSG_UPDATE_PRESENCE, payload: { presence } });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function broadcastEvent(event: string, payload?: unknown) {
|
|
349
|
+
send({ type: MSG_BROADCAST_EVENT, payload: { event, payload } });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function sendChat(message: string, metadata?: Record<string, unknown>) {
|
|
353
|
+
send({ type: MSG_SEND_CHAT, payload: { message, metadata } });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function subscribe(listener: (state: LiveSyncClientState) => void): () => void {
|
|
357
|
+
listeners.add(listener);
|
|
358
|
+
return () => listeners.delete(listener);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
connect,
|
|
363
|
+
disconnect,
|
|
364
|
+
joinRoom,
|
|
365
|
+
leaveRoom,
|
|
366
|
+
updatePresence,
|
|
367
|
+
broadcastEvent,
|
|
368
|
+
sendChat,
|
|
369
|
+
getConnectionStatus: () => state.connectionStatus,
|
|
370
|
+
getPresence: () => ({ ...state.presence }),
|
|
371
|
+
getChatMessages: () => [...state.chatMessages],
|
|
372
|
+
getCurrentRoomId: () => state.currentRoomId,
|
|
373
|
+
getState: () => ({
|
|
374
|
+
connectionStatus: state.connectionStatus,
|
|
375
|
+
currentRoomId: state.currentRoomId,
|
|
376
|
+
connectionId: state.connectionId,
|
|
377
|
+
presence: { ...state.presence },
|
|
378
|
+
chatMessages: [...state.chatMessages],
|
|
379
|
+
lastError: state.lastError ? { ...state.lastError } : null,
|
|
380
|
+
}),
|
|
381
|
+
subscribe,
|
|
382
|
+
};
|
|
383
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @openlivesync/client - Browser package for collaboration and presence.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createLiveSyncClient } from "./client.js";
|
|
6
|
+
export type {
|
|
7
|
+
LiveSyncClient,
|
|
8
|
+
LiveSyncClientConfig,
|
|
9
|
+
LiveSyncClientState,
|
|
10
|
+
ConnectionStatus,
|
|
11
|
+
} from "./client.js";
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
Presence,
|
|
15
|
+
UserInfo,
|
|
16
|
+
ClientMessage,
|
|
17
|
+
ServerMessage,
|
|
18
|
+
JoinRoomPayload,
|
|
19
|
+
LeaveRoomPayload,
|
|
20
|
+
UpdatePresencePayload,
|
|
21
|
+
BroadcastEventPayload,
|
|
22
|
+
SendChatPayload,
|
|
23
|
+
PresenceEntry,
|
|
24
|
+
RoomJoinedPayload,
|
|
25
|
+
PresenceUpdatedPayload,
|
|
26
|
+
BroadcastEventRelayPayload,
|
|
27
|
+
ChatMessagePayload,
|
|
28
|
+
StoredChatMessage,
|
|
29
|
+
ErrorPayload,
|
|
30
|
+
ChatMessageInput,
|
|
31
|
+
} from "./protocol.js";
|
|
32
|
+
export {
|
|
33
|
+
MSG_JOIN_ROOM,
|
|
34
|
+
MSG_LEAVE_ROOM,
|
|
35
|
+
MSG_UPDATE_PRESENCE,
|
|
36
|
+
MSG_BROADCAST_EVENT,
|
|
37
|
+
MSG_SEND_CHAT,
|
|
38
|
+
MSG_ROOM_JOINED,
|
|
39
|
+
MSG_PRESENCE_UPDATED,
|
|
40
|
+
MSG_BROADCAST_EVENT_RELAY,
|
|
41
|
+
MSG_CHAT_MESSAGE,
|
|
42
|
+
MSG_ERROR,
|
|
43
|
+
} from "./protocol.js";
|