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:
imccyu
2026-08-10 21:32:50 +08:00
parent b64da061a8
commit d88f771e19
59 changed files with 956 additions and 257 deletions

View File

@@ -22,12 +22,17 @@
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./types": {
"types": "./lib/types/types.d.ts",
"default": "./lib/types/types.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",

View File

@@ -8,15 +8,13 @@
import { Context, Service } from '@deepseek-ai/cordis'
import type z from '@deepseek-ai/schemastery'
import type { Branded } from '@deepseek-ai/dsh-brand'
import { redactSecrets } from './redact.ts'
import type { RedactedSecret } from './redact.ts'
import type { SettingsNamespace, SettingsUpdateSource } from './types.ts'
export { redactSecrets } from './redact.ts'
export type { RedactedSecret, RedactedValue } from './redact.ts'
/** Nominal id of one registered settings namespace. */
export type SettingsNamespace = Branded<'SettingsNamespace'>
export type { SettingsNamespace, SettingsUpdateSource } from './types.ts'
const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/
@@ -35,9 +33,6 @@ export function settingsNamespace(value: string): SettingsNamespace {
/** When a namespace's changes take effect for its owner. */
export type SettingsApplies = 'live' | 'restart'
/** Origin of one committed settings change. */
export type SettingsUpdateSource = 'update' | 'provider'
/** Registration options beyond the namespace schema. */
export interface SettingsRegisterOptions<T> {
/** Composition-layer values resolved below the user layer (entry-config subset). */
@@ -137,38 +132,6 @@ declare module '@deepseek-ai/cordis' {
interface Context {
settings: Settings
}
interface Events {
/**
* Committed change to one registered namespace's resolved value. Emitted
* after the provider persisted (for `update`) or published (`provider`)
* the change; never emitted when the resolved value is deep-equal.
* Listener failures are contained and logged — a sync throw and an async
* rejection alike — except `INVARIANT`-coded failures, which rethrow
* after every listener ran; that rethrow reaches the emitter only from
* synchronous listeners, so invariant checks on this event must not be
* async functions.
* @param ns - the namespace whose resolved value changed.
* @param next - the new resolved value.
* @param prev - the previous resolved value.
* @param source - whether the change entered through `update()` or the provider.
* @mode emit
*/
'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
/**
* One registered namespace's RAW user section changed, whether or not the
* resolved value did. `settings/updated` is the consumer-facing event and
* stays deep-equal-gated; this one exists for configuration surfaces,
* which must learn that a field went from inherited to overridden (same
* resolved value, different meaning) and that their held revision is
* stale. Listener containment matches `settings/updated`.
* @param ns - the namespace whose stored section changed.
* @param revision - the namespace's new revision.
* @mode emit
*/
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
}
}
/**

View File

@@ -0,0 +1,50 @@
/**
* Client-safe type surface of the user-settings seam: the namespace brand, the
* commit-origin union, and the seam's Cordis event declarations. Types only —
* no runtime code, and nothing here reaches a Host-only symbol, so a Client
* compilation face reads exactly the signatures the Host emits.
*
* @module @deepseek-ai/dsh-settings/types
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
/** Nominal id of one registered settings namespace. */
export type SettingsNamespace = Branded<'SettingsNamespace'>
/** Origin of one committed settings change. */
export type SettingsUpdateSource = 'update' | 'provider'
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* Committed change to one registered namespace's resolved value. Emitted
* after the provider persisted (for `update`) or published (`provider`)
* the change; never emitted when the resolved value is deep-equal.
* Listener failures are contained and logged — a sync throw and an async
* rejection alike — except `INVARIANT`-coded failures, which rethrow
* after every listener ran; that rethrow reaches the emitter only from
* synchronous listeners, so invariant checks on this event must not be
* async functions.
* @param ns - the namespace whose resolved value changed.
* @param next - the new resolved value.
* @param prev - the previous resolved value.
* @param source - whether the change entered through `update()` or the provider.
* @mode emit
*/
'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
/**
* One registered namespace's RAW user section changed, whether or not the
* resolved value did. `settings/updated` is the consumer-facing event and
* stays deep-equal-gated; this one exists for configuration surfaces,
* which must learn that a field went from inherited to overridden (same
* resolved value, different meaning) and that their held revision is
* stale. Listener containment matches `settings/updated`.
* @param ns - the namespace whose stored section changed.
* @param revision - the namespace's new revision.
* @mode emit
*/
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
}
}