alchemy 2.0.0-beta.22 → 2.0.0-beta.24
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.js +12590 -2452
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/cli.js +18 -0
- package/bin/commands/bootstrap.ts +88 -4
- package/bin/commands/deploy.ts +54 -20
- package/bin/commands/dev.ts +45 -0
- package/bin/commands/logs.ts +1 -1
- package/bin/commands/tail.ts +1 -1
- package/bin/exec.ts +22 -0
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +25 -21
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/AWS/Lambda/HttpServer.d.ts.map +1 -1
- package/lib/AWS/Lambda/HttpServer.js +3 -1
- package/lib/AWS/Lambda/HttpServer.js.map +1 -1
- package/lib/AdoptPolicy.d.ts +2 -2
- package/lib/AdoptPolicy.d.ts.map +1 -1
- package/lib/AdoptPolicy.js.map +1 -1
- package/lib/AlchemyContext.d.ts +1 -0
- package/lib/AlchemyContext.d.ts.map +1 -1
- package/lib/AlchemyContext.js +1 -0
- package/lib/AlchemyContext.js.map +1 -1
- package/lib/Build/Command.d.ts.map +1 -1
- package/lib/Build/Command.js +8 -1
- package/lib/Build/Command.js.map +1 -1
- package/lib/Bundle/Bundle.d.ts +21 -9
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +29 -15
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Cloudflare/Container/Container.d.ts +55 -2
- package/lib/Cloudflare/Container/Container.d.ts.map +1 -1
- package/lib/Cloudflare/Container/Container.js +55 -2
- package/lib/Cloudflare/Container/Container.js.map +1 -1
- package/lib/Cloudflare/D1/D1Export.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Export.js.map +1 -1
- package/lib/Cloudflare/D1/D1SqlFile.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1SqlFile.js.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
- package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
- package/lib/Cloudflare/Local/Sidecar.js +24 -0
- package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
- package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
- package/lib/Cloudflare/Local/SidecarServer.js +27 -0
- package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
- package/lib/Cloudflare/Queue/Queue.d.ts +34 -4
- package/lib/Cloudflare/Queue/Queue.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/Queue.js +34 -4
- package/lib/Cloudflare/Queue/Queue.js.map +1 -1
- package/lib/Cloudflare/Queue/QueueBinding.d.ts +33 -0
- package/lib/Cloudflare/Queue/QueueBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/QueueBinding.js +33 -0
- package/lib/Cloudflare/Queue/QueueBinding.js.map +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
- package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
- package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +35 -0
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +187 -45
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/StateStore/index.d.ts +1 -1
- package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/index.js +1 -1
- package/lib/Cloudflare/StateStore/index.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Assets.js +6 -1
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
- package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/Compatibility.js +10 -0
- package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +103 -6
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js +103 -6
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
- package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +54 -7
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
- package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
- package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/WorkerName.js +9 -0
- package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
- package/lib/Cloudflare/Workers/Workflow.d.ts +94 -0
- package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.js +94 -0
- package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
- package/lib/Cloudflare/Workers/index.d.ts +1 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +1 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Platform.d.ts +1 -1
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js +1 -1
- package/lib/Platform.js.map +1 -1
- package/lib/Sidecar/Lock.d.ts +24 -0
- package/lib/Sidecar/Lock.d.ts.map +1 -0
- package/lib/Sidecar/Lock.js +102 -0
- package/lib/Sidecar/Lock.js.map +1 -0
- package/lib/Sidecar/RpcClient.d.ts +15 -0
- package/lib/Sidecar/RpcClient.d.ts.map +1 -0
- package/lib/Sidecar/RpcClient.js +75 -0
- package/lib/Sidecar/RpcClient.js.map +1 -0
- package/lib/Sidecar/RpcHandler.d.ts +48 -0
- package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
- package/lib/Sidecar/RpcHandler.js +64 -0
- package/lib/Sidecar/RpcHandler.js.map +1 -0
- package/lib/Sidecar/RpcPaths.d.ts +14 -0
- package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
- package/lib/Sidecar/RpcPaths.js +22 -0
- package/lib/Sidecar/RpcPaths.js.map +1 -0
- package/lib/Sidecar/RpcServer.d.ts +10 -0
- package/lib/Sidecar/RpcServer.d.ts.map +1 -0
- package/lib/Sidecar/RpcServer.js +50 -0
- package/lib/Sidecar/RpcServer.js.map +1 -0
- package/lib/Sidecar/RpcTransport.d.ts +22 -0
- package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
- package/lib/Sidecar/RpcTransport.js +81 -0
- package/lib/Sidecar/RpcTransport.js.map +1 -0
- package/lib/Telemetry/Metrics.d.ts +15 -0
- package/lib/Telemetry/Metrics.d.ts.map +1 -1
- package/lib/Telemetry/Metrics.js +20 -0
- package/lib/Telemetry/Metrics.js.map +1 -1
- package/lib/Util/PlatformServices.d.ts +3 -0
- package/lib/Util/PlatformServices.d.ts.map +1 -1
- package/lib/Util/PlatformServices.js +13 -0
- package/lib/Util/PlatformServices.js.map +1 -1
- package/lib/Util/layer-scoped.d.ts +16 -0
- package/lib/Util/layer-scoped.d.ts.map +1 -0
- package/lib/Util/layer-scoped.js +14 -0
- package/lib/Util/layer-scoped.js.map +1 -0
- package/package.json +4 -2
- package/src/AWS/Lambda/Function.ts +30 -24
- package/src/AWS/Lambda/HttpServer.ts +3 -1
- package/src/AdoptPolicy.ts +9 -9
- package/src/AlchemyContext.ts +2 -0
- package/src/Build/Command.ts +8 -1
- package/src/Bundle/Bundle.ts +65 -25
- package/src/Cloudflare/Container/Container.ts +55 -2
- package/src/Cloudflare/D1/D1Export.ts +1 -5
- package/src/Cloudflare/D1/D1SqlFile.ts +1 -3
- package/src/Cloudflare/Local/Sidecar.ts +44 -0
- package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
- package/src/Cloudflare/Local/SidecarServer.ts +57 -0
- package/src/Cloudflare/Queue/Queue.ts +34 -4
- package/src/Cloudflare/Queue/QueueBinding.ts +33 -0
- package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
- package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
- package/src/Cloudflare/StateStore/State.ts +281 -64
- package/src/Cloudflare/StateStore/index.ts +1 -1
- package/src/Cloudflare/Workers/Assets.ts +10 -1
- package/src/Cloudflare/Workers/Compatibility.ts +12 -0
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +103 -6
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
- package/src/Cloudflare/Workers/Worker.ts +102 -19
- package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
- package/src/Cloudflare/Workers/WorkerName.ts +10 -0
- package/src/Cloudflare/Workers/Workflow.ts +94 -0
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Platform.ts +2 -2
- package/src/Sidecar/Lock.ts +190 -0
- package/src/Sidecar/RpcClient.ts +141 -0
- package/src/Sidecar/RpcHandler.ts +169 -0
- package/src/Sidecar/RpcPaths.ts +29 -0
- package/src/Sidecar/RpcServer.ts +80 -0
- package/src/Sidecar/RpcTransport.ts +85 -0
- package/src/Telemetry/Metrics.ts +38 -0
- package/src/Util/PlatformServices.ts +19 -0
- package/src/Util/layer-scoped.ts +25 -0
|
@@ -301,6 +301,51 @@ export class DurableObjectNamespaceScope extends Context.Service<
|
|
|
301
301
|
* };
|
|
302
302
|
* ```
|
|
303
303
|
*
|
|
304
|
+
* @section Returning Streams from RPC
|
|
305
|
+
* RPC methods can return an Effect `Stream` and the caller will see
|
|
306
|
+
* the chunks as they're produced. Combine with `Stream.schedule` to
|
|
307
|
+
* pace emission, or with `Stream.fromQueue` to bridge an inbound
|
|
308
|
+
* subscription.
|
|
309
|
+
*
|
|
310
|
+
* @example Streaming sequential numbers
|
|
311
|
+
* ```typescript
|
|
312
|
+
* import * as Schedule from "effect/Schedule";
|
|
313
|
+
* import * as Stream from "effect/Stream";
|
|
314
|
+
*
|
|
315
|
+
* return {
|
|
316
|
+
* tick: (n: number) =>
|
|
317
|
+
* Stream.iterate(0, (i) => i + 1).pipe(
|
|
318
|
+
* Stream.take(n),
|
|
319
|
+
* Stream.schedule(Schedule.spaced("100 millis")),
|
|
320
|
+
* ),
|
|
321
|
+
* };
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* @example Forwarding the stream as a chunked HTTP response
|
|
325
|
+
* ```typescript
|
|
326
|
+
* // in a Worker fetch handler
|
|
327
|
+
* const counter = counters.getByName("tick");
|
|
328
|
+
* const stream = counter.tick(5).pipe(
|
|
329
|
+
* Stream.map((i) => `${i}\n`),
|
|
330
|
+
* Stream.encodeText,
|
|
331
|
+
* );
|
|
332
|
+
* return HttpServerResponse.stream(stream, {
|
|
333
|
+
* headers: { "content-type": "text/plain" },
|
|
334
|
+
* });
|
|
335
|
+
* ```
|
|
336
|
+
*
|
|
337
|
+
* @section Worker → DO HTTP forwarding
|
|
338
|
+
* In addition to RPC methods, the typed stub exposes a `fetch`
|
|
339
|
+
* method that forwards an `HttpServerRequest` straight to the DO.
|
|
340
|
+
* The DO's own `fetch` Effect produces the response — useful for
|
|
341
|
+
* WebSocket upgrades and other request-shaped interactions.
|
|
342
|
+
*
|
|
343
|
+
* @example Forwarding an HTTP request to a DO
|
|
344
|
+
* ```typescript
|
|
345
|
+
* const room = rooms.getByName(roomId);
|
|
346
|
+
* return yield* room.fetch(request);
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
304
349
|
* @section Accessing Instance State
|
|
305
350
|
* Each Durable Object instance has its own transactional key-value
|
|
306
351
|
* storage via `Cloudflare.DurableObjectState`. Use `storage.get` and
|
|
@@ -356,14 +401,66 @@ export class DurableObjectNamespaceScope extends Context.Service<
|
|
|
356
401
|
* ```
|
|
357
402
|
*
|
|
358
403
|
* @example Recovering sessions after hibernation
|
|
404
|
+
* Place the rehydration loop **inside the inner `Effect.gen`** so
|
|
405
|
+
* it runs every time the DO instance is reconstructed (including
|
|
406
|
+
* after Cloudflare wakes the DO from hibernation).
|
|
407
|
+
*
|
|
359
408
|
* ```typescript
|
|
360
|
-
*
|
|
361
|
-
*
|
|
409
|
+
* return Effect.gen(function* () {
|
|
410
|
+
* const state = yield* Cloudflare.DurableObjectState;
|
|
411
|
+
* const sessions = new Map<string, Cloudflare.DurableWebSocket>();
|
|
412
|
+
*
|
|
413
|
+
* // Rehydrate the in-memory session map after hibernation.
|
|
414
|
+
* for (const socket of yield* state.getWebSockets()) {
|
|
415
|
+
* const data = socket.deserializeAttachment<{ id: string }>();
|
|
416
|
+
* if (data) sessions.set(data.id, socket);
|
|
417
|
+
* }
|
|
362
418
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
419
|
+
* return {
|
|
420
|
+
* fetch: Effect.gen(function* () {
|
|
421
|
+
* const [response, socket] = yield* Cloudflare.upgrade();
|
|
422
|
+
* const id = crypto.randomUUID();
|
|
423
|
+
* socket.serializeAttachment({ id });
|
|
424
|
+
* sessions.set(id, socket);
|
|
425
|
+
* return response;
|
|
426
|
+
* }),
|
|
427
|
+
* webSocketMessage: Effect.fnUntraced(function* (socket, message) {
|
|
428
|
+
* const text =
|
|
429
|
+
* typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
430
|
+
* for (const peer of sessions.values()) {
|
|
431
|
+
* yield* peer.send(text);
|
|
432
|
+
* }
|
|
433
|
+
* }),
|
|
434
|
+
* };
|
|
435
|
+
* });
|
|
436
|
+
* ```
|
|
437
|
+
*
|
|
438
|
+
* @section Scheduled Alarms
|
|
439
|
+
* Each Durable Object can have a single alarm timestamp. Alchemy
|
|
440
|
+
* layers a small SQLite-backed scheduler on top via
|
|
441
|
+
* `Cloudflare.scheduleEvent` and `Cloudflare.processScheduledEvents`,
|
|
442
|
+
* so you can register many named events with arbitrary payloads and
|
|
443
|
+
* fire them from a single `alarm` handler.
|
|
444
|
+
*
|
|
445
|
+
* @example Scheduling and processing events
|
|
446
|
+
* ```typescript
|
|
447
|
+
* // schedule from a request or message handler
|
|
448
|
+
* yield* Cloudflare.scheduleEvent(
|
|
449
|
+
* "reminder-1",
|
|
450
|
+
* new Date(Date.now() + 60_000),
|
|
451
|
+
* { message: "your meeting starts in a minute" },
|
|
452
|
+
* );
|
|
453
|
+
*
|
|
454
|
+
* return {
|
|
455
|
+
* alarm: () =>
|
|
456
|
+
* Effect.gen(function* () {
|
|
457
|
+
* const fired = yield* Cloudflare.processScheduledEvents;
|
|
458
|
+
* for (const event of fired) {
|
|
459
|
+
* const payload = event.payload as { message: string };
|
|
460
|
+
* // dispatch / broadcast / persist...
|
|
461
|
+
* }
|
|
462
|
+
* }),
|
|
463
|
+
* };
|
|
367
464
|
* ```
|
|
368
465
|
*
|
|
369
466
|
* @section Using from a Worker
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Redacted from "effect/Redacted";
|
|
3
|
+
import * as Provider from "../../Provider.ts";
|
|
4
|
+
import type { ResourceBinding } from "../../Resource.ts";
|
|
5
|
+
import { Stack } from "../../Stack.ts";
|
|
6
|
+
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
7
|
+
import { Sidecar } from "../Local/Sidecar.ts";
|
|
8
|
+
import { getCompatibility } from "./Compatibility.ts";
|
|
9
|
+
import { Worker, type WorkerBinding, type WorkerProps } from "./Worker.ts";
|
|
10
|
+
import { createWorkerName } from "./WorkerName.ts";
|
|
11
|
+
|
|
12
|
+
export const LocalWorkerProvider = () =>
|
|
13
|
+
Provider.effect(
|
|
14
|
+
Worker,
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const { accountId } = yield* CloudflareEnvironment;
|
|
17
|
+
const stack = yield* Stack;
|
|
18
|
+
const sidecar = yield* Sidecar;
|
|
19
|
+
|
|
20
|
+
const run = Effect.fn(function* (
|
|
21
|
+
id: string,
|
|
22
|
+
props: WorkerProps,
|
|
23
|
+
bindings: ResourceBinding<Worker["Binding"]>[],
|
|
24
|
+
) {
|
|
25
|
+
const name = yield* createWorkerName(id, props.name);
|
|
26
|
+
const workerBindings: WorkerBinding[] = [];
|
|
27
|
+
const durableObjectNamespaces: Record<string, string> = {};
|
|
28
|
+
for (const { sid, data } of bindings) {
|
|
29
|
+
for (const binding of data.bindings ?? []) {
|
|
30
|
+
workerBindings.push(binding);
|
|
31
|
+
if (binding.type === "durable_object_namespace") {
|
|
32
|
+
durableObjectNamespaces[binding.name] = sid;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const [key, value] of Object.entries(props.env ?? {})) {
|
|
37
|
+
if (Redacted.isRedacted(value)) {
|
|
38
|
+
workerBindings.push({
|
|
39
|
+
type: "secret_text",
|
|
40
|
+
name: key,
|
|
41
|
+
text: Redacted.value(value),
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
workerBindings.push({
|
|
45
|
+
type: "plain_text",
|
|
46
|
+
name: key,
|
|
47
|
+
text: value,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const result = yield* sidecar.serve({
|
|
52
|
+
id,
|
|
53
|
+
name,
|
|
54
|
+
main: props.main,
|
|
55
|
+
compatibility: getCompatibility(props),
|
|
56
|
+
entry: props.isExternal
|
|
57
|
+
? {
|
|
58
|
+
kind: "external",
|
|
59
|
+
}
|
|
60
|
+
: {
|
|
61
|
+
kind: "effect",
|
|
62
|
+
exports: (props.exports ?? {}) as any,
|
|
63
|
+
},
|
|
64
|
+
stack: { name: stack.name, stage: stack.stage },
|
|
65
|
+
bindings: workerBindings,
|
|
66
|
+
durableObjectNamespaces: Object.entries(durableObjectNamespaces).map(
|
|
67
|
+
([className, namespaceId]) => ({
|
|
68
|
+
className,
|
|
69
|
+
uniqueKey: namespaceId,
|
|
70
|
+
sql: true,
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
workerId: name,
|
|
76
|
+
workerName: name,
|
|
77
|
+
logpush: undefined,
|
|
78
|
+
url: result.address,
|
|
79
|
+
tags: [],
|
|
80
|
+
durableObjectNamespaces,
|
|
81
|
+
domains: [],
|
|
82
|
+
accountId,
|
|
83
|
+
} satisfies Worker["Attributes"];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
diff: () => Effect.succeed({ action: "update" }),
|
|
88
|
+
create: ({ id, news, bindings }) => run(id, news, bindings),
|
|
89
|
+
update: ({ id, news, bindings }) => run(id, news, bindings),
|
|
90
|
+
delete: ({ output }) => sidecar.stop(output.workerName),
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
@@ -22,6 +22,8 @@ import { pathToFileURL } from "node:url";
|
|
|
22
22
|
import type * as rolldown from "rolldown";
|
|
23
23
|
import Sonda from "sonda/rolldown";
|
|
24
24
|
import type * as vite from "vite";
|
|
25
|
+
import { AdoptPolicy } from "../../AdoptPolicy.ts";
|
|
26
|
+
import { AlchemyContext } from "../../AlchemyContext.ts";
|
|
25
27
|
import * as Artifacts from "../../Artifacts.ts";
|
|
26
28
|
import * as Binding from "../../Binding.ts";
|
|
27
29
|
import {
|
|
@@ -53,6 +55,7 @@ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
|
53
55
|
import { D1Database } from "../D1/D1Database.ts";
|
|
54
56
|
import { fromCloudflareFetcher } from "../Fetcher.ts";
|
|
55
57
|
import type { KVNamespace } from "../KV/KVNamespace.ts";
|
|
58
|
+
import { SidecarLive } from "../Local/Sidecar.ts";
|
|
56
59
|
import { CloudflareLogs } from "../Logs.ts";
|
|
57
60
|
import type { Providers } from "../Providers.ts";
|
|
58
61
|
import type { Queue as CloudflareQueue } from "../Queue/Queue.ts";
|
|
@@ -72,6 +75,7 @@ import {
|
|
|
72
75
|
type DurableObjectNamespaceLike,
|
|
73
76
|
} from "./DurableObjectNamespace.ts";
|
|
74
77
|
import { workersHttpHandler } from "./HttpServer.ts";
|
|
78
|
+
import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
|
|
75
79
|
import { Request } from "./Request.ts";
|
|
76
80
|
import { makeRpcStub } from "./Rpc.ts";
|
|
77
81
|
import { isWorkflowExport } from "./Workflow.ts";
|
|
@@ -966,7 +970,30 @@ function getDurableObjectTagMap(tags: ReadonlyArray<string>) {
|
|
|
966
970
|
);
|
|
967
971
|
}
|
|
968
972
|
|
|
973
|
+
const selectLayer = <
|
|
974
|
+
LayerLive extends Layer.Layer<any, any, any>,
|
|
975
|
+
LayerDev extends Layer.Layer<any, any, any>,
|
|
976
|
+
>(input: {
|
|
977
|
+
live: () => LayerLive;
|
|
978
|
+
dev: () => LayerDev;
|
|
979
|
+
}): Layer.Layer<
|
|
980
|
+
Layer.Success<LayerLive | LayerDev>,
|
|
981
|
+
Layer.Error<LayerLive | LayerDev>,
|
|
982
|
+
Layer.Services<LayerLive | LayerDev> | AlchemyContext
|
|
983
|
+
> =>
|
|
984
|
+
Layer.unwrap(
|
|
985
|
+
AlchemyContext.useSync((context) =>
|
|
986
|
+
context.dev ? input.dev() : input.live(),
|
|
987
|
+
),
|
|
988
|
+
);
|
|
989
|
+
|
|
969
990
|
export const WorkerProvider = () =>
|
|
991
|
+
selectLayer({
|
|
992
|
+
live: LiveWorkerProvider,
|
|
993
|
+
dev: () => Layer.provide(LocalWorkerProvider(), SidecarLive),
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
export const LiveWorkerProvider = () =>
|
|
970
997
|
Provider.effect(
|
|
971
998
|
Worker,
|
|
972
999
|
Effect.gen(function* () {
|
|
@@ -1094,27 +1121,83 @@ export const WorkerProvider = () =>
|
|
|
1094
1121
|
|
|
1095
1122
|
if (desired.length === 0) return [];
|
|
1096
1123
|
|
|
1124
|
+
const adoptEnabled = yield* Effect.serviceOption(AdoptPolicy).pipe(
|
|
1125
|
+
Effect.map(Option.getOrElse(() => false)),
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1097
1128
|
const zoneCache = new Map<string, string>();
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Attach `hostname` to `scriptName`, recovering from a "hostname
|
|
1132
|
+
* already in use" conflict when adoption is enabled.
|
|
1133
|
+
*
|
|
1134
|
+
* The Cloudflare API rejects a `PUT /workers/domains` whose
|
|
1135
|
+
* hostname is already registered (even when the existing entry
|
|
1136
|
+
* points at the same Worker, which is exactly what happens
|
|
1137
|
+
* after a state-store wipe). The SDK surfaces this as the
|
|
1138
|
+
* unmatched `UnknownCloudflareError` channel because there's
|
|
1139
|
+
* no dedicated error tag for it. With adoption opted-in we
|
|
1140
|
+
* list domains for the hostname; if the existing entry
|
|
1141
|
+
* already targets *this* Worker we adopt it in place. We
|
|
1142
|
+
* never transfer a hostname off another Worker — that would
|
|
1143
|
+
* silently re-route someone else's traffic.
|
|
1144
|
+
*/
|
|
1145
|
+
const attachDomain = Effect.fnUntraced(function* (hostname: string) {
|
|
1146
|
+
const zoneId = yield* inferZoneIdForHostname(hostname, zoneCache);
|
|
1147
|
+
return yield* putDomain({
|
|
1148
|
+
accountId,
|
|
1149
|
+
hostname,
|
|
1150
|
+
service: scriptName,
|
|
1151
|
+
zoneId,
|
|
1152
|
+
}).pipe(
|
|
1153
|
+
Effect.map((res) => ({
|
|
1154
|
+
hostname,
|
|
1155
|
+
id: res.id ?? "",
|
|
1156
|
+
zoneId: res.zoneId ?? zoneId,
|
|
1157
|
+
})),
|
|
1158
|
+
Effect.catchTag("UnknownCloudflareError", (err) =>
|
|
1159
|
+
adoptEnabled && /already in use/i.test(err.message)
|
|
1160
|
+
? adoptDomainIfSameService(hostname, zoneId, err)
|
|
1161
|
+
: Effect.fail(err),
|
|
1162
|
+
),
|
|
1163
|
+
);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const adoptDomainIfSameService = Effect.fnUntraced(function* (
|
|
1167
|
+
hostname: string,
|
|
1168
|
+
zoneId: string,
|
|
1169
|
+
originalError: { message: string },
|
|
1170
|
+
) {
|
|
1171
|
+
const existing = yield* listDomains({
|
|
1172
|
+
accountId,
|
|
1173
|
+
hostname,
|
|
1174
|
+
}).pipe(
|
|
1175
|
+
Effect.map((r) =>
|
|
1176
|
+
(r.result ?? []).find((d) => d.hostname === hostname),
|
|
1177
|
+
),
|
|
1178
|
+
);
|
|
1179
|
+
if (existing?.id && existing.service === scriptName) {
|
|
1180
|
+
return {
|
|
1181
|
+
hostname,
|
|
1182
|
+
id: existing.id,
|
|
1183
|
+
zoneId: existing.zoneId ?? zoneId,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
// The hostname exists but on a different Worker. Refuse to
|
|
1187
|
+
// hijack it; surface a more actionable message than the raw
|
|
1188
|
+
// Cloudflare error.
|
|
1189
|
+
return yield* Effect.die(
|
|
1190
|
+
new Error(
|
|
1191
|
+
`Cannot attach hostname '${hostname}' to Worker '${scriptName}': ` +
|
|
1192
|
+
`it is already attached to Worker '${existing?.service ?? "<unknown>"}'. ` +
|
|
1193
|
+
`Detach it from that Worker first, or pick a different hostname. ` +
|
|
1194
|
+
`(Cloudflare reported: ${originalError.message})`,
|
|
1195
|
+
),
|
|
1196
|
+
);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1098
1199
|
const applied = yield* Effect.all(
|
|
1099
|
-
desired.map(
|
|
1100
|
-
Effect.gen(function* () {
|
|
1101
|
-
const zoneId = yield* inferZoneIdForHostname(
|
|
1102
|
-
hostname,
|
|
1103
|
-
zoneCache,
|
|
1104
|
-
);
|
|
1105
|
-
const res = yield* putDomain({
|
|
1106
|
-
accountId,
|
|
1107
|
-
hostname,
|
|
1108
|
-
service: scriptName,
|
|
1109
|
-
zoneId,
|
|
1110
|
-
});
|
|
1111
|
-
return {
|
|
1112
|
-
hostname,
|
|
1113
|
-
id: res.id ?? "",
|
|
1114
|
-
zoneId: res.zoneId ?? zoneId,
|
|
1115
|
-
};
|
|
1116
|
-
}),
|
|
1117
|
-
),
|
|
1200
|
+
desired.map(attachDomain),
|
|
1118
1201
|
{ concurrency: "unbounded" },
|
|
1119
1202
|
);
|
|
1120
1203
|
return applied;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import cloudflareRolldown from "@distilled.cloud/cloudflare-rolldown-plugin";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import { flow } from "effect/Function";
|
|
5
|
+
import type * as Path from "effect/Path";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import type * as rolldown from "rolldown";
|
|
8
|
+
import * as Artifacts from "../../Artifacts.ts";
|
|
9
|
+
import * as Bundle from "../../Bundle/Bundle.ts";
|
|
10
|
+
import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
|
|
11
|
+
import { Self } from "../../Self.ts";
|
|
12
|
+
import {
|
|
13
|
+
isDurableObjectExport,
|
|
14
|
+
type DurableObjectExport,
|
|
15
|
+
} from "./DurableObjectNamespace.ts";
|
|
16
|
+
import { isWorkflowExport, type WorkflowExport } from "./Workflow.ts";
|
|
17
|
+
|
|
18
|
+
export interface WorkerBundleOptions {
|
|
19
|
+
id: string;
|
|
20
|
+
main: string;
|
|
21
|
+
compatibility: {
|
|
22
|
+
date: string;
|
|
23
|
+
flags: string[];
|
|
24
|
+
};
|
|
25
|
+
entry:
|
|
26
|
+
| {
|
|
27
|
+
kind: "external";
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
kind: "effect";
|
|
31
|
+
exports: Record<string, DurableObjectExport | WorkflowExport>;
|
|
32
|
+
};
|
|
33
|
+
stack: { name: string; stage: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const WorkerBundle = Effect.gen(function* () {
|
|
37
|
+
const fs = yield* FileSystem.FileSystem;
|
|
38
|
+
const context = yield* Effect.context<FileSystem.FileSystem | Path.Path>();
|
|
39
|
+
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
40
|
+
|
|
41
|
+
const makeOptions = Effect.fnUntraced(function* (
|
|
42
|
+
options: WorkerBundleOptions,
|
|
43
|
+
) {
|
|
44
|
+
const realMain = yield* fs.realPath(options.main).pipe(
|
|
45
|
+
Effect.mapError(
|
|
46
|
+
(cause) =>
|
|
47
|
+
new Bundle.BundleError({
|
|
48
|
+
message: `Failed to find real path for bundle: ${options.main}`,
|
|
49
|
+
cause,
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
const inputOptions: rolldown.InputOptions = {
|
|
54
|
+
input: realMain,
|
|
55
|
+
cwd: yield* findCwdForBundle(realMain).pipe(
|
|
56
|
+
Effect.mapError(
|
|
57
|
+
(cause) =>
|
|
58
|
+
new Bundle.BundleError({
|
|
59
|
+
message: `Failed to find cwd for bundle: ${realMain}`,
|
|
60
|
+
cause,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
Effect.provide(context),
|
|
64
|
+
),
|
|
65
|
+
plugins: [
|
|
66
|
+
cloudflareRolldown({
|
|
67
|
+
compatibilityDate: options.compatibility.date,
|
|
68
|
+
compatibilityFlags: options.compatibility.flags,
|
|
69
|
+
}),
|
|
70
|
+
options.entry.kind === "effect"
|
|
71
|
+
? virtualEntryPlugin(
|
|
72
|
+
makeEffectVirtualEntry(options.entry.exports, options.stack),
|
|
73
|
+
)
|
|
74
|
+
: undefined,
|
|
75
|
+
],
|
|
76
|
+
checks: {
|
|
77
|
+
// Suppress unresolved import warnings for unrelated AWS packages
|
|
78
|
+
unresolvedImport: false,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const outputOptions: rolldown.OutputOptions = {
|
|
82
|
+
format: "esm",
|
|
83
|
+
sourcemap: "hidden",
|
|
84
|
+
minify: true,
|
|
85
|
+
keepNames: true,
|
|
86
|
+
dir: `.alchemy/bundles/${options.id}`,
|
|
87
|
+
};
|
|
88
|
+
return { inputOptions, outputOptions };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
build: flow(
|
|
93
|
+
makeOptions,
|
|
94
|
+
Effect.flatMap((resolved) =>
|
|
95
|
+
Bundle.build(resolved.inputOptions, resolved.outputOptions),
|
|
96
|
+
),
|
|
97
|
+
Artifacts.cached("build"),
|
|
98
|
+
),
|
|
99
|
+
watch: flow(
|
|
100
|
+
makeOptions,
|
|
101
|
+
Stream.fromEffect,
|
|
102
|
+
Stream.flatMap((resolved) =>
|
|
103
|
+
Bundle.watch(resolved.inputOptions, resolved.outputOptions),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const makeEffectVirtualEntry = (
|
|
110
|
+
exports: Record<string, DurableObjectExport | WorkflowExport>,
|
|
111
|
+
stack: { name: string; stage: string },
|
|
112
|
+
) => {
|
|
113
|
+
const durableObjectClasses: string[] = [];
|
|
114
|
+
const workflowClasses: string[] = [];
|
|
115
|
+
for (const [className, entry] of Object.entries(exports)) {
|
|
116
|
+
if (isDurableObjectExport(entry)) {
|
|
117
|
+
durableObjectClasses.push(className);
|
|
118
|
+
} else if (isWorkflowExport(entry)) {
|
|
119
|
+
workflowClasses.push(className);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return (importPath: string) => `import * as Config from "effect/Config";
|
|
123
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
124
|
+
import * as Console from "effect/Console";
|
|
125
|
+
import * as Effect from "effect/Effect";
|
|
126
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
127
|
+
import * as Layer from "effect/Layer";
|
|
128
|
+
import * as Logger from "effect/Logger";
|
|
129
|
+
import * as Context from "effect/Context";
|
|
130
|
+
import * as Stream from "effect/Stream";
|
|
131
|
+
|
|
132
|
+
import { env, DurableObject${workflowClasses.length > 0 ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
|
|
133
|
+
import { MinimumLogLevel } from "effect/References";
|
|
134
|
+
import { NodeServices } from "@effect/platform-node";
|
|
135
|
+
import { Stack } from "alchemy/Stack";
|
|
136
|
+
import { WorkerEnvironment, makeDurableObjectBridge${workflowClasses.length > 0 ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
|
|
137
|
+
|
|
138
|
+
import entry from "${importPath}";
|
|
139
|
+
|
|
140
|
+
const tag = Context.Service("${Self.key}")
|
|
141
|
+
const layer =
|
|
142
|
+
typeof entry?.build === "function"
|
|
143
|
+
? entry
|
|
144
|
+
: Layer.effect(tag, typeof entry?.asEffect === "function" ? entry.asEffect() : entry);
|
|
145
|
+
|
|
146
|
+
const platform = Layer.mergeAll(
|
|
147
|
+
NodeServices.layer,
|
|
148
|
+
FetchHttpClient.layer,
|
|
149
|
+
// TODO(sam): wire this up to telemetry more directly
|
|
150
|
+
Logger.layer([Logger.consolePretty()]),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const stack = Layer.succeed(
|
|
154
|
+
Stack,
|
|
155
|
+
{
|
|
156
|
+
name: "${stack.name}",
|
|
157
|
+
stage: "${stack.stage}",
|
|
158
|
+
bindings: {},
|
|
159
|
+
resources: {}
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const exportsEffect = tag.asEffect().pipe(
|
|
164
|
+
Effect.flatMap(func => func.ExecutionContext.exports),
|
|
165
|
+
Effect.map(exports => exports),
|
|
166
|
+
Effect.provide(
|
|
167
|
+
layer.pipe(
|
|
168
|
+
Layer.provideMerge(stack),
|
|
169
|
+
// TODO(sam): additional credentials?
|
|
170
|
+
Layer.provideMerge(platform),
|
|
171
|
+
Layer.provideMerge(
|
|
172
|
+
Layer.succeed(
|
|
173
|
+
ConfigProvider.ConfigProvider,
|
|
174
|
+
ConfigProvider.orElse(
|
|
175
|
+
ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
|
|
176
|
+
ConfigProvider.fromUnknown(env),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
),
|
|
180
|
+
Layer.provideMerge(
|
|
181
|
+
Layer.succeed(
|
|
182
|
+
WorkerEnvironment,
|
|
183
|
+
env,
|
|
184
|
+
)
|
|
185
|
+
),
|
|
186
|
+
Layer.provideMerge(
|
|
187
|
+
Layer.succeed(
|
|
188
|
+
MinimumLogLevel,
|
|
189
|
+
env.DEBUG ? "Debug" : "Info",
|
|
190
|
+
)
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
),
|
|
194
|
+
Effect.scoped
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// TODO(sam): we could kick this off during module init, but any I/O will break deploy
|
|
198
|
+
// let exportsPromise = Effect.runPromise(exportsEffect);
|
|
199
|
+
|
|
200
|
+
// for now, we delay initializing the worker until the first request
|
|
201
|
+
let exportsPromise;
|
|
202
|
+
|
|
203
|
+
// don't initialize the workerEffect during module init because Cloudflare does not allow I/O during module init
|
|
204
|
+
// we cache it synchronously (??=) to guarnatee only one initialization ever happens
|
|
205
|
+
const getExports = () => (exportsPromise ??= Effect.runPromise(exportsEffect))
|
|
206
|
+
const getExport = (name) => getExports().then(exports => exports[name]?.make)
|
|
207
|
+
const worker = () => getExports().then(exports => exports.default)
|
|
208
|
+
|
|
209
|
+
export default Object.fromEntries(ExportedHandlerMethods.map(
|
|
210
|
+
method => [method, async (...args) => (await worker())[method](...args)])
|
|
211
|
+
) satisfies Required<cf.ExportedHandler>;
|
|
212
|
+
|
|
213
|
+
// export class proxy stubs for Durable Objects and Workflows
|
|
214
|
+
${[
|
|
215
|
+
...(durableObjectClasses.length > 0
|
|
216
|
+
? [
|
|
217
|
+
"const DurableObjectBridge = makeDurableObjectBridge(DurableObject, getExport);",
|
|
218
|
+
...durableObjectClasses.map(
|
|
219
|
+
(id) =>
|
|
220
|
+
`export class ${id} extends DurableObjectBridge("${id}") {}`,
|
|
221
|
+
),
|
|
222
|
+
]
|
|
223
|
+
: []),
|
|
224
|
+
...(workflowClasses.length > 0
|
|
225
|
+
? [
|
|
226
|
+
"const WorkflowBridgeFn = makeWorkflowBridge(WorkflowEntrypoint, getExport);",
|
|
227
|
+
...workflowClasses.map(
|
|
228
|
+
(id) => `export class ${id} extends WorkflowBridgeFn("${id}") {}`,
|
|
229
|
+
),
|
|
230
|
+
]
|
|
231
|
+
: []),
|
|
232
|
+
].join("\n")}
|
|
233
|
+
`;
|
|
234
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { createPhysicalName } from "../../PhysicalName.ts";
|
|
3
|
+
|
|
4
|
+
export const createWorkerName = (id: string, name: string | undefined) =>
|
|
5
|
+
name
|
|
6
|
+
? Effect.succeed(name)
|
|
7
|
+
: createPhysicalName({
|
|
8
|
+
id,
|
|
9
|
+
maxLength: 54,
|
|
10
|
+
}).pipe(Effect.map((name) => name.toLowerCase()));
|