4.0.0 • Published 5 years ago

meridvia v4.0.0

Weekly downloads
12
License
MIT
Repository
github
Last release
5 years ago

Meridvia

Build Status Coverage Status

This library helps with managing the lifecycle of data fetched from a resource. For example when there is a HTTP resource that you need data from, this library will tell you when to to perform the actual request to fetch the data and when to clear/cleanup the data.

Originally this library was created to make it easier to manage asynchronous actions in redux. The "standard" way of performing async actions in redux (described in its documentation) has many advantages, however it also has a few disadvantages:

  • Data that is no longer needed is never cleared out of the state store
  • Pending API calls are not aborted if they are no longer needed
  • Periodically refreshing data is difficult

This library was created to help with these disadvantages. However it does not actually depend on redux and is generic enough to be used in many other situations.

Installation

npm install meridvia

And then you can include it in your project using import or require(), assuming you are using something like webpack or browserify:

const {createManager} = require('meridvia');

or

import {createManager} from 'meridvia';

How it works

There are a few important concepts: The Resource Definition which defines the behaviour of each resource. And the Session by which you can begin a transaction.

For each asynchronous source of data that you have, a Resource Definition should be created. At the very least it contains a unique resource name, a fetch callback and a clear callback. It is also possible to configure if and when the data is cached and refreshed. A resource is requested using its resource name and an a key/value map of params. These params can be represented using a plain javascript object, or as an Immutable Map and are passed to the fetch callback and the clear callback.

Each unique combination of resource name and params is considered to be one specific resource. For example if you call the request function multiple times with the same resource name and params, the fetch callback will only be called once. If a plain javascript object is passed as the params, it is compared to the params of other resources using a shallow equality check.

A Session is used to manage which resources are in use. For each place in your codebase where you would normally perform an API call, a Session should be created instead. A Session object lets you start and end a transaction.

The only time that a resource can be requested is between the start and end of such a transaction. When requesting data the Session will figure out if any existing data can be re-used, if not the fetch callback is called.

When a transaction ends, the session will compare all the resources that have been requested to the requested resources of the previous transaction. For all resources that are no longer in use, the clear callback is called. This callback will not occur immediately if caching has been configured.

An example to demonstrate these concepts:

const {createManager} = require('meridvia');

// Create a new instance of this library by calling
// createManager(), this is usually only done once
// in your codebase
const manager = createManager();

// Register a new Resource Definition by calling
// manager.resource(...)
manager.resource({
    name: 'example',
    fetch: async (params) => {
        console.log('Fetch the example resource with params', params);
        // This is where you would normally fetch data using an
        // actual API call and store the result in your state store
    },
    clear: (params) => {
        console.log('Clear the example resource with params', params);
        // This is where you would normally remove the previously
        // fetched data from your state store
    },
});

// Create a new session
const session = manager.createSession();

console.log('\nLets begin the first transaction!');
// The session transaction begins now:
session(request => {
    // Fetch two resources for the first time:
    request('example', {exampleId: 1});
    request('example', {exampleId: 2});
});
// The session transaction has ended

console.log('\nThe second transaction!');
// The session transaction begins now:
session(request => {
    // This resource we had already fetched before,
    // so it is reused:
    request('example', {exampleId: 1});
    // This resource is fetched for the first time:
    request('example', {exampleId: 9001});
});
// The session transaction has ended,
// The resource with {exampleId: 2} was no longer
// used, so it is cleared

console.log('\nAll done, lets destroy the session');
// All resources will be cleared now:
session.destroy();

console.log('End of example');

Output:

Lets begin the first transaction!
Fetch the example resource with params { exampleId: 1 }
Fetch the example resource with params { exampleId: 2 }

The second transaction!
Fetch the example resource with params { exampleId: 9001 }
Clear the example resource with params { exampleId: 2 }

All done, lets destroy the session
Clear the example resource with params { exampleId: 1 }
Clear the example resource with params { exampleId: 9001 }
End of example

API Reference

Top-Level Exports

createManager([dispatcher], [options]) ⇒ Manager

