fix: slot
This commit is contained in:
@@ -18,8 +18,7 @@ import { Service } from 'cordis'
|
||||
import type { Context } from 'cordis'
|
||||
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type {
|
||||
ChildrenDecl, ComposedProps, HandleOf, InjectParams, KindOptions, OwnerOf,
|
||||
SlotComponent, SlotEntryDef, SlotMap, SlotRenderer, SlotRendererHost,
|
||||
OwnerOf, SlotEntryDef, SlotMap, SlotRenderer, SlotRendererHost,
|
||||
SlotScope, SlotSpec, StoreDecl, StoredEntry, StoreInstanceLike,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
@@ -57,34 +56,6 @@ interface StoreAxisRecord {
|
||||
instances: Map<string, EngineStoreInstance>
|
||||
}
|
||||
|
||||
/**
|
||||
* Register options as the service face declares them (structurally the
|
||||
* core's BaseOptions, re-declared because ui-slots keeps it private).
|
||||
* FIXME(slot-parity): dedupe once ui-slots exports its options type.
|
||||
*/
|
||||
type RegisterOptions<K extends keyof SlotMap & string, D extends ChildrenDecl, H> = {
|
||||
/** Target slot key (the entry contributes INTO this slot). */
|
||||
name: K
|
||||
/** Child-slot declaration + render authorization + runtime spec, in one table. */
|
||||
children?: D
|
||||
/** Store seat: a shared handle (apply-constructed) or an exclusive factory (framework-called per entry). */
|
||||
store?: H
|
||||
/** Registrant identity label for diagnostics (defaults to the caller's fiber name). */
|
||||
registrant?: string
|
||||
} & KindOptions<SlotMap[K]>
|
||||
|
||||
/**
|
||||
* Compile-time presence check: an entry declaring children MUST consume
|
||||
* `renderSlot` (declaring is claiming). Structural copy of the core's
|
||||
* private RendersCheck; same FIXME as {@link RegisterOptions}.
|
||||
*/
|
||||
type RendersCheck<C, D> =
|
||||
[keyof D & keyof SlotMap & string] extends [never] ? unknown
|
||||
: C extends (props: infer P) => unknown
|
||||
? ('renderSlot' extends keyof P ? unknown
|
||||
: { 'children declared but the component consumes no renderSlot': keyof D & keyof SlotMap & string })
|
||||
: unknown
|
||||
|
||||
/** Type-erased options view the implementation works with (the typed overloads proved the shares). */
|
||||
interface ErasedRegisterOptions {
|
||||
name: string
|
||||
@@ -118,46 +89,23 @@ export class SlotsService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* The single registration API (see SlotCore.register for the full
|
||||
* semantics: children declaration, store seat, inject face, load-time
|
||||
* validation, unload cascade). This layer adds: disposal through the
|
||||
* caller's ctx.effect (fiber unload = cascade), exclusive-factory minting
|
||||
* (`store: createXxxStore` becomes a per-entry handle), the registrant
|
||||
* diagnostics stamp, and store-instance lifecycle on the entry axis.
|
||||
* @param options - name + children + store + inject (+ kind-shaped key/id/order/label).
|
||||
* @param component - pure component typed by the four-share composed props.
|
||||
* @returns disposer (idempotent; stale calls after fiber teardown are no-ops).
|
||||
* The single registration API. The typed face IS the core's register
|
||||
* (both overloads reused verbatim — one authority, no structural copy;
|
||||
* see SlotCore.register for children declaration, store seat, inject
|
||||
* face, load-time validation, and the unload cascade). This layer adds:
|
||||
* disposal through the caller's ctx.effect (fiber unload = cascade),
|
||||
* exclusive-factory minting (`store: createXxxStore` becomes a per-entry
|
||||
* handle), the registrant diagnostics stamp, and store-instance lifecycle
|
||||
* on the entry axis.
|
||||
*
|
||||
* Declared here, implemented by prototype assignment below the class: it
|
||||
* MUST stay a prototype method (never an instance arrow) — the cordis
|
||||
* service proxy binds `this.ctx` to the CALLER's context at call time,
|
||||
* which is what routes the effect (and the unload cascade) into the
|
||||
* caller's fiber. An arrow property would freeze `this` to the service's
|
||||
* own root ctx and silently break per-plugin disposal.
|
||||
*/
|
||||
register<
|
||||
K extends keyof SlotMap & string,
|
||||
const D extends ChildrenDecl = Record<never, never>,
|
||||
H extends StoreDecl | undefined = undefined,
|
||||
C extends SlotComponent<never> = SlotComponent<never>,
|
||||
>(
|
||||
options: RegisterOptions<K, D, H> & { inject?: undefined },
|
||||
component: C
|
||||
& SlotComponent<ComposedProps<K, keyof NoInfer<D> & keyof SlotMap & string, HandleOf<NoInfer<H>>, object>>
|
||||
& RendersCheck<C, D>,
|
||||
): () => void
|
||||
register<
|
||||
K extends keyof SlotMap & string,
|
||||
I extends object,
|
||||
const D extends ChildrenDecl = Record<never, never>,
|
||||
H extends StoreDecl | undefined = undefined,
|
||||
C extends SlotComponent<never> = SlotComponent<never>,
|
||||
>(
|
||||
options: RegisterOptions<K, D, H> & { inject: (...args: InjectParams<K, H>) => I },
|
||||
component: C
|
||||
& SlotComponent<ComposedProps<K, keyof NoInfer<D> & keyof SlotMap & string, HandleOf<NoInfer<H>>, I>>
|
||||
& RendersCheck<C, D>,
|
||||
): () => void
|
||||
register(rawOptions: object, component: unknown): () => void {
|
||||
// The typed overloads above proved the shares; the implementation works
|
||||
// on the erased view (same pattern as the core's register).
|
||||
const options = rawOptions as ErasedRegisterOptions
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return this.ctx.effect(() => this._register(options, component), 'slots.register()')
|
||||
}
|
||||
declare readonly register: SlotCore['register']
|
||||
|
||||
/**
|
||||
* Install the shell's renderer (web-react's createSlotRenderer product).
|
||||
@@ -253,11 +201,6 @@ export class SlotsService extends Service {
|
||||
return this._core.getVersion(key)
|
||||
}
|
||||
|
||||
/** The wrapped pure core (invariant checks read through this). */
|
||||
get core(): SlotCore {
|
||||
return this._core
|
||||
}
|
||||
|
||||
/** Delegating registration path: factory minting + registrant stamp + core write + instance-axis bookkeeping. */
|
||||
private _register(options: ErasedRegisterOptions, component: unknown): () => void {
|
||||
// Exclusive stores pass the factory itself: minted here into a per-entry
|
||||
@@ -356,3 +299,16 @@ export class SlotsService extends Service {
|
||||
if (record.refs === 0) this._stores.delete(handle)
|
||||
}
|
||||
}
|
||||
|
||||
// register's implementation (prototype assignment pairs with the `declare`
|
||||
// inside the class — see its JSDoc for why it must live on the prototype).
|
||||
// Element access reaches the private _register legally and keeps it a
|
||||
// TS-visible read.
|
||||
;(SlotsService.prototype as { register: (options: object, component: unknown) => () => void }).register
|
||||
= function register(this: SlotsService, rawOptions: object, component: unknown): () => void {
|
||||
// The core's overloads proved the shares; the implementation works on
|
||||
// the erased view (same pattern as the core's own implementation arm).
|
||||
const options = rawOptions as ErasedRegisterOptions
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return this.ctx.effect(() => this['_register'](options, component), 'slots.register()')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user