Skip to content

Commit

Permalink
fix: improve error message upon using Telefunc for SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
brillout committed Nov 15, 2022
1 parent 95b54df commit 4d17fae
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 73 deletions.
50 changes: 15 additions & 35 deletions telefunc/node/server/getContext.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,61 @@
export { getContext }
export { getContextOptional }
export { provideTelefuncContext }
export { Telefunc }
export { restoreContext }
export { installAsyncMode }
export { isAsyncMode }
export { restoreContext }
export type { Telefunc }

import {
getContext_sync,
provideTelefuncContext_sync,
restoreContext_sync,
getContextOptional_sync
} from './getContext/sync'
import { assert, assertUsage, assertWarning, isObject, getGlobalObject } from '../utils'
import { getContext_sync, provideTelefuncContext_sync, restoreContext_sync } from './getContext/sync'
import { assert, isObject, getGlobalObject } from '../utils'
import type { Telefunc } from './getContext/TelefuncNamespace'
import { provideErrMsg } from './getContext/provideErrMessage'

type GetContext = () => Telefunc.Context
type ProvideTelefuncContext = (context: Telefunc.Context) => void
type RestoreContext = (context: null | Telefunc.Context) => void
type GetContextOptional = () => null | Telefunc.Context

const globalObject = getGlobalObject<{
getContext: GetContext
provideTelefuncContext: ProvideTelefuncContext
restoreContext: RestoreContext
neverProvided: boolean
provideTelefuncContext: ProvideTelefuncContext
isAsyncMode: boolean
getContextOptional: GetContextOptional
}>('getContext.ts', {
getContext: getContext_sync,
provideTelefuncContext: provideTelefuncContext_sync,
restoreContext: restoreContext_sync,
getContextOptional: getContextOptional_sync,
isAsyncMode: false,
neverProvided: true
provideTelefuncContext: provideTelefuncContext_sync,
isAsyncMode: false
})

function getContext<Context extends object = Telefunc.Context>(): Context {
assertUsage(globalObject.neverProvided === false, provideErrMsg)
const context = globalObject.getContext()
assert(isObject(context))
return context as Context
}

function getContextOptional() {
const context = globalObject.getContextOptional()
return context
}

function provideTelefuncContext<Context extends object = Telefunc.Context>(context: Context) {
// TODO: check whether it's possible to deprecate `provideTelefuncContext()` for Nuxt
// assertWarning(false, 'provideTelefuncContext() is deprecated', { onlyOnce: true })
globalObject.neverProvided = false
/* TODO: check whether it's possible to deprecate `provideTelefuncContext()` for Nuxt.
assertWarning(false, 'provideTelefuncContext() is deprecated', { onlyOnce: true })
*/
assert(isObject(context))
globalObject.provideTelefuncContext(context)
}

function restoreContext(context: null | Telefunc.Context) {
globalObject.neverProvided = false
return globalObject.restoreContext(context)
assert(context === null || isObject(context))
globalObject.restoreContext(context)
}

