Skip to content

Commit 2b0bc2a

Browse files
Endpoint manifest v3 (#551)
* Discovery manifest V3. I regenerated the discovery manifest using a generator, the documentation for that is in the README. * Expose all the new service manifest configuration options. * Make sure that setting the v3 fields will fail discovery when using an old runtime.
1 parent 1bfce60 commit 2b0bc2a

File tree

12 files changed

+517
-169
lines changed

12 files changed

+517
-169
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ npm run -w packages/restate-sdk-examples workflow
109109

110110
See https://github.com/restatedev/e2e/ for more details.
111111

112+
### Re-generating the discovery manifest
113+
114+
```shell
115+
npx --package=json-schema-to-typescript json2ts endpoint_manifest_schema.json packages/restate-sdk/src/types/discovery.ts
116+
```
117+
112118
## Releasing the package
113119

114120
### Releasing via release-it
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2023-2024 - 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+
import {
13+
service,
14+
endpoint,
15+
handlers,
16+
type Context,
17+
} from "@restatedev/restate-sdk";
18+
19+
const greeter = service({
20+
name: "greeter",
21+
handlers: {
22+
greet: handlers.handler(
23+
{
24+
journalRetention: { days: 1 },
25+
},
26+
async (ctx: Context, name: string) => {
27+
return `Hello ${name}`;
28+
}
29+
),
30+
},
31+
options: {
32+
journalRetention: { days: 2 },
33+
},
34+
});
35+
36+
export type Greeter = typeof greeter;
37+
38+
endpoint().bind(greeter).listen();

packages/restate-sdk/src/common_api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export type {
7979
ServiceOpts,
8080
ObjectOpts,
8181
WorkflowOpts,
82+
ServiceOptions,
83+
ObjectOptions,
84+
WorkflowOptions,
8285
} from "./types/rpc.js";
8386
export {
8487
service,

packages/restate-sdk/src/endpoint/endpoint_builder.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import type {
1818
WorkflowDefinition,
1919
} from "@restatedev/restate-sdk-core";
2020

21+
import type {
22+
ObjectOptions,
23+
ServiceOptions,
24+
WorkflowOptions,
25+
} from "../types/rpc.js";
2126
import { HandlerWrapper } from "../types/rpc.js";
2227
import type { Component } from "../types/components.js";
2328
import {
@@ -59,6 +64,7 @@ function isWorkflowDefinition<P extends string, M>(
5964
type ServiceAuxInfo = {
6065
description?: string;
6166
metadata?: Record<string, any>;
67+
options?: ServiceOptions | ObjectOptions | WorkflowOptions;
6268
};
6369

6470
export class EndpointBuilder {
@@ -165,7 +171,8 @@ export class EndpointBuilder {
165171
const component = new ServiceComponent(
166172
name,
167173
definition.description,
168-
definition.metadata
174+
definition.metadata,
175+
definition?.options as ServiceOptions
169176
);
170177

171178
for (const [route, handler] of Object.entries(
@@ -193,7 +200,8 @@ export class EndpointBuilder {
193200
const component = new VirtualObjectComponent(
194201
name,
195202
definition.description,
196-
definition.metadata
203+
definition.metadata,
204+
definition?.options as ObjectOptions
197205
);
198206

199207
for (const [route, handler] of Object.entries(
@@ -220,7 +228,8 @@ export class EndpointBuilder {
220228
const component = new WorkflowComponent(
221229
name,
222230
definition.description,
223-
definition.metadata
231+
definition.metadata,
232+
definition?.options as WorkflowOptions
224233
);
225234

226235
for (const [route, handler] of Object.entries(

packages/restate-sdk/src/endpoint/fetch_endpoint.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { EndpointBuilder } from "./endpoint_builder.js";
1919
import type { RestateEndpointBase } from "../endpoint.js";
2020
import { GenericHandler } from "./handlers/generic.js";
2121
import { fetcher } from "./handlers/fetch.js";
22-
import { ProtocolMode } from "../types/discovery.js";
2322
import type { LoggerTransport } from "../logging/logger_transport.js";
23+
import type { ProtocolMode } from "../types/discovery.js";
2424

2525
/**
2626
* Generic Fetch encapsulates all the Restate services served by this endpoint.
@@ -90,9 +90,7 @@ export class FetchEndpointImpl implements FetchEndpoint {
9090
}
9191

9292
public bidirectional(set: boolean = true): FetchEndpoint {
93-
this.protocolMode = set
94-
? ProtocolMode.BIDI_STREAM
95-
: ProtocolMode.REQUEST_RESPONSE;
93+
this.protocolMode = set ? "BIDI_STREAM" : "REQUEST_RESPONSE";
9694
return this;
9795
}
9896

packages/restate-sdk/src/endpoint/handlers/generic.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export interface RestateHandler {
7171
): Promise<RestateResponse>;
7272
}
7373

74+
const ENDPOINT_MANIFEST_V2 = "application/vnd.restate.endpointmanifest.v2+json";
75+
const ENDPOINT_MANIFEST_V3 = "application/vnd.restate.endpointmanifest.v3+json";
76+
7477
/**
7578
* This is an internal API to support 'fetch' like handlers.
7679
* It supports both request-reply mode and bidirectional streaming mode.
@@ -429,11 +432,13 @@ export class GenericHandler implements RestateHandler {
429432
return this.toErrorResponse(415, errorMessage);
430433
}
431434

432-
if (
433-
!acceptVersionsString.includes(
434-
"application/vnd.restate.endpointmanifest.v1+json"
435-
)
436-
) {
435+
// Negotiate version to use
436+
let manifestVersion;
437+
if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V3)) {
438+
manifestVersion = 3;
439+
} else if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V2)) {
440+
manifestVersion = 2;
441+
} else {
437442
const errorMessage = `Unsupported service discovery protocol version '${acceptVersionsString}'`;
438443
this.endpoint.rlog.warn(errorMessage);
439444
return this.toErrorResponse(415, errorMessage);
@@ -442,9 +447,71 @@ export class GenericHandler implements RestateHandler {
442447
const discovery = this.endpoint.computeDiscovery(this.protocolMode);
443448
const body = JSON.stringify(discovery);
444449

450+
// type AllowedNames<T, U> = { [K in keyof T]: T[K] extends U ? K : never; }[keyof T];
451+
//
452+
// const checkUnsupportedFeature = (obj: Record<string, unknown>, fields: Array<string>)=> {
453+
// for (const field of fields) {
454+
// if (field in obj && obj[field] !== undefined) {
455+
// return this.toErrorResponse(500, `The code uses the new discovery feature '${field}' but the runtime doesn't support it yet. Either remove the usage of this feature, or upgrade the runtime.`);
456+
// }
457+
// }
458+
// return;
459+
// }
460+
461+
const checkUnsupportedFeature = <T extends object>(
462+
obj: T,
463+
...fields: Array<keyof T>
464+
) => {
465+
for (const field of fields) {
466+
if (field in obj && obj[field] !== undefined) {
467+
return this.toErrorResponse(
468+
500,
469+
`The code uses the new discovery feature '${String(
470+
field
471+
)}' but the runtime doesn't support it yet. Either remove the usage of this feature, or upgrade the runtime.`
472+
);
473+
}
474+
}
475+
return;
476+
};
477+
478+
// Verify none of the manifest v3 configuration options are used.
479+
if (manifestVersion < 3) {
480+
for (const service of discovery.services) {
481+
const error = checkUnsupportedFeature(
482+
service,
483+
"journalRetention",
484+
"idempotencyRetention",
485+
"inactivityTimeout",
486+
"abortTimeout",
487+
"enableLazyState",
488+
"ingressPrivate"
489+
);
490+
if (error !== undefined) {
491+
return error;
492+
}
493+
for (const handler of service.handlers) {
494+
const error = checkUnsupportedFeature(
495+
handler,
496+
"journalRetention",
497+
"idempotencyRetention",
498+
"workflowCompletionRetention",
499+
"inactivityTimeout",
500+
"abortTimeout",
501+
"enableLazyState",
502+
"ingressPrivate"
503+
);
504+
if (error !== undefined) {
505+
return error;
506+
}
507+
}
508+
}
509+
}
510+
445511
return {
446512
headers: {
447-
"content-type": "application/vnd.restate.endpointmanifest.v1+json",
513+
"content-type":
514+
manifestVersion === 2 ? ENDPOINT_MANIFEST_V2 : ENDPOINT_MANIFEST_V3,
448515
"x-restate-server": X_RESTATE_SERVER,
449516
},
450517
statusCode: 200,

packages/restate-sdk/src/endpoint/lambda_endpoint.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { EndpointBuilder } from "./endpoint_builder.js";
1919
import type { RestateEndpointBase } from "../endpoint.js";
2020
import { GenericHandler } from "./handlers/generic.js";
2121
import { LambdaHandler } from "./handlers/lambda.js";
22-
import { ProtocolMode } from "../types/discovery.js";
2322
import type { LoggerTransport } from "../logging/logger_transport.js";
2423

2524
/**
@@ -78,10 +77,7 @@ export class LambdaEndpointImpl implements LambdaEndpoint {
7877

7978
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8079
handler(): (event: any, ctx: any) => Promise<any> {
81-
const genericHandler = new GenericHandler(
82-
this.builder,
83-
ProtocolMode.REQUEST_RESPONSE
84-
);
80+
const genericHandler = new GenericHandler(this.builder, "REQUEST_RESPONSE");
8581
const lambdaHandler = new LambdaHandler(genericHandler);
8682
return lambdaHandler.handleRequest.bind(lambdaHandler);
8783
}

packages/restate-sdk/src/endpoint/node_endpoint.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { EndpointBuilder } from "./endpoint_builder.js";
2727
import { GenericHandler } from "./handlers/generic.js";
2828
import { Readable, Writable } from "node:stream";
2929
import type { WritableStream } from "node:stream/web";
30-
import { ProtocolMode } from "../types/discovery.js";
3130
import { ensureError } from "../types/errors.js";
3231
import type { LoggerTransport } from "../logging/logger_transport.js";
3332

@@ -70,7 +69,7 @@ export class NodeEndpoint implements RestateEndpoint {
7069
request: Http2ServerRequest,
7170
response: Http2ServerResponse
7271
) => void {
73-
const handler = new GenericHandler(this.builder, ProtocolMode.BIDI_STREAM);
72+
const handler = new GenericHandler(this.builder, "BIDI_STREAM");
7473

7574
return (request, response) => {
7675
(async () => {
@@ -113,10 +112,7 @@ export class NodeEndpoint implements RestateEndpoint {
113112

114113
// eslint-disable-next-line @typescript-eslint/no-explicit-any
115114
lambdaHandler(): (event: any, ctx: any) => Promise<any> {
116-
const genericHandler = new GenericHandler(
117-
this.builder,
118-
ProtocolMode.REQUEST_RESPONSE
119-
);
115+
const genericHandler = new GenericHandler(this.builder, "REQUEST_RESPONSE");
120116
const handler = new LambdaHandler(genericHandler);
121117
return handler.handleRequest.bind(handler);
122118
}

packages/restate-sdk/src/fetch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ import {
1515
type FetchEndpoint,
1616
FetchEndpointImpl,
1717
} from "./endpoint/fetch_endpoint.js";
18-
import { ProtocolMode } from "./types/discovery.js";
1918

2019
/**
2120
* Create a new {@link RestateEndpoint} in request response protocol mode.
2221
* Bidirectional mode (must be served over http2) can be enabled with .enableHttp2()
2322
*/
2423
export function endpoint(): FetchEndpoint {
25-
return new FetchEndpointImpl(ProtocolMode.REQUEST_RESPONSE);
24+
return new FetchEndpointImpl("REQUEST_RESPONSE");
2625
}

0 commit comments

Comments
 (0)