qnce-engine 1.2.0 → 1.2.2
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/README.md +713 -7
- package/dist/cli/audit.js +0 -0
- package/dist/cli/init.js +0 -0
- package/dist/cli/perf.d.ts.map +1 -1
- package/dist/cli/perf.js +2 -1
- package/dist/cli/perf.js.map +1 -1
- package/dist/cli/play.d.ts +4 -0
- package/dist/cli/play.d.ts.map +1 -0
- package/dist/cli/play.js +259 -0
- package/dist/cli/play.js.map +1 -0
- package/dist/engine/condition.d.ts +69 -0
- package/dist/engine/condition.d.ts.map +1 -0
- package/dist/engine/condition.js +195 -0
- package/dist/engine/condition.js.map +1 -0
- package/dist/engine/core.d.ts +274 -3
- package/dist/engine/core.d.ts.map +1 -1
- package/dist/engine/core.js +1148 -9
- package/dist/engine/core.js.map +1 -1
- package/dist/engine/demo-story.d.ts.map +1 -1
- package/dist/engine/demo-story.js +99 -13
- package/dist/engine/demo-story.js.map +1 -1
- package/dist/engine/errors.d.ts +76 -0
- package/dist/engine/errors.d.ts.map +1 -0
- package/dist/engine/errors.js +178 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/types.d.ts +445 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +9 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/engine/validation.d.ts +110 -0
- package/dist/engine/validation.d.ts.map +1 -0
- package/dist/engine/validation.js +261 -0
- package/dist/engine/validation.js.map +1 -0
- package/dist/examples/examples/autosave-undo-demo.js +248 -0
- package/dist/examples/examples/persistence-demo.js +63 -0
- package/dist/examples/src/engine/condition.js +194 -0
- package/dist/examples/src/engine/core.js +1382 -0
- package/dist/examples/src/engine/demo-story.js +200 -0
- package/dist/examples/src/engine/types.js +8 -0
- package/dist/examples/src/index.js +35 -0
- package/dist/examples/src/integrations/react.js +322 -0
- package/dist/examples/src/narrative/branching/engine-simple.js +348 -0
- package/dist/examples/src/narrative/branching/index.js +55 -0
- package/dist/examples/src/narrative/branching/models.js +5 -0
- package/dist/examples/src/performance/ObjectPool.js +296 -0
- package/dist/examples/src/performance/PerfReporter.js +280 -0
- package/dist/examples/src/performance/ThreadPool.js +347 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/react.d.ts +200 -0
- package/dist/integrations/react.d.ts.map +1 -0
- package/dist/integrations/react.js +365 -0
- package/dist/integrations/react.js.map +1 -0
- package/dist/narrative/branching/engine-simple.js +3 -3
- package/dist/narrative/branching/engine-simple.js.map +1 -1
- package/dist/narrative/branching/engine.d.ts +1 -0
- package/dist/narrative/branching/engine.d.ts.map +1 -0
- package/dist/narrative/branching/engine.js +2 -0
- package/dist/narrative/branching/engine.js.map +1 -0
- package/dist/narrative/branching/models.d.ts.map +1 -1
- package/dist/performance/HotReloadDelta.d.ts +25 -8
- package/dist/performance/HotReloadDelta.d.ts.map +1 -1
- package/dist/performance/HotReloadDelta.js +10 -15
- package/dist/performance/HotReloadDelta.js.map +1 -1
- package/dist/ui/__tests__/AutosaveIndicator.test.d.ts +2 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.d.ts.map +1 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.js +329 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.js.map +1 -0
- package/dist/ui/__tests__/UndoRedoControls.test.d.ts +2 -0
- package/dist/ui/__tests__/UndoRedoControls.test.d.ts.map +1 -0
- package/dist/ui/__tests__/UndoRedoControls.test.js +245 -0
- package/dist/ui/__tests__/UndoRedoControls.test.js.map +1 -0
- package/dist/ui/__tests__/autosave-simple.test.d.ts +2 -0
- package/dist/ui/__tests__/autosave-simple.test.d.ts.map +1 -0
- package/dist/ui/__tests__/autosave-simple.test.js +29 -0
- package/dist/ui/__tests__/autosave-simple.test.js.map +1 -0
- package/dist/ui/__tests__/setup.d.ts +2 -0
- package/dist/ui/__tests__/setup.d.ts.map +1 -0
- package/dist/ui/__tests__/setup.js +40 -0
- package/dist/ui/__tests__/setup.js.map +1 -0
- package/dist/ui/__tests__/smoke-test.d.ts +2 -0
- package/dist/ui/__tests__/smoke-test.d.ts.map +1 -0
- package/dist/ui/__tests__/smoke-test.js +18 -0
- package/dist/ui/__tests__/smoke-test.js.map +1 -0
- package/dist/ui/__tests__/smoke-test.test.d.ts +2 -0
- package/dist/ui/__tests__/smoke-test.test.d.ts.map +1 -0
- package/dist/ui/__tests__/smoke-test.test.js +18 -0
- package/dist/ui/__tests__/smoke-test.test.js.map +1 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts +2 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts.map +1 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.js +374 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.js.map +1 -0
- package/dist/ui/components/AutosaveIndicator.d.ts +18 -0
- package/dist/ui/components/AutosaveIndicator.d.ts.map +1 -0
- package/dist/ui/components/AutosaveIndicator.js +175 -0
- package/dist/ui/components/AutosaveIndicator.js.map +1 -0
- package/dist/ui/components/UndoRedoControls.d.ts +16 -0
- package/dist/ui/components/UndoRedoControls.d.ts.map +1 -0
- package/dist/ui/components/UndoRedoControls.js +144 -0
- package/dist/ui/components/UndoRedoControls.js.map +1 -0
- package/dist/ui/hooks/useKeyboardShortcuts.d.ts +22 -0
- package/dist/ui/hooks/useKeyboardShortcuts.d.ts.map +1 -0
- package/dist/ui/hooks/useKeyboardShortcuts.js +162 -0
- package/dist/ui/hooks/useKeyboardShortcuts.js.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +14 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/types.d.ts +141 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +51 -0
- package/dist/ui/types.js.map +1 -0
- package/examples/autosave-undo-demo.ts +306 -0
- package/examples/branching-demo-simple.ts +0 -0
- package/examples/branching-demo.ts +0 -0
- package/examples/persistence-demo.ts +84 -0
- package/examples/tsconfig.json +13 -0
- package/examples/ui-components-demo.tsx +320 -0
- package/examples/validation-demo-story.json +177 -0
- package/examples/validation-demo.js +163 -0
- package/package.json +24 -4
- package/docs/branching/PDM.md +0 -443
- package/docs/branching/RELEASE-v1.2.0.md +0 -169
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// QNCE Core Engine - Framework Agnostic
|
|
3
|
+
// Quantum Narrative Convergence Engine
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.QNCEEngine = void 0;
|
|
6
|
+
exports.createQNCEEngine = createQNCEEngine;
|
|
7
|
+
exports.loadStoryData = loadStoryData;
|
|
8
|
+
const ObjectPool_1 = require("../performance/ObjectPool");
|
|
9
|
+
const ThreadPool_1 = require("../performance/ThreadPool");
|
|
10
|
+
const PerfReporter_1 = require("../performance/PerfReporter");
|
|
11
|
+
// Branching system imports
|
|
12
|
+
const branching_1 = require("../narrative/branching");
|
|
13
|
+
// State persistence imports - Sprint 3.3
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
// Conditional choice evaluator - Sprint 3.4
|
|
16
|
+
const condition_1 = require("./condition");
|
|
17
|
+
// Demo narrative data moved to demo-story.ts
|
|
18
|
+
function findNode(nodes, id) {
|
|
19
|
+
const node = nodes.find(n => n.id === id);
|
|
20
|
+
if (!node)
|
|
21
|
+
throw new Error(`Node not found: ${id}`);
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* QNCE Engine - Core narrative state management
|
|
26
|
+
* Framework agnostic implementation with object pooling integration
|
|
27
|
+
*/
|
|
28
|
+
class QNCEEngine {
|
|
29
|
+
state;
|
|
30
|
+
storyData;
|
|
31
|
+
activeFlowEvents = [];
|
|
32
|
+
performanceMode = false;
|
|
33
|
+
enableProfiling = false;
|
|
34
|
+
branchingEngine;
|
|
35
|
+
// Sprint 3.3: State persistence and checkpoints
|
|
36
|
+
checkpoints = new Map();
|
|
37
|
+
maxCheckpoints = 50;
|
|
38
|
+
autoCheckpointEnabled = false;
|
|
39
|
+
// Sprint 3.3: State persistence properties
|
|
40
|
+
checkpointManager;
|
|
41
|
+
// Sprint 3.5: Autosave and Undo/Redo properties
|
|
42
|
+
undoStack = [];
|
|
43
|
+
redoStack = [];
|
|
44
|
+
autosaveConfig = {
|
|
45
|
+
enabled: false,
|
|
46
|
+
triggers: ['choice', 'flag-change'],
|
|
47
|
+
maxEntries: 10,
|
|
48
|
+
throttleMs: 1000,
|
|
49
|
+
includeMetadata: true
|
|
50
|
+
};
|
|
51
|
+
undoRedoConfig = {
|
|
52
|
+
enabled: true,
|
|
53
|
+
maxUndoEntries: 50,
|
|
54
|
+
maxRedoEntries: 20,
|
|
55
|
+
trackFlagChanges: true,
|
|
56
|
+
trackChoiceText: true,
|
|
57
|
+
trackActions: ['choice', 'flag-change', 'state-load']
|
|
58
|
+
};
|
|
59
|
+
lastAutosaveTime = 0;
|
|
60
|
+
isUndoRedoOperation = false;
|
|
61
|
+
constructor(storyData, initialState, performanceMode = false, threadPoolConfig) {
|
|
62
|
+
this.storyData = storyData;
|
|
63
|
+
this.performanceMode = performanceMode;
|
|
64
|
+
this.enableProfiling = performanceMode; // Enable profiling with performance mode
|
|
65
|
+
// Initialize ThreadPool if in performance mode
|
|
66
|
+
if (this.performanceMode && threadPoolConfig) {
|
|
67
|
+
(0, ThreadPool_1.getThreadPool)(threadPoolConfig);
|
|
68
|
+
}
|
|
69
|
+
this.state = {
|
|
70
|
+
currentNodeId: initialState?.currentNodeId || storyData.initialNodeId,
|
|
71
|
+
flags: initialState?.flags || {},
|
|
72
|
+
history: initialState?.history || [storyData.initialNodeId],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
getCurrentNode() {
|
|
76
|
+
const cacheKey = `node-${this.state.currentNodeId}`;
|
|
77
|
+
// Profiling: Record cache operation
|
|
78
|
+
if (this.enableProfiling) {
|
|
79
|
+
const found = findNode(this.storyData.nodes, this.state.currentNodeId);
|
|
80
|
+
PerfReporter_1.perf.cacheHit(cacheKey, { nodeId: this.state.currentNodeId });
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
if (this.performanceMode) {
|
|
84
|
+
// Use pooled node for enhanced node data
|
|
85
|
+
const pooledNode = ObjectPool_1.poolManager.borrowNode();
|
|
86
|
+
const coreNode = findNode(this.storyData.nodes, this.state.currentNodeId);
|
|
87
|
+
pooledNode.initialize(coreNode.id, coreNode.text, coreNode.choices);
|
|
88
|
+
// Return the pooled node (caller should return it when done)
|
|
89
|
+
return {
|
|
90
|
+
id: pooledNode.id,
|
|
91
|
+
text: pooledNode.text,
|
|
92
|
+
choices: pooledNode.choices
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return findNode(this.storyData.nodes, this.state.currentNodeId);
|
|
96
|
+
}
|
|
97
|
+
getState() {
|
|
98
|
+
return { ...this.state };
|
|
99
|
+
}
|
|
100
|
+
getFlags() {
|
|
101
|
+
return { ...this.state.flags };
|
|
102
|
+
}
|
|
103
|
+
setFlag(key, value) {
|
|
104
|
+
// Sprint 3.5: Save state for undo before making changes
|
|
105
|
+
const preChangeState = this.deepCopy(this.state);
|
|
106
|
+
this.state.flags[key] = value;
|
|
107
|
+
// Sprint 3.5: Track state change for undo/redo
|
|
108
|
+
if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
|
|
109
|
+
this.undoRedoConfig.trackActions.includes('flag-change')) {
|
|
110
|
+
this.pushToUndoStack(preChangeState, 'flag-change', {
|
|
111
|
+
flagsChanged: [key],
|
|
112
|
+
flagValue: value
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Sprint 3.5: Trigger autosave if enabled
|
|
116
|
+
if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('flag-change')) {
|
|
117
|
+
this.triggerAutosave('flag-change', {
|
|
118
|
+
flagKey: key,
|
|
119
|
+
flagValue: value
|
|
120
|
+
}).catch((error) => {
|
|
121
|
+
console.warn('[QNCE] Autosave failed:', error.message);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
getHistory() {
|
|
126
|
+
return [...this.state.history];
|
|
127
|
+
}
|
|
128
|
+
selectChoice(choice) {
|
|
129
|
+
// Sprint 3.5: Save state for undo before making changes
|
|
130
|
+
const preChangeState = this.deepCopy(this.state);
|
|
131
|
+
// S2-T4: Add state transition profiling
|
|
132
|
+
const transitionSpanId = this.enableProfiling
|
|
133
|
+
? (0, PerfReporter_1.getPerfReporter)().startSpan('state-transition', {
|
|
134
|
+
fromNodeId: this.state.currentNodeId,
|
|
135
|
+
toNodeId: choice.nextNodeId,
|
|
136
|
+
hasEffects: !!choice.flagEffects
|
|
137
|
+
})
|
|
138
|
+
: null;
|
|
139
|
+
const fromNodeId = this.state.currentNodeId;
|
|
140
|
+
const toNodeId = choice.nextNodeId;
|
|
141
|
+
// Create flow event for tracking narrative progression
|
|
142
|
+
if (this.performanceMode) {
|
|
143
|
+
const flowSpanId = this.enableProfiling
|
|
144
|
+
? PerfReporter_1.perf.flowStart(fromNodeId, { toNodeId })
|
|
145
|
+
: null;
|
|
146
|
+
const flowEvent = this.createFlowEvent(fromNodeId, toNodeId, choice.flagEffects);
|
|
147
|
+
this.recordFlowEvent(flowEvent);
|
|
148
|
+
// Return the flow immediately after recording (we don't need to keep it)
|
|
149
|
+
// This ensures proper pool recycling
|
|
150
|
+
ObjectPool_1.poolManager.returnFlow(flowEvent);
|
|
151
|
+
if (flowSpanId) {
|
|
152
|
+
PerfReporter_1.perf.flowComplete(flowSpanId, toNodeId, { transitionType: 'choice' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.state.currentNodeId = choice.nextNodeId;
|
|
156
|
+
this.state.history.push(choice.nextNodeId);
|
|
157
|
+
if (choice.flagEffects) {
|
|
158
|
+
this.state.flags = { ...this.state.flags, ...choice.flagEffects };
|
|
159
|
+
// S2-T4: Track flag updates
|
|
160
|
+
if (this.enableProfiling) {
|
|
161
|
+
PerfReporter_1.perf.record('custom', {
|
|
162
|
+
flagCount: Object.keys(choice.flagEffects).length,
|
|
163
|
+
nodeId: toNodeId,
|
|
164
|
+
eventType: 'flag-update'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Sprint 3.5: Track state change for undo/redo
|
|
169
|
+
if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
|
|
170
|
+
this.undoRedoConfig.trackActions.includes('choice')) {
|
|
171
|
+
this.pushToUndoStack(preChangeState, 'choice', {
|
|
172
|
+
nodeId: fromNodeId,
|
|
173
|
+
choiceText: this.undoRedoConfig.trackChoiceText ? choice.text : undefined,
|
|
174
|
+
flagsChanged: choice.flagEffects ? Object.keys(choice.flagEffects) : undefined
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// Sprint 3.5: Trigger autosave if enabled
|
|
178
|
+
if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('choice')) {
|
|
179
|
+
this.triggerAutosave('choice', {
|
|
180
|
+
fromNodeId,
|
|
181
|
+
toNodeId,
|
|
182
|
+
choiceText: choice.text
|
|
183
|
+
}).catch(error => {
|
|
184
|
+
console.warn('[QNCE] Autosave failed:', error.message);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Complete state transition span
|
|
188
|
+
if (transitionSpanId) {
|
|
189
|
+
(0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, {
|
|
190
|
+
historyLength: this.state.history.length,
|
|
191
|
+
flagCount: Object.keys(this.state.flags).length
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// S2-T2: Background operations after state change
|
|
195
|
+
if (this.performanceMode) {
|
|
196
|
+
// Preload next possible nodes in background
|
|
197
|
+
this.preloadNextNodes().catch(error => {
|
|
198
|
+
console.warn('[QNCE] Background preload failed:', error.message);
|
|
199
|
+
});
|
|
200
|
+
// Write telemetry data in background
|
|
201
|
+
this.backgroundTelemetryWrite({
|
|
202
|
+
action: 'choice-selected',
|
|
203
|
+
fromNodeId,
|
|
204
|
+
toNodeId,
|
|
205
|
+
choiceText: choice.text.slice(0, 50) // First 50 chars for privacy
|
|
206
|
+
}).catch(error => {
|
|
207
|
+
console.warn('[QNCE] Background telemetry failed:', error.message);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
resetNarrative() {
|
|
212
|
+
// Sprint 3.5: Save state for undo before reset
|
|
213
|
+
const preChangeState = this.deepCopy(this.state);
|
|
214
|
+
// Clean up pooled objects before reset
|
|
215
|
+
if (this.performanceMode) {
|
|
216
|
+
this.cleanupPools();
|
|
217
|
+
}
|
|
218
|
+
this.state.currentNodeId = this.storyData.initialNodeId;
|
|
219
|
+
this.state.flags = {};
|
|
220
|
+
this.state.history = [this.storyData.initialNodeId];
|
|
221
|
+
// Sprint 3.5: Track state change for undo/redo
|
|
222
|
+
if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
|
|
223
|
+
this.undoRedoConfig.trackActions.includes('reset')) {
|
|
224
|
+
this.pushToUndoStack(preChangeState, 'reset', {
|
|
225
|
+
nodeId: this.storyData.initialNodeId
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Load simple state (legacy method for backward compatibility)
|
|
231
|
+
* @param state - QNCEState to load
|
|
232
|
+
*/
|
|
233
|
+
loadSimpleState(state) {
|
|
234
|
+
// Sprint 3.5: Save state for undo before loading
|
|
235
|
+
const preChangeState = this.deepCopy(this.state);
|
|
236
|
+
this.state = { ...state };
|
|
237
|
+
// Sprint 3.5: Track state change for undo/redo
|
|
238
|
+
if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
|
|
239
|
+
this.undoRedoConfig.trackActions.includes('state-load')) {
|
|
240
|
+
this.pushToUndoStack(preChangeState, 'state-load', {
|
|
241
|
+
nodeId: state.currentNodeId
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// Sprint 3.5: Trigger autosave if enabled
|
|
245
|
+
if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('state-load')) {
|
|
246
|
+
this.triggerAutosave('state-load', {
|
|
247
|
+
nodeId: state.currentNodeId
|
|
248
|
+
}).catch((error) => {
|
|
249
|
+
console.warn('[QNCE] Autosave failed:', error.message);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Sprint 3.3: State Persistence & Checkpoints Implementation
|
|
254
|
+
/**
|
|
255
|
+
* Save current engine state to a serialized format
|
|
256
|
+
* @param options - Serialization options
|
|
257
|
+
* @returns Promise resolving to serialized state
|
|
258
|
+
*/
|
|
259
|
+
async saveState(options = {}) {
|
|
260
|
+
const startTime = performance.now();
|
|
261
|
+
if (!this.state.currentNodeId) {
|
|
262
|
+
throw new Error('Invalid state: currentNodeId is missing.');
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
// Create serialization metadata
|
|
266
|
+
const metadata = {
|
|
267
|
+
engineVersion: types_1.PERSISTENCE_VERSION,
|
|
268
|
+
timestamp: new Date().toISOString(),
|
|
269
|
+
storyId: this.generateStoryHash(),
|
|
270
|
+
customMetadata: options.customMetadata || {},
|
|
271
|
+
compression: options.compression || 'none'
|
|
272
|
+
};
|
|
273
|
+
// Build serialized state
|
|
274
|
+
const serializedState = {
|
|
275
|
+
state: this.deepCopy(this.state),
|
|
276
|
+
flowEvents: options.includeFlowEvents !== false ?
|
|
277
|
+
this.deepCopy(this.activeFlowEvents) : [],
|
|
278
|
+
metadata
|
|
279
|
+
};
|
|
280
|
+
// Add optional data based on options
|
|
281
|
+
if (options.includePerformanceData && this.performanceMode) {
|
|
282
|
+
serializedState.performanceState = {
|
|
283
|
+
performanceMode: this.performanceMode,
|
|
284
|
+
enableProfiling: this.enableProfiling,
|
|
285
|
+
backgroundTasks: [], // Would be populated with actual background task IDs
|
|
286
|
+
telemetryData: [] // Would be populated with telemetry history
|
|
287
|
+
};
|
|
288
|
+
serializedState.poolStats = this.getPoolStats() || {};
|
|
289
|
+
}
|
|
290
|
+
if (options.includeBranchingContext && this.branchingEngine) {
|
|
291
|
+
serializedState.branchingContext = {
|
|
292
|
+
activeBranches: [], // Would be populated from branching engine
|
|
293
|
+
branchStates: {},
|
|
294
|
+
convergencePoints: []
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (options.includeValidationState) {
|
|
298
|
+
serializedState.validationState = {
|
|
299
|
+
disabledRules: [],
|
|
300
|
+
customRules: {},
|
|
301
|
+
validationErrors: []
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// Generate checksum if requested
|
|
305
|
+
if (options.generateChecksum) {
|
|
306
|
+
const stateToHash = { ...serializedState };
|
|
307
|
+
delete stateToHash.metadata.checksum; // Exclude checksum from hash
|
|
308
|
+
const stateString = JSON.stringify(stateToHash);
|
|
309
|
+
metadata.checksum = await this.generateChecksum(stateString);
|
|
310
|
+
serializedState.metadata = metadata;
|
|
311
|
+
}
|
|
312
|
+
// Performance tracking
|
|
313
|
+
if (this.enableProfiling) {
|
|
314
|
+
const duration = performance.now() - startTime;
|
|
315
|
+
PerfReporter_1.perf.record('custom', {
|
|
316
|
+
eventType: 'state-serialized',
|
|
317
|
+
duration,
|
|
318
|
+
stateSize: JSON.stringify(serializedState).length,
|
|
319
|
+
includePerformance: !!options.includePerformanceData,
|
|
320
|
+
includeFlowEvents: options.includeFlowEvents !== false
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return serializedState;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown serialization error';
|
|
327
|
+
if (this.enableProfiling) {
|
|
328
|
+
PerfReporter_1.perf.record('custom', {
|
|
329
|
+
eventType: 'state-serialization-failed',
|
|
330
|
+
error: errorMessage,
|
|
331
|
+
duration: performance.now() - startTime
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`Failed to save state: ${errorMessage}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Load engine state from serialized format
|
|
339
|
+
* @param serializedState - The serialized state to load
|
|
340
|
+
* @param options - Load options
|
|
341
|
+
* @returns Promise resolving to persistence result
|
|
342
|
+
*/
|
|
343
|
+
async loadState(serializedState, options = {}) {
|
|
344
|
+
const startTime = performance.now();
|
|
345
|
+
try {
|
|
346
|
+
// Validate serialized state structure
|
|
347
|
+
const validationResult = this.validateSerializedState(serializedState);
|
|
348
|
+
if (!validationResult.isValid) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: `Invalid serialized state: ${validationResult.errors.join(', ')}`,
|
|
352
|
+
warnings: validationResult.warnings
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// Check compatibility
|
|
356
|
+
if (!options.skipCompatibilityCheck) {
|
|
357
|
+
const compatibilityCheck = this.checkCompatibility(serializedState.metadata);
|
|
358
|
+
if (!compatibilityCheck.compatible) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
error: `Incompatible state version: ${compatibilityCheck.reason}`,
|
|
362
|
+
warnings: compatibilityCheck.suggestions
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Verify checksum if available and requested
|
|
367
|
+
if (options.verifyChecksum && serializedState.metadata.checksum) {
|
|
368
|
+
const isValid = await this.verifyChecksum(serializedState);
|
|
369
|
+
if (!isValid) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: 'Checksum verification failed - state may be corrupted'
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Story hash validation
|
|
377
|
+
if (this.generateStoryHash() !== serializedState.metadata.storyId) {
|
|
378
|
+
return {
|
|
379
|
+
success: false,
|
|
380
|
+
error: 'Story hash mismatch. The state belongs to a different narrative.'
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// Apply migration if needed
|
|
384
|
+
let stateToLoad = serializedState;
|
|
385
|
+
if (options.migrationFunction) {
|
|
386
|
+
stateToLoad = options.migrationFunction(serializedState);
|
|
387
|
+
}
|
|
388
|
+
// Load core state
|
|
389
|
+
this.state = this.deepCopy(stateToLoad.state);
|
|
390
|
+
// Restore optional data based on options
|
|
391
|
+
if (options.restoreFlowEvents && stateToLoad.flowEvents) {
|
|
392
|
+
this.activeFlowEvents = this.deepCopy(stateToLoad.flowEvents);
|
|
393
|
+
}
|
|
394
|
+
if (options.restorePerformanceState && stateToLoad.performanceState) {
|
|
395
|
+
this.performanceMode = stateToLoad.performanceState.performanceMode;
|
|
396
|
+
this.enableProfiling = stateToLoad.performanceState.enableProfiling;
|
|
397
|
+
// Background tasks would be restored here
|
|
398
|
+
}
|
|
399
|
+
if (options.restoreBranchingContext && stateToLoad.branchingContext && this.branchingEngine) {
|
|
400
|
+
// Restore branching context - would be implemented with actual branching engine
|
|
401
|
+
}
|
|
402
|
+
// Performance tracking
|
|
403
|
+
const duration = performance.now() - startTime;
|
|
404
|
+
if (this.enableProfiling) {
|
|
405
|
+
PerfReporter_1.perf.record('custom', {
|
|
406
|
+
eventType: 'state-loaded',
|
|
407
|
+
duration,
|
|
408
|
+
stateSize: JSON.stringify(stateToLoad).length,
|
|
409
|
+
restoredFlowEvents: !!options.restoreFlowEvents,
|
|
410
|
+
restoredPerformance: !!options.restorePerformanceState
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
data: {
|
|
416
|
+
size: JSON.stringify(stateToLoad).length,
|
|
417
|
+
duration,
|
|
418
|
+
checksum: stateToLoad.metadata.checksum
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown loading error';
|
|
424
|
+
const duration = performance.now() - startTime;
|
|
425
|
+
if (this.enableProfiling) {
|
|
426
|
+
PerfReporter_1.perf.record('custom', {
|
|
427
|
+
eventType: 'state-loading-failed',
|
|
428
|
+
error: errorMessage,
|
|
429
|
+
duration
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
success: false,
|
|
434
|
+
error: `Failed to load state: ${errorMessage}`,
|
|
435
|
+
data: { duration }
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Create a lightweight checkpoint of current state
|
|
441
|
+
* @param name - Optional checkpoint name
|
|
442
|
+
* @param options - Checkpoint options
|
|
443
|
+
* @returns Promise resolving to created checkpoint
|
|
444
|
+
*/
|
|
445
|
+
async createCheckpoint(name, options = {}) {
|
|
446
|
+
const checkpointId = this.generateCheckpointId();
|
|
447
|
+
const timestamp = new Date().toISOString();
|
|
448
|
+
const checkpoint = {
|
|
449
|
+
id: checkpointId,
|
|
450
|
+
name: name || `Checkpoint ${checkpointId.slice(-8)}`,
|
|
451
|
+
state: this.deepCopy(this.state),
|
|
452
|
+
timestamp,
|
|
453
|
+
description: options.includeMetadata ? `Auto-checkpoint at node ${this.state.currentNodeId}` : undefined,
|
|
454
|
+
tags: options.autoTags || [],
|
|
455
|
+
metadata: options.includeMetadata ? {
|
|
456
|
+
nodeTitle: this.getCurrentNode().text.slice(0, 50),
|
|
457
|
+
choiceCount: this.getCurrentNode().choices.length,
|
|
458
|
+
flagCount: Object.keys(this.state.flags).length,
|
|
459
|
+
historyLength: this.state.history.length
|
|
460
|
+
} : undefined
|
|
461
|
+
};
|
|
462
|
+
// Store checkpoint
|
|
463
|
+
this.checkpoints.set(checkpointId, checkpoint);
|
|
464
|
+
// Cleanup old checkpoints if needed
|
|
465
|
+
const maxCheckpoints = options.maxCheckpoints || this.maxCheckpoints;
|
|
466
|
+
if (this.checkpoints.size > maxCheckpoints) {
|
|
467
|
+
await this.cleanupCheckpoints(options.cleanupStrategy || 'lru', maxCheckpoints);
|
|
468
|
+
}
|
|
469
|
+
// Performance tracking
|
|
470
|
+
if (this.enableProfiling) {
|
|
471
|
+
PerfReporter_1.perf.record('custom', {
|
|
472
|
+
eventType: 'checkpoint-created',
|
|
473
|
+
checkpointId,
|
|
474
|
+
checkpointCount: this.checkpoints.size,
|
|
475
|
+
stateSize: JSON.stringify(checkpoint.state).length
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return checkpoint;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Restore engine state from a checkpoint
|
|
482
|
+
* @param checkpointId - ID of checkpoint to restore
|
|
483
|
+
* @returns Promise resolving to persistence result
|
|
484
|
+
*/
|
|
485
|
+
async restoreFromCheckpoint(checkpointId) {
|
|
486
|
+
const startTime = performance.now();
|
|
487
|
+
try {
|
|
488
|
+
const checkpoint = this.checkpoints.get(checkpointId);
|
|
489
|
+
if (!checkpoint) {
|
|
490
|
+
return {
|
|
491
|
+
success: false,
|
|
492
|
+
error: `Checkpoint not found: ${checkpointId}`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// Restore state
|
|
496
|
+
this.state = this.deepCopy(checkpoint.state);
|
|
497
|
+
const duration = performance.now() - startTime;
|
|
498
|
+
// Performance tracking
|
|
499
|
+
if (this.enableProfiling) {
|
|
500
|
+
PerfReporter_1.perf.record('custom', {
|
|
501
|
+
eventType: 'checkpoint-restored',
|
|
502
|
+
checkpointId,
|
|
503
|
+
duration,
|
|
504
|
+
stateSize: JSON.stringify(checkpoint.state).length
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
success: true,
|
|
509
|
+
data: {
|
|
510
|
+
size: JSON.stringify(checkpoint.state).length,
|
|
511
|
+
duration,
|
|
512
|
+
checksum: checkpointId
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown restore error';
|
|
518
|
+
const duration = performance.now() - startTime;
|
|
519
|
+
if (this.enableProfiling) {
|
|
520
|
+
PerfReporter_1.perf.record('custom', {
|
|
521
|
+
eventType: 'checkpoint-restore-failed',
|
|
522
|
+
checkpointId,
|
|
523
|
+
error: errorMessage,
|
|
524
|
+
duration
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
success: false,
|
|
529
|
+
error: `Failed to restore checkpoint: ${errorMessage}`,
|
|
530
|
+
data: { duration }
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Utility method for checking flag conditions
|
|
535
|
+
checkFlag(flagName, expectedValue) {
|
|
536
|
+
if (expectedValue === undefined) {
|
|
537
|
+
return this.state.flags[flagName] !== undefined;
|
|
538
|
+
}
|
|
539
|
+
return this.state.flags[flagName] === expectedValue;
|
|
540
|
+
}
|
|
541
|
+
// Get available choices (with conditional filtering)
|
|
542
|
+
getAvailableChoices() {
|
|
543
|
+
const currentNode = this.getCurrentNode();
|
|
544
|
+
const context = {
|
|
545
|
+
state: this.state,
|
|
546
|
+
timestamp: Date.now(),
|
|
547
|
+
customData: {}
|
|
548
|
+
};
|
|
549
|
+
return currentNode.choices.filter((choice) => {
|
|
550
|
+
// If no condition is specified, choice is always available
|
|
551
|
+
if (!choice.condition) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
// Evaluate the condition using the condition evaluator
|
|
556
|
+
return condition_1.conditionEvaluator.evaluate(choice.condition, context);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
// Log condition evaluation errors but don't block other choices
|
|
560
|
+
if (error instanceof condition_1.ConditionEvaluationError) {
|
|
561
|
+
console.warn(`[QNCE] Choice condition evaluation failed: ${error.message}`, {
|
|
562
|
+
choiceText: choice.text,
|
|
563
|
+
condition: choice.condition,
|
|
564
|
+
nodeId: this.state.currentNodeId
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
console.warn(`[QNCE] Unexpected error evaluating choice condition:`, error);
|
|
569
|
+
}
|
|
570
|
+
// Return false for invalid conditions (choice won't be shown)
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
// Performance and object pooling methods
|
|
576
|
+
/**
|
|
577
|
+
* Create a flow event using pooled objects for performance tracking
|
|
578
|
+
*/
|
|
579
|
+
createFlowEvent(fromNodeId, toNodeId, metadata) {
|
|
580
|
+
const flow = ObjectPool_1.poolManager.borrowFlow();
|
|
581
|
+
flow.initialize(fromNodeId, metadata);
|
|
582
|
+
flow.addTransition(fromNodeId, toNodeId);
|
|
583
|
+
return flow;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Record and manage flow events
|
|
587
|
+
*/
|
|
588
|
+
recordFlowEvent(flow) {
|
|
589
|
+
const flowEvent = {
|
|
590
|
+
id: `${flow.nodeId}-${Date.now()}`,
|
|
591
|
+
fromNodeId: flow.nodeId,
|
|
592
|
+
toNodeId: flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '',
|
|
593
|
+
timestamp: flow.timestamp,
|
|
594
|
+
metadata: flow.metadata
|
|
595
|
+
};
|
|
596
|
+
this.activeFlowEvents.push(flowEvent);
|
|
597
|
+
// Clean up old flow events (basic LRU-style cleanup)
|
|
598
|
+
if (this.activeFlowEvents.length > 10) {
|
|
599
|
+
this.activeFlowEvents.shift(); // Remove oldest
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Get current flow events (for debugging/monitoring)
|
|
604
|
+
*/
|
|
605
|
+
getActiveFlows() {
|
|
606
|
+
return [...this.activeFlowEvents];
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Get object pool statistics for performance monitoring
|
|
610
|
+
*/
|
|
611
|
+
getPoolStats() {
|
|
612
|
+
return this.performanceMode ? ObjectPool_1.poolManager.getAllStats() : null;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Return all pooled objects (cleanup method)
|
|
616
|
+
*/
|
|
617
|
+
cleanupPools() {
|
|
618
|
+
// Clear flow events (no pooled objects to return since we return them immediately)
|
|
619
|
+
this.activeFlowEvents.length = 0;
|
|
620
|
+
}
|
|
621
|
+
// S2-T2: Background ThreadPool Operations
|
|
622
|
+
/**
|
|
623
|
+
* Preload next possible nodes in background using ThreadPool
|
|
624
|
+
*/
|
|
625
|
+
async preloadNextNodes(choice) {
|
|
626
|
+
if (!this.performanceMode)
|
|
627
|
+
return;
|
|
628
|
+
const threadPool = (0, ThreadPool_1.getThreadPool)();
|
|
629
|
+
const currentNode = this.getCurrentNode();
|
|
630
|
+
const choicesToPreload = choice ? [choice] : currentNode.choices;
|
|
631
|
+
// Submit background jobs for each node to preload
|
|
632
|
+
for (const ch of choicesToPreload) {
|
|
633
|
+
threadPool.submitJob('cache-load', { nodeId: ch.nextNodeId }, 'normal').catch(error => {
|
|
634
|
+
if (this.enableProfiling) {
|
|
635
|
+
PerfReporter_1.perf.record('cache-miss', {
|
|
636
|
+
nodeId: ch.nextNodeId,
|
|
637
|
+
error: error.message,
|
|
638
|
+
jobType: 'preload'
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Write telemetry data in background using ThreadPool
|
|
646
|
+
*/
|
|
647
|
+
async backgroundTelemetryWrite(eventData) {
|
|
648
|
+
if (!this.performanceMode || !this.enableProfiling)
|
|
649
|
+
return;
|
|
650
|
+
const threadPool = (0, ThreadPool_1.getThreadPool)();
|
|
651
|
+
const telemetryData = {
|
|
652
|
+
timestamp: performance.now(),
|
|
653
|
+
sessionId: this.state.history[0], // Use first node as session ID
|
|
654
|
+
eventData,
|
|
655
|
+
stateSnapshot: {
|
|
656
|
+
nodeId: this.state.currentNodeId,
|
|
657
|
+
flagCount: Object.keys(this.state.flags).length,
|
|
658
|
+
historyLength: this.state.history.length
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
threadPool.submitJob('telemetry-write', telemetryData, 'low').catch(error => {
|
|
662
|
+
console.warn('[QNCE] Telemetry write failed:', error.message);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
// ================================
|
|
666
|
+
// Sprint #3: Advanced Branching System Integration
|
|
667
|
+
// ================================
|
|
668
|
+
/**
|
|
669
|
+
* Enable advanced branching capabilities for this story
|
|
670
|
+
* Integrates the QNCE Branching API with the core engine
|
|
671
|
+
*/
|
|
672
|
+
enableBranching(story) {
|
|
673
|
+
if (this.branchingEngine) {
|
|
674
|
+
console.warn('[QNCE] Branching already enabled for this engine instance');
|
|
675
|
+
return this.branchingEngine;
|
|
676
|
+
}
|
|
677
|
+
// Create branching engine with current state
|
|
678
|
+
this.branchingEngine = (0, branching_1.createBranchingEngine)(story, this.state);
|
|
679
|
+
if (this.enableProfiling) {
|
|
680
|
+
PerfReporter_1.perf.record('custom', {
|
|
681
|
+
eventType: 'branching-enabled',
|
|
682
|
+
storyId: story.id,
|
|
683
|
+
chapterCount: story.chapters.length,
|
|
684
|
+
performanceMode: this.performanceMode
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return this.branchingEngine;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get the branching engine if enabled
|
|
691
|
+
*/
|
|
692
|
+
getBranchingEngine() {
|
|
693
|
+
return this.branchingEngine;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Check if branching is enabled
|
|
697
|
+
*/
|
|
698
|
+
isBranchingEnabled() {
|
|
699
|
+
return !!this.branchingEngine;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Sync core engine state with branching engine
|
|
703
|
+
* Call this when core state changes to keep branching engine updated
|
|
704
|
+
*/
|
|
705
|
+
syncBranchingState() {
|
|
706
|
+
if (this.branchingEngine) {
|
|
707
|
+
// The branching engine maintains its own state copy
|
|
708
|
+
// This method could be extended to sync state changes
|
|
709
|
+
if (this.enableProfiling) {
|
|
710
|
+
PerfReporter_1.perf.record('custom', {
|
|
711
|
+
eventType: 'branching-state-synced',
|
|
712
|
+
currentNodeId: this.state.currentNodeId
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Disable branching and cleanup resources
|
|
719
|
+
*/
|
|
720
|
+
disableBranching() {
|
|
721
|
+
if (this.branchingEngine) {
|
|
722
|
+
this.branchingEngine = undefined;
|
|
723
|
+
if (this.enableProfiling) {
|
|
724
|
+
PerfReporter_1.perf.record('custom', {
|
|
725
|
+
eventType: 'branching-disabled'
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Background cache warming for story data
|
|
732
|
+
*/
|
|
733
|
+
async warmCache() {
|
|
734
|
+
if (!this.performanceMode)
|
|
735
|
+
return;
|
|
736
|
+
const threadPool = (0, ThreadPool_1.getThreadPool)();
|
|
737
|
+
// Cache all nodes in background
|
|
738
|
+
const cacheWarmData = {
|
|
739
|
+
nodeIds: this.storyData.nodes.map(n => n.id),
|
|
740
|
+
storyId: this.storyData.initialNodeId
|
|
741
|
+
};
|
|
742
|
+
if (this.enableProfiling) {
|
|
743
|
+
PerfReporter_1.perf.record('custom', {
|
|
744
|
+
eventType: 'cache-warm-start',
|
|
745
|
+
nodeCount: this.storyData.nodes.length
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
threadPool.submitJob('cache-load', cacheWarmData, 'low').catch(error => {
|
|
749
|
+
if (this.enableProfiling) {
|
|
750
|
+
PerfReporter_1.perf.record('custom', {
|
|
751
|
+
eventType: 'cache-warm-failed',
|
|
752
|
+
error: error.message
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
// Sprint 3.3: State persistence utility methods
|
|
758
|
+
/**
|
|
759
|
+
* Deep copy utility for state objects
|
|
760
|
+
* @param obj - Object to deep copy
|
|
761
|
+
* @returns Deep copied object
|
|
762
|
+
*/
|
|
763
|
+
deepCopy(obj) {
|
|
764
|
+
if (obj === null || typeof obj !== 'object') {
|
|
765
|
+
return obj;
|
|
766
|
+
}
|
|
767
|
+
if (obj instanceof Date) {
|
|
768
|
+
return new Date(obj.getTime());
|
|
769
|
+
}
|
|
770
|
+
if (obj instanceof Array) {
|
|
771
|
+
return obj.map(item => this.deepCopy(item));
|
|
772
|
+
}
|
|
773
|
+
if (typeof obj === 'object') {
|
|
774
|
+
const copy = {};
|
|
775
|
+
for (const key in obj) {
|
|
776
|
+
if (obj.hasOwnProperty(key)) {
|
|
777
|
+
copy[key] = this.deepCopy(obj[key]);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return copy;
|
|
781
|
+
}
|
|
782
|
+
return obj;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Generate a hash for the current story data
|
|
786
|
+
* @returns Story hash string
|
|
787
|
+
*/
|
|
788
|
+
generateStoryHash() {
|
|
789
|
+
const storyString = JSON.stringify({
|
|
790
|
+
initialNodeId: this.storyData.initialNodeId,
|
|
791
|
+
nodeCount: this.storyData.nodes.length,
|
|
792
|
+
nodeIds: this.storyData.nodes.map(n => n.id).sort()
|
|
793
|
+
});
|
|
794
|
+
// Simple hash function (in production, use crypto.subtle.digest)
|
|
795
|
+
let hash = 0;
|
|
796
|
+
for (let i = 0; i < storyString.length; i++) {
|
|
797
|
+
const char = storyString.charCodeAt(i);
|
|
798
|
+
hash = ((hash << 5) - hash) + char;
|
|
799
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
800
|
+
}
|
|
801
|
+
return hash.toString(16);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Generate a unique checkpoint ID
|
|
805
|
+
* @returns Unique checkpoint ID
|
|
806
|
+
*/
|
|
807
|
+
generateCheckpointId() {
|
|
808
|
+
const timestamp = Date.now();
|
|
809
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
810
|
+
return `checkpoint_${timestamp}_${random}`;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Generate checksum for data integrity
|
|
814
|
+
* @param data - Data to generate checksum for
|
|
815
|
+
* @returns Promise resolving to checksum string
|
|
816
|
+
*/
|
|
817
|
+
async generateChecksum(data) {
|
|
818
|
+
// Simple checksum implementation (in production, use crypto.subtle.digest)
|
|
819
|
+
let checksum = 0;
|
|
820
|
+
for (let i = 0; i < data.length; i++) {
|
|
821
|
+
checksum = ((checksum << 5) - checksum) + data.charCodeAt(i);
|
|
822
|
+
checksum = checksum & checksum;
|
|
823
|
+
}
|
|
824
|
+
return checksum.toString(16);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Verify checksum integrity
|
|
828
|
+
* @param serializedState - State with checksum to verify
|
|
829
|
+
* @returns Promise resolving to verification result
|
|
830
|
+
*/
|
|
831
|
+
async verifyChecksum(serializedState) {
|
|
832
|
+
const receivedChecksum = serializedState.metadata.checksum;
|
|
833
|
+
if (!receivedChecksum)
|
|
834
|
+
return false;
|
|
835
|
+
const stateToHash = { ...serializedState };
|
|
836
|
+
delete stateToHash.metadata.checksum;
|
|
837
|
+
const stateString = JSON.stringify(stateToHash);
|
|
838
|
+
const expectedChecksum = await this.generateChecksum(stateString);
|
|
839
|
+
return receivedChecksum === expectedChecksum;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Validate the structure of the serialized state object
|
|
843
|
+
* @param serializedState - State to validate
|
|
844
|
+
* @returns Validation result
|
|
845
|
+
*/
|
|
846
|
+
validateSerializedState(serializedState) {
|
|
847
|
+
const errors = [];
|
|
848
|
+
const warnings = [];
|
|
849
|
+
// Check required fields
|
|
850
|
+
if (!serializedState.state) {
|
|
851
|
+
errors.push('Missing state field');
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
if (!serializedState.state.currentNodeId) {
|
|
855
|
+
errors.push('Missing currentNodeId in state');
|
|
856
|
+
}
|
|
857
|
+
if (!serializedState.state.flags) {
|
|
858
|
+
warnings.push('Missing flags in state');
|
|
859
|
+
}
|
|
860
|
+
if (!serializedState.state.history) {
|
|
861
|
+
warnings.push('Missing history in state');
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (!serializedState.metadata) {
|
|
865
|
+
errors.push('Missing metadata field');
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
if (!serializedState.metadata.engineVersion) {
|
|
869
|
+
warnings.push('Missing engine version in metadata');
|
|
870
|
+
}
|
|
871
|
+
if (!serializedState.metadata.timestamp) {
|
|
872
|
+
warnings.push('Missing timestamp in metadata');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
isValid: errors.length === 0,
|
|
877
|
+
errors,
|
|
878
|
+
warnings
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Check version compatibility
|
|
883
|
+
* @param metadata - Serialization metadata
|
|
884
|
+
* @returns Compatibility check result
|
|
885
|
+
*/
|
|
886
|
+
checkCompatibility(metadata) {
|
|
887
|
+
const currentVersion = types_1.PERSISTENCE_VERSION;
|
|
888
|
+
const stateVersion = metadata.engineVersion;
|
|
889
|
+
if (!stateVersion) {
|
|
890
|
+
return {
|
|
891
|
+
compatible: false,
|
|
892
|
+
reason: 'Unknown state version',
|
|
893
|
+
suggestions: ['State may be from an older engine version']
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
if (stateVersion === currentVersion) {
|
|
897
|
+
return { compatible: true };
|
|
898
|
+
}
|
|
899
|
+
// Simple version comparison (in production, use semver)
|
|
900
|
+
const [currentMajor, currentMinor] = currentVersion.split('.').map(Number);
|
|
901
|
+
const [stateMajor, stateMinor] = stateVersion.split('.').map(Number);
|
|
902
|
+
if (stateMajor > currentMajor ||
|
|
903
|
+
(stateMajor === currentMajor && stateMinor > currentMinor)) {
|
|
904
|
+
return {
|
|
905
|
+
compatible: false,
|
|
906
|
+
reason: `State from newer engine version (${stateVersion} > ${currentVersion})`,
|
|
907
|
+
suggestions: ['Update the engine to a newer version']
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (stateMajor < currentMajor) {
|
|
911
|
+
return {
|
|
912
|
+
compatible: false,
|
|
913
|
+
reason: `State from incompatible major version (${stateVersion} vs ${currentVersion})`,
|
|
914
|
+
suggestions: ['Use migration function to upgrade state format']
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
// Minor version differences are usually compatible
|
|
918
|
+
return {
|
|
919
|
+
compatible: true,
|
|
920
|
+
suggestions: [`State from older minor version (${stateVersion})`]
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Cleanup old checkpoints based on strategy
|
|
925
|
+
* @param strategy - Cleanup strategy
|
|
926
|
+
* @param maxCheckpoints - Maximum number of checkpoints to keep
|
|
927
|
+
* @returns Promise resolving to number of cleaned checkpoints
|
|
928
|
+
*/
|
|
929
|
+
async cleanupCheckpoints(strategy, maxCheckpoints) {
|
|
930
|
+
const checkpoints = Array.from(this.checkpoints.entries());
|
|
931
|
+
if (checkpoints.length <= maxCheckpoints) {
|
|
932
|
+
return 0;
|
|
933
|
+
}
|
|
934
|
+
const toRemove = checkpoints.length - maxCheckpoints;
|
|
935
|
+
let checkpointsToDelete = [];
|
|
936
|
+
switch (strategy) {
|
|
937
|
+
case 'fifo':
|
|
938
|
+
// Remove oldest by creation order (assuming IDs contain timestamp)
|
|
939
|
+
checkpointsToDelete = checkpoints
|
|
940
|
+
.sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
|
|
941
|
+
.slice(0, toRemove)
|
|
942
|
+
.map(([id]) => id);
|
|
943
|
+
break;
|
|
944
|
+
case 'timestamp':
|
|
945
|
+
// Remove oldest by timestamp
|
|
946
|
+
checkpointsToDelete = checkpoints
|
|
947
|
+
.sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
|
|
948
|
+
.slice(0, toRemove)
|
|
949
|
+
.map(([id]) => id);
|
|
950
|
+
break;
|
|
951
|
+
case 'lru':
|
|
952
|
+
default:
|
|
953
|
+
// For now, same as FIFO (would need access tracking in production)
|
|
954
|
+
checkpointsToDelete = checkpoints
|
|
955
|
+
.sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
|
|
956
|
+
.slice(0, toRemove)
|
|
957
|
+
.map(([id]) => id);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
// Remove checkpoints
|
|
961
|
+
for (const id of checkpointsToDelete) {
|
|
962
|
+
this.checkpoints.delete(id);
|
|
963
|
+
}
|
|
964
|
+
if (this.enableProfiling) {
|
|
965
|
+
PerfReporter_1.perf.record('custom', {
|
|
966
|
+
eventType: 'checkpoints-cleaned',
|
|
967
|
+
strategy,
|
|
968
|
+
removedCount: checkpointsToDelete.length,
|
|
969
|
+
remainingCount: this.checkpoints.size
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
return checkpointsToDelete.length;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Get all checkpoints
|
|
976
|
+
* @returns Array of all checkpoints
|
|
977
|
+
*/
|
|
978
|
+
getCheckpoints() {
|
|
979
|
+
return Array.from(this.checkpoints.values());
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Get specific checkpoint by ID
|
|
983
|
+
* @param id - Checkpoint ID
|
|
984
|
+
* @returns Checkpoint or undefined
|
|
985
|
+
*/
|
|
986
|
+
getCheckpoint(id) {
|
|
987
|
+
return this.checkpoints.get(id);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Delete a checkpoint
|
|
991
|
+
* @param id - Checkpoint ID to delete
|
|
992
|
+
* @returns Whether checkpoint was deleted
|
|
993
|
+
*/
|
|
994
|
+
deleteCheckpoint(id) {
|
|
995
|
+
return this.checkpoints.delete(id);
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Enable/disable automatic checkpointing
|
|
999
|
+
* @param enabled - Whether to enable auto-checkpointing
|
|
1000
|
+
*/
|
|
1001
|
+
setAutoCheckpoint(enabled) {
|
|
1002
|
+
this.autoCheckpointEnabled = enabled;
|
|
1003
|
+
}
|
|
1004
|
+
// Sprint 3.5: Autosave and Undo/Redo Implementation
|
|
1005
|
+
/**
|
|
1006
|
+
* Configure autosave settings
|
|
1007
|
+
* @param config - Autosave configuration
|
|
1008
|
+
*/
|
|
1009
|
+
configureAutosave(config) {
|
|
1010
|
+
this.autosaveConfig = { ...this.autosaveConfig, ...config };
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Configure undo/redo settings
|
|
1014
|
+
* @param config - Undo/redo configuration
|
|
1015
|
+
*/
|
|
1016
|
+
configureUndoRedo(config) {
|
|
1017
|
+
this.undoRedoConfig = { ...this.undoRedoConfig, ...config };
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Push a state to the undo stack
|
|
1021
|
+
* @param state - State to save
|
|
1022
|
+
* @param action - Action that caused this state change
|
|
1023
|
+
* @param metadata - Optional metadata about the change
|
|
1024
|
+
*/
|
|
1025
|
+
pushToUndoStack(state, action, metadata) {
|
|
1026
|
+
const startTime = performance.now();
|
|
1027
|
+
const entry = {
|
|
1028
|
+
id: this.generateHistoryId(),
|
|
1029
|
+
state: this.deepCopy(state),
|
|
1030
|
+
timestamp: new Date().toISOString(),
|
|
1031
|
+
action,
|
|
1032
|
+
metadata
|
|
1033
|
+
};
|
|
1034
|
+
this.undoStack.push(entry);
|
|
1035
|
+
// Clear redo stack when new change is made
|
|
1036
|
+
this.redoStack = [];
|
|
1037
|
+
// Enforce max undo entries
|
|
1038
|
+
if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) {
|
|
1039
|
+
this.undoStack.shift(); // Remove oldest entry
|
|
1040
|
+
}
|
|
1041
|
+
// Performance tracking
|
|
1042
|
+
if (this.enableProfiling) {
|
|
1043
|
+
const duration = performance.now() - startTime;
|
|
1044
|
+
PerfReporter_1.perf.record('custom', {
|
|
1045
|
+
eventType: 'undo-stack-push',
|
|
1046
|
+
duration,
|
|
1047
|
+
undoCount: this.undoStack.length,
|
|
1048
|
+
action
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Undo the last operation
|
|
1054
|
+
* @returns Result of undo operation
|
|
1055
|
+
*/
|
|
1056
|
+
undo() {
|
|
1057
|
+
const startTime = performance.now();
|
|
1058
|
+
if (this.undoStack.length === 0) {
|
|
1059
|
+
return {
|
|
1060
|
+
success: false,
|
|
1061
|
+
error: 'No operations to undo',
|
|
1062
|
+
stackSizes: {
|
|
1063
|
+
undoCount: this.undoStack.length,
|
|
1064
|
+
redoCount: this.redoStack.length
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
// Save current state to redo stack
|
|
1070
|
+
const currentEntry = {
|
|
1071
|
+
id: this.generateHistoryId(),
|
|
1072
|
+
state: this.deepCopy(this.state),
|
|
1073
|
+
timestamp: new Date().toISOString(),
|
|
1074
|
+
action: 'redo-point'
|
|
1075
|
+
};
|
|
1076
|
+
this.redoStack.push(currentEntry);
|
|
1077
|
+
// Enforce max redo entries
|
|
1078
|
+
if (this.redoStack.length > this.undoRedoConfig.maxRedoEntries) {
|
|
1079
|
+
this.redoStack.shift(); // Remove oldest entry
|
|
1080
|
+
}
|
|
1081
|
+
// Restore previous state
|
|
1082
|
+
const entryToRestore = this.undoStack.pop();
|
|
1083
|
+
// Set flag to prevent triggering undo/redo tracking during restore
|
|
1084
|
+
this.isUndoRedoOperation = true;
|
|
1085
|
+
this.state = this.deepCopy(entryToRestore.state);
|
|
1086
|
+
this.isUndoRedoOperation = false;
|
|
1087
|
+
const duration = performance.now() - startTime;
|
|
1088
|
+
// Performance tracking
|
|
1089
|
+
if (this.enableProfiling) {
|
|
1090
|
+
PerfReporter_1.perf.record('custom', {
|
|
1091
|
+
eventType: 'undo-operation',
|
|
1092
|
+
duration,
|
|
1093
|
+
undoCount: this.undoStack.length,
|
|
1094
|
+
redoCount: this.redoStack.length
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
success: true,
|
|
1099
|
+
restoredState: this.deepCopy(this.state),
|
|
1100
|
+
entry: {
|
|
1101
|
+
id: entryToRestore.id,
|
|
1102
|
+
timestamp: entryToRestore.timestamp,
|
|
1103
|
+
action: entryToRestore.action,
|
|
1104
|
+
nodeId: entryToRestore.metadata?.nodeId
|
|
1105
|
+
},
|
|
1106
|
+
stackSizes: {
|
|
1107
|
+
undoCount: this.undoStack.length,
|
|
1108
|
+
redoCount: this.redoStack.length
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
this.isUndoRedoOperation = false; // Ensure flag is reset on error
|
|
1114
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown undo error';
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
error: `Undo failed: ${errorMessage}`,
|
|
1118
|
+
stackSizes: {
|
|
1119
|
+
undoCount: this.undoStack.length,
|
|
1120
|
+
redoCount: this.redoStack.length
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Redo the last undone operation
|
|
1127
|
+
* @returns Result of redo operation
|
|
1128
|
+
*/
|
|
1129
|
+
redo() {
|
|
1130
|
+
const startTime = performance.now();
|
|
1131
|
+
if (this.redoStack.length === 0) {
|
|
1132
|
+
return {
|
|
1133
|
+
success: false,
|
|
1134
|
+
error: 'No operations to redo',
|
|
1135
|
+
stackSizes: {
|
|
1136
|
+
undoCount: this.undoStack.length,
|
|
1137
|
+
redoCount: this.redoStack.length
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
// Save current state to undo stack
|
|
1143
|
+
const currentEntry = {
|
|
1144
|
+
id: this.generateHistoryId(),
|
|
1145
|
+
state: this.deepCopy(this.state),
|
|
1146
|
+
timestamp: new Date().toISOString(),
|
|
1147
|
+
action: 'undo-point'
|
|
1148
|
+
};
|
|
1149
|
+
this.undoStack.push(currentEntry);
|
|
1150
|
+
// Enforce max undo entries
|
|
1151
|
+
if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) {
|
|
1152
|
+
this.undoStack.shift(); // Remove oldest entry
|
|
1153
|
+
}
|
|
1154
|
+
// Restore redo state
|
|
1155
|
+
const entryToRestore = this.redoStack.pop();
|
|
1156
|
+
// Set flag to prevent triggering undo/redo tracking during restore
|
|
1157
|
+
this.isUndoRedoOperation = true;
|
|
1158
|
+
this.state = this.deepCopy(entryToRestore.state);
|
|
1159
|
+
this.isUndoRedoOperation = false;
|
|
1160
|
+
const duration = performance.now() - startTime;
|
|
1161
|
+
// Performance tracking
|
|
1162
|
+
if (this.enableProfiling) {
|
|
1163
|
+
PerfReporter_1.perf.record('custom', {
|
|
1164
|
+
eventType: 'redo-operation',
|
|
1165
|
+
duration,
|
|
1166
|
+
undoCount: this.undoStack.length,
|
|
1167
|
+
redoCount: this.redoStack.length
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
success: true,
|
|
1172
|
+
restoredState: this.deepCopy(this.state),
|
|
1173
|
+
entry: {
|
|
1174
|
+
id: entryToRestore.id,
|
|
1175
|
+
timestamp: entryToRestore.timestamp,
|
|
1176
|
+
action: entryToRestore.action,
|
|
1177
|
+
nodeId: entryToRestore.metadata?.nodeId
|
|
1178
|
+
},
|
|
1179
|
+
stackSizes: {
|
|
1180
|
+
undoCount: this.undoStack.length,
|
|
1181
|
+
redoCount: this.redoStack.length
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
this.isUndoRedoOperation = false; // Ensure flag is reset on error
|
|
1187
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown redo error';
|
|
1188
|
+
return {
|
|
1189
|
+
success: false,
|
|
1190
|
+
error: `Redo failed: ${errorMessage}`,
|
|
1191
|
+
stackSizes: {
|
|
1192
|
+
undoCount: this.undoStack.length,
|
|
1193
|
+
redoCount: this.redoStack.length
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Check if undo is available
|
|
1200
|
+
* @returns True if undo is possible
|
|
1201
|
+
*/
|
|
1202
|
+
canUndo() {
|
|
1203
|
+
return this.undoRedoConfig.enabled && this.undoStack.length > 0;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if redo is available
|
|
1207
|
+
* @returns True if redo is possible
|
|
1208
|
+
*/
|
|
1209
|
+
canRedo() {
|
|
1210
|
+
return this.undoRedoConfig.enabled && this.redoStack.length > 0;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Get the number of available undo operations
|
|
1214
|
+
* @returns Number of undo entries
|
|
1215
|
+
*/
|
|
1216
|
+
getUndoCount() {
|
|
1217
|
+
return this.undoStack.length;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Get the number of available redo operations
|
|
1221
|
+
* @returns Number of redo entries
|
|
1222
|
+
*/
|
|
1223
|
+
getRedoCount() {
|
|
1224
|
+
return this.redoStack.length;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Clear all undo/redo history
|
|
1228
|
+
*/
|
|
1229
|
+
clearHistory() {
|
|
1230
|
+
this.undoStack = [];
|
|
1231
|
+
this.redoStack = [];
|
|
1232
|
+
if (this.enableProfiling) {
|
|
1233
|
+
PerfReporter_1.perf.record('custom', {
|
|
1234
|
+
eventType: 'history-cleared'
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Get history summary for debugging
|
|
1240
|
+
* @returns Summary of undo/redo history
|
|
1241
|
+
*/
|
|
1242
|
+
getHistorySummary() {
|
|
1243
|
+
return {
|
|
1244
|
+
undoEntries: this.undoStack.map(entry => ({
|
|
1245
|
+
id: entry.id,
|
|
1246
|
+
timestamp: entry.timestamp,
|
|
1247
|
+
action: entry.action
|
|
1248
|
+
})),
|
|
1249
|
+
redoEntries: this.redoStack.map(entry => ({
|
|
1250
|
+
id: entry.id,
|
|
1251
|
+
timestamp: entry.timestamp,
|
|
1252
|
+
action: entry.action
|
|
1253
|
+
}))
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Trigger an autosave operation
|
|
1258
|
+
* @param trigger - What triggered the autosave
|
|
1259
|
+
* @param metadata - Optional metadata about the trigger
|
|
1260
|
+
* @returns Promise resolving to autosave result
|
|
1261
|
+
*/
|
|
1262
|
+
async triggerAutosave(trigger, metadata) {
|
|
1263
|
+
const startTime = performance.now();
|
|
1264
|
+
// Check if autosave is enabled
|
|
1265
|
+
if (!this.autosaveConfig.enabled) {
|
|
1266
|
+
return { success: false, error: 'Autosave is disabled' };
|
|
1267
|
+
}
|
|
1268
|
+
// Check throttling
|
|
1269
|
+
const now = performance.now();
|
|
1270
|
+
if (now - this.lastAutosaveTime < this.autosaveConfig.throttleMs) {
|
|
1271
|
+
return {
|
|
1272
|
+
success: false,
|
|
1273
|
+
error: 'Autosave throttled',
|
|
1274
|
+
trigger
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
// Create autosave checkpoint
|
|
1279
|
+
const checkpointName = `autosave-${trigger}-${Date.now()}`;
|
|
1280
|
+
const checkpoint = await this.createCheckpoint(checkpointName, {
|
|
1281
|
+
includeMetadata: this.autosaveConfig.includeMetadata,
|
|
1282
|
+
autoTags: ['autosave', trigger],
|
|
1283
|
+
maxCheckpoints: this.autosaveConfig.maxEntries
|
|
1284
|
+
});
|
|
1285
|
+
this.lastAutosaveTime = now;
|
|
1286
|
+
const duration = performance.now() - startTime;
|
|
1287
|
+
// Performance tracking
|
|
1288
|
+
if (this.enableProfiling) {
|
|
1289
|
+
PerfReporter_1.perf.record('custom', {
|
|
1290
|
+
eventType: 'autosave-completed',
|
|
1291
|
+
duration,
|
|
1292
|
+
trigger,
|
|
1293
|
+
checkpointId: checkpoint.id
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
success: true,
|
|
1298
|
+
checkpointId: checkpoint.id,
|
|
1299
|
+
trigger,
|
|
1300
|
+
duration,
|
|
1301
|
+
size: JSON.stringify(checkpoint.state).length
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown autosave error';
|
|
1306
|
+
const duration = performance.now() - startTime;
|
|
1307
|
+
if (this.enableProfiling) {
|
|
1308
|
+
PerfReporter_1.perf.record('custom', {
|
|
1309
|
+
eventType: 'autosave-failed',
|
|
1310
|
+
duration,
|
|
1311
|
+
trigger,
|
|
1312
|
+
error: errorMessage
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
success: false,
|
|
1317
|
+
error: `Autosave failed: ${errorMessage}`,
|
|
1318
|
+
trigger,
|
|
1319
|
+
duration
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Manually trigger an autosave
|
|
1325
|
+
* @param metadata - Optional metadata
|
|
1326
|
+
* @returns Promise resolving to autosave result
|
|
1327
|
+
*/
|
|
1328
|
+
async manualAutosave(metadata) {
|
|
1329
|
+
return this.triggerAutosave('manual', metadata);
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Generate a unique history entry ID
|
|
1333
|
+
* @returns Unique ID string
|
|
1334
|
+
*/
|
|
1335
|
+
generateHistoryId() {
|
|
1336
|
+
return `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1337
|
+
}
|
|
1338
|
+
// Sprint 3.4: Conditional Choice Display API
|
|
1339
|
+
/**
|
|
1340
|
+
* Set a custom condition evaluator function for complex choice logic
|
|
1341
|
+
* @param evaluator - Custom evaluator function
|
|
1342
|
+
*/
|
|
1343
|
+
setConditionEvaluator(evaluator) {
|
|
1344
|
+
condition_1.conditionEvaluator.setCustomEvaluator(evaluator);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Clear the custom condition evaluator
|
|
1348
|
+
*/
|
|
1349
|
+
clearConditionEvaluator() {
|
|
1350
|
+
condition_1.conditionEvaluator.clearCustomEvaluator();
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Validate a condition expression without evaluating it
|
|
1354
|
+
* @param expression - Condition expression to validate
|
|
1355
|
+
* @returns Validation result
|
|
1356
|
+
*/
|
|
1357
|
+
validateCondition(expression) {
|
|
1358
|
+
return condition_1.conditionEvaluator.validateExpression(expression);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Get flags referenced in a condition expression
|
|
1362
|
+
* @param expression - Condition expression to analyze
|
|
1363
|
+
* @returns Array of flag names referenced
|
|
1364
|
+
*/
|
|
1365
|
+
getConditionFlags(expression) {
|
|
1366
|
+
return condition_1.conditionEvaluator.getReferencedFlags(expression);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
exports.QNCEEngine = QNCEEngine;
|
|
1370
|
+
/**
|
|
1371
|
+
* Factory function to create a QNCE engine instance
|
|
1372
|
+
*/
|
|
1373
|
+
function createQNCEEngine(storyData, initialState, performanceMode = false) {
|
|
1374
|
+
return new QNCEEngine(storyData, initialState, performanceMode);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Load story data from JSON
|
|
1378
|
+
*/
|
|
1379
|
+
function loadStoryData(jsonData) {
|
|
1380
|
+
// Add validation here in the future
|
|
1381
|
+
return jsonData;
|
|
1382
|
+
}
|