Guide · @staticflux/core

Core & dependency injection

The fabric every package builds on: typed tokens, units that declare what they require and provide, and a hierarchical injector that knows the difference between “created” and “ready”.

Tokens and units

A token is a typed identity: token('pkg/name').type<T>() for one value, .multi<T>() for a contribution list (routes, menu items, telemetry sinks). The string id is namespaced, type-checked and used for devtools — there are no string lookups at runtime.

A unit declares provides, scope, deps and a synchronous create. Optional init makes it ready asynchronously (single-flight, cached, retried by retry), refreshOn re-runs it when a token or the app epoch refreshes, and critical: true makes boot wait for it.

the create → init pattern (packages/core/AGENTS.md)
import { err, toSfError, token, unit } from '@staticflux/core';

export const DocRuntimeToken = token('app/doc-runtime').type<DocRuntimeHolder>();

export const docRuntimeUnit = unit({
  id: 'app/doc-runtime-unit',
  provides: DocRuntimeToken,
  scope: 'app',
  deps: { required: { client: DataClientToken, supervisor: SupervisorToken } },
  // Capture deps in create (synchronous, cheap, no I/O) …
  create: ({ client, supervisor }, ctx): DocRuntimeHolder => {
    const holder: DocRuntimeHolder = {
      current: undefined,
      async load(signal) {
        const chunk = await import('./doc-runtime.js');   // heavy code stays off the boot path
        if (signal.aborted) return err(toSfError(signal.reason));
        holder.current = chunk.createRuntime({ client, telemetry: ctx.telemetry, scope: ctx.scope });
        return supervisor.register(holder.current.subsystem);
      },
    };
    return holder;
  },
  // … use them in init, through the instance. init makes the unit ready (or fails it).
  init: (holder, ctx) => holder.load(ctx.signal),
  retry: { attempts: 3 },
});

Three kinds of dependency

KindYou receiveSemanticsDefault init
requiredTMust be ready before this unit is created (components call it render: placeholder until ready)eager
lazyDepHandle<T>Arrives any time; reactive state / value; whenReady()on-arrival
functionalFunctionalDep<T>Resolved and initialized on first get(); prewarm() on intenton-demand

Override timing per dependency: { token, init: 'idle' | 'on-visible' | 'on-intent' | … }. Hooks let a consumer act on a dependency inside a scope that is disposed when the consumer goes away or the dependency refreshes:

TypeScript
create: ({ presence }, ctx) => {
  const count = box(0);
  // re-runs against the fresh instance after every refresh, in a fresh child scope
  ctx.onReady(presence, (feed, scope) => {
    scope.defer(feed.subscribe((n) => count.set(n)));
  });
  return { count };
},

Scopes, levels and the injector tree

Scope levels run from longest- to shortest-lived: app → session → workspace → route → feature → view → component. injector.child(level, name, units) creates a child injector; a longer-lived unit can never hold a shorter-lived one (SF-DI-002), and cycles are rejected with the path (SF-DI-004). Logging out disposes the session injector and everything in it.

Every instance owns a Scope (ctx.scope): defer cleanups, use disposables, listen to events, child scopes, and a signal that aborts when the scope is disposed.

Overrides and variants

TypeScript
import { bind, variant } from '@staticflux/core';

const units = [
  rankerUnit,                                                        // package default
  bind(fastRankerUnit, { precedence: 'app', source: 'app config' }), // app override
  variant({ of: RankerToken, unit: rankerBUnit, when: () => cohort === 'B' }), // experiment
];
// precedence: experiment > workspace > app > default — and every override shows in devtools

In apps, experiments come from flags through @staticflux/config’s createFlagBinder().variants(...), so feature code never reads a flag.

Compile-time completeness

TypeScript
import { composeUnits, defineModule } from '@staticflux/core';

export const appUnits = composeUnits([docsUnit, dataClientUnit /* missing: transport */]);
//  ✗ Property '__missingProviders' is missing … "data/transport"

// a package or feature module declares what its host must provide
export const docsModule = defineModule({ id: 'docs/module', units: [docsUnit], external: [DataClientToken] });

Results, Tasks and machines

  • Result<T, E> with ok / err / attempt / attemptAsync; errors are SfErrors from defineError(code, kind, summary, fix).
  • Task<T> = (signal) => Promise<Result<T>>, composed with retry, timeout, createSingleFlight, createLimiter, createCircuitBreaker, settleAll, anySignal, sleep.
  • Every lifecycle is defineMachine({ id, initial, states, transitions, terminal }) run by createMachine — reactive state, can(event), invalid transitions return SF-CORE-010 instead of corrupting state.
  • Telemetry: createTelemetry({ defaultLevel, levels }) with levels off | critical | standard | verbose | debug per subsystem, spans, events and typed metric.* declarations.
  • Reactivity: box, derived, effect, watch, batch, untracked — MobX 7 behind a thin port.

AGENTS.md on GitHub: @staticflux/core · @staticflux/testkit