@sigi/ssr
Version:
Server side rendering support for sigi framework
162 lines (148 loc) • 5.17 kB
text/typescript
import { EffectModule, TERMINATE_ACTION_TYPE_SYMBOL, getSSREffectMeta, RETRY_ACTION_TYPE_SYMBOL } from '@sigi/core'
import { rootInjector, Injector, Provider } from '@sigi/di'
import { ConstructorOf, Action } from '@sigi/types'
import { StateToPersist } from './state-to-persist'
export type ModuleMeta = ConstructorOf<EffectModule<any>>
export const SKIP_SYMBOL = Symbol('skip-symbol')
/**
* Run all `@Effect({ ssr: true })` decorated effects of given modules and extract latest states.
* `cleanup` function returned must be called before end of responding
*
* @param ctx request context, which will be passed to payloadGetter in SSREffect decorator param
* @param modules used EffectModules
* @param config
* @param config.providers providers to override the default services
* @param config.uuid the same uuid would reuse the same state which was created before
* @param config.timeout seconds to wait before all effects stream out TERMINATE_ACTION, default is `1`.
* @returns EffectModule states
*/
export const runSSREffects = <Context, Returned = any>(
ctx: Context,
modules: ModuleMeta[],
config: {
timeout?: number
providers?: Provider[]
} = {},
): { injector: Injector; pendingState: Promise<StateToPersist<Returned>> } => {
const stateToSerialize = {} as Returned
const actionsToRetry: { [index: string]: string[] } = {}
const { providers, timeout = 1 } = config
const injector = rootInjector.createChild([...modules, ...(providers ?? [])])
const cleanupFns: (() => void)[] = []
let timer: NodeJS.Timeout | undefined
let terminatedCount = 0
let effectsCount = 0
const moduleInstanceCache = new Map()
// @ts-expect-error
injector.serverCache = moduleInstanceCache
const pendingState = new Promise<void>((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 = getSSREffectMeta(constructor.prototype, [])!
const effectModuleInstance: EffectModule<unknown> = 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 === RETRY_ACTION_TYPE_SYMBOL) {
const { name } = payload as Action<{ name: string }>['payload']
if (!actionsToRetry[moduleName]) {
actionsToRetry[moduleName] = [name] as string[]
} else {
actionsToRetry[moduleName].push(name as string)
}
}
if (type === TERMINATE_ACTION_TYPE_SYMBOL) {
terminatedCount++
}
if (terminatedCount === effectsCount) {
resolve()
}
},
error: (e) => {
reject(e)
},
})
for (const ssrActionMeta of ssrActionsMeta) {
if (ssrActionMeta.payloadGetter) {
let maybeDeferredPayload: any
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()
// @ts-expect-error
!isAllSkipped && (stateToSerialize[moduleName] = store.state)
})
}
if (!effectsCount) {
resolve()
}
})
// Could not use `finally` here, because we need support Node.js@10
.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 }
}