diff --git a/docs/architecture-audit-2026-10.md b/docs/architecture-audit-2026-10.md index b7443c4..9ebea10 100644 --- a/docs/architecture-audit-2026-10.md +++ b/docs/architecture-audit-2026-10.md @@ -35,17 +35,12 @@ `schema-tool-state.svelte.ts` (стейт шагов), `AlignedLayout.svelte` / `WorkspaceLayout.svelte`. -### H2. Race condition превью: устаревший ответ worker'а перезаписывает новый +### H2. Race condition превью — СДЕЛАНО (2026-10) -`SchemaToolView.svelte:430-465` (`run()`) + эффект :567-575. Debounce (:199) -отменяет только запланированный запуск; уже летящий `runChain` не отменяется и -нет generation-токена: при смене параметров во время долгого worker-прогона -старый результат перезапишет новый (`assignResult` без проверки актуальности). -Сам executor с request-id корректен (`executor/executor.ts:45-96`) — проблема на -уровне UI. Пользователь видит превью не от своих параметров. - -Минимальный фикс: -`let runGen = 0; const gen = ++runGen; ... if (gen !== runGen) return;`. +`SchemaToolView.svelte`: добавлен generation-counter `runGen`; устаревший прогон +`runChain` после `await` выходит без записи результатов, ошибок и сброса +`running` (`finally` сбрасывает только актуальный); `init()` инкрементом +инвалидирует прогоны прошлого хоста. ### H3. `registry-schema.ts` — 723 строки, 45 экспортов, 5 ответственностей diff --git a/web/src/lib/components/schema/SchemaToolView.svelte b/web/src/lib/components/schema/SchemaToolView.svelte index a7dbfef..b27393b 100644 --- a/web/src/lib/components/schema/SchemaToolView.svelte +++ b/web/src/lib/components/schema/SchemaToolView.svelte @@ -122,6 +122,9 @@ undefined, ); let running = $state(false); + // Generation of the latest run(): a stale in-flight chain (params changed + // while the worker was busy) must not overwrite newer results. + let runGen = 0; let displayError = $state(null); let sourceWarnings = $state([]); let runWarnings = $state([]); @@ -220,6 +223,8 @@ const host = chainId ? `chain:${chainId}` : page.slug; if (host === initedFor) return; initedFor = host; + // Invalidate any in-flight run from the previous host. + runGen++; if (chainId) { const handoff = takeHandoff(chainId); steps = getChain(chainId)?.steps ?? createChain(page); @@ -429,6 +434,7 @@ async function run() { if (!schema || steps.length === 0) return; + const gen = ++runGen; displayError = null; running = true; try { @@ -439,12 +445,14 @@ ? [{ name: `${baseName(page.slug)}.png`, image: source }] : []; const r = await runChain(steps, input, textSource || undefined, execute); + if (gen !== runGen) return; stepDims = r.stepDims; stepResults = r.stepResults; stepFileSets = r.stepFileSets; runWarnings = r.warnings; assignResult(r.out); } catch (e) { + if (gen !== runGen) return; if (e instanceof ChainStepError) { const base = toDisplayError(e.cause); const msg = base.kind === "i18n" ? t(base.key, base.vars) : base.text; @@ -460,7 +468,7 @@ displayError = toDisplayError(e); } } finally { - running = false; + if (gen === runGen) running = false; } }