-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
17,212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
# Examples for Remix | ||
|
||
The examples in this directory illustrate the use of GRIP using | ||
a [Remix](https://remix.run) application as the backend. | ||
|
||
* [`http-stream/`](./http-stream) - HTTP streaming using GRIP. | ||
* [`websocket/`](./websocket) - WebSocket-over-HTTP using GRIP. | ||
|
||
For details on each example, view the `README` file in its | ||
respective directory. | ||
|
||
## Running the examples locally | ||
|
||
Each example can be run locally by running it alongside an instance of | ||
[Pushpin](https://pushpin.org/). | ||
|
||
To run the examples locally, you'll need: | ||
|
||
* Node.js (v18 or newer) | ||
* Pushpin - [installation instructions](https://pushpin.org/docs/install/) | ||
|
||
> NOTE: Instead of local Pushpin, you can also run the examples using Fastly Fanout for the GRIP proxy. | ||
See [Running the examples on Fastly Fanout](#running-the-examples-with-fastly-fanout-as-the-grip-proxy) below. | ||
|
||
1. Set up Pushpin by modifying the `routes` file with the following content | ||
(See [this page](https://pushpin.org/docs/configuration/) for details on | ||
Pushpin configuration): | ||
|
||
``` | ||
* 127.0.0.1:3000 | ||
``` | ||
|
||
2. Start Pushpin. | ||
|
||
``` | ||
pushpin | ||
``` | ||
|
||
By default, it will listen on port 7999, with a publishing | ||
endpoint open on port 5561. Leave Pushpin running in that terminal window. | ||
|
||
3. In a new terminal window, switch to the example's directory, and | ||
install dependencies: | ||
|
||
``` | ||
npm install | ||
``` | ||
|
||
4. Start the example: | ||
|
||
``` | ||
npm run start | ||
``` | ||
|
||
This will invoke [`@remix-run/serve`](https://remix.run/docs/en/main/other-api/serve) | ||
to start the local server to run the example application. | ||
|
||
5. Go on to follow the steps under each example's `README` file. | ||
|
||
## Description of common code between the examples | ||
|
||
Each example has the same general structure in an `app/` directory that contains: | ||
* `routes/` to define the API endpoints | ||
* Checking GRIP status | ||
* Handling (specific to the example) | ||
* `utils/` directory | ||
* Configuring GRIP and instantiating the `Publisher` | ||
|
||
### API Routes | ||
|
||
Following the format of [API Routes (Resource Routes)](https://remix.run/docs/en/main/guides/api-routes#resource-routes) | ||
in Remix applications, these examples declare files in the `app/routes/` directory. | ||
|
||
### Configuration of GRIP | ||
|
||
Each example interfaces with GRIP using the `Publisher` class. The code for this | ||
exists in the `app/utils/publisher.ts` file. | ||
|
||
To configure `Publisher`, a GRIP configuration object `gripConfig` is used. | ||
The example applications give it a default value of `http://127.0.0.1:5561/` to point to | ||
local Pushpin. | ||
|
||
```typescript | ||
let gripConfig: string | IGripConfig = 'http://127.0.0.1:5561/'; | ||
``` | ||
|
||
It may be overridden using a `GRIP_URL`, which in the Remix backend application is set as | ||
a [server environment variable](https://remix.run/docs/en/main/guides/envvars#server-environment-variables). | ||
Additionally, in the example, the utility function `parseGripUri` is used to merge in the `GRIP_VERIFY_KEY` | ||
if it's required by the proxy. | ||
|
||
```typescript | ||
let gripConfig: string | IGripConfig = 'http://127.0.0.1:5561/'; | ||
const gripUrl = process.env.GRIP_URL; | ||
if (gripUrl) { | ||
gripConfig = parseGripUri(gripUrl, { 'verify-key': process.env.GRIP_VERIFY_KEY }); | ||
} | ||
``` | ||
|
||
Alternatively, the values for `FANOUT_SERVICE_ID` and `FANOUT_API_TOKEN` are checked, and if present, | ||
they are used with the `buildFanoutGripConfig()` function to build the `gripConfig`. | ||
|
||
```typescript | ||
const fanoutServiceId = process.env.FANOUT_SERVICE_ID; | ||
const fanoutApiToken = process.env.FANOUT_API_TOKEN; | ||
if (fanoutServiceId != null && fanoutApiToken != null) { | ||
gripConfig = buildFanoutGripConfig({ | ||
serviceId: fanoutServiceId, | ||
apiToken: fanoutApiToken, | ||
}); | ||
} | ||
``` | ||
|
||
Finally, this `gripConfig` is used to instantiate `Publisher`. | ||
|
||
```typescript | ||
const publisher = new Publisher(gripConfig); | ||
``` | ||
|
||
In the Remix example, this initialization happens in the `app/utils/publisher.ts` file, | ||
and that single instance is reused among incoming requests. | ||
|
||
### GRIP status | ||
|
||
The backend application is intended to be called via a GRIP proxy. When the handler runs, | ||
a GRIP proxy will have inserted a `Grip-Sig` header into the request, which it has | ||
signed with a secret or key. | ||
|
||
Route handlers that issue GRIP instructions call `publisher.validateGripSig` to validate | ||
this header, storing the result in the `gripStatus` variable. | ||
```typescript | ||
const gripStatus = await publisher.validateGripSig(request.headers.get('grip-sig')); | ||
``` | ||
|
||
This result can be checked for three fields: | ||
`gripStatus.isProxied` - When `true`, indicates that the current request is behind | ||
a GRIP proxy. If `needsSigned` is `true`, then this will only be `true` if the | ||
signature validation has also succeeded. | ||
`gripStatus.needsSigned` - When `true`, indicates that the GRIP proxy specified in the | ||
configuration signs incoming requests. | ||
`gripStatus.isSigned` - When `true`, indicates that the signature validation was successful. | ||
|
||
### Handling Routes | ||
|
||
The route handlers in each example handle requests in their respective ways. Refer | ||
to the README in each project for details. | ||
|
||
## Running the examples with Fastly Fanout as the GRIP proxy | ||
|
||
By publishing these examples publicly, they can also be run behind | ||
[Fastly Fanout](https://docs.fastly.com/products/fanout) to benefit from a global | ||
network and holding client connections at the edge. | ||
|
||
Aside from your backend application running publicly on the internet, | ||
you will need a separate Fastly Compute service with Fanout enabled. | ||
This Fastly service runs a small program at the edge that examines | ||
each request and performs a "handoff" to Fanout for relevant requests, | ||
allowing Fanout to hold client connections and interpret GRIP messages. | ||
|
||
The [Fastly Fanout Forwarding Starter Kit (JavaScript)](https://github.com/fastly/compute-starter-kit-javascript-fanout-forward#readme) | ||
can be used for this purpose. In many cases it can be used as is, | ||
or as a starting point for further customization. | ||
|
||
Back to [examples](../) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
/public/build | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# HTTP Streaming Example for Remix | ||
|
||
This example illustrates the use of GRIP to stream HTTP responses | ||
using a Remix application as the backend. | ||
|
||
## Running the example | ||
|
||
For instructions on setting up and running the example, either locally or using | ||
Fastly Fanout as the GRIP proxy, refer to the [`README` file in the parent directory](../). | ||
|
||
This example also requires `curl`, which is included with most OSes. | ||
|
||
## Testing the example | ||
|
||
After you have set up Pushpin and started the application, test the example | ||
following these steps. | ||
|
||
> NOTE: If you are using Fastly Fanout as the GRIP proxy, follow these steps, but | ||
replace `127.0.0.1:7999` with the public URL of your Fanout Forwarding service. | ||
|
||
1. Open a new terminal window, and type the following: | ||
|
||
``` | ||
curl http://127.0.0.1:7999/api/stream | ||
``` | ||
|
||
You should see the following response text, and then the response should hang open: | ||
``` | ||
[stream open] | ||
``` | ||
|
||
`curl` now has an open HTTP stream, held open by Pushpin (listening on a channel internally called `test`). | ||
|
||
2. Open a separate terminal window, and type the following: | ||
|
||
``` | ||
curl -X POST -d "Hello" "https://127.0.0.1:7999/api/broadcast" | ||
``` | ||
|
||
This publishes the given message (to the channel `test`). You should see the message `Hello` | ||
appear in the stream held open by the first terminal. | ||
|
||
## How it works | ||
|
||
For an explanation of the common startup and initialization code, as well as | ||
validating the GRIP header, refer to the [`README` file in the parent | ||
directory](../README.md#description-of-common-code-between-the-examples). | ||
|
||
The example exposes two API Routes (Resource Routes): | ||
|
||
1. A `GET` request at `/api/stream` (File: `app/routes/api.stream.ts`) | ||
|
||
This endpoint is intended to be called through your configured GRIP proxy. | ||
|
||
The handler calls `publisher.validateGripSig` to validate this header, storing the result in | ||
the `gripStatus` variable. | ||
|
||
It checks `gripStatus.isProxied` to make sure we are being run behind a valid | ||
GRIP proxy. This value will be `false` if the request did not come through a GRIP proxy, | ||
or if the signature validation failed. | ||
|
||
```typescript | ||
if (!gripStatus.isProxied) { | ||
// emit an error | ||
} | ||
``` | ||
|
||
If successful, then the handler goes on to set up a GRIP instruction. | ||
This instruction asks the GRIP proxy to hold the current connection open | ||
as a streaming connection, listening to the channel named `'test'`. | ||
|
||
```typescript | ||
const gripInstruct = new GripInstruct('test'); | ||
gripInstruct.setHoldStream(); | ||
``` | ||
|
||
Finally, a response is generated and returned, including the | ||
`gripInstruct` in the response headers. | ||
|
||
```typescript | ||
return new Response( | ||
'[stream open]\n', | ||
{ | ||
status: 200, | ||
headers: { | ||
...gripInstruct.toHeaders(), | ||
'Content-Type': 'text/plain', | ||
}, | ||
}, | ||
); | ||
``` | ||
|
||
That's all that's needed to hold a connection open. Note that the connection between | ||
your backend and the GRIP proxy ends here. After this point, the GRIP proxy holds the | ||
connection open with the client. | ||
|
||
2. A `POST` request at `/api/publish` (File: `app/routes/api.publish.ts`) | ||
|
||
This handler starts by checking to make sure the method is in fact `POST` and that | ||
the content type header specifies that the body is of type `text/plain`. Afterward, | ||
the handler reads the request body into a string. | ||
|
||
```typescript | ||
if (request.method !== 'POST') { | ||
// emit an error | ||
} | ||
if (request.headers.get('content-type')?.split(';')[0] !== 'text/plain') { | ||
// emit an error | ||
} | ||
const body = await request.text(); | ||
``` | ||
|
||
Next, the handler proceeds to call `publisher.publishHttpStream` to send the | ||
body text as a message to the `test` channel. Any listener on this channel, | ||
such as those that have opened a stream through the endpoint described above, | ||
will receive the message. | ||
|
||
```typescript | ||
await publisher.publishHttpStream('test', body + '\n'); | ||
``` | ||
|
||
Finally, it returns a simple success response message and ends. | ||
|
||
```typescript | ||
return new Response( | ||
'Ok\n', | ||
{ | ||
status: 200, | ||
headers: { | ||
'Content-Type': 'text/plain', | ||
}, | ||
}, | ||
); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { | ||
Outlet, | ||
} from "@remix-run/react"; | ||
|
||
export default function App() { | ||
return ( | ||
<Outlet /> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// noinspection DuplicatedCode | ||
|
||
import { ActionFunctionArgs } from '@remix-run/node'; | ||
import publisher from '~/utils/publisher'; | ||
|
||
export async function action({ request }: ActionFunctionArgs) { | ||
if (request.method !== 'POST') { | ||
return new Response( | ||
'Method not allowed', | ||
{ | ||
status: 405, | ||
}, | ||
); | ||
} | ||
|
||
// Only accept text bodies | ||
if (request.headers.get('content-type')?.split(';')[0] !== 'text/plain') { | ||
return new Response( | ||
'Body must be text/plain\n', { | ||
status: 415, | ||
headers: { | ||
'Content-Type': 'text/plain', | ||
}, | ||
}, | ||
); | ||
} | ||
|
||
// Read the body | ||
const body = await request.text(); | ||
|
||
// Publish the body to GRIP clients that listen to http-stream format | ||
await publisher.publishHttpStream('test', body + '\n'); | ||
|
||
// Return a success response | ||
return new Response( | ||
'Ok\n', | ||
{ | ||
status: 200, | ||
headers: { | ||
'Content-Type': 'text/plain', | ||
}, | ||
}, | ||
); | ||
} |
Oops, something went wrong.