diff --git a/packages/patterns/charms-ref-in-cell.tsx b/packages/patterns/charms-ref-in-cell.tsx index 3b33756da1..6a26347acd 100644 --- a/packages/patterns/charms-ref-in-cell.tsx +++ b/packages/patterns/charms-ref-in-cell.tsx @@ -2,7 +2,7 @@ import { Cell, cell, - createCell, + Default, derive, h, handler, @@ -11,59 +11,36 @@ import { NAME, navigateTo, recipe, + toSchema, UI, } from "commontools"; -// the simple charm (to which we'll store references within a cell) -const SimpleRecipe = recipe("Simple Recipe", () => ({ - [NAME]: "Some Simple Recipe", - [UI]:
Some Simple Recipe
, -})); +type Charm = { + [NAME]: string; + [UI]: string; + [key: string]: any; +}; -// Create a cell to store an array of charms -const createCellRef = lift( - { - type: "object", - properties: { - isInitialized: { type: "boolean", default: false, asCell: true }, - storedCellRef: { type: "object", asCell: true }, - }, - }, - undefined, - ({ isInitialized, storedCellRef }) => { - if (!isInitialized.get()) { - console.log("Creating cellRef - first time"); - const newCellRef = createCell(undefined, "charmsArray"); - newCellRef.set([]); - storedCellRef.set(newCellRef); - isInitialized.set(true); - return { - cellRef: newCellRef, - }; - } else { - console.log("cellRef already initialized"); - } - // If already initialized, return the stored cellRef - return { - cellRef: storedCellRef, - }; - }, -); +// Define interfaces for type safety +interface AddCharmState { + charm: any; + cellRef: Cell; + isInitialized: Cell; +} +const AddCharmSchema = toSchema(); -// Add a charm to the array and navigate to it -// we get a new isInitialized passed in for each -// charm we add to the list. this makes sure -// we only try to add the charm once to the list -// and we only call navigateTo once +// Simple charm that will be instantiated multiple times +const SimpleRecipe = recipe<{ id: string }>("Simple Recipe", ({ id }) => ({ + [NAME]: derive(id, (idValue) => `SimpleRecipe: ${idValue}`), + [UI]:
Simple Recipe id {id}
, +})); + +// Lift that adds a charm to the array and navigates to it. +// The isInitialized flag prevents duplicate additions: +// - Without it: lift runs → adds to array → array changes → lift runs again → duplicate +// - With it: lift runs once → sets isInitialized → subsequent runs skip const addCharmAndNavigate = lift( - { - type: "object", - properties: { - charm: { type: "object" }, - cellRef: { type: "array", asCell: true }, - isInitialized: { type: "boolean", asCell: true }, - }, - }, + AddCharmSchema, undefined, ({ charm, cellRef, isInitialized }) => { if (!isInitialized.get()) { @@ -79,14 +56,18 @@ const addCharmAndNavigate = lift( }, ); -// Create a new SimpleRecipe and add it to the array -const createSimpleRecipe = handler }>( +// Handler that creates a new charm instance and adds it to the array. +// Each invocation creates its own isInitialized cell for tracking. +const createSimpleRecipe = handler }>( (_, { cellRef }) => { // Create isInitialized cell for this charm addition const isInitialized = cell(false); - // Create the charm - const charm = SimpleRecipe({}); + // Create a random 5-digit ID + const randomId = Math.floor(10000 + Math.random() * 90000).toString(); + + // Create the charm with unique ID + const charm = SimpleRecipe({ id: randomId }); // Store the charm in the array and navigate return addCharmAndNavigate({ charm, cellRef, isInitialized }); @@ -94,53 +75,55 @@ const createSimpleRecipe = handler }>( ); // Handler to navigate to a specific charm from the list -const goToCharm = handler( +const goToCharm = handler( (_, { charm }) => { console.log("goToCharm clicked"); return navigateTo(charm); }, ); -// create the named cell inside the recipe body, so we do it just once -export default recipe("Charms Launcher", () => { - // cell to store array of charms we created - const { cellRef } = createCellRef({ - isInitialized: cell(false), - storedCellRef: cell(), - }); +// Recipe input/output type +type RecipeInOutput = { + cellRef: Default; +}; - return { - [NAME]: "Charms Launcher", - [UI]: ( -
-

Stored Charms:

- {ifElse( - !cellRef?.length, -
No charms created yet
, -
    - {cellRef.map((charm: any, index: number) => ( -
  • - - Go to Charm {derive(index, (i) => i + 1)} - - - Charm {derive(index, (i) => i + 1)}:{" "} - {charm[NAME] || "Unnamed"} - -
  • - ))} -
, - )} +// Main recipe that manages an array of charm references +export default recipe( + "Charms Launcher", + ({ cellRef }) => { + return { + [NAME]: "Charms Launcher", + [UI]: ( +
+

Stored Charms:

