@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
package/src/journal.ts ADDED
@@ -0,0 +1,537 @@
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 * as p from "./types/protocol";
13
+ import {
14
+ Failure,
15
+ GetStateKeysEntryMessage_StateKeys,
16
+ } from "./generated/proto/protocol";
17
+ import {
18
+ AWAKEABLE_ENTRY_MESSAGE_TYPE,
19
+ AwakeableEntryMessage,
20
+ BACKGROUND_INVOKE_ENTRY_MESSAGE_TYPE,
21
+ CLEAR_ALL_STATE_ENTRY_MESSAGE_TYPE,
22
+ CLEAR_STATE_ENTRY_MESSAGE_TYPE,
23
+ COMBINATOR_ENTRY_MESSAGE,
24
+ COMPLETE_AWAKEABLE_ENTRY_MESSAGE_TYPE,
25
+ CompletionMessage,
26
+ EntryAckMessage,
27
+ GET_STATE_ENTRY_MESSAGE_TYPE,
28
+ GET_STATE_KEYS_ENTRY_MESSAGE_TYPE,
29
+ GetStateEntryMessage,
30
+ GetStateKeysEntryMessage,
31
+ INVOKE_ENTRY_MESSAGE_TYPE,
32
+ InvokeEntryMessage,
33
+ OUTPUT_STREAM_ENTRY_MESSAGE_TYPE,
34
+ OutputStreamEntryMessage,
35
+ POLL_INPUT_STREAM_ENTRY_MESSAGE_TYPE,
36
+ PollInputStreamEntryMessage,
37
+ SET_STATE_ENTRY_MESSAGE_TYPE,
38
+ SIDE_EFFECT_ENTRY_MESSAGE_TYPE,
39
+ SLEEP_ENTRY_MESSAGE_TYPE,
40
+ SleepEntryMessage,
41
+ SUSPENSION_MESSAGE_TYPE,
42
+ SuspensionMessage,
43
+ } from "./types/protocol";
44
+ import { equalityCheckers, jsonDeserialize } from "./utils/utils";
45
+ import { Message } from "./types/types";
46
+ import { SideEffectEntryMessage } from "./generated/proto/javascript";
47
+ import { Invocation } from "./invocation";
48
+ import { failureToError, RetryableError } from "./types/errors";
49
+ import { CompletablePromise } from "./utils/promises";
50
+
51
+ const RESOLVED = Promise.resolve(undefined);
52
+
53
+ export class Journal<I, O> {
54
+ private state = NewExecutionState.REPLAYING;
55
+
56
+ private userCodeJournalIndex = 0;
57
+
58
+ // Journal entries waiting for arrival of runtime completion
59
+ // 0 = root promise of the method invocation
60
+ private pendingJournalEntries = new Map<number, JournalEntry>();
61
+
62
+ constructor(readonly invocation: Invocation<I, O>) {
63
+ const inputMessage = invocation.replayEntries.get(0);
64
+ if (
65
+ !inputMessage ||
66
+ inputMessage.messageType !== POLL_INPUT_STREAM_ENTRY_MESSAGE_TYPE
67
+ ) {
68
+ throw RetryableError.protocolViolation(
69
+ "First message of replay entries needs to be PollInputStreamMessage"
70
+ );
71
+ }
72
+ this.handleInputMessage(
73
+ inputMessage.message as PollInputStreamEntryMessage
74
+ );
75
+ }
76
+
77
+ handleInputMessage(m: p.PollInputStreamEntryMessage) {
78
+ if (this.invocation.nbEntriesToReplay === 1) {
79
+ this.transitionState(NewExecutionState.PROCESSING);
80
+ }
81
+
82
+ const rootEntry = new JournalEntry(
83
+ p.POLL_INPUT_STREAM_ENTRY_MESSAGE_TYPE,
84
+ m
85
+ );
86
+
87
+ this.pendingJournalEntries.set(0, rootEntry);
88
+ }
89
+
90
+ public handleUserSideMessage(
91
+ messageType: bigint,
92
+ message: p.ProtocolMessage | Uint8Array
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ ): Promise<any | undefined> {
95
+ this.incrementUserCodeIndex();
96
+
97
+ switch (this.state) {
98
+ case NewExecutionState.REPLAYING: {
99
+ const replayEntry = this.invocation.replayEntries.get(
100
+ this.userCodeJournalIndex
101
+ );
102
+ if (replayEntry === undefined) {
103
+ throw RetryableError.internal(
104
+ `Illegal state: no replay message was received for the entry at journal index ${this.userCodeJournalIndex}`
105
+ );
106
+ }
107
+
108
+ const journalEntry = new JournalEntry(messageType, message);
109
+ this.handleReplay(this.userCodeJournalIndex, replayEntry, journalEntry);
110
+ return journalEntry.completablePromise.promise;
111
+ }
112
+ case NewExecutionState.PROCESSING: {
113
+ switch (messageType) {
114
+ case p.SUSPENSION_MESSAGE_TYPE:
115
+ case p.OUTPUT_STREAM_ENTRY_MESSAGE_TYPE: {
116
+ this.handleClosingMessage(
117
+ messageType,
118
+ message as p.SuspensionMessage | p.OutputStreamEntryMessage
119
+ );
120
+ return RESOLVED;
121
+ }
122
+ case p.SET_STATE_ENTRY_MESSAGE_TYPE:
123
+ case p.CLEAR_STATE_ENTRY_MESSAGE_TYPE:
124
+ case p.CLEAR_ALL_STATE_ENTRY_MESSAGE_TYPE:
125
+ case p.COMPLETE_AWAKEABLE_ENTRY_MESSAGE_TYPE:
126
+ case p.BACKGROUND_INVOKE_ENTRY_MESSAGE_TYPE: {
127
+ // Do not need completion
128
+ return RESOLVED;
129
+ }
130
+ case p.GET_STATE_ENTRY_MESSAGE_TYPE: {
131
+ const getStateMsg = message as GetStateEntryMessage;
132
+ if (
133
+ getStateMsg.value !== undefined ||
134
+ getStateMsg.empty !== undefined
135
+ ) {
136
+ // State was eagerly filled by the local state store
137
+ return Promise.resolve(getStateMsg.value || getStateMsg.empty);
138
+ } else {
139
+ // Need to retrieve state by going to the runtime.
140
+ return this.appendJournalEntry(messageType, message);
141
+ }
142
+ }
143
+ case p.GET_STATE_KEYS_ENTRY_MESSAGE_TYPE: {
144
+ const getStateMsg = message as GetStateKeysEntryMessage;
145
+ if (getStateMsg.value !== undefined) {
146
+ // State was eagerly filled by the local state store
147
+ return Promise.resolve(getStateMsg.value);
148
+ } else {
149
+ // Need to retrieve state by going to the runtime.
150
+ return this.appendJournalEntry(messageType, message);
151
+ }
152
+ }
153
+ default: {
154
+ return this.appendJournalEntry(messageType, message);
155
+ }
156
+ }
157
+ }
158
+ case NewExecutionState.CLOSED: {
159
+ // We cannot do anything anymore because an output was already sent back
160
+ // This should actually never happen because the state is only transitioned to closed if the root promise is resolved/rejected
161
+ // So no more user messages can come in...
162
+ // - Print warning log and continue...
163
+ //TODO received user-side message but state machine is closed
164
+ return RESOLVED;
165
+ }
166
+ default: {
167
+ throw RetryableError.protocolViolation(
168
+ "Did not receive input message before other messages."
169
+ );
170
+ }
171
+ }
172
+ }
173
+
174
+ public isUnResolved(index: number): boolean {
175
+ const journalEntry = this.pendingJournalEntries.get(index);
176
+ return journalEntry !== undefined;
177
+ }
178
+
179
+ public handleRuntimeCompletionMessage(m: CompletionMessage) {
180
+ // Get message at that entryIndex in pendingJournalEntries
181
+ const journalEntry = this.pendingJournalEntries.get(m.entryIndex);
182
+
183
+ if (journalEntry === undefined) {
184
+ //TODO received completion message but there is no pending promise for that index
185
+ return;
186
+ }
187
+
188
+ if (m.value !== undefined) {
189
+ if (journalEntry.messageType === GET_STATE_KEYS_ENTRY_MESSAGE_TYPE) {
190
+ // In case of get state keys we expect the parsed message
191
+ journalEntry.completablePromise.resolve(
192
+ GetStateKeysEntryMessage_StateKeys.decode(m.value)
193
+ );
194
+ this.pendingJournalEntries.delete(m.entryIndex);
195
+ } else {
196
+ journalEntry.completablePromise.resolve(m.value);
197
+ this.pendingJournalEntries.delete(m.entryIndex);
198
+ }
199
+ } else if (m.failure !== undefined) {
200
+ // we do all completions with Terminal Errors, because failures triggered by those exceptions
201
+ // when the bubble up would otherwise lead to re-tries, deterministic replay, re-throwing, and
202
+ // thus an infinite loop that keeps replay-ing but never makes progress
203
+ // these failures here consequently need to cause terminal failures, unless caught and handled
204
+ // by the handler code
205
+ journalEntry.completablePromise.reject(failureToError(m.failure, true));
206
+ this.pendingJournalEntries.delete(m.entryIndex);
207
+ } else if (m.empty !== undefined) {
208
+ journalEntry.completablePromise.resolve(m.empty);
209
+ this.pendingJournalEntries.delete(m.entryIndex);
210
+ } else {
211
+ //TODO completion message without a value/failure/empty
212
+ }
213
+ }
214
+
215
+ public handleEntryAckMessage(m: EntryAckMessage) {
216
+ // Get message at that entryIndex in pendingJournalEntries
217
+ const journalEntry = this.pendingJournalEntries.get(m.entryIndex);
218
+
219
+ if (journalEntry === undefined) {
220
+ return;
221
+ }
222
+
223
+ // Just needs an ack
224
+ journalEntry.completablePromise.resolve(undefined);
225
+ this.pendingJournalEntries.delete(m.entryIndex);
226
+ }
227
+
228
+ private handleReplay(
229
+ journalIndex: number,
230
+ replayMessage: Message,
231
+ journalEntry: JournalEntry
232
+ ) {
233
+ // Do the journal mismatch check
234
+ const match = this.checkJournalMatch(
235
+ replayMessage.messageType,
236
+ replayMessage.message,
237
+ journalEntry.messageType,
238
+ journalEntry.message
239
+ );
240
+
241
+ // Journal mismatch check failedf
242
+ if (!match) {
243
+ throw RetryableError.journalMismatch(
244
+ journalIndex,
245
+ replayMessage,
246
+ journalEntry
247
+ );
248
+ }
249
+
250
+ // If journal mismatch check passed
251
+ /*
252
+ - Else if the runtime replay message contains a completion
253
+ - If the completion is a value
254
+ - Return the resolved user code promise with the value
255
+ - Else if the completion is a failure
256
+ - Return the rejected user code promise with the failure as Error
257
+ - Else if the completion is an Empty message
258
+ - Return the resolved user code promise with the Empty message
259
+ - Remove the journal entry
260
+ - Else the replayed message was uncompleted
261
+ - Create the user code promise
262
+ - Add message to the pendingJournalEntries
263
+ - Return the user code promise
264
+ */
265
+ switch (journalEntry.messageType) {
266
+ case SUSPENSION_MESSAGE_TYPE:
267
+ case OUTPUT_STREAM_ENTRY_MESSAGE_TYPE: {
268
+ this.handleClosingMessage(
269
+ journalEntry.messageType,
270
+ journalEntry.message as SuspensionMessage | OutputStreamEntryMessage
271
+ );
272
+ break;
273
+ }
274
+ case GET_STATE_ENTRY_MESSAGE_TYPE: {
275
+ const getStateMsg = replayMessage.message as GetStateEntryMessage;
276
+ this.resolveResult(
277
+ journalIndex,
278
+ journalEntry,
279
+ getStateMsg.value || getStateMsg.empty,
280
+ getStateMsg.failure
281
+ );
282
+ break;
283
+ }
284
+ case GET_STATE_KEYS_ENTRY_MESSAGE_TYPE: {
285
+ const getStateMsg = replayMessage.message as GetStateKeysEntryMessage;
286
+ this.resolveResult(
287
+ journalIndex,
288
+ journalEntry,
289
+ getStateMsg.value,
290
+ getStateMsg.failure
291
+ );
292
+ break;
293
+ }
294
+ case INVOKE_ENTRY_MESSAGE_TYPE: {
295
+ const invokeMsg = replayMessage.message as InvokeEntryMessage;
296
+ this.resolveResult(
297
+ journalIndex,
298
+ journalEntry,
299
+ invokeMsg.value,
300
+ invokeMsg.failure
301
+ );
302
+ break;
303
+ }
304
+ case SLEEP_ENTRY_MESSAGE_TYPE: {
305
+ const sleepMsg = replayMessage.message as SleepEntryMessage;
306
+ this.resolveResult(
307
+ journalIndex,
308
+ journalEntry,
309
+ sleepMsg.empty,
310
+ sleepMsg.failure
311
+ );
312
+ break;
313
+ }
314
+ case AWAKEABLE_ENTRY_MESSAGE_TYPE: {
315
+ const awakeableMsg = replayMessage.message as AwakeableEntryMessage;
316
+ this.resolveResult(
317
+ journalIndex,
318
+ journalEntry,
319
+ awakeableMsg.value,
320
+ awakeableMsg.failure
321
+ );
322
+ break;
323
+ }
324
+ case SIDE_EFFECT_ENTRY_MESSAGE_TYPE: {
325
+ const sideEffectMsg = replayMessage.message as SideEffectEntryMessage;
326
+ if (sideEffectMsg.value !== undefined) {
327
+ this.resolveResult(
328
+ journalIndex,
329
+ journalEntry,
330
+ jsonDeserialize(sideEffectMsg.value.toString())
331
+ );
332
+ } else if (sideEffectMsg.failure !== undefined) {
333
+ this.resolveResult(
334
+ journalIndex,
335
+ journalEntry,
336
+ undefined,
337
+ sideEffectMsg.failure.failure,
338
+ sideEffectMsg.failure.terminal
339
+ );
340
+ } else {
341
+ // A side effect can have a void return type
342
+ // If it was replayed, then it is acked, so we should resolve it.
343
+ journalEntry.completablePromise.resolve(undefined);
344
+ this.pendingJournalEntries.delete(journalIndex);
345
+ }
346
+ break;
347
+ }
348
+ case SET_STATE_ENTRY_MESSAGE_TYPE:
349
+ case CLEAR_STATE_ENTRY_MESSAGE_TYPE:
350
+ case CLEAR_ALL_STATE_ENTRY_MESSAGE_TYPE:
351
+ case COMPLETE_AWAKEABLE_ENTRY_MESSAGE_TYPE:
352
+ case BACKGROUND_INVOKE_ENTRY_MESSAGE_TYPE:
353
+ case COMBINATOR_ENTRY_MESSAGE: {
354
+ // Do not need a completion. So if the match has passed then the entry can be deleted.
355
+ journalEntry.completablePromise.resolve(undefined);
356
+ this.pendingJournalEntries.delete(journalIndex);
357
+ break;
358
+ }
359
+ default: {
360
+ // TODO received replay message of unknown type
361
+ }
362
+ }
363
+ }
364
+
365
+ resolveResult<T>(
366
+ journalIndex: number,
367
+ journalEntry: JournalEntry,
368
+ value: T | undefined,
369
+ failure?: Failure | undefined,
370
+ failureWouldBeTerminal?: boolean
371
+ ) {
372
+ if (value !== undefined) {
373
+ journalEntry.completablePromise.resolve(value);
374
+ this.pendingJournalEntries.delete(journalIndex);
375
+ } else if (failure !== undefined) {
376
+ const error = failureToError(failure, failureWouldBeTerminal ?? true);
377
+ journalEntry.completablePromise.reject(error);
378
+ this.pendingJournalEntries.delete(journalIndex);
379
+ } else {
380
+ this.pendingJournalEntries.set(journalIndex, journalEntry);
381
+ }
382
+ }
383
+
384
+ handleClosingMessage(
385
+ messageType: bigint,
386
+ message: OutputStreamEntryMessage | SuspensionMessage
387
+ ) {
388
+ this.transitionState(NewExecutionState.CLOSED);
389
+ const rootJournalEntry = this.pendingJournalEntries.get(0);
390
+
391
+ if (rootJournalEntry === undefined) {
392
+ // We have no other option than to throw an error here
393
+ // Because without the root promise we cannot resolve the method or continue
394
+ throw RetryableError.internal(
395
+ "Illegal state: No root journal entry found to resolve with output stream message"
396
+ );
397
+ }
398
+
399
+ this.pendingJournalEntries.delete(0);
400
+ rootJournalEntry.completablePromise.resolve(
401
+ new Message(messageType, message)
402
+ );
403
+ }
404
+
405
+ private checkJournalMatch(
406
+ runtimeMsgType: bigint,
407
+ runtimeMsg: p.ProtocolMessage | Uint8Array,
408
+ userCodeMsgType: bigint,
409
+ userCodeMsg: p.ProtocolMessage | Uint8Array
410
+ ): boolean {
411
+ if (runtimeMsgType === userCodeMsgType) {
412
+ const equalityFct = equalityCheckers.get(runtimeMsgType);
413
+ if (equalityFct === undefined) {
414
+ // TODO no equality function was defined for the message type
415
+ return true;
416
+ }
417
+ return equalityFct(runtimeMsg, userCodeMsg);
418
+ } else {
419
+ return false;
420
+ }
421
+ }
422
+
423
+ // To get the indices that need to be completed with suspension
424
+ public getCompletableIndices(): number[] {
425
+ // return all entries except for the root entry
426
+ return [...this.pendingJournalEntries.entries()]
427
+ .filter((el) => el[0] !== 0)
428
+ .map((el) => el[0]);
429
+ }
430
+
431
+ private transitionState(newExecState: NewExecutionState) {
432
+ // If the state is already closed then you cannot transition anymore
433
+ if (
434
+ this.state === NewExecutionState.CLOSED &&
435
+ newExecState !== NewExecutionState.CLOSED
436
+ ) {
437
+ // do nothing
438
+ return;
439
+ } else {
440
+ this.state = newExecState;
441
+ return;
442
+ }
443
+ }
444
+
445
+ incrementUserCodeIndex() {
446
+ this.userCodeJournalIndex++;
447
+ if (
448
+ this.userCodeJournalIndex === this.invocation.nbEntriesToReplay &&
449
+ this.state === NewExecutionState.REPLAYING
450
+ ) {
451
+ this.transitionState(NewExecutionState.PROCESSING);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Read the next replay entry
457
+ */
458
+ public readNextReplayEntry() {
459
+ this.incrementUserCodeIndex();
460
+ return this.invocation.replayEntries.get(this.userCodeJournalIndex);
461
+ }
462
+
463
+ /**
464
+ * Append journal entry. This won't increment the journal index.
465
+ */
466
+ public appendJournalEntry(
467
+ messageType: bigint,
468
+ message: p.ProtocolMessage | Uint8Array
469
+ ): Promise<unknown> {
470
+ const journalEntry = new JournalEntry(messageType, message);
471
+ this.pendingJournalEntries.set(this.userCodeJournalIndex, journalEntry);
472
+ return journalEntry.completablePromise.promise;
473
+ }
474
+
475
+ public isClosed(): boolean {
476
+ return this.state === NewExecutionState.CLOSED;
477
+ }
478
+
479
+ public isProcessing(): boolean {
480
+ return this.state === NewExecutionState.PROCESSING;
481
+ }
482
+
483
+ public isReplaying(): boolean {
484
+ return this.state === NewExecutionState.REPLAYING;
485
+ }
486
+
487
+ public getUserCodeJournalIndex(): number {
488
+ return this.userCodeJournalIndex;
489
+ }
490
+
491
+ public close() {
492
+ this.transitionState(NewExecutionState.CLOSED);
493
+ }
494
+
495
+ public outputMsgWasReplayed() {
496
+ // Check if the last message of the replay entries is an output message
497
+ const lastEntry = this.invocation.replayEntries.get(
498
+ this.invocation.nbEntriesToReplay - 1
499
+ );
500
+ return (
501
+ lastEntry && lastEntry.messageType === OUTPUT_STREAM_ENTRY_MESSAGE_TYPE
502
+ );
503
+ }
504
+
505
+ // We use this for side effects.
506
+ // The restate context needs to know if the user-defined fct needs to be executed or not.
507
+ // It needs to know this before it can craft the message and call this.stateMachine.handleUserSideMessage(...)
508
+ // so before the index got incremented and the state got transitioned.
509
+ // So we cannot use isReplaying().
510
+ // So we need to check in the journal if the next entry (= our side effect) will be replayed or not.
511
+ nextEntryWillBeReplayed() {
512
+ return this.userCodeJournalIndex + 1 < this.invocation.nbEntriesToReplay;
513
+ }
514
+ }
515
+
516
+ export class JournalEntry {
517
+ public completablePromise: CompletablePromise<unknown>;
518
+
519
+ constructor(
520
+ readonly messageType: bigint,
521
+ readonly message: p.ProtocolMessage | Uint8Array
522
+ ) {
523
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
524
+ this.completablePromise = new CompletablePromise<any>();
525
+ }
526
+ }
527
+
528
+ // "WAITING_FOR_START" before receiving start message
529
+ // "WAITING_FOR_REPLAY" when waiting for all replay entries to arrive from the runtime
530
+ // "REPLAYING" when receiving input stream message
531
+ // "PROCESSING" when both sides have finished replaying
532
+ // "CLOSED" when input stream connection channel gets closed
533
+ export enum NewExecutionState {
534
+ REPLAYING = "REPLAYING",
535
+ PROCESSING = "PROCESSING",
536
+ CLOSED = "CLOSED",
537
+ }
@@ -0,0 +1,94 @@
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
+ ClearAllStateEntryMessage,
14
+ ClearStateEntryMessage,
15
+ GetStateEntryMessage,
16
+ GetStateKeysEntryMessage,
17
+ GetStateKeysEntryMessage_StateKeys,
18
+ SetStateEntryMessage,
19
+ StartMessage_StateEntry,
20
+ } from "./generated/proto/protocol";
21
+ import { Empty } from "./generated/google/protobuf/empty";
22
+ import { jsonSerialize } from "./utils/utils";
23
+
24
+ export class LocalStateStore {
25
+ private state: Map<string, Buffer | Empty>;
26
+
27
+ constructor(private isPartial: boolean, state: StartMessage_StateEntry[]) {
28
+ this.state = new Map<string, Buffer | Empty>(
29
+ state.map(({ key, value }) => [key.toString(), value])
30
+ );
31
+ }
32
+
33
+ // Returns true if completed
34
+ public tryCompleteGet(key: string, msg: GetStateEntryMessage): boolean {
35
+ const stateEntry = this.state.get(key);
36
+ if (stateEntry === undefined) {
37
+ if (this.isPartial) {
38
+ // Partial eager state, so retrieve state from the runtime
39
+ return false;
40
+ } else {
41
+ // Complete eager state, so state entry is null
42
+ msg.empty = Empty.create({});
43
+ return true;
44
+ }
45
+ }
46
+
47
+ if (stateEntry instanceof Buffer) {
48
+ msg.value = stateEntry;
49
+ } else {
50
+ // stateEntry is Empty
51
+ msg.empty = stateEntry;
52
+ }
53
+ return true;
54
+ }
55
+
56
+ // Returns true if completed
57
+ public tryCompletedGetStateKeys(msg: GetStateKeysEntryMessage): boolean {
58
+ if (this.isPartial) {
59
+ return false;
60
+ }
61
+
62
+ msg.value = GetStateKeysEntryMessage_StateKeys.create({
63
+ keys: Array.from(this.state.keys()).map((b) => Buffer.from(b)),
64
+ });
65
+ return true;
66
+ }
67
+
68
+ public set<T>(key: string, value: T): SetStateEntryMessage {
69
+ const bytes = Buffer.from(jsonSerialize(value));
70
+ this.state.set(key, bytes);
71
+ return SetStateEntryMessage.create({
72
+ key: Buffer.from(key, "utf8"),
73
+ value: bytes,
74
+ });
75
+ }
76
+
77
+ public clear(key: string): ClearStateEntryMessage {
78
+ this.state.set(key, Empty.create({}));
79
+ return ClearStateEntryMessage.create({ key: Buffer.from(key) });
80
+ }
81
+
82
+ // When a GetState request does not have a local entry and we have partial state,
83
+ // then the request goes to the runtime.
84
+ // When we get the response of the runtime, we add the state to the localStateStore.
85
+ public add(key: string, result: Buffer | Empty): void {
86
+ this.state.set(key, result);
87
+ }
88
+
89
+ public clearAll(): ClearAllStateEntryMessage {
90
+ this.state.clear();
91
+ this.isPartial = false;
92
+ return {};
93
+ }
94
+ }