Skip to content

Instantly share code, notes, and snippets.

@guiseek
Last active April 4, 2024 22:23
Show Gist options
  • Save guiseek/ca631907bf1e5f3769fe8192636bffdb to your computer and use it in GitHub Desktop.
Save guiseek/ca631907bf1e5f3769fe8192636bffdb to your computer and use it in GitHub Desktop.
Idle Detection
export interface IdleConfig {
maxIdle: number;
warnPeriod?: number;
scope?: Node;
}
import type { IdleConfig } from "./idle-config";
import {
merge,
filter,
Subject,
interval,
fromEvent,
takeUntil,
debounceTime,
BehaviorSubject,
Subscription,
} from "rxjs";
export class IdleObserver {
#idled;
#warned;
#active;
#counter;
#progress;
#config: Required<IdleConfig>;
get config() {
return this.#config;
}
get state() {
const idled = this.#idled.value;
const warned = this.#warned.value;
const active = this.#active.observed;
const counter = this.#counter.value;
const progress = this.#progress.value;
return { idled, warned, active, counter, progress };
}
#subs = new Subscription();
constructor(config: IdleConfig) {
this.#config = this.#mergeConfig(config);
this.#idled = new BehaviorSubject(false);
this.#active = new BehaviorSubject(false);
this.#warned = new BehaviorSubject(false);
this.#counter = new BehaviorSubject(config.maxIdle);
this.#progress = new BehaviorSubject(0);
this.#active = new Subject<boolean>();
}
set onwarn(callback: (value: boolean) => void) {
this.#subs.add(this.#warned.asObservable().subscribe(callback));
}
set onidle(callback: (value: boolean) => void) {
this.#subs.add(this.#idled.asObservable().subscribe(callback));
}
set onactive(callback: (value: boolean) => void) {
this.#subs.add(this.#active.asObservable().subscribe(callback));
}
set oncounter(callback: (value: number) => void) {
this.#subs.add(this.#counter.asObservable().subscribe(callback));
}
set onprogress(callback: (value: number) => void) {
this.#subs.add(this.#progress.asObservable().subscribe(callback));
}
warn(callback: (value: boolean) => void) {
return this.#warned.asObservable().subscribe(callback);
}
idle(callback: (value: boolean) => void) {
return this.#idled.asObservable().subscribe(callback);
}
active(callback: (value: boolean) => void) {
return this.#active.asObservable().subscribe(callback);
}
counter(callback: (value: number) => void) {
return this.#counter.asObservable().subscribe(callback);
}
progress(callback: (value: number) => void) {
return this.#progress.asObservable().subscribe(callback);
}
subscribe = () => {
const $interval = this.#subscribe();
const key$ = fromEvent(this.config.scope, "keydown");
const mouse$ = fromEvent(this.config.scope, "mousemove");
const merge$ = merge(key$, mouse$).pipe(
takeUntil(this.#active),
debounceTime(10)
);
const $merge = merge$.subscribe(this.subscribe);
const unsubscribe = () => {
this.#active.next(false);
$merge.unsubscribe();
$interval.unsubscribe();
};
return { unsubscribe };
};
setConfig(value: Partial<IdleConfig>) {
this.#config = { ...this.config, ...value };
return this;
}
#subscribe = () => {
this.#resetValues();
const interval$ = interval(1000).pipe(
takeUntil(this.#active),
filter(() => !this.#idled.value)
);
return interval$.subscribe(this.#update);
};
#resetValues = () => {
this.#active.next(true);
this.#progress.next(0);
this.#idled.next(false);
this.#warned.next(false);
this.#counter.next(this.config.maxIdle);
};
#update = (value: number) => {
const idled = this.#idleExceed(value);
if (idled !== this.#idled.value) {
this.#idled.next(idled);
}
const warned = this.#warnExceed(value);
if (warned !== this.#warned.value) {
this.#warned.next(warned);
}
this.#counter.next(this.#getCounter(value));
this.#progress.next(this.#getProgress(value));
};
#getCounter(value: number) {
return this.config.maxIdle - value;
}
#getProgress(value: number) {
const percent = (value / this.config.maxIdle) * 100;
return parseInt(String(percent), 10);
}
#idleExceed(value: number) {
return value >= this.config.maxIdle;
}
#warnExceed(value: number) {
return value >= this.config.maxIdle - this.config.warnPeriod;
}
#mergeConfig(value: IdleConfig): Required<IdleConfig> {
return { warnPeriod: 20, scope: document.body, ...value };
}
}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/ts.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Idle Observer</title>
</head>
<body>
<header>
<img src="ts.svg" width="64" alt="Vite" />
</header>
<main>
<section id="section">
<h2>Counter: <code id="counter"></code></h2>
<h2>Progress: <code id="progress"></code>%</h2>
<h2>Active: <code id="active"></code></h2>
<h2>Warned: <code id="warned"></code></h2>
<h2>Idled: <code id="idled"></code></h2>
</section>
</main>
<form id="form">
<label>
Max idle time
<input type="number" name="maxIdle" value="80" />
</label>
<label>
Warning period
<input type="number" name="warnPeriod" value="20" />
</label>
<button type="button" id="activate">Activate</button>
<button type="button" id="deactivate">Deactivate</button>
</form>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
import { IdleObserver } from "./lib/idle-observer";
import { parse } from "./utils/parse";
import "./style.css";
const idleObserver = new IdleObserver({
scope: section,
maxIdle: 80,
warnPeriod: 20,
});
idleObserver.onidle = (value) => (idled.textContent = String(value));
idleObserver.onwarn = (value) => (warned.textContent = String(value));
idleObserver.onactive = (value) => (active.textContent = String(value));
idleObserver.oncounter = (value) => (counter.textContent = String(value));
idleObserver.onprogress = (value) => (progress.textContent = String(value));
let detector$: { unsubscribe(): void };
form.onchange = (ev) => {
ev.preventDefault();
detector$ = idleObserver.setConfig(parse(form)).subscribe();
};
activate.onclick = () => {
detector$ = idleObserver.subscribe();
};
deactivate.onclick = () => {
detector$.unsubscribe();
};
export const parse = <T extends object>(form: HTMLFormElement) => {
return Object.fromEntries(new FormData(form).entries()) as T;
};
:root {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(179, 174, 174, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
code {
font-family: monospace;
white-space: pre-wrap;
font-size: 1.4rem;
color: white;
}
body {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
form {
gap: 16px;
}
form,
label {
display: flex;
flex-direction: column;
}
input {
padding: 8px 16px;
border-radius: 4px;
border: 1px solid rgb(255, 255, 255, 0.2);
transition: border-color 0.25s;
}
input:hover,
input:active,
input:focus {
border-color: #646cff;
}
#section {
margin: 60px;
width: 280px;
padding: 16px 32px;
border-radius: 24px;
transition: background-color 0.4s, border-color 0.8s;
border: 2px dashed rgb(255, 255, 255, 0.1);
}
#section:hover {
background-color: rgb(255, 255, 255, 0.01);
border-color: rgb(255, 255, 255, 0.3);
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
img {
width: 80px;
border-radius: 12px;
box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, 0.4);
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
/// <reference types="vite/client" />
declare const section: HTMLElement
declare const counter: HTMLElement
declare const progress: HTMLElement
declare const active: HTMLElement
declare const warned: HTMLElement
declare const idled: HTMLElement
declare const activate: HTMLButtonElement
declare const deactivate: HTMLButtonElement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment