Skip to content

Commit 8873b3c

Browse files
authored
Merge pull request #1 from Handfish/0.0.3
0.0.3
2 parents 4985ee6 + adf6495 commit 8873b3c

39 files changed

+1454
-388
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": ["demo", "docs"]
10+
"ignore": ["demo", "demo-advanced", "docs"]
1111
}

.changeset/free-jars-fly.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"effstate": patch
3+
"@effstate/react": patch
4+
---
5+
6+
Require schema and runtime, better api more typesafe

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@
2828
- **Invocations**: Async operations with automatic result handling
2929
- **Parent-child machines**: Spawn child machines and communicate via events
3030
- **Cross-tab sync**: Built-in support for synchronizing state across browser tabs
31-
- **Schema validation**: Optional Effect Schema integration for context validation
31+
- **Schema-first**: Required Effect Schema for context - enables serialization, cross-tab sync, and validation
3232

3333
## Why effstate over XState?
3434

3535
| Metric | effstate | XState |
3636
|--------|----------|--------|
3737
| **Bundle size (gzip)** | **~3.9 kB** | 13.7 kB |
38-
| Event processing | **30x faster** | - |
39-
| With subscribers | **14x 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

@@ -80,7 +80,7 @@ class Retry extends Data.TaggedClass("RETRY")<{}> {}
8080
type ConnectionEvent = Connect | Disconnect | Retry;
8181

8282
// =============================================================================
83-
// 2. Define context schema (optional but recommended)
83+
// 2. Define context schema (required for all machines)
8484
// =============================================================================
8585

8686
const ConnectionContextSchema = Schema.Struct({

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;

0 commit comments

Comments
 (0)