simple-ai-sdk 1.0.14 → 1.0.15

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.
@@ -61,6 +61,7 @@ declare class StreamManager implements IStreamManager {
61
61
  private rafFallbackTimer;
62
62
  private getRAF;
63
63
  private getCAF;
64
+ private pendingCloneSessions;
64
65
  private maxSessions;
65
66
  private debug;
66
67
  private ttlMs?;
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,9 @@ class StreamManager {
23
23
  ? g.cancelAnimationFrame
24
24
  : undefined;
25
25
  }
26
+ // 1フレーム内での messages 配列クローンをセッション単位で一度に抑制するためのフラグ
27
+ // 意図: チャンクが高頻度で到着するケースで、配列コピーをフレームあたり1回に抑える
28
+ pendingCloneSessions = new Set();
26
29
  maxSessions;
27
30
  debug;
28
31
  ttlMs;
@@ -119,6 +122,8 @@ class StreamManager {
119
122
  }
120
123
  this.rafScheduled = false;
121
124
  this.listeners.forEach((listener) => listener());
125
+ // 即時通知時はクローン抑制フラグをクリア(次フレームに持ち越さない)
126
+ this.pendingCloneSessions.clear();
122
127
  }
123
128
  else {
124
129
  // ストリーミング中の頻繁な更新は rAF で 1 フレームにまとめる
@@ -131,6 +136,8 @@ class StreamManager {
131
136
  this.rafId = null;
132
137
  this.rafScheduled = false;
133
138
  this.listeners.forEach((listener) => listener());
139
+ // フレームの終わりでクローン抑制フラグをクリア
140
+ this.pendingCloneSessions.clear();
134
141
  });
135
142
  }
136
143
  else {
@@ -139,6 +146,8 @@ class StreamManager {
139
146
  this.rafFallbackTimer = null;
140
147
  this.rafScheduled = false;
141
148
  this.listeners.forEach((listener) => listener());
149
+ // Fallbackでもフレーム相当のタイミングでクリア
150
+ this.pendingCloneSessions.clear();
142
151
  }, 16);
143
152
  }
144
153
  }
@@ -159,8 +168,18 @@ class StreamManager {
159
168
  }
160
169
  }
