@stackstackstack/dsh-api-gateway 0.1.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +42 -0
- package/README.zh.md +42 -0
- package/lib/client.js +420 -0
- package/lib/index.js +396 -0
- package/lib/invariant.js +24 -0
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/index.js +467 -0
- package/lib/types/index.d.ts +61 -0
- package/lib/types/index.js +508 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/types.d.ts +34 -0
- package/lib/types/types.js +6 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/api/gateway/README.md
|
|
5
|
+
README.md: 7d41b607fbc2bf2ced6c2519314b5c6713722bc3
|
|
6
|
+
README.zh.md: 869010d16d7a48185c57cc1f3fdc28e82e9ad038
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @stackstackstack/dsh-api-gateway
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Two-sided Typert RPC endpoint for Host and Client Cordis environments. The Host entry provides `ctx.typertGateway`, while `@stackstackstack/dsh-api-gateway/client` provides `ctx.remote`; both consume the same generated `InvocationDescriptor` contract and leave business selection to API Remotes and transport, request correlation, trust, and response envelopes to Connection.
|
|
6
|
+
|
|
7
|
+
## Host service: `TypertGatewayService` (ctx key: `typertGateway`)
|
|
8
|
+
|
|
9
|
+
`ctx.typertGateway.invoke()` resolves the current descriptor and Cordis Service for each call, validates exact named arguments, resolves registered object or Context identities, invokes the public business method, and validates its result. Business Services extend `TypertRemoteService` and mark methods with `@Remote` or `@RemoteScope` from [`dsh-typert-protocol`](../../typert/protocol/README.md); `bindTypertRemote()` remains available when another base class owns inheritance.
|
|
10
|
+
|
|
11
|
+
Strict mode reads generated invocation descriptors from `ctx.typert.local`. Lookup parameters use the currently active resolver in `ctx.typert.lookups`: the business package registers the stable declaration and default policy, while Host composition can override resolution behavior with effect-scoped `configure()`; `@RemoteScope` resolves its receiver through a registered Host Context provider. SRC mode is a development fallback for endpoints that have never had a strict definition; it parses simple parameter names and accepts only JSON-safe values for non-lookup parameters. Withdrawing an observed strict definition fails instead of weakening validation.
|
|
12
|
+
|
|
13
|
+
The Host entry registers a trusted-host interceptor on Connection's shared `/api` FetchHandler. Connection passes this composite handler through its HTTP bridge; the handler dispatches claimed endpoints to Gateway and unclaimed endpoints to API Proxy. Direct `invoke()` calls preserve business errors; `TypertGatewayError` distinguishes failures owned by dispatch, binding, providers, lookup, Context, arguments, and codecs. A resolver may use `TypertLookupFailure` to carry an existing RPC error, preserving its original error code for policy rejections such as cold-resume failures or ownership fences.
|
|
14
|
+
|
|
15
|
+
A cancellation-aware Remote method declares `signal: AbortSignal` as its final Host parameter. The signal is descriptor metadata rather than a wire argument: Connection supplies it to the Gateway, and the Gateway injects it after decoded business parameters. SRC recognizes the reserved final name, while strict generation additionally requires the global `AbortSignal` type.
|
|
16
|
+
|
|
17
|
+
## Client service: `ClientRemote` (ctx key: `remote`)
|
|
18
|
+
|
|
19
|
+
`ctx.remote.$mount()` validates and registers a generated Host-for-Client contribution, then installs concrete direct and scoped methods for the calling Cordis fiber. Each namespace is a traced `remote.<namespace>` child Service and unloads after its last method is withdrawn. Duplicate endpoints, namespace collisions, and descriptors without strict generated codecs fail before methods become callable.
|
|
20
|
+
|
|
21
|
+
Each call validates positional inputs, constructs the descriptor's exact named `args`, and sends it through `ctx.connection.rpc.call('/api', endpoint, ...)`. Generated cancellation-aware methods accept a final optional `AbortSignal`; the Client combines it with the contribution mount lifetime before calling Connection. The returned value is validated before reaching application code. Withdrawing a contribution removes its descriptors and methods together, aborts in-flight calls, and makes retained method handles reject.
|
|
22
|
+
|
|
23
|
+
`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. Delivery is one-way and follows registration order; a listener that throws is logged and isolated from the remaining listeners, which never affects the frame pump. `ctx.remote.$dispatch()` is the other half of that surface, and it is the carrier's: the Client half owning the Host frame sink hands each decoded frame over, and an event name nobody subscribes to is dropped, since the wire carries whatever the Host selected. A consumer subscribes and never calls it.
|
|
24
|
+
|
|
25
|
+
Generated declaration merges provide the TypeScript API through the shared `TypertClientRemote` contract. The Client entry contains no Host Service or Host Cordis interface merge, and method lookup and invocation use ordinary objects and functions rather than a JavaScript Proxy.
|
|
26
|
+
|
|
27
|
+
## Model Experience
|
|
28
|
+
|
|
29
|
+
None, as the package dispatches application calls and registers no prompt, tool, or session event.
|
|
30
|
+
|
|
31
|
+
#### KV Cache effect
|
|
32
|
+
|
|
33
|
+
No direct effect; invoked business Services own any model-visible result.
|
|
34
|
+
|
|
35
|
+
## Known Limitations and Deferred Work
|
|
36
|
+
|
|
37
|
+
- The Connection adapter maps ordinary dispatch failures and business exceptions to the RPC `internal` code with empty details; lookup-policy errors carried by `TypertLookupFailure` are returned unchanged. Structured `TypertGatewayError` categories remain available only to same-process callers.
|
|
38
|
+
- SRC mode supports unique identifier parameters without destructuring, defaults, or rest parameters. It validates JSON safety rather than generated business types and never infers optional fields.
|
|
39
|
+
- Only strict generated contributions can mount on the Client face. SRC markers have no Client codec or type projection.
|
|
40
|
+
- The package dispatches unary methods only. Incremental Session data uses a separate named-stream protocol over the same Connection.
|
|
41
|
+
- Lookup resolvers are configured per key; an individual Remote parameter or endpoint cannot currently select a live-only policy under the same `agent`/`session` key.
|
|
42
|
+
- Forwarded events reach `$on` exactly as the Host emitted them: no payload projection or redaction, no Scope-bound subscription, and no replay after a reconnect.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @stackstackstack/dsh-api-gateway
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
为 Host 与 Client 两侧的 Cordis 环境提供 Typert RPC endpoint。Host 入口提供 `ctx.typertGateway`,`@stackstackstack/dsh-api-gateway/client` 则提供 `ctx.remote`;两者使用同一份生成的 `InvocationDescriptor` 约定,并将业务选择交给 API Remotes,将传输、请求关联、信任和响应封装交给 Connection。
|
|
6
|
+
|
|
7
|
+
## Host 服务:`TypertGatewayService`(ctx key:`typertGateway`)
|
|
8
|
+
|
|
9
|
+
每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务继承 [`dsh-typert-protocol`](../../typert/protocol/README.md) 的 `TypertRemoteService`,并用 `@Remote` 或 `@RemoteScope` 标记方法;已有其他基类时仍可改用 `bindTypertRemote()`。
|
|
10
|
+
|
|
11
|
+
严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的 resolver:业务包注册稳定声明与默认策略,Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context 提供方解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
|
|
12
|
+
|
|
13
|
+
Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领 endpoint 则交给 API Proxy。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 可区分分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。resolver 可以用 `TypertLookupFailure` 携带既有 RPC error,使冷恢复失败或 ownership fence 等策略拒绝保持原错误码。
|
|
14
|
+
|
|
15
|
+
支持取消的 Remote 方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。signal 是 descriptor 元数据,而不是 wire 参数:Connection 将它提供给 Gateway,Gateway 则在已解码的业务参数之后注入它。SRC 识别这个保留的末位参数名,严格生成还要求它具有全局 `AbortSignal` 类型。
|
|
16
|
+
|
|
17
|
+
## Client 服务:`ClientRemote`(ctx key:`remote`)
|
|
18
|
+
|
|
19
|
+
`ctx.remote.$mount()` 会校验并注册生成的 Host-for-Client 贡献项,然后为发起调用的 Cordis fiber 安装具体的直接方法和作用域方法。每个 namespace 都是可追踪的 `remote.<namespace>` 子 Service,并在最后一个方法撤回后卸载。重复端点、命名空间冲突,以及缺少生成的严格编解码器的描述符,都会在方法可调用前报错。
|
|
20
|
+
|
|
21
|
+
每次调用都会校验位置参数,构造与描述符完全匹配的具名 `args`,再通过 `ctx.connection.rpc.call('/api', endpoint, ...)` 发送。生成的支持取消的方法接受最后一个可选 `AbortSignal`;Client 会在调用 Connection 前将它与贡献项的挂载生命周期合并。返回值经过校验后才会交给应用代码。撤回贡献项会同时移除其描述符和方法、中止正在进行的调用,并使外部仍持有的方法句柄在调用时返回拒绝。
|
|
22
|
+
|
|
23
|
+
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber,并随该 fiber 一起消失。投递是单向的,并按注册顺序进行;抛错的 listener 会被记录并与其余 listener 隔离,绝不影响帧泵。`ctx.remote.$dispatch()` 是该面的另一半,且属于载体:持有 Host 帧 sink 的 Client 半把每个解码后的帧交进来,收到无人订阅的事件名即丢弃,因为 wire 上出现什么取决于 Host 的转发选择。消费方只订阅,绝不调用它。
|
|
24
|
+
|
|
25
|
+
生成的声明合并通过共享的 `TypertClientRemote` 约定提供 TypeScript API。Client 入口不包含 Host 服务或 Host Cordis 接口合并;方法查找和调用使用普通对象与函数,而不使用 JavaScript Proxy。
|
|
26
|
+
|
|
27
|
+
## 模型体验
|
|
28
|
+
|
|
29
|
+
无,因为该包分发应用调用,不注册任何提示词、工具或会话事件。
|
|
30
|
+
|
|
31
|
+
#### KV Cache 影响
|
|
32
|
+
|
|
33
|
+
无直接影响;被调用的业务服务负责产生任何模型可见结果。
|
|
34
|
+
|
|
35
|
+
## 已知限制与延期工作
|
|
36
|
+
|
|
37
|
+
- Connection 适配器将普通分发故障和业务异常映射为 RPC 的 `internal` 代码,且不附带详细信息;`TypertLookupFailure` 携带的 lookup 策略错误会原样返回。结构化的 `TypertGatewayError` 类别仅供同进程调用方使用。
|
|
38
|
+
- SRC 模式仅支持名称唯一的标识符参数,不支持解构、默认值或剩余参数。它只校验值能否安全表示为 JSON,不校验生成的业务类型,也绝不会推断可选字段。
|
|
39
|
+
- Client 侧只能挂载严格模式生成的贡献项。SRC 标记不具备 Client 编解码器或类型投影。
|
|
40
|
+
- 该包只分发一元方法。增量会话数据通过同一个 Connection 上独立的具名流协议传输。
|
|
41
|
+
- lookup resolver 按 key 配置;当前无法让单个 Remote 参数或 endpoint 在同一 `agent`/`session` key 下选择 live-only 策略。
|
|
42
|
+
- 被转发的事件原样到达 `$on`:没有载荷投影或脱敏,不支持 Scope 化订阅,重连后也不重放。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@stackstackstack/dsh-api-gateway",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
8
|
+
//#region lib/types/client/index.js
|
|
9
|
+
/**
|
|
10
|
+
* Client projection of generated Typert Remote descriptors. Contributions
|
|
11
|
+
* install traced `remote.<namespace>` services; no JavaScript Proxy
|
|
12
|
+
* participates in method lookup, invocation, or type exposure.
|
|
13
|
+
*/
|
|
14
|
+
/** Required Client services: the Typert registry and the existing Connection carrier. */
|
|
15
|
+
const inject = ["typert", "connection"];
|
|
16
|
+
/**
|
|
17
|
+
* Install the typed Client Remote service.
|
|
18
|
+
* @param ctx - Client Cordis root.
|
|
19
|
+
*/
|
|
20
|
+
function apply(ctx) {
|
|
21
|
+
new ClientRemoteService(ctx);
|
|
22
|
+
}
|
|
23
|
+
var ClientRemoteService = class extends _deepseek_ai_cordis.Service {
|
|
24
|
+
ownerCtx;
|
|
25
|
+
namespaces = /* @__PURE__ */ new Map();
|
|
26
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
27
|
+
mutations = Promise.resolve();
|
|
28
|
+
constructor(ctx) {
|
|
29
|
+
super(ctx, "remote");
|
|
30
|
+
this.ownerCtx = ctx;
|
|
31
|
+
ctx.effect(() => () => {
|
|
32
|
+
this.subscriptions.clear();
|
|
33
|
+
}, "api-gateway.client.subscriptions");
|
|
34
|
+
}
|
|
35
|
+
async $mount(contribution) {
|
|
36
|
+
const callerCtx = this.ctx;
|
|
37
|
+
const owned = callerCtx.effect(async () => {
|
|
38
|
+
const dispose = await this.enqueue(() => this.mountContribution(callerCtx, contribution));
|
|
39
|
+
return () => this.enqueue(dispose);
|
|
40
|
+
}, `api-gateway.client.$mount(${JSON.stringify(contribution.package)})`);
|
|
41
|
+
await owned;
|
|
42
|
+
return async () => {
|
|
43
|
+
await owned();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
$on(event, listener) {
|
|
47
|
+
const subscription = { listener };
|
|
48
|
+
const owned = this.ctx.effect(() => {
|
|
49
|
+
const listeners = this.listeners(event);
|
|
50
|
+
listeners.push(subscription);
|
|
51
|
+
return () => {
|
|
52
|
+
const at = listeners.indexOf(subscription);
|
|
53
|
+
/* v8 ignore next -- listener */
|
|
54
|
+
if (at >= 0) listeners.splice(at, 1);
|
|
55
|
+
};
|
|
56
|
+
}, `api-gateway.client.$on(${JSON.stringify(event)})`);
|
|
57
|
+
return () => {
|
|
58
|
+
owned();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Deliver one forwarded event in registration order, isolating a listener
|
|
63
|
+
* that fails either synchronously or by rejecting a returned promise; see
|
|
64
|
+
* {@link TypertClientRemote.$dispatch} for the caller contract.
|
|
65
|
+
*/
|
|
66
|
+
$dispatch(event, args) {
|
|
67
|
+
const listeners = this.subscriptions.get(event);
|
|
68
|
+
if (listeners === void 0) return;
|
|
69
|
+
for (const { listener } of [...listeners]) {
|
|
70
|
+
const report = (error) => {
|
|
71
|
+
console.error(`client api: Remote event ${JSON.stringify(event)} listener threw:`, error);
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
const settled = listener(...args);
|
|
75
|
+
if (settled instanceof Promise) settled.catch(report);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
report(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Subscriptions for one event name; empty arrays are retained, bounded by the Host's selection. */
|
|
82
|
+
listeners(event) {
|
|
83
|
+
let listeners = this.subscriptions.get(event);
|
|
84
|
+
if (listeners === void 0) {
|
|
85
|
+
listeners = [];
|
|
86
|
+
this.subscriptions.set(event, listeners);
|
|
87
|
+
}
|
|
88
|
+
return listeners;
|
|
89
|
+
}
|
|
90
|
+
enqueue(operation) {
|
|
91
|
+
const result = this.mutations.then(operation, operation);
|
|
92
|
+
this.mutations = result.then(() => void 0, () => void 0);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
async mountContribution(callerCtx, contribution) {
|
|
96
|
+
this.validateContribution(contribution);
|
|
97
|
+
const disposeRemote = callerCtx.typert.remotes.register(contribution);
|
|
98
|
+
const installed = [];
|
|
99
|
+
try {
|
|
100
|
+
for (const descriptor of contribution.descriptors) installed.push(await this.install(descriptor));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
for (const dispose of installed.reverse()) await dispose();
|
|
103
|
+
await disposeRemote();
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
return async () => {
|
|
107
|
+
for (const dispose of installed.reverse()) await dispose();
|
|
108
|
+
await disposeRemote();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
validateContribution(contribution) {
|
|
112
|
+
const direct = /* @__PURE__ */ new Map();
|
|
113
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
114
|
+
const add = (table, descriptor, kind) => {
|
|
115
|
+
const methods = table.get(descriptor.namespace) ?? /* @__PURE__ */ new Set();
|
|
116
|
+
if (methods.has(descriptor.method)) throw new Error(`client api: contribution repeats ${kind} method ${endpointOf(descriptor)}`);
|
|
117
|
+
methods.add(descriptor.method);
|
|
118
|
+
table.set(descriptor.namespace, methods);
|
|
119
|
+
if ((this.namespaces.get(descriptor.namespace)?.service)?.has(kind, descriptor.method) === true) throw new Error(`client api: ${kind} method ${endpointOf(descriptor)} is already mounted`);
|
|
120
|
+
};
|
|
121
|
+
for (const descriptor of contribution.descriptors) {
|
|
122
|
+
requireStrictDescriptor(descriptor);
|
|
123
|
+
if (descriptor.invocation.kind === "direct") add(direct, descriptor, "direct");
|
|
124
|
+
if (scopedProjection(descriptor) !== void 0) add(scoped, descriptor, "scoped");
|
|
125
|
+
}
|
|
126
|
+
const namespaces = new Set([...direct.keys(), ...scoped.keys()]);
|
|
127
|
+
for (const namespace of namespaces) {
|
|
128
|
+
const service = this.namespaces.get(namespace)?.service;
|
|
129
|
+
if (service === void 0) {
|
|
130
|
+
if (namespace in this) throw new Error(`client api: namespace ${JSON.stringify(namespace)} conflicts with the Remote service`);
|
|
131
|
+
const serviceKey = remoteServiceKey(namespace);
|
|
132
|
+
if (this.ownerCtx.reflect.props[serviceKey]?.type === "accessor" || this.ownerCtx.get(serviceKey) !== void 0) throw new Error(`client api: namespace ${JSON.stringify(namespace)} conflicts with an existing Remote namespace`);
|
|
133
|
+
}
|
|
134
|
+
for (const method of new Set([...direct.get(namespace) ?? [], ...scoped.get(namespace) ?? []])) if (service === void 0) RemoteNamespaceService.assertMethodAvailable(namespace, method);
|
|
135
|
+
else service.assertMethodAvailable(method);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async install(descriptor) {
|
|
139
|
+
const token = {
|
|
140
|
+
active: true,
|
|
141
|
+
abort: new AbortController()
|
|
142
|
+
};
|
|
143
|
+
const installed = [];
|
|
144
|
+
try {
|
|
145
|
+
if (descriptor.invocation.kind === "direct") installed.push(await this.installDirect(descriptor, token));
|
|
146
|
+
const projection = scopedProjection(descriptor);
|
|
147
|
+
if (projection !== void 0) installed.push(await this.installScoped(descriptor, projection, token));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
token.active = false;
|
|
150
|
+
token.abort.abort();
|
|
151
|
+
for (const dispose of installed.reverse()) await dispose();
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
return async () => {
|
|
155
|
+
/* v8 ignore next -- Cordis effect disposers are idempotent and invoke this cleanup at most once. */
|
|
156
|
+
if (!token.active) return;
|
|
157
|
+
token.active = false;
|
|
158
|
+
token.abort.abort();
|
|
159
|
+
for (const dispose of installed.reverse()) await dispose();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async installDirect(descriptor, token) {
|
|
163
|
+
const namespace = await this.namespace(descriptor.namespace);
|
|
164
|
+
try {
|
|
165
|
+
namespace.service.installDirect(descriptor, token);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
return async () => {
|
|
171
|
+
namespace.service.remove("direct", descriptor.method, token);
|
|
172
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async installScoped(descriptor, projection, token) {
|
|
176
|
+
const namespace = await this.namespace(descriptor.namespace);
|
|
177
|
+
try {
|
|
178
|
+
namespace.service.installScoped(descriptor, projection, token);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
return async () => {
|
|
184
|
+
namespace.service.remove("scoped", descriptor.method, token);
|
|
185
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async namespace(name) {
|
|
189
|
+
let namespace = this.namespaces.get(name);
|
|
190
|
+
if (namespace !== void 0) return namespace;
|
|
191
|
+
let service;
|
|
192
|
+
const fiber = this.ownerCtx.plugin({
|
|
193
|
+
name: remoteServiceKey(name),
|
|
194
|
+
apply: (ctx) => {
|
|
195
|
+
service = new RemoteNamespaceService(ctx, name, (direct, scoped, caller, args) => this.invokeMethod(direct, scoped, caller, args));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
await fiber;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await fiber.dispose();
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
/* v8 ignore next -- a settled namespace fiber synchronously constructs its Service. */
|
|
205
|
+
if (service === void 0) throw new Error(`client api: namespace ${JSON.stringify(name)} did not start`);
|
|
206
|
+
namespace = {
|
|
207
|
+
service,
|
|
208
|
+
dispose: fiber.dispose
|
|
209
|
+
};
|
|
210
|
+
this.namespaces.set(name, namespace);
|
|
211
|
+
return namespace;
|
|
212
|
+
}
|
|
213
|
+
async disposeNamespace(name, namespace) {
|
|
214
|
+
if (!namespace.service.empty || this.namespaces.get(name) !== namespace) return;
|
|
215
|
+
this.namespaces.delete(name);
|
|
216
|
+
await namespace.dispose();
|
|
217
|
+
}
|
|
218
|
+
invokeMethod(direct, scoped, callerCtx, values) {
|
|
219
|
+
if (scoped !== void 0) {
|
|
220
|
+
const identity = this.ownerCtx.typert.contexts.getClient(scoped.projection.context)?.identity(callerCtx);
|
|
221
|
+
if (identity !== void 0) return this.invoke(scoped.descriptor, scoped.projection, scoped.token, callerCtx, values, { value: identity });
|
|
222
|
+
}
|
|
223
|
+
if (direct !== void 0) return this.invoke(direct.descriptor, void 0, direct.token, callerCtx, values);
|
|
224
|
+
if (scoped !== void 0) return this.invoke(scoped.descriptor, scoped.projection, scoped.token, callerCtx, values);
|
|
225
|
+
throw new Error("client api: Remote method is no longer mounted");
|
|
226
|
+
}
|
|
227
|
+
async invoke(descriptor, projection, token, callerCtx, values, boundIdentity) {
|
|
228
|
+
const endpoint = endpointOf(descriptor);
|
|
229
|
+
if (!token.active) return withdrawn(endpoint);
|
|
230
|
+
const expected = descriptor.parameters.length - (projection?.parameterIndex === void 0 ? 0 : 1);
|
|
231
|
+
const hasCallerSignal = descriptor.cancellation !== void 0 && values.length === expected + 1;
|
|
232
|
+
if (values.length !== expected && !hasCallerSignal) {
|
|
233
|
+
const contract = descriptor.cancellation === void 0 ? `${String(expected)} argument(s)` : `${String(expected)} business argument(s) plus an optional AbortSignal`;
|
|
234
|
+
throw new Error(`client api: ${endpoint} expected ${contract}, got ${String(values.length)}`);
|
|
235
|
+
}
|
|
236
|
+
const args = Object.create(null);
|
|
237
|
+
if (projection !== void 0) {
|
|
238
|
+
const binder = boundIdentity === void 0 ? this.ownerCtx.typert.contexts.getClient(projection.context) : void 0;
|
|
239
|
+
if (boundIdentity === void 0 && binder === void 0) throw new Error(`client api: ${endpoint} has no Client Context binder for ${JSON.stringify(projection.context)}`);
|
|
240
|
+
const identity = boundIdentity === void 0 ? binder?.identity(callerCtx) : boundIdentity.value;
|
|
241
|
+
if (identity === void 0) throw new Error(`client api: ${endpoint} requires a ${JSON.stringify(projection.context)} Context`);
|
|
242
|
+
args[projection.wire] = parse(projection.codec, identity, endpoint, projection.wire);
|
|
243
|
+
}
|
|
244
|
+
let valueIndex = 0;
|
|
245
|
+
descriptor.parameters.forEach((parameter, parameterIndex) => {
|
|
246
|
+
if (parameterIndex === projection?.parameterIndex) return;
|
|
247
|
+
const value = parse(parameter.codec, values[valueIndex], endpoint, parameter.wire);
|
|
248
|
+
if (value !== void 0) args[parameter.wire] = value;
|
|
249
|
+
valueIndex += 1;
|
|
250
|
+
});
|
|
251
|
+
const connection = this.ownerCtx.get("connection");
|
|
252
|
+
if (connection === void 0) throw new Error(`client api: ${endpoint} has no active Connection`);
|
|
253
|
+
const callerSignal = hasCallerSignal ? values[expected] : void 0;
|
|
254
|
+
const signal = callerSignal === void 0 ? token.abort.signal : AbortSignal.any([token.abort.signal, callerSignal]);
|
|
255
|
+
try {
|
|
256
|
+
const result = await connection.rpc.call("/api", endpoint, { args }, signal);
|
|
257
|
+
if (!mountActive(token)) return withdrawn(endpoint);
|
|
258
|
+
if (!result.ok) return {
|
|
259
|
+
ok: false,
|
|
260
|
+
error: result.error
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
value: parse(descriptor.result, result.value, endpoint, "result")
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return carrierFailure(endpoint, error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
var RemoteNamespaceService = class RemoteNamespaceService extends _deepseek_ai_cordis.Service {
|
|
272
|
+
invokeRemote;
|
|
273
|
+
methods = /* @__PURE__ */ new Map();
|
|
274
|
+
namespace;
|
|
275
|
+
static assertMethodAvailable(namespace, method) {
|
|
276
|
+
if (REMOTE_NAMESPACE_FIELDS.has(method) || method in RemoteNamespaceService.prototype) throw new Error(`client api: method ${JSON.stringify(`${namespace}/${method}`)} conflicts with its namespace service`);
|
|
277
|
+
}
|
|
278
|
+
constructor(ctx, name, invokeRemote) {
|
|
279
|
+
super(ctx, remoteServiceKey(name));
|
|
280
|
+
this.invokeRemote = invokeRemote;
|
|
281
|
+
this.namespace = name;
|
|
282
|
+
}
|
|
283
|
+
assertMethodAvailable(method) {
|
|
284
|
+
RemoteNamespaceService.assertMethodAvailable(this.namespace, method);
|
|
285
|
+
if (method in this && !this.methods.has(method)) throw new Error(`client api: method ${JSON.stringify(`${this.namespace}/${method}`)} conflicts with its namespace service`);
|
|
286
|
+
}
|
|
287
|
+
get empty() {
|
|
288
|
+
return this.methods.size === 0;
|
|
289
|
+
}
|
|
290
|
+
has(kind, method) {
|
|
291
|
+
return this.methods.get(method)?.[kind] !== void 0;
|
|
292
|
+
}
|
|
293
|
+
installDirect(descriptor, token) {
|
|
294
|
+
this.install(descriptor.method, "direct", {
|
|
295
|
+
descriptor,
|
|
296
|
+
token
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
installScoped(descriptor, projection, token) {
|
|
300
|
+
this.install(descriptor.method, "scoped", {
|
|
301
|
+
descriptor,
|
|
302
|
+
projection,
|
|
303
|
+
token
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
install(method, kind, value) {
|
|
307
|
+
this.assertMethodAvailable(method);
|
|
308
|
+
let record = this.methods.get(method);
|
|
309
|
+
const fresh = record === void 0;
|
|
310
|
+
record ??= {};
|
|
311
|
+
if (fresh) {
|
|
312
|
+
Object.defineProperty(this, method, {
|
|
313
|
+
configurable: true,
|
|
314
|
+
enumerable: true,
|
|
315
|
+
get: function() {
|
|
316
|
+
const callerCtx = this.ctx;
|
|
317
|
+
const current = this.methods.get(method);
|
|
318
|
+
const direct = current?.direct;
|
|
319
|
+
const scoped = current?.scoped;
|
|
320
|
+
return (...args) => {
|
|
321
|
+
return this.invokeRemote(direct, scoped, callerCtx, args);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
this.methods.set(method, record);
|
|
326
|
+
}
|
|
327
|
+
if (kind === "direct") record.direct = value;
|
|
328
|
+
else record.scoped = value;
|
|
329
|
+
}
|
|
330
|
+
remove(kind, method, token) {
|
|
331
|
+
const record = this.methods.get(method);
|
|
332
|
+
const current = record?.[kind];
|
|
333
|
+
/* v8 ignore next -- duplicate live variants are rejected before installation, so no newer token can replace this one. */
|
|
334
|
+
if (record === void 0 || current?.token !== token) return;
|
|
335
|
+
if (kind === "direct") delete record.direct;
|
|
336
|
+
else delete record.scoped;
|
|
337
|
+
if (record.direct !== void 0 || record.scoped !== void 0) return;
|
|
338
|
+
this.methods.delete(method);
|
|
339
|
+
Reflect.deleteProperty(this, method);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const REMOTE_NAMESPACE_FIELDS = new Set([
|
|
343
|
+
"ctx",
|
|
344
|
+
"empty",
|
|
345
|
+
"invokeRemote",
|
|
346
|
+
"methods",
|
|
347
|
+
"name",
|
|
348
|
+
"namespace"
|
|
349
|
+
]);
|
|
350
|
+
function remoteServiceKey(namespace) {
|
|
351
|
+
return `remote.${namespace}`;
|
|
352
|
+
}
|
|
353
|
+
function endpointOf(descriptor) {
|
|
354
|
+
return `${descriptor.namespace}/${descriptor.method}`;
|
|
355
|
+
}
|
|
356
|
+
function mountActive(token) {
|
|
357
|
+
return token.active;
|
|
358
|
+
}
|
|
359
|
+
function scopedProjection(descriptor) {
|
|
360
|
+
if (descriptor.invocation.kind === "context") return {
|
|
361
|
+
context: descriptor.invocation.context,
|
|
362
|
+
wire: descriptor.invocation.wire,
|
|
363
|
+
codec: descriptor.invocation.codec
|
|
364
|
+
};
|
|
365
|
+
if (descriptor.scope === void 0) return void 0;
|
|
366
|
+
const lookupParameters = descriptor.parameters.map((parameter, index) => ({
|
|
367
|
+
parameter,
|
|
368
|
+
index
|
|
369
|
+
})).filter((candidate) => candidate.parameter.source === "lookup");
|
|
370
|
+
const selected = lookupParameters.length === 1 ? lookupParameters[0] : void 0;
|
|
371
|
+
if (selected === void 0 || selected.parameter.wire !== descriptor.scope.wire || selected.parameter.lookup !== descriptor.scope.context) throw new Error(`client api: generated Remote ${endpointOf(descriptor)} scope must select its only lookup parameter`);
|
|
372
|
+
return {
|
|
373
|
+
context: descriptor.scope.context,
|
|
374
|
+
wire: descriptor.scope.wire,
|
|
375
|
+
codec: selected.parameter.codec,
|
|
376
|
+
parameterIndex: selected.index
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function requireStrictDescriptor(descriptor) {
|
|
380
|
+
const endpoint = endpointOf(descriptor);
|
|
381
|
+
requireStrictCodec(descriptor.result, endpoint, "result");
|
|
382
|
+
for (const parameter of descriptor.parameters) requireStrictCodec(parameter.codec, endpoint, parameter.wire);
|
|
383
|
+
if (descriptor.invocation.kind === "context") requireStrictCodec(descriptor.invocation.codec, endpoint, descriptor.invocation.wire);
|
|
384
|
+
}
|
|
385
|
+
function requireStrictCodec(codec, endpoint, field) {
|
|
386
|
+
if (codec.mode !== "strict") throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`);
|
|
387
|
+
}
|
|
388
|
+
function parse(codec, value, endpoint, field) {
|
|
389
|
+
if (codec.mode !== "strict") throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`);
|
|
390
|
+
try {
|
|
391
|
+
return codec.schema.parse(value);
|
|
392
|
+
} catch (cause) {
|
|
393
|
+
throw new Error(`client api: ${endpoint} rejected ${JSON.stringify(field)}`, { cause });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** The namespace retired before or during the call, so no request outcome exists. */
|
|
397
|
+
function withdrawn(endpoint) {
|
|
398
|
+
return internalFailure(`client api: Remote method ${endpoint} is no longer mounted`);
|
|
399
|
+
}
|
|
400
|
+
function carrierFailure(endpoint, error) {
|
|
401
|
+
return internalFailure(`client api: ${endpoint} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
402
|
+
}
|
|
403
|
+
function internalFailure(message) {
|
|
404
|
+
return {
|
|
405
|
+
ok: false,
|
|
406
|
+
error: {
|
|
407
|
+
code: "internal",
|
|
408
|
+
message,
|
|
409
|
+
details: {}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
exports.apply = apply;
|
|
415
|
+
exports.inject = inject;
|
|
416
|
+
return module.exports;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
//# sourceMappingURL=client.js.map
|