restate-sdk 0.14.0 → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "restate_internal"
3
- version = "0.14.0"
3
+ version = "1.1.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -12,5 +12,5 @@ doc = false
12
12
  [dependencies]
13
13
  magnus = { version = "0.8", features = ["rb-sys"] }
14
14
  rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
15
- restate-sdk-shared-core = { version = "=0.7.0", features = ["request_identity", "sha2_random_seed"] }
15
+ restate-sdk-shared-core = { version = "7.0.0" , features = ["request_identity", "sha2_random_seed", "rust_crypto"] }
16
16
  tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
@@ -1,14 +1,13 @@
1
1
  use magnus::{
2
2
  function, method,
3
- prelude::*,
4
3
  value::ReprValue,
5
- Error, ExceptionClass, Module, Object, RArray, RString, Ruby, Value,
4
+ Error, ExceptionClass, Module, Object, RArray, RString, Ruby, Symbol, TryConvert, Value,
6
5
  };
7
6
  use restate_sdk_shared_core::{
8
- CallHandle, CoreVM, DoProgressResponse, Error as CoreError, Header, IdentityVerifier, Input,
9
- NonEmptyValue, NotificationHandle, ResponseHead, RetryPolicy, RunExitResult,
10
- TakeOutputResult, Target, TerminalFailure, VMOptions, Value as CoreValue, VM,
11
- CANCEL_NOTIFICATION_HANDLE,
7
+ AwaitResponse, CallHandle, CoreVM, Error as CoreError, Header, IdentityVerifier, Input,
8
+ NonEmptyValue, NotificationHandle, OnMaxAttempts, ResponseHead, RetryPolicy, RunExitResult,
9
+ RunHandle, State, Target, TerminalFailure, UnresolvedFuture, VMOptions, Value as CoreValue,
10
+ VM, CANCEL_NOTIFICATION_HANDLE,
12
11
  };
13
12
  use std::cell::RefCell;
14
13
  use std::fmt;
@@ -299,6 +298,7 @@ impl From<RbExponentialRetryConfig> for RetryPolicy {
299
298
  .max_interval
300
299
  .map(Duration::from_millis)
301
300
  .or_else(|| Some(Duration::from_secs(10))),
301
+ on_max_attempts: OnMaxAttempts::FailAsTerminal,
302
302
  }
303
303
  } else {
304
304
  RetryPolicy::Infinite
@@ -355,6 +355,30 @@ impl From<CallHandle> for RbCallHandle {
355
355
  }
356
356
  }
357
357
 
358
+ #[magnus::wrap(class = "Restate::Internal::Run")]
359
+ struct RbRun {
360
+ replayed: bool,
361
+ handle: u32,
362
+ }
363
+
364
+ impl RbRun {
365
+ fn replayed(&self) -> bool {
366
+ self.replayed
367
+ }
368
+ fn handle(&self) -> u32 {
369
+ self.handle
370
+ }
371
+ }
372
+
373
+ impl From<RunHandle> for RbRun {
374
+ fn from(r: RunHandle) -> Self {
375
+ RbRun {
376
+ replayed: r.replayed,
377
+ handle: r.handle.into(),
378
+ }
379
+ }
380
+ }
381
+
358
382
  // ── VM ──
359
383
 
360
384
  #[magnus::wrap(class = "Restate::Internal::VM")]
