Skip to content

Commit 7ad76ec

Browse files
committed
Add simulators
1 parent 1e10e2f commit 7ad76ec

File tree

12 files changed

+2188
-1
lines changed

12 files changed

+2188
-1
lines changed

docs/features/queries.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
sidebar_position: 2
33
---
44

5+
import QuerySimulator from '@site/src/components/QuerySimulator';
6+
57
# Queries
68

79
Queries allow you to retrieve information about the current state of a workflow without affecting its execution. This is useful for monitoring and debugging purposes.
@@ -34,4 +36,6 @@ $workflow = WorkflowStub::load($workflowId);
3436
$ready = $workflow->getReady();
3537
```
3638

39+
<QuerySimulator />
40+
3741
**Note:** Querying a workflow does not advance its execution, unlike signals.

docs/features/signal+timer.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class MyWorkflow extends Workflow
1818
#[SignalMethod]
1919
public function setReady($ready)
2020
{
21-
$this->ready = $ready
21+
$this->ready = $ready;
2222
}
2323

2424
public function execute()
@@ -28,6 +28,10 @@ class MyWorkflow extends Workflow
2828
}
2929
```
3030

31+
import SignalTimerSimulator from '@site/src/components/SignalTimerSimulator';
32+
33+
<SignalTimerSimulator />
34+
3135
The workflow will reach the call to `awaitWithTimeout()` and then hibernate until either some external code signals the workflow like this.
3236

3337
```php

docs/features/signals.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
sidebar_position: 1
33
---
44

5+
import SignalSimulator from '@site/src/components/SignalSimulator';
6+
57
# Signals
68

79
Signals allow you to trigger events in a workflow from outside the workflow. This can be useful for reacting to external events, enabling *human-in-the-loop* interventions, or for signaling the completion of an external task.
@@ -58,4 +60,6 @@ class MyWorkflow extends Workflow
5860
}
5961
```
6062

63+
<SignalSimulator />
64+
6165
**Important:** The `await()` function should only be used in a workflow, not an activity.

docs/features/timers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
sidebar_position: 3
33
---
44

5+
import TimerSimulator from '@site/src/components/TimerSimulator';
6+
57
# Timers
68

