Skip to content

Latest commit

 

History

History
184 lines (146 loc) · 6.47 KB

File metadata and controls

184 lines (146 loc) · 6.47 KB

Application Lifecycle

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.

createApp(options)

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.

Phase ordering

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)
       - run module onInit() hooks
       - start effects
     (steps under init() are tracked by an internal init promise)

app.start()
  - await the init promise
  - run module onStart() hooks
  - mark the app started

app.stop()
  - 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
  - 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

A few consequences worth internalizing:

  • onInit and effects run during creation, not during start(). Many apps never need start() at all — call it only when you have explicit startup work in onStart hooks.
  • start() awaits init. If a plugin's setup (e.g. storage hydration) is async, start() waits for it.
  • Teardown hooks run in reverse order and fail fast. If an onStop or onDispose hook throws, later lifecycle hooks in that phase do not run and the error is re-thrown after plugin error hooks are notified.

Reading the app

app.state.version; // increments on every store change
app.started; // boolean
app.store.getPureState(); // full plain state tree

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 providers

Subscribe 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

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();

Loading model

  • Passing lazyModule(...) in createApp({ 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).

  • 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 dynamic import() 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);

Scopes

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.

Hierarchical apps

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] });

Next