diff --git a/package.json b/package.json index 58a655f..76fd2bf 100644 --- a/package.json +++ b/package.json @@ -22,21 +22,23 @@ "type": "git" }, "scripts": { - "test": "NODE_PATH=src node node_modules/.bin/ava 'tests/*.spec.ts' -e", - "test:debug": "NODE_PATH=src node inspect node_modules/ava/profile.js", - "test:all": "NODE_PATH=src node node_modules/.bin/ava", - "lint": "tslint -p tsconfig.json", - "validate:tslint": "tslint-config-prettier-check ./tslint.json", - "prepublishOnly": "yarn lint && yarn test && yarn tsc" + "test": "NODE_PATH=src node node_modules/.bin/ava 'tests/*.spec.ts' -e", + "test:debug": "NODE_PATH=src node inspect node_modules/ava/profile.js", + "test:all": "NODE_PATH=src node node_modules/.bin/ava", + "lint": "tslint -p tsconfig.json", + "validate:tslint": "tslint-config-prettier-check ./tslint.json", + "prepublishOnly": "yarn lint && yarn test && yarn tsc" }, "dependencies": {}, "devDependencies": { "@types/chai": "^4.1.7", "@types/node": "^12.7.2", + "@types/sinon": "^7.0.13", "ava": "^1.0.0-rc.2", "chai": "^4.2.0", "mhysa": "./", "prettier": "^1.14.3", + "sinon": "^7.4.2", "ts-node": "^8.3.0", "tslint": "^5.11.0", "tslint-config-prettier": "^1.16.0", diff --git a/src/functions/compose.ts b/src/functions/compose.ts index da46ee8..00ff00e 100644 --- a/src/functions/compose.ts +++ b/src/functions/compose.ts @@ -29,6 +29,7 @@ enum EventSubscription { All, Self, } + const eventsTarget = { close: EventSubscription.Last, data: EventSubscription.Last, diff --git a/src/functions/demux.ts b/src/functions/demux.ts index 138e6c7..9c9e624 100644 --- a/src/functions/demux.ts +++ b/src/functions/demux.ts @@ -1,5 +1,27 @@ import { WritableOptions, Writable } from "stream"; +enum EventSubscription { + Last = 0, + First, + All, + Self, + Unhandled, +} + +const eventsTarget = { + close: EventSubscription.Self, + data: EventSubscription.All, + drain: EventSubscription.Self, + end: EventSubscription.Self, + error: EventSubscription.Self, + finish: EventSubscription.Self, + pause: EventSubscription.Self, + pipe: EventSubscription.Unhandled, + readable: EventSubscription.Self, + resume: EventSubscription.Self, + unpipe: EventSubscription.Unhandled, +}; + /** * Return a Duplex stream that is pushed data from multiple sources * @param streams Source streams to multiplex @@ -76,4 +98,24 @@ class Demux extends Writable { }); } } + + public on(event: string, cb: any) { + switch (eventsTarget[event]) { + case EventSubscription.Self: + super.on(event, cb); + break; + case EventSubscription.All: + Object.keys(this.streamsByKey).forEach(key => + this.streamsByKey[key].stream.on(event, cb), + ); + break; + case EventSubscription.Unhandled: + throw new Error( + "Stream must be multiplexed before handling this event", + ); + default: + super.on(event, cb); + } + return this; + } } diff --git a/tests/compose.spec.ts b/tests/compose.spec.ts index f05a248..6b97798 100644 --- a/tests/compose.spec.ts +++ b/tests/compose.spec.ts @@ -1,6 +1,6 @@ const test = require("ava"); const { expect } = require("chai"); -const { compose, composeDuplex, map, rate } = require("../src"); +const { compose, map } = require("../src"); const { sleep } = require("../src/helpers"); import { performance } from "perf_hooks"; @@ -308,12 +308,12 @@ test.cb( t => { t.plan(6); interface Chunk { - index: number; - mapped: string[]; + key: string; + mapped: number[]; } const first = map( async (chunk: Chunk) => { - chunk.mapped.push("first"); + chunk.mapped.push(1); return chunk; }, { @@ -324,9 +324,10 @@ test.cb( const second = map( async (chunk: Chunk) => { pendingReads--; - await sleep(500); + await sleep(200); + expect(second._writableState.length).to.be.equal(1); expect(first._readableState.length).to.equal(pendingReads); - chunk.mapped.push("second"); + chunk.mapped.push(2); return chunk; }, { objectMode: true, highWaterMark: 1 }, @@ -342,27 +343,25 @@ test.cb( composed.on("drain", () => { expect(composed._writableState.length).to.be.equal(0); - expect(performance.now() - start).to.be.lessThan(100); + expect(performance.now() - start).to.be.lessThan(50); t.pass(); }); composed.on("data", (chunk: Chunk) => { // Since second is bottleneck, composed will write into first immediately. Buffer should be empty. expect(composed._writableState.length).to.be.equal(0); - expect(chunk.mapped.length).to.equal(2); - expect(chunk.mapped).to.deep.equal(["first", "second"]); t.pass(); - if (chunk.index === 5) { + if (chunk.key === "e") { t.end(); } }); const input = [ - { index: 1, mapped: [] }, - { index: 2, mapped: [] }, - { index: 3, mapped: [] }, - { index: 4, mapped: [] }, - { index: 5, mapped: [] }, + { key: "a", mapped: [] }, + { key: "b", mapped: [] }, + { key: "c", mapped: [] }, + { key: "d", mapped: [] }, + { key: "e", mapped: [] }, ]; let pendingReads = input.length; diff --git a/tests/demux.spec.ts b/tests/demux.spec.ts index cdc91d5..9ff3498 100644 --- a/tests/demux.spec.ts +++ b/tests/demux.spec.ts @@ -1,53 +1,38 @@ import test from "ava"; import { expect } from "chai"; -import { demux, map } from "../src"; +const { demux, map } = require("../src"); import { Writable } from "stream"; +const sinon = require("sinon"); +const { sleep } = require("../src/helpers"); +import { performance } from "perf_hooks"; interface Test { key: string; - val: number; + visited: number[]; } -test.cb("should spread per key", t => { - t.plan(5); +test.cb("demux() constructor should be called once per key", t => { + t.plan(1); const input = [ - { key: "a", val: 1 }, - { key: "b", val: 2 }, - { key: "a", val: 3 }, - { key: "c", val: 4 }, + { key: "a", visited: [] }, + { key: "b", visited: [] }, + { key: "a", visited: [] }, + { key: "c", visited: [] }, ]; - const results = [ - { key: "a", val: 2 }, - { key: "b", val: 3 }, - { key: "a", val: 4 }, - { key: "c", val: 5 }, - ]; - const destinationStreamKeys = []; - let i = 0; - const sink = new Writable({ - objectMode: true, - write(chunk, enc, cb) { - expect(results).to.deep.include(chunk); - expect(input).to.not.deep.include(chunk); - t.pass(); - cb(); - }, - }); - const construct = (destKey: string) => { - destinationStreamKeys.push(destKey); + const construct = sinon.spy((destKey: string) => { const dest = map((chunk: Test) => { - return { - ...chunk, - val: chunk.val + 1, - }; + chunk.visited.push(1); + return chunk; }); - dest.pipe(sink); return dest; - }; + }); const demuxed = demux(construct, { key: "key" }, { objectMode: true }); + demuxed.on("finish", () => { - expect(destinationStreamKeys).to.deep.equal(["a", "b", "c"]); + expect(construct.withArgs("a").callCount).to.equal(1); + expect(construct.withArgs("b").callCount).to.equal(1); + expect(construct.withArgs("c").callCount).to.equal(1); t.pass(); t.end(); }); @@ -56,50 +41,34 @@ test.cb("should spread per key", t => { demuxed.end(); }); -test.cb("should spread per key using keyBy", t => { - t.plan(5); +test.cb("demux() constructor should be called once per key using keyBy", t => { + t.plan(1); const input = [ - { key: "a", val: 1 }, - { key: "b", val: 2 }, - { key: "a", val: 3 }, - { key: "c", val: 4 }, + { key: "a", visited: [] }, + { key: "b", visited: [] }, + { key: "a", visited: [] }, + { key: "c", visited: [] }, ]; - const results = [ - { key: "a", val: 2 }, - { key: "b", val: 3 }, - { key: "a", val: 4 }, - { key: "c", val: 5 }, - ]; - const destinationStreamKeys = []; - const sink = new Writable({ - objectMode: true, - write(chunk, enc, cb) { - expect(results).to.deep.include(chunk); - expect(input).to.not.deep.include(chunk); - t.pass(); - cb(); - }, - }); - const construct = (destKey: string) => { - destinationStreamKeys.push(destKey); + + const construct = sinon.spy((destKey: string) => { const dest = map((chunk: Test) => { - return { - ...chunk, - val: chunk.val + 1, - }; + chunk.visited.push(1); + return chunk; }); - dest.pipe(sink); return dest; - }; + }); const demuxed = demux( construct, - { keyBy: (chunk: any) => chunk.key }, + { keyBy: item => item.key }, { objectMode: true }, ); + demuxed.on("finish", () => { - expect(destinationStreamKeys).to.deep.equal(["a", "b", "c"]); + expect(construct.withArgs("a").callCount).to.equal(1); + expect(construct.withArgs("b").callCount).to.equal(1); + expect(construct.withArgs("c").callCount).to.equal(1); t.pass(); t.end(); }); @@ -110,17 +79,18 @@ test.cb("should spread per key using keyBy", t => { test.cb("should emit errors", t => { t.plan(2); + let index = 0; const input = [ - { key: "a", val: 1 }, - { key: "b", val: 2 }, - { key: "a", val: 3 }, - { key: "a", val: 4 }, + { key: "a", visited: [] }, + { key: "b", visited: [] }, + { key: "a", visited: [] }, + { key: "a", visited: [] }, ]; const results = [ - { key: "a", val: 2 }, - { key: "b", val: 3 }, - { key: "a", val: 4 }, - { key: "a", val: 5 }, + { key: "a", visited: [0] }, + { key: "b", visited: [1] }, + { key: "a", visited: [2] }, + { key: "a", visited: [3] }, ]; const destinationStreamKeys = []; const sink = new Writable({ @@ -131,7 +101,7 @@ test.cb("should emit errors", t => { t.pass(); cb(); }, - }).on("unpipe", e => console.log("sink err")); + }); const construct = (destKey: string) => { destinationStreamKeys.push(destKey); @@ -139,11 +109,12 @@ test.cb("should emit errors", t => { if (chunk.key === "b") { throw new Error("Caught object with key 'b'"); } - return { - ...chunk, - val: chunk.val + 1, - }; - }).on("error", e => console.log("got err")); + + const _chunk = { ...chunk, visited: [] }; + _chunk.visited.push(index); + index++; + return _chunk; + }).on("error", () => {}); dest.pipe(sink); return dest; @@ -162,3 +133,374 @@ test.cb("should emit errors", t => { input.forEach(event => demuxed.write(event)); demuxed.end(); }); + +test("compose() should emit drain event ~rate * highWaterMark ms for every write that causes backpressure", async t => { + t.plan(7); + const highWaterMark = 5; + const _rate = 25; + return new Promise(async (resolve, reject) => { + interface Chunk { + key: string; + mapped: number[]; + } + const sink = new Writable({ + objectMode: true, + write(chunk, encoding, cb) { + cb(); + t.pass(); + pendingReads--; + if (pendingReads === 0) { + resolve(); + } + }, + }); + const construct = (destKey: string) => { + const first = map(async (chunk: Chunk) => { + await sleep(_rate); + chunk.mapped.push(1); + return chunk; + }); + + const second = map(async (chunk: Chunk) => { + chunk.mapped.push(2); + return chunk; + }); + + first.pipe(second).pipe(sink); + return first; + }; + const _demux = demux( + construct, + { key: "key" }, + { + objectMode: true, + highWaterMark, + }, + ); + _demux.on("error", err => { + reject(); + }); + + _demux.on("drain", () => { + expect(_demux._writableState.length).to.be.equal(0); + expect(performance.now() - start).to.be.greaterThan(_rate); + t.pass(); + }); + + const input = [ + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + ]; + let pendingReads = input.length; + + let start = performance.now(); + for (const item of input) { + const res = _demux.write(item); + expect(_demux._writableState.length).to.be.at.most(highWaterMark); + if (!res) { + start = performance.now(); + await sleep(100); + } + } + }); +}); + +test("demux() should emit one drain event when writing 6 items with highWaterMark of 5", t => { + t.plan(7); + const highWaterMark = 5; + return new Promise(async (resolve, reject) => { + interface Chunk { + key: string; + mapped: number[]; + } + const sink = new Writable({ + objectMode: true, + write(chunk, encoding, cb) { + cb(); + t.pass(); + if (chunk.key === "f") { + resolve(); + } + }, + }); + const construct = (destKey: string) => { + const first = map(async (chunk: Chunk) => { + chunk.mapped.push(1); + return chunk; + }); + + const second = map(async (chunk: Chunk) => { + chunk.mapped.push(2); + return chunk; + }); + + first.pipe(second).pipe(sink); + return first; + }; + const _demux = demux( + construct, + { key: "key" }, + { + objectMode: true, + highWaterMark, + }, + ); + _demux.on("error", err => { + reject(); + }); + + _demux.on("drain", () => { + expect(_demux._writableState.length).to.be.equal(0); + t.pass(); + }); + + const input = [ + { key: "a", mapped: [] }, + { key: "b", mapped: [] }, + { key: "c", mapped: [] }, + { key: "d", mapped: [] }, + { key: "e", mapped: [] }, + { key: "f", mapped: [] }, + ]; + + for (const item of input) { + const res = _demux.write(item); + expect(_demux._writableState.length).to.be.at.most(highWaterMark); + if (!res) { + await sleep(10); + } + } + }); +}); + +test.cb( + "demux() should emit drain event after 500 ms when writing 5 items that take 100ms to process with a highWaterMark of 5 ", + t => { + t.plan(6); + const _rate = 100; + const highWaterMark = 5; + interface Chunk { + key: string; + mapped: number[]; + } + const sink = new Writable({ + objectMode: true, + write(chunk, encoding, cb) { + t.pass(); + cb(); + if (pendingReads === 0) { + t.end(); + } + }, + }); + const construct = (destKey: string) => { + const first = map( + async (chunk: Chunk) => { + chunk.mapped.push(1); + await sleep(_rate); + return chunk; + }, + { objectMode: true }, + ); + + const second = map( + (chunk: Chunk) => { + pendingReads--; + chunk.mapped.push(2); + return chunk; + }, + { objectMode: true, highWaterMark: 1 }, + ); + + first.pipe(second).pipe(sink); + return first; + }; + const _demux = demux( + construct, + { key: "key" }, + { + objectMode: true, + highWaterMark, + }, + ); + _demux.on("error", err => { + t.end(err); + }); + + _demux.on("drain", () => { + expect(_demux._writableState.length).to.be.equal(0); + expect(performance.now() - start).to.be.greaterThan( + _rate * input.length, + ); + t.pass(); + }); + + const input = [ + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + ]; + + let pendingReads = input.length; + input.forEach(item => { + _demux.write(item); + }); + const start = performance.now(); + }, +); +test.cb( + "demux() should emit drain event immediately when second stream is bottleneck", + t => { + t.plan(6); + const highWaterMark = 5; + interface Chunk { + key: string; + mapped: number[]; + } + const sink = new Writable({ + objectMode: true, + write(chunk, encoding, cb) { + t.pass(); + cb(); + if (pendingReads === 0) { + t.end(); + } + }, + }); + const construct = (destKey: string) => { + const first = map( + (chunk: Chunk) => { + chunk.mapped.push(1); + return chunk; + }, + { objectMode: true }, + ); + + const second = map( + async (chunk: Chunk) => { + pendingReads--; + await sleep(200); + chunk.mapped.push(2); + expect(second._writableState.length).to.be.equal(1); + expect(first._readableState.length).to.equal(pendingReads); + return chunk; + }, + { objectMode: true, highWaterMark: 1 }, + ); + + first.pipe(second).pipe(sink); + return first; + }; + const _demux = demux( + construct, + { key: "key" }, + { + objectMode: true, + highWaterMark, + }, + ); + _demux.on("error", err => { + t.end(err); + }); + + _demux.on("drain", () => { + expect(_demux._writableState.length).to.be.equal(0); + expect(performance.now() - start).to.be.lessThan(50); + t.pass(); + }); + + const input = [ + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + ]; + + let pendingReads = input.length; + input.forEach(item => { + _demux.write(item); + }); + const start = performance.now(); + }, +); + +test("demux() should emit drain event and first should contain up to highWaterMark items in readable state when second is bottleneck", t => { + t.plan(6); + const highWaterMark = 5; + return new Promise(async (resolve, reject) => { + interface Chunk { + key: string; + mapped: number[]; + } + const sink = new Writable({ + objectMode: true, + write(chunk, encoding, cb) { + t.pass(); + cb(); + if (pendingReads === 0) { + resolve(); + } + }, + }); + const construct = (destKey: string) => { + const first = map( + (chunk: Chunk) => { + expect(first._readableState.length).to.be.at.most(2); + chunk.mapped.push(1); + return chunk; + }, + { objectMode: 2, highWaterMark: 2 }, + ); + + const second = map( + async (chunk: Chunk) => { + chunk.mapped.push(2); + expect(second._writableState.length).to.be.equal(1); + await sleep(100); + pendingReads--; + return chunk; + }, + { objectMode: 2, highWaterMark: 2 }, + ); + + first.pipe(second).pipe(sink); + return first; + }; + const _demux = demux( + construct, + { key: "key" }, + { + objectMode: true, + highWaterMark, + }, + ); + _demux.on("error", err => { + reject(); + }); + + _demux.on("drain", () => { + expect(_demux._writableState.length).to.be.equal(0); + t.pass(); + }); + + const input = [ + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + { key: "a", mapped: [] }, + ]; + let pendingReads = input.length; + + input.forEach(item => { + _demux.write(item); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index ee57991..ba435ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,6 +326,35 @@ dependencies: arrify "^1.0.1" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" + integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/formatio@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.1.tgz#52310f2f9bcbc67bdac18c94ad4901b95fde267e" + integrity sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" + +"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" + integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== + dependencies: + "@sinonjs/commons" "^1.3.0" + array-from "^2.1.1" + lodash "^4.17.15" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@types/chai@^4.1.7": version "4.2.0" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.0.tgz#2478260021408dec32c123a7cad3414beb811a07" @@ -355,6 +384,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg== +"@types/sinon@^7.0.13": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" + integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -452,6 +486,11 @@ array-find-index@^1.0.1: resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= +array-from@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= + array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -1113,7 +1152,7 @@ detect-libc@^1.0.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= -diff@^3.2.0: +diff@^3.2.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== @@ -1826,6 +1865,11 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1893,6 +1937,11 @@ json5@^2.1.0: dependencies: minimist "^1.2.0" +just-extend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" + integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2011,7 +2060,7 @@ lodash.merge@^4.6.1: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.13: +lodash@^4.17.13, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -2023,6 +2072,11 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" +lolex@^4.1.0, lolex@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" + integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== + loud-rejection@^1.0.0, loud-rejection@^1.2.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -2253,6 +2307,17 @@ needle@^2.2.1: iconv-lite "^0.4.4" sax "^1.2.4" +nise@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652" + integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA== + dependencies: + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + lolex "^4.1.0" + path-to-regexp "^1.7.0" + node-pre-gyp@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" @@ -2545,6 +2610,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= + dependencies: + isarray "0.0.1" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -2906,6 +2978,19 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +sinon@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.4.2.tgz#ecd54158fef2fcfbdb231a3fa55140e8cb02ad6c" + integrity sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ== + dependencies: + "@sinonjs/commons" "^1.4.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/samsam" "^3.3.3" + diff "^3.5.0" + lolex "^4.2.0" + nise "^1.5.2" + supports-color "^5.5.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -3126,7 +3211,7 @@ supertap@^1.0.0: serialize-error "^2.1.0" strip-ansi "^4.0.0" -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -3281,7 +3366,7 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==