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 @@ /// /// /// +/// /// /// ///