@lukso/transaction-decoder 1.0.1-dev.0f1bea5

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.
Files changed (110) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +486 -0
  3. package/dist/browser.cjs +6912 -0
  4. package/dist/browser.cjs.map +1 -0
  5. package/dist/browser.d.cts +6 -0
  6. package/dist/browser.d.ts +6 -0
  7. package/dist/browser.js +131 -0
  8. package/dist/browser.js.map +1 -0
  9. package/dist/cdn/transaction-decoder.global.js +296 -0
  10. package/dist/cdn/transaction-decoder.global.js.map +1 -0
  11. package/dist/chunk-GGBHTWJL.js +437 -0
  12. package/dist/chunk-GGBHTWJL.js.map +1 -0
  13. package/dist/chunk-GXZOF3QY.js +839 -0
  14. package/dist/chunk-GXZOF3QY.js.map +1 -0
  15. package/dist/chunk-LJ6ES5XF.js +776 -0
  16. package/dist/chunk-LJ6ES5XF.js.map +1 -0
  17. package/dist/chunk-XVHJWV5U.js +4925 -0
  18. package/dist/chunk-XVHJWV5U.js.map +1 -0
  19. package/dist/data.cjs +5518 -0
  20. package/dist/data.cjs.map +1 -0
  21. package/dist/data.d.cts +43 -0
  22. package/dist/data.d.ts +43 -0
  23. package/dist/data.js +55 -0
  24. package/dist/data.js.map +1 -0
  25. package/dist/index-BzXh7poJ.d.cts +524 -0
  26. package/dist/index-BzXh7poJ.d.ts +524 -0
  27. package/dist/index.cjs +6912 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +756 -0
  30. package/dist/index.d.ts +756 -0
  31. package/dist/index.js +131 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/server.cjs +5644 -0
  34. package/dist/server.cjs.map +1 -0
  35. package/dist/server.d.cts +217 -0
  36. package/dist/server.d.ts +217 -0
  37. package/dist/server.js +644 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/utils-CBAkjQh3.d.cts +108 -0
  40. package/dist/utils-xT9-km0r.d.ts +108 -0
  41. package/package.json +101 -0
  42. package/src/browser.ts +13 -0
  43. package/src/client/resolveAddresses.ts +157 -0
  44. package/src/core/addressCollector.ts +153 -0
  45. package/src/core/addressResolver.ts +135 -0
  46. package/src/core/dataModel.ts +888 -0
  47. package/src/core/instance.ts +33 -0
  48. package/src/core/integrateDecoder.ts +325 -0
  49. package/src/data.ts +70 -0
  50. package/src/decoder/GENERATOR_PROPOSAL.md +182 -0
  51. package/src/decoder/THREE_PHASE_EXAMPLE.md +108 -0
  52. package/src/decoder/aggregation.ts +218 -0
  53. package/src/decoder/browserCache.ts +237 -0
  54. package/src/decoder/cache/README.md +126 -0
  55. package/src/decoder/cache/index.ts +44 -0
  56. package/src/decoder/cache.ts +139 -0
  57. package/src/decoder/constants.ts +125 -0
  58. package/src/decoder/decodeTransaction.ts +292 -0
  59. package/src/decoder/errors.ts +95 -0
  60. package/src/decoder/events.ts +192 -0
  61. package/src/decoder/functionSignature.ts +344 -0
  62. package/src/decoder/getDataFromExternalSources.ts +248 -0
  63. package/src/decoder/graphqlWS.ts +22 -0
  64. package/src/decoder/interfaces.ts +185 -0
  65. package/src/decoder/keyValue.ts +5 -0
  66. package/src/decoder/kvCache.ts +241 -0
  67. package/src/decoder/lruCache.ts +184 -0
  68. package/src/decoder/lsp7Mint.test.ts +179 -0
  69. package/src/decoder/lsp7TransferBatch.test.ts +105 -0
  70. package/src/decoder/plugins/RegistryAbi.ts +562 -0
  71. package/src/decoder/plugins/enhanceBurntPix.ts +132 -0
  72. package/src/decoder/plugins/enhanceGraffiti.ts +70 -0
  73. package/src/decoder/plugins/enhanceLSP0ERC725Account.ts +179 -0
  74. package/src/decoder/plugins/enhanceLSP26FollowerSystem.ts +88 -0
  75. package/src/decoder/plugins/enhanceLSP6KeyManager.ts +231 -0
  76. package/src/decoder/plugins/enhanceLSP7DigitalAsset.ts +165 -0
  77. package/src/decoder/plugins/enhanceLSP8IdentifiableDigitalAsset.ts +170 -0
  78. package/src/decoder/plugins/enhanceLSP9Vault.ts +57 -0
  79. package/src/decoder/plugins/enhanceRetrieveAbi.ts +85 -0
  80. package/src/decoder/plugins/enhanceSetData.ts +135 -0
  81. package/src/decoder/plugins/index.ts +99 -0
  82. package/src/decoder/plugins/schemaDefault.ts +318 -0
  83. package/src/decoder/plugins/standardPlugin.ts +202 -0
  84. package/src/decoder/registry.ts +322 -0
  85. package/src/decoder/singleGQL.ts +293 -0
  86. package/src/decoder/transaction.ts +198 -0
  87. package/src/decoder/types.ts +465 -0
  88. package/src/decoder/utils.ts +212 -0
  89. package/src/example/usage.ts +172 -0
  90. package/src/index.ts +174 -0
  91. package/src/server/addressResolver.ts +68 -0
  92. package/src/server/caches.ts +209 -0
  93. package/src/server/decodeTransactionSync.ts +156 -0
  94. package/src/server/decodeTransactionsBatch.ts +207 -0
  95. package/src/server/finishDecoding.ts +116 -0
  96. package/src/server/index.ts +81 -0
  97. package/src/server/lsp23Resolver.test.ts +46 -0
  98. package/src/server/lsp23Resolver.ts +419 -0
  99. package/src/server/types.ts +168 -0
  100. package/src/server.ts +22 -0
  101. package/src/shared/addressResolver.ts +651 -0
  102. package/src/shared/cache.ts +144 -0
  103. package/src/shared/constants.ts +21 -0
  104. package/src/stubs/tty.ts +13 -0
  105. package/src/stubs/util.ts +42 -0
  106. package/src/types/index.ts +154 -0
  107. package/src/types/provider.ts +46 -0
  108. package/src/umd.ts +13 -0
  109. package/src/utils/debug.ts +49 -0
  110. package/src/utils/json-bigint.ts +47 -0
