bulltrackers-module 1.0.306 → 1.0.308

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.
@@ -1,247 +1,130 @@
1
1
  /**
2
2
  * FILENAME: computation-system/utils/utils.js
3
+ * UPDATED: Removed hardcoded date logic. Earliest dates are now fully data-driven.
3
4
  */
4
5
 
5
6
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
6
7
  const crypto = require('crypto');
7
8
 
8
- // [NEW] Single Source of Truth for Data Availability
9
- // These are defaults/fallbacks. The dynamic function getEarliestDataDates below is the primary source.
9
+ // [UPDATED] Registry for Data Availability.
10
+ // Populated dynamically by getEarliestDataDates().
10
11
  const DEFINITIVE_EARLIEST_DATES = {
11
- portfolio: new Date('2025-01-01T00:00:00Z'),
12
- history: new Date('2025-08-01T00:00:00Z'),
13
- social: new Date('2025-08-01T00:00:00Z'),
14
- insights: new Date('2025-08-01T00:00:00Z'),
15
- price: new Date('2025-08-01T00:00:00Z'),
16
- absoluteEarliest: new Date('2023-08-01T00:00:00Z')
12
+ portfolio: null,
13
+ history: null,
14
+ social: null,
15
+ insights: null,
16
+ price: null,
17
+ absoluteEarliest: null
17
18
  };
18
19
 
19
- /** Stage 1: Normalize a calculation name to kebab-case */
20
+ /** Normalizes a calculation name to kebab-case */
20
21
  function normalizeName(name) { return name.replace(/_/g, '-'); }
21
22
 
22
- /**
23
- * Generates a SHA-256 hash of a code string.
24
- */
23
+ /** Generates a SHA-256 hash of a code string. */
25
24
  function generateCodeHash(codeString) {
26
25
  if (!codeString) return 'unknown';
27
- let clean = codeString.replace(/\/\/.*$/gm, '');
28
- clean = clean.replace(/\/\*[\s\S]*?\*\//g, '');
29
- clean = clean.replace(/\s+/g, '');
26
+ let clean = codeString.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, '');
30
27
  return crypto.createHash('sha256').update(clean).digest('hex');
31
28
  }
32
29
 
33
- /**
34
- * [NEW] Generates a stable SHA-256 hash of a data object.
35
- * Keys are sorted to ensure determinism.
36
- */
30
+ /** Generates a stable SHA-256 hash of a data object. */
37
31
  function generateDataHash(data) {
38
32
  if (data === undefined) return 'undefined';
39
-
40
- // Recursive stable stringify
41
33
  const stableStringify = (obj) => {
42
- if (typeof obj !== 'object' || obj === null) {
43
- return JSON.stringify(obj);
44
- }
45
- if (Array.isArray(obj)) {
46
- return '[' + obj.map(stableStringify).join(',') + ']';
47
- }
48
- return '{' + Object.keys(obj).sort().map(k =>
49
- JSON.stringify(k) + ':' + stableStringify(obj[k])
50
- ).join(',') + '}';
34
+ if (typeof obj !== 'object' || obj === null) return JSON.stringify(obj);
35
+ if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
36
+ return '{' + Object.keys(obj).sort().map(k => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
51
37
  };
52
-
53
38
  try {
54
- const str = stableStringify(data);
55
- return crypto.createHash('sha256').update(str).digest('hex');
56
- } catch (e) {
57
- return 'hash_error';
58
- }
39
+ return crypto.createHash('sha256').update(stableStringify(data)).digest('hex');
40
+ } catch (e) { return 'hash_error'; }
59
41
  }
60
42
 
