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 17export class PersistentStoreProxy { 18 static new<T extends object>(key: string, defaultState: T, storage: Storage): T { 19 const storedState = JSON.parse(storage.getItem(key) ?? '{}'); 20 const currentState = mergeDeep({}, structuredClone(defaultState)); 21 mergeDeepKeepingStructure(currentState, storedState); 22 return wrapWithPersistentStoreProxy(key, currentState, storage) as T; 23 } 24} 25 26function wrapWithPersistentStoreProxy( 27 storeKey: string, 28 object: object, 29 storage: Storage, 30 baseObject: object = object 31): object { 32 const updatableProps: string[] = []; 33 34 for (const [key, value] of Object.entries(object)) { 35 if (typeof value === 'string' || typeof value === 'boolean' || value === undefined) { 36 if (!Array.isArray(object)) { 37 updatableProps.push(key); 38 } 39 } else { 40 (object as any)[key] = wrapWithPersistentStoreProxy(storeKey, value, storage, baseObject); 41 } 42 } 43 44 const proxyObj = new Proxy(object, { 45 set: (target, prop, newValue) => { 46 if (typeof prop === 'symbol') { 47 throw Error("Can't use symbol keys only strings"); 48 } 49 if (Array.isArray(target) && typeof prop === 'number') { 50 target[prop] = newValue; 51 storage.setItem(storeKey, JSON.stringify(baseObject)); 52 return true; 53 } 54 if (!Array.isArray(target) && updatableProps.includes(prop)) { 55 (target as any)[prop] = newValue; 56 storage.setItem(storeKey, JSON.stringify(baseObject)); 57 return true; 58 } 59 throw Error( 60 `Object property '${prop}' is not updatable. Can only update leaf keys: [${updatableProps}]` 61 ); 62 }, 63 }); 64 65 return proxyObj; 66} 67 68function isObject(item: any): boolean { 69 return item && typeof item === 'object' && !Array.isArray(item); 70} 71 72/** 73 * Merge sources into the target keeping the structure of the target. 74 * @param target the object we mutate by merging the data from source into, but keep the object structure of 75 * @param source the object we merge into target 76 * @return the mutated target object 77 */ 78function mergeDeepKeepingStructure(target: any, source: any): any { 79 if (isObject(target) && isObject(source)) { 80 for (const key in target) { 81 if (source[key] === undefined) { 82 continue; 83 } 84 85 if (isObject(target[key]) && isObject(source[key])) { 86 mergeDeepKeepingStructure(target[key], source[key]); 87 continue; 88 } 89 90 if (!isObject(target[key]) && !isObject(source[key])) { 91 Object.assign(target, {[key]: source[key]}); 92 continue; 93 } 94 } 95 } 96 97 return target; 98} 99 100function mergeDeep(target: any, ...sources: any): any { 101 if (!sources.length) return target; 102 const source = sources.shift(); 103 104 if (isObject(target) && isObject(source)) { 105 for (const key in source) { 106 if (isObject(source[key])) { 107 if (!target[key]) Object.assign(target, {[key]: {}}); 108 mergeDeep(target[key], source[key]); 109 } else { 110 Object.assign(target, {[key]: source[key]}); 111 } 112 } 113 } 114 115 return mergeDeep(target, ...sources); 116} 117