@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,618 @@
|
|
|
1
|
+
# History State Pattern in Praxis
|
|
2
|
+
|
|
3
|
+
This guide explains how to implement and use the history state pattern in Praxis, similar to XState's history states.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The history state pattern allows you to track and navigate through state transitions in your application. This is particularly useful for:
|
|
8
|
+
|
|
9
|
+
- **Undo/Redo functionality**: Let users reverse actions
|
|
10
|
+
- **Time-travel debugging**: Inspect and navigate application state history
|
|
11
|
+
- **Authentication flows**: Return to previous states after authentication
|
|
12
|
+
- **Complex workflows**: Remember where users were before interruptions
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
1. [Basic History Tracking](#basic-history-tracking)
|
|
17
|
+
2. [Using History State Manager](#using-history-state-manager)
|
|
18
|
+
3. [History Engine Wrapper](#history-engine-wrapper)
|
|
19
|
+
4. [Svelte 5 Integration](#svelte-5-integration)
|
|
20
|
+
5. [Authentication Flow Example](#authentication-flow-example)
|
|
21
|
+
6. [Best Practices](#best-practices)
|
|
22
|
+
|
|
23
|
+
## Basic History Tracking
|
|
24
|
+
|
|
25
|
+
The simplest way to track history is using the `HistoryStateManager`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { HistoryStateManager } from '@plures/praxis/svelte';
|
|
29
|
+
|
|
30
|
+
// Create a history manager with max 50 entries
|
|
31
|
+
const history = new HistoryStateManager<MyContext>(50);
|
|
32
|
+
|
|
33
|
+
// Record state changes
|
|
34
|
+
history.record(engine.getState(), events, 'Login Action');
|
|
35
|
+
|
|
36
|
+
// Navigate history
|
|
37
|
+
if (history.canGoBack()) {
|
|
38
|
+
const previousState = history.back();
|
|
39
|
+
console.log('Previous state:', previousState);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (history.canGoForward()) {
|
|
43
|
+
const nextState = history.forward();
|
|
44
|
+
console.log('Next state:', nextState);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get all history
|
|
48
|
+
const allHistory = history.getHistory();
|
|
49
|
+
console.log(`Total history entries: ${allHistory.length}`);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Using History State Manager
|
|
53
|
+
|
|
54
|
+
The `HistoryStateManager` provides full control over state history:
|
|
55
|
+
|
|
56
|
+
### Recording History
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { createPraxisEngine, HistoryStateManager } from '@plures/praxis';
|
|
60
|
+
|
|
61
|
+
interface AppContext {
|
|
62
|
+
user: string | null;
|
|
63
|
+
page: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const engine = createPraxisEngine<AppContext>({ /* ... */ });
|
|
67
|
+
const history = new HistoryStateManager<AppContext>(100);
|
|
68
|
+
|
|
69
|
+
// Record initial state
|
|
70
|
+
history.record(engine.getState(), [], 'Initial State');
|
|
71
|
+
|
|
72
|
+
// Process events and record
|
|
73
|
+
const events = [Login.create({ username: 'alice' })];
|
|
74
|
+
engine.step(events);
|
|
75
|
+
history.record(engine.getState(), events, 'User Login');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Navigating History
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Go back one step
|
|
82
|
+
const previousEntry = history.back();
|
|
83
|
+
if (previousEntry) {
|
|
84
|
+
console.log('Moved to:', previousEntry.label);
|
|
85
|
+
console.log('State:', previousEntry.state);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Go forward one step
|
|
89
|
+
const nextEntry = history.forward();
|
|
90
|
+
|
|
91
|
+
// Jump to specific point
|
|
92
|
+
const entry = history.goTo(5);
|
|
93
|
+
|
|
94
|
+
// Check navigation availability
|
|
95
|
+
console.log('Can undo:', history.canGoBack());
|
|
96
|
+
console.log('Can redo:', history.canGoForward());
|
|
97
|
+
console.log('Current position:', history.getCurrentIndex());
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Inspecting History
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Get current state
|
|
104
|
+
const current = history.current();
|
|
105
|
+
if (current) {
|
|
106
|
+
console.log('Current:', current.label);
|
|
107
|
+
console.log('Timestamp:', new Date(current.timestamp));
|
|
108
|
+
console.log('Events:', current.events);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get all history entries
|
|
112
|
+
const allEntries = history.getHistory();
|
|
113
|
+
allEntries.forEach((entry, index) => {
|
|
114
|
+
console.log(`${index}: ${entry.label} at ${new Date(entry.timestamp)}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Clear history
|
|
118
|
+
history.clear();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## History Engine Wrapper
|
|
122
|
+
|
|
123
|
+
For automatic history tracking, use `createHistoryEngine`:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { createPraxisEngine, createHistoryEngine } from '@plures/praxis/svelte';
|
|
127
|
+
|
|
128
|
+
// Create base engine
|
|
129
|
+
const baseEngine = createPraxisEngine<AppContext>({
|
|
130
|
+
initialContext: { user: null, page: 'home' },
|
|
131
|
+
registry,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Wrap with history tracking
|
|
135
|
+
const historyEngine = createHistoryEngine(baseEngine, {
|
|
136
|
+
maxHistorySize: 50,
|
|
137
|
+
initialLabel: 'App Started',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Use like normal engine, but with history
|
|
141
|
+
historyEngine.dispatch([Login.create({ username: 'alice' })], 'User Login');
|
|
142
|
+
|
|
143
|
+
// Undo/redo
|
|
144
|
+
if (historyEngine.canUndo()) {
|
|
145
|
+
historyEngine.undo();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (historyEngine.canRedo()) {
|
|
149
|
+
historyEngine.redo();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Navigate to specific point
|
|
153
|
+
historyEngine.goToHistory(3);
|
|
154
|
+
|
|
155
|
+
// Inspect history
|
|
156
|
+
const history = historyEngine.getHistory();
|
|
157
|
+
history.forEach((entry) => {
|
|
158
|
+
console.log(`${entry.label}: ${entry.events.length} events`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Clear history if needed
|
|
162
|
+
historyEngine.clearHistory();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Svelte 5 Integration
|
|
166
|
+
|
|
167
|
+
Praxis provides first-class Svelte 5 support with runes:
|
|
168
|
+
|
|
169
|
+
### Using `usePraxisEngine` with History
|
|
170
|
+
|
|
171
|
+
```svelte
|
|
172
|
+
<script lang="ts">
|
|
173
|
+
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
174
|
+
import { createMyEngine, Login, Logout } from './my-engine';
|
|
175
|
+
|
|
176
|
+
const engine = createMyEngine();
|
|
177
|
+
const {
|
|
178
|
+
context,
|
|
179
|
+
dispatch,
|
|
180
|
+
undo,
|
|
181
|
+
redo,
|
|
182
|
+
canUndo,
|
|
183
|
+
canRedo,
|
|
184
|
+
snapshots,
|
|
185
|
+
historyIndex
|
|
186
|
+
} = usePraxisEngine(engine, {
|
|
187
|
+
enableHistory: true,
|
|
188
|
+
maxHistorySize: 50
|
|
189
|
+
});
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<div class="app">
|
|
193
|
+
<header>
|
|
194
|
+
<h1>Welcome {context.user?.name || 'Guest'}</h1>
|
|
195
|
+
<p>History: {historyIndex + 1} / {snapshots.length}</p>
|
|
196
|
+
</header>
|
|
197
|
+
|
|
198
|
+
<main>
|
|
199
|
+
{#if !context.user}
|
|
200
|
+
<button onclick={() => dispatch([Login.create({ username: 'alice' })])}>
|
|
201
|
+
Login
|
|
202
|
+
</button>
|
|
203
|
+
{:else}
|
|
204
|
+
<button onclick={() => dispatch([Logout.create({})])}>
|
|
205
|
+
Logout
|
|
206
|
+
</button>
|
|
207
|
+
{/if}
|
|
208
|
+
</main>
|
|
209
|
+
|
|
210
|
+
<footer>
|
|
211
|
+
<button onclick={undo} disabled={!canUndo}>
|
|
212
|
+
Undo
|
|
213
|
+
</button>
|
|
214
|
+
<button onclick={redo} disabled={!canRedo}>
|
|
215
|
+
Redo
|
|
216
|
+
</button>
|
|
217
|
+
</footer>
|
|
218
|
+
</div>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Time-Travel Debugging
|
|
222
|
+
|
|
223
|
+
```svelte
|
|
224
|
+
<script lang="ts">
|
|
225
|
+
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
226
|
+
import { createMyEngine } from './my-engine';
|
|
227
|
+
|
|
228
|
+
const engine = createMyEngine();
|
|
229
|
+
const { context, snapshots, goToSnapshot, historyIndex } =
|
|
230
|
+
usePraxisEngine(engine, { enableHistory: true });
|
|
231
|
+
</script>
|
|
232
|
+
|
|
233
|
+
<div class="debugger">
|
|
234
|
+
<h2>Time-Travel Debugger</h2>
|
|
235
|
+
|
|
236
|
+
<div class="timeline">
|
|
237
|
+
{#each snapshots as snapshot, index}
|
|
238
|
+
<button
|
|
239
|
+
class:active={index === historyIndex}
|
|
240
|
+
onclick={() => goToSnapshot(index)}
|
|
241
|
+
>
|
|
242
|
+
{new Date(snapshot.timestamp).toLocaleTimeString()}
|
|
243
|
+
{#if snapshot.events.length > 0}
|
|
244
|
+
({snapshot.events.length} events)
|
|
245
|
+
{/if}
|
|
246
|
+
</button>
|
|
247
|
+
{/each}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="state-view">
|
|
251
|
+
<h3>Current State</h3>
|
|
252
|
+
<pre>{JSON.stringify(context, null, 2)}</pre>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Authentication Flow Example
|
|
258
|
+
|
|
259
|
+
Here's a complete example showing how to use history states for auth flows:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// auth-engine.ts
|
|
263
|
+
import {
|
|
264
|
+
createPraxisEngine,
|
|
265
|
+
PraxisRegistry,
|
|
266
|
+
defineFact,
|
|
267
|
+
defineEvent,
|
|
268
|
+
defineRule,
|
|
269
|
+
findEvent,
|
|
270
|
+
} from '@plures/praxis';
|
|
271
|
+
|
|
272
|
+
interface AuthContext {
|
|
273
|
+
user: { id: string; name: string } | null;
|
|
274
|
+
previousPage: string | null;
|
|
275
|
+
currentPage: string;
|
|
276
|
+
attemptedSecurePage: string | null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Define facts
|
|
280
|
+
const UserAuthenticated = defineFact<'UserAuthenticated', {
|
|
281
|
+
userId: string;
|
|
282
|
+
name: string
|
|
283
|
+
}>('UserAuthenticated');
|
|
284
|
+
|
|
285
|
+
const NavigatedToPage = defineFact<'NavigatedToPage', {
|
|
286
|
+
page: string
|
|
287
|
+
}>('NavigatedToPage');
|
|
288
|
+
|
|
289
|
+
// Define events
|
|
290
|
+
const Login = defineEvent<'LOGIN', {
|
|
291
|
+
username: string;
|
|
292
|
+
password: string
|
|
293
|
+
}>('LOGIN');
|
|
294
|
+
|
|
295
|
+
const Logout = defineEvent<'LOGOUT', {}>('LOGOUT');
|
|
296
|
+
|
|
297
|
+
const NavigateTo = defineEvent<'NAVIGATE_TO', {
|
|
298
|
+
page: string;
|
|
299
|
+
requiresAuth?: boolean;
|
|
300
|
+
}>('NAVIGATE_TO');
|
|
301
|
+
|
|
302
|
+
// Define rules
|
|
303
|
+
const loginRule = defineRule<AuthContext>({
|
|
304
|
+
id: 'auth.login',
|
|
305
|
+
description: 'Authenticate user',
|
|
306
|
+
impl: (state, events) => {
|
|
307
|
+
const loginEvent = findEvent(events, Login);
|
|
308
|
+
if (!loginEvent) return [];
|
|
309
|
+
|
|
310
|
+
// Simulate authentication
|
|
311
|
+
const { username } = loginEvent.payload;
|
|
312
|
+
state.context.user = { id: username, name: username };
|
|
313
|
+
|
|
314
|
+
// Return to attempted secure page if exists
|
|
315
|
+
if (state.context.attemptedSecurePage) {
|
|
316
|
+
state.context.currentPage = state.context.attemptedSecurePage;
|
|
317
|
+
state.context.attemptedSecurePage = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return [
|
|
321
|
+
UserAuthenticated.create({ userId: username, name: username })
|
|
322
|
+
];
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const logoutRule = defineRule<AuthContext>({
|
|
327
|
+
id: 'auth.logout',
|
|
328
|
+
description: 'Log out user',
|
|
329
|
+
impl: (state, events) => {
|
|
330
|
+
const logoutEvent = findEvent(events, Logout);
|
|
331
|
+
if (!logoutEvent) return [];
|
|
332
|
+
|
|
333
|
+
state.context.previousPage = state.context.currentPage;
|
|
334
|
+
state.context.user = null;
|
|
335
|
+
state.context.currentPage = 'login';
|
|
336
|
+
|
|
337
|
+
return [];
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const navigationRule = defineRule<AuthContext>({
|
|
342
|
+
id: 'navigation.navigate',
|
|
343
|
+
description: 'Handle navigation',
|
|
344
|
+
impl: (state, events) => {
|
|
345
|
+
const navEvent = findEvent(events, NavigateTo);
|
|
346
|
+
if (!navEvent) return [];
|
|
347
|
+
|
|
348
|
+
const { page, requiresAuth } = navEvent.payload;
|
|
349
|
+
|
|
350
|
+
// Check if page requires authentication
|
|
351
|
+
if (requiresAuth && !state.context.user) {
|
|
352
|
+
state.context.attemptedSecurePage = page;
|
|
353
|
+
state.context.currentPage = 'login';
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
state.context.previousPage = state.context.currentPage;
|
|
358
|
+
state.context.currentPage = page;
|
|
359
|
+
|
|
360
|
+
return [NavigatedToPage.create({ page })];
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
export function createAuthEngine() {
|
|
365
|
+
const registry = new PraxisRegistry<AuthContext>();
|
|
366
|
+
registry.registerRule(loginRule);
|
|
367
|
+
registry.registerRule(logoutRule);
|
|
368
|
+
registry.registerRule(navigationRule);
|
|
369
|
+
|
|
370
|
+
return createPraxisEngine<AuthContext>({
|
|
371
|
+
initialContext: {
|
|
372
|
+
user: null,
|
|
373
|
+
previousPage: null,
|
|
374
|
+
currentPage: 'home',
|
|
375
|
+
attemptedSecurePage: null,
|
|
376
|
+
},
|
|
377
|
+
registry,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { Login, Logout, NavigateTo };
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Using the Auth Engine with History
|
|
385
|
+
|
|
386
|
+
```svelte
|
|
387
|
+
<script lang="ts">
|
|
388
|
+
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
389
|
+
import { createAuthEngine, Login, Logout, NavigateTo } from './auth-engine';
|
|
390
|
+
|
|
391
|
+
const engine = createAuthEngine();
|
|
392
|
+
const { context, dispatch, undo, canUndo } = usePraxisEngine(engine, {
|
|
393
|
+
enableHistory: true
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
function login() {
|
|
397
|
+
dispatch([Login.create({
|
|
398
|
+
username: 'alice',
|
|
399
|
+
password: 'secret123'
|
|
400
|
+
})], 'User Login');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function logout() {
|
|
404
|
+
dispatch([Logout.create({})], 'User Logout');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function goToProfile() {
|
|
408
|
+
dispatch([NavigateTo.create({
|
|
409
|
+
page: 'profile',
|
|
410
|
+
requiresAuth: true
|
|
411
|
+
})], 'Navigate to Profile');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function returnToPrevious() {
|
|
415
|
+
if (context.previousPage) {
|
|
416
|
+
dispatch([NavigateTo.create({
|
|
417
|
+
page: context.previousPage
|
|
418
|
+
})], 'Return to Previous Page');
|
|
419
|
+
} else if (canUndo) {
|
|
420
|
+
undo();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
</script>
|
|
424
|
+
|
|
425
|
+
<div class="auth-app">
|
|
426
|
+
<nav>
|
|
427
|
+
<button onclick={() => dispatch([NavigateTo.create({ page: 'home' })])}>
|
|
428
|
+
Home
|
|
429
|
+
</button>
|
|
430
|
+
<button onclick={goToProfile}>
|
|
431
|
+
Profile {context.user ? '' : '(requires login)'}
|
|
432
|
+
</button>
|
|
433
|
+
</nav>
|
|
434
|
+
|
|
435
|
+
<main>
|
|
436
|
+
{#if context.currentPage === 'login'}
|
|
437
|
+
<div class="login">
|
|
438
|
+
<h2>Please Login</h2>
|
|
439
|
+
{#if context.attemptedSecurePage}
|
|
440
|
+
<p>You need to login to access: {context.attemptedSecurePage}</p>
|
|
441
|
+
{/if}
|
|
442
|
+
<button onclick={login}>Login as Alice</button>
|
|
443
|
+
</div>
|
|
444
|
+
{:else if context.currentPage === 'profile'}
|
|
445
|
+
<div class="profile">
|
|
446
|
+
<h2>Profile</h2>
|
|
447
|
+
<p>Welcome, {context.user?.name}!</p>
|
|
448
|
+
<button onclick={logout}>Logout</button>
|
|
449
|
+
</div>
|
|
450
|
+
{:else}
|
|
451
|
+
<div class="home">
|
|
452
|
+
<h2>Home</h2>
|
|
453
|
+
{#if context.user}
|
|
454
|
+
<p>Logged in as: {context.user.name}</p>
|
|
455
|
+
<button onclick={logout}>Logout</button>
|
|
456
|
+
{:else}
|
|
457
|
+
<button onclick={login}>Login</button>
|
|
458
|
+
{/if}
|
|
459
|
+
</div>
|
|
460
|
+
{/if}
|
|
461
|
+
</main>
|
|
462
|
+
|
|
463
|
+
<footer>
|
|
464
|
+
{#if context.previousPage || canUndo}
|
|
465
|
+
<button onclick={returnToPrevious}>
|
|
466
|
+
← Back
|
|
467
|
+
</button>
|
|
468
|
+
{/if}
|
|
469
|
+
</footer>
|
|
470
|
+
</div>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Best Practices
|
|
474
|
+
|
|
475
|
+
### 1. Set Appropriate History Limits
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// For user-facing undo/redo: smaller limit
|
|
479
|
+
const userHistory = new HistoryStateManager<Context>(20);
|
|
480
|
+
|
|
481
|
+
// For debugging: larger limit
|
|
482
|
+
const debugHistory = new HistoryStateManager<Context>(100);
|
|
483
|
+
|
|
484
|
+
// For critical audit trails: very large or unlimited
|
|
485
|
+
const auditHistory = new HistoryStateManager<Context>(10000);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### 2. Label Important States
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
historyEngine.dispatch(
|
|
492
|
+
[PlaceOrder.create({ orderId: '123' })],
|
|
493
|
+
'Order Placed' // Clear label for debugging
|
|
494
|
+
);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### 3. Clear History When Appropriate
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// Clear on logout to protect privacy
|
|
501
|
+
function handleLogout() {
|
|
502
|
+
historyEngine.dispatch([Logout.create({})]);
|
|
503
|
+
historyEngine.clearHistory();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Clear when starting new workflow
|
|
507
|
+
function startNewProject() {
|
|
508
|
+
historyEngine.clearHistory();
|
|
509
|
+
historyEngine.dispatch([CreateProject.create({})],'New Project');
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### 4. Combine with Context State
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
interface AppContext {
|
|
517
|
+
// Application state
|
|
518
|
+
user: User | null;
|
|
519
|
+
|
|
520
|
+
// Built-in history tracking in context
|
|
521
|
+
navigationHistory: string[];
|
|
522
|
+
|
|
523
|
+
// Previous state for quick back
|
|
524
|
+
previousView: string | null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// You can use both engine history and context-based history
|
|
528
|
+
const navigationRule = defineRule<AppContext>({
|
|
529
|
+
id: 'nav.track',
|
|
530
|
+
impl: (state, events) => {
|
|
531
|
+
const nav = findEvent(events, Navigate);
|
|
532
|
+
if (!nav) return [];
|
|
533
|
+
|
|
534
|
+
state.context.previousView = state.context.currentView;
|
|
535
|
+
state.context.navigationHistory.push(nav.payload.page);
|
|
536
|
+
state.context.currentView = nav.payload.page;
|
|
537
|
+
|
|
538
|
+
return [];
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 5. Persist Critical History
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { HistoryStateManager } from '@plures/praxis/svelte';
|
|
547
|
+
|
|
548
|
+
// Save to localStorage
|
|
549
|
+
function saveHistory(history: HistoryStateManager<Context>) {
|
|
550
|
+
const entries = history.getHistory();
|
|
551
|
+
localStorage.setItem('app-history', JSON.stringify(entries));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Restore from localStorage
|
|
555
|
+
function loadHistory(history: HistoryStateManager<Context>) {
|
|
556
|
+
const saved = localStorage.getItem('app-history');
|
|
557
|
+
if (saved) {
|
|
558
|
+
const entries = JSON.parse(saved);
|
|
559
|
+
entries.forEach((entry: any) => {
|
|
560
|
+
history.record(entry.state, entry.events, entry.label);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### 6. Use History for Error Recovery
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
const errorRecoveryRule = defineRule<AppContext>({
|
|
570
|
+
id: 'error.recover',
|
|
571
|
+
impl: (state, events) => {
|
|
572
|
+
const error = findEvent(events, ErrorOccurred);
|
|
573
|
+
if (!error) return [];
|
|
574
|
+
|
|
575
|
+
// Log error with context
|
|
576
|
+
console.error('Error at state:', state.context);
|
|
577
|
+
|
|
578
|
+
// You can use history to diagnose or recover
|
|
579
|
+
// historyEngine.undo() to revert to last good state
|
|
580
|
+
|
|
581
|
+
return [ErrorLogged.create({ error: error.payload })];
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## Comparison with XState
|
|
587
|
+
|
|
588
|
+
| Feature | XState | Praxis |
|
|
589
|
+
|---------|--------|--------|
|
|
590
|
+
| **Built-in History States** | ✅ Native support | ✅ Pattern-based implementation |
|
|
591
|
+
| **History Types** | Shallow/Deep | Configurable via HistoryStateManager |
|
|
592
|
+
| **Time-Travel** | Via DevTools | ✅ Built into usePraxisEngine |
|
|
593
|
+
| **Undo/Redo** | Custom implementation | ✅ Built-in with createHistoryEngine |
|
|
594
|
+
| **Snapshot Support** | ✅ Via snapshot() | ✅ Via usePraxisEngine snapshots |
|
|
595
|
+
| **History Size Limits** | Manual | ✅ Automatic with maxHistorySize |
|
|
596
|
+
|
|
597
|
+
## Summary
|
|
598
|
+
|
|
599
|
+
The history state pattern in Praxis provides:
|
|
600
|
+
|
|
601
|
+
- ✅ **Flexible History Tracking**: Use `HistoryStateManager` for full control
|
|
602
|
+
- ✅ **Automatic History**: Wrap engines with `createHistoryEngine`
|
|
603
|
+
- ✅ **Svelte 5 Integration**: Native runes support with `usePraxisEngine`
|
|
604
|
+
- ✅ **Time-Travel Debugging**: Navigate and inspect state history
|
|
605
|
+
- ✅ **Undo/Redo**: Built-in support for user actions
|
|
606
|
+
- ✅ **Auth Flow Support**: Return to previous states after authentication
|
|
607
|
+
|
|
608
|
+
The pattern is designed to be:
|
|
609
|
+
- **Simple**: Easy to understand and use
|
|
610
|
+
- **Flexible**: Works with any Praxis engine
|
|
611
|
+
- **Performant**: Configurable history limits
|
|
612
|
+
- **Type-Safe**: Full TypeScript support
|
|
613
|
+
- **Framework-Agnostic**: Use with or without Svelte
|
|
614
|
+
|
|
615
|
+
For more examples, see:
|
|
616
|
+
- [Svelte Counter Example](/src/examples/svelte-counter/index.ts)
|
|
617
|
+
- [Auth Basic Example](/src/examples/auth-basic/index.ts)
|
|
618
|
+
- [Svelte Integration Tests](/src/__tests__/svelte-integration.test.ts)
|