alchemy 2.0.0-beta.24 → 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 +40 -55
- package/bin/alchemy.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 +1 -6
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +81 -70
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/package.json +1 -1
- package/src/Cloudflare/Workers/Assets.ts +1 -10
- package/src/Cloudflare/Workers/Worker.ts +112 -99
|
@@ -22,7 +22,6 @@ 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
25
|
import { AlchemyContext } from "../../AlchemyContext.ts";
|
|
27
26
|
import * as Artifacts from "../../Artifacts.ts";
|
|
28
27
|
import * as Binding from "../../Binding.ts";
|
|
@@ -1105,11 +1104,44 @@ export const LiveWorkerProvider = () =>
|
|
|
1105
1104
|
const reconcileDomains = (
|
|
1106
1105
|
scriptName: string,
|
|
1107
1106
|
desired: string[],
|
|
1108
|
-
|
|
1107
|
+
_previous: Worker["Attributes"]["domains"],
|
|
1109
1108
|
) =>
|
|
1110
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
|
+
|
|
1111
1137
|
const desiredSet = new Set(desired);
|
|
1112
|
-
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
|
+
);
|
|
1113
1145
|
yield* Effect.all(
|
|
1114
1146
|
toRemove.map((d) =>
|
|
1115
1147
|
deleteDomain({ accountId, domainId: d.id }).pipe(
|
|
@@ -1121,79 +1153,59 @@ export const LiveWorkerProvider = () =>
|
|
|
1121
1153
|
|
|
1122
1154
|
if (desired.length === 0) return [];
|
|
1123
1155
|
|
|
1124
|
-
const adoptEnabled = yield* Effect.serviceOption(AdoptPolicy).pipe(
|
|
1125
|
-
Effect.map(Option.getOrElse(() => false)),
|
|
1126
|
-
);
|
|
1127
|
-
|
|
1128
1156
|
const zoneCache = new Map<string, string>();
|
|
1129
1157
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
*/
|
|
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.
|
|
1145
1163
|
const attachDomain = Effect.fnUntraced(function* (hostname: string) {
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
});
|
|
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
|
+
}
|
|
1165
1172
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
) {
|
|
1171
|
-
const existing = yield* listDomains({
|
|
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({
|
|
1172
1177
|
accountId,
|
|
1173
1178
|
hostname,
|
|
1174
1179
|
}).pipe(
|
|
1175
1180
|
Effect.map((r) =>
|
|
1176
|
-
(r.result ?? []).find(
|
|
1181
|
+
(r.result ?? []).find(
|
|
1182
|
+
(d) => d.hostname === hostname && d.service !== scriptName,
|
|
1183
|
+
),
|
|
1177
1184
|
),
|
|
1185
|
+
Effect.catch(() => Effect.succeed(undefined)),
|
|
1178
1186
|
);
|
|
1179
|
-
if (
|
|
1180
|
-
return
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
+
);
|
|
1185
1195
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
+
};
|
|
1197
1209
|
});
|
|
1198
1210
|
|
|
1199
1211
|
const applied = yield* Effect.all(
|
|
@@ -1288,21 +1300,15 @@ export const LiveWorkerProvider = () =>
|
|
|
1288
1300
|
return undefined;
|
|
1289
1301
|
}
|
|
1290
1302
|
|
|
1291
|
-
// Handle AssetsWithHash (from Build resource)
|
|
1292
|
-
// Props are resolved by Plan, so Input<string> values are already strings at runtime
|
|
1293
1303
|
if (
|
|
1294
1304
|
typeof assets === "object" &&
|
|
1295
1305
|
"path" in assets &&
|
|
1296
1306
|
"hash" in assets
|
|
1297
1307
|
) {
|
|
1298
|
-
|
|
1308
|
+
return yield* readAssets({
|
|
1299
1309
|
directory: assets.path as string,
|
|
1300
1310
|
config: assets.config,
|
|
1301
1311
|
});
|
|
1302
|
-
return {
|
|
1303
|
-
...result,
|
|
1304
|
-
hash: assets.hash as string,
|
|
1305
|
-
};
|
|
1306
1312
|
}
|
|
1307
1313
|
|
|
1308
1314
|
// Handle string path or AssetsProps
|
|
@@ -1630,31 +1636,30 @@ ${[
|
|
|
1630
1636
|
let metadataAssets:
|
|
1631
1637
|
| workers.PutScriptRequest["metadata"]["assets"]
|
|
1632
1638
|
| undefined;
|
|
1633
|
-
|
|
1639
|
+
const keepAssets = false;
|
|
1634
1640
|
if (assets) {
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
}
|
|
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
|
+
};
|
|
1658
1663
|
metadataBindings.push({
|
|
1659
1664
|
type: "assets",
|
|
1660
1665
|
name: "ASSETS",
|
|
@@ -1951,11 +1956,19 @@ ${[
|
|
|
1951
1956
|
const input = yield* hashDirectory(props.vite);
|
|
1952
1957
|
return input !== output.hash?.input;
|
|
1953
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.
|
|
1954
1969
|
const [assetsHash, bundleHash] = yield* Effect.all(
|
|
1955
1970
|
[
|
|
1956
|
-
|
|
1957
|
-
? Effect.succeed(output.hash.assets)
|
|
1958
|
-
: prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
|
|
1971
|
+
prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
|
|
1959
1972
|
prepareBundle(id, props).pipe(Effect.map((b) => b.hash)),
|
|
1960
1973
|
],
|
|
1961
1974
|
{ concurrency: "unbounded" },
|