Hooks
Edit on GitHubHooks are executed throughout various stages in the lifecycle of a Primate app. Some will execute only once on startup, others on given events. Hook subscribers accept different types of parameters, depending on the hook.
Lifecycle
┌─ # read configuration and merge with defaults
│
├─ `load`
│ └─ # modules load other modules
│
├─ `init`
│ └─ # modules initialize
│
├─ # *begin* start-up phase, all hooks in this phase are called once
│
├─ `register`
│ └─ # modules register component file extensions for the view handler
│
├─ # create `build/{server,components}` directory
├─ # copy all files from `components` to `build/components`
├─ # copy .js files from `components` to `build/server`
│
├─ `publish`
│ └─ # modules publish client-side code and announce entry points
│
├─ # *end* start-up phase
│
├─ # *begin* client request phase, hooks here are called per request
│
├─ `handle` # on client request
│ └─ # modules handle client request themselves or yield to `next`
│
├─ # if yielded through, match request against static assets in `public`
│
├─ # if unmatched, match request against in-memory assets
│
| # if yielded through, match route
│
├─ `route` # if previously unmatched
│ └─ # modules handle routing request themselves or yield to `next`
│
├─ `context` #
│ └─ # modules provide context information to the frontend framework
│
| # if yielded through, execute route function
│
└─ # *end* client request phase due to program shutdown
load
Executed once
Precondition configuration has been loaded and merged with defaults
The first hook to be called, directly after app start-up and loading the configuration file. This hook is for modules to load other modules.
export default {
modules: [{
name: "load-hook-example-module",
// return a list of modules
load() {
return [
{
name: "dependent-name"
},
];
},
}],
};
app.load
are not currently triggered
by load
hook themselves. Also, unlike most other hooks, the load
hook does
not accept a final next
parameter.init
Executed once
Precondition all modules have been loaded
The hook allows modules to initialize state before any other hooks are called.
const module = () => {
let app;
return {
name: "init-hook-example-module",
init(app$) {
// storage reference to app
app = app$;
},
};
};
export default {
modules: [
module(),
],
};
init
hook does not accept a final next
parameter.register
Executed once
Precondition none
This hook allows modules to register a component file extension to be handled
by view
.
const mustacheHandler = (name, props, options) => {
// load the component file using its name and render it into HTML with props
};
export default {
modules: [{
name: "register-hook-example-module",
// accepts the app object augmented with a `register` function
register(app, next) {
app.register("mustache", mustacheHandler);
return next(app);
},
}],
};
By that definition, any mustache
file in components
will be handled by the
specified mustacheHandler
handler function.
import { view } from "primate";
export default {
get(request) {
const time = request.query.get("time");
return view("clock.mustache", { time });
},
};
Assuming a clock component which takes a time value and shows a clock has been
defined in components/clock.mustache
, a client requesting
GET /clock?time=11:45am
will have the view
handler show a mustache
component rendered into HTML.
publish
*Executed once
Precondition none
This hook allows modules to publish client-side code and entry points. It is particularly useful for frontend frameworks, which may need to register their own core scripts.
Publishing entry points allow bundler modules to effectively consolidate code
during the bundle
hook.
This hook accepts the app as its first and the next subscriber as its second parameter.
handle
Executed for every request
Precondition all start-up hooks have been executed, and Primate has left the start-up phase and is serving content
This hook allows modules to handle requests themselves, before Primate tries to handle them on its own. It has the highest priority, being resolved before any static or in-memory assets are served and before route functions are matched. It is particularly useful if you want to treat certain routes in a special way, but otherwise let Primate do the routing for you.
It is particularly useful for modules which modify the request object in a way that exposes further functionality for all subsequent handlers, such as the session module.
If you need to manipulate the request object for just for the route function,
use the route
hook instead.
const augment = request => {
return { ...request, /*
augment request with additional properties such as client ip, by reading
them from the original request at `request.original`
*/ };
};
export default {
modules: [{
name: "handle-hook-example-module",
// accepts the request object without the `path` property
handle(request, next) {
return next(augment(request));
},
}],
};
route
Executed for every request
Precondition the request hasn't been completely handled by the handle
phase and a route has been matched
This hook allows modules to transform requests after they have been matched by a route function but before it executes.
It is particularly useful for modifying the request object in a way that exposes further functionality specifically for the route function such as the store module.
If you need to manipulate the request object for all requests (including
assets), use the handle
hook instead.
const delegate = request => {
// pass the request to admin app
};
export default {
modules: [{
name: "route-hook-example-module",
// accepts the request object without the `path` property
route(request, next) {
if (request.url.pathname.startsWith("/admin")) {
// delegate request to admin app
return delegate(request);
}
// otherwise let other subscribers or Primate handle the request
return next(request);
},
}],
};
When should I call `next`?
Calling next
in a hook means calling the next module in line which has
subscribed to the hook, and in the case of the handle
and route
hooks,
eventually passing over control back to Primate. Most of the time, it makes
sense to call next
because you want other modules to do their job. However,
especially in the case of handle
and route
, you sometimes deliberately
don't want to call next
because you're handling or routing the request
completely on your own. In some circumstances, a hybrid solution makes the most
sense: some requests you're handling on your own, and for some you want the
normal Primate logic to run its due course.