refactor(tui): extract session timeline state
This commit is contained in:
parent
daf8e539bf
commit
a653744d78
15 changed files with 1897 additions and 586 deletions
|
|
@ -1,2 +1,3 @@
|
|||
export { Keyed } from "./keyed"
|
||||
export { Layout } from "./layout"
|
||||
export { Computed, State, Transaction, type Readable, type Writable } from "./reactivity"
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ export namespace Keyed {
|
|||
readonly values: Readable<readonly A[]>
|
||||
has(key: Key): boolean
|
||||
get(key: Key): Readable<A> | undefined
|
||||
set(values: readonly A[]): void
|
||||
set(values: readonly A[]): boolean
|
||||
update(value: A): boolean
|
||||
modify(key: Key, f: (value: A) => A): boolean
|
||||
insert(value: A, position?: Position<Key>): Readable<A>
|
||||
remove(key: Key): boolean
|
||||
move(key: Key, position?: Position<Key>): boolean
|
||||
before(key: Key): Readable<A> | undefined
|
||||
after(key: Key): Readable<A> | undefined
|
||||
}
|
||||
|
||||
export function make<A, Key>(options: {
|
||||
|
|
@ -44,7 +47,8 @@ export namespace Keyed {
|
|||
const retained = new Set(keys)
|
||||
if (retained.size !== keys.length) throw new Error("Keyed values must have unique keys")
|
||||
|
||||
Transaction.run(() => {
|
||||
return Transaction.run(() => {
|
||||
let changed = false
|
||||
const previous = slots()
|
||||
const reconciled = next.map((value, index) => {
|
||||
const key = keys[index]
|
||||
|
|
@ -54,12 +58,7 @@ export namespace Keyed {
|
|||
byKey.set(key, created)
|
||||
return created
|
||||
}
|
||||
if (!equivalent(slot(), value)) {
|
||||
slot.set(value)
|
||||
if (options.metrics) options.metrics.slotPublications++
|
||||
} else if (options.metrics) {
|
||||
options.metrics.equivalenceSuppressions++
|
||||
}
|
||||
if (publish(slot, value)) changed = true
|
||||
return slot
|
||||
})
|
||||
byKey.forEach((_slot, key) => {
|
||||
|
|
@ -68,20 +67,23 @@ export namespace Keyed {
|
|||
if (!same(previous, reconciled)) {
|
||||
slots.set(reconciled)
|
||||
if (options.metrics) options.metrics.structuralPublications++
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
})
|
||||
},
|
||||
update(value) {
|
||||
const key = options.key(value)
|
||||
const slot = byKey.get(key)
|
||||
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
if (equivalent(slot(), value)) {
|
||||
if (options.metrics) options.metrics.equivalenceSuppressions++
|
||||
return false
|
||||
}
|
||||
slot.set(value)
|
||||
if (options.metrics) options.metrics.slotPublications++
|
||||
return true
|
||||
return publish(slot, value)
|
||||
},
|
||||
modify(key, f) {
|
||||
const slot = byKey.get(key)
|
||||
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
const value = f(slot())
|
||||
if (byKey.get(options.key(value)) !== slot) throw new Error("Keyed modify must preserve the value key")
|
||||
return publish(slot, value)
|
||||
},
|
||||
insert(value, position) {
|
||||
const key = options.key(value)
|
||||
|
|
@ -118,6 +120,22 @@ export namespace Keyed {
|
|||
if (options.metrics) options.metrics.structuralPublications++
|
||||
return true
|
||||
},
|
||||
before(key) {
|
||||
return neighbor(key, -1)
|
||||
},
|
||||
after(key) {
|
||||
return neighbor(key, 1)
|
||||
},
|
||||
}
|
||||
|
||||
function publish(slot: Writable<A>, value: A) {
|
||||
if (equivalent(slot(), value)) {
|
||||
if (options.metrics) options.metrics.equivalenceSuppressions++
|
||||
return false
|
||||
}
|
||||
slot.set(value)
|
||||
if (options.metrics) options.metrics.slotPublications++
|
||||
return true
|
||||
}
|
||||
|
||||
function positionIndex(current: readonly Writable<A>[], position?: Position<Key>) {
|
||||
|
|
@ -131,6 +149,13 @@ export namespace Keyed {
|
|||
if (!target) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
return current.indexOf(target)
|
||||
}
|
||||
|
||||
function neighbor(key: Key, offset: -1 | 1) {
|
||||
const slot = byKey.get(key)
|
||||
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
const current = slots()
|
||||
return current[current.indexOf(slot) + offset]
|
||||
}
|
||||
}
|
||||
|
||||
export function metrics(): Metrics {
|
||||
|
|
|
|||
660
packages/quark/src/layout.ts
Normal file
660
packages/quark/src/layout.ts
Normal file
|
|
@ -0,0 +1,660 @@
|
|||
import { Keyed } from "./keyed"
|
||||
import { Transaction, type Readable } from "./reactivity"
|
||||
|
||||
export namespace Layout {
|
||||
export interface Field<A> {
|
||||
readonly isKey?: true
|
||||
readonly primitive?: true
|
||||
readonly immutable?: true
|
||||
equivalent(left: A, right: A): boolean
|
||||
}
|
||||
|
||||
export interface KeyField<A> extends Field<A> {
|
||||
readonly isKey: true
|
||||
}
|
||||
|
||||
export interface NamedKey<Name extends PropertyKey, A> {
|
||||
readonly name: Name
|
||||
readonly field: Field<A>
|
||||
}
|
||||
|
||||
export type Type<Field> = Field extends Layout.Field<infer A> ? A : never
|
||||
|
||||
type Fields = Readonly<Record<PropertyKey, Field<unknown>>>
|
||||
type Value<StructFields> = { readonly [Key in keyof StructFields]: Type<StructFields[Key]> }
|
||||
type KeyName<StructFields> = {
|
||||
readonly [Key in keyof StructFields]: StructFields[Key] extends KeyField<unknown> ? Key : never
|
||||
}[keyof StructFields]
|
||||
type Variant<Tag extends PropertyKey, Variants> = {
|
||||
readonly [Name in keyof Variants & string]: { readonly [Key in Tag]: Name } & Type<Variants[Name]>
|
||||
}[keyof Variants & string]
|
||||
type KeyedVariant<Name extends PropertyKey, A, Tag extends PropertyKey, Variants> = {
|
||||
readonly [Key in Name]: A
|
||||
} & Variant<Tag, Variants>
|
||||
|
||||
export interface Struct<StructFields extends Fields> extends Field<Value<StructFields>> {
|
||||
readonly type: "struct"
|
||||
readonly fields: StructFields
|
||||
}
|
||||
|
||||
export interface Union<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>
|
||||
extends Field<Variant<Tag, Variants>> {
|
||||
readonly type: "union"
|
||||
readonly tag: Tag
|
||||
readonly variants: Variants
|
||||
}
|
||||
|
||||
export interface KeyedUnion<
|
||||
Name extends PropertyKey,
|
||||
A,
|
||||
Tag extends PropertyKey,
|
||||
Variants extends Readonly<Record<string, Field<unknown>>>,
|
||||
> extends Field<KeyedVariant<Name, A, Tag, Variants>> {
|
||||
readonly type: "keyed-union"
|
||||
readonly key: NamedKey<Name, A>
|
||||
readonly tag: Tag
|
||||
readonly variants: Variants
|
||||
}
|
||||
|
||||
export interface Plan<A, Key> {
|
||||
readonly key: PropertyKey
|
||||
readonly fields?: Fields
|
||||
readonly equivalent: (left: A, right: A) => boolean
|
||||
readonly keyOf: (value: A) => Key
|
||||
make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Keyed.Keyed<A, Key>
|
||||
}
|
||||
|
||||
export interface MembersIndex<A, Member> {
|
||||
readonly type: "members"
|
||||
readonly extract: (value: A) => Iterable<Member>
|
||||
}
|
||||
|
||||
export interface FirstIndex<A> {
|
||||
readonly type: "first"
|
||||
readonly matches: (value: A) => boolean
|
||||
}
|
||||
|
||||
export type Index<A> = MembersIndex<A, unknown> | FirstIndex<A>
|
||||
type Indexes<A> = Readonly<Record<PropertyKey, Index<A>>>
|
||||
type MembersNames<Definitions> = {
|
||||
readonly [Name in keyof Definitions]: Definitions[Name] extends { readonly type: "members" } ? Name : never
|
||||
}[keyof Definitions]
|
||||
type FirstNames<Definitions> = {
|
||||
readonly [Name in keyof Definitions]: Definitions[Name] extends { readonly type: "first" } ? Name : never
|
||||
}[keyof Definitions]
|
||||
type Member<Definition> = Definition extends { readonly extract: (value: never) => Iterable<infer A> } ? A : never
|
||||
type MemberChanges<Definitions> = {
|
||||
readonly [Name in MembersNames<Definitions>]?: {
|
||||
readonly add?: readonly Member<Definitions[Name]>[]
|
||||
readonly remove?: readonly Member<Definitions[Name]>[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface IndexBuilder<A> {
|
||||
members<Member>(extract: (value: A) => Iterable<Member>): MembersIndex<A, Member>
|
||||
first(matches: (value: A) => boolean): FirstIndex<A>
|
||||
}
|
||||
|
||||
export interface Collection<A, Key, Definitions extends Indexes<A>> extends Keyed.Keyed<A, Key> {
|
||||
modify(key: Key, f: (value: A) => A, changes?: { readonly members?: MemberChanges<Definitions> }): boolean
|
||||
hasMember<Name extends MembersNames<Definitions>>(name: Name, member: Member<Definitions[Name]>): boolean
|
||||
first<Name extends FirstNames<Definitions>>(name: Name): Readable<A> | undefined
|
||||
}
|
||||
|
||||
export interface CollectionPlan<A, Key, Definitions extends Indexes<A>> extends Plan<A, Key> {
|
||||
make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Collection<A, Key, Definitions>
|
||||
}
|
||||
|
||||
export const string: Field<string> = primitive()
|
||||
export const number: Field<number> = primitive()
|
||||
export const boolean: Field<boolean> = primitive()
|
||||
|
||||
export function array<A>(item: Field<A>): Field<readonly A[]> {
|
||||
return make((left, right) => {
|
||||
if (left.length !== right.length) return false
|
||||
for (let index = 0; index < left.length; index++) {
|
||||
if (!item.equivalent(left[index], right[index])) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function immutable<A>(field: Field<A>): Field<A> {
|
||||
return { ...field, immutable: true, equivalent: () => true }
|
||||
}
|
||||
|
||||
export function key<A>(field: Field<A>): KeyField<A>
|
||||
export function key<const Name extends PropertyKey, A>(name: Name, field: Field<A>): NamedKey<Name, A>
|
||||
export function key<A>(name: Field<A> | PropertyKey, field?: Field<A>): KeyField<A> | NamedKey<PropertyKey, A> {
|
||||
if (field) return { name: name as PropertyKey, field }
|
||||
return { ...(name as Field<A>), isKey: true }
|
||||
}
|
||||
|
||||
export function struct<const StructFields extends Fields>(fields: StructFields): Struct<StructFields> {
|
||||
return {
|
||||
type: "struct",
|
||||
fields,
|
||||
equivalent: compileFields<Value<StructFields>>(fields),
|
||||
}
|
||||
}
|
||||
|
||||
export function union<
|
||||
const Tag extends PropertyKey,
|
||||
const Variants extends Readonly<Record<string, Field<unknown>>>,
|
||||
>(options: { readonly tag: Tag; readonly variants: Variants }): Union<Tag, Variants> {
|
||||
const equivalent = compileUnion<Tag, Variants>(options.tag, options.variants)
|
||||
return { type: "union", ...options, equivalent }
|
||||
}
|
||||
|
||||
export function keyedUnion<
|
||||
const Name extends PropertyKey,
|
||||
A,
|
||||
const Tag extends PropertyKey,
|
||||
const Variants extends Readonly<Record<string, Field<unknown>>>,
|
||||
>(options: {
|
||||
readonly key: NamedKey<Name, A>
|
||||
readonly tag: Tag
|
||||
readonly variants: Variants
|
||||
}): KeyedUnion<Name, A, Tag, Variants> {
|
||||
const equivalentVariant = compileUnion<Tag, Variants>(options.tag, options.variants)
|
||||
const key = (value: KeyedVariant<Name, A, Tag, Variants>) => value[options.key.name]
|
||||
const equivalent = (left: KeyedVariant<Name, A, Tag, Variants>, right: KeyedVariant<Name, A, Tag, Variants>) =>
|
||||
options.key.field.equivalent(key(left), key(right)) && equivalentVariant(left, right)
|
||||
return { type: "keyed-union", ...options, equivalent }
|
||||
}
|
||||
|
||||
export function compile<const StructFields extends Fields>(
|
||||
layout: Struct<StructFields>,
|
||||
options?: { readonly backend?: "closure" | "generated" },
|
||||
): Plan<Value<StructFields>, Value<StructFields>[KeyName<StructFields>]>
|
||||
export function compile<
|
||||
const Name extends PropertyKey,
|
||||
A,
|
||||
const Tag extends PropertyKey,
|
||||
const Variants extends Readonly<Record<string, Field<unknown>>>,
|
||||
>(
|
||||
layout: KeyedUnion<Name, A, Tag, Variants>,
|
||||
options?: { readonly backend?: "closure" | "generated" },
|
||||
): Plan<KeyedVariant<Name, A, Tag, Variants>, A>
|
||||
export function compile(input: unknown, options: { readonly backend?: "closure" | "generated" } = {}): unknown {
|
||||
const layout = input as
|
||||
| Struct<Fields>
|
||||
| KeyedUnion<PropertyKey, unknown, PropertyKey, Readonly<Record<string, Field<unknown>>>>
|
||||
if (layout.type === "keyed-union") {
|
||||
return makePlan(
|
||||
layout.key.name,
|
||||
(value: unknown) => (value as Record<PropertyKey, unknown>)[layout.key.name],
|
||||
(options.backend === "generated"
|
||||
? generateUnion(layout.tag, layout.variants)
|
||||
: compileUnion(layout.tag, layout.variants)) as (left: unknown, right: unknown) => boolean,
|
||||
)
|
||||
}
|
||||
|
||||
const keys = Reflect.ownKeys(layout.fields).filter((name) => layout.fields[name].isKey)
|
||||
if (keys.length !== 1) throw new Error("Keyed layout must declare exactly one key field")
|
||||
const key = keys[0]
|
||||
const fields = Reflect.ownKeys(layout.fields)
|
||||
.filter((name) => name !== key && !layout.fields[name].immutable)
|
||||
.map((name) => ({ name, field: layout.fields[name] }))
|
||||
const equivalent =
|
||||
options.backend === "generated"
|
||||
? generateEquivalent<unknown>(generated(fields))
|
||||
: compileEquivalent<unknown>(fields)
|
||||
return {
|
||||
fields: layout.fields,
|
||||
...makePlan(key, (value: unknown) => (value as Record<PropertyKey, unknown>)[key], equivalent),
|
||||
}
|
||||
}
|
||||
|
||||
export function collection<const StructFields extends Fields, const Definitions extends Indexes<Value<StructFields>>>(
|
||||
layout: Struct<StructFields>,
|
||||
define: (index: IndexBuilder<Value<StructFields>>) => Definitions,
|
||||
options?: { readonly backend?: "closure" | "generated" },
|
||||
): CollectionPlan<Value<StructFields>, Value<StructFields>[KeyName<StructFields>], Definitions>
|
||||
export function collection<
|
||||
const Name extends PropertyKey,
|
||||
A,
|
||||
const Tag extends PropertyKey,
|
||||
const Variants extends Readonly<Record<string, Field<unknown>>>,
|
||||
const Definitions extends Indexes<KeyedVariant<Name, A, Tag, Variants>>,
|
||||
>(
|
||||
layout: KeyedUnion<Name, A, Tag, Variants>,
|
||||
define: (index: IndexBuilder<KeyedVariant<Name, A, Tag, Variants>>) => Definitions,
|
||||
options?: { readonly backend?: "closure" | "generated" },
|
||||
): CollectionPlan<KeyedVariant<Name, A, Tag, Variants>, A, Definitions>
|
||||
export function collection(
|
||||
input: unknown,
|
||||
define: unknown,
|
||||
options?: { readonly backend?: "closure" | "generated" },
|
||||
): unknown {
|
||||
const plan = compile(input as never, options) as Plan<unknown, unknown>
|
||||
const definitions = (define as (index: IndexBuilder<unknown>) => Indexes<unknown>)({
|
||||
members: (extract) => ({ type: "members", extract }),
|
||||
first: (matches) => ({ type: "first", matches }),
|
||||
})
|
||||
return {
|
||||
...plan,
|
||||
make(initial: readonly unknown[] = [], makeOptions?: { readonly metrics?: Keyed.Metrics }) {
|
||||
return makeCollection(plan, definitions, initial, makeOptions)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function makePlan<A, Key>(key: PropertyKey, getKey: (value: A) => Key, equivalent: (left: A, right: A) => boolean) {
|
||||
return {
|
||||
key,
|
||||
equivalent,
|
||||
keyOf: getKey,
|
||||
make(initial: readonly A[] = [], options?: { readonly metrics?: Keyed.Metrics }) {
|
||||
const values = Keyed.make({ key: getKey, equivalent, metrics: options?.metrics })
|
||||
values.set(initial)
|
||||
return values
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function makeCollection<A, Key, Definitions extends Indexes<A>>(
|
||||
plan: Plan<A, Key>,
|
||||
definitions: Definitions,
|
||||
initial: readonly A[],
|
||||
options?: { readonly metrics?: Keyed.Metrics },
|
||||
): Collection<A, Key, Definitions> {
|
||||
const values = plan.make([], options)
|
||||
type MemberEntry = {
|
||||
readonly type: "members"
|
||||
readonly name: PropertyKey
|
||||
readonly extract: (value: A) => Iterable<unknown>
|
||||
readonly counts: Map<unknown, number>
|
||||
readonly byKey: Map<Key, { readonly source: Iterable<unknown>; readonly members: Set<unknown> }>
|
||||
}
|
||||
type FirstEntry = {
|
||||
readonly type: "first"
|
||||
readonly name: PropertyKey
|
||||
readonly matches: (value: A) => boolean
|
||||
readonly matching: Set<Key>
|
||||
slot?: Readable<A>
|
||||
}
|
||||
type Entry = MemberEntry | FirstEntry
|
||||
type Inspection =
|
||||
| {
|
||||
readonly type: "members"
|
||||
readonly value: { readonly source: Iterable<unknown>; readonly members: Set<unknown> }
|
||||
}
|
||||
| { readonly type: "members-change"; readonly add: readonly unknown[]; readonly remove: readonly unknown[] }
|
||||
| { readonly type: "first"; readonly value: boolean }
|
||||
const entries: Entry[] = Reflect.ownKeys(definitions).map((name) => {
|
||||
const definition = definitions[name]
|
||||
if (definition.type === "members") {
|
||||
return { type: "members", name, extract: definition.extract, counts: new Map(), byKey: new Map() }
|
||||
}
|
||||
return { type: "first", name, matches: definition.matches, matching: new Set() }
|
||||
})
|
||||
const byName = new Map(entries.map((entry) => [entry.name, entry]))
|
||||
const emptyMembers: readonly unknown[] = []
|
||||
|
||||
const collection: Collection<A, Key, Definitions> = {
|
||||
...values,
|
||||
set(next) {
|
||||
const keys = next.map(plan.keyOf)
|
||||
if (new Set(keys).size !== keys.length) return values.set(next)
|
||||
const prepared = new Map(
|
||||
next.map((value, index) => {
|
||||
const key = keys[index]
|
||||
const previous = values.get(key)?.()
|
||||
return [key, previous && plan.equivalent(previous, value) ? current(key) : inspect(value, key)]
|
||||
}),
|
||||
)
|
||||
return Transaction.run(() => {
|
||||
const changed = values.set(next)
|
||||
if (!changed) return false
|
||||
entries.forEach(clear)
|
||||
values.slots().forEach((slot) => {
|
||||
const key = plan.keyOf(slot())
|
||||
const inspection = prepared.get(key)!
|
||||
entries.forEach((entry, index) => add(entry, key, slot, inspection[index]))
|
||||
})
|
||||
entries.forEach((entry) => entry.type === "first" && findFirst(entry))
|
||||
return true
|
||||
})
|
||||
},
|
||||
update(value) {
|
||||
const key = plan.keyOf(value)
|
||||
const slot = values.get(key)
|
||||
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
const inspection = inspect(value, key)
|
||||
return Transaction.run(() => {
|
||||
const changed = values.update(value)
|
||||
if (changed) entries.forEach((entry, index) => replace(entry, key, slot, inspection[index]))
|
||||
return changed
|
||||
})
|
||||
},
|
||||
modify(key, f, changes) {
|
||||
const slot = values.get(key)
|
||||
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
let inspection: readonly Inspection[] | undefined
|
||||
return Transaction.run(() => {
|
||||
const changed = values.modify(key, (previous) => {
|
||||
const value = f(previous)
|
||||
if (values.get(plan.keyOf(value)) !== slot) throw new Error("Keyed modify must preserve the value key")
|
||||
inspection = inspect(value, key, changes?.members)
|
||||
return value
|
||||
})
|
||||
if (changed) entries.forEach((entry, index) => replace(entry, key, slot, inspection![index]))
|
||||
return changed
|
||||
})
|
||||
},
|
||||
insert(value, position) {
|
||||
const key = plan.keyOf(value)
|
||||
if (values.has(key)) return values.insert(value, position)
|
||||
requirePosition(position)
|
||||
const inspection = inspect(value, key)
|
||||
return Transaction.run(() => {
|
||||
const slot = values.insert(value, position)
|
||||
entries.forEach((entry, index) => add(entry, key, slot, inspection[index]))
|
||||
entries.forEach((entry) => entry.type === "first" && updateFirstAfterPlacement(entry, slot))
|
||||
return slot
|
||||
})
|
||||
},
|
||||
remove(key) {
|
||||
const slot = values.get(key)
|
||||
if (!slot) return false
|
||||
return Transaction.run(() => {
|
||||
const removed = values.remove(key)
|
||||
entries.forEach((entry) => remove(entry, key))
|
||||
entries.forEach((entry) => entry.type === "first" && entry.slot === slot && findFirst(entry))
|
||||
return removed
|
||||
})
|
||||
},
|
||||
move(key, position) {
|
||||
const slot = values.get(key)
|
||||
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
return Transaction.run(() => {
|
||||
const moved = values.move(key, position)
|
||||
if (moved) entries.forEach((entry) => entry.type === "first" && updateFirstAfterPlacement(entry, slot))
|
||||
return moved
|
||||
})
|
||||
},
|
||||
hasMember(name, member) {
|
||||
const entry = byName.get(normalizeName(name))
|
||||
return entry?.type === "members" && entry.counts.has(member)
|
||||
},
|
||||
first(name) {
|
||||
const entry = byName.get(normalizeName(name))
|
||||
return entry?.type === "first" ? entry.slot : undefined
|
||||
},
|
||||
}
|
||||
collection.set(initial)
|
||||
return collection
|
||||
|
||||
function inspect(value: A, key: Key, changes?: Readonly<Record<PropertyKey, unknown>>): readonly Inspection[] {
|
||||
return entries.map((entry) =>
|
||||
entry.type === "members"
|
||||
? (() => {
|
||||
const change = changes?.[entry.name] as
|
||||
| { readonly add?: readonly unknown[]; readonly remove?: readonly unknown[] }
|
||||
| undefined
|
||||
if (change)
|
||||
return {
|
||||
type: "members-change" as const,
|
||||
add: change.add ?? emptyMembers,
|
||||
remove: change.remove ?? emptyMembers,
|
||||
}
|
||||
const source = entry.extract(value)
|
||||
const previous = entry.byKey.get(key)
|
||||
return {
|
||||
type: "members" as const,
|
||||
value: { source, members: source === previous?.source ? previous.members : new Set(source) },
|
||||
}
|
||||
})()
|
||||
: { type: "first", value: entry.matches(value) },
|
||||
)
|
||||
}
|
||||
|
||||
function current(key: Key): readonly Inspection[] {
|
||||
return entries.map((entry) => {
|
||||
if (entry.type === "members") return { type: "members", value: entry.byKey.get(key)! }
|
||||
return { type: "first", value: entry.matching.has(key) }
|
||||
})
|
||||
}
|
||||
|
||||
function clear(entry: Entry) {
|
||||
if (entry.type === "members") entry.byKey.clear()
|
||||
if (entry.type === "first") entry.matching.clear()
|
||||
if (entry.type === "members") entry.counts.clear()
|
||||
if (entry.type === "first") entry.slot = undefined
|
||||
}
|
||||
|
||||
function add(entry: Entry, key: Key, slot: Readable<A>, inspection: Inspection) {
|
||||
if (entry.type === "members" && inspection.type === "members") {
|
||||
entry.byKey.set(key, inspection.value)
|
||||
inspection.value.members.forEach((member) => entry.counts.set(member, (entry.counts.get(member) ?? 0) + 1))
|
||||
return
|
||||
}
|
||||
if (entry.type === "first" && inspection.type === "first" && inspection.value) entry.matching.add(key)
|
||||
}
|
||||
|
||||
function remove(entry: Entry, key: Key) {
|
||||
if (entry.type === "first") {
|
||||
entry.matching.delete(key)
|
||||
return
|
||||
}
|
||||
entry.byKey.get(key)?.members.forEach((member) => adjust(entry.counts, member, -1))
|
||||
entry.byKey.delete(key)
|
||||
}
|
||||
|
||||
function replace(entry: Entry, key: Key, slot: Readable<A>, inspection: Inspection) {
|
||||
if (entry.type === "members" && inspection.type === "members-change") {
|
||||
const current = entry.byKey.get(key)!
|
||||
const members = current.members
|
||||
inspection.remove.forEach((member) => {
|
||||
if (!members.delete(member)) return
|
||||
adjust(entry.counts, member, -1)
|
||||
})
|
||||
inspection.add.forEach((member) => {
|
||||
if (members.has(member)) return
|
||||
members.add(member)
|
||||
adjust(entry.counts, member, 1)
|
||||
})
|
||||
entry.byKey.set(key, { source: members, members })
|
||||
return
|
||||
}
|
||||
if (entry.type === "members" && inspection.type === "members") {
|
||||
const previous = entry.byKey.get(key)!.members
|
||||
if (previous === inspection.value.members) {
|
||||
entry.byKey.set(key, inspection.value)
|
||||
return
|
||||
}
|
||||
inspection.value.members.forEach((member) => !previous.has(member) && adjust(entry.counts, member, 1))
|
||||
previous.forEach((member) => !inspection.value.members.has(member) && adjust(entry.counts, member, -1))
|
||||
entry.byKey.set(key, inspection.value)
|
||||
return
|
||||
}
|
||||
if (entry.type !== "first" || inspection.type !== "first") return
|
||||
const previous = entry.matching.has(key)
|
||||
if (inspection.value) entry.matching.add(key)
|
||||
if (!inspection.value) entry.matching.delete(key)
|
||||
if (entry.slot === slot && !inspection.value) findFirst(entry)
|
||||
if (entry.slot !== slot && !previous && inspection.value) updateFirstAfterPlacement(entry, slot)
|
||||
}
|
||||
|
||||
function adjust(counts: Map<unknown, number>, member: unknown, amount: 1 | -1) {
|
||||
const count = (counts.get(member) ?? 0) + amount
|
||||
if (count === 0) counts.delete(member)
|
||||
if (count > 0) counts.set(member, count)
|
||||
}
|
||||
|
||||
function updateFirstAfterPlacement(entry: FirstEntry, slot: Readable<A>) {
|
||||
if (!entry.matching.has(plan.keyOf(slot()))) return
|
||||
if (!entry.slot) {
|
||||
entry.slot = slot
|
||||
return
|
||||
}
|
||||
if (entry.slot === slot) return findFirst(entry)
|
||||
const slots = values.slots()
|
||||
if (slots.indexOf(slot) < slots.indexOf(entry.slot)) entry.slot = slot
|
||||
}
|
||||
|
||||
function findFirst(entry: FirstEntry) {
|
||||
entry.slot = values.slots().find((slot) => entry.matching.has(plan.keyOf(slot())))
|
||||
}
|
||||
|
||||
function requirePosition(position?: Keyed.Position<Key>) {
|
||||
if (!position || position === "end") return
|
||||
const key = "before" in position ? position.before : position.after
|
||||
if (!values.has(key)) throw new Error(`Keyed value does not exist: ${String(key)}`)
|
||||
}
|
||||
|
||||
function normalizeName(name: PropertyKey) {
|
||||
return typeof name === "number" ? String(name) : name
|
||||
}
|
||||
}
|
||||
|
||||
function make<A>(equivalent: (left: A, right: A) => boolean): Field<A> {
|
||||
return { equivalent }
|
||||
}
|
||||
|
||||
function primitive<A>(): Field<A> {
|
||||
return { primitive: true, equivalent: Object.is }
|
||||
}
|
||||
|
||||
function compileUnion<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>(
|
||||
tag: Tag,
|
||||
variants: Variants,
|
||||
) {
|
||||
type A = Variant<Tag, Variants>
|
||||
return (left: A, right: A) => {
|
||||
const name = left[tag]
|
||||
if (name !== right[tag] || typeof name !== "string") return false
|
||||
const variant = variants[name]
|
||||
return variant ? variant.equivalent(left, right) : false
|
||||
}
|
||||
}
|
||||
|
||||
function compileFields<A>(fields: Fields) {
|
||||
return compileEquivalent<A>(
|
||||
Reflect.ownKeys(fields)
|
||||
.filter((name) => !fields[name].immutable)
|
||||
.map((name) => ({ name, field: fields[name] })),
|
||||
)
|
||||
}
|
||||
|
||||
function generateEquivalent<A>(
|
||||
fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>,
|
||||
) {
|
||||
if (fields.some((field) => typeof field.name === "symbol")) return compileEquivalent<A>(fields)
|
||||
const custom: Array<Field<unknown>["equivalent"]> = []
|
||||
const comparisons = fields.map((field) => {
|
||||
const name = JSON.stringify(String(field.name))
|
||||
if (field.field.primitive) return `Object.is(left[${name}], right[${name}])`
|
||||
const index = custom.push(field.field.equivalent) - 1
|
||||
return `custom[${index}](left[${name}], right[${name}])`
|
||||
})
|
||||
const factory = Function("custom", `return (left, right) => ${comparisons.join(" && ") || "true"}`) as (
|
||||
custom: ReadonlyArray<Field<unknown>["equivalent"]>,
|
||||
) => (left: A, right: A) => boolean
|
||||
return factory(custom)
|
||||
}
|
||||
|
||||
function generateUnion<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>(
|
||||
tag: Tag,
|
||||
variants: Variants,
|
||||
) {
|
||||
if (typeof tag === "symbol") return compileUnion(tag, variants)
|
||||
const names = Object.keys(variants)
|
||||
const custom = names.map((name) => generateField(variants[name]))
|
||||
const cases = names
|
||||
.map((name, index) => `case ${JSON.stringify(name)}: return custom[${index}](left, right)`)
|
||||
.join(";")
|
||||
const property = JSON.stringify(String(tag))
|
||||
const factory = Function(
|
||||
"custom",
|
||||
`return (left, right) => { if (left[${property}] !== right[${property}]) return false; switch (left[${property}]) { ${cases}; default: return false } }`,
|
||||
) as (custom: ReadonlyArray<Field<unknown>["equivalent"]>) => (left: unknown, right: unknown) => boolean
|
||||
return factory(custom)
|
||||
}
|
||||
|
||||
function generateField(field: Field<unknown>): Field<unknown>["equivalent"] {
|
||||
if (field.immutable) return () => true
|
||||
const layout = field as Field<unknown> & {
|
||||
readonly type?: "struct" | "union" | "keyed-union"
|
||||
readonly fields?: Fields
|
||||
readonly tag?: PropertyKey
|
||||
readonly variants?: Readonly<Record<string, Field<unknown>>>
|
||||
readonly key?: NamedKey<PropertyKey, unknown>
|
||||
}
|
||||
if (layout.type === "struct") {
|
||||
const fields = Reflect.ownKeys(layout.fields!)
|
||||
.filter((name) => !layout.fields![name].immutable)
|
||||
.map((name) => ({ name, field: layout.fields![name] }))
|
||||
return generateEquivalent(generated(fields))
|
||||
}
|
||||
if (layout.type === "union") return generateUnion(layout.tag!, layout.variants!)
|
||||
if (layout.type !== "keyed-union") return field.equivalent
|
||||
const equivalent = generateUnion(layout.tag!, layout.variants!) as (left: unknown, right: unknown) => boolean
|
||||
return (left, right) => {
|
||||
const a = left as Record<PropertyKey, unknown>
|
||||
const b = right as Record<PropertyKey, unknown>
|
||||
return layout.key!.field.equivalent(a[layout.key!.name], b[layout.key!.name]) && equivalent(left, right)
|
||||
}
|
||||
}
|
||||
|
||||
function generated(fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>) {
|
||||
return fields.map((field) => ({ ...field, field: { ...field.field, equivalent: generateField(field.field) } }))
|
||||
}
|
||||
|
||||
function compileEquivalent<A>(fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>) {
|
||||
const value = (input: A) => input as Record<PropertyKey, unknown>
|
||||
if (fields.length === 0) return (_left: A, _right: A) => true
|
||||
if (fields.length === 1) {
|
||||
const first = fields[0]
|
||||
return (left: A, right: A) => first.field.equivalent(value(left)[first.name], value(right)[first.name])
|
||||
}
|
||||
if (fields.length === 2) {
|
||||
const first = fields[0]
|
||||
const second = fields[1]
|
||||
return (left: A, right: A) => {
|
||||
const a = value(left)
|
||||
const b = value(right)
|
||||
return (
|
||||
first.field.equivalent(a[first.name], b[first.name]) &&
|
||||
second.field.equivalent(a[second.name], b[second.name])
|
||||
)
|
||||
}
|
||||
}
|
||||
if (fields.length === 3) {
|
||||
const first = fields[0]
|
||||
const second = fields[1]
|
||||
const third = fields[2]
|
||||
return (left: A, right: A) => {
|
||||
const a = value(left)
|
||||
const b = value(right)
|
||||
return (
|
||||
first.field.equivalent(a[first.name], b[first.name]) &&
|
||||
second.field.equivalent(a[second.name], b[second.name]) &&
|
||||
third.field.equivalent(a[third.name], b[third.name])
|
||||
)
|
||||
}
|
||||
}
|
||||
if (fields.length === 4) {
|
||||
const first = fields[0]
|
||||
const second = fields[1]
|
||||
const third = fields[2]
|
||||
const fourth = fields[3]
|
||||
return (left: A, right: A) => {
|
||||
const a = value(left)
|
||||
const b = value(right)
|
||||
return (
|
||||
first.field.equivalent(a[first.name], b[first.name]) &&
|
||||
second.field.equivalent(a[second.name], b[second.name]) &&
|
||||
third.field.equivalent(a[third.name], b[third.name]) &&
|
||||
fourth.field.equivalent(a[fourth.name], b[fourth.name])
|
||||
)
|
||||
}
|
||||
}
|
||||
return (left: A, right: A) => {
|
||||
const a = value(left)
|
||||
const b = value(right)
|
||||
return fields.every((field) => field.field.equivalent(a[field.name], b[field.name]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,6 +131,20 @@ describe("Keyed", () => {
|
|||
dispose()
|
||||
})
|
||||
|
||||
it("modifies one existing slot while preserving its key", () => {
|
||||
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
|
||||
keyed.set([item(1, "one")])
|
||||
const slot = keyed.slots()[0]
|
||||
|
||||
expect(keyed.modify(1, (value) => ({ ...value, label: "ONE" }))).toBe(true)
|
||||
expect(keyed.modify(1, (value) => value)).toBe(false)
|
||||
|
||||
expect(keyed.slots()[0]).toBe(slot)
|
||||
expect(slot()).toEqual(item(1, "ONE"))
|
||||
expect(() => keyed.modify(1, (value) => ({ ...value, id: 2 }))).toThrow("Keyed modify must preserve the value key")
|
||||
expect(() => keyed.modify(2, (value) => value)).toThrow("Keyed value does not exist: 2")
|
||||
})
|
||||
|
||||
it("checks key membership without reading the aggregate", () => {
|
||||
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
|
||||
keyed.set([item(1, "one")])
|
||||
|
|
@ -139,6 +153,8 @@ describe("Keyed", () => {
|
|||
expect(keyed.has(2)).toBe(false)
|
||||
expect(keyed.get(1)).toBe(keyed.slots()[0])
|
||||
expect(keyed.get(2)).toBeUndefined()
|
||||
expect(keyed.before(1)).toBeUndefined()
|
||||
expect(keyed.after(1)).toBeUndefined()
|
||||
keyed.remove(1)
|
||||
expect(keyed.has(1)).toBe(false)
|
||||
expect(keyed.get(1)).toBeUndefined()
|
||||
|
|
@ -154,6 +170,8 @@ describe("Keyed", () => {
|
|||
expect(keyed.slots()).toEqual([one, two, three])
|
||||
const four = keyed.insert(item(4, "four"), { after: 3 })
|
||||
expect(keyed.slots()).toEqual([one, two, three, four])
|
||||
expect(keyed.before(3)).toBe(two)
|
||||
expect(keyed.after(3)).toBe(four)
|
||||
expect(keyed.move(3, { before: 1 })).toBe(true)
|
||||
expect(keyed.slots()).toEqual([three, one, two, four])
|
||||
expect(keyed.move(3, { before: 1 })).toBe(false)
|
||||
|
|
|
|||
253
packages/quark/test/layout.test.ts
Normal file
253
packages/quark/test/layout.test.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { describe, expect, it } from "bun:test"
|
||||
import { Layout } from "../src"
|
||||
|
||||
const Item = Layout.struct({
|
||||
id: Layout.key(Layout.number),
|
||||
label: Layout.string,
|
||||
})
|
||||
|
||||
const Job = Layout.struct({
|
||||
id: Layout.key(Layout.string),
|
||||
labels: Layout.array(Layout.string),
|
||||
status: Layout.string,
|
||||
})
|
||||
|
||||
const Jobs = Layout.collection(Job, ({ members, first }) => ({
|
||||
labels: members((job) => job.labels),
|
||||
nextRetry: first((job) => job.status === "retrying"),
|
||||
}))
|
||||
|
||||
describe("Layout", () => {
|
||||
it("compiles a keyed collection from trusted structural metadata", () => {
|
||||
const plan = Layout.compile(Item)
|
||||
const original = { id: 1, label: "one" }
|
||||
const values = plan.make([original])
|
||||
const slot = values.slots()[0]
|
||||
|
||||
expect(plan.key).toBe("id")
|
||||
expect(plan.fields).toBe(Item.fields)
|
||||
expect(values.update({ id: 1, label: "one" })).toBe(false)
|
||||
expect(slot()).toBe(original)
|
||||
expect(values.update({ id: 1, label: "ONE" })).toBe(true)
|
||||
expect(slot()).toEqual({ id: 1, label: "ONE" })
|
||||
})
|
||||
|
||||
it("requires exactly one key field", () => {
|
||||
expect(() => Layout.compile(Layout.struct({ value: Layout.number }))).toThrow(
|
||||
"Keyed layout must declare exactly one key field",
|
||||
)
|
||||
expect(() =>
|
||||
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 generated = Layout.compile(Item, { backend: "generated" })
|
||||
const values = [
|
||||
{ id: 1, label: "one" },
|
||||
{ id: 1, label: "ONE" },
|
||||
{ id: 2, label: "one" },
|
||||
]
|
||||
|
||||
values.forEach((left) => {
|
||||
values.forEach((right) => {
|
||||
expect(generated.equivalent(left, right)).toBe(closure.equivalent(left, right))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("compiles nested discriminated unions and skips immutable fields", () => {
|
||||
const Ref = Layout.struct({ messageID: Layout.string, partID: Layout.string })
|
||||
const Row = Layout.keyedUnion({
|
||||
key: Layout.key("id", Layout.string),
|
||||
tag: "type",
|
||||
variants: {
|
||||
message: Layout.struct({ messageID: Layout.string }),
|
||||
group: Layout.union({
|
||||
tag: "kind",
|
||||
variants: {
|
||||
reasoning: Layout.struct({
|
||||
origin: Layout.immutable(Ref),
|
||||
refs: Layout.array(Ref),
|
||||
completed: Layout.boolean,
|
||||
}),
|
||||
exploration: Layout.struct({
|
||||
origin: Layout.immutable(Ref),
|
||||
refs: Layout.array(Ref),
|
||||
pending: Layout.array(Ref),
|
||||
completed: Layout.boolean,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
const plan = Layout.compile(Row)
|
||||
const generated = Layout.compile(Row, { backend: "generated" })
|
||||
const group = {
|
||||
id: "group-1",
|
||||
type: "group" as const,
|
||||
kind: "exploration" as const,
|
||||
origin: { messageID: "assistant-1", partID: "read-1" },
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
pending: [] as Array<{ messageID: string; partID: string }>,
|
||||
completed: false,
|
||||
}
|
||||
|
||||
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, {
|
||||
id: "group-1",
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
origin: group.origin,
|
||||
refs: group.refs,
|
||||
completed: false,
|
||||
}),
|
||||
).toBe(false)
|
||||
const candidates = [
|
||||
group,
|
||||
{ ...group, origin: { messageID: "ignored", partID: "ignored" } },
|
||||
{ ...group, completed: true },
|
||||
{ ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] },
|
||||
{
|
||||
id: "group-1",
|
||||
type: "group" as const,
|
||||
kind: "reasoning" as const,
|
||||
origin: group.origin,
|
||||
refs: group.refs,
|
||||
completed: false,
|
||||
},
|
||||
]
|
||||
candidates.forEach((left) => {
|
||||
candidates.forEach((right) => expect(generated.equivalent(left, right)).toBe(plan.equivalent(left, right)))
|
||||
})
|
||||
})
|
||||
|
||||
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 parent = Layout.compile(Parent)
|
||||
|
||||
expect(
|
||||
parent.equivalent({ id: 1, child: { id: 1, value: "same" } }, { id: 1, child: { id: 2, value: "same" } }),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("composes indexed collections without domain-specific behavior", () => {
|
||||
const jobs = Jobs.make([
|
||||
{ id: "one", labels: ["billing", "urgent"], status: "running" },
|
||||
{ id: "two", labels: ["billing"], status: "retrying" },
|
||||
{ id: "three", labels: [], status: "retrying" },
|
||||
])
|
||||
|
||||
expect(jobs.hasMember("labels", "billing")).toBe(true)
|
||||
expect(jobs.hasMember("labels", "missing")).toBe(false)
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("two")
|
||||
expect(jobs.before("two")?.().id).toBe("one")
|
||||
expect(jobs.after("two")?.().id).toBe("three")
|
||||
|
||||
jobs.modify("one", (job) => ({ ...job, labels: [], status: "retrying" }))
|
||||
expect(jobs.hasMember("labels", "urgent")).toBe(false)
|
||||
expect(jobs.hasMember("labels", "billing")).toBe(true)
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("one")
|
||||
|
||||
jobs.remove("two")
|
||||
expect(jobs.hasMember("labels", "billing")).toBe(false)
|
||||
jobs.move("three", { before: "one" })
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("three")
|
||||
})
|
||||
|
||||
it("keeps indexes synchronized across inserts, updates, and replacement", () => {
|
||||
const jobs = Jobs.make([{ id: "one", labels: ["one"], status: "running" }])
|
||||
|
||||
jobs.insert({ id: "three", labels: ["shared"], status: "retrying" })
|
||||
jobs.insert({ id: "two", labels: ["shared"], status: "retrying" }, { before: "three" })
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("two")
|
||||
|
||||
jobs.update({ id: "two", labels: [], status: "done" })
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("three")
|
||||
expect(jobs.hasMember("labels", "shared")).toBe(true)
|
||||
|
||||
jobs.remove("three")
|
||||
expect(jobs.first("nextRetry")).toBeUndefined()
|
||||
expect(jobs.hasMember("labels", "shared")).toBe(false)
|
||||
|
||||
jobs.set([
|
||||
{ id: "four", labels: ["replacement"], status: "retrying" },
|
||||
{ id: "five", labels: [], status: "running" },
|
||||
])
|
||||
expect(jobs.hasMember("labels", "replacement")).toBe(true)
|
||||
expect(jobs.hasMember("labels", "one")).toBe(false)
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("four")
|
||||
})
|
||||
|
||||
it("applies explicit member deltas without re-extracting unchanged membership", () => {
|
||||
let extractions = 0
|
||||
const IndexedJobs = Layout.collection(Job, ({ members }) => ({
|
||||
labels: members((job) => {
|
||||
extractions++
|
||||
return job.labels
|
||||
}),
|
||||
}))
|
||||
const jobs = IndexedJobs.make([
|
||||
{ id: "one", labels: ["billing"], status: "running" },
|
||||
{ id: "two", labels: ["billing"], status: "running" },
|
||||
])
|
||||
const before = extractions
|
||||
|
||||
jobs.modify("one", (job) => ({ ...job, labels: ["urgent"] }), {
|
||||
members: { labels: { add: ["urgent"], remove: ["billing"] } },
|
||||
})
|
||||
|
||||
expect(extractions).toBe(before)
|
||||
expect(jobs.hasMember("labels", "billing")).toBe(true)
|
||||
expect(jobs.hasMember("labels", "urgent")).toBe(true)
|
||||
|
||||
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" } } })
|
||||
}
|
||||
})
|
||||
|
||||
it("does not commit mutations when an index callback throws", () => {
|
||||
const ThrowingJobs = Layout.collection(Job, ({ members, first }) => ({
|
||||
labels: members((job) => {
|
||||
if (job.labels.includes("boom")) throw new Error("boom")
|
||||
return job.labels
|
||||
}),
|
||||
nextRetry: first((job) => job.status === "retrying"),
|
||||
}))
|
||||
const jobs = ThrowingJobs.make([{ id: "one", labels: ["safe"], status: "running" }])
|
||||
|
||||
expect(() => jobs.update({ id: "one", labels: ["boom"], status: "retrying" })).toThrow("boom")
|
||||
expect(() => jobs.set([{ id: "two", labels: ["boom"], status: "retrying" }])).toThrow("boom")
|
||||
|
||||
expect(jobs.values()).toEqual([{ id: "one", labels: ["safe"], status: "running" }])
|
||||
expect(jobs.hasMember("labels", "safe")).toBe(true)
|
||||
expect(jobs.hasMember("labels", "boom")).toBe(false)
|
||||
expect(jobs.first("nextRetry")).toBeUndefined()
|
||||
|
||||
expect(() => jobs.insert({ id: "two", labels: ["boom"], status: "retrying" }, { before: "missing" })).toThrow(
|
||||
"Keyed value does not exist: missing",
|
||||
)
|
||||
})
|
||||
|
||||
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") }),
|
||||
)
|
||||
const jobs = OptionalJobs.make([{ id: undefined, status: "running" }])
|
||||
|
||||
jobs.update({ id: undefined, status: "retrying" })
|
||||
|
||||
expect(jobs.first("retry")?.()).toEqual({ id: undefined, status: "retrying" })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue