mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 18:26:09 +00:00
chore(quark): sync tests with standalone
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user