fix: slot
This commit is contained in:
@@ -18,8 +18,7 @@ import { Service } from 'cordis'
|
|||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
|
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
import type {
|
import type {
|
||||||
ChildrenDecl, ComposedProps, HandleOf, InjectParams, KindOptions, OwnerOf,
|
OwnerOf, SlotEntryDef, SlotMap, SlotRenderer, SlotRendererHost,
|
||||||
SlotComponent, SlotEntryDef, SlotMap, SlotRenderer, SlotRendererHost,
|
|
||||||
SlotScope, SlotSpec, StoreDecl, StoredEntry, StoreInstanceLike,
|
SlotScope, SlotSpec, StoreDecl, StoredEntry, StoreInstanceLike,
|
||||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
|
|
||||||
@@ -57,34 +56,6 @@ interface StoreAxisRecord {
|
|||||||
instances: Map<string, EngineStoreInstance>
|
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). */
|
/** Type-erased options view the implementation works with (the typed overloads proved the shares). */
|
||||||
interface ErasedRegisterOptions {
|
interface ErasedRegisterOptions {
|
||||||
name: string
|
name: string
|
||||||
@@ -118,46 +89,23 @@ export class SlotsService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The single registration API (see SlotCore.register for the full
|
* The single registration API. The typed face IS the core's register
|
||||||
* semantics: children declaration, store seat, inject face, load-time
|
* (both overloads reused verbatim — one authority, no structural copy;
|
||||||
* validation, unload cascade). This layer adds: disposal through the
|
* see SlotCore.register for children declaration, store seat, inject
|
||||||
* caller's ctx.effect (fiber unload = cascade), exclusive-factory minting
|
* face, load-time validation, and the unload cascade). This layer adds:
|
||||||
* (`store: createXxxStore` becomes a per-entry handle), the registrant
|
* disposal through the caller's ctx.effect (fiber unload = cascade),
|
||||||
* diagnostics stamp, and store-instance lifecycle on the entry axis.
|
* exclusive-factory minting (`store: createXxxStore` becomes a per-entry
|
||||||
* @param options - name + children + store + inject (+ kind-shaped key/id/order/label).
|
* handle), the registrant diagnostics stamp, and store-instance lifecycle
|
||||||
* @param component - pure component typed by the four-share composed props.
|
* on the entry axis.
|
||||||
* @returns disposer (idempotent; stale calls after fiber teardown are no-ops).
|
*
|
||||||
|
* 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<
|
declare readonly register: SlotCore['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()')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install the shell's renderer (web-react's createSlotRenderer product).
|
* Install the shell's renderer (web-react's createSlotRenderer product).
|
||||||
@@ -253,11 +201,6 @@ export class SlotsService extends Service {
|
|||||||
return this._core.getVersion(key)
|
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. */
|
/** Delegating registration path: factory minting + registrant stamp + core write + instance-axis bookkeeping. */
|
||||||
private _register(options: ErasedRegisterOptions, component: unknown): () => void {
|
private _register(options: ErasedRegisterOptions, component: unknown): () => void {
|
||||||
// Exclusive stores pass the factory itself: minted here into a per-entry
|
// 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)
|
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