@checksub_team/peaks_timeline 2.2.1 → 2.3.0-alpha.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.
@@ -39,16 +39,28 @@ define([
39
39
  this._allowEditing = allowEditing;
40
40
  this._sourcesGroup = {};
41
41
  this._layer = new Konva.Layer();
42
+ // Separate layer for dragged elements - always renders on top of sources layer
43
+ this._dragLayer = new Konva.Layer();
42
44
  this._dataRetriever = new DataRetriever(peaks);
43
45
  this._lineGroups = new LineGroups(peaks, view, this);
44
46
  this._lineGroups.addToLayer(this);
45
47
 
46
48
  this._loadedData = {};
47
49
 
48
- this._debouncedRescale = new Invoker().debounce(
49
- this._rescale, 150
50
+ // Create invoker for performance optimizations
51
+ this._invoker = new Invoker();
52
+
53
+ // Version counter for cancellable rescale operations
54
+ this._rescaleVersion = 0;
55
+
56
+ // Throttled batch draw to prevent excessive redraws
57
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
58
+ this._layer.batchDraw.bind(this._layer)
50
59
  );
51
60
 
61
+ // Pending draw flag to coalesce multiple draw requests
62
+ this._drawPending = false;
63
+
52
64
  this._peaks.on('handler.sources.add', this._onSourcesAdd.bind(this));
53
65
  this._peaks.on('handler.sources.destroy', this._onSourcesDestroy.bind(this));
54
66
  this._peaks.on('handler.sources.show', this._onSourcesShow.bind(this));
@@ -59,8 +71,16 @@ define([
59
71
  this._peaks.on('handler.segments.show', this._onSegmentsShow.bind(this));
60
72
  this._peaks.on('model.source.setIndicators', this.setIndicators.bind(this));
61
73
  this._peaks.on('handler.view.mouseup', this._stopDrag.bind(this));
74
+ this._peaks.on('sources.delayedLineChange', this._onSourcesDelayedLineChanged.bind(this));
62
75
  }
63
76
 
77
+ SourcesLayer.prototype._onSourcesDelayedLineChanged = function() {
78
+ // Update dragged source groups when sources change line after a delay (e.g., after automatic line creation)
79
+ if (this._dragGhosts && this._draggedElements && this._draggedElements.length > 0) {
80
+ this._dragSourcesGroup();
81
+ }
82
+ };
83
+
64
84
  SourcesLayer.prototype._stopDrag = function() {
65
85
  const draggedSourceGroup = this._sourcesGroup[this._draggedElementId];
66
86
 
@@ -97,6 +117,7 @@ define([
97
117
 
98
118
  SourcesLayer.prototype.addToStage = function(stage) {
99
119
  stage.add(this._layer);
120
+ stage.add(this._dragLayer);
100
121
  };
101
122
 
102
123
  SourcesLayer.prototype.enableEditing = function(enable) {
@@ -190,7 +211,7 @@ define([
190
211
 
191
212
  this._view.updateTimelineLength();
192
213
 
193
- this._layer.batchDraw();
214
+ this.batchDraw();
194
215
  };
195
216
 
196
217
  SourcesLayer.prototype._onSourcesShow = function(sources) {
@@ -200,7 +221,7 @@ define([
200
221
  self._sourcesGroup[source.id].setWrapping(false, true);
201
222
  });
202
223
 
203
- this._layer.batchDraw();
224
+ this.batchDraw();
204
225
  };
205
226
 
206
227
  SourcesLayer.prototype._onSourcesHide = function(sources) {
@@ -210,7 +231,7 @@ define([
210
231
  self._sourcesGroup[source.id].setWrapping(true, true);
211
232
  });
212
233
 
213
- this._layer.batchDraw();
234
+ this.batchDraw();
214
235
  };
215
236
 
216
237
  SourcesLayer.prototype._onDataRetrieved = function(data, source, url) {
@@ -243,7 +264,7 @@ define([
243
264
  SourcesLayer.prototype._onSegmentsShow = function(segmentsGroupId, lineId) {
244
265
  this._lineGroups.addSegments(segmentsGroupId, lineId);
245
266
  this._view.updateTimelineLength();
246
- this._layer.batchDraw();
267
+ this.batchDraw();
247
268
  };
248
269
 
249
270
  /**
@@ -285,12 +306,13 @@ define([
285
306
 
286
307
  if (sourceGroup) {
287
308
  sourceGroup.createIndicators();
288
- this._layer.batchDraw();
309
+ this.batchDraw();
289
310
  }
290
311
  };
291
312
 
292
313
  /**
293
314
  * Updates the positions of all displayed sources in the view.
315
+ * Uses optimized batching to reduce draw calls.
294
316
  *
295
317
  * @param {Number} startTime The start of the visible range in the view,
296
318
  * in seconds.
@@ -307,12 +329,16 @@ define([
307
329
 
308
330
  var count = sources.length;
309
331
 
310
- sources.forEach(this._updateSource.bind(this));
332
+ // Batch update all sources
333
+ for (var i = 0; i < sources.length; i++) {
334
+ this._updateSource(sources[i]);
335
+ }
311
336
 
312
337
  count += this._removeInvisibleSources(startTime, endTime);
313
338
 
314
339
  if (count > 0) {
315
- this._layer.batchDraw();
340
+ // Use throttled batch draw for consistent performance
341
+ this.batchDraw();
316
342
  }
317
343
  };
318
344
 
@@ -348,23 +374,85 @@ define([
348
374
  orderable: true,
349
375
  draggable: true
350
376
  });
377
+
378
+ // Store initial source positions and create ghost previews
379
+ var self = this;
380
+
381
+ this._dragGhosts = [];
382
+ this._initialSourcePositions = {};
383
+
384
+ this._draggedElements.forEach(function(source) {
385
+ // Store initial position for ALL dragged sources (even those outside view)
386
+ // This is needed for time calculations during drag
387
+ self._initialSourcePositions[source.id] = {
388
+ startTime: source.startTime,
389
+ endTime: source.endTime,
390
+ lineId: source.lineId
391
+ };
392
+
393
+ var sourceGroup = self._sourcesGroup[source.id];
394
+
395
+ if (sourceGroup) {
396
+ // Mark as dragging (for all sources, not just the clicked one)
397
+ sourceGroup.setDragging(true);
398
+
399
+ // Get absolute Y position before moving (relative to line group)
400
+ var absoluteY = sourceGroup.getAbsoluteY();
401
+
402
+ // Move source to the drag layer so it draws above ALL other sources/segments
403
+ sourceGroup.moveTo(self._dragLayer);
404
+ // Restore the Y position (now relative to drag layer, which is at y=0)
405
+ sourceGroup.y(absoluteY);
406
+
407
+ // Create ghost preview
408
+ var ghost = self._createDragGhost(sourceGroup);
409
+
410
+ self._dragGhosts.push({
411
+ ghost: ghost,
412
+ sourceId: source.id
413
+ });
414
+ }
415
+ });
351
416
  };
352
417
 
353
418
  SourcesLayer.prototype.onSourcesGroupDragEnd = function() {
419
+ // Clean up ghost previews
420
+ if (this._dragGhosts) {
421
+ this._dragGhosts.forEach(function(item) {
422
+ if (item.ghost) {
423
+ item.ghost.destroy();
424
+ }
425
+ });
426
+ this._dragGhosts = null;
427
+ }
428
+ this._initialSourcePositions = null;
429
+ this._dragOffsetX = undefined;
430
+ this._dragOffsetY = undefined;
431
+
432
+ var self = this;
433
+
354
434
  const updatedSources = this._draggedElements.map(
355
435
  function(source) {
356
- const sourceGroup = this._sourcesGroup[source.id];
436
+ const sourceGroup = self._sourcesGroup[source.id];
357
437
 
358
438
  if (sourceGroup) {
439
+ // Clear dragging state before moving back to line group
440
+ sourceGroup.setDragging(false);
359
441
  sourceGroup.prepareDragEnd();
442
+ // Move source back to its line group (it was moved to layer during drag)
443
+ self._lineGroups.addSource(source, sourceGroup);
444
+ // Reset Y position to 0 relative to parent line group
445
+ // (source followed cursor during drag, now snap to line position)
446
+ sourceGroup.y(0);
360
447
  }
361
448
 
362
449
  return source;
363
- }.bind(this)
450
+ }
364
451
  );
365
452
 
366
453
  this._draggedElementId = null;
367
454
 
455
+ this.refresh();
368
456
  this._view.batchDrawSourcesLayer();
369
457
  this._view.updateTimelineLength();
370
458
 
@@ -372,8 +460,72 @@ define([
372
460
  };
373
461
 
374
462
  SourcesLayer.prototype.onSourcesGroupDrag = function(draggedElement) {
463
+ var pointerPos = this._view.getPointerPosition();
464
+
375
465
  this._view.updateWithAutoScroll(this._dragSourcesGroup.bind(this));
376
466
 
467
+ // Return position that follows the mouse cursor exactly
468
+ // The ghost preview shows where it will actually be placed
469
+ var clickedSourceGroup = this._sourcesGroup[this._draggedElementId];
470
+
471
+ if (clickedSourceGroup) {
472
+ var mouseX = pointerPos.x;
473
+ var mouseY = pointerPos.y;
474
+ var offsetX = this._dragOffsetX || 0;
475
+ var offsetY = this._dragOffsetY || 0;
476
+
477
+ // Calculate offset on first drag if not set
478
+ if (this._dragOffsetX === undefined) {
479
+ var currentPos = draggedElement.absolutePosition();
480
+
481
+ this._dragOffsetX = currentPos.x - mouseX;
482
+ this._dragOffsetY = currentPos.y - mouseY;
483
+ offsetX = this._dragOffsetX;
484
+ offsetY = this._dragOffsetY;
485
+ }
486
+
487
+ var clickedSourceX = mouseX + offsetX;
488
+ var clickedSourceY = mouseY + offsetY;
489
+
490
+ // Position all other dragged sources relative to the clicked source
491
+ // They should maintain their initial relative positions
492
+ if (this._draggedElements && this._draggedElements.length > 1 && this._initialSourcePositions) {
493
+ var self = this;
494
+ var clickedInitialPos = this._initialSourcePositions[this._draggedElementId];
495
+
496
+ if (clickedInitialPos) {
497
+ var clickedInitialPixelX = this._view.timeToPixels(clickedInitialPos.startTime);
498
+
499
+ this._draggedElements.forEach(function(source) {
500
+ if (source.id !== self._draggedElementId) {
501
+ var sourceGroup = self._sourcesGroup[source.id];
502
+
503
+ if (sourceGroup) {
504
+ var initialPos = self._initialSourcePositions[source.id];
505
+
506
+ if (initialPos) {
507
+ // Calculate pixel offset from clicked source's initial position
508
+ var initialPixelX = self._view.timeToPixels(initialPos.startTime);
509
+ var pixelOffset = initialPixelX - clickedInitialPixelX;
510
+
511
+ // Position this source relative to clicked source's current position
512
+ sourceGroup.absolutePosition({
513
+ x: clickedSourceX + pixelOffset,
514
+ y: clickedSourceY
515
+ });
516
+ }
517
+ }
518
+ }
519
+ });
520
+ }
521
+ }
522
+
523
+ return {
524
+ x: clickedSourceX,
525
+ y: clickedSourceY
526
+ };
527
+ }
528
+
377
529
  return {
378
530
  x: draggedElement.absolutePosition().x,
379
531
  y: draggedElement.absolutePosition().y
@@ -399,17 +551,23 @@ define([
399
551
  return;
400
552
  }
401
553
 
554
+ var newStartTime = Utils.roundTime(initialStartTime + timeOffsetDiff + timeDiff);
555
+ var newEndTime = Utils.roundTime(initialEndTime + timeOffsetDiff + timeDiff);
556
+
402
557
  const shouldRedraw = this.manageSourceMovements(
403
558
  this._draggedElements,
404
- initialStartTime + timeOffsetDiff + timeDiff,
405
- initialEndTime + timeOffsetDiff + timeDiff,
559
+ newStartTime,
560
+ newEndTime,
406
561
  orderable,
407
562
  mousePosX,
408
563
  mousePosY
409
564
  );
410
565
 
566
+ this._updateDragGhosts();
567
+
411
568
  if (shouldRedraw) {
412
569
  this.batchDraw();
570
+ this._dragLayer.batchDraw();
413
571
  }
414
572
  };
415
573
 
@@ -424,8 +582,112 @@ define([
424
582
  );
425
583
  };
426
584
 
427
- SourcesLayer.prototype._applyTimeChangesToSources = function(sources, initialStartTime, newStartTime,
428
- newEndTime
585
+ /**
586
+ * Creates a ghost preview element for a source being dragged.
587
+ * The ghost shows where the source will be placed when released.
588
+ *
589
+ * @private
590
+ * @param {SourceGroup} sourceGroup The source group to create a ghost for
591
+ * @returns {Konva.Rect} The ghost preview element
592
+ */
593
+ SourcesLayer.prototype._createDragGhost = function(sourceGroup) {
594
+ var source = sourceGroup.getSource();
595
+ var frameOffset = this._view.getFrameOffset();
596
+ var x = this._view.timeToPixels(source.startTime) - frameOffset;
597
+ var width = this._view.timeToPixels(source.endTime - source.startTime);
598
+ var height = sourceGroup.getCurrentHeight();
599
+ var y = sourceGroup.getAbsoluteY();
600
+
601
+ var ghost = new Konva.Rect({
602
+ x: x,
603
+ y: y,
604
+ width: width,
605
+ height: height,
606
+ fill: source.backgroundColor,
607
+ opacity: 0.4,
608
+ stroke: source.selectedBorderColor,
609
+ strokeWidth: 2,
610
+ dash: [8, 4],
611
+ cornerRadius: 8,
612
+ listening: false
613
+ });
614
+
615
+ this._layer.add(ghost);
616
+ ghost.moveToBottom();
617
+
618
+ return ghost;
619
+ };
620
+
621
+ /**
622
+ * Updates the positions of all drag ghost previews.
623
+ *
624
+ * @private
625
+ */
626
+ SourcesLayer.prototype._updateDragGhosts = function() {
627
+ if (!this._dragGhosts) {
628
+ return;
629
+ }
630
+
631
+ var self = this;
632
+ var frameOffset = this._view.getFrameOffset();
633
+ var lineGroupsById = this._lineGroups.getLineGroupsById();
634
+
635
+ this._dragGhosts.forEach(function(item) {
636
+ var sourceGroup = self._sourcesGroup[item.sourceId];
637
+
638
+ if (!sourceGroup || !item.ghost) {
639
+ return;
640
+ }
641
+
642
+ // Use current source times (updated during drag)
643
+ var sourceData = sourceGroup.getSource();
644
+ var x = self._view.timeToPixels(sourceData.startTime) - frameOffset;
645
+
646
+ item.ghost.x(x);
647
+
648
+ // Update Y position based on line
649
+ var lineGroup = lineGroupsById[sourceData.lineId];
650
+
651
+ if (lineGroup) {
652
+ item.ghost.y(lineGroup.y());
653
+ }
654
+ });
655
+ };
656
+
657
+ /**
658
+ * Updates source times during drag using initial positions.
659
+ *
660
+ * @private
661
+ * @param {Number} newStartTime The new start time for the first source
662
+ */
663
+ SourcesLayer.prototype._updateSourceTimesDuringDrag = function(newStartTime) {
664
+ if (!this._initialSourcePositions || !this._draggedElements) {
665
+ return;
666
+ }
667
+
668
+ var self = this;
669
+ var firstSourceInitial = this._initialSourcePositions[this._draggedElements[0].id];
670
+
671
+ if (!firstSourceInitial) {
672
+ return;
673
+ }
674
+
675
+ // Calculate time diff from INITIAL position, not current position
676
+ var timeDiff = Utils.roundTime(newStartTime - firstSourceInitial.startTime);
677
+
678
+ this._draggedElements.forEach(function(source) {
679
+ var initialPos = self._initialSourcePositions[source.id];
680
+
681
+ if (initialPos) {
682
+ source.updateTimes(
683
+ Utils.roundTime(initialPos.startTime + timeDiff),
684
+ Utils.roundTime(initialPos.endTime + timeDiff)
685
+ );
686
+ }
687
+ });
688
+ };
689
+
690
+ SourcesLayer.prototype._applyTimeChangesToSources = function(sources, initialStartTime, newStartTime, newEndTime
429
691
  ) {
430
692
  if (sources.length === 1) {
431
693
  sources[0].updateTimes(
@@ -434,18 +696,49 @@ define([
434
696
  );
435
697
  }
436
698
  else {
437
- // We cannot have more than 1 source being resized at a time
438
- // Reaching this point implies that that we are only dragging, not resizing
439
- // So, we can safely assume that the difference between the initial times and the new times is the same
440
- const timeDiff = Utils.roundTime(newStartTime - initialStartTime);
441
-
442
- if (timeDiff !== 0) {
443
- sources.forEach(function(source) {
444
- source.updateTimes(
445
- Utils.roundTime(source.startTime + timeDiff),
446
- Utils.roundTime(source.endTime + timeDiff)
447
- );
448
- });
699
+ // When moving multiple sources via block-drag, compute time diff from the
700
+ // drag-start positions to avoid cumulative drift as times are updated.
701
+ var canUseInitialPositions = Boolean(this._initialSourcePositions
702
+ && this._initialSourcePositions[sources[0].id]);
703
+
704
+ if (canUseInitialPositions) {
705
+ for (var i = 0; i < sources.length; i++) {
706
+ if (!this._initialSourcePositions[sources[i].id]) {
707
+ canUseInitialPositions = false;
708
+ break;
709
+ }
710
+ }
711
+ }
712
+
713
+ if (canUseInitialPositions) {
714
+ var firstInitial = this._initialSourcePositions[sources[0].id];
715
+ var timeDiffFromInitial = Utils.roundTime(newStartTime - firstInitial.startTime);
716
+
717
+ if (timeDiffFromInitial !== 0) {
718
+ var self = this;
719
+
720
+ sources.forEach(function(source) {
721
+ var initialPos = self._initialSourcePositions[source.id];
722
+
723
+ source.updateTimes(
724
+ Utils.roundTime(initialPos.startTime + timeDiffFromInitial),
725
+ Utils.roundTime(initialPos.endTime + timeDiffFromInitial)
726
+ );
727
+ });
728
+ }
729
+ }
730
+ else {
731
+ // Fallback for non-drag multi-updates.
732
+ const timeDiff = Utils.roundTime(newStartTime - initialStartTime);
733
+
734
+ if (timeDiff !== 0) {
735
+ sources.forEach(function(source) {
736
+ source.updateTimes(
737
+ Utils.roundTime(source.startTime + timeDiff),
738
+ Utils.roundTime(source.endTime + timeDiff)
739
+ );
740
+ });
741
+ }
449
742
  }
450
743
  }
451
744
 
@@ -501,9 +794,39 @@ define([
501
794
 
502
795
  SourcesLayer.prototype._findOrAddSourceGroup = function(source) {
503
796
  var sourceGroup = this._sourcesGroup[source.id];
797
+ var isNewlyCreated = false;
504
798
 
505
799
  if (!sourceGroup) {
506
800
  sourceGroup = this._addSourceGroup(source);
801
+ isNewlyCreated = true;
802
+ }
803
+
804
+ // If this source is being dragged and was just recreated (came back into view),
805
+ // set it up properly for dragging
806
+ if (isNewlyCreated && this._draggedElements && this._initialSourcePositions) {
807
+ var isDraggedSource = this._draggedElements.some(function(s) {
808
+ return s.id === source.id;
809
+ });
810
+
811
+ if (isDraggedSource) {
812
+ // Mark as dragging
813
+ sourceGroup.setDragging(true);
814
+
815
+ // Get Y position from line group before moving
816
+ var absoluteY = sourceGroup.getAbsoluteY();
817
+
818
+ // Move to drag layer
819
+ sourceGroup.moveTo(this._dragLayer);
820
+ sourceGroup.y(absoluteY);
821
+
822
+ // Create ghost preview for this source
823
+ var ghost = this._createDragGhost(sourceGroup);
824
+
825
+ this._dragGhosts.push({
826
+ ghost: ghost,
827
+ sourceId: source.id
828
+ });
829
+ }
507
830
  }
508
831
 
509
832
  return sourceGroup;
@@ -625,8 +948,12 @@ define([
625
948
  this._layer.setVisible(visible);
626
949
  };
627
950
 
951
+ /**
952
+ * Schedules a batch draw using RAF throttling for better performance.
953
+ * Multiple calls within the same frame are coalesced into one draw.
954
+ */
628
955
  SourcesLayer.prototype.batchDraw = function() {
629
- this._layer.batchDraw();
956
+ this._throttledBatchDraw();
630
957
  };
631
958
 
632
959
  SourcesLayer.prototype.listening = function(bool) {
@@ -654,21 +981,36 @@ define([
654
981
  return endsLater || startsEarlier;
655
982
  };
656
983
 
657
- SourcesLayer.prototype.rescale = function(debounce) {
658
- if (debounce) {
659
- this._debouncedRescale();
660
- }
661
- else {
662
- this._rescale();
663
- }
984
+ SourcesLayer.prototype.rescale = function() {
985
+ // Increment the rescale version to cancel any in-progress rescale
986
+ this._rescaleVersion = (this._rescaleVersion || 0) + 1;
987
+ this._rescale(this._rescaleVersion);
664
988
  };
665
989
 
666
- SourcesLayer.prototype._rescale = function() {
667
- var id, audioPreviews, urls = [], self = this;
990
+ SourcesLayer.prototype._rescale = function(version) {
991
+ var self = this;
992
+ var ids = Object.keys(this._sourcesGroup);
993
+ var urls = [];
994
+ var index = 0;
995
+
996
+ function processNext() {
997
+ // Check if this rescale was cancelled (a newer one started)
998
+ if (self._rescaleVersion !== version) {
999
+ return;
1000
+ }
1001
+
1002
+ if (index >= ids.length) {
1003
+ // Done processing all sources
1004
+ self.batchDraw();
1005
+ return;
1006
+ }
1007
+
1008
+ var id = ids[index];
1009
+ var sourceGroup = self._sourcesGroup[id];
668
1010
 
669
- for (id in this._sourcesGroup) {
670
- if (Utils.objectHasProperty(this._sourcesGroup, id)) {
671
- audioPreviews = this._sourcesGroup[id].getAudioPreview();
1011
+ // Skip if source group was removed during async processing
1012
+ if (sourceGroup) {
1013
+ var audioPreviews = sourceGroup.getAudioPreview();
672
1014
 
673
1015
  audioPreviews.forEach(function(audioPreview) {
674
1016
  if (self._shouldResampleAudio(audioPreview.url, urls)) {
@@ -681,8 +1023,14 @@ define([
681
1023
  }
682
1024
  });
683
1025
  }
1026
+
1027
+ index++;
1028
+
1029
+ // Yield to allow cancellation and UI updates
1030
+ Utils.scheduleIdle(processNext, { timeout: 16 });
684
1031
  }
685
- this._layer.batchDraw();
1032
+
1033
+ processNext();
686
1034
  };
687
1035
 
688
1036
  SourcesLayer.prototype._shouldResampleAudio = function(audioUrl, urls) {
@@ -708,6 +1056,14 @@ define([
708
1056
  this._peaks.off('handler.segments.show', this._onSegmentsShow);
709
1057
  this._peaks.off('model.source.setIndicators', this.setIndicators);
710
1058
  this._peaks.off('handler.view.mouseup', this._stopDrag);
1059
+
1060
+ // Cancel any in-progress rescale
1061
+ this._rescaleVersion++;
1062
+
1063
+ // Clean up invoker resources
1064
+ if (this._invoker) {
1065
+ this._invoker.destroy();
1066
+ }
711
1067
  };
712
1068
 
713
1069
  SourcesLayer.prototype.getHeight = function() {
@@ -16,13 +16,8 @@ define([
16
16
 
17
17
  var isXhr2 = ('withCredentials' in new XMLHttpRequest());
18
18
 
19
- // Schedule heavy work during idle time to avoid UI stutter
20
- function scheduleIdle(fn) {
21
- if (typeof window !== 'undefined' && window.requestIdleCallback) {
22
- return window.requestIdleCallback(fn, { timeout: 80 });
23
- }
24
- return setTimeout(fn, 0);
25
- }
19
+ // Waveform data cache for better performance on repeated requests
20
+ var waveformCache = Utils.createLRUCache(50);
26
21
 
27
22
  /**
28
23
  * Creates and returns a WaveformData object, either by requesting the
@@ -39,6 +34,14 @@ define([
39
34
  this._peaks = peaks;
40
35
  }
41
36
 
37
+ /**
38
+ * Clears the waveform data cache.
39
+ * Useful when memory needs to be freed.
40
+ */
41
+ WaveformBuilder.clearCache = function() {
42
+ waveformCache.clear();
43
+ };
44
+
42
45
  /**
43
46
  * Options for requesting remote waveform data.
44
47
  *
@@ -192,6 +195,18 @@ define([
192
195
  return;
193
196
  }
194
197
 
198
+ // Check cache first for better performance
199
+ var cacheKey = url + ':' + requestType;
200
+ var cachedData = waveformCache.get(cacheKey);
201
+
202
+ if (cachedData) {
203
+ // Return cached waveform data asynchronously to maintain consistent API
204
+ Utils.scheduleIdle(function() {
205
+ callback(null, cachedData);
206
+ });
207
+ return;
208
+ }
209
+
195
210
  var xhr = self._createXHR(url, requestType, options.withCredentials, function(event) {
196
211
  if (this.readyState !== 4) {
197
212
  return;
@@ -205,7 +220,7 @@ define([
205
220
  return;
206
221
  }
207
222
 
208
- scheduleIdle(function() {
223
+ Utils.scheduleIdle(function() {
209
224
  try {
210
225
  var waveformData = WaveformData.create(event.target.response);
211
226
 
@@ -214,6 +229,9 @@ define([
214
229
  return;
215
230
  }
216
231
 
232
+ // Cache the waveform data for future use
233
+ waveformCache.set(cacheKey, waveformData);
234
+
217
235
  callback(null, waveformData);
218
236
  }
219
237
  catch (err) {
@@ -270,7 +288,7 @@ define([
270
288
  return;
271
289
  }
272
290
 
273
- scheduleIdle(function() {
291
+ Utils.scheduleIdle(function() {
274
292
  try {
275
293
  var createdWaveformData = WaveformData.create(data);
276
294