Skip to content

ユーティリティの選び方

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 の非同期版です。呼び出しが未解決のあいだ、あとから来た相手はそれに相乗りします。

memoizeonce の旧称で、やることも同じです。名前から想像されるような、引数ごとのキャッシュはしません。新しく書くコードでは 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 / useI18nranuts/i18n)は翻訳のエンジンです。平らなメッセージ辞書、{param} の差し込み、実行時の切り替えを担います。
  • segmentByRangespaginateText はテキストの配置のためのものです。前者は位置とハイライト、後者は箱に収まるページへの切り分けです。

i18n エンジンを使っていなくても resolveLocale は使う価値があります。この関数が下している判断、つまり読み手の navigator.languages を順番どおりに尊重する(先頭だけを見ない)という部分こそ、自前で書くと間違えやすいところだからです。

モデルの応答をストリーミングする

三つの層があり、それぞれ単独でも使えます。

  1. ranuts/stream: SSE を解析し、createStreamAccumulator() で差分をひとつのスナップショットへ畳み込みます。提供元に依らない形なので、本文・推論・ツール呼び出しの差分は、誰が出したものでも同じ形に落ち着きます。
  2. ranuts/conversation: 追記だけのイベントログを、createConversationEngine() で描画できるノードへ射影します。行が 何であるか を決めるだけで、描画はしません。
  3. ranui の <r-conversation>: それらのノードを実際に描く要素です。表示を末尾に貼り付けたまま保ち、行の差分を突き合わせます。

本文を出すだけなら第 1 層で止めてかまいません。やり取りに射影する価値のある構造が出てきたら第 2 層を、スクロールと差分の突き合わせまで面倒を見てほしくなったら第 3 層を足してください。

どのエントリーから import するか

サブパスはどれも独立した、tree shaking の効くバレルです。そのシンボルを持っているものから import してください。深いソースパスを直接指してはいけません。

import 元中身動く場所
ranuts根のバレル。ユーティリティと visual の面ブラウザー + Node
ranuts/utilsDOM/BOM、文字列、オブジェクト、数値、色、時間、ストレージなどブラウザー + Node*
ranuts/nodeHTTP サーバー、ルーター、WebSocket、fs、ストリーム、ミドルウェアNode 専用
ranuts/visual2D の描画エンジン(Canvas / WebGL / WebGPU)ブラウザー専用
ranuts/i18n翻訳エンジン。DOM に依存しませんブラウザー + Node
ranuts/swキャッシュ戦略と、プリキャッシュ手順の Worker 側Service Worker
ranuts/vnodeSnabbdom 流の仮想 DOMブラウザー
ranuts/streamSSE の解析、モデルストリームの畳み込み、トークンの割り当てブラウザー + Node
ranuts/conversationイベントログから、描画できる会話ノードへブラウザー + Node

* ranuts/utils は幅が広いです。多くはブラウザー向けですが、純粋なヘルパー(文字列、オブジェクト、数値、composecloneDeep など)はどこでも動きます。ブラウザー側のコードで ranuts/node を import してはいけません。 fs / http / child_process を引き込んでしまいます。

それでも決められないときは

API リファレンスを検索してみてください。すべての export が、ソースから生成されたシグネチャと一行の説明つきで載っています。両方の一行を読んでもなお二つが取り替えのきくものに見えるなら、それはドキュメントの不具合です。報告していただけると助かります。

MIT ライセンスのもとで公開されています。