Initial implementation of Bluetooth toy control app
CI / Static checks (push) Successful in 1m27s
CI / Build and push image (push) Skipped

Next.js app with a browser-side buttplug/buttplug-wasm control layer
(server never touches real-time device commands), SQLite storage via
Drizzle, single-secret auth, recordings/replay with device remapping,
a usage stats dashboard, Docker deployment, and Gitea CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb
This commit is contained in:
2026-08-25 07:51:38 +02:00
co-authored by Claude Sonnet 5
commit 1119c8eea0
112 changed files with 15522 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
/**
* Per-actuator leading+trailing debounce so a dragged slider doesn't fire a
* device command on every pointermove - devices don't need mouse-move-rate
* updates, and most BLE stacks choke on a command flood.
*/
const THROTTLE_MS = 75;
interface ThrottleEntry {
timer: ReturnType<typeof setTimeout> | null;
pendingValue: number | null;
pendingDuration: number | undefined;
lastSentAt: number;
}
const entries = new Map<string, ThrottleEntry>();
export function throttledSend(
key: string,
send: (value: number, durationMs?: number) => void | Promise<void>,
value: number,
durationMs?: number,
): void {
let entry = entries.get(key);
if (!entry) {
entry = { timer: null, pendingValue: null, pendingDuration: undefined, lastSentAt: 0 };
entries.set(key, entry);
}
entry.pendingValue = value;
entry.pendingDuration = durationMs;
const flush = (): void => {
const v = entry!.pendingValue;
entry!.pendingValue = null;
entry!.timer = null;
entry!.lastSentAt = Date.now();
if (v !== null) void send(v, entry!.pendingDuration);
};
const elapsed = Date.now() - entry.lastSentAt;
if (elapsed >= THROTTLE_MS) {
if (entry.timer) clearTimeout(entry.timer);
flush();
} else if (!entry.timer) {
entry.timer = setTimeout(flush, THROTTLE_MS - elapsed);
}
}
export function clearThrottle(key: string): void {
const entry = entries.get(key);
if (entry?.timer) clearTimeout(entry.timer);
entries.delete(key);
}