@sip-protocol/sdk 0.7.2 → 0.7.4
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/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +48874 -18336
- package/dist/browser.mjs +674 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/chunk-YWGJ77A2.mjs +33806 -0
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/index-DXh2IGkz.d.ts +24681 -0
- package/dist/index-DeE1ZzA4.d.mts +24681 -0
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48676 -17318
- package/dist/index.mjs +583 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +54 -21
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +276 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +201 -0
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +402 -0
- package/src/chains/solana/providers/index.ts +85 -0
- package/src/chains/solana/providers/interface.ts +221 -0
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +790 -0
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +170 -73
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +77 -7
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +37 -0
- package/src/compliance/range-sas.ts +956 -0
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +785 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +336 -0
- package/src/privacy-backends/interface.ts +906 -0
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash-types.ts +278 -0
- package/src/privacy-backends/privacycash.ts +456 -0
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +690 -0
- package/src/privacy-backends/router.ts +626 -0
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +256 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +111 -30
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/surveillance/algorithms/address-reuse.ts +143 -0
- package/src/surveillance/algorithms/cluster.ts +247 -0
- package/src/surveillance/algorithms/exchange.ts +295 -0
- package/src/surveillance/algorithms/temporal.ts +337 -0
- package/src/surveillance/analyzer.ts +442 -0
- package/src/surveillance/index.ts +64 -0
- package/src/surveillance/scoring.ts +372 -0
- package/src/surveillance/types.ts +264 -0
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-Q4NAVBTS.mjs +0 -46
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Cache Implementation for Proof Caching
|
|
3
|
+
*
|
|
4
|
+
* @module proofs/cache/persistent-cache
|
|
5
|
+
* @description IndexedDB (browser) and file-based (Node.js) persistent caching
|
|
6
|
+
*
|
|
7
|
+
* M20-13: Implement proof caching layer (#313)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SingleProof } from '@sip-protocol/types'
|
|
11
|
+
import type {
|
|
12
|
+
CacheKey,
|
|
13
|
+
CacheEntryMetadata,
|
|
14
|
+
CacheLookupResult,
|
|
15
|
+
ProofCacheStats,
|
|
16
|
+
CacheEvent,
|
|
17
|
+
CacheEventListener,
|
|
18
|
+
PersistentCacheConfig,
|
|
19
|
+
IPersistentCache,
|
|
20
|
+
} from './interface'
|
|
21
|
+
import { DEFAULT_PERSISTENT_CONFIG, INITIAL_PROOF_CACHE_STATS } from './interface'
|
|
22
|
+
|
|
23
|
+
// ─── Environment Detection ───────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function isBrowser(): boolean {
|
|
26
|
+
return typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isNode(): boolean {
|
|
30
|
+
return typeof process !== 'undefined' && process.versions?.node !== undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── IndexedDB Cache (Browser) ───────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const DB_VERSION = 1
|
|
36
|
+
const STORE_NAME = 'proofs'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* IndexedDB-based persistent cache for browsers
|
|
40
|
+
*/
|
|
41
|
+
export class IndexedDBCache<T = SingleProof> implements IPersistentCache<T> {
|
|
42
|
+
private readonly config: PersistentCacheConfig
|
|
43
|
+
private db: IDBDatabase | null = null
|
|
44
|
+
private readonly listeners: Set<CacheEventListener> = new Set()
|
|
45
|
+
private initialized = false
|
|
46
|
+
|
|
47
|
+
// Statistics
|
|
48
|
+
private totalLookups = 0
|
|
49
|
+
private hits = 0
|
|
50
|
+
private misses = 0
|
|
51
|
+
private totalLookupTimeMs = 0
|
|
52
|
+
|
|
53
|
+
constructor(config: Partial<PersistentCacheConfig> = {}) {
|
|
54
|
+
this.config = { ...DEFAULT_PERSISTENT_CONFIG, ...config }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initialize the IndexedDB storage
|
|
59
|
+
*/
|
|
60
|
+
async initialize(): Promise<void> {
|
|
61
|
+
if (this.initialized) return
|
|
62
|
+
if (!isBrowser()) {
|
|
63
|
+
throw new Error('IndexedDB is only available in browser environments')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const request = indexedDB.open(this.config.storageName, DB_VERSION)
|
|
68
|
+
|
|
69
|
+
request.onerror = () => {
|
|
70
|
+
reject(new Error(`Failed to open IndexedDB: ${request.error?.message}`))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
request.onsuccess = () => {
|
|
74
|
+
this.db = request.result
|
|
75
|
+
this.initialized = true
|
|
76
|
+
resolve()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
request.onupgradeneeded = (event) => {
|
|
80
|
+
const db = (event.target as IDBOpenDBRequest).result
|
|
81
|
+
|
|
82
|
+
// Create object store if it doesn't exist
|
|
83
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
84
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' })
|
|
85
|
+
store.createIndex('expiresAt', 'metadata.expiresAt', { unique: false })
|
|
86
|
+
store.createIndex('createdAt', 'metadata.createdAt', { unique: false })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Close the IndexedDB connection
|
|
94
|
+
*/
|
|
95
|
+
async close(): Promise<void> {
|
|
96
|
+
if (this.db) {
|
|
97
|
+
this.db.close()
|
|
98
|
+
this.db = null
|
|
99
|
+
this.initialized = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if IndexedDB is available
|
|
105
|
+
*/
|
|
106
|
+
isAvailable(): boolean {
|
|
107
|
+
return isBrowser() && typeof indexedDB !== 'undefined'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get storage usage information
|
|
112
|
+
*/
|
|
113
|
+
async getStorageInfo(): Promise<{ used: number; available: number; quota: number }> {
|
|
114
|
+
if (!isBrowser() || !navigator.storage?.estimate) {
|
|
115
|
+
return { used: 0, available: this.config.maxSizeBytes, quota: this.config.maxSizeBytes }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const estimate = await navigator.storage.estimate()
|
|
120
|
+
return {
|
|
121
|
+
used: estimate.usage ?? 0,
|
|
122
|
+
available: (estimate.quota ?? this.config.maxSizeBytes) - (estimate.usage ?? 0),
|
|
123
|
+
quota: estimate.quota ?? this.config.maxSizeBytes,
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
return { used: 0, available: this.config.maxSizeBytes, quota: this.config.maxSizeBytes }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compact the storage (remove expired entries)
|
|
132
|
+
*/
|
|
133
|
+
async compact(): Promise<void> {
|
|
134
|
+
if (!this.db) return
|
|
135
|
+
|
|
136
|
+
const now = Date.now()
|
|
137
|
+
const transaction = this.db.transaction(STORE_NAME, 'readwrite')
|
|
138
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
139
|
+
const index = store.index('expiresAt')
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const range = IDBKeyRange.bound(1, now) // Entries with expiresAt between 1 and now
|
|
143
|
+
const request = index.openCursor(range)
|
|
144
|
+
|
|
145
|
+
request.onsuccess = (event) => {
|
|
146
|
+
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
|
147
|
+
if (cursor) {
|
|
148
|
+
cursor.delete()
|
|
149
|
+
cursor.continue()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
transaction.oncomplete = () => resolve()
|
|
154
|
+
transaction.onerror = () => reject(transaction.error)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get an entry from the cache
|
|
160
|
+
*/
|
|
161
|
+
async get(key: CacheKey | string): Promise<CacheLookupResult<T>> {
|
|
162
|
+
const startTime = Date.now()
|
|
163
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
164
|
+
|
|
165
|
+
this.totalLookups++
|
|
166
|
+
|
|
167
|
+
if (!this.db) {
|
|
168
|
+
await this.initialize()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
const transaction = this.db!.transaction(STORE_NAME, 'readonly')
|
|
173
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
174
|
+
const request = store.get(keyStr)
|
|
175
|
+
|
|
176
|
+
request.onsuccess = () => {
|
|
177
|
+
const entry = request.result as StoredEntry<T> | undefined
|
|
178
|
+
|
|
179
|
+
if (!entry) {
|
|
180
|
+
this.misses++
|
|
181
|
+
this.updateLookupTime(startTime)
|
|
182
|
+
this.emitEvent({ type: 'miss', key: keyStr, timestamp: Date.now() })
|
|
183
|
+
resolve({
|
|
184
|
+
hit: false,
|
|
185
|
+
missReason: 'not_found',
|
|
186
|
+
lookupTimeMs: Date.now() - startTime,
|
|
187
|
+
})
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check expiration
|
|
192
|
+
if (entry.metadata.expiresAt > 0 && Date.now() > entry.metadata.expiresAt) {
|
|
193
|
+
this.delete(keyStr) // Async delete, don't wait
|
|
194
|
+
this.misses++
|
|
195
|
+
this.updateLookupTime(startTime)
|
|
196
|
+
this.emitEvent({ type: 'expire', key: keyStr, timestamp: Date.now() })
|
|
197
|
+
resolve({
|
|
198
|
+
hit: false,
|
|
199
|
+
missReason: 'expired',
|
|
200
|
+
lookupTimeMs: Date.now() - startTime,
|
|
201
|
+
})
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.hits++
|
|
206
|
+
this.updateLookupTime(startTime)
|
|
207
|
+
this.emitEvent({ type: 'hit', key: keyStr, timestamp: Date.now() })
|
|
208
|
+
|
|
209
|
+
resolve({
|
|
210
|
+
hit: true,
|
|
211
|
+
entry: {
|
|
212
|
+
key: typeof key === 'string'
|
|
213
|
+
? { key, components: {} as CacheKey['components'], generatedAt: 0 }
|
|
214
|
+
: key,
|
|
215
|
+
value: entry.value,
|
|
216
|
+
metadata: entry.metadata,
|
|
217
|
+
},
|
|
218
|
+
lookupTimeMs: Date.now() - startTime,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
request.onerror = () => {
|
|
223
|
+
this.misses++
|
|
224
|
+
this.updateLookupTime(startTime)
|
|
225
|
+
resolve({
|
|
226
|
+
hit: false,
|
|
227
|
+
missReason: 'invalid',
|
|
228
|
+
lookupTimeMs: Date.now() - startTime,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Set an entry in the cache
|
|
236
|
+
*/
|
|
237
|
+
async set(key: CacheKey | string, value: T, ttlMs?: number): Promise<boolean> {
|
|
238
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
239
|
+
const effectiveTtl = ttlMs ?? this.config.defaultTtlMs
|
|
240
|
+
|
|
241
|
+
if (!this.db) {
|
|
242
|
+
await this.initialize()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const metadata: CacheEntryMetadata = {
|
|
246
|
+
createdAt: Date.now(),
|
|
247
|
+
lastAccessedAt: Date.now(),
|
|
248
|
+
accessCount: 0,
|
|
249
|
+
sizeBytes: this.estimateSize(value),
|
|
250
|
+
ttlMs: effectiveTtl,
|
|
251
|
+
expiresAt: effectiveTtl > 0 ? Date.now() + effectiveTtl : 0,
|
|
252
|
+
source: 'generation',
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const entry: StoredEntry<T> = {
|
|
256
|
+
key: keyStr,
|
|
257
|
+
value,
|
|
258
|
+
metadata,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
const transaction = this.db!.transaction(STORE_NAME, 'readwrite')
|
|
263
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
264
|
+
const request = store.put(entry)
|
|
265
|
+
|
|
266
|
+
request.onsuccess = () => {
|
|
267
|
+
this.emitEvent({ type: 'set', key: keyStr, timestamp: Date.now() })
|
|
268
|
+
resolve(true)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
request.onerror = () => {
|
|
272
|
+
resolve(false)
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Delete an entry from the cache
|
|
279
|
+
*/
|
|
280
|
+
async delete(key: CacheKey | string): Promise<boolean> {
|
|
281
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
282
|
+
|
|
283
|
+
if (!this.db) return false
|
|
284
|
+
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
const transaction = this.db!.transaction(STORE_NAME, 'readwrite')
|
|
287
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
288
|
+
const request = store.delete(keyStr)
|
|
289
|
+
|
|
290
|
+
request.onsuccess = () => {
|
|
291
|
+
this.emitEvent({ type: 'delete', key: keyStr, timestamp: Date.now() })
|
|
292
|
+
resolve(true)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
request.onerror = () => {
|
|
296
|
+
resolve(false)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if an entry exists
|
|
303
|
+
*/
|
|
304
|
+
async has(key: CacheKey | string): Promise<boolean> {
|
|
305
|
+
const result = await this.get(key)
|
|
306
|
+
return result.hit
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Clear all entries
|
|
311
|
+
*/
|
|
312
|
+
async clear(): Promise<void> {
|
|
313
|
+
if (!this.db) return
|
|
314
|
+
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const transaction = this.db!.transaction(STORE_NAME, 'readwrite')
|
|
317
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
318
|
+
const request = store.clear()
|
|
319
|
+
|
|
320
|
+
request.onsuccess = () => {
|
|
321
|
+
this.emitEvent({ type: 'clear', timestamp: Date.now() })
|
|
322
|
+
resolve()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
request.onerror = () => {
|
|
326
|
+
reject(request.error)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get cache statistics
|
|
333
|
+
*/
|
|
334
|
+
getStats(): ProofCacheStats {
|
|
335
|
+
return {
|
|
336
|
+
...INITIAL_PROOF_CACHE_STATS,
|
|
337
|
+
totalLookups: this.totalLookups,
|
|
338
|
+
hits: this.hits,
|
|
339
|
+
misses: this.misses,
|
|
340
|
+
hitRate: this.totalLookups > 0 ? this.hits / this.totalLookups : 0,
|
|
341
|
+
maxSizeBytes: this.config.maxSizeBytes,
|
|
342
|
+
avgLookupTimeMs: this.totalLookups > 0 ? this.totalLookupTimeMs / this.totalLookups : 0,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get all keys matching a pattern
|
|
348
|
+
*/
|
|
349
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
350
|
+
if (!this.db) {
|
|
351
|
+
await this.initialize()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
const transaction = this.db!.transaction(STORE_NAME, 'readonly')
|
|
356
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
357
|
+
const request = store.getAllKeys()
|
|
358
|
+
|
|
359
|
+
request.onsuccess = () => {
|
|
360
|
+
let keys = request.result as string[]
|
|
361
|
+
|
|
362
|
+
if (pattern) {
|
|
363
|
+
const regexPattern = pattern
|
|
364
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
365
|
+
.replace(/\*/g, '.*')
|
|
366
|
+
.replace(/\?/g, '.')
|
|
367
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
368
|
+
keys = keys.filter((key) => regex.test(key))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
resolve(keys)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
request.onerror = () => {
|
|
375
|
+
reject(request.error)
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
addEventListener(listener: CacheEventListener): void {
|
|
381
|
+
this.listeners.add(listener)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
removeEventListener(listener: CacheEventListener): void {
|
|
385
|
+
this.listeners.delete(listener)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private estimateSize(value: T): number {
|
|
389
|
+
try {
|
|
390
|
+
const json = JSON.stringify(value, (_, v) => {
|
|
391
|
+
if (typeof v === 'bigint') return v.toString()
|
|
392
|
+
return v
|
|
393
|
+
})
|
|
394
|
+
return new TextEncoder().encode(json).length
|
|
395
|
+
} catch {
|
|
396
|
+
return 1024
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private updateLookupTime(startTime: number): void {
|
|
401
|
+
this.totalLookupTimeMs += Date.now() - startTime
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private emitEvent(event: CacheEvent): void {
|
|
405
|
+
for (const listener of this.listeners) {
|
|
406
|
+
try {
|
|
407
|
+
listener(event)
|
|
408
|
+
} catch {
|
|
409
|
+
// Ignore listener errors
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ─── File-based Cache (Node.js) ──────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
interface StoredEntry<T> {
|
|
418
|
+
key: string
|
|
419
|
+
value: T
|
|
420
|
+
metadata: CacheEntryMetadata
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* File-based persistent cache for Node.js
|
|
425
|
+
*/
|
|
426
|
+
export class FileCache<T = SingleProof> implements IPersistentCache<T> {
|
|
427
|
+
private readonly config: PersistentCacheConfig
|
|
428
|
+
private readonly listeners: Set<CacheEventListener> = new Set()
|
|
429
|
+
private initialized = false
|
|
430
|
+
private fs: typeof import('fs/promises') | null = null
|
|
431
|
+
private path: typeof import('path') | null = null
|
|
432
|
+
private cachePath = ''
|
|
433
|
+
|
|
434
|
+
// Statistics
|
|
435
|
+
private totalLookups = 0
|
|
436
|
+
private hits = 0
|
|
437
|
+
private misses = 0
|
|
438
|
+
private totalLookupTimeMs = 0
|
|
439
|
+
|
|
440
|
+
constructor(config: Partial<PersistentCacheConfig> = {}) {
|
|
441
|
+
this.config = { ...DEFAULT_PERSISTENT_CONFIG, ...config }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Initialize the file-based storage
|
|
446
|
+
*/
|
|
447
|
+
async initialize(): Promise<void> {
|
|
448
|
+
if (this.initialized) return
|
|
449
|
+
if (!isNode()) {
|
|
450
|
+
throw new Error('File-based cache is only available in Node.js environments')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Dynamic imports for Node.js modules
|
|
454
|
+
this.fs = await import('fs/promises')
|
|
455
|
+
this.path = await import('path')
|
|
456
|
+
|
|
457
|
+
// Determine cache path
|
|
458
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp'
|
|
459
|
+
this.cachePath = this.path.join(homeDir, '.cache', this.config.storageName)
|
|
460
|
+
|
|
461
|
+
// Ensure cache directory exists
|
|
462
|
+
await this.fs.mkdir(this.cachePath, { recursive: true })
|
|
463
|
+
|
|
464
|
+
this.initialized = true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Close the file cache (no-op for files)
|
|
469
|
+
*/
|
|
470
|
+
async close(): Promise<void> {
|
|
471
|
+
this.initialized = false
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Check if file system is available
|
|
476
|
+
*/
|
|
477
|
+
isAvailable(): boolean {
|
|
478
|
+
return isNode()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get storage usage information
|
|
483
|
+
*/
|
|
484
|
+
async getStorageInfo(): Promise<{ used: number; available: number; quota: number }> {
|
|
485
|
+
if (!this.fs || !this.initialized) {
|
|
486
|
+
return { used: 0, available: this.config.maxSizeBytes, quota: this.config.maxSizeBytes }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
let totalSize = 0
|
|
491
|
+
const files = await this.fs.readdir(this.cachePath)
|
|
492
|
+
|
|
493
|
+
for (const file of files) {
|
|
494
|
+
if (file.endsWith('.json')) {
|
|
495
|
+
const stat = await this.fs.stat(this.path!.join(this.cachePath, file))
|
|
496
|
+
totalSize += stat.size
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
used: totalSize,
|
|
502
|
+
available: this.config.maxSizeBytes - totalSize,
|
|
503
|
+
quota: this.config.maxSizeBytes,
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
return { used: 0, available: this.config.maxSizeBytes, quota: this.config.maxSizeBytes }
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Compact the storage (remove expired entries)
|
|
512
|
+
*/
|
|
513
|
+
async compact(): Promise<void> {
|
|
514
|
+
if (!this.fs || !this.initialized) return
|
|
515
|
+
|
|
516
|
+
const now = Date.now()
|
|
517
|
+
const files = await this.fs.readdir(this.cachePath)
|
|
518
|
+
|
|
519
|
+
for (const file of files) {
|
|
520
|
+
if (!file.endsWith('.json')) continue
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const filePath = this.path!.join(this.cachePath, file)
|
|
524
|
+
const content = await this.fs.readFile(filePath, 'utf-8')
|
|
525
|
+
const entry = JSON.parse(content) as StoredEntry<T>
|
|
526
|
+
|
|
527
|
+
if (entry.metadata.expiresAt > 0 && now > entry.metadata.expiresAt) {
|
|
528
|
+
await this.fs.unlink(filePath)
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
// Ignore errors, file might be corrupted
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get an entry from the cache
|
|
538
|
+
*/
|
|
539
|
+
async get(key: CacheKey | string): Promise<CacheLookupResult<T>> {
|
|
540
|
+
const startTime = Date.now()
|
|
541
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
542
|
+
|
|
543
|
+
this.totalLookups++
|
|
544
|
+
|
|
545
|
+
if (!this.initialized) {
|
|
546
|
+
await this.initialize()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const filePath = this.getFilePath(keyStr)
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const content = await this.fs!.readFile(filePath, 'utf-8')
|
|
553
|
+
const entry = JSON.parse(content) as StoredEntry<T>
|
|
554
|
+
|
|
555
|
+
// Check expiration
|
|
556
|
+
if (entry.metadata.expiresAt > 0 && Date.now() > entry.metadata.expiresAt) {
|
|
557
|
+
await this.fs!.unlink(filePath).catch(() => {})
|
|
558
|
+
this.misses++
|
|
559
|
+
this.updateLookupTime(startTime)
|
|
560
|
+
this.emitEvent({ type: 'expire', key: keyStr, timestamp: Date.now() })
|
|
561
|
+
return {
|
|
562
|
+
hit: false,
|
|
563
|
+
missReason: 'expired',
|
|
564
|
+
lookupTimeMs: Date.now() - startTime,
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
this.hits++
|
|
569
|
+
this.updateLookupTime(startTime)
|
|
570
|
+
this.emitEvent({ type: 'hit', key: keyStr, timestamp: Date.now() })
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
hit: true,
|
|
574
|
+
entry: {
|
|
575
|
+
key: typeof key === 'string'
|
|
576
|
+
? { key, components: {} as CacheKey['components'], generatedAt: 0 }
|
|
577
|
+
: key,
|
|
578
|
+
value: entry.value,
|
|
579
|
+
metadata: entry.metadata,
|
|
580
|
+
},
|
|
581
|
+
lookupTimeMs: Date.now() - startTime,
|
|
582
|
+
}
|
|
583
|
+
} catch {
|
|
584
|
+
this.misses++
|
|
585
|
+
this.updateLookupTime(startTime)
|
|
586
|
+
this.emitEvent({ type: 'miss', key: keyStr, timestamp: Date.now() })
|
|
587
|
+
return {
|
|
588
|
+
hit: false,
|
|
589
|
+
missReason: 'not_found',
|
|
590
|
+
lookupTimeMs: Date.now() - startTime,
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Set an entry in the cache
|
|
597
|
+
*/
|
|
598
|
+
async set(key: CacheKey | string, value: T, ttlMs?: number): Promise<boolean> {
|
|
599
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
600
|
+
const effectiveTtl = ttlMs ?? this.config.defaultTtlMs
|
|
601
|
+
|
|
602
|
+
if (!this.initialized) {
|
|
603
|
+
await this.initialize()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const metadata: CacheEntryMetadata = {
|
|
607
|
+
createdAt: Date.now(),
|
|
608
|
+
lastAccessedAt: Date.now(),
|
|
609
|
+
accessCount: 0,
|
|
610
|
+
sizeBytes: 0, // Will be calculated after serialization
|
|
611
|
+
ttlMs: effectiveTtl,
|
|
612
|
+
expiresAt: effectiveTtl > 0 ? Date.now() + effectiveTtl : 0,
|
|
613
|
+
source: 'generation',
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const entry: StoredEntry<T> = {
|
|
617
|
+
key: keyStr,
|
|
618
|
+
value,
|
|
619
|
+
metadata,
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const content = JSON.stringify(entry, (_, v) => {
|
|
624
|
+
if (typeof v === 'bigint') return `bigint:${v.toString()}`
|
|
625
|
+
return v
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
const filePath = this.getFilePath(keyStr)
|
|
629
|
+
await this.fs!.writeFile(filePath, content, 'utf-8')
|
|
630
|
+
|
|
631
|
+
this.emitEvent({ type: 'set', key: keyStr, timestamp: Date.now() })
|
|
632
|
+
return true
|
|
633
|
+
} catch {
|
|
634
|
+
return false
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Delete an entry from the cache
|
|
640
|
+
*/
|
|
641
|
+
async delete(key: CacheKey | string): Promise<boolean> {
|
|
642
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
643
|
+
|
|
644
|
+
if (!this.initialized) return false
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const filePath = this.getFilePath(keyStr)
|
|
648
|
+
await this.fs!.unlink(filePath)
|
|
649
|
+
this.emitEvent({ type: 'delete', key: keyStr, timestamp: Date.now() })
|
|
650
|
+
return true
|
|
651
|
+
} catch {
|
|
652
|
+
return false
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Check if an entry exists
|
|
658
|
+
*/
|
|
659
|
+
async has(key: CacheKey | string): Promise<boolean> {
|
|
660
|
+
const result = await this.get(key)
|
|
661
|
+
return result.hit
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Clear all entries
|
|
666
|
+
*/
|
|
667
|
+
async clear(): Promise<void> {
|
|
668
|
+
if (!this.fs || !this.initialized) return
|
|
669
|
+
|
|
670
|
+
const files = await this.fs.readdir(this.cachePath)
|
|
671
|
+
|
|
672
|
+
for (const file of files) {
|
|
673
|
+
if (file.endsWith('.json')) {
|
|
674
|
+
await this.fs.unlink(this.path!.join(this.cachePath, file)).catch(() => {})
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
this.emitEvent({ type: 'clear', timestamp: Date.now() })
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get cache statistics
|
|
683
|
+
*/
|
|
684
|
+
getStats(): ProofCacheStats {
|
|
685
|
+
return {
|
|
686
|
+
...INITIAL_PROOF_CACHE_STATS,
|
|
687
|
+
totalLookups: this.totalLookups,
|
|
688
|
+
hits: this.hits,
|
|
689
|
+
misses: this.misses,
|
|
690
|
+
hitRate: this.totalLookups > 0 ? this.hits / this.totalLookups : 0,
|
|
691
|
+
maxSizeBytes: this.config.maxSizeBytes,
|
|
692
|
+
avgLookupTimeMs: this.totalLookups > 0 ? this.totalLookupTimeMs / this.totalLookups : 0,
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get all keys matching a pattern
|
|
698
|
+
*/
|
|
699
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
700
|
+
if (!this.initialized) {
|
|
701
|
+
await this.initialize()
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const files = await this.fs!.readdir(this.cachePath)
|
|
705
|
+
let keys = files
|
|
706
|
+
.filter((f) => f.endsWith('.json'))
|
|
707
|
+
.map((f) => this.fileNameToKey(f))
|
|
708
|
+
|
|
709
|
+
if (pattern) {
|
|
710
|
+
const regexPattern = pattern
|
|
711
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
712
|
+
.replace(/\*/g, '.*')
|
|
713
|
+
.replace(/\?/g, '.')
|
|
714
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
715
|
+
keys = keys.filter((key) => regex.test(key))
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return keys
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
addEventListener(listener: CacheEventListener): void {
|
|
722
|
+
this.listeners.add(listener)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
removeEventListener(listener: CacheEventListener): void {
|
|
726
|
+
this.listeners.delete(listener)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private getFilePath(key: string): string {
|
|
730
|
+
// Sanitize key for file system
|
|
731
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
732
|
+
return this.path!.join(this.cachePath, `${safeKey}.json`)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private fileNameToKey(fileName: string): string {
|
|
736
|
+
// This is a simplified reverse - in practice, you'd need a mapping
|
|
737
|
+
return fileName.replace('.json', '')
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private updateLookupTime(startTime: number): void {
|
|
741
|
+
this.totalLookupTimeMs += Date.now() - startTime
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private emitEvent(event: CacheEvent): void {
|
|
745
|
+
for (const listener of this.listeners) {
|
|
746
|
+
try {
|
|
747
|
+
listener(event)
|
|
748
|
+
} catch {
|
|
749
|
+
// Ignore listener errors
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ─── Factory Functions ───────────────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Create the appropriate persistent cache for the current environment
|
|
759
|
+
*/
|
|
760
|
+
export function createPersistentCache<T = SingleProof>(
|
|
761
|
+
config?: Partial<PersistentCacheConfig>
|
|
762
|
+
): IPersistentCache<T> {
|
|
763
|
+
if (isBrowser()) {
|
|
764
|
+
return new IndexedDBCache<T>(config)
|
|
765
|
+
}
|
|
766
|
+
if (isNode()) {
|
|
767
|
+
return new FileCache<T>(config)
|
|
768
|
+
}
|
|
769
|
+
throw new Error('No persistent storage available in this environment')
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Create an IndexedDB cache (browser only)
|
|
774
|
+
*/
|
|
775
|
+
export function createIndexedDBCache<T = SingleProof>(
|
|
776
|
+
config?: Partial<PersistentCacheConfig>
|
|
777
|
+
): IPersistentCache<T> {
|
|
778
|
+
return new IndexedDBCache<T>(config)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Create a file-based cache (Node.js only)
|
|
783
|
+
*/
|
|
784
|
+
export function createFileCache<T = SingleProof>(
|
|
785
|
+
config?: Partial<PersistentCacheConfig>
|
|
786
|
+
): IPersistentCache<T> {
|
|
787
|
+
return new FileCache<T>(config)
|
|
788
|
+
}
|