summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/idle.js
blob: e24f75831c47dc0b336c9e74f0e48270447568cb (plain) (blame)
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
// Signing a browser out after a stretch with nobody at it.
//
// The hub cannot measure this. It hears a token renewal every few hours from
// any open tab, attended or not, and nothing at all while a film plays over
// WebRTC. So the page decides, and the hub only says how long
// (`browser_idle_hours` in /v1/hub/info).
//
// Activity is input, or any <video>/<audio> on the page that is playing: a film
// nobody touches for two hours is somebody watching it. The last-active time
// lives in localStorage so every tab of this browser shares it — one idle tab
// must not sign out the tab in use — and a browser closed without signing out
// is caught the moment it is opened again.
//
// Never started in the desktop application (app.js): that is its owner's own
// machine, and it signs back in with its device key.

const LAST_ACTIVE_KEY = 'mb_last_active';
const CHECK_MS = 60000;
// Input fires constantly; the timestamp only has to be minutes-accurate.
const WRITE_EVERY_MS = 30000;
const INPUT_EVENTS = ['pointerdown', 'keydown', 'wheel', 'touchstart'];

let lastWrite = 0;

function readLastActive() {
  try {
    const v = Number(localStorage.getItem(LAST_ACTIVE_KEY));
    return Number.isFinite(v) && v > 0 ? v : null;
  } catch {
    return null;
  }
}

/** Record activity now. `force` skips the write throttle — a sign-in uses it. */
function markActive(force = false) {
  const now = Date.now();
  if (!force && now - lastWrite < WRITE_EVERY_MS) return;
  lastWrite = now;
  try { localStorage.setItem(LAST_ACTIVE_KEY, String(now)); } catch { /* private mode */ }
}

function mediaPlaying() {
  return [...document.querySelectorAll('video, audio')]
    .some((m) => !m.paused && !m.ended);
}

/**
 * Watch for `idleMs` without activity, then call `onIdle` once.
 * Returns the function that stops watching.
 */
function startIdleWatch(idleMs, onIdle) {
  let fired = false;
  const onInput = () => markActive();
  const check = () => {
    if (fired) return;
    if (mediaPlaying()) { markActive(); return; }
    const last = readLastActive();
    // No record at all is a browser that predates this, not an idle one.
    if (last === null) { markActive(true); return; }
    if (Date.now() - last > idleMs) { fired = true; onIdle(); }
  };

  INPUT_EVENTS.forEach((e) =>
    window.addEventListener(e, onInput, { capture: true, passive: true }));
  document.addEventListener('visibilitychange', check);
  const timer = setInterval(check, CHECK_MS);
  check();

  return () => {
    INPUT_EVENTS.forEach((e) =>
      window.removeEventListener(e, onInput, { capture: true }));
    document.removeEventListener('visibilitychange', check);
    clearInterval(timer);
  };
}

export { startIdleWatch, markActive, LAST_ACTIVE_KEY };