Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["demo", "docs"]
"ignore": ["demo", "demo-advanced", "docs"]
}
6 changes: 6 additions & 0 deletions .changeset/free-jars-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"effstate": patch
"@effstate/react": patch
---

Require schema and runtime, better api more typesafe
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@
- **Invocations**: Async operations with automatic result handling
- **Parent-child machines**: Spawn child machines and communicate via events
- **Cross-tab sync**: Built-in support for synchronizing state across browser tabs
- **Schema validation**: Optional Effect Schema integration for context validation
- **Schema-first**: Required Effect Schema for context - enables serialization, cross-tab sync, and validation

## Why effstate over XState?

| Metric | effstate | XState |
|--------|----------|--------|
| **Bundle size (gzip)** | **~3.9 kB** | 13.7 kB |
| Event processing | **30x faster** | - |
| With subscribers | **14x faster** | - |
| Event processing | **25x faster** | - |
| Realistic app lifecycle | **5x faster** | - |

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

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

// =============================================================================
// 2. Define context schema (optional but recommended)
// 2. Define context schema (required for all machines)
// =============================================================================

const ConnectionContextSchema = Schema.Struct({
Expand Down
16 changes: 16 additions & 0 deletions apps/demo-advanced/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'off',
'@typescript-eslint/ban-types': 'off',
},
}
102 changes: 102 additions & 0 deletions apps/demo-advanced/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Advanced Demo: `interpretManual()`

> ⚠️ **WARNING: This demo shows an advanced pattern that is NOT RECOMMENDED for most applications.**

## What is this?

This demo demonstrates `interpretManual()`, an alternative to `interpret()` that provides slightly faster actor creation at the cost of **manual lifecycle management**.

## Should I use `interpretManual()`?

**Almost certainly not.**

| Question | If Yes | If No |
|----------|--------|-------|
| Are you creating thousands of actors per second? | Maybe consider it | Use `interpret()` |
| Have you profiled and confirmed actor creation is a bottleneck? | Maybe consider it | Use `interpret()` |
| Are you comfortable managing cleanup manually? | Maybe consider it | Use `interpret()` |
| Is the 1.6x speedup significant for your use case? | Maybe consider it | Use `interpret()` |

## Performance Comparison

| Metric | `interpret()` | `interpretManual()` |
|--------|--------------|---------------------|
| Actor creation speed | Baseline | ~1.6x faster |
| Cleanup | Automatic (via Scope) | **Manual** (you call `stop()`) |
| Memory leak risk | None | **High if you forget cleanup** |
| Code complexity | Simple | Complex |
| Recommended | ✅ Yes | ❌ No (usually) |

## The Problem with `interpretManual()`

```typescript
// With interpret() - cleanup is automatic
const actor = yield* interpret(machine);
// When Scope closes → finalizer runs → actor.stop() called automatically

// With interpretManual() - YOU must cleanup
const actor = Effect.runSync(interpretManual(machine));
// If you forget to call actor.stop(), the actor LEAKS:
// - Activities keep running forever
// - Timers keep firing
// - Memory is never freed
```

## Required Cleanup Pattern

If you DO use `interpretManual()`, you MUST handle cleanup:

```tsx
// In React:
useEffect(() => {
const actor = Effect.runSync(interpretManual(machine));

return () => {
actor.stop(); // CRITICAL! Without this, you leak!
};
}, []);
```

## Why does `interpretManual()` exist?

For rare cases where:
1. You're creating many short-lived actors
2. Actor creation overhead is a measured bottleneck
3. You're managing lifecycle manually anyway
4. The ~1.6x speedup matters for your use case

## Running This Demo

```bash
pnpm --filter demo-advanced dev
```

Watch the lifecycle log to see:
- When actors are created
- When cleanup happens (or doesn't!)
- What gets logged when you stop/restart

## Files in This Demo

- `src/data-access/manual-actor.ts` - The complex lifecycle management code
- `src/components/ManualLifecycleDemo.tsx` - UI showing the pattern
- `src/App.tsx` - Entry point with cleanup in useEffect

## Compare to the Main Demo

The main demo (`apps/demo`) uses `interpret()` with Effect-Atom for a much simpler pattern:

```typescript
// Main demo approach - simple and safe
const actorAtom = appRuntime
.atom(interpret(machine))
.pipe(Atom.keepAlive);

// That's it! No manual cleanup needed.
```

## Conclusion

**Use `interpret()` unless you have a very specific, measured need for `interpretManual()`.**

The complexity and risk of memory leaks almost never justifies the small performance gain.
13 changes: 13 additions & 0 deletions apps/demo-advanced/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>effstate - Advanced Demo (interpretManual)</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
40 changes: 40 additions & 0 deletions apps/demo-advanced/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "demo-advanced",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"effect": "^3.19.12",
"effstate": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"class-variance-authority": "^0.7.0",
"@radix-ui/react-slot": "^1.1.0"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.14",
"typescript": "^5.2.2",
"vite": "^5.3.4"
}
}
6 changes: 6 additions & 0 deletions apps/demo-advanced/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
56 changes: 56 additions & 0 deletions apps/demo-advanced/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Advanced Demo: interpretManual() Lifecycle Management
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* !! WARNING: This demo shows an ADVANCED pattern that is NOT RECOMMENDED !!
* !! for most applications. Use interpret() instead for automatic cleanup. !!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* This demo exists to:
* 1. Show HOW interpretManual() works
* 2. Demonstrate the cleanup complexity required
* 3. Explain the (small) performance gains
*
* Performance gains: ~1.6x faster actor creation
* Complexity cost: Manual lifecycle management, risk of memory leaks
*
* RECOMMENDATION: Use interpret() unless you have measured a performance
* bottleneck in actor creation AND you're creating thousands of actors.
*/

import { useEffect } from "react";
import { ManualLifecycleDemo } from "./components/ManualLifecycleDemo";
import {
initializeActor,
cleanupActor,
} from "./data-access/manual-actor";

function App() {
// CRITICAL: This is the cleanup pattern required for interpretManual()
// Without this useEffect, the actor would LEAK when the component unmounts
useEffect(() => {
initializeActor();
return () => {
cleanupActor();
};
}, []);

return (
<div className="min-h-screen bg-slate-900">
{/* Warning banner */}
<div className="bg-red-900/80 border-b-2 border-red-500 px-4 py-3 text-center">
<div className="text-red-200 text-sm font-bold">
⚠️ ADVANCED DEMO - NOT RECOMMENDED FOR PRODUCTION ⚠️
</div>
<div className="text-red-300 text-xs mt-1">
This demonstrates <code className="bg-red-800 px-1 rounded">interpretManual()</code> which requires manual cleanup.
Use <code className="bg-green-800 px-1 rounded">interpret()</code> instead for automatic lifecycle management.
</div>
</div>

<ManualLifecycleDemo />
</div>
);
}

export default App;
Loading