@checksub_team/peaks_timeline 2.2.1 → 2.3.0-alpha.1

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.
@@ -43,12 +43,29 @@ define([
43
43
  this._lineGroups = new LineGroups(peaks, view, this);
44
44
  this._lineGroups.addToLayer(this);
45
45
 
46
+ // Drag overlay container.
47
+ // Use a Group inside the main sources layer to avoid adding an extra Stage layer.
48
+ this._dragGroup = new Konva.Group({
49
+ listening: false
50
+ });
51
+ this._layer.add(this._dragGroup);
52
+
46
53
  this._loadedData = {};
47
54
 
48
- this._debouncedRescale = new Invoker().debounce(
49
- this._rescale, 150
55
+ // Create invoker for performance optimizations
56
+ this._invoker = new Invoker();
57
+
58
+ // Version counter for cancellable rescale operations
59
+ this._rescaleVersion = 0;
60
+
61
+ // Throttled batch draw to prevent excessive redraws
62
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
63
+ this._layer.batchDraw.bind(this._layer)
50
64
  );
51
65
 
66
+ // Pending draw flag to coalesce multiple draw requests
67
+ this._drawPending = false;
68
+
52
69
  this._peaks.on('handler.sources.add', this._onSourcesAdd.bind(this));
53
70
  this._peaks.on('handler.sources.destroy', this._onSourcesDestroy.bind(this));
54
71
  this._peaks.on('handler.sources.show', this._onSourcesShow.bind(this));
@@ -59,8 +76,16 @@ define([
59
76
  this._peaks.on('handler.segments.show', this._onSegmentsShow.bind(this));
60
77
  this._peaks.on('model.source.setIndicators', this.setIndicators.bind(this));
61
78
  this._peaks.on('handler.view.mouseup', this._stopDrag.bind(this));
79
+ this._peaks.on('sources.delayedLineChange', this._onSourcesDelayedLineChanged.bind(this));
62
80
  }
63
81
 
82
+ SourcesLayer.prototype._onSourcesDelayedLineChanged = function() {
83
+ // Update dragged source groups when sources change line after a delay (e.g., after automatic line creation)
84
+ if (this._draggedElements && this._draggedElements.length > 0) {
85
+ this._dragSourcesGroup();
86
+ }
87
+ };
88
+
64
89
  SourcesLayer.prototype._stopDrag = function() {
65
90
  const draggedSourceGroup = this._sourcesGroup[this._draggedElementId];
66
91
 
@@ -87,6 +112,11 @@ define([
87
112
 
88
113
  SourcesLayer.prototype.add = function(element) {
89
114
  this._layer.add(element);
115
+
116
+ // Keep drag group on top even if other callers add elements later.
117
+ if (this._dragGroup) {
118
+ this._dragGroup.moveToTop();
119
+ }
90
120
  };
91
121
 
92
122
  /**
@@ -190,7 +220,7 @@ define([
190
220
 
191
221
  this._view.updateTimelineLength();
192
222
 
193
- this._layer.batchDraw();
223
+ this.batchDraw();
194
224
  };
195
225
 
196
226
  SourcesLayer.prototype._onSourcesShow = function(sources) {
@@ -200,7 +230,7 @@ define([
200
230
  self._sourcesGroup[source.id].setWrapping(false, true);
201
231
  });
202
232
 
203
- this._layer.batchDraw();
233
+ this.batchDraw();
204
234
  };
205
235
 
206
236
  SourcesLayer.prototype._onSourcesHide = function(sources) {
@@ -210,7 +240,7 @@ define([
210
240
  self._sourcesGroup[source.id].setWrapping(true, true);
211
241
  });
212
242
 
213
- this._layer.batchDraw();
243
+ this.batchDraw();
214
244
  };
215
245
 
216
246
  SourcesLayer.prototype._onDataRetrieved = function(data, source, url) {
@@ -243,7 +273,7 @@ define([
243
273
  SourcesLayer.prototype._onSegmentsShow = function(segmentsGroupId, lineId) {
244
274
  this._lineGroups.addSegments(segmentsGroupId, lineId);
245
275
  this._view.updateTimelineLength();
246
- this._layer.batchDraw();
276
+ this.batchDraw();
247
277
  };
248
278
 
249
279
  /**
@@ -285,12 +315,13 @@ define([
285
315
 
286
316
  if (sourceGroup) {
287
317
  sourceGroup.createIndicators();
288
- this._layer.batchDraw();
318
+ this.batchDraw();
289
319
  }
290
320
  };
291
321
 
292
322
  /**
293
323
  * Updates the positions of all displayed sources in the view.
324
+ * Uses optimized batching to reduce draw calls.
294
325
  *
295
326
  * @param {Number} startTime The start of the visible range in the view,
296
327
  * in seconds.
@@ -307,12 +338,16 @@ define([
307
338
 
308
339
  var count = sources.length;
309
340
 
310
- sources.forEach(this._updateSource.bind(this));
341
+ // Batch update all sources
342
+ for (var i = 0; i < sources.length; i++) {
343
+ this._updateSource(sources[i]);
344
+ }
311
345
 
312
346
  count += this._removeInvisibleSources(startTime, endTime);
313
347
 
314
348
  if (count > 0) {
315
- this._layer.batchDraw();
349
+ // Use throttled batch draw for consistent performance
350
+ this.batchDraw();
316
351
  }
317
352
  };
318
353
 
@@ -348,23 +383,67 @@ define([
348
383
  orderable: true,
349
384
  draggable: true
350
385
  });
386
+
387
+ var self = this;
388
+
389
+ this._initialSourcePositions = {};
390
+
391
+ this._draggedElements.forEach(function(source) {
392
+ // Store initial position for ALL dragged sources (even those outside view)
393
+ // This is needed for time calculations during drag
394
+ self._initialSourcePositions[source.id] = {
395
+ startTime: source.startTime,
396
+ endTime: source.endTime,
397
+ lineId: source.lineId
398
+ };
399
+
400
+ var sourceGroup = self._sourcesGroup[source.id];
401
+
402
+ if (sourceGroup) {
403
+ // Mark as dragging (for all sources, not just the clicked one)
404
+ sourceGroup.setDragging(true);
405
+
406
+ // Get absolute Y position before moving (relative to line group)
407
+ var absoluteY = sourceGroup.getAbsoluteY();
408
+
409
+ // Move source to the drag group so it draws above ALL other sources/segments
410
+ // without introducing an additional Konva.Layer on the Stage.
411
+ sourceGroup.moveTo(self._dragGroup);
412
+ // Restore the Y position (now relative to drag layer, which is at y=0)
413
+ sourceGroup.y(absoluteY);
414
+ }
415
+ });
351
416
  };
352
417
 
353
418
  SourcesLayer.prototype.onSourcesGroupDragEnd = function() {
419
+ var self = this;
420
+
421
+ this._initialSourcePositions = null;
422
+ this._dragOffsetX = undefined;
423
+ this._dragOffsetY = undefined;
424
+
354
425
  const updatedSources = this._draggedElements.map(
355
426
  function(source) {
356
- const sourceGroup = this._sourcesGroup[source.id];
427
+ const sourceGroup = self._sourcesGroup[source.id];
357
428
 
358
429
  if (sourceGroup) {
430
+ // Clear dragging state before moving back to line group
431
+ sourceGroup.setDragging(false);
359
432
  sourceGroup.prepareDragEnd();
433
+ // Move source back to its line group (it was moved to layer during drag)
434
+ self._lineGroups.addSource(source, sourceGroup);
435
+ // Reset Y position to 0 relative to parent line group
436
+ // (source followed cursor during drag, now snap to line position)
437
+ sourceGroup.y(0);
360
438
  }
361
439
 
362
440
  return source;
363
- }.bind(this)
441
+ }
364
442
  );
365
443
 
366
444
  this._draggedElementId = null;
367
445
 
446
+ this.refresh();
368
447
  this._view.batchDrawSourcesLayer();
369
448
  this._view.updateTimelineLength();
370
449
 
@@ -372,7 +451,70 @@ define([
372
451
  };
373
452
 
374
453
  SourcesLayer.prototype.onSourcesGroupDrag = function(draggedElement) {
375
- this._view.updateWithAutoScroll(this._dragSourcesGroup.bind(this));
454
+ var pointerPos = this._view.getPointerPosition();
455
+
456
+ this._view.updateWithAutoScroll(this._dragSourcesGroup.bind(this), null, true);
457
+
458
+ // Return position that follows the mouse cursor exactly
459
+ var clickedSourceGroup = this._sourcesGroup[this._draggedElementId];
460
+
461
+ if (clickedSourceGroup) {
462
+ var mouseX = pointerPos.x;
463
+ var mouseY = pointerPos.y;
464
+ var offsetX = this._dragOffsetX || 0;
465
+ var offsetY = this._dragOffsetY || 0;
466
+
467
+ // Calculate offset on first drag if not set
468
+ if (this._dragOffsetX === undefined) {
469
+ var currentPos = draggedElement.absolutePosition();
470
+
471
+ this._dragOffsetX = currentPos.x - mouseX;
472
+ this._dragOffsetY = currentPos.y - mouseY;
473
+ offsetX = this._dragOffsetX;
474
+ offsetY = this._dragOffsetY;
475
+ }
476
+
477
+ var clickedSourceX = mouseX + offsetX;
478
+ var clickedSourceY = mouseY + offsetY;
479
+
480
+ // Position all other dragged sources relative to the clicked source
481
+ // They should maintain their initial relative positions
482
+ if (this._draggedElements && this._draggedElements.length > 1 && this._initialSourcePositions) {
483
+ var self = this;
484
+ var clickedInitialPos = this._initialSourcePositions[this._draggedElementId];
485
+
486
+ if (clickedInitialPos) {
487
+ var clickedInitialPixelX = this._view.timeToPixels(clickedInitialPos.startTime);
488
+
489
+ this._draggedElements.forEach(function(source) {
490
+ if (source.id !== self._draggedElementId) {
491
+ var sourceGroup = self._sourcesGroup[source.id];
492
+
493
+ if (sourceGroup) {
494
+ var initialPos = self._initialSourcePositions[source.id];
495
+
496
+ if (initialPos) {
497
+ // Calculate pixel offset from clicked source's initial position
498
+ var initialPixelX = self._view.timeToPixels(initialPos.startTime);
499
+ var pixelOffset = initialPixelX - clickedInitialPixelX;
500
+
501
+ // Position this source relative to clicked source's current position
502
+ sourceGroup.absolutePosition({
503
+ x: clickedSourceX + pixelOffset,
504
+ y: clickedSourceY
505
+ });
506
+ }
507
+ }
508
+ }
509
+ });
510
+ }
511
+ }
512
+
513
+ return {
514
+ x: clickedSourceX,
515
+ y: clickedSourceY
516
+ };
517
+ }
376
518
 
377
519
  return {
378
520
  x: draggedElement.absolutePosition().x,
@@ -399,10 +541,13 @@ define([
399
541
  return;
400
542
  }
401
543
 
544
+ var newStartTime = Utils.roundTime(initialStartTime + timeOffsetDiff + timeDiff);
545
+ var newEndTime = Utils.roundTime(initialEndTime + timeOffsetDiff + timeDiff);
546
+
402
547
  const shouldRedraw = this.manageSourceMovements(
403
548
  this._draggedElements,
404
- initialStartTime + timeOffsetDiff + timeDiff,
405
- initialEndTime + timeOffsetDiff + timeDiff,
549
+ newStartTime,
550
+ newEndTime,
406
551
  orderable,
407
552
  mousePosX,
408
553
  mousePosY
@@ -424,8 +569,40 @@ define([
424
569
  );
425
570
  };
426
571
 
427
- SourcesLayer.prototype._applyTimeChangesToSources = function(sources, initialStartTime, newStartTime,
428
- newEndTime
572
+ /**
573
+ * Updates source times during drag using initial positions.
574
+ *
575
+ * @private
576
+ * @param {Number} newStartTime The new start time for the first source
577
+ */
578
+ SourcesLayer.prototype._updateSourceTimesDuringDrag = function(newStartTime) {
579
+ if (!this._initialSourcePositions || !this._draggedElements) {
580
+ return;
581
+ }
582
+
583
+ var self = this;
584
+ var firstSourceInitial = this._initialSourcePositions[this._draggedElements[0].id];
585
+
586
+ if (!firstSourceInitial) {
587
+ return;
588
+ }
589
+
590
+ // Calculate time diff from INITIAL position, not current position
591
+ var timeDiff = Utils.roundTime(newStartTime - firstSourceInitial.startTime);
592
+
593
+ this._draggedElements.forEach(function(source) {
594
+ var initialPos = self._initialSourcePositions[source.id];
595
+
596
+ if (initialPos) {
597
+ source.updateTimes(
598
+ Utils.roundTime(initialPos.startTime + timeDiff),
599
+ Utils.roundTime(initialPos.endTime + timeDiff)
600
+ );
601
+ }
602
+ });
603
+ };
604
+
605
+ SourcesLayer.prototype._applyTimeChangesToSources = function(sources, initialStartTime, newStartTime, newEndTime
429
606
  ) {
430
607
  if (sources.length === 1) {
431
608
  sources[0].updateTimes(
@@ -434,18 +611,49 @@ define([
434
611
  );
435
612
  }
436
613
  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
- });
614
+ // When moving multiple sources via block-drag, compute time diff from the
615
+ // drag-start positions to avoid cumulative drift as times are updated.
616
+ var canUseInitialPositions = Boolean(this._initialSourcePositions
617
+ && this._initialSourcePositions[sources[0].id]);
618
+
619
+ if (canUseInitialPositions) {
620
+ for (var i = 0; i < sources.length; i++) {
621
+ if (!this._initialSourcePositions[sources[i].id]) {
622
+ canUseInitialPositions = false;
623
+ break;
624
+ }
625
+ }
626
+ }
627
+
628
+ if (canUseInitialPositions) {
629
+ var firstInitial = this._initialSourcePositions[sources[0].id];
630
+ var timeDiffFromInitial = Utils.roundTime(newStartTime - firstInitial.startTime);
631
+
632
+ if (timeDiffFromInitial !== 0) {
633
+ var self = this;
634
+
635
+ sources.forEach(function(source) {
636
+ var initialPos = self._initialSourcePositions[source.id];
637
+
638
+ source.updateTimes(
639
+ Utils.roundTime(initialPos.startTime + timeDiffFromInitial),
640
+ Utils.roundTime(initialPos.endTime + timeDiffFromInitial)
641
+ );
642
+ });
643
+ }
644
+ }
645
+ else {
646
+ // Fallback for non-drag multi-updates.
647
+ const timeDiff = Utils.roundTime(newStartTime - initialStartTime);
648
+
649
+ if (timeDiff !== 0) {
650
+ sources.forEach(function(source) {
651
+ source.updateTimes(
652
+ Utils.roundTime(source.startTime + timeDiff),
653
+ Utils.roundTime(source.endTime + timeDiff)
654
+ );
655
+ });
656
+ }
449
657
  }
450
658
  }
451
659
 
@@ -501,9 +709,31 @@ define([
501
709
 
502
710
  SourcesLayer.prototype._findOrAddSourceGroup = function(source) {
503
711
  var sourceGroup = this._sourcesGroup[source.id];
712
+ var isNewlyCreated = false;
504
713
 
505
714
  if (!sourceGroup) {
506
715
  sourceGroup = this._addSourceGroup(source);
716
+ isNewlyCreated = true;
717
+ }
718
+
719
+ // If this source is being dragged and was just recreated (came back into view),
720
+ // set it up properly for dragging
721
+ if (isNewlyCreated && this._draggedElements && this._initialSourcePositions) {
722
+ var isDraggedSource = this._draggedElements.some(function(s) {
723
+ return s.id === source.id;
724
+ });
725
+
726
+ if (isDraggedSource) {
727
+ // Mark as dragging
728
+ sourceGroup.setDragging(true);
729
+
730
+ // Get Y position from line group before moving
731
+ var absoluteY = sourceGroup.getAbsoluteY();
732
+
733
+ // Move to drag group
734
+ sourceGroup.moveTo(this._dragGroup);
735
+ sourceGroup.y(absoluteY);
736
+ }
507
737
  }
508
738
 
509
739
  return sourceGroup;
@@ -625,8 +855,12 @@ define([
625
855
  this._layer.setVisible(visible);
626
856
  };
627
857
 
858
+ /**
859
+ * Schedules a batch draw using RAF throttling for better performance.
860
+ * Multiple calls within the same frame are coalesced into one draw.
861
+ */
628
862
  SourcesLayer.prototype.batchDraw = function() {
629
- this._layer.batchDraw();
863
+ this._throttledBatchDraw();
630
864
  };
631
865
 
632
866
  SourcesLayer.prototype.listening = function(bool) {
@@ -654,21 +888,36 @@ define([
654
888
  return endsLater || startsEarlier;
655
889
  };
656
890
 
657
- SourcesLayer.prototype.rescale = function(debounce) {
658
- if (debounce) {
659
- this._debouncedRescale();
660
- }
661
- else {
662
- this._rescale();
663
- }
891
+ SourcesLayer.prototype.rescale = function() {
892
+ // Increment the rescale version to cancel any in-progress rescale
893
+ this._rescaleVersion = (this._rescaleVersion || 0) + 1;
894
+ this._rescale(this._rescaleVersion);
664
895
  };
665
896
 
666
- SourcesLayer.prototype._rescale = function() {
667
- var id, audioPreviews, urls = [], self = this;
897
+ SourcesLayer.prototype._rescale = function(version) {
898
+ var self = this;
899
+ var ids = Object.keys(this._sourcesGroup);
900
+ var urls = [];
901
+ var index = 0;
902
+
903
+ function processNext() {
904
+ // Check if this rescale was cancelled (a newer one started)
905
+ if (self._rescaleVersion !== version) {
906
+ return;
907
+ }
908
+
909
+ if (index >= ids.length) {
910
+ // Done processing all sources
911
+ self.batchDraw();
912
+ return;
913
+ }
914
+
915
+ var id = ids[index];
916
+ var sourceGroup = self._sourcesGroup[id];
668
917
 
669
- for (id in this._sourcesGroup) {
670
- if (Utils.objectHasProperty(this._sourcesGroup, id)) {
671
- audioPreviews = this._sourcesGroup[id].getAudioPreview();
918
+ // Skip if source group was removed during async processing
919
+ if (sourceGroup) {
920
+ var audioPreviews = sourceGroup.getAudioPreview();
672
921
 
673
922
  audioPreviews.forEach(function(audioPreview) {
674
923
  if (self._shouldResampleAudio(audioPreview.url, urls)) {
@@ -681,8 +930,14 @@ define([
681
930
  }
682
931
  });
683
932
  }
933
+
934
+ index++;
935
+
936
+ // Yield to allow cancellation and UI updates
937
+ Utils.scheduleIdle(processNext, { timeout: 16 });
684
938
  }
685
- this._layer.batchDraw();
939
+
940
+ processNext();
686
941
  };
687
942
 
688
943
  SourcesLayer.prototype._shouldResampleAudio = function(audioUrl, urls) {
@@ -708,6 +963,14 @@ define([
708
963
  this._peaks.off('handler.segments.show', this._onSegmentsShow);
709
964
  this._peaks.off('model.source.setIndicators', this.setIndicators);
710
965
  this._peaks.off('handler.view.mouseup', this._stopDrag);
966
+
967
+ // Cancel any in-progress rescale
968
+ this._rescaleVersion++;
969
+
970
+ // Clean up invoker resources
971
+ if (this._invoker) {
972
+ this._invoker.destroy();
973
+ }
711
974
  };
712
975
 
713
976
  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
 
package/src/utils.js CHANGED
@@ -338,6 +338,96 @@ define([
338
338
  var BB = ((B.toString(16).length === 1) ? '0' + B.toString(16) : B.toString(16));
339
339
 
340
340
  return '#' + RR + GG + BB;
341
+ },
342
+
343
+ // ==================== Performance Utilities ====================
344
+
345
+ /**
346
+ * Schedules work during browser idle time.
347
+ * Falls back to setTimeout if requestIdleCallback is not available.
348
+ *
349
+ * @param {Function} callback Function to execute
350
+ * @param {Object} options Options object with timeout property
351
+ * @returns {Number} ID that can be used to cancel
352
+ */
353
+ scheduleIdle: function(callback, options) {
354
+ options = options || { timeout: 50 };
355
+
356
+ if (typeof window !== 'undefined' && window.requestIdleCallback) {
357
+ return window.requestIdleCallback(callback, options);
358
+ }
359
+ return setTimeout(function() {
360
+ callback({
361
+ didTimeout: true,
362
+ timeRemaining: function() {
363
+ return 0;
364
+ }
365
+ });
366
+ }, 0);
367
+ },
368
+
369
+ /**
370
+ * Cancels a scheduled idle callback.
371
+ *
372
+ * @param {Number} id The ID returned by scheduleIdle
373
+ */
374
+ cancelIdle: function(id) {
375
+ if (typeof window !== 'undefined' && window.cancelIdleCallback) {
376
+ window.cancelIdleCallback(id);
377
+ }
378
+ else {
379
+ clearTimeout(id);
380
+ }
381
+ },
382
+
383
+ /**
384
+ * Creates a simple LRU (Least Recently Used) cache.
385
+ *
386
+ * @param {Number} maxSize Maximum number of items to cache
387
+ * @returns {Object} Cache with get(), set(), has(), delete(), clear() methods
388
+ */
389
+ createLRUCache: function(maxSize) {
390
+ var cache = new Map();
391
+
392
+ maxSize = maxSize || 100;
393
+
394
+ return {
395
+ get: function(key) {
396
+ if (!cache.has(key)) {
397
+ return undefined;
398
+ }
399
+ // Move to end (most recently used)
400
+ var value = cache.get(key);
401
+
402
+ cache.delete(key);
403
+ cache.set(key, value);
404
+ return value;
405
+ },
406
+ set: function(key, value) {
407
+ if (cache.has(key)) {
408
+ cache.delete(key);
409
+ }
410
+ else if (cache.size >= maxSize) {
411
+ // Delete oldest (first) entry
412
+ var firstKey = cache.keys().next().value;
413
+
414
+ cache.delete(firstKey);
415
+ }
416
+ cache.set(key, value);
417
+ },
418
+ has: function(key) {
419
+ return cache.has(key);
420
+ },
421
+ delete: function(key) {
422
+ return cache.delete(key);
423
+ },
424
+ clear: function() {
425
+ cache.clear();
426
+ },
427
+ size: function() {
428
+ return cache.size;
429
+ }
430
+ };
341
431
  }
342
432
  };
343
433
  });