upload-progress.ts 1.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. import { useCallback, useSyncExternalStore } from 'react';
  2. /** Upload progress per upload id, in whole percent. Module-level so it survives form remounts. */
  3. const progressById = new Map<string, number>();
  4. const listenersById = new Map<string, Set<() => void>>();
  5. function notify(id: string) {
  6. listenersById.get(id)?.forEach((listener) => listener());
  7. }
  8. /** Record `fraction` (0..1) sent for `id`; subscribers re-render only when the whole percent changes. */
  9. export function setUploadProgress(id: string | null | undefined, fraction: number) {
  10. if (!id) return;
  11. const percent = Math.max(0, Math.min(100, Math.floor(fraction * 100)));
  12. if (progressById.get(id) === percent) return;
  13. progressById.set(id, percent);
  14. notify(id);
  15. }
  16. export function clearUploadProgress(id: string | null | undefined) {
  17. if (!id || !progressById.has(id)) return;
  18. progressById.delete(id);
  19. notify(id);
  20. }
  21. /** Percent sent (0..100) for an upload, `undefined` before the first progress event. */
  22. export function useUploadProgress(id: string | null | undefined): number | undefined {
  23. const subscribe = useCallback((listener: () => void) => {
  24. if (!id) return () => {};
  25. let listeners = listenersById.get(id);
  26. if (!listeners) {
  27. listeners = new Set();
  28. listenersById.set(id, listeners);
  29. }
  30. listeners.add(listener);
  31. return () => {
  32. listeners.delete(listener);
  33. if (!listeners.size) listenersById.delete(id);
  34. };
  35. }, [id]);
  36. const getSnapshot = () => (id ? progressById.get(id) : undefined);
  37. return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
  38. }