Skip to content

feat(core): factory-form Platform/Resource for parametric default exports#180

Open
sam-goodwin wants to merge 6 commits into
mainfrom
claude/musing-pare-97317a
Open

feat(core): factory-form Platform/Resource for parametric default exports#180
sam-goodwin wants to merge 6 commits into
mainfrom
claude/musing-pare-97317a

Conversation

@sam-goodwin
Copy link
Copy Markdown
Contributor

@sam-goodwin sam-goodwin commented May 4, 2026

Adds a factory overload for Platform/Resource so a default-exported worker can be parametric, and uses it to fix Cloudflare.state({ workerName }) end-to-end.

// User-facing: parametric default export
export default Worker((scriptName: string) => [
  "Api",
  {
    name: scriptName,
    main: import.meta.filename,
    ...
  },
  Effect.gen(function* () { ... }),
]);

The factory function returns the same [id, props, body] tuple the positional Worker(id, props, body) form accepts — no nested Worker(...Worker(...)). Resource (non-Platform) takes a [id, props] 2-tuple.

At deploy time, yield* MyWorker(...args) applies the tuple to the constructor and stamps each arg into Props.env as its own binding (__ALCHEMY_FACTORY_ARG_<i>__, plus __ALCHEMY_FACTORY_ARG_COUNT__). Top-level Redacted args ride the secret_text lifecycle; everything else rides plain_text. Output args resolve at deploy time via the engine's normal Output resolver, so Output<Redacted<string>> lands on secret_text too — same encoding as the existing ctx.set/ctx.get pair.

+import * as Redacted from "effect/Redacted";

+const __decodeFactoryArg = (raw) => {
+  if (raw == null) return raw;
+  try {
+    const parsed = JSON.parse(raw);
+    if (parsed?._tag === "Redacted" && "value" in parsed) {
+      return Redacted.make(parsed.value);
+    }
+    return parsed;
+  } catch { return raw; }
+};
+const __alchemyFactoryArgs = Array.from(
+  { length: parseInt(env.__ALCHEMY_FACTORY_ARG_COUNT__ ?? "0", 10) },
+  (_, i) => __decodeFactoryArg(env["__ALCHEMY_FACTORY_ARG_" + i + "__"]),
+);
+const resolved = entry?.__alchemyFactory ? entry(...__alchemyFactoryArgs) : entry;

 const tag = Context.Service("...")
 const layer =
-  typeof entry?.build === "function"
-    ? entry
-    : Layer.effect(tag, typeof entry?.asEffect === "function" ? entry.asEffect() : entry);
+  typeof resolved?.build === "function"
+    ? resolved
+    : Layer.effect(tag, typeof resolved?.asEffect === "function" ? resolved.asEffect() : resolved);

Cloudflare/StateStore/Api.ts switches to the factory form. Combined with the existing loginWithCloudflare(scriptName) change, Cloudflare.state({ workerName: "custom-state" }) now deploys and looks up custom-state — the bug that motivated this work.

🤖 Generated with Claude Code

`Cloudflare.state({ workerName })` was a no-op — the prop flowed into
existence checks and the deploy stage but never reached the deployed
worker's name or the post-deploy login probe, so a custom name would
deploy under `alchemy-state-store` and login would always look up the
default URL.

- `Api` is now `(scriptName) => Worker(...)` instead of a hardcoded
  default-exported Worker, so the deployed worker honors the prop.
- `loginWithCloudflare` takes a `scriptName` and uses it for both the
  edge-preview secret probe and the workers.dev URL derivation.
- All three callers thread `scriptName` through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alchemy-version-bot
Copy link
Copy Markdown
Contributor

alchemy-version-bot Bot commented May 4, 2026

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/af49738

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/af49738

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/af49738

…orts

Adds an overload so a Platform-style resource (Worker, Container, Lambda…)
or a plain Resource can be defined as a function:

    export default Worker((scriptName: string) =>
      Worker("Api", { name: scriptName, main: import.meta.filename, ... }, body));

At deploy time `yield* MyWorker(...args)` runs the inner Effect and
stamps the args into `Props.env` under `__ALCHEMY_FACTORY_ARGS__`,
riding the existing env-binding lifecycle as a `plain_text` binding.
At runtime, the generated Cloudflare Worker entrypoint detects the
`__alchemyFactory` marker on the imported default export, JSON-decodes
the args from `env`, and calls the function before treating the
result as a Layer/Effect.

v1 limitation: factory args must be JSON-serializable. Output /
Redacted args TODO.

Updates `Cloudflare/StateStore/Api.ts` to use the factory form so
`Cloudflare.state({ workerName })` actually deploys/looks up the
custom worker name end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sam-goodwin sam-goodwin changed the title fix(cloudflare/state-store): respect workerName in Cloudflare.state feat(core): factory-form Platform/Resource for parametric default exports May 4, 2026
sam-goodwin and others added 4 commits May 4, 2026 17:35
Switch from a single combined JSON binding to per-arg bindings. Each
arg lands at \`__ALCHEMY_FACTORY_ARG_<i>__\` with a count at
\`__ALCHEMY_FACTORY_ARG_COUNT__\`. Top-level Redacted args ride
\`secret_text\` via the same JSON-with-\`_tag: "Redacted"\`-marker
encoding \`ctx.set\`/\`ctx.get\` already use; Output args resolve at
deploy time, so \`Output<Redacted<string>>\` lands on \`secret_text\`
too. Generated entrypoint decodes per arg and rebuilds the Redacted
wrapper before calling the factory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous shape \`Worker((args) => Worker("Api", ...))\` repeats the
\`Worker\` token at both ends with two different meanings — outer is
the factory wrapper, inner is the actual resource constructor. Switch
the inner side to a tuple so the factory returns the same positional
shape the constructor already accepts:

    export default Worker((scriptName: string) => [
      "Api",
      { name: scriptName, main: import.meta.filename, ... },
      Effect.gen(function* () { ... }),
    ]);

The wrapper applies the tuple to the constructor itself; the args
persistence is unchanged. Resource (non-Platform) gets the same
treatment with a 2-tuple \`[id, props]\`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six tests covering the deploy-time encode and a runtime-side decode
that mirrors the generated worker entrypoint:

- marker exposed on the wrapper
- plain args land as JSON-encoded plain_text bindings under
  __ALCHEMY_FACTORY_ARG_<i>__ + __ALCHEMY_FACTORY_ARG_COUNT__
- Redacted args stay Redacted in env so the lifecycle binds
  secret_text, with the JSON \`_tag: "Redacted"\` marker preserved
- Output args pass through as Output for the engine resolver to
  unwrap at deploy time
- decode round-trip rebuilds Redacted and primitives
- makeFactory (the shared helper used by both Platform and Resource
  overloads) attaches the marker and stamps the args

Reorders the factory overload before the methods overload in
ResourceConstructor — TS overload resolution picks the first match,
and a function value also satisfies \`{ [key: string]: any }\`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant