From c826d942057ac80098511b97b096f0b884d3a204 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Tue, 31 Mar 2026 16:09:51 -0500 Subject: [PATCH] Add interactive Workflow Message Passing demo --- docs/workflow-message-passing-demo.mdx | 22 + .../WorkflowMessagingDemo/BuildIt.tsx | 272 ++++ .../WorkflowMessagingDemo/HowItWorks.tsx | 438 ++++++ .../WorkflowMessagingDemo/MessageTypes.tsx | 234 +++ .../WorkflowMessagingDemo/Overview.tsx | 158 ++ src/components/WorkflowMessagingDemo/Quiz.tsx | 151 ++ .../WorkflowMessagingDemo.module.css | 1382 +++++++++++++++++ .../WorkflowMessagingDemo/buildData.ts | 875 +++++++++++ .../WorkflowMessagingDemo/buildSteps.ts | 159 ++ .../WorkflowMessagingDemo/flowSteps.ts | 126 ++ .../WorkflowMessagingDemo/index.tsx | 50 + .../WorkflowMessagingDemo/quizQuestions.ts | 71 + 12 files changed, 3938 insertions(+) create mode 100644 docs/workflow-message-passing-demo.mdx create mode 100644 src/components/WorkflowMessagingDemo/BuildIt.tsx create mode 100644 src/components/WorkflowMessagingDemo/HowItWorks.tsx create mode 100644 src/components/WorkflowMessagingDemo/MessageTypes.tsx create mode 100644 src/components/WorkflowMessagingDemo/Overview.tsx create mode 100644 src/components/WorkflowMessagingDemo/Quiz.tsx create mode 100644 src/components/WorkflowMessagingDemo/WorkflowMessagingDemo.module.css create mode 100644 src/components/WorkflowMessagingDemo/buildData.ts create mode 100644 src/components/WorkflowMessagingDemo/buildSteps.ts create mode 100644 src/components/WorkflowMessagingDemo/flowSteps.ts create mode 100644 src/components/WorkflowMessagingDemo/index.tsx create mode 100644 src/components/WorkflowMessagingDemo/quizQuestions.ts diff --git a/docs/workflow-message-passing-demo.mdx b/docs/workflow-message-passing-demo.mdx new file mode 100644 index 0000000000..4ba997d1a1 --- /dev/null +++ b/docs/workflow-message-passing-demo.mdx @@ -0,0 +1,22 @@ +--- +id: workflow-message-passing-demo +title: Workflow Message Passing - Interactive Demo +sidebar_label: Workflow Message Passing +description: An interactive walkthrough of Temporal Workflow message passing — Signals, Queries, and Updates — what they are, how they work, and how to build with them. +slug: /workflow-message-passing-demo +tags: + - Workflow + - Signals + - Queries + - Updates +keywords: + - temporal workflow message passing + - signals queries updates + - workflow messaging demo + - temporal interactive +hide_table_of_contents: true +--- + +import WorkflowMessagingDemo from '@site/src/components/WorkflowMessagingDemo'; + + diff --git a/src/components/WorkflowMessagingDemo/BuildIt.tsx b/src/components/WorkflowMessagingDemo/BuildIt.tsx new file mode 100644 index 0000000000..963dae7149 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/BuildIt.tsx @@ -0,0 +1,272 @@ +import React, { useState } from 'react'; +import styles from './WorkflowMessagingDemo.module.css'; +import { + LANGUAGES, + MESSAGE_TYPE_DATA, + type LangId, + type MessageTypeId, +} from './buildData'; + +type Props = { onNext: () => void }; + +const MESSAGE_TABS: { id: MessageTypeId; label: string }[] = [ + { id: 'signal', label: 'Signal' }, + { id: 'query', label: 'Query' }, + { id: 'update', label: 'Update' }, +]; + +/* ── Line-highlighted code pane ── */ +function CodePane({ + title, + code, + activeLines, + accentColor, +}: { + title: string; + code: string; + activeLines: Set; + accentColor: string; +}) { + const lines = code.split('\n'); + const hasActive = activeLines.size > 0; + + return ( +
+
+ {title} +
+
+
+          {lines.map((line, i) => {
+            const lineNum = i + 1;
+            const isActive = activeLines.has(lineNum);
+            return (
+              
+ {line || ' '} +
+ ); + })} +
+
+
+ ); +} + +export default function BuildIt({ onNext }: Props) { + const [msgType, setMsgType] = useState('signal'); + const [lang, setLang] = useState('python'); + const [activeAnnotation, setActiveAnnotation] = useState(null); + + function selectMsgType(id: MessageTypeId) { + setMsgType(id); + setActiveAnnotation(null); + } + + function selectLang(id: LangId) { + setLang(id); + setActiveAnnotation(null); + } + + function toggleAnnotation(i: number) { + setActiveAnnotation((prev) => (prev === i ? null : i)); + } + + const data = MESSAGE_TYPE_DATA[msgType]; + const langCode = data.code[lang]; + const ann = activeAnnotation !== null ? data.annotations[activeAnnotation] : null; + const annLines = ann?.lines[lang]; + const wfLines = new Set(annLines?.workflowLines ?? []); + const clientLines = new Set(annLines?.clientLines ?? []); + + return ( +
+
+
+
+ +

Build It

+

+ Code examples for all five SDKs. Select a message type and language, then click an + annotation to highlight the relevant lines. +

+ + {/* Message type tabs */} +
+ {MESSAGE_TABS.map((t) => ( + + ))} +
+ + {/* Language tabs */} +
+ {LANGUAGES.map((l) => ( + + ))} +
+ +

+ {data.description} +

+ + {/* Two-column code view */} +
+ + +
+ + {/* Annotation pills */} +
+
+ Highlight a concept +
+
+ {data.annotations.map((a, i) => { + const isActive = activeAnnotation === i; + return ( + + ); + })} +
+
+ + {/* Annotation description box */} +
+ {ann && ( +

+ {ann.description} +

+ )} +
+ +
{data.note}
+ +
+ +
+
+ ); +} diff --git a/src/components/WorkflowMessagingDemo/HowItWorks.tsx b/src/components/WorkflowMessagingDemo/HowItWorks.tsx new file mode 100644 index 0000000000..82156b6290 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/HowItWorks.tsx @@ -0,0 +1,438 @@ +import React, { useState, useRef } from 'react'; +import styles from './WorkflowMessagingDemo.module.css'; + +type MsgType = 'signal' | 'query' | 'update'; +type Props = { onNext: () => void }; + +const ACCENT: Record = { + signal: 'var(--nd-green)', + query: 'var(--nd-purple)', + update: 'var(--ifm-color-primary)', +}; + +const STATE_KEY: Record = { + signal: 'approved', + query: 'status', + update: 'priority', +}; + +const STATE_BEFORE: Record = { + signal: 'false', + query: '"processing"', + update: '1', +}; + +const STATE_AFTER: Record = { + signal: 'true', + query: '"processing"', + update: '3', +}; + +const METHOD: Record = { + signal: 'approve()', + query: 'get_status()', + update: 'set_priority(3)', +}; + +const SUMMARY: Record = { + signal: { + default: 'Signal written to event history. Worker picked it up and updated Workflow state. Client moved on immediately — no reply.', + }, + query: { + default: 'Query went to a live Worker. Current state was read and returned synchronously. Nothing was written to history.', + }, + update: { + valid: 'Validator ran first and approved the input. State changed. Result returned to the client.', + invalid: 'Validator ran first and rejected the input. State is unchanged. Error returned to the client.', + }, +}; + +export default function HowItWorks({ onNext }: Props) { + const [msgType, setMsgType] = useState('signal'); + const [updateValid, setUpdateValid] = useState(true); + + // Animation progress values (0–1) + const [fwdA, setFwdA] = useState(0); // forward: full for signal/query; left half for update + const [fwdB, setFwdB] = useState(0); // forward: right half for update only + const [rev, setRev] = useState(0); // return arrow + + // Update validator state + const [valResult, setValResult] = useState(null); + + // Visual state + const [stateChanged, setStateChanged] = useState(false); + const [clientReceived, setClientReceived] = useState(null); + const [playing, setPlaying] = useState(false); + + const timers = useRef[]>([]); + + function schedule(fn: () => void, ms: number) { + const id = setTimeout(fn, ms); + timers.current.push(id); + } + + function reset() { + timers.current.forEach(clearTimeout); + timers.current = []; + setFwdA(0); + setFwdB(0); + setRev(0); + setValResult(null); + setStateChanged(false); + setClientReceived(null); + setPlaying(false); + } + + function changeType(t: MsgType) { + setMsgType(t); + reset(); + } + + function play() { + reset(); + schedule(() => setPlaying(true), 30); + schedule(() => setFwdA(1), 80); // triggers CSS transition + + if (msgType === 'signal') { + schedule(() => setStateChanged(true), 850); + schedule(() => setPlaying(false), 950); + } else if (msgType === 'query') { + schedule(() => setRev(1), 850); + schedule(() => setClientReceived('"processing"'), 1600); + schedule(() => setPlaying(false), 1700); + } else { + // update: fwdA goes to validator (left half) + schedule(() => setValResult(updateValid ? 'accept' : 'reject'), 850); + if (updateValid) { + schedule(() => setFwdB(1), 1200); + schedule(() => setStateChanged(true), 1950); + schedule(() => setRev(1), 2050); + schedule(() => setClientReceived('"Priority set to 3"'), 2800); + schedule(() => setPlaying(false), 2900); + } else { + schedule(() => setRev(1), 1350); + schedule(() => setClientReceived('Error: invalid priority'), 2100); + schedule(() => setPlaying(false), 2200); + } + } + } + + const isUpdate = msgType === 'update'; + const color = ACCENT[msgType]; + const hasRun = fwdA > 0; + const stateValue = stateChanged ? STATE_AFTER[msgType] : STATE_BEFORE[msgType]; + const stateActuallyChanged = stateChanged && msgType !== 'query'; + const revColor = valResult === 'reject' ? 'var(--nd-red)' : color; + + // Forward arrow widths + const fwdAWidth = isUpdate ? `${fwdA * 50}%` : `${fwdA * 100}%`; + const fwdBLeft = 'calc(50% + 30px)'; // right of validator badge + const fwdBWidth = `calc(${fwdB * 50}% - 30px)`; + + const summaryText = msgType === 'update' + ? (valResult === 'reject' ? SUMMARY.update.invalid : SUMMARY.update.valid) + : SUMMARY[msgType].default; + + return ( +
+
+
+
+ +

How It Works

+

+ Watch how each message type travels between your client code and a running Workflow. +

+ + {/* Type tabs */} +
+ {(['signal', 'query', 'update'] as MsgType[]).map(t => ( + + ))} +
+ + {/* Update: valid/invalid toggle */} + {isUpdate && ( +
+ {([true, false] as const).map(v => { + const active = updateValid === v; + const c = v ? color : 'var(--nd-red)'; + return ( + + ); + })} +
+ )} + + {/* Diagram */} +
+
+ + {/* CLIENT box */} +
+
+ Client +
+
+ {METHOD[msgType]} +
+ {clientReceived && ( +
+ ← {clientReceived} +
+ )} + {!clientReceived && playing && msgType !== 'signal' && ( +
waiting…
+ )} +
+ + {/* Arrow track */} +
+ + {/* ── Forward arrow ── */} + {/* Track background */} +
+ + {/* Segment A fill (full for signal/query; left half for update) */} +
+ + {/* Segment B fill (update only: right half, validator → workflow) */} + {isUpdate && ( +
+ )} + + {/* Arrowhead (right end of forward arrow) */} + {fwdA >= 0.99 && !isUpdate && ( +
+ )} + {isUpdate && fwdB >= 0.99 && valResult === 'accept' && ( +
+ )} + + {/* Method name label */} + {fwdA > 0.3 && ( +
+ {METHOD[msgType]} +
+ )} + + {/* Validator badge (update only) */} + {isUpdate && ( +
+ + {valResult === 'accept' ? '✓' : valResult === 'reject' ? '✗' : '?'} + + validate +
+ )} + + {/* ── Return arrow ── */} + {(msgType === 'query' || msgType === 'update') && ( + <> + {/* Track background */} +
+ + {/* Return fill (grows right → left) */} +
+ + {/* Arrowhead (left end) */} + {rev >= 0.99 && ( +
+ )} + + {/* Return label */} + {clientReceived && ( +
+ {clientReceived} +
+ )} + + )} + + {/* "No reply" label for signal */} + {msgType === 'signal' && fwdA >= 0.99 && ( +
+ no reply +
+ )} +
+ + {/* WORKFLOW box */} +
+
+ Workflow +
+
+ {STATE_KEY[msgType]}: + {' '} + + {stateValue} + +
+ {msgType === 'query' && fwdA > 0 && ( +
+ read only +
+ )} +
+
+ + {/* Summary after animation */} + {hasRun && !playing && summaryText && ( +
+ {summaryText} +
+ )} +
+ + {/* Controls */} +
+ + {hasRun && !playing && ( + + )} +
+ +
+ +
+
+ ); +} diff --git a/src/components/WorkflowMessagingDemo/MessageTypes.tsx b/src/components/WorkflowMessagingDemo/MessageTypes.tsx new file mode 100644 index 0000000000..5f3643e1b5 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/MessageTypes.tsx @@ -0,0 +1,234 @@ +import React, { useState } from 'react'; +import styles from './WorkflowMessagingDemo.module.css'; + +type Props = { onNext: () => void }; +type TypeId = 'signal' | 'query' | 'update'; + +const TYPES: { id: TypeId; label: string }[] = [ + { id: 'signal', label: 'Signal' }, + { id: 'query', label: 'Query' }, + { id: 'update', label: 'Update' }, +]; + +const TYPE_CONTENT = { + signal: { + tagLabel: 'Async · No response', + headline: 'One-way notification, no response needed.', + description: + `A Signal is a one-way message to a running Workflow. You send it and your code keeps going. No waiting, no response. Temporal writes the signal to the Workflow's event history, so delivery is guaranteed even if the Worker is down when you send it. The handler runs as soon as the Worker is back.`, + properties: [ + { label: 'Direction', value: 'Caller to Workflow (one-way)' }, + { label: 'Response', value: 'None' }, + { label: 'Buffered?', value: 'Yes, delivered even if Worker is offline' }, + { label: 'Can change state?', value: 'Yes' }, + { label: 'Can be async?', value: 'Yes' }, + ], + useCases: [ + 'Approve or reject a pending order', + 'Tell a Workflow that a payment cleared', + 'Cancel or pause a long-running job', + 'Unblock a Workflow waiting on a human decision', + ], + code: `@workflow.defn +class OrderWorkflow: + + @workflow.signal + async def approve(self) -> None: + self._approved = True + self._status = "approved" + + @workflow.signal + async def cancel(self, reason: str) -> None: + self._status = f"cancelled: {reason}"`, + clientCode: `# Send it. No waiting for a response. +await handle.signal(OrderWorkflow.approve) +await handle.signal(OrderWorkflow.cancel, "out of stock")`, + accentColor: 'var(--nd-green)', + bgColor: 'var(--nd-green-bg)', + }, + query: { + tagLabel: 'Sync · Read-only', + headline: 'Ask a question. Get an answer.', + description: + `A Query reads the current in-memory state of a running Workflow. Your code sends the query and waits synchronously for the value. Query handlers must be plain synchronous functions: no async, no side effects, no state changes. If no Worker is running when you query, it fails. Queries are not buffered.`, + properties: [ + { label: 'Direction', value: 'Caller to Workflow, back to Caller' }, + { label: 'Response', value: 'The return value of the handler' }, + { label: 'Buffered?', value: 'No, requires a live Worker' }, + { label: 'Can change state?', value: 'No, read-only' }, + { label: 'Can be async?', value: 'No, must be a regular def' }, + ], + useCases: [ + 'Check the current status of an order', + 'Read how far along a long job is', + 'Pull counters or config for a status dashboard', + 'Inspect state before deciding what to do next', + ], + code: `@workflow.defn +class OrderWorkflow: + + @workflow.query + def get_status(self) -> str: + return self._status + + @workflow.query + def get_details(self) -> dict: + return { + "status": self._status, + "approved": self._approved, + }`, + clientCode: `# Waits synchronously for the response +status = await handle.query(OrderWorkflow.get_status) +details = await handle.query(OrderWorkflow.get_details)`, + accentColor: 'var(--nd-purple)', + bgColor: 'var(--nd-purple-bg)', + }, + update: { + tagLabel: 'Sync · Validated · Returns result', + headline: 'Change state and get confirmation.', + description: + `An Update is the most capable type: it can change Workflow state (like a Signal) and return a result to the caller (like a Query). It also has an optional validator that runs before any changes happen. If the validator rejects the input, the Workflow state is untouched and the caller gets a clean error. The caller waits for the outcome.`, + properties: [ + { label: 'Direction', value: 'Caller to Workflow, back to Caller' }, + { label: 'Response', value: 'Return value of the handler, or a rejection error' }, + { label: 'Buffered?', value: 'Yes, durable like a Signal' }, + { label: 'Can change state?', value: 'Yes' }, + { label: 'Has validator?', value: 'Optional, runs before the handler' }, + ], + useCases: [ + 'Change a setting and confirm the new value', + 'Reserve a slot and get back a confirmation ID', + 'Apply a correction to a running job and verify it took effect', + 'Extend a deadline only if the Workflow is still in a valid state', + ], + code: `@workflow.defn +class OrderWorkflow: + + @workflow.update + async def set_priority(self, priority: int) -> str: + self._priority = priority + return f"Priority set to {priority}" + + @set_priority.validator + def validate_priority(self, priority: int) -> None: + if not (1 <= priority <= 5): + raise ValueError(f"Priority must be 1-5")`, + clientCode: `# Waits for a result or a rejection +try: + result = await handle.execute_update( + OrderWorkflow.set_priority, 3 + ) + print(result) # "Priority set to 3" +except Exception as e: + print(e) # "Priority must be 1-5"`, + accentColor: 'var(--ifm-color-primary)', + bgColor: 'var(--nd-primary-bg)', + }, +}; + +export default function MessageTypes({ onNext }: Props) { + const [active, setActive] = useState('signal'); + const content = TYPE_CONTENT[active]; + + return ( +
+
+
+
+ +

The Three Message Types

+

+ Each type handles a different situation. Pick based on whether you need a response, + whether you need to change state, and whether the caller can wait. +

+ +
+ {TYPES.map((t) => ( + + ))} +
+ +
+
+ {content.tagLabel} +
+

{content.headline}

+

{content.description}

+
+ +
+
+

Properties

+ + + {content.properties.map((p) => ( + + + + + ))} + +
+ {p.label} + + {p.value} +
+
+ +
+

Good for

+
    + {content.useCases.map((u) => ( +
  • {u}
  • + ))} +
+
+
+ +

Quick reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SituationUseWhy
Push a notification, no response neededSignalAsync, durable delivery, no waiting
Read current state without touching itQuerySynchronous read, no history entry written
Change state and confirm it workedUpdateValidates input, returns a result, durable
One Workflow triggers work in anotherSignalStandard pattern for Workflow-to-Workflow communication
+ +
+ +
+
+ ); +} diff --git a/src/components/WorkflowMessagingDemo/Overview.tsx b/src/components/WorkflowMessagingDemo/Overview.tsx new file mode 100644 index 0000000000..a68f4a0fe2 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/Overview.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import styles from './WorkflowMessagingDemo.module.css'; + +type Props = { onNext: () => void }; + +export default function Overview({ onNext }: Props) { + return ( +
+
+
+
+ +

What is Workflow Message Passing?

+

+ You've started a long-running Workflow. It's running. Now what? How do you check on + its progress? How do you tell it something changed? How do you get a value out of it? +

+ +
+

+ The core idea in one sentence +

+

+ You define named handlers on your Workflow for three types of messages:{' '} + Signals (push a notification in),{' '} + Queries (read state out), and{' '} + Updates (change state and get a result back). + Your client calls them by name on a Workflow handle, and the running Workflow reacts. +

+
+ +

A concrete way to see it

+
+

+ Say you have a chef cooking a multi-course meal. The meal is in progress + and the chef keeps working, but the rest of the restaurant still needs to reach them: +

+
+
+
Signal
+

+ A waiter tells the chef "Table six would like more bread." The chef notes it and keeps cooking. + No reply needed. The waiter moves on. +

+
+
+
Query
+

+ A manager asks "How many courses have gone out?" The chef checks the board + and answers without stopping work. +

+
+
+
Update
+

+ A customer asks "Can you add a dessert?" The chef checks if there's still time. + If yes: "Added." If no: "The order is already closed." The customer waits for that answer + before leaving the table. +

+
+
+
+ +

The three types, side by side

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeLike saying...Gets a response?Can change state?
Signal"Hey, FYI"NoYes
Query"What's the current status?"Yes (read-only)No
Update"Please change X and confirm it"Yes (result or rejection)Yes
+ +

What it looks like in code

+
+
+
Your client code
+
+
C
+
+
Order Service
+
sends messages to the Workflow
+
+
+
+ handle.signal(approve) +
+ handle.query(get_status) +
+ handle.execute_update(set_priority, 3) +
+
+ +
+
+
+
Temporal Server
+
Routes · Persists · Delivers
+
+
+
+ +
+
Your Workflow
+
+
W
+
+
OrderWorkflow
+
running, waiting for messages
+
+
+
+ @workflow.signal approve() +
+ @workflow.query get_status() +
+ @workflow.update set_priority() +
+
+
+ +
+ +
+
+ ); +} diff --git a/src/components/WorkflowMessagingDemo/Quiz.tsx b/src/components/WorkflowMessagingDemo/Quiz.tsx new file mode 100644 index 0000000000..05cf219ecf --- /dev/null +++ b/src/components/WorkflowMessagingDemo/Quiz.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import styles from './WorkflowMessagingDemo.module.css'; +import { quizQuestions } from './quizQuestions'; + +type AnswerState = number | null; + +export default function Quiz() { + const [answers, setAnswers] = useState( + () => new Array(quizQuestions.length).fill(null), + ); + const [key, setKey] = useState(0); + + const allAnswered = answers.every((a) => a !== null); + const correctCount = answers.filter((a, i) => a === quizQuestions[i].correct).length; + const pct = Math.round((correctCount / quizQuestions.length) * 100); + + function answer(qi: number, oi: number) { + if (answers[qi] !== null) return; + setAnswers((prev) => { + const next = [...prev]; + next[qi] = oi; + return next; + }); + } + + function reset() { + setAnswers(new Array(quizQuestions.length).fill(null)); + setKey((k) => k + 1); + } + + return ( +
+
+
+
+ +

Test Yourself

+ +
+ {quizQuestions.map((q, qi) => { + const given = answers[qi]; + const answered = given !== null; + + return ( +
+
+ Q{qi + 1}. {q.q} +
+
+ {q.options.map((opt, oi) => { + const isCorrect = oi === q.correct; + const isChosen = oi === given; + + let optClass = styles.quizOpt; + if (answered && isChosen && isCorrect) optClass += ` ${styles.quizOptCorrect}`; + else if (answered && isChosen && !isCorrect) + optClass += ` ${styles.quizOptWrong}`; + else if (answered && isCorrect) optClass += ` ${styles.quizOptCorrect}`; + + return ( + + ); + })} +
+ + {answered && ( +
+ {given === q.correct ? 'Correct: ' : 'Not quite: '} + {q.explanation} +
+ )} +
+ ); + })} +
+ + {allAnswered && ( +
+
+ {correctCount} / {quizQuestions.length} correct ({pct}%) +
+ +
+ )} + +

Resources

+
+
+
Encyclopedia
+

Workflow Message Passing

+

+ Full reference for Signals, Queries, and Updates: delivery guarantees, edge cases, and how each type is recorded in event history. +

+ + Read the docs + +
+ +
+
SDK Guides
+

Message passing by language

+

+ SDK-specific guidance for Go, TypeScript, Python, Java, and .NET. +

+
+ Go + TypeScript + Python + Java + .NET +
+
+ +
+
Signals
+

Signal with Start

+

+ One atomic operation: starts a Workflow if it does not exist, then sends a signal to it. Useful when you are not sure if the Workflow is already running. +

+ + Learn more + +
+ +
+
Updates
+

Async Updates

+

+ You can send an Update and poll for the result later without blocking your client. Useful when the Update handler takes a while to complete. +

+ + Explore + +
+
+
+ ); +} diff --git a/src/components/WorkflowMessagingDemo/WorkflowMessagingDemo.module.css b/src/components/WorkflowMessagingDemo/WorkflowMessagingDemo.module.css new file mode 100644 index 0000000000..6a1034f145 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/WorkflowMessagingDemo.module.css @@ -0,0 +1,1382 @@ +/* + * WorkflowMessagingDemo.module.css + * Uses Docusaurus IFM variables for automatic light/dark mode support. + * Demo-specific accent colors are declared per-theme below. + */ + +/* ── Per-theme accent vars ─────────────────────────────── */ +:global([data-theme='dark']) { + --nd-surface: #1e1e1e; + --nd-surface2: #252525; + --nd-accent-text: #d4d7ff; /* Very light periwinkle — readable on dark, Temporal UV family */ + --nd-num-badge-bg: #7F86F1; /* Temporal Lilac for number badges */ + --nd-border: rgba(255, 255, 255, 0.1); + --nd-muted: #94a3b8; + /* Nav tab colors — active/hover purple, inactive stays muted */ + --nd-nav-inactive: #94a3b8; + --nd-nav-hover: #d4d7ff; + --nd-nav-active: #7F86F1; + --nd-progress-fill: #7F86F1; + --nd-purple: #a78bfa; + --nd-purple-bg: rgba(167, 139, 250, 0.12); + --nd-green: #34d399; + --nd-green-bg: rgba(52, 211, 153, 0.1); + --nd-red: #f87171; + --nd-red-bg: rgba(248, 113, 113, 0.1); + --nd-orange: #f97316; + --nd-orange-bg: rgba(249, 115, 22, 0.15); + --nd-amber: #fbbf24; + --nd-amber-bg: rgba(251, 191, 36, 0.1); + --nd-primary-bg: rgba(191, 219, 254, 0.1); + /* Temporal brand accent colors */ + --nd-uv: #444CE7; + --nd-uv-badge-bg: rgba(68, 76, 231, 0.2); + --nd-uv-badge-border: rgba(68, 76, 231, 0.4); + --nd-uv-glow: rgba(68, 76, 231, 0.25); + --nd-teal: #1FF1A5; + --nd-teal-badge-bg: rgba(31, 241, 165, 0.15); + --nd-teal-badge-border: rgba(31, 241, 165, 0.3); + --nd-teal-glow: rgba(31, 241, 165, 0.18); + --nd-pink: #FF6BFF; + --nd-pink-badge-bg: rgba(255, 107, 255, 0.15); + --nd-pink-badge-border: rgba(255, 107, 255, 0.3); + --nd-pink-glow: rgba(255, 107, 255, 0.18); + --nd-grellow: #CFFF0D; + --nd-grellow-badge-bg: rgba(207, 255, 13, 0.12); + --nd-grellow-badge-border: rgba(207, 255, 13, 0.25); + --nd-grellow-glow: rgba(207, 255, 13, 0.14); + /* Card surface for Option D style */ + --nd-comp-card-bg: rgba(31, 32, 63, 0.6); + --nd-comp-card-border: rgba(127, 134, 241, 0.15); +} + +:global([data-theme='light']) { + --nd-surface: #ffffff; + --nd-surface2: #f1f5f9; + --nd-accent-text: #444CE7; /* Temporal UV Blue — readable on light */ + --nd-num-badge-bg: #444CE7; /* UV Blue for number badges in light */ + --nd-border: rgba(0, 0, 0, 0.08); + --nd-muted: #64748b; + /* Nav tab colors — UV blue in light mode */ + --nd-nav-inactive: #64748b; + --nd-nav-hover: #444CE7; + --nd-nav-active: #444CE7; + --nd-progress-fill: #444CE7; + --nd-purple: #7c3aed; + --nd-purple-bg: rgba(124, 58, 237, 0.08); + --nd-green: #059669; + --nd-green-bg: rgba(5, 150, 105, 0.08); + --nd-red: #dc2626; + --nd-red-bg: rgba(220, 38, 38, 0.08); + --nd-orange: #ea580c; + --nd-orange-bg: rgba(234, 88, 12, 0.1); + --nd-amber: #d97706; + --nd-amber-bg: rgba(217, 119, 6, 0.08); + --nd-primary-bg: rgba(29, 78, 216, 0.06); + /* Temporal brand accent colors — light mode adapted */ + --nd-uv: #3a41cc; + --nd-uv-badge-bg: rgba(58, 65, 204, 0.1); + --nd-uv-badge-border: rgba(58, 65, 204, 0.25); + --nd-uv-glow: rgba(58, 65, 204, 0.07); + --nd-teal: #059669; + --nd-teal-badge-bg: rgba(5, 150, 105, 0.1); + --nd-teal-badge-border: rgba(5, 150, 105, 0.25); + --nd-teal-glow: rgba(5, 150, 105, 0.07); + --nd-pink: #9333ea; + --nd-pink-badge-bg: rgba(147, 51, 234, 0.1); + --nd-pink-badge-border: rgba(147, 51, 234, 0.25); + --nd-pink-glow: rgba(147, 51, 234, 0.07); + --nd-grellow: #65a30d; + --nd-grellow-badge-bg: rgba(101, 163, 13, 0.1); + --nd-grellow-badge-border: rgba(101, 163, 13, 0.25); + --nd-grellow-glow: rgba(101, 163, 13, 0.07); + /* Card surface for Option D style */ + --nd-comp-card-bg: #ffffff; + --nd-comp-card-border: rgba(0, 0, 0, 0.09); +} + +/* ── Shell ─────────────────────────────────────────────── */ +.shell { + font-family: var(--ifm-font-family-base); + color: var(--ifm-font-color-base); + background: var(--ifm-background-color); + min-height: 100vh; +} + +:global([data-theme='light']) .shell { + background: #f4f6f9; +} + +:global([data-theme='light']) .card { + background: #ffffff; +} + +/* ── Nav ───────────────────────────────────────────────── */ +.nav { + position: sticky; + top: var(--ifm-navbar-height); + z-index: 50; + background: var(--ifm-background-color); + border-bottom: 1px solid var(--nd-border); + display: flex; + align-items: center; + gap: 4px; + padding: 0 24px; + overflow-x: auto; + scrollbar-width: none; +} + +.nav::-webkit-scrollbar { + display: none; +} + +.navLogo { + font-weight: 700; + font-size: 14px; + color: var(--ifm-color-primary); + margin-right: 12px; + white-space: nowrap; + flex-shrink: 0; +} + +.navBtn { + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--nd-nav-inactive); + cursor: pointer; + font-size: 13px; + font-family: var(--ifm-font-family-base); + padding: 12px 14px; + white-space: nowrap; + transition: color 0.15s, border-color 0.15s; + flex-shrink: 0; + margin-bottom: -1px; +} + +.navBtn:hover { + color: var(--nd-nav-hover); +} + +.navBtnActive { + color: var(--nd-nav-active); + border-bottom-color: var(--nd-nav-active); +} + +/* ── Sections ──────────────────────────────────────────── */ +.section { + padding: 40px 24px 64px; + max-width: 960px; + margin: 0 auto; + animation: fadeUp 0.25s ease forwards; +} + +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: none; + } +} + +/* ── Progress bar ──────────────────────────────────────── */ +.progressBar { + height: 3px; + background: var(--nd-border); + border-radius: 0; + margin-bottom: 36px; + overflow: hidden; +} + +.progressFill { + height: 100%; + background: var(--nd-progress-fill); + border-radius: 0; + transition: width 0.4s ease; +} + +/* ── Typography ────────────────────────────────────────── */ +.lead { + font-size: 16px; + color: var(--ifm-font-color-base); + max-width: 680px; + margin-bottom: 32px; + line-height: 1.7; +} + +.sectionHeading { + font-size: 20px; + font-weight: 600; + color: var(--ifm-font-color-base); + margin: 32px 0 16px; +} + +/* ── Cards ─────────────────────────────────────────────── */ +.card { + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 20px; + margin-bottom: 16px; +} + +.cardGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-bottom: 32px; +} + +.cardGrid .card { + margin-bottom: 0; +} + +.tag { + display: inline-block; + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 0; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.tagBlue { + background: var(--nd-primary-bg); + color: var(--ifm-color-primary); +} + +.tagPurple { + background: var(--nd-purple-bg); + color: var(--nd-purple); +} + +.tagGreen { + background: var(--nd-green-bg); + color: var(--nd-green); +} + +.tagRed { + background: var(--nd-red-bg); + color: var(--nd-red); +} + +.tagAmber { + background: var(--nd-amber-bg); + color: var(--nd-amber); +} + +.tagOrange { + background: var(--nd-orange-bg); + color: var(--nd-orange); +} + +/* ── Namespace layout ──────────────────────────────────── */ +.nsContainer { + display: flex; + gap: 16px; + align-items: stretch; + flex-wrap: wrap; + margin: 24px 0; +} + +.nsBox { + flex: 1; + min-width: 200px; + background: var(--nd-surface); + border: 2px solid var(--nd-border); + border-radius: 0; + padding: 16px; + transition: border-color 0.2s; +} + +.nsBoxCaller { + border-color: var(--ifm-color-primary); +} + +.nsBoxHandler { + border-color: var(--nd-purple); +} + +.nsLabel { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + padding: 3px 10px; + border-radius: 0; + display: inline-block; + margin-bottom: 12px; +} + +.nsLabelCaller { + background: var(--nd-primary-bg); + color: var(--ifm-color-primary); +} + +.nsLabelHandler { + background: var(--nd-purple-bg); + color: var(--nd-purple); +} + +.nsArrow { + display: flex; + align-items: center; + justify-content: center; + color: var(--nd-muted); + font-size: 24px; + flex-shrink: 0; + flex-direction: column; + gap: 8px; +} + +.endpointBridge { + background: var(--nd-primary-bg); + border: 2px dashed var(--ifm-color-primary); + border-radius: 0; + padding: 14px 20px; + text-align: center; +} + +.endpointLabel { + font-size: 12px; + color: var(--ifm-color-primary); + font-weight: 700; +} + +.endpointSub { + font-size: 11px; + color: var(--nd-muted); + margin-top: 4px; +} + +/* ── Workflow blocks ────────────────────────────────────── */ +.wfBlock { + background: var(--nd-surface2); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 10px 14px; + margin-bottom: 8px; + font-size: 13px; + display: flex; + align-items: center; + gap: 8px; +} + +.wfIcon { + width: 24px; + height: 24px; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; +} + +.wfIconBlue { + background: var(--nd-primary-bg); + color: var(--ifm-color-primary); +} + +.wfIconPurple { + background: var(--nd-purple-bg); + color: var(--nd-purple); +} + +/* ── Buttons ────────────────────────────────────────────── */ +.btn { + background: var(--ifm-color-primary); + color: var(--ifm-background-color); + border: none; + padding: 10px 20px; + border-radius: 0; + cursor: pointer; + font-size: 13px; + font-weight: 600; + font-family: var(--ifm-font-family-base); + transition: opacity 0.15s, transform 0.1s; +} + +.btn:hover { + opacity: 0.88; + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(0); +} + +.btnSecondary { + background: var(--nd-surface2); + color: var(--ifm-font-color-base); + border: 1px solid var(--nd-border); +} + +.btnSecondary:hover { + opacity: 1; + background: var(--nd-surface); +} + +.nextRow { + text-align: right; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid var(--nd-border); +} + +/* ── How It Works: flow diagram ───────────────────────── */ +.flowOuter { + margin: 24px 0; +} + +.flowModeTabs { + display: flex; + gap: 4px; + margin-bottom: 20px; +} + +.flowModeTab { + padding: 8px 18px; + background: none; + border: 1px solid var(--nd-border); + border-radius: 0; + color: var(--nd-muted); + cursor: pointer; + font-size: 13px; + font-family: var(--ifm-font-family-base); + transition: all 0.15s; +} + +.flowModeTabActive { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: var(--ifm-background-color); +} + +.flowDiagram { + position: relative; /* anchor for singlePacket */ + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 28px 20px; + display: flex; + align-items: center; + gap: 0; + margin-bottom: 20px; + overflow: hidden; +} + +.flowNode { + background: var(--nd-surface2); + border: 2px solid var(--nd-border); + border-radius: 0; + padding: 12px 16px; + text-align: center; + flex: 0 0 auto; + width: 130px; + transition: border-color 0.3s, background 0.3s; + position: relative; + z-index: 2; +} + +.flowNodeActive { + border-color: var(--ifm-color-primary); + background: var(--nd-primary-bg); + animation: nodePulse 1.6s ease-out infinite; +} + +@keyframes nodePulse { + 0% { box-shadow: 0 0 0 0px var(--ifm-color-primary); } + 60% { box-shadow: 0 0 0 7px transparent; } + 100% { box-shadow: 0 0 0 7px transparent; } +} + +.flowNodeTitle { + font-size: 12px; + font-weight: 700; +} + +.flowNodeSub { + font-size: 10px; + color: var(--nd-muted); + margin-top: 3px; +} + +.flowTrackWrap { + flex: 1; + position: relative; + height: 32px; + display: flex; + align-items: center; +} + +.flowTrack { + width: 100%; + height: 2px; + background: var(--nd-border); + position: relative; +} + +.flowTrackFill { + position: absolute; + left: 0; + top: 0; + height: 100%; + background: var(--ifm-color-primary); + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Hide the dot when the track fill is empty (no signal traveling yet) */ +.flowTrackFillEmpty::after { + display: none; +} + +.packet { + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--ifm-color-primary); + box-shadow: 0 0 10px var(--ifm-color-primary); + top: 50%; + transform: translateY(-50%); + transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 3; +} + +/* Single continuously-moving packet across full track span */ +.singlePacket { + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--ifm-color-primary); + box-shadow: 0 0 12px var(--ifm-color-primary); + top: 50%; + margin-top: -6px; + z-index: 5; + pointer-events: none; +} + +/* ── Flow step detail ───────────────────────────────────── */ +.flowDetail { + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 16px 20px; + margin-bottom: 16px; + min-height: 72px; +} + +.flowDetailLabel { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; + color: var(--ifm-color-primary); +} + +.flowDetailText { + font-size: 13px; + color: var(--nd-muted); + line-height: 1.6; +} + +.flowControls { + display: flex; + gap: 10px; + align-items: center; + margin-top: 16px; + margin-bottom: 20px; +} + +.stepCounter { + font-size: 12px; + color: var(--nd-muted); + margin-left: auto; +} + +/* ── Status log ─────────────────────────────────────────── */ +.statusLog { + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 14px 16px; + font-family: var(--ifm-font-family-monospace); + font-size: 12px; + max-height: 180px; + overflow-y: auto; +} + +.logLine { + padding: 2px 0; + display: flex; + gap: 8px; +} + +.logTs { + color: var(--nd-muted); + flex-shrink: 0; +} + +.logEvent { + flex-shrink: 0; + min-width: 80px; +} + +.logEventInfo { color: var(--ifm-color-primary); } +.logEventSuccess { color: var(--nd-green); } +.logEventWarn { color: var(--nd-amber); } + +.logMsg { + color: var(--ifm-font-color-base); +} + +/* ── Build It ───────────────────────────────────────────── */ +.buildLayout { + display: grid; + grid-template-columns: 190px 1fr; + gap: 20px; + align-items: start; +} + +@media (max-width: 680px) { + .buildLayout { + grid-template-columns: 1fr; + } +} + +.buildStepList { + display: flex; + flex-direction: column; + gap: 4px; + position: sticky; + top: calc(var(--ifm-navbar-height) + 50px); +} + +.buildStepBtn { + background: none; + border: 1px solid transparent; + border-radius: 0; + padding: 10px 12px; + cursor: pointer; + text-align: left; + font-family: var(--ifm-font-family-base); + transition: all 0.15s; + width: 100%; +} + +.buildStepBtn:hover { + background: var(--nd-surface2); + border-color: var(--nd-border); +} + +.buildStepBtnActive { + background: var(--nd-primary-bg); + border-color: var(--ifm-color-primary); +} + +.buildStepNum { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--nd-muted); + margin-bottom: 2px; +} + +.buildStepBtnActive .buildStepNum { + color: var(--ifm-color-primary); +} + +.buildStepTitle { + font-size: 12px; + font-weight: 500; + color: var(--ifm-font-color-base); + line-height: 1.4; +} + +.buildPanel { + min-width: 0; +} + +.buildFileHeader { + font-family: var(--ifm-font-family-monospace); + font-size: 12px; + color: var(--nd-muted); + background: var(--nd-surface2); + border: 1px solid var(--nd-border); + border-bottom: none; + border-radius: 0; + padding: 8px 16px; +} + +.buildNote { + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-top: none; + border-radius: 0; + padding: 14px 16px; + font-size: 13px; + color: var(--nd-muted); + line-height: 1.6; +} + +.buildNote strong { + color: var(--ifm-font-color-base); +} + +.buildNav { + display: flex; + gap: 8px; + margin-top: 16px; +} + +/* ── CodeBlock override: connect to file header ──────── */ +.buildPanel :global(.theme-code-block) { + border-radius: 0; + margin-bottom: 0; + border-top: none; +} + +/* ── Components section ─────────────────────────────────── */ +.componentLayout { + display: flex; + flex-direction: column; + gap: 20px; +} + +.componentRow { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.componentNum { + width: 48px; + height: 48px; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: 700; + flex-shrink: 0; +} + +.componentContent { + flex: 1; +} + +.componentTitle { + font-size: 17px; + font-weight: 600; + margin-bottom: 4px; + color: var(--ifm-font-color-base); +} + +.componentRole { + font-size: 12px; + color: var(--nd-muted); + margin-bottom: 10px; +} + +.componentCode { + background: var(--nd-surface2); + border-radius: 0; + padding: 10px 14px; + font-size: 12px; + font-family: var(--ifm-font-family-monospace); + color: var(--nd-muted); + margin-top: 10px; + line-height: 1.8; +} + +.componentPills { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 10px; +} + +.componentPill { + background: var(--nd-surface2); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 5px 12px; + font-size: 12px; + color: var(--ifm-font-color-base); +} + +.componentPillNew { + color: var(--nd-muted); + border-style: dashed; +} + +.opTabs { + display: flex; + gap: 4px; + border-bottom: 1px solid var(--nd-border); + margin-bottom: 14px; +} + +.opTab { + padding: 6px 14px; + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--nd-muted); + cursor: pointer; + font-size: 13px; + font-family: var(--ifm-font-family-base); + margin-bottom: -1px; + transition: color 0.15s, border-color 0.15s; +} + +.opTab:hover { + color: var(--ifm-font-color-base); +} + +.opTabActive { + color: var(--ifm-color-primary); + border-bottom-color: var(--ifm-color-primary); +} + +/* ── Timeline ────────────────────────────────────────────── */ +.timeline { + display: flex; + flex-direction: column; + gap: 0; + margin-top: 20px; +} + +.tlStep { + display: flex; + gap: 14px; + align-items: flex-start; + padding-bottom: 14px; + opacity: 0.25; + transition: opacity 0.5s ease, color 0.3s ease; +} + +.tlStepActive { + opacity: 1; +} + +.tlStepDone { + opacity: 0.55; +} + +.tlNum { + width: 26px; + height: 26px; + border-radius: 50%; + background: var(--nd-surface2); + border: 2px solid var(--nd-border); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + flex-shrink: 0; + transition: background 0.3s, border-color 0.3s; + color: var(--ifm-font-color-base); +} + +.tlStepActive .tlNum { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: var(--ifm-background-color); +} + +.tlStepDone .tlNum { + background: var(--nd-green); + border-color: var(--nd-green); + color: var(--ifm-background-color); +} + +.tlContent { + flex: 1; + padding-top: 2px; +} + +.tlTitle { + font-size: 13px; + font-weight: 600; + margin-bottom: 2px; + color: var(--ifm-font-color-base); +} + +.tlDesc { + font-size: 12px; + color: var(--nd-muted); +} + +/* ── Run It ─────────────────────────────────────────────── */ +.runTabs { + display: flex; + gap: 4px; + border-bottom: 1px solid var(--nd-border); + margin-bottom: 24px; +} + +.runTab { + padding: 8px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--nd-muted); + cursor: pointer; + font-size: 13px; + font-family: var(--ifm-font-family-base); + margin-bottom: -1px; + transition: color 0.15s, border-color 0.15s; +} + +.runTab:hover { + color: var(--ifm-font-color-base); +} + +.runTabActive { + color: var(--ifm-color-primary); + border-bottom-color: var(--ifm-color-primary); +} + +.runSteps { + display: flex; + flex-direction: column; + gap: 0; + margin-bottom: 24px; +} + +.runStep { + display: flex; + gap: 16px; + padding-bottom: 20px; + position: relative; +} + +.runStep:not(:last-child)::before { + content: ''; + position: absolute; + left: 14px; + top: 30px; + bottom: 0; + width: 1px; + background: var(--nd-border); +} + +.runStepNum { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--ifm-color-primary); + color: var(--ifm-background-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; +} + +.runStepBody { + flex: 1; + padding-top: 4px; +} + +.runStepTitle { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; +} + +.runStepDesc { + font-size: 13px; + color: var(--nd-muted); + margin-bottom: 10px; +} + +.repoLink { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + font-size: 13px; + font-weight: 500; + color: var(--ifm-font-color-base); + text-decoration: none; + transition: border-color 0.15s; +} + +.repoLink:hover { + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); + text-decoration: none; +} + +/* Run It command blocks */ +.runitLink { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 0; + font-size: 13px; + font-weight: 600; + text-decoration: none; + border: 1px solid var(--nd-border); + color: var(--ifm-font-color-base); + background: var(--nd-surface2); + transition: all 0.15s; +} + +.runitLink:hover { + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); + text-decoration: none; +} + +.runitLinkPrimary { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: var(--ifm-background-color); +} + +.runitLinkPrimary:hover { + opacity: 0.88; + color: var(--ifm-background-color); + border-color: var(--ifm-color-primary); +} + +.runitSectionLabel { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--nd-muted); + margin: 0 0 10px; +} + +.runitCmd { + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 10px 16px; + font-family: var(--ifm-font-family-monospace); + font-size: 13px; + margin-bottom: 6px; + display: flex; + gap: 12px; + align-items: baseline; +} + +.runitPrompt { + color: var(--nd-muted); + user-select: none; + flex-shrink: 0; + min-width: 30px; +} + +.runitCmdText { + color: var(--ifm-font-color-base); + flex: 1; + white-space: pre-wrap; + word-break: break-all; +} + +.runitOutput { + font-family: var(--ifm-font-family-monospace); + font-size: 12px; + color: var(--nd-green); + padding: 6px 16px; + margin-top: 2px; + margin-bottom: 8px; +} + +/* ── Quiz ───────────────────────────────────────────────── */ +.quizCard { + background: var(--nd-surface); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 24px; + margin-bottom: 16px; +} + +.quizQ { + font-size: 15px; + font-weight: 600; + margin-bottom: 16px; + color: var(--ifm-font-color-base); +} + +.quizOptions { + display: flex; + flex-direction: column; + gap: 8px; +} + +.quizOpt { + background: var(--nd-surface2); + border: 2px solid var(--nd-border); + border-radius: 0; + padding: 10px 16px; + cursor: pointer; + font-size: 13px; + font-family: var(--ifm-font-family-base); + color: var(--ifm-font-color-base); + text-align: left; + transition: border-color 0.15s; + width: 100%; +} + +.quizOpt:hover:not(:disabled) { + border-color: var(--ifm-color-primary); +} + +.quizOptCorrect { + border-color: var(--nd-green) !important; + background: var(--nd-green-bg) !important; + color: var(--nd-green) !important; +} + +.quizOptWrong { + border-color: var(--nd-red) !important; + background: var(--nd-red-bg) !important; + color: var(--nd-red) !important; +} + +.quizFeedback { + margin-top: 12px; + font-size: 13px; + padding: 10px 14px; + border-radius: 0; + line-height: 1.5; +} + +.quizFeedbackCorrect { + background: var(--nd-green-bg); + color: var(--nd-green); +} + +.quizFeedbackWrong { + background: var(--nd-red-bg); + color: var(--nd-red); +} + +.scoreCard { + background: var(--nd-surface); + border: 2px solid var(--ifm-color-primary); + border-radius: 0; + padding: 32px; + text-align: center; + margin-top: 24px; +} + +.scoreTitle { + font-size: 22px; + font-weight: 700; + margin-bottom: 8px; +} + +.scoreText { + font-size: 16px; + color: var(--ifm-color-primary); + font-weight: 600; + margin-bottom: 8px; +} + +.scoreSub { + font-size: 14px; + color: var(--nd-muted); + margin-bottom: 24px; +} + +/* ── Comparison table ───────────────────────────────────── */ +.table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + margin-bottom: 24px; +} + +.table th { + background: var(--nd-surface2); + padding: 10px 14px; + text-align: left; + font-weight: 600; + color: var(--nd-muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.table td { + padding: 10px 14px; + border-bottom: 1px solid var(--nd-border); + color: var(--ifm-font-color-base); +} + +.table tr:last-child td { + border-bottom: none; +} + +.table tr:hover td { + background: var(--nd-surface2); +} + +.check { + color: var(--nd-green); + font-weight: 700; +} + +.cross { + color: var(--nd-muted); +} + +/* ── Option E: UV blue minimal list ─────────────────────── */ +.compEList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.compECard { + display: flex; + border: 1px solid var(--nd-border); +} + +.compENumCol { + width: 60px; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 24px; + flex-shrink: 0; + border-right: 1px solid var(--nd-border); + background: var(--nd-surface2); +} + +.compENum { + width: 30px; + height: 30px; + background: var(--nd-num-badge-bg); + color: #fff; + font-size: 14px; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.compEContent { + padding: 24px; + flex: 1; + min-width: 0; +} + +.compERole { + font-size: 11px; + color: var(--nd-accent-text); + letter-spacing: 0.05em; + font-weight: 600; + margin-bottom: 12px; + text-transform: uppercase; +} + +.compECode { + font-family: var(--ifm-font-family-monospace); + font-size: 12px; + background: var(--nd-surface2); + border-left: 2px solid var(--nd-accent-text); + padding: 10px 14px; + margin-top: 12px; + color: var(--ifm-font-color-base); + line-height: 1.8; +} + +/* ── Option D: glass cards with gradient glow ───────────── */ +.compCard { + background: var(--nd-comp-card-bg); + border: 1px solid var(--nd-comp-card-border); + padding: 24px; + position: relative; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.compCard::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 70px; + pointer-events: none; +} + +.compCard1::after { background: linear-gradient(to top, var(--nd-uv-glow), transparent); } +.compCard2::after { background: linear-gradient(to top, var(--nd-teal-glow), transparent); } +.compCard3::after { background: linear-gradient(to top, var(--nd-pink-glow), transparent); } +.compCard4::after { background: linear-gradient(to top, var(--nd-grellow-glow), transparent); } + +.compBadge { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + font-weight: 800; + flex-shrink: 0; +} + +.compBadge1 { background: var(--nd-uv-badge-bg); color: var(--nd-uv); border: 1px solid var(--nd-uv-badge-border); } +.compBadge2 { background: var(--nd-teal-badge-bg); color: var(--nd-teal); border: 1px solid var(--nd-teal-badge-border); } +.compBadge3 { background: var(--nd-pink-badge-bg); color: var(--nd-pink); border: 1px solid var(--nd-pink-badge-border); } +.compBadge4 { background: var(--nd-grellow-badge-bg); color: var(--nd-grellow); border: 1px solid var(--nd-grellow-badge-border); } + +.compAccent1 { color: var(--nd-uv); } +.compAccent2 { color: var(--nd-teal); } +.compAccent3 { color: var(--nd-pink); } +.compAccent4 { color: var(--nd-grellow); } + +.compPill { + display: inline-block; + border: 1px solid var(--nd-comp-card-border); + color: var(--nd-muted); + font-size: 11px; + padding: 4px 10px; + margin: 3px 3px 0 0; + font-family: var(--ifm-font-family-monospace); +} + +.compPillAccent { + border-color: var(--nd-uv-badge-border); + color: var(--nd-uv); +} + +/* ── Inline code block (MessageTypes) ───────────────────── */ +.codeBlock { + background: var(--nd-surface2); + border: 1px solid var(--nd-border); + border-radius: 0; + padding: 16px 20px; + font-family: var(--ifm-font-family-monospace); + font-size: 13px; + color: var(--ifm-font-color-base); + line-height: 1.75; + overflow-x: auto; + margin: 0 0 4px; + white-space: pre; +} diff --git a/src/components/WorkflowMessagingDemo/buildData.ts b/src/components/WorkflowMessagingDemo/buildData.ts new file mode 100644 index 0000000000..f343330037 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/buildData.ts @@ -0,0 +1,875 @@ +export type LangId = 'python' | 'typescript' | 'go' | 'java' | 'dotnet'; +export type MessageTypeId = 'signal' | 'query' | 'update'; + +export const LANGUAGES: { id: LangId; label: string; syntax: string }[] = [ + { id: 'python', label: 'Python', syntax: 'python' }, + { id: 'typescript', label: 'TypeScript', syntax: 'typescript' }, + { id: 'go', label: 'Go', syntax: 'go' }, + { id: 'java', label: 'Java', syntax: 'java' }, + { id: 'dotnet', label: '.NET', syntax: 'csharp' }, +]; + +export type AnnotationLines = { + workflowLines: number[]; + clientLines: number[]; +}; + +export type Annotation = { + label: string; + description: string; + lines: Record; +}; + +export type LangCode = { + workflowCode: string; + clientCode: string; +}; + +export type MessageTypeData = { + description: string; + note: string; + accentColor: string; + code: Record; + annotations: Annotation[]; +}; + +/* ───────────────────────────────────────────────── + SIGNAL +───────────────────────────────────────────────── */ +export const signalData: MessageTypeData = { + description: + 'A Signal is a one-way push into a running Workflow. The caller does not wait for the handler to run. Temporal persists signals so they are delivered even if the Worker is temporarily offline.', + note: + 'The Workflow parks at the blocking condition until a Signal arrives and flips the flag. The client only waits for the server to accept the signal, not for the handler to finish.', + accentColor: 'var(--nd-green)', + code: { + python: { + workflowCode: `from temporalio import workflow + +@workflow.defn +class OrderWorkflow: + def __init__(self) -> None: + self._approved: bool = False + self._status: str = "pending" + + @workflow.run + async def run(self, order_id: str) -> str: + await workflow.wait_condition( + lambda: self._approved + ) + return f"Order {order_id} processed" + + @workflow.signal + async def approve(self) -> None: + self._approved = True + self._status = "approved" + + @workflow.signal + async def cancel(self, reason: str) -> None: + self._status = f"cancelled: {reason}"`, + clientCode: `from temporalio.client import Client +from order_workflow import OrderWorkflow + +async def main(): + client = await Client.connect("localhost:7233") + handle = await client.start_workflow( + OrderWorkflow.run, + "order-123", + id="order-wf-123", + task_queue="orders", + ) + + # Returns when server accepts the signal + await handle.signal(OrderWorkflow.approve) + + # Signals can carry arguments + await handle.signal( + OrderWorkflow.cancel, "out of stock" + )`, + }, + typescript: { + workflowCode: `import { defineSignal, setHandler, condition } from '@temporalio/workflow'; + +export const approveSignal = defineSignal('approve'); +export const cancelSignal = defineSignal<[string]>('cancel'); + +export async function orderWorkflow(orderId: string): Promise { + let approved = false; + let status = 'pending'; + + setHandler(approveSignal, () => { + approved = true; + status = 'approved'; + }); + + setHandler(cancelSignal, (reason: string) => { + status = \`cancelled: \${reason}\`; + }); + + await condition(() => approved); + return \`Order \${orderId} processed\`; +}`, + clientCode: `import { Client } from '@temporalio/client'; +import { orderWorkflow, approveSignal, cancelSignal } from './workflows'; + +async function main() { + const client = new Client(); + const handle = await client.workflow.start(orderWorkflow, { + taskQueue: 'orders', + workflowId: 'order-wf-123', + args: ['order-123'], + }); + + // Returns when server accepts the signal + await handle.signal(approveSignal); + + // Signals can carry arguments + await handle.signal(cancelSignal, 'out of stock'); +}`, + }, + go: { + workflowCode: `package workflows + +import "go.temporal.io/sdk/workflow" + +func OrderWorkflow(ctx workflow.Context, orderID string) (string, error) { + approved := false + status := "pending" + + workflow.SetSignalHandler(ctx, "approve", func() { + approved = true + status = "approved" + }) + + workflow.SetSignalHandler(ctx, "cancel", func(reason string) { + status = "cancelled: " + reason + }) + + _ = workflow.Await(ctx, func() bool { return approved }) + return "Order " + orderID + " processed", nil +}`, + clientCode: `package main + +import ( + "context" + "go.temporal.io/sdk/client" +) + +func main() { + c, _ := client.Dial(client.Options{}) + defer c.Close() + + run, _ := c.ExecuteWorkflow( + context.Background(), + client.StartWorkflowOptions{ + ID: "order-wf-123", TaskQueue: "orders", + }, + OrderWorkflow, "order-123", + ) + + // Returns when server accepts the signal + _ = c.SignalWorkflow( + context.Background(), + "order-wf-123", run.GetRunID(), "approve", nil, + ) + + // Signals can carry arguments + _ = c.SignalWorkflow( + context.Background(), + "order-wf-123", run.GetRunID(), "cancel", "out of stock", + ) +}`, + }, + java: { + workflowCode: `import io.temporal.workflow.*; + +@WorkflowInterface +public interface OrderWorkflow { + @WorkflowMethod + String run(String orderId); + + @SignalMethod + void approve(); + + @SignalMethod + void cancel(String reason); +} + +public class OrderWorkflowImpl implements OrderWorkflow { + private boolean approved = false; + private String status = "pending"; + + @Override + public String run(String orderId) { + Workflow.await(() -> approved); + return "Order " + orderId + " processed"; + } + + @Override + public void approve() { + approved = true; + status = "approved"; + } + + @Override + public void cancel(String reason) { + status = "cancelled: " + reason; + } +}`, + clientCode: `WorkflowOptions options = WorkflowOptions.newBuilder() + .setWorkflowId("order-wf-123") + .setTaskQueue("orders") + .build(); +OrderWorkflow workflow = client.newWorkflowStub( + OrderWorkflow.class, options +); +WorkflowClient.start(workflow::run, "order-123"); + +// Returns when server accepts the signal +workflow.approve(); + +// Signals can carry arguments +workflow.cancel("out of stock");`, + }, + dotnet: { + workflowCode: `using Temporalio.Workflows; + +[Workflow] +public class OrderWorkflow +{ + private bool _approved = false; + private string _status = "pending"; + + [WorkflowRun] + public async Task RunAsync(string orderId) + { + await Workflow.WaitConditionAsync(() => _approved); + return $"Order {orderId} processed"; + } + + [WorkflowSignal] + public async Task ApproveAsync() + { + _approved = true; + _status = "approved"; + } + + [WorkflowSignal] + public async Task CancelAsync(string reason) + { + _status = $"cancelled: {reason}"; + } +}`, + clientCode: `var client = await TemporalClient.ConnectAsync( + new("localhost:7233") +); +var handle = await client.StartWorkflowAsync( + (OrderWorkflow wf) => wf.RunAsync("order-123"), + new WorkflowOptions("order-wf-123", "orders") +); + +// Returns when server accepts the signal +await handle.SignalAsync(wf => wf.ApproveAsync()); + +// Signals can carry arguments +await handle.SignalAsync(wf => wf.CancelAsync("out of stock"));`, + }, + }, + annotations: [ + { + label: '@workflow.signal decorator', + description: + 'The decorator marks the method as a signal handler. The handler cannot return a value — signals are always fire-and-forget from the caller\'s perspective. The method can be either async def or a plain def.', + lines: { + python: { workflowLines: [16, 17, 18, 19, 21, 22, 23], clientLines: [] }, + typescript: { workflowLines: [3, 4], clientLines: [] }, + go: { workflowLines: [9, 10, 11, 12], clientLines: [] }, + java: { workflowLines: [8, 9, 10, 11], clientLines: [] }, + dotnet: { workflowLines: [14, 15, 16, 17, 18, 19], clientLines: [] }, + }, + }, + { + label: 'Handler updates instance variables', + description: + 'The signal handler writes to instance variables that the main run() loop reads. This is the core pattern: the handler mutates state, and wait_condition re-evaluates based on that state.', + lines: { + python: { workflowLines: [5, 6, 7, 16, 17, 18, 19], clientLines: [] }, + typescript: { workflowLines: [7, 8, 10, 11, 12, 13], clientLines: [] }, + go: { workflowLines: [6, 7, 9, 10, 11, 12], clientLines: [] }, + java: { workflowLines: [15, 16, 24, 25, 26, 27], clientLines: [] }, + dotnet: { workflowLines: [6, 7, 17, 18, 19, 20], clientLines: [] }, + }, + }, + { + label: 'wait_condition unblocks the Workflow', + description: + 'wait_condition() (or the equivalent in each SDK) pauses the Workflow without spinning. When the signal handler updates the watched variable, Temporal re-evaluates the condition and resumes the Workflow automatically.', + lines: { + python: { workflowLines: [9, 10, 11, 12, 13, 14], clientLines: [] }, + typescript: { workflowLines: [18, 19, 20], clientLines: [] }, + go: { workflowLines: [17, 18], clientLines: [] }, + java: { workflowLines: [19, 20, 21], clientLines: [] }, + dotnet: { workflowLines: [11, 12, 13], clientLines: [] }, + }, + }, + { + label: 'handle.signal() from the client', + description: + 'WorkflowHandle.signal() sends the signal. It returns as soon as the server accepts it — your code does not wait for the handler to run. Pass arguments as additional parameters after the signal name.', + lines: { + python: { workflowLines: [], clientLines: [13, 14, 16, 17, 18] }, + typescript: { workflowLines: [], clientLines: [12, 13, 15] }, + go: { workflowLines: [], clientLines: [20, 21, 22, 23, 24, 26, 27, 28, 29, 30] }, + java: { workflowLines: [], clientLines: [9, 10, 12, 13] }, + dotnet: { workflowLines: [], clientLines: [9, 10, 12, 13] }, + }, + }, + ], +}; + +/* ───────────────────────────────────────────────── + QUERY +───────────────────────────────────────────────── */ +export const queryData: MessageTypeData = { + description: + 'A Query reads the current in-memory state of a running Workflow. The caller waits for the return value. Query handlers must not change state. If no Worker is running when a query arrives, it fails immediately.', + note: + 'Queries are not written to event history, so they have no impact on Workflow execution. Use them freely for dashboards or status checks.', + accentColor: 'var(--nd-purple)', + code: { + python: { + workflowCode: `from temporalio import workflow + +@workflow.defn +class OrderWorkflow: + def __init__(self) -> None: + self._status: str = "pending" + self._approved: bool = False + self._priority: int = 3 + + @workflow.query + def get_status(self) -> str: + return self._status + + @workflow.query + def get_details(self) -> dict: + return { + "status": self._status, + "approved": self._approved, + "priority": self._priority, + }`, + clientCode: `from temporalio.client import Client +from order_workflow import OrderWorkflow + +async def main(): + client = await Client.connect("localhost:7233") + handle = client.get_workflow_handle("order-wf-123") + + status = await handle.query( + OrderWorkflow.get_status + ) + print(f"Status: {status}") + + details = await handle.query( + OrderWorkflow.get_details + ) + print(details)`, + }, + typescript: { + workflowCode: `import { defineQuery, setHandler } from '@temporalio/workflow'; + +export const getStatusQuery = defineQuery('getStatus'); +export const getDetailsQuery = defineQuery>('getDetails'); + +export async function orderWorkflow(orderId: string): Promise { + let status = 'pending'; + let approved = false; + let priority = 3; + + setHandler(getStatusQuery, () => status); + + setHandler(getDetailsQuery, () => ({ + status, + approved, + priority, + })); + + // ... workflow logic + return \`Order \${orderId} processed\`; +}`, + clientCode: `import { Client } from '@temporalio/client'; +import { getStatusQuery, getDetailsQuery } from './workflows'; + +async function main() { + const client = new Client(); + const handle = client.workflow.getHandle('order-wf-123'); + + const status = await handle.query(getStatusQuery); + console.log(\`Status: \${status}\`); + + const details = await handle.query(getDetailsQuery); + console.log(details); +}`, + }, + go: { + workflowCode: `package workflows + +import "go.temporal.io/sdk/workflow" + +func OrderWorkflow(ctx workflow.Context, orderID string) (string, error) { + status := "pending" + approved := false + priority := 3 + + workflow.SetQueryHandler(ctx, "getStatus", + func() (string, error) { + return status, nil + }, + ) + + workflow.SetQueryHandler(ctx, "getDetails", + func() (map[string]interface{}, error) { + return map[string]interface{}{ + "status": status, + "approved": approved, + "priority": priority, + }, nil + }, + ) + + // ... workflow logic + return "Order " + orderID + " processed", nil +}`, + clientCode: `resp, _ := c.QueryWorkflow( + context.Background(), + "order-wf-123", "", + "getStatus", +) +var status string +_ = resp.Get(&status) +fmt.Printf("Status: %s\\n", status) + +resp2, _ := c.QueryWorkflow( + context.Background(), + "order-wf-123", "", + "getDetails", +) +var details map[string]interface{} +_ = resp2.Get(&details) +fmt.Println(details)`, + }, + java: { + workflowCode: `import io.temporal.workflow.*; +import java.util.Map; + +@WorkflowInterface +public interface OrderWorkflow { + @WorkflowMethod + String run(String orderId); + + @QueryMethod + String getStatus(); + + @QueryMethod + Map getDetails(); +} + +public class OrderWorkflowImpl implements OrderWorkflow { + private String status = "pending"; + private boolean approved = false; + private int priority = 3; + + @Override + public String run(String orderId) { + return "Order " + orderId + " processed"; + } + + @Override + public String getStatus() { + return status; + } + + @Override + public Map getDetails() { + return Map.of( + "status", status, + "approved", approved, + "priority", priority + ); + } +}`, + clientCode: `OrderWorkflow workflow = client.newWorkflowStub( + OrderWorkflow.class, "order-wf-123" +); + +String status = workflow.getStatus(); +System.out.println("Status: " + status); + +Map details = workflow.getDetails(); +System.out.println(details);`, + }, + dotnet: { + workflowCode: `using Temporalio.Workflows; +using System.Collections.Generic; + +[Workflow] +public class OrderWorkflow +{ + private string _status = "pending"; + private bool _approved = false; + private int _priority = 3; + + [WorkflowRun] + public async Task RunAsync(string orderId) + { + return $"Order {orderId} processed"; + } + + [WorkflowQuery] + public string GetStatus() => _status; + + [WorkflowQuery] + public Dictionary GetDetails() => + new() { + ["status"] = _status, + ["approved"] = _approved, + ["priority"] = _priority, + }; +}`, + clientCode: `var handle = client.GetWorkflowHandle("order-wf-123"); + +var status = await handle.QueryAsync( + wf => wf.GetStatus() +); +Console.WriteLine($"Status: {status}"); + +var details = await handle.QueryAsync( + wf => wf.GetDetails() +); +Console.WriteLine(details);`, + }, + }, + annotations: [ + { + label: '@workflow.query decorator', + description: + 'This decorator (or its SDK equivalent) is what registers the method as a query handler in Temporal. Without it, calling handle.query() by that name will fail.', + lines: { + python: { workflowLines: [10, 11, 12, 14, 15], clientLines: [] }, + typescript: { workflowLines: [3, 4], clientLines: [] }, + go: { workflowLines: [10, 11, 12, 13, 14], clientLines: [] }, + java: { workflowLines: [9, 10, 12, 13], clientLines: [] }, + dotnet: { workflowLines: [17, 18, 20, 21], clientLines: [] }, + }, + }, + { + label: 'Must be synchronous', + description: + 'Query handlers must not be async. The SDK enforces this at registration time. Queries read in-memory state and must return immediately without yielding to the event loop.', + lines: { + python: { workflowLines: [11], clientLines: [] }, + typescript: { workflowLines: [11, 13], clientLines: [] }, + go: { workflowLines: [11, 12, 13], clientLines: [] }, + java: { workflowLines: [27, 28, 29, 31, 32, 33], clientLines: [] }, + dotnet: { workflowLines: [18], clientLines: [] }, + }, + }, + { + label: 'Not buffered', + description: + 'Unlike Signals, queries are not persisted. If no Worker is polling the task queue when a query arrives, it fails immediately. This is the key behavioral difference to understand between Signals and Queries.', + lines: { + python: { workflowLines: [], clientLines: [8, 9, 10] }, + typescript: { workflowLines: [], clientLines: [8] }, + go: { workflowLines: [], clientLines: [1, 2, 3, 4, 5] }, + java: { workflowLines: [], clientLines: [5] }, + dotnet: { workflowLines: [], clientLines: [3, 4, 5] }, + }, + }, + { + label: 'handle.query()', + description: + 'The client sends the query and waits synchronously for the return value. The Workflow keeps running — queries do not pause or interrupt it, and nothing is written to event history.', + lines: { + python: { workflowLines: [], clientLines: [8, 9, 10, 11, 13, 14, 15, 16] }, + typescript: { workflowLines: [], clientLines: [8, 9, 11, 12] }, + go: { workflowLines: [], clientLines: [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17] }, + java: { workflowLines: [], clientLines: [5, 6, 8, 9] }, + dotnet: { workflowLines: [], clientLines: [3, 4, 5, 6, 8, 9, 10, 11] }, + }, + }, + ], +}; + +/* ───────────────────────────────────────────────── + UPDATE +───────────────────────────────────────────────── */ +export const updateData: MessageTypeData = { + description: + 'An Update can change Workflow state and return a result to the caller. An optional validator runs before any changes happen — if it rejects the input, the Workflow state is left untouched and the caller receives the error.', + note: + 'The validator runs first. Only if it passes does the handler execute and state change. The caller waits for the full outcome: either the return value or the rejection error.', + accentColor: 'var(--ifm-color-primary)', + code: { + python: { + workflowCode: `from temporalio import workflow + +@workflow.defn +class OrderWorkflow: + def __init__(self) -> None: + self._priority: int = 3 + + @workflow.update + async def set_priority(self, priority: int) -> str: + self._priority = priority + return f"Priority set to {priority}" + + @set_priority.validator + def validate_priority(self, priority: int) -> None: + if not (1 <= priority <= 5): + raise ValueError( + f"Priority must be 1-5, got {priority}" + )`, + clientCode: `from temporalio.client import Client +from order_workflow import OrderWorkflow + +async def main(): + client = await Client.connect("localhost:7233") + handle = client.get_workflow_handle("order-wf-123") + + try: + result = await handle.execute_update( + OrderWorkflow.set_priority, 3 + ) + print(result) # "Priority set to 3" + except Exception as e: + print(e) # "Priority must be 1-5, got 6"`, + }, + typescript: { + workflowCode: `import { defineUpdate, setHandler } from '@temporalio/workflow'; + +export const setPriorityUpdate = defineUpdate('setPriority'); + +export async function orderWorkflow(orderId: string): Promise { + let priority = 3; + + setHandler( + setPriorityUpdate, + (newPriority: number) => { + priority = newPriority; + return \`Priority set to \${newPriority}\`; + }, + { + validator: (newPriority: number) => { + if (newPriority < 1 || newPriority > 5) { + throw new Error( + \`Priority must be 1-5, got \${newPriority}\` + ); + } + }, + } + ); + + // ... workflow logic + return \`Order \${orderId} processed\`; +}`, + clientCode: `import { Client } from '@temporalio/client'; +import { setPriorityUpdate } from './workflows'; + +async function main() { + const client = new Client(); + const handle = client.workflow.getHandle('order-wf-123'); + + try { + const result = await handle.executeUpdate( + setPriorityUpdate, { args: [3] } + ); + console.log(result); // "Priority set to 3" + } catch (err) { + console.error(err); // "Priority must be 1-5, got 6" + } +}`, + }, + go: { + workflowCode: `package workflows + +import ( + "fmt" + "go.temporal.io/sdk/workflow" +) + +func OrderWorkflow(ctx workflow.Context, orderID string) (string, error) { + priority := 3 + + workflow.SetUpdateHandlerWithOptions(ctx, "setPriority", + func(ctx workflow.Context, p int) (string, error) { + priority = p + return fmt.Sprintf("Priority set to %d", p), nil + }, + workflow.UpdateHandlerOptions{ + Validator: func(ctx workflow.Context, p int) error { + if p < 1 || p > 5 { + return fmt.Errorf("priority must be 1-5, got %d", p) + } + return nil + }, + }, + ) + + // ... workflow logic + return "Order " + orderID + " processed", nil +}`, + clientCode: `updateHandle, _ := c.UpdateWorkflow( + context.Background(), + client.UpdateWorkflowOptions{ + WorkflowID: "order-wf-123", + UpdateName: "setPriority", + Args: []interface{}{3}, + WaitForStage: client.WorkflowUpdateStageCompleted, + }, +) + +var result string +err := updateHandle.Get(context.Background(), &result) +if err != nil { + fmt.Println(err) // "priority must be 1-5, got 6" +} else { + fmt.Println(result) // "Priority set to 3" +}`, + }, + java: { + workflowCode: `import io.temporal.workflow.*; + +@WorkflowInterface +public interface OrderWorkflow { + @WorkflowMethod + String run(String orderId); + + @UpdateMethod + String setPriority(int priority); + + @UpdateValidatorMethod(updateName = "setPriority") + void validatePriority(int priority); +} + +public class OrderWorkflowImpl implements OrderWorkflow { + private int priority = 3; + + @Override + public String run(String orderId) { + return "Order " + orderId + " processed"; + } + + @Override + public String setPriority(int priority) { + this.priority = priority; + return "Priority set to " + priority; + } + + @Override + public void validatePriority(int priority) { + if (priority < 1 || priority > 5) { + throw new IllegalArgumentException( + "Priority must be 1-5, got " + priority + ); + } + } +}`, + clientCode: `OrderWorkflow workflow = client.newWorkflowStub( + OrderWorkflow.class, "order-wf-123" +); + +try { + String result = workflow.setPriority(3); + System.out.println(result); // "Priority set to 3" +} catch (Exception e) { + System.out.println(e.getMessage()); // "Priority must be 1-5" +}`, + }, + dotnet: { + workflowCode: `using Temporalio.Workflows; + +[Workflow] +public class OrderWorkflow +{ + private int _priority = 3; + + [WorkflowRun] + public async Task RunAsync(string orderId) + { + return $"Order {orderId} processed"; + } + + [WorkflowUpdate] + public async Task SetPriorityAsync(int priority) + { + _priority = priority; + return $"Priority set to {priority}"; + } + + [WorkflowUpdateValidator(nameof(SetPriorityAsync))] + public void ValidatePriority(int priority) + { + if (priority < 1 || priority > 5) + throw new ArgumentException( + $"Priority must be 1-5, got {priority}" + ); + } +}`, + clientCode: `var handle = client.GetWorkflowHandle("order-wf-123"); + +try { + var result = await handle.ExecuteUpdateAsync( + wf => wf.SetPriorityAsync(3) + ); + Console.WriteLine(result); // "Priority set to 3" +} catch (Exception ex) { + Console.WriteLine(ex.Message); // "Priority must be 1-5, got 6" +}`, + }, + }, + annotations: [ + { + label: '@workflow.update decorator', + description: + 'This decorator registers the method as an Update handler. Unlike a Signal, the handler can return a value. Unlike a Query, it can change state. It is the only type that does both.', + lines: { + python: { workflowLines: [8, 9, 10, 11], clientLines: [] }, + typescript: { workflowLines: [3, 8, 9], clientLines: [] }, + go: { workflowLines: [11, 12, 13, 14, 15], clientLines: [] }, + java: { workflowLines: [8, 9, 21, 22, 23, 24, 25], clientLines: [] }, + dotnet: { workflowLines: [12, 13, 14, 15, 16, 17, 18], clientLines: [] }, + }, + }, + { + label: 'Validator runs first', + description: + 'The validator is a separate method linked to the handler. It runs before any state is touched. If it raises an exception, the Update is rejected and the Workflow state is unchanged — the handler never runs.', + lines: { + python: { workflowLines: [13, 14, 15, 16, 17, 18], clientLines: [13, 14] }, + typescript: { workflowLines: [14, 15, 16, 17, 18, 19, 20], clientLines: [12, 13, 14] }, + go: { workflowLines: [9, 10, 11, 12, 13, 14, 15, 16, 17], clientLines: [12, 13] }, + java: { workflowLines: [10, 11, 27, 28, 29, 30, 31, 32, 33], clientLines: [8, 9] }, + dotnet: { workflowLines: [19, 20, 21, 22, 23, 24, 25, 26, 27], clientLines: [8, 9] }, + }, + }, + { + label: 'handle.execute_update()', + description: + 'The client call blocks until the full round trip completes: validator runs, handler runs, result returns. There is no fire-and-forget option for Updates. The caller always waits for either a result or a rejection.', + lines: { + python: { workflowLines: [], clientLines: [7, 8, 9, 10, 11, 12, 13, 14] }, + typescript: { workflowLines: [], clientLines: [7, 8, 9, 10, 11, 12, 13, 14] }, + go: { workflowLines: [], clientLines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16] }, + java: { workflowLines: [], clientLines: [4, 5, 6, 7, 8, 9] }, + dotnet: { workflowLines: [], clientLines: [2, 3, 4, 5, 6, 7, 8, 9, 10] }, + }, + }, + ], +}; + +export const MESSAGE_TYPE_DATA: Record = { + signal: signalData, + query: queryData, + update: updateData, +}; diff --git a/src/components/WorkflowMessagingDemo/buildSteps.ts b/src/components/WorkflowMessagingDemo/buildSteps.ts new file mode 100644 index 0000000000..b017e50e94 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/buildSteps.ts @@ -0,0 +1,159 @@ +export type BuildStep = { + num: string; + title: string; + file: string; + language: string; + code: string; + note: string; +}; + +export const buildSteps: BuildStep[] = [ + { + num: 'Step 1 of 6', + title: 'Define the Workflow with state', + file: 'order_workflow.py', + language: 'python', + code: `from dataclasses import dataclass +from temporalio import workflow + +@workflow.defn +class OrderWorkflow: + def __init__(self) -> None: + self._status: str = "pending" + self._approved: bool = False + self._priority: int = 3 + + @workflow.run + async def run(self, order_id: str) -> str: + # handlers wired below as class methods + await workflow.wait_condition(lambda: self._approved) + return f"Order {order_id} processed with priority {self._priority}"`, + note: `Start with a class that holds state. These instance variables are what Signals will write to, Queries will read from, and Updates will validate and change. The run method blocks on wait_condition and resumes automatically when a Signal sets _approved to True.`, + }, + { + num: 'Step 2 of 6', + title: 'Add a Signal handler', + file: 'order_workflow.py', + language: 'python', + code: `@workflow.defn +class OrderWorkflow: + ... + + @workflow.signal + async def approve(self) -> None: + """Fire-and-forget: caller gets no response.""" + self._approved = True + self._status = "approved" + + @workflow.signal + async def cancel(self, reason: str) -> None: + """Signals can carry input arguments.""" + self._status = f"cancelled: {reason}" + raise asyncio.CancelledError(reason)`, + note: `Signal handlers are one-way. The caller sends a signal and keeps moving with no return value and no confirmation of when the handler ran. Use signals when you want to push an event into a Workflow without waiting for acknowledgment.`, + }, + { + num: 'Step 3 of 6', + title: 'Add a Query handler', + file: 'order_workflow.py', + language: 'python', + code: `@workflow.defn +class OrderWorkflow: + ... + + @workflow.query + def get_status(self) -> str: + """Read-only. Must NOT be async.""" + return self._status + + @workflow.query + def get_details(self) -> dict: + """Queries can return complex types.""" + return { + "status": self._status, + "approved": self._approved, + "priority": self._priority, + }`, + note: `Query handlers must be regular synchronous functions, not async. They read state and return it. They must not change anything. If no Worker is running when a query is sent, it fails immediately because queries are not buffered.`, + }, + { + num: 'Step 4 of 6', + title: 'Add an Update handler with a validator', + file: 'order_workflow.py', + language: 'python', + code: `@workflow.defn +class OrderWorkflow: + ... + + @workflow.update + async def set_priority(self, priority: int) -> str: + """Request-response: caller waits for the return value.""" + self._priority = priority + return f"Priority set to {priority}" + + @set_priority.validator + def validate_priority(self, priority: int) -> None: + """Validator runs BEFORE the handler. + Raise here to reject without touching state.""" + if not (1 <= priority <= 5): + raise ValueError( + f"Priority must be 1-5, got {priority}" + )`, + note: `The validator runs first. If it raises, the Update is rejected and no state is changed. The caller gets the exception as an error. The handler only runs if the validator passes. This makes Updates safe for operations that need validated input before changing anything.`, + }, + { + num: 'Step 5 of 6', + title: 'Start the Workflow and send a Signal', + file: 'client.py', + language: 'python', + code: `import asyncio +from temporalio.client import Client +from order_workflow import OrderWorkflow + +async def main(): + client = await Client.connect("localhost:7233") + + # Start the Workflow (blocks at wait_condition immediately) + handle = await client.start_workflow( + OrderWorkflow.run, + "order-123", + id="order-wf-123", + task_queue="orders", + ) + print(f"Started: {handle.id}") + + # Send a Signal (returns as soon as the server accepts it) + await handle.signal(OrderWorkflow.approve) + print("Signal sent") + +asyncio.run(main())`, + note: `The Workflow starts and parks at wait_condition(lambda: self._approved). The approve signal sets _approved = True, which unblocks the condition and lets the Workflow finish. The client does not wait for the signal to be processed. It only confirms the server accepted it.`, + }, + { + num: 'Step 6 of 6', + title: 'Query state and send an Update', + file: 'client.py', + language: 'python', + code: `async def main(): + client = await Client.connect("localhost:7233") + handle = client.get_workflow_handle("order-wf-123") + + # Query the Workflow's current state (synchronous read) + status = await handle.query(OrderWorkflow.get_status) + print(f"Status: {status}") # "pending" or "approved" + + # Send an Update (caller waits for result or rejection) + try: + result = await handle.execute_update( + OrderWorkflow.set_priority, 3 + ) + print(f"Update result: {result}") + # "Priority set to 3" + except Exception as e: + print(f"Update rejected: {e}") + # ValueError: "Priority must be 1-5, got 6" + +asyncio.run(main())`, + note: `Queries return the current state without touching anything. Updates wait for a result. If the validator rejects the input (priority 6 in this case), execute_update raises on the client and the Workflow state is never modified. You can also get a workflow handle at any time with just the workflow ID, without needing the original start call.`, + }, +]; diff --git a/src/components/WorkflowMessagingDemo/flowSteps.ts b/src/components/WorkflowMessagingDemo/flowSteps.ts new file mode 100644 index 0000000000..dd69127d56 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/flowSteps.ts @@ -0,0 +1,126 @@ +export type FlowMode = 'signal' | 'query' | 'update'; + +export type FlowStep = { + label: string; + detail: string; + activeNode: 0 | 1 | 2; + packetPct: number; + log: { + level: 'info' | 'success' | 'warn'; + event: string; + msg: string; + }; +}; + +export const signalSteps: FlowStep[] = [ + { + label: 'Client sends Signal', + detail: + `Your code calls handle.signal(approve). The SDK serializes it and sends it off. No response is awaited. Signals are always fire-and-forget.`, + activeNode: 0, + packetPct: 10, + log: { level: 'info', event: 'SIGNAL_SENT', msg: 'handle.signal(approve) dispatched' }, + }, + { + label: 'Server persists Signal', + detail: + `The Temporal Server writes the signal into the Workflow's event history. It will survive crashes and be delivered reliably, even if the Worker is currently offline.`, + activeNode: 1, + packetPct: 50, + log: { level: 'info', event: 'SIGNAL_QUEUED', msg: 'signal persisted to event history' }, + }, + { + label: 'Signal delivered to Worker', + detail: + `The Worker's polling loop picks up the event and routes it to the correct Workflow execution running on that Worker.`, + activeNode: 1, + packetPct: 75, + log: { level: 'info', event: 'SIGNAL_DELIVERED', msg: 'routing to workflow execution' }, + }, + { + label: 'Signal handler executes', + detail: + `The @workflow.signal handler runs. It can update internal state, wake a blocked condition, or kick off further actions inside the Workflow.`, + activeNode: 2, + packetPct: 100, + log: { level: 'success', event: 'SIGNAL_HANDLED', msg: 'approve() completed, _approved = True' }, + }, +]; + +export const querySteps: FlowStep[] = [ + { + label: 'Client sends Query', + detail: + `handle.query(get_status) is called. Unlike signals, the SDK waits synchronously for a response. Queries never change Workflow state.`, + activeNode: 0, + packetPct: 10, + log: { level: 'info', event: 'QUERY_SENT', msg: 'handle.query(get_status) dispatched' }, + }, + { + label: 'Server routes synchronously', + detail: + `The Server routes the query to an available Worker running this Workflow. Queries are not buffered. If no Worker is running, the query fails immediately.`, + activeNode: 1, + packetPct: 50, + log: { level: 'info', event: 'QUERY_ROUTING', msg: 'routing to available worker' }, + }, + { + label: 'Query handler reads state', + detail: + `The @workflow.query handler runs synchronously. It reads Workflow state and returns a value. It must not change anything or use async operations.`, + activeNode: 2, + packetPct: 100, + log: { level: 'success', event: 'QUERY_HANDLED', msg: 'get_status() \u2192 "approved"' }, + }, + { + label: 'Result returned to client', + detail: + `The result comes back to the waiting caller. The Workflow keeps running, completely unaffected by the query.`, + activeNode: 2, + packetPct: 100, + log: { level: 'success', event: 'QUERY_RESULT', msg: 'result received: "approved"' }, + }, +]; + +export const updateSteps: FlowStep[] = [ + { + label: 'Client sends Update request', + detail: + `handle.execute_update(set_priority, 3) is called. The client waits for a validated response. If the input is rejected, the caller gets a clean error and the Workflow is untouched.`, + activeNode: 0, + packetPct: 10, + log: { level: 'info', event: 'UPDATE_SENT', msg: 'execute_update(set_priority, 3) dispatched' }, + }, + { + label: 'Server routes to Worker', + detail: + `The Temporal Server routes the Update to an available Worker handling this Workflow execution.`, + activeNode: 1, + packetPct: 50, + log: { level: 'info', event: 'UPDATE_ROUTING', msg: 'routing to workflow worker' }, + }, + { + label: 'Validator runs first', + detail: + `The @update.validator runs before any state changes. If it raises an exception, the Update is rejected right here and the Workflow state stays intact.`, + activeNode: 2, + packetPct: 75, + log: { level: 'info', event: 'UPDATE_VALIDATING', msg: 'validate_priority(3) \u2192 valid' }, + }, + { + label: 'Update handler executes', + detail: + `Validation passed. The @workflow.update handler runs, changes state, and returns a result to the waiting caller.`, + activeNode: 2, + packetPct: 100, + log: { level: 'success', event: 'UPDATE_HANDLED', msg: 'set_priority(3) \u2192 "Priority set to 3"' }, + }, + { + label: 'Result returned to client', + detail: + `The caller gets the return value from the update handler. The Workflow keeps running with the updated state.`, + activeNode: 2, + packetPct: 100, + log: { level: 'success', event: 'UPDATE_COMPLETE', msg: 'result received: "Priority set to 3"' }, + }, +]; diff --git a/src/components/WorkflowMessagingDemo/index.tsx b/src/components/WorkflowMessagingDemo/index.tsx new file mode 100644 index 0000000000..9c87a2bf18 --- /dev/null +++ b/src/components/WorkflowMessagingDemo/index.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import BuildIt from './BuildIt'; +import HowItWorks from './HowItWorks'; +import MessageTypes from './MessageTypes'; +import Overview from './Overview'; +import Quiz from './Quiz'; +import styles from './WorkflowMessagingDemo.module.css'; + +type SectionId = 'overview' | 'types' | 'howitworks' | 'buildit' | 'quiz'; + +const NAV: { id: SectionId; label: string }[] = [ + { id: 'overview', label: 'What is it?' }, + { id: 'types', label: 'Message Types' }, + { id: 'howitworks', label: 'How It Works' }, + { id: 'buildit', label: 'Build It' }, + { id: 'quiz', label: 'Test Yourself' }, +]; + +export default function WorkflowMessagingDemo() { + const [active, setActive] = useState('overview'); + + function next(current: SectionId) { + const idx = NAV.findIndex((n) => n.id === current); + if (idx < NAV.length - 1) setActive(NAV[idx + 1].id); + } + + return ( +
+ + + {active === 'overview' && next('overview')} />} + {active === 'types' && next('types')} />} + {active === 'howitworks' && next('howitworks')} />} + {active === 'buildit' && next('buildit')} />} + {active === 'quiz' && } +
+ ); +} diff --git a/src/components/WorkflowMessagingDemo/quizQuestions.ts b/src/components/WorkflowMessagingDemo/quizQuestions.ts new file mode 100644 index 0000000000..b656f8f80a --- /dev/null +++ b/src/components/WorkflowMessagingDemo/quizQuestions.ts @@ -0,0 +1,71 @@ +export type QuizQuestion = { + q: string; + options: readonly string[]; + correct: number; + explanation: string; +}; + +export const quizQuestions: QuizQuestion[] = [ + { + q: 'You want to tell a running Workflow that an external payment succeeded. You do not need a response back. Which type do you use?', + options: ['Query', 'Signal', 'Update', 'Start a new Workflow'], + correct: 1, + explanation: + 'Signals are the right choice when you just need to push an event in with no response. They are fire-and-forget with durable, buffered delivery.', + }, + { + q: 'You send a Query to a Workflow whose Worker is not currently running. What happens?', + options: [ + 'The query is buffered and delivered when the Worker restarts', + 'The query fails with an error', + 'Temporal returns the last known state from history', + 'The query waits indefinitely until a Worker comes back', + ], + correct: 1, + explanation: + 'Queries are not buffered. They require a live Worker. If nothing is polling that task queue, the query fails right away. This is the key behavioral difference from Signals, which are persisted and delivered later.', + }, + { + q: `An Update handler's validator raises an exception. What happens?`, + options: [ + 'The Update handler still runs but the exception is logged', + 'The Update is rejected and Workflow state is unchanged', + 'The Workflow is cancelled', + 'The Update is queued and retried later', + ], + correct: 1, + explanation: + 'The validator runs before any state changes happen. If it raises, the Update is rejected and returned as an error to the caller. The Workflow state is never touched.', + }, + { + q: 'Which message type returns a value to the caller AND can change Workflow state?', + options: ['Signal', 'Query', 'Update', 'Both Signal and Query'], + correct: 2, + explanation: + 'Updates are the only type that does both. Queries are read-only. Signals have no return value. Updates can change state and give the caller a result back.', + }, + { + q: 'A @workflow.query handler is defined with "async def" instead of "def". What happens?', + options: [ + 'It works fine, async is optional for queries', + 'The Temporal SDK raises an error at registration time', + 'The query runs but result delivery is delayed', + 'The query mutates state as a side effect', + ], + correct: 1, + explanation: + 'Query handlers must be plain synchronous functions. The SDK enforces this at registration time and raises an error if you use async def. Queries must complete without yielding to the event loop.', + }, + { + q: 'Your Workflow uses workflow.wait_condition(lambda: self._approved). What is the standard way to unblock it?', + options: [ + 'A Query that reads self._approved', + 'A Signal handler that sets self._approved = True', + 'An Update that returns self._approved', + 'Starting a Child Workflow', + ], + correct: 1, + explanation: + 'Signals are the standard way to push state into a Workflow and wake a blocked condition. The signal handler sets the flag, wait_condition re-evaluates, and the Workflow continues.', + }, +];