ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
180 lines (179 loc) • 8.24 kB
TypeScript
/**
* Resource factory with declared dependencies and optional disposal callback
*/
export interface ResourceFactoryWithDeps<T, Context = unknown, D extends string = string> {
dependsOn?: readonly D[];
factory: (context: Context) => T | Promise<T>;
onDispose?: (resource: T, context: Context) => void | Promise<void>;
}
/** @internal */
export declare const RESOURCE_DIRECT: unique symbol;
/**
* Branded wrapper for storing a value as-is, bypassing factory detection.
* The value is carried on the symbol key to avoid structural conflicts
* with user resource types that have a `value` property.
* Create via the `directValue()` helper.
*/
export interface ResourceDirectValue<T> {
[RESOURCE_DIRECT]: T;
}
/**
* Wrap a value to store it as-is, bypassing factory detection.
* Use when the resource itself is a function or class that should not be invoked.
*
* @example
* ```ts
* import { directValue } from 'ecspresso';
* world.addResource('handler', directValue(myFunction));
* world.addResource('MyClass', directValue(MyClass));
* ```
*/
export declare function directValue<T>(value: T): ResourceDirectValue<T>;
/**
* When Context is unknown (default), context args are optional.
* When Context is a specific type (e.g. ECSpresso<...>), context is required.
*/
type ContextArgs<Context> = unknown extends Context ? [context?: Context] : [context: Context];
export default class ResourceManager<ResourceTypes extends Record<string, any> = Record<string, any>, Context = unknown> {
private resources;
private resourceFactories;
private resourceDependencies;
private resourceDisposers;
private initializedResourceKeys;
private _changeSubscribers;
/** Shallow snapshots of observed resources, keyed by resource key */
private _observedSnapshots;
/**
* Add a resource to the manager.
*
* Resolution order:
* 1. `{ factory, dependsOn?, onDispose? }` → factory with optional deps/disposal
* 2. `{ value }` → direct value wrapper (use to store functions/classes as-is)
* 3. `typeof === 'function'` → bare factory (no deps)
* 4. Anything else → direct value
*
* @param label The resource key
* @param resource The resource value, a factory function, or a factory with dependencies
* @returns The resource manager instance for chaining
*/
add<K extends keyof ResourceTypes>(label: K, resource: ResourceTypes[K] | ((context: Context) => ResourceTypes[K] | Promise<ResourceTypes[K]>) | ResourceFactoryWithDeps<ResourceTypes[K], Context, keyof ResourceTypes & string> | ResourceDirectValue<ResourceTypes[K]>): this;
/**
* Try to get a resource from the manager.
* Returns the resource value if it exists, or undefined if not found.
* Like `get`, initializes factory resources on first access.
* @param label The resource key
* @param context Context to pass to factory functions (usually the ECSpresso instance)
* @returns The resource value, or undefined if not found
* @see get — the throwing alternative
*/
tryGet<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): ResourceTypes[K] | undefined;
/**
* Get a resource from the manager
* @param label The resource key
* @param context Context to pass to factory functions (usually the ECSpresso instance)
* @returns The resource value
* @throws Error if resource not found
* @see tryGet — the non-throwing alternative
*/
get<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): ResourceTypes[K];
/**
* Check if a resource exists
* @param label The resource key
* @returns True if the resource exists
*/
has<K extends keyof ResourceTypes>(label: K): boolean;
/**
* Remove a resource (without calling onDispose)
* @param label The resource key
* @returns True if the resource was removed
*/
remove<K extends keyof ResourceTypes>(label: K): boolean;
/**
* Get all resource keys
* @returns Array of resource keys
*/
getKeys(): Array<keyof ResourceTypes>;
/**
* Check if a resource needs to be initialized
* @param label The resource key
* @returns True if the resource needs initialization
*/
needsInitialization<K extends keyof ResourceTypes>(label: K): boolean;
/**
* Get all resource keys that need to be initialized
* @returns Array of resource keys that need initialization
*/
getPendingInitializationKeys(): Array<keyof ResourceTypes>;
/**
* Initialize a specific resource if it's a factory function
* @param label The resource key
* @param context Context to pass to factory functions
* @returns Promise that resolves when the resource is initialized
*/
initializeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<void>;
/**
* Initialize specific resources or all resources that haven't been initialized yet.
* Resources are initialized in topological order based on their dependencies.
* @param context Context to pass to factory functions (usually the ECSpresso instance)
* @param keys Optional array of resource keys to initialize
* @returns Promise that resolves when the specified resources are initialized
*/
initializeResources<K extends keyof ResourceTypes>(...args: [...ContextArgs<Context>, ...K[]]): Promise<void>;
/**
* Get the dependencies of a resource
* @param label The resource key
* @returns Array of resource keys that this resource depends on
*/
getDependencies<K extends keyof ResourceTypes>(label: K): readonly (keyof ResourceTypes & string)[];
/**
* Dispose a single resource, calling its onDispose callback if it exists
* @param label The resource key to dispose
* @param context Context to pass to the onDispose callback
* @returns True if the resource existed and was disposed, false if it didn't exist
*/
disposeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<boolean>;
/**
* Subscribe to changes for a specific resource key.
*
* Subscribing marks the resource as "observed." Observed resources:
* - Are re-resolved each frame by `withResources` (no stale cache)
* - Are shallow-diffed at the end of each frame via `flushObserved()`,
* so in-place mutations are detected and subscribers notified
*
* When the last subscriber unsubscribes, the resource reverts to
* normal (cached, no per-frame diff).
*
* @param key The resource key to watch
* @param callback Function called with (newValue, oldValue) when the resource changes
* @returns Unsubscribe function
*/
onResourceChange<K extends keyof ResourceTypes>(key: K, callback: (newValue: ResourceTypes[K], oldValue: ResourceTypes[K]) => void): () => void;
/**
* Notify subscribers of a resource value change.
* Skips notification if the value is unchanged (via Object.is).
* @param key The resource key that changed
* @param newValue The new resource value
* @param oldValue The previous resource value
*/
notifyChange<K extends keyof ResourceTypes>(key: K, newValue: ResourceTypes[K], oldValue: ResourceTypes[K]): void;
/**
* Whether a resource has active change subscribers.
* Observed resources should not be cached by systems — they need
* to be re-resolved each frame so external mutations are visible.
*/
isObserved<K extends keyof ResourceTypes>(key: K): boolean;
/**
* Diff all observed resources against their snapshots.
* Fires subscribers for any resource whose shallow properties changed
* since the last snapshot, then updates the snapshot.
* Call once per frame after all systems have run.
*/
flushObserved(): void;
/**
* Dispose all initialized resources in reverse dependency order.
* Resources that depend on others are disposed first.
* @param context Context to pass to onDispose callbacks
*/
disposeResources(...args: ContextArgs<Context>): Promise<void>;
}
export {};