feat: add TypeRT remote gateway infrastructure
This commit is contained in:
6
packages/typert/type-meta/README.i18n.yaml
Normal file
6
packages/typert/type-meta/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/type-meta/README.md
|
||||
README.md: 9dd8dadd07b219c7471c8851262958d4d9e96a43
|
||||
README.zh.md: 5716f56d988c6d2dd9cd237346c3b02ec9ae7c4e
|
||||
33
packages/typert/type-meta/README.md
Normal file
33
packages/typert/type-meta/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# @deepseek-ai/dsh-type-meta
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
- `remoteMethods(service)` returns a detached declaration-order snapshot used by the Gateway's SRC fallback.
|
||||
|
||||
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.
|
||||
|
||||
## TypeRT protocol
|
||||
|
||||
Business packages extend `TypeRTLookupMap` and `TypeRTContextMap` to associate Host objects or scoped Contexts with their wire identities. Generated artifacts extend `TypeRTRemoteMap`, `TypeRTRemoteContextMap`, and `TypeRTRemoteNamespaceMap` so Client imports expose only selected Remote methods. `InvocationDescriptor` is the shared runtime form consumed by the registry, Gateway, and Client API.
|
||||
|
||||
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`. 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 no model surface.
|
||||
|
||||
#### 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.
|
||||
33
packages/typert/type-meta/README.zh.md
Normal file
33
packages/typert/type-meta/README.zh.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# @deepseek-ai/dsh-type-meta
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
该包提供不依赖编译器的声明,由业务包、生成的 TypeRT 产物、Host Gateway 和 Client API 共享。它负责 Remote 装饰器、显式服务绑定、可通过声明合并扩展的协议映射、调用描述符、编解码器和提供方契约;它不执行 TypeScript 分析,也不提供 Cordis 服务。
|
||||
|
||||
## Remote 声明
|
||||
|
||||
- `@Remote` 将公开实例方法标记为可在其注册的 Cordis 服务上直接调用。
|
||||
- `@RemoteContext(key)` 标记接收者选自合并声明的作用域 Context 类型的方法。
|
||||
- `bindTypeRTGateway(this, serviceKey, options?)` 在服务实例、其准确的 Cordis key 与协议命名空间之间创建可见且冻结的绑定。
|
||||
- `remoteMethods(service)` 返回按声明顺序排列、与内部状态分离的快照,供 Gateway 的 SRC 回退路径使用。
|
||||
|
||||
装饰器初始化器将标记保存在以服务 prototype 为键的模块私有 `WeakMap` 中。它们不会在构造函数上添加 symbol,也不会添加 prototype 属性、参数元数据或运行时反射字段。服务通过自身的 `typertGateway` 绑定字段显式接入。
|
||||
|
||||
## TypeRT 协议
|
||||
|
||||
业务包扩展 `TypeRTLookupMap` 和 `TypeRTContextMap`,以关联 Host 对象或作用域 Context 与其协议身份。生成的产物扩展 `TypeRTRemoteMap`、`TypeRTRemoteContextMap` 和 `TypeRTRemoteNamespaceMap`,使 Client 导入后仅暴露选定的 Remote 方法。`InvocationDescriptor` 是供注册表、Gateway 和 Client API 使用的共享运行时形式。
|
||||
|
||||
查找包与 Context 包同时负责其契约的两侧:声明合并提供静态关联,运行时提供方则向 `ctx.typert` 注册身份解析。严格编解码器携带生成的 schema;`src-json` 编解码器标识约束更弱的源码启动路径。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无,因为该协议包声明应用反射,不注册任何模型接口。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- 装饰器标记仅包含方法名,以及直接调用或 Context 调用模式。参数、结果、查找和 schema 反射需要 TypeRT 构建流水线。
|
||||
- Remote 装饰器只接受具有字符串名称的公开、非静态实例方法。SRC 执行无法表示重载签名,以及包含解构参数、默认参数或剩余参数的方法签名。
|
||||
42
packages/typert/type-meta/package.json
Normal file
42
packages/typert/type-meta/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-type-meta",
|
||||
"description": "Compiler-independent Remote metadata and TypeRT provider protocols",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"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",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
223
packages/typert/type-meta/src/index.ts
Normal file
223
packages/typert/type-meta/src/index.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Remote decorators and explicit Gateway bindings backed only by private
|
||||
* module state. Strict reflection remains a TypeRT compiler responsibility.
|
||||
* @module @deepseek-ai/dsh-type-meta
|
||||
*/
|
||||
|
||||
import type { TypeRTContextMap } from './types.ts'
|
||||
|
||||
export type {
|
||||
InvocationDescriptor,
|
||||
InvocationParameterDescriptor,
|
||||
InvocationSourceLocation,
|
||||
TypeRTClientContextBinder,
|
||||
TypeRTCodec,
|
||||
TypeRTContext,
|
||||
TypeRTContextMap,
|
||||
TypeRTContextRegistry,
|
||||
TypeRTContextWire,
|
||||
TypeRTDisposer,
|
||||
TypeRTHostContextProvider,
|
||||
TypeRTLocalRegistry,
|
||||
TypeRTLookup,
|
||||
TypeRTLookupHost,
|
||||
TypeRTLookupMap,
|
||||
TypeRTLookupProvider,
|
||||
TypeRTLookupRegistry,
|
||||
TypeRTLookupWire,
|
||||
TypeRTRemoteContextApi,
|
||||
TypeRTRemoteContextMap,
|
||||
TypeRTRemoteContextNamespace,
|
||||
TypeRTRemoteContribution,
|
||||
TypeRTRemoteMap,
|
||||
TypeRTRemoteNamespace,
|
||||
TypeRTRemoteNamespaceMap,
|
||||
TypeRTRemoteRegistry,
|
||||
TypeRTRegistryChange,
|
||||
TypeRTRegistryListener,
|
||||
TypeRTSchema,
|
||||
TypeRTService,
|
||||
} 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 bindTypeRTGateway<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 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('type-meta: Remote decorator context is missing')
|
||||
addMarkerInitializer(context, { kind: 'direct' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a decorator for a method resolved from one scoped Remote Context.
|
||||
* @param key - merge-declared Context key.
|
||||
* @param exportName - optional Remote export name; defaults to the method name.
|
||||
* @returns a standard method decorator that records only private module state.
|
||||
*/
|
||||
export function RemoteContext(
|
||||
key: Extract<keyof TypeRTContextMap, string>,
|
||||
exportName?: string,
|
||||
): RemoteMethodDecorator {
|
||||
validateName('Context 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('type-meta: 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(`type-meta: 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(`type-meta: 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 (value.length === 0 || value.includes('/')) {
|
||||
throw new TypeError(`type-meta: ${subject} must be nonempty and must not contain "/"`)
|
||||
}
|
||||
}
|
||||
30
packages/typert/type-meta/src/invariant.ts
Normal file
30
packages/typert/type-meta/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-type-meta`.
|
||||
* @module @deepseek-ai/dsh-type-meta/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-type-meta'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'type-meta-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 */
|
||||
358
packages/typert/type-meta/src/types.ts
Normal file
358
packages/typert/type-meta/src/types.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Compiler-independent TypeRT protocol shared by business packages, generated
|
||||
* Remote artifacts, the Host Gateway, and Client API implementations.
|
||||
* @module @deepseek-ai/dsh-type-meta/types
|
||||
*/
|
||||
|
||||
import type { Context } from '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 {}
|
||||
|
||||
/** Merge-extensible scoped Remote method signatures generated for consumers. */
|
||||
export interface TypeRTRemoteContextMap {}
|
||||
|
||||
/**
|
||||
* 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 TypeRTRemoteContextNamespace<
|
||||
Namespace extends string,
|
||||
ContextKey extends string = string,
|
||||
> = {
|
||||
[Endpoint in keyof TypeRTRemoteContextMap as Endpoint extends `${ContextKey}:${Namespace}/${infer Method}`
|
||||
? Method
|
||||
: never]: TypeRTRemoteContextMap[Endpoint]
|
||||
}
|
||||
|
||||
type TypeRTRemoteContextNamespaceKey<
|
||||
ContextKey extends string,
|
||||
Endpoint = keyof TypeRTRemoteContextMap,
|
||||
> = Endpoint extends `${ContextKey}:${infer Namespace}/${string}` ? Namespace : never
|
||||
|
||||
/** Generated scoped Remote namespaces available to one Context kind. */
|
||||
export type TypeRTRemoteContextApi<ContextKey extends string> = {
|
||||
[Namespace in TypeRTRemoteContextNamespaceKey<ContextKey>]:
|
||||
TypeRTRemoteContextNamespace<Namespace, ContextKey>
|
||||
}
|
||||
|
||||
/** Merge-extensible direct namespace surface generated for Client API 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
|
||||
}
|
||||
|
||||
/** 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[]
|
||||
/** 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[]
|
||||
}
|
||||
|
||||
/** Runtime resolver 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 to the current live Host object.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the live object, or `undefined` when it is unavailable.
|
||||
*/
|
||||
resolve(id: Wire): Host | undefined
|
||||
}
|
||||
|
||||
/** Host resolver for one scoped Remote Context 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
|
||||
}
|
||||
|
||||
/** 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
|
||||
/**
|
||||
* 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 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
|
||||
/**
|
||||
* 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 TypeRTService {
|
||||
readonly local: TypeRTLocalRegistry
|
||||
readonly remotes: TypeRTRemoteRegistry
|
||||
readonly lookups: TypeRTLookupRegistry
|
||||
readonly contexts: TypeRTContextRegistry
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
typert: TypeRTService
|
||||
}
|
||||
}
|
||||
29
packages/typert/type-meta/tests/fixtures/source-launch.ts
vendored
Normal file
29
packages/typert/type-meta/tests/fixtures/source-launch.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
bindTypeRTGateway,
|
||||
Remote,
|
||||
RemoteContext,
|
||||
remoteMethods,
|
||||
} from '@deepseek-ai/dsh-type-meta'
|
||||
|
||||
class Goals {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
|
||||
@Remote
|
||||
create(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
@RemoteContext('agent')
|
||||
scoped(value: string): string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const methods = remoteMethods(new Goals())
|
||||
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)
|
||||
132
packages/typert/type-meta/tests/type-meta.spec.ts
Normal file
132
packages/typert/type-meta/tests/type-meta.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
bindTypeRTGateway,
|
||||
Remote,
|
||||
RemoteContext,
|
||||
remoteMethods,
|
||||
type TypeRTContext,
|
||||
} from '@deepseek-ai/dsh-type-meta'
|
||||
|
||||
declare module '@deepseek-ai/dsh-type-meta' {
|
||||
interface TypeRTContextMap {
|
||||
metaFixture: TypeRTContext<string>
|
||||
}
|
||||
}
|
||||
|
||||
describe('type-meta Remote declarations', () => {
|
||||
it('executes standard decorator syntax through the Vitest source transform', () => {
|
||||
class Goals {
|
||||
readonly typertGateway = bindTypeRTGateway(this, 'goals')
|
||||
|
||||
@Remote
|
||||
create(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
@RemoteContext('metaFixture')
|
||||
scoped(value: string): string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const goals = new Goals()
|
||||
expect(remoteMethods(goals)).toEqual([
|
||||
{ method: 'create', invocation: { kind: 'direct' } },
|
||||
{ method: 'scoped', invocation: { kind: 'context', context: 'metaFixture' } },
|
||||
])
|
||||
})
|
||||
|
||||
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 typertGateway = bindTypeRTGateway(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),
|
||||
)
|
||||
RemoteContext('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.typertGateway).toEqual({ service: goals, serviceKey: 'goals', namespace: 'goals' })
|
||||
expect(Object.isFrozen(goals.typertGateway)).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('rejects ambiguous binding names', () => {
|
||||
expect(() => bindTypeRTGateway({}, '')).toThrow('service key')
|
||||
expect(() => bindTypeRTGateway({}, 'goals', { namespace: 'api/goals' })).toThrow('namespace')
|
||||
})
|
||||
})
|
||||
|
||||
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/type-meta/tsconfig.json
Normal file
21
packages/typert/type-meta/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": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user