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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions packages/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,12 @@ export interface IKeyableOpaque<T> {
key<K extends PropertyKey>(
this: IsThisObject,
valueKey: K,
): unknown extends K ? OpaqueRef<any>
: K extends keyof UnwrapCell<T> ? (0 extends (1 & T) ? OpaqueRef<any>
: UnwrapCell<T>[K] extends never ? OpaqueRef<any>
: UnwrapCell<T>[K] extends BrandedCell<infer U> ? OpaqueRef<U>
: OpaqueRef<UnwrapCell<T>[K]>)
: OpaqueRef<any>;
): unknown extends K ? OpaqueCell<any>
: K extends keyof UnwrapCell<T> ? (0 extends (1 & T) ? OpaqueCell<any>
: UnwrapCell<T>[K] extends never ? OpaqueCell<any>
: UnwrapCell<T>[K] extends BrandedCell<infer U> ? OpaqueCell<U>
: OpaqueCell<UnwrapCell<T>[K]>)
: OpaqueCell<any>;
}

/**
Expand Down Expand Up @@ -352,12 +352,16 @@ export interface WriteonlyCell<T>
* OpaqueRef is a variant of OpaqueCell with recursive proxy behavior.
* Each key access returns another OpaqueRef, allowing chained property access.
* This is temporary until AST transformation handles .key() automatically.
*
* OpaqueRef<Cell<T>> unwraps to Cell<T>.
*/
export type OpaqueRef<T> =
& OpaqueCell<T>
& (T extends Array<infer U> ? Array<OpaqueRef<U>>
: T extends object ? { [K in keyof T]: OpaqueRef<T[K]> }
: T);
export type OpaqueRef<T> = T extends BrandedCell<any> ? T
:
& OpaqueCell<T>
& (T extends Array<infer U> ? Array<OpaqueRef<U>>
: T extends BrandedCell<any> ? T
: T extends object ? { [K in keyof T]: OpaqueRef<T[K]> }
: T);

// ============================================================================
// CellLike and Opaque - Utility types for accepting cells
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

interface ConditionalBranchArgs {
value: Default<number, 0>;
enabled: Default<boolean, false>;
enabled: Cell<Default<boolean, false>>;
}

const toggleFlag = handler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ export const counterWithConditionalChildInstantiation = recipe<
) => {
const existing = childSlot.get();
if (!state.active) {
if (existing !== undefined) childSlot.set(undefined);
if (existing !== undefined) {
(childSlot as unknown as Cell<ChildCounterState | undefined>).set(
undefined,
);
}
return state.active;
}
if (existing === undefined) {
Expand Down
7 changes: 4 additions & 3 deletions packages/runner/src/builder/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ModuleFactory,
NodeRef,
Opaque,
OpaqueCell,
OpaqueRef,
Schema,
SchemaWithoutCell,
Expand Down Expand Up @@ -41,7 +42,7 @@ export function createNodeFactory<T = any, R = any>(
const node: NodeRef = { module, inputs, outputs, frame: getTopFrame() };

connectInputAndOutputs(node);
outputs.connect(node);
(outputs as OpaqueCell<R>).connect(node);

return outputs;
}, module);
Expand Down Expand Up @@ -160,7 +161,7 @@ export function handler<E, T>(
const stream = opaqueRef<E>(undefined, eventSchema);

// Set stream marker (cast to E as stream is typed for the events it accepts)
stream.set({ $stream: true } as E);
(stream as OpaqueCell<E>).set({ $stream: true } as E);
const node: NodeRef = {
module,
inputs: { $ctx: props, $event: stream },
Expand All @@ -169,7 +170,7 @@ export function handler<E, T>(
};

connectInputAndOutputs(node);
stream.connect(node);
(stream as OpaqueCell<E>).connect(node);

return stream;
}, module);
Expand Down
8 changes: 4 additions & 4 deletions packages/runner/src/builder/node-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
makeOpaqueRef,
type NodeRef,
type Opaque,
type OpaqueRef,
OpaqueCell,
} from "./types.ts";
import { ContextualFlowControl } from "../cfc.ts";
import { traverseValue } from "./traverse-utils.ts";
Expand Down Expand Up @@ -56,7 +56,7 @@ export function applyInputIfcToOutput<T, R>(
const cfc = new ContextualFlowControl();
traverseValue(inputs, (item: unknown) => {
if (isOpaqueCell(item)) {
const { schema: inputSchema, rootSchema } = (item as OpaqueRef<T>)
const { schema: inputSchema, rootSchema } = (item as OpaqueCell<T>)
.export();
if (inputSchema !== undefined) {
ContextualFlowControl.joinSchema(
Expand All @@ -81,7 +81,7 @@ function attachCfcToOutputs<T, R>(
lubClassification: string,
) {
if (isOpaqueCell(outputs)) {
const exported = (outputs as OpaqueRef<T>).export();
const exported = (outputs as OpaqueCell<T>).export();
const outputSchema = exported.schema ?? true;
// we may have fields in the output schema, so incorporate those
const joined = new Set<string>([lubClassification]);
Expand All @@ -99,7 +99,7 @@ function attachCfcToOutputs<T, R>(
...outpuSchemaObj,
ifc,
};
(outputs as OpaqueRef<T>).setSchema(cfcSchema);
(outputs as OpaqueCell<T>).setSchema(cfcSchema);
return;
} else if (isRecord(outputs)) {
// Descend into objects and arrays
Expand Down
8 changes: 7 additions & 1 deletion packages/runner/src/builder/opaque-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ export function opaqueRef<T>(
export: () => {
// Store's schema won't be the same as ours as a nested proxy
// We also don't adjust the defaultValue to be relative to our path
return { cell: top, ...store, path, rootSchema, schema: nestedSchema };
return {
cell: top as OpaqueCell<T>,
...store,
path,
rootSchema,
schema: nestedSchema,
};
},
unsafe_bindToRecipeAndPath: (
recipe: Recipe,
Expand Down
5 changes: 3 additions & 2 deletions packages/runner/src/builder/recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type Node,
type NodeRef,
type Opaque,
type OpaqueCell,
type OpaqueRef,
type Recipe,
type RecipeFactory,
Expand Down Expand Up @@ -293,7 +294,7 @@ function factoryFromRecipe<T, R>(

// Collect default values for the inputs
const defaults = toJSONWithLegacyAliases(
inputs.export().defaultValue ?? {},
(inputs as OpaqueCell<T>).export().defaultValue ?? {},
paths,
true,
)!;
Expand Down Expand Up @@ -386,7 +387,7 @@ function factoryFromRecipe<T, R>(
};

connectInputAndOutputs(node);
outputs.connect(node);
(outputs as OpaqueCell<R>).connect(node);

return outputs;
}, recipe) satisfies RecipeFactory<T, R>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ export default recipe({
properties: {
count: {
type: "number",
asCell: true,
asOpaque: true
asCell: true
}
},
required: ["count"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ export default recipe({
properties: {
count: {
type: "number",
asCell: true,
asOpaque: true
asCell: true
}
},
required: ["count"]
Expand Down
2 changes: 1 addition & 1 deletion recipes/simple-draw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const UserSession = recipe<
y={derive(m, (msg) => msg.y)}
hidden={ifElse(
derive(m, (msg) => msg.hidden === true),
"true",
"true" as const,
undefined,
)}
onpositionchange={updateMessagePosition({
Expand Down