@restatedev/restate-sdk 0.7.3-worker → 0.8.1

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.
Files changed (231) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -51
  3. package/dist/clients/workflow_client.d.ts +77 -0
  4. package/dist/clients/workflow_client.d.ts.map +1 -0
  5. package/dist/clients/workflow_client.js +172 -0
  6. package/dist/clients/workflow_client.js.map +1 -0
  7. package/dist/connection/buffered_connection.js +44 -0
  8. package/dist/connection/buffered_connection.js.map +1 -0
  9. package/dist/connection/connection.js +13 -0
  10. package/dist/connection/connection.js.map +1 -0
  11. package/dist/connection/embedded_connection.js +59 -0
  12. package/dist/connection/embedded_connection.js.map +1 -0
  13. package/dist/connection/http_connection.js +203 -0
  14. package/dist/connection/http_connection.js.map +1 -0
  15. package/dist/connection/lambda_connection.js +58 -0
  16. package/dist/connection/lambda_connection.js.map +1 -0
  17. package/dist/{restate_context.d.ts → context.d.ts} +239 -170
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +113 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/{restate_context_impl.d.ts → context_impl.d.ts} +26 -30
  22. package/dist/context_impl.d.ts.map +1 -0
  23. package/dist/context_impl.js +439 -0
  24. package/dist/context_impl.js.map +1 -0
  25. package/dist/embedded/api.d.ts +2 -2
  26. package/dist/embedded/api.d.ts.map +1 -1
  27. package/dist/embedded/api.js +35 -0
  28. package/dist/embedded/api.js.map +1 -0
  29. package/dist/embedded/handler.d.ts +2 -2
  30. package/dist/embedded/handler.d.ts.map +1 -1
  31. package/dist/embedded/handler.js +26 -0
  32. package/dist/embedded/handler.js.map +1 -0
  33. package/dist/embedded/http2_remote.js +91 -0
  34. package/dist/embedded/http2_remote.js.map +1 -0
  35. package/dist/embedded/invocation.d.ts.map +1 -1
  36. package/dist/embedded/invocation.js +94 -0
  37. package/dist/embedded/invocation.js.map +1 -0
  38. package/dist/endpoint/endpoint_impl.d.ts +35 -0
  39. package/dist/endpoint/endpoint_impl.d.ts.map +1 -0
  40. package/dist/endpoint/endpoint_impl.js +405 -0
  41. package/dist/endpoint/endpoint_impl.js.map +1 -0
  42. package/dist/endpoint/http2_handler.d.ts +11 -0
  43. package/dist/endpoint/http2_handler.d.ts.map +1 -0
  44. package/dist/endpoint/http2_handler.js +119 -0
  45. package/dist/endpoint/http2_handler.js.map +1 -0
  46. package/dist/endpoint/lambda_handler.d.ts +15 -0
  47. package/dist/endpoint/lambda_handler.d.ts.map +1 -0
  48. package/dist/endpoint/lambda_handler.js +144 -0
  49. package/dist/endpoint/lambda_handler.js.map +1 -0
  50. package/dist/endpoint.d.ts +161 -0
  51. package/dist/endpoint.d.ts.map +1 -0
  52. package/dist/endpoint.js +22 -0
  53. package/dist/endpoint.js.map +1 -0
  54. package/dist/generated/dev/restate/events.js +371 -0
  55. package/dist/generated/dev/restate/events.js.map +1 -0
  56. package/dist/generated/dev/restate/ext.js +215 -0
  57. package/dist/generated/dev/restate/ext.js.map +1 -0
  58. package/dist/generated/google/protobuf/descriptor.js +6676 -0
  59. package/dist/generated/google/protobuf/descriptor.js.map +1 -0
  60. package/dist/generated/google/protobuf/empty.js +107 -0
  61. package/dist/generated/google/protobuf/empty.js.map +1 -0
  62. package/dist/generated/google/protobuf/struct.js +754 -0
  63. package/dist/generated/google/protobuf/struct.js.map +1 -0
  64. package/dist/generated/proto/discovery.js +364 -0
  65. package/dist/generated/proto/discovery.js.map +1 -0
  66. package/dist/generated/proto/dynrpc.js +668 -0
  67. package/dist/generated/proto/dynrpc.js.map +1 -0
  68. package/dist/generated/proto/javascript.d.ts +13 -0
  69. package/dist/generated/proto/javascript.d.ts.map +1 -1
  70. package/dist/generated/proto/javascript.js +416 -0
  71. package/dist/generated/proto/javascript.js.map +1 -0
  72. package/dist/generated/proto/protocol.d.ts +43 -0
  73. package/dist/generated/proto/protocol.d.ts.map +1 -1
  74. package/dist/generated/proto/protocol.js +2641 -0
  75. package/dist/generated/proto/protocol.js.map +1 -0
  76. package/dist/generated/proto/services.js +1535 -0
  77. package/dist/generated/proto/services.js.map +1 -0
  78. package/dist/generated/proto/test.js +321 -0
  79. package/dist/generated/proto/test.js.map +1 -0
  80. package/dist/invocation.d.ts +4 -1
  81. package/dist/invocation.d.ts.map +1 -1
  82. package/dist/invocation.js +157 -0
  83. package/dist/invocation.js.map +1 -0
  84. package/dist/io/decoder.d.ts +1 -0
  85. package/dist/io/decoder.d.ts.map +1 -1
  86. package/dist/io/decoder.js +140 -0
  87. package/dist/io/decoder.js.map +1 -0
  88. package/dist/io/encoder.d.ts +1 -2
  89. package/dist/io/encoder.d.ts.map +1 -1
  90. package/dist/io/encoder.js +68 -0
  91. package/dist/io/encoder.js.map +1 -0
  92. package/dist/journal.d.ts +13 -4
  93. package/dist/journal.d.ts.map +1 -1
  94. package/dist/journal.js +405 -0
  95. package/dist/journal.js.map +1 -0
  96. package/dist/local_state_store.d.ts +5 -3
  97. package/dist/local_state_store.d.ts.map +1 -1
  98. package/dist/local_state_store.js +82 -0
  99. package/dist/local_state_store.js.map +1 -0
  100. package/dist/logger.d.ts +19 -0
  101. package/dist/logger.d.ts.map +1 -0
  102. package/dist/logger.js +90 -0
  103. package/dist/logger.js.map +1 -0
  104. package/dist/promise_combinator_tracker.d.ts +29 -0
  105. package/dist/promise_combinator_tracker.d.ts.map +1 -0
  106. package/dist/promise_combinator_tracker.js +128 -0
  107. package/dist/promise_combinator_tracker.js.map +1 -0
  108. package/dist/public_api.d.ts +5 -5
  109. package/dist/public_api.d.ts.map +1 -1
  110. package/dist/public_api.js +60 -0
  111. package/dist/public_api.js.map +1 -0
  112. package/dist/state_machine.d.ts +19 -12
  113. package/dist/state_machine.d.ts.map +1 -1
  114. package/dist/state_machine.js +437 -0
  115. package/dist/state_machine.js.map +1 -0
  116. package/dist/types/errors.d.ts +12 -3
  117. package/dist/types/errors.d.ts.map +1 -1
  118. package/dist/types/errors.js +273 -0
  119. package/dist/types/errors.js.map +1 -0
  120. package/dist/types/grpc.d.ts +6 -4
  121. package/dist/types/grpc.d.ts.map +1 -1
  122. package/dist/types/grpc.js +81 -0
  123. package/dist/types/grpc.js.map +1 -0
  124. package/dist/types/protocol.d.ts +9 -5
  125. package/dist/types/protocol.d.ts.map +1 -1
  126. package/dist/types/protocol.js +147 -0
  127. package/dist/types/protocol.js.map +1 -0
  128. package/dist/types/router.d.ts +8 -8
  129. package/dist/types/router.d.ts.map +1 -1
  130. package/dist/types/router.js +36 -0
  131. package/dist/types/router.js.map +1 -0
  132. package/dist/types/types.d.ts +1 -0
  133. package/dist/types/types.d.ts.map +1 -1
  134. package/dist/types/types.js +138 -0
  135. package/dist/types/types.js.map +1 -0
  136. package/dist/utils/{assumpsions.d.ts → assumptions.d.ts} +1 -1
  137. package/dist/utils/{assumpsions.d.ts.map → assumptions.d.ts.map} +1 -1
  138. package/dist/utils/assumptions.js +101 -0
  139. package/dist/utils/assumptions.js.map +1 -0
  140. package/dist/utils/message_logger.d.ts +28 -0
  141. package/dist/utils/message_logger.d.ts.map +1 -0
  142. package/dist/utils/message_logger.js +88 -0
  143. package/dist/utils/message_logger.js.map +1 -0
  144. package/dist/utils/promises.d.ts +15 -0
  145. package/dist/utils/promises.d.ts.map +1 -0
  146. package/dist/utils/promises.js +67 -0
  147. package/dist/utils/promises.js.map +1 -0
  148. package/dist/utils/public_utils.js +49 -0
  149. package/dist/utils/public_utils.js.map +1 -0
  150. package/dist/utils/rand.d.ts +1 -1
  151. package/dist/utils/rand.d.ts.map +1 -1
  152. package/dist/utils/rand.js +114 -0
  153. package/dist/utils/rand.js.map +1 -0
  154. package/dist/utils/utils.d.ts +1 -10
  155. package/dist/utils/utils.d.ts.map +1 -1
  156. package/dist/utils/utils.js +122 -0
  157. package/dist/utils/utils.js.map +1 -0
  158. package/dist/workflows/workflow.d.ts +101 -0
  159. package/dist/workflows/workflow.d.ts.map +1 -0
  160. package/dist/workflows/workflow.js +80 -0
  161. package/dist/workflows/workflow.js.map +1 -0
  162. package/dist/workflows/workflow_state_service.d.ts +35 -0
  163. package/dist/workflows/workflow_state_service.d.ts.map +1 -0
  164. package/dist/workflows/workflow_state_service.js +201 -0
  165. package/dist/workflows/workflow_state_service.js.map +1 -0
  166. package/dist/workflows/workflow_wrapper_service.d.ts +10 -0
  167. package/dist/workflows/workflow_wrapper_service.d.ts.map +1 -0
  168. package/dist/workflows/workflow_wrapper_service.js +264 -0
  169. package/dist/workflows/workflow_wrapper_service.js.map +1 -0
  170. package/package.json +38 -39
  171. package/src/clients/workflow_client.ts +290 -0
  172. package/src/connection/buffered_connection.ts +47 -0
  173. package/src/connection/connection.ts +34 -0
  174. package/src/connection/embedded_connection.ts +62 -0
  175. package/src/connection/http_connection.ts +228 -0
  176. package/src/connection/lambda_connection.ts +69 -0
  177. package/src/context.ts +633 -0
  178. package/src/context_impl.ts +721 -0
  179. package/src/embedded/api.ts +57 -0
  180. package/src/embedded/handler.ts +36 -0
  181. package/src/embedded/http2_remote.ts +103 -0
  182. package/src/embedded/invocation.ts +126 -0
  183. package/src/endpoint/endpoint_impl.ts +623 -0
  184. package/src/endpoint/http2_handler.ts +151 -0
  185. package/src/endpoint/lambda_handler.ts +181 -0
  186. package/src/endpoint.ts +187 -0
  187. package/src/generated/dev/restate/events.ts +430 -0
  188. package/src/generated/dev/restate/ext.ts +238 -0
  189. package/src/generated/google/protobuf/descriptor.ts +7889 -0
  190. package/src/generated/google/protobuf/empty.ts +150 -0
  191. package/src/generated/google/protobuf/struct.ts +878 -0
  192. package/src/generated/proto/discovery.ts +423 -0
  193. package/src/generated/proto/dynrpc.ts +768 -0
  194. package/src/generated/proto/javascript.ts +488 -0
  195. package/src/generated/proto/protocol.ts +3091 -0
  196. package/src/generated/proto/services.ts +1834 -0
  197. package/src/generated/proto/test.ts +387 -0
  198. package/src/invocation.ts +212 -0
  199. package/src/io/decoder.ts +171 -0
  200. package/src/io/encoder.ts +72 -0
  201. package/src/journal.ts +537 -0
  202. package/src/local_state_store.ts +94 -0
  203. package/src/logger.ts +121 -0
  204. package/src/promise_combinator_tracker.ts +191 -0
  205. package/src/public_api.ts +53 -0
  206. package/src/state_machine.ts +635 -0
  207. package/src/types/errors.ts +297 -0
  208. package/src/types/grpc.ts +97 -0
  209. package/src/types/protocol.ts +201 -0
  210. package/src/types/router.ts +118 -0
  211. package/src/types/types.ts +160 -0
  212. package/src/utils/assumptions.ts +131 -0
  213. package/src/utils/message_logger.ts +112 -0
  214. package/src/utils/promises.ts +118 -0
  215. package/src/utils/public_utils.ts +91 -0
  216. package/src/utils/rand.ts +142 -0
  217. package/src/utils/utils.ts +178 -0
  218. package/src/workflows/workflow.ts +178 -0
  219. package/src/workflows/workflow_state_service.ts +299 -0
  220. package/src/workflows/workflow_wrapper_service.ts +314 -0
  221. package/dist/cloudflare_bundle.js +0 -27387
  222. package/dist/restate_context.d.ts.map +0 -1
  223. package/dist/restate_context_impl.d.ts.map +0 -1
  224. package/dist/server/base_restate_server.d.ts +0 -32
  225. package/dist/server/base_restate_server.d.ts.map +0 -1
  226. package/dist/server/restate_lambda_handler.d.ts +0 -104
  227. package/dist/server/restate_lambda_handler.d.ts.map +0 -1
  228. package/dist/server/restate_server.d.ts +0 -97
  229. package/dist/server/restate_server.d.ts.map +0 -1
  230. package/dist/utils/logger.d.ts +0 -60
  231. package/dist/utils/logger.d.ts.map +0 -1
