State management fixes

This commit is contained in:
minibits-cash
2026-03-11 23:27:51 +01:00
parent 5ddc3de9e0
commit d7fdba7c91
5 changed files with 82 additions and 73 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"react-native": {
"messaging_ios_auto_register_for_remote_messages": false
}
}
+11 -13
View File
@@ -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)
} }
}, },
@@ -572,13 +572,14 @@ export const MintModel = types
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()
@@ -637,10 +638,7 @@ export const MintModel = types
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)
+15 -21
View File
@@ -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)
@@ -239,14 +239,12 @@ 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,33 +268,29 @@ 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
}
for (const p of liveProofs) {
p.isPending = false p.isPending = false
p.isSpent = true 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
}
for (const p of liveProofs) {
p.isPending = false p.isPending = false
p.isSpent = false p.isSpent = false
} }
+4 -4
View File
@@ -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]
+13 -1
View File
@@ -34,9 +34,14 @@ 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()
@@ -53,6 +58,7 @@ export async function setupRootStore(rootStore: RootStore) {
} }
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'})
@@ -87,11 +93,16 @@ export async function setupRootStore(rootStore: RootStore) {
_disposer() _disposer()
} }
if (snapshotApplied) {
_disposer = onSnapshot(rootStore, snapshot => { _disposer = onSnapshot(rootStore, snapshot => {
MMKVStorage.save(ROOT_STORAGE_KEY, 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
if (snapshotApplied) {
try { try {
log.info(`RootStore loaded from MMKV, version is: ${rootStore.version}`, {caller: 'setupRootStore'}) log.info(`RootStore loaded from MMKV, version is: ${rootStore.version}`, {caller: 'setupRootStore'})
@@ -101,6 +112,7 @@ export async function setupRootStore(rootStore: 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 = () => {
_disposer() _disposer()