alchemy 2.0.0-beta.23 → 2.0.0-beta.25
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 +12574 -2451
- 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/exec.ts +22 -0
- 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/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/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/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/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 +89 -31
- 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/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/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/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/Local/Sidecar.ts +44 -0
- package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
- package/src/Cloudflare/Local/SidecarServer.ts +57 -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/Compatibility.ts +12 -0
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
- package/src/Cloudflare/Workers/Worker.ts +151 -55
- package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
- package/src/Cloudflare/Workers/WorkerName.ts +10 -0
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Platform.ts +1 -1
- 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
|
@@ -22,6 +22,7 @@ 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 { AlchemyContext } from "../../AlchemyContext.ts";
|
|
25
26
|
import * as Artifacts from "../../Artifacts.ts";
|
|
26
27
|
import * as Binding from "../../Binding.ts";
|
|
27
28
|
import {
|
|
@@ -53,6 +54,7 @@ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
|
53
54
|
import { D1Database } from "../D1/D1Database.ts";
|
|
54
55
|
import { fromCloudflareFetcher } from "../Fetcher.ts";
|
|
55
56
|
import type { KVNamespace } from "../KV/KVNamespace.ts";
|
|
57
|
+
import { SidecarLive } from "../Local/Sidecar.ts";
|
|
56
58
|
import { CloudflareLogs } from "../Logs.ts";
|
|
57
59
|
import type { Providers } from "../Providers.ts";
|
|
58
60
|
import type { Queue as CloudflareQueue } from "../Queue/Queue.ts";
|
|
@@ -72,6 +74,7 @@ import {
|
|
|
72
74
|
type DurableObjectNamespaceLike,
|
|
73
75
|
} from "./DurableObjectNamespace.ts";
|
|
74
76
|
import { workersHttpHandler } from "./HttpServer.ts";
|
|
77
|
+
import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
|
|
75
78
|
import { Request } from "./Request.ts";
|
|
76
79
|
import { makeRpcStub } from "./Rpc.ts";
|
|
77
80
|
import { isWorkflowExport } from "./Workflow.ts";
|
|
@@ -966,7 +969,30 @@ function getDurableObjectTagMap(tags: ReadonlyArray<string>) {
|
|
|
966
969
|
);
|
|
967
970
|
}
|
|
968
971
|
|
|
972
|
+
const selectLayer = <
|
|
973
|
+
LayerLive extends Layer.Layer<any, any, any>,
|
|
974
|
+
LayerDev extends Layer.Layer<any, any, any>,
|
|
975
|
+
>(input: {
|
|
976
|
+
live: () => LayerLive;
|
|
977
|
+
dev: () => LayerDev;
|
|
978
|
+
}): Layer.Layer<
|
|
979
|
+
Layer.Success<LayerLive | LayerDev>,
|
|
980
|
+
Layer.Error<LayerLive | LayerDev>,
|
|
981
|
+
Layer.Services<LayerLive | LayerDev> | AlchemyContext
|
|
982
|
+
> =>
|
|
983
|
+
Layer.unwrap(
|
|
984
|
+
AlchemyContext.useSync((context) =>
|
|
985
|
+
context.dev ? input.dev() : input.live(),
|
|
986
|
+
),
|
|
987
|
+
);
|
|
988
|
+
|
|
969
989
|
export const WorkerProvider = () =>
|
|
990
|
+
selectLayer({
|
|
991
|
+
live: LiveWorkerProvider,
|
|
992
|
+
dev: () => Layer.provide(LocalWorkerProvider(), SidecarLive),
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
export const LiveWorkerProvider = () =>
|
|
970
996
|
Provider.effect(
|
|
971
997
|
Worker,
|
|
972
998
|
Effect.gen(function* () {
|
|
@@ -1078,11 +1104,44 @@ export const WorkerProvider = () =>
|
|
|
1078
1104
|
const reconcileDomains = (
|
|
1079
1105
|
scriptName: string,
|
|
1080
1106
|
desired: string[],
|
|
1081
|
-
|
|
1107
|
+
_previous: Worker["Attributes"]["domains"],
|
|
1082
1108
|
) =>
|
|
1083
1109
|
Effect.gen(function* () {
|
|
1110
|
+
// Always query the live state of domains attached to *this*
|
|
1111
|
+
// Worker rather than trusting `_previous` from local state.
|
|
1112
|
+
// State may have been wiped, populated by another machine, or
|
|
1113
|
+
// simply be out of date. Without this we PUT domains that are
|
|
1114
|
+
// already registered to this same Worker and Cloudflare
|
|
1115
|
+
// returns a confusing "hostname already in use" error.
|
|
1116
|
+
const liveAll = yield* listDomains({
|
|
1117
|
+
accountId,
|
|
1118
|
+
service: scriptName,
|
|
1119
|
+
}).pipe(
|
|
1120
|
+
Effect.map((r) =>
|
|
1121
|
+
(r.result ?? []).flatMap((d) =>
|
|
1122
|
+
d.id && d.hostname && d.zoneId
|
|
1123
|
+
? [
|
|
1124
|
+
{
|
|
1125
|
+
id: d.id,
|
|
1126
|
+
hostname: d.hostname,
|
|
1127
|
+
zoneId: d.zoneId,
|
|
1128
|
+
service: d.service ?? undefined,
|
|
1129
|
+
},
|
|
1130
|
+
]
|
|
1131
|
+
: [],
|
|
1132
|
+
),
|
|
1133
|
+
),
|
|
1134
|
+
Effect.catch(() => Effect.succeed([])),
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1084
1137
|
const desiredSet = new Set(desired);
|
|
1085
|
-
const
|
|
1138
|
+
const liveByHostname = new Map(liveAll.map((d) => [d.hostname, d]));
|
|
1139
|
+
|
|
1140
|
+
// Detach what's no longer wanted. Use the live list so we
|
|
1141
|
+
// don't try to delete domains we no longer track.
|
|
1142
|
+
const toRemove = liveAll.filter(
|
|
1143
|
+
(d) => !desiredSet.has(d.hostname),
|
|
1144
|
+
);
|
|
1086
1145
|
yield* Effect.all(
|
|
1087
1146
|
toRemove.map((d) =>
|
|
1088
1147
|
deleteDomain({ accountId, domainId: d.id }).pipe(
|
|
@@ -1095,26 +1154,62 @@ export const WorkerProvider = () =>
|
|
|
1095
1154
|
if (desired.length === 0) return [];
|
|
1096
1155
|
|
|
1097
1156
|
const zoneCache = new Map<string, string>();
|
|
1157
|
+
|
|
1158
|
+
// Attach `hostname` to this Worker. Skip the PUT entirely if
|
|
1159
|
+
// the hostname is already attached to *this* Worker — that's a
|
|
1160
|
+
// no-op for Cloudflare and avoids the "already in use" 409.
|
|
1161
|
+
// If it's attached to a *different* Worker, refuse with a
|
|
1162
|
+
// clear message rather than silently re-routing traffic.
|
|
1163
|
+
const attachDomain = Effect.fnUntraced(function* (hostname: string) {
|
|
1164
|
+
const live = liveByHostname.get(hostname);
|
|
1165
|
+
if (live) {
|
|
1166
|
+
return {
|
|
1167
|
+
hostname: live.hostname,
|
|
1168
|
+
id: live.id,
|
|
1169
|
+
zoneId: live.zoneId,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Not attached to this Worker — but it could still belong
|
|
1174
|
+
// to another Worker. Check before we try to PUT so we can
|
|
1175
|
+
// emit a helpful error instead of the raw 409.
|
|
1176
|
+
const otherOwner = yield* listDomains({
|
|
1177
|
+
accountId,
|
|
1178
|
+
hostname,
|
|
1179
|
+
}).pipe(
|
|
1180
|
+
Effect.map((r) =>
|
|
1181
|
+
(r.result ?? []).find(
|
|
1182
|
+
(d) => d.hostname === hostname && d.service !== scriptName,
|
|
1183
|
+
),
|
|
1184
|
+
),
|
|
1185
|
+
Effect.catch(() => Effect.succeed(undefined)),
|
|
1186
|
+
);
|
|
1187
|
+
if (otherOwner?.id) {
|
|
1188
|
+
return yield* Effect.die(
|
|
1189
|
+
new Error(
|
|
1190
|
+
`Cannot attach hostname '${hostname}' to Worker '${scriptName}': ` +
|
|
1191
|
+
`it is already attached to Worker '${otherOwner.service ?? "<unknown>"}'. ` +
|
|
1192
|
+
`Detach it from that Worker first, or pick a different hostname.`,
|
|
1193
|
+
),
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const zoneId = yield* inferZoneIdForHostname(hostname, zoneCache);
|
|
1198
|
+
const res = yield* putDomain({
|
|
1199
|
+
accountId,
|
|
1200
|
+
hostname,
|
|
1201
|
+
service: scriptName,
|
|
1202
|
+
zoneId,
|
|
1203
|
+
});
|
|
1204
|
+
return {
|
|
1205
|
+
hostname,
|
|
1206
|
+
id: res.id ?? "",
|
|
1207
|
+
zoneId: res.zoneId ?? zoneId,
|
|
1208
|
+
};
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1098
1211
|
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
|
-
),
|
|
1212
|
+
desired.map(attachDomain),
|
|
1118
1213
|
{ concurrency: "unbounded" },
|
|
1119
1214
|
);
|
|
1120
1215
|
return applied;
|
|
@@ -1205,21 +1300,15 @@ export const WorkerProvider = () =>
|
|
|
1205
1300
|
return undefined;
|
|
1206
1301
|
}
|
|
1207
1302
|
|
|
1208
|
-
// Handle AssetsWithHash (from Build resource)
|
|
1209
|
-
// Props are resolved by Plan, so Input<string> values are already strings at runtime
|
|
1210
1303
|
if (
|
|
1211
1304
|
typeof assets === "object" &&
|
|
1212
1305
|
"path" in assets &&
|
|
1213
1306
|
"hash" in assets
|
|
1214
1307
|
) {
|
|
1215
|
-
|
|
1308
|
+
return yield* readAssets({
|
|
1216
1309
|
directory: assets.path as string,
|
|
1217
1310
|
config: assets.config,
|
|
1218
1311
|
});
|
|
1219
|
-
return {
|
|
1220
|
-
...result,
|
|
1221
|
-
hash: assets.hash as string,
|
|
1222
|
-
};
|
|
1223
1312
|
}
|
|
1224
1313
|
|
|
1225
1314
|
// Handle string path or AssetsProps
|
|
@@ -1547,31 +1636,30 @@ ${[
|
|
|
1547
1636
|
let metadataAssets:
|
|
1548
1637
|
| workers.PutScriptRequest["metadata"]["assets"]
|
|
1549
1638
|
| undefined;
|
|
1550
|
-
|
|
1639
|
+
const keepAssets = false;
|
|
1551
1640
|
if (assets) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
}
|
|
1641
|
+
// Always upload assets on every deploy. The "skip if hash
|
|
1642
|
+
// unchanged" optimization was the source of subtle bundle/
|
|
1643
|
+
// asset desync bugs (deploy succeeds but worker serves 404s
|
|
1644
|
+
// because the bundle references hashed asset filenames that
|
|
1645
|
+
// aren't in the still-on-Cloudflare manifest). The upload
|
|
1646
|
+
// session is content-addressed on Cloudflare's side — only
|
|
1647
|
+
// genuinely-new asset bytes are PUT; unchanged assets cost
|
|
1648
|
+
// a manifest check and nothing else, so the optimization
|
|
1649
|
+
// wasn't worth the failure mode.
|
|
1650
|
+
yield* Effect.logInfo(
|
|
1651
|
+
`Cloudflare Worker ${olds ? "update" : "create"}: uploading assets for ${name}`,
|
|
1652
|
+
);
|
|
1653
|
+
const { jwt } = yield* uploadAssets(
|
|
1654
|
+
accountId,
|
|
1655
|
+
name,
|
|
1656
|
+
assets,
|
|
1657
|
+
session,
|
|
1658
|
+
);
|
|
1659
|
+
metadataAssets = {
|
|
1660
|
+
jwt,
|
|
1661
|
+
config: assets.config,
|
|
1662
|
+
};
|
|
1575
1663
|
metadataBindings.push({
|
|
1576
1664
|
type: "assets",
|
|
1577
1665
|
name: "ASSETS",
|
|
@@ -1868,11 +1956,19 @@ ${[
|
|
|
1868
1956
|
const input = yield* hashDirectory(props.vite);
|
|
1869
1957
|
return input !== output.hash?.input;
|
|
1870
1958
|
}
|
|
1959
|
+
// Always recompute both hashes by walking `dist/` (assets) and
|
|
1960
|
+
// re-bundling the worker. The previous short-circuit
|
|
1961
|
+
// (`Effect.succeed(output.hash.assets)`) compared the cached
|
|
1962
|
+
// hash to itself and could never detect drift, and the
|
|
1963
|
+
// `props.assets.hash` form is the *input* (source) hash which
|
|
1964
|
+
// misses non-deterministic build output (e.g. Astro/Vite
|
|
1965
|
+
// shuffling content-hashed filenames between builds). The
|
|
1966
|
+
// result of either shortcut was deploying a new bundle on top
|
|
1967
|
+
// of stale assets and 404'ing in production. Re-walking is the
|
|
1968
|
+
// only correct comparison.
|
|
1871
1969
|
const [assetsHash, bundleHash] = yield* Effect.all(
|
|
1872
1970
|
[
|
|
1873
|
-
|
|
1874
|
-
? Effect.succeed(output.hash.assets)
|
|
1875
|
-
: prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
|
|
1971
|
+
prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
|
|
1876
1972
|
prepareBundle(id, props).pipe(Effect.map((b) => b.hash)),
|
|
1877
1973
|
],
|
|
1878
1974
|
{ concurrency: "unbounded" },
|
|
@@ -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()));
|
|
@@ -7,6 +7,7 @@ export * from "./DynamicWorkerLoader.ts";
|
|
|
7
7
|
export * from "./Fetch.ts";
|
|
8
8
|
export * from "./HttpServer.ts";
|
|
9
9
|
export * from "./InferEnv.ts";
|
|
10
|
+
export * from "./LocalWorkerProvider.ts";
|
|
10
11
|
export * from "./Request.ts";
|
|
11
12
|
export * from "./Rpc.ts";
|
|
12
13
|
export * from "./ScheduledEvents.ts";
|