From 2a8ad383ea92ebb674b8f0f2f5fe485249286ef2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Jul 2026 23:45:56 -0400 Subject: [PATCH] chore(quark): sync reactive kernel and rename package to @opencode-ai/quark --- packages/quark/package.json | 2 +- packages/quark/src/layout.ts | 232 +++++++++----------- packages/quark/src/solid.ts | 6 +- packages/tui/bench/session-timeline.ts | 2 +- packages/tui/package.json | 2 +- packages/tui/src/routes/session/content.ts | 2 +- packages/tui/src/routes/session/index.tsx | 2 +- packages/tui/src/routes/session/rows.ts | 4 +- packages/tui/src/routes/session/timeline.ts | 2 +- 9 files changed, 119 insertions(+), 135 deletions(-) diff --git a/packages/quark/package.json b/packages/quark/package.json index bd1ae13ccf..642d1593d6 100644 --- a/packages/quark/package.json +++ b/packages/quark/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "name": "effect-quark", + "name": "@opencode-ai/quark", "version": "0.0.0", "private": true, "type": "module", diff --git a/packages/quark/src/layout.ts b/packages/quark/src/layout.ts index 85677be2c8..662006f09e 100644 --- a/packages/quark/src/layout.ts +++ b/packages/quark/src/layout.ts @@ -74,13 +74,17 @@ export namespace Layout { make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Keyed.Keyed } + type IndexField = A extends unknown ? keyof A : never + export interface MembersIndex { readonly type: "members" + readonly fields?: readonly IndexField[] readonly extract: (value: A) => Iterable } export interface FirstIndex { readonly type: "first" + readonly fields?: readonly IndexField[] readonly matches: (value: A) => boolean } @@ -114,7 +118,7 @@ export namespace Layout { /** * Comparator backend. "generated" (the default) compiles one specialized - * Function per plan; "closure" is the compatibility fallback for + * Function per plan operation; "closure" is the compatibility fallback for * environments that forbid runtime code generation (CSP). */ export interface CompileOptions { @@ -123,7 +127,9 @@ export namespace Layout { export interface IndexBuilder { members(extract: (value: A) => Iterable): MembersIndex + members(fields: readonly IndexField[], extract: (value: A) => Iterable): MembersIndex first(matches: (value: A) => boolean): FirstIndex + first(fields: readonly IndexField[], matches: (value: A) => boolean): FirstIndex } export interface Collection> extends Keyed.Keyed { @@ -270,8 +276,14 @@ export namespace Layout { export function collection(input: unknown, define: unknown, options?: CompileOptions): unknown { const plan = compile(input as never, options) as Plan const definitions = (define as (index: IndexBuilder) => Indexes)({ - members: (extract) => ({ type: "members", extract }), - first: (matches) => ({ type: "first", matches }), + members: ((fieldsOrExtract: readonly PropertyKey[] | ((value: unknown) => Iterable), extract?: (value: unknown) => Iterable) => + typeof fieldsOrExtract === "function" + ? { type: "members", extract: fieldsOrExtract } + : { type: "members", fields: fieldsOrExtract, extract: extract! }) as IndexBuilder["members"], + first: ((fieldsOrMatches: readonly PropertyKey[] | ((value: unknown) => boolean), matches?: (value: unknown) => boolean) => + typeof fieldsOrMatches === "function" + ? { type: "first", matches: fieldsOrMatches } + : { type: "first", fields: fieldsOrMatches, matches: matches! }) as IndexBuilder["first"], }) return { ...plan, @@ -314,52 +326,22 @@ export namespace Layout { ): Collection { // `pending` hands a diff the collection already computed for a staged // mutation to the comparator, so equivalence costs one comparison, not two. - let pending: number | ReadonlyMap | undefined + let pending: { readonly previous: A; readonly next: A; readonly mask: number } | undefined const values = Keyed.make({ key: plan.keyOf, equivalent(left, right) { - const mask = typeof pending === "number" ? pending : pending?.get(plan.keyOf(left)) - return (mask ?? plan.diff(left, right)) === 0 + if (pending && pending.previous === left && pending.next === right) { + const mask = pending.mask + pending = undefined + return mask === 0 + } + return plan.diff(left, right) === 0 }, metrics: options?.metrics, }) - // Projection dependency tracker: extract/matches run against a proxy that - // records which top-level fields they read, as a bitmask aligned with - // plan.diff. Enumerating keys or reading an unknown property is - // conservative (all bits). Soundness: a pure projection re-reads a field - // only if the fields it previously read are unchanged or it is re-run. - let trackTarget: Record | undefined - let trackMask = 0 - const trackProxy = new Proxy({} as Record, { - get(_, property) { - trackMask |= plan.bits.get(property) ?? -1 - return trackTarget![property] - }, - has(_, property) { - trackMask |= plan.bits.get(property) ?? -1 - return property in trackTarget! - }, - ownKeys() { - trackMask = -1 - return Reflect.ownKeys(trackTarget!) - }, - getOwnPropertyDescriptor(_, property) { - trackMask = -1 - const descriptor = Reflect.getOwnPropertyDescriptor(trackTarget!, property) - return descriptor && { ...descriptor, configurable: true } - }, - }) - function tracked(value: A, f: (value: A) => T) { - trackTarget = value as Record - trackMask = 0 - try { - const result = f(trackProxy as A) - return { result, reads: trackMask } - } finally { - trackTarget = undefined - trackMask = 0 - } + function dependencyMask(fields: readonly PropertyKey[] | undefined) { + return fields?.reduce((mask, field) => mask | (plan.bits.get(normalizeName(field)) ?? -1), 0) ?? -1 } // Each index entry stages user-code projections before any state mutation @@ -369,20 +351,23 @@ export namespace Layout { type MemberChange = { readonly add?: readonly unknown[]; readonly remove?: readonly unknown[] } interface Entry { readonly name: PropertyKey - reads(key: Key): number | undefined + readonly reads: number stage(key: Key, value: A, mode: "add" | "replace"): Commit applyDelta?(key: Key, change: MemberChange): void remove(key: Key, slot?: Readable): void - afterPlacement(slot: Readable): void - afterRebuild(): void + afterPlacement?(slot: Readable): void + afterRebuild?(): void member?(candidate: unknown): boolean firstSlot?(): Readable | undefined } - function membersEntry(name: PropertyKey, extract: (value: A) => Iterable): Entry { + function membersEntry( + name: PropertyKey, + fields: readonly PropertyKey[] | undefined, + extract: (value: A) => Iterable, + ): Entry { const counts = new Map() const byKey = new Map; readonly members: Set }>() - const readsByKey = new Map() function adjust(member: unknown, amount: 1 | -1) { const count = (counts.get(member) ?? 0) + amount @@ -395,24 +380,18 @@ export namespace Layout { return { name, - reads: (key) => readsByKey.get(key), + reads: dependencyMask(fields), stage(key, value) { const previous = byKey.get(key) - const trackedMembers = tracked(value, (candidate) => { - const source = extract(candidate) - return { - source, - members: source === previous?.source ? previous.members : new Set(source), - } - }) + const source = extract(value) + const members = source === previous?.source ? previous.members : new Set(source) return () => { - readsByKey.set(key, trackedMembers.reads) - if (!previous) trackedMembers.result.members.forEach((member) => adjust(member, 1)) - if (previous && previous.members !== trackedMembers.result.members) { - trackedMembers.result.members.forEach((member) => !previous.members.has(member) && adjust(member, 1)) - previous.members.forEach((member) => !trackedMembers.result.members.has(member) && adjust(member, -1)) + if (!previous) members.forEach((member) => adjust(member, 1)) + if (previous && previous.members !== members) { + members.forEach((member) => !previous.members.has(member) && adjust(member, 1)) + previous.members.forEach((member) => !members.has(member) && adjust(member, -1)) } - byKey.set(key, trackedMembers.result) + byKey.set(key, { source, members }) } }, applyDelta(key, change) { @@ -425,25 +404,23 @@ export namespace Layout { members.add(member) adjust(member, 1) }) - // The set was mutated in place: poison the source-identity check and - // drop observed reads so the next non-delta projection recomputes. + // The set was mutated in place, so poison the source-identity check. byKey.set(key, { source: members, members }) - readsByKey.delete(key) }, remove(key) { byKey.get(key)?.members.forEach((member) => adjust(member, -1)) byKey.delete(key) - readsByKey.delete(key) }, - afterPlacement() {}, - afterRebuild() {}, member: (candidate) => counts.has(candidate), } } - function firstEntry(name: PropertyKey, matches: (value: A) => boolean): Entry { + function firstEntry( + name: PropertyKey, + fields: readonly PropertyKey[] | undefined, + matches: (value: A) => boolean, + ): Entry { const matching = new Set() - const readsByKey = new Map() let slot: Readable | undefined function findFirst() { @@ -472,25 +449,23 @@ export namespace Layout { return { name, - reads: (key) => readsByKey.get(key), + reads: dependencyMask(fields), stage(key, value, mode) { - const matched = tracked(value, matches) + const matched = matches(value) return (valueSlot) => { - readsByKey.set(key, matched.reads) const previous = matching.has(key) - if (matched.result) matching.add(key) - if (!matched.result) matching.delete(key) + if (matched) matching.add(key) + if (!matched) matching.delete(key) if (mode === "add") return - if (slot === valueSlot && !matched.result) { + if (slot === valueSlot && !matched) { findFirst() return } - if (slot !== valueSlot && !previous && matched.result) afterPlacement(valueSlot) + if (slot !== valueSlot && !previous && matched) afterPlacement(valueSlot) } }, remove(key, removedSlot) { matching.delete(key) - readsByKey.delete(key) if (removedSlot && slot === removedSlot) findFirst() }, afterPlacement, @@ -501,13 +476,13 @@ export namespace Layout { const entries: Entry[] = Reflect.ownKeys(definitions).map((name) => { const definition = definitions[name] - if (definition.type === "members") return membersEntry(name, definition.extract) - return firstEntry(name, definition.matches) + if (definition.type === "members") return membersEntry(name, definition.fields, definition.extract) + return firstEntry(name, definition.fields, definition.matches) }) const byName = new Map(entries.map((entry) => [entry.name, entry])) // Stage refresh work for one changed value: explicit member deltas win, - // then projections whose observed reads are disjoint from the change are + // then projections whose declared fields are disjoint from the change are // skipped entirely. Returns undefined when no index work is required. function stageRefresh(key: Key, value: A, mask: number, changes?: unknown) { const record = changes as Readonly> | undefined @@ -519,14 +494,19 @@ export namespace Layout { ;(staged ??= []).push(() => delta(key, change)) continue } - const reads = entry.reads(key) - if (reads !== undefined && (mask & reads) === 0) continue + if ((mask & entry.reads) === 0) continue ;(staged ??= []).push(entry.stage(key, value, "replace")) } return staged } - function publishStaged(value: A, mask: number, staged: readonly Commit[] | undefined, slot: Readable) { + function publishStaged( + previous: A, + value: A, + mask: number, + staged: readonly Commit[] | undefined, + slot: Readable, + ) { // Indexes are plain data, not reactive state; the transaction exists so // subscribers cannot observe published values with stale indexes. With // no staged commits there is nothing to observe out of order. @@ -538,16 +518,12 @@ export namespace Layout { }) function publishValue() { - return withPending(mask, () => values.update(value)) - } - } - - function withPending(mask: number | ReadonlyMap, f: () => T) { - pending = mask - try { - return f() - } finally { - pending = undefined + pending = { previous, next: value, mask } + try { + return values.update(value) + } finally { + pending = undefined + } } } @@ -555,13 +531,13 @@ export namespace Layout { ...values, set(next) { const keys = next.map(plan.keyOf) - if (new Set(keys).size !== keys.length) throw new Error("Keyed values must have unique keys") + const retained = new Set(keys) + if (retained.size !== keys.length) throw new Error("Keyed values must have unique keys") const existing = new Map() values.slots().forEach((slot) => { const value = slot() existing.set(plan.keyOf(value), value) }) - const masks = new Map() // Stage user-code projections for new and changed values only; // retained equivalent values keep their index state and reads. const staged: Array<{ readonly key: Key; readonly commits: readonly Commit[] }> = [] @@ -571,44 +547,52 @@ export namespace Layout { staged.push({ key, commits: entries.map((entry) => entry.stage(key, value, "add")) }) return } - const mask = plan.diff(existing.get(key)!, value) - masks.set(key, mask) + const previous = existing.get(key)! + if (previous === value) return + const mask = plan.diff(previous, value) if (mask === 0) return const commits = stageRefresh(key, value, mask) if (commits) staged.push({ key, commits }) }) - const retained = new Set(keys) + const structure = values.slots() return Transaction.run(() => { - const changed = withPending(masks, () => values.set(next)) - existing.forEach((_value, key) => { - if (!retained.has(key)) entries.forEach((entry) => entry.remove(key)) - }) + const changed = values.set(next) + if (!changed) return false + const structural = structure !== values.slots() + if (structural) + existing.forEach((_value, key) => { + if (!retained.has(key)) entries.forEach((entry) => entry.remove(key)) + }) staged.forEach((item) => { const slot = values.get(item.key)! item.commits.forEach((commit) => commit(slot)) }) - entries.forEach((entry) => entry.afterRebuild()) - return changed + if (structural) entries.forEach((entry) => entry.afterRebuild?.()) + return true }) }, update(value) { const key = plan.keyOf(value) const slot = values.get(key) if (!slot) return false - const mask = plan.diff(slot(), value) + const previous = slot() + if (previous === value) return false + const mask = plan.diff(previous, value) const staged = mask === 0 ? undefined : stageRefresh(key, value, mask) - return publishStaged(value, mask, staged, slot) + return publishStaged(previous, value, mask, staged, slot) }, modify(key, f, changes) { const slot = values.get(key) if (!slot) return false - const value = f(slot()) + const previous = slot() + const value = f(previous) // Publication below targets the slot for the value's own key, so the // modify key-preservation invariant must be enforced here. if (values.get(plan.keyOf(value)) !== slot) throw new Error("Keyed modify must preserve the value key") - const mask = plan.diff(slot(), value) + if (previous === value) return false + const mask = plan.diff(previous, value) const staged = mask === 0 ? undefined : stageRefresh(key, value, mask, changes?.members) - return publishStaged(value, mask, staged, slot) + return publishStaged(previous, value, mask, staged, slot) }, insert(value, position) { const key = plan.keyOf(value) @@ -618,7 +602,7 @@ export namespace Layout { return Transaction.run(() => { const slot = values.insert(value, position) staged.forEach((commit) => commit(slot)) - entries.forEach((entry) => entry.afterPlacement(slot)) + entries.forEach((entry) => entry.afterPlacement?.(slot)) return slot }) }, @@ -636,7 +620,7 @@ export namespace Layout { if (!slot) return false return Transaction.run(() => { const moved = values.move(key, position) - if (moved) entries.forEach((entry) => entry.afterPlacement(slot)) + if (moved) entries.forEach((entry) => entry.afterPlacement?.(slot)) return moved }) }, @@ -712,7 +696,7 @@ export namespace Layout { const factory = Function( "custom", `${emitter.declarations.join("\n")}\nreturn (left, right) => { let changed = 0\n${statements.join("\n")}\nreturn changed }`, - ) as (custom: ReadonlyArray["equivalent"]>) => (left: A, right: A) => number + ) as (custom: ReadonlyArray>) => (left: A, right: A) => number return factory(emitter.custom) } @@ -792,8 +776,8 @@ export namespace Layout { const fields = model.perVariant.get(name) if (!fields) { const variant = variants[name] - const index = emitter.custom.push((left, right) => variant.equivalent(left, right)) - 1 - return `case ${label}: return custom[${index}](left, right) ? 0 : -1` + const index = emitter.custom.push(variant) - 1 + return `case ${label}: return custom[${index}].equivalent(left, right) ? 0 : -1` } const statements = fields.map((field) => { const fieldProperty = JSON.stringify(String(field.name)) @@ -804,17 +788,17 @@ export namespace Layout { const factory = Function( "custom", `${emitter.declarations.join("\n")}\nreturn (left, right) => { if (left[${property}] !== right[${property}]) return -1; switch (left[${property}]) { ${cases.join("\n")}\ndefault: return -1 } }`, - ) as (custom: ReadonlyArray["equivalent"]>) => (left: unknown, right: unknown) => number + ) as (custom: ReadonlyArray>) => (left: unknown, right: unknown) => number return factory(emitter.custom) } - // Whole-tree generation: one Function() per plan whose source inlines struct - // comparisons, emits real loops for arrays, and hoists unions into named - // inner functions the engine can inline. User-supplied equivalence functions - // remain indirect calls through the `custom` array; everything structural - // compiles to direct code with no interior closure boundaries. + // Whole-tree generation: one Function() per plan operation whose source + // inlines struct comparisons, emits real loops for arrays, and hoists unions + // into named inner functions the engine can inline. User-supplied equivalence + // functions remain indirect calls through the `custom` array; everything + // structural compiles to direct code with no interior closure boundaries. type Emitter = { - readonly custom: Array["equivalent"]> + readonly custom: Field[] readonly declarations: string[] readonly names: Map } @@ -849,7 +833,7 @@ export namespace Layout { function assemble(emitter: Emitter, body: string) { const factory = Function("custom", `${emitter.declarations.join("\n")}\nreturn (left, right) => ${body}`) as ( - custom: ReadonlyArray["equivalent"]>, + custom: ReadonlyArray>, ) => (left: A, right: A) => boolean return factory(emitter.custom) } @@ -896,8 +880,8 @@ export namespace Layout { }) return `${name}(${left}, ${right})` } - const index = emitter.custom.push((left, right) => field.equivalent(left, right)) - 1 - return `custom[${index}](${left}, ${right})` + const index = emitter.custom.push(field) - 1 + return `custom[${index}].equivalent(${left}, ${right})` } function declareUnion( diff --git a/packages/quark/src/solid.ts b/packages/quark/src/solid.ts index 6be648be09..69c90360da 100644 --- a/packages/quark/src/solid.ts +++ b/packages/quark/src/solid.ts @@ -7,9 +7,9 @@ export function useValue(readable: Readable): Accessor { } /** - * Reactive accessor for one keyed slot. Tracks structure only until the slot - * exists; afterwards value changes flow through the slot alone, so unrelated - * structural churn cannot re-render the consumer. + * Reactive accessor for one keyed slot. Structural changes re-resolve the + * slot, while memo equality prevents an unchanged slot from propagating to the + * consumer. Value changes flow through the slot itself. */ export function useSlot(keyed: Keyed.Keyed, key: () => Key): Accessor { const structure = useValue(keyed.slots) diff --git a/packages/tui/bench/session-timeline.ts b/packages/tui/bench/session-timeline.ts index adcdfd0483..e3ff4ef7f4 100644 --- a/packages/tui/bench/session-timeline.ts +++ b/packages/tui/bench/session-timeline.ts @@ -1,4 +1,4 @@ -import { Keyed } from "effect-quark" +import { Keyed } from "@opencode-ai/quark" import { createStore, produce } from "solid-js/store" import { SessionTimeline, type PartRef } from "../src/routes/session/timeline" import { createHarness, type Workload } from "../../quark/bench/harness" diff --git a/packages/tui/package.json b/packages/tui/package.json index 655f980e1c..4cbfbc3a62 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -87,7 +87,7 @@ "clipboardy": "4.0.0", "diff": "catalog:", "effect": "catalog:", - "effect-quark": "workspace:*", + "@opencode-ai/quark": "workspace:*", "fuzzysort": "catalog:", "get-east-asian-width": "catalog:", "open": "10.1.2", diff --git a/packages/tui/src/routes/session/content.ts b/packages/tui/src/routes/session/content.ts index bd454f84af..f4c0a8de20 100644 --- a/packages/tui/src/routes/session/content.ts +++ b/packages/tui/src/routes/session/content.ts @@ -1,5 +1,5 @@ import type { SessionMessageAssistant } from "@opencode-ai/client" -import { Keyed, Layout } from "effect-quark" +import { Keyed, Layout } from "@opencode-ai/quark" /** * Stable per-part reactive slots for assistant message content. diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index d5967e239e..1f77617219 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -13,7 +13,7 @@ import { Switch, useContext, } from "solid-js" -import { KeyedFor, useSlot, useValue } from "effect-quark/solid" +import { KeyedFor, useSlot, useValue } from "@opencode-ai/quark/solid" import { SessionContent } from "./content" import path from "node:path" import { EOL, tmpdir } from "node:os" diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 90cf0dfb00..931dc7946c 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -1,5 +1,5 @@ -import { Keyed } from "effect-quark" -import { useValue } from "effect-quark/solid" +import { Keyed } from "@opencode-ai/quark" +import { useValue } from "@opencode-ai/quark/solid" import { batch, createEffect, on, onCleanup, type Accessor } from "solid-js" import { useData } from "../../context/data" import { useClient } from "../../context/client" diff --git a/packages/tui/src/routes/session/timeline.ts b/packages/tui/src/routes/session/timeline.ts index 1b70203622..924db1e5b3 100644 --- a/packages/tui/src/routes/session/timeline.ts +++ b/packages/tui/src/routes/session/timeline.ts @@ -1,5 +1,5 @@ import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" -import { Keyed, Layout, Transaction } from "effect-quark" +import { Keyed, Layout, Transaction } from "@opencode-ai/quark" import { SessionContent } from "./content" const PartRefLayout = Layout.struct({