@plures/praxis 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FRAMEWORK.md +420 -0
- package/LICENSE +21 -0
- package/README.md +1310 -0
- package/dist/adapters/cli.d.ts +43 -0
- package/dist/adapters/cli.d.ts.map +1 -0
- package/dist/adapters/cli.js +126 -0
- package/dist/adapters/cli.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +26 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +233 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/cloud.d.ts +27 -0
- package/dist/cli/commands/cloud.d.ts.map +1 -0
- package/dist/cli/commands/cloud.js +232 -0
- package/dist/cli/commands/cloud.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +25 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +168 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +179 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cloud/auth.d.ts +51 -0
- package/dist/cloud/auth.d.ts.map +1 -0
- package/dist/cloud/auth.js +194 -0
- package/dist/cloud/auth.js.map +1 -0
- package/dist/cloud/billing.d.ts +184 -0
- package/dist/cloud/billing.d.ts.map +1 -0
- package/dist/cloud/billing.js +179 -0
- package/dist/cloud/billing.js.map +1 -0
- package/dist/cloud/client.d.ts +39 -0
- package/dist/cloud/client.d.ts.map +1 -0
- package/dist/cloud/client.js +176 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/cloud/index.d.ts +44 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +44 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/marketplace.d.ts +166 -0
- package/dist/cloud/marketplace.d.ts.map +1 -0
- package/dist/cloud/marketplace.js +159 -0
- package/dist/cloud/marketplace.js.map +1 -0
- package/dist/cloud/provisioning.d.ts +110 -0
- package/dist/cloud/provisioning.d.ts.map +1 -0
- package/dist/cloud/provisioning.js +148 -0
- package/dist/cloud/provisioning.js.map +1 -0
- package/dist/cloud/relay/endpoints.d.ts +62 -0
- package/dist/cloud/relay/endpoints.d.ts.map +1 -0
- package/dist/cloud/relay/endpoints.js +217 -0
- package/dist/cloud/relay/endpoints.js.map +1 -0
- package/dist/cloud/relay/health/index.d.ts +5 -0
- package/dist/cloud/relay/health/index.d.ts.map +1 -0
- package/dist/cloud/relay/health/index.js +9 -0
- package/dist/cloud/relay/health/index.js.map +1 -0
- package/dist/cloud/relay/stats/index.d.ts +5 -0
- package/dist/cloud/relay/stats/index.d.ts.map +1 -0
- package/dist/cloud/relay/stats/index.js +9 -0
- package/dist/cloud/relay/stats/index.js.map +1 -0
- package/dist/cloud/relay/sync/index.d.ts +5 -0
- package/dist/cloud/relay/sync/index.d.ts.map +1 -0
- package/dist/cloud/relay/sync/index.js +9 -0
- package/dist/cloud/relay/sync/index.js.map +1 -0
- package/dist/cloud/relay/usage/index.d.ts +5 -0
- package/dist/cloud/relay/usage/index.d.ts.map +1 -0
- package/dist/cloud/relay/usage/index.js +9 -0
- package/dist/cloud/relay/usage/index.js.map +1 -0
- package/dist/cloud/sponsors.d.ts +81 -0
- package/dist/cloud/sponsors.d.ts.map +1 -0
- package/dist/cloud/sponsors.js +130 -0
- package/dist/cloud/sponsors.js.map +1 -0
- package/dist/cloud/types.d.ts +169 -0
- package/dist/cloud/types.d.ts.map +1 -0
- package/dist/cloud/types.js +7 -0
- package/dist/cloud/types.js.map +1 -0
- package/dist/components/index.d.ts +43 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +17 -0
- package/dist/components/index.js.map +1 -0
- package/dist/core/actors.d.ts +95 -0
- package/dist/core/actors.d.ts.map +1 -0
- package/dist/core/actors.js +158 -0
- package/dist/core/actors.js.map +1 -0
- package/dist/core/component/generator.d.ts +122 -0
- package/dist/core/component/generator.d.ts.map +1 -0
- package/dist/core/component/generator.js +307 -0
- package/dist/core/component/generator.js.map +1 -0
- package/dist/core/engine.d.ts +92 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +199 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/introspection.d.ts +141 -0
- package/dist/core/introspection.d.ts.map +1 -0
- package/dist/core/introspection.js +208 -0
- package/dist/core/introspection.js.map +1 -0
- package/dist/core/logic/generator.d.ts +76 -0
- package/dist/core/logic/generator.d.ts.map +1 -0
- package/dist/core/logic/generator.js +339 -0
- package/dist/core/logic/generator.js.map +1 -0
- package/dist/core/pluresdb/generator.d.ts +58 -0
- package/dist/core/pluresdb/generator.d.ts.map +1 -0
- package/dist/core/pluresdb/generator.js +162 -0
- package/dist/core/pluresdb/generator.js.map +1 -0
- package/dist/core/protocol.d.ts +121 -0
- package/dist/core/protocol.d.ts.map +1 -0
- package/dist/core/protocol.js +46 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/rules.d.ts +120 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +81 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/schema/loader.d.ts +47 -0
- package/dist/core/schema/loader.d.ts.map +1 -0
- package/dist/core/schema/loader.js +189 -0
- package/dist/core/schema/loader.js.map +1 -0
- package/dist/core/schema/normalize.d.ts +72 -0
- package/dist/core/schema/normalize.d.ts.map +1 -0
- package/dist/core/schema/normalize.js +190 -0
- package/dist/core/schema/normalize.js.map +1 -0
- package/dist/core/schema/types.d.ts +370 -0
- package/dist/core/schema/types.d.ts.map +1 -0
- package/dist/core/schema/types.js +161 -0
- package/dist/core/schema/types.js.map +1 -0
- package/dist/dsl/index.d.ts +152 -0
- package/dist/dsl/index.d.ts.map +1 -0
- package/dist/dsl/index.js +132 -0
- package/dist/dsl/index.js.map +1 -0
- package/dist/dsl.d.ts +124 -0
- package/dist/dsl.d.ts.map +1 -0
- package/dist/dsl.js +130 -0
- package/dist/dsl.js.map +1 -0
- package/dist/examples/advanced-todo/index.d.ts +55 -0
- package/dist/examples/advanced-todo/index.d.ts.map +1 -0
- package/dist/examples/advanced-todo/index.js +222 -0
- package/dist/examples/advanced-todo/index.js.map +1 -0
- package/dist/examples/auth-basic/index.d.ts +17 -0
- package/dist/examples/auth-basic/index.d.ts.map +1 -0
- package/dist/examples/auth-basic/index.js +122 -0
- package/dist/examples/auth-basic/index.js.map +1 -0
- package/dist/examples/cart/index.d.ts +19 -0
- package/dist/examples/cart/index.d.ts.map +1 -0
- package/dist/examples/cart/index.js +202 -0
- package/dist/examples/cart/index.js.map +1 -0
- package/dist/examples/hero-ecommerce/index.d.ts +39 -0
- package/dist/examples/hero-ecommerce/index.d.ts.map +1 -0
- package/dist/examples/hero-ecommerce/index.js +506 -0
- package/dist/examples/hero-ecommerce/index.js.map +1 -0
- package/dist/examples/svelte-counter/index.d.ts +31 -0
- package/dist/examples/svelte-counter/index.d.ts.map +1 -0
- package/dist/examples/svelte-counter/index.js +123 -0
- package/dist/examples/svelte-counter/index.js.map +1 -0
- package/dist/flows.d.ts +125 -0
- package/dist/flows.d.ts.map +1 -0
- package/dist/flows.js +160 -0
- package/dist/flows.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/pluresdb.d.ts +56 -0
- package/dist/integrations/pluresdb.d.ts.map +1 -0
- package/dist/integrations/pluresdb.js +46 -0
- package/dist/integrations/pluresdb.js.map +1 -0
- package/dist/integrations/svelte.d.ts +306 -0
- package/dist/integrations/svelte.d.ts.map +1 -0
- package/dist/integrations/svelte.js +447 -0
- package/dist/integrations/svelte.js.map +1 -0
- package/dist/registry.d.ts +94 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +181 -0
- package/dist/registry.js.map +1 -0
- package/dist/runtime/terminal-adapter.d.ts +105 -0
- package/dist/runtime/terminal-adapter.d.ts.map +1 -0
- package/dist/runtime/terminal-adapter.js +113 -0
- package/dist/runtime/terminal-adapter.js.map +1 -0
- package/dist/step.d.ts +34 -0
- package/dist/step.d.ts.map +1 -0
- package/dist/step.js +111 -0
- package/dist/step.js.map +1 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/MONETIZATION.md +394 -0
- package/docs/TERMINAL_NODE.md +588 -0
- package/docs/guides/canvas.md +389 -0
- package/docs/guides/getting-started.md +347 -0
- package/docs/guides/history-state-pattern.md +618 -0
- package/docs/guides/orchestration.md +617 -0
- package/docs/guides/parallel-state-pattern.md +767 -0
- package/docs/guides/svelte-integration.md +691 -0
- package/package.json +96 -0
- package/src/__tests__/actors.test.ts +270 -0
- package/src/__tests__/billing.test.ts +175 -0
- package/src/__tests__/cloud.test.ts +247 -0
- package/src/__tests__/dsl.test.ts +154 -0
- package/src/__tests__/edge-cases.test.ts +475 -0
- package/src/__tests__/engine.test.ts +137 -0
- package/src/__tests__/generators.test.ts +270 -0
- package/src/__tests__/introspection.test.ts +321 -0
- package/src/__tests__/protocol.test.ts +40 -0
- package/src/__tests__/provisioning.test.ts +162 -0
- package/src/__tests__/schema.test.ts +241 -0
- package/src/__tests__/svelte-integration.test.ts +431 -0
- package/src/__tests__/terminal-node.test.ts +352 -0
- package/src/adapters/cli.ts +175 -0
- package/src/cli/commands/auth.ts +271 -0
- package/src/cli/commands/cloud.ts +281 -0
- package/src/cli/commands/generate.ts +225 -0
- package/src/cli/index.ts +190 -0
- package/src/cloud/README.md +383 -0
- package/src/cloud/auth.ts +245 -0
- package/src/cloud/billing.ts +336 -0
- package/src/cloud/client.ts +221 -0
- package/src/cloud/index.ts +121 -0
- package/src/cloud/marketplace.ts +303 -0
- package/src/cloud/provisioning.ts +254 -0
- package/src/cloud/relay/endpoints.ts +307 -0
- package/src/cloud/relay/health/function.json +17 -0
- package/src/cloud/relay/health/index.ts +10 -0
- package/src/cloud/relay/host.json +15 -0
- package/src/cloud/relay/local.settings.json +8 -0
- package/src/cloud/relay/stats/function.json +17 -0
- package/src/cloud/relay/stats/index.ts +10 -0
- package/src/cloud/relay/sync/function.json +17 -0
- package/src/cloud/relay/sync/index.ts +10 -0
- package/src/cloud/relay/usage/function.json +17 -0
- package/src/cloud/relay/usage/index.ts +10 -0
- package/src/cloud/sponsors.ts +213 -0
- package/src/cloud/types.ts +198 -0
- package/src/components/README.md +125 -0
- package/src/components/TerminalNode.svelte +457 -0
- package/src/components/index.ts +46 -0
- package/src/core/actors.ts +205 -0
- package/src/core/component/generator.ts +432 -0
- package/src/core/engine.ts +243 -0
- package/src/core/introspection.ts +329 -0
- package/src/core/logic/generator.ts +420 -0
- package/src/core/pluresdb/generator.ts +229 -0
- package/src/core/protocol.ts +132 -0
- package/src/core/rules.ts +167 -0
- package/src/core/schema/loader.ts +247 -0
- package/src/core/schema/normalize.ts +322 -0
- package/src/core/schema/types.ts +557 -0
- package/src/dsl/index.ts +218 -0
- package/src/dsl.ts +214 -0
- package/src/examples/advanced-todo/App.svelte +506 -0
- package/src/examples/advanced-todo/README.md +371 -0
- package/src/examples/advanced-todo/index.ts +309 -0
- package/src/examples/auth-basic/index.ts +163 -0
- package/src/examples/cart/index.ts +259 -0
- package/src/examples/hero-ecommerce/index.ts +657 -0
- package/src/examples/svelte-counter/index.ts +168 -0
- package/src/flows.ts +268 -0
- package/src/index.ts +154 -0
- package/src/integrations/pluresdb.ts +93 -0
- package/src/integrations/svelte.ts +617 -0
- package/src/registry.ts +223 -0
- package/src/runtime/terminal-adapter.ts +175 -0
- package/src/step.ts +151 -0
- package/src/types.ts +70 -0
- package/templates/basic-app/README.md +147 -0
- package/templates/fullstack-app/README.md +279 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Advanced Todo App with Praxis
|
|
2
|
+
|
|
3
|
+
A comprehensive example demonstrating Praxis's Svelte 5 integration with history state pattern.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Core Functionality
|
|
8
|
+
- ✅ Add, complete, and remove todos
|
|
9
|
+
- ✅ Filter by all, active, or completed
|
|
10
|
+
- ✅ Complete all todos at once
|
|
11
|
+
- ✅ Clear completed todos
|
|
12
|
+
- ✅ Real-time statistics
|
|
13
|
+
|
|
14
|
+
### Advanced Features
|
|
15
|
+
- 🔄 **Undo/Redo**: Full history tracking with keyboard shortcuts
|
|
16
|
+
- 🕰️ **Time-Travel Debugging**: Navigate through state snapshots
|
|
17
|
+
- ⚡ **Svelte 5 Runes**: Modern reactive API
|
|
18
|
+
- 🎨 **Beautiful UI**: Clean, intuitive interface
|
|
19
|
+
- ⌨️ **Keyboard Shortcuts**: Efficient workflow
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
### State Management
|
|
24
|
+
- **Engine**: Praxis logic engine managing todo state
|
|
25
|
+
- **Rules**: Pure functions handling events
|
|
26
|
+
- **Facts**: Immutable history of what happened
|
|
27
|
+
- **Context**: Current application state
|
|
28
|
+
|
|
29
|
+
### Svelte Integration
|
|
30
|
+
- **usePraxisEngine**: Main composable with history support
|
|
31
|
+
- **Reactive Derivations**: Computed values with `$:` syntax
|
|
32
|
+
- **Event Dispatching**: Type-safe event handling
|
|
33
|
+
- **Snapshot Navigation**: Time-travel through history
|
|
34
|
+
|
|
35
|
+
## Code Structure
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
advanced-todo/
|
|
39
|
+
├── index.ts # Engine definition, rules, and logic
|
|
40
|
+
├── App.svelte # Svelte 5 component with UI
|
|
41
|
+
└── README.md # This file
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Engine Definition (`index.ts`)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Define context type
|
|
48
|
+
interface TodoContext {
|
|
49
|
+
todos: TodoItem[];
|
|
50
|
+
filter: 'all' | 'active' | 'completed';
|
|
51
|
+
nextId: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Define events
|
|
55
|
+
const AddTodo = defineEvent<'ADD_TODO', { text: string }>('ADD_TODO');
|
|
56
|
+
const ToggleTodo = defineEvent<'TOGGLE_TODO', { id: string }>('TOGGLE_TODO');
|
|
57
|
+
// ... more events
|
|
58
|
+
|
|
59
|
+
// Define rules
|
|
60
|
+
const addTodoRule = defineRule<TodoContext>({
|
|
61
|
+
id: 'todo.add',
|
|
62
|
+
description: 'Add a new todo item',
|
|
63
|
+
impl: (state, events) => {
|
|
64
|
+
// Pure function: no side effects
|
|
65
|
+
// Returns facts about what happened
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create engine
|
|
70
|
+
export function createTodoEngine() {
|
|
71
|
+
const registry = new PraxisRegistry<TodoContext>();
|
|
72
|
+
registry.registerRule(addTodoRule);
|
|
73
|
+
// ... register more rules
|
|
74
|
+
|
|
75
|
+
return createPraxisEngine({
|
|
76
|
+
initialContext: { todos: [], filter: 'all', nextId: 1 },
|
|
77
|
+
registry,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Svelte Component (`App.svelte`)
|
|
83
|
+
|
|
84
|
+
```svelte
|
|
85
|
+
<script lang="ts">
|
|
86
|
+
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
87
|
+
import { createTodoEngine, AddTodo } from './index';
|
|
88
|
+
|
|
89
|
+
const engine = createTodoEngine();
|
|
90
|
+
const {
|
|
91
|
+
context, // Reactive context
|
|
92
|
+
dispatch, // Dispatch events
|
|
93
|
+
undo, // Undo last action
|
|
94
|
+
redo, // Redo action
|
|
95
|
+
canUndo, // Boolean: can undo?
|
|
96
|
+
canRedo, // Boolean: can redo?
|
|
97
|
+
snapshots, // History snapshots
|
|
98
|
+
historyIndex, // Current position
|
|
99
|
+
goToSnapshot, // Jump to snapshot
|
|
100
|
+
} = usePraxisEngine(engine, {
|
|
101
|
+
enableHistory: true,
|
|
102
|
+
maxHistorySize: 50,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
function handleAddTodo(text: string) {
|
|
106
|
+
dispatch([AddTodo.create({ text })], 'Add Todo');
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<div>
|
|
111
|
+
<p>Todos: {context.todos.length}</p>
|
|
112
|
+
<button onclick={undo} disabled={!canUndo}>Undo</button>
|
|
113
|
+
<button onclick={redo} disabled={!canRedo}>Redo</button>
|
|
114
|
+
</div>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Running the Example
|
|
118
|
+
|
|
119
|
+
### Non-Svelte Version
|
|
120
|
+
|
|
121
|
+
Run the TypeScript example directly:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd /home/runner/work/praxis/praxis
|
|
125
|
+
npm run build
|
|
126
|
+
node dist/examples/advanced-todo/index.js
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Output:
|
|
130
|
+
```
|
|
131
|
+
=== Advanced Todo Example ===
|
|
132
|
+
|
|
133
|
+
1. Adding todos:
|
|
134
|
+
Total todos: 3
|
|
135
|
+
- Learn Praxis
|
|
136
|
+
- Build awesome app
|
|
137
|
+
- Deploy to production
|
|
138
|
+
|
|
139
|
+
2. Completing first todo:
|
|
140
|
+
Active: 2, Completed: 1
|
|
141
|
+
|
|
142
|
+
3. Filtering to active todos:
|
|
143
|
+
Filter: active
|
|
144
|
+
Showing 2 todos:
|
|
145
|
+
- Build awesome app
|
|
146
|
+
- Deploy to production
|
|
147
|
+
|
|
148
|
+
4. Completing all todos:
|
|
149
|
+
All completed: 3/3
|
|
150
|
+
|
|
151
|
+
5. Clearing completed todos:
|
|
152
|
+
Remaining todos: 0
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Svelte 5 Version
|
|
156
|
+
|
|
157
|
+
To use the Svelte component in your app:
|
|
158
|
+
|
|
159
|
+
1. Install dependencies:
|
|
160
|
+
```bash
|
|
161
|
+
npm install @plures/praxis svelte@^5.0.0
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
2. Import and use the component:
|
|
165
|
+
```svelte
|
|
166
|
+
<script>
|
|
167
|
+
import TodoApp from '@plures/praxis/examples/advanced-todo/App.svelte';
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<TodoApp />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or create your own component using the engine:
|
|
174
|
+
|
|
175
|
+
```svelte
|
|
176
|
+
<script lang="ts">
|
|
177
|
+
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
178
|
+
import { createTodoEngine, AddTodo } from '@plures/praxis/examples/advanced-todo';
|
|
179
|
+
|
|
180
|
+
const engine = createTodoEngine();
|
|
181
|
+
const { context, dispatch } = usePraxisEngine(engine);
|
|
182
|
+
|
|
183
|
+
let text = '';
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<input bind:value={text} />
|
|
187
|
+
<button onclick={() => dispatch([AddTodo.create({ text })])}>
|
|
188
|
+
Add
|
|
189
|
+
</button>
|
|
190
|
+
|
|
191
|
+
<ul>
|
|
192
|
+
{#each context.todos as todo}
|
|
193
|
+
<li>{todo.text}</li>
|
|
194
|
+
{/each}
|
|
195
|
+
</ul>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Keyboard Shortcuts
|
|
199
|
+
|
|
200
|
+
- **Ctrl+Z** / **Cmd+Z**: Undo
|
|
201
|
+
- **Ctrl+Y** / **Cmd+Y**: Redo
|
|
202
|
+
- **Ctrl+D** / **Cmd+D**: Toggle debugger
|
|
203
|
+
- **Enter**: Add todo (when input is focused)
|
|
204
|
+
|
|
205
|
+
## Key Concepts
|
|
206
|
+
|
|
207
|
+
### 1. Immutable State Updates
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// ❌ Don't mutate directly (outside rules)
|
|
211
|
+
context.todos.push(newTodo);
|
|
212
|
+
|
|
213
|
+
// ✅ Do dispatch events
|
|
214
|
+
dispatch([AddTodo.create({ text: 'New todo' })]);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 2. Pure Rules
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const addTodoRule = defineRule({
|
|
221
|
+
id: 'todo.add',
|
|
222
|
+
impl: (state, events) => {
|
|
223
|
+
// ✅ Pure function
|
|
224
|
+
// - Deterministic
|
|
225
|
+
// - No side effects
|
|
226
|
+
// - Testable
|
|
227
|
+
const event = findEvent(events, AddTodo);
|
|
228
|
+
if (!event) return [];
|
|
229
|
+
|
|
230
|
+
state.context.todos.push({ /* ... */ });
|
|
231
|
+
return [TodoAdded.create({ /* ... */ })];
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 3. Facts as History
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Facts are immutable records of what happened
|
|
240
|
+
const facts = engine.getFacts();
|
|
241
|
+
|
|
242
|
+
// Find specific facts
|
|
243
|
+
const added = facts.filter(f => f.tag === 'TodoAdded');
|
|
244
|
+
const toggled = facts.filter(f => f.tag === 'TodoToggled');
|
|
245
|
+
|
|
246
|
+
// Facts enable:
|
|
247
|
+
// - Audit trails
|
|
248
|
+
// - Event sourcing
|
|
249
|
+
// - Time-travel debugging
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 4. Reactive Derivations
|
|
253
|
+
|
|
254
|
+
```svelte
|
|
255
|
+
<script>
|
|
256
|
+
// Svelte automatically tracks dependencies
|
|
257
|
+
$: filteredTodos = getFilteredTodos(context.todos, context.filter);
|
|
258
|
+
$: stats = getStats(context.todos);
|
|
259
|
+
$: hasCompleted = stats.completed > 0;
|
|
260
|
+
</script>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### 5. History State Pattern
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Enable history tracking
|
|
267
|
+
const { undo, redo, snapshots, goToSnapshot } = usePraxisEngine(engine, {
|
|
268
|
+
enableHistory: true,
|
|
269
|
+
maxHistorySize: 50,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Navigate history
|
|
273
|
+
undo(); // Go back
|
|
274
|
+
redo(); // Go forward
|
|
275
|
+
goToSnapshot(5); // Jump to specific snapshot
|
|
276
|
+
console.log(snapshots); // View all snapshots
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Testing
|
|
280
|
+
|
|
281
|
+
The engine can be tested independently of Svelte:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { createTodoEngine, AddTodo, ToggleTodo } from './index';
|
|
285
|
+
|
|
286
|
+
describe('Todo Engine', () => {
|
|
287
|
+
it('should add todos', () => {
|
|
288
|
+
const engine = createTodoEngine();
|
|
289
|
+
engine.step([AddTodo.create({ text: 'Test todo' })]);
|
|
290
|
+
|
|
291
|
+
const context = engine.getContext();
|
|
292
|
+
expect(context.todos.length).toBe(1);
|
|
293
|
+
expect(context.todos[0].text).toBe('Test todo');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should toggle todos', () => {
|
|
297
|
+
const engine = createTodoEngine();
|
|
298
|
+
engine.step([AddTodo.create({ text: 'Test' })]);
|
|
299
|
+
|
|
300
|
+
const context = engine.getContext();
|
|
301
|
+
const todoId = context.todos[0].id;
|
|
302
|
+
|
|
303
|
+
engine.step([ToggleTodo.create({ id: todoId })]);
|
|
304
|
+
expect(engine.getContext().todos[0].completed).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Performance Tips
|
|
310
|
+
|
|
311
|
+
1. **Batch Events**: Dispatch multiple events at once
|
|
312
|
+
```typescript
|
|
313
|
+
dispatch([
|
|
314
|
+
AddTodo.create({ text: 'Todo 1' }),
|
|
315
|
+
AddTodo.create({ text: 'Todo 2' }),
|
|
316
|
+
AddTodo.create({ text: 'Todo 3' }),
|
|
317
|
+
], 'Batch Add');
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
2. **Limit History**: Adjust history size based on needs
|
|
321
|
+
```typescript
|
|
322
|
+
usePraxisEngine(engine, { maxHistorySize: 20 });
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
3. **Selective Subscriptions**: Use derived stores for specific values
|
|
326
|
+
```typescript
|
|
327
|
+
const count = createDerivedStore(engine, ctx => ctx.todos.length);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Comparison with Other Libraries
|
|
331
|
+
|
|
332
|
+
### vs. Redux + Redux Toolkit
|
|
333
|
+
|
|
334
|
+
| Feature | Redux + RTK | Praxis |
|
|
335
|
+
|---------|-------------|--------|
|
|
336
|
+
| State Updates | Reducers | Rules |
|
|
337
|
+
| Side Effects | Thunks/Sagas | Actors |
|
|
338
|
+
| Time-Travel | DevTools | Built-in |
|
|
339
|
+
| TypeScript | Good | Excellent |
|
|
340
|
+
| Learning Curve | Medium | Low |
|
|
341
|
+
|
|
342
|
+
### vs. XState
|
|
343
|
+
|
|
344
|
+
| Feature | XState | Praxis |
|
|
345
|
+
|---------|--------|--------|
|
|
346
|
+
| State Machines | Native | Engine-based |
|
|
347
|
+
| Svelte Integration | @xstate/svelte | @plures/praxis/svelte |
|
|
348
|
+
| History States | Built-in | Pattern-based |
|
|
349
|
+
| Visual Tools | Stately | State-Docs (planned) |
|
|
350
|
+
| Learning Curve | High | Medium |
|
|
351
|
+
|
|
352
|
+
### vs. Svelte Stores
|
|
353
|
+
|
|
354
|
+
| Feature | Svelte Stores | Praxis |
|
|
355
|
+
|---------|---------------|--------|
|
|
356
|
+
| Simplicity | Very High | High |
|
|
357
|
+
| Structure | Manual | Built-in |
|
|
358
|
+
| Time-Travel | Manual | Built-in |
|
|
359
|
+
| Rules/Logic | Manual | Built-in |
|
|
360
|
+
| TypeScript | Good | Excellent |
|
|
361
|
+
|
|
362
|
+
## Next Steps
|
|
363
|
+
|
|
364
|
+
- Explore [History State Pattern Guide](../../docs/guides/history-state-pattern.md)
|
|
365
|
+
- Read [Svelte Integration Guide](../../docs/guides/svelte-integration.md)
|
|
366
|
+
- Check [Parallel State Pattern](../../docs/guides/parallel-state-pattern.md)
|
|
367
|
+
- View [More Examples](../)
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Svelte 5 Integration Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the new Svelte 5 runes support with history state pattern.
|
|
5
|
+
* This example shows a todo app with undo/redo and time-travel debugging.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createPraxisEngine,
|
|
10
|
+
PraxisRegistry,
|
|
11
|
+
defineFact,
|
|
12
|
+
defineEvent,
|
|
13
|
+
defineRule,
|
|
14
|
+
findEvent,
|
|
15
|
+
} from '../../index.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Context & Domain Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
interface TodoItem {
|
|
22
|
+
id: string;
|
|
23
|
+
text: string;
|
|
24
|
+
completed: boolean;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TodoContext {
|
|
29
|
+
todos: TodoItem[];
|
|
30
|
+
filter: 'all' | 'active' | 'completed';
|
|
31
|
+
nextId: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Facts
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
const TodoAdded = defineFact<'TodoAdded', { id: string; text: string }>('TodoAdded');
|
|
39
|
+
const TodoToggled = defineFact<'TodoToggled', { id: string }>('TodoToggled');
|
|
40
|
+
const TodoRemoved = defineFact<'TodoRemoved', { id: string }>('TodoRemoved');
|
|
41
|
+
const FilterChanged = defineFact<'FilterChanged', { filter: string }>('FilterChanged');
|
|
42
|
+
const AllCompleted = defineFact<'AllCompleted', {}>('AllCompleted');
|
|
43
|
+
const CompletedCleared = defineFact<'CompletedCleared', {}>('CompletedCleared');
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Events
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
const AddTodo = defineEvent<'ADD_TODO', { text: string }>('ADD_TODO');
|
|
50
|
+
const ToggleTodo = defineEvent<'TOGGLE_TODO', { id: string }>('TOGGLE_TODO');
|
|
51
|
+
const RemoveTodo = defineEvent<'REMOVE_TODO', { id: string }>('REMOVE_TODO');
|
|
52
|
+
const SetFilter = defineEvent<'SET_FILTER', { filter: 'all' | 'active' | 'completed' }>(
|
|
53
|
+
'SET_FILTER'
|
|
54
|
+
);
|
|
55
|
+
const CompleteAll = defineEvent<'COMPLETE_ALL', {}>('COMPLETE_ALL');
|
|
56
|
+
const ClearCompleted = defineEvent<'CLEAR_COMPLETED', {}>('CLEAR_COMPLETED');
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Rules
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
const addTodoRule = defineRule<TodoContext>({
|
|
63
|
+
id: 'todo.add',
|
|
64
|
+
description: 'Add a new todo item',
|
|
65
|
+
impl: (state, events) => {
|
|
66
|
+
const event = findEvent(events, AddTodo);
|
|
67
|
+
if (!event || !event.payload.text.trim()) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const id = `todo-${state.context.nextId}`;
|
|
72
|
+
const todo: TodoItem = {
|
|
73
|
+
id,
|
|
74
|
+
text: event.payload.text.trim(),
|
|
75
|
+
completed: false,
|
|
76
|
+
createdAt: Date.now(),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
state.context.todos.push(todo);
|
|
80
|
+
state.context.nextId++;
|
|
81
|
+
|
|
82
|
+
return [TodoAdded.create({ id, text: todo.text })];
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const toggleTodoRule = defineRule<TodoContext>({
|
|
87
|
+
id: 'todo.toggle',
|
|
88
|
+
description: 'Toggle todo completion status',
|
|
89
|
+
impl: (state, events) => {
|
|
90
|
+
const event = findEvent(events, ToggleTodo);
|
|
91
|
+
if (!event) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const todo = state.context.todos.find((t) => t.id === event.payload.id);
|
|
96
|
+
if (todo) {
|
|
97
|
+
todo.completed = !todo.completed;
|
|
98
|
+
return [TodoToggled.create({ id: todo.id })];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return [];
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const removeTodoRule = defineRule<TodoContext>({
|
|
106
|
+
id: 'todo.remove',
|
|
107
|
+
description: 'Remove a todo item',
|
|
108
|
+
impl: (state, events) => {
|
|
109
|
+
const event = findEvent(events, RemoveTodo);
|
|
110
|
+
if (!event) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const index = state.context.todos.findIndex((t) => t.id === event.payload.id);
|
|
115
|
+
if (index !== -1) {
|
|
116
|
+
state.context.todos.splice(index, 1);
|
|
117
|
+
return [TodoRemoved.create({ id: event.payload.id })];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [];
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const setFilterRule = defineRule<TodoContext>({
|
|
125
|
+
id: 'todo.setFilter',
|
|
126
|
+
description: 'Change the filter',
|
|
127
|
+
impl: (state, events) => {
|
|
128
|
+
const event = findEvent(events, SetFilter);
|
|
129
|
+
if (!event) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
state.context.filter = event.payload.filter;
|
|
134
|
+
return [FilterChanged.create({ filter: event.payload.filter })];
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const completeAllRule = defineRule<TodoContext>({
|
|
139
|
+
id: 'todo.completeAll',
|
|
140
|
+
description: 'Mark all todos as completed',
|
|
141
|
+
impl: (state, events) => {
|
|
142
|
+
const event = findEvent(events, CompleteAll);
|
|
143
|
+
if (!event) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
state.context.todos.forEach((todo) => {
|
|
148
|
+
todo.completed = true;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return [AllCompleted.create({})];
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const clearCompletedRule = defineRule<TodoContext>({
|
|
156
|
+
id: 'todo.clearCompleted',
|
|
157
|
+
description: 'Remove all completed todos',
|
|
158
|
+
impl: (state, events) => {
|
|
159
|
+
const event = findEvent(events, ClearCompleted);
|
|
160
|
+
if (!event) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const beforeCount = state.context.todos.length;
|
|
165
|
+
state.context.todos = state.context.todos.filter((t) => !t.completed);
|
|
166
|
+
const afterCount = state.context.todos.length;
|
|
167
|
+
|
|
168
|
+
if (beforeCount !== afterCount) {
|
|
169
|
+
return [CompletedCleared.create({})];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [];
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Engine Factory
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
export function createTodoEngine() {
|
|
181
|
+
const registry = new PraxisRegistry<TodoContext>();
|
|
182
|
+
|
|
183
|
+
registry.registerRule(addTodoRule);
|
|
184
|
+
registry.registerRule(toggleTodoRule);
|
|
185
|
+
registry.registerRule(removeTodoRule);
|
|
186
|
+
registry.registerRule(setFilterRule);
|
|
187
|
+
registry.registerRule(completeAllRule);
|
|
188
|
+
registry.registerRule(clearCompletedRule);
|
|
189
|
+
|
|
190
|
+
return createPraxisEngine<TodoContext>({
|
|
191
|
+
initialContext: {
|
|
192
|
+
todos: [],
|
|
193
|
+
filter: 'all',
|
|
194
|
+
nextId: 1,
|
|
195
|
+
},
|
|
196
|
+
registry,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Helper Functions for Svelte
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
export function getFilteredTodos(todos: TodoItem[], filter: string): TodoItem[] {
|
|
205
|
+
switch (filter) {
|
|
206
|
+
case 'active':
|
|
207
|
+
return todos.filter((t) => !t.completed);
|
|
208
|
+
case 'completed':
|
|
209
|
+
return todos.filter((t) => t.completed);
|
|
210
|
+
default:
|
|
211
|
+
return todos;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getStats(todos: TodoItem[]) {
|
|
216
|
+
const total = todos.length;
|
|
217
|
+
const active = todos.filter((t) => !t.completed).length;
|
|
218
|
+
const completed = todos.filter((t) => t.completed).length;
|
|
219
|
+
|
|
220
|
+
return { total, active, completed };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Example Usage (non-Svelte demonstration)
|
|
225
|
+
// ============================================================================
|
|
226
|
+
|
|
227
|
+
function runExample() {
|
|
228
|
+
console.log('=== Advanced Todo Example ===\n');
|
|
229
|
+
|
|
230
|
+
const engine = createTodoEngine();
|
|
231
|
+
|
|
232
|
+
// Add some todos
|
|
233
|
+
console.log('1. Adding todos:');
|
|
234
|
+
engine.step([AddTodo.create({ text: 'Learn Praxis' })]);
|
|
235
|
+
engine.step([AddTodo.create({ text: 'Build awesome app' })]);
|
|
236
|
+
engine.step([AddTodo.create({ text: 'Deploy to production' })]);
|
|
237
|
+
|
|
238
|
+
const context1 = engine.getContext();
|
|
239
|
+
console.log(` Total todos: ${context1.todos.length}`);
|
|
240
|
+
context1.todos.forEach((todo) => {
|
|
241
|
+
console.log(` - ${todo.text}`);
|
|
242
|
+
});
|
|
243
|
+
console.log();
|
|
244
|
+
|
|
245
|
+
// Toggle a todo
|
|
246
|
+
console.log('2. Completing first todo:');
|
|
247
|
+
const firstId = context1.todos[0].id;
|
|
248
|
+
engine.step([ToggleTodo.create({ id: firstId })]);
|
|
249
|
+
|
|
250
|
+
const context2 = engine.getContext();
|
|
251
|
+
const stats = getStats(context2.todos);
|
|
252
|
+
console.log(` Active: ${stats.active}, Completed: ${stats.completed}`);
|
|
253
|
+
console.log();
|
|
254
|
+
|
|
255
|
+
// Change filter
|
|
256
|
+
console.log('3. Filtering to active todos:');
|
|
257
|
+
engine.step([SetFilter.create({ filter: 'active' })])
|
|
258
|
+
|
|
259
|
+
const context3 = engine.getContext();
|
|
260
|
+
const filtered = getFilteredTodos(context3.todos, context3.filter);
|
|
261
|
+
console.log(` Filter: ${context3.filter}`);
|
|
262
|
+
console.log(` Showing ${filtered.length} todos:`);
|
|
263
|
+
filtered.forEach((todo) => {
|
|
264
|
+
console.log(` - ${todo.text}`);
|
|
265
|
+
});
|
|
266
|
+
console.log();
|
|
267
|
+
|
|
268
|
+
// Complete all
|
|
269
|
+
console.log('4. Completing all todos:');
|
|
270
|
+
engine.step([CompleteAll.create({})]);
|
|
271
|
+
|
|
272
|
+
const context4 = engine.getContext();
|
|
273
|
+
const stats4 = getStats(context4.todos);
|
|
274
|
+
console.log(` All completed: ${stats4.completed}/${stats4.total}`);
|
|
275
|
+
console.log();
|
|
276
|
+
|
|
277
|
+
// Clear completed
|
|
278
|
+
console.log('5. Clearing completed todos:');
|
|
279
|
+
engine.step([ClearCompleted.create({})]);
|
|
280
|
+
|
|
281
|
+
const context5 = engine.getContext();
|
|
282
|
+
console.log(` Remaining todos: ${context5.todos.length}`);
|
|
283
|
+
console.log();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Run example if executed directly
|
|
287
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
288
|
+
runExample();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export {
|
|
292
|
+
// Types
|
|
293
|
+
type TodoItem,
|
|
294
|
+
type TodoContext,
|
|
295
|
+
// Events
|
|
296
|
+
AddTodo,
|
|
297
|
+
ToggleTodo,
|
|
298
|
+
RemoveTodo,
|
|
299
|
+
SetFilter,
|
|
300
|
+
CompleteAll,
|
|
301
|
+
ClearCompleted,
|
|
302
|
+
// Facts
|
|
303
|
+
TodoAdded,
|
|
304
|
+
TodoToggled,
|
|
305
|
+
TodoRemoved,
|
|
306
|
+
FilterChanged,
|
|
307
|
+
AllCompleted,
|
|
308
|
+
CompletedCleared,
|
|
309
|
+
};
|