61
- /**
62
- * Executes a function with exponential backoff retry logic.
63
- * @param {Function} fn - Async function to execute
64
- * @param {string} operationName - Label for logging
65
- * @param {number} maxRetries - Max attempts (default 3)
66
- */
43
+ /** Exponential backoff retry logic. */
67
44
  async function withRetry(fn, operationName, maxRetries = 3) {
68
45
  let attempt = 0;
69
46
  while (attempt < maxRetries) {
70
- try {
71
- return await fn();
72
- } catch (error) {
47
+ try { return await fn(); } catch (error) {
73
48
  attempt++;
74
- console.warn(`[Retry] ${operationName} failed (Attempt ${attempt}/${maxRetries}): ${error.message}`);
75
49
  if (attempt >= maxRetries) throw error;
76
- // Exponential backoff: 1s, 2s, 4s...
77
50
  await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
78
51
  }
79
52
  }
80
53
  }
81
54
 
82
- /** Stage 2: Commit a batch of writes in chunks
83
- * UPDATED: Now supports { type: 'DELETE' } in the write object.
84
- */
55
+ /** Commit a batch of writes in chunks. */
85
56
  async function commitBatchInChunks(config, deps, writes, operationName) {
86
57
  const { db, logger } = deps;
87
- // Use the local withRetry if not provided in deps
88
- const retryFn = (deps.calculationUtils && deps.calculationUtils.withRetry) ? deps.calculationUtils.withRetry : withRetry;
89
-
90
- if (!writes || !writes.length) {
91
- logger.log('WARN', `[${operationName}] No writes to commit.`);
92
- return;
93
- }
94
-
95
- const MAX_BATCH_OPS = 300;
96
- const MAX_BATCH_BYTES = 9 * 1024 * 1024;
58
+ const retryFn = (deps.calculationUtils?.withRetry) || withRetry;
59
+ if (!writes?.length) return;
97
60
 
98
- let currentBatch = db.batch();
99
- let currentOpsCount = 0;
100
- let currentBytesEst = 0;
101
- let batchIndex = 1;
61
+ const MAX_BATCH_OPS = 300, MAX_BATCH_BYTES = 9 * 1024 * 1024;
62
+ let currentBatch = db.batch(), currentOpsCount = 0, currentBytesEst = 0, batchIndex = 1;
102
63
 
103
64
  const commitAndReset = async () => {
104
65
  if (currentOpsCount > 0) {
105
- try {
106
- await retryFn(
107
- () => currentBatch.commit(),
108
- `${operationName} (Chunk ${batchIndex})`
109
- );
110
- logger.log('INFO', `[${operationName}] Committed chunk ${batchIndex} (${currentOpsCount} ops, ~${(currentBytesEst / 1024 / 1024).toFixed(2)} MB).`);
111
- batchIndex++;
112
- } catch (err) {
113
- logger.log('ERROR', `[${operationName}] Failed to commit chunk ${batchIndex}. Size: ${(currentBytesEst / 1024 / 1024).toFixed(2)} MB.`, { error: err.message });
114
- throw err;
115
- }
66
+ await retryFn(() => currentBatch.commit(), `${operationName} (Chunk ${batchIndex})`);
67
+ batchIndex++;
116
68
  }
117
- currentBatch = db.batch();
118
- currentOpsCount = 0;
119
- currentBytesEst = 0;
69
+ currentBatch = db.batch(); currentOpsCount = 0; currentBytesEst = 0;
120
70
  };
121
71
 
122
72
  for (const write of writes) {
123
- // [NEW] Handle DELETE operations
124
73
  if (write.type === 'DELETE') {
125
- if ((currentOpsCount + 1 > MAX_BATCH_OPS)) {
126
- await commitAndReset();
127
- }
128
- currentBatch.delete(write.ref);
129
- currentOpsCount++;
130
- continue;
74
+ if (currentOpsCount + 1 > MAX_BATCH_OPS) await commitAndReset();
75
+ currentBatch.delete(write.ref); currentOpsCount++; continue;
131
76
  }
132
-
133
- // Standard SET/UPDATE operations
134
77
  let docSize = 100;
135
78
  try { if (write.data) docSize = JSON.stringify(write.data).length; } catch (e) { }
136
-
137
- if (docSize > 900 * 1024) {
138
- logger.log('WARN', `[${operationName}] Large document detected (~${(docSize / 1024).toFixed(2)} KB).`);
139
- }
140
-
141
- if ((currentOpsCount + 1 > MAX_BATCH_OPS) || (currentBytesEst + docSize > MAX_BATCH_BYTES)) {
142
- await commitAndReset();
143
- }
144
-
145
- // USE PROVIDED OPTIONS OR DEFAULT TO MERGE: TRUE
146
- const options = write.options || { merge: true };
147
- currentBatch.set(write.ref, write.data, options);
148
-
149
- currentOpsCount++;
150
- currentBytesEst += docSize;
79
+ if ((currentOpsCount + 1 > MAX_BATCH_OPS) || (currentBytesEst + docSize > MAX_BATCH_BYTES)) await commitAndReset();
80
+ currentBatch.set(write.ref, write.data, write.options || { merge: true });
81
+ currentOpsCount++; currentBytesEst += docSize;
151
82
  }
152
-
153
83
  await commitAndReset();
154
84
  }
155
85
 
156
- /** Stage 3: Generate an array of expected date strings between two dates */
86
+ /** Generate array of date strings between two dates. */
157
87
  function getExpectedDateStrings(startDate, endDate) {
158
88
  const dateStrings = [];
159
- if (startDate <= endDate) {
89
+ if (startDate && endDate && startDate <= endDate) {
160
90
  const startUTC = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()));
161
- const endUTC = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()));
162
- for (let d = startUTC; d <= endUTC; d.setUTCDate(d.getUTCDate() + 1)) { dateStrings.push(new Date(d).toISOString().slice(0, 10)); }
91
+ for (let d = startUTC; d <= endDate; d.setUTCDate(d.getUTCDate() + 1)) { dateStrings.push(new Date(d).toISOString().slice(0, 10)); }
163
92
  }
164
93
  return dateStrings;
165
94
  }
166
95
 
167
- /** * Stage 4: Get the earliest date from the Centralized Root Data Index.
168
- * This REPLACES the expensive/error-prone raw collection scanning.
169
- * It queries the 'system_root_data_index' to find the first date where data flags are TRUE.
96
+ /**
97
+ * [UPDATED] Single Source of Truth for Data Availability.
98
+ * Updates the global DEFINITIVE_EARLIEST_DATES object.
170
99
  */
