@@ -4,26 +4,8 @@ import { resolveLogs } from '../logging/resolve';
44import type { Logs } from '../logging/types' ;
55import { now } from '../telemetry/Telemetry' ;
66
7- export type RegistryCaller < R extends ServiceRegistry = ServiceRegistry > = (
8- serviceName : keyof R & string ,
9- methodName : string ,
10- args ?: JsonObject ,
11- ) => Promise < JsonObject > ;
12-
13- export function createCaller < R extends ServiceRegistry > ( registry : R , ctx : ServiceContext ) : RegistryCaller < R > {
14- return ( serviceName , methodName , args ) => callServiceMethod ( registry , serviceName , methodName , ( args ?? { } ) as JsonObject , ctx ) ;
15- }
16-
17- // ctx has a bound `call` function (returns the same object reference)?
18- export function ensureServiceCaller < R extends ServiceRegistry > (
19- registry : R ,
20- ctx : ServiceContext & Partial < { call : RegistryCaller < R > } > ,
21- ) : asserts ctx is ServiceContext & { call : RegistryCaller < R > } {
22- if ( ! ctx . call ) ( ctx as any ) . call = createCaller ( registry , ctx ) ;
23- }
24-
257// runtime checks instead happens at the boundary
26- type JsonPrimitive = string | number | boolean | null ;
8+ export type JsonPrimitive = string | number | boolean | null ;
279export type JsonValue = JsonPrimitive | JsonValue [ ] | { [ k : string ] : JsonValue } ;
2810export type JsonObject = { [ k : string ] : JsonValue } ;
2911
@@ -35,15 +17,56 @@ const runSchema = <T>(schema: NarrowSchema<T> | undefined, input: unknown): T =>
3517 return typeof ( schema as any ) . parse === 'function' ? ( schema as any ) . parse ( input ) : ( schema as ( u : unknown ) => T ) ( input ) ;
3618} ;
3719
38- export type ServiceContext = {
20+ type BaseServiceContext = {
3921 signal ?: AbortSignal ; // request/client abort passed in request
4022 deadlineMs ?: number ; // available to userland; not enforced here
4123 traceId ?: string ;
4224 logger ?: Logs ;
4325 user ?: { id : string ; roles : string [ ] } | null ;
44- call ?: ( service : string , method : string , args ?: JsonObject ) => Promise < JsonObject > ;
4526} ;
4627
28+ type UntypedRegistryCaller = ( serviceName : string , methodName : string , args ?: JsonObject ) => Promise < JsonObject > ;
29+ type RuntimeServiceContext = BaseServiceContext & { call ?: UntypedRegistryCaller } ;
30+
31+ // Augment with app-specific fields only; use TypedServiceContext<typeof serviceRegistry>
32+ // when you want a registry-aware ctx.call type.
33+ export interface ServiceContext extends BaseServiceContext { }
34+
35+ export type ServiceMethod < P extends JsonObject = JsonObject , R extends JsonObject = JsonObject , Ctx extends BaseServiceContext = TypedServiceContext > = (
36+ params : P ,
37+ ctx : Ctx ,
38+ ) => Promise < R > ;
39+ type RuntimeServiceMethod < P extends JsonObject = JsonObject , R extends JsonObject = JsonObject > = ( params : P , ctx : RuntimeServiceContext ) => Promise < R > ;
40+
41+ export type ServiceDefinition = Readonly < Record < string , RuntimeServiceMethod < any , JsonObject > > > ;
42+ export type ServiceRegistry = Readonly < Record < string , ServiceDefinition > > ;
43+
44+ type ServiceMethodParams < M > = M extends ( params : infer P , ctx : any ) => Promise < any > ? P : never ;
45+ type ServiceMethodResult < M > = Awaited < M extends ( ...args : any [ ] ) => Promise < infer R > ? R : never > ;
46+ type RegistryCallerArgs < R extends ServiceRegistry , S extends keyof R & string , M extends keyof R [ S ] & string > = undefined extends ServiceMethodParams < R [ S ] [ M ] >
47+ ? [ serviceName : S , methodName : M , args ?: ServiceMethodParams < R [ S ] [ M ] > ]
48+ : [ serviceName : S , methodName : M , args : ServiceMethodParams < R [ S ] [ M ] > ] ;
49+
50+ export type RegistryCaller < R extends ServiceRegistry = ServiceRegistry > = < S extends keyof R & string , M extends keyof R [ S ] & string > (
51+ ...args : RegistryCallerArgs < R , S , M >
52+ ) => Promise < ServiceMethodResult < R [ S ] [ M ] > > ;
53+
54+ // Binds ctx.call to a concrete registry without creating a parallel contract type.
55+ export type TypedServiceContext < R extends ServiceRegistry = ServiceRegistry > = ServiceContext & { call ?: RegistryCaller < R > } ;
56+
57+ export function createCaller < R extends ServiceRegistry > ( registry : R , ctx : BaseServiceContext ) : RegistryCaller < R > {
58+ return ( ( ( serviceName : string , methodName : string , args ?: JsonObject ) =>
59+ callServiceMethod ( registry , serviceName , methodName , ( args ?? { } ) as JsonObject , ctx ) ) as unknown ) as RegistryCaller < R > ;
60+ }
61+
62+ // ctx has a bound `call` function (returns the same object reference)?
63+ export function ensureServiceCaller < R extends ServiceRegistry > (
64+ registry : R ,
65+ ctx : BaseServiceContext & Partial < { call : RegistryCaller < R > } > ,
66+ ) : asserts ctx is BaseServiceContext & { call : RegistryCaller < R > } {
67+ if ( ! ctx . call ) ( ctx as any ) . call = createCaller ( registry , ctx ) ;
68+ }
69+
4770// Helper for userland: combine a parent AbortSignal with a per-call timeout
4871export function withDeadline ( signal : AbortSignal | undefined , ms ?: number ) : AbortSignal | undefined {
4972 if ( ! ms ) return signal ;
@@ -63,45 +86,42 @@ export function withDeadline(signal: AbortSignal | undefined, ms?: number): Abor
6386 return ctrl . signal ;
6487}
6588
66- export type ServiceMethod < P , R extends JsonObject = JsonObject > = ( params : P , ctx : ServiceContext ) => Promise < R > ;
67- export type ServiceDefinition = Readonly < Record < string , ServiceMethod < any , JsonObject > > > ;
68- export type ServiceRegistry = Readonly < Record < string , ServiceDefinition > > ;
69-
7089export type ServiceDescriptor = {
7190 serviceName : string ;
7291 serviceMethod : string ;
7392 args ?: JsonObject ;
7493} ;
7594
95+ type ServiceSpecEntry =
96+ | ServiceMethod < any , JsonObject >
97+ | { handler : ServiceMethod < any , JsonObject > ; params ?: NarrowSchema < any > ; result ?: NarrowSchema < any > } ;
98+ type ServiceSpec = Record < string , ServiceSpecEntry > ;
99+ type ExtractServiceMethod < T > = T extends { handler : infer H } ? H : T ;
100+ type NormalizeServiceMethod < M > = M extends ( params : infer P extends JsonObject , ctx : any ) => Promise < infer R extends JsonObject > ? RuntimeServiceMethod < P , R > : never ;
101+ type NormalizedServiceSpec < T extends ServiceSpec > = {
102+ [ K in keyof T ] : NormalizeServiceMethod < ExtractServiceMethod < T [ K ] > > ;
103+ } ;
104+
76105export function defineService <
77- T extends Record <
78- string ,
79- ServiceMethod < any , JsonObject > | { handler : ServiceMethod < any , JsonObject > ; params ?: NarrowSchema < any > ; result ?: NarrowSchema < any > }
80- > ,
106+ T extends ServiceSpec ,
81107> ( spec : T ) {
82- const out : Record < string , ServiceMethod < any , JsonObject > > = { } ;
108+ const out : Record < string , RuntimeServiceMethod < any , JsonObject > > = { } ;
83109
84110 for ( const [ name , v ] of Object . entries ( spec ) ) {
85111 if ( typeof v === 'function' ) {
86- out [ name ] = v ; // already a handler
112+ out [ name ] = v as RuntimeServiceMethod < any , JsonObject > ;
87113 } else {
88114 const { handler, params : paramsSchema , result : resultSchema } = v ;
89115 out [ name ] = async ( params , ctx ) => {
90116 const p = runSchema ( paramsSchema , params ) ;
91- const r = await handler ( p , ctx ) ;
117+ const r = await handler ( p , ctx as ServiceContext ) ;
92118
93119 return runSchema ( resultSchema , r ) ;
94120 } ;
95121 }
96122 }
97123
98- return Object . freeze ( out ) as {
99- [ K in keyof T ] : T [ K ] extends ServiceMethod < infer P , infer R >
100- ? ServiceMethod < P , R >
101- : T [ K ] extends { handler : ServiceMethod < infer P , infer R > }
102- ? ServiceMethod < P , R >
103- : never ;
104- } ;
124+ return Object . freeze ( out ) as NormalizedServiceSpec < T > ;
105125}
106126
107127export const defineServiceRegistry = < R extends ServiceRegistry > ( registry : R ) : R =>
@@ -115,7 +135,7 @@ export async function callServiceMethod(
115135 serviceName : string ,
116136 methodName : string ,
117137 params : JsonObject | undefined ,
118- ctx : ServiceContext ,
138+ ctx : BaseServiceContext ,
119139) : Promise < JsonObject > {
120140 if ( ctx . signal ?. aborted ) throw AppError . timeout ( 'Request canceled' ) ;
121141
@@ -138,7 +158,7 @@ export async function callServiceMethod(
138158
139159 try {
140160 // No automatic deadlines here; handlers can use ctx.signal or withDeadline(ctx.signal, ms)
141- const result = await method ( params ?? { } , ctx ) ;
161+ const result = await method ( params ?? { } , ctx as RuntimeServiceContext ) ;
142162
143163 if ( typeof result !== 'object' || result === null ) {
144164 throw AppError . internal ( `Non-object result from ${ serviceName } .${ methodName } ` ) ;
0 commit comments