fix(typert): preserve remote lookup semantics
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/typert/registry/README.md
|
||||
README.md: 83c03ab284abf2b7cab4dd1ee70d7e855184a1e0
|
||||
README.zh.md: db2140e51d85be53bbf6eb4d1dd86ec38ceefc58
|
||||
README.md: dae8c3ed124fd6e2d61eb47964e2c07dda762b48
|
||||
README.zh.md: aea74b3753feccd88ee132363dc60ade02161498
|
||||
|
||||
@@ -9,6 +9,7 @@ Package reflection is keyed by `<package>#<face>`. Schemas are keyed by `<packag
|
||||
## Public API
|
||||
|
||||
- `TypertRegistry` is the default plugin and provides `ctx.typert`.
|
||||
- `ctx.typert.lookups.register()` registers the wire declaration and default resolver owned by the business package; `configure()` registers a resolver owned by Host composition that may run asynchronously. Their lifetimes are independent: configuration may precede the provider, and unloading the configuration restores the default policy.
|
||||
- `register(contribution)` rejects malformed identities and duplicate package-face or schema keys before committing anything, then returns the exact Cordis effect disposer.
|
||||
- `get(key)`, `resolve(key)`, and `list(filter?)` query live schemas. `resolve()` distinguishes a malformed key, an absent package, and a package that contributes no schema under that name.
|
||||
- `getPackage(packageName, face?)` and `listPackages(filter?)` query generated service, event, and object reflection; the default face is `host`.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
## 公开 API
|
||||
|
||||
- `TypertRegistry` 是默认插件,并提供 `ctx.typert`。
|
||||
- `ctx.typert.lookups.register()` 注册业务包拥有的 wire 声明和默认 resolver;`configure()` 注册 Host 组合拥有、可异步执行的 resolver。两者生命周期独立,配置可以先于 provider,卸载配置会恢复默认策略。
|
||||
- `register(contribution)` 会在提交任何内容之前拒绝格式错误的标识,以及重复的包与 face 组合键或 schema 键,随后返回 Cordis effect 提供的同一资源释放函数。
|
||||
- `get(key)`、`resolve(key)` 和 `list(filter?)` 查询当前有效的 schema。`resolve()` 能区分格式错误的键、未注册的包,以及已注册但未以该名称提供 schema 的包。
|
||||
- `getPackage(packageName, face?)` 和 `listPackages(filter?)` 查询生成的服务、事件和对象反射信息;默认 face 为 `host`。
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
TypeRTLookupDefinition,
|
||||
TypeRTLookupMap,
|
||||
TypeRTLookupProvider,
|
||||
TypeRTLookupResolver,
|
||||
TypeRTLookupRegistry,
|
||||
TypeRTLookupWire,
|
||||
TypeRTRemoteContribution,
|
||||
@@ -213,6 +214,7 @@ class RemoteStore {
|
||||
|
||||
class LookupStore {
|
||||
private readonly providers = new Map<string, ProviderEntry<TypeRTLookupProvider>>()
|
||||
private readonly resolvers = new Map<string, ProviderEntry<LookupResolverEntry>>()
|
||||
private readonly definitions = new Map<string, TypeRTLookupDefinition>()
|
||||
private readonly changes: ChangeSource
|
||||
|
||||
@@ -229,13 +231,61 @@ class LookupStore {
|
||||
TypeRTLookupWire<TypeRTLookupMap[K]>
|
||||
>,
|
||||
) => this.register(ctx, key, provider),
|
||||
get: key => this.providers.get(key)?.provider,
|
||||
configure: <K extends Extract<keyof TypeRTLookupMap, string>>(
|
||||
key: K,
|
||||
resolver: TypeRTLookupResolver<
|
||||
TypeRTLookupHost<TypeRTLookupMap[K]>,
|
||||
TypeRTLookupWire<TypeRTLookupMap[K]>
|
||||
>,
|
||||
) => this.configure(ctx, key, resolver),
|
||||
get: key => this.get(key),
|
||||
definitions: () => [...this.definitions.values()],
|
||||
keys: () => [...this.providers.keys()],
|
||||
subscribe: listener => this.changes.subscribe(ctx, listener),
|
||||
}
|
||||
}
|
||||
|
||||
private get(key: string): TypeRTLookupProvider | undefined {
|
||||
const provider = this.providers.get(key)?.provider
|
||||
if (provider === undefined) return undefined
|
||||
const resolver = this.resolvers.get(key)?.provider
|
||||
if (resolver === undefined) return provider
|
||||
return {
|
||||
parameter: provider.parameter,
|
||||
wire: provider.wire,
|
||||
hostTypeSymbol: provider.hostTypeSymbol,
|
||||
wireTypeSymbol: provider.wireTypeSymbol,
|
||||
resolve: id => resolver.resolve(id),
|
||||
}
|
||||
}
|
||||
|
||||
private configure<Host, Wire>(
|
||||
ctx: Context,
|
||||
key: string,
|
||||
resolver: TypeRTLookupResolver<Host, Wire>,
|
||||
): TypeRTDisposer {
|
||||
validateSegment('lookup key', key)
|
||||
if (this.resolvers.has(key)) throw new Error(`typert: lookup "${key}" resolver is already configured`)
|
||||
const owner = {}
|
||||
// The map erases each merge-declared Wire type; restore it only at the
|
||||
// typed configure() boundary so strict function variance remains sound.
|
||||
const entry: ProviderEntry<LookupResolverEntry> = {
|
||||
provider: { resolve: async id => resolver(id as Wire) },
|
||||
owner,
|
||||
}
|
||||
const { resolvers, changes } = this
|
||||
return ctx.effect(function* () {
|
||||
resolvers.set(key, entry)
|
||||
changes.emit({ kind: 'lookup', key })
|
||||
yield () => {
|
||||
/* v8 ignore next -- duplicate configuration is rejected, so this effect remains the key's unique owner. */
|
||||
if (resolvers.get(key) !== entry) return
|
||||
resolvers.delete(key)
|
||||
changes.emit({ kind: 'lookup', key })
|
||||
}
|
||||
}, `typert.lookups.configure(${JSON.stringify(key)})`)
|
||||
}
|
||||
|
||||
private register<Host, Wire>(ctx: Context, key: string, provider: TypeRTLookupProvider<Host, Wire>): TypeRTDisposer {
|
||||
validateSegment('lookup key', key)
|
||||
validateSegment('lookup parameter', provider.parameter)
|
||||
@@ -271,6 +321,10 @@ class LookupStore {
|
||||
}
|
||||
}
|
||||
|
||||
interface LookupResolverEntry {
|
||||
resolve(id: unknown): Promise<unknown>
|
||||
}
|
||||
|
||||
function lookupDefinitionEquals(left: TypeRTLookupDefinition, right: TypeRTLookupDefinition): boolean {
|
||||
return left.parameter === right.parameter
|
||||
&& left.wire === right.wire
|
||||
|
||||
@@ -355,6 +355,40 @@ describe('TypertRegistry', () => {
|
||||
expect(ctx.typert.contexts.getClient('registryFixture')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('configures an asynchronous lookup resolver independently of provider load order', async () => {
|
||||
const ctx = await makeCtx()
|
||||
const fallback = { id: 'fallback' }
|
||||
const configured = { id: 'configured' }
|
||||
const disposeResolver = ctx.typert.lookups.configure('fixture', async id =>
|
||||
id === configured.id ? configured : undefined)
|
||||
|
||||
expect(ctx.typert.lookups.get('fixture')).toBeUndefined()
|
||||
const disposeProvider = ctx.typert.lookups.register('fixture', {
|
||||
parameter: 'agent',
|
||||
wire: 'agentId',
|
||||
hostTypeSymbol: '@fixture/agent#Agent',
|
||||
wireTypeSymbol: '@fixture/session#SessionId',
|
||||
resolve: id => id === fallback.id ? fallback : undefined,
|
||||
})
|
||||
await expect(ctx.typert.lookups.get('fixture')?.resolve('configured')).resolves.toBe(configured)
|
||||
expect(() => ctx.typert.lookups.configure('fixture', () => undefined)).toThrow('already configured')
|
||||
|
||||
await disposeProvider()
|
||||
expect(ctx.typert.lookups.get('fixture')).toBeUndefined()
|
||||
const disposeReloadedProvider = ctx.typert.lookups.register('fixture', {
|
||||
parameter: 'agent',
|
||||
wire: 'agentId',
|
||||
hostTypeSymbol: '@fixture/agent#Agent',
|
||||
wireTypeSymbol: '@fixture/session#SessionId',
|
||||
resolve: id => id === fallback.id ? fallback : undefined,
|
||||
})
|
||||
await expect(ctx.typert.lookups.get('fixture')?.resolve('configured')).resolves.toBe(configured)
|
||||
|
||||
await disposeResolver()
|
||||
expect(ctx.typert.lookups.get('fixture')?.resolve('fallback')).toBe(fallback)
|
||||
await disposeReloadedProvider()
|
||||
})
|
||||
|
||||
it('publishes provider changes, rejects duplicate providers, and disposes subscriptions', async () => {
|
||||
const ctx = await makeCtx()
|
||||
const changes: string[] = []
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/typert/type-meta/README.md
|
||||
README.md: 245df305efcf711486b2d3f32e40a8b415f2682e
|
||||
README.zh.md: 592aa5d027a52a7a277a90ba5d51f19101f055f6
|
||||
README.md: b394c843409e840b75bbb08b128614379e528001
|
||||
README.zh.md: 5bd9bb18289a0320e0603d8b373e60d7f1e3c7e5
|
||||
|
||||
@@ -20,7 +20,7 @@ Decorator initializers retain markers in a module-private `WeakMap` keyed by the
|
||||
|
||||
Business packages extend `TypeRTLookupMap` and `TypeRTContextMap` to associate Host objects or scoped Contexts with their wire identities. Generated artifacts extend `TypeRTRemoteMap`, `TypeRTRemoteContextMap`, and `TypeRTRemoteNamespaceMap` so Client imports expose only selected Remote methods. `InvocationDescriptor` is the shared runtime form consumed by the registry, Gateway, and Client API.
|
||||
|
||||
Lookup and Context packages own both sides of their contract: declaration merging supplies the static association, while runtime providers register identity resolution with `ctx.typert`. Strict codecs carry generated schemas; `src-json` codecs identify the weaker source-launch path.
|
||||
Lookup and Context packages own both sides of their contract: declaration merging supplies the static association, while runtime providers register identity resolution with `ctx.typert`. A lookup provider supplies the stable declaration and default resolver, while Host composition may separately configure a synchronous or asynchronous resolver; policy rejections may use `TypeRTLookupFailure` to carry a failure value owned by the boundary adapter. Strict codecs carry generated schemas; `src-json` codecs identify the weaker source-launch path.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Host 方法通过将 `signal: AbortSignal` 声明为最后一个参数来启用
|
||||
|
||||
业务包扩展 `TypeRTLookupMap` 和 `TypeRTContextMap`,以关联 Host 对象或作用域 Context 与其协议身份。生成的产物扩展 `TypeRTRemoteMap`、`TypeRTRemoteContextMap` 和 `TypeRTRemoteNamespaceMap`,使 Client 导入后仅暴露选定的 Remote 方法。`InvocationDescriptor` 是供注册表、Gateway 和 Client API 使用的共享运行时形式。
|
||||
|
||||
查找包与 Context 包同时负责其契约的两侧:声明合并提供静态关联,运行时提供方则向 `ctx.typert` 注册身份解析。严格编解码器携带生成的 schema;`src-json` 编解码器标识约束更弱的源码启动路径。
|
||||
查找包与 Context 包同时负责其契约的两侧:声明合并提供静态关联,运行时提供方则向 `ctx.typert` 注册身份解析。lookup provider 提供稳定声明与默认 resolver,Host 组合可以另行配置同步或异步 resolver;策略拒绝可用 `TypeRTLookupFailure` 携带由边界适配器拥有的失败值。严格编解码器携带生成的 schema;`src-json` 编解码器标识约束更弱的源码启动路径。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -18,6 +18,25 @@ export function isTypeRTRemoteSegment(value: string): boolean {
|
||||
return value !== '.' && value !== '..' && TYPERT_REMOTE_SEGMENT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* A lookup policy rejection whose typed payload belongs to the active boundary adapter.
|
||||
* Gateway adapters preserve this payload instead of collapsing it into an infrastructure failure.
|
||||
*/
|
||||
export class TypeRTLookupFailure<Failure = unknown> extends Error {
|
||||
/** Adapter-owned failure returned to the caller. */
|
||||
readonly failure: Failure
|
||||
|
||||
/**
|
||||
* Wrap one adapter failure without exposing the rejected identity.
|
||||
* @param failure - typed failure owned by the active boundary adapter.
|
||||
*/
|
||||
constructor(failure: Failure) {
|
||||
super('TypeRT lookup policy rejected the requested identity')
|
||||
this.name = 'TypeRTLookupFailure'
|
||||
this.failure = failure
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
InvocationDescriptor,
|
||||
InvocationParameterDescriptor,
|
||||
@@ -36,6 +55,7 @@ export type {
|
||||
TypeRTLookupHost,
|
||||
TypeRTLookupMap,
|
||||
TypeRTLookupProvider,
|
||||
TypeRTLookupResolver,
|
||||
TypeRTLookupRegistry,
|
||||
TypeRTLookupWire,
|
||||
TypeRTRemoteContextApi,
|
||||
|
||||
@@ -176,7 +176,16 @@ export interface TypeRTRemoteContribution {
|
||||
readonly descriptors: readonly InvocationDescriptor[]
|
||||
}
|
||||
|
||||
/** Runtime resolver for one declared Host object lookup. */
|
||||
/**
|
||||
* Resolve one validated wire identity, synchronously or asynchronously.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the Host object, or `undefined` when unavailable.
|
||||
*/
|
||||
export type TypeRTLookupResolver<Host = unknown, Wire = unknown> = (
|
||||
id: Wire,
|
||||
) => Host | undefined | Promise<Host | undefined>
|
||||
|
||||
/** Runtime provider for one declared Host object lookup. */
|
||||
export interface TypeRTLookupProvider<Host = unknown, Wire = unknown> {
|
||||
/** Source parameter name recognized by the SRC weak parser. */
|
||||
readonly parameter: string
|
||||
@@ -187,11 +196,11 @@ export interface TypeRTLookupProvider<Host = unknown, Wire = unknown> {
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
/**
|
||||
* Resolve a wire identity to the current live Host object.
|
||||
* Resolve a wire identity through the provider's default policy.
|
||||
* @param id - validated wire identity.
|
||||
* @returns the live object, or `undefined` when it is unavailable.
|
||||
* @returns the object, `undefined` when unavailable, or either asynchronously.
|
||||
*/
|
||||
resolve(id: Wire): Host | undefined
|
||||
resolve(id: Wire): Host | undefined | Promise<Host | undefined>
|
||||
}
|
||||
|
||||
/** Stable wire declaration retained after a lookup provider unloads. */
|
||||
@@ -304,6 +313,20 @@ export interface TypeRTLookupRegistry {
|
||||
TypeRTLookupWire<TypeRTLookupMap[K]>
|
||||
>,
|
||||
): TypeRTDisposer
|
||||
/**
|
||||
* Replace one provider's default resolution policy while this contribution is active.
|
||||
* Configuration may precede provider registration; without a live provider, `get()` remains unavailable.
|
||||
* @param key - lookup key whose wire declaration remains provider-owned.
|
||||
* @param resolver - composition-owned resolver used by every lookup of this key.
|
||||
* @returns disposer restoring the provider's default resolver.
|
||||
*/
|
||||
configure<K extends StringKeyOf<TypeRTLookupMap>>(
|
||||
key: K,
|
||||
resolver: TypeRTLookupResolver<
|
||||
TypeRTLookupHost<TypeRTLookupMap[K]>,
|
||||
TypeRTLookupWire<TypeRTLookupMap[K]>
|
||||
>,
|
||||
): TypeRTDisposer
|
||||
/**
|
||||
* Look up one provider by runtime key.
|
||||
* @param key - descriptor lookup key.
|
||||
|
||||
Reference in New Issue
Block a user