ArgumentTypeDefault
dispatcherfunctionx => xThe dispatcher callback. This function will be called with the return value of any fetch callback and any clear callback
options.allowTransactionAbortbooleanfalseIf false, overlapping transactions are not allowed. If true an overlapping transaction for a Session will cause the previous transaction to be aborted. This option can also be set per session, see manager.createSession([options])Session.

Return value: A new Manager instance

Create a new Manager instance. See Manager API

const {createManager} = require('meridvia');

const manager = createManager();

Manager API

Manager

manager.resource(options)

ArgumentTypeDefault
options.namestringrequiredA unique resource name for this resource. The same name can later be used to request this resource.
options.fetchfunction(params, options)requiredThe fetch callback for this resource. Called whenever the asynchronous data should be retrieved.
options.clearfunction(params, options)nullThe clear callback for this resource. Called whenever asynchronous data that has been previously retrieved, is no longer in use.
options.initStoragefunction(params)() => ({})Called the first time a resource is fetched, the return value is available to the other actions of the same resource.
options.maximumStalenessTime interval0The maximum amount of time that the data of a fetched resource will be reused in a future transaction. A value of 0 means forever/infinite.
options.maximumRejectedStalenessTime intervalmaximumStalenessThe maximum amount of time that the rejected or thrown error of a fetched resource will be reused in a future transaction. A value of 0 means forever/infinite.
options.cacheMaxAgeTime interval0The maximum amount of time that the data of a fetched resource may be cached if no Session is using the resource. A value of 0 disables caching.
options.refreshIntervalTime interval0How often to fetch the resource again, as long as there is a Session using this resource. A value of 0 disables refreshing.

Return value: undefined \ Throws: IllegalStateError if the Manager has been destroyed \ Throws: TypeError if the any of the options has an invalid type \ Throws: ValueError if the given resource name is already in use

Register a Resource Definition with the given options. Each Resource Definition is identified by its unique resource name (options.name), the resource can be requested using this resource name during a transaction using the request function. The other options define the behaviour of the resource.

The options.maximumStaleness and options.cacheMaxAge values have very similar effects. They both define for how long the asynchronous data retrieved by the fetch callback may be reused by a future transaction. The difference is that if options.cacheMaxAge is not set, the resource is always cleared if it is no longer in use by any Session. If options.cacheMaxAge is set, the data may be reused even if there was a moment where the resource was not in use by any Session.

If options.refreshInterval is set, the fetch callback is called again periodically to refresh the data, but only if the resource is currently in use by a Session.

"fetch" callback
ArgumentType
paramsobject | Immutable.MapThe params that were passed to the request function during the transaction.
options.storageanyThe value that was previously returned by the "initStorage" callback.
options.invalidatefunction()May be called at any time to indicate that the data from this specific fetch should no longer be cached in any way.
options.onCancelfunction(callback)May be called to register a cancellation callback.

Return value: object | Promise

The fetch callback function is called whenever the asynchronous data should be retrieved, either for the first time or to refresh existing data. For example, this is where you would perform an HTTP request. As its first argument the callback receives the params that were given during the transaction. The second argument is an options object containing optional utilities.

The options.invalidate() function may be called at any time to indicate that the data from this specific fetch should no longer be cached in any way. If a transaction requests this resource again it will always result in the fetch callback being called again. This can be used to implement more advanced caching strategies

The options.onCancel(callback) function maybe called to register a cancellation callback. When a resource is no longer in use, or if a fetch callback is superseded by a more recent fetch callback, all cancellation callbacks will be called. This can be used for example to cancel a http request.

The return value of the fetch callback is passed to the dispatcher callback of the Manager. This allows for easy integration with state store frameworks such as redux.

"clear" callback
ArgumentType
paramsobject | Immutable.MapThe params that were passed to the request function during the transaction.
options.storageanyThe value that was previously returned by the "initStorage" callback

Return value: object | Promise

The clear callback callback function is called whenever asynchronous data that has been previously retrieved, is no longer in use.

When integration with a state store framework such as redux, this is where an action should be dispatched that causes the asynchronous data to be removed from the store.

The return value of the clear callback is passed to the dispatcher callback of the Manager.

"initStorage" callback
ArgumentType
paramsobject | Immutable.MapThe params that were given during the transaction.

