docs: move experiment records to the quark repo

This commit is contained in:
Kit Langton 2026-07-17 23:40:51 -04:00
commit c377b73995
11 changed files with 876 additions and 1565 deletions

View file

@ -0,0 +1,33 @@
# Third-Party Notices
## alien-signals
The internal reactive kernel in `src/reactivity.ts` adapts the dependency
graph algorithm of [alien-signals](https://github.com/stackblitz/alien-signals)
3.2.1: the intrusive doubly-linked dependency and subscriber links with
versioned in-place reuse, and the iterative `propagate` and `checkDirty`
graph walks. alien-signals is not a runtime dependency of Quark.
```
MIT License
Copyright (c) 2024-present Johnson Chu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

View file

@ -14,7 +14,6 @@
"test": "bun --conditions=browser test"
},
"dependencies": {
"alien-signals": "3.2.1",
"solid-js": "catalog:"
}
}

View file

@ -133,7 +133,7 @@ export namespace Keyed {
function publish(slot: Writable<A>, value: A) {
const current = slot()
// alien-signals uses SameValueZero-compatible identity for primitive writes.
// The reactive kernel uses strict identity for primitive writes.
if (current === value || equivalent(current, value)) {
if (options.metrics) options.metrics.equivalenceSuppressions++
return false

View file

@ -1,5 +1,3 @@
import { computed, effect, endBatch, setActiveSub, signal, startBatch } from "alien-signals"
export interface Readable<A> {
(): A
subscribe(listener: (value: A) => void): () => void
@ -10,57 +8,466 @@ export interface Writable<A> extends Readable<A> {
update(f: (value: A) => A): void
}
function subscribe<A>(read: () => A, listener: (value: A) => void) {
let initialized = false
return effect(() => {
const value = read()
if (!initialized) {
initialized = true
return
}
const active = setActiveSub()
try {
listener(value)
} finally {
setActiveSub(active)
}
})
}
export namespace State {
export function make<A>(initial: A): Writable<A> {
const state = signal(initial)
const read = (() => state()) as Writable<A>
read.set = (value) => state(value)
read.update = (f) => {
// Read untracked: calling update inside an effect must not make the
// effect depend on (and re-trigger from) this signal.
const active = setActiveSub()
const current = state()
setActiveSub(active)
state(f(current))
const node: StateNode<A> = {
flags: Flags.Mutable,
value: initial,
pending: initial,
deps: undefined,
depsTail: undefined,
subs: undefined,
subsTail: undefined,
}
read.subscribe = (listener) => subscribe(read, listener)
const read = (() => readState(node)) as Writable<A>
read.set = (value) => writeState(node, value)
read.update = (f) => {
// Read untracked: calling update inside a tracked evaluation must not
// make the caller depend on (and re-trigger from) this state.
const previous = swapActiveSub(undefined)
try {
writeState(node, f(readState(node)))
} finally {
activeSub = previous
}
}
read.subscribe = (listener) => subscribeNode(node, read, listener)
return read
}
}
export namespace Computed {
export function make<A>(evaluate: (previous: A | undefined) => A): Readable<A> {
const value = computed(evaluate)
const read = (() => value()) as Readable<A>
read.subscribe = (listener) => subscribe(read, listener)
const node: ComputedNode<A> = {
flags: Flags.None,
value: undefined,
evaluate,
deps: undefined,
depsTail: undefined,
subs: undefined,
subsTail: undefined,
}
const read = (() => readComputed(node)) as Readable<A>
read.subscribe = (listener) => subscribeNode(node, read, listener)
return read
}
}
export namespace Transaction {
export function run<A>(f: () => A): A {
startBatch()
batchDepth++
try {
return f()
} finally {
endBatch()
if (!--batchDepth) flush()
}
}
}
// Internal reactive kernel.
//
// The dependency graph representation (intrusive doubly-linked dependency and
// subscriber links with versioned in-place reuse) and the iterative
// `propagate`/`checkDirty` walks are adapted from alien-signals 3.2.1
// (https://github.com/stackblitz/alien-signals, MIT). See
// THIRD_PARTY_NOTICES.md. Quark departs from the reference in three ways:
// subscribers are fixed single-dependency watchers instead of general
// effects, orphaned computeds are detached with an iterative work queue
// instead of recursive unwatch callbacks, and reading a computed that is
// currently evaluating throws a stable cycle error instead of looping
// (stackblitz/alien-signals#118, #123).
const enum Flags {
None = 0,
/** The node can produce a new value: states always, computeds once evaluated. */
Mutable = 1,
/** The node is a subscription watcher delivering values to a listener. */
Watching = 2,
/** The computed is currently evaluating; reading it again is a cycle. */
Computing = 4,
/** The watcher is queued for the next flush. */
Queued = 8,
/** The node's value is known stale. */
Dirty = 16,
/** The node's value is possibly stale pending dependency revalidation. */
Pending = 32,
}
interface ReactiveNode {
flags: Flags
deps?: Link
depsTail?: Link
subs?: Link
subsTail?: Link
}
interface StateNode<A = unknown> extends ReactiveNode {
value: A
pending: A
}
interface ComputedNode<A = unknown> extends ReactiveNode {
value: A | undefined
evaluate: (previous: A | undefined) => A
}
interface WatcherNode extends ReactiveNode {
read: () => unknown
listener: (value: unknown) => void
}
interface Link {
version: number
dep: ReactiveNode
sub: ReactiveNode
prevSub: Link | undefined
nextSub: Link | undefined
prevDep: Link | undefined
nextDep: Link | undefined
}
interface Stack<A> {
value: A
prev: Stack<A> | undefined
}
let activeSub: ReactiveNode | undefined
let batchDepth = 0
let version = 0
let flushIndex = 0
let queueLength = 0
const queue: Array<WatcherNode | undefined> = []
const orphans: Array<ReactiveNode> = []
function swapActiveSub(sub: ReactiveNode | undefined): ReactiveNode | undefined {
const previous = activeSub
activeSub = sub
return previous
}
function readState<A>(node: StateNode<A>): A {
if (node.flags & Flags.Dirty && updateState(node) && node.subs !== undefined) {
shallowPropagate(node.subs)
}
if (activeSub !== undefined) link(node, activeSub, version)
return node.value
}
function writeState<A>(node: StateNode<A>, value: A): void {
if (node.pending === (node.pending = value)) return
node.flags = Flags.Mutable | Flags.Dirty
if (node.subs !== undefined) {
propagate(node.subs)
if (!batchDepth) flush()
}
}
function readComputed<A>(node: ComputedNode<A>): A {
const flags = node.flags
if (flags & Flags.Computing) {
throw new Error("Reactive cycle detected: a computed depends on its own value")
}
if (
flags & Flags.Dirty ||
(flags & Flags.Pending &&
(checkDirty(node.deps!, node) || ((node.flags = flags & ~Flags.Pending), false))) ||
!flags
) {
if (updateComputed(node) && node.subs !== undefined) {
shallowPropagate(node.subs)
}
}
if (activeSub !== undefined) link(node, activeSub, version)
return node.value!
}
function updateState(node: StateNode): boolean {
node.flags = Flags.Mutable
return node.value !== (node.value = node.pending)
}
function updateComputed<A>(node: ComputedNode<A>): boolean {
node.depsTail = undefined
node.flags = Flags.Mutable | Flags.Computing
const previous = swapActiveSub(node)
version++
let completed = false
try {
const oldValue = node.value
const changed = oldValue !== (node.value = node.evaluate(oldValue))
completed = true
return changed
} finally {
activeSub = previous
node.flags &= ~Flags.Computing
// A throwing evaluation stays dirty so the next read retries instead of
// serving a stale value with no dependency links.
if (!completed) node.flags |= Flags.Dirty
purgeDeps(node)
}
}
function subscribeNode<A>(node: ReactiveNode, read: () => A, listener: (value: A) => void): () => void {
// Evaluate untracked before linking so a throwing computed leaves no
// partially initialized watcher behind.
const previous = swapActiveSub(undefined)
try {
read()
} finally {
activeSub = previous
}
const watcher: WatcherNode = {
flags: Flags.Watching,
read,
listener: listener as (value: unknown) => void,
deps: undefined,
depsTail: undefined,
}
link(node, watcher, ++version)
return () => {
if (!(watcher.flags & Flags.Watching)) return
watcher.flags &= ~(Flags.Watching | Flags.Dirty | Flags.Pending)
unlink(watcher.deps!, watcher)
drainOrphans()
}
}
function runWatcher(watcher: WatcherNode): void {
const flags = watcher.flags
watcher.flags = flags & ~(Flags.Queued | Flags.Dirty | Flags.Pending)
if (!(flags & Flags.Watching)) return
const dirty =
!!(flags & Flags.Dirty) || (!!(flags & Flags.Pending) && checkDirty(watcher.deps!, watcher))
// Revalidation runs user code in computed evaluations, which may dispose
// this watcher; a disposed watcher must not deliver.
if (!dirty || !(watcher.flags & Flags.Watching)) return
// Listener reads stay untracked and listeners may dispose subscriptions,
// including their own.
const previous = swapActiveSub(undefined)
try {
watcher.listener(watcher.read())
} finally {
activeSub = previous
}
}
function flush(): void {
try {
while (flushIndex < queueLength) {
const watcher = queue[flushIndex]!
queue[flushIndex++] = undefined
runWatcher(watcher)
}
} finally {
// A throwing listener aborts this flush; the remaining watchers keep
// their Dirty/Pending flags and requeue on the next propagation.
while (flushIndex < queueLength) {
const watcher = queue[flushIndex]!
queue[flushIndex++] = undefined
watcher.flags &= ~Flags.Queued
}
flushIndex = 0
queueLength = 0
}
}
function enqueue(watcher: WatcherNode): void {
watcher.flags |= Flags.Queued
queue[queueLength++] = watcher
}
function link(dep: ReactiveNode, sub: ReactiveNode, linkVersion: number): void {
const prevDep = sub.depsTail
if (prevDep !== undefined && prevDep.dep === dep) return
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps
if (nextDep !== undefined && nextDep.dep === dep) {
nextDep.version = linkVersion
sub.depsTail = nextDep
return
}
const prevSub = dep.subsTail
if (prevSub !== undefined && prevSub.version === linkVersion && prevSub.sub === sub) return
const newLink: Link = {
version: linkVersion,
dep,
sub,
prevDep,
nextDep,
prevSub,
nextSub: undefined,
}
sub.depsTail = newLink
dep.subsTail = newLink
if (nextDep !== undefined) nextDep.prevDep = newLink
if (prevDep !== undefined) prevDep.nextDep = newLink
else sub.deps = newLink
if (prevSub !== undefined) prevSub.nextSub = newLink
else dep.subs = newLink
}
function unlink(current: Link, sub: ReactiveNode): Link | undefined {
const dep = current.dep
const prevDep = current.prevDep
const nextDep = current.nextDep
const nextSub = current.nextSub
const prevSub = current.prevSub
if (nextDep !== undefined) nextDep.prevDep = prevDep
else sub.depsTail = prevDep
if (prevDep !== undefined) prevDep.nextDep = nextDep
else sub.deps = nextDep
if (nextSub !== undefined) nextSub.prevSub = prevSub
else dep.subsTail = prevSub
if (prevSub !== undefined) prevSub.nextSub = nextSub
else if ((dep.subs = nextSub) === undefined && dep.deps !== undefined) orphans.push(dep)
return nextDep
}
// Detaches computeds that lost their last subscriber. Iterative on purpose:
// a recursive unwatch cascade overflows the stack on deep chains. Only nodes
// with dependencies enter the queue (states and dep-less computeds have
// nothing to detach). As in the reference, a computed that is read but never
// subscribed stays linked to its sources until they are collected; create
// long-lived computeds rather than per-operation ones.
function drainOrphans(): void {
while (orphans.length > 0) {
const node = orphans.pop()!
if (!("evaluate" in node) || node.depsTail === undefined) continue
node.flags = Flags.Mutable | Flags.Dirty
let current = node.depsTail as Link | undefined
while (current !== undefined) {
const prev = current.prevDep
unlink(current, node)
current = prev
}
}
}
function purgeDeps(sub: ReactiveNode): void {
const depsTail = sub.depsTail
let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps
while (dep !== undefined) {
dep = unlink(dep, sub)
}
drainOrphans()
}
function propagate(current: Link): void {
let next = current.nextSub
let stack: Stack<Link | undefined> | undefined
top: do {
const sub = current.sub
const flags = sub.flags
const unmarked = !(flags & (Flags.Dirty | Flags.Pending))
if (unmarked) sub.flags = flags | Flags.Pending
if (flags & Flags.Watching && !(flags & Flags.Queued)) enqueue(sub as WatcherNode)
if (unmarked && flags & Flags.Mutable) {
const subSubs = sub.subs
if (subSubs !== undefined) {
current = subSubs
const nextSub = subSubs.nextSub
if (nextSub !== undefined) {
stack = { value: next, prev: stack }
next = nextSub
}
continue
}
}
if (next !== undefined) {
current = next
next = current.nextSub
continue
}
while (stack !== undefined) {
const continuation = stack.value
stack = stack.prev
if (continuation !== undefined) {
current = continuation
next = current.nextSub
continue top
}
}
break
} while (true)
}
function shallowPropagate(current: Link): void {
let iterator: Link | undefined = current
do {
const sub = iterator.sub
const flags = sub.flags
if ((flags & (Flags.Pending | Flags.Dirty)) === Flags.Pending) {
sub.flags = flags | Flags.Dirty
if (flags & Flags.Watching && !(flags & Flags.Queued)) enqueue(sub as WatcherNode)
}
iterator = iterator.nextSub
} while (iterator !== undefined)
}
function updateNode(node: ReactiveNode): boolean {
if ("evaluate" in node) return updateComputed(node as ComputedNode)
return updateState(node as StateNode)
}
function checkDirty(current: Link, sub: ReactiveNode): boolean {
let stack: Stack<Link> | undefined
let checkDepth = 0
let dirty = false
top: do {
const dep = current.dep
const flags = dep.flags
if (sub.flags & Flags.Dirty) {
dirty = true
} else if ((flags & (Flags.Mutable | Flags.Dirty)) === (Flags.Mutable | Flags.Dirty)) {
const subs = dep.subs!
if (updateNode(dep)) {
if (subs.nextSub !== undefined) shallowPropagate(subs)
dirty = true
}
} else if ((flags & (Flags.Mutable | Flags.Pending)) === (Flags.Mutable | Flags.Pending)) {
stack = { value: current, prev: stack }
current = dep.deps!
sub = dep
checkDepth++
continue
}
if (!dirty) {
const nextDep = current.nextDep
if (nextDep !== undefined) {
current = nextDep
continue
}
}
while (checkDepth--) {
current = stack!.value
stack = stack!.prev
if (dirty) {
const subs = sub.subs!
if (updateNode(sub)) {
if (subs.nextSub !== undefined) shallowPropagate(subs)
sub = current.sub
continue
}
dirty = false
} else {
sub.flags &= ~Flags.Pending
}
sub = current.sub
const nextDep = current.nextDep
if (nextDep !== undefined) {
current = nextDep
continue top
}
}
return dirty
} while (true)
}

View file

@ -1,13 +1,110 @@
import { describe, expect, it } from "bun:test"
import { createEffect, createRoot } from "solid-js"
import { State } from "../src"
import { useValue } from "../src/solid"
import { Computed, State, Transaction, type Readable } from "../src/reactivity"
describe("reactivity", () => {
it("keeps computed values lazy", () => {
const source = State.make(1)
let evaluations = 0
const doubled = Computed.make(() => {
evaluations++
return source() * 2
})
expect(evaluations).toBe(0)
expect(doubled()).toBe(2)
expect(doubled()).toBe(2)
expect(evaluations).toBe(1)
source.set(2)
expect(evaluations).toBe(1)
expect(doubled()).toBe(4)
expect(evaluations).toBe(2)
})
it("propagates a diamond without glitches", () => {
const source = State.make(1)
const left = Computed.make(() => source() * 2)
const right = Computed.make(() => source() * 3)
const total = Computed.make(() => left() + right())
const values: Array<number> = []
const dispose = total.subscribe((value) => values.push(value))
source.set(2)
expect(values).toEqual([10])
dispose()
})
it("tracks dynamic dependencies", () => {
const enabled = State.make(true)
const left = State.make(1)
const right = State.make(10)
const selected = Computed.make(() => (enabled() ? left() : right()))
const values: Array<number> = []
const dispose = selected.subscribe((value) => values.push(value))
left.set(2)
enabled.set(false)
left.set(3)
right.set(11)
expect(values).toEqual([2, 10, 11])
dispose()
})
it("batches transactions", () => {
const left = State.make(1)
const right = State.make(2)
const total = Computed.make(() => left() + right())
const values: Array<number> = []
const dispose = total.subscribe((value) => values.push(value))
Transaction.run(() => {
left.set(2)
right.set(3)
})
expect(values).toEqual([5])
dispose()
})
it("cuts off unchanged derived values", () => {
const source = State.make(1)
let parityEvaluations = 0
let labelEvaluations = 0
const parity = Computed.make(() => {
parityEvaluations++
return source() % 2
})
const label = Computed.make(() => {
labelEvaluations++
return parity() === 0 ? "even" : "odd"
})
const dispose = label.subscribe(() => {})
source.set(3)
expect(parityEvaluations).toBe(2)
expect(labelEvaluations).toBe(1)
dispose()
})
it("disposes subscriptions", () => {
const source = State.make(1)
const values: Array<number> = []
const dispose = source.subscribe((value) => values.push(value))
source.set(2)
dispose()
source.set(3)
expect(values).toEqual([2])
})
describe("Quark", () => {
it("does not track state read by subscription listeners", () => {
const source = State.make(1)
const unrelated = State.make(1)
const values: number[] = []
const values: Array<number> = []
const dispose = source.subscribe((value) => {
unrelated()
values.push(value)
@ -20,20 +117,311 @@ describe("Quark", () => {
dispose()
})
it("bridges values into a Solid owner and disposes with it", () => {
it("does not track the state read by update", () => {
const trigger = State.make(0)
const updated = State.make(0)
let runs = 0
const dispose = trigger.subscribe(() => {
runs++
updated.update((value) => value + 1)
})
trigger.set(1)
updated.set(10)
expect(runs).toBe(1)
expect(updated()).toBe(10)
dispose()
})
it("delivers the previous value to computed evaluation", () => {
const source = State.make(1)
const values: number[] = []
const previousValues: Array<number | undefined> = []
const running = Computed.make<number>((previous) => {
previousValues.push(previous)
return (previous ?? 0) + source()
})
expect(running()).toBe(1)
source.set(2)
expect(running()).toBe(3)
source.set(3)
expect(running()).toBe(6)
expect(previousValues).toEqual([undefined, 1, 3])
})
it("detaches and reattaches dynamic dependencies", () => {
const enabled = State.make(true)
const left = State.make(1)
const right = State.make(10)
let evaluations = 0
const selected = Computed.make(() => {
evaluations++
return enabled() ? left() : right()
})
const dispose = selected.subscribe(() => {})
expect(evaluations).toBe(1)
enabled.set(false)
expect(evaluations).toBe(2)
// Detached: left writes must not re-evaluate the computed.
left.set(2)
left.set(3)
expect(evaluations).toBe(2)
// Reattached: left writes re-evaluate, right writes no longer do.
enabled.set(true)
expect(evaluations).toBe(3)
expect(selected()).toBe(3)
right.set(11)
expect(evaluations).toBe(3)
left.set(4)
expect(evaluations).toBe(4)
expect(selected()).toBe(4)
dispose()
})
it("notifies a diamond subscriber exactly once per write", () => {
const source = State.make(1)
const left = Computed.make(() => source() * 2)
const right = Computed.make(() => source() * 3)
const total = Computed.make(() => left() + right())
let notifications = 0
const dispose = total.subscribe(() => notifications++)
source.set(2)
source.set(3)
expect(notifications).toBe(2)
dispose()
})
it("batches nested transactions until the outermost ends", () => {
const left = State.make(1)
const right = State.make(2)
const total = Computed.make(() => left() + right())
const values: Array<number> = []
const dispose = total.subscribe((value) => values.push(value))
Transaction.run(() => {
left.set(2)
Transaction.run(() => {
right.set(3)
})
expect(values).toEqual([])
left.set(3)
})
expect(values).toEqual([6])
dispose()
})
it("restores batch state when a transaction throws", () => {
const source = State.make(1)
const values: Array<number> = []
const dispose = source.subscribe((value) => values.push(value))
expect(() =>
Transaction.run(() => {
source.set(2)
throw new Error("boom")
}),
).toThrow("boom")
// The write that happened before the throw still flushes once the
// transaction unwinds, and later writes are not batched.
expect(values).toEqual([2])
source.set(3)
expect(values).toEqual([2, 3])
dispose()
})
it("supports disposing another subscription during notification", () => {
const source = State.make(1)
const first: Array<number> = []
const second: Array<number> = []
let disposeSecond = () => {}
const disposeFirst = source.subscribe((value) => {
first.push(value)
disposeSecond()
})
disposeSecond = source.subscribe((value) => second.push(value))
source.set(2)
source.set(3)
expect(first).toEqual([2, 3])
expect(second).toEqual([])
disposeFirst()
})
it("does not deliver to a watcher disposed during revalidation", () => {
const source = State.make(0)
let dispose = () => {}
createRoot((rootDispose) => {
dispose = rootDispose
const value = useValue(source)
createEffect(() => values.push(value()))
const gate = Computed.make(() => {
const value = source()
if (value > 0) dispose()
return value
})
const values: Array<number> = []
dispose = gate.subscribe((value) => values.push(value))
// Revalidating the watcher re-evaluates gate, whose evaluation disposes
// the subscription before the value could be delivered.
source.set(1)
source.set(2)
expect(values).toEqual([])
})
it("supports a subscription disposing itself during notification", () => {
const source = State.make(1)
const values: Array<number> = []
let dispose = () => {}
dispose = source.subscribe((value) => {
values.push(value)
dispose()
})
source.set(2)
dispose()
source.set(3)
expect(values).toEqual([1, 2])
expect(values).toEqual([2])
})
it("does not track State.update reading its own value", () => {
const counter = State.make(0)
const source = State.make(1)
let evaluations = 0
const tracked = Computed.make(() => {
evaluations++
counter.update((value) => value)
return source()
})
const dispose = tracked.subscribe(() => {})
expect(evaluations).toBe(1)
counter.set(5)
expect(evaluations).toBe(1)
source.set(2)
expect(evaluations).toBe(2)
dispose()
})
it("recovers tracking after a computed evaluation throws", () => {
const shouldThrow = State.make(true)
const source = State.make(1)
const throwing = Computed.make(() => {
if (shouldThrow()) throw new Error("computed boom")
return source()
})
expect(() => throwing()).toThrow("computed boom")
// The failed evaluation must restore the active observer so unrelated
// graphs keep tracking correctly afterwards.
const other = State.make(1)
const doubled = Computed.make(() => other() * 2)
const values: Array<number> = []
const dispose = doubled.subscribe((value) => values.push(value))
other.set(2)
expect(values).toEqual([4])
dispose()
shouldThrow.set(false)
expect(throwing()).toBe(1)
source.set(2)
expect(throwing()).toBe(2)
})
it("keeps notifying other subscribers after a listener throws", () => {
const source = State.make(1)
const values: Array<number> = []
const disposeThrowing = source.subscribe(() => {
throw new Error("listener boom")
})
const dispose = source.subscribe((value) => values.push(value))
expect(() => source.set(2)).toThrow("listener boom")
disposeThrowing()
source.set(3)
expect(values).toContain(3)
dispose()
})
it("leaves no dependency links behind when subscription initialization fails", () => {
const source = State.make(1)
const throwing = Computed.make(() => {
if (source() === 1) throw new Error("init boom")
return source()
})
const values: Array<number> = []
expect(() => throwing.subscribe((value) => values.push(value))).toThrow("init boom")
// The failed subscription must not stay linked to the graph.
source.set(2)
source.set(3)
expect(values).toEqual([])
// The computed itself remains usable.
expect(throwing()).toBe(3)
})
it("propagates deep chains without recursive stack growth", () => {
// A cold pull of an unevaluated chain necessarily nests user getter
// frames, so each layer is evaluated as it is built. The kernel-owned
// paths under test are dirty propagation and revalidation, which must
// walk the full depth iteratively.
const depth = 100_000
const source = State.make(0)
const chain = Array.from({ length: depth }).reduce<Readable<number>>((current) => {
const next = Computed.make(() => current() + 1)
next()
return next
}, source)
expect(chain()).toBe(depth)
const values: Array<number> = []
const dispose = chain.subscribe((value) => values.push(value))
source.set(1)
expect(values).toEqual([depth + 1])
dispose()
})
it("propagates wide fan-out without recursive stack growth", () => {
const width = 50_000
const source = State.make(0)
const nodes = Array.from({ length: width }, () => Computed.make(() => source() + 1))
const total = Computed.make(() => nodes.reduce((sum, node) => sum + node(), 0))
let sink = 0
const dispose = total.subscribe((value) => {
sink = value
})
source.set(1)
expect(sink).toBe(width * 2)
dispose()
})
it("fails deterministically on cyclic computed dependencies", () => {
// Regression for stackblitz/alien-signals#123: mutually dependent
// computed graphs must throw a stable error instead of looping or
// exhausting memory.
const fieldA = State.make(false)
const fieldB = State.make(false)
const a: Readable<boolean | null> = Computed.make(() => (b() !== true ? fieldA() : null))
const b: Readable<boolean | null> = Computed.make(() => (a() !== true ? fieldB() : null))
// Every read that reaches the cycle fails the same way; a failed
// evaluation stays dirty and retries instead of serving a stale value.
expect(() => a()).toThrow(/cycle/i)
expect(() => b()).toThrow(/cycle/i)
fieldA.set(true)
expect(() => a()).toThrow(/cycle/i)
})
})

View file

@ -7,7 +7,7 @@ import { Keyed, Layout } from "effect-quark"
* Streaming events name their part on the wire (text/reasoning by ordinal,
* tools by callID); each assistant message owns one keyed collection so a
* delta publishes exactly one slot instead of reconciling a content array.
* See docs/design/quark-message-content.md.
* Design record: quark repo, docs/experiments/quark-message-content.md.
*/
export namespace SessionContent {
type ContentPart = SessionMessageAssistant["content"][number]