+ {ifElse( + !cellRef?.length, +
No charms created yet
, +
    + {cellRef.map((charm: any, index: number) => ( +
  • + + Go to Charm {derive(index, (i) => i + 1)} + + + Charm {derive(index, (i) => i + 1)}:{" "} + {charm[NAME] || "Unnamed"} + +
  • + ))} +
, + )} - - Create New Charm - -
- ), - cellRef, - }; -}); + + Create New Charm + +
+ ), + cellRef, + }; + }, +); diff --git a/packages/runner/src/traverse.ts b/packages/runner/src/traverse.ts index cfa8bf47b4..9441e9d6bd 100644 --- a/packages/runner/src/traverse.ts +++ b/packages/runner/src/traverse.ts @@ -468,10 +468,6 @@ function followPointer( const target: BaseMemoryAddress = (link.id !== undefined) ? { id: link.id, type: "application/json" } : doc.address; - const targetDoc = { - address: doc.address, - value: doc.value, - }; if (selector !== undefined) { // We'll need to re-root the selector for the target doc // Remove the portions of doc.path from selector.path, limiting schema if @@ -492,43 +488,47 @@ function followPointer( // Cycle detected - treat this as notFound to avoid traversal return [notFound(doc.address), selector]; } + // We may access portions of the doc outside what we have in our doc + // attestation, so reload the top level doc from the manager. + const valueEntry = manager.load(target); + if (valueEntry === null) { + return [notFound(doc.address), selector]; + } if (link.id !== undefined) { - // We have a reference to a different cell, so track the dependency + // We have a reference to a different doc, so track the dependency // and update our targetDoc - const valueEntry = manager.load(target); - if (valueEntry === null) { - return [notFound(doc.address), selector]; - } if (schemaTracker !== undefined && selector !== undefined) { schemaTracker.add(manager.toKey(target), selector); } - // If the object we're pointing to is a retracted fact, just return undefined. - // We can't do a better match, but we do want to include the result so we watch this doc - if (valueEntry.value === undefined) { - return [notFound(target), selector]; + // Load the sources/recipes recursively unless we're a retracted fact. + if (valueEntry.value !== undefined) { + loadSource( + manager, + valueEntry, + new Set(), + schemaTracker, + ); } - // Otherwise, we can continue with the target. - // an assertion fact.is will be an object with a value property, and - // that's what our schema is relative to. - targetDoc.address = { ...target, path: ["value"] }; - targetDoc.value = (valueEntry.value as Immutable)["value"]; - // Load any sources (recursively) if they exist and any linked recipes - loadSource( - manager, - valueEntry, - new Set(), - schemaTracker, - ); } + // If the object we're pointing to is a retracted fact, just return undefined. + // We can't do a better match, but we do want to include the result so we watch this doc + if (valueEntry.value === undefined) { + return [notFound(target), selector]; + } + // We can continue with the target, but provide the top level target doc + // to getAtPath. + // An assertion fact.is will be an object with a value property, and + // that's what our schema is relative to, so we'll grab the value part. + const targetDoc = { + address: { ...target, path: ["value"] }, + value: (valueEntry.value as Immutable)["value"], + }; // We've loaded the linked doc, so walk the path to get to the right part of that doc (or whatever doc that path leads to), // then the provided path from the arguments. return getAtPath( manager, - { - address: targetDoc.address, - value: targetDoc.value, - }, + targetDoc, [...link.path, ...path] as string[], tracker, schemaTracker, diff --git a/packages/runner/test/traverse.test.ts b/packages/runner/test/traverse.test.ts new file mode 100644 index 0000000000..16272ca379 --- /dev/null +++ b/packages/runner/test/traverse.test.ts @@ -0,0 +1,105 @@ +import { describe, it } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { refer } from "merkle-reference/json"; +import type { + Entity, + Revision, + State, + URI, +} from "@commontools/memory/interface"; +import { SchemaObjectTraverser } from "../src/traverse.ts"; +import { StoreObjectManager } from "../src/storage/query.ts"; + +describe("SchemaObjectTraverser.traverseDAG", () => { + it("follows legacy cell links when traversing", () => { + const store = new Map>(); + const type = "application/json" as const; + const doc1Uri = "of:doc-1" as URI; + const doc2Uri = "of:doc-2" as URI; + const doc1Entity = doc1Uri as Entity; + const doc2Entity = doc2Uri as Entity; + + const doc1Value = { employees: [{ name: "Bob" }] }; + const doc1EntityId = { "/": doc1Uri }; + + const doc1Revision: Revision = { + the: type, + of: doc1Entity, + is: { value: doc1Value }, + cause: refer({ the: type, of: doc1Entity }), + since: 1, + }; + store.set( + `${doc1Revision.of}/${doc1Revision.the}`, + doc1Revision, + ); + + const doc2Value = { + employeeName: { + cell: doc1EntityId, + path: ["employees", "0", "name"], + }, + argument: { + tools: { + search_web: { + pattern: { + result: { + $alias: { + path: ["internal", "__#0"], + }, + }, + }, + }, + }, + }, + internal: { + "__#0": { + name: "Foo", + }, + }, + }; + + const doc2Revision: Revision = { + the: type, + of: doc2Entity, + is: { value: doc2Value }, + cause: refer({ the: type, of: doc2Entity }), + since: 2, + }; + store.set( + `${doc2Revision.of}/${doc2Revision.the}`, + doc2Revision, + ); + + const manager = new StoreObjectManager(store); + const traverser = new SchemaObjectTraverser(manager, { + path: [], + schemaContext: { schema: true, rootSchema: true }, + }); + + const result = traverser.traverse({ + address: { id: doc2Uri, type, path: ["value"] }, + value: doc2Value, + }); + + expect(result).toEqual({ + argument: { + tools: { + search_web: { + pattern: { + result: { + name: "Foo", + }, + }, + }, + }, + }, + employeeName: "Bob", + internal: { + "__#0": { + name: "Foo", + }, + }, + }); + }); +});