@design.estate/dees-catalog 6.12.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.smartconfig.json +5 -1
  2. package/dist_bundle/bundle.js +24090 -1235
  3. package/dist_bundle/bundle.js.map +1 -0
  4. package/dist_bundle/chunks/U6Ua8p1giFoD1s7vrUZUdpbg/.tsbundle-artifacts.json +7 -0
  5. package/dist_bundle/chunks/U6Ua8p1giFoD1s7vrUZUdpbg/worker-KZQQYSUG.js +58301 -0
  6. package/dist_bundle/chunks/U6Ua8p1giFoD1s7vrUZUdpbg/worker-KZQQYSUG.js.map +1 -0
  7. package/dist_bundle/third-party-notices.md +190 -0
  8. package/dist_ts_web/00_commitinfo_data.js +2 -2
  9. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +3 -3
  10. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +4 -1
  11. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +29 -2
  12. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.d.ts +4 -1
  13. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.js +65 -8
  14. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +1 -0
  15. package/dist_ts_web/elements/00group-media/dees-pdf-shared/CanvasPool.d.ts +1 -0
  16. package/dist_ts_web/elements/00group-media/dees-pdf-shared/CanvasPool.js +15 -1
  17. package/dist_ts_web/elements/00group-media/dees-pdf-shared/PdfManager.d.ts +6 -4
  18. package/dist_ts_web/elements/00group-media/dees-pdf-shared/PdfManager.js +183 -21
  19. package/dist_ts_web/elements/00group-media/dees-pdf-shared/utils.d.ts +3 -0
  20. package/dist_ts_web/elements/00group-media/dees-pdf-shared/utils.js +67 -1
  21. package/dist_ts_web/elements/00group-media/dees-pdf-viewer/component.d.ts +25 -1
  22. package/dist_ts_web/elements/00group-media/dees-pdf-viewer/component.js +693 -285
  23. package/dist_ts_web/elements/00group-media/dees-pdf-viewer/demo.js +7 -7
  24. package/dist_ts_web/elements/00group-media/dees-thumbnail-pdf/component.d.ts +20 -1
  25. package/dist_ts_web/elements/00group-media/dees-thumbnail-pdf/component.js +379 -116
  26. package/dist_ts_web/elements/00group-media/dees-thumbnail-pdf/demo.js +2 -2
  27. package/dist_ts_web/elements/00group-media/dees-thumbnail-shared/DeesThumbnailBase.d.ts +1 -0
  28. package/dist_ts_web/elements/00group-media/dees-thumbnail-shared/DeesThumbnailBase.js +7 -1
  29. package/package.json +5 -4
  30. package/readme.hints.md +26 -19
  31. package/readme.md +104 -53
  32. package/third-party-notices.md +190 -0
  33. package/ts_web/00_commitinfo_data.ts +1 -1
  34. package/ts_web/elements/00group-chart/dees-chart-area/component.ts +2 -2
  35. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +13 -0
  36. package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts +51 -6
  37. package/ts_web/elements/00group-harness/interfaces.ts +2 -0
  38. package/ts_web/elements/00group-media/dees-pdf-shared/CanvasPool.ts +15 -1
  39. package/ts_web/elements/00group-media/dees-pdf-shared/PdfManager.ts +214 -24
  40. package/ts_web/elements/00group-media/dees-pdf-shared/utils.ts +77 -1
  41. package/ts_web/elements/00group-media/dees-pdf-viewer/component.ts +774 -327
  42. package/ts_web/elements/00group-media/dees-pdf-viewer/demo.ts +7 -7
  43. package/ts_web/elements/00group-media/dees-thumbnail-pdf/component.ts +427 -128
  44. package/ts_web/elements/00group-media/dees-thumbnail-pdf/demo.ts +1 -1
  45. package/ts_web/elements/00group-media/dees-thumbnail-shared/DeesThumbnailBase.ts +5 -0
