Last active
June 20, 2024 20:46
-
-
Save TheWass/0de9048a32f4be8105f5fa669c25e626 to your computer and use it in GitHub Desktop.
Helpful TS Extensions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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