Skip to content

Commit 0711885

Browse files
committed
feat: advanced demo
chore: fix lint
1 parent c44558f commit 0711885

File tree

23 files changed

+1057
-53
lines changed

23 files changed

+1057
-53
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
| Metric | effstate | XState |
3636
|--------|----------|--------|
3737
| **Bundle size (gzip)** | **~3.9 kB** | 13.7 kB |
38-
| Event processing | **24x faster** | - |
39-
| Realistic app lifecycle | **4.5x faster** | - |
38+
| Event processing | **25x faster** | - |
39+
| Realistic app lifecycle | **5x faster** | - |
4040

4141
[See full comparison →](https://handfish.github.io/effstate/getting-started/comparison/)
4242

apps/demo-advanced/.eslintrc.cjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = {
2+
root: true,
3+
env: { browser: true, es2020: true },
4+
extends: [
5+
'eslint:recommended',
6+
'plugin:@typescript-eslint/recommended',
7+
'plugin:react-hooks/recommended',
8+
],
9+
ignorePatterns: ['dist', '.eslintrc.cjs'],
10+
parser: '@typescript-eslint/parser',
11+
plugins: ['react-refresh'],
12+
rules: {
13+
'react-refresh/only-export-components': 'off',
14+
'@typescript-eslint/ban-types': 'off',
15+
},
16+
}

apps/demo-advanced/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Advanced Demo: `interpretManual()`
2+
3+
> ⚠️ **WARNING: This demo shows an advanced pattern that is NOT RECOMMENDED for most applications.**
4+
5+
## What is this?
6+
7+
This demo demonstrates `interpretManual()`, an alternative to `interpret()` that provides slightly faster actor creation at the cost of **manual lifecycle management**.
8+
9+
## Should I use `interpretManual()`?
10+
11+
**Almost certainly not.**
12+
13+
| Question | If Yes | If No |
14+
|----------|--------|-------|
15+
| Are you creating thousands of actors per second? | Maybe consider it | Use `interpret()` |
16+
| Have you profiled and confirmed actor creation is a bottleneck? | Maybe consider it | Use `interpret()` |
17+
| Are you comfortable managing cleanup manually? | Maybe consider it | Use `interpret()` |
18+
| Is the 1.6x speedup significant for your use case? | Maybe consider it | Use `interpret()` |
19+
20+
## Performance Comparison
21+
22+
| Metric | `interpret()` | `interpretManual()` |
23+
|--------|--------------|---------------------|
24+
| Actor creation speed | Baseline | ~1.6x faster |
25+
| Cleanup | Automatic (via Scope) | **Manual** (you call `stop()`) |
26+
| Memory leak risk | None | **High if you forget cleanup** |
27+
| Code complexity | Simple | Complex |
28+
| Recommended | ✅ Yes | ❌ No (usually) |
29+
30+
## The Problem with `interpretManual()`
31+
32+
```typescript
33+
// With interpret() - cleanup is automatic
34+
const actor = yield* interpret(machine);
35+
// When Scope closes → finalizer runs → actor.stop() called automatically
36+
37+
// With interpretManual() - YOU must cleanup
38+
const actor = Effect.runSync(interpretManual(machine));
39+
// If you forget to call actor.stop(), the actor LEAKS:
40+
// - Activities keep running forever
41+
// - Timers keep firing
42+
// - Memory is never freed
43+
```
44+
45+
## Required Cleanup Pattern
46+
47+
If you DO use `interpretManual()`, you MUST handle cleanup:
48+
49+
```tsx
50+
// In React:
51+
useEffect(() => {
52+
const actor = Effect.runSync(interpretManual(machine));
53+
54+
return () => {
55+
actor.stop(); // CRITICAL! Without this, you leak!
56+
};
57+
}, []);
58+
```
59+
60+
## Why does `interpretManual()` exist?
61+
62+
For rare cases where:
63+
1. You're creating many short-lived actors
64+
2. Actor creation overhead is a measured bottleneck
65+
3. You're managing lifecycle manually anyway
66+
4. The ~1.6x speedup matters for your use case
67+
68+
## Running This Demo
69+
70+
```bash
71+
pnpm --filter demo-advanced dev
72+
```
73+
74+
Watch the lifecycle log to see:
75+
- When actors are created
76+
- When cleanup happens (or doesn't!)
77+
- What gets logged when you stop/restart
78+
79+
## Files in This Demo
80+
81+
- `src/data-access/manual-actor.ts` - The complex lifecycle management code
82+
- `src/components/ManualLifecycleDemo.tsx` - UI showing the pattern
83+
- `src/App.tsx` - Entry point with cleanup in useEffect
84+
85+
## Compare to the Main Demo
86+
87+
The main demo (`apps/demo`) uses `interpret()` with Effect-Atom for a much simpler pattern:
88+
89+
```typescript
90+
// Main demo approach - simple and safe
91+
const actorAtom = appRuntime
92+
.atom(interpret(machine))
93+
.pipe(Atom.keepAlive);
94+
95+
// That's it! No manual cleanup needed.
96+
```
97+
98+
## Conclusion
99+
100+
**Use `interpret()` unless you have a very specific, measured need for `interpretManual()`.**
101+
102+
The complexity and risk of memory leaks almost never justifies the small performance gain.

apps/demo-advanced/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>effstate - Advanced Demo (interpretManual)</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

apps/demo-advanced/package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "demo-advanced",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10+
"preview": "vite preview",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"effect": "^3.19.12",
15+
"effstate": "workspace:*",
16+
"react": "^18.3.1",
17+
"react-dom": "^18.3.1",
18+
"clsx": "^2.1.1",
19+
"tailwind-merge": "^2.5.4",
20+
"tailwindcss-animate": "^1.0.7",
21+
"class-variance-authority": "^0.7.0",
22+
"@radix-ui/react-slot": "^1.1.0"
23+
},
24+
"devDependencies": {
25+
"@types/node": "^22.9.0",
26+
"@types/react": "^18.3.3",
27+
"@types/react-dom": "^18.3.0",
28+
"@typescript-eslint/eslint-plugin": "^7.15.0",
29+
"@typescript-eslint/parser": "^7.15.0",
30+
"@vitejs/plugin-react-swc": "^3.5.0",
31+
"autoprefixer": "^10.4.20",
32+
"eslint": "^8.57.0",
33+
"eslint-plugin-react-hooks": "^4.6.2",
34+
"eslint-plugin-react-refresh": "^0.4.7",
35+
"postcss": "^8.4.49",
36+
"tailwindcss": "^3.4.14",
37+
"typescript": "^5.2.2",
38+
"vite": "^5.3.4"
39+
}
40+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}

