@qilin-agent/typert-protocol 3.1.4 → 3.1.6

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
@@ -18,8 +18,11 @@
18
18
  en: c479b5b665822198
19
19
  zh: 1ffb789c18043baf
20
20
  /qilin-agent-typert-protocol/use-this-package/exposing-a-host-method:
21
- en: c01079b561c897c8
22
- zh: b06c90b647740d2c
21
+ en: 679b5a511a6d0ae5
22
+ zh: d59effcb46f842e3
23
+ /qilin-agent-typert-protocol/use-this-package/identifying-the-caller-and-admitting-non-owner-callers:
24
+ en: 54595a1d13856382
25
+ zh: cb9a2bf60fe9b8af
23
26
  /qilin-agent-typert-protocol/use-this-package/associating-host-objects-and-contexts-with-wire-identities:
24
27
  en: 3db384ae6f215632
25
28
  zh: 56207184b09d4018
@@ -36,17 +39,17 @@
36
39
  en: 4f45bb46a1920984
37
40
  zh: 70408d3b59d2735a
38
41
  /qilin-agent-typert-protocol/understand-the-implementation/remote-markers:
39
- en: 93b83a9441a5a3c2
40
- zh: c4eb143ddd167a27
42
+ en: 082530713c5cea83
43
+ zh: 0dc20fe54eb8846a
41
44
  /qilin-agent-typert-protocol/understand-the-implementation/protocol-maps-and-descriptors:
42
- en: 164b30b3fa543936
43
- zh: 0831edd3e04a3d3a
45
+ en: c202904201402211
46
+ zh: e77142e26a46eeac
44
47
  /qilin-agent-typert-protocol/understand-the-implementation/wire-identity-grammar:
45
48
  en: aac79a7b4682371e
46
49
  zh: bc9cc513c1ced3e6
47
50
  /qilin-agent-typert-protocol/understand-the-implementation/source-map:
48
- en: 62da405f16379f39
49
- zh: fcbd0f10dd198018
51
+ en: 74fdb78bf4e7753b
52
+ zh: ab705ae046ccaa5b
50
53
  /qilin-agent-typert-protocol/further-exploration:
51
54
  en: 464aab7c939e064c
52
55
  zh: 47167dc0ac8a47e2
@@ -57,8 +60,8 @@
57
60
  en: b1c49d5e440d9d41
58
61
  zh: 87ead78472415189
59
62
  /qilin-agent-typert-protocol/known-limitations-and-deferred-work:
60
- en: 0be59e6d97923487
61
- zh: 70523fb0dc567ed6
63
+ en: 32a7ede4de20ad4e
64
+ zh: abc3eeade40b0d1c
62
65
  /qilin-agent-typert-protocol/known-limitations-and-deferred-work/dev-note:
63
66
  en: e67f633dc6fe7773
64
67
  zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -44,7 +44,21 @@ 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 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.
