chore(quark): sync tests with standalone

This commit is contained in:
Kit Langton
2026-07-18 22:54:23 -04:00
parent 2a8ad383ea
commit f36acdc6b9
3 changed files with 563 additions and 68 deletions
+173 -53
View File
@@ -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("creates a fresh slot after removal and reinsertion", () => {
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 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")])
const inserted = keyed.slots()[0]
keyed.set([item(1, "new one"), item(2, "new two")])
const [reinserted, retained] = keyed.slots()
expect(keyed.slots()[0]).not.toBe(removed)
expect(keyed.slots()[1]).toBe(retained)
expect(retained()).toEqual(item(2, "TWO"))
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
View File
@@ -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
View File
@@ -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()
}
}
}
})
})