mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(typert): bind remote services through base class
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
|
||||
2026-08-02-typert-remote-method-calls.md: 552e910b403312c7c7a1cec3a14c0dc1f9cc4380
|
||||
2026-08-02-typert-remote-method-calls.zh.md: 18b8c1687d2c01aa23bb7cb9402fccf85fec333d
|
||||
2026-08-02-typert-remote-method-calls.md: ade8eb827ae765677be8dcdb0ffec965c67bc4ab
|
||||
2026-08-02-typert-remote-method-calls.zh.md: 2de887a2a0e46148fbb2b5ac52cfd7e3b2305b8d
|
||||
|
||||
@@ -16,7 +16,7 @@ The Host and Browser Client use separate TypeScript Programs because each side a
|
||||
|
||||
## Decision
|
||||
|
||||
A business Service declares callable methods with `@Remote` or `@RemoteContext()` and explicitly joins the Gateway through `bindTypeRTGateway()`. TypeRT generates the Host-local reflection artifact and a platform-independent Remote consumer projection from the Host Program. The Client Program continues to generate its own local reflection artifact independently.
|
||||
A business Service extends `GatewayService` and declares callable methods with `@Remote` or `@RemoteContext()`. A Service that already has another base class may instead expose the same binding through `bindTypeRTGateway()`. TypeRT generates the Host-local reflection artifact and a platform-independent Remote consumer projection from the Host Program. The Client Program continues to generate its own local reflection artifact independently.
|
||||
|
||||
The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. The `.d.ts` exposes only methods marked with a Remote decorator and refers to the business package's single public type symbols. The `.d.ts.map` navigates consumer API methods back to their Host business method implementations. The `.js` carries endpoint, parameter, Context, and Zod information for the same contract. At the assembly layer, the Browser Client mounts the required Remote JS contributions onto the Client API Service. The projection and API abstraction remain platform-independent so that a future TUI can reuse them.
|
||||
|
||||
@@ -26,7 +26,7 @@ The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. T
|
||||
|
||||
| Component | Cordis service | Responsibility |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-type-meta` | Declares only the minimal `ctx.typert` protocol | Decorators, bindings, descriptors, lookup/Context, and the Remote map; no dependency on the compiler, Zod, Connection, or Browser |
|
||||
| `@deepseek-ai/dsh-type-meta` | Declares only the minimal `ctx.typert` protocol | `GatewayService`, decorators, binding fallback, descriptors, lookup/Context, and the Remote map; no dependency on the compiler, Zod, Connection, or Browser |
|
||||
| TypeRT registry | `ctx.typert` | Separately stores reflection for the current environment, imported Remote contributions, lookup providers, and Context providers |
|
||||
| TypeRT generator/loader | No new business service | Generates three kinds of `lib` artifacts from the Host/Client Programs and registers the current environment's artifacts with `ctx.typert` |
|
||||
| Host API Gateway's Host face | `ctx.typertGateway` | Associates Host definitions with live Services, decodes parameters, resolves receivers, invokes methods, and encodes results |
|
||||
@@ -43,8 +43,10 @@ The Host Gateway does not depend on concrete implementations of `ctx.agents`, `c
|
||||
Ordinary direct calls use `@Remote`. When migrating to an existing Service or Registry, do not rename or alter existing methods. Add `remoteExport*` entry points at the end of the class and use decorator arguments to declare their short API names. A method explicitly declares every required business object in a top-level parameter position:
|
||||
|
||||
```text
|
||||
export class GoalService extends Service {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
export class GoalService extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
create(agent: Agent, request: CreateGoalRequest): CreateGoalResult {
|
||||
// Existing business method remains unchanged.
|
||||
@@ -57,13 +59,15 @@ export class GoalService extends Service {
|
||||
}
|
||||
```
|
||||
|
||||
`goals` is an explicit Cordis service key and is the default wire namespace. Override it through an option to `bindTypeRTGateway()` only when the protocol namespace genuinely needs to differ from the service key.
|
||||
`goals` is the explicit Cordis service key passed to `super()` and is the default wire namespace. Pass a `namespace` option as the third argument only when the protocol namespace genuinely needs to differ from the service key.
|
||||
|
||||
Use `@RemoteContext()` when the Service receiver must be resolved within an isolated kind of Context. Context identity does not enter the business method's parameters:
|
||||
|
||||
```text
|
||||
export class ScopedGoalService extends Service {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
export class ScopedGoalService extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
@RemoteContext('agent', 'create')
|
||||
remoteExportCreate(request: CreateGoalRequest): Promise<CreateGoalResult> {
|
||||
@@ -74,17 +78,17 @@ export class ScopedGoalService extends Service {
|
||||
|
||||
An endpoint selects exactly one invocation mode. A flow that needs an explicit `Agent` parameter uses `@Remote`. A flow that first switches to an Agent Context and then resolves a scoped receiver uses `@RemoteContext('agent')`. TypeRT does not infer either mode from the method body or from a missing parameter.
|
||||
|
||||
Business packages depend only on the lightweight `@deepseek-ai/dsh-type-meta`. It provides declaration protocols for decorators, `bindTypeRTGateway()`, lookup, Remote Context, and descriptors, without depending on the TypeScript compiler, Zod, HTTP, or the Client runtime.
|
||||
Business packages depend only on the lightweight `@deepseek-ai/dsh-type-meta`. It provides `GatewayService` and declaration protocols for decorators, the binding fallback, lookup, Remote Context, and descriptors, without depending on the TypeScript compiler, Zod, HTTP, or the Client runtime.
|
||||
|
||||
A method that cooperatively supports cancellation declares `signal: AbortSignal` as its final Host parameter. This reserved parameter is not a business value, lookup, or JSON field. The generated consumer method exposes it as a final optional parameter so ordinary calls remain unchanged while callers that own cancellation can pass a signal.
|
||||
|
||||
## Decorators and the explicit Gateway facet
|
||||
|
||||
A decorator only states that a method participates in the Remote contract. It performs no runtime type reflection and injects no hidden symbol into a Service constructor. The arguments to `@Remote('create')` and `@RemoteContext('agent', 'create')` are external method names, while the actual member remains named `remoteExportCreate`. The member name becomes the external method name only when no alias is provided. `typertGateway` is the sole explicit marker that a Service has joined the Gateway, making this capability visible on both the business class and its runtime instance.
|
||||
A decorator only states that a method participates in the Remote contract. It performs no runtime type reflection and injects no hidden symbol into a Service constructor. The arguments to `@Remote('create')` and `@RemoteContext('agent', 'create')` are external method names, while the actual member remains named `remoteExportCreate`. The member name becomes the external method name only when no alias is provided. Inheriting `GatewayService` is the normal explicit declaration that a Service has joined the Gateway; its public readonly `typertGateway` field keeps the binding visible on the runtime instance.
|
||||
|
||||
In SRC mode, the decorator may record the prototype, method name, and invocation mode in a `WeakMap` internal to `dsh-type-meta`. It writes no custom properties to a Service instance, prototype, constructor, or method function.
|
||||
|
||||
In LIB mode, the TypeRT compiler performs strict method discovery, type resolution, and descriptor generation. Generation neither rewrites business source nor secretly supplies generated arguments to `bindTypeRTGateway()`.
|
||||
In LIB mode, the TypeRT compiler performs strict method discovery, type resolution, and descriptor generation. It accepts a literal service key in `GatewayService`'s direct `super()` call or the explicit binding fallback; generation neither rewrites business source nor injects hidden registration metadata.
|
||||
|
||||
## Lookup and Remote Context registration
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Host 与 Browser Client 使用独立的 TypeScript Program,因为两边会以
|
||||
|
||||
## 决策
|
||||
|
||||
业务 Service 通过 `@Remote` 或 `@RemoteContext()` 声明可调用方法,并通过 `bindTypeRTGateway()` 显式加入 Gateway。TypeRT 从 Host Program 生成 Host 本地反射产物和平台无关的 Remote 消费端投影;Client Program 继续独立生成自己的本地反射产物。
|
||||
业务 Service 继承 `GatewayService`,并通过 `@Remote` 或 `@RemoteContext()` 声明可调用方法;已有其他基类的 Service 可以改用 `bindTypeRTGateway()` 暴露同一绑定。TypeRT 从 Host Program 生成 Host 本地反射产物和平台无关的 Remote 消费端投影;Client Program 继续独立生成自己的本地反射产物。
|
||||
|
||||
Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只暴露被 Remote decorator 标记的方法,并引用业务包唯一的公共类型符号;`.d.ts.map` 把消费端 API 方法导航回 Host 业务方法实现;`.js` 携带同一契约的 endpoint、参数、Context 和 Zod 信息。Browser Client 在 assembly 层把需要的 Remote JS 贡献集中挂到 Client API Service;该投影和 API 抽象保持平台无关,以便未来 TUI 复用。
|
||||
|
||||
@@ -26,7 +26,7 @@ Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只
|
||||
|
||||
| 组件 | Cordis 服务 | 职责 |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-type-meta` | 只声明 `ctx.typert` 的最小协议 | decorator、binding、descriptor、lookup/Context 和 Remote map;不依赖 compiler、Zod、Connection 或 Browser |
|
||||
| `@deepseek-ai/dsh-type-meta` | 只声明 `ctx.typert` 的最小协议 | `GatewayService`、decorator、binding 回退、descriptor、lookup/Context 和 Remote map;不依赖 compiler、Zod、Connection 或 Browser |
|
||||
| TypeRT registry | `ctx.typert` | 分开保存当前环境 reflection、导入的 Remote contribution、lookup provider 和 Context provider |
|
||||
| TypeRT generator/loader | 无新增业务服务 | 从 Host/Client Program 生成三类 `lib` 产物,并把当前环境产物注册到 `ctx.typert` |
|
||||
| Host API Gateway 的 Host face | `ctx.typertGateway` | 关联 Host definition 与活 Service,解码参数、解析 receiver、调用方法和编码结果 |
|
||||
@@ -43,8 +43,10 @@ Host Gateway 不依赖 `ctx.agents`、`ctx.sessions`、`ctx.goals` 或 `ctx.http
|
||||
普通直接调用使用 `@Remote`。迁移到现存 Service 或 Registry 时不重命名、不改变存量方法;类末尾新增 `remoteExport*` 出口,并由 decorator 参数声明短 API 名。方法需要哪个业务对象,就在顶层参数位置显式声明该对象:
|
||||
|
||||
```text
|
||||
export class GoalService extends Service {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
export class GoalService extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
create(agent: Agent, request: CreateGoalRequest): CreateGoalResult {
|
||||
// Existing business method remains unchanged.
|
||||
@@ -57,13 +59,15 @@ export class GoalService extends Service {
|
||||
}
|
||||
```
|
||||
|
||||
`goals` 是明确的 Cordis service key,并默认作为 wire namespace。只有协议 namespace 确实需要与 service key 不同时,才通过 `bindTypeRTGateway()` 的选项覆盖。
|
||||
`goals` 是传给 `super()` 的明确 Cordis service key,并默认作为 wire namespace。只有协议 namespace 确实需要与 service key 不同时,才通过第三个参数传入 `namespace` 选项。
|
||||
|
||||
需要在某类隔离 Context 中查找 Service receiver 时使用 `@RemoteContext()`。Context identity 不进入业务方法参数:
|
||||
|
||||
```text
|
||||
export class ScopedGoalService extends Service {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
export class ScopedGoalService extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
@RemoteContext('agent', 'create')
|
||||
remoteExportCreate(request: CreateGoalRequest): Promise<CreateGoalResult> {
|
||||
@@ -74,17 +78,17 @@ export class ScopedGoalService extends Service {
|
||||
|
||||
同一个 endpoint 只能选择一种调用模式。需要显式 `Agent` 参数的流程使用 `@Remote`;需要切换到 Agent Context 再解析 scoped receiver 的流程使用 `@RemoteContext('agent')`,两者不会由 TypeRT 根据方法体或参数缺失自动猜测。
|
||||
|
||||
业务包只依赖轻量的 `@deepseek-ai/dsh-type-meta`。它提供 decorator、`bindTypeRTGateway()`、lookup、Remote Context 和 descriptor 的声明协议,不依赖 TypeScript compiler、Zod、HTTP 或 Client runtime。
|
||||
业务包只依赖轻量的 `@deepseek-ai/dsh-type-meta`。它提供 `GatewayService`,以及 decorator、binding 回退、lookup、Remote Context 和 descriptor 的声明协议,不依赖 TypeScript compiler、Zod、HTTP 或 Client runtime。
|
||||
|
||||
支持协作式取消的方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。这个保留参数不是业务值、lookup 或 JSON 字段。生成的消费方方法将其暴露为最后一个可选参数,因此普通调用保持不变,而拥有取消控制权的调用方可以传入 signal。
|
||||
|
||||
## Decorator 与显式 Gateway facet
|
||||
|
||||
Decorator 只表达“该方法参与 Remote 契约”,不负责运行时类型反射,也不向 Service constructor 注入隐藏 symbol。`@Remote('create')` 和 `@RemoteContext('agent', 'create')` 的参数是外部方法名,实际成员名保持 `remoteExportCreate`;未给别名时才使用成员名作为外部方法名。`typertGateway` 是 Service 加入 Gateway 的唯一显式标志,使业务类和运行时实例都能直接看出这项能力。
|
||||
Decorator 只表达“该方法参与 Remote 契约”,不负责运行时类型反射,也不向 Service constructor 注入隐藏 symbol。`@Remote('create')` 和 `@RemoteContext('agent', 'create')` 的参数是外部方法名,实际成员名保持 `remoteExportCreate`;未给别名时才使用成员名作为外部方法名。继承 `GatewayService` 是 Service 加入 Gateway 的常规显式声明;其 public readonly `typertGateway` 字段使运行时实例上的绑定保持可见。
|
||||
|
||||
SRC 运行时允许 decorator 在 `dsh-type-meta` 内部的 `WeakMap` 记录 prototype、方法名和调用模式。它不向 Service 实例、prototype、constructor 或方法函数写入自定义属性。
|
||||
|
||||
LIB 的严格方法发现、类型解析和 descriptor 生成由 TypeRT compiler 完成。生成过程不改写业务源码,也不向 `bindTypeRTGateway()` 偷注生成参数。
|
||||
LIB 的严格方法发现、类型解析和 descriptor 生成由 TypeRT compiler 完成。它接受 `GatewayService` 直接 `super()` 调用中的字面量 service key,或显式 binding 回退;生成过程不改写业务源码,也不注入隐藏注册元数据。
|
||||
|
||||
## Lookup 与 Remote Context 注册
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { Context, Service } from 'cordis'
|
||||
import { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { z as zod } from 'zod'
|
||||
import type { ZodType } from 'zod'
|
||||
import { agentEvents } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { Remote, bindTypeRTGateway } from '@deepseek-ai/dsh-type-meta'
|
||||
import { GatewayService, Remote } from '@deepseek-ai/dsh-type-meta'
|
||||
// Type-only: resolves ctx.sessionProjections for the optional unit child.
|
||||
import type {} from '@deepseek-ai/dsh-session-projection'
|
||||
import {
|
||||
@@ -180,7 +180,7 @@ function resolveBlockReason(reason: unknown): GoalBlockReason {
|
||||
}
|
||||
|
||||
/** Goal service (`ctx.goals`) backed exclusively by the owning session log. */
|
||||
export class GoalService extends Service {
|
||||
export class GoalService extends GatewayService {
|
||||
static inject = ['agents']
|
||||
|
||||
static Config: z<Config> = z.object({
|
||||
@@ -190,9 +190,6 @@ export class GoalService extends Service {
|
||||
private readonly resolved: ResolvedConfig
|
||||
private readonly caches = new WeakMap<Session, GoalCache>()
|
||||
|
||||
/** Explicit participation in the TypeRT Gateway under the Cordis service key. */
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
|
||||
constructor(ctx: Context, config: Config = {}) {
|
||||
super(ctx, 'goals')
|
||||
this.resolved = {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/host/api-gateway/README.md
|
||||
README.md: 9cb6e7e1c0a23789ab4ab2c999b5a6c2d4cd32f9
|
||||
README.zh.md: 609580ceb77649ba8df6103093a72092c9ccc8a1
|
||||
README.md: 43e8f464e2a2790d05628a7fba61143a6a5ab26a
|
||||
README.zh.md: 761045d0c1afc17dfc230f9f45849c46e4e579fc
|
||||
|
||||
@@ -6,7 +6,7 @@ Two-sided Remote control for Host and Client Cordis environments. The Host entry
|
||||
|
||||
## Host service: `TypertGatewayService` (ctx key: `typertGateway`)
|
||||
|
||||
`ctx.typertGateway.invoke()` resolves the current descriptor and Cordis Service for each call, validates exact named arguments, resolves registered object or Context identities, invokes the public business method, and validates its result. Business Services declare participation with `bindTypeRTGateway()` and `@Remote` or `@RemoteContext` from [`dsh-type-meta`](../../typert/type-meta/README.md).
|
||||
`ctx.typertGateway.invoke()` resolves the current descriptor and Cordis Service for each call, validates exact named arguments, resolves registered object or Context identities, invokes the public business method, and validates its result. Business Services extend `GatewayService` and mark methods with `@Remote` or `@RemoteContext` from [`dsh-type-meta`](../../typert/type-meta/README.md); `bindTypeRTGateway()` remains available when another base class owns inheritance.
|
||||
|
||||
Strict mode reads generated invocation descriptors from `ctx.typert.local`. Lookup parameters use registered `ctx.typert.lookups` providers, while `@RemoteContext` resolves its receiver through a registered Host Context provider. SRC mode is a development fallback for endpoints that have never had a strict definition; it parses simple parameter names and accepts only JSON-safe values for non-lookup parameters. Withdrawing an observed strict definition fails instead of weakening validation.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## Host 服务:`TypertGatewayService`(ctx key:`typertGateway`)
|
||||
|
||||
每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务调用 `bindTypeRTGateway()` 并使用 [`dsh-type-meta`](../../typert/type-meta/README.md) 提供的 `@Remote` 或 `@RemoteContext` 装饰器,以显式声明接入。
|
||||
每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务继承 [`dsh-type-meta`](../../typert/type-meta/README.md) 的 `GatewayService`,并用 `@Remote` 或 `@RemoteContext` 标记方法;已有其他基类时仍可改用 `bindTypeRTGateway()`。
|
||||
|
||||
严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用已向 `ctx.typert.lookups` 注册的提供方,`@RemoteContext` 则通过已注册的 Host Context 提供方解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ interface StaticContextDeclaration {
|
||||
interface GatewayBinding {
|
||||
readonly service: string
|
||||
readonly namespace: string
|
||||
readonly site: ts.PropertyDeclaration
|
||||
readonly site: ts.Node
|
||||
}
|
||||
|
||||
type ReferenceSite = ts.TypeReferenceNode | ts.ExpressionWithTypeArguments | ts.ImportTypeNode
|
||||
@@ -927,7 +927,10 @@ class FaceAnalyzer {
|
||||
if (first === undefined) continue
|
||||
const binding = this.gatewayBinding(statement)
|
||||
if (binding === undefined) {
|
||||
this.fail(first.method, 'Remote methods require readonly typertGateway = bindTypeRTGateway(this, serviceKey)')
|
||||
this.fail(
|
||||
first.method,
|
||||
'Remote methods require GatewayService or readonly typertGateway = bindTypeRTGateway(this, serviceKey)',
|
||||
)
|
||||
}
|
||||
for (const { method, invocation } of marked) {
|
||||
result.push(this.invocationModel(registration, binding, method, invocation))
|
||||
@@ -1089,6 +1092,15 @@ class FaceAnalyzer {
|
||||
}
|
||||
|
||||
private gatewayBinding(declaration: ts.ClassDeclaration): GatewayBinding | undefined {
|
||||
const field = this.gatewayFieldBinding(declaration)
|
||||
const base = this.gatewayServiceBinding(declaration)
|
||||
if (field !== undefined && base !== undefined) {
|
||||
this.fail(field.site, 'GatewayService subclasses must not declare a second typertGateway binding')
|
||||
}
|
||||
return field ?? base
|
||||
}
|
||||
|
||||
private gatewayFieldBinding(declaration: ts.ClassDeclaration): GatewayBinding | undefined {
|
||||
const candidates = declaration.members.filter((member): member is ts.PropertyDeclaration =>
|
||||
ts.isPropertyDeclaration(member) && memberName(member.name) === 'typertGateway')
|
||||
const [property, duplicate] = candidates
|
||||
@@ -1111,10 +1123,38 @@ class FaceAnalyzer {
|
||||
if (call.arguments[0]?.kind !== ts.SyntaxKind.ThisKeyword) {
|
||||
this.fail(call.arguments[0] ?? call, 'bindTypeRTGateway() first argument must be this')
|
||||
}
|
||||
return this.gatewayBindingArguments(call, property)
|
||||
}
|
||||
|
||||
private gatewayServiceBinding(declaration: ts.ClassDeclaration): GatewayBinding | undefined {
|
||||
const heritage = (declaration.heritageClauses ?? [])
|
||||
.filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword)
|
||||
.flatMap(clause => [...clause.types])
|
||||
.find(type => this.isTypeMetaSymbol(type.expression, 'GatewayService'))
|
||||
if (heritage === undefined) return undefined
|
||||
|
||||
const constructor = declaration.members.find(ts.isConstructorDeclaration)
|
||||
if (constructor?.body === undefined) {
|
||||
this.fail(heritage, 'GatewayService subclasses must declare a constructor with super(ctx, serviceKey)')
|
||||
}
|
||||
const call = constructor.body.statements.flatMap((statement) => {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression)) return []
|
||||
return statement.expression.expression.kind === ts.SyntaxKind.SuperKeyword ? [statement.expression] : []
|
||||
})[0]
|
||||
if (call === undefined) {
|
||||
this.fail(constructor, 'GatewayService constructor must call super(ctx, serviceKey) directly')
|
||||
}
|
||||
if (call.arguments.length < 2 || call.arguments.length > 3) {
|
||||
this.fail(call, 'GatewayService super() requires context, service key, and an optional options object')
|
||||
}
|
||||
return this.gatewayBindingArguments(call, heritage)
|
||||
}
|
||||
|
||||
private gatewayBindingArguments(call: ts.CallExpression, site: ts.Node): GatewayBinding {
|
||||
const serviceArgument = call.arguments[1]
|
||||
if (serviceArgument === undefined) this.fail(call, 'bindTypeRTGateway() service key must be a string literal')
|
||||
if (serviceArgument === undefined) this.fail(call, 'Gateway service key must be a string literal')
|
||||
const service = stringLiteralValue(serviceArgument)
|
||||
if (service === undefined) this.fail(serviceArgument, 'bindTypeRTGateway() service key must be a string literal')
|
||||
if (service === undefined) this.fail(serviceArgument, 'Gateway service key must be a string literal')
|
||||
let namespace = service
|
||||
const options = call.arguments[2]
|
||||
if (options !== undefined) {
|
||||
@@ -1133,7 +1173,7 @@ class FaceAnalyzer {
|
||||
}
|
||||
if (!isRemoteSegment(service)) this.fail(serviceArgument, 'Gateway service key must be nonempty and must not contain "/"')
|
||||
if (!isRemoteSegment(namespace)) this.fail(options ?? call, 'Gateway namespace must be nonempty and must not contain "/"')
|
||||
return { service, namespace, site: property }
|
||||
return { service, namespace, site }
|
||||
}
|
||||
|
||||
private remoteMarker(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Remote, RemoteContext, bindTypeRTGateway } from '@deepseek-ai/dsh-type-meta'
|
||||
import { GatewayService, Remote, RemoteContext } from '@deepseek-ai/dsh-type-meta'
|
||||
import type { Agent } from '@fixture/domain'
|
||||
import type {
|
||||
CreateGoalRequest,
|
||||
@@ -8,8 +8,10 @@ import type {
|
||||
} from './types.ts'
|
||||
|
||||
/** Remote-only business Service with no Cordis declaration merge. */
|
||||
export class GoalService {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
export class GoalService extends GatewayService {
|
||||
constructor() {
|
||||
super(undefined, 'goals')
|
||||
}
|
||||
|
||||
@Remote
|
||||
async create(agent: Agent, request: CreateGoalRequest, signal: AbortSignal): Promise<CreateGoalResult> {
|
||||
|
||||
@@ -26,6 +26,19 @@ declare module '@deepseek-ai/dsh-type-meta' {
|
||||
readonly descriptors: readonly unknown[]
|
||||
}
|
||||
|
||||
export abstract class GatewayService {
|
||||
readonly typertGateway: {
|
||||
readonly service: GatewayService
|
||||
readonly serviceKey: string
|
||||
readonly namespace: string
|
||||
}
|
||||
protected constructor(
|
||||
ctx: unknown,
|
||||
serviceKey: string,
|
||||
options?: { readonly namespace?: string },
|
||||
)
|
||||
}
|
||||
|
||||
export function bindTypeRTGateway<Service extends object>(
|
||||
service: Service,
|
||||
serviceKey: string,
|
||||
|
||||
@@ -219,8 +219,56 @@ export type GenericResult = {
|
||||
it.each([
|
||||
{
|
||||
name: 'missing binding',
|
||||
edit: (source: string) => source.replace(" readonly typertGateway = bindTypeRTGateway(this, 'goals')\n\n", ''),
|
||||
message: 'Remote methods require readonly typertGateway',
|
||||
edit: (source: string) => source.replace(
|
||||
"export class GoalService extends GatewayService {\n constructor() {\n super(undefined, 'goals')\n }",
|
||||
'export class GoalService {',
|
||||
),
|
||||
message: 'Remote methods require GatewayService',
|
||||
},
|
||||
{
|
||||
name: 'dynamic GatewayService key',
|
||||
edit: (source: string) => source.replace(
|
||||
" constructor() {\n super(undefined, 'goals')\n }",
|
||||
' constructor(serviceKey: string) {\n super(undefined, serviceKey)\n }',
|
||||
),
|
||||
message: 'Gateway service key must be a string literal',
|
||||
},
|
||||
{
|
||||
name: 'GatewayService without a constructor',
|
||||
edit: (source: string) => source.replace(
|
||||
" constructor() {\n super(undefined, 'goals')\n }\n\n",
|
||||
'',
|
||||
),
|
||||
message: 'GatewayService subclasses must declare a constructor',
|
||||
},
|
||||
{
|
||||
name: 'GatewayService without a direct super call',
|
||||
edit: (source: string) => source.replace(
|
||||
" super(undefined, 'goals')",
|
||||
' void undefined',
|
||||
),
|
||||
message: 'GatewayService constructor must call super',
|
||||
},
|
||||
{
|
||||
name: 'GatewayService super call without a service key',
|
||||
edit: (source: string) => source.replace(
|
||||
" super(undefined, 'goals')",
|
||||
' super(undefined)',
|
||||
),
|
||||
message: 'GatewayService super\\(\\) requires context, service key',
|
||||
},
|
||||
{
|
||||
name: 'duplicate GatewayService field binding',
|
||||
edit: (source: string) => source
|
||||
.replace(
|
||||
'import { GatewayService, Remote, RemoteContext }',
|
||||
'import { GatewayService, Remote, RemoteContext, bindTypeRTGateway }',
|
||||
)
|
||||
.replace(
|
||||
'export class GoalService extends GatewayService {',
|
||||
"export class GoalService extends GatewayService {\n readonly typertGateway = bindTypeRTGateway(this, 'goals')",
|
||||
),
|
||||
message: 'GatewayService subclasses must not declare a second typertGateway binding',
|
||||
},
|
||||
{
|
||||
name: 'private method',
|
||||
@@ -351,8 +399,10 @@ export type GenericResult = {
|
||||
it('rejects duplicate endpoints across Remote services', () => {
|
||||
const root = copyFixture()
|
||||
editFile(root, 'packages/remote/src/index.ts', source => `${source}
|
||||
export class DuplicateGoalService {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'duplicate', { namespace: 'goals' })
|
||||
export class DuplicateGoalService extends GatewayService {
|
||||
constructor() {
|
||||
super(undefined, 'duplicate', { namespace: 'goals' })
|
||||
}
|
||||
|
||||
@Remote
|
||||
create(request: CreateGoalRequest): CreateGoalResult {
|
||||
@@ -521,7 +571,7 @@ ctx.api.goals.create('agent-1', { title: 'must not compile' })
|
||||
if (config.error !== undefined) throw new Error(formatDiagnostics([config.error]))
|
||||
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, consumerRoot, undefined, configPath)
|
||||
const diagnostics = ts.getPreEmitDiagnostics(ts.createProgram(parsed.fileNames, parsed.options))
|
||||
expect(diagnostics).toHaveLength(1)
|
||||
expect(diagnostics, formatDiagnostics(diagnostics)).toHaveLength(1)
|
||||
expect(diagnostics[0]?.code).toBe(2339)
|
||||
expect(ts.flattenDiagnosticMessageText(diagnostics[0]?.messageText ?? '', '\n')).toContain("Property 'goals' does not exist")
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/typert/type-meta/README.md
|
||||
README.md: 95716446c01c7fd510cdf55a82509b5b8af6f3ae
|
||||
README.zh.md: 0d30b3122265d9bb3caa289345f843fe67377be3
|
||||
README.md: 245df305efcf711486b2d3f32e40a8b415f2682e
|
||||
README.zh.md: 592aa5d027a52a7a277a90ba5d51f19101f055f6
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Compiler-independent declarations shared by business packages, generated TypeRT artifacts, the Host Gateway, and Client API. This package owns Remote decorators, the explicit Service binding, merge-extensible protocol maps, invocation descriptors, codecs, and provider contracts; it does not run TypeScript analysis or provide a Cordis service.
|
||||
Compiler-independent declarations shared by business packages, generated TypeRT artifacts, the Host Gateway, and Client API. This package owns the Remote Service base, decorators, explicit binding fallback, merge-extensible protocol maps, invocation descriptors, codecs, and provider contracts; it does not run TypeScript analysis or register a concrete Cordis service.
|
||||
|
||||
## Remote declarations
|
||||
|
||||
- `@Remote` marks a public instance method for direct invocation on its registered Cordis Service.
|
||||
- `@RemoteContext(key)` marks a method whose receiver is selected from a merge-declared scoped Context kind.
|
||||
- `bindTypeRTGateway(this, serviceKey, options?)` creates the visible, frozen binding between a Service instance, its exact Cordis key, and its wire namespace.
|
||||
- `GatewayService` binds the Cordis key passed to `super(ctx, serviceKey, options?)` to the same default wire namespace.
|
||||
- `bindTypeRTGateway(this, serviceKey, options?)` provides the same visible, frozen binding for a Service that cannot inherit from `GatewayService`.
|
||||
- `remoteMethods(service)` returns a detached declaration-order snapshot used by the Gateway's SRC fallback.
|
||||
|
||||
A Host method opts into cooperative cancellation by declaring `signal: AbortSignal` as its final parameter. `InvocationDescriptor.cancellation` records that reserved injection point; the signal never becomes a JSON parameter or lookup field. SRC recognizes the final parameter name, while strict generation also verifies the global `AbortSignal` type.
|
||||
|
||||
Decorator initializers retain markers in a module-private `WeakMap` keyed by the Service prototype. They do not add constructor symbols, prototype properties, parameter metadata, or runtime reflection fields. The Service opts in explicitly through its `typertGateway` binding field.
|
||||
Decorator initializers retain markers in a module-private `WeakMap` keyed by the Service prototype. They do not add constructor symbols, prototype properties, parameter metadata, or runtime reflection fields. A `GatewayService` exposes the same public readonly `typertGateway` binding that the explicit helper returns.
|
||||
|
||||
## TypeRT protocol
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
该包提供不依赖编译器的声明,由业务包、生成的 TypeRT 产物、Host Gateway 和 Client API 共享。它负责 Remote 装饰器、显式服务绑定、可通过声明合并扩展的协议映射、调用描述符、编解码器和提供方契约;它不执行 TypeScript 分析,也不提供 Cordis 服务。
|
||||
该包提供不依赖编译器的声明,由业务包、生成的 TypeRT 产物、Host Gateway 和 Client API 共享。它负责 Remote Service 基类、装饰器、显式 binding 回退、可通过声明合并扩展的协议映射、调用描述符、编解码器和提供方契约;它不执行 TypeScript 分析,也不注册具体 Cordis 服务。
|
||||
|
||||
## Remote 声明
|
||||
|
||||
- `@Remote` 将公开实例方法标记为可在其注册的 Cordis 服务上直接调用。
|
||||
- `@RemoteContext(key)` 标记接收者选自合并声明的作用域 Context 类型的方法。
|
||||
- `bindTypeRTGateway(this, serviceKey, options?)` 在服务实例、其准确的 Cordis key 与协议命名空间之间创建可见且冻结的绑定。
|
||||
- `GatewayService` 将 `super(ctx, serviceKey, options?)` 接收的 Cordis key 同时绑定为默认 wire namespace。
|
||||
- `bindTypeRTGateway(this, serviceKey, options?)` 为无法继承 `GatewayService` 的 Service 提供同样可见且冻结的绑定。
|
||||
- `remoteMethods(service)` 返回按声明顺序排列、与内部状态分离的快照,供 Gateway 的 SRC 回退路径使用。
|
||||
|
||||
Host 方法通过将 `signal: AbortSignal` 声明为最后一个参数来启用协作式取消。`InvocationDescriptor.cancellation` 记录这个保留的注入点;signal 绝不会成为 JSON 参数或 lookup 字段。SRC 识别末位参数名,严格生成还会校验它是否具有全局 `AbortSignal` 类型。
|
||||
|
||||
装饰器初始化器将标记保存在以服务 prototype 为键的模块私有 `WeakMap` 中。它们不会在构造函数上添加 symbol,也不会添加 prototype 属性、参数元数据或运行时反射字段。服务通过自身的 `typertGateway` 绑定字段显式接入。
|
||||
装饰器初始化器将标记保存在以服务 prototype 为键的模块私有 `WeakMap` 中。它们不会在构造函数上添加 symbol,也不会添加 prototype 属性、参数元数据或运行时反射字段。`GatewayService` 会暴露与显式 helper 相同的 public readonly `typertGateway` 绑定。
|
||||
|
||||
## TypeRT 协议
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* @module @deepseek-ai/dsh-type-meta
|
||||
*/
|
||||
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type { TypeRTContextMap } from './types.ts'
|
||||
|
||||
export type {
|
||||
@@ -104,6 +105,23 @@ export function bindTypeRTGateway<Service extends object>(
|
||||
return Object.freeze({ service, serviceKey, namespace })
|
||||
}
|
||||
|
||||
/** Cordis Service base that exposes its registered name through TypeRT Gateway. */
|
||||
export abstract class GatewayService<out T = never> extends Service<T> {
|
||||
/** Visible binding consumed by the Gateway's source-mode discovery. */
|
||||
readonly typertGateway: TypeRTGatewayBinding<this>
|
||||
|
||||
/**
|
||||
* Register the Service and bind the same key to TypeRT Gateway.
|
||||
* @param ctx - owning Cordis Context.
|
||||
* @param serviceKey - exact Cordis service key and default wire namespace.
|
||||
* @param options - optional distinct wire namespace.
|
||||
*/
|
||||
protected constructor(ctx: Context, serviceKey: string, options: TypeRTGatewayBindingOptions = {}) {
|
||||
super(ctx, serviceKey)
|
||||
this.typertGateway = bindTypeRTGateway(this, this.name, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark one public instance method as a direct Remote invocation.
|
||||
* @param _method - decorated method; retained only by the class itself.
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Context } from 'cordis'
|
||||
import {
|
||||
bindTypeRTGateway,
|
||||
GatewayService,
|
||||
Remote,
|
||||
RemoteContext,
|
||||
remoteMethods,
|
||||
} from '@deepseek-ai/dsh-type-meta'
|
||||
|
||||
class Goals {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
class Goals extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
@Remote
|
||||
create(value: string): string {
|
||||
@@ -19,7 +22,7 @@ class Goals {
|
||||
}
|
||||
}
|
||||
|
||||
const methods = remoteMethods(new Goals())
|
||||
const methods = remoteMethods(new Goals(new Context()))
|
||||
const actual = JSON.stringify(methods)
|
||||
const expected = JSON.stringify([
|
||||
{ method: 'create', invocation: { kind: 'direct' } },
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from 'cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
bindTypeRTGateway,
|
||||
GatewayService,
|
||||
Remote,
|
||||
RemoteContext,
|
||||
remoteMethods,
|
||||
@@ -16,9 +18,11 @@ declare module '@deepseek-ai/dsh-type-meta' {
|
||||
}
|
||||
|
||||
describe('type-meta Remote declarations', () => {
|
||||
it('executes standard decorator syntax through the Vitest source transform', () => {
|
||||
class Goals {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
it('binds a GatewayService name and executes decorators through the Vitest source transform', async () => {
|
||||
class Goals extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
@Remote
|
||||
create(value: string): string {
|
||||
@@ -31,11 +35,26 @@ describe('type-meta Remote declarations', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goals = new Goals()
|
||||
class NamespacedGoals extends GatewayService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'internalGoals', { namespace: 'goals' })
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = new Context()
|
||||
const goals = new Goals(ctx)
|
||||
const namespaced = new NamespacedGoals(ctx)
|
||||
expect(goals.typertGateway).toEqual({ service: goals, serviceKey: 'goals', namespace: 'goals' })
|
||||
expect(namespaced.typertGateway).toEqual({
|
||||
service: namespaced,
|
||||
serviceKey: 'internalGoals',
|
||||
namespace: 'goals',
|
||||
})
|
||||
expect(remoteMethods(goals)).toEqual([
|
||||
{ method: 'create', invocation: { kind: 'direct' } },
|
||||
{ method: 'scoped', invocation: { kind: 'context', context: 'metaFixture' } },
|
||||
])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('executes standard decorator syntax through the TSX source launcher', () => {
|
||||
|
||||
Reference in New Issue
Block a user