go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/store/user_config/user_config.ts (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import { debounce } from 'lodash-es';
    16  import { Duration } from 'luxon';
    17  import {
    18    addDisposer,
    19    addMiddleware,
    20    applySnapshot,
    21    getEnv,
    22    getSnapshot,
    23    Instance,
    24    SnapshotIn,
    25    SnapshotOut,
    26    types,
    27  } from 'mobx-state-tree';
    28  
    29  import { logging } from '@/common/tools/logging';
    30  
    31  import { BuildConfig } from './build_config';
    32  import { TestsConfig } from './tests_config';
    33  
    34  export const V2_CACHE_KEY = 'user-config-v2';
    35  const DEFAULT_TRANSIENT_KEY_TTL = Duration.fromObject({ week: 4 }).toMillis();
    36  
    37  export interface UserConfigEnv {
    38    readonly storage?: Storage;
    39    readonly transientKeysTTL?: number;
    40  }
    41  
    42  export const UserConfig = types
    43    .model('UserConfig', {
    44      id: types.optional(types.identifierNumber, () => Math.random()),
    45      build: types.optional(BuildConfig, {}),
    46      tests: types.optional(TestsConfig, {}),
    47    })
    48    .actions((self) => ({
    49      deleteStaleKeys(before: Date) {
    50        self.build.deleteStaleKeys(before);
    51      },
    52      restoreConfig(storage: Storage) {
    53        try {
    54          const snapshotStr = storage.getItem(V2_CACHE_KEY);
    55          if (snapshotStr === null) {
    56            return;
    57          }
    58          applySnapshot(self, { ...JSON.parse(snapshotStr), id: self.id });
    59        } catch (e) {
    60          logging.error(e);
    61          logging.warn(
    62            'encountered an error when restoring user configs from the cache, deleting it',
    63          );
    64          storage.removeItem(V2_CACHE_KEY);
    65        }
    66      },
    67      enableCaching() {
    68        const env: UserConfigEnv = getEnv(self) || {};
    69        const storage = env.storage || window.localStorage;
    70        const ttl = env.transientKeysTTL || DEFAULT_TRANSIENT_KEY_TTL;
    71  
    72        this.restoreConfig(storage);
    73        this.deleteStaleKeys(new Date(Date.now() - ttl));
    74  
    75        const persistConfig = debounce(
    76          () => storage.setItem(V2_CACHE_KEY, JSON.stringify(getSnapshot(self))),
    77          // Add a tiny delay so updates happened in the same event cycle will
    78          // only trigger one save event.
    79          1,
    80        );
    81  
    82        addDisposer(
    83          self,
    84          // We cannot use `onAction` because it will not intercept actions
    85          // initiated on the ancestor nodes, even if those actions calls the
    86          // actions on this node.
    87          // Use `addMiddleware` allows use to intercept any action acted on this
    88          // node (and its descendent). However, if the parent decided to modify
    89          // the child node without calling its action, we still can't intercept
    90          // it. Currently, there's no way around this.
    91          //
    92          // See https://github.com/mobxjs/mobx-state-tree/issues/1948
    93          addMiddleware(self, (call, next) => {
    94            next(call, (value) => {
    95              // persistConfig AFTER the action is applied.
    96              persistConfig();
    97              return value;
    98            });
    99          }),
   100        );
   101      },
   102    }));
   103  
   104  export type UserConfigInstance = Instance<typeof UserConfig>;
   105  export type UserConfigSnapshotIn = SnapshotIn<typeof UserConfig>;
   106  export type UserConfigSnapshotOut = SnapshotOut<typeof UserConfig>;