alchemy 2.0.0-beta.3 → 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} +170 -170
- 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 +38 -38
- 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/ContainerApplication.ts +3 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +2 -2
- package/src/Cloudflare/Website/StaticSite.ts +44 -15
- package/src/Cloudflare/Website/Vite.ts +34 -12
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +230 -370
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- 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 +261 -61
- package/src/Cloudflare/Workers/index.ts +3 -0
- package/src/Construct.ts +2 -2
- package/src/GitHub/Secret.ts +52 -7
- package/src/GitHub/Variable.ts +45 -2
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- 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 +4 -1
- package/bin/alchemy-effect.js.map +0 -1
- 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,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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|