Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions src/components/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TreeStore } from '@stoplight/tree-list';
import { Intent, Spinner } from '@stoplight/ui-kit';
import cn from 'classnames';
import { runInAction } from 'mobx';
import { action, runInAction } from 'mobx';
import * as React from 'react';
import SchemaWorker, { WebWorker } from 'web-worker:../workers/schema.ts';

Expand All @@ -10,7 +10,7 @@ import { GoToRefHandler, RowRenderer, SchemaTreeListNode } from '../types';
import { isCombiner } from '../utils/isCombiner';
import { isSchemaViewerEmpty } from '../utils/isSchemaViewerEmpty';
import { renderSchema } from '../utils/renderSchema';
import { ComputeSchemaMessage, RenderedSchemaMessage } from '../workers/messages';
import { ComputeSchemaMessageData, isRenderedSchemaMessage } from '../workers/messages';
import { SchemaTree } from './SchemaTree';

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

export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaViewer> {
export interface IJsonSchemaViewerComponent extends Omit<IJsonSchemaViewer, 'FallbackComponent'> {
onError(err: Error): void;
}

export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaViewerComponent> {
protected treeStore: TreeStore;
protected instanceId: string;
protected schemaWorker?: WebWorker;
Expand All @@ -41,7 +45,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
computing: false,
};

constructor(props: IJsonSchemaViewer) {
constructor(props: IJsonSchemaViewerComponent) {
super(props);

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

protected handleWorkerMessage = (message: MessageEvent) => {
if (!message.data || !('instanceId' in message.data) || !('nodes' in message.data)) return;
const data = message.data as RenderedSchemaMessage;
protected handleWorkerMessage = action<(message: MessageEvent) => void>(message => {
if (!isRenderedSchemaMessage(message)) return;
const { data } = message;

if (data.instanceId === this.instanceId) {
runInAction(() => {
this.setState({ computing: false });
this.setState({ computing: false });
if (data.error === null) {
this.treeStore.nodes = data.nodes;
});
} else {
this.props.onError(new Error(data.error));
}
}
};
});

protected prerenderSchema() {
const schema = this.schema;
Expand Down Expand Up @@ -136,7 +142,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
return;
}

const message: ComputeSchemaMessage = {
const message: ComputeSchemaMessageData = {
instanceId: this.instanceId,
schema: this.schema,
mergeAllOf: this.props.mergeAllOf !== false,
Expand Down Expand Up @@ -231,12 +237,16 @@ export class JsonSchemaViewer extends React.PureComponent<IJsonSchemaViewer, { e
return { error };
}

private onError: IJsonSchemaViewerComponent['onError'] = error => {
this.setState({ error });
};

public render() {
const { FallbackComponent: Fallback = JsonSchemaFallbackComponent, ...props } = this.props;
if (this.state.error) {
return <Fallback error={this.state.error} />;
}

return <JsonSchemaViewerComponent {...props} />;
return <JsonSchemaViewerComponent {...props} onError={this.onError} />;
}
}
40 changes: 32 additions & 8 deletions src/components/__tests__/JsonSchemaViewer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ describe('JSON Schema Viewer component', () => {

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

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

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

shallow(<JsonSchemaViewerComponent schema={schemaAllOf} maxRows={10} />);
shallow(<JsonSchemaViewerComponent schema={schemaAllOf} maxRows={10} onError={jest.fn()} />);

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

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

expect(SchemaWorker.prototype.postMessage).not.toHaveBeenCalledWith();
});

test('should pre-render maxRows nodes and perform full processing in a worker if provided schema has more nodes than maxRows', () => {
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} />);
const wrapper = shallow(
<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} onError={jest.fn()} />,
);
expect(SchemaWorker.prototype.postMessage).toHaveBeenCalledWith({
instanceId: expect.any(String),
mergeAllOf: true,
Expand Down Expand Up @@ -152,6 +156,7 @@ describe('JSON Schema Viewer component', () => {
SchemaWorker.prototype.addEventListener.mock.calls[0][1]({
data: {
instanceId: SchemaWorker.prototype.postMessage.mock.calls[0][0].instanceId,
error: null,
nodes,
},
});
Expand All @@ -161,14 +166,33 @@ describe('JSON Schema Viewer component', () => {

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

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

test('should handle exceptions that may occur during full rendering', () => {
const onError = jest.fn();
shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={1} onError={onError} />);

SchemaWorker.prototype.addEventListener.mock.calls[0][1]({
data: {
instanceId: SchemaWorker.prototype.postMessage.mock.calls[0][0].instanceId,
error: 'error occurred',
nodes: null,
},
});

expect(onError).toHaveBeenCalledWith(new Error('error occurred'));
});

test('should not apply result of full processing in a worker if instance ids do not match', () => {
const wrapper = shallow(<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={0} />);
const wrapper = shallow(
<JsonSchemaViewerComponent schema={schema as JSONSchema4} maxRows={0} onError={jest.fn()} />,
);
expect(SchemaWorker.prototype.postMessage).toHaveBeenCalledWith({
instanceId: expect.any(String),
mergeAllOf: true,
Expand Down
23 changes: 21 additions & 2 deletions src/workers/messages.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { JSONSchema4 } from 'json-schema';
import { SchemaTreeListNode } from '../types';

export type ComputeSchemaMessage = {
interface WorkerMessageEvent<T> extends Omit<MessageEvent, 'data'> {
data: T;
}

export type ComputeSchemaMessageData = {
instanceId: string;
schema: JSONSchema4;
mergeAllOf: boolean;
};

export type RenderedSchemaMessage = {
export type RenderedSchemaMessageData = RenderedSchemaSuccessMessageData | RenderedSchemaErrorMessageData;

export type RenderedSchemaSuccessMessageData = {
instanceId: string;
error: null;
nodes: SchemaTreeListNode[];
};

export type RenderedSchemaErrorMessageData = {
instanceId: string;
error: string;
nodes: null;
};

export const isRenderedSchemaMessage = (message: MessageEvent): message is WorkerMessageEvent<RenderedSchemaMessageData> => message.data && 'instanceId' in message.data && 'nodes' in message.data;

export const isComputeSchemaMessage = (message: MessageEvent): message is WorkerMessageEvent<ComputeSchemaMessageData> => message.data && 'instanceId' in message.data && 'schema' in message.data;


27 changes: 18 additions & 9 deletions src/workers/schema.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { mergeAllOf } from '../utils/mergeAllOf';
import { renderSchema } from '../utils/renderSchema';
import { ComputeSchemaMessage } from './messages';
import { isComputeSchemaMessage, RenderedSchemaErrorMessageData, RenderedSchemaSuccessMessageData } from './messages';

declare const self: DedicatedWorkerGlobalScope;

self.addEventListener('message', e => {
const msg = e.data;
if (typeof msg !== 'object' || msg === null || !('schema' in msg) || !('instanceId' in msg)) return;
if (!isComputeSchemaMessage(e)) return;
const {
data: { instanceId, schema, mergeAllOf: shouldMergeAllOf },
} = e;

const schema = (msg as ComputeSchemaMessage).schema;

self.postMessage({
instanceId: (msg as ComputeSchemaMessage).instanceId,
nodes: Array.from(renderSchema((msg as ComputeSchemaMessage).mergeAllOf ? mergeAllOf(schema) : schema)),
});
try {
self.postMessage({
instanceId,
error: null,
nodes: Array.from(renderSchema(shouldMergeAllOf ? mergeAllOf(schema) : schema)),
} as RenderedSchemaSuccessMessageData);
} catch (ex) {
self.postMessage({
instanceId,
error: ex.message,
nodes: null,
} as RenderedSchemaErrorMessageData);
Copy link
Contributor

@billiegoose billiegoose Sep 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤢 Think it is time to pull out MagicPortal back into its own repo so we can have some of that goodness here?

One of the nice things about MagicPortal is that it automatically handles errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wmhilton yeah, that would be great

}
});