mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
AAAHHHJ RATE LIMITTT
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CvHOwJu1.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bm6FTwrt.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B1UrCI1T.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B1UrCI1T.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ const { isAdmin, isLockdownAdmin, getRole } = require('./roleService');
|
|||||||
const { getRoomCamera } = require('./roomCameraService');
|
const { getRoomCamera } = require('./roomCameraService');
|
||||||
const { roomCameraStreamEvents, getRoomCameraState } = require('./roomCameraSnapshotService');
|
const { roomCameraStreamEvents, getRoomCameraState } = require('./roomCameraSnapshotService');
|
||||||
|
|
||||||
|
const SUBSCRIBE_LIMIT = 20;
|
||||||
|
const SUBSCRIBE_WINDOW_MS = 1000;
|
||||||
|
|
||||||
function passesMode(socket) {
|
function passesMode(socket) {
|
||||||
const mode = getMode();
|
const mode = getMode();
|
||||||
if (mode === MODES.LOCKDOWN) {
|
if (mode === MODES.LOCKDOWN) {
|
||||||
@@ -23,6 +26,7 @@ function canViewRoomCamera(socket) {
|
|||||||
|
|
||||||
const cameraSubscribers = new Map(); // id -> Set(socketId)
|
const cameraSubscribers = new Map(); // id -> Set(socketId)
|
||||||
const socketSubscriptions = new Map(); // socketId -> Set(id)
|
const socketSubscriptions = new Map(); // socketId -> Set(id)
|
||||||
|
const subscribeBuckets = new Map(); // socketId -> { start, count }
|
||||||
|
|
||||||
function addSubscription(socket, cameraId) {
|
function addSubscription(socket, cameraId) {
|
||||||
if (!cameraSubscribers.has(cameraId)) {
|
if (!cameraSubscribers.has(cameraId)) {
|
||||||
@@ -59,6 +63,17 @@ function removeAllSubscriptions(socketId) {
|
|||||||
bucket.forEach((cameraId) => removeSubscription(socketId, cameraId));
|
bucket.forEach((cameraId) => removeSubscription(socketId, cameraId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function allowSubscribe(socketId) {
|
||||||
|
const now = Date.now();
|
||||||
|
let bucket = subscribeBuckets.get(socketId);
|
||||||
|
if (!bucket || now - bucket.start >= SUBSCRIBE_WINDOW_MS) {
|
||||||
|
bucket = { start: now, count: 0 };
|
||||||
|
}
|
||||||
|
bucket.count += 1;
|
||||||
|
subscribeBuckets.set(socketId, bucket);
|
||||||
|
return bucket.count <= SUBSCRIBE_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
function sendFrame(socket, cameraId, payload, buffer) {
|
function sendFrame(socket, cameraId, payload, buffer) {
|
||||||
socket.emit('roomCamera:frame', { id: cameraId, ...payload }, buffer);
|
socket.emit('roomCamera:frame', { id: cameraId, ...payload }, buffer);
|
||||||
}
|
}
|
||||||
@@ -91,6 +106,10 @@ io.on('connection', (socket) => {
|
|||||||
socket.on('roomCamera:subscribe', (payload = {}, cb = () => {}) => {
|
socket.on('roomCamera:subscribe', (payload = {}, cb = () => {}) => {
|
||||||
const cameraId = payload.roomCameraId || payload.id;
|
const cameraId = payload.roomCameraId || payload.id;
|
||||||
try {
|
try {
|
||||||
|
if (!allowSubscribe(socket.id)) {
|
||||||
|
cb({ error: 'Rate limited' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!cameraId) {
|
if (!cameraId) {
|
||||||
throw new Error('roomCameraId required');
|
throw new Error('roomCameraId required');
|
||||||
}
|
}
|
||||||
@@ -122,5 +141,6 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
removeAllSubscriptions(socket.id);
|
removeAllSubscriptions(socket.id);
|
||||||
|
subscribeBuckets.delete(socket.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSocket } from '../context/SocketContext.jsx';
|
import { useSocket } from '../context/SocketContext.jsx';
|
||||||
|
|
||||||
|
const RETRY_DELAY_MS = 3000;
|
||||||
|
|
||||||
function normalizeCamera(entry) {
|
function normalizeCamera(entry) {
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
if (typeof entry === 'string') {
|
if (typeof entry === 'string') {
|
||||||
@@ -29,11 +31,14 @@ export function useRoomCameraSnapshots(sourceList = []) {
|
|||||||
const objectUrls = useRef(new Map());
|
const objectUrls = useRef(new Map());
|
||||||
const normalizedEntries = useMemo(() => dedupe(sourceList.map(normalizeCamera).filter(Boolean)), [sourceList]);
|
const normalizedEntries = useMemo(() => dedupe(sourceList.map(normalizeCamera).filter(Boolean)), [sourceList]);
|
||||||
const entriesKey = useMemo(() => normalizedEntries.map((e) => e.key).join('|'), [normalizedEntries]);
|
const entriesKey = useMemo(() => normalizedEntries.map((e) => e.key).join('|'), [normalizedEntries]);
|
||||||
|
const retryTimers = useRef(new Map());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
||||||
objectUrls.current.clear();
|
objectUrls.current.clear();
|
||||||
setFeeds({});
|
setFeeds({});
|
||||||
|
retryTimers.current.forEach((timer) => clearTimeout(timer));
|
||||||
|
retryTimers.current.clear();
|
||||||
}, [entriesKey]);
|
}, [entriesKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,6 +47,23 @@ export function useRoomCameraSnapshots(sourceList = []) {
|
|||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
|
const clearRetry = (id) => {
|
||||||
|
const timer = retryTimers.current.get(id);
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
retryTimers.current.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleRetry = (id) => {
|
||||||
|
clearRetry(id);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
retryTimers.current.delete(id);
|
||||||
|
requestSubscribe(id);
|
||||||
|
}, RETRY_DELAY_MS);
|
||||||
|
retryTimers.current.set(id, timer);
|
||||||
|
};
|
||||||
|
|
||||||
const handleFrame = (meta = {}, buffer) => {
|
const handleFrame = (meta = {}, buffer) => {
|
||||||
if (cancelled || !meta.id || !buffer) return;
|
if (cancelled || !meta.id || !buffer) return;
|
||||||
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
||||||
@@ -78,18 +100,24 @@ export function useRoomCameraSnapshots(sourceList = []) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on('roomCamera:frame', handleFrame);
|
const requestSubscribe = (id) => {
|
||||||
socket.on('roomCamera:status', handleStatus);
|
const entry = normalizedEntries.find((e) => e.id === id);
|
||||||
|
if (!entry) return;
|
||||||
normalizedEntries.forEach((entry) => {
|
|
||||||
socket.emit('roomCamera:subscribe', { roomCameraId: entry.id }, (resp = {}) => {
|
socket.emit('roomCamera:subscribe', { roomCameraId: entry.id }, (resp = {}) => {
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
handleStatus({ id: entry.id, error: resp.error });
|
handleStatus({ id: entry.id, error: resp.error });
|
||||||
|
scheduleRetry(entry.id);
|
||||||
} else {
|
} else {
|
||||||
|
clearRetry(entry.id);
|
||||||
handleStatus({ id: entry.id, stale: true });
|
handleStatus({ id: entry.id, stale: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
socket.on('roomCamera:frame', handleFrame);
|
||||||
|
socket.on('roomCamera:status', handleStatus);
|
||||||
|
|
||||||
|
normalizedEntries.forEach((entry) => requestSubscribe(entry.id));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -100,6 +128,8 @@ export function useRoomCameraSnapshots(sourceList = []) {
|
|||||||
socket.off('roomCamera:status', handleStatus);
|
socket.off('roomCamera:status', handleStatus);
|
||||||
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
|
||||||
objectUrls.current.clear();
|
objectUrls.current.clear();
|
||||||
|
retryTimers.current.forEach((timer) => clearTimeout(timer));
|
||||||
|
retryTimers.current.clear();
|
||||||
};
|
};
|
||||||
}, [socket, normalizedEntries, entriesKey]);
|
}, [socket, normalizedEntries, entriesKey]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user