Skip to content

Documentation · ARDOR

Dependency Injection ​

ARDOR applications use IGNIS inversion of control in the browser to bind decorated classes and resolve them inside React components. The container manages services, repositories, and datasources as singletons across the tab session.

tsx
import React from 'react';
import { BaseService, service, useService } from '@venizia/ardor';

@service()
export class PriceCalculationService extends BaseService {
  constructor() {
    super({ scope: PriceCalculationService.name });
  }

  calculate(opts: { amount: number }): number {
    return opts.amount * 1.1;
  }
}

export function PriceDisplay(props: { amount: number }) {
  const priceService = useService({ target: PriceCalculationService });
  const total = priceService.calculate({ amount: props.amount });

  return <span>Total: {total}</span>;
}

The container is the application ​

BaseArdorApplication in packages/kernel/src/base/applications/abstract.ts extends Container from @venizia/ignis-inversion directly. The application instance is the container.

preConfigure() itself binds three keys: CoreBindings.APPLICATION_INSTANCE, CoreBindings.APPLICATION_INFO, and CoreBindings.FEATURES. The bind order inside preConfigure() is "least explicit first, so the most explicit wins: stereotype, features, bindingList(), bindContext()". Everything else is bound by the application through @provide methods, bindingList(), bindContext(), or stereotype discovery in registerArtifacts() (see Configuration).

Stereotype decorators and derived keys ​

ARDOR exports class stereotypes from @venizia/ardor (re-exported from packages/kernel/src/base/metadata/index.ts), among them @configuration(), which Configuration covers. These four derive a conventional binding key <namespace>.<ClassName> recorded in MetadataRegistry:

Stereotype decoratorNamespaceDefault derived key
@service()servicesservices.<ClassName>
@repository()repositoriesrepositories.<ClassName>
@datasource()datasourcesdatasources.<ClassName>
@component()componentscomponents.<ClassName>

When code resolves a dependency by passing { target: ClassName }, ARDOR reads the recorded key directly from metadata.

Build discovery: AutoArtifacts and Vite ​

In the browser, ARDOR discovers decorated classes at build time. In vite.config.ts, IgnisArtifactsVitePlugin.create() scans src/, bundles decorated classes into the virtual module @venizia/ignis-boot/auto, and pins class names against minification.

In src/application.ts, return AutoArtifacts from artifacts():

ts
import 'reflect-metadata';

import AutoArtifacts from '@venizia/ignis-boot/auto';

import { BaseArdorApplication, type IApplicationInfo, type IArtifactIndex } from '@venizia/ardor';

// The decorated classes live in their own files and this module imports none of them: the build lists
// every one under src/ in `AutoArtifacts`, and importing it here keeps the list out of any shared kit.
export class Application extends BaseArdorApplication {
  getAppInfo(): IApplicationInfo {
    return { name: 'quickstart', version: '0.0.0', description: 'ARDOR 5-minute quickstart' };
  }

  override artifacts(): Array<IArtifactIndex> {
    return [AutoArtifacts];
  }

  bindContext(): void {}
}

When application.start() runs, registerArtifacts() binds each discovered class as a singleton.

WARNING

Keep src/application.ts free of decorated classes. The module that imports AutoArtifacts must never declare decorated classes or be imported by modules that declare decorated classes, which creates an import cycle at startup.

To register classes without build discovery, use helper methods in bindContext():

ts
import { BaseArdorApplication, type IApplicationInfo } from '@venizia/ardor';

export class PricingService {
  getPrice(opts: { id: string }): number {
    return 100;
  }
}

export class ShopApplication extends BaseArdorApplication {
  getAppInfo(): IApplicationInfo {
    return { name: 'shop', version: '1.0.0', description: 'Shop admin' };
  }

  bindContext(): void {
    this.service(PricingService);
  }
}

The application helper methods bind under <namespace>.<Target.name> as a singleton: this.service(Target), this.repository(Target), this.dataSource(Target), and this.component(Target).

Resolving dependencies from React components ​

React components resolve dependencies through typed hooks exported from @venizia/ardor:

