From fd77a6fc4dfa8a318c1d1fbc47452261e85e54c9 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Mon, 21 Apr 2025 15:15:10 -0400 Subject: [PATCH 01/11] wip combining woohoo - so close - existing works, but new fail more cleanup more wip --- background-charm-service/cast-admin.ts | 4 +- background-charm-service/src/worker.ts | 4 +- charm/src/commands.ts | 6 +- charm/src/iframe/recipe.ts | 39 ++- charm/src/imagine.ts | 8 +- charm/src/iterate.ts | 14 +- charm/src/manager.ts | 56 ++-- charm/src/workflow.ts | 9 +- cli/cast-recipe.ts | 4 +- cli/main.ts | 4 +- jumble/src/services/spellbook.ts | 10 +- .../views/spellbook/SpellbookLaunchView.tsx | 2 +- runner/src/blobby-storage.ts | 52 --- runner/src/index.ts | 16 +- runner/src/recipe-manager.ts | 316 ++++++++++++++++++ runner/src/recipe-map.ts | 77 ----- runner/src/runner.ts | 18 +- runner/test/recipes.test.ts | 6 +- 18 files changed, 425 insertions(+), 220 deletions(-) create mode 100644 runner/src/recipe-manager.ts delete mode 100644 runner/src/recipe-map.ts diff --git a/background-charm-service/cast-admin.ts b/background-charm-service/cast-admin.ts index fc00fa5bd..eed22f867 100644 --- a/background-charm-service/cast-admin.ts +++ b/background-charm-service/cast-admin.ts @@ -3,7 +3,7 @@ import { CharmManager, compileRecipe } from "@commontools/charm"; import { getCell, getEntityId, - setBobbyServerUrl, + setBlobbyServerUrl, storage, } from "@commontools/runner"; import { type DID } from "@commontools/identity"; @@ -43,7 +43,7 @@ const identity = await getIdentity( ); storage.setRemoteStorage(new URL(toolshedUrl)); -setBobbyServerUrl(toolshedUrl); +setBlobbyServerUrl(toolshedUrl); async function castRecipe() { const spaceId = BG_SYSTEM_SPACE_ID; diff --git a/background-charm-service/src/worker.ts b/background-charm-service/src/worker.ts index 884d22529..415d37189 100644 --- a/background-charm-service/src/worker.ts +++ b/background-charm-service/src/worker.ts @@ -7,7 +7,7 @@ import { isStream, onConsole, onError, - setBobbyServerUrl, + setBlobbyServerUrl, setRecipeEnvironment, storage, } from "@commontools/runner"; @@ -88,7 +88,7 @@ async function initialize( const apiUrl = new URL(toolshedUrl); // Initialize storage and remote connection storage.setRemoteStorage(apiUrl); - setBobbyServerUrl(toolshedUrl); + setBlobbyServerUrl(toolshedUrl); storage.setSigner(identity); setRecipeEnvironment({ apiUrl, diff --git a/charm/src/commands.ts b/charm/src/commands.ts index d268eeb55..1083e50f4 100644 --- a/charm/src/commands.ts +++ b/charm/src/commands.ts @@ -1,5 +1,5 @@ import { DEFAULT_MODEL_NAME, fixRecipePrompt } from "@commontools/llm"; -import { Cell, getRecipe } from "@commontools/runner"; +import { Cell, recipeManager } from "@commontools/runner"; import { Charm, CharmManager } from "./manager.ts"; import { getIframeRecipe } from "./iframe/recipe.ts"; import { extractUserCode, injectUserCode } from "./iframe/static.ts"; @@ -17,9 +17,7 @@ export const castSpellAsCharm = async ( if (recipeKey && argument) { console.log("Syncing..."); const recipeId = recipeKey.replace("spell-", ""); - await charmManager.syncRecipeBlobby(recipeId); - - const recipe = getRecipe(recipeId); + const recipe = await charmManager.syncRecipeById(recipeId); if (!recipe) return; console.log("Casting..."); diff --git a/charm/src/iframe/recipe.ts b/charm/src/iframe/recipe.ts index 4a47b5f92..7a67426e9 100644 --- a/charm/src/iframe/recipe.ts +++ b/charm/src/iframe/recipe.ts @@ -1,6 +1,6 @@ -import { JSONSchema, TYPE } from "@commontools/builder"; -import { Charm, processSchema } from "../manager.ts"; -import { Cell, getRecipe, getRecipeSrc } from "@commontools/runner"; +import { JSONSchema } from "@commontools/builder"; +import { Charm, getRecipeIdFromCharm, processSchema } from "../manager.ts"; +import { Cell, recipeManager, TYPE, getRecipe, getRecipeSrc } from "@commontools/runner"; export type IFrameRecipe = { src: string; @@ -61,16 +61,6 @@ function parseIframeRecipe(source: string): IFrameRecipe { return JSON.parse(match[1]) as IFrameRecipe; } -export const getIframeRecipe = (charm: Cell) => { - const { src, recipeId, recipe } = getRecipeFrom(charm); - try { - return { recipeId, iframe: parseIframeRecipe(src) }; - } catch (error) { - console.warn("Error parsing iframe recipe:", error); - return { recipeId, iframe: undefined }; - } -}; - export const getRecipeFrom = (charm: Cell) => { const recipeId = charm.getSourceCell(processSchema)?.get()?.[TYPE]; const recipe = getRecipe(recipeId)!; @@ -78,3 +68,26 @@ export const getRecipeFrom = (charm: Cell) => { return { recipeId, recipe, src }; }; + +export const getIframeRecipe = (charm: Cell): { + recipeId: string; + src: string; + iframe?: IFrameRecipe; +} => { + const recipeId = getRecipeIdFromCharm(charm); + if (!recipeId) { + console.warn("No recipeId found for charm", charm.getId()); + return { recipeId, src: "", iframe: undefined }; + } + const src = recipeManager.getRecipeMeta({ recipeId })?.src; + if (!src) { + console.warn("No src found for charm", charm.getId()); + return { recipeId, src, iframe: undefined }; + } + try { + return { recipeId, src, iframe: parseIframeRecipe(src) }; + } catch (error) { + console.warn("Error parsing iframe recipe:", error); + return { recipeId, src, iframe: undefined }; + } +}; diff --git a/charm/src/imagine.ts b/charm/src/imagine.ts index 4631ec12d..b04c59773 100644 --- a/charm/src/imagine.ts +++ b/charm/src/imagine.ts @@ -1,9 +1,5 @@ -import { Cell, getEntityId, isCell, isStream } from "@commontools/runner"; -import { isObj } from "@commontools/utils"; -import { JSONSchema } from "@commontools/builder"; -import { Charm, CharmManager } from "./manager.ts"; -import { getIframeRecipe } from "./iframe/recipe.ts"; -import { extractUserCode } from "./iframe/static.ts"; +import { Cell } from "@commontools/runner"; +import { Charm } from "./manager.ts"; // Re-export workflow types and functions from workflow module export type { WorkflowConfig, WorkflowType } from "./workflow.ts"; diff --git a/charm/src/iterate.ts b/charm/src/iterate.ts index f8e40621d..6120070b9 100644 --- a/charm/src/iterate.ts +++ b/charm/src/iterate.ts @@ -4,6 +4,8 @@ import { isStream, registerNewRecipe, runtime, + recipeManager, + tsToExports, } from "@commontools/runner"; import { isObj } from "@commontools/utils"; import { @@ -509,7 +511,17 @@ export async function compileRecipe( throw new Error("No default recipe found in the compiled exports."); } const parentsIds = parents?.map((id) => id.toString()); - registerNewRecipe(recipe, recipeSrc, spec, parentsIds); + recipeManager.registerRecipe({ + recipeId: recipeManager.generateRecipeId(recipe), + space: charmManager.getSpace(), + recipe, + recipeMeta: { + id: recipe.id, + src: recipeSrc, + spec, + parents: parentsIds, + }, + }); return recipe; } diff --git a/charm/src/manager.ts b/charm/src/manager.ts index fdef37e04..580e83967 100644 --- a/charm/src/manager.ts +++ b/charm/src/manager.ts @@ -17,14 +17,13 @@ import { getCell, getCellFromEntityId, getEntityId, - getRecipe, idle, isCell, isCellLink, isDoc, maybeGetCellLink, + recipeManager, runSynced, - syncRecipeBlobby, } from "@commontools/runner"; import { storage } from "@commontools/runner"; import { type Session } from "@commontools/identity"; @@ -267,7 +266,10 @@ export class CharmManager { if (!recipeId) throw new Error("Cannot duplicate charm: missing recipe ID"); // Get the recipe - const recipe = getRecipe(recipeId); + const recipe = await recipeManager.loadRecipe({ + recipeId, + space: this.space, + }); if (!recipe) throw new Error("Cannot duplicate charm: recipe not found"); // Get the original inputs @@ -317,12 +319,15 @@ export class CharmManager { charm = doc.asCell(); } + const recipeId = getRecipeIdFromCharm(charm); + // Make sure we have the recipe so we can run it! - let recipeId: string | undefined; let recipe: Recipe | Module | undefined; try { - recipeId = await this.syncRecipe(charm); - recipe = getRecipe(recipeId!)!; + recipe = await recipeManager.loadRecipe({ + recipeId, + space: this.space, + }); } catch (e) { console.warn("recipeId", recipeId); console.warn("recipe", recipe); @@ -1142,10 +1147,16 @@ export class CharmManager { } // Return Cell with argument content according to the schema of the charm. - getArgument(charm: Cell): Cell | undefined { + async getArgument( + charm: Cell, + ): Promise | undefined> { const source = charm.getSourceCell(processSchema); - const recipeId = source?.get()?.[TYPE]; - const recipe = getRecipe(recipeId); + const recipeId = source?.get()?.[TYPE]!; + const recipe = await recipeManager.loadRecipe({ + recipeId, + space: this.space, + }); + if (!recipe) return undefined; const argumentSchema = recipe?.argumentSchema; return source?.key("argument").asSchema(argumentSchema!) as | Cell @@ -1248,6 +1259,8 @@ export class CharmManager { ); } + this.syncRecipe(charm); + return charm; } @@ -1256,31 +1269,28 @@ export class CharmManager { await storage.syncCell(charm); const sourceCell = charm.getSourceCell(); - if (!sourceCell) throw new Error("charm missing source cell"); - await storage.syncCell(sourceCell); const recipeId = sourceCell.get()?.[TYPE]; if (!recipeId) throw new Error("charm missing recipe ID"); - await Promise.all([ - this.syncRecipeCells(recipeId), - this.syncRecipeBlobby(recipeId), - ]); - return recipeId; - } + await this.syncRecipeById(recipeId); - async syncRecipeCells(recipeId: string) { - // NOTE(ja): this doesn't sync recipe to storage - if (recipeId) await storage.syncCellById(this.space, { "/": recipeId }); + return recipeId; } - // FIXME(ja): blobby seems to be using toString not toJSON - async syncRecipeBlobby(recipeId: string) { - await syncRecipeBlobby(recipeId); + async syncRecipeById(recipeId: string) { + return await recipeManager.ensureRecipeAvailable({ + recipeId, + space: this.space, + }); } async sync(entity: Cell, waitForStorage: boolean = false) { await storage.syncCell(entity, waitForStorage); } } + +export const getRecipeIdFromCharm = (charm: Cell): string => { + return charm.getSourceCell(processSchema)?.get()?.[TYPE]; +}; diff --git a/charm/src/workflow.ts b/charm/src/workflow.ts index c131fbc0c..7a433efc9 100644 --- a/charm/src/workflow.ts +++ b/charm/src/workflow.ts @@ -10,7 +10,7 @@ * 6. Spell search and casting */ -import { Cell, getRecipe } from "@commontools/runner"; +import { Cell, recipeManager } from "@commontools/runner"; import { Charm, charmId, CharmManager } from "./manager.ts"; import { JSONSchema } from "@commontools/builder"; import { classifyWorkflow, generateWorkflowPlan } from "@commontools/llm"; @@ -223,7 +223,7 @@ function extractContext(charm: Cell) { undefined; } } catch { - console.warn("Failed to extract context from charm"); + console.info("Failed to extract context from charm"); } return { @@ -712,8 +712,9 @@ export async function processWorkflow( throw new Error("No charm found for id: " + form.spellToCast.charmId); } - await charmManager.syncRecipeBlobby(form.spellToCast.spellId); - const recipe = getRecipe(form.spellToCast.spellId); + const recipe = await charmManager.syncRecipeById( + form.spellToCast.spellId, + ); if (!recipe) { throw new Error( diff --git a/cli/cast-recipe.ts b/cli/cast-recipe.ts index 2f5a1d48f..7f5caef02 100644 --- a/cli/cast-recipe.ts +++ b/cli/cast-recipe.ts @@ -3,7 +3,7 @@ import { CharmManager, compileRecipe } from "@commontools/charm"; import { getEntityId, isStream, - setBobbyServerUrl, + setBlobbyServerUrl, storage, } from "@commontools/runner"; import { createAdminSession, type DID, Identity } from "@commontools/identity"; @@ -33,7 +33,7 @@ const toolshedUrl = Deno.env.get("TOOLSHED_API_URL") ?? const OPERATOR_PASS = Deno.env.get("OPERATOR_PASS") ?? "implicit trust"; storage.setRemoteStorage(new URL(toolshedUrl)); -setBobbyServerUrl(toolshedUrl); +setBlobbyServerUrl(toolshedUrl); async function castRecipe() { console.log(`Casting recipe from ${recipePath} in space ${spaceId}`); diff --git a/cli/main.ts b/cli/main.ts index 51881823a..8855b3a62 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -5,7 +5,7 @@ import { getEntityId, idle, isStream, - setBobbyServerUrl, + setBlobbyServerUrl, storage, } from "@commontools/runner"; import { @@ -47,7 +47,7 @@ const toolshedUrl = Deno.env.get("TOOLSHED_API_URL") ?? const OPERATOR_PASS = Deno.env.get("OPERATOR_PASS") ?? "implicit trust"; storage.setRemoteStorage(new URL(toolshedUrl)); -setBobbyServerUrl(toolshedUrl); +setBlobbyServerUrl(toolshedUrl); async function main() { if (!spaceName && !spaceDID) { diff --git a/jumble/src/services/spellbook.ts b/jumble/src/services/spellbook.ts index 51eac30d8..b91ada246 100644 --- a/jumble/src/services/spellbook.ts +++ b/jumble/src/services/spellbook.ts @@ -1,8 +1,4 @@ -import { - getRecipeParents, - getRecipeSpec, - getRecipeSrc, -} from "@commontools/runner"; +import { recipeManager } from "@commontools/runner"; import { UI } from "@commontools/builder"; export interface Spell { @@ -105,9 +101,7 @@ export async function saveSpell( ): Promise { try { // Get all the required data from commontools first - const src = getRecipeSrc(spellId); - const spec = getRecipeSpec(spellId); - const parents = getRecipeParents(spellId); + const { src, spec, parents } = await recipeManager.getRecipeMeta(recipe); const ui = spell.resultRef?.[UI]; if (spellId === undefined) { diff --git a/jumble/src/views/spellbook/SpellbookLaunchView.tsx b/jumble/src/views/spellbook/SpellbookLaunchView.tsx index d87a738a2..9b3a3c30b 100644 --- a/jumble/src/views/spellbook/SpellbookLaunchView.tsx +++ b/jumble/src/views/spellbook/SpellbookLaunchView.tsx @@ -51,7 +51,7 @@ function Launcher() { try { console.log("Attempting to sync recipe for spellId:", spellId); // Sync the recipe - await charmManager.syncRecipeBlobby(spellId); + await charmManager.syncRecipeById(spellId); console.log("Recipe sync completed"); const recipe = getRecipe(spellId); diff --git a/runner/src/blobby-storage.ts b/runner/src/blobby-storage.ts index 03eee4d04..e1bc2be51 100644 --- a/runner/src/blobby-storage.ts +++ b/runner/src/blobby-storage.ts @@ -19,55 +19,3 @@ export function setBlobbyServerUrl(url: string) { export function getBlobbyServerUrl(): string { return BLOBBY_SERVER_URL; } - -/** - * Saves an item to the Blobby server - * @param prefix The prefix to use for the item (e.g., "spell" for recipes, "schema" for schemas) - * @param id The ID of the item - * @param data The data to save - * @returns A promise that resolves to true if the save was successful, false otherwise - */ -export async function saveToBlobby( - prefix: string, - id: string, - data: Record, -): Promise { - console.log(`Saving ${prefix}-${id}`); - const response = await fetch(`${BLOBBY_SERVER_URL}/${prefix}-${id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }); - return response.ok; -} - -/** - * Loads an item from the Blobby server - * @param prefix The prefix to use for the item (e.g., "spell" for recipes, "schema" for schemas) - * @param id The ID of the item - * @returns A promise that resolves to the loaded data or null if not found - */ -export async function loadFromBlobby>( - prefix: string, - id: string, -): Promise { - const response = await fetch(`${BLOBBY_SERVER_URL}/${prefix}-${id}`); - if (!response.ok) return null; - - try { - return await response.json() as T; - } catch (e) { - const text = await response.text(); - return { src: text } as unknown as T; - } -} - -/** - * Creates a set to track items known to storage to avoid redundant saves - * @returns A new Set to track items - */ -export function createItemsKnownToStorageSet(): Set { - return new Set(); -} diff --git a/runner/src/index.ts b/runner/src/index.ts index c4f2e02ce..7eb593e11 100644 --- a/runner/src/index.ts +++ b/runner/src/index.ts @@ -37,22 +37,9 @@ export { getDocByEntityId, getEntityId, } from "./doc-map.ts"; -export { - allRecipesByName, - getRecipe, - getRecipeId, - getRecipeName, - getRecipeParents, - getRecipeSpec, - getRecipeSrc, - registerNewRecipe, - registerRecipe, -} from "./recipe-map.ts"; -// export { addSchema, getSchema, getSchemaId } from "./schema-map.ts"; export { type AddCancel, type Cancel, noOp, useCancelGroup } from "./cancel.ts"; export { type Storage, storage } from "./storage.ts"; export { setBobbyServerUrl, syncRecipeBlobby } from "./recipe-sync.ts"; -// export { saveSchema, syncSchemaBlobby } from "./schema-sync.ts"; export { getBlobbyServerUrl, loadFromBlobby, @@ -60,9 +47,12 @@ export { setBlobbyServerUrl, } from "./blobby-storage.ts"; export { ConsoleMethod, runtime } from "./runtime/index.ts"; +export { getBlobbyServerUrl, setBlobbyServerUrl } from "./blobby-storage.ts"; +export { tsToExports } from "./local-build.ts"; export { addCommonIDfromObjectID, followAliases, maybeGetCellLink, } from "./utils.ts"; export { ContextualFlowControl } from "./cfc.ts"; +export * from "./recipe-manager.ts"; diff --git a/runner/src/recipe-manager.ts b/runner/src/recipe-manager.ts new file mode 100644 index 000000000..4b934fa16 --- /dev/null +++ b/runner/src/recipe-manager.ts @@ -0,0 +1,316 @@ +/** + * RecipeManager: Unified Recipe Storage and Sync System + * + * Design goals: + * 1. Single storage model: Uses cells in the storage layer for persistent storage + * 2. Preserves recipe IDs: Maintains consistency between local and remote IDs + * 3. Clear publishing flow: Only syncs with Blobby when explicitly requested + * 4. Attempts to download recipes from Blobby if no cell is found + * 5. Minimize requirements that Blobby is available for a space to run recipes + * + * Storage layers: + * - In-memory cache: Fast access during runtime + * - Cell storage: Persistent local storage + * - Blobby storage: Remote storage for sharing recipes + * + * Usage: + * - Use the singleton instance exported as `recipeManager` + * - For new code, prefer the `recipeManager` object + */ + +import { JSONSchema, Module, Recipe, Schema } from "@commontools/builder"; +import { storage } from "./storage.ts"; +import { Cell } from "./cell.ts"; +import { createRef } from "./doc-map.ts"; +import { getCell } from "./cell.ts"; +import { buildRecipe } from "./local-build.ts"; +import { getBlobbyServerUrl } from "./blobby-storage.ts"; + +// Schema definitions +export const recipeMetaSchema = { + type: "object", + properties: { + id: { type: "string" }, + src: { type: "string" }, + spec: { type: "string" }, + parents: { type: "array", items: { type: "string" } }, + recipeName: { type: "string" }, + }, + required: ["id"], +} as const satisfies JSONSchema; + +export type RecipeMeta = Schema; + +// Type guard to check if an object is a Recipe +function isRecipe(obj: Recipe | Module): obj is Recipe { + return "result" in obj && "nodes" in obj; +} + +// FIXME(ja): what happens when we have multiple active spaces... how do we make +// sure we register the same recipeMeta in multiple spaces? +const recipeMetaMap = new WeakMap>(); +const recipeIdMap = new Map(); + +class RecipeManager { + private async getRecipeMetaCell( + { recipeId, space }: { recipeId: string; space: string }, + ) { + const cell = getCell( + space, + { recipeId, type: "recipe" }, + recipeMetaSchema, + ); + + await storage.syncCell(cell); + await storage.synced(); + return cell; + } + + // returns the recipeMeta for a loaded recipe + getRecipeMeta( + input: Recipe | Module | { recipeId: string }, + ): RecipeMeta | undefined { + if ("recipeId" in input) { + const recipe = this.recipeById(input.recipeId); + return recipe ? recipeMetaMap.get(recipe)?.get() : undefined; + } + return recipeMetaMap.get(input as Recipe)?.get(); + } + + generateRecipeId(recipe: Recipe | Module, src?: string) { + let id = recipeMetaMap.get(recipe as Recipe)?.get()?.id; + if (id) { + console.log("generateRecipeId: existing recipe id", id); + return id; + } + + id = src + ? createRef({ src }, "recipe source").toString() + : createRef(recipe, "recipe").toString(); + + console.log("generateRecipeId: generated id", id); + + return id; + } + + async registerRecipe( + { recipeId, space, recipe, recipeMeta }: { + recipeId: string; + space: string; + recipe: Recipe | Module; + recipeMeta: RecipeMeta; + }, + ): Promise { + console.log("registerRecipe", recipeId, space); + + // FIXME(ja): is there a reason to save if we don't have src? + // mostly wondering about modules... + if (!recipeMeta.src) { + console.error( + "registerRecipe: no reason to save recipe, missing src", + recipeId, + ); + return false; + } + + // FIXME(ja): should we update the recipeMeta if it already exists? when does this happen? + if (recipeMetaMap.has(recipe as Recipe)) { + return true; + } + + const recipeMetaCell = await this.getRecipeMetaCell({ recipeId, space }); + recipeMetaCell.set(recipeMeta); + recipeMetaMap.set(recipe as Recipe, recipeMetaCell); + await storage.syncCell(recipeMetaCell); + await storage.synced(); + + recipeIdMap.set(recipeId, recipe as Recipe); + recipeMetaMap.set(recipe as Recipe, recipeMetaCell); + + return true; + } + + // returns a recipe already loaded + recipeById(recipeId: string): Recipe | undefined { + return recipeIdMap.get(recipeId); + } + + async loadRecipe( + { space, recipeId }: { space: string; recipeId: string }, + ): Promise { + if (recipeIdMap.has(recipeId)) { + return recipeIdMap.get(recipeId); + } + + const metaCell = await this.getRecipeMetaCell({ recipeId, space }); + + const recipeMeta = metaCell.get(); + // if we don't have the recipeMeta, we should try to import from blobby + // as it might be from before we started saving recipes in cells + if (recipeMeta.id !== recipeId) { + const { recipe, recipeMeta } = await this.importFromBlobby({ recipeId }); + if (recipe) { + metaCell.set(recipeMeta); + await storage.syncCell(metaCell); + await storage.synced(); + recipeIdMap.set(recipeId, recipe); + recipeMetaMap.set(recipe, metaCell); + return recipe; + } + return undefined; + } + + const { src } = recipeMeta; + + const { recipe, errors } = await buildRecipe(src!); + if (errors || !recipe) { + console.error(`Failed to build recipe ${recipeId}:`, errors); + return undefined; + } + + metaCell.set(recipeMeta); + await storage.syncCell(metaCell); + await storage.synced(); + recipeIdMap.set(recipeId, recipe); + recipeMetaMap.set(recipe, metaCell); + return recipe; + } + + /** + * Load a recipe from Blobby, returning the recipe and recipeMeta + */ + // FIXME(ja): move this back to blobby! + private async importFromBlobby( + { recipeId }: { recipeId: string }, + ): Promise< + { recipe: Recipe; recipeMeta: RecipeMeta } | Record + > { + const response = await fetch(`${getBlobbyServerUrl()}/spell-${recipeId}`); + if (!response.ok) { + return {}; + } + + const recipeJson = await response.json() as { + src: string; + spec?: string; + parents?: string[]; + }; + + try { + const { recipe, errors } = await buildRecipe(recipeJson.src!); + if (errors || !recipe) { + console.error( + `Failed to build recipe ${recipeId} from Blobby:`, + errors, + ); + return {}; + } + + return { + recipe, + recipeMeta: { + id: recipeId, + src: recipeJson.src, + spec: recipeJson.spec, + parents: recipeJson.parents, + }, + }; + } catch (error) { + console.error(`Error loading recipe ${recipeId} from Blobby:`, error); + return {}; + } + } + + // FIXME(ja): move this back to blobby! + async publishToBlobby( + recipeId: string, + spellbookTitle?: string, + spellbookTags?: string[], + ) { + const recipe = recipeIdMap.get(recipeId); + if (!recipe) { + throw new Error(`Recipe ${recipeId} not found`); + } + const recipeMeta = recipeMetaMap.get(recipe)?.get(); + if (!recipeMeta) { + throw new Error(`Recipe meta for recipe ${recipeId} not found`); + } + + if (!recipeMeta.src) { + throw new Error(`Source for recipe ${recipeId} not found`); + } + + const data = { + src: recipeMeta.src, + recipe: JSON.parse(JSON.stringify(recipe)), + spec: recipeMeta.spec, + parents: recipeMeta.parents, + recipeName: recipeMeta.recipeName, + spellbookTitle, + spellbookTags, + }; + + console.log(`Saving spell-${recipeId}`); + const response = await fetch(`${getBlobbyServerUrl()}/spell-${recipeId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return response.ok; + } + + /** + * Ensure a recipe is available, trying cell storage first then Blobby + */ + async ensureRecipeAvailable({ + space, + recipeId, + }: { + space: string; + recipeId: string; + }): Promise { + // First check if it's already in memory + let recipe = recipeIdMap.get(recipeId); + if (recipe) return recipe; + + // Try to load from cell storage + const loadedFromCell = await this.loadRecipe({ space, recipeId }); + if (loadedFromCell) { + recipe = loadedFromCell; + if (recipe) return recipe; + } + + // Try to load from Blobby + const loadedFromBlobby = await this.importFromBlobby({ recipeId }); + if (loadedFromBlobby) { + recipe = loadedFromBlobby.recipe; + if (recipe) { + // Save to cell for future use + await this.registerRecipe({ + recipeId, + space, + recipe, + recipeMeta: loadedFromBlobby.recipeMeta, + }); + return recipe; + } + } + + throw new Error( + `Could not find recipe ${recipeId} in any storage location`, + ); + } +} + +export const recipeManager = new RecipeManager(); +export const { + getRecipeMeta, + generateRecipeId, + registerRecipe, + loadRecipe, + ensureRecipeAvailable, + publishToBlobby, + recipeById, +} = recipeManager; diff --git a/runner/src/recipe-map.ts b/runner/src/recipe-map.ts deleted file mode 100644 index d5e4a0651..000000000 --- a/runner/src/recipe-map.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { Module, Recipe } from "@commontools/builder"; -import { createRef } from "./doc-map.ts"; - -const recipeById = new Map(); -const recipeNameById = new Map(); -const recipeByName = new Map(); -const idByRecipe = new Map(); -const srcById = new Map(); -const specById = new Map(); -const parentsById = new Map(); - -export function registerNewRecipe( - recipe: Recipe, - src?: string, - spec?: string, - parents?: string[], -): string { - if (idByRecipe.has(recipe)) return idByRecipe.get(recipe)!; - - const id = src - ? createRef({ src }, "recipe source").toString() - : createRef(recipe, "recipe").toString(); - - return registerRecipe(id, recipe, src, spec, parents); -} - -export function registerRecipe( - id: string, - recipe: Recipe, - src?: string, - spec?: string, - parents?: string[], -): string { - if (idByRecipe.has(recipe)) return idByRecipe.get(recipe)!; - - recipeById.set(id, recipe); - idByRecipe.set(recipe, id); - - if (src) srcById.set(id, src); - if (spec) specById.set(id, spec); - if (parents) parentsById.set(id, parents); - const name = (recipe.argumentSchema as { description: string })?.description; - if (name) { - recipeByName.set(name, recipe); - recipeNameById.set(id, name); - } - - return id; -} - -export function getRecipe(id: string) { - return recipeById.get(id); -} - -export function getRecipeId(recipe: Recipe | Module) { - return idByRecipe.get(recipe); -} - -export function getRecipeName(id: string) { - return recipeNameById.get(id); -} - -export function getRecipeSrc(id: string) { - return srcById.get(id); -} - -export function getRecipeSpec(id: string) { - return specById.get(id); -} - -export function getRecipeParents(id: string) { - return parentsById.get(id); -} - -export function allRecipesByName() { - return recipeByName; -} diff --git a/runner/src/runner.ts b/runner/src/runner.ts index 95d315541..e85e21cff 100644 --- a/runner/src/runner.ts +++ b/runner/src/runner.ts @@ -18,6 +18,7 @@ import { } from "@commontools/builder"; import { type DocImpl, getDoc, isDoc } from "./doc.ts"; import { type Cell, getCellFromLink } from "./cell.ts"; +import { recipeManager } from "./recipe-manager.ts"; import { Action, addEventHandler, @@ -39,7 +40,6 @@ import { import { getModuleByRef } from "./module.ts"; import { type AddCancel, type Cancel, useCancelGroup } from "./cancel.ts"; import "./builtins/index.ts"; -import { getRecipe, getRecipeId, registerNewRecipe } from "./recipe-map.ts"; import { type CellLink, isCell, isCellLink } from "./cell.ts"; import { isQueryResultForDereferencing } from "./query-result-proxy.ts"; import { getCellLinkOrThrow } from "./query-result-proxy.ts"; @@ -107,7 +107,7 @@ export function run( if (!recipeOrModule && processCell.get()?.[TYPE]) { recipeId = processCell.get()[TYPE]; - recipeOrModule = getRecipe(recipeId); + recipeOrModule = recipeManager.recipeById(recipeId); if (!recipeOrModule) throw new Error(`Unknown recipe: ${recipeId}`); } else if (!recipeOrModule) { console.warn( @@ -122,7 +122,7 @@ export function run( // passing arguments in unmodified and passing all results through as is if (isModule(recipeOrModule)) { const module = recipeOrModule as Module; - recipeId ??= getRecipeId(module); + recipeId ??= recipeManager.generateRecipeId(module); recipe = { argumentSchema: module.argumentSchema ?? {}, @@ -140,7 +140,7 @@ export function run( recipe = recipeOrModule as Recipe; } - recipeId ??= registerNewRecipe(recipe); + recipeId ??= recipeManager.generateRecipeId(recipe); if (cancels.has(resultCell)) { // If it's already running and no new recipe or argument are given, @@ -634,7 +634,8 @@ export async function runSynced( } // Now get used recipe to extract schema - recipe = getRecipe(resultCell.getSourceCell().get()[TYPE]!); + const recipeId = resultCell.getSourceCell().get()[TYPE]!; + recipe = recipeManager.recipeById(recipeId); return recipe?.resultSchema ? resultCell.asSchema(recipe.resultSchema) @@ -656,9 +657,12 @@ async function syncCellsForRunningRecipe( const recipeId = sourceCell.get()[TYPE]; if (!recipeId) throw new Error(`No recipe ID found in source cell`); - await syncRecipeBlobby(recipeId); + await recipeManager.ensureRecipeAvailable({ + space: sourceCell.getAsCellLink().space!, + recipeId, + }); - const recipe = getRecipe(recipeId); + const recipe = recipeManager.recipeById(recipeId); if (!recipe) throw new Error(`Unknown recipe: ${recipeId}`); // We could support this by replicating what happens in runner, but since diff --git a/runner/test/recipes.test.ts b/runner/test/recipes.test.ts index 69c8d2ed9..c45aa40f2 100644 --- a/runner/test/recipes.test.ts +++ b/runner/test/recipes.test.ts @@ -6,7 +6,7 @@ import { addModuleByRef } from "../src/module.ts"; import { getDoc } from "../src/doc.ts"; import { type ErrorWithContext, idle, onError } from "../src/scheduler.ts"; import { type Cell } from "../src/cell.ts"; -import { getRecipeId } from "../src/recipe-map.ts"; +import { getRecipeIdFromCharm } from "../../charm/src/manager.ts"; describe("Recipe Runner", () => { it("should run a simple recipe", async () => { @@ -724,7 +724,7 @@ describe("Recipe Runner", () => { expect(errors).toBe(1); expect(charm.getAsQueryResult()).toMatchObject({ result: 5 }); - expect(lastError?.recipeId).toBe(getRecipeId(divRecipe)); + expect(lastError?.recipeId).toBe(getRecipeIdFromCharm(charm)); expect(lastError?.space).toBe("test"); expect(lastError?.charmId).toBe( JSON.parse(JSON.stringify(charm.entityId))["/"], @@ -787,7 +787,7 @@ describe("Recipe Runner", () => { expect(errors).toBe(1); expect(charm.getAsQueryResult()).toMatchObject({ result: 10 }); - expect(lastError?.recipeId).toBe(getRecipeId(divRecipe)); + expect(lastError?.recipeId).toBe(getRecipeIdFromCharm(charm)); expect(lastError?.space).toBe("test"); expect(lastError?.charmId).toBe( JSON.parse(JSON.stringify(charm.entityId))["/"], From 2ad0b11546db509730f152d75afaf238db6929f6 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 08:54:49 -0400 Subject: [PATCH 02/11] back to only 2 tests failing --- background-charm-service/cast-admin.ts | 6 +- charm/src/iframe/recipe.ts | 22 ++--- charm/src/iterate.ts | 59 +++++++------- charm/src/manager.ts | 21 +++-- cli/cast-recipe.ts | 6 +- cli/main.ts | 12 +-- deno.lock | 30 ++++--- jumble/src/components/Composer.tsx | 3 - jumble/src/components/SpecPreview.tsx | 11 +-- jumble/src/services/spellbook.ts | 2 +- .../views/spellbook/SpellbookLaunchView.tsx | 7 +- runner/src/index.ts | 10 +-- runner/src/recipe-manager.ts | 62 ++++++-------- runner/src/recipe-sync.ts | 81 ------------------- runner/src/runner.ts | 1 - runner/src/runtime/eval-runtime.ts | 8 +- 16 files changed, 109 insertions(+), 232 deletions(-) delete mode 100644 runner/src/recipe-sync.ts diff --git a/background-charm-service/cast-admin.ts b/background-charm-service/cast-admin.ts index eed22f867..b7d1e7fc5 100644 --- a/background-charm-service/cast-admin.ts +++ b/background-charm-service/cast-admin.ts @@ -64,11 +64,6 @@ async function castRecipe() { // Load and compile the recipe first console.log("Loading recipe..."); const recipeSrc = await Deno.readTextFile(recipePath!); - const recipe = await compileRecipe(recipeSrc, "recipe", []); - - if (!recipe) { - throw new Error(`Failed to compile recipe from ${recipePath}`); - } if (!cause) { throw new Error("Cell ID is required"); @@ -100,6 +95,7 @@ async function castRecipe() { // Create charm manager for the specified space const charmManager = new CharmManager(session); + const recipe = await compileRecipe(recipeSrc, "recipe", charmManager); const charm = await charmManager.runPersistent( recipe, diff --git a/charm/src/iframe/recipe.ts b/charm/src/iframe/recipe.ts index 7a67426e9..3e25de0a6 100644 --- a/charm/src/iframe/recipe.ts +++ b/charm/src/iframe/recipe.ts @@ -1,6 +1,6 @@ import { JSONSchema } from "@commontools/builder"; -import { Charm, getRecipeIdFromCharm, processSchema } from "../manager.ts"; -import { Cell, recipeManager, TYPE, getRecipe, getRecipeSrc } from "@commontools/runner"; +import { Charm, getRecipeIdFromCharm } from "../manager.ts"; +import { Cell, getEntityId, recipeManager } from "@commontools/runner"; export type IFrameRecipe = { src: string; @@ -61,33 +61,25 @@ function parseIframeRecipe(source: string): IFrameRecipe { return JSON.parse(match[1]) as IFrameRecipe; } -export const getRecipeFrom = (charm: Cell) => { - const recipeId = charm.getSourceCell(processSchema)?.get()?.[TYPE]; - const recipe = getRecipe(recipeId)!; - const src = getRecipeSrc(recipeId)!; - - return { recipeId, recipe, src }; -}; - export const getIframeRecipe = (charm: Cell): { recipeId: string; - src: string; + src?: string; iframe?: IFrameRecipe; } => { const recipeId = getRecipeIdFromCharm(charm); if (!recipeId) { - console.warn("No recipeId found for charm", charm.getId()); + console.warn("No recipeId found for charm", getEntityId(charm)); return { recipeId, src: "", iframe: undefined }; } const src = recipeManager.getRecipeMeta({ recipeId })?.src; if (!src) { - console.warn("No src found for charm", charm.getId()); - return { recipeId, src, iframe: undefined }; + console.warn("No src found for charm", getEntityId(charm)); + return { recipeId }; } try { return { recipeId, src, iframe: parseIframeRecipe(src) }; } catch (error) { console.warn("Error parsing iframe recipe:", error); - return { recipeId, src, iframe: undefined }; + return { recipeId, src }; } }; diff --git a/charm/src/iterate.ts b/charm/src/iterate.ts index 6120070b9..84464541e 100644 --- a/charm/src/iterate.ts +++ b/charm/src/iterate.ts @@ -2,10 +2,8 @@ import { Cell, isCell, isStream, - registerNewRecipe, - runtime, recipeManager, - tsToExports, + runtime, } from "@commontools/runner"; import { isObj } from "@commontools/utils"; import { @@ -14,11 +12,7 @@ import { JSONSchemaMutable, } from "@commontools/builder"; import { Charm, CharmManager, charmSourceCellSchema } from "./manager.ts"; -import { - buildFullRecipe, - getIframeRecipe, - getRecipeFrom, -} from "./iframe/recipe.ts"; +import { buildFullRecipe, getIframeRecipe } from "./iframe/recipe.ts"; import { buildPrompt, RESPONSE_PREFILL } from "./iframe/prompt.ts"; import { applyDefaults, @@ -154,26 +148,32 @@ export const generateNewRecipeVersion = async ( generationId?: string, llmRequestId?: string, ) => { - const { iframe } = getIframeRecipe(parent); - const { recipe, recipeId } = getRecipeFrom(parent); + const parentInfo = getIframeRecipe(parent); + if (!parentInfo.recipeId) { + throw new Error("No recipeId found for charm"); + } + const parentRecipe = await recipeManager.loadRecipe({ + space: charmManager.getSpace(), + recipeId: parentInfo.recipeId, + }); const name = extractTitle(newRecipe.src, ""); - // If we have an iframe already, just spread everything - const fullSrc = buildFullRecipe( - iframe - ? { - ...iframe, - ...newRecipe, - name: name, - } - // otherwise, we are editing a non-iframe recipe and need to grab/fill the schema - : { - argumentSchema: recipe.argumentSchema ?? { type: "object" }, - resultSchema: recipe.resultSchema ?? { type: "object" }, - ...newRecipe, - name, - }, - ); + const argumentSchema = + (parentInfo.iframe + ? parentInfo.iframe.argumentSchema + : parentRecipe.argumentSchema) ?? { type: "object" }; + const resultSchema = + (parentInfo.iframe + ? parentInfo.iframe.resultSchema + : parentRecipe.resultSchema) ?? { type: "object" }; + + const fullSrc = buildFullRecipe({ + ...parentInfo.iframe, // ignored if undefined + argumentSchema, + resultSchema, + ...newRecipe, + name, + }); globalThis.dispatchEvent( new CustomEvent("job-update", { @@ -191,7 +191,7 @@ export const generateNewRecipeVersion = async ( fullSrc, newRecipe.spec!, parent.getSourceCell()?.key("argument"), - recipeId ? [recipeId] : undefined, + parentInfo.recipeId ? [parentInfo.recipeId] : undefined, llmRequestId, ); @@ -504,6 +504,7 @@ export async function castNewRecipe( export async function compileRecipe( recipeSrc: string, spec: string, + charmManager: CharmManager, parents?: string[], ) { const recipe = await runtime.compile(recipeSrc); @@ -516,7 +517,7 @@ export async function compileRecipe( space: charmManager.getSpace(), recipe, recipeMeta: { - id: recipe.id, + id: recipeManager.generateRecipeId(recipe), src: recipeSrc, spec, parents: parentsIds, @@ -533,7 +534,7 @@ export async function compileAndRunRecipe( parents?: string[], llmRequestId?: string, ): Promise> { - const recipe = await compileRecipe(recipeSrc, spec, parents); + const recipe = await compileRecipe(recipeSrc, spec, charmManager, parents); if (!recipe) { throw new Error("Failed to compile recipe"); } diff --git a/charm/src/manager.ts b/charm/src/manager.ts index 580e83967..9ce35ee66 100644 --- a/charm/src/manager.ts +++ b/charm/src/manager.ts @@ -1146,21 +1146,18 @@ export class CharmManager { ); } - // Return Cell with argument content according to the schema of the charm. - async getArgument( + // Return Cell with argument content of already loaded recipe according + // to the schema of the charm. + getArgument( charm: Cell, - ): Promise | undefined> { + ): Cell { const source = charm.getSourceCell(processSchema); const recipeId = source?.get()?.[TYPE]!; - const recipe = await recipeManager.loadRecipe({ - recipeId, - space: this.space, - }); - if (!recipe) return undefined; - const argumentSchema = recipe?.argumentSchema; - return source?.key("argument").asSchema(argumentSchema!) as - | Cell - | undefined; + if (!recipeId) throw new Error("charm missing recipe ID"); + const recipe = recipeManager.recipeById(recipeId); + if (!recipe) throw new Error(`Recipe ${recipeId} not loaded`); + // FIXME(ja): return should be Cell> I think? + return source.key("argument").asSchema(recipe.argumentSchema); } // note: removing a charm doesn't clean up the charm's cells diff --git a/cli/cast-recipe.ts b/cli/cast-recipe.ts index 7f5caef02..f4a626bd1 100644 --- a/cli/cast-recipe.ts +++ b/cli/cast-recipe.ts @@ -55,11 +55,6 @@ async function castRecipe() { // Load and compile the recipe first console.log("Loading recipe..."); const recipeSrc = await Deno.readTextFile(recipePath!); - const recipe = await compileRecipe(recipeSrc, "recipe", []); - - if (!recipe) { - throw new Error(`Failed to compile recipe from ${recipePath}`); - } console.log("Recipe compiled successfully"); @@ -72,6 +67,7 @@ async function castRecipe() { // Create charm manager for the specified space const charmManager = new CharmManager(session); + const recipe = await compileRecipe(recipeSrc, "recipe", charmManager); const charm = await charmManager.runPersistent( recipe, diff --git a/cli/main.ts b/cli/main.ts index 8855b3a62..fb00f1bea 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -94,8 +94,8 @@ async function main() { }) satisfies Session; // TODO(seefeld): It only wants the space, so maybe we simplify the above and just space the space did? - const manager = new CharmManager(session); - const charms = manager.getCharms(); + const charmManager = new CharmManager(session); + const charms = charmManager.getCharms(); charms.sink((charms) => { console.log( "all charms:", @@ -104,7 +104,7 @@ async function main() { }); if (charmId) { - const charm = await manager.get(charmId); + const charm = await charmManager.get(charmId); if (quit) { if (!charm) { console.error("charm not found:", charmId); @@ -148,9 +148,9 @@ async function main() { if (recipeFile) { try { const recipeSrc = await Deno.readTextFile(recipeFile); - const recipe = await compileRecipe(recipeSrc, "recipe", []); - const charm = await manager.runPersistent(recipe, inputValue, cause); - const charmWithSchema = (await manager.get(charm))!; + const recipe = await compileRecipe(recipeSrc, "recipe", charmManager); + const charm = await charmManager.runPersistent(recipe, inputValue, cause); + const charmWithSchema = (await charmManager.get(charm))!; charmWithSchema.sink((value) => { console.log("running charm:", getEntityId(charm), value); }); diff --git a/deno.lock b/deno.lock index f46cd26b8..6ef50c207 100644 --- a/deno.lock +++ b/deno.lock @@ -83,6 +83,7 @@ "npm:@tailwindcss/typography@~0.5.16": "0.5.16_tailwindcss@4.1.4", "npm:@tailwindcss/vite@^4.0.1": "4.1.4_vite@6.3.2__@types+node@22.14.1__picomatch@4.0.2_@types+node@22.14.1", "npm:@types/jsdom@^21.1.7": "21.1.7", + "npm:@types/node@*": "22.14.1", "npm:@types/node@^22.12.0": "22.14.1", "npm:@types/node@^22.5.5": "22.14.1", "npm:@types/react-dom@^18.3.1": "18.3.6_@types+react@18.3.20", @@ -1042,12 +1043,12 @@ "@isaacs/cliui@8.0.2": { "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dependencies": [ - "string-width-cjs@npm:string-width@4.2.3", "string-width@5.1.2", - "strip-ansi-cjs@npm:strip-ansi@6.0.1", + "string-width-cjs@npm:string-width@4.2.3", "strip-ansi@7.1.0", - "wrap-ansi-cjs@npm:wrap-ansi@7.0.0", - "wrap-ansi@8.1.0" + "strip-ansi-cjs@npm:strip-ansi@6.0.1", + "wrap-ansi@8.1.0", + "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" ] }, "@jridgewell/gen-mapping@0.3.8": { @@ -3263,15 +3264,15 @@ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dependencies": [ "@esbuild/aix-ppc64@0.21.5", - "@esbuild/android-arm64@0.21.5", "@esbuild/android-arm@0.21.5", + "@esbuild/android-arm64@0.21.5", "@esbuild/android-x64@0.21.5", "@esbuild/darwin-arm64@0.21.5", "@esbuild/darwin-x64@0.21.5", "@esbuild/freebsd-arm64@0.21.5", "@esbuild/freebsd-x64@0.21.5", - "@esbuild/linux-arm64@0.21.5", "@esbuild/linux-arm@0.21.5", + "@esbuild/linux-arm64@0.21.5", "@esbuild/linux-ia32@0.21.5", "@esbuild/linux-loong64@0.21.5", "@esbuild/linux-mips64el@0.21.5", @@ -3291,15 +3292,15 @@ "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", "dependencies": [ "@esbuild/aix-ppc64@0.23.1", - "@esbuild/android-arm64@0.23.1", "@esbuild/android-arm@0.23.1", + "@esbuild/android-arm64@0.23.1", "@esbuild/android-x64@0.23.1", "@esbuild/darwin-arm64@0.23.1", "@esbuild/darwin-x64@0.23.1", "@esbuild/freebsd-arm64@0.23.1", "@esbuild/freebsd-x64@0.23.1", - "@esbuild/linux-arm64@0.23.1", "@esbuild/linux-arm@0.23.1", + "@esbuild/linux-arm64@0.23.1", "@esbuild/linux-ia32@0.23.1", "@esbuild/linux-loong64@0.23.1", "@esbuild/linux-mips64el@0.23.1", @@ -3320,15 +3321,15 @@ "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", "dependencies": [ "@esbuild/aix-ppc64@0.25.3", - "@esbuild/android-arm64@0.25.3", "@esbuild/android-arm@0.25.3", + "@esbuild/android-arm64@0.25.3", "@esbuild/android-x64@0.25.3", "@esbuild/darwin-arm64@0.25.3", "@esbuild/darwin-x64@0.25.3", "@esbuild/freebsd-arm64@0.25.3", "@esbuild/freebsd-x64@0.25.3", - "@esbuild/linux-arm64@0.25.3", "@esbuild/linux-arm@0.25.3", + "@esbuild/linux-arm64@0.25.3", "@esbuild/linux-ia32@0.25.3", "@esbuild/linux-loong64@0.25.3", "@esbuild/linux-mips64el@0.25.3", @@ -5947,8 +5948,8 @@ "tinyexec", "tinypool", "tinyrainbow", - "vite-node", "vite@5.4.18_@types+node@22.14.1", + "vite-node", "why-is-node-running" ] }, @@ -6129,7 +6130,9 @@ } }, "redirects": { - "https://deno.land/x/is_docker/mod.ts": "https://deno.land/x/is_docker@v2.0.0/mod.ts" + "https://deno.land/x/is_docker/mod.ts": "https://deno.land/x/is_docker@v2.0.0/mod.ts", + "https://esm.sh/@babel/standalone": "https://esm.sh/@babel/standalone@7.27.0", + "https://esm.sh/@types/babel__standalone@~7.1.9/index.d.ts": "https://esm.sh/@types/babel__standalone@7.1.9/index.d.ts" }, "remote": { "https://deno.land/std@0.106.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", @@ -6138,7 +6141,8 @@ "https://deno.land/std@0.106.0/path/posix.ts": "b81974c768d298f8dcd2c720229639b3803ca4a241fa9a355c762fa2bc5ef0c1", "https://deno.land/x/is_docker@v2.0.0/mod.ts": "4c8753346f4afbb6c251d7984a609aa84055559cf713fba828939a5d39c95cd0", "https://deno.land/x/is_wsl@v1.1.0/mod.ts": "30996b09376652df7a4d495320e918154906ab94325745c1399e13e658dca5da", - "https://deno.land/x/open@v0.0.6/index.ts": "c7484a7bf2628236f33bbe354520e651811faf1a7cbc3c3f80958ce81b4c42ef" + "https://deno.land/x/open@v0.0.6/index.ts": "c7484a7bf2628236f33bbe354520e651811faf1a7cbc3c3f80958ce81b4c42ef", + "https://esm.sh/@babel/standalone@7.27.0": "82a6873adc960aaa2ab9fcb605fc634754f5cd39ea121b442a0a000fbd24ef58" }, "workspace": { "dependencies": [ diff --git a/jumble/src/components/Composer.tsx b/jumble/src/components/Composer.tsx index 5d5557fc4..575c57593 100644 --- a/jumble/src/components/Composer.tsx +++ b/jumble/src/components/Composer.tsx @@ -32,9 +32,6 @@ import { withReact, } from "slate-react"; import { createPortal } from "react-dom"; -import { CharmManager } from "../../../charm/src/index.ts"; -import { Module, Recipe, TYPE } from "@commontools/builder"; -import { Cell, getRecipe } from "@commontools/runner"; import { LuSend } from "react-icons/lu"; import { DitheredCube } from "@/components/DitherCube.tsx"; diff --git a/jumble/src/components/SpecPreview.tsx b/jumble/src/components/SpecPreview.tsx index 460aa907f..91e20614a 100644 --- a/jumble/src/components/SpecPreview.tsx +++ b/jumble/src/components/SpecPreview.tsx @@ -1,14 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { DitheredCube } from "./DitherCube.tsx"; -import { animated, useSpring, useTransition } from "@react-spring/web"; -import { ToggleButton } from "./common/CommonToggle.tsx"; -import type { - ExecutionPlan, - WorkflowForm, - WorkflowType, -} from "@commontools/charm"; -import { getRecipe } from "@commontools/runner"; -import { charmId } from "@commontools/charm"; +import { useSpring } from "@react-spring/web"; +import type { WorkflowForm, WorkflowType } from "@commontools/charm"; import { SpellRecord, WORKFLOWS } from "../../../charm/src/workflow.ts"; import CodeMirror from "@uiw/react-codemirror"; import { javascript } from "@codemirror/lang-javascript"; diff --git a/jumble/src/services/spellbook.ts b/jumble/src/services/spellbook.ts index b91ada246..3087e6f2f 100644 --- a/jumble/src/services/spellbook.ts +++ b/jumble/src/services/spellbook.ts @@ -101,7 +101,7 @@ export async function saveSpell( ): Promise { try { // Get all the required data from commontools first - const { src, spec, parents } = await recipeManager.getRecipeMeta(recipe); + const { src, spec, parents } = recipeManager.getRecipeMeta(spell); const ui = spell.resultRef?.[UI]; if (spellId === undefined) { diff --git a/jumble/src/views/spellbook/SpellbookLaunchView.tsx b/jumble/src/views/spellbook/SpellbookLaunchView.tsx index 9b3a3c30b..a7bdde437 100644 --- a/jumble/src/views/spellbook/SpellbookLaunchView.tsx +++ b/jumble/src/views/spellbook/SpellbookLaunchView.tsx @@ -4,7 +4,7 @@ import { CharmsManagerProvider, useCharmManager, } from "@/contexts/CharmManagerContext.tsx"; -import { getRecipe } from "@commontools/runner"; +import { recipeManager } from "@commontools/runner"; import { createPath } from "@/routes.ts"; import { useAuthentication } from "@/contexts/AuthenticationContext.tsx"; import { AuthenticationView } from "@/views/AuthenticationView.tsx"; @@ -54,7 +54,10 @@ function Launcher() { await charmManager.syncRecipeById(spellId); console.log("Recipe sync completed"); - const recipe = getRecipe(spellId); + const recipe = await recipeManager.loadRecipe({ + recipeId: spellId, + space: charmManager.getSpace(), + }); console.log("Retrieved recipe:", recipe); if (!recipe) { diff --git a/runner/src/index.ts b/runner/src/index.ts index 7eb593e11..577bd55ab 100644 --- a/runner/src/index.ts +++ b/runner/src/index.ts @@ -39,16 +39,8 @@ export { } from "./doc-map.ts"; export { type AddCancel, type Cancel, noOp, useCancelGroup } from "./cancel.ts"; export { type Storage, storage } from "./storage.ts"; -export { setBobbyServerUrl, syncRecipeBlobby } from "./recipe-sync.ts"; -export { - getBlobbyServerUrl, - loadFromBlobby, - saveToBlobby, - setBlobbyServerUrl, -} from "./blobby-storage.ts"; -export { ConsoleMethod, runtime } from "./runtime/index.ts"; export { getBlobbyServerUrl, setBlobbyServerUrl } from "./blobby-storage.ts"; -export { tsToExports } from "./local-build.ts"; +export { ConsoleMethod, runtime } from "./runtime/index.ts"; export { addCommonIDfromObjectID, followAliases, diff --git a/runner/src/recipe-manager.ts b/runner/src/recipe-manager.ts index 4b934fa16..03bf737cd 100644 --- a/runner/src/recipe-manager.ts +++ b/runner/src/recipe-manager.ts @@ -23,7 +23,7 @@ import { storage } from "./storage.ts"; import { Cell } from "./cell.ts"; import { createRef } from "./doc-map.ts"; import { getCell } from "./cell.ts"; -import { buildRecipe } from "./local-build.ts"; +import { runtime } from "@commontools/runner"; import { getBlobbyServerUrl } from "./blobby-storage.ts"; // Schema definitions @@ -69,12 +69,13 @@ class RecipeManager { // returns the recipeMeta for a loaded recipe getRecipeMeta( input: Recipe | Module | { recipeId: string }, - ): RecipeMeta | undefined { + ): RecipeMeta { if ("recipeId" in input) { const recipe = this.recipeById(input.recipeId); - return recipe ? recipeMetaMap.get(recipe)?.get() : undefined; + if (!recipe) throw new Error(`Recipe ${input.recipeId} not loaded`); + return recipeMetaMap.get(recipe)?.get()!; } - return recipeMetaMap.get(input as Recipe)?.get(); + return recipeMetaMap.get(input as Recipe)?.get()!; } generateRecipeId(recipe: Recipe | Module, src?: string) { @@ -137,9 +138,10 @@ class RecipeManager { async loadRecipe( { space, recipeId }: { space: string; recipeId: string }, - ): Promise { - if (recipeIdMap.has(recipeId)) { - return recipeIdMap.get(recipeId); + ): Promise { + const existingRecipe = recipeIdMap.get(recipeId); + if (existingRecipe) { + return existingRecipe; } const metaCell = await this.getRecipeMetaCell({ recipeId, space }); @@ -157,16 +159,12 @@ class RecipeManager { recipeMetaMap.set(recipe, metaCell); return recipe; } - return undefined; + throw new Error(`Failed to import recipe ${recipeId} from blobby`); } const { src } = recipeMeta; - const { recipe, errors } = await buildRecipe(src!); - if (errors || !recipe) { - console.error(`Failed to build recipe ${recipeId}:`, errors); - return undefined; - } + const recipe = await runtime.compile(src!); metaCell.set(recipeMeta); await storage.syncCell(metaCell); @@ -182,12 +180,10 @@ class RecipeManager { // FIXME(ja): move this back to blobby! private async importFromBlobby( { recipeId }: { recipeId: string }, - ): Promise< - { recipe: Recipe; recipeMeta: RecipeMeta } | Record - > { + ): Promise<{ recipe: Recipe; recipeMeta: RecipeMeta }> { const response = await fetch(`${getBlobbyServerUrl()}/spell-${recipeId}`); if (!response.ok) { - return {}; + throw new Error(`Failed to fetch recipe ${recipeId} from blobby`); } const recipeJson = await response.json() as { @@ -196,29 +192,17 @@ class RecipeManager { parents?: string[]; }; - try { - const { recipe, errors } = await buildRecipe(recipeJson.src!); - if (errors || !recipe) { - console.error( - `Failed to build recipe ${recipeId} from Blobby:`, - errors, - ); - return {}; - } + const recipe = await runtime.compile(recipeJson.src!); - return { - recipe, - recipeMeta: { - id: recipeId, - src: recipeJson.src, - spec: recipeJson.spec, - parents: recipeJson.parents, - }, - }; - } catch (error) { - console.error(`Error loading recipe ${recipeId} from Blobby:`, error); - return {}; - } + return { + recipe, + recipeMeta: { + id: recipeId, + src: recipeJson.src, + spec: recipeJson.spec, + parents: recipeJson.parents, + }, + }; } // FIXME(ja): move this back to blobby! diff --git a/runner/src/recipe-sync.ts b/runner/src/recipe-sync.ts deleted file mode 100644 index d511e4b18..000000000 --- a/runner/src/recipe-sync.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - getRecipe, - getRecipeName, - getRecipeParents, - getRecipeSpec, - getRecipeSrc, - registerRecipe, -} from "./recipe-map.ts"; -import { - createItemsKnownToStorageSet, - getBlobbyServerUrl, - loadFromBlobby, - saveToBlobby, - setBlobbyServerUrl, -} from "./blobby-storage.ts"; -import { runtime } from "./runtime/index.ts"; - -// For backward compatibility -export function setBobbyServerUrl(url: string) { - setBlobbyServerUrl(url); -} - -// Track recipes known to storage to avoid redundant saves -const recipesKnownToStorage = createItemsKnownToStorageSet(); - -// FIXME(JA): this really really really needs to be revisited -export async function syncRecipeBlobby(id: string) { - if (getRecipe(id)) { - if (recipesKnownToStorage.has(id)) return; - const src = getRecipeSrc(id); - const spec = getRecipeSpec(id); - const parents = getRecipeParents(id); - if (src) saveRecipe(id, src, spec, parents); - return; - } - - const response = await loadFromBlobby<{ - src: string; - spec?: string; - parents?: string[]; - }>("spell", id); - - if (!response) return; - - const src = response.src; - const spec = response.spec || ""; - const parents = response.parents || []; - - const recipe = await runtime.compile(src); - - registerRecipe(id, recipe!, src, spec, parents); - recipesKnownToStorage.add(id); -} - -function saveRecipe( - id: string, - src: string, - spec?: string, - parents?: string[], - spellbookTitle?: string, - spellbookTags?: string[], -): Promise { - // If the recipe is already known to storage, we don't need to save it again, - // unless the user is trying to attach a spellbook title or tags. - if (recipesKnownToStorage.has(id) && !spellbookTitle) { - return Promise.resolve(true); - } - recipesKnownToStorage.add(id); - - const data = { - src, - recipe: JSON.parse(JSON.stringify(getRecipe(id))), - spec, - parents, - recipeName: getRecipeName(id), - spellbookTitle, - spellbookTags, - }; - - return saveToBlobby("spell", id, data); -} diff --git a/runner/src/runner.ts b/runner/src/runner.ts index e85e21cff..6c0057cf9 100644 --- a/runner/src/runner.ts +++ b/runner/src/runner.ts @@ -44,7 +44,6 @@ import { type CellLink, isCell, isCellLink } from "./cell.ts"; import { isQueryResultForDereferencing } from "./query-result-proxy.ts"; import { getCellLinkOrThrow } from "./query-result-proxy.ts"; import { storage } from "./storage.ts"; -import { syncRecipeBlobby } from "./recipe-sync.ts"; import { runtime } from "./runtime/index.ts"; export const cancels = new WeakMap, Cancel>(); diff --git a/runner/src/runtime/eval-runtime.ts b/runner/src/runtime/eval-runtime.ts index 37d254217..ebbac7571 100644 --- a/runner/src/runtime/eval-runtime.ts +++ b/runner/src/runtime/eval-runtime.ts @@ -15,14 +15,18 @@ export class UnsafeEvalRuntime extends EventTarget implements Runtime { // by the eval script scope. globalThis[RUNTIME_CONSOLE_HOOK] = new Console(this); } - async compile(source: string): Promise { + // FIXME(ja): perhaps we need the errors? + async compile(source: string): Promise { if (!source) { throw new Error("No source provided."); } const exports = await tsToExports(source, { injection: `const console = globalThis.${RUNTIME_CONSOLE_HOOK};`, }); - return "default" in exports ? exports.default : undefined; + if (!("default" in exports)) { + throw new Error("No default export found in compiled recipe."); + } + return exports.default; } getInvocation(source: string): RuntimeFunction { return eval(source); From 0d79e4850d95ecfae3d6d661bda8465b0d7251ed Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 08:57:54 -0400 Subject: [PATCH 03/11] missed await - thx jordan --- charm/src/manager.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/charm/src/manager.ts b/charm/src/manager.ts index 9ce35ee66..d7a0a48db 100644 --- a/charm/src/manager.ts +++ b/charm/src/manager.ts @@ -1256,13 +1256,13 @@ export class CharmManager { ); } - this.syncRecipe(charm); + await this.syncRecipe(charm); return charm; } // FIXME(JA): this really really really needs to be revisited - async syncRecipe(charm: Cell): Promise { + async syncRecipe(charm: Cell) { await storage.syncCell(charm); const sourceCell = charm.getSourceCell(); @@ -1272,8 +1272,6 @@ export class CharmManager { if (!recipeId) throw new Error("charm missing recipe ID"); await this.syncRecipeById(recipeId); - - return recipeId; } async syncRecipeById(recipeId: string) { From f3287f19d4d37108b11d45e750bdff0dd2217c7f Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 09:01:31 -0400 Subject: [PATCH 04/11] hmm, maybe the typechecking is easy to fix --- runner/src/runner.ts | 1 + runner/test/recipes.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/runner/src/runner.ts b/runner/src/runner.ts index 6c0057cf9..79f92556e 100644 --- a/runner/src/runner.ts +++ b/runner/src/runner.ts @@ -236,6 +236,7 @@ export function run( ); } + // NOTE(ja): perhaps this should actually return as a Cell? return resultCell; } diff --git a/runner/test/recipes.test.ts b/runner/test/recipes.test.ts index c45aa40f2..6cc86d21a 100644 --- a/runner/test/recipes.test.ts +++ b/runner/test/recipes.test.ts @@ -724,7 +724,7 @@ describe("Recipe Runner", () => { expect(errors).toBe(1); expect(charm.getAsQueryResult()).toMatchObject({ result: 5 }); - expect(lastError?.recipeId).toBe(getRecipeIdFromCharm(charm)); + expect(lastError?.recipeId).toBe(getRecipeIdFromCharm(charm.asCell())); expect(lastError?.space).toBe("test"); expect(lastError?.charmId).toBe( JSON.parse(JSON.stringify(charm.entityId))["/"], @@ -787,7 +787,7 @@ describe("Recipe Runner", () => { expect(errors).toBe(1); expect(charm.getAsQueryResult()).toMatchObject({ result: 10 }); - expect(lastError?.recipeId).toBe(getRecipeIdFromCharm(charm)); + expect(lastError?.recipeId).toBe(getRecipeIdFromCharm(charm.asCell())); expect(lastError?.space).toBe("test"); expect(lastError?.charmId).toBe( JSON.parse(JSON.stringify(charm.entityId))["/"], From 5f5f903fbb1280a15b3feb297065631b4cd6076b Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 10:12:22 -0400 Subject: [PATCH 05/11] finishing touches --- jumble/src/components/commands.ts | 3 --- jumble/src/views/CharmDetailView.tsx | 30 ++++++++++++++-------------- runner/src/recipe-manager.ts | 5 +++++ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/jumble/src/components/commands.ts b/jumble/src/components/commands.ts index f178aa387..e3c382336 100644 --- a/jumble/src/components/commands.ts +++ b/jumble/src/components/commands.ts @@ -447,9 +447,6 @@ async function handleUseDataInSpell(ctx: CommandContext) { const charm = await ctx.charmManager.get(ctx.focusedCharmId); if (!charm) throw new Error("No current charm found"); const argument = ctx.charmManager.getArgument(charm); - if (!argument) { - throw new Error("No sourceCell/argument found for current charm"); - } ctx.setLoading(true); const newCharm = await castSpellAsCharm( diff --git a/jumble/src/views/CharmDetailView.tsx b/jumble/src/views/CharmDetailView.tsx index d60e21037..f4981a254 100644 --- a/jumble/src/views/CharmDetailView.tsx +++ b/jumble/src/views/CharmDetailView.tsx @@ -1,16 +1,9 @@ import { Charm, charmId, - extractUserCode, extractVersionTag, - generateNewRecipeVersion, getIframeRecipe, - IFrameRecipe, - injectUserCode, modifyCharm, - processWorkflow, - WorkflowForm, - WorkflowType, } from "@commontools/charm"; import { useCharmReferences } from "@/hooks/use-charm-references.ts"; import { isCell, isStream } from "@commontools/runner"; @@ -35,11 +28,6 @@ import { useCharmManager } from "@/contexts/CharmManagerContext.tsx"; import { LoadingSpinner } from "@/components/Loader.tsx"; import { useCharm } from "@/hooks/use-charm.ts"; import CharmCodeEditor from "@/components/CharmCodeEditor.tsx"; -import CodeMirror from "@uiw/react-codemirror"; -import { javascript } from "@codemirror/lang-javascript"; -import { markdown } from "@codemirror/lang-markdown"; -import { json } from "@codemirror/lang-json"; -import { EditorView } from "@codemirror/view"; import { CharmRenderer } from "@/components/CharmRunner.tsx"; import { DitheredCube } from "@/components/DitherCube.tsx"; import { @@ -926,6 +914,20 @@ const DataTab = () => { ); + let argumentJson: Record; + try { + if (isArgumentExpanded) { + argumentJson = translateCellsAndStreamsToPlainJSON( + charmManager.getArgument(charm).get(), + ) as Record; + } else { + argumentJson = {}; + } + } catch (error) { + console.warn("Error translating argument to JSON:", error); + argumentJson = {}; + } + return (
{charm.getSourceCell() && ( @@ -943,9 +945,7 @@ const DataTab = () => {
{/* @ts-expect-error JsonView is imported as any */} Date: Sun, 27 Apr 2025 10:13:37 -0400 Subject: [PATCH 06/11] add note to remove patch --- runner/src/recipe-manager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runner/src/recipe-manager.ts b/runner/src/recipe-manager.ts index cfbe888b4..889ee2309 100644 --- a/runner/src/recipe-manager.ts +++ b/runner/src/recipe-manager.ts @@ -128,6 +128,8 @@ class RecipeManager { recipeIdMap.set(recipeId, recipe as Recipe); recipeMetaMap.set(recipe as Recipe, recipeMetaCell); + // FIXME(ja): in a week we should remove auto-publishing to blobby + // if this patch doesn't need to be reverted await this.publishToBlobby(recipeId); return true; From 78f7da0b0124ba6d84cf09ddfdc3d39d2edc8f69 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 14:17:05 -0400 Subject: [PATCH 07/11] fix seeder to work with blobby --- seeder/cli.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seeder/cli.ts b/seeder/cli.ts index f572d2c02..3e15447b5 100644 --- a/seeder/cli.ts +++ b/seeder/cli.ts @@ -1,5 +1,5 @@ import { parseArgs } from "@std/cli/parse-args"; -import { setBobbyServerUrl, storage } from "@commontools/runner"; +import { setBlobbyServerUrl, storage } from "@commontools/runner"; import { setLLMUrl } from "@commontools/llm"; import { processScenario } from "./processor.ts"; import { type ExecutedScenario } from "./interfaces.ts"; @@ -31,7 +31,7 @@ if (!name) { } storage.setRemoteStorage(new URL(toolshedUrl)); -setBobbyServerUrl(toolshedUrl); +setBlobbyServerUrl(toolshedUrl); setLLMUrl(toolshedUrl); // Track executed scenarios and steps From 76790fe278fa20d5e09660bc745e6d905eb3daaa Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 15:18:19 -0400 Subject: [PATCH 08/11] use promise map to only compile once once we start compiling, use a shared promise to send to anyone else asking for the compiled version --- runner/src/recipe-manager.ts | 66 +++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/runner/src/recipe-manager.ts b/runner/src/recipe-manager.ts index 889ee2309..5f6482ff7 100644 --- a/runner/src/recipe-manager.ts +++ b/runner/src/recipe-manager.ts @@ -26,6 +26,8 @@ import { getCell } from "./cell.ts"; import { runtime } from "@commontools/runner"; import { getBlobbyServerUrl } from "./blobby-storage.ts"; +const inProgressCompilations = new Map>(); + // Schema definitions export const recipeMetaSchema = { type: "object", @@ -140,35 +142,32 @@ class RecipeManager { return recipeIdMap.get(recipeId); } - async loadRecipe( - { space, recipeId }: { space: string; recipeId: string }, + // we need to ensure we only compile once otherwise we get ~12 +/- 4 + // compiles of each recipe + private async compileRecipeOnce( + recipeId: string, + space: string, ): Promise { - const existingRecipe = recipeIdMap.get(recipeId); - if (existingRecipe) { - return existingRecipe; - } - const metaCell = await this.getRecipeMetaCell({ recipeId, space }); + let recipeMeta = metaCell.get(); - const recipeMeta = metaCell.get(); - // if we don't have the recipeMeta, we should try to import from blobby - // as it might be from before we started saving recipes in cells + // 1. Fallback to Blobby if cell missing or stale if (recipeMeta.id !== recipeId) { - const { recipe, recipeMeta } = await this.importFromBlobby({ recipeId }); - if (recipe) { - metaCell.set(recipeMeta); - await storage.syncCell(metaCell); - await storage.synced(); - recipeIdMap.set(recipeId, recipe); - recipeMetaMap.set(recipe, metaCell); - return recipe; - } - throw new Error(`Failed to import recipe ${recipeId} from blobby`); + const imported = await this.importFromBlobby({ recipeId }); + recipeMeta = imported.recipeMeta; + metaCell.set(recipeMeta); + await storage.syncCell(metaCell); + await storage.synced(); + recipeIdMap.set(recipeId, imported.recipe); + recipeMetaMap.set(imported.recipe, metaCell); + return imported.recipe; } - const { src } = recipeMeta; - - const recipe = await runtime.compile(src!); + // 2. Compile from stored source + if (!recipeMeta.src) { + throw new Error(`Recipe ${recipeId} has no stored source`); + } + const recipe = await runtime.compile(recipeMeta.src); metaCell.set(recipeMeta); await storage.syncCell(metaCell); @@ -178,6 +177,27 @@ class RecipeManager { return recipe; } + async loadRecipe( + { space, recipeId }: { space: string; recipeId: string }, + ): Promise { + // quick paths + const existingRecipe = recipeIdMap.get(recipeId); + if (existingRecipe) { + return existingRecipe; + } + + if (inProgressCompilations.has(recipeId)) { + return inProgressCompilations.get(recipeId)!; + } + + // single-flight compilation + const compilationPromise = this.compileRecipeOnce(recipeId, space) + .finally(() => inProgressCompilations.delete(recipeId)); // tidy up + + inProgressCompilations.set(recipeId, compilationPromise); + return await compilationPromise; + } + /** * Load a recipe from Blobby, returning the recipe and recipeMeta */ From 169c0e8c805203861d1d8295939191c188dc4d42 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 27 Apr 2025 19:44:39 -0400 Subject: [PATCH 09/11] put the console log in the right place --- background-charm-service/cast-admin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/background-charm-service/cast-admin.ts b/background-charm-service/cast-admin.ts index b7d1e7fc5..88daef15f 100644 --- a/background-charm-service/cast-admin.ts +++ b/background-charm-service/cast-admin.ts @@ -69,8 +69,6 @@ async function castRecipe() { throw new Error("Cell ID is required"); } - console.log("Recipe compiled successfully"); - const targetCell = getCell( spaceId as DID, cause, @@ -96,6 +94,7 @@ async function castRecipe() { // Create charm manager for the specified space const charmManager = new CharmManager(session); const recipe = await compileRecipe(recipeSrc, "recipe", charmManager); + console.log("Recipe compiled successfully"); const charm = await charmManager.runPersistent( recipe, From 6dd2987e81c4e1d5ff82ec91f8263c08ec166d30 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:06:48 +1000 Subject: [PATCH 10/11] Wrap JSONified data in useMemo --- jumble/src/views/CharmDetailView.tsx | 44 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/jumble/src/views/CharmDetailView.tsx b/jumble/src/views/CharmDetailView.tsx index f4981a254..8c4aaf42a 100644 --- a/jumble/src/views/CharmDetailView.tsx +++ b/jumble/src/views/CharmDetailView.tsx @@ -893,6 +893,34 @@ const DataTab = () => { const [isLineageExpanded, setIsLineageExpanded] = useState(false); const [isReferencesExpanded, setIsReferencesExpanded] = useState(false); + const argumentJson = React.useMemo>(() => { + if (!isArgumentExpanded) { + return {}; + } + + try { + return translateCellsAndStreamsToPlainJSON( + charmManager.getArgument(charm)?.get(), + ) as Record; + } catch (error) { + console.warn("Error translating argument to JSON:", error); + return {}; + } + }, [isArgumentExpanded, charmManager, charm]); + + const resultJson = React.useMemo>(() => { + if (!isResultExpanded) { + return {}; + } + + try { + return translateCellsAndStreamsToPlainJSON(charm.get()) ?? {}; + } catch (error) { + console.warn("Error translating result to JSON:", error); + return {}; + } + }, [isResultExpanded, charm]); + if (!charm) return null; const lineage = charmManager.getLineage(charm); @@ -914,20 +942,6 @@ const DataTab = () => {
); - let argumentJson: Record; - try { - if (isArgumentExpanded) { - argumentJson = translateCellsAndStreamsToPlainJSON( - charmManager.getArgument(charm).get(), - ) as Record; - } else { - argumentJson = {}; - } - } catch (error) { - console.warn("Error translating argument to JSON:", error); - argumentJson = {}; - } - return (
{charm.getSourceCell() && ( @@ -970,7 +984,7 @@ const DataTab = () => {
{/* @ts-expect-error JsonView is imported as any */} <5009316+bfollington@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:16:08 +1000 Subject: [PATCH 11/11] Handle missing charm Edit commit to re-trigger actions --- jumble/src/views/CharmDetailView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jumble/src/views/CharmDetailView.tsx b/jumble/src/views/CharmDetailView.tsx index 8c4aaf42a..7af9c7955 100644 --- a/jumble/src/views/CharmDetailView.tsx +++ b/jumble/src/views/CharmDetailView.tsx @@ -894,7 +894,7 @@ const DataTab = () => { const [isReferencesExpanded, setIsReferencesExpanded] = useState(false); const argumentJson = React.useMemo>(() => { - if (!isArgumentExpanded) { + if (!isArgumentExpanded || !charm) { return {}; } @@ -909,7 +909,7 @@ const DataTab = () => { }, [isArgumentExpanded, charmManager, charm]); const resultJson = React.useMemo>(() => { - if (!isResultExpanded) { + if (!isResultExpanded || !charm) { return {}; }