Home Reference Source

src/task-loop.ts

  1. /**
  2. * Sub-class specialization of EventHandler base class.
  3. *
  4. * TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop,
  5. * scheduled asynchroneously, avoiding recursive calls in the same tick.
  6. *
  7. * The task itself is implemented in `doTick`. It can be requested and called for single execution
  8. * using the `tick` method.
  9. *
  10. * It will be assured that the task execution method (`tick`) only gets called once per main loop "tick",
  11. * no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly.
  12. *
  13. * If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`,
  14. * and cancelled with `clearNextTick`.
  15. *
  16. * The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`).
  17. *
  18. * Sub-classes need to implement the `doTick` method which will effectively have the task execution routine.
  19. *
  20. * Further explanations:
  21. *
  22. * The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously
  23. * only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks.
  24. *
  25. * When the task execution (`tick` method) is called in re-entrant way this is detected and
  26. * we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further
  27. * task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo).
  28. */
  29. export default class TaskLoop {
  30. private readonly _boundTick: () => void;
  31. private _tickTimer: number | null = null;
  32. private _tickInterval: number | null = null;
  33. private _tickCallCount = 0;
  34.  
  35. constructor() {
  36. this._boundTick = this.tick.bind(this);
  37. }
  38.  
  39. public destroy() {
  40. this.onHandlerDestroying();
  41. this.onHandlerDestroyed();
  42. }
  43.  
  44. protected onHandlerDestroying() {
  45. // clear all timers before unregistering from event bus
  46. this.clearNextTick();
  47. this.clearInterval();
  48. }
  49.  
  50. protected onHandlerDestroyed() {}
  51.  
  52. /**
  53. * @returns {boolean}
  54. */
  55. public hasInterval(): boolean {
  56. return !!this._tickInterval;
  57. }
  58.  
  59. /**
  60. * @returns {boolean}
  61. */
  62. public hasNextTick(): boolean {
  63. return !!this._tickTimer;
  64. }
  65.  
  66. /**
  67. * @param {number} millis Interval time (ms)
  68. * @returns {boolean} True when interval has been scheduled, false when already scheduled (no effect)
  69. */
  70. public setInterval(millis: number): boolean {
  71. if (!this._tickInterval) {
  72. this._tickInterval = self.setInterval(this._boundTick, millis);
  73. return true;
  74. }
  75. return false;
  76. }
  77.  
  78. /**
  79. * @returns {boolean} True when interval was cleared, false when none was set (no effect)
  80. */
  81. public clearInterval(): boolean {
  82. if (this._tickInterval) {
  83. self.clearInterval(this._tickInterval);
  84. this._tickInterval = null;
  85. return true;
  86. }
  87. return false;
  88. }
  89.  
  90. /**
  91. * @returns {boolean} True when timeout was cleared, false when none was set (no effect)
  92. */
  93. public clearNextTick(): boolean {
  94. if (this._tickTimer) {
  95. self.clearTimeout(this._tickTimer);
  96. this._tickTimer = null;
  97. return true;
  98. }
  99. return false;
  100. }
  101.  
  102. /**
  103. * Will call the subclass doTick implementation in this main loop tick
  104. * or in the next one (via setTimeout(,0)) in case it has already been called
  105. * in this tick (in case this is a re-entrant call).
  106. */
  107. public tick(): void {
  108. this._tickCallCount++;
  109. if (this._tickCallCount === 1) {
  110. this.doTick();
  111. // re-entrant call to tick from previous doTick call stack
  112. // -> schedule a call on the next main loop iteration to process this task processing request
  113. if (this._tickCallCount > 1) {
  114. // make sure only one timer exists at any time at max
  115. this.tickImmediate();
  116. }
  117. this._tickCallCount = 0;
  118. }
  119. }
  120.  
  121. public tickImmediate(): void {
  122. this.clearNextTick();
  123. this._tickTimer = self.setTimeout(this._boundTick, 0);
  124. }
  125.  
  126. /**
  127. * For subclass to implement task logic
  128. * @abstract
  129. */
  130. protected doTick(): void {}
  131. }