shimmer-worker.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { parentPort, workerData } from 'worker_threads';
  2. import { writeSync } from 'fs';
  3. import type { ShimmerWorkerMessage } from './types';
  4. // Write directly to fd 1 (stdout) instead of writeStdout().
  5. // In Node.js worker threads, process.stdout is proxied through the main
  6. // thread's event loop — so if the main thread is blocked (e.g. SQLite),
  7. // stdout writes from the worker queue up and the animation freezes.
  8. // fs.writeSync(1, ...) is a direct kernel syscall that bypasses this.
  9. function writeStdout(s: string): void {
  10. writeSync(1, s);
  11. }
  12. const SPINNER_GLYPHS = ['·', '✢', '✳', '✶', '✻', '✽'];
  13. const ANIM_INTERVAL = 150;
  14. const FRAMES_PER_GLYPH = 3;
  15. const RST = '\x1b[0m';
  16. const DM = '\x1b[2m';
  17. const GRN = '\x1b[32m';
  18. const BOLD = '\x1b[1m';
  19. const startTime: number = workerData.startTime;
  20. function animFrame(): number {
  21. return Math.floor((Date.now() - startTime) / ANIM_INTERVAL);
  22. }
  23. function lerp(a: number, b: number, t: number): number {
  24. return Math.round(a + (b - a) * t);
  25. }
  26. function shimmerColor(frame: number): string {
  27. const t = (Math.sin(frame * 2 * Math.PI / 13) + 1) / 2;
  28. const r = lerp(160, 251, t);
  29. const g = lerp(100, 191, t);
  30. const b = lerp(9, 36, t);
  31. return `\x1b[38;2;${r};${g};${b}m${BOLD}`;
  32. }
  33. function formatNumber(n: number): string {
  34. return n.toLocaleString();
  35. }
  36. function renderBar(frame: number, filled: number, empty: number): string {
  37. if (filled === 0) return `${DM}${'░'.repeat(empty)}${RST}`;
  38. const cycleFrames = 24;
  39. const shimmerPos = ((frame % cycleFrames) / cycleFrames) * (filled + 6) - 3;
  40. const shimmerWidth = 3;
  41. let bar = '';
  42. for (let i = 0; i < filled; i++) {
  43. const dist = Math.abs(i - shimmerPos);
  44. const t = Math.max(0, 1 - dist / shimmerWidth);
  45. const r = lerp(160, 251, t);
  46. const g = lerp(100, 191, t);
  47. const b = lerp(9, 36, t);
  48. bar += `\x1b[38;2;${r};${g};${b}m${BOLD}█`;
  49. }
  50. bar += `${RST}${DM}${'░'.repeat(empty)}${RST}`;
  51. return bar;
  52. }
  53. // Mutable state
  54. let currentMessage = '';
  55. let currentPercent = -1;
  56. let currentCount = 0;
  57. function render(): void {
  58. if (!currentMessage) return;
  59. const frame = animFrame();
  60. const glyphIdx = Math.floor(frame / FRAMES_PER_GLYPH) % SPINNER_GLYPHS.length;
  61. const glyph = SPINNER_GLYPHS[glyphIdx] ?? '·';
  62. const color = shimmerColor(frame);
  63. let line: string;
  64. if (currentPercent >= 0) {
  65. const barWidth = 25;
  66. const filled = Math.round(barWidth * currentPercent / 100);
  67. const empty = barWidth - filled;
  68. line = `${DM}│${RST} ${color}${glyph}${RST} ${currentMessage} ${renderBar(frame, filled, empty)} ${currentPercent}%`;
  69. } else if (currentCount > 0) {
  70. line = `${DM}│${RST} ${color}${glyph}${RST} ${currentMessage}... ${formatNumber(currentCount)} found`;
  71. } else {
  72. line = `${DM}│${RST} ${color}${glyph}${RST} ${currentMessage}...`;
  73. }
  74. writeStdout(`\r\x1b[K${line}`);
  75. }
  76. function finishPhase(): void {
  77. if (!currentMessage) return;
  78. writeStdout(`\r\x1b[K`);
  79. let detail = '';
  80. if (currentPercent >= 0) detail = ' — done';
  81. else if (currentCount > 0) detail = ` — ${formatNumber(currentCount)} found`;
  82. writeStdout(`${DM}│${RST} ${GRN}◆${RST} ${currentMessage}${detail}\n`);
  83. currentMessage = '';
  84. currentPercent = -1;
  85. currentCount = 0;
  86. }
  87. // Render loop — independent of main thread
  88. const tickInterval = setInterval(render, 50);
  89. parentPort!.on('message', (msg: ShimmerWorkerMessage) => {
  90. if (msg.type === 'update') {
  91. currentMessage = msg.phaseName;
  92. currentPercent = msg.percent;
  93. currentCount = msg.count;
  94. } else if (msg.type === 'finish-phase') {
  95. finishPhase();
  96. } else if (msg.type === 'stop') {
  97. clearInterval(tickInterval);
  98. finishPhase();
  99. parentPort!.postMessage({ type: 'stopped' });
  100. }
  101. });