fix(typert): recognize Cordis Context across built copies
This commit is contained in:
1 parent
1d0ff1d2e4
commit
c233ca3df5
6 files changed
+62
-5
No files matched your search
@@ -18,8 +18,8 @@
|
||||
en: c479b5b665822198
|
||||
zh: 1ffb789c18043baf
|
||||
/deepseek-ai-dsh-typert-protocol/use-this-package/exposing-a-host-method:
|
||||
en: ca7df7ea76fb09d8
|
||||
zh: add884160fbbd5c7
|
||||
en: 63968de5fc817d06
|
||||
zh: fbe2804b2a2d70d3
|
||||
/deepseek-ai-dsh-typert-protocol/use-this-package/identifying-the-caller-and-admitting-non-owner-callers:
|
||||
en: 78e08ae732a8f6f4
|
||||
zh: be15fb7599994fb9
|
||||
|
||||
@@ -46,7 +46,7 @@ Generation turns the method into a wire endpoint under the service's namespace;
|
||||
|
||||
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.
|
||||
|
||||
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. 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.
|
||||
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. Cordis'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.
|
||||
|
||||
`@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.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export class GoalService extends TypertRemoteService {
|
||||
|
||||
一元方法可以直接返回 `Uint8Array`,也可以将其放在嵌套对象、数组、元组、可选字段、联合类型和递归类型中。生成器提供可选的结果 codec `encode()` 和 `decode()`:编码仅访问类型可能包含字节的子树,解码则校验还原后的值;Client 声明在每个字节位置使用 `Uint8Array<ArrayBuffer>`,同时保留其他字段类型。纯 JSON 结果不经 Host 字节识别或 Client 解析直接传递。参数、事件与流条目仍仅支持 JSON;不支持运行时对象循环。
|
||||
|
||||
流方法(`@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 方法产出的值,原样透传。
|
||||
流方法(`@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。Cordis 的跨副本 `Context.is()` 可识别传给构建产物 Service 的源码 Context,因此混合源码/构建组合仍保留此 accessor 和调用局部的 principal。生成的 Client 流方法返回 `RemoteStreamHandle<Out, In>`:在下行迭代之外提供 `send`、`end` 与 `dispose` 的句柄。上行项在 Host 逐项校验,因为它们来自浏览器;下行项是 Host 方法产出的值,原样透传。
|
||||
|
||||
`@Remote` 选项仅包含 `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 分发。[生成器](<../generator/README.zh.md#ordinary-remote-metadata>)负责发布及配套 codec。
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ export function bindTypertRemote<Service extends object>(
|
||||
const namespace = options.namespace ?? serviceKey
|
||||
validateName('namespace', namespace)
|
||||
const ctx: unknown = Reflect.get(service, 'ctx')
|
||||
if (ctx instanceof Context) provideInvocationAccessor(ctx)
|
||||
if (Context.is(ctx)) provideInvocationAccessor(ctx)
|
||||
return Object.freeze({ service, serviceKey, namespace })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
@@ -8,3 +9,53 @@ it.each(['../lib/index.js', '../lib/types/remote-info.js'])(
|
||||
expect(source).not.toMatch(/\b(?:from|import|require)\s*(?:\(\s*)?['"]zod(?:\/[^'"]*)?['"]/)
|
||||
},
|
||||
)
|
||||
|
||||
it('recognizes source and built Cordis Contexts in an active built RemoteAccess plugin', () => {
|
||||
const script = `
|
||||
import { Context as SourceContext } from '@deepseek-ai/cordis/src/index.ts'
|
||||
import { Context as BuiltContext } from '@deepseek-ai/cordis'
|
||||
import RemoteAccess from '@deepseek-ai/dsh-api-remote-access'
|
||||
|
||||
const observations = []
|
||||
for (const [copy, Context] of [['source', SourceContext], ['built', BuiltContext]]) {
|
||||
const root = new Context()
|
||||
let observed
|
||||
try {
|
||||
await root.plugin({ apply(ctx) {
|
||||
const access = new RemoteAccess(ctx)
|
||||
const guestInvocation = {
|
||||
request: { namespace: 'session', method: 'search', args: {} },
|
||||
peer: { principal: { kind: 'guest' } },
|
||||
}
|
||||
const guest = ctx.extend({ invocation: guestInvocation })
|
||||
observed = {
|
||||
copy,
|
||||
sameConstructor: ctx instanceof BuiltContext,
|
||||
recognized: BuiltContext.is(ctx),
|
||||
active: !!ctx.fiber.runtime,
|
||||
accessor: Object.hasOwn(root.reflect.props, 'invocation'),
|
||||
outside: ctx.invocation ?? null,
|
||||
allowsOutside: access.ctx.remoteAccess.allows({ kind: 'global' }),
|
||||
preservedGuest: guest.invocation === guestInvocation,
|
||||
allowsGuest: guest.remoteAccess.allows({ kind: 'global' }),
|
||||
}
|
||||
} })
|
||||
observations.push(observed)
|
||||
} finally {
|
||||
await root.fiber.dispose()
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(observations))
|
||||
`
|
||||
const output = execFileSync(process.execPath, ['--import', 'tsx/esm', '--input-type=module', '-e', script], {
|
||||
cwd: new URL('../../../api/remote-access/', import.meta.url),
|
||||
encoding: 'utf8',
|
||||
timeout: 15_000,
|
||||
})
|
||||
expect(JSON.parse(output)).toEqual([
|
||||
{ copy: 'source', sameConstructor: false, recognized: true, active: true, accessor: true,
|
||||
outside: null, allowsOutside: true, preservedGuest: true, allowsGuest: false },
|
||||
{ copy: 'built', sameConstructor: true, recognized: true, active: true, accessor: true,
|
||||
outside: null, allowsOutside: true, preservedGuest: true, allowsGuest: false },
|
||||
])
|
||||
})
|
||||
@@ -412,6 +412,12 @@ describe('typert-protocol Remote declarations', () => {
|
||||
expect(() => { directSecond[0]!.call(reverseService) }).toThrow('conflicting invocation markers')
|
||||
})
|
||||
|
||||
it('leaves bindings without a Cordis Context free of an invocation accessor', () => {
|
||||
const service = { ctx: {} }
|
||||
expect(bindTypertRemote(service, 'plain')).toEqual({ service, serviceKey: 'plain', namespace: 'plain' })
|
||||
expect(Object.hasOwn(service.ctx, 'invocation')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects ambiguous binding names', () => {
|
||||
expect(() => bindTypertRemote({}, '')).toThrow('service key')
|
||||
expect(() => bindTypertRemote({}, 'goals', { namespace: 'api/goals' })).toThrow('namespace')
|
||||
|
||||
Reference in new issue
Block a user