feat(remote): deliver allowlisted Host events through ctx.remote.$on
api/remotes owns the allowlist and its type projection; type-meta owns the shape predicate, the selection seat, and the internal remote/host-event carrier signal; api/gateway's Client half turns that signal into $on callbacks through a private dispatch. apiproxy forwards each allowlisted emission verbatim in one host/remote-event frame, registered ahead of the derived invalidation frames so frame order is unchanged, and drops the three per-event variants it replaces. Owner packages move their Events declarations into client-safe ./types exports, so a consumer's listener signature is the Host's own declaration.
This commit is contained in:
@@ -34,7 +34,8 @@
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-client-ui-settings",
|
||||
"@deepseek-ai/dsh-client-locale"
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-api-remotes"
|
||||
],
|
||||
"platform": "web"
|
||||
}
|
||||
@@ -45,6 +46,8 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-schema-form": "workspace:^",
|
||||
@@ -52,10 +55,10 @@
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
|
||||
@@ -12,6 +12,9 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
// Type-only: pulls the ctx.remote merge and the forwarded-event key face
|
||||
// (settings/credentials invalidations ride the allowlist) into this program.
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { ModelsSection } from './ModelsSection.tsx'
|
||||
import type { ModelsSectionInjected } from './ModelsSection.tsx'
|
||||
import { DeepSeekOnboardingDialog } from './DeepSeekOnboardingDialog.tsx'
|
||||
@@ -48,7 +51,7 @@ export function refreshIfLoaded(controller: ModelsSettingsStore): void {
|
||||
* ui-settings' apply, whose activation order relative to this one is NOT
|
||||
* constrained; registration depends on each slot through `slots.inject()`.
|
||||
*/
|
||||
export const inject = ['slots', 'locale', 'connection']
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote']
|
||||
|
||||
/**
|
||||
* Register the Models section once the `settings.section` declaration is on
|
||||
@@ -82,8 +85,8 @@ export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => {
|
||||
const refresh = (): void => { refreshIfLoaded(controller) }
|
||||
const disposers = [
|
||||
ctx.on('settings/changed', refresh),
|
||||
ctx.on('credentials/changed', refresh),
|
||||
ctx.remote.$on('settings/document-updated', refresh),
|
||||
ctx.remote.$on('credentials/updated', refresh),
|
||||
ctx.on('models/changed', refresh),
|
||||
ctx.on('connection/reset', refresh),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply, inject, refreshIfLoaded } from '@deepseek-ai/dsh-client-ui-models/client'
|
||||
import { ModelsSection } from '../src/client/ModelsSection.tsx'
|
||||
import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
||||
@@ -18,6 +18,9 @@ async function bench() {
|
||||
await ctx.plugin(SlotsService).await()
|
||||
const locale = new LocaleService(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
// The plugins inject `remote`; forwarded events reach them through the
|
||||
// same `remote/host-event` signal the connection sink republishes.
|
||||
new TestRemote(ctx)
|
||||
// The apply path only captures the wire face; no call leaves this fake
|
||||
// until a section actually loads.
|
||||
ctx.provide('connection', { api: {} } as never)
|
||||
@@ -39,7 +42,7 @@ function declare(slots: SlotsService): () => void {
|
||||
|
||||
describe('ui-models apply', () => {
|
||||
it('declares the services it uses', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection'])
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote'])
|
||||
})
|
||||
|
||||
it('registers the models nav entry for declarations before or after apply', async () => {
|
||||
@@ -135,8 +138,8 @@ describe('pushed invalidations', () => {
|
||||
declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
// The fake wire face has no methods: a fetch attempt would throw.
|
||||
b.ctx.emit('settings/changed', 'llm-pi-ai')
|
||||
b.ctx.emit('credentials/changed', 'OPENAI_API_KEY')
|
||||
b.ctx.emit('remote/host-event', 'settings/document-updated', ['llm-pi-ai', 1])
|
||||
b.ctx.emit('remote/host-event', 'credentials/updated', ['OPENAI_API_KEY'])
|
||||
b.ctx.emit('models/changed')
|
||||
b.ctx.emit('connection/reset')
|
||||
})
|
||||
@@ -167,7 +170,7 @@ describe('pushed invalidations', () => {
|
||||
)()
|
||||
injected.controller.store.update((state) => { state.status = 'ready' })
|
||||
const load = vi.spyOn(injected.controller, 'load').mockResolvedValue()
|
||||
b.ctx.emit('credentials/changed', 'DEEPSEEK_API_KEY')
|
||||
b.ctx.emit('remote/host-event', 'credentials/updated', ['DEEPSEEK_API_KEY'])
|
||||
expect(load).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../api/remotes/tsconfig.client.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user