-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f587d83
commit c35bb8e
Showing
9 changed files
with
215 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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 file contains 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 file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,17 @@ | |
"engines": { | ||
"node": ">=16" | ||
}, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.mjs", | ||
"types": "./dist/index.d.ts", | ||
"exports": { | ||
"./package.json": "./dist/package.json", | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.mjs", | ||
"default": "./dist/index.js" | ||
} | ||
}, | ||
"packageManager": "[email protected]", | ||
"dependencies": { | ||
"@babel/runtime": "^7.18.6" | ||
|
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export const newAbortError = () => | ||
new DOMException('This operation was aborted', 'AbortError') | ||
|
||
const noop = () => {} | ||
|
||
export function abortable<T>( | ||
promise: Promise<T>, | ||
signal: AbortSignal | undefined | ||
): Promise<T> { | ||
if (!signal) return promise | ||
return new Promise<T>((resolve, reject) => { | ||
if (signal.aborted) { | ||
reject(newAbortError()) | ||
return | ||
} | ||
const cleanup = () => { | ||
const callbacks = { resolve, reject } | ||
// Prevent memory leaks. If the input promise never resolves, then the handlers | ||
// below would retain this enclosing Promise's resolve and reject callbacks, | ||
// which would retain the enclosing Promise and anything waiting on it. | ||
// By replacing references to these callbacks, we enable the enclosing Promise to | ||
// be garbage collected | ||
resolve = noop | ||
reject = noop | ||
// Memory could also leak if the signal never aborts, unless we remove the abort | ||
// handler | ||
signal.removeEventListener('abort', onAbort) | ||
return callbacks | ||
} | ||
const onAbort = () => cleanup().reject(newAbortError()) | ||
signal.addEventListener('abort', onAbort) | ||
promise.then( | ||
(value) => cleanup().resolve(value), | ||
(error) => cleanup().reject(error) | ||
) | ||
}) | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { describe, it } from 'mocha' | ||
import { abortable } from '../src/index' | ||
import { expect } from 'chai' | ||
import { withResolvers } from './withResolvers' | ||
import { tried } from './tried' | ||
|
||
describe(`abortable`, function () { | ||
it(`returns promise if signal is undefined`, async function () { | ||
const promise = Promise.resolve(42) | ||
expect(abortable(promise, undefined)).to.equal(promise) | ||
}) | ||
it(`resolves if promise resolves first`, async function () { | ||
const ac = new AbortController() | ||
expect(await abortable(Promise.resolve(42), ac.signal)).to.equal(42) | ||
ac.abort() | ||
}) | ||
it(`rejects if promise rejects first`, async function () { | ||
const ac = new AbortController() | ||
await expect(abortable(Promise.reject(new Error('test')), ac.signal)) | ||
.to.be.rejectedWith(Error) | ||
.that.eventually.deep.equals(new Error('test')) | ||
ac.abort() | ||
}) | ||
it(`rejects if signal is already aborted`, async function () { | ||
const p = withResolvers<number>() | ||
const ac = new AbortController() | ||
ac.abort() | ||
const [, error] = tried(() => ac.signal.throwIfAborted())() | ||
await Promise.all([ | ||
expect(abortable(p.promise, ac.signal)) | ||
.to.be.rejectedWith(DOMException) | ||
.that.eventually.deep.equals(error), | ||
p.resolve(42), | ||
]) | ||
}) | ||
it(`rejects if signal aborts before promise resolves`, async function () { | ||
const p = withResolvers<number>() | ||
const ac = new AbortController() | ||
await Promise.all([ | ||
expect(abortable(p.promise, ac.signal)) | ||
.to.be.rejectedWith(DOMException) | ||
.that.eventually.deep.equals( | ||
new DOMException('This operation was aborted', 'AbortError') | ||
), | ||
ac.abort(), | ||
p.resolve(42), | ||
]) | ||
}) | ||
it(`rejects if signal aborts before promise rejects`, async function () { | ||
const p = withResolvers<number>() | ||
const ac = new AbortController() | ||
await Promise.all([ | ||
expect(abortable(p.promise, ac.signal)) | ||
.to.be.rejectedWith(DOMException) | ||
.that.eventually.deep.equals( | ||
new DOMException('This operation was aborted', 'AbortError') | ||
), | ||
ac.abort(), | ||
p.reject(new Error('test')), | ||
]) | ||
}) | ||
}) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import chai from 'chai' | ||
import chaiAsPromised from 'chai-as-promised' | ||
chai.use(chaiAsPromised) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
type Tried<T> = [T | undefined, any] | ||
|
||
/** | ||
* Helper for getting the result or error of an operation inline, inspired | ||
* by proposed JS try expressions | ||
* | ||
* Examples: | ||
* | ||
* Sync function: | ||
* const [result, error] = tried((x) => x * 2)(21) // [42, undefined] | ||
* const [result, error] = tried(() => { throw new Error('test') })() // [undefined, new Error('test)] | ||
* | ||
* Async function: | ||
* const [result, error] = await tried(async (x) => x * 2)(21) // [42, undefined] | ||
* const [result, error] = await tried(async () => { throw new Error('test') })() // [undefined, new Error('test)] | ||
* | ||
* Promise: | ||
* const [result, error] = await tried(Promise.resolve(42)) // [42, undefined] | ||
* const [result, error] = await tried(Promise.reject(new Error('test'))) // [undefined, new Error('test')] | ||
*/ | ||
export function tried<Args extends any[], T>( | ||
fn: (...args: Args) => T | ||
): (...args: Args) => Tried<T> | ||
export function tried<Args extends any[], T>( | ||
fn: (...args: Args) => PromiseLike<T> | ||
): (...args: Args) => Promise<Tried<T>> | ||
export function tried<T>(promise: PromiseLike<T>): Promise<Tried<T>> | ||
export function tried<Args extends any[], T>( | ||
x: PromiseLike<T> | ((...args: Args) => T | PromiseLike<T>) | ||
): ((...args: Args) => Tried<T> | Promise<Tried<T>>) | Promise<Tried<T>> { | ||
if (isPromiseLike<T>(x)) { | ||
return (x as Promise<T>).then( | ||
(value) => [value, undefined], | ||
(reason) => [undefined, reason] | ||
) | ||
} | ||
return (...args: Args) => { | ||
if (typeof x !== 'function') { | ||
return [ | ||
undefined, | ||
new Error('invalid input, must be a function or a Promise'), | ||
] as const | ||
} | ||
try { | ||
const result = x(...args) | ||
return isPromiseLike<T>(result) ? tried(result) : [result, undefined] | ||
} catch (error) { | ||
return [undefined, error] | ||
} | ||
} | ||
} | ||
|
||
function isPromiseLike<T>(x: any): x is PromiseLike<T> { | ||
return typeof x?.then === 'function' | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export type PromiseWithResolvers<T> = { | ||
promise: Promise<T> | ||
resolve: (value: T | PromiseLike<T>) => void | ||
reject: (reason?: any) => void | ||
} | ||
|
||
/** | ||
* Userland implementation of [Promise.withResolvers]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers}. | ||
* Once we upgrade to Node 22, we can switch to the builtin. | ||
*/ | ||
export function withResolvers<T>(): PromiseWithResolvers<T> | ||
export function withResolvers<T>( | ||
this: PromiseConstructor | ||
): PromiseWithResolvers<T> | ||
export function withResolvers<T>( | ||
this: PromiseConstructor | undefined | ||
): PromiseWithResolvers<T> { | ||
const PromiseConstructor = this || Promise | ||
let resolve, reject | ||
const promise = new PromiseConstructor<T>((res, rej) => { | ||
resolve = res | ||
reject = rej | ||
}) | ||
return { promise, resolve: resolve!, reject: reject! } | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
{ | ||
"extends": "./node_modules/@jcoreio/toolchain-typescript/tsconfig.json", | ||
"include": ["./src", "./test"], | ||
"exclude": ["node_modules"] | ||
"exclude": ["node_modules"], | ||
"compilerOptions": { | ||
"lib": ["es2019", "DOM"] | ||
} | ||
} |