Merge remote-tracking branch 'origin/master' into codex/migrate-to-oxlint

This commit is contained in:
Turtle
2026-07-29 14:32:21 +08:00
308 changed files with 10890 additions and 1735 deletions

View File

@@ -36,13 +36,22 @@ export interface DeferredRegistration {
* @param name - target slot name.
* @param component - the component whose ledger presence marks "registered".
* @param register - performs the actual registration; returns its disposer.
* @param onFailure - owns a registration failure that fires from a LATER
* ledger flush (a declaration landing after two providers deferred, say):
* the deferral first removes its own subscription, then hands the error
* over instead of throwing through the flush — the callback's chance to
* roll back sibling deferrals and surface the conflict on a loud channel.
* Absent, a late failure rethrows out of the flush.
* @returns the deferral handle (dispose in the owning effect's disposer).
* @throws the immediate registration's failure, after removing the
* just-installed subscription — a throwing construction leaves nothing live.
*/
export function deferRegistration(
registry: DeferralRegistry,
name: string,
component: unknown,
register: () => () => void,
onFailure?: (error: unknown) => void,
): DeferredRegistration {
let dispose: (() => void) | undefined
const tryRegister = (): void => {
@@ -50,8 +59,24 @@ export function deferRegistration(
if (registry.entries(name).some(e => e.component === component)) return
dispose = register()
}
const unsubscribe = registry.subscribe(name, () => { tryRegister() })
tryRegister()
const unsubscribe = registry.subscribe(name, () => {
try {
tryRegister()
} catch (error) {
unsubscribe()
if (onFailure === undefined) throw error
onFailure(error)
}
})
try {
tryRegister()
} catch (error) {
// A synchronous registration failure (the declared slot is already
// occupied) must not leave the just-installed subscription behind: the
// caller receives no handle to dispose it through.
unsubscribe()
throw error
}
return {
refresh() {
dispose?.()
@@ -64,3 +89,40 @@ export function deferRegistration(
},
}
}
/**
* Defer ONE occupant into several holes as a unit. Construction that throws
* partway (a declared hole already occupied registers synchronously) rolls
* every earlier deferral back before rethrowing; a failure surfacing from a
* LATER ledger flush (holes declared after rival providers activated) rolls
* the whole group back the same way and re-raises the wrapped error on the
* global channel the boot's fail-loud handler owns — never a throw through
* the slot flush, never partial occupancy from the group's owner.
* @param registry - the slot registry face.
* @param names - the target holes (one registration per name).
* @param component - the occupant whose ledger presence marks "registered".
* @param register - performs one hole's registration; returns its disposer.
* @returns the group handle (dispose in the owning effect's disposer).
* @throws the immediate registration's failure, after rolling the group back.
*/
export function deferGroupRegistration<K extends string>(
registry: DeferralRegistry,
names: readonly K[],
component: unknown,
register: (name: K) => () => void,
): { dispose: () => void } {
const deferred: DeferredRegistration[] = []
const lateFailure = (error: unknown): void => {
for (const entry of deferred) entry.dispose()
queueMicrotask(() => { throw error instanceof Error ? error : new Error(String(error)) })
}
try {
for (const name of names) {
deferred.push(deferRegistration(registry, name, component, () => register(name), lateFailure))
}
} catch (error) {
for (const entry of deferred) entry.dispose()
throw error
}
return { dispose: () => { for (const entry of deferred) entry.dispose() } }
}

View File

@@ -0,0 +1,126 @@
// deferRegistration lifecycle: declaration-aware registration, HMR
// re-registration, and — the failure contract — no subscription survives a
// construction that throws synchronously (an already-occupied single slot).
import { describe, expect, it, vi } from 'vitest'
import { deferGroupRegistration, deferRegistration, SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
// Shares the merges declared by core.spec.ts (same program); reuse its keys.
const HOLE = 'test.single' as const
function declared(): SlotCore {
const core = new SlotCore()
core.register({ name: 'root', children: { [HOLE]: { kind: 'single', scope: 'root' } } } as never, (() => null) as never)
return core
}
describe('deferRegistration', () => {
it('registers immediately under an existing declaration and disposes cleanly', () => {
const core = declared()
const component = (): null => null
const handle = deferRegistration(core, HOLE, component, () =>
core.register({ name: HOLE } as never, component as never))
expect(core.entries(HOLE)).toHaveLength(1)
handle.dispose()
expect(core.entries(HOLE)).toHaveLength(0)
})
it('hands a late registration failure to onFailure after unsubscribing itself', async () => {
const core = new SlotCore()
const component = (): null => null
const foreign = (): null => null
const failures: unknown[] = []
// Nothing is declared yet: the deferral just subscribes and waits.
const register = vi.fn(() => core.register({ name: HOLE } as never, component as never))
deferRegistration(core, HOLE, component, register, (error) => { failures.push(error) })
// The declaration lands with a foreign occupant racing in first: the
// deferral's flush-time attempt fails, unsubscribes itself, and reports
// through onFailure instead of throwing out of the flush.
core.register({ name: 'root', children: { [HOLE]: { kind: 'single', scope: 'root' } } } as never, (() => null) as never)
const disposeForeign = core.register({ name: HOLE } as never, foreign as never)
await Promise.resolve()
expect(failures.map(String).join('')).toContain('already has a registration')
// Unsubscribed: freeing the hole must not resurrect the loser.
disposeForeign()
await Promise.resolve()
expect(core.entries(HOLE)).toHaveLength(0)
})
it('drops its subscription when the immediate registration throws', async () => {
const core = declared()
const foreign = (): null => null
const disposeForeign = core.register({ name: HOLE } as never, foreign as never)
const component = (): null => null
const register = vi.fn(() => core.register({ name: HOLE } as never, component as never))
// The single hole is occupied: the immediate attempt throws out of the
// constructor, and the caller never receives a handle to dispose.
expect(() => deferRegistration(core, HOLE, component, register)).toThrow(/already has a registration/)
expect(register).toHaveBeenCalledOnce()
// The subscription rolled back with it: freeing the hole flushes a
// notification that must not resurrect the failed registration.
disposeForeign()
await Promise.resolve()
expect(register).toHaveBeenCalledOnce()
expect(core.entries(HOLE)).toHaveLength(0)
})
})
describe('deferGroupRegistration', () => {
const HOLES = ['test.single', 'test.grandchild'] as const
function declaredPair(): SlotCore {
const core = new SlotCore()
core.register({
name: 'root',
children: Object.fromEntries(HOLES.map(name => [name, { kind: 'single', scope: 'root' }])),
} as never, (() => null) as never)
return core
}
it('registers the whole group and disposes it as a unit', () => {
const core = declaredPair()
const component = (): null => null
const group = deferGroupRegistration(core, HOLES, component, name =>
core.register({ name } as never, component as never))
for (const name of HOLES) expect(core.entries(name)).toHaveLength(1)
group.dispose()
for (const name of HOLES) expect(core.entries(name)).toHaveLength(0)
})
it('rolls the group back when construction fails partway', () => {
const core = declaredPair()
const component = (): null => null
core.register({ name: HOLES[1] } as never, (() => null) as never)
expect(() => deferGroupRegistration(core, HOLES, component, name =>
core.register({ name } as never, component as never))).toThrow(/already has a registration/)
// The first hole's registration and subscription rolled back with it.
expect(core.entries(HOLES[0])).toHaveLength(0)
})
it('rolls the group back and re-raises loudly on a late conflict', async () => {
const core = new SlotCore()
const component = (): null => null
const failures: unknown[] = []
const onLoud = (reason: unknown): void => { failures.push(reason) }
process.on('uncaughtException', onLoud)
try {
const group = deferGroupRegistration(core, HOLES, component, name =>
core.register({ name } as never, component as never))
// Declaration lands with a rival racing in ahead of the flush.
core.register({
name: 'root',
children: Object.fromEntries(HOLES.map(name => [name, { kind: 'single', scope: 'root' }])),
} as never, (() => null) as never)
core.register({ name: HOLES[0] } as never, (() => null) as never)
core.register({ name: HOLES[1] } as never, (() => null) as never)
await new Promise(resolve => setTimeout(resolve, 20))
expect(failures.map(String).join('')).toContain('already has a registration')
// No partial occupancy from the group's owner survives.
for (const name of HOLES) {
expect(core.entries(name).filter(entry => entry.component === component)).toHaveLength(0)
}
group.dispose()
} finally {
process.off('uncaughtException', onLoud)
}
})
})