bulltrackers-module 1.0.765 → 1.0.768
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/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
- package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +119 -122
- package/functions/computation-system-v2/framework/storage/StorageManager.js +16 -18
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
- package/functions/computation-system-v2/handlers/scheduler.js +15 -5
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
|
@@ -84,7 +84,7 @@ module.exports = {
|
|
|
84
84
|
'portfolio_snapshots': {
|
|
85
85
|
dateField: 'date',
|
|
86
86
|
entityField: 'user_id',
|
|
87
|
-
dataField: 'portfolio_data',
|
|
87
|
+
//dataField: 'portfolio_data',
|
|
88
88
|
description: 'Daily portfolio snapshots for all users'
|
|
89
89
|
},
|
|
90
90
|
|
|
@@ -92,7 +92,7 @@ module.exports = {
|
|
|
92
92
|
'trade_history_snapshots': {
|
|
93
93
|
dateField: 'date',
|
|
94
94
|
entityField: 'user_id',
|
|
95
|
-
dataField: 'history_data',
|
|
95
|
+
//dataField: 'history_data',
|
|
96
96
|
description: 'Daily trade history snapshots'
|
|
97
97
|
},
|
|
98
98
|
|
|
@@ -100,7 +100,7 @@ module.exports = {
|
|
|
100
100
|
'social_post_snapshots': {
|
|
101
101
|
dateField: 'date',
|
|
102
102
|
entityField: 'user_id',
|
|
103
|
-
dataField: 'posts_data',
|
|
103
|
+
//dataField: 'posts_data',
|
|
104
104
|
description: 'Daily social post snapshots'
|
|
105
105
|
},
|
|
106
106
|
|
|
@@ -108,7 +108,7 @@ module.exports = {
|
|
|
108
108
|
'asset_prices': {
|
|
109
109
|
dateField: 'date',
|
|
110
110
|
entityField: 'instrument_id',
|
|
111
|
-
dataField: null, // Flat table
|
|
111
|
+
//dataField: null, // Flat table
|
|
112
112
|
description: 'Daily asset prices'
|
|
113
113
|
},
|
|
114
114
|
|
|
@@ -116,7 +116,7 @@ module.exports = {
|
|
|
116
116
|
'pi_rankings': {
|
|
117
117
|
dateField: 'date',
|
|
118
118
|
entityField: 'pi_id',
|
|
119
|
-
dataField: 'rankings_data',
|
|
119
|
+
//dataField: 'rankings_data',
|
|
120
120
|
description: 'Daily PI rankings snapshot'
|
|
121
121
|
},
|
|
122
122
|
|
|
@@ -124,7 +124,7 @@ module.exports = {
|
|
|
124
124
|
'pi_master_list': {
|
|
125
125
|
dateField: null, // Not date-partitioned
|
|
126
126
|
entityField: 'cid',
|
|
127
|
-
dataField: null,
|
|
127
|
+
//dataField: null,
|
|
128
128
|
description: 'Master list of all Popular Investors'
|
|
129
129
|
},
|
|
130
130
|
|
|
@@ -132,7 +132,7 @@ module.exports = {
|
|
|
132
132
|
'pi_ratings': {
|
|
133
133
|
dateField: 'date',
|
|
134
134
|
entityField: 'pi_id',
|
|
135
|
-
dataField: null,
|
|
135
|
+
//dataField: null,
|
|
136
136
|
description: 'Daily PI ratings'
|
|
137
137
|
},
|
|
138
138
|
|
|
@@ -140,7 +140,7 @@ module.exports = {
|
|
|
140
140
|
'pi_page_views': {
|
|
141
141
|
dateField: 'date',
|
|
142
142
|
entityField: 'pi_id',
|
|
143
|
-
dataField: null,
|
|
143
|
+
//dataField: null,
|
|
144
144
|
description: 'Daily PI page view metrics'
|
|
145
145
|
},
|
|
146
146
|
|
|
@@ -148,7 +148,7 @@ module.exports = {
|
|
|
148
148
|
'watchlist_membership': {
|
|
149
149
|
dateField: 'date',
|
|
150
150
|
entityField: 'pi_id',
|
|
151
|
-
dataField: null,
|
|
151
|
+
//dataField: null,
|
|
152
152
|
description: 'Daily watchlist membership counts'
|
|
153
153
|
},
|
|
154
154
|
|
|
@@ -156,7 +156,7 @@ module.exports = {
|
|
|
156
156
|
'pi_alert_history': {
|
|
157
157
|
dateField: 'date',
|
|
158
158
|
entityField: 'pi_id',
|
|
159
|
-
dataField: 'metadata',
|
|
159
|
+
//dataField: 'metadata',
|
|
160
160
|
description: 'Daily alert trigger history'
|
|
161
161
|
},
|
|
162
162
|
|
|
@@ -164,7 +164,7 @@ module.exports = {
|
|
|
164
164
|
'instrument_insights': {
|
|
165
165
|
dateField: 'date',
|
|
166
166
|
entityField: 'instrument_id',
|
|
167
|
-
dataField: 'insights_data',
|
|
167
|
+
//dataField: 'insights_data',
|
|
168
168
|
description: 'Daily instrument insights'
|
|
169
169
|
},
|
|
170
170
|
|
|
@@ -172,7 +172,7 @@ module.exports = {
|
|
|
172
172
|
'ticker_mappings': {
|
|
173
173
|
dateField: null,
|
|
174
174
|
entityField: 'instrument_id',
|
|
175
|
-
dataField: null,
|
|
175
|
+
//dataField: null,
|
|
176
176
|
description: 'Instrument ID to ticker symbol mappings'
|
|
177
177
|
},
|
|
178
178
|
|
|
@@ -180,15 +180,27 @@ module.exports = {
|
|
|
180
180
|
'computation_results': {
|
|
181
181
|
dateField: 'date',
|
|
182
182
|
entityField: null, // Keyed by computation_name
|
|
183
|
-
dataField: 'result_data',
|
|
183
|
+
//dataField: 'result_data',
|
|
184
184
|
description: 'Stored computation results'
|
|
185
185
|
},
|
|
186
186
|
// NEW: Sector Mappings Table
|
|
187
187
|
'sector_mappings': {
|
|
188
188
|
dateField: null, // Static data
|
|
189
189
|
entityField: 'symbol', // Key the data by symbol for fast lookup
|
|
190
|
-
dataField: null,
|
|
190
|
+
//dataField: null,
|
|
191
191
|
description: 'Ticker to Sector mappings migrated from Firestore'
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// NEW: Map the abstract requirement 'behavioral_features' to the actual BQ table
|
|
195
|
+
'behavioral_features': {
|
|
196
|
+
tableName: 'daily_behavioral_features',
|
|
197
|
+
dateField: 'date',
|
|
198
|
+
entityField: 'user_id', // Important for per-entity fetching
|
|
199
|
+
schema: [
|
|
200
|
+
{ name: 'user_id', type: 'STRING' },
|
|
201
|
+
{ name: 'hhi_score', type: 'FLOAT' },
|
|
202
|
+
{ name: 'martingale_events', type: 'INTEGER' }
|
|
203
|
+
]
|
|
192
204
|
}
|
|
193
205
|
},
|
|
194
206
|
|
|
@@ -39,12 +39,9 @@ class ManifestBuilder {
|
|
|
39
39
|
if (entry) {
|
|
40
40
|
manifestMap.set(entry.name, entry);
|
|
41
41
|
|
|
42
|
-
// CRITICAL FIX: Include conditional dependencies in the DAG for cycle detection and topological sort.
|
|
43
|
-
// Even if the dependency is conditional at runtime, the execution order (Pass) must respect it.
|
|
44
42
|
const graphDeps = [...entry.dependencies];
|
|
45
43
|
if (entry.conditionalDependencies) {
|
|
46
44
|
entry.conditionalDependencies.forEach(cd => {
|
|
47
|
-
// Ensure we use the normalized name for the graph
|
|
48
45
|
graphDeps.push(cd.computation);
|
|
49
46
|
});
|
|
50
47
|
}
|
|
@@ -60,7 +57,7 @@ class ManifestBuilder {
|
|
|
60
57
|
throw new Error(`[Manifest] Circular dependency detected: ${cycle}`);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
// 3. Topological Sort
|
|
60
|
+
// 3. Topological Sort
|
|
64
61
|
const sortedItems = Graph.topologicalSort(nodes, adjacency);
|
|
65
62
|
|
|
66
63
|
// 4. Hydrate Sorted List
|
|
@@ -115,8 +112,6 @@ class ManifestBuilder {
|
|
|
115
112
|
compositeHash += `|RULE:${mod}:${h}`;
|
|
116
113
|
}
|
|
117
114
|
|
|
118
|
-
// Normalize conditional dependencies if they exist
|
|
119
|
-
// This ensures the Orchestrator can look them up by normalized name later
|
|
120
115
|
const conditionalDependencies = (config.conditionalDependencies || []).map(cd => ({
|
|
121
116
|
...cd,
|
|
122
117
|
computation: this._normalize(cd.computation)
|
|
@@ -128,16 +123,20 @@ class ManifestBuilder {
|
|
|
128
123
|
class: ComputationClass,
|
|
129
124
|
category: config.category || 'default',
|
|
130
125
|
type: config.type || 'global',
|
|
126
|
+
|
|
127
|
+
outputTable: config.outputTable || null,
|
|
128
|
+
// --------------------------------------
|
|
129
|
+
|
|
131
130
|
requires: config.requires || {},
|
|
132
131
|
dependencies: (config.dependencies || []).map(d => this._normalize(d)),
|
|
133
|
-
conditionalDependencies,
|
|
132
|
+
conditionalDependencies,
|
|
134
133
|
isHistorical: config.isHistorical || false,
|
|
135
134
|
isTest: config.isTest || false,
|
|
136
135
|
schedule: this.scheduleValidator.parseSchedule(config.schedule),
|
|
137
136
|
storage: this._parseStorageConfig(config.storage),
|
|
138
137
|
ttlDays: config.ttlDays,
|
|
139
|
-
pass: 0,
|
|
140
|
-
hash: this._hashCode(compositeHash),
|
|
138
|
+
pass: 0,
|
|
139
|
+
hash: this._hashCode(compositeHash),
|
|
141
140
|
weight: ComputationClass.getWeight ? ComputationClass.getWeight() : 1.0,
|
|
142
141
|
composition: {
|
|
143
142
|
epoch: this.epoch,
|
|
@@ -152,7 +151,6 @@ class ManifestBuilder {
|
|
|
152
151
|
_computeFinalHashes(sorted, manifestMap) {
|
|
153
152
|
for (const entry of sorted) {
|
|
154
153
|
let hashInput = entry.hash;
|
|
155
|
-
// Includes strict dependencies in the hash chain
|
|
156
154
|
if (entry.dependencies.length > 0) {
|
|
157
155
|
const depHashes = entry.dependencies.sort().map(d => {
|
|
158
156
|
const h = manifestMap.get(d)?.hash;
|
|
@@ -161,10 +159,6 @@ class ManifestBuilder {
|
|
|
161
159
|
});
|
|
162
160
|
hashInput += `|DEPS:${depHashes.join('|')}`;
|
|
163
161
|
}
|
|
164
|
-
// Note: Conditional dependencies are currently excluded from the hash chain
|
|
165
|
-
// because they might not be loaded. If strict versioning is required for them,
|
|
166
|
-
// they should be added here too.
|
|
167
|
-
|
|
168
162
|
entry.hash = this._hashCode(hashInput);
|
|
169
163
|
}
|
|
170
164
|
}
|
|
@@ -203,7 +197,7 @@ class ManifestBuilder {
|
|
|
203
197
|
const used = {};
|
|
204
198
|
for (const [name, exports] of Object.entries(this.sharedLayers)) {
|
|
205
199
|
const found = Object.keys(exports).filter(exp =>
|
|
206
|
-
code.includes(exp)
|
|
200
|
+
code.includes(exp)
|
|
207
201
|
);
|
|
208
202
|
if (found.length) used[name] = found;
|
|
209
203
|
}
|
|
@@ -240,7 +234,6 @@ class ManifestBuilder {
|
|
|
240
234
|
|
|
241
235
|
_log(l, m) { this.logger ? this.logger.log(l, `[Manifest] ${m}`) : console.log(`[Manifest] ${m}`); }
|
|
242
236
|
|
|
243
|
-
// Public alias for groupByPass matching the Interface
|
|
244
237
|
groupByPass(m) { return this._groupByPass(m); }
|
|
245
238
|
}
|
|
246
239
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @fileoverview Run Analyzer
|
|
3
3
|
* * Pure logic component that determines which computations need to run.
|
|
4
4
|
* Decouples decision-making from execution and storage.
|
|
5
|
+
* * * UPDATE: Removed SQL bypass. All computations are now checked for data availability.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
class RunAnalyzer {
|
|
@@ -57,7 +58,7 @@ class RunAnalyzer {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// 2. Data Availability Check
|
|
60
|
-
//
|
|
61
|
+
// UPDATE: Removed isSql check. All computations must have their raw data available.
|
|
61
62
|
const availability = await this.dataFetcher.checkAvailability(requires, dateStr);
|
|
62
63
|
if (!availability.canRun) {
|
|
63
64
|
if (!isToday) {
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
* * V2.4 FIX: Runaway Query Cost Prevention [Fix #3].
|
|
10
10
|
* * V2.5 UPDATE: Super-Entity Monitoring [Safety Valve for Fix #6].
|
|
11
11
|
* - Warns if a single entity exceeds reasonable batch limits (Memory Risk).
|
|
12
|
+
* * V2.6 UPDATE: Query Result Caching.
|
|
13
|
+
* - Implemented in-memory LRU cache to prevent redundant BigQuery costs for reference data.
|
|
14
|
+
* * V2.7 FIX: Double-Encoded JSON Normalization.
|
|
15
|
+
* - Automatically detects and recursively parses JSON strings (e.g. posts_data) to prevent downstream parsing errors.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
const { BigQuery } = require('@google-cloud/bigquery');
|
|
19
|
+
const crypto = require('crypto');
|
|
15
20
|
|
|
16
21
|
// FIX #3: Hard limit to prevent cost spirals
|
|
17
22
|
const MAX_LOOKBACK_DAYS = 30;
|
|
@@ -29,11 +34,24 @@ class DataFetcher {
|
|
|
29
34
|
|
|
30
35
|
this.client = new BigQuery({ projectId: this.projectId });
|
|
31
36
|
|
|
37
|
+
// Cache Configuration (V2.6)
|
|
38
|
+
this.cacheConfig = config.queryCache || {
|
|
39
|
+
enabled: true,
|
|
40
|
+
ttlMs: 300000, // 5 minutes default
|
|
41
|
+
maxSize: 1000 // Max unique queries to cache
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Use Map as LRU cache (insertion order preserved)
|
|
45
|
+
this.cache = new Map();
|
|
46
|
+
|
|
32
47
|
this.stats = {
|
|
33
48
|
queries: 0,
|
|
34
49
|
rowsFetched: 0,
|
|
35
50
|
errors: 0,
|
|
36
|
-
bytesProcessed: 0
|
|
51
|
+
bytesProcessed: 0,
|
|
52
|
+
cacheHits: 0,
|
|
53
|
+
cacheMisses: 0,
|
|
54
|
+
cacheEvictions: 0
|
|
37
55
|
};
|
|
38
56
|
}
|
|
39
57
|
|
|
@@ -273,21 +291,74 @@ class DataFetcher {
|
|
|
273
291
|
|
|
274
292
|
return { canRun: missing.length === 0, available, missing };
|
|
275
293
|
}
|
|
294
|
+
|
|
276
295
|
|
|
277
296
|
getStats() { return { ...this.stats }; }
|
|
278
|
-
|
|
297
|
+
|
|
298
|
+
resetStats() {
|
|
299
|
+
this.stats = {
|
|
300
|
+
queries: 0,
|
|
301
|
+
rowsFetched: 0,
|
|
302
|
+
errors: 0,
|
|
303
|
+
bytesProcessed: 0,
|
|
304
|
+
cacheHits: 0,
|
|
305
|
+
cacheMisses: 0,
|
|
306
|
+
cacheEvictions: 0
|
|
307
|
+
};
|
|
308
|
+
this.cache.clear();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
clearCache() {
|
|
312
|
+
this.cache.clear();
|
|
313
|
+
this._log('DEBUG', 'Query cache cleared');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// =========================================================================
|
|
317
|
+
// PRIVATE METHODS
|
|
318
|
+
// =========================================================================
|
|
279
319
|
|
|
280
320
|
async _execute(query) {
|
|
321
|
+
// V2.6: Query Caching
|
|
322
|
+
if (this.cacheConfig.enabled) {
|
|
323
|
+
const cacheKey = this._generateCacheKey(query);
|
|
324
|
+
const cached = this.cache.get(cacheKey);
|
|
325
|
+
|
|
326
|
+
if (cached) {
|
|
327
|
+
if (Date.now() - cached.timestamp < this.cacheConfig.ttlMs) {
|
|
328
|
+
this.stats.cacheHits++;
|
|
329
|
+
// Refresh LRU position (delete and re-set moves to end)
|
|
330
|
+
this.cache.delete(cacheKey);
|
|
331
|
+
this.cache.set(cacheKey, cached);
|
|
332
|
+
// Return cached rows immediately - no BigQuery cost
|
|
333
|
+
return cached.rows;
|
|
334
|
+
} else {
|
|
335
|
+
this.cache.delete(cacheKey); // Expired
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.stats.cacheMisses++;
|
|
339
|
+
}
|
|
340
|
+
|
|
281
341
|
this.stats.queries++;
|
|
342
|
+
|
|
282
343
|
try {
|
|
283
344
|
const [job] = await this.client.createQueryJob({
|
|
284
345
|
query: query.sql, params: query.params, location: this.location
|
|
285
346
|
});
|
|
286
347
|
const [rows] = await job.getQueryResults();
|
|
287
348
|
const [metadata] = await job.getMetadata();
|
|
349
|
+
|
|
288
350
|
this.stats.rowsFetched += rows.length;
|
|
289
351
|
this.stats.bytesProcessed += parseInt(metadata.statistics?.totalBytesProcessed || 0, 10);
|
|
290
|
-
|
|
352
|
+
|
|
353
|
+
// FIX V2.7: Normalize Rows (Recursive JSON Parse) BEFORE caching
|
|
354
|
+
const normalizedRows = rows.map(r => this._normalizeRow(r));
|
|
355
|
+
|
|
356
|
+
// Store in cache if enabled
|
|
357
|
+
if (this.cacheConfig.enabled) {
|
|
358
|
+
this._addToCache(query, normalizedRows);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return normalizedRows;
|
|
291
362
|
} catch (e) {
|
|
292
363
|
this.stats.errors++;
|
|
293
364
|
this._log('ERROR', `Query failed: ${e.message}`);
|
|
@@ -296,6 +367,8 @@ class DataFetcher {
|
|
|
296
367
|
}
|
|
297
368
|
|
|
298
369
|
async *_executeStream(query) {
|
|
370
|
+
// NOTE: We do NOT cache streams. They are typically massive datasets (batch processing)
|
|
371
|
+
// and caching them in memory would cause OOM.
|
|
299
372
|
this.stats.queries++;
|
|
300
373
|
try {
|
|
301
374
|
const [job] = await this.client.createQueryJob({
|
|
@@ -304,7 +377,8 @@ class DataFetcher {
|
|
|
304
377
|
const stream = job.getQueryResultsStream();
|
|
305
378
|
for await (const row of stream) {
|
|
306
379
|
this.stats.rowsFetched++;
|
|
307
|
-
|
|
380
|
+
// FIX V2.7: Normalize Rows (Recursive JSON Parse)
|
|
381
|
+
yield this._normalizeRow(row);
|
|
308
382
|
}
|
|
309
383
|
} catch (e) {
|
|
310
384
|
this.stats.errors++;
|
|
@@ -312,6 +386,70 @@ class DataFetcher {
|
|
|
312
386
|
throw e;
|
|
313
387
|
}
|
|
314
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* V2.8 FIX: JSON Detection Logic
|
|
392
|
+
*/
|
|
393
|
+
_normalizeRow(row) {
|
|
394
|
+
const normalized = { ...row };
|
|
395
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
396
|
+
if (typeof value === 'string') {
|
|
397
|
+
const trimmed = value.trim();
|
|
398
|
+
// FIX: Check for " (Double Encoded JSON) in addition to { and [
|
|
399
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"')) {
|
|
400
|
+
normalized[key] = this._safeRecursiveParse(value);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return normalized;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* V2.7 FIX: Helper to safely recursively parse JSON.
|
|
409
|
+
* Handles: Double-Encoded JSON Strings (parsed recursively)
|
|
410
|
+
*/
|
|
411
|
+
_safeRecursiveParse(input) {
|
|
412
|
+
if (!input) return null;
|
|
413
|
+
if (typeof input === 'object') return input;
|
|
414
|
+
try {
|
|
415
|
+
const parsed = JSON.parse(input);
|
|
416
|
+
// Recursion for double-encoded strings
|
|
417
|
+
if (typeof parsed === 'string') return this._safeRecursiveParse(parsed);
|
|
418
|
+
return parsed;
|
|
419
|
+
} catch (e) {
|
|
420
|
+
return input; // Not JSON, return original
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* V2.6: Generate a unique cache key for a query
|
|
426
|
+
*/
|
|
427
|
+
_generateCacheKey(query) {
|
|
428
|
+
// Hash the SQL + Params to ensure uniqueness
|
|
429
|
+
const str = query.sql + JSON.stringify(query.params || {});
|
|
430
|
+
return crypto.createHash('md5').update(str).digest('hex');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* V2.6: Add to cache with LRU eviction
|
|
435
|
+
*/
|
|
436
|
+
_addToCache(query, rows) {
|
|
437
|
+
// Generate key
|
|
438
|
+
const key = this._generateCacheKey(query);
|
|
439
|
+
|
|
440
|
+
// Eviction Logic
|
|
441
|
+
if (this.cache.size >= this.cacheConfig.maxSize) {
|
|
442
|
+
// Map iterator yields in insertion order. First item is oldest.
|
|
443
|
+
const oldestKey = this.cache.keys().next().value;
|
|
444
|
+
this.cache.delete(oldestKey);
|
|
445
|
+
this.stats.cacheEvictions++;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.cache.set(key, {
|
|
449
|
+
rows: rows,
|
|
450
|
+
timestamp: Date.now()
|
|
451
|
+
});
|
|
452
|
+
}
|
|
315
453
|
|
|
316
454
|
/**
|
|
317
455
|
* Transforms raw rows into a structured object.
|