@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.
- package/LICENSE +201 -0
- package/README.md +486 -0
- package/dist/browser.cjs +6912 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +6 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +131 -0
- package/dist/browser.js.map +1 -0
- package/dist/cdn/transaction-decoder.global.js +296 -0
- package/dist/cdn/transaction-decoder.global.js.map +1 -0
- package/dist/chunk-GGBHTWJL.js +437 -0
- package/dist/chunk-GGBHTWJL.js.map +1 -0
- package/dist/chunk-GXZOF3QY.js +839 -0
- package/dist/chunk-GXZOF3QY.js.map +1 -0
- package/dist/chunk-LJ6ES5XF.js +776 -0
- package/dist/chunk-LJ6ES5XF.js.map +1 -0
- package/dist/chunk-XVHJWV5U.js +4925 -0
- package/dist/chunk-XVHJWV5U.js.map +1 -0
- package/dist/data.cjs +5518 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.cts +43 -0
- package/dist/data.d.ts +43 -0
- package/dist/data.js +55 -0
- package/dist/data.js.map +1 -0
- package/dist/index-BzXh7poJ.d.cts +524 -0
- package/dist/index-BzXh7poJ.d.ts +524 -0
- package/dist/index.cjs +6912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +756 -0
- package/dist/index.d.ts +756 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +5644 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +217 -0
- package/dist/server.d.ts +217 -0
- package/dist/server.js +644 -0
- package/dist/server.js.map +1 -0
- package/dist/utils-CBAkjQh3.d.cts +108 -0
- package/dist/utils-xT9-km0r.d.ts +108 -0
- package/package.json +101 -0
- package/src/browser.ts +13 -0
- package/src/client/resolveAddresses.ts +157 -0
- package/src/core/addressCollector.ts +153 -0
- package/src/core/addressResolver.ts +135 -0
- package/src/core/dataModel.ts +888 -0
- package/src/core/instance.ts +33 -0
- package/src/core/integrateDecoder.ts +325 -0
- package/src/data.ts +70 -0
- package/src/decoder/GENERATOR_PROPOSAL.md +182 -0
- package/src/decoder/THREE_PHASE_EXAMPLE.md +108 -0
- package/src/decoder/aggregation.ts +218 -0
- package/src/decoder/browserCache.ts +237 -0
- package/src/decoder/cache/README.md +126 -0
- package/src/decoder/cache/index.ts +44 -0
- package/src/decoder/cache.ts +139 -0
- package/src/decoder/constants.ts +125 -0
- package/src/decoder/decodeTransaction.ts +292 -0
- package/src/decoder/errors.ts +95 -0
- package/src/decoder/events.ts +192 -0
- package/src/decoder/functionSignature.ts +344 -0
- package/src/decoder/getDataFromExternalSources.ts +248 -0
- package/src/decoder/graphqlWS.ts +22 -0
- package/src/decoder/interfaces.ts +185 -0
- package/src/decoder/keyValue.ts +5 -0
- package/src/decoder/kvCache.ts +241 -0
- package/src/decoder/lruCache.ts +184 -0
- package/src/decoder/lsp7Mint.test.ts +179 -0
- package/src/decoder/lsp7TransferBatch.test.ts +105 -0
- package/src/decoder/plugins/RegistryAbi.ts +562 -0
- package/src/decoder/plugins/enhanceBurntPix.ts +132 -0
- package/src/decoder/plugins/enhanceGraffiti.ts +70 -0
- package/src/decoder/plugins/enhanceLSP0ERC725Account.ts +179 -0
- package/src/decoder/plugins/enhanceLSP26FollowerSystem.ts +88 -0
- package/src/decoder/plugins/enhanceLSP6KeyManager.ts +231 -0
- package/src/decoder/plugins/enhanceLSP7DigitalAsset.ts +165 -0
- package/src/decoder/plugins/enhanceLSP8IdentifiableDigitalAsset.ts +170 -0
- package/src/decoder/plugins/enhanceLSP9Vault.ts +57 -0
- package/src/decoder/plugins/enhanceRetrieveAbi.ts +85 -0
- package/src/decoder/plugins/enhanceSetData.ts +135 -0
- package/src/decoder/plugins/index.ts +99 -0
- package/src/decoder/plugins/schemaDefault.ts +318 -0
- package/src/decoder/plugins/standardPlugin.ts +202 -0
- package/src/decoder/registry.ts +322 -0
- package/src/decoder/singleGQL.ts +293 -0
- package/src/decoder/transaction.ts +198 -0
- package/src/decoder/types.ts +465 -0
- package/src/decoder/utils.ts +212 -0
- package/src/example/usage.ts +172 -0
- package/src/index.ts +174 -0
- package/src/server/addressResolver.ts +68 -0
- package/src/server/caches.ts +209 -0
- package/src/server/decodeTransactionSync.ts +156 -0
- package/src/server/decodeTransactionsBatch.ts +207 -0
- package/src/server/finishDecoding.ts +116 -0
- package/src/server/index.ts +81 -0
- package/src/server/lsp23Resolver.test.ts +46 -0
- package/src/server/lsp23Resolver.ts +419 -0
- package/src/server/types.ts +168 -0
- package/src/server.ts +22 -0
- package/src/shared/addressResolver.ts +651 -0
- package/src/shared/cache.ts +144 -0
- package/src/shared/constants.ts +21 -0
- package/src/stubs/tty.ts +13 -0
- package/src/stubs/util.ts +42 -0
- package/src/types/index.ts +154 -0
- package/src/types/provider.ts +46 -0
- package/src/umd.ts +13 -0
- package/src/utils/debug.ts +49 -0
- 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
|
+
```
|