aiden-shared-calculations-unified 1.0.34 → 1.0.36
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 +77 -77
- package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
- package/calculations/activity/historical/daily_asset_activity.js +85 -85
- package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
- package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
- package/calculations/asset_metrics/asset_position_size.js +57 -57
- package/calculations/backtests/strategy-performance.js +229 -245
- package/calculations/behavioural/historical/asset_crowd_flow.js +165 -170
- package/calculations/behavioural/historical/drawdown_response.js +58 -58
- package/calculations/behavioural/historical/dumb-cohort-flow.js +249 -249
- package/calculations/behavioural/historical/gain_response.js +57 -57
- package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
- package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
- package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
- package/calculations/behavioural/historical/position_count_pnl.js +67 -67
- package/calculations/behavioural/historical/smart-cohort-flow.js +250 -250
- package/calculations/behavioural/historical/smart_money_flow.js +165 -165
- package/calculations/behavioural/historical/user-investment-profile.js +412 -412
- package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
- package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
- package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
- package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
- package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
- package/calculations/insights/daily_ownership_delta.js +55 -55
- package/calculations/insights/daily_total_positions_held.js +39 -39
- package/calculations/meta/capital_deployment_strategy.js +129 -137
- package/calculations/meta/capital_liquidation_performance.js +121 -163
- package/calculations/meta/capital_vintage_performance.js +121 -158
- package/calculations/meta/cash-flow-deployment.js +110 -124
- package/calculations/meta/cash-flow-liquidation.js +126 -142
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
- package/calculations/meta/profit_cohort_divergence.js +77 -91
- package/calculations/meta/smart-dumb-divergence-index.js +116 -138
- package/calculations/meta/social_flow_correlation.js +99 -125
- package/calculations/pnl/asset_pnl_status.js +46 -46
- package/calculations/pnl/historical/profitability_migration.js +57 -57
- package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
- package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
- package/calculations/sectors/historical/diversification_pnl.js +76 -76
- package/calculations/sectors/historical/sector_rotation.js +67 -67
- package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
- package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
- package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
- package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
- package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
- package/calculations/socialPosts/social_activity_aggregation.js +103 -103
- package/calculations/socialPosts/social_event_correlation.js +121 -121
- package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
- package/calculations/speculators/historical/risk_appetite_change.js +54 -54
- package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
- package/index.js +33 -33
- package/package.json +32 -32
- package/utils/firestore_utils.js +76 -76
- package/utils/price_data_provider.js +142 -142
- package/utils/sector_mapping_provider.js +74 -74
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compares the average P/L of speculator users who use TSL
|
|
3
|
-
* vs. those who do not.
|
|
4
|
-
*/
|
|
5
|
-
class TslEffectiveness {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.tsl_group = { pnl_sum: 0, count: 0 };
|
|
8
|
-
this.nontsl_group = { pnl_sum: 0, count: 0 };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* FIX: Helper function to calculate total P&L from positions
|
|
13
|
-
* @param {object} portfolio
|
|
14
|
-
* @returns {number|null}
|
|
15
|
-
*/
|
|
16
|
-
_calculateTotalPnl(portfolio) {
|
|
17
|
-
// Speculators use PublicPositions
|
|
18
|
-
const positions = portfolio?.PublicPositions;
|
|
19
|
-
if (positions && Array.isArray(positions)) {
|
|
20
|
-
// Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
|
|
21
|
-
return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
27
|
-
// Check if user is a speculator and we have today's data
|
|
28
|
-
// FIX: yesterdayPortfolio is not needed for this logic, only today's P&L
|
|
29
|
-
if (todayPortfolio?.context?.userType !== 'speculator' || !todayPortfolio) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const positions = todayPortfolio.PublicPositions;
|
|
34
|
-
|
|
35
|
-
if (!positions || !Array.isArray(positions)) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// FIX: Calculate dailyPnl by summing NetProfit from all positions
|
|
40
|
-
const dailyPnl = this._calculateTotalPnl(todayPortfolio);
|
|
41
|
-
|
|
42
|
-
if (dailyPnl === null) {
|
|
43
|
-
return; // Cannot calculate P&L
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const usesTSL = positions.some(p => p.IsTslEnabled);
|
|
47
|
-
|
|
48
|
-
if (usesTSL) {
|
|
49
|
-
this.tsl_group.pnl_sum += dailyPnl;
|
|
50
|
-
this.tsl_group.count++;
|
|
51
|
-
} else {
|
|
52
|
-
this.nontsl_group.pnl_sum += dailyPnl;
|
|
53
|
-
this.nontsl_group.count++;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getResult() {
|
|
58
|
-
// Return final calculated averages
|
|
59
|
-
const tsl_avg_pnl = (this.tsl_group.count > 0) ? this.tsl_group.pnl_sum / this.tsl_group.count : 0;
|
|
60
|
-
const nontsl_avg_pnl = (this.nontsl_group.count > 0) ? this.nontsl_group.pnl_sum / this.nontsl_group.count : 0;
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
tsl_users: {
|
|
64
|
-
avg_pnl: tsl_avg_pnl,
|
|
65
|
-
count: this.tsl_group.count
|
|
66
|
-
},
|
|
67
|
-
nontsl_users: {
|
|
68
|
-
avg_pnl: nontsl_avg_pnl,
|
|
69
|
-
count: this.nontsl_group.count
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Compares the average P/L of speculator users who use TSL
|
|
3
|
+
* vs. those who do not.
|
|
4
|
+
*/
|
|
5
|
+
class TslEffectiveness {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.tsl_group = { pnl_sum: 0, count: 0 };
|
|
8
|
+
this.nontsl_group = { pnl_sum: 0, count: 0 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* FIX: Helper function to calculate total P&L from positions
|
|
13
|
+
* @param {object} portfolio
|
|
14
|
+
* @returns {number|null}
|
|
15
|
+
*/
|
|
16
|
+
_calculateTotalPnl(portfolio) {
|
|
17
|
+
// Speculators use PublicPositions
|
|
18
|
+
const positions = portfolio?.PublicPositions;
|
|
19
|
+
if (positions && Array.isArray(positions)) {
|
|
20
|
+
// Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
|
|
21
|
+
return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
27
|
+
// Check if user is a speculator and we have today's data
|
|
28
|
+
// FIX: yesterdayPortfolio is not needed for this logic, only today's P&L
|
|
29
|
+
if (todayPortfolio?.context?.userType !== 'speculator' || !todayPortfolio) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const positions = todayPortfolio.PublicPositions;
|
|
34
|
+
|
|
35
|
+
if (!positions || !Array.isArray(positions)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// FIX: Calculate dailyPnl by summing NetProfit from all positions
|
|
40
|
+
const dailyPnl = this._calculateTotalPnl(todayPortfolio);
|
|
41
|
+
|
|
42
|
+
if (dailyPnl === null) {
|
|
43
|
+
return; // Cannot calculate P&L
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const usesTSL = positions.some(p => p.IsTslEnabled);
|
|
47
|
+
|
|
48
|
+
if (usesTSL) {
|
|
49
|
+
this.tsl_group.pnl_sum += dailyPnl;
|
|
50
|
+
this.tsl_group.count++;
|
|
51
|
+
} else {
|
|
52
|
+
this.nontsl_group.pnl_sum += dailyPnl;
|
|
53
|
+
this.nontsl_group.count++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getResult() {
|
|
58
|
+
// Return final calculated averages
|
|
59
|
+
const tsl_avg_pnl = (this.tsl_group.count > 0) ? this.tsl_group.pnl_sum / this.tsl_group.count : 0;
|
|
60
|
+
const nontsl_avg_pnl = (this.nontsl_group.count > 0) ? this.nontsl_group.pnl_sum / this.nontsl_group.count : 0;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
tsl_users: {
|
|
64
|
+
avg_pnl: tsl_avg_pnl,
|
|
65
|
+
count: this.tsl_group.count
|
|
66
|
+
},
|
|
67
|
+
nontsl_users: {
|
|
68
|
+
avg_pnl: nontsl_avg_pnl,
|
|
69
|
+
count: this.nontsl_group.count
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
75
|
module.exports = TslEffectiveness;
|
package/index.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* index.js
|
|
3
|
-
* Main entry point for the Unified Calculations package.
|
|
4
|
-
*
|
|
5
|
-
* This file uses require-all to export all calculations dynamically
|
|
6
|
-
* from their respective subdirectories. It also manually exports utility
|
|
7
|
-
* functions for use within the calculations.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const requireAll = require('require-all');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
|
|
13
|
-
// --- Utils (Manually Exported) ---
|
|
14
|
-
const firestoreUtils = require('./utils/firestore_utils');
|
|
15
|
-
const mappingProvider = require('./utils/sector_mapping_provider');
|
|
16
|
-
const priceProvider = require('./utils/price_data_provider'); // <-- ADD THIS
|
|
17
|
-
|
|
18
|
-
// --- Calculations (Dynamically Loaded) ---
|
|
19
|
-
const calculations = requireAll({
|
|
20
|
-
dirname: path.join(__dirname, 'calculations'), // Use __dirname to get the correct path
|
|
21
|
-
filter: /(.+)\.js$/, // Only load .js files
|
|
22
|
-
recursive: true, // Load from subdirectories
|
|
23
|
-
map: name => name.replace(/_/g, '-') // Convert snake_case filenames to kebab-case keys
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
module.exports = {
|
|
27
|
-
calculations,
|
|
28
|
-
utils: {
|
|
29
|
-
...firestoreUtils,
|
|
30
|
-
...mappingProvider,
|
|
31
|
-
...priceProvider // <-- ADD THIS
|
|
32
|
-
}
|
|
33
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* index.js
|
|
3
|
+
* Main entry point for the Unified Calculations package.
|
|
4
|
+
*
|
|
5
|
+
* This file uses require-all to export all calculations dynamically
|
|
6
|
+
* from their respective subdirectories. It also manually exports utility
|
|
7
|
+
* functions for use within the calculations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const requireAll = require('require-all');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// --- Utils (Manually Exported) ---
|
|
14
|
+
const firestoreUtils = require('./utils/firestore_utils');
|
|
15
|
+
const mappingProvider = require('./utils/sector_mapping_provider');
|
|
16
|
+
const priceProvider = require('./utils/price_data_provider'); // <-- ADD THIS
|
|
17
|
+
|
|
18
|
+
// --- Calculations (Dynamically Loaded) ---
|
|
19
|
+
const calculations = requireAll({
|
|
20
|
+
dirname: path.join(__dirname, 'calculations'), // Use __dirname to get the correct path
|
|
21
|
+
filter: /(.+)\.js$/, // Only load .js files
|
|
22
|
+
recursive: true, // Load from subdirectories
|
|
23
|
+
map: name => name.replace(/_/g, '-') // Convert snake_case filenames to kebab-case keys
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
calculations,
|
|
28
|
+
utils: {
|
|
29
|
+
...firestoreUtils,
|
|
30
|
+
...mappingProvider,
|
|
31
|
+
...priceProvider // <-- ADD THIS
|
|
32
|
+
}
|
|
33
|
+
};
|
package/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "aiden-shared-calculations-unified",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Shared calculation modules for the BullTrackers Computation System.",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"index.js",
|
|
8
|
-
"calculations/",
|
|
9
|
-
"utils/"
|
|
10
|
-
],
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
-
},
|
|
14
|
-
"keywords": [
|
|
15
|
-
"bulltrackers",
|
|
16
|
-
"etoro",
|
|
17
|
-
"precompute",
|
|
18
|
-
"calculations",
|
|
19
|
-
"finance"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@google-cloud/firestore": "^7.11.3",
|
|
23
|
-
"sharedsetup": "latest",
|
|
24
|
-
"require-all": "^3.0.0"
|
|
25
|
-
},
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=20"
|
|
28
|
-
},
|
|
29
|
-
"publishConfig": {
|
|
30
|
-
"access": "public"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "aiden-shared-calculations-unified",
|
|
3
|
+
"version": "1.0.36",
|
|
4
|
+
"description": "Shared calculation modules for the BullTrackers Computation System.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"calculations/",
|
|
9
|
+
"utils/"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bulltrackers",
|
|
16
|
+
"etoro",
|
|
17
|
+
"precompute",
|
|
18
|
+
"calculations",
|
|
19
|
+
"finance"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@google-cloud/firestore": "^7.11.3",
|
|
23
|
+
"sharedsetup": "latest",
|
|
24
|
+
"require-all": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/utils/firestore_utils.js
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Provides a resilient retry wrapper for Firestore operations.
|
|
3
|
-
*/
|
|
4
|
-
const { logger } = require("sharedsetup")(__filename); // Note: In the package, you might need to adjust how the logger is obtained if sharedsetup isn't a direct dependency or passed in.
|
|
5
|
-
|
|
6
|
-
// Firestore error codes that are safe to retry
|
|
7
|
-
const RETRYABLE_ERROR_CODES = new Set([
|
|
8
|
-
'DEADLINE_EXCEEDED',
|
|
9
|
-
'UNAVAILABLE',
|
|
10
|
-
'INTERNAL',
|
|
11
|
-
'RESOURCE_EXHAUSTED', // Can indicate contention
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
const MAX_RETRIES = 5;
|
|
15
|
-
const INITIAL_BACKOFF_MS = 1000;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Delays execution for a specified number of milliseconds.
|
|
19
|
-
* @param {number} ms The number of milliseconds to wait.
|
|
20
|
-
* @returns {Promise<void>}
|
|
21
|
-
*/
|
|
22
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Wraps an asynchronous Firestore operation with an exponential backoff retry mechanism.
|
|
26
|
-
*
|
|
27
|
-
* @param {() => Promise<T>} asyncOperation The async function to execute.
|
|
28
|
-
* @param {string} operationName A descriptive name for the operation (for logging).
|
|
29
|
-
* @returns {Promise<T>} The result of the async operation.
|
|
30
|
-
* @template T
|
|
31
|
-
*/
|
|
32
|
-
async function withRetry(asyncOperation, operationName = 'Firestore operation') {
|
|
33
|
-
let lastError = null;
|
|
34
|
-
let backoff = INITIAL_BACKOFF_MS;
|
|
35
|
-
|
|
36
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
37
|
-
try {
|
|
38
|
-
return await asyncOperation();
|
|
39
|
-
} catch (error) {
|
|
40
|
-
lastError = error;
|
|
41
|
-
const code = error.code || error.details;
|
|
42
|
-
|
|
43
|
-
if (RETRYABLE_ERROR_CODES.has(code)) {
|
|
44
|
-
// Assuming logger is available globally or passed in somehow in the package context
|
|
45
|
-
if (typeof logger !== 'undefined' && logger.log) {
|
|
46
|
-
logger.log('WARN', `[Retry ${attempt}/${MAX_RETRIES}] Retrying ${operationName} due to ${code}. Waiting ${backoff}ms...`);
|
|
47
|
-
} else {
|
|
48
|
-
console.warn(`[Retry ${attempt}/${MAX_RETRIES}] Retrying ${operationName} due to ${code}. Waiting ${backoff}ms...`);
|
|
49
|
-
}
|
|
50
|
-
await sleep(backoff);
|
|
51
|
-
backoff *= 2; // Exponential backoff
|
|
52
|
-
} else {
|
|
53
|
-
if (typeof logger !== 'undefined' && logger.log) {
|
|
54
|
-
logger.log('ERROR', `[Retry] Non-retryable error during ${operationName}: ${code}`, { errorMessage: error.message });
|
|
55
|
-
} else {
|
|
56
|
-
console.error(`[Retry] Non-retryable error during ${operationName}: ${code}`, { errorMessage: error.message });
|
|
57
|
-
}
|
|
58
|
-
throw error; // Not a retryable error, re-throw immediately
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (typeof logger !== 'undefined' && logger.log) {
|
|
64
|
-
logger.log('ERROR', `[Retry] ${operationName} failed after ${MAX_RETRIES} attempts.`, {
|
|
65
|
-
finalErrorCode: lastError?.code || lastError?.details, // Added safe navigation
|
|
66
|
-
errorMessage: lastError?.message // Added safe navigation
|
|
67
|
-
});
|
|
68
|
-
} else {
|
|
69
|
-
console.error(`[Retry] ${operationName} failed after ${MAX_RETRIES} attempts.`, {
|
|
70
|
-
finalErrorCode: lastError?.code || lastError?.details,
|
|
71
|
-
errorMessage: lastError?.message
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
throw lastError; // All retries failed, throw the last error
|
|
75
|
-
}
|
|
76
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a resilient retry wrapper for Firestore operations.
|
|
3
|
+
*/
|
|
4
|
+
const { logger } = require("sharedsetup")(__filename); // Note: In the package, you might need to adjust how the logger is obtained if sharedsetup isn't a direct dependency or passed in.
|
|
5
|
+
|
|
6
|
+
// Firestore error codes that are safe to retry
|
|
7
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
8
|
+
'DEADLINE_EXCEEDED',
|
|
9
|
+
'UNAVAILABLE',
|
|
10
|
+
'INTERNAL',
|
|
11
|
+
'RESOURCE_EXHAUSTED', // Can indicate contention
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const MAX_RETRIES = 5;
|
|
15
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Delays execution for a specified number of milliseconds.
|
|
19
|
+
* @param {number} ms The number of milliseconds to wait.
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wraps an asynchronous Firestore operation with an exponential backoff retry mechanism.
|
|
26
|
+
*
|
|
27
|
+
* @param {() => Promise<T>} asyncOperation The async function to execute.
|
|
28
|
+
* @param {string} operationName A descriptive name for the operation (for logging).
|
|
29
|
+
* @returns {Promise<T>} The result of the async operation.
|
|
30
|
+
* @template T
|
|
31
|
+
*/
|
|
32
|
+
async function withRetry(asyncOperation, operationName = 'Firestore operation') {
|
|
33
|
+
let lastError = null;
|
|
34
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
35
|
+
|
|
36
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
return await asyncOperation();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
lastError = error;
|
|
41
|
+
const code = error.code || error.details;
|
|
42
|
+
|
|
43
|
+
if (RETRYABLE_ERROR_CODES.has(code)) {
|
|
44
|
+
// Assuming logger is available globally or passed in somehow in the package context
|
|
45
|
+
if (typeof logger !== 'undefined' && logger.log) {
|
|
46
|
+
logger.log('WARN', `[Retry ${attempt}/${MAX_RETRIES}] Retrying ${operationName} due to ${code}. Waiting ${backoff}ms...`);
|
|
47
|
+
} else {
|
|
48
|
+
console.warn(`[Retry ${attempt}/${MAX_RETRIES}] Retrying ${operationName} due to ${code}. Waiting ${backoff}ms...`);
|
|
49
|
+
}
|
|
50
|
+
await sleep(backoff);
|
|
51
|
+
backoff *= 2; // Exponential backoff
|
|
52
|
+
} else {
|
|
53
|
+
if (typeof logger !== 'undefined' && logger.log) {
|
|
54
|
+
logger.log('ERROR', `[Retry] Non-retryable error during ${operationName}: ${code}`, { errorMessage: error.message });
|
|
55
|
+
} else {
|
|
56
|
+
console.error(`[Retry] Non-retryable error during ${operationName}: ${code}`, { errorMessage: error.message });
|
|
57
|
+
}
|
|
58
|
+
throw error; // Not a retryable error, re-throw immediately
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof logger !== 'undefined' && logger.log) {
|
|
64
|
+
logger.log('ERROR', `[Retry] ${operationName} failed after ${MAX_RETRIES} attempts.`, {
|
|
65
|
+
finalErrorCode: lastError?.code || lastError?.details, // Added safe navigation
|
|
66
|
+
errorMessage: lastError?.message // Added safe navigation
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
console.error(`[Retry] ${operationName} failed after ${MAX_RETRIES} attempts.`, {
|
|
70
|
+
finalErrorCode: lastError?.code || lastError?.details,
|
|
71
|
+
errorMessage: lastError?.message
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw lastError; // All retries failed, throw the last error
|
|
75
|
+
}
|
|
76
|
+
|
|
77
77
|
module.exports = { withRetry, sleep };
|