@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 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: e41fa9fa305d8f55ce55d3eace647ad5afcea0e3
6
- README.zh.md: e784e771016ebec0913e0ea847258fe653ae3bd0
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 };
@@ -4,18 +4,19 @@
4
4
  * remains a Typert compiler responsibility.
5
5
  * @module @deepseek-ai/dsh-typert-protocol
6
6
  */
7
- import { Service, type Context } from '@deepseek-ai/cordis';
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
- /** Deliver each Iterable item over the shared logical-stream carrier. */
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.
@@ -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
@@ -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 validate and deliver every yielded item. */
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.6-alpha.2",
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": "^4.0.2"
38
+ "@deepseek-ai/cordis": "~4.0.4"
36
39
  },
37
40
  "devDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.2"
41
+ "@deepseek-ai/cordis": "~4.0.4"
39
42
  }
40
43
  }