restate-sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/restate.rb CHANGED
@@ -16,9 +16,21 @@ require_relative 'restate/server_context'
16
16
  require_relative 'restate/durable_future'
17
17
  require_relative 'restate/discovery'
18
18
  require_relative 'restate/endpoint'
19
+ require_relative 'restate/service_proxy'
20
+ require_relative 'restate/config'
21
+ require_relative 'restate/client'
19
22
 
20
23
  # Restate Ruby SDK — build resilient applications with durable execution.
21
- module Restate
24
+ #
25
+ # All handler-facing operations are available as module methods on +Restate+.
26
+ # Inside a handler, call +Restate.run_sync+, +Restate.sleep+, +Restate.get+,
27
+ # etc. directly — no context parameter needed.
28
+ #
29
+ # The context is stored in +Thread.current[]+ (fiber-scoped in Ruby 3.0+).
30
+ # We intentionally use +Thread.current[]+ rather than +Fiber[]+ (Ruby 3.2+)
31
+ # because +Thread.current[]+ is NOT inherited by child fibers, which prevents
32
+ # accidental context leaks when Async spawns child tasks for run blocks.
33
+ module Restate # rubocop:disable Metrics/ModuleLength
22
34
  extend T::Sig
23
35
 
24
36
  module_function
@@ -44,61 +56,42 @@ module Restate
44
56
  ep
45
57
  end
46
58
 
47
- # ── Fiber-local context accessors ──
48
- #
49
- # The SDK passes the context as the first argument to every handler.
50
- # It is also stored in fiber-local storage (Thread.current[], which is
51
- # fiber-scoped in Ruby). These methods retrieve it with the appropriate
52
- # type for IDE completion.
53
- #
54
- # Use these from nested helper methods that don't have +ctx+ in scope.
55
-
56
- # Returns the current context for a Service handler.
57
- # Raises if called outside a Restate handler.
58
- #
59
- # @return [Context]
60
- sig { returns(Context) }
61
- def current_context
62
- fetch_context!
63
- end
59
+ # ── Global configuration ──
64
60
 
65
- # Returns the current context for a VirtualObject exclusive handler.
66
- # Raises if not inside a VirtualObject exclusive handler.
61
+ # Configure the SDK globally. Settings are used by +Restate.client+.
67
62
  #
68
- # @return [ObjectContext]
69
- sig { returns(ObjectContext) }
70
- def current_object_context
71
- fetch_context!(service_kind: 'object', handler_kind: 'exclusive')
63
+ # @example
64
+ # Restate.configure do |c|
65
+ # c.ingress_url = "http://localhost:8080"
66
+ # c.admin_url = "http://localhost:9070"
67
+ # end
68
+ sig { params(_block: T.proc.params(arg0: Config).void).void }
69
+ def configure(&_block)
70
+ yield config
72
71
  end
73
72
 
74
- # Returns the current context for a VirtualObject shared handler.
75
- # Read-only state: +get+ and +state_keys+ only, no +set+/+clear+.
76
- # Raises if not inside a VirtualObject shared handler.
77
- #
78
- # @return [ObjectSharedContext]
79
- sig { returns(ObjectSharedContext) }
80
- def current_shared_context
81
- fetch_context!(service_kind: 'object', handler_kind: 'shared')
73
+ # Returns the global configuration. Creates a default one on first access.
74
+ sig { returns(Config) }
75
+ def config
76
+ @config = T.let(@config, T.nilable(Config)) unless defined?(@config)
77
+ @config ||= Config.new
82
78
  end
83
79
 
84
- # Returns the current context for a Workflow main handler.
85
- # Raises if not inside a Workflow main handler.
80
+ # Returns a pre-configured Client using the global +config+.
81
+ # Creates a new Client on each call (stateless — safe to discard).
86
82
  #
87
- # @return [WorkflowContext]
88
- sig { returns(WorkflowContext) }
89
- def current_workflow_context
90
- fetch_context!(service_kind: 'workflow', handler_kind: 'workflow')
83
+ # @example
84
+ # Restate.client.service(Greeter).greet("World")
85
+ # Restate.client.resolve_awakeable(id, payload)
86
+ # Restate.client.create_deployment("http://localhost:9080")
87
+ sig { returns(Client) }
88
+ def client
89
+ cfg = config
90
+ Client.new(ingress_url: cfg.ingress_url, admin_url: cfg.admin_url,
91
+ ingress_headers: cfg.ingress_headers, admin_headers: cfg.admin_headers)
91
92
  end
