Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EF waiting room example #90

Merged
merged 7 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
node_modules/
**/node_modules/
**/.edgio/**
**/.yalc/**
yalc.lock
15 changes: 9 additions & 6 deletions examples/v7-edge-functions/edgio.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
require('dotenv').config();

module.exports = {
// The name of the site in Edgio to which this app should be deployed.
name: 'edgio-functions-examples',

// The name of the team in Edgio to which this app should be deployed.
team: 'edge-functions-sandbox',

// Overrides the default path to the routes file. The path should be relative to the root of your app.
// routes: 'routes.js',

Expand All @@ -30,6 +24,15 @@ module.exports = {
},
],
},
{
name: 'echo',
override_host_header: 'http-echo.raees.me',
hosts: [
{
location: 'http-echo.raees.me',
},
],
},
{
name: 'planetscale',
override_host_header: 'aws.connect.psdb.cloud',
Expand Down
132 changes: 0 additions & 132 deletions examples/v7-edge-functions/functions/database/upstash/index.js

This file was deleted.

27 changes: 27 additions & 0 deletions examples/v7-edge-functions/functions/general/caching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import createFetchForOrigin from '../../utils/createFetchForOrigin';
import '../../utils/polyfills/URL';

const fetch = createFetchForOrigin('echo');

export async function handleHttpRequest(request, context) {
const { method } = request;
const body = method === 'POST' ? await request.arrayBuffer() : null;

// get the headers from the incoming request, removing the content-length header
const headers = Object.fromEntries(
[...request.headers.entries()].filter(([key]) => key !== 'content-length')
);

const newRequest = new Request('https://http-echo.raees.me', {
method,
headers,
body,
});

const response = await fetch(newRequest);

// apply caching headers to the response for all HTTP methods
response.headers.set('cache-control', 's-maxage=600');

return response;
}
27 changes: 25 additions & 2 deletions examples/v7-edge-functions/functions/general/sample-html-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,34 @@ export async function handleHttpRequest(request, context) {
<p>Transactional queries with a PlanetScale database.</p>
</li>
<li>
<h4>Upstash Database (<a href="/example/upstash-database">View Example</a>)</h4>
<p>A waiting room example using Upstash + Redis.</p>
<h4>Waiting Room using PlanetScale (<a href="/example/waiting-room">View Example</a>)</h4>
<p>A waiting room example using PlanetScale for session tracking.</p>
</li>
</ul>
</section>

<section>
<h2>Caching</h2>
<p>Examples demonstrating caching for different request types. Observe unique caching for GET and POST w/ body requests.</p>
<ul>
<li>
<strong>GET Request</strong>
<pre><code>${createCURLCommand(
'/example/caching'
)}</code></pre>
</li>
<li>
<strong>POST Request with JSON payload 1</strong>
<pre><code>curl -i -X POST ${domain}/example/caching -d '{"key": "value1"}'</code></pre>
</li>
<li>
<strong>POST Request with JSON payload 2</strong>
<pre><code>curl -i -X POST ${domain}/example/caching -d '{"key": "value2"}'</code></pre>
</li>
</ul>
</section>


</div>
<div style="margin-top: 30px; text-align: center;">
<a href="https://docs.edg.io/guides/v7/edge-functions" target="_blank">Edge Functions Documentation</a> | <a href="https://github.com/edgio-docs/edgio-v7-edge-functions-example" target="_blank">View the demo code on GitHub</a>
Expand Down
77 changes: 77 additions & 0 deletions examples/v7-edge-functions/functions/waiting-room/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export const COOKIE_NAME_ID = '__edgio_session_id';
export const COOKIE_NAME_TIME = '__edgio_session_last_update_time';
export const TOTAL_ACTIVE_SESSIONS = 2;

// Active sessions should persist longer than queued sessions
export const ACTIVE_SESSION_DURATION_SECONDS = 150;
export const QUEUED_SESSION_DURATION_SECONDS = 15;

// Queries
export const CREATE_SESSION_QUERY = `
-- Conditional insert if the session does not exist.
-- This is cheaper than a SELECT followed by an INSERT.
INSERT INTO sessions (sessionId, status, createdAt, updatedAt)
SELECT
:id,
IF((SELECT COUNT(*) FROM sessions WHERE status = 'active') >= :sessionsLimit, 'queued', 'active'),
NOW(),
NOW()
FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM sessions WHERE sessionId = :id
);
`;

export const GET_SESSION_QUERY = `
-- Get the session data and the position in the queue.
SELECT
sessions_all.*,
IF(sessions_all.status = 'queued', queued_info.position, -1) AS position,
(SELECT COUNT(*) FROM sessions WHERE status = 'active') AS active_count,
(SELECT COUNT(*) FROM sessions WHERE status = 'queued') AS queued_count
FROM
(SELECT
sessionId,
status,
createdAt,
updatedAt
FROM sessions) AS sessions_all
LEFT JOIN
(SELECT
sessionId,
ROW_NUMBER() OVER (ORDER BY createdAt) AS position
FROM sessions
WHERE status = 'queued') AS queued_info
ON sessions_all.sessionId = queued_info.sessionId
WHERE sessions_all.sessionId = :id;
`;

export const EXPIRE_SESSIONS_QUERY = `
DELETE FROM sessions WHERE
(updatedAt < DATE_SUB(NOW(), INTERVAL :activeDuration SECOND) AND status = 'active')
OR (updatedAt < DATE_SUB(NOW(), INTERVAL :queuedDuration SECOND) AND status = 'queued');
`;

export const GET_SESSION_ID_QUERY = `
SELECT sessionId FROM (
SELECT sessionId FROM sessions WHERE sessionId = :id
UNION ALL
SELECT UUID() AS sessionId
ORDER BY (sessionId IS NOT NULL) DESC
LIMIT 1
) AS uuid_selection;
`;

export const REFRESH_SESSION_QUERY = `
UPDATE sessions
SET
updatedAt = :date,
status = :status
WHERE sessionId = :id;
`;

export const AVAILABLE_STATUS_QUERY = `
SELECT IF(COUNT(*) < :sessionsLimit, 'active', 'queued') as newStatus
FROM sessions
WHERE status = 'active';
`;
78 changes: 78 additions & 0 deletions examples/v7-edge-functions/functions/waiting-room/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
getCookiesFromRequest,
setCookieToResponse,
} from '../../utils/cookies';
import { setEnvFromContext } from '../../utils/polyfills/process.env';
import waitingPage from './waiting-room-capacity.html';
import landingPage from './waiting-room-landing.html';
import { COOKIE_NAME_ID, COOKIE_NAME_TIME } from './constants';
import { getSessionData } from './planetscale';

/**
* Main handler for the edge request.
*/
export async function handleHttpRequest(request, context) {
let response;

// Set context environment variables to process.env
setEnvFromContext(context);

const cookies = getCookiesFromRequest(request);

// Get user ID from cookie or generate a new one
const sessionId = cookies[COOKIE_NAME_ID];

// Get the current number of active sessions and the active user
const { session, activeCount, queuedCount } = await getSessionData(sessionId);

// Check capacity
if (session.status === 'active') {
response = await getDefaultResponse(request, session);
} else {
response = await getWaitingRoomResponse(request, session);
}

// Update the session cookie with the latest timestamp
setSessionCookie(response, session.sessionId, session.updatedAt);

return response;
}

/**
* Handle the default response.
*/
async function getDefaultResponse(request, session) {
const response = new Response(landingPage({ requestUrl: request.url }));
response.headers.set('content-type', 'text/html;charset=UTF-8');

return response;
}
/**
* Response for the waiting room.
*/
async function getWaitingRoomResponse(request, session) {
// update the waiting page to show the position in the queue, replacing {{queuePosition}}
const body = waitingPage({
queuePosition: session.position,
});

const response = new Response(body);
response.headers.set('content-type', 'text/html;charset=UTF-8');

return response;
}

/**
* Sets the session cookie to the response.
*
* @param {Response} response
* @param {Object} session
* @param {number} date
* @returns {Promise<void>}
*/
export async function setSessionCookie(response, sessionId, date) {
const now = date || Date.now();

setCookieToResponse(response, [[COOKIE_NAME_TIME, now.toString()]]);
setCookieToResponse(response, [[COOKIE_NAME_ID, sessionId]]);
}
Loading
Loading