refactor(typert): bind remote services through base class
This commit is contained in:
@@ -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