Skip to content

@wirestate/core [monorepo] [docs]

npmlicense

Core package for Wirestate. Provides the DI container, service primitives, and event/command/query buses. React integration is in @wirestate/react, and Lit integration is in @wirestate/lit.

Installation

bash
npm install @wirestate/core reflect-metadata

Import reflect-metadata once at your application entry point, before any Wirestate imports:

ts
import "reflect-metadata";

Services

Services are plain classes decorated with @Injectable. Each service may inject a WireScope which provides access to the event, command, and query buses and to other services in the container.

@OnActivated and @OnDeactivation methods are invoked during the synchronous Inversify lifecycle. If they return a promise, Wirestate does not block container resolution or disposal. @OnProvision and @OnDeprovision methods are invoked by framework providers such as React and Lit when a container is attached to or detached from a UI subtree. Services that inject WireScope also participate in provider deprovision state tracking, even when they do not declare provider lifecycle hooks.

ts
import { Injectable, Inject, WireScope } from "@wirestate/core";

@Injectable()
export class CounterService {
  public count = 0;

  public constructor(@Inject(WireScope) private scope: WireScope) {}

  public increment(): void {
    this.count++;
  }
}

Container

ts
import { createContainer, bindService } from "@wirestate/core";

const container = createContainer({
  seed: { baseUrl: "https://example.com" },
  entries: [CounterService],
});

bindService(container, AnotherService);

const counterService = container.get(CounterService);
const anotherService = container.get(AnotherService);

bindService binds a class in singleton scope by default. Use bindConstant to bind a value, bindEntry to bind under a custom token.

Events

Events are fire-and-forget messages. Any service can emit or subscribe.

ts
import { OnEvent, WireScope, Inject } from "@wirestate/core";

@Injectable()
export class SenderService {
  public constructor(@Inject(WireScope) private scope: WireScope) {}

  public notify(): void {
    this.scope.emitEvent("USER_LOGGED_OUT");
  }
}

@Injectable()
export class ReceiverService {
  @OnEvent("USER_LOGGED_OUT")
  public onLogout(): void {
    // handle logout
  }
}

@OnEvent() with no argument subscribes to all events.

Commands

Commands are write operations dispatched by token. A single handler is expected per command type.

ts
import { OnCommand, WireScope, Inject } from "@wirestate/core";

@Injectable()
export class AuthService {
  @OnCommand("LOGIN")
  public async onLogin(payload: { username: string }): Promise<void> {
    // perform login
  }
}

@Injectable()
export class AnotherService {
  public constructor(@Inject(WireScope) private scope: WireScope) {}

  public async login(): Promise<void> {
    await this.scope.executeCommand("LOGIN").task;
  }
}

Use executeOptionalCommand from WireScope or CommandBus.commandOptional when a handler may not be registered; both return null instead of throwing.

Queries

Queries are request-response operations. A single handler is expected per query type.

ts
import { OnQuery, WireScope, Inject } from "@wirestate/core";

@Injectable()
export class StoreService {
  private items: Array<string> = [];

  @OnQuery("STORE_ITEMS")
  public onGetItems(): Array<string> {
    return this.items;
  }
}

@Injectable()
export class AnotherService {
  public constructor(@Inject(WireScope) private scope: WireScope) {}

  public async someActionRequiringItems(): Promise<void> {
    const syncItems: Array<string> = this.scope.queryData("STORE_ITEMS");
    const asyncItems: Array<string> = await this.scope.queryDataAsync("STORE_ITEMS");
  }
}

Seeds

Seeds pass initial data to services when they are activated.

ts
import { SEED, Injectable, Inject } from "@wirestate/core";

// Shared seed - same object injected into all services in the tree:
@Injectable()
export class MyService {
  public constructor(@Inject(SEED) private seed: { theme: string }) {}
}

// Per-service seed - each service gets its own seed value:
@Injectable()
export class OtherService {
  public constructor(@Inject(WireScope) scope: WireScope) {
    const { count } = scope.getSeed(OtherService) as { count: number };
  }
}

Seeds are applied via applySeeds / applySharedSeed and removed via unapplySeeds. For managed React containers, pass seed or seeds inside ContainerProvider config. For external containers, pass seeds to createContainer or apply them before services are activated.

Lifecycle