@@ -1,9 +1,16 @@
1
1
  import { property, state, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element';
2
+ import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
2
3
  import { DeesThumbnailBase } from '../dees-thumbnail-shared/DeesThumbnailBase.js';
3
4
  import { thumbnailBaseStyles } from '../dees-thumbnail-shared/styles.js';
4
5
  import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
5
6
  import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js';
6
- import { PerformanceMonitor, throttle, formatFileSize } from '../dees-pdf-shared/utils.js';
7
+ import {
8
+ throttle,
9
+ formatFileSize,
10
+ raceWithAbort,
11
+ settlePromisesWithin,
12
+ waitForAnimationFrame,
13
+ } from '../dees-pdf-shared/utils.js';
7
14
  import { tilePdfStyles } from './styles.js';
8
15
  import { demo as demoFunc } from './demo.js';
9
16
 
@@ -13,6 +20,13 @@ declare global {
13
20
  }
14
21
  }
15
22
 
23
+ interface ILoadOperation {
24
+ key: string;
25
+ promise: Promise<void>;
26
+ }
27
+
28
+ const pipelineSettlementGraceMs = 2_500;
29
+
16
30
  @customElement('dees-thumbnail-pdf')
17
31
  export class DeesThumbnailPdf extends DeesThumbnailBase {
18
32
  public static demo = demoFunc;
@@ -41,8 +55,21 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
41
55
  accessor fileSize: number = 0;
42
56
 
43
57
  private renderPagesTask: Promise<void> | null = null;
58
+ private renderPagesRequestPromise: Promise<void> | null = null;
59
+ private currentPageRenderTask: RenderTask | null = null;
44
60
  private renderPagesQueued: boolean = false;
45
- private pdfDocument: any;
61
+ private renderQueueEpoch = 0;
62
+ private pdfDocument: PDFDocumentProxy | null = null;
63
+ private pdfLoadPromise: Promise<PDFDocumentProxy> | null = null;
64
+ private loadAbortController: AbortController | null = null;
65
+ private loadOperation: ILoadOperation | null = null;
66
+ private loadGeneration = 0;
67
+ private connectionEpoch = 0;
68
+ private cleanupPromise: Promise<void> | null = null;
69
+ private renderFailurePromise: Promise<void> | null = null;
70
+ private renderAdmissionOpen = false;
71
+ private renderGeneration = 0;
72
+ private documentUsePipelines = new Set<Promise<void>>();
46
73
  private canvases: PooledCanvas[] = [];
47
74
  private resizeObserver?: ResizeObserver;
48
75
  private stackElement: HTMLElement | null = null;
@@ -90,8 +117,8 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
90
117
  }
91
118
 
92
119
  protected onBecameVisible(): void {
93
- if (!this.rendered && this.pdfUrl) {
94
- this.loadAndRenderPreview();
120
+ if (this.isConnected && !this.rendered && this.pdfUrl) {
121
+ void this.schedulePdfLoad(this.connectionEpoch);
95
122
  }
96
123
  }
97
124
 
@@ -103,7 +130,6 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
103
130
  this.isHovering = false;
104
131
  if (this.currentPreviewPage !== 1) {
105
132
  this.currentPreviewPage = 1;
106
- void this.scheduleRenderPages();
107
133
  }
108
134
  }
109
135
 
@@ -119,171 +145,376 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
119
145
 
120
146
  if (newPage !== this.currentPreviewPage) {
121
147
  this.currentPreviewPage = newPage;
122
- void this.scheduleRenderPages();
123
148
  }
124
149
  }
125
150
 
126
151
  public async connectedCallback(): Promise<void> {
152
+ const connectionEpoch = ++this.connectionEpoch;
127
153
  await super.connectedCallback();
128
154
  await this.updateComplete;
155
+ if (!this.isConnected || connectionEpoch !== this.connectionEpoch) return;
129
156
  this.cacheElements();
130
157
  this.setupResizeObserver();
158
+ if (
159
+ this.pdfUrl
160
+ && this.isVisible
161
+ ) {
162
+ const operationKey = `${connectionEpoch}:${this.pdfUrl}`;
163
+ if (this.loadOperation?.key === operationKey) {
164
+ await this.loadOperation.promise;
165
+ } else if (connectionEpoch > 1 || !this.pdfDocument) {
166
+ await this.schedulePdfLoad(connectionEpoch);
167
+ }
168
+ }
131
169
  }
132
170
 
