@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,290 @@
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 restate from "../public_api";
13
+ import { ensureError } from "../types/errors";
14
+
15
+ /**
16
+ * A client to interact with running workflows.
17
+ */
18
+ export interface WorkflowClient<R, U> {
19
+ /**
20
+ * Gets the ID of the workflow that this client talks to.
21
+ */
22
+ workflowId(): string;
23
+
24
+ /**
25
+ * Gets the status of the workflow, as a {@link restate.workflow.LifecycleStatus}.
26
+ * This will take on the values "NOT_STARTED", "RUNNING", "FINISHED", "FAILED".
27
+ */
28
+ status(): Promise<restate.workflow.LifecycleStatus>;
29
+
30
+ /**
31
+ * Returns a promise completed with the result. This will resolve successfully on successful
32
+ * termination of the workflow, and will be rejected if the workflow throws an Error.
33
+ */
34
+ result(): Promise<R>;
35
+
36
+ /**
37
+ * Gets the interface to the workflow through which all the workflow's additional methods
38
+ * can be called.
39
+ *
40
+ * To get the proper typed client, use the {@link WorkflowConnection.submitWorkflow} or
41
+ * {@link WorkflowConnection.connectToWorkflow} functions that accpet a typed ServiceApi
42
+ * object, as in the example below.
43
+ *
44
+ * @example
45
+ * In the workflow definition:
46
+ * ```
47
+ * const myWorkflow = restate.workflow.workflow("acme.myworkflow", { ... });
48
+ * export const myWorkflowApi = myworkflow.api;
49
+ * ```
50
+ * In the client code:
51
+ * ```
52
+ * import { myWorkflowApi } from "../server/myWorkflow"
53
+ * ...
54
+ * const restate = connectWorkflows("https://restatehost:8080");
55
+ * restate.submitWorkflow(myWorkflowApi, workflowId, args);
56
+ * restate.connectToWorkflow(myWorkflowApi, workflowId);
57
+ * ```
58
+ */
59
+ workflowInterface(): restate.Client<restate.workflow.WorkflowClientApi<U>>;
60
+ }
61
+
62
+ /**
63
+ * A connection to Restate that let's you submit workflows or connect to workflows.
64
+ * This is a typed client that internally makes HTTP calls to Restate to launch trigger
65
+ * an execution of a workflow service, or to connect to an existing execution.
66
+ */
67
+ export interface RestateClient {
68
+ submitWorkflow<R, T>(
69
+ path: string,
70
+ workflowId: string,
71
+ params: T
72
+ ): Promise<{
73
+ status: restate.workflow.WorkflowStartResult;
74
+ client: WorkflowClient<R, unknown>;
75
+ }>;
76
+
77
+ submitWorkflow<R, T, U>(
78
+ workflowApi: restate.ServiceApi<
79
+ restate.workflow.WorkflowRestateRpcApi<R, T, U>
80
+ >,
81
+ workflowId: string,
82
+ params: T
83
+ ): Promise<{
84
+ status: restate.workflow.WorkflowStartResult;
85
+ client: WorkflowClient<R, U>;
86
+ }>;
87
+
88
+ connectToWorkflow<R = unknown>(
89
+ path: string,
90
+ workflowId: string
91
+ ): Promise<{
92
+ status: restate.workflow.LifecycleStatus;
93
+ client: WorkflowClient<R, unknown>;
94
+ }>;
95
+
96
+ connectToWorkflow<R, T, U>(
97
+ workflowApi: restate.ServiceApi<
98
+ restate.workflow.WorkflowRestateRpcApi<R, T, U>
99
+ >,
100
+ workflowId: string
101
+ ): Promise<{
102
+ status: restate.workflow.LifecycleStatus;
103
+ client: WorkflowClient<R, U>;
104
+ }>;
105
+ }
106
+
107
+ /**
108
+ * Creates a typed client to start and interact with workflow executions.
109
+ * The specifiec URI must point to the Restate request endpoint (ingress).
110
+ *
111
+ * This function doesn't immediately verify the connection, it will not fail
112
+ * if Restate is unreachable. Connection failures will only manifest when
113
+ * attempting to submit or connect a specific workflow.
114
+ */
115
+ export function connect(restateUri: string): RestateClient {
116
+ return {
117
+ submitWorkflow: async <R, T, U>(
118
+ pathOrApi:
119
+ | string
120
+ | restate.ServiceApi<restate.workflow.WorkflowRestateRpcApi<R, T, U>>,
121
+ workflowId: string,
122
+ params: T
123
+ ): Promise<{
124
+ status: restate.workflow.WorkflowStartResult;
125
+ client: WorkflowClient<R, U>;
126
+ }> => {
127
+ const path = typeof pathOrApi === "string" ? pathOrApi : pathOrApi.path;
128
+
129
+ let result: restate.workflow.WorkflowStartResult;
130
+ try {
131
+ result = await makeCall(restateUri, path, "submit", workflowId, params);
132
+ } catch (err) {
133
+ const error = ensureError(err);
134
+ throw new Error("Cannot start workflow: " + error.message, {
135
+ cause: error,
136
+ });
137
+ }
138
+
139
+ return {
140
+ status: result,
141
+ client: new WorkflowClientImpl(restateUri, path, workflowId),
142
+ };
143
+ },
144
+
145
+ async connectToWorkflow<R, T, U>(
146
+ pathOrApi:
147
+ | string
148
+ | restate.ServiceApi<restate.workflow.WorkflowRestateRpcApi<R, T, U>>,
149
+ workflowId: string
150
+ ): Promise<{
151
+ status: restate.workflow.LifecycleStatus;
152
+ client: WorkflowClient<R, U>;
153
+ }> {
154
+ const path = typeof pathOrApi === "string" ? pathOrApi : pathOrApi.path;
155
+ const client: WorkflowClient<R, U> = new WorkflowClientImpl(
156
+ restateUri,
157
+ path,
158
+ workflowId
159
+ );
160
+ const status = await client.status();
161
+ if (status === restate.workflow.LifecycleStatus.NOT_STARTED) {
162
+ throw new Error(
163
+ "No workflow running/finished/failed with ID " + workflowId
164
+ );
165
+ }
166
+ return {
167
+ status,
168
+ client: new WorkflowClientImpl(restateUri, path, workflowId),
169
+ };
170
+ },
171
+ } satisfies RestateClient;
172
+ }
173
+
174
+ class WorkflowClientImpl<R, U> implements WorkflowClient<R, U> {
175
+ constructor(
176
+ private readonly restateUri: string,
177
+ private readonly serviceName: string,
178
+ private readonly wfId: string
179
+ ) {}
180
+
181
+ workflowId(): string {
182
+ return this.wfId;
183
+ }
184
+
185
+ status(): Promise<restate.workflow.LifecycleStatus> {
186
+ return this.makeCall("status", {});
187
+ }
188
+
189
+ result(): Promise<R> {
190
+ return this.makeCall("waitForResult", {});
191
+ }
192
+
193
+ workflowInterface(): restate.Client<restate.workflow.WorkflowClientApi<U>> {
194
+ const clientProxy = new Proxy(
195
+ {},
196
+ {
197
+ get: (_target, prop) => {
198
+ const method = prop as string;
199
+ return async (args: unknown) => {
200
+ return this.makeCall(method, args);
201
+ };
202
+ },
203
+ }
204
+ );
205
+
206
+ return clientProxy as restate.Client<restate.workflow.WorkflowClientApi<U>>;
207
+ }
208
+
209
+ private async makeCall<RR, TT>(method: string, args: TT): Promise<RR> {
210
+ return await makeCall(
211
+ this.restateUri,
212
+ this.serviceName,
213
+ method,
214
+ this.wfId,
215
+ args
216
+ );
217
+ }
218
+ }
219
+
220
+ // ----------------------------------------------------------------------------
221
+ // Utils
222
+ // ----------------------------------------------------------------------------
223
+
224
+ async function makeCall<R, T>(
225
+ restateUri: string,
226
+ serviceName: string,
227
+ method: string,
228
+ workflowId: string,
229
+ params: T
230
+ ): Promise<R> {
231
+ if (!workflowId || typeof workflowId !== "string") {
232
+ throw new Error("missing workflowId");
233
+ }
234
+ if (params === undefined) {
235
+ params = {} as T;
236
+ }
237
+ if (typeof params !== "object") {
238
+ throw new Error("invalid parameters: must be an object");
239
+ }
240
+
241
+ const url = `${restateUri}/${serviceName}/${method}`;
242
+ const data = {
243
+ request: {
244
+ workflowId,
245
+ ...params,
246
+ } satisfies restate.workflow.WorkflowRequest<T>,
247
+ };
248
+
249
+ let body: string;
250
+ try {
251
+ body = JSON.stringify(data);
252
+ } catch (err) {
253
+ throw new Error("Cannot encode request: " + err, { cause: err });
254
+ }
255
+
256
+ // eslint-disable-next-line no-console
257
+ console.debug(`Making call to Restate at ${url}`);
258
+
259
+ const httpResponse = await fetch(url, {
260
+ method: "POST",
261
+ headers: {
262
+ "Content-Type": "application/json",
263
+ },
264
+ body,
265
+ });
266
+
267
+ const responseText = await httpResponse.text();
268
+ if (!httpResponse.ok) {
269
+ throw new Error(`Request failed: ${httpResponse.status}\n${responseText}`);
270
+ }
271
+
272
+ let response;
273
+ try {
274
+ response = JSON.parse(responseText);
275
+ } catch (err) {
276
+ throw new Error("Cannot parse response JSON: " + err, { cause: err });
277
+ }
278
+
279
+ if (response.error) {
280
+ throw new Error(response.error);
281
+ }
282
+ if (response.response) {
283
+ return response.response as R;
284
+ }
285
+ if (Object.keys(response).length === 0) {
286
+ return undefined as R;
287
+ }
288
+
289
+ throw new Error("Unrecognized response object: " + responseText);
290
+ }
@@ -0,0 +1,47 @@
1
+ import { encodeMessages } from "../io/encoder";
2
+ import { Message } from "../types/types";
3
+ import { Connection } from "./connection";
4
+
5
+ export class BufferedConnection implements Connection {
6
+ private queue: Message[] = [];
7
+ private flushing: Promise<void> = Promise.resolve();
8
+
9
+ constructor(private readonly flushFn: (buffer: Buffer) => Promise<void>) {}
10
+
11
+ send(msg: Message): Promise<void> {
12
+ const len = this.queue.push(msg);
13
+ if (len === 1) {
14
+ // we are the first in line, therefore we schedule a flush,
15
+ // BUT we must wait for the previous flush to end.
16
+ this.flushing = this.flushing.then(() => this.scheduleFlush());
17
+ }
18
+ // we don't need to reschedule the `flush` here,
19
+ // because the flush happens anyway at the end of the current event loop iteration.
20
+ // tag along to the previously scheduled flush.
21
+ return this.flushing;
22
+ }
23
+
24
+ end(): Promise<void> {
25
+ this.flushing = this.flushing.then(() => this.flush());
26
+ return this.flushing;
27
+ }
28
+
29
+ private scheduleFlush(): Promise<void> {
30
+ // schedule a flush at the end of the current event loop iteration.
31
+ return new Promise((resolve, reject) =>
32
+ setImmediate(() => {
33
+ this.flush().then(resolve).catch(reject);
34
+ })
35
+ );
36
+ }
37
+
38
+ private async flush(): Promise<void> {
39
+ if (this.queue.length === 0) {
40
+ return Promise.resolve();
41
+ }
42
+ const buffer = encodeMessages(this.queue) as Buffer;
43
+ this.queue = [];
44
+
45
+ return this.flushFn(buffer);
46
+ }
47
+ }
@@ -0,0 +1,34 @@
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 { Message } from "../types/types";
13
+
14
+ /**
15
+ * A connection from the service/SDK to Restate.
16
+ * Accepts messages to be sent and committed to the journal.
17
+ */
18
+ export interface Connection {
19
+ send(msg: Message): Promise<void>;
20
+
21
+ end(): Promise<void>;
22
+ }
23
+
24
+ /**
25
+ * A consumer of a message stream from Restate.
26
+ * Messages include journal replay messages and completion messages.
27
+ */
28
+ export interface RestateStreamConsumer {
29
+ handleMessage(m: Message): boolean;
30
+
31
+ handleStreamError(e: Error): void;
32
+
33
+ handleInputClosed(): void;
34
+ }
@@ -0,0 +1,62 @@
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 { RemoteContext } from "../generated/proto/services";
13
+ import { Message } from "../types/types";
14
+ import { BufferedConnection } from "./buffered_connection";
15
+ import { Connection } from "./connection";
16
+
17
+ export class FencedOffError extends Error {
18
+ constructor() {
19
+ super("FencedOff");
20
+ }
21
+ }
22
+
23
+ export class InvocationAlreadyCompletedError extends Error {
24
+ constructor() {
25
+ super("Completed");
26
+ }
27
+ }
28
+
29
+ export class EmbeddedConnection implements Connection {
30
+ private buffered: BufferedConnection;
31
+
32
+ constructor(
33
+ private readonly operationId: string,
34
+ private readonly streamId: string,
35
+ private readonly remote: RemoteContext
36
+ ) {
37
+ this.buffered = new BufferedConnection((buffer) => this.sendBuffer(buffer));
38
+ }
39
+
40
+ send(msg: Message): Promise<void> {
41
+ return this.buffered.send(msg);
42
+ }
43
+
44
+ end(): Promise<void> {
45
+ return this.buffered.end();
46
+ }
47
+
48
+ private async sendBuffer(buffer: Buffer): Promise<void> {
49
+ const res = await this.remote.send({
50
+ operationId: this.operationId,
51
+ streamId: this.streamId,
52
+ messages: buffer,
53
+ });
54
+
55
+ if (res.invalidStream !== undefined) {
56
+ throw new FencedOffError();
57
+ }
58
+ if (res.invocationCompleted !== undefined) {
59
+ throw new InvocationAlreadyCompletedError();
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,228 @@
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 stream from "stream";
13
+ import { streamDecoder } from "../io/decoder";
14
+ import { Connection, RestateStreamConsumer } from "./connection";
15
+ import { Message } from "../types/types";
16
+ import { rlog } from "../logger";
17
+ import { finished } from "stream/promises";
18
+ import { BufferedConnection } from "./buffered_connection";
19
+
20
+ // utility promise, for cases where we want to save allocation of an extra promise
21
+ const RESOLVED: Promise<void> = Promise.resolve();
22
+
23
+ /**
24
+ * A duplex stream with Restate Messages over HTTP2.
25
+ *
26
+ * This stream handles the following concerns:
27
+ *
28
+ * (1) encoding and decoding of messages from and from raw bytes
29
+ *
30
+ * (2) buffering the outgoing messages, because the call sites that produce (potentially) large
31
+ * messages might not await their transfer. Aside from the fact that we achieve better pipelining
32
+ * that way, we also simply cannot guarantee that users of the Restate SDK actually await the
33
+ * relevant async API methods.
34
+ *
35
+ * This stream essentially buffers messages and, upon flush, sends them asynchronously, as the
36
+ * stream has availability. Flush requests queue up, if new data gets flushed while the previous
37
+ * data is still being sent.
38
+ *
39
+ * (3) Input messages can be pipelined to a sequence of consumers. For example, first to a journal,
40
+ * and afterwards to the state machine.
41
+ *
42
+ * (4) Handling the relevant stream events for errors and consolidating them to one error handler, plus
43
+ * notifications for cleanly closed input (to trigger suspension).
44
+ */
45
+ export class RestateHttp2Connection implements Connection {
46
+ /**
47
+ * create a RestateDuplex stream from an http2 (duplex) stream.
48
+ */
49
+ public static from(http2stream: stream.Duplex): RestateHttp2Connection {
50
+ return new RestateHttp2Connection(http2stream);
51
+ }
52
+
53
+ // --------------------------------------------------------------------------
54
+
55
+ // input as decoded messages
56
+ private readonly sdkInput: stream.Readable;
57
+
58
+ // consumer handling
59
+ private currentConsumer: RestateStreamConsumer | null = null;
60
+ private inputBuffer: Message[] = [];
61
+ private consumerError?: Error;
62
+ private consumerInputClosed = false;
63
+
64
+ private outputBuffer: BufferedConnection;
65
+
66
+ constructor(private readonly rawStream: stream.Duplex) {
67
+ this.sdkInput = rawStream.pipe(streamDecoder());
68
+
69
+ this.outputBuffer = new BufferedConnection((buffer) => {
70
+ const hasMoreCapacity = rawStream.write(buffer);
71
+ if (hasMoreCapacity) {
72
+ return RESOLVED;
73
+ } else {
74
+ return new Promise((resolve) => rawStream.once("drain", resolve));
75
+ }
76
+ });
77
+
78
+ // remember and forward messages
79
+ this.sdkInput.on("data", (m: Message) => {
80
+ // deliver message, if we have a consumer. otherwise buffer the message.
81
+ if (this.currentConsumer) {
82
+ if (this.currentConsumer.handleMessage(m)) {
83
+ this.removeCurrentConsumer();
84
+ }
85
+ } else {
86
+ this.inputBuffer.push(m);
87
+ }
88
+ });
89
+
90
+ // remember and forward close events
91
+ this.sdkInput.on("end", () => {
92
+ this.consumerInputClosed = true;
93
+ if (this.currentConsumer) {
94
+ this.currentConsumer.handleInputClosed();
95
+ }
96
+ });
97
+
98
+ // --------- error handling --------
99
+ // - a.k.a. node event wrangling...
100
+
101
+ // the error handler for all sorts of errors coming from streams
102
+ const errorHandler = (e: Error) => {
103
+ // make sure we don't overwrite the initial error
104
+ if (this.consumerError !== undefined) {
105
+ return;
106
+ }
107
+ this.consumerError = e;
108
+ if (this.currentConsumer) {
109
+ this.currentConsumer.handleStreamError(e);
110
+ }
111
+ };
112
+
113
+ // those two event types should cover all types of connection losses
114
+ rawStream.on("aborted", () => {
115
+ rlog.error("Connection to Restate was lost");
116
+ errorHandler(new Error("Connection to Restate was lost"));
117
+ });
118
+
119
+ // this is both the raw http2 stream and the output SDK->Restate
120
+ rawStream.on("error", (e: Error) => {
121
+ rlog.error("Error in http2 stream to Restate: " + (e.stack ?? e.message));
122
+ errorHandler(e);
123
+ });
124
+
125
+ // these events notify of errors in the decoding pipeline
126
+ this.sdkInput.on("error", (e: Error) => {
127
+ rlog.error(
128
+ "Error in input stream (Restate to Service): " + (e.stack ?? e.message)
129
+ );
130
+ errorHandler(e);
131
+ });
132
+
133
+ // see if streams get torn down before they end cleanly
134
+ this.sdkInput.on("close", () => {
135
+ if (!this.consumerInputClosed) {
136
+ errorHandler(new Error("stream was destroyed before end"));
137
+ }
138
+ });
139
+ }
140
+
141
+ // --------------------------------------------------------------------------
142
+ // input stream handling
143
+ // --------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Pipes the messages from this connection to the given consumer. The consumer
147
+ * will also receive error and stream closing notifications.
148
+ *
149
+ * Once the 'handleMessage()' method returns 'true', the consumer is immediately removed.
150
+ * That way, consumers can consume a bounded amount of messages (like just the initial journal).
151
+ *
152
+ * There can only be one consumer at a time.
153
+ */
154
+ public pipeToConsumer(consumer: RestateStreamConsumer): void {
155
+ if (this.currentConsumer !== null) {
156
+ throw new Error("Already piping to a consumer");
157
+ }
158
+
159
+ this.currentConsumer = consumer;
160
+
161
+ // propagate pre-existing information
162
+ if (this.consumerError) {
163
+ consumer.handleStreamError(this.consumerError);
164
+ }
165
+ if (this.consumerInputClosed) {
166
+ consumer.handleInputClosed();
167
+ }
168
+
169
+ // pipe the buffered input messages, if we buffered some before the consumer was registered
170
+ const input = this.inputBuffer;
171
+ if (input.length > 0) {
172
+ let i = 0;
173
+ while (i < input.length) {
174
+ const done = consumer.handleMessage(input[i]);
175
+ i++;
176
+ if (done) {
177
+ this.removeCurrentConsumer();
178
+ break;
179
+ }
180
+ }
181
+ this.inputBuffer = i === input.length ? [] : this.inputBuffer.slice(i);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Removes the current consumer, if there is one.
187
+ */
188
+ public removeCurrentConsumer(): void {
189
+ this.currentConsumer = null;
190
+ }
191
+
192
+ // --------------------------------------------------------------------------
193
+ // output stream handling
194
+ // --------------------------------------------------------------------------
195
+
196
+ /**
197
+ * Adds a message to the output stream.
198
+ *
199
+ * This always puts the message into the node stream, but will return a promise that is resolved once
200
+ * further messages can be written.
201
+ *
202
+ * The reasoning is that some, but not all Restate operations return promises and are typically
203
+ * awaited. For example, rpc, sleep, side-effect have promises and are awaited, while one-way-sends and
204
+ * state updates don't return promises.
205
+ *
206
+ * As a pragmatic solution, we always accept messages, but return a promise for when the output has
207
+ * capacity again, so that at least the operations that await results will respect backpressure.
208
+ */
209
+ public send(msg: Message): Promise<void> {
210
+ return this.outputBuffer.send(msg);
211
+ }
212
+
213
+ /**
214
+ * Ends the stream, awaiting pending writes.
215
+ */
216
+ public async end(): Promise<void> {
217
+ await this.outputBuffer.end();
218
+
219
+ this.rawStream.end();
220
+
221
+ const options = {
222
+ error: true,
223
+ cleanup: true,
224
+ };
225
+
226
+ await finished(this.rawStream, options);
227
+ }
228
+ }