1import { Icon } from "@iconify/react";
2import { useEffect, useRef, useState } from "react";
3import { highlightTS } from "../marketing/highlightTS";
4
5/**
6 * Showcase for the "Compose Effects and Layers..." (Effect-pilled) section.
7 *
8 * Single terminal-chromed card with a tab strip in the header and one
9 * focused example per tab. Each panel pairs a trimmed code snippet (left)
10 * with a stylized "generated artifact" panel (right) that visually proves
11 * what Alchemy wired up. Tabs auto-cycle while in view; tap to pin.
12 *
13 * Tabs:
14 * - IAM bindings — `S3.GetObject(...)` → permissions + env vars
15 * - Event sources — `DynamoDB.stream(...).process(...)` → EventSourceMapping
16 * - Durable Objects — `Cloudflare.DurableObject`
17 * - Containers — `Cloudflare.Container`
18 * - Workflows — `Cloudflare.Workflow`
19 * - Layers — `Layer.effect(JobStorage, ...)`
20 */
21
22type Tab = "iam" | "stream" | "do" | "container" | "workflow" | "layer";
23
24const TABS: { id: Tab; label: string }[] = [
25 { id: "iam", label: "IAM bindings" },
26 { id: "stream", label: "Event sources" },
27 { id: "do", label: "Durable Objects" },
28 { id: "container", label: "Containers" },
29 { id: "workflow", label: "Workflows" },
30 { id: "layer", label: "Layers" },
31];
32
33const CYCLE_MS = 6000;
34/** Longer dwell on the Layer tab so both impls (ddb / d1) get airtime. */
35const LAYER_CYCLE_MS = 13000;
36/** How long each Layer impl is "held" before the breathing cross-fade swaps. */
37const LAYER_IMPL_SWAP_MS = 3500;
38
39const IAM_CODE = `export default AWS.Lambda.Function(
40 "JobApi",
41 Effect.gen(function* () {
42 const getPhoto = yield* S3.GetObject(Photos);
43 const putJob = yield* DynamoDB.PutItem(Jobs);
44
45 return {
46 fetch: Effect.gen(function* () {
47 const req = yield* HttpServerRequest;
48 return yield* getPhoto({ key: req.url.slice(1) });
49 }),
50 };
51 }),
52);`;
53
54const STREAM_CODE = `export default AWS.Lambda.Function(
55 "JobsConsumer",
56 Effect.gen(function* () {
57 yield* DynamoDB.stream(Jobs).process((stream) =>
58 stream.pipe(
59 Stream.map((r) => r.dynamodb.NewImage),
60 Stream.tap((job) => Effect.log(\`new job: \${job.id.S}\`)),
61 Stream.runDrain,
62 ),
63 );
64 }),
65);`;
66
67const DO_CODE = `export default class Room extends Cloudflare.DurableObject<Room>()(
68 "Rooms",
69 Effect.gen(function* () {
70 const state = yield* Cloudflare.DurableObjectState;
71 return Effect.gen(function* () {
72 const sessions = new Map<string, Cloudflare.WebSocket>();
73
74 return {
75 fetch: Effect.gen(function* () {
76 const [response, socket] = yield* Cloudflare.upgrade();
77 sessions.set(crypto.randomUUID(), socket);
78 return response;
79 }),
80 };
81 });
82 }),
83) {}`;
84
85const CONTAINER_CODE = `export class Sandbox extends Cloudflare.Container<Sandbox, {
86 exec: (cmd: string) => Effect.Effect<ExecResult>;
87}>()("Sandbox") {}
88
89export const SandboxLive = Sandbox.make(
90 Stack.useSync((stack) => ({
91 main: import.meta.url,
92 instanceType: stack.stage === "prod" ? "standard-1" : "dev",
93 })),
94 Effect.gen(function* () {
95 const cp = yield* ChildProcessSpawner;
96 return Sandbox.of({
97 exec: (cmd) => cp.spawn(ChildProcess.make(cmd, { shell: true })),
98 });
99 }),
100);`;
101
102const WORKFLOW_CODE = `export default class Notifier extends Cloudflare.Workflow<Notifier>()(
103 "Notifier",
104 Effect.gen(function* () {
105 const rooms = yield* Room;
106 return Effect.gen(function* () {
107 const { roomId, message } = (yield* Cloudflare.Workflows.WorkflowEvent).payload;
108 yield* Cloudflare.Workflows.task("store", rooms.getByName(roomId).store(message));
109 yield* Cloudflare.Workflows.task("process", processMessage(message));
110 yield* Cloudflare.Workflows.task("broadcast", rooms.getByName(roomId).broadcast(message));
111 yield* Cloudflare.Workflows.sleep("cooldown", "2 seconds");
112 yield* Cloudflare.Workflows.task("finalize", rooms.getByName(roomId).cleanup());
113 });
114 }),
115) {}`;
116
117type LayerImpl = "ddb" | "d1";
118
119/**
120 * The Layer tab renders a structural diff between JobStorageDynamoDB and
121 * JobStorageD1. The two snippets are split into segments: "common" lines
122 * stay static; each "diff" segment overlays the ddb and d1 variants in the
123 * same grid cell so the inactive one fully hides (opacity 0) and the active
124 * one occupies that space — crossfading smoothly between them.
125 *
126 * Structurally parallel layout keeps the diff readable column-for-column.
127 */
128type LayerSegment =
129 | { kind: "common"; lines: string[] }
130 | { kind: "diff"; ddb: string[]; d1: string[] };
131
132const LAYER_SEGMENTS: LayerSegment[] = [
133 {
134 kind: "common",
135 lines: [
136 "export class JobStorage extends Context.Service<JobStorage, {",
137 " putJob: (job: Job) => Effect.Effect<Job, PutJobError>;",
138 " getJob: (id: string) => Effect.Effect<Job | undefined, GetJobError>;",
139 '}>()("JobStorage") {}',
140 "",
141 ],
142 },
143 {
144 kind: "diff",
145 ddb: [
146 "export const JobStorageDynamoDB = Layer.effect(JobStorage, Effect.gen(function* () {",
147 ' const table = yield* DynamoDB.Table("JobsTable", { partitionKey: "id" });',
148 " const putItem = yield* DynamoDB.PutItem(table);",
149 " const getItem = yield* DynamoDB.GetItem.bind(table);",
150 ],
151 d1: [
152 "export const JobStorageD1 = Layer.effect(JobStorage, Effect.gen(function* () {",
153 ' const db = yield* Cloudflare.D1.Database("JobsDB");',
154 " const conn = yield* Cloudflare.D1.QueryDatabase(db);",
155 "",
156 ],
157 },
158 {
159 kind: "common",
160 lines: [" return JobStorage.of({"],
161 },
162 {
163 kind: "diff",
164 ddb: [
165 " putJob: (job) => putItem({ Item: { id: { S: job.id } } }),",
166 " getJob: (id) => getItem({ Key: { id: { S: id } } }),",
167 ],
168 d1: [
169 " putJob: (job) => conn.exec(`INSERT INTO jobs (id) VALUES ('${job.id}')`),",
170 " getJob: (id) => conn.exec(`SELECT * FROM jobs WHERE id = '${id}'`),",
171 ],
172 },
173 {
174 kind: "common",
175 lines: [" });", "}));"],
176 },
177];
178
179const CODE_BY_TAB: Record<Exclude<Tab, "layer">, string> = {
180 iam: IAM_CODE,
181 stream: STREAM_CODE,
182 do: DO_CODE,
183 container: CONTAINER_CODE,
184 workflow: WORKFLOW_CODE,
185};
186
187export default function EffectPilledShowcase() {
188 const [tab, setTab] = useState<Tab>("iam");
189 const [pinned, setPinned] = useState(false);
190 const [layerImpl, setLayerImpl] = useState<LayerImpl>("ddb");
191 const wrapRef = useRef<HTMLDivElement>(null);
192
193 // Tab auto-cycle. Dwell longer on the Layer tab so both impls show
194 // before moving on to the next tab.
195 useEffect(() => {
196 if (pinned) return;
197 let cancelled = false;
198 let timer: ReturnType<typeof setTimeout> | null = null;
199
200 const schedule = () => {
201 const dwell = tab === "layer" ? LAYER_CYCLE_MS : CYCLE_MS;
202 timer = setTimeout(() => {
203 if (cancelled) return;
204 setTab((t) => {
205 const idx = TABS.findIndex((x) => x.id === t);
206 return TABS[(idx + 1) % TABS.length]!.id;
207 });
208 }, dwell);
209 };
210
211 const obs = new IntersectionObserver(
212 (entries) => {
213 for (const e of entries) {
214 if (e.isIntersecting) {
215 schedule();
216 } else if (timer) {
217 clearTimeout(timer);
218 timer = null;
219 }
220 }
221 },
222 { threshold: 0.3 },
223 );
224 if (wrapRef.current) obs.observe(wrapRef.current);
225
226 return () => {
227 cancelled = true;
228 if (timer) clearTimeout(timer);
229 obs.disconnect();
230 };
231 }, [tab, pinned]);
232
233 // Inner oscillation for the Layer tab — swap between ddb and d1
234 // implementations on a separate timer. Resets to "ddb" whenever we
235 // leave the tab so the user always sees the Dynamo example first.
236 useEffect(() => {
237 if (tab !== "layer") {
238 setLayerImpl("ddb");
239 return;
240 }
241 let cancelled = false;
242 const timer = setInterval(() => {
243 if (cancelled) return;
244 setLayerImpl((i) => (i === "ddb" ? "d1" : "ddb"));
245 }, LAYER_IMPL_SWAP_MS);
246 return () => {
247 cancelled = true;
248 clearInterval(timer);
249 };
250 }, [tab]);
251
252 const onTab = (next: Tab) => {
253 setPinned(true);
254 setTab(next);
255 };
256
257 const codeHtml = tab === "layer" ? null : highlightTS(CODE_BY_TAB[tab]);
258 // Drives the fade-in for the whole panel on tab switch. Intentionally
259 // does NOT include layerImpl — the Layer tab uses CSS transitions to
260 // breathe between its two impls without remounting.
261 const splitKey = tab;
262
263 return (
264 <div ref={wrapRef} className="eff-showcase" data-nosnippet="">
265 <div className="eff-showcase__chrome">
266 <div className="eff-showcase__header">
267 <span
268 className="alc-code-block__dot"
269 style={{ background: "var(--alc-danger)" }}
270 />
271 <span
272 className="alc-code-block__dot"
273 style={{ background: "var(--alc-warn)" }}
274 />
275 <span
276 className="alc-code-block__dot"
277 style={{ background: "var(--alc-accent-bright)" }}
278 />
279 <div className="eff-showcase__tabs" role="tablist">
280 {TABS.map((t) => (
281 <button
282 key={t.id}
283 type="button"
284 role="tab"
285 aria-selected={tab === t.id}
286 className="eff-showcase__tab"
287 data-active={tab === t.id}
288 onClick={() => onTab(t.id)}
289 >
290 {t.label}
291 </button>
292 ))}
293 </div>
294 </div>
295
296 <div className="eff-showcase__body">
297 <div className="eff-showcase__split" key={splitKey}>
298 <div className="eff-showcase__code">
299 {tab === "layer" ? (
300 <LayerCodeDiff impl={layerImpl} />
301 ) : (
302 <pre
303 className="eff-showcase__pre eff-showcase__fade"
304 dangerouslySetInnerHTML={{ __html: codeHtml ?? "" }}
305 />
306 )}
307 </div>
308 <div className="eff-showcase__artifact">
309 <ArtifactPanel tab={tab} layerImpl={layerImpl} />
310 </div>
311 </div>
312 </div>
313 </div>
314 <div className="eff-showcase__hint" aria-hidden>
315 {pinned ? (
316 <span>tap a tab to switch</span>
317 ) : (
318 <span>cycling · tap to pin</span>
319 )}
320 </div>
321 </div>
322 );
323}
324
325function ArtifactPanel({ tab, layerImpl }: { tab: Tab; layerImpl: LayerImpl }) {
326 switch (tab) {
327 case "iam":
328 return <IamPanel />;
329 case "stream":
330 return <StreamPanel />;
331 case "do":
332 return <DurableObjectPanel />;
333 case "container":
334 return <ContainerPanel />;
335 case "workflow":
336 return <WorkflowPanel />;
337 case "layer":
338 return <LayerPanel impl={layerImpl} />;
339 }
340}
341
342/* ───────────────────────── IAM bindings ───────────────────────── */
343
344function IamPanel() {
345 const PERMISSIONS = [
346 { icon: "logos:aws-s3", action: "s3:GetObject", target: "Photos/*" },
347 { icon: "logos:aws-dynamodb", action: "dynamodb:PutItem", target: "Jobs" },
348 ];
349 const ENV_VARS = [
350 { name: "PHOTOS_BUCKET", value: "arn:aws:s3:::photos-…" },
351 { name: "JOBS_TABLE", value: "arn:aws:dynamodb:…:table/Jobs-…" },
352 ];
353 return (
354 <>
355 <div className="eff-showcase__group">
356 <div className="eff-showcase__group-title">Permissions</div>
357 {PERMISSIONS.map((p, i) => (
358 <div
359 className="eff-showcase__perm"
360 key={p.action}
361 style={{ animationDelay: `${120 + i * 120}ms` }}
362 >
363 <Icon
364 icon={p.icon}
365 width={20}
366 height={20}
367 aria-hidden
368 className="eff-showcase__perm-icon"
369 />
370 <span className="eff-showcase__perm-action">{p.action}</span>
371 <span className="eff-showcase__perm-arrow" aria-hidden>
372 →
373 </span>
374 <span className="eff-showcase__perm-target">{p.target}</span>
375 </div>
376 ))}
377 </div>
378 <div className="eff-showcase__group">
379 <div className="eff-showcase__group-title">Environment</div>
380 <div className="eff-showcase__envs">
381 {ENV_VARS.map((e, i) => (
382 <div
383 className="eff-showcase__env"
384 key={e.name}
385 style={{ animationDelay: `${360 + i * 100}ms` }}
386 >
387 <span className="eff-showcase__env-name">{e.name}</span>
388 <span className="eff-showcase__env-eq" aria-hidden>
389 =
390 </span>
391 <span className="eff-showcase__env-value">{e.value}</span>
392 </div>
393 ))}
394 </div>
395 </div>
396 </>
397 );
398}
399
400/* ───────────────────────── Event sources ───────────────────────── */
401
402function StreamPanel() {
403 return (
404 <div className="eff-showcase__group">
405 <div className="eff-showcase__group-title">Wired event source</div>
406 <div className="eff-showcase__flow">
407 <FlowNode
408 icon="logos:aws-dynamodb"
409 label="Jobs"
410 sub="DynamoDB.Table"
411 delay={120}
412 />
413 <FlowEdge label="stream" delay={260} />
414 <FlowNode
415 icon="logos:aws-lambda"
416 label="EventSourceMapping"
417 sub="created by alchemy"
418 accent
419 delay={320}
420 />
421 <FlowEdge label="invoke" delay={440} />
422 <FlowNode
423 icon="mdi:lambda"
424 label="handler"
425 sub="your function"
426 delay={500}
427 />
428 </div>
429 </div>
430 );
431}
432
433/* ───────────────────────── Durable Objects ───────────────────────── */
434
435function DurableObjectPanel() {
436 const FEATURES = [
437 {
438 icon: "logos:cloudflare-icon",
439 label: "Cloudflare.DurableObject",
440 sub: "one instance per name · single-threaded",
441 },
442 {
443 icon: "mdi:database",
444 label: "DurableObjectState",
445 sub: "per-instance SQLite, persists across restarts",
446 },
447 {
448 icon: "mdi:transit-connection-variant",
449 label: "WebSocket sessions",
450 sub: "hibernation-safe via serializeAttachment",
451 },
452 ];
453 return (
454 <div className="eff-showcase__group">
455 <div className="eff-showcase__group-title">Generated DO</div>
456 {FEATURES.map((f, i) => (
457 <FeatureRow key={f.label} {...f} delay={120 + i * 120} />
458 ))}
459 </div>
460 );
461}
462
463/* ───────────────────────── Containers ───────────────────────── */
464
465function ContainerPanel() {
466 const FEATURES = [
467 {
468 icon: "logos:docker-icon",
469 label: "Image",
470 sub: "Dockerfile generated · platform builds",
471 },
472 {
473 icon: "mdi:server",
474 label: "Instance",
475 sub: "dev → 256 MB · prod → standard-1",
476 },
477 {
478 icon: "mdi:console",
479 label: "exec(cmd)",
480 sub: "typed Effect · spawns via ChildProcessSpawner",
481 },
482 ];
483 return (
484 <div className="eff-showcase__group">
485 <div className="eff-showcase__group-title">Generated container</div>
486 {FEATURES.map((f, i) => (
487 <FeatureRow key={f.label} {...f} delay={120 + i * 120} />
488 ))}
489 </div>
490 );
491}
492
493/* ───────────────────────── Workflows ───────────────────────── */
494
495function WorkflowPanel() {
496 const STEPS = [
497 { label: "store", sub: "task · durable retry" },
498 { label: "process", sub: "task · pure transform" },
499 { label: "broadcast", sub: "task · DO call" },
500 { label: "cooldown", sub: "sleep · 2 seconds" },
501 { label: "finalize", sub: "task · DO call" },
502 ];
503 return (
504 <div className="eff-showcase__group">
505 <div className="eff-showcase__group-title">Workflow steps</div>
506 <div className="eff-showcase__steps">
507 {STEPS.map((s, i) => (
508 <div
509 className="eff-showcase__step"
510 key={s.label}
511 style={{ animationDelay: `${100 + i * 90}ms` }}
512 >
513 <span className="eff-showcase__step-num">{i + 1}</span>
514 <span className="eff-showcase__step-label">{s.label}</span>
515 <span className="eff-showcase__step-sub">{s.sub}</span>
516 </div>
517 ))}
518 </div>
519 </div>
520 );
521}
522
523/* ───────────────────────── Layers ───────────────────────── */
524
525function LayerPanel({ impl }: { impl: LayerImpl }) {
526 // The "Composed from" group overlays the ddb and d1 row-stacks in the
527 // same grid cell so only the active variant is visible. Same crossfade
528 // pattern as the code diff on the left.
529 return (
530 <>
531 <div className="eff-showcase__group">
532 <div className="eff-showcase__group-title">Provides</div>
533 <FeatureRow
534 icon="mdi:layers-triple"
535 label="JobStorage"
536 sub="Context.Service · putJob · getJob"
537 delay={120}
538 />
539 </div>
540 <div className="eff-showcase__group">
541 <div className="eff-showcase__group-title">Composed from</div>
542 <div className="eff-showcase__variant-stack">
543 <div
544 className="eff-showcase__variant"
545 data-active={impl === "ddb"}
546 aria-hidden={impl !== "ddb"}
547 >
548 <FeatureRow
549 icon="logos:aws-dynamodb"
550 label="DynamoDB.Table"
551 sub='"JobsTable" · partitionKey: "id"'
552 delay={240}
553 />
554 <FeatureRow
555 icon="mdi:key"
556 label="GetItem · PutItem"
557 sub="bound capabilities · IAM auto-generated"
558 delay={340}
559 />
560 </div>
561 <div
562 className="eff-showcase__variant"
563 data-active={impl === "d1"}
564 aria-hidden={impl !== "d1"}
565 >
566 <FeatureRow
567 icon="logos:cloudflare-icon"
568 label="Cloudflare.D1.Database"
569 sub='"JobsDB" · serverless SQL'
570 delay={240}
571 />
572 <FeatureRow
573 icon="mdi:database-cog"
574 label="D1Connection"
575 sub="bound capability · prepare · exec · batch"
576 delay={340}
577 />
578 </div>
579 </div>
580 </div>
581 </>
582 );
583}
584
585/**
586 * Layer-tab code panel. Common lines render statically; each diff segment is
587 * an overlay (CSS grid with both variants in the same grid cell) so only the
588 * active variant is visible while the inactive one fades to opacity 0. The
589 * slot's height is the max of both variants, so the panel stays stable as
590 * the active impl swaps.
591 */
592function LayerCodeDiff({ impl }: { impl: LayerImpl }) {
593 return (
594 <div className="eff-showcase__diff">
595 {LAYER_SEGMENTS.map((seg, i) => {
596 if (seg.kind === "common") {
597 return (
598 <div key={i} className="eff-diff__common">
599 {seg.lines.map((line, j) => (
600 <CodeLine key={j} text={line} kind="common" />
601 ))}
602 </div>
603 );
604 }
605 return (
606 <div key={i} className="eff-diff__slot">
607 <div
608 className="eff-diff__variant"
609 data-active={impl === "ddb"}
610 aria-hidden={impl !== "ddb"}
611 >
612 {seg.ddb.map((line, j) => (
613 <CodeLine key={j} text={line} kind="ddb" />
614 ))}
615 </div>
616 <div
617 className="eff-diff__variant"
618 data-active={impl === "d1"}
619 aria-hidden={impl !== "d1"}
620 >
621 {seg.d1.map((line, j) => (
622 <CodeLine key={j} text={line} kind="d1" />
623 ))}
624 </div>
625 </div>
626 );
627 })}
628 </div>
629 );
630}
631
632function CodeLine({
633 text,
634 kind,
635}: {
636 text: string;
637 kind: "common" | "ddb" | "d1";
638}) {
639 const html = highlightTS(text) || "&nbsp;";
640 return (
641 <div className={`eff-diff__line eff-diff__line--${kind}`}>
642 <span className="eff-diff__marker" aria-hidden />
643 <span
644 className="eff-diff__content"
645 dangerouslySetInnerHTML={{ __html: html }}
646 />
647 </div>
648 );
649}
650
651/* ───────────────────────── Reusable building blocks ───────────────────────── */
652
653function FeatureRow({
654 icon,
655 label,
656 sub,
657 delay = 0,
658}: {
659 icon: string;
660 label: string;
661 sub: string;
662 delay?: number;
663}) {
664 return (
665 <div
666 className="eff-showcase__feature"
667 style={{ animationDelay: `${delay}ms` }}
668 >
669 <Icon
670 icon={icon}
671 width={22}
672 height={22}
673 aria-hidden
674 className="eff-showcase__feature-icon"
675 />
676 <div className="eff-showcase__feature-text">
677 <div className="eff-showcase__feature-label">{label}</div>
678 <div className="eff-showcase__feature-sub">{sub}</div>
679 </div>
680 </div>
681 );
682}
683
684function FlowNode({
685 icon,
686 label,
687 sub,
688 accent,
689 delay = 0,
690}: {
691 icon: string;
692 label: string;
693 sub: string;
694 accent?: boolean;
695 delay?: number;
696}) {
697 return (
698 <div
699 className={`eff-showcase__node${accent ? " eff-showcase__node--accent" : ""}`}
700 style={{ animationDelay: `${delay}ms` }}
701 >
702 <Icon icon={icon} width={22} height={22} aria-hidden />
703 <div className="eff-showcase__node-label">{label}</div>
704 <div className="eff-showcase__node-sub">{sub}</div>
705 </div>
706 );
707}
708
709function FlowEdge({ label, delay = 0 }: { label: string; delay?: number }) {
710 return (
711 <div
712 className="eff-showcase__edge"
713 data-label={label}
714 style={{ animationDelay: `${delay}ms` }}
715 aria-hidden
716 />
717 );
718}