【GAS】CacheServiceをもっと簡単に使おう

GASでデータを保存する場所は、時折頭を悩ませます。
Webアプリを作成しようとすると、
GSSは、速度に難あり。
ScriptPropatiesサービスは、容量に難あり。
ということで、両方の欠点を補えるCacheサービスをラッピングしました。

<h1>CacheServiceの問題点</h1>

優れているとは言ったものの、致命的な問題もあります。

<h3>・MAX6時間までしか保存できない</h3>

GSSやScriptPropatiesは半永久的に保存しておいてくれるのに、CacheServiceは6時間が限界らしいです。おい。

<h3>・「全てのキーを取得」がない</h3>

じゃ、じゃあ、6時間毎に全てのデータを保存し直せばいいじゃないか(汗

と思ってCacheServiceの公式ドキュメントを読んでみると、<b>全てのキー一覧・データを取得する関数はサポートされていません。</b>ScriptPropatiesならあるのに。


https://developers.google.com/apps-script/reference/cache/cache

なんでやねん。


<h1>ソースコード</h1>

上記の問題点を踏まえ、ラップ以外にも処理を追加しました。
```js
const Cache = (function(){

    const ScriptCache = CacheService.getScriptCache();
  
    const parseJSON = str => {
  
      try{
  
        return JSON.parse(str);
  
      }catch{
  
        return str;
      }
  
    };
  
    return {
  
      /**
       * @return {array} キャッシュに登録されている全てのキーを取得します。[key1,key2,key3,...] 
       */
      getKeys(){
  
        return this.require("ALL_KEYS_OF_THIS");
  
      },
  
      /**
       * キーからデータを取得。
       * @param {string} 取得したいデータのキー。(省略可)
       * @param {array} 取得したいデータのキー一覧。(省略可)
       * @return {object} キーに対応するデータ。省略した場合は、キャッシュされている全てのデータ。
       */
      require(key){
  
        let all;
        if(typeof key === "string"){
  
          return parseJSON(ScriptCache.get(key));
  
        }else if(Array.isArray(key)){
  
          all = ScriptCache.getAll(key);
  
        }else if(!key){
  
          all = ScriptCache.getAll(this.getKeys());
  
        }
  
        const obj = {};
  
        for(const key in all){
          obj[key] = parseJSON(all[key]);
        }
        return obj;
      },
  
      /**
       * データを追加。
       *  
       * @param {string} key キー名
       * @param {object} key {key,value}の形にすること。
       * @param {} value  型はなんでも良い。(省略可)
       */
      exports(key,value){
  
        //""をキー名として許可すると、this.require()で区別がつかない。
        if(!key) throw new Error("第一引数は必須です。");
        if(key === "ALL_KEYS_OF_THIS") throw new Error("禁止ワードです。");
  
  
        //日付をJSONに変換する時の処理。デフォルトだと扱いにくいので上書き。
        Date.prototype.toJSON = function(){
  
            return Utilities.formatDate(this,"JST","yyyy-MM-dd hh:mm:ss");
  
        }
  
        if(typeof key === "string"){
  
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
  
          ScriptCache.put(key,JSON.stringify(value),21600);
  
          keys.push(key);
  
          ScriptCache.put("ALL_KEYS_OF_THIS",JSON.stringify(keys),21600);
  
        }else if(typeof key === "object"){
  
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
  
          for (let i in key){
            key[i] = JSON.stringify(key[i]);
            keys.push(i);
          }
          
          ScriptCache.put("ALL_KEYS_OF_THIS",JSON.stringify(keys),21600)
          ScriptCache.putAll(key,21600);
  
        }
  
        return this;
  
      },
  
      /**
       * データの削除。
       * @param {string} key 削除したいデータのキー名(省略可)
       * @param {array} key 削除したいデータの全てのキー名(省略可)
       * @param {} key 省略した場合、全てのデータを削除。
       */
      remove(key){
  
        if(typeof key==="string"){
  
          ScriptCache.remove(key);
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
          this.exports("ALL_KEYS_OF_THIS",keys.filter(k => k!==key));
  
        }else if(Array.isArray(key)){
  
          ScriptCache.removeAll(key);
          const keys = this.require("ALL_KEYS_OF_THIS") || [];
          this.exports("ALL_KEYS_OF_THIS",keys.filter(k => !keys.includes(key)));
  
        }else if(!key){
  
          ScriptCache.removeAll(this.getKeys());
          this.exports("ALL_KEYS_OF_THIS",[]);
  
        }
  
        return this;
  
      }
  
    }
  })();

/**
 * キャッシュを持続するための関数。
 * 4時間に1度トリガーを回すことを推奨します。
 */
function conserveData(){

  const data = Cache.require();
  Cache.exports(data);

  const TRIGGER_NAME = "conserveData";

  const triggers = ScriptApp.getScriptTriggers();

  if(triggers.some(v => v.getHandlerFunction() === TRIGGER_NAME)) return ;
  
  ScriptApp.newTrigger(TRIGGER_NAME)
           .timeBased()
           .everyHours(4)
           .create();

}
```

<h1>解説</h1>

<h3>①日付の処理</h3>

```js
      Date.prototype.toJSON = function(){

          return Utilities.formatDate(this,"JST","yyyy-MM-dd hh:mm:ss");

      }
```
プロトタイプを汚染しています。よくないです。でもGASなので許してください。

というのも、Date型の標準のtoJSON関数は、かなり扱いづらいんですよね。

```js
  const now = new Date();
  const now_json = JSON.stringify(now);
  console.log(now_json); //"2022-12-27T13:52:56.140Z"
  console.log(new Date(now_json)); //Invalid Date
```
このように、JSONにはできるけど元に戻せない...ので、やむを得ず上書きしました。

<h3>②キー一覧の作成</h3>

おそらく最も重要です。

Cacheに追加するたびに、内部でキー一覧を勝手に追加しています。
```js
     const keys = this.require("ALL_KEY_OF_THIS") || [];

        ScriptCache.put(key,JSON.stringify(value),21600);

        keys.push(key);

        ScriptCache.put("ALL_KEY_OF_THIS",JSON.stringify(keys),21600);
```
ALL_KEY_OF_THISプロパティを用意することで、キー一覧を管理しています。ちなみに、
```js
Cache.getKeys();
```
で、キー一覧を取得できます。また、全ての値を取得するときには、
```js
Cache.require(Cache.getKeys());
Cache.require(); //引数を省略すると、全て取得。
```
このどちらかで行なってください。推奨は下です。

<h3>③データの保持</h3>
CacheServieは、最大で6時間しかデータを保持してくれません。
そのため、数時間おきに保存し直すことで、データの保持を持続させます。

```js

/**
 * キャッシュを持続するための関数。
 * 4時間に1度トリガーを回すことを推奨します。
 */
function conserveData(){

  const data = Cache.require();
  Cache.exports(data);

  const TRIGGER_NAME = "conserveData";

  const triggers = ScriptApp.getScriptTriggers();

  if(triggers.some(v => v.getHandlerFunction() === TRIGGER_NAME)) return ;

  ScriptApp.newTrigger(TRIGGER_NAME)
           .timeBased()
           .everyHours(4)
           .create();

}
```
これを実行すると、4時間毎にキャッシュを保存し直します。
最初は6時間だとギリギリ遅いので5時間にしようと思っていたのですが、GUI上では6時間の次は4時間でしか設定できかったので、ソースコードもそれに合わせました。

conserveData関数を一度実行すれば、トリガーを勝手に作成してデータを保持します。