| 1 | import * as Cloudflare from "@/Cloudflare"; |
| 2 | import * as Output from "@/Output"; |
| 3 | import * as Config from "effect/Config"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Redacted from "effect/Redacted"; |
| 6 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 7 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 8 | |
| 9 | /** |
| 10 | * Effect-native Worker fixture that exercises every supported |
| 11 | * `WorkerBindingResource` shape and echoes the resolved values back as |
| 12 | * JSON so the test can assert the deploy → runtime round-trip. |
| 13 | * |
| 14 | * Mixes the two declaration styles: |
| 15 | * - `env: { ... }` literal/Redacted/Config values declared on the |
| 16 | * resource (resolved via `WorkerEnvironment` at runtime). |
| 17 | * - `yield* Config.xxx(...)` resolved during Init — Alchemy captures |
| 18 | * the binding automatically and the same `Config` re-resolves from |
| 19 | * the binding at runtime. |
| 20 | */ |
| 21 | export default class EnvEffectWorker extends Cloudflare.Worker<EnvEffectWorker>()( |
| 22 | "EnvEffectWorker", |
| 23 | { |
| 24 | main: import.meta.url, |
| 25 | env: { |
| 26 | STR: "hello", |
| 27 | NUM: 42, |
| 28 | BOOL: true, |
| 29 | NULL: null, |
| 30 | OBJ: { nested: { value: "ok" }, count: 7 }, |
| 31 | ARR: [1, 2, 3], |
| 32 | OUTPUT_STR: Output.literal("output-str"), |
| 33 | SECRET_STR: Redacted.make("shh"), |
| 34 | SECRET_JSON: Redacted.make({ token: "abc", scopes: ["read", "write"] }), |
| 35 | // Config declared statically on `env` — Alchemy resolves at deploy |
| 36 | // time and binds it as `secret_text` on the Worker. |
| 37 | CONFIG_REDACTED: Config.Redacted("CONFIG_REDACTED"), |
| 38 | }, |
| 39 | }, |
| 40 | Effect.gen(function* () { |
| 41 | // Captured during Init — Alchemy binds these onto the Worker and the |
| 42 | // runtime ConfigProvider (backed by `env`) re-resolves them here. |
| 43 | const configStr = yield* Config.String("CONFIG_STR"); |
| 44 | const configNum = yield* Config.Number("CONFIG_NUM"); |
| 45 | const configRedactedInit = yield* Config.Redacted("CONFIG_REDACTED_INIT"); |
| 46 | // Composite forms — each member is bound individually, so the whole |
| 47 | // shape must survive the deploy → runtime round-trip. |
| 48 | const configAllObj = yield* Config.all({ |
| 49 | str: Config.String("CONFIG_STR"), |
| 50 | num: Config.Number("CONFIG_NUM"), |
| 51 | redacted: Config.Redacted("CONFIG_REDACTED_INIT"), |
| 52 | }); |
| 53 | const configAllTuple = yield* Config.all([ |
| 54 | Config.String("CONFIG_STR"), |
| 55 | Config.Number("CONFIG_NUM"), |
| 56 | ]); |
| 57 | // Nested prefix — bound under the flattened `CONFIG_NESTED_HOST` key. |
| 58 | const configNested = yield* Config.String("HOST").pipe( |
| 59 | Config.nested("CONFIG_NESTED"), |
| 60 | ); |
| 61 | |
| 62 | // Yieldable binding form — attaches the `version_metadata` binding to |
| 63 | // this Worker and returns a deferred accessor resolved at runtime. |
| 64 | const versionMetadata = yield* Cloudflare.Workers.VersionMetadata(); |
| 65 | |
| 66 | return { |
| 67 | fetch: Effect.gen(function* () { |
| 68 | const request = yield* HttpServerRequest; |
| 69 | const env = yield* Cloudflare.Workers.WorkerEnvironment; |
| 70 | const pathname = new URL(request.originalUrl).pathname; |
| 71 | |
| 72 | if (pathname === "/env") { |
| 73 | return yield* HttpServerResponse.json({ |
| 74 | STR: env.STR, |
| 75 | NUM: env.NUM, |
| 76 | BOOL: env.BOOL, |
| 77 | NULL: env.NULL, |
| 78 | OBJ: env.OBJ, |
| 79 | ARR: env.ARR, |
| 80 | OUTPUT_STR: env.OUTPUT_STR, |
| 81 | SECRET_STR: env.SECRET_STR, |
| 82 | SECRET_JSON: |
| 83 | typeof env.SECRET_JSON === "string" |
| 84 | ? JSON.parse(env.SECRET_JSON) |
| 85 | : env.SECRET_JSON, |
| 86 | }); |
| 87 | } |
| 88 | |
| 89 | if (pathname === "/version") { |
| 90 | const { id, tag, timestamp } = yield* versionMetadata; |
| 91 | return yield* HttpServerResponse.json({ id, tag, timestamp }); |
| 92 | } |
| 93 | |
| 94 | if (pathname === "/config") { |
| 95 | return yield* HttpServerResponse.json({ |
| 96 | CONFIG_STR: configStr, |
| 97 | CONFIG_NUM: configNum, |
| 98 | CONFIG_REDACTED: env.CONFIG_REDACTED, |
| 99 | CONFIG_REDACTED_INIT: Redacted.value(configRedactedInit), |
| 100 | CONFIG_REDACTED_INIT_IS_REDACTED: |
| 101 | Redacted.isRedacted(configRedactedInit), |
| 102 | CONFIG_ALL_OBJ: { |
| 103 | str: configAllObj.str, |
| 104 | num: configAllObj.num, |
| 105 | redacted: Redacted.value(configAllObj.redacted), |
| 106 | redactedIsRedacted: Redacted.isRedacted(configAllObj.redacted), |
| 107 | }, |
| 108 | CONFIG_ALL_TUPLE: configAllTuple, |
| 109 | CONFIG_NESTED_HOST: configNested, |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | if (pathname === "/config-runtime") { |
| 114 | // Re-resolve the same Configs captured during Init — this time at |
| 115 | // request time, deep inside a nested effect, where the env-backed |
| 116 | // runtime ConfigProvider (not the Init interceptor) answers the |
| 117 | // read. The bound values arrive in `env` as |
| 118 | // `{"_tag":"Redacted","value":...}` markers and must be reified |
| 119 | // transparently: `Config.Number` must decode the raw source value, |
| 120 | // not the marker JSON. |
| 121 | const nested = yield* Effect.gen(function* () { |
| 122 | const allObj = yield* Config.all({ |
| 123 | str: Config.String("CONFIG_STR"), |
| 124 | num: Config.Number("CONFIG_NUM"), |
| 125 | redacted: Config.Redacted("CONFIG_REDACTED_INIT"), |
| 126 | }); |
| 127 | return { |
| 128 | CONFIG_STR: yield* Config.String("CONFIG_STR"), |
| 129 | CONFIG_NUM: yield* Config.Number("CONFIG_NUM"), |
| 130 | // Combinators re-apply at runtime against the bound source. |
| 131 | CONFIG_NUM_WITH_DEFAULT: yield* Config.Number("CONFIG_NUM").pipe( |
| 132 | Config.withDefault(999), |
| 133 | ), |
| 134 | // Never read during Init, so never bound — the default applies. |
| 135 | CONFIG_UNSET_WITH_DEFAULT: yield* Config.Number( |
| 136 | "CONFIG_UNSET", |
| 137 | ).pipe(Config.withDefault(3000)), |
| 138 | CONFIG_ALL_OBJ: { |
| 139 | str: allObj.str, |
| 140 | num: allObj.num, |
| 141 | redacted: Redacted.value(allObj.redacted), |
| 142 | redactedIsRedacted: Redacted.isRedacted(allObj.redacted), |
| 143 | }, |
| 144 | CONFIG_ALL_TUPLE: yield* Config.all([ |
| 145 | Config.String("CONFIG_STR"), |
| 146 | Config.Number("CONFIG_NUM"), |
| 147 | ]), |
| 148 | CONFIG_NESTED_HOST: yield* Config.String("HOST").pipe( |
| 149 | Config.nested("CONFIG_NESTED"), |
| 150 | ), |
| 151 | }; |
| 152 | }); |
| 153 | const redactedAtRuntime = yield* Config.Redacted( |
| 154 | "CONFIG_REDACTED_INIT", |
| 155 | ); |
| 156 | return yield* HttpServerResponse.json({ |
| 157 | ...nested, |
| 158 | CONFIG_REDACTED_INIT: Redacted.value(redactedAtRuntime), |
| 159 | CONFIG_REDACTED_INIT_IS_REDACTED: |
| 160 | Redacted.isRedacted(redactedAtRuntime), |
| 161 | }); |
| 162 | } |
| 163 | |
| 164 | return HttpServerResponse.text("ok"); |
| 165 | // The request-time Config reads carry ConfigError; a failed read is |
| 166 | // a test bug, so die (surfaces as a 500 the test assertions catch). |
| 167 | }).pipe(Effect.orDie), |
| 168 | }; |
| 169 | }).pipe(Effect.provide(Cloudflare.Workers.VersionMetadataBinding)), |
| 170 | ) {} |