Writing plugins

Writing plugins is very easy once you grasp what hooks are, how they work and what getExtensions method is all about. Reading about lifecycle of methods will help a great deal also.

As there is nothing better than learning by example, here is a simple plugin. It adds prefix to a key under which we will store our items, adds this information to extra and provides method to get prefix used in that plugin.

Plugin:

export default function prefixPlugin(prefix) {
    return {
        hooks: [
            {
                event: 'preBuildKey',
                handler: ({ key, cacheInstance }) => {
                    const keyWithPrefix = `${prefix}.${key}`;

                    return { key: keyWithPrefix, cacheInstance };
                }
            },
            {
                event: 'preSetItem',
                handler: ({ key, value, extra, cacheInstance }) => {
                    const extraWithPrefix = Object.assign({}, extra, { prefix });

                    return { key, value, extra: extraWithPrefix, cacheInstance };
                }
            }
        ],
        getExtensions: (cacheInstance) => {
            return {
                getPrefix() {
                    return prefix;
                }
            }
        }
    }
}

And usage:

import { createCache, registerPlugins } from 'stash-it';
import createMemoryAdapter from 'stash-it-memory-adapter';
import createPrefixPlugin from 'some-module-we-created-this-plugin-in';

// First, cache instance
const adapter = createMemoryAdapter({ namespace: 'someNamespace' });
const cache = createCache(adapter);

// Now, plugin
const prefixPlugin = createPrefixPlugin('somePrefix');
const cacheWithPrefixes = registerPlugins(cache, [ prefixPlugin ];

// If we will store an item like so:
cacheWithPrefixes.setItem('itemKey', 'some value');

// And get it from cache (mind that I am using key used to set this item)
const item = cahceWithPrefixes.getItem('itemKey');

item.key; // 'someNamespace.somePrefix.itemKey

cacheWithPrefixes.getPrefix(); // somePrefix

I will now explain what is going on in every part of this plugin.

prefixPlugin(prefix)

As plugins are simple functions, here we name it prefixPlugin (there is no rule on how to name it), and pass prefix to it. In our usage example, we passed somePrefix.

This function will return an object that will have both array of hooks and getExtensions method. As you remember, plugins must have at least one of them (either hooks or getExtensions).

hooks

Hooks are an array of objects that contain event names and handlers. Let's have a look at first hook:

{
    event: 'preBuildKey',
    handler: ({ key, cacheInstance }) => {
        const keyWithPrefix = `${prefix}.${key}`;

        return { key: keyWithPrefix, cacheInstance };
    }
}

This hook uses preBuildKey event name. This means that whenever key will about to be built, whatever you will do in handler, will happen first. Here, key that user will pass will be prefixed with our prefix. See: preBuildKey. We're returning an object containing key and cacheInstance properties, as this is what buildKey will require.

2nd hook:

{
    event: 'preSetItem',
    handler: ({ key, value, extra, cacheInstance }) => {
        const extraWithPrefix = Object.assign({}, extra, { prefix });

        return { key, value, extra: extraWithPrefix, cacheInstance };
    }
}

This hook uses preSetItem event name. This means that whenever an item will about to be set, whatever you will do in handler, will happen first. Here, we are taking prefix and adding it to extra data to store it in item.

Now that hooks are explained, let's have a look at getExtesions.

getExtensions(cacheInstance)

This method will always have cacheInstance passed. Although we are not using it in our example, you are free to use all of it's methods. Have a look at API of cacheInstance.

Getting back to getExtensions. It must return an object with methods. Any methods? Not quite, have a look at registerPlugins API to see when it can throw. So, our object with methods we want our cache to have:

{
    getPrefix() {
        return prefix;
    }
}
What about lifecycle for each added methods, do I need to add them?

No. Here, in above example you don't need to. But it is a very good practice to add support for lifecycle of those methods.

As that plugin did not needed that, let's have a look, again, at a plugin shown in getExtensions docs. I will (almost) copy and paste whatever is written there for the sake of having this info on one page here. First, plugin:

export default function multiPlugin(prefix) {
    return {
        getExtensions: (cacheInstance) => {
            return {
                getItems: (keys) => {
                    const preData = getPreData('getItems', { keys, cacheInstance });
                    const items = preData.keys.map(cacheInstance.getItem);
                    const postData = getPostData('getItems', { keys: preData.keys, items, cacheInstance: preData.cacheInstance });

                    return postData.items; 
                }
            }
        }
    }
}

This new method is ready for events preGetItems and postGetItems. If you would be a creator of such a plugin, your responsibility is to provide detailed information how lifecycle of such a method looks like:

  • what event names are here (by convention it's the same as created method's name, plus prefixes); here we will have preGetItems and postGetItems;
  • what properties are passed in 2nd argument to getPreData and getPostData; here we will have, for pre object:
    { keys, cacheInstance }
    and for post:
    { keys, items, cacheInstance }

  • what should be returned by each method (getPreData and getPostData); here we will have, for pre:
    { keys, cacheInstance } (so the same as object passed, convention, see below)
    and for post:
    { keys, items, cacheInstance } (same thing as for pre)

  • what is returned by newly created method, here an array of items.

Final word

There is no strict rule for what is needed to be passed from preData to postData. There is just a convention that I encourage you to follow.

  • pre handlers should take an object as an argument, with properties being the arguments passed to extension, plus cacheInstance
  • pre handlers should always return an object with those properties (and cacheInstance)
  • extensions body (any functionality there is) should use / take data only returned by pre (including cacheInstance)
  • post should take an object as an argument, the very same in structure as pre but with values taken from pre and with result of extensions method
  • post should return an object with those properties
  • finally that method should return extensions method value passed to post and returned by it
  • you should always check for existence of item or extra as you might want to check their properties, but since they can be undefined - it's best to check that first

Mind that cacheInstance is always passed to getExtensions method.


results matching ""

    No results matching ""