Skip to content

Commit b69f0a9

Browse files
authored
Merge pull request #1225 from interval/global-error-handler
Add global error callbacks to Interval class constructor
2 parents f3c2fda + 83ae24f commit b69f0a9

4 files changed

Lines changed: 161 additions & 50 deletions

File tree

src/classes/IntervalClient.ts

Lines changed: 115 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ import type {
5151
PageError,
5252
IntervalRouteDefinitions,
5353
IntervalPageHandler,
54+
IntervalErrorHandler,
5455
} from '../types'
5556
import type { DataChannelConnection } from './DataChannelConnection'
5657
import type { IceServer } from './DataChannelConnection'
5758
import TransactionLoadingState from './TransactionLoadingState'
5859
import { Interval, InternalConfig, IntervalError } from '..'
5960
import Page from './Page'
61+
import Action from './Action'
6062
import {
6163
Layout,
6264
BasicLayout,
@@ -122,6 +124,7 @@ export default class IntervalClient {
122124
#resolveShutdown: (() => void) | undefined
123125
#config: InternalConfig
124126

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

142+
#onError: IntervalErrorHandler | undefined
143+
139144
constructor(interval: Interval, config: InternalConfig) {
140145
this.#interval = interval
141146
this.#apiKey = config.apiKey
@@ -185,43 +190,55 @@ export default class IntervalClient {
185190
if (config.setHostHandlers) {
186191
config.setHostHandlers(this.#createRPCHandlers())
187192
}
193+
194+
if (config.onError) {
195+
this.#onError = config.onError
196+
}
188197
}
189198

190199
async #walkRoutes() {
200+
const routes = new Map<string, Action | Page>()
201+
191202
const pageDefinitions: PageDefinition[] = []
192203
const actionDefinitions: (ActionDefinition & { handler: undefined })[] = []
193204
const actionHandlers = new Map<string, IntervalActionHandler>()
194205
const pageHandlers = new Map<string, IntervalPageHandler>()
195206

196-
function walkRouter(groupSlug: string, router: Page) {
207+
function walkRouter(groupSlug: string, page: Page) {
208+
routes.set(groupSlug, page)
209+
197210
pageDefinitions.push({
198211
slug: groupSlug,
199-
name: router.name,
200-
description: router.description,
201-
hasHandler: !!router.handler,
202-
unlisted: router.unlisted,
203-
access: router.access,
212+
name: page.name,
213+
description: page.description,
214+
hasHandler: !!page.handler,
215+
unlisted: page.unlisted,
216+
access: page.access,
204217
})
205218

206-
if (router.handler) {
207-
pageHandlers.set(groupSlug, router.handler)
219+
if (page.handler) {
220+
pageHandlers.set(groupSlug, page.handler)
208221
}
209222

210-
for (const [slug, def] of Object.entries(router.routes)) {
223+
for (let [slug, def] of Object.entries(page.routes)) {
211224
if (def instanceof Page) {
212225
walkRouter(`${groupSlug}/${slug}`, def)
213226
} else {
227+
const fullSlug = `${groupSlug}/${slug}`
228+
229+
if (!(def instanceof Action)) {
230+
def = new Action(def)
231+
routes.set(fullSlug, def)
232+
}
233+
214234
actionDefinitions.push({
215235
groupSlug,
216236
slug,
217-
...('handler' in def ? def : {}),
237+
...def,
218238
handler: undefined,
219239
})
220240

221-
actionHandlers.set(
222-
`${groupSlug}/${slug}`,
223-
'handler' in def ? def.handler : def
224-
)
241+
actionHandlers.set(fullSlug, def.handler)
225242
}
226243
}
227244
}
@@ -247,28 +264,33 @@ export default class IntervalClient {
247264
}
248265
}
249266

250-
const routes = {
267+
const allRoutes = {
251268
...this.#config.actions,
252269
...this.#config.groups,
253270
...fileSystemRoutes,
254271
...this.#config.routes,
255272
}
256273

257-
if (routes) {
258-
for (const [slug, def] of Object.entries(routes)) {
259-
if (def instanceof Page) {
260-
walkRouter(slug, def)
261-
} else {
262-
actionDefinitions.push({
263-
slug,
264-
...('handler' in def ? def : {}),
265-
handler: undefined,
266-
})
267-
actionHandlers.set(slug, 'handler' in def ? def.handler : def)
274+
for (let [slug, def] of Object.entries(allRoutes)) {
275+
if (def instanceof Page) {
276+
walkRouter(slug, def)
277+
} else {
278+
if (!(def instanceof Action)) {
279+
def = new Action(def)
268280
}
281+
282+
actionDefinitions.push({
283+
slug,
284+
...def,
285+
handler: undefined,
286+
})
287+
288+
routes.set(slug, def)
289+
actionHandlers.set(slug, def.handler)
269290
}
270291
}
271292

293+
this.#routes = routes
272294
this.#pageDefinitions = pageDefinitions
273295
this.#actionDefinitions = actionDefinitions
274296
this.#actionHandlers = actionHandlers
@@ -1148,6 +1170,16 @@ export default class IntervalClient {
11481170
}
11491171
}
11501172

1173+
this.#onError?.({
1174+
error: err,
1175+
route: action.slug,
1176+
routeDefinition: this.#routes.get(action.slug),
1177+
params: ctx.params,
1178+
environment: ctx.environment,
1179+
user: ctx.user,
1180+
organization: ctx.organization,
1181+
})
1182+
11511183
const result: ActionResultSchema = {
11521184
schemaVersion: TRANSACTION_RESULT_SCHEMA_VERSION,
11531185
status: 'FAILURE',
@@ -1545,6 +1577,15 @@ export default class IntervalClient {
15451577
page.title = page.title()
15461578
} catch (err) {
15471579
this.#logger.error(err)
1580+
this.#onError?.({
1581+
error: err,
1582+
route: ctx.page.slug,
1583+
routeDefinition: this.#routes.get(ctx.page.slug),
1584+
params: ctx.params,
1585+
environment: ctx.environment,
1586+
user: ctx.user,
1587+
organization: ctx.organization,
1588+
})
15481589
errors.push(pageError(err, 'title'))
15491590
}
15501591
}
@@ -1559,6 +1600,15 @@ export default class IntervalClient {
15591600
})
15601601
.catch(err => {
15611602
this.#logger.error(err)
1603+
this.#onError?.({
1604+
error: err,
1605+
route: ctx.page.slug,
1606+
routeDefinition: this.#routes.get(ctx.page.slug),
1607+
params: ctx.params,
1608+
environment: ctx.environment,
1609+
user: ctx.user,
1610+
organization: ctx.organization,
1611+
})
15621612
errors.push(pageError(err, 'title'))
15631613
scheduleSendPage()
15641614
})
@@ -1570,6 +1620,15 @@ export default class IntervalClient {
15701620
page.description = page.description()
15711621
} catch (err) {
15721622
this.#logger.error(err)
1623+
this.#onError?.({
1624+
error: err,
1625+
route: ctx.page.slug,
1626+
routeDefinition: this.#routes.get(ctx.page.slug),
1627+
params: ctx.params,
1628+
environment: ctx.environment,
1629+
user: ctx.user,
1630+
organization: ctx.organization,
1631+
})
15731632
errors.push(pageError(err, 'description'))
15741633
}
15751634
}
@@ -1584,6 +1643,15 @@ export default class IntervalClient {
15841643
})
15851644
.catch(err => {
15861645
this.#logger.error(err)
1646+
this.#onError?.({
1647+
error: err,
1648+
route: ctx.page.slug,
1649+
routeDefinition: this.#routes.get(ctx.page.slug),
1650+
params: ctx.params,
1651+
environment: ctx.environment,
1652+
user: ctx.user,
1653+
organization: ctx.organization,
1654+
})
15871655
errors.push(pageError(err, 'description'))
15881656
scheduleSendPage()
15891657
})
@@ -1628,6 +1696,16 @@ export default class IntervalClient {
16281696
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
16291697
err => {
16301698
this.#logger.error(err)
1699+
this.#onError?.({
1700+
error: err,
1701+
route: ctx.page.slug,
1702+
routeDefinition: this.#routes.get(ctx.page.slug),
1703+
params: ctx.params,
1704+
environment: ctx.environment,
1705+
user: ctx.user,
1706+
organization: ctx.organization,
1707+
})
1708+
16311709
if (err instanceof IOError && err.cause) {
16321710
errors.push(pageError(err.cause, 'children'))
16331711
} else {
@@ -1644,6 +1722,17 @@ export default class IntervalClient {
16441722
.catch(async err => {
16451723
this.#logger.error('Error in page:', err)
16461724
errors.push(pageError(err))
1725+
1726+
this.#onError?.({
1727+
error: err,
1728+
route: ctx.page.slug,
1729+
routeDefinition: this.#routes.get(ctx.page.slug),
1730+
params: ctx.params,
1731+
environment: ctx.environment,
1732+
user: ctx.user,
1733+
organization: ctx.organization,
1734+
})
1735+
16471736
const pageLayout: LayoutSchemaInput = {
16481737
kind: 'BASIC',
16491738
errors,

src/examples/basic/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,9 @@ const interval = new Interval({
488488
apiKey: 'alex_dev_kcLjzxNFxmGLf0aKtLVhuckt6sziQJtxFOdtM19tBrMUp5mj',
489489
logLevel: 'debug',
490490
endpoint: 'ws://localhost:3000/websocket',
491+
onError: props => {
492+
console.debug('onError', props)
493+
},
491494
routes: {
492495
sidebar_depth,
493496
echoContext,

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
IntervalPageStore,
2727
PageCtx,
2828
IntervalActionDefinition,
29+
IntervalErrorHandler,
2930
} from './types'
3031
import IntervalError from './classes/IntervalError'
3132
import IntervalClient, {
@@ -69,6 +70,8 @@ export interface InternalConfig {
6970

7071
closeUnresponsiveConnectionTimeoutMs?: number
7172
reinitializeBatchTimeoutMs?: number
73+
onError?: IntervalErrorHandler
74+
7275
/* @internal */ getClientHandlers?: () =>
7376
| DuplexRPCHandlers<ClientSchema>
7477
| undefined

src/types.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,37 @@ export type Prettify<T> = {
5151
[K in keyof T]: T[K]
5252
} & {}
5353

54+
export type CtxUser = {
55+
/**
56+
* The email of the user running the action or page.
57+
*/
58+
email: string
59+
/**
60+
* The first name of the user running the action or page, if present.
61+
*/
62+
firstName: string | null
63+
/**
64+
* The last name of the user running the action or page, if present.
65+
*/
66+
lastName: string | null
67+
}
68+
69+
export type CtxOrganization = {
70+
/**
71+
* The name of the organization.
72+
*/
73+
name: string
74+
/**
75+
* The unique slug of the organization.
76+
*/
77+
slug: string
78+
}
79+
5480
export type ActionCtx = {
5581
/**
5682
* Basic information about the user running the action or page.
5783
*/
58-
user: {
59-
/**
60-
* The email of the user running the action or page.
61-
*/
62-
email: string
63-
/**
64-
* The first name of the user running the action or page, if present.
65-
*/
66-
firstName: string | null
67-
/**
68-
* The last name of the user running the action or page, if present.
69-
*/
70-
lastName: string | null
71-
}
84+
user: CtxUser
7285
/**
7386
* A key/value object containing the query string URL parameters of the running action or page.
7487
*/
@@ -130,16 +143,7 @@ export type ActionCtx = {
130143
/**
131144
* Basic information about the organization.
132145
*/
133-
organization: {
134-
/**
135-
* The name of the organization.
136-
*/
137-
name: string
138-
/**
139-
* The unique slug of the organization.
140-
*/
141-
slug: string
142-
}
146+
organization: CtxOrganization
143147
/**
144148
* Information about the currently running action.
145149
*/
@@ -542,3 +546,15 @@ export type PageError = {
542546
cause?: string
543547
layoutKey?: keyof BasicLayoutConfig
544548
}
549+
550+
export type IntervalErrorProps = {
551+
error: Error | unknown
552+
route: string
553+
routeDefinition: Action | Page | undefined
554+
params: SerializableRecord
555+
environment: ActionEnvironment
556+
user: CtxUser
557+
organization: CtxOrganization
558+
}
559+
560+
export type IntervalErrorHandler = (props: IntervalErrorProps) => void

0 commit comments

Comments
 (0)