Skip to content

Commit 5198884

Browse files
author
Jake Champion
committed
feat: implement Fanout for JS SDK
1 parent efbbd64 commit 5198884

File tree

18 files changed

+306
-12
lines changed

18 files changed

+306
-12
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ jobs:
230230
- error
231231
- extend-from-builtins
232232
- extend-from-request
233+
- fanout
233234
- geoip
234235
- hello-world
235236
- includeBytes
@@ -378,14 +379,15 @@ jobs:
378379
strategy:
379380
fail-fast: false
380381
matrix:
381-
fixture:
382+
fixture:
382383
- 'async-select'
383384
- 'byob'
384385
- 'byte-repeater'
385386
- 'cache-override'
386387
- 'crypto'
387388
- 'edge-dictionary'
388389
- 'error'
390+
- 'fanout'
389391
- 'geoip'
390392
- 'hello-world'
391393
- 'multiple-set-cookie'

.vscode/c_cpp_properties.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"name": "sm-wasi",
55
"includePath": [
66
"${workspaceFolder}/runtime/spidermonkey/debug/include",
7+
"${workspaceFolder}/runtime/spidermonkey/release/include",
78
"/opt/wasi-sdk/share/wasi-sysroot/include/",
89
"${workspaceFolder}/runtime/js-compute-runtime",
910
"${workspaceFolder}/runtime/js-compute-runtime/build/openssl-3.0.7/include"
@@ -25,4 +26,4 @@
2526
}
2627
],
2728
"version": 4
28-
}
29+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
8+
# createFanoutHandoff
9+
10+
The **`createFanoutHandoff()`** function creates a Response instance which informs Fastly to pass the original Request through Fanout, to the declared backend.
11+
12+
## Syntax
13+
14+
```js
15+
createFanoutHandoff(request, backend)
16+
```
17+
18+
### Parameters
19+
20+
- `request` _: Request_
21+
- The request to pass through Fanout.
22+
- `backend` _: string_
23+
- The name of the backend that Fanout should send the request to.
24+
- The name has to be between 1 and 254 characters inclusive.
25+
- Throws a [`TypeError`](../../globals/TypeError/TypeError.mdx) if the value is not valid. I.E. The value is null, undefined, an empty string or a string with more than 254 characters.
26+
27+
### Return value
28+
29+
A Response instance is returned, which can then be used via `event.respondWith`.
30+
31+
## Examples
32+
33+
In this example application requests to the path `/stream` and sent handled via Fanout.
34+
35+
```js
36+
import { createFanoutHandoff } from "fastly:fanout";
37+
38+
async function handleRequest(event) {
39+
try {
40+
const url = new URL(event.request.url);
41+
if (url.pathname === '/stream') {
42+
return createFanoutHandoff(event.request, 'fanout');
43+
} else {
44+
return new Response('oopsie, make a request to /stream for some fanout goodies', { status: 404 });
45+
}
46+
} catch (error) {
47+
console.error({error});
48+
return new Response(error.message, {status:500})
49+
}
50+
}
51+
52+
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
53+
```
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/// <reference types="@fastly/js-compute" />
2+
/* eslint-env serviceworker */
3+
4+
import { pass, assert, assertDoesNotThrow, assertThrows } from "../../../assertions.js";
5+
import { routes } from "../../../test-harness.js";
6+
import { createFanoutHandoff } from "fastly:fanout";
7+
8+
let error;
9+
routes.set("/createFanoutHandoff", async () => {
10+
error = assert(typeof createFanoutHandoff, "function", "typeof createFanoutHandoff");
11+
if (error) { return error; }
12+
13+
error = assert(createFanoutHandoff.name, "createFanoutHandoff", "createFanoutHandoff.name");
14+
if (error) { return error; }
15+
16+
error = assert(createFanoutHandoff.length, 2, "createFanoutHandoff.length");
17+
if (error) { return error; }
18+
19+
error = assertDoesNotThrow(() => createFanoutHandoff(new Request('.'), 'hello'));
20+
if (error) { return error; }
21+
22+
error = assertThrows(() => createFanoutHandoff());
23+
if (error) { return error; }
24+
25+
error = assertThrows(() => createFanoutHandoff(1, ''));
26+
if (error) { return error; }
27+
28+
let result = createFanoutHandoff(new Request('.'), 'hello');
29+
error = assert(result instanceof Response, true, 'result instanceof Response');
30+
if (error) { return error; }
31+
32+
error = assertThrows(() => new createFanoutHandoff(new Request('.'), 'hello'), TypeError, `createFanoutHandoff is not a constructor`)
33+
if (error) { return error }
34+
35+
error = await assertDoesNotThrow(async () => {
36+
createFanoutHandoff.call(undefined, new Request('.'), '1')
37+
})
38+
if (error) { return error }
39+
40+
// https://tc39.es/ecma262/#sec-tostring
41+
// routes.set("/object-store/get/key-parameter-calls-7.1.17-ToString", async () => {
42+
let sentinel;
43+
const test = () => {
44+
sentinel = Symbol();
45+
const key = {
46+
toString() {
47+
throw sentinel;
48+
}
49+
}
50+
createFanoutHandoff(new Request('.'), key)
51+
}
52+
error = assertThrows(test)
53+
if (error) { return error }
54+
try {
55+
test()
56+
} catch (thrownError) {
57+
let error = assert(thrownError, sentinel, 'thrownError === sentinel')
58+
if (error) { return error }
59+
}
60+
error = assertThrows(() => {
61+
createFanoutHandoff(new Request('.'), Symbol())
62+
}, TypeError, `can't convert symbol to string`)
63+
if (error) { return error }
64+
65+
error = assertThrows(() => createFanoutHandoff(new Request('.')), TypeError, `createFanoutHandoff: At least 2 arguments required, but only 1 passed`)
66+
if (error) { return error }
67+
68+
error = assertThrows(() => createFanoutHandoff(new Request('.'), ''), Error, `createFanoutHandoff: Backend parameter can not be an empty string`)
69+
if (error) { return error }
70+
71+
72+
return pass();
73+
});
74+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This file describes a Fastly Compute@Edge package. To learn more visit:
2+
# https://developer.fastly.com/reference/fastly-toml/
3+
4+
authors = ["[email protected]"]
5+
description = ""
6+
language = "other"
7+
manifest_version = 2
8+
name = "fanout"
9+
service_id = ""
10+
11+
[scripts]
12+
build = "node ../../../../js-compute-runtime-cli.js"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"GET /createFanoutHandoff": {
3+
"environments": ["c@e", "viceroy"],
4+
"downstream_request": {
5+
"method": "GET",
6+
"pathname": "/createFanoutHandoff"
7+
},
8+
"downstream_response": {
9+
"status": 200
10+
}
11+
}
12+
}