92
93
 
93
- # Returns the current context for a Workflow shared handler.
94
- # Read-only state: +get+ and +state_keys+ only, no +set+/+clear+.
95
- # Raises if not inside a Workflow shared handler.
96
- #
97
- # @return [WorkflowSharedContext]
98
- sig { returns(WorkflowSharedContext) }
99
- def current_shared_workflow_context
100
- fetch_context!(service_kind: 'workflow', handler_kind: 'shared')
101
- end
94
+ # ── Context accessor (internal) ──
102
95
 
103
96
  # @!visibility private
104
97
  sig do
@@ -108,7 +101,7 @@ module Restate
108
101
  ctx = Thread.current[:restate_context]
109
102
  unless ctx
110
103
  Kernel.raise 'Not inside a Restate handler. ' \
111
- 'Context accessors can only be called during handler execution.'
104
+ 'Restate.* methods can only be called during handler execution.'
112
105
  end
113
106
 
114
107
  if service_kind
@@ -127,4 +120,260 @@ module Restate
127
120
 
128
121
  T.cast(ctx, ServerContext)
129
122
  end
123
+
124
+ # ── Durable execution ──
125
+
126
+ # Execute a durable side effect. The block runs at most once; the result
127
+ # is journaled and replayed on retries. Returns a DurableFuture.
128
+ sig do
129
+ params(name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
130
+ background: T::Boolean, action: T.proc.returns(T.untyped)).returns(DurableFuture)
131
+ end
132
+ def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
133
+ fetch_context!.run(name, serde: serde, retry_policy: retry_policy, background: background, &action)
134
+ end
135
+
136
+ # Convenience shortcut for +run(...).await+. Returns the result directly.
137
+ sig do
138
+ params(name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
139
+ background: T::Boolean, action: T.proc.returns(T.untyped)).returns(T.untyped)
140
+ end
141
+ def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
142
+ fetch_context!.run_sync(name, serde: serde, retry_policy: retry_policy, background: background, &action)
143
+ end
144
+
145
+ # Durable timer that survives handler restarts.
146
+ sig { params(seconds: Numeric).returns(DurableFuture) }
147
+ def sleep(seconds)
148
+ fetch_context!.sleep(seconds)
149
+ end
150
+
151
+ # ── State operations (VirtualObject / Workflow) ──
152
+
153
+ # Durably retrieve a state entry. Returns nil if unset.
154
+ sig { params(name: String, serde: T.untyped).returns(T.untyped) }
155
+ def get(name, serde: JsonSerde)
156
+ fetch_context!.get(name, serde: serde)
157
+ end
158
+
159
+ # Durably retrieve a state entry, returning a DurableFuture instead of blocking.
160
+ sig { params(name: String, serde: T.untyped).returns(DurableFuture) }
161
+ def get_async(name, serde: JsonSerde)
162
+ fetch_context!.get_async(name, serde: serde)
163
+ end
164
+
165
+ # Durably set a state entry.
166
+ sig { params(name: String, value: T.untyped, serde: T.untyped).void }
167
+ def set(name, value, serde: JsonSerde)
168
+ fetch_context!.set(name, value, serde: serde)
169
+ end
170
+
171
+ # Durably remove a single state entry.
172
+ sig { params(name: String).void }
173
+ def clear(name)
174
+ fetch_context!.clear(name)
175
+ end
176
+
177
+ # Durably remove all state entries.
178
+ sig { void }
179
+ def clear_all
180
+ fetch_context!.clear_all
181
+ end
182
+
183
+ # List all state entry names.
184
+ sig { returns(T.untyped) }
185
+ def state_keys
186
+ fetch_context!.state_keys
187
+ end
188
+
189
+ # List all state entry names, returning a DurableFuture.
190
+ sig { returns(DurableFuture) }
191
+ def state_keys_async
192
+ fetch_context!.state_keys_async
193
+ end
194
+
195
+ # ── Service communication ──
196
+
197
+ # Durably call a handler on a Restate service.
198
+ sig do
199
+ params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
200
+ arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String),
201
+ headers: T.nilable(T::Hash[String, String]),
202
+ input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture)
203
+ end
204
+ def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
205
+ input_serde: NOT_SET, output_serde: NOT_SET)
206
+ ctx = fetch_context!
207
+ ctx.service_call(service, handler, arg, key: key, idempotency_key: idempotency_key,
208
+ headers: headers, input_serde: input_serde, output_serde: output_serde)
209
+ end
210
+
211
+ # Fire-and-forget send to a Restate service handler.
212
+ sig do
213
+ params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
214
+ arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric),
215
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
216
+ input_serde: T.untyped).returns(SendHandle)
217
+ end
218
+ def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil,
219
+ headers: nil, input_serde: NOT_SET)
220
+ ctx = fetch_context!
221
+ ctx.service_send(service, handler, arg, key: key, delay: delay, idempotency_key: idempotency_key,
222
+ headers: headers, input_serde: input_serde)
223
+ end
224
+
225
+ # Durably call a handler on a Restate virtual object.
226
+ sig do
227
+ params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
228
+ key: String, arg: T.untyped, idempotency_key: T.nilable(String),
229
+ headers: T.nilable(T::Hash[String, String]),
230
+ input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture)
231
+ end
232
+ def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
233
+ input_serde: NOT_SET, output_serde: NOT_SET)
234
+ ctx = fetch_context!
235
+ ctx.object_call(service, handler, key, arg, idempotency_key: idempotency_key,
236
+ headers: headers, input_serde: input_serde, output_serde: output_serde)
237
+ end
238
+
239
+ # Fire-and-forget send to a Restate virtual object handler.
240
+ sig do
241
+ params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
242
+ key: String, arg: T.untyped, delay: T.nilable(Numeric),
243
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
244
+ input_serde: T.untyped).returns(SendHandle)
245
+ end
246
+ def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
247
+ headers: nil, input_serde: NOT_SET)
248
+ ctx = fetch_context!
249
+ ctx.object_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key,
250
+ headers: headers, input_serde: input_serde)
251
+ end
252
+
253
+ # Durably call a handler on a Restate workflow.
254
+ sig do
255
+ params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
256
+ key: String, arg: T.untyped, idempotency_key: T.nilable(String),
257
+ headers: T.nilable(T::Hash[String, String]),
258
+ input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture)
259
+ end
260
+ def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
261
+ input_serde: NOT_SET, output_serde: NOT_SET)
262
+ ctx = fetch_context!
263
+ ctx.workflow_call(service, handler, key, arg,
264
+ idempotency_key: idempotency_key, headers: headers,
265
+ input_serde: input_serde, output_serde: output_serde)
266
+ end
267
+
268
+ # Fire-and-forget send to a Restate workflow handler.
269
+ sig do
270
+ params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
271
+ key: String, arg: T.untyped, delay: T.nilable(Numeric),
272
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
273
+ input_serde: T.untyped).returns(SendHandle)
274
+ end
275
+ def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
276
+ headers: nil, input_serde: NOT_SET)
277
+ ctx = fetch_context!
278
+ ctx.workflow_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key,
279
+ headers: headers, input_serde: input_serde)
280
+ end
281
+
282
+ # Durably call a handler using raw bytes (no serialization).
283
+ sig do
284
+ params(service: String, handler: String, arg: String,
285
+ key: T.nilable(String), idempotency_key: T.nilable(String),
286
+ headers: T.nilable(T::Hash[String, String])).returns(DurableCallFuture)
287
+ end
288
+ def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil)
289
+ fetch_context!.generic_call(service, handler, arg, key: key,
290
+ idempotency_key: idempotency_key, headers: headers)
291
+ end
292
+
293
+ # Fire-and-forget send using raw bytes (no serialization).
294
+ sig do
295
+ params(service: String, handler: String, arg: String,
296
+ key: T.nilable(String), delay: T.nilable(Numeric),
297
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String])).returns(SendHandle)
298
+ end
299
+ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil)
300
+ fetch_context!.generic_send(service, handler, arg, key: key, delay: delay,
301
+ idempotency_key: idempotency_key, headers: headers)
302
+ end
303
+
304
+ # ── Awakeables ──
305
+
306
+ # Create an awakeable for external callbacks. Returns [awakeable_id, DurableFuture].
307
+ sig { params(serde: T.untyped).returns([String, DurableFuture]) }
308
+ def awakeable(serde: JsonSerde)
309
+ fetch_context!.awakeable(serde: serde)
310
+ end
311
+
312
+ # Resolve an awakeable with a success value.
313
+ sig { params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void }
314
+ def resolve_awakeable(awakeable_id, payload, serde: JsonSerde)
315
+ fetch_context!.resolve_awakeable(awakeable_id, payload, serde: serde)
316
+ end
317
+
318
+ # Reject an awakeable with a terminal failure.
319
+ sig { params(awakeable_id: String, message: String, code: Integer).void }
320
+ def reject_awakeable(awakeable_id, message, code: 500)
321
+ fetch_context!.reject_awakeable(awakeable_id, message, code: code)
322
+ end
323
+
324
+ # ── Promises (Workflow only) ──
325
+
326
+ # Get a durable promise value, blocking until resolved.
327
+ sig { params(name: String, serde: T.untyped).returns(T.untyped) }
328
+ def promise(name, serde: JsonSerde)
329
+ fetch_context!.promise(name, serde: serde)
330
+ end
331
+
332
+ # Peek at a durable promise without blocking. Returns nil if not yet resolved.
333
+ sig { params(name: String, serde: T.untyped).returns(T.untyped) }
334
+ def peek_promise(name, serde: JsonSerde)
335
+ fetch_context!.peek_promise(name, serde: serde)
336
+ end
337
+
338
+ # Resolve a durable promise with a value.
339
+ sig { params(name: String, payload: T.untyped, serde: T.untyped).void }
340
+ def resolve_promise(name, payload, serde: JsonSerde)
341
+ fetch_context!.resolve_promise(name, payload, serde: serde)
342
+ end
343
+
344
+ # Reject a durable promise with a terminal failure.
345
+ sig { params(name: String, message: String, code: Integer).void }
346
+ def reject_promise(name, message, code: 500)
347
+ fetch_context!.reject_promise(name, message, code: code)
348
+ end
349
+
350
+ # ── Futures ──
351
+
352
+ # Wait until any of the given futures completes. Returns [completed, remaining].
353
+ sig { params(futures: T::Array[DurableFuture]).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) }
354
+ def wait_any(*futures)
355
+ T.unsafe(fetch_context!).wait_any(*futures)
356
+ end
357
+
358
+ # ── Request metadata ──
359
+
360
+ # Returns metadata about the current invocation (id, headers, raw body).
361
+ sig { returns(T.untyped) }
362
+ def request
363
+ fetch_context!.request
364
+ end
365
+
366
+ # Returns the key for this virtual object or workflow invocation.
367
+ sig { returns(String) }
368
+ def key
369
+ fetch_context!.key
370
+ end
371
+
372
+ # ── Invocation control ──
373
+
374
+ # Request cancellation of another invocation.
375
+ sig { params(invocation_id: String).void }
376
+ def cancel_invocation(invocation_id)
377
+ fetch_context!.cancel_invocation(invocation_id)
378
+ end
130
379
  end
@@ -10,8 +10,8 @@ module Tapioca
10
10
  module Compilers
11
11
  # Generates Sorbet sigs for Restate handler methods.
12
12
  #
13
- # Every handler receives +ctx+ as its first parameter. Handlers that
14
- # accept input receive it as the second parameter (arity 2).
13
+ # Handlers take 0 or 1 parameters (the input). Context is implicit
14
+ # via +Restate.*+ module methods.
15
15
  #
16
16
  # Usage:
17
17
  # bundle exec tapioca dsl
@@ -58,9 +58,8 @@ module Tapioca
58
58
  def decorate # rubocop:disable Metrics/MethodLength
59
59
  root.create_path(constant) do |klass|
60
60
  constant.handlers.each do |name, handler|
61
- ctx_type = resolve_context_type(constant, handler)
62
- params = [create_param('ctx', type: ctx_type)]
63
- if handler.arity == 2
61
+ params = []
62
+ if handler.arity == 1
64
63
  input_type = resolve_input_type(handler)
65
64
  params << create_param('input', type: input_type)
66
65
  end