Skip to content

Commit

Permalink
feat(suspend): New function to simplify React Suspension use-cases
Browse files Browse the repository at this point in the history
  • Loading branch information
Jocelyn Badgley (Twipped) committed Mar 12, 2023
1 parent f7bcdd1 commit 64808ec
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 0 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ import * as u from '@twipped/utils';
**Returns**: <code>boolean</code> - Returns true if all items in the array are falsey.


### [resetSuspension](#resetSuspension)
Removes a stored async result from the suspense cache, allowing
it to be invoked again on next render.


| Param | Type | Description |
| --- | --- | --- |
| key | <code>string</code> | The operation name to remove. |




### [containsArrays](#containsArrays)
**Category**: Collections
**Returns**: <code>boolean</code> - Returns true if any items in the array are arrays.
Expand Down Expand Up @@ -1185,6 +1197,24 @@ Produce a collection without certain values
**Returns**: [<code>Collection</code>](#Collection)


### [module.exports](#module-exports)
Produces a promise aware debounced function that delays invoking `func` until
after `wait` milliseconds have elapsed since the last time the debounced
function was invoked.


| Param | Type |
| --- | --- |
| func | <code>function</code> |
| wait | <code>number</code> |
| options | <code>Object</code> |
| options.leading | <code>boolean</code> |
| options.maxWait | <code>number</code> |
| options.context | <code>\*</code> |

**Returns**: <code>function</code>


### [module.exports](#module-exports)
Produce a collection containing only certain values

Expand Down Expand Up @@ -1283,6 +1313,29 @@ Produces a sorting function using predicate logic.
**Returns**: <code>function</code>


### [module.exports](#module-exports)
Performs an asyncronous task in a manner compatible with
the React Suspension API.

Note, if you don't know how Suspension works, you probably
shouldn't use this function.

This will execute the given function and throw the promise it
produces so that React.Suspense can await its completion, dismounting
your component while it completes. When the promise finishes,
React.Suspense will remount your component, invoking this function again.
At that point, this function returns the resolved value for the rest of
the life of the application.


| Param | Type | Description |
| --- | --- | --- |
| key | <code>string</code> | A name for this operation that is unique across your entire application (including multiple instances of your component). |
| fn | [<code>TaskCallback</code>](#TaskCallback) | The async task to perform. |

**Returns**: <code>any</code>


### [module.exports](#module-exports)
Converts a collection into an array of key/value tuples.

Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export { default as sorter } from './sorter.js';
export { default as stddev } from './stddev.js';
export { default as stripIndent } from './stripIndent.js';
export { default as sum } from './sum.js';
export { default as suspend, resetSuspension } from './suspend.js';
export { default as threepiece } from './threepiece.js';
export { default as timeout } from './timeout.js';
export { default as toPairs } from './toPairs.js';
Expand Down
87 changes: 87 additions & 0 deletions src/suspend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {isPromise} from './types.js';

/**
* Basic little wrapper that ensures we always get a promise back.
*
* @param fn
* @param {...any} args
* @private
*/
async function wrapPromise(fn, ...args) {
const res = await fn(...args);
return res;
}
const suspensions = new Map();

class Failed {
constructor(err) {
this.err = err;
}
}

/**
* @callback TaskCallback
* @param {string} key The operation name.
* @returns {Promise<any>}
*/

/**
* Performs an asyncronous task in a manner compatible with
* the React Suspension API.
*
* Note, if you don't know how Suspension works, you probably
* shouldn't use this function.
*
* This will execute the given function and throw the promise it
* produces so that React.Suspense can await its completion, dismounting
* your component while it completes. When the promise finishes,
* React.Suspense will remount your component, invoking this function again.
* At that point, this function returns the resolved value for the rest of
* the life of the application.
*
* @param {string} key A name for this operation that is unique across
* your entire application (including multiple instances of your component).
* @param {TaskCallback} fn The async task to perform.
* @returns {any}
*/
export default function suspend(key, fn) {
if (typeof key !== 'string') throw new Error('suspend must receive a string key to test against.');

const cached = suspensions.get(key);

// nothing suspended yet, create the promise and throw it to suspend
if (!cached) {
const p = wrapPromise(fn, key).then((m) => {
suspensions.set(key, m);
}, (err) => {
suspensions.set(key, new Failed(err));
});
suspensions.set(key, p);
throw p;
}

if (cached instanceof Failed) {
suspensions.delete(key);
throw cached.err;
}

// a promise is already suspended, throw it again to maintain suspense.
if (isPromise(cached)) {
throw cached;
}

// promise has finished, return the actual result
return cached;
}

/**
* Removes a stored async result from the suspense cache, allowing
* it to be invoked again on next render.
*
* @param {string} key The operation name to remove.
*/
export function resetSuspension(key) {
suspensions.delete(key);
}

suspend.reset = resetSuspension();

0 comments on commit 64808ec

Please sign in to comment.