Created
April 16, 2018 21:20
-
-
Save svdoever/2f432f02334bce6e2c2c23785f27f9b3 to your computer and use it in GitHub Desktop.
Hypernova server implementation suporting async rendering
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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