101 lines
3.4 KiB
TypeScript
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);
|
|
}
|