alchemy 2.0.0-beta.32 → 2.0.0-beta.34
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/lib/Cli/commands/bootstrap.d.ts +14 -1
- package/lib/Cli/commands/bootstrap.d.ts.map +1 -1
- package/lib/Cli/commands/dev.d.ts.map +1 -1
- package/lib/Cli/commands/dev.js +11 -6
- package/lib/Cli/commands/dev.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +16 -1
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Api.js +37 -36
- package/lib/Cloudflare/StateStore/Api.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +36 -28
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +68 -8
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +23 -5
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +14 -0
- package/lib/State/HttpStateApi.d.ts.map +1 -1
- package/lib/State/HttpStateApi.js +15 -1
- package/lib/State/HttpStateApi.js.map +1 -1
- package/lib/State/HttpStateStore.js +4 -6
- package/lib/State/HttpStateStore.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/Cli/commands/dev.ts +11 -6
- package/src/Cloudflare/StateStore/Api.ts +41 -46
- package/src/Cloudflare/StateStore/State.ts +86 -33
- package/src/Cloudflare/Workers/Worker.ts +26 -5
- package/src/State/HttpStateApi.ts +15 -1
- package/src/State/HttpStateStore.ts +4 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.34",
|
|
4
4
|
"homepage": "https://alchemy.run",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
@@ -275,14 +275,14 @@
|
|
|
275
275
|
"@ai-sdk/provider": "^3.0.8",
|
|
276
276
|
"@aws-sdk/credential-providers": "^3.0.0",
|
|
277
277
|
"@clack/prompts": "^0.11.0",
|
|
278
|
-
"@distilled.cloud/aws": "^0.
|
|
279
|
-
"@distilled.cloud/axiom": "^0.
|
|
280
|
-
"@distilled.cloud/cloudflare": "^0.
|
|
278
|
+
"@distilled.cloud/aws": "^0.17.0",
|
|
279
|
+
"@distilled.cloud/axiom": "^0.17.0",
|
|
280
|
+
"@distilled.cloud/cloudflare": "^0.17.0",
|
|
281
281
|
"@distilled.cloud/cloudflare-rolldown-plugin": "0.3.0",
|
|
282
282
|
"@distilled.cloud/cloudflare-runtime": "0.2.1",
|
|
283
283
|
"@distilled.cloud/cloudflare-vite-plugin": "0.2.0",
|
|
284
|
-
"@distilled.cloud/core": "^0.
|
|
285
|
-
"@distilled.cloud/neon": "^0.
|
|
284
|
+
"@distilled.cloud/core": "^0.17.0",
|
|
285
|
+
"@distilled.cloud/neon": "^0.17.0",
|
|
286
286
|
"pg": "^8.13.0",
|
|
287
287
|
"@effect/vitest": ">=4.0.0-beta.60 || >=4.0.0",
|
|
288
288
|
"@libsql/client": "^0.17.0",
|
package/src/Cli/commands/dev.ts
CHANGED
|
@@ -32,14 +32,19 @@ export const devCommand = Command.make(
|
|
|
32
32
|
dev: true,
|
|
33
33
|
});
|
|
34
34
|
const bin = typeof globalThis.Bun !== "undefined" ? "bun" : "node";
|
|
35
|
+
const main = fileURLToPath(import.meta.resolve(execEntry));
|
|
35
36
|
const child = yield* ChildProcess.make(
|
|
36
37
|
bin,
|
|
37
|
-
|
|
38
|
-
"run",
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
{
|
|
39
|
+
bun: ["run", ...process.execArgv, "--watch", "--no-clear-screen", main],
|
|
40
|
+
node: [
|
|
41
|
+
...process.execArgv,
|
|
42
|
+
"--experimental-transform-types",
|
|
43
|
+
"--watch",
|
|
44
|
+
"--watch-preserve-output",
|
|
45
|
+
main,
|
|
46
|
+
],
|
|
47
|
+
}[bin],
|
|
43
48
|
{
|
|
44
49
|
stdin: "inherit",
|
|
45
50
|
stdout: "inherit",
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BearerTokenValidator,
|
|
3
|
-
StateApi,
|
|
4
|
-
StateAuthLive,
|
|
5
|
-
} from "../../State/HttpStateApi.ts";
|
|
6
1
|
import * as Effect from "effect/Effect";
|
|
7
2
|
import * as Layer from "effect/Layer";
|
|
8
3
|
import * as Path from "effect/Path";
|
|
9
4
|
import * as Etag from "effect/unstable/http/Etag";
|
|
10
|
-
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
11
5
|
import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
|
|
12
6
|
import * as HttpRouter from "effect/unstable/http/HttpRouter";
|
|
13
7
|
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
|
|
14
8
|
import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
|
|
15
|
-
import * as OtlpLogger from "effect/unstable/observability/OtlpLogger";
|
|
16
|
-
import * as OtlpMetrics from "effect/unstable/observability/OtlpMetrics";
|
|
17
|
-
import * as OtlpSerialization from "effect/unstable/observability/OtlpSerialization";
|
|
18
|
-
import * as OtlpTracer from "effect/unstable/observability/OtlpTracer";
|
|
19
9
|
import crypto from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
BearerTokenValidator,
|
|
12
|
+
StateApi,
|
|
13
|
+
StateAuthLive,
|
|
14
|
+
} from "../../State/HttpStateApi.ts";
|
|
20
15
|
import * as Secret from "../SecretsStore/Secret.ts";
|
|
21
16
|
import { SecretBindingLive } from "../SecretsStore/SecretBinding.ts";
|
|
22
17
|
import { Worker } from "../Workers/Worker.ts";
|
|
@@ -34,7 +29,7 @@ export const STATE_STORE_SCRIPT_NAME = "alchemy-state-store" as const;
|
|
|
34
29
|
* compare against this constant; a mismatch (or 404) triggers a
|
|
35
30
|
* forced redeploy via the bootstrap flow.
|
|
36
31
|
*/
|
|
37
|
-
export const STATE_STORE_VERSION =
|
|
32
|
+
export const STATE_STORE_VERSION = 3 as const;
|
|
38
33
|
|
|
39
34
|
/**
|
|
40
35
|
* Hard-coded OTLP/HTTP endpoints. Point at the public ingest relay
|
|
@@ -43,9 +38,9 @@ export const STATE_STORE_VERSION = 2 as const;
|
|
|
43
38
|
* coded on purpose: the worker has no env-var plumbing and the relay
|
|
44
39
|
* is account-level infra that lives outside any single deploy.
|
|
45
40
|
*/
|
|
46
|
-
const OTEL_TRACES_URL = "https://otel.alchemy.run/v1/traces";
|
|
47
|
-
const OTEL_METRICS_URL = "https://otel.alchemy.run/v1/metrics";
|
|
48
|
-
const OTEL_LOGS_URL = "https://otel.alchemy.run/v1/logs";
|
|
41
|
+
// const OTEL_TRACES_URL = "https://otel.alchemy.run/v1/traces";
|
|
42
|
+
// const OTEL_METRICS_URL = "https://otel.alchemy.run/v1/metrics";
|
|
43
|
+
// const OTEL_LOGS_URL = "https://otel.alchemy.run/v1/logs";
|
|
49
44
|
|
|
50
45
|
/**
|
|
51
46
|
* OTLP traces + metrics + logs Layer for the state-store worker.
|
|
@@ -55,38 +50,38 @@ const OTEL_LOGS_URL = "https://otel.alchemy.run/v1/logs";
|
|
|
55
50
|
* brand each signal with the worker's service name + contract version
|
|
56
51
|
* so they're easy to slice in Axiom.
|
|
57
52
|
*/
|
|
58
|
-
const otelResource = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} as const;
|
|
53
|
+
// const otelResource = {
|
|
54
|
+
// serviceName: STATE_STORE_SCRIPT_NAME,
|
|
55
|
+
// serviceVersion: String(STATE_STORE_VERSION),
|
|
56
|
+
// attributes: {
|
|
57
|
+
// "alchemy.state_store.script_name": STATE_STORE_SCRIPT_NAME,
|
|
58
|
+
// "alchemy.state_store.version": STATE_STORE_VERSION,
|
|
59
|
+
// },
|
|
60
|
+
// } as const;
|
|
66
61
|
|
|
67
|
-
const TelemetryLive = Layer.mergeAll(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
).pipe(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
62
|
+
// const TelemetryLive = Layer.mergeAll(
|
|
63
|
+
// OtlpTracer.layer({
|
|
64
|
+
// url: OTEL_TRACES_URL,
|
|
65
|
+
// resource: otelResource,
|
|
66
|
+
// exportInterval: "1 second",
|
|
67
|
+
// }),
|
|
68
|
+
// OtlpMetrics.layer({
|
|
69
|
+
// url: OTEL_METRICS_URL,
|
|
70
|
+
// resource: otelResource,
|
|
71
|
+
// exportInterval: "1 second",
|
|
72
|
+
// }),
|
|
73
|
+
// // Replace (don't merge with) the default stdout logger so worker logs
|
|
74
|
+
// // ship to Axiom instead of just `wrangler tail`.
|
|
75
|
+
// OtlpLogger.layer({
|
|
76
|
+
// url: OTEL_LOGS_URL,
|
|
77
|
+
// resource: otelResource,
|
|
78
|
+
// exportInterval: "1 second",
|
|
79
|
+
// mergeWithExisting: false,
|
|
80
|
+
// }),
|
|
81
|
+
// ).pipe(
|
|
82
|
+
// Layer.provide(OtlpSerialization.layerJson),
|
|
83
|
+
// Layer.provide(FetchHttpClient.layer),
|
|
84
|
+
// );
|
|
90
85
|
|
|
91
86
|
/**
|
|
92
87
|
* Path on disk to *this* file, used as the worker's bundling entry.
|
|
@@ -288,7 +283,7 @@ export default Worker(
|
|
|
288
283
|
// file-response surface is stubbed.
|
|
289
284
|
Layer.provide([Etag.layer, HttpPlatformStub, Path.layer]),
|
|
290
285
|
HttpRouter.toHttpEffect,
|
|
291
|
-
Effect.provide(TelemetryLive),
|
|
286
|
+
// Effect.provide(TelemetryLive),
|
|
292
287
|
),
|
|
293
288
|
};
|
|
294
289
|
}).pipe(Effect.provide(Layer.mergeAll(SecretBindingLive))),
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as SecretsStore from "@distilled.cloud/cloudflare/secrets-store";
|
|
2
2
|
import * as workers from "@distilled.cloud/cloudflare/workers";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
-
import * as Output from "../../Output.ts";
|
|
5
4
|
import * as Layer from "effect/Layer";
|
|
6
5
|
import * as Redacted from "effect/Redacted";
|
|
6
|
+
import * as Schedule from "effect/Schedule";
|
|
7
7
|
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
8
8
|
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
9
9
|
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
|
|
10
10
|
import crypto from "node:crypto";
|
|
11
|
+
import * as Output from "../../Output.ts";
|
|
11
12
|
|
|
12
13
|
import * as Config from "effect/Config";
|
|
13
14
|
import { adopt } from "../../AdoptPolicy.ts";
|
|
@@ -198,32 +199,7 @@ export const bootstrap = (options: BootstrapOptions = {}) =>
|
|
|
198
199
|
}),
|
|
199
200
|
);
|
|
200
201
|
|
|
201
|
-
export const state = (
|
|
202
|
-
/**
|
|
203
|
-
* The name of the script to use for the state store.
|
|
204
|
-
* @default "alchemy-state-store"
|
|
205
|
-
*/
|
|
206
|
-
workerName?: string;
|
|
207
|
-
/**
|
|
208
|
-
* Suppress the per-deployment Cloudflare account hash on telemetry
|
|
209
|
-
* spans. Lets a caller opt out of having their state-store's
|
|
210
|
-
* accountId pseudonymously counted on the maintainer dashboard
|
|
211
|
-
* without disabling all telemetry.
|
|
212
|
-
*
|
|
213
|
-
* - `true` — always opt out, regardless of env.
|
|
214
|
-
* - `false` — always opt in, regardless of env.
|
|
215
|
-
* - `undefined` (default) — fall back to the `NO_TRACK` env var
|
|
216
|
-
* (`true`/`1` → opt out). Default-default is opt-in.
|
|
217
|
-
*
|
|
218
|
-
* Independent of the global telemetry kill-switch
|
|
219
|
-
* (`DO_NOT_TRACK` / `ALCHEMY_TELEMETRY_DISABLED`), which kills the
|
|
220
|
-
* entire OTLP exporter — `noTrack` here only suppresses the
|
|
221
|
-
* `alchemy.cloudflare.account_hash` attribute on state-store
|
|
222
|
-
* spans, leaving the rest of the telemetry intact.
|
|
223
|
-
* @default undefined
|
|
224
|
-
*/
|
|
225
|
-
noTrack?: boolean;
|
|
226
|
-
}) =>
|
|
202
|
+
export const state = () =>
|
|
227
203
|
Layer.effect(
|
|
228
204
|
State,
|
|
229
205
|
Effect.gen(function* () {
|
|
@@ -235,13 +211,15 @@ export const state = (props?: {
|
|
|
235
211
|
CREDENTIALS_FILE,
|
|
236
212
|
);
|
|
237
213
|
|
|
238
|
-
const scriptName =
|
|
214
|
+
const scriptName = STATE_STORE_SCRIPT_NAME;
|
|
239
215
|
yield* Effect.annotateCurrentSpan({
|
|
240
216
|
"alchemy.state_store.script_name": scriptName,
|
|
241
217
|
"alchemy.state_store.profile": profileName,
|
|
242
218
|
"alchemy.state_store.ci": isCI,
|
|
243
219
|
});
|
|
244
|
-
yield* annotateAccountHash(
|
|
220
|
+
yield* annotateAccountHash(
|
|
221
|
+
yield* Config.boolean("NO_TRACK").pipe(Config.withDefault(false)),
|
|
222
|
+
);
|
|
245
223
|
|
|
246
224
|
// The bootstrap of the Cloudflare State Store is only considered
|
|
247
225
|
// successful once two invariants hold:
|
|
@@ -529,6 +507,16 @@ const deployStateStore = (scriptName: string, state?: StateService) =>
|
|
|
529
507
|
// TODO(sam): we should not need to do this, but types do complain. fix deploy
|
|
530
508
|
Effect.provide(stateLayer),
|
|
531
509
|
);
|
|
510
|
+
// Cloudflare's worker upload is eventually consistent: the deploy
|
|
511
|
+
// call returns as soon as the script upload is accepted, but the
|
|
512
|
+
// edge can keep serving the previous version for several seconds
|
|
513
|
+
// afterwards. Block here until `/version` reports the version this
|
|
514
|
+
// CLI was built against — otherwise downstream steps (syncing
|
|
515
|
+
// local state into the deployed store, version probes during
|
|
516
|
+
// adoption) end up talking to the old worker and may either
|
|
517
|
+
// observe stale data or trip the staleness check and recurse into
|
|
518
|
+
// another redeploy.
|
|
519
|
+
yield* waitForStateStoreVersion(url);
|
|
532
520
|
return { url, authToken, localState };
|
|
533
521
|
}).pipe(
|
|
534
522
|
Effect.withSpan("state_store.deploy", {
|
|
@@ -694,19 +682,84 @@ const redeployIfStale = ({
|
|
|
694
682
|
* since the caller's response in every case is the same: fall
|
|
695
683
|
* through to the idempotent bootstrap flow.
|
|
696
684
|
*/
|
|
685
|
+
/**
|
|
686
|
+
* Block until the worker at `url` reports the {@link STATE_STORE_VERSION}
|
|
687
|
+
* this CLI was built against, with bounded exponential retry. Used
|
|
688
|
+
* post-deploy to wait out Cloudflare's edge cache so any subsequent
|
|
689
|
+
* read or write goes to the new worker.
|
|
690
|
+
*
|
|
691
|
+
* Failures (transport errors, 404 from a pre-`/version` build, schema
|
|
692
|
+
* mismatch, version mismatch) all collapse to "not ready yet" and are
|
|
693
|
+
* retried; once the budget is exhausted we surface a hard failure
|
|
694
|
+
* since continuing would talk to the wrong worker.
|
|
695
|
+
*/
|
|
696
|
+
class StateStoreVersionNotReady extends Error {
|
|
697
|
+
readonly _tag = "StateStoreVersionNotReady";
|
|
698
|
+
constructor(
|
|
699
|
+
readonly expected: number,
|
|
700
|
+
readonly observed: number | undefined,
|
|
701
|
+
) {
|
|
702
|
+
super(
|
|
703
|
+
`Cloudflare State Store version not ready (expected v${expected}, observed v${observed ?? "unknown"}).`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const waitForStateStoreVersion = (url: string) =>
|
|
709
|
+
Effect.gen(function* () {
|
|
710
|
+
const { matches, expected, observed } = yield* checkStateStoreVersion(url);
|
|
711
|
+
if (!matches) {
|
|
712
|
+
return yield* Effect.fail(
|
|
713
|
+
new StateStoreVersionNotReady(expected, observed),
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}).pipe(
|
|
717
|
+
Effect.retry({
|
|
718
|
+
while: (error) => error instanceof StateStoreVersionNotReady,
|
|
719
|
+
// Edge propagation is usually sub-second; poll fast and cap the
|
|
720
|
+
// overall wait at ~10s so we fail loudly if something is really
|
|
721
|
+
// wrong rather than silently hanging.
|
|
722
|
+
schedule: Schedule.spaced("200 millis").pipe(
|
|
723
|
+
Schedule.both(Schedule.recurs(50)),
|
|
724
|
+
),
|
|
725
|
+
}),
|
|
726
|
+
Effect.withSpan("state_store.wait_for_version", {
|
|
727
|
+
attributes: {
|
|
728
|
+
"alchemy.state_store.op": "wait_for_version",
|
|
729
|
+
"alchemy.state_store.url": url,
|
|
730
|
+
"alchemy.state_store.expected_version": STATE_STORE_VERSION,
|
|
731
|
+
},
|
|
732
|
+
}),
|
|
733
|
+
);
|
|
734
|
+
|
|
697
735
|
const checkStateStoreVersion = (url: string) =>
|
|
698
736
|
Effect.gen(function* () {
|
|
699
737
|
const client = yield* HttpApiClient.make(StateApi, { baseUrl: url });
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
738
|
+
// The /version route may 404 transiently after a fresh deploy
|
|
739
|
+
// while Cloudflare propagates the new script to the edge, and may
|
|
740
|
+
// also surface transport-level blips on cold workers.dev hosts.
|
|
741
|
+
// Retry the probe itself for ~10s before giving up — only after
|
|
742
|
+
// exhausting that budget do we collapse to `undefined` and let
|
|
743
|
+
// the caller treat it as a version mismatch.
|
|
744
|
+
const result = yield* client.version.getVersion().pipe(
|
|
745
|
+
Effect.retry({
|
|
746
|
+
schedule: Schedule.spaced("250 millis").pipe(
|
|
747
|
+
Schedule.both(Schedule.recurs(40)),
|
|
748
|
+
),
|
|
749
|
+
}),
|
|
750
|
+
Effect.catch(() => Effect.succeed(undefined)),
|
|
751
|
+
);
|
|
703
752
|
const matches = result?.version === STATE_STORE_VERSION;
|
|
704
753
|
yield* Effect.annotateCurrentSpan({
|
|
705
754
|
"alchemy.state_store.expected_version": STATE_STORE_VERSION,
|
|
706
755
|
"alchemy.state_store.observed_version": result?.version ?? -1,
|
|
707
756
|
"alchemy.state_store.version_match": matches,
|
|
708
757
|
});
|
|
709
|
-
return {
|
|
758
|
+
return {
|
|
759
|
+
matches,
|
|
760
|
+
expected: STATE_STORE_VERSION,
|
|
761
|
+
observed: result?.version,
|
|
762
|
+
};
|
|
710
763
|
}).pipe(
|
|
711
764
|
Effect.withSpan("state_store.check_version", {
|
|
712
765
|
attributes: { "alchemy.state_store.op": "check_version" },
|
|
@@ -893,8 +893,8 @@ export const Worker: Platform<
|
|
|
893
893
|
const scope = Scope.makeUnsafe();
|
|
894
894
|
return eff
|
|
895
895
|
.pipe(
|
|
896
|
-
Scope.provide(scope),
|
|
897
896
|
Effect.provideContext(services),
|
|
897
|
+
Scope.provide(scope),
|
|
898
898
|
Effect.provide(
|
|
899
899
|
Layer.succeed(WorkerExecutionContext, context),
|
|
900
900
|
),
|
|
@@ -1091,11 +1091,18 @@ export const LiveWorkerProvider = () =>
|
|
|
1091
1091
|
accountId,
|
|
1092
1092
|
}).pipe(Effect.map((result) => result.subdomain));
|
|
1093
1093
|
|
|
1094
|
+
// Toggle the workers.dev subdomain via `POST /subdomain` with
|
|
1095
|
+
// `enabled: true | false`. Mirrors the upstream Alchemy
|
|
1096
|
+
// implementation in `.vendor/alchemy/.../worker-subdomain.ts`.
|
|
1097
|
+
// When enabling we also set `previewsEnabled: true` so the
|
|
1098
|
+
// script is reachable both at its stable workers.dev URL and at
|
|
1099
|
+
// version-preview URLs; on disable we send just `enabled: false`.
|
|
1094
1100
|
const setWorkerSubdomain = (name: string, enabled: boolean) =>
|
|
1095
1101
|
createScriptSubdomain({
|
|
1096
1102
|
accountId,
|
|
1097
1103
|
scriptName: name,
|
|
1098
1104
|
enabled,
|
|
1105
|
+
previewsEnabled: enabled ? true : undefined,
|
|
1099
1106
|
});
|
|
1100
1107
|
|
|
1101
1108
|
const createWorkerName = (id: string, name: string | undefined) =>
|
|
@@ -1923,12 +1930,26 @@ export const LiveWorkerProvider = () =>
|
|
|
1923
1930
|
name,
|
|
1924
1931
|
expectedDurableObjectClassNames,
|
|
1925
1932
|
);
|
|
1926
|
-
|
|
1927
|
-
|
|
1933
|
+
// Reconcile workers.dev subdomain against observed cloud state.
|
|
1934
|
+
// We can't diff `news.url` against `olds.url` here because both
|
|
1935
|
+
// default to `undefined` (meaning "enable") — that comparison
|
|
1936
|
+
// would skip the API call on every deploy where the user never
|
|
1937
|
+
// explicitly set `url`, leaving the subdomain in whatever state
|
|
1938
|
+
// Cloudflare currently has it (disabled by default, or whatever
|
|
1939
|
+
// a previous failed/external action left it as).
|
|
1940
|
+
const desiredSubdomainEnabled = news.url !== false;
|
|
1941
|
+
const observedSubdomainEnabled = yield* getScriptSubdomain({
|
|
1942
|
+
accountId,
|
|
1943
|
+
scriptName: name,
|
|
1944
|
+
}).pipe(
|
|
1945
|
+
Effect.map((s) => s.enabled === true),
|
|
1946
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
1947
|
+
);
|
|
1948
|
+
if (desiredSubdomainEnabled !== observedSubdomainEnabled) {
|
|
1928
1949
|
yield* session.note(
|
|
1929
|
-
`${
|
|
1950
|
+
`${desiredSubdomainEnabled ? "Enabling" : "Disabling"} workers.dev subdomain...`,
|
|
1930
1951
|
);
|
|
1931
|
-
yield* setWorkerSubdomain(name,
|
|
1952
|
+
yield* setWorkerSubdomain(name, desiredSubdomainEnabled);
|
|
1932
1953
|
}
|
|
1933
1954
|
const desiredDomains = normalizeDomains(news.domain);
|
|
1934
1955
|
const previousDomains = output?.domains ?? [];
|
|
@@ -11,7 +11,21 @@ import * as HttpApiMiddleware from "effect/unstable/httpapi/HttpApiMiddleware";
|
|
|
11
11
|
import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
|
|
12
12
|
import * as HttpApiSecurity from "effect/unstable/httpapi/HttpApiSecurity";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Resource-state payload shape on the wire.
|
|
16
|
+
*
|
|
17
|
+
* Pinned to JSON encoding via {@link HttpApiSchema.asJson} so that both
|
|
18
|
+
* sides of the API agree on `Content-Type: application/json`. With a
|
|
19
|
+
* bare `Schema.Any` the server's payload-decoder map is keyed off the
|
|
20
|
+
* default content type only — any client request that arrives with a
|
|
21
|
+
* differently-shaped body (or a transport that drops the
|
|
22
|
+
* `Content-Type` header entirely) falls into the `payloadBy.get(
|
|
23
|
+
* contentType)` miss branch in `HttpApiBuilder.decodePayload` and
|
|
24
|
+
* surfaces as a confusing `415 Unsupported Media Type`. Annotating
|
|
25
|
+
* the schema makes the encoding explicit on both endpoints and the
|
|
26
|
+
* client encoder, so the wire format is unambiguous.
|
|
27
|
+
*/
|
|
28
|
+
export const ResourceStateSchema = Schema.Any.pipe(HttpApiSchema.asJson());
|
|
15
29
|
|
|
16
30
|
export class BearerTokenValidator extends Context.Service<
|
|
17
31
|
BearerTokenValidator,
|
|
@@ -159,13 +159,12 @@ const isTransient = (e: any): boolean => {
|
|
|
159
159
|
const retryTransient = <A, Err, Req>(eff: Effect.Effect<A, Err, Req>) =>
|
|
160
160
|
Effect.retry(eff, {
|
|
161
161
|
while: isTransient,
|
|
162
|
-
// Exponential backoff capped at 2s,
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// stranded `syncState` mid-flight, leaving the remote store
|
|
166
|
-
// partially populated).
|
|
162
|
+
// Exponential backoff capped at 2s, max 5 attempts. Beyond that
|
|
163
|
+
// the issue isn't transient and we'd rather surface a hard
|
|
164
|
+
// failure than block the deploy indefinitely.
|
|
167
165
|
schedule: Schedule.exponential(100).pipe(
|
|
168
166
|
Schedule.either(Schedule.spaced("2 seconds")),
|
|
167
|
+
Schedule.both(Schedule.recurs(5)),
|
|
169
168
|
),
|
|
170
169
|
});
|
|
171
170
|
|