@dizzlkheinz/ynab-mcpb 0.18.4 → 0.19.0
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/CLAUDE.md +87 -8
- package/bin/ynab-mcp-server.cjs +2 -2
- package/bin/ynab-mcp-server.js +3 -3
- package/biome.json +39 -0
- package/dist/bundle/index.cjs +67 -67
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -27
- package/dist/server/YNABMCPServer.d.ts +3 -4
- package/dist/server/YNABMCPServer.js +111 -116
- package/dist/server/budgetResolver.d.ts +6 -5
- package/dist/server/budgetResolver.js +46 -36
- package/dist/server/cacheKeys.js +6 -6
- package/dist/server/cacheManager.js +14 -11
- package/dist/server/completions.d.ts +2 -2
- package/dist/server/completions.js +20 -15
- package/dist/server/config.d.ts +10 -5
- package/dist/server/config.js +24 -7
- package/dist/server/deltaCache.d.ts +2 -2
- package/dist/server/deltaCache.js +22 -16
- package/dist/server/deltaCache.merge.d.ts +2 -2
- package/dist/server/diagnostics.d.ts +4 -4
- package/dist/server/diagnostics.js +38 -32
- package/dist/server/errorHandler.d.ts +5 -12
- package/dist/server/errorHandler.js +219 -217
- package/dist/server/prompts.d.ts +2 -2
- package/dist/server/prompts.js +45 -45
- package/dist/server/rateLimiter.js +4 -4
- package/dist/server/requestLogger.d.ts +1 -1
- package/dist/server/requestLogger.js +40 -35
- package/dist/server/resources.d.ts +3 -3
- package/dist/server/resources.js +55 -52
- package/dist/server/responseFormatter.js +6 -6
- package/dist/server/securityMiddleware.d.ts +2 -2
- package/dist/server/securityMiddleware.js +22 -20
- package/dist/server/serverKnowledgeStore.js +1 -1
- package/dist/server/toolRegistry.d.ts +3 -3
- package/dist/server/toolRegistry.js +47 -40
- package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
- package/dist/tools/__tests__/deltaTestUtils.js +2 -2
- package/dist/tools/accountTools.d.ts +9 -8
- package/dist/tools/accountTools.js +47 -47
- package/dist/tools/adapters.d.ts +13 -8
- package/dist/tools/adapters.js +21 -11
- package/dist/tools/budgetTools.d.ts +8 -7
- package/dist/tools/budgetTools.js +22 -22
- package/dist/tools/categoryTools.d.ts +9 -8
- package/dist/tools/categoryTools.js +68 -59
- package/dist/tools/compareTransactions/formatter.d.ts +3 -3
- package/dist/tools/compareTransactions/formatter.js +9 -9
- package/dist/tools/compareTransactions/index.d.ts +6 -6
- package/dist/tools/compareTransactions/index.js +58 -43
- package/dist/tools/compareTransactions/matcher.d.ts +1 -1
- package/dist/tools/compareTransactions/matcher.js +28 -15
- package/dist/tools/compareTransactions/parser.d.ts +2 -2
- package/dist/tools/compareTransactions/parser.js +144 -138
- package/dist/tools/compareTransactions/types.d.ts +4 -4
- package/dist/tools/compareTransactions.d.ts +1 -1
- package/dist/tools/compareTransactions.js +1 -1
- package/dist/tools/deltaFetcher.d.ts +2 -2
- package/dist/tools/deltaFetcher.js +16 -15
- package/dist/tools/deltaSupport.d.ts +4 -4
- package/dist/tools/deltaSupport.js +35 -41
- package/dist/tools/exportTransactions.d.ts +5 -4
- package/dist/tools/exportTransactions.js +61 -59
- package/dist/tools/monthTools.d.ts +7 -6
- package/dist/tools/monthTools.js +31 -29
- package/dist/tools/payeeTools.d.ts +7 -6
- package/dist/tools/payeeTools.js +28 -28
- package/dist/tools/reconcileAdapter.d.ts +2 -2
- package/dist/tools/reconcileAdapter.js +19 -12
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +73 -59
- package/dist/tools/reconciliation/csvParser.d.ts +3 -3
- package/dist/tools/reconciliation/csvParser.js +128 -104
- package/dist/tools/reconciliation/executor.d.ts +4 -4
- package/dist/tools/reconciliation/executor.js +148 -109
- package/dist/tools/reconciliation/index.d.ts +10 -10
- package/dist/tools/reconciliation/index.js +96 -83
- package/dist/tools/reconciliation/matcher.d.ts +3 -3
- package/dist/tools/reconciliation/matcher.js +17 -16
- package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
- package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
- package/dist/tools/reconciliation/recommendationEngine.js +40 -40
- package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
- package/dist/tools/reconciliation/reportFormatter.js +59 -58
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +16 -16
- package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
- package/dist/tools/schemas/common.d.ts +1 -1
- package/dist/tools/schemas/common.js +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
- package/dist/tools/schemas/outputs/index.d.ts +14 -14
- package/dist/tools/schemas/outputs/index.js +14 -14
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
- package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
- package/dist/tools/schemas/shared/commonOutputs.js +15 -9
- package/dist/tools/transactionReadTools.d.ts +11 -0
- package/dist/tools/transactionReadTools.js +202 -0
- package/dist/tools/transactionSchemas.d.ts +7 -7
- package/dist/tools/transactionSchemas.js +77 -57
- package/dist/tools/transactionTools.d.ts +6 -24
- package/dist/tools/transactionTools.js +7 -1499
- package/dist/tools/transactionUtils.d.ts +6 -6
- package/dist/tools/transactionUtils.js +78 -63
- package/dist/tools/transactionWriteTools.d.ts +20 -0
- package/dist/tools/transactionWriteTools.js +1342 -0
- package/dist/tools/utilityTools.d.ts +5 -4
- package/dist/tools/utilityTools.js +11 -11
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +6 -6
- package/dist/types/reconciliation.d.ts +1 -1
- package/dist/types/toolRegistration.d.ts +14 -12
- package/dist/utils/amountUtils.js +1 -1
- package/dist/utils/dateUtils.js +4 -4
- package/dist/utils/errors.d.ts +3 -3
- package/dist/utils/errors.js +4 -4
- package/dist/utils/money.d.ts +2 -2
- package/dist/utils/money.js +8 -8
- package/dist/utils/validationError.d.ts +1 -1
- package/dist/utils/validationError.js +1 -1
- package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
- package/docs/assets/schemas/reconciliation-v2.json +360 -336
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -111
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +178 -169
- package/scripts/run-domain-integration-tests.js +28 -18
- package/scripts/run-generate-mcpb.js +19 -17
- package/scripts/run-throttled-integration-tests.js +92 -83
- package/scripts/test-delta-params.mjs +149 -120
- package/scripts/test-recommendations.ts +36 -32
- package/scripts/tmpTransaction.ts +80 -43
- package/scripts/validate-env.js +98 -91
- package/scripts/verify-build.js +78 -76
- package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
- package/src/__tests__/performance.test.ts +723 -671
- package/src/__tests__/setup.ts +442 -395
- package/src/__tests__/smoke.e2e.test.ts +41 -39
- package/src/__tests__/testRunner.ts +314 -295
- package/src/__tests__/testUtils.ts +456 -364
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
- package/src/index.ts +68 -59
- package/src/server/CLAUDE.md +480 -0
- package/src/server/YNABMCPServer.ts +821 -794
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
- package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
- package/src/server/__tests__/budgetResolver.test.ts +466 -423
- package/src/server/__tests__/cacheManager.test.ts +891 -874
- package/src/server/__tests__/completions.integration.test.ts +115 -106
- package/src/server/__tests__/completions.test.ts +334 -313
- package/src/server/__tests__/config.test.ts +98 -86
- package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
- package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
- package/src/server/__tests__/deltaCache.test.ts +946 -759
- package/src/server/__tests__/diagnostics.test.ts +825 -792
- package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
- package/src/server/__tests__/errorHandler.test.ts +402 -397
- package/src/server/__tests__/prompts.test.ts +424 -347
- package/src/server/__tests__/rateLimiter.test.ts +313 -309
- package/src/server/__tests__/requestLogger.test.ts +443 -403
- package/src/server/__tests__/resources.template.test.ts +196 -185
- package/src/server/__tests__/resources.test.ts +294 -288
- package/src/server/__tests__/security.integration.test.ts +487 -421
- package/src/server/__tests__/securityMiddleware.test.ts +519 -444
- package/src/server/__tests__/server-startup.integration.test.ts +509 -490
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
- package/src/server/__tests__/toolRegistration.test.ts +239 -210
- package/src/server/__tests__/toolRegistry.test.ts +907 -845
- package/src/server/budgetResolver.ts +221 -181
- package/src/server/cacheKeys.ts +6 -6
- package/src/server/cacheManager.ts +498 -484
- package/src/server/completions.ts +267 -243
- package/src/server/config.ts +35 -14
- package/src/server/deltaCache.merge.ts +146 -128
- package/src/server/deltaCache.ts +352 -309
- package/src/server/diagnostics.ts +257 -242
- package/src/server/errorHandler.ts +747 -744
- package/src/server/prompts.ts +181 -176
- package/src/server/rateLimiter.ts +131 -129
- package/src/server/requestLogger.ts +350 -322
- package/src/server/resources.ts +442 -374
- package/src/server/responseFormatter.ts +41 -37
- package/src/server/securityMiddleware.ts +223 -205
- package/src/server/serverKnowledgeStore.ts +67 -67
- package/src/server/toolRegistry.ts +508 -474
- package/src/tools/CLAUDE.md +604 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
- package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
- package/src/tools/__tests__/accountTools.test.ts +685 -638
- package/src/tools/__tests__/adapters.test.ts +142 -108
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
- package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
- package/src/tools/__tests__/budgetTools.test.ts +442 -413
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
- package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
- package/src/tools/__tests__/categoryTools.test.ts +656 -625
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
- package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
- package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
- package/src/tools/__tests__/compareTransactions.test.ts +352 -332
- package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
- package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
- package/src/tools/__tests__/deltaSupport.test.ts +211 -184
- package/src/tools/__tests__/deltaTestUtils.ts +37 -33
- package/src/tools/__tests__/exportTransactions.test.ts +205 -200
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
- package/src/tools/__tests__/monthTools.test.ts +561 -512
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
- package/src/tools/__tests__/payeeTools.test.ts +486 -434
- package/src/tools/__tests__/transactionSchemas.test.ts +1202 -1186
- package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
- package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
- package/src/tools/__tests__/transactionUtils.test.ts +1004 -977
- package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
- package/src/tools/__tests__/utilityTools.test.ts +68 -58
- package/src/tools/accountTools.ts +293 -271
- package/src/tools/adapters.ts +120 -63
- package/src/tools/budgetTools.ts +121 -116
- package/src/tools/categoryTools.ts +379 -339
- package/src/tools/compareTransactions/formatter.ts +131 -119
- package/src/tools/compareTransactions/index.ts +249 -214
- package/src/tools/compareTransactions/matcher.ts +259 -209
- package/src/tools/compareTransactions/parser.ts +517 -487
- package/src/tools/compareTransactions/types.ts +38 -38
- package/src/tools/compareTransactions.ts +1 -1
- package/src/tools/deltaFetcher.ts +281 -260
- package/src/tools/deltaSupport.ts +264 -259
- package/src/tools/exportTransactions.ts +230 -218
- package/src/tools/monthTools.ts +180 -165
- package/src/tools/payeeTools.ts +152 -140
- package/src/tools/reconcileAdapter.ts +297 -252
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +133 -124
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -230
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -400
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
- package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
- package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -989
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -533
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -74
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -62
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +56 -55
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
- package/src/tools/reconciliation/analyzer.ts +564 -504
- package/src/tools/reconciliation/csvParser.ts +656 -609
- package/src/tools/reconciliation/executor.ts +1290 -1128
- package/src/tools/reconciliation/index.ts +580 -528
- package/src/tools/reconciliation/matcher.ts +256 -240
- package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
- package/src/tools/reconciliation/recommendationEngine.ts +357 -345
- package/src/tools/reconciliation/reportFormatter.ts +343 -307
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -159
- package/src/tools/reconciliation/ynabAdapter.ts +17 -15
- package/src/tools/schemas/CLAUDE.md +546 -0
- package/src/tools/schemas/common.ts +1 -1
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
- package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
- package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
- package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
- package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
- package/src/tools/schemas/outputs/index.ts +163 -163
- package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
- package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
- package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
- package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
- package/src/tools/schemas/shared/commonOutputs.ts +27 -19
- package/src/tools/toolCategories.ts +114 -114
- package/src/tools/transactionReadTools.ts +327 -0
- package/src/tools/transactionSchemas.ts +322 -291
- package/src/tools/transactionTools.ts +84 -2246
- package/src/tools/transactionUtils.ts +507 -422
- package/src/tools/transactionWriteTools.ts +2110 -0
- package/src/tools/utilityTools.ts +46 -41
- package/src/types/CLAUDE.md +477 -0
- package/src/types/__tests__/index.test.ts +51 -51
- package/src/types/index.ts +43 -39
- package/src/types/integration-tests.d.ts +26 -26
- package/src/types/reconciliation.ts +29 -29
- package/src/types/toolAnnotations.ts +30 -30
- package/src/types/toolRegistration.ts +43 -32
- package/src/utils/CLAUDE.md +508 -0
- package/src/utils/__tests__/dateUtils.test.ts +174 -168
- package/src/utils/__tests__/money.test.ts +193 -187
- package/src/utils/amountUtils.ts +5 -5
- package/src/utils/baseError.ts +5 -5
- package/src/utils/dateUtils.ts +29 -26
- package/src/utils/errors.ts +14 -14
- package/src/utils/money.ts +66 -52
- package/src/utils/validationError.ts +1 -1
- package/tsconfig.json +29 -29
- package/tsconfig.prod.json +16 -16
- package/vitest-reporters/split-json-reporter.ts +247 -204
- package/vitest.config.ts +99 -95
- package/.prettierignore +0 -10
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -49
|
@@ -4,500 +4,514 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
interface CacheEntry<T> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
data: T;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
ttl: number;
|
|
10
|
+
staleWhileRevalidate?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
interface CacheSetOptions {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
ttl?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Stale-while-revalidate window in milliseconds.
|
|
17
|
+
* When explicitly set to undefined, uses the default stale window.
|
|
18
|
+
* When omitted entirely, no stale-while-revalidate is applied.
|
|
19
|
+
*/
|
|
20
|
+
staleWhileRevalidate?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export class CacheManager {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
24
|
+
private cache = new Map<string, CacheEntry<unknown>>();
|
|
25
|
+
private readonly defaultTTL: number;
|
|
26
|
+
private hits = 0;
|
|
27
|
+
private misses = 0;
|
|
28
|
+
private evictions = 0;
|
|
29
|
+
private lastCleanup: number | null = null;
|
|
30
|
+
private maxEntries: number;
|
|
31
|
+
private defaultStaleWindow: number;
|
|
32
|
+
private pendingFetches = new Map<string, Promise<unknown>>();
|
|
33
|
+
private pendingRefresh = new Set<string>();
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.maxEntries = this.parseEnvInt("YNAB_MCP_CACHE_MAX_ENTRIES", 1000);
|
|
37
|
+
this.defaultStaleWindow = this.parseEnvInt(
|
|
38
|
+
"YNAB_MCP_CACHE_STALE_MS",
|
|
39
|
+
2 * 60 * 1000,
|
|
40
|
+
);
|
|
41
|
+
this.defaultTTL = this.parseEnvInt("YNAB_MCP_CACHE_DEFAULT_TTL_MS", 300000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get cached data if valid, null if expired or not found
|
|
46
|
+
*/
|
|
47
|
+
get<T>(key: string): T | null {
|
|
48
|
+
const entry = this.cache.get(key);
|
|
49
|
+
|
|
50
|
+
if (!entry) {
|
|
51
|
+
this.misses++;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const age = now - entry.timestamp;
|
|
57
|
+
|
|
58
|
+
// Check if entry is expired
|
|
59
|
+
if (age > entry.ttl) {
|
|
60
|
+
// Check if we're within stale-while-revalidate window
|
|
61
|
+
const staleWindow = entry.staleWhileRevalidate || 0;
|
|
62
|
+
if (staleWindow > 0 && age <= entry.ttl + staleWindow) {
|
|
63
|
+
this.hits++;
|
|
64
|
+
// Update access order for LRU
|
|
65
|
+
this.cache.delete(key);
|
|
66
|
+
this.cache.set(key, entry);
|
|
67
|
+
// Mark for background refresh
|
|
68
|
+
this.pendingRefresh.add(key);
|
|
69
|
+
return entry.data as T;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.cache.delete(key);
|
|
73
|
+
this.pendingFetches.delete(key);
|
|
74
|
+
this.pendingRefresh.delete(key);
|
|
75
|
+
this.misses++;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.hits++;
|
|
80
|
+
// Update access order for LRU
|
|
81
|
+
this.cache.delete(key);
|
|
82
|
+
this.cache.set(key, entry);
|
|
83
|
+
return entry.data as T;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a valid cache entry exists without updating hit/miss counters
|
|
88
|
+
*/
|
|
89
|
+
has(key: string): boolean {
|
|
90
|
+
const entry = this.cache.get(key);
|
|
91
|
+
if (!entry) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const age = now - entry.timestamp;
|
|
97
|
+
if (age > entry.ttl) {
|
|
98
|
+
const staleWindow = entry.staleWhileRevalidate || 0;
|
|
99
|
+
if (staleWindow > 0 && age <= entry.ttl + staleWindow) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.cache.delete(key);
|
|
104
|
+
this.pendingFetches.delete(key);
|
|
105
|
+
this.pendingRefresh.delete(key);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set cache entry with optional TTL or options
|
|
114
|
+
*
|
|
115
|
+
* @param key - Cache key
|
|
116
|
+
* @param data - Data to cache
|
|
117
|
+
* @param ttlOrOptions - TTL in milliseconds (number) or options object
|
|
118
|
+
*
|
|
119
|
+
* Note: Default stale-while-revalidate window is applied only when:
|
|
120
|
+
* - An options object is provided AND
|
|
121
|
+
* - The staleWhileRevalidate property is explicitly present (even if undefined)
|
|
122
|
+
*
|
|
123
|
+
* When using the simple number interface or when staleWhileRevalidate property
|
|
124
|
+
* is not present in the options object, no default stale window is applied.
|
|
125
|
+
*/
|
|
126
|
+
set<T>(key: string, data: T, ttlOrOptions?: number | CacheSetOptions): void {
|
|
127
|
+
// Don't cache anything if maxEntries is 0
|
|
128
|
+
if (this.maxEntries <= 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isUpdate = this.cache.has(key);
|
|
133
|
+
if (!isUpdate) {
|
|
134
|
+
this.evictIfNeeded();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let ttl: number;
|
|
138
|
+
let staleWhileRevalidate: number | undefined;
|
|
139
|
+
|
|
140
|
+
if (typeof ttlOrOptions === "number") {
|
|
141
|
+
ttl = Number.isFinite(ttlOrOptions) ? ttlOrOptions : this.defaultTTL;
|
|
142
|
+
// When using simple number interface, no stale window is applied
|
|
143
|
+
staleWhileRevalidate = undefined;
|
|
144
|
+
} else if (ttlOrOptions === undefined) {
|
|
145
|
+
// When called without any options (simple set), use defaults but NO stale window
|
|
146
|
+
ttl = this.defaultTTL;
|
|
147
|
+
staleWhileRevalidate = undefined;
|
|
148
|
+
} else {
|
|
149
|
+
const providedTtl = ttlOrOptions?.ttl;
|
|
150
|
+
ttl = providedTtl !== undefined ? providedTtl : this.defaultTTL;
|
|
151
|
+
const hasStaleWhileRevalidate =
|
|
152
|
+
ttlOrOptions !== undefined && "staleWhileRevalidate" in ttlOrOptions;
|
|
153
|
+
if (hasStaleWhileRevalidate) {
|
|
154
|
+
staleWhileRevalidate = ttlOrOptions.staleWhileRevalidate;
|
|
155
|
+
if (staleWhileRevalidate === undefined && this.defaultStaleWindow > 0) {
|
|
156
|
+
staleWhileRevalidate = this.defaultStaleWindow;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
staleWhileRevalidate = undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const entry: CacheEntry<T> = {
|
|
163
|
+
data,
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
ttl,
|
|
166
|
+
};
|
|
167
|
+
if (staleWhileRevalidate !== undefined) {
|
|
168
|
+
entry.staleWhileRevalidate = staleWhileRevalidate;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isUpdate) {
|
|
172
|
+
// When updating, delete then set to preserve MRU ordering
|
|
173
|
+
this.cache.delete(key);
|
|
174
|
+
}
|
|
175
|
+
this.cache.set(key, entry);
|
|
176
|
+
// Clear any pending operations since we have fresh data
|
|
177
|
+
this.pendingFetches.delete(key);
|
|
178
|
+
this.pendingRefresh.delete(key);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Clear specific cache entry
|
|
183
|
+
*/
|
|
184
|
+
delete(key: string): boolean {
|
|
185
|
+
const deleted = this.cache.delete(key);
|
|
186
|
+
if (deleted) {
|
|
187
|
+
this.pendingFetches.delete(key);
|
|
188
|
+
this.pendingRefresh.delete(key);
|
|
189
|
+
}
|
|
190
|
+
return deleted;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Delete multiple cache entries in a single operation
|
|
195
|
+
*/
|
|
196
|
+
deleteMany(keys: Iterable<string>): void {
|
|
197
|
+
for (const key of keys) {
|
|
198
|
+
this.cache.delete(key);
|
|
199
|
+
this.pendingFetches.delete(key);
|
|
200
|
+
this.pendingRefresh.delete(key);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Delete cache entries whose keys begin with the provided prefix.
|
|
206
|
+
* Useful for invalidating a specific resource type across budgets.
|
|
207
|
+
*
|
|
208
|
+
* @param prefix - Cache key prefix (e.g., 'transactions:' or 'accounts:list:')
|
|
209
|
+
* @returns The number of entries removed
|
|
210
|
+
*/
|
|
211
|
+
deleteByPrefix(prefix: string): number {
|
|
212
|
+
if (!prefix) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const normalizedPrefix = prefix.endsWith(":")
|
|
217
|
+
? prefix.slice(0, -1)
|
|
218
|
+
: prefix;
|
|
219
|
+
const prefixWithColon = `${normalizedPrefix}:`;
|
|
220
|
+
|
|
221
|
+
let removed = 0;
|
|
222
|
+
for (const key of this.cache.keys()) {
|
|
223
|
+
if (key === normalizedPrefix || key.startsWith(prefixWithColon)) {
|
|
224
|
+
this.cache.delete(key);
|
|
225
|
+
this.pendingFetches.delete(key);
|
|
226
|
+
this.pendingRefresh.delete(key);
|
|
227
|
+
removed++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return removed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Delete cache entries that belong to a specific budget.
|
|
235
|
+
* Matches keys containing the budget ID (e.g., '...:budget-123:...').
|
|
236
|
+
*
|
|
237
|
+
* @param budgetId - Budget identifier to match
|
|
238
|
+
*/
|
|
239
|
+
deleteByBudgetId(budgetId: string): number {
|
|
240
|
+
if (!budgetId) {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let removed = 0;
|
|
245
|
+
for (const key of this.cache.keys()) {
|
|
246
|
+
const segments = key.split(":");
|
|
247
|
+
if (segments.some((segment) => segment === budgetId)) {
|
|
248
|
+
this.cache.delete(key);
|
|
249
|
+
this.pendingFetches.delete(key);
|
|
250
|
+
this.pendingRefresh.delete(key);
|
|
251
|
+
removed++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return removed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Return all cache keys for debugging and diagnostics.
|
|
259
|
+
*
|
|
260
|
+
* @returns Snapshot of cache keys in insertion order
|
|
261
|
+
*/
|
|
262
|
+
getKeys(): string[] {
|
|
263
|
+
return Array.from(this.cache.keys());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Clear all cache entries
|
|
268
|
+
*/
|
|
269
|
+
clear(): void {
|
|
270
|
+
this.cache.clear();
|
|
271
|
+
this.hits = 0;
|
|
272
|
+
this.misses = 0;
|
|
273
|
+
this.evictions = 0;
|
|
274
|
+
this.lastCleanup = null;
|
|
275
|
+
this.pendingFetches.clear();
|
|
276
|
+
this.pendingRefresh.clear();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get cache statistics
|
|
281
|
+
*/
|
|
282
|
+
getStats(): {
|
|
283
|
+
size: number;
|
|
284
|
+
keys: string[];
|
|
285
|
+
hits: number;
|
|
286
|
+
misses: number;
|
|
287
|
+
evictions: number;
|
|
288
|
+
lastCleanup: number | null;
|
|
289
|
+
maxEntries: number;
|
|
290
|
+
hitRate: number;
|
|
291
|
+
} {
|
|
292
|
+
const totalRequests = this.hits + this.misses;
|
|
293
|
+
return {
|
|
294
|
+
size: this.cache.size,
|
|
295
|
+
keys: Array.from(this.cache.keys()),
|
|
296
|
+
hits: this.hits,
|
|
297
|
+
misses: this.misses,
|
|
298
|
+
evictions: this.evictions,
|
|
299
|
+
lastCleanup: this.lastCleanup,
|
|
300
|
+
maxEntries: this.maxEntries,
|
|
301
|
+
hitRate: totalRequests > 0 ? this.hits / totalRequests : 0,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Provide a filtered snapshot for cache size estimation without exposing expired entries.
|
|
307
|
+
*/
|
|
308
|
+
getEntriesForSizeEstimation(): [string, CacheEntry<unknown>][] {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
return Array.from(this.cache.entries()).filter(
|
|
311
|
+
([, entry]) => now - entry.timestamp <= entry.ttl,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get lightweight cache metadata for size estimation without full entry data.
|
|
317
|
+
* Returns summaries with keys, timestamps, and TTLs for estimating memory usage.
|
|
318
|
+
*/
|
|
319
|
+
getCacheMetadata(): {
|
|
320
|
+
key: string;
|
|
321
|
+
timestamp: number;
|
|
322
|
+
ttl: number;
|
|
323
|
+
staleWhileRevalidate?: number;
|
|
324
|
+
dataType: string;
|
|
325
|
+
isExpired: boolean;
|
|
326
|
+
}[] {
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
return Array.from(this.cache.entries()).map(([key, entry]) => {
|
|
329
|
+
const metadata: {
|
|
330
|
+
key: string;
|
|
331
|
+
timestamp: number;
|
|
332
|
+
ttl: number;
|
|
333
|
+
staleWhileRevalidate?: number;
|
|
334
|
+
dataType: string;
|
|
335
|
+
isExpired: boolean;
|
|
336
|
+
} = {
|
|
337
|
+
key,
|
|
338
|
+
timestamp: entry.timestamp,
|
|
339
|
+
ttl: entry.ttl,
|
|
340
|
+
dataType: typeof entry.data,
|
|
341
|
+
isExpired: now - entry.timestamp > entry.ttl,
|
|
342
|
+
};
|
|
343
|
+
if (entry.staleWhileRevalidate !== undefined) {
|
|
344
|
+
metadata.staleWhileRevalidate = entry.staleWhileRevalidate;
|
|
345
|
+
}
|
|
346
|
+
return metadata;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Clean up expired entries
|
|
352
|
+
*/
|
|
353
|
+
cleanup(): number {
|
|
354
|
+
const result = this.cleanupDetailed();
|
|
355
|
+
return result.cleaned;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Clean up expired entries with detailed information
|
|
360
|
+
*/
|
|
361
|
+
cleanupDetailed(): { cleaned: number; evictions: number } {
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
let cleaned = 0;
|
|
364
|
+
const initialEvictions = this.evictions;
|
|
365
|
+
|
|
366
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
367
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
368
|
+
this.cache.delete(key);
|
|
369
|
+
this.pendingFetches.delete(key);
|
|
370
|
+
this.pendingRefresh.delete(key);
|
|
371
|
+
cleaned++;
|
|
372
|
+
this.evictions++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.lastCleanup = now;
|
|
377
|
+
return { cleaned, evictions: this.evictions - initialEvictions };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Wrap a loader function with caching and concurrent deduplication
|
|
382
|
+
*/
|
|
383
|
+
async wrap<T>(
|
|
384
|
+
key: string,
|
|
385
|
+
options: CacheSetOptions & { loader: () => Promise<T> },
|
|
386
|
+
): Promise<T> {
|
|
387
|
+
// Check cache first and preserve existing entry for background refresh
|
|
388
|
+
const existingEntry = this.cache.get(key);
|
|
389
|
+
const cached = this.get<T>(key);
|
|
390
|
+
if (cached !== null) {
|
|
391
|
+
// Check if this key was marked for background refresh (stale-while-revalidate)
|
|
392
|
+
if (this.pendingRefresh.has(key) && !this.pendingFetches.has(key)) {
|
|
393
|
+
// Start background refresh
|
|
394
|
+
const refreshPromise = options.loader().then(
|
|
395
|
+
(result) => {
|
|
396
|
+
// Preserve existing TTL/SWR if not specified in options
|
|
397
|
+
const refreshOptions: CacheSetOptions = {};
|
|
398
|
+
const ttl = options.ttl ?? existingEntry?.ttl;
|
|
399
|
+
if (ttl !== undefined) {
|
|
400
|
+
refreshOptions.ttl = ttl;
|
|
401
|
+
}
|
|
402
|
+
const staleWhileRevalidate =
|
|
403
|
+
options.staleWhileRevalidate ??
|
|
404
|
+
existingEntry?.staleWhileRevalidate;
|
|
405
|
+
if (staleWhileRevalidate !== undefined) {
|
|
406
|
+
refreshOptions.staleWhileRevalidate = staleWhileRevalidate;
|
|
407
|
+
}
|
|
408
|
+
// Cache the successful result
|
|
409
|
+
this.set(key, result, refreshOptions);
|
|
410
|
+
// Clean up
|
|
411
|
+
this.pendingFetches.delete(key);
|
|
412
|
+
this.pendingRefresh.delete(key);
|
|
413
|
+
return result;
|
|
414
|
+
},
|
|
415
|
+
(error) => {
|
|
416
|
+
// Clean up on error
|
|
417
|
+
this.pendingFetches.delete(key);
|
|
418
|
+
this.pendingRefresh.delete(key);
|
|
419
|
+
throw error;
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
this.pendingFetches.set(key, refreshPromise);
|
|
423
|
+
}
|
|
424
|
+
return cached;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Check if there's already a pending fetch for this key
|
|
428
|
+
const existingFetch = this.pendingFetches.get(key) as
|
|
429
|
+
| Promise<T>
|
|
430
|
+
| undefined;
|
|
431
|
+
if (existingFetch) {
|
|
432
|
+
return existingFetch;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Execute the loader
|
|
436
|
+
const fetchPromise = options.loader().then(
|
|
437
|
+
(result) => {
|
|
438
|
+
// Cache the successful result using provided options (no existing entry to preserve)
|
|
439
|
+
this.set(key, result, options);
|
|
440
|
+
// Clean up pending fetch
|
|
441
|
+
this.pendingFetches.delete(key);
|
|
442
|
+
this.pendingRefresh.delete(key);
|
|
443
|
+
return result;
|
|
444
|
+
},
|
|
445
|
+
(error) => {
|
|
446
|
+
// Clean up on error, don't cache failures
|
|
447
|
+
this.pendingFetches.delete(key);
|
|
448
|
+
this.pendingRefresh.delete(key);
|
|
449
|
+
throw error;
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Store the pending fetch
|
|
454
|
+
this.pendingFetches.set(key, fetchPromise);
|
|
455
|
+
return fetchPromise;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Evict least recently used entries if cache is at capacity
|
|
460
|
+
*/
|
|
461
|
+
private evictIfNeeded(): void {
|
|
462
|
+
if (this.maxEntries <= 0) return;
|
|
463
|
+
|
|
464
|
+
while (this.cache.size >= this.maxEntries) {
|
|
465
|
+
// Get the first (oldest) entry
|
|
466
|
+
const firstKey = this.cache.keys().next().value;
|
|
467
|
+
if (firstKey) {
|
|
468
|
+
this.cache.delete(firstKey);
|
|
469
|
+
this.pendingFetches.delete(firstKey);
|
|
470
|
+
this.pendingRefresh.delete(firstKey);
|
|
471
|
+
this.evictions++;
|
|
472
|
+
} else {
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Parse environment variable as integer with fallback
|
|
480
|
+
*/
|
|
481
|
+
private parseEnvInt(key: string, defaultValue: number): number {
|
|
482
|
+
const value = process.env[key];
|
|
483
|
+
if (!value) return defaultValue;
|
|
484
|
+
|
|
485
|
+
const parsed = Number.parseInt(value, 10);
|
|
486
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Generate cache key from parameters
|
|
491
|
+
*/
|
|
492
|
+
static generateKey(
|
|
493
|
+
prefix: string,
|
|
494
|
+
...params: (string | number | boolean | undefined)[]
|
|
495
|
+
): string {
|
|
496
|
+
const cleanParams = params
|
|
497
|
+
.filter((p) => p !== undefined)
|
|
498
|
+
.map((p) => String(p))
|
|
499
|
+
.join(":");
|
|
500
|
+
|
|
501
|
+
return `${prefix}:${cleanParams}`;
|
|
502
|
+
}
|
|
489
503
|
}
|
|
490
504
|
|
|
491
505
|
// Cache TTL configurations for different data types
|
|
492
506
|
export const CACHE_TTLS = {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
507
|
+
BUDGETS: 10 * 60 * 1000, // 10 minutes - budgets don't change often
|
|
508
|
+
ACCOUNTS: 5 * 60 * 1000, // 5 minutes - account info is fairly static
|
|
509
|
+
CATEGORIES: 5 * 60 * 1000, // 5 minutes - categories change infrequently
|
|
510
|
+
PAYEES: 10 * 60 * 1000, // 10 minutes - payees are relatively stable
|
|
511
|
+
TRANSACTIONS: 2 * 60 * 1000, // 2 minutes - transactions change more frequently
|
|
512
|
+
SCHEDULED_TRANSACTIONS: 5 * 60 * 1000, // 5 minutes - scheduled transactions rarely change rapidly
|
|
513
|
+
USER_INFO: 30 * 60 * 1000, // 30 minutes - user info rarely changes
|
|
514
|
+
MONTHS: 5 * 60 * 1000, // 5 minutes - month data changes with new transactions
|
|
501
515
|
} as const;
|
|
502
516
|
|
|
503
517
|
// Singleton cache manager instance
|