runtime/js-compute-runtime/builtins/fastly.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "builtins/env.h"
1313
#include "builtins/fastly.h"
1414
#include "builtins/logger.h"
15+
#include "builtins/request-response.h"
1516
#include "builtins/shared/url.h"
1617
#include "core/geo_ip.h"
1718
#include "host_interface/host_call.h"
@@ -130,6 +131,69 @@ bool Fastly::includeBytes(JSContext *cx, unsigned argc, JS::Value *vp) {
130131
return true;
131132
}
132133

134+
bool Fastly::createFanoutHandoff(JSContext *cx, unsigned argc, JS::Value *vp) {
135+
JS::CallArgs args = CallArgsFromVp(argc, vp);
136+
REQUEST_HANDLER_ONLY("createFanoutHandoff");
137+
if (!args.requireAtLeast(cx, "createFanoutHandoff", 2)) {
138+
return false;
139+
}
140+
141+
auto request_value = args.get(0);
142+
if (!Request::is_instance(request_value)) {
143+
JS_ReportErrorUTF8(cx, "createFanoutHandoff: request parameter must be an instance of Request");
144+
return false;
145+
}
146+
147+
fastly_response_handle_t response_handle = INVALID_HANDLE;
148+
fastly_error_t err;
149+
if (!fastly_http_resp_new(&response_handle, &err)) {
150+
HANDLE_ERROR(cx, err);
151+
return false;
152+
}
153+
fastly_body_handle_t body_handle = INVALID_HANDLE;
154+
if (!fastly_http_body_new(&body_handle, &err)) {
155+
HANDLE_ERROR(cx, err);
156+
return false;
157+
}
158+
159+
JS::RootedObject response_instance(cx, JS_NewObjectWithGivenProto(cx, &builtins::Response::class_,
160+
builtins::Response::proto_obj));
161+
if (!response_instance) {
162+
return false;
163+
}
164+
165+
auto backend_value = args.get(1);
166+
size_t length;
167+
auto backend_chars = encode(cx, backend_value, &length);
168+
if (!backend_chars) {
169+
return false;
170+
}
171+
if (length == 0) {
172+
JS_ReportErrorUTF8(cx, "createFanoutHandoff: Backend parameter can not be an empty string");
173+
return false;
174+
}
175+
176+
if (length > 254) {
177+
JS_ReportErrorUTF8(cx, "createFanoutHandoff: name can not be more than 254 characters");
178+
return false;
179+
}
180+
181+
bool is_upstream = true;
182+
bool is_grip_upgrade = true;
183+
JS::RootedObject response(
184+
cx, builtins::Response::create(cx, response_instance, response_handle, body_handle,
185+
is_upstream, is_grip_upgrade, std::move(backend_chars)));
186+
if (!response) {
187+
return false;
188+
}
189+
190+
builtins::RequestOrResponse::set_url(response,
191+
builtins::RequestOrResponse::url(&request_value.toObject()));
192+
args.rval().setObject(*response);
193+
194+
return true;
195+
}
196+
133197
bool Fastly::now(JSContext *cx, unsigned argc, JS::Value *vp) {
134198
JS::CallArgs args = CallArgsFromVp(argc, vp);
135199
args.rval().setNumber(JS_Now());
@@ -228,6 +292,7 @@ bool Fastly::create(JSContext *cx, JS::HandleObject global, FastlyOptions option
228292
JS_FN("getGeolocationForIpAddress", getGeolocationForIpAddress, 1, JSPROP_ENUMERATE),
229293
JS_FN("getLogger", getLogger, 1, JSPROP_ENUMERATE),
230294
JS_FN("includeBytes", includeBytes, 1, JSPROP_ENUMERATE),
295+
JS_FN("createFanoutHandoff", createFanoutHandoff, 2, JSPROP_ENUMERATE),
231296
options.getExperimentalHighResolutionTimeMethodsEnabled() ? nowfn : end,
232297
end};
233298

runtime/js-compute-runtime/builtins/fastly.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Fastly : public BuiltinNoConstructor<Fastly> {
2323

2424
static const JSPropertySpec properties[];
2525

26+
static bool createFanoutHandoff(JSContext *cx, unsigned argc, JS::Value *vp);
2627
static bool now(JSContext *cx, unsigned argc, JS::Value *vp);
2728
static bool dump(JSContext *cx, unsigned argc, JS::Value *vp);
2829
static bool enableDebugLogging(JSContext *cx, unsigned argc, JS::Value *vp);

runtime/js-compute-runtime/builtins/fetch-event.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@ bool response_promise_then_handler(JSContext *cx, JS::HandleObject event, JS::Ha
223223
}
224224

225225
bool streaming = false;
226+
if (Response::is_grip_upgrade(response_obj)) {
227+
std::string backend(Response::grip_backend(response_obj));
228+
fastly_world_string_t backend_str;
229+
backend_str.len = backend.length();
230+
backend_str.ptr = backend.data();
231+
232+
fastly_error_t err;
233+
if (!fastly_http_req_redirect_to_grip_proxy(&backend_str, &err)) {
234+
HANDLE_ERROR(cx, err);
235+
return false;
236+
}
237+
return true;
238+
}
239+
226240
if (!RequestOrResponse::maybe_stream_body(cx, response_obj, &streaming)) {
227241
return false;
228242
}

runtime/js-compute-runtime/builtins/request-response.cpp

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include "js/JSON.h"
1818
#include "js/Stream.h"
1919
#include <algorithm>
20-
#include <iostream>
2120
#include <vector>
2221

2322
#pragma clang diagnostic push
@@ -1978,6 +1977,20 @@ bool Response::is_upstream(JSObject *obj) {
19781977
return JS::GetReservedSlot(obj, static_cast<uint32_t>(Slots::IsUpstream)).toBoolean();
19791978
}
19801979

1980+
bool Response::is_grip_upgrade(JSObject *obj) {
1981+
MOZ_ASSERT(is_instance(obj));
1982+
return JS::GetReservedSlot(obj, static_cast<uint32_t>(Slots::IsGripUpgrade)).toBoolean();
1983+
}
1984+
1985+
const char *Response::grip_backend(JSObject *obj) {
1986+
MOZ_ASSERT(is_instance(obj));
1987+
1988+
auto backend = reinterpret_cast<char *>(
1989+
JS::GetReservedSlot(obj, static_cast<uint32_t>(Slots::GripBackend)).toPrivate());
1990+
MOZ_ASSERT(backend);
1991+
return backend;
1992+
}
1993+
19811994
uint16_t Response::status(JSObject *obj) {
19821995
MOZ_ASSERT(is_instance(obj));
19831996
return (uint16_t)JS::GetReservedSlot(obj, static_cast<uint32_t>(Slots::Status)).toInt32();
@@ -2341,7 +2354,8 @@ bool Response::redirect(JSContext *cx, unsigned argc, JS::Value *vp) {
23412354
if (!response_instance) {
23422355
return false;
23432356
}
2344-
JS::RootedObject response(cx, create(cx, response_instance, response_handle, body.handle, false));
2357+
JS::RootedObject response(
2358+
cx, create(cx, response_instance, response_handle, body.handle, false, false, nullptr));
23452359
if (!response) {
23462360
return false;
23472361
}
@@ -2485,7 +2499,8 @@ bool Response::json(JSContext *cx, unsigned argc, JS::Value *vp) {
24852499
if (!response_instance) {
24862500
return false;
24872501
}
2488-
JS::RootedObject response(cx, create(cx, response_instance, response_handle, body.handle, false));
2502+
JS::RootedObject response(
2503+
cx, create(cx, response_instance, response_handle, body.handle, false, false, nullptr));
24892504
if (!response) {
24902505
return false;
24912506
}
@@ -2644,7 +2659,8 @@ bool Response::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
26442659

26452660
auto body = make_res.unwrap();
26462661
JS::RootedObject responseInstance(cx, JS_NewObjectForConstructor(cx, &class_, args));
2647-
JS::RootedObject response(cx, create(cx, responseInstance, response_handle, body.handle, false));
2662+
JS::RootedObject response(
2663+
cx, create(cx, responseInstance, response_handle, body.handle, false, false, nullptr));
26482664
if (!response) {
26492665
return false;
26502666
}
@@ -2730,7 +2746,8 @@ bool Response::init_class(JSContext *cx, JS::HandleObject global) {
27302746

27312747
JSObject *Response::create(JSContext *cx, JS::HandleObject response,
27322748
fastly_response_handle_t response_handle,
2733-
fastly_body_handle_t body_handle, bool is_upstream) {
2749+
fastly_body_handle_t body_handle, bool is_upstream, bool is_grip,
2750+
JS::UniqueChars backend) {
27342751
// MOZ_ASSERT(cx);
27352752
// MOZ_ASSERT(is_instance(response));
27362753
// MOZ_ASSERT(response_handle);
@@ -2745,6 +2762,8 @@ JSObject *Response::create(JSContext *cx, JS::HandleObject response,
27452762
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Redirected), JS::FalseValue());
27462763
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::IsUpstream),
27472764
JS::BooleanValue(is_upstream));
2765+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::IsGripUpgrade),
2766+
JS::BooleanValue(is_grip));
27482767

27492768
if (is_upstream) {
27502769
uint16_t status = 0;
@@ -2761,7 +2780,8 @@ JSObject *Response::create(JSContext *cx, JS::HandleObject response,
27612780
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::HasBody), JS::TrueValue());
27622781
}
27632782
}
2764-
2783+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::GripBackend),
2784+
JS::PrivateValue(std::move(backend.release())));
27652785
return response;
27662786
}
27672787

0 commit comments

Comments
 (0)