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 type { CommandSchema, CommandRegistry, PromptEvent } from '@/utils/command';
import type {CommandSchema, CommandRegistry, PromptEvent, CommandRunnerContextExport} from '@/utils/command';
import type { MutableSignal } from '@/utils/mutable-signal';
import { createGameContext } from './game';
import {createGameContext, IGameContext} from './game';
export type GameHostStatus = 'created' | 'running' | 'disposed';
export interface GameModule<TState extends Record<string, unknown>> {
registry: CommandRegistry<MutableSignal<TState>>;
registry: CommandRegistry<IGameContext<TState>>;
createInitialState: () => TState;
}
export class GameHost<TState extends Record<string, unknown>> {
readonly state: ReadonlySignal<TState>;
readonly commands: ReturnType<typeof createGameContext<TState>>['commands'];
readonly context: IGameContext<TState>;
readonly status: ReadonlySignal<GameHostStatus>;
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
readonly activePromptPlayer: ReadonlySignal<string | null>;
private _state: MutableSignal<TState>;
private _commands: CommandRunnerContextExport<IGameContext<TState>>;
private _status: Signal<GameHostStatus>;
private _activePromptSchema: Signal<CommandSchema | null>;
private _activePromptPlayer: Signal<string | null>;
@ -26,17 +26,14 @@ export class GameHost<TState extends Record<string, unknown>> {
private _isDisposed = false;
constructor(
registry: CommandRegistry<MutableSignal<TState>>,
registry: CommandRegistry<IGameContext<TState>>,
createInitialState: () => TState,
) {
this._createInitialState = createInitialState;
this._eventListeners = new Map();
const initialState = createInitialState();
const context = createGameContext(registry, initialState);
this._state = context.state;
this.commands = context.commands;
this.context = createGameContext(registry, initialState);
this._status = new Signal<GameHostStatus>('created');
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 = this._activePromptPlayer;
this.state = this._state;
this._state = this.context._state;
this._commands = this.context._commands;
this._setupPromptTracking();
}
@ -55,13 +53,13 @@ export class GameHost<TState extends Record<string, unknown>> {
private _setupPromptTracking() {
let currentPromptEvent: PromptEvent | null = null;
this.commands.on('prompt', (e) => {
this._commands.on('prompt', (e) => {
currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
});
this.commands.on('promptEnd', () => {
this._commands.on('promptEnd', () => {
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
@ -76,7 +74,7 @@ export class GameHost<TState extends Record<string, unknown>> {
if (this._isDisposed) {
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');
}
this.commands._cancel();
this._commands._cancel();
const initialState = this._createInitialState();
this._state.value = initialState as any;
// Start the setup command but don't wait for it to complete
// 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
});
@ -121,7 +119,7 @@ export class GameHost<TState extends Record<string, unknown>> {
}
this._isDisposed = true;
this.commands._cancel();
this._commands._cancel();
this._status.value = 'disposed';
// Emit dispose event BEFORE clearing listeners

View File

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

View File

@ -5,10 +5,36 @@ import { applyCommandSchema } from './command-validate';
import { parseCommandSchema } from './schema-parse';
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> {
return new Map();
return new CommandRegistry();
}
export function registerCommand<TContext, TResult>(
@ -137,7 +163,7 @@ export function createCommandRunnerContext<TContext>(
registry,
context,
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,
on,
off,