Port some utils: AbortablePromise, turnIntoDelayableExecution, bubbleUpDeepMutations

This commit is contained in:
ochafik 2023-03-25 11:53:17 +00:00
commit bdf12cafdb
2 changed files with 112 additions and 0 deletions

64
src/deep-mutate.ts Normal file
View file

@ -0,0 +1,64 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
type KVObject = {[key: string]: any};
type KVEntriesMap = Map<KVObject, [string, any][]>;
/**
*
* @param o the object we want to mutate
* @param mutate a function that modifies any part of the object.
* @returns an object tree in which each node is identical to its original if no value under its subtree truly changed. If any did, the node's identity is new.
*/
export function bubbleUpDeepMutations<T extends KVObject>(o: T, mutate: (o: T) => void): T {
const allOriginalEntries = collectObjectEntriesDeeply(o);
mutate(o);
return bubbleChangesUp(o, allOriginalEntries) as T;
}
function collectObjectEntriesDeeply(o: KVObject, out: KVEntriesMap = new Map()): KVEntriesMap {
if (out.get(o)) {
return out; // Graph cycle
}
const entries = [...Object.entries(o)];
out.set(o, entries);
for (const [, v] of entries) {
if (typeof v !== 'object') {
continue;
}
if (v instanceof RegExp) {
continue;
}
collectObjectEntriesDeeply(v, out);
}
return out;
}
function bubbleChangesUp(o: KVObject, allOriginalEntries: KVEntriesMap) {
const entries = Object.entries(o);
const originalEntries = allOriginalEntries.get(o);
if (!originalEntries) {
// the object has already changed as we can't find it, return it = new
return o;
}
let changed = false;
if (entries.length != originalEntries.length) {
changed = true;
} else {
for (let i = 0; i < entries.length; i++) {
const [originalName, originalValue] = originalEntries[i];
const [newName, newValue] = entries[i];
if (originalName !== newName) {
changed = true;
break;
}
const updatedValue = bubbleChangesUp(newValue, allOriginalEntries);
if (updatedValue !== originalValue) {
changed = true;
break;
}
}
}
return changed ? Object.fromEntries(entries) : o;
}

View file

@ -10,3 +10,51 @@ export function mapObject(o: any, f: (key: string, value: any) => any, ifPred: (
}
return ret;
}
type Killer = () => void;
export type AbortablePromise<T> = Promise<T> & {kill: Killer}
export function AbortablePromise<T>(f: (resolve: (result: T) => void, reject: (error: any) => void) => Killer): AbortablePromise<T>
{
let kill: Killer;
const promise = new Promise<T>((res, rej) => {
kill = f(res, rej);
});
return Object.assign(promise, {kill: kill!});
}
export function turnIntoDelayableExecution<T>(delay: number, job: () => AbortablePromise<T>, callback: (result: T) => void) {
let pendingId: number | null;
let runningJobKillSignal: (() => void) | null;
const doExecute = async () => {
if (runningJobKillSignal) {
runningJobKillSignal();
runningJobKillSignal = null;
}
const abortablePromise = job();
runningJobKillSignal = abortablePromise.kill;
try {
callback(await abortablePromise);
} finally {
runningJobKillSignal = null;
}
}
return async ({now}: {now: boolean}) => {
if (pendingId) {
clearTimeout(pendingId);
pendingId = null;
}
if (now) {
doExecute();
} else {
pendingId = window.setTimeout(doExecute, delay);
}
};
}
export const validateString = (s: string, orElse: () => string = () => '') => s != null && typeof s === 'string' ? s : orElse();
export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orElse: () => T[]) => {
if (!(a instanceof Array)) return orElse();
return a.map(validateElement);
}