@rivetkit/cloudflare-workers 0.9.0 → 0.9.2
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/dist/mod.cjs +240 -156
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +19 -58
- package/dist/mod.d.ts +19 -58
- package/dist/mod.js +243 -159
- package/dist/mod.js.map +1 -1
- package/package.json +6 -5
- package/src/actor-driver.ts +122 -3
- package/src/actor-handler-do.ts +73 -43
- package/src/config.ts +1 -1
- package/src/handler.ts +16 -18
- package/src/manager-driver.ts +160 -153
- package/src/mod.ts +1 -1
- package/src/util.ts +73 -74
- package/src/websocket.ts +1 -1
package/src/manager-driver.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
+
import type { Encoding } from "@rivetkit/core";
|
|
1
2
|
import {
|
|
2
|
-
type ManagerDriver,
|
|
3
|
-
type GetForIdInput,
|
|
4
|
-
type GetWithKeyInput,
|
|
5
3
|
type ActorOutput,
|
|
6
4
|
type CreateInput,
|
|
5
|
+
type GetForIdInput,
|
|
7
6
|
type GetOrCreateWithKeyInput,
|
|
8
|
-
type
|
|
9
|
-
HEADER_EXPOSE_INTERNAL_ERROR,
|
|
10
|
-
HEADER_ENCODING,
|
|
11
|
-
HEADER_CONN_PARAMS,
|
|
7
|
+
type GetWithKeyInput,
|
|
12
8
|
HEADER_AUTH_DATA,
|
|
9
|
+
HEADER_CONN_PARAMS,
|
|
10
|
+
HEADER_ENCODING,
|
|
11
|
+
HEADER_EXPOSE_INTERNAL_ERROR,
|
|
12
|
+
type ManagerDriver,
|
|
13
13
|
} from "@rivetkit/core/driver-helpers";
|
|
14
14
|
import { ActorAlreadyExists, InternalError } from "@rivetkit/core/errors";
|
|
15
|
-
import {
|
|
16
|
-
import { logger } from "./log";
|
|
17
|
-
import { serializeNameAndKey, serializeKey } from "./util";
|
|
15
|
+
import type { Context as HonoContext } from "hono";
|
|
18
16
|
import { getCloudflareAmbientEnv } from "./handler";
|
|
19
|
-
import {
|
|
17
|
+
import { logger } from "./log";
|
|
18
|
+
import type { Bindings } from "./mod";
|
|
19
|
+
import { serializeKey, serializeNameAndKey } from "./util";
|
|
20
20
|
|
|
21
21
|
// Actor metadata structure
|
|
22
22
|
interface ActorData {
|
|
@@ -48,149 +48,156 @@ const STANDARD_WEBSOCKET_HEADERS = [
|
|
|
48
48
|
];
|
|
49
49
|
|
|
50
50
|
export class CloudflareActorsManagerDriver implements ManagerDriver {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Upgrade: "websocket",
|
|
86
|
-
Connection: "Upgrade",
|
|
87
|
-
[HEADER_EXPOSE_INTERNAL_ERROR]: "true",
|
|
88
|
-
[HEADER_ENCODING]: encodingKind,
|
|
89
|
-
};
|
|
90
|
-
if (params) {
|
|
91
|
-
headers[HEADER_CONN_PARAMS] = JSON.stringify(params);
|
|
92
|
-
}
|
|
93
|
-
// HACK: See packages/platforms/cloudflare-workers/src/websocket.ts
|
|
94
|
-
headers["sec-websocket-protocol"] = "rivetkit";
|
|
95
|
-
|
|
96
|
-
const response = await stub.fetch("http://actor/connect/websocket", {
|
|
97
|
-
headers,
|
|
98
|
-
});
|
|
99
|
-
const webSocket = response.webSocket;
|
|
100
|
-
|
|
101
|
-
if (!webSocket) {
|
|
102
|
-
throw new InternalError(
|
|
103
|
-
"missing websocket connection in response from DO",
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
logger().debug("durable object websocket connection open", {
|
|
108
|
-
actorId,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
webSocket.accept();
|
|
112
|
-
|
|
113
|
-
// TODO: Is this still needed?
|
|
114
|
-
// HACK: Cloudflare does not call onopen automatically, so we need
|
|
115
|
-
// to call this on the next tick
|
|
116
|
-
setTimeout(() => {
|
|
117
|
-
(webSocket as any).onopen?.(new Event("open"));
|
|
118
|
-
}, 0);
|
|
119
|
-
|
|
120
|
-
return webSocket as unknown as WebSocket;
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
proxyRequest: async (c, actorRequest, actorId): Promise<Response> => {
|
|
124
|
-
logger().debug("forwarding request to durable object", {
|
|
125
|
-
actorId,
|
|
126
|
-
method: actorRequest.method,
|
|
127
|
-
url: actorRequest.url,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const id = c.env.ACTOR_DO.idFromString(actorId);
|
|
131
|
-
const stub = c.env.ACTOR_DO.get(id);
|
|
132
|
-
|
|
133
|
-
return await stub.fetch(actorRequest);
|
|
134
|
-
},
|
|
135
|
-
proxyWebSocket: async (
|
|
136
|
-
c,
|
|
137
|
-
path,
|
|
138
|
-
actorId,
|
|
139
|
-
encoding,
|
|
140
|
-
params,
|
|
141
|
-
authData,
|
|
142
|
-
) => {
|
|
143
|
-
logger().debug("forwarding websocket to durable object", {
|
|
144
|
-
actorId,
|
|
145
|
-
path,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Validate upgrade
|
|
149
|
-
const upgradeHeader = c.req.header("Upgrade");
|
|
150
|
-
if (!upgradeHeader || upgradeHeader !== "websocket") {
|
|
151
|
-
return new Response("Expected Upgrade: websocket", {
|
|
152
|
-
status: 426,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// TODO: strip headers
|
|
157
|
-
const newUrl = new URL(`http://actor${path}`);
|
|
158
|
-
const actorRequest = new Request(newUrl, c.req.raw);
|
|
159
|
-
|
|
160
|
-
// Always build fresh request to prevent forwarding unwanted headers
|
|
161
|
-
// HACK: Since we can't build a new request, we need to remove
|
|
162
|
-
// non-standard headers manually
|
|
163
|
-
const headerKeys: string[] = [];
|
|
164
|
-
actorRequest.headers.forEach((v, k) => headerKeys.push(k));
|
|
165
|
-
for (const k of headerKeys) {
|
|
166
|
-
if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) {
|
|
167
|
-
actorRequest.headers.delete(k);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Add RivetKit headers
|
|
172
|
-
actorRequest.headers.set(HEADER_EXPOSE_INTERNAL_ERROR, "true");
|
|
173
|
-
actorRequest.headers.set(HEADER_ENCODING, encoding);
|
|
174
|
-
if (params) {
|
|
175
|
-
actorRequest.headers.set(
|
|
176
|
-
HEADER_CONN_PARAMS,
|
|
177
|
-
JSON.stringify(params),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
if (authData) {
|
|
181
|
-
actorRequest.headers.set(
|
|
182
|
-
HEADER_AUTH_DATA,
|
|
183
|
-
JSON.stringify(authData),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const id = c.env.ACTOR_DO.idFromString(actorId);
|
|
188
|
-
const stub = c.env.ACTOR_DO.get(id);
|
|
189
|
-
|
|
190
|
-
return await stub.fetch(actorRequest);
|
|
191
|
-
},
|
|
192
|
-
},
|
|
51
|
+
async sendRequest(actorId: string, actorRequest: Request): Promise<Response> {
|
|
52
|
+
const env = getCloudflareAmbientEnv();
|
|
53
|
+
|
|
54
|
+
logger().debug("sending request to durable object", {
|
|
55
|
+
actorId,
|
|
56
|
+
method: actorRequest.method,
|
|
57
|
+
url: actorRequest.url,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const id = env.ACTOR_DO.idFromString(actorId);
|
|
61
|
+
const stub = env.ACTOR_DO.get(id);
|
|
62
|
+
|
|
63
|
+
return await stub.fetch(actorRequest);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async openWebSocket(
|
|
67
|
+
path: string,
|
|
68
|
+
actorId: string,
|
|
69
|
+
encoding: Encoding,
|
|
70
|
+
params: unknown,
|
|
71
|
+
): Promise<WebSocket> {
|
|
72
|
+
const env = getCloudflareAmbientEnv();
|
|
73
|
+
|
|
74
|
+
logger().debug("opening websocket to durable object", { actorId, path });
|
|
75
|
+
|
|
76
|
+
// Make a fetch request to the Durable Object with WebSocket upgrade
|
|
77
|
+
const id = env.ACTOR_DO.idFromString(actorId);
|
|
78
|
+
const stub = env.ACTOR_DO.get(id);
|
|
79
|
+
|
|
80
|
+
const headers: Record<string, string> = {
|
|
81
|
+
Upgrade: "websocket",
|
|
82
|
+
Connection: "Upgrade",
|
|
83
|
+
[HEADER_EXPOSE_INTERNAL_ERROR]: "true",
|
|
84
|
+
[HEADER_ENCODING]: encoding,
|
|
193
85
|
};
|
|
86
|
+
if (params) {
|
|
87
|
+
headers[HEADER_CONN_PARAMS] = JSON.stringify(params);
|
|
88
|
+
}
|
|
89
|
+
// HACK: See packages/drivers/cloudflare-workers/src/websocket.ts
|
|
90
|
+
headers["sec-websocket-protocol"] = "rivetkit";
|
|
91
|
+
|
|
92
|
+
// Use the path parameter to determine the URL
|
|
93
|
+
const url = `http://actor${path}`;
|
|
94
|
+
|
|
95
|
+
logger().debug("rewriting websocket url", {
|
|
96
|
+
from: path,
|
|
97
|
+
to: url,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const response = await stub.fetch(url, {
|
|
101
|
+
headers,
|
|
102
|
+
});
|
|
103
|
+
const webSocket = response.webSocket;
|
|
104
|
+
|
|
105
|
+
if (!webSocket) {
|
|
106
|
+
throw new InternalError(
|
|
107
|
+
"missing websocket connection in response from DO",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
logger().debug("durable object websocket connection open", {
|
|
112
|
+
actorId,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
webSocket.accept();
|
|
116
|
+
|
|
117
|
+
// TODO: Is this still needed?
|
|
118
|
+
// HACK: Cloudflare does not call onopen automatically, so we need
|
|
119
|
+
// to call this on the next tick
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
const event = new Event("open");
|
|
122
|
+
(webSocket as any).onopen?.(event);
|
|
123
|
+
(webSocket as any).dispatchEvent(event);
|
|
124
|
+
}, 0);
|
|
125
|
+
|
|
126
|
+
return webSocket as unknown as WebSocket;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async proxyRequest(
|
|
130
|
+
c: HonoContext<{ Bindings: Bindings }>,
|
|
131
|
+
actorRequest: Request,
|
|
132
|
+
actorId: string,
|
|
133
|
+
): Promise<Response> {
|
|
134
|
+
logger().debug("forwarding request to durable object", {
|
|
135
|
+
actorId,
|
|
136
|
+
method: actorRequest.method,
|
|
137
|
+
url: actorRequest.url,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const id = c.env.ACTOR_DO.idFromString(actorId);
|
|
141
|
+
const stub = c.env.ACTOR_DO.get(id);
|
|
142
|
+
|
|
143
|
+
return await stub.fetch(actorRequest);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async proxyWebSocket(
|
|
147
|
+
c: HonoContext<{ Bindings: Bindings }>,
|
|
148
|
+
path: string,
|
|
149
|
+
actorId: string,
|
|
150
|
+
encoding: Encoding,
|
|
151
|
+
params: unknown,
|
|
152
|
+
authData: unknown,
|
|
153
|
+
): Promise<Response> {
|
|
154
|
+
logger().debug("forwarding websocket to durable object", {
|
|
155
|
+
actorId,
|
|
156
|
+
path,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Validate upgrade
|
|
160
|
+
const upgradeHeader = c.req.header("Upgrade");
|
|
161
|
+
if (!upgradeHeader || upgradeHeader !== "websocket") {
|
|
162
|
+
return new Response("Expected Upgrade: websocket", {
|
|
163
|
+
status: 426,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// TODO: strip headers
|
|
168
|
+
const newUrl = new URL(`http://actor${path}`);
|
|
169
|
+
const actorRequest = new Request(newUrl, c.req.raw);
|
|
170
|
+
|
|
171
|
+
logger().debug("rewriting websocket url", {
|
|
172
|
+
from: c.req.url,
|
|
173
|
+
to: actorRequest.url,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Always build fresh request to prevent forwarding unwanted headers
|
|
177
|
+
// HACK: Since we can't build a new request, we need to remove
|
|
178
|
+
// non-standard headers manually
|
|
179
|
+
const headerKeys: string[] = [];
|
|
180
|
+
actorRequest.headers.forEach((v, k) => headerKeys.push(k));
|
|
181
|
+
for (const k of headerKeys) {
|
|
182
|
+
if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) {
|
|
183
|
+
actorRequest.headers.delete(k);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Add RivetKit headers
|
|
188
|
+
actorRequest.headers.set(HEADER_EXPOSE_INTERNAL_ERROR, "true");
|
|
189
|
+
actorRequest.headers.set(HEADER_ENCODING, encoding);
|
|
190
|
+
if (params) {
|
|
191
|
+
actorRequest.headers.set(HEADER_CONN_PARAMS, JSON.stringify(params));
|
|
192
|
+
}
|
|
193
|
+
if (authData) {
|
|
194
|
+
actorRequest.headers.set(HEADER_AUTH_DATA, JSON.stringify(authData));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const id = c.env.ACTOR_DO.idFromString(actorId);
|
|
198
|
+
const stub = c.env.ACTOR_DO.get(id);
|
|
199
|
+
|
|
200
|
+
return await stub.fetch(actorRequest);
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
async getForId({
|
package/src/mod.ts
CHANGED
package/src/util.ts
CHANGED
|
@@ -4,102 +4,101 @@ export const KEY_SEPARATOR = ",";
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Serializes an array of key strings into a single string for use with idFromName
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param name The actor name
|
|
9
9
|
* @param key Array of key strings to serialize
|
|
10
10
|
* @returns A single string containing the serialized name and key
|
|
11
11
|
*/
|
|
12
12
|
export function serializeNameAndKey(name: string, key: string[]): string {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Escape colons in the name
|
|
14
|
+
const escapedName = name.replace(/:/g, "\\:");
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
// For empty keys, just return the name and a marker
|
|
17
|
+
if (key.length === 0) {
|
|
18
|
+
return `${escapedName}:${EMPTY_KEY}`;
|
|
19
|
+
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Serialize the key array
|
|
22
|
+
const serializedKey = serializeKey(key);
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Combine name and serialized key
|
|
25
|
+
return `${escapedName}:${serializedKey}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Serializes an array of key strings into a single string
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
31
|
* @param key Array of key strings to serialize
|
|
32
32
|
* @returns A single string containing the serialized key
|
|
33
33
|
*/
|
|
34
34
|
export function serializeKey(key: string[]): string {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Deserializes a key string back into an array of key strings
|
|
58
|
-
*
|
|
58
|
+
*
|
|
59
59
|
* @param keyString The serialized key string
|
|
60
60
|
* @returns Array of key strings
|
|
61
61
|
*/
|
|
62
62
|
export function deserializeKey(keyString: string): string[] {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
104
|
}
|
|
105
|
-
|
package/src/websocket.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This version calls the open event by default
|
|
4
4
|
|
|
5
|
-
import { WSContext, defineWebSocketHelper } from "hono/ws";
|
|
6
5
|
import type { UpgradeWebSocket, WSEvents, WSReadyState } from "hono/ws";
|
|
6
|
+
import { defineWebSocketHelper, WSContext } from "hono/ws";
|
|
7
7
|
|
|
8
8
|
// Based on https://github.com/honojs/hono/issues/1153#issuecomment-1767321332
|
|
9
9
|
export const upgradeWebSocket: UpgradeWebSocket<
|