133
171
  public async disconnectedCallback(): Promise<void> {
172
+ this.connectionEpoch += 1;
173
+ this.loadGeneration += 1;
174
+ this.renderAdmissionOpen = false;
175
+ this.loadAbortController?.abort();
176
+ const cleanupPromise = this.cleanup();
134
177
  await super.disconnectedCallback();
135
- this.cleanup();
136
- this.resizeObserver?.disconnect();
137
- this.resizeObserver = undefined;
178
+ await cleanupPromise;
138
179
  }
139
180
 
140
- private async loadAndRenderPreview(): Promise<void> {
141
- if (this.rendered || this.loading) return;
181
+ private schedulePdfLoad(connectionEpochArg: number): Promise<void> {
182
+ const url = this.pdfUrl;
183
+ const key = `${connectionEpochArg}:${url}`;
184
+ if (this.loadOperation?.key === key) {
185
+ return this.loadOperation.promise;
186
+ }
187
+ const generation = ++this.loadGeneration;
188
+ const operation: ILoadOperation = {
189
+ key,
190
+ promise: Promise.resolve(),
191
+ };
192
+ operation.promise = this.performPdfLoad(generation, connectionEpochArg, url)
193
+ .finally(() => {
194
+ if (this.loadOperation === operation) {
195
+ this.loadOperation = null;
196
+ }
197
+ });
198
+ this.loadOperation = operation;
199
+ return operation.promise;
200
+ }
201
+
202
+ private isCurrentLoad(generationArg: number, connectionEpochArg: number, urlArg: string): boolean {
203
+ return this.isConnected
204
+ && generationArg === this.loadGeneration
205
+ && connectionEpochArg === this.connectionEpoch
206
+ && urlArg === this.pdfUrl;
207
+ }
208
+
209
+ private async performPdfLoad(
210
+ generationArg: number,
211
+ connectionEpochArg: number,
212
+ urlArg: string,
213
+ ): Promise<void> {
214
+ await this.cleanup();
215
+ if (
216
+ !this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)
217
+ || !urlArg
218
+ || !this.isVisible
219
+ ) return;
142
220
 
143
221
  this.loading = true;
144
222
  this.error = false;
145
- PerformanceMonitor.mark(`preview-load-${this.pdfUrl}`);
223
+ const loadStartedAt = performance.now();
224
+ const abortController = new AbortController();
225
+ this.loadAbortController = abortController;
226
+ const { signal } = abortController;
146
227
 
147
228
  try {
148
- this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
149
- this.pageCount = this.pdfDocument.numPages;
150
- this.currentPreviewPage = 1;
151
-
229
+ const pdfLoadPromise = PdfManager.loadDocument(urlArg, signal);
230
+ this.pdfLoadPromise = pdfLoadPromise;
231
+ let pdfDocument: PDFDocumentProxy;
152
232
  try {
153
- const downloadInfo = await this.pdfDocument.getDownloadInfo();
154
- this.fileSize = downloadInfo.length;
155
- } catch {
156
- // File size unavailable — not critical
233
+ pdfDocument = await pdfLoadPromise;
234
+ } finally {
235
+ if (this.pdfLoadPromise === pdfLoadPromise) {
236
+ this.pdfLoadPromise = null;
237
+ }
157
238
  }
158
- this.loadedPdfUrl = this.pdfUrl;
159
-
160
- this.loading = false;
161
- await this.updateComplete;
162
- this.cacheElements();
239
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) || signal.aborted) {
240
+ await PdfManager.releaseDocument(pdfDocument);
241
+ return;
242
+ }
243
+ this.pdfDocument = pdfDocument;
244
+ this.renderAdmissionOpen = true;
245
+ const documentUsePipeline = (async () => {
246
+ this.pageCount = pdfDocument.numPages;
247
+ this.currentPreviewPage = 1;
248
+
249
+ try {
250
+ const downloadInfo = await raceWithAbort(pdfDocument.getDownloadInfo(), signal);
251
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
252
+ this.fileSize = downloadInfo.length;
253
+ } catch {
254
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
255
+ }
256
+ this.loadedPdfUrl = urlArg;
257
+
258
+ this.loading = false;
259
+ await this.updateComplete;
260
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
261
+ this.cacheElements();
262
+ this.setupResizeObserver();
263
+
264
+ await this.scheduleRenderPages();
265
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
266
+ this.rendered = true;
267
+
268
+ const duration = performance.now() - loadStartedAt;
269
+ console.log(`PDF tile rendered in ${duration}ms`);
270
+ })();
271
+ this.documentUsePipelines.add(documentUsePipeline);
272
+ try {
273
+ await documentUsePipeline;
274
+ } finally {
275
+ this.documentUsePipelines.delete(documentUsePipeline);
276
+ }
277
+ } catch (error) {
278
+ if (this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) && !signal.aborted) {
279
+ await this.handleDocumentFailure(
280
+ error,
281
+ generationArg,
282
+ connectionEpochArg,
283
+ urlArg,
284
+ this.pdfDocument,
285
+ );
286
+ }
287
+ }
288
+ }
163
289
 
