Skip to content
This repository was archived by the owner on Dec 21, 2021. It is now read-only.

Commit 3123fad

Browse files
authored
Merge pull request #250 from streamr-dev/improve-until-util
Improve "until": Don't evaluate the condition twice if it's true from the beginning
2 parents e23fa6d + 4d328cb commit 3123fad

File tree

2 files changed

+77
-18
lines changed

2 files changed

+77
-18
lines changed

src/utils/index.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -528,29 +528,35 @@ export async function sleep(ms: number = 0) {
528528
})
529529
}
530530

531+
// condition could as well return any instead of boolean, could be convenient sometimes if waiting until a value is returned. Maybe change if such use case emerges.
531532
/**
532533
* Wait until a condition is true
533534
* @param condition - wait until this callback function returns true
534535
* @param timeOutMs - stop waiting after that many milliseconds, -1 for disable
535536
* @param pollingIntervalMs - check condition between so many milliseconds
537+
* @param failedMsgFn - append the string return value of this getter function to the error message, if given
538+
* @return the (last) truthy value returned by the condition function
536539
*/
537540
export async function until(condition: MaybeAsync<() => boolean>, timeOutMs = 10000, pollingIntervalMs = 100, failedMsgFn?: () => string) {
538541
const err = new Error(`Timeout after ${timeOutMs} milliseconds`)
539542
let timeout = false
540543
if (timeOutMs > 0) {
541544
setTimeout(() => { timeout = true }, timeOutMs)
542545
}
543-
544546
// Promise wrapped condition function works for normal functions just the same as Promises
545-
while (!await Promise.resolve().then(condition)) { // eslint-disable-line no-await-in-loop
546-
if (timeout) {
547-
if (failedMsgFn) {
548-
err.message += ` ${failedMsgFn()}`
549-
}
550-
throw err
547+
let wasDone
548+
while (!wasDone && !timeout) { // eslint-disable-line no-await-in-loop
549+
wasDone = await Promise.resolve().then(condition) // eslint-disable-line no-await-in-loop
550+
if (!wasDone && !timeout) {
551+
await sleep(pollingIntervalMs) // eslint-disable-line no-await-in-loop
551552
}
552553

553-
await sleep(pollingIntervalMs) // eslint-disable-line no-await-in-loop
554554
}
555-
return condition()
555+
if (timeout) {
556+
if (failedMsgFn) {
557+
err.message += ` ${failedMsgFn()}`
558+
}
559+
throw err
560+
}
561+
return wasDone
556562
}

test/unit/utils.test.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import Debug from 'debug'
33
import express, { Application } from 'express'
44

55
import authFetch from '../../src/rest/authFetch'
6-
import { uuid, getEndpointUrl } from '../../src/utils'
6+
import * as utils from '../../src/utils'
7+
import { describeRepeats } from '../utils'
78
import { Server } from 'http'
89

910
const debug = Debug('StreamrClient::test::utils')
@@ -12,7 +13,7 @@ interface TestResponse {
1213
test: string
1314
}
1415

15-
describe('utils', () => {
16+
describeRepeats('utils', () => {
1617
let session: any
1718
let expressApp: Application
1819
let server: Server
@@ -81,20 +82,72 @@ describe('utils', () => {
8182

8283
describe('uuid', () => {
8384
it('generates different ids', () => {
84-
expect(uuid('test')).not.toEqual(uuid('test'))
85+
expect(utils.uuid('test')).not.toEqual(utils.uuid('test'))
8586
})
8687
it('includes text', () => {
87-
expect(uuid('test')).toContain('test')
88+
expect(utils.uuid('test')).toContain('test')
8889
})
8990
it('increments', () => {
90-
const uid = uuid('test') // generate new text to ensure count starts at 1
91-
expect(uuid(uid) < uuid(uid)).toBeTruthy()
91+
const uid = utils.uuid('test') // generate new text to ensure count starts at 1
92+
expect(utils.uuid(uid) < utils.uuid(uid)).toBeTruthy()
9293
})
9394
})
9495

9596
describe('getEndpointUrl', () => {
96-
const streamId = 'x/y'
97-
const url = getEndpointUrl('http://example.com', 'abc', streamId, 'def')
98-
expect(url.toLowerCase()).toBe('http://example.com/abc/x%2fy/def')
97+
it('works', () => {
98+
const streamId = 'x/y'
99+
const url = utils.getEndpointUrl('http://example.com', 'abc', streamId, 'def')
100+
expect(url.toLowerCase()).toBe('http://example.com/abc/x%2fy/def')
101+
})
102+
})
103+
104+
describe('until', () => {
105+
it('works with sync true', async () => {
106+
const condition = jest.fn(() => true)
107+
await utils.until(condition)
108+
expect(condition).toHaveBeenCalledTimes(1)
109+
})
110+
111+
it('works with async true', async () => {
112+
const condition = jest.fn(async () => true)
113+
await utils.until(condition)
114+
expect(condition).toHaveBeenCalledTimes(1)
115+
})
116+
117+
it('works with sync false -> true', async () => {
118+
let calls = 0
119+
const condition = jest.fn(() => {
120+
calls += 1
121+
return calls > 1
122+
})
123+
await utils.until(condition)
124+
expect(condition).toHaveBeenCalledTimes(2)
125+
})
126+
127+
it('works with sync false -> true', async () => {
128+
let calls = 0
129+
const condition = jest.fn(async () => {
130+
calls += 1
131+
return calls > 1
132+
})
133+
await utils.until(condition)
134+
expect(condition).toHaveBeenCalledTimes(2)
135+
})
136+
137+
it('can time out', async () => {
138+
const condition = jest.fn(() => false)
139+
await expect(async () => {
140+
await utils.until(condition, 100)
141+
}).rejects.toThrow('Timeout')
142+
expect(condition).toHaveBeenCalled()
143+
})
144+
145+
it('can set interval', async () => {
146+
const condition = jest.fn(() => false)
147+
await expect(async () => {
148+
await utils.until(condition, 100, 20)
149+
}).rejects.toThrow('Timeout')
150+
expect(condition).toHaveBeenCalledTimes(5) // exactly 5
151+
})
99152
})
100153
})

0 commit comments

Comments
 (0)