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
24 changes: 13 additions & 11 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,36 @@ Runtime layer built on top of `@reflex/core`.
- reactive execution helpers
- scheduler-oriented runtime APIs

### `@reflex/reactive`
### `@volynets/reflex`

Public application-facing facade.

- `signal`
- `computed`
- `memo`
- `effect`
- `flush`
- `batchWrite`
- `createRuntime`
- `map` / `filter` / `merge`
- `scan` / `hold` / `subscribeOnce`

## Recommended Entry Point

For application code, start with `@reflex/reactive`.
For application code, start with `@volynets/reflex`.

```ts
import { signal, computed, effect, flush } from "@reflex/reactive";
import { signal, computed, effect, createRuntime } from "@volynets/reflex";

const count = signal(0);
const double = computed(() => count.read() * 2);
const rt = createRuntime();

const [count, setCount] = signal(0);
const double = computed(() => count() * 2);

effect(() => {
console.log(count.read(), double());
console.log(count(), double());
});

count.write(5);
flush();
setCount(5);
rt.flush();
```

## Architecture
Expand All @@ -58,7 +60,7 @@ The repository is currently organized around three active layers:

1. `@reflex/core`
2. `@reflex/runtime`
3. `@reflex/reactive`
3. `@volynets/reflex`

There is also a DOM adapter in the repository as `reflex-dom`.

