@rivetkit/cloudflare-workers 0.9.0

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/src/util.ts ADDED
@@ -0,0 +1,105 @@
1
+ // Constants for key handling
2
+ export const EMPTY_KEY = "(none)";
3
+ export const KEY_SEPARATOR = ",";
4
+
5
+ /**
6
+ * Serializes an array of key strings into a single string for use with idFromName
7
+ *
8
+ * @param name The actor name
9
+ * @param key Array of key strings to serialize
10
+ * @returns A single string containing the serialized name and key
11
+ */
12
+ export function serializeNameAndKey(name: string, key: string[]): string {
13
+ // Escape colons in the name
14
+ const escapedName = name.replace(/:/g, "\\:");
15
+
16
+ // For empty keys, just return the name and a marker
17
+ if (key.length === 0) {
18
+ return `${escapedName}:${EMPTY_KEY}`;
19
+ }
20
+
21
+ // Serialize the key array
22
+ const serializedKey = serializeKey(key);
23
+
24
+ // Combine name and serialized key
25
+ return `${escapedName}:${serializedKey}`;
26
+ }
27
+
28
+ /**
29
+ * Serializes an array of key strings into a single string
30
+ *
31
+ * @param key Array of key strings to serialize
32
+ * @returns A single string containing the serialized key
33
+ */
34
+ export function serializeKey(key: string[]): string {
35
+ // Use a special marker for empty key arrays
36
+ if (key.length === 0) {
37
+ return EMPTY_KEY;
38
+ }
39
+
40
+ // Escape each key part to handle the separator and the empty key marker
41
+ const escapedParts = key.map(part => {
42
+ // First check if it matches our empty key marker
43
+ if (part === EMPTY_KEY) {
44
+ return `\\${EMPTY_KEY}`;
45
+ }
46
+
47
+ // Escape backslashes first, then commas
48
+ let escaped = part.replace(/\\/g, "\\\\");
49
+ escaped = escaped.replace(/,/g, "\\,");
50
+ return escaped;
51
+ });
52
+
53
+ return escapedParts.join(KEY_SEPARATOR);
54
+ }
55
+
56
+ /**
57
+ * Deserializes a key string back into an array of key strings
58
+ *
59
+ * @param keyString The serialized key string
60
+ * @returns Array of key strings
61
+ */
62
+ export function deserializeKey(keyString: string): string[] {
63
+ // Handle empty values
64
+ if (!keyString) {
65
+ return [];
66
+ }
67
+
68
+ // Check for special empty key marker
69
+ if (keyString === EMPTY_KEY) {
70
+ return [];
71
+ }
72
+
73
+ // Split by unescaped commas and unescape the escaped characters
74
+ const parts: string[] = [];
75
+ let currentPart = '';
76
+ let escaping = false;
77
+
78
+ for (let i = 0; i < keyString.length; i++) {
79
+ const char = keyString[i];
80
+
81
+ if (escaping) {
82
+ // This is an escaped character, add it directly
83
+ currentPart += char;
84
+ escaping = false;
85
+ } else if (char === '\\') {
86
+ // Start of an escape sequence
87
+ escaping = true;
88
+ } else if (char === KEY_SEPARATOR) {
89
+ // This is a separator
90
+ parts.push(currentPart);
91
+ currentPart = '';
92
+ } else {
93
+ // Regular character
94
+ currentPart += char;
95
+ }
96
+ }
97
+
98
+ // Add the last part if it exists
99
+ if (currentPart || parts.length > 0) {
100
+ parts.push(currentPart);
101
+ }
102
+
103
+ return parts;
104
+ }
105
+
@@ -0,0 +1,70 @@
1
+ // Modified from https://github.com/honojs/hono/blob/40ea0eee58e39b31053a0246c595434f1094ad31/src/adapter/cloudflare-workers/websocket.ts#L17
2
+ //
3
+ // This version calls the open event by default
4
+
5
+ import { WSContext, defineWebSocketHelper } from "hono/ws";
6
+ import type { UpgradeWebSocket, WSEvents, WSReadyState } from "hono/ws";
7
+
8
+ // Based on https://github.com/honojs/hono/issues/1153#issuecomment-1767321332
9
+ export const upgradeWebSocket: UpgradeWebSocket<
10
+ WebSocket,
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ any,
13
+ WSEvents<WebSocket>
14
+ > = defineWebSocketHelper(async (c, events) => {
15
+ const upgradeHeader = c.req.header("Upgrade");
16
+ if (upgradeHeader !== "websocket") {
17
+ return;
18
+ }
19
+
20
+ const webSocketPair = new WebSocketPair();
21
+ const client: WebSocket = webSocketPair[0];
22
+ const server: WebSocket = webSocketPair[1];
23
+
24
+ const wsContext = new WSContext<WebSocket>({
25
+ close: (code, reason) => server.close(code, reason),
26
+ get protocol() {
27
+ return server.protocol;
28
+ },
29
+ raw: server,
30
+ get readyState() {
31
+ return server.readyState as WSReadyState;
32
+ },
33
+ url: server.url ? new URL(server.url) : null,
34
+ send: (source) => server.send(source),
35
+ });
36
+
37
+ if (events.onClose) {
38
+ server.addEventListener("close", (evt: CloseEvent) =>
39
+ events.onClose?.(evt, wsContext),
40
+ );
41
+ }
42
+ if (events.onMessage) {
43
+ server.addEventListener("message", (evt: MessageEvent) =>
44
+ events.onMessage?.(evt, wsContext),
45
+ );
46
+ }
47
+ if (events.onError) {
48
+ server.addEventListener("error", (evt: Event) =>
49
+ events.onError?.(evt, wsContext),
50
+ );
51
+ }
52
+
53
+ server.accept?.();
54
+
55
+ // note: cloudflare actors doesn't support 'open' event, so we call it immediately with a fake event
56
+ //
57
+ // we have to do this after `server.accept() is called`
58
+ events.onOpen?.(new Event("open"), wsContext);
59
+
60
+ return new Response(null, {
61
+ status: 101,
62
+ headers: {
63
+ // HACK: Required in order for Cloudflare to not error with "Network connection lost"
64
+ //
65
+ // This bug undocumented. Cannot easily reproduce outside of RivetKit.
66
+ "Sec-WebSocket-Protocol": "rivetkit",
67
+ },
68
+ webSocket: client,
69
+ });
70
+ });