From d9d4eb3d4994c30850e5829f1e609755112f5b06 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Fri, 29 Sep 2023 11:47:59 -0400 Subject: [PATCH 1/2] feat: move until utility function to toolbox --- packages/snap-toolbox/src/until/README.md | 56 +++++++++ packages/snap-toolbox/src/until/until.test.ts | 108 ++++++++++++++++++ packages/snap-toolbox/src/until/until.ts | 65 +++++++++++ 3 files changed, 229 insertions(+) create mode 100644 packages/snap-toolbox/src/until/README.md create mode 100644 packages/snap-toolbox/src/until/until.test.ts create mode 100644 packages/snap-toolbox/src/until/until.ts diff --git a/packages/snap-toolbox/src/until/README.md b/packages/snap-toolbox/src/until/README.md new file mode 100644 index 000000000..4d2dac552 --- /dev/null +++ b/packages/snap-toolbox/src/until/README.md @@ -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(); +``` diff --git a/packages/snap-toolbox/src/until/until.test.ts b/packages/snap-toolbox/src/until/until.test.ts new file mode 100644 index 000000000..6c745791c --- /dev/null +++ b/packages/snap-toolbox/src/until/until.test.ts @@ -0,0 +1,108 @@ +import { until, type UntilOptions } from './until'; + +describe('until', () => { + it('should reject try/catch', async () => { + const thing = undefined; + expect.assertions(1); + + try { + await until(thing); + } catch (e) { + expect(e).toBe(undefined); + } + }); + + it('should reject .catch()', async () => { + const thing = undefined; + expect.assertions(1); + + const returnVal = await until(thing) + .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); + + const options: Partial = { + checkMax: 5, // limit checks to 5 because max jest timer is 5s + }; + + 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); + expect(thing).toHaveBeenCalledTimes(1); + expect(returnVal).toBe('success'); + }); + + it('should not executeFunction', async () => { + const thing = jest.fn(() => 'success'); + expect.assertions(2); + + const options: Partial = { + executeFunction: false, // return the function reference instead of executing it + }; + + const returnVal = await until(thing, options); + 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); + 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); + + const options: Partial = { + checkMax: 5, // limit checks to 5 because max jest timer is 5s + }; + + 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, { 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, { defer: true }) + .then((val) => val) + .catch(() => 'rejected'); + expect(returnVal).toBe('success'); + }); +}); diff --git a/packages/snap-toolbox/src/until/until.ts b/packages/snap-toolbox/src/until/until.ts new file mode 100644 index 000000000..2652957f2 --- /dev/null +++ b/packages/snap-toolbox/src/until/until.ts @@ -0,0 +1,65 @@ +export type UntilOptions = { + checkMax: number; + checkCount: number; + checkTime: number; + exponential: number; + defer: boolean; + executeFunction: boolean; +}; + +const defaultUntilOptions: UntilOptions = { + checkMax: 20, + checkCount: 0, + checkTime: 50, + exponential: 1.1, + defer: false, + executeFunction: true, +}; + +export const until = async (thing: unknown, customOptions?: Partial): Promise => { + 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(); + } + }); +}; From 15da428b38541dc328de4b6144efff32e94f2a77 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Tue, 3 Oct 2023 11:16:29 -0400 Subject: [PATCH 2/2] test: pr feedback --- packages/snap-toolbox/src/until/until.test.ts | 72 ++++++++++++++----- packages/snap-toolbox/src/until/until.ts | 4 +- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/packages/snap-toolbox/src/until/until.test.ts b/packages/snap-toolbox/src/until/until.test.ts index 6c745791c..9b5c41907 100644 --- a/packages/snap-toolbox/src/until/until.test.ts +++ b/packages/snap-toolbox/src/until/until.test.ts @@ -1,12 +1,27 @@ import { until, type UntilOptions } from './until'; +const wait = (time = 1) => { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +}; + describe('until', () => { + const options: Partial = { + 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); + await until(thing, options); } catch (e) { expect(e).toBe(undefined); } @@ -16,7 +31,7 @@ describe('until', () => { const thing = undefined; expect.assertions(1); - const returnVal = await until(thing) + const returnVal = await until(thing, options) .then(() => 'success') .catch(() => 'rejected'); expect(returnVal).toBe('rejected'); @@ -26,10 +41,6 @@ describe('until', () => { const things = [false, 0, '', null, undefined, NaN]; expect.assertions(things.length); - const options: Partial = { - checkMax: 5, // limit checks to 5 because max jest timer is 5s - }; - for (let i = 0; i < things.length; i++) { const thing = things[i]; const returnVal = await until(thing, options) @@ -43,7 +54,7 @@ describe('until', () => { const thing = jest.fn(() => 'success'); expect.assertions(2); - const returnVal = await until(thing); + const returnVal = await until(thing, options); expect(thing).toHaveBeenCalledTimes(1); expect(returnVal).toBe('success'); }); @@ -52,11 +63,8 @@ describe('until', () => { const thing = jest.fn(() => 'success'); expect.assertions(2); - const options: Partial = { - executeFunction: false, // return the function reference instead of executing it - }; - - const returnVal = await until(thing, options); + // return the function reference instead of executing it + const returnVal = await until(thing, { ...options, executeFunction: false }); expect(thing).toHaveBeenCalledTimes(0); expect(returnVal).toBe(thing); }); @@ -65,7 +73,7 @@ describe('until', () => { const thing = 'success'; expect.assertions(1); - const returnVal = await until(thing); + const returnVal = await until(thing, options); expect(returnVal).toBe(thing); }); @@ -73,10 +81,6 @@ describe('until', () => { const things = [true, {}, [], jest.fn, 42, -42, 42.4, -42.5, 42n, Infinity, -Infinity, 'false', 'string', new Date()]; expect.assertions(things.length); - const options: Partial = { - checkMax: 5, // limit checks to 5 because max jest timer is 5s - }; - for (let i = 0; i < things.length; i++) { const thing = things[i]; const returnVal = await until(thing, options) @@ -90,7 +94,7 @@ describe('until', () => { const thing = jest.fn(() => 'success'); expect.assertions(1); - const returnVal = await until(thing, { defer: true }) + const returnVal = await until(thing, { ...options, defer: true }) .then((val) => val) .catch(() => 'rejected'); expect(returnVal).toBe('success'); @@ -100,9 +104,39 @@ describe('until', () => { const thing = 'success'; expect.assertions(1); - const returnVal = await until(thing, { defer: true }) + 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(''); + }); }); diff --git a/packages/snap-toolbox/src/until/until.ts b/packages/snap-toolbox/src/until/until.ts index 2652957f2..a15bc7827 100644 --- a/packages/snap-toolbox/src/until/until.ts +++ b/packages/snap-toolbox/src/until/until.ts @@ -8,9 +8,9 @@ export type UntilOptions = { }; const defaultUntilOptions: UntilOptions = { - checkMax: 20, + checkMax: 25, checkCount: 0, - checkTime: 50, + checkTime: 60, exponential: 1.1, defer: false, executeFunction: true,