Return value: any

This callback function is called the first time a resource is fetched (for the specific combination of resource name and params). The return value is passed to any subsequent fetch callback and clear callback. This feature is useful if you need to keep track of some sort of state between (re-)fetching and clearing the same resource.

Time interval values

Time intervals, such as "cacheMaxAge", can be expressed in two ways. If the value is a javascript number, it specifies the amount of milliseconds. If the value is a string, it must consist of a (floating point / rational) number and a suffix to indicate if the number indicates milliseconds ("ms"), seconds ("s"), minutes ("m"), hours ("h") or days ("d"). Here are some examples:

Input ValueMillisecondsDescription
101010 Milliseconds
"10ms"1010 Milliseconds
"10s"1000010 Seconds
"10m"60000010 Minutes
"10h"3600000010 Hours
"10d"86400000010 Days (10 * 24 hours)

A minimal example

manager.resource({
    name: 'thing',
    fetch: async (params) => {
        const payload = await doApiCall('/thing', params);
        return {type: 'FETCH_THING', params, payload};
    },
    clear: (params) => {
        return {type: 'CLEAR_THING', params};
    },
});

A more exhaustive example:

manager.resource({
    name: 'thing',
    initStorage: (params) => {
        return {
            fetchCount: 0,
        };
    },
    fetch: async (params, {storage, invalidate, onCancel}) => {
        ++storage.fetchCount;

        const controller = new AbortController();
        onCancel(() => controller.abort());

        const url = '/thing/' + encodeURIComponent(params.thingId);
        const response = await fetch(url, {
            signal: controller.signal,
        });
        const payload = await response.json();

        if (payload.maximumCacheDurationMs) {
            setTimeout(() => invalidate(), payload.maximumCacheDurationMs);
        }

        return {type: 'FETCH_THING', params, payload};
    },
    clear: (params, {storage}) => {
        console.log('This resource was fetched', storage.fetchCount, 'times!');
        return {type: 'CLEAR_THING', params};
    },
    maximumStaleness: '10m',
    cacheMaxAge: '5m',
    refreshInterval: '30s',
});

manager.resources(options)

Return value: undefined \ Throws: See manager.resource(options)

This function is a simple shorthand that lets you register multiple resources in a single call. It accepts an array for which every item is registered as a resource in exactly the same way as manager.resource(options).

Example:

manager.resources([
    {
        name: 'thing',
        fetch: async (params) => {
            const payload = await doApiCall('/thing', params);
            return {type: 'FETCH_THING', params, payload};
        },
        clear: (params) => {
            return {type: 'CLEAR_THING', params};
        },
    },
    {
        name: 'otherThing',
        fetch: async (params) => {
            const payload = await doApiCall('/otherThing', params);
            return {type: 'FETCH_OTHER_THING', params, payload};
        },
        clear: (params) => {
            return {type: 'CLEAR_OTHER_THING', params};
        },
    },
]);

manager.createSession([options])Session

ArgumentTypeDefault
options.allowTransactionAbortbooleanValue of the allowTransactionAbort option passed to the createManager functionIf false, overlapping transactions are not allowed for this session. If true an overlapping transaction for this Session will cause the previous transaction to be aborted.

Return value: Session object \ Throws: IllegalStateError if the Manager has been destroyed

Creates a new Session object, which is used to manage which resources are actually in use. See Session API.

manager.invalidate([resourceName], [params]) ⇒ number

ArgumentType
resourceNamestringThe resource name that was previously given to the request function.
paramsobject | Immutable.MapThe params that was previously given to the request function.

Return value: number : The number of resources that have actually been invalidated \ Throws: No

Invalidate all matching resources. If a matching resource is currently in-use by a Session, the next time the resource is requested the fetch callback will be called again. If a matching resource is not currently in-use by any Session the clear callback will be called immediately.

If 0 arguments are passed to this function, all resources will be invalidated. If 1 argument is passed, all resources with the given resource name are invalidated. If 2 arguments are passed, only one specific resource is invalidated.

manager.refresh([resourceName], [params]) ⇒ number

ArgumentType
resourceNamestringThe resource name that was previously given to the request function.
paramsobject | Immutable.MapThe params that was previously given to the request function.

