Registrations declare a dictionary namespace (locale: NS) and the renderer
synthesizes a typed t prop for the entry's component from the installed
LocaleFace; the seat binding is re-derived per locale revision, so a language
switch hands out fresh t references and memoized consumers re-render through
ordinary shallow comparison. LocaleNamespaceMap is the declare-merge table
(namespace -> dictionary key union); TranslateNS<'ns'> is the
namespace-addressed translate type (namespace keys plus the shared common
vocabulary), carried by the t seat and by the locale service's typed bind.
LocaleService implements the face (lookup ns -> common -> zh -> key,
revision-carrying snapshots with subscriber isolation) and installs it
through the boot-once slots.installLocale seam, mirroring the renderer
install. The typed register(ns, {zh, en}) overload checks each dictionary
against the namespace's key union and requires every shipped locale, so a
missing or extra key and an unbalanced translation are compile errors.
Dictionary registration bumps the face revision without emitting
locale/change — the event now means exactly 'the active locale switched',
so registration-heavy boot cannot storm event listeners.
184 lines
7.6 KiB
TypeScript
184 lines
7.6 KiB
TypeScript
/** React-free contracts between the slot host and an installed renderer. */
|
|
import type { ReactNode } from 'react'
|
|
import type { SlotEntryDef, SlotSpec, StoredEntry, Translate } from './index.ts'
|
|
|
|
/**
|
|
* The locale face the render machinery consumes: namespace binding plus an
|
|
* observable revision (getSnapshot/subscribe pair — the same HostObservable
|
|
* currency as every other standard-kit source). The revision moves on every
|
|
* active-locale or registry change; the renderer re-derives each entry's `t`
|
|
* from (namespace, revision), so a locale switch hands out NEW function
|
|
* references and memoized components re-render naturally. Implemented by the
|
|
* locale plugin, installed through the runtime SlotsService (installLocale).
|
|
* Install before the first render that needs the seat: outlets bind their
|
|
* revision subscription at mount, and a face appearing later has no channel
|
|
* to notify already-mounted outlets (the locale plugin is immediately-tier
|
|
* infrastructure, so normal compositions install during boot).
|
|
*/
|
|
export interface LocaleFace extends HostObservable<{ revision: number }> {
|
|
/**
|
|
* Bind a namespace to a translate function reading the active locale at
|
|
* call time. Identity may be stable per namespace — freshness of rendered
|
|
* text is carried by the renderer's (ns, revision) seat derivation, not by
|
|
* this binding.
|
|
* @param ns - dictionary namespace.
|
|
* @returns the namespace-bound translate function.
|
|
*/
|
|
bind(ns: string): Translate
|
|
}
|
|
|
|
/** 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}): a bare snapshot source plus the draft-stripped
|
|
* action callbacks. No React hook crosses this seam — the render machinery
|
|
* binds `useStore` from the source at its own side (cached per instance);
|
|
* typing lands at the component seam via {@link PropsStore}.
|
|
*/
|
|
export interface StoreInstanceLike {
|
|
getSnapshot(): unknown
|
|
/**
|
|
* Subscribe to state changes (uSES subscribe side).
|
|
* @param fn - change callback.
|
|
* @returns unsubscribe.
|
|
*/
|
|
subscribe(fn: () => void): () => void
|
|
readonly actions: Record<string, (...params: never[]) => void>
|
|
}
|
|
|
|
/**
|
|
* Per-session standard props resolved per session id (identity-stable per
|
|
* session scope; a recreated scope yields a new info). Plugins contribute
|
|
* members through the runtime `sessions.provide` seam; the render side binds
|
|
* every `hooks` source into a `use<Name>` selector hook (hooks never appear
|
|
* on the host contract) and spreads `props` verbatim. The runtime itself
|
|
* contributes the first entry (`'session'` → `useSession`).
|
|
*/
|
|
export interface SessionMaybeProvideInfo {
|
|
/** Current session id, absent while the application is in no-session mode. */
|
|
sessionId: string | undefined
|
|
/**
|
|
* Static hook roster. Each value is absent with the session; keys remain so
|
|
* session-maybe entries always receive the same hook-shaped standard kit.
|
|
*/
|
|
hooks: Record<string, HostObservable<unknown> | undefined>
|
|
/** Static plain-member roster; values are undefined with the session. */
|
|
props: Record<string, unknown>
|
|
/**
|
|
* Key-addressed projection value sources (the useProjection framework seat,
|
|
* session-projection RFC). Unlike `hooks`, the key space is open — values
|
|
* arrive from host-computed push frames — so the render side binds per
|
|
* resolved key instead of per static roster member. Faces are always
|
|
* defined per key (absence is an `undefined` snapshot); the whole member is
|
|
* absent with the session.
|
|
*/
|
|
projections?: { faceOf(key: string): HostObservable<unknown> } | undefined
|
|
}
|
|
|
|
/** Definite per-session standard props resolved for strict session slots. */
|
|
export interface SessionProvideInfo extends SessionMaybeProvideInfo {
|
|
sessionId: string
|
|
/** Bare observable sources, keyed by hook base name ('session' → useSession). */
|
|
hooks: Record<string, HostObservable<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>
|
|
/**
|
|
* Atomic current-session provide projection used by SessionProvider:
|
|
* selection changes and provider-roster changes publish through this one
|
|
* source, so a stable current id cannot strand mounted entries on an
|
|
* obsolete hook/prop schema. Carries the static roster with sessionId
|
|
* undefined while no current session resolves.
|
|
*/
|
|
provideInfo: HostObservable<SessionMaybeProvideInfo>
|
|
}
|
|
/** Workspace-side standard-kit sources. */
|
|
workspaces: {
|
|
/** Workspace list source backing the useWorkspaces standard hook. */
|
|
list: HostObservable<unknown>
|
|
}
|
|
/**
|
|
* Installed locale face backing the `t` standard seat (absent until the
|
|
* locale plugin installs one; rendering an entry that declared `locale:`
|
|
* without it is an assembly failure).
|
|
*/
|
|
locale?: LocaleFace | 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 {}
|