rivetkit 2.0.1 → 2.0.3

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 (253) hide show
  1. package/dist/schemas/actor-persist/v1.ts +228 -0
  2. package/dist/schemas/client-protocol/v1.ts +429 -0
  3. package/dist/schemas/file-system-driver/v1.ts +102 -0
  4. package/dist/tsup/actor/errors.cjs +69 -0
  5. package/dist/tsup/actor/errors.cjs.map +1 -0
  6. package/dist/tsup/actor/errors.d.cts +143 -0
  7. package/dist/tsup/actor/errors.d.ts +143 -0
  8. package/dist/tsup/actor/errors.js +69 -0
  9. package/dist/tsup/actor/errors.js.map +1 -0
  10. package/dist/tsup/chunk-2CRLFV6Z.cjs +202 -0
  11. package/dist/tsup/chunk-2CRLFV6Z.cjs.map +1 -0
  12. package/dist/tsup/chunk-3H7O2A7I.js +525 -0
  13. package/dist/tsup/chunk-3H7O2A7I.js.map +1 -0
  14. package/dist/tsup/chunk-42I3OZ3Q.js +15 -0
  15. package/dist/tsup/chunk-42I3OZ3Q.js.map +1 -0
  16. package/dist/tsup/chunk-4NSUQZ2H.js +1790 -0
  17. package/dist/tsup/chunk-4NSUQZ2H.js.map +1 -0
  18. package/dist/tsup/chunk-6PDXBYI5.js +132 -0
  19. package/dist/tsup/chunk-6PDXBYI5.js.map +1 -0
  20. package/dist/tsup/chunk-6WKQDDUD.cjs +1790 -0
  21. package/dist/tsup/chunk-6WKQDDUD.cjs.map +1 -0
  22. package/dist/tsup/chunk-CTBOSFUH.cjs +116 -0
  23. package/dist/tsup/chunk-CTBOSFUH.cjs.map +1 -0
  24. package/dist/tsup/chunk-EGVZZFE2.js +2857 -0
  25. package/dist/tsup/chunk-EGVZZFE2.js.map +1 -0
  26. package/dist/tsup/chunk-FCCPJNMA.cjs +132 -0
  27. package/dist/tsup/chunk-FCCPJNMA.cjs.map +1 -0
  28. package/dist/tsup/chunk-FLMTTN27.js +244 -0
  29. package/dist/tsup/chunk-FLMTTN27.js.map +1 -0
  30. package/dist/tsup/chunk-GIR3AFFI.cjs +315 -0
  31. package/dist/tsup/chunk-GIR3AFFI.cjs.map +1 -0
  32. package/dist/tsup/chunk-INGJP237.js +315 -0
  33. package/dist/tsup/chunk-INGJP237.js.map +1 -0
  34. package/dist/tsup/chunk-KJCJLKRM.js +116 -0
  35. package/dist/tsup/chunk-KJCJLKRM.js.map +1 -0
  36. package/dist/tsup/chunk-KUPQZYUQ.cjs +15 -0
  37. package/dist/tsup/chunk-KUPQZYUQ.cjs.map +1 -0
  38. package/dist/tsup/chunk-O2MBYIXO.cjs +2857 -0
  39. package/dist/tsup/chunk-O2MBYIXO.cjs.map +1 -0
  40. package/dist/tsup/chunk-OGAPU3UG.cjs +525 -0
  41. package/dist/tsup/chunk-OGAPU3UG.cjs.map +1 -0
  42. package/dist/tsup/chunk-OV6AYD4S.js +4406 -0
  43. package/dist/tsup/chunk-OV6AYD4S.js.map +1 -0
  44. package/dist/tsup/chunk-PO4VLDWA.js +47 -0
  45. package/dist/tsup/chunk-PO4VLDWA.js.map +1 -0
  46. package/dist/tsup/chunk-R2OPSKIV.cjs +244 -0
  47. package/dist/tsup/chunk-R2OPSKIV.cjs.map +1 -0
  48. package/dist/tsup/chunk-TZJKSBUQ.cjs +47 -0
  49. package/dist/tsup/chunk-TZJKSBUQ.cjs.map +1 -0
  50. package/dist/tsup/chunk-UBUC5C3G.cjs +189 -0
  51. package/dist/tsup/chunk-UBUC5C3G.cjs.map +1 -0
  52. package/dist/tsup/chunk-UIM22YJL.cjs +4406 -0
  53. package/dist/tsup/chunk-UIM22YJL.cjs.map +1 -0
  54. package/dist/tsup/chunk-URVFQMYI.cjs +230 -0
  55. package/dist/tsup/chunk-URVFQMYI.cjs.map +1 -0
  56. package/dist/tsup/chunk-UVUPOS46.js +230 -0
  57. package/dist/tsup/chunk-UVUPOS46.js.map +1 -0
  58. package/dist/tsup/chunk-VRRHBNJC.js +189 -0
  59. package/dist/tsup/chunk-VRRHBNJC.js.map +1 -0
  60. package/dist/tsup/chunk-XFSS33EQ.js +202 -0
  61. package/dist/tsup/chunk-XFSS33EQ.js.map +1 -0
  62. package/dist/tsup/client/mod.cjs +32 -0
  63. package/dist/tsup/client/mod.cjs.map +1 -0
  64. package/dist/tsup/client/mod.d.cts +26 -0
  65. package/dist/tsup/client/mod.d.ts +26 -0
  66. package/dist/tsup/client/mod.js +32 -0
  67. package/dist/tsup/client/mod.js.map +1 -0
  68. package/dist/tsup/common/log.cjs +13 -0
  69. package/dist/tsup/common/log.cjs.map +1 -0
  70. package/dist/tsup/common/log.d.cts +20 -0
  71. package/dist/tsup/common/log.d.ts +20 -0
  72. package/dist/tsup/common/log.js +13 -0
  73. package/dist/tsup/common/log.js.map +1 -0
  74. package/dist/tsup/common/websocket.cjs +10 -0
  75. package/dist/tsup/common/websocket.cjs.map +1 -0
  76. package/dist/tsup/common/websocket.d.cts +3 -0
  77. package/dist/tsup/common/websocket.d.ts +3 -0
  78. package/dist/tsup/common/websocket.js +10 -0
  79. package/dist/tsup/common/websocket.js.map +1 -0
  80. package/dist/tsup/common-CpqORuCq.d.cts +218 -0
  81. package/dist/tsup/common-CpqORuCq.d.ts +218 -0
  82. package/dist/tsup/connection-BR_Ve4ku.d.cts +2117 -0
  83. package/dist/tsup/connection-BwUMoe6n.d.ts +2117 -0
  84. package/dist/tsup/driver-helpers/mod.cjs +33 -0
  85. package/dist/tsup/driver-helpers/mod.cjs.map +1 -0
  86. package/dist/tsup/driver-helpers/mod.d.cts +18 -0
  87. package/dist/tsup/driver-helpers/mod.d.ts +18 -0
  88. package/dist/tsup/driver-helpers/mod.js +33 -0
  89. package/dist/tsup/driver-helpers/mod.js.map +1 -0
  90. package/dist/tsup/driver-test-suite/mod.cjs +4619 -0
  91. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -0
  92. package/dist/tsup/driver-test-suite/mod.d.cts +57 -0
  93. package/dist/tsup/driver-test-suite/mod.d.ts +57 -0
  94. package/dist/tsup/driver-test-suite/mod.js +4619 -0
  95. package/dist/tsup/driver-test-suite/mod.js.map +1 -0
  96. package/dist/tsup/inspector/mod.cjs +53 -0
  97. package/dist/tsup/inspector/mod.cjs.map +1 -0
  98. package/dist/tsup/inspector/mod.d.cts +408 -0
  99. package/dist/tsup/inspector/mod.d.ts +408 -0
  100. package/dist/tsup/inspector/mod.js +53 -0
  101. package/dist/tsup/inspector/mod.js.map +1 -0
  102. package/dist/tsup/mod.cjs +73 -0
  103. package/dist/tsup/mod.cjs.map +1 -0
  104. package/dist/tsup/mod.d.cts +100 -0
  105. package/dist/tsup/mod.d.ts +100 -0
  106. package/dist/tsup/mod.js +73 -0
  107. package/dist/tsup/mod.js.map +1 -0
  108. package/dist/tsup/router-endpoints-AYkXG8Tl.d.cts +66 -0
  109. package/dist/tsup/router-endpoints-DAbqVFx2.d.ts +66 -0
  110. package/dist/tsup/test/mod.cjs +21 -0
  111. package/dist/tsup/test/mod.cjs.map +1 -0
  112. package/dist/tsup/test/mod.d.cts +27 -0
  113. package/dist/tsup/test/mod.d.ts +27 -0
  114. package/dist/tsup/test/mod.js +21 -0
  115. package/dist/tsup/test/mod.js.map +1 -0
  116. package/dist/tsup/utils-CT0cv4jd.d.cts +17 -0
  117. package/dist/tsup/utils-CT0cv4jd.d.ts +17 -0
  118. package/dist/tsup/utils.cjs +26 -0
  119. package/dist/tsup/utils.cjs.map +1 -0
  120. package/dist/tsup/utils.d.cts +36 -0
  121. package/dist/tsup/utils.d.ts +36 -0
  122. package/dist/tsup/utils.js +26 -0
  123. package/dist/tsup/utils.js.map +1 -0
  124. package/package.json +208 -5
  125. package/src/actor/action.ts +182 -0
  126. package/src/actor/config.ts +765 -0
  127. package/src/actor/connection.ts +260 -0
  128. package/src/actor/context.ts +171 -0
  129. package/src/actor/database.ts +23 -0
  130. package/src/actor/definition.ts +86 -0
  131. package/src/actor/driver.ts +84 -0
  132. package/src/actor/errors.ts +360 -0
  133. package/src/actor/generic-conn-driver.ts +234 -0
  134. package/src/actor/instance.ts +1800 -0
  135. package/src/actor/log.ts +15 -0
  136. package/src/actor/mod.ts +113 -0
  137. package/src/actor/persisted.ts +42 -0
  138. package/src/actor/protocol/old.ts +281 -0
  139. package/src/actor/protocol/serde.ts +131 -0
  140. package/src/actor/router-endpoints.ts +685 -0
  141. package/src/actor/router.ts +263 -0
  142. package/src/actor/schedule.ts +17 -0
  143. package/src/actor/unstable-react.ts +110 -0
  144. package/src/actor/utils.ts +98 -0
  145. package/src/client/actor-common.ts +30 -0
  146. package/src/client/actor-conn.ts +804 -0
  147. package/src/client/actor-handle.ts +208 -0
  148. package/src/client/client.ts +623 -0
  149. package/src/client/errors.ts +41 -0
  150. package/src/client/http-client-driver.ts +326 -0
  151. package/src/client/log.ts +7 -0
  152. package/src/client/mod.ts +56 -0
  153. package/src/client/raw-utils.ts +92 -0
  154. package/src/client/test.ts +44 -0
  155. package/src/client/utils.ts +150 -0
  156. package/src/common/eventsource-interface.ts +47 -0
  157. package/src/common/eventsource.ts +80 -0
  158. package/src/common/fake-event-source.ts +266 -0
  159. package/src/common/inline-websocket-adapter2.ts +445 -0
  160. package/src/common/log-levels.ts +27 -0
  161. package/src/common/log.ts +139 -0
  162. package/src/common/logfmt.ts +228 -0
  163. package/src/common/network.ts +2 -0
  164. package/src/common/router.ts +87 -0
  165. package/src/common/utils.ts +322 -0
  166. package/src/common/versioned-data.ts +95 -0
  167. package/src/common/websocket-interface.ts +49 -0
  168. package/src/common/websocket.ts +43 -0
  169. package/src/driver-helpers/mod.ts +22 -0
  170. package/src/driver-helpers/utils.ts +17 -0
  171. package/src/driver-test-suite/log.ts +7 -0
  172. package/src/driver-test-suite/mod.ts +213 -0
  173. package/src/driver-test-suite/test-inline-client-driver.ts +402 -0
  174. package/src/driver-test-suite/tests/action-features.ts +136 -0
  175. package/src/driver-test-suite/tests/actor-auth.ts +591 -0
  176. package/src/driver-test-suite/tests/actor-conn-state.ts +249 -0
  177. package/src/driver-test-suite/tests/actor-conn.ts +349 -0
  178. package/src/driver-test-suite/tests/actor-driver.ts +25 -0
  179. package/src/driver-test-suite/tests/actor-error-handling.ts +158 -0
  180. package/src/driver-test-suite/tests/actor-handle.ts +259 -0
  181. package/src/driver-test-suite/tests/actor-inline-client.ts +152 -0
  182. package/src/driver-test-suite/tests/actor-inspector.ts +570 -0
  183. package/src/driver-test-suite/tests/actor-metadata.ts +116 -0
  184. package/src/driver-test-suite/tests/actor-onstatechange.ts +95 -0
  185. package/src/driver-test-suite/tests/actor-schedule.ts +108 -0
  186. package/src/driver-test-suite/tests/actor-sleep.ts +413 -0
  187. package/src/driver-test-suite/tests/actor-state.ts +54 -0
  188. package/src/driver-test-suite/tests/actor-vars.ts +93 -0
  189. package/src/driver-test-suite/tests/manager-driver.ts +365 -0
  190. package/src/driver-test-suite/tests/raw-http-direct-registry.ts +226 -0
  191. package/src/driver-test-suite/tests/raw-http-request-properties.ts +414 -0
  192. package/src/driver-test-suite/tests/raw-http.ts +347 -0
  193. package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +392 -0
  194. package/src/driver-test-suite/tests/raw-websocket.ts +484 -0
  195. package/src/driver-test-suite/tests/request-access.ts +244 -0
  196. package/src/driver-test-suite/utils.ts +68 -0
  197. package/src/drivers/default.ts +31 -0
  198. package/src/drivers/engine/actor-driver.ts +360 -0
  199. package/src/drivers/engine/api-endpoints.ts +128 -0
  200. package/src/drivers/engine/api-utils.ts +70 -0
  201. package/src/drivers/engine/config.ts +39 -0
  202. package/src/drivers/engine/keys.test.ts +266 -0
  203. package/src/drivers/engine/keys.ts +89 -0
  204. package/src/drivers/engine/kv.ts +3 -0
  205. package/src/drivers/engine/log.ts +7 -0
  206. package/src/drivers/engine/manager-driver.ts +391 -0
  207. package/src/drivers/engine/mod.ts +36 -0
  208. package/src/drivers/engine/ws-proxy.ts +170 -0
  209. package/src/drivers/file-system/actor.ts +91 -0
  210. package/src/drivers/file-system/global-state.ts +673 -0
  211. package/src/drivers/file-system/log.ts +7 -0
  212. package/src/drivers/file-system/manager.ts +306 -0
  213. package/src/drivers/file-system/mod.ts +48 -0
  214. package/src/drivers/file-system/utils.ts +109 -0
  215. package/src/globals.d.ts +6 -0
  216. package/src/inline-client-driver/log.ts +7 -0
  217. package/src/inline-client-driver/mod.ts +385 -0
  218. package/src/inspector/actor.ts +298 -0
  219. package/src/inspector/config.ts +83 -0
  220. package/src/inspector/log.ts +5 -0
  221. package/src/inspector/manager.ts +86 -0
  222. package/src/inspector/mod.ts +2 -0
  223. package/src/inspector/protocol/actor.ts +10 -0
  224. package/src/inspector/protocol/common.ts +196 -0
  225. package/src/inspector/protocol/manager.ts +10 -0
  226. package/src/inspector/protocol/mod.ts +2 -0
  227. package/src/inspector/utils.ts +76 -0
  228. package/src/manager/auth.ts +121 -0
  229. package/src/manager/driver.ts +80 -0
  230. package/src/manager/hono-websocket-adapter.ts +333 -0
  231. package/src/manager/log.ts +7 -0
  232. package/src/manager/mod.ts +2 -0
  233. package/src/manager/protocol/mod.ts +24 -0
  234. package/src/manager/protocol/query.ts +89 -0
  235. package/src/manager/router.ts +1792 -0
  236. package/src/mod.ts +20 -0
  237. package/src/registry/config.ts +32 -0
  238. package/src/registry/log.ts +7 -0
  239. package/src/registry/mod.ts +124 -0
  240. package/src/registry/run-config.ts +54 -0
  241. package/src/registry/serve.ts +53 -0
  242. package/src/schemas/actor-persist/mod.ts +1 -0
  243. package/src/schemas/actor-persist/versioned.ts +25 -0
  244. package/src/schemas/client-protocol/mod.ts +1 -0
  245. package/src/schemas/client-protocol/versioned.ts +63 -0
  246. package/src/schemas/file-system-driver/mod.ts +1 -0
  247. package/src/schemas/file-system-driver/versioned.ts +28 -0
  248. package/src/serde.ts +84 -0
  249. package/src/test/config.ts +16 -0
  250. package/src/test/log.ts +7 -0
  251. package/src/test/mod.ts +153 -0
  252. package/src/utils.ts +172 -0
  253. package/README.md +0 -13