Expand Down
2 changes: 1 addition & 1 deletion packages/@reflex/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reflex/runtime",
"version": "1.1.0",
"version": "0.1.0",
"type": "module",
"description": "Runtime / Universe / Laws for Reflex",
"license": "MIT",
Expand Down
3 changes: 0 additions & 3 deletions packages/@reflex/runtime/src/api/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { ExecutionContext } from "../reactivity/context";
import { compare as defaultComparator } from "./compare";
import { devAssertWriteAlive, devRecordWriteProducer } from "../reactivity/dev";
import {
DIRTY_STATE,
IMMEDIATE,
isDisposedNode,
propagate,
Expand Down Expand Up @@ -90,8 +89,6 @@ export function writeProducer<T>(

// Update the payload to the new value
node.payload = value;
// Clear any stale dirty bits from before (node is now clean with new value)
node.state &= ~DIRTY_STATE;

// Get the first subscriber edge (if any)
const firstSubscriberEdge = node.firstOut;
Expand Down
44 changes: 44 additions & 0 deletions packages/@reflex/runtime/src/reactivity/shape/ReactiveMeta.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
import type ReactiveNode from "./ReactiveNode";

/**
* Bit flags describing the current role and lifecycle state of a reactive node.
*
* Layout:
* - exactly one kind bit should normally be present: Producer / Consumer / Watcher
* - dirty bits are mutually exclusive in supported flows: Invalid or Changed
* - walker bits (`Visited`, `Tracking`) are transient and only meaningful during
* propagation / pull-walk execution
*
* High-level semantics:
* - `Changed` means "upstream change is already confirmed, recompute directly"
* - `Invalid` means "upstream may have changed, verify through shouldRecompute()"
* - producers commit on write and should not normally participate in pull-walk
*/
export enum ReactiveNodeState {
/** Mutable source node. Holds committed payload directly and never recomputes. */
Producer = 1 << 0,
/** Pure computed node. Re-executes lazily when its dependencies become dirty. */
Consumer = 1 << 1,
/** Effect-like sink. Invalidations schedule or notify work rather than return data. */
Watcher = 1 << 2,

/** Maybe stale: value is not confirmed changed yet and must be verified on read. */
Invalid = 1 << 3,
/** Definitely stale: a direct upstream dependency already confirmed a change. */
Changed = 1 << 4,
/** Re-entrant marker used when a tracked dependency invalidates mid-computation. */
Visited = 1 << 5,
/** Terminal lifecycle state. Disposed nodes must no longer participate in the graph. */
Disposed = 1 << 6,
/** Node is currently executing its compute function. Used for cycle detection. */
Computing = 1 << 7,
/** Watcher has already been scheduled/notified for the current invalidation wave. */
Scheduled = 1 << 8,
/** Node is collecting dependencies during the current computation pass. */
Tracking = 1 << 9,
}

/** Mask for the mutually-exclusive node kind bits. */
export const NODE_KIND_STATE =
ReactiveNodeState.Producer |
ReactiveNodeState.Consumer |
Expand All @@ -22,56 +47,75 @@ export const NODE_KIND_STATE =
// export const MAYBE_CHANGE_STATE = ReactiveNodeState.Invalid;
// export const CHANGED_STATE = ReactiveNodeState.Changed;

/** All dirty bits. In supported runtime flows this is either `Invalid` or `Changed`. */
export const DIRTY_STATE =
ReactiveNodeState.Invalid | ReactiveNodeState.Changed;

/** Clean producer. Normal steady state for source nodes. */
export const PRODUCER_INITIAL_STATE = ReactiveNodeState.Producer;

/**
* Legacy/testing helper for a producer carrying `Changed`.
* Runtime write flow should normally commit producers immediately instead.
*/
export const PRODUCER_CHANGED =
ReactiveNodeState.Producer | ReactiveNodeState.Changed;

/** Legacy/testing helper for any dirty producer state. */
export const PRODUCER_DIRTY = ReactiveNodeState.Producer | DIRTY_STATE;

/** Directly invalidated computed node: skip verification and recompute on read. */
export const CONSUMER_CHANGED =
ReactiveNodeState.Changed | ReactiveNodeState.Consumer;

/** Computed node carrying either `Invalid` or `Changed`. */
export const CONSUMER_DIRTY = ReactiveNodeState.Consumer | DIRTY_STATE;

/** Directly invalidated watcher. */
export const WATCHER_CHANGED =
ReactiveNodeState.Changed | ReactiveNodeState.Watcher;

/** Transient walker-only bits that should not survive a settled execution. */
export const WALKER_STATE =
ReactiveNodeState.Visited | ReactiveNodeState.Tracking;

/** Clear the re-entrant marker after the walker no longer needs it. */
export function clearNodeVisited(node: ReactiveNode): void {
node.state &= ~ReactiveNodeState.Visited;
}

/** Enter dependency collection mode for the current compute pass. */
export function beginNodeTracking(node: ReactiveNode): void {
node.state =
(node.state & ~ReactiveNodeState.Visited) | ReactiveNodeState.Tracking;
}

/** Leave dependency collection mode after compute finishes. */
export function clearNodeTracking(node: ReactiveNode): void {
node.state &= ~ReactiveNodeState.Tracking;
}

/** Mark a node as actively executing its compute function. */
export function markNodeComputing(node: ReactiveNode): void {
node.state |= ReactiveNodeState.Computing;
}

/** Clear the active-computation marker. */
export function clearNodeComputing(node: ReactiveNode): void {
node.state &= ~ReactiveNodeState.Computing;
}

/** Clear both `Invalid` and `Changed`, returning the node to a clean state. */
export function clearDirtyState(node: ReactiveNode): void {
node.state &= ~DIRTY_STATE;
}

/** Runtime helper for the terminal lifecycle check. */
export function isDisposedNode(node: ReactiveNode): boolean {
return (node.state & ReactiveNodeState.Disposed) !== 0;
}

/** Collapse a node to kind + disposed, dropping transient execution flags. */
export function markDisposedNode(node: ReactiveNode): void {
node.state = (node.state & NODE_KIND_STATE) | ReactiveNodeState.Disposed;
}
35 changes: 1 addition & 34 deletions packages/@reflex/runtime/src/reactivity/shape/ReactiveNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,23 @@ const UNINITIALIZED: unique symbol = Symbol.for("UNINITIALIZED");

class ReactiveNode<T = unknown> implements Reactivable {
state: number;
compute: ComputeFn<T>;
firstOut: ReactiveEdge | null;
firstIn: ReactiveEdge | null;
lastOut: ReactiveEdge | null;
lastIn: ReactiveEdge | null;
compute: ComputeFn<T>;
depsTail: ReactiveEdge | null;
payload: T;

constructor(payload: T | undefined, compute: ComputeFn<T>, state: number) {
this.state = state;
this.compute = compute;
this.firstOut = null;
this.firstIn = null;
this.lastOut = null;
this.lastIn = null;
this.depsTail = null;
this.payload = payload as T;
}
}

type ComputeFnAsync<T> = () => Promise<T> & Promise<T>;

export class ReactiveNodeAsync<T, E> implements ReactiveNode {
phase: number;

state: number;
compute: ComputeFnAsync<T>;
firstOut: ReactiveEdge | null;
firstIn: ReactiveEdge | null;
lastOut: ReactiveEdge | null;
lastIn: ReactiveEdge | null;
depsTail: ReactiveEdge | null;
payload: T | E;
pending: T | null;

constructor(
payload: T | undefined,
compute: ComputeFnAsync<T>,
state: number,
) {
this.phase = 0;
this.state = state;
this.compute = compute;
this.firstOut = null;
this.firstIn = null;
this.lastOut = null;
this.lastIn = null;
this.depsTail = null;
this.payload = payload as T;
this.pending = null;
}
}

Expand Down
38 changes: 20 additions & 18 deletions packages/@reflex/runtime/src/reactivity/walkers/propagate.branch.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,69 @@
import type { ExecutionContext } from "../context";
import type { ReactiveEdge } from "../shape";
import { ReactiveNodeState } from "../shape";
import { INVALIDATION_SLOW_PATH_MASK, NON_IMMEDIATE } from "./propagate.constants";
import { propagateBranching } from "./propagate.branching"
import { CAN_ESCAPE_INVALIDATION, NON_IMMEDIATE } from "./propagate.constants";
import { propagateBranching } from "./propagate.branching";
import { getSlowInvalidatedSubscriberState } from "./propagate.utils";
import {
recordPropagation,
notifyWatcherInvalidation,
} from "./propagationWatchers";
} from "./propagation.watchers";

// ─── propagateBranch ──────────────────────────────────────────────────────────
//
// Hot path: tight loop for chains with no fanout.
// Escalates to propagateBranching the moment a sibling edge appears.
//
// Promotion fix: when escalating, `promote` (not hardcoded NON_IMMEDIATE) is
// Promotion fix: when escalating, `promoteBit` (not hardcoded NON_IMMEDIATE) is
// passed as `resumePromote` so the sibling `next` stays in the correct
// promotion zone. The child level (firstOut) still resets to NON_IMMEDIATE.
export function propagateBranch(
edge: ReactiveEdge,
promote: number,
promoteBit: number,
thrown: unknown,
context: ExecutionContext,
): unknown {
while (true) {
const sub = edge.to;
const state = sub.state;
const nextState =
(state & INVALIDATION_SLOW_PATH_MASK) === 0
? state |
(promote ? ReactiveNodeState.Changed : ReactiveNodeState.Invalid)
: getSlowInvalidatedSubscriberState(edge, state, promote);
let nextState = 0;

if ((state & CAN_ESCAPE_INVALIDATION) === 0) {
nextState = state | promoteBit;
} else {
nextState = getSlowInvalidatedSubscriberState(edge, state, promoteBit);
}

const next = edge.nextOut;

if (nextState) {
sub.state = nextState;
if (__DEV__) recordPropagation(edge, nextState, promote, context);
if (__DEV__) recordPropagation(edge, nextState, promoteBit, context);

if ((nextState & ReactiveNodeState.Watcher) !== 0) {
thrown = notifyWatcherInvalidation(sub, thrown, context);
} else {
const firstOut = sub.firstOut;
if (firstOut !== null) {
edge = firstOut;
if (next !== null) {
// Fanout: escalate. `next` is a sibling at the current promote level.
return propagateBranching(
edge,
NON_IMMEDIATE, // child level always starts non-immediate
firstOut,
next,
promote, // fix: siblings inherit current promote, not NON_IMMEDIATE
promoteBit,
thrown,
context,
);
}
promote = NON_IMMEDIATE;

edge = firstOut;
promoteBit = NON_IMMEDIATE;
continue;
}
}
}

if (next === null) return thrown;
edge = next;
// promote stays the same: siblings at the same level share promotion status
// promoteBit stays the same: siblings at the same level share promotion status
}
}
Loading
Loading