refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
6
packages/typert/protocol/README.i18n.yaml
Normal file
6
packages/typert/protocol/README.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/protocol/README.md
|
||||
README.md: 84f9f1b31cca35cffce5dc5fc84db1017adaad08
|
||||
README.zh.md: 97e4734a7292f0a305ff8ed00ff1d72d1a53b5e9
|
||||
38
packages/typert/protocol/README.md
Normal file
38
packages/typert/protocol/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# @deepseek-ai/dsh-typert-protocol
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
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.
|
||||
- `@RemoteScope(key)` marks a method whose receiver is selected from a merge-declared scoped Context kind.
|
||||
- `TypertRemoteService` binds the Cordis key passed to `super(ctx, serviceKey, options?)` to the same default wire namespace.
|
||||
- `bindTypertRemote(this, serviceKey, options?)` provides the same visible, frozen binding for a Service that cannot inherit from `TypertRemoteService`.
|
||||
- `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. A `TypertRemoteService` exposes the same public readonly `typertRemote` binding that the explicit helper returns.
|
||||
|
||||
## Typert protocol
|
||||
|
||||
Business packages extend `TypertLookupMap` and `TypertContextMap` to associate Host objects or scoped Contexts with their wire identities. Generated artifacts extend `TypertRemoteMap`, `TypertRemoteScopeMap`, and `TypertRemoteNamespaceMap` so Client imports expose only selected Remote methods. `InvocationDescriptor` is the shared runtime form consumed by the registry, Gateway, and Client Remote.
|
||||
|
||||
The Host assembly extends `TypertRemoteEventSelection` with the Host events it forwards to consumers, which narrows the `ctx.remote.$on` key face; `TypertForwardableEvent` states the shapes a one-way delivery can carry at all, excluding Scope-bound and answered events. `TypertClientRemote` carries both roles of that surface: consumers subscribe through `$on`, and the Client half owning the host frame sink hands frames over through `$dispatch`.
|
||||
|
||||
Lookup and Context packages own both sides of their contract: declaration merging supplies the static association, while runtime providers register identity resolution with `ctx.typert`. A lookup or Host Context provider supplies the stable declaration and default resolver, while Host composition may separately configure a synchronous or asynchronous resolver; policy rejections may use `TypertLookupFailure` to carry a failure value owned by the boundary adapter. Strict codecs carry generated schemas; `src-json` codecs identify the weaker source-launch path.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as this protocol package declares application reflection and registers nothing model-facing.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct effect.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- Decorator markers contain only the method name and direct or Context invocation mode. Parameter, result, lookup, and schema reflection require the Typert build pipeline.
|
||||
- Remote decorators accept only public, non-static instance methods with string names. SRC execution cannot represent overloaded, destructured, defaulted, or rest-parameter signatures.
|
||||
38
packages/typert/protocol/README.zh.md
Normal file
38
packages/typert/protocol/README.zh.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# @deepseek-ai/dsh-typert-protocol
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
该包提供不依赖编译器的声明,由业务包、生成的 Typert 产物、宿主网关和客户端 API 共享。它负责 Remote 服务基类、装饰器、显式绑定回退、可通过声明合并扩展的协议映射、调用描述符、编解码器和提供方约定;它不执行 TypeScript 分析,也不注册具体 Cordis 服务。
|
||||
|
||||
## Remote 声明
|
||||
|
||||
- `@Remote` 将公开实例方法标记为可在其注册的 Cordis 服务上直接调用。
|
||||
- `@RemoteScope(key)` 标记接收者选自合并声明的作用域 Context 类型的方法。
|
||||
- `TypertRemoteService` 将传给 `super(ctx, serviceKey, options?)` 的 Cordis 键绑定到同一默认协议命名空间。
|
||||
- `bindTypertRemote(this, serviceKey, options?)` 为无法继承 `TypertRemoteService` 的服务提供同样可见且冻结的绑定。
|
||||
- `remoteMethods(service)` 返回按声明顺序排列、与内部状态分离的快照,供 Gateway 的 SRC 回退路径使用。
|
||||
|
||||
宿主方法通过将 `signal: AbortSignal` 声明为最后一个参数来启用协作式取消。`InvocationDescriptor.cancellation` 记录这个保留的注入点;该信号绝不会成为 JSON 参数或查找字段。SRC 识别末位参数名,严格生成还会校验它是否具有全局 `AbortSignal` 类型。
|
||||
|
||||
装饰器初始化器将标记保存在以服务原型为键的模块私有 `WeakMap` 中。它们不会在构造函数上添加符号,也不会添加原型属性、参数元数据或运行时反射字段。`TypertRemoteService` 会暴露与显式辅助函数相同的公开只读 `typertRemote` 绑定。
|
||||
|
||||
## Typert 协议
|
||||
|
||||
业务包扩展 `TypertLookupMap` 和 `TypertContextMap`,以关联宿主对象或作用域 Context 与其协议身份。生成的产物扩展 `TypertRemoteMap`、`TypertRemoteScopeMap` 和 `TypertRemoteNamespaceMap`,使客户端导入后仅暴露选定的 Remote 方法。`InvocationDescriptor` 是供注册表、网关和客户端 Remote 使用的共享运行时形式。
|
||||
|
||||
Host 装配以转发给消费端的 Host 事件扩展 `TypertRemoteEventSelection`,从而收窄 `ctx.remote.$on` 的键面;`TypertForwardableEvent` 陈述单向投递根本能承载哪些形状,把 Scope 化事件与有返回值的事件排除在外。`TypertClientRemote` 承载该面的两种角色:消费方经 `$on` 订阅,持有 Host 帧 sink 的 Client 半经 `$dispatch` 交出帧。
|
||||
|
||||
查找包与 Context 包同时负责该约定的两侧:声明合并提供静态关联,运行时提供方则向 `ctx.typert` 注册身份解析。查找提供方或宿主 Context 提供方提供稳定声明与默认解析器,宿主组合可以另行配置同步或异步解析器;策略拒绝可用 `TypertLookupFailure` 携带由边界适配器拥有的失败值。严格编解码器携带生成的 schema;`src-json` 编解码器标识约束更弱的源码启动路径。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无,因为该协议包声明应用反射,不注册任何面向模型的内容。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- 装饰器标记仅包含方法名,以及直接调用或 Context 调用模式。参数、结果、查找和 schema 反射需要 Typert 构建流水线。
|
||||
- Remote 装饰器只接受具有字符串名称的公开、非静态实例方法。SRC 执行无法表示重载签名,以及包含解构参数、默认参数或剩余参数的方法签名。
|
||||
47
packages/typert/protocol/package.json
Normal file
47
packages/typert/protocol/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-typert-protocol",
|
||||
"description": "Compiler-independent Remote metadata and Typert provider protocols",
|
||||
"version": "0.0.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/typert/protocol"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./lib/types/types.d.ts",
|
||||
"default": "./lib/types/types.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
280
packages/typert/protocol/src/index.ts
Normal file
280
packages/typert/protocol/src/index.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* Remote decorators and explicit Gateway bindings backed only by private
|
||||
* module state. Strict reflection remains a Typert compiler responsibility.
|
||||
* @module @deepseek-ai/dsh-typert-protocol
|
||||
*/
|
||||
|
||||
import { Service, type Context } from '@deepseek-ai/cordis'
|
||||
import type { TypertContextMap } from './types.ts'
|
||||
|
||||
const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
|
||||
|
||||
/**
|
||||
* Test one generated Remote name against the Connection endpoint grammar.
|
||||
* @param value - namespace, method, lookup, or Context segment.
|
||||
* @returns whether the value can cross the shared RPC carrier unchanged.
|
||||
*/
|
||||
export function isTypertRemoteSegment(value: string): boolean {
|
||||
return value !== '.' && value !== '..' && TYPERT_REMOTE_SEGMENT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* A lookup policy rejection whose typed payload belongs to the active boundary adapter.
|
||||
* Gateway adapters preserve this payload instead of collapsing it into an infrastructure failure.
|
||||
*/
|
||||
export class TypertLookupFailure<Failure = unknown> extends Error {
|
||||
/** Adapter-owned failure returned to the caller. */
|
||||
readonly failure: Failure
|
||||
|
||||
/**
|
||||
* Wrap one adapter failure without exposing the rejected identity.
|
||||
* @param failure - typed failure owned by the active boundary adapter.
|
||||
*/
|
||||
constructor(failure: Failure) {
|
||||
super('Typert lookup policy rejected the requested identity')
|
||||
this.name = 'TypertLookupFailure'
|
||||
this.failure = failure
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
InvocationDescriptor,
|
||||
InvocationParameterDescriptor,
|
||||
InvocationSourceLocation,
|
||||
RemoteFailure,
|
||||
RemoteResult,
|
||||
TypertClientRemote,
|
||||
TypertClientContextBinder,
|
||||
TypertCodec,
|
||||
TypertContext,
|
||||
TypertContextMap,
|
||||
TypertContextRegistry,
|
||||
TypertContextWire,
|
||||
TypertDisposer,
|
||||
TypertForwardableEvent,
|
||||
TypertHostContextProvider,
|
||||
TypertHostContextResolver,
|
||||
TypertLocalRegistry,
|
||||
TypertLookup,
|
||||
TypertLookupDefinition,
|
||||
TypertLookupHost,
|
||||
TypertLookupMap,
|
||||
TypertLookupProvider,
|
||||
TypertLookupResolver,
|
||||
TypertLookupRegistry,
|
||||
TypertLookupWire,
|
||||
TypertRemoteScopeApi,
|
||||
TypertRemoteScopeMap,
|
||||
TypertRemoteScopeNamespace,
|
||||
TypertRemoteContribution,
|
||||
TypertRemoteEvent,
|
||||
TypertRemoteEventSelection,
|
||||
TypertRemoteMap,
|
||||
TypertRemoteNamespace,
|
||||
TypertRemoteNamespaceMap,
|
||||
TypertRemoteRegistry,
|
||||
TypertRegistryChange,
|
||||
TypertRegistryListener,
|
||||
TypertSchema,
|
||||
TypertRegistryContract,
|
||||
} from './types.ts'
|
||||
|
||||
/** Options for an explicit Service-to-Gateway binding. */
|
||||
export interface TypertGatewayBindingOptions {
|
||||
/** Wire namespace; defaults to the Cordis service key. */
|
||||
readonly namespace?: string
|
||||
}
|
||||
|
||||
/** Visible declaration that one Service participates in Typert Gateway export. */
|
||||
export interface TypertGatewayBinding<Service extends object = object> {
|
||||
readonly service: Service
|
||||
readonly serviceKey: string
|
||||
readonly namespace: string
|
||||
}
|
||||
|
||||
/** Invocation mode recorded by a Remote method decorator. */
|
||||
export type RemoteInvocationMarker =
|
||||
| { readonly kind: 'direct' }
|
||||
| { readonly kind: 'context'; readonly context: string }
|
||||
|
||||
/** One decorator marker discovered for a live Service instance. */
|
||||
export interface RemoteMethodMarker {
|
||||
/** Public instance method carrying the implementation. */
|
||||
readonly method: string
|
||||
/** Endpoint method when it differs from the implementation member. */
|
||||
readonly exportName?: string
|
||||
readonly invocation: RemoteInvocationMarker
|
||||
}
|
||||
|
||||
type RemoteMethodDecorator = <This extends object, Args extends unknown[], Result>(
|
||||
method: (this: This, ...args: Args) => Result,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
|
||||
) => void
|
||||
|
||||
interface RemoteInitializerContext<This extends object> {
|
||||
readonly private: boolean
|
||||
readonly static: boolean
|
||||
readonly name: string | symbol
|
||||
addInitializer(initializer: (this: This) => void): void
|
||||
}
|
||||
|
||||
interface StoredRemoteMethodMarker {
|
||||
readonly exportName?: string
|
||||
readonly invocation: RemoteInvocationMarker
|
||||
}
|
||||
|
||||
const markers = new WeakMap<object, Map<string, StoredRemoteMethodMarker>>()
|
||||
|
||||
/**
|
||||
* Bind one visible Service field to a Cordis key and Remote namespace.
|
||||
* @param service - owning Service instance, normally `this`.
|
||||
* @param serviceKey - exact Cordis service key.
|
||||
* @param options - optional distinct wire namespace.
|
||||
* @returns a frozen, inspectable binding with no compiler-injected metadata.
|
||||
*/
|
||||
export function bindTypertRemote<Service extends object>(
|
||||
service: Service,
|
||||
serviceKey: string,
|
||||
options: TypertGatewayBindingOptions = {},
|
||||
): TypertGatewayBinding<Service> {
|
||||
validateName('service key', serviceKey)
|
||||
const namespace = options.namespace ?? serviceKey
|
||||
validateName('namespace', namespace)
|
||||
return Object.freeze({ service, serviceKey, namespace })
|
||||
}
|
||||
|
||||
/** Cordis Service base that exposes its registered name through Typert Gateway. */
|
||||
export abstract class TypertRemoteService<out T = never> extends Service<T> {
|
||||
/** Visible binding consumed by the Gateway's source-mode discovery. */
|
||||
readonly typertRemote: 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.typertRemote = bindTypertRemote(this, this.name, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark one public instance method as a direct Remote invocation.
|
||||
* @param _method - decorated method; retained only by the class itself.
|
||||
* @param context - standard decorator context used to schedule private marking.
|
||||
*/
|
||||
export function Remote<This extends object, Args extends unknown[], Result>(
|
||||
_method: (this: This, ...args: Args) => Result,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
|
||||
): void
|
||||
/**
|
||||
* Mark one public instance method under a distinct exported method name.
|
||||
* @param exportName - Remote endpoint method, without a namespace or slash.
|
||||
* @returns a standard method decorator.
|
||||
*/
|
||||
export function Remote(exportName: string): RemoteMethodDecorator
|
||||
export function Remote<This extends object, Args extends unknown[], Result>(
|
||||
methodOrExportName: string | ((this: This, ...args: Args) => Result),
|
||||
context?: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
|
||||
): void | RemoteMethodDecorator {
|
||||
if (typeof methodOrExportName === 'string') {
|
||||
validateName('Remote export name', methodOrExportName)
|
||||
return function <DecoratorThis extends object, DecoratorArgs extends unknown[], DecoratorResult>(
|
||||
_method: (this: DecoratorThis, ...args: DecoratorArgs) => DecoratorResult,
|
||||
decoratorContext: ClassMethodDecoratorContext<
|
||||
DecoratorThis,
|
||||
(this: DecoratorThis, ...args: DecoratorArgs) => DecoratorResult
|
||||
>,
|
||||
): void {
|
||||
addMarkerInitializer(decoratorContext, { kind: 'direct' }, methodOrExportName)
|
||||
}
|
||||
}
|
||||
if (context === undefined) throw new TypeError('typert-protocol: Remote decorator context is missing')
|
||||
addMarkerInitializer(context, { kind: 'direct' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a decorator for a method resolved from one Remote Scope.
|
||||
* @param key - scope key declared through the Context map.
|
||||
* @param exportName - optional Remote export name; defaults to the method name.
|
||||
* @returns a standard method decorator that records only private module state.
|
||||
*/
|
||||
export function RemoteScope(
|
||||
key: Extract<keyof TypertContextMap, string>,
|
||||
exportName?: string,
|
||||
): RemoteMethodDecorator {
|
||||
validateName('Scope key', key)
|
||||
if (exportName !== undefined) validateName('Remote export name', exportName)
|
||||
return function <This extends object, Args extends unknown[], Result>(
|
||||
_method: (this: This, ...args: Args) => Result,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
|
||||
): void {
|
||||
addMarkerInitializer(context, { kind: 'context', context: key }, exportName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Remote markers attached to a live Service by decorator initializers.
|
||||
* The returned snapshot cannot mutate the private marker table.
|
||||
* @param service - live Service instance.
|
||||
* @returns markers in class declaration order.
|
||||
*/
|
||||
export function remoteMethods(service: object): readonly RemoteMethodMarker[] {
|
||||
const prototype = Object.getPrototypeOf(service) as object | null
|
||||
if (prototype === null) return []
|
||||
return [...(markers.get(prototype) ?? [])].map(([method, marker]) => ({ method, ...marker }))
|
||||
}
|
||||
|
||||
function addMarkerInitializer<This extends object>(
|
||||
context: RemoteInitializerContext<This>,
|
||||
invocation: RemoteInvocationMarker,
|
||||
exportName?: string,
|
||||
): void {
|
||||
if (context.private || context.static || typeof context.name !== 'string') {
|
||||
throw new TypeError('typert-protocol: Remote decorators require a public instance method with a string name')
|
||||
}
|
||||
const method = context.name
|
||||
context.addInitializer(function (this: This) {
|
||||
const prototype = Object.getPrototypeOf(this) as object | null
|
||||
if (prototype === null) {
|
||||
throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`)
|
||||
}
|
||||
mark(prototype, method, invocation, exportName)
|
||||
})
|
||||
}
|
||||
|
||||
function mark(
|
||||
prototype: object,
|
||||
method: string,
|
||||
invocation: RemoteInvocationMarker,
|
||||
exportName?: string,
|
||||
): void {
|
||||
let table = markers.get(prototype)
|
||||
if (table === undefined) {
|
||||
table = new Map()
|
||||
markers.set(prototype, table)
|
||||
}
|
||||
const marker: StoredRemoteMethodMarker = {
|
||||
...(exportName === undefined || exportName === method ? {} : { exportName }),
|
||||
invocation: Object.freeze(invocation),
|
||||
}
|
||||
const current = table.get(method)
|
||||
if (current !== undefined) {
|
||||
if (current.exportName === marker.exportName && sameInvocation(current.invocation, invocation)) return
|
||||
throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`)
|
||||
}
|
||||
table.set(method, Object.freeze(marker))
|
||||
}
|
||||
|
||||
function sameInvocation(left: RemoteInvocationMarker, right: RemoteInvocationMarker): boolean {
|
||||
return left.kind === right.kind
|
||||
&& (left.kind === 'direct' || (right.kind === 'context' && left.context === right.context))
|
||||
}
|
||||
|
||||
function validateName(subject: string, value: string): void {
|
||||
if (!isTypertRemoteSegment(value)) {
|
||||
throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`)
|
||||
}
|
||||
}
|
||||
30
packages/typert/protocol/src/invariant.ts
Normal file
30
packages/typert/protocol/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-typert-protocol`.
|
||||
* @module @deepseek-ai/dsh-typert-protocol/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'typert-protocol-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: decorators retain private immutable declarations and
|
||||
* bindings are frozen values with no independent event stream to cross-check.
|
||||
*/
|
||||
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 */
|
||||
491
packages/typert/protocol/src/types.ts
Normal file
491
packages/typert/protocol/src/types.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
/**
|
||||
* Compiler-independent Typert protocol shared by business packages, generated
|
||||
* Remote artifacts, the Host Gateway, and Client API implementations.
|
||||
* @module @deepseek-ai/dsh-typert-protocol/types
|
||||
*/
|
||||
|
||||
import type { Context, Events } from '@deepseek-ai/cordis'
|
||||
|
||||
declare const LOOKUP_HOST: unique symbol
|
||||
declare const LOOKUP_WIRE: unique symbol
|
||||
declare const CONTEXT_WIRE: unique symbol
|
||||
|
||||
/** Type-level association between a Host object and its wire identity. */
|
||||
export interface TypertLookup<Host, Wire> {
|
||||
readonly [LOOKUP_HOST]: Host
|
||||
readonly [LOOKUP_WIRE]: Wire
|
||||
}
|
||||
|
||||
/** Extract the Host object associated with one lookup declaration. */
|
||||
export type TypertLookupHost<Lookup> = Lookup extends TypertLookup<infer Host, infer _Wire> ? Host : never
|
||||
|
||||
/** Extract the wire identity associated with one lookup declaration. */
|
||||
export type TypertLookupWire<Lookup> = Lookup extends TypertLookup<infer _Host, infer Wire> ? Wire : never
|
||||
|
||||
/** Type-level association between a scoped Context kind and its wire identity. */
|
||||
export interface TypertContext<Wire> {
|
||||
readonly [CONTEXT_WIRE]: Wire
|
||||
}
|
||||
|
||||
/** Extract the wire identity associated with one scoped Context declaration. */
|
||||
export type TypertContextWire<ContextType> = ContextType extends TypertContext<infer Wire> ? Wire : never
|
||||
|
||||
/** Merge-extensible Host object lookup declarations. */
|
||||
export interface TypertLookupMap {}
|
||||
|
||||
/** Merge-extensible scoped Context declarations. */
|
||||
export interface TypertContextMap {}
|
||||
|
||||
/** Merge-extensible direct Remote method signatures generated for consumers. */
|
||||
export interface TypertRemoteMap {}
|
||||
|
||||
/**
|
||||
* One Remote call's failure as the carrier reported it. `code` stays open here:
|
||||
* the closed RPC code union belongs to the carrier package, which already
|
||||
* depends on this one, so naming it would invert that edge.
|
||||
*/
|
||||
export interface RemoteFailure {
|
||||
readonly code: string
|
||||
readonly message: string
|
||||
readonly details: object
|
||||
}
|
||||
|
||||
/**
|
||||
* What every generated Remote method resolves to. The Remote face itself folds
|
||||
* carrier failures into the error branch, so no consumer wraps a call to
|
||||
* recover one; only assembly faults (arity, an unmounted method, a missing
|
||||
* Context binder) still reject.
|
||||
* @template T - the Host method's business result.
|
||||
*/
|
||||
export type RemoteResult<T> =
|
||||
| { readonly ok: true; readonly value: T }
|
||||
| { readonly ok: false; readonly error: RemoteFailure }
|
||||
|
||||
/** Merge-extensible scoped Remote method signatures generated for consumers. */
|
||||
export interface TypertRemoteScopeMap {}
|
||||
|
||||
/**
|
||||
* Cordis event names whose shape a one-way Remote delivery can carry: unbound
|
||||
* from any Scope and returning `void`. Which ones are actually forwarded is the
|
||||
* Host assembly's selection; this predicate only excludes shapes the carrier
|
||||
* cannot represent.
|
||||
*/
|
||||
export type TypertForwardableEvent = {
|
||||
[Event in keyof Events]: unknown extends ThisParameterType<Events[Event]>
|
||||
? ReturnType<Events[Event]> extends void ? Event : never
|
||||
: never
|
||||
}[keyof Events]
|
||||
|
||||
/** Merge-extensible forwarding selection declared once by the Host assembly. */
|
||||
export interface TypertRemoteEventSelection {}
|
||||
|
||||
/** Legal `$on` keys: selected events that exist in the current compilation face. */
|
||||
export type TypertRemoteEvent = Extract<keyof Events, keyof TypertRemoteEventSelection>
|
||||
|
||||
/**
|
||||
* Resolve one direct Remote namespace from the generated flat endpoint map.
|
||||
* @template Namespace - wire namespace before the endpoint slash.
|
||||
*/
|
||||
export type TypertRemoteNamespace<Namespace extends string> = {
|
||||
[Endpoint in keyof TypertRemoteMap as Endpoint extends `${Namespace}/${infer Method}`
|
||||
? Method
|
||||
: never]: TypertRemoteMap[Endpoint]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one scoped Remote namespace across every generated Context kind.
|
||||
* The calling Cordis Context supplies the concrete identity at runtime.
|
||||
* @template Namespace - wire namespace between the Context prefix and method.
|
||||
*/
|
||||
export type TypertRemoteScopeNamespace<
|
||||
Namespace extends string,
|
||||
ContextKey extends string = string,
|
||||
> = {
|
||||
[Endpoint in keyof TypertRemoteScopeMap as Endpoint extends `${ContextKey}:${Namespace}/${infer Method}`
|
||||
? Method
|
||||
: never]: TypertRemoteScopeMap[Endpoint]
|
||||
}
|
||||
|
||||
type TypertRemoteScopeNamespaceKey<
|
||||
ContextKey extends string,
|
||||
Endpoint = keyof TypertRemoteScopeMap,
|
||||
> = Endpoint extends `${ContextKey}:${infer Namespace}/${string}` ? Namespace : never
|
||||
|
||||
/** Generated scoped Remote namespaces available to one Context kind. */
|
||||
export type TypertRemoteScopeApi<ContextKey extends string> = {
|
||||
[Namespace in TypertRemoteScopeNamespaceKey<ContextKey>]:
|
||||
TypertRemoteScopeNamespace<Namespace, ContextKey>
|
||||
}
|
||||
|
||||
/** Merge-extensible direct namespace surface generated for Client Remote services. */
|
||||
export interface TypertRemoteNamespaceMap {}
|
||||
|
||||
/** Awaitable disposer returned by Cordis-owned Typert registrations. */
|
||||
export type TypertDisposer = () => Promise<void>
|
||||
|
||||
type StringKeyOf<Value> = Extract<keyof Value, string>
|
||||
|
||||
/** Minimal runtime-schema capability carried by strict generated codecs. */
|
||||
export interface TypertSchema<Output = unknown> {
|
||||
/**
|
||||
* Parse and validate one boundary value.
|
||||
* @param value - untrusted boundary value.
|
||||
* @returns the validated value.
|
||||
*/
|
||||
parse(value: unknown): Output
|
||||
}
|
||||
|
||||
/** Codec attached to one invocation parameter or result. */
|
||||
export type TypertCodec =
|
||||
| {
|
||||
readonly mode: 'strict'
|
||||
readonly typeSymbol: string
|
||||
readonly schema: TypertSchema
|
||||
}
|
||||
| {
|
||||
readonly mode: 'src-json'
|
||||
}
|
||||
|
||||
/** One ordered business parameter in a Remote invocation. */
|
||||
export interface InvocationParameterDescriptor {
|
||||
/** Source-level parameter name. */
|
||||
readonly name: string
|
||||
/** Required key in the wire `args` object. */
|
||||
readonly wire: string
|
||||
/** Whether the value is JSON or requires a registered Host lookup. */
|
||||
readonly source: 'json' | 'lookup'
|
||||
/** Lookup key when `source` is `lookup`. */
|
||||
readonly lookup?: string
|
||||
/** Boundary codec for the wire representation. */
|
||||
readonly codec: TypertCodec
|
||||
/** Missing wire fields decode to `undefined` only for an explicitly declared `T | undefined`. */
|
||||
readonly acceptsUndefined?: true
|
||||
}
|
||||
|
||||
/** Source position retained for diagnostics from generated definitions. */
|
||||
export interface InvocationSourceLocation {
|
||||
readonly file: string
|
||||
readonly line: number
|
||||
readonly column: number
|
||||
}
|
||||
|
||||
/** Carrier-independent description of one exported method invocation. */
|
||||
export interface InvocationDescriptor {
|
||||
/** Globally stable generated identity. */
|
||||
readonly id: string
|
||||
/** Cordis service key owning the method. */
|
||||
readonly service: string
|
||||
/** Wire namespace, defaulting to the service key. */
|
||||
readonly namespace: string
|
||||
/** Public instance method name. */
|
||||
readonly method: string
|
||||
/** Service member invoked when the exported method name is an alias. */
|
||||
readonly implementation?: string
|
||||
/** Receiver selection mode. */
|
||||
readonly invocation:
|
||||
| { readonly kind: 'direct' }
|
||||
| {
|
||||
readonly kind: 'context'
|
||||
readonly context: string
|
||||
readonly wire: string
|
||||
readonly codec: TypertCodec
|
||||
}
|
||||
/** Optional consuming-Context projection for one direct lookup parameter. */
|
||||
readonly scope?: {
|
||||
/** Context kind whose Client binder supplies the identity. */
|
||||
readonly context: string
|
||||
/** Lookup parameter wire field replaced by the Context identity. */
|
||||
readonly wire: string
|
||||
}
|
||||
/** Ordered business parameters. */
|
||||
readonly parameters: readonly InvocationParameterDescriptor[]
|
||||
/** Transport cancellation injected after business parameters instead of entering wire args. */
|
||||
readonly cancellation?: {
|
||||
/** Reserved final Host method parameter. */
|
||||
readonly parameter: 'signal'
|
||||
}
|
||||
/** Codec for the resolved method result. */
|
||||
readonly result: TypertCodec
|
||||
/** Source declaration used only for diagnostics. */
|
||||
readonly sourceLocation?: InvocationSourceLocation
|
||||
}
|
||||
|
||||
/** Generated Host contract selected explicitly by a Client assembly. */
|
||||
export interface TypertRemoteContribution {
|
||||
/** npm package that owns the Remote methods. */
|
||||
readonly package: string
|
||||
/** Consumer-side invocation descriptors generated from that package. */
|
||||
readonly descriptors: readonly InvocationDescriptor[]
|
||||
}
|
||||
|
||||
/** Client Remote capability implemented by the Gateway and consumed by Remote assemblies. */
|
||||
export interface TypertClientRemote extends TypertRemoteNamespaceMap {
|
||||
/**
|
||||
* Mount one generated Host-for-Client contribution in the caller's fiber.
|
||||
* @param contribution - explicitly selected Remote package artifact.
|
||||
* @returns disposer after namespace services and concrete methods are ready.
|
||||
*/
|
||||
$mount(contribution: TypertRemoteContribution): Promise<TypertDisposer>
|
||||
/**
|
||||
* Subscribe to one forwarded Host event; delivery is one-way, in registration
|
||||
* order, and isolates a throwing listener from the rest.
|
||||
* @template Event - forwarded event name selected by the Host assembly.
|
||||
* @param event - forwarded Host event name, unchanged on the wire.
|
||||
* @param listener - receives the Host's argument list as declared by Cordis `Events`.
|
||||
* @returns disposer owned by the calling fiber.
|
||||
*/
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): () => void
|
||||
/**
|
||||
* Hand one decoded forwarded frame to the subscription table. The carrier
|
||||
* owning the Host frame sink calls this; a consumer subscribes with
|
||||
* {@link TypertClientRemote.$on} and never calls it.
|
||||
*
|
||||
* `event` is a plain string because this is the wire boundary: the name is
|
||||
* whatever the Host assembly's allowlist selected, and one nobody subscribed
|
||||
* to is dropped silently.
|
||||
* @param event - forwarded Host event name, exactly as the Host emitted it.
|
||||
* @param args - the Host argument list, already JSON-decoded.
|
||||
*/
|
||||
$dispatch(event: string, args: readonly unknown[]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one validated wire identity, synchronously or asynchronously.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the Host object, or `undefined` when unavailable.
|
||||
*/
|
||||
export type TypertLookupResolver<Host = unknown, Wire = unknown> = (
|
||||
id: Wire,
|
||||
) => Host | undefined | Promise<Host | undefined>
|
||||
|
||||
/** Runtime provider for one declared Host object lookup. */
|
||||
export interface TypertLookupProvider<Host = unknown, Wire = unknown> {
|
||||
/** Source parameter name recognized by the SRC weak parser. */
|
||||
readonly parameter: string
|
||||
/** Wire field replacing the Host object parameter. */
|
||||
readonly wire: string
|
||||
/** Canonical Host type symbol used by strict generation. */
|
||||
readonly hostTypeSymbol: string
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
/**
|
||||
* Resolve a wire identity through the provider's default policy.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the object, `undefined` when unavailable, or either asynchronously.
|
||||
*/
|
||||
resolve(id: Wire): Host | undefined | Promise<Host | undefined>
|
||||
}
|
||||
|
||||
/** Stable wire declaration retained after a lookup provider unloads. */
|
||||
export interface TypertLookupDefinition {
|
||||
/** Merge-declared lookup key. */
|
||||
readonly key: string
|
||||
/** Source parameter name recognized by the SRC weak parser. */
|
||||
readonly parameter: string
|
||||
/** Wire field replacing the Host object parameter. */
|
||||
readonly wire: string
|
||||
/** Canonical Host type symbol used by strict generation. */
|
||||
readonly hostTypeSymbol: string
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
}
|
||||
|
||||
/** Host resolver for one scoped Remote kind. */
|
||||
export interface TypertHostContextProvider<Wire = unknown> {
|
||||
/** Wire field carrying the Context identity. */
|
||||
readonly wire: string
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
/**
|
||||
* Resolve a wire identity to its live scoped Context.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the scoped Context, or `undefined` when unavailable.
|
||||
*/
|
||||
resolve(id: Wire): Context | undefined | Promise<Context | undefined>
|
||||
}
|
||||
|
||||
/** Composition-owned resolver replacing one Host Context provider's default lookup policy. */
|
||||
export type TypertHostContextResolver<Wire = unknown> = (
|
||||
id: Wire,
|
||||
) => Context | undefined | Promise<Context | undefined>
|
||||
|
||||
/** Client resolver for the identity carried by the calling scoped Context. */
|
||||
export interface TypertClientContextBinder<Wire = unknown> {
|
||||
/**
|
||||
* Read the Remote identity represented by a calling Context.
|
||||
* @param ctx - Context rebound by the Cordis service tracker.
|
||||
* @returns the wire identity, or `undefined` when the Context has the wrong scope.
|
||||
*/
|
||||
identity(ctx: Context): Wire | undefined
|
||||
}
|
||||
|
||||
/** Notification emitted after a Typert runtime registry changes. */
|
||||
export interface TypertRegistryChange {
|
||||
readonly kind: 'local' | 'remote' | 'lookup' | 'host-context' | 'client-context'
|
||||
readonly key: string
|
||||
}
|
||||
|
||||
/** Listener for one Typert runtime registry. */
|
||||
export type TypertRegistryListener = (change: TypertRegistryChange) => void
|
||||
|
||||
/** Current-environment invocation definitions. */
|
||||
export interface TypertLocalRegistry {
|
||||
/**
|
||||
* Look up one invocation by `<namespace>/<method>`.
|
||||
* @param endpoint - canonical endpoint.
|
||||
* @returns the live descriptor, or `undefined` when absent.
|
||||
*/
|
||||
get(endpoint: string): InvocationDescriptor | undefined
|
||||
/**
|
||||
* Report whether a strict definition has existed during this Typert Service lifetime.
|
||||
* @param endpoint - canonical endpoint.
|
||||
* @returns `true` after the endpoint has been registered at least once, even if withdrawn.
|
||||
*/
|
||||
hasSeen(endpoint: string): boolean
|
||||
/** @returns a registration-order snapshot of local descriptors. */
|
||||
list(): readonly InvocationDescriptor[]
|
||||
/**
|
||||
* Observe later local-definition changes.
|
||||
* @param listener - synchronous contained observer.
|
||||
* @returns disposer for this subscription.
|
||||
*/
|
||||
subscribe(listener: TypertRegistryListener): TypertDisposer
|
||||
}
|
||||
|
||||
/** Consumer-selected Remote contribution registry. */
|
||||
export interface TypertRemoteRegistry {
|
||||
/**
|
||||
* Register one generated contribution for the calling Cordis fiber.
|
||||
* @param contribution - generated Remote descriptors.
|
||||
* @returns disposer withdrawing the exact contribution.
|
||||
*/
|
||||
register(contribution: TypertRemoteContribution): TypertDisposer
|
||||
/**
|
||||
* Look up one Remote descriptor by endpoint.
|
||||
* @param endpoint - canonical endpoint.
|
||||
* @returns the descriptor, or `undefined` when unmounted.
|
||||
*/
|
||||
get(endpoint: string): InvocationDescriptor | undefined
|
||||
/** @returns a registration-order snapshot of Remote descriptors. */
|
||||
list(): readonly InvocationDescriptor[]
|
||||
/**
|
||||
* Observe later Remote contribution changes.
|
||||
* @param listener - synchronous contained observer.
|
||||
* @returns disposer for this subscription.
|
||||
*/
|
||||
subscribe(listener: TypertRegistryListener): TypertDisposer
|
||||
}
|
||||
|
||||
/** Runtime registry for Host object lookup providers. */
|
||||
export interface TypertLookupRegistry {
|
||||
/**
|
||||
* Register one provider under its merge-declared key.
|
||||
* @param key - lookup key.
|
||||
* @param provider - owning package's live resolver.
|
||||
* @returns disposer withdrawing the exact provider.
|
||||
*/
|
||||
register<K extends StringKeyOf<TypertLookupMap>>(
|
||||
key: K,
|
||||
provider: TypertLookupProvider<
|
||||
TypertLookupHost<TypertLookupMap[K]>,
|
||||
TypertLookupWire<TypertLookupMap[K]>
|
||||
>,
|
||||
): TypertDisposer
|
||||
/**
|
||||
* Replace one provider's default resolution policy while this contribution is active.
|
||||
* Configuration may precede provider registration; without a live provider, `get()` remains unavailable.
|
||||
* @param key - lookup key whose wire declaration remains provider-owned.
|
||||
* @param resolver - composition-owned resolver used by every lookup of this key.
|
||||
* @returns disposer restoring the provider's default resolver.
|
||||
*/
|
||||
configure<K extends StringKeyOf<TypertLookupMap>>(
|
||||
key: K,
|
||||
resolver: TypertLookupResolver<
|
||||
TypertLookupHost<TypertLookupMap[K]>,
|
||||
TypertLookupWire<TypertLookupMap[K]>
|
||||
>,
|
||||
): TypertDisposer
|
||||
/**
|
||||
* Look up one provider by runtime key.
|
||||
* @param key - descriptor lookup key.
|
||||
* @returns the live provider, or `undefined` when absent.
|
||||
*/
|
||||
get(key: string): TypertLookupProvider | undefined
|
||||
/** @returns lookup declarations observed during this Typert Service lifetime. */
|
||||
definitions(): readonly TypertLookupDefinition[]
|
||||
/** @returns a snapshot of registered provider keys. */
|
||||
keys(): readonly string[]
|
||||
/**
|
||||
* Observe later lookup changes.
|
||||
* @param listener - synchronous contained observer.
|
||||
* @returns disposer for this subscription.
|
||||
*/
|
||||
subscribe(listener: TypertRegistryListener): TypertDisposer
|
||||
}
|
||||
|
||||
/** Runtime registry for Host Context resolvers and Client Context binders. */
|
||||
export interface TypertContextRegistry {
|
||||
/**
|
||||
* Register a Host Context resolver.
|
||||
* @param key - merge-declared Context key.
|
||||
* @param provider - owning package's Host resolver.
|
||||
* @returns disposer withdrawing the exact provider.
|
||||
*/
|
||||
registerHost<K extends StringKeyOf<TypertContextMap>>(
|
||||
key: K,
|
||||
provider: TypertHostContextProvider<TypertContextWire<TypertContextMap[K]>>,
|
||||
): TypertDisposer
|
||||
/**
|
||||
* Override one Host Context key's identity policy for the calling fiber.
|
||||
* Configuration may precede provider registration and restores the provider's default resolver on disposal.
|
||||
* @param key - merge-declared Context key.
|
||||
* @param resolver - composition-owned resolver used by every Host Context lookup of this key.
|
||||
* @returns disposer restoring the provider's default resolver.
|
||||
*/
|
||||
configureHost<K extends StringKeyOf<TypertContextMap>>(
|
||||
key: K,
|
||||
resolver: TypertHostContextResolver<TypertContextWire<TypertContextMap[K]>>,
|
||||
): TypertDisposer
|
||||
/**
|
||||
* Register a Client Context identity binder.
|
||||
* @param key - merge-declared Context key.
|
||||
* @param binder - Client scope identity resolver.
|
||||
* @returns disposer withdrawing the exact binder.
|
||||
*/
|
||||
registerClient<K extends StringKeyOf<TypertContextMap>>(
|
||||
key: K,
|
||||
binder: TypertClientContextBinder<TypertContextWire<TypertContextMap[K]>>,
|
||||
): TypertDisposer
|
||||
/**
|
||||
* Look up a Host Context resolver.
|
||||
* @param key - descriptor Context key.
|
||||
* @returns the provider, or `undefined` when absent.
|
||||
*/
|
||||
getHost(key: string): TypertHostContextProvider | undefined
|
||||
/**
|
||||
* Look up a Client Context binder.
|
||||
* @param key - descriptor Context key.
|
||||
* @returns the binder, or `undefined` when absent.
|
||||
*/
|
||||
getClient(key: string): TypertClientContextBinder | undefined
|
||||
/**
|
||||
* Observe later Context provider changes.
|
||||
* @param listener - synchronous contained observer.
|
||||
* @returns disposer for this subscription.
|
||||
*/
|
||||
subscribe(listener: TypertRegistryListener): TypertDisposer
|
||||
}
|
||||
|
||||
/** Minimal Typert runtime consumed through dependency inversion. */
|
||||
export interface TypertRegistryContract {
|
||||
readonly local: TypertLocalRegistry
|
||||
readonly remotes: TypertRemoteRegistry
|
||||
readonly lookups: TypertLookupRegistry
|
||||
readonly contexts: TypertContextRegistry
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
typert: TypertRegistryContract
|
||||
}
|
||||
}
|
||||
32
packages/typert/protocol/tests/fixtures/source-launch.ts
vendored
Normal file
32
packages/typert/protocol/tests/fixtures/source-launch.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import {
|
||||
TypertRemoteService,
|
||||
Remote,
|
||||
RemoteScope,
|
||||
remoteMethods,
|
||||
} from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
class Goals extends TypertRemoteService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
@Remote
|
||||
create(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
@RemoteScope('agent')
|
||||
scoped(value: string): string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const methods = remoteMethods(new Goals(new Context()))
|
||||
const actual = JSON.stringify(methods)
|
||||
const expected = JSON.stringify([
|
||||
{ method: 'create', invocation: { kind: 'direct' } },
|
||||
{ method: 'scoped', invocation: { kind: 'context', context: 'agent' } },
|
||||
])
|
||||
if (actual !== expected) throw new Error(`unexpected Remote declarations: ${actual}`)
|
||||
process.stdout.write(actual)
|
||||
266
packages/typert/protocol/tests/protocol.spec.ts
Normal file
266
packages/typert/protocol/tests/protocol.spec.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import {
|
||||
bindTypertRemote,
|
||||
TypertRemoteService,
|
||||
Remote,
|
||||
RemoteScope,
|
||||
remoteMethods,
|
||||
type TypertContext,
|
||||
type TypertForwardableEvent,
|
||||
type TypertRemoteEvent,
|
||||
} from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Events {
|
||||
/**
|
||||
* Test-only one-way event: bound to no Scope and returning nothing.
|
||||
* @param value - marker payload.
|
||||
*/
|
||||
'meta-fixture/forwardable'(value: string): void
|
||||
/**
|
||||
* Test-only Scope-bound event, which no carrier can deliver one-way.
|
||||
* @param value - marker payload.
|
||||
*/
|
||||
'meta-fixture/scoped'(this: Context, value: string): void
|
||||
/**
|
||||
* Test-only answered event, whose result no one-way delivery can return.
|
||||
* @param value - marker payload.
|
||||
* @returns the replacement value.
|
||||
*/
|
||||
'meta-fixture/answered'(value: string): string
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface TypertContextMap {
|
||||
metaFixture: TypertContext<string>
|
||||
}
|
||||
|
||||
interface TypertRemoteEventSelection extends
|
||||
Record<'meta-fixture/forwardable' | 'meta-fixture/absent', true> {}
|
||||
}
|
||||
|
||||
describe('typert-protocol Remote declarations', () => {
|
||||
it('binds a TypertRemoteService name and executes decorators through the Vitest source transform', async () => {
|
||||
class Goals extends TypertRemoteService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'goals')
|
||||
}
|
||||
|
||||
@Remote
|
||||
create(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
@RemoteScope('metaFixture')
|
||||
scoped(value: string): string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class NamespacedGoals extends TypertRemoteService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'internalGoals', { namespace: 'goals' })
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = new Context()
|
||||
const goals = new Goals(ctx)
|
||||
const namespaced = new NamespacedGoals(ctx)
|
||||
expect(goals.typertRemote).toEqual({ service: goals, serviceKey: 'goals', namespace: 'goals' })
|
||||
expect(namespaced.typertRemote).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', () => {
|
||||
const fixture = fileURLToPath(new URL('./fixtures/source-launch.ts', import.meta.url))
|
||||
const output = execFileSync(process.execPath, ['--import', 'tsx/esm', fixture], { encoding: 'utf8' })
|
||||
expect(JSON.parse(output)).toEqual([
|
||||
{ method: 'create', invocation: { kind: 'direct' } },
|
||||
{ method: 'scoped', invocation: { kind: 'context', context: 'agent' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps decorator markers in private module state', () => {
|
||||
class Goals {
|
||||
readonly typertRemote = bindTypertRemote(this, 'goals')
|
||||
|
||||
create(agent: object, request: object): object {
|
||||
return { agent, request }
|
||||
}
|
||||
|
||||
scoped(request: object): object {
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
const initializers: Array<(this: Goals) => void> = []
|
||||
Remote(
|
||||
Reflect.get(Goals.prototype, 'create') as (this: Goals, ...args: unknown[]) => unknown,
|
||||
methodContext('create', initializers),
|
||||
)
|
||||
RemoteScope('metaFixture')(
|
||||
Reflect.get(Goals.prototype, 'scoped') as (this: Goals, ...args: unknown[]) => unknown,
|
||||
methodContext('scoped', initializers),
|
||||
)
|
||||
|
||||
const goals = new Goals()
|
||||
for (const initialize of initializers) initialize.call(goals)
|
||||
expect(goals.typertRemote).toEqual({ service: goals, serviceKey: 'goals', namespace: 'goals' })
|
||||
expect(Object.isFrozen(goals.typertRemote)).toBe(true)
|
||||
expect(remoteMethods(goals)).toEqual([
|
||||
{ method: 'create', invocation: { kind: 'direct' } },
|
||||
{ method: 'scoped', invocation: { kind: 'context', context: 'metaFixture' } },
|
||||
])
|
||||
expect(Reflect.ownKeys(Goals)).toEqual(['length', 'name', 'prototype'])
|
||||
expect(Reflect.ownKeys(Goals.prototype)).toEqual(['constructor', 'create', 'scoped'])
|
||||
})
|
||||
|
||||
it('keeps markers idempotent across instances and returns detached snapshots', () => {
|
||||
class Service {
|
||||
run(value: string): string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const initializers: Array<(this: Service) => void> = []
|
||||
Remote(
|
||||
Reflect.get(Service.prototype, 'run') as (this: Service, ...args: unknown[]) => unknown,
|
||||
methodContext('run', initializers),
|
||||
)
|
||||
|
||||
const first = new Service()
|
||||
const second = new Service()
|
||||
for (const initialize of initializers) {
|
||||
initialize.call(first)
|
||||
initialize.call(second)
|
||||
}
|
||||
const snapshot = remoteMethods(first)
|
||||
expect(remoteMethods(second)).toEqual(snapshot)
|
||||
;(snapshot as unknown as { method: string }[])[0]!.method = 'changed'
|
||||
expect(remoteMethods(first)).toEqual([{ method: 'run', invocation: { kind: 'direct' } }])
|
||||
})
|
||||
|
||||
it('supports explicit export names without exposing marker storage', () => {
|
||||
class Service {
|
||||
run(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
scoped(value: string): string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
const initializers: Array<(this: Service) => void> = []
|
||||
Remote('execute')(
|
||||
Reflect.get(Service.prototype, 'run') as (this: Service, ...args: unknown[]) => unknown,
|
||||
methodContext('run', initializers),
|
||||
)
|
||||
RemoteScope('metaFixture', 'inspect')(
|
||||
Reflect.get(Service.prototype, 'scoped') as (this: Service, ...args: unknown[]) => unknown,
|
||||
methodContext('scoped', initializers),
|
||||
)
|
||||
const service = new Service()
|
||||
for (const initialize of initializers) initialize.call(service)
|
||||
|
||||
expect(remoteMethods(service)).toEqual([
|
||||
{ method: 'run', exportName: 'execute', invocation: { kind: 'direct' } },
|
||||
{ method: 'scoped', exportName: 'inspect', invocation: { kind: 'context', context: 'metaFixture' } },
|
||||
])
|
||||
expect(remoteMethods({})).toEqual([])
|
||||
const prototypeLess: object = {}
|
||||
Reflect.setPrototypeOf(prototypeLess, null)
|
||||
expect(remoteMethods(prototypeLess)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects malformed decorator calls and targets', () => {
|
||||
const method: (this: object) => void = function (this: object): void {}
|
||||
expect(() => { (Remote as unknown as (value: typeof method) => void)(method) }).toThrow('context is missing')
|
||||
expect(() => Remote('bad/name')).toThrow('export name')
|
||||
expect(() => Remote('bad#name')).toThrow('export name')
|
||||
expect(() => Remote('bad name')).toThrow('export name')
|
||||
expect(() => Remote('.')).toThrow('export name')
|
||||
expect(() => Remote('..')).toThrow('export name')
|
||||
expect(() => RemoteScope('' as 'metaFixture')).toThrow('Scope key')
|
||||
expect(() => RemoteScope('metaFixture', 'bad/name')).toThrow('export name')
|
||||
|
||||
for (const context of [
|
||||
{ ...methodContext('run', []), private: true },
|
||||
{ ...methodContext('run', []), static: true },
|
||||
{ ...methodContext('run', []), name: Symbol('run') },
|
||||
]) {
|
||||
expect(() => { Remote(method, context) })
|
||||
.toThrow('public instance method')
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects prototype-less initialization and conflicting markers', () => {
|
||||
const method: (this: object) => void = function (this: object): void {}
|
||||
const direct: Array<(this: object) => void> = []
|
||||
Remote(method, methodContext('run', direct))
|
||||
const prototypeLess: object = {}
|
||||
Reflect.setPrototypeOf(prototypeLess, null)
|
||||
expect(() => { direct[0]!.call(prototypeLess) }).toThrow('without a prototype')
|
||||
|
||||
class Service {
|
||||
run(): void {}
|
||||
}
|
||||
const conflicting: Array<(this: Service) => void> = []
|
||||
Remote(
|
||||
Reflect.get(Service.prototype, 'run'),
|
||||
methodContext('run', conflicting),
|
||||
)
|
||||
RemoteScope('metaFixture')(
|
||||
Reflect.get(Service.prototype, 'run'),
|
||||
methodContext('run', conflicting),
|
||||
)
|
||||
const service = new Service()
|
||||
conflicting[0]!.call(service)
|
||||
expect(() => { conflicting[1]!.call(service) }).toThrow('conflicting invocation markers')
|
||||
})
|
||||
|
||||
it('rejects ambiguous binding names', () => {
|
||||
expect(() => bindTypertRemote({}, '')).toThrow('service key')
|
||||
expect(() => bindTypertRemote({}, 'goals', { namespace: 'api/goals' })).toThrow('namespace')
|
||||
expect(() => bindTypertRemote({}, 'goals', { namespace: 'api goals' })).toThrow('namespace')
|
||||
})
|
||||
|
||||
it('admits only one-way event shapes and only selected events that exist', () => {
|
||||
expectTypeOf<'meta-fixture/forwardable'>().toExtend<TypertForwardableEvent>()
|
||||
expectTypeOf<'meta-fixture/scoped'>().not.toExtend<TypertForwardableEvent>()
|
||||
expectTypeOf<'meta-fixture/answered'>().not.toExtend<TypertForwardableEvent>()
|
||||
|
||||
expectTypeOf<'meta-fixture/forwardable'>().toExtend<TypertRemoteEvent>()
|
||||
expectTypeOf<'meta-fixture/scoped'>().not.toExtend<TypertRemoteEvent>()
|
||||
expectTypeOf<'meta-fixture/absent'>().not.toExtend<TypertRemoteEvent>()
|
||||
})
|
||||
})
|
||||
|
||||
function methodContext<This extends object>(
|
||||
name: string,
|
||||
initializers: Array<(this: This) => void>,
|
||||
): ClassMethodDecoratorContext<This, (this: This, ...args: unknown[]) => unknown> {
|
||||
return {
|
||||
kind: 'method',
|
||||
name,
|
||||
static: false,
|
||||
private: false,
|
||||
metadata: {},
|
||||
access: {
|
||||
has: object => name in object,
|
||||
get: object => (object as Record<string, unknown>)[name] as (this: This, ...args: unknown[]) => unknown,
|
||||
},
|
||||
addInitializer: (initializer) => { initializers.push(initializer) },
|
||||
}
|
||||
}
|
||||
21
packages/typert/protocol/tsconfig.json
Normal file
21
packages/typert/protocol/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user