mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 18:10:47 -04:00
ratelimit slop
This commit is contained in:
@@ -35,6 +35,7 @@ const SPOTLIGHT_VERIFY_DELAY_MS = 1200;
|
|||||||
const PUBLISHER_STDERR_SYNC_MS = 10000;
|
const PUBLISHER_STDERR_SYNC_MS = 10000;
|
||||||
const PUBLISHER_RTSP_TIMEOUT_US = 10000000;
|
const PUBLISHER_RTSP_TIMEOUT_US = 10000000;
|
||||||
const REOLINK_API_RETRY_MS = 1000;
|
const REOLINK_API_RETRY_MS = 1000;
|
||||||
|
const REOLINK_API_WRITE_INTERVAL_MS = 1000;
|
||||||
|
|
||||||
const events = new EventEmitter();
|
const events = new EventEmitter();
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@@ -92,6 +93,8 @@ let snapshotTimer = null;
|
|||||||
let spotlightVerifyTimer = null;
|
let spotlightVerifyTimer = null;
|
||||||
let vendorStatePromise = Promise.resolve();
|
let vendorStatePromise = Promise.resolve();
|
||||||
let reolinkApiLogNextAt = 0;
|
let reolinkApiLogNextAt = 0;
|
||||||
|
let reolinkWriteInFlight = false;
|
||||||
|
let lastReolinkWriteAcceptedAt = 0;
|
||||||
let lastSnapshotState = null;
|
let lastSnapshotState = null;
|
||||||
const snapshotSubscribers = new Map();
|
const snapshotSubscribers = new Map();
|
||||||
const socketSnapshotSubscriptions = new Map();
|
const socketSnapshotSubscriptions = new Map();
|
||||||
@@ -815,6 +818,47 @@ function serializeVendorState(operation) {
|
|||||||
return vendorStatePromise;
|
return vendorStatePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reserveReolinkWriteSlot(control) {
|
||||||
|
/*
|
||||||
|
Reolink light/IR writes are physical camera API writes, not high-frequency
|
||||||
|
control signals. Rejecting too-fast requests here, before they enter
|
||||||
|
serializeVendorState(), is what makes this a true rate limit instead of a
|
||||||
|
delayed queue. A request either gets the current write slot immediately or
|
||||||
|
fails immediately; the server never stores skipped toggle states to replay
|
||||||
|
later.
|
||||||
|
*/
|
||||||
|
if (reolinkWriteInFlight) {
|
||||||
|
const err = new Error('PTZ API rate limited');
|
||||||
|
err.code = 'PTZ_REOLINK_RATE_LIMITED';
|
||||||
|
err.control = control;
|
||||||
|
err.retryAfterMs = REOLINK_API_WRITE_INTERVAL_MS;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - lastReolinkWriteAcceptedAt;
|
||||||
|
if (elapsed < REOLINK_API_WRITE_INTERVAL_MS) {
|
||||||
|
const err = new Error('PTZ API rate limited');
|
||||||
|
err.code = 'PTZ_REOLINK_RATE_LIMITED';
|
||||||
|
err.control = control;
|
||||||
|
err.retryAfterMs = REOLINK_API_WRITE_INTERVAL_MS - elapsed;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
reolinkWriteInFlight = true;
|
||||||
|
lastReolinkWriteAcceptedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseReolinkWriteSlot() {
|
||||||
|
/*
|
||||||
|
Keep the in-flight flag separate from the timestamp. The flag blocks
|
||||||
|
overlap while an accepted write is still talking to the camera or waiting
|
||||||
|
through API reconnect; the timestamp blocks a second accepted write from
|
||||||
|
starting immediately after the first one finishes.
|
||||||
|
*/
|
||||||
|
reolinkWriteInFlight = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
if (!enabled || state.initialized || state.initializing) return;
|
if (!enabled || state.initialized || state.initializing) return;
|
||||||
state.initializing = true;
|
state.initializing = true;
|
||||||
@@ -1173,7 +1217,9 @@ async function removePreset(socket, payload = {}) {
|
|||||||
|
|
||||||
async function setSpotlight(socket, payload = {}) {
|
async function setSpotlight(socket, payload = {}) {
|
||||||
requireOperator(socket);
|
requireOperator(socket);
|
||||||
|
reserveReolinkWriteSlot('spotlight');
|
||||||
return serializeVendorState(async () => {
|
return serializeVendorState(async () => {
|
||||||
|
try {
|
||||||
let current = state.light ? normalizeSpotlightState(state.light) : null;
|
let current = state.light ? normalizeSpotlightState(state.light) : null;
|
||||||
if (payload.state === undefined && !current) {
|
if (payload.state === undefined && !current) {
|
||||||
/*
|
/*
|
||||||
@@ -1212,12 +1258,17 @@ async function setSpotlight(socket, payload = {}) {
|
|||||||
await callReolinkApi('SetWhiteLed', { WhiteLed: cameraPayload });
|
await callReolinkApi('SetWhiteLed', { WhiteLed: cameraPayload });
|
||||||
scheduleSpotlightVerification();
|
scheduleSpotlightVerification();
|
||||||
return state.light;
|
return state.light;
|
||||||
|
} finally {
|
||||||
|
releaseReolinkWriteSlot();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setIr(socket, payload = {}) {
|
async function setIr(socket, payload = {}) {
|
||||||
requireOperator(socket);
|
requireOperator(socket);
|
||||||
|
reserveReolinkWriteSlot('ir');
|
||||||
return serializeVendorState(async () => {
|
return serializeVendorState(async () => {
|
||||||
|
try {
|
||||||
const nextState = normalizeIrState(payload.state);
|
const nextState = normalizeIrState(payload.state);
|
||||||
/*
|
/*
|
||||||
The camera requires channel inside IrLights. Without it, SetIrLights
|
The camera requires channel inside IrLights. Without it, SetIrLights
|
||||||
@@ -1231,6 +1282,9 @@ async function setIr(socket, payload = {}) {
|
|||||||
await refreshVendorState();
|
await refreshVendorState();
|
||||||
emitChange('ir');
|
emitChange('ir');
|
||||||
return state.ir;
|
return state.ir;
|
||||||
|
} finally {
|
||||||
|
releaseReolinkWriteSlot();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user