@design.estate/dees-catalog 6.12.1 → 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 (43) hide show
  1. package/.smartconfig.json +5 -1
  2. package/dist_bundle/bundle.js +24088 -1233
  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-harness/dees-harness-chat/dees-harness-chat.d.ts +4 -1
  10. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +29 -2
  11. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.d.ts +4 -1
  12. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.js +65 -8
  13. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +1 -0
  14. package/dist_ts_web/elements/00group-media/dees-pdf-shared/CanvasPool.d.ts +1 -0
  15. package/dist_ts_web/elements/00group-media/dees-pdf-shared/CanvasPool.js +15 -1
  16. package/dist_ts_web/elements/00group-media/dees-pdf-shared/PdfManager.d.ts +6 -4
  17. package/dist_ts_web/elements/00group-media/dees-pdf-shared/PdfManager.js +183 -21
  18. package/dist_ts_web/elements/00group-media/dees-pdf-shared/utils.d.ts +3 -0
  19. package/dist_ts_web/elements/00group-media/dees-pdf-shared/utils.js +67 -1
  20. package/dist_ts_web/elements/00group-media/dees-pdf-viewer/component.d.ts +25 -1
  21. package/dist_ts_web/elements/00group-media/dees-pdf-viewer/component.js +693 -285
  22. package/dist_ts_web/elements/00group-media/dees-pdf-viewer/demo.js +7 -7
  23. package/dist_ts_web/elements/00group-media/dees-thumbnail-pdf/component.d.ts +20 -1
  24. package/dist_ts_web/elements/00group-media/dees-thumbnail-pdf/component.js +379 -116
  25. package/dist_ts_web/elements/00group-media/dees-thumbnail-pdf/demo.js +2 -2
  26. package/dist_ts_web/elements/00group-media/dees-thumbnail-shared/DeesThumbnailBase.d.ts +1 -0
  27. package/dist_ts_web/elements/00group-media/dees-thumbnail-shared/DeesThumbnailBase.js +7 -1
  28. package/package.json +5 -4
  29. package/readme.hints.md +26 -19
  30. package/readme.md +97 -53
  31. package/third-party-notices.md +190 -0
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +13 -0
  34. package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts +51 -6
  35. package/ts_web/elements/00group-harness/interfaces.ts +2 -0
  36. package/ts_web/elements/00group-media/dees-pdf-shared/CanvasPool.ts +15 -1
  37. package/ts_web/elements/00group-media/dees-pdf-shared/PdfManager.ts +214 -24
  38. package/ts_web/elements/00group-media/dees-pdf-shared/utils.ts +77 -1
  39. package/ts_web/elements/00group-media/dees-pdf-viewer/component.ts +774 -327
  40. package/ts_web/elements/00group-media/dees-pdf-viewer/demo.ts +7 -7
  41. package/ts_web/elements/00group-media/dees-thumbnail-pdf/component.ts +427 -128
  42. package/ts_web/elements/00group-media/dees-thumbnail-pdf/demo.ts +1 -1
  43. package/ts_web/elements/00group-media/dees-thumbnail-shared/DeesThumbnailBase.ts +5 -0
@@ -1,5 +1,16 @@
1
1
  import { DeesElement, property, html, customElement, type TemplateResult, directives } from '@design.estate/dees-element';
2
+ import {
3
+ TextLayer,
4
+ type PDFDocumentProxy,
5
+ type PDFPageProxy,
6
+ type RenderTask,
7
+ } from 'pdfjs-dist';
2
8
  import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
9
+ import {
10
+ raceWithAbort,
11
+ settlePromisesWithin,
12
+ waitForAnimationFrame,
13
+ } from '../dees-pdf-shared/utils.js';
3
14
  import { viewerStyles } from './styles.js';
4
15
  import { demo as demoFunc } from './demo.js';
5
16
  import '../../00group-utility/dees-icon/dees-icon.js';
