@tanstack/ai-client 0.22.1 → 0.23.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/README.md +15 -1
- package/dist/esm/audio-recorder.js +190 -213
- package/dist/esm/audio-recorder.js.map +1 -1
- package/dist/esm/chat-client.d.ts +172 -3
- package/dist/esm/chat-client.js +1656 -1386
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/cleared-stream-tracker.d.ts +23 -0
- package/dist/esm/cleared-stream-tracker.js +97 -0
- package/dist/esm/cleared-stream-tracker.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +25 -12
- package/dist/esm/client-persistor.js +260 -235
- package/dist/esm/client-persistor.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +231 -10
- package/dist/esm/connection-adapters.js +989 -574
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +79 -139
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +31 -1
- package/dist/esm/devtools.js +977 -1127
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/events.js +224 -226
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +145 -2
- package/dist/esm/generation-client.js +659 -321
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-reconstruct.d.ts +21 -0
- package/dist/esm/generation-reconstruct.js +85 -0
- package/dist/esm/generation-reconstruct.js.map +1 -0
- package/dist/esm/generation-types.d.ts +289 -3
- package/dist/esm/generation-types.js +356 -13
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +7 -39
- package/dist/esm/interrupt-manager.d.ts +77 -0
- package/dist/esm/interrupt-manager.js +787 -0
- package/dist/esm/interrupt-manager.js.map +1 -0
- package/dist/esm/mcp-app-bridge.js +56 -64
- package/dist/esm/mcp-app-bridge.js.map +1 -1
- package/dist/esm/realtime-client.js +366 -440
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/response-stream.js +19 -26
- package/dist/esm/response-stream.js.map +1 -1
- package/dist/esm/sse-parser.js +44 -47
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/sse-utils.js +8 -9
- package/dist/esm/sse-utils.js.map +1 -1
- package/dist/esm/storage-adapters.d.ts +62 -0
- package/dist/esm/storage-adapters.js +174 -0
- package/dist/esm/storage-adapters.js.map +1 -0
- package/dist/esm/types.d.ts +212 -10
- package/dist/esm/types.js +38 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +113 -2
- package/dist/esm/video-generation-client.js +665 -379
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -7
- package/src/chat-client.ts +1079 -61
- package/src/cleared-stream-tracker.ts +151 -0
- package/src/client-persistor.ts +102 -33
- package/src/connection-adapters.ts +1185 -142
- package/src/devtools-noop.ts +4 -3
- package/src/devtools.ts +121 -3
- package/src/generation-client.ts +563 -13
- package/src/generation-reconstruct.ts +121 -0
- package/src/generation-types.ts +727 -3
- package/src/index.ts +56 -1
- package/src/interrupt-manager.ts +1440 -0
- package/src/storage-adapters.ts +242 -0
- package/src/types.ts +301 -9
- package/src/video-generation-client.ts +479 -13
- package/dist/esm/index.js.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getChunkRunId } from "./connection-adapters.js";
|
|
2
|
+
//#region src/cleared-stream-tracker.ts
|
|
3
|
+
function getChunkToolCallId(chunk) {
|
|
4
|
+
return "toolCallId" in chunk && typeof chunk.toolCallId === "string" ? chunk.toolCallId : void 0;
|
|
5
|
+
}
|
|
6
|
+
function getChunkMessageId(chunk) {
|
|
7
|
+
return "messageId" in chunk && typeof chunk.messageId === "string" ? chunk.messageId : void 0;
|
|
8
|
+
}
|
|
9
|
+
function getChunkParentMessageId(chunk) {
|
|
10
|
+
return "parentMessageId" in chunk && typeof chunk.parentMessageId === "string" ? chunk.parentMessageId : void 0;
|
|
11
|
+
}
|
|
12
|
+
/** Tracks stream chunks that must be ignored after the owning chat is cleared. */
|
|
13
|
+
var ClearedStreamTracker = class {
|
|
14
|
+
clearedMessageIds = /* @__PURE__ */ new Set();
|
|
15
|
+
clearedRunIds = /* @__PURE__ */ new Set();
|
|
16
|
+
ignoredActiveRunIds = /* @__PURE__ */ new Set();
|
|
17
|
+
clearedToolCallIds = /* @__PURE__ */ new Set();
|
|
18
|
+
currentRunlessRunId = null;
|
|
19
|
+
snapshotClear(context) {
|
|
20
|
+
for (const message of context.messages) this.clearedMessageIds.add(message.id);
|
|
21
|
+
for (const runId of context.activeRunIds) {
|
|
22
|
+
this.clearedRunIds.add(runId);
|
|
23
|
+
this.ignoredActiveRunIds.add(runId);
|
|
24
|
+
}
|
|
25
|
+
if (context.currentRunId) {
|
|
26
|
+
this.clearedRunIds.add(context.currentRunId);
|
|
27
|
+
this.ignoredActiveRunIds.add(context.currentRunId);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
shouldIgnoreChunk(chunk) {
|
|
31
|
+
const runId = getChunkRunId(chunk);
|
|
32
|
+
if (runId && this.clearedRunIds.has(runId)) {
|
|
33
|
+
if (chunk.type === "RUN_STARTED") {
|
|
34
|
+
this.ignoredActiveRunIds.add(runId);
|
|
35
|
+
this.currentRunlessRunId = runId;
|
|
36
|
+
}
|
|
37
|
+
this.markIgnoredChunkIds(chunk);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (runId && this.ignoredActiveRunIds.has(runId)) {
|
|
41
|
+
this.markIgnoredChunkIds(chunk);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (this.isRunlessChunkFromIgnoredRun(chunk)) {
|
|
45
|
+
this.markIgnoredChunkIds(chunk);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const toolCallId = getChunkToolCallId(chunk);
|
|
49
|
+
if (toolCallId && this.clearedToolCallIds.has(toolCallId)) return true;
|
|
50
|
+
const parentMessageId = getChunkParentMessageId(chunk);
|
|
51
|
+
if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
|
|
52
|
+
if (toolCallId) this.clearedToolCallIds.add(toolCallId);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const messageId = getChunkMessageId(chunk);
|
|
56
|
+
return Boolean(messageId && this.clearedMessageIds.has(messageId));
|
|
57
|
+
}
|
|
58
|
+
onRunStarted(runId) {
|
|
59
|
+
this.currentRunlessRunId = runId;
|
|
60
|
+
}
|
|
61
|
+
onRunSettled(runId) {
|
|
62
|
+
this.ignoredActiveRunIds.delete(runId);
|
|
63
|
+
this.clearedRunIds.delete(runId);
|
|
64
|
+
if (this.currentRunlessRunId === runId) this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
|
|
65
|
+
}
|
|
66
|
+
onSessionRunError() {
|
|
67
|
+
this.ignoredActiveRunIds.clear();
|
|
68
|
+
this.currentRunlessRunId = null;
|
|
69
|
+
}
|
|
70
|
+
resetActiveRuns() {
|
|
71
|
+
this.ignoredActiveRunIds.clear();
|
|
72
|
+
this.currentRunlessRunId = null;
|
|
73
|
+
}
|
|
74
|
+
takeRunlessRunId() {
|
|
75
|
+
const runId = this.currentRunlessRunId;
|
|
76
|
+
if (!runId) return null;
|
|
77
|
+
this.ignoredActiveRunIds.delete(runId);
|
|
78
|
+
this.clearedRunIds.delete(runId);
|
|
79
|
+
this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
|
|
80
|
+
return runId;
|
|
81
|
+
}
|
|
82
|
+
markIgnoredChunkIds(chunk) {
|
|
83
|
+
const messageId = getChunkMessageId(chunk);
|
|
84
|
+
if (messageId) this.clearedMessageIds.add(messageId);
|
|
85
|
+
const toolCallId = getChunkToolCallId(chunk);
|
|
86
|
+
if (toolCallId) this.clearedToolCallIds.add(toolCallId);
|
|
87
|
+
}
|
|
88
|
+
isRunlessChunkFromIgnoredRun(chunk) {
|
|
89
|
+
if (getChunkRunId(chunk) || !this.currentRunlessRunId) return false;
|
|
90
|
+
if (!this.ignoredActiveRunIds.has(this.currentRunlessRunId) && !this.clearedRunIds.has(this.currentRunlessRunId)) return false;
|
|
91
|
+
return chunk.type === "TEXT_MESSAGE_START" || chunk.type === "TEXT_MESSAGE_CONTENT" || chunk.type === "TOOL_CALL_START" || chunk.type === "TOOL_CALL_ARGS" || chunk.type === "TOOL_CALL_END" || chunk.type === "TOOL_CALL_RESULT" || chunk.type === "MESSAGES_SNAPSHOT" || chunk.type === "RUN_ERROR";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
export { ClearedStreamTracker };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=cleared-stream-tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleared-stream-tracker.js","names":[],"sources":["../../src/cleared-stream-tracker.ts"],"sourcesContent":["import { getChunkRunId } from './connection-adapters'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type { UIMessage } from './types'\n\nfunction getChunkToolCallId(chunk: StreamChunk): string | undefined {\n return 'toolCallId' in chunk && typeof chunk.toolCallId === 'string'\n ? chunk.toolCallId\n : undefined\n}\n\nfunction getChunkMessageId(chunk: StreamChunk): string | undefined {\n return 'messageId' in chunk && typeof chunk.messageId === 'string'\n ? chunk.messageId\n : undefined\n}\n\nfunction getChunkParentMessageId(chunk: StreamChunk): string | undefined {\n return 'parentMessageId' in chunk && typeof chunk.parentMessageId === 'string'\n ? chunk.parentMessageId\n : undefined\n}\n\n/** Tracks stream chunks that must be ignored after the owning chat is cleared. */\nexport class ClearedStreamTracker {\n private readonly clearedMessageIds = new Set<string>()\n private readonly clearedRunIds = new Set<string>()\n private readonly ignoredActiveRunIds = new Set<string>()\n private readonly clearedToolCallIds = new Set<string>()\n private currentRunlessRunId: string | null = null\n\n snapshotClear(context: {\n messages: Array<UIMessage>\n activeRunIds: Set<string>\n currentRunId: string | null\n }): void {\n for (const message of context.messages) {\n this.clearedMessageIds.add(message.id)\n }\n for (const runId of context.activeRunIds) {\n this.clearedRunIds.add(runId)\n this.ignoredActiveRunIds.add(runId)\n }\n if (context.currentRunId) {\n this.clearedRunIds.add(context.currentRunId)\n this.ignoredActiveRunIds.add(context.currentRunId)\n }\n }\n\n shouldIgnoreChunk(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId && this.clearedRunIds.has(runId)) {\n if (chunk.type === 'RUN_STARTED') {\n this.ignoredActiveRunIds.add(runId)\n this.currentRunlessRunId = runId\n }\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (runId && this.ignoredActiveRunIds.has(runId)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (this.isRunlessChunkFromIgnoredRun(chunk)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {\n return true\n }\n\n const parentMessageId = getChunkParentMessageId(chunk)\n if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n return true\n }\n\n const messageId = getChunkMessageId(chunk)\n return Boolean(messageId && this.clearedMessageIds.has(messageId))\n }\n\n onRunStarted(runId: string): void {\n this.currentRunlessRunId = runId\n }\n\n onRunSettled(runId: string): void {\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n if (this.currentRunlessRunId === runId) {\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n }\n }\n\n onSessionRunError(): void {\n this.ignoredActiveRunIds.clear()\n this.currentRunlessRunId = null\n }\n\n resetActiveRuns(): void {\n this.ignoredActiveRunIds.clear()\n this.currentRunlessRunId = null\n }\n\n takeRunlessRunId(): string | null {\n const runId = this.currentRunlessRunId\n if (!runId) return null\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n return runId\n }\n\n private markIgnoredChunkIds(chunk: StreamChunk): void {\n const messageId = getChunkMessageId(chunk)\n if (messageId) {\n this.clearedMessageIds.add(messageId)\n }\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n }\n\n private isRunlessChunkFromIgnoredRun(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId || !this.currentRunlessRunId) return false\n if (\n !this.ignoredActiveRunIds.has(this.currentRunlessRunId) &&\n !this.clearedRunIds.has(this.currentRunlessRunId)\n ) {\n return false\n }\n return (\n chunk.type === 'TEXT_MESSAGE_START' ||\n chunk.type === 'TEXT_MESSAGE_CONTENT' ||\n chunk.type === 'TOOL_CALL_START' ||\n chunk.type === 'TOOL_CALL_ARGS' ||\n chunk.type === 'TOOL_CALL_END' ||\n chunk.type === 'TOOL_CALL_RESULT' ||\n chunk.type === 'MESSAGES_SNAPSHOT' ||\n chunk.type === 'RUN_ERROR'\n )\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { StreamChunk } from '@tanstack/ai/client';
|
|
2
|
-
import { ChatClientPersistence, UIMessage } from './types.js';
|
|
2
|
+
import { ChatClientPersistence, ChatPersistedState, ChatResumeSnapshot, UIMessage } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Encapsulates everything persistence-related for `ChatClient` so the client
|
|
5
5
|
* itself stays focused on streaming and message state.
|
|
6
6
|
*
|
|
7
7
|
* Two responsibilities live here:
|
|
8
8
|
*
|
|
9
|
-
* 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save
|
|
10
|
-
* `
|
|
11
|
-
* `removeItem(id)` on clear
|
|
12
|
-
*
|
|
9
|
+
* 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save a
|
|
10
|
+
* combined `{ messages, resume? }` record via `setItem` on every change
|
|
11
|
+
* through an ordered write queue, and `removeItem(id)` on clear (or when
|
|
12
|
+
* both the transcript and resume pointer are empty). A generation counter
|
|
13
|
+
* discards stale writes when a removal or a newer conversation supersedes
|
|
14
|
+
* an in-flight async operation.
|
|
13
15
|
* 2. **Clear-during-stream suppression** — when a conversation is cleared while a
|
|
14
16
|
* stream is still producing, late chunks for the cleared run(s) must not
|
|
15
17
|
* repopulate the now-empty state. The persistor tracks the cleared ids and
|
|
@@ -22,33 +24,44 @@ export declare class ChatPersistor {
|
|
|
22
24
|
private readonly adapter;
|
|
23
25
|
private readonly id;
|
|
24
26
|
private readonly applyMessages;
|
|
27
|
+
private readonly applyResume?;
|
|
25
28
|
private skipNextPersist;
|
|
26
29
|
private generation;
|
|
27
30
|
private queue;
|
|
28
31
|
private queuePending;
|
|
29
32
|
private messagesGeneration;
|
|
33
|
+
private lastMessages;
|
|
34
|
+
private lastResume;
|
|
30
35
|
private readonly clearedMessageIds;
|
|
31
36
|
private readonly clearedRunIds;
|
|
32
37
|
private readonly ignoredActiveRunIds;
|
|
33
38
|
private readonly clearedToolCallIds;
|
|
34
39
|
private currentRunlessRunId;
|
|
35
|
-
constructor(adapter: ChatClientPersistence, id: string, applyMessages: (messages: Array<UIMessage>) => void);
|
|
40
|
+
constructor(adapter: ChatClientPersistence, id: string, applyMessages: (messages: Array<UIMessage>) => void, applyResume?: ((snapshot: ChatResumeSnapshot) => void) | undefined);
|
|
41
|
+
/** Persist the current state as one combined `{ messages, resume? }` record. */
|
|
42
|
+
private writeState;
|
|
36
43
|
/**
|
|
37
|
-
* Synchronously read the persisted
|
|
38
|
-
* Returns the
|
|
44
|
+
* Synchronously read the persisted state for constructor-time hydration.
|
|
45
|
+
* Returns the normalized combined record, or a promise of it for async stores.
|
|
39
46
|
*/
|
|
40
|
-
readInitial():
|
|
47
|
+
readInitial(): ChatPersistedState | undefined | Promise<ChatPersistedState | undefined>;
|
|
41
48
|
/**
|
|
42
|
-
* Apply
|
|
49
|
+
* Apply state from an async `getItem` once it resolves, unless the message
|
|
43
50
|
* list has already changed since hydration began.
|
|
44
51
|
*/
|
|
45
|
-
hydrateAsync(
|
|
52
|
+
hydrateAsync(persistedState: ChatPersistedState | undefined | Promise<ChatPersistedState | undefined>): void;
|
|
46
53
|
/**
|
|
47
|
-
* Record a message-list change and queue a
|
|
54
|
+
* Record a message-list change and queue a combined write for it. Skips a
|
|
48
55
|
* single write after {@link beginClear} so the clear's empty snapshot isn't
|
|
49
56
|
* persisted between `clearMessages()` and {@link remove}.
|
|
50
57
|
*/
|
|
51
58
|
notifyMessagesChanged(messages: Array<UIMessage>): void;
|
|
59
|
+
/**
|
|
60
|
+
* Record the current resume snapshot (which run to rejoin / which interrupts
|
|
61
|
+
* are pending) and persist it alongside the messages. Pass `null` to clear it
|
|
62
|
+
* once the run reaches a non-interrupt terminal.
|
|
63
|
+
*/
|
|
64
|
+
persistResumeSnapshot(snapshot: ChatResumeSnapshot | null): void;
|
|
52
65
|
/** Remove the persisted conversation. Invalidates any queued writes. */
|
|
53
66
|
remove(): void;
|
|
54
67
|
private runOperation;
|
|
@@ -1,243 +1,268 @@
|
|
|
1
1
|
import { getChunkRunId } from "./connection-adapters.js";
|
|
2
|
+
//#region src/client-persistor.ts
|
|
3
|
+
/** Normalize a raw `getItem` result (legacy bare array or combined record). */
|
|
4
|
+
function normalizePersistedState(raw) {
|
|
5
|
+
if (Array.isArray(raw)) return { messages: raw };
|
|
6
|
+
if (raw && Array.isArray(raw.messages)) return raw;
|
|
7
|
+
}
|
|
2
8
|
function getChunkToolCallId(chunk) {
|
|
3
|
-
|
|
9
|
+
return "toolCallId" in chunk && typeof chunk.toolCallId === "string" ? chunk.toolCallId : void 0;
|
|
4
10
|
}
|
|
5
11
|
function getChunkMessageId(chunk) {
|
|
6
|
-
|
|
12
|
+
return "messageId" in chunk && typeof chunk.messageId === "string" ? chunk.messageId : void 0;
|
|
7
13
|
}
|
|
8
14
|
function getChunkParentMessageId(chunk) {
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
class ChatPersistor {
|
|
12
|
-
constructor(adapter, id, applyMessages) {
|
|
13
|
-
this.adapter = adapter;
|
|
14
|
-
this.id = id;
|
|
15
|
-
this.applyMessages = applyMessages;
|
|
16
|
-
}
|
|
17
|
-
adapter;
|
|
18
|
-
id;
|
|
19
|
-
applyMessages;
|
|
20
|
-
// --- storage queue state ---
|
|
21
|
-
skipNextPersist = false;
|
|
22
|
-
generation = 0;
|
|
23
|
-
queue = Promise.resolve();
|
|
24
|
-
queuePending = false;
|
|
25
|
-
// Bumped on every message change; lets an in-flight async hydration detect
|
|
26
|
-
// that the message list moved on and avoid clobbering it.
|
|
27
|
-
messagesGeneration = 0;
|
|
28
|
-
// --- clear-during-stream suppression state ---
|
|
29
|
-
clearedMessageIds = /* @__PURE__ */ new Set();
|
|
30
|
-
clearedRunIds = /* @__PURE__ */ new Set();
|
|
31
|
-
ignoredActiveRunIds = /* @__PURE__ */ new Set();
|
|
32
|
-
clearedToolCallIds = /* @__PURE__ */ new Set();
|
|
33
|
-
currentRunlessRunId = null;
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Storage orchestration
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
/**
|
|
38
|
-
* Synchronously read the persisted messages for constructor-time hydration.
|
|
39
|
-
* Returns the raw `getItem` result (which may be a promise for async stores).
|
|
40
|
-
*/
|
|
41
|
-
readInitial() {
|
|
42
|
-
try {
|
|
43
|
-
return this.adapter.getItem(this.id);
|
|
44
|
-
} catch {
|
|
45
|
-
return void 0;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Apply messages from an async `getItem` once it resolves, unless the message
|
|
50
|
-
* list has already changed since hydration began.
|
|
51
|
-
*/
|
|
52
|
-
hydrateAsync(persistedMessages) {
|
|
53
|
-
if (!(persistedMessages instanceof Promise)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const hydrationGeneration = this.messagesGeneration;
|
|
57
|
-
persistedMessages.then((messages) => {
|
|
58
|
-
if (Array.isArray(messages) && this.messagesGeneration === hydrationGeneration) {
|
|
59
|
-
this.applyMessages(messages);
|
|
60
|
-
}
|
|
61
|
-
}).catch(() => {
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Record a message-list change and queue a `setItem` write for it. Skips a
|
|
66
|
-
* single write after {@link beginClear} so the clear's empty snapshot isn't
|
|
67
|
-
* persisted between `clearMessages()` and {@link remove}.
|
|
68
|
-
*/
|
|
69
|
-
notifyMessagesChanged(messages) {
|
|
70
|
-
this.messagesGeneration++;
|
|
71
|
-
if (this.skipNextPersist) {
|
|
72
|
-
this.skipNextPersist = false;
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const generation = this.generation;
|
|
76
|
-
const messagesSnapshot = [...messages];
|
|
77
|
-
this.runOperation(() => {
|
|
78
|
-
if (generation !== this.generation) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
return this.adapter.setItem(this.id, messagesSnapshot);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/** Remove the persisted conversation. Invalidates any queued writes. */
|
|
85
|
-
remove() {
|
|
86
|
-
const generation = ++this.generation;
|
|
87
|
-
this.runOperation(() => {
|
|
88
|
-
if (generation !== this.generation) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
return this.adapter.removeItem(this.id);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
runOperation(operation) {
|
|
95
|
-
if (this.queuePending) {
|
|
96
|
-
const queued = this.queue.then(operation).catch(() => {
|
|
97
|
-
});
|
|
98
|
-
this.queue = queued;
|
|
99
|
-
void queued.finally(() => {
|
|
100
|
-
if (this.queue === queued) {
|
|
101
|
-
this.queuePending = false;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
const result = operation();
|
|
108
|
-
if (result instanceof Promise) {
|
|
109
|
-
this.queuePending = true;
|
|
110
|
-
const queued = result.catch(() => {
|
|
111
|
-
});
|
|
112
|
-
this.queue = queued;
|
|
113
|
-
void queued.finally(() => {
|
|
114
|
-
if (this.queue === queued) {
|
|
115
|
-
this.queuePending = false;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// Clear-during-stream suppression
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
/**
|
|
126
|
-
* Capture the message/run ids that exist at the moment of a clear so chunks
|
|
127
|
-
* still arriving for them can be ignored.
|
|
128
|
-
*/
|
|
129
|
-
snapshotClear(context) {
|
|
130
|
-
for (const message of context.messages) {
|
|
131
|
-
this.clearedMessageIds.add(message.id);
|
|
132
|
-
}
|
|
133
|
-
for (const runId of context.activeRunIds) {
|
|
134
|
-
this.clearedRunIds.add(runId);
|
|
135
|
-
this.ignoredActiveRunIds.add(runId);
|
|
136
|
-
}
|
|
137
|
-
if (context.currentRunId) {
|
|
138
|
-
this.clearedRunIds.add(context.currentRunId);
|
|
139
|
-
this.ignoredActiveRunIds.add(context.currentRunId);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/** Mark that the next persisted message change (the clear itself) is skipped. */
|
|
143
|
-
beginClear() {
|
|
144
|
-
this.skipNextPersist = true;
|
|
145
|
-
}
|
|
146
|
-
/** Whether a chunk belongs to cleared state and should not be processed. */
|
|
147
|
-
shouldIgnoreChunk(chunk) {
|
|
148
|
-
const runId = getChunkRunId(chunk);
|
|
149
|
-
if (runId && this.clearedRunIds.has(runId)) {
|
|
150
|
-
if (chunk.type === "RUN_STARTED") {
|
|
151
|
-
this.ignoredActiveRunIds.add(runId);
|
|
152
|
-
this.currentRunlessRunId = runId;
|
|
153
|
-
}
|
|
154
|
-
this.markIgnoredChunkIds(chunk);
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
if (runId && this.ignoredActiveRunIds.has(runId)) {
|
|
158
|
-
this.markIgnoredChunkIds(chunk);
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
if (this.isRunlessChunkFromIgnoredRun(chunk)) {
|
|
162
|
-
this.markIgnoredChunkIds(chunk);
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
const toolCallId = getChunkToolCallId(chunk);
|
|
166
|
-
if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
const parentMessageId = getChunkParentMessageId(chunk);
|
|
170
|
-
if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
|
|
171
|
-
if (toolCallId) {
|
|
172
|
-
this.clearedToolCallIds.add(toolCallId);
|
|
173
|
-
}
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
const messageId = getChunkMessageId(chunk);
|
|
177
|
-
if (!messageId) {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
if (this.clearedMessageIds.has(messageId)) {
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* The owning client calls this when a run starts so runless content chunks
|
|
187
|
-
* (adapters that omit `runId` on content events) can be attributed to it.
|
|
188
|
-
*/
|
|
189
|
-
onRunStarted(runId) {
|
|
190
|
-
this.currentRunlessRunId = runId;
|
|
191
|
-
}
|
|
192
|
-
/** Forget a settled run, advancing the runless pointer to another ignored run. */
|
|
193
|
-
onRunSettled(runId) {
|
|
194
|
-
this.ignoredActiveRunIds.delete(runId);
|
|
195
|
-
this.clearedRunIds.delete(runId);
|
|
196
|
-
if (this.currentRunlessRunId === runId) {
|
|
197
|
-
this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
/** A session-level (runId-less) RUN_ERROR clears all ignored-run tracking. */
|
|
201
|
-
onSessionRunError() {
|
|
202
|
-
this.ignoredActiveRunIds.clear();
|
|
203
|
-
this.currentRunlessRunId = null;
|
|
204
|
-
}
|
|
205
|
-
/** Clear the ignored-active-run markers (mirrors a session-generating reset). */
|
|
206
|
-
resetIgnored() {
|
|
207
|
-
this.ignoredActiveRunIds.clear();
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Consume the current runless run id (if any), forgetting it. Used when an
|
|
211
|
-
* ignored, runId-less RUN_ERROR drains the run the client is still tracking.
|
|
212
|
-
*/
|
|
213
|
-
takeRunlessRunId() {
|
|
214
|
-
const runId = this.currentRunlessRunId;
|
|
215
|
-
if (!runId) return null;
|
|
216
|
-
this.ignoredActiveRunIds.delete(runId);
|
|
217
|
-
this.clearedRunIds.delete(runId);
|
|
218
|
-
this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
|
|
219
|
-
return runId;
|
|
220
|
-
}
|
|
221
|
-
markIgnoredChunkIds(chunk) {
|
|
222
|
-
const messageId = getChunkMessageId(chunk);
|
|
223
|
-
if (messageId) {
|
|
224
|
-
this.clearedMessageIds.add(messageId);
|
|
225
|
-
}
|
|
226
|
-
const toolCallId = getChunkToolCallId(chunk);
|
|
227
|
-
if (toolCallId) {
|
|
228
|
-
this.clearedToolCallIds.add(toolCallId);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
isRunlessChunkFromIgnoredRun(chunk) {
|
|
232
|
-
const runId = getChunkRunId(chunk);
|
|
233
|
-
if (runId || !this.currentRunlessRunId) return false;
|
|
234
|
-
if (!this.ignoredActiveRunIds.has(this.currentRunlessRunId) && !this.clearedRunIds.has(this.currentRunlessRunId)) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
return chunk.type === "TEXT_MESSAGE_START" || chunk.type === "TEXT_MESSAGE_CONTENT" || chunk.type === "TOOL_CALL_START" || chunk.type === "TOOL_CALL_ARGS" || chunk.type === "TOOL_CALL_END" || chunk.type === "TOOL_CALL_RESULT" || chunk.type === "MESSAGES_SNAPSHOT" || chunk.type === "RUN_ERROR";
|
|
238
|
-
}
|
|
15
|
+
return "parentMessageId" in chunk && typeof chunk.parentMessageId === "string" ? chunk.parentMessageId : void 0;
|
|
239
16
|
}
|
|
240
|
-
|
|
241
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Encapsulates everything persistence-related for `ChatClient` so the client
|
|
19
|
+
* itself stays focused on streaming and message state.
|
|
20
|
+
*
|
|
21
|
+
* Two responsibilities live here:
|
|
22
|
+
*
|
|
23
|
+
* 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save a
|
|
24
|
+
* combined `{ messages, resume? }` record via `setItem` on every change
|
|
25
|
+
* through an ordered write queue, and `removeItem(id)` on clear (or when
|
|
26
|
+
* both the transcript and resume pointer are empty). A generation counter
|
|
27
|
+
* discards stale writes when a removal or a newer conversation supersedes
|
|
28
|
+
* an in-flight async operation.
|
|
29
|
+
* 2. **Clear-during-stream suppression** — when a conversation is cleared while a
|
|
30
|
+
* stream is still producing, late chunks for the cleared run(s) must not
|
|
31
|
+
* repopulate the now-empty state. The persistor tracks the cleared ids and
|
|
32
|
+
* decides, per chunk, whether the client should ignore it.
|
|
33
|
+
*
|
|
34
|
+
* All adapter calls are best-effort: a throwing or rejecting adapter is swallowed
|
|
35
|
+
* so storage problems never break the chat.
|
|
36
|
+
*/
|
|
37
|
+
var ChatPersistor = class {
|
|
38
|
+
adapter;
|
|
39
|
+
id;
|
|
40
|
+
applyMessages;
|
|
41
|
+
applyResume;
|
|
42
|
+
skipNextPersist = false;
|
|
43
|
+
generation = 0;
|
|
44
|
+
queue = Promise.resolve();
|
|
45
|
+
queuePending = false;
|
|
46
|
+
messagesGeneration = 0;
|
|
47
|
+
lastMessages = [];
|
|
48
|
+
lastResume = null;
|
|
49
|
+
clearedMessageIds = /* @__PURE__ */ new Set();
|
|
50
|
+
clearedRunIds = /* @__PURE__ */ new Set();
|
|
51
|
+
ignoredActiveRunIds = /* @__PURE__ */ new Set();
|
|
52
|
+
clearedToolCallIds = /* @__PURE__ */ new Set();
|
|
53
|
+
currentRunlessRunId = null;
|
|
54
|
+
constructor(adapter, id, applyMessages, applyResume) {
|
|
55
|
+
this.adapter = adapter;
|
|
56
|
+
this.id = id;
|
|
57
|
+
this.applyMessages = applyMessages;
|
|
58
|
+
this.applyResume = applyResume;
|
|
59
|
+
}
|
|
60
|
+
/** Persist the current state as one combined `{ messages, resume? }` record. */
|
|
61
|
+
writeState() {
|
|
62
|
+
const messages = [...this.lastMessages];
|
|
63
|
+
if (messages.length === 0 && !this.lastResume) {
|
|
64
|
+
const generation = this.generation;
|
|
65
|
+
this.runOperation(() => {
|
|
66
|
+
if (generation !== this.generation) return;
|
|
67
|
+
return this.adapter.removeItem(this.id);
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const generation = this.generation;
|
|
72
|
+
const state = {
|
|
73
|
+
messages,
|
|
74
|
+
...this.lastResume ? { resume: this.lastResume } : {}
|
|
75
|
+
};
|
|
76
|
+
this.runOperation(() => {
|
|
77
|
+
if (generation !== this.generation) return;
|
|
78
|
+
return this.adapter.setItem(this.id, state);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Synchronously read the persisted state for constructor-time hydration.
|
|
83
|
+
* Returns the normalized combined record, or a promise of it for async stores.
|
|
84
|
+
*/
|
|
85
|
+
readInitial() {
|
|
86
|
+
try {
|
|
87
|
+
const raw = this.adapter.getItem(this.id);
|
|
88
|
+
if (raw instanceof Promise) return raw.then(normalizePersistedState).catch(() => void 0);
|
|
89
|
+
const state = normalizePersistedState(raw);
|
|
90
|
+
if (state) {
|
|
91
|
+
this.lastMessages = state.messages;
|
|
92
|
+
this.lastResume = state.resume ?? null;
|
|
93
|
+
}
|
|
94
|
+
return state;
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Apply state from an async `getItem` once it resolves, unless the message
|
|
101
|
+
* list has already changed since hydration began.
|
|
102
|
+
*/
|
|
103
|
+
hydrateAsync(persistedState) {
|
|
104
|
+
if (!(persistedState instanceof Promise)) return;
|
|
105
|
+
const hydrationGeneration = this.messagesGeneration;
|
|
106
|
+
persistedState.then((state) => {
|
|
107
|
+
if (!state || this.messagesGeneration !== hydrationGeneration) return;
|
|
108
|
+
this.lastResume = state.resume ?? null;
|
|
109
|
+
this.lastMessages = state.messages;
|
|
110
|
+
this.applyMessages(state.messages);
|
|
111
|
+
if (state.resume && this.applyResume) this.applyResume(state.resume);
|
|
112
|
+
}).catch(() => {});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Record a message-list change and queue a combined write for it. Skips a
|
|
116
|
+
* single write after {@link beginClear} so the clear's empty snapshot isn't
|
|
117
|
+
* persisted between `clearMessages()` and {@link remove}.
|
|
118
|
+
*/
|
|
119
|
+
notifyMessagesChanged(messages) {
|
|
120
|
+
this.messagesGeneration++;
|
|
121
|
+
this.lastMessages = [...messages];
|
|
122
|
+
if (this.skipNextPersist) {
|
|
123
|
+
this.skipNextPersist = false;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.writeState();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Record the current resume snapshot (which run to rejoin / which interrupts
|
|
130
|
+
* are pending) and persist it alongside the messages. Pass `null` to clear it
|
|
131
|
+
* once the run reaches a non-interrupt terminal.
|
|
132
|
+
*/
|
|
133
|
+
persistResumeSnapshot(snapshot) {
|
|
134
|
+
this.lastResume = snapshot;
|
|
135
|
+
if (this.skipNextPersist) return;
|
|
136
|
+
this.writeState();
|
|
137
|
+
}
|
|
138
|
+
/** Remove the persisted conversation. Invalidates any queued writes. */
|
|
139
|
+
remove() {
|
|
140
|
+
this.lastMessages = [];
|
|
141
|
+
this.lastResume = null;
|
|
142
|
+
const generation = ++this.generation;
|
|
143
|
+
this.runOperation(() => {
|
|
144
|
+
if (generation !== this.generation) return;
|
|
145
|
+
return this.adapter.removeItem(this.id);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
runOperation(operation) {
|
|
149
|
+
if (this.queuePending) {
|
|
150
|
+
const queued = this.queue.then(operation).catch(() => {});
|
|
151
|
+
this.queue = queued;
|
|
152
|
+
queued.finally(() => {
|
|
153
|
+
if (this.queue === queued) this.queuePending = false;
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const result = operation();
|
|
159
|
+
if (result instanceof Promise) {
|
|
160
|
+
this.queuePending = true;
|
|
161
|
+
const queued = result.catch(() => {});
|
|
162
|
+
this.queue = queued;
|
|
163
|
+
queued.finally(() => {
|
|
164
|
+
if (this.queue === queued) this.queuePending = false;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Capture the message/run ids that exist at the moment of a clear so chunks
|
|
171
|
+
* still arriving for them can be ignored.
|
|
172
|
+
*/
|
|
173
|
+
snapshotClear(context) {
|
|
174
|
+
for (const message of context.messages) this.clearedMessageIds.add(message.id);
|
|
175
|
+
for (const runId of context.activeRunIds) {
|
|
176
|
+
this.clearedRunIds.add(runId);
|
|
177
|
+
this.ignoredActiveRunIds.add(runId);
|
|
178
|
+
}
|
|
179
|
+
if (context.currentRunId) {
|
|
180
|
+
this.clearedRunIds.add(context.currentRunId);
|
|
181
|
+
this.ignoredActiveRunIds.add(context.currentRunId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Mark that the next persisted message change (the clear itself) is skipped. */
|
|
185
|
+
beginClear() {
|
|
186
|
+
this.skipNextPersist = true;
|
|
187
|
+
}
|
|
188
|
+
/** Whether a chunk belongs to cleared state and should not be processed. */
|
|
189
|
+
shouldIgnoreChunk(chunk) {
|
|
190
|
+
const runId = getChunkRunId(chunk);
|
|
191
|
+
if (runId && this.clearedRunIds.has(runId)) {
|
|
192
|
+
if (chunk.type === "RUN_STARTED") {
|
|
193
|
+
this.ignoredActiveRunIds.add(runId);
|
|
194
|
+
this.currentRunlessRunId = runId;
|
|
195
|
+
}
|
|
196
|
+
this.markIgnoredChunkIds(chunk);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (runId && this.ignoredActiveRunIds.has(runId)) {
|
|
200
|
+
this.markIgnoredChunkIds(chunk);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (this.isRunlessChunkFromIgnoredRun(chunk)) {
|
|
204
|
+
this.markIgnoredChunkIds(chunk);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const toolCallId = getChunkToolCallId(chunk);
|
|
208
|
+
if (toolCallId && this.clearedToolCallIds.has(toolCallId)) return true;
|
|
209
|
+
const parentMessageId = getChunkParentMessageId(chunk);
|
|
210
|
+
if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
|
|
211
|
+
if (toolCallId) this.clearedToolCallIds.add(toolCallId);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
const messageId = getChunkMessageId(chunk);
|
|
215
|
+
if (!messageId) return false;
|
|
216
|
+
if (this.clearedMessageIds.has(messageId)) return true;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The owning client calls this when a run starts so runless content chunks
|
|
221
|
+
* (adapters that omit `runId` on content events) can be attributed to it.
|
|
222
|
+
*/
|
|
223
|
+
onRunStarted(runId) {
|
|
224
|
+
this.currentRunlessRunId = runId;
|
|
225
|
+
}
|
|
226
|
+
/** Forget a settled run, advancing the runless pointer to another ignored run. */
|
|
227
|
+
onRunSettled(runId) {
|
|
228
|
+
this.ignoredActiveRunIds.delete(runId);
|
|
229
|
+
this.clearedRunIds.delete(runId);
|
|
230
|
+
if (this.currentRunlessRunId === runId) this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
|
|
231
|
+
}
|
|
232
|
+
/** A session-level (runId-less) RUN_ERROR clears all ignored-run tracking. */
|
|
233
|
+
onSessionRunError() {
|
|
234
|
+
this.ignoredActiveRunIds.clear();
|
|
235
|
+
this.currentRunlessRunId = null;
|
|
236
|
+
}
|
|
237
|
+
/** Clear the ignored-active-run markers (mirrors a session-generating reset). */
|
|
238
|
+
resetIgnored() {
|
|
239
|
+
this.ignoredActiveRunIds.clear();
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Consume the current runless run id (if any), forgetting it. Used when an
|
|
243
|
+
* ignored, runId-less RUN_ERROR drains the run the client is still tracking.
|
|
244
|
+
*/
|
|
245
|
+
takeRunlessRunId() {
|
|
246
|
+
const runId = this.currentRunlessRunId;
|
|
247
|
+
if (!runId) return null;
|
|
248
|
+
this.ignoredActiveRunIds.delete(runId);
|
|
249
|
+
this.clearedRunIds.delete(runId);
|
|
250
|
+
this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
|
|
251
|
+
return runId;
|
|
252
|
+
}
|
|
253
|
+
markIgnoredChunkIds(chunk) {
|
|
254
|
+
const messageId = getChunkMessageId(chunk);
|
|
255
|
+
if (messageId) this.clearedMessageIds.add(messageId);
|
|
256
|
+
const toolCallId = getChunkToolCallId(chunk);
|
|
257
|
+
if (toolCallId) this.clearedToolCallIds.add(toolCallId);
|
|
258
|
+
}
|
|
259
|
+
isRunlessChunkFromIgnoredRun(chunk) {
|
|
260
|
+
if (getChunkRunId(chunk) || !this.currentRunlessRunId) return false;
|
|
261
|
+
if (!this.ignoredActiveRunIds.has(this.currentRunlessRunId) && !this.clearedRunIds.has(this.currentRunlessRunId)) return false;
|
|
262
|
+
return chunk.type === "TEXT_MESSAGE_START" || chunk.type === "TEXT_MESSAGE_CONTENT" || chunk.type === "TOOL_CALL_START" || chunk.type === "TOOL_CALL_ARGS" || chunk.type === "TOOL_CALL_END" || chunk.type === "TOOL_CALL_RESULT" || chunk.type === "MESSAGES_SNAPSHOT" || chunk.type === "RUN_ERROR";
|
|
263
|
+
}
|
|
242
264
|
};
|
|
243
|
-
//#
|
|
265
|
+
//#endregion
|
|
266
|
+
export { ChatPersistor };
|
|
267
|
+
|
|
268
|
+
//# sourceMappingURL=client-persistor.js.map
|