Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 77 additions & 94 deletions packages/patterns/charms-ref-in-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {
Cell,
cell,
createCell,
Default,
derive,
h,
handler,
Expand All @@ -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]: <div>Some Simple Recipe</div>,
}));
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<Charm[]>;
isInitialized: Cell<boolean>;
}
const AddCharmSchema = toSchema<AddCharmState>();

// 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]: <div>Simple Recipe id {id}</div>,
}));

// 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()) {
Expand All @@ -79,68 +56,74 @@ const addCharmAndNavigate = lift(
},
);

// Create a new SimpleRecipe and add it to the array
const createSimpleRecipe = handler<unknown, { cellRef: Cell<any[]> }>(
// 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<unknown, { cellRef: Cell<Charm[]> }>(
(_, { 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 });
},
);

// Handler to navigate to a specific charm from the list
const goToCharm = handler<unknown, { charm: any }>(
const goToCharm = handler<unknown, { charm: Charm }>(
(_, { 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<Charm[], []>;
};

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

<ct-button
onClick={createSimpleRecipe({ cellRef })}
>
Create New Charm
</ct-button>
</div>
),
cellRef,
};
});
<ct-button
onClick={createSimpleRecipe({ cellRef })}
>
Create New Charm
</ct-button>
</div>
),
cellRef,
};
},
);
58 changes: 29 additions & 29 deletions packages/runner/src/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,6 @@ function followPointer<S extends BaseMemoryAddress>(
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
Expand All @@ -492,43 +488,47 @@ function followPointer<S extends BaseMemoryAddress>(
// 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<string>(),
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<JSONObject>)["value"];
// Load any sources (recursively) if they exist and any linked recipes
loadSource(
manager,
valueEntry,
new Set<string>(),
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<JSONObject>)["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,
Expand Down
Loading
Loading