1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport {produce, Draft} from 'immer'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {getPath, Path, setPath} from './object_utils'; 17*6dbdd20aSAndroid Build Coastguard Worker 18*6dbdd20aSAndroid Build Coastguard Workerexport type Migrate<T> = (init: unknown) => T; 19*6dbdd20aSAndroid Build Coastguard Workerexport type Edit<T> = (draft: Draft<T>) => void; 20*6dbdd20aSAndroid Build Coastguard Workerexport type Callback<T> = (store: Store<T>, previous: T) => void; 21*6dbdd20aSAndroid Build Coastguard Worker 22*6dbdd20aSAndroid Build Coastguard Worker/** 23*6dbdd20aSAndroid Build Coastguard Worker * Create a new root-level store. 24*6dbdd20aSAndroid Build Coastguard Worker * 25*6dbdd20aSAndroid Build Coastguard Worker * @template T The type of this store's state. 26*6dbdd20aSAndroid Build Coastguard Worker * @param {T} initialState Initial state of the store. 27*6dbdd20aSAndroid Build Coastguard Worker * @returns {Store<T>} The newly created store. 28*6dbdd20aSAndroid Build Coastguard Worker */ 29*6dbdd20aSAndroid Build Coastguard Workerexport function createStore<T>(initialState: T): Store<T> { 30*6dbdd20aSAndroid Build Coastguard Worker return new RootStore<T>(initialState); 31*6dbdd20aSAndroid Build Coastguard Worker} 32*6dbdd20aSAndroid Build Coastguard Worker 33*6dbdd20aSAndroid Build Coastguard Workerexport interface Store<T> extends Disposable { 34*6dbdd20aSAndroid Build Coastguard Worker /** 35*6dbdd20aSAndroid Build Coastguard Worker * Access the immutable state of this store. 36*6dbdd20aSAndroid Build Coastguard Worker */ 37*6dbdd20aSAndroid Build Coastguard Worker get state(): T; 38*6dbdd20aSAndroid Build Coastguard Worker 39*6dbdd20aSAndroid Build Coastguard Worker /** 40*6dbdd20aSAndroid Build Coastguard Worker * Mutate the store's state. 41*6dbdd20aSAndroid Build Coastguard Worker * 42*6dbdd20aSAndroid Build Coastguard Worker * @param edits The edit (or edits) to the store. 43*6dbdd20aSAndroid Build Coastguard Worker */ 44*6dbdd20aSAndroid Build Coastguard Worker edit(edits: Edit<T> | Edit<T>[]): void; 45*6dbdd20aSAndroid Build Coastguard Worker 46*6dbdd20aSAndroid Build Coastguard Worker /** 47*6dbdd20aSAndroid Build Coastguard Worker * Create a sub-store from a subtree of the state from this store. 48*6dbdd20aSAndroid Build Coastguard Worker * 49*6dbdd20aSAndroid Build Coastguard Worker * The returned store looks and feels like a regular store but acts only on a 50*6dbdd20aSAndroid Build Coastguard Worker * specific subtree of its parent store. Reads are writes are channelled 51*6dbdd20aSAndroid Build Coastguard Worker * through to the parent store via the |migrate| function. 52*6dbdd20aSAndroid Build Coastguard Worker * 53*6dbdd20aSAndroid Build Coastguard Worker * |migrate| is called the first time we access our sub-store's state and 54*6dbdd20aSAndroid Build Coastguard Worker * whenever the subtree changes in the root store. 55*6dbdd20aSAndroid Build Coastguard Worker * This migrate function takes the state of the subtree from the sub-store's 56*6dbdd20aSAndroid Build Coastguard Worker * parent store which has unknown type and is responsible for returning a 57*6dbdd20aSAndroid Build Coastguard Worker * value whose type matches that of the sub-store's state. 58*6dbdd20aSAndroid Build Coastguard Worker * 59*6dbdd20aSAndroid Build Coastguard Worker * Sub-stores may be created over the top of subtrees which are not yet fully 60*6dbdd20aSAndroid Build Coastguard Worker * defined. The state is written to the parent store on first edit. The 61*6dbdd20aSAndroid Build Coastguard Worker * sub-store can also deal with the underlying subtree becoming undefined 62*6dbdd20aSAndroid Build Coastguard Worker * again at some point in the future, and so is robust to unpredictable 63*6dbdd20aSAndroid Build Coastguard Worker * changes to the root store. 64*6dbdd20aSAndroid Build Coastguard Worker * 65*6dbdd20aSAndroid Build Coastguard Worker * @template U The type of the sub-store's state. 66*6dbdd20aSAndroid Build Coastguard Worker * @param path The path to the subtree this sub-store is based on. 67*6dbdd20aSAndroid Build Coastguard Worker * @example 68*6dbdd20aSAndroid Build Coastguard Worker * // Given a store whose state takes the form: 69*6dbdd20aSAndroid Build Coastguard Worker * { 70*6dbdd20aSAndroid Build Coastguard Worker * foo: { 71*6dbdd20aSAndroid Build Coastguard Worker * bar: [ {baz: 123}, {baz: 42} ], 72*6dbdd20aSAndroid Build Coastguard Worker * }, 73*6dbdd20aSAndroid Build Coastguard Worker * } 74*6dbdd20aSAndroid Build Coastguard Worker * 75*6dbdd20aSAndroid Build Coastguard Worker * // A sub-store crated on path: ['foo','bar', 1] would only see the state: 76*6dbdd20aSAndroid Build Coastguard Worker * { 77*6dbdd20aSAndroid Build Coastguard Worker * baz: 42, 78*6dbdd20aSAndroid Build Coastguard Worker * } 79*6dbdd20aSAndroid Build Coastguard Worker * @param migrate A function used to migrate from the parent store's subtree 80*6dbdd20aSAndroid Build Coastguard Worker * to the sub-store's state. 81*6dbdd20aSAndroid Build Coastguard Worker * @example 82*6dbdd20aSAndroid Build Coastguard Worker * interface RootState {dict: {[key: string]: unknown}}; 83*6dbdd20aSAndroid Build Coastguard Worker * interface SubState {foo: string}; 84*6dbdd20aSAndroid Build Coastguard Worker * 85*6dbdd20aSAndroid Build Coastguard Worker * const store = createStore({dict: {}}); 86*6dbdd20aSAndroid Build Coastguard Worker * const migrate = (init: unknown) => (init ?? {foo: 'bar'}) as SubState; 87*6dbdd20aSAndroid Build Coastguard Worker * const subStore = store.createSubStore(store, ['dict', 'foo'], migrate); 88*6dbdd20aSAndroid Build Coastguard Worker * // |dict['foo']| will be created the first time we edit our sub-store. 89*6dbdd20aSAndroid Build Coastguard Worker * Warning: Migration functions should properly validate the incoming state. 90*6dbdd20aSAndroid Build Coastguard Worker * Blindly using type assertions can lead to instability. 91*6dbdd20aSAndroid Build Coastguard Worker * @returns {Store<U>} The newly created sub-store. 92*6dbdd20aSAndroid Build Coastguard Worker */ 93*6dbdd20aSAndroid Build Coastguard Worker createSubStore<U>(path: Path, migrate: Migrate<U>): Store<U>; 94*6dbdd20aSAndroid Build Coastguard Worker 95*6dbdd20aSAndroid Build Coastguard Worker /** 96*6dbdd20aSAndroid Build Coastguard Worker * Subscribe for notifications when any edits are made to this store. 97*6dbdd20aSAndroid Build Coastguard Worker * 98*6dbdd20aSAndroid Build Coastguard Worker * @param callback The function to be called. 99*6dbdd20aSAndroid Build Coastguard Worker * @returns When this is disposed, the subscription is removed. 100*6dbdd20aSAndroid Build Coastguard Worker */ 101*6dbdd20aSAndroid Build Coastguard Worker subscribe(callback: Callback<T>): Disposable; 102*6dbdd20aSAndroid Build Coastguard Worker} 103*6dbdd20aSAndroid Build Coastguard Worker 104*6dbdd20aSAndroid Build Coastguard Worker/** 105*6dbdd20aSAndroid Build Coastguard Worker * This class implements a standalone store (i.e. one that does not depend on a 106*6dbdd20aSAndroid Build Coastguard Worker * subtree of another store). 107*6dbdd20aSAndroid Build Coastguard Worker * @template T The type of the store's state. 108*6dbdd20aSAndroid Build Coastguard Worker */ 109*6dbdd20aSAndroid Build Coastguard Workerclass RootStore<T> implements Store<T> { 110*6dbdd20aSAndroid Build Coastguard Worker private internalState: T; 111*6dbdd20aSAndroid Build Coastguard Worker private subscriptions = new Set<Callback<T>>(); 112*6dbdd20aSAndroid Build Coastguard Worker 113*6dbdd20aSAndroid Build Coastguard Worker constructor(initialState: T) { 114*6dbdd20aSAndroid Build Coastguard Worker // Run initial state through immer to take advantage of auto-freezing 115*6dbdd20aSAndroid Build Coastguard Worker this.internalState = produce(initialState, () => {}); 116*6dbdd20aSAndroid Build Coastguard Worker } 117*6dbdd20aSAndroid Build Coastguard Worker 118*6dbdd20aSAndroid Build Coastguard Worker get state() { 119*6dbdd20aSAndroid Build Coastguard Worker return this.internalState; 120*6dbdd20aSAndroid Build Coastguard Worker } 121*6dbdd20aSAndroid Build Coastguard Worker 122*6dbdd20aSAndroid Build Coastguard Worker edit(edit: Edit<T> | Edit<T>[]): void { 123*6dbdd20aSAndroid Build Coastguard Worker if (Array.isArray(edit)) { 124*6dbdd20aSAndroid Build Coastguard Worker this.applyEdits(edit); 125*6dbdd20aSAndroid Build Coastguard Worker } else { 126*6dbdd20aSAndroid Build Coastguard Worker this.applyEdits([edit]); 127*6dbdd20aSAndroid Build Coastguard Worker } 128*6dbdd20aSAndroid Build Coastguard Worker } 129*6dbdd20aSAndroid Build Coastguard Worker 130*6dbdd20aSAndroid Build Coastguard Worker private applyEdits(edits: Edit<T>[]): void { 131*6dbdd20aSAndroid Build Coastguard Worker const originalState = this.internalState; 132*6dbdd20aSAndroid Build Coastguard Worker 133*6dbdd20aSAndroid Build Coastguard Worker const newState = edits.reduce((state, edit) => { 134*6dbdd20aSAndroid Build Coastguard Worker return produce(state, edit); 135*6dbdd20aSAndroid Build Coastguard Worker }, originalState); 136*6dbdd20aSAndroid Build Coastguard Worker 137*6dbdd20aSAndroid Build Coastguard Worker this.internalState = newState; 138*6dbdd20aSAndroid Build Coastguard Worker 139*6dbdd20aSAndroid Build Coastguard Worker // Notify subscribers 140*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.forEach((sub) => { 141*6dbdd20aSAndroid Build Coastguard Worker sub(this, originalState); 142*6dbdd20aSAndroid Build Coastguard Worker }); 143*6dbdd20aSAndroid Build Coastguard Worker } 144*6dbdd20aSAndroid Build Coastguard Worker 145*6dbdd20aSAndroid Build Coastguard Worker createSubStore<U>(path: Path, migrate: Migrate<U>): Store<U> { 146*6dbdd20aSAndroid Build Coastguard Worker return new SubStore(this, path, migrate); 147*6dbdd20aSAndroid Build Coastguard Worker } 148*6dbdd20aSAndroid Build Coastguard Worker 149*6dbdd20aSAndroid Build Coastguard Worker subscribe(callback: Callback<T>): Disposable { 150*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.add(callback); 151*6dbdd20aSAndroid Build Coastguard Worker return { 152*6dbdd20aSAndroid Build Coastguard Worker [Symbol.dispose]: () => { 153*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.delete(callback); 154*6dbdd20aSAndroid Build Coastguard Worker }, 155*6dbdd20aSAndroid Build Coastguard Worker }; 156*6dbdd20aSAndroid Build Coastguard Worker } 157*6dbdd20aSAndroid Build Coastguard Worker 158*6dbdd20aSAndroid Build Coastguard Worker [Symbol.dispose]() { 159*6dbdd20aSAndroid Build Coastguard Worker // No-op 160*6dbdd20aSAndroid Build Coastguard Worker } 161*6dbdd20aSAndroid Build Coastguard Worker} 162*6dbdd20aSAndroid Build Coastguard Worker 163*6dbdd20aSAndroid Build Coastguard Worker/** 164*6dbdd20aSAndroid Build Coastguard Worker * This class implements a sub-store, one that is based on a subtree of another 165*6dbdd20aSAndroid Build Coastguard Worker * store. The parent store can be a root level store or another sub-store. 166*6dbdd20aSAndroid Build Coastguard Worker * 167*6dbdd20aSAndroid Build Coastguard Worker * This particular implementation of a sub-tree implements a write-through cache 168*6dbdd20aSAndroid Build Coastguard Worker * style implementation. The sub-store's state is cached internally and all 169*6dbdd20aSAndroid Build Coastguard Worker * edits are written through to the parent store as with a best-effort approach. 170*6dbdd20aSAndroid Build Coastguard Worker * If the subtree does not exist in the parent store, an error is printed to 171*6dbdd20aSAndroid Build Coastguard Worker * the console but the operation is still treated as a success. 172*6dbdd20aSAndroid Build Coastguard Worker * 173*6dbdd20aSAndroid Build Coastguard Worker * @template T The type of the sub-store's state. 174*6dbdd20aSAndroid Build Coastguard Worker * @template ParentT The type of the parent store's state. 175*6dbdd20aSAndroid Build Coastguard Worker */ 176*6dbdd20aSAndroid Build Coastguard Workerclass SubStore<T, ParentT> implements Store<T> { 177*6dbdd20aSAndroid Build Coastguard Worker private parentState: unknown; 178*6dbdd20aSAndroid Build Coastguard Worker private cachedState: T; 179*6dbdd20aSAndroid Build Coastguard Worker private parentStoreSubscription: Disposable; 180*6dbdd20aSAndroid Build Coastguard Worker private subscriptions = new Set<Callback<T>>(); 181*6dbdd20aSAndroid Build Coastguard Worker 182*6dbdd20aSAndroid Build Coastguard Worker constructor( 183*6dbdd20aSAndroid Build Coastguard Worker private readonly parentStore: Store<ParentT>, 184*6dbdd20aSAndroid Build Coastguard Worker private readonly path: Path, 185*6dbdd20aSAndroid Build Coastguard Worker private readonly migrate: (init: unknown) => T, 186*6dbdd20aSAndroid Build Coastguard Worker ) { 187*6dbdd20aSAndroid Build Coastguard Worker this.parentState = getPath<unknown>(this.parentStore.state, this.path); 188*6dbdd20aSAndroid Build Coastguard Worker 189*6dbdd20aSAndroid Build Coastguard Worker // Run initial state through immer to take advantage of auto-freezing 190*6dbdd20aSAndroid Build Coastguard Worker this.cachedState = produce(migrate(this.parentState), () => {}); 191*6dbdd20aSAndroid Build Coastguard Worker 192*6dbdd20aSAndroid Build Coastguard Worker // Subscribe to parent store changes. 193*6dbdd20aSAndroid Build Coastguard Worker this.parentStoreSubscription = this.parentStore.subscribe(() => { 194*6dbdd20aSAndroid Build Coastguard Worker const newRootState = getPath<unknown>(this.parentStore.state, this.path); 195*6dbdd20aSAndroid Build Coastguard Worker if (newRootState !== this.parentState) { 196*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.forEach((callback) => { 197*6dbdd20aSAndroid Build Coastguard Worker callback(this, this.cachedState); 198*6dbdd20aSAndroid Build Coastguard Worker }); 199*6dbdd20aSAndroid Build Coastguard Worker } 200*6dbdd20aSAndroid Build Coastguard Worker }); 201*6dbdd20aSAndroid Build Coastguard Worker } 202*6dbdd20aSAndroid Build Coastguard Worker 203*6dbdd20aSAndroid Build Coastguard Worker get state(): T { 204*6dbdd20aSAndroid Build Coastguard Worker const parentState = getPath<unknown>(this.parentStore.state, this.path); 205*6dbdd20aSAndroid Build Coastguard Worker if (this.parentState === parentState) { 206*6dbdd20aSAndroid Build Coastguard Worker return this.cachedState; 207*6dbdd20aSAndroid Build Coastguard Worker } else { 208*6dbdd20aSAndroid Build Coastguard Worker this.parentState = parentState; 209*6dbdd20aSAndroid Build Coastguard Worker return (this.cachedState = produce(this.cachedState, () => { 210*6dbdd20aSAndroid Build Coastguard Worker return this.migrate(parentState); 211*6dbdd20aSAndroid Build Coastguard Worker })); 212*6dbdd20aSAndroid Build Coastguard Worker } 213*6dbdd20aSAndroid Build Coastguard Worker } 214*6dbdd20aSAndroid Build Coastguard Worker 215*6dbdd20aSAndroid Build Coastguard Worker edit(edit: Edit<T> | Edit<T>[]): void { 216*6dbdd20aSAndroid Build Coastguard Worker if (Array.isArray(edit)) { 217*6dbdd20aSAndroid Build Coastguard Worker this.applyEdits(edit); 218*6dbdd20aSAndroid Build Coastguard Worker } else { 219*6dbdd20aSAndroid Build Coastguard Worker this.applyEdits([edit]); 220*6dbdd20aSAndroid Build Coastguard Worker } 221*6dbdd20aSAndroid Build Coastguard Worker } 222*6dbdd20aSAndroid Build Coastguard Worker 223*6dbdd20aSAndroid Build Coastguard Worker private applyEdits(edits: Edit<T>[]): void { 224*6dbdd20aSAndroid Build Coastguard Worker const originalState = this.cachedState; 225*6dbdd20aSAndroid Build Coastguard Worker 226*6dbdd20aSAndroid Build Coastguard Worker const newState = edits.reduce((state, edit) => { 227*6dbdd20aSAndroid Build Coastguard Worker return produce(state, edit); 228*6dbdd20aSAndroid Build Coastguard Worker }, originalState); 229*6dbdd20aSAndroid Build Coastguard Worker 230*6dbdd20aSAndroid Build Coastguard Worker this.parentState = newState; 231*6dbdd20aSAndroid Build Coastguard Worker try { 232*6dbdd20aSAndroid Build Coastguard Worker this.parentStore.edit((draft) => { 233*6dbdd20aSAndroid Build Coastguard Worker setPath(draft, this.path, newState); 234*6dbdd20aSAndroid Build Coastguard Worker }); 235*6dbdd20aSAndroid Build Coastguard Worker } catch (error) { 236*6dbdd20aSAndroid Build Coastguard Worker if (error instanceof TypeError) { 237*6dbdd20aSAndroid Build Coastguard Worker console.warn('Failed to update parent store at ', this.path); 238*6dbdd20aSAndroid Build Coastguard Worker } else { 239*6dbdd20aSAndroid Build Coastguard Worker throw error; 240*6dbdd20aSAndroid Build Coastguard Worker } 241*6dbdd20aSAndroid Build Coastguard Worker } 242*6dbdd20aSAndroid Build Coastguard Worker 243*6dbdd20aSAndroid Build Coastguard Worker this.cachedState = newState; 244*6dbdd20aSAndroid Build Coastguard Worker 245*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.forEach((sub) => { 246*6dbdd20aSAndroid Build Coastguard Worker sub(this, originalState); 247*6dbdd20aSAndroid Build Coastguard Worker }); 248*6dbdd20aSAndroid Build Coastguard Worker } 249*6dbdd20aSAndroid Build Coastguard Worker 250*6dbdd20aSAndroid Build Coastguard Worker createSubStore<SubtreeState>( 251*6dbdd20aSAndroid Build Coastguard Worker path: Path, 252*6dbdd20aSAndroid Build Coastguard Worker migrate: Migrate<SubtreeState>, 253*6dbdd20aSAndroid Build Coastguard Worker ): Store<SubtreeState> { 254*6dbdd20aSAndroid Build Coastguard Worker return new SubStore(this, path, migrate); 255*6dbdd20aSAndroid Build Coastguard Worker } 256*6dbdd20aSAndroid Build Coastguard Worker 257*6dbdd20aSAndroid Build Coastguard Worker subscribe(callback: Callback<T>): Disposable { 258*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.add(callback); 259*6dbdd20aSAndroid Build Coastguard Worker return { 260*6dbdd20aSAndroid Build Coastguard Worker [Symbol.dispose]: () => { 261*6dbdd20aSAndroid Build Coastguard Worker this.subscriptions.delete(callback); 262*6dbdd20aSAndroid Build Coastguard Worker }, 263*6dbdd20aSAndroid Build Coastguard Worker }; 264*6dbdd20aSAndroid Build Coastguard Worker } 265*6dbdd20aSAndroid Build Coastguard Worker 266*6dbdd20aSAndroid Build Coastguard Worker [Symbol.dispose]() { 267*6dbdd20aSAndroid Build Coastguard Worker this.parentStoreSubscription[Symbol.dispose](); 268*6dbdd20aSAndroid Build Coastguard Worker } 269*6dbdd20aSAndroid Build Coastguard Worker} 270