refactor(gui): slot system standard — single register, four props shares, framework store seat
The definitive slot model for the web client, replacing the first-generation
define/register two-step, ScopedSlots whitelist faces, and binding handles:
- 'root' is the only a-priori slot (SlotsService built-in); the shell renders
exactly ctx.slots.renderSlot('root', {}).
- register is the single API: children = slot declaration + render
authorization + runtime spec in one options object; misconfiguration fails
loud at load (duplicate declaration, undeclared contribution, one store
handle under two scopes).
- Component props arrive in four auto-derived shares: PropsRuntime<K>
(owner params + session/global standard kits via declare-merge),
PropsRenderSlots<S>, PropsStore<H>, and the inject business face.
sessionId is framework-supplied; hooks are framework-made only.
- Framework store seat: defineStore factories declare schema/actions/persist;
read = useStore, write = baked actions only; store scope derives from the
mounting entry; per-session persist keys and clearPersisted lifecycle.
- inject factories read the apply closure's own ctx (binding handles retired;
root-ctx back door closed); SessionProvider is self-wired render-prop.
- Rendering sits behind the SlotRenderer install seam; runtime stays
React-free; ownership ledger keyed to the single entry axis closes the
stale-authority window (StaleAuthorizationError probes).
Docs: the slot type-chain note is refreshed in place as the slot system
standard RFC (bilingual pair re-recorded); the web client architecture RFC
defers its slot sections there; packages/client/AGENTS.md gains the slot and
props discipline; gui-testing/web-styling notes drop missions/ references.
Tests: suites rewritten to the standard (props fed directly, real store
engines via createXXXStore().create(), no render machinery); load-time
negative samples for declaration/authorization/store conflicts; verified by
real-host playwright run (three columns, empty state, collapse, keyed session
remount, cross-slot selection sharing).
docs(ui-sidebar): point contract reference at the committed slot standard RFC
missions/ is workspace-local and never committed; the README must not cite it.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Renderer install seam (slot terminal design §8): the SlotRenderer interface
|
||||
* web-react's machinery implements, the host surface the runtime SlotsService
|
||||
* presents to the installed renderer, and the render-path authorization
|
||||
* errors. Pure types plus two error classes — this package stays React-free
|
||||
* at runtime (React types only).
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import type { SlotEntryDef, SlotSpec, StoredEntry } from './index.ts'
|
||||
|
||||
/** Minimal observable surface for host-provided standard-kit data sources. */
|
||||
export interface HostObservable<T> {
|
||||
getSnapshot(): T
|
||||
subscribe(fn: () => void): () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-erased store instance face at the render seam (the typed twin is
|
||||
* {@link StoreInstance}): selector hook plus draft-stripped action callbacks.
|
||||
* Typing lands at the component seam via {@link PropsStore}.
|
||||
*/
|
||||
export interface StoreInstanceLike {
|
||||
readonly useSelector: unknown
|
||||
readonly actions: Record<string, (...params: never[]) => void>
|
||||
}
|
||||
|
||||
/** Session standard kit resolved per session id (identity-stable per session scope; a recreated scope yields a new cell). */
|
||||
export interface SessionCell {
|
||||
sessionId: string
|
||||
/** Bound conversation-snapshot selector hook (wide here; runtime narrows at its export seam). */
|
||||
useSession: unknown
|
||||
}
|
||||
|
||||
/** renderSlot dispatch options at the machinery level: keyed dispatch key, list filtering, empty fallback. */
|
||||
export interface RenderOpts {
|
||||
entryKey?: string
|
||||
only?: string
|
||||
fallback?: ReactNode
|
||||
}
|
||||
|
||||
/** Host surface the runtime SlotsService presents to the installed renderer. */
|
||||
export interface SlotRendererHost {
|
||||
/**
|
||||
* Subscribe to a key's registration changes (microtask-batched).
|
||||
* @param key - slot key.
|
||||
* @param fn - change callback.
|
||||
* @returns unsubscribe.
|
||||
*/
|
||||
subscribe(key: string, fn: () => void): () => void
|
||||
/**
|
||||
* Monotonic version for uSES pairing.
|
||||
* @param key - slot key.
|
||||
* @returns current version.
|
||||
*/
|
||||
getVersion(key: string): number
|
||||
/**
|
||||
* Snapshot the registered entries for a key (stable reference between mutations).
|
||||
* @param key - slot key.
|
||||
* @returns entries in registration (list: order) sequence.
|
||||
*/
|
||||
entriesOf(key: string): readonly StoredEntry[]
|
||||
/**
|
||||
* Declared runtime spec from the declarations ledger.
|
||||
* @param key - slot key.
|
||||
* @returns the spec, or undefined while the key is undeclared (outlets render empty).
|
||||
*/
|
||||
specOf(key: string): SlotSpec<SlotEntryDef> | undefined
|
||||
/**
|
||||
* Stale-authorization check: whether the entry is still in the ledger.
|
||||
* @param entry - a previously rendered entry.
|
||||
* @returns false once the entry's registration was disposed.
|
||||
*/
|
||||
isLive(entry: StoredEntry): boolean
|
||||
/**
|
||||
* Resolve (create or return cached) the store instance for an entry's
|
||||
* declared handle under a scope key; lifecycle rides the ledger axis.
|
||||
* @param entry - entry whose declaration carries the handle.
|
||||
* @param scopeKey - session id for session-scope slots, undefined for root scope.
|
||||
* @returns the instance, or undefined when the entry declares no store.
|
||||
*/
|
||||
storeOf(entry: StoredEntry, scopeKey: string | undefined): StoreInstanceLike | undefined
|
||||
/** Session-side standard-kit sources. */
|
||||
sessions: {
|
||||
/** Session list source backing the useSessions standard hook. */
|
||||
list: HostObservable<unknown>
|
||||
/** Current-session source backing SessionProvider's self-wiring (design fiat ①). */
|
||||
current: HostObservable<string | undefined>
|
||||
/**
|
||||
* Resolve the session standard kit.
|
||||
* @param id - session id.
|
||||
* @returns the cell, or undefined for an unknown session (provider falls to empty).
|
||||
*/
|
||||
cell(id: string): SessionCell | undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** The install seam: runtime owns install()/renderSlot(); web-react implements rendering. */
|
||||
export interface SlotRenderer {
|
||||
/**
|
||||
* Render the root slot tree over the host surface (the only ctx-level entry).
|
||||
* @param host - the installing service's host surface.
|
||||
* @param ownerProps - owner props from the shell's renderSlot('root', ...) call.
|
||||
* @returns the rendered tree.
|
||||
*/
|
||||
renderRoot(host: SlotRendererHost, ownerProps: object): ReactNode
|
||||
}
|
||||
|
||||
/** Thrown when a retained renderSlot binding is invoked after its declaring entry was disposed. */
|
||||
export class StaleAuthorizationError extends Error {}
|
||||
|
||||
/**
|
||||
* Thrown when a renderSlot binding is invoked for a key outside its entry's
|
||||
* children declaration (plain-JS backstop; typed callers are narrowed
|
||||
* statically).
|
||||
*/
|
||||
export class SlotOwnershipError extends Error {}
|
||||
Reference in New Issue
Block a user