@composy/chat-core 0.0.1 → 0.0.2
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 +187 -123
- package/dist/index.js +2018 -1035
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1370 -1644
- package/dist/index.min.js.map +1 -0
- package/es/.dts-output-manifest.json +1 -0
- package/es/.dts.tsbuildinfo +1 -0
- package/es/constants/dom.d.ts +3 -19
- package/es/constants/dom.js +4 -4
- package/es/constants/dom.js.map +1 -1
- package/es/core/chat-manager.d.ts +33 -22
- package/es/core/chat-manager.js +174 -50
- package/es/core/chat-manager.js.map +1 -1
- package/es/core/event-emitter.d.ts +0 -1
- package/es/core/event-emitter.js +14 -7
- package/es/core/event-emitter.js.map +1 -1
- package/es/core/mention-engine.d.ts +1 -2
- package/es/core/mention-engine.js +9 -9
- package/es/core/mention-engine.js.map +1 -1
- package/es/core/message-schema.d.ts +19 -0
- package/es/core/message-schema.js +197 -0
- package/es/core/message-schema.js.map +1 -0
- package/es/core/outbox.d.ts +63 -0
- package/es/core/outbox.js +160 -0
- package/es/core/outbox.js.map +1 -0
- package/es/core/transport.d.ts +79 -0
- package/es/core/transport.js +102 -0
- package/es/core/transport.js.map +1 -0
- package/es/index.d.ts +12 -7
- package/es/index.js +167 -36
- package/es/index.js.map +1 -1
- package/es/renderer/dom-renderer.d.ts +16 -4
- package/es/renderer/dom-renderer.js +316 -61
- package/es/renderer/dom-renderer.js.map +1 -1
- package/es/renderer/style-manager.d.ts +14 -38
- package/es/renderer/style-manager.js +92 -63
- package/es/renderer/style-manager.js.map +1 -1
- package/es/theme/theme-manager.d.ts +4 -3
- package/es/theme/theme-manager.js +19 -9
- package/es/theme/theme-manager.js.map +1 -1
- package/es/theme/tokens.d.ts +0 -1
- package/es/theme/tokens.js +92 -92
- package/es/theme/tokens.js.map +1 -1
- package/es/types/index.d.ts +104 -24
- package/es/types/index.js +8 -3
- package/es/types/index.js.map +1 -0
- package/esm/constants/dom.d.ts +3 -19
- package/esm/constants/dom.js +4 -4
- package/esm/constants/dom.js.map +1 -1
- package/esm/core/chat-manager.d.ts +33 -22
- package/esm/core/chat-manager.js +174 -50
- package/esm/core/chat-manager.js.map +1 -1
- package/esm/core/event-emitter.d.ts +0 -1
- package/esm/core/event-emitter.js +14 -7
- package/esm/core/event-emitter.js.map +1 -1
- package/esm/core/mention-engine.d.ts +1 -2
- package/esm/core/mention-engine.js +9 -9
- package/esm/core/mention-engine.js.map +1 -1
- package/esm/core/message-schema.d.ts +19 -0
- package/esm/core/message-schema.js +197 -0
- package/esm/core/message-schema.js.map +1 -0
- package/esm/core/outbox.d.ts +63 -0
- package/esm/core/outbox.js +160 -0
- package/esm/core/outbox.js.map +1 -0
- package/esm/core/transport.d.ts +79 -0
- package/esm/core/transport.js +102 -0
- package/esm/core/transport.js.map +1 -0
- package/esm/index.d.ts +12 -7
- package/esm/index.js +169 -38
- package/esm/index.js.map +1 -1
- package/esm/renderer/dom-renderer.d.ts +16 -4
- package/esm/renderer/dom-renderer.js +316 -61
- package/esm/renderer/dom-renderer.js.map +1 -1
- package/esm/renderer/style-manager.d.ts +14 -38
- package/esm/renderer/style-manager.js +92 -63
- package/esm/renderer/style-manager.js.map +1 -1
- package/esm/theme/theme-manager.d.ts +4 -3
- package/esm/theme/theme-manager.js +19 -9
- package/esm/theme/theme-manager.js.map +1 -1
- package/esm/theme/tokens.d.ts +0 -1
- package/esm/theme/tokens.js +92 -92
- package/esm/theme/tokens.js.map +1 -1
- package/esm/types/index.d.ts +104 -24
- package/esm/types/index.js +8 -3
- package/esm/types/index.js.map +1 -0
- package/lib/constants/dom.cjs +4 -4
- package/lib/constants/dom.cjs.map +1 -1
- package/lib/constants/dom.d.cts +25 -0
- package/lib/constants/dom.d.ts +3 -19
- package/lib/core/chat-manager.cjs +174 -50
- package/lib/core/chat-manager.cjs.map +1 -1
- package/lib/core/chat-manager.d.cts +45 -0
- package/lib/core/chat-manager.d.ts +33 -22
- package/lib/core/event-emitter.cjs +14 -7
- package/lib/core/event-emitter.cjs.map +1 -1
- package/lib/core/event-emitter.d.cts +47 -0
- package/lib/core/event-emitter.d.ts +0 -1
- package/lib/core/mention-engine.cjs +9 -9
- package/lib/core/mention-engine.cjs.map +1 -1
- package/lib/core/mention-engine.d.cts +40 -0
- package/lib/core/mention-engine.d.ts +1 -2
- package/lib/core/message-schema.cjs +202 -0
- package/lib/core/message-schema.cjs.map +1 -0
- package/lib/core/message-schema.d.cts +19 -0
- package/lib/core/message-schema.d.ts +19 -0
- package/lib/core/outbox.cjs +162 -0
- package/lib/core/outbox.cjs.map +1 -0
- package/lib/core/outbox.d.cts +63 -0
- package/lib/core/outbox.d.ts +63 -0
- package/lib/core/transport.cjs +103 -0
- package/lib/core/transport.cjs.map +1 -0
- package/lib/core/transport.d.cts +79 -0
- package/lib/core/transport.d.ts +79 -0
- package/lib/index.cjs +175 -35
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +33 -0
- package/lib/index.d.ts +12 -7
- package/lib/renderer/dom-renderer.cjs +316 -61
- package/lib/renderer/dom-renderer.cjs.map +1 -1
- package/lib/renderer/dom-renderer.d.cts +91 -0
- package/lib/renderer/dom-renderer.d.ts +16 -4
- package/lib/renderer/style-manager.cjs +92 -62
- package/lib/renderer/style-manager.cjs.map +1 -1
- package/lib/renderer/style-manager.d.cts +34 -0
- package/lib/renderer/style-manager.d.ts +14 -38
- package/lib/theme/theme-manager.cjs +19 -9
- package/lib/theme/theme-manager.cjs.map +1 -1
- package/lib/theme/theme-manager.d.cts +34 -0
- package/lib/theme/theme-manager.d.ts +4 -3
- package/lib/theme/tokens.cjs +92 -92
- package/lib/theme/tokens.cjs.map +1 -1
- package/lib/theme/tokens.d.cts +56 -0
- package/lib/theme/tokens.d.ts +0 -1
- package/lib/types/index.cjs +8 -3
- package/lib/types/index.cjs.map +1 -0
- package/lib/types/index.d.cts +284 -0
- package/lib/types/index.d.ts +104 -24
- package/package.json +16 -13
- package/es/constants/dom.d.ts.map +0 -1
- package/es/core/chat-manager.d.ts.map +0 -1
- package/es/core/event-emitter.d.ts.map +0 -1
- package/es/core/mention-engine.d.ts.map +0 -1
- package/es/index.d.ts.map +0 -1
- package/es/renderer/dom-renderer.d.ts.map +0 -1
- package/es/renderer/style-manager.d.ts.map +0 -1
- package/es/theme/theme-manager.d.ts.map +0 -1
- package/es/theme/tokens.d.ts.map +0 -1
- package/es/types/index.d.ts.map +0 -1
- package/esm/constants/dom.d.ts.map +0 -1
- package/esm/core/chat-manager.d.ts.map +0 -1
- package/esm/core/event-emitter.d.ts.map +0 -1
- package/esm/core/mention-engine.d.ts.map +0 -1
- package/esm/index.d.ts.map +0 -1
- package/esm/renderer/dom-renderer.d.ts.map +0 -1
- package/esm/renderer/style-manager.d.ts.map +0 -1
- package/esm/theme/theme-manager.d.ts.map +0 -1
- package/esm/theme/tokens.d.ts.map +0 -1
- package/esm/types/index.d.ts.map +0 -1
- package/lib/constants/dom.d.ts.map +0 -1
- package/lib/core/chat-manager.d.ts.map +0 -1
- package/lib/core/event-emitter.d.ts.map +0 -1
- package/lib/core/mention-engine.d.ts.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/renderer/dom-renderer.d.ts.map +0 -1
- package/lib/renderer/style-manager.d.ts.map +0 -1
- package/lib/theme/theme-manager.d.ts.map +0 -1
- package/lib/theme/tokens.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-schema.cjs","names":["clone"],"sources":["../../src/core/message-schema.ts"],"sourcesContent":["import type {\n ChatContentBlock,\n ChatMessage,\n ChatUser,\n FileAttachment,\n ImageAttachment,\n MentionItem,\n} from '../types'\nimport { CHAT_MESSAGE_SCHEMA_VERSION } from '../types'\n\n/**\n * 归一化消息并复制所有可变字段。\n *\n * 该函数是 transport、outbox、ChatManager 共享的边界:外部数据先经过\n * schema/version 校验,再进入内存状态,避免调用方继续修改内部消息。\n */\nexport function normalizeChatMessage(message: ChatMessage): ChatMessage {\n if (!message || typeof message !== 'object') {\n throw new TypeError('Chat message must be an object.')\n }\n if (typeof message.id !== 'string' || !message.id.trim()) {\n throw new TypeError('Chat message ID must be a non-empty string.')\n }\n if (!Number.isFinite(message.timestamp)) {\n throw new TypeError('Chat message timestamp must be finite.')\n }\n const schemaVersion = message.schemaVersion ?? CHAT_MESSAGE_SCHEMA_VERSION\n if (schemaVersion > CHAT_MESSAGE_SCHEMA_VERSION) {\n throw new Error(\n `Unsupported chat message schema version: ${schemaVersion}.`\n )\n }\n\n return cloneChatMessage({\n ...message,\n schemaVersion: CHAT_MESSAGE_SCHEMA_VERSION,\n ...(message.contentBlocks && {\n contentBlocks: normalizeChatContentBlocks(message.contentBlocks),\n }),\n })\n}\n\n/**\n * 对来自网络/持久化的 content block 做运行时白名单归一化。\n * 不认识的块优先使用 fallback 文本,否则丢弃,避免将任意对象带入渲染器。\n */\nexport function normalizeChatContentBlocks(\n blocks: unknown[]\n): ChatContentBlock[] {\n const normalized: ChatContentBlock[] = []\n for (const value of blocks) {\n const block = normalizeContentBlock(value)\n if (block) normalized.push(block)\n }\n return normalized\n}\n\n/** 归一化消息集合,并在进入状态层前拒绝重复的消息 ID/client ID。 */\nexport function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[] {\n const result: ChatMessage[] = []\n const ids = new Set<string>()\n const clientIds = new Set<string>()\n for (const message of messages) {\n const normalized = normalizeChatMessage(message)\n if (ids.has(normalized.id)) {\n throw new Error(`Duplicate chat message ID: ${normalized.id}`)\n }\n if (\n normalized.clientMessageId &&\n clientIds.has(normalized.clientMessageId)\n ) {\n throw new Error(\n `Duplicate chat client message ID: ${normalized.clientMessageId}`\n )\n }\n ids.add(normalized.id)\n if (normalized.clientMessageId) clientIds.add(normalized.clientMessageId)\n result.push(normalized)\n }\n return result\n}\n\n/** 复制消息,用于事件 payload、outbox 与公开读取,避免可变引用泄漏。 */\nexport function cloneChatMessage(message: ChatMessage): ChatMessage {\n return {\n ...message,\n sender: cloneUser(message.sender),\n ...(message.image && { image: cloneImage(message.image) }),\n ...(message.file && { file: cloneFile(message.file) }),\n ...(message.mentions && {\n mentions: message.mentions.map(cloneMention),\n }),\n ...(message.contentBlocks && {\n contentBlocks: message.contentBlocks.map(cloneContentBlock),\n }),\n ...(message.customData && {\n customData: cloneRecord(message.customData),\n }),\n }\n}\n\n/** 根据服务端 sequence/timestamp 给出稳定排序值。 */\nexport function compareChatMessages(left: ChatMessage, right: ChatMessage) {\n if (\n Number.isFinite(left.sequence) &&\n Number.isFinite(right.sequence) &&\n left.sequence !== right.sequence\n ) {\n return (left.sequence as number) - (right.sequence as number)\n }\n if (left.timestamp !== right.timestamp)\n return left.timestamp - right.timestamp\n return left.id.localeCompare(right.id)\n}\n\nfunction cloneContentBlock(block: ChatContentBlock): ChatContentBlock {\n switch (block.type) {\n case 'text':\n return { type: 'text', text: block.text }\n case 'mention':\n return { type: 'mention', id: block.id, label: block.label }\n case 'link':\n return {\n type: 'link',\n url: block.url,\n ...(block.label && { label: block.label }),\n }\n case 'code':\n return {\n type: 'code',\n code: block.code,\n ...(block.language && { language: block.language }),\n }\n case 'image':\n return {\n type: 'image',\n src: block.src,\n ...(block.alt && { alt: block.alt }),\n ...(block.width !== undefined && { width: block.width }),\n ...(block.height !== undefined && { height: block.height }),\n }\n case 'file':\n return {\n type: 'file',\n url: block.url,\n name: block.name,\n ...(block.size !== undefined && { size: block.size }),\n ...(block.mimeType && { mimeType: block.mimeType }),\n }\n case 'break':\n return { type: 'break' }\n case 'custom':\n return {\n type: 'custom',\n kind: block.kind,\n data: cloneRecord(block.data),\n ...(block.fallback && { fallback: block.fallback }),\n }\n }\n}\n\nfunction normalizeContentBlock(value: unknown): ChatContentBlock | null {\n if (!isRecord(value) || typeof value.type !== 'string') return null\n switch (value.type) {\n case 'text':\n return typeof value.text === 'string'\n ? { type: 'text', text: value.text }\n : null\n case 'mention':\n return typeof value.id === 'string' && typeof value.label === 'string'\n ? { type: 'mention', id: value.id, label: value.label }\n : null\n case 'link':\n return typeof value.url === 'string'\n ? {\n type: 'link',\n url: value.url,\n ...(typeof value.label === 'string' && { label: value.label }),\n }\n : null\n case 'code':\n return typeof value.code === 'string'\n ? {\n type: 'code',\n code: value.code,\n ...(typeof value.language === 'string' && {\n language: value.language,\n }),\n }\n : null\n case 'image':\n return typeof value.src === 'string'\n ? {\n type: 'image',\n src: value.src,\n ...(typeof value.alt === 'string' && { alt: value.alt }),\n ...(isPositiveNumber(value.width) && { width: value.width }),\n ...(isPositiveNumber(value.height) && { height: value.height }),\n }\n : null\n case 'file':\n return typeof value.url === 'string' && typeof value.name === 'string'\n ? {\n type: 'file',\n url: value.url,\n name: value.name,\n ...(isPositiveNumber(value.size) && { size: value.size }),\n ...(typeof value.mimeType === 'string' && {\n mimeType: value.mimeType,\n }),\n }\n : null\n case 'break':\n return { type: 'break' }\n case 'custom':\n return typeof value.kind === 'string' && isRecord(value.data)\n ? {\n type: 'custom',\n kind: value.kind,\n data: cloneRecord(value.data),\n ...(typeof value.fallback === 'string' && {\n fallback: value.fallback,\n }),\n }\n : fallbackBlock(value)\n default:\n return fallbackBlock(value)\n }\n}\n\nfunction fallbackBlock(\n value: Record<string, unknown>\n): ChatContentBlock | null {\n return typeof value.fallback === 'string'\n ? { type: 'text', text: value.fallback }\n : null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isPositiveNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n}\n\nfunction cloneUser(user: ChatUser): ChatUser {\n return { ...user }\n}\n\nfunction cloneMention(mention: MentionItem): MentionItem {\n return { ...mention }\n}\n\nfunction cloneImage(image: ImageAttachment): ImageAttachment {\n return { ...image }\n}\n\nfunction cloneFile(file: FileAttachment): FileAttachment {\n return { ...file }\n}\n\nfunction cloneRecord<T>(value: T, seen = new WeakMap<object, unknown>()): T {\n if (!value || typeof value !== 'object') return value\n if (seen.has(value)) return seen.get(value) as T\n if (Array.isArray(value)) {\n const clone: unknown[] = []\n seen.set(value, clone)\n value.forEach(item => clone.push(cloneRecord(item, seen)))\n return clone as T\n }\n\n const prototype = Object.getPrototypeOf(value)\n if (prototype !== Object.prototype && prototype !== null) return value\n const clone: Record<string, unknown> = {}\n seen.set(value, clone)\n Object.entries(value).forEach(([key, item]) => {\n clone[key] = cloneRecord(item, seen)\n })\n return clone as T\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChatContentBlock, ChatMessage } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 归一化消息并复制所有可变字段。
|
|
4
|
+
*
|
|
5
|
+
* 该函数是 transport、outbox、ChatManager 共享的边界:外部数据先经过
|
|
6
|
+
* schema/version 校验,再进入内存状态,避免调用方继续修改内部消息。
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeChatMessage(message: ChatMessage): ChatMessage;
|
|
9
|
+
/**
|
|
10
|
+
* 对来自网络/持久化的 content block 做运行时白名单归一化。
|
|
11
|
+
* 不认识的块优先使用 fallback 文本,否则丢弃,避免将任意对象带入渲染器。
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeChatContentBlocks(blocks: unknown[]): ChatContentBlock[];
|
|
14
|
+
/** 归一化消息集合,并在进入状态层前拒绝重复的消息 ID/client ID。 */
|
|
15
|
+
export declare function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[];
|
|
16
|
+
/** 复制消息,用于事件 payload、outbox 与公开读取,避免可变引用泄漏。 */
|
|
17
|
+
export declare function cloneChatMessage(message: ChatMessage): ChatMessage;
|
|
18
|
+
/** 根据服务端 sequence/timestamp 给出稳定排序值。 */
|
|
19
|
+
export declare function compareChatMessages(left: ChatMessage, right: ChatMessage): number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChatContentBlock, ChatMessage } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 归一化消息并复制所有可变字段。
|
|
4
|
+
*
|
|
5
|
+
* 该函数是 transport、outbox、ChatManager 共享的边界:外部数据先经过
|
|
6
|
+
* schema/version 校验,再进入内存状态,避免调用方继续修改内部消息。
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeChatMessage(message: ChatMessage): ChatMessage;
|
|
9
|
+
/**
|
|
10
|
+
* 对来自网络/持久化的 content block 做运行时白名单归一化。
|
|
11
|
+
* 不认识的块优先使用 fallback 文本,否则丢弃,避免将任意对象带入渲染器。
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeChatContentBlocks(blocks: unknown[]): ChatContentBlock[];
|
|
14
|
+
/** 归一化消息集合,并在进入状态层前拒绝重复的消息 ID/client ID。 */
|
|
15
|
+
export declare function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[];
|
|
16
|
+
/** 复制消息,用于事件 payload、outbox 与公开读取,避免可变引用泄漏。 */
|
|
17
|
+
export declare function cloneChatMessage(message: ChatMessage): ChatMessage;
|
|
18
|
+
/** 根据服务端 sequence/timestamp 给出稳定排序值。 */
|
|
19
|
+
export declare function compareChatMessages(left: ChatMessage, right: ChatMessage): number;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/chat-core v0.0.2 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-10-12 21:52:18 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
const require_message_schema = require("./message-schema.cjs");
|
|
12
|
+
//#region src/core/outbox.ts
|
|
13
|
+
var MemoryChatOutboxStorage = class {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.entries = [];
|
|
16
|
+
}
|
|
17
|
+
load() {
|
|
18
|
+
return cloneEntries(this.entries);
|
|
19
|
+
}
|
|
20
|
+
save(entries) {
|
|
21
|
+
this.entries = cloneEntries(entries);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var ChatOutbox = class {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.entries = [];
|
|
27
|
+
this.hydrated = false;
|
|
28
|
+
this.writePromise = Promise.resolve();
|
|
29
|
+
this.storage = options.storage ?? new MemoryChatOutboxStorage();
|
|
30
|
+
this.now = options.now ?? Date.now;
|
|
31
|
+
}
|
|
32
|
+
async hydrate() {
|
|
33
|
+
if (!this.hydrated) {
|
|
34
|
+
this.entries = (await this.storage.load()).map(normalizeEntry);
|
|
35
|
+
this.hydrated = true;
|
|
36
|
+
}
|
|
37
|
+
return this.list();
|
|
38
|
+
}
|
|
39
|
+
list() {
|
|
40
|
+
return cloneEntries(this.entries);
|
|
41
|
+
}
|
|
42
|
+
async enqueue(message) {
|
|
43
|
+
await this.hydrate();
|
|
44
|
+
const normalized = require_message_schema.normalizeChatMessage({
|
|
45
|
+
...message,
|
|
46
|
+
clientMessageId: message.clientMessageId ?? message.id
|
|
47
|
+
});
|
|
48
|
+
const key = getOutboxKey(normalized);
|
|
49
|
+
const existing = this.entries.find((entry2) => entry2.id === key);
|
|
50
|
+
if (existing) return cloneEntry(existing);
|
|
51
|
+
const entry = {
|
|
52
|
+
id: key,
|
|
53
|
+
message: normalized,
|
|
54
|
+
enqueuedAt: this.now(),
|
|
55
|
+
attempts: 0,
|
|
56
|
+
state: "pending"
|
|
57
|
+
};
|
|
58
|
+
this.entries.push(entry);
|
|
59
|
+
await this.persist();
|
|
60
|
+
return cloneEntry(entry);
|
|
61
|
+
}
|
|
62
|
+
async remove(id) {
|
|
63
|
+
await this.hydrate();
|
|
64
|
+
const index = this.entries.findIndex((entry) => entry.id === id);
|
|
65
|
+
if (index < 0) return false;
|
|
66
|
+
this.entries.splice(index, 1);
|
|
67
|
+
await this.persist();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
async retry(id) {
|
|
71
|
+
await this.hydrate();
|
|
72
|
+
for (const entry of this.entries) if (!id || entry.id === id) {
|
|
73
|
+
entry.state = "pending";
|
|
74
|
+
entry.lastError = void 0;
|
|
75
|
+
}
|
|
76
|
+
await this.persist();
|
|
77
|
+
}
|
|
78
|
+
async clear() {
|
|
79
|
+
await this.hydrate();
|
|
80
|
+
this.entries = [];
|
|
81
|
+
await this.persist();
|
|
82
|
+
}
|
|
83
|
+
async flush(send, options = {}) {
|
|
84
|
+
await this.hydrate();
|
|
85
|
+
const maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? 3));
|
|
86
|
+
const concurrency = Math.min(8, Math.max(1, Math.floor(options.concurrency ?? 1)));
|
|
87
|
+
const candidates = this.entries.filter((entry) => entry.state === "pending");
|
|
88
|
+
const sent = [];
|
|
89
|
+
const failed = [];
|
|
90
|
+
let cursor = 0;
|
|
91
|
+
const worker = async () => {
|
|
92
|
+
while (cursor < candidates.length) {
|
|
93
|
+
if (options.signal?.aborted) throw createAbortError();
|
|
94
|
+
const entry = candidates[cursor++];
|
|
95
|
+
entry.attempts += 1;
|
|
96
|
+
try {
|
|
97
|
+
await send(entry.message, {
|
|
98
|
+
signal: options.signal,
|
|
99
|
+
clientMessageId: entry.message.clientMessageId ?? entry.id
|
|
100
|
+
});
|
|
101
|
+
const index = this.entries.findIndex((item) => item.id === entry.id);
|
|
102
|
+
if (index >= 0) this.entries.splice(index, 1);
|
|
103
|
+
sent.push(entry.id);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
entry.lastError = toErrorMessage(error);
|
|
106
|
+
if (entry.attempts >= maxAttempts) entry.state = "failed";
|
|
107
|
+
failed.push({
|
|
108
|
+
id: entry.id,
|
|
109
|
+
error
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
await this.persist();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
116
|
+
return {
|
|
117
|
+
sent,
|
|
118
|
+
failed,
|
|
119
|
+
remaining: this.list()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async persist() {
|
|
123
|
+
const snapshot = cloneEntries(this.entries);
|
|
124
|
+
const previous = this.writePromise;
|
|
125
|
+
this.writePromise = previous.then(() => this.storage.save(snapshot));
|
|
126
|
+
await this.writePromise;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function getOutboxKey(message) {
|
|
130
|
+
return message.clientMessageId ?? message.id;
|
|
131
|
+
}
|
|
132
|
+
function cloneEntry(entry) {
|
|
133
|
+
return {
|
|
134
|
+
...entry,
|
|
135
|
+
message: require_message_schema.cloneChatMessage(entry.message)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function cloneEntries(entries) {
|
|
139
|
+
return entries.map(cloneEntry);
|
|
140
|
+
}
|
|
141
|
+
function normalizeEntry(entry) {
|
|
142
|
+
const message = require_message_schema.normalizeChatMessage(entry.message);
|
|
143
|
+
return {
|
|
144
|
+
id: typeof entry.id === "string" && entry.id ? entry.id : getOutboxKey(message),
|
|
145
|
+
message,
|
|
146
|
+
enqueuedAt: Number.isFinite(entry.enqueuedAt) ? entry.enqueuedAt : Date.now(),
|
|
147
|
+
attempts: Number.isFinite(entry.attempts) ? Math.max(0, Math.floor(entry.attempts)) : 0,
|
|
148
|
+
state: entry.state === "failed" ? "failed" : "pending",
|
|
149
|
+
...typeof entry.lastError === "string" && { lastError: entry.lastError }
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function toErrorMessage(error) {
|
|
153
|
+
return error instanceof Error ? error.message : String(error);
|
|
154
|
+
}
|
|
155
|
+
function createAbortError() {
|
|
156
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
exports.ChatOutbox = ChatOutbox;
|
|
160
|
+
exports.MemoryChatOutboxStorage = MemoryChatOutboxStorage;
|
|
161
|
+
/*! End of @composy/chat-core | Powered by @composy/builder */
|
|
162
|
+
//# sourceMappingURL=outbox.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.cjs","names":["entry"],"sources":["../../src/core/outbox.ts"],"sourcesContent":["import type { ChatMessage } from '../types'\nimport { cloneChatMessage, normalizeChatMessage } from './message-schema'\n\n/** 离线 outbox 中的一条待发送消息。 */\nexport interface ChatOutboxEntry {\n id: string\n message: ChatMessage\n enqueuedAt: number\n attempts: number\n state: 'pending' | 'failed'\n lastError?: string\n}\n\n/** outbox 持久化适配器;可接 IndexedDB、SQLite bridge 或业务缓存。 */\nexport interface ChatOutboxStorage {\n load: () => ChatOutboxEntry[] | Promise<ChatOutboxEntry[]>\n save: (entries: ChatOutboxEntry[]) => void | Promise<void>\n}\n\n/** 内存适配器,适合默认运行和单元测试。 */\nexport class MemoryChatOutboxStorage implements ChatOutboxStorage {\n private entries: ChatOutboxEntry[] = []\n\n load(): ChatOutboxEntry[] {\n return cloneEntries(this.entries)\n }\n\n save(entries: ChatOutboxEntry[]): void {\n this.entries = cloneEntries(entries)\n }\n}\n\nexport interface ChatOutboxOptions {\n storage?: ChatOutboxStorage\n now?: () => number\n}\n\nexport interface ChatOutboxFlushOptions {\n signal?: AbortSignal\n maxAttempts?: number\n concurrency?: number\n}\n\nexport interface ChatOutboxFlushResult {\n sent: string[]\n failed: Array<{ id: string; error: unknown }>\n remaining: ChatOutboxEntry[]\n}\n\n/**\n * 幂等离线队列。\n *\n * 队列只负责保存和重试,不假设网络协议;发送函数由 ChatTransportController\n * 或业务层注入。每条消息以 clientMessageId 优先作为幂等键,重复 enqueue 不会产生重复项。\n */\nexport class ChatOutbox {\n private readonly storage: ChatOutboxStorage\n private readonly now: () => number\n private entries: ChatOutboxEntry[] = []\n private hydrated = false\n private writePromise: Promise<void> = Promise.resolve()\n\n constructor(options: ChatOutboxOptions = {}) {\n this.storage = options.storage ?? new MemoryChatOutboxStorage()\n this.now = options.now ?? Date.now\n }\n\n async hydrate(): Promise<ChatOutboxEntry[]> {\n if (!this.hydrated) {\n this.entries = (await this.storage.load()).map(normalizeEntry)\n this.hydrated = true\n }\n return this.list()\n }\n\n list(): ChatOutboxEntry[] {\n return cloneEntries(this.entries)\n }\n\n async enqueue(message: ChatMessage): Promise<ChatOutboxEntry> {\n await this.hydrate()\n const normalized = normalizeChatMessage({\n ...message,\n clientMessageId: message.clientMessageId ?? message.id,\n })\n const key = getOutboxKey(normalized)\n const existing = this.entries.find(entry => entry.id === key)\n if (existing) return cloneEntry(existing)\n\n const entry: ChatOutboxEntry = {\n id: key,\n message: normalized,\n enqueuedAt: this.now(),\n attempts: 0,\n state: 'pending',\n }\n this.entries.push(entry)\n await this.persist()\n return cloneEntry(entry)\n }\n\n async remove(id: string): Promise<boolean> {\n await this.hydrate()\n const index = this.entries.findIndex(entry => entry.id === id)\n if (index < 0) return false\n this.entries.splice(index, 1)\n await this.persist()\n return true\n }\n\n async retry(id?: string): Promise<void> {\n await this.hydrate()\n for (const entry of this.entries) {\n if (!id || entry.id === id) {\n entry.state = 'pending'\n entry.lastError = undefined\n }\n }\n await this.persist()\n }\n\n async clear(): Promise<void> {\n await this.hydrate()\n this.entries = []\n await this.persist()\n }\n\n async flush(\n send: (\n message: ChatMessage,\n options: { signal?: AbortSignal; clientMessageId: string }\n ) => Promise<unknown>,\n options: ChatOutboxFlushOptions = {}\n ): Promise<ChatOutboxFlushResult> {\n await this.hydrate()\n const maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? 3))\n const concurrency = Math.min(\n 8,\n Math.max(1, Math.floor(options.concurrency ?? 1))\n )\n const candidates = this.entries.filter(entry => entry.state === 'pending')\n const sent: string[] = []\n const failed: Array<{ id: string; error: unknown }> = []\n let cursor = 0\n\n const worker = async () => {\n while (cursor < candidates.length) {\n if (options.signal?.aborted) throw createAbortError()\n const entry = candidates[cursor++]\n entry.attempts += 1\n try {\n await send(entry.message, {\n signal: options.signal,\n clientMessageId: entry.message.clientMessageId ?? entry.id,\n })\n const index = this.entries.findIndex(item => item.id === entry.id)\n if (index >= 0) this.entries.splice(index, 1)\n sent.push(entry.id)\n } catch (error) {\n entry.lastError = toErrorMessage(error)\n if (entry.attempts >= maxAttempts) entry.state = 'failed'\n failed.push({ id: entry.id, error })\n }\n await this.persist()\n }\n }\n\n await Promise.all(Array.from({ length: concurrency }, () => worker()))\n return { sent, failed, remaining: this.list() }\n }\n\n private async persist(): Promise<void> {\n const snapshot = cloneEntries(this.entries)\n const previous = this.writePromise\n this.writePromise = previous.then(() => this.storage.save(snapshot))\n await this.writePromise\n }\n}\n\nfunction getOutboxKey(message: ChatMessage): string {\n return message.clientMessageId ?? message.id\n}\n\nfunction cloneEntry(entry: ChatOutboxEntry): ChatOutboxEntry {\n return {\n ...entry,\n message: cloneChatMessage(entry.message),\n }\n}\n\nfunction cloneEntries(entries: ChatOutboxEntry[]): ChatOutboxEntry[] {\n return entries.map(cloneEntry)\n}\n\nfunction normalizeEntry(entry: ChatOutboxEntry): ChatOutboxEntry {\n const message = normalizeChatMessage(entry.message)\n const id =\n typeof entry.id === 'string' && entry.id ? entry.id : getOutboxKey(message)\n const enqueuedAt = Number.isFinite(entry.enqueuedAt)\n ? entry.enqueuedAt\n : Date.now()\n const attempts = Number.isFinite(entry.attempts)\n ? Math.max(0, Math.floor(entry.attempts))\n : 0\n const state = entry.state === 'failed' ? 'failed' : 'pending'\n return {\n id,\n message,\n enqueuedAt,\n attempts,\n state,\n ...(typeof entry.lastError === 'string' && {\n lastError: entry.lastError,\n }),\n }\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nfunction createAbortError(): DOMException {\n return new DOMException('The operation was aborted.', 'AbortError')\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types';
|
|
2
|
+
/** 离线 outbox 中的一条待发送消息。 */
|
|
3
|
+
export interface ChatOutboxEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
message: ChatMessage;
|
|
6
|
+
enqueuedAt: number;
|
|
7
|
+
attempts: number;
|
|
8
|
+
state: 'pending' | 'failed';
|
|
9
|
+
lastError?: string;
|
|
10
|
+
}
|
|
11
|
+
/** outbox 持久化适配器;可接 IndexedDB、SQLite bridge 或业务缓存。 */
|
|
12
|
+
export interface ChatOutboxStorage {
|
|
13
|
+
load: () => ChatOutboxEntry[] | Promise<ChatOutboxEntry[]>;
|
|
14
|
+
save: (entries: ChatOutboxEntry[]) => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** 内存适配器,适合默认运行和单元测试。 */
|
|
17
|
+
export declare class MemoryChatOutboxStorage implements ChatOutboxStorage {
|
|
18
|
+
private entries;
|
|
19
|
+
load(): ChatOutboxEntry[];
|
|
20
|
+
save(entries: ChatOutboxEntry[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatOutboxOptions {
|
|
23
|
+
storage?: ChatOutboxStorage;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
export interface ChatOutboxFlushOptions {
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
maxAttempts?: number;
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ChatOutboxFlushResult {
|
|
32
|
+
sent: string[];
|
|
33
|
+
failed: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
error: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
remaining: ChatOutboxEntry[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 幂等离线队列。
|
|
41
|
+
*
|
|
42
|
+
* 队列只负责保存和重试,不假设网络协议;发送函数由 ChatTransportController
|
|
43
|
+
* 或业务层注入。每条消息以 clientMessageId 优先作为幂等键,重复 enqueue 不会产生重复项。
|
|
44
|
+
*/
|
|
45
|
+
export declare class ChatOutbox {
|
|
46
|
+
private readonly storage;
|
|
47
|
+
private readonly now;
|
|
48
|
+
private entries;
|
|
49
|
+
private hydrated;
|
|
50
|
+
private writePromise;
|
|
51
|
+
constructor(options?: ChatOutboxOptions);
|
|
52
|
+
hydrate(): Promise<ChatOutboxEntry[]>;
|
|
53
|
+
list(): ChatOutboxEntry[];
|
|
54
|
+
enqueue(message: ChatMessage): Promise<ChatOutboxEntry>;
|
|
55
|
+
remove(id: string): Promise<boolean>;
|
|
56
|
+
retry(id?: string): Promise<void>;
|
|
57
|
+
clear(): Promise<void>;
|
|
58
|
+
flush(send: (message: ChatMessage, options: {
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
clientMessageId: string;
|
|
61
|
+
}) => Promise<unknown>, options?: ChatOutboxFlushOptions): Promise<ChatOutboxFlushResult>;
|
|
62
|
+
private persist;
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types';
|
|
2
|
+
/** 离线 outbox 中的一条待发送消息。 */
|
|
3
|
+
export interface ChatOutboxEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
message: ChatMessage;
|
|
6
|
+
enqueuedAt: number;
|
|
7
|
+
attempts: number;
|
|
8
|
+
state: 'pending' | 'failed';
|
|
9
|
+
lastError?: string;
|
|
10
|
+
}
|
|
11
|
+
/** outbox 持久化适配器;可接 IndexedDB、SQLite bridge 或业务缓存。 */
|
|
12
|
+
export interface ChatOutboxStorage {
|
|
13
|
+
load: () => ChatOutboxEntry[] | Promise<ChatOutboxEntry[]>;
|
|
14
|
+
save: (entries: ChatOutboxEntry[]) => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** 内存适配器,适合默认运行和单元测试。 */
|
|
17
|
+
export declare class MemoryChatOutboxStorage implements ChatOutboxStorage {
|
|
18
|
+
private entries;
|
|
19
|
+
load(): ChatOutboxEntry[];
|
|
20
|
+
save(entries: ChatOutboxEntry[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatOutboxOptions {
|
|
23
|
+
storage?: ChatOutboxStorage;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
export interface ChatOutboxFlushOptions {
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
maxAttempts?: number;
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ChatOutboxFlushResult {
|
|
32
|
+
sent: string[];
|
|
33
|
+
failed: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
error: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
remaining: ChatOutboxEntry[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 幂等离线队列。
|
|
41
|
+
*
|
|
42
|
+
* 队列只负责保存和重试,不假设网络协议;发送函数由 ChatTransportController
|
|
43
|
+
* 或业务层注入。每条消息以 clientMessageId 优先作为幂等键,重复 enqueue 不会产生重复项。
|
|
44
|
+
*/
|
|
45
|
+
export declare class ChatOutbox {
|
|
46
|
+
private readonly storage;
|
|
47
|
+
private readonly now;
|
|
48
|
+
private entries;
|
|
49
|
+
private hydrated;
|
|
50
|
+
private writePromise;
|
|
51
|
+
constructor(options?: ChatOutboxOptions);
|
|
52
|
+
hydrate(): Promise<ChatOutboxEntry[]>;
|
|
53
|
+
list(): ChatOutboxEntry[];
|
|
54
|
+
enqueue(message: ChatMessage): Promise<ChatOutboxEntry>;
|
|
55
|
+
remove(id: string): Promise<boolean>;
|
|
56
|
+
retry(id?: string): Promise<void>;
|
|
57
|
+
clear(): Promise<void>;
|
|
58
|
+
flush(send: (message: ChatMessage, options: {
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
clientMessageId: string;
|
|
61
|
+
}) => Promise<unknown>, options?: ChatOutboxFlushOptions): Promise<ChatOutboxFlushResult>;
|
|
62
|
+
private persist;
|
|
63
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/chat-core v0.0.2 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-10-12 21:52:18 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
//#region src/core/transport.ts
|
|
12
|
+
var ChatTransportController = class {
|
|
13
|
+
constructor(transport, options = {}) {
|
|
14
|
+
this.status = "idle";
|
|
15
|
+
this.unsubscribeEvents = null;
|
|
16
|
+
this.unsubscribeStatus = null;
|
|
17
|
+
this.destroyed = false;
|
|
18
|
+
this.connectPromise = null;
|
|
19
|
+
this.lifecycle = 0;
|
|
20
|
+
this.transport = transport;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.cursor = options.cursor;
|
|
23
|
+
}
|
|
24
|
+
getStatus() {
|
|
25
|
+
return this.status;
|
|
26
|
+
}
|
|
27
|
+
getCursor() {
|
|
28
|
+
return this.cursor;
|
|
29
|
+
}
|
|
30
|
+
async connect(signal) {
|
|
31
|
+
if (this.destroyed) throw new Error("Chat transport controller is disposed.");
|
|
32
|
+
if (this.status === "connected") return;
|
|
33
|
+
if (this.connectPromise) return this.connectPromise;
|
|
34
|
+
const lifecycle = ++this.lifecycle;
|
|
35
|
+
this.setStatus("connecting");
|
|
36
|
+
this.connectPromise = (async () => {
|
|
37
|
+
this.unsubscribeEvents?.();
|
|
38
|
+
this.unsubscribeStatus?.();
|
|
39
|
+
this.unsubscribeEvents = this.transport.subscribe((event) => this.handleEvent(event));
|
|
40
|
+
this.unsubscribeStatus = this.transport.onStatusChange?.((status) => this.setStatus(status)) ?? null;
|
|
41
|
+
try {
|
|
42
|
+
await this.transport.connect({
|
|
43
|
+
conversationId: this.options.conversationId,
|
|
44
|
+
cursor: this.cursor,
|
|
45
|
+
signal
|
|
46
|
+
});
|
|
47
|
+
if (this.destroyed || lifecycle !== this.lifecycle) return;
|
|
48
|
+
this.setStatus("connected");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!this.destroyed && lifecycle === this.lifecycle) this.setStatus("error");
|
|
51
|
+
throw error;
|
|
52
|
+
} finally {
|
|
53
|
+
this.connectPromise = null;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
return this.connectPromise;
|
|
57
|
+
}
|
|
58
|
+
async disconnect() {
|
|
59
|
+
if (this.destroyed) return;
|
|
60
|
+
this.lifecycle += 1;
|
|
61
|
+
this.connectPromise = null;
|
|
62
|
+
try {
|
|
63
|
+
await this.transport.disconnect();
|
|
64
|
+
} finally {
|
|
65
|
+
this.setStatus("disconnected");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async send(message, options = {}) {
|
|
69
|
+
if (this.destroyed) throw new Error("Chat transport controller is disposed.");
|
|
70
|
+
if (this.status !== "connected") throw new Error(`Chat transport is not connected: ${this.status}`);
|
|
71
|
+
const result = await this.transport.send(message, options);
|
|
72
|
+
if (result && result.cursor) this.cursor = result.cursor;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
dispose() {
|
|
76
|
+
if (this.destroyed) return;
|
|
77
|
+
this.destroyed = true;
|
|
78
|
+
this.lifecycle += 1;
|
|
79
|
+
this.unsubscribeEvents?.();
|
|
80
|
+
this.unsubscribeStatus?.();
|
|
81
|
+
this.unsubscribeEvents = null;
|
|
82
|
+
this.unsubscribeStatus = null;
|
|
83
|
+
this.connectPromise = null;
|
|
84
|
+
Promise.resolve(this.transport.disconnect()).catch(() => void 0);
|
|
85
|
+
this.setStatus("disconnected");
|
|
86
|
+
}
|
|
87
|
+
handleEvent(event) {
|
|
88
|
+
if (this.destroyed) return;
|
|
89
|
+
if ("cursor" in event && event.cursor) this.cursor = event.cursor;
|
|
90
|
+
if (event.type === "ack" && event.ack.cursor) this.cursor = event.ack.cursor;
|
|
91
|
+
if (event.type === "error" && !event.recoverable) this.setStatus("error");
|
|
92
|
+
this.options.onEvent?.(event);
|
|
93
|
+
}
|
|
94
|
+
setStatus(status) {
|
|
95
|
+
if (this.status === status) return;
|
|
96
|
+
this.status = status;
|
|
97
|
+
this.options.onStatusChange?.(status);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
exports.ChatTransportController = ChatTransportController;
|
|
102
|
+
/*! End of @composy/chat-core | Powered by @composy/builder */
|
|
103
|
+
//# sourceMappingURL=transport.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.cjs","names":[],"sources":["../../src/core/transport.ts"],"sourcesContent":["import type { ChatMessage } from '../types'\n\n/** transport 连接状态。 */\nexport type ChatTransportStatus =\n 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'\n\n/** 建立 transport 时传入的同步上下文。 */\nexport interface ChatTransportContext {\n conversationId?: string\n cursor?: string\n signal?: AbortSignal\n}\n\n/** transport ack,可携带服务端补全后的消息和同步序号。 */\nexport interface ChatTransportAck {\n clientMessageId: string\n message?: Partial<ChatMessage>\n sequence?: number\n cursor?: string\n}\n\n/** transport 推送的统一事件协议。 */\nexport type ChatTransportEvent =\n | {\n type: 'message'\n message: ChatMessage\n sequence?: number\n cursor?: string\n }\n | { type: 'ack'; ack: ChatTransportAck }\n | { type: 'cursor'; cursor: string }\n | { type: 'error'; error: unknown; recoverable?: boolean }\n\n/** 可由 HTTP/SSE/WebSocket/原生 App bridge 实现的最小 transport 协议。 */\nexport interface ChatTransport {\n connect: (context?: ChatTransportContext) => void | Promise<void>\n disconnect: () => void | Promise<void>\n send: (\n message: ChatMessage,\n options?: { signal?: AbortSignal; clientMessageId?: string }\n ) => Promise<ChatTransportAck | void>\n subscribe: (listener: (event: ChatTransportEvent) => void) => () => void\n onStatusChange?: (\n listener: (status: ChatTransportStatus) => void\n ) => () => void\n}\n\nexport interface ChatTransportControllerOptions {\n conversationId?: string\n cursor?: string\n onEvent?: (event: ChatTransportEvent) => void\n onStatusChange?: (status: ChatTransportStatus) => void\n}\n\n/**\n * 连接生命周期、游标和外部 transport 事件的统一控制器。\n *\n * Controller 不假设网络实现,负责避免重复 connect、转发状态、保存 cursor,\n * 并在 dispose 后拒绝迟到的异步结果。\n */\nexport class ChatTransportController {\n private readonly transport: ChatTransport\n private readonly options: ChatTransportControllerOptions\n private status: ChatTransportStatus = 'idle'\n private cursor?: string\n private unsubscribeEvents: (() => void) | null = null\n private unsubscribeStatus: (() => void) | null = null\n private destroyed = false\n private connectPromise: Promise<void> | null = null\n private lifecycle = 0\n\n constructor(\n transport: ChatTransport,\n options: ChatTransportControllerOptions = {}\n ) {\n this.transport = transport\n this.options = options\n this.cursor = options.cursor\n }\n\n getStatus(): ChatTransportStatus {\n return this.status\n }\n\n getCursor(): string | undefined {\n return this.cursor\n }\n\n async connect(signal?: AbortSignal): Promise<void> {\n if (this.destroyed)\n throw new Error('Chat transport controller is disposed.')\n if (this.status === 'connected') return\n if (this.connectPromise) return this.connectPromise\n\n const lifecycle = ++this.lifecycle\n this.setStatus('connecting')\n this.connectPromise = (async () => {\n this.unsubscribeEvents?.()\n this.unsubscribeStatus?.()\n this.unsubscribeEvents = this.transport.subscribe(event =>\n this.handleEvent(event)\n )\n this.unsubscribeStatus =\n this.transport.onStatusChange?.(status => this.setStatus(status)) ??\n null\n try {\n await this.transport.connect({\n conversationId: this.options.conversationId,\n cursor: this.cursor,\n signal,\n })\n if (this.destroyed || lifecycle !== this.lifecycle) return\n this.setStatus('connected')\n } catch (error) {\n if (!this.destroyed && lifecycle === this.lifecycle) {\n this.setStatus('error')\n }\n throw error\n } finally {\n this.connectPromise = null\n }\n })()\n\n return this.connectPromise\n }\n\n async disconnect(): Promise<void> {\n if (this.destroyed) return\n this.lifecycle += 1\n this.connectPromise = null\n try {\n await this.transport.disconnect()\n } finally {\n this.setStatus('disconnected')\n }\n }\n\n async send(\n message: ChatMessage,\n options: { signal?: AbortSignal; clientMessageId?: string } = {}\n ): Promise<ChatTransportAck | void> {\n if (this.destroyed)\n throw new Error('Chat transport controller is disposed.')\n if (this.status !== 'connected') {\n throw new Error(`Chat transport is not connected: ${this.status}`)\n }\n const result = await this.transport.send(message, options)\n if (result && result.cursor) this.cursor = result.cursor\n return result\n }\n\n dispose(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.lifecycle += 1\n this.unsubscribeEvents?.()\n this.unsubscribeStatus?.()\n this.unsubscribeEvents = null\n this.unsubscribeStatus = null\n this.connectPromise = null\n void Promise.resolve(this.transport.disconnect()).catch(() => undefined)\n this.setStatus('disconnected')\n }\n\n private handleEvent(event: ChatTransportEvent): void {\n if (this.destroyed) return\n if ('cursor' in event && event.cursor) this.cursor = event.cursor\n if (event.type === 'ack' && event.ack.cursor) {\n this.cursor = event.ack.cursor\n }\n if (event.type === 'error' && !event.recoverable) this.setStatus('error')\n this.options.onEvent?.(event)\n }\n\n private setStatus(status: ChatTransportStatus): void {\n if (this.status === status) return\n this.status = status\n this.options.onStatusChange?.(status)\n }\n}\n"],"mappings":";;;;;;;;;;;AA4DO,IAAM,0BAAN,MAA8B;CAWnC,YACE,WACA,UAA0C,CAAC,GAC3C;EAXF,KAAQ,SAA8B;EAEtC,KAAQ,oBAAyC;EACjD,KAAQ,oBAAyC;EACjD,KAAQ,YAAY;EACpB,KAAQ,iBAAuC;EAC/C,KAAQ,YAAY;EAMlB,KAAK,YAAY;EACjB,KAAK,UAAU;EACf,KAAK,SAAS,QAAQ;CACxB;CAEA,YAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,YAAgC;EAC9B,OAAO,KAAK;CACd;CAEA,MAAM,QAAQ,QAAqC;EACjD,IAAI,KAAK,WACP,MAAM,IAAI,MAAM,wCAAwC;EAC1D,IAAI,KAAK,WAAW,aAAa;EACjC,IAAI,KAAK,gBAAgB,OAAO,KAAK;EAErC,MAAM,YAAY,EAAE,KAAK;EACzB,KAAK,UAAU,YAAY;EAC3B,KAAK,kBAAkB,YAAY;GACjC,KAAK,oBAAoB;GACzB,KAAK,oBAAoB;GACzB,KAAK,oBAAoB,KAAK,UAAU,WAAU,UAChD,KAAK,YAAY,KAAK,CACxB;GACA,KAAK,oBACH,KAAK,UAAU,kBAAiB,WAAU,KAAK,UAAU,MAAM,CAAC,KAChE;GACF,IAAI;IACF,MAAM,KAAK,UAAU,QAAQ;KAC3B,gBAAgB,KAAK,QAAQ;KAC7B,QAAQ,KAAK;KACb;IACF,CAAC;IACD,IAAI,KAAK,aAAa,cAAc,KAAK,WAAW;IACpD,KAAK,UAAU,WAAW;GAC5B,SAAS,OAAO;IACd,IAAI,CAAC,KAAK,aAAa,cAAc,KAAK,WACxC,KAAK,UAAU,OAAO;IAExB,MAAM;GACR,UAAE;IACA,KAAK,iBAAiB;GACxB;EACF,EAAA,CAAG;EAEH,OAAO,KAAK;CACd;CAEA,MAAM,aAA4B;EAChC,IAAI,KAAK,WAAW;EACpB,KAAK,aAAa;EAClB,KAAK,iBAAiB;EACtB,IAAI;GACF,MAAM,KAAK,UAAU,WAAW;EAClC,UAAE;GACA,KAAK,UAAU,cAAc;EAC/B;CACF;CAEA,MAAM,KACJ,SACA,UAA8D,CAAC,GAC7B;EAClC,IAAI,KAAK,WACP,MAAM,IAAI,MAAM,wCAAwC;EAC1D,IAAI,KAAK,WAAW,aAClB,MAAM,IAAI,MAAM,oCAAoC,KAAK,QAAQ;EAEnE,MAAM,SAAS,MAAM,KAAK,UAAU,KAAK,SAAS,OAAO;EACzD,IAAI,UAAU,OAAO,QAAQ,KAAK,SAAS,OAAO;EAClD,OAAO;CACT;CAEA,UAAgB;EACd,IAAI,KAAK,WAAW;EACpB,KAAK,YAAY;EACjB,KAAK,aAAa;EAClB,KAAK,oBAAoB;EACzB,KAAK,oBAAoB;EACzB,KAAK,oBAAoB;EACzB,KAAK,oBAAoB;EACzB,KAAK,iBAAiB;EACtB,QAAa,QAAQ,KAAK,UAAU,WAAW,CAAC,CAAA,CAAE,YAAY,KAAA,CAAS;EACvE,KAAK,UAAU,cAAc;CAC/B;CAEQ,YAAY,OAAiC;EACnD,IAAI,KAAK,WAAW;EACpB,IAAI,YAAY,SAAS,MAAM,QAAQ,KAAK,SAAS,MAAM;EAC3D,IAAI,MAAM,SAAS,SAAS,MAAM,IAAI,QACpC,KAAK,SAAS,MAAM,IAAI;EAE1B,IAAI,MAAM,SAAS,WAAW,CAAC,MAAM,aAAa,KAAK,UAAU,OAAO;EACxE,KAAK,QAAQ,UAAU,KAAK;CAC9B;CAEQ,UAAU,QAAmC;EACnD,IAAI,KAAK,WAAW,QAAQ;EAC5B,KAAK,SAAS;EACd,KAAK,QAAQ,iBAAiB,MAAM;CACtC;AACF"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types';
|
|
2
|
+
/** transport 连接状态。 */
|
|
3
|
+
export type ChatTransportStatus = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
4
|
+
/** 建立 transport 时传入的同步上下文。 */
|
|
5
|
+
export interface ChatTransportContext {
|
|
6
|
+
conversationId?: string;
|
|
7
|
+
cursor?: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
/** transport ack,可携带服务端补全后的消息和同步序号。 */
|
|
11
|
+
export interface ChatTransportAck {
|
|
12
|
+
clientMessageId: string;
|
|
13
|
+
message?: Partial<ChatMessage>;
|
|
14
|
+
sequence?: number;
|
|
15
|
+
cursor?: string;
|
|
16
|
+
}
|
|
17
|
+
/** transport 推送的统一事件协议。 */
|
|
18
|
+
export type ChatTransportEvent = {
|
|
19
|
+
type: 'message';
|
|
20
|
+
message: ChatMessage;
|
|
21
|
+
sequence?: number;
|
|
22
|
+
cursor?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'ack';
|
|
25
|
+
ack: ChatTransportAck;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'cursor';
|
|
28
|
+
cursor: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'error';
|
|
31
|
+
error: unknown;
|
|
32
|
+
recoverable?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** 可由 HTTP/SSE/WebSocket/原生 App bridge 实现的最小 transport 协议。 */
|
|
35
|
+
export interface ChatTransport {
|
|
36
|
+
connect: (context?: ChatTransportContext) => void | Promise<void>;
|
|
37
|
+
disconnect: () => void | Promise<void>;
|
|
38
|
+
send: (message: ChatMessage, options?: {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
clientMessageId?: string;
|
|
41
|
+
}) => Promise<ChatTransportAck | void>;
|
|
42
|
+
subscribe: (listener: (event: ChatTransportEvent) => void) => () => void;
|
|
43
|
+
onStatusChange?: (listener: (status: ChatTransportStatus) => void) => () => void;
|
|
44
|
+
}
|
|
45
|
+
export interface ChatTransportControllerOptions {
|
|
46
|
+
conversationId?: string;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
onEvent?: (event: ChatTransportEvent) => void;
|
|
49
|
+
onStatusChange?: (status: ChatTransportStatus) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 连接生命周期、游标和外部 transport 事件的统一控制器。
|
|
53
|
+
*
|
|
54
|
+
* Controller 不假设网络实现,负责避免重复 connect、转发状态、保存 cursor,
|
|
55
|
+
* 并在 dispose 后拒绝迟到的异步结果。
|
|
56
|
+
*/
|
|
57
|
+
export declare class ChatTransportController {
|
|
58
|
+
private readonly transport;
|
|
59
|
+
private readonly options;
|
|
60
|
+
private status;
|
|
61
|
+
private cursor?;
|
|
62
|
+
private unsubscribeEvents;
|
|
63
|
+
private unsubscribeStatus;
|
|
64
|
+
private destroyed;
|
|
65
|
+
private connectPromise;
|
|
66
|
+
private lifecycle;
|
|
67
|
+
constructor(transport: ChatTransport, options?: ChatTransportControllerOptions);
|
|
68
|
+
getStatus(): ChatTransportStatus;
|
|
69
|
+
getCursor(): string | undefined;
|
|
70
|
+
connect(signal?: AbortSignal): Promise<void>;
|
|
71
|
+
disconnect(): Promise<void>;
|
|
72
|
+
send(message: ChatMessage, options?: {
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
clientMessageId?: string;
|
|
75
|
+
}): Promise<ChatTransportAck | void>;
|
|
76
|
+
dispose(): void;
|
|
77
|
+
private handleEvent;
|
|
78
|
+
private setStatus;
|
|
79
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types';
|
|
2
|
+
/** transport 连接状态。 */
|
|
3
|
+
export type ChatTransportStatus = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
4
|
+
/** 建立 transport 时传入的同步上下文。 */
|
|
5
|
+
export interface ChatTransportContext {
|
|
6
|
+
conversationId?: string;
|
|
7
|
+
cursor?: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
/** transport ack,可携带服务端补全后的消息和同步序号。 */
|
|
11
|
+
export interface ChatTransportAck {
|
|
12
|
+
clientMessageId: string;
|
|
13
|
+
message?: Partial<ChatMessage>;
|
|
14
|
+
sequence?: number;
|
|
15
|
+
cursor?: string;
|
|
16
|
+
}
|
|
17
|
+
/** transport 推送的统一事件协议。 */
|
|
18
|
+
export type ChatTransportEvent = {
|
|
19
|
+
type: 'message';
|
|
20
|
+
message: ChatMessage;
|
|
21
|
+
sequence?: number;
|
|
22
|
+
cursor?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'ack';
|
|
25
|
+
ack: ChatTransportAck;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'cursor';
|
|
28
|
+
cursor: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'error';
|
|
31
|
+
error: unknown;
|
|
32
|
+
recoverable?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** 可由 HTTP/SSE/WebSocket/原生 App bridge 实现的最小 transport 协议。 */
|
|
35
|
+
export interface ChatTransport {
|
|
36
|
+
connect: (context?: ChatTransportContext) => void | Promise<void>;
|
|
37
|
+
disconnect: () => void | Promise<void>;
|
|
38
|
+
send: (message: ChatMessage, options?: {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
clientMessageId?: string;
|
|
41
|
+
}) => Promise<ChatTransportAck | void>;
|
|
42
|
+
subscribe: (listener: (event: ChatTransportEvent) => void) => () => void;
|
|
43
|
+
onStatusChange?: (listener: (status: ChatTransportStatus) => void) => () => void;
|
|
44
|
+
}
|
|
45
|
+
export interface ChatTransportControllerOptions {
|
|
46
|
+
conversationId?: string;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
onEvent?: (event: ChatTransportEvent) => void;
|
|
49
|
+
onStatusChange?: (status: ChatTransportStatus) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 连接生命周期、游标和外部 transport 事件的统一控制器。
|
|
53
|
+
*
|
|
54
|
+
* Controller 不假设网络实现,负责避免重复 connect、转发状态、保存 cursor,
|
|
55
|
+
* 并在 dispose 后拒绝迟到的异步结果。
|
|
56
|
+
*/
|
|
57
|
+
export declare class ChatTransportController {
|
|
58
|
+
private readonly transport;
|
|
59
|
+
private readonly options;
|
|
60
|
+
private status;
|
|
61
|
+
private cursor?;
|
|
62
|
+
private unsubscribeEvents;
|
|
63
|
+
private unsubscribeStatus;
|
|
64
|
+
private destroyed;
|
|
65
|
+
private connectPromise;
|
|
66
|
+
private lifecycle;
|
|
67
|
+
constructor(transport: ChatTransport, options?: ChatTransportControllerOptions);
|
|
68
|
+
getStatus(): ChatTransportStatus;
|
|
69
|
+
getCursor(): string | undefined;
|
|
70
|
+
connect(signal?: AbortSignal): Promise<void>;
|
|
71
|
+
disconnect(): Promise<void>;
|
|
72
|
+
send(message: ChatMessage, options?: {
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
clientMessageId?: string;
|
|
75
|
+
}): Promise<ChatTransportAck | void>;
|
|
76
|
+
dispose(): void;
|
|
77
|
+
private handleEvent;
|
|
78
|
+
private setStatus;
|
|
79
|
+
}
|