From ec19e84e67555e5c47cbfd406eac8435283fecee Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 19 Apr 2023 15:21:33 -0500 Subject: [PATCH 1/2] Add global error callbacks to Interval class constructor Closes T-901 --- src/classes/IntervalClient.ts | 113 ++++++++++++++++++++++++++-------- src/examples/basic/index.ts | 3 + src/index.ts | 3 + src/types.ts | 8 +++ 4 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/classes/IntervalClient.ts b/src/classes/IntervalClient.ts index bb0fc34..6dc00c6 100644 --- a/src/classes/IntervalClient.ts +++ b/src/classes/IntervalClient.ts @@ -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, @@ -121,6 +123,7 @@ export default class IntervalClient { #resolveShutdown: (() => void) | undefined #config: InternalConfig + #routes: Map = new Map() #actionDefinitions: ActionDefinition[] = [] #pageDefinitions: PageDefinition[] = [] #actionHandlers: Map = new Map() @@ -135,6 +138,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 @@ -180,43 +185,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() + const pageDefinitions: PageDefinition[] = [] const actionDefinitions: (ActionDefinition & { handler: undefined })[] = [] const actionHandlers = new Map() const pageHandlers = new Map() - 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) } } } @@ -242,28 +259,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 @@ -1137,6 +1159,12 @@ export default class IntervalClient { } } + this.#onError?.({ + error: err, + route: action.slug, + routeDefinition: this.#routes.get(action.slug), + }) + const result: ActionResultSchema = { schemaVersion: TRANSACTION_RESULT_SCHEMA_VERSION, status: 'FAILURE', @@ -1532,6 +1560,11 @@ export default class IntervalClient { page.title = page.title() } catch (err) { this.#logger.error(err) + this.#onError?.({ + error: err, + route: inputs.page.slug, + routeDefinition: this.#routes.get(inputs.page.slug), + }) errors.push(pageError(err, 'title')) } } @@ -1546,6 +1579,11 @@ export default class IntervalClient { }) .catch(err => { this.#logger.error(err) + this.#onError?.({ + error: err, + route: inputs.page.slug, + routeDefinition: this.#routes.get(inputs.page.slug), + }) errors.push(pageError(err, 'title')) scheduleSendPage() }) @@ -1557,6 +1595,11 @@ export default class IntervalClient { page.description = page.description() } catch (err) { this.#logger.error(err) + this.#onError?.({ + error: err, + route: inputs.page.slug, + routeDefinition: this.#routes.get(inputs.page.slug), + }) errors.push(pageError(err, 'description')) } } @@ -1571,6 +1614,11 @@ export default class IntervalClient { }) .catch(err => { this.#logger.error(err) + this.#onError?.({ + error: err, + route: inputs.page.slug, + routeDefinition: this.#routes.get(inputs.page.slug), + }) errors.push(pageError(err, 'description')) scheduleSendPage() }) @@ -1615,6 +1663,12 @@ 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: inputs.page.slug, + routeDefinition: this.#routes.get(inputs.page.slug), + }) + if (err instanceof IOError && err.cause) { errors.push(pageError(err.cause, 'children')) } else { @@ -1631,6 +1685,13 @@ export default class IntervalClient { .catch(async err => { this.#logger.error('Error in page:', err) errors.push(pageError(err)) + + this.#onError?.({ + error: err, + route: inputs.page.slug, + routeDefinition: this.#routes.get(inputs.page.slug), + }) + const pageLayout: LayoutSchemaInput = { kind: 'BASIC', errors, diff --git a/src/examples/basic/index.ts b/src/examples/basic/index.ts index 2d7b94a..f566eed 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -473,6 +473,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, diff --git a/src/index.ts b/src/index.ts index ca2c65f..8faff3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import type { IntervalPageStore, PageCtx, IntervalActionDefinition, + IntervalErrorHandler, } from './types' import IntervalError from './classes/IntervalError' import IntervalClient, { @@ -68,6 +69,8 @@ export interface InternalConfig { closeUnresponsiveConnectionTimeoutMs?: number reinitializeBatchTimeoutMs?: number + onError?: IntervalErrorHandler + /* @internal */ getClientHandlers?: () => | DuplexRPCHandlers | undefined diff --git a/src/types.ts b/src/types.ts index 962b932..67ace9e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -538,3 +538,11 @@ export type PageError = { cause?: string layoutKey?: keyof BasicLayoutConfig } + +export type IntervalErrorProps = { + error: Error | unknown + route: string + routeDefinition: Action | Page | undefined +} + +export type IntervalErrorHandler = (props: IntervalErrorProps) => void From 83ae24f92a00dbfd7af59dfec110e437d6bf5e38 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Thu, 20 Apr 2023 12:51:30 -0500 Subject: [PATCH 2/2] Add ctx informational values to onError callback props Added `params`, `environment`, `user`, `organization`. --- src/classes/IntervalClient.ts | 52 ++++++++++++++++++++++++-------- src/types.ts | 56 ++++++++++++++++++++--------------- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/classes/IntervalClient.ts b/src/classes/IntervalClient.ts index 6dc00c6..e55860c 100644 --- a/src/classes/IntervalClient.ts +++ b/src/classes/IntervalClient.ts @@ -1163,6 +1163,10 @@ export default class IntervalClient { 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 = { @@ -1562,8 +1566,12 @@ export default class IntervalClient { this.#logger.error(err) this.#onError?.({ error: err, - route: inputs.page.slug, - routeDefinition: this.#routes.get(inputs.page.slug), + 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')) } @@ -1581,8 +1589,12 @@ export default class IntervalClient { this.#logger.error(err) this.#onError?.({ error: err, - route: inputs.page.slug, - routeDefinition: this.#routes.get(inputs.page.slug), + 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() @@ -1597,8 +1609,12 @@ export default class IntervalClient { this.#logger.error(err) this.#onError?.({ error: err, - route: inputs.page.slug, - routeDefinition: this.#routes.get(inputs.page.slug), + 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')) } @@ -1616,8 +1632,12 @@ export default class IntervalClient { this.#logger.error(err) this.#onError?.({ error: err, - route: inputs.page.slug, - routeDefinition: this.#routes.get(inputs.page.slug), + 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() @@ -1665,8 +1685,12 @@ export default class IntervalClient { this.#logger.error(err) this.#onError?.({ error: err, - route: inputs.page.slug, - routeDefinition: this.#routes.get(inputs.page.slug), + 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) { @@ -1688,8 +1712,12 @@ export default class IntervalClient { this.#onError?.({ error: err, - route: inputs.page.slug, - routeDefinition: this.#routes.get(inputs.page.slug), + 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 = { diff --git a/src/types.ts b/src/types.ts index 67ace9e..20a720c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,24 +51,37 @@ export type Prettify = { [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. */ @@ -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. */ @@ -543,6 +547,10 @@ 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