restate-sdk 0.4.3

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.
@@ -0,0 +1,336 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable Metrics/ModuleLength,Metrics/ParameterLists,Style/EmptyMethod
5
+ module Restate
6
+ # Signals when the current invocation attempt has finished — either the handler
7
+ # completed, the connection was lost, or a transient error occurred.
8
+ #
9
+ # Use this to clean up attempt-scoped resources (open connections, temp files,
10
+ # etc.) that should not outlive the current attempt.
11
+ #
12
+ # Available via +ctx.request.attempt_finished_event+.
13
+ #
14
+ # @example Cancel a long-running HTTP call when the attempt finishes
15
+ # event = ctx.request.attempt_finished_event
16
+ # ctx.run('call-api') do
17
+ # # poll event.set? periodically, or pass it to your HTTP client
18
+ # end
19
+ class AttemptFinishedEvent
20
+ extend T::Sig
21
+
22
+ sig { void }
23
+ def initialize
24
+ @mutex = T.let(Mutex.new, Mutex)
25
+ @set = T.let(false, T::Boolean)
26
+ @waiters = T.let([], T::Array[Thread::Queue])
27
+ end
28
+
29
+ # Returns true if the attempt has finished.
30
+ sig { returns(T::Boolean) }
31
+ def set?
32
+ @set
33
+ end
34
+
35
+ # Blocks the current fiber/thread until the attempt finishes.
36
+ sig { void }
37
+ def wait
38
+ return if @set
39
+
40
+ waiter = T.let(nil, T.nilable(Thread::Queue))
41
+ @mutex.synchronize do
42
+ unless @set
43
+ waiter = Thread::Queue.new
44
+ @waiters << waiter
45
+ end
46
+ end
47
+ waiter&.pop
48
+ end
49
+
50
+ # Marks the event as set and wakes all waiters.
51
+ # Called internally by the SDK when the attempt ends.
52
+ sig { void }
53
+ def set!
54
+ @mutex.synchronize do
55
+ @set = true
56
+ @waiters.each { |w| w.push(true) }
57
+ @waiters.clear
58
+ end
59
+ end
60
+ end
61
+
62
+ # Request metadata available to handlers via +ctx.request+.
63
+ #
64
+ # @!attribute [r] id
65
+ # @return [String] the invocation ID
66
+ # @!attribute [r] headers
67
+ # @return [Hash{String => String}] request headers
68
+ # @!attribute [r] body
69
+ # @return [String] raw input bytes
70
+ # @!attribute [r] attempt_finished_event
71
+ # @return [AttemptFinishedEvent] signaled when this attempt ends
72
+ Request = Struct.new(:id, :headers, :body, :attempt_finished_event, keyword_init: true)
73
+
74
+ # Base context interface for all Restate handlers.
75
+ #
76
+ # Provides durable execution (+run+, +run_sync+), timers (+sleep+),
77
+ # service-to-service calls, awakeables, and request metadata.
78
+ #
79
+ # @see ObjectContext for VirtualObject handlers (adds state operations)
80
+ # @see WorkflowContext for Workflow handlers (adds promise operations)
81
+ module Context
82
+ extend T::Sig
83
+ extend T::Helpers
84
+
85
+ abstract!
86
+
87
+ # Execute a durable side effect. The block runs at most once; the result
88
+ # is journaled and replayed on retries.
89
+ #
90
+ # Pass +background: true+ to offload the block to a real OS Thread,
91
+ # keeping the fiber event loop responsive for CPU-intensive work.
92
+ sig do
93
+ abstract.params(
94
+ name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
95
+ background: T::Boolean, action: T.proc.returns(T.untyped)
96
+ ).returns(DurableFuture)
97
+ end
98
+ def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action); end
99
+
100
+ # Convenience shortcut for +run(...).await+. Returns the result directly.
101
+ # Accepts all the same options as +run+, including +background: true+.
102
+ sig do
103
+ abstract.params(
104
+ name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
105
+ background: T::Boolean, action: T.proc.returns(T.untyped)
106
+ ).returns(T.untyped)
107
+ end
108
+ def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action); end
109
+
110
+ # Durable timer that survives handler restarts.
111
+ sig { params(seconds: Numeric).returns(DurableFuture) }
112
+ def sleep(seconds) # rubocop:disable Lint/UnusedMethodArgument
113
+ Kernel.raise NotImplementedError
114
+ end
115
+
116
+ # Durably call a handler on a Restate service.
117
+ sig do
118
+ abstract.params(
119
+ service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
120
+ arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String),
121
+ headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped
122
+ ).returns(DurableCallFuture)
123
+ end
124
+ def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
125
+ input_serde: NOT_SET, output_serde: NOT_SET)
126
+ end
127
+ # Fire-and-forget send to a Restate service handler.
128
+ sig do
129
+ abstract.params(
130
+ service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
131
+ arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric),
132
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
133
+ input_serde: T.untyped
134
+ ).returns(SendHandle)
135
+ end
136
+ def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil,
137
+ headers: nil, input_serde: NOT_SET)
138
+ end
139
+ # Durably call a handler on a Restate virtual object.
140
+ sig do
141
+ abstract.params(
142
+ service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
143
+ key: String, arg: T.untyped, idempotency_key: T.nilable(String),
144
+ headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped
145
+ ).returns(DurableCallFuture)
146
+ end
147
+ def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
148
+ input_serde: NOT_SET, output_serde: NOT_SET)
149
+ end
150
+ # Fire-and-forget send to a Restate virtual object handler.
151
+ sig do
152
+ abstract.params(
153
+ service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
154
+ key: String, arg: T.untyped, delay: T.nilable(Numeric),
155
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
156
+ input_serde: T.untyped
157
+ ).returns(SendHandle)
158
+ end
159
+ def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
160
+ headers: nil, input_serde: NOT_SET)
161
+ end
162
+ # Durably call a handler on a Restate workflow.
163
+ sig do
164
+ abstract.params(
165
+ service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
166
+ key: String, arg: T.untyped, idempotency_key: T.nilable(String),
167
+ headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped
168
+ ).returns(DurableCallFuture)
169
+ end
170
+ def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
171
+ input_serde: NOT_SET, output_serde: NOT_SET)
172
+ end
173
+ # Fire-and-forget send to a Restate workflow handler.
174
+ sig do
175
+ abstract.params(
176
+ service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
177
+ key: String, arg: T.untyped, delay: T.nilable(Numeric),
178
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
179
+ input_serde: T.untyped
180
+ ).returns(SendHandle)
181
+ end
182
+ def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
183
+ headers: nil, input_serde: NOT_SET)
184
+ end
185
+ # Durably call a handler using raw bytes (no serialization).
186
+ sig do
187
+ abstract.params(
188
+ service: String, handler: String, arg: String,
189
+ key: T.nilable(String), idempotency_key: T.nilable(String),
190
+ headers: T.nilable(T::Hash[String, String])
191
+ ).returns(DurableCallFuture)
192
+ end
193
+ def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil); end
194
+
195
+ # Fire-and-forget send using raw bytes (no serialization).
196
+ sig do
197
+ abstract.params(
198
+ service: String, handler: String, arg: String,
199
+ key: T.nilable(String), delay: T.nilable(Numeric),
200
+ idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String])
201
+ ).returns(SendHandle)
202
+ end
203
+ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil); end
204
+
205
+ # Create an awakeable for external callbacks.
206
+ # Returns [awakeable_id, DurableFuture].
207
+ sig { abstract.params(serde: T.untyped).returns([String, DurableFuture]) }
208
+ def awakeable(serde: JsonSerde); end
209
+
210
+ # Resolve an awakeable with a success value.
211
+ sig { abstract.params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void }
212
+ def resolve_awakeable(awakeable_id, payload, serde: JsonSerde); end
213
+
214
+ # Reject an awakeable with a terminal failure.
215
+ sig { abstract.params(awakeable_id: String, message: String, code: Integer).void }
216
+ def reject_awakeable(awakeable_id, message, code: 500); end
217
+
218
+ # Request cancellation of another invocation.
219
+ sig { abstract.params(invocation_id: String).void }
220
+ def cancel_invocation(invocation_id); end
221
+
222
+ # Wait until any of the given futures completes.
223
+ # Returns [completed, remaining].
224
+ sig { abstract.params(futures: DurableFuture).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) }
225
+ def wait_any(*futures); end
226
+
227
+ # Returns metadata about the current invocation.
228
+ sig { abstract.returns(Request) }
229
+ def request; end
230
+
231
+ # Returns the key for this virtual object or workflow invocation.
232
+ sig { abstract.returns(String) }
233
+ def key; end
234
+ end
235
+
236
+ # Context interface for VirtualObject shared handlers (read-only state).
237
+ # Extends {Context} with +get+, +state_keys+, and +key+ — but no mutations.
238
+ module ObjectSharedContext
239
+ extend T::Sig
240
+ extend T::Helpers
241
+
242
+ abstract!
243
+ include Context
244
+
245
+ # Durably retrieve a state entry. Returns nil if unset.
246
+ sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
247
+ def get(name, serde: JsonSerde); end
248
+
249
+ # Durably retrieve a state entry, returning a DurableFuture instead of blocking.
250
+ sig { abstract.params(name: String, serde: T.untyped).returns(DurableFuture) }
251
+ def get_async(name, serde: JsonSerde); end
252
+
253
+ # List all state entry names.
254
+ sig { abstract.returns(T.untyped) }
255
+ def state_keys; end
256
+
257
+ # List all state entry names, returning a DurableFuture instead of blocking.
258
+ sig { abstract.returns(DurableFuture) }
259
+ def state_keys_async; end
260
+ end
261
+
262
+ # Context interface for VirtualObject exclusive handlers (full state access).
263
+ # Extends {ObjectSharedContext} with mutating state operations.
264
+ module ObjectContext
265
+ extend T::Sig
266
+ extend T::Helpers
267
+
268
+ abstract!
269
+ include ObjectSharedContext
270
+
271
+ # Durably set a state entry.
272
+ sig { abstract.params(name: String, value: T.untyped, serde: T.untyped).void }
273
+ def set(name, value, serde: JsonSerde); end
274
+
275
+ # Durably remove a single state entry.
276
+ sig { abstract.params(name: String).void }
277
+ def clear(name); end
278
+
279
+ # Durably remove all state entries.
280
+ sig { abstract.void }
281
+ def clear_all; end
282
+ end
283
+
284
+ # Context interface for Workflow shared handlers (read-only state + promises).
285
+ # Extends {ObjectSharedContext} with durable promise operations.
286
+ module WorkflowSharedContext
287
+ extend T::Sig
288
+ extend T::Helpers
289
+
290
+ abstract!
291
+ include ObjectSharedContext
292
+
293
+ # Get a durable promise value, blocking until resolved.
294
+ sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
295
+ def promise(name, serde: JsonSerde); end
296
+
297
+ # Peek at a durable promise without blocking. Returns nil if not yet resolved.
298
+ sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
299
+ def peek_promise(name, serde: JsonSerde); end
300
+
301
+ # Resolve a durable promise with a value.
302
+ sig { abstract.params(name: String, payload: T.untyped, serde: T.untyped).void }
303
+ def resolve_promise(name, payload, serde: JsonSerde); end
304
+
305
+ # Reject a durable promise with a terminal failure.
306
+ sig { abstract.params(name: String, message: String, code: Integer).void }
307
+ def reject_promise(name, message, code: 500); end
308
+ end
309
+
310
+ # Context interface for Workflow main handler (full state + promises).
311
+ # Extends {ObjectContext} with durable promise operations.
312
+ module WorkflowContext
313
+ extend T::Sig
314
+ extend T::Helpers
315
+
316
+ abstract!
317
+ include ObjectContext
318
+
319
+ # Get a durable promise value, blocking until resolved.
320
+ sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
321
+ def promise(name, serde: JsonSerde); end
322
+
323
+ # Peek at a durable promise without blocking. Returns nil if not yet resolved.
324
+ sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
325
+ def peek_promise(name, serde: JsonSerde); end
326
+
327
+ # Resolve a durable promise with a value.
328
+ sig { abstract.params(name: String, payload: T.untyped, serde: T.untyped).void }
329
+ def resolve_promise(name, payload, serde: JsonSerde); end
330
+
331
+ # Reject a durable promise with a terminal failure.
332
+ sig { abstract.params(name: String, message: String, code: Integer).void }
333
+ def reject_promise(name, message, code: 500); end
334
+ end
335
+ end
336
+ # rubocop:enable Metrics/ModuleLength,Metrics/ParameterLists,Style/EmptyMethod
@@ -0,0 +1,150 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+
6
+ module Restate
7
+ module Discovery # rubocop:disable Metrics/ModuleLength
8
+ extend T::Sig
9
+
10
+ PROTOCOL_MODES = T.let({
11
+ 'bidi' => 'BIDI_STREAM',
12
+ 'request_response' => 'REQUEST_RESPONSE'
13
+ }.freeze, T::Hash[String, String])
14
+
15
+ SERVICE_TYPES = T.let({
16
+ 'service' => 'SERVICE',
17
+ 'object' => 'VIRTUAL_OBJECT',
18
+ 'workflow' => 'WORKFLOW'
19
+ }.freeze, T::Hash[String, String])
20
+
21
+ HANDLER_TYPES = T.let({
22
+ 'exclusive' => 'EXCLUSIVE',
23
+ 'shared' => 'SHARED',
24
+ 'workflow' => 'WORKFLOW'
25
+ }.freeze, T::Hash[String, String])
26
+
27
+ module_function
28
+
29
+ # Generate the discovery JSON for the given endpoint.
30
+ sig { params(endpoint: Endpoint, _version: Integer, discovered_as: String).returns(String) }
31
+ def compute_discovery_json(endpoint, _version, discovered_as)
32
+ ep = compute_discovery(endpoint, discovered_as)
33
+ JSON.generate(ep, allow_nan: false)
34
+ end
35
+
36
+ # Build the discovery hash for the endpoint.
37
+ sig { params(endpoint: Endpoint, discovered_as: String).returns(T::Hash[Symbol, T.untyped]) }
38
+ def compute_discovery(endpoint, discovered_as)
39
+ services = endpoint.services.values.map do |service|
40
+ build_service(service)
41
+ end
42
+
43
+ protocol_mode = PROTOCOL_MODES.fetch(endpoint.protocol || discovered_as)
44
+
45
+ compact(
46
+ protocolMode: protocol_mode,
47
+ minProtocolVersion: 5,
48
+ maxProtocolVersion: 5,
49
+ services: services
50
+ )
51
+ end
52
+
53
+ sig { params(service: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
54
+ def build_service(service) # rubocop:disable Metrics/AbcSize
55
+ service_type = SERVICE_TYPES.fetch(service.service_tag.kind)
56
+
57
+ handlers = service.handlers.values.map do |handler|
58
+ build_handler(handler)
59
+ end
60
+
61
+ svc_name = service.service_name
62
+
63
+ result = compact(
64
+ name: svc_name,
65
+ ty: service_type,
66
+ handlers: handlers,
67
+ documentation: service.service_tag.description,
68
+ metadata: service.service_tag.metadata,
69
+ enableLazyState: service.lazy_state?,
70
+ inactivityTimeout: seconds_to_ms(service.svc_inactivity_timeout),
71
+ abortTimeout: seconds_to_ms(service.svc_abort_timeout),
72
+ journalRetention: seconds_to_ms(service.svc_journal_retention),
73
+ idempotencyRetention: seconds_to_ms(service.svc_idempotency_retention),
74
+ ingressPrivate: service.svc_ingress_private
75
+ )
76
+
77
+ policy = service.svc_invocation_retry_policy
78
+ merge_retry_policy!(result, policy) if policy
79
+
80
+ result
81
+ end
82
+
83
+ sig { params(handler: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
84
+ def build_handler(handler) # rubocop:disable Metrics/AbcSize
85
+ ty = handler.kind ? HANDLER_TYPES.fetch(handler.kind) : nil
86
+
87
+ input_payload = {
88
+ required: false,
89
+ contentType: handler.handler_io.accept,
90
+ jsonSchema: handler.handler_io.input_serde.json_schema
91
+ }
92
+
93
+ output_payload = {
94
+ setContentTypeIfEmpty: false,
95
+ contentType: handler.handler_io.content_type,
96
+ jsonSchema: handler.handler_io.output_serde.json_schema
97
+ }
98
+
99
+ result = compact(
100
+ name: handler.name,
101
+ ty: ty,
102
+ input: compact(**input_payload),
103
+ output: compact(**output_payload),
104
+ enableLazyState: handler.enable_lazy_state,
105
+ documentation: handler.description,
106
+ metadata: handler.metadata,
107
+ inactivityTimeout: seconds_to_ms(handler.inactivity_timeout),
108
+ abortTimeout: seconds_to_ms(handler.abort_timeout),
109
+ journalRetention: seconds_to_ms(handler.journal_retention),
110
+ idempotencyRetention: seconds_to_ms(handler.idempotency_retention),
111
+ workflowCompletionRetention: seconds_to_ms(handler.workflow_completion_retention),
112
+ ingressPrivate: handler.ingress_private
113
+ )
114
+
115
+ merge_retry_policy!(result, handler.invocation_retry_policy) if handler.invocation_retry_policy
116
+
117
+ result
118
+ end
119
+
120
+ # Convert seconds to milliseconds (integer). Returns nil if input is nil.
121
+ sig { params(seconds: T.nilable(Numeric)).returns(T.nilable(Integer)) }
122
+ def seconds_to_ms(seconds)
123
+ return nil if seconds.nil?
124
+
125
+ (seconds * 1000).to_i
126
+ end
127
+
128
+ # Merge retry policy fields (flattened) into the target hash.
129
+ sig { params(target: T::Hash[Symbol, T.untyped], policy: T.nilable(T::Hash[Symbol, T.untyped])).void }
130
+ def merge_retry_policy!(target, policy) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
131
+ return if policy.nil? || policy.empty?
132
+
133
+ target[:retryPolicyInitialInterval] = seconds_to_ms(policy[:initial_interval]) if policy[:initial_interval]
134
+ target[:retryPolicyMaxInterval] = seconds_to_ms(policy[:max_interval]) if policy[:max_interval]
135
+ target[:retryPolicyMaxAttempts] = policy[:max_attempts] if policy[:max_attempts]
136
+ target[:retryPolicyExponentiationFactor] = policy[:exponentiation_factor] if policy[:exponentiation_factor]
137
+ target[:retryPolicyOnMaxAttempts] = policy[:on_max_attempts].to_s.upcase if policy[:on_max_attempts]
138
+ end
139
+
140
+ # Remove nil values from a hash (non-recursive for top level, recursive for nested).
141
+ sig { params(kwargs: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
142
+ def compact(**kwargs)
143
+ kwargs.each_with_object({}) do |(k, v), result|
144
+ next if v.nil?
145
+
146
+ result[k] = v.is_a?(Hash) ? compact(**v) : v
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,131 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Restate
5
+ # A durable future wrapping a VM handle. Lazily resolves on first +await+ and caches the result.
6
+ # Returned by +ctx.run+ and +ctx.sleep+.
7
+ class DurableFuture
8
+ extend T::Sig
9
+
10
+ sig { returns(Integer) }
11
+ attr_reader :handle
12
+
13
+ sig { params(ctx: ServerContext, handle: Integer, serde: T.untyped).void }
14
+ def initialize(ctx, handle, serde: nil)
15
+ @ctx = T.let(ctx, ServerContext)
16
+ @handle = T.let(handle, Integer)
17
+ @serde = T.let(serde, T.untyped)
18
+ @resolved = T.let(false, T::Boolean)
19
+ @value = T.let(nil, T.untyped)
20
+ end
21
+
22
+ # Block until the result is available and return it. Caches across calls.
23
+ #
24
+ # @return [Object] the deserialized result
25
+ sig { returns(T.untyped) }
26
+ def await
27
+ unless @resolved
28
+ raw = @ctx.resolve_handle(@handle)
29
+ @value = @serde ? @serde.deserialize(raw) : raw
30
+ @resolved = true
31
+ end
32
+ @value
33
+ end
34
+
35
+ # Check whether the future has completed (non-blocking).
36
+ #
37
+ # @return [Boolean]
38
+ sig { returns(T::Boolean) }
39
+ def completed?
40
+ @resolved || @ctx.completed?(@handle)
41
+ end
42
+ end
43
+
44
+ # A durable future for service/object/workflow calls.
45
+ # Adds +invocation_id+ and +cancel+ on top of DurableFuture.
46
+ # Returned by +ctx.service_call+, +ctx.object_call+, +ctx.workflow_call+.
47
+ class DurableCallFuture < DurableFuture
48
+ extend T::Sig
49
+
50
+ sig do
51
+ params(
52
+ ctx: ServerContext,
53
+ result_handle: Integer,
54
+ invocation_id_handle: Integer,
55
+ output_serde: T.untyped
56
+ ).void
57
+ end
58
+ def initialize(ctx, result_handle, invocation_id_handle, output_serde:)
59
+ super(ctx, result_handle)
60
+ @invocation_id_handle = T.let(invocation_id_handle, Integer)
61
+ @output_serde = T.let(output_serde, T.untyped)
62
+ @invocation_id_resolved = T.let(false, T::Boolean)
63
+ @invocation_id_value = T.let(nil, T.untyped)
64
+ end
65
+
66
+ # Block until the result is available and return it. Deserializes via +output_serde+.
67
+ sig { returns(T.untyped) }
68
+ def await
69
+ unless @resolved
70
+ raw = @ctx.resolve_handle(@handle)
71
+ @value = if raw.nil? || @output_serde.nil?
72
+ raw
73
+ else
74
+ @output_serde.deserialize(raw)
75
+ end
76
+ @resolved = true
77
+ end
78
+ @value
79
+ end
80
+
81
+ # Returns the invocation ID of the remote call. Lazily resolved.
82
+ #
83
+ # @return [String] the invocation ID
84
+ sig { returns(String) }
85
+ def invocation_id
86
+ unless @invocation_id_resolved
87
+ @invocation_id_value = @ctx.resolve_handle(@invocation_id_handle)
88
+ @invocation_id_resolved = true
89
+ end
90
+ T.must(@invocation_id_value)
91
+ end
92
+
93
+ # Cancel the remote invocation.
94
+ sig { void }
95
+ def cancel
96
+ @ctx.cancel_invocation(invocation_id)
97
+ end
98
+ end
99
+
100
+ # A handle for fire-and-forget send operations.
101
+ # Returned by +ctx.service_send+, +ctx.object_send+, +ctx.workflow_send+.
102
+ class SendHandle
103
+ extend T::Sig
104
+
105
+ sig { params(ctx: ServerContext, invocation_id_handle: Integer).void }
106
+ def initialize(ctx, invocation_id_handle)
107
+ @ctx = T.let(ctx, ServerContext)
108
+ @invocation_id_handle = T.let(invocation_id_handle, Integer)
109
+ @invocation_id_resolved = T.let(false, T::Boolean)
110
+ @invocation_id_value = T.let(nil, T.untyped)
111
+ end
112
+
113
+ # Returns the invocation ID of the sent call. Lazily resolved.
114
+ #
115
+ # @return [String] the invocation ID
116
+ sig { returns(String) }
117
+ def invocation_id
118
+ unless @invocation_id_resolved
119
+ @invocation_id_value = @ctx.resolve_handle(@invocation_id_handle)
120
+ @invocation_id_resolved = true
121
+ end
122
+ T.must(@invocation_id_value)
123
+ end
124
+
125
+ # Cancel the remote invocation.
126
+ sig { void }
127
+ def cancel
128
+ @ctx.cancel_invocation(invocation_id)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,69 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Restate
5
+ # Container for registered services. Bind services here, then create the Rack app.
6
+ class Endpoint
7
+ extend T::Sig
8
+
9
+ sig { returns(T::Hash[String, T.untyped]) }
10
+ attr_reader :services
11
+
12
+ sig { returns(T::Array[String]) }
13
+ attr_reader :identity_keys
14
+
15
+ sig { returns(T.nilable(String)) }
16
+ attr_accessor :protocol
17
+
18
+ sig { void }
19
+ def initialize
20
+ @services = T.let({}, T::Hash[String, T.untyped])
21
+ @protocol = T.let(nil, T.nilable(String))
22
+ @identity_keys = T.let([], T::Array[String])
23
+ end
24
+
25
+ # Bind one or more services to this endpoint.
26
+ #
27
+ # @param svcs [Array<Class<Service>, Class<VirtualObject>, Class<Workflow>>] services to bind
28
+ # @return [self]
29
+ # @raise [ArgumentError] if a service with the same name is already bound
30
+ sig { params(svcs: T.untyped).returns(T.self_type) }
31
+ def bind(*svcs)
32
+ svcs.each do |svc|
33
+ svc_name = svc.service_name
34
+ raise ArgumentError, "Service #{svc_name} already exists" if @services.key?(svc_name)
35
+
36
+ @services[svc_name] = svc
37
+ end
38
+ self
39
+ end
40
+
41
+ # Force bidirectional streaming protocol.
42
+ sig { returns(T.self_type) }
43
+ def streaming_protocol
44
+ @protocol = 'bidi'
45
+ self
46
+ end
47
+
48
+ # Force request/response protocol.
49
+ sig { returns(T.self_type) }
50
+ def request_response_protocol
51
+ @protocol = 'request_response'
52
+ self
53
+ end
54
+
55
+ # Add an identity key for request verification.
56
+ sig { params(key: String).returns(T.self_type) }
57
+ def identity_key(key)
58
+ @identity_keys << key
59
+ self
60
+ end
61
+
62
+ # Build and return the Rack-compatible application.
63
+ sig { returns(T.untyped) }
64
+ def app
65
+ require_relative 'server'
66
+ Server.new(self)
67
+ end
68
+ end
69
+ end