fix(files): harden drag-and-drop uploads

Co-authored-by: Serhii Dziupin <serkraser@gmail.com>

Co-authored-by: Alan Chen <2144783+alanzchen@users.noreply.github.com>
This commit is contained in:
Bohdan Triapitsyn
2026-08-18 23:16:46 +03:00
co-authored by Serhii Dziupin Alan Chen
parent 0c5e183c62
commit 99873a7b12
9 changed files with 297 additions and 73 deletions
@@ -0,0 +1,38 @@
import { describe, expect, test } from 'bun:test';
import {
notifyFileContentInvalidated,
subscribeToFileContentInvalidation,
} from './fileContentInvalidation';
describe('fileContentInvalidation', () => {
test('publishes normalized paths within the captured runtime', () => {
const received: Array<{ runtimeKey: string; paths: readonly string[] }> = [];
const unsubscribe = subscribeToFileContentInvalidation((invalidation) => {
received.push(invalidation);
});
notifyFileContentInvalidated({
runtimeKey: ' runtime-a ',
paths: [' /repo/a.txt ', '/repo/a.txt', '', '/repo/b.txt'],
});
unsubscribe();
expect(received).toEqual([{
runtimeKey: 'runtime-a',
paths: ['/repo/a.txt', '/repo/b.txt'],
}]);
});
test('stops publishing after unsubscribe', () => {
let calls = 0;
const unsubscribe = subscribeToFileContentInvalidation(() => {
calls += 1;
});
unsubscribe();
notifyFileContentInvalidated({ runtimeKey: 'runtime-a', paths: ['/repo/a.txt'] });
expect(calls).toBe(0);
});
});
@@ -0,0 +1,25 @@
type FileContentInvalidation = {
runtimeKey: string;
paths: readonly string[];
};
type FileContentInvalidationListener = (invalidation: FileContentInvalidation) => void;
const listeners = new Set<FileContentInvalidationListener>();
export const notifyFileContentInvalidated = (invalidation: FileContentInvalidation): void => {
const runtimeKey = invalidation.runtimeKey.trim();
const paths = Array.from(new Set(invalidation.paths.map((path) => path.trim()).filter(Boolean)));
if (!runtimeKey || paths.length === 0) return;
for (const listener of listeners) {
listener({ runtimeKey, paths });
}
};
export const subscribeToFileContentInvalidation = (
listener: FileContentInvalidationListener,
): (() => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
};