Complete API documentation for Skeleton Crew Runtime v0.3.2 including all TypeScript interfaces, classes, methods, and types with full generic support.
- Service Locator (v0.3.1) - Type-safe inter-plugin communication via
ctx.services - Config Validation (v0.3.0) - Validate plugin configuration with detailed error reporting
- Introspection (v0.3.0) - Enhanced runtime metadata and discovery API
- Plugin Discovery (v0.2.1) - Automatic plugin loading from file paths and npm packages
- Dependency Resolution (v0.2.1) - Automatic topological sorting for plugin initialization
- Generic Runtime/Context - Full TypeScript generic support for type-safe configuration
- Sync Config Access - Direct synchronous access to configuration via
ctx.config
The main orchestrator that coordinates all subsystems. Handles initialization, shutdown, and lifecycle state tracking. v0.2.0 adds full generic support for type-safe configuration.
constructor<TConfig = Record<string, unknown>>(options?: RuntimeOptions<TConfig>)Creates a new Runtime instance with optional configuration.
Type Parameters:
TConfig: Configuration object type (defaults toRecord<string, unknown>)
Parameters:
options(optional): Runtime configuration optionsoptions.logger(optional): Custom logger implementation (defaults toConsoleLogger)options.hostContext(optional): Host application services to inject (defaults to empty object)options.config(optional): [NEW v0.2.0] Runtime configuration objectoptions.enablePerformanceMonitoring(optional): Enable performance monitoringoptions.pluginPaths(optional): [NEW v0.2.1] Paths to plugin files or directoriesoptions.pluginPackages(optional): [NEW v0.2.1] npm package names to load as plugins
Example:
import { Runtime, ConsoleLogger } from "skeleton-crew";
// v0.2.0: Define your config interface
interface MyAppConfig {
apiUrl: string;
apiKey: string;
features: {
analytics: boolean;
debugging: boolean;
};
}
// v0.2.0: Create typed runtime
const runtime = new Runtime<MyAppConfig>({
config: {
apiUrl: 'https://api.example.com',
apiKey: process.env.API_KEY!,
features: {
analytics: true,
debugging: process.env.NODE_ENV === 'development'
}
},
logger: new ConsoleLogger()
});
// Legacy usage still works (backward compatible)
const legacyRuntime = new Runtime({
hostContext: {
db: databaseConnection,
logger: applicationLogger
}
});Registers a plugin before initialization. Plugins registered this way will have their setup callbacks executed during initialize(). v0.2.0 adds dependency resolution.
Parameters:
plugin: The plugin definition to register (now with optional dependencies)
Throws:
Errorif runtime is already initializedErrorif plugin dependencies cannot be resolved
Example:
// v0.2.0: Plugin with dependencies
runtime.registerPlugin({
name: "data-plugin",
version: "1.0.0",
dependencies: ['config', 'logger'], // Will initialize after these plugins
setup(ctx: RuntimeContext<MyAppConfig>) {
// ctx.config is fully typed!
const { apiUrl, apiKey } = ctx.config;
ctx.logger.info(`Data plugin connecting to ${apiUrl}`);
}
});
// Legacy plugin still works
runtime.registerPlugin({
name: "legacy-plugin",
version: "1.0.0",
setup(ctx) {
// Works without types
}
});Initializes the runtime following the strict initialization sequence. Creates all subsystems in order, then executes plugin setup callbacks. Emits runtime:initialized event after successful initialization.
Throws:
Errorif initialize is called twiceErrorif any plugin setup fails
Initialization Sequence:
- Create PluginRegistry
- Create ScreenRegistry
- Create ActionEngine
- Create EventBus
- Create UIBridge
- Create RuntimeContext
- Execute plugin setup callbacks
Example:
await runtime.initialize();Shuts down the runtime following the strict shutdown sequence. Emits runtime:shutdown event at start of shutdown. Disposes initialized plugins, shuts down UI provider, clears all registries, and releases resources. Safe to call multiple times (idempotent).
Shutdown Sequence:
- Execute dispose callbacks for initialized plugins (in reverse order)
- Shutdown UI provider
- Clear all registries
- Set initialized flag to false
Example:
await runtime.shutdown();Returns the RuntimeContext for this runtime instance. v0.2.0 returns fully typed context.
Returns: The RuntimeContext with full type information
Throws:
Errorif runtime is not initialized
Example:
const ctx = runtime.getContext(); // Fully typed in v0.2.0
const config = ctx.config; // Type-safe access to configuration[NEW v0.2.0] Returns the runtime configuration object.
Returns: Readonly configuration object
Example:
const config = runtime.getConfig();
// config is typed as Readonly<MyAppConfig>Returns whether the runtime has been initialized.
Returns: true if runtime is initialized, false otherwise
Example:
if (runtime.isInitialized()) {
// Runtime is ready
}Returns the current lifecycle state of the runtime.
Returns: The current RuntimeState enum value
Example:
const state = runtime.getState();
// state can be: Uninitialized, Initializing, Initialized, ShuttingDown, or ShutdownRegisters a UI provider with the runtime. Can be called after initialization completes.
Parameters:
provider: The UI provider implementation
Throws:
Errorif provider is invalid or already registered
Example:
runtime.setUIProvider({
mount(target, ctx) {
// Initialize UI framework
},
renderScreen(screen) {
// Render screen
return renderedOutput;
}
});Returns the registered UI provider.
Returns: The registered UIProvider or null if none registered
Example:
const provider = runtime.getUIProvider();Renders a screen by looking it up in the ScreenRegistry and delegating to UIBridge.
Parameters:
screenId: The screen identifier to render
Returns: The result from the UI provider's render method
Throws:
Errorif screen is not foundErrorif no UI provider is registered
Example:
const result = runtime.renderScreen("home");Provides a safe API facade for subsystems. Passed to plugins and action handlers without exposing internal mutable structures.
constructor(
screenRegistry: ScreenRegistry,
actionEngine: ActionEngine,
pluginRegistry: PluginRegistry,
eventBus: EventBus,
runtime: Runtime
)Note: This is typically created internally by the Runtime. You don't need to instantiate it directly.
Exposes Screen Registry operations.
Methods:
registerScreen(screen: ScreenDefinition): () => void- Registers a screen and returns an unregister functiongetScreen(id: string): ScreenDefinition | null- Retrieves a screen by IDgetAllScreens(): ScreenDefinition[]- Returns all registered screens
Example:
const unregister = ctx.screens.registerScreen({
id: "home",
title: "Home",
component: HomeComponent
});
const screen = ctx.screens.getScreen("home");
const allScreens = ctx.screens.getAllScreens();
// Unregister when done
unregister();Exposes Action Engine operations.
Methods:
registerAction<P, R>(action: ActionDefinition<P, R>): () => void- Registers an action and returns an unregister functionrunAction<P, R>(id: string, params?: P): Promise<R>- Executes an action by ID
Example:
const unregister = ctx.actions.registerAction({
id: "loadUsers",
handler: async () => {
return await fetchUsers();
}
});
const users = await ctx.actions.runAction("loadUsers");
// Unregister when done
unregister();Exposes Plugin Registry operations.
Methods:
registerPlugin(plugin: PluginDefinition): void- Registers a plugingetPlugin(name: string): PluginDefinition | null- Retrieves a plugin by namegetAllPlugins(): PluginDefinition[]- Returns all registered pluginsgetInitializedPlugins(): string[]- Returns names of initialized plugins
Example:
ctx.plugins.registerPlugin({
name: "my-plugin",
version: "1.0.0",
setup(ctx) {
// Setup logic
}
});
const plugin = ctx.plugins.getPlugin("my-plugin");
const allPlugins = ctx.plugins.getAllPlugins();
const initialized = ctx.plugins.getInitializedPlugins();Exposes Event Bus operations.
Methods:
emit(event: string, data?: unknown): void- Emits an event synchronouslyemitAsync(event: string, data?: unknown): Promise<void>- Emits an event asynchronouslyon(event: string, handler: (data: unknown) => void): () => void- Registers an event handler and returns an unsubscribe function
Example:
// Subscribe to events
const unsubscribe = ctx.events.on("user:created", (data) => {
console.log("User created:", data);
});
// Emit events
ctx.events.emit("user:created", { id: 123, name: "Alice" });
// Async emit
await ctx.events.emitAsync("user:created", { id: 124, name: "Bob" });
// Unsubscribe when done
unsubscribe();Exposes the host context as a readonly object. Returns a frozen copy of the host context provided during runtime initialization.
Type: Readonly<Record<string, unknown>>
Example:
// Access injected services
const db = ctx.host.db;
const logger = ctx.host.logger;
const config = ctx.host.config;
// Attempt to mutate throws TypeError
ctx.host.newKey = 'value'; // ❌ TypeError: Cannot add propertyUse Cases:
- Accessing legacy application services from plugins
- Reading configuration injected by host application
- Using existing database connections, HTTP clients, etc.
Security: The returned object is frozen to prevent mutation. Plugins receive a shallow copy, ensuring isolation between plugins.
[NEW v0.3.1]
Exposes the Service Locator API for type-safe inter-plugin communication.
Methods:
register<T>(name: string, service: T): void- Registers a serviceget<T>(name: string): T- Retrieves a service (throws if not found)has(name: string): boolean- Checks if a service existslist(): string[]- Lists all registered services
Example:
// Register a service
ctx.services.register('my-api', {
doSomething: () => console.log('Hello')
});
// Consume a service
if (ctx.services.has('my-api')) {
const api = ctx.services.get<MyApi>('my-api');
api.doSomething();
}Exposes the introspection API for querying runtime metadata. v0.3.0 adds enhanced metadata including getMetadata() override.
Type: IntrospectionAPI
Example:
// List all registered resources
const actions = ctx.introspect.listActions();
const plugins = ctx.introspect.listPlugins();
const screens = ctx.introspect.listScreens();
// Get specific metadata
const actionMeta = ctx.introspect.getActionDefinition('users:load');
const pluginMeta = ctx.introspect.getPluginDefinition('users');
const screenMeta = ctx.introspect.getScreenDefinition('users:list');
// Get runtime statistics
const stats = ctx.introspect.getMetadata();
console.log(`Runtime v${stats.runtimeVersion} with ${stats.totalPlugins} plugins`);Use Cases:
- Building admin dashboards
- Development and debugging tools
- Dynamic UI generation based on available resources
- Runtime monitoring and health checks
Security: All returned metadata is deeply frozen to prevent mutation of internal state.
Returns the Runtime instance.
Returns: The Runtime instance
Example:
const runtime = ctx.getRuntime();Manages plugin registration and lifecycle.
constructor(logger: Logger)Note: Typically created internally by the Runtime.
Registers a plugin definition.
Parameters:
plugin: The plugin definition to register
Throws:
ValidationErrorif plugin is missing required fields (name, version, setup)DuplicateRegistrationErrorif a plugin with the same name is already registered
Example:
pluginRegistry.registerPlugin({
name: "my-plugin",
version: "1.0.0",
setup(ctx) {
// Setup logic
}
});Retrieves a plugin definition by name.
Parameters:
name: The plugin name
Returns: The plugin definition or null if not found
Retrieves all registered plugin definitions.
Returns: Array copy of all registered plugins
Returns the names of all successfully initialized plugins in initialization order.
Returns: Array of initialized plugin names
Executes plugin setup callbacks sequentially in registration order. Aborts on first failure and rolls back already-initialized plugins.
Parameters:
context: The RuntimeContext to pass to setup callbacks
Throws:
Errorif any plugin setup fails
Note: This is called internally by Runtime during initialization.
Executes plugin dispose callbacks in reverse order of initialization. Logs errors but continues cleanup.
Parameters:
context: The RuntimeContext to pass to dispose callbacks
Note: This is called internally by Runtime during shutdown.
Clears all registered plugins. Used during shutdown.
Manages screen definitions with O(1) lookup performance.
constructor(logger: Logger)Note: Typically created internally by the Runtime.
Registers a screen definition. Validates required fields and rejects duplicate IDs.
Parameters:
screen: The screen definition to register
Returns: An unregister function that removes the screen when called
Throws:
ValidationErrorif screen is missing required fields (id, title, component)DuplicateRegistrationErrorif a screen with the same ID is already registered
Example:
const unregister = screenRegistry.registerScreen({
id: "home",
title: "Home",
component: "HomeComponent"
});
// Later, unregister
unregister();Retrieves a screen definition by ID.
Parameters:
id: The screen identifier
Returns: The screen definition or null if not found
Retrieves all registered screen definitions.
Returns: Array copy of all registered screens
Clears all registered screens. Used during shutdown.
Manages action registration and execution with O(1) lookup performance.
constructor(logger: Logger)Note: Typically created internally by the Runtime.
Sets the RuntimeContext for this ActionEngine. Must be called after RuntimeContext is created during initialization.
Parameters:
context: The RuntimeContext to pass to action handlers
Note: This is called internally by Runtime during initialization.
Registers an action definition. Rejects duplicate action IDs.
Type Parameters:
P: Parameter type (defaults tounknown)R: Return type (defaults tounknown)
Parameters:
action: The action definition to register
Returns: An unregister function that removes the action when called
Throws:
ValidationErrorif required fields are missing or invalidDuplicateRegistrationErrorif an action with the same ID is already registered
Example:
const unregister = actionEngine.registerAction({
id: "loadUsers",
handler: async (params, ctx) => {
return await fetchUsers();
},
timeout: 5000 // Optional timeout in milliseconds
});
// Later, unregister
unregister();Executes an action by ID with optional parameters. Passes the RuntimeContext to the action handler. Handles both synchronous and asynchronous handlers. Enforces timeout if specified.
Type Parameters:
P: Parameter type (defaults tounknown)R: Return type (defaults tounknown)
Parameters:
id: The action identifierparams: Optional parameters to pass to the action handler
Returns: The result from the action handler
Throws:
Errorif the action ID does not existActionExecutionErrorif the handler throws an errorActionTimeoutErrorif the action exceeds its timeout
Example:
const result = await actionEngine.runAction("loadUsers");
const result2 = await actionEngine.runAction("createUser", { name: "Alice" });Retrieves an action definition by ID. For internal use.
Parameters:
id: The action identifier
Returns: The action definition or null if not found
Retrieves all registered action definitions.
Returns: Array copy of all registered actions
Clears all registered actions. Used during shutdown.
Provides publish-subscribe event communication with O(1) lookup performance.
constructor(logger: Logger)Note: Typically created internally by the Runtime.
Emits an event to all registered handlers synchronously. Handlers are invoked in registration order. Handler errors are caught, logged, and do not prevent other handlers from executing.
Parameters:
event: The event namedata: Optional data to pass to handlers
Example:
eventBus.emit("user:created", { id: 123, name: "Alice" });Emits an event to all registered handlers asynchronously. Returns a Promise that resolves when all handlers complete or fail. Uses Promise.allSettled to ensure all handlers are invoked even if some fail.
Parameters:
event: The event namedata: Optional data to pass to handlers
Returns: Promise that resolves when all handlers complete
Example:
await eventBus.emitAsync("user:created", { id: 123, name: "Alice" });Registers an event handler for a specific event.
Parameters:
event: The event namehandler: The handler function to invoke when the event is emitted
Returns: An unsubscribe function that removes the handler when called
Example:
const unsubscribe = eventBus.on("user:created", (data) => {
console.log("User created:", data);
});
// Later, unsubscribe
unsubscribe();Clears all registered event handlers. Used during shutdown.
Manages optional UI provider registration and screen rendering.
[NEW v0.2.1] Handles automatic plugin discovery and loading from file paths and npm packages.
constructor(logger: Logger)Creates a new DirectoryPluginLoader instance with the specified logger.
async loadPlugins(
pluginPaths?: string[],
pluginPackages?: string[]
): Promise<PluginDefinition[]>Loads plugins from specified paths and packages.
Parameters:
pluginPaths(optional): Array of file paths or directory paths to scan for pluginspluginPackages(optional): Array of npm package names to load as plugins
Returns: Promise resolving to array of loaded plugin definitions
Example:
import { DirectoryPluginLoader, ConsoleLogger } from 'skeleton-crew';
const loader = new DirectoryPluginLoader(new ConsoleLogger());
// Load from paths and packages
const plugins = await loader.loadPlugins(
['./plugins', './custom/auth-plugin.js'],
['@my-org/plugin-auth', 'shared-utils-plugin']
);
// Register loaded plugins
for (const plugin of plugins) {
runtime.registerPlugin(plugin);
}Plugin Discovery Rules:
- File Extensions: Recognizes
.js,.mjs, and.tsfiles as potential plugins - Directory Scanning: Recursively scans directories for plugin files
- Exclusions: Ignores
node_modules,dist, test files (*.test.*,*.spec.*) - Export Patterns: Looks for plugins in
default,plugin, or module root exports - Validation: Validates plugin structure (name, version, setup function required)
- Error Handling: Logs errors but continues loading other plugins
constructor(logger: Logger)Note: Typically created internally by the Runtime.
Registers a UI provider with the runtime.
Parameters:
provider: The UI provider implementation
Throws:
ValidationErrorif provider is missing required methods (mount, renderScreen)DuplicateRegistrationErrorif provider is already registered
Example:
uiBridge.setProvider({
mount(target, ctx) {
// Initialize UI
},
renderScreen(screen) {
// Render screen
return output;
}
});Returns the registered UI provider.
Returns: The registered UIProvider or null if none registered
Renders a screen using the registered UI provider.
Parameters:
screen: The screen definition to render
Returns: The result from the UI provider's renderScreen method
Throws:
Errorif no UI provider is registered
Shuts down the UI provider by calling unmount if it exists.
Note: This is called internally by Runtime during shutdown.
Clears the UI provider. Used during shutdown.
Defines a plugin that can extend the runtime.
interface PluginDefinition<TConfig = Record<string, unknown>> {
name: string;
version: string;
dependencies?: string[];
configKeys?: string[];
validateConfig?: (config: TConfig) => boolean | ConfigValidationResult | Promise<boolean | ConfigValidationResult>;
setup: (context: RuntimeContext<TConfig>) => void | Promise<void>;
dispose?: (context: RuntimeContext<TConfig>) => void | Promise<void>;
}Properties:
name(required): Unique plugin identifierversion(required): Plugin version stringdependencies(optional): [NEW v0.2.1] Array of plugin names this plugin depends on (auto-sorted)configKeys(optional): [NEW v0.3.0] Array of config keys this plugin uses (for documentation)validateConfig(optional): [NEW v0.3.0] Function to validate configuration before setupsetup(required): Setup callback executed during initializationdispose(optional): Cleanup callback executed during shutdown
Example:
const myPlugin: PluginDefinition<MyConfig> = {
name: "my-plugin",
version: "1.0.0",
dependencies: ["auth"], // Wait for auth plugin
// v0.3.0: Validate config
validateConfig(config) {
if (!config.apiKey) {
return { valid: false, errors: ["Missing API Key"] };
}
return true;
},
setup(ctx) {
ctx.screens.registerScreen({
id: "my-screen",
title: "My Screen",
component: "MyComponent"
});
},
dispose(ctx) {
// Cleanup logic
}
};Defines a screen that can be rendered by a UI provider.
interface ScreenDefinition {
id: string;
title: string;
component: string;
}Properties:
id(required): Unique screen identifiertitle(required): Human-readable screen titlecomponent(required): Component reference (string or any type)
Example:
const homeScreen: ScreenDefinition = {
id: "home",
title: "Home",
component: "HomeComponent"
};Defines an action that can be executed by the runtime.
interface ActionDefinition<P = unknown, R = unknown> {
id: string;
handler: (params: P, context: RuntimeContext) => Promise<R> | R;
timeout?: number;
}Type Parameters:
P: Parameter type (defaults tounknown)R: Return type (defaults tounknown)
Properties:
id(required): Unique action identifierhandler(required): Action handler function (sync or async)timeout(optional): Timeout in milliseconds
Example:
const loadUsersAction: ActionDefinition<void, User[]> = {
id: "loadUsers",
handler: async (params, ctx) => {
return await fetchUsers();
},
timeout: 5000
};
const createUserAction: ActionDefinition<{ name: string }, User> = {
id: "createUser",
handler: async (params, ctx) => {
return await createUser(params.name);
}
};Defines a UI provider that can render screens.
interface UIProvider {
mount(target: unknown, context: RuntimeContext): void | Promise<void>;
renderScreen(screen: ScreenDefinition): unknown | Promise<unknown>;
unmount?(): void | Promise<void>;
}Properties:
mount(required): Initialize the UI frameworkrenderScreen(required): Render a screenunmount(optional): Cleanup callback during shutdown
Example:
const reactUIProvider: UIProvider = {
mount(target, ctx) {
// Initialize React
},
renderScreen(screen) {
// Render React component
return <Component />;
},
unmount() {
// Cleanup React
}
};Provides a safe API facade for subsystems. Passed to plugins and action handlers.
interface RuntimeContext {
screens: {
registerScreen(screen: ScreenDefinition): () => void;
getScreen(id: string): ScreenDefinition | null;
getAllScreens(): ScreenDefinition[];
};
actions: {
registerAction<P = unknown, R = unknown>(action: ActionDefinition<P, R>): () => void;
runAction<P = unknown, R = unknown>(id: string, params?: P): Promise<R>;
};
plugins: {
registerPlugin(plugin: PluginDefinition): void;
getPlugin(name: string): PluginDefinition | null;
getAllPlugins(): PluginDefinition[];
getInitializedPlugins(): string[];
};
events: {
emit(event: string, data?: unknown): void;
emitAsync(event: string, data?: unknown): Promise<void>;
on(event: string, handler: (data: unknown) => void): () => void;
};
readonly host: Readonly<Record<string, unknown>>;
readonly introspect: IntrospectionAPI;
getRuntime(): Runtime;
}See RuntimeContextImpl for detailed method documentation.
Interface for pluggable logging implementations.
interface Logger {
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
}Methods:
debug: Log debug messagesinfo: Log informational messageswarn: Log warning messageserror: Log error messages
Example:
class CustomLogger implements Logger {
debug(message: string, ...args: unknown[]): void {
console.debug(`[DEBUG] ${message}`, ...args);
}
info(message: string, ...args: unknown[]): void {
console.info(`[INFO] ${message}`, ...args);
}
warn(message: string, ...args: unknown[]): void {
console.warn(`[WARN] ${message}`, ...args);
}
error(message: string, ...args: unknown[]): void {
console.error(`[ERROR] ${message}`, ...args);
}
}Configuration options for Runtime initialization.
interface RuntimeOptions<TConfig = Record<string, unknown>> {
logger?: Logger;
hostContext?: Record<string, unknown>;
config?: TConfig;
enablePerformanceMonitoring?: boolean;
// Plugin Discovery Options (v0.2.1)
pluginPaths?: string[];
pluginPackages?: string[];
}Properties:
logger(optional): Custom logger implementation (defaults toConsoleLogger)hostContext(optional): Host application services to inject into the runtime (defaults to empty object)config(optional): [NEW v0.2.0] Runtime configuration object for type-safe accessenablePerformanceMonitoring(optional): Enable performance monitoring and metrics collectionpluginPaths(optional): [NEW v0.2.1] Array of file paths or directories to load plugins frompluginPackages(optional): [NEW v0.2.1] Array of npm package names to load as plugins
Host Context Usage:
The hostContext option enables legacy applications to inject existing services into the runtime, allowing plugins to access these services without tight coupling. This is particularly useful for incremental migration scenarios.
Plugin Discovery Options (v0.2.1):
The pluginPaths and pluginPackages options enable automatic plugin discovery and loading:
pluginPaths: Specify file paths or directories containing plugin files. The runtime will automatically load and register plugins from these locations.pluginPackages: Specify npm package names to load as plugins. Useful for loading published plugin packages.
Plugin Discovery Examples:
// Load plugins from directories and files
const runtime = new Runtime({
pluginPaths: [
'./plugins', // Load all plugins from directory
'./custom-plugins/auth.js', // Load specific plugin file
'../shared/plugins' // Load from relative path
]
});
// Load plugins from npm packages
const runtime = new Runtime({
pluginPackages: [
'@my-org/plugin-auth', // Scoped package
'my-custom-plugin', // Regular package
'@company/shared-plugins' // Organization package
]
});
// Combine both approaches
const runtime = new Runtime({
pluginPaths: ['./local-plugins'],
pluginPackages: ['@my-org/plugin-auth'],
config: { apiUrl: 'https://api.example.com' }
});
await runtime.initialize(); // Plugins loaded automaticallyPlugin Discovery Behavior:
- File Discovery: For
pluginPaths, the runtime scans for.js,.mjs, and.tsfiles - Package Loading: For
pluginPackages, the runtime uses dynamic imports to load npm packages - Validation: All discovered plugins are validated before registration
- Error Handling: Invalid plugins are logged but don't stop the initialization process
- Load Order: Discovered plugins are loaded before manually registered plugins
pluginPackages: Specify npm package names that export plugins. The runtime will dynamically import and register these packages as plugins.
Example:
// Inject existing services for plugins to use
const runtime = new Runtime({
hostContext: {
db: legacyApp.database,
logger: legacyApp.logger,
cache: legacyApp.cacheService,
config: {
apiKey: process.env.API_KEY,
apiUrl: 'https://api.example.com'
}
},
// v0.2.1: Automatic plugin discovery
pluginPaths: [
'./plugins/core-plugin.js',
'./plugins/custom/', // Load all plugins from directory
'/absolute/path/to/plugin.js'
],
pluginPackages: [
'@myorg/analytics-plugin',
'skeleton-crew-ui-plugin',
'my-custom-plugin-package'
],
// v0.2.0: Type-safe configuration
config: {
apiUrl: 'https://api.example.com',
features: {
analytics: true,
debugging: process.env.NODE_ENV === 'development'
}
},
enablePerformanceMonitoring: true
});
await runtime.initialize();
// Plugins can now access these services via context.host
const myPlugin = {
name: 'data-plugin',
version: '1.0.0',
setup(context) {
const db = context.host.db;
const config = context.host.config;
context.actions.registerAction({
id: 'data:load',
handler: async () => {
return await db.query('SELECT * FROM users');
}
});
}
};Validation Warnings:
The runtime validates host context and logs warnings for common issues:
- Objects larger than 1MB (may impact performance)
- Function values (should be wrapped in objects)
These are warnings only - initialization continues normally.
Plugin Discovery Best Practices (v0.2.1):
- ✅ DO use relative paths for project-local plugins:
'./plugins/my-plugin.js' - ✅ DO use absolute paths for system-wide plugins:
'/usr/local/lib/plugins/system-plugin.js' - ✅ DO specify directories to load all plugins:
'./plugins/'(loads all.jsfiles) - ✅ DO use published npm packages:
'@myorg/analytics-plugin' - ❌ DON'T mix plugin discovery with manual registration (choose one approach)
- ❌ DON'T use plugin discovery in production without proper validation
- ❌ DON'T load untrusted plugins from arbitrary paths
Host Context Best Practices:
- ✅ DO inject: Database connections, HTTP clients, loggers, configuration objects
- ✅ DO inject: Stateless services and utilities
- ❌ DON'T inject: Request-scoped data (user sessions, request objects)
- ❌ DON'T inject: Large objects (> 1MB)
- ❌ DON'T inject: Functions directly (wrap in objects instead)
Interface for querying runtime metadata. Accessible via context.introspect.
interface IntrospectionAPI {
// Action introspection
listActions(): string[];
getActionDefinition(id: string): ActionMetadata | null;
// Plugin introspection
listPlugins(): string[];
getPluginDefinition(name: string): PluginMetadata | null;
// Screen introspection
listScreens(): string[];
getScreenDefinition(id: string): ScreenDefinition | null;
// Runtime introspection
getMetadata(): IntrospectionMetadata;
}Methods:
Returns an array of all registered action IDs.
Example:
const actionIds = context.introspect.listActions();
// ['users:load', 'users:create', 'reports:generate']Returns metadata for a specific action, or null if not found. Handler function is excluded.
Returns: ActionMetadata object or null
Example:
const metadata = context.introspect.getActionDefinition('users:load');
// { id: 'users:load', timeout: 5000 }Returns an array of all registered plugin names.
Example:
const pluginNames = context.introspect.listPlugins();
// ['users', 'reports', 'analytics']Returns metadata for a specific plugin, or null if not found. Setup and dispose functions are excluded.
Returns: PluginMetadata object or null
Example:
const metadata = context.introspect.getPluginDefinition('users');
// { name: 'users', version: '1.0.0' }Returns an array of all registered screen IDs.
Example:
const screenIds = context.introspect.listScreens();
// ['users:list', 'users:detail', 'reports:overview']Returns the full screen definition, or null if not found.
Returns: ScreenDefinition object or null
Example:
const screen = context.introspect.getScreenDefinition('users:list');
// { id: 'users:list', title: 'User List', component: UserListComponent }Returns overall runtime statistics.
Returns: IntrospectionMetadata object
Example:
const metadata = context.introspect.getMetadata();
// {
// runtimeVersion: '0.1.0',
// totalActions: 15,
// totalPlugins: 5,
// totalScreens: 8
// }Use Cases:
- Building admin dashboards
- Debugging and development tools
- Dynamic UI generation
- Runtime monitoring
- Plugin discovery
Security Note: All returned objects are deeply frozen to prevent mutation of internal runtime state.
[NEW v0.3.0]
Result interface for the validateConfig plugin hook.
interface ConfigValidationResult {
valid: boolean;
errors?: string[];
}Properties:
valid(required): Boolean indicating if configuration is validerrors(optional): Array of error message strings if invalid
Example:
const result: ConfigValidationResult = {
valid: false,
errors: ["Missing API Key", "Invalid timeout value"]
};Metadata for an action definition (excludes handler function).
interface ActionMetadata {
id: string;
timeout?: number;
}Properties:
id: Unique action identifiertimeout(optional): Timeout in milliseconds
Note: The handler function is intentionally excluded from metadata for security and encapsulation.
Metadata for a plugin definition (excludes setup and dispose functions).
interface PluginMetadata {
name: string;
version: string;
}Properties:
name: Unique plugin identifierversion: Plugin version string
Note: Setup and dispose functions are intentionally excluded from metadata for security and encapsulation.
Overall runtime statistics and metadata.
interface IntrospectionMetadata {
runtimeVersion: string;
totalActions: number;
totalPlugins: number;
totalScreens: number;
}Properties:
runtimeVersion: Version of the runtime (from package.json)totalActions: Count of registered actionstotalPlugins: Count of registered pluginstotalScreens: Count of registered screens
Error thrown when validation fails for a resource.
class ValidationError extends Error {
constructor(
public resourceType: string,
public field: string,
public resourceId?: string
)
}Properties:
resourceType: Type of resource (e.g., "Plugin", "Screen", "Action")field: Name of the missing or invalid fieldresourceId: Optional identifier of the resource
Example:
throw new ValidationError("Plugin", "name");
// Error: Validation failed for Plugin: missing or invalid field "name"
throw new ValidationError("Screen", "title", "home");
// Error: Validation failed for Screen "home": missing or invalid field "title"Error thrown when attempting to register a duplicate resource.
class DuplicateRegistrationError extends Error {
constructor(
public resourceType: string,
public identifier: string
)
}Properties:
resourceType: Type of resource (e.g., "Plugin", "Screen", "Action")identifier: The duplicate identifier
Example:
throw new DuplicateRegistrationError("Plugin", "my-plugin");
// Error: Plugin with identifier "my-plugin" is already registeredError thrown when an action execution exceeds its timeout.
class ActionTimeoutError extends Error {
constructor(
public actionId: string,
public timeoutMs: number
)
}Properties:
actionId: The action identifiertimeoutMs: The timeout value in milliseconds
Example:
throw new ActionTimeoutError("loadUsers", 5000);
// Error: Action "loadUsers" timed out after 5000msError thrown when an action handler throws an error.
class ActionExecutionError extends Error {
constructor(
public actionId: string,
public cause: Error
)
}Properties:
actionId: The action identifiercause: The original error from the handler
Example:
throw new ActionExecutionError("loadUsers", new Error("Network error"));
// Error: Action "loadUsers" execution failed: Network errorDefault console-based logger implementation.
class ConsoleLogger implements Logger {
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
}Example:
const logger = new ConsoleLogger();
logger.info("Runtime initialized");Runtime lifecycle states.
enum RuntimeState {
Uninitialized = 'uninitialized',
Initializing = 'initializing',
Initialized = 'initialized',
ShuttingDown = 'shutting_down',
Shutdown = 'shutdown'
}Values:
Uninitialized: Runtime has been created but not initializedInitializing: Runtime is currently initializingInitialized: Runtime is fully initialized and readyShuttingDown: Runtime is currently shutting downShutdown: Runtime has been shut down
Example:
const state = runtime.getState();
if (state === RuntimeState.Initialized) {
// Runtime is ready
}Actions support generic type parameters for type-safe parameter and return types.
// Action with no parameters, returns string
const action1: ActionDefinition<void, string> = {
id: "getMessage",
handler: async () => "Hello"
};
// Action with typed parameters, returns typed result
interface CreateUserParams {
name: string;
email: string;
}
interface User {
id: number;
name: string;
email: string;
}
const action2: ActionDefinition<CreateUserParams, User> = {
id: "createUser",
handler: async (params, ctx) => {
return {
id: 123,
name: params.name,
email: params.email
};
}
};
// Type-safe execution
const user = await ctx.actions.runAction<CreateUserParams, User>(
"createUser",
{ name: "Alice", email: "alice@example.com" }
);import {
Runtime,
PluginDefinition,
ScreenDefinition,
ActionDefinition,
UIProvider,
RuntimeContext
} from "skeleton-crew";
// Define a plugin
const myPlugin: PluginDefinition = {
name: "my-plugin",
version: "1.0.0",
setup(ctx: RuntimeContext) {
// Register a screen
ctx.screens.registerScreen({
id: "home",
title: "Home",
component: "HomeComponent"
});
// Register an action
ctx.actions.registerAction({
id: "loadData",
handler: async (params, ctx) => {
return { data: "loaded" };
},
timeout: 5000
});
// Subscribe to events
ctx.events.on("data:loaded", (data) => {
console.log("Data loaded:", data);
});
},
dispose(ctx: RuntimeContext) {
console.log("Cleaning up plugin");
}
};
// Define a UI provider
const uiProvider: UIProvider = {
mount(target, ctx) {
console.log("UI mounted");
},
renderScreen(screen) {
console.log(`Rendering screen: ${screen.title}`);
return screen.component;
},
unmount() {
console.log("UI unmounted");
}
};
// Create and initialize runtime
const runtime = new Runtime();
runtime.registerPlugin(myPlugin);
await runtime.initialize();
// Set UI provider
runtime.setUIProvider(uiProvider);
// Get context and use it
const ctx = runtime.getContext();
// Execute action
const result = await ctx.actions.runAction("loadData");
// Emit event
ctx.events.emit("data:loaded", result);
// Render screen
runtime.renderScreen("home");
// Shutdown when done
await runtime.shutdown();Complete example showing how to use host context injection and introspection for migrating legacy applications:
import { Runtime } from "skeleton-crew";
// Legacy application with existing services
class LegacyApp {
constructor() {
this.database = {
query: async (sql) => {
// Existing database logic
return [];
}
};
this.logger = {
log: (message) => console.log(`[Legacy] ${message}`)
};
this.cache = new Map();
}
}
// Create legacy app instance
const legacyApp = new LegacyApp();
// Create runtime with host context
const runtime = new Runtime({
hostContext: {
db: legacyApp.database,
logger: legacyApp.logger,
cache: legacyApp.cache,
config: {
apiUrl: 'https://api.example.com',
apiKey: process.env.API_KEY
}
}
});
// Plugin that uses injected services
const dataPlugin = {
name: 'data-plugin',
version: '1.0.0',
setup(context) {
// Access host services
const db = context.host.db;
const logger = context.host.logger;
const config = context.host.config;
// Register action using legacy database
context.actions.registerAction({
id: 'data:load',
handler: async (params) => {
logger.log('Loading data...');
const results = await db.query('SELECT * FROM users');
return results;
}
});
// Use introspection for debugging
context.actions.registerAction({
id: 'debug:info',
handler: () => {
const metadata = context.introspect.getMetadata();
logger.log(`Runtime v${metadata.runtimeVersion}`);
logger.log(`Actions: ${metadata.totalActions}`);
logger.log(`Plugins: ${metadata.totalPlugins}`);
const actions = context.introspect.listActions();
logger.log(`Available actions: ${actions.join(', ')}`);
return metadata;
}
});
}
};
// Initialize runtime
runtime.registerPlugin(dataPlugin);
await runtime.initialize();
const context = runtime.getContext();
// Use the plugin
const data = await context.actions.runAction('data:load');
const debugInfo = await context.actions.runAction('debug:info');
// Introspection from outside plugins
console.log('All registered actions:', context.introspect.listActions());
console.log('All registered plugins:', context.introspect.listPlugins());
const actionMeta = context.introspect.getActionDefinition('data:load');
console.log('Action metadata:', actionMeta);
// { id: 'data:load', timeout: undefined }
// Host context is immutable
try {
context.host.newService = {}; // ❌ Throws TypeError
} catch (error) {
console.log('Host context is immutable:', error.message);
}
// Cleanup
await runtime.shutdown();Key Points:
- Host Context Injection: Legacy services are injected via
hostContextoption - Plugin Access: Plugins access services via
context.host - Immutability: Host context is frozen to prevent mutation
- Introspection: Query runtime state via
context.introspect - Metadata Only: Introspection returns metadata without function implementations
- Deep Freeze: All introspection results are deeply frozen
The runtime emits the following built-in events:
Emitted after successful runtime initialization.
Data:
{
context: RuntimeContext
}Example:
ctx.events.on("runtime:initialized", (data) => {
console.log("Runtime initialized");
});Emitted at the start of runtime shutdown.
Data:
{
context: RuntimeContext
}Example:
ctx.events.on("runtime:shutdown", (data) => {
console.log("Runtime shutting down");
});Skeleton Crew Runtime v0.2.1 introduces automatic plugin discovery capabilities, allowing you to load plugins from file paths and npm packages without manual registration.
Automatically load plugins from specified file paths or directories.
Supported Formats:
- Single File:
'./plugins/my-plugin.js'- Load specific plugin file - Directory:
'./plugins/'- Load all.jsfiles from directory (non-recursive) - Absolute Path:
'/usr/local/lib/plugins/system-plugin.js'- Load from absolute path - Glob Patterns:
'./plugins/**/*.plugin.js'- Load files matching pattern
Example:
const runtime = new Runtime({
pluginPaths: [
'./plugins/core-plugin.js', // Single file
'./plugins/custom/', // Directory (all .js files)
'/absolute/path/to/plugin.js', // Absolute path
'./plugins/**/*.plugin.js' // Glob pattern
]
});Automatically load plugins from npm packages.
Supported Formats:
- Package Name:
'my-plugin-package'- Load from node_modules - Scoped Package:
'@myorg/analytics-plugin'- Load scoped package - Version Specific:
'plugin-name@1.2.3'- Load specific version (if available)
Example:
const runtime = new Runtime({
pluginPackages: [
'@myorg/analytics-plugin',
'skeleton-crew-ui-plugin',
'my-custom-plugin-package'
]
});-
Discovery Phase (during
runtime.initialize()):- Resolve all paths in
pluginPaths - Import all packages in
pluginPackages - Validate plugin exports
- Register discovered plugins
- Resolve all paths in
-
Registration Phase:
- Manually registered plugins (via
registerPlugin()) - Auto-discovered plugins (from paths and packages)
- Dependency resolution across all plugins
- Manually registered plugins (via
-
Initialization Phase:
- Execute plugin setup callbacks in dependency order
For automatic discovery, plugins must export a default plugin definition:
ES Module Export:
// my-plugin.js
import { PluginDefinition } from 'skeleton-crew';
const myPlugin: PluginDefinition = {
name: 'my-plugin',
version: '1.0.0',
setup(ctx) {
// Plugin logic
}
};
export default myPlugin;CommonJS Export:
// my-plugin.js
module.exports = {
name: 'my-plugin',
version: '1.0.0',
setup(ctx) {
// Plugin logic
}
};import { Runtime } from 'skeleton-crew';
// Create runtime with mixed plugin loading strategies
const runtime = new Runtime({
// Automatic discovery
pluginPaths: [
'./plugins/core/', // Load all plugins from directory
'./plugins/analytics.plugin.js', // Specific analytics plugin
'./custom/special-plugin.js' // Custom plugin
],
pluginPackages: [
'@myorg/ui-plugin', // Organization plugin
'skeleton-crew-logger-plugin', // Community plugin
'my-data-plugin' // Custom npm package
],
// Configuration for discovered plugins
config: {
analytics: {
enabled: true,
apiKey: process.env.ANALYTICS_KEY
},
ui: {
theme: 'dark',
animations: true
}
}
});
// Manual plugin registration (optional - can mix with discovery)
const customPlugin = {
name: 'custom-manual-plugin',
version: '1.0.0',
dependencies: ['analytics'], // Can depend on discovered plugins
setup(ctx) {
ctx.logger.info('Manual plugin initialized');
}
};
runtime.registerPlugin(customPlugin);
// Initialize - discovers and registers all plugins
await runtime.initialize();
const ctx = runtime.getContext();
// All plugins (discovered + manual) are now available
console.log('Loaded plugins:', ctx.introspect.listPlugins());
// ['analytics', 'ui-plugin', 'logger-plugin', 'data-plugin', 'custom-manual-plugin']File System Access:
- Plugin discovery requires file system access
- Only load plugins from trusted directories
- Validate plugin sources in production environments
- Consider using allowlists for production deployments
Package Loading:
- npm packages are loaded from
node_modules - Ensure packages are from trusted sources
- Use package-lock.json to pin versions
- Audit dependencies regularly
Skeleton Crew Runtime includes optional performance monitoring utilities designed to have near-zero overhead when not in use.
interface PerformanceMonitor {
startTimer(label: string): () => number;
recordMetric(name: string, value: number): void;
getMetrics(): Record<string, number>;
}Methods:
startTimer(label: string): Returns a function that, when called, records the elapsed timerecordMetric(name: string, value: number): Records a custom metric valuegetMetrics(): Returns all recorded metrics as a key-value object
Production-ready no-op implementation with zero overhead.
import { NoOpPerformanceMonitor } from 'skeleton-crew';
const monitor = new NoOpPerformanceMonitor();
const timer = monitor.startTimer('operation'); // No-op
timer(); // Returns 0
monitor.recordMetric('custom', 123); // No-op
console.log(monitor.getMetrics()); // {}Development implementation that records actual performance metrics.
import { SimplePerformanceMonitor } from 'skeleton-crew';
const monitor = new SimplePerformanceMonitor();
// Time an operation
const timer = monitor.startTimer('database-query');
await performDatabaseQuery();
const duration = timer(); // Returns actual duration in milliseconds
// Record custom metrics
monitor.recordMetric('users-loaded', 150);
monitor.recordMetric('cache-hits', 42);
// Get all metrics
console.log(monitor.getMetrics());
// { 'database-query': 245.67, 'users-loaded': 150, 'cache-hits': 42 }Creates the appropriate performance monitor based on environment.
import { createPerformanceMonitor } from 'skeleton-crew';
// Development - enabled monitoring
const devMonitor = createPerformanceMonitor(true);
// Returns SimplePerformanceMonitor
// Production - disabled monitoring (default)
const prodMonitor = createPerformanceMonitor(false);
// Returns NoOpPerformanceMonitor
// Auto-detect from environment
const monitor = createPerformanceMonitor(process.env.NODE_ENV === 'development');Enable performance monitoring via RuntimeOptions:
import { Runtime, createPerformanceMonitor } from 'skeleton-crew';
const runtime = new Runtime({
enablePerformanceMonitoring: true, // Enables SimplePerformanceMonitor
// Or provide custom monitor
performanceMonitor: createPerformanceMonitor(process.env.ENABLE_METRICS === 'true')
});Access performance monitoring through the runtime context:
const myPlugin: PluginDefinition = {
name: 'data-plugin',
version: '1.0.0',
setup(ctx) {
ctx.actions.registerAction({
id: 'data:load',
handler: async (params) => {
// Time the operation (works with both monitor types)
const timer = ctx.performance?.startTimer('data-load');
try {
const data = await loadData(params);
// Record custom metrics
ctx.performance?.recordMetric('records-loaded', data.length);
return data;
} finally {
// Record timing
const duration = timer?.() ?? 0;
ctx.logger.debug(`Data load took ${duration}ms`);
}
}
});
}
};Development:
// ✅ Enable monitoring in development
const runtime = new Runtime({
enablePerformanceMonitoring: process.env.NODE_ENV === 'development'
});Production:
// ✅ Disable monitoring in production (default)
const runtime = new Runtime({
enablePerformanceMonitoring: false // or omit entirely
});Plugin Usage:
// ✅ Safe usage - works with both monitor types
const timer = ctx.performance?.startTimer('operation');
const result = await performOperation();
const duration = timer?.() ?? 0;
// ✅ Conditional metrics
if (ctx.performance) {
ctx.performance.recordMetric('custom-metric', value);
}Performance Impact:
- NoOpPerformanceMonitor: Zero overhead, all methods are no-ops
- SimplePerformanceMonitor: Minimal overhead, uses
performance.now() - Memory Usage: SimplePerformanceMonitor stores metrics in memory
- Thread Safety: Both implementations are safe for single-threaded JavaScript
- Always provide a dispose callback for cleanup
- Use unregister functions returned from registration methods
- Handle errors gracefully in event handlers
- Use typed actions for better type safety
- Namespace your screen and action IDs (e.g., "myplugin:screen-name")
- DO inject stateless services: Database connections, HTTP clients, loggers, configuration
- DON'T inject request-scoped data: User sessions, request objects, temporary state
- DON'T inject large objects: Keep host context under 1MB for performance
- Wrap functions in objects: Instead of
{ fn: () => {} }, use{ service: { fn: () => {} } } - Treat as immutable: Never attempt to modify
context.hostin plugins
- Use for debugging and tooling: Admin dashboards, development tools, monitoring
- Don't rely on implementation details: Metadata excludes functions intentionally
- Handle null returns: Resources may not exist, always check for null
- Leverage deep freeze: Returned objects are safe to pass around without copying
- Query efficiently: Introspection is fast (< 1ms) but avoid unnecessary queries in hot paths
- Catch specific error types for better error handling
- Use try-catch around action execution
- Log errors appropriately
- Provide context in error messages
- Use O(1) lookups - all registries use Map-based storage
- Unregister unused handlers to prevent memory leaks
- Use action timeouts for long-running operations
- Batch event emissions when possible
- Create isolated runtime instances for each test
- Always shutdown runtime after tests
- Test plugin lifecycle (setup and dispose)
- Mock UI providers for testing
- Test error scenarios explicitly
For best TypeScript experience, ensure your tsconfig.json includes:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true
}
}Skeleton Crew uses ESM (ES Modules). All imports must use .js extensions:
// Correct
import { Runtime } from "./runtime.js";
// Incorrect
import { Runtime } from "./runtime";This example shows migrating from v0.1.x to v0.2.0 with full type safety and modern patterns.
// Old way - no types, host context injection
import { Runtime } from 'skeleton-crew';
const runtime = new Runtime({
hostContext: {
config: {
host: 'localhost:3000',
jobId: 'job-123',
workDir: '/tmp/work',
token: 'abc123'
}
}
});
const downloaderPlugin = {
name: 'downloader',
version: '1.0.0',
setup(ctx) {
// ❌ No type safety - requires casting
const config = (ctx.host.config as any);
const { jobId, workDir } = config;
ctx.actions.registerAction({
id: 'download:start',
handler: async (params) => {
// ❌ No type safety on params or return
console.log(`Downloading to ${workDir}`);
return { success: true };
}
});
}
};
runtime.registerPlugin(downloaderPlugin);
await runtime.initialize();// New way - full type safety, clean architecture
import { Runtime, PluginDefinition, RuntimeContext } from 'skeleton-crew';
// ✅ Define your config interface
interface PreviewConfig {
host: string;
jobId: string;
workDir: string;
token?: string;
}
// ✅ Define typed parameters and results
interface DownloadParams {
url: string;
filename: string;
}
interface DownloadResult {
success: boolean;
path: string;
size: number;
}
// ✅ Create typed runtime
const runtime = new Runtime<PreviewConfig>({
config: {
host: 'localhost:3000',
jobId: 'job-123',
workDir: '/tmp/work',
token: 'abc123'
}
});
// ✅ Fully typed plugin
const downloaderPlugin: PluginDefinition<PreviewConfig> = {
name: 'downloader',
version: '1.0.0',
dependencies: ['config'], // ✅ Explicit dependencies
setup(ctx: RuntimeContext<PreviewConfig>) {
// ✅ Full type safety - no casting needed!
const { jobId, workDir, token } = ctx.config;
ctx.logger.info(`Downloader initialized for job: ${jobId}`);
// ✅ Type-safe action registration
ctx.actions.registerAction<DownloadParams, DownloadResult>({
id: 'download:start',
handler: async (params, ctx) => {
// ✅ params is typed as DownloadParams
// ✅ return must match DownloadResult
const { url, filename } = params;
const fullPath = `${ctx.config.workDir}/${filename}`;
ctx.logger.info(`Downloading ${url} to ${fullPath}`);
// Simulate download
await new Promise(resolve => setTimeout(resolve, 1000));
return {
success: true,
path: fullPath,
size: 1024
};
},
timeout: 30000
});
}
};
runtime.registerPlugin(downloaderPlugin);
await runtime.initialize();
// ✅ Type-safe action execution
const result = await runtime.getContext().actions.runAction<DownloadParams, DownloadResult>(
'download:start',
{ url: 'https://example.com/file.zip', filename: 'download.zip' }
);
console.log(`Downloaded ${result.size} bytes to ${result.path}`);interface AppConfig {
database: {
url: string;
maxConnections: number;
};
cache: {
ttl: number;
maxSize: number;
};
}
const runtime = new Runtime<AppConfig>({
config: {
database: {
url: 'postgresql://localhost:5432/myapp',
maxConnections: 10
},
cache: {
ttl: 3600,
maxSize: 1000
}
}
});
// Base plugin - no dependencies
const configPlugin: PluginDefinition<AppConfig> = {
name: 'config',
version: '1.0.0',
setup(ctx) {
ctx.logger.info('Config plugin initialized');
ctx.actions.registerAction({
id: 'config:get',
handler: () => ctx.config
});
}
};
// Database plugin - depends on config
const databasePlugin: PluginDefinition<AppConfig> = {
name: 'database',
version: '1.0.0',
dependencies: ['config'], // ✅ Will initialize after config
setup(ctx) {
const { database } = ctx.config;
ctx.logger.info(`Connecting to database: ${database.url}`);
ctx.actions.registerAction({
id: 'db:query',
handler: async (sql: string) => {
// Database query logic
return [];
}
});
}
};
// Cache plugin - depends on config
const cachePlugin: PluginDefinition<AppConfig> = {
name: 'cache',
version: '1.0.0',
dependencies: ['config'], // ✅ Will initialize after config
setup(ctx) {
const { cache } = ctx.config;
ctx.logger.info(`Cache initialized with TTL: ${cache.ttl}s`);
ctx.actions.registerAction({
id: 'cache:get',
handler: async (key: string) => {
// Cache get logic
return null;
}
});
}
};
// Data service - depends on both database and cache
const dataServicePlugin: PluginDefinition<AppConfig> = {
name: 'data-service',
version: '1.0.0',
dependencies: ['database', 'cache'], // ✅ Will initialize after both
setup(ctx) {
ctx.logger.info('Data service initialized');
ctx.actions.registerAction({
id: 'data:getUser',
handler: async (userId: string) => {
// Try cache first
const cached = await ctx.actions.runAction('cache:get', `user:${userId}`);
if (cached) return cached;
// Query database
const user = await ctx.actions.runAction('db:query', `SELECT * FROM users WHERE id = '${userId}'`);
// Cache result
await ctx.actions.runAction('cache:set', { key: `user:${userId}`, value: user });
return user;
}
});
}
};
// Register in any order - dependencies will be resolved
runtime.registerPlugin(dataServicePlugin); // Depends on database, cache
runtime.registerPlugin(cachePlugin); // Depends on config
runtime.registerPlugin(configPlugin); // No dependencies
runtime.registerPlugin(databasePlugin); // Depends on config
// ✅ Initialization order will be: config → database, cache → data-service
await runtime.initialize();interface MyConfig {
apiUrl: string;
retryCount: number;
timeout: number;
}
const myPlugin: PluginDefinition<MyConfig> = {
name: 'api-client',
version: '1.0.0',
setup(ctx) {
// ✅ v0.2.0: Synchronous config access
const { apiUrl, retryCount, timeout } = ctx.config;
// ✅ No need for async config loading
const client = new ApiClient({
baseURL: apiUrl,
timeout: timeout,
retries: retryCount
});
ctx.actions.registerAction({
id: 'api:request',
handler: async (params: { endpoint: string; data?: any }) => {
// Config is always available synchronously
ctx.logger.info(`Making request to ${ctx.config.apiUrl}${params.endpoint}`);
return await client.request(params.endpoint, params.data);
}
});
// ✅ Config access in event handlers
ctx.events.on('api:error', (error) => {
ctx.logger.error(`API error (${ctx.config.apiUrl}):`, error);
});
}
};// v0.2.0: Clean, typed Express integration
interface ServerConfig {
port: number;
cors: {
origins: string[];
credentials: boolean;
};
database: {
url: string;
pool: {
min: number;
max: number;
};
};
auth: {
jwtSecret: string;
tokenExpiry: string;
};
}
const runtime = new Runtime<ServerConfig>({
config: {
port: parseInt(process.env.PORT || '3000'),
cors: {
origins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
},
database: {
url: process.env.DATABASE_URL!,
pool: {
min: 2,
max: 10
}
},
auth: {
jwtSecret: process.env.JWT_SECRET!,
tokenExpiry: '24h'
}
}
});
// Database plugin
const databasePlugin: PluginDefinition<ServerConfig> = {
name: 'database',
version: '1.0.0',
setup(ctx) {
const { database } = ctx.config;
// Initialize database with typed config
const pool = new Pool({
connectionString: database.url,
min: database.pool.min,
max: database.pool.max
});
ctx.actions.registerAction({
id: 'db:query',
handler: async (sql: string, params?: any[]) => {
const client = await pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
});
}
};
// Auth plugin
const authPlugin: PluginDefinition<ServerConfig> = {
name: 'auth',
version: '1.0.0',
dependencies: ['database'],
setup(ctx) {
const { auth } = ctx.config;
ctx.actions.registerAction({
id: 'auth:login',
handler: async (credentials: { email: string; password: string }) => {
const users = await ctx.actions.runAction('db:query',
'SELECT * FROM users WHERE email = $1', [credentials.email]);
if (users.length === 0) {
throw new Error('User not found');
}
// Verify password, generate JWT with typed config
const token = jwt.sign({ userId: users[0].id }, auth.jwtSecret, {
expiresIn: auth.tokenExpiry
});
return { token, user: users[0] };
}
});
}
};
// API routes plugin
const apiPlugin: PluginDefinition<ServerConfig> = {
name: 'api',
version: '1.0.0',
dependencies: ['database', 'auth'],
setup(ctx) {
const { cors } = ctx.config;
// Configure CORS with typed config
const corsOptions = {
origin: cors.origins,
credentials: cors.credentials
};
ctx.actions.registerAction({
id: 'api:setup',
handler: (app: Express) => {
app.use(cors(corsOptions));
app.post('/api/login', async (req, res) => {
try {
const result = await ctx.actions.runAction('auth:login', req.body);
res.json(result);
} catch (error) {
res.status(401).json({ error: error.message });
}
});
app.get('/api/users', async (req, res) => {
const users = await ctx.actions.runAction('db:query', 'SELECT * FROM users');
res.json(users);
});
}
});
}
};
// Initialize and start server
runtime.registerPlugin(databasePlugin);
runtime.registerPlugin(authPlugin);
runtime.registerPlugin(apiPlugin);
await runtime.initialize();
const app = express();
app.use(express.json());
// Setup API routes
await runtime.getContext().actions.runAction('api:setup', app);
// Start server with typed config
const { port } = runtime.getConfig();
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});v0.1.x → v0.2.0 Benefits:
-
Type Safety
- ❌ Before:
(ctx.host.config as any).apiUrl - ✅ After:
ctx.config.apiUrl(fully typed)
- ❌ Before:
-
Developer Experience
- ❌ Before: No IDE autocomplete, runtime errors
- ✅ After: Full IntelliSense, compile-time errors
-
Plugin Dependencies
- ❌ Before: Manual initialization order management
- ✅ After: Automatic dependency resolution
-
Configuration Access
- ❌ Before: Async host context access
- ✅ After: Synchronous typed config access
-
Error Prevention
- ❌ Before: Runtime type errors, missing properties
- ✅ After: Compile-time validation, required properties
Migration Effort: Minimal - mostly adding type annotations and moving from hostContext to config.
For more examples, see the /example folder in the repository.