simple-ai-sdk 1.0.15 → 1.0.17
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/dist/client/manager.d.ts +18 -9
- package/dist/client/manager.d.ts.map +1 -1
- package/dist/client/manager.js +147 -110
- package/dist/client/types.d.ts +10 -9
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/useChatSession.d.ts +1 -1
- package/dist/client/useChatSession.d.ts.map +1 -1
- package/dist/client/useChatSession.js +4 -6
- package/dist/server/createChatHandler.d.ts.map +1 -1
- package/dist/server/createChatHandler.js +224 -134
- package/dist/server/types.d.ts +9 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shared/wire.d.ts +9 -14
- package/dist/shared/wire.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/client/manager.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import type { Message, JSONValue } from "../shared/index.js";
|
|
|
3
3
|
export interface IStreamManager {
|
|
4
4
|
subscribe(listener: StreamManagerListener): () => void;
|
|
5
5
|
getSnapshot(): StreamManagerState;
|
|
6
|
+
/**
|
|
7
|
+
* セッション情報を読み取り専用で取得する。
|
|
8
|
+
* - 状態参照のためのユーティリティ(副作用は発生しない)。
|
|
9
|
+
*/
|
|
6
10
|
getSession(sessionId: string): ChatSession | undefined;
|
|
7
11
|
initSession(sessionId: string, initialMessages?: Message[]): void;
|
|
8
12
|
/**
|
|
@@ -11,10 +15,6 @@ export interface IStreamManager {
|
|
|
11
15
|
* - コンポーネントがアンマウントされても保持され、完了時に呼ばれる。
|
|
12
16
|
*/
|
|
13
17
|
setOnFinishHandler(sessionId: string, handler: ((messages: Message[]) => void) | undefined): void;
|
|
14
|
-
/**
|
|
15
|
-
* セッション単位で最新の onMetadata ハンドラを登録する。
|
|
16
|
-
*/
|
|
17
|
-
setOnMetadataHandler(sessionId: string, handler: ((metadata: Record<string, JSONValue>) => void) | undefined): void;
|
|
18
18
|
/**
|
|
19
19
|
* セッション単位で最新の onError ハンドラを登録する。
|
|
20
20
|
*/
|
|
@@ -32,7 +32,6 @@ export interface IStreamManager {
|
|
|
32
32
|
* レンダリング中に呼ばれる可能性があるため、通知は行わない。
|
|
33
33
|
*/
|
|
34
34
|
touchSession(sessionId: string): void;
|
|
35
|
-
updateSession(sessionId: string, updates: Partial<ChatSession>): void;
|
|
36
35
|
streamChat(sessionId: string, messages: Message[], options: {
|
|
37
36
|
api: string;
|
|
38
37
|
body?: Record<string, JSONValue>;
|
|
@@ -61,20 +60,17 @@ declare class StreamManager implements IStreamManager {
|
|
|
61
60
|
private rafFallbackTimer;
|
|
62
61
|
private getRAF;
|
|
63
62
|
private getCAF;
|
|
64
|
-
private pendingCloneSessions;
|
|
65
63
|
private maxSessions;
|
|
66
64
|
private debug;
|
|
67
65
|
private ttlMs?;
|
|
68
66
|
private sweepTimer;
|
|
69
67
|
private activeSubscribers;
|
|
70
68
|
private latestOnFinish;
|
|
71
|
-
private latestOnMetadata;
|
|
72
69
|
private latestOnError;
|
|
73
70
|
private streamIds;
|
|
74
71
|
constructor(options?: StreamManagerOptions);
|
|
75
72
|
subscribe(listener: StreamManagerListener): () => void;
|
|
76
73
|
setOnFinishHandler(sessionId: string, handler: ((messages: Message[]) => void) | undefined): void;
|
|
77
|
-
setOnMetadataHandler(sessionId: string, handler: ((metadata: Record<string, JSONValue>) => void) | undefined): void;
|
|
78
74
|
setOnErrorHandler(sessionId: string, handler: ((error: Error) => void) | undefined): void;
|
|
79
75
|
getSnapshot(): StreamManagerState;
|
|
80
76
|
private notify;
|
|
@@ -83,9 +79,22 @@ declare class StreamManager implements IStreamManager {
|
|
|
83
79
|
initSession(sessionId: string, initialMessages?: Message[]): void;
|
|
84
80
|
retainSession(sessionId: string): void;
|
|
85
81
|
releaseSession(sessionId: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* LRU に従いセッションを1件削除する。
|
|
84
|
+
* - 第一候補: 非ストリーミング(ready/error/aborted)かつ購読者なし
|
|
85
|
+
* - 第二候補: 非ストリーミング(購読者有無は問わない)
|
|
86
|
+
* - 進行中(submitted/streaming)は破棄しない(ユーザー操作中断の回避)
|
|
87
|
+
* @returns 削除が行われた場合は true、候補が無い場合は false
|
|
88
|
+
*/
|
|
86
89
|
private removeOldestSession;
|
|
87
90
|
private logSessionInfo;
|
|
88
|
-
|
|
91
|
+
/**
|
|
92
|
+
* セッションの部分更新(内部専用)
|
|
93
|
+
* - 参照の安定性を保ちながら、Map の再生成により変更を通知する
|
|
94
|
+
* - important: messages を更新した場合、messagesVersion をインクリメント
|
|
95
|
+
* - immediate=true の場合、rAF をバイパスして即時通知(ステータス変更等のUX向上)
|
|
96
|
+
*/
|
|
97
|
+
private updateSession;
|
|
89
98
|
streamChat(sessionId: string, messages: Message[], options: {
|
|
90
99
|
api: string;
|
|
91
100
|
body?: Record<string, JSONValue>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI7D,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI,CAAC;IACvD,WAAW,IAAI,kBAAkB,CAAC;IAClC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IACvD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAClE;;;;OAIG;IACH,kBAAkB,CAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,GAAG,SAAS,GACnD,IAAI,CAAC;IACR;;OAEG;IACH,
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
|
package/dist/client/manager.js
CHANGED
|
@@ -12,6 +12,20 @@ class StreamManager {
|
|
|
12
12
|
rafFallbackTimer = null;
|
|
13
13
|
// Typed accessors for rAF/cAF without relying on DOM types
|
|
14
14
|
getRAF() {
|
|
15
|
+
// If the page is hidden, avoid rAF because it can be throttled heavily
|
|
16
|
+
try {
|
|
17
|
+
if (typeof window !== "undefined" &&
|
|
18
|
+
typeof document !== "undefined" &&
|
|
19
|
+
// visibilityState can be missing in some environments
|
|
20
|
+
typeof document.visibilityState ===
|
|
21
|
+
"string" &&
|
|
22
|
+
document.visibilityState === "hidden") {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore detection errors and fall through
|
|
28
|
+
}
|
|
15
29
|
const g = globalThis;
|
|
16
30
|
return typeof g.requestAnimationFrame === "function"
|
|
17
31
|
? g.requestAnimationFrame
|
|
@@ -23,9 +37,6 @@ class StreamManager {
|
|
|
23
37
|
? g.cancelAnimationFrame
|
|
24
38
|
: undefined;
|
|
25
39
|
}
|
|
26
|
-
// 1フレーム内での messages 配列クローンをセッション単位で一度に抑制するためのフラグ
|
|
27
|
-
// 意図: チャンクが高頻度で到着するケースで、配列コピーをフレームあたり1回に抑える
|
|
28
|
-
pendingCloneSessions = new Set();
|
|
29
40
|
maxSessions;
|
|
30
41
|
debug;
|
|
31
42
|
ttlMs;
|
|
@@ -34,8 +45,6 @@ class StreamManager {
|
|
|
34
45
|
activeSubscribers = new Map();
|
|
35
46
|
// セッション単位で「最新の onFinish」を保持(常に最新を呼びたい要件に対応)
|
|
36
47
|
latestOnFinish = new Map();
|
|
37
|
-
// セッション単位で「最新の onMetadata」を保持
|
|
38
|
-
latestOnMetadata = new Map();
|
|
39
48
|
// セッション単位で「最新の onError」を保持
|
|
40
49
|
latestOnError = new Map();
|
|
41
50
|
// 意図: 各セッションの最新ストリームIDを保持し、古いストリームの遅延イベントを無視する
|
|
@@ -72,14 +81,6 @@ class StreamManager {
|
|
|
72
81
|
this.latestOnFinish.delete(sessionId);
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
setOnMetadataHandler(sessionId, handler) {
|
|
76
|
-
if (handler) {
|
|
77
|
-
this.latestOnMetadata.set(sessionId, handler);
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
this.latestOnMetadata.delete(sessionId);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
84
|
setOnErrorHandler(sessionId, handler) {
|
|
84
85
|
if (handler) {
|
|
85
86
|
this.latestOnError.set(sessionId, handler);
|
|
@@ -121,9 +122,16 @@ class StreamManager {
|
|
|
121
122
|
this.rafFallbackTimer = null;
|
|
122
123
|
}
|
|
123
124
|
this.rafScheduled = false;
|
|
124
|
-
this.listeners.forEach((listener) =>
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
this.listeners.forEach((listener) => {
|
|
126
|
+
try {
|
|
127
|
+
listener();
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// 重要: 1つの購読者の例外で全体の通知が止まらないように防御
|
|
131
|
+
if (this.debug)
|
|
132
|
+
console.warn("[SM] listener error (immediate)", err);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
127
135
|
}
|
|
128
136
|
else {
|
|
129
137
|
// ストリーミング中の頻繁な更新は rAF で 1 フレームにまとめる
|
|
@@ -135,9 +143,15 @@ class StreamManager {
|
|
|
135
143
|
this.rafId = raf(() => {
|
|
136
144
|
this.rafId = null;
|
|
137
145
|
this.rafScheduled = false;
|
|
138
|
-
this.listeners.forEach((listener) =>
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
this.listeners.forEach((listener) => {
|
|
147
|
+
try {
|
|
148
|
+
listener();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (this.debug)
|
|
152
|
+
console.warn("[SM] listener error (rAF)", err);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
141
155
|
});
|
|
142
156
|
}
|
|
143
157
|
else {
|
|
@@ -145,20 +159,23 @@ class StreamManager {
|
|
|
145
159
|
this.rafFallbackTimer = setTimeout(() => {
|
|
146
160
|
this.rafFallbackTimer = null;
|
|
147
161
|
this.rafScheduled = false;
|
|
148
|
-
this.listeners.forEach((listener) =>
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
this.listeners.forEach((listener) => {
|
|
163
|
+
try {
|
|
164
|
+
listener();
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (this.debug)
|
|
168
|
+
console.warn("[SM] listener error (timer)", err);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
151
171
|
}, 16);
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
174
|
}
|
|
155
175
|
getSession(sessionId) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
session.lastAccessed = Date.now();
|
|
160
|
-
}
|
|
161
|
-
return session;
|
|
176
|
+
// 読み取り専用: lastAccessed を更新しない
|
|
177
|
+
// (アクセス時間の更新は touchSession() 側で明示的に実施するポリシー)
|
|
178
|
+
return this.state.sessions.get(sessionId);
|
|
162
179
|
}
|
|
163
180
|
// LRU 用に最終アクセス時刻のみを更新する(通知はしない)
|
|
164
181
|
touchSession(sessionId) {
|
|
@@ -171,7 +188,8 @@ class StreamManager {
|
|
|
171
188
|
const existingSession = this.state.sessions.get(sessionId);
|
|
172
189
|
// streaming中のセッションが存在する場合は、initialMessagesで上書きしない
|
|
173
190
|
if (existingSession) {
|
|
174
|
-
if (existingSession.status === "streaming" ||
|
|
191
|
+
if (existingSession.status === "streaming" ||
|
|
192
|
+
existingSession.status === "submitted") {
|
|
175
193
|
if (this.debug) {
|
|
176
194
|
console.log(`[SM] Skip initialMessages for streaming session: ${sessionId}`);
|
|
177
195
|
}
|
|
@@ -182,11 +200,20 @@ class StreamManager {
|
|
|
182
200
|
}
|
|
183
201
|
// Check if we need to remove old sessions
|
|
184
202
|
if (this.state.sessions.size >= this.maxSessions) {
|
|
185
|
-
|
|
203
|
+
// 進行中(submitted/streaming)のセッションは破棄しないポリシー
|
|
204
|
+
// それ以外の候補が見つからない場合は新規セッションの生成を見送る
|
|
205
|
+
const removed = this.removeOldestSession();
|
|
206
|
+
if (!removed) {
|
|
207
|
+
if (this.debug) {
|
|
208
|
+
console.warn(`[SM] Reached maxSessions; no eligible session to evict. Skipping init for: ${sessionId}`);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
186
212
|
}
|
|
187
213
|
this.state.sessions.set(sessionId, {
|
|
188
214
|
id: sessionId,
|
|
189
215
|
messages: initialMessages,
|
|
216
|
+
metadata: [],
|
|
190
217
|
messagesVersion: 0,
|
|
191
218
|
status: "ready",
|
|
192
219
|
error: null,
|
|
@@ -196,7 +223,7 @@ class StreamManager {
|
|
|
196
223
|
if (this.debug) {
|
|
197
224
|
this.logSessionInfo(`Session added: ${sessionId}`);
|
|
198
225
|
}
|
|
199
|
-
this.notify(true); //
|
|
226
|
+
this.notify(true); // 即時に通知(初期化はUX上重要)
|
|
200
227
|
if (this.debug)
|
|
201
228
|
console.log("[SM] init sessions", this.state.sessions, this.activeSubscribers);
|
|
202
229
|
}
|
|
@@ -207,18 +234,6 @@ class StreamManager {
|
|
|
207
234
|
if (this.debug) {
|
|
208
235
|
console.info(`[SM] retainSession: ${sessionId} -> ${next}`);
|
|
209
236
|
}
|
|
210
|
-
// 修正意図(日本語):
|
|
211
|
-
// - 0→1 に購読者数が増えた直後は、これまで抑制されていたストリーミング更新を
|
|
212
|
-
// 即座に UI に反映させたいケースがある。
|
|
213
|
-
// - そこで、対象セッションが streaming/submitted の場合に限り、即時通知を一度だけ行う。
|
|
214
|
-
// (注意: 現在の useChatSession 実装では retain が subscribe より先に呼ばれるため、
|
|
215
|
-
// この通知は新規購読者より前に発生することがある。将来の呼び出し順や他の購読者に対しては有効。)
|
|
216
|
-
if (next === 1) {
|
|
217
|
-
const s = this.state.sessions.get(sessionId);
|
|
218
|
-
if (s && (s.status === "streaming" || s.status === "submitted")) {
|
|
219
|
-
this.notify(true);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
237
|
}
|
|
223
238
|
releaseSession(sessionId) {
|
|
224
239
|
const prev = this.activeSubscribers.get(sessionId) ?? 0;
|
|
@@ -233,6 +248,13 @@ class StreamManager {
|
|
|
233
248
|
console.info(`[SM] releaseSession: ${sessionId} -> ${next}`);
|
|
234
249
|
}
|
|
235
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* LRU に従いセッションを1件削除する。
|
|
253
|
+
* - 第一候補: 非ストリーミング(ready/error/aborted)かつ購読者なし
|
|
254
|
+
* - 第二候補: 非ストリーミング(購読者有無は問わない)
|
|
255
|
+
* - 進行中(submitted/streaming)は破棄しない(ユーザー操作中断の回避)
|
|
256
|
+
* @returns 削除が行われた場合は true、候補が無い場合は false
|
|
257
|
+
*/
|
|
236
258
|
removeOldestSession() {
|
|
237
259
|
let oldestSessionId = null;
|
|
238
260
|
let oldestTime = Date.now();
|
|
@@ -248,10 +270,12 @@ class StreamManager {
|
|
|
248
270
|
}
|
|
249
271
|
}
|
|
250
272
|
}
|
|
251
|
-
//
|
|
273
|
+
// 上で見つからない場合は、「非ストリーミング限定」で最古を探す
|
|
252
274
|
if (!oldestSessionId) {
|
|
253
275
|
for (const [id, session] of this.state.sessions) {
|
|
254
|
-
if (session.
|
|
276
|
+
if (session.status !== "streaming" &&
|
|
277
|
+
session.status !== "submitted" &&
|
|
278
|
+
session.lastAccessed < oldestTime) {
|
|
255
279
|
oldestTime = session.lastAccessed;
|
|
256
280
|
oldestSessionId = id;
|
|
257
281
|
}
|
|
@@ -266,14 +290,15 @@ class StreamManager {
|
|
|
266
290
|
this.state.sessions.delete(oldestSessionId);
|
|
267
291
|
// セッション破棄時はハンドラも破棄
|
|
268
292
|
this.latestOnFinish.delete(oldestSessionId);
|
|
269
|
-
this.latestOnMetadata.delete(oldestSessionId);
|
|
270
293
|
this.latestOnError.delete(oldestSessionId);
|
|
271
294
|
// 意図: streamId も破棄してメモリリーク・誤判定を防止
|
|
272
295
|
this.streamIds.delete(oldestSessionId);
|
|
273
296
|
if (this.debug) {
|
|
274
297
|
this.logSessionInfo(`Session removed: ${oldestSessionId} (LRU eviction)`);
|
|
275
298
|
}
|
|
299
|
+
return true;
|
|
276
300
|
}
|
|
301
|
+
return false;
|
|
277
302
|
}
|
|
278
303
|
logSessionInfo(action) {
|
|
279
304
|
const sessionInfo = Array.from(this.state.sessions.entries()).map(([id, session]) => ({
|
|
@@ -290,6 +315,12 @@ class StreamManager {
|
|
|
290
315
|
sessions: sessionInfo,
|
|
291
316
|
});
|
|
292
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* セッションの部分更新(内部専用)
|
|
320
|
+
* - 参照の安定性を保ちながら、Map の再生成により変更を通知する
|
|
321
|
+
* - important: messages を更新した場合、messagesVersion をインクリメント
|
|
322
|
+
* - immediate=true の場合、rAF をバイパスして即時通知(ステータス変更等のUX向上)
|
|
323
|
+
*/
|
|
293
324
|
updateSession(sessionId, updates, immediate = false) {
|
|
294
325
|
const session = this.state.sessions.get(sessionId);
|
|
295
326
|
if (session) {
|
|
@@ -305,25 +336,25 @@ class StreamManager {
|
|
|
305
336
|
// messages が指定されたタイミングでのみバージョンを進める
|
|
306
337
|
session.messagesVersion = (session.messagesVersion ?? 0) + 1;
|
|
307
338
|
}
|
|
308
|
-
// 再レンダリング抑制: 購読者が0のセッションに対する
|
|
309
|
-
// ストリーミング由来の message 更新では通知しない。
|
|
310
|
-
// (onFinish/onError等のイベントは immediate=true で通知される)
|
|
311
339
|
if (immediate) {
|
|
312
340
|
this.notify(true);
|
|
313
341
|
}
|
|
314
342
|
else {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.notify(false);
|
|
318
|
-
}
|
|
319
|
-
else if (this.debug) {
|
|
320
|
-
// デバッグ時は抑制されたことをログ
|
|
321
|
-
console.info(`[SM] notify suppressed (no subscribers): ${sessionId}`);
|
|
322
|
-
}
|
|
343
|
+
// 常に rAF バッチで通知(購読者数による抑制は行わない)
|
|
344
|
+
this.notify(false);
|
|
323
345
|
}
|
|
324
346
|
}
|
|
325
347
|
}
|
|
326
348
|
async streamChat(sessionId, messages, options) {
|
|
349
|
+
const isObject = (v) => typeof v === "object" && v !== null;
|
|
350
|
+
const isChunkPayload = (v) => isObject(v) &&
|
|
351
|
+
(typeof v.text === "undefined" ||
|
|
352
|
+
typeof v.text === "string") &&
|
|
353
|
+
(typeof v.meta === "undefined" ||
|
|
354
|
+
Array.isArray(v.meta));
|
|
355
|
+
const isErrorPayload = (v) => isObject(v) &&
|
|
356
|
+
(typeof v.message === "undefined" ||
|
|
357
|
+
typeof v.message === "string");
|
|
327
358
|
const session = this.state.sessions.get(sessionId);
|
|
328
359
|
if (!session) {
|
|
329
360
|
if (this.debug)
|
|
@@ -350,6 +381,8 @@ class StreamManager {
|
|
|
350
381
|
status: "submitted",
|
|
351
382
|
error: null,
|
|
352
383
|
abortController,
|
|
384
|
+
// 新規ストリーム開始時にメタデータはリセット(current stream のみ保持)
|
|
385
|
+
metadata: [],
|
|
353
386
|
}, true); // Immediate for status change
|
|
354
387
|
try {
|
|
355
388
|
const requestBody = {
|
|
@@ -366,13 +399,13 @@ class StreamManager {
|
|
|
366
399
|
});
|
|
367
400
|
// HTTP レベルのエラーハンドリング:SSE 解析に入る前に早期失敗
|
|
368
401
|
if (!response.ok) {
|
|
369
|
-
let message
|
|
402
|
+
let message;
|
|
370
403
|
try {
|
|
371
|
-
|
|
372
|
-
message += text ? `: ${text}` : "";
|
|
404
|
+
message = await response.text();
|
|
373
405
|
}
|
|
374
406
|
catch (err) {
|
|
375
407
|
console.warn("[SM] response.text() error", err);
|
|
408
|
+
message = `Unknown error: ${response.status}`;
|
|
376
409
|
}
|
|
377
410
|
const err = new Error(message);
|
|
378
411
|
this.updateSession(sessionId, {
|
|
@@ -422,7 +455,7 @@ class StreamManager {
|
|
|
422
455
|
if (dataLines.length === 0 && !eventName)
|
|
423
456
|
return;
|
|
424
457
|
const dataStr = dataLines.join("\n");
|
|
425
|
-
//
|
|
458
|
+
// JSON を安全にパース(失敗時は null)
|
|
426
459
|
let parsed;
|
|
427
460
|
try {
|
|
428
461
|
parsed = dataStr ? JSON.parse(dataStr) : null;
|
|
@@ -433,57 +466,43 @@ class StreamManager {
|
|
|
433
466
|
}
|
|
434
467
|
switch (eventName) {
|
|
435
468
|
case "chunk": {
|
|
436
|
-
if (parsed &&
|
|
469
|
+
if (parsed && isChunkPayload(parsed)) {
|
|
437
470
|
const latestId = this.streamIds.get(sessionId);
|
|
438
471
|
if (latestId !== streamId)
|
|
439
472
|
return; // 中断済みのストリームからの遅延イベントを破棄
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
this.updateSession(sessionId, {
|
|
448
|
-
messages: [...updatedMessages],
|
|
449
|
-
}); // rAFバッチで通知
|
|
473
|
+
// Handle text delta if provided
|
|
474
|
+
if (typeof parsed.text === "string" && parsed.text.length > 0) {
|
|
475
|
+
// NOTE: 連結は O(n^2) 化を避ける最適化余地がある(必要ならバッファ化)
|
|
476
|
+
const wasEmpty = assistantMessage.content.length === 0;
|
|
477
|
+
assistantMessage.content += parsed.text;
|
|
478
|
+
// 最初のテキストチャンクは即時反映して初動レイテンシを下げる
|
|
479
|
+
if (wasEmpty) {
|
|
480
|
+
this.updateSession(sessionId, { messages: [...updatedMessages] }, true);
|
|
450
481
|
}
|
|
451
482
|
else {
|
|
452
|
-
//
|
|
453
|
-
|
|
483
|
+
// 以降は rAF バッチで配列参照を更新
|
|
484
|
+
this.updateSession(sessionId, {
|
|
485
|
+
messages: [...updatedMessages],
|
|
486
|
+
});
|
|
454
487
|
}
|
|
455
488
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const latestId = this.streamIds.get(sessionId);
|
|
465
|
-
if (latestId !== streamId)
|
|
466
|
-
return; // 中断済みのストリームからの遅延イベントを破棄
|
|
467
|
-
assistantMessage.content = parsed.content;
|
|
468
|
-
this.updateSession(sessionId, {
|
|
469
|
-
messages: [...updatedMessages],
|
|
470
|
-
}, true); // 即時更新
|
|
471
|
-
}
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
case "metadata": {
|
|
475
|
-
if (parsed) {
|
|
476
|
-
try {
|
|
477
|
-
this.latestOnMetadata.get(sessionId)?.(parsed);
|
|
478
|
-
}
|
|
479
|
-
catch {
|
|
480
|
-
console.error("[SM] Failed to call onMetadata handler");
|
|
489
|
+
// Handle bundled metadata if present
|
|
490
|
+
if (Array.isArray(parsed.meta) && parsed.meta.length > 0) {
|
|
491
|
+
const s = this.state.sessions.get(sessionId);
|
|
492
|
+
if (s) {
|
|
493
|
+
// Append new metadata items
|
|
494
|
+
const nextMeta = s.metadata.concat(parsed.meta);
|
|
495
|
+
this.updateSession(sessionId, { metadata: nextMeta });
|
|
496
|
+
}
|
|
481
497
|
}
|
|
482
498
|
}
|
|
483
499
|
break;
|
|
484
500
|
}
|
|
485
501
|
case "error": {
|
|
486
|
-
const
|
|
502
|
+
const message = isErrorPayload(parsed) && parsed.message
|
|
503
|
+
? parsed.message
|
|
504
|
+
: "Stream error";
|
|
505
|
+
const err = new Error(message);
|
|
487
506
|
try {
|
|
488
507
|
abortController.abort();
|
|
489
508
|
}
|
|
@@ -517,7 +536,8 @@ class StreamManager {
|
|
|
517
536
|
abortController: null,
|
|
518
537
|
}, true);
|
|
519
538
|
try {
|
|
520
|
-
this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages ||
|
|
539
|
+
this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages ||
|
|
540
|
+
[]));
|
|
521
541
|
}
|
|
522
542
|
catch {
|
|
523
543
|
console.error("[SM] Failed to call onFinish handler");
|
|
@@ -541,7 +561,7 @@ class StreamManager {
|
|
|
541
561
|
const lines = buffer.split("\n");
|
|
542
562
|
buffer = lines.pop() || ""; // 末尾の未完成行をバッファに残す
|
|
543
563
|
for (const line of lines) {
|
|
544
|
-
if (line === "") {
|
|
564
|
+
if (line.trim() === "") {
|
|
545
565
|
// 空行で一つのイベントをフラッシュ
|
|
546
566
|
flushEvent();
|
|
547
567
|
eventName = null;
|
|
@@ -566,6 +586,19 @@ class StreamManager {
|
|
|
566
586
|
if (shouldTerminate)
|
|
567
587
|
break;
|
|
568
588
|
}
|
|
589
|
+
// 読み取り終了時の明示クリーンアップ(環境により不要だが安全)
|
|
590
|
+
try {
|
|
591
|
+
await reader.cancel();
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
/* noop */
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
await response.body?.cancel();
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
/* noop */
|
|
601
|
+
}
|
|
569
602
|
// 意図: 正常完了(サーバが done を送らないケース)だけをここで処理。
|
|
570
603
|
// failed=true(error)の場合は既に状態更新済みなので何もしない。
|
|
571
604
|
if (!failed && !shouldTerminate) {
|
|
@@ -584,10 +617,14 @@ class StreamManager {
|
|
|
584
617
|
catch (error) {
|
|
585
618
|
// 意図: エラーイベントは catch に到達しない(failed フラグで分岐)ため、ここは接続エラーや中断のみ
|
|
586
619
|
if (isAbortError(error)) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
620
|
+
// 既に stopStream() 等で aborted 済みであれば二重更新を避ける
|
|
621
|
+
const s = this.state.sessions.get(sessionId);
|
|
622
|
+
if (s?.status !== "aborted") {
|
|
623
|
+
this.updateSession(sessionId, {
|
|
624
|
+
status: "aborted",
|
|
625
|
+
abortController: null,
|
|
626
|
+
}, true); // Immediate for status change
|
|
627
|
+
}
|
|
591
628
|
}
|
|
592
629
|
else {
|
|
593
630
|
const err = error instanceof Error ? error : new Error("Unknown error");
|
|
@@ -630,6 +667,9 @@ class StreamManager {
|
|
|
630
667
|
performSweep() {
|
|
631
668
|
if (this.debug)
|
|
632
669
|
console.info("[SM] Running TTL sweep", this.state.sessions);
|
|
670
|
+
const ttlMs = this.ttlMs;
|
|
671
|
+
if (!ttlMs || ttlMs <= 0)
|
|
672
|
+
return; // TTL 未設定なら何もしない
|
|
633
673
|
if (this.state.sessions.size === 0)
|
|
634
674
|
return;
|
|
635
675
|
const now = Date.now();
|
|
@@ -642,7 +682,7 @@ class StreamManager {
|
|
|
642
682
|
const subCount = this.activeSubscribers.get(id) ?? 0;
|
|
643
683
|
if (subCount > 0)
|
|
644
684
|
continue;
|
|
645
|
-
if (now - session.lastAccessed >
|
|
685
|
+
if (now - session.lastAccessed > ttlMs) {
|
|
646
686
|
// 安全のため abortController が残っていれば中断
|
|
647
687
|
try {
|
|
648
688
|
session.abortController?.abort();
|
|
@@ -655,7 +695,6 @@ class StreamManager {
|
|
|
655
695
|
this.state.sessions.delete(id);
|
|
656
696
|
// 意図: ハンドラと streamId も破棄してリークを防止
|
|
657
697
|
this.latestOnFinish.delete(id);
|
|
658
|
-
this.latestOnMetadata.delete(id);
|
|
659
698
|
this.latestOnError.delete(id);
|
|
660
699
|
this.streamIds.delete(id);
|
|
661
700
|
removed = true;
|
|
@@ -701,10 +740,8 @@ class StreamManager {
|
|
|
701
740
|
this.listeners.clear();
|
|
702
741
|
this.activeSubscribers.clear();
|
|
703
742
|
this.latestOnFinish.clear();
|
|
704
|
-
this.latestOnMetadata.clear();
|
|
705
743
|
this.latestOnError.clear();
|
|
706
744
|
this.streamIds.clear();
|
|
707
|
-
this.pendingCloneSessions.clear();
|
|
708
745
|
}
|
|
709
746
|
}
|
|
710
747
|
// Export the StreamManager class for custom initialization
|
package/dist/client/types.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export type ChatStatus = "ready" | "submitted" | "streaming" | "error" | "aborte
|
|
|
3
3
|
export type ChatSession = {
|
|
4
4
|
id: string;
|
|
5
5
|
messages: Message[];
|
|
6
|
+
/**
|
|
7
|
+
* Accumulated metadata payloads received during the current stream.
|
|
8
|
+
* Newer items append to the end.
|
|
9
|
+
*/
|
|
10
|
+
metadata: Array<Record<string, JSONValue>>;
|
|
6
11
|
/**
|
|
7
12
|
* messages の内容が更新されるたびにインクリメントされるバージョン番号。
|
|
8
13
|
* - ストリーミング中は配列参照を再生成しない最適化を行っているため、
|
|
@@ -15,19 +20,11 @@ export type ChatSession = {
|
|
|
15
20
|
abortController: AbortController | null;
|
|
16
21
|
lastAccessed: number;
|
|
17
22
|
};
|
|
18
|
-
export type OnChatMetadataCallback<T extends Record<string, JSONValue> = Record<string, JSONValue>> = (metadata: T) => void;
|
|
19
23
|
export type OnChatErrorCallback = (error: Error) => void;
|
|
20
24
|
export type OnChatFinishCallback = (messages: Message[]) => void;
|
|
21
25
|
export type UseChatSessionOptions = {
|
|
22
|
-
api
|
|
26
|
+
api: string;
|
|
23
27
|
initialMessages?: Message[];
|
|
24
|
-
/**
|
|
25
|
-
* メタデータ受信時のコールバック
|
|
26
|
-
* memo化された関数を渡すこと
|
|
27
|
-
* @param metadata 受信したメタデータ
|
|
28
|
-
* @returns
|
|
29
|
-
*/
|
|
30
|
-
onMetadata?: OnChatMetadataCallback;
|
|
31
28
|
/**
|
|
32
29
|
* エラー発生時のコールバック
|
|
33
30
|
* memo化された関数を渡すこと
|
|
@@ -49,6 +46,10 @@ export type ReloadMessage = (params?: {
|
|
|
49
46
|
}) => Promise<void>;
|
|
50
47
|
export type UseChatSessionReturn = {
|
|
51
48
|
messages: Message[];
|
|
49
|
+
/**
|
|
50
|
+
* Latest list of metadata payloads received during streaming.
|
|
51
|
+
*/
|
|
52
|
+
metadata: Array<Record<string, JSONValue>>;
|
|
52
53
|
/**
|
|
53
54
|
* messages の内容更新を検知するためのバージョン番号。
|
|
54
55
|
* useMemo の依存などに利用できる。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,WAAW,GACX,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,WAAW,GACX,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;OAGG;IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAEjE,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,EAAE;IACpC,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3C;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { UseChatSessionOptions, UseChatSessionReturn } from "./types.js";
|
|
2
|
-
export declare function useChatSession(sessionId: string, options
|
|
2
|
+
export declare function useChatSession(sessionId: string, options: UseChatSessionOptions): UseChatSessionReturn;
|
|
3
3
|
//# sourceMappingURL=useChatSession.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAKpB,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CAsItB"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useSyncExternalStore, useCallback, useRef } from "react";
|
|
2
2
|
import { useAIStreamManager } from "./context.js";
|
|
3
3
|
import { generateId } from "../shared/generate-id.js";
|
|
4
|
-
export function useChatSession(sessionId, options
|
|
5
|
-
const { api
|
|
4
|
+
export function useChatSession(sessionId, options) {
|
|
5
|
+
const { api, initialMessages = [], onError, onFinish } = options;
|
|
6
6
|
const manager = useAIStreamManager();
|
|
7
7
|
// セッションIDの変更検知用。初期化時に正しい initialMessages を渡すため、
|
|
8
8
|
// レンダリング中に参照を更新する(副作用ではないので安全)。
|
|
@@ -43,10 +43,6 @@ export function useChatSession(sessionId, options = {}) {
|
|
|
43
43
|
// onFinish が undefined の場合は登録解除される
|
|
44
44
|
manager.setOnFinishHandler(sessionId, onFinish);
|
|
45
45
|
}, [manager, sessionId, onFinish]);
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
// onMetadata が undefined の場合は登録解除される
|
|
48
|
-
manager.setOnMetadataHandler(sessionId, onMetadata);
|
|
49
|
-
}, [manager, sessionId, onMetadata]);
|
|
50
46
|
useEffect(() => {
|
|
51
47
|
// onError が undefined の場合は登録解除される
|
|
52
48
|
manager.setOnErrorHandler(sessionId, onError);
|
|
@@ -54,6 +50,7 @@ export function useChatSession(sessionId, options = {}) {
|
|
|
54
50
|
const session = state.sessions.get(sessionId) || {
|
|
55
51
|
id: sessionId,
|
|
56
52
|
messages: initialMessages,
|
|
53
|
+
metadata: [],
|
|
57
54
|
messagesVersion: 0,
|
|
58
55
|
status: "ready",
|
|
59
56
|
error: null,
|
|
@@ -97,6 +94,7 @@ export function useChatSession(sessionId, options = {}) {
|
|
|
97
94
|
}, [sessionId, manager]);
|
|
98
95
|
return {
|
|
99
96
|
messages: session.messages,
|
|
97
|
+
metadata: session.metadata ?? [],
|
|
100
98
|
messagesVersion: session.messagesVersion ?? 0,
|
|
101
99
|
status: session.status,
|
|
102
100
|
error: session.error,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../../src/server/createChatHandler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EAGb,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAKpB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,iBAAiB,
|
|
1
|
+
{"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../../src/server/createChatHandler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EAGb,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAKpB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,iBAAiB,CAyb3E"}
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import { isAbortError } from "../shared/error-utils.js";
|
|
2
2
|
// Implemented for usage inside Hono's `streamText()` callback.
|
|
3
3
|
export function createChatHandler(options) {
|
|
4
|
-
const { providers, transforms = [], throttle,
|
|
4
|
+
const { providers, transforms = [], throttle, onFinish, onError, messages, timeoutMs = 60000, temperature, maxOutputTokens, debug = false, } = options;
|
|
5
5
|
// Convert single provider config to array for consistent handling
|
|
6
6
|
const providerConfigs = Array.isArray(providers) ? providers : [providers];
|
|
7
|
-
//
|
|
8
|
-
|
|
7
|
+
// Shared metadata queue for this handler instance.
|
|
8
|
+
// - External sendMetadata() calls enqueue here even before handler starts
|
|
9
|
+
// - During handler execution, queued metadata are bundled into next chunk
|
|
10
|
+
// or flushed immediately as a metadata-only chunk
|
|
11
|
+
const metaQueue = [];
|
|
12
|
+
// Flusher the handler sets when stream is ready; external callers invoke it to flush immediately
|
|
13
|
+
let currentFlushMeta = null;
|
|
14
|
+
// Track previous throttle value for debug logging
|
|
15
|
+
let previousThrottle = null;
|
|
16
|
+
const enqueueMetadata = (meta) => {
|
|
17
|
+
metaQueue.push(meta);
|
|
18
|
+
if (debug) {
|
|
19
|
+
console.log("[cch] Enqueued metadata:", meta, `(queue size: ${metaQueue.length})`);
|
|
20
|
+
}
|
|
21
|
+
// If stream is active, flush immediately as metadata-only chunk
|
|
22
|
+
if (currentFlushMeta) {
|
|
23
|
+
try {
|
|
24
|
+
currentFlushMeta();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore flush error; stream may be closing
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
9
31
|
const handler = async (req, stream) => {
|
|
10
32
|
// OpenAI SDK のストリーム・タイムアウト・中断制御に関する一時状態
|
|
11
33
|
let chatStream;
|
|
@@ -14,10 +36,12 @@ export function createChatHandler(options) {
|
|
|
14
36
|
let abortNotified = false; // onError('aborted') の多重呼び出し防止
|
|
15
37
|
let currentModel = "";
|
|
16
38
|
let currentProvider = "openai";
|
|
17
|
-
// Content send control for throttling
|
|
39
|
+
// Content send control for throttling
|
|
18
40
|
let lastContentSendTime = 0;
|
|
19
41
|
// Tracks the accText length at last send. Used to compute delta size when sending chunks.
|
|
20
42
|
let lastContentSendLength = 0;
|
|
43
|
+
// Track total characters streamed across all provider attempts
|
|
44
|
+
let totalStreamedChars = 0;
|
|
21
45
|
const sendEvent = async (event, data) => {
|
|
22
46
|
// 接続が既に中断されている場合は送信しない
|
|
23
47
|
if (isAborted)
|
|
@@ -58,7 +82,10 @@ export function createChatHandler(options) {
|
|
|
58
82
|
abortNotified = true;
|
|
59
83
|
try {
|
|
60
84
|
// ユーザー中断/接続断をサーバ利用側に通知
|
|
61
|
-
onError(new Error("Aborted"),
|
|
85
|
+
onError(new Error("Aborted"), {
|
|
86
|
+
reason: "aborted",
|
|
87
|
+
streamedChars: totalStreamedChars,
|
|
88
|
+
});
|
|
62
89
|
}
|
|
63
90
|
catch {
|
|
64
91
|
// ignore handler error
|
|
@@ -70,7 +97,9 @@ export function createChatHandler(options) {
|
|
|
70
97
|
try {
|
|
71
98
|
// Also listen to the Request's abort signal defensively
|
|
72
99
|
if (req?.signal) {
|
|
73
|
-
req.signal.addEventListener("abort", handleAbort, {
|
|
100
|
+
req.signal.addEventListener("abort", handleAbort, {
|
|
101
|
+
once: true,
|
|
102
|
+
});
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
105
|
catch {
|
|
@@ -78,13 +107,33 @@ export function createChatHandler(options) {
|
|
|
78
107
|
}
|
|
79
108
|
try {
|
|
80
109
|
const startTime = Date.now();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
// Helper to drain queued metadata
|
|
111
|
+
const drainMeta = () => {
|
|
112
|
+
if (metaQueue.length === 0)
|
|
113
|
+
return [];
|
|
114
|
+
const out = metaQueue.splice(0, metaQueue.length);
|
|
115
|
+
return out;
|
|
116
|
+
};
|
|
117
|
+
// Flush metadata-only chunk immediately (text: "")
|
|
118
|
+
const flushMetaOnly = async () => {
|
|
119
|
+
if (isAborted)
|
|
120
|
+
return;
|
|
121
|
+
const metas = drainMeta();
|
|
122
|
+
if (metas.length === 0)
|
|
123
|
+
return;
|
|
124
|
+
if (debug) {
|
|
125
|
+
console.log(`[cch] Flushing ${metas.length} metadata items as meta-only chunk:`, metas);
|
|
126
|
+
}
|
|
127
|
+
await sendEvent("chunk", {
|
|
128
|
+
text: "",
|
|
129
|
+
meta: metas,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
// Expose flusher so external sendMetadata() can flush immediately when possible
|
|
133
|
+
currentFlushMeta = () => {
|
|
134
|
+
// Fire and forget; do not await to avoid blocking caller
|
|
135
|
+
void flushMetaOnly();
|
|
85
136
|
};
|
|
86
|
-
// Set the current sendMetadata for this request
|
|
87
|
-
currentSendMetadata = sendMetadata;
|
|
88
137
|
if (timeoutMs) {
|
|
89
138
|
// サーバ側で全体の処理時間を制限する。クライアント中断済みであれば何もしない
|
|
90
139
|
timeoutId = setTimeout(async () => {
|
|
@@ -103,19 +152,32 @@ export function createChatHandler(options) {
|
|
|
103
152
|
code: "TIMEOUT",
|
|
104
153
|
});
|
|
105
154
|
if (onError)
|
|
106
|
-
onError(new Error("Request timeout"),
|
|
155
|
+
onError(new Error("Request timeout"), {
|
|
156
|
+
reason: "timeout",
|
|
157
|
+
streamedChars: totalStreamedChars,
|
|
158
|
+
});
|
|
107
159
|
}, timeoutMs);
|
|
108
160
|
}
|
|
109
|
-
// Try each provider
|
|
161
|
+
// Try each provider end-to-end. If a provider fails before sending
|
|
162
|
+
// any content, fallback to the next. Once content is sent, do not fallback.
|
|
163
|
+
let lastError = null;
|
|
110
164
|
for (let i = 0; i < providerConfigs.length; i++) {
|
|
111
165
|
const config = providerConfigs[i];
|
|
166
|
+
if (debug) {
|
|
167
|
+
console.log(`[cch] Trying provider ${i + 1}/${providerConfigs.length}: ${config.provider}/${config.model}`);
|
|
168
|
+
}
|
|
169
|
+
// Local attempt state
|
|
170
|
+
let localAccText = "";
|
|
171
|
+
let localUsage;
|
|
172
|
+
lastContentSendTime = 0;
|
|
173
|
+
lastContentSendLength = 0;
|
|
112
174
|
// geminiでは"none"を指定することでreasoning_effortを無効化できる
|
|
113
175
|
let reasoningEffort = config.reasoningEffort ?? "none";
|
|
114
176
|
if (config.provider === "openai" && reasoningEffort === "none") {
|
|
115
177
|
reasoningEffort = undefined;
|
|
116
178
|
}
|
|
117
179
|
try {
|
|
118
|
-
//
|
|
180
|
+
// Establish stream for this provider
|
|
119
181
|
chatStream = config.openai.chat.completions.stream({
|
|
120
182
|
model: config.model,
|
|
121
183
|
messages,
|
|
@@ -127,129 +189,157 @@ export function createChatHandler(options) {
|
|
|
127
189
|
});
|
|
128
190
|
currentModel = config.model;
|
|
129
191
|
currentProvider = config.provider;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
catch (error) {
|
|
133
|
-
// Continue to next provider if available
|
|
134
|
-
if (i === providerConfigs.length - 1) {
|
|
135
|
-
// This was the last provider, rethrow the error
|
|
136
|
-
throw error;
|
|
192
|
+
if (debug) {
|
|
193
|
+
console.log(`[cch] Successfully connected to ${config.provider}/${config.model}`);
|
|
137
194
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!chatStream) {
|
|
142
|
-
throw new Error("Failed to create chat stream with any provider");
|
|
143
|
-
}
|
|
144
|
-
// Stream abort is handled by onAbort above
|
|
145
|
-
for await (const chunk of chatStream) {
|
|
146
|
-
if (isAborted)
|
|
147
|
-
break;
|
|
148
|
-
const delta = chunk.choices[0]?.delta;
|
|
149
|
-
const finishReason = chunk.choices[0]?.finish_reason;
|
|
150
|
-
if (chunk.usage) {
|
|
151
|
-
usage = {
|
|
152
|
-
prompt_tokens: chunk.usage.prompt_tokens,
|
|
153
|
-
completion_tokens: chunk.usage.completion_tokens,
|
|
154
|
-
total_tokens: chunk.usage.total_tokens,
|
|
155
|
-
reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (delta?.content) {
|
|
159
|
-
let text = delta.content;
|
|
160
|
-
const ctx = {
|
|
161
|
-
accText,
|
|
162
|
-
sendMetadata,
|
|
163
|
-
};
|
|
164
|
-
for (const transform of transforms) {
|
|
165
|
-
text = await transform(text, ctx);
|
|
195
|
+
// If there is pending metadata, flush once before first content
|
|
196
|
+
if (debug && metaQueue.length > 0) {
|
|
197
|
+
console.log(`[cch] Found ${metaQueue.length} pre-queued metadata items, flushing...`);
|
|
166
198
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
await flushMetaOnly();
|
|
200
|
+
// Consume the stream
|
|
201
|
+
for await (const chunk of chatStream) {
|
|
202
|
+
if (isAborted)
|
|
203
|
+
break;
|
|
204
|
+
const delta = chunk.choices[0]?.delta;
|
|
205
|
+
const finishReason = chunk.choices[0]?.finish_reason;
|
|
206
|
+
if (chunk.usage) {
|
|
207
|
+
localUsage = {
|
|
208
|
+
prompt_tokens: chunk.usage.prompt_tokens,
|
|
209
|
+
completion_tokens: chunk.usage.completion_tokens,
|
|
210
|
+
total_tokens: chunk.usage.total_tokens,
|
|
211
|
+
reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (delta?.content) {
|
|
215
|
+
let text = delta.content;
|
|
216
|
+
const ctx = {
|
|
217
|
+
accText: localAccText,
|
|
218
|
+
// Queue metadata; let sender bundle into next chunk
|
|
219
|
+
sendMetadata: (meta) => enqueueMetadata(meta),
|
|
220
|
+
};
|
|
221
|
+
for (const transform of transforms) {
|
|
222
|
+
text = await transform(text, ctx);
|
|
189
223
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
224
|
+
if (text) {
|
|
225
|
+
localAccText += text;
|
|
226
|
+
// Throttle sending for both modes (converter vs chunk)
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
const timeDiff = now - lastContentSendTime;
|
|
229
|
+
const accTextLength = localAccText.length;
|
|
230
|
+
const charDiff = accTextLength - lastContentSendLength;
|
|
231
|
+
const throttleSettings = throttle
|
|
232
|
+
? typeof throttle === "function"
|
|
233
|
+
? throttle(accTextLength)
|
|
234
|
+
: throttle
|
|
235
|
+
: { timeMs: 0, chars: 0 };
|
|
236
|
+
const shouldSendContent = lastContentSendTime === 0 || // First content is sent immediately
|
|
237
|
+
timeDiff >= throttleSettings.timeMs ||
|
|
238
|
+
charDiff >= throttleSettings.chars;
|
|
239
|
+
if (shouldSendContent) {
|
|
240
|
+
// Send only the newly added delta since last send
|
|
241
|
+
const deltaToSend = localAccText.slice(lastContentSendLength);
|
|
242
|
+
if (deltaToSend) {
|
|
243
|
+
const metas = drainMeta();
|
|
244
|
+
if (debug &&
|
|
245
|
+
(!previousThrottle ||
|
|
246
|
+
JSON.stringify(throttleSettings) !==
|
|
247
|
+
JSON.stringify(previousThrottle))) {
|
|
248
|
+
console.log(`[cch] throttle: ${JSON.stringify(throttleSettings)}`);
|
|
249
|
+
previousThrottle = throttleSettings;
|
|
250
|
+
}
|
|
251
|
+
const payload = metas.length
|
|
252
|
+
? { text: deltaToSend, meta: metas }
|
|
253
|
+
: { text: deltaToSend };
|
|
254
|
+
await sendEvent("chunk", payload);
|
|
255
|
+
}
|
|
256
|
+
lastContentSendTime = now;
|
|
257
|
+
lastContentSendLength = localAccText.length;
|
|
258
|
+
totalStreamedChars = Math.max(totalStreamedChars, lastContentSendLength);
|
|
195
259
|
}
|
|
196
260
|
}
|
|
197
|
-
|
|
198
|
-
|
|
261
|
+
}
|
|
262
|
+
// Check for explicit stream end conditions
|
|
263
|
+
if (finishReason === "stop" || finishReason === "length") {
|
|
264
|
+
// Wait a bit for the final usage chunk if needed
|
|
265
|
+
if (!localUsage && chunk.usage === undefined) {
|
|
266
|
+
// Give it one more iteration to get usage
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// If we have usage or this is the second iteration after finish, break
|
|
270
|
+
if (
|
|
271
|
+
// finishReasonの後にusageが来るのでusageを持って終了とする
|
|
272
|
+
localUsage &&
|
|
273
|
+
finishReason) {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
199
276
|
}
|
|
200
277
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
278
|
+
if (!isAborted) {
|
|
279
|
+
// Send final content if there are unsent updates
|
|
280
|
+
if (localAccText.length > lastContentSendLength) {
|
|
281
|
+
const deltaToSend = localAccText.slice(lastContentSendLength);
|
|
282
|
+
if (deltaToSend) {
|
|
283
|
+
const metas = drainMeta();
|
|
284
|
+
const payload = metas.length
|
|
285
|
+
? { text: deltaToSend, meta: metas }
|
|
286
|
+
: { text: deltaToSend };
|
|
287
|
+
await sendEvent("chunk", payload);
|
|
288
|
+
totalStreamedChars = Math.max(totalStreamedChars, localAccText.length);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// If any metadata remain, flush them as a final meta-only chunk
|
|
292
|
+
await flushMetaOnly();
|
|
293
|
+
await sendEvent("done", null);
|
|
208
294
|
}
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
295
|
+
// 正常終了時もタイマーはクリア(次のattemptへは到達しない)
|
|
296
|
+
clearTimeoutSafe();
|
|
297
|
+
if (onFinish && !isAborted) {
|
|
298
|
+
const finishInfo = {
|
|
299
|
+
model: currentModel,
|
|
300
|
+
provider: currentProvider,
|
|
301
|
+
text: localAccText,
|
|
302
|
+
usage: localUsage,
|
|
303
|
+
durationMs: Date.now() - startTime,
|
|
304
|
+
};
|
|
305
|
+
if (debug) {
|
|
306
|
+
console.log(`[cch] Stream finished:`, {
|
|
307
|
+
model: currentModel,
|
|
308
|
+
provider: currentProvider,
|
|
309
|
+
textLength: localAccText.length,
|
|
310
|
+
usage: localUsage,
|
|
311
|
+
durationMs: finishInfo.durationMs,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
onFinish(finishInfo);
|
|
215
315
|
}
|
|
316
|
+
// Completed successfully; stop retrying
|
|
317
|
+
return;
|
|
216
318
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const ctx = {
|
|
222
|
-
accText,
|
|
223
|
-
sendMetadata,
|
|
224
|
-
};
|
|
225
|
-
if (converter) {
|
|
226
|
-
const finalContent = await converter(accText, ctx);
|
|
227
|
-
await sendEvent("fullContent", {
|
|
228
|
-
type: "replace",
|
|
229
|
-
content: finalContent,
|
|
230
|
-
});
|
|
319
|
+
catch (error) {
|
|
320
|
+
lastError = error;
|
|
321
|
+
if (debug) {
|
|
322
|
+
console.log(`[cch] Provider ${config.provider}/${config.model} failed during streaming:`, error);
|
|
231
323
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
324
|
+
// If aborted, do not proceed further
|
|
325
|
+
if (isAbortError(error) || isAborted)
|
|
326
|
+
return;
|
|
327
|
+
// If any content has already been sent from this attempt, do not fallback
|
|
328
|
+
const contentAlreadySent = lastContentSendLength > 0;
|
|
329
|
+
const isLast = i === providerConfigs.length - 1;
|
|
330
|
+
if (!contentAlreadySent && !isLast) {
|
|
331
|
+
if (debug) {
|
|
332
|
+
console.log("[cch] Falling back to next provider...");
|
|
236
333
|
}
|
|
334
|
+
// try next provider
|
|
335
|
+
continue;
|
|
237
336
|
}
|
|
337
|
+
// Otherwise rethrow to outer catch
|
|
338
|
+
throw error;
|
|
238
339
|
}
|
|
239
|
-
await sendEvent("done", null);
|
|
240
|
-
}
|
|
241
|
-
// 正常終了時もタイマーはクリア
|
|
242
|
-
clearTimeoutSafe();
|
|
243
|
-
if (onFinish && !isAborted) {
|
|
244
|
-
const finishInfo = {
|
|
245
|
-
model: currentModel,
|
|
246
|
-
provider: currentProvider,
|
|
247
|
-
text: accText,
|
|
248
|
-
usage,
|
|
249
|
-
durationMs: Date.now() - startTime,
|
|
250
|
-
};
|
|
251
|
-
onFinish(finishInfo);
|
|
252
340
|
}
|
|
341
|
+
// If we get here, all providers failed before sending any content
|
|
342
|
+
throw (lastError ?? new Error("Failed to create chat stream with any provider"));
|
|
253
343
|
}
|
|
254
344
|
catch (error) {
|
|
255
345
|
// 例外経路でもタイマーをクリア
|
|
@@ -257,26 +347,26 @@ export function createChatHandler(options) {
|
|
|
257
347
|
if (isAbortError(error) || isAborted)
|
|
258
348
|
return;
|
|
259
349
|
if (onError)
|
|
260
|
-
onError(error,
|
|
350
|
+
onError(error, {
|
|
351
|
+
reason: "unknown",
|
|
352
|
+
streamedChars: totalStreamedChars,
|
|
353
|
+
});
|
|
261
354
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
262
355
|
await sendEvent("error", { message: errorMessage });
|
|
263
356
|
}
|
|
264
357
|
finally {
|
|
265
|
-
//
|
|
266
|
-
|
|
358
|
+
// Disable external flusher after request completes
|
|
359
|
+
currentFlushMeta = null;
|
|
360
|
+
// Clear any remaining metadata for safety
|
|
361
|
+
metaQueue.splice(0, metaQueue.length);
|
|
267
362
|
}
|
|
268
363
|
};
|
|
269
364
|
// Create the sendMetadata function
|
|
270
365
|
const sendMetadata = (meta) => {
|
|
271
|
-
|
|
272
|
-
currentSendMetadata(meta);
|
|
273
|
-
}
|
|
366
|
+
enqueueMetadata(meta);
|
|
274
367
|
};
|
|
275
|
-
|
|
276
|
-
// Also make it callable for backward compatibility
|
|
277
|
-
const result = Object.assign(handler, {
|
|
368
|
+
return {
|
|
278
369
|
handler,
|
|
279
370
|
sendMetadata,
|
|
280
|
-
}
|
|
281
|
-
return result;
|
|
371
|
+
};
|
|
282
372
|
}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -30,27 +30,31 @@ export type ServerEmitTools = {
|
|
|
30
30
|
sendMetadata: (meta: Record<string, JSONValue>) => void;
|
|
31
31
|
};
|
|
32
32
|
export type ChatHandler = (req: Request, stream: StreamingApi) => Promise<void>;
|
|
33
|
-
export type ChatHandlerResult =
|
|
33
|
+
export type ChatHandlerResult = {
|
|
34
34
|
handler: ChatHandler;
|
|
35
35
|
sendMetadata: (meta: Record<string, JSONValue>) => void;
|
|
36
36
|
};
|
|
37
37
|
export type StreamTransform = (chunk: string, ctx: StreamTransformContext) => string | Promise<string>;
|
|
38
|
-
export type ContentConverter = (accumulatedText: string, ctx: StreamTransformContext) => string | Promise<string>;
|
|
39
38
|
export type ThrottleSettings = {
|
|
40
39
|
timeMs: number;
|
|
41
40
|
chars: number;
|
|
42
41
|
};
|
|
43
42
|
export type ErrorReason = "unknown" | "timeout" | "aborted";
|
|
43
|
+
export type StreamHandlerOnError = (err: unknown, details: {
|
|
44
|
+
reason: ErrorReason;
|
|
45
|
+
streamedChars: number;
|
|
46
|
+
}) => void;
|
|
47
|
+
export type StreamHandlerOnFinish = (info: FinishInfo) => void;
|
|
44
48
|
export type ServerOptions = {
|
|
45
49
|
providers: AIProviderConfig | AIProviderConfig[];
|
|
46
50
|
transforms?: StreamTransform[];
|
|
47
51
|
throttle?: ThrottleSettings | ((accTextLength: number) => ThrottleSettings);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
onError?: (err: unknown, reason: ErrorReason) => void;
|
|
52
|
+
onFinish?: StreamHandlerOnFinish;
|
|
53
|
+
onError?: StreamHandlerOnError;
|
|
51
54
|
maxOutputTokens?: number;
|
|
52
55
|
timeoutMs?: number;
|
|
53
56
|
temperature?: number;
|
|
54
57
|
messages: Message[];
|
|
58
|
+
debug?: boolean;
|
|
55
59
|
};
|
|
56
60
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;AAE1C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,UAAU,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,UAAU,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE;QACN,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACjC,CAAC;IACF,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,IAAI,CAAC;CACzD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,IAAI,CAAC;CACzD,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEhF,MAAM,MAAM,iBAAiB,GAAG
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
|
package/dist/shared/wire.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { JSONValue } from "./types.js";
|
|
2
|
-
export type SSEEventType = "chunk" | "
|
|
2
|
+
export type SSEEventType = "chunk" | "error" | "done";
|
|
3
3
|
export type SSEChunkData = {
|
|
4
4
|
text: string;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Optional metadata bundled with this chunk. When present, represents
|
|
7
|
+
* one or more metadata payloads that occurred since the previous
|
|
8
|
+
* emitted chunk.
|
|
9
|
+
*/
|
|
10
|
+
meta?: Array<{
|
|
11
|
+
[k: string]: JSONValue;
|
|
12
|
+
}>;
|
|
12
13
|
};
|
|
13
14
|
export type SSEErrorData = {
|
|
14
15
|
message: string;
|
|
@@ -18,12 +19,6 @@ export type SSEDoneData = null;
|
|
|
18
19
|
export type SSEEvent = {
|
|
19
20
|
event: "chunk";
|
|
20
21
|
data: SSEChunkData;
|
|
21
|
-
} | {
|
|
22
|
-
event: "fullContent";
|
|
23
|
-
data: SSEFullContentData;
|
|
24
|
-
} | {
|
|
25
|
-
event: "metadata";
|
|
26
|
-
data: SSEMetadataData;
|
|
27
22
|
} | {
|
|
28
23
|
event: "error";
|
|
29
24
|
data: SSEErrorData;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/shared/wire.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/shared/wire.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC;AAE/B,MAAM,MAAM,QAAQ,GAChB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC"}
|