Files
workinf_Blender_Wasm/web/protocol/corrective-command-registry.ts
mes123456 a081c68c87
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Reorganize Blender Web execution around capabilities
2026-08-24 18:37:46 -04:00

117 lines
4.3 KiB
TypeScript

import {
Authorization,
CaseSurface,
CommandContext,
CorrectiveCaseRunnerError,
MainMutationCommand,
MainMutationResult,
SurfaceReceipt,
TargetKind,
runDesktopCase,
runWasmCase,
} from "./corrective-case-runner";
export const CORRECTIVE_COMMAND_REGISTRY_SCHEMA = 1 as const;
export type CommandExecutor = (command: MainMutationCommand) => MainMutationResult | Promise<MainMutationResult>;
export interface RegisteredCommand {
commandType: string;
targetKinds: readonly TargetKind[];
authorizations: readonly Exclude<Authorization, "NONE">[];
execute: CommandExecutor;
}
export class CommandRegistryError extends Error {
readonly code: "REGISTRY_INVALID" | "REGISTRY_DUPLICATE";
constructor(code: "REGISTRY_INVALID" | "REGISTRY_DUPLICATE", message: string) {
super(`${code}: ${message}`);
this.name = "CommandRegistryError";
this.code = code;
}
}
export interface DispatchOptions {
command: unknown;
mainRevision: number;
}
const COMMAND_TYPE = /^[a-z][A-Za-z0-9]{0,127}$/u;
const TARGET_KINDS = new Set<TargetKind>(["ASSET_LIBRARY", "DATABLOCK", "MESH", "MODIFIER", "SCENE"]);
const AUTHORIZATIONS = new Set<Exclude<Authorization, "NONE">>(["USER_EDIT", "SYSTEM"]);
function unique<T>(values: readonly T[]): boolean {
return new Set(values).size === values.length;
}
function validateDefinition(definition: RegisteredCommand): void {
if (!definition || typeof definition !== "object" || !COMMAND_TYPE.test(definition.commandType)) {
throw new CommandRegistryError("REGISTRY_INVALID", "commandType is invalid");
}
if (!Array.isArray(definition.targetKinds) || definition.targetKinds.length === 0 ||
!unique(definition.targetKinds) || definition.targetKinds.some((kind) => !TARGET_KINDS.has(kind))) {
throw new CommandRegistryError("REGISTRY_INVALID", "targetKinds must contain unique supported targets");
}
if (!Array.isArray(definition.authorizations) || definition.authorizations.length === 0 ||
!unique(definition.authorizations) || definition.authorizations.some((authorization) => !AUTHORIZATIONS.has(authorization))) {
throw new CommandRegistryError("REGISTRY_INVALID", "authorizations must contain unique mutating authorities");
}
if (typeof definition.execute !== "function") {
throw new CommandRegistryError("REGISTRY_INVALID", "execute must be a function");
}
}
function contextError(context: CommandContext, message: string): CorrectiveCaseRunnerError {
return new CorrectiveCaseRunnerError("INVALID_CONTEXT", `${context.targetKind}: ${message}`);
}
export class CorrectiveCommandRegistry {
readonly schemaVersion = CORRECTIVE_COMMAND_REGISTRY_SCHEMA;
private readonly commands = new Map<string, RegisteredCommand>();
register(definition: RegisteredCommand): this {
validateDefinition(definition);
if (this.commands.has(definition.commandType)) {
throw new CommandRegistryError("REGISTRY_DUPLICATE", `commandType ${definition.commandType} is already registered`);
}
this.commands.set(definition.commandType, {
commandType: definition.commandType,
targetKinds: [...definition.targetKinds],
authorizations: [...definition.authorizations],
execute: definition.execute,
});
return this;
}
has(commandType: string): boolean {
return this.commands.has(commandType);
}
list(): string[] {
return [...this.commands.keys()].sort();
}
async dispatch(surface: CaseSurface, options: DispatchOptions): Promise<SurfaceReceipt> {
const run = surface === "DESKTOP" ? runDesktopCase : runWasmCase;
return run({
command: options.command,
mainRevision: options.mainRevision,
execute: async (command) => {
const definition = this.commands.get(command.commandType);
if (!definition) {
throw new CorrectiveCaseRunnerError("COMMAND_UNSUPPORTED", `commandType ${command.commandType} is not registered`);
}
if (!definition.targetKinds.includes(command.context.targetKind)) {
throw contextError(command.context, "target is not authorized for this command");
}
if (!definition.authorizations.includes(command.context.authorization as Exclude<Authorization, "NONE">)) {
throw contextError(command.context, "authorization is not allowed for this command");
}
return definition.execute(command);
},
});
}
}