Created
December 16, 2015 19:05
-
-
Save eriktrom/f3a0569c26453316ef9c to your computer and use it in GitHub Desktop.
ember-fastboot-render
This file contains hidden or 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
import Promise from 'bluebird'; | |
import SimpleDOM from 'simple-dom'; | |
import Wreck from 'wreck'; | |
import Redis from 'redis'; | |
import Contextify from 'contextify'; | |
Promise.promisifyAll(Wreck); | |
Promise.promisifyAll(Redis.RedisClient.prototype); | |
export default { | |
visit(options={}) { | |
if (!options.url) throw Error('Must pass url option'); | |
let { url, revision } = options; | |
let bootOptions = { | |
isBrowser: false, | |
document: this.doc, | |
rootElement: this.doc.body, | |
autoRun: false | |
}; | |
return this._createEmberApplication(revision) | |
.then(app => app.visit(url, bootOptions)) | |
.then(instance => { | |
let outerHtmlBody = this.htmlSerializer.serialize(instance.rootElement); | |
let innerHtmlBody = outerHtmlBody.slice(6, outerHtmlBody.length-7); // strip duplicate <body></body> | |
instance = void 0; // TODO: destroy instance, although garbage collection seems to handle this pretty well | |
return { | |
// url: instance.getUrl(), // TODO: for redirects? | |
// title: 'Hello World', // TODO | |
body: innerHtmlBody | |
}; | |
}); | |
}, | |
replaceHtml(options={}) { | |
let { doc, revision } = options; | |
if (!doc) throw Error("'doc' option must be given"); | |
if (!doc.body) throw Error("'doc.body' must be given"); | |
if (!revision) | |
return this._htmlFile().then(html => html.replace("<!-- FASTBOOT_BUILD_BODY -->", doc.body)); // refactor | |
throw Error('HTML replacement failed'); | |
}, | |
/* | |
Private | |
*/ | |
_createEmberApplication(revision) { | |
/* | |
When no revision is passed as a query param, we init/run the application | |
once and memoize it. This is what the world sees, so we want it fast/low memory | |
*/ | |
if (!revision) { | |
if (this._emberApplication) return Promise.resolve(this._emberApplication); | |
return Promise.props({ | |
vendorFile: this._vendorFile(), | |
appFile: this._appFile(), | |
sandbox: Promise.resolve(this.sandbox) | |
}).then(deps => { | |
deps.sandbox.run(deps.vendorFile); | |
deps.sandbox.run(deps.appFile); | |
let app = deps.sandbox.require('hhs-client/app').default; | |
let config = deps.sandbox.require('hhs-client/config/environment').default; | |
config.APP.autoboot = false; | |
// create + memoize app | |
// TODO: make a hapi method to put this into redis LRU cache (reduce mem footprint) | |
this._emberApplication = app.create(config.APP); | |
return this._emberApplication; | |
}); | |
} | |
/* | |
Upon activation of a revision we will restart pm2, busting the memoized | |
current revision above. | |
When we want to test a revision we DO NOT memoize, we simply build a new | |
ember application for the revision and dereference the built application when | |
the request is finished. This is slower by ~500ms and takes ~100mb of extra | |
memory, but its just for our in house checks that before activating a revision | |
the site passes a manual spot check, so all is good, no real traffic will | |
hit this code path | |
*/ | |
// if (revision) { | |
// return Promise.props({ | |
// vendorFile: this._fileForRevision({ revision, fileKey: 'vendorjs' }), | |
// appFile: this._fileForRevision({ revision, fileKey: 'appjs' }), | |
// sandbox: this.sandbox | |
// }).then(deps => { | |
// deps.sandbox.run(deps.vendorFile); | |
// deps.sandbox.run(deps.appFile); | |
// let app = deps.sandbox.require('hhs-client/app').default; | |
// let config = deps.sandbox.require('hhs-client/config/environment').default; | |
// config.APP.autoboot = false; | |
// return app.create(config.APP); | |
// }); | |
// } | |
throw Error('Ember application failed to initialize and run'); | |
}, | |
// private | |
/* | |
Return the hhs-client.js file. This file is DIFFERENT from what ember build outputs | |
in that it does not require certain modules (by excluding them in treeForApp hook). | |
TODO: The only real difference then that ember-cli-fastboot-build **does** | |
is output a different hhs-client.js file, which can exclude modules from | |
being required from vendor.js. Consider if this 'double build' system really | |
warrants this - we could simply wrap the same build output in !isBrowser and | |
be done with it. We will still need to upload all files to s3 and redis, | |
but that's taken care of by ember-cli-deploy-fastboot. | |
During development we return the default revision, which is the result of the | |
the last fastboot build | |
During production we return the 'activated' revision | |
*/ | |
_appFile() { | |
if (this.isProd) return this._appjsForProd(); | |
else return this._appjsForDev(); | |
}, | |
_appjsForDev() { | |
return this.redisClient.getAsync('hhs-client:appjs:default'); | |
}, | |
_appjsForProd() { | |
return this.redisClient.getAsync('hhs-client:appjs:current') | |
.then(this._cacheActiveRevision.bind(this)) | |
.then(activeRevision => this._appjsForProdActiveRevision(activeRevision)); | |
}, | |
_appjsForProdActiveRevision(activeRevision) { | |
return this.redisClient.getAsync(`hhs-client:appjs:${activeRevision}`); | |
}, | |
/* | |
Return the vendor.js file. This file is exactly what ember build outputs | |
TODO: refactor ember-cli-build-fastboot to not duplicate the work of the normal | |
build | |
During development we return the default revision, which is the result of the | |
the last fastboot build | |
During production we return the 'activated' revision | |
*/ | |
_vendorFile() { | |
if (this.isProd) return this._vendorjsForProd(); | |
else return this._vendorjsForDev(); | |
}, | |
_vendorjsForDev() { | |
return this.redisClient.getAsync('hhs-client:vendorjs:default'); | |
}, | |
_vendorjsForProd() { | |
return this.redisClient.getAsync('hhs-client:vendorjs:current') | |
.then(this._cacheActiveRevision.bind(this)) | |
.then(activeRevision => this._vendorjsForProdActiveRevision(activeRevision)); | |
}, | |
_vendorjsForProdActiveRevision(activeRevision) { | |
return this.redisClient.getAsync(`hhs-client:vendorjs:${activeRevision}`); | |
}, | |
/* | |
Return the FastBoot HTML file | |
During development we return the default revision, which is the result of the | |
the last fastboot build | |
During production we return the 'activated' revision | |
*/ | |
_htmlFile() { | |
if (this.isProd) return this._htmlForProd(); | |
else return this._htmlForDev(); | |
}, | |
_htmlForDev() { | |
return this.redisClient.getAsync('hhs-client:index:default'); | |
}, | |
_htmlForProd() { | |
return this.redisClient.getAsync('hhs-client:index:current-content'); | |
}, | |
_cacheActiveRevision(activeRevision) { | |
if (this.activeRevision) { | |
return this.activeRevision; | |
} else { | |
this.activeRevision = activeRevision; | |
return activeRevision; | |
} | |
}, | |
/* | |
Return a file for a revision. Does not try to cache current revision | |
and thus its a dynamic function. This will only run when a query param | |
of ?revision=abcd is passed to the url of the Ember App. | |
If the provided revision does not exist, the returned page will show | |
a list of all the revisions available. | |
*/ | |
// _fileForRevision(options={}) { | |
// let { revision, fileKey } = options; | |
// if (!revision) throw Error('Must pass a revision option'); | |
// if (!fileKey) throw Error('Must pass a fileKey option'); | |
// return this.redisClient.getAsync(`hhs-client:${fileKey}:${revision}`) | |
// .then(content => { | |
// if (!content) return this._showRevisionList(); | |
// else return content; | |
// }); | |
// }, | |
// _showRevisionList() { | |
// return this.redisClient.zrevrangeAsync('hhs-client:index:revisions', 0, -1) | |
// .then(revisionList => { | |
// return ` | |
// <h1>Bad Revision Number Given as Query Param. Try Again.</h1> | |
// ${revisionList} | |
// `; | |
// }); | |
// }, | |
/* | |
Memoized Getters | |
*/ | |
get redisClient() { | |
if (this._redisClient) return this._redisClient; | |
this._redisClient = Redis.createClient(); | |
return this._redisClient; | |
}, | |
get htmlSerializer() { | |
if (this._htmlSerializer) return this._htmlSerializer; | |
this._htmlSerializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap); | |
return this._htmlSerializer; | |
}, | |
/* | |
Live Getters | |
*/ | |
get isProd() { | |
if (process.env.NODE_ENV === 'production') return true; | |
else return false; | |
}, | |
// memoizing this will cache the response, for all url's, fyi | |
get doc() { | |
return new SimpleDOM.Document(); | |
}, | |
// can we memoize this? | |
get sandbox() { | |
let fqdn = 'http://127.0.0.1'; | |
let localOrigin = this.isProd ? fqdn : `${fqdn}:8000`; | |
// build sandbox | |
let sandbox = { | |
console, // hackz - use vm.global* for fix | |
setTimeout, // hackz - use vm.global* for fix | |
clearTimeout, // hackz - use vm.global* for fix | |
module: { exports: {} }, // make jQuery think we're in node | |
matchMedia: true, // hackz - wrap liquid-fire app.import('matchMedia') for fix | |
Wreck, | |
localOrigin // hackz: use server.info.uri, but need this to be in a plugin | |
}; | |
sandbox.window = sandbox; | |
sandbox.window.self = sandbox; // remove me after refactor | |
Contextify(sandbox); // die soon bitch | |
return sandbox; | |
} | |
}; |
error happens on line 24/25 - not sure yet - but application boots, just first app instance used to throw, now they all throw
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Route Handler:
Note, its not clean, but when it becomes a hapi plugin, it will be very powerful b/c other plugins, like auth/session support and catbox-redis will make it fast as all heck