cynx-lib 1.3.48 → 1.3.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.48",
3
+ "version": "1.3.50",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -120,11 +120,11 @@ export function ActionsMenu({ items, align = 'right' }) {
120
120
  display: 'flex', alignItems: 'center', gap: 8,
121
121
  padding: '6px 10px', borderRadius: 'var(--rads,8px)',
122
122
  fontSize: 12, cursor: 'pointer', transition: 'all .14s',
123
- color: item.variant === 'danger' ? 'var(--danger, #f64747)' : item.variant === 'success' ? 'var(--success, #27ae60)' : item.variant === 'warning' ? 'var(--accent, #f99e2c)' : 'var(--text,#1a1a2e)',
123
+ color: (item.variant === 'danger' || item.danger) ? 'var(--danger, #ef4444)' : item.variant === 'success' ? 'var(--success, #16a34a)' : item.variant === 'warning' ? 'var(--warning, #f59e0b)' : 'var(--primary-text, #18181b)',
124
124
  fontWeight: 500,
125
125
  }}
126
126
  onMouseEnter={e => {
127
- e.currentTarget.style.background = item.variant === 'danger' ? 'var(--dd, rgba(246,71,71,.08))' : item.variant === 'warning' ? 'var(--pd, rgba(249,158,44,.08))' : 'var(--grey-1,#f5f5f8)';
127
+ e.currentTarget.style.background = (item.variant === 'danger' || item.danger) ? 'var(--danger-bg, rgba(239,68,68,.10))' : item.variant === 'success' ? 'var(--success-bg, rgba(22,163,74,.10))' : item.variant === 'warning' ? 'var(--warning-bg, rgba(245,158,11,.12))' : 'var(--grey-1, #f4f4f5)';
128
128
  }}
129
129
  onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
130
130
  >
