@cameo69/node-red-ratelimit
Version:
implements rate limit with sliding window
340 lines (301 loc) • 15.2 kB
HTML
<!--
ISC License
Copyright (c) 2023 cameo69
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
-->
<!-- Define setting UI for weather-data node -->
<script type="text/html" data-template-name="rate-limiter">
<div class="form-row">
<label for="node-input-delay-action"><i class="fa fa-tasks"></i> <span data-i18n="rate-limiter.action"></span></label>
<select id="node-input-delay_action" style="width:270px !important">
<option value="ratelimit" data-i18n="rate-limiter.ratelimit"></option>
</select>
</div>
<div id="rate-details">
<div class="form-row">
<label for="node-input-rate"><i class="fa fa-clock-o"></i> <span data-i18n="rate-limiter.rate"></span></label>
<input type="text" id="node-input-rate" placeholder="1" style="text-align:end; width:60px !important">
<label for="node-input-rateUnits"> <span data-i18n="rate-limiter.msgper"></span></label>
<input type="text" id="node-input-nbRateUnits" placeholder="1" style="text-align:end; width:60px !important">
<select id="node-input-rateUnits" style="width:120px !important">
<option value="millisecond" data-i18n="rate-limiter.label.units.millisecond.singular"></option>
<option value="second" data-i18n="rate-limiter.label.units.second.singular"></option>
<option value="minute" data-i18n="rate-limiter.label.units.minute.singular"></option>
<option value="hour" data-i18n="rate-limiter.label.units.hour.singular"></option>
<option value="day" data-i18n="rate-limiter.label.units.day.singular"></option>
</select>
</div>
<div class="form-row">
<input type="hidden" id="node-input-outputs">
<label></label>
<select id="node-input-drop_select" style="width: 70%">
<option value="queue" data-i18n="rate-limiter.queuemsg"></option>
<option value="drop" data-i18n="rate-limiter.dropmsg"></option>
<!--
<option value="emit" data-i18n="rate-limiter.sendmsg"></option>
-->
</select>
</div>
<!-- max queue size & keep newest -->
<div id="queue_max_size">
<div class="form-row">
<label for="node-input-buffer_size"><span data-i18n="rate-limiter.buffer_config"></span></label>
<input type="text" id="node-input-buffer_size" placeholder="1000" style="text-align:end; width:120px !important">
<label for="node-input-buffer_size"> <span data-i18n="rate-limiter.buffer_size"></span></label>
</div>
<div class="form-row">
<label></label>
<select id="node-input-buffer_drop" style="width: 70%">
<option value="buffer_drop_old" data-i18n="rate-limiter.buffer_drop_old"></option>
<option value="buffer_drop_new" data-i18n="rate-limiter.buffer_drop_new"></option>
</select>
</div>
</div>
<div class="form-row" style="display: flex; align-items: center">
<label></label>
<input style="width:30px; margin:0" type="checkbox" id="node-input-emit_msg_2nd">
<label style="margin:0;width: auto;" for="node-input-emit_msg_2nd"> <span data-i18n="rate-limiter.emit_msg_2nd"></span></label>
</div>
<div class="form-row" style="display: flex; align-items: center">
<label></label>
<input style="width:30px; margin:0" type="checkbox" id="node-input-addcurrentcount">
<label style="margin:0;width: auto;" for="node-input-addcurrentcount"> <span data-i18n="rate-limiter.addcurrentcount"></span></label>
</div>
</div>
<div class="form-row" id="control-details">
<label for="node-input-control_topic"><i class="fa fa-tasks"></i> <span data-i18n="rate-limiter.control_topic_head"></span></label>
<input type="text" id="node-input-control_topic" data-i18n="[placeholder]rate-limiter.control_topic" style="width:120px !important">
<label style="margin:5;width: auto;" for="node-input-control_topic"> <span data-i18n="rate-limiter.control_topic_text"></span></label>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="rate-limiter.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]rate-limiter.name">
</div>
<div class="form-row">
<label></label>
<label id="node-versionlabel">Version</label>
</div>
</script>
<!-- register rate-limiter node -->
<script type="text/javascript">
RED.nodes.registerType('rate-limiter',{
category: 'function',
color: '#E6E0F8',
defaults: {
delay_action: {value: "ratelimit"},
rate: {
value:"1",
required:true,
label:"Rate",
validate:function(v,opt) {
function isPosInt(str) {
return /^\+?\d+$/.test(str);
}
if (RED.validators.number(v) && isPosInt(v) && (v > 0)) {
return true;
}
return RED._("rate-limiter.errors.invalid-rate");
}
},
nbRateUnits: {
value:"1",
required:true,
validate:function(v,opt) {
function isPosInt(str) {
return /^\+?\d+$/.test(str);
}
function isPositiveInteger(n) {
return 0 === n % (!isNaN(parseFloat(n)) && 0 <= ~~n);
}
if (RED.validators.number(v) && (v > 0)) {
let rateunitvalue = $("#node-input-rateUnits").val() || this.rateUnits;
let validated = rateunitvalue === "millisecond" && isPosInt(v);
validated = validated || ( rateunitvalue === "second" && isPositiveInteger(v*1000) );
validated = validated || ( rateunitvalue === "minute" && isPositiveInteger(v*1000*60) );
validated = validated || ( rateunitvalue === "hour" && isPositiveInteger(v*1000*60*60) );
validated = validated || ( rateunitvalue === "day" && isPositiveInteger(v*1000*60*60*24) );
if (validated) return true;
}
return RED._("rate-limiter.errors.invalid-rate-unit");
}
},
rateUnits: {value: "second"},
drop_select: {value: "queue"},
addcurrentcount: {value: false},
name: {value: ""},
outputs: {value: 1},
buffer_size: {
value: "0",
validate:function(v,opt) {
function isPosInt(str) {
return /^\+?\d+$/.test(str);
}
if (typeof v == "undefined") {
return true;
}
if (RED.validators.number(v) && isPosInt(v)) {
return true;
}
return RED._("rate-limiter.errors.invalid-rate");
}},
buffer_drop: {value: "buffer_drop_new"},
emit_msg_2nd: {value: false},
control_topic: {value: ""},
version: {value: 0.0018}
},
inputs:1,
outputs:1,
icon: "font-awesome/fa-clock-o",
label: function() {
// using this to set value for all
//if ((this.version || 0) == 0) this.version = 0.0009;
//if (typeof this.buffer_size == "undefined") this.buffer_size = 0;
//if (!this.buffer_size) this.buffer_size = 100;
//if ('buffer_size' in this) delete this.buffer_size;
// determine label
if (this.name) {
return this.name;
}
const rateUnitTxt = this.rateUnits ? (this.rateUnits === "millisecond" ? "ms" : this.rateUnits.charAt(0)) : "s";
const rateTxt = this.rate+" msg/" + (this.nbRateUnits == 1 ? '' : this.nbRateUnits) + rateUnitTxt;
let drpSelectTxt;
if (this.drop_select === "queue") {
if ((typeof this.buffer_size == "undefined") || (this.buffer_size === "0")) {
drpSelectTxt = " (queued)";
} else {
//drpSelectTxt = " (queued " + this.buffer_size + (this.buffer_drop === "buffer_drop_old" ? ", drop oldest)" : ", drop new)");
drpSelectTxt = " (queued " + this.buffer_size + ")";
}
} else if (this.drop_select === "drop" && !this.emit_msg_2nd) {
drpSelectTxt = " (drop)";
} else {
drpSelectTxt = " (2nd out)";
}
return "rate limit " + rateTxt + drpSelectTxt;
},
oneditprepare: function() {
var node = this;
/*
node-input-delay_action
node-input-rate
node-input-nbRateUnits
node-input-rateUnits
node-input-outputs
node-input-drop_select
node-input-emit_msg_2nd
node-input-addcurrentcount
node-input-name
*/
//fill empty from previous version
//added with v0.0.10
if (typeof this.delay_action == "undefined") {
//this.delay_action = "ratelimit";
$("#node-input-delay_action").val('ratelimit');
}
/*if (!this.drop_select) {
//this.drop_select = "emit";
$("#node-input-drop_select").val("emit");
}*/
//added post v0.0.12
if (typeof this.buffer_size == "undefined") {
//this.delay_action = "ratelimit";
$("#node-input-buffer_size").val(0);
}
if (typeof this.buffer_drop == "undefined") {
//$("#node-input-buffer_drop").val('buffer_drop_new');
$("#node-input-buffer_drop option[value='buffer_drop_new']").prop('selected', true);
}
if (typeof this.emit_msg_2nd == "undefined")
{
$("#node-input-emit_msg_2nd").prop('checked', (this.outputs === 2) );
}
if (typeof this.drop_select == "undefined" || this.drop_select === "emit") {
$("#node-input-outputs").val(2);
$("#node-input-emit_msg_2nd").prop('checked', true);
//setTimeout(() => {
//$("#node-input-drop_select option[value='drop']").prop('selected', true);
$("#node-input-drop_select").val("drop").trigger("change");
//}, 50);
$("#queue_max_size").hide();
}
//added post v0.0.13
if (typeof this.control_topic == "undefined") {
//this.delay_action = "ratelimit";
$("#node-input-control_topic").val('');
}
//normal definitions
$( "#node-input-rate" ).spinner({min:1});
$( "#node-input-nbRateUnits" ).spinner({min:1});
$("#node-input-buffer_size").spinner({min:0});
$('.ui-spinner-button').on("click", function() {
$(this).siblings('input').trigger("change");
});
$( "#node-input-nbRateUnits" ).on('change keyup', function() {
var $this = $(this);
var val = parseFloat($this.val());
var type = "singular";
if (val > 1) {
type = "plural";
}
if ($this.attr("data-type") == type) {
return;
}
$this.attr("data-type", type);
$("#node-input-rateUnits option").each(function () {
var $option = $(this);
var key = "rate-limiter.label.units." + $option.val() + "." + type;
$option.attr('data-i18n', key);
$option.html(node._(key));
});
});
$("#node-input-emit_msg_2nd").on("change", function() {
//$("#node-input-outputs").val( ( $("#node-input-emit_msg_2nd").val() ? 2 : 1) );
const outp = $("#node-input-emit_msg_2nd").is(":checked") ? 2 : 1;
$("#node-input-outputs").val(outp);
//$("#node-input-outputs").val( (this.prop('checked') ? 2 : 1) );
}).trigger("change");
$("#node-input-buffer_size").on("change keyup", function() {
debugger;
if (this.value === "0" && $("#node-input-drop_select").val() === "queue") {
//$("#node-input-buffer_drop").show();
$("#node-input-buffer_drop").prop('disabled', true);
$("#node-input-emit_msg_2nd").prop('disabled', true);
$("#node-input-emit_msg_2nd").prop('checked', false).change();
} else {
//$("#node-input-buffer_drop").hide();
$("#node-input-buffer_drop").prop('disabled', false);
$("#node-input-emit_msg_2nd").prop('disabled', false);
}
}).trigger("change");
$("#node-input-drop_select").on("change", function() {
if (this.value === "queue") {
$("#queue_max_size").show();
$("#node-input-buffer_size").change();
} else {
$("#queue_max_size").hide();
$("#node-input-emit_msg_2nd").prop('disabled', false);
//$("#node-input-buffer_size").change();
}
}).trigger("change");
// configure version number
const version = RED.nodes.registry.getModule("@cameo69/node-red-ratelimit").version;
$('#node-versionlabel').append(" " + version);
},
oneditsave: function() {
//this.outputs = $("#node-input-emit_msg_2nd").is(":checked") ? 2 : 1;
this.outputs = $("#node-input-outputs").val();
}
});
</script>
<script type="text/html" data-help-name="rate-limiter">
<p>A simple node that offers rate limiting based on a sliding window.</p>
</script>