The Fusion.js plugin architecture allows plugins to explicitly depend on other service plugins. This allows us to swap implementations of various subsystems, for example for testing, or to provide extended functionality.
A service plugin is a plugin that contains a service that exposes a programmatic API. The benefit of encapsulating a service into a plugin is that a plugin allows the service instance to be memoized on a per-request basis without polluting the middleware context, and the plugin can also encapsulate the colocation of all code needed to implement related endpoints, services and HTML template modifications.
Let's see how we can depend on a service that gets instantiated per request:
// src/main.js
import App from 'fusion-react';
import JWTSession from 'fusion-plugin-jwt';
import {SessionToken} from 'fusion-tokens';
import Name from './plugins/name'
export default () => {
const app = new App();
app.register(SessionToken, JWTSession);
app.register(Name);
}
// src/plugins/name.js
import {createPlugin} from 'fusion-core';
import {SessionToken} from 'fusion-tokens';
export default createPlugin({
deps: {Session: SessionToken},
middleware({Session}) {
return (ctx, next) => {
if (ctx.query.name) {
const session = Session.from(ctx);
session.set('name', ctx.query.name);
ctx.body = {ok: 1};
}
return next();
}
}
});
The Name
plugin simply saves the value in ?name=[value]
to a session cookie if that querystring value is defined.
Notice that the middleware
method of the Name
plugin receives {Session}
as an argument. This is the same {Session}
that we passed to app.register(SessionToken, JWTSession)
and it's how Fusion.js plugins do dependency injection.
We then called Session.from(ctx)
, which is how that plugin creates a memoized instance per request.
Let's create another contrived plugin to print the the value of the name
key from the session store:
// src/plugins/greeting.js
import {createPlugin} from 'fusion-core';
import {SessionToken} from 'fusion-tokens';
export default createPlugin({
deps: {Session: SessionToken},
middleware: ({Session}) => async (ctx, next) => {
if (ctx.path === '/greet') {
const session = Session.from(ctx);
ctx.body = {greeting: 'hello ' + await session.get('name')};
}
return next();
}
});
// src/main.js
import App from 'fusion-react';
import JWTSession from 'fusion-plugin-jwt';
import {SessionToken} from 'fusion-tokens';
import Name from './plugins/name';
import Greeting from './plugins/greeting';
export default () => {
const app = new App();
app.register(SessionToken, JWTSession);
app.register(Name);
app.register(Greeting);
}
Here, the Name
plugin sets state in an instance of the session service, and the Greeting
plugin reads the state from the same instance of the session service.
Notice that in the example above, once we have the memoized instance of the session service (which the plugin provided to us) we only ever call session.set
and session.get
.
This means we can easily mock the Session
dependency in both Name
and Greeting
plugins when unit testing them, and it also means that if we want to use, for example, Redis as a session store, all we need to do is replace fusion-plugin-jwt
with a plugin that uses Redis.