/** * RuntimeClient - Main thread controller for the worker-based Runtime * * This class manages a web worker that runs the Runtime, providing a clean API * for interacting with cells across the worker boundary. */ import type { DID, Identity } from "@commontools/identity"; import type { JSONSchema, RuntimeTelemetryMarkerResult, SchedulerGraphSnapshot, } from "@commontools/runner/shared"; import { Program } from "@commontools/js-compiler/interface"; import { CellHandle } from "./cell-handle.ts"; import { type CellRef, ConsoleNotification, ErrorNotification, InitializationData, JSONValue, type LoggerCountsData, type LoggerFlagsData, type LoggerMetadata, type LoggerTimingData, type LogLevel, NavigateRequestNotification, RequestType, TelemetryNotification, } from "./protocol/mod.ts"; import { NameSchema } from "@commontools/runner/schemas"; import { RuntimeTransport } from "./client/transport.ts"; import { EventEmitter } from "./client/emitter.ts"; import { InitializedRuntimeConnection, RuntimeConnection, } from "./client/connection.ts"; import { PageHandle } from "./page-handle.ts"; export interface RuntimeClientOptions extends Omit { apiUrl: URL; identity: Identity; spaceIdentity?: Identity; } export type RuntimeClientEvents = { console: [ConsoleNotification]; navigaterequest: [{ cell: CellHandle }]; error: [ErrorNotification]; telemetry: [RuntimeTelemetryMarkerResult]; }; export const $conn = Symbol("$request"); /** * RuntimeClient provides a main-thread interface to a Runtime running elsewhere. */ export class RuntimeClient extends EventEmitter { #conn: InitializedRuntimeConnection; private constructor( conn: InitializedRuntimeConnection, _options: RuntimeClientOptions, ) { super(); this.#conn = conn; this.#conn.on("console", this._onConsole); this.#conn.on("navigaterequest", this._onNavigateRequest); this.#conn.on("error", this._onError); this.#conn.on("telemetry", this._onTelemetry); } static async initialize( transport: RuntimeTransport, options: RuntimeClientOptions, ): Promise { const initialized = await (new RuntimeConnection(transport)).initialize({ apiUrl: options.apiUrl.toString(), identity: options.identity.serialize(), spaceIdentity: options.spaceIdentity?.serialize(), spaceDid: options.spaceDid, spaceName: options.spaceName, experimental: options.experimental, }); return new RuntimeClient(initialized, options); } getCellFromRef( ref: CellRef, ): CellHandle { return new CellHandle(this, ref); } // TODO(unused) // Currently unused in shell, but a PieceManager-like layer // could be built using this async getCell( space: DID, cause: JSONValue, schema?: JSONSchema, ): Promise> { const response = await this.#conn.request({ type: RequestType.GetCell, space, cause, schema, }); return new CellHandle(this, response.cell); } async getHomeSpaceCell(): Promise> { const response = await this.#conn.request({ type: RequestType.GetHomeSpaceCell, }); return new CellHandle(this, response.cell); } /** * Ensure the home space's default pattern is running and return a CellHandle to it. * This starts the pattern if needed and waits for it to be ready. */ async ensureHomePatternRunning(): Promise> { const response = await this.#conn.request< RequestType.EnsureHomePatternRunning >({ type: RequestType.EnsureHomePatternRunning, }); return new CellHandle(this, response.cell); } // TODO(unused) async idle(): Promise { await this.#conn.request({ type: RequestType.Idle }); } async createPage( input: string | URL | Program, options?: { argument?: JSONValue; run?: boolean }, ): Promise> { const source = input instanceof URL ? { url: input.href } : typeof input === "string" ? { program: { main: "/main.tsx", files: [{ name: "/main.tsx", contents: input, }], }, } : { program: input }; const response = await this.#conn.request< RequestType.PageCreate >({ type: RequestType.PageCreate, source, argument: options?.argument, run: options?.run, }); return new PageHandle(this, response.page); } async getSpaceRootPattern(): Promise> { const response = await this.#conn.request< RequestType.GetSpaceRootPattern >({ type: RequestType.GetSpaceRootPattern, }); return new PageHandle(this, response.page); } async recreateSpaceRootPattern(): Promise> { const response = await this.#conn.request< RequestType.RecreateSpaceRootPattern >({ type: RequestType.RecreateSpaceRootPattern, }); return new PageHandle(this, response.page); } async getPage( pageId: string, runIt?: boolean, ): Promise | null> { const response = await this.#conn.request({ type: RequestType.PageGet, pageId: pageId, runIt, }); if (!response) return null; return new PageHandle(this, response.page); } async removePage(pageId: string): Promise { const res = await this.#conn.request({ type: RequestType.PageRemove, pageId: pageId, }); return res.value; } /** * Get the pieces list cell. * Subscribe to this cell to get reactive updates of all pieces in the space. */ async getPiecesListCell(): Promise> { const response = await this.#conn.request({ type: RequestType.PageGetAll, }); return new CellHandle(this, response.cell); } /** * Wait for the PieceManager to be synced with storage. */ async synced(): Promise { await this.#conn.request({ type: RequestType.PageSynced, }); } async getGraphSnapshot(): Promise { const res = await this.#conn.request({ type: RequestType.GetGraphSnapshot, }); return res.snapshot; } async setPullMode(pullMode: boolean): Promise { await this.#conn.request({ type: RequestType.SetPullMode, pullMode, }); } async getLoggerCounts(): Promise<{ counts: LoggerCountsData; metadata: LoggerMetadata; timing: LoggerTimingData; flags: LoggerFlagsData; }> { const res = await this.#conn.request({ type: RequestType.GetLoggerCounts, }); return { counts: res.counts, metadata: res.metadata, timing: res.timing, flags: res.flags, }; } /** * Set log level for a logger in the worker. * @param level - The log level to set * @param loggerName - Optional logger name. If not provided, sets level for all loggers. */ async setLoggerLevel(level: LogLevel, loggerName?: string): Promise { await this.#conn.request({ type: RequestType.SetLoggerLevel, level, loggerName, }); } /** * Enable or disable a logger in the worker. * @param enabled - Whether to enable or disable the logger * @param loggerName - Optional logger name. If not provided, sets enabled for all loggers. */ async setLoggerEnabled(enabled: boolean, loggerName?: string): Promise { await this.#conn.request({ type: RequestType.SetLoggerEnabled, enabled, loggerName, }); } /** * Enable or disable telemetry data emission from the worker. * When disabled, telemetry events will not be sent over IPC. * @param enabled - Whether to enable or disable telemetry */ async setTelemetryEnabled(enabled: boolean): Promise { await this.#conn.request({ type: RequestType.SetTelemetryEnabled, enabled, }); } /** * Reset logger baselines for both counts and timing in the worker. * After calling this, loggers will track deltas from this baseline. */ async resetLoggerBaselines(): Promise { await this.#conn.request({ type: RequestType.ResetLoggerBaselines, }); } async dispose(): Promise { await this.#conn.dispose(); } async [Symbol.asyncDispose]() { await this.dispose(); } [$conn](): InitializedRuntimeConnection { return this.#conn; } private _onConsole = (data: ConsoleNotification): void => { this.emit("console", data); }; private _onNavigateRequest = (data: NavigateRequestNotification): void => { this.emit("navigaterequest", { cell: new CellHandle(this, data.targetCellRef), }); }; private _onError = (data: ErrorNotification): void => { this.emit("error", data); }; private _onTelemetry = (data: TelemetryNotification): void => { this.emit("telemetry", data.marker); }; }