immer-compose
Version:
A utility for composing concurrent operations, yet allowing state to be merged in series.
104 lines (95 loc) • 3.15 kB
JavaScript
;
var immer = require('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 immer.produce(newState, thunk);
}
for await (const next of watchUpdates(deferred.map((thunk) => immer.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 immer.produceWithPatches(newState, thunk);
newState = next;
patches.push(...patch);
reversed.push(...reverse);
}
for await (const [next, patch, reverse] of watchUpdates(deferred.map((thunk) => immer.produceWithPatches(newState, thunk)))) {
newState = next;
patches.push(...patch);
reversed.push(...reverse);
}
return [newState, patches, reversed];
};
exports.compose = compose;
exports.composeWithPatches = composeWithPatches;