Return value: number \ Throws: IllegalStateError if the Manager has been destroyed \ Throws: CompositeError containing further errors in the "errors" property, if the dispatcher callback has thrown for any resource.

Refresh all matching resources. If a matching resource is currently in-use by a Session, the fetch callback is immediately called again. If a matching resource is not currently in-use by any Session the clear callback will be called immediately.

If 0 arguments are passed to this function, all resources will be refreshed. If 1 argument is passed, all resources with the given resource name are refreshed. If 2 arguments are passed, only one specific resource is refreshed.

manager.destroy()

Return value: undefined \ Throws: No

Destroy the Manager instance. All resources are cleared, all sessions are destroyed and the Manager is no longer allowed to be used.


Session API

A Session object is used to request resources from the Manager. If multiple Session objects request the same resources, the Manager will make sure that the same resource is only fetched once. The Session object will remember which resources you are currently using. A resource that is in-use will never be cleared.

A transaction is used to change which resources are in-use by a Session. Such a transaction has an explicit beginning and end. A transaction from the same Session object is not allowed to overlap with a different transaction that is still active. While the transaction is active a request function is available which should be called to request a specific resource. Doing so marks a specific resource as being in-use in the Session. When the transaction ends, all of the requested resources are compared to those requested in the previous transaction, the resources that have not been requested again are then no longer marked as in-use.

session(callback) ⇒ any

ArgumentType
callbackfunction(request)The callback function that determines the lifetime of the transaction. The request function is passed as the first argument to this callback.

Return value: Same as the return value of the called "callback" \ Throws: TypeError if callback is not a function \ Throws: IllegalStateError if the Session has been destroyed \ Throws: IllegalStateError if another transaction is still in progress (can only occur if allowTransactionAbort is false) \ Throws: Any thrown value from the called callback function

By calling the Session object as a function, a new transaction begins. The given "callback" argument is then immediately called, with the request function as an argument. This request function is used to request resources with. When the "callback" function returns, the transaction ends. If a Promise is returned the transaction will end after the promise has settled.

If the allowTransactionAbort option passed to createManager was set to false (the default), an overlapping transaction will result in an error to be thrown by this function. If the option was set to true, the previous transaction will be aborted if they overlap. If an transaction is aborted, the request function will throw an error any time it is used.

request(resourceName, params) ⇒ any
ArgumentType
resourceNamestringA resource name belonging to a previously registered Resource Definition
paramsobject | Immutable.MapThe params to pass on to the fetch callback

Return value: The value returned by the dispatcher callback \ Throws: MeridviaTransactionAborted if the transaction has been aborted (can only occur if allowTransactionAbort is true) \ Throws: MeridviaTransactionAborted if the session has been destroyed \ Throws: IllegalStateError if the transaction has ended \ Throws: ValueError if the given resource name has not been registered \ Throws: Any thrown value from the dispatcher callback

Request a specific resource and mark it as in-use for the Session. The resource name must be belong to a registered Resource Definition. The Manager will determine if the combination of resource name and params (a resource) has been requested previously and is allowed to be cached.

If the resource is not cached: the fetch callback of the Resource Definition will be called, and the return value of this callback is passed to the dispatcher callback of the Manager. The return value of the dispatcher callback is then returned from the request function. If the resource is cached then the request function will simply return the same value as it did the first time the resource was requested.

Conceptually, the implementation of the request function looks a bit like this:

function request(resourceName, params) {
    const resourceDefinition = getResourceDefinition(resourceName);
    const resource = getResource(resourceName, params);

    if (resource.isCached()) {
        return resource.cachedValue;
    }
    else {
        return resource.cachedValue = dispatcher(resourceDefinition.fetch(params));
    }
}

Example of a transaction

session(request => {
    request('post', {postId: 1});
    request('post', {postId: 2});
    request('comments', {postId: 2});
});

Example of a transaction with promises

async function example() {
    await session(async request => {
        const post = await request('post', {postId: 1});
        request('user', {userId: post.authorId});
    });
}

example().then(() => console.log('End of example'));

session.destroy()

Return value: undefined \ Throws: No

