Skip to content

Instantly share code, notes, and snippets.

@Toxicable
Created February 5, 2019 21:09
Show Gist options
  • Save Toxicable/228d72c2a1e5c17c2a0d9d21608bacf5 to your computer and use it in GitHub Desktop.
Save Toxicable/228d72c2a1e5c17c2a0d9d21608bacf5 to your computer and use it in GitHub Desktop.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import * as http from 'http';
import * as fs from 'fs';
import * as util from 'util';
import * as path from 'path';
import * as url from 'url';
import * as mime from 'mime-types';
import * as os from 'os';
import { ɵCommonEngine as CommonEngine } from '@nguniversal/common/engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { enableProdMode } from '@angular/core';
const statAsync = util.promisify(fs.stat);
const { Worker, isMainThread, parentPort, threadId } = require('worker_threads');
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./server/main');
const STATIC_FILE_ROOT = '/dist/browser';
const documentFilePath = path.join(process.cwd(), STATIC_FILE_ROOT, 'index.html');
let requestId = 0;
function startServer() {
let workerPoolCurrentIndex = 0;
// leave 1 cpu for the main thread
const numberOfWorkers = os.cpus().length - 1;
const workerPool: any[] = Array(numberOfWorkers).fill(null).map(_ => new Worker(__filename));
const PORT = process.env.PORT || 4000;
const server = http.createServer(async (req, res) => {
const uri = url.parse(req.url).pathname;
console.log(uri);
// we use this to set the browser folder as a static root
const fileUri = path.join(process.cwd(), STATIC_FILE_ROOT, uri);
// might not fit all use cases, but in general your application urls shouldn't have dots in them
if (fileUri.includes('.')) {
// apparently fs.exists (async version is deprecated)
const exists = fs.existsSync(fileUri);
if (!exists) {
// if there's a dot in the path and nothing on disk, tell them we cant find anything
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.write('404 Not Found\n');
res.end();
return;
}
const stat = await statAsync(fileUri);
// send thatd down
res.writeHead(200, {
'Content-Type': mime.lookup(fileUri) || 'application/octet-stream',
'Content-Length': stat.size
});
// send the file down as a stream
// avoids in issues with reading the file into RAM
const readStream = fs.createReadStream(fileUri);
readStream.pipe(res);
return;
} else {
console.log(`using worker index=${workerPoolCurrentIndex}`);
const worker = workerPool[workerPoolCurrentIndex];
// round robin through out workers
workerPoolCurrentIndex = workerPoolCurrentIndex === workerPool.length - 1 ? 0 : workerPoolCurrentIndex + 1;
const myId = requestId;
requestId = requestId + 1;
const msgHander = (buff: WorkerRenderResponse) => {
if (buff.id === myId) {
const html = bufferToString(buff.html);
res.writeHead(200, {
'Content-Type': 'text/html',
});
res.end(html);
worker.removeListener('message', msgHander);
worker.removeListener('error', errorHandler);
}
};
const errorHandler = (err: any) => {
res.writeHead(400);
res.end(err);
worker.removeListener('message', msgHander);
worker.removeListener('error', errorHandler);
};
worker.on('message', msgHander);
worker.once('error', errorHandler);
worker.postMessage({id: myId, uri});
}
});
server.listen(PORT);
}
function startWorker() {
console.log(`worker started, threadId=${threadId}`);
enableProdMode();
const engine = new CommonEngine(AppServerModuleNgFactory, [
provideModuleMap(LAZY_MODULE_MAP)
]);
parentPort.on('message', async (msg: WorkerRenderRequest) => {
console.log(`worker threadId=${threadId}, rendering`);
const html = await engine.render({
url: msg.uri,
bootstrap: AppServerModuleNgFactory,
documentFilePath
});
const response = stringToBuffer(html);
parentPort.postMessage({id: msg.id, html: response});
});
}
function bufferToString(buf: SharedArrayBuffer): string {
const array = new Uint8Array(buf);
return String.fromCharCode.apply(null, array);
}
function stringToBuffer(str: string) {
// since the html charset will be UTF-8 we only need 1 byte per character
const bytes = str.length * 1;
const buffer = new SharedArrayBuffer(bytes);
const arrayBuffer = new Uint8Array(buffer);
for (let i = 0, strLen = str.length; i < strLen; i++) {
arrayBuffer[i] = str.charCodeAt(i);
}
return buffer;
}
function main() {
if (isMainThread) {
startServer();
} else {
startWorker();
}
}
main();
interface WorkerRenderRequest {
id: number;
uri: string;
}
interface WorkerRenderResponse {
id: number;
html: SharedArrayBuffer;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment