tameable v1.0.0
Tameable.js
Three functions for managing objects which depend on other objects to be in a valid state.
- We specify a key, and how to construct its object, with
spec. - We request to use objects by their keys with
once. We invalidate an object by it's key with
invalidate.
This ensure we minimise repeat work, only doing it when an object we need is not valid. Depending on how a dependancy tree is designed, work can be deffered in different ways. My specific usecase was with the WebGPU API in which pipelines and buffers have to be reconstructed when certain parameters change, such as screen size or rendering mode.
Tameable attempts to be small and low overhead.
Examples
Example 1 - Side-effects
import tame from 'tame'
tame.spec({key: 'shout once'}, async () => {
console.log("Scream, and shout, and let it all out!");
return tame.SUCCESS;
});
tame.once(['shout once']);
tame.once(['shout once']);
tame.once(['shout once']);Note:
- Every spec function must return a non-null object (or a promise to one).
- We provide tame.SUCCESS if there is a situation where no objects need to be returned. ie we use the spec for it's side effects. It's a bit clearer than returning an empty object.
Example 2 - Render on demand
import tame from 'tameable'
// setup
tame.spec({key: 'renderObjects', deps: [...]}, async (...) => {
// ...
return renderObjects;
});
tame.spec(
{
key: 'render',
deps: ['renderObjects']
},
async renderObjects => {
// ..
return tame.SUCCESS;
}
);
// once(['renderObjects']); // optionally preload, otherwise loaded on first frame
// render loop
async function frame() {
await tame.once(['render']) // only render if we need to
requestAnimationFrame(frame);
}
// events
// render will be reconstructed on next call to `once`
event.on('something-happened', _ => invalidate('render'));
// render and renderObjects will be reconstructed on next call to `once`
event.on('something-else-happened', _ => invalidate('renderObjects'));Note:
- In this example, the 'render' spec function is called only once (unless an event is triggered)
Hence, the
oncecall only checks that the frame is up to date in most frames. - If something does change the render is reconstructed, but only the dependancies which are invalid.
- If
oncewas called without `awaitthen any errors thrown will be uncaught and likley lead to console spam.
Example 3 - Wrapping callbacks
import tame from 'tame'
tame.spec({key: 'dom'}, async () => {
if(document.readyState === 'loading') {
// wrap 'DOMContentLoaded' callback with a promise
return new Promise(resolve =>
document.addEventListener('DOMContentLoaded', _ => resolve(document)));
}
return document;
});
// wait for dom to load
tame.once(['dom'])
.then(_ => console.log('DOM loaded!'));Note:
- This example shows a spec that waits for the DOM to be loaded. All dependants on
domwill also wait. - Circular dependencies are illegal.
- A spec function must return a non-null object, hence the use of
documentas a return value. If a promise is returned, it will be awaited and its return value will be the non-null object returned.
Mental model
- Each
keyis associated with an object which can bevalid,validating,invalid, orinitial. - Each
keyhas an associated functionfnwhich returns a non-null javascript object.
When `spec is called,
- The list of dependencies,
fn, and other info is stored under thekey - No work is done here
When once is called,
- For each
keythat is a dependency,- Lookup the
key - If the object under the
keyisvalid, return it - Otherwise, go to the first step for this object's dependencies
- Then construct the object itself
- If the constructed object is a non-null object, return it
- Otherwise, throw an error
- Lookup the
When invalidate is called,
- If the object under this
keyisvalid- mark it as
invalid - call
invalidateFnif it exists - invalidate all objects that depend on this one
- mark it as
- Otherwise, do nothing
- No work is done here
About invalidate
- When an object is invalidated, so are all its children (dependants).
- Use
invalidatein callbacks. - If you find yourself using
invalidatein a spec function but not a callback, maybe it should be in the dependancy list instead? - Calling
invalidateon an object that isvalidatingorinitialwill do nothing, since these objects will be, or are being, reconstructed anyway.
Debug mode
Unless the library is bundled for production, extra checks will be performed and errors omitted for common mistakes such as forgetting to return a value from fn or creating a dependency on a key which doesn't yet exist (thus preventing circular dependencies). This features assumes a bundler and minifier will replace process.env.NODE_ENV !== 'production' with false and unreachable code will be optimised away, which it should. As such, errors in production code are less likley to be helpful. An error is only thrown in production if an objects fn returns a non-null object.
Future features to consider
- Tree analysis
- Tooling
- Promise.any / Promise.allSettled support, ie use whatever dependancy path first completes (usecases?)
2 years ago