1
0
Fork 0
tsgames/src/common/rpg/core/registry.ts

101 lines
3.4 KiB
TypeScript

import type { Component } from './world';
type ComponentConstructor = abstract new (...args: any[]) => Component<any>;
type MigrationFn = (state: Record<string, unknown>) => Record<string, unknown>;
interface ComponentMeta {
ctor: ComponentConstructor;
version: number;
}
interface MigrationEntry {
toVersion: number;
fn: MigrationFn;
}
const registry = new Map<string, ComponentMeta>();
const reverseRegistry = new Map<ComponentConstructor, string>();
/** migrations[name][fromVersion] → { toVersion, fn } */
const migrations = new Map<string, Map<number, MigrationEntry>>();
function register(name: string, ctor: ComponentConstructor, version: number): void {
registry.set(name, { ctor, version });
reverseRegistry.set(ctor, name);
}
export function getComponentMeta(name: string): ComponentMeta | undefined {
return registry.get(name);
}
export function getComponentName(ctor: Function): string | undefined {
return reverseRegistry.get(ctor as ComponentConstructor);
}
/**
* Register a migration that upgrades component state from `fromVersion` to `toVersion`.
* Migrations are chained automatically: registering 0→1 and 1→2 handles saves at version 0.
*/
export function registerMigration(
name: string,
fromVersion: number,
toVersion: number,
fn: MigrationFn,
): void {
if (!migrations.has(name)) migrations.set(name, new Map());
migrations.get(name)!.set(fromVersion, { toVersion, fn });
}
/**
* Apply all registered migrations to bring `state` from `fromVersion` up to the current
* registered version. Returns the migrated state (may be the same object if no migrations ran).
*/
export function migrateState(
name: string,
state: Record<string, unknown>,
fromVersion: number,
): Record<string, unknown> {
const meta = registry.get(name);
if (!meta) return state;
const chain = migrations.get(name);
if (!chain) return state;
let current = fromVersion;
let s = state;
while (current < meta.version) {
const entry = chain.get(current);
if (!entry) throw new Error(
`[registry] No migration for '${name}' from version ${current} to ${meta.version}. ` +
`Register one with registerMigration('${name}', ${current}, ...).`
);
s = entry.fn(s);
current = entry.toVersion;
}
return s;
}
interface ComponentOptions {
name?: string;
version?: number;
}
type ComponentDecorator = (target: ComponentConstructor, ctx: ClassDecoratorContext) => void;
export function component(target: ComponentConstructor, ctx: ClassDecoratorContext): void;
export function component(name: string): ComponentDecorator;
export function component(options: ComponentOptions): ComponentDecorator;
export function component(
nameOrTargetOrOptions: string | ComponentConstructor | ComponentOptions,
ctx?: ClassDecoratorContext,
): void | ComponentDecorator {
if (typeof nameOrTargetOrOptions === 'string') {
const name = nameOrTargetOrOptions;
return (target: ComponentConstructor) => register(name, target, 0);
}
if (typeof nameOrTargetOrOptions === 'object') {
const { name, version = 0 } = nameOrTargetOrOptions;
return (target: ComponentConstructor, ctx: ClassDecoratorContext) =>
register(name ?? String(ctx.name), target, version);
}
// Used as bare @component
register(String(ctx!.name), nameOrTargetOrOptions, 0);
}