diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8e68224ccb..f5d9ab44cc 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,7 +10,7 @@ defaults:
run:
shell: bash
env:
- viceroy_version: 0.8.1
+ viceroy_version: 0.9.4
wasm-tools_version: 1.0.28
fastly-cli_version: 10.4.0
@@ -133,7 +133,7 @@ jobs:
matrix:
include:
- crate: viceroy
- version: 0.8.1 # Note: workflow-level env vars can't be used in matrix definitions
+ version: 0.9.4 # Note: workflow-level env vars can't be used in matrix definitions
options: ""
- crate: wasm-tools
version: 1.0.28 # Note: workflow-level env vars can't be used in matrix definitions
diff --git a/documentation/docs/fastly:edge-rate-limiter/RateCounter/RateCounter.mdx b/documentation/docs/fastly:edge-rate-limiter/RateCounter/RateCounter.mdx
new file mode 100644
index 0000000000..04780a1fc0
--- /dev/null
+++ b/documentation/docs/fastly:edge-rate-limiter/RateCounter/RateCounter.mdx
@@ -0,0 +1,35 @@
+---
+hide_title: false
+hide_table_of_contents: false
+pagination_next: null
+pagination_prev: null
+---
+# `RateCounter()`
+
+The **`RateCounter` constructor** can be used with a [Edge Rate Limiter](../EdgeRateLimiter/EdgeRateLimiter.mdx) or standalone for counting and rate calculations.
+
+>**Note**: Can only be used when processing requests, not during build-time initialization.
+
+## Syntax
+
+```js
+new RateCounter(name)
+```
+
+> **Note:** `RateCounter()` can only be constructed with `new`. Attempting to call it without `new` throws a [`TypeError`](../../globals/TypeError/TypeError.mdx).
+
+### Parameters
+
+- `name` _: string_
+ - Open a RateCounter with the given name
+
+
+### Return value
+
+A new `RateCounter` object instance.
+
+### Exceptions
+
+- `TypeError`
+ - Thrown if the provided `name` value can not be coerced into a string
+
diff --git a/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/increment.mdx b/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/increment.mdx
new file mode 100644
index 0000000000..8bd5d00cf8
--- /dev/null
+++ b/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/increment.mdx
@@ -0,0 +1,32 @@
+---
+hide_title: false
+hide_table_of_contents: false
+pagination_next: null
+pagination_prev: null
+---
+# RateCounter.prototype.increment
+
+Increment the given `entry` in the RateCounter instance with the given `delta` value.
+
+## Syntax
+```js
+increment(entry, delta)
+```
+
+### Parameters
+
+- `entry` _: string_
+ - The name of the entry to look up
+- `delta` _: number_
+ - The amount to increment the entry by
+
+
+### Return value
+
+Returns `undefined`.
+
+### Exceptions
+
+- `TypeError`
+ - Thrown if the provided `entry` value can not be coerced into a string
+ - Thrown if the provided `delta` value is not a positive, finite number.
diff --git a/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/lookupCount.mdx b/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/lookupCount.mdx
new file mode 100644
index 0000000000..5b07777aec
--- /dev/null
+++ b/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/lookupCount.mdx
@@ -0,0 +1,32 @@
+---
+hide_title: false
+hide_table_of_contents: false
+pagination_next: null
+pagination_prev: null
+---
+# RateCounter.prototype.lookupCount
+
+Look up the current rate for the given `entry` and the given `duration`.
+
+## Syntax
+```js
+lookupCount(entry, duration)
+```
+
+### Parameters
+
+- `entry` _: string_
+ - The name of the entry to look up
+- `duration` _: number_
+ - The duration to lookup alongside the entry, has to be either, 10, 20, 30, 40, 50, or 60 seconds.
+
+
+### Return value
+
+Returns a number which is the count for the given `entry` and `duration` in this `RateCounter` instance.
+
+### Exceptions
+
+- `TypeError`
+ - Thrown if the provided `entry` value can not be coerced into a string
+ - Thrown if the provided `duration` value is not either, 10, 20, 30, 40, 50 or 60.
diff --git a/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/lookupRate.mdx b/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/lookupRate.mdx
new file mode 100644
index 0000000000..496d39cd21
--- /dev/null
+++ b/documentation/docs/fastly:edge-rate-limiter/RateCounter/prototype/lookupRate.mdx
@@ -0,0 +1,32 @@
+---
+hide_title: false
+hide_table_of_contents: false
+pagination_next: null
+pagination_prev: null
+---
+# RateCounter.prototype.lookupRate
+
+Look up the current rate for the given `entry` and the given `window`.
+
+## Syntax
+```js
+lookupRate(entry, window)
+```
+
+### Parameters
+
+- `entry` _: string_
+ - The name of the entry to look up
+- `window` _: number_
+ - The window to look up alongside the entry, has to be either 1 second, 10 seconds, or 60 seconds
+
+
+### Return value
+
+Returns a number which is the rate for the given `entry` and `window` in this `RateCounter` instance.
+
+### Exceptions
+
+- `TypeError`
+ - Thrown if the provided `entry` value can not be coerced into a string
+ - Thrown if the provided `window` value is not either, 1, 10, or 60.
diff --git a/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js b/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js
new file mode 100644
index 0000000000..3d898f2fe0
--- /dev/null
+++ b/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js
@@ -0,0 +1,532 @@
+///
+/* eslint-env serviceworker */
+
+import { pass, assert, assertThrows } from "./assertions.js";
+import { RateCounter } from 'fastly:edge-rate-limiter';
+import { routes, isRunningLocally } from "./routes.js";
+
+let error;
+// RateCounter
+{
+ routes.set("/rate-counter/interface", () => {
+
+ let actual = Reflect.ownKeys(RateCounter)
+ let expected = ["prototype", "length", "name"]
+ error = assert(actual, expected, `Reflect.ownKeys(RateCounter)`)
+ if (error) { return error }
+
+ // Check the prototype descriptors are correct
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter, 'prototype')
+ expected = {
+ "value": RateCounter.prototype,
+ "writable": false,
+ "enumerable": false,
+ "configurable": false
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter, 'prototype')`)
+ if (error) { return error }
+ }
+
+ // Check the constructor function's defined parameter length is correct
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter, 'length')
+ expected = {
+ "value": 0,
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter, 'length')`)
+ if (error) { return error }
+ }
+
+ // Check the constructor function's name is correct
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter, 'name')
+ expected = {
+ "value": "RateCounter",
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter, 'name')`)
+ if (error) { return error }
+ }
+
+ // Check the prototype has the correct keys
+ {
+ actual = Reflect.ownKeys(RateCounter.prototype)
+ expected = ["constructor", "increment", "lookupRate", "lookupCount", Symbol.toStringTag]
+ error = assert(actual, expected, `Reflect.ownKeys(RateCounter.prototype)`)
+ if (error) { return error }
+ }
+
+ // Check the constructor on the prototype is correct
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'constructor')
+ expected = { "writable": true, "enumerable": false, "configurable": true, value: RateCounter.prototype.constructor }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'constructor')`)
+ if (error) { return error }
+
+ error = assert(typeof RateCounter.prototype.constructor, 'function', `typeof RateCounter.prototype.constructor`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.constructor, 'length')
+ expected = {
+ "value": 0,
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.constructor, 'length')`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.constructor, 'name')
+ expected = {
+ "value": "RateCounter",
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.constructor, 'name')`)
+ if (error) { return error }
+ }
+
+ // Check the Symbol.toStringTag on the prototype is correct
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype, Symbol.toStringTag)
+ expected = { "writable": false, "enumerable": false, "configurable": true, value: "RateCounter" }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype, [Symbol.toStringTag])`)
+ if (error) { return error }
+
+ error = assert(typeof RateCounter.prototype[Symbol.toStringTag], 'string', `typeof RateCounter.prototype[Symbol.toStringTag]`)
+ if (error) { return error }
+ }
+
+ // Check the increment method has correct descriptors, length and name
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'increment')
+ expected = { "writable": true, "enumerable": true, "configurable": true, value: RateCounter.prototype.increment }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'increment')`)
+ if (error) { return error }
+
+ error = assert(typeof RateCounter.prototype.increment, 'function', `typeof RateCounter.prototype.increment`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.increment, 'length')
+ expected = {
+ "value": 2,
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.increment, 'length')`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.increment, 'name')
+ expected = {
+ "value": "increment",
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.increment, 'name')`)
+ if (error) { return error }
+ }
+
+ // Check the lookupRate method has correct descriptors, length and name
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'lookupRate')
+ expected = { "writable": true, "enumerable": true, "configurable": true, value: RateCounter.prototype.lookupRate }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'lookupRate')`)
+ if (error) { return error }
+
+ error = assert(typeof RateCounter.prototype.lookupRate, 'function', `typeof RateCounter.prototype.lookupRate`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupRate, 'length')
+ expected = {
+ "value": 2,
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupRate, 'length')`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupRate, 'name')
+ expected = {
+ "value": "lookupRate",
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupRate, 'name')`)
+ if (error) { return error }
+ }
+
+ // Check the lookupCount method has correct descriptors, length and name
+ {
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'lookupCount')
+ expected = { "writable": true, "enumerable": true, "configurable": true, value: RateCounter.prototype.lookupCount }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype, 'lookupCount')`)
+ if (error) { return error }
+
+ error = assert(typeof RateCounter.prototype.lookupCount, 'function', `typeof RateCounter.prototype.lookupCount`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupCount, 'length')
+ expected = {
+ "value": 2,
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupCount, 'length')`)
+ if (error) { return error }
+
+ actual = Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupCount, 'name')
+ expected = {
+ "value": "lookupCount",
+ "writable": false,
+ "enumerable": false,
+ "configurable": true
+ }
+ error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(RateCounter.prototype.lookupCount, 'name')`)
+ if (error) { return error }
+ }
+
+ return pass('ok')
+ });
+
+ // RateCounter constructor
+ {
+ routes.set("/rate-counter/constructor/called-as-regular-function", () => {
+ error = assertThrows(() => {
+ RateCounter()
+ }, Error, `calling a builtin RateCounter constructor without new is forbidden`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/constructor/called-as-constructor-no-arguments", () => {
+ error = assertThrows(() => new RateCounter(), Error, `RateCounter constructor: At least 1 argument required, but only 0 passed`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ // Ensure we correctly coerce the parameter to a string as according to
+ // https://tc39.es/ecma262/#sec-tostring
+ routes.set("/rate-counter/constructor/name-parameter-calls-7.1.17-ToString", () => {
+ if (!isRunningLocally()) {
+ let sentinel;
+ const test = () => {
+ sentinel = Symbol('sentinel');
+ const name = {
+ toString() {
+ throw sentinel;
+ }
+ }
+ new RateCounter(name)
+ }
+ error = assertThrows(test)
+ if (error) { return error }
+ try {
+ test()
+ } catch (thrownError) {
+ error = assert(thrownError, sentinel, 'thrownError === sentinel')
+ if (error) { return error }
+ }
+ error = assertThrows(() => {
+ new RateCounter(Symbol())
+ }, Error, `can't convert symbol to string`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/constructor/happy-path", () => {
+ error = assert(new RateCounter("rc") instanceof RateCounter, true, `new RateCounter("rc") instanceof RateCounter`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ }
+
+ // RateCounter increment method
+ // increment(entry: string, delta: number): void;
+ {
+ routes.set("/rate-counter/increment/called-as-constructor", () => {
+ error = assertThrows(() => {
+ new RateCounter.prototype.increment('entry', 1)
+ }, Error, `RateCounter.prototype.increment is not a constructor`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ // Ensure we correctly coerce the parameter to a string as according to
+ // https://tc39.es/ecma262/#sec-tostring
+ routes.set("/rate-counter/increment/entry-parameter-calls-7.1.17-ToString", () => {
+ let sentinel;
+ const test = () => {
+ sentinel = Symbol('sentinel');
+ const entry = {
+ toString() {
+ throw sentinel;
+ }
+ }
+ let rc = new RateCounter("rc");
+ rc.increment(entry, 1)
+ }
+ error = assertThrows(test)
+ if (error) { return error }
+ try {
+ test()
+ } catch (thrownError) {
+ console.log({ thrownError })
+ error = assert(thrownError, sentinel, 'thrownError === sentinel')
+ if (error) { return error }
+ }
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.increment(Symbol(), 1)
+ }, Error, `can't convert symbol to string`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/increment/entry-parameter-not-supplied", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.increment()
+ }, Error, `increment: At least 2 arguments required, but only 0 passed`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/increment/delta-parameter-not-supplied", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.increment("entry")
+ }, Error, `increment: At least 2 arguments required, but only 1 passed`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/increment/delta-parameter-negative", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.increment("entry", -1)
+ }, Error, `increment: delta parameter is an invalid value, only positive numbers can be used for delta values.`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/increment/delta-parameter-infinity", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.increment("entry", Infinity)
+ }, Error, `increment: delta parameter is an invalid value, only positive numbers can be used for delta values.`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/increment/delta-parameter-NaN", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.increment("entry", NaN)
+ }, Error, `increment: delta parameter is an invalid value, only positive numbers can be used for delta values.`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/increment/returns-undefined", () => {
+ let rc = new RateCounter("rc");
+ error = assert(rc.increment('meow', 1), undefined, "rc.increment('meow', 1)")
+ if (error) { return error }
+ return pass('ok')
+ });
+ }
+
+ // RateCounter lookupRate method
+ // lookupRate(entry: string, window: [1, 10, 60]): number;
+ {
+ routes.set("/rate-counter/lookupRate/called-as-constructor", () => {
+ error = assertThrows(() => {
+ new RateCounter.prototype.lookupRate('entry', 1)
+ }, Error, `RateCounter.prototype.lookupRate is not a constructor`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ // Ensure we correctly coerce the parameter to a string as according to
+ // https://tc39.es/ecma262/#sec-tostring
+ routes.set("/rate-counter/lookupRate/entry-parameter-calls-7.1.17-ToString", () => {
+ let sentinel;
+ const test = () => {
+ sentinel = Symbol('sentinel');
+ const entry = {
+ toString() {
+ throw sentinel;
+ }
+ }
+ let rc = new RateCounter("rc");
+ rc.lookupRate(entry, 1)
+ }
+ error = assertThrows(test)
+ if (error) { return error }
+ try {
+ test()
+ } catch (thrownError) {
+ console.log({ thrownError })
+ error = assert(thrownError, sentinel, 'thrownError === sentinel')
+ if (error) { return error }
+ }
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupRate(Symbol(), 1)
+ }, Error, `can't convert symbol to string`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupRate/entry-parameter-not-supplied", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupRate()
+ }, Error, `lookupRate: At least 2 arguments required, but only 0 passed`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupRate/window-parameter-not-supplied", () => {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupRate("entry")
+ }, Error, `lookupRate: At least 2 arguments required, but only 1 passed`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupRate/window-parameter-negative", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupRate("entry", -1)
+ }, Error, `lookupRate: window parameter must be either: 1, 10, or 60`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupRate/window-parameter-infinity", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupRate("entry", Infinity)
+ }, Error, `lookupRate: window parameter must be either: 1, 10, or 60`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupRate/window-parameter-NaN", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupRate("entry", NaN)
+ }, Error, `lookupRate: window parameter must be either: 1, 10, or 60`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupRate/returns-number", () => {
+ let rc = new RateCounter("rc");
+ error = assert(typeof rc.lookupRate('meow', 1), "number", `typeof rc.lookupRate('meow', 1)`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ }
+
+ // RateCounter lookupCount method
+ // lookupCount(entry: string, duration: [10, 20, 30, 40, 50, 60]): number;
+ {
+ routes.set("/rate-counter/lookupCount/called-as-constructor", () => {
+ error = assertThrows(() => {
+ new RateCounter.prototype.lookupCount('entry', 1)
+ }, Error, `RateCounter.prototype.lookupCount is not a constructor`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ // Ensure we correctly coerce the parameter to a string as according to
+ // https://tc39.es/ecma262/#sec-tostring
+ routes.set("/rate-counter/lookupCount/entry-parameter-calls-7.1.17-ToString", () => {
+ let sentinel;
+ const test = () => {
+ sentinel = Symbol('sentinel');
+ const entry = {
+ toString() {
+ throw sentinel;
+ }
+ }
+ let rc = new RateCounter("rc");
+ rc.lookupCount(entry, 1)
+ }
+ error = assertThrows(test)
+ if (error) { return error }
+ try {
+ test()
+ } catch (thrownError) {
+ console.log({ thrownError })
+ error = assert(thrownError, sentinel, 'thrownError === sentinel')
+ if (error) { return error }
+ }
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupCount(Symbol(), 1)
+ }, Error, `can't convert symbol to string`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupCount/entry-parameter-not-supplied", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupCount()
+ }, Error, `lookupCount: At least 2 arguments required, but only 0 passed`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupCount/duration-parameter-not-supplied", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupCount("entry")
+ }, Error, `lookupCount: At least 2 arguments required, but only 1 passed`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupCount/duration-parameter-negative", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupCount("entry", -1)
+ }, Error, `lookupCount: duration parameter must be either: 10, 20, 30, 40, 50, or 60`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupCount/duration-parameter-infinity", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupCount("entry", Infinity)
+ }, Error, `lookupCount: duration parameter must be either: 10, 20, 30, 40, 50, or 60`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupCount/duration-parameter-NaN", () => {
+ if (!isRunningLocally()) {
+ error = assertThrows(() => {
+ let rc = new RateCounter("rc");
+ rc.lookupCount("entry", NaN)
+ }, Error, `lookupCount: duration parameter must be either: 10, 20, 30, 40, 50, or 60`)
+ if (error) { return error }
+ }
+ return pass('ok')
+ });
+ routes.set("/rate-counter/lookupCount/returns-number", () => {
+ let rc = new RateCounter("rc");
+ error = assert(typeof rc.lookupCount('meow', 10), "number", `typeof rc.lookupCount('meow', 1)`)
+ if (error) { return error }
+ return pass('ok')
+ });
+ }
+}
diff --git a/integration-tests/js-compute/fixtures/app/src/index.js b/integration-tests/js-compute/fixtures/app/src/index.js
index f5f6bf37c1..e02aa78062 100644
--- a/integration-tests/js-compute/fixtures/app/src/index.js
+++ b/integration-tests/js-compute/fixtures/app/src/index.js
@@ -18,6 +18,7 @@ import "./console.js"
import "./crypto.js"
import "./dictionary.js"
import "./dynamic-backend.js"
+import "./edge-rate-limiter.js"
import "./env.js"
import "./fanout.js"
import "./fastly-now.js"
diff --git a/integration-tests/js-compute/fixtures/app/tests.json b/integration-tests/js-compute/fixtures/app/tests.json
index 1e21dc0c81..82335e37f8 100644
--- a/integration-tests/js-compute/fixtures/app/tests.json
+++ b/integration-tests/js-compute/fixtures/app/tests.json
@@ -6839,5 +6839,325 @@
"body": "ok",
"status": 200
}
+ },
+
+ "GET /rate-counter/interface": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/interface"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/constructor/called-as-regular-function": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/constructor/called-as-regular-function"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/constructor/called-as-constructor-no-arguments": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/constructor/called-as-constructor-no-arguments"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/constructor/name-parameter-calls-7.1.17-ToString": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/constructor/name-parameter-calls-7.1.17-ToString"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/constructor/happy-path": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/constructor/happy-path"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/called-as-constructor": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/called-as-constructor"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/entry-parameter-calls-7.1.17-ToString": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/entry-parameter-calls-7.1.17-ToString"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/entry-parameter-not-supplied": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/entry-parameter-not-supplied"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/delta-parameter-not-supplied": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/delta-parameter-not-supplied"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/delta-parameter-negative": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/delta-parameter-negative"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/delta-parameter-infinity": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/delta-parameter-infinity"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/delta-parameter-NaN": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/delta-parameter-NaN"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/increment/returns-undefined": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/increment/returns-undefined"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/called-as-constructor": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/called-as-constructor"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/entry-parameter-calls-7.1.17-ToString": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/entry-parameter-calls-7.1.17-ToString"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/entry-parameter-not-supplied": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/entry-parameter-not-supplied"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/window-parameter-not-supplied": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/window-parameter-not-supplied"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/window-parameter-negative": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/window-parameter-negative"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/window-parameter-infinity": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/window-parameter-infinity"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/window-parameter-NaN": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/window-parameter-NaN"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupRate/returns-number": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupRate/returns-number"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/called-as-constructor": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/called-as-constructor"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/entry-parameter-calls-7.1.17-ToString": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/entry-parameter-calls-7.1.17-ToString"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/entry-parameter-not-supplied": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/entry-parameter-not-supplied"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/duration-parameter-not-supplied": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/duration-parameter-not-supplied"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/duration-parameter-negative": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/duration-parameter-negative"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/duration-parameter-infinity": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/duration-parameter-infinity"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/duration-parameter-NaN": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/duration-parameter-NaN"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /rate-counter/lookupCount/returns-number": {
+ "environments": ["viceroy", "compute"],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/rate-counter/lookupCount/returns-number"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
}
}
diff --git a/runtime/js-compute-runtime/builtins/cache-core.cpp b/runtime/js-compute-runtime/builtins/cache-core.cpp
index 802a8dce0c..c9303ed372 100644
--- a/runtime/js-compute-runtime/builtins/cache-core.cpp
+++ b/runtime/js-compute-runtime/builtins/cache-core.cpp
@@ -264,7 +264,7 @@ JS::Result parseTransactionUpdateOptions(JSContext
JS::UniqueChars data;
std::tie(data, length) = result.unwrap();
host_api::HostBytes metadata(std::move(data), length);
- options.metadata = metadata;
+ options.metadata = std::move(metadata);
}
return options;
diff --git a/runtime/js-compute-runtime/builtins/edge-rate-limiter.cpp b/runtime/js-compute-runtime/builtins/edge-rate-limiter.cpp
new file mode 100644
index 0000000000..f8edb3e47f
--- /dev/null
+++ b/runtime/js-compute-runtime/builtins/edge-rate-limiter.cpp
@@ -0,0 +1,185 @@
+#include "edge-rate-limiter.h"
+#include "builtin.h"
+#include "core/encode.h"
+#include "host_interface/host_api.h"
+#include "js-compute-builtins.h"
+#include "js/Result.h"
+#include
+
+namespace builtins {
+
+JSString *RateCounter::get_name(JSObject *self) {
+ MOZ_ASSERT(is_instance(self));
+ MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString());
+
+ return JS::GetReservedSlot(self, Slots::Name).toString();
+}
+
+// increment(entry: string, delta: number): void;
+bool RateCounter::increment(JSContext *cx, unsigned argc, JS::Value *vp) {
+ REQUEST_HANDLER_ONLY("The RateCounter builtin");
+ METHOD_HEADER(2);
+
+ // Convert entry parameter into a string
+ auto entry = core::encode(cx, args.get(0));
+ if (!entry) {
+ return false;
+ }
+
+ // Convert delta parameter into a number
+ double delta;
+ if (!JS::ToNumber(cx, args.get(1), &delta)) {
+ return false;
+ }
+
+ // This needs to happen on the happy-path as these all end up being valid uint32_t values that the
+ // host-call accepts
+ if (delta < 0 || std::isnan(delta) || std::isinf(delta)) {
+ JS_ReportErrorASCII(cx,
+ "increment: delta parameter is an invalid value, only positive numbers can "
+ "be used for delta values.");
+ return false;
+ }
+
+ MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString());
+ JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString());
+ auto name = core::encode(cx, name_val);
+ if (!name) {
+ return false;
+ }
+
+ auto res = host_api::RateCounter::increment(name, entry, delta);
+ if (auto *err = res.to_err()) {
+ HANDLE_ERROR(cx, *err);
+ return false;
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
+// lookupRate(entry: string, window: [1, 10, 60]): number;
+bool RateCounter::lookupRate(JSContext *cx, unsigned argc, JS::Value *vp) {
+ REQUEST_HANDLER_ONLY("The RateCounter builtin");
+ METHOD_HEADER(2);
+
+ // Convert entry parameter into a string
+ auto entry = core::encode(cx, args.get(0));
+ if (!entry) {
+ return false;
+ }
+
+ // Convert window parameter into a number
+ double window;
+ if (!JS::ToNumber(cx, args.get(1), &window)) {
+ return false;
+ }
+
+ MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString());
+ JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString());
+ auto name = core::encode(cx, name_val);
+ if (!name) {
+ return false;
+ }
+
+ auto res = host_api::RateCounter::lookup_rate(name, entry, window);
+ if (auto *err = res.to_err()) {
+ if (host_api::error_is_generic(*err) || host_api::error_is_invalid_argument(*err)) {
+ if (window != 1 && window != 10 && window != 60) {
+ JS_ReportErrorASCII(cx, "lookupRate: window parameter must be either: 1, 10, or 60");
+ return false;
+ }
+ }
+ HANDLE_ERROR(cx, *err);
+ return false;
+ }
+
+ JS::RootedValue rate(cx, JS::NumberValue(res.unwrap()));
+ args.rval().set(rate);
+ return true;
+}
+
+// lookupCount(entry: string, duration: [10, 20, 30, 40, 50, 60]): number;
+bool RateCounter::lookupCount(JSContext *cx, unsigned argc, JS::Value *vp) {
+ REQUEST_HANDLER_ONLY("The RateCounter builtin");
+ METHOD_HEADER(2);
+
+ // Convert entry parameter into a string
+ auto entry = core::encode(cx, args.get(0));
+ if (!entry) {
+ return false;
+ }
+
+ // Convert duration parameter into a number
+ double duration;
+ if (!JS::ToNumber(cx, args.get(1), &duration)) {
+ return false;
+ }
+
+ MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString());
+ JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString());
+ auto name = core::encode(cx, name_val);
+ if (!name) {
+ return false;
+ }
+
+ auto res = host_api::RateCounter::lookup_count(name, entry, duration);
+ if (auto *err = res.to_err()) {
+ if (host_api::error_is_generic(*err) || host_api::error_is_invalid_argument(*err)) {
+ if (duration != 10 && duration != 20 && duration != 30 && duration != 40 && duration != 50 &&
+ duration != 60) {
+ JS_ReportErrorASCII(
+ cx, "lookupCount: duration parameter must be either: 10, 20, 30, 40, 50, or 60");
+ return false;
+ }
+ }
+ HANDLE_ERROR(cx, *err);
+ return false;
+ }
+
+ JS::RootedValue rate(cx, JS::NumberValue(res.unwrap()));
+ args.rval().set(rate);
+ return true;
+}
+
+const JSFunctionSpec RateCounter::static_methods[] = {
+ JS_FS_END,
+};
+
+const JSPropertySpec RateCounter::static_properties[] = {
+ JS_PS_END,
+};
+
+const JSFunctionSpec RateCounter::methods[] = {
+ JS_FN("increment", increment, 2, JSPROP_ENUMERATE),
+ JS_FN("lookupRate", lookupRate, 2, JSPROP_ENUMERATE),
+ JS_FN("lookupCount", lookupCount, 2, JSPROP_ENUMERATE), JS_FS_END};
+
+const JSPropertySpec RateCounter::properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "RateCounter", JSPROP_READONLY), JS_PS_END};
+
+// Open a RateCounter instance with the given name
+// constructor(name: string);
+bool RateCounter::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
+ REQUEST_HANDLER_ONLY("The RateCounter builtin");
+ CTOR_HEADER("RateCounter", 1);
+ auto name = core::encode(cx, args.get(0));
+ if (!name) {
+ return false;
+ }
+
+ JS::RootedObject instance(cx, JS_NewObjectForConstructor(cx, &class_, args));
+ if (!instance) {
+ return false;
+ }
+ JS::SetReservedSlot(instance, static_cast(Slots::Name),
+ JS::StringValue(JS_NewStringCopyZ(cx, name.begin())));
+ args.rval().setObject(*instance);
+ return true;
+}
+
+bool RateCounter::init_class(JSContext *cx, JS::HandleObject global) {
+ return BuiltinImpl::init_class_impl(cx, global);
+}
+
+} // namespace builtins
diff --git a/runtime/js-compute-runtime/builtins/edge-rate-limiter.h b/runtime/js-compute-runtime/builtins/edge-rate-limiter.h
new file mode 100644
index 0000000000..9d54801ff4
--- /dev/null
+++ b/runtime/js-compute-runtime/builtins/edge-rate-limiter.h
@@ -0,0 +1,32 @@
+#ifndef JS_COMPUTE_RUNTIME_EDGE_RATE_LIMITER_H
+#define JS_COMPUTE_RUNTIME_EDGE_RATE_LIMITER_H
+
+#include "builtin.h"
+#include "js-compute-builtins.h"
+
+namespace builtins {
+
+class RateCounter final : public BuiltinImpl {
+ static bool increment(JSContext *cx, unsigned argc, JS::Value *vp);
+ static bool lookupRate(JSContext *cx, unsigned argc, JS::Value *vp);
+ static bool lookupCount(JSContext *cx, unsigned argc, JS::Value *vp);
+
+public:
+ static constexpr const char *class_name = "RateCounter";
+ enum Slots { Name, Count };
+ static const JSFunctionSpec static_methods[];
+ static const JSPropertySpec static_properties[];
+ static const JSFunctionSpec methods[];
+ static const JSPropertySpec properties[];
+
+ static const unsigned ctor_length = 0;
+
+ static bool init_class(JSContext *cx, JS::HandleObject global);
+ static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
+
+ static JSString *get_name(JSObject *self);
+};
+
+} // namespace builtins
+
+#endif
diff --git a/runtime/js-compute-runtime/host_interface/component/fastly_world.c b/runtime/js-compute-runtime/host_interface/component/fastly_world.c
index d53f2b3678..d8f299cf23 100644
--- a/runtime/js-compute-runtime/host_interface/component/fastly_world.c
+++ b/runtime/js-compute-runtime/host_interface/component/fastly_world.c
@@ -444,6 +444,29 @@ typedef struct {
} val;
} fastly_world_result_u64_fastly_compute_at_edge_cache_error_t;
+typedef struct {
+ bool is_err;
+ union {
+ bool ok;
+ fastly_compute_at_edge_edge_rate_limiter_error_t err;
+ } val;
+} fastly_world_result_bool_fastly_compute_at_edge_edge_rate_limiter_error_t;
+
+typedef struct {
+ bool is_err;
+ union {
+ fastly_compute_at_edge_edge_rate_limiter_error_t err;
+ } val;
+} fastly_world_result_void_fastly_compute_at_edge_edge_rate_limiter_error_t;
+
+typedef struct {
+ bool is_err;
+ union {
+ uint32_t ok;
+ fastly_compute_at_edge_edge_rate_limiter_error_t err;
+ } val;
+} fastly_world_result_u32_fastly_compute_at_edge_edge_rate_limiter_error_t;
+
typedef struct {
bool is_err;
union {
@@ -546,6 +569,24 @@ void __wasm_import_fastly_compute_at_edge_dictionary_open(int32_t, int32_t, int3
__attribute__((__import_module__("fastly:compute-at-edge/dictionary"), __import_name__("get")))
void __wasm_import_fastly_compute_at_edge_dictionary_get(int32_t, int32_t, int32_t, int32_t);
+__attribute__((__import_module__("fastly:compute-at-edge/edge-rate-limiter"), __import_name__("check-rate")))
+void __wasm_import_fastly_compute_at_edge_edge_rate_limiter_check_rate(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
+
+__attribute__((__import_module__("fastly:compute-at-edge/edge-rate-limiter"), __import_name__("ratecounter-increment")))
+void __wasm_import_fastly_compute_at_edge_edge_rate_limiter_ratecounter_increment(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
+
+__attribute__((__import_module__("fastly:compute-at-edge/edge-rate-limiter"), __import_name__("ratecounter-lookup-rate")))
+void __wasm_import_fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_rate(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
+
+__attribute__((__import_module__("fastly:compute-at-edge/edge-rate-limiter"), __import_name__("ratecounter-lookup-count")))
+void __wasm_import_fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_count(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
+
+__attribute__((__import_module__("fastly:compute-at-edge/edge-rate-limiter"), __import_name__("penaltybox-add")))
+void __wasm_import_fastly_compute_at_edge_edge_rate_limiter_penaltybox_add(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
+
+__attribute__((__import_module__("fastly:compute-at-edge/edge-rate-limiter"), __import_name__("penaltybox-has")))
+void __wasm_import_fastly_compute_at_edge_edge_rate_limiter_penaltybox_has(int32_t, int32_t, int32_t, int32_t, int32_t);
+
__attribute__((__import_module__("fastly:compute-at-edge/geo"), __import_name__("lookup")))
void __wasm_import_fastly_compute_at_edge_geo_lookup(int32_t, int32_t, int32_t);
@@ -1710,6 +1751,164 @@ bool fastly_compute_at_edge_dictionary_get(fastly_compute_at_edge_dictionary_han
}
}
+bool fastly_compute_at_edge_edge_rate_limiter_check_rate(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t delta, uint32_t window, uint32_t limit, fastly_world_string_t *penalty_box_name, uint32_t time_to_live, bool *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ __attribute__((__aligned__(1)))
+ uint8_t ret_area[2];
+ int32_t ptr = (int32_t) &ret_area;
+ __wasm_import_fastly_compute_at_edge_edge_rate_limiter_check_rate((int32_t) (*rate_counter_name).ptr, (int32_t) (*rate_counter_name).len, (int32_t) (*entry).ptr, (int32_t) (*entry).len, (int32_t) (delta), (int32_t) (window), (int32_t) (limit), (int32_t) (*penalty_box_name).ptr, (int32_t) (*penalty_box_name).len, (int32_t) (time_to_live), ptr);
+ fastly_world_result_bool_fastly_compute_at_edge_edge_rate_limiter_error_t result;
+ switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
+ case 0: {
+ result.is_err = false;
+ result.val.ok = (int32_t) (*((uint8_t*) (ptr + 1)));
+ break;
+ }
+ case 1: {
+ result.is_err = true;
+ result.val.err = (int32_t) (*((uint8_t*) (ptr + 1)));
+ break;
+ }
+ }
+ if (!result.is_err) {
+ *ret = result.val.ok;
+ return 1;
+ } else {
+ *err = result.val.err;
+ return 0;
+ }
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_increment(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t delta, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ __attribute__((__aligned__(1)))
+ uint8_t ret_area[2];
+ int32_t ptr = (int32_t) &ret_area;
+ __wasm_import_fastly_compute_at_edge_edge_rate_limiter_ratecounter_increment((int32_t) (*rate_counter_name).ptr, (int32_t) (*rate_counter_name).len, (int32_t) (*entry).ptr, (int32_t) (*entry).len, (int32_t) (delta), ptr);
+ fastly_world_result_void_fastly_compute_at_edge_edge_rate_limiter_error_t result;
+ switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
+ case 0: {
+ result.is_err = false;
+ break;
+ }
+ case 1: {
+ result.is_err = true;
+ result.val.err = (int32_t) (*((uint8_t*) (ptr + 1)));
+ break;
+ }
+ }
+ if (!result.is_err) {
+ return 1;
+ } else {
+ *err = result.val.err;
+ return 0;
+ }
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_rate(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t window, uint32_t *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ __attribute__((__aligned__(4)))
+ uint8_t ret_area[8];
+ int32_t ptr = (int32_t) &ret_area;
+ __wasm_import_fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_rate((int32_t) (*rate_counter_name).ptr, (int32_t) (*rate_counter_name).len, (int32_t) (*entry).ptr, (int32_t) (*entry).len, (int32_t) (window), ptr);
+ fastly_world_result_u32_fastly_compute_at_edge_edge_rate_limiter_error_t result;
+ switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
+ case 0: {
+ result.is_err = false;
+ result.val.ok = (uint32_t) (*((int32_t*) (ptr + 4)));
+ break;
+ }
+ case 1: {
+ result.is_err = true;
+ result.val.err = (int32_t) (*((uint8_t*) (ptr + 4)));
+ break;
+ }
+ }
+ if (!result.is_err) {
+ *ret = result.val.ok;
+ return 1;
+ } else {
+ *err = result.val.err;
+ return 0;
+ }
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_count(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t duration, uint32_t *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ __attribute__((__aligned__(4)))
+ uint8_t ret_area[8];
+ int32_t ptr = (int32_t) &ret_area;
+ __wasm_import_fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_count((int32_t) (*rate_counter_name).ptr, (int32_t) (*rate_counter_name).len, (int32_t) (*entry).ptr, (int32_t) (*entry).len, (int32_t) (duration), ptr);
+ fastly_world_result_u32_fastly_compute_at_edge_edge_rate_limiter_error_t result;
+ switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
+ case 0: {
+ result.is_err = false;
+ result.val.ok = (uint32_t) (*((int32_t*) (ptr + 4)));
+ break;
+ }
+ case 1: {
+ result.is_err = true;
+ result.val.err = (int32_t) (*((uint8_t*) (ptr + 4)));
+ break;
+ }
+ }
+ if (!result.is_err) {
+ *ret = result.val.ok;
+ return 1;
+ } else {
+ *err = result.val.err;
+ return 0;
+ }
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_penaltybox_add(fastly_world_string_t *penalty_box_name, fastly_world_string_t *entry, uint32_t time_to_live, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ __attribute__((__aligned__(1)))
+ uint8_t ret_area[2];
+ int32_t ptr = (int32_t) &ret_area;
+ __wasm_import_fastly_compute_at_edge_edge_rate_limiter_penaltybox_add((int32_t) (*penalty_box_name).ptr, (int32_t) (*penalty_box_name).len, (int32_t) (*entry).ptr, (int32_t) (*entry).len, (int32_t) (time_to_live), ptr);
+ fastly_world_result_void_fastly_compute_at_edge_edge_rate_limiter_error_t result;
+ switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
+ case 0: {
+ result.is_err = false;
+ break;
+ }
+ case 1: {
+ result.is_err = true;
+ result.val.err = (int32_t) (*((uint8_t*) (ptr + 1)));
+ break;
+ }
+ }
+ if (!result.is_err) {
+ return 1;
+ } else {
+ *err = result.val.err;
+ return 0;
+ }
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_penaltybox_has(fastly_world_string_t *penalty_box_name, fastly_world_string_t *entry, bool *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ __attribute__((__aligned__(1)))
+ uint8_t ret_area[2];
+ int32_t ptr = (int32_t) &ret_area;
+ __wasm_import_fastly_compute_at_edge_edge_rate_limiter_penaltybox_has((int32_t) (*penalty_box_name).ptr, (int32_t) (*penalty_box_name).len, (int32_t) (*entry).ptr, (int32_t) (*entry).len, ptr);
+ fastly_world_result_bool_fastly_compute_at_edge_edge_rate_limiter_error_t result;
+ switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
+ case 0: {
+ result.is_err = false;
+ result.val.ok = (int32_t) (*((uint8_t*) (ptr + 1)));
+ break;
+ }
+ case 1: {
+ result.is_err = true;
+ result.val.err = (int32_t) (*((uint8_t*) (ptr + 1)));
+ break;
+ }
+ }
+ if (!result.is_err) {
+ *ret = result.val.ok;
+ return 1;
+ } else {
+ *err = result.val.err;
+ return 0;
+ }
+}
+
bool fastly_compute_at_edge_geo_lookup(fastly_world_list_u8_t *addr_octets, fastly_world_string_t *ret, fastly_compute_at_edge_geo_error_t *err) {
__attribute__((__aligned__(4)))
uint8_t ret_area[12];
diff --git a/runtime/js-compute-runtime/host_interface/component/fastly_world.h b/runtime/js-compute-runtime/host_interface/component/fastly_world.h
index 0f5e67d45a..537d00386d 100644
--- a/runtime/js-compute-runtime/host_interface/component/fastly_world.h
+++ b/runtime/js-compute-runtime/host_interface/component/fastly_world.h
@@ -570,6 +570,8 @@ typedef struct {
fastly_compute_at_edge_cache_handle_t f1;
} fastly_world_tuple2_fastly_compute_at_edge_cache_body_handle_fastly_compute_at_edge_cache_handle_t;
+typedef fastly_compute_at_edge_types_error_t fastly_compute_at_edge_edge_rate_limiter_error_t;
+
typedef fastly_compute_at_edge_http_types_request_t fastly_compute_at_edge_reactor_request_t;
// Imported Functions from `fastly:compute-at-edge/async-io`
@@ -703,6 +705,14 @@ bool fastly_compute_at_edge_cache_get_hits(fastly_compute_at_edge_cache_handle_t
bool fastly_compute_at_edge_dictionary_open(fastly_world_string_t *name, fastly_compute_at_edge_dictionary_handle_t *ret, fastly_compute_at_edge_dictionary_error_t *err);
bool fastly_compute_at_edge_dictionary_get(fastly_compute_at_edge_dictionary_handle_t h, fastly_world_string_t *key, fastly_world_option_string_t *ret, fastly_compute_at_edge_dictionary_error_t *err);
+// Imported Functions from `fastly:compute-at-edge/edge-rate-limiter`
+bool fastly_compute_at_edge_edge_rate_limiter_check_rate(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t delta, uint32_t window, uint32_t limit, fastly_world_string_t *penalty_box_name, uint32_t time_to_live, bool *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err);
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_increment(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t delta, fastly_compute_at_edge_edge_rate_limiter_error_t *err);
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_rate(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t window, uint32_t *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err);
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_count(fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t duration, uint32_t *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err);
+bool fastly_compute_at_edge_edge_rate_limiter_penaltybox_add(fastly_world_string_t *penalty_box_name, fastly_world_string_t *entry, uint32_t time_to_live, fastly_compute_at_edge_edge_rate_limiter_error_t *err);
+bool fastly_compute_at_edge_edge_rate_limiter_penaltybox_has(fastly_world_string_t *penalty_box_name, fastly_world_string_t *entry, bool *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err);
+
// Imported Functions from `fastly:compute-at-edge/geo`
// JSON string for now
bool fastly_compute_at_edge_geo_lookup(fastly_world_list_u8_t *addr_octets, fastly_world_string_t *ret, fastly_compute_at_edge_geo_error_t *err);
diff --git a/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp b/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp
index f07b1a3a11..78ad5cbc90 100644
--- a/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp
+++ b/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp
@@ -1247,3 +1247,30 @@ bool fastly_compute_at_edge_backend_is_healthy(fastly_world_string_t *backend,
*ret = convert_fastly_backend_health(fastly_backend_health);
return true;
}
+
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_increment(
+ fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t delta,
+ fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ return convert_result(fastly::ratecounter_increment(rate_counter_name->ptr,
+ rate_counter_name->len, entry->ptr,
+ entry->len, delta),
+ err);
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_rate(
+ fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t window,
+ uint32_t *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ return convert_result(fastly::ratecounter_lookup_rate(rate_counter_name->ptr,
+ rate_counter_name->len, entry->ptr,
+ entry->len, window, ret),
+ err);
+}
+
+bool fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_count(
+ fastly_world_string_t *rate_counter_name, fastly_world_string_t *entry, uint32_t duration,
+ uint32_t *ret, fastly_compute_at_edge_edge_rate_limiter_error_t *err) {
+ return convert_result(fastly::ratecounter_lookup_count(rate_counter_name->ptr,
+ rate_counter_name->len, entry->ptr,
+ entry->len, duration, ret),
+ err);
+}
diff --git a/runtime/js-compute-runtime/host_interface/component/fastly_world_component_type.o b/runtime/js-compute-runtime/host_interface/component/fastly_world_component_type.o
index fbe30d5753..f80f5bdb68 100644
Binary files a/runtime/js-compute-runtime/host_interface/component/fastly_world_component_type.o and b/runtime/js-compute-runtime/host_interface/component/fastly_world_component_type.o differ
diff --git a/runtime/js-compute-runtime/host_interface/fastly.h b/runtime/js-compute-runtime/host_interface/fastly.h
index 9cef95d49a..abef1004f7 100644
--- a/runtime/js-compute-runtime/host_interface/fastly.h
+++ b/runtime/js-compute-runtime/host_interface/fastly.h
@@ -513,6 +513,31 @@ int cache_get_age_ns(fastly_compute_at_edge_cache_handle_t handle, uint64_t *ret
WASM_IMPORT("fastly_cache", "get_hits")
int cache_get_hits(fastly_compute_at_edge_cache_handle_t handle, uint64_t *ret);
+WASM_IMPORT("fastly_erl", "check_rate")
+int check_rate(const char *rc, size_t rc_len, const char *entry, size_t entry_len, uint32_t delta,
+ uint32_t window, uint32_t limit, const char *pb, size_t pb_len, uint32_t ttl,
+ bool *blocked_out);
+
+WASM_IMPORT("fastly_erl", "ratecounter_increment")
+int ratecounter_increment(const char *rc, size_t rc_len, const char *entry, size_t entry_len,
+ uint32_t delta);
+
+WASM_IMPORT("fastly_erl", "ratecounter_lookup_rate")
+int ratecounter_lookup_rate(const char *rc, size_t rc_len, const char *entry, size_t entry_len,
+ uint32_t window, uint32_t *rate_out);
+
+WASM_IMPORT("fastly_erl", "ratecounter_lookup_count")
+int ratecounter_lookup_count(const char *rc, size_t rc_len, const char *entry, size_t entry_len,
+ uint32_t duration, uint32_t *count_out);
+
+WASM_IMPORT("fastly_erl", "penaltybox_add")
+int penaltybox_add(const char *pb, size_t pb_len, const char *entry, size_t entry_len,
+ uint32_t ttl);
+
+WASM_IMPORT("fastly_erl", "penaltybox_has")
+int penaltybox_has(const char *pb, size_t pb_len, const char *entry, size_t entry_len,
+ bool *has_out);
+
} // namespace fastly
#ifdef __cplusplus
}
diff --git a/runtime/js-compute-runtime/host_interface/host_api.cpp b/runtime/js-compute-runtime/host_interface/host_api.cpp
index a467811bf0..cf8b90be05 100644
--- a/runtime/js-compute-runtime/host_interface/host_api.cpp
+++ b/runtime/js-compute-runtime/host_interface/host_api.cpp
@@ -1791,4 +1791,52 @@ Result Backend::health(std::string_view name) {
return res;
}
+Result RateCounter::increment(std::string_view name, std::string_view entry, uint32_t delta) {
+ auto name_str = string_view_to_world_string(name);
+ auto entry_str = string_view_to_world_string(entry);
+ fastly_compute_at_edge_types_error_t err;
+ if (!fastly_compute_at_edge_edge_rate_limiter_ratecounter_increment(&name_str, &entry_str, delta,
+ &err)) {
+ return Result::err(err);
+ }
+
+ return Result::ok();
+}
+
+Result RateCounter::lookup_rate(std::string_view name, std::string_view entry,
+ uint32_t window) {
+ Result res;
+
+ auto name_str = string_view_to_world_string(name);
+ auto entry_str = string_view_to_world_string(entry);
+ uint32_t ret;
+ fastly_compute_at_edge_types_error_t err;
+ if (!fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_rate(&name_str, &entry_str,
+ window, &ret, &err)) {
+ res.emplace_err(err);
+ } else {
+ res.emplace(ret);
+ }
+
+ return res;
+}
+
+Result RateCounter::lookup_count(std::string_view name, std::string_view entry,
+ uint32_t duration) {
+ Result res;
+
+ auto name_str = string_view_to_world_string(name);
+ auto entry_str = string_view_to_world_string(entry);
+ uint32_t ret;
+ fastly_compute_at_edge_types_error_t err;
+ if (!fastly_compute_at_edge_edge_rate_limiter_ratecounter_lookup_count(&name_str, &entry_str,
+ duration, &ret, &err)) {
+ res.emplace_err(err);
+ } else {
+ res.emplace(ret);
+ }
+
+ return res;
+}
+
} // namespace host_api
diff --git a/runtime/js-compute-runtime/host_interface/host_api.h b/runtime/js-compute-runtime/host_interface/host_api.h
index b6605bff08..04b651f1aa 100644
--- a/runtime/js-compute-runtime/host_interface/host_api.h
+++ b/runtime/js-compute-runtime/host_interface/host_api.h
@@ -780,6 +780,16 @@ class Backend final {
static Result exists(std::string_view name);
static Result health(std::string_view name);
};
+
+class RateCounter final {
+public:
+ static Result increment(std::string_view name, std::string_view entry, uint32_t delta);
+ static Result lookup_rate(std::string_view name, std::string_view entry,
+ uint32_t window);
+ static Result lookup_count(std::string_view name, std::string_view entry,
+ uint32_t duration);
+};
+
} // namespace host_api
#endif
diff --git a/runtime/js-compute-runtime/host_interface/wit/deps/fastly/compute-at-edge.wit b/runtime/js-compute-runtime/host_interface/wit/deps/fastly/compute-at-edge.wit
index 927f535fbc..f29e2c4952 100644
--- a/runtime/js-compute-runtime/host_interface/wit/deps/fastly/compute-at-edge.wit
+++ b/runtime/js-compute-runtime/host_interface/wit/deps/fastly/compute-at-edge.wit
@@ -882,6 +882,22 @@ interface cache {
get-hits: func(handle: handle) -> result
}
+interface edge-rate-limiter {
+ use types.{error}
+
+ check-rate: func(rate-counter-name: string, entry: string, delta: u32, window: u32, limit: u32, penalty-box-name: string, time-to-live: u32) -> result
+
+ ratecounter-increment: func(rate-counter-name: string, entry: string, delta: u32) -> result<_, error>
+
+ ratecounter-lookup-rate: func(rate-counter-name: string, entry: string, window: u32) -> result
+
+ ratecounter-lookup-count: func(rate-counter-name: string, entry: string, duration: u32) -> result
+
+ penaltybox-add: func(penalty-box-name: string, entry: string, time-to-live: u32) -> result<_, error>
+
+ penaltybox-has: func(penalty-box-name: string, entry: string) -> result
+}
+
interface reactor {
use http-types.{request}
diff --git a/runtime/js-compute-runtime/host_interface/wit/js-compute-runtime.wit b/runtime/js-compute-runtime/host_interface/wit/js-compute-runtime.wit
index e4e015e6fc..29f6d56db9 100644
--- a/runtime/js-compute-runtime/host_interface/wit/js-compute-runtime.wit
+++ b/runtime/js-compute-runtime/host_interface/wit/js-compute-runtime.wit
@@ -6,6 +6,7 @@ world fastly-world {
import fastly:compute-at-edge/backend
import fastly:compute-at-edge/cache
import fastly:compute-at-edge/dictionary
+ import fastly:compute-at-edge/edge-rate-limiter
import fastly:compute-at-edge/geo
import fastly:compute-at-edge/http-body
import fastly:compute-at-edge/http-req
diff --git a/runtime/js-compute-runtime/js-compute-builtins.cpp b/runtime/js-compute-runtime/js-compute-builtins.cpp
index 13a22159af..856ab3c073 100644
--- a/runtime/js-compute-runtime/js-compute-builtins.cpp
+++ b/runtime/js-compute-runtime/js-compute-builtins.cpp
@@ -46,6 +46,7 @@
#include "builtins/crypto.h"
#include "builtins/decompression-stream.h"
#include "builtins/dictionary.h"
+#include "builtins/edge-rate-limiter.h"
#include "builtins/env.h"
#include "builtins/fastly.h"
#include "builtins/fetch-event.h"
@@ -1072,6 +1073,9 @@ bool define_fastly_sys(JSContext *cx, HandleObject global, FastlyOptions options
if (!builtins::Performance::create(cx, global)) {
return false;
}
+ if (!builtins::RateCounter::init_class(cx, global)) {
+ return false;
+ }
core::EventLoop::init(cx);
diff --git a/src/bundle.js b/src/bundle.js
index f4e845f95c..a8bf0daa55 100644
--- a/src/bundle.js
+++ b/src/bundle.js
@@ -16,6 +16,13 @@ let fastlyPlugin = {
case 'cache-override': { return { contents: `export const CacheOverride = globalThis.CacheOverride;` } }
case 'config-store': { return { contents: `export const ConfigStore = globalThis.ConfigStore;` } }
case 'dictionary': { return { contents: `export const Dictionary = globalThis.Dictionary;` } }
+ case 'edge-rate-limiter': {
+ return {
+ contents: `
+export const RateCounter = globalThis.RateCounter;
+`
+ }
+ }
case 'env': { return { contents: `export const env = globalThis.fastly.env.get;` } }
case 'experimental': {
return {
diff --git a/types/edge-rate-limiter.d.ts b/types/edge-rate-limiter.d.ts
new file mode 100644
index 0000000000..eff2c41092
--- /dev/null
+++ b/types/edge-rate-limiter.d.ts
@@ -0,0 +1,32 @@
+declare module "fastly:edge-rate-limiter" {
+
+ /**
+ * A rate counter that can be used with a EdgeRateLimiter or standalone for counting and rate calculations
+ */
+ export class RateCounter {
+ /**
+ * Open a RateCounter instance with the given name
+ * @param name The name of the rate-counter
+ */
+ constructor(name: string);
+ /**
+ * Increment the `entry` by `delta`
+ * @param entry The entry to increment
+ * @param delta The amount to increment the `entry` by
+ */
+ increment(entry: string, delta: number): void;
+ /**
+ * Lookup the current rate for entry for a given window
+ * @param entry The entry to lookup
+ * @param window The window to lookup alongside the entry, has to be either 1 second, 10 seconds, or 60 seconds
+ */
+ lookupRate(entry: string, window: 1 | 10 | 60): number;
+ /**
+ * Lookup the current count for entry for a given duration
+ * @param entry The entry to lookup
+ * @param duration The duration to lookup alongside the entry, has to be either, 10, 20, 30, 40, 50, or 60 seconds.
+ */
+ lookupCount(entry: string, duration: 10 | 20 | 30 | 40 | 50 | 60): number;
+ }
+
+}
diff --git a/types/index.d.ts b/types/index.d.ts
index 30844b5cf3..d7d1b567dc 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -4,6 +4,7 @@
///
///
///
+///
///
///
///