Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 92 additions & 43 deletions runtime/fastly/builtins/cache-simple.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ using fastly::FastlyGetErrorMessage;
using fastly::fastly::convertBodyInit;
using fastly::fetch::RequestOrResponse;

namespace {
api::Engine *GLOBAL_ENGINE;
}

namespace fastly::cache_simple {

template <RequestOrResponse::BodyReadResult result_type>
Expand Down Expand Up @@ -394,62 +398,39 @@ bool get_or_set_catch_handler(JSContext *cx, JS::HandleObject lookup_state,
return true;
}

} // namespace

// 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) {
bool process_pending_cache_lookup(JSContext *cx, host_api::CacheHandle::Handle handle,
JS::HandleObject context_obj, JS::HandleValue) {
host_api::CacheHandle pending_lookup(handle);
JS::RootedValue key_val(cx);
if (!JS_GetProperty(cx, context_obj, "key", &key_val)) {
return false;
}

if (key_chars.len == 0) {
JS_ReportErrorASCII(cx, "SimpleCache.getOrSet: key can not be an empty string");
auto key_chars = core::encode(cx, key_val);
JS::RootedValue set_function_val(cx);
if (!JS_GetProperty(cx, context_obj, "set_function", &set_function_val)) {
return false;
}
if (key_chars.len > 8135) {
JS_ReportErrorASCII(
cx, "SimpleCache.getOrSet: key is too long, the maximum allowed length is 8135.");
JS::RootedValue promise_val(cx);
if (!JS_GetProperty(cx, context_obj, "promise", &promise_val)) {
return false;
}
JS::RootedObject promise_obj(cx, &promise_val.toObject());

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);
BEGIN_TRANSACTION(transaction, cx, promise_obj, pending_lookup);

// 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();
auto state_res = pending_lookup.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{});
auto body_res = pending_lookup.get_body(host_api::CacheGetBodyOptions{});
if (auto *err = body_res.to_err()) {
return false;
}
Expand All @@ -461,16 +442,15 @@ bool SimpleCache::getOrSet(JSContext *cx, unsigned argc, JS::Value *vp) {

JS::RootedValue result(cx);
result.setObject(*entry);
JS::ResolvePromise(cx, promise, result);
JS::ResolvePromise(cx, promise_obj, result);
return true;
} else {
auto arg1 = args.get(1);
if (!arg1.isObject() || !JS::IsCallable(&arg1.toObject())) {
if (!set_function_val.isObject() || !JS::IsCallable(&set_function_val.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::RootedObject fn(cx, &set_function_val.toObject());
JS::RootedValue result(cx);
if (!JS::Call(cx, JS::NullHandleValue, fn, fnargs, &result)) {
return false;
Expand All @@ -483,7 +463,7 @@ bool SimpleCache::getOrSet(JSContext *cx, unsigned argc, JS::Value *vp) {

// JS::RootedObject owner(cx, JS_NewPlainObject(cx));
JS::RootedObject lookup_state(cx, JS_NewPlainObject(cx));
JS::RootedValue handle_val(cx, JS::NumberValue(handle.handle));
JS::RootedValue handle_val(cx, JS::NumberValue(handle));
if (!JS_SetProperty(cx, lookup_state, "handle", handle_val)) {
return false;
}
Expand All @@ -492,7 +472,7 @@ bool SimpleCache::getOrSet(JSContext *cx, unsigned argc, JS::Value *vp) {
if (!JS_SetProperty(cx, lookup_state, "key", keyVal)) {
return false;
}
JS::RootedValue promise_val(cx, JS::ObjectValue(*promise));
JS::RootedValue promise_val(cx, JS::ObjectValue(*promise_obj));
if (!JS_SetProperty(cx, lookup_state, "promise", promise_val)) {
return false;
}
Expand All @@ -517,6 +497,73 @@ bool SimpleCache::getOrSet(JSContext *cx, unsigned argc, JS::Value *vp) {
}
}

} // namespace

// 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;
}

// The async task requires some extra state: the key, the `set` function, and the promise.
// We wrap this all up into one object, so `process_pending_cache_lookup` can retrieve everything.
// This could be avoided with some changes to `FastlyAsyncTask`, see
// (https://github.com/fastly/js-compute-runtime/issues/1245)
JS::RootedObject context_obj(cx, JS_NewPlainObject(cx));
if (!context_obj) {
return false;
}
JS::RootedValue key_val(cx, args.get(0));
if (!JS_SetProperty(cx, context_obj, "key", key_val)) {
return false;
}
JS::RootedValue set_func_val(cx, args.get(1));
if (!JS_SetProperty(cx, context_obj, "set_function", set_func_val)) {
return false;
}
JS::RootedValue promise_val(cx, JS::ObjectValue(*promise));
if (!JS_SetProperty(cx, context_obj, "promise", promise_val)) {
return false;
}
auto handle = res.unwrap();
GLOBAL_ENGINE->queue_async_task(new FastlyAsyncTask(
handle.handle, context_obj, JS::UndefinedHandleValue, process_pending_cache_lookup));

args.rval().setObject(*promise);
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) {
Expand Down Expand Up @@ -791,6 +838,8 @@ const JSPropertySpec SimpleCache::properties[] = {
JS_STRING_SYM_PS(toStringTag, "SimpleCache", JSPROP_READONLY), JS_PS_END};

bool install(api::Engine *engine) {
GLOBAL_ENGINE = engine;

if (!SimpleCacheEntry::init_class_impl(engine->cx(), engine->global())) {
return false;
}
Expand Down
Loading