ユーティリティの選び方
API リファレンスには export のすべてが並んでいます。このページが答えるのは、そこでは答えられない問いです。似たもの二つのうちどちらが欲しいのか、そしてなぜか。
こんなときに:ほしいものはだいたい分かっている(「呼ぶ回数を減らしたい」「一度だけにしたい」「これを保存したい」「Worker とやり取りしたい」)けれど、どの export がそれなのか分からないとき。
まず、プラットフォームにもうありませんか
ranuts は標準ライブラリを置き換えようとはしていません。まずはプラットフォームに手を伸ばし、ユーティリティを使うのはそれが本当に何かを足してくれるときだけにしてください。
| ~の代わりに | プラットフォーム側にあるもの | ranuts のほうを使う場面 |
|---|---|---|
cloneDeep(value) | structuredClone(value) | 値に関数など、structuredClone が受けつけないものが入っているとき。あちらは DataCloneError を投げますが、cloneDeep は写せるものを写し、残りは参照のまま持ち越します。 |
getAllQueryString(url) | new URL(url).searchParams | イテレーターではなく、素のオブジェクトを一発でほしいとき。 |
localStorageGetItem(key) | localStorage.getItem(key) | そのコードが、ストレージのない場所や塞がれた場所でも動くとき。ラッパーは例外を投げずに '' を返します(Safari のプライベートモード、SSR、サンドボックス化された iframe)。 |
escapeHtml(str) | textContent = str | ノードではなく、文字列を組み立てているとき。 |
呼ぶ回数を減らす
「呼ぶ回数を減らす」には、四つのちがう意味がありえます。
| やりたいこと | 使うもの | 挙動 |
|---|---|---|
| 立て込んだ呼び出しのうち、最後の一回だけ(検索ボックス、リサイズ) | debounce(fn, ms) | 立て込みが止まってから ms 後に走ります。立て込んでいるあいだは何も走りません。 |
| 立て込んでいるあいだ、一定の間隔で(スクロール位置、進捗の表示) | throttle(fn, ms) | 最初の呼び出しはすぐ走り、そのあとは ms ごとに多くて一回です。 |
| 生涯でちょうど一度だけ走ってほしい(初期化、一度きりの警告) | once(fn) | 最初の呼び出しで評価し、それ以降の呼び出しはすべて同じ結果を返します。 |
| 同時に呼んだ相手どうしで、進行中の一本のリクエストを分け合う | singleFlight(fn) | once の非同期版です。呼び出しが未解決のあいだ、あとから来た相手はそれに相乗りします。 |
memoize は once の旧称で、やることも同じです。名前から想像されるような、引数ごとのキャッシュはしません。新しく書くコードでは once と書いてください。
効いてくるちがいはここです。キー入力のハンドラーに debounce を掛けると、打っているあいだは何も走りません。throttle なら、ずっと何かが走り続けます。ただし一キーごとではないだけです。検索候補がほしいなら debounce、「残り何文字」のカウンターなら throttle です。
非同期の仕事を手なずける
| やりたいこと | 使うもの |
|---|---|
| たくさんの仕事を、一度に n 本までで走らせたい | new QuestQueue({ simultaneous: n }) |
| 時間がかかりすぎる promise をあきらめたい | withTimeout(promise, ms) |
| …そのうえで例外ではなく既定値で先へ進みたい | withTimeoutFallback(promise, ms, fallback) |
| まったく別の場所から解決する promise がほしい | deferred() |
| Koa 流に非同期の段をつなぎ、各段が次の段を包めるようにしたい | compose(middleware) |
ぜんぶ を一度に走らせたいなら Promise.all が正解です。「一度にぜんぶ」が接続を 60 本開けてしまうなら QuestQueue が正解です。withTimeout は reject するので catch と組にしてください。タイムアウトがあなたにとってエラーでないなら、フォールバック版のほうを使います。
何かを保存する
| 寿命と大きさ | 使うもの |
|---|---|
| リロードを生き延びる小さな文字列 | localStorageSetItem / localStorageGetItem / localStorageRemoveItem |
| 構造のあるデータ、たくさんのレコード、あるいは数 MB を超えるもの | new WebDB({ dbName, stores })。IndexedDB を promise で包んだもの |
| このページから次のページへ手渡す値ひとつ | createHandoff({ dbName, storeName, key }) |
localStorage* のラッパーがあるのは、ストレージが使えない場所(Safari のプライベートモード、サンドボックス化された iframe、サイトデータを止めているブラウザー)でネイティブの呼び出しが例外を投げるからです。設定がひとつ読めないことより、読み出しで落ちることのほうがひどい壊れ方です。ラッパーは '' を返して先へ進みます。
createHandoff は、ほかのどちらも合わない場合のためのものです。ページ遷移をちょうど一回だけ生き延びて、そのあとは消えてほしい値です。
別のコンテキストと話す
| 橋渡しする相手 | 使うもの |
|---|---|
| ページと Web Worker のあいだ。要求と応答の形 | new WorkerClient({ create })。リクエスト ID で返事を突き合わせます |
MessagePort の両端どうし、なんでも | createPortBridge(port) |
| 互いを見つけあう必要のある二つのウィンドウや iframe | 片側で acceptPortBridge()、もう片側でハンドシェイク |
Worker が問いに答える形なら、手を伸ばすべきは WorkerClient です。リクエスト ID がなければ、重なった二つの呼び出しは、届いた返事がどちらのものか区別できません。ブリッジはもう一段低い層です。やり取りが要求と応答の形でないとき、あるいは通り道がすでにあるときに使ってください。
オブジェクトを扱う
| やりたいこと | 使うもの | 補足 |
|---|---|---|
| ほかの誰とも共有しない複製がほしい | cloneDeep(value) | 循環参照と、よく使う組み込み型を扱えます。 |
| 二つの値が同じ中身かどうかを知りたい | isEqual(a, b) | 参照の同一性ではなく、深い比較です。 |
| 二つのオブジェクトを合わせたい | merge(a, b) | 浅い統合です。b のキーが勝ち、a のほうが書き換わります。 |
| いくつかのキーを落としたい | filterObj(obj, keys) | 挙げたキーを除いた複製を返します。 |
ロケールとテキスト
resolveLocale({ supported, … })は、あなたが 用意したロケールのどれを使うかを、いつもの連鎖(明示的な指定、ストレージ、navigator.languages、フォールバック)から選びます。答えるのは「どの言語か」であって、「この文字列は何と言っているか」ではありません。createI18n/useI18n(ranuts/i18n)は翻訳のエンジンです。平らなメッセージ辞書、{param}の差し込み、実行時の切り替えを担います。segmentByRangesとpaginateTextはテキストの配置のためのものです。前者は位置とハイライト、後者は箱に収まるページへの切り分けです。
i18n エンジンを使っていなくても resolveLocale は使う価値があります。この関数が下している判断、つまり読み手の navigator.languages を順番どおりに尊重する(先頭だけを見ない)という部分こそ、自前で書くと間違えやすいところだからです。
モデルの応答をストリーミングする
三つの層があり、それぞれ単独でも使えます。
ranuts/stream: SSE を解析し、createStreamAccumulator()で差分をひとつのスナップショットへ畳み込みます。提供元に依らない形なので、本文・推論・ツール呼び出しの差分は、誰が出したものでも同じ形に落ち着きます。ranuts/conversation: 追記だけのイベントログを、createConversationEngine()で描画できるノードへ射影します。行が 何であるか を決めるだけで、描画はしません。- ranui の
<r-conversation>: それらのノードを実際に描く要素です。表示を末尾に貼り付けたまま保ち、行の差分を突き合わせます。
本文を出すだけなら第 1 層で止めてかまいません。やり取りに射影する価値のある構造が出てきたら第 2 層を、スクロールと差分の突き合わせまで面倒を見てほしくなったら第 3 層を足してください。
どのエントリーから import するか
サブパスはどれも独立した、tree shaking の効くバレルです。そのシンボルを持っているものから import してください。深いソースパスを直接指してはいけません。
| import 元 | 中身 | 動く場所 |
|---|---|---|
ranuts | 根のバレル。ユーティリティと visual の面 | ブラウザー + Node |
ranuts/utils | DOM/BOM、文字列、オブジェクト、数値、色、時間、ストレージなど | ブラウザー + Node* |
ranuts/node | HTTP サーバー、ルーター、WebSocket、fs、ストリーム、ミドルウェア | Node 専用 |
ranuts/visual | 2D の描画エンジン(Canvas / WebGL / WebGPU) | ブラウザー専用 |
ranuts/i18n | 翻訳エンジン。DOM に依存しません | ブラウザー + Node |
ranuts/sw | キャッシュ戦略と、プリキャッシュ手順の Worker 側 | Service Worker |
ranuts/vnode | Snabbdom 流の仮想 DOM | ブラウザー |
ranuts/stream | SSE の解析、モデルストリームの畳み込み、トークンの割り当て | ブラウザー + Node |
ranuts/conversation | イベントログから、描画できる会話ノードへ | ブラウザー + Node |
* ranuts/utils は幅が広いです。多くはブラウザー向けですが、純粋なヘルパー(文字列、オブジェクト、数値、compose、cloneDeep など)はどこでも動きます。ブラウザー側のコードで ranuts/node を import してはいけません。 fs / http / child_process を引き込んでしまいます。
それでも決められないときは
API リファレンスを検索してみてください。すべての export が、ソースから生成されたシグネチャと一行の説明つきで載っています。両方の一行を読んでもなお二つが取り替えのきくものに見えるなら、それはドキュメントの不具合です。報告していただけると助かります。