fix: slot

This commit is contained in:
imccyu
2026-07-23 03:22:56 +08:00
parent 1b0ea07bce
commit 004a988168

View File

@@ -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()')
}