@@ -0,0 +1,108 @@
1
+ # Three-Phase Transaction Decoding
2
+
3
+ The decoder now supports a three-phase approach for optimal performance and user experience:
4
+
5
+ ## Phase 1: Sync Decoding (Immediate)
6
+ - Uses known ABIs to decode transactions instantly
7
+ - Returns basic transaction information
8
+ - Status: `decoded`
9
+
10
+ ## Phase 2: ABI Retrieval (Async Plugins)
11
+ - Fetches unknown ABIs from external sources
12
+ - Enhances transaction with additional context
13
+ - Status: `enhanced`
14
+
15
+ ## Phase 3: Address Resolution (Batch)
16
+ - Resolves address metadata (profiles, assets, etc.)
17
+ - Batches requests across multiple transactions for efficiency
18
+ - Status: `complete`
19
+
20
+ ## Usage Examples
21
+
22
+ ### Single Transaction
23
+ ```typescript
24
+ import { decodeTransaction } from '@lukso/decoder'
25
+
26
+ const result = await decodeTransaction(transaction, {
27
+ plugins: defaultPlugins,
28
+ schemaPlugins: defaultSchemaPlugins,
29
+ chain: lukso,
30
+ addressResolver: myAddressResolver, // Optional
31
+ })
32
+
33
+ // Immediate access to sync-decoded data
34
+ console.log(result.immediate)
35
+
36
+ // Subscribe to progressive updates
37
+ result.signal.subscribe((state) => {
38
+ switch (state.decodingStatus) {
39
+ case 'decoded':
40
+ // Phase 1 complete - basic decoding done
41
+ updateUI(state.data)
42
+ break
43
+ case 'enhanced':
44
+ // Phase 2 complete - ABI retrieval done
45
+ updateUIWithEnhancedData(state.data)
46
+ break
47
+ case 'complete':
48
+ // Phase 3 complete - addresses resolved
49
+ updateUIWithFullData(state.data)
50
+ break
51
+ }
52
+ })
53
+ ```
54
+
55
+ ### Batch Transactions (Optimal for Address Resolution)
56
+ ```typescript
57
+ const transactions = [tx1, tx2, tx3, tx4, tx5]
58
+
59
+ const results = await decodeTransaction(transactions, {
60
+ plugins: defaultPlugins,
61
+ schemaPlugins: defaultSchemaPlugins,
62
+ chain: lukso,
63
+ addressResolver: myAddressResolver,
64
+ })
65
+
66
+ // All addresses from all transactions are batched together
67
+ // Much more efficient than decoding one by one
68
+
69
+ results.forEach((result, index) => {
70
+ console.log(`Transaction ${index}:`, result.immediate)
71
+
72
+ result.signal.subscribe((state) => {
73
+ updateTransactionUI(index, state)
74
+ })
75
+ })
76
+ ```
77
+
78
+ ### Address Resolver Interface
79
+ ```typescript
80
+ class MyAddressResolver implements AddressResolver {
81
+ async resolveAddresses(addresses: DataKey[]): Promise<void> {
82
+ // Batch fetch address metadata
83
+ const chunks = chunk(addresses, 20) // GraphQL batch size
84
+
85
+ for (const batch of chunks) {
86
+ const results = await fetchAddressMetadata(batch)
87
+ // Update your cache/store with results
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Benefits
94
+
95
+ 1. **Immediate Feedback**: Users see decoded transaction info instantly
96
+ 2. **Progressive Enhancement**: UI updates as more data becomes available
97
+ 3. **Efficient Batching**: Address resolution is optimized across transactions
98
+ 4. **No Blocking**: Address resolution doesn't block ABI retrieval results
99
+
100
+ ## Status Flow
101
+
102
+ ```
103
+ raw → decoded → enhanced → complete
104
+ ↓ ↓ ↓
105
+ (sync) (async) (addresses)
106
+ ```
107
+
108
+ Each phase provides value without waiting for the next, ensuring the best possible user experience.
@@ -0,0 +1,218 @@
1
+ import createDebug from '../utils/debug'
2
+ import type { Aggregation, DecoderResult } from './types'
3
+
4
+ /**
5
+ * Helper function to create a typed aggregation configuration
6
+ * @param config The aggregation configuration
7
+ * @returns A frozen aggregation object
8
+ */
9
+ export function standardAggregation<T>(config: {
10
+ key: string
11
+ map: (tx: DecoderResult) => T | undefined
12
+ reduce: (state: T | undefined, mapped: T) => T
13
+ finalize: (state: T) => Omit<import('./types').ResultAggregate, 'resultType'>
14
+ }): Aggregation<T> {
15
+ return Object.freeze(config)
16
+ }
17
+
18
+ /**
19
+ * Aggregation engine for processing transactions with map/reduce pattern
20
+ */
21
+ export interface AggregationState<T = unknown> {
22
+ data: T
23
+ lastTransaction: DecoderResult
24
+ }
25
+
26
+ const debug = createDebug('decoder:aggregation')
27
+
28
+ export class PluginAggregationEngine {
29
+ private plugins = new Map<string, import('./types').DecoderPlugin>()
30
+ private states = new Map<string, Map<string, AggregationState>>() // plugin -> key -> state
31
+
32
+ /**
33
+ * Register a plugin with aggregations
34
+ */
35
+ registerPlugin(plugin: import('./types').DecoderPlugin) {
36
+ if (plugin.name && plugin.aggregations && plugin.aggregations.length > 0) {
37
+ debug(
38
+ `Registering plugin ${plugin.name} with ${plugin.aggregations.length} aggregations`
39
+ )
40
+ this.plugins.set(plugin.name, plugin)
41
+ this.states.set(plugin.name, new Map())
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Process a transaction through relevant aggregations
47
+ */
48
+ processTransaction(tx: DecoderResult) {
49
+ // Skip if no aggregations needed
50
+ if (!tx.aggregationKeys || tx.aggregationKeys.length === 0) return
51
+
52
+ // Process aggregations based on the full aggregation keys
53
+ for (const fullKey of tx.aggregationKeys) {
54
+ // Split the full key to get plugin name and aggregation key
55
+ const [pluginName, ...keyParts] = fullKey.split(':')
56
+ const aggregationKey = keyParts.join(':') // Handle keys with colons
57
+
58
+ const plugin = this.plugins.get(pluginName)
59
+ if (!plugin || !plugin.aggregations) {
60
+ debug(`processTransaction: No plugin found for ${pluginName}`)
61
+ continue
62
+ }
63
+
64
+ const pluginStates = this.states.get(pluginName)
65
+ if (!pluginStates) {
66
+ debug(`processTransaction: No states for plugin ${pluginName}`)
67
+ continue
68
+ }
69
+
70
+ const agg = plugin.aggregations.find((a) => a.key === aggregationKey)
71
+ if (!agg) {
72
+ debug(
73
+ `processTransaction: No aggregation found for key ${aggregationKey} in plugin ${pluginName}`
74
+ )
75
+ continue
76
+ }
77
+
78
+ const mapped = agg.map(tx)
79
+ if (mapped !== undefined) {
80
+ const currentState = pluginStates.get(aggregationKey)
81
+ if (!currentState) {
82
+ // First transaction for this aggregation
83
+ debug(
84
+ `processTransaction: First transaction for ${pluginName}:${aggregationKey}`
85
+ )
86
+ pluginStates.set(aggregationKey, {
87
+ data: mapped,
88
+ lastTransaction: tx,
89
+ })
90
+ } else {
91
+ // Update existing state
92
+ const newData = agg.reduce(currentState.data, mapped)
93
+ pluginStates.set(aggregationKey, {
94
+ data: newData,
95
+ lastTransaction: tx, // Always update to latest transaction
96
+ })
97
+ }
98
+ } else {
99
+ debug(
100
+ `processTransaction: map returned undefined for ${pluginName}:${aggregationKey}`
101
+ )
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Process a batch of transactions
108
+ */
109
+ processBatch(txs: DecoderResult[]) {
110
+ debug(`Processing batch of ${txs.length} transactions`)
111
+ let processedCount = 0
112
+ for (const tx of txs) {
113
+ this.processTransaction(tx)
114
+ if (tx.aggregationKeys && tx.aggregationKeys.length > 0) {
115
+ processedCount++
116
+ }
117
+ }
118
+ debug(`Processed ${processedCount} transactions with aggregationKeys`)
119
+ }
120
+
121
+ /**
122
+ * Get finalized result for a specific plugin and aggregation key
123
+ */
124
+ getResult(pluginName: string, key: string): DecoderResult | undefined {
125
+ const plugin = this.plugins.get(pluginName)
126
+ const pluginStates = this.states.get(pluginName)
127
+
128
+ if (!plugin || !pluginStates) return undefined
129
+
130
+ const agg = plugin.aggregations?.find((a) => a.key === key)
131
+ const state = pluginStates.get(key)
132
+
133
+ if (!agg || state === undefined) return undefined
134
+
135
+ // Merge aggregation result with last transaction metadata
136
+ const aggregationResult = agg.finalize(state.data)
137
+
138
+ return {
139
+ ...state.lastTransaction,
140
+ ...aggregationResult,
141
+ resultType: 'aggregate' as const,
142
+ } as DecoderResult
143
+ }
144
+
145
+ /**
146
+ * Get all aggregation results
147
+ */
148
+ getAllResults(): Array<{
149
+ plugin: string
150
+ key: string
151
+ result: DecoderResult
152
+ }> {
153
+ const results: Array<{
154
+ plugin: string
155
+ key: string
156
+ result: DecoderResult
157
+ }> = []
158
+
159
+ for (const [pluginName, plugin] of this.plugins) {
160
+ const pluginStates = this.states.get(pluginName)
161
+
162
+ // Skip if this plugin has no states (hasn't processed any transactions yet)
163
+ if (!pluginStates) {
164
+ continue
165
+ }
166
+
167
+ for (const agg of plugin.aggregations || []) {
168
+ const state = pluginStates.get(agg.key)
169
+ if (state !== undefined) {
170
+ results.push({
171
+ plugin: pluginName,
172
+ key: agg.key,
173
+ result: {
174
+ ...state.lastTransaction,
175
+ ...agg.finalize(state.data),
176
+ resultType: 'aggregate' as const,
177
+ } as DecoderResult,
178
+ })
179
+ }
180
+ }
181
+ }
182
+
183
+ return results
184
+ }
185
+
186
+ /**
187
+ * Clear all aggregation states
188
+ */
189
+ clear() {
190
+ for (const states of this.states.values()) {
191
+ states.clear()
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Clear states for a specific plugin
197
+ */
198
+ clearPlugin(pluginName: string) {
199
+ this.states.get(pluginName)?.clear()
200
+ }
201
+
202
+ /**
203
+ * Get current state (for persistence)
204
+ */
205
+ getStates(): Map<string, Map<string, AggregationState>> {
206
+ return new Map(this.states)
207
+ }
208
+
209
+ /**
210
+ * Restore states (from persistence)
211
+ */
212
+ restoreStates(states: Map<string, Map<string, AggregationState>>) {
213
+ // Merge states instead of replacing to preserve plugin initialization
214
+ for (const [pluginName, pluginStates] of states) {
215
+ this.states.set(pluginName, new Map(pluginStates))
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Browser Cache API implementation of DecoderCache interface
3
+ * Uses the standard Cache API available in browsers and service workers
4
+ */
5
+
6
+ import type { CacheEntry, CacheOptions, DecoderCache } from './cache'
7
+
8
+ /**
9
+ * Options for creating a browser cache instance
10
+ */
11
+ export interface BrowserCacheOptions {
12
+ /** Name of the cache to open */
13
+ cacheName?: string
14
+ /** Default TTL in milliseconds */
15
+ ttl?: number
16
+ /** Default error TTL in milliseconds */
17
+ errorTTL?: number
18
+ /** Default stale-while-revalidate time in milliseconds */
19
+ staleWhileRevalidate?: number
20
+ }
21
+
22
+ /**
23
+ * Browser Cache API implementation of DecoderCache
24
+ */
25
+ export class BrowserDecoderCache implements DecoderCache {
26
+ private cacheName: string
27
+ private cachePromise: Promise<Cache>
28
+ private promises: Map<string, Promise<unknown>>
29
+ private defaultTTL: number
30
+ private defaultErrorTTL: number
31
+ private defaultStaleWhileRevalidate?: number
32
+
33
+ constructor(options: BrowserCacheOptions = {}) {
34
+ this.cacheName = options.cacheName ?? 'decoder-cache-v1'
35
+ this.defaultTTL = options.ttl ?? 5 * 60 * 1000 // 5 minutes
36
+ this.defaultErrorTTL = options.errorTTL ?? 30 * 1000 // 30 seconds
37
+ this.defaultStaleWhileRevalidate = options.staleWhileRevalidate
38
+ this.promises = new Map()
39
+
40
+ // Check if Cache API is available
41
+ if (typeof caches === 'undefined') {
42
+ throw new Error('Cache API not available in this environment')
43
+ }
44
+
45
+ this.cachePromise = caches.open(this.cacheName)
46
+ }
47
+
48
+ private getUrl(key: string): string {
49
+ // Create a fake URL for cache storage
50
+ return `https://decoder.cache/${encodeURIComponent(key)}`
51
+ }
52
+
53
+ async get<T>(key: string): Promise<CacheEntry<T> | undefined> {
54
+ try {
55
+ const cache = await this.cachePromise
56
+ const response = await cache.match(this.getUrl(key))
57
+
58
+ if (!response) {
59
+ return undefined
60
+ }
61
+
62
+ const entry = (await response.json()) as CacheEntry<T>
63
+
64
+ // Check if expired
65
+ if (entry.expires && Date.now() > entry.expires) {
66
+ // Don't delete here as it might be used for stale-while-revalidate
67
+ return undefined
68
+ }
69
+
70
+ return entry
71
+ } catch (error) {
72
+ console.error('Browser cache get error:', error)
73
+ return undefined
74
+ }
75
+ }
76
+
77
+ async set<T>(key: string, value: T, options?: CacheOptions): Promise<void> {
78
+ const now = Date.now()
79
+ const ttl =
80
+ options?.ttl ?? (options?.errorTTL ? options.errorTTL : this.defaultTTL)
81
+ const isError = options?.errorTTL !== undefined
82
+
83
+ const entry: CacheEntry<T> = {
84
+ value,
85
+ expires: now + ttl,
86
+ isError,
87
+ }
88
+
89
+ const staleWhileRevalidate =
90
+ options?.staleWhileRevalidate ?? this.defaultStaleWhileRevalidate
91
+ if (staleWhileRevalidate) {
92
+ entry.stale = now + ttl + staleWhileRevalidate
93
+ }
94
+
95
+ try {
96
+ const cache = await this.cachePromise
97
+ const response = new Response(JSON.stringify(entry), {
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ // Set cache control headers for browser caching
101
+ 'Cache-Control': `private, max-age=${Math.ceil(ttl / 1000)}`,
102
+ },
103
+ })
104
+
105
+ await cache.put(this.getUrl(key), response)
106
+ } catch (error) {
107
+ console.error('Browser cache set error:', error)
108
+ throw error
109
+ }
110
+ }
111
+
112
+ async delete(key: string): Promise<void> {
113
+ try {
114
+ const cache = await this.cachePromise
115
+ await cache.delete(this.getUrl(key))
116
+ } catch (error) {
117
+ console.error('Browser cache delete error:', error)
118
+ }
119
+ }
120
+
121
+ async getMany<T>(keys: string[]): Promise<Map<string, CacheEntry<T>>> {
122
+ const result = new Map<string, CacheEntry<T>>()
123
+
124
+ // Cache API doesn't support batch operations, so we do it in parallel
125
+ const promises = keys.map(async (key) => {
126
+ const entry = await this.get<T>(key)
127
+ if (entry) {
128
+ result.set(key, entry)
129
+ }
130
+ })
131
+
132
+ await Promise.all(promises)
133
+ return result
134
+ }
135
+
136
+ async setMany<T>(
137
+ entries: Array<{ key: string; value: T; options?: CacheOptions }>
138
+ ): Promise<void> {
139
+ // Cache API doesn't support batch operations, so we do it in parallel
140
+ const promises = entries.map(({ key, value, options }) =>
141
+ this.set(key, value, options)
142
+ )
143
+
144
+ await Promise.all(promises)
145
+ }
146
+
147
+ async clear(): Promise<void> {
148
+ // Clear in-flight promises
149
+ this.promises.clear()
150
+
151
+ try {
152
+ // Delete the entire cache
153
+ await caches.delete(this.cacheName)
154
+ // Re-open it
155
+ this.cachePromise = caches.open(this.cacheName)
156
+ } catch (error) {
157
+ console.error('Browser cache clear error:', error)
158
+ }
159
+ }
160
+
161
+ async getOrSet<T>(
162
+ key: string,
163
+ factory: (signal?: AbortSignal) => Promise<T>,
164
+ options?: CacheOptions & { signal?: AbortSignal }
165
+ ): Promise<T> {
166
+ // Check cache first
167
+ const cached = await this.get<T>(key)
168
+ const now = Date.now()
169
+
170
+ // If we have a valid (non-expired) entry, return it
171
+ if (cached && (!cached.expires || now < cached.expires)) {
172
+ return cached.value
173
+ }
174
+
175
+ // If we have a stale entry and stale-while-revalidate is enabled
176
+ if (
177
+ cached &&
178
+ cached.stale &&
179
+ now < cached.stale &&
180
+ !this.promises.has(key)
181
+ ) {
182
+ // Return stale value and revalidate in background
183
+ const backgroundPromise = factory(options?.signal)
184
+ .then(async (fresh) => {
185
+ await this.set(key, fresh, options)
186
+ return fresh
187
+ })
188
+ .catch(() => {
189
+ // On error, keep the stale value
190
+ return cached.value
191
+ })
192
+ .finally(() => {
193
+ this.promises.delete(key)
194
+ })
195
+
196
+ this.promises.set(key, backgroundPromise)
197
+ return cached.value
198
+ }
199
+
200
+ // Check if we have an in-flight promise
201
+ const existingPromise = this.promises.get(key) as Promise<T> | undefined
202
+ if (existingPromise) {
203
+ return existingPromise
204
+ }
205
+
206
+ // No valid cache entry, fetch new value
207
+ const promise = factory(options?.signal)
208
+ .then(async (value) => {
209
+ await this.set(key, value, options)
210
+ return value
211
+ })
212
+ .catch(async (error) => {
213
+ // Cache the error with shorter TTL
214
+ await this.set(key, error as T, {
215
+ ...options,
216
+ ttl: options?.errorTTL ?? this.defaultErrorTTL,
217
+ errorTTL: options?.errorTTL ?? this.defaultErrorTTL,
218
+ })
219
+ throw error
220
+ })
221
+ .finally(() => {
222
+ this.promises.delete(key)
223
+ })
224
+
225
+ this.promises.set(key, promise)
226
+ return promise as Promise<T>
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Create a browser cache instance
232
+ */
233
+ export function createBrowserCache(
234
+ options?: BrowserCacheOptions
235
+ ): DecoderCache {
236
+ return new BrowserDecoderCache(options)
237
+ }
@@ -0,0 +1,126 @@
1
+ # Decoder Cache Implementations
2
+
3
+ This directory contains three cache implementations for the decoder, all implementing the `DecoderCache` interface:
4
+
5
+ ## 1. LRU Cache (Memory-based)
6
+
7
+ Best for Node.js environments and development.
8
+
9
+ ```typescript
10
+ import { createLRUCache } from '@lukso/transaction-decoder'
11
+
12
+ const cache = createLRUCache({
13
+ max: 1000, // Maximum items in cache
14
+ ttl: 5 * 60 * 1000, // 5 minutes TTL
15
+ errorTTL: 30 * 1000, // 30 seconds for errors
16
+ staleWhileRevalidate: 60 * 1000 // 1 minute stale time
17
+ })
18
+
19
+ // Use with decoder
20
+ const decoded = await decodeTransaction(tx, {
21
+ async: AsyncOperations.ENABLE_PLUGINS,
22
+ cache
23
+ })
24
+ ```
25
+
26
+ ## 2. KV Cache (Key-Value Store)
27
+
28
+ Perfect for Cloudflare Workers or other KV-based environments.
29
+
30
+ ```typescript
31
+ import { createKVCache } from '@lukso/transaction-decoder'
32
+
33
+ // Cloudflare Workers example
34
+ const cache = createKVCache({
35
+ kv: env.MY_KV_NAMESPACE, // Your KV namespace binding
36
+ prefix: 'decoder:', // Key prefix
37
+ ttl: 5 * 60 * 1000,
38
+ errorTTL: 30 * 1000
39
+ })
40
+
41
+ // Deno KV example
42
+ const kv = await Deno.openKv()
43
+ const cache = createKVCache({
44
+ kv: {
45
+ get: (key) => kv.get([key]).then(r => r.value),
46
+ put: (key, value, opts) => kv.set([key], value, {
47
+ expireIn: opts?.expirationTtl ? opts.expirationTtl * 1000 : undefined
48
+ }),
49
+ delete: (key) => kv.delete([key])
50
+ }
51
+ })
52
+ ```
53
+
54
+ ## 3. Browser Cache (Cache API)
55
+
56
+ Ideal for Progressive Web Apps and Service Workers.
57
+
58
+ ```typescript
59
+ import { createBrowserCache } from '@lukso/transaction-decoder'
60
+
61
+ const cache = createBrowserCache({
62
+ cacheName: 'decoder-v1', // Cache name
63
+ ttl: 5 * 60 * 1000,
64
+ errorTTL: 30 * 1000,
65
+ staleWhileRevalidate: 60 * 1000
66
+ })
67
+
68
+ // Works in service workers too!
69
+ self.addEventListener('fetch', (event) => {
70
+ // Use cache for decoder operations
71
+ })
72
+ ```
73
+
74
+ ## Features
75
+
76
+ All cache implementations support:
77
+
78
+ - **Promise deduplication**: Prevents duplicate requests for the same resource
79
+ - **Stale-while-revalidate**: Serves stale content while fetching fresh data
80
+ - **Error caching**: Caches errors with shorter TTL to allow retries
81
+ - **AbortSignal support**: Cancellable operations for SSR timeouts
82
+ - **Batch operations**: `getMany` and `setMany` for efficiency
83
+
84
+ ## SSR Example with Timeout
85
+
86
+ ```typescript
87
+ import { createLRUCache, AsyncOperations } from '@lukso/transaction-decoder'
88
+
89
+ // Create cache with SSR-friendly settings
90
+ const cache = createLRUCache({
91
+ max: 500,
92
+ ttl: 60 * 1000, // 1 minute
93
+ errorTTL: 10 * 1000, // 10 seconds for errors
94
+ staleWhileRevalidate: 5 * 60 * 1000 // 5 minutes
95
+ })
96
+
97
+ // Decode with timeout
98
+ const controller = new AbortController()
99
+ const timeout = setTimeout(() => controller.abort(), 100) // 100ms timeout
100
+
101
+ try {
102
+ const decoded = await decodeTransaction(tx, {
103
+ async: AsyncOperations.ENABLE_PLUGINS, // Only ABI fetching
104
+ cache,
105
+ signal: controller.signal
106
+ })
107
+ clearTimeout(timeout)
108
+
109
+ // Continue with address resolution on client if needed
110
+ } catch (error) {
111
+ if (error.name === 'AbortError') {
112
+ // Timeout reached, return partial result
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## Auto Cache Selection
118
+
119
+ Use `createAutoCache()` to automatically select the best cache for your environment:
120
+
121
+ ```typescript
122
+ import { createAutoCache } from '@lukso/transaction-decoder'
123
+
124
+ const cache = createAutoCache()
125
+ // Returns BrowserCache in browser, LRUCache in Node.js
126
+ ```