ts
import { OnActivated, OnDeactivation, OnDeprovision, OnProvision } from "@wirestate/core";

@Injectable()
export class PollingService {
  private timer?: ReturnType<typeof setInterval>;
  private ubsubscribe?: () => void;

  @OnActivated()
  public onActivated(): void {
    this.timer = setInterval(() => console.info("interval execution"), 5000);
  }

  @OnDeactivation()
  public onDeactivation(): void {
    clearInterval(this.timer);
  }

  @OnProvision()
  public onProvision(): void {
    this.ubsubscribe = connectToProviderScopedResource();
  }

  @OnDeprovision()
  public onDeprovision(): void {
    this.ubsubscribe?.();
    this.ubsubscribe = undefined;
  }
}

@OnActivated runs after the service is bound and all dependencies are resolved. @OnDeactivation runs when the container scope is disposed. @OnProvision runs when a React or Lit provider exposes the container to a subtree. @OnDeprovision runs before that provider removes or replaces the container; external containers are not disposed by the provider.

Injected WireScope instances expose lifecycle state for async guards:

  • scope.isDisposed becomes true after service deactivation.
  • scope.isDeprovisioned is null before provider provisioning reaches the service, false while it is provider-owned, and true after provider deprovision.
  • scope.isInactive is true when either disposal or deprovision ended the service's usable lifecycle.

WireScope API

WireScope is injected per-service and exposes:

MemberDescription
isDisposedtrue after service deactivation
isDeprovisionednull before provider provisioning, false while owned, true after provider deprovision
isInactivetrue when isDisposed or isDeprovisioned === true
getContainer()Access the raw IoC container
resolve(token)Resolve a service or value by token
resolveOptional(token)Resolve a service or value, returns null if not bound
getSeed(token?)Get the per-service or shared seed
emitEvent(type, payload?, from?)Emit an event
subscribeToEvent(handler)Subscribe a handler to all events; returns unsubscribe function
unsubscribeFromEvent(handler)Remove a specific event subscription by handler reference
queryData(type, data?)Dispatch a synchronous query and return the result
queryDataAsync(type, data?)Dispatch a query and return the result as a promise
queryOptionalData(type, data?)Dispatch a synchronous query; returns null if no handler is registered
queryOptionalDataAsync(type, data?)Dispatch a query as a promise; returns null if no handler is registered
registerQueryHandler(type, handler)Register a query handler; returns unregister function
unregisterQueryHandler(type, handler)Remove a specific query handler by type and reference
executeCommand(type, data?)Dispatch a command and return a descriptor
executeOptionalCommand(type, data?)Dispatch a command; returns null if no handler is registered
registerCommandHandler(type, handler)Register a command handler; returns unregister function
unregisterCommandHandler(type, handler)Remove a specific command handler by type and reference

Test utilities

Available via @wirestate/core/test-utils:

ts
import {
  mockContainer,
  mockService,
  mockBindService,
  mockBindEntry,
  mockUnbindService,
} from "@wirestate/core/test-utils";

mockContainer(options?)

Creates a configured IoC container for testing. Accepts an optional object:

OptionTypeDescription
entriesArray<Newable | InjectableDescriptor>Services or descriptors to bind
activateboolean | Array<ServiceIdentifier>true to resolve all entries, or specific tokens to resolve immediately after binding
skipLifecyclebooleanSkip @OnActivated / @OnDeactivation hooks
ts
const container = mockContainer({
  entries: [CounterService, LoggerService],
  activate: [CounterService],
});
ts
const container = mockContainer({
  entries: [CounterService, LoggerService],
  activate: true,
});

mockService(ServiceClass, container?, options?)

Binds a service class to a container and returns its instance. Creates a new mockContainer if none is provided.

ts
const counter = mockService(CounterService);
counter.increment();
expect(counter.count).toBe(1);

mockBindService(container, ServiceClass, options?)

Binds a service class to an existing container. Accepts { skipLifecycle?: boolean }.

mockBindEntry(container, entry, options?)

Binds a service class or InjectableDescriptor to an existing container. Accepts { skipLifecycle?: boolean }.

mockUnbindService(container, ServiceClass)

Removes a service binding from the container. Useful for overriding registrations between tests.

ts
mockUnbindService(container, CounterService);
mockBindEntry(container, { id: CounterService, value: fakeCounter });

License

MIT