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の更新日時を取得する
テストコード
code:test1.js
import {dataCache, handleCacheUpdate} from '/api/code/takker/external-completionにcache機能を導入する/script.js';
window.dataCache = dataCache;
window.handleCacheUpdate = handleCacheUpdate;
code: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);
});
}
code: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,};
}
code:script.js
timestamp(key) {
return parseInt(localStorage.getItem(${this._versionKeyPrefix}${key}) ?? 0);
}
code:script.js
// dataを全て削除する
clear() {
const keys = JSON.parse(localStorage.getItem(this._keyListKey) ?? '[]');
for (const key of keys) {
localStorage.removeItem(key);
}
}
内部関数
データを取得してLocal Storageに格納する
code: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));
};