From 1bfb7c1dcdf8986b51a1efd20b7980ddb68457f2 Mon Sep 17 00:00:00 2001 From: Alexander Teague Date: Tue, 26 Aug 2025 08:49:58 +0200 Subject: [PATCH] Add Typescript SDK serde for Effect --- lefthook.yml | 35 +++++++++ packages/restate-sdk-effect/.eslintignore | 3 + packages/restate-sdk-effect/README.md | 62 +++++++++++++++ .../restate-sdk-effect/api-extractor.json | 7 ++ packages/restate-sdk-effect/package.json | 64 +++++++++++++++ packages/restate-sdk-effect/src/public_api.ts | 13 ++++ packages/restate-sdk-effect/src/serde_api.ts | 78 +++++++++++++++++++ .../restate-sdk-effect/tsconfig.eslint.json | 5 ++ packages/restate-sdk-effect/tsconfig.json | 9 +++ 9 files changed, 276 insertions(+) create mode 100644 lefthook.yml create mode 100644 packages/restate-sdk-effect/.eslintignore create mode 100644 packages/restate-sdk-effect/README.md create mode 100644 packages/restate-sdk-effect/api-extractor.json create mode 100644 packages/restate-sdk-effect/package.json create mode 100644 packages/restate-sdk-effect/src/public_api.ts create mode 100644 packages/restate-sdk-effect/src/serde_api.ts create mode 100644 packages/restate-sdk-effect/tsconfig.eslint.json create mode 100644 packages/restate-sdk-effect/tsconfig.json diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 00000000..f6e5dfe5 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,35 @@ +# EXAMPLE USAGE: +# +# Refer for explanation to following link: +# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md +# +# pre-push: +# commands: +# packages-audit: +# tags: frontend security +# run: yarn audit +# gems-audit: +# tags: backend security +# run: bundle audit +# +# pre-commit: +# parallel: true +# commands: +# eslint: +# glob: "*.{js,ts,jsx,tsx}" +# run: yarn eslint {staged_files} +# rubocop: +# tags: backend style +# glob: "*.rb" +# exclude: '(^|/)(application|routes)\.rb$' +# run: bundle exec rubocop --force-exclusion {all_files} +# govet: +# tags: backend style +# files: git ls-files -m +# glob: "*.go" +# run: go vet {files} +# scripts: +# "hello.js": +# runner: node +# "any.go": +# runner: go run diff --git a/packages/restate-sdk-effect/.eslintignore b/packages/restate-sdk-effect/.eslintignore new file mode 100644 index 00000000..bc70c0df --- /dev/null +++ b/packages/restate-sdk-effect/.eslintignore @@ -0,0 +1,3 @@ +node_modules +dist +src/generated diff --git a/packages/restate-sdk-effect/README.md b/packages/restate-sdk-effect/README.md new file mode 100644 index 00000000..07f20645 --- /dev/null +++ b/packages/restate-sdk-effect/README.md @@ -0,0 +1,62 @@ +[![Documentation](https://img.shields.io/badge/doc-reference-blue)](https://docs.restate.dev) +[![Examples](https://img.shields.io/badge/view-examples-blue)](https://github.com/restatedev/examples) +[![NPM Version](https://img.shields.io/npm/v/%40restatedev%2Frestate-sdk-effect)](https://www.npmjs.com/package/@restatedev/restate-sdk-effect) +[![Discord](https://img.shields.io/discord/1128210118216007792?logo=discord)](https://discord.gg/skW3AZ6uGd) +[![Twitter](https://img.shields.io/twitter/follow/restatedev.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=restatedev) + +# Restate Typescript SDK Effect integration + +[Restate](https://restate.dev/) is a system for easily building resilient applications using *distributed durable async/await*. + +This package contains a effect integration, allowing to define input/output models of your handlers. + +```typescript +import * as restate from "@restatedev/restate-sdk"; +import { serde } from "@restatedev/restate-sdk-effect"; +import { Schema } from "effect"; + +const Greeting = Schema.Struct({ + name: Schema.String +}); + +const greeter = restate.service({ + name: "greeter", + handlers: { + greet: restate.handlers.handler( + { + input: serde.effect(Greeting), + output: serde.effect(Schema.String), + }, + async (ctx, greeting) => { + return `Hello ${greeting.name}!`; + } + ), + }, +}); + +export type Greeter = typeof greeter; + +restate.serve({ services: [greeter], port: 9080 }); +``` + +For the SDK main package, checkout [`@restatedev/restate-sdk`](../restate-sdk). + +## Community + +* 🤗️ [Join our online community](https://discord.gg/skW3AZ6uGd) for help, sharing feedback and talking to the community. +* 📖 [Check out our documentation](https://docs.restate.dev) to get quickly started! +* 📣 [Follow us on Twitter](https://twitter.com/restatedev) for staying up to date. +* 🙋 [Create a GitHub issue](https://github.com/restatedev/sdk-typescript/issues) for requesting a new feature or reporting a problem. +* 🏠 [Visit our GitHub org](https://github.com/restatedev) for exploring other repositories. + +## Using the library + +To use this library, add the dependency to your project: + +```shell +npm install --save-dev @restatedev/restate-sdk-effect +``` + +## Versions + +This library follows [Semantic Versioning](https://semver.org/). diff --git a/packages/restate-sdk-effect/api-extractor.json b/packages/restate-sdk-effect/api-extractor.json new file mode 100644 index 00000000..a93ade2b --- /dev/null +++ b/packages/restate-sdk-effect/api-extractor.json @@ -0,0 +1,7 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.base.json" +} diff --git a/packages/restate-sdk-effect/package.json b/packages/restate-sdk-effect/package.json new file mode 100644 index 00000000..0b56e08e --- /dev/null +++ b/packages/restate-sdk-effect/package.json @@ -0,0 +1,64 @@ +{ + "name": "@restatedev/restate-sdk-effect", + "version": "1.0.0", + "description": "Restate Typescript SDK Effect", + "author": "Alexander Teague", + "license": "MIT", + "email": "alex@teague.de", + "homepage": "https://github.com/restatedev/sdk-typescript#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/restatedev/sdk-typescript.git" + }, + "bugs": { + "url": "https://github.com/restatedev/sdk-typescript/issues" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/cjs/src/public_api.js", + "types": "./dist/cjs/src/public_api.d.ts", + "module": "./dist/esm/src/public_api.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/src/public_api.d.ts", + "default": "./dist/esm/src/public_api.js" + }, + "require": { + "types": "./dist/cjs/src/public_api.d.ts", + "default": "./dist/cjs/src/public_api.js" + } + } + }, + "files": [ + "dist" + ], + "devDependencies": { + "@restatedev/restate-sdk-core": "^1.8.3" + }, + "dependencies": { + "effect": "^3.17.9" + }, + "scripts": { + "api:extract": "api-extractor run --local", + "build": "npm run build:cjs && npm run build:esm", + "build:cjs": "tsc --module commonjs --verbatimModuleSyntax false --moduleResolution node10 --outDir ./dist/cjs --declaration --declarationDir ./dist/cjs && echo >./dist/cjs/package.json '{\"type\":\"commonjs\"}'", + "build:esm": "tsc --outDir ./dist/esm --declaration --declarationDir ./dist/esm", + "lint": "eslint --ignore-path .eslintignore --max-warnings=0 --ext .ts .", + "format": "prettier --ignore-path .eslintignore --write \"**/*.+(js|ts|json)\"", + "format-check": "prettier --ignore-path .eslintignore --check \"**/*.+(js|ts|json)\"", + "attw": "attw --pack", + "verify": "npm run format-check && npm run lint && npm run build && npm run attw && npm run api:extract", + "release": "release-it" + }, + "engines": { + "node": ">= 20.19" + }, + "directories": { + "example": "examples", + "test": "test" + }, + "publishConfig": { + "@restatedev:registry": "https://registry.npmjs.org" + } +} diff --git a/packages/restate-sdk-effect/src/public_api.ts b/packages/restate-sdk-effect/src/public_api.ts new file mode 100644 index 00000000..6adbe503 --- /dev/null +++ b/packages/restate-sdk-effect/src/public_api.ts @@ -0,0 +1,13 @@ +/* + * 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 + */ + +export { serde } from "./serde_api.js"; +export type { Serde } from "./serde_api.js"; diff --git a/packages/restate-sdk-effect/src/serde_api.ts b/packages/restate-sdk-effect/src/serde_api.ts new file mode 100644 index 00000000..1ad4fff5 --- /dev/null +++ b/packages/restate-sdk-effect/src/serde_api.ts @@ -0,0 +1,78 @@ +import type { Serde } from "@restatedev/restate-sdk-core"; +import { Effect, JSONSchema, ParseResult, Schema } from "effect"; + +export type { Serde } from "@restatedev/restate-sdk-core"; + +class EffectSerde implements Serde { + contentType? = "application/json"; + jsonSchema?: object | undefined; + + constructor(private readonly schema: Schema.Schema) { + // Generate JSON schema from Effect Schema + this.jsonSchema = JSONSchema.make(schema); + + // Handle void/undefined types + if (schema.Type === Schema.Void || schema.Type === Schema.Undefined) { + this.contentType = undefined; + } + } + + serialize(value: A): Uint8Array { + if (value === undefined) { + return new Uint8Array(0); + } + + // Encode the value using the schema + const encoded = Schema.encode(this.schema)(value); + + // Run the effect synchronously and handle potential errors + const result = Effect.runSync( + Effect.catchAll(encoded, (error) => + Effect.die( + new Error( + `Serialization failed: ${ParseResult.TreeFormatter.formatErrorSync( + error + )}` + ) + ) + ) + ); + + return new TextEncoder().encode(JSON.stringify(result)); + } + + deserialize(data: Uint8Array): A { + const js = + data.length === 0 + ? undefined + : JSON.parse(new TextDecoder().decode(data)); + + // Decode the value using the schema + const decoded = Schema.decode(this.schema)(js); + + // Run the effect and handle errors + return Effect.runSync( + Effect.catchAll(decoded, (error) => + Effect.die( + new Error( + `Deserialization failed: ${ParseResult.TreeFormatter.formatErrorSync( + error + )}` + ) + ) + ) + ); + } +} + +export namespace serde { + /** + * An Effect Schema based serde. + * + * @param schema the Effect schema + * @returns a serde that will validate the data with the Effect schema + */ + export const effect = (schema: Schema.Schema): Serde => { + return new EffectSerde(schema); + }; +} diff --git a/packages/restate-sdk-effect/tsconfig.eslint.json b/packages/restate-sdk-effect/tsconfig.eslint.json new file mode 100644 index 00000000..c805a67c --- /dev/null +++ b/packages/restate-sdk-effect/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "test/**/*.ts"], + "references": [{ "path": "../restate-sdk-core" }] +} diff --git a/packages/restate-sdk-effect/tsconfig.json b/packages/restate-sdk-effect/tsconfig.json new file mode 100644 index 00000000..9e060ce4 --- /dev/null +++ b/packages/restate-sdk-effect/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/esm", + "paths": {} + }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../restate-sdk-core" }] +}