From 0f82405c250323afa23df8f6a7e086889ea3b621 Mon Sep 17 00:00:00 2001 From: igalshilman Date: Fri, 20 Mar 2026 18:43:38 +0100 Subject: [PATCH 1/3] [example] Exclude Gemfile.lock from the example --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c57c5be..7a147da 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ sorbet/rbi/gems/ sorbet/rbi/dsl/ sorbet/rbi/annotations/ +# Sub-project lock files (middleware_example has its own Gemfile) +middleware_example/Gemfile.lock + # OS .DS_Store Thumbs.db From fc873aea5ed38190709260d3c1cd24080535375f Mon Sep 17 00:00:00 2001 From: igalshilman Date: Fri, 20 Mar 2026 19:04:36 +0100 Subject: [PATCH 2/3] Remove sorbet --- .rubocop.yml | 12 +- Gemfile | 3 - Gemfile.lock | 53 --- Makefile | 18 +- README.md | 25 +- docs/INTERNALS.md | 25 +- docs/USER_GUIDE.md | 108 +---- etc/smoke-test.sh | 12 +- examples/config.ru | 3 - examples/greeter.rb | 1 - examples/typed_handlers.rb | 1 - examples/typed_handlers_sorbet.rb | 86 ---- lib/restate.rb | 98 +---- lib/restate/client.rb | 27 +- lib/restate/config.rb | 16 +- lib/restate/context.rb | 140 +------ lib/restate/discovery.rb | 22 +- lib/restate/durable_future.rb | 55 +-- lib/restate/endpoint.rb | 28 +- lib/restate/errors.rb | 16 +- lib/restate/handler.rb | 7 - lib/restate/serde.rb | 120 ------ lib/restate/server.rb | 33 +- lib/restate/server_context.rb | 242 +---------- lib/restate/service.rb | 4 +- lib/restate/service_dsl.rb | 17 +- lib/restate/service_proxy.rb | 16 +- lib/restate/testing.rb | 1 - lib/restate/version.rb | 1 - lib/restate/virtual_object.rb | 6 +- lib/restate/vm.rb | 82 +--- lib/restate/workflow.rb | 6 +- lib/tapioca/dsl/compilers/restate.rb | 115 ----- rbi/restate-sdk.rbi | 582 -------------------------- restate-sdk.gemspec | 2 - sorbet/config | 13 - sorbet/rbi/shims/async.rbi | 46 -- sorbet/rbi/shims/dry_struct.rbi | 17 - sorbet/rbi/shims/restate_internal.rbi | 259 ------------ sorbet/rbi/shims/tapioca.rbi | 14 - spec/harness_spec.rb | 28 +- template/AGENTS.md | 20 +- template/CLAUDE.md | 32 +- template/codex.md | 20 +- template/config.ru | 3 +- template/greeter.rb | 25 +- 46 files changed, 197 insertions(+), 2263 deletions(-) delete mode 100644 examples/typed_handlers_sorbet.rb delete mode 100644 lib/tapioca/dsl/compilers/restate.rb delete mode 100644 rbi/restate-sdk.rbi delete mode 100644 sorbet/config delete mode 100644 sorbet/rbi/shims/async.rbi delete mode 100644 sorbet/rbi/shims/dry_struct.rbi delete mode 100644 sorbet/rbi/shims/restate_internal.rbi delete mode 100644 sorbet/rbi/shims/tapioca.rbi diff --git a/.rubocop.yml b/.rubocop.yml index 99544e4..baa9c10 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,3 @@ -plugins: - - rubocop-sorbet - AllCops: TargetRubyVersion: 3.1 NewCops: enable @@ -9,7 +6,6 @@ AllCops: - "ext/**/*" - ".bundle/**/*" - "pkg/**/*" - - "sorbet/rbi/**/*" - "spec/**/*" - "tmp/**/*" - "vendor/**/*" @@ -85,6 +81,7 @@ Metrics/BlockLength: Metrics/ParameterLists: Exclude: - "lib/restate.rb" + - "lib/restate/context.rb" - "lib/restate/server_context.rb" - "lib/restate/virtual_object.rb" - "lib/restate/vm.rb" @@ -94,13 +91,6 @@ Lint/DuplicateBranch: Exclude: - "lib/restate/server.rb" -# Sorbet sigs require named block parameters, conflicts with anonymous forwarding -Naming/BlockForwarding: - Enabled: false - -Style/ArgumentsForwarding: - Enabled: false - # These marker classes are intentionally empty (used as type tags) Lint/EmptyClass: Exclude: diff --git a/Gemfile b/Gemfile index 2e44d5c..5b123f6 100644 --- a/Gemfile +++ b/Gemfile @@ -15,9 +15,6 @@ group :development, :test do gem 'falcon', '~> 0.47', require: false gem 'rspec', '~> 3.12' gem 'rubocop', require: false - gem 'rubocop-sorbet', require: false - gem 'sorbet', require: false - gem 'tapioca', require: false gem 'testcontainers-core', require: false # For middleware_example/ diff --git a/Gemfile.lock b/Gemfile.lock index e2326e9..c9197fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,7 +4,6 @@ PATH restate-sdk (0.7.0) async (~> 2.0) rack (>= 2.0) - sorbet-runtime GEM remote: https://rubygems.org/ @@ -42,7 +41,6 @@ GEM async-utilization (0.3.1) console (~> 1.0) base64 (0.3.0) - benchmark (0.5.0) bigdecimal (4.0.1) concurrent-ruby (1.3.6) console (1.34.3) @@ -75,7 +73,6 @@ GEM dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) - erubi (1.13.1) excon (1.4.0) logger falcon (0.55.2) @@ -112,7 +109,6 @@ GEM json-schema (>= 4.1) metrics (0.15.0) multi_json (1.19.1) - netrc (0.11.0) openssl (4.0.1) opentelemetry-api (1.8.0) logger @@ -154,16 +150,7 @@ GEM rake-compiler-dock (1.11.0) rb_sys (0.9.124) rake-compiler-dock (= 1.11.0) - rbi (0.3.9) - prism (~> 1.0) - rbs (>= 3.4.4) - rbs (4.0.0) - logger - prism (>= 1.6.0) - tsort regexp_parser (2.11.3) - require-hooks (0.2.3) - rexml (3.4.4) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -192,54 +179,17 @@ GEM rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) - rubocop-sorbet (0.12.0) - lint_roller - rubocop (>= 1.75.2) ruby-progressbar (1.13.0) samovar (2.4.1) console (~> 1.0) mapping (~> 1.0) - sorbet (0.6.13043) - sorbet-static (= 0.6.13043) - sorbet-runtime (0.6.13043) - sorbet-static (0.6.13043-universal-darwin) - sorbet-static (0.6.13043-x86_64-linux) - sorbet-static-and-runtime (0.6.13043) - sorbet (= 0.6.13043) - sorbet-runtime (= 0.6.13043) - spoom (1.7.11) - erubi (>= 1.10.0) - prism (>= 0.28.0) - rbi (>= 0.3.3) - rbs (>= 4.0.0.dev.4) - rexml (>= 3.2.6) - sorbet-static-and-runtime (>= 0.5.10187) - thor (>= 0.19.2) string-format (0.2.0) - tapioca (0.18.0) - benchmark - bundler (>= 2.2.25) - netrc (>= 0.11.0) - parallel (>= 1.21.0) - rbi (>= 0.3.7) - require-hooks (>= 0.2.2) - sorbet-static-and-runtime (>= 0.5.11087) - spoom (>= 1.7.9) - thor (>= 1.2.0) - tsort - yard-sorbet testcontainers-core (0.2.0) docker-api (~> 2.2) - thor (1.5.0) traces (0.18.2) - tsort (0.2.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) - yard (0.9.38) - yard-sorbet (0.9.0) - sorbet-runtime - yard zeitwerk (2.7.5) PLATFORMS @@ -258,9 +208,6 @@ DEPENDENCIES restate-sdk! rspec (~> 3.12) rubocop - rubocop-sorbet - sorbet - tapioca testcontainers-core BUNDLED WITH diff --git a/Makefile b/Makefile index 87bf0b7..2feeadb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build compile test test-harness test-integration clean fmt check install typecheck lint lint-fix verify tapioca +.PHONY: build compile test test-harness test-integration clean fmt check install lint lint-fix verify # Build the native extension and compile build: compile @@ -18,14 +18,6 @@ test-harness: compile test-integration: compile ./etc/run-integration-tests.sh -# Generate Tapioca DSL RBI files (typed handler signatures) -tapioca: compile - bundle exec tapioca dsl - -# Type checking -typecheck: - bundle exec srb tc - # Linting lint: bundle exec rubocop @@ -54,8 +46,8 @@ install: gem: compile gem build restate-sdk.gemspec -# Build, lint, typecheck, and run unit tests (no integration tests) -verify: compile tapioca lint typecheck test-harness +# Build, lint, and run unit tests (no integration tests) +verify: compile lint test-harness -# Run everything (install, compile, test, typecheck, lint) -all: install compile test typecheck lint +# Run everything (install, compile, test, lint) +all: install compile test lint diff --git a/README.md b/README.md index ee58732..5384504 100644 --- a/README.md +++ b/README.md @@ -58,23 +58,28 @@ Or add the gem to an existing project: gem install restate-sdk ``` -### Typed handlers with T::Struct +### Typed handlers with Dry::Struct -Use Sorbet's `T::Struct` for typed input/output with automatic JSON Schema generation: +Use [dry-struct](https://dry-rb.org/gems/dry-struct/) for typed input/output with automatic JSON Schema generation: ```ruby require 'restate' +require 'dry-struct' -class RegistrationRequest < T::Struct - const :event_name, String - const :attendee, String - const :num_guests, Integer - const :note, T.nilable(String) +module Types + include Dry.Types() end -class RegistrationResponse < T::Struct - const :registration_id, String - const :status, String +class RegistrationRequest < Dry::Struct + attribute :event_name, Types::String + attribute :attendee, Types::String + attribute :num_guests, Types::Integer + attribute? :note, Types::String # optional attribute +end + +class RegistrationResponse < Dry::Struct + attribute :registration_id, Types::String + attribute :status, Types::String end class EventService < Restate::Service diff --git a/docs/INTERNALS.md b/docs/INTERNALS.md index 40cefdb..d74ca69 100644 --- a/docs/INTERNALS.md +++ b/docs/INTERNALS.md @@ -47,7 +47,7 @@ lib/ ├── endpoint.rb Endpoint — holds services, builds Rack app ├── errors.rb TerminalError, SuspendedError, InternalError, DisconnectedError ├── handler.rb Handler, HandlerIO, ServiceTag structs + invoke_handler - ├── serde.rb JsonSerde, BytesSerde, TStructSerde, DryStructSerde, serde resolution + ├── serde.rb JsonSerde, BytesSerde, DryStructSerde, serde resolution ├── server.rb Rack 3 app — routes, I/O streaming, Async tasks ├── server_context.rb ctx object — state, sleep, run, calls, progress loop ├── client.rb HTTP client for external invocation (Restate::Client) @@ -58,12 +58,6 @@ lib/ ├── testing.rb Test harness (opt-in: require 'restate/testing') ├── vm.rb VMWrapper — Ruby bridge to native VM └── workflow.rb Workflow class + main/handler DSL + .call/.send! -lib/tapioca/dsl/compilers/ -└── restate.rb Tapioca DSL compiler — generates typed handler sigs - -rbi/ -└── restate-sdk.rbi Shipped RBI — Tapioca auto-discovers via `tapioca gems` - ext/restate_internal/ ├── Cargo.toml Depends on restate-sdk-shared-core 0.7.0, magnus 0.7 └── src/lib.rs Rust ↔ Ruby bindings (~1095 lines) @@ -87,8 +81,7 @@ examples/ Runnable examples showcasing SDK features ├── workflow.rb Promises, signals, workflow state ├── service_communication.rb Calls, sends, fan-out, wait_any, awakeables ├── service_configuration.rb Service-level config: timeouts, retention, retry policy -├── typed_handlers.rb Dry::Struct input/output, JSON Schema generation -└── typed_handlers_sorbet.rb T::Struct (Sorbet) input/output, JSON Schema generation +└── typed_handlers.rb Dry::Struct input/output, JSON Schema generation middleware_example/ Self-contained middleware example (own Gemfile) ├── config.ru OTel + tenant middleware wiring @@ -262,15 +255,11 @@ Useful for long-running handlers that need to flush work or perform cleanup befo - **`NOT_SET`** — frozen sentinel to distinguish "caller didn't pass serde" from `nil`. - **`Serde.resolve(type_or_serde)`** — resolves a type class or serde object into a serde with `serialize`/`deserialize`/`json_schema`. Priority: already a serde → use directly; - `T::Struct` subclass → `TStructSerde`; `Dry::Struct` subclass → `DryStructSerde`; + `Dry::Struct` subclass → `DryStructSerde`; primitive type → `TypeSerde` with schema; class with `.json_schema` → `TypeSerde`; fallback → `JsonSerde`. - **`TypeSerde`** — wraps a primitive type or custom-schema class. Delegates to `JsonSerde` for serialize/deserialize, adds `json_schema` from the type. -- **`TStructSerde`** — for `T::Struct` subclasses (Sorbet). Deserializes JSON via - `T::Struct.from_hash`, serializes via `T::Struct#serialize`. Generates JSON Schema by - introspecting Sorbet `T::Types` (handles `Simple`, `Union`/nilable, `TypedArray`, - `TypedHash`, nested structs). - **`DryStructSerde`** — for `Dry::Struct` subclasses. Deserializes JSON into struct instances via `Struct.new(**hash)`, serializes via `to_h` + `JSON.generate`. Generates JSON Schema by introspecting dry-types (handles Nominal, Sum/optional, Array::Member, Constrained, @@ -694,8 +683,7 @@ curl localhost:8080/Signup/user1/run -H 'content-type: application/json' -d '"us - **Falcon not responding to curl**: Falcon uses HTTP/2. Use `--http2-prior-knowledge` or go through the Restate ingress (port 8080) which handles protocol negotiation. -- **Worker crashes on startup**: Check Falcon logs (JSON to stdout). Common cause: Sorbet runtime - `NameError` from eager sig evaluation — ensure all types referenced in sigs are loaded. +- **Worker crashes on startup**: Check Falcon logs (JSON to stdout) for error details. - **Port stuck after crash**: `lsof -ti :9080 | xargs kill -9` - **Restate can't reach Falcon**: If Restate runs in Docker, bind Falcon to `0.0.0.0` not `localhost`, and use the host machine's IP in the registration URI. @@ -773,8 +761,3 @@ lsof -ti :9080 | xargs kill -9 Use `-n 1` flag for single worker during development. -### 9. Sorbet Eager Sig Evaluation - -Sorbet sigs are evaluated eagerly at class load time. If a sig references a class that hasn't -been `require`d yet, you get `NameError` at runtime (even though `srb tc` passes). Fix: use -`T.untyped` for return types of methods that lazy-load their dependencies. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 01ea360..af1c3c4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -686,50 +686,10 @@ The `input:` and `output:` options on handler declarations let you use typed str handler I/O. The SDK automatically deserializes input JSON into struct instances and generates JSON Schema for Restate's discovery protocol. -Two struct libraries are supported out of the box — pick whichever fits your project: - -### Using T::Struct (Sorbet) - -If you already use [Sorbet](https://sorbet.org/), `T::Struct` gives you full type safety -and IDE support with no extra dependencies. - -```ruby -require 'restate' - -class GreetingRequest < T::Struct - const :name, String - const :greeting, T.nilable(String) -end - -class Greeter < Restate::Service - handler :greet, input: GreetingRequest, output: String - def greet(request) - # request is a GreetingRequest instance, not a raw Hash - greeting = request.greeting || "Hello" - "#{greeting}, #{request.name}!" - end -end -``` - -The SDK introspects `T::Struct` props to generate JSON Schema. Serialization uses -`T::Struct#serialize` and `.from_hash`. - -Supported Sorbet type mappings: -| Sorbet type | JSON Schema | -|-------------|-------------| -| `String` | `{type: 'string'}` | -| `Integer` | `{type: 'integer'}` | -| `Float` | `{type: 'number'}` | -| `T::Boolean` | `{type: 'boolean'}` | -| `T.nilable(String)` | `{anyOf: [{type: 'string'}, {type: 'null'}]}` | -| `T::Array[String]` | `{type: 'array', items: {type: 'string'}}` | -| `T::Hash[String, Integer]` | `{type: 'object'}` | -| Nested `T::Struct` | Recursive object schema | - ### Using Dry::Struct [dry-struct](https://dry-rb.org/gems/dry-struct/) is a popular typed struct library that -works without Sorbet. Add it as an optional dependency: +Add it as an optional dependency: ```ruby gem 'dry-struct' @@ -771,7 +731,7 @@ Supported dry-types mappings: ### How It Works -Both struct types are auto-detected at runtime — no configuration needed. When a handler +`Dry::Struct` types are auto-detected at runtime — no configuration needed. When a handler declares `input: MyRequest`: - Input JSON is deserialized into a struct instance (not a raw Hash) - JSON Schema is generated from the struct definition and published via Restate discovery @@ -793,11 +753,10 @@ and use standard JSON serialization. When `input:` or `output:` is provided, the SDK resolves a serde in this order: 1. **Serde object** — if it responds to `serialize` and `deserialize`, use it directly -2. **T::Struct subclass** — use `TStructSerde` (Sorbet native) -3. **Dry::Struct subclass** — use `DryStructSerde` -4. **Primitive type** (`String`, `Integer`, etc.) — use `JsonSerde` with type schema -5. **Class with `.json_schema`** — use `JsonSerde` with that schema -6. **Fallback** — `JsonSerde` with no schema +2. **Dry::Struct subclass** — use `DryStructSerde` +3. **Primitive type** (`String`, `Integer`, etc.) — use `JsonSerde` with type schema +4. **Class with `.json_schema`** — use `JsonSerde` with that schema +5. **Fallback** — `JsonSerde` with no schema --- @@ -891,60 +850,6 @@ go-to-definition for all Restate types — no extra setup needed. Since all Restate operations are called as `Restate.*` module methods, code completion works automatically without any YARD annotations. -### Sorbet + Tapioca (Optional) - -For full static type checking, the SDK ships RBI files inside the gem and a -[Tapioca](https://github.com/Shopify/tapioca) DSL compiler that generates typed handler -signatures. - -**1. Add Sorbet and Tapioca to your Gemfile:** - -```ruby -group :development do - gem 'sorbet', require: false - gem 'tapioca', require: false -end -``` - -**2. Generate type information:** - -```bash -bundle install -bundle exec tapioca gems # Generate RBI for all gems (one-time) -bundle exec tapioca dsl # Generate typed handler signatures -``` - -This creates RBI files under `sorbet/rbi/`. For example, given: - -```ruby -class Counter < Restate::VirtualObject - handler def add(addend) - old = Restate.get('count') || 0 - Restate.set('count', old + addend) - end - - shared def get - Restate.get('count') || 0 - end -end -``` - -Tapioca generates: - -```ruby -# sorbet/rbi/dsl/counter.rbi (auto-generated, do not edit) -class Counter - sig { params(input: T.untyped).returns(T.untyped) } - def add(input); end - - sig { returns(T.untyped) } - def get; end -end -``` - -Run `tapioca dsl` again whenever you add or rename handlers. Commit the generated -`sorbet/rbi/` files to version control so the whole team benefits. - --- ## HTTP Client @@ -1132,7 +1037,6 @@ The `examples/` directory contains runnable examples: | `workflow.rb` | Declarative state, promises, signals | | `service_communication.rb` | Fluent call API, fan-out/fan-in, `wait_any`, awakeables | | `typed_handlers.rb` | `input:`/`output:` with `Dry::Struct`, JSON Schema generation | -| `typed_handlers_sorbet.rb` | `input:`/`output:` with `T::Struct` (Sorbet), JSON Schema generation | | `service_configuration.rb` | Service-level config: timeouts, retention, retry policy, lazy state | | [`middleware_example/`](../middleware_example/) | Real OpenTelemetry tracing + tenant isolation middleware (self-contained) | diff --git a/etc/smoke-test.sh b/etc/smoke-test.sh index 4a370fe..b3fa930 100755 --- a/etc/smoke-test.sh +++ b/etc/smoke-test.sh @@ -90,9 +90,9 @@ puts "Harness started — ingress at #{harness.ingress_url}" # Test 1: Invoke the greeter print 'Test 1: Greeter returns greeting... ' -resp = post(harness.ingress_url, '/Greeter/greet', { 'name' => 'SmokeTest' }) +resp = post(harness.ingress_url, '/Greeter/greet', 'SmokeTest') body = JSON.parse(resp.body) -if resp.code == '200' && body['message'] == 'Hello, SmokeTest!' +if resp.code == '200' && body == 'Hello, SmokeTest!' puts "PASS (#{body})" passed += 1 else @@ -102,9 +102,9 @@ end # Test 2: Invoke with a different name (verifies durable execution) print 'Test 2: Greeter with different input... ' -resp = post(harness.ingress_url, '/Greeter/greet', { 'name' => 'Ruby' }) +resp = post(harness.ingress_url, '/Greeter/greet', 'Ruby') body = JSON.parse(resp.body) -if resp.code == '200' && body['message'] == 'Hello, Ruby!' +if resp.code == '200' && body == 'Hello, Ruby!' puts "PASS (#{body})" passed += 1 else @@ -116,8 +116,8 @@ end print 'Test 3: Restate::Client invocation... ' begin client = Restate::Client.new(ingress_url: harness.ingress_url) - result = client.service('Greeter').greet({ 'name' => 'Client' }) - if result['message'] == 'Hello, Client!' + result = client.service('Greeter').greet('Client') + if result == 'Hello, Client!' puts "PASS (#{result})" passed += 1 else diff --git a/examples/config.ru b/examples/config.ru index 8706a05..7c88fe6 100644 --- a/examples/config.ru +++ b/examples/config.ru @@ -1,4 +1,3 @@ -# typed: false # frozen_string_literal: true # Serves all example services on a single endpoint. @@ -15,7 +14,6 @@ require_relative 'virtual_objects' require_relative 'workflow' require_relative 'service_communication' require_relative 'typed_handlers' -require_relative 'typed_handlers_sorbet' require_relative 'service_configuration' endpoint = Restate.endpoint( @@ -25,7 +23,6 @@ endpoint = Restate.endpoint( UserSignup, Worker, FanOut, TicketService, - EventService, OrderProcessor ) diff --git a/examples/greeter.rb b/examples/greeter.rb index 02f5ea1..f927e3a 100644 --- a/examples/greeter.rb +++ b/examples/greeter.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true # diff --git a/examples/typed_handlers.rb b/examples/typed_handlers.rb index 0f6f880..70e9ceb 100644 --- a/examples/typed_handlers.rb +++ b/examples/typed_handlers.rb @@ -1,4 +1,3 @@ -# typed: ignore # rubocop:disable Sorbet/FalseSigil # frozen_string_literal: true # diff --git a/examples/typed_handlers_sorbet.rb b/examples/typed_handlers_sorbet.rb deleted file mode 100644 index ac5385e..0000000 --- a/examples/typed_handlers_sorbet.rb +++ /dev/null @@ -1,86 +0,0 @@ -# typed: true -# frozen_string_literal: true - -# -# Example: Typed Handlers with T::Struct (Sorbet) -# -# Shows how to use Sorbet's T::Struct for typed input/output with automatic -# JSON Schema generation. This is the Sorbet-native alternative to dry-struct, -# giving you full IDE support and type checking. -# -# Features: -# - input: / output: — declare typed handler I/O -# - T::Struct — auto-detected, generates JSON Schema -# - T.nilable — optional fields -# - Primitive types — String, Integer, etc. also generate schema -# -# Try it: -# curl localhost:8080/EventService/register \ -# -H 'content-type: application/json' \ -# -d '{"event_name": "restate-conf", "attendee": "Alice", "num_guests": 2}' -# -# curl localhost:8080/EventService/register \ -# -H 'content-type: application/json' \ -# -d '{"event_name": "restate-conf", "attendee": "Bob", "num_guests": 1, "note": "vegetarian"}' - -require 'restate' - -# ────────────────────────────────────────────── -# Typed request/response structs -# ────────────────────────────────────────────── - -class RegistrationRequest < T::Struct - const :event_name, String - const :attendee, String - const :num_guests, Integer - const :note, T.nilable(String) -end - -class RegistrationResponse < T::Struct - const :registration_id, String - const :event_name, String - const :attendee, String - const :num_guests, Integer - const :status, String -end - -# ────────────────────────────────────────────── -# Service with typed handlers -# ────────────────────────────────────────────── - -class EventService < Restate::Service - # input: and output: accept type classes — the SDK auto-resolves - # serde and JSON Schema from T::Struct definitions. - handler :register, input: RegistrationRequest, output: RegistrationResponse - # @param request [RegistrationRequest] - # @return [RegistrationResponse] - def register(request) - # request is a RegistrationRequest instance, not a raw Hash - registration_id = Restate.run_sync('create-registration') do - "reg_#{request.event_name}_#{rand(10_000)}" - end - - note = request.note || 'none' - - Restate.run_sync('confirm-seats') do - puts "Confirming #{request.num_guests} seats for #{request.attendee} at #{request.event_name} (note: #{note})" - end - - # Return a RegistrationResponse — serialized to JSON automatically - RegistrationResponse.new( - registration_id: registration_id, - event_name: request.event_name, - attendee: request.attendee, - num_guests: request.num_guests, - status: 'confirmed' - ) - end - - # Primitive types also generate JSON Schema for discovery - handler :lookup, input: String, output: String - # @param registration_id [String] - # @return [String] - def lookup(registration_id) - "status for #{registration_id}: confirmed" - end -end diff --git a/lib/restate.rb b/lib/restate.rb index c2d611e..a563fee 100644 --- a/lib/restate.rb +++ b/lib/restate.rb @@ -1,7 +1,5 @@ -# typed: true # frozen_string_literal: true -require 'sorbet-runtime' require_relative 'restate/version' require_relative 'restate/errors' require_relative 'restate/serde' @@ -31,8 +29,6 @@ # because +Thread.current[]+ is NOT inherited by child fibers, which prevents # accidental context leaks when Async spawns child tasks for run blocks. module Restate # rubocop:disable Metrics/ModuleLength - extend T::Sig - module_function # Create an endpoint, optionally binding services. @@ -40,13 +36,6 @@ module Restate # rubocop:disable Metrics/ModuleLength # # @param services [Array] service classes or instances to bind # @return [Endpoint] - sig do - params( - services: T.untyped, - protocol: T.nilable(String), - identity_keys: T.nilable(T::Array[String]) - ).returns(Endpoint) - end def endpoint(*services, protocol: nil, identity_keys: nil) ep = Endpoint.new ep.streaming_protocol if protocol == 'bidi' @@ -65,15 +54,13 @@ def endpoint(*services, protocol: nil, identity_keys: nil) # c.ingress_url = "http://localhost:8080" # c.admin_url = "http://localhost:9070" # end - sig { params(_block: T.proc.params(arg0: Config).void).void } - def configure(&_block) + def configure(&) yield config end # Returns the global configuration. Creates a default one on first access. - sig { returns(Config) } def config - @config = T.let(@config, T.nilable(Config)) unless defined?(@config) + @config = nil unless defined?(@config) @config ||= Config.new end @@ -84,7 +71,6 @@ def config # Restate.client.service(Greeter).greet("World") # Restate.client.resolve_awakeable(id, payload) # Restate.client.create_deployment("http://localhost:9080") - sig { returns(Client) } def client cfg = config Client.new(ingress_url: cfg.ingress_url, admin_url: cfg.admin_url, @@ -94,9 +80,6 @@ def client # ── Context accessor (internal) ── # @!visibility private - sig do - params(service_kind: T.nilable(String), handler_kind: T.nilable(String)).returns(ServerContext) - end def fetch_context!(service_kind: nil, handler_kind: nil) # rubocop:disable Metrics ctx = Thread.current[:restate_context] unless ctx @@ -118,32 +101,23 @@ def fetch_context!(service_kind: nil, handler_kind: nil) # rubocop:disable Metri end end - T.cast(ctx, ServerContext) + ctx end # ── Durable execution ── # Execute a durable side effect. The block runs at most once; the result # is journaled and replayed on retries. Returns a DurableFuture. - sig do - params(name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped)).returns(DurableFuture) - end def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action) fetch_context!.run(name, serde: serde, retry_policy: retry_policy, background: background, &action) end # Convenience shortcut for +run(...).await+. Returns the result directly. - sig do - params(name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped)).returns(T.untyped) - end def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action) fetch_context!.run_sync(name, serde: serde, retry_policy: retry_policy, background: background, &action) end # Durable timer that survives handler restarts. - sig { params(seconds: Numeric).returns(DurableFuture) } def sleep(seconds) fetch_context!.sleep(seconds) end @@ -151,43 +125,36 @@ def sleep(seconds) # ── State operations (VirtualObject / Workflow) ── # Durably retrieve a state entry. Returns nil if unset. - sig { params(name: String, serde: T.untyped).returns(T.untyped) } def get(name, serde: JsonSerde) fetch_context!.get(name, serde: serde) end # Durably retrieve a state entry, returning a DurableFuture instead of blocking. - sig { params(name: String, serde: T.untyped).returns(DurableFuture) } def get_async(name, serde: JsonSerde) fetch_context!.get_async(name, serde: serde) end # Durably set a state entry. - sig { params(name: String, value: T.untyped, serde: T.untyped).void } def set(name, value, serde: JsonSerde) fetch_context!.set(name, value, serde: serde) end # Durably remove a single state entry. - sig { params(name: String).void } def clear(name) fetch_context!.clear(name) end # Durably remove all state entries. - sig { void } def clear_all fetch_context!.clear_all end # List all state entry names. - sig { returns(T.untyped) } def state_keys fetch_context!.state_keys end # List all state entry names, returning a DurableFuture. - sig { returns(DurableFuture) } def state_keys_async fetch_context!.state_keys_async end @@ -195,12 +162,6 @@ def state_keys_async # ── Service communication ── # Durably call a handler on a Restate service. - sig do - params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture) - end def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) ctx = fetch_context! @@ -209,12 +170,6 @@ def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: end # Fire-and-forget send to a Restate service handler. - sig do - params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped).returns(SendHandle) - end def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) ctx = fetch_context! @@ -223,12 +178,6 @@ def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: n end # Durably call a handler on a Restate virtual object. - sig do - params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture) - end def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) ctx = fetch_context! @@ -237,12 +186,6 @@ def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, end # Fire-and-forget send to a Restate virtual object handler. - sig do - params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped).returns(SendHandle) - end def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) ctx = fetch_context! @@ -251,12 +194,6 @@ def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, end # Durably call a handler on a Restate workflow. - sig do - params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture) - end def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) ctx = fetch_context! @@ -266,12 +203,6 @@ def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil end # Fire-and-forget send to a Restate workflow handler. - sig do - params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped).returns(SendHandle) - end def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) ctx = fetch_context! @@ -280,22 +211,12 @@ def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, end # Durably call a handler using raw bytes (no serialization). - sig do - params(service: String, handler: String, arg: String, - key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String])).returns(DurableCallFuture) - end def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil) fetch_context!.generic_call(service, handler, arg, key: key, idempotency_key: idempotency_key, headers: headers) end # Fire-and-forget send using raw bytes (no serialization). - sig do - params(service: String, handler: String, arg: String, - key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String])).returns(SendHandle) - end def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil) fetch_context!.generic_send(service, handler, arg, key: key, delay: delay, idempotency_key: idempotency_key, headers: headers) @@ -304,19 +225,16 @@ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: n # ── Awakeables ── # Create an awakeable for external callbacks. Returns [awakeable_id, DurableFuture]. - sig { params(serde: T.untyped).returns([String, DurableFuture]) } def awakeable(serde: JsonSerde) fetch_context!.awakeable(serde: serde) end # Resolve an awakeable with a success value. - sig { params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void } def resolve_awakeable(awakeable_id, payload, serde: JsonSerde) fetch_context!.resolve_awakeable(awakeable_id, payload, serde: serde) end # Reject an awakeable with a terminal failure. - sig { params(awakeable_id: String, message: String, code: Integer).void } def reject_awakeable(awakeable_id, message, code: 500) fetch_context!.reject_awakeable(awakeable_id, message, code: code) end @@ -324,25 +242,21 @@ def reject_awakeable(awakeable_id, message, code: 500) # ── Promises (Workflow only) ── # Get a durable promise value, blocking until resolved. - sig { params(name: String, serde: T.untyped).returns(T.untyped) } def promise(name, serde: JsonSerde) fetch_context!.promise(name, serde: serde) end # Peek at a durable promise without blocking. Returns nil if not yet resolved. - sig { params(name: String, serde: T.untyped).returns(T.untyped) } def peek_promise(name, serde: JsonSerde) fetch_context!.peek_promise(name, serde: serde) end # Resolve a durable promise with a value. - sig { params(name: String, payload: T.untyped, serde: T.untyped).void } def resolve_promise(name, payload, serde: JsonSerde) fetch_context!.resolve_promise(name, payload, serde: serde) end # Reject a durable promise with a terminal failure. - sig { params(name: String, message: String, code: Integer).void } def reject_promise(name, message, code: 500) fetch_context!.reject_promise(name, message, code: code) end @@ -350,21 +264,18 @@ def reject_promise(name, message, code: 500) # ── Futures ── # Wait until any of the given futures completes. Returns [completed, remaining]. - sig { params(futures: T::Array[DurableFuture]).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) } def wait_any(*futures) - T.unsafe(fetch_context!).wait_any(*futures) + fetch_context!.wait_any(*futures) end # ── Request metadata ── # Returns metadata about the current invocation (id, headers, raw body). - sig { returns(T.untyped) } def request fetch_context!.request end # Returns the key for this virtual object or workflow invocation. - sig { returns(String) } def key fetch_context!.key end @@ -372,7 +283,6 @@ def key # ── Invocation control ── # Request cancellation of another invocation. - sig { params(invocation_id: String).void } def cancel_invocation(invocation_id) fetch_context!.cancel_invocation(invocation_id) end diff --git a/lib/restate/client.rb b/lib/restate/client.rb index c0fef20..c71c439 100644 --- a/lib/restate/client.rb +++ b/lib/restate/client.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true require 'net/http' @@ -31,13 +30,6 @@ module Restate # client.cancel_invocation(invocation_id) # client.create_deployment("http://localhost:9080") class Client - extend T::Sig - - sig do - params(ingress_url: String, admin_url: String, - ingress_headers: T::Hash[String, String], - admin_headers: T::Hash[String, String]).void - end def initialize(ingress_url: 'http://localhost:8080', admin_url: 'http://localhost:9070', ingress_headers: {}, admin_headers: {}) @ingress_url = ingress_url.chomp('/') @@ -49,19 +41,16 @@ def initialize(ingress_url: 'http://localhost:8080', admin_url: 'http://localhos # ── Service invocation proxies ── # Returns a proxy for calling a stateless service. - sig { params(service: T.any(String, T::Class[T.anything])).returns(ClientServiceProxy) } def service(service) ClientServiceProxy.new(@ingress_url, resolve_name(service), nil, @ingress_headers) end # Returns a proxy for calling a keyed virtual object. - sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) } def object(service, key) ClientServiceProxy.new(@ingress_url, resolve_name(service), key, @ingress_headers) end # Returns a proxy for calling a workflow. - sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) } def workflow(service, key) ClientServiceProxy.new(@ingress_url, resolve_name(service), key, @ingress_headers) end @@ -69,13 +58,11 @@ def workflow(service, key) # ── Awakeable operations ── # Resolve an awakeable from outside the Restate runtime. - sig { params(awakeable_id: String, payload: T.untyped).void } def resolve_awakeable(awakeable_id, payload) post_ingress("/restate/awakeables/#{awakeable_id}/resolve", payload) end # Reject an awakeable from outside the Restate runtime. - sig { params(awakeable_id: String, message: String, code: Integer).void } def reject_awakeable(awakeable_id, message, code: 500) post_ingress("/restate/awakeables/#{awakeable_id}/reject", { 'message' => message, 'code' => code }) @@ -84,29 +71,25 @@ def reject_awakeable(awakeable_id, message, code: 500) # ── Invocation management ── # Cancel a running invocation. - sig { params(invocation_id: String).void } def cancel_invocation(invocation_id) post_admin("/restate/invocations/#{invocation_id}/cancel", nil) end # Kill a running invocation (immediate termination, no cleanup). - sig { params(invocation_id: String).void } def kill_invocation(invocation_id) post_admin("/restate/invocations/#{invocation_id}/kill", nil) end private - sig { params(service: T.any(String, T::Class[T.anything])).returns(String) } def resolve_name(service) if service.is_a?(Class) && service.respond_to?(:service_name) - T.unsafe(service).service_name + service.service_name else service.to_s end end - sig { params(path: String, body: T.untyped).returns(T.untyped) } def post_ingress(path, body) # rubocop:disable Metrics/AbcSize uri = URI("#{@ingress_url}#{path}") request = Net::HTTP::Post.new(uri) @@ -120,7 +103,6 @@ def post_ingress(path, body) # rubocop:disable Metrics/AbcSize parse_response(response) end - sig { params(path: String, body: T.untyped).returns(T.untyped) } def post_admin(path, body) # rubocop:disable Metrics/AbcSize uri = URI("#{@admin_url}#{path}") request = Net::HTTP::Post.new(uri) @@ -134,7 +116,6 @@ def post_admin(path, body) # rubocop:disable Metrics/AbcSize parse_response(response) end - sig { params(response: Net::HTTPResponse).returns(T.untyped) } def parse_response(response) body = response.body body && !body.empty? ? JSON.parse(body) : nil @@ -146,12 +127,6 @@ def parse_response(response) # # @!visibility private class ClientServiceProxy - extend T::Sig - - sig do - params(base_url: String, service_name: String, key: T.nilable(String), - headers: T::Hash[String, String]).void - end def initialize(base_url, service_name, key, headers) @base_url = base_url @service_name = service_name diff --git a/lib/restate/config.rb b/lib/restate/config.rb index c9fe842..74369cb 100644 --- a/lib/restate/config.rb +++ b/lib/restate/config.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -13,30 +12,23 @@ module Restate # # Then use the pre-configured client: # Restate.client.service(Greeter).greet("World") class Config - extend T::Sig - # Restate ingress URL (for invoking services). - sig { returns(String) } attr_accessor :ingress_url # Restate admin URL (for deployments, invocation management). - sig { returns(String) } attr_accessor :admin_url # Default headers sent with every ingress request. - sig { returns(T::Hash[String, String]) } attr_accessor :ingress_headers # Default headers sent with every admin request. - sig { returns(T::Hash[String, String]) } attr_accessor :admin_headers - sig { void } def initialize - @ingress_url = T.let('http://localhost:8080', String) - @admin_url = T.let('http://localhost:9070', String) - @ingress_headers = T.let({}, T::Hash[String, String]) - @admin_headers = T.let({}, T::Hash[String, String]) + @ingress_url = 'http://localhost:8080' + @admin_url = 'http://localhost:9070' + @ingress_headers = {} + @admin_headers = {} end end end diff --git a/lib/restate/context.rb b/lib/restate/context.rb index ae63b15..dd254b4 100644 --- a/lib/restate/context.rb +++ b/lib/restate/context.rb @@ -1,7 +1,6 @@ -# typed: true # frozen_string_literal: true -# rubocop:disable Metrics/ModuleLength,Metrics/ParameterLists,Style/EmptyMethod +# rubocop:disable Style/EmptyMethod module Restate # Signals when the current invocation attempt has finished — either the handler # completed, the connection was lost, or a transient error occurred. @@ -17,27 +16,22 @@ module Restate # # poll event.set? periodically, or pass it to your HTTP client # end class AttemptFinishedEvent - extend T::Sig - - sig { void } def initialize - @mutex = T.let(Mutex.new, Mutex) - @set = T.let(false, T::Boolean) - @waiters = T.let([], T::Array[Thread::Queue]) + @mutex = Mutex.new + @set = false + @waiters = [] end # Returns true if the attempt has finished. - sig { returns(T::Boolean) } def set? @set end # Blocks the current fiber/thread until the attempt finishes. - sig { void } def wait return if @set - waiter = T.let(nil, T.nilable(Thread::Queue)) + waiter = nil @mutex.synchronize do unless @set waiter = Thread::Queue.new @@ -49,7 +43,6 @@ def wait # Marks the event as set and wakes all waiters. # Called internally by the SDK when the attempt ends. - sig { void } def set! @mutex.synchronize do @set = true @@ -79,258 +72,149 @@ def set! # @see ObjectContext for VirtualObject handlers (adds state operations) # @see WorkflowContext for Workflow handlers (adds promise operations) module Context - extend T::Sig - extend T::Helpers - - abstract! - # Execute a durable side effect. The block runs at most once; the result # is journaled and replayed on retries. # # Pass +background: true+ to offload the block to a real OS Thread, # keeping the fiber event loop responsive for CPU-intensive work. - sig do - abstract.params( - name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped) - ).returns(DurableFuture) - end def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action); end # Convenience shortcut for +run(...).await+. Returns the result directly. # Accepts all the same options as +run+, including +background: true+. - sig do - abstract.params( - name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped) - ).returns(T.untyped) - end def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action); end # Durable timer that survives handler restarts. - sig { params(seconds: Numeric).returns(DurableFuture) } def sleep(seconds) # rubocop:disable Lint/UnusedMethodArgument Kernel.raise NotImplementedError end # Durably call a handler on a Restate service. - sig do - abstract.params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) end + # Fire-and-forget send to a Restate service handler. - sig do - abstract.params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) end + # Durably call a handler on a Restate virtual object. - sig do - abstract.params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) end + # Fire-and-forget send to a Restate virtual object handler. - sig do - abstract.params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) end + # Durably call a handler on a Restate workflow. - sig do - abstract.params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) end + # Fire-and-forget send to a Restate workflow handler. - sig do - abstract.params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) end + # Durably call a handler using raw bytes (no serialization). - sig do - abstract.params( - service: String, handler: String, arg: String, - key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(DurableCallFuture) - end def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil); end # Fire-and-forget send using raw bytes (no serialization). - sig do - abstract.params( - service: String, handler: String, arg: String, - key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]) - ).returns(SendHandle) - end def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil); end # Create an awakeable for external callbacks. # Returns [awakeable_id, DurableFuture]. - sig { abstract.params(serde: T.untyped).returns([String, DurableFuture]) } def awakeable(serde: JsonSerde); end # Resolve an awakeable with a success value. - sig { abstract.params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void } def resolve_awakeable(awakeable_id, payload, serde: JsonSerde); end # Reject an awakeable with a terminal failure. - sig { abstract.params(awakeable_id: String, message: String, code: Integer).void } def reject_awakeable(awakeable_id, message, code: 500); end # Request cancellation of another invocation. - sig { abstract.params(invocation_id: String).void } def cancel_invocation(invocation_id); end # Wait until any of the given futures completes. # Returns [completed, remaining]. - sig { abstract.params(futures: DurableFuture).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) } def wait_any(*futures); end # Returns metadata about the current invocation. - sig { abstract.returns(Request) } def request; end # Returns the key for this virtual object or workflow invocation. - sig { abstract.returns(String) } def key; end end # Context interface for VirtualObject shared handlers (read-only state). # Extends {Context} with +get+, +state_keys+, and +key+ — but no mutations. module ObjectSharedContext - extend T::Sig - extend T::Helpers - - abstract! include Context # Durably retrieve a state entry. Returns nil if unset. - sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) } def get(name, serde: JsonSerde); end # Durably retrieve a state entry, returning a DurableFuture instead of blocking. - sig { abstract.params(name: String, serde: T.untyped).returns(DurableFuture) } def get_async(name, serde: JsonSerde); end # List all state entry names. - sig { abstract.returns(T.untyped) } def state_keys; end # List all state entry names, returning a DurableFuture instead of blocking. - sig { abstract.returns(DurableFuture) } def state_keys_async; end end # Context interface for VirtualObject exclusive handlers (full state access). # Extends {ObjectSharedContext} with mutating state operations. module ObjectContext - extend T::Sig - extend T::Helpers - - abstract! include ObjectSharedContext # Durably set a state entry. - sig { abstract.params(name: String, value: T.untyped, serde: T.untyped).void } def set(name, value, serde: JsonSerde); end # Durably remove a single state entry. - sig { abstract.params(name: String).void } def clear(name); end # Durably remove all state entries. - sig { abstract.void } def clear_all; end end # Context interface for Workflow shared handlers (read-only state + promises). # Extends {ObjectSharedContext} with durable promise operations. module WorkflowSharedContext - extend T::Sig - extend T::Helpers - - abstract! include ObjectSharedContext # Get a durable promise value, blocking until resolved. - sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) } def promise(name, serde: JsonSerde); end # Peek at a durable promise without blocking. Returns nil if not yet resolved. - sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) } def peek_promise(name, serde: JsonSerde); end # Resolve a durable promise with a value. - sig { abstract.params(name: String, payload: T.untyped, serde: T.untyped).void } def resolve_promise(name, payload, serde: JsonSerde); end # Reject a durable promise with a terminal failure. - sig { abstract.params(name: String, message: String, code: Integer).void } def reject_promise(name, message, code: 500); end end # Context interface for Workflow main handler (full state + promises). # Extends {ObjectContext} with durable promise operations. module WorkflowContext - extend T::Sig - extend T::Helpers - - abstract! include ObjectContext # Get a durable promise value, blocking until resolved. - sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) } def promise(name, serde: JsonSerde); end # Peek at a durable promise without blocking. Returns nil if not yet resolved. - sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) } def peek_promise(name, serde: JsonSerde); end # Resolve a durable promise with a value. - sig { abstract.params(name: String, payload: T.untyped, serde: T.untyped).void } def resolve_promise(name, payload, serde: JsonSerde); end # Reject a durable promise with a terminal failure. - sig { abstract.params(name: String, message: String, code: Integer).void } def reject_promise(name, message, code: 500); end end end -# rubocop:enable Metrics/ModuleLength,Metrics/ParameterLists,Style/EmptyMethod +# rubocop:enable Style/EmptyMethod diff --git a/lib/restate/discovery.rb b/lib/restate/discovery.rb index cca24fa..3582653 100644 --- a/lib/restate/discovery.rb +++ b/lib/restate/discovery.rb @@ -1,40 +1,35 @@ -# typed: true # frozen_string_literal: true require 'json' module Restate module Discovery # rubocop:disable Metrics/ModuleLength - extend T::Sig - - PROTOCOL_MODES = T.let({ + PROTOCOL_MODES = { 'bidi' => 'BIDI_STREAM', 'request_response' => 'REQUEST_RESPONSE' - }.freeze, T::Hash[String, String]) + }.freeze - SERVICE_TYPES = T.let({ + SERVICE_TYPES = { 'service' => 'SERVICE', 'object' => 'VIRTUAL_OBJECT', 'workflow' => 'WORKFLOW' - }.freeze, T::Hash[String, String]) + }.freeze - HANDLER_TYPES = T.let({ + HANDLER_TYPES = { 'exclusive' => 'EXCLUSIVE', 'shared' => 'SHARED', 'workflow' => 'WORKFLOW' - }.freeze, T::Hash[String, String]) + }.freeze module_function # Generate the discovery JSON for the given endpoint. - sig { params(endpoint: Endpoint, _version: Integer, discovered_as: String).returns(String) } def compute_discovery_json(endpoint, _version, discovered_as) ep = compute_discovery(endpoint, discovered_as) JSON.generate(ep, allow_nan: false) end # Build the discovery hash for the endpoint. - sig { params(endpoint: Endpoint, discovered_as: String).returns(T::Hash[Symbol, T.untyped]) } def compute_discovery(endpoint, discovered_as) services = endpoint.services.values.map do |service| build_service(service) @@ -50,7 +45,6 @@ def compute_discovery(endpoint, discovered_as) ) end - sig { params(service: T.untyped).returns(T::Hash[Symbol, T.untyped]) } def build_service(service) # rubocop:disable Metrics/AbcSize service_type = SERVICE_TYPES.fetch(service.service_tag.kind) @@ -80,7 +74,6 @@ def build_service(service) # rubocop:disable Metrics/AbcSize result end - sig { params(handler: T.untyped).returns(T::Hash[Symbol, T.untyped]) } def build_handler(handler) # rubocop:disable Metrics/AbcSize ty = handler.kind ? HANDLER_TYPES.fetch(handler.kind) : nil @@ -118,7 +111,6 @@ def build_handler(handler) # rubocop:disable Metrics/AbcSize end # Convert seconds to milliseconds (integer). Returns nil if input is nil. - sig { params(seconds: T.nilable(Numeric)).returns(T.nilable(Integer)) } def seconds_to_ms(seconds) return nil if seconds.nil? @@ -126,7 +118,6 @@ def seconds_to_ms(seconds) end # Merge retry policy fields (flattened) into the target hash. - sig { params(target: T::Hash[Symbol, T.untyped], policy: T.nilable(T::Hash[Symbol, T.untyped])).void } def merge_retry_policy!(target, policy) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity return if policy.nil? || policy.empty? @@ -138,7 +129,6 @@ def merge_retry_policy!(target, policy) # rubocop:disable Metrics/AbcSize,Metric end # Remove nil values from a hash (non-recursive for top level, recursive for nested). - sig { params(kwargs: T.untyped).returns(T::Hash[Symbol, T.untyped]) } def compact(**kwargs) kwargs.each_with_object({}) do |(k, v), result| next if v.nil? diff --git a/lib/restate/durable_future.rb b/lib/restate/durable_future.rb index 9b055e8..c1d0909 100644 --- a/lib/restate/durable_future.rb +++ b/lib/restate/durable_future.rb @@ -1,28 +1,22 @@ -# typed: true # frozen_string_literal: true module Restate # A durable future wrapping a VM handle. Lazily resolves on first +await+ and caches the result. # Returned by +ctx.run+ and +ctx.sleep+. class DurableFuture - extend T::Sig - - sig { returns(Integer) } attr_reader :handle - sig { params(ctx: ServerContext, handle: Integer, serde: T.untyped).void } def initialize(ctx, handle, serde: nil) - @ctx = T.let(ctx, ServerContext) - @handle = T.let(handle, Integer) - @serde = T.let(serde, T.untyped) - @resolved = T.let(false, T::Boolean) - @value = T.let(nil, T.untyped) + @ctx = ctx + @handle = handle + @serde = serde + @resolved = false + @value = nil end # Block until the result is available and return it. Caches across calls. # # @return [Object] the deserialized result - sig { returns(T.untyped) } def await unless @resolved raw = @ctx.resolve_handle(@handle) @@ -35,7 +29,6 @@ def await # Check whether the future has completed (non-blocking). # # @return [Boolean] - sig { returns(T::Boolean) } def completed? @resolved || @ctx.completed?(@handle) end @@ -45,26 +38,15 @@ def completed? # Adds +invocation_id+ and +cancel+ on top of DurableFuture. # Returned by +ctx.service_call+, +ctx.object_call+, +ctx.workflow_call+. class DurableCallFuture < DurableFuture - extend T::Sig - - sig do - params( - ctx: ServerContext, - result_handle: Integer, - invocation_id_handle: Integer, - output_serde: T.untyped - ).void - end def initialize(ctx, result_handle, invocation_id_handle, output_serde:) super(ctx, result_handle) - @invocation_id_handle = T.let(invocation_id_handle, Integer) - @output_serde = T.let(output_serde, T.untyped) - @invocation_id_resolved = T.let(false, T::Boolean) - @invocation_id_value = T.let(nil, T.untyped) + @invocation_id_handle = invocation_id_handle + @output_serde = output_serde + @invocation_id_resolved = false + @invocation_id_value = nil end # Block until the result is available and return it. Deserializes via +output_serde+. - sig { returns(T.untyped) } def await unless @resolved raw = @ctx.resolve_handle(@handle) @@ -81,17 +63,15 @@ def await # Returns the invocation ID of the remote call. Lazily resolved. # # @return [String] the invocation ID - sig { returns(String) } def invocation_id unless @invocation_id_resolved @invocation_id_value = @ctx.resolve_handle(@invocation_id_handle) @invocation_id_resolved = true end - T.must(@invocation_id_value) + @invocation_id_value end # Cancel the remote invocation. - sig { void } def cancel @ctx.cancel_invocation(invocation_id) end @@ -100,30 +80,25 @@ def cancel # A handle for fire-and-forget send operations. # Returned by +ctx.service_send+, +ctx.object_send+, +ctx.workflow_send+. class SendHandle - extend T::Sig - - sig { params(ctx: ServerContext, invocation_id_handle: Integer).void } def initialize(ctx, invocation_id_handle) - @ctx = T.let(ctx, ServerContext) - @invocation_id_handle = T.let(invocation_id_handle, Integer) - @invocation_id_resolved = T.let(false, T::Boolean) - @invocation_id_value = T.let(nil, T.untyped) + @ctx = ctx + @invocation_id_handle = invocation_id_handle + @invocation_id_resolved = false + @invocation_id_value = nil end # Returns the invocation ID of the sent call. Lazily resolved. # # @return [String] the invocation ID - sig { returns(String) } def invocation_id unless @invocation_id_resolved @invocation_id_value = @ctx.resolve_handle(@invocation_id_handle) @invocation_id_resolved = true end - T.must(@invocation_id_value) + @invocation_id_value end # Cancel the remote invocation. - sig { void } def cancel @ctx.cancel_invocation(invocation_id) end diff --git a/lib/restate/endpoint.rb b/lib/restate/endpoint.rb index 40153af..763e83e 100644 --- a/lib/restate/endpoint.rb +++ b/lib/restate/endpoint.rb @@ -1,29 +1,17 @@ -# typed: true # frozen_string_literal: true module Restate # Container for registered services. Bind services here, then create the Rack app. class Endpoint - extend T::Sig + attr_reader :services, :identity_keys, :middleware - sig { returns(T::Hash[String, T.untyped]) } - attr_reader :services - - sig { returns(T::Array[String]) } - attr_reader :identity_keys - - sig { returns(T.nilable(String)) } attr_accessor :protocol - sig { returns(T::Array[T.untyped]) } - attr_reader :middleware - - sig { void } def initialize - @services = T.let({}, T::Hash[String, T.untyped]) - @protocol = T.let(nil, T.nilable(String)) - @identity_keys = T.let([], T::Array[String]) - @middleware = T.let([], T::Array[T.untyped]) + @services = {} + @protocol = nil + @identity_keys = [] + @middleware = [] end # Bind one or more services to this endpoint. @@ -31,7 +19,6 @@ def initialize # @param svcs [Array, Class, Class>] services to bind # @return [self] # @raise [ArgumentError] if a service with the same name is already bound - sig { params(svcs: T.untyped).returns(T.self_type) } def bind(*svcs) svcs.each do |svc| svc_name = svc.service_name @@ -43,21 +30,18 @@ def bind(*svcs) end # Force bidirectional streaming protocol. - sig { returns(T.self_type) } def streaming_protocol @protocol = 'bidi' self end # Force request/response protocol. - sig { returns(T.self_type) } def request_response_protocol @protocol = 'request_response' self end # Add an identity key for request verification. - sig { params(key: String).returns(T.self_type) } def identity_key(key) @identity_keys << key self @@ -116,7 +100,6 @@ def identity_key(key) # @param args [Array] positional arguments for the middleware constructor # @param kwargs [Hash] keyword arguments for the middleware constructor # @return [self] - sig { params(klass: T.untyped, args: T.untyped, kwargs: T.untyped).returns(T.self_type) } def use(klass, *args, **kwargs) instance = if kwargs.empty? klass.new(*args) @@ -128,7 +111,6 @@ def use(klass, *args, **kwargs) end # Build and return the Rack-compatible application. - sig { returns(T.untyped) } def app require_relative 'server' Server.new(self) diff --git a/lib/restate/errors.rb b/lib/restate/errors.rb index 144da59..19ecd3b 100644 --- a/lib/restate/errors.rb +++ b/lib/restate/errors.rb @@ -1,4 +1,3 @@ -# typed: strict # frozen_string_literal: true module Restate @@ -7,24 +6,17 @@ module Restate # @example # raise Restate::TerminalError.new('not found', status_code: 404) class TerminalError < StandardError - extend T::Sig - - sig { returns(Integer) } attr_reader :status_code - sig { params(message: String, status_code: Integer).void } def initialize(message = 'Internal Server Error', status_code: 500) super(message) - @status_code = T.let(status_code, Integer) + @status_code = status_code end end # Internal: raised when the VM suspends execution. # User code should NOT catch this. class SuspendedError < StandardError - extend T::Sig - - sig { void } def initialize super( "Invocation got suspended, Restate will resume this invocation when progress can be made.\n" \ @@ -37,9 +29,6 @@ def initialize # Internal: raised when the VM encounters a retryable error. class InternalError < StandardError - extend T::Sig - - sig { void } def initialize super( "Invocation attempt raised a retryable error.\n" \ @@ -50,9 +39,6 @@ def initialize # Internal: raised when the HTTP connection is lost. class DisconnectedError < StandardError - extend T::Sig - - sig { void } def initialize super('Disconnected. The connection to the restate server was lost. Restate will retry the attempt.') end diff --git a/lib/restate/handler.rb b/lib/restate/handler.rb index f850561..fefb72e 100644 --- a/lib/restate/handler.rb +++ b/lib/restate/handler.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -27,18 +26,12 @@ def initialize(accept: 'application/json', content_type: 'application/json', keyword_init: true ) - extend T::Sig - module_function # Invoke a handler with the context and raw input bytes. # The context is passed as the first argument to every handler. # Middleware (if any) wraps the handler call. # Returns raw output bytes. - sig do - params(handler: T.untyped, ctx: T.untyped, in_buffer: String, - middleware: T::Array[T.untyped]).returns(String) - end def invoke_handler(handler:, ctx:, in_buffer:, middleware: []) # rubocop:disable Metrics/AbcSize call_handler = Kernel.proc do if handler.arity == 1 diff --git a/lib/restate/serde.rb b/lib/restate/serde.rb index 8062318..75dcd92 100644 --- a/lib/restate/serde.rb +++ b/lib/restate/serde.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true require 'json' @@ -7,12 +6,9 @@ module Restate # JSON serializer/deserializer (default). # Converts Ruby objects to JSON byte strings and back. module JsonSerde - extend T::Sig - module_function # Serialize a Ruby object to a JSON byte string. Returns empty bytes for nil. - sig { params(obj: T.untyped).returns(String) } def serialize(obj) return ''.b if obj.nil? @@ -20,7 +16,6 @@ def serialize(obj) end # Deserialize a JSON byte string to a Ruby object. Returns nil for nil or empty input. - sig { params(buf: T.nilable(String)).returns(T.untyped) } def deserialize(buf) return nil if buf.nil? || buf.empty? @@ -28,7 +23,6 @@ def deserialize(buf) end # Return the JSON Schema for this serde, or nil if unspecified. - sig { returns(T.nilable(T::Hash[String, T.untyped])) } def json_schema nil end @@ -40,12 +34,9 @@ def json_schema # Pass-through bytes serializer/deserializer. # Passes binary data through without transformation. module BytesSerde - extend T::Sig - module_function # Serialize an object by returning its binary encoding. Returns empty bytes for nil. - sig { params(obj: T.untyped).returns(String) } def serialize(obj) return ''.b if obj.nil? @@ -53,13 +44,11 @@ def serialize(obj) end # Deserialize by returning the raw buffer unchanged. - sig { params(buf: T.nilable(String)).returns(T.nilable(String)) } def deserialize(buf) buf end # Return the JSON Schema for this serde, or nil if unspecified. - sig { returns(T.nilable(T::Hash[String, T.untyped])) } def json_schema nil end @@ -79,22 +68,17 @@ def json_schema # Serde resolution utilities: converts a type or serde into a serde object. module Serde - extend T::Sig - module_function # Check if an object quacks like a serde (has serialize + deserialize). - sig { params(obj: T.untyped).returns(T::Boolean) } def serde?(obj) obj.respond_to?(:serialize) && obj.respond_to?(:deserialize) end # Resolve a type or serde into a serde object with serialize/deserialize/json_schema. - sig { params(type_or_serde: T.untyped).returns(T.untyped) } def resolve(type_or_serde) return JsonSerde if type_or_serde.nil? return type_or_serde if serde?(type_or_serde) - return TStructSerde.new(type_or_serde) if t_struct?(type_or_serde) return DryStructSerde.new(type_or_serde) if dry_struct?(type_or_serde) return TypeSerde.new(type_or_serde, PRIMITIVE_SCHEMAS[type_or_serde]) if PRIMITIVE_SCHEMAS.key?(type_or_serde) return TypeSerde.new(type_or_serde, type_or_serde.json_schema) if type_or_serde.respond_to?(:json_schema) @@ -102,60 +86,12 @@ def resolve(type_or_serde) JsonSerde end - # Check if a value is a T::Struct subclass. - sig { params(val: T.untyped).returns(T::Boolean) } - def t_struct?(val) - !!(val.is_a?(Class) && val < T::Struct) - end - # Check if a value is a Dry::Struct subclass. - sig { params(val: T.untyped).returns(T.nilable(T::Boolean)) } def dry_struct?(val) defined?(::Dry::Struct) && val.is_a?(Class) && val < ::Dry::Struct end - # Generate a JSON Schema from a T::Struct class by introspecting its props. - sig { params(struct_class: T.class_of(T::Struct)).returns(T::Hash[String, T.untyped]) } - def t_struct_to_json_schema(struct_class) # rubocop:disable Metrics - properties = {} - required = [] - - T.unsafe(struct_class).props.each do |name, meta| - prop_name = (meta[:serialized_form] || name).to_s - properties[prop_name] = t_type_to_json_schema(meta[:type_object] || meta[:type]) - required << prop_name unless meta[:fully_optional] || meta[:_tnilable] - end - - schema = { 'type' => 'object', 'properties' => properties } - schema['required'] = required unless required.empty? - schema - end - - # Convert a Sorbet T::Types type object to a JSON Schema hash. - sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) } - def t_type_to_json_schema(type) # rubocop:disable Metrics - case type - when T::Types::Simple - PRIMITIVE_SCHEMAS[type.raw_type] || {} - when T::Types::Union - schemas = type.types.map { |t| t_type_to_json_schema(t) } - schemas.uniq! - schemas.length == 1 ? schemas.first : { 'anyOf' => schemas } - when T::Types::TypedArray - { 'type' => 'array', 'items' => t_type_to_json_schema(type.type) } - when T::Types::TypedHash - { 'type' => 'object' } - when Class - return t_struct_to_json_schema(type) if type < T::Struct - - PRIMITIVE_SCHEMAS[type] || {} - else - {} - end - end - # Generate a JSON Schema from a Dry::Struct class. - sig { params(struct_class: T.untyped).returns(T::Hash[String, T.untyped]) } def dry_struct_to_json_schema(struct_class) properties = {} required = [] @@ -172,7 +108,6 @@ def dry_struct_to_json_schema(struct_class) end # Convert a dry-types type to a JSON Schema hash. - sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) } def dry_type_to_json_schema(type) # rubocop:disable Metrics type_class = type.class.name || '' @@ -198,7 +133,6 @@ def dry_type_to_json_schema(type) # rubocop:disable Metrics end # Convert a nominal dry-type (with .primitive) to JSON Schema. - sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) } def nominal_to_json_schema(type) prim = type.primitive return dry_struct_to_json_schema(prim) if dry_struct?(prim) @@ -210,32 +144,25 @@ def nominal_to_json_schema(type) # Serde wrapper for primitive types and classes with a .json_schema method. # Delegates serialize/deserialize to JsonSerde, adds schema. class TypeSerde - extend T::Sig - - sig { returns(T.untyped) } attr_reader :type_class # Create a TypeSerde for the given type with a precomputed JSON Schema. - sig { params(type: T.untyped, schema: T.nilable(T::Hash[String, T.untyped])).void } def initialize(type, schema) @type_class = type @schema = schema end # Serialize a Ruby object to JSON bytes via JsonSerde. - sig { params(obj: T.untyped).returns(String) } def serialize(obj) JsonSerde.serialize(obj) end # Deserialize JSON bytes to a Ruby object via JsonSerde. - sig { params(buf: T.nilable(String)).returns(T.untyped) } def deserialize(buf) JsonSerde.deserialize(buf) end # Return the JSON Schema for this type. - sig { returns(T.nilable(T::Hash[String, T.untyped])) } def json_schema @schema end @@ -244,19 +171,14 @@ def json_schema # Serde for Dry::Struct types. # Deserializes JSON into struct instances, serializes structs to JSON. class DryStructSerde - extend T::Sig - - sig { returns(T.untyped) } attr_reader :struct_class # Create a DryStructSerde for the given Dry::Struct class. - sig { params(struct_class: T.untyped).void } def initialize(struct_class) @struct_class = struct_class end # Serialize a Dry::Struct (or hash-like object) to JSON bytes. - sig { params(obj: T.untyped).returns(String) } def serialize(obj) return ''.b if obj.nil? @@ -265,7 +187,6 @@ def serialize(obj) end # Deserialize JSON bytes into a Dry::Struct instance. - sig { params(buf: T.nilable(String)).returns(T.untyped) } def deserialize(buf) return nil if buf.nil? || buf.empty? @@ -274,49 +195,8 @@ def deserialize(buf) end # Return the JSON Schema derived from the Dry::Struct definition. - sig { returns(T::Hash[String, T.untyped]) } def json_schema @json_schema ||= Serde.dry_struct_to_json_schema(@struct_class) end end - - # Serde for T::Struct types (Sorbet's native typed structs). - # Uses T::Struct#serialize for output and T::Struct.from_hash for input. - # Generates JSON Schema from T::Struct props introspection. - class TStructSerde - extend T::Sig - - sig { returns(T.class_of(T::Struct)) } - attr_reader :struct_class - - # Create a TStructSerde for the given T::Struct subclass. - sig { params(struct_class: T.class_of(T::Struct)).void } - def initialize(struct_class) - @struct_class = struct_class - end - - # Serialize a T::Struct instance to JSON bytes. - sig { params(obj: T.untyped).returns(String) } - def serialize(obj) - return ''.b if obj.nil? - - hash = obj.is_a?(T::Struct) ? obj.serialize : obj - JSON.generate(hash).b - end - - # Deserialize JSON bytes into a T::Struct instance. - sig { params(buf: T.nilable(String)).returns(T.untyped) } - def deserialize(buf) - return nil if buf.nil? || buf.empty? - - hash = JSON.parse(buf) - T.unsafe(@struct_class).from_hash(hash) - end - - # Return the JSON Schema derived from the T::Struct props. - sig { returns(T::Hash[String, T.untyped]) } - def json_schema - @json_schema ||= Serde.t_struct_to_json_schema(@struct_class) - end - end end diff --git a/lib/restate/server.rb b/lib/restate/server.rb index c60852c..77be4e0 100644 --- a/lib/restate/server.rb +++ b/lib/restate/server.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true require 'async' @@ -14,21 +13,17 @@ module Restate # GET /health → health check # POST /invoke/:service/:handler → handler invocation class Server - extend T::Sig + SDK_VERSION = Internal::SDK_VERSION + X_RESTATE_SERVER = "restate-sdk-ruby/#{SDK_VERSION}".freeze - SDK_VERSION = T.let(Internal::SDK_VERSION, String) - X_RESTATE_SERVER = T.let("restate-sdk-ruby/#{SDK_VERSION}".freeze, String) + LOGGER = Logger.new($stdout, progname: 'Restate::Server') - LOGGER = T.let(Logger.new($stdout, progname: 'Restate::Server'), Logger) - - sig { params(endpoint: Endpoint).void } def initialize(endpoint) - @endpoint = T.let(endpoint, Endpoint) - @identity_verifier = T.let(Internal::IdentityVerifier.new(endpoint.identity_keys), Internal::IdentityVerifier) + @endpoint = endpoint + @identity_verifier = Internal::IdentityVerifier.new(endpoint.identity_keys) end # Rack interface - sig { params(env: T::Hash[String, T.untyped]).returns(T.untyped) } def call(env) path = env['PATH_INFO'] || '/' parsed = parse_path(path) @@ -51,7 +46,6 @@ def call(env) private - sig { params(path: String).returns(T::Hash[Symbol, T.untyped]) } def parse_path(path) segments = path.split('/').reject(&:empty?) @@ -77,22 +71,18 @@ def parse_path(path) end end - sig { returns(T.untyped) } def health_response [200, { 'content-type' => 'application/json', 'x-restate-server' => X_RESTATE_SERVER }, ['{"status":"ok"}']] end - sig { returns(T.untyped) } def not_found_response [404, { 'x-restate-server' => X_RESTATE_SERVER }, ['']] end - sig { params(status: Integer, message: String).returns(T.untyped) } def error_response(status, message) [status, { 'content-type' => 'text/plain', 'x-restate-server' => X_RESTATE_SERVER }, [message]] end - sig { params(env: T::Hash[String, T.untyped]).returns(T.untyped) } def handle_discover(env) # Detect HTTP version for protocol mode http_version = env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.1' @@ -119,7 +109,6 @@ def handle_discover(env) end end - sig { params(accept: String).returns(T.nilable(Integer)) } def negotiate_version(accept) if accept.include?('application/vnd.restate.endpointmanifest.v4+json') 4 @@ -132,7 +121,6 @@ def negotiate_version(accept) end end - sig { params(env: T::Hash[String, T.untyped], service_name: T.untyped, handler_name: T.untyped).returns(T.untyped) } def handle_invocation(env, service_name, handler_name) # Verify identity request_headers = extract_headers(env) @@ -154,7 +142,6 @@ def handle_invocation(env, service_name, handler_name) process_invocation(env, handler, request_headers) end - sig { params(env: T::Hash[String, T.untyped], handler: T.untyped, request_headers: T.untyped).returns(T.untyped) } def process_invocation(env, handler, request_headers) vm = VMWrapper.new(request_headers) status, response_headers = vm.get_response_head @@ -171,7 +158,7 @@ def process_invocation(env, handler, request_headers) # Read request body chunks and feed to VM until ready to execute, # then continue feeding remaining chunks via the input queue. rack_input = env['rack.input'] - ready = T.let(false, T::Boolean) + ready = false if rack_input # Feed chunks until the VM has enough to start execution while (chunk = rack_input.read_partial(16_384)) @@ -246,11 +233,8 @@ def process_invocation(env, handler, request_headers) # Rack 3 streaming body that yields chunks from an Async::Queue. # Terminates when nil is dequeued. class StreamingBody - extend T::Sig - - sig { params(queue: Async::Queue).void } def initialize(queue) - @queue = T.let(queue, Async::Queue) + @queue = queue end def each @@ -263,9 +247,8 @@ def each end end - sig { params(env: T::Hash[String, T.untyped]).returns(T::Array[T::Array[String]]) } def extract_headers(env) - headers = T.let([], T::Array[T::Array[String]]) + headers = [] env.each do |key, value| next unless key.start_with?('HTTP_') diff --git a/lib/restate/server_context.rb b/lib/restate/server_context.rb index 2e55357..40ff530 100644 --- a/lib/restate/server_context.rb +++ b/lib/restate/server_context.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true require 'async' @@ -18,35 +17,25 @@ module Restate class ServerContext include WorkflowContext include WorkflowSharedContext - extend T::Sig - LOGGER = T.let(Logger.new($stdout, progname: 'Restate::ServerContext'), Logger) + LOGGER = Logger.new($stdout, progname: 'Restate::ServerContext') - sig { returns(VMWrapper) } - attr_reader :vm + attr_reader :vm, :invocation - sig { returns(T.untyped) } - attr_reader :invocation - - sig do - params(vm: VMWrapper, handler: T.untyped, invocation: T.untyped, send_output: T.untyped, - input_queue: Async::Queue, middleware: T::Array[T.untyped]).void - end def initialize(vm:, handler:, invocation:, send_output:, input_queue:, middleware: []) - @vm = T.let(vm, VMWrapper) - @handler = T.let(handler, T.untyped) - @invocation = T.let(invocation, T.untyped) - @send_output = T.let(send_output, T.untyped) - @input_queue = T.let(input_queue, Async::Queue) - @run_coros_to_execute = T.let({}, T::Hash[Integer, T.untyped]) - @attempt_finished_event = T.let(AttemptFinishedEvent.new, AttemptFinishedEvent) - @middleware = T.let(middleware, T::Array[T.untyped]) + @vm = vm + @handler = handler + @invocation = invocation + @send_output = send_output + @input_queue = input_queue + @run_coros_to_execute = {} + @attempt_finished_event = AttemptFinishedEvent.new + @middleware = middleware end # ── Main entry point ── # Runs the handler to completion, writing the output (or failure) to the journal. - sig { void } def enter Thread.current[:restate_context] = self Thread.current[:restate_service_kind] = @handler.service_tag.kind @@ -66,8 +55,8 @@ def enter raise rescue StandardError => e # Walk the cause chain for TerminalError or internal exceptions - cause = T.let(e, T.nilable(Exception)) - handled = T.let(false, T::Boolean) + cause = e + handled = false while cause if cause.is_a?(TerminalError) f = Failure.new(code: cause.status_code, message: cause.message) @@ -95,7 +84,6 @@ def enter # Called by the server when the attempt ends (handler completed, disconnected, # or transient error). Signals the attempt_finished_event so that user code # and background pool jobs can clean up. - sig { void } def on_attempt_finished @attempt_finished_event.set! end @@ -103,44 +91,37 @@ def on_attempt_finished # ── State operations ── # Durably retrieves a state entry by name. Returns nil if unset. - sig { override.params(name: String, serde: T.untyped).returns(T.untyped) } def get(name, serde: JsonSerde) get_async(name, serde: serde).await end # Returns a DurableFuture for a state entry. Resolves to nil if unset. - sig { override.params(name: String, serde: T.untyped).returns(DurableFuture) } def get_async(name, serde: JsonSerde) handle = @vm.sys_get_state(name) DurableFuture.new(self, handle, serde: serde) end # Durably sets a state entry. The value is serialized via +serde+. - sig { override.params(name: String, value: T.untyped, serde: T.untyped).void } def set(name, value, serde: JsonSerde) @vm.sys_set_state(name, serde.serialize(value).b) end # Durably removes a single state entry by name. - sig { override.params(name: String).void } def clear(name) @vm.sys_clear_state(name) end # Durably removes all state entries for this virtual object or workflow. - sig { override.void } def clear_all @vm.sys_clear_all_state end # Returns the list of all state entry names for this virtual object or workflow. - sig { override.returns(T.untyped) } def state_keys state_keys_async.await end # Returns a DurableFuture for the list of all state entry names. - sig { override.returns(DurableFuture) } def state_keys_async handle = @vm.sys_get_state_keys DurableFuture.new(self, handle) @@ -150,7 +131,6 @@ def state_keys_async # Returns a durable future that completes after the given duration. # The timer survives handler restarts. - sig { params(seconds: Numeric).returns(DurableFuture) } def sleep(seconds) millis = (seconds * 1000).to_i handle = @vm.sys_sleep(millis) @@ -158,32 +138,27 @@ def sleep(seconds) end # Block until a previously created handle completes. Returns the value. - sig { params(handle: Integer).returns(T.untyped) } def resolve_handle(handle) poll_and_take(handle) end # Wait until any of the given handles completes. Does not take notifications. - sig { params(handles: T::Array[Integer]).void } def wait_any_handle(handles) poll_or_cancel(handles) unless handles.any? { |h| @vm.is_completed(h) } end # Check if a handle is completed (non-blocking). - sig { params(handle: Integer).returns(T::Boolean) } def completed?(handle) @vm.is_completed(handle) end # Take a completed handle's notification, returning the value. # Raises TerminalError if the handle resolved to a failure. - sig { params(handle: Integer).returns(T.untyped) } def take_completed(handle) must_take_notification(handle) end # Wait until any of the given futures completes. Returns [completed, remaining]. - sig { override.params(futures: DurableFuture).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) } def wait_any(*futures) handles = futures.map(&:handle) wait_any_handle(handles) @@ -207,15 +182,6 @@ def wait_any(*futures) # Pass +background: true+ to run the block in a real OS Thread, keeping the # fiber event loop responsive for other concurrent handlers. Use this for # CPU-intensive work. - sig do - override.params( - name: String, - serde: T.untyped, - retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, - action: T.proc.returns(T.untyped) - ).returns(DurableFuture) - end def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action) handle = @vm.sys_run(name) @@ -229,15 +195,6 @@ def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action) # and returns the result directly. # # Accepts all the same options as +run+, including +background: true+. - sig do - override.params( - name: String, - serde: T.untyped, - retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, - action: T.proc.returns(T.untyped) - ).returns(T.untyped) - end def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action) run(name, serde: serde, retry_policy: retry_policy, background: background, &action).await end @@ -245,18 +202,6 @@ def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &acti # ── Service calls ── # Durably calls a handler on a Restate service and returns a future for its result. - sig do - override.params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol), - arg: T.untyped, - key: T.nilable(String), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped, - output_serde: T.untyped - ).returns(DurableCallFuture) - end def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) svc_name, handler_name, handler_meta = resolve_call_target(service, handler) @@ -272,18 +217,6 @@ def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: end # Sends a one-way invocation to a Restate service handler (fire-and-forget). - sig do - override.params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol), - arg: T.untyped, - key: T.nilable(String), - delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) svc_name, handler_name, handler_meta = resolve_call_target(service, handler) @@ -298,18 +231,6 @@ def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: n end # Durably calls a handler on a Restate virtual object, keyed by +key+. - sig do - override.params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol), - key: String, - arg: T.untyped, - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped, - output_serde: T.untyped - ).returns(DurableCallFuture) - end def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) svc_name, handler_name, handler_meta = resolve_call_target(service, handler) @@ -325,18 +246,6 @@ def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, end # Sends a one-way invocation to a Restate virtual object handler (fire-and-forget). - sig do - override.params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol), - key: String, - arg: T.untyped, - delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) svc_name, handler_name, handler_meta = resolve_call_target(service, handler) @@ -351,18 +260,6 @@ def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, he end # Durably calls a handler on a Restate workflow, keyed by +key+. - sig do - override.params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol), - key: String, - arg: T.untyped, - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped, - output_serde: T.untyped - ).returns(DurableCallFuture) - end def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil, input_serde: NOT_SET, output_serde: NOT_SET) object_call(service, handler, key, arg, idempotency_key: idempotency_key, headers: headers, @@ -370,18 +267,6 @@ def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil end # Sends a one-way invocation to a Restate workflow handler (fire-and-forget). - sig do - override.params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol), - key: String, - arg: T.untyped, - delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil, input_serde: NOT_SET) object_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key, headers: headers, @@ -391,20 +276,17 @@ def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, # ── Awakeables ── # Creates an awakeable and returns [awakeable_id, DurableFuture]. - sig { override.params(serde: T.untyped).returns([String, DurableFuture]) } def awakeable(serde: JsonSerde) id, handle = @vm.sys_awakeable [id, DurableFuture.new(self, handle, serde: serde)] end # Resolves an awakeable with a success value. - sig { override.params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void } def resolve_awakeable(awakeable_id, payload, serde: JsonSerde) @vm.sys_complete_awakeable_success(awakeable_id, serde.serialize(payload).b) end # Rejects an awakeable with a terminal failure. - sig { override.params(awakeable_id: String, message: String, code: Integer).void } def reject_awakeable(awakeable_id, message, code: 500) failure = Failure.new(code: code, message: message) @vm.sys_complete_awakeable_failure(awakeable_id, failure) @@ -413,7 +295,6 @@ def reject_awakeable(awakeable_id, message, code: 500) # ── Promises (Workflow API) ── # Gets a durable promise value, blocking until resolved. - sig { override.params(name: String, serde: T.untyped).returns(T.untyped) } def promise(name, serde: JsonSerde) handle = @vm.sys_get_promise(name) poll_and_take(handle) do |raw| @@ -422,7 +303,6 @@ def promise(name, serde: JsonSerde) end # Peeks at a durable promise value without blocking. Returns nil if not yet resolved. - sig { override.params(name: String, serde: T.untyped).returns(T.untyped) } def peek_promise(name, serde: JsonSerde) handle = @vm.sys_peek_promise(name) poll_and_take(handle) do |raw| @@ -431,7 +311,6 @@ def peek_promise(name, serde: JsonSerde) end # Resolves a durable promise with a success value. - sig { override.params(name: String, payload: T.untyped, serde: T.untyped).void } def resolve_promise(name, payload, serde: JsonSerde) handle = @vm.sys_complete_promise_success(name, serde.serialize(payload).b) poll_and_take(handle) @@ -439,7 +318,6 @@ def resolve_promise(name, payload, serde: JsonSerde) end # Rejects a durable promise with a terminal failure. - sig { override.params(name: String, message: String, code: Integer).void } def reject_promise(name, message, code: 500) failure = Failure.new(code: code, message: message) handle = @vm.sys_complete_promise_failure(name, failure) @@ -450,7 +328,6 @@ def reject_promise(name, message, code: 500) # ── Cancel invocation ── # Requests cancellation of another invocation by its id. - sig { override.params(invocation_id: String).void } def cancel_invocation(invocation_id) @vm.sys_cancel_invocation(invocation_id) end @@ -458,16 +335,6 @@ def cancel_invocation(invocation_id) # ── Generic calls (raw bytes, no serde) ── # Durably calls a handler using raw bytes (no serialization). Useful for proxying. - sig do - override.params( - service: String, - handler: String, - arg: String, - key: T.nilable(String), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(DurableCallFuture) - end def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil) call_handle = @vm.sys_call( service: service, handler: handler, parameter: arg.b, @@ -478,17 +345,6 @@ def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: end # Sends a one-way invocation using raw bytes (no serialization). Useful for proxying. - sig do - override.params( - service: String, - handler: String, - arg: String, - key: T.nilable(String), - delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(SendHandle) - end def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil) delay_ms = delay ? (delay * 1000).to_i : nil invocation_id_handle = @vm.sys_send( @@ -501,7 +357,6 @@ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: n # ── Request metadata ── # Returns metadata about the current invocation (id, headers, raw body). - sig { override.returns(T.untyped) } def request @request ||= Request.new( id: @invocation.invocation_id, @@ -512,7 +367,6 @@ def request end # Returns the key for this virtual object or workflow invocation. - sig { override.returns(String) } def key @invocation.key end @@ -522,18 +376,11 @@ def key # ── Progress loop ── # Polls until the given handle(s) complete, then takes the notification. - sig do - params( - handle: Integer, - block: T.nilable(T.proc.params(arg0: T.untyped).returns(T.untyped)) - ).returns(T.untyped) - end - def poll_and_take(handle, &block) + def poll_and_take(handle, &) poll_or_cancel([handle]) unless @vm.is_completed(handle) - must_take_notification(handle, &block) + must_take_notification(handle, &) end - sig { params(handles: T::Array[Integer]).void } def poll_or_cancel(handles) loop do flush_output @@ -579,12 +426,6 @@ def poll_or_cancel(handles) end end - sig do - params( - handle: Integer, - block: T.nilable(T.proc.params(arg0: T.untyped).returns(T.untyped)) - ).returns(T.untyped) - end def must_take_notification(handle, &block) result = @vm.take_notification(handle) @@ -607,7 +448,6 @@ def must_take_notification(handle, &block) end end - sig { void } def flush_output loop do output = @vm.take_output @@ -621,17 +461,11 @@ def flush_output # Resolves a service+handler pair from class/symbol or string/string. # Returns [service_name, handler_name, handler_metadata_or_nil]. - sig do - params( - service: T.any(String, T::Class[T.anything]), - handler: T.any(String, Symbol) - ).returns([String, String, T.nilable(Handler)]) - end def resolve_call_target(service, handler) if service.is_a?(Class) && service.respond_to?(:service_name) - svc_name = T.unsafe(service).service_name + svc_name = service.service_name handler_name = handler.to_s - handler_meta = service.respond_to?(:handlers) ? T.unsafe(service).handlers[handler_name] : nil + handler_meta = service.respond_to?(:handlers) ? service.handlers[handler_name] : nil [svc_name, handler_name, handler_meta] else [service.to_s, handler.to_s, nil] @@ -639,7 +473,6 @@ def resolve_call_target(service, handler) end # Resolves a serde value: if the caller passed NOT_SET, fall back to handler metadata, then JsonSerde. - sig { params(caller_serde: T.untyped, handler_meta: T.nilable(Handler), field: Symbol).returns(T.untyped) } def resolve_serde(caller_serde, handler_meta, field) return caller_serde unless caller_serde.equal?(NOT_SET) @@ -652,41 +485,17 @@ def resolve_serde(caller_serde, handler_meta, field) # ── Run execution ── - sig do - params( - handle: Integer, - action: T.proc.returns(T.untyped), - serde: T.untyped, - retry_policy: T.nilable(RunRetryPolicy) - ).void - end def execute_run(handle, action, serde, retry_policy) propose_run_result(handle, action, serde, retry_policy) end # Like execute_run, but offloads the action to a real OS Thread. # The fiber yields (via IO.pipe) while the thread runs, keeping the event loop responsive. - sig do - params( - handle: Integer, - action: T.proc.returns(T.untyped), - serde: T.untyped, - retry_policy: T.nilable(RunRetryPolicy) - ).void - end def execute_run_threaded(handle, action, serde, retry_policy) propose_run_result(handle, -> { offload_to_thread(action) }, serde, retry_policy) end # Runs the action and proposes the result (success/failure/transient) to the VM. - sig do - params( - handle: Integer, - action: T.proc.returns(T.untyped), - serde: T.untyped, - retry_policy: T.nilable(RunRetryPolicy) - ).void - end def propose_run_result(handle, action, serde, retry_policy) start = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin @@ -734,11 +543,10 @@ def propose_run_result(handle, action, serde, retry_policy) # most blocking I/O (Net::HTTP, TCPSocket, etc.) and yields the fiber # automatically. +background: true+ is only needed for CPU-heavy native # extensions that release the GVL (e.g., image processing, crypto). - sig { params(action: T.proc.returns(T.untyped)).returns(T.untyped) } def offload_to_thread(action) read_io, write_io = IO.pipe - result = T.let(nil, T.untyped) - error = T.let(nil, T.nilable(Exception)) + result = nil + error = nil event = @attempt_finished_event begin @@ -772,25 +580,21 @@ def offload_to_thread(action) # Avoids creating a new Thread per call (~1ms + ~1MB stack each). # Workers are daemon threads that do not prevent process exit. module BackgroundPool - extend T::Sig - - @queue = T.let(Queue.new, Queue) - @workers = T.let([], T::Array[Thread]) - @mutex = T.let(Mutex.new, Mutex) - @size = T.let(0, Integer) + @queue = Queue.new + @workers = [] + @mutex = Mutex.new + @size = 0 - POOL_SIZE = T.let(Integer(ENV.fetch('RESTATE_BACKGROUND_POOL_SIZE', 8)), Integer) + POOL_SIZE = Integer(ENV.fetch('RESTATE_BACKGROUND_POOL_SIZE', 8)) module_function # Submit a block to be executed by a pool worker. - sig { params(block: T.proc.void).void } def submit(&block) ensure_started @queue.push(block) end - sig { void } def ensure_started return if @size >= POOL_SIZE diff --git a/lib/restate/service.rb b/lib/restate/service.rb index a13f72c..6ef3c02 100644 --- a/lib/restate/service.rb +++ b/lib/restate/service.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -11,7 +10,6 @@ module Restate # end # end class Service - extend T::Sig extend ServiceDSL # Register a handler method on this service. @@ -27,7 +25,7 @@ def self.handler(method_name = nil, **opts) end return method_name unless method_name.is_a?(Symbol) - _register_handler(method_name, **T.unsafe({ kind: nil, **opts })) + _register_handler(method_name, kind: nil, **opts) end # Returns a call proxy for fluent durable calls to this service. diff --git a/lib/restate/service_dsl.rb b/lib/restate/service_dsl.rb index 5710c4a..d5da037 100644 --- a/lib/restate/service_dsl.rb +++ b/lib/restate/service_dsl.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -57,7 +56,7 @@ def inherited(subclass) # rubocop:disable Metrics/MethodLength # end # end def state(name, default: nil, serde: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity - unless T.unsafe(self).respond_to?(:_service_kind) && %w[object workflow].include?(T.unsafe(self)._service_kind) + unless respond_to?(:_service_kind) && %w[object workflow].include?(_service_kind) Kernel.raise ArgumentError, 'state declarations are only available on VirtualObject and Workflow' end @@ -68,7 +67,7 @@ def state(name, default: nil, serde: nil) # rubocop:disable Metrics/MethodLength state_default = default # Getter: reads from durable state, returns default if unset - T.unsafe(self).define_method(name) do + define_method(name) do ctx = Thread.current[:restate_context] Kernel.raise 'Not inside a Restate handler' unless ctx @@ -77,7 +76,7 @@ def state(name, default: nil, serde: nil) # rubocop:disable Metrics/MethodLength end # Setter: writes to durable state - T.unsafe(self).define_method(:"#{name}=") do |value| + define_method(:"#{name}=") do |value| ctx = Thread.current[:restate_context] Kernel.raise 'Not inside a Restate handler' unless ctx @@ -89,7 +88,7 @@ def state(name, default: nil, serde: nil) # rubocop:disable Metrics/MethodLength end # Clear: removes the state entry - T.unsafe(self).define_method(:"clear_#{name}") do + define_method(:"clear_#{name}") do ctx = Thread.current[:restate_context] Kernel.raise 'Not inside a Restate handler' unless ctx @@ -105,7 +104,7 @@ def service_name(name = nil) if name @_service_name = name else - @_service_name || T.unsafe(self).name&.split('::')&.last + @_service_name || self.name&.split('::')&.last end end @@ -199,7 +198,7 @@ def invocation_retry_policy(initial_interval: nil, max_interval: nil, max_attemp # # @return [ServiceTag] def service_tag - ServiceTag.new(kind: T.unsafe(self)._service_kind, name: service_name, + ServiceTag.new(kind: _service_kind, name: service_name, description: @_description, metadata: @_metadata) end @@ -259,7 +258,7 @@ def _register_handler(method_name, kind:, **opts) def _build_handlers # rubocop:disable Metrics/AbcSize,Metrics/MethodLength tag = service_tag result = {} - instance = T.unsafe(self).allocate + instance = allocate @_handler_registry.each do |name, meta| # rubocop:disable Metrics/BlockLength input_serde = Serde.resolve(meta[:input]) @@ -272,7 +271,7 @@ def _build_handlers # rubocop:disable Metrics/AbcSize,Metrics/MethodLength output_serde: output_serde ) - um = T.unsafe(self).instance_method(name) + um = instance_method(name) arity = um.arity.abs unless [0, 1].include?(arity) Kernel.raise ArgumentError, "handler '#{name}' must accept 0 or 1 parameters ([input]), got #{arity}" diff --git a/lib/restate/service_proxy.rb b/lib/restate/service_proxy.rb index 109cf05..e53feac 100644 --- a/lib/restate/service_proxy.rb +++ b/lib/restate/service_proxy.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -16,9 +15,6 @@ module Restate # # @!visibility private class ServiceCallProxy - extend T::Sig - - sig { params(service_class: T.untyped, key: T.nilable(String), call_method: Symbol).void } def initialize(service_class, key: nil, call_method: :service_call) @service_class = service_class @key = key @@ -35,7 +31,7 @@ def method_missing(handler_name, arg = nil, **opts) end def respond_to_missing?(method_name, include_private = false) - (@service_class.respond_to?(:handlers) && T.unsafe(@service_class).handlers.key?(method_name.to_s)) || super + (@service_class.respond_to?(:handlers) && @service_class.handlers.key?(method_name.to_s)) || super end end @@ -52,14 +48,6 @@ def respond_to_missing?(method_name, include_private = false) # # @!visibility private class ServiceSendProxy - extend T::Sig - - sig do - params( - service_class: T.untyped, key: T.nilable(String), - send_method: Symbol, delay: T.nilable(Numeric) - ).void - end def initialize(service_class, key: nil, send_method: :service_send, delay: nil) @service_class = service_class @key = key @@ -78,7 +66,7 @@ def method_missing(handler_name, arg = nil, **opts) end def respond_to_missing?(method_name, include_private = false) - (@service_class.respond_to?(:handlers) && T.unsafe(@service_class).handlers.key?(method_name.to_s)) || super + (@service_class.respond_to?(:handlers) && @service_class.handlers.key?(method_name.to_s)) || super end end end diff --git a/lib/restate/testing.rb b/lib/restate/testing.rb index 4689583..88bf74e 100644 --- a/lib/restate/testing.rb +++ b/lib/restate/testing.rb @@ -1,4 +1,3 @@ -# typed: false # frozen_string_literal: true require 'restate' diff --git a/lib/restate/version.rb b/lib/restate/version.rb index b511954..f7f8bd9 100644 --- a/lib/restate/version.rb +++ b/lib/restate/version.rb @@ -1,4 +1,3 @@ -# typed: strict # frozen_string_literal: true module Restate diff --git a/lib/restate/virtual_object.rb b/lib/restate/virtual_object.rb index b40d402..a7a52b8 100644 --- a/lib/restate/virtual_object.rb +++ b/lib/restate/virtual_object.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -17,7 +16,6 @@ module Restate # end # end class VirtualObject - extend T::Sig extend ServiceDSL # Register an exclusive handler. Use as: +handler def my_method(ctx, arg)+ @@ -33,7 +31,7 @@ def self.handler(method_name = nil, kind: :exclusive, **opts) end return method_name unless method_name.is_a?(Symbol) - _register_handler(method_name, **T.unsafe({ kind: kind.to_s, **opts })) + _register_handler(method_name, kind: kind.to_s, **opts) end # Register a shared (concurrent-access) handler. @@ -46,7 +44,7 @@ def self.shared(method_name, **opts) "handler expects a Symbol (use `shared def #{method_name}(...)` or `shared :#{method_name}`)" end - _register_handler(method_name, **T.unsafe({ kind: 'shared', **opts })) + _register_handler(method_name, kind: 'shared', **opts) end # Returns a call proxy for fluent durable calls to this virtual object. diff --git a/lib/restate/vm.rb b/lib/restate/vm.rb index be16e8a..a14a570 100644 --- a/lib/restate/vm.rb +++ b/lib/restate/vm.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true begin @@ -19,9 +18,9 @@ module Restate class NotReady; end class Suspended; end - NOT_READY = T.let(NotReady.new.freeze, NotReady) - SUSPENDED = T.let(Suspended.new.freeze, Suspended) - CANCEL_HANDLE = T.let(Internal::CANCEL_NOTIFICATION_HANDLE, Integer) + NOT_READY = NotReady.new.freeze + SUSPENDED = Suspended.new.freeze + CANCEL_HANDLE = Internal::CANCEL_NOTIFICATION_HANDLE # Progress loop result types class DoProgressAnyCompleted; end @@ -29,10 +28,10 @@ class DoProgressReadFromInput; end class DoProgressCancelSignalReceived; end class DoWaitPendingRun; end - DO_PROGRESS_ANY_COMPLETED = T.let(DoProgressAnyCompleted.new.freeze, DoProgressAnyCompleted) - DO_PROGRESS_READ_FROM_INPUT = T.let(DoProgressReadFromInput.new.freeze, DoProgressReadFromInput) - DO_PROGRESS_CANCEL_SIGNAL_RECEIVED = T.let(DoProgressCancelSignalReceived.new.freeze, DoProgressCancelSignalReceived) - DO_WAIT_PENDING_RUN = T.let(DoWaitPendingRun.new.freeze, DoWaitPendingRun) + DO_PROGRESS_ANY_COMPLETED = DoProgressAnyCompleted.new.freeze + DO_PROGRESS_READ_FROM_INPUT = DoProgressReadFromInput.new.freeze + DO_PROGRESS_CANCEL_SIGNAL_RECEIVED = DoProgressCancelSignalReceived.new.freeze + DO_WAIT_PENDING_RUN = DoWaitPendingRun.new.freeze DoProgressExecuteRun = Struct.new(:handle, keyword_init: true) @@ -52,50 +51,39 @@ class DoWaitPendingRun; end # Wraps the native Restate::Internal::VM, mapping native types to Ruby types. class VMWrapper - extend T::Sig - - sig { params(headers: T.untyped).void } def initialize(headers) - @vm = T.let(Internal::VM.new(headers), Internal::VM) + @vm = Internal::VM.new(headers) end - sig { returns([Integer, T.untyped]) } def get_response_head result = @vm.get_response_head [result.status_code, result.headers] end - sig { params(buf: String).void } def notify_input(buf) @vm.notify_input(buf) end - sig { void } def notify_input_closed @vm.notify_input_closed end - sig { params(error: String, stacktrace: T.nilable(String)).void } def notify_error(error, stacktrace = nil) @vm.notify_error(error, stacktrace) end - sig { returns(T.nilable(String)) } def take_output @vm.take_output end - sig { returns(T::Boolean) } def is_ready_to_execute @vm.is_ready_to_execute end - sig { params(handle: Integer).returns(T::Boolean) } def is_completed(handle) @vm.is_completed(handle) end - sig { params(handles: T::Array[Integer]).returns(T.untyped) } def do_progress(handles) result = @vm.do_progress(handles) map_do_progress(result) @@ -103,7 +91,6 @@ def do_progress(handles) e end - sig { params(handle: Integer).returns(T.untyped) } def take_notification(handle) result = @vm.take_notification(handle) map_notification(result) @@ -111,7 +98,6 @@ def take_notification(handle) e end - sig { returns(T.untyped) } def sys_input inp = @vm.sys_input headers = inp.headers.map { |h| [h.key, h.value] } @@ -124,94 +110,56 @@ def sys_input ) end - sig { params(name: String).returns(Integer) } def sys_get_state(name) @vm.sys_get_state(name) end - sig { returns(Integer) } def sys_get_state_keys @vm.sys_get_state_keys end - sig { params(name: String, value: String).void } def sys_set_state(name, value) @vm.sys_set_state(name, value) end - sig { params(name: String).void } def sys_clear_state(name) @vm.sys_clear_state(name) end - sig { void } def sys_clear_all_state @vm.sys_clear_all_state end - sig { params(millis: Integer, name: T.nilable(String)).returns(Integer) } def sys_sleep(millis, name = nil) # Rust side always expects 2 args: (millis, name_or_nil) @vm.sys_sleep(millis, name) end - sig do - params( - service: String, - handler: String, - parameter: String, - key: T.nilable(String), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(Internal::CallHandle) - end def sys_call(service:, handler:, parameter:, key: nil, idempotency_key: nil, headers: nil) # Rust side expects 6 args: (service, handler, buffer, key_or_nil, idem_key_or_nil, headers_or_nil) hdr_array = headers&.map { |k, v| [k, v] } @vm.sys_call(service, handler, parameter, key, idempotency_key, hdr_array) end - sig do - params( - service: String, - handler: String, - parameter: String, - key: T.nilable(String), - delay: T.nilable(Integer), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(Integer) - end def sys_send(service:, handler:, parameter:, key: nil, delay: nil, idempotency_key: nil, headers: nil) # Rust side expects 7 args hdr_array = headers&.map { |k, v| [k, v] } @vm.sys_send(service, handler, parameter, key, delay, idempotency_key, hdr_array) end - sig { params(name: String).returns(Integer) } def sys_run(name) @vm.sys_run(name) end - sig { params(handle: Integer, output: String).void } def propose_run_completion_success(handle, output) @vm.propose_run_completion_success(handle, output) end - sig { params(handle: Integer, failure: T.untyped).void } def propose_run_completion_failure(handle, failure) native_failure = Internal::Failure.new(failure.code, failure.message, nil) @vm.propose_run_completion_failure(handle, native_failure) end - sig do - params( - handle: Integer, - failure: T.untyped, - attempt_duration_ms: Integer, - config: T.untyped - ).void - end def propose_run_completion_transient(handle, failure:, attempt_duration_ms:, config:) native_failure = Internal::Failure.new(failure.code, failure.message, failure.stacktrace) native_config = Internal::ExponentialRetryConfig.new( @@ -222,73 +170,60 @@ def propose_run_completion_transient(handle, failure:, attempt_duration_ms:, con @vm.propose_run_completion_failure_transient(handle, native_failure, attempt_duration_ms, native_config) end - sig { params(output: String).void } def sys_write_output_success(output) @vm.sys_write_output_success(output) end - sig { params(failure: T.untyped).void } def sys_write_output_failure(failure) native_failure = Internal::Failure.new(failure.code, failure.message, nil) @vm.sys_write_output_failure(native_failure) end - sig { void } def sys_end @vm.sys_end end - sig { returns(T::Boolean) } def is_replaying @vm.is_replaying end # Returns [awakeable_id (String), notification_handle (Integer)] - sig { returns([String, Integer]) } def sys_awakeable @vm.sys_awakeable end - sig { params(awakeable_id: String, value: String).void } def sys_complete_awakeable_success(awakeable_id, value) @vm.sys_complete_awakeable_success(awakeable_id, value) end - sig { params(awakeable_id: String, failure: T.untyped).void } def sys_complete_awakeable_failure(awakeable_id, failure) native_failure = Internal::Failure.new(failure.code, failure.message, nil) @vm.sys_complete_awakeable_failure(awakeable_id, native_failure) end - sig { params(key: String).returns(Integer) } def sys_get_promise(key) @vm.sys_get_promise(key) end - sig { params(key: String).returns(Integer) } def sys_peek_promise(key) @vm.sys_peek_promise(key) end - sig { params(key: String, value: String).returns(Integer) } def sys_complete_promise_success(key, value) @vm.sys_complete_promise_success(key, value) end - sig { params(key: String, failure: T.untyped).returns(Integer) } def sys_complete_promise_failure(key, failure) native_failure = Internal::Failure.new(failure.code, failure.message, nil) @vm.sys_complete_promise_failure(key, native_failure) end - sig { params(invocation_id: String).void } def sys_cancel_invocation(invocation_id) @vm.sys_cancel_invocation(invocation_id) end private - sig { params(result: T.untyped).returns(T.untyped) } def map_do_progress(result) case result when Internal::Suspended @@ -308,7 +243,6 @@ def map_do_progress(result) end end - sig { params(result: T.untyped).returns(T.untyped) } def map_notification(result) case result when Internal::Suspended diff --git a/lib/restate/workflow.rb b/lib/restate/workflow.rb index ad4159d..94f0b59 100644 --- a/lib/restate/workflow.rb +++ b/lib/restate/workflow.rb @@ -1,4 +1,3 @@ -# typed: true # frozen_string_literal: true module Restate @@ -15,7 +14,6 @@ module Restate # end # end class Workflow - extend T::Sig extend ServiceDSL # Register the main workflow entry point. @@ -31,7 +29,7 @@ def self.main(method_name = nil, **opts) end return method_name unless method_name.is_a?(Symbol) - _register_handler(method_name, **T.unsafe({ kind: 'workflow', **opts })) + _register_handler(method_name, kind: 'workflow', **opts) end # Register a shared handler on this workflow. @@ -46,7 +44,7 @@ def self.handler(method_name = nil, **opts) end return method_name unless method_name.is_a?(Symbol) - _register_handler(method_name, **T.unsafe({ kind: 'shared', **opts })) + _register_handler(method_name, kind: 'shared', **opts) end # Returns a call proxy for fluent durable calls to this workflow. diff --git a/lib/tapioca/dsl/compilers/restate.rb b/lib/tapioca/dsl/compilers/restate.rb deleted file mode 100644 index 7b6344d..0000000 --- a/lib/tapioca/dsl/compilers/restate.rb +++ /dev/null @@ -1,115 +0,0 @@ -# typed: false -# frozen_string_literal: true - -return unless defined?(Tapioca::Dsl::Compiler) - -require 'restate' - -module Tapioca - module Dsl - module Compilers - # Generates Sorbet sigs for Restate handler methods. - # - # Handlers take 0 or 1 parameters (the input). Context is implicit - # via +Restate.*+ module methods. - # - # Usage: - # bundle exec tapioca dsl - class Restate < Compiler - ConstantType = type_member { { fixed: Module } } - - class << self - def gather_constants - # Load service files so they're visible to all_classes. - # In non-Rails apps, Tapioca doesn't auto-load application code. - load_service_files - - all_classes.select do |klass| - klass.is_a?(Class) && ( - klass < ::Restate::Service || - klass < ::Restate::VirtualObject || - klass < ::Restate::Workflow - ) - rescue TypeError - false - end - end - - private - - def load_service_files # rubocop:disable Metrics/MethodLength - root = Bundler.root.to_s - patterns = [ - "#{root}/*.rb", - "#{root}/app/**/*.rb", - "#{root}/services/**/*.rb", - "#{root}/examples/**/*.rb" - ] - Dir.glob(patterns).each do |file| - next if file.end_with?('config.ru', 'Rakefile') - - require file - rescue LoadError, StandardError - nil # skip files that can't be loaded - end - end - end - - def decorate # rubocop:disable Metrics/MethodLength - root.create_path(constant) do |klass| - constant.handlers.each do |name, handler| - params = [] - if handler.arity == 1 - input_type = resolve_input_type(handler) - params << create_param('input', type: input_type) - end - output_type = resolve_output_type(handler) - klass.create_method(name, parameters: params, return_type: output_type) - end - end - end - - private - - # Maps (service kind, handler kind) to the correct context module. - def resolve_context_type(klass, handler) - if klass < ::Restate::Workflow - handler.kind == 'workflow' ? 'Restate::WorkflowContext' : 'Restate::WorkflowSharedContext' - elsif klass < ::Restate::VirtualObject - handler.kind == 'shared' ? 'Restate::ObjectSharedContext' : 'Restate::ObjectContext' - else - 'Restate::Context' - end - end - - # Resolves the Sorbet type string for the handler's input serde. - def resolve_input_type(handler) - type_class = handler.handler_io&.input_serde - sorbet_type_name(type_class) || 'T.untyped' - end - - # Resolves the Sorbet type string for the handler's output serde. - def resolve_output_type(handler) - type_class = handler.handler_io&.output_serde - sorbet_type_name(type_class) || 'T.untyped' - end - - # Returns a Sorbet type string if the serde wraps a known type, nil otherwise. - def sorbet_type_name(serde) - return nil if serde.nil? - - # TStructSerde exposes .struct_class (T::Struct subclasses are visible to Sorbet) - return serde.struct_class.name if serde.is_a?(::Restate::TStructSerde) - - # TypeSerde wraps a primitive type in .type_class - if serde.respond_to?(:type_class) - name = serde.type_class.name - return name if %w[String Integer Float].include?(name) - end - - nil - end - end - end - end -end diff --git a/rbi/restate-sdk.rbi b/rbi/restate-sdk.rbi deleted file mode 100644 index 596790c..0000000 --- a/rbi/restate-sdk.rbi +++ /dev/null @@ -1,582 +0,0 @@ -# typed: true - -# RBI shipped with the restate-sdk gem. -# Tapioca merges this automatically when users run `tapioca gems`. - -module Restate - # Create an endpoint, optionally binding services. - sig do - params( - services: T.untyped, - protocol: T.nilable(String), - identity_keys: T.nilable(T::Array[String]) - ).returns(Restate::Endpoint) - end - def self.endpoint(*services, protocol: nil, identity_keys: nil); end - - # ── Durable execution ── - - # Execute a durable side effect. Returns a DurableFuture. - sig do - params( - name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped) - ).returns(DurableFuture) - end - def self.run(name, serde: Restate::JsonSerde, retry_policy: nil, background: false, &action); end - - # Convenience shortcut for +run(...).await+. Returns the result directly. - sig do - params( - name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped) - ).returns(T.untyped) - end - def self.run_sync(name, serde: Restate::JsonSerde, retry_policy: nil, background: false, &action); end - - # Durable timer that survives handler restarts. - sig { params(seconds: Numeric).returns(DurableFuture) } - def self.sleep(seconds); end - - # ── State operations (VirtualObject / Workflow) ── - - # Durably retrieve a state entry. Returns nil if unset. - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def self.get(name, serde: Restate::JsonSerde); end - - # Durably retrieve a state entry, returning a DurableFuture instead of blocking. - sig { params(name: String, serde: T.untyped).returns(DurableFuture) } - def self.get_async(name, serde: Restate::JsonSerde); end - - # Durably set a state entry. - sig { params(name: String, value: T.untyped, serde: T.untyped).void } - def self.set(name, value, serde: Restate::JsonSerde); end - - # Durably remove a single state entry. - sig { params(name: String).void } - def self.clear(name); end - - # Durably remove all state entries. - sig { void } - def self.clear_all; end - - # List all state entry names. - sig { returns(T.untyped) } - def self.state_keys; end - - # List all state entry names, returning a DurableFuture. - sig { returns(DurableFuture) } - def self.state_keys_async; end - - # ── Service communication ── - - # Durably call a handler on a Restate service. - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end - def self.service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil, - input_serde: T.unsafe(nil), output_serde: T.unsafe(nil)); end - - # Fire-and-forget send to a Restate service handler. - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end - def self.service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, - headers: nil, input_serde: T.unsafe(nil)); end - - # Durably call a handler on a Restate virtual object. - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end - def self.object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, - input_serde: T.unsafe(nil), output_serde: T.unsafe(nil)); end - - # Fire-and-forget send to a Restate virtual object handler. - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end - def self.object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, - headers: nil, input_serde: T.unsafe(nil)); end - - # Durably call a handler on a Restate workflow. - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end - def self.workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil, - input_serde: T.unsafe(nil), output_serde: T.unsafe(nil)); end - - # Fire-and-forget send to a Restate workflow handler. - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end - def self.workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, - headers: nil, input_serde: T.unsafe(nil)); end - - # Durably call a handler using raw bytes (no serialization). - sig do - params( - service: String, handler: String, arg: String, - key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(DurableCallFuture) - end - def self.generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil); end - - # Fire-and-forget send using raw bytes (no serialization). - sig do - params( - service: String, handler: String, arg: String, - key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]) - ).returns(SendHandle) - end - def self.generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil); end - - # ── Awakeables ── - - # Create an awakeable for external callbacks. Returns [awakeable_id, DurableFuture]. - sig { params(serde: T.untyped).returns([String, DurableFuture]) } - def self.awakeable(serde: Restate::JsonSerde); end - - # Resolve an awakeable with a success value. - sig { params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void } - def self.resolve_awakeable(awakeable_id, payload, serde: Restate::JsonSerde); end - - # Reject an awakeable with a terminal failure. - sig { params(awakeable_id: String, message: String, code: Integer).void } - def self.reject_awakeable(awakeable_id, message, code: 500); end - - # ── Promises (Workflow only) ── - - # Get a durable promise value, blocking until resolved. - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def self.promise(name, serde: Restate::JsonSerde); end - - # Peek at a durable promise without blocking. Returns nil if not yet resolved. - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def self.peek_promise(name, serde: Restate::JsonSerde); end - - # Resolve a durable promise with a value. - sig { params(name: String, payload: T.untyped, serde: T.untyped).void } - def self.resolve_promise(name, payload, serde: Restate::JsonSerde); end - - # Reject a durable promise with a terminal failure. - sig { params(name: String, message: String, code: Integer).void } - def self.reject_promise(name, message, code: 500); end - - # ── Futures ── - - # Wait until any of the given futures completes. Returns [completed, remaining]. - sig { params(futures: DurableFuture).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) } - def self.wait_any(*futures); end - - # ── Request metadata ── - - # Returns metadata about the current invocation (id, headers, raw body). - sig { returns(T.untyped) } - def self.request; end - - # Returns the key for this virtual object or workflow invocation. - sig { returns(String) } - def self.key; end - - # ── Invocation control ── - - # Request cancellation of another invocation. - sig { params(invocation_id: String).void } - def self.cancel_invocation(invocation_id); end - - class TerminalError < StandardError - sig { returns(Integer) } - def status_code; end - - sig { params(message: String, status_code: Integer).void } - def initialize(message = '', status_code: 500); end - end - - class AttemptFinishedEvent - sig { returns(T::Boolean) } - def set?; end - - sig { void } - def wait; end - end - - Request = T.type_alias { T.untyped } - - class RunRetryPolicy < T::Struct - const :initial_interval, T.nilable(Integer) - const :max_attempts, T.nilable(Integer) - const :max_duration, T.nilable(Integer) - const :max_interval, T.nilable(Integer) - const :interval_factor, T.nilable(Float) - end - - class DurableFuture - sig { returns(T.untyped) } - def await; end - - sig { returns(T::Boolean) } - def completed?; end - - sig { returns(Integer) } - def handle; end - end - - class DurableCallFuture < DurableFuture - sig { returns(String) } - def invocation_id; end - - sig { void } - def cancel; end - end - - class SendHandle - sig { returns(String) } - def invocation_id; end - - sig { void } - def cancel; end - end - - # Base context interface for all Restate handlers. - module Context - sig do - params( - name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped) - ).returns(DurableFuture) - end - def run(name, serde: Restate::JsonSerde, retry_policy: nil, background: false, &action); end - - sig do - params( - name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy), - background: T::Boolean, action: T.proc.returns(T.untyped) - ).returns(T.untyped) - end - def run_sync(name, serde: Restate::JsonSerde, retry_policy: nil, background: false, &action); end - - sig { params(seconds: Numeric).returns(DurableFuture) } - def sleep(seconds); end - - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end - def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil, - input_serde: T.unsafe(nil), output_serde: T.unsafe(nil)); end - - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end - def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, - headers: nil, input_serde: T.unsafe(nil)); end - - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end - def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil, - input_serde: T.unsafe(nil), output_serde: T.unsafe(nil)); end - - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end - def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, - headers: nil, input_serde: T.unsafe(nil)); end - - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped - ).returns(DurableCallFuture) - end - def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil, - input_serde: T.unsafe(nil), output_serde: T.unsafe(nil)); end - - sig do - params( - service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol), - key: String, arg: T.untyped, delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]), - input_serde: T.untyped - ).returns(SendHandle) - end - def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, - headers: nil, input_serde: T.unsafe(nil)); end - - sig do - params( - service: String, handler: String, arg: String, - key: T.nilable(String), idempotency_key: T.nilable(String), - headers: T.nilable(T::Hash[String, String]) - ).returns(DurableCallFuture) - end - def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil); end - - sig do - params( - service: String, handler: String, arg: String, - key: T.nilable(String), delay: T.nilable(Numeric), - idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]) - ).returns(SendHandle) - end - def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil); end - - sig { params(serde: T.untyped).returns([String, DurableFuture]) } - def awakeable(serde: Restate::JsonSerde); end - - sig { params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void } - def resolve_awakeable(awakeable_id, payload, serde: Restate::JsonSerde); end - - sig { params(awakeable_id: String, message: String, code: Integer).void } - def reject_awakeable(awakeable_id, message, code: 500); end - - sig { params(invocation_id: String).void } - def cancel_invocation(invocation_id); end - - sig { params(futures: DurableFuture).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) } - def wait_any(*futures); end - - sig { returns(T.untyped) } - def request; end - - sig { returns(String) } - def key; end - end - - # VirtualObject shared handler context (read-only state). - module ObjectSharedContext - include Context - - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def get(name, serde: Restate::JsonSerde); end - - sig { params(name: String, serde: T.untyped).returns(DurableFuture) } - def get_async(name, serde: Restate::JsonSerde); end - - sig { returns(T.untyped) } - def state_keys; end - - sig { returns(DurableFuture) } - def state_keys_async; end - end - - # VirtualObject exclusive handler context (full state access). - module ObjectContext - include ObjectSharedContext - - sig { params(name: String, value: T.untyped, serde: T.untyped).void } - def set(name, value, serde: Restate::JsonSerde); end - - sig { params(name: String).void } - def clear(name); end - - sig { void } - def clear_all; end - end - - # Workflow shared handler context (read-only state + promises). - module WorkflowSharedContext - include ObjectSharedContext - - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def promise(name, serde: Restate::JsonSerde); end - - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def peek_promise(name, serde: Restate::JsonSerde); end - - sig { params(name: String, payload: T.untyped, serde: T.untyped).void } - def resolve_promise(name, payload, serde: Restate::JsonSerde); end - - sig { params(name: String, message: String, code: Integer).void } - def reject_promise(name, message, code: 500); end - end - - # Workflow main handler context (full state + promises). - module WorkflowContext - include ObjectContext - - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def promise(name, serde: Restate::JsonSerde); end - - sig { params(name: String, serde: T.untyped).returns(T.untyped) } - def peek_promise(name, serde: Restate::JsonSerde); end - - sig { params(name: String, payload: T.untyped, serde: T.untyped).void } - def resolve_promise(name, payload, serde: Restate::JsonSerde); end - - sig { params(name: String, message: String, code: Integer).void } - def reject_promise(name, message, code: 500); end - end - - # Stateless service base class. - class Service - sig { returns(ServiceCallProxy) } - def self.call; end - - sig { params(delay: T.nilable(Numeric)).returns(ServiceSendProxy) } - def self.send!(delay: nil); end - end - - # Keyed virtual object base class. - class VirtualObject - sig { params(key: String).returns(ServiceCallProxy) } - def self.call(key); end - - sig { params(key: String, delay: T.nilable(Numeric)).returns(ServiceSendProxy) } - def self.send!(key, delay: nil); end - - sig { params(name: Symbol, default: T.untyped, serde: T.untyped).void } - def self.state(name, default: nil, serde: nil); end - end - - # Durable workflow base class. - class Workflow - sig { params(key: String).returns(ServiceCallProxy) } - def self.call(key); end - - sig { params(key: String, delay: T.nilable(Numeric)).returns(ServiceSendProxy) } - def self.send!(key, delay: nil); end - - sig { params(name: Symbol, default: T.untyped, serde: T.untyped).void } - def self.state(name, default: nil, serde: nil); end - end - - # Proxy for fluent durable calls. - class ServiceCallProxy; end - - # Proxy for fluent fire-and-forget sends. - class ServiceSendProxy; end - - # Global SDK configuration. - class Config - sig { returns(String) } - attr_accessor :ingress_url - - sig { returns(String) } - attr_accessor :admin_url - - sig { returns(T::Hash[String, String]) } - attr_accessor :ingress_headers - - sig { returns(T::Hash[String, String]) } - attr_accessor :admin_headers - end - - # Configure the SDK globally. - sig { params(block: T.proc.params(arg0: Config).void).void } - def self.configure(&block); end - - # Returns the global configuration. - sig { returns(Config) } - def self.config; end - - # Returns a pre-configured Client using the global config. - sig { returns(Client) } - def self.client; end - - # HTTP client for invoking Restate services and managing the runtime. - class Client - sig do - params(ingress_url: String, admin_url: String, - ingress_headers: T::Hash[String, String], - admin_headers: T::Hash[String, String]).void - end - def initialize(ingress_url: 'http://localhost:8080', admin_url: 'http://localhost:9070', - ingress_headers: {}, admin_headers: {}); end - - sig { params(service: T.any(String, T::Class[T.anything])).returns(ClientServiceProxy) } - def service(service); end - - sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) } - def object(service, key); end - - sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) } - def workflow(service, key); end - - sig { params(awakeable_id: String, payload: T.untyped).void } - def resolve_awakeable(awakeable_id, payload); end - - sig { params(awakeable_id: String, message: String, code: Integer).void } - def reject_awakeable(awakeable_id, message, code: 500); end - - sig { params(invocation_id: String).void } - def cancel_invocation(invocation_id); end - - sig { params(invocation_id: String).void } - def kill_invocation(invocation_id); end - - end - - # Proxy for HTTP client calls. - class ClientServiceProxy; end - - class Endpoint - sig { params(services: T.untyped).void } - def bind(*services); end - - sig { void } - def streaming_protocol; end - - sig { void } - def request_response_protocol; end - - sig { params(key: String).void } - def identity_key(key); end - - sig { params(klass: T.untyped, args: T.untyped, kwargs: T.untyped).returns(T.self_type) } - def use(klass, *args, **kwargs); end - - sig { returns(T.untyped) } - def app; end - end - - module JsonSerde; end - module BytesSerde; end -end diff --git a/restate-sdk.gemspec b/restate-sdk.gemspec index 8f53e73..5556e47 100644 --- a/restate-sdk.gemspec +++ b/restate-sdk.gemspec @@ -15,7 +15,6 @@ Gem::Specification.new do |spec| spec.files = Dir[ 'lib/**/*.rb', 'ext/**/*.{rs,toml,rb}', - 'rbi/**/*.rbi', 'Cargo.*', 'LICENSE', 'README.md' @@ -26,6 +25,5 @@ Gem::Specification.new do |spec| spec.add_dependency 'async', '~> 2.0' spec.add_dependency 'rack', '>= 2.0' - spec.add_dependency 'sorbet-runtime' spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/sorbet/config b/sorbet/config deleted file mode 100644 index 9c5166b..0000000 --- a/sorbet/config +++ /dev/null @@ -1,13 +0,0 @@ ---dir -. ---ignore=ext/ ---ignore=spec/ ---ignore=.bundle/ ---ignore=.gem/ ---ignore=tmp/ ---ignore=pkg/ ---ignore=vendor/ ---ignore=template/ ---ignore=test-services/ ---ignore=middleware_example/ ---ignore=/rbi/ diff --git a/sorbet/rbi/shims/async.rbi b/sorbet/rbi/shims/async.rbi deleted file mode 100644 index 5165350..0000000 --- a/sorbet/rbi/shims/async.rbi +++ /dev/null @@ -1,46 +0,0 @@ -# typed: true - -module Async - class Queue - sig { void } - def initialize; end - - sig { params(item: T.untyped).void } - def enqueue(item); end - - sig { returns(T.untyped) } - def dequeue; end - end - - module HTTP - class Endpoint - sig { params(url: String, options: T.untyped).returns(T.untyped) } - def self.parse(url, **options); end - end - end -end - -module Falcon - class Server - sig { params(middleware: T.untyped, endpoint: T.untyped).void } - def initialize(middleware, endpoint); end - - sig { params(app: T.untyped, options: T.untyped).returns(T.untyped) } - def self.middleware(app, **options); end - - sig { void } - def run; end - end -end - -module Testcontainers - class DockerContainer - sig { params(image: String).void } - def initialize(image); end - end -end - -module Kernel - sig { params(block: T.proc.void).returns(T.untyped) } - def Async(&block); end -end diff --git a/sorbet/rbi/shims/dry_struct.rbi b/sorbet/rbi/shims/dry_struct.rbi deleted file mode 100644 index b97293f..0000000 --- a/sorbet/rbi/shims/dry_struct.rbi +++ /dev/null @@ -1,17 +0,0 @@ -# typed: true - -module Dry - class Struct - sig { params(args: T.untyped).void } - def initialize(**args); end - - sig { returns(T::Hash[Symbol, T.untyped]) } - def to_h; end - - sig { returns(T.untyped) } - def self.schema; end - - sig { returns(T::Array[Symbol]) } - def self.attribute_names; end - end -end diff --git a/sorbet/rbi/shims/restate_internal.rbi b/sorbet/rbi/shims/restate_internal.rbi deleted file mode 100644 index 0cc9331..0000000 --- a/sorbet/rbi/shims/restate_internal.rbi +++ /dev/null @@ -1,259 +0,0 @@ -# typed: true - -module Restate - module Internal - SDK_VERSION = T.let(T.unsafe(nil), String) - CANCEL_NOTIFICATION_HANDLE = T.let(T.unsafe(nil), Integer) - - class VMError < RuntimeError; end - class IdentityKeyError < RuntimeError; end - class IdentityVerificationError < RuntimeError; end - - class Header - sig { params(key: String, value: String).void } - def initialize(key, value); end - - sig { returns(String) } - def key; end - - sig { returns(String) } - def value; end - end - - class ResponseHead - sig { returns(Integer) } - def status_code; end - - sig { returns(T::Array[T::Array[String]]) } - def headers; end - end - - class Failure - sig { params(code: Integer, message: String, stacktrace: T.nilable(String)).void } - def initialize(code, message, stacktrace = nil); end - - sig { returns(Integer) } - def code; end - - sig { returns(String) } - def message; end - - sig { returns(T.nilable(String)) } - def stacktrace; end - end - - class Void; end - class Suspended; end - - class StateKeys - sig { returns(T::Array[String]) } - def keys; end - end - - class Input - sig { returns(String) } - def invocation_id; end - - sig { returns(Integer) } - def random_seed; end - - sig { returns(String) } - def key; end - - sig { returns(T::Array[Header]) } - def headers; end - - sig { returns(String) } - def input; end - end - - class ExponentialRetryConfig - sig do - params( - initial_interval: T.nilable(Integer), - max_attempts: T.nilable(Integer), - max_duration: T.nilable(Integer), - max_interval: T.nilable(Integer), - factor: T.nilable(Float) - ).void - end - def initialize(initial_interval = nil, max_attempts = nil, max_duration = nil, max_interval = nil, - factor = nil); end - - sig { returns(T.nilable(Integer)) } - def initial_interval; end - - sig { returns(T.nilable(Integer)) } - def max_attempts; end - - sig { returns(T.nilable(Integer)) } - def max_duration; end - - sig { returns(T.nilable(Integer)) } - def max_interval; end - - sig { returns(T.nilable(Float)) } - def factor; end - end - - class DoProgressAnyCompleted; end - class DoProgressReadFromInput; end - - class DoProgressExecuteRun - sig { returns(Integer) } - def handle; end - end - - class DoProgressCancelSignalReceived; end - class DoWaitForPendingRun; end - - class CallHandle - sig { returns(Integer) } - def invocation_id_handle; end - - sig { returns(Integer) } - def result_handle; end - end - - class IdentityVerifier - sig { params(keys: T::Array[String]).void } - def initialize(keys); end - - sig { params(headers: T::Array[T::Array[String]], path: String).void } - def verify(headers, path); end - end - - class VM - sig { params(headers: T::Array[T::Array[String]]).void } - def initialize(headers); end - - sig { returns(ResponseHead) } - def get_response_head; end - - sig { params(buffer: String).void } - def notify_input(buffer); end - - sig { void } - def notify_input_closed; end - - sig { params(error: String, stacktrace: T.untyped).void } - def notify_error(error, stacktrace = nil); end - - sig { returns(T.nilable(String)) } - def take_output; end - - sig { returns(T::Boolean) } - def is_ready_to_execute; end - - sig { params(handle: Integer).returns(T::Boolean) } - def is_completed(handle); end - - sig { params(handles: T::Array[Integer]).returns(T.untyped) } - def do_progress(handles); end - - sig { params(handle: Integer).returns(T.untyped) } - def take_notification(handle); end - - sig { returns(Input) } - def sys_input; end - - sig { params(key: String).returns(Integer) } - def sys_get_state(key); end - - sig { returns(Integer) } - def sys_get_state_keys; end - - sig { params(key: String, buffer: String).void } - def sys_set_state(key, buffer); end - - sig { params(key: String).void } - def sys_clear_state(key); end - - sig { void } - def sys_clear_all_state; end - - sig { params(millis: Integer, name: T.nilable(String)).returns(Integer) } - def sys_sleep(millis, name = nil); end - - sig do - params( - service: String, - handler: String, - buffer: String, - key: T.nilable(String), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Array[T::Array[String]]) - ).returns(CallHandle) - end - def sys_call(service, handler, buffer, key = nil, idempotency_key = nil, headers = nil); end - - sig do - params( - service: String, - handler: String, - buffer: String, - key: T.nilable(String), - delay: T.nilable(Integer), - idempotency_key: T.nilable(String), - headers: T.nilable(T::Array[T::Array[String]]) - ).returns(Integer) - end - def sys_send(service, handler, buffer, key = nil, delay = nil, idempotency_key = nil, headers = nil); end - - sig { params(name: String).returns(Integer) } - def sys_run(name); end - - sig { params(handle: Integer, buffer: String).void } - def propose_run_completion_success(handle, buffer); end - - sig { params(handle: Integer, failure: Failure).void } - def propose_run_completion_failure(handle, failure); end - - sig do - params( - handle: Integer, - failure: Failure, - attempt_duration: Integer, - config: ExponentialRetryConfig - ).void - end - def propose_run_completion_failure_transient(handle, failure, attempt_duration, config); end - - sig { params(buffer: String).void } - def sys_write_output_success(buffer); end - - sig { params(failure: Failure).void } - def sys_write_output_failure(failure); end - - sig { void } - def sys_end; end - - sig { returns(T::Boolean) } - def is_replaying; end - - sig { returns(T.untyped) } - def sys_awakeable; end - - sig { params(id: String, buffer: String).void } - def sys_complete_awakeable_success(id, buffer); end - - sig { params(id: String, failure: Failure).void } - def sys_complete_awakeable_failure(id, failure); end - - sig { params(key: String).returns(Integer) } - def sys_get_promise(key); end - - sig { params(key: String).returns(Integer) } - def sys_peek_promise(key); end - - sig { params(key: String, buffer: String).returns(Integer) } - def sys_complete_promise_success(key, buffer); end - - sig { params(key: String, failure: Failure).returns(Integer) } - def sys_complete_promise_failure(key, failure); end - - sig { params(invocation_id: String).void } - def sys_cancel_invocation(invocation_id); end - end - end -end diff --git a/sorbet/rbi/shims/tapioca.rbi b/sorbet/rbi/shims/tapioca.rbi deleted file mode 100644 index 89a342e..0000000 --- a/sorbet/rbi/shims/tapioca.rbi +++ /dev/null @@ -1,14 +0,0 @@ -# typed: true - -module Tapioca - module Dsl - class Compiler - def self.all_classes; end - def self.gather_constants; end - def root; end - def constant; end - def create_param(name, type:); end - def decorate; end - end - end -end diff --git a/spec/harness_spec.rb b/spec/harness_spec.rb index 8607441..fc17221 100644 --- a/spec/harness_spec.rb +++ b/spec/harness_spec.rb @@ -73,19 +73,6 @@ def do_work(input) end end -class TStructRequest < T::Struct - const :name, String - const :greeting, T.nilable(String) -end - -class TStructGreeter < Restate::Service - handler :greet, input: TStructRequest, output: String - def greet(request) - greeting = request.greeting || "Hello" - "#{greeting}, #{request.name}!" - end -end - class TypedGreeter < Restate::Service handler :greet, input: GreetingRequest, output: String def greet(request) @@ -177,7 +164,7 @@ def post_json(base_url, path, body, headers: {}) before(:all) do @harness = Restate::Testing::RestateTestHarness.new( TestGreeter, TestCounter, TestWorker, TestOrchestrator, TestRunSync, TestFiberLocalCtx, - TStructGreeter, TypedGreeter, MiddlewareTestService, + TypedGreeter, MiddlewareTestService, TestDeclCounter, TestFluentWorker, TestFluentOrchestrator ) do |endpoint| endpoint.use(TestHeaderMiddleware) @@ -229,19 +216,6 @@ def post_json(base_url, path, body, headers: {}) expect(JSON.parse(response.body)).to eq("orchestrated:processed:hello") end - it "handles typed T::Struct input" do - response = post_json(@harness.ingress_url, "/TStructGreeter/greet", { "name" => "World" }) - expect(response.code).to eq("200") - expect(JSON.parse(response.body)).to eq("Hello, World!") - end - - it "handles typed T::Struct input with optional field" do - response = post_json(@harness.ingress_url, "/TStructGreeter/greet", - { "name" => "World", "greeting" => "Hey" }) - expect(response.code).to eq("200") - expect(JSON.parse(response.body)).to eq("Hey, World!") - end - it "handles typed dry-struct input" do response = post_json(@harness.ingress_url, "/TypedGreeter/greet", { "name" => "World" }) expect(response.code).to eq("200") diff --git a/template/AGENTS.md b/template/AGENTS.md index 2abce7c..7e71551 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -80,14 +80,20 @@ Restate.service_send(MyService, :handler, arg) Restate.object_send(MyObject, :handler, 'key', arg, delay: 60.0) ``` -### Typed handlers with T::Struct +### Typed handlers with Dry::Struct -Use Sorbet's `T::Struct` for typed input/output with automatic JSON Schema generation: +Use [dry-struct](https://dry-rb.org/gems/dry-struct/) for typed input/output with automatic JSON Schema generation: ```ruby -class MyRequest < T::Struct - const :name, String - const :age, T.nilable(Integer) +require 'dry-struct' + +module Types + include Dry.Types() +end + +class MyRequest < Dry::Struct + attribute :name, Types::String + attribute? :age, Types::Integer # optional attribute end class MyService < Restate::Service @@ -98,7 +104,7 @@ class MyService < Restate::Service end ``` -Supported types: `String`, `Integer`, `Float`, `T::Boolean`, `T.nilable(...)`, `T::Array[...]`, `T::Hash[...]`, nested `T::Struct`. +Supported types: `Types::String`, `Types::Integer`, `Types::Float`, `Types::Bool`, `.optional`, `Types::Array.of(...)`, nested `Dry::Struct`. ### Sleep (durable timer) @@ -140,7 +146,7 @@ run endpoint.app # in config.ru ## Code style -- Use `T::Struct` for typed handler input/output +- Use primitive types or `Dry::Struct` for typed handler input/output - Use `Restate.*` module methods for all operations (run, sleep, get, set, service_call, etc.) - Use `run_sync` for sequential side effects, `run` + `.await` for fan-out - Catch `Restate::TerminalError` specifically, never bare `rescue => e` diff --git a/template/CLAUDE.md b/template/CLAUDE.md index 934896f..7e71551 100644 --- a/template/CLAUDE.md +++ b/template/CLAUDE.md @@ -63,23 +63,37 @@ Restate.state_keys # List keys ### Service communication ```ruby -# Synchronous call (durable) +# Fluent call API (recommended) +result = Worker.call.process(task).await +result = Counter.call("key").add(5).await + +# Fluent fire-and-forget +Worker.send!.process(task) +Worker.send!(delay: 60).process(task) + +# Explicit calls result = Restate.service_call(MyService, :handler, arg).await result = Restate.object_call(MyObject, :handler, 'key', arg).await -# Fire-and-forget +# Explicit fire-and-forget Restate.service_send(MyService, :handler, arg) Restate.object_send(MyObject, :handler, 'key', arg, delay: 60.0) ``` -### Typed handlers with T::Struct +### Typed handlers with Dry::Struct -Use Sorbet's `T::Struct` for typed input/output with automatic JSON Schema generation: +Use [dry-struct](https://dry-rb.org/gems/dry-struct/) for typed input/output with automatic JSON Schema generation: ```ruby -class MyRequest < T::Struct - const :name, String - const :age, T.nilable(Integer) +require 'dry-struct' + +module Types + include Dry.Types() +end + +class MyRequest < Dry::Struct + attribute :name, Types::String + attribute? :age, Types::Integer # optional attribute end class MyService < Restate::Service @@ -90,7 +104,7 @@ class MyService < Restate::Service end ``` -Supported types: `String`, `Integer`, `Float`, `T::Boolean`, `T.nilable(...)`, `T::Array[...]`, `T::Hash[...]`, nested `T::Struct`. +Supported types: `Types::String`, `Types::Integer`, `Types::Float`, `Types::Bool`, `.optional`, `Types::Array.of(...)`, nested `Dry::Struct`. ### Sleep (durable timer) @@ -132,7 +146,7 @@ run endpoint.app # in config.ru ## Code style -- Use `T::Struct` for typed handler input/output +- Use primitive types or `Dry::Struct` for typed handler input/output - Use `Restate.*` module methods for all operations (run, sleep, get, set, service_call, etc.) - Use `run_sync` for sequential side effects, `run` + `.await` for fan-out - Catch `Restate::TerminalError` specifically, never bare `rescue => e` diff --git a/template/codex.md b/template/codex.md index 2abce7c..7e71551 100644 --- a/template/codex.md +++ b/template/codex.md @@ -80,14 +80,20 @@ Restate.service_send(MyService, :handler, arg) Restate.object_send(MyObject, :handler, 'key', arg, delay: 60.0) ``` -### Typed handlers with T::Struct +### Typed handlers with Dry::Struct -Use Sorbet's `T::Struct` for typed input/output with automatic JSON Schema generation: +Use [dry-struct](https://dry-rb.org/gems/dry-struct/) for typed input/output with automatic JSON Schema generation: ```ruby -class MyRequest < T::Struct - const :name, String - const :age, T.nilable(Integer) +require 'dry-struct' + +module Types + include Dry.Types() +end + +class MyRequest < Dry::Struct + attribute :name, Types::String + attribute? :age, Types::Integer # optional attribute end class MyService < Restate::Service @@ -98,7 +104,7 @@ class MyService < Restate::Service end ``` -Supported types: `String`, `Integer`, `Float`, `T::Boolean`, `T.nilable(...)`, `T::Array[...]`, `T::Hash[...]`, nested `T::Struct`. +Supported types: `Types::String`, `Types::Integer`, `Types::Float`, `Types::Bool`, `.optional`, `Types::Array.of(...)`, nested `Dry::Struct`. ### Sleep (durable timer) @@ -140,7 +146,7 @@ run endpoint.app # in config.ru ## Code style -- Use `T::Struct` for typed handler input/output +- Use primitive types or `Dry::Struct` for typed handler input/output - Use `Restate.*` module methods for all operations (run, sleep, get, set, service_call, etc.) - Use `run_sync` for sequential side effects, `run` + `.await` for fan-out - Catch `Restate::TerminalError` specifically, never bare `rescue => e` diff --git a/template/config.ru b/template/config.ru index 09052ca..518a79c 100644 --- a/template/config.ru +++ b/template/config.ru @@ -1,4 +1,3 @@ -# typed: false # frozen_string_literal: true # @@ -11,7 +10,7 @@ # Invoke: # curl localhost:8080/Greeter/greet \ # -H 'content-type: application/json' \ -# -d '{"name": "World"}' +# -d '"World"' # require_relative 'greeter' diff --git a/template/greeter.rb b/template/greeter.rb index b0e6f69..88b66f4 100644 --- a/template/greeter.rb +++ b/template/greeter.rb @@ -1,25 +1,14 @@ -# typed: true # frozen_string_literal: true require 'restate' -class GreetingRequest < T::Struct - const :name, String -end - -class GreetingResponse < T::Struct # rubocop:disable Style/OneClassPerFile - const :message, String -end - -class Greeter < Restate::Service # rubocop:disable Style/OneClassPerFile - handler :greet, input: GreetingRequest, output: GreetingResponse - # @param request [GreetingRequest] - # @return [GreetingResponse] - def greet(request) - message = Restate.run_sync('build-greeting') do - "Hello, #{request.name}!" +class Greeter < Restate::Service + handler :greet, input: String, output: String + # @param name [String] + # @return [String] + def greet(name) + Restate.run_sync('build-greeting') do + "Hello, #{name}!" end - - GreetingResponse.new(message: message) end end From 39e3066df502646c9d224793012e7ebf04450a8b Mon Sep 17 00:00:00 2001 From: igalshilman Date: Fri, 20 Mar 2026 20:00:45 +0100 Subject: [PATCH 3/3] Remove Sorbet as a runtime dependency, keep as dev-only Strip all Sorbet type annotations (T.let, T.sig, extend T::Sig) from runtime code so consumers never pull in sorbet-runtime. Sorbet remains as a dev-only dependency for internal static analysis via `srb tc`. Add hand-written RBS signatures (sig/restate.rbs) and a Steepfile so consumers get IDE completions via Steep/Ruby LSP. --- Gemfile | 2 + Gemfile.lock | 66 +++++++++ Makefile | 9 +- Steepfile | 39 +++++ lib/restate.rb | 1 + lib/restate/client.rb | 9 +- lib/restate/config.rb | 1 + lib/restate/context.rb | 1 + lib/restate/discovery.rb | 1 + lib/restate/durable_future.rb | 1 + lib/restate/endpoint.rb | 1 + lib/restate/errors.rb | 1 + lib/restate/handler.rb | 1 + lib/restate/serde.rb | 1 + lib/restate/server.rb | 1 + lib/restate/server_context.rb | 1 + lib/restate/service.rb | 1 + lib/restate/service_dsl.rb | 1 + lib/restate/service_proxy.rb | 1 + lib/restate/testing.rb | 1 + lib/restate/version.rb | 1 + lib/restate/virtual_object.rb | 1 + lib/restate/vm.rb | 1 + lib/restate/workflow.rb | 1 + restate-sdk.gemspec | 1 + sig/restate.rbs | 196 ++++++++++++++++++++++++++ sorbet/config | 12 ++ sorbet/rbi/shims/async.rbi | 54 +++++++ sorbet/rbi/shims/dry.rbi | 8 ++ sorbet/rbi/shims/restate_internal.rbi | 78 ++++++++++ sorbet/rbi/shims/service_dsl.rbi | 15 ++ 31 files changed, 502 insertions(+), 6 deletions(-) create mode 100644 Steepfile create mode 100644 sig/restate.rbs create mode 100644 sorbet/config create mode 100644 sorbet/rbi/shims/async.rbi create mode 100644 sorbet/rbi/shims/dry.rbi create mode 100644 sorbet/rbi/shims/restate_internal.rbi create mode 100644 sorbet/rbi/shims/service_dsl.rbi diff --git a/Gemfile b/Gemfile index 5b123f6..f00eddd 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,8 @@ group :development, :test do gem 'falcon', '~> 0.47', require: false gem 'rspec', '~> 3.12' gem 'rubocop', require: false + gem 'sorbet', require: false + gem 'steep', require: false gem 'testcontainers-core', require: false # For middleware_example/ diff --git a/Gemfile.lock b/Gemfile.lock index c9197fe..f02c7d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,19 @@ PATH GEM remote: https://rubygems.org/ specs: + activesupport (8.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) @@ -43,14 +56,17 @@ GEM base64 (0.3.0) bigdecimal (4.0.1) concurrent-ruby (1.3.6) + connection_pool (3.0.2) console (1.34.3) fiber-annotation fiber-local (~> 1.1) json + csv (3.3.5) diff-lcs (1.6.2) docker-api (2.4.0) excon (>= 0.64.0) multi_json + drb (2.2.3) dry-core (1.2.0) concurrent-ruby (~> 1.0) logger @@ -88,10 +104,15 @@ GEM protocol-http (~> 0.31) protocol-rack (~> 0.7) samovar (~> 2.3) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-linux-gnu) fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage fiber-storage (1.0.1) + fileutils (1.8.0) + i18n (1.14.8) + concurrent-ruby (~> 1.0) ice_nine (0.11.2) io-endpoint (0.17.2) io-event (1.14.4) @@ -102,13 +123,21 @@ GEM bigdecimal (>= 3.1, < 5) language_server-protocol (3.17.0.5) lint_roller (1.1.0) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) localhost (1.7.0) logger (1.7.0) mapping (1.1.3) mcp (0.8.0) json-schema (>= 4.1) metrics (0.15.0) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) multi_json (1.19.1) + mutex_m (0.3.0) openssl (4.0.1) opentelemetry-api (1.8.0) logger @@ -148,8 +177,14 @@ GEM rake-compiler (1.3.1) rake rake-compiler-dock (1.11.0) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) rb_sys (0.9.124) rake-compiler-dock (= 1.11.0) + rbs (3.10.3) + logger + tsort regexp_parser (2.11.3) rspec (3.13.2) rspec-core (~> 3.13.0) @@ -183,13 +218,42 @@ GEM samovar (2.4.1) console (~> 1.0) mapping (~> 1.0) + securerandom (0.4.1) + sorbet (0.6.13051) + sorbet-static (= 0.6.13051) + sorbet-static (0.6.13051-universal-darwin) + sorbet-static (0.6.13051-x86_64-linux) + steep (1.10.0) + activesupport (>= 5.1) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.17.0.4, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + mutex_m (>= 0.3.0) + parser (>= 3.1) + rainbow (>= 2.2.2, < 4.0) + rbs (~> 3.9) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 5) + uri (>= 0.12.0) string-format (0.2.0) + strscan (3.1.7) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) testcontainers-core (0.2.0) docker-api (~> 2.2) traces (0.18.2) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) + uri (1.1.1) zeitwerk (2.7.5) PLATFORMS @@ -208,6 +272,8 @@ DEPENDENCIES restate-sdk! rspec (~> 3.12) rubocop + sorbet + steep testcontainers-core BUNDLED WITH diff --git a/Makefile b/Makefile index 2feeadb..c2a5e66 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build compile test test-harness test-integration clean fmt check install lint lint-fix verify +.PHONY: build compile test test-harness test-integration clean fmt check install lint lint-fix typecheck verify # Build the native extension and compile build: compile @@ -25,6 +25,11 @@ lint: lint-fix: bundle exec rubocop -A +# Type check — Steep (public API, shipped RBS) + Sorbet (internal, dev-only) +typecheck: + bundle exec steep check + bundle exec srb tc + # Check Rust code compiles check: cargo check @@ -47,7 +52,7 @@ gem: compile gem build restate-sdk.gemspec # Build, lint, and run unit tests (no integration tests) -verify: compile lint test-harness +verify: compile lint typecheck test-harness # Run everything (install, compile, test, lint) all: install compile test lint diff --git a/Steepfile b/Steepfile new file mode 100644 index 0000000..44f2fd0 --- /dev/null +++ b/Steepfile @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Steepfile — configuration for the Steep type checker. +# Run: bundle exec steep check + +target :lib do + signature 'sig' + + check 'lib/restate/config.rb' + check 'lib/restate/errors.rb' + check 'lib/restate/durable_future.rb' + check 'lib/restate/endpoint.rb' + check 'lib/restate/service_proxy.rb' + check 'lib/restate/client.rb' + + # Files with heavy metaprogramming — skip for now + # check "lib/restate.rb" # module_function pattern + # check "lib/restate/service.rb" # extend ServiceDSL + # check "lib/restate/virtual_object.rb" + # check "lib/restate/workflow.rb" + # check "lib/restate/service_dsl.rb" # define_method + # check "lib/restate/server_context.rb" + # check "lib/restate/server.rb" + # check "lib/restate/vm.rb" + # check "lib/restate/context.rb" + # check "lib/restate/discovery.rb" + # check "lib/restate/serde.rb" + # check "lib/restate/handler.rb" + # check "lib/restate/testing.rb" + + library 'json' + library 'net-http' + library 'uri' + + # method_missing proxies can't be fully typed + configure_code_diagnostics do |hash| + hash[Steep::Diagnostic::Ruby::MethodArityMismatch] = :information + end +end diff --git a/lib/restate.rb b/lib/restate.rb index a563fee..a81a779 100644 --- a/lib/restate.rb +++ b/lib/restate.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true require_relative 'restate/version' diff --git a/lib/restate/client.rb b/lib/restate/client.rb index c71c439..c1f9363 100644 --- a/lib/restate/client.rb +++ b/lib/restate/client.rb @@ -1,3 +1,4 @@ +# typed: false # frozen_string_literal: true require 'net/http' @@ -84,7 +85,7 @@ def kill_invocation(invocation_id) def resolve_name(service) if service.is_a?(Class) && service.respond_to?(:service_name) - service.service_name + service.service_name # steep:ignore NoMethod else service.to_s end @@ -96,7 +97,7 @@ def post_ingress(path, body) # rubocop:disable Metrics/AbcSize request['Content-Type'] = 'application/json' @ingress_headers.each { |k, v| request[k] = v } request.body = JSON.generate(body) if body - response = Net::HTTP.start(uri.hostname, uri.port, + response = Net::HTTP.start(uri.hostname, uri.port, # steep:ignore ArgumentTypeMismatch use_ssl: uri.scheme == 'https', read_timeout: 30) { |http| http.request(request) } Kernel.raise "Restate ingress error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess) @@ -109,7 +110,7 @@ def post_admin(path, body) # rubocop:disable Metrics/AbcSize request['Content-Type'] = 'application/json' @admin_headers.each { |k, v| request[k] = v } request.body = JSON.generate(body) if body - response = Net::HTTP.start(uri.hostname, uri.port, + response = Net::HTTP.start(uri.hostname, uri.port, # steep:ignore ArgumentTypeMismatch use_ssl: uri.scheme == 'https', read_timeout: 30) { |http| http.request(request) } Kernel.raise "Restate admin error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess) @@ -141,7 +142,7 @@ def method_missing(handler_name, arg = nil) # rubocop:disable Metrics/AbcSize,Me request['Content-Type'] = 'application/json' @headers.each { |k, v| request[k] = v } request.body = JSON.generate(arg) - response = Net::HTTP.start(uri.hostname, uri.port, + response = Net::HTTP.start(uri.hostname, uri.port, # steep:ignore ArgumentTypeMismatch use_ssl: uri.scheme == 'https', read_timeout: 30) { |http| http.request(request) } Kernel.raise "Restate ingress error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess) diff --git a/lib/restate/config.rb b/lib/restate/config.rb index 74369cb..af7e5cf 100644 --- a/lib/restate/config.rb +++ b/lib/restate/config.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/context.rb b/lib/restate/context.rb index dd254b4..9bd6a40 100644 --- a/lib/restate/context.rb +++ b/lib/restate/context.rb @@ -1,3 +1,4 @@ +# typed: false # frozen_string_literal: true # rubocop:disable Style/EmptyMethod diff --git a/lib/restate/discovery.rb b/lib/restate/discovery.rb index 3582653..3d0fd05 100644 --- a/lib/restate/discovery.rb +++ b/lib/restate/discovery.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true require 'json' diff --git a/lib/restate/durable_future.rb b/lib/restate/durable_future.rb index c1d0909..422235f 100644 --- a/lib/restate/durable_future.rb +++ b/lib/restate/durable_future.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/endpoint.rb b/lib/restate/endpoint.rb index 763e83e..d8aaf5e 100644 --- a/lib/restate/endpoint.rb +++ b/lib/restate/endpoint.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/errors.rb b/lib/restate/errors.rb index 19ecd3b..571b069 100644 --- a/lib/restate/errors.rb +++ b/lib/restate/errors.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/handler.rb b/lib/restate/handler.rb index fefb72e..e428bf7 100644 --- a/lib/restate/handler.rb +++ b/lib/restate/handler.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/serde.rb b/lib/restate/serde.rb index 75dcd92..4d1bc88 100644 --- a/lib/restate/serde.rb +++ b/lib/restate/serde.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true require 'json' diff --git a/lib/restate/server.rb b/lib/restate/server.rb index 77be4e0..53a2bec 100644 --- a/lib/restate/server.rb +++ b/lib/restate/server.rb @@ -1,3 +1,4 @@ +# typed: ignore # frozen_string_literal: true require 'async' diff --git a/lib/restate/server_context.rb b/lib/restate/server_context.rb index 40ff530..d45a999 100644 --- a/lib/restate/server_context.rb +++ b/lib/restate/server_context.rb @@ -1,3 +1,4 @@ +# typed: false # frozen_string_literal: true require 'async' diff --git a/lib/restate/service.rb b/lib/restate/service.rb index 6ef3c02..0f899a0 100644 --- a/lib/restate/service.rb +++ b/lib/restate/service.rb @@ -1,3 +1,4 @@ +# typed: false # frozen_string_literal: true module Restate diff --git a/lib/restate/service_dsl.rb b/lib/restate/service_dsl.rb index d5da037..916cf3e 100644 --- a/lib/restate/service_dsl.rb +++ b/lib/restate/service_dsl.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/service_proxy.rb b/lib/restate/service_proxy.rb index e53feac..39827b5 100644 --- a/lib/restate/service_proxy.rb +++ b/lib/restate/service_proxy.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/testing.rb b/lib/restate/testing.rb index 88bf74e..5f5f79a 100644 --- a/lib/restate/testing.rb +++ b/lib/restate/testing.rb @@ -1,3 +1,4 @@ +# typed: ignore # frozen_string_literal: true require 'restate' diff --git a/lib/restate/version.rb b/lib/restate/version.rb index f7f8bd9..476b112 100644 --- a/lib/restate/version.rb +++ b/lib/restate/version.rb @@ -1,3 +1,4 @@ +# typed: true # frozen_string_literal: true module Restate diff --git a/lib/restate/virtual_object.rb b/lib/restate/virtual_object.rb index a7a52b8..bad1d12 100644 --- a/lib/restate/virtual_object.rb +++ b/lib/restate/virtual_object.rb @@ -1,3 +1,4 @@ +# typed: false # frozen_string_literal: true module Restate diff --git a/lib/restate/vm.rb b/lib/restate/vm.rb index a14a570..723ba58 100644 --- a/lib/restate/vm.rb +++ b/lib/restate/vm.rb @@ -1,3 +1,4 @@ +# typed: ignore # frozen_string_literal: true begin diff --git a/lib/restate/workflow.rb b/lib/restate/workflow.rb index 94f0b59..449c2a8 100644 --- a/lib/restate/workflow.rb +++ b/lib/restate/workflow.rb @@ -1,3 +1,4 @@ +# typed: false # frozen_string_literal: true module Restate diff --git a/restate-sdk.gemspec b/restate-sdk.gemspec index 5556e47..b7bca60 100644 --- a/restate-sdk.gemspec +++ b/restate-sdk.gemspec @@ -14,6 +14,7 @@ Gem::Specification.new do |spec| spec.files = Dir[ 'lib/**/*.rb', + 'sig/**/*.rbs', 'ext/**/*.{rs,toml,rb}', 'Cargo.*', 'LICENSE', diff --git a/sig/restate.rbs b/sig/restate.rbs new file mode 100644 index 0000000..1a9a00f --- /dev/null +++ b/sig/restate.rbs @@ -0,0 +1,196 @@ +# Public API type signatures for the Restate Ruby SDK. +# Used by Steep for static type checking and by Ruby LSP for IDE completion. + +module Restate + # ── Endpoint ── + + def self.endpoint: (*untyped services, ?protocol: String?, ?identity_keys: Array[String]?) -> Endpoint + def self.configure: () { (Config) -> void } -> void + def self.config: () -> Config + def self.client: () -> Client + + # ── Durable execution ── + + def self.run: (String name, ?serde: untyped, ?retry_policy: untyped, ?background: bool) { () -> untyped } -> DurableFuture + def self.run_sync: (String name, ?serde: untyped, ?retry_policy: untyped, ?background: bool) { () -> untyped } -> untyped + def self.sleep: (Numeric seconds) -> DurableFuture + + # ── State operations ── + + def self.get: (String name, ?serde: untyped) -> untyped + def self.get_async: (String name, ?serde: untyped) -> DurableFuture + def self.set: (String name, untyped value, ?serde: untyped) -> void + def self.clear: (String name) -> void + def self.clear_all: () -> void + def self.state_keys: () -> Array[String] + def self.state_keys_async: () -> DurableFuture + + # ── Service communication ── + + def self.service_call: (untyped service, (String | Symbol) handler, untyped arg, ?key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture + def self.service_send: (untyped service, (String | Symbol) handler, untyped arg, ?key: String?, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle + def self.object_call: (untyped service, (String | Symbol) handler, String key, untyped arg, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture + def self.object_send: (untyped service, (String | Symbol) handler, String key, untyped arg, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle + def self.workflow_call: (untyped service, (String | Symbol) handler, String key, untyped arg, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture + def self.workflow_send: (untyped service, (String | Symbol) handler, String key, untyped arg, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle + def self.generic_call: (String service, String handler, String arg, ?key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?) -> DurableCallFuture + def self.generic_send: (String service, String handler, String arg, ?key: String?, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?) -> SendHandle + + # ── Awakeables ── + + def self.awakeable: (?serde: untyped) -> [String, DurableFuture] + def self.resolve_awakeable: (String awakeable_id, untyped payload, ?serde: untyped) -> void + def self.reject_awakeable: (String awakeable_id, String message, ?code: Integer) -> void + + # ── Promises ── + + def self.promise: (String name, ?serde: untyped) -> untyped + def self.peek_promise: (String name, ?serde: untyped) -> untyped + def self.resolve_promise: (String name, untyped payload, ?serde: untyped) -> void + def self.reject_promise: (String name, String message, ?code: Integer) -> void + + # ── Futures / Metadata / Control ── + + def self.wait_any: (*DurableFuture futures) -> [Array[DurableFuture], Array[DurableFuture]] + def self.request: () -> untyped + def self.key: () -> String + def self.cancel_invocation: (String invocation_id) -> void + def self.fetch_context!: (?service_kind: String?, ?handler_kind: String?) -> untyped + + # ── Errors ── + + class TerminalError < StandardError + attr_reader status_code: Integer + def initialize: (?String message, ?status_code: Integer) -> void + end + + class SuspendedError < StandardError + def initialize: () -> void + end + + class InternalError < StandardError + def initialize: () -> void + end + + class DisconnectedError < StandardError + def initialize: () -> void + end + + # ── Futures ── + + class DurableFuture + attr_reader handle: Integer + def initialize: (untyped ctx, Integer handle, ?serde: untyped) -> void + def await: () -> untyped + def completed?: () -> bool + end + + class DurableCallFuture < DurableFuture + def initialize: (untyped ctx, Integer result_handle, Integer invocation_id_handle, output_serde: untyped) -> void + def invocation_id: () -> String + def cancel: () -> void + end + + class SendHandle + def initialize: (untyped ctx, Integer invocation_id_handle) -> void + def invocation_id: () -> String + def cancel: () -> void + end + + # ── Config ── + + class Config + attr_accessor ingress_url: String + attr_accessor admin_url: String + attr_accessor ingress_headers: Hash[String, String] + attr_accessor admin_headers: Hash[String, String] + def initialize: () -> void + end + + # ── Client ── + + class Client + def initialize: (?ingress_url: String, ?admin_url: String, ?ingress_headers: Hash[String, String], ?admin_headers: Hash[String, String]) -> void + def service: (untyped service) -> ClientServiceProxy + def object: (untyped service, String key) -> ClientServiceProxy + def workflow: (untyped service, String key) -> ClientServiceProxy + def resolve_awakeable: (String awakeable_id, untyped payload) -> void + def reject_awakeable: (String awakeable_id, String message, ?code: Integer) -> void + def cancel_invocation: (String invocation_id) -> void + def kill_invocation: (String invocation_id) -> void + + private + + def resolve_name: (untyped service) -> String + def post_ingress: (String path, untyped body) -> untyped + def post_admin: (String path, untyped body) -> untyped + def parse_response: (Net::HTTPResponse response) -> untyped + end + + class ClientServiceProxy + def initialize: (String base_url, String service_name, String? key, Hash[String, String] headers) -> void + end + + # ── Endpoint ── + + class Endpoint + attr_reader services: Hash[String, untyped] + attr_reader identity_keys: Array[String] + attr_accessor protocol: String? + attr_reader middleware: Array[untyped] + def initialize: () -> void + def bind: (*untyped svcs) -> self + def streaming_protocol: () -> self + def request_response_protocol: () -> self + def identity_key: (String key) -> self + def use: (untyped klass, *untyped args, **untyped kwargs) -> self + def app: () -> untyped + end + + # ── Service proxies ── + + class ServiceCallProxy + def initialize: (untyped service_class, ?key: String?, ?call_method: Symbol) -> void + end + + class ServiceSendProxy + def initialize: (untyped service_class, ?key: String?, ?send_method: Symbol, ?delay: Numeric?) -> void + end + + # ── Service classes (declared but not checked — heavy metaprogramming) ── + + class Service + end + + class VirtualObject + end + + class Workflow + end + + # ── Internal ── + + class Server + def initialize: (untyped endpoint) -> void + end + + module JsonSerde + def self.serialize: (untyped obj) -> String + def self.deserialize: (String? buf) -> untyped + def self.json_schema: () -> Hash[String, untyped]? + end + + module BytesSerde + def self.serialize: (untyped obj) -> String + def self.deserialize: (String? buf) -> String? + def self.json_schema: () -> Hash[String, untyped]? + end + + class AttemptFinishedEvent + def set?: () -> bool + def wait: () -> void + end + + NOT_SET: untyped + RunRetryPolicy: untyped +end diff --git a/sorbet/config b/sorbet/config new file mode 100644 index 0000000..0b97a2b --- /dev/null +++ b/sorbet/config @@ -0,0 +1,12 @@ +--dir +lib/ +--dir +sorbet/rbi/ +--ignore=template/ +--ignore=test-services/ +--ignore=middleware_example/ +--ignore=spec/ +--ignore=examples/ +--ignore=.bundle/ +--ignore=.gem/ +--suppress-error-code=5002 diff --git a/sorbet/rbi/shims/async.rbi b/sorbet/rbi/shims/async.rbi new file mode 100644 index 0000000..c0dad44 --- /dev/null +++ b/sorbet/rbi/shims/async.rbi @@ -0,0 +1,54 @@ +# typed: true + +# Minimal shims for Async/Falcon/Testcontainers — enough for Sorbet to check our code. + +module Kernel + def Async(&block); end +end + +module Async + class Queue + def initialize; end + def enqueue(item); end + def dequeue; end + end + + class HTTP + class Body + class Hijack + def initialize(&block); end + end + end + end +end + +module Falcon + class Server + def initialize(app, endpoint, **opts); end + def start; end + def stop; end + end + + module Endpoint + def self.parse(url); end + end +end + +module Testcontainers + class DockerContainer + def initialize(image, **opts); end + def start; end + def stop; end + def remove; end + def mapped_port(port); end + def host; end + def logs; end + def wait_for_logs(matcher, timeout:); end + def with_exposed_ports(*ports); end + def with_env(env); end + def wait_for_http(path:, port:, timeout:); end + + private + def _container_create_options; end + end +end diff --git a/sorbet/rbi/shims/dry.rbi b/sorbet/rbi/shims/dry.rbi new file mode 100644 index 0000000..aa5d14f --- /dev/null +++ b/sorbet/rbi/shims/dry.rbi @@ -0,0 +1,8 @@ +# typed: true + +# Minimal shim for Dry::Struct — enough for Sorbet to check serde.rb + +module Dry + class Struct + end +end diff --git a/sorbet/rbi/shims/restate_internal.rbi b/sorbet/rbi/shims/restate_internal.rbi new file mode 100644 index 0000000..039f429 --- /dev/null +++ b/sorbet/rbi/shims/restate_internal.rbi @@ -0,0 +1,78 @@ +# typed: true + +# Shim for the native Rust extension (ext/restate_internal/). +# These classes/methods are defined in Rust via Magnus and not visible to Sorbet. + +module Restate + module Internal + CANCEL_NOTIFICATION_HANDLE = T.let(0, Integer) + + class VM + def initialize(headers:, input:); end + def notify_input(bytes); end + def notify_input_closed; end + def is_ready_to_execute?; end + def sys_input; end + def do_progress(handles); end + def take_output; end + def take_notification(handle); end + def is_completed(handle); end + def sys_get_state(name); end + def sys_get_state_keys; end + def sys_set_state(name, value); end + def sys_clear_state(name); end + def sys_clear_all_state; end + def sys_sleep(millis); end + def sys_run(name); end + def sys_call(service:, handler:, parameter:, key:, idempotency_key:, headers:); end + def sys_send(service:, handler:, parameter:, key:, delay:, idempotency_key:, headers:); end + def sys_awakeable; end + def sys_complete_awakeable_success(id, value); end + def sys_complete_awakeable_failure(id, failure); end + def sys_get_promise(name); end + def sys_peek_promise(name); end + def sys_complete_promise_success(name, value); end + def sys_complete_promise_failure(name, failure); end + def sys_cancel_invocation(id); end + def sys_write_output_success(bytes); end + def sys_write_output_failure(failure); end + def sys_end; end + def notify_error(message, stacktrace); end + def propose_run_completion_success(handle, value); end + def propose_run_completion_failure(handle, failure); end + def propose_run_completion_transient(handle, failure:, attempt_duration_ms:, config:); end + def is_replaying; end + end + + class IdentityVerifier + def initialize(keys); end + def verify(path, headers); end + end + end + + # VM wrapper result types (defined in Ruby in vm.rb but referenced as bare constants + # from the native extension which Sorbet can't see through) + class VMError < StandardError; end + class Failure; end + class ExponentialRetryConfig; end + class StateKeys; end + class Void; end + class Suspended; end + class DoProgressAnyCompleted; end + class DoProgressReadFromInput; end + class DoProgressExecuteRun; end + class DoProgressCancelSignalReceived; end + class DoWaitForPendingRun; end + + # Additional types referenced by server_context.rb and endpoint.rb + class NotReady; end + class DoWaitPendingRun; end + class RunRetryConfig; end + class Server + def initialize(endpoint); end + end + + # Server-level types + class IdentityVerificationError < StandardError; end + SDK_VERSION = T.let('', String) +end diff --git a/sorbet/rbi/shims/service_dsl.rbi b/sorbet/rbi/shims/service_dsl.rbi new file mode 100644 index 0000000..962b934 --- /dev/null +++ b/sorbet/rbi/shims/service_dsl.rbi @@ -0,0 +1,15 @@ +# typed: true + +# ServiceDSL is extended into classes, so it has access to Module/Class methods. +# Sorbet doesn't know this when analyzing the module in isolation. + +module Restate + module ServiceDSL + def respond_to?(name, include_all = false); end + def _service_kind; end + def define_method(name, &block); end + def name; end + def allocate; end + def instance_method(name); end + end +end