diff --git a/README.md b/README.md index 39755813..ee200108 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ restate.serve({ services: [greeter], port: 9080 }); ## Using the SDK Prerequisites: -- [NodeJS](https://nodejs.org/en/) >= v18.17.1 or [Bun](https://bun.sh/docs/installation) or [Deno](https://deno.land/#installation) +- [NodeJS](https://nodejs.org/en/) >= v20.19 or [Bun](https://bun.sh/docs/installation) or [Deno](https://deno.land/#installation) - [npm CLI](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) >= 9.6.7 To use this SDK, add the dependency to your project: diff --git a/package-lock.json b/package-lock.json index faf23636..a3e20196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "root", - "version": "1.8.0", + "version": "1.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "root", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "workspaces": [ "packages/restate-sdk-core", @@ -22,7 +22,7 @@ "@arethetypeswrong/cli": "^0.15.3", "@microsoft/api-extractor": "^7.52.8", "@release-it-plugins/workspaces": "^4.2.0", - "@types/node": "^20.10.4", + "@types/node": "^20.19.11", "@typescript-eslint/eslint-plugin": "^7.13.0", "@typescript-eslint/parser": "^7.13.0", "eslint": "^8.57.0", @@ -31,14 +31,14 @@ "eslint-plugin-require-extensions": "^0.1.3", "prettier": "^2.8.4", "release-it": "^17.11.0", - "typedoc": "^0.28.7", - "typescript": "^5.4.5", + "typedoc": "^0.28.10", + "typescript": "^5.9.2", "vitest": "^3.0.9", "wasm-pack": "^0.0.0", "wasm-pack-inline": "^0.1.2" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "node_modules/@andrewbranch/untar.js": { @@ -713,16 +713,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz", - "integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.11.0.tgz", + "integrity": "sha512-ooCDMAOKv71O7MszbXjSQGcI6K5T6NKlemQZOBHLq7Sv/oXCRfYbZ7UgbzFdl20lSXju6Juds4I3y30R6rHA4Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.7.0", - "@shikijs/langs": "^3.7.0", - "@shikijs/themes": "^3.7.0", - "@shikijs/types": "^3.7.0", + "@shikijs/engine-oniguruma": "^3.11.0", + "@shikijs/langs": "^3.11.0", + "@shikijs/themes": "^3.11.0", + "@shikijs/types": "^3.11.0", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -998,6 +998,20 @@ "node": ">=10" } }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", @@ -1881,40 +1895,40 @@ "license": "BSD-3-Clause" }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz", - "integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.11.0.tgz", + "integrity": "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0", + "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.7.0.tgz", - "integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.11.0.tgz", + "integrity": "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.11.0" } }, "node_modules/@shikijs/themes": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.7.0.tgz", - "integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.11.0.tgz", + "integrity": "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.11.0" } }, "node_modules/@shikijs/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.7.0.tgz", - "integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.11.0.tgz", + "integrity": "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2017,12 +2031,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.4.tgz", - "integrity": "sha512-Fi1Bj8qTJr4f1FDdHFR7oMlOawEYSzkHNdBJK+aRjcDDNHwEV3jPPjuZP2Lh2QNgXeqzM8Y+U6b6urKAog2rZw==", + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/ssh2": { @@ -9309,13 +9323,13 @@ } }, "node_modules/typedoc": { - "version": "0.28.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.7.tgz", - "integrity": "sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==", + "version": "0.28.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.10.tgz", + "integrity": "sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.7.0", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", @@ -9329,7 +9343,7 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -9359,9 +9373,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -9416,9 +9430,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unicode-emoji-modifier-base": { @@ -10244,99 +10258,99 @@ }, "packages/restate-e2e-services": { "name": "@restatedev/restate-e2e-services", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { - "@restatedev/restate-sdk": "^1.8.0", - "@restatedev/restate-sdk-clients": "^1.8.0", + "@restatedev/restate-sdk": "^1.8.3", + "@restatedev/restate-sdk-clients": "^1.8.3", "heapdump": "^0.3.15", "uuid": "^9.0.0" }, "devDependencies": { "@types/heapdump": "^0.3.4", - "@types/node": "^20.17.4", + "@types/node": "^20.19.11", "@types/uuid": "^10.0.0", "tsx": "^4.15.7" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "packages/restate-sdk": { "name": "@restatedev/restate-sdk", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { - "@restatedev/restate-sdk-core": "^1.8.0" + "@restatedev/restate-sdk-core": "^1.8.3" }, "devDependencies": { "@types/aws-lambda": "^8.10.115" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "packages/restate-sdk-clients": { "name": "@restatedev/restate-sdk-clients", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { - "@restatedev/restate-sdk-core": "^1.8.0" + "@restatedev/restate-sdk-core": "^1.8.3" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "packages/restate-sdk-cloudflare-workers": { "name": "@restatedev/restate-sdk-cloudflare-workers", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { - "@restatedev/restate-sdk-core": "^1.8.0" + "@restatedev/restate-sdk-core": "^1.8.3" }, "devDependencies": { "@types/aws-lambda": "^8.10.115" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "packages/restate-sdk-core": { "name": "@restatedev/restate-sdk-core", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "packages/restate-sdk-examples": { "name": "@restatedev/restate-sdk-examples", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { - "@restatedev/restate-sdk": "^1.8.0", - "@restatedev/restate-sdk-clients": "^1.8.0" + "@restatedev/restate-sdk": "^1.8.3", + "@restatedev/restate-sdk-clients": "^1.8.3" }, "devDependencies": { - "@restatedev/restate-sdk-testcontainers": "^1.8.0", + "@restatedev/restate-sdk-testcontainers": "^1.8.3", "tsx": "^4.15.7" }, "engines": { - "node": ">= 18.13" + "node": ">= 22.15" } }, "packages/restate-sdk-testcontainers": { "name": "@restatedev/restate-sdk-testcontainers", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { - "@restatedev/restate-sdk": "^1.8.0", - "@restatedev/restate-sdk-clients": "^1.8.0", + "@restatedev/restate-sdk": "^1.8.3", + "@restatedev/restate-sdk-clients": "^1.8.3", "apache-arrow": "^18.0.0", "testcontainers": "^10.24.1" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } }, "packages/restate-sdk-testcontainers/node_modules/@types/dockerode": { @@ -10470,17 +10484,17 @@ }, "packages/restate-sdk-zod": { "name": "@restatedev/restate-sdk-zod", - "version": "1.8.0", + "version": "1.8.3", "license": "MIT", "dependencies": { "zod": "^3.24.1", "zod-to-json-schema": "3.24.3" }, "devDependencies": { - "@restatedev/restate-sdk-core": "^1.8.0" + "@restatedev/restate-sdk-core": "^1.8.3" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } } } diff --git a/package.json b/package.json index 058cc70d..67941000 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@arethetypeswrong/cli": "^0.15.3", "@microsoft/api-extractor": "^7.52.8", "@release-it-plugins/workspaces": "^4.2.0", - "@types/node": "^20.10.4", + "@types/node": "^20.19.11", "@typescript-eslint/eslint-plugin": "^7.13.0", "@typescript-eslint/parser": "^7.13.0", "eslint": "^8.57.0", @@ -50,14 +50,14 @@ "eslint-plugin-require-extensions": "^0.1.3", "prettier": "^2.8.4", "release-it": "^17.11.0", - "typedoc": "^0.28.7", - "typescript": "^5.4.5", + "typedoc": "^0.28.10", + "typescript": "^5.9.2", "vitest": "^3.0.9", "wasm-pack": "^0.0.0", "wasm-pack-inline": "^0.1.2" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "publishConfig": { "@restatedev:registry": "https://registry.npmjs.org", diff --git a/packages/restate-e2e-services/package.json b/packages/restate-e2e-services/package.json index b35c5fc1..d53b6457 100644 --- a/packages/restate-e2e-services/package.json +++ b/packages/restate-e2e-services/package.json @@ -37,11 +37,11 @@ }, "devDependencies": { "@types/heapdump": "^0.3.4", - "@types/node": "^20.17.4", + "@types/node": "^20.19.11", "@types/uuid": "^10.0.0", "tsx": "^4.15.7" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" } } diff --git a/packages/restate-sdk-clients/package.json b/packages/restate-sdk-clients/package.json index 658104ae..7bcfae51 100644 --- a/packages/restate-sdk-clients/package.json +++ b/packages/restate-sdk-clients/package.json @@ -50,7 +50,7 @@ "@restatedev/restate-sdk-core": "^1.8.3" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "directories": { "test": "test" diff --git a/packages/restate-sdk-cloudflare-workers/package.json b/packages/restate-sdk-cloudflare-workers/package.json index ec31559f..78fe0a8f 100644 --- a/packages/restate-sdk-cloudflare-workers/package.json +++ b/packages/restate-sdk-cloudflare-workers/package.json @@ -74,7 +74,7 @@ "@types/aws-lambda": "^8.10.115" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "publishConfig": { "@restatedev:registry": "https://registry.npmjs.org" diff --git a/packages/restate-sdk-core/package.json b/packages/restate-sdk-core/package.json index 050f0d0e..f893cfca 100644 --- a/packages/restate-sdk-core/package.json +++ b/packages/restate-sdk-core/package.json @@ -46,7 +46,7 @@ "release": "release-it" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "directories": { "example": "examples", diff --git a/packages/restate-sdk-examples/package.json b/packages/restate-sdk-examples/package.json index b7e97c74..2feb1be8 100644 --- a/packages/restate-sdk-examples/package.json +++ b/packages/restate-sdk-examples/package.json @@ -44,6 +44,6 @@ "@restatedev/restate-sdk-testcontainers": "^1.8.3" }, "engines": { - "node": ">= 18.13" + "node": ">= 22.15" } } diff --git a/packages/restate-sdk-testcontainers/package.json b/packages/restate-sdk-testcontainers/package.json index 1fd9ee09..5f768f09 100644 --- a/packages/restate-sdk-testcontainers/package.json +++ b/packages/restate-sdk-testcontainers/package.json @@ -53,7 +53,7 @@ "testcontainers": "^10.24.1" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "directories": { "test": "test" diff --git a/packages/restate-sdk-zod/package.json b/packages/restate-sdk-zod/package.json index 94971f4d..5c301ee6 100644 --- a/packages/restate-sdk-zod/package.json +++ b/packages/restate-sdk-zod/package.json @@ -53,7 +53,7 @@ "release": "release-it" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "directories": { "example": "examples", diff --git a/packages/restate-sdk/package.json b/packages/restate-sdk/package.json index 3c93fb88..22dab62b 100644 --- a/packages/restate-sdk/package.json +++ b/packages/restate-sdk/package.json @@ -99,7 +99,7 @@ "@types/aws-lambda": "^8.10.115" }, "engines": { - "node": ">= 18.13" + "node": ">= 20.19" }, "directories": { "example": "examples", diff --git a/packages/restate-sdk/src/endpoint/discovery.ts b/packages/restate-sdk/src/endpoint/discovery.ts index 1d204268..cb6dee0a 100644 --- a/packages/restate-sdk/src/endpoint/discovery.ts +++ b/packages/restate-sdk/src/endpoint/discovery.ts @@ -25,6 +25,10 @@ export interface Endpoint { * Maximum supported protocol version */ maxProtocolVersion: number; + /** + * Compression used when the endpoint is a Lambda. This is unsupported if the endpoint is a regular HTTP endpoint. + */ + lambdaCompression?: "zstd"; services: Service[]; } export interface Service { diff --git a/packages/restate-sdk/src/endpoint/fetch_endpoint.ts b/packages/restate-sdk/src/endpoint/fetch_endpoint.ts index 707ab1ce..9e93e4b2 100644 --- a/packages/restate-sdk/src/endpoint/fetch_endpoint.ts +++ b/packages/restate-sdk/src/endpoint/fetch_endpoint.ts @@ -102,7 +102,8 @@ export class FetchEndpointImpl implements FetchEndpoint { } { const genericHandler = new GenericHandler( this.builder.build(), - this.protocolMode + this.protocolMode, + {} ); return fetcher(genericHandler); } diff --git a/packages/restate-sdk/src/endpoint/handlers/generic.ts b/packages/restate-sdk/src/endpoint/handlers/generic.ts index 2a5afc56..448ab8fa 100644 --- a/packages/restate-sdk/src/endpoint/handlers/generic.ts +++ b/packages/restate-sdk/src/endpoint/handlers/generic.ts @@ -16,7 +16,10 @@ import { RetryableError, TerminalError, } from "../../types/errors.js"; -import type { ProtocolMode } from "../discovery.js"; +import type { + ProtocolMode, + Endpoint as EndpointManifest, +} from "../discovery.js"; import type { Component, ComponentHandler } from "../components.js"; import { parseUrlComponents } from "../components.js"; import { X_RESTATE_SERVER } from "../../user_agent.js"; @@ -78,6 +81,7 @@ export interface RestateHandler { const ENDPOINT_MANIFEST_V2 = "application/vnd.restate.endpointmanifest.v2+json"; const ENDPOINT_MANIFEST_V3 = "application/vnd.restate.endpointmanifest.v3+json"; +const ENDPOINT_MANIFEST_V4 = "application/vnd.restate.endpointmanifest.v4+json"; /** * This is an internal API to support 'fetch' like handlers. @@ -93,7 +97,8 @@ export class GenericHandler implements RestateHandler { constructor( readonly endpoint: Endpoint, - private readonly protocolMode: ProtocolMode + private readonly protocolMode: ProtocolMode, + private readonly additionalDiscoveryFields: Partial ) { // Setup identity verifier if ( @@ -489,7 +494,9 @@ export class GenericHandler implements RestateHandler { // Negotiate version to use let manifestVersion; - if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V3)) { + if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V4)) { + manifestVersion = 4; + } else if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V3)) { manifestVersion = 3; } else if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V2)) { manifestVersion = 2; @@ -501,20 +508,9 @@ export class GenericHandler implements RestateHandler { const discovery = { ...this.endpoint.discoveryMetadata, + ...this.additionalDiscoveryFields, protocolMode: this.protocolMode, }; - const body = JSON.stringify(discovery); - - // type AllowedNames = { [K in keyof T]: T[K] extends U ? K : never; }[keyof T]; - // - // const checkUnsupportedFeature = (obj: Record, fields: Array)=> { - // for (const field of fields) { - // if (field in obj && obj[field] !== undefined) { - // 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.`); - // } - // } - // return; - // } const checkUnsupportedFeature = ( obj: T, @@ -526,7 +522,7 @@ export class GenericHandler implements RestateHandler { 500, `The code uses the new discovery feature '${String( field - )}' but the runtime doesn't support it yet. Either remove the usage of this feature, or upgrade the runtime.` + )}' but the runtime doesn't support it yet (discovery protocol negotiated version ${manifestVersion}). Either remove the usage of this feature, or upgrade the runtime.` ); } } @@ -566,10 +562,20 @@ export class GenericHandler implements RestateHandler { } } + if (manifestVersion < 4) { + // Blank the lambda compression field. No need to fail in this case. + discovery.lambdaCompression = undefined; + } + + const body = JSON.stringify(discovery); return { headers: { "content-type": - manifestVersion === 2 ? ENDPOINT_MANIFEST_V2 : ENDPOINT_MANIFEST_V3, + manifestVersion === 2 + ? ENDPOINT_MANIFEST_V2 + : manifestVersion === 3 + ? ENDPOINT_MANIFEST_V3 + : ENDPOINT_MANIFEST_V4, "x-restate-server": X_RESTATE_SERVER, }, statusCode: 200, diff --git a/packages/restate-sdk/src/endpoint/handlers/lambda.ts b/packages/restate-sdk/src/endpoint/handlers/lambda.ts index b9258aeb..ddc26321 100644 --- a/packages/restate-sdk/src/endpoint/handlers/lambda.ts +++ b/packages/restate-sdk/src/endpoint/handlers/lambda.ts @@ -26,9 +26,15 @@ import { WritableStream, type ReadableStream } from "node:stream/web"; import { OnceStream } from "../../utils/streams.js"; import { X_RESTATE_SERVER } from "../../user_agent.js"; import { ensureError } from "../../types/errors.js"; +import * as zlib from "node:zlib"; + +const RESPONSE_COMPRESSION_THRESHOLD = 3 * 1024 * 1024; export class LambdaHandler { - constructor(private readonly handler: GenericHandler) {} + constructor( + private readonly handler: GenericHandler, + private readonly compressionSupported: boolean + ) {} async handleRequest( event: APIGatewayProxyEvent | APIGatewayProxyEventV2, @@ -39,33 +45,74 @@ export class LambdaHandler { // const path = "path" in event ? event.path : event.rawPath; + // Deal with content-encoding + let requestContentEncoding; + let requestAcceptEncoding; + for (const [key, value] of Object.entries(event.headers)) { + if ( + key.localeCompare("content-encoding", undefined, { + sensitivity: "accent", + }) + ) { + requestContentEncoding = value; + continue; + } + if ( + key.localeCompare("accept-encoding", undefined, { + sensitivity: "accent", + }) + ) { + requestAcceptEncoding = value; + } + } + // // Convert the request body to a Uint8Array stream // Lambda functions receive the body as base64 encoded string // - let body: ReadableStream | null; + let bodyStream: ReadableStream | null; if (!event.body) { - body = null; - } else if (event.isBase64Encoded) { - body = OnceStream(Buffer.from(event.body, "base64")); + bodyStream = null; } else { - body = OnceStream(new TextEncoder().encode(event.body)); + let bodyBuffer: Buffer | undefined; + if (event.isBase64Encoded) { + bodyBuffer = Buffer.from(event.body, "base64"); + } else { + bodyBuffer = Buffer.from(new TextEncoder().encode(event.body)); + } + + // Now decode if needed + if (requestContentEncoding && requestContentEncoding.includes("zstd")) { + if (!this.compressionSupported) { + throw new Error( + "The input is compressed using zstd, but this lambda deployment doesn't support compression. Make sure to deploy the Lambda using Node > 22" + ); + } + + // Input encoded with zstd, let's decode it! + bodyBuffer = ( + zlib as unknown as { zstdDecompressSync: (b: Buffer) => Buffer } + ).zstdDecompressSync(bodyBuffer); + } + + // Prep the stream to pass through the endpoint handler + bodyStream = OnceStream(bodyBuffer); } const abortController = new AbortController(); const request: RestateRequest = { - body, + body: bodyStream, headers: event.headers, url: path, extraArgs: [context], abortSignal: abortController.signal, }; - let resp: RestateResponse; + let response: RestateResponse; try { - resp = await this.handler.handle(request, { + response = await this.handler.handle(request, { AWSRequestId: context.awsRequestId, }); } catch (e) { @@ -76,7 +123,7 @@ export class LambdaHandler { const chunks: Uint8Array[] = []; try { - await resp.body.pipeTo( + await response.body.pipeTo( new WritableStream({ write: (chunk) => { chunks.push(chunk); @@ -103,11 +150,35 @@ export class LambdaHandler { abortController.abort(); } + const responseBodyBuffer = Buffer.concat(chunks); + let responseBody; + + // Now let's encode if we need to. + if ( + this.compressionSupported && + responseBodyBuffer.length > RESPONSE_COMPRESSION_THRESHOLD && + requestAcceptEncoding && + requestAcceptEncoding.includes("zstd") + ) { + response.headers["content-encoding"] = "zstd"; + + responseBody = ( + zlib as unknown as { zstdCompressSync: (b: Buffer) => Buffer } + ) + .zstdCompressSync(responseBodyBuffer) + .toString("base64"); + } else { + responseBody = responseBodyBuffer.toString("base64"); + } return { - headers: resp.headers, - statusCode: resp.statusCode, + headers: response.headers, + statusCode: response.statusCode, isBase64Encoded: true, - body: Buffer.concat(chunks).toString("base64"), + body: responseBody, }; } } + +export function isCompressionSupported() { + return "zstdDecompressSync" in zlib && "zstdCompressSync" in zlib; +} diff --git a/packages/restate-sdk/src/endpoint/lambda_endpoint.ts b/packages/restate-sdk/src/endpoint/lambda_endpoint.ts index 6e032bd0..ce2dbc59 100644 --- a/packages/restate-sdk/src/endpoint/lambda_endpoint.ts +++ b/packages/restate-sdk/src/endpoint/lambda_endpoint.ts @@ -21,7 +21,7 @@ import type { RestateEndpointBase, } from "../endpoint.js"; import { GenericHandler } from "./handlers/generic.js"; -import { LambdaHandler } from "./handlers/lambda.js"; +import { isCompressionSupported, LambdaHandler } from "./handlers/lambda.js"; import type { LoggerTransport } from "../logging/logger_transport.js"; /** @@ -80,11 +80,18 @@ export class LambdaEndpointImpl implements LambdaEndpoint { // eslint-disable-next-line @typescript-eslint/no-explicit-any handler(): (event: any, ctx: any) => Promise { + const compressionEnabled = isCompressionSupported(); + const genericHandler = new GenericHandler( this.builder.build(), - "REQUEST_RESPONSE" + "REQUEST_RESPONSE", + compressionEnabled + ? { + lambdaCompression: "zstd", + } + : {} ); - const lambdaHandler = new LambdaHandler(genericHandler); + const lambdaHandler = new LambdaHandler(genericHandler, compressionEnabled); return lambdaHandler.handleRequest.bind(lambdaHandler); } } diff --git a/packages/restate-sdk/src/endpoint/node_endpoint.ts b/packages/restate-sdk/src/endpoint/node_endpoint.ts index acac53f0..b01d5033 100644 --- a/packages/restate-sdk/src/endpoint/node_endpoint.ts +++ b/packages/restate-sdk/src/endpoint/node_endpoint.ts @@ -111,7 +111,7 @@ export class NodeEndpoint implements RestateEndpoint { function nodeHttp2Handler( endpoint: Endpoint ): (request: Http2ServerRequest, response: Http2ServerResponse) => void { - const handler = new GenericHandler(endpoint, "BIDI_STREAM"); + const handler = new GenericHandler(endpoint, "BIDI_STREAM", {}); return (request, response) => { (async () => {