Fix async picker race

This commit is contained in:
“Naeel”
2026-09-05 12:31:17 +03:00
parent eda5263d8b
commit 1e305485be
4 changed files with 75 additions and 44 deletions
+20
View File
@@ -146,6 +146,26 @@
ZIP с `folder/../../escape.txt`, `/absolute.md`, `windows\\..\\bad.txt`, `folder/./dot.txt` и безопасным `safe/normal.txt` оставил только `paths.zip/safe/normal.txt`. Группы `..`, пустая группа и абсолютный путь больше не отображаются. Результат: `1 файлов · 2 B`. ZIP с `folder/../../escape.txt`, `/absolute.md`, `windows\\..\\bad.txt`, `folder/./dot.txt` и безопасным `safe/normal.txt` оставил только `paths.zip/safe/normal.txt`. Группы `..`, пустая группа и абсолютный путь больше не отображаются. Результат: `1 файлов · 2 B`.
## Исправление async race — 2026-09-05
### Причина
`onFilesChange` запускал асинхронный `addFiles` без ожидания и без установки `state.busy` до первого `await`. При быстрых повторных событиях выбора ZIP несколько обработчиков могли одновременно менять дерево и вызывать `render`.
### Исправление
- `onFilesChange` теперь сразу устанавливает `state.busy`, ожидает `addFiles` и сбрасывает флаг в `finally`.
- `onFolderChange` использует тот же порядок: ранний выход при занятом состоянии, блокировка до асинхронного разбора ZIP и гарантированный сброс в `finally`.
- Ошибка разбора ZIP больше не оставляет picker заблокированным.
### Проверки
- Все frontend `.js` файлы проходят `node --input-type=module --check`.
- `site/app.py` проходит `python3 -m py_compile`.
- `git diff --check` проходит.
- Flask smoke test загрузил страницу и оба изменённых ES-модуля без HTTP-ошибок.
- Узкий Node-тест не применён: в проекте отсутствует `package.json` с `type: module`, поэтому Node трактует browser ES-модули как CommonJS; синтаксис проверен отдельным module-check, а загрузка проверена браузером.
- `c0b8788 Implement reusable upload platform` — первоначальный reusable upload platform. - `c0b8788 Implement reusable upload platform` — первоначальный reusable upload platform.
- `85605fe Remove VM upload layer; keep file picker only` — удаление VM upload и переход к picker-only. - `85605fe Remove VM upload layer; keep file picker only` — удаление VM upload и переход к picker-only.
- `a965f18 Add hierarchical file picker` — иерархическое дерево, вложенные ZIP, удаление групп и файлов. - `a965f18 Add hierarchical file picker` — иерархическое дерево, вложенные ZIP, удаление групп и файлов.
+1 -1
View File
@@ -9,7 +9,7 @@ with (ROOT / "config.json").open(encoding="utf-8") as config_file:
CONFIG = json.load(config_file) CONFIG = json.load(config_file)
VERSION = "0.1.8" VERSION = "0.1.9"
app = Flask(__name__, template_folder="templates", static_folder="static") app = Flask(__name__, template_folder="templates", static_folder="static")
+8 -2
View File
@@ -25,7 +25,13 @@ export async function addFiles(state, cfg, files, elements) {
} }
export function onFilesChange(state, cfg, elements) { export function onFilesChange(state, cfg, elements) {
return () => { return async () => {
if (!state.busy) addFiles(state, cfg, elements.fileInputEl.files, elements); if (state.busy) return;
state.busy = true;
try {
await addFiles(state, cfg, elements.fileInputEl.files, elements);
} finally {
state.busy = false;
}
}; };
} }
@@ -12,6 +12,8 @@ function rebaseTree(root, prefix) {
export function onFolderChange(state, cfg, elements) { export function onFolderChange(state, cfg, elements) {
return async () => { return async () => {
if (state.busy) return; if (state.busy) return;
state.busy = true;
try {
const roots = new Map(); const roots = new Map();
for (const file of Array.from(elements.folderInputEl.files)) { for (const file of Array.from(elements.folderInputEl.files)) {
const parts = (file.webkitRelativePath || file.name).split('/'); const parts = (file.webkitRelativePath || file.name).split('/');
@@ -56,5 +58,8 @@ export function onFolderChange(state, cfg, elements) {
roots.forEach((root) => addFileWithDedup(state, root)); roots.forEach((root) => addFileWithDedup(state, root));
elements.folderInputEl.value = ''; elements.folderInputEl.value = '';
render(state, elements); render(state, elements);
} finally {
state.busy = false;
}
}; };
} }