Destroy the session. All resources that were marked as in-use for this Session are unmarked as such. Attempting to use the Session again will result in an error.


Example: React Lifecycle methods and setState

/*
This example demonstrates how this library could be used to fetch and
display the details of a "user account" using the react
lifecycle methods componentDidMount, componentWillUnmount,
componentDidUpdate and then store the result in the react component
state.

This example includes:
* A fake API which pretends to fetch details of a user
  account using a http request
* A meridvia resource manager on which we register a resource for
  the user account details
* A react component which lets the resource manager know which
  resources it needs using a meridvia session and stores the result
* Some logging to demonstrate what is going on

This is a trivial example to demonstrate one way to integrate this
library with react. It has been kept simple on purpose, however the
strength of this library becomes most apparent in more complex code
bases, for example: When the same resource is used in multiple
places in the code base; When resources should be cached; When data
has to be refreshed periodically; Et cetera.
*/

const {Component, createElement} = require('react');
const ReactDOM = require('react-dom');
const {createManager} = require('meridvia');

const myApi = {
    // Perform a http request to fetch the user details for the
    // given userId
    userDetails: async (userId) => {
        // This is where we would normally perform a real http
        // request. For example:
        //   const response = await fetch(`/user/${encodeURIComponent(userId)}`);
        //   if (!response.ok) {
        //     throw Error(`Request failed: ${response.status}`);
        //   }
        //   return await response.json();
        // however to keep this example simple, we only pretend.
        await new Promise(resolve => setTimeout(resolve, 10));
        if (userId === 4) {
            return {name: 'Jack O\'Neill', email: 'jack@example.com'};
        }
        else if (userId === 5) {
            return {name: 'Isaac Clarke', email: 'iclarke@unitology.gov'};
        }
        throw Error(`Unknown userId ${userId}`);
    },
};

const setupResourceManager = () => {
    // Set the `dispatch` callback so that the return value of
    // the "fetch" callback (a promise that will resolve to the
    // api result) is returned as-is from the request function during
    // the session.
    // (The library will cache this value as appropriate).
    const dispatcher = value => value;
    const resourceManager = createManager(dispatcher, {
        // Because we are using promises during the transaction, it is
        // possible that the transactions might overlap. Normally this
        // is not allowed. By setting this option to true, the
        // older transaction will be aborted instead.
        allowTransactionAbort: true,
    });

    resourceManager.resource({
        name: 'userDetails',
        fetch: async (params) => {
            console.log('Resource userDetails: fetch', params);
            const {userId} = params;
            const result = await myApi.userDetails(userId);
            return result;
        },
    });

    return resourceManager;
};

class Hello extends Component {
    constructor(props) {
        super(props);
        this.state = {user: null};
    }

    componentDidMount() {
        console.log('<Hello/> componentDidMount');
        // Component is now present in the DOM. Create a new
        // meridvia session which will represent the resources
        // in use by this component. The resource manager will
        // combine the state of all active sessions to make
        // its decisions.
        this.session = this.props.resourceManager.createSession();
        this.updateResources();
    }
    componentWillUnmount() {
        console.log('<Hello/> componentWillUnmount');
        // The component is going to be removed from the DOM.
        // Destroy the meridvia session to indicate that we
        // no longer need any resources. Attempting to use
        // the session again will result in an error.
        this.session.destroy();
    }
    componentDidUpdate() {
        console.log('<Hello/> componentDidUpdate');
        // The props have changed.
        // In this example the specific resource that we need is based
        // on the "userId" prop, so we have to update our meridvia
        // session
        this.updateResources();
    }

    updateResources() {
        this.session(async request => {
            const user = await request('userDetails', {
                userId: this.props.userId,
            });

            if (user !== this.state.user) {
                this.setState({user});
            }
        });
    }

    render() {
        const {user} = this.state;
        return createElement('div', {className: 'Hello'},
            user ? `Hello ${user.name}` : 'Loading...'
        );
        /* If you prefer JSX, this is what it would look like:
        return <div className="Hello">
            {user ? `Hello ${user.name}` : 'Loading...'}
        </div>
        */
    }
}