@@ -19,6 +19,9 @@ var realtime_exports = {};
19
19
  __export(realtime_exports, {
20
20
  ConnectedClient: () => ConnectedClient,
21
21
  EventBus: () => EventBus,
22
+ RealtimeClient: () => RealtimeClient,
23
+ RealtimeDispatcher: () => RealtimeDispatcher,
24
+ RealtimeProxy: () => RealtimeProxy,
22
25
  RealtimeServer: () => RealtimeServer,
23
26
  RealtimeSocket: () => RealtimeSocket,
24
27
  SSEStream: () => SSEStream,
@@ -26,11 +29,15 @@ __export(realtime_exports, {
26
29
  TabChannel: () => TabChannel,
27
30
  createEventBus: () => createEventBus,
28
31
  createPoller: () => createPoller,
32
+ createRealtimeClient: () => createRealtimeClient,
33
+ createRealtimeDispatcher: () => createRealtimeDispatcher,
34
+ createRealtimeProxy: () => createRealtimeProxy,
29
35
  createRealtimeServer: () => createRealtimeServer,
30
36
  createRealtimeSocket: () => createRealtimeSocket,
31
37
  createSSE: () => createSSE,
32
38
  createTabChannel: () => createTabChannel,
33
39
  default: () => realtime_default,
40
+ normalizeWsUrl: () => normalizeWsUrl,
34
41
  parseAIStream: () => parseAIStream
35
42
  });
36
43
  module.exports = __toCommonJS(realtime_exports);
@@ -844,10 +851,494 @@ class RealtimeServer {
844
851
  function createRealtimeServer(options) {
845
852
  return new RealtimeServer(options);
846
853
  }
854
+ function normalizeWsUrl(rawUrl, defaultPath = "/ws") {
855
+ if (!rawUrl || typeof rawUrl !== "string") {
856
+ if (isBrowser) {
857
+ const isHttps2 = window.location.protocol === "https:";
858
+ const defaultScheme2 = isHttps2 ? "wss:" : "ws:";
859
+ return `${defaultScheme2}//${window.location.host}${defaultPath}`;
860
+ }
861
+ return "";
862
+ }
863
+ let url = rawUrl.trim();
864
+ const isHttps = isBrowser ? window.location.protocol === "https:" : false;
865
+ const defaultScheme = isHttps ? "wss:" : "ws:";
866
+ if (url.startsWith("//")) {
867
+ url = `${defaultScheme}${url}`;
868
+ } else if (url.startsWith("/")) {
869
+ if (isBrowser) {
870
+ url = `${defaultScheme}//${window.location.host}${url}`;
871
+ }
872
+ } else if (/^http:\/\//i.test(url)) {
873
+ url = url.replace(/^http:\/\//i, "ws://");
874
+ } else if (/^https:\/\//i.test(url)) {
875
+ url = url.replace(/^https:\/\//i, "wss://");
876
+ } else if (!/^wss?:\/\//i.test(url)) {
877
+ const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
878
+ const scheme = isLocal && !isHttps ? "ws://" : "wss://";
879
+ url = `${scheme}${url}`;
880
+ }
881
+ try {
882
+ const httpEquivalent = url.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:");
883
+ const parsed = new URL(httpEquivalent);
884
+ let pathname = parsed.pathname || defaultPath;
885
+ if (!pathname || pathname === "/") {
886
+ pathname = defaultPath;
887
+ }
888
+ pathname = pathname.replace(/\/+/g, "/").replace(/\/ws\/ws(\/|$)/g, "/ws$1");
889
+ const scheme = url.toLowerCase().startsWith("wss:") ? "wss:" : "ws:";
890
+ return `${scheme}//${parsed.host}${pathname}`;
891
+ } catch {
892
+ return url;
893
+ }
894
+ }
895
+ class RealtimeClient {
896
+ constructor(options = {}) {
897
+ const defaultUrl = isBrowser ? (window.location.protocol === "https:" ? "wss:" : "ws:") + "//" + window.location.host + "/ws" : "/ws";
898
+ this.options = {
899
+ url: defaultUrl,
900
+ token: void 0,
901
+ // string or function: () => token
902
+ room: void 0,
903
+ // optional room name or function: () => room
904
+ autoConnect: true,
905
+ reconnect: true,
906
+ maxRetries: Infinity,
907
+ initialRetryDelay: 1e3,
908
+ maxRetryDelay: 3e4,
909
+ backoffFactor: 1.5,
910
+ jitter: true,
911
+ heartbeatInterval: 2e4,
912
+ pingPayload: { type: "PING" },
913
+ ...options
914
+ };
915
+ this.events = new EventBus();
916
+ this.ws = null;
917
+ this.status = "DISCONNECTED";
918
+ this.retries = 0;
919
+ this._pingTimer = null;
920
+ this._reconnectTimer = null;
921
+ this._isExplicitClose = false;
922
+ this._connecting = false;
923
+ if (this.options.autoConnect) {
924
+ this.init();
925
+ }
926
+ }
927
+ getToken() {
928
+ if (typeof this.options.token === "function") {
929
+ return this.options.token();
930
+ }
931
+ if (typeof this.options.token === "string") {
932
+ return this.options.token;
933
+ }
934
+ if (isBrowser) {
935
+ return localStorage.getItem("token") || sessionStorage.getItem("token") || "";
936
+ }
937
+ return "";
938
+ }
939
+ getRoom() {
940
+ if (typeof this.options.room === "function") {
941
+ return this.options.room();
942
+ }
943
+ if (typeof this.options.room === "string") {
944
+ return this.options.room;
945
+ }
946
+ return "";
947
+ }
948
+ init() {
949
+ if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) {
950
+ return;
951
+ }
952
+ const token = this.getToken();
953
+ if (!token && !this.options.anonymous) {
954
+ return;
955
+ }
956
+ this.connect();
957
+ }
958
+ connect() {
959
+ if (this._connecting) return;
960
+ if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) return;
961
+ this._connecting = true;
962
+ this._isExplicitClose = false;
963
+ this.cleanup();
964
+ const WS = getWebSocketConstructor(this.options.WebSocket);
965
+ if (!WS) {
966
+ this._connecting = false;
967
+ console.warn("[cynx/realtime] WebSocket implementation not found in this environment.");
968
+ return;
969
+ }
970
+ try {
971
+ const rawUrl = typeof this.options.url === "function" ? this.options.url() : this.options.url;
972
+ const normalized = normalizeWsUrl(rawUrl);
973
+ const token = this.getToken();
974
+ const room = this.getRoom();
975
+ const urlObj = new URL(normalized.replace(/^ws:/i, "http:").replace(/^wss:/i, "https:"));
976
+ if (token && !urlObj.searchParams.has("token")) {
977
+ urlObj.searchParams.set("token", token);
978
+ }
979
+ if (room && !urlObj.searchParams.has("room")) {
980
+ urlObj.searchParams.set("room", room);
981
+ }
982
+ const finalScheme = normalized.toLowerCase().startsWith("wss:") ? "wss:" : "ws:";
983
+ const finalUrl = `${finalScheme}//${urlObj.host}${urlObj.pathname}${urlObj.search}`;
984
+ this.ws = new WS(finalUrl);
985
+ this.status = this.retries > 0 ? "RECONNECTING" : "CONNECTING";
986
+ this.ws.onopen = (e) => {
987
+ this._connecting = false;
988
+ this.retries = 0;
989
+ this.status = "CONNECTED";
990
+ this.events.emit("open", e);
991
+ this.events.emit("status", this.status);
992
+ this._startHeartbeat();
993
+ };
994
+ this.ws.onmessage = (event) => {
995
+ try {
996
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
997
+ const parsed = safeJSONParse(raw, raw);
998
+ const eventType = parsed && typeof parsed === "object" ? parsed.type || parsed.event || "message" : "message";
999
+ if (parsed && parsed.type === "PONG") {
1000
+ return;
1001
+ }
1002
+ this.events.emit(eventType, parsed);
1003
+ this.events.emit("message", parsed);
1004
+ this.events.emit("*", parsed);
1005
+ } catch (err) {
1006
+ console.warn("[cynx/realtime] Message parse warning:", err);
1007
+ }
1008
+ };
1009
+ this.ws.onerror = (err) => {
1010
+ this.events.emit("error", err);
1011
+ };
1012
+ this.ws.onclose = (e) => {
1013
+ this._connecting = false;
1014
+ this.status = "DISCONNECTED";
1015
+ this._stopHeartbeat();
1016
+ this.events.emit("close", e);
1017
+ this.events.emit("status", this.status);
1018
+ if (!this._isExplicitClose && this.options.reconnect && this.retries < this.options.maxRetries) {
1019
+ this._handleReconnect();
1020
+ }
1021
+ };
1022
+ } catch (err) {
1023
+ this._connecting = false;
1024
+ this._handleReconnect();
1025
+ }
1026
+ }
1027
+ send(eventOrData, payload = void 0) {
1028
+ if (!this.ws || this.ws.readyState !== 1) return false;
1029
+ let message;
1030
+ if (payload !== void 0) {
1031
+ message = JSON.stringify({ type: eventOrData, payload });
1032
+ } else if (typeof eventOrData === "object") {
1033
+ message = JSON.stringify(eventOrData);
1034
+ } else {
1035
+ message = String(eventOrData);
1036
+ }
1037
+ this.ws.send(message);
1038
+ return true;
1039
+ }
1040
+ emit(event, payload = void 0) {
1041
+ return this.send(event, payload);
1042
+ }
1043
+ subscribe(callback) {
1044
+ const unsub = this.events.on("*", callback);
1045
+ this.init();
1046
+ return unsub;
1047
+ }
1048
+ on(event, callback) {
1049
+ const unsub = this.events.on(event, callback);
1050
+ this.init();
1051
+ return unsub;
1052
+ }
1053
+ once(event, callback) {
1054
+ const unsub = this.events.once(event, callback);
1055
+ this.init();
1056
+ return unsub;
1057
+ }
1058
+ off(event, callback) {
1059
+ this.events.off(event, callback);
1060
+ }
1061
+ disconnect(code = 1e3, reason = "Client disconnected") {
1062
+ this._isExplicitClose = true;
1063
+ this.cleanup();
1064
+ if (this.ws) {
1065
+ try {
1066
+ this.ws.close(code, reason);
1067
+ } catch {
1068
+ }
1069
+ this.ws = null;
1070
+ }
1071
+ this.status = "DISCONNECTED";
1072
+ this.events.emit("status", this.status);
1073
+ }
1074
+ cleanup() {
1075
+ this._stopHeartbeat();
1076
+ if (this._reconnectTimer) {
1077
+ clearTimeout(this._reconnectTimer);
1078
+ this._reconnectTimer = null;
1079
+ }
1080
+ }
1081
+ _startHeartbeat() {
1082
+ this._stopHeartbeat();
1083
+ if (!this.options.heartbeatInterval) return;
1084
+ this._pingTimer = setInterval(() => {
1085
+ if (this.ws && this.ws.readyState === 1) {
1086
+ try {
1087
+ this.ws.send(JSON.stringify(this.options.pingPayload));
1088
+ } catch {
1089
+ }
1090
+ }
1091
+ }, this.options.heartbeatInterval);
1092
+ }
1093
+ _stopHeartbeat() {
1094
+ if (this._pingTimer) {
1095
+ clearInterval(this._pingTimer);
1096
+ this._pingTimer = null;
1097
+ }
1098
+ }
1099
+ _handleReconnect() {
1100
+ if (this._reconnectTimer) return;
1101
+ this.retries++;
1102
+ const delay = Math.min(
1103
+ this.options.initialRetryDelay * Math.pow(this.options.backoffFactor, this.retries - 1),
1104
+ this.options.maxRetryDelay
1105
+ );
1106
+ const finalDelay = this.options.jitter ? delay * (0.8 + Math.random() * 0.4) : delay;
1107
+ this._reconnectTimer = setTimeout(() => {
1108
+ this._reconnectTimer = null;
1109
+ this.connect();
1110
+ }, finalDelay);
1111
+ }
1112
+ }
1113
+ function createRealtimeClient(options) {
1114
+ return new RealtimeClient(options);
1115
+ }
1116
+ class RealtimeProxy {
1117
+ constructor(server, options = {}) {
1118
+ this.server = server;
1119
+ this.options = {
1120
+ path: "/ws",
1121
+ upstreamUrl: void 0,
1122
+ // process.env.WS_URL
1123
+ upstreamKey: void 0,
1124
+ // process.env.WS_KEY
1125
+ room: "default",
1126
+ // process.env.WS_ROOM
1127
+ authenticate: void 0,
1128
+ // async (req, token) => ({ userId })
1129
+ WebSocket: void 0,
1130
+ WebSocketServer: void 0,
1131
+ ...options
1132
+ };
1133
+ this.wss = null;
1134
+ this.events = new EventBus();
1135
+ if (this.server) {
1136
+ this.attach(this.server);
1137
+ }
1138
+ }
1139
+ attach(server) {
1140
+ this.server = server;
1141
+ let WSS = this.options.WebSocketServer;
1142
+ if (!WSS && typeof globalThis !== "undefined" && globalThis.WebSocketServer) {
1143
+ WSS = globalThis.WebSocketServer;
1144
+ }
1145
+ if (!WSS) {
1146
+ try {
1147
+ const wsModule = typeof require === "function" ? require("ws") : null;
1148
+ if (wsModule && wsModule.WebSocketServer) {
1149
+ WSS = wsModule.WebSocketServer;
1150
+ } else if (wsModule && wsModule.Server) {
1151
+ WSS = wsModule.Server;
1152
+ }
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ if (!WSS) {
1157
+ console.warn("[cynx/realtime] WebSocketServer not available for RealtimeProxy.");
1158
+ return;
1159
+ }
1160
+ this.wss = new WSS({ noServer: true });
1161
+ server.on("upgrade", async (request, socket, head) => {
1162
+ try {
1163
+ const url = new URL(request.url, `http://${request.headers.host || "localhost"}`);
1164
+ if (url.pathname !== this.options.path) {
1165
+ return;
1166
+ }
1167
+ const token = (url.searchParams.get("token") || request.headers["authorization"]?.replace(/^Bearer\s+/i, "") || request.headers["x-auth-token"] || "").trim();
1168
+ let authData = null;
1169
+ if (typeof this.options.authenticate === "function") {
1170
+ authData = await this.options.authenticate(request, token);
1171
+ if (!authData) {
1172
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
1173
+ socket.destroy();
1174
+ return;
1175
+ }
1176
+ }
1177
+ const room = (url.searchParams.get("room") || this.options.room || "default").trim().toLowerCase();
1178
+ this.wss.handleUpgrade(request, socket, head, (clientWs) => {
1179
+ this.wss.emit("connection", clientWs, request, { authData, room });
1180
+ });
1181
+ } catch (err) {
1182
+ console.warn("[RealtimeProxy Upgrade Error]:", err.message);
1183
+ try {
1184
+ socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
1185
+ socket.destroy();
1186
+ } catch {
1187
+ }
1188
+ }
1189
+ });
1190
+ this.wss.on("connection", (clientWs, request, { authData, room }) => {
1191
+ this._handleConnection(clientWs, { authData, room });
1192
+ });
1193
+ }
1194
+ _handleConnection(clientWs, { authData, room }) {
1195
+ const WS_URL = this.options.upstreamUrl || (typeof process !== "undefined" ? process.env.WS_URL : "");
1196
+ const WS_KEY = this.options.upstreamKey || (typeof process !== "undefined" ? process.env.WS_KEY : "");
1197
+ const targetRoom = room || this.options.room || "default";
1198
+ if (!WS_URL || !WS_KEY) {
1199
+ clientWs.close(1011, "Real-Time upstream not configured");
1200
+ return;
1201
+ }
1202
+ const upstreamBase = WS_URL.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://").replace(/\/ws\/?$/i, "").replace(/\/$/, "");
1203
+ const upstreamUrl = `${upstreamBase}/ws?room=${encodeURIComponent(targetRoom)}&token=${encodeURIComponent(WS_KEY)}`;
1204
+ let WS = this.options.WebSocket;
1205
+ if (!WS && typeof globalThis !== "undefined" && globalThis.WebSocket) {
1206
+ WS = globalThis.WebSocket;
1207
+ }
1208
+ if (!WS) {
1209
+ try {
1210
+ const wsModule = typeof require === "function" ? require("ws") : null;
1211
+ if (wsModule) WS = wsModule;
1212
+ } catch {
1213
+ }
1214
+ }
1215
+ if (!WS) {
1216
+ clientWs.close(1011, "WebSocket client constructor not available");
1217
+ return;
1218
+ }
1219
+ let upstreamWs = null;
1220
+ try {
1221
+ upstreamWs = new WS(upstreamUrl);
1222
+ } catch (err) {
1223
+ clientWs.close(1011, "Failed to connect upstream");
1224
+ return;
1225
+ }
1226
+ upstreamWs.onmessage = (event) => {
1227
+ if (clientWs.readyState === 1) {
1228
+ clientWs.send(event.data);
1229
+ }
1230
+ };
1231
+ if (typeof upstreamWs.on === "function") {
1232
+ upstreamWs.on("message", (data, isBinary) => {
1233
+ if (clientWs.readyState === 1) {
1234
+ clientWs.send(data, { binary: isBinary });
1235
+ }
1236
+ });
1237
+ upstreamWs.on("error", (err) => {
1238
+ console.warn("[RealtimeProxy Upstream Error]:", err.message);
1239
+ if (clientWs.readyState === 1) clientWs.close(1011, "Upstream error");
1240
+ });
1241
+ upstreamWs.on("close", (code, reason) => {
1242
+ if (clientWs.readyState === 1) clientWs.close(code, reason);
1243
+ });
1244
+ }
1245
+ if (typeof clientWs.on === "function") {
1246
+ clientWs.on("message", (data, isBinary) => {
1247
+ if (upstreamWs && upstreamWs.readyState === 1) {
1248
+ upstreamWs.send(data, { binary: isBinary });
1249
+ }
1250
+ });
1251
+ clientWs.on("error", () => {
1252
+ if (upstreamWs && upstreamWs.readyState === 1) upstreamWs.close();
1253
+ });
1254
+ clientWs.on("close", () => {
1255
+ if (upstreamWs && upstreamWs.readyState === 1) upstreamWs.close();
1256
+ });
1257
+ }
1258
+ }
1259
+ }
1260
+ function createRealtimeProxy(server, options) {
1261
+ return new RealtimeProxy(server, options);
1262
+ }
1263
+ class RealtimeDispatcher {
1264
+ constructor(options = {}) {
1265
+ this.options = {
1266
+ url: void 0,
1267
+ // process.env.WS_URL
1268
+ key: void 0,
1269
+ // process.env.WS_KEY
1270
+ room: void 0,
1271
+ // process.env.WS_ROOM
1272
+ fetch: typeof globalThis !== "undefined" ? globalThis.fetch : void 0,
1273
+ ...options
1274
+ };
1275
+ }
1276
+ async broadcast(eventType, payload = {}, options = {}) {
1277
+ const rawUrl = options.url || this.options.url || (typeof process !== "undefined" ? process.env.WS_URL : "");
1278
+ const key = options.key || this.options.key || (typeof process !== "undefined" ? process.env.WS_KEY : "");
1279
+ const room = (options.room || this.options.room || (typeof process !== "undefined" ? process.env.WS_ROOM : "") || "").trim().toLowerCase();
1280
+ if (!rawUrl || !key || !room) {
1281
+ return { ok: false, error: "Missing WS_URL, WS_KEY, or WS_ROOM configuration" };
1282
+ }
1283
+ const fetchFn = this.options.fetch || globalThis.fetch;
1284
+ if (!fetchFn) {
1285
+ return { ok: false, error: "fetch function not found in environment" };
1286
+ }
1287
+ const baseUrl = rawUrl.replace(/^ws:\/\//i, "http://").replace(/^wss:\/\//i, "https://").replace(/\/ws\/?$/i, "").replace(/\/$/, "");
1288
+ const endpoint = `${baseUrl}/api/broadcast?room=${encodeURIComponent(room)}`;
1289
+ const body = JSON.stringify({
1290
+ type: eventType,
1291
+ room,
1292
+ payload,
1293
+ timestamp: Date.now(),
1294
+ ...options.extra
1295
+ });
1296
+ const headers = {
1297
+ "Content-Type": "application/json",
1298
+ "X-Secret-Key": key,
1299
+ "x-ws-token": key,
1300
+ "x-room": room,
1301
+ "Authorization": `Bearer ${key}`
1302
+ };
1303
+ try {
1304
+ let res = await fetchFn(endpoint, {
1305
+ method: "POST",
1306
+ headers,
1307
+ body
1308
+ });
1309
+ if (res.status === 404) {
1310
+ const legacyEndpoint = `${baseUrl}/broadcast?room=${encodeURIComponent(room)}`;
1311
+ res = await fetchFn(legacyEndpoint, { method: "POST", headers, body });
1312
+ }
1313
+ if (!res.ok) {
1314
+ const errText = await res.text().catch(() => "");
1315
+ console.warn(`[cynx/realtime] Broadcast warning (${res.status}):`, errText);
1316
+ return { ok: false, status: res.status, error: errText };
1317
+ }
1318
+ const data = await res.json().catch(() => ({ status: "ok" }));
1319
+ return { ok: true, data };
1320
+ } catch (err) {
1321
+ console.warn("[cynx/realtime] Broadcast network error:", err.message);
1322
+ return { ok: false, error: err.message };
1323
+ }
1324
+ }
1325
+ async broadcastSession(token, eventType, payload = {}, options = {}) {
1326
+ return this.broadcast("session_event", payload, {
1327
+ ...options,
1328
+ extra: { token, eventType, sessionToken: token }
1329
+ });
1330
+ }
1331
+ }
1332
+ function createRealtimeDispatcher(options) {
1333
+ return new RealtimeDispatcher(options);
1334
+ }
847
1335
  var realtime_default = {
848
1336
  // Client
849
1337
  RealtimeSocket,
850
1338
  createRealtimeSocket,
1339
+ RealtimeClient,
1340
+ createRealtimeClient,
1341
+ normalizeWsUrl,
851
1342
  SSEStream,
852
1343
  createSSE,
853
1344
  parseAIStream,
@@ -860,12 +1351,19 @@ var realtime_default = {
860
1351
  // Server
861
1352
  ConnectedClient,
862
1353
  RealtimeServer,
863
- createRealtimeServer
1354
+ createRealtimeServer,
1355
+ RealtimeProxy,
1356
+ createRealtimeProxy,
1357
+ RealtimeDispatcher,
1358
+ createRealtimeDispatcher
864
1359
  };
865
1360
  // Annotate the CommonJS export names for ESM import in node:
866
1361
  0 && (module.exports = {
867
1362
  ConnectedClient,
868
1363
  EventBus,
1364
+ RealtimeClient,
1365
+ RealtimeDispatcher,
1366
+ RealtimeProxy,
869
1367
  RealtimeServer,
870
1368
  RealtimeSocket,
871
1369
  SSEStream,
@@ -873,9 +1371,13 @@ var realtime_default = {
873
1371
  TabChannel,
874
1372
  createEventBus,
875
1373
  createPoller,
1374
+ createRealtimeClient,
1375
+ createRealtimeDispatcher,
1376
+ createRealtimeProxy,
876
1377
  createRealtimeServer,
877
1378
  createRealtimeSocket,
878
1379
  createSSE,
879
1380
  createTabChannel,
1381
+ normalizeWsUrl,
880
1382
  parseAIStream
881
1383
  });
@@ -1018,12 +1018,602 @@ export function createRealtimeServer(options) {
1018
1018
  return new RealtimeServer(options);
1019
1019
  }
1020
1020
 
1021
+ // ─── 7. UNIVERSAL URL NORMALIZER ────────────────────────────────────────────────
1022
+ /**
1023
+ * Normalizes HTTP/HTTPS/WS/WSS and relative URLs into valid WebSocket endpoints.
1024
+ * Automatically removes accidental duplicate /ws paths or trailing slashes.
1025
+ */
1026
+ export function normalizeWsUrl(rawUrl, defaultPath = '/ws') {
1027
+ if (!rawUrl || typeof rawUrl !== 'string') {
1028
+ if (isBrowser) {
1029
+ const isHttps = window.location.protocol === 'https:';
1030
+ const defaultScheme = isHttps ? 'wss:' : 'ws:';
1031
+ return `${defaultScheme}//${window.location.host}${defaultPath}`;
1032
+ }
1033
+ return '';
1034
+ }
1035
+
1036
+ let url = rawUrl.trim();
1037
+ const isHttps = isBrowser ? window.location.protocol === 'https:' : false;
1038
+ const defaultScheme = isHttps ? 'wss:' : 'ws:';
1039
+
1040
+ // 1. Protocol-relative URL: //example.com/ws
1041
+ if (url.startsWith('//')) {
1042
+ url = `${defaultScheme}${url}`;
1043
+ }
1044
+ // 2. Relative path: /ws
1045
+ else if (url.startsWith('/')) {
1046
+ if (isBrowser) {
1047
+ url = `${defaultScheme}//${window.location.host}${url}`;
1048
+ }
1049
+ }
1050
+ // 3. HTTP -> WS
1051
+ else if (/^http:\/\//i.test(url)) {
1052
+ url = url.replace(/^http:\/\//i, 'ws://');
1053
+ }
1054
+ // 4. HTTPS -> WSS
1055
+ else if (/^https:\/\//i.test(url)) {
1056
+ url = url.replace(/^https:\/\//i, 'wss://');
1057
+ }
1058
+ // 5. Raw host without scheme
1059
+ else if (!/^wss?:\/\//i.test(url)) {
1060
+ const isLocal = url.includes('localhost') || url.includes('127.0.0.1');
1061
+ const scheme = isLocal && !isHttps ? 'ws://' : 'wss://';
1062
+ url = `${scheme}${url}`;
1063
+ }
1064
+
1065
+ // Ensure path is normalized without duplicate /ws/ws
1066
+ try {
1067
+ const httpEquivalent = url.replace(/^wss:/i, 'https:').replace(/^ws:/i, 'http:');
1068
+ const parsed = new URL(httpEquivalent);
1069
+ let pathname = parsed.pathname || defaultPath;
1070
+ if (!pathname || pathname === '/') {
1071
+ pathname = defaultPath;
1072
+ }
1073
+ pathname = pathname.replace(/\/+/g, '/').replace(/\/ws\/ws(\/|$)/g, '/ws$1');
1074
+ const scheme = url.toLowerCase().startsWith('wss:') ? 'wss:' : 'ws:';
1075
+ return `${scheme}//${parsed.host}${pathname}`;
1076
+ } catch {
1077
+ return url;
1078
+ }
1079
+ }
1080
+
1081
+ // ─── 8. UNIVERSAL REALTIME FRONTEND CLIENT ──────────────────────────────────────
1082
+ /**
1083
+ * Universal Realtime Client for frontends/browsers.
1084
+ * Manages WebSocket connections through proxy or direct, user token resolution,
1085
+ * automatic heartbeat ping-pong, typed event bus, and global subscriptions.
1086
+ */
1087
+ export class RealtimeClient {
1088
+ constructor(options = {}) {
1089
+ const defaultUrl = isBrowser ? (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws' : '/ws';
1090
+ this.options = {
1091
+ url: defaultUrl,
1092
+ token: undefined, // string or function: () => token
1093
+ room: undefined, // optional room name or function: () => room
1094
+ autoConnect: true,
1095
+ reconnect: true,
1096
+ maxRetries: Infinity,
1097
+ initialRetryDelay: 1000,
1098
+ maxRetryDelay: 30000,
1099
+ backoffFactor: 1.5,
1100
+ jitter: true,
1101
+ heartbeatInterval: 20000,
1102
+ pingPayload: { type: 'PING' },
1103
+ ...options,
1104
+ };
1105
+
1106
+ this.events = new EventBus();
1107
+ this.ws = null;
1108
+ this.status = 'DISCONNECTED';
1109
+ this.retries = 0;
1110
+ this._pingTimer = null;
1111
+ this._reconnectTimer = null;
1112
+ this._isExplicitClose = false;
1113
+ this._connecting = false;
1114
+
1115
+ if (this.options.autoConnect) {
1116
+ this.init();
1117
+ }
1118
+ }
1119
+
1120
+ getToken() {
1121
+ if (typeof this.options.token === 'function') {
1122
+ return this.options.token();
1123
+ }
1124
+ if (typeof this.options.token === 'string') {
1125
+ return this.options.token;
1126
+ }
1127
+ if (isBrowser) {
1128
+ return localStorage.getItem('token') || sessionStorage.getItem('token') || '';
1129
+ }
1130
+ return '';
1131
+ }
1132
+
1133
+ getRoom() {
1134
+ if (typeof this.options.room === 'function') {
1135
+ return this.options.room();
1136
+ }
1137
+ if (typeof this.options.room === 'string') {
1138
+ return this.options.room;
1139
+ }
1140
+ return '';
1141
+ }
1142
+
1143
+ init() {
1144
+ if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) {
1145
+ return;
1146
+ }
1147
+ const token = this.getToken();
1148
+ if (!token && !this.options.anonymous) {
1149
+ return; // Wait until token is available
1150
+ }
1151
+ this.connect();
1152
+ }
1153
+
1154
+ connect() {
1155
+ if (this._connecting) return;
1156
+ if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) return;
1157
+
1158
+ this._connecting = true;
1159
+ this._isExplicitClose = false;
1160
+ this.cleanup();
1161
+
1162
+ const WS = getWebSocketConstructor(this.options.WebSocket);
1163
+ if (!WS) {
1164
+ this._connecting = false;
1165
+ console.warn('[cynx/realtime] WebSocket implementation not found in this environment.');
1166
+ return;
1167
+ }
1168
+
1169
+ try {
1170
+ const rawUrl = typeof this.options.url === 'function' ? this.options.url() : this.options.url;
1171
+ const normalized = normalizeWsUrl(rawUrl);
1172
+ const token = this.getToken();
1173
+ const room = this.getRoom();
1174
+
1175
+ const urlObj = new URL(normalized.replace(/^ws:/i, 'http:').replace(/^wss:/i, 'https:'));
1176
+ if (token && !urlObj.searchParams.has('token')) {
1177
+ urlObj.searchParams.set('token', token);
1178
+ }
1179
+ if (room && !urlObj.searchParams.has('room')) {
1180
+ urlObj.searchParams.set('room', room);
1181
+ }
1182
+
1183
+ const finalScheme = normalized.toLowerCase().startsWith('wss:') ? 'wss:' : 'ws:';
1184
+ const finalUrl = `${finalScheme}//${urlObj.host}${urlObj.pathname}${urlObj.search}`;
1185
+
1186
+ this.ws = new WS(finalUrl);
1187
+ this.status = this.retries > 0 ? 'RECONNECTING' : 'CONNECTING';
1188
+
1189
+ this.ws.onopen = (e) => {
1190
+ this._connecting = false;
1191
+ this.retries = 0;
1192
+ this.status = 'CONNECTED';
1193
+ this.events.emit('open', e);
1194
+ this.events.emit('status', this.status);
1195
+ this._startHeartbeat();
1196
+ };
1197
+
1198
+ this.ws.onmessage = (event) => {
1199
+ try {
1200
+ const raw = typeof event.data === 'string' ? event.data : String(event.data);
1201
+ const parsed = safeJSONParse(raw, raw);
1202
+ const eventType = (parsed && typeof parsed === 'object') ? (parsed.type || parsed.event || 'message') : 'message';
1203
+
1204
+ if (parsed && parsed.type === 'PONG') {
1205
+ return;
1206
+ }
1207
+
1208
+ this.events.emit(eventType, parsed);
1209
+ this.events.emit('message', parsed);
1210
+ this.events.emit('*', parsed);
1211
+ } catch (err) {
1212
+ console.warn('[cynx/realtime] Message parse warning:', err);
1213
+ }
1214
+ };
1215
+
1216
+ this.ws.onerror = (err) => {
1217
+ this.events.emit('error', err);
1218
+ };
1219
+
1220
+ this.ws.onclose = (e) => {
1221
+ this._connecting = false;
1222
+ this.status = 'DISCONNECTED';
1223
+ this._stopHeartbeat();
1224
+ this.events.emit('close', e);
1225
+ this.events.emit('status', this.status);
1226
+
1227
+ if (!this._isExplicitClose && this.options.reconnect && this.retries < this.options.maxRetries) {
1228
+ this._handleReconnect();
1229
+ }
1230
+ };
1231
+ } catch (err) {
1232
+ this._connecting = false;
1233
+ this._handleReconnect();
1234
+ }
1235
+ }
1236
+
1237
+ send(eventOrData, payload = undefined) {
1238
+ if (!this.ws || this.ws.readyState !== 1) return false;
1239
+ let message;
1240
+ if (payload !== undefined) {
1241
+ message = JSON.stringify({ type: eventOrData, payload });
1242
+ } else if (typeof eventOrData === 'object') {
1243
+ message = JSON.stringify(eventOrData);
1244
+ } else {
1245
+ message = String(eventOrData);
1246
+ }
1247
+ this.ws.send(message);
1248
+ return true;
1249
+ }
1250
+
1251
+ emit(event, payload = undefined) {
1252
+ return this.send(event, payload);
1253
+ }
1254
+
1255
+ subscribe(callback) {
1256
+ const unsub = this.events.on('*', callback);
1257
+ this.init();
1258
+ return unsub;
1259
+ }
1260
+
1261
+ on(event, callback) {
1262
+ const unsub = this.events.on(event, callback);
1263
+ this.init();
1264
+ return unsub;
1265
+ }
1266
+
1267
+ once(event, callback) {
1268
+ const unsub = this.events.once(event, callback);
1269
+ this.init();
1270
+ return unsub;
1271
+ }
1272
+
1273
+ off(event, callback) {
1274
+ this.events.off(event, callback);
1275
+ }
1276
+
1277
+ disconnect(code = 1000, reason = 'Client disconnected') {
1278
+ this._isExplicitClose = true;
1279
+ this.cleanup();
1280
+ if (this.ws) {
1281
+ try { this.ws.close(code, reason); } catch {}
1282
+ this.ws = null;
1283
+ }
1284
+ this.status = 'DISCONNECTED';
1285
+ this.events.emit('status', this.status);
1286
+ }
1287
+
1288
+ cleanup() {
1289
+ this._stopHeartbeat();
1290
+ if (this._reconnectTimer) {
1291
+ clearTimeout(this._reconnectTimer);
1292
+ this._reconnectTimer = null;
1293
+ }
1294
+ }
1295
+
1296
+ _startHeartbeat() {
1297
+ this._stopHeartbeat();
1298
+ if (!this.options.heartbeatInterval) return;
1299
+ this._pingTimer = setInterval(() => {
1300
+ if (this.ws && this.ws.readyState === 1) {
1301
+ try {
1302
+ this.ws.send(JSON.stringify(this.options.pingPayload));
1303
+ } catch {}
1304
+ }
1305
+ }, this.options.heartbeatInterval);
1306
+ }
1307
+
1308
+ _stopHeartbeat() {
1309
+ if (this._pingTimer) {
1310
+ clearInterval(this._pingTimer);
1311
+ this._pingTimer = null;
1312
+ }
1313
+ }
1314
+
1315
+ _handleReconnect() {
1316
+ if (this._reconnectTimer) return;
1317
+ this.retries++;
1318
+ const delay = Math.min(
1319
+ this.options.initialRetryDelay * Math.pow(this.options.backoffFactor, this.retries - 1),
1320
+ this.options.maxRetryDelay
1321
+ );
1322
+ const finalDelay = this.options.jitter ? delay * (0.8 + Math.random() * 0.4) : delay;
1323
+
1324
+ this._reconnectTimer = setTimeout(() => {
1325
+ this._reconnectTimer = null;
1326
+ this.connect();
1327
+ }, finalDelay);
1328
+ }
1329
+ }
1330
+
1331
+ export function createRealtimeClient(options) {
1332
+ return new RealtimeClient(options);
1333
+ }
1334
+
1335
+ // ─── 9. UNIVERSAL BACKEND WEBSOCKET PROXY / REPEATER ────────────────────────────
1336
+ /**
1337
+ * Universal WebSocket Repeater/Proxy for application backends.
1338
+ * Intercepts /ws upgrades, validates client sessions (protecting master WS_KEY),
1339
+ * and safely pipes traffic to/from upstream ws-server.
1340
+ */
1341
+ export class RealtimeProxy {
1342
+ constructor(server, options = {}) {
1343
+ this.server = server;
1344
+ this.options = {
1345
+ path: '/ws',
1346
+ upstreamUrl: undefined, // process.env.WS_URL
1347
+ upstreamKey: undefined, // process.env.WS_KEY
1348
+ room: 'default', // process.env.WS_ROOM
1349
+ authenticate: undefined,// async (req, token) => ({ userId })
1350
+ WebSocket: undefined,
1351
+ WebSocketServer: undefined,
1352
+ ...options,
1353
+ };
1354
+
1355
+ this.wss = null;
1356
+ this.events = new EventBus();
1357
+
1358
+ if (this.server) {
1359
+ this.attach(this.server);
1360
+ }
1361
+ }
1362
+
1363
+ attach(server) {
1364
+ this.server = server;
1365
+ let WSS = this.options.WebSocketServer;
1366
+ if (!WSS && typeof globalThis !== 'undefined' && globalThis.WebSocketServer) {
1367
+ WSS = globalThis.WebSocketServer;
1368
+ }
1369
+
1370
+ if (!WSS) {
1371
+ try {
1372
+ const wsModule = typeof require === 'function' ? require('ws') : null;
1373
+ if (wsModule && wsModule.WebSocketServer) {
1374
+ WSS = wsModule.WebSocketServer;
1375
+ } else if (wsModule && wsModule.Server) {
1376
+ WSS = wsModule.Server;
1377
+ }
1378
+ } catch {}
1379
+ }
1380
+
1381
+ if (!WSS) {
1382
+ console.warn('[cynx/realtime] WebSocketServer not available for RealtimeProxy.');
1383
+ return;
1384
+ }
1385
+
1386
+ this.wss = new WSS({ noServer: true });
1387
+
1388
+ server.on('upgrade', async (request, socket, head) => {
1389
+ try {
1390
+ const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
1391
+ if (url.pathname !== this.options.path) {
1392
+ return;
1393
+ }
1394
+
1395
+ const token = (
1396
+ url.searchParams.get('token') ||
1397
+ request.headers['authorization']?.replace(/^Bearer\s+/i, '') ||
1398
+ request.headers['x-auth-token'] ||
1399
+ ''
1400
+ ).trim();
1401
+
1402
+ let authData = null;
1403
+ if (typeof this.options.authenticate === 'function') {
1404
+ authData = await this.options.authenticate(request, token);
1405
+ if (!authData) {
1406
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
1407
+ socket.destroy();
1408
+ return;
1409
+ }
1410
+ }
1411
+
1412
+ const room = (url.searchParams.get('room') || this.options.room || 'default').trim().toLowerCase();
1413
+
1414
+ this.wss.handleUpgrade(request, socket, head, (clientWs) => {
1415
+ this.wss.emit('connection', clientWs, request, { authData, room });
1416
+ });
1417
+ } catch (err) {
1418
+ console.warn('[RealtimeProxy Upgrade Error]:', err.message);
1419
+ try {
1420
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
1421
+ socket.destroy();
1422
+ } catch {}
1423
+ }
1424
+ });
1425
+
1426
+ this.wss.on('connection', (clientWs, request, { authData, room }) => {
1427
+ this._handleConnection(clientWs, { authData, room });
1428
+ });
1429
+ }
1430
+
1431
+ _handleConnection(clientWs, { authData, room }) {
1432
+ const WS_URL = this.options.upstreamUrl || (typeof process !== 'undefined' ? process.env.WS_URL : '');
1433
+ const WS_KEY = this.options.upstreamKey || (typeof process !== 'undefined' ? process.env.WS_KEY : '');
1434
+ const targetRoom = room || this.options.room || 'default';
1435
+
1436
+ if (!WS_URL || !WS_KEY) {
1437
+ clientWs.close(1011, 'Real-Time upstream not configured');
1438
+ return;
1439
+ }
1440
+
1441
+ // Rock-solid URL normalization: strip duplicate /ws/
1442
+ const upstreamBase = WS_URL
1443
+ .replace(/^http:\/\//i, 'ws://')
1444
+ .replace(/^https:\/\//i, 'wss://')
1445
+ .replace(/\/ws\/?$/i, '')
1446
+ .replace(/\/$/, '');
1447
+
1448
+ const upstreamUrl = `${upstreamBase}/ws?room=${encodeURIComponent(targetRoom)}&token=${encodeURIComponent(WS_KEY)}`;
1449
+
1450
+ let WS = this.options.WebSocket;
1451
+ if (!WS && typeof globalThis !== 'undefined' && globalThis.WebSocket) {
1452
+ WS = globalThis.WebSocket;
1453
+ }
1454
+ if (!WS) {
1455
+ try {
1456
+ const wsModule = typeof require === 'function' ? require('ws') : null;
1457
+ if (wsModule) WS = wsModule;
1458
+ } catch {}
1459
+ }
1460
+
1461
+ if (!WS) {
1462
+ clientWs.close(1011, 'WebSocket client constructor not available');
1463
+ return;
1464
+ }
1465
+
1466
+ let upstreamWs = null;
1467
+ try {
1468
+ upstreamWs = new WS(upstreamUrl);
1469
+ } catch (err) {
1470
+ clientWs.close(1011, 'Failed to connect upstream');
1471
+ return;
1472
+ }
1473
+
1474
+ // Upstream -> Client
1475
+ upstreamWs.onmessage = (event) => {
1476
+ if (clientWs.readyState === 1) {
1477
+ clientWs.send(event.data);
1478
+ }
1479
+ };
1480
+ if (typeof upstreamWs.on === 'function') {
1481
+ upstreamWs.on('message', (data, isBinary) => {
1482
+ if (clientWs.readyState === 1) {
1483
+ clientWs.send(data, { binary: isBinary });
1484
+ }
1485
+ });
1486
+ upstreamWs.on('error', (err) => {
1487
+ console.warn('[RealtimeProxy Upstream Error]:', err.message);
1488
+ if (clientWs.readyState === 1) clientWs.close(1011, 'Upstream error');
1489
+ });
1490
+ upstreamWs.on('close', (code, reason) => {
1491
+ if (clientWs.readyState === 1) clientWs.close(code, reason);
1492
+ });
1493
+ }
1494
+
1495
+ // Client -> Upstream
1496
+ if (typeof clientWs.on === 'function') {
1497
+ clientWs.on('message', (data, isBinary) => {
1498
+ if (upstreamWs && upstreamWs.readyState === 1) {
1499
+ upstreamWs.send(data, { binary: isBinary });
1500
+ }
1501
+ });
1502
+ clientWs.on('error', () => {
1503
+ if (upstreamWs && upstreamWs.readyState === 1) upstreamWs.close();
1504
+ });
1505
+ clientWs.on('close', () => {
1506
+ if (upstreamWs && upstreamWs.readyState === 1) upstreamWs.close();
1507
+ });
1508
+ }
1509
+ }
1510
+ }
1511
+
1512
+ export function createRealtimeProxy(server, options) {
1513
+ return new RealtimeProxy(server, options);
1514
+ }
1515
+
1516
+ // ─── 10. UNIVERSAL REALTIME DISPATCHER (HTTP BROADCASTER) ───────────────────────
1517
+ /**
1518
+ * Universal Event Broadcaster for backend services and microservices.
1519
+ * Sends broadcast events to ws-server via HTTP REST (POST /api/broadcast and /broadcast).
1520
+ */
1521
+ export class RealtimeDispatcher {
1522
+ constructor(options = {}) {
1523
+ this.options = {
1524
+ url: undefined, // process.env.WS_URL
1525
+ key: undefined, // process.env.WS_KEY
1526
+ room: undefined, // process.env.WS_ROOM
1527
+ fetch: typeof globalThis !== 'undefined' ? globalThis.fetch : undefined,
1528
+ ...options,
1529
+ };
1530
+ }
1531
+
1532
+ async broadcast(eventType, payload = {}, options = {}) {
1533
+ const rawUrl = options.url || this.options.url || (typeof process !== 'undefined' ? process.env.WS_URL : '');
1534
+ const key = options.key || this.options.key || (typeof process !== 'undefined' ? process.env.WS_KEY : '');
1535
+ const room = (options.room || this.options.room || (typeof process !== 'undefined' ? process.env.WS_ROOM : '') || '').trim().toLowerCase();
1536
+
1537
+ if (!rawUrl || !key || !room) {
1538
+ return { ok: false, error: 'Missing WS_URL, WS_KEY, or WS_ROOM configuration' };
1539
+ }
1540
+
1541
+ const fetchFn = this.options.fetch || globalThis.fetch;
1542
+ if (!fetchFn) {
1543
+ return { ok: false, error: 'fetch function not found in environment' };
1544
+ }
1545
+
1546
+ const baseUrl = rawUrl
1547
+ .replace(/^ws:\/\//i, 'http://')
1548
+ .replace(/^wss:\/\//i, 'https://')
1549
+ .replace(/\/ws\/?$/i, '')
1550
+ .replace(/\/$/, '');
1551
+
1552
+ const endpoint = `${baseUrl}/api/broadcast?room=${encodeURIComponent(room)}`;
1553
+ const body = JSON.stringify({
1554
+ type: eventType,
1555
+ room,
1556
+ payload,
1557
+ timestamp: Date.now(),
1558
+ ...options.extra,
1559
+ });
1560
+
1561
+ const headers = {
1562
+ 'Content-Type': 'application/json',
1563
+ 'X-Secret-Key': key,
1564
+ 'x-ws-token': key,
1565
+ 'x-room': room,
1566
+ 'Authorization': `Bearer ${key}`,
1567
+ };
1568
+
1569
+ try {
1570
+ let res = await fetchFn(endpoint, {
1571
+ method: 'POST',
1572
+ headers,
1573
+ body,
1574
+ });
1575
+
1576
+ // Fallback to legacy /broadcast if /api/broadcast returned 404
1577
+ if (res.status === 404) {
1578
+ const legacyEndpoint = `${baseUrl}/broadcast?room=${encodeURIComponent(room)}`;
1579
+ res = await fetchFn(legacyEndpoint, { method: 'POST', headers, body });
1580
+ }
1581
+
1582
+ if (!res.ok) {
1583
+ const errText = await res.text().catch(() => '');
1584
+ console.warn(`[cynx/realtime] Broadcast warning (${res.status}):`, errText);
1585
+ return { ok: false, status: res.status, error: errText };
1586
+ }
1587
+
1588
+ const data = await res.json().catch(() => ({ status: 'ok' }));
1589
+ return { ok: true, data };
1590
+ } catch (err) {
1591
+ console.warn('[cynx/realtime] Broadcast network error:', err.message);
1592
+ return { ok: false, error: err.message };
1593
+ }
1594
+ }
1595
+
1596
+ async broadcastSession(token, eventType, payload = {}, options = {}) {
1597
+ return this.broadcast('session_event', payload, {
1598
+ ...options,
1599
+ extra: { token, eventType, sessionToken: token },
1600
+ });
1601
+ }
1602
+ }
1603
+
1604
+ export function createRealtimeDispatcher(options) {
1605
+ return new RealtimeDispatcher(options);
1606
+ }
1607
+
1021
1608
  // ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
1022
1609
 
1023
1610
  export default {
1024
1611
  // Client
1025
1612
  RealtimeSocket,
1026
1613
  createRealtimeSocket,
1614
+ RealtimeClient,
1615
+ createRealtimeClient,
1616
+ normalizeWsUrl,
1027
1617
  SSEStream,
1028
1618
  createSSE,
1029
1619
  parseAIStream,
@@ -1037,4 +1627,8 @@ export default {
1037
1627
  ConnectedClient,
1038
1628
  RealtimeServer,
1039
1629
  createRealtimeServer,
1630
+ RealtimeProxy,
1631
+ createRealtimeProxy,
1632
+ RealtimeDispatcher,
1633
+ createRealtimeDispatcher,
1040
1634
  };