Skip to content

Commit

Permalink
test: removed only modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
vladcos committed Apr 18, 2023
1 parent 3e6c208 commit 2f6bdac
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 21 deletions.
24 changes: 23 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"lodash.isequal": "^4.5.0",
"lodash.isplainobject": "^4.0.6",
"p-debounce": "^4.0.0",
"p-defer": "^4.0.0"
"p-defer": "^4.0.0",
"promise-supplement": "^1.0.5"
},
"devDependencies": {
"@playwright/test": "^1.32.2",
Expand All @@ -33,6 +34,7 @@
"semantic-release": "^21.0.1",
"tsc-alias": "1.8.5",
"tsconfig-paths": "^4.2.0",
"type-fest": "^3.8.0",
"typescript": "^5.0.3",
"vite": "^4.2.1",
"vite-tsconfig-paths": "^4.0.8",
Expand Down
53 changes: 53 additions & 0 deletions src/abortable-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createPromiseMixin } from 'promise-supplement'

type InnerShape<R> = (
resolve: (value: R) => void,
reject: (reason?: any) => void,
) => () => void

export function getAbortablePromiseTemplate<
R,
O,
A extends any[],
// T = (...inputArguments: A) => any,
// T extends (...inputArguments: any[]) => InnerShape<R> = (
// ...inputArguments: any[]
// ) => InnerShape<R>,
>(
inputShape: <I>(
...inputArguments: A
) => <V>(
resolve: (value: V) => void,
reject: (reason?: any) => void,
) => () => void,
// inputShape: T extends (...inputArguments: A) => infer B
// ? (...inputArguments: A) => B
// : never,
): (...input: O) => Promise<any> {
const controller = new AbortController()
const signal = controller.signal

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/promise-function-async
return function (...arguments_) {
const executor = inputShape(...arguments_)

return createPromiseMixin(
new Promise<R>((resolve, reject) => {
if (signal.aborted) {
reject(signal.reason)
}
const abortHandler = executor(resolve, reject)

signal.addEventListener('abort', () => {
abortHandler()
reject(signal.reason)
})
}),
{
abort: (reason?: any) => {
controller.abort(reason)
},
},
)
}
}
90 changes: 75 additions & 15 deletions src/aborter.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,126 @@
import pDebounce from 'p-debounce'
import pDefer from 'p-defer'
import { createPromiseMixin } from 'promise-supplement'

import type { SignalFunction } from '@/src/types.js'

import type { DeferredPromise } from 'p-defer'

interface Aborter<V> {
(debounced?: boolean): DeferredPromise<V>['promise'] & {
abortAndReject: () => void
}
abort: () => number
abortAndResolveWith: () => Awaited<DeferredPromise<V>['promise']>
abortAndReject: () => any
getAbortController: () => AbortController
}

interface AbortReference {
internal: AbortController
external: AbortController
}

function refreshControllers(reference: AbortReference) {
const externalListener = () => {
console.log('aborted externally:begin ')
reference.internal.abort()
reference.external = new AbortController()
// reference.external.signal.addEventListener('abort', externalListener)
console.log('aborted externally: end')
}

const interalListener = () => {
console.log('aborted internally: begin')
reference.internal = new AbortController()
reference.internal.signal.addEventListener('abort', interalListener)
console.log('aborted internally: end')
}

reference.external.signal.addEventListener('abort', externalListener)
reference.internal.signal.addEventListener('abort', interalListener)

return reference
}