@@ -13,6 +24,24 @@ declare global {
13
24
 
14
25
  type RenderState = 'idle' | 'loading' | 'rendering-main' | 'rendering-thumbs' | 'rendered' | 'error' | 'disposed';
15
26
 
27
+ interface ILoadOperation {
28
+ key: string;
29
+ promise: Promise<void>;
30
+ }
31
+
32
+ interface IRenderRecoveryState {
33
+ currentPage: number;
34
+ currentZoom: number;
35
+ viewportMode: 'auto' | 'page-fit' | 'page-width' | 'custom';
36
+ }
37
+
38
+ const pipelineSettlementGraceMs = 2_500;
39
+
40
+ interface ITextLayerRenderTask {
41
+ textLayer: TextLayer;
42
+ promise: Promise<unknown>;
43
+ }
44
+
16
45
  @customElement('dees-pdf-viewer')
17
46
  export class DeesPdfViewer extends DeesElement {
18
47
  public static demo = demoFunc;
@@ -61,19 +90,38 @@ export class DeesPdfViewer extends DeesElement {
61
90
  @property({ type: Number })
62
91
  accessor pdfFileSize: number = 0;
63
92
 
64
- private pdfDocument: any;
93
+ private pdfDocument: PDFDocumentProxy | null = null;
94
+ private pdfLoadPromise: Promise<PDFDocumentProxy> | null = null;
95
+ private loadOperation: ILoadOperation | null = null;
96
+ private loadGeneration = 0;
97
+ private connectionEpoch = 0;
98
+ private cleanupPromise: Promise<void> | null = null;
99
+ private renderAdmissionOpen = false;
100
+ private renderGeneration = 0;
65
101
  private renderState: RenderState = 'idle';
66
102
  private renderAbortController: AbortController | null = null;
67
103
  private pageRendering: boolean = false;
68
104
  private pageNumPending: number | null = null;
69
105
  private currentRenderTask: any = null;
70
106
  private currentRenderPromise: Promise<void> | null = null;
71
- private thumbnailRenderTasks: any[] = [];
72
- private pageRenderTasks: Map<number, any> = new Map();
73
- private textLayerRenderTasks: Map<number, any> = new Map();
107
+ private thumbnailRenderTasks = new Set<RenderTask>();
108
+ private thumbnailRenderPromise: Promise<void> | null = null;
109
+ private thumbnailRenderGeneration = 0;
110
+ private thumbnailRenderQueued = false;
111
+ private thumbnailRenderQueueEpoch = 0;
112
+ private pageRenderTasks = new Map<number, RenderTask>();
113
+ private pageRenderPipelines = new Set<Promise<void>>();
114
+ private documentUsePipelines = new Set<Promise<void>>();
115
+ private rerenderPromise: Promise<void> | null = null;
116
+ private rerenderQueued = false;
117
+ private rerenderQueueEpoch = 0;
118
+ private rerenderRecoveryTimeout?: number;
119
+ private textLayerRenderTasks = new Map<number, ITextLayerRenderTask>();
120
+ private textSelectionCleanupFunctions = new Map<number, () => void>();
74
121
  private canvas: HTMLCanvasElement | undefined;
75
122
  private ctx: CanvasRenderingContext2D | undefined;
76
123
  private viewerMain: HTMLElement | null = null;
124
+ private wheelEventTarget: HTMLElement | null = null;
77
125
  private resizeObserver?: ResizeObserver;
78
126
  private intersectionObserver?: IntersectionObserver;
79
127
  private scrollThrottleTimeout?: number;
@@ -268,35 +316,28 @@ export class DeesPdfViewer extends DeesElement {
268
316
  }
269
317
 
270
318
  public async connectedCallback() {
319
+ const connectionEpoch = ++this.connectionEpoch;
271
320
  await super.connectedCallback();
272
321
  await this.updateComplete;
322
+ if (!this.isConnected || connectionEpoch !== this.connectionEpoch) return;
273
323
  this.ensureViewerRefs();
274
-
275
- // Generate a unique document ID for this connection
276
- if (this.pdfUrl) {
277
- this.documentId = `${this.pdfUrl}-${Date.now()}-${Math.random()}`;
278
- await this.loadPdf();
324
+ const operationKey = `${connectionEpoch}:${this.pdfUrl}`;
325
+ if (this.loadOperation?.key === operationKey) {
326
+ await this.loadOperation.promise;
327
+ } else if (connectionEpoch > 1 || !this.pdfDocument) {
328
+ await this.schedulePdfLoad(connectionEpoch);
279
329
  }
280
330
  }
281
331
 
282
332
  public async disconnectedCallback() {
333
+ this.connectionEpoch += 1;
334
+ this.loadGeneration += 1;
335
+ this.renderAdmissionOpen = false;
336
+ this.renderAbortController?.abort();
337
+ const cleanupPromise = this.cleanupDocument();
283
338
  await super.disconnectedCallback();
284
- this.resizeObserver?.disconnect();
285
- this.resizeObserver = undefined;
286
- this.intersectionObserver?.disconnect();
287
- this.intersectionObserver = undefined;
288
-
289
- // Clear scroll timeout
290
- if (this.scrollThrottleTimeout) {
291
- clearTimeout(this.scrollThrottleTimeout);
292
- this.scrollThrottleTimeout = undefined;
293
- }
294
-
295
- // Mark as disposed and clean up
339
+ await cleanupPromise;
296
340
  this.renderState = 'disposed';
297
- await this.cleanupDocument();
298
-
299
- // Clear all references
300
341
  this.canvas = undefined;
301
342
  this.ctx = undefined;
302
343
  }
@@ -304,23 +345,52 @@ export class DeesPdfViewer extends DeesElement {
304
345
  public async updated(changedProperties: Map<PropertyKey, unknown>) {
305
346
  super.updated(changedProperties);
306
347
 
307
- if (changedProperties.has('pdfUrl') && this.pdfUrl) {
308
- const previousUrl = changedProperties.get('pdfUrl') as string | undefined;
309
- if (previousUrl) {
310
- PdfManager.releaseDocument(previousUrl);
348
+ if (changedProperties.has('pdfUrl') && this.isConnected) {
349
+ await this.schedulePdfLoad(this.connectionEpoch);
350
+ }
351
+
352
+ if (changedProperties.has('showSidebar') && !this.showSidebar) {
353
+ this.thumbnailRenderGeneration += 1;
354
+ this.thumbnailRenderQueued = false;
355
+ for (const task of this.thumbnailRenderTasks) {
356
+ try {
357
+ task.cancel();
358
+ } catch {
359
+ // The scheduler still owns each task promise until it settles.
360
+ }
311
361
  }
312
- // Generate new document ID for new URL
313
- this.documentId = `${this.pdfUrl}-${Date.now()}-${Math.random()}`;
314
- await this.loadPdf();
315
362
  }
316
363
 
317
364
  // Re-render thumbnails when sidebar becomes visible and document is loaded
318
- if (changedProperties.has('showSidebar') && this.showSidebar && this.pdfDocument) {
319
- // Use requestAnimationFrame to ensure DOM is ready
320
- await new Promise(resolve => requestAnimationFrame(resolve));
365
+ if (
366
+ changedProperties.has('showSidebar')
367
+ && this.showSidebar
368
+ && this.renderAdmissionOpen
369
+ && this.pdfDocument
370
+ ) {
371
+ const connectionEpoch = this.connectionEpoch;
372
+ const loadGeneration = this.loadGeneration;
373
+ const pdfDocument = this.pdfDocument;
374
+ await waitForAnimationFrame();
375
+ if (
376
+ !this.isConnected
377
+ || connectionEpoch !== this.connectionEpoch
378
+ || loadGeneration !== this.loadGeneration
379
+ || pdfDocument !== this.pdfDocument
380
+ || !this.renderAdmissionOpen
381
+ || !this.showSidebar
382
+ ) return;
321
383
  // Force re-render of thumbnails by resetting their rendered state
322
384
  this.thumbnailData.forEach(thumb => thumb.rendered = false);
323
385
  await this.renderThumbnails();
386
+ if (
387
+ !this.isConnected
388
+ || connectionEpoch !== this.connectionEpoch
389
+ || loadGeneration !== this.loadGeneration
390
+ || pdfDocument !== this.pdfDocument
391
+ || !this.renderAdmissionOpen
392
+ || !this.showSidebar
393
+ ) return;
324
394
  // Re-setup intersection observer for lazy loading of pages
325
395
  this.setupIntersectionObserver();
326
396
  // Scroll to active thumbnail after rendering
@@ -334,82 +404,159 @@ export class DeesPdfViewer extends DeesElement {
334
404
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
335
405
  }
336
406
 
337
- private async loadPdf() {
338
- this.loading = true;
339
- this.renderState = 'loading';
340
-
341
- try {
342
- await this.cleanupDocument();
407
+ private schedulePdfLoad(
408
+ connectionEpochArg: number,
409
+ renderRecoveryStateArg?: IRenderRecoveryState,
410
+ ): Promise<void> {
411
+ const url = this.pdfUrl;
412
+ const baseKey = `${connectionEpochArg}:${url}`;
413
+ if (!renderRecoveryStateArg && this.loadOperation?.key === baseKey) {
414
+ return this.loadOperation.promise;
415
+ }
416
+ const generation = ++this.loadGeneration;
417
+ const key = renderRecoveryStateArg
418
+ ? `${baseKey}:render-recovery:${generation}`
419
+ : baseKey;
420
+ const operation: ILoadOperation = {
421
+ key,
422
+ promise: Promise.resolve(),
423
+ };
424
+ operation.promise = this.performPdfLoad(
425
+ generation,
426
+ connectionEpochArg,
427
+ url,
428
+ renderRecoveryStateArg,
429
+ )
430
+ .finally(() => {
431
+ if (this.loadOperation === operation) {
432
+ this.loadOperation = null;
433
+ }
434
+ });
435
+ this.loadOperation = operation;
436
+ return operation.promise;
437
+ }
343
438
 
344
- // Create new abort controller for this load operation
345
- this.renderAbortController = new AbortController();
346
- const signal = this.renderAbortController.signal;
439
+ private isCurrentLoad(generationArg: number, connectionEpochArg: number, urlArg: string): boolean {
440
+ return this.isConnected
441
+ && generationArg === this.loadGeneration
442
+ && connectionEpochArg === this.connectionEpoch
443
+ && urlArg === this.pdfUrl;
444
+ }
347
445
 
348
- this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
349
- if (signal.aborted) return;
446
+ private async performPdfLoad(
447
+ generationArg: number,
448
+ connectionEpochArg: number,
449
+ urlArg: string,
450
+ renderRecoveryStateArg?: IRenderRecoveryState,
451
+ ): Promise<void> {
452
+ await this.cleanupDocument();
453
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) {
454
+ return;
455
+ }
456
+ if (!urlArg) {
457
+ this.loading = false;
458
+ this.renderState = 'idle';
459
+ return;
460
+ }
350
461
 
351
- this.totalPages = this.pdfDocument.numPages;
352
- this.currentPage = this.initialPage;
353
- this.resolveInitialViewportMode();
462
+ this.loading = true;
463
+ this.renderState = 'loading';
464
+ const abortController = new AbortController();
465
+ this.renderAbortController = abortController;
466
+ const { signal } = abortController;
354
467
 
355
- // Get file size
468
+ try {
469
+ const pdfLoadPromise = PdfManager.loadDocument(urlArg, signal);
470
+ this.pdfLoadPromise = pdfLoadPromise;
471
+ let pdfDocument: PDFDocumentProxy;
356
472
  try {
357
- const data = await this.pdfDocument.getData();
358
- this.pdfFileSize = data.length;
359
- } catch (e) {
360
- this.pdfFileSize = 0;
473
+ pdfDocument = await pdfLoadPromise;
474
+ } finally {
475
+ if (this.pdfLoadPromise === pdfLoadPromise) {
476
+ this.pdfLoadPromise = null;
477
+ }
361
478
  }
362
-
363
- // Initialize thumbnail and page data arrays
364
- this.thumbnailData = Array.from({length: this.totalPages}, (_, i) => ({
365
- page: i + 1,
366
- rendered: false
367
- }));
368
-
369
- this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
370
- page: i + 1,
371
- rendered: false,
372
- rendering: false,
373
- textLayerRendered: false,
374
- }));
375
-
376
- // Set loading to false to render the pages
377
- this.loading = false;
378
- await this.updateComplete;
379
- this.ensureViewerRefs();
380
- this.setupIntersectionObserver();
381
-
382
- // Wait for next frame to ensure DOM is ready
383
- await new Promise(resolve => requestAnimationFrame(resolve));
384
- if (signal.aborted) return;
385
-
386
- this.renderState = 'rendering-main';
387
-
388
- // Render initial visible pages
389
- await this.renderVisiblePages();
390
- if (signal.aborted) return;
391
-
392
- // Scroll to initial page
393
- if (this.initialPage > 1) {
394
- await this.scrollToPage(this.initialPage, false);
479
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) || signal.aborted) {
480
+ await PdfManager.releaseDocument(pdfDocument);
481
+ return;
395
482
  }
483
+ this.pdfDocument = pdfDocument;
484
+ this.renderAdmissionOpen = true;
485
+ this.documentId = `${urlArg}-${generationArg}`;
486
+ const documentUsePipeline = (async () => {
487
+ this.totalPages = pdfDocument.numPages;
488
+ if (renderRecoveryStateArg) {
489
+ this.currentPage = Math.min(
490
+ Math.max(renderRecoveryStateArg.currentPage, 1),
491
+ this.totalPages,
492
+ );
493
+ this.currentZoom = renderRecoveryStateArg.currentZoom;
494
+ this.viewportMode = renderRecoveryStateArg.viewportMode;
495
+ } else {
496
+ this.currentPage = this.initialPage;
497
+ this.resolveInitialViewportMode();
498
+ }
396
499
 
397
- if (this.showSidebar) {
398
- // Ensure sidebar is in DOM after loading = false
500
+ try {
501
+ const data = await raceWithAbort(pdfDocument.getData(), signal);
502
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
503
+ this.pdfFileSize = data.length;
504
+ } catch {
505
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
506
+ this.pdfFileSize = 0;
507
+ }
508
+
509
+ this.thumbnailData = Array.from({length: this.totalPages}, (_, i) => ({
510
+ page: i + 1,
511
+ rendered: false
512
+ }));
513
+ this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
514
+ page: i + 1,
515
+ rendered: false,
516
+ rendering: false,
517
+ textLayerRendered: false,
518
+ }));
519
+
520
+ this.loading = false;
399
521
  await this.updateComplete;
400
- // Wait for next frame to ensure DOM is fully ready
401
- await new Promise(resolve => requestAnimationFrame(resolve));
402
- if (signal.aborted) return;
522
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) return;
523
+ this.ensureViewerRefs();
524
+ this.setupIntersectionObserver();
525
+
526
+ await waitForAnimationFrame(signal);
527
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) || signal.aborted) return;
528
+ this.renderState = 'rendering-main';
529
+
530
+ await this.renderVisiblePages();
531
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) || signal.aborted) return;
532
+ if (this.currentPage > 1) {
533
+ await this.scrollToPage(this.currentPage, false);
534
+ }
403
535
 
404
- await this.renderThumbnails();
405
- if (signal.aborted) return;
406
- }
536
+ if (this.showSidebar) {
537
+ await this.updateComplete;
538
+ await waitForAnimationFrame(signal);
539
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) || signal.aborted) return;
540
+ await this.renderThumbnails();
541
+ if (!this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) || signal.aborted) return;
542
+ }
407
543
 
