forked from coracle/caravel
Fix login/tenant create race
This commit is contained in:
@@ -6,7 +6,7 @@ import { EventStore } from "applesauce-core"
|
|||||||
import { createEventLoaderForStore } from "applesauce-loaders/loaders"
|
import { createEventLoaderForStore } from "applesauce-loaders/loaders"
|
||||||
import { RelayPool } from "applesauce-relay"
|
import { RelayPool } from "applesauce-relay"
|
||||||
import { NostrConnectSigner } from "applesauce-signers"
|
import { NostrConnectSigner } from "applesauce-signers"
|
||||||
import { getIdentity, getTenant, listPlans, listTenantInvoices, listTenantRelays, registerAccountGetter, type Plan } from "@/lib/api"
|
import { createTenant, getIdentity, getTenant, listPlans, listTenantInvoices, listTenantRelays, registerAccountGetter, type Plan } from "@/lib/api"
|
||||||
|
|
||||||
export type UnsignedEvent = {
|
export type UnsignedEvent = {
|
||||||
kind: number
|
kind: number
|
||||||
@@ -56,10 +56,15 @@ export const [identity, { refetch: refetchIdentity, mutate: setIdentity }] = cre
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Shared billing reads, fetched once per session and consumed by the dashboard
|
// Shared billing reads, fetched once per session and consumed by the dashboard
|
||||||
// shell, the billing page, and the billing-prompt surface. Keyed on the active
|
// shell, the billing page, and the billing-prompt surface. They're gated on
|
||||||
// pubkey so they refetch on account switch; refetchBilling() refreshes them all
|
// billingPubkey rather than the active account directly: the tenant row is
|
||||||
// after a mutation (payment, method update, plan change).
|
// provisioned lazily on first login, so getTenant/listTenantInvoices 404 until
|
||||||
const billingKey = () => account()?.pubkey
|
// ensureSessionTenant() has created it. billingPubkey is reset on activation and
|
||||||
|
// re-set once the tenant is ensured, so the reads still refetch on account switch
|
||||||
|
// without racing ahead of provisioning. refetchBilling() refreshes them all after
|
||||||
|
// a mutation (payment, method update, plan change).
|
||||||
|
const [billingPubkey, setBillingPubkey] = createSignal<string>()
|
||||||
|
const billingKey = () => billingPubkey()
|
||||||
|
|
||||||
export const [billingTenant, { refetch: refetchBillingTenant }] = createResource(billingKey, getTenant)
|
export const [billingTenant, { refetch: refetchBillingTenant }] = createResource(billingKey, getTenant)
|
||||||
export const [billingInvoices, { refetch: refetchBillingInvoices }] = createResource(billingKey, listTenantInvoices)
|
export const [billingInvoices, { refetch: refetchBillingInvoices }] = createResource(billingKey, listTenantInvoices)
|
||||||
@@ -71,6 +76,31 @@ export function refetchBilling() {
|
|||||||
void refetchBillingRelays()
|
void refetchBillingRelays()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the active pubkey's tenant row exists, then unlock billing reads. The
|
||||||
|
// tenant is created lazily on first login, so this must run before any
|
||||||
|
// tenant-scoped read. The in-flight promise is shared so the login flow (which
|
||||||
|
// awaits it to roll back on failure) and the activation subscriber below don't
|
||||||
|
// double-provision; createTenant is itself idempotent.
|
||||||
|
let tenantEnsure: { pubkey: string; promise: Promise<void> } | undefined
|
||||||
|
|
||||||
|
export function ensureSessionTenant(): Promise<void> {
|
||||||
|
const pubkey = account()?.pubkey
|
||||||
|
if (!pubkey) return Promise.resolve()
|
||||||
|
if (tenantEnsure?.pubkey === pubkey) return tenantEnsure.promise
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
await createTenant()
|
||||||
|
if (account()?.pubkey === pubkey) setBillingPubkey(pubkey)
|
||||||
|
} finally {
|
||||||
|
if (tenantEnsure?.pubkey === pubkey) tenantEnsure = undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
tenantEnsure = { pubkey, promise }
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
// Deferred to avoid circular-import TDZ errors (api.ts <-> state.ts)
|
// Deferred to avoid circular-import TDZ errors (api.ts <-> state.ts)
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
try {
|
try {
|
||||||
@@ -97,5 +127,10 @@ queueMicrotask(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refetchIdentity()
|
refetchIdentity()
|
||||||
|
|
||||||
|
// Lock billing reads until the new account's tenant is ensured, so they never
|
||||||
|
// fire against a not-yet-provisioned tenant during signup.
|
||||||
|
setBillingPubkey(undefined)
|
||||||
|
if (account) void ensureSessionTenant().catch(() => {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { ExtensionAccount, NostrConnectAccount, PasswordAccount, PrivateKeyAccou
|
|||||||
import { PasswordSigner } from "applesauce-signers"
|
import { PasswordSigner } from "applesauce-signers"
|
||||||
import QrScanner from "qr-scanner"
|
import QrScanner from "qr-scanner"
|
||||||
import QRCode from "qrcode"
|
import QRCode from "qrcode"
|
||||||
import { accountManager, identity, PLATFORM_NAME } from "@/lib/state"
|
import { accountManager, ensureSessionTenant, identity, PLATFORM_NAME } from "@/lib/state"
|
||||||
import { createTenant } from "@/lib/api"
|
|
||||||
import useMinLoading from "@/components/useMinLoading"
|
import useMinLoading from "@/components/useMinLoading"
|
||||||
|
|
||||||
const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc']
|
const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc']
|
||||||
@@ -71,7 +70,7 @@ export default function Login(props: LoginPageProps = {}) {
|
|||||||
accountManager.addAccount(account)
|
accountManager.addAccount(account)
|
||||||
accountManager.setActive(account)
|
accountManager.setActive(account)
|
||||||
try {
|
try {
|
||||||
await createTenant()
|
await ensureSessionTenant()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
accountManager.removeAccount(account)
|
accountManager.removeAccount(account)
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
Reference in New Issue
Block a user