diff --git a/packages/background-charm-service/src/utils.ts b/packages/background-charm-service/src/utils.ts
index e20895855..53ebfa1ef 100644
--- a/packages/background-charm-service/src/utils.ts
+++ b/packages/background-charm-service/src/utils.ts
@@ -123,7 +123,7 @@ export async function setBGCharm({
console.log(
"charmsCell",
- JSON.stringify(charmsCell.getAsCellLink(), null, 2),
+ JSON.stringify(charmsCell.getAsLink(), null, 2),
);
const charms = charmsCell.get() || [];
diff --git a/packages/charm/src/iframe/recipe.ts b/packages/charm/src/iframe/recipe.ts
index 421b9008f..99902a57e 100644
--- a/packages/charm/src/iframe/recipe.ts
+++ b/packages/charm/src/iframe/recipe.ts
@@ -1,5 +1,9 @@
-import { JSONSchema } from "@commontools/runner";
-import { Cell, getEntityId, type Runtime } from "@commontools/runner";
+import {
+ Cell,
+ getEntityId,
+ type JSONSchema,
+ type Runtime,
+} from "@commontools/runner";
import { Charm, getRecipeIdFromCharm } from "../manager.ts";
export type IFrameRecipe = {
diff --git a/packages/charm/src/iterate.ts b/packages/charm/src/iterate.ts
index bf466f50c..4e764e108 100644
--- a/packages/charm/src/iterate.ts
+++ b/packages/charm/src/iterate.ts
@@ -264,19 +264,21 @@ export function scrub(data: any): any {
}
/**
- * Turn cells references into aliases, this forces writes to go back
- * to the original cell.
+ * Turn cells references into writes redirects, this forces writes to go back to
+ * the original cell.
+ * @param data The data to process
+ * @param baseSpace Optional base space DID to make links relative to
*/
-function turnCellsIntoAliases(data: any): any {
+function turnCellsIntoWriteRedirects(data: any, baseSpace?: MemorySpace): any {
if (isCell(data)) {
- return { $alias: data.getAsCellLink() };
+ return data.getAsWriteRedirectLink(baseSpace ? { baseSpace } : undefined);
} else if (Array.isArray(data)) {
- return data.map((value) => turnCellsIntoAliases(value));
+ return data.map((value) => turnCellsIntoWriteRedirects(value, baseSpace));
} else if (isObject(data)) {
return Object.fromEntries(
Object.entries(data).map((
[key, value],
- ) => [key, turnCellsIntoAliases(value)]),
+ ) => [key, turnCellsIntoWriteRedirects(value, baseSpace)]),
);
} else return data;
}
@@ -478,7 +480,11 @@ export async function castNewRecipe(
const scrubbed = scrub(form.input.references);
// First, extract any existing schema if we have data
- const existingSchema = createJsonSchema(scrubbed);
+ const existingSchema = createJsonSchema(
+ scrubbed,
+ false,
+ charmManager.runtime,
+ );
// Prototype workflow: combine steps
const { newSpec, newRecipeSrc, llmRequestId } =
@@ -486,7 +492,7 @@ export async function castNewRecipe(
? await singlePhaseCodeGeneration(form, existingSchema)
: await twoPhaseCodeGeneration(form, existingSchema);
- const input = turnCellsIntoAliases(scrubbed);
+ const input = turnCellsIntoWriteRedirects(scrubbed, charmManager.getSpace());
globalThis.dispatchEvent(
new CustomEvent("job-update", {
diff --git a/packages/charm/src/manager.ts b/packages/charm/src/manager.ts
index f34d68329..f86ba00d9 100644
--- a/packages/charm/src/manager.ts
+++ b/packages/charm/src/manager.ts
@@ -1,29 +1,24 @@
import {
+ type Cell,
Classification,
- isAlias,
+ EntityId,
+ getEntityId,
+ isCell,
+ isLink,
JSONSchema,
+ type MemorySpace,
Module,
NAME,
+ parseLink,
Recipe,
+ Runtime,
Schema,
TYPE,
UI,
-} from "@commontools/runner";
-import {
- type Cell,
- createRef,
- EntityId,
- followAliases,
- getEntityId,
- isCell,
- isCellLink,
- isDoc,
- maybeGetCellLink,
- type MemorySpace,
- Runtime,
+ URI,
} from "@commontools/runner";
import { type Session } from "@commontools/identity";
-import { isObject } from "@commontools/utils/types";
+import { isObject, isRecord } from "@commontools/utils/types";
/**
* Extracts the ID from a charm.
@@ -435,7 +430,7 @@ export class CharmManager {
let argumentLink: any;
try {
- argumentLink = argumentCell.getAsCellLink();
+ argumentLink = argumentCell.getAsLegacyCellLink();
if (!argumentLink || !argumentLink.cell) return result;
argumentValue = argumentLink.cell.getAtPath(argumentLink.path);
@@ -508,11 +503,8 @@ export class CharmManager {
// If we've reached the end and have a resultRef, return it
if (value.resultRef) {
- // Use maybeGetCellLink for safer access to resultRef
- const resultLink = maybeGetCellLink(value.resultRef);
- if (resultLink) {
- return getEntityId(resultLink.cell);
- }
+ const { id: source } = parseLink(value.resultRef, cell)!;
+ if (source) return getEntityId(source);
}
}
} catch (err) {
@@ -534,7 +526,7 @@ export class CharmManager {
visited = new Set(), // Track objects directly, not string representations
depth = 0,
) => {
- if (!value || typeof value !== "object" || depth > maxDepth) return;
+ if (!isRecord(value) || depth > maxDepth) return;
// Prevent cycles in our traversal by tracking object references directly
if (visited.has(value)) return;
@@ -542,164 +534,21 @@ export class CharmManager {
try {
// Handle values that are themselves cells, docs, or cell links
- if (isCell(value)) {
- try {
- const cellLink = value.getAsCellLink();
- if (cellLink && cellLink.cell) {
- const cellId = getEntityId(cellLink.cell);
- if (cellId) addMatchingCharm(cellId);
-
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(cellLink),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- }
- } catch (err) {
- console.debug("Error handling cell:", err);
+ if (isLink(value)) {
+ const link = parseLink(value, parent);
+ if (link.id) {
+ addMatchingCharm(getEntityId(link.id)!);
}
- return; // Don't process contents of cells
- }
- if (isDoc(value)) {
- try {
- const docId = getEntityId(value);
- if (docId) addMatchingCharm(docId);
-
- const sourceRefId = followSourceToResultRef(
- value.asCell(),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- } catch (err) {
- console.debug("Error handling doc:", err);
- }
- return; // Don't process contents of docs
- }
-
- if (isCellLink(value)) {
- try {
- const cellId = getEntityId(value.cell);
- if (cellId) addMatchingCharm(cellId);
-
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(value),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- } catch (err) {
- console.debug("Error handling cell link:", err);
- }
- return; // Don't process contents of cell links
- }
-
- // Process aliases - follow them to their sources
- if (isAlias(value)) {
- try {
- // Use followAliases, which is safer than manual traversal
- const cellLink = followAliases(value, parent.getDoc());
- if (cellLink && cellLink.cell) {
- const cellId = getEntityId(cellLink.cell);
- if (cellId) addMatchingCharm(cellId);
-
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(cellLink),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- }
- } catch (err) {
- console.debug("Error following aliases:", err);
- }
- return; // Aliases have been fully handled
- }
-
- // Try to get a cell link from various types of values
- const cellLink = maybeGetCellLink(value, parent.getDoc());
- if (cellLink) {
- try {
- const cellId = getEntityId(cellLink.cell);
- if (cellId) addMatchingCharm(cellId);
-
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(cellLink),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- } catch (err) {
- console.debug("Error handling cell link from value:", err);
- }
- return; // Direct cell references fully handled
- }
-
- // Direct $alias handling (for cases not caught by isAlias)
- if (value.$alias && value.$alias.cell) {
- try {
- const aliasId = getEntityId(value.$alias.cell);
- if (aliasId) addMatchingCharm(aliasId);
-
- const sourceRefId = followSourceToResultRef(
- value.$alias.cell.asCell(),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- } catch (err) {
- console.debug("Error handling alias reference:", err);
- }
- }
-
- // Direct cell reference handling (for cases not caught by maybeGetCellLink)
- if (value.cell && value.path !== undefined) {
- try {
- const cellId = getEntityId(value.cell);
- if (cellId) addMatchingCharm(cellId);
-
- const sourceRefId = followSourceToResultRef(
- value.cell.asCell(),
- new Set(),
- 0,
- );
- if (sourceRefId) addMatchingCharm(sourceRefId);
- } catch (err) {
- console.debug("Error handling direct cell reference:", err);
- }
- }
-
- // Safe recursive processing of arrays
- if (Array.isArray(value)) {
+ const sourceRefId = followSourceToResultRef(
+ this.runtime.getCellFromLink(link),
+ new Set(),
+ 0,
+ );
+ if (sourceRefId) addMatchingCharm(sourceRefId);
+ } else if (Array.isArray(value)) {
+ // Safe recursive processing of arrays
for (let i = 0; i < value.length; i++) {
- // Skip null/undefined items
- if (value[i] == null) continue;
-
- // Skip items that might be cells to avoid Copy trap
- if (
- typeof value[i] === "object" &&
- (isCell(value[i]) || isDoc(value[i]) || isCellLink(value[i]))
- ) {
- try {
- // Process each cell directly
- processValue(
- value[i],
- parent,
- new Set([...visited]),
- depth + 1,
- );
- } catch (err) {
- console.debug(
- `Error processing special array item at index ${i}:`,
- err,
- );
- }
- continue;
- }
-
- // Process regular items
try {
processValue(
value[i],
@@ -720,14 +569,6 @@ export class CharmManager {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
- // Skip properties that might be or contain Cell objects
- if (
- key === "sourceCell" || key === "cell" || key === "value" ||
- key === "getAsCellLink" || key === "getSourceCell"
- ) {
- continue;
- }
-
try {
processValue(
value[key],
@@ -811,19 +652,14 @@ export class CharmManager {
cell: Cell,
visited = new Set(),
depth = 0,
- ): EntityId | undefined => {
+ ): URI | undefined => {
if (depth > maxDepth) return undefined; // Prevent infinite recursion
- const docId = getEntityId(cell);
- if (!docId || !docId["/"]) return undefined;
-
- const docIdStr = typeof docId["/"] === "string"
- ? docId["/"]
- : JSON.stringify(docId["/"]);
+ const cellURI = cell.sourceURI;
// Prevent cycles
- if (visited.has(docIdStr)) return undefined;
- visited.add(docIdStr);
+ if (visited.has(cellURI)) return undefined;
+ visited.add(cellURI);
// If document has a sourceCell, follow it
const value = cell.getRaw();
@@ -837,10 +673,10 @@ export class CharmManager {
// If we've reached the end and have a resultRef, return it
if (value && typeof value === "object" && value.resultRef) {
- return getEntityId(value.resultRef);
+ return parseLink(value.resultRef, cell)?.id;
}
- return docId; // Return the current document's ID if no further references
+ return cellURI; // Return the current document's ID if no further references
};
// Helper to check if a document refers to our target charm
@@ -850,80 +686,27 @@ export class CharmManager {
visited = new Set(), // Track objects directly, not string representations
depth = 0,
): boolean => {
- if (!value || typeof value !== "object" || depth > maxDepth) return false;
+ if (!isRecord(value) || depth > maxDepth) return false;
// Prevent cycles in our traversal by tracking object references directly
if (visited.has(value)) return false;
visited.add(value);
try {
- // Handle cells, docs, and cell links directly
- if (isCell(value)) {
+ if (isLink(value)) {
try {
- const cellLink = value.getAsCellLink();
- if (cellLink && cellLink.cell) {
- // Check if this cell's doc is our target
- const cellId = getEntityId(cellLink.cell);
- if (cellId && cellId["/"] === charmId["/"]) {
- return true;
- }
+ const link = parseLink(value, parent);
- // Check if this cell's source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(cellLink),
- new Set(),
- 0,
- );
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
- }
- } catch (err) {
- console.debug("Error handling cell in checkRefersToTarget:", err);
- }
- return false; // Don't process cell contents
- }
-
- if (isDoc(value)) {
- try {
- // Check if this doc is our target
- const docId = getEntityId(value);
- if (docId && docId["/"] === charmId["/"]) {
- return true;
- }
-
- // Check if this doc's source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- value.asCell(),
- new Set(),
- 0,
- );
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
- } catch (err) {
- console.debug("Error handling doc in checkRefersToTarget:", err);
- }
- return false; // Don't process doc contents
- }
-
- if (isCellLink(value)) {
- try {
// Check if the cell link's doc is our target
- const cellId = getEntityId(value.cell);
- if (cellId && cellId["/"] === charmId["/"]) {
- return true;
- }
+ if (link.id === charm.sourceURI) return true;
// Check if cell link's source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- value.cell.asCell(),
+ const sourceResultRefURI = followSourceToResultRef(
+ this.runtime.getCellFromLink(link),
new Set(),
0,
);
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
+ if (sourceResultRefURI === charm.sourceURI) return true;
} catch (err) {
console.debug(
"Error handling cell link in checkRefersToTarget:",
@@ -933,150 +716,9 @@ export class CharmManager {
return false; // Don't process cell link contents
}
- // Use isAlias and followAliases for aliases
- if (isAlias(value)) {
- try {
- // Follow all aliases to their source
- const cellLink = followAliases(value, parent.getDoc());
- if (cellLink && cellLink.cell) {
- // Check if the aliased doc is our target
- const cellId = getEntityId(cellLink.cell);
- if (cellId && cellId["/"] === charmId["/"]) {
- return true;
- }
-
- // Check if source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(cellLink),
- new Set(),
- 0,
- );
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
- }
- } catch (err) {
- console.debug(
- "Error following aliases in checkRefersToTarget:",
- err,
- );
- }
- return false; // Aliases have been fully handled
- }
-
- // Use maybeGetCellLink to handle various reference types
- const cellLink = maybeGetCellLink(value, parent.getDoc());
- if (cellLink) {
- try {
- // Check if the linked doc is our target
- const cellId = getEntityId(cellLink.cell);
- if (cellId && cellId["/"] === charmId["/"]) {
- return true;
- }
-
- // Check if source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- this.runtime.getCellFromLink(cellLink),
- new Set(),
- 0,
- );
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
- } catch (err) {
- console.debug(
- "Error handling maybeGetCellLink in checkRefersToTarget:",
- err,
- );
- }
- return false; // Cell link has been fully handled
- }
-
- // Direct $alias handling (for cases not caught by isAlias)
- if (value.$alias && value.$alias.cell) {
- try {
- // Check if the alias points to our target
- const aliasId = getEntityId(value.$alias.cell);
- if (aliasId && aliasId["/"] === charmId["/"]) {
- return true;
- }
-
- // Check if source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- value.$alias.cell,
- new Set(),
- 0,
- );
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
- } catch (err) {
- console.debug(
- "Error handling direct alias in checkRefersToTarget:",
- err,
- );
- }
- }
-
- // Direct cell reference handling (for cases not caught by maybeGetCellLink)
- if (value.cell && value.path !== undefined) {
- try {
- // Check if cell reference points to our target
- const cellId = getEntityId(value.cell);
- if (cellId && cellId["/"] === charmId["/"]) {
- return true;
- }
-
- // Check if source chain leads to our target
- const sourceRefId = followSourceToResultRef(
- value.cell,
- new Set(),
- 0,
- );
- if (sourceRefId && sourceRefId["/"] === charmId["/"]) {
- return true;
- }
- } catch (err) {
- console.debug(
- "Error handling direct cell ref in checkRefersToTarget:",
- err,
- );
- }
- }
-
// Safe recursive processing of arrays
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
- // Skip null/undefined items
- if (value[i] == null) continue;
-
- // Handle cells carefully
- if (
- typeof value[i] === "object" &&
- (isCell(value[i]) || isDoc(value[i]) || isCellLink(value[i]))
- ) {
- try {
- // Process cells directly to avoid copy trap
- if (
- checkRefersToTarget(
- value[i],
- parent,
- new Set([...visited]),
- depth + 1,
- )
- ) {
- return true;
- }
- } catch (err) {
- console.debug(
- `Error checking special array item at index ${i}:`,
- err,
- );
- }
- continue;
- }
-
- // Regular value processing
try {
if (
checkRefersToTarget(
@@ -1092,20 +734,12 @@ export class CharmManager {
console.debug(`Error checking array item at index ${i}:`, err);
}
}
- } else if (typeof value === "object") {
+ } else if (isRecord(value)) {
// Process regular object properties
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
- // Skip properties that might be or contain Cell objects
- if (
- key === "sourceCell" || key === "cell" || key === "value" ||
- key === "getAsCellLink" || key === "getSourceCell"
- ) {
- continue;
- }
-
try {
if (
checkRefersToTarget(
@@ -1133,53 +767,28 @@ export class CharmManager {
for (const otherCharm of allCharms) {
if (isSameEntity(otherCharm, charm)) continue; // Skip self
- // First check the charm document
- try {
- const otherCellLink = otherCharm.getAsCellLink();
- if (!otherCellLink.cell) continue;
-
- const charmValue = otherCellLink.cell.get();
-
- // Check if the charm document references our target
- if (charmValue && typeof charmValue === "object") {
- if (
- checkRefersToTarget(
- charmValue,
- this.runtime.getCellFromLink(otherCellLink),
- new Set(),
- 0,
- )
- ) {
- addReadingCharm(otherCharm);
- continue; // Skip additional checks for this charm
- }
- }
- } catch (err) {
- // Error checking charm references - continue to check argument references
+ if (checkRefersToTarget(otherCharm, otherCharm, new Set(), 0)) {
+ addReadingCharm(otherCharm);
+ continue; // Skip additional checks for this charm
}
// Also specifically check the argument data where references are commonly found
try {
const argumentCell = this.getArgument(otherCharm);
if (argumentCell) {
- const argumentLink = argumentCell.getAsCellLink();
- if (argumentLink && argumentLink.cell) {
- const argumentValue = argumentLink.cell.getAtPath(
- argumentLink.path,
- );
+ const argumentValue = argumentCell.getRaw();
- // Check if the argument references our target
- if (argumentValue && typeof argumentValue === "object") {
- if (
- checkRefersToTarget(
- argumentValue,
- this.runtime.getCellFromLink(argumentLink),
- new Set(),
- 0,
- )
- ) {
- addReadingCharm(otherCharm);
- }
+ // Check if the argument references our target
+ if (argumentValue && typeof argumentValue === "object") {
+ if (
+ checkRefersToTarget(
+ argumentValue,
+ argumentCell,
+ new Set(),
+ 0,
+ )
+ ) {
+ addReadingCharm(otherCharm);
}
}
}
diff --git a/packages/charm/test/charm-references.test.ts b/packages/charm/test/charm-references.test.ts
index 724257f0b..673ba5489 100644
--- a/packages/charm/test/charm-references.test.ts
+++ b/packages/charm/test/charm-references.test.ts
@@ -1,14 +1,6 @@
import { assertEquals } from "@std/assert";
-import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
-import { CharmManager } from "../src/manager.ts";
-import {
- Cell,
- DocImpl,
- EntityId,
- getEntityId,
- maybeGetCellLink,
-} from "@commontools/runner";
-import { NAME } from "@commontools/runner";
+import { describe, it } from "@std/testing/bdd";
+import { EntityId } from "@commontools/runner";
// Create a mock environment for testing reference detection
describe("Charm reference detection", () => {
diff --git a/packages/charm/test/reference-detection.test.ts b/packages/charm/test/reference-detection.test.ts
index dab58eb97..880dcff37 100644
--- a/packages/charm/test/reference-detection.test.ts
+++ b/packages/charm/test/reference-detection.test.ts
@@ -1,6 +1,5 @@
import { assertEquals } from "@std/assert";
import { describe, it } from "@std/testing/bdd";
-import { maybeGetCellLink } from "@commontools/runner";
/**
* These tests focus on the core functionality used by our charm reference detection
diff --git a/packages/cli/lib/charm.ts b/packages/cli/lib/charm.ts
index ac41ace0b..485a8fdad 100644
--- a/packages/cli/lib/charm.ts
+++ b/packages/cli/lib/charm.ts
@@ -288,8 +288,6 @@ export async function linkCharms(
sourceResultCell = sourceResultCell.key(segment);
}
- const sourceCellLink = sourceResultCell.getAsCellLink();
-
// Navigate to the target path
const targetKey = targetPath.pop();
if (!targetKey) {
@@ -310,7 +308,7 @@ export async function linkCharms(
targetInputCell = targetInputCell.key(segment);
}
- targetInputCell.key(targetKey).set(sourceCellLink);
+ targetInputCell.key(targetKey).set(sourceResultCell);
await manager.runtime.idle();
await manager.synced();
diff --git a/packages/jumble/integration/basic-flow.test.ts b/packages/jumble/integration/basic-flow.test.ts
index 9b78d7541..a36669917 100644
--- a/packages/jumble/integration/basic-flow.test.ts
+++ b/packages/jumble/integration/basic-flow.test.ts
@@ -17,10 +17,13 @@ const TAKE_SNAPSHOTS = false;
const TOOLSHED_API_URL = Deno.env.get("TOOLSHED_API_URL") ??
"http://localhost:8000/";
const FRONTEND_URL = Deno.env.get("FRONTEND_URL") ?? "http://localhost:5173/";
-const HEADLESS = true;
+const HEADLESS = !Deno.env.get("RUN_IN_BROWSER");
const ASTRAL_TIMEOUT = 60_000;
const RECIPE_PATH = "../../recipes/simpleValue.tsx";
-const COMMON_CLI_PATH = path.join(import.meta.dirname!, "../../../scripts/main.ts");
+const COMMON_CLI_PATH = path.join(
+ import.meta.dirname!,
+ "../../../scripts/main.ts",
+);
const SNAPSHOTS_DIR = join(Deno.cwd(), "test_snapshots");
console.log(`TOOLSHED_API_URL=${TOOLSHED_API_URL}`);
diff --git a/packages/jumble/src/iframe-ctx.ts b/packages/jumble/src/iframe-ctx.ts
index 1ea2dff73..8fbd864fe 100644
--- a/packages/jumble/src/iframe-ctx.ts
+++ b/packages/jumble/src/iframe-ctx.ts
@@ -251,7 +251,7 @@ export const setupIframe = (runtime: Runtime) =>
};
// Schedule the action with appropriate reactivity log
- const reads = isCell(context) ? [context.getAsCellLink()] : [];
+ const reads = isCell(context) ? [context.getAsLegacyCellLink()] : [];
const cancel = runtime.scheduler.schedule(action, { reads, writes: [] });
return { action, cancel };
},
diff --git a/packages/runner/README.md b/packages/runner/README.md
index 9d36a7381..27bc37ff0 100644
--- a/packages/runner/README.md
+++ b/packages/runner/README.md
@@ -34,7 +34,7 @@ import { StorageManager } from "@commontools/runner/storage/cache.deno";
const runtime = new Runtime({
storageManager: new StorageManager({
address: "https://example.com/storage",
- signer: myIdentitySigner
+ signer: myIdentitySigner,
}),
consoleHandler: myConsoleHandler, // Optional
errorHandlers: [myErrorHandler], // Optional
@@ -88,7 +88,7 @@ relationship between documents and cells:
- **Cell**: The user-facing abstraction that provides a reactive view over one
or more documents. Cells are defined by schemas and can traverse document
- relationships through cell links and aliases.
+ relationships through sigil-based links.
While DocImpl handles the low-level storage concerns, Cells provide the
higher-level programming model with schema validation, reactivity, and
@@ -105,13 +105,11 @@ storage:
- Schemas can define nested cells with `asCell: true`
- Schema validation happens automatically when setting values
-### CellLink and Aliases
+### Sigil-based Links
-Cells can reference other cells through links and aliases:
+Cells can reference other cells through a unified sigil-based linking system. This approach replaces the previous distinction between CellLinks and Aliases.
-- **CellLink**: A reference to another cell, containing a space ID and document
- ID
-- **Aliases**: Named references within documents that point to other documents
+- **Sigil Links**: A flexible, JSON-based format for representing references to other cells. They can be simple links to other documents or write-redirects (previously aliases).
- These mechanisms allow building complex, interconnected data structures
- The system automatically traverses links when needed
@@ -178,6 +176,7 @@ const runtime = new Runtime({
});
```
+```typescript
// Create a cell with schema and default values
const settingsCell = runtime.getCell(
"my-space", // The space this cell belongs to
@@ -234,7 +233,6 @@ const cleanup = settingsCell.sink((value) => {
// Clean up subscription when done
cleanup();
-
```
### Cells with Type-Safe Schemas
@@ -260,6 +258,7 @@ const runtime = new Runtime({
});
```
+```typescript
// Define a schema with type assertions for TypeScript inference
const userSchema = {
type: "object",
@@ -312,7 +311,6 @@ settingsCell.set({ theme: "dark", notifications: false });
// Key navigation preserves schema
const nameCell = userCell.key("name");
console.log(nameCell.get()); // "Alice"
-
```
### Running Recipes
@@ -338,6 +336,7 @@ const runtime = new Runtime({
});
```
+```typescript
// Define a recipe with input and output schemas
const doubleNumberRecipe = recipe(
// Input schema
@@ -386,7 +385,6 @@ console.log(result.get()); // { result: 20 }
// Stop recipe execution when no longer needed
runtime.runner.stop(result);
-
```
### Storage
@@ -405,7 +403,7 @@ const signer = await Identity.fromPassphrase("my-passphrase");
// Create storage manager (for production, use StorageManager.open() with remote storage)
const storageManager = StorageManager.open({
as: signer,
- address: new URL("https://example.com/api")
+ address: new URL("https://example.com/api"),
});
// Create a runtime instance with configuration
@@ -419,6 +417,7 @@ const runtime = new Runtime({
});
```
+```typescript
// Sync a cell with storage
await runtime.storage.syncCell(userCell);
@@ -430,7 +429,6 @@ await runtime.storage.synced();
// When cells with the same causal ID are synced across instances,
// they will automatically be kept in sync with the latest value
-
```
## Advanced Features
@@ -456,6 +454,7 @@ const runtime = new Runtime({
});
```
+```typescript
// Original data source cell
const sourceCell = runtime.getCell(
"my-space",
@@ -503,14 +502,14 @@ const mappingCell = runtime.getCell(
firstTag: { type: "string" },
},
default: {
- // References to source cell values
- id: { cell: sourceCell, path: ["id"] },
+ // References to source cell values using sigil links
+ id: sourceCell.key("id").getAsLink(),
// Turn single value to array
- changes: [{ cell: sourceCell, path: ["metadata", "createdAt"] }],
+ changes: [sourceCell.key("metadata").key("createdAt").getAsLink()],
// Rename field and uplift from nested element
- kind: { cell: sourceCell, path: ["metadata", "type"] },
+ kind: sourceCell.key("metadata").key("type").getAsLink(),
// Reference to first array element
- firstTag: { cell: sourceCell, path: ["tags", 0] },
+ firstTag: sourceCell.key("tags").key(0).getAsLink(),
},
},
);
@@ -524,7 +523,6 @@ console.log(result);
// kind: "user",
// firstTag: "tag1"
// }
-
```
### Nested Reactivity
@@ -547,6 +545,7 @@ const runtime = new Runtime({
});
```
+```typescript
const rootCell = runtime.getCell(
"my-space",
"nested-example",
@@ -604,7 +603,6 @@ rootCell.key("current").key("label").set("updated");
// "Label value: updated"
// "Nested value: { label: 'updated' }"
// "Root changed: { value: 'root', current: { label: 'updated' } }"
-
```
## Migration from Singleton Pattern
@@ -663,7 +661,8 @@ interface RuntimeOptions {
### Storage Manager
-Storage manager is used by runtime to open storage providers when reading or writing documents into a corresponding space.
+Storage manager is used by runtime to open storage providers when reading or
+writing documents into a corresponding space.
```ts
export interface IStorageManager {
@@ -671,7 +670,8 @@ export interface IStorageManager {
}
```
-The storage manager opens storage providers for different memory spaces. The StorageManager provides convenient factory methods:
+The storage manager opens storage providers for different memory spaces. The
+StorageManager provides convenient factory methods:
```ts
import { StorageManager } from "@commontools/runner/storage/cache ";
@@ -689,7 +689,8 @@ const storageManager = StorageManager.open({
});
```
-The `@commontools/storage/cache` provides a default implementation of the `IStorageManager` interface.
+The `@commontools/storage/cache` provides a default implementation of the
+`IStorageManager` interface.
- `"volatile://"` - In-memory storage (for testing)
- `"https://example.com/storage"` - Remote storage with schema queries
@@ -709,7 +710,7 @@ components interact:
2. **Validation** → Schema validation ensures data conforms to expected
structure (so far only on get, not yet on write)
3. **Processing** → Recipes transform data according to their logic
-4. **Reactivity** → Changes propagate to dependent cells and recipes
+4. **Reactivity** → Changes propagate to dependent cells and recipes through the unified sigil-based linking system
5. **Storage** → Updated data is persisted to storage if configured
6. **Synchronization** → Changes are synchronized across clients if enabled
diff --git a/packages/runner/src/builder/built-in.ts b/packages/runner/src/builder/built-in.ts
index 9802fff55..73cd0961a 100644
--- a/packages/runner/src/builder/built-in.ts
+++ b/packages/runner/src/builder/built-in.ts
@@ -146,17 +146,15 @@ export function str(
* by the order of invocation, which is less stable.
* @param value - Optional, the initial value of the cell.
*/
-declare global {
- function createCell(
- schema?: JSONSchema,
- name?: string,
- value?: T,
- ): Cell;
- function createCell(
- schema: S,
- name?: string,
- value?: Schema,
- ): Cell>;
-}
+declare function createCell(
+ schema?: JSONSchema,
+ name?: string,
+ value?: T,
+): Cell;
+declare function createCell(
+ schema: S,
+ name?: string,
+ value?: Schema,
+): Cell>;
export type { createCell };
diff --git a/packages/runner/src/builder/json-utils.ts b/packages/runner/src/builder/json-utils.ts
index 94d52e8ec..29b07a21f 100644
--- a/packages/runner/src/builder/json-utils.ts
+++ b/packages/runner/src/builder/json-utils.ts
@@ -1,10 +1,8 @@
-import { isObject, isRecord } from "@commontools/utils/types";
-import { type CellLink, isCell, isCellLink, isDoc } from "../index.ts";
-import { createShadowRef } from "./opaque-ref.ts";
+import { isRecord } from "@commontools/utils/types";
+import { type LegacyAlias } from "../sigil-types.ts";
+import { isLegacyAlias, isLink } from "../link-utils.ts";
import {
- type Alias,
canBeOpaqueRef,
- isAlias,
isOpaqueRef,
isRecipe,
isShadowRef,
@@ -20,9 +18,11 @@ import {
unsafe_originalRecipe,
} from "./types.ts";
import { getTopFrame } from "./recipe.ts";
-import { deepEqual, getValueAtPath } from "../path-utils.ts";
+import { deepEqual } from "../path-utils.ts";
+import { IRuntime } from "../runtime.ts";
+import { parseLink } from "../link-utils.ts";
-export function toJSONWithAliases(
+export function toJSONWithLegacyAliases(
value: Opaque,
paths: Map, PropertyKey[]>,
ignoreSelfAliases: boolean = false,
@@ -58,12 +58,12 @@ export function toJSONWithAliases(
...(exported?.schema ? { schema: exported.schema } : {}),
...(exported?.rootSchema ? { rootSchema: exported.rootSchema } : {}),
},
- } satisfies Alias;
+ } satisfies LegacyAlias;
} else throw new Error(`Cell not found in paths`);
}
- if (isAlias(value)) {
- const alias = (value as Alias).$alias;
+ if (isLegacyAlias(value)) {
+ const alias = (value as LegacyAlias).$alias;
if (isShadowRef(alias.cell)) {
const cell = alias.cell.shadowOf;
if (cell.export().frame !== getTopFrame()) {
@@ -83,14 +83,14 @@ export function toJSONWithAliases(
$alias: {
path: [...paths.get(cell)!, ...alias.path] as (string | number)[],
},
- } satisfies Alias;
+ } satisfies LegacyAlias;
} else if (!("cell" in alias) || typeof alias.cell === "number") {
return {
$alias: {
cell: ((alias.cell as number) ?? 0) + 1,
path: alias.path as (string | number)[],
},
- } satisfies Alias;
+ } satisfies LegacyAlias;
} else {
throw new Error(`Invalid alias cell`);
}
@@ -98,7 +98,7 @@ export function toJSONWithAliases(
if (Array.isArray(value)) {
return (value as Opaque).map((v: Opaque, i: number) =>
- toJSONWithAliases(v, paths, ignoreSelfAliases, [...path, i])
+ toJSONWithLegacyAliases(v, paths, ignoreSelfAliases, [...path, i])
);
}
@@ -106,7 +106,7 @@ export function toJSONWithAliases(
const result: any = {};
let hasValue = false;
for (const key in value as any) {
- const jsonValue = toJSONWithAliases(
+ const jsonValue = toJSONWithLegacyAliases(
value[key],
paths,
ignoreSelfAliases,
@@ -129,30 +129,31 @@ export function toJSONWithAliases(
export function createJsonSchema(
example: any,
addDefaults = false,
+ runtime?: IRuntime,
): JSONSchemaMutable {
+ const seen = new Map();
+
function analyzeType(value: any): JSONSchema {
- if (isCell(value)) {
- if (value.schema) {
- return value.schema;
- } else {
- value = value.get();
+ if (isLink(value)) {
+ const link = parseLink(value);
+ const linkAsStr = JSON.stringify(link);
+ if (seen.has(linkAsStr)) {
+ // Return a copy of the schema to avoid mutating the original.
+ return JSON.parse(JSON.stringify(seen.get(linkAsStr)!));
}
- }
- if (isDoc(value)) value = { cell: value, path: [] } satisfies CellLink;
-
- if (isCellLink(value)) {
- value = value.cell.getAtPath(value.path);
- return analyzeType(value);
- }
+ const cell = runtime?.getCellFromLink(link);
+ if (!cell) return {}; // TODO(seefeld): Should be `true`
- if (isAlias(value)) {
- if (isDoc(value.$alias.cell)) {
- value = value.$alias.cell.getAtPath(value.$alias.path);
- } else {
- value = getValueAtPath(example, value.$alias.path);
+ let schema = cell.schema;
+ if (!schema) {
+ // If we find pointing back here, assume an empty schema. This is
+ // overwritten below. (TODO(seefeld): This should create `$ref: "#/.."`)
+ seen.set(linkAsStr, {} as JSONSchemaMutable);
+ schema = analyzeType(cell.getRaw());
}
- return analyzeType(value);
+ seen.set(linkAsStr, schema as JSONSchemaMutable);
+ return schema;
}
const type = typeof value;
@@ -162,33 +163,19 @@ export function createJsonSchema(
case "object":
if (Array.isArray(value)) {
schema.type = "array";
- // Check the array type. The array type is determined by the first element
- // of the array, or if objects, a superset of all properties of the object elements.
- // If array is empty, `items` is `{}`.
if (value.length === 0) {
- schema.items = {};
+ schema.items = {}; // TODO(seefeld): Should be `true`
} else {
- const first = value[0];
- if (isObject(first)) {
- const properties: { [key: string]: any } = {};
- for (let i = 0; i < value.length; i++) {
- const item = value?.[i];
- if (isRecord(item)) {
- Object.keys(item).forEach((key) => {
- if (!(key in properties)) {
- properties[key] = analyzeType(
- value?.[i]?.[key],
- );
- }
- });
- }
- }
- schema.items = {
- type: "object",
- properties,
- };
+ const schemas = value.map((v) => analyzeType(v)).map((s) =>
+ JSON.stringify(s)
+ );
+ const uniqueSchemas = [...new Set(schemas)].map((s) =>
+ JSON.parse(s)
+ );
+ if (uniqueSchemas.length === 1) {
+ schema.items = uniqueSchemas[0];
} else {
- schema.items = analyzeType(first) as JSONSchemaMutable;
+ schema.items = { anyOf: uniqueSchemas };
}
}
} else if (value !== null) {
diff --git a/packages/runner/src/builder/module.ts b/packages/runner/src/builder/module.ts
index d6cef3042..550cfa42c 100644
--- a/packages/runner/src/builder/module.ts
+++ b/packages/runner/src/builder/module.ts
@@ -18,7 +18,7 @@ import {
connectInputAndOutputs,
} from "./node-utils.ts";
import { moduleToJSON } from "./json-utils.ts";
-import { traverseValue } from "../traverse-utils.ts";
+import { traverseValue } from "./traverse-utils.ts";
import { getTopFrame } from "./recipe.ts";
export function createNodeFactory(
diff --git a/packages/runner/src/builder/node-utils.ts b/packages/runner/src/builder/node-utils.ts
index 2d8a3ad2a..35e32d211 100644
--- a/packages/runner/src/builder/node-utils.ts
+++ b/packages/runner/src/builder/node-utils.ts
@@ -10,7 +10,7 @@ import {
type OpaqueRef,
} from "./types.ts";
import { ContextualFlowControl } from "../cfc.ts";
-import { traverseValue } from "../traverse-utils.ts";
+import { traverseValue } from "./traverse-utils.ts";
export function connectInputAndOutputs(node: NodeRef) {
function connect(value: any): any {
diff --git a/packages/runner/src/builder/recipe.ts b/packages/runner/src/builder/recipe.ts
index 6f97b710e..a1ee27833 100644
--- a/packages/runner/src/builder/recipe.ts
+++ b/packages/runner/src/builder/recipe.ts
@@ -30,10 +30,10 @@ import {
createJsonSchema,
moduleToJSON,
recipeToJSON,
- toJSONWithAliases,
+ toJSONWithLegacyAliases,
} from "./json-utils.ts";
import { setValueAtPath } from "../path-utils.ts";
-import { traverseValue } from "../traverse-utils.ts";
+import { traverseValue } from "./traverse-utils.ts";
/** Declare a recipe
*
@@ -257,10 +257,10 @@ function factoryFromRecipe(
});
// Creates a query (i.e. aliases) into the cells for the result
- const result = toJSONWithAliases(outputs ?? {}, paths, true)!;
+ const result = toJSONWithLegacyAliases(outputs ?? {}, paths, true)!;
// Collect default values for the inputs
- const defaults = toJSONWithAliases(
+ const defaults = toJSONWithLegacyAliases(
inputs.export().defaultValue ?? {},
paths,
true,
@@ -307,9 +307,12 @@ function factoryFromRecipe(
applyArgumentIfcToResult(argumentSchema, resultSchemaArg) || {};
const serializedNodes = Array.from(nodes).map((node) => {
- const module = toJSONWithAliases(node.module, paths) as unknown as Module;
- const inputs = toJSONWithAliases(node.inputs, paths)!;
- const outputs = toJSONWithAliases(node.outputs, paths)!;
+ const module = toJSONWithLegacyAliases(
+ node.module,
+ paths,
+ ) as unknown as Module;
+ const inputs = toJSONWithLegacyAliases(node.inputs, paths)!;
+ const outputs = toJSONWithLegacyAliases(node.outputs, paths)!;
return { module, inputs, outputs } satisfies Node;
});
diff --git a/packages/runner/src/traverse-utils.ts b/packages/runner/src/builder/traverse-utils.ts
similarity index 97%
rename from packages/runner/src/traverse-utils.ts
rename to packages/runner/src/builder/traverse-utils.ts
index 048640d35..0688b5a14 100644
--- a/packages/runner/src/traverse-utils.ts
+++ b/packages/runner/src/builder/traverse-utils.ts
@@ -5,7 +5,7 @@ import {
isRecipe,
isShadowRef,
type Opaque,
-} from "./builder/types.ts";
+} from "./types.ts";
/**
* Traverse a value, _not_ entering cells
diff --git a/packages/runner/src/builder/types.ts b/packages/runner/src/builder/types.ts
index 46215ee90..ee3f124c5 100644
--- a/packages/runner/src/builder/types.ts
+++ b/packages/runner/src/builder/types.ts
@@ -135,26 +135,11 @@ export type SchemaContext = {
rootSchema: JSONSchema | boolean;
};
-export type Alias = {
- $alias: {
- cell?: unknown;
- path: PropertyKey[];
- schema?: JSONSchema;
- rootSchema?: JSONSchema;
- };
-};
-
-export function isAlias(value: unknown): value is Alias {
- return isObject(value) && "$alias" in value && isObject(value.$alias) &&
- "path" in value.$alias &&
- Array.isArray(value.$alias.path);
-}
-
-export type StreamAlias = {
+export type StreamValue = {
$stream: true;
};
-export function isStreamAlias(value: unknown): value is StreamAlias {
+export function isStreamValue(value: unknown): value is StreamValue {
return isObject(value) && "$stream" in value && value.$stream === true;
}
@@ -177,7 +162,7 @@ export function isModule(value: unknown): value is Module {
export type Node = {
description?: string;
- module: Module | Alias;
+ module: Module; // TODO(seefeld): Add `Alias` here once supported
inputs: JSONValue;
outputs: JSONValue;
};
diff --git a/packages/runner/src/builtins/if-else.ts b/packages/runner/src/builtins/if-else.ts
index d39b80b2b..adce8b0f1 100644
--- a/packages/runner/src/builtins/if-else.ts
+++ b/packages/runner/src/builtins/if-else.ts
@@ -23,7 +23,7 @@ export function ifElse(
const condition = inputsCell.withLog(log).key(0).get();
const ref = inputsCell.withLog(log).key(condition ? 1 : 2)
- .getAsCellLink();
+ .getAsLink({ base: result.asCell() });
result.send(ref, log);
};
diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts
index 58c86ee7e..bd815fde0 100644
--- a/packages/runner/src/cell.ts
+++ b/packages/runner/src/cell.ts
@@ -1,10 +1,11 @@
import { isObject, isRecord } from "@commontools/utils/types";
+import { type MemorySpace } from "@commontools/memory/interface";
import { getTopFrame } from "./builder/recipe.ts";
import {
type Cell,
ID,
ID_FIELD,
- isStreamAlias,
+ isStreamValue,
type JSONSchema,
type Schema,
} from "./builder/types.ts";
@@ -15,11 +16,24 @@ import {
type QueryResult,
} from "./query-result-proxy.ts";
import { diffAndUpdate } from "./data-updating.ts";
-import { resolveLinkToAlias, resolveLinkToValue } from "./link-resolution.ts";
+import {
+ resolveLinkToValue,
+ resolveLinkToWriteRedirect,
+} from "./link-resolution.ts";
import type { ReactivityLog } from "./scheduler.ts";
import { type EntityId } from "./doc-map.ts";
import { type Cancel, isCancel, useCancelGroup } from "./cancel.ts";
import { validateAndTransform } from "./schema.ts";
+import { toURI } from "./uri-utils.ts";
+import {
+ type JSONCellLink,
+ type LegacyCellLink,
+ LINK_V1_TAG,
+ type SigilLink,
+ type SigilWriteRedirectLink,
+ type URI,
+} from "./sigil-types.ts";
+import { areLinksSame, isLink } from "./link-utils.ts";
/**
* This is the regular Cell interface, generated by DocImpl.asCell().
@@ -42,8 +56,8 @@ import { validateAndTransform } from "./schema.ts";
* @returns {void}
*
* @method push Adds an item to the end of an array cell.
- * @param {U | DocImpl | CellLink} value - The value to add, where U is the
- * array element type.
+ * @param {U | DocImpl | LegacyCellLink} value - The value to add, where U is
+ * the array element type.
* @returns {void}
*
* @method equals Compares two cells for equality.
@@ -73,16 +87,19 @@ import { validateAndTransform } from "./schema.ts";
* @param {ReactivityLog} log - Optional reactivity log.
* @returns {QueryResult>}
*
- * @method getAsCellLink Returns a cell link for the cell.
- * @returns {CellLink}
+ * @method getAsLegacyCellLink Returns a cell link for the cell (legacy format).
+ * @returns {LegacyCellLink}
*
- * @method getRaw Raw access method, without following aliases (which would
- * write to the destination instead of the cell itself).
+ * @method getAsLink Returns a cell link for the cell (new sigil format).
+ * @returns {SigilLink}
+ *
+ * @method getRaw Raw access method, without following aliases (which would
+ * write to the destination instead of the cell itself).
* @returns {any} - Raw document data
*
- * @method setRaw Raw write method that bypasses Cell validation,
- * transformation, and alias resolution. Writes directly to the cell without
- * following aliases.
+ * @method setRaw Raw write method that bypasses Cell validation,
+ * transformation, and alias resolution. Writes directly to the cell without
+ * following aliases.
* @param {any} value - Raw value to write directly to document
* @returns {boolean} - Result from underlying doc.send()
*
@@ -97,7 +114,10 @@ import { validateAndTransform } from "./schema.ts";
* @returns {{cell: {"/": string} | undefined, path: PropertyKey[]}}
*
* @property entityId Returns the current entity ID of the cell.
- * @returns {EntityId | undefined}
+ * @returns {EntityId}
+ *
+ * @property sourceURI Returns the source URI of the cell.
+ * @returns {URI}
*
* @property schema Optional schema for the cell.
* @returns {JSONSchema | undefined}
@@ -114,7 +134,7 @@ import { validateAndTransform } from "./schema.ts";
* @returns {T}
*
* @property cellLink The cell link representing this cell.
- * @returns {CellLink}
+ * @returns {LegacyCellLink}
*/
declare module "@commontools/api" {
interface Cell {
@@ -127,10 +147,10 @@ declare module "@commontools/api" {
push(
...value: Array<
| (T extends Array ? (Cellify | U | DocImpl) : any)
- | CellLink
+ | Cell
>
): void;
- equals(other: Cell): boolean;
+ equals(other: any): boolean;
key ? keyof S : keyof T>(
valueKey: K,
): Cell<
@@ -149,7 +169,21 @@ declare module "@commontools/api" {
path?: Path,
log?: ReactivityLog,
): QueryResult>;
- getAsCellLink(): CellLink;
+ getAsLegacyCellLink(): LegacyCellLink;
+ getAsLink(
+ options?: {
+ base?: Cell;
+ baseSpace?: MemorySpace;
+ includeSchema?: boolean;
+ },
+ ): SigilLink;
+ getAsWriteRedirectLink(
+ options?: {
+ base?: Cell;
+ baseSpace?: MemorySpace;
+ includeSchema?: boolean;
+ },
+ ): SigilWriteRedirectLink;
getDoc(): DocImpl;
getRaw(): any;
setRaw(value: any): boolean;
@@ -172,12 +206,15 @@ declare module "@commontools/api" {
& ("argument" extends keyof Schema ? unknown
: { argument: any })
>;
- toJSON(): { cell: { "/": string } | undefined; path: PropertyKey[] };
+ toJSON(): JSONCellLink;
schema?: JSONSchema;
rootSchema?: JSONSchema;
value: T;
- cellLink: CellLink;
- entityId: EntityId | undefined;
+ cellLink: LegacyCellLink;
+ space: MemorySpace;
+ entityId: EntityId;
+ sourceURI: URI;
+ path: PropertyKey[];
[isCellMarker]: true;
copyTrap: boolean;
}
@@ -185,6 +222,8 @@ declare module "@commontools/api" {
export type { Cell } from "@commontools/api";
+export type { MemorySpace } from "@commontools/memory/interface";
+
/**
* Cellify is a type utility that allows any part of type T to be wrapped in
* Cell<>, and allow any part of T that is currently wrapped in Cell<> to be
@@ -214,37 +253,27 @@ export interface Stream {
[isStreamMarker]: true;
}
-/**
- * Cell link.
- *
- * A cell link is a doc and a path within that doc.
- */
-export type CellLink = {
- space?: string;
- cell: DocImpl;
- path: PropertyKey[];
- schema?: JSONSchema;
- rootSchema?: JSONSchema;
-};
-
export function createCell(
doc: DocImpl,
path: PropertyKey[] = [],
log?: ReactivityLog,
schema?: JSONSchema,
rootSchema: JSONSchema | undefined = schema,
+ noResolve = false,
): Cell {
// Resolve the path to check whether it's a stream. We're not logging this right now.
// The corner case where during it's lifetime this changes from non-stream to stream
// or vice versa will not be detected.
- const ref = resolveLinkToValue(doc, path, undefined, schema, rootSchema);
+ const ref = noResolve
+ ? { cell: doc, path, schema, rootSchema }
+ : resolveLinkToValue(doc, path, undefined, schema, rootSchema);
// Use schema from alias if provided and no explicit schema was set
if (!schema && ref.schema) {
schema = ref.schema;
rootSchema = ref.rootSchema || ref.schema;
}
- if (isStreamAlias(ref.cell.getAtPath(ref.path))) {
+ if (isStreamValue(ref.cell.getAtPath(ref.path))) {
return createStreamCell(
ref.cell,
ref.path,
@@ -309,7 +338,7 @@ function createRegularCell(
set: (newValue: Cellify) =>
// TODO(@ubik2) investigate whether i need to check classified as i walk down my own obj
diffAndUpdate(
- resolveLinkToAlias(doc, path, log, schema, rootSchema),
+ resolveLinkToWriteRedirect(doc, path, log, schema, rootSchema),
newValue,
log,
getTopFrame()?.cause,
@@ -327,7 +356,8 @@ function createRegularCell(
if (currentValue === undefined) {
if (schema) {
// Check if schema allows objects
- const allowsObject = schema.type === "object" || (Array.isArray(schema.type) && schema.type.includes("object")) ||
+ const allowsObject = schema.type === "object" ||
+ (Array.isArray(schema.type) && schema.type.includes("object")) ||
(schema.anyOf &&
schema.anyOf.some((s) =>
typeof s === "object" && s.type === "object"
@@ -351,7 +381,7 @@ function createRegularCell(
push: (
...values: Array<
| (T extends Array ? (Cellify | U | DocImpl) : any)
- | CellLink
+ | Cell
>
) => {
// Follow aliases and references, since we want to get to an assumed
@@ -365,20 +395,12 @@ function createRegularCell(
}
// If this is an object and it doesn't have an ID, add one.
- const valuesToWrite = values.map((value: any) => {
- if (
- !isCell(value) && !isCellLink(value) && !isDoc(value) &&
- isObject(value) &&
- (value as { [ID]?: unknown })[ID] === undefined && getTopFrame()
- ) {
- return {
- [ID]: getTopFrame()!.generatedIdCounter++,
- ...value,
- };
- } else {
- return value;
- }
- });
+ const valuesToWrite = values.map((value: any) =>
+ (!isLink(value) && isObject(value) &&
+ (value as { [ID]?: unknown })[ID] === undefined && getTopFrame())
+ ? { [ID]: getTopFrame()!.generatedIdCounter++, ...value }
+ : value
+ );
// If there is no array yet, create it first. We have to do this as a
// separate operation, so that in the next steps [ID] is properly anchored
@@ -412,8 +434,7 @@ function createRegularCell(
return [...(newBaseValue as unknown[]), ...newValues];
});
},
- equals: (other: Cell) =>
- JSON.stringify(self) === JSON.stringify(other),
+ equals: (other: any) => areLinksSame(self, other),
key: ? keyof S : keyof T>(
valueKey: K,
): T extends Cell ? Cell : Cell => {
@@ -439,27 +460,63 @@ function createRegularCell(
subscribeToReferencedDocs(callback, doc, path, schema, rootSchema),
getAsQueryResult: (subPath: PropertyKey[] = [], newLog?: ReactivityLog) =>
createQueryResultProxy(doc, [...path, ...subPath], newLog ?? log),
- getAsCellLink: () => // Add space here, so that JSON.stringify() of this retains the space.
- ({ space: doc.space, cell: doc, path, schema, rootSchema }),
+ getAsLegacyCellLink: (): LegacyCellLink => {
+ return { space: doc.space, cell: doc, path, schema, rootSchema };
+ },
+ getAsLink: (
+ options?: {
+ base?: Cell;
+ baseSpace?: MemorySpace;
+ includeSchema?: boolean;
+ },
+ ): SigilLink => {
+ return createSigilLink(
+ doc,
+ path,
+ schema,
+ rootSchema,
+ options,
+ ) as SigilLink;
+ },
+ getAsWriteRedirectLink: (
+ options?: {
+ base?: Cell;
+ baseSpace?: MemorySpace;
+ includeSchema?: boolean;
+ },
+ ): SigilWriteRedirectLink => {
+ return createSigilLink(
+ doc,
+ path,
+ schema,
+ rootSchema,
+ { ...options, overwrite: "redirect" },
+ ) as SigilWriteRedirectLink;
+ },
getDoc: () => doc,
getRaw: () => doc.getAtPath(path),
setRaw: (value: any) => doc.setAtPath(path, value),
getSourceCell: (newSchema?: JSONSchema) =>
doc.sourceCell?.asCell([], log, newSchema, newSchema) as Cell,
- toJSON: () =>
- // TODO(seefeld): Should this include the schema, as cells are defiined by doclink & schema?
- ({ cell: doc.toJSON(), path }) satisfies {
- cell: { "/": string } | undefined;
- path: PropertyKey[];
- },
+ toJSON: (): JSONCellLink => // Keep old format for backward compatibility
+ ({ cell: doc.toJSON()!, path: path as (string | number)[] }),
get value(): T {
return self.get();
},
- get cellLink(): CellLink {
+ get cellLink(): LegacyCellLink {
return { space: doc.space, cell: doc, path, schema, rootSchema };
},
+ get space(): MemorySpace {
+ return doc.space;
+ },
get entityId(): EntityId | undefined {
- return getEntityId(self.getAsCellLink());
+ return getEntityId({ cell: doc, path });
+ },
+ get sourceURI(): URI {
+ return toURI(doc.entityId);
+ },
+ get path(): PropertyKey[] {
+ return path;
},
[isCellMarker]: true,
get copyTrap(): boolean {
@@ -529,6 +586,64 @@ function subscribeToReferencedDocs(
};
}
+/**
+ * Creates a sigil reference (link or alias) with shared logic
+ */
+function createSigilLink(
+ doc: DocImpl,
+ path: PropertyKey[],
+ schema?: JSONSchema,
+ rootSchema?: JSONSchema,
+ options: {
+ base?: Cell;
+ baseSpace?: MemorySpace;
+ includeSchema?: boolean;
+ overwrite?: "redirect" | "this"; // default is "this"
+ } = {},
+): SigilLink {
+ // Create the base structure
+ const sigil: SigilLink = {
+ "/": {
+ [LINK_V1_TAG]: {
+ path: path.map((p) => p.toString()),
+ },
+ },
+ };
+
+ const reference = sigil["/"][LINK_V1_TAG];
+
+ // Handle base cell for relative references
+ if (options.base) {
+ const baseDoc = options.base.getDoc();
+
+ // Only include id if it's different from base
+ if (getEntityId(doc)!["/"] !== getEntityId(baseDoc)?.["/"]) {
+ reference.id = toURI(doc.entityId);
+ }
+
+ // Only include space if it's different from base
+ if (doc.space && doc.space !== baseDoc.space) reference.space = doc.space;
+ } else {
+ reference.id = toURI(doc.entityId);
+
+ // Handle baseSpace option - only include space if different from baseSpace
+ if (doc.space !== options.baseSpace) reference.space = doc.space;
+ }
+
+ // Include schema if requested
+ if (options.includeSchema && schema) {
+ reference.schema = schema;
+ reference.rootSchema = rootSchema;
+ }
+
+ // Include overwrite if present and it's a redirect
+ if (options.overwrite && options.overwrite !== "this") {
+ reference.overwrite = "redirect";
+ }
+
+ return sigil;
+}
+
/**
* Check if value is a simple cell.
*
@@ -551,15 +666,3 @@ export function isStream(value: any): value is Stream {
}
const isStreamMarker = Symbol("isStream");
-
-/**
- * Check if value is a cell link.
- *
- * @param {any} value - The value to check.
- * @returns {boolean}
- */
-export function isCellLink(value: any): value is CellLink {
- return (
- isRecord(value) && isDoc(value.cell) && Array.isArray(value.path)
- );
-}
diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts
index 243b1502a..e188755e2 100644
--- a/packages/runner/src/data-updating.ts
+++ b/packages/runner/src/data-updating.ts
@@ -1,19 +1,23 @@
import { isRecord } from "@commontools/utils/types";
-import {
- type Alias,
- ID,
- ID_FIELD,
- isAlias,
- type JSONSchema,
- type JSONValue,
-} from "./builder/types.ts";
+import { ID, ID_FIELD, type JSONSchema } from "./builder/types.ts";
import { ContextualFlowControl } from "./cfc.ts";
import { type DocImpl, isDoc } from "./doc.ts";
import { createRef } from "./doc-map.ts";
-import { type CellLink, isCell, isCellLink } from "./cell.ts";
+import { isCell } from "./cell.ts";
+import { isAnyCellLink } from "./link-utils.ts";
+import { type LegacyCellLink } from "./sigil-types.ts";
import { type ReactivityLog } from "./scheduler.ts";
-import { followAliases } from "./link-resolution.ts";
-import { maybeUnwrapProxy, arrayEqual } from "./type-utils.ts";
+import { followWriteRedirects } from "./link-resolution.ts";
+import {
+ areLinksSame,
+ isLink,
+ isWriteRedirectLink,
+ parseToLegacyCellLink,
+} from "./link-utils.ts";
+import {
+ getCellLinkOrThrow,
+ isQueryResultForDereferencing,
+} from "./query-result-proxy.ts";
// Sets a value at a path, following aliases and recursing into objects. Returns
// success, meaning no frozen docs were in the way. That is, also returns true
@@ -25,8 +29,8 @@ export function setNestedValue(
log?: ReactivityLog,
): boolean {
const destValue = doc.getAtPath(path);
- if (isAlias(destValue)) {
- const ref = followAliases(destValue, doc, log);
+ if (isWriteRedirectLink(destValue)) {
+ const ref = followWriteRedirects(destValue, doc, log);
return setNestedValue(ref.cell, ref.path, value, log);
}
@@ -37,7 +41,7 @@ export function setNestedValue(
isRecord(value) &&
Array.isArray(value) === Array.isArray(destValue) &&
!isDoc(value) &&
- !isCellLink(value) &&
+ !isAnyCellLink(value) &&
!isCell(value)
) {
let success = true;
@@ -62,10 +66,8 @@ export function setNestedValue(
}
return success;
- } else if (isCellLink(value) && isCellLink(destValue)) {
- if (
- value.cell !== destValue.cell || !arrayEqual(value.path, destValue.path)
- ) {
+ } else if (isLink(value) && isLink(destValue)) {
+ if (!areLinksSame(value, destValue, doc.asCell())) {
doc.setAtPath(path, value, log);
}
return true;
@@ -95,7 +97,7 @@ export function setNestedValue(
* @returns Whether any changes were made.
*/
export function diffAndUpdate(
- current: CellLink,
+ current: LegacyCellLink,
newValue: unknown,
log?: ReactivityLog,
context?: unknown,
@@ -105,7 +107,7 @@ export function diffAndUpdate(
return changes.length > 0;
}
-type ChangeSet = { location: CellLink; value: unknown }[];
+type ChangeSet = { location: LegacyCellLink; value: unknown }[];
/**
* Traverses objects and returns an array of changes that should be written. An
@@ -129,7 +131,7 @@ type ChangeSet = { location: CellLink; value: unknown }[];
* @returns An array of changes that should be written.
*/
export function normalizeAndDiff(
- current: CellLink,
+ current: LegacyCellLink,
newValue: unknown,
log?: ReactivityLog,
context?: unknown,
@@ -153,19 +155,22 @@ export function normalizeAndDiff(
if (current.path.length > 1) {
const parent = current.cell.getAtPath(current.path.slice(0, -1));
if (Array.isArray(parent)) {
+ const base = current.cell.asCell(current.path);
for (const v of parent) {
- if (isCellLink(v)) {
- const sibling = v.cell.getAtPath(v.path);
+ if (isLink(v)) {
+ const sibling = parseToLegacyCellLink(v, base);
if (
- isRecord(sibling) &&
- sibling[fieldName as PropertyKey] === id
+ sibling.cell.getAtPath([
+ ...sibling.path,
+ fieldName as PropertyKey,
+ ]) === id
) {
// We found a sibling with the same id, so ...
return [
// ... reuse the existing document
...normalizeAndDiff(current, v, log, context),
// ... and update it to the new value
- ...normalizeAndDiff(v, rest, log, context),
+ ...normalizeAndDiff(sibling, rest, log, context),
];
}
}
@@ -177,19 +182,23 @@ export function normalizeAndDiff(
}
// Unwrap proxies and handle special types
- newValue = maybeUnwrapProxy(newValue);
- if (isDoc(newValue)) newValue = { cell: newValue, path: [] };
- if (isCell(newValue)) newValue = newValue.getAsCellLink();
+ if (isQueryResultForDereferencing(newValue)) {
+ newValue = getCellLinkOrThrow(newValue);
+ }
+
+ if (isDoc(newValue)) {
+ newValue = { cell: newValue, path: [] } satisfies LegacyCellLink;
+ }
+ if (isCell(newValue)) newValue = newValue.getAsLegacyCellLink();
// Get current value to compare against
const currentValue = current.cell.getAtPath(current.path);
// A new alias can overwrite a previous alias. No-op if the same.
- if (isAlias(newValue)) {
+ if (isWriteRedirectLink(newValue)) {
if (
- isAlias(currentValue) &&
- newValue.$alias.cell === currentValue.$alias.cell &&
- arrayEqual(newValue.$alias.path, currentValue.$alias.path)
+ isWriteRedirectLink(currentValue) &&
+ areLinksSame(currentValue, newValue, current.cell.asCell())
) {
return [];
} else {
@@ -199,18 +208,17 @@ export function normalizeAndDiff(
}
// Handle alias in current value (at this point: if newValue is not an alias)
- if (isAlias(currentValue)) {
+ if (isWriteRedirectLink(currentValue)) {
// Log reads of the alias, so that changing aliases cause refreshes
log?.reads.push({ ...current });
- const ref = followAliases(currentValue, current.cell, log);
+ const ref = followWriteRedirects(currentValue, current.cell, log);
return normalizeAndDiff(ref, newValue, log, context);
}
- if (isCellLink(newValue)) {
+ if (isAnyCellLink(newValue)) {
if (
- isCellLink(currentValue) &&
- currentValue.cell === newValue.cell &&
- arrayEqual(currentValue.path, newValue.path)
+ isAnyCellLink(currentValue) &&
+ areLinksSame(newValue, currentValue, current.cell.asCell())
) {
return [];
} else {
@@ -221,10 +229,7 @@ export function normalizeAndDiff(
}
// Handle ID-based object (convert to entity)
- if (
- isRecord(newValue) &&
- newValue[ID] !== undefined
- ) {
+ if (isRecord(newValue) && newValue[ID] !== undefined) {
const { [ID]: id, ...rest } = newValue;
let path = current.path;
@@ -319,10 +324,7 @@ export function normalizeAndDiff(
if (isRecord(newValue)) {
// If the current value is not a (regular) object, set it to an empty object
// Note that the alias case is handled above
- if (
- typeof currentValue !== "object" || currentValue === null ||
- isCellLink(currentValue)
- ) {
+ if (!isRecord(currentValue) || isAnyCellLink(currentValue)) {
changes.push({ location: current, value: {} });
}
@@ -369,7 +371,7 @@ export function normalizeAndDiff(
return changes;
}
- // Handle primitive values and other cases
+ // Handle primitive values and other cases (Object.is handles NaN and -0)
if (!Object.is(currentValue, newValue)) {
changes.push({ location: current, value: newValue });
}
@@ -420,11 +422,11 @@ export function addCommonIDfromObjectID(
if (
isRecord(obj) && !isCell(obj) &&
- !isCellLink(obj) && !isDoc(obj)
+ !isAnyCellLink(obj) && !isDoc(obj)
) {
Object.values(obj).forEach((v) => traverse(v));
}
}
traverse(obj);
-}
\ No newline at end of file
+}
diff --git a/packages/runner/src/doc-map.ts b/packages/runner/src/doc-map.ts
index b19e2db6f..f074d0583 100644
--- a/packages/runner/src/doc-map.ts
+++ b/packages/runner/src/doc-map.ts
@@ -6,8 +6,10 @@ import {
getCellLinkOrThrow,
isQueryResultForDereferencing,
} from "./query-result-proxy.ts";
-import { type CellLink, isCell, isCellLink } from "./cell.ts";
+import { isCell } from "./cell.ts";
+import { parseLink } from "./link-utils.ts";
import type { IDocumentMap, IRuntime, MemorySpace } from "./runtime.ts";
+import { fromURI } from "./uri-utils.ts";
export type EntityId = {
"/": string | Uint8Array;
@@ -78,26 +80,25 @@ export function createRef(
*/
export function getEntityId(value: any): { "/": string } | undefined {
if (typeof value === "string") {
+ // Handle URI format with "of:" prefix
+ if (value.startsWith("of:")) value = fromURI(value);
return value.startsWith("{") ? JSON.parse(value) : { "/": value };
}
if (isRecord(value) && "/" in value) {
return JSON.parse(JSON.stringify(value));
}
- let ref: CellLink | undefined = undefined;
+ const link = parseLink(value);
- if (isQueryResultForDereferencing(value)) ref = getCellLinkOrThrow(value);
- else if (isCellLink(value)) ref = value;
- else if (isCell(value)) ref = value.getAsCellLink();
- else if (isDoc(value)) ref = { cell: value, path: [] };
+ if (!link || !link.id) return undefined;
- if (!ref?.cell.entityId) return undefined;
+ const entityId = { "/": fromURI(link.id) };
- if (ref.path.length > 0) {
+ if (link.path && link.path.length > 0) {
return JSON.parse(
- JSON.stringify(createRef({ path: ref.path }, ref.cell.entityId)),
+ JSON.stringify(createRef({ path: link.path }, entityId)),
);
- } else return JSON.parse(JSON.stringify(ref.cell.entityId));
+ } else return entityId;
}
/**
@@ -173,23 +174,31 @@ export class DocumentMap implements IDocumentMap {
constructor(readonly runtime: IRuntime) {}
+ getDocByEntityId(
+ space: MemorySpace,
+ entityId: EntityId | string,
+ createIfNotFound?: true,
+ sourceIfCreated?: DocImpl,
+ ): DocImpl;
+ getDocByEntityId(
+ space: MemorySpace,
+ entityId: EntityId | string,
+ createIfNotFound: false,
+ sourceIfCreated?: DocImpl,
+ ): DocImpl | undefined;
getDocByEntityId(
space: MemorySpace,
entityId: EntityId | string,
createIfNotFound = true,
sourceIfCreated?: DocImpl,
): DocImpl | undefined {
- const id = typeof entityId === "string"
- ? entityId
- : JSON.stringify(entityId);
- let doc = this.entityIdToDocMap.get(space, id);
+ const normalizedId = normalizeEntityId(entityId);
+
+ let doc = this.entityIdToDocMap.get(space, JSON.stringify(normalizedId));
if (doc) return doc;
if (!createIfNotFound) return undefined;
- if (typeof entityId === "string") {
- entityId = JSON.parse(entityId) as EntityId;
- }
- doc = createDoc(undefined as T, entityId, space, this.runtime);
+ doc = this.createDoc(undefined as T, normalizedId, space);
doc.sourceCell = sourceIfCreated;
return doc;
}
@@ -244,7 +253,19 @@ export class DocumentMap implements IDocumentMap {
space: MemorySpace,
): DocImpl {
// Use the full createDoc implementation with runtime parameter
- const doc = createDoc(value, entityId, space, this.runtime);
- return doc;
+ return createDoc(value, entityId, space, this.runtime);
+ }
+}
+
+function normalizeEntityId(entityId: EntityId | string): EntityId {
+ if (typeof entityId === "string") {
+ if (entityId.startsWith("of:")) {
+ return { "/": fromURI(entityId) };
+ }
+ return JSON.parse(entityId) as EntityId;
+ } else if (isRecord(entityId) && "/" in entityId) {
+ return entityId;
+ } else {
+ throw new Error("Invalid entity ID: " + JSON.stringify(entityId));
}
}
diff --git a/packages/runner/src/doc.ts b/packages/runner/src/doc.ts
index 49a8e67a7..6c16e9ea2 100644
--- a/packages/runner/src/doc.ts
+++ b/packages/runner/src/doc.ts
@@ -19,9 +19,7 @@ import type { IRuntime } from "./runtime.ts";
import { type ReactivityLog } from "./scheduler.ts";
import { type Cancel } from "./cancel.ts";
import { Labels, MemorySpace } from "./storage.ts";
-import { arrayEqual } from "./type-utils.ts";
-
-// Remove the arrayEqual function since we import it now
+import { arrayEqual } from "./path-utils.ts";
/**
* Lowest level cell implementation.
@@ -296,7 +294,9 @@ export function createDoc(
let changed = false;
if (path.length > 0) {
- if (value === undefined) value = (typeof path[0] === "number" ? [] : {}) as T;
+ if (value === undefined) {
+ value = (typeof path[0] === "number" ? [] : {}) as T;
+ }
changed = setValueAtPath(value, path, newValue);
} else if (!deepEqual(value, newValue)) {
changed = true;
@@ -351,7 +351,7 @@ export function createDoc(
return sourceCell;
},
set sourceCell(cell: DocImpl | undefined) {
- if (sourceCell && sourceCell !== cell) {
+ if (sourceCell && JSON.stringify(sourceCell) !== JSON.stringify(cell)) {
throw new Error(
`Source cell already set: ${JSON.stringify(sourceCell)} -> ${
JSON.stringify(cell)
diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts
index 6bd05a755..71e4330de 100644
--- a/packages/runner/src/index.ts
+++ b/packages/runner/src/index.ts
@@ -8,14 +8,15 @@ export type {
} from "./runtime.ts";
export { raw } from "./module.ts";
export type { DocImpl } from "./doc.ts";
-export type { Cell, CellLink, Stream } from "./cell.ts";
+export type { Cell, Stream } from "./cell.ts";
+export type { LegacyCellLink, URI } from "./sigil-types.ts";
export type { EntityId } from "./doc-map.ts";
export { createRef, getEntityId } from "./doc-map.ts";
export type { QueryResult } from "./query-result-proxy.ts";
export type { Action, ErrorWithContext, ReactivityLog } from "./scheduler.ts";
export * as StorageInspector from "./storage/inspector.ts";
export { isDoc } from "./doc.ts";
-export { isCell, isCellLink, isStream } from "./cell.ts";
+export { isCell, isStream } from "./cell.ts";
export {
getCellLinkOrThrow,
getCellLinkOrValue,
@@ -34,7 +35,16 @@ export {
EngineProgramResolver,
} from "./harness/index.ts";
export { addCommonIDfromObjectID } from "./data-updating.ts";
-export { followAliases, maybeGetCellLink } from "./link-resolution.ts";
+export { followWriteRedirects } from "./link-resolution.ts";
+export {
+ areLinksSame,
+ isCellLink,
+ isLink,
+ isWriteRedirectLink,
+ parseLink,
+ parseLinkOrThrow,
+ parseToLegacyCellLink,
+} from "./link-utils.ts";
export * from "./recipe-manager.ts";
// Builder functionality (migrated from @commontools/builder package)
@@ -60,7 +70,6 @@ export {
recipeFromFrame,
} from "./builder/recipe.ts";
export {
- type Alias,
AuthSchema,
type Cell as BuilderCell,
type Child,
@@ -69,11 +78,10 @@ export {
type HandlerFactory,
ID,
ID_FIELD,
- isAlias,
isModule,
isOpaqueRef,
isRecipe,
- isStreamAlias,
+ isStreamValue,
type JSONObject,
type JSONSchema,
type JSONSchemaMutable,
@@ -92,8 +100,7 @@ export {
schema,
type SchemaContext,
type SchemaWithoutCell,
- type Stream as BuilderStream,
- type StreamAlias,
+ type StreamValue,
type toJSON,
toOpaqueRef,
TYPE,
diff --git a/packages/runner/src/link-resolution.ts b/packages/runner/src/link-resolution.ts
index 66ca701bb..1454188d4 100644
--- a/packages/runner/src/link-resolution.ts
+++ b/packages/runner/src/link-resolution.ts
@@ -1,17 +1,19 @@
-import {
- type Alias,
- isAlias,
- type JSONSchema,
-} from "./builder/types.ts";
+import { type JSONSchema } from "./builder/types.ts";
import { ContextualFlowControl } from "./cfc.ts";
import { type DocImpl, isDoc } from "./doc.ts";
-import { type Cell, type CellLink, isCell, isCellLink } from "./cell.ts";
+import { type Cell, createCell } from "./cell.ts";
+import {
+ type LegacyAlias,
+ type LegacyCellLink,
+ type SigilWriteRedirectLink,
+} from "./sigil-types.ts";
import { type ReactivityLog } from "./scheduler.ts";
+import { arrayEqual } from "./path-utils.ts";
import {
- getCellLinkOrThrow,
- isQueryResultForDereferencing,
-} from "./query-result-proxy.ts";
-import { arrayEqual } from "./type-utils.ts";
+ isWriteRedirectLink,
+ parseLink,
+ parseToLegacyCellLink,
+} from "./link-utils.ts";
/**
* Track visited cell links and memoize results during path resolution
@@ -19,11 +21,11 @@ import { arrayEqual } from "./type-utils.ts";
*/
interface Visits {
/** Tracks visited cell links to detect cycles */
- seen: CellLink[];
+ seen: LegacyCellLink[];
/** Cache for resolvePath results */
- resolvePathCache: Map;
+ resolvePathCache: Map;
/** Cache for followLinks results */
- followLinksCache: Map;
+ followLinksCache: Map;
}
/**
@@ -54,25 +56,28 @@ export function resolveLinkToValue(
log?: ReactivityLog,
schema?: JSONSchema,
rootSchema?: JSONSchema,
-): CellLink {
+): LegacyCellLink {
const visits = createVisits();
const ref = resolvePath(doc, path, log, schema, rootSchema, visits);
return followLinks(ref, log, visits);
}
-export function resolveLinkToAlias(
+export function resolveLinkToWriteRedirect(
doc: DocImpl,
path: PropertyKey[],
log?: ReactivityLog,
schema?: JSONSchema,
rootSchema?: JSONSchema,
-): CellLink {
+): LegacyCellLink {
const visits = createVisits();
const ref = resolvePath(doc, path, log, schema, rootSchema, visits);
return followLinks(ref, log, visits, true);
}
-export function resolveLinks(ref: CellLink, log?: ReactivityLog): CellLink {
+export function resolveLinks(
+ ref: LegacyCellLink,
+ log?: ReactivityLog,
+): LegacyCellLink {
const visits = createVisits();
return followLinks(ref, log, visits);
}
@@ -84,7 +89,7 @@ function resolvePath(
schema?: JSONSchema,
rootSchema?: JSONSchema,
visits: Visits = createVisits(),
-): CellLink { // Follow aliases, doc links, etc. in path, so that we end up on the right
+): LegacyCellLink { // Follow aliases, doc links, etc. in path, so that we end up on the right
// doc, meaning the one that contains the value we want to access without any
// redirects in between.
//
@@ -108,7 +113,7 @@ function resolvePath(
}
// Try to find a cached result for a shorter path
- let startRef: CellLink = { cell: doc, path: [] };
+ let startRef: LegacyCellLink = { cell: doc, path: [] };
let keys = [...path];
// Look for the longest matching prefix path in the cache
@@ -166,19 +171,19 @@ function resolvePath(
// log all taken links, so not the returned one, and thus nothing if the ref
// already pointed to a value.
export function followLinks(
- ref: CellLink,
+ ref: LegacyCellLink,
log: ReactivityLog | undefined,
visits: Visits,
- onlyAliases = false,
-): CellLink {
+ onlyWriteRedirects = false,
+): LegacyCellLink {
// Check if we already followed these links
- const cacheKey = createPathCacheKey(ref.cell, ref.path, onlyAliases);
+ const cacheKey = createPathCacheKey(ref.cell, ref.path, onlyWriteRedirects);
const cached = visits.followLinksCache.get(cacheKey);
if (cached) {
return cached;
}
- let nextRef: CellLink | undefined;
+ let nextRef: LegacyCellLink | undefined;
let result = ref;
do {
@@ -201,8 +206,18 @@ export function followLinks(
const target = result.cell.getAtPath(result.path);
- nextRef = !onlyAliases || isAlias(target)
- ? maybeGetCellLink(target, result.cell)
+ nextRef = !onlyWriteRedirects || isWriteRedirectLink(target)
+ ? parseToLegacyCellLink(
+ target,
+ createCell(
+ result.cell,
+ [], // Use empty path to reference the document itself
+ undefined,
+ undefined,
+ undefined,
+ true,
+ ),
+ )
: undefined;
if (nextRef !== undefined) {
@@ -241,40 +256,36 @@ export function followLinks(
return result;
}
-export function maybeGetCellLink(
- value: unknown,
- parent?: DocImpl,
-): CellLink | undefined {
- if (isQueryResultForDereferencing(value)) return getCellLinkOrThrow(value);
- else if (isCellLink(value)) return value;
- else if (isDoc(value)) return { cell: value, path: [] } satisfies CellLink;
- else if (isCell(value)) return value.getAsCellLink();
- else if (isAlias(value)) {
- if (!parent && !value.$alias.cell) {
- throw new Error(
- `Alias without cell and no parent provided: ${JSON.stringify(value)}`,
- );
- }
- return { cell: parent, ...value.$alias } as CellLink;
- } else return undefined;
-}
-
// Follows aliases and returns cell reference describing the last alias.
// Only logs interim aliases, not the first one, and not the non-alias value.
-export function followAliases(
- alias: Alias,
- docOrCell: DocImpl | Cell,
+export function followWriteRedirects(
+ writeRedirect: LegacyAlias | SigilWriteRedirectLink,
+ base: DocImpl | Cell,
log?: ReactivityLog,
-): CellLink {
- if (isAlias(alias)) {
- const doc = isCell(docOrCell) ? docOrCell.getDoc() : docOrCell;
+): LegacyCellLink {
+ if (isDoc(base)) base = base.asCell();
+ else base = base as Cell; // Makes TS happy
+
+ if (isWriteRedirectLink(writeRedirect)) {
+ const link = parseLink(writeRedirect, base);
return followLinks(
- { cell: doc, ...alias.$alias } as CellLink,
+ {
+ cell: base.getDoc().runtime.documentMap.getDocByEntityId(
+ link.space!,
+ link.id!,
+ ),
+ path: link.path,
+ space: link.space,
+ schema: link.schema,
+ rootSchema: link.rootSchema,
+ } as LegacyCellLink,
log,
createVisits(),
true,
);
} else {
- throw new Error(`Alias expected: ${JSON.stringify(alias)}`);
+ throw new Error(
+ `Write redirect expected: ${JSON.stringify(writeRedirect)}`,
+ );
}
}
diff --git a/packages/runner/src/link-utils.ts b/packages/runner/src/link-utils.ts
new file mode 100644
index 000000000..16fc399b4
--- /dev/null
+++ b/packages/runner/src/link-utils.ts
@@ -0,0 +1,467 @@
+import { isObject, isRecord } from "@commontools/utils/types";
+import { type JSONSchema } from "./builder/types.ts";
+import { type DocImpl, isDoc } from "./doc.ts";
+import { type Cell, isCell, type MemorySpace } from "./cell.ts";
+import {
+ type JSONCellLink,
+ type LegacyAlias,
+ type LegacyCellLink,
+ LINK_V1_TAG,
+ type SigilLink,
+ type SigilValue,
+ type SigilWriteRedirectLink,
+ type URI,
+} from "./sigil-types.ts";
+import { toURI } from "./uri-utils.ts";
+import { arrayEqual } from "./path-utils.ts";
+import {
+ getCellLinkOrThrow,
+ isQueryResultForDereferencing,
+ QueryResultInternals,
+} from "./query-result-proxy.ts";
+
+/**
+ * Normalized link structure returned by parsers
+ */
+export type NormalizedLink = {
+ id?: URI; // URI format with "of:" prefix
+ path: string[];
+ space?: MemorySpace;
+ schema?: JSONSchema;
+ rootSchema?: JSONSchema;
+ overwrite?: "redirect"; // "this" gets normalized away to undefined
+};
+
+/**
+ * Normalized link with required id and space (when base Cell is provided)
+ */
+export type NormalizedFullLink = {
+ id: URI; // URI format with "of:" prefix
+ path: string[];
+ space: MemorySpace;
+ schema?: JSONSchema;
+ rootSchema?: JSONSchema;
+ overwrite?: "redirect"; // "this" gets normalized away to undefined
+};
+
+export type CellLink =
+ | Cell
+ | DocImpl
+ | LegacyCellLink
+ | SigilLink
+ | JSONCellLink
+ | LegacyAlias
+ | QueryResultInternals
+ | { "/": string };
+
+/**
+ * Check if value is a sigil value with any type
+ *
+ * Any object that is strictly `{ "/": Record }`, no other props
+ */
+export function isSigilValue(value: any): value is SigilValue {
+ return isRecord(value) &&
+ "/" in value &&
+ Object.keys(value).length === 1 &&
+ isObject(value["/"]);
+}
+
+/**
+ * Check if value is a legacy cell link.
+ *
+ * @param {any} value - The value to check.
+ * @returns {boolean}
+ */
+export function isCellLink(value: any): value is LegacyCellLink | JSONCellLink {
+ return isLegacyCellLink(value) || isJSONCellLink(value);
+}
+
+/**
+ * Check if value is a legacy cell link.
+ *
+ * @param {any} value - The value to check.
+ * @returns {boolean}
+ */
+export function isLegacyCellLink(value: any): value is LegacyCellLink {
+ return (
+ isRecord(value) && isDoc(value.cell) && Array.isArray(value.path)
+ );
+}
+
+/**
+ * Check if value is a JSON cell link (storage format).
+ */
+export function isJSONCellLink(value: any): value is JSONCellLink {
+ return (
+ isRecord(value) &&
+ isRecord(value.cell) &&
+ typeof value.cell["/"] === "string" &&
+ Array.isArray(value.path)
+ );
+}
+
+/**
+ * Check if value is a sigil link.
+ */
+export function isSigilLink(value: any): value is SigilLink {
+ return (isSigilValue(value) && LINK_V1_TAG in value["/"]);
+}
+
+/**
+ * Check if value is a sigil alias (link with overwrite field).
+ */
+export function isSigilWriteRedirectLink(
+ value: any,
+): value is SigilWriteRedirectLink {
+ return isSigilLink(value) &&
+ value["/"][LINK_V1_TAG].overwrite === "redirect";
+}
+
+/**
+ * Check if value is any kind of cell link format.
+ */
+export function isAnyCellLink(
+ value: any,
+): value is LegacyCellLink | SigilLink | JSONCellLink | LegacyAlias {
+ return isCellLink(value) || isJSONCellLink(value) || isSigilLink(value) ||
+ isLegacyAlias(value);
+}
+
+/**
+ * Check if value is any kind of link or linkable entity
+ */
+export function isLink(
+ value: any,
+): value is CellLink {
+ return (
+ isQueryResultForDereferencing(value) ||
+ isAnyCellLink(value) ||
+ isCell(value) ||
+ isDoc(value) ||
+ (isRecord(value) && "/" in value && typeof value["/"] === "string") // EntityId format
+ );
+}
+
+/**
+ * Check if value is an alias in any format (old $alias or new sigil)
+ */
+export function isWriteRedirectLink(
+ value: any,
+): value is LegacyAlias | SigilWriteRedirectLink {
+ // Check legacy $alias format
+ if (isLegacyAlias(value)) {
+ return true;
+ }
+
+ // Check new sigil format (link@1 with overwrite field)
+ if (isSigilLink(value)) {
+ return value["/"][LINK_V1_TAG].overwrite === "redirect";
+ }
+
+ return false;
+}
+
+export function isLegacyAlias(value: any): value is LegacyAlias {
+ return isRecord(value) && "$alias" in value && isRecord(value.$alias) &&
+ Array.isArray(value.$alias.path);
+}
+
+/**
+ * Parse any link-like value to normalized format
+ *
+ * Overloads just help make fields non-optional that can be guaranteed to exist
+ * in various combinations.
+ */
+export function parseLink(
+ value:
+ | Cell
+ | DocImpl,
+ base?: Cell | NormalizedLink,
+): NormalizedFullLink;
+export function parseLink(
+ value: CellLink,
+ base: Cell | NormalizedFullLink,
+): NormalizedFullLink;
+export function parseLink(
+ value: CellLink,
+ base?: Cell | NormalizedLink,
+): NormalizedLink;
+export function parseLink(
+ value: any,
+ base?: Cell | NormalizedLink,
+): NormalizedLink | undefined;
+export function parseLink(
+ value: any,
+ base?: Cell | NormalizedLink,
+): NormalizedLink | undefined {
+ // Has to be first, since below we check for "/" in value and we don't want to
+ // see userland "/".
+ if (isQueryResultForDereferencing(value)) value = getCellLinkOrThrow(value);
+
+ if (isCell(value)) {
+ return {
+ id: toURI(value.getDoc().entityId),
+ path: value.path.map((p) => p.toString()),
+ space: value.space,
+ schema: value.schema,
+ rootSchema: value.rootSchema,
+ };
+ }
+
+ if (isDoc(value)) {
+ // Extract from DocImpl
+ return {
+ id: toURI(value.entityId),
+ path: [],
+ space: value.space,
+ };
+ }
+
+ // Handle new sigil format
+ if (isSigilLink(value)) {
+ const link = value["/"][LINK_V1_TAG];
+
+ // Resolve relative references
+ let id = link.id;
+ const path = link.path || [];
+ const resolvedSpace = link.space || base?.space;
+
+ // If no id provided, use base cell's document
+ if (!id && base) {
+ id = isCell(base) ? toURI(base.getDoc().entityId) : base.id;
+ }
+
+ return {
+ id: id,
+ path: path.map((p) => p.toString()),
+ space: resolvedSpace,
+ schema: link.schema,
+ rootSchema: link.rootSchema,
+ overwrite: link.overwrite === "redirect" ? "redirect" : undefined,
+ };
+ }
+
+ // Handle legacy CellLink format (runtime format with DocImpl)
+ if (isLegacyCellLink(value)) {
+ return {
+ id: toURI(value.cell.entityId),
+ path: value.path.map((p) => p.toString()),
+ space: value.cell.space,
+ schema: value.schema,
+ rootSchema: value.rootSchema,
+ };
+ }
+
+ // Handle JSON CellLink format (storage format with { "/": string })
+ if (isJSONCellLink(value)) {
+ return {
+ id: toURI(value.cell["/"]),
+ path: value.path.map((p) => p.toString()),
+ space: base?.space, // Space must come from context for JSON links
+ };
+ }
+
+ if (isRecord(value) && "/" in value) {
+ return {
+ id: toURI(value["/"]),
+ path: [],
+ space: base?.space, // Space must come from context for JSON links
+ };
+ }
+
+ // Handle legacy alias format
+ if (isLegacyAlias(value)) {
+ const alias = value.$alias;
+ let id: URI | undefined;
+ let resolvedSpace = base?.space;
+
+ // If cell is provided, convert to URI
+ if (alias.cell) {
+ if (isDoc(alias.cell)) {
+ id = toURI(alias.cell.entityId);
+ resolvedSpace = alias.cell.space;
+ } else if (isRecord(alias.cell) && "/" in alias.cell) {
+ id = toURI(alias.cell);
+ }
+ }
+
+ // If no cell provided, use base cell's document
+ if (!id && base) {
+ id = isCell(base) ? toURI(base.getDoc().entityId) : base.id;
+ }
+
+ return {
+ id: id,
+ path: Array.isArray(alias.path)
+ ? alias.path.map((p) => p.toString())
+ : [],
+ space: resolvedSpace,
+ schema: alias.schema as JSONSchema | undefined,
+ rootSchema: alias.rootSchema as JSONSchema | undefined,
+ };
+ }
+
+ return undefined;
+}
+
+/**
+ * Parse any link-like value to normalized format, throwing on failure
+ */
+export function parseLinkOrThrow(
+ value: any,
+ baseCell?: Cell,
+): NormalizedLink {
+ const result = parseLink(value, baseCell);
+ if (!result) {
+ throw new Error(`Cannot parse value as link: ${JSON.stringify(value)}`);
+ }
+ return result;
+}
+
+/**
+ * Parse a link to a legacy CellLink format
+ *
+ * @param value - The value to parse
+ * @param baseCell - The base cell to use for resolving relative references
+ * @returns The parsed cell link, or undefined if the value cannot be parsed
+ */
+export function parseToLegacyCellLink(
+ value: CellLink,
+ baseCell?: Cell,
+): LegacyCellLink;
+export function parseToLegacyCellLink(
+ value: any,
+ baseCell?: Cell,
+): LegacyCellLink | undefined;
+export function parseToLegacyCellLink(
+ value: any,
+ baseCell?: Cell,
+): LegacyCellLink | undefined {
+ const partial = parseToLegacyCellLinkWithMaybeACell(value, baseCell);
+ if (!partial) return undefined;
+ if (!isDoc(partial.cell)) throw new Error("No id or base cell provided");
+ return partial as LegacyCellLink;
+}
+
+/**
+ * Parse a link to a legacy Alias format
+ *
+ * @param value - The value to parse
+ * @param baseCell - The base cell to use for resolving relative references
+ * @returns The parsed alias, or undefined if the value cannot be parsed
+ */
+export function parseToLegacyAlias(
+ value: CellLink,
+): LegacyAlias;
+export function parseToLegacyAlias(value: any): LegacyAlias | undefined;
+export function parseToLegacyAlias(value: any): LegacyAlias | undefined {
+ const partial = parseToLegacyCellLinkWithMaybeACell(value);
+ if (!partial) return undefined;
+ return { $alias: partial } as LegacyAlias;
+}
+
+function parseToLegacyCellLinkWithMaybeACell(
+ value: any,
+ baseCell?: Cell,
+): Partial | undefined {
+ // Has to be first, since below we check for "/" in value and we don't want to
+ // see userland "/".
+ if (isQueryResultForDereferencing(value)) value = getCellLinkOrThrow(value);
+
+ // parseLink "forgets" the legacy docs, so we for now parse it here as well.
+ // This is in case no baseCell was provided.
+ const doc = isDoc(value)
+ ? value
+ : isCell(value)
+ ? value.getDoc()
+ : (isRecord(value) && isDoc((value as any).cell))
+ ? (value as any).cell
+ : (isRecord(value) && (value as any).$alias &&
+ isDoc((value as any).$alias.cell))
+ ? (value as any).$alias.cell
+ : undefined;
+
+ const link = parseLink(value, baseCell);
+ if (!link) return undefined;
+
+ const cellValue = doc ??
+ (link.id && baseCell
+ ? baseCell.getDoc().runtime!.documentMap.getDocByEntityId(
+ link.space ?? baseCell!.space!,
+ link.id!,
+ true,
+ )
+ : undefined);
+
+ return {
+ cell: cellValue,
+ path: link.path ?? [],
+ space: link.space,
+ schema: link.schema,
+ rootSchema: link.rootSchema,
+ } satisfies Partial;
+}
+
+/**
+ * Compare two link values for equality, supporting all link formats
+ */
+export function areLinksSame(
+ value1: any,
+ value2: any,
+ base?: Cell | NormalizedLink,
+): boolean {
+ // If both are the same object, they're equal
+ if (value1 === value2) return true;
+
+ // If either is null/undefined, they're only equal if both are
+ if (!value1 || !value2) return value1 === value2;
+
+ // Try parsing both as links
+ const link1 = parseLink(value1, base);
+ const link2 = parseLink(value2, base);
+
+ // If one parses and the other doesn't, they're not equal
+ if (!link1 || !link2) return false;
+
+ // Compare normalized links
+ return (
+ link1.id === link2.id &&
+ link1.space === link2.space &&
+ arrayEqual(link1.path, link2.path)
+ );
+}
+
+export function createSigilLinkFromParsedLink(
+ link: NormalizedLink,
+ base?: Cell | NormalizedLink,
+): SigilLink {
+ const sigilLink: SigilLink = {
+ "/": {
+ [LINK_V1_TAG]: {
+ path: link.path,
+ schema: link.schema,
+ rootSchema: link.rootSchema,
+ },
+ },
+ };
+
+ // Only add space if different from base
+ if (link.space !== base?.space) {
+ sigilLink["/"][LINK_V1_TAG].space = link.space;
+ }
+
+ // Only add id if different from base
+ const baseId = base
+ ? (isCell(base) ? toURI(base.getDoc().entityId) : base.id)
+ : undefined;
+ if (link.id !== baseId) {
+ sigilLink["/"][LINK_V1_TAG].id = link.id;
+ }
+
+ // Only add overwrite if it's a redirect
+ if (link.overwrite === "redirect") {
+ sigilLink["/"][LINK_V1_TAG].overwrite = link.overwrite;
+ }
+
+ return sigilLink;
+}
diff --git a/packages/runner/src/path-utils.ts b/packages/runner/src/path-utils.ts
index 3215cdb27..9662547a3 100644
--- a/packages/runner/src/path-utils.ts
+++ b/packages/runner/src/path-utils.ts
@@ -64,3 +64,10 @@ export const deepEqual = (a: any, b: any): boolean => {
}
return a !== a && b !== b; // NaN check
};
+
+export function arrayEqual(a?: PropertyKey[], b?: PropertyKey[]): boolean {
+ if (!a || !b) return a === b;
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
+ return true;
+}
diff --git a/packages/runner/src/query-result-proxy.ts b/packages/runner/src/query-result-proxy.ts
index d25d279b1..af3b4c2d7 100644
--- a/packages/runner/src/query-result-proxy.ts
+++ b/packages/runner/src/query-result-proxy.ts
@@ -2,7 +2,7 @@ import { isRecord } from "@commontools/utils/types";
import { getTopFrame } from "./builder/recipe.ts";
import { toOpaqueRef } from "./builder/types.ts";
import { type DocImpl, makeOpaqueRef } from "./doc.ts";
-import { type CellLink } from "./cell.ts";
+import { type LegacyCellLink } from "./sigil-types.ts";
import { type ReactivityLog } from "./scheduler.ts";
import { diffAndUpdate, setNestedValue } from "./data-updating.ts";
import { resolveLinkToValue } from "./link-resolution.ts";
@@ -97,7 +97,7 @@ export function createQueryResultProxy(
get: (target, prop, receiver) => {
if (typeof prop === "symbol") {
if (prop === getCellLink) {
- return { cell: valueCell, path: valuePath } satisfies CellLink;
+ return { cell: valueCell, path: valuePath } satisfies LegacyCellLink;
} else if (prop === toOpaqueRef) {
return () => makeOpaqueRef(valueCell, valuePath);
}
@@ -302,9 +302,9 @@ function isProxyForArrayValue(value: any): value is ProxyForArrayValue {
* Get cell link or return values as is if not a cell value proxy.
*
* @param {any} value - The value to get the cell link or value from.
- * @returns {CellLink | any}
+ * @returns {LegacyCellLink | any}
*/
-export function getCellLinkOrValue(value: any): CellLink {
+export function getCellLinkOrValue(value: any): LegacyCellLink {
if (isQueryResult(value)) return value[getCellLink];
else return value;
}
@@ -313,10 +313,10 @@ export function getCellLinkOrValue(value: any): CellLink {
* Get cell link or throw if not a cell value proxy.
*
* @param {any} value - The value to get the cell link from.
- * @returns {CellLink}
+ * @returns {LegacyCellLink}
* @throws {Error} If the value is not a cell value proxy.
*/
-export function getCellLinkOrThrow(value: any): CellLink {
+export function getCellLinkOrThrow(value: any): LegacyCellLink {
if (isQueryResult(value)) return value[getCellLink];
else throw new Error("Value is not a cell proxy");
}
@@ -347,7 +347,7 @@ export function isQueryResultForDereferencing(
}
export type QueryResultInternals = {
- [getCellLink]: CellLink;
+ [getCellLink]: LegacyCellLink;
};
export type QueryResult = T & QueryResultInternals;
diff --git a/packages/runner/src/recipe-binding.ts b/packages/runner/src/recipe-binding.ts
index 1dc23e6a0..44688d1d0 100644
--- a/packages/runner/src/recipe-binding.ts
+++ b/packages/runner/src/recipe-binding.ts
@@ -1,17 +1,17 @@
import { isRecord } from "@commontools/utils/types";
import {
- type Alias,
- isAlias,
type Recipe,
unsafe_materializeFactory,
unsafe_originalRecipe,
unsafe_parentRecipe,
type UnsafeBinding,
} from "./builder/types.ts";
+import { isLegacyAlias, isLink } from "./link-utils.ts";
import { type DocImpl, isDoc } from "./doc.ts";
-import { type Cell, type CellLink, isCell, isCellLink } from "./cell.ts";
+import { type Cell, isCell } from "./cell.ts";
+import { type LegacyCellLink } from "./sigil-types.ts";
import { type ReactivityLog } from "./scheduler.ts";
-import { followAliases } from "./link-resolution.ts";
+import { followWriteRedirects } from "./link-resolution.ts";
import { diffAndUpdate } from "./data-updating.ts";
/**
@@ -32,8 +32,8 @@ export function sendValueToBinding(
log?: ReactivityLog,
): void {
const doc = isCell(docOrCell) ? docOrCell.getDoc() : docOrCell;
- if (isAlias(binding)) {
- const ref = followAliases(binding, doc, log);
+ if (isLegacyAlias(binding)) {
+ const ref = followWriteRedirects(binding, doc, log);
diffAndUpdate(ref, value, log, { doc, binding });
} else if (Array.isArray(binding)) {
if (Array.isArray(value)) {
@@ -85,7 +85,7 @@ export function unwrapOneLevelAndBindtoDoc(
): T {
const doc = isCell(docOrCell) ? docOrCell.getDoc() : docOrCell;
function convert(binding: unknown): unknown {
- if (isAlias(binding)) {
+ if (isLegacyAlias(binding)) {
const alias = { ...binding.$alias };
if (typeof alias.cell === "number") {
if (alias.cell === 1) {
@@ -144,14 +144,15 @@ export function unsafe_createParentBindings(
}
}
-// Traverses binding and returns all docs reacheable through aliases.
-export function findAllAliasedCells(
+// Traverses binding and returns all docs reacheable through legacy aliases.
+// TODO(seefeld): Transition to all write redirects once recipes use those.
+export function findAllLegacyAliasedCells(
binding: unknown,
doc: DocImpl,
-): CellLink[] {
- const docs: CellLink[] = [];
+): LegacyCellLink[] {
+ const docs: LegacyCellLink[] = [];
function find(binding: unknown, origDoc: DocImpl): void {
- if (isAlias(binding)) {
+ if (isLegacyAlias(binding)) {
// Numbered docs are yet to be unwrapped nested recipes. Ignore them.
if (typeof binding.$alias.cell === "number") return;
const doc = (binding.$alias.cell ?? origDoc) as DocImpl;
@@ -161,13 +162,7 @@ export function findAllAliasedCells(
find(doc.getAtPath(path), doc);
} else if (Array.isArray(binding)) {
for (const value of binding) find(value, origDoc);
- } else if (
- typeof binding === "object" &&
- binding !== null &&
- !isCellLink(binding) &&
- !isDoc(binding) &&
- !isCell(binding)
- ) {
+ } else if (isRecord(binding) && !isLink(binding)) {
for (const value of Object.values(binding)) find(value, origDoc);
}
}
diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts
index 827b144bd..fa8847dca 100644
--- a/packages/runner/src/runner.ts
+++ b/packages/runner/src/runner.ts
@@ -1,10 +1,9 @@
-import { isObject, isRecord } from "@commontools/utils/types";
+import { isObject, isRecord, type Mutable } from "@commontools/utils/types";
import {
- type Alias,
- isAlias,
isModule,
+ isOpaqueRef,
isRecipe,
- isStreamAlias,
+ isStreamValue,
type JSONSchema,
type JSONValue,
type Module,
@@ -23,20 +22,25 @@ import {
import { type DocImpl, isDoc } from "./doc.ts";
import { type Cell } from "./cell.ts";
import { type Action, type ReactivityLog } from "./scheduler.ts";
-import { containsOpaqueRef, deepCopy } from "./type-utils.ts";
import { diffAndUpdate } from "./data-updating.ts";
import {
- findAllAliasedCells,
+ findAllLegacyAliasedCells,
unsafe_noteParentOnRecipes,
unwrapOneLevelAndBindtoDoc,
} from "./recipe-binding.ts";
-import { followAliases, maybeGetCellLink } from "./link-resolution.ts";
+import { followWriteRedirects } from "./link-resolution.ts";
+import {
+ areLinksSame,
+ isLink,
+ isWriteRedirectLink,
+ parseLink,
+ parseToLegacyAlias,
+} from "./link-utils.ts";
import { sendValueToBinding } from "./recipe-binding.ts";
import { type AddCancel, type Cancel, useCancelGroup } from "./cancel.ts";
import "./builtins/index.ts";
-import { type CellLink, isCell, isCellLink } from "./cell.ts";
-import { isQueryResultForDereferencing } from "./query-result-proxy.ts";
-import { getCellLinkOrThrow } from "./query-result-proxy.ts";
+import { isCell } from "./cell.ts";
+import { type LegacyCellLink } from "./sigil-types.ts";
import type { IRunner, IRuntime } from "./runtime.ts";
export class Runner implements IRunner {
@@ -177,21 +181,8 @@ export class Runner implements IRunner {
this.allCancels.add(cancel);
// If the bindings are a cell, doc or doc link, convert them to an alias
- if (
- isDoc(argument) ||
- isCellLink(argument) ||
- isCell(argument) ||
- isQueryResultForDereferencing(argument)
- ) {
- const ref = isCellLink(argument)
- ? argument
- : isCell(argument)
- ? argument.getAsCellLink()
- : isQueryResultForDereferencing(argument)
- ? getCellLinkOrThrow(argument)
- : ({ cell: argument, path: [] } satisfies CellLink);
-
- argument = { $alias: ref } as T;
+ if (isLink(argument)) {
+ argument = parseToLegacyAlias(argument) as T;
}
// Walk the recipe's schema and extract all default values
@@ -201,8 +192,10 @@ export class Runner implements IRunner {
const previousInternal = processCell.get()?.internal;
const internal: JSONValue = Object.assign(
{},
- deepCopy((defaults as unknown as { internal: JSONValue })?.internal),
- deepCopy(
+ cellAwareDeepCopy(
+ (defaults as unknown as { internal: JSONValue })?.internal,
+ ),
+ cellAwareDeepCopy(
isRecord(recipe.initial) && isRecord(recipe.initial.internal)
? recipe.initial.internal
: {},
@@ -305,10 +298,12 @@ export class Runner implements IRunner {
if (seen.has(value)) return;
seen.add(value);
- const link = maybeGetCellLink(value);
+ const link = parseLink(value, resultCell);
- if (link && link.cell) {
- const maybePromise = this.runtime.storage.syncCell(link.cell);
+ if (link) {
+ const maybePromise = this.runtime.storage.syncCell(
+ this.runtime.getCellFromLink(link),
+ );
if (maybePromise instanceof Promise) promises.add(maybePromise);
} else if (isRecord(value)) {
for (const key in value) syncAllMentionedCells(value[key]);
@@ -338,8 +333,8 @@ export class Runner implements IRunner {
for (const node of recipe.nodes) {
const sourceDoc = sourceCell.getDoc();
- const inputs = findAllAliasedCells(node.inputs, sourceDoc);
- const outputs = findAllAliasedCells(node.outputs, sourceDoc);
+ const inputs = findAllLegacyAliasedCells(node.inputs, sourceDoc);
+ const outputs = findAllLegacyAliasedCells(node.outputs, sourceDoc);
// TODO(seefeld): This ignores schemas provided by modules, so it might
// still fetch a lot.
@@ -389,7 +384,7 @@ export class Runner implements IRunner {
}
private instantiateNode(
- module: Module | Alias,
+ module: Module,
inputBindings: JSONValue,
outputBindings: JSONValue,
processCell: DocImpl,
@@ -453,7 +448,7 @@ export class Runner implements IRunner {
default:
throw new Error(`Unknown module type: ${module.type}`);
}
- } else if (isAlias(module)) {
+ } else if (isWriteRedirectLink(module)) {
// TODO(seefeld): Implement, a dynamic node
} else {
throw new Error(`Unknown module: ${JSON.stringify(module)}`);
@@ -473,10 +468,10 @@ export class Runner implements IRunner {
processCell,
);
- const reads = findAllAliasedCells(inputs, processCell);
+ const reads = findAllLegacyAliasedCells(inputs, processCell);
const outputs = unwrapOneLevelAndBindtoDoc(outputBindings, processCell);
- const writes = findAllAliasedCells(outputs, processCell);
+ const writes = findAllLegacyAliasedCells(outputs, processCell);
let fn = (
typeof module.implementation === "string"
@@ -489,20 +484,20 @@ export class Runner implements IRunner {
}
// Check if any of the read cells is a stream alias
- let streamRef: CellLink | undefined = undefined;
+ let streamRef: LegacyCellLink | undefined = undefined;
if (isRecord(inputs)) {
for (const key in inputs) {
let doc = processCell;
let path: PropertyKey[] = [key];
let value = inputs[key];
- while (isAlias(value)) {
- const ref = followAliases(value, processCell);
+ while (isWriteRedirectLink(value)) {
+ const ref = followWriteRedirects(value, processCell);
doc = ref.cell;
path = ref.path;
value = doc.getAtPath(path);
}
- if (isStreamAlias(value)) {
- streamRef = { cell: doc, path };
+ if (isStreamValue(value)) {
+ streamRef = { cell: doc, path } satisfies LegacyCellLink;
break;
}
}
@@ -517,17 +512,20 @@ export class Runner implements IRunner {
const eventInputs = { ...(inputs as Record) };
const cause = { ...(inputs as Record) };
for (const key in eventInputs) {
- if (
- isAlias(eventInputs[key]) &&
- eventInputs[key].$alias.cell === stream.cell &&
- eventInputs[key].$alias.path.length === stream.path.length &&
- eventInputs[key].$alias.path.every(
- (value: PropertyKey, index: number) =>
- value === stream.path[index],
- )
- ) {
- eventInputs[key] = event;
- cause[key] = crypto.randomUUID();
+ if (isWriteRedirectLink(eventInputs[key])) {
+ // Use format-agnostic comparison for aliases
+ const alias = eventInputs[key];
+
+ if (
+ areLinksSame(
+ alias,
+ streamRef,
+ processCell.asCell(),
+ )
+ ) {
+ eventInputs[key] = event;
+ cause[key] = crypto.randomUUID();
+ }
}
}
@@ -695,8 +693,14 @@ export class Runner implements IRunner {
// note the parent recipe on the closure recipes.
unsafe_noteParentOnRecipes(recipe, mappedInputBindings);
- const inputCells = findAllAliasedCells(mappedInputBindings, processCell);
- const outputCells = findAllAliasedCells(mappedOutputBindings, processCell);
+ const inputCells = findAllLegacyAliasedCells(
+ mappedInputBindings,
+ processCell,
+ );
+ const outputCells = findAllLegacyAliasedCells(
+ mappedOutputBindings,
+ processCell,
+ );
const action = module.implementation(
processCell.runtime!.documentMap.getDoc(
@@ -732,10 +736,10 @@ export class Runner implements IRunner {
const inputsCell = processCell.runtime!.documentMap.getDoc(inputs, {
immutable: inputs,
}, processCell.space);
- const reads = findAllAliasedCells(inputs, processCell);
+ const reads = findAllLegacyAliasedCells(inputs, processCell);
const outputs = unwrapOneLevelAndBindtoDoc(outputBindings, processCell);
- const writes = findAllAliasedCells(outputs, processCell);
+ const writes = findAllLegacyAliasedCells(outputs, processCell);
const action: Action = (log: ReactivityLog) => {
const inputsProxy = inputsCell.getAsQueryResult([], log);
@@ -786,6 +790,29 @@ export class Runner implements IRunner {
}
}
+function containsOpaqueRef(value: unknown): boolean {
+ if (isOpaqueRef(value)) return true;
+ if (isLink(value)) return false;
+ if (isRecord(value)) {
+ return Object.values(value).some(containsOpaqueRef);
+ }
+ return false;
+}
+
+export function cellAwareDeepCopy(value: T): Mutable {
+ if (isLink(value)) return value as Mutable;
+ if (isRecord(value)) {
+ return Array.isArray(value)
+ ? value.map(cellAwareDeepCopy) as unknown as Mutable
+ : Object.fromEntries(
+ Object.entries(value).map((
+ [key, value],
+ ) => [key, cellAwareDeepCopy(value)]),
+ ) as unknown as Mutable;
+ // Literal value:
+ } else return value as Mutable;
+}
+
/**
* Extracts default values from a JSON schema object.
* @param schema - The JSON schema to extract defaults from
@@ -801,7 +828,9 @@ export function extractDefaultValues(
) {
// Ignore the schema.default if it's not an object, since it's not a valid
// default value for an object.
- const obj = deepCopy(isRecord(schema.default) ? schema.default : {});
+ const obj = cellAwareDeepCopy(
+ isRecord(schema.default) ? schema.default : {},
+ );
for (const [propKey, propSchema] of Object.entries(schema.properties)) {
const value = extractDefaultValues(propSchema);
if (value !== undefined) {
@@ -836,15 +865,7 @@ export function mergeObjects(
// If we have a literal value, return it. Same for arrays, since we wouldn't
// know how to merge them. Note that earlier objects take precedence, so if
// an earlier was e.g. an object, we'll return that instead of the literal.
- if (
- typeof obj !== "object" ||
- obj === null ||
- Array.isArray(obj) ||
- isAlias(obj) ||
- isCellLink(obj) ||
- isDoc(obj) ||
- isCell(obj)
- ) {
+ if (!isObject(obj) || isLink(obj)) {
return obj as T;
}
diff --git a/packages/runner/src/runtime.ts b/packages/runner/src/runtime.ts
index d895e140a..050893b50 100644
--- a/packages/runner/src/runtime.ts
+++ b/packages/runner/src/runtime.ts
@@ -8,15 +8,13 @@ import type {
import type { RecipeEnvironment } from "./builder/env.ts";
import { ContextualFlowControl } from "./cfc.ts";
import { setRecipeEnvironment } from "./builder/env.ts";
-
import type {
IStorageManager,
IStorageProvider,
MemorySpace,
} from "./storage/interface.ts";
-
-export type { IStorageManager, IStorageProvider, MemorySpace };
-import type { Cell, CellLink } from "./cell.ts";
+import { type Cell } from "./cell.ts";
+import { type JSONCellLink, type LegacyCellLink } from "./sigil-types.ts";
import type { DocImpl } from "./doc.ts";
import { isDoc } from "./doc.ts";
import { type EntityId, getEntityId } from "./doc-map.ts";
@@ -25,6 +23,9 @@ import type { Action, EventHandler, ReactivityLog } from "./scheduler.ts";
import type { Harness } from "./harness/harness.ts";
import { Engine } from "./harness/index.ts";
import { ConsoleMethod } from "./harness/console.ts";
+import { isCellLink, type NormalizedLink } from "./link-utils.ts";
+
+export type { IStorageManager, IStorageProvider, MemorySpace };
export type ErrorWithContext = Error & {
action: Action;
@@ -99,12 +100,12 @@ export interface IRuntime {
log?: ReactivityLog,
): Cell>;
getCellFromLink(
- cellLink: CellLink,
+ cellLink: LegacyCellLink | NormalizedLink,
schema?: JSONSchema,
log?: ReactivityLog,
): Cell;
getCellFromLink(
- cellLink: CellLink,
+ cellLink: LegacyCellLink | NormalizedLink,
schema: S,
log?: ReactivityLog,
): Cell>;
@@ -158,8 +159,8 @@ export interface IScheduler {
unschedule(action: Action): void;
onConsole(fn: ConsoleHandler): void;
onError(fn: ErrorHandler): void;
- queueEvent(eventRef: CellLink, event: any): void;
- addEventHandler(handler: EventHandler, ref: CellLink): Cancel;
+ queueEvent(eventRef: LegacyCellLink, event: any): void;
+ addEventHandler(handler: EventHandler, ref: LegacyCellLink): Cancel;
runningPromise: Promise | undefined;
}
@@ -203,7 +204,13 @@ export interface IDocumentMap {
getDocByEntityId(
space: MemorySpace,
entityId: EntityId | string,
- createIfNotFound?: boolean,
+ createIfNotFound?: true,
+ sourceIfCreated?: DocImpl,
+ ): DocImpl;
+ getDocByEntityId(
+ space: MemorySpace,
+ entityId: EntityId | string,
+ createIfNotFound: false,
sourceIfCreated?: DocImpl,
): DocImpl | undefined;
registerDoc(entityId: EntityId, doc: DocImpl, space: MemorySpace): void;
@@ -420,38 +427,52 @@ export class Runtime implements IRuntime {
}
getCellFromLink(
- cellLink: CellLink,
+ cellLink: LegacyCellLink | JSONCellLink | NormalizedLink,
schema?: JSONSchema,
log?: ReactivityLog,
): Cell;
getCellFromLink(
- cellLink: CellLink,
+ cellLink: LegacyCellLink | JSONCellLink | NormalizedLink,
schema: S,
log?: ReactivityLog,
): Cell>;
getCellFromLink(
- cellLink: CellLink,
+ cellLink: LegacyCellLink | NormalizedLink,
schema?: JSONSchema,
log?: ReactivityLog,
): Cell {
let doc;
- if (isDoc(cellLink.cell)) {
- doc = cellLink.cell;
- } else if (cellLink.space) {
+ if (isCellLink(cellLink)) {
+ if (isDoc(cellLink.cell)) {
+ doc = cellLink.cell;
+ } else if (cellLink.space) {
+ doc = this.documentMap.getDocByEntityId(
+ cellLink.space as MemorySpace,
+ getEntityId(cellLink.cell)!,
+ true,
+ )!;
+ if (!doc) {
+ throw new Error(`Can't find ${cellLink.space}/${cellLink.cell}!`);
+ }
+ } else {
+ throw new Error("Cell link has no space");
+ }
+ } else {
doc = this.documentMap.getDocByEntityId(
cellLink.space as MemorySpace,
- getEntityId(cellLink.cell)!,
+ getEntityId((cellLink as NormalizedLink).id)!,
true,
)!;
- if (!doc) {
- throw new Error(`Can't find ${cellLink.space}/${cellLink.cell}!`);
- }
- } else {
- throw new Error("Cell link has no space");
}
+
// If we aren't passed a schema, use the one in the cellLink
- return doc.asCell(cellLink.path, log, schema ?? cellLink.schema);
+ return doc.asCell(
+ cellLink.path,
+ log,
+ schema ?? cellLink.schema,
+ schema ? undefined : cellLink.rootSchema,
+ );
}
getImmutableCell(
diff --git a/packages/runner/src/scheduler.ts b/packages/runner/src/scheduler.ts
index b3cb5d603..345f9243e 100644
--- a/packages/runner/src/scheduler.ts
+++ b/packages/runner/src/scheduler.ts
@@ -2,7 +2,7 @@ import { getTopFrame } from "./builder/recipe.ts";
import { TYPE } from "./builder/types.ts";
import type { DocImpl } from "./doc.ts";
import type { Cancel } from "./cancel.ts";
-import { type CellLink } from "./cell.ts";
+import { type LegacyCellLink } from "./sigil-types.ts";
import {
getCellLinkOrThrow,
isQueryResultForDereferencing,
@@ -30,8 +30,8 @@ export type EventHandler = (event: any) => any;
* dependencies and to topologically sort pending actions before executing them.
*/
export type ReactivityLog = {
- reads: CellLink[];
- writes: CellLink[];
+ reads: LegacyCellLink[];
+ writes: LegacyCellLink[];
};
const MAX_ITERATIONS_PER_RUN = 100;
@@ -39,7 +39,7 @@ const MAX_ITERATIONS_PER_RUN = 100;
export class Scheduler implements IScheduler {
private pending = new Set();
private eventQueue: (() => void)[] = [];
- private eventHandlers: [CellLink, EventHandler][] = [];
+ private eventHandlers: [LegacyCellLink, EventHandler][] = [];
private dirty = new Set>();
private dependencies = new WeakMap();
private cancels = new WeakMap();
@@ -179,7 +179,7 @@ export class Scheduler implements IScheduler {
});
}
- queueEvent(eventRef: CellLink, event: any): void {
+ queueEvent(eventRef: LegacyCellLink, event: any): void {
for (const [ref, handler] of this.eventHandlers) {
if (
ref.cell === eventRef.cell &&
@@ -192,7 +192,7 @@ export class Scheduler implements IScheduler {
}
}
- addEventHandler(handler: EventHandler, ref: CellLink): Cancel {
+ addEventHandler(handler: EventHandler, ref: LegacyCellLink): Cancel {
this.eventHandlers.push([ref, handler]);
return () => {
const index = this.eventHandlers.findIndex(([r, h]) =>
@@ -216,7 +216,10 @@ export class Scheduler implements IScheduler {
this.scheduled = true;
}
- private setDependencies(action: Action, log: ReactivityLog): CellLink[] {
+ private setDependencies(
+ action: Action,
+ log: ReactivityLog,
+ ): LegacyCellLink[] {
const reads = compactifyPaths(log.reads);
const writes = compactifyPaths(log.writes);
this.dependencies.set(action, { reads, writes });
@@ -423,7 +426,7 @@ function topologicalSort(
}
// Remove longer paths already covered by shorter paths
-export function compactifyPaths(entries: CellLink[]): CellLink[] {
+export function compactifyPaths(entries: LegacyCellLink[]): LegacyCellLink[] {
// First group by doc via a Map
const docToPaths = new Map, PropertyKey[][]>();
for (const { cell: doc, path } of entries) {
@@ -434,7 +437,7 @@ export function compactifyPaths(entries: CellLink[]): CellLink[] {
// For each cell, sort the paths by length, then only return those that don't
// have a prefix earlier in the list
- const result: CellLink[] = [];
+ const result: LegacyCellLink[] = [];
for (const [doc, paths] of docToPaths.entries()) {
paths.sort((a, b) => a.length - b.length);
for (let i = 0; i < paths.length; i++) {
diff --git a/packages/runner/src/schema.ts b/packages/runner/src/schema.ts
index 50eb08d9a..80a6fc84d 100644
--- a/packages/runner/src/schema.ts
+++ b/packages/runner/src/schema.ts
@@ -1,14 +1,12 @@
import { isObject, isRecord, type Mutable } from "@commontools/utils/types";
import { ContextualFlowControl } from "./cfc.ts";
-import {
- isAlias,
- type JSONSchema,
- type JSONValue,
-} from "./builder/types.ts";
+import { type JSONSchema, type JSONValue } from "./builder/types.ts";
+import { isAnyCellLink, isWriteRedirectLink, parseLink } from "./link-utils.ts";
import { type DocImpl } from "./doc.ts";
-import { type CellLink, createCell, isCell, isCellLink } from "./cell.ts";
+import { createCell, isCell } from "./cell.ts";
+import { type LegacyCellLink } from "./sigil-types.ts";
import { type ReactivityLog } from "./scheduler.ts";
-import { resolveLinks, resolveLinkToAlias } from "./link-resolution.ts";
+import { resolveLinks, resolveLinkToWriteRedirect } from "./link-resolution.ts";
/**
* Schemas are mostly a subset of JSONSchema.
@@ -281,7 +279,7 @@ export function validateAndTransform(
// Follow aliases, etc. to last element on path + just aliases on that last one
// When we generate cells below, we want them to be based off this value, as that
// is what a setter would change when they update a value or reference.
- const resolvedRef = resolveLinkToAlias(
+ const resolvedRef = resolveLinkToWriteRedirect(
doc,
path,
log,
@@ -333,20 +331,21 @@ export function validateAndTransform(
// the top of the current doc is already a reference.
for (let i = -1; i < path.length; i++) {
const value = doc.getAtPath(path.slice(0, i + 1));
- if (isAlias(value)) {
+ if (isWriteRedirectLink(value)) {
throw new Error(
- "Unexpected alias in path, should have been handled by resolvePath",
+ "Unexpected write redirect in path, should have been handled by resolvePath",
);
}
- if (isCellLink(value)) {
+ if (isAnyCellLink(value)) {
+ const link = parseLink(value, doc.asCell());
log?.reads.push({ cell: doc, path: path.slice(0, i + 1) });
const extraPath = [...path.slice(i + 1)];
- const newPath = [...value.path, ...extraPath];
+ const newPath = [...link.path, ...extraPath];
const cfc = doc.runtime.cfc;
let newSchema;
- if (value.schema !== undefined) {
+ if (link.schema !== undefined) {
newSchema = cfc.getSchemaAtPath(
- value.schema,
+ link.schema,
extraPath.map((key) => key.toString()),
rootSchema,
);
@@ -357,7 +356,7 @@ export function validateAndTransform(
newSchema = cfc.getSchemaAtPath(resolvedSchema, []);
}
return createCell(
- value.cell,
+ doc.runtime.documentMap.getDocByEntityId(link.space, link.id, true),
newPath,
log,
newSchema,
@@ -509,7 +508,7 @@ export function validateAndTransform(
// Merge all the object extractions
let merged: Record = {};
- const extraReads: CellLink[] = [];
+ const extraReads: LegacyCellLink[] = [];
for (const { result, extraLog } of candidates) {
if (isCell(result)) {
merged = result;
diff --git a/packages/runner/src/sigil-types.ts b/packages/runner/src/sigil-types.ts
new file mode 100644
index 000000000..43fa47140
--- /dev/null
+++ b/packages/runner/src/sigil-types.ts
@@ -0,0 +1,82 @@
+import type { JSONSchema } from "@commontools/api";
+import type { MemorySpace } from "@commontools/memory/interface";
+import type { ShadowRef } from "./builder/types.ts";
+import type { DocImpl } from "./doc.ts";
+import type { URI } from "@commontools/memory/interface";
+
+export type { URI } from "@commontools/memory/interface";
+
+/**
+ * Generic sigil value type for future extensions
+ */
+export type SigilValue = { "/": T };
+
+/**
+ * Link sigil value v1
+ */
+
+export const LINK_V1_TAG = "link@1" as const;
+
+export type LinkV1 = {
+ [LINK_V1_TAG]: {
+ id?: URI;
+ path?: string[];
+ space?: MemorySpace;
+ schema?: JSONSchema;
+ rootSchema?: JSONSchema;
+ overwrite?: "redirect" | "this"; // default is "this"
+ };
+};
+
+export type WriteRedirectV1 = LinkV1 & {
+ [LINK_V1_TAG]: { overwrite: "redirect" };
+};
+/**
+ * Sigil link type
+ */
+
+export type SigilLink = SigilValue;
+/**
+ * Sigil alias type - uses LinkV1 with overwrite field
+ */
+
+export type SigilWriteRedirectLink = SigilValue;
+
+/****************
+ * Legacy types *
+ ****************/
+
+/**
+ * Cell link.
+ *
+ * A cell link is a doc and a path within that doc.
+ */
+export type LegacyCellLink = {
+ space?: MemorySpace;
+ cell: DocImpl;
+ path: PropertyKey[];
+ schema?: JSONSchema;
+ rootSchema?: JSONSchema;
+};
+
+/**
+ * Legacy alias.
+ *
+ * A legacy alias is a cell and a path within that cell.
+ */
+export type LegacyAlias = {
+ $alias: {
+ cell?: DocImpl | ShadowRef | number;
+ path: PropertyKey[];
+ schema?: JSONSchema;
+ rootSchema?: JSONSchema;
+ };
+};
+
+/**
+ * JSON cell link format used in storage
+ */
+export type JSONCellLink = {
+ cell: { "/": string };
+ path: (string | number)[];
+};
diff --git a/packages/runner/src/storage.ts b/packages/runner/src/storage.ts
index ada72daa8..b3c495c13 100644
--- a/packages/runner/src/storage.ts
+++ b/packages/runner/src/storage.ts
@@ -10,13 +10,14 @@ import {
} from "@commontools/memory/interface";
import { SchemaNone } from "@commontools/memory/schema";
import { type AddCancel, type Cancel, useCancelGroup } from "./cancel.ts";
-import { Cell, type CellLink, isCell, isCellLink, isStream } from "./cell.ts";
+import { Cell, isCell, isStream } from "./cell.ts";
import { type DocImpl, isDoc } from "./doc.ts";
-import { type EntityId } from "./doc-map.ts";
import {
getCellLinkOrThrow,
isQueryResultForDereferencing,
} from "./query-result-proxy.ts";
+import { type EntityId } from "./doc-map.ts";
+import { isLink, parseLink } from "./link-utils.ts";
import type {
IStorageManager,
IStorageProvider,
@@ -265,31 +266,28 @@ export class Storage implements IStorage {
// Traverse the value and for each doc reference, make sure it's persisted.
// This is done recursively.
- const traverse = (
- value: Readonly,
- path: PropertyKey[],
- ): any => {
- // If it's a doc, make it a doc link
- if (isDoc(value)) value = { cell: value, path: [] } satisfies CellLink;
-
- // If it's a query result proxy, make it a doc link
- if (isQueryResultForDereferencing(value)) {
- value = getCellLinkOrThrow(value);
- }
-
- // If it's a doc link, convert it to a doc link with an id
- if (isCellLink(value)) {
- dependencies.add(this._ensureIsSynced(value.cell));
- return { ...value, cell: value.cell.toJSON() /* = the id */ };
+ const traverse = (value: Readonly): any => {
+ // If it's a link, add it as dependency and convert it to a sigil link
+ if (isLink(value)) {
+ const link = parseLink(value, doc.asCell());
+ const cell = this.runtime.getCellFromLink(link);
+ dependencies.add(this._ensureIsSynced(cell));
+
+ // Don't convert to sigil link here, as we plan to remove this whole
+ // transformation soon. So return value instead of creating a sigil
+ // link. Roundtripping through JSON converts all Cells and Docs to a
+ // serializable format.
+ if (isQueryResultForDereferencing(value)) {
+ value = getCellLinkOrThrow(value);
+ }
+ return JSON.parse(JSON.stringify(value));
} else if (isRecord(value)) {
if (Array.isArray(value)) {
- return value.map((value, index) => traverse(value, [...path, index]));
+ return value.map(traverse);
} else {
return Object.fromEntries(
- Object.entries(value).map(([key, value]: [PropertyKey, any]) => [
- key,
- traverse(value, [...path, key]),
- ]),
+ Object.entries(value as Record)
+ .map(([key, value]) => [key, traverse(value)]),
);
}
} else return value;
@@ -300,7 +298,7 @@ export class Storage implements IStorage {
// Convert all doc references to ids and remember as dependent docs
const value: StorageValue = {
- value: traverse(doc.get(), []),
+ value: traverse(doc.get()),
source: doc.sourceCell?.entityId,
...(labels !== undefined) ? { labels: labels } : {},
};
@@ -341,9 +339,20 @@ export class Storage implements IStorage {
const traverse = (value: any): any => {
if (typeof value !== "object" || value === null) {
return value;
+ } else if (isLink(value)) {
+ const link = parseLink(value, doc.asCell());
+ const cell = this.runtime.getCellFromLink(link);
+
+ // If the doc is not yet loaded, load it. As it's referenced in
+ // something that came from storage, the id is known in storage and so
+ // we have to wait for it to load. Hence true as second parameter.
+ dependencies.add(this._ensureIsSynced(cell));
+
+ // We don't convert here as we plan to remove this whole transformation
+ // soon. So return value instead of creating a sigil link.
+ return value;
} else if ("cell" in value && "path" in value) {
- // If we see a doc link with just an id, then we replace it with
- // the actual doc:
+ // If we see a doc link an id, then we sync the mentioned doc.
if (
isRecord(value.cell) &&
"/" in value.cell &&
@@ -353,9 +362,6 @@ export class Storage implements IStorage {
const valueSchema = (label !== undefined)
? this.runtime.cfc.schemaWithLub(value.schema ?? {}, label)
: value.schema;
- // If the doc is not yet loaded, load it. As it's referenced in
- // something that came from storage, the id is known in storage and so
- // we have to wait for it to load. Hence true as second parameter.
const dependency = this._ensureIsSyncedById(
doc.space,
value.cell,
@@ -365,11 +371,10 @@ export class Storage implements IStorage {
: undefined,
);
dependencies.add(dependency);
- return { ...value, cell: dependency };
} else {
console.warn("unexpected doc link", value);
- return value;
}
+ return value;
} else if (Array.isArray(value)) {
return value.map(traverse);
} else {
diff --git a/packages/runner/src/storage/cache.ts b/packages/runner/src/storage/cache.ts
index 2b4ff5c1f..4fd987391 100644
--- a/packages/runner/src/storage/cache.ts
+++ b/packages/runner/src/storage/cache.ts
@@ -1,7 +1,11 @@
import { fromString, refer } from "merkle-reference";
import { isBrowser } from "@commontools/utils/env";
import { isObject } from "@commontools/utils/types";
-import { type JSONSchema } from "../builder/types.ts";
+import {
+ type JSONSchema,
+ type JSONValue,
+ type SchemaContext,
+} from "../builder/types.ts";
import { ContextualFlowControl } from "../cfc.ts";
import { deepEqual } from "../path-utils.ts";
import { MapSet } from "../traverse.ts";
@@ -15,15 +19,12 @@ import type {
Entity,
Fact,
FactAddress,
- JSONValue,
MemorySpace,
Protocol,
ProviderCommand,
- ProviderSession,
QueryError,
Result,
Revision,
- SchemaContext,
SchemaPathSelector,
SchemaQueryArgs,
Signer,
diff --git a/packages/runner/src/traverse.ts b/packages/runner/src/traverse.ts
index eb4067b21..b7a6cb347 100644
--- a/packages/runner/src/traverse.ts
+++ b/packages/runner/src/traverse.ts
@@ -4,7 +4,6 @@ import {
type Immutable,
isNumber,
isObject,
- isRecord,
isString,
} from "../../utils/src/types.ts";
import { ContextualFlowControl } from "./cfc.ts";
@@ -14,8 +13,15 @@ import type {
JSONValue,
SchemaContext,
} from "./builder/types.ts";
-import { isAlias } from "./builder/types.ts";
import { deepEqual } from "./path-utils.ts";
+import {
+ isLegacyAlias,
+ isSigilLink,
+ type NormalizedLink,
+ parseLink,
+} from "./link-utils.ts";
+import { fromURI } from "./uri-utils.ts";
+import { type JSONCellLink } from "./sigil-types.ts";
export type SchemaPathSelector = {
path: readonly string[];
@@ -82,7 +88,6 @@ export type PointerCycleTracker = CycleTracker<
Immutable
>;
-type JSONCellLink = { cell: { "/": string }; path: string[] };
export type CellTarget = { path: string[]; cellTarget: string | undefined };
export interface ObjectStorageManager {
@@ -448,7 +453,7 @@ function narrowSchema(
}
/**
- * Extract the path and cellTarget from an Alias or JSONCellLink
+ * Extract the path and cellTarget from an Alias, JSONCellLink, or sigil value
*
* @param value - The JSON object that might contain pointer information
* @returns A CellTarget object containing:
@@ -456,26 +461,16 @@ function narrowSchema(
* - cellTarget: The target cell identifier as a string, or undefined if it refers to the current document
*/
export function getPointerInfo(value: Immutable): CellTarget {
- if (isAlias(value)) {
- if (isObject(value.$alias.cell) && "/" in value.$alias.cell) {
- return {
- path: value.$alias.path.map((p) => p.toString()),
- cellTarget: value.$alias.cell["/"] as string,
- };
- }
- return {
- path: value.$alias.path.map((p) => p.toString()),
- cellTarget: undefined,
- };
- } else if (isJSONCellLink(value)) {
- //console.error("cell: ", obj.cell, "; path: ", obj.path);
- return { path: value.path, cellTarget: value.cell["/"] as string };
- }
- return { path: [], cellTarget: undefined };
+ const link = parseLink(value, {} as NormalizedLink);
+ if (!link) return { path: [], cellTarget: undefined };
+ return {
+ path: link.path ?? [],
+ cellTarget: link.id ? fromURI(link.id) : undefined,
+ };
}
export function isPointer(value: unknown): boolean {
- return (isAlias(value) || isJSONCellLink(value));
+ return (isSigilLink(value) || isJSONCellLink(value) || isLegacyAlias(value));
}
/**
diff --git a/packages/runner/src/type-utils.ts b/packages/runner/src/type-utils.ts
deleted file mode 100644
index 2554efd11..000000000
--- a/packages/runner/src/type-utils.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { isRecord, type Mutable } from "@commontools/utils/types";
-import { isOpaqueRef } from "./builder/types.ts";
-import { isDoc } from "./doc.ts";
-import { type CellLink, isCell, isCellLink } from "./cell.ts";
-import {
- getCellLinkOrThrow,
- isQueryResultForDereferencing,
-} from "./query-result-proxy.ts";
-
-export function maybeUnwrapProxy(value: unknown): unknown {
- return isQueryResultForDereferencing(value)
- ? getCellLinkOrThrow(value)
- : value;
-}
-
-export function arrayEqual(a: PropertyKey[], b: PropertyKey[]): boolean {
- if (a.length !== b.length) return false;
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
- return true;
-}
-
-export function isEqualCellLink(a: CellLink, b: CellLink): boolean {
- return isCellLink(a) && isCellLink(b) && a.cell === b.cell &&
- arrayEqual(a.path, b.path);
-}
-
-export function containsOpaqueRef(value: unknown): boolean {
- if (isOpaqueRef(value)) return true;
- if (isCell(value) || isCellLink(value) || isDoc(value)) return false;
- if (isRecord(value)) {
- return Object.values(value).some(containsOpaqueRef);
- }
- return false;
-}
-
-export function deepCopy(value: T): Mutable {
- if (isQueryResultForDereferencing(value)) {
- return deepCopy(getCellLinkOrThrow(value)) as unknown as Mutable;
- }
- if (isDoc(value) || isCell(value)) return value as Mutable;
- if (isRecord(value)) {
- return Array.isArray(value)
- ? value.map(deepCopy) as unknown as Mutable
- : Object.fromEntries(
- Object.entries(value).map(([key, value]) => [key, deepCopy(value)]),
- ) as unknown as Mutable;
- // Literal value:
- } else return value as Mutable;
-}
diff --git a/packages/runner/src/uri-utils.ts b/packages/runner/src/uri-utils.ts
new file mode 100644
index 000000000..5318041f4
--- /dev/null
+++ b/packages/runner/src/uri-utils.ts
@@ -0,0 +1,44 @@
+import { isRecord } from "@commontools/utils/types";
+import type { URI } from "./sigil-types.ts";
+
+/**
+ * Convert an entity ID to URI format with "of:" prefix
+ */
+export function toURI(value: unknown): URI {
+ if (isRecord(value)) {
+ // Converts EntityId to JSON
+ const parsed = JSON.parse(JSON.stringify(value)) as { "/": string };
+
+ // Handle EntityId object
+ if (typeof parsed["/"] === "string") return `of:${parsed["/"]}`;
+ } else if (typeof value === "string") {
+ // Already has prefix with colon
+ if (value.includes(":")) {
+ // TODO(seefeld): Remove this once we want to support any URI, ideally
+ // once there are no bare ids anymore
+ if (!value.startsWith("of:")) {
+ throw new Error(`Invalid URI: ${value}`);
+ }
+ return value as URI;
+ }
+
+ // Add "of:" prefix
+ return `of:${value}`;
+ }
+
+ throw new Error(`Cannot convert value to URI: ${JSON.stringify(value)}`);
+}
+
+/**
+ * Extract the hash from a URI by removing the "of:" prefix
+ */
+export function fromURI(uri: URI | string): string {
+ if (!uri.includes(":")) {
+ return uri;
+ } else if (uri.startsWith("of:")) {
+ return uri.slice(3);
+ } else {
+ // TODO(seefeld): Remove this once we want to support any URI
+ throw new Error(`Invalid URI: ${uri}`);
+ }
+}
diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts
index 1805da61f..191aa31b9 100644
--- a/packages/runner/test/cell.test.ts
+++ b/packages/runner/test/cell.test.ts
@@ -1,20 +1,24 @@
import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
-import { type DocImpl, isDoc } from "../src/doc.ts";
-import { isCell, isCellLink } from "../src/cell.ts";
+import { isCell } from "../src/cell.ts";
+import { LINK_V1_TAG } from "../src/sigil-types.ts";
import { isQueryResult } from "../src/query-result-proxy.ts";
import { type ReactivityLog } from "../src/scheduler.ts";
import { ID, JSONSchema } from "../src/builder/types.ts";
import { popFrame, pushFrame } from "../src/builder/recipe.ts";
import { Runtime } from "../src/runtime.ts";
import { addCommonIDfromObjectID } from "../src/data-updating.ts";
+import { isCellLink } from "../src/link-utils.ts";
import { Identity } from "@commontools/identity";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
-import { expectCellLinksEqual, normalizeCellLink } from "./test-helpers.ts";
+import { areLinksSame } from "../src/link-utils.ts";
const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
+const signer2 = await Identity.fromPassphrase("test operator 2");
+const space2 = signer2.did();
+
describe("Cell", () => {
let runtime: Runtime;
let storageManager: ReturnType;
@@ -211,7 +215,7 @@ describe("Cell utility functions", () => {
"should identify a cell reference",
);
c.set({ x: 10 });
- const ref = c.key("x").getAsCellLink();
+ const ref = c.key("x").getAsLegacyCellLink();
expect(isCellLink(ref)).toBe(true);
expect(isCellLink({})).toBe(false);
});
@@ -309,7 +313,7 @@ describe("createProxy", () => {
"should handle cell references",
);
c.set({ x: 42 });
- const ref = c.key("x").getAsCellLink();
+ const ref = c.key("x").getAsLegacyCellLink();
const proxy = c.getAsQueryResult();
proxy.y = ref;
expect(proxy.y).toBe(42);
@@ -321,7 +325,7 @@ describe("createProxy", () => {
"should handle infinite loops in cell references",
);
c.set({ x: 42 });
- const ref = c.key("x").getAsCellLink();
+ const ref = c.key("x").getAsLegacyCellLink();
const proxy = c.getAsQueryResult();
proxy.x = ref;
expect(() => proxy.x).toThrow();
@@ -519,10 +523,11 @@ describe("createProxy", () => {
const proxy = c.getAsQueryResult([], log);
proxy.length = 2;
expect(c.get()).toEqual([1, 2]);
- expectCellLinksEqual(log.writes).toEqual([
- c.key("length").getAsCellLink(),
- c.key(2).getAsCellLink(),
- ]);
+ expect(areLinksSame(log.writes[0], c.key("length").getAsLegacyCellLink()))
+ .toBe(true);
+ expect(areLinksSame(log.writes[1], c.key(2).getAsLegacyCellLink())).toBe(
+ true,
+ );
proxy.length = 4;
expect(c.get()).toEqual([1, 2, undefined, undefined]);
expect(log.writes.length).toBe(5);
@@ -649,7 +654,7 @@ describe("asCell", () => {
});
it("should call sink only when the cell changes on the subpath", async () => {
- const c = runtime.getCell<{ a: { b: number, c: number }, d: number }>(
+ const c = runtime.getCell<{ a: { b: number; c: number }; d: number }>(
space,
"should call sink only when the cell changes on the subpath",
);
@@ -1163,8 +1168,8 @@ describe("asCell with schema", () => {
"should handle all types of references in underlying cell: inner",
);
innerCell.set({ value: 42 });
- const cellRef = innerCell.getAsCellLink();
- const aliasRef = { $alias: innerCell.getAsCellLink() };
+ const cellRef = innerCell.getAsLegacyCellLink();
+ const aliasRef = { $alias: innerCell.getAsLegacyCellLink() };
// Create a cell that uses all reference types
const c = runtime.getCell<{
@@ -1223,22 +1228,22 @@ describe("asCell with schema", () => {
"should handle nested references: inner",
);
innerCell.set({ value: 42 });
-
- const ref1 = innerCell.getAsCellLink();
-
+
+ const ref1 = innerCell.getAsLegacyCellLink();
+
const ref2Cell = runtime.getCell<{ ref: any }>(
space,
"should handle nested references: ref2",
);
ref2Cell.set({ ref: ref1 });
- const ref2 = ref2Cell.key("ref").getAsCellLink();
-
+ const ref2 = ref2Cell.key("ref").getAsLegacyCellLink();
+
const ref3Cell = runtime.getCell<{ ref: any }>(
space,
"should handle nested references: ref3",
);
ref3Cell.setRaw({ ref: ref2 });
- const ref3 = ref3Cell.key("ref").getAsCellLink();
+ const ref3 = ref3Cell.key("ref").getAsLegacyCellLink();
// Create a cell that uses the nested reference
const c = runtime.getCell<{
@@ -1275,15 +1280,15 @@ describe("asCell with schema", () => {
expect(value.context.nested.get().value).toBe(42);
// Check that 4 unique documents were read (by entity ID)
- const readEntityIds = new Set(log.reads.map(r => r.cell.entityId));
+ const readEntityIds = new Set(log.reads.map((r) => r.cell.entityId));
expect(readEntityIds.size).toBe(4);
-
+
// Verify each cell was read using equals()
- const readCells = log.reads.map(r => r.cell.asCell());
- expect(readCells.some(cell => cell.equals(c))).toBe(true);
- expect(readCells.some(cell => cell.equals(ref3Cell))).toBe(true);
- expect(readCells.some(cell => cell.equals(ref2Cell))).toBe(true);
- expect(readCells.some(cell => cell.equals(innerCell))).toBe(true);
+ const readCells = log.reads.map((r) => r.cell.asCell());
+ expect(readCells.some((cell) => cell.equals(c))).toBe(true);
+ expect(readCells.some((cell) => cell.equals(ref3Cell))).toBe(true);
+ expect(readCells.some((cell) => cell.equals(ref2Cell))).toBe(true);
+ expect(readCells.some((cell) => cell.equals(innerCell))).toBe(true);
// Changes to the original cell should propagate through the chain
innerCell.send({ value: 100 });
@@ -1608,8 +1613,8 @@ describe("asCell with schema", () => {
// Let's make sure we got a different doc with the different context
expect(testDoc.getRaw()[0].cell).not.toBe(docFromContext1);
- expect(testDoc.getRaw()[0].cell.entityId.toString()).not.toBe(
- docFromContext1.entityId.toString(),
+ expect(JSON.stringify(testDoc.getRaw()[0].cell.entityId)).not.toBe(
+ JSON.stringify(docFromContext1.entityId),
);
expect(testCell.get()).toEqual(initialData);
@@ -1633,7 +1638,6 @@ describe("asCell with schema", () => {
arrayCell.push(d);
arrayCell.push(dCell);
arrayCell.push(d.getAsQueryResult());
- arrayCell.push(d.getAsCellLink());
// helper to normalize CellLinks because different push operations
// may result in CellLinks with different extra properties (ex: space)
@@ -1643,13 +1647,12 @@ describe("asCell with schema", () => {
});
const rawItems = c.getRaw().items;
- const expectedCellLink = normalizeCellLink(d.getAsCellLink());
+ const expectedCellLink = normalizeCellLink(d.getAsLegacyCellLink());
expect(rawItems.map(normalizeCellLink)).toEqual([
expectedCellLink,
expectedCellLink,
expectedCellLink,
- expectedCellLink,
]);
});
@@ -1680,6 +1683,373 @@ describe("asCell with schema", () => {
});
});
+describe("getAsLink method", () => {
+ let runtime: Runtime;
+ let storageManager: ReturnType;
+
+ beforeEach(() => {
+ storageManager = StorageManager.emulate({ as: signer });
+
+ runtime = new Runtime({
+ blobbyServerUrl: import.meta.url,
+ storageManager,
+ });
+ });
+
+ afterEach(async () => {
+ await runtime?.dispose();
+ await storageManager?.close();
+ });
+
+ it("should return new sigil format", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-test",
+ space,
+ );
+ const cell = c.asCell();
+
+ // Get the new sigil format
+ const link = cell.getAsLink();
+
+ // Verify structure
+ expect(link["/"]).toBeDefined();
+ expect(link["/"][LINK_V1_TAG]).toBeDefined();
+ expect(link["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(link["/"][LINK_V1_TAG].path).toBeDefined();
+
+ // Verify id has of: prefix
+ expect(link["/"][LINK_V1_TAG].id).toMatch(/^of:/);
+
+ // Verify path is empty array
+ expect(link["/"][LINK_V1_TAG].path).toEqual([]);
+
+ // Verify space is included if present
+ expect(link["/"][LINK_V1_TAG].space).toBe(space);
+ });
+
+ it("should return correct path for nested cells", () => {
+ const c = runtime.documentMap.getDoc(
+ { nested: { value: 42 } },
+ "getAsLink-nested-test",
+ space,
+ );
+ const nestedCell = c.asCell(["nested", "value"]);
+
+ const link = nestedCell.getAsLink();
+
+ expect(link["/"][LINK_V1_TAG].path).toEqual(["nested", "value"]);
+ });
+
+ it("should return different formats for getAsLink vs toJSON", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-json-test",
+ space,
+ );
+ const cell = c.asCell();
+
+ const link = cell.getAsLink();
+ const json = cell.toJSON();
+
+ // Debug: log actual values
+ console.log("getAsLink result:", JSON.stringify(link, null, 2));
+ console.log("toJSON result:", JSON.stringify(json, null, 2));
+
+ // getAsLink returns new sigil format
+ expect(link).toHaveProperty("/");
+ console.log("getAsLink result /:", JSON.stringify(link["/"], null, 2));
+ expect(link["/"][LINK_V1_TAG]).toBeDefined();
+
+ // toJSON returns old format for backward compatibility
+ expect(json).toHaveProperty("cell");
+ expect(json).toHaveProperty("path");
+ expect((json as any).cell).toHaveProperty("/");
+ });
+
+ it("should create relative links with base parameter - same document", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42, other: "test" },
+ "getAsLink-base-test",
+ space,
+ );
+ const cell = c.asCell(["value"]);
+ const baseCell = c.asCell();
+
+ // Link relative to base cell (same document)
+ const link = cell.getAsLink({ base: baseCell });
+
+ // Should omit id and space since they're the same
+ expect(link["/"][LINK_V1_TAG].id).toBeUndefined();
+ expect(link["/"][LINK_V1_TAG].space).toBeUndefined();
+ expect(link["/"][LINK_V1_TAG].path).toEqual(["value"]);
+ });
+
+ it("should create relative links with base parameter - different document", () => {
+ const c1 = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-base-test-1",
+ space,
+ );
+ const c2 = runtime.documentMap.getDoc(
+ { other: "test" },
+ "getAsLink-base-test-2",
+ space,
+ );
+ const cell = c1.asCell(["value"]);
+ const baseCell = c2.asCell();
+
+ // Link relative to base cell (different document, same space)
+ const link = cell.getAsLink({ base: baseCell });
+
+ // Should include id but not space since space is the same
+ expect(link["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(link["/"][LINK_V1_TAG].id).toMatch(/^of:/);
+ expect(link["/"][LINK_V1_TAG].space).toBeUndefined();
+ expect(link["/"][LINK_V1_TAG].path).toEqual(["value"]);
+ });
+
+ it("should create relative links with base parameter - different space", () => {
+ const c1 = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-base-test-1",
+ space,
+ );
+ const c2 = runtime.documentMap.getDoc(
+ { other: "test" },
+ "getAsLink-base-test-2",
+ space2,
+ );
+ const cell = c1.asCell(["value"]);
+ const baseCell = c2.asCell();
+
+ // Link relative to base cell (different space)
+ const link = cell.getAsLink({ base: baseCell });
+
+ // Should include both id and space since they're different
+ expect(link["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(link["/"][LINK_V1_TAG].id).toMatch(/^of:/);
+ expect(link["/"][LINK_V1_TAG].space).toBe(space);
+ expect(link["/"][LINK_V1_TAG].path).toEqual(["value"]);
+ });
+
+ it("should include schema when includeSchema is true", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-schema-test",
+ space,
+ );
+ const schema = { type: "number", minimum: 0 } as const;
+ const cell = c.asCell(["value"], undefined, schema);
+
+ // Link with schema included
+ const link = cell.getAsLink({ includeSchema: true });
+
+ expect(link["/"][LINK_V1_TAG].schema).toEqual(schema);
+ expect(link["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(link["/"][LINK_V1_TAG].path).toEqual(["value"]);
+ });
+
+ it("should not include schema when includeSchema is false", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-no-schema-test",
+ space,
+ );
+ const schema = { type: "number", minimum: 0 } as const;
+ const cell = c.asCell(["value"], undefined, schema);
+
+ // Link without schema
+ const link = cell.getAsLink({ includeSchema: false });
+
+ expect(link["/"][LINK_V1_TAG].schema).toBeUndefined();
+ });
+
+ it("should not include schema when includeSchema is undefined", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-default-schema-test",
+ space,
+ );
+ const schema = { type: "number", minimum: 0 } as const;
+ const cell = c.asCell(["value"], undefined, schema);
+
+ // Link with default options (no schema)
+ const link = cell.getAsLink();
+
+ expect(link["/"][LINK_V1_TAG].schema).toBeUndefined();
+ });
+
+ it("should handle both base and includeSchema options together", () => {
+ const c1 = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-combined-test-1",
+ space,
+ );
+ const c2 = runtime.documentMap.getDoc(
+ { other: "test" },
+ "getAsLink-combined-test-2",
+ space,
+ );
+ const schema = { type: "number", minimum: 0 } as const;
+ const cell = c1.asCell(["value"], undefined, schema);
+ const baseCell = c2.asCell();
+
+ // Link with both base and schema options
+ const link = cell.getAsLink({ base: baseCell, includeSchema: true });
+
+ // Should include id (different docs) but not space (same space)
+ expect(link["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(link["/"][LINK_V1_TAG].space).toBeUndefined();
+ expect(link["/"][LINK_V1_TAG].path).toEqual(["value"]);
+ expect(link["/"][LINK_V1_TAG].schema).toEqual(schema);
+ });
+
+ it("should handle cell without schema when includeSchema is true", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsLink-no-cell-schema-test",
+ space,
+ );
+ const cell = c.asCell(["value"]); // No schema provided
+
+ // Link with includeSchema but cell has no schema
+ const link = cell.getAsLink({ includeSchema: true });
+
+ expect(link["/"][LINK_V1_TAG].schema).toBeUndefined();
+ });
+});
+
+describe("getAsWriteRedirectLink method", () => {
+ let runtime: Runtime;
+ let storageManager: ReturnType;
+
+ beforeEach(() => {
+ storageManager = StorageManager.emulate({ as: signer });
+
+ runtime = new Runtime({
+ blobbyServerUrl: import.meta.url,
+ storageManager,
+ });
+ });
+
+ afterEach(async () => {
+ await runtime?.dispose();
+ await storageManager?.close();
+ });
+
+ it("should return new sigil alias format", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsWriteRedirectLink-test",
+ space,
+ );
+ const cell = c.asCell();
+
+ // Get the new sigil alias format
+ const alias = cell.getAsWriteRedirectLink();
+
+ // Verify structure
+ expect(alias["/"]).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG]).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG].path).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG].overwrite).toBe("redirect");
+
+ // Verify id has of: prefix
+ expect(alias["/"][LINK_V1_TAG].id).toMatch(/^of:/);
+
+ // Verify path is empty array
+ expect(alias["/"][LINK_V1_TAG].path).toEqual([]);
+
+ // Verify space is included if present
+ expect(alias["/"][LINK_V1_TAG].space).toBe(space);
+ });
+
+ it("should return correct path for nested cells", () => {
+ const c = runtime.documentMap.getDoc(
+ { nested: { value: 42 } },
+ "getAsWriteRedirectLink-nested-test",
+ space,
+ );
+ const nestedCell = c.asCell(["nested", "value"]);
+
+ const alias = nestedCell.getAsWriteRedirectLink();
+
+ expect(alias["/"][LINK_V1_TAG].path).toEqual(["nested", "value"]);
+ });
+
+ it("should omit space when baseSpace matches", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsWriteRedirectLink-baseSpace-test",
+ space,
+ );
+ const cell = c.asCell();
+
+ // Alias with same baseSpace should omit space
+ const alias = cell.getAsWriteRedirectLink({ baseSpace: space });
+
+ expect(alias["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG].space).toBeUndefined();
+ expect(alias["/"][LINK_V1_TAG].path).toEqual([]);
+ });
+
+ it("should include space when baseSpace differs", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsWriteRedirectLink-different-baseSpace-test",
+ space2,
+ );
+ const cell = c.asCell();
+
+ // Alias with different baseSpace should include space
+ const alias = cell.getAsWriteRedirectLink({ baseSpace: space });
+
+ expect(alias["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG].space).toBe(space2);
+ expect(alias["/"][LINK_V1_TAG].path).toEqual([]);
+ });
+
+ it("should include schema when includeSchema is true", () => {
+ const c = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsWriteRedirectLink-schema-test",
+ space,
+ );
+ const schema = { type: "number", minimum: 0 } as const;
+ const cell = c.asCell(["value"], undefined, schema);
+
+ // Alias with includeSchema option
+ const alias = cell.getAsWriteRedirectLink({ includeSchema: true });
+
+ expect(alias["/"][LINK_V1_TAG].schema).toEqual(schema);
+ });
+
+ it("should handle base cell for relative aliases", () => {
+ const c1 = runtime.documentMap.getDoc(
+ { value: 42 },
+ "getAsWriteRedirectLink-base-test-1",
+ space,
+ );
+ const c2 = runtime.documentMap.getDoc(
+ { other: "test" },
+ "getAsWriteRedirectLink-base-test-2",
+ space,
+ );
+ const cell = c1.asCell(["value"]);
+ const baseCell = c2.asCell();
+
+ // Alias relative to base cell (different document, same space)
+ const alias = cell.getAsWriteRedirectLink({ base: baseCell });
+
+ // Should include id (different docs) but not space (same space)
+ expect(alias["/"][LINK_V1_TAG].id).toBeDefined();
+ expect(alias["/"][LINK_V1_TAG].space).toBeUndefined();
+ expect(alias["/"][LINK_V1_TAG].path).toEqual(["value"]);
+ });
+});
+
describe("JSON.stringify bug", () => {
let storageManager: ReturnType;
let runtime: Runtime;
@@ -1708,14 +2078,23 @@ describe("JSON.stringify bug", () => {
space,
"json-test2",
);
- d.setRaw({ internal: { "__#2": normalizeCellLink(c.key("result").getAsCellLink()) } });
+ const cLink = c.key("result").getAsLink({ base: d });
+ d.setRaw({
+ internal: {
+ "__#2": cLink,
+ },
+ });
const e = runtime.getCell<{ internal: { a: any } }>(
space,
"json-test3",
);
+ const dLink = d.key("internal").key("__#2").key("data")
+ .getAsWriteRedirectLink(
+ { base: e },
+ );
e.setRaw({
internal: {
- a: { $alias: normalizeCellLink(d.key("internal").key("__#2").key("data").getAsCellLink()) },
+ a: dLink,
},
});
const proxy = e.getAsQueryResult();
@@ -1724,16 +2103,10 @@ describe("JSON.stringify bug", () => {
expect(JSON.stringify(c.get())).toEqual('{"result":{"data":1}}');
expect(JSON.stringify(d.getRaw())).toEqual(
- `{"internal":{"__#2":{"cell":${
- JSON.stringify(c.entityId)
- },"path":["result"]}}}`,
+ `{"internal":{"__#2":${JSON.stringify(cLink)}}}`,
);
expect(JSON.stringify(e.getRaw())).toEqual(
- `{"internal":{"a":{"$alias":{"cell":${
- JSON.stringify(
- d.entityId,
- )
- },"path":["internal","__#2","data"]}}}}`,
+ `{"internal":{"a":${JSON.stringify(dLink)}}}`,
);
});
});
diff --git a/packages/runner/test/data-updating.test.ts b/packages/runner/test/data-updating.test.ts
index cc0920689..a02658643 100644
--- a/packages/runner/test/data-updating.test.ts
+++ b/packages/runner/test/data-updating.test.ts
@@ -8,13 +8,18 @@ import {
normalizeAndDiff,
setNestedValue,
} from "../src/data-updating.ts";
-import { isEqualCellLink } from "../src/type-utils.ts";
import { Runtime } from "../src/runtime.ts";
-import { CellLink, isCellLink } from "../src/cell.ts";
+import {
+ areLinksSame,
+ isAnyCellLink,
+ isCellLink,
+ isLegacyCellLink,
+} from "../src/link-utils.ts";
+import type { LegacyCellLink } from "../src/sigil-types.ts";
+import { arrayEqual } from "../src/path-utils.ts";
import { type ReactivityLog } from "../src/scheduler.ts";
import { Identity } from "@commontools/identity";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
-import { expectCellLinksEqual } from "./test-helpers.ts";
const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
@@ -51,7 +56,9 @@ describe("data-updating", () => {
});
it("should delete no longer used fields when setting a nested value", () => {
- const testCell = runtime.getCell<{ a: number; b: { c: number; d?: number } }>(
+ const testCell = runtime.getCell<
+ { a: number; b: { c: number; d?: number } }
+ >(
space,
"should delete no longer used fields 1",
);
@@ -68,7 +75,10 @@ describe("data-updating", () => {
);
testCell.set({ a: 1, b: { c: 2 } });
const log: ReactivityLog = { reads: [], writes: [] };
- const success = setNestedValue(testCell.getDoc(), [], { a: 1, b: { c: 2 } }, log);
+ const success = setNestedValue(testCell.getDoc(), [], {
+ a: 1,
+ b: { c: 2 },
+ }, log);
expect(success).toBe(true); // No changes is still a success
expect(testCell.get()).toEqual({ a: 1, b: { c: 2 } });
expect(log.writes).toEqual([]);
@@ -81,7 +91,10 @@ describe("data-updating", () => {
);
testCell.set({ a: 1, b: { c: 2 } });
const log: ReactivityLog = { reads: [], writes: [] };
- const success = setNestedValue(testCell.getDoc(), [], { a: 1, b: { c: 3 } }, log);
+ const success = setNestedValue(testCell.getDoc(), [], {
+ a: 1,
+ b: { c: 3 },
+ }, log);
expect(success).toBe(true);
expect(testCell.get()).toEqual({ a: 1, b: { c: 3 } });
expect(log.writes.length).toEqual(1);
@@ -96,7 +109,10 @@ describe("data-updating", () => {
testCell.set({ a: 1, b: { c: 2 } });
testCell.getDoc().freeze("test");
const log: ReactivityLog = { reads: [], writes: [] };
- const success = setNestedValue(testCell.getDoc(), [], { a: 1, b: { c: 3 } }, log);
+ const success = setNestedValue(testCell.getDoc(), [], {
+ a: 1,
+ b: { c: 3 },
+ }, log);
expect(success).toBe(false);
});
@@ -143,7 +159,7 @@ describe("data-updating", () => {
"normalizeAndDiff simple value changes",
);
testCell.set({ value: 42 });
- const current = testCell.key("value").getAsCellLink();
+ const current = testCell.key("value").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, 100);
expect(changes.length).toBe(1);
@@ -157,29 +173,27 @@ describe("data-updating", () => {
"normalizeAndDiff object property changes",
);
testCell.set({ user: { name: "John", age: 30 } });
- const current = testCell.key("user").getAsCellLink();
+ const current = testCell.key("user").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, { name: "Jane", age: 30 });
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("user").key("name").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("user").key("name"))).toBe(true);
expect(changes[0].value).toBe("Jane");
});
it("should detect added object properties", () => {
- const testCell = runtime.getCell<{ user: { name: string; age?: number } }>(
+ const testCell = runtime.getCell<
+ { user: { name: string; age?: number } }
+ >(
space,
"normalizeAndDiff added object properties",
);
testCell.set({ user: { name: "John" } });
- const current = testCell.key("user").getAsCellLink();
+ const current = testCell.key("user").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, { name: "John", age: 30 });
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("user").key("age").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("user").key("age"))).toBe(true);
expect(changes[0].value).toBe(30);
});
@@ -189,13 +203,11 @@ describe("data-updating", () => {
"normalizeAndDiff removed object properties",
);
testCell.set({ user: { name: "John", age: 30 } });
- const current = testCell.key("user").getAsCellLink();
+ const current = testCell.key("user").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, { name: "John" });
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("user").key("age").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("user").key("age"))).toBe(true);
expect(changes[0].value).toBe(undefined);
});
@@ -205,13 +217,11 @@ describe("data-updating", () => {
"normalizeAndDiff array length changes",
);
testCell.set({ items: [1, 2, 3] });
- const current = testCell.key("items").getAsCellLink();
+ const current = testCell.key("items").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, [1, 2]);
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("items").key("length").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("items").key("length"))).toBe(true);
expect(changes[0].value).toBe(2);
});
@@ -221,13 +231,11 @@ describe("data-updating", () => {
"normalizeAndDiff array element changes",
);
testCell.set({ items: [1, 2, 3] });
- const current = testCell.key("items").getAsCellLink();
+ const current = testCell.key("items").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, [1, 5, 3]);
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("items").key(1).getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("items").key(1))).toBe(true);
expect(changes[0].value).toBe(5);
});
@@ -243,14 +251,12 @@ describe("data-updating", () => {
value: 42,
alias: { $alias: { path: ["value"] } },
});
- const current = testCell.key("alias").getAsCellLink();
+ const current = testCell.key("alias").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, 100);
// Should follow alias to value and change it there
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("value").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("value"))).toBe(true);
expect(changes[0].value).toBe(100);
});
@@ -268,14 +274,12 @@ describe("data-updating", () => {
value2: 200,
alias: { $alias: { path: ["value"] } },
});
- const current = testCell.key("alias").getAsCellLink();
+ const current = testCell.key("alias").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, 100);
// Should follow alias to value and change it there
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("value").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("value"))).toBe(true);
expect(changes[0].value).toBe(100);
applyChangeSet(changes);
@@ -287,17 +291,13 @@ describe("data-updating", () => {
applyChangeSet(changes2);
expect(changes2.length).toBe(1);
- expectCellLinksEqual(changes2[0].location).toEqual(
- testCell.key("alias").getAsCellLink()
- );
+ expect(areLinksSame(changes2[0].location, testCell.key("alias"))).toBe(true);
expect(changes2[0].value).toEqual({ $alias: { path: ["value2"] } });
const changes3 = normalizeAndDiff(current, 300);
expect(changes3.length).toBe(1);
- expectCellLinksEqual(changes3[0].location).toEqual(
- testCell.key("value2").getAsCellLink()
- );
+ expect(areLinksSame(changes3[0].location, testCell.key("value2"))).toBe(true);
expect(changes3[0].value).toBe(300);
});
@@ -329,7 +329,7 @@ describe("data-updating", () => {
},
},
});
- const current = testCell.key("user").key("profile").getAsCellLink();
+ const current = testCell.key("user").key("profile").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, {
details: {
address: {
@@ -340,9 +340,9 @@ describe("data-updating", () => {
});
expect(changes.length).toBe(1);
- expectCellLinksEqual(changes[0].location).toEqual(
- testCell.key("user").key("profile").key("details").key("address").key("city").getAsCellLink()
- );
+ expect(areLinksSame(changes[0].location, testCell.key("user").key("profile").key("details").key("address").key(
+ "city",
+ ))).toBe(true);
expect(changes[0].value).toBe("Boston");
});
@@ -352,7 +352,7 @@ describe("data-updating", () => {
"should handle ID-based entity objects",
);
testCell.set({ items: [] });
- const current = testCell.key("items").key(0).getAsCellLink();
+ const current = testCell.key("items").key(0).getAsLegacyCellLink();
const newValue = { [ID]: "item1", name: "First Item" };
const changes = normalizeAndDiff(
@@ -378,7 +378,7 @@ describe("data-updating", () => {
"should update the same document with ID-based entity objects",
);
testCell.set({ items: [] });
- const current = testCell.key("items").key(0).getAsCellLink();
+ const current = testCell.key("items").key(0).getAsLegacyCellLink();
const newValue = { [ID]: "item1", name: "First Item" };
diffAndUpdate(
@@ -397,18 +397,22 @@ describe("data-updating", () => {
],
};
diffAndUpdate(
- testCell.getAsCellLink(),
+ testCell.getAsLegacyCellLink(),
newValue2,
undefined,
"should update the same document with ID-based entity objects",
);
-
- expect(isCellLink(testCell.getRaw().items[0])).toBe(true);
- expect(isCellLink(testCell.getRaw().items[1])).toBe(true);
+
+ expect(isAnyCellLink(testCell.getRaw().items[0])).toBe(true);
+ expect(isAnyCellLink(testCell.getRaw().items[1])).toBe(true);
expect(testCell.getRaw().items[0].cell).not.toBe(newDoc);
- expect(testCell.getRaw().items[0].cell.get().name).toEqual("Inserted before");
+ expect(testCell.getRaw().items[0].cell.get().name).toEqual(
+ "Inserted before",
+ );
expect(testCell.getRaw().items[1].cell).toBe(newDoc);
- expect(testCell.getRaw().items[1].cell.get().name).toEqual("Second Value");
+ expect(testCell.getRaw().items[1].cell.get().name).toEqual(
+ "Second Value",
+ );
});
it("should update the same document with numeric ID-based entity objects", () => {
@@ -417,7 +421,7 @@ describe("data-updating", () => {
"should update the same document with ID-based entity objects",
);
testCell.set({ items: [] });
- const current = testCell.key("items").key(0).getAsCellLink();
+ const current = testCell.key("items").key(0).getAsLegacyCellLink();
const newValue = { [ID]: 1, name: "First Item" };
diffAndUpdate(
@@ -436,16 +440,20 @@ describe("data-updating", () => {
],
};
diffAndUpdate(
- testCell.getAsCellLink(),
+ testCell.getAsLegacyCellLink(),
newValue2,
undefined,
"should update the same document with ID-based entity objects",
);
expect(testCell.getRaw().items[0].cell).not.toBe(newDoc);
- expect(testCell.getRaw().items[0].cell.get().name).toEqual("Inserted before");
+ expect(testCell.getRaw().items[0].cell.get().name).toEqual(
+ "Inserted before",
+ );
expect(testCell.getRaw().items[1].cell).toBe(newDoc);
- expect(testCell.getRaw().items[1].cell.get().name).toEqual("Second Value");
+ expect(testCell.getRaw().items[1].cell.get().name).toEqual(
+ "Second Value",
+ );
});
it("should handle ID_FIELD redirects and reuse existing documents", () => {
@@ -459,7 +467,7 @@ describe("data-updating", () => {
const data = { id: "item1", name: "First Item" };
addCommonIDfromObjectID(data);
diffAndUpdate(
- testCell.key("items").key(0).getAsCellLink(),
+ testCell.key("items").key(0).getAsLegacyCellLink(),
data,
undefined,
"test ID_FIELD redirects",
@@ -477,7 +485,7 @@ describe("data-updating", () => {
addCommonIDfromObjectID(newValue);
diffAndUpdate(
- testCell.getAsCellLink(),
+ testCell.getAsLegacyCellLink(),
newValue,
undefined,
"test ID_FIELD redirects",
@@ -487,7 +495,9 @@ describe("data-updating", () => {
expect(isCellLink(testCell.getRaw().items[0])).toBe(true);
expect(isCellLink(testCell.getRaw().items[1])).toBe(true);
expect(testCell.getRaw().items[1].cell).toBe(initialDoc);
- expect(testCell.getRaw().items[1].cell.get().name).toEqual("Updated Item");
+ expect(testCell.getRaw().items[1].cell.get().name).toEqual(
+ "Updated Item",
+ );
expect(testCell.getRaw().items[0].cell.get().name).toEqual("New Item");
});
@@ -497,7 +507,7 @@ describe("data-updating", () => {
"it should treat different properties as different ID namespaces",
);
testCell.set(undefined);
- const current = testCell.getAsCellLink();
+ const current = testCell.getAsLegacyCellLink();
const newValue = {
a: { [ID]: "item1", name: "First Item" },
@@ -523,7 +533,7 @@ describe("data-updating", () => {
"normalizeAndDiff no changes",
);
testCell.set({ value: 42 });
- const current = testCell.key("value").getAsCellLink();
+ const current = testCell.key("value").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, 42);
expect(changes.length).toBe(0);
@@ -541,12 +551,12 @@ describe("data-updating", () => {
);
cellB.set({ value: { name: "Original" } });
- const current = cellB.key("value").getAsCellLink();
+ const current = cellB.key("value").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, cellA.getDoc());
expect(changes.length).toBe(1);
expect(changes[0].location).toEqual(current);
- expectCellLinksEqual(changes[0].value).toEqual(cellA.getAsCellLink());
+ expect(areLinksSame(changes[0].value, cellA)).toBe(true);
});
it("should handle doc and cell references that don't change", () => {
@@ -561,12 +571,12 @@ describe("data-updating", () => {
);
cellB.set({ value: { name: "Original" } });
- const current = cellB.key("value").getAsCellLink();
+ const current = cellB.key("value").getAsLegacyCellLink();
const changes = normalizeAndDiff(current, cellA.getDoc());
expect(changes.length).toBe(1);
expect(changes[0].location).toEqual(current);
- expectCellLinksEqual(changes[0].value).toEqual(cellA.getAsCellLink());
+ expect(areLinksSame(changes[0].value, cellA)).toBe(true);
applyChangeSet(changes);
@@ -584,37 +594,41 @@ describe("data-updating", () => {
});
it("should reuse items", () => {
+ function isEqualCellLink(a: LegacyCellLink, b: LegacyCellLink): boolean {
+ return isLegacyCellLink(a) && isLegacyCellLink(b) &&
+ a.cell === b.cell &&
+ arrayEqual(a.path, b.path);
+ }
+
const itemCell = runtime.getCell<{ id: string; name: string }>(
space,
"addCommonIDfromObjectID reuse items",
);
itemCell.set({ id: "item1", name: "Original Item" });
-
+
const testCell = runtime.getCell<{ items: any[] }>(
space,
"addCommonIDfromObjectID arrays",
);
- testCell.setRaw({ items: [itemCell.getAsCellLink()] });
+ testCell.setRaw({ items: [itemCell.getAsLegacyCellLink()] });
const data = {
items: [{ id: "item1", name: "New Item" }, itemCell],
};
addCommonIDfromObjectID(data);
diffAndUpdate(
- testCell.getAsCellLink(),
+ testCell.getAsLegacyCellLink(),
data,
undefined,
"addCommonIDfromObjectID reuse items",
);
const result = testCell.getRaw();
- expect(isCellLink(result.items[0])).toBe(true);
- expect(isCellLink(result.items[1])).toBe(true);
+ expect(isAnyCellLink(result.items[0])).toBe(true);
+ expect(isAnyCellLink(result.items[1])).toBe(true);
expect(isEqualCellLink(result.items[0] as any, result.items[1] as any))
- .toBe(
- true,
- );
+ .toBe(true);
expect(result.items[1].cell.get().name).toBe("New Item");
});
});
-});
+});
\ No newline at end of file
diff --git a/packages/runner/test/doc-map.test.ts b/packages/runner/test/doc-map.test.ts
index 8b34d30a0..cad93f0a0 100644
--- a/packages/runner/test/doc-map.test.ts
+++ b/packages/runner/test/doc-map.test.ts
@@ -57,10 +57,10 @@ describe("cell-map", () => {
const cell = runtime.getCell(space, "test-cell");
cell.set({});
const id = getEntityId(cell);
-
+
expect(getEntityId(cell)).toEqual(id);
expect(getEntityId(cell.getAsQueryResult())).toEqual(id);
- expect(getEntityId(cell.getAsCellLink())).toEqual(id);
+ expect(getEntityId(cell.getAsLegacyCellLink())).toEqual(id);
});
it("should return a different entity ID for reference with paths", () => {
@@ -74,13 +74,13 @@ describe("cell-map", () => {
expect(getEntityId(c.getAsQueryResult())).toEqual(id);
expect(getEntityId(c.getAsQueryResult(["foo"]))).not.toEqual(id);
expect(getEntityId(c.key("foo"))).not.toEqual(id);
- expect(getEntityId(c.key("foo").getAsCellLink())).not.toEqual(id);
+ expect(getEntityId(c.key("foo").getAsLegacyCellLink())).not.toEqual(id);
expect(getEntityId(c.getAsQueryResult(["foo"]))).toEqual(
getEntityId(c.key("foo")),
);
expect(getEntityId(c.getAsQueryResult(["foo"]))).toEqual(
- getEntityId(c.key("foo").getAsCellLink()),
+ getEntityId(c.key("foo").getAsLegacyCellLink()),
);
});
});
@@ -99,7 +99,7 @@ describe("cell-map", () => {
// Verify we got the same cell
expect(retrievedCell.entityId).toEqual(c.entityId);
expect(retrievedCell.get()).toEqual({ value: 42 });
-
+
// Also verify the cells are equal
expect(retrievedCell.equals(c)).toBe(true);
});
@@ -117,10 +117,10 @@ describe("cell-map", () => {
it("should serialize the entity ID", () => {
const c = runtime.getCell<{ value: number }>(space, "test-json");
c.set({ value: 42 });
-
+
const expected = JSON.stringify({
cell: c.entityId,
- path: []
+ path: [],
});
expect(JSON.stringify(c)).toEqual(expected);
});
diff --git a/packages/runner/test/json-utils.test.ts b/packages/runner/test/json-utils.test.ts
index 2117d18de..d3e0057e8 100644
--- a/packages/runner/test/json-utils.test.ts
+++ b/packages/runner/test/json-utils.test.ts
@@ -10,13 +10,12 @@ const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
describe("createJsonSchema", () => {
- let storageManager: ReturnType;
let runtime: Runtime;
+ let storageManager: ReturnType;
beforeEach(() => {
storageManager = StorageManager.emulate({ as: signer });
- // Create runtime with the shared storage provider
- // We need to bypass the URL-based configuration for this test
+
runtime = new Runtime({
blobbyServerUrl: import.meta.url,
storageManager,
@@ -50,16 +49,28 @@ describe("createJsonSchema", () => {
name: "item2",
value: 42,
}]);
- expect(mixedArraySchema).toEqual({
- type: "array",
- items: {
- type: "object",
- properties: {
- name: { type: "string" },
- value: { type: "integer" },
+ expect(mixedArraySchema).toEqual(
+ {
+ type: "array",
+ items: {
+ anyOf: [
+ {
+ type: "object",
+ properties: {
+ name: { type: "string" },
+ },
+ },
+ {
+ type: "object",
+ properties: {
+ name: { type: "string" },
+ value: { type: "integer" },
+ },
+ },
+ ],
},
- },
- });
+ } satisfies JSONSchema,
+ );
});
it("should create schema for objects", () => {
@@ -162,7 +173,7 @@ describe("createJsonSchema", () => {
{ type: "string", format: "email" },
);
- const schema = createJsonSchema(cellWithSchema);
+ const schema = createJsonSchema(cellWithSchema, false, runtime);
expect(schema).toEqual({ type: "string", format: "email" });
});
@@ -176,7 +187,7 @@ describe("createJsonSchema", () => {
},
);
- const schema = createJsonSchema(cellWithoutSchema);
+ const schema = createJsonSchema(cellWithoutSchema, false, runtime);
expect(schema).toEqual({
type: "object",
properties: {
@@ -188,9 +199,12 @@ describe("createJsonSchema", () => {
});
it("should handle array cell without schema", () => {
- const arrayCell = runtime.getImmutableCell(space, [1, 2, 3, 4]);
+ const arrayCell = runtime.getImmutableCell(
+ space,
+ [1, 2, 3, 4],
+ );
- const schema = createJsonSchema(arrayCell);
+ const schema = createJsonSchema(arrayCell, false, runtime);
expect(schema).toEqual({
type: "array",
@@ -201,7 +215,10 @@ describe("createJsonSchema", () => {
});
it("should handle nested cells with and without schema", () => {
- const userCell = runtime.getImmutableCell(space, { id: 1, name: "Alice" });
+ const userCell = runtime.getImmutableCell(
+ space,
+ { id: 1, name: "Alice" },
+ );
const prefsSchema = {
type: "object",
@@ -222,7 +239,7 @@ describe("createJsonSchema", () => {
preferences: prefsCell,
};
- const schema = createJsonSchema(nestedObject);
+ const schema = createJsonSchema(nestedObject, false, runtime);
expect(schema).toEqual({
type: "object",
properties: {
diff --git a/packages/runner/test/link-resolution.test.ts b/packages/runner/test/link-resolution.test.ts
index 5d6ca57ac..6d2679bff 100644
--- a/packages/runner/test/link-resolution.test.ts
+++ b/packages/runner/test/link-resolution.test.ts
@@ -1,10 +1,10 @@
import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
-import { followAliases } from "../src/link-resolution.ts";
+import { followWriteRedirects } from "../src/link-resolution.ts";
import { Runtime } from "../src/runtime.ts";
import { Identity } from "@commontools/identity";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
-import { expectCellLinksEqual } from "./test-helpers.ts";
+import { areLinksSame } from "../src/link-utils.ts";
const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
@@ -28,7 +28,7 @@ describe("link-resolution", () => {
await storageManager?.close();
});
- describe("followAliases", () => {
+ describe("followWriteRedirects", () => {
it("should follow a simple alias", () => {
const testCell = runtime.getCell<{ value: number }>(
space,
@@ -36,7 +36,7 @@ describe("link-resolution", () => {
);
testCell.set({ value: 42 });
const binding = { $alias: { path: ["value"] } };
- const result = followAliases(binding, testCell);
+ const result = followWriteRedirects(binding, testCell);
expect(result.cell.getAtPath(result.path)).toBe(42);
});
@@ -51,11 +51,11 @@ describe("link-resolution", () => {
"should follow nested aliases 2",
);
outerCell.setRaw({
- outer: { $alias: innerCell.key("inner").getAsCellLink() },
+ outer: { $alias: innerCell.key("inner").getAsLegacyCellLink() },
});
const binding = { $alias: { path: ["outer"] } };
- const result = followAliases(binding, outerCell);
- expectCellLinksEqual(result).toEqual(innerCell.key("inner").getAsCellLink());
+ const result = followWriteRedirects(binding, outerCell);
+ expect(areLinksSame(result, innerCell.key("inner"))).toBe(true);
expect(result.cell.getAtPath(result.path)).toBe(10);
});
@@ -70,10 +70,16 @@ describe("link-resolution", () => {
"should throw an error on circular aliases 2",
);
cellB.set({});
- cellA.setRaw({ alias: { $alias: cellB.key("alias").getAsCellLink() } });
- cellB.setRaw({ alias: { $alias: cellA.key("alias").getAsCellLink() } });
+ cellA.setRaw({
+ alias: { $alias: cellB.key("alias").getAsLegacyCellLink() },
+ });
+ cellB.setRaw({
+ alias: { $alias: cellA.key("alias").getAsLegacyCellLink() },
+ });
const binding = { $alias: { path: ["alias"] } };
- expect(() => followAliases(binding, cellA)).toThrow("cycle detected");
+ expect(() => followWriteRedirects(binding, cellA)).toThrow(
+ "cycle detected",
+ );
});
it("should allow aliases in aliased paths", () => {
@@ -85,8 +91,10 @@ describe("link-resolution", () => {
a: { a: { $alias: { path: ["a", "b"] } }, b: { c: 1 } },
});
const binding = { $alias: { path: ["a", "a", "c"] } };
- const result = followAliases(binding, testCell);
- expectCellLinksEqual(result).toEqual(testCell.key("a").key("b").key("c").getAsCellLink());
+ const result = followWriteRedirects(binding, testCell);
+ expect(areLinksSame(result, testCell.key("a").key("b").key("c"))).toBe(
+ true,
+ );
expect(result.cell.getAtPath(result.path)).toBe(1);
});
});
diff --git a/packages/runner/test/link-utils.test.ts b/packages/runner/test/link-utils.test.ts
new file mode 100644
index 000000000..efb5decb3
--- /dev/null
+++ b/packages/runner/test/link-utils.test.ts
@@ -0,0 +1,587 @@
+import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
+import { expect } from "@std/expect";
+import {
+ areLinksSame,
+ createSigilLinkFromParsedLink,
+ isLegacyAlias,
+ isLink,
+ isSigilValue,
+ isWriteRedirectLink,
+ type NormalizedLink,
+ parseLink,
+ parseLinkOrThrow,
+ parseToLegacyCellLink,
+} from "../src/link-utils.ts";
+import { LINK_V1_TAG } from "../src/sigil-types.ts";
+import { Runtime } from "../src/runtime.ts";
+import { Identity } from "@commontools/identity";
+import { StorageManager } from "@commontools/runner/storage/cache.deno";
+
+const signer = await Identity.fromPassphrase("test operator");
+const space = signer.did();
+
+describe("link-utils", () => {
+ let storageManager: ReturnType;
+ let runtime: Runtime;
+
+ beforeEach(() => {
+ storageManager = StorageManager.emulate({ as: signer });
+ runtime = new Runtime({
+ blobbyServerUrl: import.meta.url,
+ storageManager,
+ });
+ });
+
+ afterEach(async () => {
+ await runtime?.dispose();
+ await storageManager?.close();
+ });
+
+ describe("isSigilValue", () => {
+ it("should identify valid sigil values", () => {
+ const validSigil = { "/": { someKey: "someValue" } };
+ expect(isSigilValue(validSigil)).toBe(true);
+ });
+
+ it("should identify sigil values with empty record", () => {
+ const emptySigil = { "/": {} };
+ expect(isSigilValue(emptySigil)).toBe(true);
+ });
+
+ it("should identify sigil values with nested objects", () => {
+ const nestedSigil = { "/": { nested: { deep: "value" } } };
+ expect(isSigilValue(nestedSigil)).toBe(true);
+ });
+
+ it("should not identify objects with / and other properties", () => {
+ const invalidSigil1 = { "/": { key: "value" }, otherProp: "value" };
+ expect(isSigilValue(invalidSigil1)).toBe(false);
+
+ const invalidSigil2 = { "/": { key: "value" }, extra: 123 };
+ expect(isSigilValue(invalidSigil2)).toBe(false);
+
+ const invalidSigil3 = {
+ "/": { key: "value" },
+ nested: { prop: "value" },
+ };
+ expect(isSigilValue(invalidSigil3)).toBe(false);
+ });
+
+ it("should not identify objects without / property", () => {
+ const noSlash = { otherProp: "value" };
+ expect(isSigilValue(noSlash)).toBe(false);
+
+ const emptyObject = {};
+ expect(isSigilValue(emptyObject)).toBe(false);
+ });
+
+ it("should not identify objects where / is not a record", () => {
+ const stringSlash = { "/": "not a record" };
+ expect(isSigilValue(stringSlash)).toBe(false);
+
+ const numberSlash = { "/": 123 };
+ expect(isSigilValue(numberSlash)).toBe(false);
+
+ const arraySlash = { "/": ["not", "a", "record"] };
+ expect(isSigilValue(arraySlash)).toBe(false);
+
+ const nullSlash = { "/": null };
+ expect(isSigilValue(nullSlash)).toBe(false);
+
+ const undefinedSlash = { "/": undefined };
+ expect(isSigilValue(undefinedSlash)).toBe(false);
+ });
+
+ it("should not identify non-objects", () => {
+ expect(isSigilValue("string")).toBe(false);
+ expect(isSigilValue(123)).toBe(false);
+ expect(isSigilValue(true)).toBe(false);
+ expect(isSigilValue(null)).toBe(false);
+ expect(isSigilValue(undefined)).toBe(false);
+ expect(isSigilValue([])).toBe(false);
+ });
+
+ it("should handle edge cases with multiple properties", () => {
+ const multipleProps = {
+ "/": { key: "value" },
+ prop1: "value1",
+ prop2: "value2",
+ };
+ expect(isSigilValue(multipleProps)).toBe(false);
+
+ const onlySlashButMultiple = { "/": { key: "value" }, "/extra": "value" };
+ expect(isSigilValue(onlySlashButMultiple)).toBe(false);
+ });
+ });
+
+ describe("isLink", () => {
+ it("should identify query results as links", () => {
+ const cell = runtime.getCell(space, "test");
+ // Has to be an object, otherwise asQueryResult() returns a literal
+ cell.set({ value: 42 });
+ const queryResult = cell.getAsQueryResult();
+ expect(isLink(queryResult)).toBe(true);
+ });
+
+ it("should identify cell links as links", () => {
+ const cell = runtime.getCell(space, "test");
+ const cellLink = cell.getAsLegacyCellLink();
+ expect(isLink(cellLink)).toBe(true);
+ });
+
+ it("should identify cells as links", () => {
+ const cell = runtime.getCell(space, "test");
+ expect(isLink(cell)).toBe(true);
+ });
+
+ it("should identify docs as links", () => {
+ const cell = runtime.getCell(space, "test");
+ const doc = cell.getDoc();
+ expect(isLink(doc)).toBe(true);
+ });
+
+ it("should identify EntityId format as links", () => {
+ expect(isLink({ "/": "of:test" })).toBe(true);
+ });
+
+ it("should not identify non-links as links", () => {
+ expect(isLink("string")).toBe(false);
+ expect(isLink(123)).toBe(false);
+ expect(isLink({ notLink: "value" })).toBe(false);
+ expect(isLink(null)).toBe(false);
+ expect(isLink(undefined)).toBe(false);
+ });
+ });
+
+ describe("isWriteRedirectLink", () => {
+ it("should identify legacy aliases as write redirect links", () => {
+ const legacyAlias = { $alias: { path: ["test"] } };
+ expect(isWriteRedirectLink(legacyAlias)).toBe(true);
+ });
+
+ it("should identify sigil links with overwrite redirect as write redirect links", () => {
+ const sigilLink = {
+ "/": {
+ [LINK_V1_TAG]: { id: "test", overwrite: "redirect" },
+ },
+ };
+ expect(isWriteRedirectLink(sigilLink)).toBe(true);
+ });
+
+ it("should not identify regular sigil links as write redirect links", () => {
+ const sigilLink = {
+ "/": {
+ [LINK_V1_TAG]: { id: "test" },
+ },
+ };
+ expect(isWriteRedirectLink(sigilLink)).toBe(false);
+ });
+
+ it("should not identify non-links as write redirect links", () => {
+ expect(isWriteRedirectLink("string")).toBe(false);
+ expect(isWriteRedirectLink({ notLink: "value" })).toBe(false);
+ });
+ });
+
+ describe("isLegacyAlias", () => {
+ it("should identify legacy aliases", () => {
+ const legacyAlias = { $alias: { path: ["test"] } };
+ expect(isLegacyAlias(legacyAlias)).toBe(true);
+ });
+
+ it("should identify legacy aliases with cell", () => {
+ const cell = runtime.getCell(space, "test");
+ const legacyAlias = { $alias: { cell: cell.getDoc(), path: ["test"] } };
+ expect(isLegacyAlias(legacyAlias)).toBe(true);
+ });
+
+ it("should not identify non-legacy aliases", () => {
+ expect(isLegacyAlias({ notAlias: "value" })).toBe(false);
+ expect(isLegacyAlias({ $alias: "not object" })).toBe(false);
+ expect(isLegacyAlias({ $alias: { notPath: "value" } })).toBe(false);
+ });
+ });
+
+ describe("parseLink", () => {
+ it("should parse cells to normalized links", () => {
+ const cell = runtime.getCell(space, "test");
+ cell.set({ value: 42 });
+ const result = parseLink(cell);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: [],
+ space: space,
+ schema: undefined,
+ rootSchema: undefined,
+ });
+ });
+
+ it("should parse cells with paths to normalized links", () => {
+ const cell = runtime.getCell(space, "test");
+ cell.set({ nested: { value: 42 } });
+ const nestedCell = cell.key("nested");
+ const result = parseLink(nestedCell);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: ["nested"],
+ space: space,
+ schema: undefined,
+ rootSchema: undefined,
+ });
+ });
+
+ it("should parse docs to normalized links", () => {
+ const cell = runtime.getCell(space, "test");
+ const doc = cell.getDoc();
+ const result = parseLink(doc);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: [],
+ space: space,
+ });
+ });
+
+ it("should parse sigil links to normalized links", () => {
+ const sigilLink = {
+ "/": {
+ [LINK_V1_TAG]: {
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ },
+ },
+ };
+ const result = parseLink(sigilLink);
+
+ expect(result).toEqual({
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ });
+ });
+
+ it("should parse sigil links to normalized links", () => {
+ const sigilLink = {
+ "/": {
+ [LINK_V1_TAG]: {
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ overwrite: "this",
+ },
+ },
+ };
+ const result = parseLink(sigilLink);
+
+ expect(result).toEqual({
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ });
+ });
+
+ it("should parse sigil links with overwrite this to normalized links", () => {
+ const sigilLink = {
+ "/": {
+ [LINK_V1_TAG]: {
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ overwrite: "redirect",
+ },
+ },
+ };
+ const result = parseLink(sigilLink);
+
+ expect(result).toEqual({
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ overwrite: "redirect",
+ });
+ });
+
+ it("should parse sigil links with relative references", () => {
+ const baseCell = runtime.getCell(space, "base");
+ const sigilLink = {
+ "/": {
+ [LINK_V1_TAG]: {
+ path: ["nested", "value"],
+ space: space,
+ },
+ },
+ };
+ const result = parseLink(sigilLink, baseCell);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: ["nested", "value"],
+ space: space,
+ schema: undefined,
+ rootSchema: undefined,
+ });
+ });
+
+ it("should parse cell links to normalized links", () => {
+ const cell = runtime.getCell(space, "test");
+ const cellLink = cell.getAsLegacyCellLink();
+ const result = parseLink(cellLink);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: [],
+ space: space,
+ schema: undefined,
+ rootSchema: undefined,
+ });
+ });
+
+ it("should parse JSON cell links to normalized links", () => {
+ const jsonLink = {
+ cell: { "/": "of:test" },
+ path: ["nested", "value"],
+ };
+ const baseCell = runtime.getCell(space, "base");
+ const result = parseLink(jsonLink, baseCell);
+
+ expect(result).toEqual({
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ });
+ });
+
+ it("should parse legacy aliases to normalized links", () => {
+ const cell = runtime.getCell(space, "test");
+ const legacyAlias = {
+ $alias: {
+ cell: cell.getDoc(),
+ path: ["nested", "value"],
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ },
+ };
+ const result = parseLink(legacyAlias);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ });
+ });
+
+ it("should handle legacy aliases without cell using base", () => {
+ const baseCell = runtime.getCell(space, "base");
+ const legacyAlias = {
+ $alias: {
+ path: ["nested", "value"],
+ },
+ };
+ const result = parseLink(legacyAlias, baseCell);
+
+ expect(result).toEqual({
+ id: expect.stringContaining("of:"),
+ path: ["nested", "value"],
+ space: space,
+ schema: undefined,
+ rootSchema: undefined,
+ });
+ });
+
+ it("should return undefined for non-link values", () => {
+ expect(parseLink("string")).toBeUndefined();
+ expect(parseLink(123)).toBeUndefined();
+ expect(parseLink({ notLink: "value" })).toBeUndefined();
+ });
+ });
+
+ describe("parseLinkOrThrow", () => {
+ it("should return parsed link for valid links", () => {
+ const cell = runtime.getCell(space, "test");
+ const result = parseLinkOrThrow(cell);
+ expect(result).toBeDefined();
+ expect(result.id).toBeDefined();
+ });
+
+ it("should throw error for non-link values", () => {
+ expect(() => parseLinkOrThrow("string")).toThrow(
+ "Cannot parse value as link",
+ );
+ expect(() => parseLinkOrThrow(123)).toThrow("Cannot parse value as link");
+ });
+ });
+
+ describe("parseToLegacyCellLink", () => {
+ it("should parse cells to legacy cell links", () => {
+ const cell = runtime.getCell(space, "test");
+ const result = parseToLegacyCellLink(cell, cell);
+
+ expect(result).toBeDefined();
+ expect(result?.cell).toBeDefined();
+ expect(result?.path).toEqual([]);
+ });
+
+ it("should parse docs to legacy cell links", () => {
+ const cell = runtime.getCell(space, "test");
+ const doc = cell.getDoc();
+ const result = parseToLegacyCellLink(doc);
+
+ expect(result).toBeDefined();
+ expect(result?.cell).toBeDefined();
+ expect(result?.path).toEqual([]);
+ });
+
+ it("should parse legacy aliases to legacy cell links", () => {
+ const cell = runtime.getCell(space, "test");
+ const legacyAlias = {
+ $alias: {
+ cell: cell.getDoc(),
+ path: ["nested", "value"],
+ },
+ };
+ const result = parseToLegacyCellLink(legacyAlias);
+
+ expect(result).toBeDefined();
+ expect(result?.cell).toBeDefined();
+ expect(result?.path).toEqual(["nested", "value"]);
+ });
+
+ it("should return undefined for non-link values", () => {
+ expect(parseToLegacyCellLink("string")).toBeUndefined();
+ expect(parseToLegacyCellLink(123)).toBeUndefined();
+ });
+
+ it("should throw error for links without base cell when needed", () => {
+ const jsonLink = {
+ cell: { "/": "of:test" },
+ path: ["nested", "value"],
+ };
+ expect(() => parseToLegacyCellLink(jsonLink)).toThrow(
+ "No id or base cell provided",
+ );
+ });
+ });
+
+ describe("areLinksSame", () => {
+ it("should return true for identical objects", () => {
+ const cell = runtime.getCell(space, "test");
+ expect(areLinksSame(cell, cell)).toBe(true);
+ });
+
+ it("should return true for equivalent links", () => {
+ const cell = runtime.getCell(space, "test");
+ const cellLink1 = cell.getAsLegacyCellLink();
+ const cellLink2 = cell.getAsLegacyCellLink();
+ expect(areLinksSame(cellLink1, cellLink2)).toBe(true);
+ });
+
+ it("should return true for different link formats pointing to same location", () => {
+ const cell = runtime.getCell(space, "test");
+ const cellLink = cell.getAsLegacyCellLink();
+ const sigilLink = cell.getAsLink();
+ expect(areLinksSame(cellLink, sigilLink)).toBe(true);
+ });
+
+ it("should return false for different links", () => {
+ const cell1 = runtime.getCell(space, "test1");
+ const cell2 = runtime.getCell(space, "test2");
+ expect(areLinksSame(cell1, cell2)).toBe(false);
+ });
+
+ it("should return false for link vs non-link", () => {
+ const cell = runtime.getCell(space, "test");
+ expect(areLinksSame(cell, "string")).toBe(false);
+ });
+
+ it("should handle null/undefined values", () => {
+ expect(areLinksSame(null, null)).toBe(true);
+ expect(areLinksSame(undefined, undefined)).toBe(true);
+ expect(areLinksSame(null, undefined)).toBe(false);
+
+ const cell = runtime.getCell(space, "test");
+ expect(areLinksSame(cell, null)).toBe(false);
+ expect(areLinksSame(null, cell)).toBe(false);
+ });
+ });
+
+ describe("createSigilLinkFromParsedLink", () => {
+ it("should create sigil link from normalized link", () => {
+ const normalizedLink: NormalizedLink = {
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ };
+
+ const result = createSigilLinkFromParsedLink(normalizedLink);
+
+ expect(result).toEqual({
+ "/": {
+ [LINK_V1_TAG]: {
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ schema: { type: "number" },
+ rootSchema: { type: "object" },
+ },
+ },
+ });
+ });
+
+ it("should omit space when same as base", () => {
+ const baseCell = runtime.getCell(space, "base");
+ const normalizedLink: NormalizedLink = {
+ id: "of:test",
+ path: ["nested", "value"],
+ space: space,
+ };
+
+ const result = createSigilLinkFromParsedLink(normalizedLink, baseCell);
+
+ expect(result["/"][LINK_V1_TAG].space).toBeUndefined();
+ });
+
+ it("should omit id when same as base", () => {
+ const baseCell = runtime.getCell(space, "base");
+ const baseId = baseCell.getDoc().entityId;
+ const normalizedLink: NormalizedLink = {
+ id: `of:${baseId}`,
+ path: ["nested", "value"],
+ };
+
+ const result = createSigilLinkFromParsedLink(normalizedLink, baseCell);
+
+ expect(result["/"][LINK_V1_TAG].id).toBe(`of:${baseId}`);
+ });
+
+ it("should include overwrite field when present", () => {
+ const normalizedLink: NormalizedLink = {
+ id: "of:test",
+ path: ["nested", "value"],
+ overwrite: "redirect",
+ };
+
+ const result = createSigilLinkFromParsedLink(normalizedLink);
+
+ expect(result["/"][LINK_V1_TAG].overwrite).toBe("redirect");
+ });
+ });
+});
diff --git a/packages/runner/test/push-conflict.test.ts b/packages/runner/test/push-conflict.test.ts
index 51e6fd46a..adcc3512c 100644
--- a/packages/runner/test/push-conflict.test.ts
+++ b/packages/runner/test/push-conflict.test.ts
@@ -3,7 +3,7 @@ import { expect } from "@std/expect";
import { ID } from "../src/builder/types.ts";
import { Identity } from "@commontools/identity";
import { type IStorage, Runtime } from "../src/runtime.ts";
-import { isCellLink } from "../src/cell.ts";
+import { isAnyCellLink } from "../src/link-utils.ts";
import * as Memory from "@commontools/memory";
import * as Consumer from "@commontools/memory/consumer";
import { Provider } from "../src/storage/cache.ts";
@@ -110,7 +110,7 @@ describe("Push conflict", () => {
"name",
);
name.set(undefined);
-
+
const list = runtime.getCell(
signer.did(),
"list 2",
@@ -175,7 +175,7 @@ describe("Push conflict", () => {
"name 2",
);
name.set(undefined);
-
+
const list = runtime.getCell(
signer.did(),
"list 3",
@@ -221,7 +221,7 @@ describe("Push conflict", () => {
// This is locally ahead of the db, and retry wasn't called yet.
expect(name.get()).toEqual("bar");
expect(list.get()).toEqual([{ n: 4 }]);
- expect(isCellLink(list.getRaw()?.[0])).toBe(true);
+ expect(isAnyCellLink(list.getRaw()?.[0])).toBe(true);
const entry = list.getRaw()[0].cell?.asCell();
expect(retryCalled).toEqual(0);
@@ -239,6 +239,6 @@ describe("Push conflict", () => {
expect(!!listDoc.retry?.length).toBe(false);
// Check that the ID is still there
- expect(entry.equals(list.getRaw()[3].cell.asCell())).toBe(true);
+ expect(JSON.stringify(entry)).toEqual(JSON.stringify(list.getRaw()[3]));
});
});
diff --git a/packages/runner/test/query.test.ts b/packages/runner/test/query.test.ts
index 5603ae7ec..cfeae77b2 100644
--- a/packages/runner/test/query.test.ts
+++ b/packages/runner/test/query.test.ts
@@ -17,7 +17,6 @@ import { Runtime } from "../src/runtime.ts";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
import { Identity } from "@commontools/identity";
import { ClientObjectManager } from "../src/storage/query.ts";
-import { entityIdToJSON } from "./test-helpers.ts";
const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
@@ -60,7 +59,7 @@ describe("Query", () => {
`query test cell 1`,
);
testCell1.set(docValue1);
- const entityId1 = testCell1.entityId!;
+ const entityId1 = JSON.parse(JSON.stringify(testCell1.entityId!));
const assert1 = {
the: "application/json",
of: `of:${entityId1["/"]}` as Entity,
@@ -70,7 +69,7 @@ describe("Query", () => {
};
const docValue2 = {
name: {
- cell: entityIdToJSON(entityId1),
+ cell: entityId1,
path: ["employees", "0", "fullName"],
},
};
@@ -151,7 +150,7 @@ describe("Query", () => {
`query test cell 1`,
);
testCell1.set({ employees: [{ name: { first: "Bob" } }] });
- const entityId1 = testCell1.entityId!;
+ const entityId1 = JSON.parse(JSON.stringify(testCell1.entityId!));
const assert1 = {
the: "application/json",
of: `of:${entityId1["/"]}` as Entity,
@@ -167,7 +166,7 @@ describe("Query", () => {
);
testCell2.setRaw({
name: {
- cell: entityIdToJSON(entityId1),
+ cell: entityId1,
path: ["employees", "0", "name"],
},
});
@@ -249,14 +248,14 @@ describe("Query", () => {
path: ["name"],
},
});
- const entityId1 = testCell1.entityId!;
+ const entityId1 = JSON.parse(JSON.stringify(testCell1.entityId!));
const assert1 = {
the: "application/json",
of: `of:${entityId1["/"]}` as Entity,
is: {
value: {
name: {
- cell: entityIdToJSON(entityId1),
+ cell: entityId1,
path: ["name"],
},
},
@@ -308,7 +307,7 @@ describe("Query", () => {
`query test cell 1`,
);
testCell1.set(docValue1);
- const entityId1 = testCell1.entityId!;
+ const entityId1 = JSON.parse(JSON.stringify(testCell1.entityId!));
const assert1 = {
the: "application/json",
of: `of:${entityId1["/"]}` as Entity,
@@ -318,7 +317,12 @@ describe("Query", () => {
};
const docValue2 = {
- employees: [{ address: { cell: entityIdToJSON(entityId1), path: ["home"] } }],
+ employees: [{
+ address: {
+ cell: entityId1,
+ path: ["home"],
+ },
+ }],
};
const testCell2 = runtime.getCell(
space,
diff --git a/packages/runner/test/recipe-binding.test.ts b/packages/runner/test/recipe-binding.test.ts
index 6530a2995..12bf24c94 100644
--- a/packages/runner/test/recipe-binding.test.ts
+++ b/packages/runner/test/recipe-binding.test.ts
@@ -7,7 +7,7 @@ import {
import { Runtime } from "../src/runtime.ts";
import { Identity } from "@commontools/identity";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
-import { expectCellLinksEqual } from "./test-helpers.ts";
+import { areLinksSame } from "../src/link-utils.ts";
const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
@@ -127,11 +127,16 @@ describe("recipe-binding", () => {
};
const result = unwrapOneLevelAndBindtoDoc(binding, testCell);
- expectCellLinksEqual(result).toEqual({
- x: { $alias: testCell.key("a").getAsCellLink() },
- y: { $alias: testCell.key("b").key("c").getAsCellLink() },
- z: 3,
- });
+ expect(
+ areLinksSame(result.x, {
+ $alias: testCell.key("a").getAsLegacyCellLink(),
+ }),
+ ).toBe(true);
+ expect(
+ areLinksSame(result.y, {
+ $alias: testCell.key("b").key("c").getAsLegacyCellLink(),
+ }),
+ ).toBe(true);
});
});
});
diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts
index d9ac8f01d..14cb3181a 100644
--- a/packages/runner/test/recipes.test.ts
+++ b/packages/runner/test/recipes.test.ts
@@ -4,7 +4,7 @@ import { type Cell, type JSONSchema } from "../src/builder/types.ts";
import { createBuilder } from "../src/builder/factory.ts";
import { Runtime } from "../src/runtime.ts";
import { type ErrorWithContext } from "../src/scheduler.ts";
-import { isCell, isCellLink } from "../src/cell.ts";
+import { isCell } from "../src/cell.ts";
import { resolveLinks } from "../src/link-resolution.ts";
import { Identity } from "@commontools/identity";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
@@ -60,7 +60,9 @@ describe("Recipe Runner", () => {
space,
"should run a simple recipe",
);
- const result = await runtime.runSynced(resultCell, simpleRecipe, { value: 5 });
+ const result = await runtime.runSynced(resultCell, simpleRecipe, {
+ value: 5,
+ });
await runtime.idle();
@@ -90,7 +92,9 @@ describe("Recipe Runner", () => {
space,
"should handle nested recipes",
);
- const result = await runtime.runSynced(resultCell, outerRecipe, { value: 4 });
+ const result = await runtime.runSynced(resultCell, outerRecipe, {
+ value: 4,
+ });
await runtime.idle();
@@ -112,7 +116,11 @@ describe("Recipe Runner", () => {
space,
"should handle recipes with defaults",
);
- const result1 = await runtime.runSynced(resultCell1, recipeWithDefaults, {});
+ const result1 = await runtime.runSynced(
+ resultCell1,
+ recipeWithDefaults,
+ {},
+ );
await runtime.idle();
@@ -122,7 +130,9 @@ describe("Recipe Runner", () => {
space,
"should handle recipes with defaults (2)",
);
- const result2 = await runtime.runSynced(resultCell2, recipeWithDefaults, { a: 20 });
+ const result2 = await runtime.runSynced(resultCell2, recipeWithDefaults, {
+ a: 20,
+ });
await runtime.idle();
@@ -200,7 +210,9 @@ describe("Recipe Runner", () => {
space,
"should handle map nodes with undefined input",
);
- const result = await runtime.runSynced(resultCell, doubleArray, { values: undefined });
+ const result = await runtime.runSynced(resultCell, doubleArray, {
+ values: undefined,
+ });
await runtime.idle();
@@ -225,7 +237,9 @@ describe("Recipe Runner", () => {
);
const resultCell = runtime.getCell(space, "should execute handlers");
- const result = await runtime.runSynced(resultCell, incRecipe, { counter: { value: 0 } });
+ const result = await runtime.runSynced(resultCell, incRecipe, {
+ counter: { value: 0 },
+ });
await runtime.idle();
@@ -261,7 +275,9 @@ describe("Recipe Runner", () => {
space,
"should execute handlers that use bind and this",
);
- const result = await runtime.runSynced(resultCell, incRecipe, { counter: { value: 0 } });
+ const result = await runtime.runSynced(resultCell, incRecipe, {
+ counter: { value: 0 },
+ });
await runtime.idle();
@@ -293,7 +309,9 @@ describe("Recipe Runner", () => {
space,
"should execute handlers that use bind and this (no types)",
);
- const result = await runtime.runSynced(resultCell, incRecipe, { counter: { value: 0 } });
+ const result = await runtime.runSynced(resultCell, incRecipe, {
+ counter: { value: 0 },
+ });
await runtime.idle();
@@ -348,7 +366,10 @@ describe("Recipe Runner", () => {
space,
"should execute recipes returned by handlers",
);
- const result = await runtime.runSynced(resultCell, incRecipe, { counter, nested });
+ const result = await runtime.runSynced(resultCell, incRecipe, {
+ counter,
+ nested,
+ });
await runtime.idle();
@@ -416,7 +437,10 @@ describe("Recipe Runner", () => {
space,
"should handle recipes returned by lifted functions",
);
- const result = await runtime.runSynced(resultCell, multiplyRecipe, { x, y });
+ const result = await runtime.runSynced(resultCell, multiplyRecipe, {
+ x,
+ y,
+ });
await runtime.idle();
@@ -466,7 +490,9 @@ describe("Recipe Runner", () => {
space,
"should support referenced modules",
);
- const result = await runtime.runSynced(resultCell, simpleRecipe, { value: 5 });
+ const result = await runtime.runSynced(resultCell, simpleRecipe, {
+ value: 5,
+ });
await runtime.idle();
@@ -581,7 +607,9 @@ describe("Recipe Runner", () => {
space,
"should handle nested cell references in schema",
);
- const result = await runtime.runSynced(resultCell, sumRecipe, { data: { items: [item1, item2] } });
+ const result = await runtime.runSynced(resultCell, sumRecipe, {
+ data: { items: [item1, item2] },
+ });
await runtime.idle();
@@ -675,7 +703,9 @@ describe("Recipe Runner", () => {
space,
"should execute handlers with schemas",
);
- const result = await runtime.runSynced(resultCell, incRecipe, { counter: 0 });
+ const result = await runtime.runSynced(resultCell, incRecipe, {
+ counter: 0,
+ });
await runtime.idle();
@@ -788,7 +818,10 @@ describe("Recipe Runner", () => {
space,
"failed lifted handlers should be ignored",
);
- const charm = await runtime.runSynced(charmCell, divRecipe, { divisor: 10, dividend });
+ const charm = await runtime.runSynced(charmCell, divRecipe, {
+ divisor: 10,
+ dividend,
+ });
await runtime.idle();
@@ -811,7 +844,9 @@ describe("Recipe Runner", () => {
// Make sure it recovers:
dividend.send(2);
await runtime.idle();
- expect((charm.getRaw() as any).result.$alias.cell).toBe(charm.getSourceCell()?.getDoc());
+ expect((charm.getRaw() as any).result.$alias.cell).toBe(
+ charm.getSourceCell()?.getDoc(),
+ );
expect(charm.getAsQueryResult()).toMatchObject({ result: 5 });
});
@@ -975,7 +1010,7 @@ describe("Recipe Runner", () => {
type: "object",
properties: { value: { type: "number", asCell: true } },
required: ["value"],
- }
+ },
);
const result = runtime.run(wrapperRecipe, { value: input }, resultCell);
@@ -987,7 +1022,7 @@ describe("Recipe Runner", () => {
expect(wrapperCell.get()).toBe(5);
// Follow all the links until we get to the doc holding the value
- const ref = resolveLinks(wrapperCell.getAsCellLink());
+ const ref = resolveLinks(wrapperCell.getAsLegacyCellLink());
expect(ref.path).toEqual([]); // = This is stored in its own document
// And let's make sure the value is correct
diff --git a/packages/runner/test/runner.test.ts b/packages/runner/test/runner.test.ts
index d8e48acd6..70c2f9573 100644
--- a/packages/runner/test/runner.test.ts
+++ b/packages/runner/test/runner.test.ts
@@ -279,7 +279,7 @@ describe("runRecipe", () => {
space,
"should allow passing a cell as a binding: input cell",
);
- inputCell.setRaw({ input: 10, output: 0 });
+ inputCell.set({ input: 10, output: 0 });
const resultCell = runtime.getCell(
space,
"should allow passing a cell as a binding",
@@ -320,7 +320,7 @@ describe("runRecipe", () => {
space,
"should allow stopping a recipe: input cell",
);
- inputCell.setRaw({ input: 10, output: 0 });
+ inputCell.set({ input: 10, output: 0 });
const resultCell = runtime.getCell(space, "should allow stopping a recipe");
const result = await runtime.runSynced(resultCell, recipe, inputCell);
@@ -711,16 +711,16 @@ describe("runner utils", () => {
"should treat cell aliases and references as values 1",
);
const obj1 = { a: { $alias: { path: [] } } };
- const obj2 = { a: 2, b: { c: testCell.getAsCellLink() } };
+ const obj2 = { a: 2, b: { c: testCell.getAsLegacyCellLink() } };
const obj3 = {
- a: { $alias: testCell.key("a").getAsCellLink() },
+ a: { $alias: testCell.key("a").getAsLegacyCellLink() },
b: { c: 4 },
};
const result = mergeObjects(obj1, obj2, obj3);
expect(result).toEqual({
a: { $alias: { path: [] } },
- b: { c: testCell.getAsCellLink() },
+ b: { c: testCell.getAsLegacyCellLink() },
});
});
});
diff --git a/packages/runner/test/scheduler.test.ts b/packages/runner/test/scheduler.test.ts
index 937d14bb5..6504d26d5 100644
--- a/packages/runner/test/scheduler.test.ts
+++ b/packages/runner/test/scheduler.test.ts
@@ -87,10 +87,10 @@ describe("scheduler", () => {
};
runtime.scheduler.schedule(adder, {
reads: [
- a.getAsCellLink(),
- b.getAsCellLink(),
+ a.getAsLegacyCellLink(),
+ b.getAsLegacyCellLink(),
],
- writes: [c.getAsCellLink()],
+ writes: [c.getAsLegacyCellLink()],
});
expect(runCount).toBe(0);
expect(c.get()).toBe(0);
@@ -155,10 +155,10 @@ describe("scheduler", () => {
};
const cancel = runtime.scheduler.schedule(adder, {
reads: [
- a.getAsCellLink(),
- b.getAsCellLink(),
+ a.getAsLegacyCellLink(),
+ b.getAsLegacyCellLink(),
],
- writes: [c.getAsCellLink()],
+ writes: [c.getAsLegacyCellLink()],
});
expect(runCount).toBe(0);
expect(c.get()).toBe(0);
@@ -371,10 +371,13 @@ describe("event handling", () => {
eventResultCell.send(event);
};
- runtime.scheduler.addEventHandler(eventHandler, eventCell.getAsCellLink());
+ runtime.scheduler.addEventHandler(
+ eventHandler,
+ eventCell.getAsLegacyCellLink(),
+ );
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 1);
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 2);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 1);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 2);
await runtime.idle();
@@ -398,10 +401,10 @@ describe("event handling", () => {
const removeHandler = runtime.scheduler.addEventHandler(
eventHandler,
- eventCell.getAsCellLink(),
+ eventCell.getAsLegacyCellLink(),
);
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 1);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 1);
await runtime.idle();
expect(eventCount).toBe(1);
@@ -409,7 +412,7 @@ describe("event handling", () => {
removeHandler();
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 2);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 2);
await runtime.idle();
expect(eventCount).toBe(1);
@@ -430,11 +433,11 @@ describe("event handling", () => {
runtime.scheduler.addEventHandler(
eventHandler,
- parentCell.key("child").key("value").getAsCellLink(),
+ parentCell.key("child").key("value").getAsLegacyCellLink(),
);
runtime.scheduler.queueEvent(
- parentCell.key("child").key("value").getAsCellLink(),
+ parentCell.key("child").key("value").getAsLegacyCellLink(),
42,
);
await runtime.idle();
@@ -454,11 +457,14 @@ describe("event handling", () => {
events.push(event);
};
- runtime.scheduler.addEventHandler(eventHandler, eventCell.getAsCellLink());
+ runtime.scheduler.addEventHandler(
+ eventHandler,
+ eventCell.getAsLegacyCellLink(),
+ );
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 1);
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 2);
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 3);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 1);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 2);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 3);
await runtime.idle();
@@ -491,11 +497,14 @@ describe("event handling", () => {
};
await runtime.scheduler.run(action);
- runtime.scheduler.addEventHandler(eventHandler, eventCell.getAsCellLink());
+ runtime.scheduler.addEventHandler(
+ eventHandler,
+ eventCell.getAsLegacyCellLink(),
+ );
expect(actionCount).toBe(1);
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 1);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 1);
await runtime.idle();
expect(eventCount).toBe(1);
@@ -503,7 +512,7 @@ describe("event handling", () => {
expect(actionCount).toBe(2);
- runtime.scheduler.queueEvent(eventCell.getAsCellLink(), 2);
+ runtime.scheduler.queueEvent(eventCell.getAsLegacyCellLink(), 2);
await runtime.idle();
expect(eventCount).toBe(2);
@@ -532,8 +541,8 @@ describe("compactifyPaths", () => {
await storageManager?.close();
});
- // helper to normalize CellLinks because compactifyPaths() does not preserve
- // the extra properties added by cell.getAsCellLink()
+ // helper to normalize LegacyCellLinks because compactifyPaths() does not preserve
+ // the extra properties added by cell.getAsLegacyCellLink()
const normalizeCellLink = (link: any) => ({
cell: link.cell,
path: link.path,
@@ -546,14 +555,14 @@ describe("compactifyPaths", () => {
);
testCell.set({});
const paths = [
- testCell.key("a").key("b").getAsCellLink(),
- testCell.key("a").getAsCellLink(),
- testCell.key("c").getAsCellLink(),
+ testCell.key("a").key("b").getAsLegacyCellLink(),
+ testCell.key("a").getAsLegacyCellLink(),
+ testCell.key("c").getAsLegacyCellLink(),
];
const result = compactifyPaths(paths);
const expected = [
- testCell.key("a").getAsCellLink(),
- testCell.key("c").getAsCellLink(),
+ testCell.key("a").getAsLegacyCellLink(),
+ testCell.key("c").getAsLegacyCellLink(),
];
expect(result.map(normalizeCellLink)).toEqual(
expected.map(normalizeCellLink),
@@ -567,11 +576,11 @@ describe("compactifyPaths", () => {
);
testCell.set({});
const paths = [
- testCell.key("a").key("b").getAsCellLink(),
- testCell.key("a").key("b").getAsCellLink(),
+ testCell.key("a").key("b").getAsLegacyCellLink(),
+ testCell.key("a").key("b").getAsLegacyCellLink(),
];
const result = compactifyPaths(paths);
- const expected = [testCell.key("a").key("b").getAsCellLink()];
+ const expected = [testCell.key("a").key("b").getAsLegacyCellLink()];
expect(result.map(normalizeCellLink)).toEqual(
expected.map(normalizeCellLink),
);
@@ -589,8 +598,8 @@ describe("compactifyPaths", () => {
);
cellB.set({});
const paths = [
- cellA.key("a").key("b").getAsCellLink(),
- cellB.key("a").key("b").getAsCellLink(),
+ cellA.key("a").key("b").getAsLegacyCellLink(),
+ cellB.key("a").key("b").getAsLegacyCellLink(),
];
const result = compactifyPaths(paths);
expect(result.map(normalizeCellLink)).toEqual(paths.map(normalizeCellLink));
@@ -603,11 +612,11 @@ describe("compactifyPaths", () => {
);
cellA.set({});
- const expectedResult = cellA.getAsCellLink();
+ const expectedResult = cellA.getAsLegacyCellLink();
const paths = [
- cellA.key("a").key("b").getAsCellLink(),
- cellA.key("c").getAsCellLink(),
- cellA.key("d").getAsCellLink(),
+ cellA.key("a").key("b").getAsLegacyCellLink(),
+ cellA.key("c").getAsLegacyCellLink(),
+ cellA.key("d").getAsLegacyCellLink(),
expectedResult,
];
const result = compactifyPaths(paths);
diff --git a/packages/runner/test/schema-lineage.test.ts b/packages/runner/test/schema-lineage.test.ts
index 6f97658e2..015303f12 100644
--- a/packages/runner/test/schema-lineage.test.ts
+++ b/packages/runner/test/schema-lineage.test.ts
@@ -58,7 +58,7 @@ describe("Schema Lineage", () => {
);
sourceCell.setRaw({
$alias: {
- ...targetCell.getAsCellLink(),
+ ...targetCell.getAsLegacyCellLink(),
schema,
rootSchema: schema,
},
@@ -66,7 +66,8 @@ describe("Schema Lineage", () => {
// Access the cell without providing a schema
// (Type script type is just to avoid compiler errors)
- const cell: Cell<{ count: number; label: string }> = sourceCell.asSchema();
+ const cell: Cell<{ count: number; label: string }> = sourceCell
+ .asSchema();
// The cell should have picked up the schema from the alias
expect(cell.schema).toBeDefined();
@@ -110,7 +111,7 @@ describe("Schema Lineage", () => {
);
sourceCell.setRaw({
$alias: {
- ...targetCell.getAsCellLink(),
+ ...targetCell.getAsLegacyCellLink(),
schema: aliasSchema,
rootSchema: aliasSchema,
},
@@ -149,7 +150,7 @@ describe("Schema Lineage", () => {
);
countCell.setRaw({
$alias: {
- ...valueCell.key("count").getAsCellLink(),
+ ...valueCell.key("count").getAsLegacyCellLink(),
schema: numberSchema,
rootSchema: numberSchema,
},
@@ -161,7 +162,7 @@ describe("Schema Lineage", () => {
"final-alias",
);
finalCell.setRaw({
- $alias: countCell.getAsCellLink(),
+ $alias: countCell.getAsLegacyCellLink(),
});
// Access the cell without providing a schema
@@ -207,15 +208,17 @@ describe("Schema Lineage", () => {
);
itemsCell.setRaw({
$alias: {
- ...nestedCell.key("items").getAsCellLink(),
+ ...nestedCell.key("items").getAsLegacyCellLink(),
schema: arraySchema,
},
});
// Access the items with a schema that specifies array items should be cells
- const itemsCellWithSchema = itemsCell.asSchema({
- asCell: true,
- } as const satisfies JSONSchema);
+ const itemsCellWithSchema = itemsCell.asSchema(
+ {
+ asCell: true,
+ } as const satisfies JSONSchema,
+ );
const value = itemsCellWithSchema.get() as any;
expect(isCell(value)).toBe(true);
@@ -289,17 +292,19 @@ describe("Schema propagation end-to-end example", () => {
result,
);
- const c = result.key(UI).asSchema({
- type: "object",
- properties: {
- type: { type: "string" },
- name: { type: "string" },
- props: {
- type: "object",
- additionalProperties: { asCell: true },
+ const c = result.key(UI).asSchema(
+ {
+ type: "object",
+ properties: {
+ type: { type: "string" },
+ name: { type: "string" },
+ props: {
+ type: "object",
+ additionalProperties: { asCell: true },
+ },
},
- },
- } as const satisfies JSONSchema);
+ } as const satisfies JSONSchema,
+ );
const cValue = c.get() as any;
expect(isCell(cValue.props.value)).toBe(true);
diff --git a/packages/runner/test/schema.test.ts b/packages/runner/test/schema.test.ts
index dd464cd2e..a07a17ea1 100644
--- a/packages/runner/test/schema.test.ts
+++ b/packages/runner/test/schema.test.ts
@@ -1,11 +1,13 @@
import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
-import { type Cell, CellLink, isCell, isStream } from "../src/cell.ts";
+import { type Cell, isCell, isStream } from "../src/cell.ts";
+import { LegacyCellLink } from "../src/sigil-types.ts";
import type { JSONSchema } from "../src/builder/types.ts";
import { Runtime } from "../src/runtime.ts";
import { Identity } from "@commontools/identity";
import { StorageManager } from "@commontools/runner/storage/cache.deno";
-import { entityIdToJSON, toPlainObject } from "./test-helpers.ts";
+import { toURI } from "../src/uri-utils.ts";
+import { parseLink } from "../src/link-utils.ts";
const signer = await Identity.fromPassphrase("test operator");
const space = signer.did();
@@ -54,23 +56,23 @@ describe("Schema Support", () => {
// This is what the system (or someone manually) would create to remap
// data to match the desired schema
const mappingCell = runtime.getCell<{
- id: CellLink;
- changes: CellLink[];
- kind: CellLink;
- tag: CellLink;
+ id: LegacyCellLink;
+ changes: LegacyCellLink[];
+ kind: LegacyCellLink;
+ tag: LegacyCellLink;
}>(
space,
"allows mapping of fields via interim cells 2",
);
mappingCell.setRaw({
// as-is
- id: c.key("id").getAsCellLink(),
+ id: c.key("id").getAsLegacyCellLink(),
// turn single value to set
- changes: [c.key("metadata").key("createdAt").getAsCellLink()],
+ changes: [c.key("metadata").key("createdAt").getAsLegacyCellLink()],
// rename field and uplift from nested element
- kind: c.key("metadata").key("type").getAsCellLink(),
+ kind: c.key("metadata").key("type").getAsLegacyCellLink(),
// turn set into a single value
- tag: c.key("tags").key(0).getAsCellLink(),
+ tag: c.key("tags").key(0).getAsLegacyCellLink(),
});
// This schema is how the recipient specifies what they want
@@ -115,17 +117,17 @@ describe("Schema Support", () => {
"should support nested sinks 1",
);
innerCell.set({ label: "first" });
-
+
const c = runtime.getCell<{
value: string;
- current: CellLink;
+ current: LegacyCellLink;
}>(
space,
"should support nested sinks 2",
);
c.setRaw({
value: "root",
- current: innerCell.getAsCellLink(),
+ current: innerCell.getAsLegacyCellLink(),
});
const cWithSchema = c.asSchema(schema);
@@ -235,14 +237,14 @@ describe("Schema Support", () => {
);
initial.set({ foo: { label: "first" } });
const initialEntityId = initial.entityId!;
-
+
const linkCell = runtime.getCell(
space,
"should support nested sinks via asCell with aliases 2",
);
- linkCell.setRaw(initial.getAsCellLink());
+ linkCell.setRaw(initial.getAsLegacyCellLink());
const linkEntityId = linkCell.entityId!;
-
+
const docCell = runtime.getCell<{
value: string;
current: any;
@@ -252,7 +254,7 @@ describe("Schema Support", () => {
);
docCell.setRaw({
value: "root",
- current: { $alias: linkCell.key("foo").getAsCellLink() },
+ current: { $alias: linkCell.key("foo").getAsLegacyCellLink() },
});
const docEntityId = docCell.entityId!;
const root = docCell.asSchema(schema);
@@ -282,8 +284,8 @@ describe("Schema Support", () => {
// Make sure the schema is correct and it is still anchored at the root
expect(current.schema).toEqual({ type: "string" });
- expect(toPlainObject(current.getAsCellLink())).toEqual({
- cell: entityIdToJSON(docCell.entityId!),
+ expect(parseLink(current.getAsLink({ includeSchema: true }))).toEqual({
+ id: toURI(docCell.entityId!),
path: ["current", "label"],
space,
schema: current.schema,
@@ -307,8 +309,8 @@ describe("Schema Support", () => {
expect(isCell(first)).toBe(true);
expect(first.get()).toEqual({ label: "first" });
const { asCell: _ignore, ...omitSchema } = schema.properties.current;
- expect(toPlainObject(first.getAsCellLink())).toEqual({
- cell: entityIdToJSON(initialEntityId),
+ expect(parseLink(first.getAsLink({ includeSchema: true }))).toEqual({
+ id: toURI(initialEntityId),
path: ["foo"],
space,
schema: omitSchema,
@@ -316,15 +318,15 @@ describe("Schema Support", () => {
});
expect(log.reads.length).toEqual(4);
expect(
- log.reads.map((r: CellLink) => ({
- cell: entityIdToJSON(r.cell.entityId!),
+ log.reads.map((r: LegacyCellLink) => ({
+ cell: toURI(r.cell.entityId!),
path: r.path,
})),
).toEqual([
- { cell: entityIdToJSON(docCell.entityId!), path: ["current"] },
- { cell: entityIdToJSON(linkEntityId), path: [] },
- { cell: entityIdToJSON(initialEntityId), path: ["foo"] },
- { cell: entityIdToJSON(initialEntityId), path: ["foo", "label"] },
+ { cell: toURI(docCell.entityId!), path: ["current"] },
+ { cell: toURI(linkEntityId), path: [] },
+ { cell: toURI(initialEntityId), path: ["foo"] },
+ { cell: toURI(initialEntityId), path: ["foo", "label"] },
]);
// Then update it
@@ -340,7 +342,7 @@ describe("Schema Support", () => {
"should support nested sinks via asCell with aliases 4",
);
second.set({ foo: { label: "second" } });
- linkCell.setRaw(second.getAsCellLink());
+ linkCell.setRaw(second.getAsLegacyCellLink());
await runtime.idle();
@@ -382,7 +384,7 @@ describe("Schema Support", () => {
);
third.set({ label: "third" });
docCell.key("current").setRaw({
- $alias: third.getAsCellLink(),
+ $alias: third.getAsLegacyCellLink(),
});
await runtime.idle();
@@ -667,9 +669,9 @@ describe("Schema Support", () => {
});
// Set up circular references using cell links
- c.key("parent").setRaw(c.getAsCellLink());
- c.key("children").key(0).key("parent").setRaw(c.getAsCellLink());
- c.key("children").key(1).key("parent").setRaw(c.getAsCellLink());
+ c.key("parent").setRaw(c.getAsLegacyCellLink());
+ c.key("children").key(0).key("parent").setRaw(c.getAsLegacyCellLink());
+ c.key("children").key(1).key("parent").setRaw(c.getAsLegacyCellLink());
const schema = {
type: "object",
@@ -723,9 +725,11 @@ describe("Schema Support", () => {
});
// Set up circular references using cell links
- c.key("nested").key("items").key(0).key("value").setRaw(c.getAsCellLink());
+ c.key("nested").key("items").key(0).key("value").setRaw(
+ c.getAsLegacyCellLink(),
+ );
c.key("nested").key("items").key(1).key("value").setRaw(
- c.key("nested").getAsCellLink()
+ c.key("nested").getAsLegacyCellLink(),
);
const schema = {
@@ -793,7 +797,7 @@ describe("Schema Support", () => {
});
// Set up circular references using cell links
- c.key("children").key(1).key("value").setRaw(c.getAsCellLink());
+ c.key("children").key(1).key("value").setRaw(c.getAsLegacyCellLink());
const schema = {
type: "object",
@@ -1254,7 +1258,7 @@ describe("Schema Support", () => {
);
childrenArrayCell.set([
{ type: "text", value: "hello" },
- innerTextCell.getAsCellLink(),
+ innerTextCell.getAsLegacyCellLink(),
]);
const withLinks = runtime.getCell<{
@@ -1275,11 +1279,11 @@ describe("Schema Support", () => {
type: "vnode",
name: "div",
props: {
- style: styleCell.getAsCellLink(),
+ style: styleCell.getAsLegacyCellLink(),
},
children: [
{ type: "text", value: "single" },
- childrenArrayCell.getAsCellLink(),
+ childrenArrayCell.getAsLegacyCellLink(),
"or just text",
],
});
diff --git a/packages/runner/test/storage.test.ts b/packages/runner/test/storage.test.ts
index fb4620a84..d22f0bd3f 100644
--- a/packages/runner/test/storage.test.ts
+++ b/packages/runner/test/storage.test.ts
@@ -65,7 +65,7 @@ describe("Storage", () => {
const testValue = {
data: "test",
- ref: refDoc.getAsCellLink(),
+ ref: refDoc.getAsLegacyCellLink(),
};
testDoc.send(testValue);
diff --git a/packages/runner/test/test-helpers.ts b/packages/runner/test/test-helpers.ts
deleted file mode 100644
index e8d6e9e8c..000000000
--- a/packages/runner/test/test-helpers.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { assertEquals } from "@std/assert";
-import { AssertionError } from "@std/assert";
-import { isAlias } from "../src/builder/types.ts";
-import { isCellLink } from "../src/cell.ts";
-import type { EntityId } from "../src/doc-map.ts";
-
-/**
- * Normalizes CellLinks by keeping only cell and path properties
- * Also normalizes path elements so numeric strings and numbers are equivalent
- */
-export function normalizeCellLink(link: any): any {
- // Normalize path elements: convert numeric strings to numbers for comparison
- const normalizedPath = link.path.map((element: any) => {
- // If it's a string that represents a valid array index, convert to number
- if (typeof element === 'string' && /^\d+$/.test(element)) {
- return parseInt(element, 10);
- }
- return element;
- });
-
- return { cell: link.cell, path: normalizedPath };
-}
-
-/**
- * Deep normalizes an object, handling CellLinks and aliases
- * Strips out extra properties like space, schema, rootSchema from CellLinks
- */
-function deepNormalizeCellLinks(obj: any): any {
- if (!obj || typeof obj !== 'object') {
- return obj;
- }
-
- // Handle bare CellLinks
- if (isCellLink(obj)) {
- return normalizeCellLink(obj);
- }
-
- // Handle arrays
- if (Array.isArray(obj)) {
- return obj.map(deepNormalizeCellLinks);
- }
-
- // Handle objects
- const result: any = {};
- for (const [key, value] of Object.entries(obj)) {
- if (isAlias(value)) {
- // Normalize CellLinks within aliases
- result[key] = {
- $alias: normalizeCellLink(value.$alias)
- };
- } else {
- result[key] = deepNormalizeCellLinks(value);
- }
- }
- return result;
-}
-
-/**
- * Custom expect-style matcher for CellLink equality
- * Usage: expectCellLinksEqual(actual).toEqual(expected)
- * Works with bare CellLinks, CellLinks in aliases, and nested structures
- */
-export function expectCellLinksEqual(actual: unknown) {
- return {
- toEqual(expected: unknown, msg?: string) {
- const normalizedActual = deepNormalizeCellLinks(actual);
- const normalizedExpected = deepNormalizeCellLinks(expected);
-
- try {
- assertEquals(normalizedActual, normalizedExpected, msg);
- } catch (error) {
- if (error instanceof AssertionError) {
- error.message = `CellLinks are not equal (after normalization).\n${error.message}`;
- throw error;
- }
- throw error;
- }
- }
- };
-}
-
-/**
- * Helper to convert objects with toJSON methods to plain objects
- * Useful for test expectations that need to compare serialized forms
- */
-export function toPlainObject(obj: T): any {
- return JSON.parse(JSON.stringify(obj));
-}
-
-/**
- * Helper to convert EntityId to JSON representation
- * Handles EntityId objects that may or may not have toJSON method
- */
-export function entityIdToJSON(entityId: EntityId): { "/": string } {
- if (entityId.toJSON) {
- return entityId.toJSON();
- }
- // Fallback: construct the object manually
- return { "/": entityId["/"].toString() };
-}
\ No newline at end of file
diff --git a/packages/runner/test/type-utils.test.ts b/packages/runner/test/type-utils.test.ts
index 0ea73fee1..dd33c930a 100644
--- a/packages/runner/test/type-utils.test.ts
+++ b/packages/runner/test/type-utils.test.ts
@@ -1,11 +1,8 @@
import { describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
-import {
- isAlias,
- isModule,
- isRecipe,
- type Opaque,
-} from "../src/builder/types.ts";
+import { isModule, isRecipe, type Opaque } from "../src/builder/types.ts";
+import { isWriteRedirectLink } from "../src/link-utils.ts";
+import { LINK_V1_TAG } from "../src/sigil-types.ts";
describe("value type", () => {
it("can destructure a value without TS errors", () => {
@@ -35,9 +32,19 @@ describe("value type", () => {
});
describe("utility functions", () => {
- it("isAlias correctly identifies aliases", () => {
- expect(isAlias({ $alias: { path: ["path", "to", "value"] } })).toBe(true);
- expect(isAlias({ notAlias: "something" })).toBe(false);
+ it("isWriteRedirectLink correctly identifies write redirects", () => {
+ expect(isWriteRedirectLink({ $alias: { path: ["path", "to", "value"] } }))
+ .toBe(true);
+ expect(
+ isWriteRedirectLink({
+ "/": {
+ [LINK_V1_TAG]: { id: "path/to/value", overwrite: "redirect" },
+ },
+ }),
+ ).toBe(
+ true,
+ );
+ expect(isWriteRedirectLink({ notAlias: "something" })).toBe(false);
});
it("isModule correctly identifies modules", () => {
@@ -58,4 +65,4 @@ describe("utility functions", () => {
).toBe(true);
expect(isRecipe({ notRecipe: "something" })).toBe(false);
});
-});
\ No newline at end of file
+});
diff --git a/packages/toolshed/routes/integrations/google-oauth/google-oauth.handlers.ts b/packages/toolshed/routes/integrations/google-oauth/google-oauth.handlers.ts
index 5e278c7f3..2698fbd4d 100644
--- a/packages/toolshed/routes/integrations/google-oauth/google-oauth.handlers.ts
+++ b/packages/toolshed/routes/integrations/google-oauth/google-oauth.handlers.ts
@@ -25,7 +25,7 @@ import {
tokenToAuthData,
} from "./google-oauth.utils.ts";
import { setBGCharm } from "@commontools/background-charm";
-import { type CellLink } from "@commontools/runner";
+import { parseLink } from "@commontools/runner";
import { runtime } from "@/index.ts";
import { Tokens } from "@cmd-johnson/oauth2-client";
@@ -201,7 +201,7 @@ export const callback: AppRouteHandler = async (c) => {
// Add this charm to the Gmail integration charms cell
try {
// Get the charm ID and space from the decodedState (which is the auth cell ID)
- const authCellLink = JSON.parse(decodedState.authCellId) as CellLink;
+ const authCellLink = parseLink(JSON.parse(decodedState.authCellId))!;
const space = authCellLink.space;
const integrationCharmId = decodedState?.integrationCharmId;
diff --git a/packages/toolshed/routes/integrations/google-oauth/google-oauth.routes.ts b/packages/toolshed/routes/integrations/google-oauth/google-oauth.routes.ts
index 3b33ccb30..f26a0836f 100644
--- a/packages/toolshed/routes/integrations/google-oauth/google-oauth.routes.ts
+++ b/packages/toolshed/routes/integrations/google-oauth/google-oauth.routes.ts
@@ -21,7 +21,8 @@ export const login = createRoute({
})
.openapi({
example: {
- authCellId: "auth-cell-123",
+ authCellId:
+ '{"/" : {"link-v0.1" : {"id" : "of:bafe...", "space" : "did:key:bafe...", "path" : ["path", "to", "value"]}}}',
integrationCharmId: "integration-charm-123",
},
}),
@@ -129,7 +130,8 @@ export const refresh = createRoute({
})
.openapi({
example: {
- authCellId: "auth-cell-123",
+ authCellId:
+ '{"/" : {"link-v0.1" : {"id" : "of:bafe...", "space" : "did:key:bafe...", "path" : ["path", "to", "value"]}}}',
},
}),
},
@@ -200,7 +202,8 @@ export const logout = createRoute({
})
.openapi({
example: {
- authCellId: "auth-cell-123",
+ authCellId:
+ '{"/" : {"link-v0.1" : {"id" : "of:bafe...", "space" : "did:key:bafe...", "path" : ["path", "to", "value"]}}}',
},
}),
},
diff --git a/packages/toolshed/routes/integrations/google-oauth/google-oauth.utils.ts b/packages/toolshed/routes/integrations/google-oauth/google-oauth.utils.ts
index 8f1f76b2a..a53aec4b7 100644
--- a/packages/toolshed/routes/integrations/google-oauth/google-oauth.utils.ts
+++ b/packages/toolshed/routes/integrations/google-oauth/google-oauth.utils.ts
@@ -1,6 +1,5 @@
import { OAuth2Client, Tokens } from "@cmd-johnson/oauth2-client";
import env from "@/env.ts";
-import { type CellLink } from "@commontools/runner";
import { runtime } from "@/index.ts";
import { Context } from "@hono/hono";
import { AuthSchema, Mutable, Schema } from "@commontools/runner";
@@ -144,18 +143,16 @@ export async function fetchUserInfo(accessToken: string): Promise {
}
// Helper function to get auth cell
-export async function getAuthCell(docLink: CellLink | string) {
+export async function getAuthCell(docLink: string) {
try {
// Parse string to docLink if needed
- const parsedDocLink = typeof docLink === "string"
- ? JSON.parse(docLink)
- : docLink;
+ const parsedDocLink = JSON.parse(docLink);
+
+ let authCell = runtime.getCellFromLink(parsedDocLink);
// We already should have the schema on the parsedDocLink (from our state),
// but if it's missing, we can add it here.
- parsedDocLink.schema = parsedDocLink.schema ?? AuthSchema;
-
- const authCell = runtime.getCellFromLink(parsedDocLink);
+ if (!authCell.schema) authCell = authCell.asSchema(AuthSchema);
// make sure the cell is live!
await runtime.storage.syncCell(authCell, true);
@@ -171,7 +168,7 @@ export async function getAuthCell(docLink: CellLink | string) {
export async function persistTokens(
oauthToken: OAuth2Tokens,
userInfo: UserInfo,
- authCellDocLink: string | CellLink,
+ authCellDocLink: string,
) {
try {
const authCell = await getAuthCell(authCellDocLink);
@@ -202,7 +199,7 @@ export async function persistTokens(
// Get tokens from the auth cell
export async function getTokensFromAuthCell(
- authCellDocLink: string | CellLink,
+ authCellDocLink: string,
) {
try {
const authCell = await getAuthCell(authCellDocLink);
@@ -279,7 +276,7 @@ export function createRefreshErrorResponse(
}
// Clears authentication data from the auth cell
-export async function clearAuthData(authCellDocLink: string | CellLink) {
+export async function clearAuthData(authCellDocLink: string) {
try {
const authCell = await getAuthCell(authCellDocLink);
diff --git a/packages/ui/src/v1/components/common-google-oauth.ts b/packages/ui/src/v1/components/common-google-oauth.ts
index b5570a622..7c84b7371 100644
--- a/packages/ui/src/v1/components/common-google-oauth.ts
+++ b/packages/ui/src/v1/components/common-google-oauth.ts
@@ -26,7 +26,7 @@ export class CommonGoogleOauthElement extends LitElement {
scopes: { type: Array },
};
- declare auth: Cell | undefined;
+ declare auth: Cell;
declare authStatus: string;
declare isLoading: boolean;
declare authResult: Record | null;
@@ -52,7 +52,7 @@ export class CommonGoogleOauthElement extends LitElement {
this.authStatus = "Initiating OAuth flow...";
this.authResult = null;
- const authCellId = JSON.stringify(this.auth?.getAsCellLink());
+ const authCellId = JSON.stringify(this.auth.getAsLink());
// `ct://${spaceDid}/${cellId}`
@@ -134,7 +134,7 @@ export class CommonGoogleOauthElement extends LitElement {
}
async handleLogout() {
- await this.auth?.set({
+ await this.auth.set({
token: "",
tokenType: "",
scope: [],
@@ -154,20 +154,20 @@ export class CommonGoogleOauthElement extends LitElement {
return html`
- ${this.auth?.get()?.user?.email && this.auth?.get()?.token
+ ${this.auth.get()?.user?.email && this.auth.get()?.token
? html`
-
-
${this.auth?.get()?.user?.name}
-
${this.auth?.get()?.user?.email}
+
${this.auth.get()?.user?.name}
+
${this.auth.get()?.user?.email}
`
: ""}
- ${this.auth?.get()?.token
+ ${this.auth.get()?.token
? html`