Skip to content

Commit

Permalink
feat: add lastCallOnly
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackGlory committed Dec 30, 2023
1 parent 79fc858 commit 994f2bb
Show file tree
Hide file tree
Showing 19 changed files with 345 additions and 44 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ because it can recognizes other errors that match the pattern of `AbortError`.
### LinkedAbortController
```ts
class LinkedAbortController extends AbortController {
constructor(abortSignal: AbortSignal)
constructor(signal: AbortSignal)
}
```

Expand Down Expand Up @@ -53,7 +53,7 @@ If `AbortSignal` is aborted, the promise will be rejected with `AbortError`.

### raceAbortSignals
```ts
function raceAbortSignals(abortSignals: Array<AbortSignal | Falsy>): AbortSignal
function raceAbortSignals(signals: Array<AbortSignal | Falsy>): AbortSignal
```

The `Promise.race` function for `AbortSignal`.
Expand All @@ -62,3 +62,10 @@ The `Promise.race` function for `AbortSignal`.
```ts
function isAbortSignal(val: unknown): val is AbortSignal
```

### lastCallOnly
```ts
function lastCallOnly<T, Args extends unknown[]>(
fn: (...args: [...args: Args, signal: AbortSignal]) => PromiseLike<T>
): (...args: [...args: Args, signal: AbortSignal | Falsy]) => Promise<T>
```
1 change: 1 addition & 0 deletions __tests__/abort-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest'
import { AbortController } from '@src/abort-controller.js'
import { AbortError } from '@src/abort-error.js'

Expand Down
45 changes: 18 additions & 27 deletions __tests__/abort-error.spec.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,43 @@
import { describe, test, expect } from 'vitest'
import { CustomError } from '@blackglory/errors'
import { AbortError } from '@src/abort-error.js'
import { AbortController } from '@src/abort-controller.js'
import { AbortError as AbortErrorFromExtraFetch } from 'node-fetch'
import { getError } from 'return-style'

describe('AbortError', () => {
describe('AbortError instanceof Error', () => {
it('true', () => {
const abortError = new AbortError()
test('AbortError instanceof Error', () => {
const abortError = new AbortError()

expect(abortError).toBeInstanceOf(Error)
})
expect(abortError).toBeInstanceOf(Error)
})

describe('AbortError instanceof CustomError', () => {
it('true', () => {
const abortError = new AbortError()
test('AbortError instanceof CustomError', () => {
const abortError = new AbortError()

expect(abortError).toBeInstanceOf(CustomError)
})
expect(abortError).toBeInstanceOf(CustomError)
})

describe('AbortError instanceof AbortError', () => {
describe('non-native AbortError', () => {
it('true', () => {
const abortError = new AbortError()
test('non-native AbortError', () => {
const abortError = new AbortError()

expect(abortError).toBeInstanceOf(AbortError)
})
expect(abortError).toBeInstanceOf(AbortError)
})

describe('native AbortError', () => {
it('true', () => {
const controller = new AbortController()
controller.abort()
test('native AbortError', () => {
const controller = new AbortController()
controller.abort()

const abortError = getError(() => controller.signal.throwIfAborted())
const abortError = getError(() => controller.signal.throwIfAborted())

expect(abortError).toBeInstanceOf(AbortError)
})
expect(abortError).toBeInstanceOf(AbortError)
})

describe('AbortError from ExtraFetch instanceof AbortError ', () => {
it('true', () => {
const abortError = new AbortErrorFromExtraFetch()
test('AbortError from ExtraFetch instanceof AbortError ', () => {
const abortError = new AbortErrorFromExtraFetch()

expect(abortError).toBeInstanceOf(AbortError)
})
expect(abortError).toBeInstanceOf(AbortError)
})
})
})
1 change: 1 addition & 0 deletions __tests__/abort-signal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, test, expect, vi } from 'vitest'
import { AbortController } from '@src/abort-controller.js'
import { getError } from 'return-style'
import { AbortError } from '@src/abort-error.js'
Expand Down
2 changes: 2 additions & 0 deletions __tests__/exports.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { test, expect } from 'vitest'
import * as target from '@src/index.js'

test('exports', () => {
Expand All @@ -13,6 +14,7 @@ test('exports', () => {
, 'timeoutSignal'
, 'isAbortSignal'
, 'isntAbortSignal'
, 'lastCallOnly'
].sort()

const actualExports = Object.keys(target).sort()
Expand Down
1 change: 1 addition & 0 deletions __tests__/is-abort-signal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest'
import { AbortController } from '@src/abort-controller.js'
import { isAbortSignal, isntAbortSignal } from '@src/is-abort-signal.js'

Expand Down
254 changes: 254 additions & 0 deletions __tests__/last-call-only.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { describe, test, vi, expect } from 'vitest'
import { lastCallOnly } from '@src/last-call-only.js'
import { waitForTimeout, waitForAllMacrotasksProcessed } from '@blackglory/wait-for'
import { getErrorPromise, toResultPromise } from 'return-style'
import { AbortError } from '@src/abort-error.js'

describe('lastCallOnly', () => {
describe('without signal', () => {
test('first call', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})

const newFn = lastCallOnly(fn)
const result = await newFn('foo', undefined)

expect(fn).toBeCalledTimes(1)
expect(result).toBe('foo')
})

describe('not first call', () => {
test('sync', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', undefined))
const promise2 = toResultPromise(newFn('bar', undefined))
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(1)
expect(fn.mock.calls[0][1].aborted).toBe(false)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrap()).toBe('bar')
})

test('async', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', undefined))
await waitForAllMacrotasksProcessed()
const promise2 = toResultPromise(newFn('bar', undefined))
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(2)
expect(fn.mock.calls[0][1].aborted).toBe(true)
expect(fn.mock.calls[1][1].aborted).toBe(false)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrap()).toBe('bar')
})
})
})

describe('with aborted signal', () => {
test('first call', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()
controller.abort()

const newFn = lastCallOnly(fn)
const err = await getErrorPromise(newFn('foo', controller.signal))

expect(fn).toBeCalledTimes(0)
expect(err).toBeInstanceOf(AbortError)
})

describe.each([
['Falsy', undefined]
, ['AbortSignal', new AbortController().signal]
])('not first call (first call with %s)', (_, firstCallSignal) => {
test('sync', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()
controller.abort()

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', firstCallSignal))
const promise2 = toResultPromise(newFn('bar', controller.signal))
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(0)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrapErr()).toBeInstanceOf(AbortError)
})

test('async', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()
controller.abort()

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', firstCallSignal))
await waitForAllMacrotasksProcessed()
const promise2 = toResultPromise(newFn('bar', controller.signal))
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(1)
expect(fn.mock.calls[0][1].aborted).toBe(true)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrapErr()).toBeInstanceOf(AbortError)
})
})
})

describe('with non-aborted signal', () => {
describe('first call', () => {
test('no abort', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()

const newFn = lastCallOnly(fn)
const result = await newFn('foo', controller.signal)

expect(fn).toBeCalledTimes(1)
expect(fn.mock.calls[0][1].aborted).toBe(false)
expect(result).toBe('foo')
})

test('abort', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()

const newFn = lastCallOnly(fn)
const promise = getErrorPromise(newFn('foo', controller.signal))
await waitForAllMacrotasksProcessed()
controller.abort()
const err = await promise

expect(fn).toBeCalledTimes(1)
expect(fn.mock.calls[0][1].aborted).toBe(true)
expect(err).toBeInstanceOf(AbortError)
})
})

describe.each([
['Falsy', undefined]
, ['AbortSignal', new AbortController().signal]
])('not first call (first call with %s)', (_, firstCallSignal) => {
describe('sync', () => {
test('no abort', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', firstCallSignal))
const promise2 = toResultPromise(newFn('bar', controller.signal))
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(1)
expect(fn.mock.calls[0][1].aborted).toBe(false)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrap()).toBe('bar')
})

test('abort', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', firstCallSignal))
const promise2 = toResultPromise(newFn('bar', controller.signal))
await waitForAllMacrotasksProcessed()
controller.abort()
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(1)
expect(fn.mock.calls[0][1].aborted).toBe(true)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrapErr()).toBeInstanceOf(AbortError)
})
})