47
+ 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` (the `PeerScope` its carrier names), 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. Kylin's cross-copy `Context.is()` recognizes a source Context passed to a built Service, so mixed source/built compositions retain this accessor and invocation-local principals. 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.
48
+
49
+ `@Remote` options contain only `name`, `mode`, and the `resources` readers described below; input policy belongs to the consumer, not a method annotation. `method.input(options?: TypertRemoteInputOptions)` returns a cached `TypertRemoteValue<Input>`, with `strict?: boolean` as its only option. `method.input()` and `{ strict: false }` retain ordinary Remote handling: declared objects accept and strip unknown fields. `{ strict: true }` rejects undeclared fields recursively, while explicit JSON values and dictionary keys stay open and dictionary values retain their declared validation. Both named-argument envelopes reject extra arguments. Select one view (`const input = method.input({ strict: true })`) for matching `input.create().parse(raw)` and `input.jsonSchema`; this does not change Host dispatch. The [generator](<../generator/README.md#ordinary-remote-metadata>) owns publication and the paired codecs.
50
+
51
+ Ordinary `./remote` artifacts export `TYPERT_REMOTE` as a `TypertRemoteInfo`, extending the mountable `TypertRemoteContribution`. Its typed `methods[endpoint]` entries are the same direct descriptor objects found in `descriptors`, with derived `input(options?)` and `invoke(remote, input, signal?)`. Parameter documentation lives in `parameters[].description`; `result` is the single result codec, parser, and optional projection. Unary descriptors omit `mode`; streams use `'stream'`. Context-only receivers remain in `descriptors` and scoped declarations, not `methods`.
52
+
53
+ `createTypertRemoteInfo(spec, z)` from the published pure `@qilin-agent/typert-protocol/remote-info` entry returns a class instance, not a constructor. Construction retains only the static spec and the caller-supplied Zod runtime; the first `descriptors` or `methods` read builds and caches their shared index. Each input-policy view, its parser, and its optional JSON Schema projection are independently lazy and cached; `method.input().create()` requests the ordinary parser. The raw spec requires Zod factories, while consumer-facing `TypertSchema<T>` promises only `parse(value: unknown): T`. JSON Schema comes from generator projections, not runtime `z.toJSONSchema()`. The leaf has no runtime imports; generated artifacts supply Zod. Metadata and schema access never invokes the network; the root also re-exports the factory but imports Kylin.
54
+
55
+ The invoker takes a caller-owned, mounted, unscoped Client Remote and resolves its namespace method at call time. It preserves the namespace receiver, optional positional holes, and signal, forwarding the same `Promise<RemoteResult<DecodedOutput>>` or complete `RemoteStreamHandle<Output, Uplink>` without an async wrapper. `DecodedOutput` defaults to the authored output; generation supplies `RemoteDecoded<Output>` only for byte-bearing unary methods, avoiding recursive remapping of ordinary JSON results; `result.create().parse()` retains the authored output type and does not normalize shared byte buffers. Documentation and parsing remain available when either input variant or the result lacks JSON Schema support; each variant's `jsonSchemaError` explains its absent projection. Registry readers use `isTypertRemoteMethod(descriptor)` from the same pure entry to narrow a base descriptor to these direct capabilities.
56
+
57
+ ### Identifying the caller and admitting non-owner callers
58
+
59
+ `PeerScope.principal` is the caller identity that in-process Host code fixes when it creates the Peer; no wire field can set it, and `PeerScope.signal` aborts when the Peer scope ends. `PrincipalMap` is merge-extensible: each driver declares one JSON entry with a unique literal `kind` and no credential, because a principal can be persisted in a Session log; the built-in entry is `owner: { kind: 'owner' }`. A Remote method that records its caller on durable output calls `principalOf(this.ctx.invocation)`, which returns the principal of a non-owner caller and `undefined` for the owner and outside a Remote call, and writes the field only when it is defined.
60
+
61
+ Every Remote method sits under access control: a non-owner caller reaches it only when the Remote-access layer admits the call on the resources it touches. `TypertResourceMap` is merge-extensible beside `TypertLookupMap`: the package that owns an identity type declares its resource kind as `TypertResource<Id>` (the Session package declares `session: TypertResource<SessionId>`, the Workspace package declares `workspace: TypertResource<WorkspaceId>`). A `RemoteAccessResource` is either the built-in identity-less `{ kind: 'global' }` or one declared kind with its identity; resources are independent, so access to `global` implies nothing about any session. At runtime the domain that owns a kind registers its default wire field through `typert.resources.register(kind, { field })`, which rejects `global`; a lookup provider and a Host Context adapter name the kind their wire identity carries through `resource`, so a method with an Agent lookup parameter or an Agent-scoped receiver carries a `session` resource without options. The `resources` option of `@Remote` and `@RemoteScope` maps a kind to a reader that receives the method's wire arguments as its own parameters; their types are inferred from the decorated method, with lookup parameters mapped to their wire types, so `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` reads a field the defaults cannot. A call whose readers and defaults name no resource touches `global`; they must name every resource the method accesses, because access control never checks one they omit. A forwarded `emit` entry of `TypertForwardableEventEntry` declares `resources` readers typed by its own event's parameters, and an entry without them touches `global`. Resource resolution belongs to the API Gateway; decisions and refusal reasons belong to the Remote-access assembly that completes this resource wave.
48
62
 
49
63
  ### Associating Host objects and Contexts with wire identities
50
64
 
@@ -87,11 +101,15 @@ The package keeps strict reflection in the compiler: decorator initializers reta
87
101
 
88
102
  ### Remote markers
89
103
 
90
- `@Remote` and `@RemoteScope` schedule an initializer that appends the method name, an optional export name, and the invocation mode to the prototype descriptor; `remoteMethods(service)` validates its version and returns a detached declaration-order snapshot that the Gateway's source-mode fallback reads. Markers require public, non-static instance methods with string names, and conflicting markers on one method are rejected.
104
+ `@Remote` and `@RemoteScope` schedule an initializer that appends the method name, an optional export name, the invocation mode, and the frozen `resources` readers to the prototype descriptor; `remoteMethods(service)` validates its version and returns a detached declaration-order snapshot that the Gateway's source-mode fallback reads. Markers require public, non-static instance methods with string names, and conflicting markers on one method are rejected.
105
+
106
+ Readers are validated when the decorator is created and frozen into one record per decorator application; the Gateway reads them from `remoteMethods(service)` by implementation member name, so another installed copy of the protocol package reads the same record.
91
107
 
92
108
  ### Protocol maps and descriptors
93
109
 
94
- 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.
110
+ 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, result codecs, and documentation. Generated `TypertRemoteMethod` objects extend this descriptor directly.
111
+
112
+ `RemoteInvocation.uplinkWindow` optionally describes the carrier's complete-frame inbox capacity and the UTF-8 envelope cost added to compact JSON items. Its absence means the carrier has no encoded-frame inbox limit; consumers still own their local buffering and acknowledgement policy.
95
113
 
96
114
  ### Wire identity grammar
97
115
 
@@ -102,9 +120,12 @@ Every namespace, method, lookup, and Context segment must satisfy `isTypertRemot
102
120
  | File | Role |
103
121
  |---|---|
