From fdeeb7a26f5b0b4456a1cee8e56a193dd28ae7d6 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 14 Jul 2025 17:40:59 -0700 Subject: [PATCH 01/19] feat(runner): support immutable cell data URIs (runner tests still broken) --- packages/runner/src/cell.ts | 73 +++++++++----------- packages/runner/src/data-updating.ts | 6 ++ packages/runner/src/link-utils.ts | 11 +-- packages/runner/src/runtime.ts | 28 ++++---- packages/runner/src/scheduler.ts | 2 +- packages/runner/test/cell.test.ts | 3 - packages/runner/test/link-resolution.test.ts | 2 + packages/runner/test/query.test.ts | 2 + 8 files changed, 58 insertions(+), 69 deletions(-) diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index 7666b2a7a..135a2774d 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -36,6 +36,7 @@ import { import { areLinksSame, isLink } from "./link-utils.ts"; import { type IRuntime } from "./runtime.ts"; import { type NormalizedFullLink } from "./link-utils.ts"; +import { getValueAtPath } from "./path-utils.ts"; import type { IExtendedStorageTransaction } from "./storage/interface.ts"; /** @@ -107,7 +108,6 @@ import type { IExtendedStorageTransaction } from "./storage/interface.ts"; * 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() * * @method getSourceCell Returns the source cell with optional schema. * @param {JSONSchema} schema - Optional schema to apply. @@ -199,7 +199,7 @@ declare module "@commontools/api" { ): SigilWriteRedirectLink; getDoc(): DocImpl; getRaw(): any; - setRaw(value: any): boolean; + setRaw(value: any): void; getSourceCell( schema?: JSONSchema, ): @@ -355,8 +355,10 @@ function createRegularCell( link: NormalizedFullLink, tx?: IExtendedStorageTransaction, ): Cell { - const doc = runtime.documentMap.getDocByEntityId(link.space, link.id); - const { path, schema, rootSchema } = link; + const doc = link.id.startsWith("data:") + ? undefined as unknown as DocImpl + : runtime.documentMap.getDocByEntityId(link.space, link.id); + const { space, path, schema, rootSchema } = link; const self = { get: () => validateAndTransform(runtime, tx, link), @@ -461,7 +463,7 @@ function createRegularCell( key: ? keyof S : keyof T>( valueKey: K, ): T extends Cell ? Cell : Cell => { - const childSchema = doc.runtime.cfc.getSchemaAtPath( + const childSchema = runtime.cfc.getSchemaAtPath( schema, [valueKey.toString()], rootSchema, @@ -498,7 +500,7 @@ function createRegularCell( ), getAsLegacyCellLink: (): LegacyDocCellLink => { return { - space: doc.space, + space: space, cell: doc, path: path as string[], schema, @@ -513,13 +515,7 @@ function createRegularCell( includeSchema?: boolean; }, ): SigilLink => { - return createSigilLink( - doc, - path, - schema, - rootSchema, - options, - ) as SigilLink; + return createSigilLink(link, options) as SigilLink; }, getAsWriteRedirectLink: ( options?: { @@ -528,17 +524,19 @@ function createRegularCell( includeSchema?: boolean; }, ): SigilWriteRedirectLink => { - return createSigilLink( - doc, - path, - schema, - rootSchema, - { ...options, overwrite: "redirect" }, - ) as SigilWriteRedirectLink; + return createSigilLink(link, { + ...options, + overwrite: "redirect", + }) as SigilWriteRedirectLink; }, getDoc: () => doc, - getRaw: () => doc.getAtPath(path as PropertyKey[]), - setRaw: (value: any) => doc.setAtPath(path as PropertyKey[], value), + getRaw: () => + (tx?.status().ok?.open ? tx : runtime.edit()) + .readValueOrThrow(link), + setRaw: (value: any) => { + if (!tx) throw new Error("Transaction required for setRaw"); + tx.writeValueOrThrow(link, value); + }, getSourceCell: (newSchema?: JSONSchema) => doc.sourceCell?.asCell([], newSchema, newSchema, tx) as Cell, setSourceCell: (sourceCell: Cell) => { @@ -562,7 +560,7 @@ function createRegularCell( }, get cellLink(): LegacyDocCellLink { return { - space: doc.space, + space: space, cell: doc, path: path as string[], schema, @@ -570,7 +568,7 @@ function createRegularCell( }; }, get space(): MemorySpace { - return doc.space; + return space; }, get entityId(): EntityId | undefined { return getEntityId({ cell: doc, path }); @@ -645,10 +643,7 @@ function subscribeToReferencedDocs( * Creates a sigil reference (link or alias) with shared logic */ function createSigilLink( - doc: DocImpl, - path: readonly PropertyKey[], - schema?: JSONSchema, - rootSchema?: JSONSchema, + link: NormalizedFullLink, options: { base?: Cell; baseSpace?: MemorySpace; @@ -660,7 +655,7 @@ function createSigilLink( const sigil: SigilLink = { "/": { [LINK_V1_TAG]: { - path: path.map((p) => p.toString()), + path: link.path.map((p) => p.toString()), }, }, }; @@ -669,26 +664,26 @@ function createSigilLink( // Handle base cell for relative references if (options.base) { - const baseDoc = options.base.getDoc(); + const baseLink = options.base.getAsNormalizedFullLink(); // Only include id if it's different from base - if (getEntityId(doc)!["/"] !== getEntityId(baseDoc)?.["/"]) { - reference.id = toURI(doc.entityId); - } + if (link.id !== baseLink.id) reference.id = toURI(link.id); // Only include space if it's different from base - if (doc.space && doc.space !== baseDoc.space) reference.space = doc.space; + if (link.space && link.space !== baseLink.space) { + reference.space = link.space; + } } else { - reference.id = toURI(doc.entityId); + reference.id = link.id; // Handle baseSpace option - only include space if different from baseSpace - if (doc.space !== options.baseSpace) reference.space = doc.space; + if (link.space !== options.baseSpace) reference.space = link.space; } // Include schema if requested - if (options.includeSchema && schema) { - reference.schema = schema; - reference.rootSchema = rootSchema; + if (options.includeSchema && link.schema) { + reference.schema = link.schema; + reference.rootSchema = link.rootSchema; } // Include overwrite if present and it's a redirect diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index c457a15fc..0b1b32b03 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -171,6 +171,12 @@ export function normalizeAndDiff( } if (isAnyCellLink(newValue)) { + const parsedLink = parseLink(newValue, link); + if (parsedLink.id.startsWith("data:")) { + // Use the tx code to make sure we read it the same way + const dataValue = runtime.edit().readValueOrThrow(parsedLink); + return normalizeAndDiff(runtime, tx, link, dataValue, context); + } if ( isAnyCellLink(currentValue) && areLinksSame(newValue, currentValue, link) diff --git a/packages/runner/src/link-utils.ts b/packages/runner/src/link-utils.ts index dec00217f..aad14a037 100644 --- a/packages/runner/src/link-utils.ts +++ b/packages/runner/src/link-utils.ts @@ -248,16 +248,7 @@ export function parseLink( // 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, - type: "application/json", - schema: value.schema, - rootSchema: value.rootSchema, - }; - } + if (isCell(value)) return value.getAsNormalizedFullLink(); if (isDoc(value)) { // Extract from DocImpl diff --git a/packages/runner/src/runtime.ts b/packages/runner/src/runtime.ts index bfa211589..36a7b2014 100644 --- a/packages/runner/src/runtime.ts +++ b/packages/runner/src/runtime.ts @@ -14,7 +14,7 @@ import type { IStorageProvider, MemorySpace, } from "./storage/interface.ts"; -import { type Cell, isCell } from "./cell.ts"; +import { type Cell, createCell } from "./cell.ts"; import { type LegacyDocCellLink } from "./sigil-types.ts"; import type { DocImpl } from "./doc.ts"; import { isDoc } from "./doc.ts"; @@ -540,17 +540,7 @@ export class Runtime implements IRuntime { ? cellLink : undefined; if (!link) throw new Error("Invalid cell link"); - doc = this.documentMap.getDocByEntityId( - link.space as MemorySpace, - getEntityId(link.id)!, - true, - )!; - return doc.asCell( - link.path, - schema ?? link.schema, - schema ? undefined : link.rootSchema, - tx, - ); + return createCell(this, link as NormalizedFullLink, tx); } } @@ -572,10 +562,16 @@ export class Runtime implements IRuntime { schema?: JSONSchema, tx?: IExtendedStorageTransaction, ): Cell { - const doc = this.documentMap.getDoc(data, { immutable: data }, space); - doc.freeze("immutable cell"); - doc.ephemeral = true; // Since soon these will be data: URIs - return doc.asCell([], schema, undefined, tx); + const dataURI = `data:application/json,${ + encodeURIComponent(JSON.stringify({ value: data })) + }` as `${string}:${string}`; + return createCell(this, { + space, + id: dataURI, + path: [], + schema, + type: "application/json", + }, tx); } // Convenience methods that delegate to the runner diff --git a/packages/runner/src/scheduler.ts b/packages/runner/src/scheduler.ts index f75d8116d..71be2b68c 100644 --- a/packages/runner/src/scheduler.ts +++ b/packages/runner/src/scheduler.ts @@ -124,7 +124,7 @@ export class Scheduler implements IScheduler { // Use runtime to get docs and call .updates this.cancels.set( action, - reads.map((addr) => { + reads.filter((addr) => !addr.id.startsWith("data:")).map((addr) => { const doc = this.runtime.documentMap.getDocByEntityId( addr.space, addr.id, diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index 5c4bcce5d..ce916ef1c 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -153,7 +153,6 @@ describe("Cell", () => { ); cell.set({ x: 1, y: 2 }); const result = cell.setRaw({ x: 10, y: 20 }); - expect(result).toBe(true); // setRaw returns boolean from doc.send() expect(cell.getRaw()).toEqual({ x: 10, y: 20 }); }); @@ -169,7 +168,6 @@ describe("Cell", () => { expect(cell.getRaw()).toBe(42); const result = cell.setRaw(100); - expect(result).toBe(true); expect(cell.getRaw()).toBe(100); }); @@ -185,7 +183,6 @@ describe("Cell", () => { expect(cell.getRaw()).toEqual([1, 2, 3]); const result = cell.setRaw([4, 5, 6]); - expect(result).toBe(true); expect(cell.getRaw()).toEqual([4, 5, 6]); }); diff --git a/packages/runner/test/link-resolution.test.ts b/packages/runner/test/link-resolution.test.ts index d495c5fca..754e48f48 100644 --- a/packages/runner/test/link-resolution.test.ts +++ b/packages/runner/test/link-resolution.test.ts @@ -107,6 +107,8 @@ describe("link-resolution", () => { const testCell = runtime.getCell( space, "should allow aliases in aliased paths 1", + undefined, + tx, ); testCell.setRaw({ a: { a: { $alias: { path: ["a", "b"] } }, b: { c: 1 } }, diff --git a/packages/runner/test/query.test.ts b/packages/runner/test/query.test.ts index 00a845879..33bde4ce4 100644 --- a/packages/runner/test/query.test.ts +++ b/packages/runner/test/query.test.ts @@ -343,6 +343,8 @@ describe("Query", () => { const testCell2 = runtime.getCell( space, `query test cell 2`, + undefined, + tx, ); testCell2.setRaw(docValue2); From 0be6421d948cbd8c043beb6707d9fe7ac54fd627 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Tue, 15 Jul 2025 10:11:17 -0700 Subject: [PATCH 02/19] various tx fixes in tests + nicer schemas for (still failing) runner tests --- packages/runner/src/runner.ts | 4 +-- packages/runner/test/cell.test.ts | 6 ++--- packages/runner/test/recipes.test.ts | 39 ++++++++++++++++++++-------- packages/runner/test/schema.test.ts | 2 ++ 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index ba77de7be..9f7e274c8 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -210,7 +210,7 @@ export class Runner implements IRunner { const recipeChanged = recipeId !== processCell.getRaw()?.[TYPE]; - processCell.setRaw({ + processCell.withTx(tx).setRaw({ ...processCell.getRaw(), [TYPE]: recipeId || "unknown", resultRef: resultCell.getAsLegacyCellLink(), @@ -232,7 +232,7 @@ export class Runner implements IRunner { if (recipeChanged) { // TODO(seefeld): Be smarter about merging in case result changed. But since // we don't yet update recipes, this isn't urgent yet. - resultCell.setRaw( + resultCell.withTx(tx).setRaw( unwrapOneLevelAndBindtoDoc(recipe.result as R, processCell), ); } diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index ce916ef1c..12def4b35 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -152,7 +152,7 @@ describe("Cell", () => { tx, ); cell.set({ x: 1, y: 2 }); - const result = cell.setRaw({ x: 10, y: 20 }); + cell.setRaw({ x: 10, y: 20 }); expect(cell.getRaw()).toEqual({ x: 10, y: 20 }); }); @@ -167,7 +167,7 @@ describe("Cell", () => { expect(cell.getRaw()).toBe(42); - const result = cell.setRaw(100); + cell.setRaw(100); expect(cell.getRaw()).toBe(100); }); @@ -182,7 +182,7 @@ describe("Cell", () => { expect(cell.getRaw()).toEqual([1, 2, 3]); - const result = cell.setRaw([4, 5, 6]); + cell.setRaw([4, 5, 6]); expect(cell.getRaw()).toEqual([4, 5, 6]); }); diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts index 65a3d40cc..4a6e889d2 100644 --- a/packages/runner/test/recipes.test.ts +++ b/packages/runner/test/recipes.test.ts @@ -166,12 +166,21 @@ describe("Recipe Runner", () => { }, ); - const resultCell = runtime.getCell< - { multiplied: { multiplied: number }[] } - >( + const resultCell = runtime.getCell( space, "should handle recipes with map nodes", - undefined, + { + type: "object", + properties: { + multiplied: { + type: "array", + items: { + type: "number", + properties: { multiplied: { type: "number" } }, + }, + }, + }, + } as const satisfies JSONSchema, tx, ); const result = runtime.run(tx, multipliedArray, { @@ -180,7 +189,7 @@ describe("Recipe Runner", () => { await runtime.idle(); - expect(result.getAsQueryResult()).toMatchObject({ + expect(result.get()).toMatchObject({ multiplied: [{ multiplied: 3 }, { multiplied: 12 }, { multiplied: 27 }], }); }); @@ -198,10 +207,15 @@ describe("Recipe Runner", () => { }, ); - const resultCell = runtime.getCell<{ doubled: number[] }>( + const resultCell = runtime.getCell( space, "should handle recipes with map nodes with closures", - undefined, + { + type: "object", + properties: { + doubled: { type: "array", items: { type: "number" } }, + }, + } as const satisfies JSONSchema, tx, ); const result = runtime.run(tx, doubleArray, { @@ -211,7 +225,7 @@ describe("Recipe Runner", () => { await runtime.idle(); - expect(result.getAsQueryResult()).toMatchObject({ + expect(result.get()).toMatchObject({ doubled: [3, 6, 9], }); }); @@ -227,10 +241,13 @@ describe("Recipe Runner", () => { }, ); - const resultCell = runtime.getCell<{ doubled: number[] }>( + const resultCell = runtime.getCell( space, "should handle map nodes with undefined input", - undefined, + { + type: "object", + properties: { values: { type: "array", items: { type: "number" } } }, + } as const satisfies JSONSchema, tx, ); const result = runtime.run(tx, doubleArray, { @@ -239,7 +256,7 @@ describe("Recipe Runner", () => { await runtime.idle(); - expect(result.getAsQueryResult()).toMatchObject({ doubled: [] }); + expect(result.get()).toMatchObject({ doubled: [] }); }); it("should execute handlers", async () => { diff --git a/packages/runner/test/schema.test.ts b/packages/runner/test/schema.test.ts index aeb341894..894c35123 100644 --- a/packages/runner/test/schema.test.ts +++ b/packages/runner/test/schema.test.ts @@ -74,6 +74,8 @@ describe("Schema Support", () => { }>( space, "allows mapping of fields via interim cells 2", + undefined, + tx, ); mappingCell.setRaw({ // as-is From d37c97f58d13a655db756167933d7e990150b659 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 08:22:47 -0700 Subject: [PATCH 03/19] refactor(runner): remove remaining direct uses of `doc` - Refactored core runner modules to eliminate direct references to the `doc` object. - Updated builder, builtins, cell, data-updating, doc-map, link-utils, query-result-proxy, recipe-binding, runner, runtime, scheduler, sigil-types, storage, storage/transaction-shim, and uri-utils to use new abstractions or access patterns. - Adjusted related tests to align with the new approach and ensure coverage. --- packages/runner/src/builder/factory.ts | 4 +- packages/runner/src/builtins/stream-data.ts | 4 - packages/runner/src/cell.ts | 79 ++++++------ packages/runner/src/data-updating.ts | 2 +- packages/runner/src/doc-map.ts | 7 +- packages/runner/src/index.ts | 3 +- packages/runner/src/link-utils.ts | 120 +----------------- packages/runner/src/query-result-proxy.ts | 20 +-- packages/runner/src/recipe-binding.ts | 9 +- packages/runner/src/runner.ts | 16 ++- packages/runner/src/runtime.ts | 14 +- packages/runner/src/scheduler.ts | 23 ++-- packages/runner/src/sigil-types.ts | 2 +- packages/runner/src/storage.ts | 4 +- packages/runner/src/uri-utils.ts | 6 +- packages/runner/test/cell.test.ts | 51 ++++++-- packages/runner/test/doc-map.test.ts | 6 +- packages/runner/test/link-resolution.test.ts | 6 +- packages/runner/test/link-utils.test.ts | 62 +-------- packages/runner/test/recipe-binding.test.ts | 16 ++- packages/runner/test/recipes.test.ts | 8 +- packages/runner/test/runner.test.ts | 8 +- packages/runner/test/schema-lineage.test.ts | 55 ++++---- packages/runner/test/schema.test.ts | 41 +++--- .../test/storage-transaction-shim.test.ts | 98 ++++++++------ packages/runner/test/storage.test.ts | 2 +- .../ui/src/v1/components/common-plaid-link.ts | 4 +- 27 files changed, 269 insertions(+), 401 deletions(-) diff --git a/packages/runner/src/builder/factory.ts b/packages/runner/src/builder/factory.ts index 80530138e..4f5c6d6ab 100644 --- a/packages/runner/src/builder/factory.ts +++ b/packages/runner/src/builder/factory.ts @@ -1,7 +1,6 @@ /** * Factory function to create builder functions with runtime dependency injection */ -import { getCellLinkOrThrow, type Runtime } from "../index.ts"; import type { BuilderFunctionsAndConstants, Cell, @@ -34,6 +33,7 @@ import { } from "./built-in.ts"; import { getRecipeEnvironment } from "./env.ts"; import type { RuntimeProgram } from "../harness/types.ts"; +import type { IRuntime } from "../runtime.ts"; /** * Creates a set of builder functions with the given runtime @@ -41,7 +41,7 @@ import type { RuntimeProgram } from "../harness/types.ts"; * @returns An object containing all builder functions */ export const createBuilder = ( - runtime: Runtime, + runtime: IRuntime, ): { commontools: BuilderFunctionsAndConstants; exportsCallback: (exports: Map) => void; diff --git a/packages/runner/src/builtins/stream-data.ts b/packages/runner/src/builtins/stream-data.ts index 540369c6b..30a889675 100644 --- a/packages/runner/src/builtins/stream-data.ts +++ b/packages/runner/src/builtins/stream-data.ts @@ -65,10 +65,6 @@ export function streamData( tx, ); - pending.getDoc().ephemeral = true; - result.getDoc().ephemeral = true; - error.getDoc().ephemeral = true; - pending.setSourceCell(parentCell); result.setSourceCell(parentCell); error.setSourceCell(parentCell); diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index 135a2774d..10bc78743 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -10,7 +10,6 @@ import { type Schema, } from "./builder/types.ts"; import { type DeepKeyLookup, type DocImpl } from "./doc.ts"; -import { getEntityId } from "./doc-map.ts"; import { createQueryResultProxy, type QueryResult, @@ -26,7 +25,6 @@ import { type Cancel, isCancel, useCancelGroup } from "./cancel.ts"; import { validateAndTransform } from "./schema.ts"; import { toURI } from "./uri-utils.ts"; import { - type LegacyDocCellLink, type LegacyJSONCellLink, LINK_V1_TAG, type SigilLink, @@ -36,8 +34,8 @@ import { import { areLinksSame, isLink } from "./link-utils.ts"; import { type IRuntime } from "./runtime.ts"; import { type NormalizedFullLink } from "./link-utils.ts"; -import { getValueAtPath } from "./path-utils.ts"; import type { IExtendedStorageTransaction } from "./storage/interface.ts"; +import { fromURI } from "./uri-utils.ts"; /** * This is the regular Cell interface, generated by DocImpl.asCell(). @@ -60,7 +58,7 @@ import type { IExtendedStorageTransaction } from "./storage/interface.ts"; * @returns {void} * * @method push Adds an item to the end of an array cell. - * @param {U | DocImpl | LegacyDocCellLink} value - The value to add, where U is + * @param {U | Cell} value - The value to add, where U is * the array element type. * @returns {void} * @@ -91,9 +89,6 @@ import type { IExtendedStorageTransaction } from "./storage/interface.ts"; * @param {ReactivityLog} log - Optional reactivity log. * @returns {QueryResult>} * - * @method getAsLegacyCellLink Returns a cell link for the cell (legacy format). - * @returns {LegacyDocCellLink} - * * @method getAsNormalizedFullLink Returns a normalized full link for the cell. * @returns {NormalizedFullLink} * @@ -181,7 +176,6 @@ declare module "@commontools/api" { path?: Readonly, tx?: IExtendedStorageTransaction, ): QueryResult>; - getAsLegacyCellLink(): LegacyDocCellLink; /** @deprecated */ getAsNormalizedFullLink(): NormalizedFullLink; getAsLink( options?: { @@ -232,9 +226,9 @@ declare module "@commontools/api" { schema?: JSONSchema; rootSchema?: JSONSchema; value: T; - cellLink: LegacyDocCellLink; + cellLink: SigilLink; space: MemorySpace; - entityId: EntityId; + entityId: { "/": string }; sourceURI: URI; path: readonly PropertyKey[]; [isCellMarker]: true; @@ -355,10 +349,8 @@ function createRegularCell( link: NormalizedFullLink, tx?: IExtendedStorageTransaction, ): Cell { - const doc = link.id.startsWith("data:") - ? undefined as unknown as DocImpl - : runtime.documentMap.getDocByEntityId(link.space, link.id); const { space, path, schema, rootSchema } = link; + let readOnly: string | undefined; const self = { get: () => validateAndTransform(runtime, tx, link), @@ -498,15 +490,6 @@ function createRegularCell( newTx ?? tx ?? runtime.edit(), { ...link, path: [...path, ...subPath.map((p) => p.toString())] }, ), - getAsLegacyCellLink: (): LegacyDocCellLink => { - return { - space: space, - cell: doc, - path: path as string[], - schema, - rootSchema, - }; - }, getAsNormalizedFullLink: () => link, getAsLink: ( options?: { @@ -529,7 +512,7 @@ function createRegularCell( overwrite: "redirect", }) as SigilWriteRedirectLink; }, - getDoc: () => doc, + getDoc: () => runtime.documentMap.getDocByEntityId(link.space, link.id), getRaw: () => (tx?.status().ok?.open ? tx : runtime.edit()) .readValueOrThrow(link), @@ -537,18 +520,38 @@ function createRegularCell( if (!tx) throw new Error("Transaction required for setRaw"); tx.writeValueOrThrow(link, value); }, - getSourceCell: (newSchema?: JSONSchema) => - doc.sourceCell?.asCell([], newSchema, newSchema, tx) as Cell, + getSourceCell: (newSchema?: JSONSchema) => { + const sourceCellId = (tx?.status().ok?.open ? tx : runtime.edit()) + .readOrThrow({ ...link, path: ["source"] }); + if (!sourceCellId) return undefined; + return createCell(runtime, { + space: link.space, + path: [], + id: toURI(sourceCellId), + type: "application/json", + schema: newSchema, + }, tx) as Cell; + }, setSourceCell: (sourceCell: Cell) => { - if (sourceCell.path.length > 0) { + if (!tx) throw new Error("Transaction required for setSourceCell"); + const sourceLink = sourceCell.getAsNormalizedFullLink(); + if (sourceLink.path.length > 0) { throw new Error("Source cell must have empty path for now"); } - doc.sourceCell = sourceCell.getDoc(); + tx.writeOrThrow({ ...link, path: ["source"] }, sourceLink.id); + }, + freeze: (reason: string) => { + readOnly = reason; + runtime.documentMap.getDocByEntityId(link.space, link.id)?.freeze(reason); }, - freeze: (reason: string) => doc.freeze(reason), - isFrozen: () => doc.isFrozen(), + isFrozen: () => !!readOnly, toJSON: (): LegacyJSONCellLink => // Keep old format for backward compatibility - ({ cell: doc.toJSON()!, path: path as (string | number)[] }), + ({ + cell: { + "/": (link.id.startsWith("data:") ? link.id : fromURI(link.id)), + }, + path: path as (string | number)[], + }), get runtime(): IRuntime { return runtime; }, @@ -558,23 +561,17 @@ function createRegularCell( get value(): T { return self.get(); }, - get cellLink(): LegacyDocCellLink { - return { - space: space, - cell: doc, - path: path as string[], - schema, - rootSchema, - }; + get cellLink(): SigilLink { + return createSigilLink(link); }, get space(): MemorySpace { return space; }, - get entityId(): EntityId | undefined { - return getEntityId({ cell: doc, path }); + get entityId(): { "/": string } { + return { "/": fromURI(link.id) }; }, get sourceURI(): URI { - return toURI(doc.entityId); + return link.id; }, get path(): readonly PropertyKey[] { return path; diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index 0b1b32b03..c324a888f 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -225,7 +225,7 @@ export function normalizeAndDiff( runtime, tx, link, - createSigilLinkFromParsedLink(newEntryLink), + createSigilLinkFromParsedLink(newEntryLink, link), context, ), // And see whether the value of the document itself changed diff --git a/packages/runner/src/doc-map.ts b/packages/runner/src/doc-map.ts index 509be2a2b..b44a56879 100644 --- a/packages/runner/src/doc-map.ts +++ b/packages/runner/src/doc-map.ts @@ -3,7 +3,7 @@ import { isRecord } from "@commontools/utils/types"; import { isOpaqueRef } from "./builder/types.ts"; import { createDoc, type DocImpl, isDoc } from "./doc.ts"; import { - getCellLinkOrThrow, + getCellOrThrow, isQueryResultForDereferencing, } from "./query-result-proxy.ts"; import { isCell } from "./cell.ts"; @@ -63,7 +63,7 @@ export function createRef( if (isQueryResultForDereferencing(obj)) { // It'll traverse this and call .toJSON on the doc in the reference. - obj = getCellLinkOrThrow(obj); + obj = getCellOrThrow(obj); } // If referencing other docs, return their ids (or random as fallback). @@ -93,9 +93,6 @@ export function getEntityId(value: any): { "/": string } | undefined { 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)); - } const link = parseLink(value); diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts index 81c9d0675..8fcc2fbc4 100644 --- a/packages/runner/src/index.ts +++ b/packages/runner/src/index.ts @@ -20,7 +20,7 @@ export type { IExtendedStorageTransaction } from "./storage/interface.ts"; export { isDoc } from "./doc.ts"; export { isCell, isStream } from "./cell.ts"; export { - getCellLinkOrThrow, + getCellOrThrow, isQueryResult, isQueryResultForDereferencing, } from "./query-result-proxy.ts"; @@ -44,7 +44,6 @@ export { isWriteRedirectLink, parseLink, parseLinkOrThrow, - parseToLegacyCellLink, } from "./link-utils.ts"; export * from "./recipe-manager.ts"; diff --git a/packages/runner/src/link-utils.ts b/packages/runner/src/link-utils.ts index aad14a037..7aa480777 100644 --- a/packages/runner/src/link-utils.ts +++ b/packages/runner/src/link-utils.ts @@ -16,7 +16,7 @@ import { import { toURI } from "./uri-utils.ts"; import { arrayEqual } from "./path-utils.ts"; import { - getCellLinkOrThrow, + getCellOrThrow, isQueryResultForDereferencing, QueryResultInternals, } from "./query-result-proxy.ts"; @@ -246,7 +246,7 @@ export function parseLink( ): 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 (isQueryResultForDereferencing(value)) value = getCellOrThrow(value); if (isCell(value)) return value.getAsNormalizedFullLink(); @@ -271,7 +271,7 @@ export function parseLink( // If no id provided, use base cell's document if (!id && base) { - id = isCell(base) ? toURI(base.getDoc().entityId) : base.id; + id = isCell(base) ? toURI(base.entityId) : base.id; } return { @@ -334,7 +334,7 @@ export function parseLink( // If no cell provided, use base cell's document if (!id && base) { - id = isCell(base) ? toURI(base.getDoc().entityId) : base.id; + id = isCell(base) ? toURI(base.entityId) : base.id; } return { @@ -366,114 +366,6 @@ export function parseLinkOrThrow( return result; } -/** - * Parse a link to a legacy CellLink format - * - * @deprecated Switch to parseLink instead. - * - * @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, -): LegacyDocCellLink; -export function parseToLegacyCellLink( - value: any, - baseCell?: Cell, -): LegacyDocCellLink | undefined; -export function parseToLegacyCellLink( - value: any, - baseCell?: Cell, -): LegacyDocCellLink | 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 LegacyDocCellLink; -} - -/** - * Parse a link to a legacy Alias format - * - * @deprecated Switch to parseLink instead. - * - * @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 as string[] ?? [], - space: link.space, - schema: link.schema, - rootSchema: link.rootSchema, - } satisfies Partial; -} - -/** - * Parse a normalized full link to a legacy doc cell link - * - * @deprecated - * - * @param link - The normalized full link to parse - * @param runtime - The runtime to use for getting the cell - * @returns The parsed legacy doc cell link - */ -export function parseNormalizedFullLinktoLegacyDocCellLink( - link: NormalizedFullLink, - runtime: IRuntime, -): LegacyDocCellLink { - const cell = runtime.getCellFromLink(link, undefined); - return { - cell: cell.getDoc(), - path: link.path as string[], - } satisfies LegacyDocCellLink; -} - /** * Compare two link values for equality, supporting all link formats */ @@ -524,6 +416,7 @@ export function areNormalizedLinksSame( export function createSigilLinkFromParsedLink( link: NormalizedLink, base?: Cell | NormalizedLink, + overwrite?: "redirect", ): SigilLink { const sigilLink: SigilLink = { "/": { @@ -531,6 +424,7 @@ export function createSigilLinkFromParsedLink( path: link.path as string[], schema: link.schema, rootSchema: link.rootSchema, + ...(overwrite === "redirect" ? { overwrite } : {}), }, }, }; @@ -542,7 +436,7 @@ export function createSigilLinkFromParsedLink( // Only add id if different from base const baseId = base - ? (isCell(base) ? toURI(base.getDoc().entityId) : base.id) + ? (isCell(base) ? toURI(base.entityId) : base.id) : undefined; if (link.id !== baseId) { sigilLink["/"][LINK_V1_TAG].id = link.id; diff --git a/packages/runner/src/query-result-proxy.ts b/packages/runner/src/query-result-proxy.ts index 097b39893..08b3a8b4b 100644 --- a/packages/runner/src/query-result-proxy.ts +++ b/packages/runner/src/query-result-proxy.ts @@ -7,6 +7,7 @@ import { type LegacyDocCellLink } from "./sigil-types.ts"; import { diffAndUpdate } from "./data-updating.ts"; import { resolveLinkToValue } from "./link-resolution.ts"; import { type NormalizedFullLink } from "./link-utils.ts"; +import { type Cell, createCell } from "./cell.ts"; import { type IRuntime } from "./runtime.ts"; import { type IExtendedStorageTransaction } from "./storage/interface.ts"; import { fromURI, toURI } from "./uri-utils.ts"; @@ -112,14 +113,7 @@ export function createQueryResultProxy( get: (target, prop, receiver) => { if (typeof prop === "symbol") { if (prop === getCellLink) { - return { - cell: runtime.documentMap.getDocByEntityId( - link.space, - link.id, - true, - ), - path: link.path as PropertyKey[], - } satisfies LegacyDocCellLink; + return createCell(runtime, link, tx, true); } else if (prop === toOpaqueRef) { return () => makeOpaqueRef(link); } @@ -322,13 +316,13 @@ export function makeOpaqueRef( } /** - * Get cell link or throw if not a cell value proxy. + * Get cell or throw if not a cell value proxy. * - * @param {any} value - The value to get the cell link from. - * @returns {LegacyDocCellLink} + * @param {any} value - The value to get the cell from. + * @returns {Cell} * @throws {Error} If the value is not a cell value proxy. */ -export function getCellLinkOrThrow(value: any): LegacyDocCellLink { +export function getCellOrThrow(value: any): Cell { if (isQueryResult(value)) return value[getCellLink]; else throw new Error("Value is not a cell proxy"); } @@ -359,7 +353,7 @@ export function isQueryResultForDereferencing( } export type QueryResultInternals = { - [getCellLink]: LegacyDocCellLink; + [getCellLink]: Cell; }; export type QueryResult = T & QueryResultInternals; diff --git a/packages/runner/src/recipe-binding.ts b/packages/runner/src/recipe-binding.ts index 5f252207a..0558b297b 100644 --- a/packages/runner/src/recipe-binding.ts +++ b/packages/runner/src/recipe-binding.ts @@ -38,7 +38,7 @@ export function sendValueToBinding( if (isLegacyAlias(binding)) { const ref = followWriteRedirects(tx, binding, cell); diffAndUpdate( - cell.getDoc().runtime, + cell.runtime, tx, ref, value as JSONValue, @@ -85,9 +85,8 @@ export function sendValueToBinding( */ export function unwrapOneLevelAndBindtoDoc( binding: T, - docOrCell: DocImpl | Cell, + cell: Cell, ): T { - const doc = isCell(docOrCell) ? docOrCell.getDoc() : docOrCell; function convert(binding: unknown): unknown { if (isLegacyAlias(binding)) { const alias = { ...binding.$alias }; @@ -100,7 +99,7 @@ export function unwrapOneLevelAndBindtoDoc( alias.cell = alias.cell - 1; } } else if (!alias.cell) { - alias.cell = doc; + alias.cell = cell.entityId; } return { $alias: alias }; } else if (isDoc(binding)) { @@ -156,7 +155,7 @@ export function findAllWriteRedirectCells( const link = parseLink(binding, baseCell); if (seen.find((s) => areNormalizedLinksSame(s, link))) return; seen.push(link); - const linkCell = baseCell.getDoc().runtime.getCellFromLink( + const linkCell = baseCell.runtime.getCellFromLink( link, undefined, baseCell.tx, diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index 9f7e274c8..c4cf946c6 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -32,16 +32,15 @@ import { import { followWriteRedirects } from "./link-resolution.ts"; import { areNormalizedLinksSame, + createSigilLinkFromParsedLink, isLink, isWriteRedirectLink, type NormalizedFullLink, 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 { isCell } from "./cell.ts"; import { LINK_V1_TAG, SigilLink } from "./sigil-types.ts"; import type { IRunner, IRuntime } from "./runtime.ts"; import type { IExtendedStorageTransaction } from "./storage/interface.ts"; @@ -103,7 +102,7 @@ export class Runner implements IRunner { let processCell: Cell; - const sourceCell = resultCell.getSourceCell(); + const sourceCell = resultCell.withTx(tx).getSourceCell(); if (sourceCell !== undefined) { processCell = sourceCell as Cell; } else { @@ -113,7 +112,7 @@ export class Runner implements IRunner { undefined, tx, ); - resultCell.getDoc().sourceCell = processCell.getDoc(); + resultCell.withTx(tx).setSourceCell(processCell); } let recipeId: string | undefined; @@ -181,7 +180,10 @@ export class Runner implements IRunner { // If the bindings are a cell, doc or doc link, convert them to an alias if (isLink(argument)) { - argument = parseToLegacyAlias(argument) as T; + argument = createSigilLinkFromParsedLink( + parseLink(argument), + processCell, + ) as T; } // Walk the recipe's schema and extract all default values @@ -213,7 +215,7 @@ export class Runner implements IRunner { processCell.withTx(tx).setRaw({ ...processCell.getRaw(), [TYPE]: recipeId || "unknown", - resultRef: resultCell.getAsLegacyCellLink(), + resultRef: resultCell.getAsLink({ base: processCell }), internal, ...(recipeId !== undefined) ? { spell: getSpellLink(recipeId) } : {}, }); @@ -826,7 +828,7 @@ export class Runner implements IRunner { tx, processCell, outputBindings, - resultCell.getAsLegacyCellLink(), + resultCell.getAsLink({ base: processCell }), ); // TODO(seefeld): Make sure to not cancel after a recipe is elevated to a // charm, e.g. via navigateTo. Nothing is cancelling right now, so leaving diff --git a/packages/runner/src/runtime.ts b/packages/runner/src/runtime.ts index 36a7b2014..926fc01ea 100644 --- a/packages/runner/src/runtime.ts +++ b/packages/runner/src/runtime.ts @@ -562,16 +562,10 @@ export class Runtime implements IRuntime { schema?: JSONSchema, tx?: IExtendedStorageTransaction, ): Cell { - const dataURI = `data:application/json,${ - encodeURIComponent(JSON.stringify({ value: data })) - }` as `${string}:${string}`; - return createCell(this, { - space, - id: dataURI, - path: [], - schema, - type: "application/json", - }, tx); + const doc = this.documentMap.getDoc(data, { immutable: data }, space); + doc.freeze("immutable cell"); + doc.ephemeral = true; // Since soon these will be data: URIs + return doc.asCell([], schema, undefined, tx); } // Convenience methods that delegate to the runner diff --git a/packages/runner/src/scheduler.ts b/packages/runner/src/scheduler.ts index 71be2b68c..2a807c110 100644 --- a/packages/runner/src/scheduler.ts +++ b/packages/runner/src/scheduler.ts @@ -3,7 +3,7 @@ import { getTopFrame } from "./builder/recipe.ts"; import { TYPE } from "./builder/types.ts"; import type { Cancel } from "./cancel.ts"; import { - getCellLinkOrThrow, + getCellOrThrow, isQueryResultForDereferencing, } from "./query-result-proxy.ts"; import { ConsoleEvent } from "./harness/console.ts"; @@ -532,14 +532,21 @@ function getCharmMetadataFromFrame(): { return; } const result: ReturnType = {}; - const { cell: source } = getCellLinkOrThrow(sourceAsProxy); - const spellLink = parseLink(source?.get()?.["spell"]); - result.spellId = spellLink?.id; - result.recipeId = source?.get()?.[TYPE]; - const resultDoc = source?.get()?.resultRef?.cell; - result.space = resultDoc?.space; + const source = getCellOrThrow(sourceAsProxy).asSchema({ + type: "object", + properties: { + [TYPE]: { type: "string" }, + resultRef: { + type: "object", + asCell: true, + }, + }, + }); + result.recipeId = source.get()?.[TYPE]; + const resultCell = source.get()?.resultRef; + result.space = source.space; result.charmId = JSON.parse( - JSON.stringify(resultDoc?.entityId ?? {}), + JSON.stringify(resultCell?.entityId ?? {}), )["/"]; return result; } diff --git a/packages/runner/src/sigil-types.ts b/packages/runner/src/sigil-types.ts index 409a396f4..067b54435 100644 --- a/packages/runner/src/sigil-types.ts +++ b/packages/runner/src/sigil-types.ts @@ -70,7 +70,7 @@ export type LegacyDocCellLink = { */ export type LegacyAlias = { $alias: { - cell?: DocImpl | ShadowRef | number; + cell?: DocImpl | ShadowRef | number | { "/": string }; path: readonly PropertyKey[]; schema?: JSONSchema; rootSchema?: JSONSchema; diff --git a/packages/runner/src/storage.ts b/packages/runner/src/storage.ts index d73ffc238..ce1931ca6 100644 --- a/packages/runner/src/storage.ts +++ b/packages/runner/src/storage.ts @@ -21,7 +21,7 @@ import type { IRuntime, IStorage } from "./runtime.ts"; import { DocObjectManager, querySchema } from "./storage/query.ts"; import { deepEqual } from "./path-utils.ts"; import { - getCellLinkOrThrow, + getCellOrThrow, isQueryResultForDereferencing, } from "./query-result-proxy.ts"; import { isLink } from "./link-utils.ts"; @@ -359,7 +359,7 @@ export class Storage implements IStorage { // link. Roundtripping through JSON converts all Cells and Docs to a // serializable format. if (isQueryResultForDereferencing(value)) { - value = getCellLinkOrThrow(value); + value = getCellOrThrow(value); } return JSON.parse(JSON.stringify(value)); } else if (isRecord(value)) { diff --git a/packages/runner/src/uri-utils.ts b/packages/runner/src/uri-utils.ts index e1ee03506..e3276aa7a 100644 --- a/packages/runner/src/uri-utils.ts +++ b/packages/runner/src/uri-utils.ts @@ -20,10 +20,10 @@ export function toURI(value: unknown): URI { throw new Error(`Invalid URI: ${value}`); } return value as URI; + } else { + // Add "of:" prefix + return `of:${value}`; } - - // Add "of:" prefix - return `of:${value}`; } throw new Error(`Cannot convert value to URI: ${JSON.stringify(value)}`); diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index 12def4b35..cb54b87cb 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -249,9 +249,9 @@ describe("Cell utility functions", () => { tx, ); c.set({ x: 10 }); - const ref = c.key("x").getAsLegacyCellLink(); - expect(isLegacyCellLink(ref)).toBe(true); - expect(isLegacyCellLink({})).toBe(false); + const ref = c.key("x").getAsLink(); + expect(isAnyCellLink(ref)).toBe(true); + expect(isAnyCellLink({})).toBe(false); }); it("should identify a cell proxy", () => { @@ -366,7 +366,7 @@ describe("createProxy", () => { tx, ); c.set({ x: 42 }); - const ref = c.key("x").getAsLegacyCellLink(); + const ref = c.key("x").getAsLink(); const proxy = c.getAsQueryResult(); proxy.y = ref; expect(proxy.y).toBe(42); @@ -380,7 +380,7 @@ describe("createProxy", () => { tx, ); c.set({ x: 42 }); - const ref = c.key("x").getAsLegacyCellLink(); + const ref = c.key("x").getAsLink(); const proxy = c.getAsQueryResult(); proxy.x = ref; expect(proxy.x).toBe(42); @@ -583,9 +583,9 @@ describe("createProxy", () => { proxy.length = 2; expect(c.get()).toEqual([1, 2]); const log = txToReactivityLog(tx); - expect(areLinksSame(log.writes[0], c.key("length").getAsLegacyCellLink())) + expect(areLinksSame(log.writes[0], c.key("length").getAsLink())) .toBe(true); - expect(areLinksSame(log.writes[1], c.key(2).getAsLegacyCellLink())).toBe( + expect(areLinksSame(log.writes[1], c.key(2).getAsLink())).toBe( true, ); proxy.length = 4; @@ -1309,8 +1309,8 @@ describe("asCell with schema", () => { tx, ); innerCell.set({ value: 42 }); - const cellRef = innerCell.getAsLegacyCellLink(); - const aliasRef = { $alias: innerCell.getAsLegacyCellLink() }; + const cellRef = innerCell.getAsLink(); + const aliasRef = innerCell.getAsWriteRedirectLink(); // Create a cell that uses all reference types const c = runtime.getCell<{ @@ -2202,3 +2202,36 @@ describe("getAsWriteRedirectLink method", () => { expect(alias["/"][LINK_V1_TAG].space).toBeUndefined(); }); }); + +describe("getImmutableCell", () => { + describe("asCell", () => { + let storageManager: ReturnType; + let runtime: Runtime; + let tx: IExtendedStorageTransaction; + + beforeEach(() => { + storageManager = StorageManager.emulate({ as: signer }); + + runtime = new Runtime({ + blobbyServerUrl: import.meta.url, + storageManager, + }); + tx = runtime.edit(); + }); + + afterEach(async () => { + await tx.commit(); + await runtime?.dispose(); + await storageManager?.close(); + }); + + it("should create a cell with the correct schema", () => { + const schema = { + type: "object", + properties: { value: { type: "number" } }, + } as const satisfies JSONSchema; + const cell = runtime.getImmutableCell(space, { value: 42 }, schema, tx); + expect(cell.get()).toEqual({ value: 42 }); + }); + }); +}); diff --git a/packages/runner/test/doc-map.test.ts b/packages/runner/test/doc-map.test.ts index 8491340fd..8f389f449 100644 --- a/packages/runner/test/doc-map.test.ts +++ b/packages/runner/test/doc-map.test.ts @@ -64,7 +64,7 @@ describe("cell-map", () => { expect(getEntityId(cell)).toEqual(id); expect(getEntityId(cell.getAsQueryResult())).toEqual(id); - expect(getEntityId(cell.getAsLegacyCellLink())).toEqual(id); + expect(getEntityId(cell.getAsLink())).toEqual(id); }); it("should return a different entity ID for reference with paths", () => { @@ -80,13 +80,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").getAsLegacyCellLink())).not.toEqual(id); + expect(getEntityId(c.key("foo").getAsLink())).not.toEqual(id); expect(getEntityId(c.getAsQueryResult(["foo"]))).toEqual( getEntityId(c.key("foo")), ); expect(getEntityId(c.getAsQueryResult(["foo"]))).toEqual( - getEntityId(c.key("foo").getAsLegacyCellLink()), + getEntityId(c.key("foo").getAsLink()), ); }); }); diff --git a/packages/runner/test/link-resolution.test.ts b/packages/runner/test/link-resolution.test.ts index 754e48f48..ba3809388 100644 --- a/packages/runner/test/link-resolution.test.ts +++ b/packages/runner/test/link-resolution.test.ts @@ -61,7 +61,7 @@ describe("link-resolution", () => { tx, ); outerCell.setRaw({ - outer: { $alias: innerCell.key("inner").getAsLegacyCellLink() }, + outer: innerCell.key("inner").getAsWriteRedirectLink(), }); const binding = { $alias: { path: ["outer"] } }; const result = followWriteRedirects(tx, binding, outerCell); @@ -92,10 +92,10 @@ describe("link-resolution", () => { ); cellB.set({}); cellA.setRaw({ - alias: { $alias: cellB.key("alias").getAsLegacyCellLink() }, + alias: cellB.key("alias").getAsWriteRedirectLink(), }); cellB.setRaw({ - alias: { $alias: cellA.key("alias").getAsLegacyCellLink() }, + alias: cellA.key("alias").getAsWriteRedirectLink(), }); const binding = { $alias: { path: ["alias"] } }; expect(() => followWriteRedirects(tx, binding, cellA)).toThrow( diff --git a/packages/runner/test/link-utils.test.ts b/packages/runner/test/link-utils.test.ts index 44240fb09..952bf2879 100644 --- a/packages/runner/test/link-utils.test.ts +++ b/packages/runner/test/link-utils.test.ts @@ -10,7 +10,6 @@ import { 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"; @@ -129,7 +128,7 @@ describe("link-utils", () => { it("should identify cell links as links", () => { const cell = runtime.getCell(space, "test", undefined, tx); - const cellLink = cell.getAsLegacyCellLink(); + const cellLink = cell.getAsLink(); expect(isLink(cellLink)).toBe(true); }); @@ -351,7 +350,7 @@ describe("link-utils", () => { it("should parse cell links to normalized links", () => { const cell = runtime.getCell(space, "test"); - const cellLink = cell.getAsLegacyCellLink(); + const cellLink = cell.getAsLink(); const result = parseLink(cellLink); expect(result).toEqual({ @@ -444,57 +443,6 @@ describe("link-utils", () => { }); }); - 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"); @@ -503,14 +451,14 @@ describe("link-utils", () => { it("should return true for equivalent links", () => { const cell = runtime.getCell(space, "test"); - const cellLink1 = cell.getAsLegacyCellLink(); - const cellLink2 = cell.getAsLegacyCellLink(); + const cellLink1 = cell.getAsLink(); + const cellLink2 = cell.getAsLink(); 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 cellLink = cell.getAsWriteRedirectLink(); const sigilLink = cell.getAsLink(); expect(areLinksSame(cellLink, sigilLink)).toBe(true); }); diff --git a/packages/runner/test/recipe-binding.test.ts b/packages/runner/test/recipe-binding.test.ts index 45c0ad147..43e7bc798 100644 --- a/packages/runner/test/recipe-binding.test.ts +++ b/packages/runner/test/recipe-binding.test.ts @@ -142,14 +142,18 @@ describe("recipe-binding", () => { const result = unwrapOneLevelAndBindtoDoc(binding, testCell); expect( - areLinksSame(result.x, { - $alias: testCell.key("a").getAsLegacyCellLink(), - }), + areLinksSame( + result.x, + testCell.key("a").getAsWriteRedirectLink(), + testCell, + ), ).toBe(true); expect( - areLinksSame(result.y, { - $alias: testCell.key("b").key("c").getAsLegacyCellLink(), - }), + areLinksSame( + result.y, + testCell.key("b").key("c").getAsWriteRedirectLink(), + testCell, + ), ).toBe(true); }); }); diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts index 4a6e889d2..323810886 100644 --- a/packages/runner/test/recipes.test.ts +++ b/packages/runner/test/recipes.test.ts @@ -175,7 +175,7 @@ describe("Recipe Runner", () => { multiplied: { type: "array", items: { - type: "number", + type: "object", properties: { multiplied: { type: "number" } }, }, }, @@ -939,8 +939,8 @@ 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).toEqual( + JSON.parse(JSON.stringify(charm.getSourceCell()?.getDoc())), ); expect(charm.getAsQueryResult()).toMatchObject({ result: 5 }); }); @@ -982,6 +982,8 @@ describe("Recipe Runner", () => { await runtime.idle(); expect(timeoutCalled).toBe(true); + console.log("raw", result.getRaw()); + console.log("get", result.get()); expect(result.get()).toMatchObject({ result: 2 }); }); diff --git a/packages/runner/test/runner.test.ts b/packages/runner/test/runner.test.ts index 56b21936e..746d404bb 100644 --- a/packages/runner/test/runner.test.ts +++ b/packages/runner/test/runner.test.ts @@ -69,7 +69,7 @@ describe("runRecipe", () => { output: { $alias: { path: ["internal", "output"], - cell: result.getSourceCell()?.getDoc(), + cell: JSON.parse(JSON.stringify(result.getSourceCell()?.getDoc())), }, }, }); @@ -767,16 +767,16 @@ describe("runner utils", () => { tx, ); const obj1 = { a: { $alias: { path: [] } } }; - const obj2 = { a: 2, b: { c: testCell.getAsLegacyCellLink() } }; + const obj2 = { a: 2, b: { c: testCell.getAsLink() } }; const obj3 = { - a: { $alias: testCell.key("a").getAsLegacyCellLink() }, + a: testCell.key("a").getAsWriteRedirectLink(), b: { c: 4 }, }; const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: { $alias: { path: [] } }, - b: { c: testCell.getAsLegacyCellLink() }, + b: { c: testCell.getAsLink() }, }); }); }); diff --git a/packages/runner/test/schema-lineage.test.ts b/packages/runner/test/schema-lineage.test.ts index 7f7e4b9bc..1298ee0c3 100644 --- a/packages/runner/test/schema-lineage.test.ts +++ b/packages/runner/test/schema-lineage.test.ts @@ -64,13 +64,11 @@ describe("Schema Lineage", () => { undefined, tx, ); - sourceCell.setRaw({ - $alias: { - ...targetCell.getAsLegacyCellLink(), - schema, - rootSchema: schema, - }, - }); + sourceCell.setRaw( + targetCell.asSchema(schema).getAsWriteRedirectLink({ + includeSchema: true, + }), + ); // Access the cell without providing a schema // (Type script type is just to avoid compiler errors) @@ -121,13 +119,11 @@ describe("Schema Lineage", () => { undefined, tx, ); - sourceCell.setRaw({ - $alias: { - ...targetCell.getAsLegacyCellLink(), - schema: aliasSchema, - rootSchema: aliasSchema, - }, - }); + sourceCell.setRaw( + targetCell.asSchema(aliasSchema).getAsWriteRedirectLink({ + includeSchema: true, + }), + ); // Access the cell with explicit schema const cell = sourceCell.asSchema(explicitSchema); @@ -155,7 +151,7 @@ describe("Schema Lineage", () => { valueCell.set({ count: 5, name: "test" }); // Create a schema for our first level alias - const numberSchema = { type: "number" }; + const numberSchema = { type: "number" } as const satisfies JSONSchema; // Create a cell with an alias specifically for the count field const countCell = runtime.getCell( @@ -164,13 +160,11 @@ describe("Schema Lineage", () => { undefined, tx, ); - countCell.setRaw({ - $alias: { - ...valueCell.key("count").getAsLegacyCellLink(), - schema: numberSchema, - rootSchema: numberSchema, - }, - }); + countCell.setRaw( + valueCell.key("count").asSchema(numberSchema).getAsWriteRedirectLink({ + includeSchema: true, + }), + ); // Create a third level of aliasing const finalCell = runtime.getCell( @@ -179,9 +173,9 @@ describe("Schema Lineage", () => { undefined, tx, ); - finalCell.setRaw({ - $alias: countCell.getAsLegacyCellLink(), - }); + finalCell.setRaw(countCell.getAsWriteRedirectLink({ + includeSchema: true, + })); // Access the cell without providing a schema const cell = finalCell.asSchema(); @@ -228,12 +222,11 @@ describe("Schema Lineage", () => { undefined, tx, ); - itemsCell.setRaw({ - $alias: { - ...nestedCell.key("items").getAsLegacyCellLink(), - schema: arraySchema, - }, - }); + itemsCell.setRaw( + nestedCell.key("items").asSchema(arraySchema).getAsWriteRedirectLink({ + includeSchema: true, + }), + ); // Access the items with a schema that specifies array items should be cells const itemsCellWithSchema = itemsCell.asSchema( diff --git a/packages/runner/test/schema.test.ts b/packages/runner/test/schema.test.ts index 894c35123..2c9d40794 100644 --- a/packages/runner/test/schema.test.ts +++ b/packages/runner/test/schema.test.ts @@ -79,13 +79,13 @@ describe("Schema Support", () => { ); mappingCell.setRaw({ // as-is - id: c.key("id").getAsLegacyCellLink(), + id: c.key("id").getAsLink(), // turn single value to set - changes: [c.key("metadata").key("createdAt").getAsLegacyCellLink()], + changes: [c.key("metadata").key("createdAt").getAsLink()], // rename field and uplift from nested element - kind: c.key("metadata").key("type").getAsLegacyCellLink(), + kind: c.key("metadata").key("type").getAsLink(), // turn set into a single value - tag: c.key("tags").key(0).getAsLegacyCellLink(), + tag: c.key("tags").key(0).getAsLink(), }); // This schema is how the recipient specifies what they want @@ -268,7 +268,7 @@ describe("Schema Support", () => { undefined, tx, ); - linkCell.setRaw(initial.getAsLegacyCellLink()); + linkCell.setRaw(initial.getAsLink()); const linkEntityId = linkCell.entityId!; const docCell = runtime.getCell<{ @@ -282,7 +282,7 @@ describe("Schema Support", () => { ); docCell.setRaw({ value: "root", - current: { $alias: linkCell.key("foo").getAsLegacyCellLink() }, + current: linkCell.key("foo").getAsWriteRedirectLink(), }); const root = docCell.asSchema(schema); @@ -372,7 +372,7 @@ describe("Schema Support", () => { tx, ); second.set({ foo: { label: "second" } }); - linkCell.setRaw(second.getAsLegacyCellLink()); + linkCell.setRaw(second.getAsLink()); await runtime.idle(); @@ -415,9 +415,7 @@ describe("Schema Support", () => { tx, ); third.set({ label: "third" }); - docCell.key("current").setRaw({ - $alias: third.getAsLegacyCellLink(), - }); + docCell.key("current").setRaw(third.getAsWriteRedirectLink()); await runtime.idle(); @@ -717,9 +715,9 @@ describe("Schema Support", () => { }); // Set up circular references using cell links - 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()); + c.key("parent").setRaw(c.getAsLink()); + c.key("children").key(0).key("parent").setRaw(c.getAsLink()); + c.key("children").key(1).key("parent").setRaw(c.getAsLink()); const schema = { type: "object", @@ -776,10 +774,10 @@ describe("Schema Support", () => { // Set up circular references using cell links c.key("nested").key("items").key(0).key("value").setRaw( - c.getAsLegacyCellLink(), + c.getAsLink(), ); c.key("nested").key("items").key(1).key("value").setRaw( - c.key("nested").getAsLegacyCellLink(), + c.key("nested").getAsLink(), ); const schema = { @@ -849,7 +847,7 @@ describe("Schema Support", () => { }); // Set up circular references using cell links - c.key("children").key(1).key("value").setRaw(c.getAsLegacyCellLink()); + c.key("children").key(1).key("value").setRaw(c.getAsLink()); const schema = { type: "object", @@ -1344,17 +1342,14 @@ describe("Schema Support", () => { ); childrenArrayCell.set([ { type: "text", value: "hello" }, - innerTextCell.getAsLegacyCellLink(), + innerTextCell.getAsLink(), ]); const withLinks = runtime.getCell<{ type: string; name: string; props: { - style: { - cell: any; - path: any[]; - }; + style: any; }; children: any[]; }>( @@ -1367,11 +1362,11 @@ describe("Schema Support", () => { type: "vnode", name: "div", props: { - style: styleCell.getAsLegacyCellLink(), + style: styleCell, }, children: [ { type: "text", value: "single" }, - childrenArrayCell.getAsLegacyCellLink(), + childrenArrayCell, "or just text", ], }); diff --git a/packages/runner/test/storage-transaction-shim.test.ts b/packages/runner/test/storage-transaction-shim.test.ts index a8e1f74fe..992a77efe 100644 --- a/packages/runner/test/storage-transaction-shim.test.ts +++ b/packages/runner/test/storage-transaction-shim.test.ts @@ -417,55 +417,69 @@ describe("StorageTransaction", () => { }); }); - it("should support readValueOrThrow for value, not found, and error cases", () => { - const transaction = runtime.edit(); + describe("readValueOrThrow and writeValueOrThrow", () => { + it("should support readValueOrThrow for value, not found, and error cases", () => { + const transaction = runtime.edit(); - // Write a value - const writeResult = transaction.write({ - space, - id: "of:test-entity", - type: "application/json", - path: ["value"], - }, { foo: 123 }); - expect(writeResult.ok).toBeDefined(); - expect(writeResult.error).toBeUndefined(); + // Write a value + const writeResult = transaction.write({ + space, + id: "of:test-entity", + type: "application/json", + path: ["value"], + }, { foo: 123 }); + expect(writeResult.ok).toBeDefined(); + expect(writeResult.error).toBeUndefined(); - // Should return the value for an existing path - const result = transaction.read({ - space, - id: "of:test-entity", - type: "application/json", - path: ["value", "foo"], - }); - expect(result.ok?.value).toBe(123); + // Should return the value for an existing path + const result = transaction.read({ + space, + id: "of:test-entity", + type: "application/json", + path: ["value", "foo"], + }); + expect(result.ok?.value).toBe(123); - // Should return the value for an existing path - const value = transaction.readOrThrow({ - space, - id: "of:test-entity", - type: "application/json", - path: ["value", "foo"], + // Should return the value for an existing path + const value = transaction.readOrThrow({ + space, + id: "of:test-entity", + type: "application/json", + path: ["value", "foo"], + }); + expect(value).toBe(123); + + // Should return undefined for a non-existent path (NotFoundError) + const notFound = transaction.readOrThrow({ + space, + id: "of:test-entity", + type: "application/json", + path: ["value", "bar"], + }); + expect(notFound).toBeUndefined(); + + // Should throw for other errors (e.g., unsupported media type) + expect(() => + transaction.readOrThrow({ + space, + id: "of:test-entity", + type: "unsupported/type", + path: ["value"], + }) + ).toThrow("Unsupported media type"); }); - expect(value).toBe(123); + }); - // Should return undefined for a non-existent path (NotFoundError) - const notFound = transaction.readOrThrow({ + it("should support writeValueOrThrow into a new document", async () => { + const transaction = runtime.edit(); + transaction.writeValueOrThrow({ space, - id: "of:test-entity", + id: "of:test-entity-new", type: "application/json", - path: ["value", "bar"], - }); - expect(notFound).toBeUndefined(); - - // Should throw for other errors (e.g., unsupported media type) - expect(() => - transaction.readOrThrow({ - space, - id: "of:test-entity", - type: "unsupported/type", - path: ["value"], - }) - ).toThrow("Unsupported media type"); + path: ["value"], + }, { foo: 123 }); + const result = await transaction.commit(); + expect(result.ok).toBeDefined(); }); }); diff --git a/packages/runner/test/storage.test.ts b/packages/runner/test/storage.test.ts index 8919490ed..699777ff0 100644 --- a/packages/runner/test/storage.test.ts +++ b/packages/runner/test/storage.test.ts @@ -74,7 +74,7 @@ describe("Storage", () => { const testValue = { data: "test", - ref: refCell.getAsLegacyCellLink(), + ref: refCell.getAsLink(), }; testCell.send(testValue); diff --git a/packages/ui/src/v1/components/common-plaid-link.ts b/packages/ui/src/v1/components/common-plaid-link.ts index ccaa07bf7..638fd142c 100644 --- a/packages/ui/src/v1/components/common-plaid-link.ts +++ b/packages/ui/src/v1/components/common-plaid-link.ts @@ -115,7 +115,7 @@ export class CommonPlaidLinkElement extends LitElement { this.isLoading = true; this.authStatus = "Creating link session..."; - const authCellId = JSON.stringify(this.auth?.getAsLegacyCellLink()); + const authCellId = JSON.stringify(this.auth?.getAsLink()); const container = CommonCharmElement.findCharmContainer(this); if (!container) { @@ -264,7 +264,7 @@ export class CommonPlaidLinkElement extends LitElement { this.isLoading = true; this.authStatus = "Removing bank connection..."; - const authCellId = JSON.stringify(this.auth?.getAsLegacyCellLink()); + const authCellId = JSON.stringify(this.auth?.getAsLink()); try { const response = await fetch( From f051c76003e5c858cc453a6c6e74702f0e824944 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 09:16:10 -0700 Subject: [PATCH 04/19] minor cleanups and added comments --- packages/runner/src/data-updating.ts | 9 +++++- packages/runner/src/runner.ts | 17 +++++------ packages/runner/src/runtime.ts | 43 ++++++---------------------- packages/runner/test/recipes.test.ts | 6 ++++ 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index c324a888f..24916d633 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -173,7 +173,14 @@ export function normalizeAndDiff( if (isAnyCellLink(newValue)) { const parsedLink = parseLink(newValue, link); if (parsedLink.id.startsWith("data:")) { - // Use the tx code to make sure we read it the same way + // If there is a data link treat it as writing it's contents instead. + + // TODO(seefeld): If the data url had a space different than the + // destination, the expectation would be that links inside of it would be + // resolved against that space as base. This is currently broken (and + // might also be broken for other relative links) + + // Use the tx code to make sure we read it the same way const dataValue = runtime.edit().readValueOrThrow(parsedLink); return normalizeAndDiff(runtime, tx, link, dataValue, context); } diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index c4cf946c6..2bed36126 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -562,9 +562,7 @@ export class Runner implements IRunner { tx, }); - const argument = module.argumentSchema - ? inputsCell.asSchema(module.argumentSchema).get() - : inputsCell.getAsQueryResult([], undefined); + const argument = inputsCell.asSchema(module.argumentSchema).get(); const result = fn(argument); const postRun = (result: any) => { @@ -621,9 +619,8 @@ export class Runner implements IRunner { let previousResultRecipeAsString: string | undefined; const action: Action = (tx: IExtendedStorageTransaction) => { - const argument = module.argumentSchema - ? inputsCell.asSchema(module.argumentSchema).withTx(tx).get() - : inputsCell.getAsQueryResult([], tx); + const argument = inputsCell.asSchema(module.argumentSchema).withTx(tx) + .get(); const frame = pushFrameFromCause( { inputs, outputs, fn: fn.toString() }, @@ -650,7 +647,7 @@ export class Runner implements IRunner { if (previousResultRecipeAsString === resultRecipeAsString) return; previousResultRecipeAsString = resultRecipeAsString; - const resultDoc = this.run( + const resultCell = this.run( tx, resultRecipe, undefined, @@ -662,15 +659,15 @@ export class Runner implements IRunner { tx, ), ); - addCancel(() => this.stop(resultDoc)); + addCancel(() => this.stop(resultCell)); if (!previousResultDoc) { - previousResultDoc = resultDoc; + previousResultDoc = resultCell; sendValueToBinding( tx, processCell, outputs, - resultDoc.getAsLink(), + resultCell.getAsLink({ base: processCell }), ); } } else { diff --git a/packages/runner/src/runtime.ts b/packages/runner/src/runtime.ts index 926fc01ea..0400d6288 100644 --- a/packages/runner/src/runtime.ts +++ b/packages/runner/src/runtime.ts @@ -507,41 +507,14 @@ export class Runtime implements IRuntime { schema?: JSONSchema, tx?: IExtendedStorageTransaction, ): Cell { - let doc; - - if (isLegacyCellLink(cellLink)) { - const link = cellLink as LegacyDocCellLink; - if (isDoc(cellLink.cell)) { - doc = cellLink.cell; - } else if (link.space) { - doc = this.documentMap.getDocByEntityId( - link.space, - getEntityId(cellLink.cell)!, - true, - )!; - if (!doc) { - throw new Error(`Can't find ${link.space}/${link.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( - link.path, - schema ?? link.schema, - schema ? undefined : link.rootSchema, - tx, - ); - } else { - const link = isLink(cellLink) - ? parseLink(cellLink) - : isNormalizedFullLink(cellLink) - ? cellLink - : undefined; - if (!link) throw new Error("Invalid cell link"); - return createCell(this, link as NormalizedFullLink, tx); - } + let link = isLink(cellLink) + ? parseLink(cellLink) + : isNormalizedFullLink(cellLink) + ? cellLink + : undefined; + if (!link) throw new Error("Invalid cell link"); + if (schema) link = { ...link, schema, rootSchema: schema }; + return createCell(this, link as NormalizedFullLink, tx); } getImmutableCell( diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts index 323810886..ef9a2df30 100644 --- a/packages/runner/test/recipes.test.ts +++ b/packages/runner/test/recipes.test.ts @@ -504,6 +504,12 @@ describe("Recipe Runner", () => { y, }, resultCell); + expect(runCounts).toMatchObject({ + multiply: 0, + multiplyGenerator: 0, + multiplyGenerator2: 0, + }); + await runtime.idle(); expect(result.getAsQueryResult()).toMatchObject({ From b2b634a078579b103a890fbd10cecb53cc7f9614 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 09:40:21 -0700 Subject: [PATCH 05/19] make process cell reads more scoped --- packages/runner/src/runner.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index 2bed36126..6a62086dc 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -117,8 +117,8 @@ export class Runner implements IRunner { let recipeId: string | undefined; - if (!recipeOrModule && processCell.getRaw()?.[TYPE]) { - recipeId = processCell.getRaw()[TYPE]; + if (!recipeOrModule && processCell.key(TYPE).getRaw()) { + recipeId = processCell.key(TYPE).getRaw(); recipeOrModule = this.runtime.recipeManager.recipeById(recipeId!); if (!recipeOrModule) throw new Error(`Unknown recipe: ${recipeId}`); } else if (!recipeOrModule) { @@ -162,7 +162,10 @@ export class Runner implements IRunner { if (this.cancels.has(resultCell.getDoc())) { // If it's already running and no new recipe or argument are given, // we are just returning the result doc - if (argument === undefined && recipeId === processCell.getRaw()?.[TYPE]) { + if ( + argument === undefined && + recipeId === processCell.key(TYPE).getRaw() + ) { return resultCell; } @@ -190,7 +193,7 @@ export class Runner implements IRunner { const defaults = extractDefaultValues(recipe.argumentSchema) as Partial; // Important to use DeepCopy here, as the resulting object will be modified! - const previousInternal = processCell.getRaw()?.internal; + const previousInternal = processCell.key("internal").getRaw(); const internal: JSONValue = Object.assign( {}, cellAwareDeepCopy( @@ -206,11 +209,11 @@ export class Runner implements IRunner { // Still necessary until we consistently use schema for defaults. // Only do it on first load. - if (!processCell.getRaw()?.argument) { + if (!processCell.key("argument").getRaw()) { argument = mergeObjects(argument as any, defaults); } - const recipeChanged = recipeId !== processCell.getRaw()?.[TYPE]; + const recipeChanged = recipeId !== processCell.key(TYPE).getRaw(); processCell.withTx(tx).setRaw({ ...processCell.getRaw(), From 9ca35721999bfc517ba9996b52c80be4a99105c1 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 14:29:31 -0700 Subject: [PATCH 06/19] - implemented .activity() with meta data in shim - updated read APIs to pass metadata along - use it to ignore certain reads in scheduler --- packages/runner/src/cell.ts | 21 +++++--- packages/runner/src/data-updating.ts | 53 ++++++++++++++----- packages/runner/src/recipe-binding.ts | 6 ++- packages/runner/src/runner.ts | 23 +++++--- packages/runner/src/scheduler.ts | 11 +++- packages/runner/src/storage/interface.ts | 11 ++-- .../runner/src/storage/transaction-shim.ts | 17 ++++-- 7 files changed, 106 insertions(+), 36 deletions(-) diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index 10bc78743..ce063f122 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -20,7 +20,6 @@ import { resolveLinkToWriteRedirect, } from "./link-resolution.ts"; import { txToReactivityLog } 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"; @@ -34,7 +33,10 @@ import { import { areLinksSame, isLink } from "./link-utils.ts"; import { type IRuntime } from "./runtime.ts"; import { type NormalizedFullLink } from "./link-utils.ts"; -import type { IExtendedStorageTransaction } from "./storage/interface.ts"; +import type { + IExtendedStorageTransaction, + IReadOptions, +} from "./storage/interface.ts"; import { fromURI } from "./uri-utils.ts"; /** @@ -97,6 +99,7 @@ import { fromURI } from "./uri-utils.ts"; * * @method getRaw Raw access method, without following aliases (which would * write to the destination instead of the cell itself). + * @param {IReadOptions} options - Optional read options. * @returns {any} - Raw document data * * @method setRaw Raw write method that bypasses Cell validation, @@ -192,7 +195,7 @@ declare module "@commontools/api" { }, ): SigilWriteRedirectLink; getDoc(): DocImpl; - getRaw(): any; + getRaw(options?: IReadOptions): any; setRaw(value: any): void; getSourceCell( schema?: JSONSchema, @@ -263,7 +266,7 @@ export type Cellify = export interface Stream { send(event: T): void; sink(callback: (event: T) => Cancel | undefined | void): Cancel; - getRaw(): any; + getRaw(options?: IReadOptions): any; getAsNormalizedFullLink(): NormalizedFullLink; getDoc(): DocImpl; withTx(tx?: IExtendedStorageTransaction): Stream; @@ -309,7 +312,7 @@ export function createCell( function createStreamCell( runtime: IRuntime, link: NormalizedFullLink, - _tx?: IExtendedStorageTransaction, + tx?: IExtendedStorageTransaction, ): Stream { const listeners = new Set<(event: T) => Cancel | undefined>(); @@ -331,7 +334,9 @@ function createStreamCell( listeners.add(callback); return () => listeners.delete(callback); }, - getRaw: () => self.getDoc().getAtPath(link.path as PropertyKey[]), + getRaw: (options?: IReadOptions) => + (tx?.status().ok?.open ? tx : runtime.edit()) + .readValueOrThrow(link, options), getAsNormalizedFullLink: () => link, getDoc: () => runtime.documentMap.getDocByEntityId(link.space, link.id), withTx: (_tx?: IExtendedStorageTransaction) => self, // No-op for streams @@ -513,9 +518,9 @@ function createRegularCell( }) as SigilWriteRedirectLink; }, getDoc: () => runtime.documentMap.getDocByEntityId(link.space, link.id), - getRaw: () => + getRaw: (options?: IReadOptions) => (tx?.status().ok?.open ? tx : runtime.edit()) - .readValueOrThrow(link), + .readValueOrThrow(link, options), setRaw: (value: any) => { if (!tx) throw new Error("Transaction required for setRaw"); tx.writeValueOrThrow(link, value); diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index 24916d633..37b691c08 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -18,6 +18,7 @@ import { import { isQueryResultForDereferencing } from "./query-result-proxy.ts"; import { type IExtendedStorageTransaction, + type IReadOptions, type JSONValue, } from "./storage/interface.ts"; import { type IRuntime } from "./runtime.ts"; @@ -44,8 +45,16 @@ export function diffAndUpdate( link: NormalizedFullLink, newValue: unknown, context?: unknown, + options?: IReadOptions, ): boolean { - const changes = normalizeAndDiff(runtime, tx, link, newValue, context); + const changes = normalizeAndDiff( + runtime, + tx, + link, + newValue, + context, + options, + ); applyChangeSet(tx, changes); return changes.length > 0; } @@ -82,6 +91,7 @@ export function normalizeAndDiff( link: NormalizedFullLink, newValue: unknown, context?: unknown, + options?: IReadOptions, ): ChangeSet { const changes: ChangeSet = []; @@ -105,7 +115,7 @@ export function normalizeAndDiff( const parent = tx.readValueOrThrow({ ...link, path: link.path.slice(0, -1), - }); + }, options); if (Array.isArray(parent)) { const base = runtime.getCellFromLink(link, undefined, tx); for (const v of parent) { @@ -114,14 +124,21 @@ export function normalizeAndDiff( const siblingId = tx.readValueOrThrow({ ...sibling, path: [...sibling.path, fieldName as string], - }); + }, options); if (siblingId === id) { // We found a sibling with the same id, so ... return [ // ... reuse the existing document - ...normalizeAndDiff(runtime, tx, link, v, context), + ...normalizeAndDiff(runtime, tx, link, v, context, options), // ... and update it to the new value - ...normalizeAndDiff(runtime, tx, sibling, rest, context), + ...normalizeAndDiff( + runtime, + tx, + sibling, + rest, + context, + options, + ), ]; } } @@ -148,7 +165,7 @@ export function normalizeAndDiff( } // Get current value to compare against - let currentValue = tx.readValueOrThrow(link); + let currentValue = tx.readValueOrThrow(link, options); // A new alias can overwrite a previous alias. No-op if the same. if (isWriteRedirectLink(newValue)) { @@ -167,7 +184,14 @@ export function normalizeAndDiff( if (isWriteRedirectLink(currentValue)) { // Log reads of the alias, so that changing aliases cause refreshes const redirectLink = followWriteRedirects(tx, currentValue, link); - return normalizeAndDiff(runtime, tx, redirectLink, newValue, context); + return normalizeAndDiff( + runtime, + tx, + redirectLink, + newValue, + context, + options, + ); } if (isAnyCellLink(newValue)) { @@ -181,8 +205,8 @@ export function normalizeAndDiff( // might also be broken for other relative links) // Use the tx code to make sure we read it the same way - const dataValue = runtime.edit().readValueOrThrow(parsedLink); - return normalizeAndDiff(runtime, tx, link, dataValue, context); + const dataValue = runtime.edit().readValueOrThrow(parsedLink, options); + return normalizeAndDiff(runtime, tx, link, dataValue, context, options); } if ( isAnyCellLink(currentValue) && @@ -209,7 +233,9 @@ export function normalizeAndDiff( // array as context, recursively. while ( path.length > 0 && - Array.isArray(tx.readValueOrThrow({ ...link, path: path.slice(0, -1) })) + Array.isArray( + tx.readValueOrThrow({ ...link, path: path.slice(0, -1) }, options), + ) ) { path = path.slice(0, -1); } @@ -234,9 +260,10 @@ export function normalizeAndDiff( link, createSigilLinkFromParsedLink(newEntryLink, link), context, + options, ), // And see whether the value of the document itself changed - ...normalizeAndDiff(runtime, tx, newEntryLink, rest, context), + ...normalizeAndDiff(runtime, tx, newEntryLink, rest, context, options), ]; } @@ -264,6 +291,7 @@ export function normalizeAndDiff( }, newValue[i], context, + options, ); changes.push(...nestedChanges); } @@ -312,6 +340,7 @@ export function normalizeAndDiff( { ...link, path: [...link.path, key], schema: childSchema }, newValue[key], context, + options, ); changes.push(...nestedChanges); } @@ -336,7 +365,7 @@ export function normalizeAndDiff( const maybeCurrentArray = tx.readValueOrThrow({ ...link, path: link.path.slice(0, -1), - }); + }, options); if (Array.isArray(maybeCurrentArray)) { const currentLength = maybeCurrentArray.length; const newLength = newValue as number; diff --git a/packages/runner/src/recipe-binding.ts b/packages/runner/src/recipe-binding.ts index 0558b297b..b6859e1d2 100644 --- a/packages/runner/src/recipe-binding.ts +++ b/packages/runner/src/recipe-binding.ts @@ -6,8 +6,8 @@ import { unsafe_parentRecipe, } from "./builder/types.ts"; import { isLegacyAlias, isLink } from "./link-utils.ts"; -import { type DocImpl, isDoc } from "./doc.ts"; -import { type Cell, isCell } from "./cell.ts"; +import { isDoc } from "./doc.ts"; +import { type Cell } from "./cell.ts"; import { followWriteRedirects } from "./link-resolution.ts"; import { diffAndUpdate } from "./data-updating.ts"; import { @@ -17,6 +17,7 @@ import { parseLink, } from "./link-utils.ts"; import type { IExtendedStorageTransaction } from "./storage/interface.ts"; +import { ignoreReadForScheduling } from "./scheduler.ts"; /** * Sends a value to a binding. If the binding is an array or object, it'll @@ -43,6 +44,7 @@ export function sendValueToBinding( ref, value as JSONValue, { cell: cell.getAsNormalizedFullLink(), binding }, + { meta: ignoreReadForScheduling }, ); } else if (Array.isArray(binding)) { if (Array.isArray(value)) { diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index 6a62086dc..a3abb5fa3 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -44,6 +44,7 @@ import "./builtins/index.ts"; import { LINK_V1_TAG, SigilLink } from "./sigil-types.ts"; import type { IRunner, IRuntime } from "./runtime.ts"; import type { IExtendedStorageTransaction } from "./storage/interface.ts"; +import { ignoreReadForScheduling } from "./scheduler.ts"; export class Runner implements IRunner { readonly cancels = new WeakMap, Cancel>(); @@ -117,8 +118,13 @@ export class Runner implements IRunner { let recipeId: string | undefined; - if (!recipeOrModule && processCell.key(TYPE).getRaw()) { - recipeId = processCell.key(TYPE).getRaw(); + if ( + !recipeOrModule && + processCell.key(TYPE).getRaw({ meta: ignoreReadForScheduling }) + ) { + recipeId = processCell.key(TYPE).getRaw({ + meta: ignoreReadForScheduling, + }); recipeOrModule = this.runtime.recipeManager.recipeById(recipeId!); if (!recipeOrModule) throw new Error(`Unknown recipe: ${recipeId}`); } else if (!recipeOrModule) { @@ -164,7 +170,8 @@ export class Runner implements IRunner { // we are just returning the result doc if ( argument === undefined && - recipeId === processCell.key(TYPE).getRaw() + recipeId === + processCell.key(TYPE).getRaw({ meta: ignoreReadForScheduling }) ) { return resultCell; } @@ -193,7 +200,9 @@ export class Runner implements IRunner { const defaults = extractDefaultValues(recipe.argumentSchema) as Partial; // Important to use DeepCopy here, as the resulting object will be modified! - const previousInternal = processCell.key("internal").getRaw(); + const previousInternal = processCell.key("internal").getRaw({ + meta: ignoreReadForScheduling, + }); const internal: JSONValue = Object.assign( {}, cellAwareDeepCopy( @@ -213,10 +222,12 @@ export class Runner implements IRunner { argument = mergeObjects(argument as any, defaults); } - const recipeChanged = recipeId !== processCell.key(TYPE).getRaw(); + const recipeChanged = recipeId !== processCell.key(TYPE).getRaw({ + meta: ignoreReadForScheduling, + }); processCell.withTx(tx).setRaw({ - ...processCell.getRaw(), + ...processCell.getRaw({ meta: ignoreReadForScheduling }), [TYPE]: recipeId || "unknown", resultRef: resultCell.getAsLink({ base: processCell }), internal, diff --git a/packages/runner/src/scheduler.ts b/packages/runner/src/scheduler.ts index 2a807c110..2088ece3f 100644 --- a/packages/runner/src/scheduler.ts +++ b/packages/runner/src/scheduler.ts @@ -17,11 +17,11 @@ import type { import { areNormalizedLinksSame, type NormalizedFullLink, - parseLink, } from "./link-utils.ts"; import type { IExtendedStorageTransaction, IMemorySpaceAddress, + Metadata, } from "./storage/interface.ts"; // Re-export types that tests expect from scheduler @@ -41,6 +41,14 @@ export type ReactivityLog = { writes: IMemorySpaceAddress[]; }; +const ignoreReadForSchedulingMarker: unique symbol = Symbol( + "ignoreReadForSchedulingMarker", +); + +export const ignoreReadForScheduling: Metadata = { + [ignoreReadForSchedulingMarker]: true, +}; + type SpaceAndURI = `${MemorySpace}:${URI}`; const MAX_ITERATIONS_PER_RUN = 100; @@ -448,6 +456,7 @@ export function txToReactivityLog( const log: ReactivityLog = { reads: [], writes: [] }; for (const activity of tx.journal.activity()) { if ("read" in activity && activity.read) { + if (activity.read.meta?.[ignoreReadForSchedulingMarker]) continue; log.reads.push({ space: activity.read.space, id: activity.read.id, diff --git a/packages/runner/src/storage/interface.ts b/packages/runner/src/storage/interface.ts index b1e2fd10f..33b435c62 100644 --- a/packages/runner/src/storage/interface.ts +++ b/packages/runner/src/storage/interface.ts @@ -488,7 +488,10 @@ export interface IExtendedStorageTransaction extends IStorageTransaction { * @param address - Memory address to read from. * @returns The read value. */ - readOrThrow(address: IMemorySpaceAddress): JSONValue | undefined; + readOrThrow( + address: IMemorySpaceAddress, + options?: IReadOptions, + ): JSONValue | undefined; /** * Reads a value from a (local) memory address and throws on error, except for @@ -499,7 +502,10 @@ export interface IExtendedStorageTransaction extends IStorageTransaction { * @param address - Memory address to read from. * @returns The read value. */ - readValueOrThrow(address: IMemorySpaceAddress): JSONValue | undefined; + readValueOrThrow( + address: IMemorySpaceAddress, + options?: IReadOptions, + ): JSONValue | undefined; /** * Writes a value into a storage at a given address, including creating parent @@ -526,7 +532,6 @@ export interface IExtendedStorageTransaction extends IStorageTransaction { address: IMemorySpaceAddress, value: JSONValue | undefined, ): void; - } export interface ITransactionReader { diff --git a/packages/runner/src/storage/transaction-shim.ts b/packages/runner/src/storage/transaction-shim.ts index 2207ff25d..b86281089 100644 --- a/packages/runner/src/storage/transaction-shim.ts +++ b/packages/runner/src/storage/transaction-shim.ts @@ -589,16 +589,25 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction { return this.tx.read(address, options); } - readOrThrow(address: IMemorySpaceAddress): JSONValue | undefined { - const readResult = this.tx.read(address); + readOrThrow( + address: IMemorySpaceAddress, + options?: IReadOptions, + ): JSONValue | undefined { + const readResult = this.tx.read(address, options); if (readResult.error && readResult.error.name !== "NotFoundError") { throw readResult.error; } return readResult.ok?.value; } - readValueOrThrow(address: IMemorySpaceAddress): JSONValue | undefined { - return this.readOrThrow({ ...address, path: ["value", ...address.path] }); + readValueOrThrow( + address: IMemorySpaceAddress, + options?: IReadOptions, + ): JSONValue | undefined { + return this.readOrThrow( + { ...address, path: ["value", ...address.path] }, + options, + ); } writer(space: MemorySpace): Result { From 4d3ccb1b9002c42e04e2a9f34b76c21ec204cf35 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 14:43:24 -0700 Subject: [PATCH 07/19] add back spellId to errors --- packages/runner/src/scheduler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/runner/src/scheduler.ts b/packages/runner/src/scheduler.ts index 2088ece3f..7b57e5350 100644 --- a/packages/runner/src/scheduler.ts +++ b/packages/runner/src/scheduler.ts @@ -545,13 +545,13 @@ function getCharmMetadataFromFrame(): { type: "object", properties: { [TYPE]: { type: "string" }, - resultRef: { - type: "object", - asCell: true, - }, + spell: { type: "object", asCell: true }, + resultRef: { type: "object", asCell: true }, }, }); result.recipeId = source.get()?.[TYPE]; + const spellCell = source.get()?.spell; + result.spellId = spellCell?.getAsNormalizedFullLink().id; const resultCell = source.get()?.resultRef; result.space = source.space; result.charmId = JSON.parse( From c3cb7f093d5a6899caa741bc76eef7b99d031b78 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 14:43:48 -0700 Subject: [PATCH 08/19] catch more non-reactive reads --- packages/runner/src/recipe-binding.ts | 2 +- packages/runner/src/runner.ts | 4 +++- packages/runner/src/storage/transaction-shim.ts | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/runner/src/recipe-binding.ts b/packages/runner/src/recipe-binding.ts index b6859e1d2..faedc1c36 100644 --- a/packages/runner/src/recipe-binding.ts +++ b/packages/runner/src/recipe-binding.ts @@ -163,7 +163,7 @@ export function findAllWriteRedirectCells( baseCell.tx, ); if (!linkCell) throw new Error("Link cell not found"); - find(linkCell.getRaw(), baseCell); + find(linkCell.getRaw({ meta: ignoreReadForScheduling }), baseCell); } else if (isLink(binding)) { // Links that are not write redirects: Ignore them. return; diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index a3abb5fa3..e03b25e71 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -218,7 +218,9 @@ export class Runner implements IRunner { // Still necessary until we consistently use schema for defaults. // Only do it on first load. - if (!processCell.key("argument").getRaw()) { + if ( + !processCell.key("argument").getRaw({ meta: ignoreReadForScheduling }) + ) { argument = mergeObjects(argument as any, defaults); } diff --git a/packages/runner/src/storage/transaction-shim.ts b/packages/runner/src/storage/transaction-shim.ts index b86281089..86c5e1ce1 100644 --- a/packages/runner/src/storage/transaction-shim.ts +++ b/packages/runner/src/storage/transaction-shim.ts @@ -36,6 +36,7 @@ import type { IRuntime } from "../runtime.ts"; import type { EntityId } from "../doc-map.ts"; import { getValueAtPath } from "../path-utils.ts"; import { getJSONFromDataURI } from "../uri-utils.ts"; +import { ignoreReadForScheduling } from "../scheduler.ts"; /** * Convert a URI string to an EntityId object @@ -630,7 +631,9 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction { // Create parent entries if needed const lastValidPath = writeResult.error.path; const valueObj = lastValidPath - ? this.readValueOrThrow({ ...address, path: lastValidPath }) + ? this.readValueOrThrow({ ...address, path: lastValidPath }, { + meta: ignoreReadForScheduling, + }) : {}; if (!isRecord(valueObj)) { throw new Error( From 2f8e00403802b02a925ec25ba695a1db63aff549 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 16:26:14 -0700 Subject: [PATCH 09/19] remove debug logging --- packages/runner/test/recipes.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts index ef9a2df30..73ad71edf 100644 --- a/packages/runner/test/recipes.test.ts +++ b/packages/runner/test/recipes.test.ts @@ -988,8 +988,6 @@ describe("Recipe Runner", () => { await runtime.idle(); expect(timeoutCalled).toBe(true); - console.log("raw", result.getRaw()); - console.log("get", result.get()); expect(result.get()).toMatchObject({ result: 2 }); }); From 335f811ff03fa4601993cbf57c439fbb19e9023d Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 16:35:24 -0700 Subject: [PATCH 10/19] remove unused import (a legacy one!) --- packages/runner/test/cell.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index cb54b87cb..0439c43ff 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -10,7 +10,6 @@ import { popFrame, pushFrame } from "../src/builder/recipe.ts"; import { Runtime } from "../src/runtime.ts"; import { txToReactivityLog } from "../src/scheduler.ts"; import { addCommonIDfromObjectID } from "../src/data-updating.ts"; -import { isLegacyCellLink } from "../src/link-utils.ts"; import { areLinksSame, isAnyCellLink, parseLink } from "../src/link-utils.ts"; import { areNormalizedLinksSame } from "../src/link-utils.ts"; import { type IExtendedStorageTransaction } from "../src/storage/interface.ts"; From 79555c066c0ca8f0511bcc57f9bbe46d8bbfdb79 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 16:36:45 -0700 Subject: [PATCH 11/19] fixed schema in test --- packages/runner/test/recipes.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runner/test/recipes.test.ts b/packages/runner/test/recipes.test.ts index 73ad71edf..c12e0effb 100644 --- a/packages/runner/test/recipes.test.ts +++ b/packages/runner/test/recipes.test.ts @@ -246,7 +246,7 @@ describe("Recipe Runner", () => { "should handle map nodes with undefined input", { type: "object", - properties: { values: { type: "array", items: { type: "number" } } }, + properties: { doubled: { type: "array", items: { type: "number" } } }, } as const satisfies JSONSchema, tx, ); From 8956e700c463bba2bd2c93475c208fffcc0ce9bb Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 16 Jul 2025 16:51:52 -0700 Subject: [PATCH 12/19] added unit test for get/setSourceCell --- packages/runner/test/cell.test.ts | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index 0439c43ff..6a489c86c 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -205,6 +205,40 @@ describe("Cell", () => { // Verify the document structure is preserved expect(c.get()).toEqual({ nested: { value: 100 } }); }); + + it("should set and get the source cell", () => { + // Create two cells + const sourceCell = runtime.getCell<{ foo: number }>( + space, + "source cell for setSourceCell/getSourceCell test", + undefined, + tx, + ); + sourceCell.set({ foo: 123 }); + + const targetCell = runtime.getCell<{ bar: string }>( + space, + "target cell for setSourceCell/getSourceCell test", + undefined, + tx, + ); + targetCell.set({ bar: "baz" }); + + // Initially, getSourceCell should return undefined + expect(targetCell.getSourceCell()).toBeUndefined(); + + // Set the source cell + targetCell.setSourceCell(sourceCell); + + // Now getSourceCell should return a Cell with the same value as sourceCell + const retrievedSource = targetCell.getSourceCell(); + expect(isCell(retrievedSource)).toBe(true); + expect(retrievedSource?.get()).toEqual({ foo: 123 }); + + // Changing the source cell's value should be reflected + sourceCell.set({ foo: 456 }); + expect(retrievedSource?.get()).toEqual({ foo: 456 }); + }); }); describe("Cell utility functions", () => { From ea4a34cfea149faee3a6333bf521c4bcff540107 Mon Sep 17 00:00:00 2001 From: Ellyse Date: Thu, 17 Jul 2025 15:33:21 -0700 Subject: [PATCH 13/19] unit test for respecting ignoreReadForScheduling --- packages/runner/test/scheduler.test.ts | 63 +++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/runner/test/scheduler.test.ts b/packages/runner/test/scheduler.test.ts index b64796158..92494c9d5 100644 --- a/packages/runner/test/scheduler.test.ts +++ b/packages/runner/test/scheduler.test.ts @@ -4,7 +4,7 @@ import { assertSpyCall, assertSpyCalls, spy } from "@std/testing/mock"; import { type IExtendedStorageTransaction } from "../src/storage/interface.ts"; import { Runtime } from "../src/runtime.ts"; import { type Action, type EventHandler } from "../src/scheduler.ts"; -import { compactifyPaths } from "../src/scheduler.ts"; +import { compactifyPaths, ignoreReadForScheduling } from "../src/scheduler.ts"; import { Identity } from "@commontools/identity"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; @@ -392,6 +392,67 @@ describe("scheduler", () => { await runtime.idle(); expect(runs).toBe(1); }); + + it("should not create dependencies when using getRaw with ignoreReadForScheduling", async () => { + // Create a source cell that will be read with ignored metadata + const sourceCell = runtime.getCell<{ value: number }>( + space, + "source-cell-for-ignore-test", + undefined, + tx, + ); + sourceCell.set({ value: 1 }); + + // Create a result cell to track action runs (avoiding self-dependencies) + const resultCell = runtime.getCell<{ count: number; lastValue: any }>( + space, + "result-cell-for-ignore-test", + undefined, + tx, + ); + resultCell.set({ count: 0, lastValue: null }); + + let actionRunCount = 0; + let lastReadValue: any; + + // Action that ONLY uses ignored reads + const ignoredReadAction: Action = (actionTx) => { + actionRunCount++; + + // Read with ignoreReadForScheduling - should NOT create dependency + lastReadValue = sourceCell.withTx(actionTx).getRaw({ + meta: ignoreReadForScheduling, + }); + + // Write to result cell to track that the action ran + resultCell.withTx(actionTx).set({ + count: actionRunCount, + lastValue: lastReadValue, + }); + }; + + // Run the action initially + await runtime.scheduler.run(ignoredReadAction); + expect(actionRunCount).toBe(1); + expect(lastReadValue).toEqual({ value: 1 }); + expect(resultCell.get()).toEqual({ count: 1, lastValue: { value: 1 } }); + + // Change the source cell + sourceCell.set({ value: 5 }); + await runtime.idle(); + + // Action should NOT run again because the read was ignored + expect(actionRunCount).toBe(1); // Still 1! + expect(resultCell.get()).toEqual({ count: 1, lastValue: { value: 1 } }); // Unchanged + + // Change the source cell again to be extra sure + sourceCell.set({ value: 10 }); + await runtime.idle(); + + // Still should not have run + expect(actionRunCount).toBe(1); + expect(resultCell.get()).toEqual({ count: 1, lastValue: { value: 1 } }); + }); }); describe("event handling", () => { From ca7af08ad75c83c07174565120368bad0abd9a3d Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 18 Jul 2025 14:31:11 -0500 Subject: [PATCH 14/19] refactor: consolidate sigil link creation and improve schema handling - Move createSigilLink function from cell.ts to link-utils.ts as createSigilLinkFromParsedLink - Add support for base cell/link parameter to enable relative references - Add sanitizeSchemaForLinks function to remove asCell/asStream flags from schemas - Update all callers to use new function signature with options object - Add comprehensive tests for schema sanitization --- packages/runner/src/cell.ts | 70 ++-------- packages/runner/src/data-updating.ts | 7 +- packages/runner/src/link-utils.ts | 110 +++++++++++++++- packages/runner/src/runner.ts | 2 +- packages/runner/test/link-utils.test.ts | 166 +++++++++++++++++++++++- 5 files changed, 287 insertions(+), 68 deletions(-) diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index ce063f122..f996f646c 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -25,14 +25,16 @@ import { validateAndTransform } from "./schema.ts"; import { toURI } from "./uri-utils.ts"; import { type LegacyJSONCellLink, - LINK_V1_TAG, type SigilLink, type SigilWriteRedirectLink, type URI, } from "./sigil-types.ts"; import { areLinksSame, isLink } from "./link-utils.ts"; import { type IRuntime } from "./runtime.ts"; -import { type NormalizedFullLink } from "./link-utils.ts"; +import { + createSigilLinkFromParsedLink, + type NormalizedFullLink, +} from "./link-utils.ts"; import type { IExtendedStorageTransaction, IReadOptions, @@ -503,7 +505,10 @@ function createRegularCell( includeSchema?: boolean; }, ): SigilLink => { - return createSigilLink(link, options) as SigilLink; + return createSigilLinkFromParsedLink(link, { + ...options, + overwrite: "this", + }); }, getAsWriteRedirectLink: ( options?: { @@ -512,7 +517,7 @@ function createRegularCell( includeSchema?: boolean; }, ): SigilWriteRedirectLink => { - return createSigilLink(link, { + return createSigilLinkFromParsedLink(link, { ...options, overwrite: "redirect", }) as SigilWriteRedirectLink; @@ -567,7 +572,7 @@ function createRegularCell( return self.get(); }, get cellLink(): SigilLink { - return createSigilLink(link); + return createSigilLinkFromParsedLink(link); }, get space(): MemorySpace { return space; @@ -641,61 +646,6 @@ function subscribeToReferencedDocs( }; } -/** - * Creates a sigil reference (link or alias) with shared logic - */ -function createSigilLink( - link: NormalizedFullLink, - 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: link.path.map((p) => p.toString()), - }, - }, - }; - - const reference = sigil["/"][LINK_V1_TAG]; - - // Handle base cell for relative references - if (options.base) { - const baseLink = options.base.getAsNormalizedFullLink(); - - // Only include id if it's different from base - if (link.id !== baseLink.id) reference.id = toURI(link.id); - - // Only include space if it's different from base - if (link.space && link.space !== baseLink.space) { - reference.space = link.space; - } - } else { - reference.id = link.id; - - // Handle baseSpace option - only include space if different from baseSpace - if (link.space !== options.baseSpace) reference.space = link.space; - } - - // Include schema if requested - if (options.includeSchema && link.schema) { - reference.schema = link.schema; - reference.rootSchema = link.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. * diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index 37b691c08..1a93f0cf8 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -258,7 +258,7 @@ export function normalizeAndDiff( runtime, tx, link, - createSigilLinkFromParsedLink(newEntryLink, link), + createSigilLinkFromParsedLink(newEntryLink, { base: link }), context, options, ), @@ -377,7 +377,10 @@ export function normalizeAndDiff( i++ ) { changes.push({ - location: { ...link, path: [...link.path.slice(0, -1), i.toString()] }, + location: { + ...link, + path: [...link.path.slice(0, -1), i.toString()], + }, value: undefined, }); } diff --git a/packages/runner/src/link-utils.ts b/packages/runner/src/link-utils.ts index 7aa480777..de3d19656 100644 --- a/packages/runner/src/link-utils.ts +++ b/packages/runner/src/link-utils.ts @@ -413,7 +413,67 @@ export function areNormalizedLinksSame( (link1.type ?? "application/json") === (link2.type ?? "application/json"); } +/** + * Creates a sigil reference (link or alias) with shared logic + */ export function createSigilLinkFromParsedLink( + link: NormalizedLink, + options: { + base?: Cell | NormalizedFullLink; + baseSpace?: MemorySpace; + includeSchema?: boolean; + overwrite?: "redirect" | "this"; // default is "this" + } = {}, +): SigilLink { + // Create the base structure + const sigil: SigilLink = { + "/": { + [LINK_V1_TAG]: { + path: link.path.map((p) => p.toString()), + }, + }, + }; + + const reference = sigil["/"][LINK_V1_TAG]; + + // Handle base cell for relative references + if (options.base) { + const baseLink = isCell(options.base) + ? options.base.getAsNormalizedFullLink() + : options.base; + + // Only include id if it's different from base + if (link.id !== baseLink.id) reference.id = toURI(link.id); + + // Only include space if it's different from base + if (link.space && link.space !== baseLink.space) { + reference.space = link.space; + } + } else { + reference.id = link.id; + + // Handle baseSpace option - only include space if different from baseSpace + if (link.space !== options.baseSpace) reference.space = link.space; + } + + // Include schema if requested + if (options.includeSchema && link.schema) { + reference.schema = link.schema; + reference.rootSchema = link.rootSchema; + } + + // Option overrides link value + if (options.overwrite) { + if (options.overwrite === "redirect") reference.overwrite = "redirect"; + // else: "this" is the default + } else if (link.overwrite === "redirect") { + reference.overwrite = "redirect"; + } + + return sigil; +} + +export function createSigilLinkFromParsedLinkOld( link: NormalizedLink, base?: Cell | NormalizedLink, overwrite?: "redirect", @@ -422,8 +482,8 @@ export function createSigilLinkFromParsedLink( "/": { [LINK_V1_TAG]: { path: link.path as string[], - schema: link.schema, - rootSchema: link.rootSchema, + schema: sanitizeSchemaForLinks(link.schema), + rootSchema: sanitizeSchemaForLinks(link.rootSchema), ...(overwrite === "redirect" ? { overwrite } : {}), }, }, @@ -449,3 +509,49 @@ export function createSigilLinkFromParsedLink( return sigilLink; } + +/** + * Traverse schema and remove all asCell and asStream flags. + */ +export function sanitizeSchemaForLinks( + schema: JSONSchema | undefined, +): JSONSchema | undefined { + return recursiveStripAsCellAndStreamFromSchema(schema); +} + +function recursiveStripAsCellAndStreamFromSchema( + schema: any, +): any { + // Handle null/undefined/boolean schemas + if ( + schema === null || typeof schema !== "object" || typeof schema === "boolean" + ) { + return schema; + } + + // Create a copy to avoid mutating the original + const result = { ...schema }; + + // Remove asCell and asStream flags from this level + delete (result as any).asCell; + delete (result as any).asStream; + + // Recursively process all object properties + for (const [key, value] of Object.entries(result)) { + if (value && typeof value === "object") { + if (Array.isArray(value)) { + // Handle arrays + (result as any)[key] = value.map((item) => + typeof item === "object" && item !== null + ? recursiveStripAsCellAndStreamFromSchema(item) + : item + ); + } else { + // Handle objects + (result as any)[key] = recursiveStripAsCellAndStreamFromSchema(value); + } + } + } + + return result; +} diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index e03b25e71..2fe1dde6b 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -192,7 +192,7 @@ export class Runner implements IRunner { if (isLink(argument)) { argument = createSigilLinkFromParsedLink( parseLink(argument), - processCell, + { base: processCell, includeSchema: true }, ) as T; } diff --git a/packages/runner/test/link-utils.test.ts b/packages/runner/test/link-utils.test.ts index 952bf2879..10f273478 100644 --- a/packages/runner/test/link-utils.test.ts +++ b/packages/runner/test/link-utils.test.ts @@ -10,7 +10,9 @@ import { type NormalizedLink, parseLink, parseLinkOrThrow, + sanitizeSchemaForLinks, } from "../src/link-utils.ts"; +import type { JSONSchema } from "../src/builder/types.ts"; import { LINK_V1_TAG } from "../src/sigil-types.ts"; import { Runtime } from "../src/runtime.ts"; import { Identity } from "@commontools/identity"; @@ -495,7 +497,9 @@ describe("link-utils", () => { rootSchema: { type: "object" }, }; - const result = createSigilLinkFromParsedLink(normalizedLink); + const result = createSigilLinkFromParsedLink(normalizedLink, { + includeSchema: true, + }); expect(result).toEqual({ "/": { @@ -518,7 +522,9 @@ describe("link-utils", () => { space: space, }; - const result = createSigilLinkFromParsedLink(normalizedLink, baseCell); + const result = createSigilLinkFromParsedLink(normalizedLink, { + base: baseCell, + }); expect(result["/"][LINK_V1_TAG].space).toBeUndefined(); }); @@ -531,7 +537,9 @@ describe("link-utils", () => { path: ["nested", "value"], }; - const result = createSigilLinkFromParsedLink(normalizedLink, baseCell); + const result = createSigilLinkFromParsedLink(normalizedLink, { + base: baseCell, + }); expect(result["/"][LINK_V1_TAG].id).toBe(`of:${baseId}`); }); @@ -548,4 +556,156 @@ describe("link-utils", () => { expect(result["/"][LINK_V1_TAG].overwrite).toBe("redirect"); }); }); + + describe("stripAsCellAndStreamFromSchema", () => { + it("should remove asCell and asStream from simple schema", () => { + const schema = { + type: "object", + asCell: true, + asStream: false, + properties: { + name: { type: "string" }, + }, + } as const satisfies JSONSchema; + + const result = sanitizeSchemaForLinks(schema); + + expect(result).toEqual({ + type: "object", + properties: { + name: { type: "string" }, + }, + }); + }); + + it("should remove asCell and asStream from nested properties", () => { + const schema = { + type: "object", + properties: { + user: { + type: "object", + asCell: true, + properties: { + name: { type: "string" }, + settings: { + type: "object", + asStream: true, + properties: { + theme: { type: "string" }, + }, + }, + }, + }, + }, + } as const satisfies JSONSchema; + + const result = sanitizeSchemaForLinks(schema); + + expect(result).toEqual({ + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { type: "string" }, + settings: { + type: "object", + properties: { + theme: { type: "string" }, + }, + }, + }, + }, + }, + }); + }); + + it("should handle arrays of schemas", () => { + const schema = { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string", + asCell: true, + }, + }, + }, + } as const satisfies JSONSchema; + + const result = sanitizeSchemaForLinks(schema); + + expect(result).toEqual({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "string", + }, + }, + }, + }); + }); + + it("should handle anyOf arrays", () => { + const schema = { + type: "object", + anyOf: [ + { type: "string", asCell: true }, + { type: "number", asStream: true }, + ], + } as const satisfies JSONSchema; + + const result = sanitizeSchemaForLinks(schema); + + expect(result).toEqual({ + type: "object", + anyOf: [ + { type: "string" }, + { type: "number" }, + ], + }); + }); + + it("should handle additionalProperties", () => { + const schema = { + type: "object", + additionalProperties: { + type: "string", + asCell: true, + }, + } as const satisfies JSONSchema; + + const result = sanitizeSchemaForLinks(schema); + + expect(result).toEqual({ + type: "object", + additionalProperties: { + type: "string", + }, + }); + }); + + it("should not mutate the original schema", () => { + const originalSchema = { + type: "object", + asCell: true, + properties: { + name: { type: "string", asStream: true }, + }, + } as const satisfies JSONSchema; + + const result = sanitizeSchemaForLinks(originalSchema); + + // Original should be unchanged + expect(originalSchema.asCell).toBe(true); + expect(originalSchema.properties.name.asStream).toBe(true); + + // Result should have flags removed + expect((result as any).asCell).toBeUndefined(); + expect((result as any).properties.name.asStream).toBeUndefined(); + }); + }); }); From 5cdc8df1c764ebd365af1336fa8e4aa487e2d9e6 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 18 Jul 2025 14:36:05 -0500 Subject: [PATCH 15/19] actually sanitize the schemas in links... --- packages/runner/src/link-utils.ts | 41 ++----------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/packages/runner/src/link-utils.ts b/packages/runner/src/link-utils.ts index de3d19656..346e6d502 100644 --- a/packages/runner/src/link-utils.ts +++ b/packages/runner/src/link-utils.ts @@ -458,8 +458,8 @@ export function createSigilLinkFromParsedLink( // Include schema if requested if (options.includeSchema && link.schema) { - reference.schema = link.schema; - reference.rootSchema = link.rootSchema; + reference.schema = sanitizeSchemaForLinks(link.schema); + reference.rootSchema = sanitizeSchemaForLinks(link.rootSchema); } // Option overrides link value @@ -473,43 +473,6 @@ export function createSigilLinkFromParsedLink( return sigil; } -export function createSigilLinkFromParsedLinkOld( - link: NormalizedLink, - base?: Cell | NormalizedLink, - overwrite?: "redirect", -): SigilLink { - const sigilLink: SigilLink = { - "/": { - [LINK_V1_TAG]: { - path: link.path as string[], - schema: sanitizeSchemaForLinks(link.schema), - rootSchema: sanitizeSchemaForLinks(link.rootSchema), - ...(overwrite === "redirect" ? { overwrite } : {}), - }, - }, - }; - - // 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.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; -} - /** * Traverse schema and remove all asCell and asStream flags. */ From a0cd848ed4d680aa8ac443065cadd875dcf41708 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 18 Jul 2025 14:39:07 -0500 Subject: [PATCH 16/19] also sanitize schemas in legacy aliases --- packages/runner/src/builder/json-utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/runner/src/builder/json-utils.ts b/packages/runner/src/builder/json-utils.ts index cea602ffa..080862599 100644 --- a/packages/runner/src/builder/json-utils.ts +++ b/packages/runner/src/builder/json-utils.ts @@ -20,7 +20,7 @@ import { import { getTopFrame } from "./recipe.ts"; import { deepEqual } from "../path-utils.ts"; import { IRuntime } from "../runtime.ts"; -import { parseLink } from "../link-utils.ts"; +import { parseLink, sanitizeSchemaForLinks } from "../link-utils.ts"; export function toJSONWithLegacyAliases( value: Opaque, @@ -55,8 +55,12 @@ export function toJSONWithLegacyAliases( $alias: { ...(isShadowRef(value) ? { cell: value } : {}), path: pathToCell as (string | number)[], - ...(exported?.schema ? { schema: exported.schema } : {}), - ...(exported?.rootSchema ? { rootSchema: exported.rootSchema } : {}), + ...(exported?.schema + ? { schema: sanitizeSchemaForLinks(exported.schema) } + : {}), + ...(exported?.rootSchema + ? { rootSchema: sanitizeSchemaForLinks(exported.rootSchema) } + : {}), }, } satisfies LegacyAlias; } else throw new Error(`Cell not found in paths`); From b0fb76c83749a612b2b362d59727172ad06dc6e2 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 18 Jul 2025 14:55:23 -0500 Subject: [PATCH 17/19] post rebase fix --- packages/runner/src/cell.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index f996f646c..dfed1f2bd 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -337,7 +337,7 @@ function createStreamCell( return () => listeners.delete(callback); }, getRaw: (options?: IReadOptions) => - (tx?.status().ok?.open ? tx : runtime.edit()) + (tx?.status().status === "ready" ? tx : runtime.edit()) .readValueOrThrow(link, options), getAsNormalizedFullLink: () => link, getDoc: () => runtime.documentMap.getDocByEntityId(link.space, link.id), @@ -524,15 +524,16 @@ function createRegularCell( }, getDoc: () => runtime.documentMap.getDocByEntityId(link.space, link.id), getRaw: (options?: IReadOptions) => - (tx?.status().ok?.open ? tx : runtime.edit()) + (tx?.status().status === "ready" ? tx : runtime.edit()) .readValueOrThrow(link, options), setRaw: (value: any) => { if (!tx) throw new Error("Transaction required for setRaw"); tx.writeValueOrThrow(link, value); }, getSourceCell: (newSchema?: JSONSchema) => { - const sourceCellId = (tx?.status().ok?.open ? tx : runtime.edit()) - .readOrThrow({ ...link, path: ["source"] }); + const sourceCellId = + (tx?.status().status === "ready" ? tx : runtime.edit()) + .readOrThrow({ ...link, path: ["source"] }); if (!sourceCellId) return undefined; return createCell(runtime, { space: link.space, From 816a4daef9bbca3d252916ce35dda3b75ef54685 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 18 Jul 2025 15:06:07 -0500 Subject: [PATCH 18/19] fix tests that expected non-sanitized schemas --- packages/runner/test/schema.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runner/test/schema.test.ts b/packages/runner/test/schema.test.ts index 2c9d40794..5d4ae28c6 100644 --- a/packages/runner/test/schema.test.ts +++ b/packages/runner/test/schema.test.ts @@ -7,7 +7,7 @@ import { Runtime } from "../src/runtime.ts"; import { Identity } from "@commontools/identity"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; import { toURI } from "../src/uri-utils.ts"; -import { parseLink } from "../src/link-utils.ts"; +import { parseLink, sanitizeSchemaForLinks } from "../src/link-utils.ts"; import { compactifyPaths, txToReactivityLog } from "../src/scheduler.ts"; import type { IExtendedStorageTransaction, @@ -316,7 +316,7 @@ describe("Schema Support", () => { path: ["current", "label"], space, schema: current.schema, - rootSchema: current.rootSchema, + rootSchema: sanitizeSchemaForLinks(current.rootSchema), type: "application/json", }); @@ -343,7 +343,7 @@ describe("Schema Support", () => { space, type: "application/json", schema: omitSchema, - rootSchema: schema, + rootSchema: sanitizeSchemaForLinks(schema), }); const log = txToReactivityLog(tx2); const reads = compactifyPaths(log.reads); From d385bf94d48aeb4e1b55f36828d25d876b8f1fdb Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 18 Jul 2025 15:15:19 -0500 Subject: [PATCH 19/19] rename readOnly to readOnlyReason --- packages/runner/src/cell.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index dfed1f2bd..9dbc79f78 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -357,7 +357,7 @@ function createRegularCell( tx?: IExtendedStorageTransaction, ): Cell { const { space, path, schema, rootSchema } = link; - let readOnly: string | undefined; + let readOnlyReason: string | undefined; const self = { get: () => validateAndTransform(runtime, tx, link), @@ -552,10 +552,10 @@ function createRegularCell( tx.writeOrThrow({ ...link, path: ["source"] }, sourceLink.id); }, freeze: (reason: string) => { - readOnly = reason; + readOnlyReason = reason; runtime.documentMap.getDocByEntityId(link.space, link.id)?.freeze(reason); }, - isFrozen: () => !!readOnly, + isFrozen: () => !!readOnlyReason, toJSON: (): LegacyJSONCellLink => // Keep old format for backward compatibility ({ cell: {