Files
devplacepy/nodes/extension/src/Limits.js
T

168 lines
4.6 KiB
JavaScript
Raw Normal View History

2026-10-16 07:20:44 +02:00
import { LIMITS, REFUSALS } from "./spec.js";
const MB = 1024 * 1024;
const HOURS = /^([01]\d|2[0-3]):([0-5]\d)$/;
const CAP_KEYS = [
"max_frame_bytes",
"max_body_bytes",
"max_concurrent_per_node",
"job_timeout_seconds",
"ping_seconds",
"url_max_chars",
];
function isCount(value) {
return typeof value === "number" && Number.isInteger(value) && value >= 1;
}
function clampInt(value, low, high, fallback) {
if (!isCount(value)) {
return fallback;
}
return Math.min(high, Math.max(low, value));
}
function minutes(text) {
const match = HOURS.exec(text);
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
}
export class Limits {
constructor(options = {}) {
this.baked = options.baked || LIMITS;
this.clock = options.clock || (() => new Date());
this.caps = {};
for (const key of CAP_KEYS) {
this.caps[key] = this.baked[key];
}
this.reconnectMin = this.baked.reconnect_min_seconds;
this.reconnectMax = this.baked.reconnect_max_seconds;
this.serverBandwidthMb = this.baked.max_donor_mb_per_day;
this.serverConcurrent = this.baked.max_concurrent_per_node;
this.local = Limits.normaliseLocal({}, this.baked);
this.inflight = 0;
if (options.local) {
this.setLocal(options.local);
}
}
static normaliseLocal(values, baked = LIMITS) {
const from = minutes(values.activeFrom || "");
const to = minutes(values.activeTo || "");
const both = from !== null && to !== null;
return {
bandwidthMb: clampInt(
values.bandwidthMb,
1,
baked.max_donor_mb_per_day,
baked.default_donor_mb_per_day,
),
maxConcurrent: clampInt(
values.maxConcurrent,
1,
baked.max_concurrent_per_node,
baked.max_concurrent_per_node,
),
activeFrom: both ? values.activeFrom : "",
activeTo: both ? values.activeTo : "",
};
}
setLocal(values) {
this.local = Limits.normaliseLocal({ ...this.local, ...values }, this.baked);
}
applyWelcome(welcomeLimits, serverSettings) {
const incoming = welcomeLimits && typeof welcomeLimits === "object" ? welcomeLimits : {};
for (const key of CAP_KEYS) {
this.caps[key] = Math.min(this.baked[key], isCount(incoming[key]) ? incoming[key] : this.baked[key]);
}
const ceiling = this.baked.reconnect_max_seconds;
this.reconnectMin = clampInt(incoming.reconnect_min_seconds, 1, ceiling, this.baked.reconnect_min_seconds);
this.reconnectMax = clampInt(incoming.reconnect_max_seconds, this.reconnectMin, ceiling, ceiling);
this.applyServerSettings(serverSettings);
}
applyServerSettings(settings) {
const values = settings && typeof settings === "object" ? settings : {};
this.serverBandwidthMb = clampInt(
values.bandwidth_mb_day,
1,
this.baked.max_donor_mb_per_day,
this.baked.max_donor_mb_per_day,
);
this.serverConcurrent = clampInt(
values.max_concurrent,
1,
this.baked.max_concurrent_per_node,
this.baked.max_concurrent_per_node,
);
}
get maxBodyBytes() {
return this.caps.max_body_bytes;
}
get maxFrameBytes() {
return this.caps.max_frame_bytes;
}
get urlMaxChars() {
return this.caps.url_max_chars;
}
get pingSeconds() {
return this.caps.ping_seconds;
}
get concurrency() {
return Math.min(this.caps.max_concurrent_per_node, this.local.maxConcurrent, this.serverConcurrent);
}
get bandwidthBytes() {
return Math.min(this.local.bandwidthMb, this.serverBandwidthMb) * MB;
}
timeoutSeconds(requested) {
const asked = isCount(requested) ? requested : this.caps.job_timeout_seconds;
return Math.min(asked, this.caps.job_timeout_seconds);
}
helloLimits() {
return { max_concurrent: this.concurrency };
}
remainingBytes(usedBytes) {
return Math.max(0, this.bandwidthBytes - usedBytes);
}
inActiveHours() {
const from = minutes(this.local.activeFrom);
const to = minutes(this.local.activeTo);
if (from === null || to === null || from === to) {
return true;
}
const now = this.clock();
const current = now.getHours() * 60 + now.getMinutes();
return from < to ? current >= from && current < to : current >= from || current < to;
}
admit(usedBytes) {
if (!this.inActiveHours()) {
return REFUSALS.OUTSIDE_HOURS;
}
if (this.remainingBytes(usedBytes) <= 0) {
return REFUSALS.BANDWIDTH_EXHAUSTED;
}
if (this.inflight >= this.concurrency) {
return REFUSALS.CONCURRENCY_EXCEEDED;
}
this.inflight += 1;
return null;
}
release() {
this.inflight = Math.max(0, this.inflight - 1);
}
}