@pinecall/chat-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ <h1 align="center">@pinecall/chat-core</h1>
2
+
3
+ <p align="center">
4
+ <strong>Text chat client for Pinecall voice agents.</strong><br/>
5
+ Framework-agnostic core + React hook. Zero dependencies.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="#install">Install</a> ·
10
+ <a href="#vanilla-js">Vanilla JS</a> ·
11
+ <a href="#react">React</a> ·
12
+ <a href="#api-reference">API</a>
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @pinecall/chat-core
21
+ ```
22
+
23
+ > **Browser-only.** Uses the native `WebSocket` and `EventTarget` APIs. Works in any modern browser, bundler, or SSR-hydrated app.
24
+
25
+ ---
26
+
27
+ ## Vanilla JS
28
+
29
+ `ChatSession` is framework-agnostic — no React, no dependencies. Works in vanilla JS, Vue, Svelte, Angular, or any framework.
30
+
31
+ ```javascript
32
+ import { ChatSession } from "@pinecall/chat-core";
33
+
34
+ const chat = new ChatSession({ agent: "florencia" });
35
+
36
+ // Listen to events via standard EventTarget
37
+ chat.addEventListener("message", (e) => {
38
+ const msg = e.detail.message;
39
+ console.log(`${msg.role}: ${msg.text}`);
40
+ });
41
+
42
+ chat.addEventListener("status", (e) => {
43
+ console.log("Status:", e.detail.status);
44
+ });
45
+
46
+ // Connect and send
47
+ await chat.connect();
48
+ chat.send("Hola, quiero reservar un turno");
49
+ ```
50
+
51
+ ### Subscribe pattern (for reactive UI)
52
+
53
+ ```javascript
54
+ // Works with any reactive system (MobX, signals, stores)
55
+ const unsubscribe = chat.subscribe(() => {
56
+ const state = chat.getState();
57
+ console.log("Messages:", state.messages.length);
58
+ console.log("Typing:", state.typing);
59
+ console.log("Status:", state.status);
60
+ });
61
+
62
+ // Clean up
63
+ unsubscribe();
64
+ ```
65
+
66
+ ### Dynamic context injection
67
+
68
+ Inject live context into the LLM system prompt — form state, user selections, page data:
69
+
70
+ ```javascript
71
+ chat.setContext("cart", JSON.stringify({
72
+ items: ["Corte de cabello", "Tinte"],
73
+ total: 85.00,
74
+ }));
75
+
76
+ // Clear a context key
77
+ chat.setContext("cart", null);
78
+ ```
79
+
80
+ ---
81
+
82
+ ## React
83
+
84
+ The `@pinecall/chat-core/react` subpath export provides a `usePinecallChat` hook. React is an **optional** peer dependency.
85
+
86
+ ```tsx
87
+ import { usePinecallChat } from "@pinecall/chat-core/react";
88
+
89
+ function Chat() {
90
+ const { messages, send, connected, typing, streamingText } = usePinecallChat({
91
+ agent: "florencia",
92
+ });
93
+
94
+ if (!connected) return <p>Connecting...</p>;
95
+
96
+ return (
97
+ <div>
98
+ {messages.map((m) => (
99
+ <p key={m.id}>
100
+ <strong>{m.role}:</strong> {m.text}
101
+ {m.isStreaming && "▊"}
102
+ </p>
103
+ ))}
104
+ {typing && <p>Bot is typing: {streamingText}▊</p>}
105
+ <input
106
+ placeholder="Type a message..."
107
+ onKeyDown={(e) => {
108
+ if (e.key === "Enter") {
109
+ send(e.currentTarget.value);
110
+ e.currentTarget.value = "";
111
+ }
112
+ }}
113
+ />
114
+ </div>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ### Hook options
120
+
121
+ ```typescript
122
+ usePinecallChat({
123
+ agent: "florencia", // Agent ID (required)
124
+ server: "https://voice.pinecall.io", // Voice server URL (default)
125
+ autoConnect: true, // Connect on mount (default: true)
126
+ });
127
+ ```
128
+
129
+ ### Hook return
130
+
131
+ | Field | Type | Description |
132
+ |-------|------|-------------|
133
+ | `messages` | `ChatMessage[]` | All messages in the conversation |
134
+ | `send` | `(text: string) => void` | Send a text message |
135
+ | `connected` | `boolean` | `true` when connected to the server |
136
+ | `typing` | `boolean` | `true` while the bot is streaming |
137
+ | `streamingText` | `string` | Partial text of the current bot response |
138
+ | `error` | `string \| null` | Current error, if any |
139
+ | `setContext` | `(key, value) => void` | Inject dynamic context into the LLM prompt |
140
+ | `connect` | `() => void` | Manually connect |
141
+ | `disconnect` | `() => void` | Manually disconnect |
142
+
143
+ ---
144
+
145
+ ## API Reference
146
+
147
+ ### `ChatSession`
148
+
149
+ ```typescript
150
+ import { ChatSession } from "@pinecall/chat-core";
151
+
152
+ const chat = new ChatSession(options);
153
+ ```
154
+
155
+ #### Options
156
+
157
+ | Field | Type | Required | Description |
158
+ |-------|------|----------|-------------|
159
+ | `agent` | `string` | ✅ | Agent slug (e.g. `"florencia"`, `"dev-berna-florencia"`) |
160
+ | `server` | `string` | — | Voice server URL (default: `https://voice.pinecall.io`) |
161
+
162
+ #### Methods
163
+
164
+ | Method | Description |
165
+ |--------|-------------|
166
+ | `connect()` | Connect to the chat server (fetches token → opens WebSocket) |
167
+ | `disconnect()` | Close the WebSocket connection |
168
+ | `destroy()` | Disconnect + clear all subscribers. Do not reuse. |
169
+ | `send(text)` | Send a text message to the agent |
170
+ | `setContext(key, value)` | Inject/update/clear keyed context in the LLM prompt |
171
+ | `getState()` | Read-only snapshot of current state |
172
+ | `subscribe(cb)` | Subscribe to state changes (for React `useSyncExternalStore`) |
173
+
174
+ #### Events (via `EventTarget`)
175
+
176
+ | Event | `detail` | When |
177
+ |-------|----------|------|
178
+ | `status` | `{ status }` | Connection status changed |
179
+ | `message` | `{ message }` | New or updated message |
180
+ | `error` | `{ error }` | Error occurred |
181
+ | `change` | `{ state }` | Any state mutation (most general) |
182
+ | `event` | raw server payload | Every raw server event |
183
+
184
+ #### State shape
185
+
186
+ ```typescript
187
+ interface ChatSessionState {
188
+ status: "idle" | "connecting" | "connected" | "error";
189
+ error: string | null;
190
+ messages: ChatMessage[];
191
+ typing: boolean;
192
+ streamingText: string;
193
+ sessionId: string | null;
194
+ }
195
+
196
+ interface ChatMessage {
197
+ id: number;
198
+ role: "user" | "bot";
199
+ text: string;
200
+ messageId?: string; // server-assigned ID (bot messages)
201
+ isStreaming?: boolean; // true while bot is still streaming
202
+ }
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Protocol
208
+
209
+ ```
210
+ Browser Voice Server
211
+ │ │
212
+ ├─ GET /chat/token?agent_id=X ──→│ (fetch short-lived token)
213
+ │←── { token: "cht_xxx" } ──────│
214
+ │ │
215
+ ├─ WS /chat/ws?token=cht_xxx ──→│ (open WebSocket)
216
+ │←── { event: "chat.connected" }│
217
+ │ │
218
+ ├─→ { event: "message", text } │ (user sends message)
219
+ │←── { event: "chat.token", … } │ (streaming tokens)
220
+ │←── { event: "chat.token", … } │
221
+ │←── { event: "chat.done", … } │ (stream complete)
222
+ │ │
223
+ ├─→ { event: "set_context", … } │ (inject LLM context)
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Related Packages
229
+
230
+ | Package | Description |
231
+ |---------|-------------|
232
+ | [`@pinecall/sdk`](https://npmjs.com/package/@pinecall/sdk) | Server-side SDK — agent, call, tools, channels |
233
+ | [`@pinecall/voice-core`](https://npmjs.com/package/@pinecall/voice-core) | WebRTC voice session (framework-agnostic) |
234
+ | [`@pinecall/voice-widget`](https://npmjs.com/package/@pinecall/voice-widget) | React voice widget with animated orb UI |
235
+
236
+ ---
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,237 @@
1
+ // src/ChatSession.ts
2
+ var INITIAL_STATE = {
3
+ status: "idle",
4
+ error: null,
5
+ messages: [],
6
+ typing: false,
7
+ streamingText: "",
8
+ sessionId: null
9
+ };
10
+ var ChatSession = class extends EventTarget {
11
+ constructor(opts) {
12
+ super();
13
+ this.opts = opts;
14
+ }
15
+ opts;
16
+ state = { ...INITIAL_STATE };
17
+ listeners = /* @__PURE__ */ new Set();
18
+ ws = null;
19
+ reconnectTimer = null;
20
+ msgCounter = 0;
21
+ /** Read-only snapshot of current state (stable ref until next mutation). */
22
+ getState() {
23
+ return this.state;
24
+ }
25
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
26
+ subscribe(listener) {
27
+ this.listeners.add(listener);
28
+ return () => {
29
+ this.listeners.delete(listener);
30
+ };
31
+ }
32
+ setState(patch) {
33
+ const prev = this.state;
34
+ this.state = { ...prev, ...patch };
35
+ for (const l of this.listeners) l();
36
+ if (patch.status !== void 0 && patch.status !== prev.status) {
37
+ this.dispatchEvent(
38
+ new CustomEvent("status", { detail: { status: this.state.status } })
39
+ );
40
+ }
41
+ if (patch.error !== void 0 && patch.error !== null && patch.error !== prev.error) {
42
+ this.dispatchEvent(
43
+ new CustomEvent("error", { detail: { error: this.state.error } })
44
+ );
45
+ }
46
+ this.dispatchEvent(
47
+ new CustomEvent("change", { detail: { state: this.state } })
48
+ );
49
+ }
50
+ setMessages(updater) {
51
+ const next = updater(this.state.messages);
52
+ this.setState({ messages: next });
53
+ const last = next[next.length - 1];
54
+ if (last) {
55
+ this.dispatchEvent(
56
+ new CustomEvent("message", { detail: { message: last } })
57
+ );
58
+ }
59
+ }
60
+ // ── Connection ──────────────────────────────────────────────────────
61
+ async connect() {
62
+ if (this.ws) return;
63
+ try {
64
+ this.setState({
65
+ status: "connecting",
66
+ error: null
67
+ });
68
+ const base = (this.opts.server ?? "https://voice.pinecall.io").replace(
69
+ /\/$/,
70
+ ""
71
+ );
72
+ const tRes = await fetch(
73
+ `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`
74
+ );
75
+ if (!tRes.ok) {
76
+ const body = await tRes.text();
77
+ throw new Error(`Token: ${tRes.status} ${body}`);
78
+ }
79
+ const { token, server: chatServer } = await tRes.json();
80
+ const wsBase = (chatServer || base).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
81
+ const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);
82
+ this.ws = ws;
83
+ ws.onopen = () => {
84
+ };
85
+ ws.onmessage = (evt) => this.handleMessage(evt);
86
+ ws.onerror = () => {
87
+ this.setState({ error: "WebSocket error", status: "error" });
88
+ };
89
+ ws.onclose = (evt) => {
90
+ this.ws = null;
91
+ if (this.state.status === "connected") {
92
+ this.setState({ status: "idle" });
93
+ }
94
+ };
95
+ } catch (err) {
96
+ this.setState({
97
+ error: err instanceof Error ? err.message : String(err),
98
+ status: "error"
99
+ });
100
+ this.ws = null;
101
+ }
102
+ }
103
+ handleMessage(evt) {
104
+ let d;
105
+ try {
106
+ d = JSON.parse(evt.data);
107
+ } catch {
108
+ return;
109
+ }
110
+ switch (d.event) {
111
+ case "chat.connected":
112
+ this.setState({
113
+ status: "connected",
114
+ sessionId: d.session_id ?? null
115
+ });
116
+ break;
117
+ case "chat.token":
118
+ case "llm.chat.token":
119
+ this.setState({
120
+ typing: true,
121
+ streamingText: d.text ?? ""
122
+ });
123
+ this.setMessages((prev) => {
124
+ const idx = prev.findIndex(
125
+ (m) => m.messageId === d.message_id && m.isStreaming
126
+ );
127
+ if (idx >= 0) {
128
+ return prev.map(
129
+ (m, i) => i === idx ? { ...m, text: d.text ?? "" } : m
130
+ );
131
+ }
132
+ return [
133
+ ...prev,
134
+ {
135
+ id: ++this.msgCounter,
136
+ role: "bot",
137
+ text: d.text ?? "",
138
+ messageId: d.message_id,
139
+ isStreaming: true
140
+ }
141
+ ];
142
+ });
143
+ break;
144
+ case "chat.done":
145
+ case "llm.chat.done":
146
+ this.setState({
147
+ typing: false,
148
+ streamingText: ""
149
+ });
150
+ this.setMessages((prev) => {
151
+ const idx = prev.findIndex(
152
+ (m) => m.messageId === d.message_id && m.isStreaming
153
+ );
154
+ if (idx >= 0) {
155
+ return prev.map(
156
+ (m, i) => i === idx ? { ...m, text: d.text ?? m.text, isStreaming: false } : m
157
+ );
158
+ }
159
+ return [
160
+ ...prev,
161
+ {
162
+ id: ++this.msgCounter,
163
+ role: "bot",
164
+ text: d.text ?? "",
165
+ messageId: d.message_id,
166
+ isStreaming: false
167
+ }
168
+ ];
169
+ });
170
+ break;
171
+ case "chat.error":
172
+ case "llm.chat.error":
173
+ this.setState({
174
+ typing: false,
175
+ error: d.error ?? "Unknown error"
176
+ });
177
+ break;
178
+ case "error":
179
+ this.setState({
180
+ error: d.error ?? "Unknown error",
181
+ status: "error"
182
+ });
183
+ break;
184
+ }
185
+ this.dispatchEvent(new CustomEvent("event", { detail: d }));
186
+ }
187
+ // ── Actions ─────────────────────────────────────────────────────────
188
+ /** Send a text message to the agent. */
189
+ send(text) {
190
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
191
+ const trimmed = text.trim();
192
+ if (!trimmed) return;
193
+ this.setMessages((prev) => [
194
+ ...prev,
195
+ {
196
+ id: ++this.msgCounter,
197
+ role: "user",
198
+ text: trimmed
199
+ }
200
+ ]);
201
+ this.ws.send(JSON.stringify({ event: "message", text: trimmed }));
202
+ }
203
+ /**
204
+ * Set or clear a keyed context block in the LLM system prompt.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
209
+ * session.setContext("form", null); // clear
210
+ * ```
211
+ */
212
+ setContext(key, value) {
213
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
214
+ this.ws.send(JSON.stringify({ event: "set_context", key, value }));
215
+ }
216
+ /** Disconnect the chat session. */
217
+ disconnect() {
218
+ if (this.reconnectTimer) {
219
+ clearTimeout(this.reconnectTimer);
220
+ this.reconnectTimer = null;
221
+ }
222
+ if (this.ws) {
223
+ this.ws.close();
224
+ this.ws = null;
225
+ }
226
+ this.setState({ status: "idle", typing: false, streamingText: "" });
227
+ }
228
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
229
+ destroy() {
230
+ this.disconnect();
231
+ this.listeners.clear();
232
+ }
233
+ };
234
+
235
+ export { ChatSession };
236
+ //# sourceMappingURL=chunk-ST5DVE5W.js.map
237
+ //# sourceMappingURL=chunk-ST5DVE5W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ChatSession.ts"],"names":[],"mappings":"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","file":"chunk-ST5DVE5W.js","sourcesContent":["/**\n * ChatSession — Framework-agnostic text chat client for Pinecall agents.\n *\n * Mirrors VoiceSession's API patterns:\n * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore\n * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)\n *\n * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat\n */\nimport type {\n ChatSessionState,\n ChatSessionOptions,\n ChatMessage,\n} from \"./types\";\n\nconst INITIAL_STATE: ChatSessionState = {\n status: \"idle\",\n error: null,\n messages: [],\n typing: false,\n streamingText: \"\",\n sessionId: null,\n};\n\nexport class ChatSession extends EventTarget {\n private state: ChatSessionState = { ...INITIAL_STATE };\n private listeners = new Set<() => void>();\n\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private msgCounter = 0;\n\n constructor(private opts: ChatSessionOptions) {\n super();\n }\n\n /** Read-only snapshot of current state (stable ref until next mutation). */\n getState(): Readonly<ChatSessionState> {\n return this.state;\n }\n\n /** Subscribe to ANY state change (for React useSyncExternalStore). */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private setState(patch: Partial<ChatSessionState>): void {\n const prev = this.state;\n this.state = { ...prev, ...patch };\n for (const l of this.listeners) l();\n\n if (patch.status !== undefined && patch.status !== prev.status) {\n this.dispatchEvent(\n new CustomEvent(\"status\", { detail: { status: this.state.status } }),\n );\n }\n if (\n patch.error !== undefined &&\n patch.error !== null &&\n patch.error !== prev.error\n ) {\n this.dispatchEvent(\n new CustomEvent(\"error\", { detail: { error: this.state.error } }),\n );\n }\n this.dispatchEvent(\n new CustomEvent(\"change\", { detail: { state: this.state } }),\n );\n }\n\n private setMessages(\n updater: (prev: ChatMessage[]) => ChatMessage[],\n ): void {\n const next = updater(this.state.messages);\n this.setState({ messages: next });\n const last = next[next.length - 1];\n if (last) {\n this.dispatchEvent(\n new CustomEvent(\"message\", { detail: { message: last } }),\n );\n }\n }\n\n // ── Connection ──────────────────────────────────────────────────────\n\n async connect(): Promise<void> {\n if (this.ws) return;\n\n try {\n this.setState({\n status: \"connecting\",\n error: null,\n });\n\n const base = (this.opts.server ?? \"https://voice.pinecall.io\").replace(\n /\\/$/,\n \"\",\n );\n\n // 1. Fetch chat token (public, no API key)\n const tRes = await fetch(\n `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`,\n );\n if (!tRes.ok) {\n const body = await tRes.text();\n throw new Error(`Token: ${tRes.status} ${body}`);\n }\n const { token, server: chatServer } = await tRes.json();\n const wsBase = (chatServer || base)\n .replace(/^http:/, \"ws:\")\n .replace(/^https:/, \"wss:\");\n\n // 2. Open WebSocket\n const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);\n this.ws = ws;\n\n ws.onopen = () => {\n // Wait for chat.connected event before setting status\n };\n\n ws.onmessage = (evt) => this.handleMessage(evt);\n\n ws.onerror = () => {\n this.setState({ error: \"WebSocket error\", status: \"error\" });\n };\n\n ws.onclose = (evt) => {\n this.ws = null;\n if (this.state.status === \"connected\") {\n // Unexpected disconnect\n this.setState({ status: \"idle\" });\n }\n };\n } catch (err) {\n this.setState({\n error: err instanceof Error ? err.message : String(err),\n status: \"error\",\n });\n this.ws = null;\n }\n }\n\n private handleMessage(evt: MessageEvent): void {\n let d: any;\n try {\n d = JSON.parse(evt.data);\n } catch {\n return;\n }\n\n switch (d.event) {\n case \"chat.connected\":\n this.setState({\n status: \"connected\",\n sessionId: d.session_id ?? null,\n });\n break;\n\n case \"chat.token\":\n case \"llm.chat.token\":\n // Streaming token from LLM\n this.setState({\n typing: true,\n streamingText: d.text ?? \"\",\n });\n\n // Update or create bot message\n this.setMessages((prev) => {\n const idx = prev.findIndex(\n (m) => m.messageId === d.message_id && m.isStreaming,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx ? { ...m, text: d.text ?? \"\" } : m,\n );\n }\n return [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"bot\",\n text: d.text ?? \"\",\n messageId: d.message_id,\n isStreaming: true,\n },\n ];\n });\n break;\n\n case \"chat.done\":\n case \"llm.chat.done\":\n // LLM finished streaming\n this.setState({\n typing: false,\n streamingText: \"\",\n });\n\n this.setMessages((prev) => {\n const idx = prev.findIndex(\n (m) => m.messageId === d.message_id && m.isStreaming,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx\n ? { ...m, text: d.text ?? m.text, isStreaming: false }\n : m,\n );\n }\n // If we missed the streaming, add the final message\n return [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"bot\",\n text: d.text ?? \"\",\n messageId: d.message_id,\n isStreaming: false,\n },\n ];\n });\n break;\n\n case \"chat.error\":\n case \"llm.chat.error\":\n this.setState({\n typing: false,\n error: d.error ?? \"Unknown error\",\n });\n break;\n\n case \"error\":\n this.setState({\n error: d.error ?? \"Unknown error\",\n status: \"error\",\n });\n break;\n }\n\n // Emit raw event for power users\n this.dispatchEvent(new CustomEvent(\"event\", { detail: d }));\n }\n\n // ── Actions ─────────────────────────────────────────────────────────\n\n /** Send a text message to the agent. */\n send(text: string): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n\n const trimmed = text.trim();\n if (!trimmed) return;\n\n // Add user message to local state immediately\n this.setMessages((prev) => [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"user\",\n text: trimmed,\n },\n ]);\n\n // Send to server\n this.ws.send(JSON.stringify({ event: \"message\", text: trimmed }));\n }\n\n /**\n * Set or clear a keyed context block in the LLM system prompt.\n *\n * @example\n * ```ts\n * session.setContext(\"form\", JSON.stringify({ name: \"Juan\" }));\n * session.setContext(\"form\", null); // clear\n * ```\n */\n setContext(key: string, value: string | null): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n this.ws.send(JSON.stringify({ event: \"set_context\", key, value }));\n }\n\n /** Disconnect the chat session. */\n disconnect(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n this.setState({ status: \"idle\", typing: false, streamingText: \"\" });\n }\n\n /** Tear down the session and clear subscribers. Do not reuse after this. */\n destroy(): void {\n this.disconnect();\n this.listeners.clear();\n }\n}\n"]}