export function createAborter<V, T extends Promise<V>>(
expression: SignalFunction<V, T>,
{
debug = false,
debounceThreshold = 10,
}: { debug?: boolean; debounceThreshold?: number | false } = {},
): (debounced?: boolean) => DeferredPromise<V>['promise'] {
let previousPromise: T | undefined
let controller = new AbortController()
): Aborter<V> {
let currentRunningPromise: T | undefined
let inhibitAbortExeceptions = true

const abortControllerPointer = refreshControllers({
internal: new AbortController(),
external: new AbortController(),
})

let deferred = pDefer<V>()

deferred.promise = createPromiseMixin(deferred.promise, {
abortAndReject: () => {
abortable.abortAndReject()
},
})

// eslint-disable-next-line @typescript-eslint/promise-function-async
function abortable(debounced = false): DeferredPromise<V>['promise'] {
if (!previousPromise && !debounced) {
if (!currentRunningPromise && !debounced) {
// eslint-disable-next-line no-use-before-define
return debouncedAbortable(true)
}

if (previousPromise) {
controller.abort()
if (currentRunningPromise) {
abortControllerPointer.internal.abort()
if (debug) {
previousPromise
currentRunningPromise
.then((s) => {
console.log(s)
})
.catch((error) => {
console.log(error)
})
}

controller = new AbortController()
}

const newPromise = expression(controller.signal)
const newPromise = expression(abortControllerPointer.internal.signal)

void newPromise
.then((value) => {
const oldDeferred = deferred
deferred = pDefer<V>()

oldDeferred.resolve(value)

previousPromise = undefined
currentRunningPromise = undefined
return value
})
.catch((error) => {
if (
error.code !== DOMException.ABORT_ERR &&
error.code !== 'ABORT_ERR'
(error.code === DOMException.ABORT_ERR ||
error.code === 'ABORT_ERR') &&
inhibitAbortExeceptions
) {
throw error
return
}

deferred.reject(error)
})

previousPromise = newPromise
currentRunningPromise = newPromise
return deferred.promise
}

abortable.abortAndReject = () => {
inhibitAbortExeceptions = false
abortControllerPointer.external.abort()
}

abortable.getAbortController = () => abortControllerPointer.external

const debouncedAbortable =
debounceThreshold === false
? abortable
Expand Down
53 changes: 52 additions & 1 deletion tests/vitest/aborter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { promiseStateAsync } from 'p-state'
import { expect, it, vi } from 'vitest'
import { expect, it, vi, describe } from 'vitest'

import { createAborter } from '@/src/aborter.js'
import { getConventionalTimeoutPromiseAborter } from '@/tests/vitest/util'
Expand Down Expand Up @@ -81,3 +81,54 @@ it('the sequential calls should be debounced', async () => {
await debounce()
expect(getConventionalTimeoutPromiseAborterSpy).toHaveBeenCalledTimes(1)
})

describe.only('promise object augmentation', () => {
it('the promise should have a abort on its prototype', async () => {
const debounce = createAborter(
async (signal) => getConventionalTimeoutPromiseAborter(200, { signal }),
{ debounceThreshold: false },
)

const promise1 = debounce()
const promise2 = debounce()

const promiseAll = expect(
Promise.all([promise1, promise2]),
).rejects.toThrowError('This operation was aborted')
promise1.abortAndReject()
promise2.abortAndReject()
await promiseAll
})
})

describe('exception handling', () => {
it('the aborter should throw exceptions for external aborts', async () => {
const debounce = createAborter(
async (signal) => getConventionalTimeoutPromiseAborter(200, { signal }),
{ debounceThreshold: false },
)

const promise = expect(
Promise.all([debounce(), debounce()]),
).rejects.toThrowError('This operation was aborted')

debounce.abortAndReject()
await promise
})
it('the aborter should passthrough the exceptions unrelated to aborts', async () => {
const debounce = createAborter(
async () =>
new Promise((_resolve, reject) => {
reject(new Error('Reason'))
}),
{ debounceThreshold: false },
)

const promise = expect(
Promise.all([debounce(), debounce()]),
).rejects.toThrowError('Reason')

debounce.abortAndReject()
await promise
})
})
3 changes: 1 addition & 2 deletions tests/vitest/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ it('should wrap executor and passthrough its options', async () => {
expect(spy).toHaveBeenCalledTimes(1)
})

it('case5 ', async () => {
it.only('case5 ', async () => {
const factory = createGateway()

const fun = async (signal: AbortSignal) =>
getTimeoutPromiseAborter(200, signal)

await Promise.all([factory.run(fun), factory.run(fun)])
})

Expand Down
Loading

0 comments on commit 2f6bdac

Please sign in to comment.