164
- await this.scheduleRenderPages();
165
- this.rendered = true;
290
+ private requestRenderPages(): void {
291
+ const requestPromise = this.scheduleRenderPages();
292
+ if (this.renderPagesRequestPromise === requestPromise) return;
293
+ this.renderPagesRequestPromise = requestPromise;
294
+ const generation = this.loadGeneration;
295
+ const connectionEpoch = this.connectionEpoch;
296
+ const url = this.pdfUrl;
297
+ const pdfDocument = this.pdfDocument;
298
+ void requestPromise.catch(async (error) => {
299
+ await this.handleDocumentFailure(
300
+ error,
301
+ generation,
302
+ connectionEpoch,
303
+ url,
304
+ pdfDocument,
305
+ );
306
+ }).finally(() => {
307
+ if (this.renderPagesRequestPromise === requestPromise) {
308
+ this.renderPagesRequestPromise = null;
309
+ }
310
+ }).catch(() => {});
311
+ }
166
312
 
167
- const duration = PerformanceMonitor.measure(`preview-render-${this.pdfUrl}`, `preview-load-${this.pdfUrl}`);
168
- console.log(`PDF tile rendered in ${duration}ms`);
169
- } catch (error) {
170
- console.error('Failed to load PDF tile:', error);
171
- this.error = true;
172
- this.loading = false;
313
+ private handleDocumentFailure(
314
+ errorArg: unknown,
315
+ generationArg: number,
316
+ connectionEpochArg: number,
317
+ urlArg: string,
318
+ pdfDocumentArg: PDFDocumentProxy | null,
319
+ ): Promise<void> {
320
+ if (
321
+ errorArg instanceof Error
322
+ && ['AbortError', 'AbortException', 'RenderingCancelledException'].includes(errorArg.name)
323
+ ) {
324
+ return Promise.resolve();
173
325
  }
326
+ if (
327
+ !this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)
328
+ || (pdfDocumentArg && pdfDocumentArg !== this.pdfDocument)
329
+ ) {
330
+ return Promise.resolve();
331
+ }
332
+ if (this.renderFailurePromise) return this.renderFailurePromise;
333
+
334
+ this.renderAdmissionOpen = false;
335
+ this.loadAbortController?.abort();
336
+ console.error('Failed to load or render PDF thumbnail:', errorArg);
337
+ const failurePromise = (async () => {
338
+ await this.cleanup();
339
+ if (
340
+ this.isConnected
341
+ && generationArg === this.loadGeneration
342
+ && connectionEpochArg === this.connectionEpoch
343
+ && urlArg === this.pdfUrl
344
+ ) {
345
+ this.error = true;
346
+ this.loading = false;
347
+ }
348
+ })();
349
+ this.renderFailurePromise = failurePromise;
350
+ void failurePromise.finally(() => {
351
+ if (this.renderFailurePromise === failurePromise) {
352
+ this.renderFailurePromise = null;
353
+ }
354
+ }).catch(() => {});
355
+ return failurePromise;
174
356
  }
175
357
 
