From e5cc6b86e7ce749df6b347f050290805cd5be74d Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Thu, 5 Oct 2017 14:27:43 -0600 Subject: [PATCH] Much better serialization/deserialization for sprinklers --- .vscode/launch.json | 1 + common/logger.ts | 2 +- common/sprinklers/Program.ts | 2 +- common/sprinklers/SprinklersDevice.ts | 4 + common/sprinklers/json/index.ts | 277 ++++++++++---------------- common/sprinklers/json/list.ts | 65 ++++++ common/sprinklers/mqtt/index.ts | 144 +++---------- common/sprinklers/schedule.ts | 37 +++- package.json | 3 + server/configureLogger.ts | 3 +- server/index.ts | 8 +- yarn.lock | 10 +- 12 files changed, 259 insertions(+), 297 deletions(-) create mode 100644 common/sprinklers/json/list.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c55cc47..0454347 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,7 @@ "env": { "NODE_ENV": "development" }, + "console": "integratedTerminal", "program": "${workspaceRoot}/dist/server/index.js" } ] diff --git a/common/logger.ts b/common/logger.ts index 5229ba8..3a1c863 100644 --- a/common/logger.ts +++ b/common/logger.ts @@ -135,7 +135,7 @@ let logger: pino.Logger = pino({ }); export function setLogger(newLogger: pino.Logger) { - logger = newLogger; + exports.default = logger = newLogger; } export default logger; diff --git a/common/sprinklers/Program.ts b/common/sprinklers/Program.ts index cc61ea9..236b923 100644 --- a/common/sprinklers/Program.ts +++ b/common/sprinklers/Program.ts @@ -9,7 +9,7 @@ export class ProgramItem { // duration of the run readonly duration: Duration; - constructor(section: number, duration: Duration) { + constructor(section: number = 0, duration: Duration = new Duration()) { this.section = section; this.duration = duration; } diff --git a/common/sprinklers/SprinklersDevice.ts b/common/sprinklers/SprinklersDevice.ts index 6d82a15..dbcf5fa 100644 --- a/common/sprinklers/SprinklersDevice.ts +++ b/common/sprinklers/SprinklersDevice.ts @@ -17,6 +17,10 @@ export abstract class SprinklersDevice { abstract pauseSectionRunner(): Promise<{}>; abstract unpauseSectionRunner(): Promise<{}>; + abstract get sectionConstructor(): typeof Section; + abstract get sectionRunnerConstructor(): typeof SectionRunner; + abstract get programConstructor(): typeof Program; + toString(): string { return `SprinklersDevice{id="${this.id}", connected=${this.connected}, ` + `sections=[${this.sections}], ` + diff --git a/common/sprinklers/json/index.ts b/common/sprinklers/json/index.ts index cdc05d5..62c8dae 100644 --- a/common/sprinklers/json/index.ts +++ b/common/sprinklers/json/index.ts @@ -1,175 +1,108 @@ +/* tslint:disable:ordered-imports */ import { assign, pick } from "lodash"; +import { + createSimpleSchema, createModelSchema, primitive, object, date, custom, + ModelSchema, PropSchema, +} from "serializr"; +import list from "./list"; import * as s from ".."; -export interface ISectionJSON { - name: string; - state: boolean; -} -const sectionProps = ["name", "state"]; - -export function sectionToJSON(sec: s.Section): ISectionJSON { - return pick(sec, sectionProps); -} - -export function sectionFromJSON(sec: s.Section, json: ISectionJSON) { - assign(sec, pick(json, sectionProps)); -} - -export interface ITimeOfDayJSON { - hour: number; - minute: number; - second: number; - millisecond: number; -} -const timeOfDayProps = ["hour", "minute", "second", "millisecond"]; - -export function timeOfDayToJSON(timeOfDay: s.TimeOfDay): ITimeOfDayJSON { - return pick(timeOfDay, timeOfDayProps); -} - -export function timeOfDayFromJSON(timeOfDay: s.TimeOfDay, json: ITimeOfDayJSON) { - assign(timeOfDay, pick(json, timeOfDayProps)); -} - -export interface IScheduleJSON { - times: ITimeOfDayJSON[]; - weekdays: number[]; - from?: string; - to?: string; -} -const scheduleProps = ["weekdays", "from", "to"]; - -export function scheduleToJSON(schedule: s.Schedule): IScheduleJSON { - return { - ...pick(schedule, scheduleProps), - times: schedule.times.map(timeOfDayToJSON), - }; -} - -export function scheduleFromJSON(schedule: s.Schedule, json: IScheduleJSON) { - assign(schedule, pick(json, scheduleProps)); - schedule.times.length = json.times.length; - schedule.times.forEach((timeOfDay, i) => - timeOfDayFromJSON(timeOfDay, json.times[i])); -} - -export interface IProgramItemJSON { - section: number; - duration: number; -} -const programItemProps = ["section"]; - -export function programItemToJSON(programItem: s.ProgramItem): IProgramItemJSON { - return { - ...pick(programItem, programItemProps), - duration: programItem.duration.toSeconds(), - }; -} - -export function programItemFromJSON(json: IProgramItemJSON): s.ProgramItem { - const duration = s.Duration.fromSeconds(json.duration); - return new s.ProgramItem(json.section, duration); -} - -export interface IProgramJSON { - name: string; - enabled: boolean; - sequence: IProgramItemJSON[]; - schedule: IScheduleJSON; - running: boolean; -} -const programProps = ["name", "enabled", "running"]; - -export function programToJSON(program: s.Program): IProgramJSON { - return { - ...pick(program, programProps), - sequence: program.sequence.map(programItemToJSON), - schedule: scheduleToJSON(program.schedule), - }; -} - -export function programFromJSON(program: s.Program, json: IProgramJSON) { - assign(program, pick(json, programProps)); - program.sequence = json.sequence.map((programItemJson) => - programItemFromJSON(programItemJson)); - scheduleFromJSON(program.schedule, json.schedule); -} - -export interface ISectionRunJSON { - id: number; - section: number; - duration: number; - startTime?: number; - pauseTime?: number; -} -const sectionRunProps = ["id", "section", "duration", "startTime", "pauseTime"]; - -export function sectionRunToJSON(sectionRun: s.SectionRun): ISectionRunJSON { - return { - ...pick(sectionRun, sectionRunProps), - duration: sectionRun.duration.toSeconds(), - }; -} - -export function sectionRunFromJSON(sectionRun: s.SectionRun, json: ISectionRunJSON) { - assign(sectionRun, pick(json, sectionRunProps)); - sectionRun.duration = s.Duration.fromSeconds(json.duration); -} - -interface ISectionRunnerJSON { - queue: ISectionRunJSON[]; - current: ISectionRunJSON | null; - paused: boolean; -} -const sectionRunnerProps = ["paused"]; - -export function sectionRunnerToJSON(sectionRunner: s.SectionRunner): ISectionRunnerJSON { - return { - ...pick(sectionRunner, sectionRunnerProps), - queue: sectionRunner.queue.map(sectionRunToJSON), - current: sectionRunner.current ? sectionRunToJSON(sectionRunner.current) : null, - }; -} - -export function sectionRunnerFromJSON(sectionRunner: s.SectionRunner, json: ISectionRunnerJSON) { - assign(sectionRunner, pick(json, sectionRunnerProps)); - sectionRunner.queue.length = json.queue.length; - sectionRunner.queue.forEach((sectionRun, i) => - sectionRunFromJSON(sectionRun, json.queue[i])); - if (json.current == null) { - sectionRunner.current = null; - } else { - if (sectionRunner.current == null) { - sectionRunner.current = new s.SectionRun(); - } - sectionRunFromJSON(sectionRunner.current, json.current); - } -} - -interface ISprinklersDeviceJSON { - connected: boolean; - sections: ISectionJSON[]; - sectionRunner: ISectionRunnerJSON; - programs: IProgramJSON[]; -} -const sprinklersDeviceProps = ["connected"]; - -export function sprinklersDeviceToJSON(sprinklersDevice: s.SprinklersDevice): ISprinklersDeviceJSON { - return { - ...pick(sprinklersDevice, sprinklersDeviceProps), - sections: sprinklersDevice.sections.map(sectionToJSON), - sectionRunner: sectionRunnerToJSON(sprinklersDevice.sectionRunner), - programs: sprinklersDevice.programs.map(programToJSON), - }; -} - -export function sprinklersDeviceFromJSON(sprinklersDevice: s.SprinklersDevice, json: ISprinklersDeviceJSON) { - assign(sprinklersDevice, pick(json, sprinklersDeviceProps)); - sprinklersDevice.sections.length = json.sections.length; - sprinklersDevice.sections.forEach((section, i) => - sectionFromJSON(section, json.sections[i])); - sectionRunnerFromJSON(sprinklersDevice.sectionRunner, json.sectionRunner); - sprinklersDevice.programs.length = json.programs.length; - sprinklersDevice.programs.forEach((program, i) => - programFromJSON(program, json.programs[i])); -} +export const durationSchema: PropSchema = { + serializer: (duration: s.Duration | null) => + duration != null ? duration.toSeconds() : null, + deserializer: (json: any) => + typeof json === "number" ? s.Duration.fromSeconds(json) : null, +}; + +export const dateSchema: PropSchema = { + serializer: (jsDate: Date | null) => jsDate != null ? + jsDate.toISOString() : null, + deserializer: (json: any) => typeof json === "string" ? + new Date(json) : null, +}; + +export const dateOfYearSchema: ModelSchema = { + factory: () => new s.DateOfYear(), + props: { + year: primitive(), + month: primitive(), // this only works if it is represented as a # from 0-12 + day: primitive(), + }, +}; + +export const timeOfDaySchema: ModelSchema = { + factory: () => new s.TimeOfDay(), + props: { + hour: primitive(), + minute: primitive(), + second: primitive(), + millisecond: primitive(), + }, +}; + +export const sectionSchema: ModelSchema = { + factory: (c) => new (c.parentContext.target as s.SprinklersDevice).sectionConstructor( + c.parentContext.target, c.json.id), + props: { + name: primitive(), + state: primitive(), + }, +}; + +export const sectionRunSchema: ModelSchema = { + factory: (c) => new s.SectionRun(c.json.id), + props: { + id: primitive(), + section: primitive(), + duration: durationSchema, + startTime: dateSchema, + endTime: dateSchema, + }, +}; + +export const sectionRunnerSchema: ModelSchema = { + factory: (c) => new (c.parentContext.target as s.SprinklersDevice).sectionRunnerConstructor( + c.parentContext.target), + props: { + queue: list(object(sectionRunSchema)), + current: object(sectionRunSchema), + paused: primitive(), + }, +}; + +export const scheduleSchema: ModelSchema = { + factory: () => new s.Schedule(), + props: { + times: list(object(timeOfDaySchema)), + weekdays: list(primitive()), + from: object(dateOfYearSchema), + to: object(dateOfYearSchema), + }, +}; + +export const programItemSchema: ModelSchema = { + factory: () => new s.ProgramItem(), + props: { + section: primitive(), + duration: durationSchema, + }, +}; + +export const programSchema: ModelSchema = { + factory: (c) => new (c.parentContext.target as s.SprinklersDevice).programConstructor( + c.parentContext.target, c.json.id), + props: { + name: primitive(), + enabled: primitive(), + schedule: object(scheduleSchema), + sequence: list(object(programItemSchema)), + running: primitive(), + }, +}; + +export const sprinklersDeviceSchema = createSimpleSchema({ + connected: primitive(), + sections: list(object(sectionSchema)), + sectionRunner: object(sectionRunnerSchema), + programs: list(object(programSchema)), +}); diff --git a/common/sprinklers/json/list.ts b/common/sprinklers/json/list.ts new file mode 100644 index 0000000..2c9d19e --- /dev/null +++ b/common/sprinklers/json/list.ts @@ -0,0 +1,65 @@ +import { primitive, PropSchema } from "serializr"; + +function invariant(cond: boolean, message?: string) { + if (!cond) { + throw new Error("[serializr] " + (message || "Illegal State")); + } +} + +function isPropSchema(thing: any) { + return thing && thing.serializer && thing.deserializer; +} + +function isAliasedPropSchema(propSchema: any) { + return typeof propSchema === "object" && !!propSchema.jsonname; +} + +function parallel(ar: any[], processor: (item: any, done: any) => void, cb: any) { + if (ar.length === 0) { + return void cb(null, []); + } + let left = ar.length; + const resultArray: any[] = []; + let failed = false; + const processorCb = (idx: number, err: any, result: any) => { + if (err) { + if (!failed) { + failed = true; + cb(err); + } + } else if (!failed) { + resultArray[idx] = result; + if (--left === 0) { + cb(null, resultArray); + } + } + }; + ar.forEach((value, idx) => processor(value, processorCb.bind(null, idx))); +} + +export default function list(propSchema: PropSchema): PropSchema { + propSchema = propSchema || primitive(); + invariant(isPropSchema(propSchema), "expected prop schema as first argument"); + invariant(!isAliasedPropSchema(propSchema), "provided prop is aliased, please put aliases first"); + return { + serializer(ar) { + invariant(ar && typeof ar.length === "number" && typeof ar.map === "function", + "expected array (like) object"); + return ar.map(propSchema.serializer); + }, + deserializer(jsonArray, done, context) { + if (jsonArray === null) { // sometimes go will return null in place of empty array + return void done(null, []); + } + if (!Array.isArray(jsonArray)) { + return void done("[serializr] expected JSON array", null); + } + parallel( + jsonArray, + (item: any, itemDone: (err: any, targetPropertyValue: any) => void) => + propSchema.deserializer(item, itemDone, context, undefined), + done, + ); + }, + }; +} diff --git a/common/sprinklers/mqtt/index.ts b/common/sprinklers/mqtt/index.ts index d08eeb9..317fe75 100644 --- a/common/sprinklers/mqtt/index.ts +++ b/common/sprinklers/mqtt/index.ts @@ -1,23 +1,15 @@ +import { cloneDeep } from "lodash"; import * as mqtt from "mqtt"; +import { deserialize, update } from "serializr"; import logger from "@common/logger"; -import { - Duration, - ISprinklersApi, - Program, - ProgramItem, - Schedule, - Section, - SectionRun, - SectionRunner, - SprinklersDevice, - TimeOfDay, -} from "@common/sprinklers"; +import * as s from "@common/sprinklers"; +import * as schema from "@common/sprinklers/json"; import { checkedIndexOf } from "@common/utils"; const log = logger.child({ source: "mqtt" }); -export class MqttApiClient implements ISprinklersApi { +export class MqttApiClient implements s.ISprinklersApi { readonly mqttUri: string; client: mqtt.Client; connected: boolean; @@ -54,7 +46,7 @@ export class MqttApiClient implements ISprinklersApi { }); } - getDevice(prefix: string): SprinklersDevice { + getDevice(prefix: string): s.SprinklersDevice { if (/\//.test(prefix)) { throw new Error("Prefix cannot contain a /"); } @@ -84,7 +76,8 @@ export class MqttApiClient implements ISprinklersApi { } } - private processMessage(topic: string, payload: Buffer, packet: mqtt.Packet) { + private processMessage(topic: string, payloadBuf: Buffer, packet: mqtt.Packet) { + const payload = payloadBuf.toString("utf8"); log.trace({ topic, payload }, "message arrived: "); const topicIdx = topic.indexOf("/"); // find the first / const prefix = topic.substr(0, topicIdx); // assume prefix does not contain a / @@ -108,7 +101,7 @@ const subscriptions = [ "/section_runner", ]; -class MqttSprinklersDevice extends SprinklersDevice { +class MqttSprinklersDevice extends s.SprinklersDevice { readonly apiClient: MqttApiClient; readonly prefix: string; @@ -127,6 +120,10 @@ class MqttSprinklersDevice extends SprinklersDevice { return this.prefix; } + get sectionConstructor() { return MqttSection; } + get sectionRunnerConstructor() { return MqttSectionRunner; } + get programConstructor() { return MqttProgram; } + doSubscribe() { const topics = subscriptions.map((filter) => this.prefix + filter); this.apiClient.client.subscribe(topics, { qos: 1 }); @@ -142,8 +139,7 @@ class MqttSprinklersDevice extends SprinklersDevice { * @param topic The topic, with prefix removed * @param payload The payload buffer */ - onMessage(topic: string, payloadBuf: Buffer) { - const payload = payloadBuf.toString("utf8"); + onMessage(topic: string, payload: string) { if (topic === "connected") { this.connected = (payload === "true"); log.trace(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`); @@ -207,7 +203,7 @@ class MqttSprinklersDevice extends SprinklersDevice { log.warn({ topic }, "MqttSprinklersDevice recieved invalid message"); } - runSection(section: Section | number, duration: Duration) { + runSection(section: s.Section | number, duration: s.Duration) { const sectionNum = checkedIndexOf(section, this.sections, "Section"); const payload: IRunSectionJSON = { duration: duration.toSeconds(), @@ -215,9 +211,9 @@ class MqttSprinklersDevice extends SprinklersDevice { return this.makeRequest(`sections/${sectionNum}/run`, payload); } - runProgram(program: Program | number) { + runProgram(program: s.Program | number) { const programNum = checkedIndexOf(program, this.programs, "Program"); - return this.makeRequest(`programs/${programNum}/run`, {}); + return this.makeRequest(`programs/${programNum}/run`); } cancelSectionRunById(id: number) { @@ -273,120 +269,40 @@ interface IRunSectionJSON { duration: number; } -class MqttSection extends Section { +class MqttSection extends s.Section { onMessage(topic: string, payload: string) { if (topic === "state") { this.state = (payload === "true"); } else if (topic == null) { - const json = JSON.parse(payload) as ISectionJSON; - this.name = json.name; + this.updateFromJSON(JSON.parse(payload)); } } -} - -interface ITimeOfDayJSON { - hour: number; - minute: number; - second: number; - millisecond: number; -} - -function timeOfDayFromJSON(json: ITimeOfDayJSON): TimeOfDay { - return new TimeOfDay(json.hour, json.minute, json.second, json.millisecond); -} - -interface IScheduleJSON { - times: ITimeOfDayJSON[]; - weekdays: number[]; - from?: string; - to?: string; -} - -function scheduleFromJSON(json: IScheduleJSON): Schedule { - const sched = new Schedule(); - sched.times = json.times.map(timeOfDayFromJSON); - sched.weekdays = json.weekdays; - sched.from = json.from == null ? null : new Date(json.from); - sched.to = json.to == null ? null : new Date(json.to); - return sched; -} - -interface IProgramItemJSON { - section: number; - duration: number; -} -interface IProgramJSON { - name: string; - enabled: boolean; - sequence: IProgramItemJSON[]; - sched: IScheduleJSON; + updateFromJSON(json: any) { + update(schema.sectionSchema, this, json); + } } -class MqttProgram extends Program { +class MqttProgram extends s.Program { onMessage(topic: string, payload: string) { if (topic === "running") { this.running = (payload === "true"); } else if (topic == null) { - const json = JSON.parse(payload) as Partial; - this.updateFromJSON(json); + this.updateFromJSON(JSON.parse(payload)); } } - updateFromJSON(json: Partial) { - if (json.name != null) { - this.name = json.name; - } - if (json.enabled != null) { - this.enabled = json.enabled; - } - if (json.sequence != null) { - this.sequence = json.sequence.map((item) => (new ProgramItem( - item.section, - Duration.fromSeconds(item.duration), - ))); - } - if (json.sched != null) { - this.schedule = scheduleFromJSON(json.sched); - } + updateFromJSON(json: any) { + update(schema.programSchema, this, json); } } -export interface ISectionRunJSON { - id: number; - section: number; - duration: number; - startTime?: number; - pauseTime?: number; -} - -function sectionRunFromJSON(json: ISectionRunJSON) { - const run = new SectionRun(json.id); - run.section = json.section; - run.duration = Duration.fromSeconds(json.duration); - run.startTime = json.startTime == null ? null : new Date(json.startTime); - run.pauseTime = json.pauseTime == null ? null : new Date(json.pauseTime); - return run; -} - -interface ISectionRunnerJSON { - queue: ISectionRunJSON[]; - current: ISectionRunJSON | null; - paused: boolean; -} - -class MqttSectionRunner extends SectionRunner { +class MqttSectionRunner extends s.SectionRunner { onMessage(payload: string) { - const json = JSON.parse(payload) as ISectionRunnerJSON; - this.updateFromJSON(json); + this.updateFromJSON(JSON.parse(payload)); } - updateFromJSON(json: ISectionRunnerJSON) { - this.queue.length = 0; - if (json.queue && json.queue.length) { // null means empty queue - this.queue.push.apply(this.queue, json.queue.map(sectionRunFromJSON)); - } - this.current = json.current == null ? null : sectionRunFromJSON(json.current); - this.paused = json.paused; + updateFromJSON(json: any) { + update(schema.sectionRunnerSchema, this, json); } } diff --git a/common/sprinklers/schedule.ts b/common/sprinklers/schedule.ts index 39e5583..a9cafe6 100644 --- a/common/sprinklers/schedule.ts +++ b/common/sprinklers/schedule.ts @@ -6,7 +6,7 @@ export class TimeOfDay { readonly second: number; readonly millisecond: number; - constructor(hour: number, minute: number = 0, second: number = 0, millisecond: number = 0) { + constructor(hour: number = 0, minute: number = 0, second: number = 0, millisecond: number = 0) { this.hour = hour; this.minute = minute; this.second = second; @@ -22,9 +22,40 @@ export enum Weekday { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, } +export enum Month { + January = 1, + February = 2, + March = 3, + April = 4, + May = 5, + June = 6, + July = 7, + August = 8, + September = 9, + October = 10, + November = 11, + December = 12, +} + +export class DateOfYear { + readonly day: number; + readonly month: Month; + readonly year: number; + + constructor(day: number = 0, month: Month = Month.January, year: number = 0) { + this.day = day; + this.month = month; + this.year = year; + } + + toString() { + return `${Month[this.month]} ${this.day}, ${this.year}`; + } +} + export class Schedule { @observable times: TimeOfDay[] = []; @observable weekdays: Weekday[] = []; - @observable from: Date | null = null; - @observable to: Date | null = null; + @observable from: DateOfYear | null = null; + @observable to: DateOfYear | null = null; } diff --git a/package.json b/package.json index 73815bc..1c2839b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "homepage": "https://github.com/amikhalev/sprinklers3#readme", "dependencies": { + "@types/async": "^2.0.43", "@types/chalk": "^0.4.31", "@types/classnames": "^2.2.0", "@types/core-js": "^0.9.43", @@ -45,6 +46,7 @@ "@types/react-dom": "^15.5.0", "@types/react-fontawesome": "^1.5.0", "@types/react-hot-loader": "^3.0.4", + "async": "^2.5.0", "autoprefixer": "^7.1.4", "case-sensitive-paths-webpack-plugin": "^2.1.1", "classnames": "^2.2.5", @@ -70,6 +72,7 @@ "react-fontawesome": "^1.6.1", "semantic-ui-css": "^2.2.10", "semantic-ui-react": "^0.73.0", + "serializr": "^1.1.13", "tslint-loader": "^3.5.3", "uglifyjs-webpack-plugin": "^0.4.6", "url-loader": "^0.5.9" diff --git a/server/configureLogger.ts b/server/configureLogger.ts index 6de12be..7da8d5d 100644 --- a/server/configureLogger.ts +++ b/server/configureLogger.ts @@ -1,6 +1,5 @@ import log, { setLogger } from "@common/logger"; setLogger(log.child({ name: "sprinklers3/server", - level: "trace", + level: "debug", })); - diff --git a/server/index.ts b/server/index.ts index 1ff3347..633ce32 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,7 @@ +/* tslint:disable:ordered-imports */ import "./configureAlias"; - import "env"; +import "./configureLogger"; import log from "@common/logger"; import * as mqtt from "@common/sprinklers/mqtt"; @@ -11,12 +12,13 @@ const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883"); mqttClient.start(); -import * as sjson from "@common/sprinklers/json"; +import { sprinklersDeviceSchema } from "@common/sprinklers/json"; import { autorun } from "mobx"; +import { serialize } from "serializr"; const device = mqttClient.getDevice("grinklers"); app.get("/api/grinklers", (req, res) => { - const j = sjson.sprinklersDeviceToJSON(device); + const j = serialize(sprinklersDeviceSchema, device); log.trace(j); res.send(j); }); diff --git a/yarn.lock b/yarn.lock index 021e371..a985177 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@types/async@^2.0.43": + version "2.0.43" + resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.43.tgz#0d6fce7e11a582b4251a4bf5439399428c79e387" + "@types/chalk@^0.4.31": version "0.4.31" resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9" @@ -334,7 +338,7 @@ async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.1.2, async@^2.4.1: +async@^2.1.2, async@^2.4.1, async@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" dependencies: @@ -4706,6 +4710,10 @@ send@0.15.4: range-parser "~1.2.0" statuses "~1.3.1" +serializr@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/serializr/-/serializr-1.1.13.tgz#21b20f220fcf94caaecd86eb09e438cde6532b25" + serve-index@^1.7.2: version "1.9.0" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7"