underwriter

An automated Promise Registry

Usage no npm install needed!

<script type="module">
  import underwriter from 'https://cdn.skypack.dev/underwriter';
</script>

README

Underwriter

A simple, yet powerful, Promise Registry.

How It Works

Underwriter provides access to Guarantors, which are simple interfaces for retrieving Promises by name. These promises can either be Resolved with a guarantee or Rejected with an error.

  1. A Guarantor is basically an object that holds Promises
  2. Each Promise (guarantee) has a name (identifier)
  3. When you call .get(identifier), you get a Promise back, and the retriever() is called
  4. Once retriever() fetches the value, an optional initializer() can be used to parse/initialize the value
  5. Once that is complete, the value is given to anyone who calls .get(identifier)

In more ambiguous terms (:^)), Underwriter uses Guarantors to provide consumers with a method of retrieving named guarantees from its registries.

Guarantors are general purpose, and can be used for anything, from asynchronously importing ESM modules using import(), one-time retrieval of static resources from an API/CDN/wherever, or anything else you can think of. It can even be used for retrieving interfaces for things already loaded in your environment, like UI Components, Controllers, Models, Stores, Actions, et cetera.

Quick Usage

import Guarantor from "underwriter";
const options = { retriever: (identifier) => fetchSomething(identifier) };
const guarantor = new Guarantor(options);
const resource = await guarantor.get(identifier);
  1. Create a Guarantor (registry)
  2. Supply a retriever(identifier: string): Promise<guarantee>
  3. Request a guarantee with Guarantor.get(identifier)

Example

Remote Resource

// /configs/api.json
{
        configVersion: "2.3.15",
        config: {
                apiEndpoint: "/api/",
                apiVersion: "v1"
        }
}

Guarantor

import Guarantor from "underwriter";

const retriever = (identifier) => (
        fetch(`/configs/${identifier}.json`).then(
                (response) => response.json()
        )
);

// Optional
const initializer = (identifier, guarantee) => guarantee.config;

const configGuarantor = new Guarantor({
        retriever,
        initializer,
});

const apiConfig = await configGuarantor.get('api');

Options

function options.retriever Required

TypeScript notation:

type retriever = (identifier: string): Promise<any>;

The retriever() can be any function that accepts an identifier and resolves with a promise once the guarantee has been retrieved. The retriever option is a function that is called whenever .get(identifier) is called. It is given an identifier and expected to retrieve the resource and return it in the form of a promise. This may take the form of a Fetch/XHR/AJAX request, an import(), or as simply mapping the identifier to the key of an object.

For those that prefer TypeScript-like notation, the retriever() should follow something like:

function options.initializer Optional

TypeScript notation:

type initializer = (identifier: string, guarantee: any): any;

The initializer is given the identifier and guarantee value after the resource is retrieved. It's role is to prepare the value for usage. Whatever it returns will be the value that is given to anyone who has requested this guarantee with .get(identifier). This could conceivably be a santization function, a function that calls JSON.parse() on the input, or constructs a new class based on the data (e.g., (id, guarantee) => new Foobar(guarantee)).

Promise option.defer Optional

If you need a Guarantor to wait before it retrieves or initializes your guarantees, you can use the defer option, which takes a Promise (or any Thenable), and waits for it to resolve before continuing. This can be useful if you need to setup your application or retrieve things before you want the Guarantor to start retrieving or initializing values.

Wait to Retrieve

This is the default functionality. By passing option.defer as a Promise, the Guarantor will not call the retriever() until the option.defer promise has resolved.

const defer = startupProcess(); // Promise
const guarantor = new Guarantor({ retriever, defer });
// Won't retrieve until startupProcess is resolved
const foobar = await guarantor.get('foobar');

A hypothetical sitation might be when you require authentication (like a JWT) before your Guarantor will be able to retrieve anything. In this scenario, you would resolve your option.defer promise once you have retrieved your hypothetical JWT.

Retrieve Now, But Wait to Initialize

boolean option.retrieveEarly Optional

This changes the behavior of option.defer by allowing the Guarantor to call the retriever() immediately, but defers the call to the initializer() until the option.defer promise has resolved.

const defer = startupProcess(); // Promise
const guarantor = new Guarantor({
        retriever,
        retrieveEarly: true, /* <<< */
        initializer,
        defer,
});
// Retrieves immediately, but doesn't initialize() or
// resolve until after startupProcess is resolved
const foobar = await guarantor.get('foobar');

Setting this to true is theoretically faster, because the Guarantor doesn't wait on anything to retrieve the resources and can do so asynchronously while the application sets itself up. But it will only initialize those values once the parent promise resolves.

Advanced/Experimental

๐Ÿงช prototype/class options.thenableApi Experimental