176
358
  private scheduleRenderPages(): Promise<void> {
177
- if (!this.pdfDocument) {
359
+ if (!this.renderAdmissionOpen || !this.pdfDocument) {
178
360
  return Promise.resolve();
179
361
  }
180
362
 
181
363
  if (this.renderPagesTask) {
364
+ this.renderGeneration += 1;
182
365
  this.renderPagesQueued = true;
366
+ try {
367
+ this.currentPageRenderTask?.cancel();
368
+ } catch {
369
+ // The active task remains part of the render queue settlement.
370
+ }
183
371
  return this.renderPagesTask;
184
372
  }
185
373
 
186
- this.renderPagesTask = (async () => {
187
- try {
188
- await this.performRenderPages();
189
- } catch (error) {
190
- console.error('Failed to render PDF tile pages:', error);
191
- }
192
- })().finally(() => {
193
- this.renderPagesTask = null;
194
- if (this.renderPagesQueued) {
374
+ this.renderGeneration += 1;
375
+ const renderQueueEpoch = ++this.renderQueueEpoch;
376
+ const renderPagesTask = (async () => {
377
+ do {
378
+ if (renderQueueEpoch !== this.renderQueueEpoch) return;
195
379
  this.renderPagesQueued = false;
196
- void this.scheduleRenderPages();
380
+ const renderGeneration = this.renderGeneration;
381
+ await this.performRenderPages(renderGeneration);
382
+ } while (
383
+ renderQueueEpoch === this.renderQueueEpoch
384
+ && this.renderPagesQueued
385
+ && this.renderAdmissionOpen
386
+ && this.pdfDocument
387
+ );
388
+ })();
389
+ this.renderPagesTask = renderPagesTask;
390
+ void renderPagesTask.finally(() => {
391
+ if (this.renderPagesTask === renderPagesTask) {
392
+ this.renderPagesTask = null;
197
393
  }
198
- });
394
+ }).catch(() => {});
199
395
 
200
- return this.renderPagesTask;
396
+ return renderPagesTask;
201
397
  }
202
398
 
203
- private async performRenderPages(): Promise<void> {
204
- if (!this.pdfDocument) return;
399
+ private async performRenderPages(renderGenerationArg: number): Promise<void> {
400
+ const pdfDocument = this.pdfDocument;
401
+ const signal = this.loadAbortController?.signal;
402
+ if (!this.renderAdmissionOpen || !pdfDocument || !signal) return;
403
+
404
+ const isCurrentRender = (): boolean => (
405
+ this.isConnected
406
+ && this.renderAdmissionOpen
407
+ && !signal.aborted
408
+ && renderGenerationArg === this.renderGeneration
409
+ && pdfDocument === this.pdfDocument
410
+ );
205
411
 
206
- await new Promise(resolve => requestAnimationFrame(resolve));
412
+ await waitForAnimationFrame();
413
+ if (!isCurrentRender()) return;
207
414
 
208
415
  const canvas = this.shadowRoot?.querySelector('.preview-canvas') as HTMLCanvasElement;
209
- if (!canvas) return;
416
+ if (!canvas) throw new Error('PDF thumbnail preview canvas is unavailable');
210
417
 
211
418
  this.clearCanvases();
212
419
  this.cacheElements();
213
420
 
214
421
  const { availableWidth, availableHeight } = this.getAvailableSize();
215
422
 
423
+ let pooledCanvas: PooledCanvas | undefined;
424
+ let pooledCanvasRenderSettled = true;
216
425
  try {
217
426
  const pageNum = this.currentPreviewPage;
218
- const page = await this.pdfDocument.getPage(pageNum);
219
-
220
- const initialViewport = page.getViewport({ scale: 1 });
221
- const aspectRatio = initialViewport.height / initialViewport.width;
222
-
223
- const a4PortraitRatio = 1.414;
224
- const a4LandscapeRatio = 0.707;
225
- const letterPortraitRatio = 1.294;
226
- const letterLandscapeRatio = 0.773;
227
- const tolerance = 0.05;
228
-
229
- const isA4Portrait = Math.abs(aspectRatio - a4PortraitRatio) < (a4PortraitRatio * tolerance);
230
- const isA4Landscape = Math.abs(aspectRatio - a4LandscapeRatio) < (a4LandscapeRatio * tolerance);
231
- const isLetterPortrait = Math.abs(aspectRatio - letterPortraitRatio) < (letterPortraitRatio * tolerance);
232
- const isLetterLandscape = Math.abs(aspectRatio - letterLandscapeRatio) < (letterLandscapeRatio * tolerance);
233
-
234
- this.isA4Format = isA4Portrait || isA4Landscape || isLetterPortrait || isLetterLandscape;
235
-
236
- const adjustedWidth = this.isA4Format ? availableWidth : availableWidth - 24;
237
- const adjustedHeight = this.isA4Format ? availableHeight : availableHeight - 24;
238
-
239
- const scaleX = adjustedWidth > 0 ? adjustedWidth / initialViewport.width : 0;
240
- const scaleY = adjustedHeight > 0 ? adjustedHeight / initialViewport.height : 0;
241
- const baseScale = Math.min(scaleX || 0.5, scaleY || scaleX || 0.5);
242
- const renderScale = Math.min(baseScale * 2, 3.0);
243
-
244
- if (!Number.isFinite(renderScale) || renderScale <= 0) {
245
- page.cleanup?.();
246
- return;
247
- }
427
+ const page = await raceWithAbort(pdfDocument.getPage(pageNum), signal);
428
+ try {
429
+ if (!isCurrentRender()) return;
430
+ const initialViewport = page.getViewport({ scale: 1 });
431
+ const aspectRatio = initialViewport.height / initialViewport.width;
248
432
 
249
- const viewport = page.getViewport({ scale: renderScale });
433
+ const a4PortraitRatio = 1.414;
434
+ const a4LandscapeRatio = 0.707;
435
+ const letterPortraitRatio = 1.294;
436
+ const letterLandscapeRatio = 0.773;
437
+ const tolerance = 0.05;
250
438
 
251
- const pooledCanvas = CanvasPool.acquire(viewport.width, viewport.height);
252
- this.canvases.push(pooledCanvas);
439
+ const isA4Portrait = Math.abs(aspectRatio - a4PortraitRatio) < (a4PortraitRatio * tolerance);
440
+ const isA4Landscape = Math.abs(aspectRatio - a4LandscapeRatio) < (a4LandscapeRatio * tolerance);
441
+ const isLetterPortrait = Math.abs(aspectRatio - letterPortraitRatio) < (letterPortraitRatio * tolerance);
442
+ const isLetterLandscape = Math.abs(aspectRatio - letterLandscapeRatio) < (letterLandscapeRatio * tolerance);
253
443
 
254
- const renderContext = {
255
- canvasContext: pooledCanvas.ctx,
256
- viewport: viewport,
257
- };
444
+ this.isA4Format = isA4Portrait || isA4Landscape || isLetterPortrait || isLetterLandscape;
258
445
 
259
- await page.render(renderContext).promise;
446
+ const adjustedWidth = this.isA4Format ? availableWidth : availableWidth - 24;
447
+ const adjustedHeight = this.isA4Format ? availableHeight : availableHeight - 24;
260
448
 
261
- canvas.width = viewport.width;
262
- canvas.height = viewport.height;
449
+ const scaleX = adjustedWidth > 0 ? adjustedWidth / initialViewport.width : 0;
450
+ const scaleY = adjustedHeight > 0 ? adjustedHeight / initialViewport.height : 0;
451
+ const baseScale = Math.min(scaleX || 0.5, scaleY || scaleX || 0.5);
452
+ const renderScale = Math.min(baseScale * 2, 3.0);
263
453
 
264
- const displayWidth = adjustedWidth;
265
- const displayHeight = (viewport.height / viewport.width) * adjustedWidth;
454
+ if (!Number.isFinite(renderScale) || renderScale <= 0) {
455
+ throw new Error('PDF thumbnail render scale is invalid');
456
+ }
266
457
 
267
- if (displayHeight > adjustedHeight) {
268
- const altDisplayHeight = adjustedHeight;
269
- const altDisplayWidth = (viewport.width / viewport.height) * adjustedHeight;
270
- canvas.style.width = `${altDisplayWidth}px`;
271
- canvas.style.height = `${altDisplayHeight}px`;
272
- } else {
273
- canvas.style.width = `${displayWidth}px`;
274
- canvas.style.height = `${displayHeight}px`;
275
- }
458
+ const viewport = page.getViewport({ scale: renderScale });
459
+ pooledCanvas = CanvasPool.acquire(viewport.width, viewport.height);
460
+ this.canvases.push(pooledCanvas);
461
+
462
+ const renderTask = page.render({
463
+ canvasContext: pooledCanvas.ctx,
464
+ viewport,
465
+ });
466
+ this.currentPageRenderTask = renderTask;
467
+ pooledCanvasRenderSettled = false;
468
+ const trackedRenderPromise = renderTask.promise.finally(() => {
469
+ pooledCanvasRenderSettled = true;
470
+ });
471
+ try {
472
+ await raceWithAbort(trackedRenderPromise, signal);
473
+ } finally {
474
+ if (this.currentPageRenderTask === renderTask) {
475
+ this.currentPageRenderTask = null;
476
+ }
477
+ }
478
+ if (!isCurrentRender()) return;
479
+
480
+ canvas.width = viewport.width;
481
+ canvas.height = viewport.height;
482
+
483
+ const displayWidth = adjustedWidth;
484
+ const displayHeight = (viewport.height / viewport.width) * adjustedWidth;
485
+
486
+ if (displayHeight > adjustedHeight) {
487
+ const altDisplayHeight = adjustedHeight;
488
+ const altDisplayWidth = (viewport.width / viewport.height) * adjustedHeight;
489
+ canvas.style.width = `${altDisplayWidth}px`;
490
+ canvas.style.height = `${altDisplayHeight}px`;
491
+ } else {
492
+ canvas.style.width = `${displayWidth}px`;
493
+ canvas.style.height = `${displayHeight}px`;
494
+ }
276
495
 
277
- const ctx = canvas.getContext('2d');
278
- if (ctx) {
496
+ const ctx = canvas.getContext('2d');
497
+ if (!ctx) throw new Error('PDF thumbnail preview context is unavailable');
279
498
  ctx.imageSmoothingEnabled = true;
280
499
  ctx.imageSmoothingQuality = 'high';
281
500
  ctx.drawImage(pooledCanvas.canvas, 0, 0);
501
+ } finally {
502
+ page.cleanup();
503
+ }
504
+ } catch (error: unknown) {
505
+ if (
506
+ error instanceof Error
507
+ && ['AbortError', 'AbortException', 'RenderingCancelledException'].includes(error.name)
508
+ ) {
509
+ return;
510
+ }
511
+ throw error;
512
+ } finally {
513
+ if (pooledCanvas && pooledCanvasRenderSettled) {
514
+ const canvasIndex = this.canvases.indexOf(pooledCanvas);
515
+ if (canvasIndex >= 0) this.canvases.splice(canvasIndex, 1);
516
+ CanvasPool.release(pooledCanvas);
282
517
  }
283
-
284
- page.cleanup();
285
- } catch (error) {
286
- console.error(`Failed to render page ${this.currentPreviewPage}:`, error);
287
518
  }
288
519
  }
289
520
 
@@ -294,15 +525,89 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
294
525
  this.canvases = [];
295
526
  }
296
527
 
297
- private cleanup(): void {
298
- this.clearCanvases();
528
+ private discardCanvases(): void {
529
+ for (const pooledCanvas of this.canvases) {
530
+ CanvasPool.discard(pooledCanvas);
531
+ }
532
+ this.canvases = [];
533
+ }
299
534
 
300
- if (this.pdfDocument) {
301
- PdfManager.releaseDocument(this.loadedPdfUrl ?? this.pdfUrl);
302
- this.pdfDocument = null;
535
+ private cleanup(): Promise<void> {
536
+ if (this.cleanupPromise) {
537
+ return this.cleanupPromise;
303
538
  }
539
+ const cleanupPromise = this.performCleanup();
540
+ this.cleanupPromise = cleanupPromise;
541
+ void cleanupPromise.finally(() => {
542
+ if (this.cleanupPromise === cleanupPromise) {
543
+ this.cleanupPromise = null;
544
+ }
545
+ }).catch(() => {});
546
+ return cleanupPromise;
547
+ }
304
548
 
549
+ private async performCleanup(): Promise<void> {
550
+ this.renderAdmissionOpen = false;
551
+ this.renderGeneration += 1;
552
+ this.renderQueueEpoch += 1;
553
+ this.resizeObserver?.disconnect();
554
+ this.resizeObserver = undefined;
555
+ const abortController = this.loadAbortController;
556
+ this.loadAbortController = null;
557
+ abortController?.abort();
558
+ const loadOperation = this.loadOperation;
559
+ const pdfLoadPromise = this.pdfLoadPromise;
560
+ const renderPagesTask = this.renderPagesTask;
561
+ const currentPageRenderTask = this.currentPageRenderTask;
562
+ const documentUsePipelines = [...this.documentUsePipelines];
305
563
  this.renderPagesQueued = false;
564
+ try {
565
+ currentPageRenderTask?.cancel();
566
+ } catch {
567
+ // The tracked render promise still participates in settlement below.
568
+ }
569
+
570
+ const pdfDocument = this.pdfDocument;
571
+ this.pdfDocument = null;
572
+ const releasePromise = pdfDocument
573
+ ? PdfManager.releaseDocument(pdfDocument).catch((error) => {
574
+ console.error('Failed to release PDF tile document:', error);
575
+ })
576
+ : null;
577
+
578
+ const pipelinesSettled = await settlePromisesWithin([
579
+ ...(pdfLoadPromise ? [pdfLoadPromise] : []),
580
+ ...documentUsePipelines,
581
+ ...(renderPagesTask ? [renderPagesTask] : []),
582
+ ...(currentPageRenderTask ? [currentPageRenderTask.promise] : []),
583
+ ...(releasePromise ? [releasePromise] : []),
584
+ ], pipelineSettlementGraceMs);
585
+ if (this.pdfLoadPromise === pdfLoadPromise) {
586
+ this.pdfLoadPromise = null;
587
+ }
588
+ if (this.loadOperation === loadOperation) {
589
+ this.loadOperation = null;
590
+ }
591
+ if (this.renderPagesTask === renderPagesTask) {
592
+ this.renderPagesTask = null;
593
+ }
594
+ if (this.currentPageRenderTask === currentPageRenderTask) {
595
+ this.currentPageRenderTask = null;
596
+ }
597
+ this.documentUsePipelines.clear();
598
+
599
+ if (pipelinesSettled) {
600
+ this.clearCanvases();
601
+ } else {
602
+ this.discardCanvases();
603
+ }
604
+ const previewCanvas = this.shadowRoot?.querySelector<HTMLCanvasElement>('.preview-canvas');
605
+ if (previewCanvas) {
606
+ previewCanvas.width = 0;
607
+ previewCanvas.height = 0;
608
+ previewCanvas.style.removeProperty('width');
609
+ previewCanvas.style.removeProperty('height');
610
+ }
306
611
  this.pageCount = 0;
307
612
  this.currentPreviewPage = 1;
308
613
  this.isHovering = false;
@@ -312,28 +617,22 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
312
617
  this.rendered = false;
313
618
  this.loading = false;
314
619
  this.error = false;
620
+ this.fileSize = 0;
315
621
  }
316
622
 
317
623
  public async updated(changedProperties: Map<PropertyKey, unknown>): Promise<void> {
318
624
  super.updated(changedProperties);
319
625
 
320
- if (changedProperties.has('pdfUrl') && this.pdfUrl) {
321
- const previousUrl = changedProperties.get('pdfUrl') as string | undefined;
322
- if (previousUrl) {
323
- PdfManager.releaseDocument(previousUrl);
324
- }
325
- this.cleanup();
326
- this.rendered = false;
327
- this.currentPreviewPage = 1;
328
-
329
- const rect = this.getBoundingClientRect();
330
- if (rect.top < window.innerHeight && rect.bottom > 0) {
331
- this.loadAndRenderPreview();
332
- }
626
+ if (changedProperties.has('pdfUrl') && this.isConnected) {
627
+ await this.schedulePdfLoad(this.connectionEpoch);
333
628
  }
334
629
 
335
- if (changedProperties.has('currentPreviewPage') && this.rendered) {
336
- await this.scheduleRenderPages();
630
+ if (
631
+ changedProperties.has('currentPreviewPage')
632
+ && this.renderAdmissionOpen
633
+ && this.rendered
634
+ ) {
635
+ this.requestRenderPages();
337
636
  }
338
637
  }
339
638
 
@@ -409,7 +708,7 @@ export class DeesThumbnailPdf extends DeesThumbnailBase {
409
708
 
410
709
  this.resizeObserver = new ResizeObserver(() => {
411
710
  if (this.rendered && this.pdfDocument && !this.loading) {
412
- void this.scheduleRenderPages();
711
+ this.requestRenderPages();
413
712
  }
414
713
  });
415
714