external-completionにcache機能を導入する
背景
serverに負荷がかかりすぎる
実際はそんなにAPI叩かなくても良い
特に複数のタブを開いている場合は、どこか一つのタブで読み込めばそれで十分なはず
従来の解決策
遅延読み込み機能の導入
一部のAPIは叩かずにすむ
しかしdefaultで読み込まれるproject dataは毎回APIを叩かないといけない
この問題の解決策として、全てのタブで共有するcacheのような仕組みを作ろうと考えた
仕組み
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.jsimport {dataCache, handleCacheUpdate} from '/api/code/takker/external-completionにcache機能を導入する/script.js';
window.dataCache = dataCache;
window.handleCacheUpdate = handleCacheUpdate;
script.jsclass 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));
};