fileState.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { fileApi } from "./fileApi";
  2. import { parseContentDispositionFilename } from "./filename";
  3. interface PreviewRequest {
  4. controller: AbortController;
  5. promise: Promise<Awaited<ReturnType<typeof fileApi.preview>>>;
  6. consumers: number;
  7. }
  8. const previewRequests = new Map<string, PreviewRequest>();
  9. const previewResults = new Map<string, Awaited<ReturnType<typeof fileApi.preview>>>();
  10. function previewKey(id: string, version: string | number = ""): string {
  11. return `${id}:${version}`;
  12. }
  13. export function loadPreviewOnce(id: string, version: string | number = "") {
  14. const key = previewKey(id, version);
  15. const cached = previewResults.get(key);
  16. if (cached) return Promise.resolve(cached);
  17. const existing = previewRequests.get(key);
  18. if (existing) return existing.promise;
  19. const controller = new AbortController();
  20. const request: PreviewRequest = {
  21. controller,
  22. consumers: 0,
  23. promise: Promise.resolve(undefined as never),
  24. };
  25. request.promise = fileApi.preview(id, controller.signal)
  26. .then((result) => {
  27. previewResults.set(key, result);
  28. return result;
  29. })
  30. .finally(() => {
  31. if (previewRequests.get(key) === request) previewRequests.delete(key);
  32. });
  33. previewRequests.set(key, request);
  34. return request.promise;
  35. }
  36. export function acquirePreview(id: string, version: string | number = ""): {
  37. promise: Promise<Awaited<ReturnType<typeof fileApi.preview>>>;
  38. release: () => void;
  39. } {
  40. const key = previewKey(id, version);
  41. const promise = loadPreviewOnce(id, version);
  42. const request = previewRequests.get(key);
  43. if (!request) return { promise, release: () => undefined };
  44. request.consumers += 1;
  45. let released = false;
  46. return {
  47. promise,
  48. release: () => {
  49. if (released) return;
  50. released = true;
  51. request.consumers -= 1;
  52. globalThis.setTimeout(() => {
  53. if (
  54. request.consumers === 0 &&
  55. previewRequests.get(key) === request
  56. ) {
  57. request.controller.abort();
  58. previewRequests.delete(key);
  59. }
  60. }, 0);
  61. },
  62. };
  63. }
  64. export function createPreviewUrl(blob: Blob): string {
  65. return URL.createObjectURL(blob);
  66. }
  67. export function revokeObjectUrl(url: string | null): void {
  68. if (url) URL.revokeObjectURL(url);
  69. }
  70. export async function downloadFile(
  71. id: string,
  72. fallbackName: string,
  73. signal?: AbortSignal,
  74. ): Promise<{ filename: string; requestId: string }> {
  75. const result = await fileApi.download(id, signal);
  76. const filename = parseContentDispositionFilename(
  77. result.contentDisposition,
  78. fallbackName,
  79. );
  80. const url = URL.createObjectURL(result.blob);
  81. const anchor = document.createElement("a");
  82. try {
  83. anchor.href = url;
  84. anchor.download = filename;
  85. anchor.style.display = "none";
  86. document.body.appendChild(anchor);
  87. anchor.click();
  88. } finally {
  89. anchor.remove();
  90. URL.revokeObjectURL(url);
  91. }
  92. return { filename, requestId: result.requestId };
  93. }
  94. export function clearPreviewRequestsForTests(): void {
  95. previewRequests.clear();
  96. previewResults.clear();
  97. }
  98. export function invalidatePreview(id: string): void {
  99. for (const key of previewResults.keys()) {
  100. if (key.startsWith(`${id}:`)) previewResults.delete(key);
  101. }
  102. }