@paramms/chat-widget 1.0.44 → 1.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/chatlist.d.ts +2 -1
- package/dist/chatlist.js +137 -115
- package/dist/chatlist.js.map +1 -1
- package/dist/chatlist.styles.d.ts +1 -0
- package/dist/codec.js +191 -157
- package/dist/codec.js.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/e2e.d.ts +21 -2
- package/dist/e2e.js +50 -40
- package/dist/e2e.js.map +1 -1
- package/dist/embed-lite.js +242 -0
- package/dist/embed-lite.js.map +1 -0
- package/dist/embed.d.ts +8 -0
- package/dist/embed.js +87 -78
- package/dist/embed.js.map +1 -1
- package/dist/history.d.ts +7 -4
- package/dist/index.d.ts +20 -0
- package/dist/index.js +632 -723
- package/dist/index.js.map +1 -1
- package/dist/outbox.js +65 -75
- package/dist/outbox.js.map +1 -1
- package/dist/protocol/entities.d.ts +0 -11
- package/dist/protocol/frames.d.ts +1 -17
- package/dist/react.d.ts +13 -5
- package/dist/react.js +225 -211
- package/dist/react.js.map +1 -1
- package/dist/renderer.d.ts +14 -19
- package/dist/renderer.styles.d.ts +1 -0
- package/dist/store.d.ts +1 -5
- package/dist/theme-tokens.d.ts +4 -0
- package/package.json +2 -2
- package/dist/annotations.d.ts +0 -11
package/dist/outbox.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
var u = Object.defineProperty;
|
|
2
|
-
var l = (
|
|
3
|
-
var
|
|
2
|
+
var l = (n, t, s) => t in n ? u(n, t, { enumerable: !0, configurable: !0, writable: !0, value: s }) : n[t] = s;
|
|
3
|
+
var i = (n, t, s) => l(n, typeof t != "symbol" ? t + "" : t, s);
|
|
4
4
|
import { d as p, i as g, e as y } from "./codec.js";
|
|
5
|
-
const I = (
|
|
5
|
+
const I = (n) => n, q = (n) => n, C = (n) => n, x = (n) => n, M = (n) => n, _ = (n) => n, A = (n) => n, O = (n) => n, T = I("anonymous");
|
|
6
6
|
class R {
|
|
7
7
|
constructor(t) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
i(this, "socket", null);
|
|
9
|
+
i(this, "state", "idle");
|
|
10
|
+
i(this, "authed", !1);
|
|
11
|
+
i(this, "everAuthed", !1);
|
|
12
|
+
i(this, "attempt", 0);
|
|
13
|
+
i(this, "outbox", []);
|
|
14
|
+
i(this, "stopped", !1);
|
|
15
|
+
i(this, "timer", null);
|
|
16
|
+
i(this, "refreshing", !1);
|
|
17
17
|
this.opts = t;
|
|
18
18
|
}
|
|
19
19
|
connect() {
|
|
@@ -23,9 +23,9 @@ class R {
|
|
|
23
23
|
const s = (this.opts.socketFactory ?? k)(this.opts.url);
|
|
24
24
|
s.binaryType = "arraybuffer", this.socket = s, s.onopen = () => {
|
|
25
25
|
this.raw({ type: "auth", token: this.opts.token });
|
|
26
|
-
}, s.onmessage = (
|
|
27
|
-
const
|
|
28
|
-
!
|
|
26
|
+
}, s.onmessage = (h) => {
|
|
27
|
+
const a = p(new Uint8Array(h.data));
|
|
28
|
+
!a || g(a) || this.handle(a);
|
|
29
29
|
}, s.onclose = () => this.onClosed(), s.onerror = () => {
|
|
30
30
|
try {
|
|
31
31
|
s.close();
|
|
@@ -57,16 +57,16 @@ class R {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
handle(t) {
|
|
60
|
-
var s, e, o,
|
|
60
|
+
var s, e, o, h;
|
|
61
61
|
if (t.type === "authed" && (this.attempt = 0, this.state = "open", this.authed = !0, this.everAuthed = !0, (e = (s = this.opts).onStatusChange) == null || e.call(s, "open"), this.raw(this.opts.open), this.opts.open.type !== "open" && this.flush()), t.type === "opened" && (this.raw({ type: "sync", conversationId: t.conversation.id, sinceSeq: this.opts.getCursor() }), this.flush(t.conversation.id)), t.type === "error" && b.has(t.code)) {
|
|
62
62
|
if (t.code === "UNAUTHORIZED" && this.opts.refreshToken && !this.refreshing) {
|
|
63
|
-
this.refreshing = !0, (
|
|
63
|
+
this.refreshing = !0, (h = (o = this.opts).onStatusChange) == null || h.call(o, "reconnecting", "Renewing session…"), this.opts.refreshToken().then((a) => {
|
|
64
64
|
var c;
|
|
65
|
-
if (this.refreshing = !1, !
|
|
65
|
+
if (this.refreshing = !1, !a) {
|
|
66
66
|
this.fatal(t);
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
this.opts.token =
|
|
69
|
+
this.opts.token = a;
|
|
70
70
|
try {
|
|
71
71
|
(c = this.socket) == null || c.close();
|
|
72
72
|
} catch {
|
|
@@ -123,59 +123,55 @@ class R {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
onClosed() {
|
|
126
|
-
var o,
|
|
126
|
+
var o, h;
|
|
127
127
|
if (this.authed = !1, this.socket = null, this.stopped) {
|
|
128
128
|
this.state = "closed";
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
this.state = "idle";
|
|
132
132
|
const t = this.opts.backoffBaseMs ?? 500, s = this.opts.backoffMaxMs ?? 15e3, e = Math.min(s, t * 2 ** this.attempt) * (0.5 + Math.random() * 0.5);
|
|
133
|
-
this.attempt++, this.attempt >= 3 && !this.everAuthed && ((
|
|
133
|
+
this.attempt++, this.attempt >= 3 && !this.everAuthed && ((h = (o = this.opts).onStatusChange) == null || h.call(o, "reconnecting", "Can't reach chat — retrying…")), this.timer = setTimeout(() => this.connect(), e);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
const b = /* @__PURE__ */ new Set(["UNAUTHORIZED"]);
|
|
137
|
-
function f(
|
|
138
|
-
switch (
|
|
137
|
+
function f(n) {
|
|
138
|
+
switch (n) {
|
|
139
139
|
case "UNAUTHORIZED":
|
|
140
140
|
return "Chat unavailable — sign-in/token was rejected";
|
|
141
141
|
default:
|
|
142
142
|
return "Chat unavailable";
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
function k(
|
|
146
|
-
return new WebSocket(
|
|
145
|
+
function k(n) {
|
|
146
|
+
return new WebSocket(n);
|
|
147
147
|
}
|
|
148
148
|
class j {
|
|
149
149
|
constructor(t) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/** Co-browsing: shared whiteboard strokes for this conversation. */
|
|
168
|
-
n(this, "annotations", []);
|
|
169
|
-
/** Bumped on any annotation change so the renderer can cheaply detect updates. */
|
|
170
|
-
n(this, "annotationVersion", 0);
|
|
150
|
+
i(this, "conversationId");
|
|
151
|
+
i(this, "state", "");
|
|
152
|
+
i(this, "version", 0);
|
|
153
|
+
i(this, "hasMoreHistory", !1);
|
|
154
|
+
i(this, "lastReadByOthers", 0);
|
|
155
|
+
i(this, "assignedAgentId");
|
|
156
|
+
i(this, "accent");
|
|
157
|
+
i(this, "subject");
|
|
158
|
+
i(this, "name");
|
|
159
|
+
i(this, "e2e", !1);
|
|
160
|
+
i(this, "offline", !1);
|
|
161
|
+
i(this, "offlineMessage", "");
|
|
162
|
+
i(this, "launcherMessage", null);
|
|
163
|
+
i(this, "preChat", null);
|
|
164
|
+
i(this, "whiteLabel", !1);
|
|
165
|
+
i(this, "typing", /* @__PURE__ */ new Set());
|
|
166
|
+
i(this, "online", /* @__PURE__ */ new Set());
|
|
171
167
|
/** Live sentiment of the guest's latest message (agent-side only). */
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
i(this, "sentiment");
|
|
169
|
+
i(this, "sentimentScore");
|
|
170
|
+
i(this, "actions", []);
|
|
171
|
+
i(this, "byId", /* @__PURE__ */ new Map());
|
|
172
|
+
i(this, "keyByClient", /* @__PURE__ */ new Map());
|
|
173
|
+
i(this, "_maxSeq", 0);
|
|
174
|
+
i(this, "_sorted", null);
|
|
179
175
|
this.me = t;
|
|
180
176
|
}
|
|
181
177
|
messages() {
|
|
@@ -208,7 +204,7 @@ class j {
|
|
|
208
204
|
var s;
|
|
209
205
|
switch (t.type) {
|
|
210
206
|
case "opened":
|
|
211
|
-
this.conversationId = t.conversation.id, this.state = t.conversation.state, t.subject && (this.subject = t.subject)
|
|
207
|
+
this.conversationId = t.conversation.id, this.state = t.conversation.state, t.subject && (this.subject = t.subject);
|
|
212
208
|
return;
|
|
213
209
|
case "manifest":
|
|
214
210
|
this.actions = t.actions, this.version = t.version, t.name && (this.name = t.name), (s = t.theme) != null && s.accent && (this.accent = t.theme.accent), t.e2e && (this.e2e = !0), this.offline = t.offline === !0, this.offlineMessage = t.offlineMessage ?? "", t.launcherMessage && (this.launcherMessage = t.launcherMessage), t.whiteLabel && (this.whiteLabel = !0), t.preChat && (this.preChat = t.preChat);
|
|
@@ -220,8 +216,8 @@ class j {
|
|
|
220
216
|
const e = this.keyByClient.get(t.clientMsgId), o = e ? this.byId.get(e) : void 0;
|
|
221
217
|
if (o && e) {
|
|
222
218
|
this.byId.delete(e);
|
|
223
|
-
const
|
|
224
|
-
this.byId.set(t.messageId,
|
|
219
|
+
const h = { ...o, id: t.messageId, seq: t.seq, ts: t.ts, status: "sent" };
|
|
220
|
+
this.byId.set(t.messageId, h), this.keyByClient.set(t.clientMsgId, t.messageId), t.seq > this._maxSeq && (this._maxSeq = t.seq);
|
|
225
221
|
}
|
|
226
222
|
this._sorted = null;
|
|
227
223
|
return;
|
|
@@ -245,8 +241,8 @@ class j {
|
|
|
245
241
|
case "reaction": {
|
|
246
242
|
const e = this.byId.get(t.messageId);
|
|
247
243
|
if (!e) return;
|
|
248
|
-
const o = { ...e.reactions ?? {} },
|
|
249
|
-
t.removed ||
|
|
244
|
+
const o = { ...e.reactions ?? {} }, h = (o[t.emoji] ?? []).filter((a) => a !== t.by);
|
|
245
|
+
t.removed || h.push(t.by), h.length ? o[t.emoji] = h : delete o[t.emoji], this.byId.set(t.messageId, { ...e, reactions: o }), this._sorted = null;
|
|
250
246
|
return;
|
|
251
247
|
}
|
|
252
248
|
case "edited": {
|
|
@@ -274,12 +270,6 @@ class j {
|
|
|
274
270
|
case "error":
|
|
275
271
|
case "pong":
|
|
276
272
|
return;
|
|
277
|
-
case "annotation":
|
|
278
|
-
this.annotations = [...this.annotations, t.stroke], this.annotationVersion++;
|
|
279
|
-
return;
|
|
280
|
-
case "annotation_clear":
|
|
281
|
-
this.annotations = [], this.annotationVersion++;
|
|
282
|
-
return;
|
|
283
273
|
case "sentiment":
|
|
284
274
|
this.sentiment = t.label, this.sentimentScore = t.score;
|
|
285
275
|
return;
|
|
@@ -294,13 +284,13 @@ class j {
|
|
|
294
284
|
markOwnStatus(t, s) {
|
|
295
285
|
const e = r(s);
|
|
296
286
|
let o = !1;
|
|
297
|
-
for (const [
|
|
298
|
-
|
|
287
|
+
for (const [h, a] of this.byId)
|
|
288
|
+
a.senderId !== this.me || a.seq <= 0 || a.seq > t || r(a.status) >= e || (this.byId.set(h, { ...a, status: s }), o = !0);
|
|
299
289
|
o && (this._sorted = null);
|
|
300
290
|
}
|
|
301
291
|
}
|
|
302
|
-
function r(
|
|
303
|
-
switch (
|
|
292
|
+
function r(n) {
|
|
293
|
+
switch (n) {
|
|
304
294
|
case "read":
|
|
305
295
|
return 3;
|
|
306
296
|
case "delivered":
|
|
@@ -312,9 +302,9 @@ function r(i) {
|
|
|
312
302
|
}
|
|
313
303
|
}
|
|
314
304
|
const d = 200, S = 7 * 864e5;
|
|
315
|
-
class
|
|
305
|
+
class m {
|
|
316
306
|
constructor(t) {
|
|
317
|
-
|
|
307
|
+
i(this, "key");
|
|
318
308
|
this.key = `ocw_outbox_${t}`;
|
|
319
309
|
}
|
|
320
310
|
/** All pending items, oldest first, with stale (>7d) entries dropped. */
|
|
@@ -322,7 +312,7 @@ class N {
|
|
|
322
312
|
try {
|
|
323
313
|
const t = localStorage.getItem(this.key);
|
|
324
314
|
if (!t) return [];
|
|
325
|
-
const s = JSON.parse(t), e = Date.now() - S, o = s.filter((
|
|
315
|
+
const s = JSON.parse(t), e = Date.now() - S, o = s.filter((h) => h.ts >= e);
|
|
326
316
|
return o.length !== s.length && this.save(o), o;
|
|
327
317
|
} catch {
|
|
328
318
|
return [];
|
|
@@ -353,15 +343,15 @@ class N {
|
|
|
353
343
|
export {
|
|
354
344
|
T as A,
|
|
355
345
|
j as C,
|
|
356
|
-
|
|
346
|
+
m as P,
|
|
357
347
|
q as a,
|
|
358
348
|
R as b,
|
|
359
|
-
|
|
349
|
+
_ as c,
|
|
360
350
|
O as d,
|
|
361
351
|
C as e,
|
|
362
|
-
|
|
352
|
+
M as f,
|
|
363
353
|
A as g,
|
|
364
|
-
|
|
354
|
+
x as h,
|
|
365
355
|
I as i
|
|
366
356
|
};
|
|
367
357
|
//# sourceMappingURL=outbox.js.map
|
package/dist/outbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbox.js","sources":["../src/protocol/ids.ts","../src/connection.ts","../src/store.ts","../src/outbox.ts"],"sourcesContent":["// Branded identifier types. A plain string can't be passed where a TenantId is\n// expected (and vice-versa), so id-confusion bugs — the kind that caused the\n// `t-1` vs `dev-tenant` mismatch in the previous system — become compile errors.\n\nexport type Brand<T, B extends string> = T & { readonly __brand: B }\n\nexport type TenantId = Brand<string, 'TenantId'>\nexport type UserId = Brand<string, 'UserId'>\nexport type ConversationId = Brand<string, 'ConversationId'>\nexport type SubjectId = Brand<string, 'SubjectId'>\nexport type MessageId = Brand<string, 'MessageId'>\nexport type ActionId = Brand<string, 'ActionId'>\nexport type ProfileId = Brand<string, 'ProfileId'>\nexport type ConnectionId = Brand<string, 'ConnectionId'>\n\nexport const asTenantId = (s: string): TenantId => s as TenantId\nexport const asUserId = (s: string): UserId => s as UserId\nexport const asConversationId = (s: string): ConversationId => s as ConversationId\nexport const asSubjectId = (s: string): SubjectId => s as SubjectId\nexport const asMessageId = (s: string): MessageId => s as MessageId\nexport const asActionId = (s: string): ActionId => s as ActionId\nexport const asProfileId = (s: string): ProfileId => s as ProfileId\nexport const asConnectionId = (s: string): ConnectionId => s as ConnectionId\n\n/** Anonymous guests live under this tenant; they bypass cross-tenant scoping but\n * are still gated by conversation membership. */\nexport const ANONYMOUS_TENANT = asTenantId('anonymous')\n","import {\n encodeFrame, decodeFrame, isClientFrame,\n type ClientFrame, type ServerFrame,\n} from './protocol/index.js'\n\n// Minimal socket surface so tests can inject a fake without a real WebSocket.\nexport interface SocketLike {\n binaryType: string\n send(data: Uint8Array): void\n close(): void\n onopen: (() => void) | null\n onclose: (() => void) | null\n onerror: (() => void) | null\n onmessage: ((ev: { data: ArrayBuffer }) => void) | null\n}\nexport type SocketFactory = (url: string) => SocketLike\n\nexport interface ConnectionOptions {\n url: string\n token: string\n /** Authenticated embeds: called when the server rejects the token\n * (typically an expired signed JWT). Return a freshly minted token to\n * resume seamlessly, or null to give up (shows the fatal error). */\n refreshToken?: () => Promise<string | null>\n open: ClientFrame // frame sent right after auth (e.g. open a conversation, or subscribe_inbox)\n onFrame: (frame: ServerFrame) => void\n getCursor: () => number // highest seq seen (for sync on reconnect)\n onStatusChange?: (status: 'connecting' | 'open' | 'reconnecting' | 'error', message?: string) => void\n socketFactory?: SocketFactory\n backoffBaseMs?: number\n backoffMaxMs?: number\n maxOutbox?: number\n}\n\ntype State = 'idle' | 'connecting' | 'open' | 'closed'\n\nexport class ConnectionManager {\n private socket: SocketLike | null = null\n private state: State = 'idle'\n private authed = false\n private everAuthed = false\n private attempt = 0\n private outbox: ClientFrame[] = []\n private stopped = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor(private readonly opts: ConnectionOptions) {}\n\n connect(): void {\n if (this.state === 'connecting' || this.state === 'open') return\n this.stopped = false\n this.state = 'connecting'\n this.authed = false\n this.opts.onStatusChange?.(this.attempt > 0 ? 'reconnecting' : 'connecting')\n const make = this.opts.socketFactory ?? defaultFactory\n const sock = make(this.opts.url)\n sock.binaryType = 'arraybuffer'\n this.socket = sock\n\n sock.onopen = () => {\n // Don't reset attempt here — reset only after successful auth ('authed').\n // A connection that opens but fails during auth (bad token, server restart)\n // should still back off, not immediately retry at base delay.\n this.raw({ type: 'auth', token: this.opts.token })\n }\n sock.onmessage = (ev) => {\n const frame = decodeFrame(new Uint8Array(ev.data))\n if (!frame || isClientFrame(frame)) return // ignore non-server frames\n this.handle(frame)\n }\n sock.onclose = () => this.onClosed()\n sock.onerror = () => { try { sock.close() } catch { /* */ } }\n }\n\n /** Queue a frame; sent immediately if open, else flushed on (re)connect.\n * The outbox is bounded so a prolonged outage can't grow memory without limit\n * — oldest queued frames are dropped past the cap. */\n send(frame: ClientFrame): void {\n if (this.state === 'open' && this.authed) { this.raw(frame); return }\n // Evicts the oldest half when full to amortise the O(n) cost of overflow.\n this.queue(frame)\n }\n\n /** How many frames are waiting to go out. Useful for a host that wants to\n * show \"message pending\" state, and for asserting the outbox stays bounded. */\n pendingCount(): number { return this.outbox.length }\n\n close(): void {\n this.stopped = true\n if (this.timer) clearTimeout(this.timer)\n this.state = 'closed'\n try { this.socket?.close() } catch { /* */ }\n }\n\n private handle(frame: ServerFrame): void {\n if (frame.type === 'authed') {\n this.attempt = 0 // reset backoff only after a fully successful auth\n this.state = 'open'\n this.authed = true; this.everAuthed = true\n this.opts.onStatusChange?.('open')\n // Open/resolve the conversation. The catch-up `sync` is sent on 'opened'\n // (below), i.e. only after the server has joined us to the room — sending\n // it here would race the async open and be rejected as \"not joined\".\n this.raw(this.opts.open)\n // The QUEUED frames need exactly the same treatment, and used to not get\n // it: flushing here fired them straight after `open`, before the server\n // had joined us, so the engine answered FORBIDDEN 'Open the conversation\n // first' and the message was gone — no ack, no requeue. This is the\n // reconnect-drops-your-message bug. Frames now wait for 'opened' below.\n //\n // EXCEPT when the open frame doesn't produce a join at all: an inbox\n // subscription (`subscribe_inbox`) never gets an 'opened' reply, so\n // waiting for one would strand the queue forever. Nothing needs joining\n // in that case, so flushing immediately is both safe and required.\n if (this.opts.open.type !== 'open') this.flush()\n }\n // 'opened' confirms we're joined — now catch up from our cursor, then\n // release anything queued while we were disconnected.\n if (frame.type === 'opened') {\n this.raw({ type: 'sync', conversationId: frame.conversation.id, sinceSeq: this.opts.getCursor() })\n this.flush(frame.conversation.id)\n }\n // A CONNECTION-FATAL error (bad or rejected token, closed or missing\n // chatroom) will never succeed on retry — stop the reconnect loop and report\n // a clear reason instead of spinning on \"connecting…\" forever. Per-frame\n // errors (rate limit, one bad message) are NOT fatal and fall through.\n if (frame.type === 'error' && FATAL_ERRORS.has(frame.code)) {\n // Token refresh (authenticated embeds): a signed JWT expiring mid-session\n // used to be a dead end — the widget showed a fatal error until reload.\n // If the host supplied refreshToken, ask it to mint a fresh one and\n // reconnect. One in-flight attempt at a time; a refresh that returns\n // null/throws (user logged out, backend down) falls through to fatal.\n if (frame.code === 'UNAUTHORIZED' && this.opts.refreshToken && !this.refreshing) {\n this.refreshing = true\n this.opts.onStatusChange?.('reconnecting', 'Renewing session…')\n void this.opts.refreshToken()\n .then((fresh) => {\n this.refreshing = false\n if (!fresh) { this.fatal(frame); return }\n this.opts.token = fresh\n try { this.socket?.close() } catch { /* */ }\n // onClosed schedules the reconnect, which re-auths with the new token.\n })\n .catch(() => { this.refreshing = false; this.fatal(frame) })\n return\n }\n this.fatal(frame)\n return\n }\n this.opts.onFrame(frame)\n }\n\n private refreshing = false\n\n private fatal(frame: Extract<ServerFrame, { type: 'error' }>): void {\n this.stopped = true\n try { this.socket?.close() } catch { /* */ }\n this.state = 'closed'\n this.opts.onStatusChange?.('error', friendlyError(frame.code))\n this.opts.onFrame(frame)\n }\n\n /** Release queued frames. When called from 'opened' we know the canonical\n * conversation id the server just resolved us to, and queued `send` frames\n * are retargeted to it. A manager only ever opens ONE conversation (its\n * `opts.open`), so every queued send belongs to that thread by\n * construction — but the id it was queued with can be STALE (queued against\n * the previous session's conversation before a reconnect). Retargeting is a\n * no-op in the normal case and rescues the message in the stale one. */\n private flush(conversationId?: string): void {\n const pending = this.outbox\n this.outbox = []\n for (const f of pending) {\n this.raw(\n conversationId && f.type === 'send' && f.conversationId !== conversationId\n ? { ...f, conversationId: conversationId as typeof f.conversationId }\n : f,\n )\n }\n }\n\n /** Bounded enqueue — the cap lives here so EVERY path that queues respects\n * it (a failed `raw` used to push straight onto the array, bypassing it). */\n private queue(frame: ClientFrame): void {\n const cap = this.opts.maxOutbox ?? 1_000\n if (this.outbox.length >= cap) {\n this.outbox = this.outbox.slice(this.outbox.length - (cap >> 1))\n }\n this.outbox.push(frame)\n }\n\n private raw(frame: ClientFrame): void {\n // `this.socket?.send(...)` silently DROPPED the frame whenever the socket\n // was null (post-disconnect, pre-reconnect): optional chaining short-\n // circuits, so nothing throws and the catch that re-queues never runs.\n // A null socket is exactly when a frame most needs to be kept.\n if (!this.socket) { this.queue(frame); return }\n try { this.socket.send(encodeFrame(frame)) } catch { this.queue(frame) }\n }\n\n private onClosed(): void {\n this.authed = false\n this.socket = null\n if (this.stopped) { this.state = 'closed'; return }\n this.state = 'idle'\n // Exponential backoff with jitter; reconnect re-auths, re-opens, re-syncs.\n const base = this.opts.backoffBaseMs ?? 500\n const max = this.opts.backoffMaxMs ?? 15_000\n const delay = Math.min(max, base * 2 ** this.attempt) * (0.5 + Math.random() * 0.5)\n this.attempt++\n // If we've never once connected after several tries, the relay is likely\n // unreachable (wrong URL, server down, blocked) — say so, but keep retrying.\n if (this.attempt >= 3 && !this.everAuthed) {\n this.opts.onStatusChange?.('reconnecting', \"Can't reach chat — retrying…\")\n }\n this.timer = setTimeout(() => this.connect(), delay)\n }\n}\n\n/** Connection-fatal error codes — retrying can't fix these. ONLY auth-handshake\n * failure qualifies: FORBIDDEN / NOT_FOUND are per-REQUEST errors (a stale\n * reference, one permission check) and must NOT tear down the whole socket. */\nconst FATAL_ERRORS = new Set(['UNAUTHORIZED'])\nfunction friendlyError(code: string): string {\n switch (code) {\n case 'UNAUTHORIZED': return 'Chat unavailable — sign-in/token was rejected'\n default: return 'Chat unavailable'\n }\n}\n\nfunction defaultFactory(url: string): SocketLike {\n return new WebSocket(url) as unknown as SocketLike\n}\n","import type {\n ServerFrame, Message, ManifestAction, MessageContent,\n ConversationId, MessageId, UserId, Subject, AnnotationStroke,\n} from './protocol/index.js'\n\nexport type SendStatus = 'pending' | 'sent' | 'delivered' | 'read'\n\nexport interface RenderMessage extends Message {\n clientMsgId?: string\n status?: SendStatus\n}\n\n/** Pure, DOM-free conversation state. Feed it ServerFrames (and local optimistic\n * sends); read an ordered, de-duplicated view out. Ordering is by `seq`; the\n * same message arriving twice (live + sync on reconnect) is collapsed by id —\n * the structural fix for the old duplicate-bubble bug. */\nexport class ChatStore {\n conversationId?: ConversationId\n state = ''\n version = 0\n hasMoreHistory = false\n lastReadByOthers = 0\n assignedAgentId: UserId | undefined\n accent: string | undefined\n subject: Subject | undefined\n name: string | undefined\n e2e = false\n offline = false\n offlineMessage = ''\n launcherMessage: { title: string; subtitle?: string } | null = null\n preChat: import('./protocol/frames.js').PreChatConfig | null = null\n whiteLabel = false\n readonly typing = new Set<string>()\n readonly online = new Set<string>()\n /** Co-browsing: shared whiteboard strokes for this conversation. */\n annotations: AnnotationStroke[] = []\n /** Bumped on any annotation change so the renderer can cheaply detect updates. */\n annotationVersion = 0\n /** Live sentiment of the guest's latest message (agent-side only). */\n sentiment: 'positive' | 'neutral' | 'frustrated' | undefined\n sentimentScore: number | undefined\n\n private actions: ManifestAction[] = []\n private readonly byId = new Map<string, RenderMessage>()\n private readonly keyByClient = new Map<string, string>()\n private _maxSeq = 0\n private _sorted: RenderMessage[] | null = null\n\n constructor(private readonly me: UserId) {}\n\n messages(): RenderMessage[] {\n if (!this._sorted) {\n this._sorted = [...this.byId.values()].sort((a, b) => {\n const ap = a.status === 'pending', bp = b.status === 'pending'\n if (ap !== bp) return ap ? 1 : -1\n if (ap && bp) return a.ts - b.ts\n return a.seq - b.seq\n })\n }\n return this._sorted\n }\n\n visibleActions(): ManifestAction[] {\n return this.actions.filter(a => !a.availableInStates || a.availableInStates.includes(this.state))\n }\n\n highestSeq(): number { return this._maxSeq }\n\n addOptimistic(clientMsgId: string, content: MessageContent): RenderMessage {\n const msg: RenderMessage = {\n id: clientMsgId as MessageId, conversationId: this.conversationId as ConversationId,\n seq: 0, senderId: this.me, senderRole: 'guest', content, ts: Date.now(),\n clientMsgId, status: 'pending',\n }\n this.byId.set(clientMsgId, msg)\n this.keyByClient.set(clientMsgId, clientMsgId)\n this._sorted = null\n return msg\n }\n\n apply(frame: ServerFrame): void {\n switch (frame.type) {\n case 'opened':\n this.conversationId = frame.conversation.id\n this.state = frame.conversation.state\n if (frame.subject) this.subject = frame.subject\n if (frame.annotations) { this.annotations = frame.annotations; this.annotationVersion++ }\n return\n case 'manifest':\n this.actions = frame.actions\n this.version = frame.version\n if (frame.name) this.name = frame.name\n if (frame.theme?.accent) this.accent = frame.theme.accent\n if (frame.e2e) this.e2e = true\n // Track the manifest EXACTLY: a sticky `offline` (only ever set, never\n // cleared) kept the widget in away-mode for the whole session once a\n // single manifest said so — which used to hide the composer entirely.\n this.offline = frame.offline === true\n this.offlineMessage = frame.offlineMessage ?? ''\n if (frame.launcherMessage) this.launcherMessage = frame.launcherMessage\n if (frame.whiteLabel) this.whiteLabel = true\n if (frame.preChat) this.preChat = frame.preChat\n return\n case 'message':\n this.upsert({ ...frame.message })\n return\n case 'ack': {\n const key = this.keyByClient.get(frame.clientMsgId)\n const msg = key ? this.byId.get(key) : undefined\n if (msg && key) {\n this.byId.delete(key)\n const confirmed: RenderMessage = { ...msg, id: frame.messageId, seq: frame.seq, ts: frame.ts, status: 'sent' }\n this.byId.set(frame.messageId, confirmed)\n this.keyByClient.set(frame.clientMsgId, frame.messageId)\n if (frame.seq > this._maxSeq) this._maxSeq = frame.seq\n }\n this._sorted = null\n return\n }\n case 'delivered':\n this.markOwnStatus(frame.seq, 'delivered')\n return\n case 'read':\n if (frame.by !== this.me) {\n this.lastReadByOthers = Math.max(this.lastReadByOthers, frame.seq)\n this.markOwnStatus(frame.seq, 'read')\n }\n return\n case 'sync':\n for (const m of frame.messages) this.upsert({ ...m })\n return\n case 'history':\n for (const m of frame.messages) this.upsert({ ...m })\n this.hasMoreHistory = frame.hasMore\n return\n case 'typing':\n if (frame.userId !== this.me) {\n if (frame.isTyping) this.typing.add(frame.userId)\n else this.typing.delete(frame.userId)\n }\n return\n case 'reaction': {\n const m = this.byId.get(frame.messageId)\n if (!m) return\n const reactions: Record<string, UserId[]> = { ...(m.reactions ?? {}) }\n const users = (reactions[frame.emoji] ?? []).filter(u => u !== frame.by)\n if (!frame.removed) users.push(frame.by)\n if (users.length) reactions[frame.emoji] = users; else delete reactions[frame.emoji]\n this.byId.set(frame.messageId, { ...m, reactions })\n this._sorted = null\n return\n }\n case 'edited': {\n const m = this.byId.get(frame.messageId)\n if (m) { this.byId.set(frame.messageId, { ...m, content: frame.content, editedAt: frame.editedAt }); this._sorted = null }\n return\n }\n case 'deleted': {\n const m = this.byId.get(frame.messageId)\n if (m) { this.byId.set(frame.messageId, { ...m, deletedAt: frame.ts }); this._sorted = null }\n return\n }\n case 'state':\n this.state = frame.state\n return\n case 'assigned':\n this.assignedAgentId = frame.agentId ?? undefined\n return\n case 'presence':\n if (frame.status === 'online') this.online.add(frame.userId)\n else this.online.delete(frame.userId)\n return\n case 'subjectState':\n case 'invoked':\n case 'authed':\n case 'error':\n case 'pong':\n return\n case 'annotation':\n this.annotations = [...this.annotations, frame.stroke]\n this.annotationVersion++\n return\n case 'annotation_clear':\n this.annotations = []\n this.annotationVersion++\n return\n case 'sentiment':\n this.sentiment = frame.label\n this.sentimentScore = frame.score\n return\n default:\n return\n }\n }\n\n private upsert(msg: RenderMessage): void {\n const existing = this.byId.get(msg.id)\n this.byId.set(msg.id, existing ? { ...existing, ...msg } : msg)\n if (msg.seq > this._maxSeq) this._maxSeq = msg.seq\n this._sorted = null\n }\n\n private markOwnStatus(uptoSeq: number, status: SendStatus): void {\n const targetRank = rank(status)\n let changed = false\n for (const [k, m] of this.byId) {\n if (m.senderId !== this.me || m.seq <= 0 || m.seq > uptoSeq) continue\n if (rank(m.status) >= targetRank) continue // already at or above target — skip\n this.byId.set(k, { ...m, status })\n changed = true\n }\n if (changed) this._sorted = null\n }\n}\nfunction rank(s: SendStatus | undefined): number {\n switch (s) { case 'read': return 3; case 'delivered': return 2; case 'sent': return 1; default: return 0 }\n}\n","import type { MessageContent } from './protocol/index.js'\n\nexport interface OutboxItem {\n clientMsgId: string\n content: MessageContent\n ts: number\n}\n\nconst MAX_ITEMS = 200 // cap so a long outage can't grow storage unboundedly\nconst MAX_AGE_MS = 7 * 86_400_000 // drop anything older than 7 days on load\n\n/** Persists not-yet-acknowledged outgoing messages to localStorage, keyed by\n * guest token, so a page reload during a connectivity drop doesn't silently\n * lose what the user typed (the \"WhatsApp\" guarantee: your message is queued\n * until it's confirmed sent, even across app restarts). */\nexport class PersistentOutbox {\n private readonly key: string\n\n constructor(token: string) {\n this.key = `ocw_outbox_${token}`\n }\n\n /** All pending items, oldest first, with stale (>7d) entries dropped. */\n load(): OutboxItem[] {\n try {\n const raw = localStorage.getItem(this.key)\n if (!raw) return []\n const items = JSON.parse(raw) as OutboxItem[]\n const cutoff = Date.now() - MAX_AGE_MS\n const fresh = items.filter(i => i.ts >= cutoff)\n if (fresh.length !== items.length) this.save(fresh)\n return fresh\n } catch {\n return []\n }\n }\n\n add(item: OutboxItem): void {\n try {\n const items = this.load()\n items.push(item)\n // Evict oldest when full — matches the in-memory ConnectionManager outbox policy.\n this.save(items.length > MAX_ITEMS ? items.slice(items.length - MAX_ITEMS) : items)\n } catch { /* localStorage unavailable (private mode, quota) — best-effort only */ }\n }\n\n /** Remove an item once it's been acknowledged by the server. */\n remove(clientMsgId: string): void {\n try {\n const items = this.load().filter(i => i.clientMsgId !== clientMsgId)\n this.save(items)\n } catch { /* best-effort */ }\n }\n\n private save(items: OutboxItem[]): void {\n try { localStorage.setItem(this.key, JSON.stringify(items)) } catch { /* quota exceeded — drop silently */ }\n }\n}\n"],"names":["asTenantId","s","asUserId","asConversationId","asSubjectId","asMessageId","asActionId","asProfileId","asConnectionId","ANONYMOUS_TENANT","ConnectionManager","opts","__publicField","_b","_a","sock","defaultFactory","ev","frame","decodeFrame","isClientFrame","FATAL_ERRORS","_d","_c","fresh","friendlyError","conversationId","pending","f","cap","encodeFrame","base","max","delay","code","url","ChatStore","me","a","b","ap","bp","clientMsgId","content","msg","key","confirmed","m","reactions","users","u","existing","uptoSeq","status","targetRank","rank","changed","k","MAX_ITEMS","MAX_AGE_MS","PersistentOutbox","token","raw","items","cutoff","i","item"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"outbox.js","sources":["../src/protocol/ids.ts","../src/connection.ts","../src/store.ts","../src/outbox.ts"],"sourcesContent":["// Branded identifier types. A plain string can't be passed where a TenantId is\n// expected (and vice-versa), so id-confusion bugs — the kind that caused the\n// `t-1` vs `dev-tenant` mismatch in the previous system — become compile errors.\n\nexport type Brand<T, B extends string> = T & { readonly __brand: B }\n\nexport type TenantId = Brand<string, 'TenantId'>\nexport type UserId = Brand<string, 'UserId'>\nexport type ConversationId = Brand<string, 'ConversationId'>\nexport type SubjectId = Brand<string, 'SubjectId'>\nexport type MessageId = Brand<string, 'MessageId'>\nexport type ActionId = Brand<string, 'ActionId'>\nexport type ProfileId = Brand<string, 'ProfileId'>\nexport type ConnectionId = Brand<string, 'ConnectionId'>\n\nexport const asTenantId = (s: string): TenantId => s as TenantId\nexport const asUserId = (s: string): UserId => s as UserId\nexport const asConversationId = (s: string): ConversationId => s as ConversationId\nexport const asSubjectId = (s: string): SubjectId => s as SubjectId\nexport const asMessageId = (s: string): MessageId => s as MessageId\nexport const asActionId = (s: string): ActionId => s as ActionId\nexport const asProfileId = (s: string): ProfileId => s as ProfileId\nexport const asConnectionId = (s: string): ConnectionId => s as ConnectionId\n\n/** Anonymous guests live under this tenant; they bypass cross-tenant scoping but\n * are still gated by conversation membership. */\nexport const ANONYMOUS_TENANT = asTenantId('anonymous')\n","import {\n encodeFrame, decodeFrame, isClientFrame,\n type ClientFrame, type ServerFrame,\n} from './protocol/index.js'\n\n// Minimal socket surface so tests can inject a fake without a real WebSocket.\nexport interface SocketLike {\n binaryType: string\n send(data: Uint8Array): void\n close(): void\n onopen: (() => void) | null\n onclose: (() => void) | null\n onerror: (() => void) | null\n onmessage: ((ev: { data: ArrayBuffer }) => void) | null\n}\nexport type SocketFactory = (url: string) => SocketLike\n\nexport interface ConnectionOptions {\n url: string\n token: string\n /** Authenticated embeds: called when the server rejects the token\n * (typically an expired signed JWT). Return a freshly minted token to\n * resume seamlessly, or null to give up (shows the fatal error). */\n refreshToken?: () => Promise<string | null>\n open: ClientFrame // frame sent right after auth (e.g. open a conversation, or subscribe_inbox)\n onFrame: (frame: ServerFrame) => void\n getCursor: () => number // highest seq seen (for sync on reconnect)\n onStatusChange?: (status: 'connecting' | 'open' | 'reconnecting' | 'error', message?: string) => void\n socketFactory?: SocketFactory\n backoffBaseMs?: number\n backoffMaxMs?: number\n maxOutbox?: number\n}\n\ntype State = 'idle' | 'connecting' | 'open' | 'closed'\n\nexport class ConnectionManager {\n private socket: SocketLike | null = null\n private state: State = 'idle'\n private authed = false\n private everAuthed = false\n private attempt = 0\n private outbox: ClientFrame[] = []\n private stopped = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor(private readonly opts: ConnectionOptions) {}\n\n connect(): void {\n if (this.state === 'connecting' || this.state === 'open') return\n this.stopped = false\n this.state = 'connecting'\n this.authed = false\n this.opts.onStatusChange?.(this.attempt > 0 ? 'reconnecting' : 'connecting')\n const make = this.opts.socketFactory ?? defaultFactory\n const sock = make(this.opts.url)\n sock.binaryType = 'arraybuffer'\n this.socket = sock\n\n sock.onopen = () => {\n // Don't reset attempt here — reset only after successful auth ('authed').\n // A connection that opens but fails during auth (bad token, server restart)\n // should still back off, not immediately retry at base delay.\n this.raw({ type: 'auth', token: this.opts.token })\n }\n sock.onmessage = (ev) => {\n const frame = decodeFrame(new Uint8Array(ev.data))\n if (!frame || isClientFrame(frame)) return // ignore non-server frames\n this.handle(frame)\n }\n sock.onclose = () => this.onClosed()\n sock.onerror = () => { try { sock.close() } catch { /* */ } }\n }\n\n /** Queue a frame; sent immediately if open, else flushed on (re)connect.\n * The outbox is bounded so a prolonged outage can't grow memory without limit\n * — oldest queued frames are dropped past the cap. */\n send(frame: ClientFrame): void {\n if (this.state === 'open' && this.authed) { this.raw(frame); return }\n // Evicts the oldest half when full to amortise the O(n) cost of overflow.\n this.queue(frame)\n }\n\n /** How many frames are waiting to go out. Useful for a host that wants to\n * show \"message pending\" state, and for asserting the outbox stays bounded. */\n pendingCount(): number { return this.outbox.length }\n\n close(): void {\n this.stopped = true\n if (this.timer) clearTimeout(this.timer)\n this.state = 'closed'\n try { this.socket?.close() } catch { /* */ }\n }\n\n private handle(frame: ServerFrame): void {\n if (frame.type === 'authed') {\n this.attempt = 0 // reset backoff only after a fully successful auth\n this.state = 'open'\n this.authed = true; this.everAuthed = true\n this.opts.onStatusChange?.('open')\n // Open/resolve the conversation. The catch-up `sync` is sent on 'opened'\n // (below), i.e. only after the server has joined us to the room — sending\n // it here would race the async open and be rejected as \"not joined\".\n this.raw(this.opts.open)\n // The QUEUED frames need exactly the same treatment, and used to not get\n // it: flushing here fired them straight after `open`, before the server\n // had joined us, so the engine answered FORBIDDEN 'Open the conversation\n // first' and the message was gone — no ack, no requeue. This is the\n // reconnect-drops-your-message bug. Frames now wait for 'opened' below.\n //\n // EXCEPT when the open frame doesn't produce a join at all: an inbox\n // subscription (`subscribe_inbox`) never gets an 'opened' reply, so\n // waiting for one would strand the queue forever. Nothing needs joining\n // in that case, so flushing immediately is both safe and required.\n if (this.opts.open.type !== 'open') this.flush()\n }\n // 'opened' confirms we're joined — now catch up from our cursor, then\n // release anything queued while we were disconnected.\n if (frame.type === 'opened') {\n this.raw({ type: 'sync', conversationId: frame.conversation.id, sinceSeq: this.opts.getCursor() })\n this.flush(frame.conversation.id)\n }\n // A CONNECTION-FATAL error (bad or rejected token, closed or missing\n // chatroom) will never succeed on retry — stop the reconnect loop and report\n // a clear reason instead of spinning on \"connecting…\" forever. Per-frame\n // errors (rate limit, one bad message) are NOT fatal and fall through.\n if (frame.type === 'error' && FATAL_ERRORS.has(frame.code)) {\n // Token refresh (authenticated embeds): a signed JWT expiring mid-session\n // used to be a dead end — the widget showed a fatal error until reload.\n // If the host supplied refreshToken, ask it to mint a fresh one and\n // reconnect. One in-flight attempt at a time; a refresh that returns\n // null/throws (user logged out, backend down) falls through to fatal.\n if (frame.code === 'UNAUTHORIZED' && this.opts.refreshToken && !this.refreshing) {\n this.refreshing = true\n this.opts.onStatusChange?.('reconnecting', 'Renewing session…')\n void this.opts.refreshToken()\n .then((fresh) => {\n this.refreshing = false\n if (!fresh) { this.fatal(frame); return }\n this.opts.token = fresh\n try { this.socket?.close() } catch { /* */ }\n // onClosed schedules the reconnect, which re-auths with the new token.\n })\n .catch(() => { this.refreshing = false; this.fatal(frame) })\n return\n }\n this.fatal(frame)\n return\n }\n this.opts.onFrame(frame)\n }\n\n private refreshing = false\n\n private fatal(frame: Extract<ServerFrame, { type: 'error' }>): void {\n this.stopped = true\n try { this.socket?.close() } catch { /* */ }\n this.state = 'closed'\n this.opts.onStatusChange?.('error', friendlyError(frame.code))\n this.opts.onFrame(frame)\n }\n\n /** Release queued frames. When called from 'opened' we know the canonical\n * conversation id the server just resolved us to, and queued `send` frames\n * are retargeted to it. A manager only ever opens ONE conversation (its\n * `opts.open`), so every queued send belongs to that thread by\n * construction — but the id it was queued with can be STALE (queued against\n * the previous session's conversation before a reconnect). Retargeting is a\n * no-op in the normal case and rescues the message in the stale one. */\n private flush(conversationId?: string): void {\n const pending = this.outbox\n this.outbox = []\n for (const f of pending) {\n this.raw(\n conversationId && f.type === 'send' && f.conversationId !== conversationId\n ? { ...f, conversationId: conversationId as typeof f.conversationId }\n : f,\n )\n }\n }\n\n /** Bounded enqueue — the cap lives here so EVERY path that queues respects\n * it (a failed `raw` used to push straight onto the array, bypassing it). */\n private queue(frame: ClientFrame): void {\n const cap = this.opts.maxOutbox ?? 1_000\n if (this.outbox.length >= cap) {\n this.outbox = this.outbox.slice(this.outbox.length - (cap >> 1))\n }\n this.outbox.push(frame)\n }\n\n private raw(frame: ClientFrame): void {\n // `this.socket?.send(...)` silently DROPPED the frame whenever the socket\n // was null (post-disconnect, pre-reconnect): optional chaining short-\n // circuits, so nothing throws and the catch that re-queues never runs.\n // A null socket is exactly when a frame most needs to be kept.\n if (!this.socket) { this.queue(frame); return }\n try { this.socket.send(encodeFrame(frame)) } catch { this.queue(frame) }\n }\n\n private onClosed(): void {\n this.authed = false\n this.socket = null\n if (this.stopped) { this.state = 'closed'; return }\n this.state = 'idle'\n // Exponential backoff with jitter; reconnect re-auths, re-opens, re-syncs.\n const base = this.opts.backoffBaseMs ?? 500\n const max = this.opts.backoffMaxMs ?? 15_000\n const delay = Math.min(max, base * 2 ** this.attempt) * (0.5 + Math.random() * 0.5)\n this.attempt++\n // If we've never once connected after several tries, the relay is likely\n // unreachable (wrong URL, server down, blocked) — say so, but keep retrying.\n if (this.attempt >= 3 && !this.everAuthed) {\n this.opts.onStatusChange?.('reconnecting', \"Can't reach chat — retrying…\")\n }\n this.timer = setTimeout(() => this.connect(), delay)\n }\n}\n\n/** Connection-fatal error codes — retrying can't fix these. ONLY auth-handshake\n * failure qualifies: FORBIDDEN / NOT_FOUND are per-REQUEST errors (a stale\n * reference, one permission check) and must NOT tear down the whole socket. */\nconst FATAL_ERRORS = new Set(['UNAUTHORIZED'])\nfunction friendlyError(code: string): string {\n switch (code) {\n case 'UNAUTHORIZED': return 'Chat unavailable — sign-in/token was rejected'\n default: return 'Chat unavailable'\n }\n}\n\nfunction defaultFactory(url: string): SocketLike {\n return new WebSocket(url) as unknown as SocketLike\n}\n","import type {\n ServerFrame, Message, ManifestAction, MessageContent,\n ConversationId, MessageId, UserId, Subject,\n} from './protocol/index.js'\n\nexport type SendStatus = 'pending' | 'sent' | 'delivered' | 'read'\n\nexport interface RenderMessage extends Message {\n clientMsgId?: string\n status?: SendStatus\n}\n\n/** Pure, DOM-free conversation state. Feed it ServerFrames (and local optimistic\n * sends); read an ordered, de-duplicated view out. Ordering is by `seq`; the\n * same message arriving twice (live + sync on reconnect) is collapsed by id —\n * the structural fix for the old duplicate-bubble bug. */\nexport class ChatStore {\n conversationId?: ConversationId\n state = ''\n version = 0\n hasMoreHistory = false\n lastReadByOthers = 0\n assignedAgentId: UserId | undefined\n accent: string | undefined\n subject: Subject | undefined\n name: string | undefined\n e2e = false\n offline = false\n offlineMessage = ''\n launcherMessage: { title: string; subtitle?: string } | null = null\n preChat: import('./protocol/frames.js').PreChatConfig | null = null\n whiteLabel = false\n readonly typing = new Set<string>()\n readonly online = new Set<string>()\n /** Live sentiment of the guest's latest message (agent-side only). */\n sentiment: 'positive' | 'neutral' | 'frustrated' | undefined\n sentimentScore: number | undefined\n\n private actions: ManifestAction[] = []\n private readonly byId = new Map<string, RenderMessage>()\n private readonly keyByClient = new Map<string, string>()\n private _maxSeq = 0\n private _sorted: RenderMessage[] | null = null\n\n constructor(private readonly me: UserId) {}\n\n messages(): RenderMessage[] {\n if (!this._sorted) {\n this._sorted = [...this.byId.values()].sort((a, b) => {\n const ap = a.status === 'pending', bp = b.status === 'pending'\n if (ap !== bp) return ap ? 1 : -1\n if (ap && bp) return a.ts - b.ts\n return a.seq - b.seq\n })\n }\n return this._sorted\n }\n\n visibleActions(): ManifestAction[] {\n return this.actions.filter(a => !a.availableInStates || a.availableInStates.includes(this.state))\n }\n\n highestSeq(): number { return this._maxSeq }\n\n addOptimistic(clientMsgId: string, content: MessageContent): RenderMessage {\n const msg: RenderMessage = {\n id: clientMsgId as MessageId, conversationId: this.conversationId as ConversationId,\n seq: 0, senderId: this.me, senderRole: 'guest', content, ts: Date.now(),\n clientMsgId, status: 'pending',\n }\n this.byId.set(clientMsgId, msg)\n this.keyByClient.set(clientMsgId, clientMsgId)\n this._sorted = null\n return msg\n }\n\n apply(frame: ServerFrame): void {\n switch (frame.type) {\n case 'opened':\n this.conversationId = frame.conversation.id\n this.state = frame.conversation.state\n if (frame.subject) this.subject = frame.subject\n return\n case 'manifest':\n this.actions = frame.actions\n this.version = frame.version\n if (frame.name) this.name = frame.name\n if (frame.theme?.accent) this.accent = frame.theme.accent\n if (frame.e2e) this.e2e = true\n // Track the manifest EXACTLY: a sticky `offline` (only ever set, never\n // cleared) kept the widget in away-mode for the whole session once a\n // single manifest said so — which used to hide the composer entirely.\n this.offline = frame.offline === true\n this.offlineMessage = frame.offlineMessage ?? ''\n if (frame.launcherMessage) this.launcherMessage = frame.launcherMessage\n if (frame.whiteLabel) this.whiteLabel = true\n if (frame.preChat) this.preChat = frame.preChat\n return\n case 'message':\n this.upsert({ ...frame.message })\n return\n case 'ack': {\n const key = this.keyByClient.get(frame.clientMsgId)\n const msg = key ? this.byId.get(key) : undefined\n if (msg && key) {\n this.byId.delete(key)\n const confirmed: RenderMessage = { ...msg, id: frame.messageId, seq: frame.seq, ts: frame.ts, status: 'sent' }\n this.byId.set(frame.messageId, confirmed)\n this.keyByClient.set(frame.clientMsgId, frame.messageId)\n if (frame.seq > this._maxSeq) this._maxSeq = frame.seq\n }\n this._sorted = null\n return\n }\n case 'delivered':\n this.markOwnStatus(frame.seq, 'delivered')\n return\n case 'read':\n if (frame.by !== this.me) {\n this.lastReadByOthers = Math.max(this.lastReadByOthers, frame.seq)\n this.markOwnStatus(frame.seq, 'read')\n }\n return\n case 'sync':\n for (const m of frame.messages) this.upsert({ ...m })\n return\n case 'history':\n for (const m of frame.messages) this.upsert({ ...m })\n this.hasMoreHistory = frame.hasMore\n return\n case 'typing':\n if (frame.userId !== this.me) {\n if (frame.isTyping) this.typing.add(frame.userId)\n else this.typing.delete(frame.userId)\n }\n return\n case 'reaction': {\n const m = this.byId.get(frame.messageId)\n if (!m) return\n const reactions: Record<string, UserId[]> = { ...(m.reactions ?? {}) }\n const users = (reactions[frame.emoji] ?? []).filter(u => u !== frame.by)\n if (!frame.removed) users.push(frame.by)\n if (users.length) reactions[frame.emoji] = users; else delete reactions[frame.emoji]\n this.byId.set(frame.messageId, { ...m, reactions })\n this._sorted = null\n return\n }\n case 'edited': {\n const m = this.byId.get(frame.messageId)\n if (m) { this.byId.set(frame.messageId, { ...m, content: frame.content, editedAt: frame.editedAt }); this._sorted = null }\n return\n }\n case 'deleted': {\n const m = this.byId.get(frame.messageId)\n if (m) { this.byId.set(frame.messageId, { ...m, deletedAt: frame.ts }); this._sorted = null }\n return\n }\n case 'state':\n this.state = frame.state\n return\n case 'assigned':\n this.assignedAgentId = frame.agentId ?? undefined\n return\n case 'presence':\n if (frame.status === 'online') this.online.add(frame.userId)\n else this.online.delete(frame.userId)\n return\n case 'subjectState':\n case 'invoked':\n case 'authed':\n case 'error':\n case 'pong':\n return\n case 'sentiment':\n this.sentiment = frame.label\n this.sentimentScore = frame.score\n return\n default:\n return\n }\n }\n\n private upsert(msg: RenderMessage): void {\n const existing = this.byId.get(msg.id)\n this.byId.set(msg.id, existing ? { ...existing, ...msg } : msg)\n if (msg.seq > this._maxSeq) this._maxSeq = msg.seq\n this._sorted = null\n }\n\n private markOwnStatus(uptoSeq: number, status: SendStatus): void {\n const targetRank = rank(status)\n let changed = false\n for (const [k, m] of this.byId) {\n if (m.senderId !== this.me || m.seq <= 0 || m.seq > uptoSeq) continue\n if (rank(m.status) >= targetRank) continue // already at or above target — skip\n this.byId.set(k, { ...m, status })\n changed = true\n }\n if (changed) this._sorted = null\n }\n}\nfunction rank(s: SendStatus | undefined): number {\n switch (s) { case 'read': return 3; case 'delivered': return 2; case 'sent': return 1; default: return 0 }\n}\n","import type { MessageContent } from './protocol/index.js'\n\nexport interface OutboxItem {\n clientMsgId: string\n content: MessageContent\n ts: number\n}\n\nconst MAX_ITEMS = 200 // cap so a long outage can't grow storage unboundedly\nconst MAX_AGE_MS = 7 * 86_400_000 // drop anything older than 7 days on load\n\n/** Persists not-yet-acknowledged outgoing messages to localStorage, keyed by\n * guest token, so a page reload during a connectivity drop doesn't silently\n * lose what the user typed (the \"WhatsApp\" guarantee: your message is queued\n * until it's confirmed sent, even across app restarts). */\nexport class PersistentOutbox {\n private readonly key: string\n\n constructor(token: string) {\n this.key = `ocw_outbox_${token}`\n }\n\n /** All pending items, oldest first, with stale (>7d) entries dropped. */\n load(): OutboxItem[] {\n try {\n const raw = localStorage.getItem(this.key)\n if (!raw) return []\n const items = JSON.parse(raw) as OutboxItem[]\n const cutoff = Date.now() - MAX_AGE_MS\n const fresh = items.filter(i => i.ts >= cutoff)\n if (fresh.length !== items.length) this.save(fresh)\n return fresh\n } catch {\n return []\n }\n }\n\n add(item: OutboxItem): void {\n try {\n const items = this.load()\n items.push(item)\n // Evict oldest when full — matches the in-memory ConnectionManager outbox policy.\n this.save(items.length > MAX_ITEMS ? items.slice(items.length - MAX_ITEMS) : items)\n } catch { /* localStorage unavailable (private mode, quota) — best-effort only */ }\n }\n\n /** Remove an item once it's been acknowledged by the server. */\n remove(clientMsgId: string): void {\n try {\n const items = this.load().filter(i => i.clientMsgId !== clientMsgId)\n this.save(items)\n } catch { /* best-effort */ }\n }\n\n private save(items: OutboxItem[]): void {\n try { localStorage.setItem(this.key, JSON.stringify(items)) } catch { /* quota exceeded — drop silently */ }\n }\n}\n"],"names":["asTenantId","s","asUserId","asConversationId","asSubjectId","asMessageId","asActionId","asProfileId","asConnectionId","ANONYMOUS_TENANT","ConnectionManager","opts","__publicField","_b","_a","sock","defaultFactory","ev","frame","decodeFrame","isClientFrame","FATAL_ERRORS","_d","_c","fresh","friendlyError","conversationId","pending","f","cap","encodeFrame","base","max","delay","code","url","ChatStore","me","a","b","ap","bp","clientMsgId","content","msg","key","confirmed","m","reactions","users","u","existing","uptoSeq","status","targetRank","rank","changed","k","MAX_ITEMS","MAX_AGE_MS","PersistentOutbox","token","raw","items","cutoff","i","item"],"mappings":"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"}
|
|
@@ -99,17 +99,6 @@ export interface Conversation {
|
|
|
99
99
|
createdAt: number;
|
|
100
100
|
updatedAt: number;
|
|
101
101
|
}
|
|
102
|
-
export interface AnnotationPoint {
|
|
103
|
-
x: number;
|
|
104
|
-
y: number;
|
|
105
|
-
}
|
|
106
|
-
export interface AnnotationStroke {
|
|
107
|
-
id: string;
|
|
108
|
-
points: AnnotationPoint[];
|
|
109
|
-
color: string;
|
|
110
|
-
width: number;
|
|
111
|
-
by: UserId;
|
|
112
|
-
}
|
|
113
102
|
export interface Subject {
|
|
114
103
|
id: SubjectId;
|
|
115
104
|
tenantId: TenantId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectionId, ConversationId, MessageId, ProfileId, SubjectId, UserId } from './ids.js';
|
|
2
|
-
import type { Channel, Conversation, Message, MessageContent, Subject
|
|
2
|
+
import type { Channel, Conversation, Message, MessageContent, Subject } from './entities.js';
|
|
3
3
|
import type { ManifestAction } from './actions.js';
|
|
4
4
|
/** Dashboard-configured pre-chat qualification form, delivered in the manifest. */
|
|
5
5
|
export interface PreChatConfig {
|
|
@@ -96,13 +96,6 @@ export type ClientFrame = {
|
|
|
96
96
|
} | {
|
|
97
97
|
type: 'agent_status';
|
|
98
98
|
status: 'online' | 'away' | 'offline';
|
|
99
|
-
} | {
|
|
100
|
-
type: 'annotate';
|
|
101
|
-
conversationId: ConversationId;
|
|
102
|
-
stroke: Omit<AnnotationStroke, 'by'>;
|
|
103
|
-
} | {
|
|
104
|
-
type: 'annotate_clear';
|
|
105
|
-
conversationId: ConversationId;
|
|
106
99
|
} | {
|
|
107
100
|
type: 'pubkey';
|
|
108
101
|
conversationId: ConversationId;
|
|
@@ -132,7 +125,6 @@ export type ServerFrame = {
|
|
|
132
125
|
type: 'opened';
|
|
133
126
|
conversation: Conversation;
|
|
134
127
|
subject?: Subject;
|
|
135
|
-
annotations?: AnnotationStroke[];
|
|
136
128
|
} | {
|
|
137
129
|
type: 'manifest';
|
|
138
130
|
conversationId: ConversationId;
|
|
@@ -228,14 +220,6 @@ export type ServerFrame = {
|
|
|
228
220
|
conversationId: ConversationId;
|
|
229
221
|
label: 'positive' | 'neutral' | 'frustrated';
|
|
230
222
|
score: number;
|
|
231
|
-
} | {
|
|
232
|
-
type: 'annotation';
|
|
233
|
-
conversationId: ConversationId;
|
|
234
|
-
stroke: AnnotationStroke;
|
|
235
|
-
} | {
|
|
236
|
-
type: 'annotation_clear';
|
|
237
|
-
conversationId: ConversationId;
|
|
238
|
-
by: UserId;
|
|
239
223
|
} | {
|
|
240
224
|
type: 'subjectState';
|
|
241
225
|
subjectId: SubjectId;
|
package/dist/react.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ interface BaseProps {
|
|
|
27
27
|
userAvatar?: string;
|
|
28
28
|
/** Brand colour hex, e.g. "#1a56db". Defaults to the profile theme colour. */
|
|
29
29
|
accent?: string;
|
|
30
|
+
/** Secondary accent (guest bubble + send button); follows `accent` when omitted. */
|
|
31
|
+
accent2?: string;
|
|
32
|
+
/** Colour scheme: 'auto'|'light'|'dark'. */
|
|
33
|
+
theme?: 'auto' | 'light' | 'dark';
|
|
30
34
|
/** Render as a floating launcher button instead of inline */
|
|
31
35
|
launcher?: boolean;
|
|
32
36
|
/** Launcher position — default 'bottom-right' */
|
|
@@ -43,6 +47,9 @@ interface BaseProps {
|
|
|
43
47
|
height?: string;
|
|
44
48
|
/** i18n string overrides for non-English sites */
|
|
45
49
|
i18n?: MountOptions['i18n'];
|
|
50
|
+
/** Per-tenant feature switches (default all ON). Set a flag false to disable
|
|
51
|
+
* reactions, CSAT, or KB deflection. */
|
|
52
|
+
features?: MountOptions['features'];
|
|
46
53
|
/** ISO language code for auto-translating incoming messages */
|
|
47
54
|
translateLang?: string;
|
|
48
55
|
/** Show a back chevron in the chatroom header that opens the full conversation
|
|
@@ -92,7 +99,7 @@ export interface ChatWidgetProps extends BaseProps {
|
|
|
92
99
|
* (inline) or `<ChatAppLauncher />` (floating bubble) — this component renders
|
|
93
100
|
* a single conversation.
|
|
94
101
|
*/
|
|
95
|
-
export declare function ChatWidget({ url, apiUrl, profileId, token, refreshToken, userId, userName, userEmail, userAvatar, contextTitle, contextSubtitle, contextStatus, subjectId, accent, launcher, position, launcherMessage, quickReplies, height, i18n, translateLang, inbox, inboxScope, }: ChatWidgetProps): JSX.Element;
|
|
102
|
+
export declare function ChatWidget({ url, apiUrl, profileId, token, refreshToken, userId, userName, userEmail, userAvatar, contextTitle, contextSubtitle, contextStatus, subjectId, accent, accent2, theme, launcher, position, launcherMessage, quickReplies, height, i18n, features, translateLang, inbox, inboxScope, }: ChatWidgetProps): JSX.Element;
|
|
96
103
|
export interface MarketplaceChatProps extends BaseProps {
|
|
97
104
|
/** Unique ID for this listing — each listing gets its own thread.
|
|
98
105
|
* Omit for a general (non-item-specific) conversation. */
|
|
@@ -142,7 +149,7 @@ export interface MarketplaceChatProps extends BaseProps {
|
|
|
142
149
|
*
|
|
143
150
|
* Only `url` and `profileId` are required. All other props are optional.
|
|
144
151
|
*/
|
|
145
|
-
export declare function MarketplaceChat({ url, apiUrl, profileId, token, refreshToken, listingId, listingTitle, listingMeta, listingPrice, listingStatus, userId, userName, userEmail, userAvatar, accent, launcher, position, launcherMessage, quickReplies, height, i18n, translateLang, inbox, inboxScope, }: MarketplaceChatProps): JSX.Element;
|
|
152
|
+
export declare function MarketplaceChat({ url, apiUrl, profileId, token, refreshToken, listingId, listingTitle, listingMeta, listingPrice, listingStatus, userId, userName, userEmail, userAvatar, accent, accent2, theme, launcher, position, launcherMessage, quickReplies, height, i18n, features, translateLang, inbox, inboxScope, }: MarketplaceChatProps): JSX.Element;
|
|
146
153
|
export interface ChatAppProps {
|
|
147
154
|
/** Relay URL — ONE url, any scheme; `https://api.relay.paramms.com` is the
|
|
148
155
|
* recommended form. The WebSocket URL and REST base are derived from it —
|
|
@@ -186,8 +193,9 @@ export interface ChatAppProps {
|
|
|
186
193
|
* the tenant FROM the profileId, so `profileId` stays the only id you
|
|
187
194
|
* configure. */
|
|
188
195
|
scope?: 'profile' | 'tenant';
|
|
189
|
-
/** Brand colour hex — default '#
|
|
196
|
+
/** Brand colour hex — default '#6c5ce7' */
|
|
190
197
|
accent?: string;
|
|
198
|
+
theme?: 'auto' | 'light' | 'dark';
|
|
191
199
|
/** Container height. Default: '100%' */
|
|
192
200
|
height?: string;
|
|
193
201
|
/** When provided, ChatApp renders an always-visible close (✕) control in the
|
|
@@ -241,7 +249,7 @@ export interface ChatAppProps {
|
|
|
241
249
|
* }
|
|
242
250
|
* ```
|
|
243
251
|
*/
|
|
244
|
-
export declare function ChatApp({ url, apiUrl, profileId, tenantId, token, refreshToken, userId, userName, userEmail, accent, height, i18n, onClose, scope, }: ChatAppProps): JSX.Element;
|
|
252
|
+
export declare function ChatApp({ url, apiUrl, profileId, tenantId, token, refreshToken, userId, userName, userEmail, accent, theme, height, i18n, onClose, scope, }: ChatAppProps): JSX.Element;
|
|
245
253
|
export interface ChatAppLauncherProps {
|
|
246
254
|
/** Relay URL — ONE url, any scheme; `https://api.relay.paramms.com` is the
|
|
247
255
|
* recommended form. The WebSocket URL and REST base are derived from it. */
|
|
@@ -268,7 +276,7 @@ export interface ChatAppLauncherProps {
|
|
|
268
276
|
/** Which conversations the panel's list shows — see ChatAppProps.scope.
|
|
269
277
|
* Default 'tenant': every conversation this user has with the business. */
|
|
270
278
|
scope?: 'profile' | 'tenant';
|
|
271
|
-
/** Brand colour hex — default '#
|
|
279
|
+
/** Brand colour hex — default '#6c5ce7' */
|
|
272
280
|
accent?: string;
|
|
273
281
|
/** true → floating bubble fixed to the corner of the screen (like Intercom)
|
|
274
282
|
* false → an inline "Messages" button that expands a panel below it
|