The construction-rollback + late-conflict-rollback + loud-rethrow block was about to be a verbatim clone across the two flow packages; ui-slots now owns it as deferGroupRegistration (one occupant, several holes, as a unit), with direct specs for all three arms, and the native flow consumes it.
129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
/**
|
|
* Declaration-aware registration deferral: the shared timing machinery for
|
|
* registering into a slot whose declaring entry activates in unconstrained
|
|
* order (dshClient.inject edges never sequence apply). Presence is judged on
|
|
* the LEDGER, not a local flag — after an HMR collapse re-declares the slot,
|
|
* the cascade has already removed the entry while the local disposer went
|
|
* stale, and a flag guard would block the re-registration.
|
|
*/
|
|
|
|
/** Minimal registry face the deferral reads (SlotsService satisfies it). */
|
|
export interface DeferralRegistry {
|
|
/** Declared spec lookup (undefined = not declared yet). */
|
|
spec(name: string): unknown
|
|
/** Current entries of the slot (component identity is the presence judge). */
|
|
entries(name: string): readonly { component: unknown }[]
|
|
/** Subscribe to the slot's ledger changes; returns the unsubscriber. */
|
|
subscribe(name: string, listener: () => void): () => void
|
|
}
|
|
|
|
/** Handle over one deferred registration. */
|
|
export interface DeferredRegistration {
|
|
/**
|
|
* Drop the current registration (stale disposers are harmless no-ops) and
|
|
* immediately re-attempt — the refresh path for registrants whose options
|
|
* carry localized text.
|
|
*/
|
|
refresh(): void
|
|
/** Unsubscribe and unregister (idempotent through the slot core). */
|
|
dispose(): void
|
|
}
|
|
|
|
/**
|
|
* Register into `name` as soon as its declaration is on the ledger, and
|
|
* re-register whenever the declaration reappears after a collapse.
|
|
* @param registry - the slot registry face.
|
|
* @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 => {
|
|
if (registry.spec(name) === undefined) return
|
|
if (registry.entries(name).some(e => e.component === component)) return
|
|
dispose = register()
|
|
}
|
|
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?.()
|
|
dispose = undefined
|
|
tryRegister()
|
|
},
|
|
dispose() {
|
|
unsubscribe()
|
|
dispose?.()
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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() } }
|
|
}
|