161
170
  initSession(sessionId, initialMessages = []) {
162
- if (this.state.sessions.has(sessionId))
171
+ const existingSession = this.state.sessions.get(sessionId);
172
+ // streaming中のセッションが存在する場合は、initialMessagesで上書きしない
173
+ if (existingSession) {
174
+ if (existingSession.status === "streaming" || existingSession.status === "submitted") {
175
+ if (this.debug) {
176
+ console.log(`[SM] Skip initialMessages for streaming session: ${sessionId}`);
177
+ }
178
+ return;
179
+ }
180
+ // streaming中でなければ既存のセッションでもそのまま終了
163
181
  return;
182
+ }
164
183
  // Check if we need to remove old sessions
165
184
  if (this.state.sessions.size >= this.maxSessions) {
166
185
  this.removeOldestSession();
@@ -168,6 +187,7 @@ class StreamManager {
168
187
  this.state.sessions.set(sessionId, {
169
188
  id: sessionId,
170
189
  messages: initialMessages,
190
+ messagesVersion: 0,
171
191
  status: "ready",
172
192
  error: null,
173
193
  abortController: null,
@@ -275,7 +295,16 @@ class StreamManager {
275
295
  if (session) {
276
296
  // Update last accessed time
277
297
  session.lastAccessed = Date.now();
298
+ // 修正意図(日本語):
299
+ // - ストリーミング最適化のため、messages 配列の参照は再生成しない設計。
300
+ // そのため、useMemo 等の依存に参照だけを使う場合に更新が伝播しないことがある。
301
+ // - messages が更新されるたびに messagesVersion をインクリメントし、
302
+ // UI で依存として使えるフックを提供するため、この段階で明示的にインクリメントする。
278
303
  Object.assign(session, updates);
304
+ if (Object.prototype.hasOwnProperty.call(updates, "messages")) {
305
+ // messages が指定されたタイミングでのみバージョンを進める
306
+ session.messagesVersion = (session.messagesVersion ?? 0) + 1;
307
+ }
279
308
  // 再レンダリング抑制: 購読者が0のセッションに対する
280
309
  // ストリーミング由来の message 更新では通知しない。
281
310
  // (onFinish/onError等のイベントは immediate=true で通知される)
@@ -366,6 +395,9 @@ class StreamManager {
366
395
  }
367
396
  const decoder = new TextDecoder();
368
397
  let buffer = "";
398
+ // SSEのパース状態(イベントをチャンク間でまたげるようループ外に配置)
399
+ let eventName = null;
400
+ let dataLines = [];
369
401
  const assistantMessage = {
370
402
  id: generateId({ prefix: "msg-" }),
371
403
  role: "assistant",
@@ -383,140 +415,131 @@ class StreamManager {
383
415
  // - 複数行の data: をサポート
384
416
  // - コメント行(先頭 ':')は無視
385
417
  // - event: が無い場合はデフォルトの 'message' を想定するが、本実装ではサーバ仕様に合わせ 'chunk' 等のみ処理
418
+ // - 末尾が改行で終わらない場合でも、ループ終了時に最後のイベントをフラッシュする
386
419
  let failed = false; // 意図: error イベント処理後に通常完了経路や catch を踏まないためのフラグ
387
420
  let shouldTerminate = false; // 意図: done/error 受信時にループ終了を指示
388
- while (true) {
389
- const { done, value } = await reader.read();
390
- if (done)
391
- break;
392
- buffer += decoder.decode(value, { stream: true });
393
- const lines = buffer.split("\n");
394
- buffer = lines.pop() || ""; // 末尾の未完成行をバッファに残す
395
- let eventName = null;
396
- let dataLines = [];
397
- const flushEvent = () => {
398
- if (dataLines.length === 0 && !eventName)
399
- return;
400
- const dataStr = dataLines.join("\n");
401
- // 本ワイヤ仕様では data は常に JSON
402
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
403
- let parsed;
404
- try {
405
- parsed = dataStr ? JSON.parse(dataStr) : null;
406
- }
407
- catch (err) {
408
- console.warn("[SM] Failed to parse JSON data:", dataStr, err);
409
- // JSON でない場合は無視(接続維持のために例外は投げない)
410
- parsed = null;
411
- }
412
- switch (eventName) {
413
- case "chunk": {
414
- if (parsed && typeof parsed.text === "string") {
415
- // stale stream を無視(state 変更前に判定)
416
- const latestId = this.streamIds.get(sessionId);
417
- if (latestId !== streamId)
418
- return; // 意図: 中断済みのストリームからの遅延イベントを破棄
419
- // 大きな文字列連結は O(n^2) になり得るが、まずは単純連結。
420
- // 必要なら閾値超過で配列バッファ化する最適化を検討。
421
- assistantMessage.content += parsed.text;
422
- // 購読者がいる場合のみ通知(いない場合は状態は更新されるが通知は抑制)
423
- const subCount = this.activeSubscribers.get(sessionId) ?? 0;
424
- if (subCount > 0) {
425
- // 配列の再構築を避け、同一配列参照で通知
421
+ const flushEvent = () => {
422
+ if (dataLines.length === 0 && !eventName)
423
+ return;
424
+ const dataStr = dataLines.join("\n");
425
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
+ let parsed;
427
+ try {
428
+ parsed = dataStr ? JSON.parse(dataStr) : null;
429
+ }
430
+ catch (err) {
431
+ console.warn("[SM] Failed to parse JSON data:", dataStr, err);
432
+ parsed = null;
433
+ }
434
+ switch (eventName) {
435
+ case "chunk": {
436
+ if (parsed && typeof parsed.text === "string") {
437
+ const latestId = this.streamIds.get(sessionId);
438
+ if (latestId !== streamId)
439
+ return; // 中断済みのストリームからの遅延イベントを破棄
440
+ // NOTE: 連結は O(n^2) 化を避ける最適化余地がある(必要ならバッファ化)
441
+ assistantMessage.content += parsed.text;
442
+ const subCount = this.activeSubscribers.get(sessionId) ?? 0;
443
+ if (subCount > 0) {
444
+ // フレームあたり1回だけ配列参照を更新
445
+ if (!this.pendingCloneSessions.has(sessionId)) {
446
+ this.pendingCloneSessions.add(sessionId);
426
447
  this.updateSession(sessionId, {
427
- messages: updatedMessages,
428
- }); // Debounced for streaming chunks
448
+ messages: [...updatedMessages],
449
+ }); // rAFバッチで通知
429
450
  }
430
- else if (this.debug) {
431
- console.info(`[SM] chunk suppressed (no subscribers): ${sessionId}`);
451
+ else {
452
+ // 2回目以降は配列複製せず、rAFでの通知に任せる
453
+ // ここでは何もしない(不要なノーティファイを避ける)
432
454
  }
433
455
  }
434
- break;
435
- }
436
- case "fullContent": {
437
- if (parsed &&
438
- parsed.type === "replace" &&
439
- typeof parsed.content === "string") {
440
- // stale stream を無視(state 変更前に判定)
441
- const latestId = this.streamIds.get(sessionId);
442
- if (latestId !== streamId)
443
- return; // 意図: 中断済みのストリームからの遅延イベントを破棄
444
- // Replace message content with transformed version
445
- assistantMessage.content = parsed.content;
446
- this.updateSession(sessionId, {
447
- messages: updatedMessages,
448
- }, true); // Immediate update for full content
456
+ else if (this.debug) {
457
+ console.info(`[SM] chunk suppressed (no subscribers): ${sessionId}`);
449
458
  }
450
- break;
451
459
  }
452
- case "metadata": {
453
- if (parsed) {
454
- // セッションに登録された最新の onMetadata を使用
455
- try {
456
- this.latestOnMetadata.get(sessionId)?.(parsed);
457
- }
458
- catch {
459
- console.error("[SM] Failed to call onMetadata handler");
460
- }
461
- }
462
- break;
463
- }
464
- case "error": {
465
- const err = new Error(parsed?.message || "Stream error");
466
- // ストリームを明示的に中断
467
- try {
468
- abortController.abort();
469
- }
470
- catch {
471
- console.error("[SM] Failed to abort stream on error");
472
- }
473
- // stale stream を無視
460
+ break;
461
+ }
462
+ case "fullContent": {
463
+ if (parsed && parsed.type === "replace" && typeof parsed.content === "string") {
474
464
  const latestId = this.streamIds.get(sessionId);
475
465
  if (latestId !== streamId)
476
- return; // 意図: 中断済みのストリームからの遅延イベントを破棄
466
+ return; // 中断済みのストリームからの遅延イベントを破棄
467
+ assistantMessage.content = parsed.content;
477
468
  this.updateSession(sessionId, {
478
- status: "error",
479
- error: err,
480
- abortController: null,
481
- }, true);
482
- // セッションに登録された最新の onError を使用
483
- try {
484
- this.latestOnError.get(sessionId)?.(err);
485
- }
486
- catch {
487
- console.error("[SM] Failed to call onError handler");
488
- }
489
- // 意図: catch に落とさず通常完了経路も避けるため、フラグを立ててループ終了
490
- failed = true;
491
- shouldTerminate = true;
492
- break;
469
+ messages: [...updatedMessages],
470
+ }, true); // 即時更新
493
471
  }
494
- case "done": {
495
- // stale stream を無視
496
- const latestId = this.streamIds.get(sessionId);
497
- if (latestId !== streamId)
498
- return; // 意図: 中断済みのストリームからの遅延イベントを破棄
499
- this.updateSession(sessionId, {
500
- status: "ready",
501
- abortController: null,
502
- }, true);
503
- // セッションに登録された最新の onFinish のみ呼ぶ
472
+ break;
473
+ }
474
+ case "metadata": {
475
+ if (parsed) {
504
476
  try {
505
- this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages ||
506
- []));
477
+ this.latestOnMetadata.get(sessionId)?.(parsed);
507
478
  }
508
479
  catch {
509
- console.error("[SM] Failed to call onFinish handler");
480
+ console.error("[SM] Failed to call onMetadata handler");
510
481
  }
511
- // 意図: 正常終了。外側の catch へは行かず処理を終了するためループ終了を指示
512
- shouldTerminate = true;
513
- break;
514
482
  }
515
- default:
516
- // 未知イベントは無視
517
- break;
483
+ break;
484
+ }
485
+ case "error": {
486
+ const err = new Error(parsed?.message || "Stream error");
487
+ try {
488
+ abortController.abort();
489
+ }
490
+ catch {
491
+ console.error("[SM] Failed to abort stream on error");
492
+ }
493
+ const latestId = this.streamIds.get(sessionId);
494
+ if (latestId !== streamId)
495
+ return; // 中断済みのストリームからの遅延イベントを破棄
496
+ this.updateSession(sessionId, {
497
+ status: "error",
498
+ error: err,
499
+ abortController: null,
500
+ }, true);
501
+ try {
502
+ this.latestOnError.get(sessionId)?.(err);
503
+ }
504
+ catch {
505
+ console.error("[SM] Failed to call onError handler");
506
+ }
507
+ failed = true;
508
+ shouldTerminate = true;
509
+ break;
518
510
  }
519
- };
511
+ case "done": {
512
+ const latestId = this.streamIds.get(sessionId);
513
+ if (latestId !== streamId)
514
+ return; // 中断済みのストリームからの遅延イベントを破棄
515
+ this.updateSession(sessionId, {
516
+ status: "ready",
517
+ abortController: null,
518
+ }, true);
519
+ try {
520
+ this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages || []));
521
+ }
522
+ catch {
523
+ console.error("[SM] Failed to call onFinish handler");
524
+ }
525
+ shouldTerminate = true;
526
+ break;
527
+ }
528
+ default:
529
+ // 未知イベントは無視
530
+ break;
531
+ }
532
+ };
533
+ while (true) {
534
+ const { done, value } = await reader.read();
535
+ if (done) {
536
+ // ストリーム終了時に、末尾の未フラッシュイベントがあれば処理
537
+ flushEvent();
538
+ break;
539
+ }
540
+ buffer += decoder.decode(value, { stream: true });
541
+ const lines = buffer.split("\n");
542
+ buffer = lines.pop() || ""; // 末尾の未完成行をバッファに残す
520
543
  for (const line of lines) {
521
544
  if (line === "") {
522
545
  // 空行で一つのイベントをフラッシュ
@@ -681,6 +704,7 @@ class StreamManager {
681
704
  this.latestOnMetadata.clear();
682
705
  this.latestOnError.clear();
683
706
  this.streamIds.clear();
707
+ this.pendingCloneSessions.clear();
684
708
  }
685
709
  }
686
710
  // Export the StreamManager class for custom initialization
@@ -3,6 +3,13 @@ export type ChatStatus = "ready" | "submitted" | "streaming" | "error" | "aborte
3
3
  export type ChatSession = {
4
4
  id: string;
5
5
  messages: Message[];
6
+ /**
7
+ * messages の内容が更新されるたびにインクリメントされるバージョン番号。
8
+ * - ストリーミング中は配列参照を再生成しない最適化を行っているため、
9
+ * useMemo 等で参照の変化だけに依存すると再計算されないことがある。
10
+ * - その場合はこの値を依存に含めることで、内容の更新を検知できる。
11
+ */
12
+ messagesVersion: number;
6
13
  status: ChatStatus;
7
14
  error: Error | null;
8
15
  abortController: AbortController | null;
@@ -42,6 +49,11 @@ export type ReloadMessage = (params?: {
42
49
  }) => Promise<void>;
43
50
  export type UseChatSessionReturn = {
44
51
  messages: Message[];
52
+ /**
53
+ * messages の内容更新を検知するためのバージョン番号。
54
+ * useMemo の依存などに利用できる。
55
+ */
56
+ messagesVersion: number;
45
57
  status: ChatStatus;
46
58
  error: Error | null;
47
59
  sendMessage: (content: string, params?: {
@@ -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,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,sBAAsB,CAChC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAC7D,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAE1B,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,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC;;;;;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,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
+ {"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,sBAAsB,CAChC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAC7D,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAE1B,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,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC;;;;;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;;;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 +1 @@
1
- {"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAK9E,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA2ItB"}
1
+ {"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAsB,MAAM,YAAY,CAAC;AAKlG,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA+ItB"}
@@ -35,7 +35,9 @@ export function useChatSession(sessionId, options = {}) {
35
35
  unsubscribe();
36
36
  // アンマウント時にハンドラは消さない(完了時に呼ぶため残す)
37
37
  };
38
- }, [manager, sessionId]), useCallback(() => manager.getSnapshot(), [manager]));
38
+ }, [manager, sessionId]), useCallback(() => manager.getSnapshot(), [manager]),
39
+ // SSR時は安定した空スナップショットを返し、Hydration差分を避ける
40
+ useCallback(() => ({ sessions: new Map() }), []));
39
41
  // ハンドラが変わるたびに最新を登録
40
42
  useEffect(() => {
41
43
  // onFinish が undefined の場合は登録解除される
@@ -52,6 +54,7 @@ export function useChatSession(sessionId, options = {}) {
52
54
  const session = state.sessions.get(sessionId) || {
53
55
  id: sessionId,
54
56
  messages: initialMessages,
57
+ messagesVersion: 0,
55
58
  status: "ready",
56
59
  error: null,
57
60
  abortController: null,
@@ -94,6 +97,7 @@ export function useChatSession(sessionId, options = {}) {
94
97
  }, [sessionId, manager]);
95
98
  return {
96
99
  messages: session.messages,
100
+ messagesVersion: session.messagesVersion ?? 0,
97
101
  status: session.status,
98
102
  error: session.error,
99
103
  sendMessage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-ai-sdk",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "private": false,
5
5
  "description": "Simple AI SDK for Hono / React19+ / OpenAI",
6
6
  "type": "module",