104
122
  | [`src/index.ts`](src/index.ts) | Decorators, Gateway bindings, `remoteMethods`, segment validation |
123
+ | [`src/principal.ts`](src/principal.ts) | `principalOf`, the caller principal a Remote-call producer records |
124
+ | [`src/remote-access.ts`](src/remote-access.ts) | `RemoteAccessResource`, `RemoteAccessResourceKind`, and `RemoteAccessResourceOf` over `TypertResourceMap` |
105
125
  | [`src/json-value.ts`](src/json-value.ts) | `isRemoteJsonValue` and `isRemoteUplinkItem`, the lossless JSON checks every carrier shares |
106
126
  | [`src/remote-error.ts`](src/remote-error.ts) | `RemoteError` and the structural `remoteErrorOf` recognizer |
107
- | [`src/types.ts`](src/types.ts) | Protocol maps, `RemoteErrorDetailsMap`, `RemoteResult`, `RemoteStream`, `RemoteStreamHandle`, `PeerScope`, `RemoteInvocation`, `InvocationDescriptor`, codecs, provider contracts, registry interfaces, `TypertClientRemote` |
127
+ | [`src/remote-info.ts`](src/remote-info.ts) | Pure lazy info factory and direct-method type guard |
128
+ | [`src/types.ts`](src/types.ts) | Protocol maps, `RemoteErrorDetailsMap`, `RemoteResult`, `RemoteStream`, `RemoteStreamHandle`, `PrincipalMap`, `Principal`, `PeerScope`, `RemoteInvocation`, `InvocationDescriptor`, codecs, provider contracts, registry interfaces, `TypertClientRemote` |
108
129
 
109
130
  </details>
110
131
 
@@ -139,7 +160,7 @@ No direct effect; the declared contracts reach a request only when an assembly p
139
160
 
140
161
  These limits define what the declarations can represent; they are current package constraints, not a task backlog.
141
162
 
142
- - **Decorator markers are minimal** — markers contain only the method name and the direct or Context invocation mode; parameter, result, lookup, and schema reflection require the Typert build pipeline.
163
+ - **Decorator markers are minimal** — markers contain the method name, optional export alias, stream mode, resource readers, and direct or Context receiver selection; parameter, result, lookup, schema, and documentation metadata require the Typert build pipeline.
143
164
  - **Remote signatures are restricted** — decorators accept only public, non-static instance methods with string names, and source-mode execution cannot represent overloaded, destructured, defaulted, or rest-parameter signatures.
144
165
 
145
166
  <a id="dev-note"></a>
