feat: add TypeRT remote gateway infrastructure

This commit is contained in:
imccyu
2026-08-05 11:17:47 +08:00
parent effd8e1ebd
commit 64a963da0b
98 changed files with 7812 additions and 444 deletions

View File

@@ -135,6 +135,11 @@ export function validateTypertManifest(pkgName: string, exported: unknown): Type
requireMembers(pkgName, object.members, `object "${object.name as string}"`)
requireTypes(pkgName, object.types, `object "${object.name as string}"`)
}
if (manifest.invocations !== undefined) {
for (const value of requireArray(pkgName, manifest.invocations, 'TYPERT.invocations')) {
requireInvocation(pkgName, value)
}
}
return manifest as unknown as TypertContribution
}
@@ -184,6 +189,88 @@ function requireTypes(pkgName: string, value: unknown, subject: string): void {
}
}
function requireInvocation(pkgName: string, value: unknown): void {
const invocation = requireObject(pkgName, value, 'invocation')
for (const key of ['id', 'service', 'namespace', 'method'] as const) {
requireString(pkgName, invocation, key, 'invocation')
}
const id = invocation.id as string
const receiver = requireObject(pkgName, invocation.invocation, `invocation "${id}" receiver`)
if (receiver.kind === 'context') {
requireString(pkgName, receiver, 'context', `invocation "${id}" Context receiver`)
requireString(pkgName, receiver, 'wire', `invocation "${id}" Context receiver`)
requireStrictCodec(pkgName, receiver.codec, `invocation "${id}" Context codec`)
} else if (receiver.kind !== 'direct') {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" receiver kind must be "direct" or "context"`)
}
const wires = new Set<string>()
const parameters = new Map<string, Record<string, unknown>>()
let lookupCount = 0
for (const valueParameter of requireArray(pkgName, invocation.parameters, `invocation "${id}" parameters`)) {
const parameter = requireObject(pkgName, valueParameter, `invocation "${id}" parameter`)
requireString(pkgName, parameter, 'name', `invocation "${id}" parameter`)
requireString(pkgName, parameter, 'wire', `invocation "${id}" parameter`)
const wire = parameter.wire as string
if (wires.has(wire)) {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" repeats wire field "${wire}"`)
}
wires.add(wire)
if (parameter.source === 'lookup') {
lookupCount += 1
requireString(pkgName, parameter, 'lookup', `invocation "${id}" lookup parameter`)
} else if (parameter.source === 'json') {
if (parameter.lookup !== undefined) {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" JSON parameter declares a lookup`)
}
} else {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" parameter source must be "json" or "lookup"`)
}
parameters.set(wire, parameter)
requireStrictCodec(pkgName, parameter.codec, `invocation "${id}" parameter codec`)
}
if (invocation.scope !== undefined) {
if (receiver.kind !== 'direct') {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" Context receiver cannot declare a direct scope projection`)
}
const scope = requireObject(pkgName, invocation.scope, `invocation "${id}" scope`)
requireString(pkgName, scope, 'context', `invocation "${id}" scope`)
requireString(pkgName, scope, 'wire', `invocation "${id}" scope`)
const parameter = parameters.get(scope.wire as string)
if (lookupCount !== 1 || parameter?.source !== 'lookup' || parameter.lookup !== scope.context) {
throw new Error(
`typert-loader: ${pkgName} invocation "${id}" scope wire "${scope.wire as string}" must select its only lookup parameter`,
)
}
}
if (receiver.kind === 'context' && wires.has(receiver.wire as string)) {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" repeats Context wire field "${receiver.wire as string}"`)
}
requireStrictCodec(pkgName, invocation.result, `invocation "${id}" result codec`)
if (invocation.sourceLocation !== undefined) {
const location = requireObject(pkgName, invocation.sourceLocation, `invocation "${id}" sourceLocation`)
requireString(pkgName, location, 'file', `invocation "${id}" sourceLocation`)
for (const key of ['line', 'column'] as const) {
if (!Number.isInteger(location[key]) || (location[key] as number) < 1) {
throw new Error(`typert-loader: ${pkgName} invocation "${id}" sourceLocation.${key} must be a positive integer`)
}
}
}
}
function requireStrictCodec(pkgName: string, value: unknown, subject: string): void {
const codec = requireObject(pkgName, value, subject)
if (codec.mode !== 'strict') {
throw new Error(`typert-loader: ${pkgName} ${subject} must use a strict codec`)
}
requireString(pkgName, codec, 'typeSymbol', subject)
if (typeof codec.schema !== 'object'
|| codec.schema === null
|| !('_zod' in codec.schema)
|| typeof (codec.schema as { parse?: unknown }).parse !== 'function') {
throw new Error(`typert-loader: ${pkgName} ${subject} is not backed by a zod v4 schema`)
}
}
/**
* Scan current Loader entries during activation, then follow entry mounts and
* unmounts for this plugin's lifetime.
@@ -202,7 +289,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const configured = new Set((config as ResolvedConfig).packages)
// Registered contributions by entry name; the disposer withdraws the entry's registration.
const registered = new Map<string, () => void>()
const registered = new Map<string, () => Promise<void>>()
// In-flight import/register tasks by entry name.
const pending = new Map<string, Promise<void>>()
// Artifact paths by package name. Negative verdicts (unresolvable specifier —
@@ -279,7 +366,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const dispose = registered.get(entryName)
if (dispose !== undefined) {
registered.delete(entryName)
dispose()
return dispose()
}
return undefined
}

View File

@@ -1,4 +1,5 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -8,6 +9,7 @@ import Loader from '@cordisjs/plugin-loader'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import * as typertLoader from '@deepseek-ai/dsh-typert-loader'
import { validateTypertManifest } from '@deepseek-ai/dsh-typert-loader'
import { z } from 'zod'
let root: string | undefined
let context: Context | undefined
@@ -63,12 +65,45 @@ function typertSource(pkgName: string, entryName: string): string {
].join('\n')
}
function invocationTypertSource(pkgName: string): string {
return [
'import { z } from \'zod\'',
'const Text = z.string()',
'export const TYPERT = {',
` package: '${pkgName}',`,
' face: \'host\',',
' schemas: [],',
' model: { services: [], events: [], objects: [] },',
' invocations: [{',
` id: '${pkgName}#goals/create',`,
' service: \'goals\', namespace: \'goals\', method: \'create\',',
' invocation: { kind: \'direct\' },',
' parameters: [{',
' name: \'request\', wire: \'request\', source: \'json\',',
` codec: { mode: 'strict', typeSymbol: '${pkgName}/types#Request', schema: Text },`,
' }],',
` result: { mode: 'strict', typeSymbol: '${pkgName}/types#Result', schema: Text },`,
' sourceLocation: { file: \'src/index.ts\', line: 8, column: 3 },',
' }],',
'}',
'',
].join('\n')
}
/** Boot a real Loader over a fixture root; plugin modules resolve from its node_modules. */
async function boot(): Promise<Context> {
context = new Context()
context.baseUrl = pathToFileURL(join(root as string, 'cordis.yml')).href
await context.plugin(TypertRegistry)
await context.plugin(Loader)
const fixtureRequire = createRequire(context.baseUrl)
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
const module: unknown = await import(pathToFileURL(fixtureRequire.resolve(specifier)).href)
return module
},
} as unknown as NonNullable<typeof context.loader.internal>
// zod must be resolvable from the fixture packages; link the workspace copy.
await mkdir(join(root as string, 'node_modules'), { recursive: true })
return context
@@ -105,6 +140,33 @@ describe('typert loader', () => {
expect(ctx.typert.getPackage('@fixture/nested')).toBeUndefined()
})
it('registers a strict invocation into the local registry and withdraws it with the loader', LOADER_TEST_TIMEOUT, async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-typert-loader-'))
await linkZod(root)
await writePackage(root, '@fixture/invocation', {
typertSource: invocationTypertSource('@fixture/invocation'),
})
const ctx = await boot()
const fiber = mountTypertLoader(ctx, { packages: ['@fixture/invocation'] })
await fiber
const descriptor = ctx.typert.local.get('goals/create')
expect(descriptor).toMatchObject({
id: '@fixture/invocation#goals/create',
invocation: { kind: 'direct' },
parameters: [{ wire: 'request', source: 'json' }],
sourceLocation: { file: 'src/index.ts', line: 8, column: 3 },
})
expect(descriptor?.parameters[0]?.codec.mode).toBe('strict')
if (descriptor?.parameters[0]?.codec.mode === 'strict') {
expect(descriptor.parameters[0].codec.schema.parse('request')).toBe('request')
}
await fiber.dispose()
expect(ctx.typert.local.get('goals/create')).toBeUndefined()
})
it('fails loud when an explicit package is absent or has no Typert export', LOADER_TEST_TIMEOUT, async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-typert-loader-'))
await writePackage(root, '@fixture/plain')
@@ -427,8 +489,156 @@ describe('validateTypertManifest', () => {
model: { ...complete.model, objects: [{ ...complete.model.objects[0], exportName: '' }] },
})).toThrow('object has a missing or empty exportName')
})
it('validates strict invocation descriptors and accepts legacy manifests without them', () => {
const legacy = completeManifest(zodish)
expect(validateTypertManifest('pkg', legacy)).toBe(legacy)
const descriptor = strictInvocation()
const manifest = { ...legacy, invocations: [descriptor] }
expect(validateTypertManifest('pkg', manifest)).toBe(manifest)
const scoped = {
...descriptor,
scope: { context: 'agent', wire: 'agentId' },
parameters: [{
name: 'agent',
wire: 'agentId',
source: 'lookup',
lookup: 'agent',
codec: strictCodec('pkg#AgentId'),
}, ...descriptor.parameters],
}
expect(validateTypertManifest('pkg', { ...legacy, invocations: [scoped] }).invocations)
.toEqual([scoped])
expect(() => validateTypertManifest('pkg', { ...legacy, invocations: {} }))
.toThrow('TYPERT.invocations must be an array')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...descriptor, invocation: { kind: 'future' } }],
})).toThrow('receiver kind must be "direct" or "context"')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...descriptor, result: { mode: 'src-json' } }],
})).toThrow('result codec must use a strict codec')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...descriptor, result: { mode: 'strict', typeSymbol: 'pkg#Result', schema: zodish } }],
})).toThrow('result codec is not backed by a zod v4 schema')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...descriptor,
parameters: [{ ...descriptor.parameters[0], source: 'future' }],
}],
})).toThrow('parameter source must be "json" or "lookup"')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...descriptor,
parameters: [{ ...descriptor.parameters[0], source: 'lookup' }],
}],
})).toThrow('lookup parameter has a missing or empty lookup')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...descriptor,
parameters: [{ ...descriptor.parameters[0], lookup: 'agent' }],
}],
})).toThrow('JSON parameter declares a lookup')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...descriptor,
parameters: [descriptor.parameters[0], { ...descriptor.parameters[0], name: 'again' }],
}],
})).toThrow('repeats wire field "request"')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...descriptor,
invocation: {
kind: 'context',
context: 'agent',
wire: 'request',
codec: strictCodec('pkg#AgentId'),
},
}],
})).toThrow('repeats Context wire field "request"')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...scoped, scope: null }],
})).toThrow('scope must be an object')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...scoped, scope: { wire: 'agentId' } }],
})).toThrow('scope has a missing or empty context')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...scoped, scope: { context: 'agent' } }],
})).toThrow('scope has a missing or empty wire')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...scoped,
invocation: {
kind: 'context',
context: 'agent',
wire: 'scopeId',
codec: strictCodec('pkg#AgentId'),
},
}],
})).toThrow('Context receiver cannot declare a direct scope projection')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...scoped, scope: { context: 'agent', wire: 'missingId' } }],
})).toThrow('must select its only lookup parameter')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{
...scoped,
parameters: [...scoped.parameters, {
name: 'other',
wire: 'otherId',
source: 'lookup',
lookup: 'agent',
codec: strictCodec('pkg#AgentId'),
}],
}],
})).toThrow('must select its only lookup parameter')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...scoped, scope: { context: 'other', wire: 'agentId' } }],
})).toThrow('must select its only lookup parameter')
expect(() => validateTypertManifest('pkg', {
...legacy,
invocations: [{ ...descriptor, sourceLocation: { file: 'src/index.ts', line: 0, column: 1 } }],
})).toThrow('sourceLocation.line must be a positive integer')
})
})
function strictCodec(typeSymbol: string) {
return { mode: 'strict', typeSymbol, schema: z.string() }
}
function strictInvocation() {
return {
id: 'pkg#goals/create',
service: 'goals',
namespace: 'goals',
method: 'create',
invocation: { kind: 'direct' },
parameters: [{
name: 'request',
wire: 'request',
source: 'json',
codec: strictCodec('pkg#Request'),
}],
result: strictCodec('pkg#Result'),
sourceLocation: { file: 'src/index.ts', line: 1, column: 1 },
}
}
function completeManifest(zodish: object) {
const member = { name: 'member', signature: 'member(): void', kind: 'method' }
const type = { name: 'Value', declaration: 'export interface Value {}' }