refactor(api): colocate gateway and remote assembly

This commit is contained in:
imccyu
2026-08-07 15:48:29 +08:00
parent d941350227
commit bb61dc13f2
82 changed files with 645 additions and 432 deletions
+423
View File
@@ -0,0 +1,423 @@
/**
* Client projection of generated TypeRT Remote descriptors. Contributions
* install concrete namespace methods; no JavaScript Proxy participates in
* lookup, invocation, or type exposure.
*/
import { Service, symbols } from 'cordis'
import type { Context } from 'cordis'
import type { ConnectionHandle, RpcError } from '@deepseek-ai/dsh-client-connection/client'
import type {
InvocationDescriptor,
TypeRTClientApi,
TypeRTCodec,
TypeRTRemoteContribution,
} from '@deepseek-ai/dsh-type-meta'
type RemoteMethod = (...args: unknown[]) => Promise<unknown>
interface MountToken {
active: boolean
readonly abort: AbortController
}
interface DirectNamespaceRecord {
readonly value: Record<string, RemoteMethod>
readonly tokens: Map<string, MountToken>
}
interface ScopedNamespaceRecord {
readonly service: ScopedRemoteNamespace
readonly tokens: Map<string, MountToken>
}
interface ScopedProjection {
readonly context: string
readonly wire: string
readonly codec: TypeRTCodec
readonly parameterIndex?: number
}
/** Typed API service augmented by generated direct Remote namespaces. */
export type ClientApi = TypeRTClientApi
declare module 'cordis' {
interface Context {
/** Generated direct Remote namespaces selected by the Client assembly. */
api: ClientApi
}
}
/** Required Client services: the TypeRT registry and the existing Connection carrier. */
export const inject = ['typert', 'connection']
/**
* Install the typed Client API service.
* @param ctx - Client Cordis root.
*/
export function apply(ctx: Context): void {
new ClientApiService(ctx)
}
class ClientApiService extends Service implements TypeRTClientApi {
private readonly ownerCtx: Context
private readonly direct = new Map<string, DirectNamespaceRecord>()
private readonly scoped = new Map<string, ScopedNamespaceRecord>()
constructor(ctx: Context) {
super(ctx, 'api')
this.ownerCtx = ctx
}
mount(contribution: TypeRTRemoteContribution): ReturnType<TypeRTClientApi['mount']> {
this.validateContribution(contribution)
const callerCtx = this.ctx
const disposeRemote = callerCtx.typert.remotes.register(contribution)
let disposeMethods: () => void | Promise<void>
try {
disposeMethods = callerCtx.effect(() => {
const installed: Array<() => void> = []
try {
for (const descriptor of contribution.descriptors) installed.push(this.install(descriptor))
} catch (error) {
for (const dispose of installed.reverse()) dispose()
throw error
}
return () => {
for (const dispose of installed.reverse()) dispose()
}
}, `api-gateway.client.mount(${JSON.stringify(contribution.package)})`)
} catch (error) {
/* v8 ignore next -- rollback disposal only rejects if Cordis teardown itself fails while handling the installation error. */
Promise.resolve(disposeRemote()).catch(() => {})
throw error
}
return async () => {
await Promise.all([disposeMethods(), disposeRemote()])
}
}
private validateContribution(contribution: TypeRTRemoteContribution): void {
const direct = new Map<string, Set<string>>()
const scoped = new Map<string, Set<string>>()
const add = (
table: Map<string, Set<string>>,
descriptor: InvocationDescriptor,
kind: 'direct' | 'scoped',
): void => {
const methods = table.get(descriptor.namespace) ?? new Set<string>()
if (methods.has(descriptor.method)) {
throw new Error(`client api: contribution repeats ${kind} method ${endpointOf(descriptor)}`)
}
methods.add(descriptor.method)
table.set(descriptor.namespace, methods)
const live = kind === 'direct'
? this.direct.get(descriptor.namespace)?.tokens
: this.scoped.get(descriptor.namespace)?.tokens
if (live?.has(descriptor.method) === true) {
throw new Error(`client api: ${kind} method ${endpointOf(descriptor)} is already mounted`)
}
}
for (const descriptor of contribution.descriptors) {
requireStrictDescriptor(descriptor)
if (descriptor.invocation.kind === 'direct') add(direct, descriptor, 'direct')
if (scopedProjection(descriptor) !== undefined) add(scoped, descriptor, 'scoped')
}
for (const namespace of direct.keys()) {
if (!this.direct.has(namespace) && namespace in this) {
throw new Error(`client api: namespace ${JSON.stringify(namespace)} conflicts with the API service`)
}
}
for (const [namespace, methods] of scoped) {
const record = this.scoped.get(namespace)
if (record !== undefined) {
for (const method of methods) record.service.assertMethodAvailable(method)
} else {
for (const method of methods) ScopedRemoteNamespace.assertMethodAvailable(namespace, method)
if (this.ownerCtx.reflect.props[namespace] !== undefined) {
throw new Error(`client api: scoped namespace ${JSON.stringify(namespace)} conflicts with an existing Context property`)
}
}
}
}
private install(descriptor: InvocationDescriptor): () => void {
const token: MountToken = { active: true, abort: new AbortController() }
const installed: (() => void)[] = []
try {
if (descriptor.invocation.kind === 'direct') {
installed.push(this.installDirect(descriptor, token))
}
const projection = scopedProjection(descriptor)
if (projection !== undefined) installed.push(this.installScoped(descriptor, projection, token))
} catch (error) {
token.active = false
for (const dispose of installed.reverse()) dispose()
token.abort.abort()
throw error
}
return () => {
/* v8 ignore next -- Cordis effect disposers are idempotent and invoke this cleanup at most once. */
if (!token.active) return
token.active = false
for (const dispose of installed.reverse()) dispose()
token.abort.abort()
}
}
private installDirect(descriptor: InvocationDescriptor, token: MountToken): () => void {
let namespace = this.direct.get(descriptor.namespace)
const fresh = namespace === undefined
if (namespace === undefined) {
namespace = { value: Object.create(null) as Record<string, RemoteMethod>, tokens: new Map() }
Object.defineProperty(this, descriptor.namespace, {
configurable: true,
enumerable: true,
value: namespace.value,
})
}
try {
Object.defineProperty(namespace.value, descriptor.method, {
configurable: true,
enumerable: true,
value: (...args: unknown[]) => this.invoke(descriptor, undefined, token, this.ownerCtx, args),
})
} catch (error) {
if (fresh) Reflect.deleteProperty(this, descriptor.namespace)
throw error
}
if (fresh) this.direct.set(descriptor.namespace, namespace)
namespace.tokens.set(descriptor.method, token)
return () => {
/* v8 ignore next -- duplicate live methods are rejected before installation, so no newer token can replace this one. */
if (namespace.tokens.get(descriptor.method) !== token) return
Reflect.deleteProperty(namespace.value, descriptor.method)
namespace.tokens.delete(descriptor.method)
if (namespace.tokens.size !== 0) return
this.direct.delete(descriptor.namespace)
Reflect.deleteProperty(this, descriptor.namespace)
}
}
private installScoped(
descriptor: InvocationDescriptor,
projection: ScopedProjection,
token: MountToken,
): () => void {
let namespace = this.scoped.get(descriptor.namespace)
if (namespace === undefined) {
const service = new ScopedRemoteNamespace(
this.ownerCtx,
descriptor.namespace,
(current, currentProjection, currentToken, caller, args) =>
this.invoke(current, currentProjection, currentToken, caller, args),
)
service.install(descriptor, projection, token)
namespace = { service, tokens: new Map() }
this.scoped.set(descriptor.namespace, namespace)
} else {
namespace.service.install(descriptor, projection, token)
}
namespace.tokens.set(descriptor.method, token)
return () => {
/* v8 ignore next -- duplicate live methods are rejected before installation, so no newer token can replace this one. */
if (namespace.tokens.get(descriptor.method) !== token) return
namespace.service.remove(descriptor.method)
namespace.tokens.delete(descriptor.method)
}
}
private async invoke(
descriptor: InvocationDescriptor,
projection: ScopedProjection | undefined,
token: MountToken,
callerCtx: Context,
values: readonly unknown[],
): Promise<unknown> {
const endpoint = endpointOf(descriptor)
if (!token.active) throw new Error(`client api: Remote method ${endpoint} is no longer mounted`)
const expected = descriptor.parameters.length - (projection?.parameterIndex === undefined ? 0 : 1)
const hasCallerSignal = descriptor.cancellation !== undefined && values.length === expected + 1
if (values.length !== expected && !hasCallerSignal) {
const contract = descriptor.cancellation === undefined
? `${String(expected)} argument(s)`
: `${String(expected)} business argument(s) plus an optional AbortSignal`
throw new Error(
`client api: ${endpoint} expected ${contract}, got ${String(values.length)}`,
)
}
const args = Object.create(null) as Record<string, unknown>
if (projection !== undefined) {
const binder = this.ownerCtx.typert.contexts.getClient(projection.context)
if (binder === undefined) {
throw new Error(`client api: ${endpoint} has no Client Context binder for ${JSON.stringify(projection.context)}`)
}
const identity = binder.identity(callerCtx)
if (identity === undefined) {
throw new Error(`client api: ${endpoint} requires a ${JSON.stringify(projection.context)} Context`)
}
args[projection.wire] = parse(projection.codec, identity, endpoint, projection.wire)
}
let valueIndex = 0
descriptor.parameters.forEach((parameter, parameterIndex) => {
if (parameterIndex === projection?.parameterIndex) return
args[parameter.wire] = parse(parameter.codec, values[valueIndex], endpoint, parameter.wire)
valueIndex += 1
})
const connection = this.ownerCtx.get('connection') as ConnectionHandle | undefined
if (connection === undefined) throw new Error(`client api: ${endpoint} has no active Connection`)
const callerSignal = hasCallerSignal ? values[expected] as AbortSignal | undefined : undefined
const signal = callerSignal === undefined
? token.abort.signal
: AbortSignal.any([token.abort.signal, callerSignal])
const result = await connection.rpc.call('/api', endpoint, { args }, signal)
if (!mountActive(token)) throw new Error(`client api: Remote method ${endpoint} was withdrawn during invocation`)
if (!result.ok) throw remoteFailure(endpoint, result.error)
return parse(descriptor.result, result.value, endpoint, 'result')
}
}
type InvokeRemote = (
descriptor: InvocationDescriptor,
projection: ScopedProjection,
token: MountToken,
callerCtx: Context,
args: readonly unknown[],
) => Promise<unknown>
class ScopedRemoteNamespace {
private readonly ctx: Context
private readonly ownerCtx: Context
private readonly methods = new Set<string>()
private provided = false
readonly name: string
static assertMethodAvailable(namespace: string, method: string): void {
if (SCOPED_NAMESPACE_FIELDS.has(method) || method in ScopedRemoteNamespace.prototype) {
throw new Error(`client api: scoped method ${JSON.stringify(`${namespace}/${method}`)} conflicts with its namespace service`)
}
}
constructor(
ctx: Context,
name: string,
private readonly invokeRemote: InvokeRemote,
) {
this.ctx = ctx
this.ownerCtx = ctx
this.name = name
Object.defineProperty(this, symbols.tracker, {
value: { associate: name, property: 'ctx' },
})
}
assertMethodAvailable(method: string): void {
ScopedRemoteNamespace.assertMethodAvailable(this.name, method)
if (method in this) {
throw new Error(`client api: scoped method ${JSON.stringify(`${this.name}/${method}`)} conflicts with its namespace service`)
}
}
install(descriptor: InvocationDescriptor, projection: ScopedProjection, token: MountToken): void {
this.assertMethodAvailable(descriptor.method)
const activate = this.methods.size === 0
const method = descriptor.method
try {
Object.defineProperty(this, method, {
configurable: true,
enumerable: true,
value: function (this: ScopedRemoteNamespace, ...args: unknown[]): Promise<unknown> {
return this.invokeRemote(descriptor, projection, token, this.ctx, args)
},
})
if (activate) {
if (this.provided) {
this.ownerCtx.set(this.name, this)
} else {
this.ownerCtx.reflect.provide(this.name, this)
this.provided = true
}
}
} catch (error) {
Reflect.deleteProperty(this, method)
throw error
}
this.methods.add(method)
}
remove(method: string): void {
Reflect.deleteProperty(this, method)
this.methods.delete(method)
if (this.methods.size === 0) this.ownerCtx.set(this.name, undefined)
}
}
const SCOPED_NAMESPACE_FIELDS = new Set(['ctx', 'invokeRemote', 'methods', 'name', 'ownerCtx', 'provided'])
function endpointOf(descriptor: Pick<InvocationDescriptor, 'namespace' | 'method'>): string {
return `${descriptor.namespace}/${descriptor.method}`
}
function mountActive(token: MountToken): boolean {
return token.active
}
function scopedProjection(descriptor: InvocationDescriptor): ScopedProjection | undefined {
if (descriptor.invocation.kind === 'context') {
return {
context: descriptor.invocation.context,
wire: descriptor.invocation.wire,
codec: descriptor.invocation.codec,
}
}
if (descriptor.scope === undefined) return undefined
const lookupParameters = descriptor.parameters
.map((parameter, index) => ({ parameter, index }))
.filter(candidate => candidate.parameter.source === 'lookup')
const selected = lookupParameters.length === 1 ? lookupParameters[0] : undefined
if (selected === undefined
|| selected.parameter.wire !== descriptor.scope.wire
|| selected.parameter.lookup !== descriptor.scope.context) {
throw new Error(
`client api: generated Remote ${endpointOf(descriptor)} scope must select its only lookup parameter`,
)
}
return {
context: descriptor.scope.context,
wire: descriptor.scope.wire,
codec: selected.parameter.codec,
parameterIndex: selected.index,
}
}
function requireStrictDescriptor(descriptor: InvocationDescriptor): void {
const endpoint = endpointOf(descriptor)
requireStrictCodec(descriptor.result, endpoint, 'result')
for (const parameter of descriptor.parameters) {
requireStrictCodec(parameter.codec, endpoint, parameter.wire)
}
if (descriptor.invocation.kind === 'context') {
requireStrictCodec(descriptor.invocation.codec, endpoint, descriptor.invocation.wire)
}
}
function requireStrictCodec(codec: TypeRTCodec, endpoint: string, field: string): void {
if (codec.mode !== 'strict') {
throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`)
}
}
function parse(codec: TypeRTCodec, value: unknown, endpoint: string, field: string): unknown {
if (codec.mode !== 'strict') {
throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`)
}
try {
return codec.schema.parse(value)
} catch (cause) {
throw new Error(`client api: ${endpoint} rejected ${JSON.stringify(field)}`, { cause })
}
}
function remoteFailure(endpoint: string, error: RpcError): Error {
return new Error(`client api: ${endpoint} failed: ${error.code}: ${error.message}`, { cause: error })
}
+637
View File
@@ -0,0 +1,637 @@
/**
* Live TypeRT Remote dispatch over Cordis Services and registered providers.
* Transport, request correlation, and response envelopes belong to Connection.
* @module @deepseek-ai/dsh-api-gateway
*/
import { Context, Service, symbols } from 'cordis'
import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
import {
remoteMethods,
TypeRTLookupFailure,
type InvocationDescriptor,
type InvocationParameterDescriptor,
type TypeRTCodec,
type TypeRTGatewayBinding,
} from '@deepseek-ai/dsh-type-meta'
import type {
InvokeRemoteRequest,
TypertGateway,
TypertGatewayErrorCode,
} from './types.ts'
export type {
InvokeRemoteRequest,
TypertGateway,
TypertGatewayErrorCode,
} from './types.ts'
interface GatewayErrorOptions {
readonly cause?: unknown
readonly field?: string
}
interface ResolvedBinding {
readonly binding: TypeRTGatewayBinding
readonly original: object
}
type ConnectionRpcResult = Awaited<ReturnType<ConnectionRpcHandler>>
type ConnectionRpcError = Extract<ConnectionRpcResult, { readonly ok: false }>['error']
const NEVER_ABORTED_SIGNAL = new AbortController().signal
/** Dispatch failure produced outside the invoked business method. */
export class TypertGatewayError extends Error {
/** Machine-readable failure category. */
readonly code: TypertGatewayErrorCode
/** Canonical `<namespace>/<method>` endpoint. */
readonly endpoint: string
/** Affected wire field when the failure is field-specific. */
readonly field: string | undefined
/**
* Construct a Gateway failure without embedding boundary values in its message.
* @param code - stable failure category.
* @param endpoint - canonical Remote endpoint.
* @param message - correction-oriented diagnostic without sensitive values.
* @param options - optional field and contained cause.
*/
constructor(
code: TypertGatewayErrorCode,
endpoint: string,
message: string,
options: GatewayErrorOptions = {},
) {
super(`typert gateway: ${endpoint}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })
this.name = 'TypertGatewayError'
this.code = code
this.endpoint = endpoint
this.field = options.field
}
}
/**
* Resolve strict generated definitions or conservative SRC markers against
* current Cordis Services and TypeRT providers.
* @typert service typertGateway
*/
export class TypertGatewayService extends Service implements TypertGateway {
static inject = ['typert']
private srcClaims: ReadonlySet<string> | undefined
/**
* Register the Gateway against the active TypeRT registry.
* @param ctx - owning Host Context with TypeRT registry access.
*/
constructor(ctx: Context) {
super(ctx, 'typertGateway')
ctx.on('internal/service', () => {
this.srcClaims = undefined
})
ctx.inject(['connection'], (connectionCtx) => {
connectionCtx.connection.rpc.intercept(
'/api',
endpoint => this.claimsEndpoint(endpoint),
(endpoint, payload, signal) => this.dispatchRpc(endpoint, payload, signal),
{ authority: 'trusted-host' },
)
})
}
private claimsEndpoint(endpoint: string): boolean {
const segments = endpoint.split('/')
if (segments.length !== 2 || segments[0] === '' || segments[1] === '') return false
if (this.ctx.typert.local.get(endpoint) !== undefined || this.ctx.typert.local.hasSeen(endpoint)) return true
this.srcClaims ??= this.collectSrcClaims()
return this.srcClaims.has(endpoint)
}
private collectSrcClaims(): ReadonlySet<string> {
const claims = new Set<string>()
for (const [serviceKey, definition] of Object.entries(this.ctx.reflect.props)) {
if (definition.type !== 'service') continue
const receiver = this.ctx.get(serviceKey) as unknown
if (!isObject(receiver)) continue
const original = originalOf(receiver)
const binding = Reflect.get(original, 'typertGateway') as unknown
if (!isObject(binding) || typeof Reflect.get(binding, 'namespace') !== 'string') continue
const namespace = Reflect.get(binding, 'namespace') as string
for (const candidate of remoteMethods(original)) {
claims.add(endpointOf(namespace, candidate.exportName ?? candidate.method))
}
}
return claims
}
/**
* Invoke one live Remote method through strict generated reflection or SRC markers.
* @param request - decoded endpoint and exact named wire arguments.
* @returns the validated business result.
* @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity.
*/
async invoke(request: InvokeRemoteRequest): Promise<unknown> {
const endpoint = endpointOf(request.namespace, request.method)
const descriptor = this.resolveDescriptor(request.namespace, request.method, endpoint)
assertExactArguments(request.args, descriptor, endpoint)
const receiverContext = this.resolveReceiverContext(descriptor, request.args, endpoint)
const receiver = receiverContext.get(descriptor.service) as unknown
if (!isObject(receiver)) {
throw new TypertGatewayError(
'service-unavailable',
endpoint,
`active Service ${JSON.stringify(descriptor.service)} is unavailable`,
)
}
validateBinding(receiver, descriptor.service, descriptor.namespace, endpoint)
const args = await Promise.all(descriptor.parameters.map(parameter =>
this.resolveParameter(parameter, request.args, endpoint)))
if (descriptor.cancellation !== undefined) args.push(request.signal ?? NEVER_ABORTED_SIGNAL)
const implementation = descriptor.implementation ?? descriptor.method
const method = Reflect.get(receiver, implementation) as unknown
if (typeof method !== 'function') {
throw new TypertGatewayError(
'method-unavailable',
endpoint,
`active Service ${JSON.stringify(descriptor.service)} has no callable method ${JSON.stringify(implementation)}`,
)
}
const result = await Reflect.apply(method, receiver, args) as unknown
return decode(descriptor.result, result, 'result-invalid', endpoint, 'result')
}
private async dispatchRpc(
endpoint: string,
payload: unknown,
signal: AbortSignal,
): Promise<ConnectionRpcResult> {
return this.invokeRpc(endpoint, payload, signal)
}
private async invokeRpc(endpoint: string, payload: unknown, signal: AbortSignal): Promise<ConnectionRpcResult> {
try {
const segments = endpoint.split('/')
if (segments.length !== 2 || segments[0] === '' || segments[1] === '') {
throw new Error(`invalid Remote endpoint ${JSON.stringify(endpoint)}`)
}
const [namespace, method] = segments as [string, string]
if (!isObject(payload)
|| !isPlainObject(payload)
|| Reflect.ownKeys(payload).length !== 1
|| !Object.hasOwn(payload, 'args')
|| !isObject(payload.args)
|| !isPlainObject(payload.args)) {
throw new Error('Remote payload must contain exactly one plain-object args field')
}
const value = await this.invoke({
namespace,
method,
args: payload.args,
signal,
})
return { ok: true, value }
} catch (error) {
return rpcFailure(error)
}
}
private resolveDescriptor(namespace: string, method: string, endpoint: string): InvocationDescriptor {
const strict = this.ctx.typert.local.get(endpoint)
if (strict !== undefined) return strict
if (this.ctx.typert.local.hasSeen(endpoint)) {
throw new TypertGatewayError(
'definition-unavailable',
endpoint,
'its strict definition was withdrawn and SRC fallback is forbidden',
)
}
return this.resolveSrcDescriptor(namespace, method, endpoint)
}
private resolveSrcDescriptor(namespace: string, method: string, endpoint: string): InvocationDescriptor {
const candidates: InvocationDescriptor[] = []
for (const [serviceKey, definition] of Object.entries(this.ctx.reflect.props)) {
if (definition.type !== 'service') continue
const receiver = this.ctx.get(serviceKey) as unknown
if (!isObject(receiver)) continue
const original = originalOf(receiver)
const value = Reflect.get(original, 'typertGateway') as unknown
if (value === undefined) continue
const binding = readBinding(value, original, serviceKey, endpoint)
if (binding.namespace !== namespace) continue
const marker = remoteMethods(original).find(candidate => (candidate.exportName ?? candidate.method) === method)
if (marker === undefined) continue
candidates.push(this.srcDescriptor(binding, marker, method, endpoint))
}
if (candidates.length === 0) {
throw new TypertGatewayError('invocation-unavailable', endpoint, 'no active Remote method exports this endpoint')
}
if (candidates.length > 1) {
throw new TypertGatewayError(
'ambiguous-endpoint',
endpoint,
`multiple active Services export this endpoint: ${candidates.map(candidate => candidate.service).sort().join(', ')}`,
)
}
return candidates[0] as InvocationDescriptor
}
private srcDescriptor(
binding: TypeRTGatewayBinding,
marker: ReturnType<typeof remoteMethods>[number],
method: string,
endpoint: string,
): InvocationDescriptor {
const names = methodParameterNames(binding.service, marker.method, endpoint)
const signalIndex = names.indexOf('signal')
if (signalIndex >= 0 && signalIndex !== names.length - 1) {
throw new TypertGatewayError(
'signature-invalid',
endpoint,
'SRC cancellation parameter signal must be the final parameter',
{ field: 'signal' },
)
}
const cancellation = signalIndex >= 0
? { parameter: 'signal' as const }
: undefined
const businessNames = cancellation === undefined ? names : names.slice(0, -1)
const parameters: InvocationParameterDescriptor[] = []
const wires = new Set<string>()
for (const name of businessNames) {
const matches = this.ctx.typert.lookups.definitions()
.filter(definition => definition.parameter === name)
if (matches.length > 1) {
throw new TypertGatewayError(
'signature-invalid',
endpoint,
`parameter ${JSON.stringify(name)} matches multiple lookup providers`,
{ field: name },
)
}
const match = matches[0]
const parameter: InvocationParameterDescriptor = match === undefined
? { name, wire: name, source: 'json', codec: { mode: 'src-json' } }
: {
name,
wire: match.wire,
source: 'lookup',
lookup: match.key,
codec: { mode: 'src-json' },
}
if (wires.has(parameter.wire)) {
throw new TypertGatewayError(
'signature-invalid',
endpoint,
`multiple parameters use wire field ${JSON.stringify(parameter.wire)}`,
{ field: parameter.wire },
)
}
wires.add(parameter.wire)
parameters.push(parameter)
}
let receiver: InvocationDescriptor['invocation'] = { kind: 'direct' }
if (marker.invocation.kind === 'context') {
const provider = this.ctx.typert.contexts.getHost(marker.invocation.context)
if (provider === undefined) {
throw new TypertGatewayError(
'context-unavailable',
endpoint,
`Context provider ${JSON.stringify(marker.invocation.context)} is unavailable`,
)
}
if (wires.has(provider.wire)) {
throw new TypertGatewayError(
'signature-invalid',
endpoint,
`Context identity conflicts with wire field ${JSON.stringify(provider.wire)}`,
{ field: provider.wire },
)
}
receiver = {
kind: 'context',
context: marker.invocation.context,
wire: provider.wire,
codec: { mode: 'src-json' },
}
}
return {
id: `src:${binding.serviceKey}#${endpoint}`,
service: binding.serviceKey,
namespace: binding.namespace,
method,
...(marker.method === method ? {} : { implementation: marker.method }),
invocation: receiver,
parameters,
...(cancellation === undefined ? {} : { cancellation }),
result: { mode: 'src-json' },
}
}
private resolveReceiverContext(
descriptor: InvocationDescriptor,
args: Readonly<Record<string, unknown>>,
endpoint: string,
): Context {
if (descriptor.invocation.kind === 'direct') return this.ctx
const invocation = descriptor.invocation
const provider = this.ctx.typert.contexts.getHost(invocation.context)
if (provider === undefined) {
throw new TypertGatewayError(
'context-unavailable',
endpoint,
`Context provider ${JSON.stringify(invocation.context)} is unavailable`,
)
}
if (provider.wire !== invocation.wire
|| (invocation.codec.mode === 'strict' && provider.wireTypeSymbol !== invocation.codec.typeSymbol)) {
throw new TypertGatewayError(
'provider-mismatch',
endpoint,
`Context provider ${JSON.stringify(invocation.context)} does not match its strict definition`,
{ field: invocation.wire },
)
}
const identity = decode(invocation.codec, args[invocation.wire], 'input-invalid', endpoint, invocation.wire)
let context: Context | undefined
try {
context = provider.resolve(identity)
} catch (cause) {
throw new TypertGatewayError(
'context-failed',
endpoint,
`Context provider ${JSON.stringify(invocation.context)} failed`,
{ cause, field: invocation.wire },
)
}
if (context === undefined) {
throw new TypertGatewayError(
'context-not-found',
endpoint,
`Context provider ${JSON.stringify(invocation.context)} did not resolve the requested identity`,
{ field: invocation.wire },
)
}
return context
}
private async resolveParameter(
parameter: InvocationParameterDescriptor,
args: Readonly<Record<string, unknown>>,
endpoint: string,
): Promise<unknown> {
const value = decode(parameter.codec, args[parameter.wire], 'input-invalid', endpoint, parameter.wire)
if (parameter.source === 'json') return value
const key = parameter.lookup
/* v8 ignore next -- registry validation rejects strict descriptors without a key, and SRC derivation always supplies one. */
if (key === undefined) {
throw new TypertGatewayError(
'lookup-unavailable',
endpoint,
`lookup parameter ${JSON.stringify(parameter.name)} has no provider key`,
{ field: parameter.wire },
)
}
const provider = this.ctx.typert.lookups.get(key)
if (provider === undefined) {
throw new TypertGatewayError(
'lookup-unavailable',
endpoint,
`lookup provider ${JSON.stringify(key)} is unavailable`,
{ field: parameter.wire },
)
}
if (provider.wire !== parameter.wire
|| (parameter.codec.mode === 'strict' && provider.wireTypeSymbol !== parameter.codec.typeSymbol)) {
throw new TypertGatewayError(
'provider-mismatch',
endpoint,
`lookup provider ${JSON.stringify(key)} does not match its strict definition`,
{ field: parameter.wire },
)
}
let resolved: unknown
try {
resolved = await provider.resolve(value)
} catch (cause) {
if (cause instanceof TypeRTLookupFailure) throw cause
throw new TypertGatewayError(
'lookup-failed',
endpoint,
`lookup provider ${JSON.stringify(key)} failed`,
{ cause, field: parameter.wire },
)
}
if (resolved === undefined) {
throw new TypertGatewayError(
'lookup-not-found',
endpoint,
`lookup provider ${JSON.stringify(key)} did not resolve the requested identity`,
{ field: parameter.wire },
)
}
return resolved
}
}
function rpcFailure(error: unknown): ConnectionRpcResult {
if (error instanceof TypeRTLookupFailure) {
return { ok: false, error: error.failure as ConnectionRpcError }
}
return {
ok: false,
error: {
code: 'internal',
message: error instanceof Error ? error.message : String(error),
details: {},
},
}
}
function endpointOf(namespace: string, method: string): string {
return `${namespace}/${method}`
}
function validateBinding(
receiver: object,
serviceKey: string,
namespace: string,
endpoint: string,
): ResolvedBinding {
const original = originalOf(receiver)
const value = Reflect.get(original, 'typertGateway') as unknown
if (value === undefined) {
throw new TypertGatewayError(
'binding-invalid',
endpoint,
`Service ${JSON.stringify(serviceKey)} has no visible typertGateway binding`,
)
}
return {
binding: readBinding(value, original, serviceKey, endpoint, namespace),
original,
}
}
function readBinding(
value: unknown,
original: object,
serviceKey: string,
endpoint: string,
namespace?: string,
): TypeRTGatewayBinding {
if (!isObject(value)
|| Reflect.get(value, 'service') !== original
|| Reflect.get(value, 'serviceKey') !== serviceKey
|| typeof Reflect.get(value, 'namespace') !== 'string'
|| (namespace !== undefined && Reflect.get(value, 'namespace') !== namespace)) {
throw new TypertGatewayError(
'binding-invalid',
endpoint,
`Service ${JSON.stringify(serviceKey)} has an inconsistent typertGateway binding`,
)
}
return value as unknown as TypeRTGatewayBinding
}
function originalOf(receiver: object): object {
const original = Reflect.get(receiver, symbols.original) as unknown
return isObject(original) ? original : receiver
}
function methodParameterNames(service: object, method: string, endpoint: string): readonly string[] {
let prototype: object | null = Object.getPrototypeOf(service) as object | null
let implementation: ((this: object, ...args: never[]) => unknown) | undefined
while (prototype !== null) {
const descriptor = Object.getOwnPropertyDescriptor(prototype, method)
if (descriptor !== undefined) {
if ('value' in descriptor && typeof descriptor.value === 'function') {
implementation = descriptor.value as (this: object, ...args: never[]) => unknown
}
break
}
prototype = Object.getPrototypeOf(prototype) as object | null
}
if (implementation === undefined) {
throw new TypertGatewayError(
'method-unavailable',
endpoint,
`Remote marker has no prototype method ${JSON.stringify(method)}`,
)
}
const source = Function.prototype.toString.call(implementation)
const open = source.indexOf('(')
const close = source.indexOf(')', open + 1)
/* v8 ignore next -- standard public class-method syntax always contains a parenthesized parameter list. */
if (open < 0 || close < 0) return invalidSignature(endpoint, method)
const body = source.slice(open + 1, close).trim()
if (body.length === 0) return []
const parts = body.split(',').map(part => part.trim())
const names = new Set<string>()
for (const part of parts) {
if (!/^[$A-Z_a-z][$\w]*$/u.test(part) || names.has(part)) return invalidSignature(endpoint, method)
names.add(part)
}
return [...names]
}
function invalidSignature(endpoint: string, method: string): never {
throw new TypertGatewayError(
'signature-invalid',
endpoint,
`SRC method ${JSON.stringify(method)} must use unique identifier parameters without destructuring, defaults, or rest`,
)
}
function assertExactArguments(
args: Readonly<Record<string, unknown>>,
descriptor: InvocationDescriptor,
endpoint: string,
): void {
if (!isPlainObject(args)) {
throw new TypertGatewayError('arguments-invalid', endpoint, 'args must be a plain object')
}
const expected = new Set(descriptor.parameters.map(parameter => parameter.wire))
if (descriptor.invocation.kind === 'context') expected.add(descriptor.invocation.wire)
const actual = Reflect.ownKeys(args)
const extra = actual.filter(key => typeof key !== 'string' || !expected.has(key))
const missing = [...expected].filter(key => !Object.hasOwn(args, key))
if (extra.length === 0 && missing.length === 0) return
const clauses: string[] = []
if (missing.length > 0) clauses.push(`missing ${missing.map(key => JSON.stringify(key)).join(', ')}`)
if (extra.length > 0) clauses.push(`unexpected ${extra.map(key => JSON.stringify(String(key))).join(', ')}`)
throw new TypertGatewayError('arguments-invalid', endpoint, `args fields do not match the descriptor: ${clauses.join('; ')}`)
}
function decode(
codec: TypeRTCodec,
value: unknown,
code: 'input-invalid' | 'result-invalid',
endpoint: string,
field: string,
): unknown {
try {
if (codec.mode === 'strict') value = codec.schema.parse(value)
assertJsonValue(value, new Set())
return value
} catch (cause) {
throw new TypertGatewayError(
code,
endpoint,
code === 'input-invalid'
? `wire field ${JSON.stringify(field)} failed boundary validation`
: 'business result failed boundary validation',
{ cause, field },
)
}
}
function assertJsonValue(value: unknown, ancestors: Set<object>): void {
if (value === null || typeof value === 'string' || typeof value === 'boolean') return
if (typeof value === 'number') {
if (Number.isFinite(value)) return
throw new TypeError('non-finite number is not JSON-safe')
}
if (!isObject(value)) throw new TypeError(`${typeof value} is not JSON-safe`)
if (ancestors.has(value)) throw new TypeError('cyclic value is not JSON-safe')
ancestors.add(value)
try {
if (Array.isArray(value)) {
if (Object.getOwnPropertySymbols(value).length > 0 || Object.keys(value).length !== value.length) {
throw new TypeError('sparse or decorated array is not JSON-safe')
}
for (let index = 0; index < value.length; index += 1) {
if (!Object.hasOwn(value, index)) throw new TypeError('sparse array is not JSON-safe')
assertJsonValue(value[index], ancestors)
}
return
}
if (!isPlainObject(value)) throw new TypeError('non-plain object is not JSON-safe')
if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError('symbol property is not JSON-safe')
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key)
/* v8 ignore next -- ownKeys() just returned this key; only a hostile same-process Proxy can delete it between operations. */
if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor)) {
throw new TypeError('non-data property is not JSON-safe')
}
assertJsonValue(descriptor.value, ancestors)
}
} finally {
ancestors.delete(value)
}
}
function isPlainObject(value: object): value is Record<string, unknown> {
if (Array.isArray(value)) return false
const prototype = Object.getPrototypeOf(value) as object | null
return prototype === null || prototype === Object.prototype
}
function isObject(value: unknown): value is object {
return (typeof value === 'object' && value !== null) || typeof value === 'function'
}
export default TypertGatewayService
+30
View File
@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-api-gateway`.
* @module @deepseek-ai/dsh-api-gateway/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-api-gateway'
/** Cordis companion plugin name. */
export const name = 'api-gateway-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: Host calls re-read authoritative Cordis and TypeRT
* state, while Client methods and descriptors mutate in one owned effect.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
+54
View File
@@ -0,0 +1,54 @@
/**
* Carrier-independent TypeRT Gateway request, service, and error contracts.
* @module @deepseek-ai/dsh-api-gateway/types
*/
/** One Remote method request after a carrier has decoded its envelope. */
export interface InvokeRemoteRequest {
/** Remote namespace selected by the generated descriptor. */
readonly namespace: string
/** Exported Service method name. */
readonly method: string
/** Named wire values; fields must exactly match the descriptor. */
readonly args: Readonly<Record<string, unknown>>
/** Carrier or direct-caller cancellation injected only into cancellation-aware methods. */
readonly signal?: AbortSignal
}
/** Stable infrastructure and boundary failures emitted before or after business execution. */
export type TypertGatewayErrorCode =
| 'ambiguous-endpoint'
| 'arguments-invalid'
| 'binding-invalid'
| 'context-failed'
| 'context-not-found'
| 'context-unavailable'
| 'definition-unavailable'
| 'input-invalid'
| 'invocation-unavailable'
| 'lookup-failed'
| 'lookup-not-found'
| 'lookup-unavailable'
| 'method-unavailable'
| 'provider-mismatch'
| 'result-invalid'
| 'service-unavailable'
| 'signature-invalid'
/** Host dispatcher consumed by Connection adapters. */
export interface TypertGateway {
/**
* Invoke one live Remote method without assuming a carrier or response envelope.
* @param request - decoded endpoint and named wire arguments.
* @returns the validated business result.
* @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity.
*/
invoke(request: InvokeRemoteRequest): Promise<unknown>
}
declare module 'cordis' {
interface Context {
/** Host dispatcher for TypeRT Remote calls. */
typertGateway: TypertGateway
}
}