@directive-run/knowledge 0.5.0 → 0.7.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/core/anti-patterns.md +10 -26
- package/core/core-patterns.md +1 -1
- package/core/history.md +344 -0
- package/core/multi-module.md +1 -1
- package/core/react-adapter.md +1 -1
- package/core/system-api.md +115 -1
- package/examples/ab-testing.ts +1 -1
- package/examples/ai-checkpoint.ts +1 -1
- package/examples/ai-guardrails.ts +1 -1
- package/examples/batch-resolver.ts +1 -1
- package/examples/checkers.ts +5 -2
- package/examples/contact-form.ts +1 -1
- package/examples/counter.ts +2 -1
- package/examples/debounce-constraints.ts +2 -10
- package/examples/error-boundaries.ts +1 -1
- package/examples/form-wizard.ts +1 -1
- package/examples/fraud-analysis.ts +2 -6
- package/examples/multi-module.ts +1 -0
- package/examples/newsletter.ts +1 -1
- package/examples/pagination.ts +1 -1
- package/examples/permissions.ts +1 -1
- package/examples/provider-routing.ts +1 -1
- package/examples/shopping-cart.ts +2 -5
- package/examples/sudoku.ts +8 -1
- package/examples/time-machine.ts +2 -1
- package/examples/url-sync.ts +1 -1
- package/package.json +3 -3
- package/core/time-travel.md +0 -238
package/core/anti-patterns.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
19 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
|
|
4
4
|
|
|
5
5
|
## 1. Unnecessary Type Casting on Facts/Derivations
|
|
6
6
|
|
|
@@ -155,23 +155,7 @@ const status = facts.auth.status;
|
|
|
155
155
|
const token = facts.auth.token;
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
## 11.
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
// WRONG – there is no builder pattern
|
|
162
|
-
const mod = module("counter")
|
|
163
|
-
.schema({ count: t.number() })
|
|
164
|
-
.build();
|
|
165
|
-
|
|
166
|
-
// CORRECT – use createModule with object syntax
|
|
167
|
-
const mod = createModule("counter", {
|
|
168
|
-
schema: {
|
|
169
|
-
facts: { count: t.number() },
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## 12. Returning Data from Resolvers
|
|
158
|
+
## 11. Returning Data from Resolvers
|
|
175
159
|
|
|
176
160
|
```typescript
|
|
177
161
|
// WRONG – resolvers return void, not data
|
|
@@ -188,7 +172,7 @@ resolve: async (req, context) => {
|
|
|
188
172
|
},
|
|
189
173
|
```
|
|
190
174
|
|
|
191
|
-
##
|
|
175
|
+
## 12. Async Logic in `init`
|
|
192
176
|
|
|
193
177
|
```typescript
|
|
194
178
|
// WRONG – init is synchronous, facts assignment only
|
|
@@ -211,7 +195,7 @@ constraints: {
|
|
|
211
195
|
},
|
|
212
196
|
```
|
|
213
197
|
|
|
214
|
-
##
|
|
198
|
+
## 13. Missing `settle()` After `start()`
|
|
215
199
|
|
|
216
200
|
```typescript
|
|
217
201
|
// WRONG – constraints fire on start, resolvers are async
|
|
@@ -224,7 +208,7 @@ await system.settle();
|
|
|
224
208
|
console.log(system.facts.data); // Resolved
|
|
225
209
|
```
|
|
226
210
|
|
|
227
|
-
##
|
|
211
|
+
## 14. Missing `crossModuleDeps` Declaration
|
|
228
212
|
|
|
229
213
|
```typescript
|
|
230
214
|
// WRONG – accessing auth facts without declaring dependency
|
|
@@ -251,7 +235,7 @@ const dataModule = createModule("data", {
|
|
|
251
235
|
});
|
|
252
236
|
```
|
|
253
237
|
|
|
254
|
-
##
|
|
238
|
+
## 15. String Literal for `require`
|
|
255
239
|
|
|
256
240
|
```typescript
|
|
257
241
|
// WRONG – require must be an object with type property
|
|
@@ -271,7 +255,7 @@ constraints: {
|
|
|
271
255
|
},
|
|
272
256
|
```
|
|
273
257
|
|
|
274
|
-
##
|
|
258
|
+
## 16. Passthrough Derivations
|
|
275
259
|
|
|
276
260
|
```typescript
|
|
277
261
|
// WRONG – derivation just returns a fact value unchanged
|
|
@@ -283,7 +267,7 @@ derive: {
|
|
|
283
267
|
// system.facts.count instead of system.derive.count
|
|
284
268
|
```
|
|
285
269
|
|
|
286
|
-
##
|
|
270
|
+
## 17. Deep Import Paths
|
|
287
271
|
|
|
288
272
|
```typescript
|
|
289
273
|
// WRONG – internal module paths are not public API
|
|
@@ -297,7 +281,7 @@ import { createModule, createSystem } from "@directive-run/core";
|
|
|
297
281
|
import { loggingPlugin } from "@directive-run/core/plugins";
|
|
298
282
|
```
|
|
299
283
|
|
|
300
|
-
##
|
|
284
|
+
## 18. Async `when()` Without `deps`
|
|
301
285
|
|
|
302
286
|
```typescript
|
|
303
287
|
// WRONG – async constraints need explicit deps for tracking
|
|
@@ -328,7 +312,7 @@ constraints: {
|
|
|
328
312
|
},
|
|
329
313
|
```
|
|
330
314
|
|
|
331
|
-
##
|
|
315
|
+
## 19. No Error Handling on Failing Resolvers
|
|
332
316
|
|
|
333
317
|
```typescript
|
|
334
318
|
// WRONG – unhandled errors crash the system
|
package/core/core-patterns.md
CHANGED
|
@@ -108,7 +108,7 @@ import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
|
|
|
108
108
|
const system = createSystem({
|
|
109
109
|
module: myModule,
|
|
110
110
|
plugins: [loggingPlugin(), devtoolsPlugin()],
|
|
111
|
-
|
|
111
|
+
history: { maxSnapshots: 100 },
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
// Multi-module – namespaced access: system.facts.auth.token
|
package/core/history.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# History & Snapshots
|
|
2
|
+
|
|
3
|
+
Directive records fact changes as snapshots, enabling undo/redo, replay, export/import, and changeset grouping.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "Should I enable history?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
What's the use case?
|
|
9
|
+
├── Debugging during development → Yes, enable with maxSnapshots cap
|
|
10
|
+
├── Production app → No, disable for performance
|
|
11
|
+
├── Bug reproduction → Enable, use export() to share
|
|
12
|
+
├── Testing → Usually no – use assertFact/assertDerivation instead
|
|
13
|
+
└── Demo / presentation → Yes, great for showing state changes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Enabling History
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createSystem } from "@directive-run/core";
|
|
20
|
+
|
|
21
|
+
const system = createSystem({
|
|
22
|
+
module: myModule,
|
|
23
|
+
history: {
|
|
24
|
+
maxSnapshots: 100, // Cap memory usage (default: 100)
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
History is disabled by default. When disabled, `system.history` is `null`. When enabled, snapshots are recorded automatically after every fact mutation.
|
|
30
|
+
|
|
31
|
+
## Filtering Snapshot Events
|
|
32
|
+
|
|
33
|
+
By default every event that changes facts creates a snapshot. Use `history.snapshotEvents` on your module to limit which events create snapshots — keeps undo/redo clean by excluding UI-only events like selection or timer ticks.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const game = createModule("game", {
|
|
37
|
+
schema: gameSchema,
|
|
38
|
+
|
|
39
|
+
// Only these events appear in undo/redo history.
|
|
40
|
+
// Omit to snapshot ALL events (the default).
|
|
41
|
+
history: {
|
|
42
|
+
snapshotEvents: [
|
|
43
|
+
"inputNumber",
|
|
44
|
+
"requestHint",
|
|
45
|
+
"newGame",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
events: {
|
|
50
|
+
tick: (facts) => { /* no snapshot */ },
|
|
51
|
+
selectCell: (facts, { index }) => { /* no snapshot */ },
|
|
52
|
+
inputNumber: (facts, { value }) => { /* creates snapshot */ },
|
|
53
|
+
requestHint: (facts) => { /* creates snapshot */ },
|
|
54
|
+
newGame: (facts, { difficulty }) => { /* creates snapshot */ },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Filtering by Module
|
|
60
|
+
|
|
61
|
+
In a multi-module system, control which modules create snapshots at the system level:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const system = createSystem({
|
|
65
|
+
modules: { ui: uiModule, game: gameModule },
|
|
66
|
+
history: {
|
|
67
|
+
maxSnapshots: 100,
|
|
68
|
+
snapshotModules: ["game"], // Only game events create snapshots
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Rules:**
|
|
74
|
+
- `snapshotEvents` omitted → all events snapshot (per module)
|
|
75
|
+
- `snapshotModules` omitted → all modules snapshot (per system)
|
|
76
|
+
- Both provided → they intersect (module must be in `snapshotModules` AND event in `snapshotEvents`)
|
|
77
|
+
- Direct fact mutations (`system.facts.x = 5`) always snapshot regardless of filtering
|
|
78
|
+
- `snapshotEvents` entries are type-checked against schema events
|
|
79
|
+
|
|
80
|
+
## Core API: `system.history` (`HistoryAPI`)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const history = system.history; // HistoryAPI | null
|
|
84
|
+
|
|
85
|
+
if (history) {
|
|
86
|
+
// Read-only state
|
|
87
|
+
history.snapshots; // Snapshot[] — all recorded snapshots
|
|
88
|
+
history.currentIndex; // number — position in the snapshot array
|
|
89
|
+
history.isPaused; // boolean — whether recording is paused
|
|
90
|
+
|
|
91
|
+
// Navigation
|
|
92
|
+
history.goBack(); // Step backward one snapshot (changeset-aware)
|
|
93
|
+
history.goBack(3); // Step backward 3 snapshots
|
|
94
|
+
history.goForward(); // Step forward one snapshot (changeset-aware)
|
|
95
|
+
history.goForward(3); // Step forward 3 snapshots
|
|
96
|
+
history.goTo(snapshotId); // Jump to a specific snapshot by its ID
|
|
97
|
+
history.replay(); // Jump to the first snapshot
|
|
98
|
+
|
|
99
|
+
// Export / Import (JSON strings)
|
|
100
|
+
history.export(); // Serialize entire history to JSON string
|
|
101
|
+
history.import(json); // Restore history from JSON string
|
|
102
|
+
|
|
103
|
+
// Changesets — group multiple snapshots into one undo/redo unit
|
|
104
|
+
history.beginChangeset("Move piece");
|
|
105
|
+
// ... mutations happen ...
|
|
106
|
+
history.endChangeset();
|
|
107
|
+
|
|
108
|
+
// Recording control
|
|
109
|
+
history.pause(); // Temporarily stop recording snapshots
|
|
110
|
+
history.resume(); // Resume recording
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Snapshot Structure
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
interface Snapshot {
|
|
118
|
+
id: number; // Auto-incrementing identifier
|
|
119
|
+
timestamp: number; // When captured (Date.now())
|
|
120
|
+
facts: Record<string, unknown>; // Complete copy of all fact values
|
|
121
|
+
trigger: string; // What caused this snapshot (e.g., "fact:count")
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Framework Hook: `useHistory` (`HistoryState`)
|
|
126
|
+
|
|
127
|
+
Each framework adapter provides a reactive `useHistory` hook that re-renders on snapshot changes. Returns `null` when history is disabled, otherwise a `HistoryState` object that wraps the core API with convenience properties.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interface SnapshotMeta {
|
|
131
|
+
id: number; // Snapshot identifier
|
|
132
|
+
timestamp: number; // When captured
|
|
133
|
+
trigger: string; // What caused this snapshot
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface HistoryState {
|
|
137
|
+
// Convenience booleans (not on core API)
|
|
138
|
+
canGoBack: boolean; // True when currentIndex > 0
|
|
139
|
+
canGoForward: boolean; // True when currentIndex < totalSnapshots - 1
|
|
140
|
+
currentIndex: number;
|
|
141
|
+
totalSnapshots: number;
|
|
142
|
+
|
|
143
|
+
// Snapshot access (metadata only — keeps re-renders cheap)
|
|
144
|
+
snapshots: SnapshotMeta[];
|
|
145
|
+
getSnapshotFacts: (id: number) => Record<string, unknown> | null;
|
|
146
|
+
|
|
147
|
+
// Navigation
|
|
148
|
+
goTo: (snapshotId: number) => void;
|
|
149
|
+
goBack: (steps: number) => void;
|
|
150
|
+
goForward: (steps: number) => void;
|
|
151
|
+
replay: () => void;
|
|
152
|
+
|
|
153
|
+
// Session persistence
|
|
154
|
+
exportSession: () => string; // Wraps history.export()
|
|
155
|
+
importSession: (json: string) => void; // Wraps history.import()
|
|
156
|
+
|
|
157
|
+
// Changesets
|
|
158
|
+
beginChangeset: (label: string) => void;
|
|
159
|
+
endChangeset: () => void;
|
|
160
|
+
|
|
161
|
+
// Recording control
|
|
162
|
+
isPaused: boolean;
|
|
163
|
+
pause: () => void;
|
|
164
|
+
resume: () => void;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### React Example
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { useHistory } from "@directive-run/react";
|
|
172
|
+
|
|
173
|
+
function HistoryToolbar() {
|
|
174
|
+
const history = useHistory(system);
|
|
175
|
+
if (!history) return null;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<button onClick={() => history.goBack()} disabled={!history.canGoBack}>Undo</button>
|
|
180
|
+
<button onClick={() => history.goForward()} disabled={!history.canGoForward}>Redo</button>
|
|
181
|
+
<span>{history.currentIndex + 1} / {history.totalSnapshots}</span>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Undo/Redo Pattern
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const system = createSystem({
|
|
191
|
+
module: editorModule,
|
|
192
|
+
history: { maxSnapshots: 200 },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
system.start();
|
|
196
|
+
|
|
197
|
+
// User makes changes
|
|
198
|
+
system.facts.text = "Hello";
|
|
199
|
+
system.facts.text = "Hello, world";
|
|
200
|
+
system.facts.text = "Hello, world!";
|
|
201
|
+
|
|
202
|
+
const history = system.history!;
|
|
203
|
+
|
|
204
|
+
// Undo (one step back)
|
|
205
|
+
history.goBack();
|
|
206
|
+
console.log(system.facts.text); // "Hello, world"
|
|
207
|
+
|
|
208
|
+
history.goBack();
|
|
209
|
+
console.log(system.facts.text); // "Hello"
|
|
210
|
+
|
|
211
|
+
// Redo (one step forward)
|
|
212
|
+
history.goForward();
|
|
213
|
+
console.log(system.facts.text); // "Hello, world"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## How Snapshots Work
|
|
217
|
+
|
|
218
|
+
Snapshots are taken **once per reconciliation cycle**, not per individual fact change. All synchronous fact mutations within the same event handler batch into a single snapshot. One `goBack()` reverts all changes from that event.
|
|
219
|
+
|
|
220
|
+
## Changesets: Grouping Multiple Events
|
|
221
|
+
|
|
222
|
+
Changesets group snapshots from **multiple separate events** into one undo/redo unit. Useful when a single user action triggers a sequence of events.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const history = system.history!;
|
|
226
|
+
|
|
227
|
+
// Two separate events = two snapshots
|
|
228
|
+
system.events.pickUp({ index: 0 }); // → snapshot 1
|
|
229
|
+
system.events.place({ from: 0, to: 1 }); // → snapshot 2
|
|
230
|
+
// goBack() only reverts the place — need two goBack() calls
|
|
231
|
+
|
|
232
|
+
// With changeset — grouped into one undo unit
|
|
233
|
+
history.beginChangeset("Move piece");
|
|
234
|
+
system.events.pickUp({ index: 0 }); // → snapshot 1 ─┐
|
|
235
|
+
system.events.place({ from: 0, to: 1 }); // → snapshot 2 ─┘ grouped
|
|
236
|
+
history.endChangeset();
|
|
237
|
+
// One goBack() reverts both events
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Also works for direct fact mutations across separate synchronous blocks:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
history.beginChangeset("Update user");
|
|
244
|
+
system.facts.firstName = "Alice";
|
|
245
|
+
system.facts.lastName = "Smith";
|
|
246
|
+
// These batch into one snapshot synchronously, but changeset
|
|
247
|
+
// still works if an async gap separates them
|
|
248
|
+
history.endChangeset();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Always close your changesets. If you forget `endChangeset()`, all subsequent mutations get grouped into the same changeset.
|
|
252
|
+
|
|
253
|
+
## Exporting and Importing History
|
|
254
|
+
|
|
255
|
+
Serialize the full snapshot history for bug reports or debugging.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const history = system.history!;
|
|
259
|
+
|
|
260
|
+
// Export — returns a JSON string
|
|
261
|
+
const exported = history.export();
|
|
262
|
+
localStorage.setItem("debug-session", exported);
|
|
263
|
+
|
|
264
|
+
// Import — restore from a JSON string
|
|
265
|
+
const saved = localStorage.getItem("debug-session");
|
|
266
|
+
if (saved) {
|
|
267
|
+
history.import(saved);
|
|
268
|
+
|
|
269
|
+
// Step through the user's exact state sequence
|
|
270
|
+
history.goTo(0); // First snapshot
|
|
271
|
+
history.goTo(5); // When the bug occurred
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Performance: maxSnapshots
|
|
276
|
+
|
|
277
|
+
Every fact mutation creates a snapshot. Cap the number to control memory:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Low memory — keeps last 20 snapshots, discards oldest
|
|
281
|
+
history: { maxSnapshots: 20 },
|
|
282
|
+
|
|
283
|
+
// Development — generous cap for deep debugging
|
|
284
|
+
history: { maxSnapshots: 500 },
|
|
285
|
+
|
|
286
|
+
// Default if not specified
|
|
287
|
+
history: true, // maxSnapshots defaults to 100
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
When the cap is reached, the oldest snapshot is discarded (ring buffer / FIFO).
|
|
291
|
+
|
|
292
|
+
## Common Mistakes
|
|
293
|
+
|
|
294
|
+
### Enabling history in production
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// WRONG — snapshots consume memory on every mutation
|
|
298
|
+
const system = createSystem({
|
|
299
|
+
module: myModule,
|
|
300
|
+
history: true,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// CORRECT — gate on environment
|
|
304
|
+
const system = createSystem({
|
|
305
|
+
module: myModule,
|
|
306
|
+
history: process.env.NODE_ENV === "development"
|
|
307
|
+
? { maxSnapshots: 100 }
|
|
308
|
+
: false,
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Forgetting to end a changeset
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// WRONG — changeset never closed, all subsequent mutations are grouped
|
|
316
|
+
history.beginChangeset("update");
|
|
317
|
+
system.facts.name = "Alice";
|
|
318
|
+
// ... forgot endChangeset()
|
|
319
|
+
system.facts.unrelated = true; // Still part of the changeset!
|
|
320
|
+
|
|
321
|
+
// CORRECT — always close changesets
|
|
322
|
+
history.beginChangeset("update");
|
|
323
|
+
system.facts.name = "Alice";
|
|
324
|
+
history.endChangeset();
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Accessing history when disabled
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// WRONG — history not enabled, system.history is null
|
|
331
|
+
const system = createSystem({ module: myModule });
|
|
332
|
+
system.history.goBack(); // TypeError!
|
|
333
|
+
|
|
334
|
+
// CORRECT — check before using
|
|
335
|
+
if (system.history) {
|
|
336
|
+
system.history.goBack();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Or enable it
|
|
340
|
+
const system = createSystem({
|
|
341
|
+
module: myModule,
|
|
342
|
+
history: true,
|
|
343
|
+
});
|
|
344
|
+
```
|
package/core/multi-module.md
CHANGED
package/core/react-adapter.md
CHANGED
|
@@ -129,7 +129,7 @@ function GameBoard() {
|
|
|
129
129
|
// System created on mount, destroyed on unmount
|
|
130
130
|
const gameSystem = useSystem({
|
|
131
131
|
module: gameModule,
|
|
132
|
-
|
|
132
|
+
history: true,
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
const score = useSelector(gameSystem, (s) => s.facts.score);
|
package/core/system-api.md
CHANGED
|
@@ -27,7 +27,7 @@ import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
|
|
|
27
27
|
const system = createSystem({
|
|
28
28
|
module: myModule,
|
|
29
29
|
plugins: [loggingPlugin(), devtoolsPlugin()],
|
|
30
|
-
|
|
30
|
+
history: { maxSnapshots: 100 },
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// Multi-module – namespaced access
|
|
@@ -212,6 +212,120 @@ system.constraints.isDisabled("fetchWhenReady"); // true
|
|
|
212
212
|
system.constraints.enable("fetchWhenReady");
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
## SSR and Hydration
|
|
216
|
+
|
|
217
|
+
Four mechanisms for populating a system with external state:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// 1. initialFacts – simplest, at construction time
|
|
221
|
+
const system = createSystem({
|
|
222
|
+
module: myModule,
|
|
223
|
+
initialFacts: { userId: "user-1", name: "Alice" },
|
|
224
|
+
});
|
|
225
|
+
system.start();
|
|
226
|
+
|
|
227
|
+
// 2. system.hydrate() – async, before start()
|
|
228
|
+
const system = createSystem({ module: myModule });
|
|
229
|
+
await system.hydrate(async () => {
|
|
230
|
+
const res = await fetch('/api/state');
|
|
231
|
+
|
|
232
|
+
return res.json();
|
|
233
|
+
});
|
|
234
|
+
system.start();
|
|
235
|
+
|
|
236
|
+
// 3. system.restore() – sync, applies facts from a snapshot
|
|
237
|
+
const system = createSystem({ module: myModule });
|
|
238
|
+
system.restore(serverSnapshot);
|
|
239
|
+
system.start();
|
|
240
|
+
|
|
241
|
+
// 4. DirectiveHydrator + useHydratedSystem (React only)
|
|
242
|
+
// Server: getDistributableSnapshot() → serialize
|
|
243
|
+
// Client: <DirectiveHydrator snapshot={s}><App /></DirectiveHydrator>
|
|
244
|
+
// useHydratedSystem(module) inside App
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### SSR Lifecycle
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Server: create → start → settle → snapshot → destroy
|
|
251
|
+
const system = createSystem({
|
|
252
|
+
module: pageModule,
|
|
253
|
+
initialFacts: { userId: req.user.id },
|
|
254
|
+
});
|
|
255
|
+
system.start();
|
|
256
|
+
await system.settle(5000); // Throws on timeout
|
|
257
|
+
const snapshot = system.getSnapshot();
|
|
258
|
+
system.stop();
|
|
259
|
+
system.destroy();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Snapshot Types
|
|
263
|
+
|
|
264
|
+
- `SystemSnapshot` – facts only, used with `getSnapshot()` / `restore()`
|
|
265
|
+
- `DistributableSnapshot` – facts + derivations + metadata + TTL, used with `getDistributableSnapshot()` and `DirectiveHydrator`
|
|
266
|
+
|
|
267
|
+
### Avoiding Singletons
|
|
268
|
+
|
|
269
|
+
Never use module-level systems in SSR – they share state across concurrent requests. Always create a fresh system per request and destroy it when done.
|
|
270
|
+
|
|
271
|
+
## Runtime Dynamics
|
|
272
|
+
|
|
273
|
+
All four subsystems (constraints, resolvers, derivations, effects) share a uniform dynamic definition interface:
|
|
274
|
+
|
|
275
|
+
### Enable / Disable (Constraints & Effects only)
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
system.constraints.disable("id");
|
|
279
|
+
system.constraints.enable("id");
|
|
280
|
+
system.constraints.isDisabled("id");
|
|
281
|
+
|
|
282
|
+
system.effects.disable("id");
|
|
283
|
+
system.effects.enable("id");
|
|
284
|
+
system.effects.isEnabled("id");
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Register / Assign / Unregister (All 4 subsystems)
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Register a new definition at runtime
|
|
291
|
+
system.constraints.register("id", { when: ..., require: ... });
|
|
292
|
+
system.resolvers.register("id", { requirement: "TYPE", resolve: ... });
|
|
293
|
+
system.derive.register("id", (facts) => facts.count * 3);
|
|
294
|
+
system.effects.register("id", { run: (facts) => { ... } });
|
|
295
|
+
|
|
296
|
+
// Override an existing definition
|
|
297
|
+
system.constraints.assign("id", { when: ..., require: ... });
|
|
298
|
+
|
|
299
|
+
// Remove a dynamically registered definition (static = no-op + dev warning)
|
|
300
|
+
system.constraints.unregister("id");
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Semantics: `register` throws if ID exists. `assign` throws if ID doesn't exist. `unregister` on static ID = dev warning, no-op. All three are deferred if called during reconciliation.
|
|
304
|
+
|
|
305
|
+
### Introspection
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
system.constraints.isDynamic("id"); // true if registered at runtime
|
|
309
|
+
system.constraints.listDynamic(); // all dynamic constraint IDs
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### getOriginal / restoreOriginal
|
|
313
|
+
|
|
314
|
+
When `assign()` overrides a static definition, the original is saved:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
system.getOriginal("constraint", "id"); // returns original definition
|
|
318
|
+
system.restoreOriginal("constraint", "id"); // restores it, returns true/false
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Dynamic Module Registration
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
system.registerModule("chat", chatModule); // adds module to running system
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
See docs: https://directive.run/docs/advanced/runtime
|
|
328
|
+
|
|
215
329
|
## Common Mistakes
|
|
216
330
|
|
|
217
331
|
### Reading facts before settling
|
package/examples/ab-testing.ts
CHANGED
package/examples/checkers.ts
CHANGED
|
@@ -43,11 +43,14 @@ import {
|
|
|
43
43
|
import type { CacheStats } from "@directive-run/ai";
|
|
44
44
|
import {
|
|
45
45
|
type CircuitState,
|
|
46
|
-
createAgentMetrics,
|
|
47
46
|
createCircuitBreaker,
|
|
48
47
|
createOTLPExporter,
|
|
49
|
-
createObservability,
|
|
50
48
|
} from "@directive-run/core/plugins";
|
|
49
|
+
// createObservability is alpha (not in bundle) — direct source import
|
|
50
|
+
import {
|
|
51
|
+
createObservability,
|
|
52
|
+
createAgentMetrics,
|
|
53
|
+
} from "../../../packages/core/src/plugins/observability.lab.js";
|
|
51
54
|
import {
|
|
52
55
|
analysisAgent,
|
|
53
56
|
chatAgent,
|
package/examples/contact-form.ts
CHANGED
package/examples/counter.ts
CHANGED
|
@@ -371,5 +371,6 @@ const numberMatch = createModule("number-match", {
|
|
|
371
371
|
export const system = createSystem({
|
|
372
372
|
module: numberMatch,
|
|
373
373
|
plugins: [devtoolsPlugin({ name: "number-match" })],
|
|
374
|
-
|
|
374
|
+
history: true,
|
|
375
|
+
trace: true,
|
|
375
376
|
});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { createSystem } from "@directive-run/core";
|
|
15
15
|
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
16
|
+
import { el } from "@directive-run/el";
|
|
16
17
|
import {
|
|
17
18
|
debounceSearchModule,
|
|
18
19
|
debounceSearchSchema,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
|
|
25
26
|
const system = createSystem({
|
|
26
27
|
module: debounceSearchModule,
|
|
27
|
-
|
|
28
|
+
trace: true,
|
|
28
29
|
plugins: [devtoolsPlugin({ name: "debounce-constraints" })],
|
|
29
30
|
});
|
|
30
31
|
system.start();
|
|
@@ -85,15 +86,6 @@ const tickInterval = setInterval(() => {
|
|
|
85
86
|
// Sliders
|
|
86
87
|
|
|
87
88
|
|
|
88
|
-
// ============================================================================
|
|
89
|
-
// Helpers
|
|
90
|
-
// ============================================================================
|
|
91
|
-
|
|
92
|
-
function escapeHtml(text: string): string {
|
|
93
|
-
|
|
94
|
-
return div.innerHTML;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
89
|
// ============================================================================
|
|
98
90
|
// Initial Render
|
|
99
91
|
// ============================================================================
|
|
@@ -344,7 +344,7 @@ let currentStrategy: RecoveryStrategy = "retry-later";
|
|
|
344
344
|
|
|
345
345
|
export const system = createSystem({
|
|
346
346
|
module: dashboardModule,
|
|
347
|
-
|
|
347
|
+
trace: true,
|
|
348
348
|
plugins: [perf, devtoolsPlugin({ name: "error-boundaries" })],
|
|
349
349
|
errorBoundary: {
|
|
350
350
|
onResolverError: (_error, resolver) => {
|
package/examples/form-wizard.ts
CHANGED
|
@@ -646,12 +646,8 @@ export const fraudAnalysisModule = createModule("fraud", {
|
|
|
646
646
|
export const system = createSystem({
|
|
647
647
|
module: fraudAnalysisModule,
|
|
648
648
|
plugins: [devtoolsPlugin({ name: "fraud-analysis", panel: true })],
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
maxSnapshots: 50,
|
|
652
|
-
runHistory: true,
|
|
653
|
-
maxRuns: 100,
|
|
654
|
-
},
|
|
649
|
+
history: { maxSnapshots: 50 },
|
|
650
|
+
trace: { maxRuns: 100 },
|
|
655
651
|
});
|
|
656
652
|
|
|
657
653
|
// ============================================================================
|
package/examples/multi-module.ts
CHANGED
package/examples/newsletter.ts
CHANGED
package/examples/pagination.ts
CHANGED
|
@@ -255,6 +255,6 @@ export const listModule = createModule("list", {
|
|
|
255
255
|
|
|
256
256
|
export const system = createSystem({
|
|
257
257
|
modules: { filters: filtersModule, list: listModule },
|
|
258
|
-
|
|
258
|
+
trace: true,
|
|
259
259
|
plugins: [loggingPlugin(), devtoolsPlugin({ name: "pagination" })],
|
|
260
260
|
});
|
package/examples/permissions.ts
CHANGED
|
@@ -414,9 +414,6 @@ export const system = createSystem({
|
|
|
414
414
|
auth: authModule,
|
|
415
415
|
},
|
|
416
416
|
plugins: [devtoolsPlugin({ name: "shopping-cart", panel: true })],
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
maxSnapshots: 50,
|
|
420
|
-
runHistory: true,
|
|
421
|
-
},
|
|
417
|
+
history: { maxSnapshots: 50 },
|
|
418
|
+
trace: true,
|
|
422
419
|
});
|
package/examples/sudoku.ts
CHANGED
|
@@ -109,7 +109,14 @@ export const sudokuSchema = {
|
|
|
109
109
|
|
|
110
110
|
export const sudokuGame = createModule("sudoku", {
|
|
111
111
|
schema: sudokuSchema,
|
|
112
|
-
|
|
112
|
+
history: {
|
|
113
|
+
snapshotEvents: [
|
|
114
|
+
"inputNumber",
|
|
115
|
+
"toggleNote",
|
|
116
|
+
"requestHint",
|
|
117
|
+
"newGame",
|
|
118
|
+
],
|
|
119
|
+
},
|
|
113
120
|
|
|
114
121
|
init: (facts) => {
|
|
115
122
|
const { puzzle, solution } = generatePuzzle("easy");
|
package/examples/time-machine.ts
CHANGED
|
@@ -142,6 +142,7 @@ const canvasModule = createModule("canvas", {
|
|
|
142
142
|
|
|
143
143
|
export const system = createSystem({
|
|
144
144
|
module: canvasModule,
|
|
145
|
-
|
|
145
|
+
history: { maxSnapshots: 200 },
|
|
146
|
+
trace: true,
|
|
146
147
|
plugins: [devtoolsPlugin({ name: "time-machine" })],
|
|
147
148
|
});
|
package/examples/url-sync.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/knowledge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Knowledge files, examples, and validation for Directive — the constraint-driven TypeScript runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jason Comes",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"tsx": "^4.19.2",
|
|
51
51
|
"typescript": "^5.7.2",
|
|
52
52
|
"vitest": "^3.0.0",
|
|
53
|
-
"@directive-run/core": "0.
|
|
54
|
-
"@directive-run/ai": "0.
|
|
53
|
+
"@directive-run/core": "0.7.0",
|
|
54
|
+
"@directive-run/ai": "0.7.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsx scripts/generate-api-skeleton.ts && tsx scripts/extract-examples.ts && tsup",
|
package/core/time-travel.md
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
# Time-Travel Debugging
|
|
2
|
-
|
|
3
|
-
Directive can record every fact change as a snapshot, enabling undo/redo, replay, and state export for bug reports.
|
|
4
|
-
|
|
5
|
-
## Decision Tree: "Should I enable time-travel?"
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
What's the use case?
|
|
9
|
-
├── Debugging during development → Yes, enable with maxSnapshots cap
|
|
10
|
-
├── Production app → No, disable for performance
|
|
11
|
-
├── Bug reproduction → Enable, use exportHistory() to share
|
|
12
|
-
├── Testing → Usually no – use assertFact/assertDerivation instead
|
|
13
|
-
└── Demo / presentation → Yes, great for showing state changes
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Enabling Time-Travel
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
import { createSystem } from "@directive-run/core";
|
|
20
|
-
|
|
21
|
-
const system = createSystem({
|
|
22
|
-
module: myModule,
|
|
23
|
-
debug: {
|
|
24
|
-
timeTravel: true, // Enable snapshot recording
|
|
25
|
-
maxSnapshots: 100, // Cap memory usage (default: 50)
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Time-travel is disabled by default. Snapshots are recorded automatically on every fact mutation.
|
|
31
|
-
|
|
32
|
-
## The TimeTravelAPI
|
|
33
|
-
|
|
34
|
-
Access via `system.debug.timeTravel`:
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
const tt = system.debug.timeTravel;
|
|
38
|
-
|
|
39
|
-
// Navigation
|
|
40
|
-
tt.canUndo(); // boolean – is there a previous snapshot?
|
|
41
|
-
tt.canRedo(); // boolean – is there a next snapshot?
|
|
42
|
-
tt.undo(); // Restore previous snapshot
|
|
43
|
-
tt.redo(); // Restore next snapshot
|
|
44
|
-
|
|
45
|
-
// Direct access
|
|
46
|
-
tt.getSnapshots(); // Array of all snapshots
|
|
47
|
-
tt.goToSnapshot(index); // Jump to a specific snapshot by index
|
|
48
|
-
|
|
49
|
-
// Each snapshot contains:
|
|
50
|
-
// {
|
|
51
|
-
// facts: { ... }, – full fact state at that point
|
|
52
|
-
// timestamp: number, – when the snapshot was taken
|
|
53
|
-
// label?: string, – optional label from changeset
|
|
54
|
-
// changedKeys: string[], – which facts changed
|
|
55
|
-
// }
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Undo/Redo Pattern
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
const system = createSystem({
|
|
62
|
-
module: editorModule,
|
|
63
|
-
debug: { timeTravel: true, maxSnapshots: 200 },
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
system.start();
|
|
67
|
-
|
|
68
|
-
// User makes changes
|
|
69
|
-
system.facts.text = "Hello";
|
|
70
|
-
system.facts.text = "Hello, world";
|
|
71
|
-
system.facts.text = "Hello, world!";
|
|
72
|
-
|
|
73
|
-
// Undo last change
|
|
74
|
-
const tt = system.debug.timeTravel;
|
|
75
|
-
tt.undo();
|
|
76
|
-
console.log(system.facts.text); // "Hello, world"
|
|
77
|
-
|
|
78
|
-
tt.undo();
|
|
79
|
-
console.log(system.facts.text); // "Hello"
|
|
80
|
-
|
|
81
|
-
// Redo
|
|
82
|
-
tt.redo();
|
|
83
|
-
console.log(system.facts.text); // "Hello, world"
|
|
84
|
-
|
|
85
|
-
// Check navigation state
|
|
86
|
-
tt.canUndo(); // true
|
|
87
|
-
tt.canRedo(); // true
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Changesets: Grouping Related Changes
|
|
91
|
-
|
|
92
|
-
Multiple fact mutations can be grouped into a single undoable unit.
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
const tt = system.debug.timeTravel;
|
|
96
|
-
|
|
97
|
-
// Without changeset – each mutation is a separate snapshot
|
|
98
|
-
system.facts.firstName = "Alice";
|
|
99
|
-
system.facts.lastName = "Smith";
|
|
100
|
-
// Two snapshots, two undos needed
|
|
101
|
-
|
|
102
|
-
// With changeset – grouped into one snapshot
|
|
103
|
-
tt.beginChangeset("Update user name");
|
|
104
|
-
system.facts.firstName = "Alice";
|
|
105
|
-
system.facts.lastName = "Smith";
|
|
106
|
-
tt.endChangeset();
|
|
107
|
-
// One snapshot, one undo restores both
|
|
108
|
-
|
|
109
|
-
// Undo reverts the entire changeset
|
|
110
|
-
tt.undo();
|
|
111
|
-
// Both firstName and lastName are restored
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Use changesets for logically related mutations: form submissions, multi-field updates, resolver results.
|
|
115
|
-
|
|
116
|
-
## Exporting and Importing History
|
|
117
|
-
|
|
118
|
-
Serialize the full snapshot history for bug reports or debugging.
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
const tt = system.debug.timeTravel;
|
|
122
|
-
|
|
123
|
-
// Export – returns a serializable object
|
|
124
|
-
const historyData = tt.exportHistory();
|
|
125
|
-
// Send to server, save to file, attach to bug report
|
|
126
|
-
console.log(JSON.stringify(historyData));
|
|
127
|
-
|
|
128
|
-
// Import – restore history from exported data
|
|
129
|
-
tt.importHistory(historyData);
|
|
130
|
-
|
|
131
|
-
// Now you can step through the user's exact state sequence
|
|
132
|
-
tt.goToSnapshot(0); // Start
|
|
133
|
-
tt.goToSnapshot(5); // When the bug occurred
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Snapshot Inspection
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
const tt = system.debug.timeTravel;
|
|
140
|
-
const snapshots = tt.getSnapshots();
|
|
141
|
-
|
|
142
|
-
// Walk through all snapshots
|
|
143
|
-
for (const snap of snapshots) {
|
|
144
|
-
console.log(`[${new Date(snap.timestamp).toISOString()}]`);
|
|
145
|
-
console.log(` Changed: ${snap.changedKeys.join(", ")}`);
|
|
146
|
-
if (snap.label) {
|
|
147
|
-
console.log(` Label: ${snap.label}`);
|
|
148
|
-
}
|
|
149
|
-
console.log(` Facts:`, snap.facts);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Jump to a specific point
|
|
153
|
-
tt.goToSnapshot(3);
|
|
154
|
-
console.log(system.facts); // State as of snapshot 3
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Performance: maxSnapshots
|
|
158
|
-
|
|
159
|
-
Every fact mutation creates a snapshot. Cap the number to control memory:
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// Low memory – keeps last 20 snapshots, discards oldest
|
|
163
|
-
debug: { timeTravel: true, maxSnapshots: 20 },
|
|
164
|
-
|
|
165
|
-
// Development – generous cap for deep debugging
|
|
166
|
-
debug: { timeTravel: true, maxSnapshots: 500 },
|
|
167
|
-
|
|
168
|
-
// Default if not specified
|
|
169
|
-
debug: { timeTravel: true }, // maxSnapshots defaults to 50
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
When the cap is reached, the oldest snapshot is discarded (FIFO). Redo history beyond the cap is lost.
|
|
173
|
-
|
|
174
|
-
## Common Mistakes
|
|
175
|
-
|
|
176
|
-
### Enabling time-travel in production
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
// WRONG – snapshots consume memory on every mutation
|
|
180
|
-
const system = createSystem({
|
|
181
|
-
module: myModule,
|
|
182
|
-
debug: { timeTravel: true },
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// CORRECT – gate on environment
|
|
186
|
-
const system = createSystem({
|
|
187
|
-
module: myModule,
|
|
188
|
-
debug: {
|
|
189
|
-
timeTravel: process.env.NODE_ENV === "development",
|
|
190
|
-
maxSnapshots: 100,
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Forgetting to end a changeset
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// WRONG – changeset never closed, all subsequent mutations are grouped
|
|
199
|
-
tt.beginChangeset("update");
|
|
200
|
-
system.facts.name = "Alice";
|
|
201
|
-
// ... forgot endChangeset()
|
|
202
|
-
system.facts.unrelated = true; // Still part of the changeset!
|
|
203
|
-
|
|
204
|
-
// CORRECT – always close changesets
|
|
205
|
-
tt.beginChangeset("update");
|
|
206
|
-
system.facts.name = "Alice";
|
|
207
|
-
tt.endChangeset();
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Accessing time-travel when disabled
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
// WRONG – timeTravel not enabled, system.debug.timeTravel is null
|
|
214
|
-
const system = createSystem({ module: myModule });
|
|
215
|
-
system.debug.timeTravel.undo(); // TypeError!
|
|
216
|
-
|
|
217
|
-
// CORRECT – check before using
|
|
218
|
-
const tt = system.debug.timeTravel;
|
|
219
|
-
if (tt) {
|
|
220
|
-
tt.undo();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Or enable it
|
|
224
|
-
const system = createSystem({
|
|
225
|
-
module: myModule,
|
|
226
|
-
debug: { timeTravel: true },
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### No maxSnapshots cap with frequent mutations
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
// WRONG – unbounded snapshots in a high-frequency update loop
|
|
234
|
-
debug: { timeTravel: true }, // Default cap is 50, which is fine
|
|
235
|
-
|
|
236
|
-
// Be explicit when mutation rate is high
|
|
237
|
-
debug: { timeTravel: true, maxSnapshots: 30 },
|
|
238
|
-
```
|