xref: /aosp_15_r20/development/tools/winscope/src/common/persistent_store_proxy.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright 2022, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {Store} from './store';
18
19export class PersistentStoreProxy {
20  static new<T extends object>(
21    key: string,
22    defaultState: T,
23    storage: Store,
24  ): T {
25    const storedState = JSON.parse(storage.get(key) ?? '{}', parseMap);
26    const currentState = mergeDeep({}, structuredClone(defaultState));
27    mergeDeepKeepingStructure(currentState, storedState);
28    return wrapWithPersistentStoreProxy(key, currentState, storage) as T;
29  }
30}
31
32function wrapWithPersistentStoreProxy(
33  storeKey: string,
34  object: object,
35  storage: Store,
36  baseObject: object = object,
37): object {
38  const updatableProps: string[] = [];
39
40  for (const [key, value] of Object.entries(object)) {
41    if (
42      typeof value === 'string' ||
43      typeof value === 'boolean' ||
44      typeof value === 'number' ||
45      value === undefined
46    ) {
47      if (!Array.isArray(object)) {
48        updatableProps.push(key);
49      }
50    } else {
51      (object as any)[key] = wrapWithPersistentStoreProxy(
52        storeKey,
53        value,
54        storage,
55        baseObject,
56      );
57    }
58  }
59  const proxyObj = new Proxy(object, {
60    set: (target, prop, newValue) => {
61      if (typeof prop === 'symbol') {
62        throw new Error("Can't use symbol keys only strings");
63      }
64      if (
65        Array.isArray(target) &&
66        (typeof prop === 'number' || !Number.isNaN(Number(prop)))
67      ) {
68        target[Number(prop)] = newValue;
69        storage.add(storeKey, JSON.stringify(baseObject, stringifyMap));
70        return true;
71      }
72      if (!Array.isArray(target) && Array.isArray(newValue)) {
73        (target as any)[prop] = wrapWithPersistentStoreProxy(
74          storeKey,
75          newValue,
76          storage,
77          baseObject,
78        );
79        storage.add(storeKey, JSON.stringify(baseObject, stringifyMap));
80        return true;
81      }
82      if (!Array.isArray(target) && updatableProps.includes(prop)) {
83        (target as any)[prop] = newValue;
84        storage.add(storeKey, JSON.stringify(baseObject, stringifyMap));
85        return true;
86      }
87      throw new Error(
88        `Object property '${prop}' is not updatable. Can only update leaf keys: [${updatableProps}]`,
89      );
90    },
91  });
92
93  return proxyObj;
94}
95
96function isObject(item: any): boolean {
97  return item && typeof item === 'object' && !Array.isArray(item);
98}
99
100/**
101 * Merge sources into the target keeping the structure of the target.
102 * @param target the object we mutate by merging the data from source into, but keep the object structure of
103 * @param source the object we merge into target
104 * @return the mutated target object
105 */
106function mergeDeepKeepingStructure(target: any, source: any): any {
107  if (isObject(target) && isObject(source)) {
108    for (const key in target) {
109      if (source[key] === undefined) {
110        continue;
111      }
112
113      if (isObject(target[key]) && isObject(source[key])) {
114        mergeDeepKeepingStructure(target[key], source[key]);
115        continue;
116      }
117
118      if (!isObject(target[key]) && !isObject(source[key])) {
119        Object.assign(target, {[key]: source[key]});
120        continue;
121      }
122    }
123  }
124
125  return target;
126}
127
128function mergeDeep(target: any, ...sources: any): any {
129  if (!sources.length) return target;
130  const source = sources.shift();
131
132  if (isObject(target) && isObject(source)) {
133    for (const key in source) {
134      if (isObject(source[key])) {
135        if (!target[key]) Object.assign(target, {[key]: {}});
136        mergeDeep(target[key], source[key]);
137      } else {
138        Object.assign(target, {[key]: source[key]});
139      }
140    }
141  }
142
143  return mergeDeep(target, ...sources);
144}
145
146export function stringifyMap(key: string, value: any) {
147  if (value instanceof Map) {
148    return {
149      type: 'Map',
150      value: [...value],
151    };
152  }
153  return value;
154}
155
156export function parseMap(key: string, value: any) {
157  if (value && value.type === 'Map') {
158    return new Map(value.value);
159  }
160  return value;
161}
162