79
Laravel Workflow provides the ability to suspend the execution of a workflow and resume at a later time. These are durable timers, meaning they survive restarts and failures while remaining consistent with workflow replay semantics. This can be useful for implementing delays, retry logic, or timeouts.
@@ -25,6 +27,8 @@ class MyWorkflow extends Workflow
2527
}
2628
```
2729

30+
<TimerSimulator />
31+
2832
You can specify the `$duration` as an integer number of seconds or as a string e.g. '5 seconds', '30 minutes' or even '3 days'. Laravel Workflow can handle any duration.
2933

3034
**Important:** Inside of a workflow, never use `Carbon::now()` or Laravel's `now()` to get the current time. Instead, use `Workflow\now()`, which returns the current time as seen by the workflow system. This is crucial because the actual time may not match your application's system clock.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import styles from './styles.module.css';
3+
4+
const ExecutionState = {
5+
IDLE: 'idle',
6+
RUNNING: 'running',
7+
WAITING: 'waiting',
8+
COMPLETED: 'completed',
9+
};
10+
11+
export default function QuerySimulator({
12+
code = `use function Workflow\\await;
13+
use Workflow\\QueryMethod;
14+
use Workflow\\SignalMethod;
15+
use Workflow\\Workflow;
16+
17+
class MyWorkflow extends Workflow
18+
{
19+
private bool $ready = false;
20+
21+
#[QueryMethod]
22+
public function getReady(): bool
23+
{
24+
return $this->ready;
25+
}
26+
27+
#[SignalMethod]
28+
public function setReady($ready)
29+
{
30+
$this->ready = $ready;
31+
}
32+
33+
public function execute()
34+
{
35+
yield await(fn () => $this->ready);
36+
}
37+
}`,
38+
title = "Query & Signal Simulator",
39+
}) {
40+
const [isExpanded, setIsExpanded] = useState(false);
41+
const [executionState, setExecutionState] = useState(ExecutionState.IDLE);
42+
const [currentLine, setCurrentLine] = useState(-1);
43+
const [waitingTime, setWaitingTime] = useState(0);
44+
const [readyValue, setReadyValue] = useState(false);
45+
const [queryResult, setQueryResult] = useState(null);
46+
const [queryFlash, setQueryFlash] = useState(false);
47+
const waitingIntervalRef = useRef(null);
48+
const animationRef = useRef(null);
49+
50+
const codeLines = code.split('\n');
51+
52+
const resetSimulation = () => {
53+
setExecutionState(ExecutionState.IDLE);
54+
setCurrentLine(-1);
55+
setWaitingTime(0);
56+
setReadyValue(false);
57+
setQueryResult(null);
58+
setQueryFlash(false);
59+
if (waitingIntervalRef.current) {
60+
clearInterval(waitingIntervalRef.current);
61+
}
62+
if (animationRef.current) {
63+
cancelAnimationFrame(animationRef.current);
64+
}
65+
};
66+
67+
const runSimulation = () => {
68+
resetSimulation();
69+
setExecutionState(ExecutionState.WAITING);
70+
setCurrentLine(24); // yield await line
71+
72+
// Start counting waiting time
73+
const startTime = Date.now();
74+
waitingIntervalRef.current = setInterval(() => {
75+
setWaitingTime(Math.floor((Date.now() - startTime) / 1000));
76+
}, 1000);
77+
};
78+
79+
const sendSignal = () => {
80+
if (executionState !== ExecutionState.WAITING) return;
81+
82+
// Clear waiting interval
83+
if (waitingIntervalRef.current) {
84+
clearInterval(waitingIntervalRef.current);
85+
}
86+
87+
// Flash the signal handler line
88+
setCurrentLine(19); // $this->ready = $ready line
89+
setReadyValue(true);
90+
setExecutionState(ExecutionState.RUNNING);
91+
92+
setTimeout(() => {
93+
setCurrentLine(24); // Back to await line briefly
94+
setTimeout(() => {
95+
setExecutionState(ExecutionState.COMPLETED);
96+
setCurrentLine(-1);
97+
}, 500);
98+
}, 300);
99+
};
100+
101+
const queryWorkflow = () => {
102+
// Flash the query method
103+
setQueryFlash(true);
104+
setQueryResult(readyValue);
105+
106+
setTimeout(() => {
107+
setQueryFlash(false);
108+
}, 500);
109+
};
110+
111+
useEffect(() => {
112+
return () => {
113+
if (waitingIntervalRef.current) {
114+
clearInterval(waitingIntervalRef.current);
115+
}
116+
if (animationRef.current) {
117+
cancelAnimationFrame(animationRef.current);
118+
}
119+
};
120+
}, []);
121+
122+
return (
123+
<div className={styles.simulatorWrapper}>
124+
<button
125+
className={`${styles.expandButton} ${isExpanded ? styles.expanded : ''}`}
126+
onClick={() => setIsExpanded(!isExpanded)}
127+
>
128+
<span className={styles.expandIcon}>{isExpanded ? '▼' : '▶'}</span>
129+
<span>Try it out!</span>
130+
</button>
131+
132+
{isExpanded && (
133+
<div className={styles.simulatorContainer}>
134+
<div className={styles.simulatorHeader}>
135+
<h4 className={styles.simulatorTitle}>{title}</h4>
136+
<div className={styles.controls}>
137+
<button
138+
className={styles.playButton}
139+
onClick={runSimulation}
140+
disabled={executionState === ExecutionState.RUNNING || executionState === ExecutionState.WAITING}
141+
>
142+
{executionState === ExecutionState.RUNNING ? '⏳ Running...' :
143+
executionState === ExecutionState.WAITING ? '⏸️ Waiting...' : '▶ Play'}
144+
</button>
145+
<button
146+
className={styles.resetButton}
147+
onClick={resetSimulation}
148+
disabled={executionState === ExecutionState.RUNNING}
149+
>
150+
🔄 Reset
151+
</button>
152+
</div>
153+
</div>
154+
155+
<div className={styles.codeContainer}>
156+
<pre className={styles.codeBlock}>
157+
{codeLines.map((line, index) => {
158+
const lineNumber = index + 1;
159+
const isHighlighted = currentLine === lineNumber;
160+
const isWaitingLine = isHighlighted && executionState === ExecutionState.WAITING;
161+
const isQueryLine = queryFlash && (lineNumber >= 10 && lineNumber <= 14);
162+
163+
return (
164+
<div
165+
key={index}
166+
className={`${styles.codeLine} ${isHighlighted ? styles.highlighted : ''} ${isWaitingLine ? styles.waiting : ''} ${isQueryLine ? styles.queryHighlight : ''}`}
167+
>
168+
<span className={styles.lineNumber}>{lineNumber}</span>
169+
<span className={styles.lineContent}>{line || ' '}</span>
170+
{isWaitingLine && (
171+
<span className={styles.waitingBadge}>{waitingTime}s</span>
172+
)}
173+
</div>
174+
);
175+
})}
176+
</pre>
177+
</div>
178+
179+
<div className={styles.actionsRow}>
180+
{(executionState === ExecutionState.WAITING || executionState === ExecutionState.COMPLETED) && (
181+
<div className={styles.querySection}>
182+
<button
183+
className={styles.queryButton}
184+
onClick={queryWorkflow}
185+
>
186+
🔍 Query: <code>getReady()</code>
187+
</button>
188+
{queryResult !== null && (
189+
<span className={styles.queryResultInline}>
190+
<code>{queryResult ? 'true' : 'false'}</code>
191+
</span>
192+
)}
193+
</div>
194+
)}
195+
196+
{executionState === ExecutionState.WAITING && (
197+
<div className={styles.signalSection}>
198+
<button
199+
className={styles.signalButton}
200+
onClick={sendSignal}
201+
>
202+
📤 Send Signal: <code>setReady(true)</code>
203+
</button>
204+
</div>
205+
)}
206+
</div>
207+
208+
<div className={styles.statusBar}>
209+
<span className={`${styles.statusIndicator} ${styles[executionState]}`}>
210+
{executionState === ExecutionState.IDLE && '⏸️ Ready'}
211+
{executionState === ExecutionState.RUNNING && '▶️ Running'}
212+
{executionState === ExecutionState.WAITING && '⏳ Waiting for Signal'}
213+
{executionState === ExecutionState.COMPLETED && '✅ Workflow Completed'}
214+
</span>
215+
<span className={styles.stateDisplay}>
216+
$ready = <code>{readyValue ? 'true' : 'false'}</code>
217+
</span>
218+
</div>
219+
</div>
220+
)}
221+
</div>
222+
);
223+
}

0 commit comments

Comments
 (0)