From c4acc26727dc79532bef2b5b77d35346e74f4d1d Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 13 Aug 2025 15:21:07 +0200 Subject: [PATCH] New API to define handlers/shared handlers without wrapped functions. This supports proper contextual typing, and allows to evolve the Typed State API as well. --- packages/restate-sdk-clients/src/api.ts | 13 +- packages/restate-sdk-clients/src/ingress.ts | 27 +- .../restate-sdk-clients/src/public_api.ts | 1 + packages/restate-sdk-core/src/core.ts | 46 +++ packages/restate-sdk-core/src/public_api.ts | 1 + .../src/ingress_client.ts | 12 +- packages/restate-sdk-examples/src/object.ts | 48 +-- packages/restate-sdk-examples/src/raw.ts | 38 +++ packages/restate-sdk/src/common_api.ts | 24 ++ packages/restate-sdk/src/context.ts | 19 +- packages/restate-sdk/src/context_impl.ts | 15 +- packages/restate-sdk/src/types/rpc.ts | 304 +++++++++++------- .../restate-sdk/test/service_bind.test.ts | 127 ++++++-- 13 files changed, 467 insertions(+), 208 deletions(-) create mode 100644 packages/restate-sdk-examples/src/raw.ts diff --git a/packages/restate-sdk-clients/src/api.ts b/packages/restate-sdk-clients/src/api.ts index 9d3f8fef..d3f0c73d 100644 --- a/packages/restate-sdk-clients/src/api.ts +++ b/packages/restate-sdk-clients/src/api.ts @@ -7,6 +7,7 @@ import type { WorkflowDefinitionFrom, Serde, Duration, + FlattenHandlersDefinition, JournalValueCodec, } from "@restatedev/restate-sdk-core"; import { millisOrDurationToMillis } from "@restatedev/restate-sdk-core"; @@ -26,7 +27,9 @@ export interface Ingress { /** * Create a client from a {@link ServiceDefinition}. */ - serviceClient(opts: ServiceDefinitionFrom): IngressClient>; + serviceClient( + opts: ServiceDefinitionFrom + ): IngressClient>>; /** * Create a client from a {@link WorkflowDefinition}. @@ -36,7 +39,7 @@ export interface Ingress { workflowClient( opts: WorkflowDefinitionFrom, key: string - ): IngressWorkflowClient>; + ): IngressWorkflowClient>>; /** * Create a client from a {@link VirtualObjectDefinition}. @@ -45,14 +48,14 @@ export interface Ingress { objectClient( opts: VirtualObjectDefinitionFrom, key: string - ): IngressClient>; + ): IngressClient>>; /** * Create a client from a {@link ServiceDefinition}. */ serviceSendClient( opts: ServiceDefinitionFrom - ): IngressSendClient>; + ): IngressSendClient>>; /** * Create a client from a {@link VirtualObjectDefinition}. @@ -60,7 +63,7 @@ export interface Ingress { objectSendClient( opts: VirtualObjectDefinitionFrom, key: string - ): IngressSendClient>; + ): IngressSendClient>>; /** * Resolve an awakeable from the ingress client. diff --git a/packages/restate-sdk-clients/src/ingress.ts b/packages/restate-sdk-clients/src/ingress.ts index cac3c3bf..80aef375 100644 --- a/packages/restate-sdk-clients/src/ingress.ts +++ b/packages/restate-sdk-clients/src/ingress.ts @@ -19,6 +19,7 @@ import { type Serde, serde, type JournalValueCodec, + type FlattenHandlersDefinition, } from "@restatedev/restate-sdk-core"; import type { ConnectionOpts, @@ -296,21 +297,27 @@ class HttpIngress implements Ingress { ); } - serviceClient(opts: ServiceDefinitionFrom): IngressClient> { - return this.proxy(opts.name) as IngressClient>; + serviceClient( + opts: ServiceDefinitionFrom + ): IngressClient>> { + return this.proxy(opts.name) as IngressClient< + FlattenHandlersDefinition> + >; } objectClient( opts: VirtualObjectDefinitionFrom, key: string - ): IngressClient> { - return this.proxy(opts.name, key) as IngressClient>; + ): IngressClient>> { + return this.proxy(opts.name, key) as IngressClient< + FlattenHandlersDefinition> + >; } workflowClient( opts: WorkflowDefinitionFrom, key: string - ): IngressWorkflowClient> { + ): IngressWorkflowClient>> { const component = opts.name; const conn = this.opts; @@ -392,23 +399,23 @@ class HttpIngress implements Ingress { }; }, } - ) as IngressWorkflowClient>; + ) as IngressWorkflowClient>>; } objectSendClient( opts: VirtualObjectDefinitionFrom, key: string - ): IngressSendClient> { + ): IngressSendClient>> { return this.proxy(opts.name, key, true) as IngressSendClient< - VirtualObject + FlattenHandlersDefinition> >; } serviceSendClient( opts: ServiceDefinitionFrom - ): IngressSendClient> { + ): IngressSendClient>> { return this.proxy(opts.name, undefined, true) as IngressSendClient< - Service + FlattenHandlersDefinition> >; } diff --git a/packages/restate-sdk-clients/src/public_api.ts b/packages/restate-sdk-clients/src/public_api.ts index f721f229..8ac063ce 100644 --- a/packages/restate-sdk-clients/src/public_api.ts +++ b/packages/restate-sdk-clients/src/public_api.ts @@ -22,6 +22,7 @@ export type { VirtualObject, Duration, JournalValueCodec, + FlattenHandlersDefinition, } from "@restatedev/restate-sdk-core"; export { serde } from "@restatedev/restate-sdk-core"; diff --git a/packages/restate-sdk-core/src/core.ts b/packages/restate-sdk-core/src/core.ts index ed43a09a..971428a5 100644 --- a/packages/restate-sdk-core/src/core.ts +++ b/packages/restate-sdk-core/src/core.ts @@ -29,12 +29,18 @@ export interface RestateWorkflowContext // ----------- service ------------------------------------------------------- +/** + * @deprecated + */ export type ArgType = T extends (ctx: any) => any ? void : T extends (ctx: any, input: infer I) => any ? I : never; +/** + * @deprecated + */ export type HandlerReturnType = T extends ( ctx: any, input: any @@ -42,6 +48,9 @@ export type HandlerReturnType = T extends ( ? R : never; +/** + * @deprecated + */ export type ServiceHandler = F extends ( ctx: C ) => Promise @@ -65,6 +74,9 @@ export type ServiceDefinitionFrom = M extends ServiceDefinition< // ----------- object ------------------------------------------------------- +/** + * @deprecated + */ export type ObjectSharedHandler< F, SC = RestateObjectSharedContext @@ -74,6 +86,9 @@ export type ObjectSharedHandler< ? F : (ctx: SC, param?: any) => Promise; +/** + * @deprecated + */ export type ObjectHandler = F extends ( ctx: C, param: any @@ -104,6 +119,9 @@ export type VirtualObjectDefinitionFrom = M extends VirtualObjectDefinition< // ----------- workflow ------------------------------------------------------- +/** + * @deprecated + */ export type WorkflowSharedHandler< F, SC = RestateWorkflowSharedContext @@ -113,6 +131,9 @@ export type WorkflowSharedHandler< ? F : (ctx: SC, param?: any) => Promise; +/** + * @deprecated + */ export type WorkflowHandler = F extends ( ctx: C, param: any @@ -135,3 +156,28 @@ export type WorkflowDefinitionFrom = M extends WorkflowDefinition< > ? M : WorkflowDefinition; + +// -------- Type manipulation for clients + +export type FlattenHandlersDefinition = { + [K in keyof M]: M[K] extends { + handler: + | ((ctx: any, param: any) => Promise) + | ((ctx: any) => Promise) + | ((ctx: any, param?: any) => Promise); + } + ? M[K]["handler"] + : M[K] extends { + sharedHandler: + | ((ctx: any, param: any) => Promise) + | ((ctx: any) => Promise) + | ((ctx: any, param?: any) => Promise); + } + ? M[K]["sharedHandler"] + : M[K] extends + | ((ctx: any, param: any) => Promise) + | ((ctx: any) => Promise) + | ((ctx: any, param?: any) => Promise) + ? M[K] + : never; +}; diff --git a/packages/restate-sdk-core/src/public_api.ts b/packages/restate-sdk-core/src/public_api.ts index 1e54987d..52a4e782 100644 --- a/packages/restate-sdk-core/src/public_api.ts +++ b/packages/restate-sdk-core/src/public_api.ts @@ -31,6 +31,7 @@ export type { WorkflowDefinitionFrom, ArgType, HandlerReturnType, + FlattenHandlersDefinition, } from "./core.js"; export type { Serde } from "./serde_api.js"; diff --git a/packages/restate-sdk-examples/src/ingress_client.ts b/packages/restate-sdk-examples/src/ingress_client.ts index 0fd057df..ce9a2dbc 100644 --- a/packages/restate-sdk-examples/src/ingress_client.ts +++ b/packages/restate-sdk-examples/src/ingress_client.ts @@ -16,9 +16,11 @@ import * as restate from "@restatedev/restate-sdk-clients"; import type { Greeter } from "./greeter.js"; import type { PaymentWorkflow } from "./workflow.js"; import type { Counter } from "./object.js"; +import type { RawService } from "./raw.js"; const Greeter: Greeter = { name: "greeter" }; const Counter: Counter = { name: "counter" }; +const RawService: RawService = { name: "raw" }; const Workflow: PaymentWorkflow = { name: "payment" }; const ingress = restate.connect({ url: "http://localhost:8080" }); @@ -157,12 +159,12 @@ const workflow = async (name: string) => { console.log(await client.workflowAttach()); }; -const binaryRawCall = async (name: string) => { - const counter = ingress.objectClient(Counter, name); +const binaryRawCall = async () => { + const rawService = ingress.serviceClient(RawService); const buffer = new TextEncoder().encode("hello!"); - const outBuffer = await counter.binary( + const outBuffer = await rawService.binary( buffer, restate.rpc.opts({ input: restate.serde.binary, @@ -173,7 +175,7 @@ const binaryRawCall = async (name: string) => { const str = new TextDecoder().decode(outBuffer); - console.log(`We got a buffer for ${name} : ${str}`); + console.log(`We got a buffer : ${str}`); }; // Before running this example, make sure @@ -198,5 +200,5 @@ Promise.resolve() .then(() => globalCustomHeaders("bob")) .then(() => customInterface("bob")) .then(() => delayedCall("bob")) - .then(() => binaryRawCall("bob")) + .then(() => binaryRawCall()) .catch((e) => console.error(e)); diff --git a/packages/restate-sdk-examples/src/object.ts b/packages/restate-sdk-examples/src/object.ts index d6c182bf..26f791f9 100644 --- a/packages/restate-sdk-examples/src/object.ts +++ b/packages/restate-sdk-examples/src/object.ts @@ -9,29 +9,17 @@ * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE */ -import { - createObjectHandler, - createObjectSharedHandler, - object, - type ObjectContext, - type ObjectSharedContext, - serde, - serve, -} from "@restatedev/restate-sdk"; +import { object, serve } from "@restatedev/restate-sdk"; export const counter = object({ name: "counter", handlers: { - /** - * Add amount to the currently stored count - */ - add: async (ctx: ObjectContext, amount: number) => { + add: async (ctx, amount: number) => { const current = await ctx.get("count"); const updated = (current ?? 0) + amount; ctx.set("count", updated); return updated; }, - /** * Get the current amount. * @@ -39,29 +27,17 @@ export const counter = object({ * These handlers can be executed concurrently to the exclusive handlers (i.e. add) * But they can not modify the state (set is missing from the ctx). */ - current: createObjectSharedHandler( - async (ctx: ObjectSharedContext): Promise => { - return (await ctx.get("count")) ?? 0; - } - ), - - /** - * Handlers (shared or exclusive) can be configured to bypass JSON serialization, - * by specifying the input (accept) and output (contentType) content types. - * - * to call that handler with binary data, you can use the following curl command: - * curl -X POST -H "Content-Type: application/octet-stream" --data-binary 'hello' ${RESTATE_INGRESS_URL}/counter/mykey/binary - */ - binary: createObjectHandler( - { - input: serde.binary, - output: serde.binary, + current: { + sharedHandler: async (ctx) => { + return (await ctx.get("count")) ?? 0; }, - async (ctx: ObjectContext, data: Uint8Array) => { - // console.log("Received binary data", data); - return data; - } - ), + }, + clear: { + handler: async (ctx) => { + ctx.clearAll(); + }, + enableLazyState: true, + }, }, }); diff --git a/packages/restate-sdk-examples/src/raw.ts b/packages/restate-sdk-examples/src/raw.ts new file mode 100644 index 00000000..a6893236 --- /dev/null +++ b/packages/restate-sdk-examples/src/raw.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH + * + * This file is part of the Restate SDK for Node.js/TypeScript, + * which is released under the MIT license. + * + * You can find a copy of the license in file LICENSE in the root + * directory of this repository or package, or at + * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE + */ + +import { service, serve } from "@restatedev/restate-sdk"; +import { serde } from "@restatedev/restate-sdk-core"; + +const rawService = service({ + name: "raw", + handlers: { + binary: { + /** + * Handlers can be configured to bypass JSON serialization, + * by specifying the input (accept) and output (contentType) content types. + * + * To call this handler with binary data, you can use the following curl command: + * curl -X POST -H "Content-Type: application/octet-stream" --data-binary 'hello' ${RESTATE_INGRESS_URL}/raw/binary + */ + input: serde.binary, + output: serde.binary, + handler: async (ctx, data: Uint8Array) => { + // console.log("Received binary data", data); + return data; + }, + }, + }, +}); + +export type RawService = typeof rawService; + +serve({ services: [rawService] }); diff --git a/packages/restate-sdk/src/common_api.ts b/packages/restate-sdk/src/common_api.ts index 472b1dac..aa065223 100644 --- a/packages/restate-sdk/src/common_api.ts +++ b/packages/restate-sdk/src/common_api.ts @@ -83,6 +83,14 @@ export type { ServiceOptions, ObjectOptions, WorkflowOptions, + ServiceHandlers, + ObjectHandlers, + WorkflowHandlers, + ServiceHandlerDefinition, + ObjectHandlerDefinition, + WorkflowHandlerDefinition, + WorkflowSharedHandlerDefinition, + Handler, } from "./types/rpc.js"; export { service, @@ -99,6 +107,7 @@ export type { ServiceDefinition, VirtualObjectDefinition, WorkflowDefinition, + FlattenHandlersDefinition, } from "@restatedev/restate-sdk-core"; export type { @@ -131,8 +140,23 @@ export type { EndpointOptions } from "./endpoint/types.js"; import { handlers } from "./types/rpc.js"; +/** + * @deprecated + */ export const createServiceHandler = handlers.handler; +/** + * @deprecated + */ export const createObjectHandler = handlers.object.exclusive; +/** + * @deprecated + */ export const createObjectSharedHandler = handlers.object.shared; +/** + * @deprecated + */ export const createWorkflowHandler = handlers.workflow.workflow; +/** + * @deprecated + */ export const createWorkflowSharedHandler = handlers.workflow.shared; diff --git a/packages/restate-sdk/src/context.ts b/packages/restate-sdk/src/context.ts index 7bbaba99..a65d2acf 100644 --- a/packages/restate-sdk/src/context.ts +++ b/packages/restate-sdk/src/context.ts @@ -24,6 +24,7 @@ import type { WorkflowDefinitionFrom, Serde, Duration, + FlattenHandlersDefinition, } from "@restatedev/restate-sdk-core"; import { ContextImpl } from "./context_impl.js"; import type { TerminalError } from "./types/errors.js"; @@ -100,7 +101,7 @@ export interface KeyValueStore { get( name: TState extends UntypedState ? string : TKey, serde?: Serde - ): Promise<(TState extends UntypedState ? TValue : TState[TKey]) | null>; + ): Promise; stateKeys(): Promise>; @@ -475,7 +476,9 @@ export interface Context extends RestateContext { * const result2 = await ctx.serviceClient(Service).anotherAction(1337); * ``` */ - serviceClient(opts: ServiceDefinitionFrom): Client>; + serviceClient( + opts: ServiceDefinitionFrom + ): Client>>; /** * Same as {@link serviceClient} but for virtual objects. @@ -486,7 +489,7 @@ export interface Context extends RestateContext { objectClient( opts: VirtualObjectDefinitionFrom, key: string - ): Client>; + ): Client>>; /** * Same as {@link serviceClient} but for workflows. @@ -497,7 +500,7 @@ export interface Context extends RestateContext { workflowClient( opts: WorkflowDefinitionFrom, key: string - ): Client>; + ): Client>>; /** * Same as {@link objectSendClient} but for workflows. @@ -508,7 +511,7 @@ export interface Context extends RestateContext { workflowSendClient( opts: WorkflowDefinitionFrom, key: string - ): SendClient>; + ): SendClient>>; /** * Makes a type-safe one-way RPC to the specified target service. This method effectively behaves @@ -554,7 +557,7 @@ export interface Context extends RestateContext { serviceSendClient( service: ServiceDefinitionFrom, opts?: SendOptions - ): SendClient>; + ): SendClient>>; /** * Same as {@link serviceSendClient} but for virtual objects. @@ -567,7 +570,7 @@ export interface Context extends RestateContext { obj: VirtualObjectDefinitionFrom, key: string, opts?: SendOptions - ): SendClient>; + ): SendClient>>; genericCall( call: GenericCall @@ -646,7 +649,7 @@ export interface ObjectSharedContext get( name: TState extends UntypedState ? string : TKey, serde?: Serde - ): Promise<(TState extends UntypedState ? TValue : TState[TKey]) | null>; + ): Promise; /** * Retrieve all the state keys for this object. diff --git a/packages/restate-sdk/src/context_impl.ts b/packages/restate-sdk/src/context_impl.ts index 59a60e73..0ea11bbe 100644 --- a/packages/restate-sdk/src/context_impl.ts +++ b/packages/restate-sdk/src/context_impl.ts @@ -51,6 +51,7 @@ import { } from "./types/rpc.js"; import type { Duration, + FlattenHandlersDefinition, JournalValueCodec, Serde, Service, @@ -343,28 +344,30 @@ export class ContextImpl implements ObjectContext, WorkflowContext { } } - serviceClient({ name }: ServiceDefinitionFrom): Client> { + serviceClient({ + name, + }: ServiceDefinitionFrom): Client>> { return makeRpcCallProxy((call) => this.genericCall(call), name); } objectClient( { name }: VirtualObjectDefinitionFrom, key: string - ): Client> { + ): Client>> { return makeRpcCallProxy((call) => this.genericCall(call), name, key); } workflowClient( { name }: WorkflowDefinitionFrom, key: string - ): Client> { + ): Client>> { return makeRpcCallProxy((call) => this.genericCall(call), name, key); } public serviceSendClient( { name }: ServiceDefinitionFrom, opts?: SendOptions - ): SendClient> { + ): SendClient>> { return makeRpcSendProxy( (send) => this.genericSend(send), name, @@ -377,7 +380,7 @@ export class ContextImpl implements ObjectContext, WorkflowContext { { name }: VirtualObjectDefinitionFrom, key: string, opts?: SendOptions - ): SendClient> { + ): SendClient>> { return makeRpcSendProxy( (send) => this.genericSend(send), name, @@ -390,7 +393,7 @@ export class ContextImpl implements ObjectContext, WorkflowContext { { name }: WorkflowDefinitionFrom, key: string, opts?: SendOptions - ): SendClient> { + ): SendClient>> { return makeRpcSendProxy( (send) => this.genericSend(send), name, diff --git a/packages/restate-sdk/src/types/rpc.ts b/packages/restate-sdk/src/types/rpc.ts index 3b92634d..4cdafe56 100644 --- a/packages/restate-sdk/src/types/rpc.ts +++ b/packages/restate-sdk/src/types/rpc.ts @@ -27,13 +27,9 @@ import type { } from "../context.js"; import { - type ServiceHandler, type ServiceDefinition, - type ObjectHandler, type VirtualObjectDefinition, - type WorkflowHandler, type WorkflowDefinition, - type WorkflowSharedHandler, type Serde, type Duration, serde, @@ -496,6 +492,9 @@ export class HandlerWrapper { } // ----------- handler decorators ---------------------------------------------- +/** + * @deprecated + */ export type RemoveVoidArgument = F extends ( ctx: infer C, arg: infer A @@ -505,12 +504,12 @@ export type RemoveVoidArgument = F extends ( : F : F; +/** + * @deprecated + */ export namespace handlers { /** - * Create a service handler. - * - * @param opts additional configuration - * @param fn the actual handler code to execute + * @deprecated To provide options, simply define the handler as */ export function handler( opts: ServiceHandlerOpts, @@ -519,7 +518,13 @@ export namespace handlers { return HandlerWrapper.from(HandlerKind.SERVICE, fn, opts).transpose(); } + /** + * @deprecated + */ export namespace workflow { + /** + * @deprecated + */ export function workflow< O, I = void, @@ -529,6 +534,9 @@ export namespace handlers { fn: (ctx: WorkflowContext, input: I) => Promise ): RemoveVoidArgument; + /** + * @deprecated + */ export function workflow< O, I = void, @@ -537,6 +545,9 @@ export namespace handlers { fn: (ctx: WorkflowContext, input: I) => Promise ): RemoveVoidArgument; + /** + * @deprecated + */ export function workflow( optsOrFn: | WorkflowHandlerOpts @@ -554,13 +565,7 @@ export namespace handlers { } /** - * Creates a shared handler for a workflow. - * - * A shared handler allows a read-only concurrent execution - * for a given key. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function shared< O, @@ -572,13 +577,7 @@ export namespace handlers { ): RemoveVoidArgument; /** - * Creates a shared handler for a workflow. - * - * A shared handler allows a read-only concurrent execution - * for a given key. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function shared< O, @@ -589,13 +588,7 @@ export namespace handlers { ): RemoveVoidArgument; /** - * Creates a shared handler for a workflow - * - * A shared handler allows a read-only concurrent execution - * for a given key. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function shared( optsOrFn: @@ -614,14 +607,12 @@ export namespace handlers { } } + /** + * @deprecated + */ export namespace object { /** - * Creates an exclusive handler for a virtual Object. - * - * note : This applies only to a virtual object. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function exclusive< O, @@ -633,16 +624,7 @@ export namespace handlers { ): RemoveVoidArgument; /** - * Creates an exclusive handler for a virtual Object. - * - * - * note 1: This applies only to a virtual object. - * note 2: This is the default for virtual objects, so if no - * additional reconfiguration is needed, you can simply - * use the handler directly (no need to use exclusive). - * This variant here is only for symmetry/convenance. - * - * @param fn the handler to execute + * @deprecated */ export function exclusive< O, @@ -653,17 +635,7 @@ export namespace handlers { ): RemoveVoidArgument; /** - * Creates an exclusive handler for a virtual Object. - * - * - * note 1: This applies only to a virtual object. - * note 2: This is the default for virtual objects, so if no - * additional reconfiguration is needed, you can simply - * use the handler directly (no need to use exclusive). - * This variant here is only for symmetry/convenance. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function exclusive( optsOrFn: @@ -682,15 +654,7 @@ export namespace handlers { } /** - * Creates a shared handler for a virtual Object. - * - * A shared handler allows a read-only concurrent execution - * for a given key. - * - * note: This applies only to a virtual object. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function shared< O, @@ -702,15 +666,7 @@ export namespace handlers { ): RemoveVoidArgument; /** - * Creates a shared handler for a virtual Object. - * - * A shared handler allows a read-only concurrent execution - * for a given key. - * - * note: This applies only to a virtual object. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function shared< O, @@ -721,15 +677,7 @@ export namespace handlers { ): RemoveVoidArgument; /** - * Creates a shared handler for a virtual Object. - * - * A shared handler allows a read-only concurrent execution - * for a given key. - * - * note: This applies only to a virtual object. - * - * @param opts additional configurations - * @param fn the handler to execute + * @deprecated */ export function shared( optsOrFn: @@ -749,14 +697,34 @@ export namespace handlers { } } +// ----------- common ---------------------------------------------- + +export type Handler = F extends ( + ctx: C, + param: I +) => Promise + ? F + : F extends (ctx: C) => Promise + ? F + : (ctx: C, param?: I) => Promise; + // ----------- services ---------------------------------------------- -export type ServiceOpts = { - [K in keyof U]: U[K] extends ServiceHandler - ? U[K] - : ServiceHandler; +export type ServiceHandlerDefinition = + | Handler + | ({ + handler: Handler; + } & ServiceHandlerOpts); + +export type ServiceHandlers = { + [K in keyof U]: ServiceHandlerDefinition; }; +/** + * @deprecated Use ServiceHandlers instead. + */ +export type ServiceOpts = ServiceHandlers; + export type ServiceOptions = { /** * The retention duration of idempotent requests to this service. @@ -907,7 +875,7 @@ export type ServiceOptions = { */ export const service =

(service: { name: P; - handlers: ServiceOpts & ThisType; + handlers: M & ServiceHandlers & ThisType; description?: string; metadata?: Record; options?: ServiceOptions; @@ -924,6 +892,23 @@ export const service =

(service: { name, HandlerWrapper.from(HandlerKind.SERVICE, handler).transpose(), ]; + } else if (typeof handler === "object") { + const handlerDefinition = handler as { + handler?: Function; + } & ServiceHandlerOpts; + if (!handlerDefinition || !handlerDefinition?.handler) { + throw new TypeError( + `Expected handler definition ${name} to contain field 'handler'` + ); + } + return [ + name, + HandlerWrapper.from( + HandlerKind.SERVICE, + handlerDefinition.handler, + handlerDefinition + ).transpose(), + ]; } throw new TypeError(`Unexpected handler type ${name}`); }); @@ -939,16 +924,32 @@ export const service =

(service: { // ----------- objects ---------------------------------------------- -export type ObjectOpts = { - [K in keyof U]: U[K] extends ObjectHandler> - ? U[K] - : U[K] extends ObjectHandler> - ? U[K] - : - | ObjectHandler> - | ObjectHandler>; +export type ObjectHandlerDefinition< + U, + TState extends TypedState = UntypedState, + I = any, + O = any +> = + | Handler, I, O> + | ({ + sharedHandler: Handler, I, O>; + } & ObjectHandlerOpts) + | ({ + handler: Handler, I, O>; + } & ObjectHandlerOpts); + +export type ObjectHandlers = { + [K in keyof U]: ObjectHandlerDefinition; }; +/** + * @deprecated Use ObjectHandlers instead. + */ +export type ObjectOpts< + U, + TState extends TypedState = UntypedState +> = ObjectHandlers; + export type ObjectOptions = ServiceOptions & { /** * When set to `true`, lazy state will be enabled for all invocations to this service. @@ -1015,7 +1016,7 @@ export type ObjectOptions = ServiceOptions & { */ export const object =

(object: { name: P; - handlers: ObjectOpts & ThisType; + handlers: M & ObjectHandlers & ThisType; description?: string; metadata?: Record; options?: ObjectOptions; @@ -1034,6 +1035,39 @@ export const object =

(object: { name, HandlerWrapper.from(HandlerKind.EXCLUSIVE, handler).transpose(), ]; + } else if (typeof handler === "object") { + const handlerDefinition = handler as { + handler?: Function; + sharedHandler?: Function; + } & ObjectHandlerOpts; + if (!handlerDefinition) { + throw new TypeError( + `Expected handler definition ${name} to be non-empty` + ); + } + if (handlerDefinition.handler) { + return [ + name, + HandlerWrapper.from( + HandlerKind.EXCLUSIVE, + handlerDefinition.handler, + handlerDefinition + ).transpose(), + ]; + } + if (handlerDefinition.sharedHandler) { + return [ + name, + HandlerWrapper.from( + HandlerKind.SHARED, + handlerDefinition.sharedHandler, + handlerDefinition + ).transpose(), + ]; + } + throw new TypeError( + `Expected handler definition ${name} to contain field 'handler' or 'sharedHandler'` + ); } throw new TypeError(`Unexpected handler type ${name}`); }); @@ -1049,6 +1083,28 @@ export const object =

(object: { // ----------- workflows ---------------------------------------------- +export type WorkflowHandlerDefinition< + U, + TState extends TypedState = UntypedState, + I = any, + O = any +> = + | Handler, I, O> + | ({ + handler: Handler, I, O>; + } & WorkflowHandlerOpts); + +export type WorkflowSharedHandlerDefinition< + U, + TState extends TypedState = UntypedState, + I = any, + O = any +> = + | Handler, I, O> + | ({ + handler: Handler, I, O>; + } & WorkflowHandlerOpts); + /** * A workflow handlers is a type that describes the handlers for a workflow. * The handlers must contain exactly one handler named 'run', and this handler must accept as a first argument a WorkflowContext. @@ -1056,22 +1112,21 @@ export const object =

(object: { * The handlers can not be named 'workflowSubmit', 'workflowAttach', 'workflowOutput' - as these are reserved. * @see {@link workflow} for an example. */ -export type WorkflowOpts = { - run: (ctx: WorkflowContext, argument: any) => Promise; -} & { +export type WorkflowHandlers = { [K in keyof U]: K extends | "workflowSubmit" | "workflowAttach" | "workflowOutput" ? `${K} is a reserved keyword` : K extends "run" - ? U[K] extends WorkflowHandler> - ? U[K] - : "An handler named 'run' must take as a first argument a WorkflowContext, and must return a Promise" - : U[K] extends WorkflowSharedHandler> - ? U[K] - : "An handler other then 'run' must accept as a first argument a WorkflowSharedContext"; -}; + ? WorkflowHandlerDefinition + : WorkflowSharedHandlerDefinition; +} & { run: any }; + +/** + * @deprecated Use WorkflowHandlers instead. + */ +export type WorkflowOpts = WorkflowHandlers; export type WorkflowOptions = ServiceOptions & { /** @@ -1132,7 +1187,7 @@ export type WorkflowOptions = ServiceOptions & { */ export const workflow =

(workflow: { name: P; - handlers: WorkflowOpts & ThisType; + handlers: M & WorkflowHandlers & ThisType; description?: string; metadata?: Record; options?: WorkflowOptions; @@ -1144,7 +1199,10 @@ export const workflow =

(workflow: { // // Add the main 'run' handler // - const runHandler = workflow.handlers["run"]; + const runHandler = workflow.handlers["run"] as + | Function + | object + | HandlerWrapper; let runWrapper: HandlerWrapper; if (runHandler instanceof HandlerWrapper) { @@ -1153,6 +1211,20 @@ export const workflow =

(workflow: { runWrapper = HandlerWrapper.fromHandler(runHandler) ?? HandlerWrapper.from(HandlerKind.WORKFLOW, runHandler); + } else if (typeof runHandler === "object") { + const handlerDefinition = runHandler as { + handler?: Function; + } & WorkflowHandlerOpts; + if (!handlerDefinition || !handlerDefinition?.handler) { + throw new TypeError( + `Expected handler definition run to contain field 'handler'` + ); + } + runWrapper = HandlerWrapper.from( + HandlerKind.WORKFLOW, + handlerDefinition.handler, + handlerDefinition + ); } else { throw new TypeError(`Missing main workflow handler, named 'run'`); } @@ -1180,6 +1252,20 @@ export const workflow =

(workflow: { wrapper = HandlerWrapper.fromHandler(handler) ?? HandlerWrapper.from(HandlerKind.SHARED, handler); + } else if (typeof handler === "object") { + const handlerDefinition = handler as { + handler?: Function; + } & WorkflowHandlerOpts; + if (!handlerDefinition || !handlerDefinition?.handler) { + throw new TypeError( + `Expected handler definition ${name} to contain field 'handler'` + ); + } + wrapper = HandlerWrapper.from( + HandlerKind.SHARED, + handlerDefinition.handler, + handlerDefinition + ); } else { throw new TypeError(`Unexpected handler type ${name}`); } diff --git a/packages/restate-sdk/test/service_bind.test.ts b/packages/restate-sdk/test/service_bind.test.ts index e2f9f059..7d46a8ae 100644 --- a/packages/restate-sdk/test/service_bind.test.ts +++ b/packages/restate-sdk/test/service_bind.test.ts @@ -36,38 +36,85 @@ describe("BindService", () => { }); }); -const inputBytes = restate.service({ - name: "acceptBytes", - handlers: { - greeter: restate.handlers.handler( - { - input: restate.serde.binary, - }, - // eslint-disable-next-line @typescript-eslint/require-await - async (_ctx: restate.Context, audio: Uint8Array) => { - return { length: audio.length }; - } - ), - }, -}); +describe("AcceptBytes using old handler factory API", () => { + const inputBytes = restate.service({ + name: "acceptBytes", + handlers: { + greeter: restate.handlers.handler( + { + input: restate.serde.binary, + }, + // eslint-disable-next-line @typescript-eslint/require-await + async (_ctx: restate.Context, audio: Uint8Array) => { + return { length: audio.length }; + } + ), + }, + }); -const inputBytesWithCustomAccept = restate.service({ - name: "acceptBytes", - handlers: { - greeter: restate.handlers.handler( - { - accept: "application/*", - input: restate.serde.binary, - }, - // eslint-disable-next-line @typescript-eslint/require-await - async (_ctx: restate.Context, audio: Uint8Array) => { - return { length: audio.length }; - } - ), - }, + const inputBytesWithCustomAccept = restate.service({ + name: "acceptBytes", + handlers: { + greeter: restate.handlers.handler( + { + accept: "application/*", + input: restate.serde.binary, + }, + // eslint-disable-next-line @typescript-eslint/require-await + async (_ctx: restate.Context, audio: Uint8Array) => { + return { length: audio.length }; + } + ), + }, + }); + + it("should declare accept content type correctly", () => { + const svc = toServiceDiscovery(inputBytes); + + expect(svc.handlers[0]?.input?.contentType).toEqual( + restate.serde.binary.contentType + ); + }); + + it("should declare accept content type correctly when custom accept is provided", () => { + const svc = toServiceDiscovery(inputBytesWithCustomAccept); + + expect(svc.handlers[0]?.input?.contentType).toEqual("application/*"); + }); }); describe("AcceptBytes", () => { + const inputBytes = restate.service({ + name: "acceptBytes", + handlers: { + greeter: { + // eslint-disable-next-line @typescript-eslint/require-await + handler: async (_ctx: restate.Context, audio: Uint8Array) => { + return { length: audio.length }; + }, + options: { + input: restate.serde.binary, + }, + }, + }, + }); + + const inputBytesWithCustomAccept = restate.service({ + name: "acceptBytes", + handlers: { + greeter: { + // eslint-disable-next-line @typescript-eslint/require-await + handler: async (_ctx: restate.Context, audio: Uint8Array) => { + return { length: audio.length }; + }, + options: { + accept: "application/*", + input: restate.serde.binary, + }, + }, + }, + }); + it("should declare accept content type correctly", () => { const svc = toServiceDiscovery(inputBytes); @@ -103,7 +150,7 @@ describe("PropagateConfigOptions", () => { expect(svc.journalRetention).toEqual(10 * 1000); }); - it("should declare config option on a handler correctly", () => { + it("should declare config option on a handler correctly with old handler factory api", () => { const svc = toServiceDiscovery( restate.service({ name: "greeter", @@ -125,6 +172,28 @@ describe("PropagateConfigOptions", () => { expect(svc.handlers[0]?.journalRetention).toEqual(10 * 1000); }); + it("should declare config option on a handler correctly", () => { + const svc = toServiceDiscovery( + restate.service({ + name: "greeter", + handlers: { + greet: { + // eslint-disable-next-line @typescript-eslint/require-await + handler: async (ctx, req: string): Promise => { + return req; + }, + options: { + journalRetention: { seconds: 10 }, + }, + }, + }, + }) + ); + + expect(svc.journalRetention).toBeUndefined(); + expect(svc.handlers[0]?.journalRetention).toEqual(10 * 1000); + }); + it("should apply endpoint global config option", () => { const svc = toServiceDiscovery( restate.service({