I want to resolveHook to useAccepted optionsWhat it checks
A repositoryuseRepository{ target: TClass<T> } or { key: TUseInjectableKeys }Throws when the class is bound under another namespace.
A serviceuseService{ target: TClass<T> } or { key: TUseInjectableKeys }Throws when the class is bound under another namespace.
A provideruseProvider{ target: TClass<T> } or { key: TUseInjectableKeys }Throws when the class is bound under another namespace.
A configurationuseConfiguration{ target: TClass<T> } or { key: TUseInjectableKeys }Throws when the class is bound under another namespace.
A componentuseComponent{ target: TClass<T> } or { key: TUseInjectableKeys }Throws when the class is bound under another namespace.
Any binding or core keyuseInjectable{ target: TClass<AnyType> } or { key: TUseInjectableKeys }Resolves any binding across all namespaces or CoreBindings.
The active DI containeruseApplicationContainer{ container?: Container } (optional)Returns opts.container else the context container and throws when neither exists.
The application containeruseApplicationContextNoneReturns the container and throws outside the application context.
The application loggeruseApplicationLoggerNoneReturns the application logger and throws outside the application context.

When resolving with { target }, TypeScript infers the return type from the class constructor without manual type casting.

Injecting dependencies into a class ​

Classes instantiated by the container declare dependencies in constructor parameters using @inject({ key }).

Every constructor parameter of a container-instantiated class must carry @inject. The check runs when the container instantiates the class (IGNIS instantiate(), which throws [ClassName] Constructor parameter N has no @inject | Every parameter of a container-instantiated class must be decorated - the container cannot supply an undecorated one), not during startup. Pass any class options to super({ scope: ClassName.name }), never as an undecorated parameter.

This rule has one exception: a @repository-decorated class whose first constructor parameter is its datasource needs no @inject, because the @repository decorator writes that injection metadata programmatically (registry.setInjectMetadata({ target, index: 0, ... })).

ts
import { BaseService, inject, service } from '@venizia/ardor';

@service()
export class AuditLogService extends BaseService {
  constructor() {
    super({ scope: AuditLogService.name });
  }

  record(opts: { action: string; resource: string }): void {
    this.logger.info('[audit] action: %s | resource: %s', opts.action, opts.resource);
  }
}

@service()
export class OrderProcessingService extends BaseService {
  constructor(
    @inject({ key: 'services.AuditLogService' })
    private readonly auditLogService: AuditLogService,
  ) {
    super({ scope: OrderProcessingService.name });
  }

  processOrder(opts: { orderId: string }): void {
    this.auditLogService.record({ action: 'process', resource: opts.orderId });
    this.logger.info('[order] processed order %s', opts.orderId);
  }
}

When OrderProcessingService is resolved, the container resolves services.AuditLogService first and passes the instance into the constructor.

Scopes: singleton by default in the browser ​

In ARDOR, all registrations default to BindingScopes.SINGLETON because hooks resolve on every render (packages/kernel/src/base/applications/abstract.ts). To opt out, pass { scope: BindingScopes.TRANSIENT } (from @venizia/ignis-inversion) when registering with @service({ scope: BindingScopes.TRANSIENT }) or this.service(Target, { scope: BindingScopes.TRANSIENT }) if an instance must hold mutable, caller-specific state.

Common startup errors and how to fix them ​

When configuring dependency injection and build discovery, you may encounter one of three startup errors from packages/kernel/src/base/applications/abstract.ts:

Missing artifact group content or resolver:

[artifacts] A group has neither content nor resolve | Return AutoArtifacts from @venizia/ignis-boot/auto, or build with IgnisArtifactsPlugin so the group arrives with its content

Return [AutoArtifacts] from artifacts() in src/application.ts and add IgnisArtifactsVitePlugin.create() to plugins in vite.config.ts.

Import cycle in application artifacts:

[artifacts] '<kind>' lists an entry that is not a class (position <position>) | Usually an import cycle: the module that imports the index (or @venizia/ignis-boot/auto) must not define decorated classes or be imported by them

Isolate each decorated class in its own file under src/services/, src/repositories/, or src/datasources/. Keep src/application.ts free of decorated classes, and do not import application.ts from any artifact module.

Class was never discovered:

[artifacts] '<kind>' lists <ClassName>, which was never discovered | Decorate it with its stereotype and regenerate the index

Add the stereotype decorator (@service(), @repository(), @datasource(), or @component()) to the class. If running vite dev, restart the development server so IgnisArtifactsVitePlugin detects the decorator.

  • Application - Application container lifecycle and startup
  • Configuration - Configuring providers with @configuration and @provide
  • Quickstart - End-to-end working example
  • Hooks - Complete reference for React hooks