package/README.zh.md CHANGED
@@ -44,7 +44,21 @@ 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
- 流方法(`@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 方法产出的值,原样透传。
47
+ 流方法(`@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。Kylin 的跨副本 `Context.is()` 能识别传给已构建 Service 的源码 Context,因此源码/构建混合组合也能保留该 accessor 与调用局部的 principal。生成的 Client 流方法返回 `RemoteStreamHandle<Out, In>`:在下行迭代之外提供 `send`、`end` 与 `dispose` 的句柄。上行项在 Host 逐项校验,因为它们来自浏览器;下行项是 Host 方法产出的值,原样透传。
48
+
49
+ `@Remote` options 只包含 `name`、`mode` 与下文说明的 `resources` 读取函数;输入策略属于消费方,不是方法注解。`method.input(options?: TypertRemoteInputOptions)` 返回缓存的 `TypertRemoteValue<Input>`,唯一选项是 `strict?: boolean`。`method.input()` 与 `{ strict: false }` 保留普通 Remote 处理:声明的对象接受并剥离未声明字段。`{ strict: true }` 递归拒绝未声明字段,而显式 JSON 值与字典键保持开放,字典值保留其已声明的校验。两种具名参数信封都拒绝多余实参。选定一个视图(`const input = method.input({ strict: true })`)即可获得配套的 `input.create().parse(raw)` 与 `input.jsonSchema`;这不会改变 Host 分派。发布与成对 codec 由[生成器](<../generator/README.zh.md#ordinary-remote-metadata>)负责。
50
+
51
+ 普通 `./remote` 产物把 `TYPERT_REMOTE` 导出为 `TypertRemoteInfo`,扩展可挂载的 `TypertRemoteContribution`。其带类型的 `methods[endpoint]` 条目与 `descriptors` 里的直接描述符是同一些对象,并派生出 `input(options?)` 与 `invoke(remote, input, signal?)`。参数文档位于 `parameters[].description`;`result` 是唯一的结果 codec、解析器与可选投影。一元描述符省略 `mode`;流使用 `'stream'`。仅 Context 的接收者只出现在 `descriptors` 与作用域声明中,不进入 `methods`。
52
+
53
+ 来自已发布纯入口 `@qilin-agent/typert-protocol/remote-info` 的 `createTypertRemoteInfo(spec, z)` 返回类实例而非构造函数。构造只保留静态 spec 与调用方提供的 Zod 运行时;首次读取 `descriptors` 或 `methods` 时构建并缓存共享索引。每个输入策略视图、其解析器与可选 JSON Schema 投影都各自惰性求值并缓存;`method.input().create()` 请求普通解析器。原始 spec 要求 Zod factory,而面向消费方的 `TypertSchema<T>` 只承诺 `parse(value: unknown): T`。JSON Schema 来自生成器投影,不是运行时 `z.toJSONSchema()`。该叶子模块没有任何运行时导入;Zod 由生成产物提供。元数据与 schema 访问绝不发起网络请求;根入口也转出该 factory,但会导入 Kylin。
54
+
55
+ invoker 接收调用方自有的、已挂载、未作用域化的 Client Remote,并在调用时解析其命名空间方法。它保留命名空间接收者、可选的位置空洞与 signal,原样转发同样的 `Promise<RemoteResult<DecodedOutput>>` 或完整 `RemoteStreamHandle<Output, Uplink>`,不包一层 async。`DecodedOutput` 缺省为原始输出;仅当一元方法携带字节时生成器才提供 `RemoteDecoded<Output>`,避免对普通 JSON 结果做递归重映射;`result.create().parse()` 保留原始输出类型,不归一化共享字节缓冲。任一输入视图或结果缺少 JSON Schema 支持时,文档与解析仍然可用;各视图用自己的 `jsonSchemaError` 说明缺失的投影。注册表读取方用同一纯入口的 `isTypertRemoteMethod(descriptor)` 把基础描述符收窄为这些直接能力。
56
+
57
+ ### 识别调用方并接纳非 owner 调用方
58
+
59
+ `PeerScope.principal` 是 Host 进程内代码创建 Peer 时固定的调用方身份;任何 wire 字段都设置不了它,`PeerScope.signal` 在 Peer 作用域结束时中止。`PrincipalMap` 可合并扩展:每个驱动声明一个 JSON 条目,带唯一字面量 `kind` 且不含凭据,因为 principal 可能被持久化进 Session 日志;内建条目是 `owner: { kind: 'owner' }`。需要把调用方记录到持久输出上的 Remote 方法调用 `principalOf(this.ctx.invocation)`:它对非 owner 调用方返回其 principal,对 owner 与 Remote 调用之外返回 `undefined`,并且只在有值时写入字段。
60
+
61
+ 每个 Remote 方法都处于访问控制之下:非 owner 调用方只有在 Remote-access 层在调用所触资源上放行时才能触达它。`TypertResourceMap` 与 `TypertLookupMap` 并列、可合并扩展:拥有身份类型的包把自己的资源种类声明为 `TypertResource<Id>`(Session 包声明 `session: TypertResource<SessionId>`,Workspace 包声明 `workspace: TypertResource<WorkspaceId>`)。`RemoteAccessResource` 要么是内建无身份的 `{ kind: 'global' }`,要么是某个已声明种类加其身份;资源相互独立,访问 `global` 不代表能访问任何 session。运行时由拥有该种类的领域通过 `typert.resources.register(kind, { field })` 登记其默认 wire 字段,登记 `global` 会被拒绝;lookup 提供方与 Host Context 适配器通过 `resource` 指明其 wire 身份携带的种类,于是带 Agent 查找参数或 Agent 作用域接收者的方法无需 options 就携带 `session` 资源。`@Remote` 与 `@RemoteScope` 的 `resources` option 把种类映射到读取函数,读取函数以方法的 wire 实参作为自己的形参;其类型从被装饰方法推断,查找参数映射到其 wire 类型,因此 `@Remote({ name: 'page', resources: { session: request => request.address.sessionId } })` 能读取默认机制读不到的字段。读取函数与默认机制都未指明资源的调用只触及 `global`;它们必须指明方法访问的每个资源,因为访问控制不会检查被省略的资源。`TypertForwardableEventEntry` 的转发 `emit` 条目按其自身事件参数类型声明 `resources` 读取函数,没有读取函数的条目触及 `global`。资源解析属于 API Gateway;判定与拒绝原因属于补齐这条资源主线的 Remote-access 装配。
48
62
 
49
63
  ### 把 Host 对象与 Context 关联到 wire identity
50
64
 
@@ -87,11 +101,15 @@ Host 装配以转发给消费方的 Kylin 事件扩展 `TypertRemoteEventSelecti
87
101
 
88
102
  ### Remote 标记
89
103
 
90
- `@Remote` 与 `@RemoteScope` 调度一个初始化器,把方法名、可选导出名与调用模式追加到原型描述符;`remoteMethods(service)` 校验其版本,并返回与已存描述符分离、按声明顺序排列的快照,供 Gateway 的源码模式回退读取。标记要求名称为字符串的公开、非静态实例方法,同一方法上的冲突标记会被拒绝。
104
+ `@Remote` 与 `@RemoteScope` 调度一个初始化器,把方法名、可选导出名、调用模式与冻结的 `resources` 读取函数追加到原型描述符;`remoteMethods(service)` 校验其版本,并返回与已存描述符分离、按声明顺序排列的快照,供 Gateway 的源码模式回退读取。标记要求名称为字符串的公开、非静态实例方法,同一方法上的冲突标记会被拒绝。
105
+
106
+ 读取函数在装饰器创建时校验,并按每次装饰器应用冻结为一条记录;Gateway 通过 `remoteMethods(service)` 按实现成员名读取它们,因此协议包的另一个已安装副本读到的是同一条记录。
91
107
 
92
108
  ### 协议映射与描述符
93
109
 
94
- 可合并扩展的协议映射在类型系统中保留静态关联,运行时提供方则向 `ctx.typert` 注册解析;映射的名称与形状见 [`src/types.ts`](src/types.ts)。`InvocationDescriptor` 是注册表、Gateway 与 Client Remote 共同消费的共享运行时形式,涵盖直接与 Context 接收者、JSON 与查找参数、作用域投影、上行编解码器、取消与结果编解码器。
110
+ 可合并扩展的协议映射在类型系统中保留静态关联,运行时提供方则向 `ctx.typert` 注册解析;映射的名称与形状见 [`src/types.ts`](src/types.ts)。`InvocationDescriptor` 是注册表、Gateway 与 Client Remote 共同消费的共享运行时形式,涵盖直接与 Context 接收者、JSON 与查找参数、作用域投影、上行编解码器、取消、结果编解码器与文档。生成的 `TypertRemoteMethod` 对象直接扩展该描述符。
111
+
112
+ `RemoteInvocation.uplinkWindow` 可选地描述载体的整帧收件箱容量,以及压缩 JSON 项附加的 UTF-8 信封开销。缺省表示载体没有编码帧收件箱上限;本地缓冲与确认策略仍由消费方自有。
95
113
 
96
114
  ### Wire 标识文法
97
115
 
@@ -102,9 +120,12 @@ Host 装配以转发给消费方的 Kylin 事件扩展 `TypertRemoteEventSelecti
102
120
  | 文件 | 职责 |
103
121
  |---|---|
104
122
  | [`src/index.ts`](src/index.ts) | 装饰器、Gateway 绑定、`remoteMethods`、段校验 |
123
+ | [`src/principal.ts`](src/principal.ts) | `principalOf`,Remote 调用产出方记录的调用方 principal |
124
+ | [`src/remote-access.ts`](src/remote-access.ts) | 基于 `TypertResourceMap` 的 `RemoteAccessResource`、`RemoteAccessResourceKind` 与 `RemoteAccessResourceOf` |
105
125
  | [`src/json-value.ts`](src/json-value.ts) | 各载体共享的无损 JSON 校验 `isRemoteJsonValue` 与 `isRemoteUplinkItem` |
106
126
  | [`src/remote-error.ts`](src/remote-error.ts) | `RemoteError` 与结构式识别函数 `remoteErrorOf` |
107
- | [`src/types.ts`](src/types.ts) | 协议映射、`RemoteErrorDetailsMap`、`RemoteResult`、`RemoteStream`、`RemoteStreamHandle`、`PeerScope`、`RemoteInvocation`、`InvocationDescriptor`、编解码器、提供方约定、注册表接口、`TypertClientRemote` |
127
+ | [`src/remote-info.ts`](src/remote-info.ts) | 纯惰性 info factory 与直接方法类型守卫 |
128
+ | [`src/types.ts`](src/types.ts) | 协议映射、`RemoteErrorDetailsMap`、`RemoteResult`、`RemoteStream`、`RemoteStreamHandle`、`PrincipalMap`、`Principal`、`PeerScope`、`RemoteInvocation`、`InvocationDescriptor`、编解码器、提供方约定、注册表接口、`TypertClientRemote` |
108
129
 
109
130
  </details>
110
131
 
@@ -139,7 +160,7 @@ Host 装配以转发给消费方的 Kylin 事件扩展 `TypertRemoteEventSelecti
139
160
 
140
161
  这些限制说明声明能表示什么;它们是当前包约束,不是任务积压。
141
162
 
142
- - **装饰器标记是最小化的**——标记只包含方法名与直接调用或 Context 调用模式;参数、结果、查找与 schema 反射需要 Typert 构建流水线。
163
+ - **装饰器标记是最小化的**——标记包含方法名、可选导出别名、流模式、资源读取函数与直接或 Context 接收者选择;参数、结果、查找、schema 与文档元数据需要 Typert 构建流水线。
143
164
  - **Remote 签名受限**——装饰器只接受具有字符串名称的公开、非静态实例方法,源码模式执行无法表示重载、解构、默认参数或剩余参数签名。
144
165
 
145
166
  <a id="dev-note"></a>
package/lib/index.js CHANGED
@@ -116,6 +116,186 @@ function visitJsonValue(value, ancestors) {
116
116
  }
117
117
  }
118
118
  //#endregion
119
+ //#region lib/types/remote-info.js
120
+ /** Lazy assembly of generated Remote definitions using caller-supplied Zod; no runtime imports. */
121
+ /**
122
+ * Retain one generated definition per endpoint without constructing parsers or projections.
123
+ * The class instance builds its shared descriptor/method index on first access;
124
+ * input schemas and JSON Schema projections are cached only when requested.
125
+ * @param spec - compiler-produced method facts and Zod factories.
126
+ * @param zod - the generated artifact's Zod runtime for named-input parsers.
127
+ * @returns a lazy, mountable info instance with no registration or transport effects.
128
+ */
129
+ function createTypertRemoteInfo(spec, zod) {
130
+ return new RemoteInfo(spec, zod);
131
+ }
132
+ var RemoteInfo = class {
133
+ spec;
134
+ zod;
135
+ index;
136
+ constructor(spec, zod) {
137
+ this.spec = spec;
138
+ this.zod = zod;
139
+ }
140
+ get package() {
141
+ return this.spec.package;
142
+ }
143
+ get descriptors() {
144
+ return this.resolve().descriptors;
145
+ }
146
+ get methods() {
147
+ return this.resolve().methods;
148
+ }
149
+ resolve() {
150
+ if (this.index !== void 0) return this.index;
151
+ const zod = this.zod;
152
+ const descriptors = [];
153
+ const methods = Object.create(null);
154
+ for (const [endpoint, definition] of Object.entries(this.spec.methods)) {
155
+ const separator = endpoint.indexOf("/");
156
+ const namespace = endpoint.slice(0, separator);
157
+ const method = endpoint.slice(separator + 1);
158
+ const parameters = definition.parameters.map((parameter) => ({
159
+ ...parameter,
160
+ wire: parameter.wire ?? parameter.name
161
+ }));
162
+ const descriptor = {
163
+ ...definition,
164
+ id: `${this.spec.package}#${endpoint}`,
165
+ namespace,
166
+ method,
167
+ parameters,
168
+ result: definition.result.description === void 0 ? definition.result : new ResultValue(definition.result)
169
+ };
170
+ if (definition.invocation.kind === "direct") {
171
+ const inputs = [];
172
+ methods[endpoint] = Object.assign(descriptor, {
173
+ input(options) {
174
+ const strict = options?.strict === true;
175
+ return inputs[strict ? 1 : 0] ??= new NamedInput(parameters, strict, zod);
176
+ },
177
+ invoke(remote, input, signal) {
178
+ const named = input;
179
+ const values = parameters.map((parameter) => named[parameter.wire]);
180
+ if (definition.cancellation !== void 0) values.push(signal);
181
+ const receiver = Reflect.get(remote, namespace);
182
+ return Reflect.get(receiver, method).apply(receiver, values);
183
+ }
184
+ });
185
+ }
186
+ descriptors.push(descriptor);
187
+ }
188
+ return this.index = {
189
+ descriptors,
190
+ methods
191
+ };
192
+ }
193
+ };
194
+ /**
195
+ * Narrow a Registry descriptor to its generated direct-method capabilities.
196
+ * @param descriptor - a live descriptor, including possible source fallback or Context-only receivers.
197
+ * @returns whether generated named input and direct invocation are available.
198
+ */
199
+ function isTypertRemoteMethod(descriptor) {
200
+ return descriptor.invocation.kind === "direct" && descriptor.result.mode === "strict" && "input" in descriptor && typeof descriptor.input === "function" && "invoke" in descriptor && typeof descriptor.invoke === "function";
201
+ }
202
+ function optional(parameter) {
203
+ return parameter.source === "json" && parameter.acceptsUndefined === true;
204
+ }
205
+ var NamedInput = class {
206
+ parameters;
207
+ strict;
208
+ zod;
209
+ schema;
210
+ projection;
211
+ constructor(parameters, strict, zod) {
212
+ this.parameters = parameters;
213
+ this.strict = strict;
214
+ this.zod = zod;
215
+ }
216
+ codec(parameter) {
217
+ return this.strict ? parameter.strictCodec : parameter.codec;
218
+ }
219
+ create = () => this.schema ??= this.zod.strictObject(Object.fromEntries(this.parameters.map((parameter) => {
220
+ const value = this.codec(parameter).create();
221
+ return [parameter.wire, optional(parameter) ? value.optional() : value.nonoptional()];
222
+ })));
223
+ get jsonSchema() {
224
+ return this.project().jsonSchema;
225
+ }
226
+ get jsonSchemaError() {
227
+ return this.project().jsonSchemaError;
228
+ }
229
+ project() {
230
+ if (this.projection !== void 0) return this.projection;
231
+ const unavailable = this.parameters.find((parameter) => this.codec(parameter).jsonSchema === void 0);
232
+ if (unavailable !== void 0) return this.projection = { jsonSchemaError: this.codec(unavailable).jsonSchemaError ?? `No JSON Schema for parameter ${unavailable.wire}` };
233
+ return this.projection = { jsonSchema: {
234
+ type: "object",
235
+ properties: Object.fromEntries(this.parameters.map((parameter) => [parameter.wire, {
236
+ ...this.codec(parameter).jsonSchema,
237
+ ...parameter.description === void 0 ? {} : { description: parameter.description }
238
+ }])),
239
+ required: this.parameters.filter((parameter) => !optional(parameter)).map((parameter) => parameter.wire),
240
+ additionalProperties: false
241
+ } };
242
+ }
243
+ };
244
+ var ResultValue = class {
245
+ spec;
246
+ projection;
247
+ constructor(spec) {
248
+ this.spec = spec;
249
+ }
250
+ get mode() {
251
+ return this.spec.mode;
252
+ }
253
+ get typeSymbol() {
254
+ return this.spec.typeSymbol;
255
+ }
256
+ get create() {
257
+ return this.spec.create;
258
+ }
259
+ get decode() {
260
+ return this.spec.decode;
261
+ }
262
+ get encode() {
263
+ return this.spec.encode;
264
+ }
265
+ get description() {
266
+ return this.spec.description;
267
+ }
268
+ get jsonSchemaError() {
269
+ return this.spec.jsonSchemaError;
270
+ }
271
+ get jsonSchema() {
272
+ if (this.projection === void 0) {
273
+ const schema = this.spec.jsonSchema;
274
+ this.projection = { value: schema === void 0 ? void 0 : {
275
+ ...schema,
276
+ description: this.spec.description
277
+ } };
278
+ }
279
+ return this.projection.value;
280
+ }
281
+ };
282
+ //#endregion
283
+ //#region lib/types/principal.js
284
+ /**
285
+ * Reads the non-owner caller of the current Remote call.
286
+ * @module @qilin-agent/typert-protocol/principal
287
+ */
288
+ /**
289
+ * The identity a Remote-call producer records on durable output.
290
+ * @param invocation - `ctx.invocation`; `undefined` outside a Remote call.
291
+ * @returns the caller's principal when it is not the owner; `undefined` for the
292
+ * owner and for trusted in-process code outside a Remote call.
293
+ */
294
+ function principalOf(invocation) {
295
+ const principal = invocation?.peer.principal;
296
+ return principal?.kind === "owner" ? void 0 : principal;
297
+ }
298
+ //#endregion
119
299
  //#region lib/types/index.js
