feat(client): inject slot declaration lifetimes
This commit is contained in:
@@ -19,7 +19,6 @@ import type { BoundActions, HandleOf, PropsStore, SnapshotSelectorHook, StoreDec
|
||||
|
||||
export * from './store.ts'
|
||||
export * from './renderer.ts'
|
||||
export * from './deferred.ts'
|
||||
|
||||
/** Slot contract table. Owners extend via declaration merging; entries are {@link SlotEntryDef}. */
|
||||
export interface SlotMap {}
|
||||
@@ -457,9 +456,12 @@ interface SlotRecord {
|
||||
spec: SlotSpec<SlotEntryDef> | undefined
|
||||
/** Diagnostics: which slot's entry declared this key ('(built-in)' for root). */
|
||||
declaredBy: string | undefined
|
||||
/** Monotonic declaration lifetime, distinct from ordinary entry mutations. */
|
||||
declarationEpoch: number
|
||||
entries: readonly StoredEntry[]
|
||||
version: number
|
||||
listeners: Set<() => void>
|
||||
declarationListeners: Set<() => void>
|
||||
}
|
||||
|
||||
const NO_ENTRIES: readonly StoredEntry[] = Object.freeze([])
|
||||
@@ -473,8 +475,10 @@ const NO_ENTRIES: readonly StoredEntry[] = Object.freeze([])
|
||||
*
|
||||
* Change propagation contract: versions bump and {@link SlotCore.onMutate}
|
||||
* fires synchronously per mutation (registry state is consistent when they
|
||||
* fire); {@link SlotCore.subscribe} notifications batch per microtask, so N
|
||||
* same-tick mutations produce one notification per touched key.
|
||||
* fire); {@link SlotCore.subscribeDeclaration} fires synchronously for each
|
||||
* declaration lifetime boundary; {@link SlotCore.subscribe} notifications
|
||||
* batch per microtask, so N same-tick mutations produce one notification per
|
||||
* touched key.
|
||||
*/
|
||||
export class SlotCore {
|
||||
private records = new Map<string, SlotRecord>()
|
||||
@@ -491,6 +495,7 @@ export class SlotCore {
|
||||
const root = this.record('root')
|
||||
root.spec = { kind: 'single', scope: 'root' }
|
||||
root.declaredBy = '(built-in)'
|
||||
root.declarationEpoch = 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -631,12 +636,22 @@ export class SlotCore {
|
||||
rec.entries = next
|
||||
this.markDirty(options.name, rec)
|
||||
if (options.children) {
|
||||
const declarations: [key: string, record: SlotRecord][] = []
|
||||
for (const [childKey, childSpec] of Object.entries(options.children)) {
|
||||
const childRec = this.record(childKey)
|
||||
childRec.spec = childSpec
|
||||
childRec.declaredBy = `an entry in "${options.name}"${options.registrant ? ` (${options.registrant})` : ''}`
|
||||
childRec.declarationEpoch += 1
|
||||
declarations.push([childKey, childRec])
|
||||
}
|
||||
// Synchronous listeners may register into or try to redeclare a sibling;
|
||||
// publish only after the whole children table owns its declarations.
|
||||
for (const [childKey, childRec] of declarations) {
|
||||
this.markDirty(childKey, childRec)
|
||||
}
|
||||
for (const [, childRec] of declarations) {
|
||||
this.notifyDeclaration(childRec)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (!rec.entries.includes(entry)) return
|
||||
@@ -692,6 +707,16 @@ export class SlotCore {
|
||||
return this.records.get(key)?.spec
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the declaration lifetime of a key. Entry additions and removals do
|
||||
* not change it; declaration creation and collapse each advance it.
|
||||
* @param key - slot key.
|
||||
* @returns monotonic epoch (0 before the first declaration).
|
||||
*/
|
||||
declarationEpoch(key: string): number {
|
||||
return this.records.get(key)?.declarationEpoch ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to registration changes for a key (microtask-batched).
|
||||
* Subscribing ahead of declaration is allowed; the declaration notifies.
|
||||
@@ -705,6 +730,22 @@ export class SlotCore {
|
||||
return () => { rec.listeners.delete(fn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to declaration lifetime boundaries for a key. Notifications
|
||||
* are synchronous so declaration teardown finishes before a subsequent
|
||||
* same-tick registration can observe stale resources. Ordinary entry
|
||||
* mutations do not notify this surface. A children table commits every
|
||||
* sibling declaration before its first notification.
|
||||
* @param key - slot key.
|
||||
* @param fn - declaration or collapse callback.
|
||||
* @returns unsubscribe.
|
||||
*/
|
||||
subscribeDeclaration(key: string, fn: () => void): () => void {
|
||||
const rec = this.record(key)
|
||||
rec.declarationListeners.add(fn)
|
||||
return () => { rec.declarationListeners.delete(fn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Monotonic version for a key, bumped synchronously per mutation so a
|
||||
* uSES getSnapshot read is never stale when its batched notification lands.
|
||||
@@ -746,8 +787,10 @@ export class SlotCore {
|
||||
const doomed = childRec.entries
|
||||
childRec.spec = undefined
|
||||
childRec.declaredBy = undefined
|
||||
childRec.declarationEpoch += 1
|
||||
childRec.entries = NO_ENTRIES
|
||||
this.markDirty(childKey, childRec)
|
||||
this.notifyDeclaration(childRec)
|
||||
for (const dead of doomed) this.releaseEntry(dead)
|
||||
}
|
||||
}
|
||||
@@ -755,7 +798,15 @@ export class SlotCore {
|
||||
private record(key: string): SlotRecord {
|
||||
let rec = this.records.get(key)
|
||||
if (!rec) {
|
||||
rec = { spec: undefined, declaredBy: undefined, entries: NO_ENTRIES, version: 0, listeners: new Set() }
|
||||
rec = {
|
||||
spec: undefined,
|
||||
declaredBy: undefined,
|
||||
declarationEpoch: 0,
|
||||
entries: NO_ENTRIES,
|
||||
version: 0,
|
||||
listeners: new Set(),
|
||||
declarationListeners: new Set(),
|
||||
}
|
||||
this.records.set(key, rec)
|
||||
}
|
||||
return rec
|
||||
@@ -771,6 +822,10 @@ export class SlotCore {
|
||||
}
|
||||
}
|
||||
|
||||
private notifyDeclaration(rec: SlotRecord): void {
|
||||
for (const fn of [...rec.declarationListeners]) fn()
|
||||
}
|
||||
|
||||
private flush(): void {
|
||||
// Reset before iterating so a mutation from inside a listener re-schedules.
|
||||
this.flushScheduled = false
|
||||
|
||||
Reference in New Issue
Block a user