Browse Source

Improved error handling

update-deps
Alex Mikhalev 7 years ago
parent
commit
1a9c1f5cbc
  1. 3
      app/components/DeviceView.tsx
  2. 22
      app/components/RunSectionForm.tsx
  3. 17
      app/sprinklers/websocket.ts
  4. 2
      app/state/StateBase.ts
  5. 0
      app/state/UiStore.ts
  6. 4
      app/state/index.ts
  7. 44
      app/state/inject.tsx
  8. 50
      app/state/reactContext.tsx
  9. 9
      common/sprinklers/ErrorCode.ts
  10. 7
      common/sprinklers/requests.ts
  11. 4
      server/websocket/index.ts
  12. 24
      yarn.lock

3
app/components/DeviceView.tsx

@ -52,6 +52,7 @@ class DeviceView extends React.Component<DeviceViewProps> { @@ -52,6 +52,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
render() {
const { id, connectionState, sections, programs, sectionRunner } = this.device;
const { uiStore } = this.props.state;
return (
<Item>
<Item.Image src={require("@app/images/raspberry_pi.png")}/>
@ -68,7 +69,7 @@ class DeviceView extends React.Component<DeviceViewProps> { @@ -68,7 +69,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
<SectionTable sections={sections}/>
</Grid.Column>
<Grid.Column width="8">
<RunSectionForm sections={sections}/>
<RunSectionForm sections={sections} uiStore={uiStore}/>
</Grid.Column>
</Grid>
<ProgramTable programs={programs} sections={sections}/>

22
app/components/RunSectionForm.tsx

@ -3,14 +3,17 @@ import { observer } from "mobx-react"; @@ -3,14 +3,17 @@ import { observer } from "mobx-react";
import * as React from "react";
import { DropdownItemProps, DropdownProps, Form, Header, Segment } from "semantic-ui-react";
import { UiStore } from "@app/state";
import { Duration } from "@common/Duration";
import log from "@common/logger";
import { Section } from "@common/sprinklers";
import { RunSectionResponse } from "@common/sprinklers/requests";
import DurationInput from "./DurationInput";
@observer
export default class RunSectionForm extends React.Component<{
sections: Section[],
uiStore: UiStore,
}, {
duration: Duration,
section: number | "",
@ -70,8 +73,23 @@ export default class RunSectionForm extends React.Component<{ @@ -70,8 +73,23 @@ export default class RunSectionForm extends React.Component<{
const section: Section = this.props.sections[this.state.section];
const { duration } = this.state;
section.run(duration.toSeconds())
.then((result) => log.debug({ result }, "requested section run"))
.catch((err) => log.error(err, "error running section"));
.then(this.onRunSuccess)
.catch(this.onRunError);
}
private onRunSuccess = (result: RunSectionResponse) => {
log.debug({ result }, "requested section run");
this.props.uiStore.addMessage({
color: "green", header: "Section running",
});
}
private onRunError = (err: RunSectionResponse) => {
log.error(err, "error running section");
this.props.uiStore.addMessage({
color: "red", header: "Error running section",
content: err.message,
});
}
private get isValid(): boolean {

17
app/sprinklers/websocket.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { update } from "serializr";
import logger from "@common/logger";
import { ErrorCode } from "@common/sprinklers/ErrorCode";
import * as s from "@common/sprinklers/index";
import * as requests from "@common/sprinklers/requests";
import * as schema from "@common/sprinklers/schema/index";
@ -10,6 +11,8 @@ import { action, autorun, observable } from "mobx"; @@ -10,6 +11,8 @@ import { action, autorun, observable } from "mobx";
const log = logger.child({ source: "websocket" });
const TIMEOUT_MS = 5000;
export class WSSprinklersDevice extends s.SprinklersDevice {
readonly api: WebSocketApiClient;
@ -84,14 +87,26 @@ export class WebSocketApiClient implements s.ISprinklersApi { @@ -84,14 +87,26 @@ export class WebSocketApiClient implements s.ISprinklersApi {
id, deviceName, data: requestData,
};
const promise = new Promise<requests.Response>((resolve, reject) => {
let timeoutHandle: number;
this.deviceResponseCallbacks[id] = (resData) => {
clearTimeout(timeoutHandle);
delete this.deviceResponseCallbacks[id];
if (resData.data.result === "success") {
resolve(resData.data);
} else {
reject(resData.data);
}
delete this.deviceResponseCallbacks[id];
};
timeoutHandle = setTimeout(() => {
delete this.deviceResponseCallbacks[id];
const res: requests.RunSectionResponse = {
type: "runSection",
result: "error",
code: ErrorCode.Timeout,
message: "the request timed out",
};
reject(res);
}, TIMEOUT_MS);
});
this.socket.send(JSON.stringify(data));
return promise;

2
app/state/StateBase.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { ISprinklersApi } from "@common/sprinklers";
import { UiStore } from "./ui";
import { UiStore } from "./UiStore";
export default abstract class StateBase {
abstract readonly sprinklersApi: ISprinklersApi;

0
app/state/ui.ts → app/state/UiStore.ts

4
app/state/index.ts

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
export { UiMessage, UiStore } from "./ui";
export * from "./inject";
export { UiMessage, UiStore } from "./UiStore";
export * from "./reactContext";
export { default as StateBase } from "./StateBase";

44
app/state/inject.tsx

@ -1,44 +0,0 @@ @@ -1,44 +0,0 @@
import * as PropTypes from "prop-types";
import * as React from "react";
import { StateBase } from "@app/state";
interface IProvidedStateContext {
providedState: StateBase;
}
const providedStateContextTypes: PropTypes.ValidationMap<any> = {
providedState: PropTypes.object,
};
export class ProvideState extends React.Component<{
state: StateBase,
}> implements React.ChildContextProvider<IProvidedStateContext> {
static childContextTypes = providedStateContextTypes;
getChildContext(): IProvidedStateContext {
return {
providedState: this.props.state,
};
}
render() {
return React.Children.only(this.props.children);
}
}
type Diff<T extends string | number | symbol, U extends string | number | symbol> =
({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
export function injectState<P extends { "state": StateBase }>(Component: React.ComponentType<P>) {
return class extends React.Component<Omit<P, "state">> {
static contextTypes = providedStateContextTypes;
context!: IProvidedStateContext;
render() {
const state = this.context.providedState;
return <Component {...this.props} state={state} />;
}
};
}

50
app/state/reactContext.tsx

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import * as React from "react";
import { StateBase } from "@app/state";
const StateContext = React.createContext<StateBase | null>(null);
export interface ProvideStateProps {
state: StateBase;
children: React.ReactNode;
}
export function ProvideState({state, children}: ProvideStateProps) {
return (
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
);
}
export interface ConsumeStateProps {
children: (state: StateBase) => React.ReactNode;
}
export function ConsumeState({children}: ConsumeStateProps) {
const consumeState = (state: StateBase | null) => {
if (state == null) {
throw new Error("Component with ConsumeState must be mounted inside ProvideState");
}
return children(state);
};
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
}
type Diff<T extends string | number | symbol, U extends string | number | symbol> =
({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
export function injectState<P extends { state: StateBase }>(Component: React.ComponentType<P>) {
return class extends React.Component<Omit<P, "state">> {
render() {
const consumeState = (state: StateBase | null) => {
if (state == null) {
throw new Error("Component with injectState must be mounted inside ProvideState");
}
return <Component {...this.props} state={state}/>;
};
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
}
};
}

9
common/sprinklers/ErrorCode.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
export enum ErrorCode {
BadRequest = 100,
NotSpecified = 101,
Parse = 102,
Range = 103,
InvalidData = 104,
Internal = 200,
Timeout = 300,
}

7
common/sprinklers/requests.ts

@ -37,9 +37,10 @@ export interface SuccessResponseData<Type extends string = string> extends WithT @@ -37,9 +37,10 @@ export interface SuccessResponseData<Type extends string = string> extends WithT
export interface ErrorResponseData<Type extends string = string> extends WithType<Type> {
result: "error";
error: string;
offset?: number;
code?: number;
message: string;
code: number;
name?: string;
cause?: any;
}
export type Response<Type extends string = string, Res = {}> =

4
server/websocket/index.ts

@ -33,11 +33,11 @@ export class WebSocketApi { @@ -33,11 +33,11 @@ export class WebSocketApi {
const stop = () => {
disposers.forEach((disposer) => disposer());
};
socket.on("message", this.handleSocketMessage);
socket.on("message", (data) => this.handleSocketMessage(socket, data));
socket.on("close", () => stop());
}
private handleSocketMessage = (socket: WebSocket, socketData: WebSocket.Data) => {
private handleSocketMessage(socket: WebSocket, socketData: WebSocket.Data) {
if (typeof socketData !== "string") {
return log.error({ type: typeof socketData }, "received invalid socket data type from client");
}

24
yarn.lock

@ -168,7 +168,7 @@ @@ -168,7 +168,7 @@
"@webassemblyjs/helper-code-frame@1.5.12":
version "1.5.12"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.12.tgz#3cdc1953093760d1c0f0caf745ccd62bdb6627c7"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-errorCode-frame/-/helper-errorCode-frame-1.5.12.tgz#3cdc1953093760d1c0f0caf745ccd62bdb6627c7"
dependencies:
"@webassemblyjs/wast-printer" "1.5.12"
@ -265,7 +265,7 @@ @@ -265,7 +265,7 @@
"@webassemblyjs/ast" "1.5.12"
"@webassemblyjs/floating-point-hex-parser" "1.5.12"
"@webassemblyjs/helper-api-error" "1.5.12"
"@webassemblyjs/helper-code-frame" "1.5.12"
"@webassemblyjs/helper-errorCode-frame" "1.5.12"
"@webassemblyjs/helper-fsm" "1.5.12"
long "^3.2.0"
mamacro "^0.0.3"
@ -564,7 +564,7 @@ aws4@^1.2.1: @@ -564,7 +564,7 @@ aws4@^1.2.1:
babel-code-frame@6.26.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
resolved "https://registry.yarnpkg.com/babel-errorCode-frame/-/babel-errorCode-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
dependencies:
chalk "^1.1.3"
esutils "^2.0.2"
@ -1076,7 +1076,7 @@ coa@~1.0.1: @@ -1076,7 +1076,7 @@ coa@~1.0.1:
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
resolved "https://registry.yarnpkg.com/errorCode-point-at/-/errorCode-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
collection-visit@^1.0.0:
version "1.0.0"
@ -1392,7 +1392,7 @@ css-loader@^0.28.11: @@ -1392,7 +1392,7 @@ css-loader@^0.28.11:
version "0.28.11"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
dependencies:
babel-code-frame "^6.26.0"
babel-errorCode-frame "^6.26.0"
css-selector-tokenizer "^0.7.0"
cssnano "^3.10.0"
icss-utils "^2.1.0"
@ -2960,13 +2960,13 @@ is-finite@^1.0.0: @@ -2960,13 +2960,13 @@ is-finite@^1.0.0:
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
resolved "https://registry.yarnpkg.com/is-fullwidth-errorCode-point/-/is-fullwidth-errorCode-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
dependencies:
number-is-nan "^1.0.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
resolved "https://registry.yarnpkg.com/is-fullwidth-errorCode-point/-/is-fullwidth-errorCode-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
is-glob@^3.1.0:
version "3.1.0"
@ -5083,7 +5083,7 @@ react-dev-utils@^5.0.1: @@ -5083,7 +5083,7 @@ react-dev-utils@^5.0.1:
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
dependencies:
address "1.0.3"
babel-code-frame "6.26.0"
babel-errorCode-frame "6.26.0"
chalk "1.1.3"
cross-spawn "5.1.0"
detect-port-alt "1.1.6"
@ -5973,15 +5973,15 @@ string-width@^1.0.1, string-width@^1.0.2: @@ -5973,15 +5973,15 @@ string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
dependencies:
code-point-at "^1.0.0"
is-fullwidth-code-point "^1.0.0"
errorCode-point-at "^1.0.0"
is-fullwidth-errorCode-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
is-fullwidth-code-point "^2.0.0"
is-fullwidth-errorCode-point "^2.0.0"
strip-ansi "^4.0.0"
string.prototype.padend@^3.0.0:
@ -6242,7 +6242,7 @@ tslint@^5.10.0: @@ -6242,7 +6242,7 @@ tslint@^5.10.0:
version "5.10.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3"
dependencies:
babel-code-frame "^6.22.0"
babel-errorCode-frame "^6.22.0"
builtin-modules "^1.1.1"
chalk "^2.3.0"
commander "^2.12.1"

Loading…
Cancel
Save