const example = () => {
    const resourceManager = setupResourceManager();

    // Create the container element used by react:
    const container = document.createElement('div');
    document.body.appendChild(container);

    // create a DOM MutationObserver so that we can log
    // what the effects of the rendering are during this example
    const observer = new MutationObserver(() => {
        console.log('Render result:', container.innerHTML);
    });
    observer.observe(container, {
        attributes: true,
        characterData: true,
        childList: true,
        subtree: true,
    });

    const renderMyApp = userId => {
        const element = createElement(Hello, {resourceManager, userId}, null);

        /* If you prefer JSX, this is what it would look like:
        const element = (
            <Hello resourceManager={resourceManager} userId={userId} />
        );
        */
        ReactDOM.render(element, container);
    };

    console.log('First render...');
    renderMyApp(4);

    setTimeout(() => {
        console.log('Second render...');
        renderMyApp(5);
    }, 100);
};

example();

Output:

First render...
<Hello/> componentDidMount
Resource userDetails: fetch { userId: 4 }
Render result: <div class="Hello">Loading...</div>
<Hello/> componentDidUpdate
Render result: <div class="Hello">Hello Jack O'Neill</div>
Second render...
<Hello/> componentDidUpdate
Resource userDetails: fetch { userId: 5 }
<Hello/> componentDidUpdate
Render result: <div class="Hello">Hello Isaac Clarke</div>

Example: React Lifecycle methods and redux

/*
This example demonstrates how this library could be used to fetch and
display the details of a "user account" using redux and the react
lifecycle methods componentDidMount, componentWillUnmount and
componentDidUpdate.

This example includes:
* A fake API which pretends to fetch details of a user
  account using a http request
* A redux store which stores the user account details
* A reducer for the redux store that handles fetch and clear
  actions for the details of a specific user account
* A meridvia resource manager on which we register a resource for
  the user account details
* A react component which lets the resource manager know which
  resources it needs using a meridvia session.
* A react-redux container which passes the user details from the
  state store to the component.
* Some logging to demonstrate what is going on

This is a trivial example to demonstrate one way to integrate this
library with react and redux. It has been kept simple on purpose,
however the strength of this library becomes most apparent in more
complex code bases, for example: When the same resource is used in
multiple places in the code base; When resources should be cached;
When data has to be refreshed periodically; Et cetera.
*/

const {Component, createElement} = require('react');
const ReactDOM = require('react-dom');
const {createStore, combineReducers, applyMiddleware} = require('redux');
const {Provider: ReduxProvider, connect} = require('react-redux');
const {default: promiseMiddleware} = require('redux-promise');
const {createManager} = require('meridvia');

const myApi = {
    // Perform a http request to fetch the user details for the
    // given userId
    userDetails: async (userId) => {
        // This is where we would normally perform a real http
        // request. For example:
        //   const response = await fetch(`/user/${encodeURIComponent(userId)}`);
        //   if (!response.ok) {
        //     throw Error(`Request failed: ${response.status}`);
        //   }
        //   return await response.json();
        // however to keep this example simple, we only pretend.
        await new Promise(resolve => setTimeout(resolve, 10));
        if (userId === 4) {
            return {name: 'Jack O\'Neill', email: 'jack@example.com'};
        }
        else if (userId === 5) {
            return {name: 'Isaac Clarke', email: 'iclarke@unitology.gov'};
        }
        throw Error(`Unknown userId ${userId}`);
    },
};

// In the state store, userDetailsById contains the
// details of a user, indexed by the userId:
//   userDetailsById[userId] = {name: ..., email: ...}
const userDetailsByIdReducer = (state = {}, action) => {
    if (action.type === 'FETCH_USER_DETAILS') {
        // In this example we only store the resolved
        // value of the api call. However you could also
        // store an error message if the api call fails,
        // or an explicit flag to indicate an api call is
        // in progress.
        const newState = Object.assign({}, state);
        newState[action.userId] = action.result;
        return newState;
    }
    else if (action.type === 'CLEAR_USER_DETAILS') {
        // Completely remove the data from the state store.
        // `delete` must be used to avoid memory leaks.
        const newState = Object.assign({}, state);
        delete newState[action.userId];
        return newState;
    }
    return state;
};