@@ -0,0 +1,804 @@
1
+ import * as cbor from "cbor-x";
2
+ import invariant from "invariant";
3
+ import pRetry from "p-retry";
4
+ import type { CloseEvent, WebSocket } from "ws";
5
+ import type { AnyActorDefinition } from "@/actor/definition";
6
+ import { inputDataToBuffer } from "@/actor/protocol/old";
7
+ import { type Encoding, jsonStringifyCompat } from "@/actor/protocol/serde";
8
+ import type {
9
+ UniversalErrorEvent,
10
+ UniversalEventSource,
11
+ UniversalMessageEvent,
12
+ } from "@/common/eventsource-interface";
13
+ import { assertUnreachable, stringifyError } from "@/common/utils";
14
+ import type { ActorQuery } from "@/manager/protocol/query";
15
+ import type * as protocol from "@/schemas/client-protocol/mod";
16
+ import {
17
+ TO_CLIENT_VERSIONED,
18
+ TO_SERVER_VERSIONED,
19
+ } from "@/schemas/client-protocol/versioned";
20
+ import {
21
+ deserializeWithEncoding,
22
+ encodingIsBinary,
23
+ serializeWithEncoding,
24
+ } from "@/serde";
25
+ import { bufferToArrayBuffer, getEnvUniversal } from "@/utils";
26
+ import type { ActorDefinitionActions } from "./actor-common";
27
+ import {
28
+ ACTOR_CONNS_SYMBOL,
29
+ type ClientDriver,
30
+ type ClientRaw,
31
+ TRANSPORT_SYMBOL,
32
+ } from "./client";
33
+ import * as errors from "./errors";
34
+ import { logger } from "./log";
35
+ import { type WebSocketMessage as ConnMessage, messageLength } from "./utils";
36
+
37
+ interface ActionInFlight {
38
+ name: string;
39
+ resolve: (response: protocol.ActionResponse) => void;
40
+ reject: (error: Error) => void;
41
+ }
42
+
43
+ interface EventSubscriptions<Args extends Array<unknown>> {
44
+ callback: (...args: Args) => void;
45
+ once: boolean;
46
+ }
47
+
48
+ /**
49
+ * A function that unsubscribes from an event.
50
+ *
51
+ * @typedef {Function} EventUnsubscribe
52
+ */
53
+ export type EventUnsubscribe = () => void;
54
+
55
+ /**
56
+ * A function that handles connection errors.
57
+ *
58
+ * @typedef {Function} ActorErrorCallback
59
+ */
60
+ export type ActorErrorCallback = (error: errors.ActorError) => void;
61
+
62
+ export interface SendHttpMessageOpts {
63
+ ephemeral: boolean;
64
+ signal?: AbortSignal;
65
+ }
66
+
67
+ export type ConnTransport =
68
+ | { websocket: WebSocket }
69
+ | { sse: UniversalEventSource };
70
+
71
+ export const CONNECT_SYMBOL = Symbol("connect");
72
+
73
+ /**
74
+ * Provides underlying functions for {@link ActorConn}. See {@link ActorConn} for using type-safe remote procedure calls.
75
+ *
76
+ * @see {@link ActorConn}
77
+ */
78
+ export class ActorConnRaw {
79
+ #disposed = false;
80
+
81
+ /* Will be aborted on dispose. */
82
+ #abortController = new AbortController();
83
+
84
+ /** If attempting to connect. Helpful for knowing if in a retry loop when reconnecting. */
85
+ #connecting = false;
86
+
87
+ // These will only be set on SSE driver
88
+ #actorId?: string;
89
+ #connectionId?: string;
90
+ #connectionToken?: string;
91
+
92
+ #transport?: ConnTransport;
93
+
94
+ #messageQueue: protocol.ToServer[] = [];
95
+ #actionsInFlight = new Map<number, ActionInFlight>();
96
+
97
+ // biome-ignore lint/suspicious/noExplicitAny: Unknown subscription type
98
+ #eventSubscriptions = new Map<string, Set<EventSubscriptions<any[]>>>();
99
+
100
+ #errorHandlers = new Set<ActorErrorCallback>();
101
+
102
+ #actionIdCounter = 0;
103
+
104
+ /**
105
+ * Interval that keeps the NodeJS process alive if this is the only thing running.
106
+ *
107
+ * See ttps://github.com/nodejs/node/issues/22088
108
+ */
109
+ #keepNodeAliveInterval: NodeJS.Timeout;
110
+
111
+ /** Promise used to indicate the socket has connected successfully. This will be rejected if the connection fails. */
112
+ #onOpenPromise?: PromiseWithResolvers<undefined>;
113
+
114
+ #client: ClientRaw;
115
+ #driver: ClientDriver;
116
+ #params: unknown;
117
+ #encodingKind: Encoding;
118
+ #actorQuery: ActorQuery;
119
+
120
+ // TODO: ws message queue
121
+
122
+ /**
123
+ * Do not call this directly.
124
+ *
125
+ * Creates an instance of ActorConnRaw.
126
+ *
127
+ * @protected
128
+ */
129
+ public constructor(
130
+ client: ClientRaw,
131
+ driver: ClientDriver,
132
+ params: unknown,
133
+ encodingKind: Encoding,
134
+ actorQuery: ActorQuery,
135
+ ) {
136
+ this.#client = client;
137
+ this.#driver = driver;
138
+ this.#params = params;
139
+ this.#encodingKind = encodingKind;
140
+ this.#actorQuery = actorQuery;
141
+
142
+ this.#keepNodeAliveInterval = setInterval(() => 60_000);
143
+ }
144
+
145
+ /**
146
+ * Call a raw action connection. See {@link ActorConn} for type-safe action calls.
147
+ *
148
+ * @see {@link ActorConn}
149
+ * @template Args - The type of arguments to pass to the action function.
150
+ * @template Response - The type of the response returned by the action function.
151
+ * @param {string} name - The name of the action function to call.
152
+ * @param {...Args} args - The arguments to pass to the action function.
153
+ * @returns {Promise<Response>} - A promise that resolves to the response of the action function.
154
+ */
155
+ async action<
156
+ Args extends Array<unknown> = unknown[],
157
+ Response = unknown,
158
+ >(opts: {
159
+ name: string;
160
+ args: Args;
161
+ signal?: AbortSignal;
162
+ }): Promise<Response> {
163
+ logger().debug("action", { name: opts.name, args: opts.args });
164
+
165
+ // If we have an active connection, use the websockactionId
166
+ const actionId = this.#actionIdCounter;
167
+ this.#actionIdCounter += 1;
168
+
169
+ const { promise, resolve, reject } =
170
+ Promise.withResolvers<protocol.ActionResponse>();
171
+ this.#actionsInFlight.set(actionId, { name: opts.name, resolve, reject });
172
+
173
+ this.#sendMessage({
174
+ body: {
175
+ tag: "ActionRequest",
176
+ val: {
177
+ id: BigInt(actionId),
178
+ name: opts.name,
179
+ args: bufferToArrayBuffer(cbor.encode(opts.args)),
180
+ },
181
+ },
182
+ } satisfies protocol.ToServer);
183
+
184
+ // TODO: Throw error if disconnect is called
185
+
186
+ const { id: responseId, output } = await promise;
187
+ if (responseId !== BigInt(actionId))
188
+ throw new Error(
189
+ `Request ID ${actionId} does not match response ID ${responseId}`,
190
+ );
191
+
192
+ return cbor.decode(new Uint8Array(output)) as Response;
193
+ }
194
+
195
+ /**
196
+ * Do not call this directly.
197
+ enc
198
+ * Establishes a connection to the server using the specified endpoint & encoding & driver.
199
+ *
200
+ * @protected
201
+ */
202
+ public [CONNECT_SYMBOL]() {
203
+ this.#connectWithRetry();
204
+ }
205
+
206
+ async #connectWithRetry() {
207
+ this.#connecting = true;
208
+
209
+ // Attempt to reconnect indefinitely
210
+ try {
211
+ await pRetry(this.#connectAndWait.bind(this), {
212
+ forever: true,
213
+ minTimeout: 250,
214
+ maxTimeout: 30_000,
215
+
216
+ onFailedAttempt: (error) => {
217
+ logger().warn("failed to reconnect", {
218
+ attempt: error.attemptNumber,
219
+ error: stringifyError(error),
220
+ });
221
+ },
222
+
223
+ // Cancel retry if aborted
224
+ signal: this.#abortController.signal,
225
+ });
226
+ } catch (err) {
227
+ if ((err as Error).name === "AbortError") {
228
+ // Ignore abortions
229
+ logger().info("connection retry aborted");
230
+ return;
231
+ } else {
232
+ // Unknown error
233
+ throw err;
234
+ }
235
+ }
236
+
237
+ this.#connecting = false;
238
+ }
239
+
240
+ async #connectAndWait() {
241
+ try {
242
+ // Create promise for open
243
+ if (this.#onOpenPromise)
244
+ throw new Error("#onOpenPromise already defined");
245
+ this.#onOpenPromise = Promise.withResolvers();
246
+
247
+ // Connect transport
248
+ if (this.#client[TRANSPORT_SYMBOL] === "websocket") {
249
+ await this.#connectWebSocket();
250
+ } else if (this.#client[TRANSPORT_SYMBOL] === "sse") {
251
+ await this.#connectSse();
252
+ } else {
253
+ assertUnreachable(this.#client[TRANSPORT_SYMBOL]);
254
+ }
255
+
256
+ // Wait for result
257
+ await this.#onOpenPromise.promise;
258
+ } finally {
259
+ this.#onOpenPromise = undefined;
260
+ }
261
+ }
262
+
263
+ async #connectWebSocket({ signal }: { signal?: AbortSignal } = {}) {
264
+ const ws = await this.#driver.connectWebSocket(
265
+ undefined,
266
+ this.#actorQuery,
267
+ this.#encodingKind,
268
+ this.#params,
269
+ signal ? { signal } : undefined,
270
+ );
271
+ this.#transport = { websocket: ws };
272
+ ws.addEventListener("open", () => {
273
+ logger().debug("websocket open");
274
+ });
275
+ ws.addEventListener("message", async (ev) => {
276
+ this.#handleOnMessage(ev.data);
277
+ });
278
+ ws.addEventListener("close", (ev) => {
279
+ this.#handleOnClose(ev);
280
+ });
281
+ ws.addEventListener("error", (_ev) => {
282
+ this.#handleOnError();
283
+ });
284
+ }
285
+
286
+ async #connectSse({ signal }: { signal?: AbortSignal } = {}) {
287
+ const eventSource = await this.#driver.connectSse(
288
+ undefined,
289
+ this.#actorQuery,
290
+ this.#encodingKind,
291
+ this.#params,
292
+ signal ? { signal } : undefined,
293
+ );
294
+ this.#transport = { sse: eventSource };
295
+ eventSource.onopen = () => {
296
+ logger().debug("eventsource open");
297
+ // #handleOnOpen is called on "i" event
298
+ };
299
+ eventSource.onmessage = (ev: UniversalMessageEvent) => {
300
+ this.#handleOnMessage(ev.data);
301
+ };
302
+ eventSource.onerror = (_ev: UniversalErrorEvent) => {
303
+ if (eventSource.readyState === eventSource.CLOSED) {
304
+ // This error indicates a close event
305
+ this.#handleOnClose(new Event("error"));
306
+ } else {
307
+ // Log error since event source is still open
308
+ this.#handleOnError();
309
+ }
310
+ };
311
+ }
312
+
313
+ /** Called by the onopen event from drivers. */
314
+ #handleOnOpen() {
315
+ logger().debug("socket open", {
316
+ messageQueueLength: this.#messageQueue.length,
317
+ });
318
+
319
+ // Resolve open promise
320
+ if (this.#onOpenPromise) {
321
+ this.#onOpenPromise.resolve(undefined);
322
+ } else {
323
+ logger().warn("#onOpenPromise is undefined");
324
+ }
325
+
326
+ // Resubscribe to all active events
327
+ for (const eventName of this.#eventSubscriptions.keys()) {
328
+ this.#sendSubscription(eventName, true);
329
+ }
330
+
331
+ // Flush queue
332
+ //
333
+ // If the message fails to send, the message will be re-queued
334
+ const queue = this.#messageQueue;
335
+ this.#messageQueue = [];
336
+ for (const msg of queue) {
337
+ this.#sendMessage(msg);
338
+ }
339
+ }
340
+
341
+ /** Called by the onmessage event from drivers. */
342
+ async #handleOnMessage(data: any) {
343
+ logger().trace("received message", {
344
+ dataType: typeof data,
345
+ isBlob: data instanceof Blob,
346
+ isArrayBuffer: data instanceof ArrayBuffer,
347
+ });
348
+
349
+ const response = await this.#parseMessage(data as ConnMessage);
350
+ logger().trace(
351
+ "parsed message",
352
+ getEnvUniversal("_RIVETKIT_LOG_MESSAGE")
353
+ ? {
354
+ message: jsonStringifyCompat(response).substring(0, 100) + "...",
355
+ }
356
+ : {},
357
+ );
358
+
359
+ if (response.body.tag === "Init") {
360
+ // This is only called for SSE
361
+ this.#actorId = response.body.val.actorId;
362
+ this.#connectionId = response.body.val.connectionId;
363
+ this.#connectionToken = response.body.val.connectionToken;
364
+ logger().trace("received init message", {
365
+ actorId: this.#actorId,
366
+ connectionId: this.#connectionId,
367
+ });
368
+ this.#handleOnOpen();
369
+ } else if (response.body.tag === "Error") {
370
+ // Connection error
371
+ const { code, message, metadata, actionId } = response.body.val;
372
+
373
+ if (actionId) {
374
+ const inFlight = this.#takeActionInFlight(Number(actionId));
375
+
376
+ logger().warn("action error", {
377
+ actionId: actionId,
378
+ actionName: inFlight?.name,
379
+ code,
380
+ message,
381
+ metadata,
382
+ });
383
+
384
+ inFlight.reject(new errors.ActorError(code, message, metadata));
385
+ } else {
386
+ logger().warn("connection error", {
387
+ code,
388
+ message,
389
+ metadata,
390
+ });
391
+
392
+ // Create a connection error
393
+ const actorError = new errors.ActorError(code, message, metadata);
394
+
395
+ // If we have an onOpenPromise, reject it with the error
396
+ if (this.#onOpenPromise) {
397
+ this.#onOpenPromise.reject(actorError);
398
+ }
399
+
400
+ // Reject any in-flight requests
401
+ for (const [id, inFlight] of this.#actionsInFlight.entries()) {
402
+ inFlight.reject(actorError);
403
+ this.#actionsInFlight.delete(id);
404
+ }
405
+
406
+ // Dispatch to error handler if registered
407
+ this.#dispatchActorError(actorError);
408
+ }
409
+ } else if (response.body.tag === "ActionResponse") {
410
+ // Action response OK
411
+ const { id: actionId } = response.body.val;
412
+ logger().trace("received action response", {
413
+ actionId,
414
+ });
415
+
416
+ const inFlight = this.#takeActionInFlight(Number(actionId));
417
+ logger().trace("resolving action promise", {
418
+ actionId,
419
+ actionName: inFlight?.name,
420
+ });
421
+ inFlight.resolve(response.body.val);
422
+ } else if (response.body.tag === "Event") {
423
+ logger().trace("received event", { name: response.body.val.name });
424
+ this.#dispatchEvent(response.body.val);
425
+ } else {
426
+ assertUnreachable(response.body);
427
+ }
428
+ }
429
+
430
+ /** Called by the onclose event from drivers. */
431
+ #handleOnClose(event: Event | CloseEvent) {
432
+ // TODO: Handle queue
433
+ // TODO: Reconnect with backoff
434
+
435
+ // Reject open promise
436
+ if (this.#onOpenPromise) {
437
+ this.#onOpenPromise.reject(new Error("Closed"));
438
+ }
439
+
440
+ // We can't use `event instanceof CloseEvent` because it's not defined in NodeJS
441
+ //
442
+ // These properties will be undefined
443
+ const closeEvent = event as CloseEvent;
444
+ if (closeEvent.wasClean) {
445
+ logger().info("socket closed", {
446
+ code: closeEvent.code,
447
+ reason: closeEvent.reason,
448
+ wasClean: closeEvent.wasClean,
449
+ });
450
+ } else {
451
+ logger().warn("socket closed", {
452
+ code: closeEvent.code,
453
+ reason: closeEvent.reason,
454
+ wasClean: closeEvent.wasClean,
455
+ });
456
+ }
457
+
458
+ this.#transport = undefined;
459
+
460
+ // Automatically reconnect. Skip if already attempting to connect.
461
+ if (!this.#disposed && !this.#connecting) {
462
+ // TODO: Fetch actor to check if it's destroyed
463
+ // TODO: Add backoff for reconnect
464
+ // TODO: Add a way of preserving connection ID for connection state
465
+
466
+ // Attempt to connect again
467
+ this.#connectWithRetry();
468
+ }
469
+ }
470
+
471
+ /** Called by the onerror event from drivers. */
472
+ #handleOnError() {
473
+ if (this.#disposed) return;
474
+
475
+ // More detailed information will be logged in onclose
476
+ logger().warn("socket error");
477
+ }
478
+
479
+ #takeActionInFlight(id: number): ActionInFlight {
480
+ const inFlight = this.#actionsInFlight.get(id);
481
+ if (!inFlight) {
482
+ throw new errors.InternalError(`No in flight response for ${id}`);
483
+ }
484
+ this.#actionsInFlight.delete(id);
485
+ return inFlight;
486
+ }
487
+
488
+ #dispatchEvent(event: protocol.Event) {
489
+ const { name, args: argsRaw } = event;
490
+ const args = cbor.decode(new Uint8Array(argsRaw));
491
+
492
+ const listeners = this.#eventSubscriptions.get(name);
493
+ if (!listeners) return;
494
+
495
+ // Create a new array to avoid issues with listeners being removed during iteration
496
+ for (const listener of [...listeners]) {
497
+ listener.callback(...args);
498
+
499
+ // Remove if this was a one-time listener
500
+ if (listener.once) {
501
+ listeners.delete(listener);
502
+ }
503
+ }
504
+
505
+ // Clean up empty listener sets
506
+ if (listeners.size === 0) {
507
+ this.#eventSubscriptions.delete(name);
508
+ }
509
+ }
510
+
511
+ #dispatchActorError(error: errors.ActorError) {
512
+ // Call all registered error handlers
513
+ for (const handler of [...this.#errorHandlers]) {
514
+ try {
515
+ handler(error);
516
+ } catch (err) {
517
+ logger().error("Error in connection error handler", {
518
+ error: stringifyError(err),
519
+ });
520
+ }
521
+ }
522
+ }
523
+
524
+ #addEventSubscription<Args extends Array<unknown>>(
525
+ eventName: string,
526
+ callback: (...args: Args) => void,
527
+ once: boolean,
528
+ ): EventUnsubscribe {
529
+ const listener: EventSubscriptions<Args> = {
530
+ callback,
531
+ once,
532
+ };
533
+
534
+ let subscriptionSet = this.#eventSubscriptions.get(eventName);
535
+ if (subscriptionSet === undefined) {
536
+ subscriptionSet = new Set();
537
+ this.#eventSubscriptions.set(eventName, subscriptionSet);
538
+ this.#sendSubscription(eventName, true);
539
+ }
540
+ subscriptionSet.add(listener);
541
+
542
+ // Return unsubscribe function
543
+ return () => {
544
+ const listeners = this.#eventSubscriptions.get(eventName);
545
+ if (listeners) {
546
+ listeners.delete(listener);
547
+ if (listeners.size === 0) {
548
+ this.#eventSubscriptions.delete(eventName);
549
+ this.#sendSubscription(eventName, false);
550
+ }
551
+ }
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Subscribes to an event that will happen repeatedly.
557
+ *
558
+ * @template Args - The type of arguments the event callback will receive.
559
+ * @param {string} eventName - The name of the event to subscribe to.
560
+ * @param {(...args: Args) => void} callback - The callback function to execute when the event is triggered.
561
+ * @returns {EventUnsubscribe} - A function to unsubscribe from the event.
562
+ * @see {@link https://rivet.gg/docs/events|Events Documentation}
563
+ */
564
+ on<Args extends Array<unknown> = unknown[]>(
565
+ eventName: string,
566
+ callback: (...args: Args) => void,
567
+ ): EventUnsubscribe {
568
+ return this.#addEventSubscription<Args>(eventName, callback, false);
569
+ }
570
+
571
+ /**
572
+ * Subscribes to an event that will be triggered only once.
573
+ *
574
+ * @template Args - The type of arguments the event callback will receive.
575
+ * @param {string} eventName - The name of the event to subscribe to.
576
+ * @param {(...args: Args) => void} callback - The callback function to execute when the event is triggered.
577
+ * @returns {EventUnsubscribe} - A function to unsubscribe from the event.
578
+ * @see {@link https://rivet.gg/docs/events|Events Documentation}
579
+ */
580
+ once<Args extends Array<unknown> = unknown[]>(
581
+ eventName: string,
582
+ callback: (...args: Args) => void,
583
+ ): EventUnsubscribe {
584
+ return this.#addEventSubscription<Args>(eventName, callback, true);
585
+ }
586
+
587
+ /**
588
+ * Subscribes to connection errors.
589
+ *
590
+ * @param {ActorErrorCallback} callback - The callback function to execute when a connection error occurs.
591
+ * @returns {() => void} - A function to unsubscribe from the error handler.
592
+ */
593
+ onError(callback: ActorErrorCallback): () => void {
594
+ this.#errorHandlers.add(callback);
595
+
596
+ // Return unsubscribe function
597
+ return () => {
598
+ this.#errorHandlers.delete(callback);
599
+ };
600
+ }
601
+
602
+ #sendMessage(message: protocol.ToServer, opts?: SendHttpMessageOpts) {
603
+ if (this.#disposed) {
604
+ throw new errors.ActorConnDisposed();
605
+ }
606
+
607
+ let queueMessage = false;
608
+ if (!this.#transport) {
609
+ // No transport connected yet
610
+ queueMessage = true;
611
+ } else if ("websocket" in this.#transport) {
612
+ if (this.#transport.websocket.readyState === 1) {
613
+ try {
614
+ const messageSerialized = serializeWithEncoding(
615
+ this.#encodingKind,
616
+ message,
617
+ TO_SERVER_VERSIONED,
618
+ );
619
+ this.#transport.websocket.send(messageSerialized);
620
+ logger().trace("sent websocket message", {
621
+ len: messageLength(messageSerialized),
622
+ });
623
+ } catch (error) {
624
+ logger().warn("failed to send message, added to queue", {
625
+ error,
626
+ });
627
+
628
+ // Assuming the socket is disconnected and will be reconnected soon
629
+ queueMessage = true;
630
+ }
631
+ } else {
632
+ queueMessage = true;
633
+ }
634
+ } else if ("sse" in this.#transport) {
635
+ if (this.#transport.sse.readyState === 1) {
636
+ // Spawn in background since #sendMessage cannot be async
637
+ this.#sendHttpMessage(message, opts);
638
+ } else {
639
+ queueMessage = true;
640
+ }
641
+ } else {
642
+ assertUnreachable(this.#transport);
643
+ }
644
+
645
+ if (!opts?.ephemeral && queueMessage) {
646
+ this.#messageQueue.push(message);
647
+ logger().debug("queued connection message");
648
+ }
649
+ }
650
+
651
+ async #sendHttpMessage(
652
+ message: protocol.ToServer,
653
+ opts?: SendHttpMessageOpts,
654
+ ) {
655
+ try {
656
+ if (!this.#actorId || !this.#connectionId || !this.#connectionToken)
657
+ throw new errors.InternalError("Missing connection ID or token.");
658
+
659
+ logger().trace(
660
+ "sent http message",
661
+ getEnvUniversal("_RIVETKIT_LOG_MESSAGE")
662
+ ? {
663
+ message: jsonStringifyCompat(message).substring(0, 100) + "...",
664
+ }
665
+ : {},
666
+ );
667
+
668
+ await this.#driver.sendHttpMessage(
669
+ undefined,
670
+ this.#actorId,
671
+ this.#encodingKind,
672
+ this.#connectionId,
673
+ this.#connectionToken,
674
+ message,
675
+ opts?.signal ? { signal: opts.signal } : undefined,
676
+ );
677
+ } catch (error) {
678
+ // TODO: This will not automatically trigger a re-broadcast of HTTP events since SSE is separate from the HTTP action
679
+
680
+ logger().warn("failed to send message, added to queue", {
681
+ error,
682
+ });
683
+
684
+ // Assuming the socket is disconnected and will be reconnected soon
685
+ //
686
+ // Will attempt to resend soon
687
+ if (!opts?.ephemeral) {
688
+ this.#messageQueue.unshift(message);
689
+ }
690
+ }
691
+ }
692
+
693
+ async #parseMessage(data: ConnMessage): Promise<protocol.ToClient> {
694
+ invariant(this.#transport, "transport must be defined");
695
+
696
+ // Decode base64 since SSE sends raw strings
697
+ if (encodingIsBinary(this.#encodingKind) && "sse" in this.#transport) {
698
+ if (typeof data === "string") {
699
+ const binaryString = atob(data);
700
+ data = new Uint8Array(
701
+ [...binaryString].map((char) => char.charCodeAt(0)),
702
+ );
703
+ } else {
704
+ throw new errors.InternalError(
705
+ `Expected data to be a string for SSE, got ${data}.`,
706
+ );
707
+ }
708
+ }
709
+
710
+ const buffer = await inputDataToBuffer(data);
711
+
712
+ return deserializeWithEncoding(
713
+ this.#encodingKind,
714
+ buffer,
715
+ TO_CLIENT_VERSIONED,
716
+ );
717
+ }
718
+
719
+ /**
720
+ * Disconnects from the actor.
721
+ *
722
+ * @returns {Promise<void>} A promise that resolves when the socket is gracefully closed.
723
+ */
724
+ async dispose(): Promise<void> {
725
+ // Internally, this "disposes" the connection
726
+
727
+ if (this.#disposed) {
728
+ logger().warn("connection already disconnected");
729
+ return;
730
+ }
731
+ this.#disposed = true;
732
+
733
+ logger().debug("disposing actor conn");
734
+
735
+ // Clear interval so NodeJS process can exit
736
+ clearInterval(this.#keepNodeAliveInterval);
737
+
738
+ // Abort
739
+ this.#abortController.abort();
740
+
741
+ // Remove from registry
742
+ this.#client[ACTOR_CONNS_SYMBOL].delete(this);
743
+
744
+ // Disconnect transport cleanly
745
+ if (!this.#transport) {
746
+ // Nothing to do
747
+ } else if ("websocket" in this.#transport) {
748
+ const ws = this.#transport.websocket;
749
+ // Check if WebSocket is already closed or closing
750
+ if (
751
+ ws.readyState === 2 /* CLOSING */ ||
752
+ ws.readyState === 3 /* CLOSED */
753
+ ) {
754
+ logger().debug("ws already closed or closing");
755
+ } else {
756
+ const { promise, resolve } = Promise.withResolvers();
757
+ ws.addEventListener("close", () => {
758
+ logger().debug("ws closed");
759
+ resolve(undefined);
760
+ });
761
+ ws.close();
762
+ await promise;
763
+ }
764
+ } else if ("sse" in this.#transport) {
765
+ this.#transport.sse.close();
766
+ } else {
767
+ assertUnreachable(this.#transport);
768
+ }
769
+ this.#transport = undefined;
770
+ }
771
+
772
+ #sendSubscription(eventName: string, subscribe: boolean) {
773
+ this.#sendMessage(
774
+ {
775
+ body: {
776
+ tag: "SubscriptionRequest",
777
+ val: {
778
+ eventName,
779
+ subscribe,
780
+ },
781
+ },
782
+ },
783
+ { ephemeral: true },
784
+ );
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Connection to a actor. Allows calling actor's remote procedure calls with inferred types. See {@link ActorConnRaw} for underlying methods.
790
+ *
791
+ * @example
792
+ * ```
793
+ * const room = client.connect<ChatRoom>(...etc...);
794
+ * // This calls the action named `sendMessage` on the `ChatRoom` actor.
795
+ * await room.sendMessage('Hello, world!');
796
+ * ```
797
+ *
798
+ * Private methods (e.g. those starting with `_`) are automatically excluded.
799
+ *
800
+ * @template AD The actor class that this connection is for.
801
+ * @see {@link ActorConnRaw}
802
+ */
803
+ export type ActorConn<AD extends AnyActorDefinition> = ActorConnRaw &
804
+ ActorDefinitionActions<AD>;