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 {};
}
| | |