Skip to content

Commit

Permalink
feat: add custom race function (#177)
Browse files Browse the repository at this point in the history
* add custom race function
* made race cancellable
* run all tests
* rewrite tests
* reject errors in fns
  • Loading branch information
jeetiss authored Feb 3, 2020
1 parent 7615c98 commit 35e4ec4
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/tools/race.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import CancelController from '../../src/tools/CancelController'

type Callback = () => void
type StrangeFn<T> = (args: {
callback: Callback
cancel: CancelController
}) => Promise<T>

const race = <T>(
fns: StrangeFn<T>[],
{ cancel }: { cancel?: CancelController } = {}
): Promise<T> => {
let lastError: Error | null = null
let winnerIndex: number | null = null
const controllers = fns.map(() => new CancelController())
const createCancelCallback = (i: number) => (): void => {
winnerIndex = i

controllers.forEach(
(controller, index) => index !== i && controller.cancel()
)
}

if (cancel) {
cancel.onCancel(() => {
controllers.forEach(controller => controller.cancel())
})
}

return Promise.all(
fns.map((fn, i) => {
const callback = createCancelCallback(i)

return Promise.resolve()
.then(() => fn({ callback, cancel: controllers[i] }))
.then(result => {
callback()

return result
})
.catch(error => {
lastError = error
return null
})
})
).then(results => {
if (winnerIndex === null) {
throw lastError
} else {
return results[winnerIndex] as T
}
})
}

export { race }
175 changes: 175 additions & 0 deletions test/tools/race.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import CancelController from '../../src/tools/CancelController'
import { race } from '../../src/tools/race'
import { cancelError } from '../../src/tools/errors'

const returnAfter = (
value: number,
cancel: CancelController,
ms = 30
): Promise<number> =>
new Promise<number>((resolve, reject) => {
const id = setTimeout(resolve, ms, value)
cancel.onCancel(() => {
clearTimeout(id)
reject(cancelError('race cancel'))
})
})

describe('race', () => {
it('should work', async () => {
const value = await race([
({ cancel }): Promise<number> => returnAfter(1, cancel),
({ cancel }): Promise<number> => returnAfter(2, cancel, 1),
({ cancel }): Promise<number> => returnAfter(3, cancel),
({ cancel }): Promise<number> => returnAfter(4, cancel),
({ cancel }): Promise<number> => returnAfter(5, cancel)
])

expect(value).toBe(2)
})

it('should work if first function fails sync', async () => {
const value = await race([
(): Promise<number> => {
throw new Error('test 1')
},
({ cancel }): Promise<number> => returnAfter(2, cancel, 1),
({ cancel }): Promise<number> => returnAfter(3, cancel),
({ cancel }): Promise<number> => returnAfter(4, cancel),
({ cancel }): Promise<number> => returnAfter(5, cancel)
])

expect(value).toBe(2)
})

it('should work if first function fails async', async () => {
const value = await race([
(): Promise<number> => Promise.reject('test 1'),
({ cancel }): Promise<number> => returnAfter(2, cancel, 1),
({ cancel }): Promise<number> => returnAfter(3, cancel),
({ cancel }): Promise<number> => returnAfter(4, cancel),
({ cancel }): Promise<number> => returnAfter(5, cancel)
])

expect(value).toBe(2)
})

it('should throw error if all function fails', async () => {
await expect(
race([
(): Promise<number> => Promise.reject(new Error('test 1')),
(): Promise<number> => Promise.reject(new Error('test 2')),
(): Promise<number> => Promise.reject(new Error('test 3')),
(): Promise<number> => Promise.reject(new Error('test 4')),
(): Promise<number> => Promise.reject(new Error('test 5'))
])
).rejects.toThrowError('test 5')
})

it('should cancel all functions when first resolves', async () => {
const spies = Array.from({ length: 5 }, i =>
jasmine.createSpy('cancel for ' + i)
)

const createCancelHandler = (index: number) => (error): number => {
spies[index]()

throw error
}

const value = await race([
({ cancel }): Promise<number> =>
returnAfter(1, cancel, 1).catch(createCancelHandler(0)),
({ cancel }): Promise<number> =>
returnAfter(2, cancel).catch(createCancelHandler(1)),
({ cancel }): Promise<number> =>
returnAfter(3, cancel).catch(createCancelHandler(2)),
({ cancel }): Promise<number> =>
returnAfter(4, cancel).catch(createCancelHandler(3)),
({ cancel }): Promise<number> =>
returnAfter(5, cancel).catch(createCancelHandler(4))
])

expect(value).toBe(1)

spies.forEach((spy, index) => {
if (index !== 0) {
expect(spy).toHaveBeenCalled()
}
})
})

it('should cancel all functions when callback fires', async () => {
const spies = Array.from({ length: 5 }, i =>
jasmine.createSpy('cancel for ' + i)
)

const createCancelHandler = (index: number) => (error): number => {
spies[index]()

throw error
}

const value = await race([
({ callback }): Promise<number> => {
callback()

return Promise.resolve(1)
},
({ cancel }): Promise<number> =>
returnAfter(2, cancel).catch(createCancelHandler(1)),
({ cancel }): Promise<number> =>
returnAfter(3, cancel).catch(createCancelHandler(2)),
({ cancel }): Promise<number> =>
returnAfter(4, cancel).catch(createCancelHandler(3)),
({ cancel }): Promise<number> =>
returnAfter(5, cancel).catch(createCancelHandler(4))
])

expect(value).toBe(1)

spies.forEach((spy, index) => {
if (index !== 0) {
expect(spy).toHaveBeenCalled()
}
})
})

it('should be cancellable', async () => {
const cancel = new CancelController()

const spies = Array.from({ length: 5 }, i =>
jasmine.createSpy('cancel for ' + i)
)

const createCancelHandler = (index: number) => (error): number => {
spies[index]()

throw error
}

setTimeout(() => cancel.cancel())

await expect(
race(
[
({ cancel }): Promise<number> =>
returnAfter(1, cancel).catch(createCancelHandler(0)),
({ cancel }): Promise<number> =>
returnAfter(2, cancel).catch(createCancelHandler(1)),
({ cancel }): Promise<number> =>
returnAfter(3, cancel).catch(createCancelHandler(2)),
({ cancel }): Promise<number> =>
returnAfter(4, cancel).catch(createCancelHandler(3)),
({ cancel }): Promise<number> =>
returnAfter(5, cancel).catch(createCancelHandler(4))
],
{ cancel }
)
).rejects.toThrowError('race cancel')

spies.forEach(spy => {
expect(spy).toHaveBeenCalled()
})
})
})

0 comments on commit 35e4ec4

Please sign in to comment.