408
- this.renderState = 'rendered';
544
+ if (this.isCurrentLoad(generationArg, connectionEpochArg, urlArg)) {
545
+ this.renderState = 'rendered';
546
+ }
547
+ })();
548
+ this.documentUsePipelines.add(documentUsePipeline);
549
+ try {
550
+ await documentUsePipeline;
551
+ } finally {
552
+ this.documentUsePipelines.delete(documentUsePipeline);
553
+ }
409
554
  } catch (error) {
410
- console.error('Error loading PDF:', error);
411
- this.loading = false;
412
- this.renderState = 'error';
555
+ if (this.isCurrentLoad(generationArg, connectionEpochArg, urlArg) && !signal.aborted) {
556
+ console.error('Error loading PDF:', error);
557
+ this.loading = false;
558
+ this.renderState = 'error';
559
+ }
413
560
  }
414
561
  }
415
562
 
@@ -468,101 +615,169 @@ export class DeesPdfViewer extends DeesElement {
468
615
  }
469
616
  }
470
617
 
471
- private async renderPageIfNeeded(pageNum: number) {
618
+ private renderPageIfNeeded(pageNum: number): Promise<void> {
472
619
  const pageInfo = this.pageData.find(p => p.page === pageNum);
473
- if (!pageInfo || pageInfo.rendered || pageInfo.rendering) return;
474
-
620
+ const pdfDocument = this.pdfDocument;
621
+ const signal = this.renderAbortController?.signal;
622
+ if (
623
+ !this.renderAdmissionOpen
624
+ || !pdfDocument
625
+ || !signal
626
+ || !pageInfo
627
+ || pageInfo.rendered
628
+ || pageInfo.rendering
629
+ ) return Promise.resolve();
630
+
631
+ const renderGeneration = this.renderGeneration;
632
+ const isCurrentRender = (): boolean => (
633
+ this.isConnected
634
+ && this.renderAdmissionOpen
635
+ && !signal.aborted
636
+ && renderGeneration === this.renderGeneration
637
+ && pdfDocument === this.pdfDocument
638
+ );
475
639
  pageInfo.rendering = true;
640
+ const renderPipeline = (async () => {
641
+ let page: PDFPageProxy | undefined;
642
+ let renderTask: RenderTask | undefined;
643
+ try {
644
+ const canvas = this.shadowRoot?.querySelector(
645
+ `.page-canvas[data-page="${pageNum}"]`,
646
+ ) as HTMLCanvasElement;
647
+ if (!canvas) {
648
+ pageInfo.rendering = false;
649
+ return;
650
+ }
476
651
 
477
- try {
478
- const canvas = this.shadowRoot?.querySelector(`.page-canvas[data-page="${pageNum}"]`) as HTMLCanvasElement;
479
- if (!canvas) {
480
- pageInfo.rendering = false;
481
- return;
482
- }
483
-
484
- const page = await this.pdfDocument.getPage(pageNum);
485
- const viewport = this.computeViewport(page);
486
-
487
- // Set canvas dimensions with device pixel ratio for sharp rendering
488
- const dpr = window.devicePixelRatio || 1;
489
- canvas.width = Math.floor(viewport.width * dpr);
490
- canvas.height = Math.floor(viewport.height * dpr);
491
- canvas.style.width = `${viewport.width}px`;
492
- canvas.style.height = `${viewport.height}px`;
493
-
494
- const ctx = canvas.getContext('2d');
495
- if (!ctx) {
496
- page.cleanup?.();
497
- pageInfo.rendering = false;
498
- return;
499
- }
500
-
501
- // Scale context for high-DPI displays
502
- ctx.scale(dpr, dpr);
652
+ page = await raceWithAbort(pdfDocument.getPage(pageNum), signal);
653
+ if (!isCurrentRender()) return;
654
+ const viewport = this.computeViewport(page);
503
655
 
504
- const renderContext = {
505
- canvasContext: ctx,
506
- viewport: viewport,
507
- };
656
+ // Set canvas dimensions with device pixel ratio for sharp rendering
657
+ const dpr = window.devicePixelRatio || 1;
658
+ canvas.width = Math.floor(viewport.width * dpr);
659
+ canvas.height = Math.floor(viewport.height * dpr);
660
+ canvas.style.width = `${viewport.width}px`;
661
+ canvas.style.height = `${viewport.height}px`;
508
662
 
509
- const renderTask = page.render(renderContext);
510
- this.pageRenderTasks.set(pageNum, renderTask);
663
+ const ctx = canvas.getContext('2d');
664
+ if (!ctx) {
665
+ pageInfo.rendering = false;
666
+ return;
667
+ }
511
668
 
512
- await renderTask.promise;
669
+ // Scale context for high-DPI displays
670
+ ctx.scale(dpr, dpr);
513
671
 
514
- page.cleanup?.();
515
- pageInfo.rendered = true;
516
- pageInfo.rendering = false;
517
- this.pageRenderTasks.delete(pageNum);
672
+ const renderContext = {
673
+ canvasContext: ctx,
674
+ viewport: viewport,
675
+ };
518
676
 
519
- // Render text layer for selection
520
- await this.renderTextLayer(pageNum);
677
+ renderTask = page.render(renderContext);
678
+ this.pageRenderTasks.set(pageNum, renderTask);
679
+ await raceWithAbort(renderTask.promise, signal);
680
+ if (!isCurrentRender()) return;
681
+ pageInfo.rendered = true;
682
+ pageInfo.rendering = false;
683
+ if (this.pageRenderTasks.get(pageNum) === renderTask) {
684
+ this.pageRenderTasks.delete(pageNum);
685
+ }
521
686
 
522
- // Update page data to reflect rendered state
523
- this.requestUpdate('pageData');
524
- } catch (error: any) {
525
- if (error?.name !== 'RenderingCancelledException') {
526
- console.error(`Error rendering page ${pageNum}:`, error);
687
+ // Render text layer for selection
688
+ await this.renderTextLayer(pageNum, renderGeneration, pdfDocument, signal);
689
+ if (!isCurrentRender()) return;
690
+
691
+ // Update page data to reflect rendered state
692
+ this.requestUpdate('pageData');
693
+ } catch (error: unknown) {
694
+ if (
695
+ !(error instanceof Error)
696
+ || !['AbortError', 'AbortException', 'RenderingCancelledException'].includes(error.name)
697
+ ) {
698
+ console.error(`Error rendering page ${pageNum}:`, error);
699
+ }
700
+ if (renderGeneration === this.renderGeneration) {
701
+ pageInfo.rendering = false;
702
+ }
703
+ } finally {
704
+ if (renderTask && this.pageRenderTasks.get(pageNum) === renderTask) {
705
+ this.pageRenderTasks.delete(pageNum);
706
+ }
707
+ page?.cleanup();
527
708
  }
528
- pageInfo.rendering = false;
529
- this.pageRenderTasks.delete(pageNum);
530
- }
709
+ })();
710
+ this.pageRenderPipelines.add(renderPipeline);
711
+ void renderPipeline.finally(() => {
712
+ this.pageRenderPipelines.delete(renderPipeline);
713
+ }).catch(() => {});
714
+ return renderPipeline;
531
715
  }
532
716
 
533
- private async renderTextLayer(pageNum: number): Promise<void> {
717
+ private async renderTextLayer(
718
+ pageNum: number,
719
+ renderGenerationArg: number,
720
+ pdfDocumentArg: PDFDocumentProxy,
721
+ signalArg: AbortSignal,
722
+ ): Promise<void> {
534
723
  const pageInfo = this.pageData.find(p => p.page === pageNum);
535
- if (!pageInfo || pageInfo.textLayerRendered) return;
536
-
724
+ if (
725
+ renderGenerationArg !== this.renderGeneration
726
+ || signalArg.aborted
727
+ || pdfDocumentArg !== this.pdfDocument
728
+ || !pageInfo
729
+ || pageInfo.textLayerRendered
730
+ ) return;
731
+
732
+ let page: PDFPageProxy | undefined;
733
+ let renderTask: ITextLayerRenderTask | undefined;
537
734
  try {
538
735
  const textLayerDiv = this.shadowRoot?.querySelector(
539
736
  `.text-layer[data-page="${pageNum}"]`
540
737
  ) as HTMLElement;
541
738
  if (!textLayerDiv) return;
542
739
 
740
+ this.textSelectionCleanupFunctions.get(pageNum)?.();
741
+ this.textSelectionCleanupFunctions.delete(pageNum);
543
742
  textLayerDiv.innerHTML = '';
544
743
 
545
- const page = await this.pdfDocument.getPage(pageNum);
546
- const textContent = await page.getTextContent();
744
+ page = await raceWithAbort(pdfDocumentArg.getPage(pageNum), signalArg);
745
+ if (
746
+ renderGenerationArg !== this.renderGeneration
747
+ || signalArg.aborted
748
+ || pdfDocumentArg !== this.pdfDocument
749
+ ) return;
750
+ const textContent = await raceWithAbort(page.getTextContent(), signalArg);
751
+ if (
752
+ renderGenerationArg !== this.renderGeneration
753
+ || signalArg.aborted
754
+ || pdfDocumentArg !== this.pdfDocument
755
+ ) return;
547
756
  const viewport = this.computeViewport(page);
548
757
 
549
- // @ts-ignore - Dynamic import of pdfjs
550
- const pdfjs = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm');
551
-
552
758
  textLayerDiv.style.width = `${viewport.width}px`;
553
759
  textLayerDiv.style.height = `${viewport.height}px`;
554
760
 
555
761
  // Set the scale factor CSS variable - required by PDF.js text layer
556
762
  textLayerDiv.style.setProperty('--scale-factor', String(viewport.scale));
557
763
 
558
- const textLayerRenderTask = pdfjs.renderTextLayer({
764
+ const textLayer = new TextLayer({
559
765
  textContentSource: textContent,
560
766
  container: textLayerDiv,
561
767
  viewport: viewport,
562
768
  });
563
-
564
- this.textLayerRenderTasks.set(pageNum, textLayerRenderTask);
565
- await textLayerRenderTask.promise;
769
+ const textLayerRenderPromise = textLayer.render();
770
+ renderTask = {
771
+ textLayer,
772
+ promise: textLayerRenderPromise,
773
+ };
774
+ this.textLayerRenderTasks.set(pageNum, renderTask);
775
+ await raceWithAbort(textLayerRenderPromise, signalArg);
776
+ if (
777
+ renderGenerationArg !== this.renderGeneration
778
+ || signalArg.aborted
779
+ || pdfDocumentArg !== this.pdfDocument
780
+ ) return;
566
781
 
567
782
  // Add endOfContent for selection boundary
568
783
  const endOfContent = document.createElement('div');
@@ -622,7 +837,7 @@ export class DeesPdfViewer extends DeesElement {
622
837
  }
623
838
  };
624
839
 
625
- textLayerDiv.addEventListener('mousedown', (e: MouseEvent) => {
840
+ const handleMouseDown = (e: MouseEvent) => {
626
841
  if (e.button !== 0) return;
627
842
 
628
843
  const pos = getTextPositionFromPoint(e.clientX, e.clientY);
@@ -643,16 +858,27 @@ export class DeesPdfViewer extends DeesElement {
643
858
  document.addEventListener('mousemove', handleMouseMove);
644
859
  document.addEventListener('mouseup', handleMouseUp);
645
860
  }
861
+ };
862
+ textLayerDiv.addEventListener('mousedown', handleMouseDown);
863
+ this.textSelectionCleanupFunctions.set(pageNum, () => {
864
+ textLayerDiv.removeEventListener('mousedown', handleMouseDown);
865
+ document.removeEventListener('mouseup', handleMouseUp);
866
+ document.removeEventListener('mousemove', handleMouseMove);
646
867
  });
647
868
 
648
869
  pageInfo.textLayerRendered = true;
649
- page.cleanup?.();
650
- this.textLayerRenderTasks.delete(pageNum);
651
- } catch (error: any) {
652
- if (error?.name !== 'RenderingCancelledException') {
870
+ } catch (error: unknown) {
871
+ if (
872
+ !(error instanceof Error)
873
+ || !['AbortError', 'AbortException', 'RenderingCancelledException'].includes(error.name)
874
+ ) {
653
875
  console.error(`Error rendering text layer for page ${pageNum}:`, error);
654
876
  }
655
- this.textLayerRenderTasks.delete(pageNum);
877
+ } finally {
878
+ if (renderTask && this.textLayerRenderTasks.get(pageNum) === renderTask) {
879
+ this.textLayerRenderTasks.delete(pageNum);
880
+ }
881
+ page?.cleanup();
656
882
  }
657
883
  }
658
884
 
@@ -762,23 +988,67 @@ export class DeesPdfViewer extends DeesElement {
762
988
  }
763
989
  }
764
990
 
765
- private async renderThumbnails() {
766
- // Check if document is loaded
767
- if (!this.pdfDocument) {
768
- return;
991
+ private renderThumbnails(): Promise<void> {
992
+ if (!this.renderAdmissionOpen || !this.showSidebar || !this.pdfDocument) {
993
+ return Promise.resolve();
769
994
  }
995
+ if (this.thumbnailRenderPromise) {
996
+ this.thumbnailRenderGeneration += 1;
997
+ this.thumbnailRenderQueued = true;
998
+ for (const task of this.thumbnailRenderTasks) {
999
+ try {
1000
+ task.cancel();
1001
+ } catch {
1002
+ // The active queue iteration still awaits the task promise.
1003
+ }
1004
+ }
1005
+ return this.thumbnailRenderPromise;
1006
+ }
1007
+ this.thumbnailRenderGeneration += 1;
1008
+ const thumbnailRenderQueueEpoch = ++this.thumbnailRenderQueueEpoch;
1009
+ const renderPromise = (async () => {
1010
+ do {
1011
+ if (thumbnailRenderQueueEpoch !== this.thumbnailRenderQueueEpoch) return;
1012
+ this.thumbnailRenderQueued = false;
1013
+ const renderGeneration = this.thumbnailRenderGeneration;
1014
+ await this.performRenderThumbnails(renderGeneration);
1015
+ } while (
1016
+ thumbnailRenderQueueEpoch === this.thumbnailRenderQueueEpoch
1017
+ && this.thumbnailRenderQueued
1018
+ && this.renderAdmissionOpen
1019
+ && this.showSidebar
1020
+ && this.pdfDocument
1021
+ );
1022
+ })();
1023
+ this.thumbnailRenderPromise = renderPromise;
1024
+ void renderPromise.finally(() => {
1025
+ if (this.thumbnailRenderPromise === renderPromise) {
1026
+ this.thumbnailRenderPromise = null;
1027
+ }
1028
+ }).catch(() => {});
1029
+ return renderPromise;
1030
+ }
770
1031
 
771
- // Check if already rendered
772
- if (this.thumbnailData.length > 0 && this.thumbnailData.every(t => t.rendered)) {
1032
+ private async performRenderThumbnails(renderGenerationArg: number): Promise<void> {
1033
+ const pdfDocument = this.pdfDocument;
1034
+ if (!this.renderAdmissionOpen || !this.showSidebar || !pdfDocument) {
773
1035
  return;
774
1036
  }
1037
+ const isCurrentRender = (): boolean => (
1038
+ this.isConnected
1039
+ && this.renderAdmissionOpen
1040
+ && this.showSidebar
1041
+ && renderGenerationArg === this.thumbnailRenderGeneration
1042
+ && pdfDocument === this.pdfDocument
1043
+ );
775
1044
 
776
- // Check abort signal
777
- if (this.renderAbortController?.signal.aborted) {
1045
+ // Check if already rendered
1046
+ if (this.thumbnailData.length > 0 && this.thumbnailData.every(t => t.rendered)) {
778
1047
  return;
779
1048
  }
780
1049
 
781
1050
  const signal = this.renderAbortController?.signal;
1051
+ if (!signal || signal.aborted) return;
782
1052
  this.renderState = 'rendering-thumbs';
783
1053
 
784
1054
  // Cancel any existing thumbnail render tasks
@@ -789,13 +1059,14 @@ export class DeesPdfViewer extends DeesElement {
789
1059
  // Ignore cancellation errors
790
1060
  }
791
1061
  }
792
- this.thumbnailRenderTasks = [];
793
1062
 
794
1063
  try {
795
1064
  await this.updateComplete;
1065
+ if (!isCurrentRender()) return;
796
1066
  const thumbnails = this.shadowRoot?.querySelectorAll('.thumbnail') as NodeListOf<HTMLElement>;
797
1067
  const thumbnailCanvases = this.shadowRoot?.querySelectorAll('.thumbnail-canvas') as NodeListOf<HTMLCanvasElement>;
798
1068
  const sidebarContent = this.shadowRoot?.querySelector('.sidebar-content') as HTMLElement;
1069
+ if (!sidebarContent) return;
799
1070
 
800
1071
  // Get the actual available width for thumbnails (sidebar width minus padding)
801
1072
  const sidebarStyles = window.getComputedStyle(sidebarContent);
@@ -811,68 +1082,62 @@ export class DeesPdfViewer extends DeesElement {
811
1082
  }
812
1083
 
813
1084
  for (let i = 0; i < thumbnailCanvases.length; i++) {
814
- if (signal?.aborted) return;
1085
+ if (signal?.aborted || !isCurrentRender()) return;
815
1086
 
816
1087
  const canvas = thumbnailCanvases[i];
817
1088
  const thumbnail = thumbnails[i];
818
1089
  const pageNum = parseInt(canvas.dataset.page || '1');
819
- const page = await this.pdfDocument.getPage(pageNum);
820
-
821
- // Get the page's natural dimensions
822
- const initialViewport = page.getViewport({ scale: 1 });
823
-
824
- // Calculate scale to fit within the max thumbnail width
825
- const scale = maxThumbnailWidth / initialViewport.width;
826
- const viewport = page.getViewport({ scale });
827
-
828
- // Set canvas dimensions with device pixel ratio for sharp thumbnails
829
- const dpr = window.devicePixelRatio || 1;
830
- canvas.width = Math.floor(viewport.width * dpr);
831
- canvas.height = Math.floor(viewport.height * dpr);
832
-
833
- // Set the display size via style to ensure proper display
834
- canvas.style.width = `${viewport.width}px`;
835
- canvas.style.height = `${viewport.height}px`;
836
-
837
- // Set the actual thumbnail container height
838
- thumbnail.style.height = `${viewport.height}px`;
839
- thumbnail.style.minHeight = `${viewport.height}px`;
840
-
841
- const context = canvas.getContext('2d');
842
- if (!context) {
843
- page.cleanup?.();
844
- continue;
845
- }
846
-
847
- // Scale context for high-DPI displays
848
- context.scale(dpr, dpr);
849
-
850
- const renderContext = {
851
- canvasContext: context,
852
- viewport: viewport,
853
- };
854
-
855
- const renderTask = page.render(renderContext);
856
- this.thumbnailRenderTasks.push(renderTask);
857
- await renderTask.promise;
858
- page.cleanup?.();
1090
+ const page = await raceWithAbort(pdfDocument.getPage(pageNum), signal);
1091
+ try {
1092
+ if (!isCurrentRender()) return;
1093
+ const initialViewport = page.getViewport({ scale: 1 });
1094
+ const scale = maxThumbnailWidth / initialViewport.width;
1095
+ const viewport = page.getViewport({ scale });
1096
+ const dpr = window.devicePixelRatio || 1;
1097
+ canvas.width = Math.floor(viewport.width * dpr);
1098
+ canvas.height = Math.floor(viewport.height * dpr);
1099
+ canvas.style.width = `${viewport.width}px`;
1100
+ canvas.style.height = `${viewport.height}px`;
1101
+ thumbnail.style.height = `${viewport.height}px`;
1102
+ thumbnail.style.minHeight = `${viewport.height}px`;
1103
+
1104
+ const context = canvas.getContext('2d');
1105
+ if (!context) continue;
1106
+ context.scale(dpr, dpr);
1107
+
1108
+ const renderTask = page.render({
1109
+ canvasContext: context,
1110
+ viewport,
1111
+ });
1112
+ this.thumbnailRenderTasks.add(renderTask);
1113
+ try {
1114
+ await raceWithAbort(renderTask.promise, signal);
1115
+ } finally {
1116
+ this.thumbnailRenderTasks.delete(renderTask);
1117
+ }
1118
+ if (!isCurrentRender()) return;
859
1119
 
860
- // Mark this thumbnail as rendered
861
- const thumbData = this.thumbnailData.find(t => t.page === pageNum);
862
- if (thumbData) {
863
- thumbData.rendered = true;
1120
+ const thumbData = this.thumbnailData.find(t => t.page === pageNum);
1121
+ if (thumbData) {
1122
+ thumbData.rendered = true;
1123
+ }
1124
+ } finally {
1125
+ page.cleanup();
864
1126
  }
865
1127
  }
866
1128
 
867
1129
  // Trigger update to reflect rendered state
868
- this.requestUpdate('thumbnailData');
869
- } catch (error: any) {
1130
+ if (isCurrentRender()) {
1131
+ this.requestUpdate('thumbnailData');
1132
+ }
1133
+ } catch (error: unknown) {
870
1134
  // Only log non-cancellation errors
871
- if (error?.name !== 'RenderingCancelledException') {
1135
+ if (
1136
+ !(error instanceof Error)
1137
+ || !['AbortError', 'AbortException', 'RenderingCancelledException'].includes(error.name)
1138
+ ) {
872
1139
  console.error('Error rendering thumbnails:', error);
873
1140
  }
874
- } finally {
875
- this.thumbnailRenderTasks = [];
876
1141
  }
877
1142
  }
878
1143
 
@@ -936,6 +1201,7 @@ export class DeesPdfViewer extends DeesElement {
936
1201
  }
937
1202
 
938
1203
  private reRenderAllPages() {
1204
+ this.renderGeneration += 1;
939
1205
  // Clear all rendered pages to force re-render with new zoom
940
1206
  this.pageData.forEach(page => {
941
1207
  page.rendered = false;
@@ -956,51 +1222,140 @@ export class DeesPdfViewer extends DeesElement {
956
1222
  // Cancel text layer render tasks
957
1223
  this.textLayerRenderTasks.forEach(task => {
958
1224
  try {
959
- task.cancel?.();
1225
+ task.textLayer.cancel();
960
1226
  } catch (error) {
961
1227
  // Ignore cancellation errors
962
1228
  }
963
1229
  });
964
1230
  this.textLayerRenderTasks.clear();
1231
+ for (const cleanup of this.textSelectionCleanupFunctions.values()) {
1232
+ cleanup();
1233
+ }
1234
+ this.textSelectionCleanupFunctions.clear();
965
1235
 
966
1236
  // Request update to re-render pages
967
1237
  this.requestUpdate();
968
1238
 
969
- // Render visible pages after update
970
- this.updateComplete.then(() => {
971
- this.renderVisiblePages();
972
- });
1239
+ this.rerenderQueued = true;
1240
+ if (this.rerenderPromise) return;
1241
+
1242
+ const rerenderQueueEpoch = ++this.rerenderQueueEpoch;
1243
+ const rerenderPromise = (async () => {
1244
+ while (
1245
+ rerenderQueueEpoch === this.rerenderQueueEpoch
1246
+ && this.rerenderQueued
1247
+ && this.renderAdmissionOpen
1248
+ && this.isConnected
1249
+ ) {
1250
+ this.rerenderQueued = false;
1251
+ const renderGeneration = this.renderGeneration;
1252
+ const priorRenderPipelines = [...this.pageRenderPipelines];
1253
+ const pipelinesSettled = await settlePromisesWithin(
1254
+ priorRenderPipelines,
1255
+ pipelineSettlementGraceMs,
1256
+ );
1257
+ if (!pipelinesSettled) {
1258
+ if (!this.rerenderRecoveryTimeout) {
1259
+ const connectionEpoch = this.connectionEpoch;
1260
+ const loadGeneration = this.loadGeneration;
1261
+ const url = this.pdfUrl;
1262
+ this.rerenderRecoveryTimeout = window.setTimeout(() => {
1263
+ this.rerenderRecoveryTimeout = undefined;
1264
+ if (
1265
+ rerenderQueueEpoch !== this.rerenderQueueEpoch
1266
+ || connectionEpoch !== this.connectionEpoch
1267
+ || loadGeneration !== this.loadGeneration
1268
+ || url !== this.pdfUrl
1269
+ || !this.renderAdmissionOpen
1270
+ || !this.isConnected
1271
+ ) return;
1272
+ void this.schedulePdfLoad(connectionEpoch, {
1273
+ currentPage: this.currentPage,
1274
+ currentZoom: this.currentZoom,
1275
+ viewportMode: this.viewportMode,
1276
+ });
1277
+ }, 0);
1278
+ }
1279
+ return;
1280
+ }
1281
+ await this.updateComplete;
1282
+ if (
1283
+ rerenderQueueEpoch === this.rerenderQueueEpoch
1284
+ && renderGeneration === this.renderGeneration
1285
+ && !this.rerenderQueued
1286
+ && this.renderAdmissionOpen
1287
+ && this.isConnected
1288
+ ) {
1289
+ await this.renderVisiblePages();
1290
+ }
1291
+ }
1292
+ })();
1293
+ this.rerenderPromise = rerenderPromise;
1294
+ void rerenderPromise.finally(() => {
1295
+ if (this.rerenderPromise === rerenderPromise) {
1296
+ this.rerenderPromise = null;
1297
+ }
1298
+ }).catch(() => {});
973
1299
  }
974
1300
 
975
1301
  private async downloadPdf() {
976
- if (!this.pdfDocument) return;
1302
+ const pdfDocument = this.pdfDocument;
1303
+ const signal = this.renderAbortController?.signal;
1304
+ if (!pdfDocument || !signal) return;
1305
+ const generation = this.loadGeneration;
1306
+ const connectionEpoch = this.connectionEpoch;
1307
+ const url = this.pdfUrl;
977
1308
 
1309
+ let ownedBlobUrl: string | undefined;
978
1310
  try {
979
1311
  // Get raw PDF data from the loaded document
980
- const data = await this.pdfDocument.getData();
981
- const blob = new Blob([data.buffer], { type: 'application/pdf' });
982
- const blobUrl = URL.createObjectURL(blob);
1312
+ const data = await raceWithAbort(pdfDocument.getData(), signal);
1313
+ if (
1314
+ !this.isCurrentLoad(generation, connectionEpoch, url)
1315
+ || signal.aborted
1316
+ || pdfDocument !== this.pdfDocument
1317
+ ) return;
1318
+ const blob = new Blob([new Uint8Array(data)], { type: 'application/pdf' });
1319
+ ownedBlobUrl = URL.createObjectURL(blob);
983
1320
 
984
1321
  const link = document.createElement('a');
985
- link.href = blobUrl;
986
- link.download = this.pdfUrl ? this.pdfUrl.split('/').pop() || 'document.pdf' : 'document.pdf';
1322
+ link.href = ownedBlobUrl;
1323
+ link.download = url ? url.split('/').pop() || 'document.pdf' : 'document.pdf';
987
1324
  link.click();
988
1325
 
989
1326
  // Clean up blob URL after short delay
990
- setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1327
+ const scheduledBlobUrl = ownedBlobUrl;
1328
+ setTimeout(() => URL.revokeObjectURL(scheduledBlobUrl), 1000);
1329
+ ownedBlobUrl = undefined;
991
1330
  } catch (error) {
992
- console.error('Error downloading PDF:', error);
1331
+ if (!(error instanceof Error) || error.name !== 'AbortError') {
1332
+ console.error('Error downloading PDF:', error);
1333
+ }
1334
+ } finally {
1335
+ if (ownedBlobUrl) URL.revokeObjectURL(ownedBlobUrl);
993
1336
  }
994
1337
  }
995
1338
 
996
1339
  private async printPdf() {
997
- if (!this.pdfDocument) return;
1340
+ const pdfDocument = this.pdfDocument;
1341
+ const signal = this.renderAbortController?.signal;
1342
+ if (!pdfDocument || !signal) return;
1343
+ const generation = this.loadGeneration;
1344
+ const connectionEpoch = this.connectionEpoch;
1345
+ const url = this.pdfUrl;
998
1346
 
1347
+ let ownedPdfUrl: string | undefined;
1348
+ let ownedHtmlUrl: string | undefined;
999
1349
  try {
1000
1350
  // Get raw PDF data from the loaded document
1001
- const data = await this.pdfDocument.getData();
1002
- const blob = new Blob([data.buffer], { type: 'application/pdf' });
1003
- const pdfUrl = URL.createObjectURL(blob);
1351
+ const data = await raceWithAbort(pdfDocument.getData(), signal);
1352
+ if (
1353
+ !this.isCurrentLoad(generation, connectionEpoch, url)
1354
+ || signal.aborted
1355
+ || pdfDocument !== this.pdfDocument
1356
+ ) return;
1357
+ const blob = new Blob([new Uint8Array(data)], { type: 'application/pdf' });
1358
+ ownedPdfUrl = URL.createObjectURL(blob);
1004
1359
 
1005
1360
  // Create an HTML wrapper page that embeds the PDF and handles print/close
1006
1361
  // This gives us control over the afterprint event (direct PDF URLs don't support it)
@@ -1019,7 +1374,7 @@ export class DeesPdfViewer extends DeesElement {
1019
1374
  </style>
1020
1375
  </head>
1021
1376
  <body>
1022
- <iframe src="${pdfUrl}" type="application/pdf"></iframe>
1377
+ <iframe src="${ownedPdfUrl}" type="application/pdf"></iframe>
1023
1378
  <script>
1024
1379
  window.onload = function() {
1025
1380
  setTimeout(function() {
@@ -1037,32 +1392,49 @@ export class DeesPdfViewer extends DeesElement {
1037
1392
  </html>
1038
1393
  `;
1039
1394
  const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
1040
- const htmlUrl = URL.createObjectURL(htmlBlob);
1395
+ ownedHtmlUrl = URL.createObjectURL(htmlBlob);
1041
1396
 
1042
- const printWindow = window.open(htmlUrl, '_blank', 'width=800,height=600');
1397
+ const printWindow = window.open(ownedHtmlUrl, '_blank', 'width=800,height=600');
1043
1398
  if (printWindow) {
1399
+ const scheduledPdfUrl = ownedPdfUrl;
1400
+ const scheduledHtmlUrl = ownedHtmlUrl;
1401
+ let urlsRevoked = false;
1402
+ const revokeUrls = (): void => {
1403
+ if (urlsRevoked) return;
1404
+ urlsRevoked = true;
1405
+ URL.revokeObjectURL(scheduledPdfUrl);
1406
+ URL.revokeObjectURL(scheduledHtmlUrl);
1407
+ };
1044
1408
  // Cleanup blob URLs when window closes
1045
1409
  const checkClosed = setInterval(() => {
1046
1410
  if (printWindow.closed) {
1047
1411
  clearInterval(checkClosed);
1048
- URL.revokeObjectURL(pdfUrl);
1049
- URL.revokeObjectURL(htmlUrl);
1412
+ revokeUrls();
1050
1413
  }
1051
1414
  }, 500);
1052
1415
  // Safety cleanup after 2 minutes
1053
1416
  setTimeout(() => {
1054
1417
  clearInterval(checkClosed);
1055
- URL.revokeObjectURL(pdfUrl);
1056
- URL.revokeObjectURL(htmlUrl);
1418
+ revokeUrls();
1057
1419
  }, 120000);
1420
+ ownedPdfUrl = undefined;
1421
+ ownedHtmlUrl = undefined;
1058
1422
  } else {
1059
1423
  // Popup blocked - fall back to direct navigation
1060
- window.open(pdfUrl, '_blank');
1061
- setTimeout(() => URL.revokeObjectURL(pdfUrl), 60000);
1062
- URL.revokeObjectURL(htmlUrl);
1424
+ window.open(ownedPdfUrl, '_blank');
1425
+ const scheduledPdfUrl = ownedPdfUrl;
1426
+ setTimeout(() => URL.revokeObjectURL(scheduledPdfUrl), 60000);
1427
+ ownedPdfUrl = undefined;
1428
+ URL.revokeObjectURL(ownedHtmlUrl);
1429
+ ownedHtmlUrl = undefined;
1063
1430
  }
1064
1431
  } catch (error) {
1065
- console.error('Error printing PDF:', error);
1432
+ if (!(error instanceof Error) || error.name !== 'AbortError') {
1433
+ console.error('Error printing PDF:', error);
1434
+ }
1435
+ } finally {
1436
+ if (ownedPdfUrl) URL.revokeObjectURL(ownedPdfUrl);
1437
+ if (ownedHtmlUrl) URL.revokeObjectURL(ownedHtmlUrl);
1066
1438
  }
1067
1439
  }
1068
1440
 
@@ -1114,6 +1486,28 @@ export class DeesPdfViewer extends DeesElement {
1114
1486
  return this.viewportMode !== 'custom' || this.currentZoom > this.MANUAL_MIN_ZOOM;
1115
1487
  }
1116
1488
 
1489
+ private readonly handleViewerWheel = (eventArg: WheelEvent): void => {
1490
+ const element = eventArg.currentTarget as HTMLElement;
1491
+ const scrollTop = element.scrollTop;
1492
+ const scrollHeight = element.scrollHeight;
1493
+ const clientHeight = element.clientHeight;
1494
+ const isAtTop = scrollTop === 0;
1495
+ const isAtBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 1;
1496
+
1497
+ if (
1498
+ (eventArg.deltaY < 0 && !isAtTop)
1499
+ || (eventArg.deltaY > 0 && !isAtBottom)
1500
+ ) {
1501
+ eventArg.stopPropagation();
1502
+ } else if (
1503
+ (eventArg.deltaY < 0 && isAtTop)
1504
+ || (eventArg.deltaY > 0 && isAtBottom)
1505
+ ) {
1506
+ eventArg.preventDefault();
1507
+ eventArg.stopPropagation();
1508
+ }
1509
+ };
1510
+
1117
1511
  private ensureViewerRefs() {
1118
1512
  if (!this.viewerMain) {
1119
1513
  this.viewerMain = this.shadowRoot?.querySelector('.viewer-main') as HTMLElement;
@@ -1128,28 +1522,11 @@ export class DeesPdfViewer extends DeesElement {
1128
1522
  });
1129
1523
  this.resizeObserver.observe(this.viewerMain);
1130
1524
  this.measureViewportDimensions();
1131
-
1132
- // Prevent scroll propagation to parent when scrolling inside viewer
1133
- this.viewerMain.addEventListener('wheel', (e) => {
1134
- const element = e.currentTarget as HTMLElement;
1135
- const scrollTop = element.scrollTop;
1136
- const scrollHeight = element.scrollHeight;
1137
- const clientHeight = element.clientHeight;
1138
- const deltaY = e.deltaY;
1139
-
1140
- // Check if we're at the boundaries
1141
- const isAtTop = scrollTop === 0;
1142
- const isAtBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 1;
1143
-
1144
- // Prevent propagation if we're scrolling within bounds
1145
- if ((deltaY < 0 && !isAtTop) || (deltaY > 0 && !isAtBottom)) {
1146
- e.stopPropagation();
1147
- } else if ((deltaY < 0 && isAtTop) || (deltaY > 0 && isAtBottom)) {
1148
- // Prevent default and propagation when at boundaries
1149
- e.preventDefault();
1150
- e.stopPropagation();
1151
- }
1152
- }, { passive: false });
1525
+ }
1526
+ if (this.viewerMain && this.wheelEventTarget !== this.viewerMain) {
1527
+ this.wheelEventTarget?.removeEventListener('wheel', this.handleViewerWheel);
1528
+ this.wheelEventTarget = this.viewerMain;
1529
+ this.wheelEventTarget.addEventListener('wheel', this.handleViewerWheel, { passive: false });
1153
1530
  }
1154
1531
  }
1155
1532
 
@@ -1232,82 +1609,152 @@ export class DeesPdfViewer extends DeesElement {
1232
1609
  return Math.min(Math.max(value, min), max);
1233
1610
  }
1234
1611
 
1235
- private async cleanupDocument() {
1236
- // Abort any ongoing render operations
1237
- if (this.renderAbortController) {
1238
- this.renderAbortController.abort();
1239
- this.renderAbortController = null;
1612
+ private cleanupDocument(): Promise<void> {
1613
+ if (this.cleanupPromise) {
1614
+ return this.cleanupPromise;
1240
1615
  }
1241
-
1242
- // Wait for any existing render to complete
1243
- if (this.currentRenderPromise) {
1244
- try {
1245
- await this.currentRenderPromise;
1246
- } catch (error) {
1247
- // Ignore errors
1616
+ const cleanupPromise = this.performDocumentCleanup();
1617
+ this.cleanupPromise = cleanupPromise;
1618
+ void cleanupPromise.finally(() => {
1619
+ if (this.cleanupPromise === cleanupPromise) {
1620
+ this.cleanupPromise = null;
1248
1621
  }
1249
- this.currentRenderPromise = null;
1622
+ }).catch(() => {});
1623
+ return cleanupPromise;
1624
+ }
1625
+
1626
+ private async performDocumentCleanup(): Promise<void> {
1627
+ this.renderAdmissionOpen = false;
1628
+ this.renderGeneration += 1;
1629
+ this.rerenderQueueEpoch += 1;
1630
+ this.thumbnailRenderGeneration += 1;
1631
+ this.thumbnailRenderQueueEpoch += 1;
1632
+ this.thumbnailRenderQueued = false;
1633
+ if (this.rerenderRecoveryTimeout) {
1634
+ clearTimeout(this.rerenderRecoveryTimeout);
1635
+ this.rerenderRecoveryTimeout = undefined;
1636
+ }
1637
+ this.intersectionObserver?.disconnect();
1638
+ this.intersectionObserver = undefined;
1639
+ this.resizeObserver?.disconnect();
1640
+ this.resizeObserver = undefined;
1641
+ this.wheelEventTarget?.removeEventListener('wheel', this.handleViewerWheel);
1642
+ this.wheelEventTarget = null;
1643
+ if (this.scrollThrottleTimeout) {
1644
+ clearTimeout(this.scrollThrottleTimeout);
1645
+ this.scrollThrottleTimeout = undefined;
1250
1646
  }
1251
1647
 
1252
- // Clear the render task reference
1253
- this.currentRenderTask = null;
1648
+ const abortController = this.renderAbortController;
1649
+ this.renderAbortController = null;
1650
+ abortController?.abort();
1651
+ const loadOperation = this.loadOperation;
1652
+ const pdfLoadPromise = this.pdfLoadPromise;
1653
+ const pageRenderTasks = [...this.pageRenderTasks.values()];
1654
+ const textLayerRenderTasks = [...this.textLayerRenderTasks.values()];
1655
+ const thumbnailRenderTasks = [...this.thumbnailRenderTasks];
1656
+ const pageRenderPipelines = [...this.pageRenderPipelines];
1657
+ const documentUsePipelines = [...this.documentUsePipelines];
1658
+ const rerenderPromise = this.rerenderPromise;
1659
+ this.rerenderQueued = false;
1660
+ const thumbnailRenderPromise = this.thumbnailRenderPromise;
1661
+ const currentRenderPromise = this.currentRenderPromise;
1662
+ for (const cleanup of this.textSelectionCleanupFunctions.values()) {
1663
+ cleanup();
1664
+ }
1665
+ this.textSelectionCleanupFunctions.clear();
1254
1666
 
1255
- // Cancel any page render tasks
1256
- this.pageRenderTasks.forEach(task => {
1667
+ for (const task of pageRenderTasks) {
1257
1668
  try {
1258
1669
  task.cancel();
1259
- } catch (error) {
1260
- // Ignore cancellation errors
1670
+ } catch {
1671
+ // The tracked task promise still participates in settlement below.
1261
1672
  }
1262
- });
1263
- this.pageRenderTasks.clear();
1264
-
1265
- // Cancel text layer render tasks
1266
- this.textLayerRenderTasks.forEach(task => {
1673
+ }
1674
+ for (const task of textLayerRenderTasks) {
1267
1675
  try {
1268
- task.cancel?.();
1269
- } catch (error) {
1270
- // Ignore cancellation errors
1676
+ task.textLayer.cancel();
1677
+ } catch {
1678
+ // The tracked task promise still participates in settlement below.
1271
1679
  }
1272
- });
1273
- this.textLayerRenderTasks.clear();
1274
-
1275
- // Cancel any thumbnail render tasks
1276
- for (const task of (this.thumbnailRenderTasks || [])) {
1680
+ }
1681
+ for (const task of thumbnailRenderTasks) {
1277
1682
  try {
1278
1683
  task.cancel();
1279
- } catch (error) {
1280
- // Ignore cancellation errors
1684
+ } catch {
1685
+ // The tracked task promise still participates in settlement below.
1281
1686
  }
1282
1687
  }
1283
- this.thumbnailRenderTasks = [];
1688
+ const renderCanvases = this.shadowRoot?.querySelectorAll<HTMLCanvasElement>(
1689
+ '.page-canvas, .thumbnail-canvas',
1690
+ );
1691
+ renderCanvases?.forEach((canvasArg) => {
1692
+ canvasArg.width = 0;
1693
+ canvasArg.height = 0;
1694
+ canvasArg.style.removeProperty('width');
1695
+ canvasArg.style.removeProperty('height');
1696
+ });
1697
+
1698
+ const pdfDocument = this.pdfDocument;
1699
+ this.pdfDocument = null;
1700
+ const releasePromise = pdfDocument
1701
+ ? PdfManager.releaseDocument(pdfDocument).catch((error) => {
1702
+ console.error('Error destroying PDF document:', error);
1703
+ })
1704
+ : null;
1705
+
1706
+ await settlePromisesWithin([
1707
+ ...(pdfLoadPromise ? [pdfLoadPromise] : []),
1708
+ ...(currentRenderPromise ? [currentRenderPromise] : []),
1709
+ ...documentUsePipelines,
1710
+ ...(rerenderPromise ? [rerenderPromise] : []),
1711
+ ...pageRenderPipelines,
1712
+ ...(thumbnailRenderPromise ? [thumbnailRenderPromise] : []),
1713
+ ...pageRenderTasks.map((task) => task.promise),
1714
+ ...textLayerRenderTasks.map((task) => task.promise),
1715
+ ...thumbnailRenderTasks.map((task) => task.promise),
1716
+ ...(releasePromise ? [releasePromise] : []),
1717
+ ], pipelineSettlementGraceMs);
1718
+ for (const cleanup of this.textSelectionCleanupFunctions.values()) {
1719
+ cleanup();
1720
+ }
1721
+ this.textSelectionCleanupFunctions.clear();
1284
1722
 
1285
- // Reset all state flags
1723
+ if (this.pdfLoadPromise === pdfLoadPromise) {
1724
+ this.pdfLoadPromise = null;
1725
+ }
1726
+ if (this.loadOperation === loadOperation) {
1727
+ this.loadOperation = null;
1728
+ }
1729
+ this.currentRenderPromise = null;
1730
+ this.currentRenderTask = null;
1731
+ this.pageRenderTasks.clear();
1732
+ this.pageRenderPipelines.clear();
1733
+ this.documentUsePipelines.clear();
1734
+ if (this.rerenderPromise === rerenderPromise) {
1735
+ this.rerenderPromise = null;
1736
+ }
1737
+ this.textLayerRenderTasks.clear();
1738
+ for (const task of thumbnailRenderTasks) {
1739
+ this.thumbnailRenderTasks.delete(task);
1740
+ }
1741
+ if (this.thumbnailRenderPromise === thumbnailRenderPromise) {
1742
+ this.thumbnailRenderPromise = null;
1743
+ }
1744
+
1745
+ this.loading = false;
1286
1746
  this.renderState = 'idle';
1287
1747
  this.pageRendering = false;
1288
1748
  this.pageNumPending = null;
1289
1749
  this.thumbnailData = [];
1290
1750
  this.pageData = [];
1291
1751
  this.documentId = '';
1292
-
1293
- // Clear canvas content
1752
+ this.totalPages = 1;
1753
+ this.currentPage = 1;
1754
+ this.pdfFileSize = 0;
1294
1755
  if (this.canvas && this.ctx) {
1295
1756
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1296
1757
  }
1297
-
1298
- // Destroy the document to free memory
1299
- if (this.pdfDocument) {
1300
- try {
1301
- this.pdfDocument.destroy();
1302
- } catch (error) {
1303
- console.error('Error destroying PDF document:', error);
1304
- }
1305
- }
1306
-
1307
- // Finally null the document reference
1308
- this.pdfDocument = null;
1309
-
1310
- // Request update to reflect state changes
1311
1758
  this.requestUpdate();
1312
1759
  }
1313
1760
  }