Skip to content

Instantly share code, notes, and snippets.

@TheWass
Last active June 20, 2024 20:46
Show Gist options
  • Save TheWass/0de9048a32f4be8105f5fa669c25e626 to your computer and use it in GitHub Desktop.
Save TheWass/0de9048a32f4be8105f5fa669c25e626 to your computer and use it in GitHub Desktop.
Helpful TS Extensions
/**
* This pairs an ES6 promise and the AbortController together.
* Useful where you need to stop the promise from continuing externally.
* Note: Async/await will not work with this class. Use the toAwaitable function to convert it back to a standard promise.
*/
export class AbortablePromise<T> implements PromiseLike<T> {
private abortController: AbortController;
private promiseWrapper: Promise<T>;
private cancelEvent!: (reason?: unknown) => void;
constructor(source: Promise<T>, abortController?: AbortController) {
this.abortController = abortController || new AbortController();
this.AbortSignal = this.abortController.signal;
this.promiseWrapper = new Promise<T>((resolve, reject) => {
// This abuses a quirk where resolving/rejecting a promise the first time causes the promise to "stick".
// It won't execute additional resolve/rejects after the first, however, the body of the promise will still run.
this.cancelEvent = (reason: unknown = { isCanceled: true }) => reject(reason);
source.then((val) => resolve(val)).catch((err) => reject(err));
});
this.then = (callback) => this.promiseWrapper.then(callback);
this.catch = (callback) => this.promiseWrapper.catch(callback);
this.finally = (callback) => this.promiseWrapper.finally(callback);
}
public AbortSignal: AbortSignal;
public then: Promise<T>['then'];
public catch: Promise<T>['catch'];
public finally: Promise<T>['finally'];
/**
* This will not directly cancel the promise, just send the abort signal to the controller.
* Use the assocated abortSignal to control the results outside of this object.
*/
public abort(reason?: unknown) {
this.abortController.abort(reason);
}
/**
* This will cancel the promise by rejecting it immediately and trigger the abort signal.
*/
public cancel(reason?: unknown) {
this.cancelEvent(reason);
this.abortController.abort(reason);
}
/** This will return the internal promise, enabling the async/await structure. */
public toAwaitable(): Promise<T> {
return this.promiseWrapper;
}
}
export {};
declare global {
type RecArray<T> = Array<RecArray<T> | T>;
interface Array<T> {
/** This processes the callback while awaiting for each element in series */
asyncForEach(callback: (e: T, i: number, a: Array<T>) => Promise<void>): Promise<void>;
/** This processes the callback on each element in parallel, and returns a promise indicating all are done, or one has failed. */
asyncForAll(callback: (e: T, i: number, a: Array<T>) => Promise<void>): Promise<void>;
/** This checks an array of objects by value using JSON serialization. */
includesDeep(element: T): boolean;
/** (UNTESTED) This flattens a multi-dimensional array using a stack */
flattenDeep<R>(): Array<R>;
/** This is a polyfill for ES2019's flat (Recursive on the levels) */
flat<T>(depth?: number): Array<T>;
/** This returns a clone of the array with duplicates removed. Use the comparator for objects. */
distinct(comparator?: (a: T, b: T) => boolean): Array<T>;
/** This removes all elements satisfying the predicate. Like filter, but in place, and faster. */
removeWhere(predicate: (e: T, i: number, a: Array<T>) => boolean): void
}
}
if (!Array.prototype.asyncForEach) {
Array.prototype.asyncForEach = async function<T> (this: Array<T>, callback: (e: T, i: number, a:Array<T>) => Promise<void>) {
for (let index = 0; index < this.length; index++) {
await callback(this[index], index, this);
}
};
}
if (!Array.prototype.asyncForAll) {
Array.prototype.asyncForAll = async function<T> (this: Array<T>, callback: (e: T, i: number, a:Array<T>) => Promise<void>) {
const promises = this.map(callback);
await Promise.all(promises);
};
}
if (!Array.prototype.includesDeep) {
Array.prototype.includesDeep = function<T> (this: Array<T>, elem: T): boolean {
const arr = this.map(val => JSON.stringify(val));
const value = JSON.stringify(elem);
return arr.includes(value);
};
}
if (!Array.prototype.flattenDeep) {
//This is a stack-based flatten. No need for recursion.
Array.prototype.flattenDeep = function<T> (this: RecArray<T>): Array<T> {
const stack = [...this];
const res = [];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
// push back array items, won't modify the original input
stack.push(...next);
} else {
res.push(next);
}
}
// reverse to restore input order
return res.reverse();
};
}
if (!Array.prototype.flat) {
Array.prototype.flat = function<T> (depth?: number) {
// If no depth is specified, default to 1
if (depth == null) {
depth = 1;
}
// Recursively reduce sub-arrays to the specified depth
const flatten = function (arr: Array<T>, depth: number): Array<T> {
// If depth is 0, return the array as-is
if (depth < 1) {
return arr.slice();
}
// Otherwise, concatenate into the parent array
return arr.reduce(function (acc, val) {
return acc.concat(Array.isArray(val) ? flatten(val, depth - 1) : val);
}, []);
};
return flatten(this, depth);
};
}
if (!Array.prototype.distinct) {
Array.prototype.distinct = function<T> (this: Array<T>, comparator: (a: T, b: T) => boolean = (a, b) => a == b): Array<T> {
return this.filter((a, index, self) => self.findIndex(b => comparator(a, b)) == index);
};
}
if (!Array.prototype.removeWhere) {
Array.prototype.removeWhere = function<T> (this: Array<T>, predicate: (e: T, i: number, a: T[]) => boolean): void {
// Removing in reverse means we don't need to futz with the index.
for (let i = this.length - 1; i >= 0; i--) {
const val = this[i];
if (predicate(val, i, this)) {
this.splice(i, 1);
}
}
};
}
/* eslint-disable no-unsafe-finally */
import { assert } from 'chai';
import { describe, it } from 'mocha';
describe('ES6Promise', () => {
it('Executes when called', () => {
const willExecute = () => {
return new Promise<void>((resolve) => {
assert(true);
resolve();
});
};
// Note, there is no .then function, but we still assert(true)
willExecute();
});
it('Does not execute when passed', () => {
const doNotExecute = () => {
return new Promise<void>((resolve) => {
assert.fail('Promise executed :(');
resolve();
});
};
const passToFunction = (func: () => Promise<void>) => {
assert(typeof func == 'function');
// You can now call func() which will execute the promise.
};
passToFunction(doNotExecute);
});
it('Executes in order', (done) => {
let ExecCounter = 0;
const promiseFactory = (callback: () => void) => {
return new Promise<void>((resolve) => {
assert(++ExecCounter == 2, 'Second');
resolve();
assert(++ExecCounter == 3, 'Third');
}).then(() => {
assert(++ExecCounter == 5, 'Fifth');
return callback();
}).then(() => {
assert(++ExecCounter == 7, 'Seventh');
});
};
assert(++ExecCounter == 1, 'First');
promiseFactory(() => {
assert(++ExecCounter == 6, 'Sixth');
}).then(() => {
assert(++ExecCounter == 8, 'Eighth');
done();
});
assert(++ExecCounter == 4, 'Fourth');
});
it('Does weird things with Async Finally', async () => {
let ExecCounter = 0;
const asyncFunction = async (): Promise<number> => {
try {
assert(++ExecCounter == 1, 'First');
// This does run...
return 1;
} catch {
assert.fail('Should not run.');
return 2;
} finally {
assert(++ExecCounter == 2, 'Second');
// ... But this actually gets returned.
return 3;
}
};
const result = await asyncFunction();
assert(++ExecCounter == 3, 'Third');
assert(result == 3);
});
it('Does not execute twice', (done) => {
let ExecCounter = 0;
const promise = new Promise<void>((resolve) => {
assert(++ExecCounter == 1);
resolve();
});
assert(ExecCounter == 1);
promise;
assert(ExecCounter == 1);
promise.then();
assert(ExecCounter == 1);
promise.then(() => done());
});
it('Does not Resolve twice', (done) => {
let ThenCounter = 0;
let ResolveCounter = 0;
let AfterResolve = 0;
const promise = new Promise<void>((resolve) => {
setTimeout(() => {
++ResolveCounter;
resolve();
++AfterResolve;
}, 0);
setTimeout(() => {
++ResolveCounter;
resolve();
++AfterResolve;
}, 0);
});
promise.then(() => {
++ThenCounter;
});
setTimeout(() => {
assert(ThenCounter == 1);
assert(ResolveCounter == 2);
assert(AfterResolve == 2);
done();
}, 5);
});
it('Does not Reject twice', (done) => {
let CatchCounter = 0;
let RejectCounter = 0;
let AfterReject = 0;
const promise = new Promise<void>((resolve, reject) => {
setTimeout(() => {
++RejectCounter;
reject();
++AfterReject;
}, 0);
setTimeout(() => {
++RejectCounter;
reject();
++AfterReject;
}, 0);
});
promise.catch(() => {
++CatchCounter;
});
setTimeout(() => {
assert(CatchCounter == 1);
assert(RejectCounter == 2);
assert(AfterReject == 2);
done();
}, 5);
});
it('Does not Catch after Resolving', (done) => {
let ThenCounter = 0;
let CatchCounter = 0;
let ResolveCounter = 0;
let RejectCounter = 0;
const promise = new Promise<void>((resolve, reject) => {
setTimeout(() => {
++ResolveCounter;
resolve();
}, 0);
setTimeout(() => {
assert(ResolveCounter == 1);
++RejectCounter;
reject();
}, 1);
});
promise.then(() => {
++ThenCounter;
}).catch(() => {
++CatchCounter;
});
setTimeout(() => {
assert(ThenCounter == 1);
assert(CatchCounter == 0);
assert(ResolveCounter == 1);
assert(RejectCounter == 1);
done();
}, 5);
});
it('Does not Then after Rejecting', (done) => {
let ThenCounter = 0;
let CatchCounter = 0;
let ResolveCounter = 0;
let RejectCounter = 0;
const promise = new Promise<void>((resolve, reject) => {
setTimeout(() => {
++RejectCounter;
reject();
}, 0);
setTimeout(() => {
assert(RejectCounter == 1);
++ResolveCounter;
resolve();
}, 1);
});
promise.then(() => {
++ThenCounter;
}).catch(() => {
++CatchCounter;
});
setTimeout(() => {
assert(ThenCounter == 0);
assert(CatchCounter == 1);
assert(ResolveCounter == 1);
assert(RejectCounter == 1);
done();
}, 5);
});
});
export type Group<T extends Record<string, unknown>> = {
key: Partial<T>;
items: T[];
}
export type GroupBy<K> = {
keys: (keyof K)[];
//thenby?: GroupBy<T>;
}
export const groupBy = <T extends Record<string, unknown>>(array: T[], grouping: GroupBy<T>): Group<T>[] => {
const keys = grouping.keys;
const groups = array.reduce((groups, item) => {
const group = groups.find((g: Group<T>) => keys.every(key => item[key] === g.key[key]));
// Not sure if the copy is necessary.
const itemCopy = Object.getOwnPropertyNames(item).reduce((o, key) => ({ ...o, [key]: item[key] }), {} as T);
return group ?
groups.map((g: Group<T>) => (g === group ? { key: g.key, items: [...g.items, itemCopy] } : g)) :
[
...groups,
{
key: keys.reduce((o, key) => ({ ...o, [key]: item[key] }), {} as Partial<T>),
items: [itemCopy]
}
];
}, [] as Group<T>[]);
//return (grouping.thenby != undefined) ? groups.map((g: Group<T>) => ({ ...g, items: groupBy(g.items, grouping.thenby!) })) : groups;
return groups;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment