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