docs: unify commands and orchestrators in bgm-engine

Messages are a discriminated union with generic command/result types;
the command host is dropped for a handler registry; commands and
orchestrators are the same async shape taking a cancellable RunContext.
This commit is contained in:
2026-08-10 18:40:30 +08:00
parent 3167d26bd6
commit 81c115cb4d
2 changed files with 122 additions and 85 deletions
+27 -20
View File
@@ -8,18 +8,20 @@ that runs against the tabletop state store and render layer.
This doc covers **command execution**: the async lifecycle, run contexts, and
tap interaction. The message layer above this — how commands are *declared*
and *fired* (triggers, orchestrators, the message queue) — is specified in
[`bgm-engine.md`](./bgm-engine.md). The command host is the `@tts/tabletop`
implementation of the engine's `CommandHost` contract.
[`bgm-engine.md`](./bgm-engine.md). Commands are async functions registered
with the engine's handler registry; `@tts/tabletop` provides the concrete
commands that mutate the tabletop store and render layer.
## 1. async commands
A command is an async function that returns a result. Every command ends in
one of three states:
one of three states, emitted as a message discriminated on the type suffix
(see `bgm-engine.md` §3):
- `ok` — completed normally.
- `cancel` — interrupted (a newer command superseded it, the user skipped, the
surface was disabled). **Not a failure.**
- `error` — genuinely failed (asset missing, bad path, a thrown exception).
- `:done` — completed normally.
- `:cancel` — interrupted (a newer command superseded it, the user skipped,
the surface was disabled). **Not a failure.**
- `:error` — genuinely failed (asset missing, bad path, a thrown exception).
`cancel` is distinct from `error`: a superseded or skipped command stops
cleanly, while a broken command surfaces loudly. The runtime treats them
@@ -27,10 +29,10 @@ differently — a script that is superseded unwinds without alarming the player,
but an `error` is reported.
```ts
type CommandResult =
| { status: 'ok' }
| { status: 'cancel' }
| { status: 'error'; error: Error };
type CommandResult<Name extends string, R = void> =
| { type: `${Name}:done`; data: R }
| { type: `${Name}:cancel` }
| { type: `${Name}:error`; error: Error };
```
## 2. run contexts
@@ -71,16 +73,18 @@ lifecycle.
**Supersede groups** cancel a running command when another in the same group
starts. A `focus` command belongs to a `camera` group, so a second `focus`
cancels the first.
cancels the first. A superseded command's `signal` is aborted, and it emits
`:cancel`.
A command is an async function taking the `RunContext` (with its `args`):
```ts
interface Command {
id: string;
supersede?: string; // group; starting one cancels others in it
execute(ctx: CommandContext): Promise<CommandResult>;
}
type Command<Args, Result> = (ctx: RunContext & { args: Args }) => Promise<Result>;
```
The engine wraps it: it builds the context from the message, runs the function,
and emits `:done` on resolve, `:cancel` on abort, `:error` on throw.
## 4. tap interaction
Only tap interaction is supported. A tap on a part is detected and reported to
@@ -117,12 +121,15 @@ Rules:
Commands subscribe to the tap stream via the context and unsubscribe on
cancel, so a cancelled `wait: tap` never leaks a handler.
## 5. command context
## 5. run context
The context a command receives is the handle to everything it can affect:
The context a command receives is the handle to everything it can affect. The
engine defines the base `RunContext` (see `bgm-engine.md` §5): `signal`
(cancellation), `emit`, `wait`, and `enableTrigger`/`disableTrigger`. Tabletop
extends it with the handles commands need to mutate the board:
```ts
interface CommandContext {
interface TabletopRunContext extends RunContext {
pkg: Package;
store: TabletopStore; // movePart, setPart, enableSurface, ...
onTap(handler: (e: TapEvent) => void): () => void; // returns unsubscribe