@deepseek-ai/dsh-typert-protocol 0.1.6-alpha.2 → 0.1.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +7 -2
- package/README.zh.md +7 -2
- package/lib/index.js +66 -3
- package/lib/types/index.d.ts +8 -4
- package/lib/types/index.js +20 -2
- package/lib/types/json-value.d.ts +20 -0
- package/lib/types/json-value.js +61 -0
- package/lib/types/types.d.ts +126 -1
- package/package.json +6 -3
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/typert/protocol/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 8a97bb339d462c8dbea7666c7855a7e51ae9b113
|
|
6
|
+
README.zh.md: 9bb2cae765ee5747bf11a92d44ec2bb201b0285d
|
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ export class GoalService extends TypertRemoteService {
|
|
|
44
44
|
|
|
45
45
|
Generation turns the method into a wire endpoint under the service's namespace; Clients call it as a typed method through `ctx.remote` (see the [API Gateway reference](../../../docs/api-gateway.md)). A method opts into cooperative cancellation by declaring `signal: AbortSignal` as its final parameter — the signal is injected, never a JSON parameter or lookup field.
|
|
46
46
|
|
|
47
|
+
A unary method can return `Uint8Array` directly or within nested objects, arrays, tuples, optional fields, unions, and recursive types. Generation supplies optional result codec `encode()` and `decode()` functions: encoding visits only subtrees whose types can contain bytes, while decoding validates reconstructed values; Client declarations use `Uint8Array<ArrayBuffer>` at every byte position while retaining other field types. Pure JSON results pass through without Host byte detection or Client parsing. Parameters, events, and stream items remain JSON-only; runtime object cycles are unsupported.
|
|
48
|
+
|
|
49
|
+
A stream method (`@Remote({ mode: 'stream' })`) returns `Iterable`, `AsyncIterable`, or `RemoteStream<Out, In>`. `In` declares the items the Client may send back on the same logical stream; the method reads them through `this.ctx.invocation.uplink<In>()`, and the descriptor carries their codec. `RemoteInvocation` also names the receiving `service`, the calling `peer` (a `PeerScope` the connection layer admitted), and the carrier `signal`; `ctx.invocation` is `undefined` on a Context no Remote call derived: the first `bindTypertRemote()` binding in a tree, which every `TypertRemoteService` constructor makes, registers that accessor on the root. A generated Client stream method returns `RemoteStreamHandle<Out, In>`: the handle with `send`, `end`, and `dispose` beside the downlink iteration. Uplink items are validated one by one at the Host because they arrive from the browser; downlink items are values the Host method produced and pass through.
|
|
50
|
+
|
|
47
51
|
### Associating Host objects and Contexts with wire identities
|
|
48
52
|
|
|
49
53
|
Complex Host objects cannot cross the wire directly. A business package declares the association through the merge-extensible `TypertLookupMap` and `TypertContextMap`. A Host Context adapter owns the stable wire declaration and resolves wire identities to live Contexts. A Client Context adapter maps in both directions because scoped calls originate from a Client Context and forwarded Host events resolve their explicit wire identity there. Host composition may override its synchronous or asynchronous resolver. A resolver that refuses on policy grounds throws `RemoteError` with its own code, which reaches the caller unchanged.
|
|
@@ -89,7 +93,7 @@ The package keeps strict reflection in the compiler: decorator initializers reta
|
|
|
89
93
|
|
|
90
94
|
### Protocol maps and descriptors
|
|
91
95
|
|
|
92
|
-
The merge-extensible protocol maps keep static associations in the type system, while runtime providers register resolution with `ctx.typert`; the map names and shapes live in [`src/types.ts`](src/types.ts). `InvocationDescriptor` is the shared runtime form consumed by the registry, the Gateway, and the Client Remote, covering direct and Context receivers, JSON and lookup parameters, scope projections, cancellation, and result codecs.
|
|
96
|
+
The merge-extensible protocol maps keep static associations in the type system, while runtime providers register resolution with `ctx.typert`; the map names and shapes live in [`src/types.ts`](src/types.ts). `InvocationDescriptor` is the shared runtime form consumed by the registry, the Gateway, and the Client Remote, covering direct and Context receivers, JSON and lookup parameters, scope projections, the uplink codec, cancellation, and result codecs.
|
|
93
97
|
|
|
94
98
|
### Wire identity grammar
|
|
95
99
|
|
|
@@ -100,8 +104,9 @@ Every namespace, method, lookup, and Context segment must satisfy `isTypertRemot
|
|
|
100
104
|
| File | Role |
|
|
101
105
|
|---|---|
|
|
102
106
|
| [`src/index.ts`](src/index.ts) | Decorators, Gateway bindings, `remoteMethods`, segment validation |
|
|
107
|
+
| [`src/json-value.ts`](src/json-value.ts) | `isRemoteJsonValue` and `isRemoteUplinkItem`, the lossless JSON checks every carrier shares |
|
|
103
108
|
| [`src/remote-error.ts`](src/remote-error.ts) | `RemoteError` and the structural `remoteErrorOf` recognizer |
|
|
104
|
-
| [`src/types.ts`](src/types.ts) | Protocol maps, `RemoteErrorDetailsMap`, `RemoteResult`, `InvocationDescriptor`, codecs, provider contracts, registry interfaces, `TypertClientRemote` |
|
|
109
|
+
| [`src/types.ts`](src/types.ts) | Protocol maps, `RemoteErrorDetailsMap`, `RemoteResult`, `RemoteStream`, `RemoteStreamHandle`, `PeerScope`, `RemoteInvocation`, `InvocationDescriptor`, codecs, provider contracts, registry interfaces, `TypertClientRemote` |
|
|
105
110
|
| — | No runtime invariant companion is published; decorators retain private immutable declarations and bindings are frozen values with no independent event stream to cross-check. |
|
|
106
111
|
|
|
107
112
|
</details>
|
package/README.zh.md
CHANGED
|
@@ -44,6 +44,10 @@ export class GoalService extends TypertRemoteService {
|
|
|
44
44
|
|
|
45
45
|
生成会把方法变为服务命名空间下的 wire 端点;Client 通过 `ctx.remote` 以类型化方法调用它(见 [API Gateway 参考](../../../docs/api-gateway.zh.md))。方法把 `signal: AbortSignal` 声明为最后一个参数即可选择协作式取消——该信号是注入的,绝不会成为 JSON 参数或查找字段。
|
|
46
46
|
|
|
47
|
+
一元方法可以直接返回 `Uint8Array`,也可以将其放在嵌套对象、数组、元组、可选字段、联合类型和递归类型中。生成器提供可选的结果 codec `encode()` 和 `decode()`:编码仅访问类型可能包含字节的子树,解码则校验还原后的值;Client 声明在每个字节位置使用 `Uint8Array<ArrayBuffer>`,同时保留其他字段类型。纯 JSON 结果不经 Host 字节识别或 Client 解析直接传递。参数、事件与流条目仍仅支持 JSON;不支持运行时对象循环。
|
|
48
|
+
|
|
49
|
+
流方法(`@Remote({ mode: 'stream' })`)返回 `Iterable`、`AsyncIterable` 或 `RemoteStream<Out, In>`。`In` 声明 Client 可以在同一条逻辑流上回送的项;方法通过 `this.ctx.invocation.uplink<In>()` 读取它们,描述符携带其 codec。`RemoteInvocation` 还给出接收服务 `service`、发起调用的 `peer`(连接层接纳的一个 `PeerScope`)与载体 `signal`;非 Remote 调用派生的 Context 上 `ctx.invocation` 为 `undefined`:树中第一个 `bindTypertRemote()` 绑定(每个 `TypertRemoteService` 构造函数都会建立一个)在根上注册该 accessor。生成的 Client 流方法返回 `RemoteStreamHandle<Out, In>`:在下行迭代之外提供 `send`、`end` 与 `dispose` 的句柄。上行项在 Host 逐项校验,因为它们来自浏览器;下行项是 Host 方法产出的值,原样透传。
|
|
50
|
+
|
|
47
51
|
### 把 Host 对象与 Context 关联到 wire identity
|
|
48
52
|
|
|
49
53
|
复杂的 Host 对象不能直接跨 wire 传输。业务包通过可合并扩展的 `TypertLookupMap` 与 `TypertContextMap` 声明关联。Host Context 适配器拥有稳定 wire 声明,并把 wire identity 解析为活跃 Context。Client Context 适配器需要双向映射,因为作用域调用从 Client Context 发起,而转发的 Host 事件要在 Client 侧解析其显式 wire identity。Host 组合可以覆盖其同步或异步解析器。因策略原因拒绝解析的解析器会抛出带有自身错误码的 `RemoteError`,该码原样到达调用方。
|
|
@@ -89,7 +93,7 @@ Host 装配以转发给消费方的 Cordis 事件扩展 `TypertRemoteEventSelect
|
|
|
89
93
|
|
|
90
94
|
### 协议映射与描述符
|
|
91
95
|
|
|
92
|
-
可合并扩展的协议映射在类型系统中保留静态关联,运行时提供方则向 `ctx.typert` 注册解析;映射的名称与形状见 [`src/types.ts`](src/types.ts)。`InvocationDescriptor` 是注册表、Gateway 与 Client Remote 共同消费的共享运行时形式,涵盖直接与 Context 接收者、JSON
|
|
96
|
+
可合并扩展的协议映射在类型系统中保留静态关联,运行时提供方则向 `ctx.typert` 注册解析;映射的名称与形状见 [`src/types.ts`](src/types.ts)。`InvocationDescriptor` 是注册表、Gateway 与 Client Remote 共同消费的共享运行时形式,涵盖直接与 Context 接收者、JSON 与查找参数、作用域投影、上行编解码器、取消与结果编解码器。
|
|
93
97
|
|
|
94
98
|
### Wire 标识文法
|
|
95
99
|
|
|
@@ -100,8 +104,9 @@ Host 装配以转发给消费方的 Cordis 事件扩展 `TypertRemoteEventSelect
|
|
|
100
104
|
| 文件 | 职责 |
|
|
101
105
|
|---|---|
|
|
102
106
|
| [`src/index.ts`](src/index.ts) | 装饰器、Gateway 绑定、`remoteMethods`、段校验 |
|
|
107
|
+
| [`src/json-value.ts`](src/json-value.ts) | 各载体共享的无损 JSON 校验 `isRemoteJsonValue` 与 `isRemoteUplinkItem` |
|
|
103
108
|
| [`src/remote-error.ts`](src/remote-error.ts) | `RemoteError` 与结构式识别函数 `remoteErrorOf` |
|
|
104
|
-
| [`src/types.ts`](src/types.ts) | 协议映射、`RemoteErrorDetailsMap`、`RemoteResult`、`InvocationDescriptor`、编解码器、提供方约定、注册表接口、`TypertClientRemote` |
|
|
109
|
+
| [`src/types.ts`](src/types.ts) | 协议映射、`RemoteErrorDetailsMap`、`RemoteResult`、`RemoteStream`、`RemoteStreamHandle`、`PeerScope`、`RemoteInvocation`、`InvocationDescriptor`、编解码器、提供方约定、注册表接口、`TypertClientRemote` |
|
|
105
110
|
| — | 不发布运行时不变量伴生入口;decorator 只保留私有不可变声明,binding 也是冻结值,没有可供交叉核对的独立事件流。 |
|
|
106
111
|
|
|
107
112
|
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
1
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
2
2
|
//#region lib/types/remote-error.js
|
|
3
3
|
/** The one Remote failure class shared by owners, the Gateway, and consumers. */
|
|
4
4
|
/**
|
|
@@ -68,6 +68,54 @@ function isTypertOwnedValue(value) {
|
|
|
68
68
|
return typeof value === "object" && value !== null && TYPERT_OWNED_VALUE in value && value[TYPERT_OWNED_VALUE] === true;
|
|
69
69
|
}
|
|
70
70
|
//#endregion
|
|
71
|
+
//#region lib/types/json-value.js
|
|
72
|
+
/**
|
|
73
|
+
* Lossless JSON checks every Remote carrier shares: the Client handle before it
|
|
74
|
+
* queues an uplink item, the Gateway at its wire and codec-less uplink
|
|
75
|
+
* boundaries, and the in-process mock.
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* Test whether a value crosses JSON transport without coercion or omission.
|
|
79
|
+
* @param value - candidate boundary value.
|
|
80
|
+
* @returns whether the value is losslessly JSON-compatible.
|
|
81
|
+
*/
|
|
82
|
+
function isRemoteJsonValue(value) {
|
|
83
|
+
return visitJsonValue(value, /* @__PURE__ */ new Set());
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Test whether a value may travel as one uplink item: a lossless JSON value, or
|
|
87
|
+
* a top-level `undefined`, which the wire carries as an `item` frame without
|
|
88
|
+
* `value`. Nested `undefined`, `NaN`, and infinities stay rejected.
|
|
89
|
+
* @param value - candidate uplink item.
|
|
90
|
+
* @returns whether the item crosses every carrier unchanged.
|
|
91
|
+
*/
|
|
92
|
+
function isRemoteUplinkItem(value) {
|
|
93
|
+
return value === void 0 || isRemoteJsonValue(value);
|
|
94
|
+
}
|
|
95
|
+
function visitJsonValue(value, ancestors) {
|
|
96
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
97
|
+
if (typeof value === "number") return Number.isFinite(value) && !Object.is(value, -0);
|
|
98
|
+
if (typeof value !== "object") return false;
|
|
99
|
+
if (ancestors.has(value)) return false;
|
|
100
|
+
ancestors.add(value);
|
|
101
|
+
try {
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
if (Object.getPrototypeOf(value) !== Array.prototype || Reflect.ownKeys(value).length !== value.length + 1) return false;
|
|
104
|
+
for (let index = 0; index < value.length; index++) if (!Object.hasOwn(value, index) || !visitJsonValue(value[index], ancestors)) return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const prototype = Object.getPrototypeOf(value);
|
|
108
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
109
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
110
|
+
if (typeof key !== "string") return false;
|
|
111
|
+
if (Object.getOwnPropertyDescriptor(value, key)?.enumerable !== true || !visitJsonValue(Reflect.get(value, key), ancestors)) return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
} finally {
|
|
115
|
+
ancestors.delete(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
71
119
|
//#region lib/types/index.js
|
|
72
120
|
/**
|
|
73
121
|
* Remote decorators and explicit Gateway bindings backed by versioned
|
|
@@ -86,7 +134,10 @@ function isTypertRemoteSegment(value) {
|
|
|
86
134
|
}
|
|
87
135
|
const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
|
|
88
136
|
/**
|
|
89
|
-
* Bind one visible Service field to a Cordis key and Remote namespace.
|
|
137
|
+
* Bind one visible Service field to a Cordis key and Remote namespace. A
|
|
138
|
+
* service that owns a Cordis Context also gives its tree `ctx.invocation`,
|
|
139
|
+
* `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
|
|
140
|
+
* a Host composition to read it.
|
|
90
141
|
* @param service - owning Service instance, normally `this`.
|
|
91
142
|
* @param serviceKey - exact Cordis service key.
|
|
92
143
|
* @param options - optional distinct wire namespace.
|
|
@@ -96,6 +147,8 @@ function bindTypertRemote(service, serviceKey, options = {}) {
|
|
|
96
147
|
validateName("service key", serviceKey);
|
|
97
148
|
const namespace = options.namespace ?? serviceKey;
|
|
98
149
|
validateName("namespace", namespace);
|
|
150
|
+
const ctx = Reflect.get(service, "ctx");
|
|
151
|
+
if (ctx instanceof Context) provideInvocationAccessor(ctx);
|
|
99
152
|
return Object.freeze({
|
|
100
153
|
service,
|
|
101
154
|
serviceKey,
|
|
@@ -117,6 +170,16 @@ var TypertRemoteService = class extends Service {
|
|
|
117
170
|
this.typertRemote = bindTypertRemote(this, this.name, options);
|
|
118
171
|
}
|
|
119
172
|
};
|
|
173
|
+
/**
|
|
174
|
+
* Make `ctx.invocation` read as `undefined` outside a Remote call instead of the
|
|
175
|
+
* reflect service's "cannot get property" error; a call-derived Context shadows
|
|
176
|
+
* the accessor with its own property. The first Remote Service constructed in a
|
|
177
|
+
* tree registers it on the root, where it outlives any one Service.
|
|
178
|
+
*/
|
|
179
|
+
function provideInvocationAccessor(ctx) {
|
|
180
|
+
if (Object.hasOwn(ctx.root.reflect.props, "invocation")) return;
|
|
181
|
+
ctx.root.accessor("invocation", { get: () => void 0 });
|
|
182
|
+
}
|
|
120
183
|
function Remote(methodExportOrOptions, context) {
|
|
121
184
|
if (typeof methodExportOrOptions === "string") {
|
|
122
185
|
validateName("Remote export name", methodExportOrOptions);
|
|
@@ -212,4 +275,4 @@ function validateName(subject, value) {
|
|
|
212
275
|
if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
|
|
213
276
|
}
|
|
214
277
|
//#endregion
|
|
215
|
-
export { Remote, RemoteError, RemoteScope, TYPERT_OWNED_VALUE, TypertRemoteService, bindTypertRemote, isTypertOwnedValue, isTypertRemoteSegment, remoteErrorOf, remoteMethods, typertOwnedValue };
|
|
278
|
+
export { Remote, RemoteError, RemoteScope, TYPERT_OWNED_VALUE, TypertRemoteService, bindTypertRemote, isRemoteJsonValue, isRemoteUplinkItem, isTypertOwnedValue, isTypertRemoteSegment, remoteErrorOf, remoteMethods, typertOwnedValue };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
* remains a Typert compiler responsibility.
|
|
5
5
|
* @module @deepseek-ai/dsh-typert-protocol
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
8
8
|
import type { TypertContextMap } from './types.ts';
|
|
9
9
|
export { RemoteError, remoteErrorOf } from './remote-error.ts';
|
|
10
10
|
export { TYPERT_OWNED_VALUE, isTypertOwnedValue, typertOwnedValue } from './owned-value.ts';
|
|
11
11
|
export type { TypertOwnedValue } from './owned-value.ts';
|
|
12
|
+
export { isRemoteJsonValue, isRemoteUplinkItem } from './json-value.ts';
|
|
12
13
|
/**
|
|
13
14
|
* Test one generated Remote name against the Connection endpoint grammar.
|
|
14
15
|
* @param value - namespace, method, lookup, or Context segment.
|
|
15
16
|
* @returns whether the value can cross the shared RPC carrier unchanged.
|
|
16
17
|
*/
|
|
17
18
|
export declare function isTypertRemoteSegment(value: string): boolean;
|
|
18
|
-
export type { InvocationDescriptor, InvocationParameterDescriptor, InvocationSourceLocation, RemoteErrorCode, RemoteErrorDetailsMap, RemoteFailure, RemoteResult, TypertClientEventListener, TypertClientRemote, TypertClientContextAdapter, TypertCodec, TypertContext, TypertContextMap, TypertContextRegistry, TypertContextWire, TypertDisposer, TypertForwardableEvent, TypertForwardableEventEntry, TypertHostContextAdapter, 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';
|
|
19
|
+
export type { InvocationDescriptor, InvocationParameterDescriptor, InvocationSourceLocation, PeerId, PeerScope, RemoteErrorCode, RemoteErrorDetailsMap, RemoteFailure, RemoteInvocation, RemoteResult, RemoteStream, RemoteStreamHandle, TypertClientEventListener, TypertClientRemote, TypertClientContextAdapter, TypertCodec, TypertContext, TypertContextMap, TypertContextRegistry, TypertContextWire, TypertDisposer, TypertForwardableEvent, TypertForwardableEventEntry, TypertHostContextAdapter, 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';
|
|
19
20
|
/** Options for an explicit Service-to-Gateway binding. */
|
|
20
21
|
export interface TypertGatewayBindingOptions {
|
|
21
22
|
/** Wire namespace; defaults to the Cordis service key. */
|
|
@@ -46,12 +47,15 @@ export interface RemoteMethodMarker {
|
|
|
46
47
|
}
|
|
47
48
|
/** Options for a non-unary Remote method. */
|
|
48
49
|
export interface RemoteMethodOptions {
|
|
49
|
-
/**
|
|
50
|
+
/** `stream`: deliver each Iterable item over the shared logical-stream carrier. */
|
|
50
51
|
readonly mode: 'stream';
|
|
51
52
|
}
|
|
52
53
|
type RemoteMethodDecorator = <This extends object, Args extends unknown[], Result>(method: (this: This, ...args: Args) => Result, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>) => void;
|
|
53
54
|
/**
|
|
54
|
-
* Bind one visible Service field to a Cordis key and Remote namespace.
|
|
55
|
+
* Bind one visible Service field to a Cordis key and Remote namespace. A
|
|
56
|
+
* service that owns a Cordis Context also gives its tree `ctx.invocation`,
|
|
57
|
+
* `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
|
|
58
|
+
* a Host composition to read it.
|
|
55
59
|
* @param service - owning Service instance, normally `this`.
|
|
56
60
|
* @param serviceKey - exact Cordis service key.
|
|
57
61
|
* @param options - optional distinct wire namespace.
|
package/lib/types/index.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* remains a Typert compiler responsibility.
|
|
5
5
|
* @module @deepseek-ai/dsh-typert-protocol
|
|
6
6
|
*/
|
|
7
|
-
import { Service } from '@deepseek-ai/cordis';
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
8
8
|
export { RemoteError, remoteErrorOf } from "./remote-error.js";
|
|
9
9
|
export { TYPERT_OWNED_VALUE, isTypertOwnedValue, typertOwnedValue } from "./owned-value.js";
|
|
10
|
+
export { isRemoteJsonValue, isRemoteUplinkItem } from "./json-value.js";
|
|
10
11
|
const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
|
|
11
12
|
/**
|
|
12
13
|
* Test one generated Remote name against the Connection endpoint grammar.
|
|
@@ -18,7 +19,10 @@ export function isTypertRemoteSegment(value) {
|
|
|
18
19
|
}
|
|
19
20
|
const REMOTE_METHOD_DESCRIPTOR = '@deepseek-ai/dsh-typert-protocol/remote-methods';
|
|
20
21
|
/**
|
|
21
|
-
* Bind one visible Service field to a Cordis key and Remote namespace.
|
|
22
|
+
* Bind one visible Service field to a Cordis key and Remote namespace. A
|
|
23
|
+
* service that owns a Cordis Context also gives its tree `ctx.invocation`,
|
|
24
|
+
* `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
|
|
25
|
+
* a Host composition to read it.
|
|
22
26
|
* @param service - owning Service instance, normally `this`.
|
|
23
27
|
* @param serviceKey - exact Cordis service key.
|
|
24
28
|
* @param options - optional distinct wire namespace.
|
|
@@ -28,6 +32,9 @@ export function bindTypertRemote(service, serviceKey, options = {}) {
|
|
|
28
32
|
validateName('service key', serviceKey);
|
|
29
33
|
const namespace = options.namespace ?? serviceKey;
|
|
30
34
|
validateName('namespace', namespace);
|
|
35
|
+
const ctx = Reflect.get(service, 'ctx');
|
|
36
|
+
if (ctx instanceof Context)
|
|
37
|
+
provideInvocationAccessor(ctx);
|
|
31
38
|
return Object.freeze({ service, serviceKey, namespace });
|
|
32
39
|
}
|
|
33
40
|
/** Cordis Service base that exposes its registered name through Typert Gateway. */
|
|
@@ -45,6 +52,17 @@ export class TypertRemoteService extends Service {
|
|
|
45
52
|
this.typertRemote = bindTypertRemote(this, this.name, options);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Make `ctx.invocation` read as `undefined` outside a Remote call instead of the
|
|
57
|
+
* reflect service's "cannot get property" error; a call-derived Context shadows
|
|
58
|
+
* the accessor with its own property. The first Remote Service constructed in a
|
|
59
|
+
* tree registers it on the root, where it outlives any one Service.
|
|
60
|
+
*/
|
|
61
|
+
function provideInvocationAccessor(ctx) {
|
|
62
|
+
if (Object.hasOwn(ctx.root.reflect.props, 'invocation'))
|
|
63
|
+
return;
|
|
64
|
+
ctx.root.accessor('invocation', { get: () => undefined });
|
|
65
|
+
}
|
|
48
66
|
export function Remote(methodExportOrOptions, context) {
|
|
49
67
|
if (typeof methodExportOrOptions === 'string') {
|
|
50
68
|
validateName('Remote export name', methodExportOrOptions);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless JSON checks every Remote carrier shares: the Client handle before it
|
|
3
|
+
* queues an uplink item, the Gateway at its wire and codec-less uplink
|
|
4
|
+
* boundaries, and the in-process mock.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Test whether a value crosses JSON transport without coercion or omission.
|
|
8
|
+
* @param value - candidate boundary value.
|
|
9
|
+
* @returns whether the value is losslessly JSON-compatible.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isRemoteJsonValue(value: unknown): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Test whether a value may travel as one uplink item: a lossless JSON value, or
|
|
14
|
+
* a top-level `undefined`, which the wire carries as an `item` frame without
|
|
15
|
+
* `value`. Nested `undefined`, `NaN`, and infinities stay rejected.
|
|
16
|
+
* @param value - candidate uplink item.
|
|
17
|
+
* @returns whether the item crosses every carrier unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isRemoteUplinkItem(value: unknown): boolean;
|
|
20
|
+
//# sourceMappingURL=json-value.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless JSON checks every Remote carrier shares: the Client handle before it
|
|
3
|
+
* queues an uplink item, the Gateway at its wire and codec-less uplink
|
|
4
|
+
* boundaries, and the in-process mock.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Test whether a value crosses JSON transport without coercion or omission.
|
|
8
|
+
* @param value - candidate boundary value.
|
|
9
|
+
* @returns whether the value is losslessly JSON-compatible.
|
|
10
|
+
*/
|
|
11
|
+
export function isRemoteJsonValue(value) {
|
|
12
|
+
return visitJsonValue(value, new Set());
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Test whether a value may travel as one uplink item: a lossless JSON value, or
|
|
16
|
+
* a top-level `undefined`, which the wire carries as an `item` frame without
|
|
17
|
+
* `value`. Nested `undefined`, `NaN`, and infinities stay rejected.
|
|
18
|
+
* @param value - candidate uplink item.
|
|
19
|
+
* @returns whether the item crosses every carrier unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export function isRemoteUplinkItem(value) {
|
|
22
|
+
return value === undefined || isRemoteJsonValue(value);
|
|
23
|
+
}
|
|
24
|
+
function visitJsonValue(value, ancestors) {
|
|
25
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
26
|
+
return true;
|
|
27
|
+
if (typeof value === 'number')
|
|
28
|
+
return Number.isFinite(value) && !Object.is(value, -0);
|
|
29
|
+
if (typeof value !== 'object')
|
|
30
|
+
return false;
|
|
31
|
+
if (ancestors.has(value))
|
|
32
|
+
return false;
|
|
33
|
+
ancestors.add(value);
|
|
34
|
+
try {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
if (Object.getPrototypeOf(value) !== Array.prototype
|
|
37
|
+
|| Reflect.ownKeys(value).length !== value.length + 1)
|
|
38
|
+
return false;
|
|
39
|
+
for (let index = 0; index < value.length; index++) {
|
|
40
|
+
if (!Object.hasOwn(value, index) || !visitJsonValue(value[index], ancestors))
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const prototype = Object.getPrototypeOf(value);
|
|
46
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
47
|
+
return false;
|
|
48
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
49
|
+
if (typeof key !== 'string')
|
|
50
|
+
return false;
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
52
|
+
if (descriptor?.enumerable !== true || !visitJsonValue(Reflect.get(value, key), ancestors))
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
ancestors.delete(value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=json-value.js.map
|
package/lib/types/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module @deepseek-ai/dsh-typert-protocol/types
|
|
5
5
|
*/
|
|
6
6
|
import type { Context, Events } from '@deepseek-ai/cordis';
|
|
7
|
+
import type { Branded } from '@deepseek-ai/dsh-brand';
|
|
7
8
|
import type { TypertOwnedValue } from './owned-value.ts';
|
|
8
9
|
declare const LOOKUP_HOST: unique symbol;
|
|
9
10
|
declare const LOOKUP_WIRE: unique symbol;
|
|
@@ -70,6 +71,51 @@ export type RemoteResult<T> = {
|
|
|
70
71
|
readonly ok: false;
|
|
71
72
|
readonly error: RemoteFailure;
|
|
72
73
|
};
|
|
74
|
+
declare const STREAM_UPLINK: unique symbol;
|
|
75
|
+
/**
|
|
76
|
+
* One Remote stream as a Host method returns it: the items it yields to the
|
|
77
|
+
* Client, iterated as a plain `AsyncIterable<Out>`. `In` is the type of the
|
|
78
|
+
* items the Client may send back on the same logical stream, read through
|
|
79
|
+
* `RemoteInvocation.uplink()`; it is carried only as a type-level marker. The
|
|
80
|
+
* default `never` declares a method that reads none, and its descriptor
|
|
81
|
+
* carries no uplink codec. A generated Client stream method returns the same
|
|
82
|
+
* stream as a `RemoteStreamHandle<Out, In>`.
|
|
83
|
+
* @template Out - item type the Host method yields.
|
|
84
|
+
* @template In - item type the Client may send; `never` when the method reads none.
|
|
85
|
+
*/
|
|
86
|
+
export type RemoteStream<Out, In = never> = AsyncIterable<Out> & {
|
|
87
|
+
readonly [STREAM_UPLINK]?: In;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* One open Remote stream as the Client holds it: the downlink items as an
|
|
91
|
+
* `AsyncIterable`, plus the uplink and cancellation of the same logical
|
|
92
|
+
* stream. A generated Client stream method returns it, and calling that
|
|
93
|
+
* method opens the stream: a holder that neither iterates nor disposes the
|
|
94
|
+
* handle keeps the Host stream alive. A handle stands for one generation:
|
|
95
|
+
* when the carrier is lost, iteration fails with the carrier error and the
|
|
96
|
+
* handle is finished.
|
|
97
|
+
* @template Out - item type the Host method yields.
|
|
98
|
+
* @template In - item type the Client may send; `never` when the method reads none.
|
|
99
|
+
*/
|
|
100
|
+
export interface RemoteStreamHandle<Out, In> extends AsyncIterable<Out> {
|
|
101
|
+
/**
|
|
102
|
+
* Send one uplink item. Items sent before the stream has opened are queued
|
|
103
|
+
* and sent once the `open` frame is on the wire. A top-level `undefined`
|
|
104
|
+
* travels as an `item` frame without `value`.
|
|
105
|
+
* @param item - item the Host validates against the method's uplink codec.
|
|
106
|
+
* @throws {Error} when the item is not a lossless JSON value, when `end()`
|
|
107
|
+
* was called, or once the stream has terminated.
|
|
108
|
+
*/
|
|
109
|
+
send(item: In): void;
|
|
110
|
+
/** Half-close the uplink: the Host's `uplink()` iteration ends. Idempotent; ignored after termination. */
|
|
111
|
+
end(): void;
|
|
112
|
+
/**
|
|
113
|
+
* Cancel the logical stream: send `cancel` unless a terminal frame has
|
|
114
|
+
* arrived, and end the downlink iterator quietly. Breaking out of
|
|
115
|
+
* `for await` early does the same.
|
|
116
|
+
*/
|
|
117
|
+
dispose(): void;
|
|
118
|
+
}
|
|
73
119
|
/** Merge-extensible scoped Remote method signatures generated for consumers. */
|
|
74
120
|
export interface TypertRemoteScopeMap {
|
|
75
121
|
}
|
|
@@ -155,6 +201,19 @@ export type TypertCodec = {
|
|
|
155
201
|
readonly typeSymbol: string;
|
|
156
202
|
/** Materialize and return the process-realm schema on first boundary use. */
|
|
157
203
|
readonly create: () => TypertSchema;
|
|
204
|
+
/**
|
|
205
|
+
* Decode a unary result whose fields require type-specific handling.
|
|
206
|
+
* @param value - result reconstructed by the RPC carrier.
|
|
207
|
+
* @returns the validated result, retaining native byte views.
|
|
208
|
+
*/
|
|
209
|
+
readonly decode?: (value: unknown) => unknown;
|
|
210
|
+
/**
|
|
211
|
+
* Project typed binary fields into RPC result attachments.
|
|
212
|
+
* @param value - native unary result.
|
|
213
|
+
* @param writeBytes - records a byte view at its result-relative path and returns its JSON placeholder.
|
|
214
|
+
* @returns JSON metadata with untouched JSON subtrees retained.
|
|
215
|
+
*/
|
|
216
|
+
readonly encode?: (value: unknown, writeBytes: (bytes: Uint8Array, path: readonly (string | number)[]) => null) => unknown;
|
|
158
217
|
} | {
|
|
159
218
|
readonly mode: 'src-json';
|
|
160
219
|
};
|
|
@@ -191,7 +250,7 @@ export interface InvocationDescriptor {
|
|
|
191
250
|
readonly method: string;
|
|
192
251
|
/** Service member invoked when the exported method name is an alias. */
|
|
193
252
|
readonly implementation?: string;
|
|
194
|
-
/** Absent for unary calls; stream calls
|
|
253
|
+
/** Absent for unary calls; stream calls deliver every yielded item as the Host produced it. */
|
|
195
254
|
readonly mode?: 'stream';
|
|
196
255
|
/** Receiver selection mode. */
|
|
197
256
|
readonly invocation: {
|
|
@@ -211,6 +270,16 @@ export interface InvocationDescriptor {
|
|
|
211
270
|
};
|
|
212
271
|
/** Ordered business parameters. */
|
|
213
272
|
readonly parameters: readonly InvocationParameterDescriptor[];
|
|
273
|
+
/**
|
|
274
|
+
* Client-to-Host items of the same logical stream, generated from the `In`
|
|
275
|
+
* type argument of the method's `RemoteStream<Out, In>` return type; absent
|
|
276
|
+
* when `In` is `never`. The method reads the items through
|
|
277
|
+
* `RemoteInvocation.uplink()`, so nothing enters the parameter list.
|
|
278
|
+
*/
|
|
279
|
+
readonly uplink?: {
|
|
280
|
+
/** Codec validating every uplink item before `uplink()` delivers it. */
|
|
281
|
+
readonly codec: TypertCodec;
|
|
282
|
+
};
|
|
214
283
|
/** Transport cancellation injected after business parameters instead of entering wire args. */
|
|
215
284
|
readonly cancellation?: {
|
|
216
285
|
/** Reserved final Host method parameter. */
|
|
@@ -221,6 +290,56 @@ export interface InvocationDescriptor {
|
|
|
221
290
|
/** Source declaration used only for diagnostics. */
|
|
222
291
|
readonly sourceLocation?: InvocationSourceLocation;
|
|
223
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Opaque identity of one Peer: a party admitted to this Host by the connection
|
|
295
|
+
* layer. "Peer" is a connection-layer word; the browser application keeps the
|
|
296
|
+
* word "Client".
|
|
297
|
+
*/
|
|
298
|
+
export type PeerId = Branded<'PeerId'>;
|
|
299
|
+
/**
|
|
300
|
+
* One Peer's session on this Host. Connection owns it: `ctx` is the Cordis
|
|
301
|
+
* scope that owns connection-lifetime registrations and is disposed with the
|
|
302
|
+
* Peer. Who the Peer is and what it may do are not recorded here.
|
|
303
|
+
*/
|
|
304
|
+
export interface PeerScope {
|
|
305
|
+
readonly id: PeerId;
|
|
306
|
+
readonly ctx: Context;
|
|
307
|
+
/**
|
|
308
|
+
* Tear down every registration made through `ctx`.
|
|
309
|
+
* @returns settles once the scope has quiesced; racing calls share one completion.
|
|
310
|
+
*/
|
|
311
|
+
dispose(): Promise<void>;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The context of one Remote call, reachable inside the receiving method as
|
|
315
|
+
* `this.ctx.invocation`. The Gateway derives the receiver from a Context that
|
|
316
|
+
* carries it, so no parameter is injected and nothing crosses the wire.
|
|
317
|
+
*/
|
|
318
|
+
export interface RemoteInvocation {
|
|
319
|
+
readonly request: {
|
|
320
|
+
readonly namespace: string;
|
|
321
|
+
readonly method: string;
|
|
322
|
+
readonly args: Readonly<Record<string, unknown>>;
|
|
323
|
+
};
|
|
324
|
+
/** Cordis service key of the receiving Service. */
|
|
325
|
+
readonly service: string;
|
|
326
|
+
/** Peer the call speaks for; an in-process carrier speaks for the operator. */
|
|
327
|
+
readonly peer: PeerScope;
|
|
328
|
+
/** Carrier cancellation: Client cancel, socket close, or an uplink failure. */
|
|
329
|
+
readonly signal: AbortSignal;
|
|
330
|
+
/**
|
|
331
|
+
* The Client's uplink items for this call. Available once; a second call
|
|
332
|
+
* throws. With an uplink codec on the descriptor every item is decoded to
|
|
333
|
+
* `In`; without one items arrive as `unknown` after a JSON-safety check.
|
|
334
|
+
* Iteration ends when the Client ends its uplink; when the method finishes
|
|
335
|
+
* its downlink the Gateway calls the iterator's `return()` and unread items
|
|
336
|
+
* are dropped. `In` is the caller's assertion: the runtime decodes by the
|
|
337
|
+
* descriptor and does not cross-check it.
|
|
338
|
+
* @template In - item type the caller reads; the descriptor codec decides what arrives.
|
|
339
|
+
* @returns the single-consumer uplink iterable.
|
|
340
|
+
*/
|
|
341
|
+
uplink<In = unknown>(): AsyncIterable<In>;
|
|
342
|
+
}
|
|
224
343
|
/** Generated Host contract selected explicitly by a Client assembly. */
|
|
225
344
|
export interface TypertRemoteContribution {
|
|
226
345
|
/** npm package that owns the Remote methods. */
|
|
@@ -453,6 +572,12 @@ export interface TypertRegistryContract {
|
|
|
453
572
|
declare module '@deepseek-ai/cordis' {
|
|
454
573
|
interface Context {
|
|
455
574
|
typert: TypertRegistryContract;
|
|
575
|
+
/**
|
|
576
|
+
* The Remote call this Context was derived for, or `undefined` on a
|
|
577
|
+
* Context no Remote call derived. A Service method reads it as
|
|
578
|
+
* `this.ctx.invocation`.
|
|
579
|
+
*/
|
|
580
|
+
readonly invocation: RemoteInvocation | undefined;
|
|
456
581
|
}
|
|
457
582
|
}
|
|
458
583
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-typert-protocol",
|
|
3
3
|
"description": "Compiler-independent Remote metadata and Typert provider protocols",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
"lib/types/**/*.d.ts"
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-brand": "0.1.7-alpha.2"
|
|
36
|
+
},
|
|
34
37
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/cordis": "
|
|
38
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "
|
|
41
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
39
42
|
}
|
|
40
43
|
}
|