qnce-engine 1.3.1 → 1.4.1
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 +136 -2
- package/dist/adapters/contracts.d.ts +1 -1
- package/dist/adapters/contracts.d.ts.map +1 -1
- package/dist/adapters/storage/MockAdapters.d.ts +89 -0
- package/dist/adapters/storage/MockAdapters.d.ts.map +1 -0
- package/dist/adapters/storage/MockAdapters.js +109 -0
- package/dist/adapters/storage/MockAdapters.js.map +1 -0
- package/dist/adapters/story/CustomJSONAdapter.d.ts +1 -1
- package/dist/adapters/story/CustomJSONAdapter.d.ts.map +1 -1
- package/dist/adapters/story/CustomJSONAdapter.js +20 -14
- package/dist/adapters/story/CustomJSONAdapter.js.map +1 -1
- package/dist/adapters/story/InkAdapter.d.ts +1 -1
- package/dist/adapters/story/InkAdapter.d.ts.map +1 -1
- package/dist/adapters/story/InkAdapter.js +7 -10
- package/dist/adapters/story/InkAdapter.js.map +1 -1
- package/dist/adapters/story/TwisonAdapter.d.ts +1 -1
- package/dist/adapters/story/TwisonAdapter.d.ts.map +1 -1
- package/dist/adapters/story/TwisonAdapter.js +2 -2
- package/dist/adapters/story/TwisonAdapter.js.map +1 -1
- package/dist/cli/audit.js +1 -0
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/import.d.ts.map +1 -1
- package/dist/cli/import.js +56 -20
- package/dist/cli/import.js.map +1 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/perf.d.ts.map +1 -1
- package/dist/cli/perf.js +30 -0
- package/dist/cli/perf.js.map +1 -1
- package/dist/cli/play.d.ts.map +1 -1
- package/dist/cli/play.js +135 -60
- package/dist/cli/play.js.map +1 -1
- package/dist/engine/condition.d.ts +25 -3
- package/dist/engine/condition.d.ts.map +1 -1
- package/dist/engine/condition.js +358 -64
- package/dist/engine/condition.js.map +1 -1
- package/dist/engine/core.d.ts +109 -3
- package/dist/engine/core.d.ts.map +1 -1
- package/dist/engine/core.js +486 -36
- package/dist/engine/core.js.map +1 -1
- package/dist/engine/demo-story.d.ts +1 -0
- package/dist/engine/demo-story.d.ts.map +1 -1
- package/dist/engine/demo-story.js +1 -0
- package/dist/engine/demo-story.js.map +1 -1
- package/dist/engine/error-factory.d.ts +86 -0
- package/dist/engine/error-factory.d.ts.map +1 -0
- package/dist/engine/error-factory.js +87 -0
- package/dist/engine/error-factory.js.map +1 -0
- package/dist/engine/errors.d.ts +3 -0
- package/dist/engine/errors.d.ts.map +1 -1
- package/dist/engine/errors.js +3 -0
- package/dist/engine/errors.js.map +1 -1
- package/dist/engine/validation.js +1 -1
- package/dist/engine/validation.js.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/react.d.ts +20 -3
- package/dist/integrations/react.d.ts.map +1 -1
- package/dist/integrations/react.js +15 -2
- package/dist/integrations/react.js.map +1 -1
- package/dist/narrative/branching/models.d.ts +1 -0
- package/dist/narrative/branching/models.d.ts.map +1 -1
- package/dist/narrative/branching/models.js.map +1 -1
- package/dist/performance/AdaptiveControllers.d.ts +51 -0
- package/dist/performance/AdaptiveControllers.d.ts.map +1 -0
- package/dist/performance/AdaptiveControllers.js +87 -0
- package/dist/performance/AdaptiveControllers.js.map +1 -0
- package/dist/performance/ContextPool.d.ts +1 -0
- package/dist/performance/ContextPool.d.ts.map +1 -0
- package/dist/performance/ContextPool.js +2 -0
- package/dist/performance/ContextPool.js.map +1 -0
- package/dist/performance/HotReloadDelta.d.ts +5 -0
- package/dist/performance/HotReloadDelta.d.ts.map +1 -1
- package/dist/performance/HotReloadDelta.js +18 -4
- package/dist/performance/HotReloadDelta.js.map +1 -1
- package/dist/performance/ObjectPool.d.ts.map +1 -1
- package/dist/performance/ObjectPool.js +4 -1
- package/dist/performance/ObjectPool.js.map +1 -1
- package/dist/performance/PerfReporter.d.ts +69 -0
- package/dist/performance/PerfReporter.d.ts.map +1 -1
- package/dist/performance/PerfReporter.js +314 -28
- package/dist/performance/PerfReporter.js.map +1 -1
- package/dist/performance/ThreadPool.d.ts +5 -0
- package/dist/performance/ThreadPool.d.ts.map +1 -1
- package/dist/performance/ThreadPool.js +33 -5
- package/dist/performance/ThreadPool.js.map +1 -1
- package/dist/persistence/StorageAdapters.d.ts.map +1 -1
- package/dist/persistence/StorageAdapters.js +13 -19
- package/dist/persistence/StorageAdapters.js.map +1 -1
- package/dist/qnce-engine.d.ts +2258 -0
- package/dist/quantum/entangler.d.ts +13 -0
- package/dist/quantum/entangler.d.ts.map +1 -0
- package/dist/quantum/entangler.js +24 -0
- package/dist/quantum/entangler.js.map +1 -0
- package/dist/quantum/flags.d.ts +24 -0
- package/dist/quantum/flags.d.ts.map +1 -0
- package/dist/quantum/flags.js +29 -0
- package/dist/quantum/flags.js.map +1 -0
- package/dist/quantum/integration.d.ts +26 -0
- package/dist/quantum/integration.d.ts.map +1 -0
- package/dist/quantum/integration.js +38 -0
- package/dist/quantum/integration.js.map +1 -0
- package/dist/quantum/measurement.d.ts +22 -0
- package/dist/quantum/measurement.d.ts.map +1 -0
- package/dist/quantum/measurement.js +44 -0
- package/dist/quantum/measurement.js.map +1 -0
- package/dist/quantum/phase.d.ts +20 -0
- package/dist/quantum/phase.d.ts.map +1 -0
- package/dist/quantum/phase.js +22 -0
- package/dist/quantum/phase.js.map +1 -0
- package/dist/quantum/types.d.ts +12 -0
- package/dist/quantum/types.d.ts.map +1 -0
- package/dist/quantum/types.js +5 -0
- package/dist/quantum/types.js.map +1 -0
- package/dist/schemas/validateStoryData.d.ts.map +1 -1
- package/dist/schemas/validateStoryData.js +8 -2
- package/dist/schemas/validateStoryData.js.map +1 -1
- package/dist/telemetry/core.d.ts +88 -0
- package/dist/telemetry/core.d.ts.map +1 -0
- package/dist/telemetry/core.js +303 -0
- package/dist/telemetry/core.js.map +1 -0
- package/dist/telemetry/types.d.ts +76 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/telemetry/types.js +4 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.js +10 -13
- package/dist/ui/__tests__/AutosaveIndicator.test.js.map +1 -1
- package/dist/ui/__tests__/UndoRedoControls.test.js +7 -9
- package/dist/ui/__tests__/UndoRedoControls.test.js.map +1 -1
- package/dist/ui/components/AutosaveIndicator.d.ts +1 -0
- package/dist/ui/components/AutosaveIndicator.d.ts.map +1 -1
- package/dist/ui/components/AutosaveIndicator.js +2 -1
- package/dist/ui/components/AutosaveIndicator.js.map +1 -1
- package/dist/ui/components/UndoRedoControls.d.ts +1 -0
- package/dist/ui/components/UndoRedoControls.d.ts.map +1 -1
- package/dist/ui/components/UndoRedoControls.js +1 -0
- package/dist/ui/components/UndoRedoControls.js.map +1 -1
- package/dist/ui/hooks/useKeyboardShortcuts.d.ts +1 -0
- package/dist/ui/hooks/useKeyboardShortcuts.d.ts.map +1 -1
- package/dist/ui/hooks/useKeyboardShortcuts.js +17 -5
- package/dist/ui/hooks/useKeyboardShortcuts.js.map +1 -1
- package/dist/ui/types.d.ts +6 -0
- package/dist/ui/types.d.ts.map +1 -1
- package/dist/ui/types.js +1 -0
- package/dist/ui/types.js.map +1 -1
- package/dist/utils/debug-logger.d.ts +20 -0
- package/dist/utils/debug-logger.d.ts.map +1 -0
- package/dist/utils/debug-logger.js +24 -0
- package/dist/utils/debug-logger.js.map +1 -0
- package/dist/utils/hot-profiler.d.ts +21 -0
- package/dist/utils/hot-profiler.d.ts.map +1 -0
- package/dist/utils/hot-profiler.js +36 -0
- package/dist/utils/hot-profiler.js.map +1 -0
- package/dist/utils/intern.d.ts +11 -0
- package/dist/utils/intern.d.ts.map +1 -0
- package/dist/utils/intern.js +45 -0
- package/dist/utils/intern.js.map +1 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +115 -0
- package/dist/utils/logger.js.map +1 -0
- package/docs/PERFORMANCE.md +330 -0
- package/examples/fluent-builder-prototype.ts +71 -0
- package/examples/quantum-integration-demo.ts +51 -0
- package/package.json +25 -9
package/dist/engine/core.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// QNCE Core Engine - Framework Agnostic
|
|
3
3
|
// Quantum Narrative Convergence Engine
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
21
|
+
if (mod && mod.__esModule) return mod;
|
|
22
|
+
var result = {};
|
|
23
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
24
|
+
__setModuleDefault(result, mod);
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
4
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
28
|
exports.QNCEEngine = exports.ChoiceValidationError = exports.QNCENavigationError = void 0;
|
|
6
29
|
exports.createQNCEEngine = createQNCEEngine;
|
|
@@ -19,8 +42,13 @@ Object.defineProperty(exports, "QNCENavigationError", { enumerable: true, get: f
|
|
|
19
42
|
Object.defineProperty(exports, "ChoiceValidationError", { enumerable: true, get: function () { return errors_2.ChoiceValidationError; } });
|
|
20
43
|
// State persistence imports - Sprint 3.3
|
|
21
44
|
const types_1 = require("./types");
|
|
45
|
+
const logger_1 = require("../utils/logger");
|
|
22
46
|
// Conditional choice evaluator - Sprint 3.4
|
|
23
47
|
const condition_1 = require("./condition");
|
|
48
|
+
const error_factory_1 = require("./error-factory");
|
|
49
|
+
const debug_logger_1 = require("../utils/debug-logger");
|
|
50
|
+
const hot_profiler_1 = require("../utils/hot-profiler");
|
|
51
|
+
const intern_1 = require("../utils/intern");
|
|
24
52
|
// Demo narrative data moved to demo-story.ts
|
|
25
53
|
function findNode(nodes, id) {
|
|
26
54
|
const node = nodes.find(n => n.id === id);
|
|
@@ -32,18 +60,45 @@ function findNode(nodes, id) {
|
|
|
32
60
|
* QNCE Engine - Core narrative state management
|
|
33
61
|
* Framework agnostic implementation with object pooling integration
|
|
34
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Main engine class.
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
35
67
|
class QNCEEngine {
|
|
36
68
|
state;
|
|
37
69
|
storyData; // Made public for hot-reload compatibility
|
|
38
70
|
activeFlowEvents = [];
|
|
39
71
|
performanceMode = false;
|
|
40
72
|
enableProfiling = false;
|
|
73
|
+
debugMode = false;
|
|
41
74
|
branchingEngine;
|
|
42
75
|
choiceValidator; // Sprint 3.2: Choice validation
|
|
43
76
|
// Sprint 3.3: State persistence and checkpoints
|
|
44
77
|
checkpoints = new Map();
|
|
45
78
|
maxCheckpoints = 50;
|
|
46
79
|
autoCheckpointEnabled = false;
|
|
80
|
+
// Flag pooling to reduce duplicate string allocations
|
|
81
|
+
flagKeyPool = new Map();
|
|
82
|
+
flagValuePool = new Map();
|
|
83
|
+
internFlagKey(raw) {
|
|
84
|
+
let pooled = this.flagKeyPool.get(raw);
|
|
85
|
+
if (!pooled) {
|
|
86
|
+
this.flagKeyPool.set(raw, raw);
|
|
87
|
+
pooled = raw;
|
|
88
|
+
}
|
|
89
|
+
return pooled;
|
|
90
|
+
}
|
|
91
|
+
internFlagValue(val) {
|
|
92
|
+
if (typeof val === 'string' && val.length <= 64) { // limit to modest strings
|
|
93
|
+
let pooled = this.flagValuePool.get(val);
|
|
94
|
+
if (!pooled) {
|
|
95
|
+
this.flagValuePool.set(val, val);
|
|
96
|
+
pooled = val;
|
|
97
|
+
}
|
|
98
|
+
return pooled;
|
|
99
|
+
}
|
|
100
|
+
return val;
|
|
101
|
+
}
|
|
47
102
|
// Sprint 3.3: State persistence properties
|
|
48
103
|
checkpointManager;
|
|
49
104
|
// Sprint 3.5: Autosave and Undo/Redo properties
|
|
@@ -67,12 +122,32 @@ class QNCEEngine {
|
|
|
67
122
|
lastAutosaveTime = 0;
|
|
68
123
|
isUndoRedoOperation = false;
|
|
69
124
|
storageAdapter;
|
|
125
|
+
// Storage retry policy (initial simple implementation)
|
|
126
|
+
storageRetryPolicy = {
|
|
127
|
+
maxAttempts: 3,
|
|
128
|
+
baseDelayMs: 5,
|
|
129
|
+
factor: 2,
|
|
130
|
+
maxDelayMs: 100,
|
|
131
|
+
jitter: true
|
|
132
|
+
};
|
|
133
|
+
// Sprint 4.1: Telemetry support
|
|
134
|
+
telemetry;
|
|
135
|
+
defaultTelemetryCtx;
|
|
136
|
+
// Hooks
|
|
137
|
+
preChoiceHooks = [];
|
|
138
|
+
postChoiceHooks = [];
|
|
139
|
+
hookCounter = 0;
|
|
140
|
+
logger = (0, logger_1.createLogger)({ level: 'warn' });
|
|
141
|
+
engineOptions;
|
|
142
|
+
minimalTelemetry = false;
|
|
70
143
|
get flags() {
|
|
71
144
|
return this.state.flags;
|
|
72
145
|
}
|
|
73
146
|
get history() {
|
|
74
147
|
return this.state.history;
|
|
75
148
|
}
|
|
149
|
+
/** Access the engine logger (public read-only) */
|
|
150
|
+
getLogger() { return this.logger; }
|
|
76
151
|
get isComplete() {
|
|
77
152
|
try {
|
|
78
153
|
return this.getCurrentNode().choices.length === 0;
|
|
@@ -81,10 +156,16 @@ class QNCEEngine {
|
|
|
81
156
|
return true; // If current node not found, consider it complete
|
|
82
157
|
}
|
|
83
158
|
}
|
|
84
|
-
constructor(storyData, initialState, performanceMode = false, threadPoolConfig) {
|
|
159
|
+
constructor(storyData, initialState, performanceMode = false, threadPoolConfig, options) {
|
|
85
160
|
this.storyData = storyData;
|
|
86
161
|
this.performanceMode = performanceMode;
|
|
87
162
|
this.enableProfiling = performanceMode; // Enable profiling with performance mode
|
|
163
|
+
if (this.performanceMode) {
|
|
164
|
+
try {
|
|
165
|
+
condition_1.conditionEvaluator.enablePooling(true);
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
}
|
|
88
169
|
// Initialize choice validator (Sprint 3.2)
|
|
89
170
|
this.choiceValidator = (0, validation_1.createChoiceValidator)();
|
|
90
171
|
// Initialize ThreadPool if in performance mode
|
|
@@ -96,39 +177,138 @@ class QNCEEngine {
|
|
|
96
177
|
flags: initialState?.flags || {},
|
|
97
178
|
history: initialState?.history || [storyData.initialNodeId],
|
|
98
179
|
};
|
|
180
|
+
// Telemetry wiring
|
|
181
|
+
this.telemetry = options?.telemetry;
|
|
182
|
+
if (options?.logger)
|
|
183
|
+
this.logger = options.logger;
|
|
184
|
+
this.engineOptions = options;
|
|
185
|
+
this.minimalTelemetry = options?.minimalTelemetry ?? false;
|
|
186
|
+
if (this.telemetry) {
|
|
187
|
+
const sessionId = options?.sessionId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
188
|
+
this.defaultTelemetryCtx = {
|
|
189
|
+
sessionId,
|
|
190
|
+
storyId: undefined,
|
|
191
|
+
appVersion: options?.appVersion,
|
|
192
|
+
engineVersion: types_1.PERSISTENCE_VERSION,
|
|
193
|
+
env: options?.env
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
this.telemetry.emit({ type: 'session.start', payload: { initialNodeId: this.state.currentNodeId }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Dispose engine resources (telemetry, performance reporter, background thread pool in perf mode)
|
|
203
|
+
* @public
|
|
204
|
+
*/
|
|
205
|
+
async dispose() {
|
|
206
|
+
try {
|
|
207
|
+
if (this.telemetry)
|
|
208
|
+
await this.telemetry.dispose();
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
try {
|
|
212
|
+
(0, PerfReporter_1.getPerfReporter)().dispose?.();
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
215
|
+
if (this.performanceMode) {
|
|
216
|
+
try {
|
|
217
|
+
const { shutdownThreadPool } = await Promise.resolve().then(() => __importStar(require('../performance/ThreadPool')));
|
|
218
|
+
await shutdownThreadPool();
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
}
|
|
99
222
|
}
|
|
100
223
|
// Lane B: StorageAdapter integration
|
|
101
224
|
/** Attach a storage adapter for persistence */
|
|
102
225
|
attachStorageAdapter(adapter) {
|
|
103
226
|
this.storageAdapter = adapter;
|
|
104
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Configure storage retry policy for transient save failures.
|
|
230
|
+
* @param policy - Partial policy overriding defaults. Set maxAttempts to 1 to disable retries.
|
|
231
|
+
* @beta
|
|
232
|
+
*/
|
|
233
|
+
setStorageRetryPolicy(policy) {
|
|
234
|
+
this.storageRetryPolicy = { ...this.storageRetryPolicy, ...policy };
|
|
235
|
+
}
|
|
236
|
+
async sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
237
|
+
computeBackoff(attempt) {
|
|
238
|
+
const { baseDelayMs, factor, maxDelayMs, jitter } = this.storageRetryPolicy;
|
|
239
|
+
const raw = Math.min(baseDelayMs * Math.pow(factor, attempt - 1), maxDelayMs);
|
|
240
|
+
if (!jitter)
|
|
241
|
+
return raw;
|
|
242
|
+
const spread = raw * 0.5;
|
|
243
|
+
return Math.max(0, raw - spread + Math.random() * spread);
|
|
244
|
+
}
|
|
105
245
|
/** Save current state through the attached storage adapter */
|
|
106
246
|
async saveToStorage(key, options = {}) {
|
|
107
247
|
if (!this.storageAdapter)
|
|
108
248
|
return { success: false, error: 'No storage adapter attached' };
|
|
249
|
+
const t0 = Date.now();
|
|
109
250
|
const serialized = await this.saveState(options);
|
|
110
|
-
|
|
251
|
+
const policy = this.storageRetryPolicy;
|
|
252
|
+
let attempt = 0;
|
|
253
|
+
let lastResult;
|
|
254
|
+
while (true) {
|
|
255
|
+
attempt++;
|
|
256
|
+
try {
|
|
257
|
+
lastResult = await this.storageAdapter.save(key, serialized, options);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
lastResult = { success: false, error: err?.message || 'unknown-error' };
|
|
261
|
+
}
|
|
262
|
+
if (lastResult.success || attempt >= policy.maxAttempts || policy.maxAttempts <= 1)
|
|
263
|
+
break;
|
|
264
|
+
// Only retry if explicitly failed (success === false)
|
|
265
|
+
const delay = this.computeBackoff(attempt);
|
|
266
|
+
await this.sleep(delay);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
this.telemetry?.emit({ type: 'storage.op', payload: { op: 'save', key, ms: Date.now() - t0, ok: !!lastResult?.success, attempts: attempt, maxAttempts: policy.maxAttempts }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
270
|
+
}
|
|
271
|
+
catch { }
|
|
272
|
+
return lastResult || { success: false, error: 'unknown' };
|
|
111
273
|
}
|
|
112
274
|
/** Load state from the attached storage adapter */
|
|
113
275
|
async loadFromStorage(key, options = {}) {
|
|
114
276
|
if (!this.storageAdapter)
|
|
115
277
|
return { success: false, error: 'No storage adapter attached' };
|
|
278
|
+
const t0 = Date.now();
|
|
116
279
|
const serialized = await this.storageAdapter.load(key, options);
|
|
117
280
|
if (!serialized)
|
|
118
281
|
return { success: false, error: `No data for key: ${key}` };
|
|
119
|
-
|
|
282
|
+
const res = await this.loadState(serialized, options);
|
|
283
|
+
try {
|
|
284
|
+
this.telemetry?.emit({ type: 'storage.op', payload: { op: 'load', key, ms: Date.now() - t0, ok: !!res?.success }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
287
|
+
return res;
|
|
120
288
|
}
|
|
121
289
|
/** Delete a stored state via the adapter */
|
|
122
290
|
async deleteFromStorage(key) {
|
|
123
291
|
if (!this.storageAdapter)
|
|
124
292
|
return false;
|
|
125
|
-
|
|
293
|
+
const t0 = Date.now();
|
|
294
|
+
const ok = await this.storageAdapter.delete(key);
|
|
295
|
+
try {
|
|
296
|
+
this.telemetry?.emit({ type: 'storage.op', payload: { op: 'delete', key, ms: Date.now() - t0, ok }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
return ok;
|
|
126
300
|
}
|
|
127
301
|
/** List keys from the adapter */
|
|
128
302
|
async listStorageKeys() {
|
|
129
303
|
if (!this.storageAdapter)
|
|
130
304
|
return [];
|
|
131
|
-
|
|
305
|
+
const t0 = Date.now();
|
|
306
|
+
const keys = await this.storageAdapter.list();
|
|
307
|
+
try {
|
|
308
|
+
this.telemetry?.emit({ type: 'storage.op', payload: { op: 'list', count: keys.length, ms: Date.now() - t0, ok: true }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
309
|
+
}
|
|
310
|
+
catch { }
|
|
311
|
+
return keys;
|
|
132
312
|
}
|
|
133
313
|
/** Check if a key exists via the adapter */
|
|
134
314
|
async storageExists(key) {
|
|
@@ -140,7 +320,13 @@ class QNCEEngine {
|
|
|
140
320
|
async clearStorage() {
|
|
141
321
|
if (!this.storageAdapter)
|
|
142
322
|
return false;
|
|
143
|
-
|
|
323
|
+
const t0 = Date.now();
|
|
324
|
+
const ok = await this.storageAdapter.clear();
|
|
325
|
+
try {
|
|
326
|
+
this.telemetry?.emit({ type: 'storage.op', payload: { op: 'clear', ms: Date.now() - t0, ok }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
return ok;
|
|
144
330
|
}
|
|
145
331
|
/** Get storage statistics from the adapter */
|
|
146
332
|
async getStorageStats() {
|
|
@@ -152,7 +338,7 @@ class QNCEEngine {
|
|
|
152
338
|
/**
|
|
153
339
|
* Navigate directly to a specific node by ID
|
|
154
340
|
* @param nodeId - The ID of the node to navigate to
|
|
155
|
-
|
|
341
|
+
* @throws \{QNCENavigationError\} When nodeId is invalid or not found
|
|
156
342
|
*/
|
|
157
343
|
goToNodeById(nodeId) {
|
|
158
344
|
// Validate node exists
|
|
@@ -210,7 +396,12 @@ class QNCEEngine {
|
|
|
210
396
|
choices: pooledNode.choices
|
|
211
397
|
};
|
|
212
398
|
}
|
|
213
|
-
|
|
399
|
+
const node = findNode(this.storyData.nodes, this.state.currentNodeId);
|
|
400
|
+
try {
|
|
401
|
+
this.telemetry?.emit({ type: 'node.enter', payload: this.minimalTelemetry ? node.id : { nodeId: node.id }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
402
|
+
}
|
|
403
|
+
catch { }
|
|
404
|
+
return node;
|
|
214
405
|
}
|
|
215
406
|
/**
|
|
216
407
|
* Get available choices from the current node with validation and conditional filtering
|
|
@@ -223,6 +414,22 @@ class QNCEEngine {
|
|
|
223
414
|
timestamp: Date.now(),
|
|
224
415
|
customData: {}
|
|
225
416
|
};
|
|
417
|
+
// Performance-mode choice array pooling: reuse a scratch array to avoid per-call allocations
|
|
418
|
+
let scratch;
|
|
419
|
+
if (this.performanceMode) {
|
|
420
|
+
// Lazily allocate a shared scratch array on first use and clear in-place
|
|
421
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
422
|
+
// @ts-ignore - attach hidden field for pooling (internal only)
|
|
423
|
+
if (!this.__choiceScratch) { /* eslint-disable-line */
|
|
424
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
425
|
+
// @ts-ignore
|
|
426
|
+
this.__choiceScratch = []; /* eslint-disable-line */
|
|
427
|
+
}
|
|
428
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
429
|
+
// @ts-ignore
|
|
430
|
+
scratch = this.__choiceScratch; /* eslint-disable-line */
|
|
431
|
+
scratch.length = 0;
|
|
432
|
+
}
|
|
226
433
|
// First apply conditional filtering (Sprint 3.4)
|
|
227
434
|
const conditionallyAvailable = currentNode.choices.filter((choice) => {
|
|
228
435
|
// If no condition is specified, choice is always available
|
|
@@ -231,19 +438,58 @@ class QNCEEngine {
|
|
|
231
438
|
}
|
|
232
439
|
try {
|
|
233
440
|
// Evaluate the condition using the condition evaluator
|
|
234
|
-
|
|
441
|
+
const t0 = Date.now();
|
|
442
|
+
const res = hot_profiler_1.globalHotProfiler.wrap('condition.evaluate', () => condition_1.conditionEvaluator.evaluate(choice.condition, context));
|
|
443
|
+
try {
|
|
444
|
+
this.telemetry?.emit({ type: 'expression.evaluate', payload: this.minimalTelemetry ? 1 : { ok: true, ms: Date.now() - t0 }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
if (this.debugMode)
|
|
448
|
+
debug_logger_1.globalDebugLogger.log('condition.ok', { nodeId: this.state.currentNodeId, choiceText: choice.text, expr: choice.condition });
|
|
449
|
+
return res;
|
|
235
450
|
}
|
|
236
451
|
catch (error) {
|
|
237
452
|
// Log condition evaluation errors but don't block other choices
|
|
238
453
|
if (error instanceof condition_1.ConditionEvaluationError) {
|
|
239
|
-
|
|
454
|
+
const struct = error_factory_1.ErrorFactory.condition('Choice condition evaluation failed', {
|
|
240
455
|
choiceText: choice.text,
|
|
241
|
-
|
|
242
|
-
nodeId: this.state.currentNodeId
|
|
456
|
+
conditionExpression: choice.condition,
|
|
457
|
+
nodeId: this.state.currentNodeId,
|
|
458
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
459
|
+
storyId: this.storyData?.id,
|
|
460
|
+
cause: error
|
|
243
461
|
});
|
|
462
|
+
// eslint-disable-next-line no-console
|
|
463
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
464
|
+
try {
|
|
465
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
466
|
+
}
|
|
467
|
+
catch { }
|
|
468
|
+
try {
|
|
469
|
+
this.telemetry?.emit({ type: 'expression.evaluate', payload: this.minimalTelemetry ? 0 : { ok: false, error: 'ConditionEvaluationError' }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
470
|
+
}
|
|
471
|
+
catch { }
|
|
244
472
|
}
|
|
245
473
|
else {
|
|
246
|
-
|
|
474
|
+
const struct = error_factory_1.ErrorFactory.condition('Unexpected error evaluating choice condition', {
|
|
475
|
+
choiceText: choice.text,
|
|
476
|
+
conditionExpression: choice.condition,
|
|
477
|
+
nodeId: this.state.currentNodeId,
|
|
478
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
479
|
+
storyId: this.storyData?.id,
|
|
480
|
+
cause: error
|
|
481
|
+
});
|
|
482
|
+
// eslint-disable-next-line no-console
|
|
483
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
484
|
+
try {
|
|
485
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
486
|
+
}
|
|
487
|
+
catch { }
|
|
488
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
489
|
+
try {
|
|
490
|
+
this.telemetry?.emit({ type: 'engine.error', payload: { where: 'getAvailableChoices', error: error?.message || String(error) }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
491
|
+
}
|
|
492
|
+
catch { }
|
|
247
493
|
}
|
|
248
494
|
// Return false for invalid conditions (choice won't be shown)
|
|
249
495
|
return false;
|
|
@@ -252,7 +498,14 @@ class QNCEEngine {
|
|
|
252
498
|
// Then apply choice validation (Sprint 3.2)
|
|
253
499
|
const validationContext = (0, validation_1.createValidationContext)(currentNode, this.state, conditionallyAvailable // Pass the conditionally filtered choices
|
|
254
500
|
);
|
|
255
|
-
|
|
501
|
+
const validated = this.choiceValidator.getAvailableChoices(validationContext);
|
|
502
|
+
if (!this.performanceMode || !scratch)
|
|
503
|
+
return validated;
|
|
504
|
+
// Copy into scratch to present a stable pooled array (avoid leaking validator's array if it allocates)
|
|
505
|
+
for (let i = 0; i < validated.length; i++)
|
|
506
|
+
scratch[i] = validated[i];
|
|
507
|
+
scratch.length = validated.length;
|
|
508
|
+
return scratch;
|
|
256
509
|
}
|
|
257
510
|
makeChoice(choiceIndex) {
|
|
258
511
|
const choices = this.getAvailableChoices();
|
|
@@ -277,9 +530,13 @@ class QNCEEngine {
|
|
|
277
530
|
return { ...this.state.flags };
|
|
278
531
|
}
|
|
279
532
|
setFlag(key, value) {
|
|
533
|
+
if (this.debugMode)
|
|
534
|
+
debug_logger_1.globalDebugLogger.log('flag.set', { key, value });
|
|
280
535
|
// Sprint 3.5: Save state for undo before making changes
|
|
281
536
|
const preChangeState = this.deepCopy(this.state);
|
|
282
|
-
this.
|
|
537
|
+
const pooledKey = this.internFlagKey(key);
|
|
538
|
+
const pooledValue = this.internFlagValue(value);
|
|
539
|
+
this.state.flags[pooledKey] = pooledValue;
|
|
283
540
|
// Sprint 3.5: Track state change for undo/redo
|
|
284
541
|
if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
|
|
285
542
|
this.undoRedoConfig.trackActions.includes('flag-change')) {
|
|
@@ -294,7 +551,20 @@ class QNCEEngine {
|
|
|
294
551
|
flagKey: key,
|
|
295
552
|
flagValue: value
|
|
296
553
|
}).catch((error) => {
|
|
297
|
-
|
|
554
|
+
const struct = error_factory_1.ErrorFactory.persistence('Autosave failed', {
|
|
555
|
+
operation: 'autosave',
|
|
556
|
+
flagKey: key,
|
|
557
|
+
flagValue: value,
|
|
558
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
559
|
+
storyId: this.storyData?.id,
|
|
560
|
+
cause: error
|
|
561
|
+
});
|
|
562
|
+
// eslint-disable-next-line no-console
|
|
563
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
564
|
+
try {
|
|
565
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
566
|
+
}
|
|
567
|
+
catch { }
|
|
298
568
|
});
|
|
299
569
|
}
|
|
300
570
|
}
|
|
@@ -302,6 +572,35 @@ class QNCEEngine {
|
|
|
302
572
|
return [...this.state.history];
|
|
303
573
|
}
|
|
304
574
|
selectChoice(choice) {
|
|
575
|
+
if (this.debugMode)
|
|
576
|
+
debug_logger_1.globalDebugLogger.log('choice.select.start', { from: this.state.currentNodeId, to: choice.nextNodeId, choiceText: choice.text });
|
|
577
|
+
// Pre-choice hooks (ordered by priority desc then registration order)
|
|
578
|
+
if (this.preChoiceHooks.length) {
|
|
579
|
+
const currentNodeForHooks = this.getCurrentNode();
|
|
580
|
+
for (const hook of this.preChoiceHooks) {
|
|
581
|
+
try {
|
|
582
|
+
const res = hook.h({ engine: this, node: currentNodeForHooks, choice });
|
|
583
|
+
if (res === false)
|
|
584
|
+
return; // cancellation
|
|
585
|
+
}
|
|
586
|
+
catch (e) {
|
|
587
|
+
const struct = error_factory_1.ErrorFactory.hook('pre-choice hook error', {
|
|
588
|
+
hookStage: 'pre-choice',
|
|
589
|
+
nodeId: currentNodeForHooks.id,
|
|
590
|
+
choiceText: choice.text,
|
|
591
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
592
|
+
storyId: this.storyData?.id,
|
|
593
|
+
cause: e
|
|
594
|
+
});
|
|
595
|
+
// eslint-disable-next-line no-console
|
|
596
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
597
|
+
try {
|
|
598
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
599
|
+
}
|
|
600
|
+
catch { }
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
305
604
|
// Sprint 3.5: Save state for undo before making changes
|
|
306
605
|
const preChangeState = this.deepCopy(this.state);
|
|
307
606
|
// S2-T4: Add state transition profiling
|
|
@@ -314,6 +613,11 @@ class QNCEEngine {
|
|
|
314
613
|
: null;
|
|
315
614
|
const fromNodeId = this.state.currentNodeId;
|
|
316
615
|
const toNodeId = choice.nextNodeId;
|
|
616
|
+
// Telemetry: choice.select
|
|
617
|
+
try {
|
|
618
|
+
this.telemetry?.emit({ type: 'choice.select', payload: { fromNodeId, toNodeId, choiceText: choice.text }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
619
|
+
}
|
|
620
|
+
catch { }
|
|
317
621
|
// Create flow event for tracking narrative progression
|
|
318
622
|
if (this.performanceMode) {
|
|
319
623
|
const flowSpanId = this.enableProfiling
|
|
@@ -357,9 +661,50 @@ class QNCEEngine {
|
|
|
357
661
|
toNodeId,
|
|
358
662
|
choiceText: choice.text
|
|
359
663
|
}).catch(error => {
|
|
360
|
-
|
|
664
|
+
const struct = error_factory_1.ErrorFactory.persistence('Autosave failed', {
|
|
665
|
+
operation: 'autosave',
|
|
666
|
+
fromNodeId,
|
|
667
|
+
toNodeId,
|
|
668
|
+
choiceText: choice.text,
|
|
669
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
670
|
+
storyId: this.storyData?.id,
|
|
671
|
+
cause: error
|
|
672
|
+
});
|
|
673
|
+
// eslint-disable-next-line no-console
|
|
674
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
675
|
+
try {
|
|
676
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
677
|
+
}
|
|
678
|
+
catch { }
|
|
361
679
|
});
|
|
362
680
|
}
|
|
681
|
+
// Post-choice hooks
|
|
682
|
+
if (this.postChoiceHooks.length) {
|
|
683
|
+
const nodeAfter = this.getCurrentNode();
|
|
684
|
+
for (const hook of this.postChoiceHooks) {
|
|
685
|
+
try {
|
|
686
|
+
hook.h({ engine: this, node: nodeAfter, choice });
|
|
687
|
+
}
|
|
688
|
+
catch (e) {
|
|
689
|
+
const struct = error_factory_1.ErrorFactory.hook('post-choice hook error', {
|
|
690
|
+
hookStage: 'post-choice',
|
|
691
|
+
nodeId: nodeAfter.id,
|
|
692
|
+
choiceText: choice.text,
|
|
693
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
694
|
+
storyId: this.storyData?.id,
|
|
695
|
+
cause: e
|
|
696
|
+
});
|
|
697
|
+
// eslint-disable-next-line no-console
|
|
698
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
699
|
+
try {
|
|
700
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
701
|
+
}
|
|
702
|
+
catch { }
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (this.debugMode)
|
|
707
|
+
debug_logger_1.globalDebugLogger.log('choice.select.end', { to: this.state.currentNodeId });
|
|
363
708
|
// Complete state transition span
|
|
364
709
|
if (transitionSpanId) {
|
|
365
710
|
(0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, {
|
|
@@ -371,7 +716,19 @@ class QNCEEngine {
|
|
|
371
716
|
if (this.performanceMode) {
|
|
372
717
|
// Preload next possible nodes in background
|
|
373
718
|
this.preloadNextNodes().catch(error => {
|
|
374
|
-
|
|
719
|
+
const struct = error_factory_1.ErrorFactory.state('Background preload failed', {
|
|
720
|
+
operation: 'preloadNextNodes',
|
|
721
|
+
nodeId: toNodeId,
|
|
722
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
723
|
+
storyId: this.storyData?.id,
|
|
724
|
+
cause: error
|
|
725
|
+
});
|
|
726
|
+
// eslint-disable-next-line no-console
|
|
727
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
728
|
+
try {
|
|
729
|
+
this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
|
|
730
|
+
}
|
|
731
|
+
catch { }
|
|
375
732
|
});
|
|
376
733
|
// Write telemetry data in background
|
|
377
734
|
this.backgroundTelemetryWrite({
|
|
@@ -380,10 +737,51 @@ class QNCEEngine {
|
|
|
380
737
|
toNodeId,
|
|
381
738
|
choiceText: choice.text.slice(0, 50) // First 50 chars for privacy
|
|
382
739
|
}).catch(error => {
|
|
383
|
-
|
|
740
|
+
const struct = error_factory_1.ErrorFactory.telemetry('Background telemetry failed', {
|
|
741
|
+
operation: 'backgroundTelemetryWrite',
|
|
742
|
+
fromNodeId,
|
|
743
|
+
toNodeId,
|
|
744
|
+
choiceText: choice.text,
|
|
745
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
746
|
+
storyId: this.storyData?.id,
|
|
747
|
+
cause: error
|
|
748
|
+
});
|
|
749
|
+
// eslint-disable-next-line no-console
|
|
750
|
+
this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
|
|
384
751
|
});
|
|
385
752
|
}
|
|
386
753
|
}
|
|
754
|
+
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
|
|
755
|
+
registerHook(type, handler, priority = 0) {
|
|
756
|
+
const list = type === 'pre-choice' ? this.preChoiceHooks : this.postChoiceHooks;
|
|
757
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
758
|
+
const entry = { h: handler, p: priority, o: this.hookCounter++ };
|
|
759
|
+
list.push(entry);
|
|
760
|
+
list.sort((a, b) => (b.p - a.p) || (a.o - b.o));
|
|
761
|
+
return () => {
|
|
762
|
+
const idx = list.indexOf(entry);
|
|
763
|
+
if (idx >= 0)
|
|
764
|
+
list.splice(idx, 1);
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/** Clear all hooks (testing / reset) */
|
|
768
|
+
clearHooks() { this.preChoiceHooks = []; this.postChoiceHooks = []; }
|
|
769
|
+
/** Enable verbose debug logging (stored in ring buffer) */
|
|
770
|
+
enableDebug() { this.debugMode = true; debug_logger_1.globalDebugLogger.setEnabled(true); }
|
|
771
|
+
disableDebug() { this.debugMode = false; if (!this.enableProfiling)
|
|
772
|
+
debug_logger_1.globalDebugLogger.setEnabled(false); }
|
|
773
|
+
getDebugLogs() { return debug_logger_1.globalDebugLogger.flush(); }
|
|
774
|
+
clearDebugLogs() { debug_logger_1.globalDebugLogger.clear(); }
|
|
775
|
+
/** Enable aggregated hot path profiling */
|
|
776
|
+
enableHotProfiling() { hot_profiler_1.globalHotProfiler.enable(true); }
|
|
777
|
+
disableHotProfiling() { hot_profiler_1.globalHotProfiler.enable(false); }
|
|
778
|
+
getHotProfileSummary() { return hot_profiler_1.globalHotProfiler.summary(); }
|
|
779
|
+
resetHotProfile() { hot_profiler_1.globalHotProfiler.reset(); }
|
|
780
|
+
/** Flush telemetry (useful before process exit) */
|
|
781
|
+
async flushTelemetry() { try {
|
|
782
|
+
await this.telemetry?.flush();
|
|
783
|
+
}
|
|
784
|
+
catch { } }
|
|
387
785
|
resetNarrative() {
|
|
388
786
|
// Sprint 3.5: Save state for undo before reset
|
|
389
787
|
const preChangeState = this.deepCopy(this.state);
|
|
@@ -422,7 +820,8 @@ class QNCEEngine {
|
|
|
422
820
|
this.triggerAutosave('state-load', {
|
|
423
821
|
nodeId: state.currentNodeId
|
|
424
822
|
}).catch((error) => {
|
|
425
|
-
|
|
823
|
+
// eslint-disable-next-line no-console
|
|
824
|
+
this.logger.warn('[QNCE] Autosave failed: ' + error.message);
|
|
426
825
|
});
|
|
427
826
|
}
|
|
428
827
|
}
|
|
@@ -480,6 +879,7 @@ class QNCEEngine {
|
|
|
480
879
|
// Generate checksum if requested
|
|
481
880
|
if (options.generateChecksum) {
|
|
482
881
|
const stateToHash = { ...serializedState };
|
|
882
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
483
883
|
delete stateToHash.metadata.checksum; // Exclude checksum from hash
|
|
484
884
|
const stateString = JSON.stringify(stateToHash);
|
|
485
885
|
metadata.checksum = await this.generateChecksum(stateString);
|
|
@@ -748,7 +1148,11 @@ class QNCEEngine {
|
|
|
748
1148
|
*/
|
|
749
1149
|
createFlowEvent(fromNodeId, toNodeId, metadata) {
|
|
750
1150
|
const flow = ObjectPool_1.poolManager.borrowFlow();
|
|
751
|
-
|
|
1151
|
+
// In minimal telemetry mode, avoid attaching heavy metadata
|
|
1152
|
+
const meta = this.minimalTelemetry ? undefined : (0, intern_1.internShallowRecord)(metadata || {});
|
|
1153
|
+
// Intern node id to reduce duplicate string allocations across events
|
|
1154
|
+
const fromId = (0, intern_1.internString)(fromNodeId);
|
|
1155
|
+
flow.initialize(fromId, meta);
|
|
752
1156
|
flow.addTransition(fromNodeId, toNodeId);
|
|
753
1157
|
return flow;
|
|
754
1158
|
}
|
|
@@ -756,16 +1160,19 @@ class QNCEEngine {
|
|
|
756
1160
|
* Record and manage flow events
|
|
757
1161
|
*/
|
|
758
1162
|
recordFlowEvent(flow) {
|
|
1163
|
+
const toId = flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '';
|
|
759
1164
|
const flowEvent = {
|
|
760
1165
|
id: `${flow.nodeId}-${Date.now()}`,
|
|
761
1166
|
fromNodeId: flow.nodeId,
|
|
762
|
-
toNodeId:
|
|
1167
|
+
toNodeId: (0, intern_1.internString)(toId),
|
|
763
1168
|
timestamp: flow.timestamp,
|
|
764
|
-
metadata
|
|
1169
|
+
// In minimal mode, drop metadata entirely
|
|
1170
|
+
metadata: this.minimalTelemetry ? undefined : flow.metadata
|
|
765
1171
|
};
|
|
766
1172
|
this.activeFlowEvents.push(flowEvent);
|
|
767
1173
|
// Clean up old flow events (basic LRU-style cleanup)
|
|
768
|
-
|
|
1174
|
+
const cap = this.minimalTelemetry ? 5 : 10;
|
|
1175
|
+
if (this.activeFlowEvents.length > cap) {
|
|
769
1176
|
this.activeFlowEvents.shift(); // Remove oldest
|
|
770
1177
|
}
|
|
771
1178
|
}
|
|
@@ -829,7 +1236,9 @@ class QNCEEngine {
|
|
|
829
1236
|
}
|
|
830
1237
|
};
|
|
831
1238
|
threadPool.submitJob('telemetry-write', telemetryData, 'low').catch(error => {
|
|
832
|
-
|
|
1239
|
+
if (this.engineOptions?.suppressTelemetryWarnings && error?.message === 'Job queue limit exceeded')
|
|
1240
|
+
return;
|
|
1241
|
+
this.logger.warn('[QNCE] Telemetry write failed: ' + error.message);
|
|
833
1242
|
});
|
|
834
1243
|
}
|
|
835
1244
|
// ================================
|
|
@@ -841,7 +1250,8 @@ class QNCEEngine {
|
|
|
841
1250
|
*/
|
|
842
1251
|
enableBranching(story) {
|
|
843
1252
|
if (this.branchingEngine) {
|
|
844
|
-
|
|
1253
|
+
// eslint-disable-next-line no-console
|
|
1254
|
+
this.logger.warn('[QNCE] Branching already enabled for this engine instance');
|
|
845
1255
|
return this.branchingEngine;
|
|
846
1256
|
}
|
|
847
1257
|
// Create branching engine with current state
|
|
@@ -1003,6 +1413,7 @@ class QNCEEngine {
|
|
|
1003
1413
|
if (!receivedChecksum)
|
|
1004
1414
|
return false;
|
|
1005
1415
|
const stateToHash = { ...serializedState };
|
|
1416
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1006
1417
|
delete stateToHash.metadata.checksum;
|
|
1007
1418
|
const stateString = JSON.stringify(stateToHash);
|
|
1008
1419
|
const expectedChecksum = await this.generateChecksum(stateString);
|
|
@@ -1194,12 +1605,19 @@ class QNCEEngine {
|
|
|
1194
1605
|
*/
|
|
1195
1606
|
pushToUndoStack(state, action, metadata) {
|
|
1196
1607
|
const startTime = performance.now();
|
|
1608
|
+
// Memory optimization (lightweight history): we store a shallow snapshot of flags + currentNodeId
|
|
1609
|
+
// instead of deep copying full engine state. We embed it in metadata under a reserved key and keep
|
|
1610
|
+
// a minimal placeholder state reference to satisfy existing types.
|
|
1611
|
+
const LIGHT_STATE_KEY = '__lightState';
|
|
1612
|
+
const prevHist = state?.history;
|
|
1613
|
+
const lightState = { currentNodeId: state.currentNodeId, flags: { ...state.flags }, history: [...(Array.isArray(prevHist) ? prevHist : this.state.history)] };
|
|
1197
1614
|
const entry = {
|
|
1198
1615
|
id: this.generateHistoryId(),
|
|
1199
|
-
|
|
1616
|
+
// Keep original deep copy for compatibility OFF for now -> minimal placeholder with current fields.
|
|
1617
|
+
state: this.deepCopy({ currentNodeId: state.currentNodeId, flags: { ...state.flags }, history: [...(Array.isArray(prevHist) ? prevHist : this.state.history)] }),
|
|
1200
1618
|
timestamp: new Date().toISOString(),
|
|
1201
1619
|
action,
|
|
1202
|
-
metadata
|
|
1620
|
+
metadata: { ...(metadata || {}), [LIGHT_STATE_KEY]: lightState }
|
|
1203
1621
|
};
|
|
1204
1622
|
this.undoStack.push(entry);
|
|
1205
1623
|
// Clear redo stack when new change is made
|
|
@@ -1239,9 +1657,10 @@ class QNCEEngine {
|
|
|
1239
1657
|
// Save current state to redo stack
|
|
1240
1658
|
const currentEntry = {
|
|
1241
1659
|
id: this.generateHistoryId(),
|
|
1242
|
-
state: this.deepCopy(this.state),
|
|
1660
|
+
state: this.deepCopy({ currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] }),
|
|
1243
1661
|
timestamp: new Date().toISOString(),
|
|
1244
|
-
action: 'redo-point'
|
|
1662
|
+
action: 'redo-point',
|
|
1663
|
+
metadata: { __lightState: { currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] } }
|
|
1245
1664
|
};
|
|
1246
1665
|
this.redoStack.push(currentEntry);
|
|
1247
1666
|
// Enforce max redo entries
|
|
@@ -1252,7 +1671,17 @@ class QNCEEngine {
|
|
|
1252
1671
|
const entryToRestore = this.undoStack.pop();
|
|
1253
1672
|
// Set flag to prevent triggering undo/redo tracking during restore
|
|
1254
1673
|
this.isUndoRedoOperation = true;
|
|
1255
|
-
|
|
1674
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- metadata may carry lightweight snapshot
|
|
1675
|
+
const light = entryToRestore.metadata?.__lightState;
|
|
1676
|
+
if (light) {
|
|
1677
|
+
this.state.currentNodeId = light.currentNodeId;
|
|
1678
|
+
this.state.flags = { ...light.flags };
|
|
1679
|
+
if (Array.isArray(light.history))
|
|
1680
|
+
this.state.history = [...light.history];
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
this.state = this.deepCopy(entryToRestore.state);
|
|
1684
|
+
}
|
|
1256
1685
|
this.isUndoRedoOperation = false;
|
|
1257
1686
|
const duration = performance.now() - startTime;
|
|
1258
1687
|
// Performance tracking
|
|
@@ -1312,9 +1741,10 @@ class QNCEEngine {
|
|
|
1312
1741
|
// Save current state to undo stack
|
|
1313
1742
|
const currentEntry = {
|
|
1314
1743
|
id: this.generateHistoryId(),
|
|
1315
|
-
state: this.deepCopy(this.state),
|
|
1744
|
+
state: this.deepCopy({ currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] }),
|
|
1316
1745
|
timestamp: new Date().toISOString(),
|
|
1317
|
-
action: 'undo-point'
|
|
1746
|
+
action: 'undo-point',
|
|
1747
|
+
metadata: { __lightState: { currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] } }
|
|
1318
1748
|
};
|
|
1319
1749
|
this.undoStack.push(currentEntry);
|
|
1320
1750
|
// Enforce max undo entries
|
|
@@ -1325,7 +1755,17 @@ class QNCEEngine {
|
|
|
1325
1755
|
const entryToRestore = this.redoStack.pop();
|
|
1326
1756
|
// Set flag to prevent triggering undo/redo tracking during restore
|
|
1327
1757
|
this.isUndoRedoOperation = true;
|
|
1328
|
-
|
|
1758
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- metadata may carry lightweight snapshot
|
|
1759
|
+
const light = entryToRestore.metadata?.__lightState;
|
|
1760
|
+
if (light) {
|
|
1761
|
+
this.state.currentNodeId = light.currentNodeId;
|
|
1762
|
+
this.state.flags = { ...light.flags };
|
|
1763
|
+
if (Array.isArray(light.history))
|
|
1764
|
+
this.state.history = [...light.history];
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
this.state = this.deepCopy(entryToRestore.state);
|
|
1768
|
+
}
|
|
1329
1769
|
this.isUndoRedoOperation = false;
|
|
1330
1770
|
const duration = performance.now() - startTime;
|
|
1331
1771
|
// Performance tracking
|
|
@@ -1429,6 +1869,8 @@ class QNCEEngine {
|
|
|
1429
1869
|
* @param metadata - Optional metadata about the trigger
|
|
1430
1870
|
* @returns Promise resolving to autosave result
|
|
1431
1871
|
*/
|
|
1872
|
+
// metadata currently unused (reserved for future extended autosave telemetry)
|
|
1873
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1432
1874
|
async triggerAutosave(trigger, metadata) {
|
|
1433
1875
|
const startTime = performance.now();
|
|
1434
1876
|
// Check if autosave is enabled
|
|
@@ -1540,12 +1982,20 @@ exports.QNCEEngine = QNCEEngine;
|
|
|
1540
1982
|
/**
|
|
1541
1983
|
* Factory function to create a QNCE engine instance
|
|
1542
1984
|
*/
|
|
1543
|
-
|
|
1544
|
-
|
|
1985
|
+
/**
|
|
1986
|
+
* Factory to create a QNCE engine instance.
|
|
1987
|
+
* @public
|
|
1988
|
+
*/
|
|
1989
|
+
function createQNCEEngine(storyData, initialState, performanceMode = false, threadPoolConfig, options) {
|
|
1990
|
+
return new QNCEEngine(storyData, initialState, performanceMode, threadPoolConfig, options);
|
|
1545
1991
|
}
|
|
1546
1992
|
/**
|
|
1547
1993
|
* Load story data from JSON
|
|
1548
1994
|
*/
|
|
1995
|
+
/**
|
|
1996
|
+
* Load and validate StoryData from JSON-like input.
|
|
1997
|
+
* @public
|
|
1998
|
+
*/
|
|
1549
1999
|
function loadStoryData(jsonData) {
|
|
1550
2000
|
// Add validation here in the future
|
|
1551
2001
|
return jsonData;
|