mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-15 17:19:10 +00:00
chore: add cloudflare worker emulator and supporting bits. now fully offline!
This commit is contained in:
parent
6ddb3e3192
commit
a44983b088
10 changed files with 601 additions and 258 deletions
75
src/backend-emulator/kv.js
Normal file
75
src/backend-emulator/kv.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
const redis = require('redis');
|
||||
const { promisify } = require('util');
|
||||
|
||||
let hasWarned = false;
|
||||
|
||||
const getConversion = {
|
||||
text: (x) => x,
|
||||
json: (x) => JSON.parse(x),
|
||||
arrayBuffer: (x) => Buffer.from(x).buffer,
|
||||
stream: (x) => Buffer.from(x),
|
||||
};
|
||||
|
||||
class RedisKVShim {
|
||||
constructor(redisClient, namespace) {
|
||||
this.namespace = namespace;
|
||||
this._redis = redisClient;
|
||||
this._redisGet = promisify(this._redis.get).bind(redisClient);
|
||||
this._redisSetex = promisify(this._redis.setex).bind(redisClient);
|
||||
this._redisSet = promisify(this._redis.set).bind(redisClient);
|
||||
this._redisDel = promisify(this._redis.del).bind(redisClient);
|
||||
}
|
||||
|
||||
key(key) {
|
||||
return `${this.namespace}__${key}`;
|
||||
}
|
||||
|
||||
async get(key, type = 'text') {
|
||||
const result = await this._redisGet(this.key(key));
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getConversion[type](result);
|
||||
}
|
||||
|
||||
async getWithMetadata(key, type) {
|
||||
return {
|
||||
value: await this.get(key, type),
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
|
||||
async put(key, value, { expirationTtl, expiration, metadata }) {
|
||||
if ((expiration || metadata) && !hasWarned) {
|
||||
console.warn(
|
||||
'expiration and metadata is lost in the emulator. Use expirationTtl, please.'
|
||||
);
|
||||
hasWarned = true;
|
||||
}
|
||||
|
||||
if (expirationTtl) {
|
||||
return this._redisSetex(this.key(key), value, expirationTtl);
|
||||
}
|
||||
|
||||
return this._redisSet(this.key(key), value);
|
||||
}
|
||||
|
||||
async delete(key) {
|
||||
return this._redisDel(this.key(key));
|
||||
}
|
||||
|
||||
list() {
|
||||
console.warn('List is frowned upon and will fail to fetch keys in the emulator.');
|
||||
return {
|
||||
keys: [],
|
||||
cursor: '0',
|
||||
list_complete: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RedisKVShim,
|
||||
};
|
160
src/backend-emulator/main.js
Normal file
160
src/backend-emulator/main.js
Normal file
|
@ -0,0 +1,160 @@
|
|||
require('dotenv').config();
|
||||
const vm = require('vm');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const webpack = require('webpack');
|
||||
const { Crypto } = require('@peculiar/webcrypto');
|
||||
const roleypolyConfig = require('../backend-worker/roleypoly.config');
|
||||
const redis = require('redis');
|
||||
const { RedisKVShim } = require('./kv');
|
||||
const crypto = new Crypto();
|
||||
|
||||
const redisClient = redis.createClient({ host: 'redis' });
|
||||
|
||||
const getKVs = (redisClient, namespaces = []) => {
|
||||
namespaces.reduce(
|
||||
(acc, ns) => ({ ...acc, [ns]: new RedisKVShim(redisClient, ns) }),
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
const workerShims = {
|
||||
...roleypolyConfig.environment,
|
||||
...getKVs(redisClient, roleypolyConfig.kv),
|
||||
};
|
||||
|
||||
let listeners = [];
|
||||
|
||||
const context = () =>
|
||||
vm.createContext(
|
||||
{
|
||||
addEventListener: (a, fn) => {
|
||||
if (a === 'fetch') {
|
||||
console.log('addEventListeners: added fetch');
|
||||
listeners.push(fn);
|
||||
}
|
||||
},
|
||||
Response: class {
|
||||
constructor(body, opts = {}) {
|
||||
this.body = Buffer.from(body || '');
|
||||
this.headers = opts.headers || {};
|
||||
this.url = opts.url || {};
|
||||
this.status = opts.status || 200;
|
||||
}
|
||||
},
|
||||
URL: URL,
|
||||
crypto: crypto,
|
||||
setTimeout: setTimeout,
|
||||
setInterval: setInterval,
|
||||
clearInterval: clearInterval,
|
||||
clearTimeout: clearTimeout,
|
||||
...workerShims,
|
||||
},
|
||||
{
|
||||
codeGeneration: {
|
||||
strings: false,
|
||||
wasm: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const event = {
|
||||
respondWith: async (value) => {
|
||||
const timeStart = Date.now();
|
||||
const response = await value;
|
||||
const timeEnd = Date.now();
|
||||
console.log(
|
||||
`${response.status} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}`
|
||||
);
|
||||
res.statusCode = response.status;
|
||||
Object.entries(response.headers).forEach(([k, v]) => res.setHeader(k, v));
|
||||
res.end(response.body);
|
||||
},
|
||||
request: {
|
||||
url: `http://${req.headers.host || 'localhost'}${req.url}`,
|
||||
body: req,
|
||||
headers: Object.entries(req.headers).reduce(
|
||||
(acc, [k, v]) => acc.set(k, v),
|
||||
new Map()
|
||||
),
|
||||
method: req.method,
|
||||
},
|
||||
};
|
||||
|
||||
event.request.headers.set('cf-client-ip', req.connection.remoteAddress);
|
||||
|
||||
if (listeners.length === 0) {
|
||||
res.statusCode = 503;
|
||||
res.end('No handlers are available.');
|
||||
console.error('No handlers are available');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let listener of listeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (e) {
|
||||
console.error('listener errored', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fork = async (fn) => fn();
|
||||
|
||||
const reload = () => {
|
||||
// Clear listeners...
|
||||
listeners = [];
|
||||
|
||||
// Fork and re-run
|
||||
fork(async () =>
|
||||
vm.runInContext(
|
||||
fs.readFileSync(path.resolve(__dirname, '../backend-worker/dist/worker.js')),
|
||||
context(),
|
||||
{
|
||||
displayErrors: true,
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const rebuild = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const webpackConfig = require('../backend-worker/webpack.config.js');
|
||||
webpackConfig.output.filename = 'worker.js';
|
||||
webpack(webpackConfig).run((err, stats) => {
|
||||
if (err) {
|
||||
console.log('Compilation failed.', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Compilation done.');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const watcher = chokidar.watch(path.resolve(__dirname, '../backend-worker'), {
|
||||
ignoreInitial: true,
|
||||
ignore: '**/dist',
|
||||
});
|
||||
|
||||
watcher.on('all', async (type, path) => {
|
||||
if (path.includes('dist')) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('change detected, rebuilding and reloading', { type, path });
|
||||
|
||||
await rebuild();
|
||||
reload();
|
||||
});
|
||||
|
||||
fork(async () => {
|
||||
await rebuild();
|
||||
reload();
|
||||
});
|
||||
|
||||
console.log('starting on http://localhost:6609');
|
||||
server.listen(6609, '0.0.0.0');
|
|
@ -6,10 +6,23 @@ import { Router } from './router';
|
|||
|
||||
const router = new Router();
|
||||
|
||||
router.addFallback('root', () => {
|
||||
return new Response('hello!!');
|
||||
});
|
||||
|
||||
router.add('GET', 'bot-join', BotJoin);
|
||||
router.add('GET', 'login-bounce', LoginBounce);
|
||||
router.add('GET', 'login-callback', LoginCallback);
|
||||
router.add('GET', 'get-session', GetSession);
|
||||
router.add('GET', 'x-headers', (request) => {
|
||||
const headers: { [x: string]: string } = {};
|
||||
|
||||
for (let [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(headers));
|
||||
});
|
||||
|
||||
addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(router.handle(event.request));
|
||||
|
|
15
src/backend-worker/roleypoly.config.js
Normal file
15
src/backend-worker/roleypoly.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const reexportEnv = (keys = []) => {
|
||||
return keys.reduce((acc, key) => ({ ...acc, [key]: process.env[key] }), {});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
environment: reexportEnv([
|
||||
'BOT_CLIENT_ID',
|
||||
'BOT_CLIENT_SECRET',
|
||||
'BOT_TOKEN',
|
||||
'UI_PUBLIC_URI',
|
||||
'API_PUBLIC_URI',
|
||||
'ROOT_USERS',
|
||||
]),
|
||||
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'],
|
||||
};
|
|
@ -35,11 +35,12 @@ export class Router {
|
|||
}
|
||||
|
||||
handle(request: Request): Promise<Response> | Response {
|
||||
if (request.url === '/') {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === '/' || url.pathname === '') {
|
||||
return this.fallbacks.root(request);
|
||||
}
|
||||
const lowerMethod = request.method.toLowerCase();
|
||||
const url = new URL(request.url);
|
||||
const rootPath = url.pathname.split('/')[1];
|
||||
const handler = this.routingTree[lowerMethod]?.[rootPath];
|
||||
|
||||
|
|
|
@ -9,6 +9,6 @@
|
|||
"./**/*.ts",
|
||||
"../../node_modules/@cloudflare/workers-types/index.d.ts"
|
||||
],
|
||||
"exclude": ["./**/*.spec.ts"],
|
||||
"exclude": ["./**/*.spec.ts", "./dist/**"],
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const self = (global as any) as Record<string, string>;
|
||||
|
||||
const env = (key: string) => self[key] ?? process?.env[key] ?? '';
|
||||
const env = (key: string) => self[key] ?? '';
|
||||
|
||||
const safeURI = (x: string) => x.replace(/\/$/, '');
|
||||
const list = (x: string) => x.split(',');
|
||||
|
||||
export const botClientID = env('BOT_CLIENT_ID');
|
||||
export const botClientSecret = env('BOT_CLIENT_SECRET');
|
||||
export const botToken = env('BOT_TOKEN');
|
||||
export const uiPublicURI = safeURI(env('UI_PUBLIC_URI'));
|
||||
export const apiPublicURI = safeURI(env('API_PUBLIC_URI'));
|
||||
export const rootUsers = list(env('ROOT_USERS'));
|
||||
export const kvPrefix = env('KV_PREFIX');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue