generated at
external-completionにcache機能を導入する
正確には、emoji-completionscrapbox-card-bubble-2にも導入する

背景
external-completion-2などの補完系UserScriptは、タブが読み込まれるたびにAPIを大量に叩いていた
serverに負荷がかかりすぎる
実際はそんなにAPI叩かなくても良い
特に複数のタブを開いている場合は、どこか一つのタブで読み込めばそれで十分なはず

従来の解決策
遅延読み込み機能の導入
一部のAPIは叩かずにすむ
しかしdefaultで読み込まれるproject dataは毎回APIを叩かないといけない

この問題の解決策として、全てのタブで共有するcacheのような仕組みを作ろうと考えた
仕組み
LocalStorageを使う
LocalStorageが使えない場合は、Script内で適当にbufferを作ってそれを使う
つまり従来と同じ
一定期間はcacheからデータを読み込む
データとcacheの更新日時を返す
cacheが更新されると通知を出す
{'key': fetch-cacher-version-${key}, 'item': timestamp}; の更新eventであるStorageEventを補足し、新しく別のeventを投げる
データ構造
2つのkey-value pairsを使う
{'key': fetch-cacher-data-${key}, 'item': JSON.stringify(object)};
{'key': fetch-cacher-version-${key}, 'item': timestamp};
key の名前変えたいな
fetch-cacherはなんか変
key はデータ読み込み時に指定する
timestamp はcacheの更新日時
汎用機能ではなく、scrapbox apiのデータ取得に特化した形にしようかな
汎用機能の実装+scrapbox api用コードの実装と、2つ機能を作る必要がある
汎用実装で提供するべき適切なinterfaceがわからない
まだ使っていないので
一度scrapbox apiのデータ取得に限定したコードを書こう
汎用版は、↑をしばらく運用して、課題を洗い出してからにする
2021-02-20 02:57:01 自分用

Interface
async get(key, fallback, {forceUpdate = false} = {})
key で指定したdataを取得する
dataがない場合は fallback() の返り値を返す
fallback() Promise を返す必要がある
dataが期限切れの場合は一旦cacheを返す
その後 fallback() を実行し、cacheを更新する
forceUpdate: true のときは、強制的にcacheを更新する
cacheではなく fallback() の返り値を直接返す
timestamp(key)
key で指定したdataの更新日時を取得する

テストコード
test1.js
import {dataCache, handleCacheUpdate} from '/api/code/takker/external-completionにcache機能を導入する/script.js'; window.dataCache = dataCache; window.handleCacheUpdate = handleCacheUpdate;

script.js
class DataCache { constructor({id, maxAge}) { this._versionKeyPrefix = `${id}-data-cache-version-`; this._valueKeyPrefix = `${id}-data-cache-value-`; this._keyListKey = `${id}-data-cache-keys`; this._updateEventName = `${id}-data-cache-update`; this._maxAge = maxAge; // 他のタブでdataが更新されたら、その通知を発行する window.addEventListener('storage', e => { if (!e.key.startsWith(this._versionKeyPrefix)) return; const key = e.key.replace(this._versionKeyPrefix, ''); this._dispatchUpdateEvent(key); }); }
script.js
async load(key, fallback, {forceUpdate = false} = {}) { const updated = this.timestamp(key); const result = localStorage.getItem(`${this._valueKeyPrefix}${key}`); if (result === null || forceUpdate) { // 新しいdataを取得する場合 return {data: await this._updateCache(key,fallback), isCache: false,}; } if ((new Date()).getTime() > updated + this._maxAge) { // cacheを取得する this._updateCache(key,fallback); } return {data: JSON.parse(result), isCache: true,}; }
script.js
timestamp(key) { return parseInt(localStorage.getItem(`${this._versionKeyPrefix}${key}`) ?? 0); }
script.js
// dataを全て削除する clear() { const keys = JSON.parse(localStorage.getItem(this._keyListKey) ?? '[]'); for (const key of keys) { localStorage.removeItem(key); } }

内部関数
データを取得してLocal Storageに格納する
script.js
async _updateCache(key, fallback) { const data = await fallback(); localStorage.setItem(`${this._valueKeyPrefix}${key}`, JSON.stringify(data)); localStorage.setItem(`${this._versionKeyPrefix}${key}`, (new Date()).getTime()); // keyの一覧を更新する const keys = JSON.parse(localStorage.getItem(this._keyListKey) ?? '[]'); localStorage.setItem(this._keyListKey, JSON.stringify([...new Set([key, ...keys])])); // 同じタブのscriptに更新通知を出す this._dispatchUpdateEvent(key); return {data, isCache: false,}; } async _dispatchUpdateEvent(key) { const dataString = localStorage.getItem(`${this._valueKeyPrefix}${key}`); window.dispatchEvent(new CustomEvent(this._updateEventName, {bubbles: true, detail: { key, data: dataString !== null ? {data: JSON.parse(dataString), isCache: false,} : undefined, }})); } } export const dataCache = new DataCache({id: 'takker-script', maxAge: 60 * 1000}); export const handleCacheUpdate = (key, callback) => { return addEventListener(dataCache._updateEventName, e => callback(e)); };

#2021-02-20 02:57:46
#2021-02-17 07:33:59
#2021-01-10 12:46:24