@@ -418,10 +442,8 @@ impl RbVM {
418
442
 
419
443
  fn take_output(&self) -> Result<Value, Error> {
420
444
  let ruby = Ruby::get().map_err(|_| Error::new(vm_error_class(), "Ruby not available"))?;
421
- Ok(match self.vm.borrow_mut().take_output() {
422
- TakeOutputResult::Buffer(b) => ruby.str_from_slice(&b).as_value(),
423
- TakeOutputResult::EOF => ruby.qnil().as_value(),
424
- })
445
+ let buffer = self.vm.borrow_mut().take_output();
446
+ Ok(ruby.str_from_slice(&buffer).as_value())
425
447
  }
426
448
 
427
449
  fn is_ready_to_execute(&self) -> Result<bool, Error> {
@@ -435,33 +457,39 @@ impl RbVM {
435
457
  self.vm.borrow().is_completed(handle.into())
436
458
  }
437
459
 
438
- fn do_progress(&self, handles: RArray) -> Result<Value, Error> {
460
+ // Drive the VM forward against an UnresolvedFuture tree. The Ruby-side
461
+ // encoding is:
462
+ // Integer handle → Single(handle)
463
+ // [:first_completed, [child, ...]] → race / wait_any
464
+ // [:all_completed, [child, ...]] → all_settled
465
+ // [:first_succeeded_or_all_failed,[child, ...]] → any
466
+ // [:all_succeeded_or_first_failed,[child, ...]] → all
467
+ // [:unknown, [child, ...]] → unknown combinator
468
+ fn do_await(&self, future_value: Value) -> Result<Value, Error> {
439
469
  let ruby = Ruby::get().map_err(|_| Error::new(vm_error_class(), "Ruby not available"))?;
440
- let handle_vec: Vec<u32> = handles.to_vec()?;
441
- let notification_handles: Vec<NotificationHandle> =
442
- handle_vec.into_iter().map(NotificationHandle::from).collect();
443
-
444
- let res = self.vm.borrow_mut().do_progress(notification_handles);
470
+ let future = parse_unresolved_future(future_value)?;
471
+ let res = self.vm.borrow_mut().do_await(future);
445
472
 
446
473
  match res {
447
474
  Err(e) if e.is_suspended_error() => Ok(ruby.into_value(RbSuspended)),
448
475
  Err(e) => Err(core_error_to_magnus(e)),
449
- Ok(DoProgressResponse::AnyCompleted) => {
476
+ Ok(AwaitResponse::AnyCompleted) => {
450
477
  Ok(ruby.into_value(RbDoProgressAnyCompleted))
451
478
  }
452
- Ok(DoProgressResponse::ReadFromInput) => {
453
- Ok(ruby.into_value(RbDoProgressReadFromInput))
454
- }
455
- Ok(DoProgressResponse::ExecuteRun(handle)) => Ok(ruby.into_value(
479
+ Ok(AwaitResponse::ExecuteRun(handle)) => Ok(ruby.into_value(
456
480
  RbDoProgressExecuteRun {
457
481
  handle: handle.into(),
458
482
  },
459
483
  )),
460
- Ok(DoProgressResponse::CancelSignalReceived) => {
484
+ Ok(AwaitResponse::CancelSignalReceived) => {
461
485
  Ok(ruby.into_value(RbDoProgressCancelSignalReceived))
462
486
  }
463
- Ok(DoProgressResponse::WaitingPendingRun) => {
464
- Ok(ruby.into_value(RbDoWaitForPendingRun))
487
+ Ok(AwaitResponse::WaitingExternalProgress { waiting_input, .. }) => {
488
+ if waiting_input {
489
+ Ok(ruby.into_value(RbDoProgressReadFromInput))
490
+ } else {
491
+ Ok(ruby.into_value(RbDoWaitForPendingRun))
492
+ }
465
493
  }
466
494
  }
467
495
  }
@@ -590,9 +618,12 @@ impl RbVM {
590
618
  handler,
591
619
  key: key_opt,
592
620
  idempotency_key: idem_opt,
621
+ scope: None,
622
+ limit_key: None,
593
623
  headers: hdr_vec,
594
624
  },
595
625
  bytes.into(),
626
+ None,
596
627
  Default::default(),
597
628
  )
598
629
  .map(Into::into)
@@ -639,6 +670,8 @@ impl RbVM {
639
670
  handler,
640
671
  key: key_opt,
641
672
  idempotency_key: idem_opt,
673
+ scope: None,
674
+ limit_key: None,
642
675
  headers: hdr_vec,
643
676
  },
644
677
  bytes.into(),
@@ -648,13 +681,14 @@ impl RbVM {
648
681
  .expect("Duration since unix epoch cannot fail")
649
682
  + Duration::from_millis(millis)
650
683
  }),
684
+ None,
651
685
  Default::default(),
652
686
  )
653
687
  .map(|s| s.invocation_id_notification_handle.into())
654
688
  .map_err(core_error_to_magnus)
655
689
  }
656
690
 
657
- fn sys_run(&self, name: String) -> Result<u32, Error> {
691
+ fn sys_run(&self, name: String) -> Result<RbRun, Error> {
658
692
  self.vm
659
693
  .borrow_mut()
660
694
  .sys_run(name)
@@ -727,21 +761,21 @@ impl RbVM {
727
761
  }
728
762
 
729
763
  fn is_replaying(&self) -> bool {
730
- self.vm.borrow().is_replaying()
764
+ matches!(self.vm.borrow().state(), State::Replaying)
731
765
  }
732
766
 
733
767
  // ── Awakeables ──
734
768
 
735
769
  fn sys_awakeable(&self) -> Result<Value, Error> {
736
770
  let ruby = Ruby::get().map_err(|_| Error::new(vm_error_class(), "Ruby not available"))?;
737
- let (id, handle) = self
771
+ let awakeable = self
738
772
  .vm
739
773
  .borrow_mut()
740
774
  .sys_awakeable()
741
775
  .map_err(core_error_to_magnus)?;
742
776
  let ary = ruby.ary_new_capa(2);
743
- ary.push(ruby.str_new(&id))?;
744
- ary.push(u32::from(handle))?;
777
+ ary.push(ruby.str_new(&awakeable.id))?;
778
+ ary.push(u32::from(awakeable.handle))?;
745
779
  Ok(ary.as_value())
746
780
  }
747
781
 
@@ -926,6 +960,54 @@ Or remove the begin/rescue altogether if you don't need it.
926
960
 
927
961
  // ── Helpers ──
928
962
 
963
+ // Recursive Ruby → UnresolvedFuture parser. Accepts an Integer handle, or a
964
+ // [Symbol, Array[children]] pair encoding a combinator node. See the do_await
965
+ // docstring for the variant tags.
966
+ fn parse_unresolved_future(value: Value) -> Result<UnresolvedFuture, Error> {
967
+ let ruby = Ruby::get().map_err(|_| Error::new(vm_error_class(), "Ruby not available"))?;
968
+
969
+ if let Ok(handle) = u32::try_convert(value) {
970
+ return Ok(UnresolvedFuture::Single(NotificationHandle::from(handle)));
971
+ }
972
+
973
+ let pair = RArray::try_convert(value).map_err(|_| {
974
+ Error::new(
975
+ ruby.exception_arg_error(),
976
+ "UnresolvedFuture must be a Integer handle or [Symbol, Array] pair",
977
+ )
978
+ })?;
979
+ if pair.len() != 2 {
980
+ return Err(Error::new(
981
+ ruby.exception_arg_error(),
982
+ "UnresolvedFuture combinator pair must be [Symbol, Array]",
983
+ ));
984
+ }
985
+ let tag: Symbol = pair.entry(0)?;
986
+ let children_arr: RArray = pair.entry(1)?;
987
+ let mut children = Vec::with_capacity(children_arr.len());
988
+ for child in children_arr.into_iter() {
989
+ children.push(parse_unresolved_future(child)?);
990
+ }
991
+ let tag_name = tag.name().map_err(|_| {
992
+ Error::new(ruby.exception_arg_error(), "UnresolvedFuture tag is not a Symbol")
993
+ })?;
994
+ match tag_name.as_ref() {
995
+ "first_completed" => Ok(UnresolvedFuture::FirstCompleted(children)),
996
+ "all_completed" => Ok(UnresolvedFuture::AllCompleted(children)),
997
+ "first_succeeded_or_all_failed" => {
998
+ Ok(UnresolvedFuture::FirstSucceededOrAllFailed(children))
999
+ }
1000
+ "all_succeeded_or_first_failed" => {
1001
+ Ok(UnresolvedFuture::AllSucceededOrFirstFailed(children))
1002
+ }
1003
+ "unknown" => Ok(UnresolvedFuture::Unknown(children)),
1004
+ other => Err(Error::new(
1005
+ ruby.exception_arg_error(),
1006
+ format!("Unknown UnresolvedFuture combinator: {other}"),
1007
+ )),
1008
+ }
1009
+ }
1010
+
929
1011
  fn parse_headers_array(ary: RArray) -> Result<Vec<Header>, Error> {
930
1012
  let mut result = Vec::new();
931
1013
  for item in ary.into_iter() {
@@ -1083,6 +1165,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1083
1165
  )?;
1084
1166
  ch_class.define_method("result_handle", method!(RbCallHandle::result_handle, 0))?;
1085
1167
 
1168
+ // Run
1169
+ let run_class = internal.define_class("Run", ruby.class_object())?;
1170
+ run_class.define_method("replayed", method!(RbRun::replayed, 0))?;
1171
+ run_class.define_method("handle", method!(RbRun::handle, 0))?;
1172
+
1086
1173
  // VM - all methods use explicit arities
1087
1174
  let vm_class = internal.define_class("VM", ruby.class_object())?;
1088
1175
  vm_class.define_singleton_method("new", function!(RbVM::new, 1))?;
@@ -1093,7 +1180,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1093
1180
  vm_class.define_method("take_output", method!(RbVM::take_output, 0))?;
1094
1181
  vm_class.define_method("is_ready_to_execute", method!(RbVM::is_ready_to_execute, 0))?;
1095
1182
  vm_class.define_method("is_completed", method!(RbVM::is_completed, 1))?;
1096
- vm_class.define_method("do_progress", method!(RbVM::do_progress, 1))?;
1183
+ vm_class.define_method("do_await", method!(RbVM::do_await, 1))?;
1097
1184
  vm_class.define_method("take_notification", method!(RbVM::take_notification, 1))?;
1098
1185
  vm_class.define_method("sys_input", method!(RbVM::sys_input, 0))?;
1099
1186
  vm_class.define_method("sys_get_state", method!(RbVM::sys_get_state, 1))?;
@@ -41,7 +41,7 @@ module Restate
41
41
  compact(
42
42
  protocolMode: protocol_mode,
43
43
  minProtocolVersion: 5,
44
- maxProtocolVersion: 6,
44
+ maxProtocolVersion: 7,
45
45
  services: services
46
46
  )
47
47
  end
@@ -15,15 +15,16 @@ module Restate
15
15
  @value = nil
16
16
  end
17
17
 
18
- # Block until the result is available and return it. Caches across calls.
18
+ # Block until the result is available and return it. Caches across calls,
19
+ # including failures — a second await on a failed future re-raises the
20
+ # same +TerminalError+ rather than re-fetching from the VM (the notification
21
+ # is single-shot).
19
22
  #
20
23
  # @return [Object] the deserialized result
21
24
  def await
22
- unless @resolved
23
- raw = @ctx.resolve_handle(@handle)
24
- @value = @serde ? @serde.deserialize(raw) : raw
25
- @resolved = true
26
- end
25
+ resolve! unless @resolved
26
+ raise @error if @error
27
+
27
28
  @value
28
29
  end
29
30
 
@@ -46,6 +47,17 @@ module Restate
46
47
 
47
48
  raise TimeoutError
48
49
  end
50
+
51
+ private
52
+
53
+ def resolve!
54
+ raw = @ctx.resolve_handle(@handle)
55
+ @value = @serde ? @serde.deserialize(raw) : raw
56
+ rescue TerminalError => e
57
+ @error = e
58
+ ensure
59
+ @resolved = true
60
+ end
49
61
  end
50
62
 
51
63
  # A durable future for service/object/workflow calls.
@@ -61,19 +73,31 @@ module Restate
61
73
  end
62
74
 
63
75
  # Block until the result is available and return it. Deserializes via +output_serde+.
76
+ # Caches both successes and TerminalError failures across calls.
64
77
  def await
65
- unless @resolved
66
- raw = @ctx.resolve_handle(@handle)
67
- @value = if raw.nil? || @output_serde.nil?
68
- raw
69
- else
70
- @output_serde.deserialize(raw)
71
- end
72
- @resolved = true
73
- end
78
+ resolve_call! unless @resolved
79
+ raise @error if @error
80
+
74
81
  @value
75
82
  end
76
83
 
84
+ private
85
+
86
+ def resolve_call!
87
+ raw = @ctx.resolve_handle(@handle)
88
+ @value = if raw.nil? || @output_serde.nil?
89
+ raw
90
+ else
91
+ @output_serde.deserialize(raw)
92
+ end
93
+ rescue TerminalError => e
94
+ @error = e
95
+ ensure
96
+ @resolved = true
97
+ end
98
+
99
+ public
100
+
77
101
  # Returns the invocation ID of the remote call. Lazily resolved.
78
102
  #
79
103
  # @return [String] the invocation ID
@@ -91,6 +115,122 @@ module Restate
91
115
  end
92
116
  end
93
117
 
118
+ # A lazy combinator over one or more child futures. The child set can mix
119
+ # +DurableFuture+ leaves (with raw handles) and nested +CombinedFuture+ nodes.
120
+ # The shared-core uses the tree shape to make suspension decisions; nothing
121
+ # blocks until +.await+ is called, so combinators are composable:
122
+ #
123
+ # Restate.race(Restate.all(a, b), c).await
124
+ #
125
+ # Supported variants (mirroring +UnresolvedFuture+ in restate-sdk-shared-core):
126
+ # :first_completed → JS Promise.race
127
+ # :all_succeeded_or_first_failed → JS Promise.all
128
+ # :first_succeeded_or_all_failed → JS Promise.any
129
+ # :all_completed → JS Promise.allSettled
130
+ class CombinedFuture
131
+ VALID_VARIANTS = %i[
132
+ first_completed
133
+ all_succeeded_or_first_failed
134
+ first_succeeded_or_all_failed
135
+ all_completed
136
+ ].freeze
137
+
138
+ def initialize(ctx, variant, children)
139
+ raise ArgumentError, "unknown combinator variant: #{variant}" unless VALID_VARIANTS.include?(variant)
140
+
141
+ @ctx = ctx
142
+ @variant = variant
143
+ @children = children
144
+ @resolved = false
145
+ @value = nil
146
+ @error = nil
147
+ end
148
+
149
+ # Recursive tree representation the native +do_await+ binding consumes.
150
+ # Leaves are integer handles; inner nodes are +[variant, [child...]]+ pairs.
151
+ def tree
152
+ [@variant, @children.map { |c| c.is_a?(CombinedFuture) ? c.tree : c.handle }]
153
+ end
154
+
155
+ # Block until this combinator settles per its variant. Caches results
156
+ # (including failures) across calls.
157
+ def await
158
+ resolve_combined! unless @resolved
159
+ raise @error if @error
160
+
161
+ @value
162
+ end
163
+
164
+ # Non-blocking introspection. True iff calling +.await+ is guaranteed not to
165
+ # block. Conservative for the variants that allow early-completion on failure
166
+ # (+:all_succeeded_or_first_failed+, +:first_succeeded_or_all_failed+) — we
167
+ # report false until every child is settled, because checking failure status
168
+ # of a leaf would require consuming its notification.
169
+ def completed?
170
+ return true if @resolved
171
+
172
+ case @variant
173
+ when :first_completed
174
+ @children.any?(&:completed?)
175
+ else
176
+ @children.all?(&:completed?)
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ def resolve_combined!
183
+ @ctx.wait_combined(tree)
184
+ @value = finalize_value
185
+ rescue TerminalError => e
186
+ @error = e
187
+ ensure
188
+ @resolved = true
189
+ end
190
+
191
+ def finalize_value
192
+ case @variant
193
+ when :first_completed then finalize_first_completed
194
+ when :all_succeeded_or_first_failed then finalize_all_succeeded
195
+ when :all_completed then finalize_all_completed
196
+ when :first_succeeded_or_all_failed then finalize_first_succeeded
197
+ end
198
+ end
199
+
200
+ def finalize_first_completed
201
+ @children.find(&:completed?).await
202
+ end
203
+
204
+ def finalize_all_succeeded
205
+ # Surface any settled-and-failed child first (short-circuit). After this
206
+ # scan, if no failure was raised, every child must be settled-and-success
207
+ # per AllSucceededOrFirstFailed semantics.
208
+ @children.each { |c| c.await if c.completed? }
209
+ @children.map(&:await)
210
+ end
211
+
212
+ def finalize_all_completed
213
+ @children.map do |c|
214
+ { status: :fulfilled, value: c.await }
215
+ rescue TerminalError => e
216
+ { status: :rejected, reason: e }
217
+ end
218
+ end
219
+
220
+ def finalize_first_succeeded
221
+ errors = [] # : Array[TerminalError]
222
+ @children.each do |c|
223
+ next unless c.completed?
224
+
225
+ return c.await
226
+ rescue TerminalError => e
227
+ errors << e
228
+ end
229
+ raise TerminalError.new("all futures failed: #{errors.map(&:message).join('; ')}",
230
+ status_code: 500)
231
+ end
232
+ end
233
+
94
234
  # A handle for fire-and-forget send operations.
95
235
  # Returned by +ctx.service_send+, +ctx.object_send+, +ctx.workflow_send+.
96
236
  class SendHandle
@@ -169,6 +169,44 @@ module Restate
169
169
  futures.partition(&:completed?)
170
170
  end
171
171
 
172
+ # Build a lazy combinator over the given futures and return it as a
173
+ # +CombinedFuture+. Nothing blocks until +.await+ is called, so the
174
+ # combinators compose: +Restate.race(Restate.all(a, b), c).await+.
175
+ #
176
+ # Semantics match JS +Promise.all+ — when awaited, returns the values in
177
+ # input order, short-circuiting on the first +TerminalError+.
178
+ def all(*futures)
179
+ futures = futures.first if futures.length == 1 && futures.first.is_a?(Array)
180
+ CombinedFuture.new(self, :all_succeeded_or_first_failed, futures)
181
+ end
182
+
183
+ # Lazy combinator. Awaiting returns the value of the first future to settle,
184
+ # or raises if it settled with a +TerminalError+. Matches JS +Promise.race+.
185
+ def race(*futures)
186
+ futures = futures.first if futures.length == 1 && futures.first.is_a?(Array)
187
+ raise ArgumentError, 'race requires at least one future' if futures.empty?
188
+
189
+ CombinedFuture.new(self, :first_completed, futures)
190
+ end
191
+
192
+ # Lazy combinator. Awaiting returns the value of the first successful future;
193
+ # raises only if every future fails terminally. Matches JS +Promise.any+.
194
+ def any(*futures)
195
+ futures = futures.first if futures.length == 1 && futures.first.is_a?(Array)
196
+ raise ArgumentError, 'any requires at least one future' if futures.empty?
197
+
198
+ CombinedFuture.new(self, :first_succeeded_or_all_failed, futures)
199
+ end
200
+
201
+ # Lazy combinator. Awaiting waits for every future to settle and returns
202
+ # an Array of +{ status: :fulfilled, value: ... }+ or
203
+ # +{ status: :rejected, reason: TerminalError }+ entries, in input order.
204
+ # Matches JS +Promise.allSettled+.
205
+ def all_settled(*futures)
206
+ futures = futures.first if futures.length == 1 && futures.first.is_a?(Array)
207
+ CombinedFuture.new(self, :all_completed, futures)
208
+ end
209
+
172
210
  # ── Durable run (side effect) ──
173
211
 
174
212
  # Executes a durable side effect. The block runs at most once; its result is
@@ -178,14 +216,18 @@ module Restate
178
216
  # fiber event loop responsive for other concurrent handlers. Use this for
179
217
  # CPU-intensive work.
180
218
  def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
181
- handle = @vm.sys_run(name)
182
-
183
- @run_coros_to_execute[handle] =
184
- if background
185
- -> { execute_run_threaded(handle, action, serde, retry_policy) }
186
- else
187
- -> { execute_run(handle, action, serde, retry_policy) }
188
- end
219
+ run = @vm.sys_run(name)
220
+ handle = run.handle
221
+
222
+ # Schedule the run closure only if the run wasn't replayed.
223
+ unless run.replayed
224
+ @run_coros_to_execute[handle] =
225
+ if background
226
+ -> { execute_run_threaded(handle, action, serde, retry_policy) }
227
+ else
228
+ -> { execute_run(handle, action, serde, retry_policy) }
229
+ end
230
+ end
189
231
 
190
232
  DurableFuture.new(self, handle, serde: serde)
191
233
  end
@@ -401,6 +443,14 @@ module Restate
401
443
  @invocation.key
402
444
  end
403
445
 
446
+ # Drive progress over a combinator tree. Returns when the combinator
447
+ # logically completes (the shared-core decides based on the tree shape).
448
+ # +future_tree+ follows the encoding documented in lib/restate/vm.rb#do_await.
449
+ # Public so +CombinedFuture#await+ can drive it via +@ctx.wait_combined+.
450
+ def wait_combined(future_tree)
451
+ progress_loop { @vm.do_await(future_tree) }
452
+ end
453
+
404
454
  private
405
455
 
406
456
  # ── Progress loop ──
@@ -411,13 +461,24 @@ module Restate
411
461
  must_take_notification(handle, &)
412
462
  end
413
463
 
464
+ # Wait for any of the given handles to complete. Wraps the flat handle
465
+ # list into a +FirstCompleted+ subtree (or a +Single+ leaf for one handle)
466
+ # and drives the VM via +do_await+ — same path as +wait_combined+.
414
467
  def poll_or_cancel(handles)
468
+ tree = handles.length == 1 ? handles.first : [:first_completed, handles]
469
+ wait_combined(tree)
470
+ end
471
+
472
+ # Shared progress-loop body for both the simple poll_or_cancel and the
473
+ # tree-aware wait_combined. The block makes one VM call per iteration and
474
+ # returns the response; this loop interprets it.
475
+ def progress_loop
415
476
  loop do
416
477
  flush_output
417
- response = @vm.do_progress(handles)
478
+ response = yield
418
479
 
419
480
  if response.is_a?(Exception)
420
- LOGGER.error("Exception in do_progress: #{response}")
481
+ LOGGER.error("Exception in progress loop: #{response}")
421
482
  raise InternalError
422
483
  end
423
484
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Restate
5
- VERSION = '0.14.0'
5
+ VERSION = '1.1.0'
6
6
  end
data/lib/restate/vm.rb CHANGED
@@ -90,8 +90,12 @@ module Restate
90
90
  @vm.is_completed(handle)
91
91
  end
92
92
 
93
- def do_progress(handles)
94
- result = @vm.do_progress(handles)
93
+ # Drive the VM forward against an +UnresolvedFuture+ tree. +future+ is either
94
+ # an Integer handle (Single leaf) or a +[tag_symbol, [children...]]+ pair
95
+ # (combinator node). See the Rust-side +parse_unresolved_future+ for the
96
+ # supported tags.
97
+ def do_await(future)
98
+ result = @vm.do_await(future)
95
99
  map_do_progress(result)
96
100
  rescue Internal::VMError => e
97
101
  e
data/lib/restate.rb CHANGED
@@ -293,6 +293,34 @@ module Restate # rubocop:disable Metrics/ModuleLength
293
293
  fetch_context!.wait_any(*futures)
294
294
  end
295
295
 
296
+ # Wait for every future to complete and return their values in input order.
297
+ # Short-circuits on the first +TerminalError+. Accepts either splat futures or
298
+ # a single Array. Semantics match JS +Promise.all+.
299
+ def all(*futures)
300
+ fetch_context!.all(*futures)
301
+ end
302
+
303
+ # Wait for the first future to settle and return its value. Raises if the
304
+ # winning future failed. Accepts either splat futures or a single Array.
305
+ # Semantics match JS +Promise.race+.
306
+ def race(*futures)
307
+ fetch_context!.race(*futures)
308
+ end
309
+
310
+ # Wait for the first successful future and return its value. Raises only if
311
+ # every future failed terminally. Accepts splat or single Array.
312
+ # Semantics match JS +Promise.any+.
313
+ def any(*futures)
314
+ fetch_context!.any(*futures)
315
+ end
316
+
317
+ # Wait for every future to settle. Returns an Array of outcome descriptors
318
+ # (+{status: :fulfilled, value: ...}+ or +{status: :rejected, reason: ...}+),
319
+ # in input order. Semantics match JS +Promise.allSettled+.
320
+ def all_settled(*futures)
321
+ fetch_context!.all_settled(*futures)
322
+ end
323
+
296
324
  # ── Request metadata ──
297
325
 
298
326
  # Returns metadata about the current invocation (id, headers, raw body).