memoizableAction.js (2542B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import { asSettled } from "./async-value"; 6 import { validateContext } from "./context"; 7 8 /** 9 * memoizableActon is a utility for actions that should only be performed 10 * once per key. It is useful for loading sources 11 * 12 * For Example 13 * 14 * export const setItem = memoizeableAction( 15 * "setItem", 16 * { 17 * hasValue: ({ a }, { getState }) => hasItem(getState(), a), 18 * getValue: ({ a }, { getState }) => getItem(getState(), a), 19 * createKey: ({ a }) => a, 20 * action: ({ a }, thunkArgs) => doSetItem(a, thunkArgs) 21 * } 22 * ); 23 * 24 * @param {string} name 25 * @param {object} options 26 * @param {Function} options.getValue 27 * Gets the result from the redux store. 28 * @param {Function} options.createKey 29 * Creates a key for the requests map. 30 * @param {Function} options.action 31 * Kicks off the async work for the action. 32 */ 33 export function memoizeableAction(name, { getValue, createKey, action }) { 34 const requests = new Map(); 35 return args => async thunkArgs => { 36 let result = asSettled(getValue(args, thunkArgs)); 37 if (!result) { 38 const key = createKey(args, thunkArgs); 39 if (!requests.has(key)) { 40 requests.set( 41 key, 42 (async () => { 43 try { 44 await action(args, thunkArgs); 45 } catch (e) { 46 console.warn(`Action ${name} had an exception:`, e); 47 } finally { 48 requests.delete(key); 49 } 50 })() 51 ); 52 } 53 54 await requests.get(key); 55 56 if (args.cx) { 57 validateContext(thunkArgs.getState(), args.cx); 58 } 59 60 result = asSettled(getValue(args, thunkArgs)); 61 if (!result) { 62 // Returning null here is not ideal. This means that the action 63 // resolved but 'getValue' didn't return a loaded value, for instance 64 // if the data the action was meant to store was deleted. In a perfect 65 // world we'd throw a ContextError here or handle cancellation somehow. 66 // Throwing will also allow us to change the return type on the action 67 // to always return a promise for the getValue AsyncValue type, but 68 // for now we have to add an additional '| null' for this case. 69 return null; 70 } 71 } 72 73 if (result.state === "rejected") { 74 throw result.value; 75 } 76 return result.value; 77 }; 78 }