Skip to content

Commit

Permalink
Merge pull request #1225 from interval/global-error-handler
Browse files Browse the repository at this point in the history
Add global error callbacks to Interval class constructor
  • Loading branch information
jacobmischka authored Apr 24, 2023
2 parents f3c2fda + 83ae24f commit b69f0a9
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 50 deletions.
141 changes: 115 additions & 26 deletions src/classes/IntervalClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ import type {
PageError,
IntervalRouteDefinitions,
IntervalPageHandler,
IntervalErrorHandler,
} from '../types'
import type { DataChannelConnection } from './DataChannelConnection'
import type { IceServer } from './DataChannelConnection'
import TransactionLoadingState from './TransactionLoadingState'
import { Interval, InternalConfig, IntervalError } from '..'
import Page from './Page'
import Action from './Action'
import {
Layout,
BasicLayout,
Expand Down Expand Up @@ -122,6 +124,7 @@ export default class IntervalClient {
#resolveShutdown: (() => void) | undefined
#config: InternalConfig

#routes: Map<string, Action | Page> = new Map()
#actionDefinitions: ActionDefinition[] = []
#pageDefinitions: PageDefinition[] = []
#actionHandlers: Map<string, IntervalActionHandler> = new Map()
Expand All @@ -136,6 +139,8 @@ export default class IntervalClient {
environment: ActionEnvironment | undefined
#forcePeerMessages = false

#onError: IntervalErrorHandler | undefined

constructor(interval: Interval, config: InternalConfig) {
this.#interval = interval
this.#apiKey = config.apiKey
Expand Down Expand Up @@ -185,43 +190,55 @@ export default class IntervalClient {
if (config.setHostHandlers) {
config.setHostHandlers(this.#createRPCHandlers())
}

if (config.onError) {
this.#onError = config.onError
}
}

async #walkRoutes() {
const routes = new Map<string, Action | Page>()

const pageDefinitions: PageDefinition[] = []
const actionDefinitions: (ActionDefinition & { handler: undefined })[] = []
const actionHandlers = new Map<string, IntervalActionHandler>()
const pageHandlers = new Map<string, IntervalPageHandler>()

function walkRouter(groupSlug: string, router: Page) {
function walkRouter(groupSlug: string, page: Page) {
routes.set(groupSlug, page)

pageDefinitions.push({
slug: groupSlug,
name: router.name,
description: router.description,
hasHandler: !!router.handler,
unlisted: router.unlisted,
access: router.access,
name: page.name,
description: page.description,
hasHandler: !!page.handler,
unlisted: page.unlisted,
access: page.access,
})

if (router.handler) {
pageHandlers.set(groupSlug, router.handler)
if (page.handler) {
pageHandlers.set(groupSlug, page.handler)
}

for (const [slug, def] of Object.entries(router.routes)) {
for (let [slug, def] of Object.entries(page.routes)) {
if (def instanceof Page) {
walkRouter(`${groupSlug}/${slug}`, def)
} else {
const fullSlug = `${groupSlug}/${slug}`

if (!(def instanceof Action)) {
def = new Action(def)
routes.set(fullSlug, def)
}

actionDefinitions.push({
groupSlug,
slug,
...('handler' in def ? def : {}),
...def,
handler: undefined,
})

actionHandlers.set(
`${groupSlug}/${slug}`,
'handler' in def ? def.handler : def
)
actionHandlers.set(fullSlug, def.handler)
}
}
}
Expand All @@ -247,28 +264,33 @@ export default class IntervalClient {
}
}

const routes = {
const allRoutes = {
...this.#config.actions,
...this.#config.groups,
...fileSystemRoutes,
...this.#config.routes,
}

if (routes) {
for (const [slug, def] of Object.entries(routes)) {
if (def instanceof Page) {
walkRouter(slug, def)
} else {
actionDefinitions.push({
slug,
...('handler' in def ? def : {}),
handler: undefined,
})
actionHandlers.set(slug, 'handler' in def ? def.handler : def)
for (let [slug, def] of Object.entries(allRoutes)) {
if (def instanceof Page) {
walkRouter(slug, def)
} else {
if (!(def instanceof Action)) {
def = new Action(def)
}

actionDefinitions.push({
slug,
...def,
handler: undefined,
})

routes.set(slug, def)
actionHandlers.set(slug, def.handler)
}
}

this.#routes = routes
this.#pageDefinitions = pageDefinitions
this.#actionDefinitions = actionDefinitions
this.#actionHandlers = actionHandlers
Expand Down Expand Up @@ -1148,6 +1170,16 @@ export default class IntervalClient {
}
}

this.#onError?.({
error: err,
route: action.slug,
routeDefinition: this.#routes.get(action.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})

const result: ActionResultSchema = {
schemaVersion: TRANSACTION_RESULT_SCHEMA_VERSION,
status: 'FAILURE',
Expand Down Expand Up @@ -1545,6 +1577,15 @@ export default class IntervalClient {
page.title = page.title()
} catch (err) {
this.#logger.error(err)
this.#onError?.({
error: err,
route: ctx.page.slug,
routeDefinition: this.#routes.get(ctx.page.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})
errors.push(pageError(err, 'title'))
}
}
Expand All @@ -1559,6 +1600,15 @@ export default class IntervalClient {
})
.catch(err => {
this.#logger.error(err)
this.#onError?.({
error: err,
route: ctx.page.slug,
routeDefinition: this.#routes.get(ctx.page.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})
errors.push(pageError(err, 'title'))
scheduleSendPage()
})
Expand All @@ -1570,6 +1620,15 @@ export default class IntervalClient {
page.description = page.description()
} catch (err) {
this.#logger.error(err)
this.#onError?.({
error: err,
route: ctx.page.slug,
routeDefinition: this.#routes.get(ctx.page.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})
errors.push(pageError(err, 'description'))
}
}
Expand All @@ -1584,6 +1643,15 @@ export default class IntervalClient {
})
.catch(err => {
this.#logger.error(err)
this.#onError?.({
error: err,
route: ctx.page.slug,
routeDefinition: this.#routes.get(ctx.page.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})
errors.push(pageError(err, 'description'))
scheduleSendPage()
})
Expand Down Expand Up @@ -1628,6 +1696,16 @@ export default class IntervalClient {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
err => {
this.#logger.error(err)
this.#onError?.({
error: err,
route: ctx.page.slug,
routeDefinition: this.#routes.get(ctx.page.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})

if (err instanceof IOError && err.cause) {
errors.push(pageError(err.cause, 'children'))
} else {
Expand All @@ -1644,6 +1722,17 @@ export default class IntervalClient {
.catch(async err => {
this.#logger.error('Error in page:', err)
errors.push(pageError(err))

this.#onError?.({
error: err,
route: ctx.page.slug,
routeDefinition: this.#routes.get(ctx.page.slug),
params: ctx.params,
environment: ctx.environment,
user: ctx.user,
organization: ctx.organization,
})

const pageLayout: LayoutSchemaInput = {
kind: 'BASIC',
errors,
Expand Down
3 changes: 3 additions & 0 deletions src/examples/basic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,9 @@ const interval = new Interval({
apiKey: 'alex_dev_kcLjzxNFxmGLf0aKtLVhuckt6sziQJtxFOdtM19tBrMUp5mj',
logLevel: 'debug',
endpoint: 'ws://localhost:3000/websocket',
onError: props => {
console.debug('onError', props)
},
routes: {
sidebar_depth,
echoContext,
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
IntervalPageStore,
PageCtx,
IntervalActionDefinition,
IntervalErrorHandler,
} from './types'
import IntervalError from './classes/IntervalError'
import IntervalClient, {
Expand Down Expand Up @@ -69,6 +70,8 @@ export interface InternalConfig {

closeUnresponsiveConnectionTimeoutMs?: number
reinitializeBatchTimeoutMs?: number
onError?: IntervalErrorHandler

/* @internal */ getClientHandlers?: () =>
| DuplexRPCHandlers<ClientSchema>
| undefined
Expand Down
64 changes: 40 additions & 24 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,37 @@ export type Prettify<T> = {
[K in keyof T]: T[K]
} & {}

export type CtxUser = {
/**
* The email of the user running the action or page.
*/
email: string
/**
* The first name of the user running the action or page, if present.
*/
firstName: string | null
/**
* The last name of the user running the action or page, if present.
*/
lastName: string | null
}

export type CtxOrganization = {
/**
* The name of the organization.
*/
name: string
/**
* The unique slug of the organization.
*/
slug: string
}

export type ActionCtx = {
/**
* Basic information about the user running the action or page.
*/
user: {
/**
* The email of the user running the action or page.
*/
email: string
/**
* The first name of the user running the action or page, if present.
*/
firstName: string | null
/**
* The last name of the user running the action or page, if present.
*/
lastName: string | null
}
user: CtxUser
/**
* A key/value object containing the query string URL parameters of the running action or page.
*/
Expand Down Expand Up @@ -130,16 +143,7 @@ export type ActionCtx = {
/**
* Basic information about the organization.
*/
organization: {
/**
* The name of the organization.
*/
name: string
/**
* The unique slug of the organization.
*/
slug: string
}
organization: CtxOrganization
/**
* Information about the currently running action.
*/
Expand Down Expand Up @@ -542,3 +546,15 @@ export type PageError = {
cause?: string
layoutKey?: keyof BasicLayoutConfig
}

export type IntervalErrorProps = {
error: Error | unknown
route: string
routeDefinition: Action | Page | undefined
params: SerializableRecord
environment: ActionEnvironment
user: CtxUser
organization: CtxOrganization
}

export type IntervalErrorHandler = (props: IntervalErrorProps) => void

0 comments on commit b69f0a9

Please sign in to comment.