171
100
  async function getEarliestDataDates(config, deps) {
172
101
  const { db, logger } = deps;
173
- // Default to 'system_root_data_index' if not configured
174
- const indexCollection = process.env.ROOT_DATA_AVAILABILITY_COLLECTION ||
175
- config.rootDataAvailabilityCollection ||
176
- 'system_root_data_index';
102
+ const indexCollection = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'system_root_data_index';
177
103
 
178
- // Helper to find earliest date where a specific flag is true
179
- // Efficient: Uses the document ID (date string) index.
180
104
  const getEarliestForType = async (flagName) => {
181
105
  try {
182
- const snapshot = await db.collection(indexCollection)
183
- .where(flagName, '==', true)
184
- .orderBy(FieldPath.documentId(), 'asc')
185
- .limit(1)
186
- .get();
187
-
188
- if (!snapshot.empty) {
189
- const dateStr = snapshot.docs[0].id; // YYYY-MM-DD
190
- // Safety check on date format
191
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
192
- return new Date(dateStr + 'T00:00:00Z');
193
- }
194
- }
195
- } catch (e) {
196
- logger.log('WARN', `[Utils] Failed to query index for ${flagName}: ${e.message}`);
197
- }
106
+ const snap = await db.collection(indexCollection).where(flagName, '==', true).orderBy(FieldPath.documentId(), 'asc').limit(1).get();
107
+ if (!snap.empty) return new Date(snap.docs[0].id + 'T00:00:00Z');
108
+ } catch (e) { logger.log('WARN', `[Utils] Index query failed for ${flagName}`); }
198
109
  return null;
199
110
  };
200
111
 
201
- // Parallel query for all data types
202
- const [portfolioDate, historyDate, socialDate, insightsDate, priceDate] = await Promise.all([
203
- getEarliestForType('hasPortfolio'),
204
- getEarliestForType('hasHistory'),
205
- getEarliestForType('hasSocial'),
206
- getEarliestForType('hasInsights'),
207
- getEarliestForType('hasPrices')
112
+ const [portfolio, history, social, insights, price] = await Promise.all([
113
+ getEarliestForType('hasPortfolio'), getEarliestForType('hasHistory'),
114
+ getEarliestForType('hasSocial'), getEarliestForType('hasInsights'), getEarliestForType('hasPrices')
208
115
  ]);
209
116
 
210
- // Calculate absolute earliest among found dates
211
- const foundDates = [portfolioDate, historyDate, socialDate, insightsDate, priceDate].filter(d => d !== null);
212
-
213
- let absoluteEarliest = null;
214
- if (foundDates.length > 0) {
215
- absoluteEarliest = new Date(Math.min(...foundDates));
216
- } else {
217
- // Fallback if index is empty
218
- const configStart = config.earliestComputationDate || '2023-01-01';
219
- absoluteEarliest = new Date(configStart + 'T00:00:00Z');
220
- logger.log('WARN', `[Utils] No data found in Root Data Index (${indexCollection}). Defaulting to ${configStart}`);
221
- }
117
+ const found = [portfolio, history, social, insights, price].filter(d => d !== null);
118
+ const absoluteEarliest = found.length > 0 ? new Date(Math.min(...found)) : new Date('2023-01-01T00:00:00Z');
222
119
 
223
- // Update the static export for consumers who use it (best effort synchronization)
224
- DEFINITIVE_EARLIEST_DATES.absoluteEarliest = absoluteEarliest;
120
+ // Sync the global registry
121
+ Object.assign(DEFINITIVE_EARLIEST_DATES, { portfolio, history, social, insights, price, absoluteEarliest });
225
122
 
226
- return {
227
- portfolio: portfolioDate || new Date('2999-12-31T00:00:00Z'),
228
- history: historyDate || new Date('2999-12-31T00:00:00Z'),
229
- insights: insightsDate || new Date('2999-12-31T00:00:00Z'),
230
- social: socialDate || new Date('2999-12-31T00:00:00Z'),
231
- price: priceDate || new Date('2999-12-31T00:00:00Z'),
232
- absoluteEarliest: absoluteEarliest
233
- };
123
+ return DEFINITIVE_EARLIEST_DATES;
234
124
  }
235
125
 
236
126
  module.exports = {
237
- FieldValue,
238
- FieldPath,
239
- normalizeName,
240
- commitBatchInChunks,
241
- getExpectedDateStrings,
242
- getEarliestDataDates,
243
- generateCodeHash,
244
- generateDataHash,
245
- withRetry,
246
- DEFINITIVE_EARLIEST_DATES
127
+ FieldValue, FieldPath, normalizeName, commitBatchInChunks,
128
+ getExpectedDateStrings, getEarliestDataDates, generateCodeHash,
129
+ generateDataHash, withRetry, DEFINITIVE_EARLIEST_DATES
247
130
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.306",
3
+ "version": "1.0.308",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [