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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc7f07e2b3bd62dbf82cb815a5b66443586e339bb3ae896e6b94594fdce9fab4
4
- data.tar.gz: 9066dc59318b6e14f249c249c59b03b8e075df6986cf2b2d8a3d9b93ca56614b
3
+ metadata.gz: 6c57bd9821f10bb65901d7da4ad7f6a38f9e64f81f36bd6eece1f2a9efcb7d57
4
+ data.tar.gz: c924c4d0f17f82012edaafba7d9867b2f78f2aecdd6fecc0a8f17cbd84c5f89a
5
5
  SHA512:
6
- metadata.gz: 79b2cee29852e255f9745665f44b8d4450badc22319c59120a2988c9ccd9dfa88723529ffb5059dbf4c94d929722368e00a5a1887c0e32b48663196513768860
7
- data.tar.gz: 8faf5c53ca4b7bc538ff348a3ccb99a0cdbac8a418d9263073f67a8476353d8c01fb714784c93237f644b35ea7f1ff08e89074a1e07a2761f9c0d59f892643d5
6
+ metadata.gz: 0153b6c412588938d243b1d806af7a4e38d9467a21296fd299b9a6c1da77296a208009a82ce264f7d2e6e722a03fcd7b33fa1e77525c736dde7f0286512ece83
7
+ data.tar.gz: 4218cc6a283aa5ddf29eaed2ffd9935769a098e58fa98d476687509da45c9ddf4e4bfc871b873cfe234c1239b44f4d4ad6810975e89b61e90017463b0a0bf3d7
data/Cargo.lock CHANGED
@@ -569,7 +569,7 @@ dependencies = [
569
569
 
570
570
  [[package]]
571
571
  name = "restate_internal"
572
- version = "0.6.0"
572
+ version = "0.7.0"
573
573
  dependencies = [
574
574
  "magnus",
575
575
  "rb-sys",
data/README.md CHANGED
@@ -13,8 +13,20 @@
13
13
  require 'restate'
14
14
 
15
15
  class Greeter < Restate::Service
16
- handler def greet(ctx, name)
17
- ctx.run_sync('build-greeting') { "Hello, #{name}!" }
16
+ handler def greet(name)
17
+ Restate.run_sync('build-greeting') { "Hello, #{name}!" }
18
+ end
19
+ end
20
+
21
+ class Counter < Restate::VirtualObject
22
+ state :count, default: 0
23
+
24
+ handler def add(addend)
25
+ self.count += addend
26
+ end
27
+
28
+ shared def get
29
+ count
18
30
  end
19
31
  end
20
32
  ```
@@ -67,8 +79,8 @@ end
67
79
 
68
80
  class EventService < Restate::Service
69
81
  handler :register, input: RegistrationRequest, output: RegistrationResponse
70
- def register(ctx, request)
71
- registration_id = ctx.run_sync('create-registration') do
82
+ def register(request)
83
+ registration_id = Restate.run_sync('create-registration') do
72
84
  "reg_#{request.event_name}_#{rand(10_000)}"
73
85
  end
74
86
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "restate_internal"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -0,0 +1,181 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'net/http'
5
+ require 'json'
6
+
7
+ module Restate
8
+ # HTTP client for invoking Restate services and managing the Restate runtime
9
+ # from outside the Restate runtime.
10
+ #
11
+ # @example Via global config (recommended)
12
+ # Restate.configure do |c|
13
+ # c.ingress_url = "http://localhost:8080"
14
+ # c.admin_url = "http://localhost:9070"
15
+ # end
16
+ # client = Restate.client
17
+ # result = client.service(Greeter).greet("World")
18
+ #
19
+ # @example Standalone
20
+ # client = Restate::Client.new(ingress_url: "http://localhost:8080",
21
+ # admin_url: "http://localhost:9070")
22
+ #
23
+ # @example Service invocation
24
+ # client.service("Greeter").greet("World")
25
+ # client.object("Counter", "my-key").add(5)
26
+ # client.workflow("UserSignup", "user42").run("user@example.com")
27
+ #
28
+ # @example Admin operations
29
+ # client.resolve_awakeable(awakeable_id, "result")
30
+ # client.reject_awakeable(awakeable_id, "failed")
31
+ # client.cancel_invocation(invocation_id)
32
+ # client.create_deployment("http://localhost:9080")
33
+ class Client
34
+ extend T::Sig
35
+
36
+ sig do
37
+ params(ingress_url: String, admin_url: String,
38
+ ingress_headers: T::Hash[String, String],
39
+ admin_headers: T::Hash[String, String]).void
40
+ end
41
+ def initialize(ingress_url: 'http://localhost:8080', admin_url: 'http://localhost:9070',
42
+ ingress_headers: {}, admin_headers: {})
43
+ @ingress_url = ingress_url.chomp('/')
44
+ @admin_url = admin_url.chomp('/')
45
+ @ingress_headers = ingress_headers
46
+ @admin_headers = admin_headers
47
+ end
48
+
49
+ # ── Service invocation proxies ──
50
+
51
+ # Returns a proxy for calling a stateless service.
52
+ sig { params(service: T.any(String, T::Class[T.anything])).returns(ClientServiceProxy) }
53
+ def service(service)
54
+ ClientServiceProxy.new(@ingress_url, resolve_name(service), nil, @ingress_headers)
55
+ end
56
+
57
+ # Returns a proxy for calling a keyed virtual object.
58
+ sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) }
59
+ def object(service, key)
60
+ ClientServiceProxy.new(@ingress_url, resolve_name(service), key, @ingress_headers)
61
+ end
62
+
63
+ # Returns a proxy for calling a workflow.
64
+ sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) }
65
+ def workflow(service, key)
66
+ ClientServiceProxy.new(@ingress_url, resolve_name(service), key, @ingress_headers)
67
+ end
68
+
69
+ # ── Awakeable operations ──
70
+
71
+ # Resolve an awakeable from outside the Restate runtime.
72
+ sig { params(awakeable_id: String, payload: T.untyped).void }
73
+ def resolve_awakeable(awakeable_id, payload)
74
+ post_ingress("/restate/awakeables/#{awakeable_id}/resolve", payload)
75
+ end
76
+
77
+ # Reject an awakeable from outside the Restate runtime.
78
+ sig { params(awakeable_id: String, message: String, code: Integer).void }
79
+ def reject_awakeable(awakeable_id, message, code: 500)
80
+ post_ingress("/restate/awakeables/#{awakeable_id}/reject",
81
+ { 'message' => message, 'code' => code })
82
+ end
83
+
84
+ # ── Invocation management ──
85
+
86
+ # Cancel a running invocation.
87
+ sig { params(invocation_id: String).void }
88
+ def cancel_invocation(invocation_id)
89
+ post_admin("/restate/invocations/#{invocation_id}/cancel", nil)
90
+ end
91
+
92
+ # Kill a running invocation (immediate termination, no cleanup).
93
+ sig { params(invocation_id: String).void }
94
+ def kill_invocation(invocation_id)
95
+ post_admin("/restate/invocations/#{invocation_id}/kill", nil)
96
+ end
97
+
98
+ private
99
+
100
+ sig { params(service: T.any(String, T::Class[T.anything])).returns(String) }
101
+ def resolve_name(service)
102
+ if service.is_a?(Class) && service.respond_to?(:service_name)
103
+ T.unsafe(service).service_name
104
+ else
105
+ service.to_s
106
+ end
107
+ end
108
+
109
+ sig { params(path: String, body: T.untyped).returns(T.untyped) }
110
+ def post_ingress(path, body) # rubocop:disable Metrics/AbcSize
111
+ uri = URI("#{@ingress_url}#{path}")
112
+ request = Net::HTTP::Post.new(uri)
113
+ request['Content-Type'] = 'application/json'
114
+ @ingress_headers.each { |k, v| request[k] = v }
115
+ request.body = JSON.generate(body) if body
116
+ response = Net::HTTP.start(uri.hostname, uri.port,
117
+ use_ssl: uri.scheme == 'https',
118
+ read_timeout: 30) { |http| http.request(request) }
119
+ Kernel.raise "Restate ingress error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
120
+ parse_response(response)
121
+ end
122
+
123
+ sig { params(path: String, body: T.untyped).returns(T.untyped) }
124
+ def post_admin(path, body) # rubocop:disable Metrics/AbcSize
125
+ uri = URI("#{@admin_url}#{path}")
126
+ request = Net::HTTP::Post.new(uri)
127
+ request['Content-Type'] = 'application/json'
128
+ @admin_headers.each { |k, v| request[k] = v }
129
+ request.body = JSON.generate(body) if body
130
+ response = Net::HTTP.start(uri.hostname, uri.port,
131
+ use_ssl: uri.scheme == 'https',
132
+ read_timeout: 30) { |http| http.request(request) }
133
+ Kernel.raise "Restate admin error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
134
+ parse_response(response)
135
+ end
136
+
137
+ sig { params(response: Net::HTTPResponse).returns(T.untyped) }
138
+ def parse_response(response)
139
+ body = response.body
140
+ body && !body.empty? ? JSON.parse(body) : nil
141
+ end
142
+ end
143
+
144
+ # Proxy that sends HTTP requests to the Restate ingress for a specific service.
145
+ # Handler calls are forwarded via +method_missing+.
146
+ #
147
+ # @!visibility private
148
+ class ClientServiceProxy
149
+ extend T::Sig
150
+
151
+ sig do
152
+ params(base_url: String, service_name: String, key: T.nilable(String),
153
+ headers: T::Hash[String, String]).void
154
+ end
155
+ def initialize(base_url, service_name, key, headers)
156
+ @base_url = base_url
157
+ @service_name = service_name
158
+ @key = key
159
+ @headers = headers
160
+ end
161
+
162
+ def method_missing(handler_name, arg = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
163
+ path = @key ? "/#{@service_name}/#{@key}/#{handler_name}" : "/#{@service_name}/#{handler_name}"
164
+ uri = URI("#{@base_url}#{path}")
165
+ request = Net::HTTP::Post.new(uri)
166
+ request['Content-Type'] = 'application/json'
167
+ @headers.each { |k, v| request[k] = v }
168
+ request.body = JSON.generate(arg)
169
+ response = Net::HTTP.start(uri.hostname, uri.port,
170
+ use_ssl: uri.scheme == 'https',
171
+ read_timeout: 30) { |http| http.request(request) }
172
+ Kernel.raise "Restate ingress error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
173
+ body = response.body
174
+ body && !body.empty? ? JSON.parse(body) : nil
175
+ end
176
+
177
+ def respond_to_missing?(_method_name, _include_private = false)
178
+ true
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,42 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Restate
5
+ # Global SDK configuration. Set via +Restate.configure+.
6
+ #
7
+ # @example
8
+ # Restate.configure do |c|
9
+ # c.ingress_url = "http://localhost:8080"
10
+ # c.admin_url = "http://localhost:9070"
11
+ # end
12
+ #
13
+ # # Then use the pre-configured client:
14
+ # Restate.client.service(Greeter).greet("World")
15
+ class Config
16
+ extend T::Sig
17
+
18
+ # Restate ingress URL (for invoking services).
19
+ sig { returns(String) }
20
+ attr_accessor :ingress_url
21
+
22
+ # Restate admin URL (for deployments, invocation management).
23
+ sig { returns(String) }
24
+ attr_accessor :admin_url
25
+
26
+ # Default headers sent with every ingress request.
27
+ sig { returns(T::Hash[String, String]) }
28
+ attr_accessor :ingress_headers
29
+
30
+ # Default headers sent with every admin request.
31
+ sig { returns(T::Hash[String, String]) }
32
+ attr_accessor :admin_headers
33
+
34
+ sig { void }
35
+ def initialize
36
+ @ingress_url = T.let('http://localhost:8080', String)
37
+ @admin_url = T.let('http://localhost:9070', String)
38
+ @ingress_headers = T.let({}, T::Hash[String, String])
39
+ @admin_headers = T.let({}, T::Hash[String, String])
40
+ end
41
+ end
42
+ end
@@ -41,19 +41,20 @@ module Restate
41
41
  end
42
42
  def invoke_handler(handler:, ctx:, in_buffer:, middleware: []) # rubocop:disable Metrics/AbcSize
43
43
  call_handler = Kernel.proc do
44
- if handler.arity == 2
44
+ if handler.arity == 1
45
45
  begin
46
46
  in_arg = handler.handler_io.input_serde.deserialize(in_buffer)
47
47
  rescue StandardError => e
48
48
  Kernel.raise TerminalError, "Unable to parse input argument: #{e.message}"
49
49
  end
50
- handler.callable.call(ctx, in_arg)
50
+ handler.callable.call(in_arg)
51
51
  else
52
- handler.callable.call(ctx)
52
+ handler.callable.call
53
53
  end
54
54
  end
55
55
 
56
56
  # Build the middleware chain so each middleware can use `yield` to call the next.
57
+ # Middleware still receives (handler, ctx) for low-level access.
57
58
  chain = middleware.reverse.reduce(call_handler) do |nxt, mw|
58
59
  Kernel.proc { mw.call(handler, ctx, &nxt) }
59
60
  end
@@ -30,6 +30,28 @@ module Restate
30
30
  _register_handler(method_name, **T.unsafe({ kind: nil, **opts }))
31
31
  end
32
32
 
33
+ # Returns a call proxy for fluent durable calls to this service.
34
+ #
35
+ # @example
36
+ # Greeter.call.greet("World").await
37
+ #
38
+ # @return [ServiceCallProxy]
39
+ def self.call
40
+ ServiceCallProxy.new(self, call_method: :service_call)
41
+ end
42
+
43
+ # Returns a send proxy for fluent fire-and-forget sends to this service.
44
+ #
45
+ # @example
46
+ # Greeter.send!.greet("World")
47
+ # Greeter.send!(delay: 60).greet("World")
48
+ #
49
+ # @param delay [Numeric, nil] optional delay in seconds
50
+ # @return [ServiceSendProxy]
51
+ def self.send!(delay: nil)
52
+ ServiceSendProxy.new(self, send_method: :service_send, delay: delay)
53
+ end
54
+
33
55
  def self._service_kind
34
56
  'service'
35
57
  end
@@ -31,6 +31,70 @@ module Restate
31
31
  subclass.instance_variable_set(:@_idempotency_retention, nil)
32
32
  subclass.instance_variable_set(:@_ingress_private, nil)
33
33
  subclass.instance_variable_set(:@_invocation_retry_policy, nil)
34
+ subclass.instance_variable_set(:@_state_declarations, {})
35
+ end
36
+
37
+ # Declare a durable state entry with auto-generated getter, setter, and clear methods.
38
+ # Only available on VirtualObject and Workflow.
39
+ #
40
+ # The generated methods delegate to the current Restate context via +Thread.current+
41
+ # (fiber-scoped in Ruby 3.0+), so they work correctly across concurrent invocations.
42
+ #
43
+ # @param name [Symbol] state key name
44
+ # @param default [Object, nil] default value returned when state is not set
45
+ # @param serde [Object] serializer/deserializer (defaults to JsonSerde)
46
+ #
47
+ # @example
48
+ # class Counter < Restate::VirtualObject
49
+ # state :count, default: 0
50
+ #
51
+ # handler def add(ctx, addend)
52
+ # self.count += addend # reads then writes via ctx.get/ctx.set
53
+ # end
54
+ #
55
+ # shared def get(ctx)
56
+ # count # reads via ctx.get, returns 0 if unset
57
+ # end
58
+ # end
59
+ def state(name, default: nil, serde: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
60
+ unless T.unsafe(self).respond_to?(:_service_kind) && %w[object workflow].include?(T.unsafe(self)._service_kind)
61
+ Kernel.raise ArgumentError, 'state declarations are only available on VirtualObject and Workflow'
62
+ end
63
+
64
+ name = name.to_sym
65
+ @_state_declarations[name] = { default: default, serde: serde }
66
+ state_key = name.to_s
67
+ state_serde = serde
68
+ state_default = default
69
+
70
+ # Getter: reads from durable state, returns default if unset
71
+ T.unsafe(self).define_method(name) do
72
+ ctx = Thread.current[:restate_context]
73
+ Kernel.raise 'Not inside a Restate handler' unless ctx
74
+
75
+ val = state_serde ? ctx.get(state_key, serde: state_serde) : ctx.get(state_key)
76
+ val.nil? ? state_default : val
77
+ end
78
+
79
+ # Setter: writes to durable state
80
+ T.unsafe(self).define_method(:"#{name}=") do |value|
81
+ ctx = Thread.current[:restate_context]
82
+ Kernel.raise 'Not inside a Restate handler' unless ctx
83
+
84
+ if state_serde
85
+ ctx.set(state_key, value, serde: state_serde)
86
+ else
87
+ ctx.set(state_key, value)
88
+ end
89
+ end
90
+
91
+ # Clear: removes the state entry
92
+ T.unsafe(self).define_method(:"clear_#{name}") do
93
+ ctx = Thread.current[:restate_context]
94
+ Kernel.raise 'Not inside a Restate handler' unless ctx
95
+
96
+ ctx.clear(state_key)
97
+ end
34
98
  end
35
99
 
36
100
  # Get or set the service name. Defaults to the unqualified class name.
@@ -210,8 +274,8 @@ module Restate
210
274
 
211
275
  um = T.unsafe(self).instance_method(name)
212
276
  arity = um.arity.abs
213
- unless [1, 2].include?(arity)
214
- Kernel.raise ArgumentError, "handler '#{name}' must accept 1 or 2 parameters (ctx[, input]), got #{arity}"
277
+ unless [0, 1].include?(arity)
278
+ Kernel.raise ArgumentError, "handler '#{name}' must accept 0 or 1 parameters ([input]), got #{arity}"
215
279
  end
216
280
 
217
281
  bound = um.bind(instance)
@@ -0,0 +1,84 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Restate
5
+ # Proxy for fluent durable calls: +Service.call.handler(arg)+
6
+ #
7
+ # Returned by the +.call+ class method on service classes. Uses +method_missing+
8
+ # to forward handler invocations to the current Restate context.
9
+ #
10
+ # @example
11
+ # # Instead of: ctx.service_call(Greeter, :greet, "World")
12
+ # Greeter.call.greet("World")
13
+ #
14
+ # # Instead of: ctx.object_call(Counter, :add, "key", 5)
15
+ # Counter.call("key").add(5)
16
+ #
17
+ # @!visibility private
18
+ class ServiceCallProxy
19
+ extend T::Sig
20
+
21
+ sig { params(service_class: T.untyped, key: T.nilable(String), call_method: Symbol).void }
22
+ def initialize(service_class, key: nil, call_method: :service_call)
23
+ @service_class = service_class
24
+ @key = key
25
+ @call_method = call_method
26
+ end
27
+
28
+ def method_missing(handler_name, arg = nil, **opts)
29
+ ctx = Restate.fetch_context!
30
+ if @key
31
+ ctx.public_send(@call_method, @service_class, handler_name, @key, arg, **opts)
32
+ else
33
+ ctx.public_send(@call_method, @service_class, handler_name, arg, **opts)
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(method_name, include_private = false)
38
+ (@service_class.respond_to?(:handlers) && T.unsafe(@service_class).handlers.key?(method_name.to_s)) || super
39
+ end
40
+ end
41
+
42
+ # Proxy for fluent fire-and-forget sends: +Service.send!.handler(arg)+
43
+ #
44
+ # Returned by the +.send!+ class method on service classes.
45
+ #
46
+ # @example
47
+ # # Instead of: ctx.service_send(Greeter, :greet, "World")
48
+ # Greeter.send!.greet("World")
49
+ #
50
+ # # Instead of: ctx.object_send(Counter, :add, "key", 5, delay: 60)
51
+ # Counter.send!("key", delay: 60).add(5)
52
+ #
53
+ # @!visibility private
54
+ class ServiceSendProxy
55
+ extend T::Sig
56
+
57
+ sig do
58
+ params(
59
+ service_class: T.untyped, key: T.nilable(String),
60
+ send_method: Symbol, delay: T.nilable(Numeric)
61
+ ).void
62
+ end
63
+ def initialize(service_class, key: nil, send_method: :service_send, delay: nil)
64
+ @service_class = service_class
65
+ @key = key
66
+ @send_method = send_method
67
+ @delay = delay
68
+ end
69
+
70
+ def method_missing(handler_name, arg = nil, **opts)
71
+ ctx = Restate.fetch_context!
72
+ opts[:delay] = @delay if @delay
73
+ if @key
74
+ ctx.public_send(@send_method, @service_class, handler_name, @key, arg, **opts)
75
+ else
76
+ ctx.public_send(@send_method, @service_class, handler_name, arg, **opts)
77
+ end
78
+ end
79
+
80
+ def respond_to_missing?(method_name, include_private = false)
81
+ (@service_class.respond_to?(:handlers) && T.unsafe(@service_class).handlers.key?(method_name.to_s)) || super
82
+ end
83
+ end
84
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Restate
5
- VERSION = '0.6.0'
5
+ VERSION = '0.7.0'
6
6
  end
@@ -49,6 +49,30 @@ module Restate
49
49
  _register_handler(method_name, **T.unsafe({ kind: 'shared', **opts }))
50
50
  end
51
51
 
52
+ # Returns a call proxy for fluent durable calls to this virtual object.
53
+ #
54
+ # @example
55
+ # Counter.call("my-key").add(5).await
56
+ #
57
+ # @param key [String] the object key
58
+ # @return [ServiceCallProxy]
59
+ def self.call(key)
60
+ ServiceCallProxy.new(self, key: key, call_method: :object_call)
61
+ end
62
+
63
+ # Returns a send proxy for fluent fire-and-forget sends to this virtual object.
64
+ #
65
+ # @example
66
+ # Counter.send!("my-key").add(5)
67
+ # Counter.send!("my-key", delay: 60).add(5)
68
+ #
69
+ # @param key [String] the object key
70
+ # @param delay [Numeric, nil] optional delay in seconds
71
+ # @return [ServiceSendProxy]
72
+ def self.send!(key, delay: nil)
73
+ ServiceSendProxy.new(self, key: key, send_method: :object_send, delay: delay)
74
+ end
75
+
52
76
  def self._service_kind
53
77
  'object'
54
78
  end
@@ -49,6 +49,30 @@ module Restate
49
49
  _register_handler(method_name, **T.unsafe({ kind: 'shared', **opts }))
50
50
  end
51
51
 
52
+ # Returns a call proxy for fluent durable calls to this workflow.
53
+ #
54
+ # @example
55
+ # UserSignup.call("user42").run("user@example.com").await
56
+ #
57
+ # @param key [String] the workflow key
58
+ # @return [ServiceCallProxy]
59
+ def self.call(key)
60
+ ServiceCallProxy.new(self, key: key, call_method: :workflow_call)
61
+ end
62
+
63
+ # Returns a send proxy for fluent fire-and-forget sends to this workflow.
64
+ #
65
+ # @example
66
+ # UserSignup.send!("user42").run("user@example.com")
67
+ # UserSignup.send!("user42", delay: 60).run("user@example.com")
68
+ #
69
+ # @param key [String] the workflow key
70
+ # @param delay [Numeric, nil] optional delay in seconds
71
+ # @return [ServiceSendProxy]
72
+ def self.send!(key, delay: nil)
73
+ ServiceSendProxy.new(self, key: key, send_method: :workflow_send, delay: delay)
74
+ end
75
+
52
76
  def self._service_kind
53
77
  'workflow'
54
78
  end