Skip to content

Instantly share code, notes, and snippets.

@svdoever
Created April 16, 2018 21:20
Show Gist options
  • Save svdoever/2f432f02334bce6e2c2c23785f27f9b3 to your computer and use it in GitHub Desktop.
Save svdoever/2f432f02334bce6e2c2c23785f27f9b3 to your computer and use it in GitHub Desktop.
Hypernova server implementation suporting async rendering
// hypernova server implementation with support for async react components
// Serge van den Oever, Macaw
//
// If the hypernova server is run from iisnode we can access the appsettings in the web.config as
// environment variable, i.e.
// <configuration><appSettings><add key="GLOBAL_PREFIX" value="somevalue"></appSettings>...
// then use var globalPrefix = process.env.GLOBAL_PREFIX; to access the setting
// Example post body:
// {
// "sheep": {
// "name": "HypernovaSheep",
// "data": { "name": "Abracadabra" }
// },
// "counter": {
// "name": "HypernovaCounter",
// "data": { "counter": 5 },
// "metadata": { "strategy": "asyncRedux", "timeout": 2500, "baseUrl": "http://api.anwbcamping.nl" }
// }
// }
// hypernova server configuration
const defaultHypernovaPort = 8080;
const defaultTimeoutMilliseconds = 30 * 1000;
const bundle = require('./server-bundle.js');
const hypernova = require('hypernova/server');
const domainTask = require('domain-task');
const domainTaskRun = require('domain-task/main').run;
const domainTaskBaseUrl = require('domain-task/main').baseUrl;
const domain = require('domain');
const path = require('path');
const express = require('express');
const connectDomain = require('connect-domain');
function getHypernovaPort() {
// if (!!process.env.HypernovaPort) {
// return parseInt(process.env.HypernovaPort);
// }
return process.env.PORT || defaultHypernovaPort;
}
function getTimeOutMilliseconds(metadata) {
if (!!metadata && !!metadata.timeout) {
return metadata.timeout;
}
if (!!process.env.HypernovaAsyncComponentTimeoutMilliseconds) {
return parseInt(process.env.HypernovaAsyncComponentTimeoutMilliseconds);
}
return defaultTimeoutMilliseconds;
}
function getBaseUrl(metadata) {
if(!!metadata && !!metadata.baseUrl) {
return metadata.baseUrl;
}
if (!!process.env.HypernovaFetchBaseUrl) {
return process.env.HypernovaFetchBaseUrl;
}
return `http://localhost:${getHypernovaPort()}`;
}
// https://github.com/aspnet/JavaScriptServices/blob/dev/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts
function wrapWithTimeout(promise, timeoutMilliseconds, timeoutRejectionValue) {
return new Promise((resolve, reject) => {
const timeoutTimer = setTimeout(() => {
reject(timeoutRejectionValue);
}, timeoutMilliseconds);
promise.then(
resolvedValue => {
clearTimeout(timeoutTimer);
resolve(resolvedValue);
},
rejectedValue => {
clearTimeout(timeoutTimer);
reject(rejectedValue);
}
)
});
}
// https://github.com/aspnet/JavaScriptServices/blob/dev/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts
function bindPromiseContinuationsToDomain(promise, domainInstance) {
const originalThen = promise.then;
promise.then = (function then(resolve, reject) {
if (typeof resolve === 'function') {
resolve = domainInstance.bind(resolve);
}
if (typeof reject === 'function') {
reject = domainInstance.bind(reject);
}
return originalThen.call(this, resolve, reject);
});
}
function getComponentAsyncRedux(component, context) {
// create a component with Redux store that is initialized with the initial store data
let initializedComponent = component(context.props);
let domainTaskCompletionPromiseResolve;
let domainTaskCompletionPromiseReject;
const domainTaskCompletionPromise = new Promise((resolve, reject) => {
domainTaskCompletionPromiseResolve = resolve;
domainTaskCompletionPromiseReject = reject;
});
domainTaskRun(/* code to run */ () => {
// Workaround for Node bug where native Promise continuations lose their domain context
// (https://github.com/nodejs/node-v0.x-archive/issues/8648)
// The domain.active property is set by the domain-context module
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']);
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
domainTaskBaseUrl(getBaseUrl(context.metadata));
// first render of the initialized component - should register promises using addTask
initializedComponent();
const domainTaskCompletionPromiseWithTimeout = wrapWithTimeout(domainTaskCompletionPromise, getTimeOutMilliseconds(context.metadata));
// When all registered promises are completed do the final rendering
domainTaskCompletionPromiseWithTimeout.then(successResult => {
domainTaskCompletionPromiseResolve(initializedComponent);
}, error => {
domainTaskCompletionPromiseReject(error);
});
}, /* completion callback */ errorOrNothing => {
if (errorOrNothing) {
domainTaskCompletionPromiseReject(errorOrNothing);
} else {
// There are no more ongoing domain tasks (typically data access operations), so we can resolve
// the domain tasks promise which notifies the boot code that it can do its final render.
domainTaskCompletionPromiseResolve(initializedComponent);
}
});
// return a promise that will resolve in the component to be rendered
return domainTaskCompletionPromise;
}
function getComponent(componentName, context) {
component = bundle[componentName];
if (component === null) {
console.log(`Component '${componentName}' not found in server bundle`);
return null;
}
if (!context.metadata || !context.metadata.strategy) {
return component;
}
switch (context.metadata.strategy) {
case "asyncRedux":
return getComponentAsyncRedux(component, context);
default:
return component;
}
}
console.log('Hypernova server running node version: ' + process.version);
let app = hypernova({
devMode: false,
getComponent: getComponent,
port: getHypernovaPort()
});
// http://andreypopp.github.io/domain-context/
app.use(connectDomain());
// implement your custom express logic
app.use('/images/logo.svg', express.static(path.join(__dirname, '../public/images/logo.svg')));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment