refactor: api surface change

This commit is contained in:
hypercross 2026-04-04 20:50:17 +08:00
parent 467a56bd84
commit 6e1c42015f
3 changed files with 62 additions and 26 deletions

View File

@ -1,23 +1,23 @@
import { ReadonlySignal, Signal } from '@preact/signals-core'; import { ReadonlySignal, Signal } from '@preact/signals-core';
import type { CommandSchema, CommandRegistry, PromptEvent } from '@/utils/command'; import type {CommandSchema, CommandRegistry, PromptEvent, CommandRunnerContextExport} from '@/utils/command';
import type { MutableSignal } from '@/utils/mutable-signal'; import type { MutableSignal } from '@/utils/mutable-signal';
import { createGameContext } from './game'; import {createGameContext, IGameContext} from './game';
export type GameHostStatus = 'created' | 'running' | 'disposed'; export type GameHostStatus = 'created' | 'running' | 'disposed';
export interface GameModule<TState extends Record<string, unknown>> { export interface GameModule<TState extends Record<string, unknown>> {
registry: CommandRegistry<MutableSignal<TState>>; registry: CommandRegistry<IGameContext<TState>>;
createInitialState: () => TState; createInitialState: () => TState;
} }
export class GameHost<TState extends Record<string, unknown>> { export class GameHost<TState extends Record<string, unknown>> {
readonly state: ReadonlySignal<TState>; readonly context: IGameContext<TState>;
readonly commands: ReturnType<typeof createGameContext<TState>>['commands'];
readonly status: ReadonlySignal<GameHostStatus>; readonly status: ReadonlySignal<GameHostStatus>;
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>; readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
readonly activePromptPlayer: ReadonlySignal<string | null>; readonly activePromptPlayer: ReadonlySignal<string | null>;
private _state: MutableSignal<TState>; private _state: MutableSignal<TState>;
private _commands: CommandRunnerContextExport<IGameContext<TState>>;
private _status: Signal<GameHostStatus>; private _status: Signal<GameHostStatus>;
private _activePromptSchema: Signal<CommandSchema | null>; private _activePromptSchema: Signal<CommandSchema | null>;
private _activePromptPlayer: Signal<string | null>; private _activePromptPlayer: Signal<string | null>;
@ -26,17 +26,14 @@ export class GameHost<TState extends Record<string, unknown>> {
private _isDisposed = false; private _isDisposed = false;
constructor( constructor(
registry: CommandRegistry<MutableSignal<TState>>, registry: CommandRegistry<IGameContext<TState>>,
createInitialState: () => TState, createInitialState: () => TState,
) { ) {
this._createInitialState = createInitialState; this._createInitialState = createInitialState;
this._eventListeners = new Map(); this._eventListeners = new Map();
const initialState = createInitialState(); const initialState = createInitialState();
const context = createGameContext(registry, initialState); this.context = createGameContext(registry, initialState);
this._state = context.state;
this.commands = context.commands;
this._status = new Signal<GameHostStatus>('created'); this._status = new Signal<GameHostStatus>('created');
this.status = this._status; this.status = this._status;
@ -47,7 +44,8 @@ export class GameHost<TState extends Record<string, unknown>> {
this._activePromptPlayer = new Signal<string | null>(null); this._activePromptPlayer = new Signal<string | null>(null);
this.activePromptPlayer = this._activePromptPlayer; this.activePromptPlayer = this._activePromptPlayer;
this.state = this._state; this._state = this.context._state;
this._commands = this.context._commands;
this._setupPromptTracking(); this._setupPromptTracking();
} }
@ -55,13 +53,13 @@ export class GameHost<TState extends Record<string, unknown>> {
private _setupPromptTracking() { private _setupPromptTracking() {
let currentPromptEvent: PromptEvent | null = null; let currentPromptEvent: PromptEvent | null = null;
this.commands.on('prompt', (e) => { this._commands.on('prompt', (e) => {
currentPromptEvent = e as PromptEvent; currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema; this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer; this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
}); });
this.commands.on('promptEnd', () => { this._commands.on('promptEnd', () => {
currentPromptEvent = null; currentPromptEvent = null;
this._activePromptSchema.value = null; this._activePromptSchema.value = null;
this._activePromptPlayer.value = null; this._activePromptPlayer.value = null;
@ -76,7 +74,7 @@ export class GameHost<TState extends Record<string, unknown>> {
if (this._isDisposed) { if (this._isDisposed) {
return 'GameHost is disposed'; return 'GameHost is disposed';
} }
return this.commands._tryCommit(input); return this._commands._tryCommit(input);
} }
/** /**
@ -100,14 +98,14 @@ export class GameHost<TState extends Record<string, unknown>> {
throw new Error('GameHost is disposed'); throw new Error('GameHost is disposed');
} }
this.commands._cancel(); this._commands._cancel();
const initialState = this._createInitialState(); const initialState = this._createInitialState();
this._state.value = initialState as any; this._state.value = initialState as any;
// Start the setup command but don't wait for it to complete // Start the setup command but don't wait for it to complete
// The command will run in the background and prompt for input // The command will run in the background and prompt for input
this.commands.run(setupCommand).catch(() => { this._commands.run(setupCommand).catch(() => {
// Command may be cancelled or fail, which is expected // Command may be cancelled or fail, which is expected
}); });
@ -121,7 +119,7 @@ export class GameHost<TState extends Record<string, unknown>> {
} }
this._isDisposed = true; this._isDisposed = true;
this.commands._cancel(); this._commands._cancel();
this._status.value = 'disposed'; this._status.value = 'disposed';
// Emit dispose event BEFORE clearing listeners // Emit dispose event BEFORE clearing listeners

View File

@ -2,7 +2,7 @@ import {MutableSignal, mutableSignal} from "@/utils/mutable-signal";
import { import {
Command, Command,
CommandRegistry, CommandResult, CommandRegistry, CommandResult,
CommandRunnerContext, CommandRunnerContext, CommandRunnerContextExport,
CommandSchema, CommandSchema,
createCommandRegistry, createCommandRegistry,
createCommandRunnerContext, createCommandRunnerContext,
@ -22,7 +22,7 @@ export interface IGameContext<TState extends Record<string, unknown> = {} > {
// test only // test only
_state: MutableSignal<TState>; _state: MutableSignal<TState>;
_commands: CommandRunnerContext<IGameContext<TState>>; _commands: CommandRunnerContextExport<IGameContext<TState>>;
} }
export function createGameContext<TState extends Record<string, unknown> = {} >( export function createGameContext<TState extends Record<string, unknown> = {} >(
@ -31,7 +31,7 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
): IGameContext<TState> { ): IGameContext<TState> {
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState; const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
const state = mutableSignal(stateValue); const state = mutableSignal(stateValue);
let commands: CommandRunnerContext<IGameContext<TState>> = null as any; let commands: CommandRunnerContextExport<IGameContext<TState>> = null as any;
const context: IGameContext<TState> = { const context: IGameContext<TState> = {
get value(): TState { get value(): TState {
@ -82,18 +82,30 @@ export function createGameCommandRegistry<TState extends Record<string, unknown>
return createCommandRegistry<IGameContext<TState>>(); return createCommandRegistry<IGameContext<TState>>();
} }
export function registerGameCommand<TState extends Record<string, unknown> = {}, TResult = unknown>( type CmdFunc<TState extends Record<string, unknown> = {}> = (this: IGameContext<TState>, ...args: any[]) => Promise<unknown>;
export function registerGameCommand<TState extends Record<string, unknown> = {}, TFunc extends CmdFunc<TState> = CmdFunc<TState>>(
registry: CommandRegistry<IGameContext<TState>>, registry: CommandRegistry<IGameContext<TState>>,
schema: CommandSchema | string, schema: CommandSchema | string,
run: (this: IGameContext<TState>, ...args: any[]) => Promise<TResult> run: TFunc
) { ) {
const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
registerCommand(registry, { registerCommand(registry, {
schema: typeof schema === 'string' ? parseCommandSchema(schema) : schema, schema: parsedSchema,
async run(this: CommandRunnerContext<IGameContext<TState>>, command: Command){ async run(this: CommandRunnerContext<IGameContext<TState>>, command: Command){
const params = command.params; const params = command.params;
return await run.call(this.context, ...params); return await run.call(this.context, ...params);
}, },
}); });
return function(game: IGameContext<TState>, ...args: Parameters<TFunc>){
return game.runParsed({
options: {},
params: args,
flags: {},
name: parsedSchema.name,
});
}
} }
export { GameHost, createGameHost } from './game-host'; export { GameHost, createGameHost } from './game-host';

View File

@ -5,10 +5,36 @@ import { applyCommandSchema } from './command-validate';
import { parseCommandSchema } from './schema-parse'; import { parseCommandSchema } from './schema-parse';
import {AsyncQueue} from "@/utils/async-queue"; import {AsyncQueue} from "@/utils/async-queue";
export type CommandRegistry<TContext> = Map<string, CommandRunner<TContext, unknown>>; type CanRunParsed = {
runParsed<T=unknown>(command: Command): Promise<CommandResult<T>>,
}
type CmdFunc<TContext> = (this: TContext, ...args: any[]) => Promise<unknown>;
export class CommandRegistry<TContext> extends Map<string, CommandRunner<TContext>>{
register<TFunc extends CmdFunc<TContext> = CmdFunc<TContext>>(schema: CommandSchema | string, run: TFunc) {
const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
registerCommand(this, {
schema: parsedSchema,
async run(this: CommandRunnerContext<TContext>, command: Command){
const params = command.params;
return await run.call(this.context, ...params);
},
});
type TResult = TFunc extends (this: TContext, ...args: any[]) => Promise<infer X> ? X : null;
return function(ctx: TContext & CanRunParsed, ...args: Parameters<TFunc>){
return ctx.runParsed({
options: {},
params: args,
flags: {},
name: parsedSchema.name,
}) as Promise<CommandResult<TResult>>;
}
}
}
export function createCommandRegistry<TContext>(): CommandRegistry<TContext> { export function createCommandRegistry<TContext>(): CommandRegistry<TContext> {
return new Map(); return new CommandRegistry();
} }
export function registerCommand<TContext, TResult>( export function registerCommand<TContext, TResult>(
@ -137,7 +163,7 @@ export function createCommandRunnerContext<TContext>(
registry, registry,
context, context,
run: <T=unknown>(input: string) => runCommandWithContext(runnerCtx, input) as Promise<CommandResult<T>>, run: <T=unknown>(input: string) => runCommandWithContext(runnerCtx, input) as Promise<CommandResult<T>>,
runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command), runParsed: <T=unknown>(command: Command) => runCommandParsedWithContext(runnerCtx, command) as Promise<CommandResult<T>>,
prompt, prompt,
on, on,
off, off,