import { Cell, NAME, Recipe, RecipeMeta, RuntimeProgram, TYPE, } from "@commontools/runner"; import { charmId, CharmManager } from "../manager.ts"; import { nameSchema, processSchema } from "@commontools/runner/schemas"; import { CellPath, compileProgram, resolveCellPath } from "./utils.ts"; import { injectUserCode } from "../iframe/static.ts"; import { buildFullRecipe, getIframeRecipe, IFrameRecipe, } from "../iframe/recipe.ts"; interface CharmCellIo { get(path?: CellPath): Promise; set(value: unknown, path?: CellPath): Promise; getCell(): Promise>; } type CharmPropIoType = "result" | "input"; class CharmPropIo implements CharmCellIo { #cc: CharmController; #type: CharmPropIoType; constructor(cc: CharmController, type: CharmPropIoType) { this.#cc = cc; this.#type = type; } async get(path?: CellPath) { const targetCell = await this.#getTargetCell(); return resolveCellPath(targetCell, path ?? []); } getCell(): Promise> { return this.#getTargetCell(); } async set(value: unknown, path?: CellPath) { const manager = this.#cc.manager(); const targetCell = await this.#getTargetCell(); await manager.runtime.editWithRetry((tx) => { // Build the path with transaction context let txCell = targetCell.withTx(tx); for (const segment of (path ?? [])) { txCell = txCell.key(segment as keyof unknown) as Cell; } txCell.set(value); }); await manager.runtime.idle(); await manager.synced(); } #getTargetCell(): Promise> { if (this.#type === "input") { return this.#cc.manager().getArgument(this.#cc.getCell()); } else if (this.#type === "result") { return Promise.resolve(this.#cc.manager().getResult(this.#cc.getCell())); } throw new Error(`Unknown property type "${this.#type}"`); } } export class CharmController { #cell: Cell; #manager: CharmManager; readonly id: string; input: CharmCellIo; result: CharmCellIo; constructor(manager: CharmManager, cell: Cell) { const id = charmId(cell); if (!id) { throw new Error("Could not get an ID from a Cell"); } this.id = id; this.#manager = manager; this.#cell = cell; this.input = new CharmPropIo(this, "input"); this.result = new CharmPropIo(this, "result"); } name(): string | undefined { return this.#cell.asSchema(nameSchema).get()[NAME]; } getCell(): Cell { return this.#cell; } async setInput(input: object): Promise { const recipe = await this.getRecipe(); // Use setup/start so we can update inputs without forcing reschedule await execute(this.#manager, this.id, recipe, input, { start: true }); } async getRecipe(): Promise { const recipeId = getRecipeIdFromCharm(this.#cell); const runtime = this.#manager.runtime; const recipe = await runtime.recipeManager.loadRecipe( recipeId, this.#manager.getSpace(), ); return recipe; } async getRecipeMeta(): Promise { const recipeId = getRecipeIdFromCharm(this.#cell); const space = this.#manager.getSpace(); // Ensure the recipe is loaded first - this populates the metadata await this.#manager.runtime.recipeManager.loadRecipe(recipeId, space); return this.#manager.runtime.recipeManager.loadRecipeMeta(recipeId, space); } // Returns an `IFrameRecipe` for the charm, or `undefined` // if not an iframe recipe. getIframeRecipe(): IFrameRecipe | undefined { return getIframeRecipe(this.#cell, this.#manager.runtime).iframe; } async setRecipe(program: RuntimeProgram): Promise { const recipe = await compileProgram(this.#manager, program); await execute(this.#manager, this.id, recipe); } // Update charm's recipe with usercode for an iframe recipe. // Throws if recipe is not an iframe recipe. async setIframeRecipe(src: string): Promise { const iframeRecipe = getIframeRecipe(this.#cell, this.#manager.runtime); if (!iframeRecipe.iframe) { throw new Error(`Expected charm "${this.id}" to be an iframe recipe.`); } iframeRecipe.iframe.src = injectUserCode(src); const recipe = await compileProgram( this.#manager, buildFullRecipe(iframeRecipe.iframe), ); await execute(this.#manager, this.id, recipe); } async readingFrom(): Promise { const cells = await this.#manager.getReadingFrom(this.#cell); return cells.map((cell) => new CharmController(this.#manager, cell)); } async readBy(): Promise { const cells = await this.#manager.getReadByCharms(this.#cell); return cells.map((cell) => new CharmController(this.#manager, cell)); } manager(): CharmManager { return this.#manager; } } async function execute( manager: CharmManager, charmId: string, recipe: Recipe, input?: object, options?: { start?: boolean }, ): Promise { await manager.runWithRecipe(recipe, charmId, input, options); await manager.runtime.idle(); await manager.synced(); } export const getRecipeIdFromCharm = (charm: Cell): string => { const sourceCell = charm.getSourceCell(processSchema); if (!sourceCell) throw new Error("charm missing source cell"); return sourceCell.get()?.[TYPE]; };