apps/demo-advanced/src/App.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Advanced Demo: interpretManual() Lifecycle Management
3+
*
4+
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
5+
* !! WARNING: This demo shows an ADVANCED pattern that is NOT RECOMMENDED !!
6+
* !! for most applications. Use interpret() instead for automatic cleanup. !!
7+
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
8+
*
9+
* This demo exists to:
10+
* 1. Show HOW interpretManual() works
11+
* 2. Demonstrate the cleanup complexity required
12+
* 3. Explain the (small) performance gains
13+
*
14+
* Performance gains: ~1.6x faster actor creation
15+
* Complexity cost: Manual lifecycle management, risk of memory leaks
16+
*
17+
* RECOMMENDATION: Use interpret() unless you have measured a performance
18+
* bottleneck in actor creation AND you're creating thousands of actors.
19+
*/
20+
21+
import { useEffect } from "react";
22+
import { ManualLifecycleDemo } from "./components/ManualLifecycleDemo";
23+
import {
24+
initializeActor,
25+
cleanupActor,
26+
} from "./data-access/manual-actor";
27+
28+
function App() {
29+
// CRITICAL: This is the cleanup pattern required for interpretManual()
30+
// Without this useEffect, the actor would LEAK when the component unmounts
31+
useEffect(() => {
32+
initializeActor();
33+
return () => {
34+
cleanupActor();
35+
};
36+
}, []);
37+
38+
return (
39+
<div className="min-h-screen bg-slate-900">
40+
{/* Warning banner */}
41+
<div className="bg-red-900/80 border-b-2 border-red-500 px-4 py-3 text-center">
42+
<div className="text-red-200 text-sm font-bold">
43+
⚠️ ADVANCED DEMO - NOT RECOMMENDED FOR PRODUCTION ⚠️
44+
</div>
45+
<div className="text-red-300 text-xs mt-1">
46+
This demonstrates <code className="bg-red-800 px-1 rounded">interpretManual()</code> which requires manual cleanup.
47+
Use <code className="bg-green-800 px-1 rounded">interpret()</code> instead for automatic lifecycle management.
48+
</div>
49+
</div>
50+
51+
<ManualLifecycleDemo />
52+
</div>
53+
);
54+
}
55+
56+
export default App;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Manual Lifecycle Demo Component
3+
*
4+
* This demonstrates the complexity required when using interpretManual().
5+
* Compare this to the simplicity of using interpret() with atoms!
6+
*/
7+
8+
import { Button } from "./ui/button";
9+
import { useManualActor, useLifecycleLogs, clearLogs, type LifecycleLog } from "../data-access/manual-actor";
10+
import { cn } from "../lib/utils";
11+
12+
const LogPanel = ({ logs }: { logs: LifecycleLog[] }) => (
13+
<div className="bg-gray-950 rounded-lg p-4 font-mono text-xs">
14+
<div className="flex items-center justify-between mb-3">
15+
<span className="text-gray-400 font-bold">Lifecycle Log</span>
16+
<button onClick={clearLogs} className="text-gray-500 hover:text-gray-300 text-xs">
17+
Clear
18+
</button>
19+
</div>
20+
<div className="space-y-1 h-64 overflow-y-auto">
21+
{logs.length === 0 ? (
22+
<div className="text-gray-600">Logs will appear here...</div>
23+
) : (
24+
logs.map((log, i) => (
25+
<div
26+
key={i}
27+
className={cn(
28+
"py-0.5",
29+
log.type === "info" && "text-blue-400",
30+
log.type === "warning" && "text-yellow-400",
31+
log.type === "error" && "text-red-400",
32+
log.type === "success" && "text-green-400",
33+
log.message.startsWith(" →") && "pl-4 text-gray-400"
34+
)}
35+
>
36+
<span className="text-gray-600 mr-2">
37+
{log.timestamp.toLocaleTimeString()}
38+
</span>
39+
{log.message}
40+
</div>
41+
))
42+
)}
43+
</div>
44+
</div>
45+
);
46+
47+
export const ManualLifecycleDemo = () => {
48+
const { count, tickCount, isStopped, increment, decrement, stop, restart } = useManualActor();
49+
const logs = useLifecycleLogs();
50+
51+
return (
52+
<div className="p-8 max-w-4xl mx-auto">
53+
<h1 className="text-2xl font-bold text-white mb-2">
54+
interpretManual() Demo
55+
</h1>
56+
<p className="text-gray-400 mb-6">
57+
This counter has an activity that ticks every 100ms. Watch what happens when you stop/restart.
58+
</p>
59+
60+
{/* Counter Display */}
61+
<div className={cn(
62+
"rounded-xl p-8 mb-6 text-center transition-all",
63+
isStopped ? "bg-red-950 border-2 border-red-800" : "bg-slate-800"
64+
)}>
65+
{isStopped && (
66+
<div className="text-red-400 text-sm mb-4 font-bold">
67+
ACTOR STOPPED - Activity interrupted!
68+
</div>
69+
)}
70+
71+
<div className="text-6xl font-bold text-white mb-2">{count}</div>
72+
<div className="text-gray-400 text-sm mb-6">
73+
Ticks: {tickCount} {!isStopped && <span className="text-green-400">(counting...)</span>}
74+
</div>
75+
76+
<div className="flex gap-3 justify-center mb-6">
77+
<Button onClick={decrement} disabled={isStopped} variant="outline" size="lg">
78+
-1
79+
</Button>
80+
<Button onClick={increment} disabled={isStopped} variant="outline" size="lg">
81+
+1
82+
</Button>
83+
</div>
84+
85+
<div className="flex gap-3 justify-center">
86+
<Button
87+
onClick={stop}
88+
disabled={isStopped}
89+
variant="destructive"
90+
>
91+
Stop Actor (actor.stop())
92+
</Button>
93+
<Button
94+
onClick={restart}
95+
variant="default"
96+
className="bg-green-700 hover:bg-green-600"
97+
>
98+
{isStopped ? "Start Actor" : "Restart Actor"}
99+
</Button>
100+
</div>
101+
</div>
102+
103+
{/* Comparison Box */}
104+
<div className="grid grid-cols-2 gap-4 mb-6">
105+
<div className="bg-green-950 border border-green-800 rounded-lg p-4">
106+
<div className="text-green-400 font-bold mb-2">✅ interpret() (Recommended)</div>
107+
<ul className="text-green-300 text-sm space-y-1">
108+
<li>• Automatic cleanup via Scope</li>
109+
<li>• No risk of memory leaks</li>
110+
<li>• Works great with atoms</li>
111+
<li>• Simpler code</li>
112+
</ul>
113+
</div>
114+
<div className="bg-red-950 border border-red-800 rounded-lg p-4">
115+
<div className="text-red-400 font-bold mb-2">⚠️ interpretManual() (This demo)</div>
116+
<ul className="text-red-300 text-sm space-y-1">
117+
<li>• ~1.6x faster actor creation</li>
118+
<li>• YOU must call actor.stop()</li>
119+
<li>• Risk of memory leaks</li>
120+
<li>• Complex lifecycle code</li>
121+
</ul>
122+
</div>
123+
</div>
124+
125+
{/* Lifecycle Log */}
126+
<LogPanel logs={logs} />
127+
128+
{/* Code Example */}
129+
<div className="mt-6 bg-gray-950 rounded-lg p-4">
130+
<div className="text-gray-400 font-bold mb-2 text-sm">Required Cleanup Pattern:</div>
131+
<pre className="text-xs text-gray-300 overflow-x-auto">
132+
{`// In your React component:
133+
useEffect(() => {
134+
initializeActor(); // Create with interpretManual()
135+
return () => {
136+
cleanupActor(); // MUST call actor.stop() here!
137+
};
138+
}, []);
139+
140+
// If you forget cleanupActor(), the actor LEAKS:
141+
// - Activities keep running
142+
// - Timers keep firing
143+
// - Memory never freed`}
144+
</pre>
145+
</div>
146+
</div>
147+
);
148+
};

0 commit comments

Comments
 (0)