UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

180 lines (179 loc) 8.24 kB
/** * 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 {};