chore: add cloudflare worker emulator and supporting bits. now fully offline!

This commit is contained in:
41666 2020-12-11 21:09:55 -05:00
parent 6ddb3e3192
commit a44983b088
10 changed files with 601 additions and 258 deletions

View 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,
};

View 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');