Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move until utility function to toolbox #907

Merged
merged 2 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/snap-toolbox/src/until/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## until
This utility function resolves the provided parameter when it becomes truthy.

The function takes a single required parameter containing any function, object, or primative you are awaiting to resolve.

Typical usage may include waiting for a third party function to become available.

```typescript
import { until } from '@searchspring/snap-toolbox';
const result = await until(window.thirdPartyFn);
```

## options

### checkMax
Maximum number of checks to poll for. Default: `20`

```typescript
const result = await until(window.thirdPartyFn, { checkMax: 20 });
```

### checkCount
Starting count of poll counter. Default: `0`

```typescript
const result = await until(window.thirdPartyFn, { checkCount: 0 });
```

### checkTime
Polling interval in milliseconds. Default: `50` (ms)

```typescript
const result = await until(window.thirdPartyFn, { checkTime: 50 });
```

### exponential
Polling interval multiplier to exponentially increase each interval. Default: `1.1`

```typescript
const result = await until(window.thirdPartyFn, { exponential: 1.1 });
```

### defer
By default, if the provided function parameter is already available it will immediately return. To defer this until after the first iteration of polling, set `defer: true`

```typescript
const result = await until(window.thirdPartyFn, { defer: true });
```

### executeFunction
By default, if the provided function parameter is a function it will be invoked without any function parameters before its return value is resolved. To instead return a reference to the function, set `executeFunction: false`

```typescript
const thirdPartyFn = await until(window.thirdPartyFn, { executeFunction: false });
thirdPartyFn();
```
142 changes: 142 additions & 0 deletions packages/snap-toolbox/src/until/until.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { until, type UntilOptions } from './until';

const wait = (time = 1) => {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
};

describe('until', () => {
const options: Partial<UntilOptions> = {
checkMax: 5, // limit checks because max jest timer is 5s
checkCount: 0,
checkTime: 60,
exponential: 1.1,
defer: false,
executeFunction: true,
};

it('should reject try/catch', async () => {
const thing = undefined;
expect.assertions(1);

try {
await until(thing, options);
} catch (e) {
expect(e).toBe(undefined);
}
});

it('should reject .catch()', async () => {
const thing = undefined;
expect.assertions(1);

const returnVal = await until(thing, options)
.then(() => 'success')
.catch(() => 'rejected');
expect(returnVal).toBe('rejected');
});

it('should reject falsy values', async () => {
const things = [false, 0, '', null, undefined, NaN];
expect.assertions(things.length);

for (let i = 0; i < things.length; i++) {
const thing = things[i];
const returnVal = await until(thing, options)
.then(() => 'success')
.catch(() => 'rejected');
expect(returnVal).toBe('rejected');
}
});

it('should resolve immediate - function', async () => {
const thing = jest.fn(() => 'success');
expect.assertions(2);

const returnVal = await until(thing, options);
expect(thing).toHaveBeenCalledTimes(1);
expect(returnVal).toBe('success');
});

it('should not executeFunction', async () => {
const thing = jest.fn(() => 'success');
expect.assertions(2);

// return the function reference instead of executing it
const returnVal = await until(thing, { ...options, executeFunction: false });
expect(thing).toHaveBeenCalledTimes(0);
expect(returnVal).toBe(thing);
});

it('should resolve immediate - primative', async () => {
const thing = 'success';
expect.assertions(1);

const returnVal = await until(thing, options);
expect(returnVal).toBe(thing);
});

it('should resolve truthy values', async () => {
const things = [true, {}, [], jest.fn, 42, -42, 42.4, -42.5, 42n, Infinity, -Infinity, 'false', 'string', new Date()];
expect.assertions(things.length);

for (let i = 0; i < things.length; i++) {
const thing = things[i];
const returnVal = await until(thing, options)
.then(() => 'success')
.catch(() => 'rejected');
expect(returnVal).toBe('success');
}
});

it('should resolve defer - function', async () => {
const thing = jest.fn(() => 'success');
expect.assertions(1);

const returnVal = await until(thing, { ...options, defer: true })
.then((val) => val)
.catch(() => 'rejected');
expect(returnVal).toBe('success');
});

it('should resolve defer - primative', async () => {
const thing = 'success';
expect.assertions(1);

const returnVal = await until(thing, { ...options, defer: true })
.then((val) => val)
.catch(() => 'rejected');
expect(returnVal).toBe('success');
});

it('should resolve eventually', async () => {
let thing = '';
expect.assertions(2);

const delay = options.checkTime! * (options.checkCount! - 1);
setTimeout(() => {
thing = 'success';
}, delay);

until(() => thing, options);

expect(thing).toBe('');

await wait(delay);

expect(thing).toBe('success');
});

it('should reject eventually', async () => {
let thing = '';
expect.assertions(1);

const delay = options.checkTime! * options.checkCount!;

until(() => thing, options);
await wait(delay + 200); // add 200ms to ensure rejects

expect(thing).toBe('');
});
});
65 changes: 65 additions & 0 deletions packages/snap-toolbox/src/until/until.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export type UntilOptions = {
checkMax: number;
checkCount: number;
checkTime: number;
exponential: number;
defer: boolean;
executeFunction: boolean;
};

const defaultUntilOptions: UntilOptions = {
checkMax: 25,
checkCount: 0,
checkTime: 60,
exponential: 1.1,
dkonieczek marked this conversation as resolved.
Show resolved Hide resolved
defer: false,
executeFunction: true,
dkonieczek marked this conversation as resolved.
Show resolved Hide resolved
};

export const until = async (thing: unknown, customOptions?: Partial<UntilOptions>): Promise<unknown> => {
const options: UntilOptions = {
...defaultUntilOptions,
...(customOptions || {}),
};

return new Promise(async (resolve, reject) => {
const checkForThing = async (thing: unknown) => {
switch (typeof thing) {
case 'function': {
if (options.executeFunction) {
return thing();
}
return thing;
}
default: {
if (thing) {
return thing;
}
}
}
};
const thingCheck = await checkForThing(thing);
if (thingCheck && !options.defer) {
resolve(thingCheck);
} else {
const waiting = () => {
window?.setTimeout(async () => {
const thingCheck = await checkForThing(thing);
if (thingCheck) {
return resolve(thingCheck);
}
options.checkCount++;
options.checkTime *= options.exponential;

if (options.checkCount < options.checkMax) {
return waiting();
}

// timeout reached
return reject();
}, options.checkTime);
};
waiting();
}
});
};
Loading