function installAsyncMode({
getContext_async,
provideTelefuncContext_async,
restoreContext_async,
getContextOptional_async
restoreContext_async
}: {
getContext_async: GetContext
provideTelefuncContext_async: ProvideTelefuncContext
restoreContext_async: RestoreContext
getContextOptional_async: GetContextOptional
}): void {
globalObject.getContext = getContext_async
globalObject.provideTelefuncContext = provideTelefuncContext_async
globalObject.restoreContext = restoreContext_async
globalObject.getContextOptional = getContextOptional_async
globalObject.provideTelefuncContext = provideTelefuncContext_async
globalObject.isAsyncMode = true
}
function isAsyncMode(): boolean {
Expand Down
25 changes: 13 additions & 12 deletions telefunc/node/server/getContext/async.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import { AsyncLocalStorage } from 'async_hooks'
import { assert, isObject, getGlobalObject, assertUsage } from '../../utils'
import { assert, assertWarning, isObject, getGlobalObject, assertUsage } from '../../utils'
import { installAsyncMode } from '../getContext'
import { provideErrMsg } from './provideErrMessage'
import type { Telefunc } from './TelefuncNamespace'

export { getContext_async }
export { provideTelefuncContext_async }

const globalObject = getGlobalObject<{ asyncStore?: AsyncLocalStorage<Telefunc.Context> }>('getContext/async.ts', {})

installAsyncMode({ getContext_async, provideTelefuncContext_async, restoreContext_async, getContextOptional_async })
installAsyncMode({ getContext_async, provideTelefuncContext_async, restoreContext_async })

function getContext_async(): Telefunc.Context {
assert(globalObject.asyncStore)
const errMsg = '[getContext()] Make sure to call provideTelefuncContext() before calling getContext()'
assertUsage(globalObject.asyncStore, errMsg)
const context = globalObject.asyncStore.getStore()
assert(context === undefined || isObject(context))
assertUsage(context, provideErrMsg)
assertUsage(context, errMsg)
return context
}

function provideTelefuncContext_async(context: Telefunc.Context): void {
assert(isObject(context))
assertUsage(isObject(context), '[provideTelefuncContext(context)] Argument `context` should be an object')
globalObject.asyncStore = globalObject.asyncStore || new AsyncLocalStorage()
globalObject.asyncStore.enterWith(context)
}

function getContextOptional_async(): any {
assert(false)
}

function restoreContext_async(..._args: any[]): any {
assert(false)
function restoreContext_async(context: null | Telefunc.Context): any {
assert(context === null || isObject(context))
assertWarning(
!context,
'When using `provideTelefuncContext()` (i.e. Async Hooks), then providing the `context` object to the server middleware `telefunc()` has no efect.',
{ onlyOnce: true }
)
}
2 changes: 0 additions & 2 deletions telefunc/node/server/getContext/provideErrMessage.ts

This file was deleted.

50 changes: 30 additions & 20 deletions telefunc/node/server/getContext/sync.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,58 @@
import { assert, isObject, getGlobalObject, assertUsage } from '../../utils'
import { provideErrMsg } from './provideErrMessage'
import type { Telefunc } from './TelefuncNamespace'

export { getContext_sync }
export { provideTelefuncContext_sync }
export { restoreContext_sync }
export { getContextOptional_sync }

// Using the global scope is needed for Next.js. I'm guessing that Next.js is including the `node_modules/` files in a seperate bundle than user files.
const globalObject = getGlobalObject<{ context: null | Telefunc.Context; isRestored: boolean }>('getContext/sync.ts', {
const globalObject = getGlobalObject<{
context: null | Telefunc.Context
hasRestoreAccess: boolean
neverProvided: boolean
neverRestored: boolean
}>('getContext/sync.ts', {
context: null,
isRestored: false
hasRestoreAccess: false,
neverProvided: true,
neverRestored: true
})

function getContext_sync(): Telefunc.Context {
const { context, isRestored } = globalObject
assert(context === null || isObject(context))
const errMsg = isRestored
? provideErrMsg
: '[getContext()] Cannot access context object, see https://telefunc.com/getContext#access'
assertUsage(context !== null, errMsg)
if (globalObject.context === null) {
// Using `neverRestored` to detect SSR doesn't always work.
if (globalObject.neverRestored) {
assertUsage(false, 'Using Telefunc to fetch the initial data of your page is discouraged, see https://telefunc.com/initial-page-data') // prettier-ignore
}
if (globalObject.hasRestoreAccess || globalObject.neverProvided) {
assertUsage(false, '[getContext()] Make sure to provide a context object, see https://telefunc.com/getContext#provide') // prettier-ignore
} else {
assertUsage(false, '[getContext()] Cannot access context object, see https://telefunc.com/getContext#access')
}
}
const { context } = globalObject
assert(isObject(context))
return context
}

function getContextOptional_sync() {
return globalObject.context
}

function restoreContext_sync(context: null | Telefunc.Context) {
provide(context, true)
globalObject.neverRestored = false
provide(context)
}

function provideTelefuncContext_sync(context: Telefunc.Context) {
provide(context, false)
assertUsage(isObject(context), '[provideTelefuncContext(context)] Argument `context` should be an object')
provide(context)
}

function provide(context: null | Telefunc.Context, isRestored: boolean) {
function provide(context: null | Telefunc.Context) {
assert(context === null || isObject(context))
if (context) {
globalObject.neverProvided = false
globalObject.context = context
}
globalObject.isRestored = isRestored
globalObject.hasRestoreAccess = true
process.nextTick(() => {
globalObject.context = null
globalObject.isRestored = false
globalObject.hasRestoreAccess = false
})
}
6 changes: 2 additions & 4 deletions telefunc/node/server/runTelefunc/executeTelefunction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { executeTelefunction }

import { isAbort, Abort } from '../Abort'
import { restoreContext, Telefunc, isAsyncMode } from '../getContext'
import { restoreContext, Telefunc } from '../getContext'
import type { Telefunction } from '../types'
import { assertUsage, isPromise } from '../../utils'

Expand All @@ -14,9 +14,7 @@ async function executeTelefunction(runContext: {
}) {
const { telefunction, telefunctionArgs } = runContext

if (!isAsyncMode()) {
restoreContext(runContext.providedContext)
}
restoreContext(runContext.providedContext)

let telefunctionError: unknown
let telefunctionHasErrored = false
Expand Down

0 comments on commit 4d17fae

Please sign in to comment.