A simple, yet powerful, Promise Registry.
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.
- A Guarantor is basically an object that holds Promises
- Each Promise (guarantee) has a name (identifier)
- When you call
.get(identifier)
, you get a Promise back, and theretriever()
is called - Once
retriever()
fetches the value, an optionalinitializer()
can be used to parse/initialize the value - 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.
import Guarantor from "underwriter";
const options = { retriever: (identifier) => fetchSomething(identifier) };
const guarantor = new Guarantor(options);
const resource = await guarantor.get(identifier);
- Create a Guarantor (registry)
- Supply a
retriever(identifier: string): Promise<guarantee>
- Request a
guarantee
withGuarantor.get(identifier)
// /configs/api.json
{
configVersion: "2.3.15",
config: {
apiEndpoint: "/api/",
apiVersion: "v1"
}
}
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');
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:
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)
).
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.
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
.
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.
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.
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.
Please be aware of the differences in behavior outlined below before using this option.
publicFulfill |
Changes |
---|---|
true |
|
false |
|
This behavior is currently being debated. Please refer to the issue ticket, or create one, to discuss.
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 |
--------------|---------|----------|---------|---------|-------------------
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) β
ββββββββββββββββββββββββββββββββββββββββ