feat(market): self-heal orphan stalls on dashboard mount (closes #38)
Stopgap for the upstream LNbits orphan-stall bug
(aiolabs/lnbits#10): _create_default_merchant historically
provisioned the merchant + stall in nostrmarket's internal SQLite
without publishing the kind-30017 stall event to relays. Upstream
fix already in c0f3743c on aiolabs/lnbits@demo, but it only helps
new signups. Existing accounts whose auto-stall never made it to a
relay stay orphaned (every product they author renders as
"Unknown Stall").
New composable useMarketStallSelfHeal() runs once per browser
session for any logged-in user landing on /market/dashboard:
1. Query the relay for kind-30017 events authored by their pubkey
2. Get LNbits's known stalls for the merchant
3. For each stall not represented on the relay, PUT it back to
LNbits — the PUT path on the LNbits side already calls
sign_and_send_to_nostr, so the kind-30017 event lands on the
relay without any user interaction
Wired from MarketDashboard.vue onMounted (after the existing
fully-authed guard). Fire-and-forget, never toasts, sessionStorage
gate prevents re-runs on remounts.
Closes #38.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
628c13c644
commit
16c03d947a
2 changed files with 104 additions and 0 deletions
99
src/modules/market/composables/useMarketStallSelfHeal.ts
Normal file
99
src/modules/market/composables/useMarketStallSelfHeal.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { useAuth } from '@/composables/useAuthService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { NostrmarketAPI } from '../services/nostrmarketAPI'
|
||||
|
||||
const SESSION_FLAG = 'market-stall-self-heal-checked'
|
||||
const STALL_EVENT_KIND = 30017
|
||||
|
||||
/**
|
||||
* Detect-and-recover from the LNbits orphan-stall bug
|
||||
* (aiolabs/lnbits#10): _create_default_merchant provisions the merchant
|
||||
* + stall in nostrmarket's internal SQLite but historically never
|
||||
* published the kind-30017 stall event to relays. The upstream fix is
|
||||
* already in c0f3743c on aiolabs/lnbits@demo, but it only helps NEW
|
||||
* signups. Existing accounts whose auto-stall never made it to a relay
|
||||
* stay orphaned until somebody republishes — which manifests in our
|
||||
* webapp as "Unknown Stall" on every product authored by them.
|
||||
*
|
||||
* This composable runs once per browser session (sessionStorage gate)
|
||||
* for any logged-in user who lands on the merchant dashboard:
|
||||
*
|
||||
* 1. Ask the relay for kind-30017 events authored by their pubkey.
|
||||
* 2. Ask LNbits for the merchant's known stalls.
|
||||
* 3. For each stall in (2) whose id isn't represented in (1), PUT the
|
||||
* stall back to LNbits. The PUT path on the LNbits side already
|
||||
* calls sign_and_send_to_nostr, so the kind-30017 event lands on
|
||||
* the relay without any user interaction.
|
||||
*
|
||||
* Silent on success. Logs to console.info on republish; console.warn on
|
||||
* failure. Never toasts — this is supposed to be invisible.
|
||||
*
|
||||
* Tracked in aiolabs/webapp#38.
|
||||
*/
|
||||
export function useMarketStallSelfHeal() {
|
||||
const { user } = useAuth()
|
||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||
const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI
|
||||
|
||||
async function selfHealOnce(): Promise<void> {
|
||||
if (sessionStorage.getItem(SESSION_FLAG)) return
|
||||
|
||||
const currentUser = user.value
|
||||
if (!currentUser?.pubkey) return
|
||||
|
||||
const wallets = (currentUser as any).wallets as Array<{ adminkey?: string; inkey?: string }> | undefined
|
||||
if (!wallets?.length) return
|
||||
|
||||
const adminWallet = wallets.find(w => w.adminkey) || wallets[0]
|
||||
if (!adminWallet?.adminkey || !adminWallet?.inkey) return
|
||||
|
||||
// Mark checked early — even on failure we don't want to retry on every
|
||||
// dashboard mount during the same tab session.
|
||||
sessionStorage.setItem(SESSION_FLAG, '1')
|
||||
|
||||
if (!relayHub || !nostrmarketAPI) {
|
||||
console.warn('[market-self-heal] Required services unavailable, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const relayEvents: Array<{ tags?: Array<[string, string?]> }> = await relayHub.queryEvents([
|
||||
{ kinds: [STALL_EVENT_KIND], authors: [currentUser.pubkey] },
|
||||
])
|
||||
const publishedStallIds = new Set<string>()
|
||||
for (const ev of relayEvents) {
|
||||
const dTag = ev.tags?.find(t => Array.isArray(t) && t[0] === 'd')
|
||||
const stallId = dTag?.[1]
|
||||
if (stallId) publishedStallIds.add(stallId)
|
||||
}
|
||||
|
||||
const lnbitsStalls = await nostrmarketAPI.getStalls(adminWallet.inkey)
|
||||
const orphans = lnbitsStalls.filter(s => !publishedStallIds.has(s.id))
|
||||
|
||||
if (orphans.length === 0) {
|
||||
console.info(
|
||||
`[market-self-heal] All ${lnbitsStalls.length} stall(s) have a relay event — no recovery needed.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
console.info(
|
||||
`[market-self-heal] Republishing ${orphans.length} orphan stall(s):`,
|
||||
orphans.map(s => `${s.id} (${s.name})`),
|
||||
)
|
||||
|
||||
for (const stall of orphans) {
|
||||
try {
|
||||
await nostrmarketAPI.updateStall(adminWallet.adminkey, stall)
|
||||
console.info(`[market-self-heal] Republished ${stall.id} (${stall.name})`)
|
||||
} catch (err) {
|
||||
console.warn(`[market-self-heal] Failed to republish ${stall.id}:`, err)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[market-self-heal] Self-heal check failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return { selfHealOnce }
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ import OrderHistory from '../components/OrderHistory.vue'
|
|||
import MerchantStore from '../components/MerchantStore.vue'
|
||||
import MarketSettings from '../components/MarketSettings.vue'
|
||||
import { auth } from '@/composables/useAuthService'
|
||||
import { useMarketStallSelfHeal } from '../composables/useMarketStallSelfHeal'
|
||||
|
||||
const route = useRoute()
|
||||
const marketStore = useMarketStore()
|
||||
|
|
@ -139,6 +140,7 @@ const tabs = computed(() => [
|
|||
])
|
||||
|
||||
const router = useRouter()
|
||||
const { selfHealOnce } = useMarketStallSelfHeal()
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
|
|
@ -153,6 +155,9 @@ onMounted(() => {
|
|||
return
|
||||
}
|
||||
console.log('Market Dashboard mounted')
|
||||
// Self-heal orphan stalls (issue #38) once per browser session.
|
||||
// Fire-and-forget — never blocks the dashboard render.
|
||||
void selfHealOnce()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue