Bin
2025-12-17 dcf780a91c16b6be28635b6e2e0e702060ee19f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import { types } from "mobx-state-tree";
 
// both approaches are valid, so code is ready for both,
// but currently we use instant logic hardcoded
enum LEAD_TIME_LOGIC_OPTIONS {
  inertial = 0, // every action counted, minus overlaps
  instant = 1, // close events are recorded from first to last with no extra time
}
 
// stored length of interactions in ms, also works as a debounce time
const LEAD_TIME_INTERACTION = 500;
 
/**
 * Calculate lead time just by calling `countTime()` on every interaction.
 * Result stored in `leadTime`.
 *
 * Explanation of how we count time + difference between inertial and instant logic:
 *
 * INERTIAL                                       2nd event out of
 *               3 events                         debounce frame
 *  debounce     with overlaps       1 event      counted separately
 *   frame       |          |        |   |        |   ||      |
 * --|---|-------*--*---*------------*------------*----*--*------> events on timeline
 *               |      |            |   |        |   ||   |
 *               last event          same         same   2 events inside
 *               stops counting,     counting            debounce frame
 * INSTANT       no extra time                           no extra time
 */
const LeadTimeMixin = types
  .model({
    leadTime: 0,
  })
  .volatile(() => ({
    leadTimeLogic: LEAD_TIME_LOGIC_OPTIONS.inertial,
    // when did the last event happen, used only for instant logic
    lastRecordedTime: 0,
    // time of the end of the current debounce frame
    debouncedTime: 0,
  }))
  .actions((self) => ({
    _countTimeInertial() {
      const now = Date.now();
 
      // new sequence of events
      if (self.debouncedTime < now) {
        self.leadTime += LEAD_TIME_INTERACTION;
      } else {
        // debounced call
        // substracting overlapped time
        self.leadTime += LEAD_TIME_INTERACTION - (self.debouncedTime - now);
      }
 
      self.debouncedTime = now + LEAD_TIME_INTERACTION;
    },
    _countTimeInstant() {
      const now = Date.now();
 
      // new sequence of events
      if (self.debouncedTime < now) {
        self.leadTime += LEAD_TIME_INTERACTION;
        self.lastRecordedTime = now + LEAD_TIME_INTERACTION;
      } else {
        // debounced call.
        // if event happened inside of initial debounced frame, it won't be counted.
        // if it happened after some events, we store the time passed after the last one.
        // debounced frame will be increased by LEAD_TIME_INTERACTION in both cases.
        if (now > self.lastRecordedTime) {
          self.leadTime += now - self.lastRecordedTime;
          self.lastRecordedTime = now;
        }
      }
 
      self.debouncedTime = now + LEAD_TIME_INTERACTION;
    },
  }))
  .actions((self) => ({
    /**
     * Calculate leadTime; call it on every interaction.
     */
    countTime() {
      if (self.leadTimeLogic === LEAD_TIME_LOGIC_OPTIONS.inertial) {
        self._countTimeInertial();
      } else if (self.leadTimeLogic === LEAD_TIME_LOGIC_OPTIONS.instant) {
        self._countTimeInstant();
      }
    },
    resetLeadTimeCounters() {
      self.lastRecordedTime = 0;
      self.debouncedTime = 0;
    },
  }));
 
export default LeadTimeMixin;