UNPKG

immer-compose

Version:

A utility for composing concurrent operations, yet allowing state to be merged in series.

101 lines (93 loc) 3.1 kB
import { produce, produceWithPatches } from 'immer'; const isObject = (o) => o !== null && typeof o === 'object'; const isFunction = (fn) => typeof fn === 'function'; const isAsyncFunction = (fn) => isFunction(fn) && fn[Symbol.toStringTag] === 'AsyncFunction'; const resolveGracefully = (maybePromise) => Promise.resolve(maybePromise).catch((err) => { console.error(err); return new Error('__SIGNAL__'); }); const immutable = (obj) => { const proxy = new Proxy(obj, { get(...args) { const target = Reflect.get(...args); return isObject(target) ? immutable(target) : target; }, set() { return false; }, }); return proxy; }; const watchUpdates = async function* (promises) { let todo = promises.map((promise, id) => ({ id, promise: promise .then((data) => ({ id, data })) .catch(() => ({ id, error: true })), })); while (todo.length) { const { id, data, error } = await Promise.race(todo.map(({ promise }) => promise)); todo = todo.filter((item) => item.id !== id); if (error || data === undefined) { if (!todo.length) { return; } continue; } yield data; } }; const compose = (...tasks) => async (state, ...args) => { const recipes = tasks.map((task) => task(immutable(state), ...args)); let newState = state; const deferred = []; for (const recipe of recipes) { const thunk = await resolveGracefully(recipe); if (thunk instanceof Error) { return state; } if (!isFunction(thunk)) { continue; } if (isAsyncFunction(thunk)) { deferred.push(thunk); continue; } newState = await produce(newState, thunk); } for await (const next of watchUpdates(deferred.map((thunk) => produce(newState, thunk)))) { newState = next; } return newState; }; const composeWithPatches = (...tasks) => async (state, ...args) => { const recipes = tasks.map((task) => task(immutable(state), ...args)); let newState = state; const patches = []; const reversed = []; const deferred = []; for (const recipe of recipes) { const thunk = await resolveGracefully(recipe); if (thunk instanceof Error) { return [state, [], []]; } if (!isFunction(thunk)) { continue; } if (isAsyncFunction(thunk)) { deferred.push(thunk); continue; } const [next, patch, reverse] = await produceWithPatches(newState, thunk); newState = next; patches.push(...patch); reversed.push(...reverse); } for await (const [next, patch, reverse] of watchUpdates(deferred.map((thunk) => produceWithPatches(newState, thunk)))) { newState = next; patches.push(...patch); reversed.push(...reverse); } return [newState, patches, reversed]; }; export { compose, composeWithPatches };