alchemy 2.0.0-beta.2 → 2.0.0-beta.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/bin/{alchemy-effect.js → alchemy.js} +171 -171
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +54 -45
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +45 -2
- package/src/Cloudflare/Website/StaticSite.ts +101 -15
- package/src/Cloudflare/Website/Vite.ts +86 -20
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +572 -67
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +4 -1
- package/src/Construct.ts +2 -2
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +257 -0
- package/src/GitHub/Variable.ts +166 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Provider.ts +1 -1
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +59 -0
- package/src/Util/index.ts +1 -0
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Cloudflare/Workers/DurableObject.ts +0 -527
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import type * as cf from "@cloudflare/workers-types";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
import type { HttpServerError } from "effect/unstable/http/HttpServerError";
|
|
6
|
+
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
|
|
7
|
+
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
|
|
8
|
+
import type { HttpEffect } from "../../Http.ts";
|
|
9
|
+
import * as Output from "../../Output.ts";
|
|
10
|
+
import type { PlatformServices } from "../../Platform.ts";
|
|
11
|
+
import { effectClass, taggedFunction } from "../../Util/effect.ts";
|
|
12
|
+
import {
|
|
13
|
+
DurableObjectState,
|
|
14
|
+
fromDurableObjectState,
|
|
15
|
+
} from "./DurableObjectState.ts";
|
|
16
|
+
import { makeRpcStub } from "./Rpc.ts";
|
|
17
|
+
import { type DurableWebSocket } from "./WebSocket.ts";
|
|
18
|
+
import { Worker, WorkerEnvironment, type WorkerServices } from "./Worker.ts";
|
|
19
|
+
|
|
20
|
+
export interface DurableObjectExport {
|
|
21
|
+
readonly kind: "durableObject";
|
|
22
|
+
readonly make: (
|
|
23
|
+
state: cf.DurableObjectState,
|
|
24
|
+
env: any,
|
|
25
|
+
) => Effect.Effect<Record<string, unknown>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const isDurableObjectExport = (
|
|
29
|
+
value: unknown,
|
|
30
|
+
): value is DurableObjectExport =>
|
|
31
|
+
typeof value === "object" &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
"kind" in value &&
|
|
34
|
+
(value as any).kind === "durableObject";
|
|
35
|
+
|
|
36
|
+
export type DurableObjectId = cf.DurableObjectId;
|
|
37
|
+
export type DurableObjectJurisdiction = cf.DurableObjectJurisdiction;
|
|
38
|
+
export type DurableObjectNamespaceGetDurableObjectOptions =
|
|
39
|
+
cf.DurableObjectNamespaceGetDurableObjectOptions;
|
|
40
|
+
|
|
41
|
+
export type AlarmInvocationInfo = cf.AlarmInvocationInfo;
|
|
42
|
+
|
|
43
|
+
type TypeId = "Cloudflare.DurableObjectNamespace";
|
|
44
|
+
const TypeId = "Cloudflare.DurableObjectNamespace";
|
|
45
|
+
|
|
46
|
+
export const isDurableObjectNamespaceLike = (
|
|
47
|
+
value: unknown,
|
|
48
|
+
): value is DurableObjectNamespaceLike =>
|
|
49
|
+
typeof value === "object" &&
|
|
50
|
+
value !== null &&
|
|
51
|
+
"kind" in value &&
|
|
52
|
+
value.kind === TypeId;
|
|
53
|
+
|
|
54
|
+
export interface DurableObjectNamespaceLike<Shape = any> {
|
|
55
|
+
kind: TypeId;
|
|
56
|
+
name: string;
|
|
57
|
+
/** @internal phantom */
|
|
58
|
+
className?: string;
|
|
59
|
+
/** @internal phantom */
|
|
60
|
+
Shape?: Shape;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DurableObjectNamespace<
|
|
64
|
+
Shape = unknown,
|
|
65
|
+
> extends DurableObjectNamespaceLike<Shape> {
|
|
66
|
+
Type: TypeId;
|
|
67
|
+
name: string;
|
|
68
|
+
namespaceId: Output.Output<string>;
|
|
69
|
+
getByName: (name: string) => DurableObjectStub<Shape>;
|
|
70
|
+
newUniqueId: () => DurableObjectId;
|
|
71
|
+
idFromName: (name: string) => DurableObjectId;
|
|
72
|
+
idFromString: (id: string) => DurableObjectId;
|
|
73
|
+
get: (
|
|
74
|
+
id: DurableObjectId,
|
|
75
|
+
options?: DurableObjectNamespaceGetDurableObjectOptions,
|
|
76
|
+
) => DurableObjectStub<Shape>;
|
|
77
|
+
jurisdiction: (
|
|
78
|
+
jurisdiction: DurableObjectJurisdiction,
|
|
79
|
+
) => DurableObjectNamespace<Shape>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DurableObjectShape {
|
|
83
|
+
fetch?: HttpEffect<DurableObjectState>;
|
|
84
|
+
alarm?: (
|
|
85
|
+
alarmInfo?: AlarmInvocationInfo,
|
|
86
|
+
) => Effect.Effect<void, never, never>;
|
|
87
|
+
webSocketMessage?: (
|
|
88
|
+
socket: DurableWebSocket,
|
|
89
|
+
message: string | ArrayBuffer,
|
|
90
|
+
) => Effect.Effect<void>;
|
|
91
|
+
webSocketClose?: (
|
|
92
|
+
socket: DurableWebSocket,
|
|
93
|
+
code: number,
|
|
94
|
+
reason: string,
|
|
95
|
+
wasClean: boolean,
|
|
96
|
+
) => Effect.Effect<void>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type DurableObjectServices =
|
|
100
|
+
| DurableObjectNamespace
|
|
101
|
+
| DurableObjectState
|
|
102
|
+
| WorkerServices
|
|
103
|
+
| PlatformServices;
|
|
104
|
+
|
|
105
|
+
export interface DurableObjectNamespaceProps {
|
|
106
|
+
/**
|
|
107
|
+
* @default name
|
|
108
|
+
*/
|
|
109
|
+
className: string;
|
|
110
|
+
// scriptName?: string | undefined;
|
|
111
|
+
// environment?: string | undefined;
|
|
112
|
+
// sqlite?: boolean | undefined;
|
|
113
|
+
// namespaceId?: string | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface DurableObjectNamespaceClass extends Effect.Effect<
|
|
117
|
+
DurableObjectNamespace,
|
|
118
|
+
never,
|
|
119
|
+
DurableObjectNamespace
|
|
120
|
+
> {
|
|
121
|
+
<Self>(): {
|
|
122
|
+
<Shape, InitReq = never>(
|
|
123
|
+
name: string,
|
|
124
|
+
impl: Effect.Effect<
|
|
125
|
+
Effect.Effect<Shape, never, DurableObjectServices>,
|
|
126
|
+
never,
|
|
127
|
+
InitReq
|
|
128
|
+
>,
|
|
129
|
+
): Effect.Effect<
|
|
130
|
+
DurableObjectNamespace<Self>,
|
|
131
|
+
never,
|
|
132
|
+
Worker | Exclude<InitReq, DurableObjectServices>
|
|
133
|
+
> & {
|
|
134
|
+
new (_: never): Shape;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
<Shape>(
|
|
138
|
+
name: string,
|
|
139
|
+
props?: DurableObjectNamespaceProps,
|
|
140
|
+
): DurableObjectNamespaceLike<Shape>;
|
|
141
|
+
<Shape, InitReq = never>(
|
|
142
|
+
name: string,
|
|
143
|
+
impl: Effect.Effect<
|
|
144
|
+
Effect.Effect<Shape, never, DurableObjectServices>,
|
|
145
|
+
never,
|
|
146
|
+
InitReq
|
|
147
|
+
>,
|
|
148
|
+
): Effect.Effect<
|
|
149
|
+
DurableObjectNamespace<Shape>,
|
|
150
|
+
never,
|
|
151
|
+
Worker | Exclude<InitReq, DurableObjectServices>
|
|
152
|
+
>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class DurableObjectNamespaceScope extends Context.Service<
|
|
156
|
+
DurableObjectNamespaceScope,
|
|
157
|
+
DurableObjectNamespace
|
|
158
|
+
>()("Cloudflare.DurableObjectNamespace") {}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A Cloudflare Durable Object namespace that manages globally unique, stateful
|
|
162
|
+
* instances with WebSocket hibernation support.
|
|
163
|
+
*
|
|
164
|
+
* A Durable Object uses a two-phase pattern with two nested `Effect.gen`
|
|
165
|
+
* blocks. The outer Effect resolves shared dependencies (other DOs,
|
|
166
|
+
* containers, etc.). The inner Effect runs once per instance and returns
|
|
167
|
+
* the object's public methods and WebSocket handlers.
|
|
168
|
+
*
|
|
169
|
+
* ```typescript
|
|
170
|
+
* Effect.gen(function* () {
|
|
171
|
+
* // Phase 1: resolve shared dependencies
|
|
172
|
+
* const db = yield* Cloudflare.D1Connection.bind(MyDB);
|
|
173
|
+
*
|
|
174
|
+
* return Effect.gen(function* () {
|
|
175
|
+
* // Phase 2: per-instance setup and public API
|
|
176
|
+
* const state = yield* Cloudflare.DurableObjectState;
|
|
177
|
+
*
|
|
178
|
+
* return {
|
|
179
|
+
* save: (data: string) => db.exec("INSERT ..."),
|
|
180
|
+
* fetch: Effect.gen(function* () { ... }),
|
|
181
|
+
* webSocketMessage: Effect.fnUntraced(function* (ws, msg) { ... }),
|
|
182
|
+
* };
|
|
183
|
+
* });
|
|
184
|
+
* })
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* There are two ways to define a Durable Object. See the
|
|
188
|
+
* {@link https://alchemy.run/concepts/platform | Platform concept} page
|
|
189
|
+
* for the full explanation.
|
|
190
|
+
*
|
|
191
|
+
* - **Inline** — Effect implementation passed directly, single file.
|
|
192
|
+
* - **Modular** — class and implementation in separate files for tree-shaking.
|
|
193
|
+
*
|
|
194
|
+
* @resource
|
|
195
|
+
*
|
|
196
|
+
* @section Inline Durable Objects
|
|
197
|
+
* Pass the Effect implementation as the second argument. This is the
|
|
198
|
+
* simplest approach — everything lives in one file. Convenient when
|
|
199
|
+
* the DO doesn't need to be referenced by other Workers or DOs that
|
|
200
|
+
* would pull in its runtime dependencies.
|
|
201
|
+
*
|
|
202
|
+
* @example Inline Durable Object
|
|
203
|
+
* ```typescript
|
|
204
|
+
* export default class Counter extends Cloudflare.DurableObjectNamespace<Counter>()(
|
|
205
|
+
* "Counter",
|
|
206
|
+
* Effect.gen(function* () {
|
|
207
|
+
* // init: bind resources
|
|
208
|
+
* const db = yield* Cloudflare.D1Connection.bind(MyDB);
|
|
209
|
+
*
|
|
210
|
+
* return Effect.gen(function* () {
|
|
211
|
+
* const state = yield* Cloudflare.DurableObjectState;
|
|
212
|
+
* const count = (yield* state.storage.get<number>("count")) ?? 0;
|
|
213
|
+
*
|
|
214
|
+
* return {
|
|
215
|
+
* // runtime: use them
|
|
216
|
+
* increment: () =>
|
|
217
|
+
* Effect.gen(function* () {
|
|
218
|
+
* const next = count + 1;
|
|
219
|
+
* yield* state.storage.put("count", next);
|
|
220
|
+
* return next;
|
|
221
|
+
* }),
|
|
222
|
+
* get: () => Effect.succeed(count),
|
|
223
|
+
* };
|
|
224
|
+
* });
|
|
225
|
+
* }),
|
|
226
|
+
* ) {}
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @section Modular Durable Objects
|
|
230
|
+
* When a Worker and a DO reference each other, or multiple Workers
|
|
231
|
+
* bind the same DO, define the class separately from its `.make()`
|
|
232
|
+
* call. The class is a lightweight identifier; `.make()` provides
|
|
233
|
+
* the runtime implementation as an `export default`. Rolldown treats
|
|
234
|
+
* `.make()` as pure, so the bundler tree-shakes it and all its
|
|
235
|
+
* runtime dependencies out of any consumer's bundle.
|
|
236
|
+
*
|
|
237
|
+
* The class and `.make()` can live in the same file. This is the
|
|
238
|
+
* same pattern used by `Worker` and `Container`.
|
|
239
|
+
*
|
|
240
|
+
* @example Modular Durable Object (class + .make() in one file)
|
|
241
|
+
* ```typescript
|
|
242
|
+
* // src/Counter.ts
|
|
243
|
+
* export default class Counter extends Cloudflare.DurableObjectNamespace<Counter>()(
|
|
244
|
+
* "Counter",
|
|
245
|
+
* ) {}
|
|
246
|
+
*
|
|
247
|
+
* export default Counter.make(
|
|
248
|
+
* Effect.gen(function* () {
|
|
249
|
+
* // init: bind resources
|
|
250
|
+
* const db = yield* Cloudflare.D1Connection.bind(MyDB);
|
|
251
|
+
*
|
|
252
|
+
* return Effect.gen(function* () {
|
|
253
|
+
* const state = yield* Cloudflare.DurableObjectState;
|
|
254
|
+
* const count = (yield* state.storage.get<number>("count")) ?? 0;
|
|
255
|
+
*
|
|
256
|
+
* return {
|
|
257
|
+
* // runtime: use them
|
|
258
|
+
* increment: () =>
|
|
259
|
+
* Effect.gen(function* () {
|
|
260
|
+
* const next = count + 1;
|
|
261
|
+
* yield* state.storage.put("count", next);
|
|
262
|
+
* yield* db.prepare("INSERT INTO logs (count) VALUES (?)").bind(next).run();
|
|
263
|
+
* return next;
|
|
264
|
+
* }),
|
|
265
|
+
* get: () => Effect.succeed(count),
|
|
266
|
+
* };
|
|
267
|
+
* });
|
|
268
|
+
* }),
|
|
269
|
+
* );
|
|
270
|
+
* ```
|
|
271
|
+
*
|
|
272
|
+
* @example Binding a modular DO from a Worker
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // imports Counter; bundler tree-shakes .make()
|
|
275
|
+
* import Counter from "./Counter.ts";
|
|
276
|
+
*
|
|
277
|
+
* // init
|
|
278
|
+
* const counters = yield* Counter;
|
|
279
|
+
*
|
|
280
|
+
* return {
|
|
281
|
+
* fetch: Effect.gen(function* () {
|
|
282
|
+
* const counter = counters.getByName("user-123");
|
|
283
|
+
* return HttpServerResponse.text(String(yield* counter.get()));
|
|
284
|
+
* }),
|
|
285
|
+
* };
|
|
286
|
+
* ```
|
|
287
|
+
*
|
|
288
|
+
* @section RPC Methods
|
|
289
|
+
* Any function you return from the inner Effect becomes an RPC method
|
|
290
|
+
* that Workers can call through a stub. Methods must return an `Effect`.
|
|
291
|
+
* The caller gets a fully typed stub — if your DO returns `increment`
|
|
292
|
+
* and `get`, the stub exposes `counter.increment()` and `counter.get()`.
|
|
293
|
+
*
|
|
294
|
+
* @example Defining RPC methods
|
|
295
|
+
* ```typescript
|
|
296
|
+
* return {
|
|
297
|
+
* increment: () => Effect.succeed(++count),
|
|
298
|
+
* get: () => Effect.succeed(count),
|
|
299
|
+
* reset: () => Effect.sync(() => { count = 0; }),
|
|
300
|
+
* };
|
|
301
|
+
* ```
|
|
302
|
+
*
|
|
303
|
+
* @section Accessing Instance State
|
|
304
|
+
* Each Durable Object instance has its own transactional key-value
|
|
305
|
+
* storage via `Cloudflare.DurableObjectState`. Use `storage.get` and
|
|
306
|
+
* `storage.put` inside the inner Effect to persist data across requests
|
|
307
|
+
* and restarts.
|
|
308
|
+
*
|
|
309
|
+
* @example Reading and writing durable storage
|
|
310
|
+
* ```typescript
|
|
311
|
+
* const state = yield* Cloudflare.DurableObjectState;
|
|
312
|
+
*
|
|
313
|
+
* yield* state.storage.put("counter", 42);
|
|
314
|
+
* const value = yield* state.storage.get("counter");
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* @section WebSocket Hibernation
|
|
318
|
+
* Durable Objects support WebSocket hibernation — the runtime can
|
|
319
|
+
* evict the object from memory while keeping connections open. Use
|
|
320
|
+
* `Cloudflare.upgrade()` to accept a connection, and return
|
|
321
|
+
* `webSocketMessage` / `webSocketClose` handlers to process events
|
|
322
|
+
* when the object wakes back up.
|
|
323
|
+
*
|
|
324
|
+
* @example Accepting a WebSocket connection
|
|
325
|
+
* ```typescript
|
|
326
|
+
* return {
|
|
327
|
+
* fetch: Effect.gen(function* () {
|
|
328
|
+
* const [response, socket] = yield* Cloudflare.upgrade();
|
|
329
|
+
* socket.serializeAttachment({ id: crypto.randomUUID() });
|
|
330
|
+
* return response;
|
|
331
|
+
* }),
|
|
332
|
+
* };
|
|
333
|
+
* ```
|
|
334
|
+
*
|
|
335
|
+
* @example Handling messages and close events
|
|
336
|
+
* ```typescript
|
|
337
|
+
* return {
|
|
338
|
+
* webSocketMessage: Effect.fnUntraced(function* (
|
|
339
|
+
* socket: Cloudflare.DurableWebSocket,
|
|
340
|
+
* message: string | Uint8Array,
|
|
341
|
+
* ) {
|
|
342
|
+
* const text = typeof message === "string"
|
|
343
|
+
* ? message
|
|
344
|
+
* : new TextDecoder().decode(message);
|
|
345
|
+
* // process the message
|
|
346
|
+
* }),
|
|
347
|
+
* webSocketClose: Effect.fnUntraced(function* (
|
|
348
|
+
* ws: Cloudflare.DurableWebSocket,
|
|
349
|
+
* code: number,
|
|
350
|
+
* reason: string,
|
|
351
|
+
* ) {
|
|
352
|
+
* yield* ws.close(code, reason);
|
|
353
|
+
* }),
|
|
354
|
+
* };
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
357
|
+
* @example Recovering sessions after hibernation
|
|
358
|
+
* ```typescript
|
|
359
|
+
* const state = yield* Cloudflare.DurableObjectState;
|
|
360
|
+
* const sockets = yield* state.getWebSockets();
|
|
361
|
+
*
|
|
362
|
+
* for (const socket of sockets) {
|
|
363
|
+
* const data = socket.deserializeAttachment<{ id: string }>();
|
|
364
|
+
* // re-populate your session map
|
|
365
|
+
* }
|
|
366
|
+
* ```
|
|
367
|
+
*
|
|
368
|
+
* @section Using from a Worker
|
|
369
|
+
* Yield the DO class in your Worker's init phase to get a namespace
|
|
370
|
+
* handle. Call `getByName` or `getById` to get a typed stub, then
|
|
371
|
+
* call any RPC method or forward an HTTP request with `fetch`.
|
|
372
|
+
*
|
|
373
|
+
* @example Calling RPC methods
|
|
374
|
+
* ```typescript
|
|
375
|
+
* // init
|
|
376
|
+
* const counters = yield* Counter;
|
|
377
|
+
*
|
|
378
|
+
* return {
|
|
379
|
+
* fetch: Effect.gen(function* () {
|
|
380
|
+
* const counter = counters.getByName("user-123");
|
|
381
|
+
* yield* counter.increment();
|
|
382
|
+
* const value = yield* counter.get();
|
|
383
|
+
* return HttpServerResponse.text(String(value));
|
|
384
|
+
* }),
|
|
385
|
+
* };
|
|
386
|
+
* ```
|
|
387
|
+
*
|
|
388
|
+
* @example Forwarding an HTTP request
|
|
389
|
+
* ```typescript
|
|
390
|
+
* // init
|
|
391
|
+
* const rooms = yield* Room;
|
|
392
|
+
*
|
|
393
|
+
* return {
|
|
394
|
+
* fetch: Effect.gen(function* () {
|
|
395
|
+
* const request = yield* HttpServerRequest;
|
|
396
|
+
* const room = rooms.getByName(roomId);
|
|
397
|
+
* return yield* room.fetch(request);
|
|
398
|
+
* }),
|
|
399
|
+
* };
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* @section Binding in an Async Worker
|
|
403
|
+
* When using an Async Worker (plain `async fetch` handler, no Effect
|
|
404
|
+
* runtime), declare Durable Objects in the `bindings` prop of the
|
|
405
|
+
* Worker resource. Pass a `DurableObjectNamespace` reference with a
|
|
406
|
+
* `className` matching the exported `DurableObject` subclass in your
|
|
407
|
+
* worker source file. Use `Cloudflare.InferEnv` to get a fully typed
|
|
408
|
+
* `env` object that includes the namespace.
|
|
409
|
+
*
|
|
410
|
+
* @example Declaring a DO binding in the stack
|
|
411
|
+
* ```typescript
|
|
412
|
+
* // alchemy.run.ts
|
|
413
|
+
* import type { Counter } from "./src/worker.ts";
|
|
414
|
+
*
|
|
415
|
+
* export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
|
|
416
|
+
*
|
|
417
|
+
* export const Worker = Cloudflare.Worker("Worker", {
|
|
418
|
+
* main: "./src/worker.ts",
|
|
419
|
+
* bindings: {
|
|
420
|
+
* Counter: Cloudflare.DurableObjectNamespace<Counter>("Counter", {
|
|
421
|
+
* className: "Counter",
|
|
422
|
+
* }),
|
|
423
|
+
* },
|
|
424
|
+
* });
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* @example Using the DO from a plain async handler
|
|
428
|
+
* ```typescript
|
|
429
|
+
* // src/worker.ts
|
|
430
|
+
* import { DurableObject } from "cloudflare:workers";
|
|
431
|
+
* import type { WorkerEnv } from "../alchemy.run.ts";
|
|
432
|
+
*
|
|
433
|
+
* export default {
|
|
434
|
+
* async fetch(request: Request, env: WorkerEnv) {
|
|
435
|
+
* const counter = env.Counter.getByName("my-counter");
|
|
436
|
+
* const count = await counter.increment();
|
|
437
|
+
* return new Response(JSON.stringify({ count }));
|
|
438
|
+
* },
|
|
439
|
+
* };
|
|
440
|
+
*
|
|
441
|
+
* export class Counter extends DurableObject {
|
|
442
|
+
* private counter = 0;
|
|
443
|
+
* async increment() {
|
|
444
|
+
* return ++this.counter;
|
|
445
|
+
* }
|
|
446
|
+
* }
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
export const DurableObjectNamespace: DurableObjectNamespaceClass =
|
|
450
|
+
taggedFunction(DurableObjectNamespaceScope, ((
|
|
451
|
+
...args:
|
|
452
|
+
| []
|
|
453
|
+
| [name: string, props?: DurableObjectNamespaceProps]
|
|
454
|
+
| [
|
|
455
|
+
name: string,
|
|
456
|
+
impl: Effect.Effect<
|
|
457
|
+
Effect.Effect<
|
|
458
|
+
DurableObjectNamespace<any>,
|
|
459
|
+
never,
|
|
460
|
+
DurableObjectState
|
|
461
|
+
>
|
|
462
|
+
>,
|
|
463
|
+
]
|
|
464
|
+
) =>
|
|
465
|
+
args.length === 0
|
|
466
|
+
? DurableObjectNamespace
|
|
467
|
+
: !Effect.isEffect(args[1])
|
|
468
|
+
? ({
|
|
469
|
+
kind: TypeId,
|
|
470
|
+
name: args[0],
|
|
471
|
+
className: (args[1] as DurableObjectNamespaceProps | undefined)
|
|
472
|
+
?.className,
|
|
473
|
+
} satisfies DurableObjectNamespaceLike<any>)
|
|
474
|
+
: effectClass(
|
|
475
|
+
Effect.gen(function* () {
|
|
476
|
+
const [namespace, impl] = args as any as [
|
|
477
|
+
name: string,
|
|
478
|
+
impl: Effect.Effect<
|
|
479
|
+
Effect.Effect<
|
|
480
|
+
DurableObjectNamespace<any>,
|
|
481
|
+
never,
|
|
482
|
+
DurableObjectState
|
|
483
|
+
>
|
|
484
|
+
>,
|
|
485
|
+
];
|
|
486
|
+
const worker = yield* Worker;
|
|
487
|
+
|
|
488
|
+
yield* worker.bind`${namespace}`({
|
|
489
|
+
// TODO(sam): automate class migrations, probably in the provider
|
|
490
|
+
bindings: [
|
|
491
|
+
{
|
|
492
|
+
type: "durable_object_namespace",
|
|
493
|
+
name: namespace,
|
|
494
|
+
className: namespace,
|
|
495
|
+
// scriptName:
|
|
496
|
+
// binding.scriptName === props.workerName
|
|
497
|
+
// ? undefined
|
|
498
|
+
// : binding.scriptName,
|
|
499
|
+
// environment: binding.environment,
|
|
500
|
+
// namespaceId: binding.namespaceId,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const services =
|
|
506
|
+
yield* Effect.context<Effect.Services<typeof impl>>();
|
|
507
|
+
|
|
508
|
+
yield* worker.export(namespace, {
|
|
509
|
+
kind: "durableObject",
|
|
510
|
+
make: (state: cf.DurableObjectState, env: any) => {
|
|
511
|
+
const doState = fromDurableObjectState(state);
|
|
512
|
+
return constructor.pipe(
|
|
513
|
+
Effect.provideContext(services),
|
|
514
|
+
Effect.provideService(DurableObjectState, doState),
|
|
515
|
+
Effect.provideService(WorkerEnvironment, env),
|
|
516
|
+
Effect.map((methods: any) => {
|
|
517
|
+
console.log(
|
|
518
|
+
"[DurableObject] wrapping methods:",
|
|
519
|
+
Object.keys(methods),
|
|
520
|
+
"own:",
|
|
521
|
+
Object.getOwnPropertyNames(methods),
|
|
522
|
+
);
|
|
523
|
+
const wrapped: Record<string, unknown> = {};
|
|
524
|
+
for (const key of Object.getOwnPropertyNames(methods)) {
|
|
525
|
+
const value = methods[key];
|
|
526
|
+
if (Effect.isEffect(value)) {
|
|
527
|
+
wrapped[key] = (
|
|
528
|
+
value as Effect.Effect<any, any, any>
|
|
529
|
+
).pipe(
|
|
530
|
+
Effect.provideService(DurableObjectState, doState),
|
|
531
|
+
);
|
|
532
|
+
} else if (typeof value === "function") {
|
|
533
|
+
wrapped[key] = (...args: unknown[]) => {
|
|
534
|
+
const result = (value as Function)(...args);
|
|
535
|
+
if (Effect.isEffect(result)) {
|
|
536
|
+
return (
|
|
537
|
+
result as Effect.Effect<any, any, any>
|
|
538
|
+
).pipe(
|
|
539
|
+
Effect.provideService(
|
|
540
|
+
DurableObjectState,
|
|
541
|
+
doState,
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return result;
|
|
546
|
+
};
|
|
547
|
+
} else {
|
|
548
|
+
wrapped[key] = value;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return wrapped;
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
},
|
|
555
|
+
} satisfies DurableObjectExport);
|
|
556
|
+
|
|
557
|
+
const binding = yield* Effect.serviceOption(
|
|
558
|
+
WorkerEnvironment,
|
|
559
|
+
).pipe(
|
|
560
|
+
Effect.map(Option.getOrUndefined),
|
|
561
|
+
Effect.flatMap((env) => {
|
|
562
|
+
if (env === undefined) {
|
|
563
|
+
// should be fine to return undefined here (it is only undefined at plantime)
|
|
564
|
+
return Effect.succeed(undefined);
|
|
565
|
+
}
|
|
566
|
+
const ns = env[namespace];
|
|
567
|
+
if (!ns) {
|
|
568
|
+
return Effect.die(
|
|
569
|
+
new Error(
|
|
570
|
+
`DurableObjectNamespace '${namespace}' not found`,
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
} else if (typeof ns.getByName === "function") {
|
|
574
|
+
return Effect.succeed(ns);
|
|
575
|
+
} else {
|
|
576
|
+
return Effect.die(
|
|
577
|
+
new Error(
|
|
578
|
+
`DurableObjectNamespace '${namespace}' is not a DurableObjectNamespace`,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const namespaceId = worker.durableObjectNamespaces.pipe(
|
|
586
|
+
Output.map(
|
|
587
|
+
(durableObjectNamespaces) =>
|
|
588
|
+
durableObjectNamespaces?.[namespace],
|
|
589
|
+
),
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const self = {
|
|
593
|
+
Type: TypeId,
|
|
594
|
+
LogicalId: namespace,
|
|
595
|
+
name: namespace,
|
|
596
|
+
namespaceId,
|
|
597
|
+
getByName: (name: string) =>
|
|
598
|
+
makeRpcStub(binding.getByName(name)),
|
|
599
|
+
// newUniqueId: () => use((ns) => ns.newUniqueId()),
|
|
600
|
+
// idFromName: (name: string) => use((ns) => ns.idFromName(name)),
|
|
601
|
+
// idFromString: (id: string) => use((ns) => ns.idFromString(id)),
|
|
602
|
+
// get: (
|
|
603
|
+
// id: cf.DurableObjectId,
|
|
604
|
+
// options?: cf.DurableObjectNamespaceGetDurableObjectOptions,
|
|
605
|
+
// ) => use((ns) => makeRpcStub(ns.get(id, options))),
|
|
606
|
+
// jurisdiction: (jurisdiction: cf.DurableObjectJurisdiction) =>
|
|
607
|
+
// use((ns) => ns.jurisdiction(jurisdiction) as any),
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const constructor = yield* impl.pipe(
|
|
611
|
+
Effect.provideService(DurableObjectNamespaceScope, self as any),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
return self;
|
|
615
|
+
}),
|
|
616
|
+
)) as any);
|
|
617
|
+
|
|
618
|
+
export type DurableObjectStub<Shape> = {
|
|
619
|
+
// TODO(sam): do we need to transform? hopefully not
|
|
620
|
+
[key in keyof Shape]: Shape[key];
|
|
621
|
+
} & {
|
|
622
|
+
fetch: (
|
|
623
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
624
|
+
) => Effect.Effect<
|
|
625
|
+
HttpServerResponse.HttpServerResponse,
|
|
626
|
+
HttpServerError,
|
|
627
|
+
never
|
|
628
|
+
>;
|
|
629
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type * as cf from "@cloudflare/workers-types";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import {
|
|
5
|
+
fromDurableObjectStorage,
|
|
6
|
+
type DurableObjectStorage,
|
|
7
|
+
} from "./DurableObjectStorage.ts";
|
|
8
|
+
import { fromWebSocket, type DurableWebSocket } from "./WebSocket.ts";
|
|
9
|
+
|
|
10
|
+
export class DurableObjectState extends Context.Service<
|
|
11
|
+
DurableObjectState,
|
|
12
|
+
{
|
|
13
|
+
readonly id: cf.DurableObjectId;
|
|
14
|
+
readonly storage: DurableObjectStorage;
|
|
15
|
+
container?: cf.Container;
|
|
16
|
+
blockConcurrencyWhile<T>(
|
|
17
|
+
callback: () => Effect.Effect<T>,
|
|
18
|
+
): Effect.Effect<T>;
|
|
19
|
+
acceptWebSocket(ws: DurableWebSocket, tags?: string[]): Effect.Effect<void>;
|
|
20
|
+
getWebSockets(tag?: string): Effect.Effect<DurableWebSocket[]>;
|
|
21
|
+
setWebSocketAutoResponse(
|
|
22
|
+
maybeReqResp?: cf.WebSocketRequestResponsePair,
|
|
23
|
+
): Effect.Effect<void>;
|
|
24
|
+
getWebSocketAutoResponse(): Effect.Effect<cf.WebSocketRequestResponsePair | null>;
|
|
25
|
+
getWebSocketAutoResponseTimestamp(
|
|
26
|
+
ws: cf.WebSocket,
|
|
27
|
+
): Effect.Effect<Date | null>;
|
|
28
|
+
setHibernatableWebSocketEventTimeout(
|
|
29
|
+
timeoutMs?: number,
|
|
30
|
+
): Effect.Effect<void>;
|
|
31
|
+
getHibernatableWebSocketEventTimeout(): Effect.Effect<number | null>;
|
|
32
|
+
getTags(ws: cf.WebSocket): Effect.Effect<string[]>;
|
|
33
|
+
abort(reason?: string): Effect.Effect<void>;
|
|
34
|
+
}
|
|
35
|
+
>()("Cloudflare.DurableObjectState") {}
|
|
36
|
+
|
|
37
|
+
export const fromDurableObjectState = (
|
|
38
|
+
state: cf.DurableObjectState,
|
|
39
|
+
): DurableObjectState["Service"] => ({
|
|
40
|
+
id: state.id,
|
|
41
|
+
container: state.container,
|
|
42
|
+
storage: fromDurableObjectStorage(state.storage),
|
|
43
|
+
blockConcurrencyWhile: <T>(callback: () => Effect.Effect<T>) =>
|
|
44
|
+
Effect.tryPromise(() =>
|
|
45
|
+
state.blockConcurrencyWhile(() => Effect.runPromise(callback())),
|
|
46
|
+
),
|
|
47
|
+
acceptWebSocket: (ws: DurableWebSocket, tags?: string[]) =>
|
|
48
|
+
Effect.sync(() => state.acceptWebSocket(ws.ws, tags)),
|
|
49
|
+
getWebSockets: (tag?: string) =>
|
|
50
|
+
Effect.sync(() => state.getWebSockets(tag).map(fromWebSocket)),
|
|
51
|
+
setWebSocketAutoResponse: (maybeReqResp?: cf.WebSocketRequestResponsePair) =>
|
|
52
|
+
Effect.sync(() => state.setWebSocketAutoResponse(maybeReqResp)),
|
|
53
|
+
getWebSocketAutoResponse: () =>
|
|
54
|
+
Effect.sync(() => state.getWebSocketAutoResponse()),
|
|
55
|
+
getWebSocketAutoResponseTimestamp: (ws: cf.WebSocket) =>
|
|
56
|
+
Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws)),
|
|
57
|
+
setHibernatableWebSocketEventTimeout: (timeoutMs?: number) =>
|
|
58
|
+
Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
|
|
59
|
+
getHibernatableWebSocketEventTimeout: () =>
|
|
60
|
+
Effect.sync(() => state.getHibernatableWebSocketEventTimeout()),
|
|
61
|
+
getTags: (ws: cf.WebSocket) => Effect.sync(() => state.getTags(ws)),
|
|
62
|
+
abort: (reason?: string) => Effect.sync(() => state.abort(reason)),
|
|
63
|
+
});
|