mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
State management fixes
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"react-native": {
|
||||||
|
"messaging_ios_auto_register_for_remote_messages": false
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-16
@@ -125,7 +125,7 @@ export const MintProofsCounterModel = types
|
|||||||
|
|
||||||
removeInFlightRequest(transactionId: number) {
|
removeInFlightRequest(transactionId: number) {
|
||||||
if (!isAlive(self)) {
|
if (!isAlive(self)) {
|
||||||
log.error('[removeInFlightRequest]', 'ProofsCounter is not alive', { keyset: self.keyset })
|
log.error('[removeInFlightRequest]', 'ProofsCounter is not alive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const key = transactionId.toString()
|
const key = transactionId.toString()
|
||||||
@@ -137,7 +137,7 @@ export const MintProofsCounterModel = types
|
|||||||
|
|
||||||
clearAllInFlightRequests() {
|
clearAllInFlightRequests() {
|
||||||
if (!isAlive(self)) {
|
if (!isAlive(self)) {
|
||||||
log.error('[clearAllInFlightRequests]', 'ProofsCounter is not alive', { keyset: self.keyset })
|
log.error('[clearAllInFlightRequests]', 'ProofsCounter is not alive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const count = self.inFlightRequests.size
|
const count = self.inFlightRequests.size
|
||||||
@@ -176,7 +176,7 @@ export const MintProofsCounterModel = types
|
|||||||
|
|
||||||
removeMeltCounterValue(transactionId: number) {
|
removeMeltCounterValue(transactionId: number) {
|
||||||
if (!isAlive(self)) {
|
if (!isAlive(self)) {
|
||||||
log.error('[removeMeltCounterValue]', 'ProofsCounter is not alive', { keyset: self.keyset })
|
log.error('[removeMeltCounterValue]', 'ProofsCounter is not alive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ export const MintProofsCounterModel = types
|
|||||||
|
|
||||||
clearAllMeltCounterValues() {
|
clearAllMeltCounterValues() {
|
||||||
if (!isAlive(self)) {
|
if (!isAlive(self)) {
|
||||||
log.error('[clearAllMeltCounterValues]', 'ProofsCounter is not alive', { keyset: self.keyset })
|
log.error('[clearAllMeltCounterValues]', 'ProofsCounter is not alive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ export const MintModel = types
|
|||||||
const index = self.proofsCounters.findIndex(p => p.keyset === counter.keyset)
|
const index = self.proofsCounters.findIndex(p => p.keyset === counter.keyset)
|
||||||
|
|
||||||
if(index !== -1) {
|
if(index !== -1) {
|
||||||
self.proofsCounters.splice(index, 0)
|
self.proofsCounters.splice(index, 1)
|
||||||
self.proofsCounters = cast(self.proofsCounters)
|
self.proofsCounters = cast(self.proofsCounters)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -567,18 +567,19 @@ export const MintModel = types
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const cashuMint = new CashuMint(self.mintUrl)
|
const cashuMint = new CashuMint(self.mintUrl)
|
||||||
const info: GetInfoResponse = yield cashuMint.getInfo()
|
const info: GetInfoResponse = yield cashuMint.getInfo()
|
||||||
|
|
||||||
if(info.name.length > 0) {
|
if(info.name.length > 0) {
|
||||||
shortname = info.name
|
shortname = info.name
|
||||||
}
|
}
|
||||||
|
|
||||||
self.shortname = shortname
|
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log.warn('[setShortname]', {error: e.message})
|
log.warn('[setShortname]', {error: e.message})
|
||||||
self.shortname = shortname
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mint may have been removed while the network call was in flight
|
||||||
|
if (!isAlive(self)) return
|
||||||
|
|
||||||
|
self.shortname = shortname
|
||||||
}),
|
}),
|
||||||
setRandomColor() {
|
setRandomColor() {
|
||||||
self.color = getRandomIconColor()
|
self.color = getRandomIconColor()
|
||||||
@@ -636,11 +637,8 @@ export const MintModel = types
|
|||||||
const counters = self.proofsCounters.filter(c => c.inFlightRequests && c.inFlightRequests.size > 0)
|
const counters = self.proofsCounters.filter(c => c.inFlightRequests && c.inFlightRequests.size > 0)
|
||||||
return counters || []
|
return counters || []
|
||||||
},
|
},
|
||||||
get allInFlightRequests() {
|
get allInFlightRequests() {
|
||||||
const requests = self.proofsCounters // Get all counters as an array
|
return self.proofsCounters.flatMap((counter) => counter.allInFlightRequests)
|
||||||
.flatMap((counter) => counter.inFlightRequests) // Combine all `inFlightRequests` arrays
|
|
||||||
|
|
||||||
return requests
|
|
||||||
},
|
},
|
||||||
get balances(): MintBalance | undefined {
|
get balances(): MintBalance | undefined {
|
||||||
const mintBalance: MintBalance | undefined = getRootStore(self).proofsStore.getMintBalance(self.mintUrl)
|
const mintBalance: MintBalance | undefined = getRootStore(self).proofsStore.getMintBalance(self.mintUrl)
|
||||||
|
|||||||
+22
-28
@@ -220,8 +220,8 @@ import {
|
|||||||
proofsByKeyset.set(proof.id, (proofsByKeyset.get(proof.id) || []).concat(proofNode))
|
proofsByKeyset.set(proof.id, (proofsByKeyset.get(proof.id) || []).concat(proofNode))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update counters only for non-pending proofs
|
// Update counters only for newly received, spendable proofs
|
||||||
if (!isPending) {
|
if (!isPending && !isSpent) {
|
||||||
for (const [keysetId, proofs] of proofsByKeyset) {
|
for (const [keysetId, proofs] of proofsByKeyset) {
|
||||||
const counter = mintInstance.getProofsCounterByKeysetId(keysetId)
|
const counter = mintInstance.getProofsCounterByKeysetId(keysetId)
|
||||||
counter?.increaseProofsCounter(proofs.length)
|
counter?.increaseProofsCounter(proofs.length)
|
||||||
@@ -238,17 +238,15 @@ import {
|
|||||||
|
|
||||||
// Only call this when proofs are locally pending (ecash send, melt prepare, etc.)
|
// Only call this when proofs are locally pending (ecash send, melt prepare, etc.)
|
||||||
// Does NOT touch pendingByMintSecrets
|
// Does NOT touch pendingByMintSecrets
|
||||||
moveToPending(proofs: Proof[]) {
|
moveToPending(proofs: Proof[]) {
|
||||||
Database.addOrUpdateProofs(proofs, true, false)
|
const liveProofs = proofs.filter(p => isAlive(p))
|
||||||
|
if (liveProofs.length === 0) return
|
||||||
|
|
||||||
for (const p of proofs) {
|
Database.addOrUpdateProofs(liveProofs, true, false)
|
||||||
if (!isAlive(p)) {
|
|
||||||
log.error('[moveToPending]', 'Proof instance is not alive, aborting state update', { secret: p.secret })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (const p of liveProofs) {
|
||||||
p.isPending = true
|
p.isPending = true
|
||||||
p.isSpent = false
|
p.isSpent = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -270,35 +268,31 @@ import {
|
|||||||
},
|
},
|
||||||
|
|
||||||
moveToSpent(proofs: Proof[]) {
|
moveToSpent(proofs: Proof[]) {
|
||||||
Database.addOrUpdateProofs(proofs, false, true)
|
const liveProofs = proofs.filter(p => isAlive(p))
|
||||||
|
if (liveProofs.length === 0) return
|
||||||
|
|
||||||
for (const p of proofs) {
|
Database.addOrUpdateProofs(liveProofs, false, true)
|
||||||
if (!isAlive(p)) {
|
|
||||||
log.error('[moveToSpent]', 'Proof instance is not alive, aborting state update', { secret: p.secret })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p.isPending = false
|
for (const p of liveProofs) {
|
||||||
p.isSpent = true
|
p.isPending = false
|
||||||
|
p.isSpent = true
|
||||||
}
|
}
|
||||||
// Automatically clean if any were in mint-pending list
|
// Automatically clean if any were in mint-pending list
|
||||||
const secrets = new Set(proofs.map(p => p.secret))
|
const secrets = new Set(liveProofs.map(p => p.secret))
|
||||||
self.pendingByMintSecrets.replace(
|
self.pendingByMintSecrets.replace(
|
||||||
self.pendingByMintSecrets.filter(s => !secrets.has(s))
|
self.pendingByMintSecrets.filter(s => !secrets.has(s))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
revertToSpendable(proofs: Proof[]) {
|
revertToSpendable(proofs: Proof[]) {
|
||||||
Database.addOrUpdateProofs(proofs, false, false)
|
const liveProofs = proofs.filter(p => isAlive(p))
|
||||||
|
if (liveProofs.length === 0) return
|
||||||
|
|
||||||
for (const p of proofs) {
|
Database.addOrUpdateProofs(liveProofs, false, false)
|
||||||
if (!isAlive(p)) {
|
|
||||||
log.error('[revertToSpendable]', 'Proof instance is not alive, aborting state update', { secret: p.secret })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p.isPending = false
|
for (const p of liveProofs) {
|
||||||
p.isSpent = false
|
p.isPending = false
|
||||||
|
p.isSpent = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ export const TransactionModel = types
|
|||||||
},
|
},
|
||||||
update(fields: Partial<Transaction>) {
|
update(fields: Partial<Transaction>) {
|
||||||
// log.trace('[update]', {fields})
|
// log.trace('[update]', {fields})
|
||||||
// Update multiple fields in database with a single query
|
|
||||||
const updatedTransaction = Database.updateTransaction(self.id, fields)
|
|
||||||
|
|
||||||
if (!isAlive(self)) {
|
if (!isAlive(self)) {
|
||||||
log.error('[update]', 'Transaction instance is not alive, aborting state update', { id: self.id })
|
log.error('[update]', 'Transaction instance is not alive, aborting update')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update multiple fields in database with a single query
|
||||||
|
const updatedTransaction = Database.updateTransaction(self.id, fields)
|
||||||
|
|
||||||
// Update the model to keep store in sync
|
// Update the model to keep store in sync
|
||||||
Object.keys(updatedTransaction).forEach(key => {
|
Object.keys(updatedTransaction).forEach(key => {
|
||||||
;(self as any)[key] = (updatedTransaction as any)[key]
|
;(self as any)[key] = (updatedTransaction as any)[key]
|
||||||
|
|||||||
@@ -34,29 +34,35 @@ export const ROOT_STORAGE_KEY = 'minibits-root-storage'
|
|||||||
|
|
||||||
export async function setupRootStore(rootStore: RootStore) {
|
export async function setupRootStore(rootStore: RootStore) {
|
||||||
let restoredState: any
|
let restoredState: any
|
||||||
let _disposer: IDisposer
|
let _disposer: IDisposer | undefined
|
||||||
// let latestSnapshot: any
|
// let latestSnapshot: any
|
||||||
|
|
||||||
|
// Guards the onSnapshot installation below. If applySnapshot throws, rootStore
|
||||||
|
// stays at empty defaults. Installing the listener in that state would save those
|
||||||
|
// defaults back to MMKV on the first mutation, silently overwriting valid data.
|
||||||
|
let snapshotApplied = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// load the last known state from storage
|
// load the last known state from storage
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
|
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
|
||||||
const mmkvLoaded = performance.now()
|
const mmkvLoaded = performance.now()
|
||||||
const dataSize = Buffer.byteLength(JSON.stringify(restoredState), 'utf8')
|
const dataSize = Buffer.byteLength(JSON.stringify(restoredState), 'utf8')
|
||||||
|
|
||||||
// log.trace({restoredState})
|
// log.trace({restoredState})
|
||||||
log.trace(`Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`, {caller: 'setupRootStore'})
|
log.trace(`Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`, {caller: 'setupRootStore'})
|
||||||
|
|
||||||
// temp dirty migration of proofStore from array to map
|
// temp dirty migration of proofStore from array to map
|
||||||
if(restoredState?.proofsStore?.proofs && Array.isArray(restoredState.proofsStore.proofs)) {
|
if(restoredState?.proofsStore?.proofs && Array.isArray(restoredState.proofsStore.proofs)) {
|
||||||
restoredState.proofsStore.proofs = {}
|
restoredState.proofsStore.proofs = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
applySnapshot(rootStore, restoredState)
|
applySnapshot(rootStore, restoredState)
|
||||||
|
snapshotApplied = true
|
||||||
|
|
||||||
const stateHydrated = performance.now()
|
const stateHydrated = performance.now()
|
||||||
log.trace(`Hydrating rooStoreModel took ${stateHydrated - mmkvLoaded} ms.`, {caller: 'setupRootStore'})
|
log.trace(`Hydrating rooStoreModel took ${stateHydrated - mmkvLoaded} ms.`, {caller: 'setupRootStore'})
|
||||||
|
|
||||||
const {proofsStore, walletProfileStore, authStore, userSettingsStore, transactionsStore} = rootStore
|
const {proofsStore, walletProfileStore, authStore, userSettingsStore, transactionsStore} = rootStore
|
||||||
|
|
||||||
if(walletProfileStore.walletId) {
|
if(walletProfileStore.walletId) {
|
||||||
@@ -72,34 +78,40 @@ export async function setupRootStore(rootStore: RootStore) {
|
|||||||
await proofsStore.loadProofsFromDatabase()
|
await proofsStore.loadProofsFromDatabase()
|
||||||
// hydrate last transactions from database
|
// hydrate last transactions from database
|
||||||
await transactionsStore.loadRecentFromDatabase()
|
await transactionsStore.loadRecentFromDatabase()
|
||||||
|
|
||||||
const proofsLoaded = performance.now()
|
const proofsLoaded = performance.now()
|
||||||
log.trace(`Loading proofs and transactions from DB and hydrating took ${proofsLoaded - stateHydrated} ms.`, {
|
log.trace(`Loading proofs and transactions from DB and hydrating took ${proofsLoaded - stateHydrated} ms.`, {
|
||||||
caller: 'setupRootStore'
|
caller: 'setupRootStore'
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log.error(Err.STORAGE_ERROR, {message: e.message, params: e.params, caller: 'setupRootStore'})
|
log.error(Err.STORAGE_ERROR, {message: e.message, params: e.params, caller: 'setupRootStore'})
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop tracking state changes if we've already setup
|
// stop tracking state changes if we've already setup
|
||||||
if (_disposer) {
|
if (_disposer) {
|
||||||
_disposer()
|
_disposer()
|
||||||
}
|
}
|
||||||
|
|
||||||
_disposer = onSnapshot(rootStore, snapshot => {
|
if (snapshotApplied) {
|
||||||
MMKVStorage.save(ROOT_STORAGE_KEY, snapshot)
|
_disposer = onSnapshot(rootStore, snapshot => {
|
||||||
})
|
MMKVStorage.save(ROOT_STORAGE_KEY, snapshot)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log.error('[setupRootStore]', 'State restore failed — skipping onSnapshot to preserve MMKV data', {caller: 'setupRootStore'})
|
||||||
|
}
|
||||||
|
|
||||||
// run migrations if needed, needs to be after onSnapshot to be persisted
|
// run migrations if needed, needs to be after onSnapshot to be persisted
|
||||||
try {
|
if (snapshotApplied) {
|
||||||
log.info(`RootStore loaded from MMKV, version is: ${rootStore.version}`, {caller: 'setupRootStore'})
|
try {
|
||||||
|
log.info(`RootStore loaded from MMKV, version is: ${rootStore.version}`, {caller: 'setupRootStore'})
|
||||||
|
|
||||||
if(rootStore.version < rootStoreModelVersion) {
|
if(rootStore.version < rootStoreModelVersion) {
|
||||||
await _runMigrations(rootStore)
|
await _runMigrations(rootStore)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log.error(Err.STORAGE_ERROR, e.message)
|
log.error(Err.STORAGE_ERROR, e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = () => {
|
const unsubscribe = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user