Skip to content

Commit 0b19442

Browse files
committed
Support KeyedEventHandler
This commit adds the support for the Event API for the keyed dynamic handlers. Since the dynamic handler API requires a string key, this means that the registered gRPC methods are of the form rpc Handle(StringKeyedEvent) returns (google.protobuf.Empty) {};
1 parent 08458c0 commit 0b19442

File tree

8 files changed

+236
-30
lines changed

8 files changed

+236
-30
lines changed

buf.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ deps:
44
- remote: buf.build
55
owner: restatedev
66
repository: proto
7-
commit: 7c1d0063691147dab51a87fc7d2befa5
8-
digest: shake256:41fb2128bd34a84f363d2b0cafbf0746b82d2b18c400cf85a1b3a94b28a2f4643fc17c9cf523cf98720b227b8d75b24fdc03fb87f8fa9a7ef75f788a5cbfe0c4
7+
commit: 4c536701ef5348ecbf3cd1ef6cf825fc
8+
digest: shake256:0fdebe27d9653dc31f9951623e0c8dc68a161d2b55146cc22c0501d2bccb22d49ab9b2b80c8f4de8827c4ba168119296f14d323eed5162ef63f128803cc64f47

examples/handler_example.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
3+
*
4+
* This file is part of the Restate SDK for Node.js/TypeScript,
5+
* which is released under the MIT license.
6+
*
7+
* You can find a copy of the license in file LICENSE in the root
8+
* directory of this repository or package, or at
9+
* https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
10+
*/
11+
12+
/* eslint-disable no-console */
13+
14+
/*
15+
* A simple example program using the Restate's event handlers.
16+
*/
17+
18+
import * as restate from "../src/public_api";
19+
20+
const registration = async (ctx: restate.RpcContext, event: restate.Event) => {
21+
// store in state the user's information as coming from the registeration event
22+
const { name } = event.json<{ name: string }>();
23+
ctx.set("name", name);
24+
};
25+
26+
const email = async (ctx: restate.RpcContext, event: restate.Event) => {
27+
// store in state the user's information as coming from the email event
28+
const { email } = event.json<{ email: string }>();
29+
ctx.set("email", email);
30+
};
31+
32+
type UserProfile = {
33+
id: string;
34+
name: string;
35+
email: string;
36+
};
37+
38+
const get = async (
39+
ctx: restate.RpcContext,
40+
id: string
41+
): Promise<UserProfile> => {
42+
return {
43+
id,
44+
name: (await ctx.get<string>("name")) ?? "",
45+
email: (await ctx.get<string>("email")) ?? "",
46+
};
47+
};
48+
49+
const profile = restate.keyedRouter({
50+
registration: restate.keyedEventHandler(registration),
51+
email: restate.keyedEventHandler(email),
52+
get,
53+
});
54+
55+
// restate server
56+
restate.createServer().bindKeyedRouter("profile", profile).listen(8080);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"verify": "npm run format -- --check && npm run test && npm run lint && npm run build",
3939
"release": "release-it",
4040
"example": "RESTATE_DEBUG_LOGGING=JOURNAL ts-node-dev --respawn --transpile-only ./examples/example.ts",
41-
"grpcexample": "RESTATE_DEBUG_LOGGING=JOURNAL ts-node-dev --respawn --transpile-only ./examples/grpc_example.ts"
41+
"grpcexample": "RESTATE_DEBUG_LOGGING=JOURNAL ts-node-dev --respawn --transpile-only ./examples/grpc_example.ts",
42+
"handlerexample": "RESTATE_DEBUG_LOGGING=JOURNAL ts-node-dev --respawn --transpile-only ./examples/handler_example.ts"
4243
},
4344
"files": [
4445
"dist"

proto/dynrpc.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
syntax = "proto3";
1313

1414
import "dev/restate/ext.proto";
15+
import "dev/restate/events.proto";
1516
import "google/protobuf/struct.proto";
17+
import "google/protobuf/empty.proto";
1618

1719
service RpcEndpoint {
1820
option (dev.restate.ext.service_type) = KEYED;
1921

2022
rpc call(RpcRequest) returns (RpcResponse) {};
2123

24+
rpc handle(dev.restate.StringKeyedEvent) returns (google.protobuf.Empty) {};
2225
}
2326

2427
service UnkeyedRpcEndpoint {

src/public_api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ export {
1919
export {
2020
router,
2121
keyedRouter,
22+
keyedEventHandler,
2223
UnKeyedRouter,
2324
KeyedRouter,
25+
KeyedEventHandler,
2426
Client,
2527
SendClient,
2628
} from "./types/router";
@@ -32,3 +34,4 @@ export {
3234
} from "./server/restate_lambda_handler";
3335
export * as RestateUtils from "./utils/public_utils";
3436
export { ErrorCodes, RestateError, TerminalError } from "./types/errors";
37+
export { Event } from "./types/types";

src/server/base_restate_server.ts

Lines changed: 121 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ import {
2222
ProtocolMode,
2323
ServiceDiscoveryResponse,
2424
} from "../generated/proto/discovery";
25+
import { Event } from "../types/types";
26+
import { StringKeyedEvent } from "../generated/dev/restate/events";
2527
import {
2628
FileDescriptorProto,
2729
UninterpretedOption,
2830
} from "../generated/google/protobuf/descriptor";
31+
import { Empty } from "../generated/google/protobuf/empty";
2932
import {
3033
FileDescriptorProto as FileDescriptorProto1,
3134
ServiceDescriptorProto as ServiceDescriptorProto1,
@@ -45,6 +48,7 @@ import { RestateContext, useContext } from "../restate_context";
4548
import { RpcContextImpl } from "../restate_context_impl";
4649
import { verifyAssumptions } from "../utils/assumpsions";
4750
import { TerminalError } from "../public_api";
51+
import { isEventHandler } from "../types/router";
4852

4953
export interface ServiceOpts {
5054
descriptor: ProtoMetadata;
@@ -148,6 +152,78 @@ export abstract class BaseRestateServer {
148152
}
149153
}
150154

155+
rpcHandler(
156+
keyed: boolean,
157+
route: string,
158+
handler: Function
159+
): {
160+
descriptor: MethodDescriptorProto1;
161+
method: GrpcServiceMethod<unknown, unknown>;
162+
} {
163+
const descriptor = createRpcMethodDescriptor(route);
164+
165+
const localMethod = (instance: unknown, input: RpcRequest) => {
166+
const ctx = useContext(instance);
167+
if (keyed) {
168+
return dispatchKeyedRpcHandler(ctx, input, handler);
169+
} else {
170+
return dispatchUnkeyedRpcHandler(ctx, input, handler);
171+
}
172+
};
173+
174+
const decoder = RpcRequest.decode;
175+
const encoder = (message: RpcResponse) =>
176+
RpcResponse.encode(message).finish();
177+
178+
const method = new GrpcServiceMethod<RpcRequest, RpcResponse>(
179+
route,
180+
route,
181+
localMethod,
182+
decoder,
183+
encoder
184+
);
185+
186+
return {
187+
descriptor: descriptor,
188+
method: method as GrpcServiceMethod<unknown, unknown>,
189+
};
190+
}
191+
192+
stringKeyedEventHandler(
193+
keyed: boolean,
194+
route: string,
195+
handler: Function
196+
): {
197+
descriptor: MethodDescriptorProto1;
198+
method: GrpcServiceMethod<unknown, unknown>;
199+
} {
200+
if (!keyed) {
201+
// TODO: support unkeyed rpc event handler
202+
throw new TerminalError("Unkeyed Event handlers are not yet supported.");
203+
}
204+
const descriptor = createStringKeyedMethodDescriptor(route);
205+
const localMethod = (instance: unknown, input: StringKeyedEvent) => {
206+
const ctx = useContext(instance);
207+
return dispatchKeyedEventHandler(ctx, input, handler);
208+
};
209+
210+
const decoder = StringKeyedEvent.decode;
211+
const encoder = (message: Empty) => Empty.encode(message).finish();
212+
213+
const method = new GrpcServiceMethod<StringKeyedEvent, Empty>(
214+
route,
215+
route,
216+
localMethod,
217+
decoder,
218+
encoder
219+
);
220+
221+
return {
222+
descriptor,
223+
method: method as GrpcServiceMethod<unknown, unknown>,
224+
};
225+
}
226+
151227
protected bindRpcService(name: string, router: RpcRouter, keyed: boolean) {
152228
const lastDot = name.indexOf(".");
153229
const serviceName = lastDot === -1 ? name : name.substring(lastDot + 1);
@@ -161,40 +237,33 @@ export abstract class BaseRestateServer {
161237
? pushKeyedService(desc, name)
162238
: pushUnKeyedService(desc, name);
163239

164-
const decoder = RpcRequest.decode;
165-
const encoder = (message: RpcResponse) =>
166-
RpcResponse.encode(message).finish();
167-
168240
for (const [route, handler] of Object.entries(router)) {
169-
serviceGrpcSpec.method.push(createRpcMethodDescriptor(route));
170-
171-
const localFn = (instance: unknown, input: RpcRequest) => {
172-
const ctx = useContext(instance);
173-
if (keyed) {
174-
return dispatchKeyedRpcHandler(ctx, input, handler);
175-
} else {
176-
return dispatchUnkeyedRpcHandler(ctx, input, handler);
177-
}
241+
let registration: {
242+
descriptor: MethodDescriptorProto1;
243+
method: GrpcServiceMethod<unknown, unknown>;
178244
};
179245

180-
const method = new GrpcServiceMethod<RpcRequest, RpcResponse>(
181-
route,
182-
route,
183-
localFn,
184-
decoder,
185-
encoder
186-
);
187-
246+
if (isEventHandler(handler)) {
247+
const theHandler = handler.handler;
248+
registration = this.stringKeyedEventHandler(keyed, route, theHandler);
249+
} else {
250+
registration = this.rpcHandler(keyed, route, handler);
251+
}
252+
serviceGrpcSpec.method.push(registration.descriptor);
188253
const url = `/invoke/${name}/${route}`;
189254
this.methods[url] = new HostedGrpcServiceMethod(
190255
{}, // we don't actually execute on any class instance
191256
servicePackage,
192257
serviceName,
193-
method
258+
registration.method
194259
) as HostedGrpcServiceMethod<unknown, unknown>;
195260

196261
rlog.info(
197-
`Registering: ${url} -> ${JSON.stringify(method, null, "\t")}`
262+
`Registering: ${url} -> ${JSON.stringify(
263+
registration.method,
264+
null,
265+
"\t"
266+
)}`
198267
);
199268
}
200269

@@ -376,6 +445,25 @@ async function dispatchUnkeyedRpcHandler(
376445
return RpcResponse.create({ response: result });
377446
}
378447

448+
async function dispatchKeyedEventHandler(
449+
origCtx: RestateContext,
450+
req: StringKeyedEvent,
451+
handler: Function
452+
): Promise<Empty> {
453+
const ctx = new RpcContextImpl(origCtx);
454+
const key = req.key;
455+
if (typeof key !== "string" || key.length === 0) {
456+
// we throw a terminal error here, because this cannot be patched by updating code:
457+
// if the request is wrong (missing a key), the request can never make it
458+
throw new TerminalError(
459+
"Keyed handlers must recieve a non null or empty string key"
460+
);
461+
}
462+
const jsEvent = new Event(key, req.payload, req.source, req.attributes);
463+
await handler(ctx, jsEvent);
464+
return Empty.create({});
465+
}
466+
379467
function copyProtoMetadata(
380468
original: RpcServiceProtoMetadata
381469
): RpcServiceProtoMetadata {
@@ -458,4 +546,14 @@ function createRpcMethodDescriptor(methodName: string): MethodDescriptorProto1 {
458546
return desc;
459547
}
460548

549+
function createStringKeyedMethodDescriptor(
550+
methodName: string
551+
): MethodDescriptorProto1 {
552+
const desc = {
553+
...rpcServiceProtoMetadata.fileDescriptor.service[0].method[1],
554+
} as MethodDescriptorProto1;
555+
desc.name = methodName;
556+
return desc;
557+
}
558+
461559
const dynrpcDescriptor = copyProtoMetadata(rpcServiceProtoMetadata);

src/types/router.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
/* eslint-disable @typescript-eslint/no-explicit-any */
1313

1414
import { RpcContext } from "../restate_context";
15+
import { Event } from "../types/types";
1516

1617
// ----------- generics -------------------------------------------------------
1718

@@ -25,11 +26,13 @@ type WithoutRpcContext<F> = F extends (
2526
: never;
2627

2728
export type Client<M> = {
28-
[K in keyof M]: M[K];
29+
[K in keyof M as M[K] extends never ? never : K]: M[K];
2930
};
3031

3132
export type SendClient<M> = {
32-
[K in keyof M]: M[K] extends (...args: infer P) => any
33+
[K in keyof M as M[K] extends never ? never : K]: M[K] extends (
34+
...args: infer P
35+
) => any
3336
? (...args: P) => void
3437
: never;
3538
};
@@ -68,11 +71,15 @@ export type KeyedHandler<F> = F extends (ctx: RpcContext) => Promise<any>
6871
: never;
6972

7073
export type KeyedRouterOpts<U> = {
71-
[K in keyof U]: U[K] extends KeyedHandler<any> ? U[K] : never;
74+
[K in keyof U]: U[K] extends KeyedHandler<any> | KeyedEventHandler<U[K]>
75+
? U[K]
76+
: never;
7277
};
7378

7479
export type KeyedRouter<U> = {
75-
[K in keyof U]: U[K] extends KeyedHandler<infer F>
80+
[K in keyof U]: U[K] extends KeyedEventHandler<U[K]>
81+
? never
82+
: U[K] extends KeyedHandler<infer F>
7683
? WithKeyArgument<WithoutRpcContext<F>>
7784
: never;
7885
};
@@ -83,3 +90,24 @@ export const keyedRouter = <M>(opts: KeyedRouterOpts<M>): KeyedRouter<M> => {
8390
}
8491
return opts as KeyedRouter<M>;
8592
};
93+
94+
// ----------- event handlers ----------------------------------------------
95+
96+
export type KeyedEventHandler<U> = U extends (
97+
ctx: RpcContext,
98+
event: Event
99+
) => Promise<void>
100+
? U
101+
: never;
102+
103+
export const keyedEventHandler = <H>(handler: KeyedEventHandler<H>): H => {
104+
return { eventHandler: true, handler: handler } as H;
105+
};
106+
107+
export const isEventHandler = (
108+
handler: any
109+
): handler is {
110+
handler: (ctx: RpcContext, event: Event) => Promise<void>;
111+
} => {
112+
return typeof handler === "object" && handler["eventHandler"];
113+
};

src/types/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,20 @@ export class Header {
120120
return res;
121121
}
122122
}
123+
124+
export class Event {
125+
constructor(
126+
readonly key: string,
127+
readonly payload: Buffer,
128+
readonly source: string,
129+
readonly attributes: Record<string, string>
130+
) {}
131+
132+
public json<T>(): T {
133+
return JSON.parse(this.payload.toString("utf-8")) as T;
134+
}
135+
136+
public body(): Uint8Array {
137+
return this.payload;
138+
}
139+
}

0 commit comments

Comments
 (0)