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",
+ },
+ },
+ });
+ });
+});