The options.thenableApi feature allows you to specify a Promise implementation different than the built-in, which should give you flexibility in the types of Promises you're working with. For instance, official support for the novel thenable-events Promise implementation is expected in the near future.

๐Ÿงช boolean options.publicFulfill Experimental

By default, a retriever will execute when a Guarantee is requested, and the return value of this retriever will be used to initialize and then fulfill the Guarantee. However, if there are times when you would like to fulfill a Guarantee outside of the standard lifecycle, you can do so by setting publicFulfill to true, which will give you a method for fulfilling a Guarantee ad-hoc:

type Guarantor.fulfill = (identifier: string, guarantee: any): Promise<any>;

Executing this function will pass the identifier and guarantee to the optional initializer, and then fulfill the Guarantee.

Note: Guarantees can only be fulfilled once. Attempting to fulfill a Guarantee outside of the standard lifecycle may cause a rejection if the Guarantee has already been fulfilled.

:warning: This may change behavior in an unexpected manner.

Please be aware of the differences in behavior outlined below before using this option.

publicFulfill Changes
true
  • A previously non-existent Guarantor.fulfill() method appears.
  • The retriever option becomes optional.
  • If the retriever() returns undefined for a particular identifier, the guarantee will not be fulfilled with a value of undefined, and instead wait for the manual invocation of Guarantor.fulfill() to fulfill the promise (see fulfill syntax above).
false
  • If options.publicFulfill = false, a warning is now outputted informing the developer that the guarantee will successfully be fulfilled with a value of undefined, which may be unintended.

This behavior is currently being debated. Please refer to the issue ticket, or create one, to discuss.

Test Coverage

  underwriter::Guarantor
    underwriter::constructor()
      โœ” should throw if no retriever is passed
      โœ” should NOT throw if optional properties are omitted from options
      โœ” should throw if an invalid retriever is passed
      โœ” should throw if an invalid defer is passed
      โœ” should throw if an invalid initializer is passed
      โœ” should throw if an invalid Thenable API is passed
      โœ” should NOT expose a fulfill method if publicFulfill option is omitted
      โœ” should NOT expose a fulfill method if publicFulfill option is false
      โœ” should expose a fulfill method if publicFulfill option is true
      โœ” should NOT throw if publicFulfill is true and no retriever is passed
    underwriter:get( identifier )
      โœ” should reject if no identifier is passed
      โœ” should reject if an invalid identifier is passed
      โœ” should reject if the retriever fails
      โœ” should create a promise and call the retriever
      โœ” should not produce new promises or call the retriever again on subsequent calls
      โœ” should wait to retrieve a guarantee until defer promise resolves if retrieveEarly is false
      โœ” should immediately retrieve a guarantee if retrieveEarly is true
      โœ” should fulfill even if retriever returns void
      โœ” should NOT fulfill if retriever returns void, but publicFulfill is true
      โœ” should not call the retriever if it was omitted and publicFulfill is true
    underwriter:fulfill( identifier, guarantee )
      โœ” should fulfill the guarantee matching the identifier

  underwriter::utils
    formatName()
      โœ” should return lowercased version of a string
      โœ” should cast other types to string
    initializeIfNeeded()
      โœ” should initialize a missing key
      โœ” should initialize a missing key with a specific value factory
      โœ” should preserve the original value if a key already exists


  30 passing (98ms)

--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |
 copy.js      |     100 |      100 |     100 |     100 |
 fulfill.js   |     100 |      100 |     100 |     100 |
 guarantor.js |     100 |      100 |     100 |     100 |
 utils.js     |     100 |      100 |     100 |     100 |
--------------|---------|----------|---------|---------|-------------------

Lifecycle Diagram

Here's a bonus for you: A horribly crude and probably unhelpful lifecycle diagram that looks like it was put together by a 5 year old :)

call  โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—      โ”Œโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”
โ‡ขโ”ˆโ”ˆโ‡ขโ”ˆ โ•‘ Guarantor.get( id ) โ•‘      โ”Š  (some local or remote resource)  โ”Š
      โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•คโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•      โ””โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ฌโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ฌโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”˜
                 โ”‚                         โ”‚                   โ”‚
                 โ”‚              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†‘โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†“โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚          (pending)      โ†‘                   โ”‚       โ”‚
 return    โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—โ‡ขโ”ˆโ”ˆโ”ˆโ”ˆโ‡ขโ”ˆ โ”‚โ”€โ”€ options.retriever(id)      โ”‚       โ”‚
โ‡ โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ‡  โ•‘ *Promise  โ•‘        โ”‚                              โ†“       โ”‚
           โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ”ˆโ‡ โ”ˆโ”ˆโ”ˆโ”ˆโ‡  โ”‚โ†โ”€ options.intializer(id, resource)   โ”‚
                           (fulfilled)                                 โ”‚
                                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