diff --git a/packages/client/runtime/src/client/slots.ts b/packages/client/runtime/src/client/slots.ts index 8c93050cd9..700026df52 100644 --- a/packages/client/runtime/src/client/slots.ts +++ b/packages/client/runtime/src/client/slots.ts @@ -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 } -/** - * 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 = { - /** 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 - -/** - * 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 = - [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, - H extends StoreDecl | undefined = undefined, - C extends SlotComponent = SlotComponent, - >( - options: RegisterOptions & { inject?: undefined }, - component: C - & SlotComponent & keyof SlotMap & string, HandleOf>, object>> - & RendersCheck, - ): () => void - register< - K extends keyof SlotMap & string, - I extends object, - const D extends ChildrenDecl = Record, - H extends StoreDecl | undefined = undefined, - C extends SlotComponent = SlotComponent, - >( - options: RegisterOptions & { inject: (...args: InjectParams) => I }, - component: C - & SlotComponent & keyof SlotMap & string, HandleOf>, I>> - & RendersCheck, - ): () => 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()') + }