// The reducer used by our redux store
const rootReducer = combineReducers({
    userDetailsById: userDetailsByIdReducer,
});

const setupResourceManager = (dispatch) => {
    // The resource manager will pass on the return value of `fetch`
    // and `clear` to the `dispatch` callback here
    const resourceManager = createManager(dispatch);

    resourceManager.resource({
        name: 'userDetails',
        fetch: async (params) => {
            // This function returns a promise. In this example
            // we are using the redux-promise middleware. Which
            // will resolve the promise before passing the action
            // on to our reducers.

            console.log('Resource userDetails: fetch', params);
            const {userId} = params;
            const result = await myApi.userDetails(userId);
            return {
                type: 'FETCH_USER_DETAILS',
                userId,
                result,
            };
        },
        clear: (params) => {
            console.log('Resource userDetails: clear', params);
            const {userId} = params;
            return {
                type: 'CLEAR_USER_DETAILS',
                userId,
            };
        },
    });

    return resourceManager;
};

class Hello extends Component {
    componentDidMount() {
        console.log('<Hello/> componentDidMount');
        // Component is now present in the DOM. Create a new
        // meridvia session which will represent the resources
        // in use by this component. The resource manager will
        // combine the state of all active sessions to make
        // its decisions.
        this.session = this.props.resourceManager.createSession();
        this.updateResources();
    }
    componentWillUnmount() {
        console.log('<Hello/> componentWillUnmount');
        // The component is going to be removed from the DOM.
        // Destroy the meridvia session to indicate that we
        // no longer need any resources. Attempting to use
        // the session again will result in an error.
        this.session.destroy();
    }
    componentDidUpdate() {
        console.log('<Hello/> componentDidUpdate');
        // The props have changed.
        // In this example the specific resource that we need is based
        // on the "userId" prop, so we have to update our meridvia
        // session
        this.updateResources();
    }

    updateResources() {
        this.session(request => {
            request('userDetails', {userId: this.props.userId});
        });
    }

    render() {
        const {user} = this.props;
        return createElement('div', {className: 'Hello'},
            user ? `Hello ${user.name}` : 'Loading...'
        );
        /* If you prefer JSX, this is what it would look like:
        return <div className="Hello">
            {user ? `Hello ${user.name}` : 'Loading...'}
        </div>
        */
    }
}
// A react-redux container component
const HelloContainer = connect((state, props) => ({
    user: state.userDetailsById[props.userId],
}))(Hello);


const example = () => {
    const store = createStore(rootReducer, applyMiddleware(promiseMiddleware));
    const resourceManager = setupResourceManager(store.dispatch);

    // Create the container element used by react:
    const container = document.createElement('div');
    document.body.appendChild(container);

    // create a DOM MutationObserver so that we can log
    // what the effects of the rendering are during this example
    const observer = new MutationObserver(() => {
        console.log('Render result:', container.innerHTML);
    });
    observer.observe(container, {
        attributes: true,
        characterData: true,
        childList: true,
        subtree: true,
    });

    const renderMyApp = userId => {
        const element = createElement(ReduxProvider, {store},
            createElement(HelloContainer, {resourceManager, userId}, null)
        );
        /* If you prefer JSX, this is what it would look like:
        const element = <ReduxProvider store={store}>
            <HelloContainer resourceManager={resourceManager} userId={userId} />
        </ReduxProvider>
        */
        ReactDOM.render(element, container);
    };

    console.log('First render...');
    renderMyApp(4);

    setTimeout(() => {
        console.log('Second render...');
        renderMyApp(5);
    }, 100);
};

example();

Output:

First render...
<Hello/> componentDidMount
Resource userDetails: fetch { userId: 4 }
Render result: <div class="Hello">Loading...</div>
<Hello/> componentDidUpdate
Render result: <div class="Hello">Hello Jack O'Neill</div>
Second render...
<Hello/> componentDidUpdate
Resource userDetails: fetch { userId: 5 }
Resource userDetails: clear { userId: 4 }
<Hello/> componentDidUpdate
Render result: <div class="Hello">Loading...</div>
<Hello/> componentDidUpdate
Render result: <div class="Hello">Hello Isaac Clarke</div>