describe('async', () => {
test('no abort', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', firstCallSignal))
await waitForAllMacrotasksProcessed()
const promise2 = toResultPromise(newFn('bar', controller.signal))
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(2)
expect(fn.mock.calls[0][1].aborted).toBe(true)
expect(fn.mock.calls[1][1].aborted).toBe(false)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrap()).toBe('bar')
})

test('abort', async () => {
const fn = vi.fn(async (value: string, signal: AbortSignal) => {
await waitForTimeout(500, signal)
return value
})
const controller = new AbortController()

const newFn = lastCallOnly(fn)
const promise1 = toResultPromise(newFn('foo', firstCallSignal))
await waitForAllMacrotasksProcessed()
const promise2 = toResultPromise(newFn('bar', controller.signal))
await waitForAllMacrotasksProcessed()
controller.abort()
const result1 = await promise1
const result2 = await promise2

expect(fn).toBeCalledTimes(2)
expect(fn.mock.calls[0][1].aborted).toBe(true)
expect(fn.mock.calls[1][1].aborted).toBe(true)
expect(result1.unwrapErr()).toBeInstanceOf(AbortError)
expect(result2.unwrapErr()).toBeInstanceOf(AbortError)
})
})
})
})
})
1 change: 1 addition & 0 deletions __tests__/linked-abort-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest'
import { LinkedAbortController } from '@src/linked-abort-controller.js'
import { AbortController } from '@src/abort-controller.js'

Expand Down
1 change: 1 addition & 0 deletions __tests__/race-abort-signals.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest'
import { raceAbortSignals } from '@src/race-abort-signals.js'
import { AbortController } from '@src/abort-controller.js'

Expand Down
1 change: 1 addition & 0 deletions __tests__/timeout-signal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest'
import { AbortSignal } from '@src/abort-signal.js'
import { timeoutSignal } from '@src/timeout-signal.js'
import { TIME_ERROR } from '@test/utils.js'
Expand Down

0 comments on commit 994f2bb

Please sign in to comment.