From aa3e0038d1a859302901ce3afc18d70611e8509c Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Tue, 1 Apr 2025 15:41:44 -0700 Subject: [PATCH 1/2] Type checking: Allow inputs to writing cell methods to have a different Cell<> structure than the destination cell --- builder/src/types.ts | 8 ++++--- runner/src/cell.ts | 48 +++++++++++++++++++++++++++++----------- runner/test/cell.test.ts | 36 +++++++++++++----------------- 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/builder/src/types.ts b/builder/src/types.ts index ef39b60f4..ee7c7fe08 100644 --- a/builder/src/types.ts +++ b/builder/src/types.ts @@ -1,7 +1,7 @@ import { isObj } from "@commontools/utils"; -export const ID: symbol = Symbol("ID, unique to the context"); -export const ID_FIELD: symbol = Symbol( +export const ID: unique symbol = Symbol("ID, unique to the context"); +export const ID_FIELD: unique symbol = Symbol( "ID_FIELD, name of sibling that contains id", ); @@ -108,9 +108,11 @@ export type JSONValue = | boolean | null | JSONValue[] - | { [key: string]: JSONValue }; + | { [key: string]: JSONValue } & { [ID]?: any; [ID_FIELD]?: any }; export type JSONSchema = { + readonly [ID]?: any; + readonly [ID_FIELD]?: any; readonly type?: | "object" | "array" diff --git a/runner/src/cell.ts b/runner/src/cell.ts index 3ea5c7ecd..ea99f08a0 100644 --- a/runner/src/cell.ts +++ b/runner/src/cell.ts @@ -1,5 +1,10 @@ import { isStreamAlias, TYPE } from "@commontools/builder"; -import { getTopFrame, ID, type JSONSchema } from "@commontools/builder"; +import { + getTopFrame, + ID, + ID_FIELD, + type JSONSchema, +} from "@commontools/builder"; import { type DeepKeyLookup, type DocImpl, getDoc, isDoc } from "./doc.ts"; import { createQueryResultProxy, @@ -103,15 +108,13 @@ import { type Schema } from "@commontools/builder"; */ export interface Cell { get(): T; - set(value: T): void; - send(value: T): void; - update(values: Partial): void; + set(value: Cellify | T): void; + send(value: Cellify | T): void; + update(values: Cellify | T>): void; push( ...value: Array< - | (T extends Array ? U : any) - | DocImpl ? U : any> + | (T extends Array ? (Cellify | U | DocImpl) : any) | CellLink - | Cell ? U : any> > ): void; equals(other: Cell): boolean; @@ -164,6 +167,26 @@ export interface Cell { copyTrap: boolean; } +/** + * Cellify is a type utility that allows any part of type T to be wrapped in + * Cell<>, and allow any part of T that is currently wrapped in Cell<> to be + * used unwrapped. This is designed for use with Cell method parameters, + * allowing flexibility in how values are passed. + */ +export type Cellify = + // Handle existing Cell<> types, allowing unwrapping + T extends Cell ? Cellify | Cell> + // Handle arrays + : T extends Array ? Array> | Cell>> + // Handle objects (excluding null), adding optional ID fields + : T extends object ? + | ({ [K in keyof T]: Cellify } & { [ID]?: any; [ID_FIELD]?: any }) + | Cell< + { [K in keyof T]: Cellify } & { [ID]?: any; [ID_FIELD]?: any } + > + // Handle primitives + : T | Cell; + export interface Stream { send(event: T): void; sink(callback: (event: T) => Cancel | undefined | void): Cancel; @@ -359,15 +382,15 @@ function createRegularCell( const self = { get: () => validateAndTransform(doc, path, schema, log, rootSchema), - set: (newValue: T) => + set: (newValue: Cellify) => diffAndUpdate( resolveLinkToAlias(doc, path, log), newValue, log, getTopFrame()?.cause, ), - send: (newValue: T) => self.set(newValue), - update: (values: Partial) => { + send: (newValue: Cellify) => self.set(newValue), + update: (values: Cellify>) => { if (typeof values !== "object" || values === null) { throw new Error("Can't update with non-object value"); } @@ -378,10 +401,9 @@ function createRegularCell( }, push: ( ...values: Array< - | (T extends Array ? U : any) - | DocImpl ? U : any> + | (T extends Array ? Cellify : any) + | DocImpl ? Cellify : any> | CellLink - | Cell ? U : any> > ) => { // Follow aliases and references, since we want to get to an assumed diff --git a/runner/test/cell.test.ts b/runner/test/cell.test.ts index d5a50c7ba..a34e24379 100644 --- a/runner/test/cell.test.ts +++ b/runner/test/cell.test.ts @@ -4,13 +4,7 @@ import { type DocImpl, getDoc, isDoc } from "../src/doc.ts"; import { isCell, isCellLink } from "../src/cell.ts"; import { isQueryResult } from "../src/query-result-proxy.ts"; import { type ReactivityLog } from "../src/scheduler.ts"; -import { - getTopFrame, - ID, - JSONSchema, - popFrame, - pushFrame, -} from "@commontools/builder"; +import { ID, JSONSchema, popFrame, pushFrame } from "@commontools/builder"; import { addEventHandler, idle } from "../src/scheduler.ts"; import { addCommonIDfromObjectID } from "../src/utils.ts"; @@ -562,7 +556,7 @@ describe("asCell with schema", () => { }, }, required: ["name", "age", "tags", "nested"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -596,7 +590,7 @@ describe("asCell with schema", () => { }, }, required: ["id", "metadata"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const value = c.asCell([], undefined, schema).get(); @@ -643,7 +637,7 @@ describe("asCell with schema", () => { }, }, required: ["name", "children"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const value = c.asCell([], undefined, schema).get(); @@ -700,7 +694,7 @@ describe("asCell with schema", () => { }, }, required: ["user"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const userCell = cell.key("user"); @@ -831,7 +825,7 @@ describe("asCell with schema", () => { }, }, required: ["id", "context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -873,7 +867,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -919,7 +913,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -965,7 +959,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -1032,7 +1026,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const log = { reads: [], writes: [] } as ReactivityLog; const cell = c.asCell([], log, schema); @@ -1082,7 +1076,7 @@ describe("asCell with schema", () => { }, }, required: ["items"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const itemsCell = cell.key("items"); @@ -1115,7 +1109,7 @@ describe("asCell with schema", () => { value: { type: "number" }, }, }, - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); @@ -1149,7 +1143,7 @@ describe("asCell with schema", () => { type: "object", properties: { anything: { asCell: true } }, }, - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); @@ -1226,8 +1220,8 @@ describe("asCell with schema", () => { const schema = { type: "array", items: { type: "object", properties: { value: { type: "number" } } }, - default: [{ [ID]: "test", "value": 10 }, { [ID]: "test2", "value": 20 }], - } as JSONSchema; + default: [{ [ID]: "test", value: 10 }, { [ID]: "test2", value: 20 }], + } as const satisfies JSONSchema; const c = getDoc({}, "push-to-undefined-schema-stable-id", "test"); const arrayCell = c.asCell(["items"], undefined, schema); From 4bae72ace09a3afed5770d645be856cb043c18b4 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Tue, 1 Apr 2025 15:54:20 -0700 Subject: [PATCH 2/2] throw type error for update() being called with something that isn't an object --- runner/src/cell.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/runner/src/cell.ts b/runner/src/cell.ts index ea99f08a0..70749e4dc 100644 --- a/runner/src/cell.ts +++ b/runner/src/cell.ts @@ -110,7 +110,9 @@ export interface Cell { get(): T; set(value: Cellify | T): void; send(value: Cellify | T): void; - update(values: Cellify | T>): void; + update | Partial>>( + values: V extends object ? V : never, + ): void; push( ...value: Array< | (T extends Array ? (Cellify | U | DocImpl) : any) @@ -401,8 +403,7 @@ function createRegularCell( }, push: ( ...values: Array< - | (T extends Array ? Cellify : any) - | DocImpl ? Cellify : any> + | (T extends Array ? (Cellify | U | DocImpl) : any) | CellLink > ) => {