refactor(api): converge the Remote failure vocabulary and client surface

Single RemoteError with a merge-extensible, domain-prefixed code map;
owners throw at the failure point; streams surface marked failures;
clients consume ctx.remote directly with isRemoteFailure as the only
discrimination point and construct no failure instances.
This commit is contained in:
imccyu
2026-08-28 22:37:36 +08:00
parent 12d7b4ed0c
commit 804b1ffbfc
252 changed files with 3182 additions and 3832 deletions
@@ -7,40 +7,15 @@
* @module @deepseek-ai/dsh-client-ui-message-feedback/client/controller
*/
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { Context as ClientContext } from '@deepseek-ai/cordis'
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
import type { MessageId } from '@deepseek-ai/dsh-client-connection/client'
import type { MessageId } from '@deepseek-ai/dsh-api-remotes/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {
MessageFeedbackDeleteResult,
MessageFeedbackItem,
MessageFeedbackListResult,
MessageFeedbackPutResult,
MessageFeedbackRating,
} from '@deepseek-ai/dsh-message-feedback/types'
/**
* The three Remote calls this controller needs. The generated face wraps every
* business result in {@link RemoteResult}: a carrier failure arrives as the
* `ok: false` branch rather than a rejection, so this controller reads one
* envelope and never wraps a call to recover a transport error.
*/
export interface MessageFeedbackRemote {
list: (request: { sessionId: SessionId }) => Promise<RemoteResult<MessageFeedbackListResult>>
put: (request: {
sessionId: SessionId
messageId: MessageId
rating: MessageFeedbackRating
note?: string
ifVersion: MessageFeedbackItem['version'] | null
}) => Promise<RemoteResult<MessageFeedbackPutResult>>
delete: (request: {
sessionId: SessionId
messageId: MessageId
ifVersion: MessageFeedbackItem['version']
}) => Promise<RemoteResult<MessageFeedbackDeleteResult>>
}
/** Load state of the one list read that seeds every per-message control. */
export type MessageFeedbackStatus = 'cold' | 'loading' | 'ready' | 'error'
@@ -111,11 +86,11 @@ export class MessageFeedbackController implements HostObservable<MessageFeedback
private disposed = false
/**
* @param remote - the messageFeedback Remote namespace.
* @param ctx - the browser plugin context carrying the messageFeedback Remote namespace.
* @param sessionId - Session owning every addressed assistant message.
*/
constructor(
private readonly remote: MessageFeedbackRemote,
private readonly ctx: ClientContext,
private readonly sessionId: SessionId,
) {}
@@ -243,7 +218,7 @@ export class MessageFeedbackController implements HostObservable<MessageFeedback
note: string | undefined,
observed: MessageFeedbackItem | undefined,
): Promise<MessageFeedbackActionResult> {
const carried = await this.remote.put({
const carried = await this.ctx.remote.messageFeedback.put({
sessionId: this.sessionId,
messageId,
rating,
@@ -265,7 +240,7 @@ export class MessageFeedbackController implements HostObservable<MessageFeedback
messageId: MessageId,
observed: MessageFeedbackItem,
): Promise<MessageFeedbackActionResult> {
const carried = await this.remote.delete({
const carried = await this.ctx.remote.messageFeedback.delete({
sessionId: this.sessionId,
messageId,
ifVersion: observed.version,
@@ -288,34 +263,26 @@ export class MessageFeedbackController implements HostObservable<MessageFeedback
/** Fetch the whole sidecar and publish it as the seeded view. */
private async load(): Promise<MessageFeedbackActionResult> {
try {
const carried = await this.remote.list({ sessionId: this.sessionId })
if (this.disposed) return OK
if (!carried.ok) {
this.publish({ status: 'error', items: this.view.items, error: carried.error.message })
return carrierFailure(carried.error)
}
const result = carried.value
if (!result.ok) {
this.publish({ status: 'error', items: this.view.items, error: describe(result.error.code) })
return fail(result.error.code)
}
const items = new Map<MessageId, MessageFeedbackItem>()
for (const item of result.value.items) items.set(item.messageId, item)
this.publish({ status: 'ready', items, error: null })
return OK
} catch (error) {
if (this.disposed) return OK
const message = error instanceof Error ? error.message : String(error)
this.publish({ status: 'error', items: this.view.items, error: message })
return { ok: false, error: { code: 'transport', message } }
const carried = await this.ctx.remote.messageFeedback.list({ sessionId: this.sessionId })
if (this.disposed) return OK
if (!carried.ok) {
this.publish({ status: 'error', items: this.view.items, error: carried.error.message })
return carrierFailure(carried.error)
}
const result = carried.value
if (!result.ok) {
this.publish({ status: 'error', items: this.view.items, error: describe(result.error.code) })
return fail(result.error.code)
}
const items = new Map<MessageId, MessageFeedbackItem>()
for (const item of result.value.items) items.set(item.messageId, item)
this.publish({ status: 'ready', items, error: null })
return OK
}
/**
* Serialize one mutation behind this Session's prior mutation so queued
* operations always compare against the committed version, and translate a
* transport throw into the same settled shape the controls already render.
* operations always compare against the committed version.
*/
private mutate(
operation: () => Promise<MessageFeedbackActionResult>,
@@ -331,17 +298,7 @@ export class MessageFeedbackController implements HostObservable<MessageFeedback
// oxlint-disable-next-line typescript/no-unnecessary-condition -- dispose() can run during the await.
if (this.disposed) return DISPOSED
}
try {
return await operation()
} catch (error) {
return {
ok: false,
error: {
code: 'transport',
message: error instanceof Error ? error.message : String(error),
},
}
}
return await operation()
}
const result = this.operationTail.then(guarded, guarded)
// `guarded` settles every carrier and business failure as a
@@ -25,7 +25,7 @@ import type { MessageFeedbackInjected } from './slots.ts'
import { en, zh } from './locales.ts'
export type {
MessageFeedbackActionResult, MessageFeedbackStatus, MessageFeedbackView, MessageFeedbackRemote,
MessageFeedbackActionResult, MessageFeedbackStatus, MessageFeedbackView,
} from './controller.ts'
export type { MessageFeedbackActionProps, MessageFeedbackInjected } from './slots.ts'
export type { MessageFeedbackKey } from './locales.ts'
@@ -48,7 +48,7 @@ export function apply(ctx: ClientContext): void {
const controllerFor = (sessionId: SessionId): MessageFeedbackController => {
let controller = controllers.get(sessionId)
if (controller === undefined) {
controller = new MessageFeedbackController(ctx.remote.messageFeedback, sessionId)
controller = new MessageFeedbackController(ctx, sessionId)
controllers.set(sessionId, controller)
}
return controller
@@ -12,7 +12,7 @@ import type {
HostObservable, InjectFace, PropsLocale, PropsRuntime,
} from '@deepseek-ai/dsh-client-ui-slots'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { MessageId } from '@deepseek-ai/dsh-client-connection/client'
import type { MessageId } from '@deepseek-ai/dsh-api-remotes/client'
import type { MessageFeedbackRating } from '@deepseek-ai/dsh-message-feedback/types'
// Type-only: pulls this package's LocaleNamespaceMap merge (the 'feedback' seat).
import type {} from './locales.ts'