fix(onboarding): atomic DB append for step completion (deferred bug L3)
markOnboardingStepDone read space_members.onboarding_progress, appended the step id in JS, and wrote the whole jsonb object back — a lost-update race where two near-simultaneous completions (double-submit, or two steps ticked quickly) both read the same base array and the second write clobbered the first, silently dropping a step. Replace it with mark_onboarding_step_done (migration 055): a single atomic UPDATE that dedup-appends the step id into onboarding_progress.completed_step_ids under the row lock — no update can be lost (READ COMMITTED re-evaluates the jsonb expression against the latest tuple via EvalPlanQual), other keys are preserved, and re-appending an existing id is a no-op. Ownership-guarded by auth.uid() so a caller can only complete steps on their own membership row; the no-JWT service path is trusted and scoped by p_member_id (mirrors class_signup_tx et al.). integration/onboarding-progress.test.ts covers dedup/idempotency, key preservation, the auth.uid ownership gate, and the point of the fix: 12 concurrent completions all survive. Migration 055 folded into schema.sql; types/database.ts regenerated. typecheck 0 · lint 0 errors · 645 unit · 58 integration · build ok
This commit is contained in:
@@ -130,20 +130,15 @@ export async function markOnboardingStepDone(stepId: string) {
|
||||
if (!auth.ok) return { error: auth.error }
|
||||
const { member } = auth
|
||||
|
||||
const { data: row } = await supabase
|
||||
.from('space_members')
|
||||
.select('onboarding_progress')
|
||||
.eq('id', member.id)
|
||||
.single()
|
||||
|
||||
const progress = (row?.onboarding_progress ?? {}) as { completed_step_ids?: string[] }
|
||||
const done = new Set(progress.completed_step_ids ?? [])
|
||||
done.add(v.data)
|
||||
|
||||
const { error } = await supabase
|
||||
.from('space_members')
|
||||
.update({ onboarding_progress: { ...progress, completed_step_ids: Array.from(done) } })
|
||||
.eq('id', member.id)
|
||||
// Atomic dedup-append in the DB (migration 055). The old read-modify-write on
|
||||
// onboarding_progress had a lost-update race: two near-simultaneous
|
||||
// completions both read the same base array and the second write clobbered
|
||||
// the first, silently dropping a step. The RPC pins the write to the caller's
|
||||
// own row via auth.uid().
|
||||
const { error } = await supabase.rpc('mark_onboarding_step_done', {
|
||||
p_member_id: member.id,
|
||||
p_step_id: v.data,
|
||||
})
|
||||
|
||||
if (error) return { error: error.message }
|
||||
return { success: true as const }
|
||||
|
||||
Reference in New Issue
Block a user