Good work on websocket device comms
This commit is contained in:
parent
889f779a59
commit
dab7a9e19e
5
.vscode/tasks.json
vendored
5
.vscode/tasks.json
vendored
@ -43,6 +43,11 @@
|
|||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "start:watch",
|
"script": "start:watch",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start:dev-server",
|
||||||
|
"problemMatcher": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -67,9 +67,8 @@ export default class RunSectionForm extends React.Component<{
|
|||||||
}
|
}
|
||||||
const section: Section = this.props.sections[this.state.section];
|
const section: Section = this.props.sections[this.state.section];
|
||||||
const { duration } = this.state;
|
const { duration } = this.state;
|
||||||
log.debug({ section, duration }, "running section");
|
|
||||||
section.run(duration)
|
section.run(duration)
|
||||||
.then((a) => log.debug("ran section", a))
|
.then((result) => log.debug({ result }, "requested section run"))
|
||||||
.catch((err) => log.error(err, "error running section"));
|
.catch((err) => log.error(err, "error running section"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@ import * as ReactDOM from "react-dom";
|
|||||||
import { AppContainer } from "react-hot-loader";
|
import { AppContainer } from "react-hot-loader";
|
||||||
|
|
||||||
import App from "@app/components/App";
|
import App from "@app/components/App";
|
||||||
import { ProvideState, MqttApiState, WebApiState } from "@app/state";
|
import { MqttApiState, ProvideState, WebApiState } from "@app/state";
|
||||||
import log, { setLogger } from "@common/logger";
|
import log from "@common/logger";
|
||||||
|
|
||||||
setLogger(log.child({ name: "sprinklers3/app" }));
|
Object.assign(log, { name: "sprinklers3/app", level: "debug" });
|
||||||
|
|
||||||
// const state = new MqttApiState();
|
// const state = new MqttApiState();
|
||||||
const state = new WebApiState();
|
const state = new WebApiState();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ISprinklersApi } from "@common/sprinklers";
|
import { ISprinklersApi } from "@common/sprinklers";
|
||||||
import { MqttApiClient } from "@common/sprinklers/mqtt";
|
import { MqttApiClient } from "@common/sprinklers/mqtt";
|
||||||
import { WebApiClient } from "./web";
|
import { WebApiClient } from "./websocket";
|
||||||
|
|
||||||
import { UiMessage, UiStore } from "./ui";
|
import { UiMessage, UiStore } from "./ui";
|
||||||
export { UiMessage, UiStore };
|
export { UiMessage, UiStore };
|
||||||
@ -19,14 +19,14 @@ export abstract class StateBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
export class MqttApiState extends StateBase {
|
export class MqttApiState extends StateBase {
|
||||||
sprinklersApi = new MqttApiClient(`ws://${location.hostname}:1884`);
|
sprinklersApi = new MqttApiClient(`ws://${location.hostname}:1884`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebApiState extends StateBase {
|
export class WebApiState extends StateBase {
|
||||||
sprinklersApi = new WebApiClient();
|
sprinklersApi = new WebApiClient(isDev ?
|
||||||
|
`ws://${location.hostname}:8080` :
|
||||||
|
`ws://${location.host}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const state = new State();
|
|
||||||
|
|
||||||
// export default state;
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import { update } from "serializr";
|
|
||||||
|
|
||||||
import * as s from "@common/sprinklers";
|
|
||||||
import * as schema from "@common/sprinklers/json";
|
|
||||||
|
|
||||||
export class WebSprinklersDevice extends s.SprinklersDevice {
|
|
||||||
get id() {
|
|
||||||
return "grinklers";
|
|
||||||
}
|
|
||||||
async runSection(section: number | s.Section, duration: s.Duration): Promise<{}> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
async runProgram(program: number | s.Program): Promise<{}> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
async cancelSectionRunById(id: number): Promise<{}> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
async pauseSectionRunner(): Promise<{}> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
async unpauseSectionRunner(): Promise<{}> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebApiClient implements s.ISprinklersApi {
|
|
||||||
start() {
|
|
||||||
// NOT IMPLEMENTED
|
|
||||||
}
|
|
||||||
|
|
||||||
getDevice(name: string): s.SprinklersDevice {
|
|
||||||
const device = new WebSprinklersDevice();
|
|
||||||
fetch("/api/grinklers")
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((json) => {
|
|
||||||
update(schema.sprinklersDeviceSchema, device, json);
|
|
||||||
})
|
|
||||||
.catch((e) => alert(e));
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeDevice(name: string) {
|
|
||||||
// NOT IMPLEMENTED
|
|
||||||
}
|
|
||||||
}
|
|
146
app/state/websocket.ts
Normal file
146
app/state/websocket.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { update } from "serializr";
|
||||||
|
|
||||||
|
import logger from "@common/logger";
|
||||||
|
import * as s from "@common/sprinklers";
|
||||||
|
import * as schema from "@common/sprinklers/json";
|
||||||
|
import * as ws from "@common/sprinklers/websocketData";
|
||||||
|
import { checkedIndexOf } from "@common/utils";
|
||||||
|
|
||||||
|
const log = logger.child({ source: "websocket" });
|
||||||
|
|
||||||
|
export class WebSprinklersDevice extends s.SprinklersDevice {
|
||||||
|
readonly api: WebApiClient;
|
||||||
|
|
||||||
|
constructor(api: WebApiClient) {
|
||||||
|
super();
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "grinklers";
|
||||||
|
}
|
||||||
|
|
||||||
|
runSection(section: number | s.Section, duration: s.Duration): Promise<{}> {
|
||||||
|
const secNum = checkedIndexOf(section, this.sections, "Section");
|
||||||
|
const dur = duration.toSeconds();
|
||||||
|
return this.makeCall("runSection", secNum, dur);
|
||||||
|
}
|
||||||
|
async runProgram(program: number | s.Program): Promise<{}> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
async cancelSectionRunById(id: number): Promise<{}> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
async pauseSectionRunner(): Promise<{}> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
async unpauseSectionRunner(): Promise<{}> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeCall(method: string, ...args: any[]) {
|
||||||
|
return this.api.makeDeviceCall(this.id, method, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebApiClient implements s.ISprinklersApi {
|
||||||
|
readonly webSocketUrl: string;
|
||||||
|
socket: WebSocket;
|
||||||
|
device: WebSprinklersDevice;
|
||||||
|
|
||||||
|
nextDeviceRequestId = Math.round(Math.random() * 1000000);
|
||||||
|
deviceResponseCallbacks: { [id: number]: (res: ws.IDeviceCallResponse) => void | undefined; } = {};
|
||||||
|
|
||||||
|
constructor(webSocketUrl: string) {
|
||||||
|
this.webSocketUrl = webSocketUrl;
|
||||||
|
this.device = new WebSprinklersDevice(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
log.debug({ url: this.webSocketUrl }, "connecting to websocket");
|
||||||
|
this.socket = new WebSocket(this.webSocketUrl);
|
||||||
|
this.socket.onopen = this.onOpen.bind(this);
|
||||||
|
this.socket.onclose = this.onClose.bind(this);
|
||||||
|
this.socket.onerror = this.onError.bind(this);
|
||||||
|
this.socket.onmessage = this.onMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDevice(name: string): s.SprinklersDevice {
|
||||||
|
if (name !== "grinklers") {
|
||||||
|
throw new Error("Devices which are not grinklers are not supported yet");
|
||||||
|
}
|
||||||
|
return this.device;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDevice(name: string) {
|
||||||
|
// NOT IMPLEMENTED
|
||||||
|
}
|
||||||
|
|
||||||
|
// args must all be JSON serializable
|
||||||
|
makeDeviceCall(deviceName: string, method: string, ...args: any[]): Promise<any> {
|
||||||
|
const id = this.nextDeviceRequestId++;
|
||||||
|
const data: ws.IDeviceCallRequest = {
|
||||||
|
type: "deviceCallRequest",
|
||||||
|
id, deviceName, method, args,
|
||||||
|
};
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
this.deviceResponseCallbacks[id] = (resData) => {
|
||||||
|
if (resData.result === "success") {
|
||||||
|
resolve(resData.data);
|
||||||
|
} else {
|
||||||
|
reject(resData.data);
|
||||||
|
}
|
||||||
|
delete this.deviceResponseCallbacks[id];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.socket.send(JSON.stringify(data));
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onOpen() {
|
||||||
|
log.info("established websocket connection");
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClose(event: CloseEvent) {
|
||||||
|
log.info({ reason: event.reason, wasClean: event.wasClean },
|
||||||
|
"disconnected from websocket");
|
||||||
|
}
|
||||||
|
|
||||||
|
private onError(event: Event) {
|
||||||
|
log.error(event, "websocket error");
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessage(event: MessageEvent) {
|
||||||
|
log.trace({ event }, "websocket message");
|
||||||
|
let data: ws.IServerMessage;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(event.data);
|
||||||
|
} catch (err) {
|
||||||
|
return log.error({ event, err }, "received invalid websocket message");
|
||||||
|
}
|
||||||
|
switch (data.type) {
|
||||||
|
case "deviceUpdate":
|
||||||
|
this.onDeviceUpdate(data);
|
||||||
|
break;
|
||||||
|
case "deviceCallResponse":
|
||||||
|
this.onDeviceCallResponse(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn({ data }, "unsupported event type received");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDeviceUpdate(data: ws.IDeviceUpdate) {
|
||||||
|
if (data.name !== "grinklers") {
|
||||||
|
return log.warn({ data }, "invalid deviceUpdate received");
|
||||||
|
}
|
||||||
|
update(schema.sprinklersDeviceSchema, this.device, data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDeviceCallResponse(data: ws.IDeviceCallResponse) {
|
||||||
|
const cb = this.deviceResponseCallbacks[data.id];
|
||||||
|
if (typeof cb === "function") {
|
||||||
|
cb(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -108,13 +108,9 @@ function formatLevel(value: any): ColoredString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let logger: pino.Logger = pino({
|
const logger: pino.Logger = pino({
|
||||||
browser: { write },
|
browser: { write },
|
||||||
level: "trace",
|
level: "trace",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setLogger(newLogger: pino.Logger) {
|
|
||||||
exports.default = logger = newLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default logger;
|
export default logger;
|
||||||
|
@ -22,7 +22,7 @@ export const dateSchema: PropSchema = {
|
|||||||
jsDate.toISOString() : null,
|
jsDate.toISOString() : null,
|
||||||
deserializer: (json: any, done) => {
|
deserializer: (json: any, done) => {
|
||||||
if (json === null) {
|
if (json === null) {
|
||||||
done(null, null);
|
return done(null, null);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
done(null, new Date(json));
|
done(null, new Date(json));
|
||||||
|
24
common/sprinklers/websocketData.ts
Normal file
24
common/sprinklers/websocketData.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export interface IDeviceUpdate {
|
||||||
|
type: "deviceUpdate";
|
||||||
|
name: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDeviceCallResponse {
|
||||||
|
type: "deviceCallResponse";
|
||||||
|
id: number;
|
||||||
|
result: "success" | "error";
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IServerMessage = IDeviceUpdate | IDeviceCallResponse;
|
||||||
|
|
||||||
|
export interface IDeviceCallRequest {
|
||||||
|
type: "deviceCallRequest";
|
||||||
|
id: number;
|
||||||
|
deviceName: string;
|
||||||
|
method: string;
|
||||||
|
args: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IClientMessage = IDeviceCallRequest;
|
@ -1,7 +1,12 @@
|
|||||||
export function checkedIndexOf<T>(o: T | number, arr: T[], type: string = "object"): number {
|
export function checkedIndexOf<T>(o: T | number, arr: T[], type: string = "object"): number {
|
||||||
const idx = (typeof o === "number")
|
let idx: number;
|
||||||
? o
|
if (typeof o === "number") {
|
||||||
: arr.indexOf(o);
|
idx = o;
|
||||||
|
} else if (typeof (o as any).id === "number") {
|
||||||
|
idx = (o as any).id;
|
||||||
|
} else {
|
||||||
|
idx = arr.indexOf(o);
|
||||||
|
}
|
||||||
if (idx < 0 || idx > arr.length) {
|
if (idx < 0 || idx > arr.length) {
|
||||||
throw new Error(`Invalid ${type} specified: ${o}`);
|
throw new Error(`Invalid ${type} specified: ${o}`);
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
"@types/react-dom": "^15.5.0",
|
"@types/react-dom": "^15.5.0",
|
||||||
"@types/react-fontawesome": "^1.5.0",
|
"@types/react-fontawesome": "^1.5.0",
|
||||||
"@types/react-hot-loader": "^3.0.4",
|
"@types/react-hot-loader": "^3.0.4",
|
||||||
|
"@types/ws": "^3.2.0",
|
||||||
"async": "^2.5.0",
|
"async": "^2.5.0",
|
||||||
"autoprefixer": "^7.1.4",
|
"autoprefixer": "^7.1.4",
|
||||||
"case-sensitive-paths-webpack-plugin": "^2.1.1",
|
"case-sensitive-paths-webpack-plugin": "^2.1.1",
|
||||||
@ -75,7 +76,8 @@
|
|||||||
"serializr": "^1.1.13",
|
"serializr": "^1.1.13",
|
||||||
"tslint-loader": "^3.5.3",
|
"tslint-loader": "^3.5.3",
|
||||||
"uglifyjs-webpack-plugin": "^0.4.6",
|
"uglifyjs-webpack-plugin": "^0.4.6",
|
||||||
"url-loader": "^0.5.9"
|
"url-loader": "^0.5.9",
|
||||||
|
"ws": "^3.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/webpack-env": "^1.13.0",
|
"@types/webpack-env": "^1.13.0",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import log, { setLogger } from "@common/logger";
|
import log from "@common/logger";
|
||||||
setLogger(log.child({
|
Object.assign(log, {
|
||||||
name: "sprinklers3/server",
|
name: "sprinklers3/server",
|
||||||
level: "debug",
|
level: "debug",
|
||||||
}));
|
});
|
||||||
|
@ -6,32 +6,96 @@ import "./configureLogger";
|
|||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import * as mqtt from "@common/sprinklers/mqtt";
|
import * as mqtt from "@common/sprinklers/mqtt";
|
||||||
import { Server } from "http";
|
import { Server } from "http";
|
||||||
|
import * as WebSocket from "ws";
|
||||||
import app from "./app";
|
import app from "./app";
|
||||||
|
|
||||||
const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883");
|
const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883");
|
||||||
|
|
||||||
mqttClient.start();
|
mqttClient.start();
|
||||||
|
|
||||||
|
import * as s from "@common/sprinklers";
|
||||||
import { sprinklersDeviceSchema } from "@common/sprinklers/json";
|
import { sprinklersDeviceSchema } from "@common/sprinklers/json";
|
||||||
|
import * as ws from "@common/sprinklers/websocketData";
|
||||||
import { autorunAsync } from "mobx";
|
import { autorunAsync } from "mobx";
|
||||||
import { serialize } from "serializr";
|
import { serialize } from "serializr";
|
||||||
const device = mqttClient.getDevice("grinklers");
|
const device = mqttClient.getDevice("grinklers");
|
||||||
|
|
||||||
autorunAsync(() => {
|
|
||||||
const j = serialize(sprinklersDeviceSchema, device);
|
|
||||||
log.info({ device: j });
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
app.get("/api/grinklers", (req, res) => {
|
app.get("/api/grinklers", (req, res) => {
|
||||||
const j = serialize(sprinklersDeviceSchema, device);
|
const j = serialize(sprinklersDeviceSchema, device);
|
||||||
res.send(j);
|
res.send(j);
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = new Server(app);
|
async function doDeviceCallRequest(data: ws.IDeviceCallRequest): Promise<any> {
|
||||||
|
const { deviceName, method, args } = data;
|
||||||
|
if (deviceName !== "grinklers") {
|
||||||
|
// error handling? or just get the right device
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (method) {
|
||||||
|
case "runSection":
|
||||||
|
return device.runSection(args[0], s.Duration.fromSeconds(args[1]));
|
||||||
|
default:
|
||||||
|
// new Error(`unsupported device call: ${data.method}`) // TODO: error handling?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deviceCallRequest(socket: WebSocket, data: ws.IDeviceCallRequest): Promise<void> {
|
||||||
|
let resData: ws.IDeviceCallResponse;
|
||||||
|
try {
|
||||||
|
const result = await doDeviceCallRequest(data);
|
||||||
|
resData = {
|
||||||
|
type: "deviceCallResponse",
|
||||||
|
id: data.id,
|
||||||
|
result: "success",
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
resData = {
|
||||||
|
type: "deviceCallResponse",
|
||||||
|
id: data.id,
|
||||||
|
result: "error",
|
||||||
|
data: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
socket.send(JSON.stringify(resData));
|
||||||
|
}
|
||||||
|
|
||||||
|
function webSocketHandler(socket: WebSocket) {
|
||||||
|
const stop = autorunAsync(() => {
|
||||||
|
const json = serialize(sprinklersDeviceSchema, device);
|
||||||
|
log.info({ device: json });
|
||||||
|
const data = { type: "deviceUpdate", name: "grinklers", data: json };
|
||||||
|
socket.send(JSON.stringify(data));
|
||||||
|
}, 100);
|
||||||
|
socket.on("message", (socketData: WebSocket.Data) => {
|
||||||
|
if (typeof socketData !== "string") {
|
||||||
|
return log.error({ type: typeof socketData }, "received invalid socket data type from client");
|
||||||
|
}
|
||||||
|
let data: ws.IClientMessage;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(socketData);
|
||||||
|
} catch (err) {
|
||||||
|
return log.error({ event, err }, "received invalid websocket message from client");
|
||||||
|
}
|
||||||
|
switch (data.type) {
|
||||||
|
case "deviceCallRequest":
|
||||||
|
deviceCallRequest(socket, data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return log.warn({ data }, "received invalid client message type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on("close", () => stop());
|
||||||
|
}
|
||||||
|
|
||||||
const port = +(process.env.PORT || 8080);
|
const port = +(process.env.PORT || 8080);
|
||||||
const host = process.env.HOST || "0.0.0.0";
|
const host = process.env.HOST || "0.0.0.0";
|
||||||
|
|
||||||
|
const server = new Server(app);
|
||||||
|
const webSocketServer = new WebSocket.Server({ server });
|
||||||
|
|
||||||
|
webSocketServer.on("connection", webSocketHandler);
|
||||||
|
|
||||||
server.listen(port, host, () => {
|
server.listen(port, host, () => {
|
||||||
log.info(`listening at ${host}:${port}`);
|
log.info(`listening at ${host}:${port}`);
|
||||||
});
|
});
|
||||||
|
@ -100,6 +100,12 @@
|
|||||||
version "1.13.1"
|
version "1.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.1.tgz#b45c222e24301bd006e3edfc762cc6b51bda236a"
|
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.1.tgz#b45c222e24301bd006e3edfc762cc6b51bda236a"
|
||||||
|
|
||||||
|
"@types/ws@^3.2.0":
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.2.0.tgz#988ff690e6ed10068a86aa0e9f842d0a03c09e21"
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
abbrev@1:
|
abbrev@1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
|
||||||
@ -5759,7 +5765,7 @@ write-file-atomic@^2.0.0:
|
|||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
signal-exit "^3.0.2"
|
signal-exit "^3.0.2"
|
||||||
|
|
||||||
ws@^3.0.0:
|
ws@^3.0.0, ws@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.2.0.tgz#d5d3d6b11aff71e73f808f40cc69d52bb6d4a185"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-3.2.0.tgz#d5d3d6b11aff71e73f808f40cc69d52bb6d4a185"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user