How it works
What createApp does in order, how config merges, what require returns, and how events dispatch.
On this page
Inside createApp
The kernel is one synchronous pass. When createApp returns, every plugin is initialized.
- Check the list. Names and dependencies, before any plugin code runs. See What the kernel checks.
- Freeze the global config. Defaults, then the app's overrides.
- Build the core plugins. For all of them
config, thencreateState, thenapi, thenonInit. Their APIs exist from here on. - Resolve plugin configs. One frozen object per plugin.
- Create state.
createStatein list order, with{ global, config }. - Register hooks. The
hooksfactories run in list order. Handlers are stored, nothing fires yet. - Build APIs. The
apifactories in list order. - Initialize.
onInitin list order, with the fullctx. - Call
onReady. The framework's, then the app's. - Return the frozen app. Plugin APIs are mounted by name.
start() and stop() add the rest:
createApp() core onInit → plugin onInit → framework onReady → app onReady
app.start() core onStart → plugin onStart → app onStart
app.stop() plugin onStop, reversed → core onStop, reversed → app onStopThe rules for each phase, the async model and failures are on Lifecycle.
How config merges
Every merge is shallow, and the last one wins.
| Value | Merge order |
|---|---|
| Global config | createCoreConfig({ config }) → createApp({ config }) |
| Plugin config | spec config → createCore({ pluginConfigs }) → createApp({ pluginConfigs }) |
| Core plugin config | spec config → createCoreConfig({ pluginConfigs }) → createCore({ pluginConfigs }) → createApp({ pluginConfigs }) |
import { createCoreConfig } from "@moku-labs/core";
const coreConfig = createCoreConfig("demo", { config: {} });
const cachePlugin = coreConfig.createPlugin("cache", {
config: { ttl: 60, size: 100, limits: { read: 1, write: 2 } },
api: ctx => ({ config: () => ctx.config })
});
const { createApp } = coreConfig.createCore(coreConfig, {
plugins: [cachePlugin],
pluginConfigs: { cache: { ttl: 300, size: 500 } }
});
const app = createApp({
pluginConfigs: { cache: { size: 900, limits: { read: 5, write: 5 } } }
});
console.log(app.cache.config()); // { ttl: 300, size: 900, limits: { read: 5, write: 5 } }- The framework's defaults for this plugin.
- A nested object is replaced whole: pass every field of it.
The freeze is shallow too: limits above stays mutable. State is the only store a plugin is meant
to change.
What require returns
ctx.require(plugin) reads the API map as it is at that moment. Inside a handler or a lifecycle
method every API exists. A factory body runs earlier, while the map is still being filled.
ctx.require(x) called in | x is earlier in the list | x is later in the list |
|---|---|---|
The body of a hooks factory | {} | {} |
The body of an api factory | its API | {} |
A hook handler, an API method, onInit, onStart | its API | its API |
A core plugin's API is always there. A plugin registered with no api gives {}. A plugin that
is not registered throws.
import { createCoreConfig } from "@moku-labs/core";
const coreConfig = createCoreConfig("demo", { config: {} });
const clockPlugin = coreConfig.createPlugin("clock", {
api: () => ({ now: () => Date.now() })
});
coreConfig.createPlugin("stamp", {
depends: [clockPlugin],
events: register => ({ "stamp:touch": register<{ by: string }>() }),
createState: () => ({ lastAt: 0 }),
hooks: ctx => ({
"stamp:touch": () => {
ctx.state.lastAt = ctx.require(clockPlugin).now();
}
})
});- Inside the handler: the API is there.
How an event dispatches
emit starts the dispatch and returns. It gives the caller no promise and no result.
- The kernel finds the handlers of that event name. No handlers: nothing happens.
- It calls them in plugin order and awaits each one before the next.
- A handler that throws or rejects goes to the error handlers. The next handler still runs.
| Error handler | Called with | Order |
|---|---|---|
createCore({ onError }) | the error, the core plugin APIs | first |
createApp({ onError }) | the error, the app callback context | second |
With neither of them the error is dropped silently, so a framework should always set one. An
error thrown inside onError is ignored too. Errors from start() and stop() never come here:
they reject the call.
Who can emit and hear what is on Events.
What exists only in the types
The run-time is small because most of the contract never runs.
| Part | At compile time | At run time |
|---|---|---|
events | Adds the event names and payloads | Checked to be a function, never called |
depends | Adds the dependency's events to hooks | The order check in createApp |
_phantom on an instance | Carries config, state, API and events types | An empty object |
First argument of createCore | Carries the createPlugin types | Ignored |
So the description in register<T>("...") is for readers. Nothing can list events at run time.
- The app is frozen, plugin APIs are not.
Object.freezecovers the app object only. An API method can still change state through its closure, which is the point. - A plugin with no
apiis not onapp.app.has("name")is stilltrue. stop()is not guarded against a second call. It runs everyonStopagain. It throws only whenstart()never finished.- A failed
start()leaves the app half-started. Plugins already started stay started. Discard the app and create a new one.
Related
- Three layers: the factory chain and who writes what.
- Plugins: every spec field and the context tiers.
- Write a framework: all of this in one project.