Skip to content

Commit c59ed0b

Browse files
committed
feat(javascript): provide createIterablePromise helper
1 parent bc1bb54 commit c59ed0b

File tree

15 files changed

+384
-189
lines changed

15 files changed

+384
-189
lines changed

.github/.cache_version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.0.11
1+
0.0.12

clients/algoliasearch-client-javascript/bundlesize.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"files": [
33
{
44
"path": "packages/algoliasearch/dist/algoliasearch.umd.js",
5-
"maxSize": "8.30KB"
5+
"maxSize": "8.40KB"
66
},
77
{
88
"path": "packages/algoliasearch/dist/lite/lite.umd.js",
@@ -30,7 +30,7 @@
3030
},
3131
{
3232
"path": "packages/client-search/dist/client-search.umd.js",
33-
"maxSize": "6.60KB"
33+
"maxSize": "6.70KB"
3434
},
3535
{
3636
"path": "packages/client-sources/dist/client-sources.umd.js",

clients/algoliasearch-client-javascript/packages/client-common/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from './src/createAuth';
22
export * from './src/createEchoRequester';
3-
export * from './src/createRetryablePromise';
3+
export * from './src/createIterablePromise';
44
export * from './src/cache';
55
export * from './src/transporter';
66
export * from './src/createAlgoliaAgent';

clients/algoliasearch-client-javascript/packages/client-common/jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const config: Config.InitialOptions = {
1414
testPathIgnorePatterns: [
1515
'src/__tests__/cache/null-cache.test.ts',
1616
'src/__tests__/cache/memory-cache.test.ts',
17+
'src/__tests__/create-iterable-promise.test.ts',
1718
],
1819
},
1920
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { createIterablePromise } from '../createIterablePromise';
2+
3+
describe('createIterablePromise', () => {
4+
describe('validate', () => {
5+
it('iterates on a `func` until `validate` is met', async () => {
6+
let calls = 0;
7+
const promise = createIterablePromise({
8+
func: () => {
9+
return new Promise((resolve) => {
10+
calls += 1;
11+
resolve(`success #${calls}`);
12+
});
13+
},
14+
validate: () => calls >= 3,
15+
});
16+
17+
await expect(promise).resolves.toEqual('success #3');
18+
expect(calls).toBe(3);
19+
});
20+
21+
it('forward the response of the `func`', async () => {
22+
let calls = 0;
23+
const promise = createIterablePromise<number>({
24+
func: () => {
25+
return new Promise((resolve) => {
26+
calls += 1;
27+
resolve(calls);
28+
});
29+
},
30+
validate: (response) => response >= 3,
31+
});
32+
33+
await expect(promise).resolves.toEqual(3);
34+
expect(calls).toBe(3);
35+
});
36+
});
37+
38+
describe('aggregator', () => {
39+
it('is called before iterating', async () => {
40+
let calls = 0;
41+
let count = 0;
42+
const promise = createIterablePromise({
43+
func: () => {
44+
return new Promise((resolve) => {
45+
calls += 1;
46+
resolve(`success #${calls}`);
47+
});
48+
},
49+
validate: () => calls >= 3,
50+
aggregator: () => (count += 3),
51+
});
52+
53+
await expect(promise).resolves.toEqual('success #3');
54+
expect(calls).toBe(3);
55+
expect(count).toBe(3 * 3);
56+
});
57+
58+
it('forward the response of the `func`', async () => {
59+
let calls = 0;
60+
const responses: string[] = [];
61+
const promise = createIterablePromise<string>({
62+
func: () => {
63+
return new Promise((resolve) => {
64+
calls += 1;
65+
resolve(`success #${calls}`);
66+
});
67+
},
68+
validate: () => calls >= 3,
69+
aggregator: (response) => {
70+
responses.push(response);
71+
},
72+
});
73+
74+
await expect(promise).resolves.toEqual('success #3');
75+
expect(calls).toBe(3);
76+
expect(responses).toEqual(['success #1', 'success #2', 'success #3']);
77+
});
78+
});
79+
80+
describe('timeout', () => {
81+
it('defaults to no timeout (0)', async () => {
82+
let calls = 0;
83+
const before = Date.now();
84+
const promise = createIterablePromise({
85+
func: () => {
86+
return new Promise((resolve) => {
87+
calls += 1;
88+
resolve(`success #${calls}`);
89+
});
90+
},
91+
validate: () => calls >= 2,
92+
});
93+
94+
await expect(promise).resolves.toEqual('success #2');
95+
96+
expect(Date.now() - before).toBeGreaterThanOrEqual(0);
97+
expect(Date.now() - before).toBeLessThan(10);
98+
expect(calls).toBe(2);
99+
});
100+
101+
it('waits before calling the `func` again', async () => {
102+
let calls = 0;
103+
const before = Date.now();
104+
const promise = createIterablePromise({
105+
func: () => {
106+
return new Promise((resolve) => {
107+
calls += 1;
108+
resolve(`success #${calls}`);
109+
});
110+
},
111+
validate: () => calls >= 2,
112+
timeout: () => 2000,
113+
});
114+
115+
await expect(promise).resolves.toEqual('success #2');
116+
117+
expect(Date.now() - before).toBeGreaterThanOrEqual(2000);
118+
expect(Date.now() - before).toBeLessThan(2010);
119+
expect(calls).toBe(2);
120+
});
121+
});
122+
123+
describe('error', () => {
124+
it('gets the rejection of the given promise via reject', async () => {
125+
let calls = 0;
126+
127+
const promise = createIterablePromise({
128+
func: () => {
129+
return new Promise((resolve, reject) => {
130+
calls += 1;
131+
if (calls <= 3) {
132+
resolve('okay');
133+
} else {
134+
reject(new Error('nope'));
135+
}
136+
});
137+
},
138+
validate: () => false,
139+
});
140+
141+
await expect(promise).rejects.toEqual(
142+
expect.objectContaining({ message: 'nope' })
143+
);
144+
});
145+
146+
it('gets the rejection of the given promise via throw', async () => {
147+
let calls = 0;
148+
149+
const promise = createIterablePromise({
150+
func: () => {
151+
return new Promise((resolve) => {
152+
calls += 1;
153+
if (calls <= 3) {
154+
resolve('okay');
155+
} else {
156+
throw new Error('nope');
157+
}
158+
});
159+
},
160+
validate: () => false,
161+
});
162+
163+
await expect(promise).rejects.toEqual(
164+
expect.objectContaining({ message: 'nope' })
165+
);
166+
});
167+
168+
it('rejects with the given `message` when `validate` hits', async () => {
169+
const MAX_RETRIES = 3;
170+
let calls = 0;
171+
172+
const promise = createIterablePromise({
173+
func: () => {
174+
return new Promise((resolve) => {
175+
calls += 1;
176+
resolve('okay');
177+
});
178+
},
179+
validate: () => false,
180+
error: {
181+
validate: () => calls >= MAX_RETRIES,
182+
message: () => `Error is thrown: ${calls}/${MAX_RETRIES}`,
183+
},
184+
});
185+
186+
await expect(promise).rejects.toEqual(
187+
expect.objectContaining({
188+
message: 'Error is thrown: 3/3',
189+
})
190+
);
191+
expect(calls).toBe(MAX_RETRIES);
192+
});
193+
194+
it('forward the response of the `func`', async () => {
195+
const MAX_RETRIES = 3;
196+
let calls = 0;
197+
198+
const promise = createIterablePromise<number>({
199+
func: () => {
200+
return new Promise((resolve) => {
201+
calls += 1;
202+
resolve(calls);
203+
});
204+
},
205+
validate: () => false,
206+
error: {
207+
validate: (response) => response >= MAX_RETRIES,
208+
message: (response) => `Error is thrown: ${response}/${MAX_RETRIES}`,
209+
},
210+
});
211+
212+
await expect(promise).rejects.toEqual(
213+
expect.objectContaining({
214+
message: 'Error is thrown: 3/3',
215+
})
216+
);
217+
expect(calls).toBe(MAX_RETRIES);
218+
});
219+
});
220+
});

clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/create-retryable-promise.test.ts

Lines changed: 0 additions & 86 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { CreateIterablePromise } from './types/CreateIterablePromise';
2+
3+
/**
4+
* Helper: Returns the promise of a given `func` to iterate on, based on a given `validate` condition.
5+
*
6+
* @param createIterator - The createIterator options.
7+
* @param createIterator.func - The function to run, which returns a promise.
8+
* @param createIterator.validate - The validator function. It receives the resolved return of `func`.
9+
* @param createIterator.aggregator - The function that runs right after the `func` method has been executed, allows you to do anything with the response before `validate`.
10+
* @param createIterator.error - The `validate` condition to throw an error, and its message.
11+
* @param createIterator.timeout - The function to decide how long to wait between iterations.
12+
*/
13+
export function createIterablePromise<TResponse>({
14+
func,
15+
validate,
16+
aggregator,
17+
error,
18+
timeout = (): number => 0,
19+
}: CreateIterablePromise<TResponse>): Promise<TResponse> {
20+
const retry = (): Promise<TResponse> => {
21+
return new Promise<TResponse>((resolve, reject) => {
22+
func()
23+
.then((response) => {
24+
if (aggregator) {
25+
aggregator(response);
26+
}
27+
28+
if (validate(response)) {
29+
return resolve(response);
30+
}
31+
32+
if (error && error.validate(response)) {
33+
return reject(new Error(error.message(response)));
34+
}
35+
36+
return setTimeout(() => {
37+
retry().then(resolve).catch(reject);
38+
}, timeout());
39+
})
40+
.catch((err) => {
41+
reject(err);
42+
});
43+
});
44+
};
45+
46+
return retry();
47+
}

0 commit comments

Comments
 (0)