Explorar el Código

chore(quark): sync tests with standalone

Kit Langton hace 4 semanas
padre
commit
819522830b
Se han modificado 3 ficheros con 564 adiciones y 69 borrados
  1. 174 54
      packages/quark/test/keyed.test.ts
  2. 305 15
      packages/quark/test/layout.test.ts
  3. 85 0
      packages/quark/test/solid.test.ts

+ 174 - 54
packages/quark/test/keyed.test.ts

@@ -1,7 +1,8 @@
 import { describe, expect, it } from "bun:test"
-import { Computed, Keyed } from "../src"
+import { Keyed } from "../src/keyed"
+import { Computed } from "../src/reactivity"
 
-type Item = {
+interface Item {
   readonly id: number
   readonly label: string
 }
@@ -9,108 +10,238 @@ type Item = {
 const item = (id: number, label: string): Item => ({ id, label })
 
 describe("Keyed", () => {
-  it("keeps slots stable while publishing value and structural changes separately", () => {
+  it("keeps slot identity across value updates and reorders", () => {
+    const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
+    keyed.set([item(1, "one"), item(2, "two"), item(3, "three")])
+    const [one, two, three] = keyed.slots()
+
+    keyed.set([item(3, "THREE"), item(1, "ONE"), item(2, "TWO")])
+
+    expect(keyed.slots()).toEqual([three, one, two])
+    expect(keyed.slots()[0]).toBe(three)
+    expect(keyed.slots()[1]).toBe(one)
+    expect(keyed.slots()[2]).toBe(two)
+    expect(one()).toEqual(item(1, "ONE"))
+    expect(two()).toEqual(item(2, "TWO"))
+    expect(three()).toEqual(item(3, "THREE"))
+  })
+
+  it("does not publish structural changes for value-only updates or identical sets", () => {
     const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
     keyed.set([item(1, "one"), item(2, "two")])
-    const [one, two] = keyed.slots()
-    const structure = keyed.slots()
-    const structures: number[][] = []
-    const values: Item[][] = []
-    const disposeSlots = keyed.slots.subscribe((slots) => structures.push(slots.map((slot) => slot().id)))
-    const disposeValues = keyed.values.subscribe((next) => values.push([...next]))
+    const initial = keyed.slots()
+    const structures: Array<readonly number[]> = []
+    const dispose = keyed.slots.subscribe((slots) => structures.push(slots.map((slot) => slot().id)))
 
     keyed.set([item(1, "ONE"), item(2, "TWO")])
+    expect(keyed.slots()).toBe(initial)
 
-    expect(keyed.slots()).toBe(structure)
-    expect(keyed.slots()).toEqual([one, two])
-    expect(values).toEqual([[item(1, "ONE"), item(2, "TWO")]])
-    expect(structures).toEqual([])
+    keyed.set([item(1, "ONE"), item(2, "TWO")])
+    expect(keyed.slots()).toBe(initial)
 
     keyed.set([item(2, "TWO"), item(1, "ONE")])
 
-    expect(keyed.slots()).toEqual([two, one])
     expect(structures).toEqual([[2, 1]])
-    disposeSlots()
-    disposeValues()
+    dispose()
+  })
+
+  it("reacts to aggregate value updates while preserving unchanged aggregate snapshots", () => {
+    const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
+    const one = item(1, "one")
+    const two = item(2, "two")
+    keyed.set([one, two])
+    const initial = keyed.values()
+    const snapshots: Array<readonly Item[]> = []
+    const dispose = keyed.values.subscribe((values) => snapshots.push(values))
+
+    keyed.set([one, item(2, "TWO")])
+    const updated = keyed.values()
+    expect(updated).not.toBe(initial)
+    expect(updated).toEqual([one, item(2, "TWO")])
+
+    keyed.set([one, updated[1]])
+
+    expect(keyed.values()).toBe(updated)
+    expect(snapshots).toEqual([[one, item(2, "TWO")]])
+    dispose()
   })
 
   it("uses custom equivalence to cut off slot and aggregate updates", () => {
     const keyed = Keyed.make<Item, number>({
       key: (value) => value.id,
-      equivalent: (left, right) => left.label.toLowerCase() === right.label.toLowerCase(),
+      equivalent: (left, right) => left.id === right.id && left.label.toLowerCase() === right.label.toLowerCase(),
     })
     const original = item(1, "one")
     keyed.set([original])
     const slot = keyed.slots()[0]
     const aggregate = keyed.values()
+    const slotValues: Item[] = []
+    const aggregateValues: Array<readonly Item[]> = []
+    const disposeSlot = slot.subscribe((value) => slotValues.push(value))
+    const disposeAggregate = keyed.values.subscribe((values) => aggregateValues.push(values))
 
     keyed.set([item(1, "ONE")])
 
     expect(slot()).toBe(original)
     expect(keyed.values()).toBe(aggregate)
+    expect(slotValues).toEqual([])
+    expect(aggregateValues).toEqual([])
+
+    keyed.set([item(1, "changed")])
+
+    expect(slot()).toEqual(item(1, "changed"))
+    expect(slotValues).toEqual([item(1, "changed")])
+    expect(aggregateValues).toEqual([[item(1, "changed")]])
+    disposeSlot()
+    disposeAggregate()
+  })
+
+  it("uses SameValueZero semantics for primitive slot values", () => {
+    const keyed = Keyed.make<number, string>({ key: () => "value" })
+    keyed.set([-0])
+
+    expect(keyed.update(0)).toBe(false)
+    expect(Object.is(keyed.get("value")?.(), -0)).toBe(true)
   })
 
-  it("creates a fresh slot after removal and reinsertion", () => {
+  it("creates slots for inserts and fresh slots after remove and reinsert", () => {
     const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
-    keyed.set([item(1, "one"), item(2, "two")])
-    const [removed, retained] = keyed.slots()
+    keyed.set([item(1, "one")])
+    const removed = keyed.slots()[0]
+    const removedValues: Item[] = []
+    const dispose = removed.subscribe((value) => removedValues.push(value))
 
     keyed.set([item(2, "two")])
-    keyed.set([item(1, "new"), item(2, "TWO")])
-
-    expect(keyed.slots()[0]).not.toBe(removed)
-    expect(keyed.slots()[1]).toBe(retained)
-    expect(retained()).toEqual(item(2, "TWO"))
+    const inserted = keyed.slots()[0]
+    keyed.set([item(1, "new one"), item(2, "new two")])
+    const [reinserted, retained] = keyed.slots()
+
+    expect(reinserted).not.toBe(removed)
+    expect(reinserted()).toEqual(item(1, "new one"))
+    expect(retained).toBe(inserted)
+    expect(retained()).toEqual(item(2, "new two"))
+    expect(removed()).toEqual(item(1, "one"))
+    expect(removedValues).toEqual([])
+    dispose()
   })
 
-  it("rejects duplicate keys without partially updating", () => {
+  it("rejects duplicate keys before making any partial update", () => {
     const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
     keyed.set([item(1, "one"), item(2, "two")])
     const slots = keyed.slots()
     const values = keyed.values()
+    const notifications: string[] = []
+    const disposeOne = slots[0].subscribe(() => notifications.push("one"))
+    const disposeTwo = slots[1].subscribe(() => notifications.push("two"))
+    const disposeSlots = keyed.slots.subscribe(() => notifications.push("slots"))
+    const disposeValues = keyed.values.subscribe(() => notifications.push("values"))
 
     expect(() => keyed.set([item(1, "changed"), item(1, "duplicate")])).toThrow("Keyed values must have unique keys")
+
     expect(keyed.slots()).toBe(slots)
     expect(keyed.values()).toBe(values)
+    expect(keyed.values()).toEqual([item(1, "one"), item(2, "two")])
+    expect(notifications).toEqual([])
+    disposeOne()
+    disposeTwo()
+    disposeSlots()
+    disposeValues()
   })
 
-  it("uses SameValueZero key equality", () => {
+  it("uses SameValueZero equality for zero keys", () => {
     const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
-    keyed.set([item(-0, "zero"), item(Number.NaN, "nan")])
-    const [zero, nan] = keyed.slots()
+    keyed.set([item(-0, "negative zero")])
+    const zero = keyed.slots()[0]
 
-    keyed.set([item(0, "ZERO"), item(Number.NaN, "NAN")])
+    keyed.set([item(0, "zero")])
 
-    expect(keyed.slots()).toEqual([zero, nan])
-    expect(() => keyed.set([item(0, "zero"), item(-0, "duplicate")])).toThrow("Keyed values must have unique keys")
-    expect(() => keyed.set([item(Number.NaN, "nan"), item(Number.NaN, "duplicate")])).toThrow(
+    expect(keyed.slots()[0]).toBe(zero)
+    expect(zero()).toEqual(item(0, "zero"))
+    expect(() => keyed.set([item(0, "zero"), item(-0, "negative zero")])).toThrow("Keyed values must have unique keys")
+    expect(keyed.slots()).toEqual([zero])
+    expect(keyed.values()).toEqual([item(0, "zero")])
+  })
+
+  it("uses SameValueZero equality for NaN keys", () => {
+    const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
+    keyed.set([item(Number.NaN, "first")])
+    const nan = keyed.slots()[0]
+
+    keyed.set([item(Number.NaN, "updated")])
+
+    expect(keyed.slots()[0]).toBe(nan)
+    expect(nan()).toEqual(item(Number.NaN, "updated"))
+    expect(() => keyed.set([item(Number.NaN, "one"), item(Number.NaN, "two")])).toThrow(
       "Keyed values must have unique keys",
     )
+    expect(keyed.slots()).toEqual([nan])
+    expect(keyed.values()).toEqual([item(Number.NaN, "updated")])
   })
 
-  it("publishes one settled aggregate when values and structure change together", () => {
+  it("publishes one glitch-free aggregate when slots and structure change together", () => {
     const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
-    keyed.set([item(1, "one"), item(2, "two")])
+    keyed.set([item(1, "one"), item(2, "two"), item(3, "three")])
     const observations: string[] = []
     const summary = Computed.make(() => {
-      const slots = keyed
+      const slotKeys = keyed
         .slots()
-        .map((slot) => `${slot().id}:${slot().label}`)
+        .map((slot) => slot().id)
         .join(",")
       const values = keyed
         .values()
         .map((value) => `${value.id}:${value.label}`)
         .join(",")
-      return `${slots}|${values}`
+      return `${slotKeys}|${values}`
     })
     const dispose = summary.subscribe((value) => observations.push(value))
 
-    keyed.set([item(2, "TWO"), item(3, "three")])
+    keyed.set([item(3, "THREE"), item(2, "TWO"), item(4, "four")])
 
-    expect(observations).toEqual(["2:TWO,3:three|2:TWO,3:three"])
+    expect(observations).toEqual(["3,2,4|3:THREE,2:TWO,4:four"])
     dispose()
   })
 
+  it("stops slot, structural, and aggregate notifications after disposal", () => {
+    const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
+    keyed.set([item(1, "one")])
+    const notifications: string[] = []
+    const disposeSlot = keyed.slots()[0].subscribe(() => notifications.push("slot"))
+    const disposeSlots = keyed.slots.subscribe(() => notifications.push("slots"))
+    const disposeValues = keyed.values.subscribe(() => notifications.push("values"))
+
+    disposeSlot()
+    disposeSlots()
+    disposeValues()
+    keyed.set([item(2, "two")])
+    keyed.set([item(2, "TWO")])
+
+    expect(notifications).toEqual([])
+  })
+
+  it("supports empty sets and repeated clears without redundant publication", () => {
+    const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
+    expect(keyed.slots()).toEqual([])
+    expect(keyed.values()).toEqual([])
+    const structures: Array<readonly unknown[]> = []
+    const aggregates: Array<readonly Item[]> = []
+    const disposeSlots = keyed.slots.subscribe((slots) => structures.push(slots))
+    const disposeValues = keyed.values.subscribe((values) => aggregates.push(values))
+
+    keyed.set([])
+    keyed.set([item(1, "one")])
+    keyed.set([])
+    const clearedSlots = keyed.slots()
+    const clearedValues = keyed.values()
+    keyed.set([])
+
+    expect(keyed.slots()).toBe(clearedSlots)
+    expect(keyed.values()).toBe(clearedValues)
+    expect(structures.map((slots) => slots.length)).toEqual([1, 0])
+    expect(aggregates).toEqual([[item(1, "one")], []])
+    disposeSlots()
+    disposeValues()
+  })
+
   it("updates one existing slot without publishing structure", () => {
     const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
     keyed.set([item(1, "one"), item(2, "two")])
@@ -182,6 +313,9 @@ describe("Keyed", () => {
     expect(keyed.remove(2)).toBe(true)
     expect(keyed.remove(2)).toBe(false)
     expect(keyed.slots()).toEqual([one, four, three])
+    expect(() => keyed.insert(item(1, "duplicate"))).toThrow("Keyed value already exists: 1")
+    expect(() => keyed.insert(item(2, "two"), { before: 5 })).toThrow("Keyed value does not exist: 5")
+    expect(keyed.move(5)).toBe(false)
   })
 
   it("counts publications and equivalence suppressions when instrumented", () => {
@@ -202,18 +336,4 @@ describe("Keyed", () => {
       equivalenceSuppressions: 1,
     })
   })
-
-  it("publishes nothing for a full unchanged rebuild", () => {
-    const metrics = Keyed.metrics()
-    const keyed = Keyed.make<Item, number>({ key: (value) => value.id, metrics })
-    const values = [item(1, "one"), item(2, "two")]
-    keyed.set(values)
-    const before = { ...metrics }
-
-    keyed.set(values)
-
-    expect(metrics.slotPublications).toBe(before.slotPublications)
-    expect(metrics.structuralPublications).toBe(before.structuralPublications)
-    expect(metrics.equivalenceSuppressions).toBe(before.equivalenceSuppressions + values.length)
-  })
 })

+ 305 - 15
packages/quark/test/layout.test.ts

@@ -13,8 +13,8 @@ const Job = Layout.struct({
 })
 
 const Jobs = Layout.collection(Job, ({ members, first }) => ({
-  labels: members((job) => job.labels),
-  nextRetry: first((job) => job.status === "retrying"),
+  labels: members(["labels"], (job) => job.labels),
+  nextRetry: first(["status"], (job) => job.status === "retrying"),
 }))
 
 describe("Layout", () => {
@@ -37,12 +37,17 @@ describe("Layout", () => {
       "Keyed layout must declare exactly one key field",
     )
     expect(() =>
-      Layout.compile(Layout.struct({ left: Layout.key(Layout.number), right: Layout.key(Layout.number) })),
+      Layout.compile(
+        Layout.struct({
+          left: Layout.key(Layout.number),
+          right: Layout.key(Layout.number),
+        }),
+      ),
     ).toThrow("Keyed layout must declare exactly one key field")
   })
 
   it("generates the same trusted equivalence as the closure backend", () => {
-    const closure = Layout.compile(Item)
+    const closure = Layout.compile(Item, { backend: "closure" })
     const generated = Layout.compile(Item, { backend: "generated" })
     const values = [
       { id: 1, label: "one" },
@@ -53,12 +58,70 @@ describe("Layout", () => {
     values.forEach((left) => {
       values.forEach((right) => {
         expect(generated.equivalent(left, right)).toBe(closure.equivalent(left, right))
+        expect(generated.diff(left, right) === 0).toBe(generated.equivalent(left, right))
+        expect(closure.diff(left, right) === 0).toBe(closure.equivalent(left, right))
       })
     })
   })
 
+  it("honors custom primitive comparators in generated plans", () => {
+    const CaseInsensitive = {
+      ...Layout.string,
+      foldCase: true,
+      equivalent(left: string, right: string) {
+        return this.foldCase ? left.toLowerCase() === right.toLowerCase() : left === right
+      },
+    }
+    const Value = Layout.struct({ id: Layout.key(Layout.number), value: CaseInsensitive })
+    const closure = Layout.compile(Value, { backend: "closure" })
+    const generated = Layout.compile(Value, { backend: "generated" })
+    const left = { id: 1, value: "same" }
+    const right = { id: 1, value: "SAME" }
+
+    expect(closure.equivalent(left, right)).toBe(true)
+    expect(generated.equivalent(left, right)).toBe(true)
+    expect(generated.diff(left, right)).toBe(0)
+  })
+
+  it("keeps generated nested keyed unions independent when they share variants", () => {
+    const variants = {
+      yes: Layout.struct({ value: Layout.string }),
+      no: Layout.struct({ value: Layout.string }),
+    }
+    const Left = Layout.keyedUnion({
+      key: Layout.key("leftID", Layout.number),
+      tag: "leftType",
+      variants,
+    })
+    const Right = Layout.keyedUnion({
+      key: Layout.key("rightID", Layout.number),
+      tag: "rightType",
+      variants,
+    })
+    const Root = Layout.struct({
+      id: Layout.key(Layout.number),
+      left: Left,
+      right: Right,
+    })
+    const value: Layout.Type<typeof Root> = {
+      id: 1,
+      left: { leftID: 1, leftType: "yes", value: "same" },
+      right: { rightID: 1, rightType: "yes", value: "same" },
+    }
+    const copy = structuredClone(value)
+    const closure = Layout.compile(Root, { backend: "closure" })
+    const generated = Layout.compile(Root, { backend: "generated" })
+
+    expect(closure.equivalent(value, copy)).toBe(true)
+    expect(generated.equivalent(value, copy)).toBe(true)
+    expect(generated.diff(value, copy)).toBe(0)
+  })
+
   it("compiles nested discriminated unions and skips immutable fields", () => {
-    const Ref = Layout.struct({ messageID: Layout.string, partID: Layout.string })
+    const Ref = Layout.struct({
+      messageID: Layout.string,
+      partID: Layout.string,
+    })
     const Row = Layout.keyedUnion({
       key: Layout.key("id", Layout.string),
       tag: "type",
@@ -82,7 +145,7 @@ describe("Layout", () => {
         }),
       },
     })
-    const plan = Layout.compile(Row)
+    const plan = Layout.compile(Row, { backend: "closure" })
     const generated = Layout.compile(Row, { backend: "generated" })
     const group = {
       id: "group-1",
@@ -94,9 +157,19 @@ describe("Layout", () => {
       completed: false,
     }
 
-    expect(plan.equivalent(group, { ...group, origin: { messageID: "ignored", partID: "ignored" } })).toBe(true)
+    expect(
+      plan.equivalent(group, {
+        ...group,
+        origin: { messageID: "ignored", partID: "ignored" },
+      }),
+    ).toBe(true)
     expect(plan.equivalent(group, { ...group, completed: true })).toBe(false)
-    expect(plan.equivalent(group, { ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] })).toBe(false)
+    expect(
+      plan.equivalent(group, {
+        ...group,
+        pending: [{ messageID: "assistant-1", partID: "read-1" }],
+      }),
+    ).toBe(false)
     expect(
       plan.equivalent(group, {
         id: "group-1",
@@ -127,8 +200,14 @@ describe("Layout", () => {
   })
 
   it("includes nested keys in structural equivalence", () => {
-    const Child = Layout.struct({ id: Layout.key(Layout.number), value: Layout.string })
-    const Parent = Layout.struct({ id: Layout.key(Layout.number), child: Child })
+    const Child = Layout.struct({
+      id: Layout.key(Layout.number),
+      value: Layout.string,
+    })
+    const Parent = Layout.struct({
+      id: Layout.key(Layout.number),
+      child: Child,
+    })
     const parent = Layout.compile(Parent)
 
     expect(
@@ -184,6 +263,158 @@ describe("Layout", () => {
     expect(jobs.first("nextRetry")?.().id).toBe("four")
   })
 
+  it("skips index projection when the change is disjoint from its declared fields", () => {
+    let extractions = 0
+    let matchChecks = 0
+    const IndexedJobs = Layout.collection(Job, ({ members, first }) => ({
+      labels: members(["labels"], (job) => {
+        extractions++
+        return job.labels
+      }),
+      nextRetry: first(["status"], (job) => {
+        matchChecks++
+        return job.status === "retrying"
+      }),
+    }))
+    const jobs = IndexedJobs.make([{ id: "one", labels: ["billing"], status: "running" }])
+    const labels = jobs.get("one")!().labels
+    const baseline = { extractions, matchChecks }
+
+    // Status-only change: labels extraction reads only `labels`, so it skips.
+    jobs.update({ id: "one", labels, status: "retrying" })
+    expect(extractions).toBe(baseline.extractions)
+    expect(matchChecks).toBe(baseline.matchChecks + 1)
+    expect(jobs.first("nextRetry")?.().id).toBe("one")
+
+    // Labels-only change: the first-match check reads only `status`, so it skips.
+    jobs.update({ id: "one", labels: ["urgent"], status: "retrying" })
+    expect(extractions).toBe(baseline.extractions + 1)
+    expect(matchChecks).toBe(baseline.matchChecks + 1)
+    expect(jobs.hasMember("labels", "urgent")).toBe(true)
+    expect(jobs.hasMember("labels", "billing")).toBe(false)
+
+    // Equivalent update: nothing runs.
+    jobs.update({ id: "one", labels: ["urgent"], status: "retrying" })
+    expect(extractions).toBe(baseline.extractions + 1)
+    expect(matchChecks).toBe(baseline.matchChecks + 1)
+  })
+
+  it("normalizes numeric dependency names", () => {
+    let extractions = 0
+    const Numeric = Layout.collection(
+      Layout.struct({ id: Layout.key(Layout.string), 0: Layout.string, other: Layout.string }),
+      ({ members }) => ({
+        zero: members([0], (value) => {
+          extractions++
+          return [value[0]]
+        }),
+      }),
+    )
+    const values = Numeric.make([{ id: "one", 0: "zero", other: "before" }])
+    const baseline = extractions
+
+    values.update({ id: "one", 0: "zero", other: "after" })
+
+    expect(extractions).toBe(baseline)
+    expect(values.hasMember("zero", "zero")).toBe(true)
+  })
+
+  it("reuses precomputed field diffs during direct collection mutations", () => {
+    let comparisons = 0
+    const counted: Layout.Field<string> = {
+      equivalent(left, right) {
+        comparisons++
+        return left === right
+      },
+    }
+    const CountedJob = Layout.struct({
+      id: Layout.key(Layout.string),
+      value: counted,
+      status: Layout.string,
+    })
+    const CountedJobs = Layout.collection(CountedJob, ({ first }) => ({
+      changed: first(["value"], (job) => job.value === "after"),
+    }))
+    const one = { id: "one", value: "before", status: "idle" }
+    const two = { id: "two", value: "before", status: "idle" }
+    const jobs = CountedJobs.make([one, two])
+    comparisons = 0
+
+    jobs.set([
+      { ...one },
+      { ...two, value: "after" },
+    ])
+
+    expect(comparisons).toBe(4)
+    expect(jobs.get("one")?.()).toBe(one)
+    expect(jobs.first("changed")?.().id).toBe("two")
+
+    comparisons = 0
+    jobs.update({ ...jobs.get("one")!(), status: "busy" })
+    expect(comparisons).toBe(1)
+
+    comparisons = 0
+    jobs.modify("two", (job) => ({ ...job, value: "done" }))
+    expect(comparisons).toBe(1)
+    expect(jobs.first("changed")).toBeUndefined()
+  })
+
+  it("refreshes indexes that read a changed union discriminant", () => {
+    const Row = Layout.keyedUnion({
+      key: Layout.key("id", Layout.number),
+      tag: "type",
+      variants: {
+        waiting: Layout.struct({ value: Layout.string }),
+        ready: Layout.struct({ value: Layout.string }),
+      },
+    })
+    const Rows = Layout.collection(Row, ({ members, first }) => ({
+      types: members(["type"], (row) => [row.type]),
+      firstReady: first(["type"], (row) => row.type === "ready"),
+    }))
+    const rows = Rows.make([{ id: 1, type: "waiting", value: "same" }])
+
+    rows.update({ id: 1, type: "ready", value: "same" })
+
+    expect(rows.get(1)?.().type).toBe("ready")
+    expect(rows.hasMember("types", "waiting")).toBe(false)
+    expect(rows.hasMember("types", "ready")).toBe(true)
+    expect(rows.first("firstReady")?.().type).toBe("ready")
+  })
+
+  it("tracks lazy member iterables while they are consumed", () => {
+    const LazyJobs = Layout.collection(Job, ({ members }) => ({
+      statuses: members(function* (job) {
+        yield job.status
+      }),
+    }))
+    const jobs = LazyJobs.make([{ id: "one", labels: [], status: "running" }])
+
+    expect(jobs.hasMember("statuses", "running")).toBe(true)
+    jobs.update({ id: "one", labels: [], status: "retrying" })
+    expect(jobs.hasMember("statuses", "running")).toBe(false)
+    expect(jobs.hasMember("statuses", "retrying")).toBe(true)
+  })
+
+  it("passes actual frozen values to identity and reflection projections", () => {
+    const actual = Object.freeze({ id: "one", labels: [] as readonly string[], status: "running" })
+    const accepted = new WeakSet<object>([actual])
+    const EnumeratedJobs = Layout.collection(Job, ({ members, first }) => ({
+      properties: members((job) => Object.keys(job)),
+      selves: members((job) => [job]),
+      frozen: members((job) => [Object.isFrozen(job)]),
+      accepted: first((job) => accepted.has(job)),
+    }))
+    const jobs = EnumeratedJobs.make([actual])
+
+    expect(jobs.hasMember("properties", "id")).toBe(true)
+    expect(jobs.hasMember("properties", "labels")).toBe(true)
+    expect(jobs.hasMember("properties", "status")).toBe(true)
+    expect(jobs.hasMember("selves", actual)).toBe(true)
+    expect(jobs.hasMember("frozen", true)).toBe(true)
+    expect(jobs.first("accepted")?.()).toBe(actual)
+  })
+
   it("applies explicit member deltas without re-extracting unchanged membership", () => {
     let extractions = 0
     const IndexedJobs = Layout.collection(Job, ({ members }) => ({
@@ -206,12 +437,20 @@ describe("Layout", () => {
     expect(jobs.hasMember("labels", "billing")).toBe(true)
     expect(jobs.hasMember("labels", "urgent")).toBe(true)
 
-    jobs.modify("two", (job) => ({ ...job, labels: [] }), { members: { labels: { remove: ["billing"] } } })
+    jobs.modify("two", (job) => ({ ...job, labels: [] }), {
+      members: { labels: { remove: ["billing"] } },
+    })
     expect(jobs.hasMember("labels", "billing")).toBe(false)
 
     if (false) {
-      // @ts-expect-error Member deltas require arrays so string members are not split into characters.
-      jobs.modify("one", (job) => job, { members: { labels: { add: "urgent" } } })
+      jobs.modify("one", (job) => job, {
+        members: {
+          labels: {
+            // @ts-expect-error Member deltas require arrays so strings are not split into characters.
+            add: "urgent",
+          },
+        },
+      })
     }
   })
 
@@ -238,16 +477,67 @@ describe("Layout", () => {
     )
   })
 
+  it("restores pending comparison state when publication throws", () => {
+    const LabeledJob = Layout.struct({
+      id: Layout.key(Layout.string),
+      label: Layout.string,
+      status: Layout.string,
+    })
+    const IndexedJobs = Layout.collection(LabeledJob, ({ first }) => ({
+      retry: first(["status"], (job) => job.status === "retrying"),
+    }))
+    const jobs = IndexedJobs.make([{ id: "one", label: "before", status: "running" }])
+    const slot = jobs.get("one")!
+    const dispose = slot.subscribe(() => {
+      throw new Error("listener failed")
+    })
+
+    expect(() => jobs.update({ id: "one", label: "after", status: "running" })).toThrow("listener failed")
+    dispose()
+    const equivalent = { ...slot() }
+
+    expect(jobs.set([equivalent])).toBe(false)
+    expect(slot()).not.toBe(equivalent)
+  })
+
+  it("does not leak a pending comparison into reentrant publication", () => {
+    const LabeledJob = Layout.struct({
+      id: Layout.key(Layout.string),
+      label: Layout.string,
+      status: Layout.string,
+    })
+    const IndexedJobs = Layout.collection(LabeledJob, ({ first }) => ({
+      retry: first(["status"], (job) => job.status === "retrying"),
+    }))
+    const jobs = IndexedJobs.make([{ id: "one", label: "before", status: "running" }])
+    const slot = jobs.get("one")!
+    let reentered = false
+    let changed: boolean | undefined
+    const dispose = slot.subscribe(() => {
+      if (reentered) return
+      reentered = true
+      changed = jobs.set([{ ...slot() }])
+    })
+
+    jobs.update({ id: "one", label: "after", status: "running" })
+
+    expect(changed).toBe(false)
+    dispose()
+  })
+
   it("tracks first matches whose key is undefined", () => {
     const undefinedField: Layout.Field<undefined> = { equivalent: Object.is }
     const OptionalJobs = Layout.collection(
       Layout.struct({ id: Layout.key(undefinedField), status: Layout.string }),
-      ({ first }) => ({ retry: first((job) => job.status === "retrying") }),
+      ({ first }) => ({ retry: first(["status"], (job) => job.status === "retrying") }),
     )
     const jobs = OptionalJobs.make([{ id: undefined, status: "running" }])
 
     jobs.update({ id: undefined, status: "retrying" })
 
-    expect(jobs.first("retry")?.()).toEqual({ id: undefined, status: "retrying" })
+    expect(jobs.first("retry")?.()).toEqual({
+      id: undefined,
+      status: "retrying",
+    })
   })
 })

+ 85 - 0
packages/quark/test/solid.test.ts

@@ -0,0 +1,85 @@
+import { describe, expect, it } from "bun:test"
+import { createComputed, createRoot, createSignal } from "solid-js"
+import { Computed, Keyed, State, Transaction, type Readable } from "../src"
+import { useSlot, useValue } from "../src/solid"
+
+describe("Solid adapter", () => {
+  it("bridges Quark values into Solid ownership", () => {
+    const left = State.make(1)
+    const right = State.make(2)
+    const total = Computed.make(() => left() + right())
+    const values: number[] = []
+
+    createRoot((dispose) => {
+      const value = useValue(total)
+      createComputed(() => values.push(value()))
+
+      Transaction.run(() => {
+        left.set(2)
+        right.set(3)
+      })
+      dispose()
+    })
+
+    left.set(4)
+    expect(values).toEqual([3, 5])
+  })
+
+  it("preserves function values", () => {
+    const first = () => 1
+    const second = () => 2
+    const state = State.make(first)
+    let value = first
+
+    createRoot((dispose) => {
+      const current = useValue(state)
+      createComputed(() => value = current())
+      state.set(second)
+      dispose()
+    })
+
+    expect(value).toBe(second)
+  })
+
+  it("switches keyed slots and releases obsolete subscriptions", () => {
+    const keyed = Keyed.make<{ readonly id: number; readonly value: string }, number>({ key: (value) => value.id })
+    keyed.set([
+      { id: 1, value: "one" },
+      { id: 2, value: "two" },
+    ])
+    const active = [0, 0]
+    track(keyed.get(1)!, 0)
+    track(keyed.get(2)!, 1)
+    const values: Array<string | undefined> = []
+
+    createRoot((dispose) => {
+      const [key, setKey] = createSignal(1)
+      const value = useSlot(keyed, key)
+      createComputed(() => values.push(value()?.value))
+
+      keyed.modify(1, (item) => ({ ...item, value: "ONE" }))
+      keyed.insert({ id: 3, value: "three" })
+      setKey(2)
+      keyed.modify(1, (item) => ({ ...item, value: "ignored" }))
+      keyed.modify(2, (item) => ({ ...item, value: "TWO" }))
+
+      expect(active).toEqual([0, 1])
+      dispose()
+    })
+
+    expect(active).toEqual([0, 0])
+    expect(values).toEqual(["one", "ONE", "two", "TWO"])
+
+    function track(slot: Readable<{ readonly id: number; readonly value: string }>, index: number) {
+      const subscribe = slot.subscribe
+      slot.subscribe = (listener) => {
+        active[index]++
+        const dispose = subscribe(listener)
+        return () => {
+          active[index]--
+          dispose()
+        }
+      }
+    }
+  })
+})