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,256 @@
|
|
|
1
|
+
import type * as cf from "@cloudflare/workers-types";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// SqlStorage — Effect-native wrapper around cf.SqlStorage
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export type SqlStorageValue = cf.SqlStorageValue;
|
|
10
|
+
|
|
11
|
+
export interface SqlCursor<
|
|
12
|
+
T extends Record<string, SqlStorageValue>,
|
|
13
|
+
> extends Stream.Stream<T> {
|
|
14
|
+
next(): Effect.Effect<
|
|
15
|
+
{ done?: false; value: T } | { done: true; value?: never }
|
|
16
|
+
>;
|
|
17
|
+
toArray(): Effect.Effect<T[]>;
|
|
18
|
+
one(): Effect.Effect<T>;
|
|
19
|
+
raw<U extends SqlStorageValue[]>(): Stream.Stream<U>;
|
|
20
|
+
readonly columnNames: string[];
|
|
21
|
+
readonly rowsRead: Effect.Effect<number>;
|
|
22
|
+
readonly rowsWritten: Effect.Effect<number>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SqlStorage {
|
|
26
|
+
exec<T extends Record<string, SqlStorageValue>>(
|
|
27
|
+
query: string,
|
|
28
|
+
...bindings: any[]
|
|
29
|
+
): Effect.Effect<SqlCursor<T>>;
|
|
30
|
+
readonly databaseSize: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fromSqlCursor = <T extends Record<string, SqlStorageValue>>(
|
|
34
|
+
cursor: cf.SqlStorageCursor<T>,
|
|
35
|
+
): SqlCursor<T> => {
|
|
36
|
+
const stream = Stream.fromIterableEffect(Effect.sync(() => cursor));
|
|
37
|
+
return Object.assign(stream, {
|
|
38
|
+
next: () => Effect.sync(() => cursor.next()),
|
|
39
|
+
toArray: () => Effect.sync(() => cursor.toArray()),
|
|
40
|
+
one: () => Effect.sync(() => cursor.one()),
|
|
41
|
+
raw: <U extends SqlStorageValue[]>() =>
|
|
42
|
+
Stream.fromIterableEffect(Effect.sync(() => cursor.raw<U>())),
|
|
43
|
+
get columnNames() {
|
|
44
|
+
return cursor.columnNames;
|
|
45
|
+
},
|
|
46
|
+
rowsRead: Effect.sync(() => cursor.rowsRead),
|
|
47
|
+
rowsWritten: Effect.sync(() => cursor.rowsWritten),
|
|
48
|
+
}) as SqlCursor<T>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const fromSqlStorage = (sql: cf.SqlStorage): SqlStorage => ({
|
|
52
|
+
exec: <T extends Record<string, SqlStorageValue>>(
|
|
53
|
+
query: string,
|
|
54
|
+
...bindings: any[]
|
|
55
|
+
): Effect.Effect<SqlCursor<T>> =>
|
|
56
|
+
Effect.sync(() => fromSqlCursor(sql.exec<T>(query, ...bindings))),
|
|
57
|
+
get databaseSize() {
|
|
58
|
+
return sql.databaseSize;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// DurableObjectTransaction
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export interface DurableObjectTransaction {
|
|
67
|
+
get<T = unknown>(
|
|
68
|
+
key: string,
|
|
69
|
+
options?: cf.DurableObjectGetOptions,
|
|
70
|
+
): Effect.Effect<T | undefined>;
|
|
71
|
+
get<T = unknown>(
|
|
72
|
+
keys: string[],
|
|
73
|
+
options?: cf.DurableObjectGetOptions,
|
|
74
|
+
): Effect.Effect<Map<string, T>>;
|
|
75
|
+
list<T = unknown>(
|
|
76
|
+
options?: cf.DurableObjectListOptions,
|
|
77
|
+
): Effect.Effect<Map<string, T>>;
|
|
78
|
+
put<T>(
|
|
79
|
+
key: string,
|
|
80
|
+
value: T,
|
|
81
|
+
options?: cf.DurableObjectPutOptions,
|
|
82
|
+
): Effect.Effect<void>;
|
|
83
|
+
put<T>(
|
|
84
|
+
entries: Record<string, T>,
|
|
85
|
+
options?: cf.DurableObjectPutOptions,
|
|
86
|
+
): Effect.Effect<void>;
|
|
87
|
+
delete(
|
|
88
|
+
key: string,
|
|
89
|
+
options?: cf.DurableObjectPutOptions,
|
|
90
|
+
): Effect.Effect<boolean>;
|
|
91
|
+
delete(
|
|
92
|
+
keys: string[],
|
|
93
|
+
options?: cf.DurableObjectPutOptions,
|
|
94
|
+
): Effect.Effect<number>;
|
|
95
|
+
rollback(): Effect.Effect<void>;
|
|
96
|
+
getAlarm(
|
|
97
|
+
options?: cf.DurableObjectGetAlarmOptions,
|
|
98
|
+
): Effect.Effect<number | null>;
|
|
99
|
+
setAlarm(
|
|
100
|
+
scheduledTime: number | Date,
|
|
101
|
+
options?: cf.DurableObjectSetAlarmOptions,
|
|
102
|
+
): Effect.Effect<void>;
|
|
103
|
+
deleteAlarm(options?: cf.DurableObjectSetAlarmOptions): Effect.Effect<void>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// DurableObjectStorage
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export interface DurableObjectStorage {
|
|
111
|
+
get<T = unknown>(
|
|
112
|
+
key: string,
|
|
113
|
+
options?: cf.DurableObjectGetOptions,
|
|
114
|
+
): Effect.Effect<T | undefined>;
|
|
115
|
+
get<T = unknown>(
|
|
116
|
+
keys: string[],
|
|
117
|
+
options?: cf.DurableObjectGetOptions,
|
|
118
|
+
): Effect.Effect<Map<string, T>>;
|
|
119
|
+
list<T = unknown>(
|
|
120
|
+
options?: cf.DurableObjectListOptions,
|
|
121
|
+
): Effect.Effect<Map<string, T>>;
|
|
122
|
+
put<T>(
|
|
123
|
+
key: string,
|
|
124
|
+
value: T,
|
|
125
|
+
options?: cf.DurableObjectPutOptions,
|
|
126
|
+
): Effect.Effect<void>;
|
|
127
|
+
put<T>(
|
|
128
|
+
entries: Record<string, T>,
|
|
129
|
+
options?: cf.DurableObjectPutOptions,
|
|
130
|
+
): Effect.Effect<void>;
|
|
131
|
+
delete(
|
|
132
|
+
key: string,
|
|
133
|
+
options?: cf.DurableObjectPutOptions,
|
|
134
|
+
): Effect.Effect<boolean>;
|
|
135
|
+
delete(
|
|
136
|
+
keys: string[],
|
|
137
|
+
options?: cf.DurableObjectPutOptions,
|
|
138
|
+
): Effect.Effect<number>;
|
|
139
|
+
deleteAll(options?: cf.DurableObjectPutOptions): Effect.Effect<void>;
|
|
140
|
+
transaction<T>(
|
|
141
|
+
closure: (txn: DurableObjectTransaction) => Effect.Effect<T>,
|
|
142
|
+
): Effect.Effect<T>;
|
|
143
|
+
getAlarm(
|
|
144
|
+
options?: cf.DurableObjectGetAlarmOptions,
|
|
145
|
+
): Effect.Effect<number | null>;
|
|
146
|
+
setAlarm(
|
|
147
|
+
scheduledTime: number | Date,
|
|
148
|
+
options?: cf.DurableObjectSetAlarmOptions,
|
|
149
|
+
): Effect.Effect<void>;
|
|
150
|
+
deleteAlarm(options?: cf.DurableObjectSetAlarmOptions): Effect.Effect<void>;
|
|
151
|
+
sync(): Effect.Effect<void>;
|
|
152
|
+
sql: SqlStorage;
|
|
153
|
+
kv: cf.SyncKvStorage;
|
|
154
|
+
transactionSync<T>(closure: () => T): T;
|
|
155
|
+
getCurrentBookmark(): Effect.Effect<string>;
|
|
156
|
+
getBookmarkForTime(timestamp: number | Date): Effect.Effect<string>;
|
|
157
|
+
onNextSessionRestoreBookmark(bookmark: string): Effect.Effect<string>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Constructors from raw Cloudflare types
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
export const fromDurableObjectTransaction = (
|
|
165
|
+
txn: cf.DurableObjectTransaction,
|
|
166
|
+
): DurableObjectTransaction => ({
|
|
167
|
+
get: ((keyOrKeys: string | string[], options?: cf.DurableObjectGetOptions) =>
|
|
168
|
+
Effect.tryPromise(() => txn.get(keyOrKeys as any, options))) as any,
|
|
169
|
+
list: (options?: cf.DurableObjectListOptions) =>
|
|
170
|
+
Effect.tryPromise(() => txn.list(options)),
|
|
171
|
+
put: ((
|
|
172
|
+
keyOrEntries: string | Record<string, unknown>,
|
|
173
|
+
valueOrOptions?: unknown,
|
|
174
|
+
maybeOptions?: cf.DurableObjectPutOptions,
|
|
175
|
+
) =>
|
|
176
|
+
typeof keyOrEntries === "string"
|
|
177
|
+
? Effect.tryPromise(() =>
|
|
178
|
+
txn.put(keyOrEntries, valueOrOptions, maybeOptions),
|
|
179
|
+
)
|
|
180
|
+
: Effect.tryPromise(() =>
|
|
181
|
+
txn.put(
|
|
182
|
+
keyOrEntries,
|
|
183
|
+
valueOrOptions as cf.DurableObjectPutOptions | undefined,
|
|
184
|
+
),
|
|
185
|
+
)) as any,
|
|
186
|
+
delete: ((
|
|
187
|
+
keyOrKeys: string | string[],
|
|
188
|
+
options?: cf.DurableObjectPutOptions,
|
|
189
|
+
) => Effect.tryPromise(() => txn.delete(keyOrKeys as any, options))) as any,
|
|
190
|
+
rollback: () => Effect.sync(() => txn.rollback()),
|
|
191
|
+
getAlarm: (options?: cf.DurableObjectGetAlarmOptions) =>
|
|
192
|
+
Effect.tryPromise(() => txn.getAlarm(options)),
|
|
193
|
+
setAlarm: (
|
|
194
|
+
scheduledTime: number | Date,
|
|
195
|
+
options?: cf.DurableObjectSetAlarmOptions,
|
|
196
|
+
) => Effect.tryPromise(() => txn.setAlarm(scheduledTime, options)),
|
|
197
|
+
deleteAlarm: (options?: cf.DurableObjectSetAlarmOptions) =>
|
|
198
|
+
Effect.tryPromise(() => txn.deleteAlarm(options)),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const fromDurableObjectStorage = (
|
|
202
|
+
storage: cf.DurableObjectStorage,
|
|
203
|
+
): DurableObjectStorage => ({
|
|
204
|
+
get: ((keyOrKeys: string | string[], options?: cf.DurableObjectGetOptions) =>
|
|
205
|
+
Effect.tryPromise(() => storage.get(keyOrKeys as any, options))) as any,
|
|
206
|
+
list: (options?: cf.DurableObjectListOptions) =>
|
|
207
|
+
Effect.tryPromise(() => storage.list(options)),
|
|
208
|
+
put: ((
|
|
209
|
+
keyOrEntries: string | Record<string, unknown>,
|
|
210
|
+
valueOrOptions?: unknown,
|
|
211
|
+
maybeOptions?: cf.DurableObjectPutOptions,
|
|
212
|
+
) =>
|
|
213
|
+
typeof keyOrEntries === "string"
|
|
214
|
+
? Effect.tryPromise(() =>
|
|
215
|
+
storage.put(keyOrEntries, valueOrOptions, maybeOptions),
|
|
216
|
+
)
|
|
217
|
+
: Effect.tryPromise(() =>
|
|
218
|
+
storage.put(
|
|
219
|
+
keyOrEntries,
|
|
220
|
+
valueOrOptions as cf.DurableObjectPutOptions | undefined,
|
|
221
|
+
),
|
|
222
|
+
)) as any,
|
|
223
|
+
delete: ((
|
|
224
|
+
keyOrKeys: string | string[],
|
|
225
|
+
options?: cf.DurableObjectPutOptions,
|
|
226
|
+
) =>
|
|
227
|
+
Effect.tryPromise(() => storage.delete(keyOrKeys as any, options))) as any,
|
|
228
|
+
deleteAll: (options?: cf.DurableObjectPutOptions) =>
|
|
229
|
+
Effect.tryPromise(() => storage.deleteAll(options)),
|
|
230
|
+
transaction: <T>(
|
|
231
|
+
closure: (txn: DurableObjectTransaction) => Effect.Effect<T>,
|
|
232
|
+
) =>
|
|
233
|
+
Effect.tryPromise(() =>
|
|
234
|
+
storage.transaction((txn) =>
|
|
235
|
+
Effect.runPromise(closure(fromDurableObjectTransaction(txn))),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
getAlarm: (options?: cf.DurableObjectGetAlarmOptions) =>
|
|
239
|
+
Effect.tryPromise(() => storage.getAlarm(options)),
|
|
240
|
+
setAlarm: (
|
|
241
|
+
scheduledTime: number | Date,
|
|
242
|
+
options?: cf.DurableObjectSetAlarmOptions,
|
|
243
|
+
) => Effect.tryPromise(() => storage.setAlarm(scheduledTime, options)),
|
|
244
|
+
deleteAlarm: (options?: cf.DurableObjectSetAlarmOptions) =>
|
|
245
|
+
Effect.tryPromise(() => storage.deleteAlarm(options)),
|
|
246
|
+
sync: () => Effect.tryPromise(() => storage.sync()),
|
|
247
|
+
sql: fromSqlStorage(storage.sql),
|
|
248
|
+
kv: storage.kv,
|
|
249
|
+
transactionSync: <T>(closure: () => T) => storage.transactionSync(closure),
|
|
250
|
+
getCurrentBookmark: () =>
|
|
251
|
+
Effect.tryPromise(() => storage.getCurrentBookmark()),
|
|
252
|
+
getBookmarkForTime: (timestamp: number | Date) =>
|
|
253
|
+
Effect.tryPromise(() => storage.getBookmarkForTime(timestamp)),
|
|
254
|
+
onNextSessionRestoreBookmark: (bookmark: string) =>
|
|
255
|
+
Effect.tryPromise(() => storage.onNextSessionRestoreBookmark(bookmark)),
|
|
256
|
+
});
|
|
@@ -70,16 +70,40 @@ export type DynamicWorkerLoader = {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Load and run ephemeral Workers at runtime from inline JavaScript
|
|
74
|
+
* modules.
|
|
74
75
|
*
|
|
75
|
-
*
|
|
76
|
-
* Worker. At runtime
|
|
77
|
-
* and
|
|
76
|
+
* `DynamicWorkerLoader` registers a `worker_loader` binding on the
|
|
77
|
+
* parent Worker at deploy time. At runtime you call `.load()` with
|
|
78
|
+
* inline module source code and get back a fully typed Worker
|
|
79
|
+
* instance you can `fetch` or call RPC methods on. Each loaded
|
|
80
|
+
* Worker runs in its own isolate with full sandboxing.
|
|
78
81
|
*
|
|
79
|
-
*
|
|
82
|
+
* This is useful for evaluating user-provided code, running
|
|
83
|
+
* untrusted plugins, or dynamically generating Workers from
|
|
84
|
+
* templates.
|
|
85
|
+
*
|
|
86
|
+
* @resource
|
|
87
|
+
*
|
|
88
|
+
* @section Creating a Loader
|
|
89
|
+
* Yield `Cloudflare.DynamicWorkerLoader` in your Worker's init
|
|
90
|
+
* phase to register the binding. The string argument becomes the
|
|
91
|
+
* binding name on the deployed Worker.
|
|
92
|
+
*
|
|
93
|
+
* @example Registering a loader
|
|
80
94
|
* ```typescript
|
|
81
|
-
*
|
|
95
|
+
* // init
|
|
96
|
+
* const loader = yield* Cloudflare.DynamicWorkerLoader("Loader");
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @section Loading a Worker
|
|
100
|
+
* Call `loader.load()` with a compatibility date, a main module
|
|
101
|
+
* name, and a map of module names to source code strings. The
|
|
102
|
+
* returned instance exposes `.fetch()` for HTTP and RPC methods
|
|
103
|
+
* for named entrypoints.
|
|
82
104
|
*
|
|
105
|
+
* @example Loading and calling a dynamic Worker
|
|
106
|
+
* ```typescript
|
|
83
107
|
* const worker = loader.load({
|
|
84
108
|
* compatibilityDate: "2026-01-28",
|
|
85
109
|
* mainModule: "worker.js",
|
|
@@ -90,10 +114,45 @@ export type DynamicWorkerLoader = {
|
|
|
90
114
|
* }
|
|
91
115
|
* }`,
|
|
92
116
|
* },
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* const response = yield* worker.fetch(
|
|
120
|
+
* HttpClientRequest.get("https://worker/"),
|
|
121
|
+
* );
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @section Sandboxing
|
|
125
|
+
* Set `globalOutbound` to `null` to block all outbound network
|
|
126
|
+
* access from the dynamic Worker, or pass an RPC stub to intercept
|
|
127
|
+
* and proxy outbound requests.
|
|
128
|
+
*
|
|
129
|
+
* @example Blocking outbound access
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const worker = loader.load({
|
|
132
|
+
* compatibilityDate: "2026-01-28",
|
|
133
|
+
* mainModule: "worker.js",
|
|
134
|
+
* modules: {
|
|
135
|
+
* "worker.js": `export default {
|
|
136
|
+
* async fetch(req) {
|
|
137
|
+
* // fetch() calls from here will fail
|
|
138
|
+
* return new Response("sandboxed");
|
|
139
|
+
* }
|
|
140
|
+
* }`,
|
|
141
|
+
* },
|
|
93
142
|
* globalOutbound: null,
|
|
94
143
|
* });
|
|
144
|
+
* ```
|
|
95
145
|
*
|
|
96
|
-
*
|
|
146
|
+
* @section Named Entrypoints
|
|
147
|
+
* If the dynamic Worker exports named entrypoints, use
|
|
148
|
+
* `.getEntrypoint(name)` to get a typed stub for calling its
|
|
149
|
+
* methods.
|
|
150
|
+
*
|
|
151
|
+
* @example Calling a named entrypoint
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const worker = loader.load({ ... });
|
|
154
|
+
* const api = worker.getEntrypoint<{ greet: (name: string) => Effect.Effect<string> }>("api");
|
|
155
|
+
* const greeting = yield* api.greet("world");
|
|
97
156
|
* ```
|
|
98
157
|
*/
|
|
99
158
|
export const DynamicWorkerLoader = Effect.fnUntraced(function* (name: string) {
|
|
@@ -14,10 +14,14 @@ export type InferEnv<W> = W extends
|
|
|
14
14
|
}
|
|
15
15
|
: never;
|
|
16
16
|
|
|
17
|
-
type GetBindingType<T> = T extends Cloudflare.
|
|
18
|
-
?
|
|
19
|
-
: T extends Cloudflare.
|
|
20
|
-
?
|
|
21
|
-
: T extends Cloudflare.
|
|
22
|
-
?
|
|
23
|
-
:
|
|
17
|
+
type GetBindingType<T> = T extends Cloudflare.Assets
|
|
18
|
+
? Service
|
|
19
|
+
: T extends Cloudflare.D1Database
|
|
20
|
+
? D1Database
|
|
21
|
+
: T extends Cloudflare.R2Bucket
|
|
22
|
+
? R2Bucket
|
|
23
|
+
: T extends Cloudflare.KVNamespace
|
|
24
|
+
? KVNamespace
|
|
25
|
+
: T extends Cloudflare.DurableObjectNamespaceLike
|
|
26
|
+
? DurableObjectNamespace<Exclude<T["Shape"], undefined>>
|
|
27
|
+
: never;
|
|
@@ -225,7 +225,7 @@ export const makeDurableObjectBridge =
|
|
|
225
225
|
) =>
|
|
226
226
|
(className: string) =>
|
|
227
227
|
class DurableObjectBridge extends DurableObject {
|
|
228
|
-
readonly object: Promise<
|
|
228
|
+
readonly object: Promise<DurableObjectShape>;
|
|
229
229
|
|
|
230
230
|
async fetch(request: cf.Request): Promise<cf.Response> {
|
|
231
231
|
const methods = await this.object;
|
|
@@ -240,6 +240,13 @@ export const makeDurableObjectBridge =
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
async alarm(alarmInfo?: cf.AlarmInvocationInfo) {
|
|
244
|
+
const methods = await this.object;
|
|
245
|
+
if (methods.alarm) {
|
|
246
|
+
await Effect.runPromise(methods.alarm(alarmInfo));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
243
250
|
async webSocketMessage(ws: cf.WebSocket, message: string | ArrayBuffer) {
|
|
244
251
|
const methods = await this.object;
|
|
245
252
|
if (methods.webSocketMessage) {
|
|
@@ -288,7 +295,10 @@ export const makeDurableObjectBridge =
|
|
|
288
295
|
? target[prop]
|
|
289
296
|
: async (...args: unknown[]) => {
|
|
290
297
|
const methods = await this.object;
|
|
291
|
-
const
|
|
298
|
+
const method = methods[
|
|
299
|
+
prop as keyof DurableObjectShape
|
|
300
|
+
] as any; // TODO(sam): what should the type be? Is it safe to always call it?
|
|
301
|
+
const value = method(...args);
|
|
292
302
|
if (Effect.isEffect(value)) {
|
|
293
303
|
const exit = await Effect.runPromiseExit(
|
|
294
304
|
value as Effect.Effect<unknown, never>,
|
|
@@ -374,6 +384,7 @@ export const makeWorkflowBridge =
|
|
|
374
384
|
}
|
|
375
385
|
};
|
|
376
386
|
|
|
387
|
+
import type { DurableObjectShape } from "./DurableObjectNamespace.ts";
|
|
377
388
|
import {
|
|
378
389
|
WorkflowEvent as WorkflowEventService,
|
|
379
390
|
WorkflowStep,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import { DurableObjectState } from "./DurableObjectState.ts";
|
|
5
|
+
import type { SqlStorageValue } from "./DurableObjectStorage.ts";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Scheduled Events — SQLite-backed cron/timer for Durable Objects
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const INIT_TABLE_SQL = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS alchemy_scheduled_events (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
run_at INTEGER NOT NULL,
|
|
15
|
+
repeat_ms INTEGER,
|
|
16
|
+
payload TEXT NOT NULL
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_alchemy_scheduled_events_run_at
|
|
19
|
+
ON alchemy_scheduled_events (run_at);
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const ensureTable = Effect.gen(function* () {
|
|
23
|
+
const ctx = yield* DurableObjectState;
|
|
24
|
+
yield* ctx.storage.sql.exec(INIT_TABLE_SQL);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export interface ScheduledEvent {
|
|
28
|
+
id: string;
|
|
29
|
+
runAt: Date;
|
|
30
|
+
repeatMs?: number;
|
|
31
|
+
payload: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface EventRow extends Record<string, SqlStorageValue> {
|
|
35
|
+
id: string;
|
|
36
|
+
run_at: number;
|
|
37
|
+
repeat_ms: number | null;
|
|
38
|
+
payload: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const toScheduledEvent = (row: EventRow): ScheduledEvent => ({
|
|
42
|
+
id: row.id,
|
|
43
|
+
runAt: new Date(row.run_at),
|
|
44
|
+
repeatMs: row.repeat_ms ?? undefined,
|
|
45
|
+
payload: JSON.parse(row.payload) as unknown,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Schedule (or reschedule) a named event on the current Durable Object.
|
|
50
|
+
*
|
|
51
|
+
* The event is persisted in a SQLite table and the DO alarm is set to
|
|
52
|
+
* the earliest pending `runAt`. If an event with the same `id` already
|
|
53
|
+
* exists it is replaced (upsert).
|
|
54
|
+
*
|
|
55
|
+
* @param id Stable identifier for the event (used for upsert / cancel).
|
|
56
|
+
* @param runAt When the event should fire.
|
|
57
|
+
* @param payload Arbitrary JSON-serialisable data delivered to the alarm handler.
|
|
58
|
+
* @param repeatMs If set, the event re-schedules itself this many ms after each fire.
|
|
59
|
+
*/
|
|
60
|
+
export const scheduleEvent = Effect.fnUntraced(function* (
|
|
61
|
+
id: string,
|
|
62
|
+
runAt: Date,
|
|
63
|
+
payload: unknown,
|
|
64
|
+
repeatMs?: number,
|
|
65
|
+
) {
|
|
66
|
+
yield* ensureTable;
|
|
67
|
+
const ctx = yield* DurableObjectState;
|
|
68
|
+
|
|
69
|
+
yield* ctx.storage.sql.exec(
|
|
70
|
+
`INSERT OR REPLACE INTO alchemy_scheduled_events (id, run_at, repeat_ms, payload)
|
|
71
|
+
VALUES (?, ?, ?, ?)`,
|
|
72
|
+
id,
|
|
73
|
+
runAt.getTime(),
|
|
74
|
+
repeatMs ?? null,
|
|
75
|
+
JSON.stringify(payload),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
yield* reconcileAlarm;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Cancel a previously scheduled event by id. No-op if the event does not exist.
|
|
83
|
+
*/
|
|
84
|
+
export const cancelEvent = Effect.fnUntraced(function* (id: string) {
|
|
85
|
+
yield* ensureTable;
|
|
86
|
+
const ctx = yield* DurableObjectState;
|
|
87
|
+
|
|
88
|
+
yield* ctx.storage.sql.exec(
|
|
89
|
+
`DELETE FROM alchemy_scheduled_events WHERE id = ?`,
|
|
90
|
+
id,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
yield* reconcileAlarm;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List all currently scheduled events, ordered by `runAt` ascending.
|
|
98
|
+
*/
|
|
99
|
+
export const listEvents: Effect.Effect<
|
|
100
|
+
ScheduledEvent[],
|
|
101
|
+
never,
|
|
102
|
+
DurableObjectState
|
|
103
|
+
> = Effect.gen(function* () {
|
|
104
|
+
yield* ensureTable;
|
|
105
|
+
const ctx = yield* DurableObjectState;
|
|
106
|
+
|
|
107
|
+
const cursor = yield* ctx.storage.sql.exec<EventRow>(
|
|
108
|
+
`SELECT id, run_at, repeat_ms, payload FROM alchemy_scheduled_events ORDER BY run_at ASC`,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return yield* cursor.pipe(Stream.map(toScheduledEvent), Stream.runCollect);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Process all events whose `runAt` <= now. Returns the fired events.
|
|
116
|
+
*
|
|
117
|
+
* - One-shot events are deleted after firing.
|
|
118
|
+
* - Repeating events have their `runAt` bumped by `repeatMs`.
|
|
119
|
+
* - The DO alarm is re-set to the next pending event (if any).
|
|
120
|
+
*
|
|
121
|
+
* Call this from your Durable Object's `alarm` handler:
|
|
122
|
+
*
|
|
123
|
+
* ```ts
|
|
124
|
+
* alarm: () => Effect.gen(function* () {
|
|
125
|
+
* const fired = yield* Cloudflare.processScheduledEvents;
|
|
126
|
+
* for (const event of fired) {
|
|
127
|
+
* // handle each event
|
|
128
|
+
* }
|
|
129
|
+
* })
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export const processScheduledEvents: Effect.Effect<
|
|
133
|
+
ScheduledEvent[],
|
|
134
|
+
never,
|
|
135
|
+
DurableObjectState
|
|
136
|
+
> = Effect.gen(function* () {
|
|
137
|
+
yield* ensureTable;
|
|
138
|
+
const ctx = yield* DurableObjectState;
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
|
|
141
|
+
const cursor = yield* ctx.storage.sql.exec<EventRow>(
|
|
142
|
+
`SELECT id, run_at, repeat_ms, payload FROM alchemy_scheduled_events WHERE run_at <= ? ORDER BY run_at ASC`,
|
|
143
|
+
now,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const fired = yield* cursor.pipe(
|
|
147
|
+
Stream.mapEffect((row) =>
|
|
148
|
+
(row.repeat_ms != null
|
|
149
|
+
? ctx.storage.sql.exec(
|
|
150
|
+
`UPDATE alchemy_scheduled_events SET run_at = ? WHERE id = ?`,
|
|
151
|
+
now + row.repeat_ms,
|
|
152
|
+
row.id,
|
|
153
|
+
)
|
|
154
|
+
: ctx.storage.sql.exec(
|
|
155
|
+
`DELETE FROM alchemy_scheduled_events WHERE id = ?`,
|
|
156
|
+
row.id,
|
|
157
|
+
)
|
|
158
|
+
).pipe(Effect.as(toScheduledEvent(row))),
|
|
159
|
+
),
|
|
160
|
+
Stream.runCollect,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
yield* reconcileAlarm;
|
|
164
|
+
return fired;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Set the DO alarm to the earliest pending event, or clear it if none remain.
|
|
169
|
+
*/
|
|
170
|
+
const reconcileAlarm: Effect.Effect<void, never, DurableObjectState> =
|
|
171
|
+
Effect.gen(function* () {
|
|
172
|
+
const ctx = yield* DurableObjectState;
|
|
173
|
+
|
|
174
|
+
const next = yield* (yield* ctx.storage.sql.exec<{
|
|
175
|
+
run_at: number;
|
|
176
|
+
}>(
|
|
177
|
+
`SELECT run_at FROM alchemy_scheduled_events ORDER BY run_at ASC LIMIT 1`,
|
|
178
|
+
)).pipe(Stream.take(1), Stream.runHead);
|
|
179
|
+
|
|
180
|
+
if (Option.isSome(next)) {
|
|
181
|
+
yield* ctx.storage.setAlarm(next.value.run_at);
|
|
182
|
+
} else {
|
|
183
|
+
yield* ctx.storage.deleteAlarm();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
@@ -2,7 +2,7 @@ import type * as cf from "@cloudflare/workers-types";
|
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as HttpBody from "effect/unstable/http/HttpBody";
|
|
4
4
|
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
|
|
5
|
-
import { DurableObjectState } from "./
|
|
5
|
+
import { DurableObjectState } from "./DurableObjectState.ts";
|
|
6
6
|
|
|
7
7
|
export type RawWebSocket = cf.WebSocket;
|
|
8
8
|
|