perf(api-gateway): cache SRC endpoint claims

This commit is contained in:
imccyu
2026-08-07 12:21:41 +08:00
parent eca3090dfa
commit e28ac506ed
6 changed files with 73 additions and 10 deletions

View File

@@ -76,12 +76,17 @@ export class TypertGatewayError extends Error {
export class TypertGatewayService extends Service implements TypertGateway {
static inject = ['typert']
private srcClaims: ReadonlySet<string> | undefined
/**
* Register the Gateway against the active TypeRT registry.
* @param ctx - owning Host Context with TypeRT registry access.
*/
constructor(ctx: Context) {
super(ctx, 'typertGateway')
ctx.on('internal/service', () => {
this.srcClaims = undefined
})
ctx.inject(['connection'], (connectionCtx) => {
connectionCtx.connection.rpc.intercept(
'/api',
@@ -95,18 +100,26 @@ export class TypertGatewayService extends Service implements TypertGateway {
private claimsEndpoint(endpoint: string): boolean {
const segments = endpoint.split('/')
if (segments.length !== 2 || segments[0] === '' || segments[1] === '') return false
const [namespace, method] = segments as [string, string]
if (this.ctx.typert.local.get(endpoint) !== undefined || this.ctx.typert.local.hasSeen(endpoint)) return true
this.srcClaims ??= this.collectSrcClaims()
return this.srcClaims.has(endpoint)
}
private collectSrcClaims(): ReadonlySet<string> {
const claims = new Set<string>()
for (const [serviceKey, definition] of Object.entries(this.ctx.reflect.props)) {
if (definition.type !== 'service') continue
const receiver = this.ctx.get(serviceKey) as unknown
if (!isObject(receiver)) continue
const original = originalOf(receiver)
const binding = Reflect.get(original, 'typertGateway') as unknown
if (!isObject(binding) || Reflect.get(binding, 'namespace') !== namespace) continue
if (remoteMethods(original).some(candidate => (candidate.exportName ?? candidate.method) === method)) return true
if (!isObject(binding) || typeof Reflect.get(binding, 'namespace') !== 'string') continue
const namespace = Reflect.get(binding, 'namespace') as string
for (const candidate of remoteMethods(original)) {
claims.add(endpointOf(namespace, candidate.exportName ?? candidate.method))
}
}
return false
return claims
}
/**

View File

@@ -314,6 +314,25 @@ class NoBindingService extends Service {
}
}
class ObservedClaimService extends Service {
private readonly binding = bindTypeRTGateway(this, 'observedClaim', { namespace: 'observed-claim' })
bindingReads = 0
constructor(ctx: Context) {
super(ctx, 'observedClaim')
}
get typertGateway() {
this.bindingReads += 1
return this.binding
}
@Remote
run(value: string): string {
return value
}
}
class MissingMethodService extends Service {
readonly typertGateway = bindTypeRTGateway(this, 'missingMethod', { namespace: 'missing-method' })
@@ -949,6 +968,36 @@ describe('TypertGatewayService', () => {
expect(connection.handler).toBeUndefined()
})
it('caches SRC ownership until the Cordis Service set changes', async () => {
const ctx = new Context()
await ctx.plugin(TypertRegistry)
await ctx.plugin(FakeConnectionService)
await ctx.plugin(TypertGatewayService)
const observedFiber = ctx.plugin(ObservedClaimService)
await observedFiber
const connection = rawConnection(ctx)
const observed = ctx.get('observedClaim') as unknown as ObservedClaimService & {
[symbols.original]?: ObservedClaimService
}
const service = observed[symbols.original] ?? observed
expect(connection.matches?.('legacy/list')).toBe(false)
expect(connection.matches?.('legacy/list')).toBe(false)
expect(service.bindingReads).toBe(1)
expect(connection.matches?.('observed-claim/run')).toBe(true)
expect(connection.matches?.('observed-claim/run')).toBe(true)
expect(service.bindingReads).toBe(1)
const unrelatedFiber = ctx.plugin(NoBindingService)
await unrelatedFiber
expect(connection.matches?.('legacy/list')).toBe(false)
expect(service.bindingReads).toBe(2)
await observedFiber.dispose()
expect(connection.matches?.('observed-claim/run')).toBe(false)
await unrelatedFiber.dispose()
})
it('dispatches claimed invocations through /api and leaves unclaimed endpoints to its fallback', async () => {
const ctx = new Context().extend({ fixtureScope: 'http-caller' })
const routes: WebRoute[] = []