Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
4924aec
feat(runner): Add ability to create cells without links and unify cel…
seefeldb Nov 4, 2025
47d14bd
deno fmt/lint
seefeldb Nov 4, 2025
01b9d2d
more deno fmt/lint
seefeldb Nov 4, 2025
5399d36
feat(runner): Implement shared CauseContainer for sibling cells
seefeldb Nov 4, 2025
b8728bd
feat(api): Change .for() polarity to fail by default
seefeldb Nov 4, 2025
83b8055
update done tasks
seefeldb Nov 4, 2025
2168ba5
feat(runner): Ensure frame.space is always set and add inHandler flag
seefeldb Nov 4, 2025
b67a004
feat(runner): First merge of OpaqueRef and Cell - add shared interface
seefeldb Nov 4, 2025
142aa6a
throw when shadowrefs are used & remove tests that relied on that
seefeldb Oct 24, 2025
9b05154
disable unsafe_materialize as well
seefeldb Oct 24, 2025
1066ab3
better error message for closed over variables in lift/derive
seefeldb Oct 24, 2025
400e93a
fix tests by no longer overriding types to not be OpaqueRef
seefeldb Oct 24, 2025
cdd6d4f
remove accidental closures to fix tests
seefeldb Oct 24, 2025
2d7c896
fix(runner): Replace isOpaqueRef with isOpaqueCell after merge
seefeldb Nov 4, 2025
01c8b09
feat(runner): Refactor opaqueRef to create Cells with runtime binding
seefeldb Nov 4, 2025
3bf9b75
feat(runner): Refactor builder to use runtime-aware Cell-backed Opaqu…
seefeldb Nov 5, 2025
d5916d5
chore: Update tests and remove setDefault references
seefeldb Nov 5, 2025
5296304
refactor(runner): Move proxy wrapping logic to CellImpl.getAsOpaqueRe…
seefeldb Nov 5, 2025
fc42a34
removed toOpaqueRef, changed every instance to use isCellResult (form…
seefeldb Nov 5, 2025
3a3c48e
refactor(runner): Use frame-based runtime access instead of *Impl clo…
seefeldb Nov 5, 2025
d5a167c
feat(runner): Add default frame with runtime at construction time
seefeldb Nov 5, 2025
af8d315
refactor(runner): Complete OpaqueRef/Cell merge, remove legacy proxy …
seefeldb Nov 5, 2025
a8ae702
refactor(runner): Add CellKind system and simplify stream/default han…
seefeldb Nov 6, 2025
17029ea
fix: use list of methods rather than inspecting `this`, as the latter…
seefeldb Nov 6, 2025
d8bc47d
use `string` as `any` for brands
seefeldb Nov 6, 2025
fe2c351
fix test by removing out of order frame
seefeldb Nov 6, 2025
2e58d28
fix another test by creating runtimes
seefeldb Nov 6, 2025
f9e6f15
re-introduce cell: number in legacy aliases, as we need it for nested…
seefeldb Nov 6, 2025
3130319
since .set() isn't really supported anymore, we don't need to travers…
seefeldb Nov 6, 2025
019607c
remove out of order pushFrame
seefeldb Nov 6, 2025
491f1b2
resolve link before calling setRaw
seefeldb Nov 6, 2025
0e09dc3
use frame from cell to assign ids + disable this for non-handler fram…
seefeldb Nov 6, 2025
2219dcf
actually, never mind, just assume the new behavior: creating ids
seefeldb Nov 6, 2025
b75c437
fix order of frame vs cell creation
seefeldb Nov 6, 2025
9e3f8d7
fix linter errors
seefeldb Nov 6, 2025
34a5276
one more fmt
seefeldb Nov 6, 2025
3727321
disable .only in wish test
seefeldb Nov 6, 2025
a7f5c09
remove unneeded IOpaqueCell augmentation (it's all in AnyCell already)
seefeldb Nov 6, 2025
9894442
fix: post-rebase type corrections and cleanup
seefeldb Nov 6, 2025
e12fc8a
missing edits for prior commit
seefeldb Nov 6, 2025
56a030d
fix(runner): correctly pop frame in case of exceptions
seefeldb Nov 6, 2025
0ffdcef
lint
seefeldb Nov 6, 2025
973f248
Revert built-in module structure to match main branch
seefeldb Nov 6, 2025
31dc4b0
fix test to no longer close over variables
seefeldb Nov 6, 2025
205a031
properly type-check cell brands
seefeldb Nov 6, 2025
e1c20f7
fix CELL_BRAND to be the passed in string again
seefeldb Nov 6, 2025
544428a
fix test that should have used a derive all along
seefeldb Nov 6, 2025
adb3442
remove unnecessary cast
seefeldb Nov 6, 2025
a9279ef
lint!
seefeldb Nov 6, 2025
0a1b52c
- clean up pushFrame a bit
seefeldb Nov 7, 2025
ada4a3c
hack in support for initial values until default schema propagation w…
seefeldb Nov 7, 2025
2bdd534
turn off setting defaults for cell() for now, since { default: ... } …
seefeldb Nov 7, 2025
7924280
fix linter issues + correctly revert generateText change
seefeldb Nov 7, 2025
9785fc6
fix pending nursery integration test
seefeldb Nov 7, 2025
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
42 changes: 24 additions & 18 deletions docs/specs/recipe-construction/rollout-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,43 @@
- [ ] Make passing the output of the second into the first work. Tricky
because we're doing almost opposite expansions on the type.
- [ ] Add ability to create a cell without a link yet.
- [ ] Change constructor for RegularCell to make link optional
- [ ] Add .for method to set a cause (within current context)
- [ ] second parameter to make it optional/flexible:
- [ ] ignores the .for if link already exists
- [x] Merge StreamCell into RegularCell and rename RegularCell to CellImpl
- [x] Primarily this means changing `.set` to first read the resolved value
to see whether we have a stream and then use the stream behavior instead
of regular set.
- [x] Change constructor for RegularCell to make link optional
- [x] Add .for method to set a cause (within current context)
- [x] second parameter to make it optional/flexible:
- [x] ignores the .for if link already exists
- [ ] adds extension if cause already exists (see tracker below)
- [ ] Make .key work even if there is no cause yet.
- [ ] Add some method to force creation of cause, which errors if in
- [x] Make .key work even if there is no cause yet.
- [x] Add some method to force creation of cause, which errors if in
non-handler context and no other information was given (as e.g. deriving
nodes, which do have ids, after asking for them -- this walks the graph up
until it hits the passed in cells)
- [ ] For now though throw in non-handler context when needing a link and it
- [x] For now though throw in non-handler context when needing a link and it
isn't there, e.g. because we need to create a link to the cell (when passed
into `anotherCell.set()` for example). We want to encourage .for use in
ambiguous cases.
- [x] Add space and event to Frame
- [ ] First merge of OpaqueRef and RegularCell
- [ ] Add methods that allow linking to node invocations
- [ ] `setPreExisting` can be deprecated (used in toOpaqueRef which itself
can go away, see below)
- [ ] `setDefault` can be deprecated
- [ ] `setSchema` is tricky (asSchema is cleaner). Let's support it for now,
- [x] Create OpaqueCell type
- [x] Make OpaqueRef a proxy around OpaqueCell
- [x] Add methods to Cell that allow linking to node invocations
- [x] `setPreExisting` deprecated
- [x] `setDefault` deprecated
- [x] `setSchema` is tricky (asSchema is cleaner). Let's support it for now,
but only if the cause isn't set yet.
- [ ] `connect` copy over and add a direction field, so can distinguish
- [x] `connect` copy over and add a direction field, so can distinguish
where this node is used as input vs where the passed node is an input to
this node.
- [ ] `export` make the analogous version, if link is present use that as
- [x] `export` make the analogous version, if link is present use that as
`external`.
- [ ] `map` and `mapWithPattern`: Copy over
- [ ] `toJSON` return `null` when no link otherwise what Cell does.
- [ ] No need for `toOpaqueRef` anymore, since all cells are now also
- [x] `map` and `mapWithPattern`: Copy over
- [x] `toJSON` return `null` when no link otherwise what Cell does.
- [x] No need for `toOpaqueRef` anymore, since all cells are now also
OpaqueRef. So remove all that.
- [ ] Call that for returned value in lift/handler, with a .for("assigned
- [x] Call that for returned value in lift/handler, with a .for("assigned
variable of property", true)
- [ ] For now treat result as recipe, but it should be one where all nodes
already have links associated with them (no internal necessary).
Expand Down
123 changes: 79 additions & 44 deletions packages/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,37 @@ export declare const CELL_BRAND: unique symbol;
* Used for type-level operations like unwrapping nested cells without
* creating circular dependencies.
*/
export type BrandedCell<T, Brand extends string = string> = {
[CELL_BRAND]: Brand;
export type CellKind =
| "cell"
| "opaque"
| "stream"
| "comparable"
| "readonly"
| "writeonly";

// `string` acts as `any`, e.g. when wanting to match any kind of cell
export type AnyBrandedCell<T, Kind extends string = string> = {
[CELL_BRAND]: Kind;
};

export type BrandedCell<T, Kind extends CellKind> = AnyBrandedCell<T, Kind>;

// ============================================================================
// Cell Capability Interfaces
// ============================================================================

// To constrain methods that only exists on objects
type IsThisObject =
export type IsThisObject =
| IsThisArray
| BrandedCell<JSONObject>
| BrandedCell<Record<string, unknown>>;
| AnyBrandedCell<JSONObject>
| AnyBrandedCell<Record<string, unknown>>;

type IsThisArray =
| BrandedCell<JSONArray>
| BrandedCell<Array<unknown>>
| BrandedCell<Array<any>>
| BrandedCell<unknown>
| BrandedCell<any>;
export type IsThisArray =
| AnyBrandedCell<JSONArray>
| AnyBrandedCell<Array<unknown>>
| AnyBrandedCell<Array<any>>
| AnyBrandedCell<unknown>
| AnyBrandedCell<any>;

/*
* IAnyCell is an interface that is used by all calls and to which the runner
Expand Down Expand Up @@ -166,7 +177,7 @@ export interface IKeyable<out T, Wrap extends HKT> {
export type KeyResultType<T, K, Wrap extends HKT> = [unknown] extends [K]
? Apply<Wrap, any> // variance guard for K = any
: [0] extends [1 & T] ? Apply<Wrap, any> // keep any as-is
: T extends BrandedCell<any, any> // wrapping a cell? delegate to it's .key
: T extends AnyBrandedCell<any, any> // wrapping a cell? delegate to it's .key
? (T extends { key(k: K): infer R } ? R : Apply<Wrap, never>)
: Apply<Wrap, K extends keyof T ? T[K] : any>; // select key, fallback to any

Expand All @@ -183,16 +194,29 @@ export interface IKeyableOpaque<T> {
): 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>
: UnwrapCell<T>[K] extends AnyBrandedCell<infer U> ? OpaqueCell<U>
: OpaqueCell<UnwrapCell<T>[K]>)
: OpaqueCell<any>;
}

/**
* Cells that can be created with a cause.
*/
export interface ICreatable<C extends AnyBrandedCell<any>> {
/**
* Set a cause for this cell. Used to create a link when the cell doesn't have
* one yet.
* @param cause - The cause to associate with this cell
* @returns This cell for method chaining
*/
for(cause: unknown): C;
}

/**
* Cells that can be resolved back to a Cell.
* Only available on full Cell<T>, not on OpaqueCell or Stream.
*/
export interface IResolvable<T, C extends BrandedCell<T>> {
export interface IResolvable<T, C extends AnyBrandedCell<T>> {
resolveAsCell(): C;
}

Expand Down Expand Up @@ -220,7 +244,7 @@ export interface IDerivable<T> {
): OpaqueRef<S[]>;
mapWithPattern<S>(
this: IsThisObject,
op: Recipe,
op: RecipeFactory<T extends Array<infer U> ? U : T, S>,
params: Record<string, any>,
): OpaqueRef<S[]>;
}
Expand All @@ -231,12 +255,6 @@ export interface IOpaquable<T> {
/** deprecated */
set(newValue: Opaque<Partial<T>>): void;
/** deprecated */
setDefault(value: Opaque<T> | T): void;
/** deprecated */
setPreExisting(ref: any): void;
/** deprecated */
setName(name: string): void;
/** deprecated */
setSchema(schema: JSONSchema): void;
}

Expand All @@ -249,7 +267,7 @@ export interface IOpaquable<T> {
* interface with internal only API. Uses a second symbol brand to distinguish
* from core cell brand without any methods.
*/
export interface AnyCell<T = unknown> extends BrandedCell<T>, IAnyCell<T> {
export interface AnyCell<T = unknown> extends AnyBrandedCell<T>, IAnyCell<T> {
}

/**
Expand All @@ -258,7 +276,12 @@ export interface AnyCell<T = unknown> extends BrandedCell<T>, IAnyCell<T> {
* Does NOT have .get()/.set()/.send()/.equals()/.resolveAsCell()
*/
export interface IOpaqueCell<T>
extends IKeyableOpaque<T>, IDerivable<T>, IOpaquable<T> {}
extends
IAnyCell<T>,
ICreatable<AnyBrandedCell<T>>,
IKeyableOpaque<T>,
IDerivable<T>,
IOpaquable<T> {}

export interface OpaqueCell<T>
extends BrandedCell<T, "opaque">, IOpaqueCell<T> {}
Expand All @@ -276,11 +299,13 @@ export interface AsCell extends HKT {
export interface ICell<T>
extends
IAnyCell<T>,
ICreatable<AnyBrandedCell<T>>,
IReadable<T>,
IWritable<T>,
IStreamable<T>,
IEquatable,
IKeyable<T, AsCell>,
IDerivable<T>,
IResolvable<T, Cell<T>> {}

export interface Cell<T = unknown> extends BrandedCell<T, "cell">, ICell<T> {}
Expand All @@ -293,7 +318,11 @@ export interface Cell<T = unknown> extends BrandedCell<T, "cell">, ICell<T> {}
* Note: This is an interface (not a type) to allow module augmentation by the runtime.
*/
export interface Stream<T>
extends BrandedCell<T, "stream">, IAnyCell<T>, IStreamable<T> {}
extends
BrandedCell<T, "stream">,
IAnyCell<T>,
ICreatable<Stream<T>>,
IStreamable<T> {}

/**
* Comparable-only cell - just for equality checks and keying.
Expand All @@ -308,6 +337,7 @@ export interface ComparableCell<T>
extends
BrandedCell<T, "comparable">,
IAnyCell<T>,
ICreatable<ComparableCell<T>>,
IEquatable,
IKeyable<T, AsComparableCell> {}

Expand All @@ -324,6 +354,7 @@ export interface ReadonlyCell<T>
extends
BrandedCell<T, "readonly">,
IAnyCell<T>,
ICreatable<ReadonlyCell<T>>,
IReadable<T>,
IEquatable,
IKeyable<T, AsReadonlyCell> {}
Expand All @@ -341,6 +372,7 @@ export interface WriteonlyCell<T>
extends
BrandedCell<T, "writeonly">,
IAnyCell<T>,
ICreatable<WriteonlyCell<T>>,
IWritable<T>,
IKeyable<T, AsWriteonlyCell> {}

Expand All @@ -355,11 +387,11 @@ export interface WriteonlyCell<T>
*
* OpaqueRef<Cell<T>> unwraps to Cell<T>.
*/
export type OpaqueRef<T> = T extends BrandedCell<any> ? T
export type OpaqueRef<T> = T extends AnyBrandedCell<any> ? T
:
& OpaqueCell<T>
& (T extends Array<infer U> ? Array<OpaqueRef<U>>
: T extends BrandedCell<any> ? T
: T extends AnyBrandedCell<any> ? T
: T extends object ? { [K in keyof T]: OpaqueRef<T[K]> }
: T);

Expand All @@ -373,10 +405,10 @@ export type OpaqueRef<T> = T extends BrandedCell<any> ? T
*
* Note: This is primarily used for type constraints that require a cell.
*/
export type CellLike<T> = BrandedCell<MaybeCellWrapped<T>>;
export type CellLike<T> = AnyBrandedCell<MaybeCellWrapped<T>>;
type MaybeCellWrapped<T> =
| T
| BrandedCell<T>
| AnyBrandedCell<T>
| (T extends Array<infer U> ? Array<MaybeCellWrapped<U>>
: T extends object ? { [K in keyof T]: MaybeCellWrapped<T[K]> }
: never);
Expand All @@ -393,10 +425,10 @@ type MaybeCellWrapped<T> =
export type Opaque<T> =
| T
// We have to list them explicitly so Typescript can unwrap them. Doesn't seem
// to work if we just say BrandedCell<T>
// to work if we just say AnyBrandedCell<T>
| OpaqueRef<T>
| AnyCell<T>
| BrandedCell<T>
| AnyBrandedCell<T>
| OpaqueCell<T>
| Cell<T>
| Stream<T>
Expand All @@ -408,9 +440,9 @@ export type Opaque<T> =
: T);

/**
* Recursively unwraps BrandedCell types at any nesting level.
* UnwrapCell<BrandedCell<BrandedCell<string>>> = string
* UnwrapCell<BrandedCell<{ a: BrandedCell<number> }>> = { a: BrandedCell<number> }
* Recursively unwraps AnyBrandedCell types at any nesting level.
* UnwrapCell<AnyBrandedCell<AnyBrandedCell<string>>> = string
* UnwrapCell<AnyBrandedCell<{ a: AnyBrandedCell<number> }>> = { a: AnyBrandedCell<number> }
*
* Special cases:
* - UnwrapCell<any> = any
Expand All @@ -419,8 +451,8 @@ export type Opaque<T> =
export type UnwrapCell<T> =
// Preserve any
0 extends (1 & T) ? T
// Unwrap BrandedCell
: T extends BrandedCell<infer S> ? UnwrapCell<S>
// Unwrap AnyBrandedCell
: T extends AnyBrandedCell<infer S> ? UnwrapCell<S>
// Otherwise return as-is
: T;

Expand All @@ -433,19 +465,19 @@ export type UnwrapCell<T> =
* allows controlling id generation and can only be passed to write operations.
*/
export type AnyCellWrapping<T> =
// Handle existing BrandedCell<> types, allowing unwrapping
T extends BrandedCell<infer U>
? AnyCellWrapping<U> | BrandedCell<AnyCellWrapping<U>>
// Handle existing AnyBrandedCell<> types, allowing unwrapping
T extends AnyBrandedCell<infer U>
? AnyCellWrapping<U> | AnyBrandedCell<AnyCellWrapping<U>>
// Handle arrays
: T extends Array<infer U>
? Array<AnyCellWrapping<U>> | BrandedCell<Array<AnyCellWrapping<U>>>
? Array<AnyCellWrapping<U>> | AnyBrandedCell<Array<AnyCellWrapping<U>>>
// Handle objects (excluding null)
: T extends object ?
| { [K in keyof T]: AnyCellWrapping<T[K]> }
& { [ID]?: AnyCellWrapping<JSONValue>; [ID_FIELD]?: string }
| BrandedCell<{ [K in keyof T]: AnyCellWrapping<T[K]> }>
| AnyBrandedCell<{ [K in keyof T]: AnyCellWrapping<T[K]> }>
// Handle primitives
: T | BrandedCell<T>;
: T | AnyBrandedCell<T>;

// Factory types

Expand Down Expand Up @@ -970,8 +1002,11 @@ export type CreateCellFunction = {
export type Default<T, V extends T = T> = T;

// Re-export opaque ref creators
export type CellFunction = <T>(value?: T, schema?: JSONSchema) => OpaqueRef<T>;
export type StreamFunction = <T>(initial?: T) => OpaqueRef<T>;
export type CellFunction = <T>(
defaultValue?: T,
schema?: JSONSchema,
) => OpaqueRef<T>;
export type StreamFunction = <T>(schema?: JSONSchema) => OpaqueRef<T>;
export type ByRefFunction = <T, R>(ref: string) => ModuleFactory<T, R>;

export type HFunction = {
Expand Down Expand Up @@ -1316,7 +1351,7 @@ export type Props = {
/** A child in a view can be one of a few things */
export type RenderNode =
| InnerRenderNode
| BrandedCell<InnerRenderNode>
| AnyBrandedCell<InnerRenderNode>
| Array<RenderNode>;

type InnerRenderNode =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,15 @@ export const counterWithBatchedHandlerUpdates = recipe<BatchedCounterArgs>(
typeof input === "string" && input.length > 0 ? input : "idle"
)(lastNote);

const lastTotal = derive(historyView, (entries) => {
if (entries.length === 0) {
return currentValue.get();
}
return entries[entries.length - 1];
});
const lastTotal = derive(
{ entries: historyView, current: currentValue },
({ entries, current }) => {
if (entries.length === 0) {
return current;
}
return entries[entries.length - 1];
},
);

const summary =
str`Processed ${processed} increments over ${batches} batches (${noteView})`;
Expand Down
Loading