@@ -0,0 +1,721 @@
1
+ /*
2
+ * Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH
3
+ *
4
+ * This file is part of the Restate SDK for Node.js/TypeScript,
5
+ * which is released under the MIT license.
6
+ *
7
+ * You can find a copy of the license in file LICENSE in the root
8
+ * directory of this repository or package, or at
9
+ * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
10
+ */
11
+
12
+ import {
13
+ CombineablePromise,
14
+ KeyedContext,
15
+ Rand,
16
+ RestateGrpcChannel,
17
+ ServiceApi,
18
+ } from "./context";
19
+ import { StateMachine } from "./state_machine";
20
+ import {
21
+ AwakeableEntryMessage,
22
+ BackgroundInvokeEntryMessage,
23
+ CompleteAwakeableEntryMessage,
24
+ DeepPartial,
25
+ GetStateEntryMessage,
26
+ GetStateKeysEntryMessage,
27
+ GetStateKeysEntryMessage_StateKeys,
28
+ InvokeEntryMessage,
29
+ SleepEntryMessage,
30
+ } from "./generated/proto/protocol";
31
+ import {
32
+ AWAKEABLE_ENTRY_MESSAGE_TYPE,
33
+ AWAKEABLE_IDENTIFIER_PREFIX,
34
+ BACKGROUND_INVOKE_ENTRY_MESSAGE_TYPE,
35
+ CLEAR_ALL_STATE_ENTRY_MESSAGE_TYPE,
36
+ CLEAR_STATE_ENTRY_MESSAGE_TYPE,
37
+ COMPLETE_AWAKEABLE_ENTRY_MESSAGE_TYPE,
38
+ GET_STATE_ENTRY_MESSAGE_TYPE,
39
+ GET_STATE_KEYS_ENTRY_MESSAGE_TYPE,
40
+ INVOKE_ENTRY_MESSAGE_TYPE,
41
+ SET_STATE_ENTRY_MESSAGE_TYPE,
42
+ SIDE_EFFECT_ENTRY_MESSAGE_TYPE,
43
+ SLEEP_ENTRY_MESSAGE_TYPE,
44
+ } from "./types/protocol";
45
+ import { SideEffectEntryMessage } from "./generated/proto/javascript";
46
+ import { AsyncLocalStorage } from "async_hooks";
47
+ import {
48
+ ErrorCodes,
49
+ RestateErrorCodes,
50
+ RestateError,
51
+ RetryableError,
52
+ TerminalError,
53
+ ensureError,
54
+ errorToFailureWithTerminal,
55
+ TimeoutError,
56
+ } from "./types/errors";
57
+ import { jsonSerialize, jsonDeserialize } from "./utils/utils";
58
+ import { Empty } from "./generated/google/protobuf/empty";
59
+ import {
60
+ DEFAULT_INFINITE_EXPONENTIAL_BACKOFF,
61
+ DEFAULT_INITIAL_DELAY_MS,
62
+ EXPONENTIAL_BACKOFF,
63
+ RetrySettings,
64
+ } from "./utils/public_utils";
65
+ import { Client, SendClient } from "./types/router";
66
+ import { RpcRequest, RpcResponse } from "./generated/proto/dynrpc";
67
+ import { requestFromArgs } from "./utils/assumptions";
68
+ import { RandImpl } from "./utils/rand";
69
+ import { newJournalEntryPromiseId } from "./promise_combinator_tracker";
70
+ import { WrappedPromise } from "./utils/promises";
71
+
72
+ export enum CallContexType {
73
+ None,
74
+ SideEffect,
75
+ OneWayCall,
76
+ }
77
+
78
+ export interface CallContext {
79
+ type: CallContexType;
80
+ delay?: number;
81
+ }
82
+
83
+ export type InternalCombineablePromise<T> = CombineablePromise<T> &
84
+ WrappedPromise<T> & {
85
+ journalIndex: number;
86
+ };
87
+
88
+ export class ContextImpl implements KeyedContext, RestateGrpcChannel {
89
+ // here, we capture the context information for actions on the Restate context that
90
+ // are executed within other actions, such as
91
+ // ctx.oneWayCall( () => client.foo(bar) );
92
+ // we also use this information to ensure we check that only allowed operations are
93
+ // used. Within side-effects, no operations are allowed on the RestateContext.
94
+ // For example, this is illegal: 'ctx.sideEffect(() => {await ctx.get("my-state")})'
95
+ static callContext = new AsyncLocalStorage<CallContext>();
96
+
97
+ // This is used to guard users against calling ctx.sideEffect without awaiting it.
98
+ // See https://github.com/restatedev/sdk-typescript/issues/197 for more details.
99
+ private executingSideEffect = false;
100
+
101
+ constructor(
102
+ public readonly id: Buffer,
103
+ public readonly serviceName: string,
104
+ public readonly console: Console,
105
+ public readonly keyedContext: boolean,
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ private readonly stateMachine: StateMachine<any, any>,
108
+ public readonly rand: Rand = new RandImpl(id)
109
+ ) {}
110
+
111
+ // DON'T make this function async!!! see sideEffect comment for details.
112
+ public get<T>(name: string): Promise<T | null> {
113
+ // Check if this is a valid action
114
+ this.checkState("get state");
115
+ this.checkStateOperation("get state");
116
+
117
+ // Create the message and let the state machine process it
118
+ const msg = GetStateEntryMessage.create({ key: Buffer.from(name) });
119
+ const completed = this.stateMachine.localStateStore.tryCompleteGet(
120
+ name,
121
+ msg
122
+ );
123
+
124
+ const getState = async (): Promise<T | null> => {
125
+ const result = await this.stateMachine.handleUserCodeMessage(
126
+ GET_STATE_ENTRY_MESSAGE_TYPE,
127
+ msg,
128
+ completed
129
+ );
130
+
131
+ // If the GetState message did not have a value or empty,
132
+ // then we went to the runtime to get the value.
133
+ // When we get the response, we set it in the localStateStore,
134
+ // to answer subsequent requests
135
+ if (!completed) {
136
+ this.stateMachine.localStateStore.add(name, result as Buffer | Empty);
137
+ }
138
+
139
+ if (!(result instanceof Buffer)) {
140
+ return null;
141
+ }
142
+
143
+ return jsonDeserialize(result.toString());
144
+ };
145
+ return getState();
146
+ }
147
+
148
+ // DON'T make this function async!!! see sideEffect comment for details.
149
+ public stateKeys(): Promise<Array<string>> {
150
+ // Check if this is a valid action
151
+ this.checkState("state keys");
152
+
153
+ // Create the message and let the state machine process it
154
+ const msg = GetStateKeysEntryMessage.create({});
155
+ const completed =
156
+ this.stateMachine.localStateStore.tryCompletedGetStateKeys(msg);
157
+
158
+ const getStateKeys = async (): Promise<Array<string>> => {
159
+ const result = await this.stateMachine.handleUserCodeMessage(
160
+ GET_STATE_KEYS_ENTRY_MESSAGE_TYPE,
161
+ msg,
162
+ completed
163
+ );
164
+
165
+ return (result as GetStateKeysEntryMessage_StateKeys).keys.map((b) =>
166
+ b.toString()
167
+ );
168
+ };
169
+ return getStateKeys();
170
+ }
171
+
172
+ public set<T>(name: string, value: T): void {
173
+ this.checkState("set state");
174
+ this.checkStateOperation("set state");
175
+ const msg = this.stateMachine.localStateStore.set(name, value);
176
+ this.stateMachine.handleUserCodeMessage(SET_STATE_ENTRY_MESSAGE_TYPE, msg);
177
+ }
178
+
179
+ public clear(name: string): void {
180
+ this.checkState("clear state");
181
+ this.checkStateOperation("clear state");
182
+
183
+ const msg = this.stateMachine.localStateStore.clear(name);
184
+ this.stateMachine.handleUserCodeMessage(
185
+ CLEAR_STATE_ENTRY_MESSAGE_TYPE,
186
+ msg
187
+ );
188
+ }
189
+
190
+ public clearAll(): void {
191
+ this.checkState("clear all state");
192
+
193
+ const msg = this.stateMachine.localStateStore.clearAll();
194
+ this.stateMachine.handleUserCodeMessage(
195
+ CLEAR_ALL_STATE_ENTRY_MESSAGE_TYPE,
196
+ msg
197
+ );
198
+ }
199
+
200
+ // --- Calls, background calls, etc
201
+
202
+ public request(
203
+ service: string,
204
+ method: string,
205
+ data: Uint8Array
206
+ ): Promise<Uint8Array> {
207
+ if (this.isInOneWayCall()) {
208
+ return this.invokeOneWay(
209
+ service,
210
+ method,
211
+ data,
212
+ this.getOneWayCallDelay()
213
+ );
214
+ } else {
215
+ return this.invoke(service, method, data);
216
+ }
217
+ }
218
+
219
+ // DON'T make this function async!!! see sideEffect comment for details.
220
+ private invoke(
221
+ service: string,
222
+ method: string,
223
+ data: Uint8Array
224
+ ): InternalCombineablePromise<Uint8Array> {
225
+ this.checkState("invoke");
226
+
227
+ const msg = InvokeEntryMessage.create({
228
+ serviceName: service,
229
+ methodName: method,
230
+ parameter: Buffer.from(data),
231
+ });
232
+ return this.markCombineablePromise(
233
+ this.stateMachine
234
+ .handleUserCodeMessage(INVOKE_ENTRY_MESSAGE_TYPE, msg)
235
+ .transform((v) => v as Uint8Array)
236
+ );
237
+ }
238
+
239
+ private async invokeOneWay(
240
+ service: string,
241
+ method: string,
242
+ data: Uint8Array,
243
+ delay?: number
244
+ ): Promise<Uint8Array> {
245
+ const actualDelay = delay || 0;
246
+ const invokeTime = actualDelay > 0 ? Date.now() + actualDelay : undefined;
247
+ const msg = BackgroundInvokeEntryMessage.create({
248
+ serviceName: service,
249
+ methodName: method,
250
+ parameter: Buffer.from(data),
251
+ invokeTime: invokeTime,
252
+ });
253
+
254
+ await this.stateMachine.handleUserCodeMessage(
255
+ BACKGROUND_INVOKE_ENTRY_MESSAGE_TYPE,
256
+ msg
257
+ );
258
+ return new Uint8Array();
259
+ }
260
+
261
+ // DON'T make this function async!!! see sideEffect comment for details.
262
+ public oneWayCall(
263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
+ call: () => Promise<any>
265
+ ): Promise<void> {
266
+ this.checkState("oneWayCall");
267
+
268
+ return ContextImpl.callContext.run(
269
+ { type: CallContexType.OneWayCall },
270
+ call
271
+ );
272
+ }
273
+
274
+ // DON'T make this function async!!! see sideEffect comment for details.
275
+ public delayedCall(
276
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
277
+ call: () => Promise<any>,
278
+ delayMillis?: number
279
+ ): Promise<void> {
280
+ this.checkState("delayedCall");
281
+
282
+ // Delayed call is a one way call with a delay
283
+ return ContextImpl.callContext.run(
284
+ { type: CallContexType.OneWayCall, delay: delayMillis },
285
+ call
286
+ );
287
+ }
288
+
289
+ rpc<M>({ path }: ServiceApi<M>): Client<M> {
290
+ const clientProxy = new Proxy(
291
+ {},
292
+ {
293
+ get: (_target, prop) => {
294
+ const route = prop as string;
295
+ return async (...args: unknown[]) => {
296
+ const request = requestFromArgs(args);
297
+ const requestBytes = RpcRequest.encode(request).finish();
298
+ return this.invoke(path, route, requestBytes).transform(
299
+ (responseBytes) => RpcResponse.decode(responseBytes).response
300
+ );
301
+ };
302
+ },
303
+ }
304
+ );
305
+
306
+ return clientProxy as Client<M>;
307
+ }
308
+
309
+ public send<M>(options: ServiceApi): SendClient<M> {
310
+ return this.sendDelayed(options, 0);
311
+ }
312
+
313
+ public sendDelayed<M>(
314
+ { path }: ServiceApi,
315
+ delayMillis: number
316
+ ): SendClient<M> {
317
+ const clientProxy = new Proxy(
318
+ {},
319
+ {
320
+ get: (_target, prop) => {
321
+ const route = prop as string;
322
+ return (...args: unknown[]) => {
323
+ const request = requestFromArgs(args);
324
+ const requestBytes = RpcRequest.encode(request).finish();
325
+ this.invokeOneWay(path, route, requestBytes, delayMillis);
326
+ };
327
+ },
328
+ }
329
+ );
330
+
331
+ return clientProxy as SendClient<M>;
332
+ }
333
+
334
+ // --- Methods exposed by respective interfaces to interact with other APIs
335
+
336
+ grpcChannel(): RestateGrpcChannel {
337
+ return this;
338
+ }
339
+
340
+ // DON'T make this function async!!!
341
+ // The reason is that we want the erros thrown by the initial checks to be propagated in the caller context,
342
+ // and not in the promise context. To understand the semantic difference, make this function async and run the
343
+ // UnawaitedSideEffectShouldFailSubsequentContextCall test.
344
+ public sideEffect<T>(
345
+ fn: () => Promise<T>,
346
+ retryPolicy: RetrySettings = DEFAULT_INFINITE_EXPONENTIAL_BACKOFF
347
+ ): Promise<T> {
348
+ if (this.isInSideEffect()) {
349
+ throw new TerminalError(
350
+ "You cannot do sideEffect calls from within a side effect.",
351
+ { errorCode: ErrorCodes.INTERNAL }
352
+ );
353
+ } else if (this.isInOneWayCall()) {
354
+ throw new TerminalError(
355
+ "Cannot do a side effect from within ctx.oneWayCall(...). " +
356
+ "Context method ctx.oneWayCall() can only be used to invoke other services unidirectionally. " +
357
+ "e.g. ctx.oneWayCall(() => client.greet(my_request))",
358
+ { errorCode: ErrorCodes.INTERNAL }
359
+ );
360
+ }
361
+ this.checkNotExecutingSideEffect();
362
+ this.executingSideEffect = true;
363
+
364
+ const executeAndLogSideEffect = async () => {
365
+ // in replay mode, we directly return the value from the log
366
+ if (this.stateMachine.nextEntryWillBeReplayed()) {
367
+ const emptyMsg = SideEffectEntryMessage.create({});
368
+ return this.stateMachine.handleUserCodeMessage<T>(
369
+ SIDE_EFFECT_ENTRY_MESSAGE_TYPE,
370
+ emptyMsg
371
+ ) as Promise<T>;
372
+ }
373
+
374
+ let sideEffectResult: T;
375
+ try {
376
+ sideEffectResult = await ContextImpl.callContext.run(
377
+ { type: CallContexType.SideEffect },
378
+ fn
379
+ );
380
+ } catch (e) {
381
+ // we commit any error from the side effet to thr journal, and re-throw it into
382
+ // the function. that way, any catching by the user and reacting to it will be
383
+ // deterministic on replay
384
+ const error = ensureError(e);
385
+ const failure = errorToFailureWithTerminal(error);
386
+ const sideEffectMsg = SideEffectEntryMessage.create({
387
+ failure: failure,
388
+ });
389
+
390
+ // this may throw an error from the SDK/runtime/connection side, in case the
391
+ // failure message cannot be committed to the journal. That error would then
392
+ // be returned from this function (replace the original error)
393
+ // that is acceptable, because in such a situation (failure to append to journal),
394
+ // the state machine closes anyways and no further operations will succeed and the
395
+ // the execution aborts
396
+ await this.stateMachine.handleUserCodeMessage<T>(
397
+ SIDE_EFFECT_ENTRY_MESSAGE_TYPE,
398
+ sideEffectMsg,
399
+ false,
400
+ undefined,
401
+ true
402
+ );
403
+
404
+ throw e;
405
+ }
406
+
407
+ // we have this code outside the above try/catch block, to ensure that any error arising
408
+ // from here is not incorrectly attributed to the side-effect
409
+ const sideEffectMsg =
410
+ sideEffectResult !== undefined
411
+ ? SideEffectEntryMessage.create({
412
+ value: Buffer.from(jsonSerialize(sideEffectResult)),
413
+ })
414
+ : SideEffectEntryMessage.create();
415
+
416
+ // if an error arises from committing the side effect result, then this error will
417
+ // be thrown here (reject the returned promise) and the function will see that error,
418
+ // even if the side-effect function completed correctly
419
+ // that is acceptable, because in such a situation (failure to append to journal),
420
+ // the state machine closes anyways and reports an execution failure, meaning no further
421
+ // operations will succeed and the the execution will be retried.
422
+ // If the side-effect result did in fact not make it to the journal, then the side-effect
423
+ // re-executes, and if it made it to the journal after all (error happend inly during
424
+ // ack-back), then retries will use the journaled result.
425
+ // So all good in any case, due to the beauty of "the runtime log is the ground thruth" approach.
426
+ await this.stateMachine.handleUserCodeMessage<T>(
427
+ SIDE_EFFECT_ENTRY_MESSAGE_TYPE,
428
+ sideEffectMsg,
429
+ false,
430
+ undefined,
431
+ true
432
+ );
433
+
434
+ return sideEffectResult;
435
+ };
436
+
437
+ const sleep = (millis: number) => this.sleepInternal(millis);
438
+ return executeWithRetries(
439
+ this.console,
440
+ retryPolicy,
441
+ executeAndLogSideEffect,
442
+ sleep
443
+ ).finally(() => {
444
+ this.executingSideEffect = false;
445
+ });
446
+ }
447
+
448
+ public sleep(millis: number): CombineablePromise<void> {
449
+ this.checkState("sleep");
450
+ return this.markCombineablePromise(this.sleepInternal(millis));
451
+ }
452
+
453
+ private sleepInternal(millis: number): WrappedPromise<void> {
454
+ return this.stateMachine.handleUserCodeMessage<void>(
455
+ SLEEP_ENTRY_MESSAGE_TYPE,
456
+ SleepEntryMessage.create({ wakeUpTime: Date.now() + millis })
457
+ );
458
+ }
459
+
460
+ // -- Awakeables
461
+
462
+ public awakeable<T>(): { id: string; promise: CombineablePromise<T> } {
463
+ this.checkState("awakeable");
464
+
465
+ const msg = AwakeableEntryMessage.create();
466
+ const promise = this.stateMachine
467
+ .handleUserCodeMessage<Buffer>(AWAKEABLE_ENTRY_MESSAGE_TYPE, msg)
468
+ .transform((result: Buffer | void) => {
469
+ if (!(result instanceof Buffer)) {
470
+ // This should either be a filled buffer or an empty buffer but never anything else.
471
+ throw RetryableError.internal(
472
+ "Awakeable was not resolved with a buffer payload"
473
+ );
474
+ }
475
+
476
+ return JSON.parse(result.toString()) as T;
477
+ });
478
+
479
+ // This needs to be done after handling the message in the state machine
480
+ // otherwise the index is not yet incremented.
481
+ const encodedEntryIndex = Buffer.alloc(4 /* Size of u32 */);
482
+ encodedEntryIndex.writeUInt32BE(
483
+ this.stateMachine.getUserCodeJournalIndex()
484
+ );
485
+
486
+ return {
487
+ id:
488
+ AWAKEABLE_IDENTIFIER_PREFIX +
489
+ Buffer.concat([this.id, encodedEntryIndex]).toString("base64url"),
490
+ promise: this.markCombineablePromise(promise),
491
+ };
492
+ }
493
+
494
+ public resolveAwakeable<T>(id: string, payload?: T): void {
495
+ // We coerce undefined to null as null can be stringified by JSON.stringify
496
+ const payloadToWrite = payload === undefined ? null : payload;
497
+
498
+ this.checkState("resolveAwakeable");
499
+ this.completeAwakeable(id, {
500
+ value: Buffer.from(JSON.stringify(payloadToWrite)),
501
+ });
502
+ }
503
+
504
+ public rejectAwakeable(id: string, reason: string): void {
505
+ this.checkState("rejectAwakeable");
506
+ this.completeAwakeable(id, {
507
+ failure: { code: ErrorCodes.UNKNOWN, message: reason },
508
+ });
509
+ }
510
+
511
+ private completeAwakeable(
512
+ id: string,
513
+ base: DeepPartial<CompleteAwakeableEntryMessage>
514
+ ): void {
515
+ base.id = id;
516
+ this.stateMachine.handleUserCodeMessage(
517
+ COMPLETE_AWAKEABLE_ENTRY_MESSAGE_TYPE,
518
+ CompleteAwakeableEntryMessage.create(base)
519
+ );
520
+ }
521
+
522
+ // Used by static methods of CombineablePromise
523
+ public createCombinator<T extends readonly CombineablePromise<unknown>[]>(
524
+ combinatorConstructor: (
525
+ promises: PromiseLike<unknown>[]
526
+ ) => Promise<unknown>,
527
+ promises: T
528
+ ): WrappedPromise<unknown> {
529
+ const outPromises = [];
530
+
531
+ for (const promise of promises) {
532
+ if (promise.__restate_context !== this) {
533
+ throw RetryableError.internal(
534
+ "You're mixing up CombineablePromises from different RestateContext. This is not supported."
535
+ );
536
+ }
537
+ const index = (promise as InternalCombineablePromise<unknown>)
538
+ .journalIndex;
539
+ outPromises.push({
540
+ id: newJournalEntryPromiseId(index),
541
+ promise: promise,
542
+ });
543
+ }
544
+
545
+ return this.stateMachine.createCombinator(
546
+ combinatorConstructor,
547
+ outPromises
548
+ );
549
+ }
550
+
551
+ // -- Various private methods
552
+
553
+ private isInSideEffect(): boolean {
554
+ const context = ContextImpl.callContext.getStore();
555
+ return context?.type === CallContexType.SideEffect;
556
+ }
557
+
558
+ private isInOneWayCall(): boolean {
559
+ const context = ContextImpl.callContext.getStore();
560
+ return context?.type === CallContexType.OneWayCall;
561
+ }
562
+
563
+ private getOneWayCallDelay(): number | undefined {
564
+ const context = ContextImpl.callContext.getStore();
565
+ return context?.delay;
566
+ }
567
+
568
+ private checkNotExecutingSideEffect() {
569
+ if (this.executingSideEffect) {
570
+ throw new TerminalError(
571
+ `Invoked a RestateContext method while a side effect is still executing.
572
+ Make sure you await the ctx.sideEffect call before using any other RestateContext method.`,
573
+ { errorCode: ErrorCodes.INTERNAL }
574
+ );
575
+ }
576
+ }
577
+
578
+ private checkState(callType: string): void {
579
+ const context = ContextImpl.callContext.getStore();
580
+ if (!context) {
581
+ this.checkNotExecutingSideEffect();
582
+ return;
583
+ }
584
+
585
+ if (context.type === CallContexType.SideEffect) {
586
+ throw new TerminalError(
587
+ `You cannot do ${callType} calls from within a side effect.`,
588
+ { errorCode: ErrorCodes.INTERNAL }
589
+ );
590
+ }
591
+
592
+ if (context.type === CallContexType.OneWayCall) {
593
+ throw new TerminalError(
594
+ `Cannot do a ${callType} from within ctx.oneWayCall(...).
595
+ Context method oneWayCall() can only be used to invoke other services in the background.
596
+ e.g. ctx.oneWayCall(() => client.greet(my_request))`,
597
+ { errorCode: ErrorCodes.INTERNAL }
598
+ );
599
+ }
600
+ }
601
+
602
+ private checkStateOperation(callType: string): void {
603
+ if (!this.keyedContext) {
604
+ throw new TerminalError(
605
+ `You can do ${callType} calls only from keyed services/routers.`,
606
+ { errorCode: ErrorCodes.INTERNAL }
607
+ );
608
+ }
609
+ }
610
+
611
+ private markCombineablePromise<T>(
612
+ p: WrappedPromise<T>
613
+ ): InternalCombineablePromise<T> {
614
+ const journalIndex = this.stateMachine.getUserCodeJournalIndex();
615
+ const orTimeout = (millis: number): Promise<T> => {
616
+ const sleepPromise: Promise<T> = this.sleepInternal(millis).transform(
617
+ () => {
618
+ throw new TimeoutError();
619
+ }
620
+ );
621
+ const sleepPromiseIndex = this.stateMachine.getUserCodeJournalIndex();
622
+
623
+ return this.stateMachine.createCombinator(Promise.race.bind(Promise), [
624
+ {
625
+ id: newJournalEntryPromiseId(journalIndex),
626
+ promise: p,
627
+ },
628
+ {
629
+ id: newJournalEntryPromiseId(sleepPromiseIndex),
630
+ promise: sleepPromise,
631
+ },
632
+ ]) as Promise<T>;
633
+ };
634
+
635
+ return Object.defineProperties(p, {
636
+ __restate_context: {
637
+ value: this,
638
+ },
639
+ journalIndex: {
640
+ value: journalIndex,
641
+ },
642
+ orTimeout: {
643
+ value: orTimeout.bind(this),
644
+ },
645
+ }) as InternalCombineablePromise<T>;
646
+ }
647
+ }
648
+
649
+ async function executeWithRetries<T>(
650
+ console: Console,
651
+ retrySettings: RetrySettings,
652
+ executeAndLogSideEffect: () => Promise<T>,
653
+ sleep: (millis: number) => Promise<void>
654
+ ): Promise<T> {
655
+ const {
656
+ initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
657
+ maxDelayMs = Number.MAX_SAFE_INTEGER,
658
+ maxRetries = Number.MAX_SAFE_INTEGER,
659
+ policy = EXPONENTIAL_BACKOFF,
660
+ name = "side-effect",
661
+ } = retrySettings;
662
+
663
+ let currentDelayMs = initialDelayMs;
664
+ let retriesLeft = maxRetries;
665
+
666
+ // eslint-disable-next-line no-constant-condition
667
+ while (true) {
668
+ try {
669
+ return await executeAndLogSideEffect();
670
+ } catch (e) {
671
+ if (e instanceof TerminalError) {
672
+ throw e;
673
+ }
674
+
675
+ // journal mismatch errors are special:
676
+ // - they are not terminal errors, because we want to allow pushing new code so
677
+ // that retries succeed later
678
+ // - they are not retried within the service, because they will never succeed within this service,
679
+ // but can only succeed within a new invocation going to service with fixed code
680
+ // we hence break the retries here similar to terminal errors
681
+ if (
682
+ e instanceof RestateError &&
683
+ e.code == RestateErrorCodes.JOURNAL_MISMATCH
684
+ ) {
685
+ throw e;
686
+ }
687
+
688
+ const error = ensureError(e);
689
+
690
+ console.debug(
691
+ "Error while executing side effect '%s': %s - %s",
692
+ name,
693
+ error.name,
694
+ error.message
695
+ );
696
+ if (error.stack) {
697
+ console.debug(error.stack);
698
+ }
699
+
700
+ if (retriesLeft > 0) {
701
+ console.debug("Retrying in %d ms", currentDelayMs);
702
+ } else {
703
+ console.debug("No retries left.");
704
+ throw new TerminalError(
705
+ `Retries exhausted for ${name}. Last error: ${error.name}: ${error.message}`,
706
+ {
707
+ errorCode: ErrorCodes.INTERNAL,
708
+ }
709
+ );
710
+ }
711
+ }
712
+
713
+ await sleep(currentDelayMs);
714
+
715
+ retriesLeft -= 1;
716
+ currentDelayMs = Math.min(
717
+ policy.computeNextDelay(currentDelayMs),
718
+ maxDelayMs
719
+ );
720
+ }
721
+ }