Skip to content

PluginServiceRegistry

PluginServiceRegistry is a type-safe service registry based on ServiceToken, designed for sharing services between plugins.

  1. Type Safety - Uses ServiceToken to carry type information
  2. Explicit Dependencies - Express dependencies clearly by importing tokens
  3. Optional Dependencies - get returns undefined, require throws
  4. Single Responsibility - Only handles registration and lookup, not lifecycle
  5. Who Defines the Interface, Exports the Token - Each module defines its own interfaces and tokens

Service tokens are used for type-safe service registration and retrieval:

import { createServiceToken, ServiceToken } from '@esengine/ecs-framework';
// Define interface
interface IAssetManager {
load(path: string): Promise<any>;
unload(path: string): void;
}
// Create service token
const AssetManagerToken = createServiceToken<IAssetManager>('assetManager');
  • Cross-package Type Safety: TypeScript preserves generic type information across packages
  • Globally Unique: Uses Symbol.for() to ensure same-named tokens reference the same Symbol
  • Explicit Dependencies: Clearly express inter-module dependencies by importing tokens
import { PluginServiceRegistry, createServiceToken } from '@esengine/ecs-framework';
// Create registry
const registry = new PluginServiceRegistry();
// Define token
interface ILogger {
log(message: string): void;
}
const LoggerToken = createServiceToken<ILogger>('logger');
// Register service
const logger: ILogger = {
log: (msg) => console.log(msg)
};
registry.register(LoggerToken, logger);
// Optional get (returns undefined if not exists)
const logger = registry.get(LoggerToken);
if (logger) {
logger.log('Hello');
}
// Required get (throws if not exists)
try {
const logger = registry.require(LoggerToken);
logger.log('Hello');
} catch (e) {
console.error('Logger not registered');
}
// Check if registered
if (registry.has(LoggerToken)) {
// ...
}
// Unregister service
registry.unregister(LoggerToken);
// Clear all services
registry.clear();

Each module should define its interfaces and tokens in tokens.ts:

packages/asset-system/src/tokens.ts
import { createServiceToken } from '@esengine/ecs-framework';
export interface IAssetManager {
load<T>(path: string): Promise<T>;
unload(path: string): void;
getCache(path: string): any | undefined;
}
export const AssetManagerToken = createServiceToken<IAssetManager>('assetManager');
packages/asset-system/src/AssetSystemPlugin.ts
import { Core } from '@esengine/ecs-framework';
import { AssetManagerToken, IAssetManager } from './tokens';
import { AssetManager } from './AssetManager';
export function installAssetSystem() {
const assetManager = new AssetManager();
// Register to Core's plugin service registry
Core.pluginServices.register(AssetManagerToken, assetManager);
}
packages/sprite/src/SpriteSystem.ts
import { Core } from '@esengine/ecs-framework';
import { AssetManagerToken, IAssetManager } from '@esengine/asset-system';
class SpriteSystem extends EntitySystem {
private assetManager!: IAssetManager;
onInitialize(): void {
// Get from plugin service registry
this.assetManager = Core.pluginServices.require(AssetManagerToken);
}
async loadSprite(path: string) {
const texture = await this.assetManager.load<Texture>(path);
// ...
}
}
FeatureServiceContainerPluginServiceRegistry
PurposeGeneral DICross-plugin service sharing
IdentifierClass or SymbolServiceToken
LifecycleSingleton/TransientNone (caller managed)
Decorator Support@Injectable, @InjectPropertyNone
Type SafetyRequires generic assertionToken carries type
function createServiceToken<T>(name: string): ServiceToken<T>

Creates a service token. Uses Symbol.for() to ensure cross-package sharing.

MethodDescription
register<T>(token, service)Register a service
get<T>(token): T | undefinedGet service (optional)
require<T>(token): TGet service (required, throws if missing)
has<T>(token): booleanCheck if registered
unregister<T>(token): booleanUnregister service
clear()Clear all services
dispose()Dispose resources