From a73bd4b0353494dc80f910452ff394703c8d963e Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 May 2024 12:51:47 -0700 Subject: [PATCH] SimpleCache StarlingMonkey port (#767) --- .github/workflows/starlingmonkey.yml | 25 +- .../js-compute/fixtures/app/setup.js | 43 +- .../fixtures/app/src/cache-simple.js | 13 +- .../js-compute/fixtures/app/src/kv-store.js | 5 +- .../fixtures/app/tests-starlingmonkey.json | 92 +- .../js-compute/fixtures/app/tests.json | 2 +- integration-tests/js-compute/test.js | 4 +- runtime/fastly/CMakeLists.txt | 2 +- runtime/fastly/builtins/cache-override.h | 4 +- runtime/fastly/builtins/cache-simple.cpp | 786 ++++++++++++++++++ runtime/fastly/builtins/cache-simple.h | 60 ++ runtime/fastly/builtins/fastly.cpp | 85 +- runtime/fastly/builtins/fastly.h | 3 + runtime/fastly/builtins/fetch-event.cpp | 3 - .../builtins/fetch/request-response.cpp | 26 +- runtime/fastly/host-api/host_api_fastly.h | 13 +- .../js-compute-builtins.cpp | 18 +- 17 files changed, 1111 insertions(+), 73 deletions(-) create mode 100644 runtime/fastly/builtins/cache-simple.cpp create mode 100644 runtime/fastly/builtins/cache-simple.h diff --git a/.github/workflows/starlingmonkey.yml b/.github/workflows/starlingmonkey.yml index ff7fc04a41..f99581bb6c 100644 --- a/.github/workflows/starlingmonkey.yml +++ b/.github/workflows/starlingmonkey.yml @@ -36,6 +36,9 @@ jobs: sdktest: if: github.ref != 'refs/heads/main' runs-on: ubuntu-latest + strategy: + matrix: + platform: [viceroy, compute] needs: [build] steps: - name: Checkout fastly/js-compute-runtime @@ -52,7 +55,9 @@ jobs: cli_version: ${{ env.fastly-cli_version }} - name: Restore Viceroy from cache + if: ${{ matrix.platform == 'viceroy' }} uses: actions/cache@v3 + id: viceroy with: path: "/home/runner/.cargo/bin/viceroy" key: crate-cache-viceroy-${{ env.viceroy_version }} @@ -64,12 +69,9 @@ jobs: path: "/home/runner/.cargo/bin/wasm-tools" key: crate-cache-wasm-tools-${{ env.wasm-tools_version }} - - name: "Check wasm-tools has been restored" - if: steps.wasm-tools.outputs.cache-hit != 'true' - run: | - echo "wasm-tools was not restored from the cache" - echo "bailing out from the build early" - exit 1 + - name: "Check caches have been restored" + if: steps.wasm-tools.outputs.cache-hit != 'true' || matrix.platform == 'viceory' && steps.viceroy.outputs.cache-hit != 'true' + run: echo "Unable to restore from the cache, bailing." && exit 1 - name: Download Engine uses: actions/download-artifact@v3 @@ -78,11 +80,8 @@ jobs: - run: yarn install --frozen-lockfile - name: Yarn install - run: | - yarn - cd ./integration-tests/js-compute - yarn + run: yarn && cd ./integration-tests/js-compute && yarn - - run: | - cd ./integration-tests/js-compute - FASTLY_API_TOKEN=${{ secrets.FASTLY_API_TOKEN }} ./test.js --starlingmonkey --local + - run: node integration-tests/js-compute/test.js --starlingmonkey ${{ matrix.platform == 'viceroy' && '--local' || '' }} + env: + FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }} diff --git a/integration-tests/js-compute/fixtures/app/setup.js b/integration-tests/js-compute/fixtures/app/setup.js index 5f842c05e8..ed9e58c079 100755 --- a/integration-tests/js-compute/fixtures/app/setup.js +++ b/integration-tests/js-compute/fixtures/app/setup.js @@ -1,11 +1,15 @@ #!/usr/bin/env node import { $ as zx } from 'zx' +import { argv } from 'node:process' + +const serviceName = argv[2] +const starlingmonkey = argv.slice(2).includes('--starlingmonkey'); const startTime = Date.now(); -zx.verbose = false; if (process.env.FASTLY_API_TOKEN === undefined) { + zx.verbose = false; try { process.env.FASTLY_API_TOKEN = String(await zx`fastly profile token --quiet`).trim() } catch { @@ -13,6 +17,7 @@ if (process.env.FASTLY_API_TOKEN === undefined) { console.error('In order to run the tests, either create a fastly profile using `fastly profile create` or export a fastly token under the name FASTLY_API_TOKEN'); process.exit(1) } + zx.verbose = true; } async function setupConfigStores() { @@ -31,7 +36,12 @@ async function setupConfigStores() { process.env.STORE_ID = STORE_ID; } await zx`echo -n 'https://twitter.com/fastly' | fastly config-store-entry update --upsert --key twitter --store-id=$STORE_ID --stdin --token $FASTLY_API_TOKEN` - await zx`fastly resource-link create --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + try { + await zx`fastly resource-link create --service-name ${serviceName} --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + } catch (e) { + if (!e.message.includes('Duplicate record')) + throw e; + } STORE_ID = stores.find(({ name }) => name === 'testconfig')?.id if (!STORE_ID) { @@ -40,7 +50,12 @@ async function setupConfigStores() { process.env.STORE_ID = STORE_ID; } await zx`echo -n 'https://twitter.com/fastly' | fastly config-store-entry update --upsert --key twitter --store-id=$STORE_ID --stdin --token $FASTLY_API_TOKEN` - await zx`fastly resource-link create --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + try { + await zx`fastly resource-link create --service-name ${serviceName} --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + } catch (e) { + if (!e.message.includes('Duplicate record')) + throw e; + } } async function setupKVStore() { @@ -52,13 +67,20 @@ async function setupKVStore() { } }()) - const STORE_ID = stores.Data.find(({ Name }) => Name === 'example-test-kv-store')?.StoreID + const existing = stores.Data.find(({ Name }) => Name === `example-test-kv-store${starlingmonkey ? '-sm' : ''}`); + // For somereason the StarlingMonkey version of this contains "ID" instead of "StoreID" + const STORE_ID = existing?.StoreID || existing?.ID; if (!STORE_ID) { - process.env.STORE_ID = JSON.parse(await zx`fastly kv-store create --quiet --name='example-test-kv-store' --json --token $FASTLY_API_TOKEN`).id + process.env.STORE_ID = JSON.parse(await zx`fastly kv-store create --quiet --name='example-test-kv-store${starlingmonkey ? '-sm' : ''}' --json --token $FASTLY_API_TOKEN`).id } else { process.env.STORE_ID = STORE_ID; } - await zx`fastly resource-link create --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + try { + await zx`fastly resource-link create --service-name ${serviceName} --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + } catch (e) { + if (!e.message.includes('Duplicate record')) + throw e; + } } async function setupSecretStore() { @@ -78,13 +100,18 @@ async function setupSecretStore() { await zx`echo -n 'This is also some secret data' | fastly secret-store-entry create --recreate-allow --name first --store-id=$STORE_ID --stdin --token $FASTLY_API_TOKEN` let key = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' await zx`echo -n 'This is some secret data' | fastly secret-store-entry create --recreate-allow --name ${key} --store-id=$STORE_ID --stdin --token $FASTLY_API_TOKEN` - await zx`fastly resource-link create --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + try { + await zx`fastly resource-link create --service-name ${serviceName} --version latest --resource-id $STORE_ID --token $FASTLY_API_TOKEN --autoclone` + } catch (e) { + if (!e.message.includes('Duplicate record')) + throw e; + } } await setupConfigStores() await setupKVStore() await setupSecretStore() -await zx`fastly service-version activate --version latest --token $FASTLY_API_TOKEN` +await zx`fastly service-version activate --service-name ${serviceName} --version latest --token $FASTLY_API_TOKEN` console.log(`Set up has finished! Took ${(Date.now() - startTime) / 1000} seconds to complete`); diff --git a/integration-tests/js-compute/fixtures/app/src/cache-simple.js b/integration-tests/js-compute/fixtures/app/src/cache-simple.js index 5c33dcc7e8..bd5660cc87 100644 --- a/integration-tests/js-compute/fixtures/app/src/cache-simple.js +++ b/integration-tests/js-compute/fixtures/app/src/cache-simple.js @@ -2,12 +2,9 @@ /* eslint-env serviceworker */ import { pass, assert, assertDoesNotThrow, assertThrows, assertRejects, iteratableToStream, streamToString, assertResolves } from "./assertions.js"; -import { SimpleCache } from 'fastly:cache'; -import * as fastlyCache from 'fastly:cache'; +import { SimpleCache, SimpleCacheEntry } from 'fastly:cache'; import { routes, isRunningLocally } from "./routes.js"; -const { SimpleCacheEntry } = fastlyCache; - let error; routes.set("/simple-cache/interface", () => { let actual = Reflect.ownKeys(SimpleCache) @@ -255,7 +252,7 @@ routes.set("/simple-cache/interface", () => { if (!isRunningLocally()) { error = assertThrows(() => { new SimpleCache.purge('1', { scope: "global" }) - }, TypeError, `SimpleCache.purge is not a constructor`) + }, TypeError) if (error) { return error } } return pass() @@ -373,7 +370,7 @@ routes.set("/simple-cache/interface", () => { if (!isRunningLocally()) { error = assertThrows(() => { new SimpleCache.set('1', 'meow', 1) - }, TypeError, `SimpleCache.set is not a constructor`) + }, TypeError) if (error) { return error } } return pass() @@ -818,7 +815,7 @@ routes.set("/simple-cache/interface", () => { if (!isRunningLocally()) { let error = assertThrows(() => { new SimpleCache.get('1') - }, TypeError, `SimpleCache.get is not a constructor`) + }, TypeError) if (error) { return error } } return pass() @@ -1188,7 +1185,7 @@ async function simpleCacheEntryInterfaceTests() { ttl: 10 } }); - }, TypeError, `SimpleCache.getOrSet is not a constructor`) + }, TypeError) if (error) { return error } } return pass() diff --git a/integration-tests/js-compute/fixtures/app/src/kv-store.js b/integration-tests/js-compute/fixtures/app/src/kv-store.js index 8b3fbede37..7e3d686141 100644 --- a/integration-tests/js-compute/fixtures/app/src/kv-store.js +++ b/integration-tests/js-compute/fixtures/app/src/kv-store.js @@ -1,8 +1,11 @@ /* globals KVStoreEntry */ import { pass, assert, assertThrows, assertRejects, assertResolves } from "./assertions.js"; import { KVStore } from "fastly:kv-store"; +import { sdkVersion } from "fastly:experimental"; import { routes, isRunningLocally } from "./routes.js"; +const starlingmonkey = sdkVersion.includes('starlingmonkey'); + // KVStore { routes.set("/kv-store/exposed-as-global", async () => { @@ -1286,7 +1289,7 @@ async function kvStoreInterfaceTests() { } function createValidStore() { - return new KVStore('example-test-kv-store') + return new KVStore(`example-test-kv-store${starlingmonkey ? '-sm' : ''}`) } function iteratableToStream(iterable) { diff --git a/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json b/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json index 23a5756db2..128fdc2e62 100644 --- a/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json +++ b/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json @@ -8,12 +8,90 @@ "GET /cache-override/constructor/invalid-mode", "GET /cache-override/constructor/valid-mode", "GET /cache-override/fetch/mode-none", - "GET /cache-override/fetch/mode-pass", - "GET /client/tlsJA3MD5", - "GET /client/tlsClientHello", - "GET /client/tlsClientCertificate", - "GET /client/tlsCipherOpensslName", - "GET /client/tlsProtocol", + "GET /simple-cache/interface", + "GET /simple-store/constructor/called-as-regular-function", + "GET /simple-cache/constructor/throws", + "GET /simple-cache/purge/called-as-constructor", + "GET /simple-cache/purge/key-parameter-calls-7.1.17-ToString", + "GET /simple-cache/purge/key-parameter-not-supplied", + "GET /simple-cache/purge/key-parameter-empty-string", + "GET /simple-cache/purge/key-parameter-8135-character-string", + "GET /simple-cache/purge/key-parameter-8136-character-string", + "GET /simple-cache/purge/options-parameter", + "GET /simple-cache/purge/returns-undefined", + "GET /simple-cache/set/called-as-constructor", + "GET /simple-cache/set/key-parameter-calls-7.1.17-ToString", + "GET /simple-cache/set/tll-parameter-7.1.4-ToNumber", + "GET /simple-cache/set/no-parameters-supplied", + "GET /simple-cache/set/key-parameter-empty-string", + "GET /simple-cache/set/key-parameter-8135-character-string", + "GET /simple-cache/set/key-parameter-8136-character-string", + "GET /simple-cache/set/ttl-parameter-negative-number", + "GET /simple-cache/set/ttl-parameter-NaN", + "GET /simple-cache/set/ttl-parameter-Infinity", + "GET /simple-cache/set/value-parameter-as-undefined", + "GET /simple-cache/set/value-parameter-readablestream-missing-length-parameter", + "GET /simple-cache/set/value-parameter-readablestream-negative-length-parameter", + "GET /simple-cache/set/value-parameter-readablestream-nan-length-parameter", + "GET /simple-cache/set/value-parameter-readablestream-negative-infinity-length-parameter", + "GET /simple-cache/set/value-parameter-readablestream-positive-infinity-length-parameter", + "GET /simple-cache/set/length-parameter-7.1.4-ToNumber", + "GET /simple-cache/set/value-parameter-readablestream-empty", + "GET /simple-cache/set/value-parameter-readablestream-locked", + "GET /simple-cache/set/value-parameter-readablestream", + "GET /simple-cache/set/value-parameter-URLSearchParams", + "GET /simple-cache/set/value-parameter-strings", + "GET /simple-cache/set/value-parameter-calls-7.1.17-ToString", + "GET /simple-cache/set/value-parameter-buffer", + "GET /simple-cache/set/value-parameter-arraybuffer", + "GET /simple-cache/set/value-parameter-typed-arrays", + "GET /simple-cache/set/value-parameter-dataview", + "GET /simple-cache/set/returns-undefined", + "GET /simple-cache/get/called-as-constructor", + "GET /simple-cache/get/key-parameter-calls-7.1.17-ToString", + "GET /simple-cache/get/key-parameter-not-supplied", + "GET /simple-cache/get/key-parameter-empty-string", + "GET /simple-cache/get/key-parameter-8135-character-string", + "GET /simple-cache/get/key-parameter-8136-character-string", + "GET /simple-cache/get/key-does-not-exist-returns-null", + "GET /simple-cache/get/key-exists", + "GET /simple-cache-entry/interface", + "GET /simple-cache-entry/text/valid", + "GET /simple-cache-entry/json/valid", + "GET /simple-cache-entry/json/invalid", + "GET /simple-cache-entry/arrayBuffer/valid", + "GET /simple-cache-entry/bodyUsed", + "GET /simple-cache-entry/readablestream", + "GET /simple-cache/getOrSet/called-as-constructor", + "GET /simple-cache/getOrSet/no-parameters-supplied", + "GET /simple-cache/getOrSet/key-parameter-calls-7.1.17-ToString", + "GET /simple-cache/getOrSet/key-parameter-empty-string", + "GET /simple-cache/getOrSet/key-parameter-8135-character-string", + "GET /simple-cache/getOrSet/key-parameter-8136-character-string", + "GET /simple-cache/getOrSet/ttl-field-7.1.4-ToNumber", + "GET /simple-cache/getOrSet/ttl-field-negative-number", + "GET /simple-cache/getOrSet/ttl-field-NaN", + "GET /simple-cache/getOrSet/ttl-field-Infinity", + "GET /simple-cache/getOrSet/value-field-as-undefined", + "GET /simple-cache/getOrSet/value-field-readablestream-missing-length-field", + "GET /simple-cache/getOrSet/value-field-readablestream-negative-length-field", + "GET /simple-cache/getOrSet/value-field-readablestream-nan-length-field", + "GET /simple-cache/getOrSet/value-field-readablestream-negative-infinity-length-field", + "GET /simple-cache/getOrSet/value-field-readablestream-positive-infinity-length-field", + "GET /simple-cache/getOrSet/length-field-7.1.4-ToNumber", + "GET /simple-cache/getOrSet/value-field-readablestream-empty", + "GET /simple-cache/getOrSet/value-field-readablestream-locked", + "GET /simple-cache/getOrSet/value-field-readablestream", + "GET /simple-cache/getOrSet/value-field-URLSearchParams", + "GET /simple-cache/getOrSet/value-field-strings", + "GET /simple-cache/getOrSet/value-field-calls-7.1.17-ToString", + "GET /simple-cache/getOrSet/value-field-buffer", + "GET /simple-cache/getOrSet/value-field-typed-arrays", + "GET /simple-cache/getOrSet/value-field-dataview", + "GET /simple-cache/getOrSet/returns-SimpleCacheEntry", + "GET /simple-cache/getOrSet/executes-the-set-method-when-key-not-in-cache", + "GET /simple-cache/getOrSet/does-not-execute-the-set-method-when-key-is-in-cache", + "GET /simple-cache/getOrSet/does-not-freeze-when-called-after-a-get", "GET /console", "GET /crypto", "GET /crypto.subtle", @@ -235,8 +313,6 @@ "GET /error", "GET /override-content-length/request/init/object-literal/true", "GET /override-content-length/request/init/object-literal/false", - "GET /override-content-length/request/clone/true", - "GET /override-content-length/request/clone/false", "GET /override-content-length/fetch/init/object-literal/true", "GET /override-content-length/fetch/init/object-literal/false", "GET /override-content-length/response/init/object-literal/true", diff --git a/integration-tests/js-compute/fixtures/app/tests.json b/integration-tests/js-compute/fixtures/app/tests.json index 5fe9ef6a9e..a373361d3c 100644 --- a/integration-tests/js-compute/fixtures/app/tests.json +++ b/integration-tests/js-compute/fixtures/app/tests.json @@ -588,7 +588,7 @@ } }, "GET /simple-cache-entry/interface": { - "environments": ["compute"], + "environments": ["compute", "viceroy"], "downstream_request": { "method": "GET", "pathname": "/simple-cache-entry/interface" diff --git a/integration-tests/js-compute/test.js b/integration-tests/js-compute/test.js index 4aeb1f03fa..8a89c255d2 100755 --- a/integration-tests/js-compute/test.js +++ b/integration-tests/js-compute/test.js @@ -58,7 +58,7 @@ zx.verbose = true; const branchName = (await zx`git branch --show-current`).stdout.trim().replace(/[^a-zA-Z0-9_-]/g, '_') const fixture = 'app'; -const serviceName = `${fixture}--${branchName}` +const serviceName = `${fixture}--${branchName}${starlingmonkey ? '--sm' : ''}` let domain; const fixturePath = join(__dirname, 'fixtures', fixture) let localServer; @@ -91,7 +91,7 @@ if (!local) { const setupPath = join(fixturePath, 'setup.js') if (existsSync(setupPath)) { core.startGroup('Extra set-up steps for the service') - await zx`${setupPath}${starlingmonkey ? ' --starlingmonkey' : ''}` + await zx`node ${setupPath} ${serviceName} ${starlingmonkey ? '--starlingmonkey' : ''}` await sleep(60) core.endGroup() } diff --git a/runtime/fastly/CMakeLists.txt b/runtime/fastly/CMakeLists.txt index c9083a7f8f..1298203ccb 100644 --- a/runtime/fastly/CMakeLists.txt +++ b/runtime/fastly/CMakeLists.txt @@ -3,11 +3,11 @@ cmake_minimum_required(VERSION 3.27) include("../StarlingMonkey/cmake/add_as_subproject.cmake") add_builtin(fastly::runtime SRC handler.cpp host-api/component/fastly_world_adapter.cpp) +add_builtin(fastly::cache_simple SRC builtins/cache-simple.cpp DEPENDENCIES OpenSSL) add_builtin(fastly::fastly SRC builtins/fastly.cpp) add_builtin(fastly::backend SRC builtins/backend.cpp) add_builtin(fastly::fetch SRC builtins/fetch/fetch.cpp builtins/fetch/request-response.cpp builtins/fetch/headers.cpp) add_builtin(fastly::cache_override SRC builtins/cache-override.cpp) add_builtin(fastly::fetch_event SRC builtins/fetch-event.cpp) -add_builtin(fastly::cache_simple SRC builtins/cache-simple.cpp DEPENDENCIES OpenSSL) project(FastlyJS) diff --git a/runtime/fastly/builtins/cache-override.h b/runtime/fastly/builtins/cache-override.h index cf34cf9072..0fbebfadea 100644 --- a/runtime/fastly/builtins/cache-override.h +++ b/runtime/fastly/builtins/cache-override.h @@ -1,5 +1,5 @@ -#ifndef JS_COMPUTE_RUNTIME_CACHE_OVERRIDE_H -#define JS_COMPUTE_RUNTIME_CACHE_OVERRIDE_H +#ifndef FASTLY_CACHE_OVERRIDE_H +#define FASTLY_CACHE_OVERRIDE_H #include "../host-api/host_api_fastly.h" #include "builtin.h" diff --git a/runtime/fastly/builtins/cache-simple.cpp b/runtime/fastly/builtins/cache-simple.cpp new file mode 100644 index 0000000000..c74af35167 --- /dev/null +++ b/runtime/fastly/builtins/cache-simple.cpp @@ -0,0 +1,786 @@ +#include "cache-simple.h" +#include "../../StarlingMonkey/builtins/web/streams/native-stream-source.h" +#include "../../StarlingMonkey/builtins/web/url.h" +#include "../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "builtin.h" +#include "fastly.h" +#include "js/ArrayBuffer.h" +#include "js/Result.h" +#include "js/Stream.h" +#include "openssl/evp.h" +#include + +using builtins::web::streams::NativeStreamSource; +using fastly::fastly::convertBodyInit; +using fastly::fastly::FastlyGetErrorMessage; + +namespace fastly::cache_simple { + +template +bool SimpleCacheEntry::bodyAll(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + return RequestOrResponse::bodyAll(cx, args, self); +} + +bool SimpleCacheEntry::body_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + if (!JS::GetReservedSlot(self, static_cast(Slots::HasBody)).isBoolean()) { + JS::SetReservedSlot(self, static_cast(Slots::HasBody), JS::BooleanValue(false)); + } + return RequestOrResponse::body_get(cx, args, self, true); +} + +bool SimpleCacheEntry::bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + if (!JS::GetReservedSlot(self, static_cast(Slots::BodyUsed)).isBoolean()) { + JS::SetReservedSlot(self, static_cast(Slots::BodyUsed), JS::BooleanValue(false)); + } + args.rval().setBoolean(RequestOrResponse::body_used(self)); + return true; +} + +const JSFunctionSpec SimpleCacheEntry::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec SimpleCacheEntry::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec SimpleCacheEntry::methods[] = { + JS_FN("arrayBuffer", bodyAll, 0, + JSPROP_ENUMERATE), + JS_FN("json", bodyAll, 0, JSPROP_ENUMERATE), + JS_FN("text", bodyAll, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec SimpleCacheEntry::properties[] = { + JS_PSG("body", body_get, JSPROP_ENUMERATE), + JS_PSG("bodyUsed", bodyUsed_get, JSPROP_ENUMERATE), + JS_STRING_SYM_PS(toStringTag, "SimpleCacheEntry", JSPROP_READONLY), + JS_PS_END, +}; + +bool SimpleCacheEntry::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorUTF8(cx, "SimpleCacheEntry can't be instantiated directly"); + return false; +} + +JSObject *SimpleCacheEntry::create(JSContext *cx, host_api::HttpBody body_handle) { + JS::RootedObject SimpleCacheEntry(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!SimpleCacheEntry) + return nullptr; + + JS::SetReservedSlot(SimpleCacheEntry, static_cast(Slots::Body), + JS::Int32Value(body_handle.handle)); + JS::SetReservedSlot(SimpleCacheEntry, static_cast(Slots::BodyStream), JS::NullValue()); + JS::SetReservedSlot(SimpleCacheEntry, static_cast(Slots::HasBody), + JS::BooleanValue(true)); + JS::SetReservedSlot(SimpleCacheEntry, static_cast(Slots::BodyUsed), JS::FalseValue()); + + return SimpleCacheEntry; +} + +namespace { +// Purging/Deleting a cache item within the Compute SDKs via a hostcall is only +// possible via surrogate-keys. We add a surrogate key to all the cache entries, +// which is the sha-256 digest of the cache entries cache-key, converted to +// uppercase hexadecimal. +// Note: We should keep this consistent across the Compute SDKs, this would allow +// a Compute Service to move from one SDK to another, and have consistent purging +// behavior between the Compute Service Versions which were using a different SDK. +JS::Result createGlobalSurrogateKeyFromCacheKey(JSContext *cx, + std::string_view cache_key) { + const EVP_MD *algorithm = EVP_sha256(); + unsigned int size = EVP_MD_size(algorithm); + std::vector md(size); + + if (!EVP_Digest(cache_key.data(), cache_key.size(), md.data(), &size, algorithm, nullptr)) { + return JS::Result(JS::Error()); + } + JS::UniqueChars data{OPENSSL_buf2hexstr(md.data(), size)}; + std::string surrogate_key{data.get(), std::remove(data.get(), data.get() + size, ':')}; + + return JS::Result(surrogate_key); +} + +// Purging/Deleting a cache item within the Compute SDKs via a hostcall is only +// possible via surrogate-keys. We add a surrogate key to all the cache entries, +// which is the sha-256 digest of the cache entries cache-key and the FASTLY_POP +// environment variable, converted to uppercase hexadecimal. +// Note: We should keep this consistent across the Compute SDKs, this would allow +// a Compute Service to move from one SDK to another, and have consistent purging +// behavior between the Compute Service Versions which were using a different SDK. +JS::Result createPopSurrogateKeyFromCacheKey(JSContext *cx, + std::string_view cache_key) { + const EVP_MD *algorithm = EVP_sha256(); + unsigned int size = EVP_MD_size(algorithm); + std::vector md(size); + + std::string key{cache_key}; + auto pop = getenv("FASTLY_POP"); + if (pop) { + key += pop; + } + + // TODO: use the incremental Digest api instead of allocating a string + if (!EVP_Digest(key.c_str(), key.length(), md.data(), &size, algorithm, nullptr)) { + return JS::Result(JS::Error()); + } + JS::UniqueChars data{OPENSSL_buf2hexstr(md.data(), size)}; + std::string surrogate_key{data.get(), std::remove(data.get(), data.get() + size, ':')}; + + return JS::Result(surrogate_key); +} + +// Create all the surrogate keys for the cache key +JS::Result createSurrogateKeysFromCacheKey(JSContext *cx, std::string_view cache_key) { + const EVP_MD *algorithm = EVP_sha256(); + unsigned int size = EVP_MD_size(algorithm); + std::vector md(size); + + if (!EVP_Digest(cache_key.data(), cache_key.size(), md.data(), &size, algorithm, nullptr)) { + return JS::Result(JS::Error()); + } + JS::UniqueChars data{OPENSSL_buf2hexstr(md.data(), size)}; + std::string surrogate_keys{data.get(), std::remove(data.get(), data.get() + size, ':')}; + + if (auto *pop = getenv("FASTLY_POP")) { + // TODO: use the incremental Digest api instead of allocating a string + std::string key{cache_key}; + key += pop; + if (!EVP_Digest(key.c_str(), key.length(), md.data(), &size, algorithm, nullptr)) { + return JS::Result(JS::Error()); + } + JS::UniqueChars data{OPENSSL_buf2hexstr(md.data(), size)}; + surrogate_keys.push_back(' '); + surrogate_keys.append(data.get(), std::remove(data.get(), data.get() + size, ':')); + } + + return JS::Result(surrogate_keys); +} + +#define BEGIN_TRANSACTION(t, cx, promise, handle) \ + CacheTransaction t{cx, promise, handle, __func__, __LINE__}; + +class CacheTransaction final { + JSContext *cx; + JS::RootedObject promise; + host_api::CacheHandle handle; + + const char *func; + int line; + +public: + CacheTransaction(JSContext *cx, JS::HandleObject promise, host_api::CacheHandle handle, + const char *func, const int line) + : cx{cx}, promise{this->cx, promise}, handle{handle}, func{func}, line{line} {}; + + ~CacheTransaction() { + // An invalid handle indicates that this transaction has been committed. + if (!this->handle.is_valid()) { + return; + } + + auto res = this->handle.transaction_cancel(); + if (auto *err = res.to_err()) { + host_api::handle_fastly_error(this->cx, *err, this->line, this->func); + } + + // We always reject the promise if the transaction hasn't committed. + RejectPromiseWithPendingError(this->cx, this->promise); + } + + /// Commit this transaction. + void commit() { + // Invalidate the handle to indicate that the transaction has been committed. + MOZ_ASSERT(this->handle.is_valid()); + this->handle = host_api::CacheHandle{}; + MOZ_ASSERT(!this->handle.is_valid()); + } +}; + +} // namespace + +bool SimpleCache::getOrSetThenHandler(JSContext *cx, JS::HandleObject owner, JS::HandleValue extra, + JS::CallArgs args) { + MOZ_ASSERT(extra.isObject()); + JS::RootedObject extraObj(cx, &extra.toObject()); + JS::RootedValue handleVal(cx); + JS::RootedValue promiseVal(cx); + if (!JS_GetProperty(cx, extraObj, "promise", &promiseVal)) { + return false; + } + MOZ_ASSERT(promiseVal.isObject()); + JS::RootedObject promise(cx, &promiseVal.toObject()); + if (!promise) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + if (!JS_GetProperty(cx, extraObj, "handle", &handleVal)) { + return RejectPromiseWithPendingError(cx, promise); + } + MOZ_ASSERT(handleVal.isInt32()); + + host_api::CacheHandle handle(handleVal.toInt32()); + + BEGIN_TRANSACTION(transaction, cx, promise, handle); + + JS::RootedValue keyVal(cx); + if (!JS_GetProperty(cx, extraObj, "key", &keyVal)) { + return false; + } + + auto arg0 = args.get(0); + if (!arg0.isObject()) { + JS_ReportErrorASCII(cx, "SimpleCache.getOrSet: does not adhere to interface {value: BodyInit, " + "ttl: number, length?:number}"); + return false; + } + JS::RootedObject insertionObject(cx, &arg0.toObject()); + + JS::RootedValue ttl_val(cx); + if (!JS_GetProperty(cx, insertionObject, "ttl", &ttl_val)) { + return false; + } + // Convert ttl (time-to-live) field into a number and check the value adheres to our + // validation rules. + double ttl; + if (!JS::ToNumber(cx, ttl_val, &ttl)) { + return false; + } + if (ttl < 0 || std::isnan(ttl) || std::isinf(ttl)) { + JS_ReportErrorASCII( + cx, "SimpleCache.getOrSet: TTL field is an invalid value, only positive numbers can " + "be used for TTL values."); + return false; + } + host_api::CacheWriteOptions options; + // turn second representation into nanosecond representation + options.max_age_ns = JS::ToUint64(ttl) * 1'000'000'000; + + JS::RootedValue body_val(cx); + if (!JS_GetProperty(cx, insertionObject, "value", &body_val)) { + return false; + } + + host_api::HttpBody source_body; + JS::UniqueChars buf; + JS::RootedObject body_obj(cx, body_val.isObject() ? &body_val.toObject() : nullptr); + // If the body is a Host-backed ReadableStream we optimise our implementation + // by using the ReadableStream's handle directly. + if (body_obj && JS::IsReadableStream(body_obj)) { + if (RequestOrResponse::body_unusable(cx, body_obj)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_READABLE_STREAM_LOCKED_OR_DISTRUBED); + return false; + } + + // If the stream is backed by a Fastly Compute body handle, we can use that handle directly. + if (NativeStreamSource::stream_is_body(cx, body_obj)) { + JS::RootedObject stream_source(cx, NativeStreamSource::get_stream_source(cx, body_obj)); + JS::RootedObject source_owner(cx, NativeStreamSource::owner(stream_source)); + source_body = RequestOrResponse::body_handle(source_owner); + } else { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_SIMPLE_CACHE_SET_CONTENT_STREAM); + return false; + } + + // The cache APIs require the length to be known upfront, we don't know the length of a + // stream upfront, which means the caller will need to supply the information explicitly for us. + bool found; + if (!JS_HasProperty(cx, insertionObject, "length", &found)) { + return false; + } + if (found) { + + JS::RootedValue length_val(cx); + if (!JS_GetProperty(cx, insertionObject, "length", &length_val)) { + return false; + } + double number; + if (!JS::ToNumber(cx, length_val, &number)) { + return false; + } + if (number < 0 || std::isnan(number) || std::isinf(number)) { + JS_ReportErrorASCII( + cx, + "SimpleCache.getOrSet: length property is an invalid value, only positive numbers can " + "be used for length values."); + return false; + } + options.length = JS::ToInteger(number); + } + } else { + auto result = convertBodyInit(cx, body_val); + if (result.isErr()) { + return false; + } + std::tie(buf, options.length) = result.unwrap(); + } + + // We create a surrogate-key from the cache-key, as this allows the cached contents to be purgable + // from within the JavaScript application + // This is because the cache API currently only supports purging via surrogate-key + auto key_chars = core::encode(cx, keyVal); + if (!key_chars) { + return false; + } + auto key_result = createSurrogateKeysFromCacheKey(cx, key_chars); + if (key_result.isErr()) { + return false; + } + options.surrogate_keys = key_result.inspect(); + + auto inserted_res = handle.transaction_insert_and_stream_back(options); + if (auto *err = inserted_res.to_err()) { + return false; + } + + auto [body, inserted_handle] = inserted_res.unwrap(); + if (!body.valid()) { + return false; + } + // source_body will only be valid when the body is a Host-backed ReadableStream + if (source_body.valid()) { + auto res = body.append(source_body); + if (auto *error = res.to_err()) { + return false; + } + } else { + auto write_res = body.write_all_back(reinterpret_cast(buf.get()), options.length); + if (auto *error = write_res.to_err()) { + return false; + } + auto close_res = body.close(); + if (auto *error = close_res.to_err()) { + return false; + } + } + + auto res = inserted_handle.get_body(host_api::CacheGetBodyOptions{}); + if (auto *err = res.to_err()) { + return false; + } + + JS::RootedObject entry(cx, SimpleCacheEntry::create(cx, res.unwrap())); + if (!entry) { + return false; + } + + transaction.commit(); + + JS::RootedValue result(cx); + result.setObject(*entry); + JS::ResolvePromise(cx, promise, result); + return true; +} + +// static getOrSet(key: string, set: () => Promise<{value: BodyInit, ttl: number}>): +// SimpleCacheEntry | null; static getOrSet(key: string, set: () => Promise<{value: ReadableStream, +// ttl: number, length: number}>): SimpleCacheEntry | null; +bool SimpleCache::getOrSet(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The SimpleCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "SimpleCache.getOrSet", 2)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key_chars = core::encode(cx, args.get(0)); + if (!key_chars) { + return false; + } + + if (key_chars.len == 0) { + JS_ReportErrorASCII(cx, "SimpleCache.getOrSet: key can not be an empty string"); + return false; + } + if (key_chars.len > 8135) { + JS_ReportErrorASCII( + cx, "SimpleCache.getOrSet: key is too long, the maximum allowed length is 8135."); + return false; + } + + JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); + if (!promise) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + auto res = host_api::CacheHandle::transaction_lookup(key_chars, host_api::CacheLookupOptions{}); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto handle = res.unwrap(); + BEGIN_TRANSACTION(transaction, cx, promise, handle); + + // Check if a fresh cache item was found, if that's the case, then we will resolve + // with a SimpleCacheEntry containing the value. Else, call the content-provided + // function in the `set` parameter and insert it's returned value property into the + // cache under the provided `key`, and then we will resolve with a SimpleCacheEntry + // containing the value. + auto state_res = handle.get_state(); + if (auto *err = state_res.to_err()) { + return false; + } + + auto state = state_res.unwrap(); + args.rval().setObject(*promise); + if (state.is_usable()) { + auto body_res = handle.get_body(host_api::CacheGetBodyOptions{}); + if (auto *err = body_res.to_err()) { + return false; + } + + JS::RootedObject entry(cx, SimpleCacheEntry::create(cx, body_res.unwrap())); + if (!entry) { + return false; + } + + JS::RootedValue result(cx); + result.setObject(*entry); + JS::ResolvePromise(cx, promise, result); + return true; + } else { + auto arg1 = args.get(1); + if (!arg1.isObject() || !JS::IsCallable(&arg1.toObject())) { + JS_ReportErrorLatin1(cx, "SimpleCache.getOrSet: set argument is not a function"); + return false; + } + JS::RootedValueArray<0> fnargs(cx); + JS::RootedObject fn(cx, &arg1.toObject()); + JS::RootedValue result(cx); + if (!JS::Call(cx, JS::NullHandleValue, fn, fnargs, &result)) { + return false; + } + // Coercion of `result` to a Promise + JS::RootedObject result_promise(cx, JS::CallOriginalPromiseResolve(cx, result)); + if (!result_promise) { + return false; + } + + // JS::RootedObject owner(cx, JS_NewPlainObject(cx)); + JS::RootedObject extraObj(cx, JS_NewPlainObject(cx)); + JS::RootedValue handleVal(cx, JS::NumberValue(handle.handle)); + if (!JS_SetProperty(cx, extraObj, "handle", handleVal)) { + return false; + } + JS::RootedValue keyVal( + cx, JS::StringValue(JS_NewStringCopyN(cx, key_chars.begin(), key_chars.len))); + if (!JS_SetProperty(cx, extraObj, "key", keyVal)) { + return false; + } + JS::RootedValue promiseVal(cx, JS::ObjectValue(*promise)); + if (!JS_SetProperty(cx, extraObj, "promise", promiseVal)) { + return false; + } + + JS::RootedValue extra(cx, JS::ObjectValue(*extraObj)); + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + JS::RootedObject then_handler(cx, + create_internal_method(cx, global, extra)); + if (!then_handler) { + return false; + } + if (!JS::AddPromiseReactions(cx, result_promise, then_handler, nullptr)) { + return false; + } + transaction.commit(); + return true; + } +} + +// static set(key: string, value: BodyInit, ttl: number): undefined; +// static set(key: string, value: ReadableStream, ttl: number, length: number): undefined; +bool SimpleCache::set(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The SimpleCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "SimpleCache.set", 3)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key = core::encode(cx, args.get(0)); + if (!key) { + return false; + } + + if (key.len == 0) { + JS_ReportErrorASCII(cx, "SimpleCache.set: key can not be an empty string"); + return false; + } + if (key.len > 8135) { + JS_ReportErrorASCII(cx, + "SimpleCache.set: key is too long, the maximum allowed length is 8135."); + return false; + } + + host_api::CacheWriteOptions options; + // Convert ttl (time-to-live) parameter into a number and check the value adheres to our + // validation rules. + JS::HandleValue ttl_val = args.get(2); + double ttl; + if (!JS::ToNumber(cx, ttl_val, &ttl)) { + return false; + } + if (ttl < 0 || std::isnan(ttl) || std::isinf(ttl)) { + JS_ReportErrorASCII( + cx, "SimpleCache.set: TTL parameter is an invalid value, only positive numbers can " + "be used for TTL values."); + return false; + } + options.max_age_ns = JS::ToUint64(ttl) * + 1'000'000'000; // turn second representation into nanosecond representation + + JS::HandleValue body_val = args.get(1); + host_api::HttpBody source_body; + JS::UniqueChars buf; + JS::RootedObject body_obj(cx, body_val.isObject() ? &body_val.toObject() : nullptr); + // If the body parameter is a Host-backed ReadableStream we optimise our implementation + // by using the ReadableStream's handle directly. + if (body_obj && JS::IsReadableStream(body_obj)) { + if (RequestOrResponse::body_unusable(cx, body_obj)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_READABLE_STREAM_LOCKED_OR_DISTRUBED); + return false; + } + + // If the stream is backed by a Fastly Compute body handle, we can use that handle directly. + if (NativeStreamSource::stream_is_body(cx, body_obj)) { + JS::RootedObject stream_source(cx, NativeStreamSource::get_stream_source(cx, body_obj)); + JS::RootedObject source_owner(cx, NativeStreamSource::owner(stream_source)); + source_body = RequestOrResponse::body_handle(source_owner); + } else { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_SIMPLE_CACHE_SET_CONTENT_STREAM); + return false; + } + + if (args.hasDefined(3)) { + JS::HandleValue length_val = args.get(3); + double number; + if (!JS::ToNumber(cx, length_val, &number)) { + return false; + } + if (number < 0 || std::isnan(number) || std::isinf(number)) { + JS_ReportErrorASCII( + cx, "SimpleCache.set: length parameter is an invalid value, only positive numbers can " + "be used for length values."); + return false; + } + options.length = JS::ToInteger(number); + } + } else { + auto result = convertBodyInit(cx, body_val); + if (result.isErr()) { + return false; + } + std::tie(buf, options.length) = result.unwrap(); + } + + // We create a surrogate-key from the cache-key, as this allows the cached contents to be purgable + // from within the JavaScript application + // This is because the cache API currently only supports purging via surrogate-key + auto key_result = createSurrogateKeysFromCacheKey(cx, key); + if (key_result.isErr()) { + return false; + } + options.surrogate_keys = key_result.inspect(); + + auto insert_res = host_api::CacheHandle::insert(key, options); + if (auto *err = insert_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto body = insert_res.unwrap(); + if (!body.valid()) { + return false; + } + // source_body will only be valid when the body parameter is a Host-backed ReadableStream + if (source_body.valid()) { + auto res = body.append(source_body); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; + } else { + auto write_res = body.write_all_back(reinterpret_cast(buf.get()), options.length); + if (auto *err = write_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + } + auto close_res = body.close(); + if (auto *err = close_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setUndefined(); + return true; +} + +// static get(key: string): SimpleCacheEntry | null; +bool SimpleCache::get(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The SimpleCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "SimpleCache.get", 1)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key = core::encode(cx, args[0]); + if (!key) { + return false; + } + + if (key.len == 0) { + JS_ReportErrorASCII(cx, "SimpleCache.get: key can not be an empty string"); + return false; + } + if (key.len > 8135) { + JS_ReportErrorASCII(cx, + "SimpleCache.get: key is too long, the maximum allowed length is 8135."); + return false; + } + + auto lookup_res = host_api::CacheHandle::lookup(key, host_api::CacheLookupOptions{}); + if (auto *err = lookup_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto handle = lookup_res.unwrap(); + + auto body_res = handle.get_body(host_api::CacheGetBodyOptions{}); + if (auto *err = body_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto body = body_res.unwrap(); + + if (!body.valid()) { + args.rval().setNull(); + } else { + JS::RootedObject entry(cx, SimpleCacheEntry::create(cx, body)); + if (!entry) { + return false; + } + args.rval().setObject(*entry); + } + + return true; +} + +// static purge(key: string, options: PurgeOptions): undefined; +bool SimpleCache::purge(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The SimpleCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "SimpleCache.purge", 2)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key_chars = core::encode(cx, args.get(0)); + if (!key_chars) { + return false; + } + + if (key_chars.len == 0) { + JS_ReportErrorASCII(cx, "SimpleCache.purge: key can not be an empty string"); + return false; + } + if (key_chars.len > 8135) { + JS_ReportErrorASCII(cx, + "SimpleCache.purge: key is too long, the maximum allowed length is 8135."); + return false; + } + + auto secondArgument = args.get(1); + if (!secondArgument.isObject()) { + JS_ReportErrorASCII(cx, "SimpleCache.purge: options parameter is not an object."); + return false; + } + + JS::RootedObject options(cx, &secondArgument.toObject()); + JS::RootedValue scope_val(cx); + if (!JS_GetProperty(cx, options, "scope", &scope_val)) { + return false; + } + auto scope_chars = core::encode(cx, scope_val); + if (!scope_chars) { + return false; + } + + std::string_view scope = scope_chars; + std::string surrogate_key; + if (scope == "pop") { + auto surrogate_key_result = createPopSurrogateKeyFromCacheKey(cx, key_chars); + if (surrogate_key_result.isErr()) { + return false; + } + surrogate_key = surrogate_key_result.unwrap(); + } else if (scope == "global") { + auto surrogate_key_result = createGlobalSurrogateKeyFromCacheKey(cx, key_chars); + if (surrogate_key_result.isErr()) { + return false; + } + surrogate_key = surrogate_key_result.unwrap(); + } else { + JS_ReportErrorASCII( + cx, + "SimpleCache.purge: scope field of options parameter must be either 'pop', or 'global'."); + return false; + } + + auto purge_res = host_api::Fastly::purge_surrogate_key(surrogate_key); + if (auto *err = purge_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + MOZ_ASSERT(!purge_res.unwrap().has_value()); + + args.rval().setUndefined(); + return true; +} + +const JSFunctionSpec SimpleCache::static_methods[] = { + JS_FN("purge", purge, 2, JSPROP_ENUMERATE), + JS_FN("get", get, 1, JSPROP_ENUMERATE), + JS_FN("getOrSet", getOrSet, 2, JSPROP_ENUMERATE), + JS_FN("set", set, 3, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec SimpleCache::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec SimpleCache::methods[] = {JS_FS_END}; + +const JSPropertySpec SimpleCache::properties[] = { + JS_STRING_SYM_PS(toStringTag, "SimpleCache", JSPROP_READONLY), JS_PS_END}; + +bool SimpleCache::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ILLEGAL_CTOR); + return false; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + return true; +} + +} // namespace fastly::cache_simple diff --git a/runtime/fastly/builtins/cache-simple.h b/runtime/fastly/builtins/cache-simple.h new file mode 100644 index 0000000000..2f9d5aaaed --- /dev/null +++ b/runtime/fastly/builtins/cache-simple.h @@ -0,0 +1,60 @@ +#ifndef FASTLY_CACHE_SIMPLE_H +#define FASTLY_CACHE_SIMPLE_H + +#include "../host-api/host_api_fastly.h" +#include "./fetch/request-response.h" +#include "builtin.h" + +using fastly::fetch::RequestOrResponse; + +namespace fastly::cache_simple { + +class SimpleCacheEntry final : public BuiltinImpl { + template + static bool bodyAll(JSContext *cx, unsigned argc, JS::Value *vp); + static bool body_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "SimpleCacheEntry"; + + using Slots = RequestOrResponse::Slots; + 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 JSObject *create(JSContext *cx, host_api::HttpBody body_handle); +}; + +class SimpleCache : public BuiltinImpl { +private: +public: + static constexpr const char *class_name = "SimpleCache"; + static const int ctor_length = 0; + enum Slots { Count }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool delete_(JSContext *cx, unsigned argc, JS::Value *vp); + static bool get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool purge(JSContext *cx, unsigned argc, JS::Value *vp); + static bool set(JSContext *cx, unsigned argc, JS::Value *vp); + static bool getOrSet(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool getOrSetThenHandler(JSContext *cx, JS::HandleObject owner, JS::HandleValue extra, + JS::CallArgs args); + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::cache_simple + +#endif diff --git a/runtime/fastly/builtins/fastly.cpp b/runtime/fastly/builtins/fastly.cpp index a7372ff1c0..9bc0bdd23e 100644 --- a/runtime/fastly/builtins/fastly.cpp +++ b/runtime/fastly/builtins/fastly.cpp @@ -11,6 +11,7 @@ #include "js/JSON.h" using builtins::web::url::URL; +using builtins::web::url::URLSearchParams; using fastly::fastly::Fastly; namespace { @@ -372,34 +373,53 @@ bool install(api::Engine *engine) { RootedString version_str( engine->cx(), JS_NewStringCopyN(engine->cx(), RUNTIME_VERSION, strlen(RUNTIME_VERSION))); RootedValue version_str_val(engine->cx(), StringValue(version_str)); - if (!JS_SetProperty(engine->cx(), experimental, "sdkVersion", version_str_val)) { + if (!JS_SetProperty(engine->cx(), experimental, "version", version_str_val)) { return false; } if (!engine->define_builtin_module("fastly:experimental", experimental_val)) { return false; } - // TODO(GB): all of the following builtin modules are just placeholder shapes for now - if (!engine->define_builtin_module("fastly:body", env_builtin_val)) { - return false; - } + // fastly:cache + // TODO(GB): move this into core-cache when that is ported RootedObject cache(engine->cx(), JS_NewObject(engine->cx(), nullptr)); RootedValue cache_val(engine->cx(), JS::ObjectValue(*cache)); + // TODO(GB): Implement core cache (placeholders used for now) if (!JS_SetProperty(engine->cx(), cache, "CoreCache", cache_val)) { return false; } if (!JS_SetProperty(engine->cx(), cache, "CacheEntry", cache_val)) { return false; } - if (!JS_SetProperty(engine->cx(), cache, "CacheEntry", cache_val)) { + if (!JS_SetProperty(engine->cx(), cache, "CacheState", cache_val)) { + return false; + } + if (!JS_SetProperty(engine->cx(), cache, "TransactionCacheEntry", cache_val)) { + return false; + } + RootedValue simple_cache_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), engine->global(), "SimpleCache", &simple_cache_val)) { + return false; + } + if (!JS_SetProperty(engine->cx(), cache, "SimpleCache", simple_cache_val)) { + return false; + } + RootedValue simple_cache_entry_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), engine->global(), "SimpleCacheEntry", + &simple_cache_entry_val)) { return false; } - if (!JS_SetProperty(engine->cx(), cache, "SimpleCache", cache_val)) { + if (!JS_SetProperty(engine->cx(), cache, "SimpleCacheEntry", simple_cache_entry_val)) { return false; } if (!engine->define_builtin_module("fastly:cache", cache_val)) { return false; } + + // TODO(GB): all of the following builtin modules are just placeholder shapes for now + if (!engine->define_builtin_module("fastly:body", env_builtin_val)) { + return false; + } if (!engine->define_builtin_module("fastly:config-store", env_builtin_val)) { return false; } @@ -478,4 +498,55 @@ bool install(api::Engine *engine) { JS_DefineProperties(engine->cx(), fastly, Fastly::properties); } +// We currently support five types of body inputs: +// - byte sequence +// - buffer source +// - USV strings +// - URLSearchParams +// After the other other options are checked explicitly, all other inputs are +// encoded to a UTF8 string to be treated as a USV string. +// TODO: Support the other possible inputs to Body. +JS::Result> convertBodyInit(JSContext *cx, + JS::HandleValue bodyInit) { + JS::RootedObject bodyObj(cx, bodyInit.isObject() ? &bodyInit.toObject() : nullptr); + JS::UniqueChars buf; + size_t length; + + if (bodyObj && JS_IsArrayBufferViewObject(bodyObj)) { + // `maybeNoGC` needs to be populated for the lifetime of `buf` because + // short typed arrays have inline data which can move on GC, so assert + // that no GC happens. (Which it doesn't, because we're not allocating + // before `buf` goes out of scope.) + JS::AutoCheckCannotGC noGC; + bool is_shared; + length = JS_GetArrayBufferViewByteLength(bodyObj); + buf = JS::UniqueChars( + reinterpret_cast(JS_GetArrayBufferViewData(bodyObj, &is_shared, noGC))); + MOZ_ASSERT(!is_shared); + return JS::Result>(std::make_tuple(std::move(buf), length)); + } else if (bodyObj && JS::IsArrayBufferObject(bodyObj)) { + bool is_shared; + uint8_t *bytes; + JS::GetArrayBufferLengthAndData(bodyObj, &length, &is_shared, &bytes); + MOZ_ASSERT(!is_shared); + buf.reset(reinterpret_cast(bytes)); + return JS::Result>(std::make_tuple(std::move(buf), length)); + } else if (bodyObj && URLSearchParams::is_instance(bodyObj)) { + jsurl::SpecSlice slice = URLSearchParams::serialize(cx, bodyObj); + buf = JS::UniqueChars(reinterpret_cast(const_cast(slice.data))); + length = slice.len; + return JS::Result>(std::make_tuple(std::move(buf), length)); + } else { + // Convert into a String following https://tc39.es/ecma262/#sec-tostring + auto str = core::encode(cx, bodyInit); + buf = std::move(str.ptr); + length = str.len; + if (!buf) { + return JS::Result>(JS::Error()); + } + return JS::Result>(std::make_tuple(std::move(buf), length)); + } + abort(); +} + } // namespace fastly::fastly diff --git a/runtime/fastly/builtins/fastly.h b/runtime/fastly/builtins/fastly.h index 925974bd36..f1f94c18ab 100644 --- a/runtime/fastly/builtins/fastly.h +++ b/runtime/fastly/builtins/fastly.h @@ -65,6 +65,9 @@ class Fastly : public BuiltinNoConstructor { static bool allowDynamicBackends_set(JSContext *cx, unsigned argc, JS::Value *vp); }; +JS::Result> convertBodyInit(JSContext *cx, + JS::HandleValue bodyInit); + } // namespace fastly::fastly #endif diff --git a/runtime/fastly/builtins/fetch-event.cpp b/runtime/fastly/builtins/fetch-event.cpp index 199e5d6f09..46b0fedaf5 100644 --- a/runtime/fastly/builtins/fetch-event.cpp +++ b/runtime/fastly/builtins/fetch-event.cpp @@ -29,9 +29,6 @@ api::Engine *ENGINE; PersistentRooted INSTANCE; JS::PersistentRootedObjectVector *FETCH_HANDLERS; -// host_api::HttpResp::ResponseOutparam RESPONSE_OUT; -// host_api::HttpOutgoingBody *STREAMING_BODY; - void inc_pending_promise_count(JSObject *self) { MOZ_ASSERT(FetchEvent::is_instance(self)); auto count = diff --git a/runtime/fastly/builtins/fetch/request-response.cpp b/runtime/fastly/builtins/fetch/request-response.cpp index 5388647f4e..bc0067fb58 100644 --- a/runtime/fastly/builtins/fetch/request-response.cpp +++ b/runtime/fastly/builtins/fetch/request-response.cpp @@ -7,6 +7,7 @@ #include "../../../StarlingMonkey/builtins/web/worker-location.h" #include "../../../StarlingMonkey/runtime/encode.h" #include "../cache-override.h" +#include "../cache-simple.h" #include "../fastly.h" #include "../fetch-event.h" #include "extension-api.h" @@ -34,6 +35,7 @@ using builtins::web::url::URL; using builtins::web::url::URLSearchParams; using builtins::web::worker_location::WorkerLocation; using fastly::cache_override::CacheOverride; +using fastly::cache_simple::SimpleCacheEntry; using fastly::fastly::FastlyGetErrorMessage; using fastly::fetch_event::FetchEvent; @@ -133,19 +135,6 @@ ReadResult read_from_handle_all(JSContext *cx, host_api::HttpBody body) { // JS::GetReservedSlot(obj, static_cast(Request::Slots::Request)).toInt32()); // } -host_api::HttpPendingReq pending_handle(JSObject *obj) { - MOZ_ASSERT(Request::is_instance(obj)); - host_api::HttpPendingReq res; - - JS::Value handle_val = - JS::GetReservedSlot(obj, static_cast(Request::Slots::PendingRequest)); - if (handle_val.isInt32()) { - res = host_api::HttpPendingReq(handle_val.toInt32()); - } - - return res; -} - } // namespace bool RequestOrResponse::process_pending_request(JSContext *cx, int32_t handle, @@ -181,8 +170,8 @@ bool RequestOrResponse::process_pending_request(JSContext *cx, int32_t handle, } bool RequestOrResponse::is_instance(JSObject *obj) { - return Request::is_instance(obj) || - Response::is_instance(obj) /* || KVStoreEntry::is_instance(obj)*/; + return Request::is_instance(obj) || Response::is_instance(obj) || + SimpleCacheEntry::is_instance(obj); } uint32_t RequestOrResponse::handle(JSObject *obj) { @@ -874,7 +863,12 @@ bool RequestOrResponse::body_source_pull_algorithm(JSContext *cx, JS::CallArgs a // // (This deadlock happens in automated tests, but admittedly might not happen // in real usage.) - ENGINE->queue_async_task(new FastlyAsyncTask(pending_handle(source).async_handle())); + + JS::RootedObject self(cx, &args.thisv().toObject()); + JS::RootedObject owner(cx, NativeStreamSource::owner(self)); + + ENGINE->queue_async_task( + new FastlyAsyncTask(RequestOrResponse::body_handle(owner).async_handle())); args.rval().setUndefined(); return true; diff --git a/runtime/fastly/host-api/host_api_fastly.h b/runtime/fastly/host-api/host_api_fastly.h index 94a60b18d2..83aa388bf0 100644 --- a/runtime/fastly/host-api/host_api_fastly.h +++ b/runtime/fastly/host-api/host_api_fastly.h @@ -1,5 +1,5 @@ -#ifndef JS_COMPUTE_RUNTIME_HOST_API_H -#define JS_COMPUTE_RUNTIME_HOST_API_H +#ifndef FASTLY_HOST_API_H +#define FASTLY_HOST_API_H #include #include @@ -23,6 +23,15 @@ typedef uint32_t FastlyHandle; struct JSErrorFormatString; +namespace host_api { +void handle_api_error(JSContext *cx, uint8_t err, int line, const char *func); +bool error_is_generic(APIError e); +bool error_is_invalid_argument(APIError e); +bool error_is_optional_none(APIError e); +bool error_is_bad_handle(APIError e); +void handle_fastly_error(JSContext *cx, APIError err, int line, const char *func); +} // namespace host_api + namespace fastly { enum FastlyAPIError { diff --git a/runtime/js-compute-runtime/js-compute-builtins.cpp b/runtime/js-compute-runtime/js-compute-builtins.cpp index e0b28260d6..92731af165 100644 --- a/runtime/js-compute-runtime/js-compute-builtins.cpp +++ b/runtime/js-compute-runtime/js-compute-builtins.cpp @@ -1221,42 +1221,58 @@ bool print_stack(JSContext *cx, FILE *fp) { // TODO: Support the other possible inputs to Body. JS::Result> convertBodyInit(JSContext *cx, JS::HandleValue bodyInit) { - + fprintf(stderr, "a"); JS::RootedObject bodyObj(cx, bodyInit.isObject() ? &bodyInit.toObject() : nullptr); + fprintf(stderr, "b"); mozilla::Maybe maybeNoGC; JS::UniqueChars buf; size_t length; if (bodyObj && JS_IsArrayBufferViewObject(bodyObj)) { + fprintf(stderr, "ca"); // `maybeNoGC` needs to be populated for the lifetime of `buf` because // short typed arrays have inline data which can move on GC, so assert // that no GC happens. (Which it doesn't, because we're not allocating // before `buf` goes out of scope.) maybeNoGC.emplace(cx); JS::AutoCheckCannotGC &noGC = maybeNoGC.ref(); + fprintf(stderr, "da"); bool is_shared; length = JS_GetArrayBufferViewByteLength(bodyObj); + fprintf(stderr, "ea"); buf = JS::UniqueChars( reinterpret_cast(JS_GetArrayBufferViewData(bodyObj, &is_shared, noGC))); + fprintf(stderr, "fa"); MOZ_ASSERT(!is_shared); } else if (bodyObj && JS::IsArrayBufferObject(bodyObj)) { + fprintf(stderr, "cb"); bool is_shared; uint8_t *bytes; JS::GetArrayBufferLengthAndData(bodyObj, &length, &is_shared, &bytes); + fprintf(stderr, "db"); MOZ_ASSERT(!is_shared); buf.reset(reinterpret_cast(bytes)); + fprintf(stderr, "eb"); } else if (bodyObj && builtins::URLSearchParams::is_instance(bodyObj)) { + fprintf(stderr, "dc"); jsurl::SpecSlice slice = builtins::URLSearchParams::serialize(cx, bodyObj); + fprintf(stderr, "ec"); buf = JS::UniqueChars(reinterpret_cast(const_cast(slice.data))); + fprintf(stderr, "fc"); length = slice.len; } else { + fprintf(stderr, "dd"); // Convert into a String following https://tc39.es/ecma262/#sec-tostring auto str = core::encode(cx, bodyInit); + fprintf(stderr, "ed"); buf = std::move(str.ptr); + fprintf(stderr, "fd"); length = str.len; if (!buf) { + fprintf(stderr, "gd"); return JS::Result>(JS::Error()); } } + fprintf(stderr, "h"); return JS::Result>(std::make_tuple(std::move(buf), length)); }