-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add custom race function (#177)
* add custom race function * made race cancellable * run all tests * rewrite tests * reject errors in fns
- Loading branch information
Showing
2 changed files
with
230 additions
and
0 deletions.
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
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 } |
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,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() | ||
}) | ||
}) | ||
}) |