@cossistant/core 0.0.33 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.d.ts +15 -2
- package/client.d.ts.map +1 -1
- package/client.js +30 -4
- package/client.js.map +1 -1
- package/env.d.ts +1 -0
- package/index.d.ts +7 -4
- package/index.js +6 -3
- package/package.json +1 -1
- package/realtime-client.d.ts +85 -0
- package/realtime-client.d.ts.map +1 -0
- package/realtime-client.js +463 -0
- package/realtime-client.js.map +1 -0
- package/realtime-event-filter.d.ts +15 -0
- package/realtime-event-filter.d.ts.map +1 -0
- package/realtime-event-filter.js +40 -0
- package/realtime-event-filter.js.map +1 -0
- package/resolve-public-key.d.ts +31 -0
- package/resolve-public-key.d.ts.map +1 -0
- package/resolve-public-key.js +52 -0
- package/resolve-public-key.js.map +1 -0
- package/rest-client.d.ts.map +1 -1
- package/rest-client.js +3 -2
- package/rest-client.js.map +1 -1
- package/store/seen-store.d.ts +1 -1
- package/store/timeline-items-store.d.ts.map +1 -1
- package/store/timeline-items-store.js +1 -1
- package/store/typing-store.d.ts +1 -1
- package/types/src/api/contact.js +297 -0
- package/types/src/api/contact.js.map +1 -0
- package/types/src/api/timeline-item.js +1 -1
- package/types/src/api/visitor.js +301 -0
- package/types/src/api/visitor.js.map +1 -0
- package/types/src/enums.js +12 -1
- package/types/src/enums.js.map +1 -1
- package/types/src/realtime-events.d.ts +2 -1
- package/types/src/realtime-events.d.ts.map +1 -1
- package/types/src/realtime-events.js +349 -0
- package/types/src/realtime-events.js.map +1 -0
- package/types/src/schemas.js +49 -0
- package/types/src/schemas.js.map +1 -0
- package/types/src/trpc/conversation.js +124 -0
- package/types/src/trpc/conversation.js.map +1 -0
- package/utils.d.ts.map +1 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { isValidEventType, validateRealtimeEvent } from "./types/src/realtime-events.js";
|
|
2
|
+
import { resolvePublicKey } from "./resolve-public-key.js";
|
|
3
|
+
|
|
4
|
+
//#region src/realtime-client.ts
|
|
5
|
+
const DEFAULT_WS_URL = "wss://api.cossistant.com/ws";
|
|
6
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
7
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 45e3;
|
|
8
|
+
const MAX_RECONNECT_DELAY_MS = 3e4;
|
|
9
|
+
const BASE_RECONNECT_DELAY_MS = 500;
|
|
10
|
+
/**
|
|
11
|
+
* Close codes that indicate the server rejected the connection permanently.
|
|
12
|
+
* No reconnect should be attempted.
|
|
13
|
+
*/
|
|
14
|
+
const PERMANENT_CLOSE_CODES = new Set([1008, 1011]);
|
|
15
|
+
/** Custom close code for heartbeat timeout. */
|
|
16
|
+
const HEARTBEAT_TIMEOUT_CODE = 4e3;
|
|
17
|
+
function decodeMessageData(data) {
|
|
18
|
+
if (typeof data === "string") return {
|
|
19
|
+
type: "raw-text",
|
|
20
|
+
data
|
|
21
|
+
};
|
|
22
|
+
if (data instanceof ArrayBuffer) try {
|
|
23
|
+
return {
|
|
24
|
+
type: "raw-text",
|
|
25
|
+
data: new TextDecoder().decode(data)
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
return { type: "unsupported" };
|
|
29
|
+
}
|
|
30
|
+
if (ArrayBuffer.isView(data)) try {
|
|
31
|
+
return {
|
|
32
|
+
type: "raw-text",
|
|
33
|
+
data: new TextDecoder().decode(data.buffer)
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return { type: "unsupported" };
|
|
37
|
+
}
|
|
38
|
+
return { type: "unsupported" };
|
|
39
|
+
}
|
|
40
|
+
function parseJson(raw) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function extractStringField(obj, field, _required = false) {
|
|
48
|
+
if (!obj || typeof obj !== "object" || !(field in obj)) return null;
|
|
49
|
+
const value = obj[field];
|
|
50
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function parseWebSocketMessage(rawText) {
|
|
54
|
+
if (rawText === "pong") return { type: "pong" };
|
|
55
|
+
const parsed = parseJson(rawText);
|
|
56
|
+
if (!parsed || typeof parsed !== "object") return { type: "invalid" };
|
|
57
|
+
const messageType = extractStringField(parsed, "type");
|
|
58
|
+
if (messageType === "CONNECTION_ESTABLISHED") {
|
|
59
|
+
const payload = parsed.payload;
|
|
60
|
+
return {
|
|
61
|
+
type: "connection-established",
|
|
62
|
+
connectionId: extractStringField(payload, "connectionId")
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if ("error" in parsed && "message" in parsed) return {
|
|
66
|
+
type: "error",
|
|
67
|
+
message: extractStringField(parsed, "message") || "Realtime connection error"
|
|
68
|
+
};
|
|
69
|
+
if (messageType && isValidEventType(messageType)) try {
|
|
70
|
+
const event = constructRealtimeEvent(parsed);
|
|
71
|
+
if (!event) return { type: "invalid" };
|
|
72
|
+
return {
|
|
73
|
+
type: "event",
|
|
74
|
+
event
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error("[Realtime] Failed to construct event", error);
|
|
78
|
+
return { type: "invalid" };
|
|
79
|
+
}
|
|
80
|
+
return { type: "invalid" };
|
|
81
|
+
}
|
|
82
|
+
function constructRealtimeEvent(parsed) {
|
|
83
|
+
if (!parsed || typeof parsed !== "object" || !("type" in parsed)) return null;
|
|
84
|
+
const type = parsed.type;
|
|
85
|
+
if (!isValidEventType(type)) return null;
|
|
86
|
+
const eventType = type;
|
|
87
|
+
const payloadSource = parsed.payload;
|
|
88
|
+
let payload;
|
|
89
|
+
try {
|
|
90
|
+
payload = validateRealtimeEvent(eventType, payloadSource);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("[Realtime] Received invalid event payload", error);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const organizationId = extractStringField(payloadSource, "organizationId", true);
|
|
96
|
+
const websiteId = extractStringField(payloadSource, "websiteId", true);
|
|
97
|
+
if (!organizationId) {
|
|
98
|
+
console.error("[Realtime] Received event without organizationId", parsed);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (!websiteId) {
|
|
102
|
+
console.error("[Realtime] Received event without websiteId", parsed);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const visitorId = extractStringField(parsed, "visitorId");
|
|
106
|
+
return {
|
|
107
|
+
type: eventType,
|
|
108
|
+
payload,
|
|
109
|
+
organizationId,
|
|
110
|
+
websiteId,
|
|
111
|
+
visitorId
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function resolvePublicKeyOrNull(explicit) {
|
|
115
|
+
return resolvePublicKey(explicit) ?? null;
|
|
116
|
+
}
|
|
117
|
+
function normalizeAuth(auth) {
|
|
118
|
+
if (!auth) return null;
|
|
119
|
+
if (auth.kind === "visitor") {
|
|
120
|
+
const visitorId = auth.visitorId?.trim() || null;
|
|
121
|
+
if (!visitorId) return null;
|
|
122
|
+
return {
|
|
123
|
+
type: "visitor",
|
|
124
|
+
visitorId,
|
|
125
|
+
websiteId: auth.websiteId?.trim() || null,
|
|
126
|
+
userId: null,
|
|
127
|
+
sessionToken: null,
|
|
128
|
+
publicKey: resolvePublicKeyOrNull(auth.publicKey ?? null)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const sessionToken = auth.sessionToken?.trim() || null;
|
|
132
|
+
if (!sessionToken) return null;
|
|
133
|
+
return {
|
|
134
|
+
type: "session",
|
|
135
|
+
visitorId: null,
|
|
136
|
+
websiteId: auth.websiteId?.trim() || null,
|
|
137
|
+
userId: auth.userId?.trim() || null,
|
|
138
|
+
sessionToken,
|
|
139
|
+
publicKey: null
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function buildSocketUrl(baseUrl, auth) {
|
|
143
|
+
if (!auth) return null;
|
|
144
|
+
try {
|
|
145
|
+
const url = new URL(baseUrl);
|
|
146
|
+
if (auth.type === "visitor") {
|
|
147
|
+
url.searchParams.set("visitorId", auth.visitorId ?? "");
|
|
148
|
+
const publicKey = auth.publicKey;
|
|
149
|
+
if (publicKey) url.searchParams.set("publicKey", publicKey);
|
|
150
|
+
} else {
|
|
151
|
+
url.searchParams.set("sessionToken", auth.sessionToken ?? "");
|
|
152
|
+
if (auth.websiteId) url.searchParams.set("websiteId", auth.websiteId);
|
|
153
|
+
}
|
|
154
|
+
return url.toString();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error("[Realtime] Failed to build WebSocket URL", error);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function authChanged(a, b) {
|
|
161
|
+
if (a === b) return false;
|
|
162
|
+
if (!(a && b)) return true;
|
|
163
|
+
return a.visitorId !== b.visitorId || a.websiteId !== b.websiteId || a.userId !== b.userId || a.sessionToken !== b.sessionToken || a.publicKey !== b.publicKey;
|
|
164
|
+
}
|
|
165
|
+
const INITIAL_STATE = {
|
|
166
|
+
status: "disconnected",
|
|
167
|
+
error: null,
|
|
168
|
+
connectionId: null
|
|
169
|
+
};
|
|
170
|
+
var RealtimeClient = class {
|
|
171
|
+
wsUrl;
|
|
172
|
+
heartbeatIntervalMs;
|
|
173
|
+
heartbeatTimeoutMs;
|
|
174
|
+
onEventCallback;
|
|
175
|
+
onConnectCallback;
|
|
176
|
+
onDisconnectCallback;
|
|
177
|
+
onErrorCallback;
|
|
178
|
+
socket = null;
|
|
179
|
+
state = { ...INITIAL_STATE };
|
|
180
|
+
auth = null;
|
|
181
|
+
reconnectAttempt = 0;
|
|
182
|
+
reconnectTimer = null;
|
|
183
|
+
heartbeatTimer = null;
|
|
184
|
+
lastHeartbeat = 0;
|
|
185
|
+
destroyed = false;
|
|
186
|
+
presenceEnabled = false;
|
|
187
|
+
presencePaused = false;
|
|
188
|
+
presenceTimer = null;
|
|
189
|
+
presenceIntervalMs = 0;
|
|
190
|
+
eventHandlers = /* @__PURE__ */ new Set();
|
|
191
|
+
stateListeners = /* @__PURE__ */ new Set();
|
|
192
|
+
constructor(options = {}) {
|
|
193
|
+
this.wsUrl = options.wsUrl ?? DEFAULT_WS_URL;
|
|
194
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
195
|
+
this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
196
|
+
this.onEventCallback = options.onEvent ?? null;
|
|
197
|
+
this.onConnectCallback = options.onConnect ?? null;
|
|
198
|
+
this.onDisconnectCallback = options.onDisconnect ?? null;
|
|
199
|
+
this.onErrorCallback = options.onError ?? null;
|
|
200
|
+
}
|
|
201
|
+
connect(auth) {
|
|
202
|
+
if (this.destroyed) return;
|
|
203
|
+
const resolved = normalizeAuth(auth);
|
|
204
|
+
if (!authChanged(this.auth, resolved)) return;
|
|
205
|
+
this.auth = resolved;
|
|
206
|
+
this.reconnectAttempt = 0;
|
|
207
|
+
this.closeSocket();
|
|
208
|
+
this.openSocket();
|
|
209
|
+
}
|
|
210
|
+
disconnect() {
|
|
211
|
+
this.clearReconnectTimer();
|
|
212
|
+
this.closeSocket();
|
|
213
|
+
this.auth = null;
|
|
214
|
+
this.setState({
|
|
215
|
+
status: "disconnected",
|
|
216
|
+
error: null,
|
|
217
|
+
connectionId: null
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
reconnect() {
|
|
221
|
+
if (this.destroyed || !this.auth) return;
|
|
222
|
+
this.closeSocket();
|
|
223
|
+
this.reconnectAttempt = 0;
|
|
224
|
+
this.openSocket();
|
|
225
|
+
}
|
|
226
|
+
updateAuth(auth) {
|
|
227
|
+
this.connect(auth);
|
|
228
|
+
}
|
|
229
|
+
send(event) {
|
|
230
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) throw new Error("Realtime connection is not established");
|
|
231
|
+
this.socket.send(JSON.stringify(event));
|
|
232
|
+
}
|
|
233
|
+
sendRaw(data) {
|
|
234
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) throw new Error("Realtime connection is not established");
|
|
235
|
+
this.socket.send(data);
|
|
236
|
+
}
|
|
237
|
+
subscribe(handler) {
|
|
238
|
+
this.eventHandlers.add(handler);
|
|
239
|
+
return () => {
|
|
240
|
+
this.eventHandlers.delete(handler);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
getState() {
|
|
244
|
+
return this.state;
|
|
245
|
+
}
|
|
246
|
+
onStateChange(listener) {
|
|
247
|
+
this.stateListeners.add(listener);
|
|
248
|
+
return () => {
|
|
249
|
+
this.stateListeners.delete(listener);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
enablePresence(intervalMs) {
|
|
253
|
+
this.presenceEnabled = true;
|
|
254
|
+
this.presencePaused = false;
|
|
255
|
+
this.presenceIntervalMs = intervalMs;
|
|
256
|
+
this.startPresenceTimer();
|
|
257
|
+
}
|
|
258
|
+
pausePresence() {
|
|
259
|
+
this.presencePaused = true;
|
|
260
|
+
this.stopPresenceTimer();
|
|
261
|
+
}
|
|
262
|
+
resumePresence() {
|
|
263
|
+
if (!this.presenceEnabled) return;
|
|
264
|
+
this.presencePaused = false;
|
|
265
|
+
this.sendPresencePing();
|
|
266
|
+
this.startPresenceTimer();
|
|
267
|
+
}
|
|
268
|
+
destroy() {
|
|
269
|
+
this.destroyed = true;
|
|
270
|
+
this.disconnect();
|
|
271
|
+
this.stopPresenceTimer();
|
|
272
|
+
this.eventHandlers.clear();
|
|
273
|
+
this.stateListeners.clear();
|
|
274
|
+
}
|
|
275
|
+
openSocket() {
|
|
276
|
+
if (this.destroyed) return;
|
|
277
|
+
const url = buildSocketUrl(this.wsUrl, this.auth);
|
|
278
|
+
if (!url) return;
|
|
279
|
+
this.setState({
|
|
280
|
+
...this.state,
|
|
281
|
+
status: "connecting",
|
|
282
|
+
error: null
|
|
283
|
+
});
|
|
284
|
+
try {
|
|
285
|
+
const socket = new WebSocket(url);
|
|
286
|
+
this.socket = socket;
|
|
287
|
+
socket.onopen = () => {
|
|
288
|
+
if (this.socket !== socket) return;
|
|
289
|
+
this.reconnectAttempt = 0;
|
|
290
|
+
this.lastHeartbeat = Date.now();
|
|
291
|
+
this.startHeartbeat();
|
|
292
|
+
this.setState({
|
|
293
|
+
status: "connected",
|
|
294
|
+
error: null,
|
|
295
|
+
connectionId: this.state.connectionId
|
|
296
|
+
});
|
|
297
|
+
this.onConnectCallback?.();
|
|
298
|
+
if (this.presenceEnabled && !this.presencePaused) {
|
|
299
|
+
this.sendPresencePing();
|
|
300
|
+
this.startPresenceTimer();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
socket.onclose = (event) => {
|
|
304
|
+
if (this.socket !== socket) return;
|
|
305
|
+
this.stopHeartbeat();
|
|
306
|
+
this.stopPresenceTimer();
|
|
307
|
+
const wasPermanent = PERMANENT_CLOSE_CODES.has(event.code);
|
|
308
|
+
this.setState({
|
|
309
|
+
status: "disconnected",
|
|
310
|
+
error: wasPermanent ? new Error(event.reason || "Realtime connection closed by server. Please check your credentials.") : this.state.error,
|
|
311
|
+
connectionId: null
|
|
312
|
+
});
|
|
313
|
+
this.onDisconnectCallback?.();
|
|
314
|
+
if (wasPermanent) {
|
|
315
|
+
if (this.state.error) this.onErrorCallback?.(this.state.error);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.scheduleReconnect();
|
|
319
|
+
};
|
|
320
|
+
socket.onmessage = (event) => {
|
|
321
|
+
if (this.socket !== socket) return;
|
|
322
|
+
this.handleMessage(event.data);
|
|
323
|
+
};
|
|
324
|
+
socket.onerror = () => {
|
|
325
|
+
if (this.socket !== socket || this.destroyed) return;
|
|
326
|
+
const err = /* @__PURE__ */ new Error("WebSocket error");
|
|
327
|
+
this.setState({
|
|
328
|
+
...this.state,
|
|
329
|
+
error: err
|
|
330
|
+
});
|
|
331
|
+
this.onErrorCallback?.(err);
|
|
332
|
+
};
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const err = error instanceof Error ? error : /* @__PURE__ */ new Error(`Failed to create WebSocket: ${String(error)}`);
|
|
335
|
+
this.setState({
|
|
336
|
+
status: "disconnected",
|
|
337
|
+
error: err,
|
|
338
|
+
connectionId: null
|
|
339
|
+
});
|
|
340
|
+
this.onErrorCallback?.(err);
|
|
341
|
+
this.scheduleReconnect();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
closeSocket() {
|
|
345
|
+
this.stopHeartbeat();
|
|
346
|
+
if (this.socket) {
|
|
347
|
+
const s = this.socket;
|
|
348
|
+
this.socket = null;
|
|
349
|
+
try {
|
|
350
|
+
s.onopen = null;
|
|
351
|
+
s.onclose = null;
|
|
352
|
+
s.onmessage = null;
|
|
353
|
+
s.onerror = null;
|
|
354
|
+
s.close();
|
|
355
|
+
} catch {}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
handleMessage(data) {
|
|
359
|
+
const decoded = decodeMessageData(data);
|
|
360
|
+
if (decoded.type === "unsupported") return;
|
|
361
|
+
const message = parseWebSocketMessage(decoded.data);
|
|
362
|
+
switch (message.type) {
|
|
363
|
+
case "pong":
|
|
364
|
+
this.lastHeartbeat = Date.now();
|
|
365
|
+
break;
|
|
366
|
+
case "connection-established":
|
|
367
|
+
this.lastHeartbeat = Date.now();
|
|
368
|
+
this.setState({
|
|
369
|
+
...this.state,
|
|
370
|
+
connectionId: message.connectionId
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
case "error": {
|
|
374
|
+
const err = new Error(message.message);
|
|
375
|
+
this.setState({
|
|
376
|
+
...this.state,
|
|
377
|
+
error: err
|
|
378
|
+
});
|
|
379
|
+
this.onErrorCallback?.(err);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "event":
|
|
383
|
+
this.lastHeartbeat = Date.now();
|
|
384
|
+
this.dispatchEvent(message.event);
|
|
385
|
+
break;
|
|
386
|
+
default: break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
dispatchEvent(event) {
|
|
390
|
+
this.onEventCallback?.(event);
|
|
391
|
+
for (const handler of this.eventHandlers) try {
|
|
392
|
+
handler(event);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const err = error instanceof Error ? error : /* @__PURE__ */ new Error(`Subscriber threw an exception: ${String(error)}`);
|
|
395
|
+
this.onErrorCallback?.(err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
startHeartbeat() {
|
|
399
|
+
this.stopHeartbeat();
|
|
400
|
+
this.heartbeatTimer = setInterval(() => {
|
|
401
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
|
|
402
|
+
if (this.lastHeartbeat !== 0 && Date.now() - this.lastHeartbeat > this.heartbeatTimeoutMs) {
|
|
403
|
+
this.socket.close(HEARTBEAT_TIMEOUT_CODE, "Heartbeat timeout");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
this.socket.send("ping");
|
|
408
|
+
} catch {}
|
|
409
|
+
}, this.heartbeatIntervalMs);
|
|
410
|
+
}
|
|
411
|
+
stopHeartbeat() {
|
|
412
|
+
if (this.heartbeatTimer !== null) {
|
|
413
|
+
clearInterval(this.heartbeatTimer);
|
|
414
|
+
this.heartbeatTimer = null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
scheduleReconnect() {
|
|
418
|
+
if (this.destroyed || !this.auth) return;
|
|
419
|
+
this.clearReconnectTimer();
|
|
420
|
+
const delay = Math.min(BASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempt, MAX_RECONNECT_DELAY_MS);
|
|
421
|
+
this.reconnectAttempt += 1;
|
|
422
|
+
this.reconnectTimer = setTimeout(() => {
|
|
423
|
+
this.reconnectTimer = null;
|
|
424
|
+
this.openSocket();
|
|
425
|
+
}, delay);
|
|
426
|
+
}
|
|
427
|
+
clearReconnectTimer() {
|
|
428
|
+
if (this.reconnectTimer !== null) {
|
|
429
|
+
clearTimeout(this.reconnectTimer);
|
|
430
|
+
this.reconnectTimer = null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
sendPresencePing() {
|
|
434
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN || this.presencePaused) return;
|
|
435
|
+
try {
|
|
436
|
+
this.socket.send("presence:ping");
|
|
437
|
+
} catch {}
|
|
438
|
+
}
|
|
439
|
+
startPresenceTimer() {
|
|
440
|
+
this.stopPresenceTimer();
|
|
441
|
+
if (!this.presenceEnabled || this.presencePaused) return;
|
|
442
|
+
if (this.presenceIntervalMs <= 0) return;
|
|
443
|
+
this.presenceTimer = setInterval(() => {
|
|
444
|
+
this.sendPresencePing();
|
|
445
|
+
}, this.presenceIntervalMs);
|
|
446
|
+
}
|
|
447
|
+
stopPresenceTimer() {
|
|
448
|
+
if (this.presenceTimer !== null) {
|
|
449
|
+
clearInterval(this.presenceTimer);
|
|
450
|
+
this.presenceTimer = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
setState(next) {
|
|
454
|
+
const prev = this.state;
|
|
455
|
+
if (prev.status === next.status && prev.error === next.error && prev.connectionId === next.connectionId) return;
|
|
456
|
+
this.state = next;
|
|
457
|
+
for (const listener of this.stateListeners) listener(next);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
//#endregion
|
|
462
|
+
export { RealtimeClient };
|
|
463
|
+
//# sourceMappingURL=realtime-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-client.js","names":["payload: unknown","INITIAL_STATE: RealtimeConnectionState"],"sources":["../src/realtime-client.ts"],"sourcesContent":["import {\n\ttype AnyRealtimeEvent,\n\tisValidEventType,\n\tvalidateRealtimeEvent,\n} from \"@cossistant/types/realtime-events\";\nimport { resolvePublicKey } from \"./resolve-public-key\";\n\nconst DEFAULT_WS_URL = \"wss://api.cossistant.com/ws\";\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;\nconst DEFAULT_HEARTBEAT_TIMEOUT_MS = 45_000;\nconst MAX_RECONNECT_DELAY_MS = 30_000;\nconst BASE_RECONNECT_DELAY_MS = 500;\n\n/**\n * Close codes that indicate the server rejected the connection permanently.\n * No reconnect should be attempted.\n */\nconst PERMANENT_CLOSE_CODES = new Set([1008, 1011]);\n\n/** Custom close code for heartbeat timeout. */\nconst HEARTBEAT_TIMEOUT_CODE = 4000;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type VisitorAuthConfig = {\n\tkind: \"visitor\";\n\tvisitorId: string | null;\n\twebsiteId?: string | null;\n\tpublicKey?: string | null;\n};\n\nexport type SessionAuthConfig = {\n\tkind: \"session\";\n\tsessionToken: string | null;\n\twebsiteId?: string | null;\n\tuserId?: string | null;\n};\n\nexport type RealtimeAuthConfig = VisitorAuthConfig | SessionAuthConfig;\n\ntype ResolvedAuthConfig = {\n\ttype: \"visitor\" | \"session\";\n\tvisitorId: string | null;\n\twebsiteId: string | null;\n\tuserId: string | null;\n\tsessionToken: string | null;\n\tpublicKey: string | null;\n};\n\nexport type RealtimeConnectionStatus =\n\t| \"disconnected\"\n\t| \"connecting\"\n\t| \"connected\";\n\nexport type RealtimeConnectionState = {\n\tstatus: RealtimeConnectionStatus;\n\terror: Error | null;\n\tconnectionId: string | null;\n};\n\ntype SubscribeHandler = (event: AnyRealtimeEvent) => void;\ntype StateChangeListener = (state: RealtimeConnectionState) => void;\n\nexport type RealtimeClientOptions = {\n\twsUrl?: string;\n\theartbeatIntervalMs?: number;\n\theartbeatTimeoutMs?: number;\n\tonEvent?: (event: AnyRealtimeEvent) => void;\n\tonConnect?: () => void;\n\tonDisconnect?: () => void;\n\tonError?: (error: Error) => void;\n};\n\n// ---------------------------------------------------------------------------\n// Message parsing (ported from React provider)\n// ---------------------------------------------------------------------------\n\ntype MessageDecodeResult =\n\t| { type: \"raw-text\"; data: string }\n\t| { type: \"unsupported\" };\n\ntype ParsedMessage =\n\t| { type: \"pong\" }\n\t| { type: \"connection-established\"; connectionId: string | null }\n\t| { type: \"error\"; message: string }\n\t| { type: \"event\"; event: AnyRealtimeEvent }\n\t| { type: \"invalid\" };\n\nfunction decodeMessageData(data: unknown): MessageDecodeResult {\n\tif (typeof data === \"string\") {\n\t\treturn { type: \"raw-text\", data };\n\t}\n\n\tif (data instanceof ArrayBuffer) {\n\t\ttry {\n\t\t\treturn { type: \"raw-text\", data: new TextDecoder().decode(data) };\n\t\t} catch {\n\t\t\treturn { type: \"unsupported\" };\n\t\t}\n\t}\n\n\tif (ArrayBuffer.isView(data)) {\n\t\ttry {\n\t\t\treturn { type: \"raw-text\", data: new TextDecoder().decode(data.buffer) };\n\t\t} catch {\n\t\t\treturn { type: \"unsupported\" };\n\t\t}\n\t}\n\n\treturn { type: \"unsupported\" };\n}\n\nfunction parseJson(raw: string): unknown {\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction extractStringField(\n\tobj: unknown,\n\tfield: string,\n\t_required = false\n): string | null {\n\tif (!obj || typeof obj !== \"object\" || !(field in obj)) {\n\t\treturn null;\n\t}\n\tconst value = (obj as Record<string, unknown>)[field];\n\tif (typeof value === \"string\" && value.length > 0) {\n\t\treturn value;\n\t}\n\treturn null;\n}\n\nfunction parseWebSocketMessage(rawText: string): ParsedMessage {\n\tif (rawText === \"pong\") {\n\t\treturn { type: \"pong\" };\n\t}\n\n\tconst parsed = parseJson(rawText);\n\tif (!parsed || typeof parsed !== \"object\") {\n\t\treturn { type: \"invalid\" };\n\t}\n\n\tconst messageType = extractStringField(parsed, \"type\");\n\n\tif (messageType === \"CONNECTION_ESTABLISHED\") {\n\t\tconst payload = (parsed as { payload?: unknown }).payload;\n\t\tconst connectionId = extractStringField(payload, \"connectionId\");\n\t\treturn { type: \"connection-established\", connectionId };\n\t}\n\n\tif (\"error\" in parsed && \"message\" in parsed) {\n\t\tconst message =\n\t\t\textractStringField(parsed, \"message\") || \"Realtime connection error\";\n\t\treturn { type: \"error\", message };\n\t}\n\n\tif (messageType && isValidEventType(messageType)) {\n\t\ttry {\n\t\t\tconst event = constructRealtimeEvent(parsed);\n\t\t\tif (!event) {\n\t\t\t\treturn { type: \"invalid\" };\n\t\t\t}\n\t\t\treturn { type: \"event\", event };\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[Realtime] Failed to construct event\", error);\n\t\t\treturn { type: \"invalid\" };\n\t\t}\n\t}\n\n\treturn { type: \"invalid\" };\n}\n\nfunction constructRealtimeEvent(parsed: unknown): AnyRealtimeEvent | null {\n\tif (!parsed || typeof parsed !== \"object\" || !(\"type\" in parsed)) {\n\t\treturn null;\n\t}\n\n\tconst type = (parsed as { type: unknown }).type;\n\tif (!isValidEventType(type)) {\n\t\treturn null;\n\t}\n\n\tconst eventType = type;\n\tconst payloadSource = (parsed as { payload?: unknown }).payload;\n\n\tlet payload: unknown;\n\ttry {\n\t\tpayload = validateRealtimeEvent(eventType, payloadSource);\n\t} catch (error) {\n\t\tconsole.error(\"[Realtime] Received invalid event payload\", error);\n\t\treturn null;\n\t}\n\n\tconst organizationId = extractStringField(\n\t\tpayloadSource,\n\t\t\"organizationId\",\n\t\ttrue\n\t);\n\tconst websiteId = extractStringField(payloadSource, \"websiteId\", true);\n\n\tif (!organizationId) {\n\t\tconsole.error(\"[Realtime] Received event without organizationId\", parsed);\n\t\treturn null;\n\t}\n\n\tif (!websiteId) {\n\t\tconsole.error(\"[Realtime] Received event without websiteId\", parsed);\n\t\treturn null;\n\t}\n\n\tconst visitorId = extractStringField(parsed, \"visitorId\");\n\n\treturn {\n\t\ttype: eventType,\n\t\tpayload,\n\t\torganizationId,\n\t\twebsiteId,\n\t\tvisitorId,\n\t} as AnyRealtimeEvent;\n}\n\n// ---------------------------------------------------------------------------\n// Auth helpers (ported from React provider)\n// ---------------------------------------------------------------------------\n\nfunction resolvePublicKeyOrNull(explicit?: string | null): string | null {\n\treturn resolvePublicKey(explicit) ?? null;\n}\n\nfunction normalizeAuth(\n\tauth: RealtimeAuthConfig | null\n): ResolvedAuthConfig | null {\n\tif (!auth) {\n\t\treturn null;\n\t}\n\n\tif (auth.kind === \"visitor\") {\n\t\tconst visitorId = auth.visitorId?.trim() || null;\n\n\t\tif (!visitorId) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"visitor\",\n\t\t\tvisitorId,\n\t\t\twebsiteId: auth.websiteId?.trim() || null,\n\t\t\tuserId: null,\n\t\t\tsessionToken: null,\n\t\t\tpublicKey: resolvePublicKeyOrNull(auth.publicKey ?? null),\n\t\t} satisfies ResolvedAuthConfig;\n\t}\n\n\tconst sessionToken = auth.sessionToken?.trim() || null;\n\n\tif (!sessionToken) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\ttype: \"session\",\n\t\tvisitorId: null,\n\t\twebsiteId: auth.websiteId?.trim() || null,\n\t\tuserId: auth.userId?.trim() || null,\n\t\tsessionToken,\n\t\tpublicKey: null,\n\t} satisfies ResolvedAuthConfig;\n}\n\nfunction buildSocketUrl(\n\tbaseUrl: string,\n\tauth: ResolvedAuthConfig | null\n): string | null {\n\tif (!auth) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst url = new URL(baseUrl);\n\n\t\tif (auth.type === \"visitor\") {\n\t\t\turl.searchParams.set(\"visitorId\", auth.visitorId ?? \"\");\n\t\t\tconst publicKey = auth.publicKey;\n\t\t\tif (publicKey) {\n\t\t\t\turl.searchParams.set(\"publicKey\", publicKey);\n\t\t\t}\n\t\t} else {\n\t\t\turl.searchParams.set(\"sessionToken\", auth.sessionToken ?? \"\");\n\t\t\tif (auth.websiteId) {\n\t\t\t\turl.searchParams.set(\"websiteId\", auth.websiteId);\n\t\t\t}\n\t\t}\n\n\t\treturn url.toString();\n\t} catch (error) {\n\t\tconsole.error(\"[Realtime] Failed to build WebSocket URL\", error);\n\t\treturn null;\n\t}\n}\n\nfunction authChanged(\n\ta: ResolvedAuthConfig | null,\n\tb: ResolvedAuthConfig | null\n): boolean {\n\tif (a === b) {\n\t\treturn false;\n\t}\n\tif (!(a && b)) {\n\t\treturn true;\n\t}\n\treturn (\n\t\ta.visitorId !== b.visitorId ||\n\t\ta.websiteId !== b.websiteId ||\n\t\ta.userId !== b.userId ||\n\t\ta.sessionToken !== b.sessionToken ||\n\t\ta.publicKey !== b.publicKey\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// RealtimeClient\n// ---------------------------------------------------------------------------\n\nconst INITIAL_STATE: RealtimeConnectionState = {\n\tstatus: \"disconnected\",\n\terror: null,\n\tconnectionId: null,\n};\n\nexport class RealtimeClient {\n\tprivate wsUrl: string;\n\tprivate heartbeatIntervalMs: number;\n\tprivate heartbeatTimeoutMs: number;\n\tprivate onEventCallback: ((event: AnyRealtimeEvent) => void) | null;\n\tprivate onConnectCallback: (() => void) | null;\n\tprivate onDisconnectCallback: (() => void) | null;\n\tprivate onErrorCallback: ((error: Error) => void) | null;\n\n\tprivate socket: WebSocket | null = null;\n\tprivate state: RealtimeConnectionState = { ...INITIAL_STATE };\n\tprivate auth: ResolvedAuthConfig | null = null;\n\tprivate reconnectAttempt = 0;\n\tprivate reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate lastHeartbeat = 0;\n\tprivate destroyed = false;\n\n\t// Presence\n\tprivate presenceEnabled = false;\n\tprivate presencePaused = false;\n\tprivate presenceTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate presenceIntervalMs = 0;\n\n\t// Subscribers\n\tprivate eventHandlers = new Set<SubscribeHandler>();\n\tprivate stateListeners = new Set<StateChangeListener>();\n\n\tconstructor(options: RealtimeClientOptions = {}) {\n\t\tthis.wsUrl = options.wsUrl ?? DEFAULT_WS_URL;\n\t\tthis.heartbeatIntervalMs =\n\t\t\toptions.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n\t\tthis.heartbeatTimeoutMs =\n\t\t\toptions.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;\n\t\tthis.onEventCallback = options.onEvent ?? null;\n\t\tthis.onConnectCallback = options.onConnect ?? null;\n\t\tthis.onDisconnectCallback = options.onDisconnect ?? null;\n\t\tthis.onErrorCallback = options.onError ?? null;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Public API\n\t// -----------------------------------------------------------------------\n\n\tconnect(auth: RealtimeAuthConfig | null): void {\n\t\tif (this.destroyed) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst resolved = normalizeAuth(auth);\n\n\t\tif (!authChanged(this.auth, resolved)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.auth = resolved;\n\t\tthis.reconnectAttempt = 0;\n\t\tthis.closeSocket();\n\t\tthis.openSocket();\n\t}\n\n\tdisconnect(): void {\n\t\tthis.clearReconnectTimer();\n\t\tthis.closeSocket();\n\t\tthis.auth = null;\n\t\tthis.setState({ status: \"disconnected\", error: null, connectionId: null });\n\t}\n\n\treconnect(): void {\n\t\tif (this.destroyed || !this.auth) {\n\t\t\treturn;\n\t\t}\n\t\tthis.closeSocket();\n\t\tthis.reconnectAttempt = 0;\n\t\tthis.openSocket();\n\t}\n\n\tupdateAuth(auth: RealtimeAuthConfig | null): void {\n\t\tthis.connect(auth);\n\t}\n\n\tsend(event: AnyRealtimeEvent): void {\n\t\tif (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n\t\t\tthrow new Error(\"Realtime connection is not established\");\n\t\t}\n\n\t\tthis.socket.send(JSON.stringify(event));\n\t}\n\n\tsendRaw(data: string): void {\n\t\tif (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n\t\t\tthrow new Error(\"Realtime connection is not established\");\n\t\t}\n\n\t\tthis.socket.send(data);\n\t}\n\n\tsubscribe(handler: SubscribeHandler): () => void {\n\t\tthis.eventHandlers.add(handler);\n\t\treturn () => {\n\t\t\tthis.eventHandlers.delete(handler);\n\t\t};\n\t}\n\n\tgetState(): RealtimeConnectionState {\n\t\treturn this.state;\n\t}\n\n\tonStateChange(listener: StateChangeListener): () => void {\n\t\tthis.stateListeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.stateListeners.delete(listener);\n\t\t};\n\t}\n\n\t// Presence management\n\n\tenablePresence(intervalMs: number): void {\n\t\tthis.presenceEnabled = true;\n\t\tthis.presencePaused = false;\n\t\tthis.presenceIntervalMs = intervalMs;\n\t\tthis.startPresenceTimer();\n\t}\n\n\tpausePresence(): void {\n\t\tthis.presencePaused = true;\n\t\tthis.stopPresenceTimer();\n\t}\n\n\tresumePresence(): void {\n\t\tif (!this.presenceEnabled) {\n\t\t\treturn;\n\t\t}\n\t\tthis.presencePaused = false;\n\t\tthis.sendPresencePing();\n\t\tthis.startPresenceTimer();\n\t}\n\n\tdestroy(): void {\n\t\tthis.destroyed = true;\n\t\tthis.disconnect();\n\t\tthis.stopPresenceTimer();\n\t\tthis.eventHandlers.clear();\n\t\tthis.stateListeners.clear();\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Internal\n\t// -----------------------------------------------------------------------\n\n\tprivate openSocket(): void {\n\t\tif (this.destroyed) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst url = buildSocketUrl(this.wsUrl, this.auth);\n\t\tif (!url) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({\n\t\t\t...this.state,\n\t\t\tstatus: \"connecting\",\n\t\t\terror: null,\n\t\t});\n\n\t\ttry {\n\t\t\tconst socket = new WebSocket(url);\n\t\t\tthis.socket = socket;\n\n\t\t\tsocket.onopen = () => {\n\t\t\t\tif (this.socket !== socket) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.reconnectAttempt = 0;\n\t\t\t\tthis.lastHeartbeat = Date.now();\n\t\t\t\tthis.startHeartbeat();\n\t\t\t\tthis.setState({\n\t\t\t\t\tstatus: \"connected\",\n\t\t\t\t\terror: null,\n\t\t\t\t\tconnectionId: this.state.connectionId,\n\t\t\t\t});\n\t\t\t\tthis.onConnectCallback?.();\n\n\t\t\t\tif (this.presenceEnabled && !this.presencePaused) {\n\t\t\t\t\tthis.sendPresencePing();\n\t\t\t\t\tthis.startPresenceTimer();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tsocket.onclose = (event) => {\n\t\t\t\tif (this.socket !== socket) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.stopHeartbeat();\n\t\t\t\tthis.stopPresenceTimer();\n\n\t\t\t\tconst wasPermanent = PERMANENT_CLOSE_CODES.has(event.code);\n\n\t\t\t\tthis.setState({\n\t\t\t\t\tstatus: \"disconnected\",\n\t\t\t\t\terror: wasPermanent\n\t\t\t\t\t\t? new Error(\n\t\t\t\t\t\t\t\tevent.reason ||\n\t\t\t\t\t\t\t\t\t\"Realtime connection closed by server. Please check your credentials.\"\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: this.state.error,\n\t\t\t\t\tconnectionId: null,\n\t\t\t\t});\n\n\t\t\t\tthis.onDisconnectCallback?.();\n\n\t\t\t\tif (wasPermanent) {\n\t\t\t\t\tif (this.state.error) {\n\t\t\t\t\t\tthis.onErrorCallback?.(this.state.error);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.scheduleReconnect();\n\t\t\t};\n\n\t\t\tsocket.onmessage = (event) => {\n\t\t\t\tif (this.socket !== socket) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.handleMessage(event.data);\n\t\t\t};\n\n\t\t\tsocket.onerror = () => {\n\t\t\t\tif (this.socket !== socket || this.destroyed) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst err = new Error(\"WebSocket error\");\n\t\t\t\tthis.setState({ ...this.state, error: err });\n\t\t\t\tthis.onErrorCallback?.(err);\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconst err =\n\t\t\t\terror instanceof Error\n\t\t\t\t\t? error\n\t\t\t\t\t: new Error(`Failed to create WebSocket: ${String(error)}`);\n\t\t\tthis.setState({\n\t\t\t\tstatus: \"disconnected\",\n\t\t\t\terror: err,\n\t\t\t\tconnectionId: null,\n\t\t\t});\n\t\t\tthis.onErrorCallback?.(err);\n\t\t\tthis.scheduleReconnect();\n\t\t}\n\t}\n\n\tprivate closeSocket(): void {\n\t\tthis.stopHeartbeat();\n\t\tif (this.socket) {\n\t\t\tconst s = this.socket;\n\t\t\tthis.socket = null;\n\t\t\ttry {\n\t\t\t\ts.onopen = null;\n\t\t\t\ts.onclose = null;\n\t\t\t\ts.onmessage = null;\n\t\t\t\ts.onerror = null;\n\t\t\t\ts.close();\n\t\t\t} catch {\n\t\t\t\t// Ignore close errors\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleMessage(data: unknown): void {\n\t\tconst decoded = decodeMessageData(data);\n\t\tif (decoded.type === \"unsupported\") {\n\t\t\treturn;\n\t\t}\n\n\t\tconst message = parseWebSocketMessage(decoded.data);\n\n\t\tswitch (message.type) {\n\t\t\tcase \"pong\":\n\t\t\t\tthis.lastHeartbeat = Date.now();\n\t\t\t\tbreak;\n\n\t\t\tcase \"connection-established\":\n\t\t\t\tthis.lastHeartbeat = Date.now();\n\t\t\t\tthis.setState({ ...this.state, connectionId: message.connectionId });\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\": {\n\t\t\t\tconst err = new Error(message.message);\n\t\t\t\tthis.setState({ ...this.state, error: err });\n\t\t\t\tthis.onErrorCallback?.(err);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"event\":\n\t\t\t\tthis.lastHeartbeat = Date.now();\n\t\t\t\tthis.dispatchEvent(message.event);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate dispatchEvent(event: AnyRealtimeEvent): void {\n\t\tthis.onEventCallback?.(event);\n\n\t\tfor (const handler of this.eventHandlers) {\n\t\t\ttry {\n\t\t\t\thandler(event);\n\t\t\t} catch (error) {\n\t\t\t\tconst err =\n\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t? error\n\t\t\t\t\t\t: new Error(`Subscriber threw an exception: ${String(error)}`);\n\t\t\t\tthis.onErrorCallback?.(err);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Heartbeat\n\n\tprivate startHeartbeat(): void {\n\t\tthis.stopHeartbeat();\n\t\tthis.heartbeatTimer = setInterval(() => {\n\t\t\tif (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tthis.lastHeartbeat !== 0 &&\n\t\t\t\tDate.now() - this.lastHeartbeat > this.heartbeatTimeoutMs\n\t\t\t) {\n\t\t\t\tthis.socket.close(HEARTBEAT_TIMEOUT_CODE, \"Heartbeat timeout\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tthis.socket.send(\"ping\");\n\t\t\t} catch {\n\t\t\t\t// Ignore send failures; reconnect logic will handle it\n\t\t\t}\n\t\t}, this.heartbeatIntervalMs);\n\t}\n\n\tprivate stopHeartbeat(): void {\n\t\tif (this.heartbeatTimer !== null) {\n\t\t\tclearInterval(this.heartbeatTimer);\n\t\t\tthis.heartbeatTimer = null;\n\t\t}\n\t}\n\n\t// Reconnect\n\n\tprivate scheduleReconnect(): void {\n\t\tif (this.destroyed || !this.auth) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.clearReconnectTimer();\n\n\t\tconst delay = Math.min(\n\t\t\tBASE_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempt,\n\t\t\tMAX_RECONNECT_DELAY_MS\n\t\t);\n\t\tthis.reconnectAttempt += 1;\n\n\t\tthis.reconnectTimer = setTimeout(() => {\n\t\t\tthis.reconnectTimer = null;\n\t\t\tthis.openSocket();\n\t\t}, delay);\n\t}\n\n\tprivate clearReconnectTimer(): void {\n\t\tif (this.reconnectTimer !== null) {\n\t\t\tclearTimeout(this.reconnectTimer);\n\t\t\tthis.reconnectTimer = null;\n\t\t}\n\t}\n\n\t// Presence\n\n\tprivate sendPresencePing(): void {\n\t\tif (\n\t\t\t!this.socket ||\n\t\t\tthis.socket.readyState !== WebSocket.OPEN ||\n\t\t\tthis.presencePaused\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.socket.send(\"presence:ping\");\n\t\t} catch {\n\t\t\t// Ignore send failures\n\t\t}\n\t}\n\n\tprivate startPresenceTimer(): void {\n\t\tthis.stopPresenceTimer();\n\t\tif (!this.presenceEnabled || this.presencePaused) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.presenceIntervalMs <= 0) {\n\t\t\treturn;\n\t\t}\n\t\tthis.presenceTimer = setInterval(() => {\n\t\t\tthis.sendPresencePing();\n\t\t}, this.presenceIntervalMs);\n\t}\n\n\tprivate stopPresenceTimer(): void {\n\t\tif (this.presenceTimer !== null) {\n\t\t\tclearInterval(this.presenceTimer);\n\t\t\tthis.presenceTimer = null;\n\t\t}\n\t}\n\n\t// State management\n\n\tprivate setState(next: RealtimeConnectionState): void {\n\t\tconst prev = this.state;\n\t\tif (\n\t\t\tprev.status === next.status &&\n\t\t\tprev.error === next.error &&\n\t\t\tprev.connectionId === next.connectionId\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.state = next;\n\n\t\tfor (const listener of this.stateListeners) {\n\t\t\tlistener(next);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AnyRealtimeEvent } from "./types/src/realtime-events.js";
|
|
2
|
+
|
|
3
|
+
//#region src/realtime-event-filter.d.ts
|
|
4
|
+
declare function getTargetVisitorId(event: AnyRealtimeEvent): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Determines whether a realtime event should be processed based on website and
|
|
7
|
+
* visitor identifiers.
|
|
8
|
+
*
|
|
9
|
+
* When a visitorId is provided (i.e. the consumer is a visitor/widget), private
|
|
10
|
+
* timeline items are filtered out to prevent leaking internal data.
|
|
11
|
+
*/
|
|
12
|
+
declare function shouldDeliverEvent(event: AnyRealtimeEvent, websiteId: string | null, visitorId: string | null): boolean;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { getTargetVisitorId, shouldDeliverEvent };
|
|
15
|
+
//# sourceMappingURL=realtime-event-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-event-filter.d.ts","names":[],"sources":["../src/realtime-event-filter.ts"],"sourcesContent":[],"mappings":";;;iBAES,kBAAA,QAA0B;;AAFuC;AA2B1E;;;;;iBAAgB,kBAAA,QACR"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/realtime-event-filter.ts
|
|
2
|
+
function getTargetVisitorId(event) {
|
|
3
|
+
const payloadVisitorId = event.payload.visitorId;
|
|
4
|
+
if (typeof payloadVisitorId === "string" && payloadVisitorId.length > 0) return payloadVisitorId;
|
|
5
|
+
if (event.type === "timelineItemCreated") {
|
|
6
|
+
const itemVisitorId = event.payload.item.visitorId;
|
|
7
|
+
if (typeof itemVisitorId === "string" && itemVisitorId.length > 0) return itemVisitorId;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Determines whether a realtime event should be processed based on website and
|
|
13
|
+
* visitor identifiers.
|
|
14
|
+
*
|
|
15
|
+
* When a visitorId is provided (i.e. the consumer is a visitor/widget), private
|
|
16
|
+
* timeline items are filtered out to prevent leaking internal data.
|
|
17
|
+
*/
|
|
18
|
+
function shouldDeliverEvent(event, websiteId, visitorId) {
|
|
19
|
+
if (websiteId && event.payload.websiteId !== websiteId) return false;
|
|
20
|
+
if (visitorId && isBlockedTimelineEventForVisitor(event)) return false;
|
|
21
|
+
if (!visitorId) return true;
|
|
22
|
+
const targetVisitorId = getTargetVisitorId(event);
|
|
23
|
+
if (targetVisitorId && targetVisitorId !== visitorId) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the event carries a blocked timeline item for visitor delivery.
|
|
28
|
+
*/
|
|
29
|
+
function isBlockedTimelineEventForVisitor(event) {
|
|
30
|
+
if (event.type === "timelineItemCreated" || event.type === "timelineItemUpdated") {
|
|
31
|
+
const item = event.payload.item;
|
|
32
|
+
if (item && item.visibility === "private") return true;
|
|
33
|
+
if (item && item.type === "tool" && (!("visibility" in item) || item.visibility !== "public")) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { getTargetVisitorId, shouldDeliverEvent };
|
|
40
|
+
//# sourceMappingURL=realtime-event-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-event-filter.js","names":[],"sources":["../src/realtime-event-filter.ts"],"sourcesContent":["import type { AnyRealtimeEvent } from \"@cossistant/types/realtime-events\";\n\nfunction getTargetVisitorId(event: AnyRealtimeEvent): string | null {\n\tconst payloadVisitorId = event.payload.visitorId;\n\n\tif (typeof payloadVisitorId === \"string\" && payloadVisitorId.length > 0) {\n\t\treturn payloadVisitorId;\n\t}\n\n\tif (event.type === \"timelineItemCreated\") {\n\t\tconst itemVisitorId = event.payload.item.visitorId;\n\n\t\tif (typeof itemVisitorId === \"string\" && itemVisitorId.length > 0) {\n\t\t\treturn itemVisitorId;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Determines whether a realtime event should be processed based on website and\n * visitor identifiers.\n *\n * When a visitorId is provided (i.e. the consumer is a visitor/widget), private\n * timeline items are filtered out to prevent leaking internal data.\n */\nexport function shouldDeliverEvent(\n\tevent: AnyRealtimeEvent,\n\twebsiteId: string | null,\n\tvisitorId: string | null\n): boolean {\n\tif (websiteId && event.payload.websiteId !== websiteId) {\n\t\treturn false;\n\t}\n\n\t// When consuming as a visitor, never deliver private/non-public tool timeline items.\n\t// This is a defense-in-depth measure; the server should also filter these.\n\tif (visitorId && isBlockedTimelineEventForVisitor(event)) {\n\t\treturn false;\n\t}\n\n\tif (!visitorId) {\n\t\treturn true;\n\t}\n\n\tconst targetVisitorId = getTargetVisitorId(event);\n\n\tif (targetVisitorId && targetVisitorId !== visitorId) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Returns true if the event carries a blocked timeline item for visitor delivery.\n */\nfunction isBlockedTimelineEventForVisitor(event: AnyRealtimeEvent): boolean {\n\tif (\n\t\tevent.type === \"timelineItemCreated\" ||\n\t\tevent.type === \"timelineItemUpdated\"\n\t) {\n\t\tconst payload = event.payload as Record<string, unknown>;\n\t\tconst item = payload.item as Record<string, unknown> | undefined;\n\t\tif (item && item.visibility === \"private\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (\n\t\t\titem &&\n\t\t\titem.type === \"tool\" &&\n\t\t\t(!(\"visibility\" in item) || item.visibility !== \"public\")\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nexport { getTargetVisitorId };\n"],"mappings":";AAEA,SAAS,mBAAmB,OAAwC;CACnE,MAAM,mBAAmB,MAAM,QAAQ;AAEvC,KAAI,OAAO,qBAAqB,YAAY,iBAAiB,SAAS,EACrE,QAAO;AAGR,KAAI,MAAM,SAAS,uBAAuB;EACzC,MAAM,gBAAgB,MAAM,QAAQ,KAAK;AAEzC,MAAI,OAAO,kBAAkB,YAAY,cAAc,SAAS,EAC/D,QAAO;;AAIT,QAAO;;;;;;;;;AAUR,SAAgB,mBACf,OACA,WACA,WACU;AACV,KAAI,aAAa,MAAM,QAAQ,cAAc,UAC5C,QAAO;AAKR,KAAI,aAAa,iCAAiC,MAAM,CACvD,QAAO;AAGR,KAAI,CAAC,UACJ,QAAO;CAGR,MAAM,kBAAkB,mBAAmB,MAAM;AAEjD,KAAI,mBAAmB,oBAAoB,UAC1C,QAAO;AAGR,QAAO;;;;;AAMR,SAAS,iCAAiC,OAAkC;AAC3E,KACC,MAAM,SAAS,yBACf,MAAM,SAAS,uBACd;EAED,MAAM,OADU,MAAM,QACD;AACrB,MAAI,QAAQ,KAAK,eAAe,UAC/B,QAAO;AAER,MACC,QACA,KAAK,SAAS,WACb,EAAE,gBAAgB,SAAS,KAAK,eAAe,UAEhD,QAAO;;AAGT,QAAO"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/resolve-public-key.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Supported framework types for environment variable detection.
|
|
4
|
+
*/
|
|
5
|
+
type Framework = "nextjs" | "vite" | "other";
|
|
6
|
+
/**
|
|
7
|
+
* Detect the current framework environment.
|
|
8
|
+
*
|
|
9
|
+
* Used primarily for user-facing error messages so we can suggest
|
|
10
|
+
* the correct environment variable name.
|
|
11
|
+
*/
|
|
12
|
+
declare function detectFramework(): Framework;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the recommended environment variable name for the detected framework.
|
|
15
|
+
*/
|
|
16
|
+
declare function getEnvVarName(framework?: Framework): string;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the Cossistant public API key from multiple sources.
|
|
19
|
+
*
|
|
20
|
+
* Priority order:
|
|
21
|
+
* 1. Explicit value passed as argument
|
|
22
|
+
* 2. `process.env.NEXT_PUBLIC_COSSISTANT_API_KEY` (Next.js / CRA / webpack)
|
|
23
|
+
* 3. `process.env.COSSISTANT_API_KEY` (generic Node.js / CRA)
|
|
24
|
+
* 4. `import.meta.env.VITE_COSSISTANT_API_KEY` (Vite)
|
|
25
|
+
*
|
|
26
|
+
* Safe across all environments: Node.js, browser, Vite, Next.js, CRA.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolvePublicKey(explicit?: string | null): string | undefined;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { Framework, detectFramework, getEnvVarName, resolvePublicKey };
|
|
31
|
+
//# sourceMappingURL=resolve-public-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-public-key.d.ts","names":[],"sources":["../src/resolve-public-key.ts"],"sourcesContent":[],"mappings":";;AAKA;AAQA;AA8BgB,KAtCJ,SAAA,GAsCiB,QAAa,GAAA,MAAA,GAAS,OAAA;AAuBnD;;;;;;iBArDgB,eAAA,CAAA,GAAmB;;;;iBA8BnB,aAAA,aAA0B;;;;;;;;;;;;iBAuB1B,gBAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/resolve-public-key.ts
|
|
2
|
+
/**
|
|
3
|
+
* Detect the current framework environment.
|
|
4
|
+
*
|
|
5
|
+
* Used primarily for user-facing error messages so we can suggest
|
|
6
|
+
* the correct environment variable name.
|
|
7
|
+
*/
|
|
8
|
+
function detectFramework() {
|
|
9
|
+
if (typeof window !== "undefined" && "__NEXT_DATA__" in window) return "nextjs";
|
|
10
|
+
if (typeof process !== "undefined" && process.env && "__NEXT_RUNTIME" in process.env) return "nextjs";
|
|
11
|
+
try {
|
|
12
|
+
if (import.meta.env?.MODE) return "vite";
|
|
13
|
+
} catch {}
|
|
14
|
+
return "other";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the recommended environment variable name for the detected framework.
|
|
18
|
+
*/
|
|
19
|
+
function getEnvVarName(framework) {
|
|
20
|
+
switch (framework ?? detectFramework()) {
|
|
21
|
+
case "nextjs": return "NEXT_PUBLIC_COSSISTANT_API_KEY";
|
|
22
|
+
case "vite": return "VITE_COSSISTANT_API_KEY";
|
|
23
|
+
default: return "COSSISTANT_API_KEY";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the Cossistant public API key from multiple sources.
|
|
28
|
+
*
|
|
29
|
+
* Priority order:
|
|
30
|
+
* 1. Explicit value passed as argument
|
|
31
|
+
* 2. `process.env.NEXT_PUBLIC_COSSISTANT_API_KEY` (Next.js / CRA / webpack)
|
|
32
|
+
* 3. `process.env.COSSISTANT_API_KEY` (generic Node.js / CRA)
|
|
33
|
+
* 4. `import.meta.env.VITE_COSSISTANT_API_KEY` (Vite)
|
|
34
|
+
*
|
|
35
|
+
* Safe across all environments: Node.js, browser, Vite, Next.js, CRA.
|
|
36
|
+
*/
|
|
37
|
+
function resolvePublicKey(explicit) {
|
|
38
|
+
const trimmed = explicit?.trim();
|
|
39
|
+
if (trimmed) return trimmed;
|
|
40
|
+
if (typeof process !== "undefined" && process.env) try {
|
|
41
|
+
const normalized = (process.env.NEXT_PUBLIC_COSSISTANT_API_KEY || process.env.COSSISTANT_API_KEY)?.trim();
|
|
42
|
+
if (normalized) return normalized;
|
|
43
|
+
} catch {}
|
|
44
|
+
try {
|
|
45
|
+
const normalized = (import.meta.env?.VITE_COSSISTANT_API_KEY)?.trim();
|
|
46
|
+
if (normalized) return normalized;
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { detectFramework, getEnvVarName, resolvePublicKey };
|
|
52
|
+
//# sourceMappingURL=resolve-public-key.js.map
|