Skip to content

Commit

Permalink
feat(javascript): provide createIterablePromise helper
Browse files Browse the repository at this point in the history
  • Loading branch information
shortcuts committed Aug 1, 2022
1 parent bc1bb54 commit 5e2a1da
Show file tree
Hide file tree
Showing 15 changed files with 372 additions and 190 deletions.
2 changes: 1 addition & 1 deletion .github/.cache_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.11
0.0.12
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"files": [
{
"path": "packages/algoliasearch/dist/algoliasearch.umd.js",
"maxSize": "8.30KB"
"maxSize": "8.40KB"
},
{
"path": "packages/algoliasearch/dist/lite/lite.umd.js",
Expand Down Expand Up @@ -30,7 +30,7 @@
},
{
"path": "packages/client-search/dist/client-search.umd.js",
"maxSize": "6.60KB"
"maxSize": "6.70KB"
},
{
"path": "packages/client-sources/dist/client-sources.umd.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './src/createAuth';
export * from './src/createEchoRequester';
export * from './src/createRetryablePromise';
export * from './src/createIterablePromise';
export * from './src/cache';
export * from './src/transporter';
export * from './src/createAlgoliaAgent';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const config: Config.InitialOptions = {
testPathIgnorePatterns: [
'src/__tests__/cache/null-cache.test.ts',
'src/__tests__/cache/memory-cache.test.ts',
'src/__tests__/create-iterable-promise.test.ts',
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { createIterablePromise } from '../createIterablePromise';

describe('createIterablePromise', () => {
describe('validate', () => {
it('iterates on a `func` until `validate` is met', async () => {
let calls = 0;
const promise = createIterablePromise({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(`success #${calls}`);
});
},
validate: () => calls >= 3,
});

await expect(promise).resolves.toEqual('success #3');
expect(calls).toBe(3);
});

it('forward the response of the `func`', async () => {
let calls = 0;
const promise = createIterablePromise<number>({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(calls);
});
},
validate: (response) => response >= 3,
});

await expect(promise).resolves.toEqual(3);
expect(calls).toBe(3);
});
});

describe('aggregator', () => {
it('is called before iterating', async () => {
let calls = 0;
let count = 0;
const promise = createIterablePromise({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(`success #${calls}`);
});
},
validate: () => calls >= 3,
aggregator: () => (count += 3),
});

await expect(promise).resolves.toEqual('success #3');
expect(calls).toBe(3);
expect(count).toBe(3 * 3);
});

it('forward the response of the `func`', async () => {
let calls = 0;
const responses: string[] = [];
const promise = createIterablePromise<string>({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(`success #${calls}`);
});
},
validate: () => calls >= 3,
aggregator: (response) => {
responses.push(response);
},
});

await expect(promise).resolves.toEqual('success #3');
expect(calls).toBe(3);
expect(responses).toEqual(['success #1', 'success #2', 'success #3']);
});
});

describe('timeout', () => {
it('defaults to no timeout (0)', async () => {
let calls = 0;
const before = Date.now();
const promise = createIterablePromise({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(`success #${calls}`);
});
},
validate: () => calls >= 2,
});

await expect(promise).resolves.toEqual('success #2');

expect(Date.now() - before).toBeGreaterThanOrEqual(0);
expect(Date.now() - before).toBeLessThan(10);
expect(calls).toBe(2);
});

it('waits before calling the `func` again', async () => {
let calls = 0;
const before = Date.now();
const promise = createIterablePromise({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(`success #${calls}`);
});
},
validate: () => calls >= 2,
timeout: () => 2000,
});

await expect(promise).resolves.toEqual('success #2');

expect(Date.now() - before).toBeGreaterThanOrEqual(2000);
expect(Date.now() - before).toBeLessThan(2010);
expect(calls).toBe(2);
});
});

describe('error', () => {
it('gets the rejection of the given promise via reject', async () => {
let calls = 0;

const promise = createIterablePromise({
func: () => {
return new Promise((resolve, reject) => {
calls += 1;
if (calls <= 3) {
resolve('okay');
} else {
reject(new Error('nope'));
}
});
},
validate: () => false,
});

await expect(promise).rejects.toEqual(
expect.objectContaining({ message: 'nope' })
);
});

it('gets the rejection of the given promise via throw', async () => {
let calls = 0;

const promise = createIterablePromise({
func: () => {
return new Promise((resolve) => {
calls += 1;
if (calls <= 3) {
resolve('okay');
} else {
throw new Error('nope');
}
});
},
validate: () => false,
});

await expect(promise).rejects.toEqual(
expect.objectContaining({ message: 'nope' })
);
});

it('rejects with the given `message` when `validate` hits', async () => {
const MAX_RETRIES = 3;
let calls = 0;

const promise = createIterablePromise({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve('okay');
});
},
validate: () => false,
error: {
validate: () => calls >= MAX_RETRIES,
message: () => `Error is thrown: ${calls}/${MAX_RETRIES}`,
},
});

await expect(promise).rejects.toEqual(
expect.objectContaining({
message: 'Error is thrown: 3/3',
})
);
expect(calls).toBe(MAX_RETRIES);
});

it('forward the response of the `func`', async () => {
const MAX_RETRIES = 3;
let calls = 0;

const promise = createIterablePromise<number>({
func: () => {
return new Promise((resolve) => {
calls += 1;
resolve(calls);
});
},
validate: () => false,
error: {
validate: (response) => response >= MAX_RETRIES,
message: (response) => `Error is thrown: ${response}/${MAX_RETRIES}`,
},
});

await expect(promise).rejects.toEqual(
expect.objectContaining({
message: 'Error is thrown: 3/3',
})
);
expect(calls).toBe(MAX_RETRIES);
});
});
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { CreateIterablePromise } from './types/CreateIterablePromise';

/**
* Helper: Returns the promise of a given `func` to iterate on, based on a given `validate` condition.
*
* @param createIterator - The createIterator options.
* @param createIterator.func - The function to run, which returns a promise.
* @param createIterator.validate - The validator function. It receives the resolved return of `func`.
* @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`.
* @param createIterator.error - The `validate` condition to throw an error, and its message.
* @param createIterator.timeout - The function to decide how long to wait between iterations.
*/
export function createIterablePromise<TResponse>({
func,
validate,
aggregator,
error,
timeout = (): number => 0,
}: CreateIterablePromise<TResponse>): Promise<TResponse> {
const retry = (): Promise<TResponse> => {
return new Promise<TResponse>((resolve, reject) => {
func()
.then((response) => {
if (aggregator) {
aggregator(response);
}

if (validate(response)) {
return resolve(response);
}

if (error && error.validate(response)) {
return reject(new Error(error.message(response)));
}

return setTimeout(() => {
retry().then(resolve).catch(reject);
}, timeout());
})
.catch((err) => {
reject(err);
});
});
};

return retry();
}
Loading

0 comments on commit 5e2a1da

Please sign in to comment.