Skip to content

Commit 80612b3

Browse files
authored
fix(rendering): uncaught exception (#51)
* fix(rendering): uncaught exception * feat: expose onError
1 parent 6918c69 commit 80612b3

File tree

4 files changed

+94
-32
lines changed

4 files changed

+94
-32
lines changed

src/components/JsonSchemaViewer.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TreeStore } from '@stoplight/tree-list';
22
import { Intent, Spinner } from '@stoplight/ui-kit';
33
import cn from 'classnames';
4-
import { runInAction } from 'mobx';
4+
import { action, runInAction } from 'mobx';
55
import * as React from 'react';
66
import SchemaWorker, { WebWorker } from 'web-worker:../workers/schema.ts';
77

@@ -10,7 +10,7 @@ import { GoToRefHandler, RowRenderer, SchemaTreeListNode } from '../types';
1010
import { isCombiner } from '../utils/isCombiner';
1111
import { isSchemaViewerEmpty } from '../utils/isSchemaViewerEmpty';
1212
import { renderSchema } from '../utils/renderSchema';
13-
import { ComputeSchemaMessage, RenderedSchemaMessage } from '../workers/messages';
13+
import { ComputeSchemaMessageData, isRenderedSchemaMessage } from '../workers/messages';
1414
import { SchemaTree } from './SchemaTree';
1515

1616
export type FallbackComponent = React.ComponentType<{ error: Error | null }>;
@@ -32,7 +32,11 @@ export interface IJsonSchemaViewer {
3232
rowRenderer?: RowRenderer;
3333
}
3434