120
300
  /**
121
301
  * Remote decorators and explicit Gateway bindings backed by versioned
@@ -140,7 +320,7 @@ const REMOTE_METHOD_DESCRIPTOR = "@qilin-agent/typert-protocol/remote-methods";
140
320
  * a Host composition to read it.
141
321
  * @param service - owning Service instance, normally `this`.
142
322
  * @param serviceKey - exact Cordis service key.
143
- * @param options - optional distinct wire namespace.
323
+ * @param options - optional wire namespace.
144
324
  * @returns a frozen, inspectable binding with no compiler-injected metadata.
145
325
  */
146
326
  function bindTypertRemote(service, serviceKey, options = {}) {
@@ -148,7 +328,7 @@ function bindTypertRemote(service, serviceKey, options = {}) {
148
328
  const namespace = options.namespace ?? serviceKey;
149
329
  validateName("namespace", namespace);
150
330
  const ctx = Reflect.get(service, "ctx");
151
- if (ctx instanceof Context) provideInvocationAccessor(ctx);
331
+ if (Context.is(ctx)) provideInvocationAccessor(ctx);
152
332
  return Object.freeze({
153
333
  service,
154
334
  serviceKey,
@@ -163,7 +343,7 @@ var TypertRemoteService = class extends Service {
163
343
  * Register the Service and bind the same key to Typert Gateway.
164
344
  * @param ctx - owning Cordis Context.
165
345
  * @param serviceKey - exact Cordis service key and default wire namespace.
166
- * @param options - optional distinct wire namespace.
346
+ * @param options - optional wire namespace.
167
347
  */
168
348
  constructor(ctx, serviceKey, options = {}) {
169
349
  super(ctx, serviceKey);
@@ -183,36 +363,57 @@ function provideInvocationAccessor(ctx) {
183
363
  function Remote(methodExportOrOptions, context) {
184
364
  if (typeof methodExportOrOptions === "string") {
185
365
  validateName("Remote export name", methodExportOrOptions);
186
- return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
187
- }
188
- if (typeof methodExportOrOptions === "object") {
189
- if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
190
- return remoteDecorator({ kind: "direct" }, "stream");
366
+ return remoteDecorator({ kind: "direct" }, { exportName: methodExportOrOptions });
191
367
  }
368
+ if (typeof methodExportOrOptions === "object") return remoteDecorator({ kind: "direct" }, markerOptions(methodExportOrOptions, REMOTE_OPTION_KEYS, "Remote options require name, mode: \"stream\", or resources"));
192
369
  if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
193
370
  addMarkerInitializer(context, { kind: "direct" });
194
371
  }
195
- function remoteOptionMode(options) {
196
- return Reflect.get(options, "mode");
372
+ const REMOTE_OPTION_KEYS = new Set([
373
+ "name",
374
+ "mode",
375
+ "resources"
376
+ ]);
377
+ const REMOTE_SCOPE_OPTION_KEYS = new Set(["name", "resources"]);
378
+ /** Validate one decorator options object against its accepted keys and select the marker fields. */
379
+ function markerOptions(options, accepted, requirement) {
380
+ const keys = Reflect.ownKeys(options);
381
+ if (keys.length === 0 || keys.some((key) => typeof key !== "string" || !accepted.has(key)) || Object.hasOwn(options, "mode") && options.mode !== "stream") throw new TypeError(`typert-protocol: ${requirement}`);
382
+ const { mode, name, resources } = options;
383
+ if (name !== void 0) validateName("Remote export name", name);
384
+ return {
385
+ ...mode === void 0 ? {} : { mode },
386
+ ...name === void 0 ? {} : { exportName: name },
387
+ ...resources === void 0 ? {} : { resources: resourceReaders(resources) }
388
+ };
389
+ }
390
+ /** Freeze declared readers into the marker record; a misdeclared reader fails at decoration. */
391
+ function resourceReaders(resources) {
392
+ const readers = {};
393
+ for (const [kind, reader] of Object.entries(resources)) {
394
+ if (typeof reader !== "function") throw new TypeError(`typert-protocol: Remote resource reader "${kind}" must be a function`);
395
+ readers[kind] = reader;
396
+ }
397
+ if (Object.keys(readers).length === 0) throw new TypeError("typert-protocol: Remote resources require at least one reader");
398
+ return Object.freeze(readers);
197
399
  }
198
- function remoteDecorator(invocation, mode, exportName) {
400
+ function remoteDecorator(invocation, options = {}) {
199
401
  return function(_method, context) {
200
- addMarkerInitializer(context, invocation, mode, exportName);
402
+ addMarkerInitializer(context, invocation, options);
201
403
  };
202
404
  }
203
- /**
204
- * Create a decorator for a method resolved from one Remote Scope.
205
- * @param key - scope key declared through the Context map.
206
- * @param exportName - optional Remote export name; defaults to the method name.
207
- * @returns a standard method decorator that records a versioned prototype descriptor.
208
- */
209
- function RemoteScope(key, exportName) {
405
+ function RemoteScope(key, option) {
210
406
  validateName("Scope key", key);
211
- if (exportName !== void 0) validateName("Remote export name", exportName);
212
- return remoteDecorator({
407
+ const invocation = {
213
408
  kind: "context",
214
409
  context: key
215
- }, void 0, exportName);
410
+ };
411
+ if (option === void 0) return remoteDecorator(invocation);
412
+ if (typeof option === "string") {
413
+ validateName("Remote export name", option);
414
+ return remoteDecorator(invocation, { exportName: option });
415
+ }
416
+ return remoteDecorator(invocation, markerOptions(option, REMOTE_SCOPE_OPTION_KEYS, "RemoteScope options require name or resources"));
216
417
  }
217
418
  /**
218
419
  * Read Remote markers attached to a live Service's class prototype.
@@ -236,26 +437,27 @@ function readRemoteMethodDescriptor(prototype) {
236
437
  if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
237
438
  return descriptor;
238
439
  }
239
- function addMarkerInitializer(context, invocation, mode, exportName) {
440
+ function addMarkerInitializer(context, invocation, options = {}) {
240
441
  if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
241
442
  const method = context.name;
242
443
  context.addInitializer(function() {
243
444
  const prototype = Object.getPrototypeOf(this);
244
445
  if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
245
- mark(prototype, method, invocation, mode, exportName);
446
+ mark(prototype, method, invocation, options);
246
447
  });
247
448
  }
248
- function mark(prototype, method, invocation, mode, exportName) {
449
+ function mark(prototype, method, invocation, { mode, exportName, resources }) {
249
450
  const descriptor = readRemoteMethodDescriptor(prototype);
250
451
  const marker = Object.freeze({
251
452
  method,
252
453
  ...exportName === void 0 || exportName === method ? {} : { exportName },
253
454
  ...mode === void 0 ? {} : { mode },
254
- invocation: Object.freeze(invocation)
455
+ invocation: Object.freeze(invocation),
456
+ ...resources === void 0 ? {} : { resources }
255
457
  });
256
458
  const current = descriptor?.methods.find((candidate) => candidate.method === method);
257
459
  if (current !== void 0) {
258
- if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
460
+ if (current.exportName === marker.exportName && current.mode === marker.mode && current.resources === marker.resources && sameInvocation(current.invocation, invocation)) return;
259
461
  throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
260
462
  }
261
463
  Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
@@ -275,4 +477,4 @@ function validateName(subject, value) {
275
477
  if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
276
478
  }
277
479
  //#endregion
278
- export { Remote, RemoteError, RemoteScope, TYPERT_OWNED_VALUE, TypertRemoteService, bindTypertRemote, isRemoteJsonValue, isRemoteUplinkItem, isTypertOwnedValue, isTypertRemoteSegment, remoteErrorOf, remoteMethods, typertOwnedValue };
480
+ export { Remote, RemoteError, RemoteScope, TYPERT_OWNED_VALUE, TypertRemoteService, bindTypertRemote, createTypertRemoteInfo, isRemoteJsonValue, isRemoteUplinkItem, isTypertOwnedValue, isTypertRemoteMethod, isTypertRemoteSegment, principalOf, remoteErrorOf, remoteMethods, typertOwnedValue };