createApp() builds the runtime; the App then moves through init, start, stop,
and dispose phases. This guide covers the options, the exact ordering, lazy
modules, and scopes.
import { createApp, createLoggerPlugin, provide } from "@cosystem/core";
const app = createApp({
providers: [Counter, provide(Logger, { useValue: console })],
plugins: [createLoggerPlugin()],
devOptions: { strictActions: true },
engine: { patches: true },
});| Option | Type | Description |
|---|---|---|
providers |
(ProviderInput | LazyModule)[] |
Modules, plain providers, and lazy-module entries. |
plugins |
Plugin[] |
Lifecycle/observability plugins. See Plugins. |
parent |
App | Container |
Parent container for hierarchical DI. |
devOptions |
{ strictActions?: boolean } |
Enforce action boundaries for all writes. |
engine |
{ patches?: boolean } |
Enable patch generation on the store. |
createApp() only creates the application runtime. It does not accept a root
view, render function, DOM container, or framework component — rendering is
always done by the host framework after the app exists.
createApp()
1. create the root container (optionally under a parent)
2. register plugin providers
3. normalize provider inputs (app providers can override plugin providers)
4. apply test overrides (testApp only)
5. freeze the provider graph
6. instantiate eager @Module providers
7. build the single Coaction store from module state
8. bind module state/actions/computed to the store
9. instantiate other eager providers
10. run onModuleCreated plugin hooks
11. init():
- run each plugin's setup(app, context) in registration order
- run module onInit() hooks
- start effects
(steps under init() are tracked by the stable app.ready promise)
app.start()
- await the init promise
- run module onStart() hooks
- mark the app started
app.stop()
- wait for staged lazy loads to roll back
- run module onStop() hooks in reverse order
- mark the app stopped
app.dispose()
- if init is still in flight, abort plugin contexts, wait for setup to settle,
and skip any remaining init work
- if start is still in flight, wait for onStart hooks before stopping
- dispose child apps in reverse creation order
- stop() if still running
- stop and drain effects
- run module onDispose() hooks in reverse order
- dispose dynamically loaded scopes in reverse load order
- dispose plugins and plugin context resources in reverse order
- dispose the container (provider dispose callbacks, reverse creation order)
- destroy the store
- if any teardown failed, throw one AggregateError after every phase ranA few consequences worth internalizing:
- Async initialization starts after
createApp()returns. Module creation andonModuleCreatedare synchronous; pluginsetup,onInit, and effects begin on the next microtask. Awaitready(orstart()) before work that depends on those phases. onInitand effects run during creation, not duringstart(). Many apps never needstart()at all — call it only when you have explicit startup work inonStarthooks.start()awaits init. If a plugin'ssetup(e.g. storage hydration) is async,start()waits for it.readyexposes init completion.app.readyis one stable promise. It rejects with initialization failures; the runtime also observes that rejection internally so creating an app without starting or awaiting it does not emit an unhandled rejection.- Lifecycle reentry is rejected. App-managed setup, lifecycle, effect, and
teardown work cannot call
start(),stop(), ordispose(): awaiting those operations from a phase they control can create a self-dependency. Setup andonInitwork likewise cannot awaitapp.ready. These calls return internally observed rejections; invoke lifecycle controls from application bootstrap. For portable enforcement after anawait, use the app supplied tosetuporModuleLifecycleContext.app; browser fallback does not treat unrelated external controls as hook reentry. - Lifecycle injection survives
await. UsePluginContext.inject()in plugin setup and theModuleLifecycleContexthook argument in modules. Their explicit resolvers remain app-scoped even when browser hooks overlap. - Teardown hooks run in reverse order and best-effort. Errors from module
hooks, effects, scopes, plugins, providers, and the store are collected while
the remaining cleanup continues, then re-thrown together as an
AggregateError. - Disposal is terminal. As soon as disposal begins, provider resolution, module lookup and writes, watches, explicit action boundaries, lazy loads, and new scopes are rejected. A module facade or nested state reference retained before disposal cannot run actions or mutate state after teardown. Disposed containers (including descendants of a disposed root) likewise reject further use.
app.state.version; // increments on every store change
app.started; // boolean
app.store.getPureState(); // full plain state tree
await app.ready; // plugin setup and module onInit are complete
app.getModule(Counter); // bound module facade
app.getModuleByName("counter"); // look up by registered name
app.get(Token); // resolve any provider (sync)
await app.getAsync(Token); // resolve, allowing async factories
app.getAll(MultiToken); // all multi providersSubscribe to derived values with watch (see
State & Reactivity):
const stop = app.watch(
() => app.getModule(Counter).count,
(value, previous) => console.log(value, previous),
{ immediate: false },
);
stop();Lazy modules let you load functionality after the app is built — for code splitting or feature gating — without mutating the root provider graph. Each lazy module is loaded into its own child scope.
import { createApp, defineModule, lazyModule } from "@cosystem/core";
class AdminCounter {
count = 0;
increase(): void {
this.count += 1;
}
}
defineModule(AdminCounter, { actions: ["increase"], name: "adminCounter", state: ["count"] });
const app = createApp();
await app.load(lazyModule(() => ({ providers: [AdminCounter] })));
app.getModule(AdminCounter).increase();-
Passing
lazyModule(...)increateApp({ providers })records the entry without loading it. -
await app.load()loads all pending lazy modules, in registration order. -
await app.load(module)loads one specific lazy module (idempotent — a second call returns the same result). -
Concurrent loads of the same
LazyModuleshare one loader and initialization promise. -
Providers and modules are staged in a temporary child scope. After
onInitand (for a started app)onStartsucceed, the runtime installs the state and performs each effect's initial synchronous run as one publication transaction. State subscribers, watches, patches, and the app state version observe only the committed result. A throwing store subscriber is reported to pluginonErrorhooks with phase"store:subscribe"; it does not roll back the committed load or prevent later subscribers from observing it. -
A failed load, including a synchronous effect-startup failure, discards that publication, restores local module bindings for teardown, disposes the temporary scope, and rolls back all staged runtime state. A later call retries from a fresh scope.
-
app.stop()waits for loads already in the staging phase to roll back. New loads reject while stopping and may be retried after the stop completes. -
Once app disposal begins, lazy loads reject instead of installing new modules.
-
A loader may return a provider, a provider array, or a module-namespace object (
{ default }/{ providers }), which makes dynamicimport()ergonomic:await app.load(lazyModule(() => import("./admin/module.js")));
When a lazy module loads after start(), its onInit and (if the app is
started) onStart hooks run, and its effects start immediately. Loading after
dispose() throws. The result describes the created modules and the scope:
const { modules, scope } = await app.load(adminModule);app.createScope(options?) returns an AppScope whose container is a child DI
scope. Use it for request/view/worker-scoped providers, or to build()
unregistered classes:
const scope = app.createScope();
const handler = scope.container.build(RequestHandler);Scoped providers resolve to one instance per child scope; see Dependency Injection.
Pass parent to nest one app/container under another, so a child app can resolve
the parent's providers:
const child = createApp({ parent: app, providers: [FeatureModule] });Disposing a parent app disposes its child apps first, in reverse creation order. Once parent disposal begins, creating another child app is rejected.
- State & Reactivity — the store, strict actions, patches.
- Plugins — hook into these phases.
- Worker & Shared Runtime — run modules off the main thread.