35-
export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaViewer> {
35+
export interface IJsonSchemaViewerComponent extends Omit<IJsonSchemaViewer, 'FallbackComponent'> {
36+
onError(err: Error): void;
37+
}
38+
39+
export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaViewerComponent> {
3640
protected treeStore: TreeStore;
3741
protected instanceId: string;
3842
protected schemaWorker?: WebWorker;
@@ -41,7 +45,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
4145
computing: false,
4246
};
4347

44-
constructor(props: IJsonSchemaViewer) {
48+
constructor(props: IJsonSchemaViewerComponent) {
4549
super(props);
4650

4751
this.treeStore = new TreeStore({
@@ -68,17 +72,19 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
6872
return this.props.dereferencedSchema || this.props.schema;
6973
}
7074

71-
protected handleWorkerMessage = (message: MessageEvent) => {
72-
if (!message.data || !('instanceId' in message.data) || !('nodes' in message.data)) return;
73-
const data = message.data as RenderedSchemaMessage;
75+
protected handleWorkerMessage = action<(message: MessageEvent) => void>(message => {
76+
if (!isRenderedSchemaMessage(message)) return;
77+
const { data } = message;
7478

7579
if (data.instanceId === this.instanceId) {
76-
runInAction(() => {
77-
this.setState({ computing: false });
80+
this.setState({ computing: false });
81+
if (data.error === null) {
7882
this.treeStore.nodes = data.nodes;
79-
});
83+
} else {
84+
this.props.onError(new Error(data.error));
85+
}
8086
}
81-
};
87+
});
8288

8389
protected prerenderSchema() {
8490
const schema = this.schema;
@@ -136,7 +142,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
136142
return;
137143
}
138144

139-
const message: ComputeSchemaMessage = {
145+
const message: ComputeSchemaMessageData = {
140146
instanceId: this.instanceId,
141147
schema: this.schema,
142148
mergeAllOf: this.props.mergeAllOf !== false,
@@ -231,12 +237,16 @@ export class JsonSchemaViewer extends React.PureComponent<IJsonSchemaViewer, { e
231237
return { error };
232238
}
233239

240+
private onError: IJsonSchemaViewerComponent['onError'] = error => {
241+
this.setState({ error });
242+
};
243+
234244
public render() {
235245
const { FallbackComponent: Fallback = JsonSchemaFallbackComponent, ...props } = this.props;
236246
if (this.state.error) {
237247
return <Fallback error={this.state.error} />;
238248
}
239249

240-
return <JsonSchemaViewerComponent {...props} />;
250+
return <JsonSchemaViewerComponent {...props} onError={this.onError} />;
241251
}
242252
}

src/components/__tests__/JsonSchemaViewer.spec.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,21 @@ describe('JSON Schema Viewer component', () => {
4747

4848
test('should render empty message if schema is empty', () => {
4949
(isSchemaViewerEmpty as jest.Mock).mockReturnValue(true);
50-
const wrapper = shallow(<JsonSchemaViewerComponent schema={{}} />);
50+
const wrapper = shallow(<JsonSchemaViewerComponent schema={{}} onError={jest.fn()} />);
5151
expect(isSchemaViewerEmpty).toHaveBeenCalledWith({});
5252
expect(wrapper.find(SchemaTree)).not.toExist();
5353
});
5454

5555
test('should render SchemaView if schema is provided', () => {
56-
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} />);
56+
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} onError={jest.fn()} />);
5757
expect(isSchemaViewerEmpty).toHaveBeenCalledWith(schema);
5858
expect(wrapper.find(SchemaTree)).toExist();
5959
});
6060

6161
test('should not perform full processing in a worker if provided schema has fewer nodes than maxRows', () => {
62-
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={10} />);
62+
const wrapper = shallow(
63+
<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={10} onError={jest.fn()} />,
64+
);
6365
expect(SchemaWorker.prototype.postMessage).not.toHaveBeenCalled();
6466
expect(wrapper.instance()).toHaveProperty('treeStore.nodes.length', 4);
6567
});
@@ -79,7 +81,7 @@ describe('JSON Schema Viewer component', () => {
7981
],
8082
};
8183

82-
shallow(<JsonSchemaViewerComponent schema={schemaAllOf} maxRows={10} />);
84+
shallow(<JsonSchemaViewerComponent schema={schemaAllOf} maxRows={10} onError={jest.fn()} />);
8385

8486
expect(SchemaWorker.prototype.postMessage).toHaveBeenCalledWith({
8587
instanceId: expect.any(String),
@@ -103,13 +105,15 @@ describe('JSON Schema Viewer component', () => {
103105
],
104106
};
105107

106-
shallow(<JsonSchemaViewerComponent schema={schemaAllOf} maxRows={10} mergeAllOf={false} />);
108+
shallow(<JsonSchemaViewerComponent schema={schemaAllOf} maxRows={10} mergeAllOf={false} onError={jest.fn()} />);
107109

108110
expect(SchemaWorker.prototype.postMessage).not.toHaveBeenCalledWith();
109111
});
110112

111113
test('should pre-render maxRows nodes and perform full processing in a worker if provided schema has more nodes than maxRows', () => {
112-
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} />);
114+
const wrapper = shallow(
115+
<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} onError={jest.fn()} />,
116+
);
113117
expect(SchemaWorker.prototype.postMessage).toHaveBeenCalledWith({
114118
instanceId: expect.any(String),
115119
mergeAllOf: true,
@@ -152,6 +156,7 @@ describe('JSON Schema Viewer component', () => {
152156
SchemaWorker.prototype.addEventListener.mock.calls[0][1]({
153157
data: {
154158
instanceId: SchemaWorker.prototype.postMessage.mock.calls[0][0].instanceId,
159+
error: null,
155160
nodes,
156161
},
157162
});
@@ -161,14 +166,33 @@ describe('JSON Schema Viewer component', () => {
161166

162167
test('should render all nodes on main thread when worker cannot be spawned regardless of maxRows or schema', () => {
163168
SchemaWorker.prototype.isShim = true;
164-
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} />);
169+
const wrapper = shallow(
170+
<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} onError={jest.fn()} />,
171+
);
165172

166173
expect(SchemaWorker.prototype.postMessage).not.toHaveBeenCalled();
167174
expect(wrapper.instance()).toHaveProperty('treeStore.nodes.length', 4);
168175
});
169176

177+
test('should handle exceptions that may occur during full rendering', () => {
178+
const onError = jest.fn();
179+
shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} onError={onError} />);
180+
181+
SchemaWorker.prototype.addEventListener.mock.calls[0][1]({
182+
data: {
183+
instanceId: SchemaWorker.prototype.postMessage.mock.calls[0][0].instanceId,
184+
error: 'error occurred',
185+
nodes: null,
186+
},
187+
});
188+
189+
expect(onError).toHaveBeenCalledWith(new Error('error occurred'));
190+
});
191+
170192
test('should not apply result of full processing in a worker if instance ids do not match', () => {
171-
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={0} />);
193+
const wrapper = shallow(
194+
<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={0} onError={jest.fn()} />,
195+
);
172196
expect(SchemaWorker.prototype.postMessage).toHaveBeenCalledWith({
173197
instanceId: expect.any(String),
174198
mergeAllOf: true,

src/workers/messages.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
import { JSONSchema4 } from 'json-schema';
22
import { SchemaTreeListNode } from '../types';
33

4-
export type ComputeSchemaMessage = {
4+
interface WorkerMessageEvent<T> extends Omit<MessageEvent, 'data'> {
5+
data: T;
6+
}
7+
8+
export type ComputeSchemaMessageData = {
59
instanceId: string;
610
schema: JSONSchema4;
711
mergeAllOf: boolean;
812
};
913

10-
export type RenderedSchemaMessage = {
14+
export type RenderedSchemaMessageData = RenderedSchemaSuccessMessageData | RenderedSchemaErrorMessageData;
15+
16+
export type RenderedSchemaSuccessMessageData = {
1117
instanceId: string;
18+
error: null;
1219
nodes: SchemaTreeListNode[];
1320
};
21+
22+
export type RenderedSchemaErrorMessageData = {
23+
instanceId: string;
24+
error: string;
25+
nodes: null;
26+
};
27+
28+
export const isRenderedSchemaMessage = (message: MessageEvent): message is WorkerMessageEvent<RenderedSchemaMessageData> => message.data && 'instanceId' in message.data && 'nodes' in message.data;
29+
30+
export const isComputeSchemaMessage = (message: MessageEvent): message is WorkerMessageEvent<ComputeSchemaMessageData> => message.data && 'instanceId' in message.data && 'schema' in message.data;
31+
32+

src/workers/schema.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import { mergeAllOf } from '../utils/mergeAllOf';
22
import { renderSchema } from '../utils/renderSchema';
3-
import { ComputeSchemaMessage } from './messages';
3+
import { isComputeSchemaMessage, RenderedSchemaErrorMessageData, RenderedSchemaSuccessMessageData } from './messages';
44

55
declare const self: DedicatedWorkerGlobalScope;
66

77
self.addEventListener('message', e => {
8-
const msg = e.data;
9-
if (typeof msg !== 'object' || msg === null || !('schema' in msg) || !('instanceId' in msg)) return;
8+
if (!isComputeSchemaMessage(e)) return;
9+
const {
10+
data: { instanceId, schema, mergeAllOf: shouldMergeAllOf },
11+
} = e;
1012

11-
const schema = (msg as ComputeSchemaMessage).schema;
12-
13-
self.postMessage({
14-
instanceId: (msg as ComputeSchemaMessage).instanceId,
15-
nodes: Array.from(renderSchema((msg as ComputeSchemaMessage).mergeAllOf ? mergeAllOf(schema) : schema)),
16-
});
13+
try {
14+
self.postMessage({
15+
instanceId,
16+
error: null,
17+
nodes: Array.from(renderSchema(shouldMergeAllOf ? mergeAllOf(schema) : schema)),
18+
} as RenderedSchemaSuccessMessageData);
19+
} catch (ex) {
20+
self.postMessage({
21+
instanceId,
22+
error: ex.message,
23+
nodes: null,
24+
} as RenderedSchemaErrorMessageData);
25+
}
1726
});

0 commit comments

Comments
 (0)