@sigi/ssr
Version:
Server side rendering support for sigi framework
205 lines (198 loc) • 7.62 kB
JavaScript
;
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