チャット
このパッケージはほとんど state を持ちません。チャットのログはメモリではなくデータベースに
置くものなので、@insession/extension-chat が実際に担っているのは判断です — 何をメッセージと
見なすか、何を wire に流すか、そして host がどの副作用を実行しなければならないか。
そのぶん、このサイトの中で effect 記述子のパターンが最も素直に見える場所でもあります。
下のデモは host 役を演じます: id を採番する偽のデータベースを持ち、broadcast を各メンバーの
受信箱へ配ります。パッケージ自身は I/O を一切しません — 以下の呼び出しはすべて
{ state, effects } を返す純関数です。
試してみること
Section titled “試してみること”- text を押して、2つの受信箱を読み比べてください。送信者の Alice には
ack: my message is now #1が届き、Bob にはメッセージ本体が届きます。この非対称が要点です — 送信者は enter を押した瞬間に自分のメッセージを楽観的に描画済みなので、broadcast からは除外され、 代わりに ack が「手元のコピーに欠けている唯一のもの」=永続 id を運びます。 - 同じ操作で calls ペインを見てください。1クリックで
reduceが2回呼ばれています:chatがpersist-chatを返し、host が draft を保存して id を得て、そのあと初めてchat-persistedが broadcast を作ります。id は保存するまで存在しない — それが1段にできない 理由そのものです。 - whitespace only を押すと
reduceはnullを返します。空行を配信するのではなく、 アクションごと捨てられます。 - sticker (own URL) と sticker (other URL) を続けて押してください。判定するのはページ側の
allowlist(
https://cdn.example.com/stickers/…)で、その答えをstickerAllowedに畳み込んで 渡しています。拒否された方はエラーにならず、キャプションを持ったただのテキストメッセージに 落ちます。取り消されたスタンプが、一緒に書いた本文まで黙って飲み込んではいけないからです。 - reply to last を押してから text を送ると、broadcast は親メッセージへの参照ではなく スナップショットを運びます。引用は「そのとき何が言われたか」の記録だからです。
- react 🔥 と react “nice!” を続けて押してください。前者は
toggle-reactionを返し、 後者はnullを返します。リアクションの絵文字は構造で検証しています — 絵文字図形を含む 書記素クラスタがちょうど1つ — なので任意の絵文字が通り、文章は通りません。なおchat-reaction-updateは送信者を含む全員に届きます: 集計はサーバーにしか計算できないため、 楽観的に描画しておけるものが何も無いからです。 - pin last と unpin を押してください。パッケージ全体で
stateが変わるのはここだけです。 ピン留めは host がメッセージを引く必要があるのでresolve-messageの往復を挟みますが、 解除は引くものが無いので往復しません。 - typing は broadcast するだけで何も触りません — 入力中インジケーターは保存も復元もしません。
裏で動いているコード
Section titled “裏で動いているコード”デモが import しているのは公開パッケージだけです:
import { createChatState } from '@insession/extension-chat';
const chat = createChatState();let state = chat.defaultState();
// 1段目: 正規化と検証。by / uid / stickerAllowed は host が入れる(wire の値を通さない)。const first = chat.reduce(state, 'chat', { text: 'hello there', clientMsgId: 'c-1', by: 'Alice', uid: 'u-Alice', stickerAllowed: false,});
// 2段目: host が persist-chat を実行し、採番された id を返す。for (const effect of first?.effects ?? []) { if (effect.type !== 'persist-chat') continue; const id = await db.insertMessage(effect.draft); const second = chat.reduce(state, 'chat-persisted', { draft: effect.draft, id }); // second.effects に broadcast(送信者除外)・送信者への chat-ack・ // 決して await してはいけない notify-bots が入る。 if (second) state = second.state;}reduce は純関数です — I/O もストレージもトランスポートも持たず、パッケージ全体で唯一
「純粋でない」ものは Date.now() だけで、それすら注入できます。同じ状態機械がサーバーでも
テストでもこのページでも動くのはそのためです。
アクションの全一覧・host が埋めるべきフィールド・永続化のヘルパー(restore)は
パッケージリファレンスを参照してください。