Port some utils: AbortablePromise, turnIntoDelayableExecution, bubbleUpDeepMutations
This commit is contained in:
parent
a890b4ce45
commit
bdf12cafdb
2 changed files with 112 additions and 0 deletions
64
src/deep-mutate.ts
Normal file
64
src/deep-mutate.ts
Normal 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;
|
||||
}
|
||||
48
src/utils.ts
48
src/utils.ts
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue