From bdf12cafdbb5500b11f128dcdad4a7e6540fa3b4 Mon Sep 17 00:00:00 2001 From: ochafik Date: Sat, 25 Mar 2023 11:53:17 +0000 Subject: [PATCH] Port some utils: AbortablePromise, turnIntoDelayableExecution, bubbleUpDeepMutations --- src/deep-mutate.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/deep-mutate.ts diff --git a/src/deep-mutate.ts b/src/deep-mutate.ts new file mode 100644 index 0000000..03895f9 --- /dev/null +++ b/src/deep-mutate.ts @@ -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; + +/** + * + * @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(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; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 91bb486..5111b62 100644 --- a/src/utils.ts +++ b/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 = Promise & {kill: Killer} +export function AbortablePromise(f: (resolve: (result: T) => void, reject: (error: any) => void) => Killer): AbortablePromise +{ + let kill: Killer; + const promise = new Promise((res, rej) => { + kill = f(res, rej); + }); + return Object.assign(promise, {kill: kill!}); +} + + +export function turnIntoDelayableExecution(delay: number, job: () => AbortablePromise, 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 = (a: Array, validateElement: (e: T) => T, orElse: () => T[]) => { + if (!(a instanceof Array)) return orElse(); + return a.map(validateElement); +}