@rivetkit/cloudflare-workers 2.2.2-rc.1 → 2.3.0-rc.14
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 +44 -6
- package/dist/mod.d.mts +44 -0
- package/dist/mod.d.ts +37 -81
- package/dist/mod.js +123 -1064
- package/dist/mod.js.map +1 -1
- package/dist/mod.mjs +152 -0
- package/dist/mod.mjs.map +1 -0
- package/package.json +52 -56
- package/LICENSE +0 -203
- package/dist/mod.cjs +0 -1093
- package/dist/mod.cjs.map +0 -1
- package/dist/mod.d.cts +0 -88
- package/src/actor-driver.ts +0 -373
- package/src/actor-handler-do.ts +0 -440
- package/src/actor-id.ts +0 -38
- package/src/actor-kv.ts +0 -120
- package/src/config.ts +0 -22
- package/src/global-kv.ts +0 -6
- package/src/handler.ts +0 -150
- package/src/log.ts +0 -5
- package/src/manager-driver.ts +0 -444
- package/src/mod.ts +0 -11
- package/src/util.ts +0 -104
- package/src/websocket.ts +0 -81
package/src/handler.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { env } from "cloudflare:workers";
|
|
2
|
-
import type { Client, Registry } from "rivetkit";
|
|
3
|
-
import { createClientWithDriver } from "rivetkit";
|
|
4
|
-
import { buildManagerRouter } from "rivetkit/driver-helpers";
|
|
5
|
-
import {
|
|
6
|
-
type ActorHandlerInterface,
|
|
7
|
-
createActorDurableObject,
|
|
8
|
-
type DurableObjectConstructor,
|
|
9
|
-
} from "./actor-handler-do";
|
|
10
|
-
import { type Config, ConfigSchema, type InputConfig } from "./config";
|
|
11
|
-
import { CloudflareActorsManagerDriver } from "./manager-driver";
|
|
12
|
-
import { upgradeWebSocket } from "./websocket";
|
|
13
|
-
|
|
14
|
-
/** Cloudflare Workers env */
|
|
15
|
-
export interface Bindings {
|
|
16
|
-
ACTOR_KV: KVNamespace;
|
|
17
|
-
ACTOR_DO: DurableObjectNamespace<ActorHandlerInterface>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Stores the env for the current request. Required since some contexts like the inline client driver does not have access to the Hono context.
|
|
22
|
-
*
|
|
23
|
-
* Use getCloudflareAmbientEnv unless using CF_AMBIENT_ENV.run.
|
|
24
|
-
*/
|
|
25
|
-
export function getCloudflareAmbientEnv(): Bindings {
|
|
26
|
-
return env as unknown as Bindings;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface InlineOutput<A extends Registry<any>> {
|
|
30
|
-
/** Client to communicate with the actors. */
|
|
31
|
-
client: Client<A>;
|
|
32
|
-
|
|
33
|
-
/** Fetch handler to manually route requests to the Rivet manager API. */
|
|
34
|
-
fetch: (request: Request, ...args: any) => Response | Promise<Response>;
|
|
35
|
-
|
|
36
|
-
config: Config;
|
|
37
|
-
|
|
38
|
-
ActorHandler: DurableObjectConstructor;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface HandlerOutput {
|
|
42
|
-
handler: ExportedHandler<Bindings>;
|
|
43
|
-
ActorHandler: DurableObjectConstructor;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Creates an inline client for accessing Rivet Actors privately without a public manager API.
|
|
48
|
-
*
|
|
49
|
-
* If you want to expose a public manager API, either:
|
|
50
|
-
*
|
|
51
|
-
* - Use `createHandler` to expose the Rivet API on `/api/rivet`
|
|
52
|
-
* - Forward Rivet API requests to `InlineOutput::fetch`
|
|
53
|
-
*/
|
|
54
|
-
export function createInlineClient<R extends Registry<any>>(
|
|
55
|
-
registry: R,
|
|
56
|
-
inputConfig?: InputConfig,
|
|
57
|
-
): InlineOutput<R> {
|
|
58
|
-
// HACK: Cloudflare does not support using `crypto.randomUUID()` before start, so we pass a default value
|
|
59
|
-
//
|
|
60
|
-
// Runner key is not used on Cloudflare
|
|
61
|
-
inputConfig = { ...inputConfig, runnerKey: "" };
|
|
62
|
-
|
|
63
|
-
// Parse config
|
|
64
|
-
const config = ConfigSchema.parse(inputConfig);
|
|
65
|
-
|
|
66
|
-
// Create Durable Object
|
|
67
|
-
const ActorHandler = createActorDurableObject(
|
|
68
|
-
registry,
|
|
69
|
-
() => upgradeWebSocket,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Configure registry for cloudflare-workers
|
|
73
|
-
registry.config.noWelcome = true;
|
|
74
|
-
// Disable inspector since it's not supported on Cloudflare Workers
|
|
75
|
-
registry.config.inspector = {
|
|
76
|
-
enabled: false,
|
|
77
|
-
token: () => "",
|
|
78
|
-
};
|
|
79
|
-
// Set manager base path to "/" since the cloudflare handler strips the /api/rivet prefix
|
|
80
|
-
registry.config.managerBasePath = "/";
|
|
81
|
-
const parsedConfig = registry.parseConfig();
|
|
82
|
-
|
|
83
|
-
// Create manager driver
|
|
84
|
-
const managerDriver = new CloudflareActorsManagerDriver();
|
|
85
|
-
|
|
86
|
-
// Build the manager router (has actor management endpoints like /actors)
|
|
87
|
-
const { router } = buildManagerRouter(
|
|
88
|
-
parsedConfig,
|
|
89
|
-
managerDriver,
|
|
90
|
-
() => upgradeWebSocket,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Create client using the manager driver
|
|
94
|
-
// Avoid excessive generic expansion in DTS generation.
|
|
95
|
-
const client = (createClientWithDriver as any)(managerDriver) as Client<R>;
|
|
96
|
-
|
|
97
|
-
return { client, fetch: router.fetch.bind(router), config, ActorHandler };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Creates a handler to be exported from a Cloudflare Worker.
|
|
102
|
-
*
|
|
103
|
-
* This will automatically expose the Rivet manager API on `/api/rivet`.
|
|
104
|
-
*
|
|
105
|
-
* This includes a `fetch` handler and `ActorHandler` Durable Object.
|
|
106
|
-
*/
|
|
107
|
-
export function createHandler(
|
|
108
|
-
registry: Registry<any>,
|
|
109
|
-
inputConfig?: InputConfig,
|
|
110
|
-
): HandlerOutput {
|
|
111
|
-
const inline = (createInlineClient as any)(registry, inputConfig);
|
|
112
|
-
const client = inline.client as any;
|
|
113
|
-
const fetch = inline.fetch as (
|
|
114
|
-
request: Request,
|
|
115
|
-
...args: any
|
|
116
|
-
) => Response | Promise<Response>;
|
|
117
|
-
const config = inline.config as Config;
|
|
118
|
-
const ActorHandler = inline.ActorHandler as DurableObjectConstructor;
|
|
119
|
-
|
|
120
|
-
// Create Cloudflare handler
|
|
121
|
-
const handler = {
|
|
122
|
-
fetch: async (request, cfEnv, ctx) => {
|
|
123
|
-
const url = new URL(request.url);
|
|
124
|
-
|
|
125
|
-
// Inject Rivet env
|
|
126
|
-
const env = Object.assign({ RIVET: client }, cfEnv);
|
|
127
|
-
|
|
128
|
-
// Mount Rivet manager API
|
|
129
|
-
if (url.pathname.startsWith(config.managerPath)) {
|
|
130
|
-
const strippedPath = url.pathname.substring(
|
|
131
|
-
config.managerPath.length,
|
|
132
|
-
);
|
|
133
|
-
url.pathname = strippedPath;
|
|
134
|
-
const modifiedRequest = new Request(url.toString(), request);
|
|
135
|
-
return fetch(modifiedRequest, env, ctx);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (config.fetch) {
|
|
139
|
-
return config.fetch(request, env, ctx);
|
|
140
|
-
} else {
|
|
141
|
-
return new Response(
|
|
142
|
-
"This is a RivetKit server.\n\nLearn more at https://rivet.dev\n",
|
|
143
|
-
{ status: 200 },
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
} satisfies ExportedHandler<Bindings>;
|
|
148
|
-
|
|
149
|
-
return { handler, ActorHandler };
|
|
150
|
-
}
|
package/src/log.ts
DELETED
package/src/manager-driver.ts
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
import type { Hono, Context as HonoContext } from "hono";
|
|
2
|
-
import type { Encoding, RegistryConfig, UniversalWebSocket } from "rivetkit";
|
|
3
|
-
import {
|
|
4
|
-
type ActorOutput,
|
|
5
|
-
type CreateInput,
|
|
6
|
-
type GetForIdInput,
|
|
7
|
-
type GetOrCreateWithKeyInput,
|
|
8
|
-
type GetWithKeyInput,
|
|
9
|
-
type ListActorsInput,
|
|
10
|
-
type ManagerDisplayInformation,
|
|
11
|
-
type ManagerDriver,
|
|
12
|
-
WS_PROTOCOL_ACTOR,
|
|
13
|
-
WS_PROTOCOL_CONN_PARAMS,
|
|
14
|
-
WS_PROTOCOL_ENCODING,
|
|
15
|
-
WS_PROTOCOL_STANDARD,
|
|
16
|
-
WS_PROTOCOL_TARGET,
|
|
17
|
-
} from "rivetkit/driver-helpers";
|
|
18
|
-
import {
|
|
19
|
-
ActorDuplicateKey,
|
|
20
|
-
ActorNotFound,
|
|
21
|
-
InternalError,
|
|
22
|
-
} from "rivetkit/errors";
|
|
23
|
-
import { assertUnreachable } from "rivetkit/utils";
|
|
24
|
-
import { parseActorId } from "./actor-id";
|
|
25
|
-
import { getCloudflareAmbientEnv } from "./handler";
|
|
26
|
-
import { logger } from "./log";
|
|
27
|
-
import type { Bindings } from "./mod";
|
|
28
|
-
import { serializeNameAndKey } from "./util";
|
|
29
|
-
|
|
30
|
-
const STANDARD_WEBSOCKET_HEADERS = [
|
|
31
|
-
"connection",
|
|
32
|
-
"upgrade",
|
|
33
|
-
"sec-websocket-key",
|
|
34
|
-
"sec-websocket-version",
|
|
35
|
-
"sec-websocket-protocol",
|
|
36
|
-
"sec-websocket-extensions",
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
export class CloudflareActorsManagerDriver implements ManagerDriver {
|
|
40
|
-
async sendRequest(
|
|
41
|
-
actorId: string,
|
|
42
|
-
actorRequest: Request,
|
|
43
|
-
): Promise<Response> {
|
|
44
|
-
const env = getCloudflareAmbientEnv();
|
|
45
|
-
|
|
46
|
-
// Parse actor ID to get DO ID
|
|
47
|
-
const [doId] = parseActorId(actorId);
|
|
48
|
-
|
|
49
|
-
logger().debug({
|
|
50
|
-
msg: "sending request to durable object",
|
|
51
|
-
actorId,
|
|
52
|
-
doId,
|
|
53
|
-
method: actorRequest.method,
|
|
54
|
-
url: actorRequest.url,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
58
|
-
const stub = env.ACTOR_DO.get(id);
|
|
59
|
-
|
|
60
|
-
return await stub.fetch(actorRequest);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async openWebSocket(
|
|
64
|
-
path: string,
|
|
65
|
-
actorId: string,
|
|
66
|
-
encoding: Encoding,
|
|
67
|
-
params: unknown,
|
|
68
|
-
): Promise<UniversalWebSocket> {
|
|
69
|
-
const env = getCloudflareAmbientEnv();
|
|
70
|
-
|
|
71
|
-
// Parse actor ID to get DO ID
|
|
72
|
-
const [doId] = parseActorId(actorId);
|
|
73
|
-
|
|
74
|
-
logger().debug({
|
|
75
|
-
msg: "opening websocket to durable object",
|
|
76
|
-
actorId,
|
|
77
|
-
doId,
|
|
78
|
-
path,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Make a fetch request to the Durable Object with WebSocket upgrade
|
|
82
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
83
|
-
const stub = env.ACTOR_DO.get(id);
|
|
84
|
-
|
|
85
|
-
const protocols: string[] = [];
|
|
86
|
-
protocols.push(WS_PROTOCOL_STANDARD);
|
|
87
|
-
protocols.push(`${WS_PROTOCOL_TARGET}actor`);
|
|
88
|
-
protocols.push(`${WS_PROTOCOL_ACTOR}${encodeURIComponent(actorId)}`);
|
|
89
|
-
protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`);
|
|
90
|
-
if (params) {
|
|
91
|
-
protocols.push(
|
|
92
|
-
`${WS_PROTOCOL_CONN_PARAMS}${encodeURIComponent(JSON.stringify(params))}`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const headers: Record<string, string> = {
|
|
97
|
-
Upgrade: "websocket",
|
|
98
|
-
Connection: "Upgrade",
|
|
99
|
-
"sec-websocket-protocol": protocols.join(", "),
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Use the path parameter to determine the URL
|
|
103
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
104
|
-
const url = `http://actor${normalizedPath}`;
|
|
105
|
-
|
|
106
|
-
logger().debug({ msg: "rewriting websocket url", from: path, to: url });
|
|
107
|
-
|
|
108
|
-
const response = await stub.fetch(url, {
|
|
109
|
-
headers,
|
|
110
|
-
});
|
|
111
|
-
const webSocket = response.webSocket;
|
|
112
|
-
|
|
113
|
-
if (!webSocket) {
|
|
114
|
-
throw new InternalError(
|
|
115
|
-
`missing websocket connection in response from DO\n\nStatus: ${response.status}\nResponse: ${await response.text()}`,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
logger().debug({
|
|
120
|
-
msg: "durable object websocket connection open",
|
|
121
|
-
actorId,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
webSocket.accept();
|
|
125
|
-
|
|
126
|
-
// TODO: Is this still needed?
|
|
127
|
-
// HACK: Cloudflare does not call onopen automatically, so we need
|
|
128
|
-
// to call this on the next tick
|
|
129
|
-
setTimeout(() => {
|
|
130
|
-
const event = new Event("open");
|
|
131
|
-
(webSocket as any).onopen?.(event);
|
|
132
|
-
(webSocket as any).dispatchEvent(event);
|
|
133
|
-
}, 0);
|
|
134
|
-
|
|
135
|
-
return webSocket as unknown as UniversalWebSocket;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async buildGatewayUrl(actorId: string): Promise<string> {
|
|
139
|
-
return `http://actor/gateway/${encodeURIComponent(actorId)}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async proxyRequest(
|
|
143
|
-
c: HonoContext<{ Bindings: Bindings }>,
|
|
144
|
-
actorRequest: Request,
|
|
145
|
-
actorId: string,
|
|
146
|
-
): Promise<Response> {
|
|
147
|
-
const env = getCloudflareAmbientEnv();
|
|
148
|
-
|
|
149
|
-
// Parse actor ID to get DO ID
|
|
150
|
-
const [doId] = parseActorId(actorId);
|
|
151
|
-
|
|
152
|
-
logger().debug({
|
|
153
|
-
msg: "forwarding request to durable object",
|
|
154
|
-
actorId,
|
|
155
|
-
doId,
|
|
156
|
-
method: actorRequest.method,
|
|
157
|
-
url: actorRequest.url,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
161
|
-
const stub = env.ACTOR_DO.get(id);
|
|
162
|
-
|
|
163
|
-
return await stub.fetch(actorRequest);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async proxyWebSocket(
|
|
167
|
-
c: HonoContext<{ Bindings: Bindings }>,
|
|
168
|
-
path: string,
|
|
169
|
-
actorId: string,
|
|
170
|
-
encoding: Encoding,
|
|
171
|
-
params: unknown,
|
|
172
|
-
): Promise<Response> {
|
|
173
|
-
logger().debug({
|
|
174
|
-
msg: "forwarding websocket to durable object",
|
|
175
|
-
actorId,
|
|
176
|
-
path,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Validate upgrade
|
|
180
|
-
const upgradeHeader = c.req.header("Upgrade");
|
|
181
|
-
if (!upgradeHeader || upgradeHeader !== "websocket") {
|
|
182
|
-
return new Response("Expected Upgrade: websocket", {
|
|
183
|
-
status: 426,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const newUrl = new URL(`http://actor${path}`);
|
|
188
|
-
const actorRequest = new Request(newUrl, c.req.raw);
|
|
189
|
-
|
|
190
|
-
logger().debug({
|
|
191
|
-
msg: "rewriting websocket url",
|
|
192
|
-
from: c.req.url,
|
|
193
|
-
to: actorRequest.url,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Always build fresh request to prevent forwarding unwanted headers
|
|
197
|
-
// HACK: Since we can't build a new request, we need to remove
|
|
198
|
-
// non-standard headers manually
|
|
199
|
-
const headerKeys: string[] = [];
|
|
200
|
-
actorRequest.headers.forEach((v, k) => {
|
|
201
|
-
headerKeys.push(k);
|
|
202
|
-
});
|
|
203
|
-
for (const k of headerKeys) {
|
|
204
|
-
if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) {
|
|
205
|
-
actorRequest.headers.delete(k);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Build protocols for WebSocket connection
|
|
210
|
-
const protocols: string[] = [];
|
|
211
|
-
protocols.push(WS_PROTOCOL_STANDARD);
|
|
212
|
-
protocols.push(`${WS_PROTOCOL_TARGET}actor`);
|
|
213
|
-
protocols.push(`${WS_PROTOCOL_ACTOR}${encodeURIComponent(actorId)}`);
|
|
214
|
-
protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`);
|
|
215
|
-
if (params) {
|
|
216
|
-
protocols.push(
|
|
217
|
-
`${WS_PROTOCOL_CONN_PARAMS}${encodeURIComponent(JSON.stringify(params))}`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
actorRequest.headers.set(
|
|
221
|
-
"sec-websocket-protocol",
|
|
222
|
-
protocols.join(", "),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
// Parse actor ID to get DO ID
|
|
226
|
-
const env = getCloudflareAmbientEnv();
|
|
227
|
-
const [doId] = parseActorId(actorId);
|
|
228
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
229
|
-
const stub = env.ACTOR_DO.get(id);
|
|
230
|
-
|
|
231
|
-
return await stub.fetch(actorRequest);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async getForId({
|
|
235
|
-
c,
|
|
236
|
-
name,
|
|
237
|
-
actorId,
|
|
238
|
-
}: GetForIdInput<{ Bindings: Bindings }>): Promise<
|
|
239
|
-
ActorOutput | undefined
|
|
240
|
-
> {
|
|
241
|
-
const env = getCloudflareAmbientEnv();
|
|
242
|
-
|
|
243
|
-
// Parse actor ID to get DO ID and expected generation
|
|
244
|
-
const [doId, expectedGeneration] = parseActorId(actorId);
|
|
245
|
-
|
|
246
|
-
// Get the Durable Object stub
|
|
247
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
248
|
-
const stub = env.ACTOR_DO.get(id);
|
|
249
|
-
|
|
250
|
-
// Call the DO's getMetadata method
|
|
251
|
-
const result = await stub.getMetadata();
|
|
252
|
-
|
|
253
|
-
if (!result) {
|
|
254
|
-
logger().debug({
|
|
255
|
-
msg: "getForId: actor not found",
|
|
256
|
-
actorId,
|
|
257
|
-
});
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Check if the actor IDs match in order to check if the generation matches
|
|
262
|
-
if (result.actorId !== actorId) {
|
|
263
|
-
logger().debug({
|
|
264
|
-
msg: "getForId: generation mismatch",
|
|
265
|
-
requestedActorId: actorId,
|
|
266
|
-
actualActorId: result.actorId,
|
|
267
|
-
});
|
|
268
|
-
return undefined;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (result.destroying) {
|
|
272
|
-
throw new ActorNotFound(actorId);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
actorId: result.actorId,
|
|
277
|
-
name: result.name,
|
|
278
|
-
key: result.key,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async getWithKey({
|
|
283
|
-
c,
|
|
284
|
-
name,
|
|
285
|
-
key,
|
|
286
|
-
}: GetWithKeyInput<{ Bindings: Bindings }>): Promise<
|
|
287
|
-
ActorOutput | undefined
|
|
288
|
-
> {
|
|
289
|
-
const env = getCloudflareAmbientEnv();
|
|
290
|
-
|
|
291
|
-
logger().debug({ msg: "getWithKey: searching for actor", name, key });
|
|
292
|
-
|
|
293
|
-
// Generate deterministic ID from the name and key
|
|
294
|
-
const nameKeyString = serializeNameAndKey(name, key);
|
|
295
|
-
const doId = env.ACTOR_DO.idFromName(nameKeyString).toString();
|
|
296
|
-
|
|
297
|
-
// Try to get the Durable Object to see if it exists
|
|
298
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
299
|
-
const stub = env.ACTOR_DO.get(id);
|
|
300
|
-
|
|
301
|
-
// Check if actor exists without creating it
|
|
302
|
-
const result = await stub.getMetadata();
|
|
303
|
-
|
|
304
|
-
if (result) {
|
|
305
|
-
logger().debug({
|
|
306
|
-
msg: "getWithKey: found actor with matching name and key",
|
|
307
|
-
actorId: result.actorId,
|
|
308
|
-
name: result.name,
|
|
309
|
-
key: result.key,
|
|
310
|
-
});
|
|
311
|
-
return {
|
|
312
|
-
actorId: result.actorId,
|
|
313
|
-
name: result.name,
|
|
314
|
-
key: result.key,
|
|
315
|
-
};
|
|
316
|
-
} else {
|
|
317
|
-
logger().debug({
|
|
318
|
-
msg: "getWithKey: no actor found with matching name and key",
|
|
319
|
-
name,
|
|
320
|
-
key,
|
|
321
|
-
doId,
|
|
322
|
-
});
|
|
323
|
-
return undefined;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async getOrCreateWithKey({
|
|
328
|
-
c,
|
|
329
|
-
name,
|
|
330
|
-
key,
|
|
331
|
-
input,
|
|
332
|
-
}: GetOrCreateWithKeyInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
|
|
333
|
-
const env = getCloudflareAmbientEnv();
|
|
334
|
-
|
|
335
|
-
// Create a deterministic ID from the actor name and key
|
|
336
|
-
// This ensures that actors with the same name and key will have the same ID
|
|
337
|
-
const nameKeyString = serializeNameAndKey(name, key);
|
|
338
|
-
const doId = env.ACTOR_DO.idFromName(nameKeyString);
|
|
339
|
-
|
|
340
|
-
// Get or create actor using the Durable Object's method
|
|
341
|
-
const actor = env.ACTOR_DO.get(doId);
|
|
342
|
-
const result = await actor.create({
|
|
343
|
-
name,
|
|
344
|
-
key,
|
|
345
|
-
input,
|
|
346
|
-
allowExisting: true,
|
|
347
|
-
});
|
|
348
|
-
if ("success" in result) {
|
|
349
|
-
const { actorId, created } = result.success;
|
|
350
|
-
logger().debug({
|
|
351
|
-
msg: "getOrCreateWithKey result",
|
|
352
|
-
actorId,
|
|
353
|
-
name,
|
|
354
|
-
key,
|
|
355
|
-
created,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
actorId,
|
|
360
|
-
name,
|
|
361
|
-
key,
|
|
362
|
-
};
|
|
363
|
-
} else if ("error" in result) {
|
|
364
|
-
throw new Error(`Error: ${JSON.stringify(result.error)}`);
|
|
365
|
-
} else {
|
|
366
|
-
assertUnreachable(result);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async createActor({
|
|
371
|
-
c,
|
|
372
|
-
name,
|
|
373
|
-
key,
|
|
374
|
-
input,
|
|
375
|
-
}: CreateInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
|
|
376
|
-
const env = getCloudflareAmbientEnv();
|
|
377
|
-
|
|
378
|
-
// Create a deterministic ID from the actor name and key
|
|
379
|
-
// This ensures that actors with the same name and key will have the same ID
|
|
380
|
-
const nameKeyString = serializeNameAndKey(name, key);
|
|
381
|
-
const doId = env.ACTOR_DO.idFromName(nameKeyString);
|
|
382
|
-
|
|
383
|
-
// Create actor - this will fail if it already exists
|
|
384
|
-
const actor = env.ACTOR_DO.get(doId);
|
|
385
|
-
const result = await actor.create({
|
|
386
|
-
name,
|
|
387
|
-
key,
|
|
388
|
-
input,
|
|
389
|
-
allowExisting: false,
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
if ("success" in result) {
|
|
393
|
-
const { actorId } = result.success;
|
|
394
|
-
return {
|
|
395
|
-
actorId,
|
|
396
|
-
name,
|
|
397
|
-
key,
|
|
398
|
-
};
|
|
399
|
-
} else if ("error" in result) {
|
|
400
|
-
if (result.error.actorAlreadyExists) {
|
|
401
|
-
throw new ActorDuplicateKey(name, key);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
throw new InternalError(
|
|
405
|
-
`Unknown error creating actor: ${JSON.stringify(result.error)}`,
|
|
406
|
-
);
|
|
407
|
-
} else {
|
|
408
|
-
assertUnreachable(result);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
async listActors({ c, name }: ListActorsInput): Promise<ActorOutput[]> {
|
|
413
|
-
logger().warn({
|
|
414
|
-
msg: "listActors not fully implemented for Cloudflare Workers",
|
|
415
|
-
name,
|
|
416
|
-
});
|
|
417
|
-
return [];
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
displayInformation(): ManagerDisplayInformation {
|
|
421
|
-
return {
|
|
422
|
-
properties: {
|
|
423
|
-
Driver: "Cloudflare Workers",
|
|
424
|
-
},
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
setGetUpgradeWebSocket(): void {
|
|
429
|
-
// No-op for Cloudflare Workers - WebSocket upgrades are handled by the DO
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async kvGet(actorId: string, key: Uint8Array): Promise<string | null> {
|
|
433
|
-
const env = getCloudflareAmbientEnv();
|
|
434
|
-
|
|
435
|
-
// Parse actor ID to get DO ID
|
|
436
|
-
const [doId] = parseActorId(actorId);
|
|
437
|
-
|
|
438
|
-
const id = env.ACTOR_DO.idFromString(doId);
|
|
439
|
-
const stub = env.ACTOR_DO.get(id);
|
|
440
|
-
|
|
441
|
-
const value = await stub.managerKvGet(key);
|
|
442
|
-
return value !== null ? new TextDecoder().decode(value) : null;
|
|
443
|
-
}
|
|
444
|
-
}
|
package/src/mod.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export type { Client } from "rivetkit";
|
|
2
|
-
export type { DriverContext } from "./actor-driver";
|
|
3
|
-
export { createActorDurableObject } from "./actor-handler-do";
|
|
4
|
-
export type { InputConfig as Config } from "./config";
|
|
5
|
-
export {
|
|
6
|
-
type Bindings,
|
|
7
|
-
createHandler,
|
|
8
|
-
createInlineClient,
|
|
9
|
-
HandlerOutput,
|
|
10
|
-
InlineOutput,
|
|
11
|
-
} from "./handler";
|
package/src/util.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
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
|
-
}
|