122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
import type {
|
|
EngineCapabilities,
|
|
EngineRequest,
|
|
EngineResponse,
|
|
EngineCommand,
|
|
} from "../../../protocol/engine";
|
|
import type { ErrorReport } from "../../../protocol/error";
|
|
import type { SceneSnapshotIR } from "../../../protocol/scene-ir";
|
|
|
|
interface PendingRequest {
|
|
resolve: (response: EngineResponse) => void;
|
|
reject: (reason: ErrorReport) => void;
|
|
timer: ReturnType<typeof setTimeout>;
|
|
}
|
|
|
|
export interface EngineClientOptions {
|
|
timeoutMs?: number;
|
|
workerFactory?: () => Worker;
|
|
}
|
|
|
|
const defaultWorkerFactory = () =>
|
|
new Worker(new URL("../workers/engine.worker.ts", import.meta.url), { type: "module" });
|
|
|
|
export class EngineClient {
|
|
private readonly timeoutMs: number;
|
|
private readonly workerFactory: () => Worker;
|
|
private worker: Worker | null = null;
|
|
private requestCounter = 0;
|
|
private pending = new Map<string, PendingRequest>();
|
|
private _revision = 0;
|
|
|
|
constructor(options: EngineClientOptions = {}) {
|
|
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
this.workerFactory = options.workerFactory ?? defaultWorkerFactory;
|
|
}
|
|
|
|
get revision(): number {
|
|
return this._revision;
|
|
}
|
|
|
|
start(): void {
|
|
if (this.worker) return;
|
|
const worker = this.workerFactory();
|
|
worker.onmessage = (event: MessageEvent<EngineResponse>) => this.handleResponse(event.data);
|
|
worker.onerror = () => this.failPending(this.report("WORKER_TERMINATED", "EngineWorker 发生异常", true));
|
|
this.worker = worker;
|
|
}
|
|
|
|
async init(protocolVersion = 1): Promise<EngineCapabilities> {
|
|
const response = await this.request({ type: "init", protocolVersion });
|
|
if (!response.capabilities) throw this.report("WASM_INIT_FAILED", "EngineWorker 未返回能力信息", false);
|
|
return response.capabilities;
|
|
}
|
|
|
|
async getSceneSnapshot(): Promise<SceneSnapshotIR> {
|
|
const response = await this.request({ type: "getSceneSnapshot", sinceRevision: this._revision });
|
|
if (!response.snapshot) throw this.report("INVALID_ARGUMENT", "EngineWorker 未返回 SceneIR", false);
|
|
return response.snapshot;
|
|
}
|
|
|
|
async setFrame(frame: number): Promise<void> {
|
|
await this.request({ type: "setFrame", frame });
|
|
}
|
|
|
|
async setObjectVisibility(objectId: string, visible: boolean): Promise<void> {
|
|
await this.request({ type: "setObjectVisibility", objectId, visible });
|
|
}
|
|
|
|
restart(): void {
|
|
this.failPending(this.report("WORKER_TERMINATED", "EngineWorker 已重启", true));
|
|
this.worker?.terminate();
|
|
this.worker = null;
|
|
this._revision = 0;
|
|
this.start();
|
|
}
|
|
|
|
terminate(): void {
|
|
this.failPending(this.report("WORKER_TERMINATED", "EngineWorker 已关闭", true));
|
|
this.worker?.terminate();
|
|
this.worker = null;
|
|
}
|
|
|
|
private request(command: EngineCommand): Promise<EngineResponse> {
|
|
this.start();
|
|
const worker = this.worker;
|
|
if (!worker) return Promise.reject(this.report("WASM_INIT_FAILED", "无法创建 EngineWorker", false));
|
|
|
|
const requestId = `engine-${++this.requestCounter}`;
|
|
const message: EngineRequest = { requestId, expectedRevision: this._revision, command };
|
|
return new Promise<EngineResponse>((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this.pending.delete(requestId);
|
|
reject(this.report("WORKER_TERMINATED", `请求超时: ${command.type}`, true));
|
|
}, this.timeoutMs);
|
|
this.pending.set(requestId, { resolve, reject, timer });
|
|
worker.postMessage(message);
|
|
});
|
|
}
|
|
|
|
private handleResponse(response: EngineResponse): void {
|
|
const pending = this.pending.get(response.requestId);
|
|
if (!pending) return;
|
|
this.pending.delete(response.requestId);
|
|
clearTimeout(pending.timer);
|
|
this._revision = response.revision;
|
|
if (response.ok) pending.resolve(response);
|
|
else pending.reject(response.reports?.[0] ?? this.report("INVALID_ARGUMENT", "EngineWorker 请求失败", true));
|
|
}
|
|
|
|
private failPending(report: ErrorReport): void {
|
|
for (const pending of this.pending.values()) {
|
|
clearTimeout(pending.timer);
|
|
pending.reject(report);
|
|
}
|
|
this.pending.clear();
|
|
}
|
|
|
|
private report(code: ErrorReport["code"], message: string, recoverable: boolean): ErrorReport {
|
|
return { code, severity: "error", message, recoverable };
|
|
}
|
|
}
|