UNPKG

@sigi/ssr

Version:

Server side rendering support for sigi framework

205 lines (198 loc) 7.62 kB
'use strict'; var core = require('@sigi/core'); var di = require('@sigi/di'); var jsxRuntime = require('react/jsx-runtime'); var serialize = require('serialize-javascript'); var pathToRegexp = require('path-to-regexp'); const ScriptId = 'sigi-persisted-data'; class StateToPersist { constructor(dataToPersist, actionsToRetry) { this.dataToPersist = dataToPersist; this.actionsToRetry = actionsToRetry; } extractToScriptString(withScriptTag = true) { if (this.dataToPersist == null) { return ''; } const contentScript = this.serialize(); return withScriptTag ? `<script id="${ScriptId}">${contentScript}</script>` : contentScript; } renderToJSX() { if (this.dataToPersist == null) { return null; } return jsxRuntime.jsx("script", { id: ScriptId, dangerouslySetInnerHTML: { __html: this.serialize() } }); } renderToDocument(doc) { if (this.dataToPersist == null) { return doc; } const endBodyPosition = doc.indexOf('</body>'); if (endBodyPosition === -1) { return doc; } return doc.substr(0, endBodyPosition) + this.extractToScriptString() + doc.substring(endBodyPosition); } extractToJSONScriptString() { if (this.dataToPersist == null) { return ''; } const stateContent = serialize(this.dataToPersist, { isJSON: true }); return `<script id="${core.GLOBAL_KEY_SYMBOL}" type="application/json">${stateContent}</script><script id="${core.RETRY_KEY_SYMBOL}" type="application/json">${JSON.stringify(this.actionsToRetry)}</script>`; } serialize() { const content = serialize(this.dataToPersist, { isJSON: true }); return `window['${core.GLOBAL_KEY_SYMBOL}']=${content};window['${core.RETRY_KEY_SYMBOL}']=${JSON.stringify(this.actionsToRetry)}`; } } const SKIP_SYMBOL = Symbol('skip-symbol'); const runSSREffects = (ctx, modules, config = {}) => { const stateToSerialize = {}; const actionsToRetry = {}; const { providers, timeout = 1 } = config; const injector = di.rootInjector.createChild([...modules, ...(providers ?? [])]); const cleanupFns = []; let timer; let terminatedCount = 0; let effectsCount = 0; const moduleInstanceCache = new Map(); injector.serverCache = moduleInstanceCache; const pendingState = new Promise((resolve, reject) => { if (!modules.length) { return resolve(); } timer = setTimeout(() => { reject(new Error('Terminate timeout')); }, timeout * 1000); for (const constructor of modules) { let isAllSkipped = true; const ssrActionsMeta = core.getSSREffectMeta(constructor.prototype, []); const effectModuleInstance = injector.getInstance(constructor); moduleInstanceCache.set(constructor, effectModuleInstance); const { store, moduleName } = effectModuleInstance; effectsCount += ssrActionsMeta.length; const subscription = store.action$.subscribe({ next: ({ type, payload }) => { isAllSkipped = false; if (type === core.RETRY_ACTION_TYPE_SYMBOL) { const { name } = payload; if (!actionsToRetry[moduleName]) { actionsToRetry[moduleName] = [name]; } else { actionsToRetry[moduleName].push(name); } } if (type === core.TERMINATE_ACTION_TYPE_SYMBOL) { terminatedCount++; } if (terminatedCount === effectsCount) { resolve(); } }, error: (e) => { reject(e); }, }); for (const ssrActionMeta of ssrActionsMeta) { if (ssrActionMeta.payloadGetter) { let maybeDeferredPayload; try { maybeDeferredPayload = ssrActionMeta.payloadGetter(ctx, SKIP_SYMBOL); } catch (e) { return reject(e); } Promise.resolve(maybeDeferredPayload) .then((payload) => { if (payload !== SKIP_SYMBOL) { isAllSkipped = false; store.dispatch({ type: ssrActionMeta.action, payload, store, }); } else { if (!actionsToRetry[moduleName]) { actionsToRetry[moduleName] = [ssrActionMeta.action]; } else { actionsToRetry[moduleName].push(ssrActionMeta.action); } effectsCount--; if (terminatedCount === effectsCount) { resolve(); } } }) .catch((e) => { reject(e); }); } else { isAllSkipped = false; store.dispatch({ type: ssrActionMeta.action, payload: undefined, store, }); } } cleanupFns.push(() => { subscription.unsubscribe(); store.dispose(); !isAllSkipped && (stateToSerialize[moduleName] = store.state); }); } if (!effectsCount) { resolve(); } }) .then(() => { if (timer) { clearTimeout(timer); timer = undefined; } for (const cleanup of cleanupFns) { cleanup(); } return new StateToPersist(stateToSerialize, actionsToRetry); }) .catch((e) => { if (timer) { clearTimeout(timer); timer = undefined; } for (const cleanup of cleanupFns) { cleanup(); } throw e; }); return { injector, pendingState }; }; function match(routers, pathFactory) { return (payloadGetter) => { return function payloadGetterWithMatch(ctx, skip) { const requestPath = pathFactory(ctx); if (requestPath && routers.some((router) => pathToRegexp.match(router)(requestPath))) { return payloadGetter(ctx, skip); } return SKIP_SYMBOL; }; }; } function restoreState() { const sigiStateContent = document.getElementById(core.GLOBAL_KEY_SYMBOL)?.textContent; const sigiRetryContent = document.getElementById(core.RETRY_KEY_SYMBOL)?.textContent; if (sigiStateContent) { window[core.GLOBAL_KEY_SYMBOL] = JSON.parse(sigiStateContent); } if (sigiRetryContent) { window[core.RETRY_KEY_SYMBOL] = JSON.parse(sigiRetryContent); } } exports.emitSSREffects = runSSREffects; exports.match = match; exports.restoreState = restoreState; //# sourceMappingURL=index.js.map