From cc302b324775ecc65ed8467c501c65fc26f843df Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Wed, 10 Feb 2021 23:47:40 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + README.md | 12 ++ example.js | 43 ++++ index.js | 165 +++++++++++++++ numbers.txt | 10 + package.json | 21 ++ src/assert-error-type.js | 16 ++ src/create-defer.js | 19 ++ src/destroy-stream.js | 11 + src/is-stdio-stream.js | 5 + src/readable/attach-handlers.js | 29 +++ src/readable/index.js | 58 ++++++ src/readable/push-buffer.js | 121 +++++++++++ src/warn.js | 5 + src/writable/attach-handlers.js | 27 +++ src/writable/index.js | 40 ++++ src/writable/write-to-stream.js | 25 +++ yarn.lock | 353 ++++++++++++++++++++++++++++++++ 18 files changed, 962 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example.js create mode 100644 index.js create mode 100644 numbers.txt create mode 100644 package.json create mode 100644 src/assert-error-type.js create mode 100644 src/create-defer.js create mode 100644 src/destroy-stream.js create mode 100644 src/is-stdio-stream.js create mode 100644 src/readable/attach-handlers.js create mode 100644 src/readable/index.js create mode 100644 src/readable/push-buffer.js create mode 100644 src/warn.js create mode 100644 src/writable/attach-handlers.js create mode 100644 src/writable/index.js create mode 100644 src/writable/write-to-stream.js create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..081f8e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +example-piped.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..10e4256 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +TODO + +# Special case: Duplex streams + +In Node.js streams, there is a thing called a Duplex stream. This is basically a stream that has an independent readable and writable part, that are not connected together. Like a TCP socket, for example; you send data through the writable half and receive data through the readable half - but the data you receive is not the same data as you sent. + +Duplex streams are really just two independent streams that are stuffed into a single object, representing a single resource, like a network socket. And this is how ppstreams treats those - as two independent streams. In ppstreams, there are no Duplex streams, so when converting a Node.js Duplex stream, you will have to do the conversion twice - once for the readable part, and once for the writable part. This can be done with the `duplexRead` and `duplexWrite` methods. + +__Why does ppstreams not have Duplex streams?__ Because they're surprisingly tricky to implement. They would make the ppstreams specification a lot more complex, and make certain things impossible; for example, a sink stream (writable stream) in ppstreams can return a Promise to signify when it's completely done receiving data, but a source stream (readable stream) needs to return a Promise with the next read value! You can only return one or the other, so supporting both would involve a lot of special logic for detecting what kind of thing is being returned. + +Aside from that, they are also just difficult to work with. Developers routinely get tripped up by the difference between a Duplex and a Transform stream, and it's much simpler to reason about your streams code when a separate readable and writable stream don't pretend to be a single stream when they're really not. + diff --git a/example.js b/example.js new file mode 100644 index 0000000..287fc1e --- /dev/null +++ b/example.js @@ -0,0 +1,43 @@ +"use strict"; + +const Promise = require("bluebird"); +const fs = require("fs"); +const through2 = require("through2"); +const fromNodeStream = require("./"); +const pipe = require("@ppstreams/pipe"); +const collect = require("@ppstreams/collect"); +const rangeNumbers = require("@ppstreams/range-numbers"); +const bufferedMap = require("@ppstreams/buffered-map"); +const map = require("@ppstreams/map"); + +let transform = through2.obj((chunk, encoding, callback) => { + callback(null, chunk + 5); +}); + +return Promise.try(() => { + return pipe([ + rangeNumbers(0, 10), + fromNodeStream(transform), + collect() + ]).read(); +}).then((result) => { + console.log("transform result:", result); + + return pipe([ + fromNodeStream(fs.createReadStream("example.js", { encoding: "utf8" })), + bufferedMap((line) => line.split("\n")), + collect() + ]).read(); +}).then((result) => { + console.log("read result:", result); + + return pipe([ + rangeNumbers(0, 10), + map((line) => String(line) + "\n"), + fromNodeStream(fs.createWriteStream("numbers.txt", { encoding: "utf8" })) + ]).read(); +}).then((result) => { + console.log("write result:", result); + console.log("numbers.txt contents:"); + console.log(fs.readFileSync("numbers.txt", { encoding: "utf8" })); +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..e810250 --- /dev/null +++ b/index.js @@ -0,0 +1,165 @@ +"use strict"; + +const Promise = require("bluebird"); +const simpleSource = require("@ppstreams/simple-source"); +const simpleSink = require("@ppstreams/simple-sink"); +const buffer = require("@ppstreams/buffer"); +const propagatePeek = require("@ppstreams/propagate-peek"); +const propagateAbort = require("@ppstreams/propagate-abort"); +const pipe = require("@ppstreams/pipe"); +const isEndOfStream = require("@ppstreams/is-end-of-stream"); + +const createDefer = require("./src/create-defer"); +const wireUpReadableInterface = require("./src/readable"); +const wireUpWritableInterface = require("./src/writable"); + +// FIXME: Maybe also an abstraction for 'handle queue of requests', as this is used in multiple stream implementations +// TODO: Improve robustness of stream-end handling using https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_stream_finished_stream_options_callback? +// FIXME: Sequentialize all of these? + +// readable +// writable +// transform +// duplex + +module.exports = function convert(stream) { + // FIXME: Proper validation and tagging + // FIXME: Wrap v1 streams + // NOTE: Standard I/O streams are specialcased here because they may be Duplex streams; even though the other half is never actually used. We're only interested in the interface that *is* being used. + if (stream === process.stdin) { + return fromReadable(stream); + } else if (stream === process.stdout || stream === process.stderr) { + return fromWritable(stream); + } else if (stream.writable != null) { + if (stream.readable != null) { + if (stream._transform != null) { + // transform + return fromTransform(stream); + } else { + throw new Error(`Duplex streams cannot be converted with the auto-detection API. Instead, use 'fromReadable' and/or 'fromWritable' manually, depending on which parts of the Duplex stream you are interested in.`); + } + } else { + return fromWritable(stream); + } + } else if (stream.readable != null) { + return fromReadable(stream); + } else { + throw new Error(`Not a Node stream`); + } +}; + +// FIXME: Duplex APIs + + +function fromReadable(stream) { + let readable = wireUpReadableInterface(stream); + + return simpleSource({ + onRequest: () => { + return readable.request(); + }, + onAbort: () => { + return readable.destroy(); + } + }); +} + +function fromWritable(stream) { + let upstreamHasEnded = false; + let mostRecentSource = { abort: function() {} }; // FIXME: Replace with a proper spec-compliant dummy stream + + let convertedStream = simpleSink({ + onResult: (result) => { + return writable.write(result); + }, + onEnd: () => { + upstreamHasEnded = true; + return writable.end(); + }, + onAbort: (_reason) => { + return writable.destroy(); + }, + onSourceChanged: (source) => { + mostRecentSource = source; + } + }); + + // NOTE: The use of `var` is intentional, to make hoisting possible here; otherwise we'd have a broken cyclical reference + var writable = wireUpWritableInterface(stream, { + onClose: () => { + if (!upstreamHasEnded) { + convertedStream.abort(true); + } + }, + onError: (error) => { + // Make sure we notify the pipeline, if any, by passing in the most recent source stream that we've seen. + convertedStream.abort(mostRecentSource, error); + } + }); + + return convertedStream; +} + +function fromTransform(stream) { + let completionDefer; + let endHandled = false; + + // FIXME: we need to specifically watch for the `error` and `end` events on the readable interface, to know when the transform stream has fully completed processing + // Respond to the EndOfStream produced by the pushbuffer in this case + // request, destroy + let readable = wireUpReadableInterface(stream, { + onEnd: () => { + if (completionDefer != null) { + completionDefer.resolve(); + } + }, + onError: (error) => { + if (completionDefer != null) { + completionDefer.reject(error); + } + } + }); + + // write, end, destroy + var writable = wireUpWritableInterface(stream); + + let convertedStream = { + _promistreamVersion: 0, + description: `converted Node.js transform stream`, + abort: propagateAbort, + peek: propagatePeek, + read: function produceValue_nodeTransformStream(source) { + return Promise.try(() => { + return source.read(); + }).then((value) => { + writable.write(value); + + // This will quite possibly return an empty buffer, but that is fine; the `buffer` stream downstream from us will just keep reading (and therefore queueing up new items to be transformed) until it gets some results. + return readable.consumeImmediateBuffer(); + }).catch(isEndOfStream, (marker) => { + // Wait for transform stream to drain fully, `error`/`end` event, and then return whatever buffer remains. + // FIXME: Error propagation logic is pretty shaky here. Verify that we don't end up with double error reports. + if (endHandled === false) { + endHandled = true; + + writable.end(); + + return Promise.try(() => { + let { promise, defer } = createDefer(); + completionDefer = defer; + return promise; + }).then(() => { + return readable.consumeImmediateBuffer(); + }); + } else { + throw marker; + } + }); + } + }; + + return pipe([ + convertedStream, + buffer() + ]); +} diff --git a/numbers.txt b/numbers.txt new file mode 100644 index 0000000..8b1acc1 --- /dev/null +++ b/numbers.txt @@ -0,0 +1,10 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 diff --git a/package.json b/package.json new file mode 100644 index 0000000..88ef1bf --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ppstreams/from-node-stream", + "version": "0.1.0", + "main": "index.js", + "repository": "http://git.cryto.net/ppstreams/from-node-stream.git", + "author": "Sven Slootweg ", + "license": "WTFPL OR CC0-1.0", + "dependencies": { + "@joepie91/unreachable": "^1.0.0", + "@ppstreams/buffer": "^0.1.0", + "@ppstreams/end-of-stream": "^0.1.0", + "@ppstreams/is-end-of-stream": "^0.1.0", + "@ppstreams/pipe": "^0.1.1", + "@ppstreams/propagate-abort": "^0.1.3", + "@ppstreams/propagate-peek": "^0.1.0", + "@ppstreams/simple-sink": "^0.1.0", + "@ppstreams/simple-source": "^0.1.1", + "bluebird": "^3.7.2", + "split-filter": "^1.1.3" + } +} diff --git a/src/assert-error-type.js b/src/assert-error-type.js new file mode 100644 index 0000000..5d8955a --- /dev/null +++ b/src/assert-error-type.js @@ -0,0 +1,16 @@ +"use strict"; + +const util = require("util"); + +module.exports = function assertErrorType(error) { + if (!(error instanceof Error)) { + let stack = (new Error).stack + .split("\n") + .filter((line, n) => (n > 0 || !line.startsWith("Error"))) + .join("\n"); + + console.warn(`Converted Node stream produced an error value that isn't an Error instance: ${util.inspect(error)}`); + console.warn(`Stack:`); + console.warn(stack); + } +}; diff --git a/src/create-defer.js b/src/create-defer.js new file mode 100644 index 0000000..7f4480d --- /dev/null +++ b/src/create-defer.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = function createDefer() { + let resolveFunc, rejectFunc; + + // NOTE: This works because the `new Promise` callback gets executed synchronously. + let promise = new Promise((resolve, reject) => { + resolveFunc = resolve; + rejectFunc = reject; + }); + + return { + promise: promise, + defer: { + resolve: resolveFunc, + reject: rejectFunc + } + }; +}; diff --git a/src/destroy-stream.js b/src/destroy-stream.js new file mode 100644 index 0000000..79ce926 --- /dev/null +++ b/src/destroy-stream.js @@ -0,0 +1,11 @@ +"use strict"; + +const warn = require("./warn"); + +module.exports = function destroyStream(stream) { + if (typeof stream.destroy === "function") { + stream.destroy(); + } else { + warn("The stream you are converting does not have a 'destroy' method, and could therefore not be destroyed. This may cause resource leaks!"); + } +}; diff --git a/src/is-stdio-stream.js b/src/is-stdio-stream.js new file mode 100644 index 0000000..37da5ca --- /dev/null +++ b/src/is-stdio-stream.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function isStdioStream(stream) { + return (stream._isStdio === true); +}; diff --git a/src/readable/attach-handlers.js b/src/readable/attach-handlers.js new file mode 100644 index 0000000..e7c7992 --- /dev/null +++ b/src/readable/attach-handlers.js @@ -0,0 +1,29 @@ +"use strict"; + +module.exports = function attachReadableStreamHandlers({ stream, onClose, onError, onData }) { + function detachEventHandlers() { + stream.removeListener("end", onCloseWrapper); + stream.removeListener("close", onCloseWrapper); + stream.removeListener("error", onErrorWrapper); + stream.removeListener("data", onData); + } + + function attachEventHandlers() { + stream.on("end", onCloseWrapper); + stream.on("close", onCloseWrapper); + stream.on("error", onErrorWrapper); + stream.on("data", onData); + } + + function onCloseWrapper() { + onClose(); + detachEventHandlers(); + } + + function onErrorWrapper(error) { + onError(error); + detachEventHandlers(); + } + + attachEventHandlers(); +}; diff --git a/src/readable/index.js b/src/readable/index.js new file mode 100644 index 0000000..51323e9 --- /dev/null +++ b/src/readable/index.js @@ -0,0 +1,58 @@ +"use strict"; + +const attachHandlers = require("./attach-handlers"); +const createPushBuffer = require("./push-buffer"); +const destroyStream = require("../destroy-stream"); +const assertErrorType = require("../assert-error-type"); + +module.exports = function wireUpReadableInterface(stream, { onEnd, onError } = {}) { + let pushBuffer = createPushBuffer({ + onPause: function () { + if (stream.pause != null) { + stream.pause(); + return true; // FIXME: Can we verify whether the pausing was successful, somehow? Eg. to deal with streams with `readable` event handlers attached. + } else { + return false; + } + }, + onResume: function () { + if (stream.resume != null) { + stream.resume(); + return true; + } else { + throw new Error(`Stream was successfully paused but does not have a resume method. This should never happen!`); + } + } + }); + + // TODO: Verify that auto-detaching all event handlers upon end/error is actually the correct thing to do! + attachHandlers({ + stream: stream, + onData: (data) => { + pushBuffer.queueValue(data); + }, + onError: (error) => { + assertErrorType(error); + pushBuffer.queueError(error); + + if (onError != null) { + onError(error); + } + }, + onClose: () => { + pushBuffer.markEnded(); + + if (onEnd != null) { + onEnd(); + } + } + }); + + return { + request: pushBuffer.queueRequest, + consumeImmediateBuffer: pushBuffer.consumeImmediateBuffer, + destroy: () => { + return destroyStream(stream); + } + }; +}; diff --git a/src/readable/push-buffer.js b/src/readable/push-buffer.js new file mode 100644 index 0000000..8a22e11 --- /dev/null +++ b/src/readable/push-buffer.js @@ -0,0 +1,121 @@ +"use strict"; + +// FIXME: Separate this out into its own package + +const splitFilter = require("split-filter"); +const unreachable = require("@joepie91/unreachable")("@ppstreams/from-node-stream"); +const EndOfStream = require("@ppstreams/end-of-stream"); + +const warn = require("../warn"); +const createDefer = require("../create-defer"); + +module.exports = function createPushBuffer(options) { + let onPause = options.onPause || function pauseNotImplemented() { + return false; + }; + + let onResume = options.onResume || function resumeNotImplemented() { + return false; + }; + + let itemBuffer = []; + let requestQueue = []; + let isPaused = false; + let hasEnded = false; + + function resumeIfEmpty() { + let bufferIsEmpty = (itemBuffer.length === 0); + + if (bufferIsEmpty && isPaused) { + if (onResume() === true) { + isPaused = false; + } + } + } + + function attemptDrain() { + // NOTE: This must remain fully synchronous, if we want to avoid unnecessary pauses in the `data` handler + + while (requestQueue.length > 0) { + let hasItems = (itemBuffer.length > 0); + let hasResponse = (hasEnded || hasItems); + + if (hasResponse) { + let defer = requestQueue.shift(); + + if (hasItems) { + // FIXME: Does this correctly deal with an error event produced as a result of an abort? + let item = itemBuffer.shift(); + + if (item.type === "value") { + defer.resolve(item.value); + } else if (item.type === "error") { + defer.reject(item.error); + } else { + unreachable(`Unexpected item type '${item.type}'`); + } + } else if (hasEnded) { + defer.reject(new EndOfStream()); + } else { + unreachable("Invalid response state, neither has items in queue nor ended"); + } + } else { + break; + } + } + + resumeIfEmpty(); + } + + return { + queueValue: function (value) { + itemBuffer.push({ type: "value", value: value }); + attemptDrain(); + + let stillHasItemsBuffered = (itemBuffer.length > 0); + + if (stillHasItemsBuffered && !isPaused) { + if (onPause() === true) { + isPaused = true; + } else { + // FIXME: Only show this warning once? + warn("The stream you are converting does not support pausing. This may lead to unexpectedly high memory usage!"); + } + } + }, + queueError: function (error) { + itemBuffer.push({ type: "error", error: error }); + attemptDrain(); + }, + queueRequest: function () { + let { defer, promise } = createDefer(); + requestQueue.push(defer); + attemptDrain(); + return promise; + }, + markEnded: function () { + hasEnded = true; + attemptDrain(); + }, + consumeImmediateBuffer: function () { + attemptDrain(); + + // FIXME: Only return successful items here? + if (requestQueue.length > 0) { + // We won't ever serve up the buffer until any individual-item requests have been fulfilled. + return []; + } else { + let [ values, errors ] = splitFilter(itemBuffer, (item) => item.type === "value"); + + if (errors.length > 0) { + itemBuffer = values; // In case we ever write code that will do something with the remaining values in the buffer + throw errors[0].error; + } else { + itemBuffer = []; + resumeIfEmpty(); // Ensure that we haven't left the source stream in a paused state, because that would deadlock the pipeline + return values.map((item) => item.value); + } + } + } + }; +}; diff --git a/src/warn.js b/src/warn.js new file mode 100644 index 0000000..c1cb12c --- /dev/null +++ b/src/warn.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function warn(message) { + console.error(`[@ppstreams/from-node-stream] Warning: ${message}`); +}; diff --git a/src/writable/attach-handlers.js b/src/writable/attach-handlers.js new file mode 100644 index 0000000..6ce6fae --- /dev/null +++ b/src/writable/attach-handlers.js @@ -0,0 +1,27 @@ +"use strict"; + +module.exports = function attachWritableStreamHandlers({ stream, onClose, onError }) { + function detachEventHandlers() { + stream.removeListener("finish", onCloseWrapper); + stream.removeListener("close", onCloseWrapper); + stream.removeListener("error", onErrorWrapper); + } + + function attachEventHandlers() { + stream.on("finish", onCloseWrapper); + stream.on("close", onCloseWrapper); + stream.on("error", onErrorWrapper); + } + + function onCloseWrapper() { + onClose(); + detachEventHandlers(); + } + + function onErrorWrapper(error) { + onError(error); + detachEventHandlers(); + } + + attachEventHandlers(); +}; diff --git a/src/writable/index.js b/src/writable/index.js new file mode 100644 index 0000000..123da3a --- /dev/null +++ b/src/writable/index.js @@ -0,0 +1,40 @@ +"use strict"; + +const attachHandlers = require("./attach-handlers"); +const writeToStream = require("./write-to-stream"); +const isStdioStream = require("../is-stdio-stream"); +const destroyStream = require("../destroy-stream"); +const assertErrorType = require("../assert-error-type"); + +module.exports = function wireUpWritableInterface(stream, { onEnd, onError } = {}) { + attachHandlers({ + stream: stream, + onClose: () => { + if (onEnd != null) { + onEnd(); + } + }, + onError: (error) => { + assertErrorType(error); + + if (onError != null) { + onError(error); + } + } + }); + + return { + write: function (value) { + return writeToStream(stream, value); + }, + end: function () { + // stdout/stderr cannot be ended like other streams + if (!isStdioStream(stream)) { + stream.end(); + } + }, + destroy: function () { + return destroyStream(stream); + } + }; +}; diff --git a/src/writable/write-to-stream.js b/src/writable/write-to-stream.js new file mode 100644 index 0000000..046efa5 --- /dev/null +++ b/src/writable/write-to-stream.js @@ -0,0 +1,25 @@ +"use strict"; + +const isStdioStream = require("../is-stdio-stream"); + +module.exports = function writeToStream(stream, value) { + if (!isStdioStream(stream)) { + let canWriteMore = stream.write(value); + + if (canWriteMore) { + return; + } else { + return new Promise((resolve, _reject) => { + stream.once("drain", () => resolve()); + }); + } + } else { + // NOTE: According to the `stream-to-pull-stream` code, stdout/stderr behave differently from normal streams, and the `drain` event doesn't work correctly there. So instead, we use the flush callback to know when to write the next bit of data. + return new Promise((resolve, _reject) => { + stream.write(value, (_error) => { + // NOTE: We ignore any errors here, and wait for them to be thrown in the `error` event, to simplify the logic. + resolve(); + }); + }); + } +}; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..0b2dcca --- /dev/null +++ b/yarn.lock @@ -0,0 +1,353 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@joepie91/unreachable@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@joepie91/unreachable/-/unreachable-1.0.0.tgz#8032bb8a5813e81bbbe516cb3031d60818526687" + integrity sha512-vZRJ5UDq4mqP1vgSrcOLD3aIfS/nzwsvGFOOHv5sj5fa1Ss0dT1xnIzrXKLD9pu5EcUvF3K6n6jdaMW8uXpNEQ== + +"@ppstreams/aborted@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/aborted/-/aborted-0.1.0.tgz#0e7d89be4234403412dda522600ad991abd0c9b6" + integrity sha512-KNrfltZBVZqcTML2ix6Pg5KjKCnD0pA4hNKEYo+PX4iNfk3oaQa+ma54lvgPEhTSOB9Vq2PKdq1ekxD/UAaMMQ== + dependencies: + create-error "^0.3.1" + +"@ppstreams/buffer@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/buffer/-/buffer-0.1.0.tgz#22247ae19b84494cd916e702fae0568c2811e350" + integrity sha512-6dZoCZH9z969kzLhTLEHEbyHUhYVFtVXZA0t9vPMoTSuQFqlJJZzSO/7p3VyVpxW9okJmE5h4DxZoN8BJudVlg== + dependencies: + "@joepie91/unreachable" "^1.0.0" + "@ppstreams/propagate-abort" "^0.1.2" + bluebird "^3.5.4" + +"@ppstreams/end-of-stream@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/end-of-stream/-/end-of-stream-0.1.0.tgz#fa1d3c7cbef9c2cdfdd5132a9467221826eef0b1" + integrity sha512-Fo3ljetPOuy0cV8bDtYc9vFxtRnePr4+1hy3vVmD8uxR2/oZWTgL64R91RkJSxh2FpI33X6hstnP7qEbbSQfhQ== + dependencies: + create-error "^0.3.1" + +"@ppstreams/is-aborted@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/is-aborted/-/is-aborted-0.1.0.tgz#0beb590a663a09683898de9f335320e07cc2afd3" + integrity sha512-Y/th7e/VtLjnUUuYo/URkN+BEF9GYmUPo91kd2YS//L3wmmeeXwCy+cMuFLf1v4T3szLQqUbdX1YM6vyfXpiIg== + +"@ppstreams/is-end-of-stream@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/is-end-of-stream/-/is-end-of-stream-0.1.0.tgz#703b0530698dc920a8fbdeea9812ef63a6fdb42c" + integrity sha512-F7J7ey5oApuH/+QD/CCotePi9ID1b5Wl1h7/IAArjL7ETDOEyHuQevsi/KXIq8kY+0YArW3F0c4QqgQujjvSnQ== + +"@ppstreams/pipe@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@ppstreams/pipe/-/pipe-0.1.1.tgz#453079d2ed8074e8a105f572967e50b0bcb7ca53" + integrity sha512-Wn/uBJArde8UwB5NrZbS4kYvUHdJtloRaobAwUAUmVKqPYTSs5zcAOy3AmxRw0k+aB4w6UtlZ47lM57K/DV/QQ== + +"@ppstreams/propagate-abort@^0.1.2", "@ppstreams/propagate-abort@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@ppstreams/propagate-abort/-/propagate-abort-0.1.3.tgz#ddb6f63e9891d642e225802d690186db6a5e2245" + integrity sha512-mFG6okJF+l/3Ct6SmUAIxG75tXmQlK3P/O7w4IO9rjjciww8fa5srd66c/XWUk30qLh0gaPgR38TmvynsHS6bA== + +"@ppstreams/propagate-peek@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/propagate-peek/-/propagate-peek-0.1.0.tgz#b8d24aa611845f64cf56b8cfd84266ce443b5f24" + integrity sha512-rX3ECnMxdD70fkEB6gQPr4aFg5bgOQ2Cks1LgFZv7Fj7qbj2SIjj1jHnhlFD1Wm6OZlcGXWuH2nbQp1hKoutkg== + +"@ppstreams/simple-sink@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@ppstreams/simple-sink/-/simple-sink-0.1.0.tgz#add7aae3068b9130a9b74d0e840a767c8b22bd0f" + integrity sha512-JjfPW9NNZmddzX724bXeht+9COVLlmq1LqL/DmLa9r3JQ5JxhvUCHVYLEjE7ER7ab4psCwxVxkmslUPpa/7+fQ== + dependencies: + "@ppstreams/is-aborted" "^0.1.0" + "@ppstreams/is-end-of-stream" "^0.1.0" + "@ppstreams/propagate-abort" "^0.1.2" + "@ppstreams/propagate-peek" "^0.1.0" + "@validatem/core" "^0.3.11" + "@validatem/default-to" "^0.1.0" + "@validatem/is-function" "^0.1.0" + "@validatem/required" "^0.1.1" + "@validatem/wrap-value-as-option" "^0.1.0" + bluebird "^3.5.4" + +"@ppstreams/simple-source@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@ppstreams/simple-source/-/simple-source-0.1.1.tgz#a8f276f2b6a27632edc0f123dc64c183414c4f9e" + integrity sha512-q0jpPZSGmld9VIhM+GmMRqwE9PXRiEJSXf3jhypf5VqiEs1JfrSNENvFeLYL/OxaF5PV+Jh0Up1hRfBWVK4+Sw== + dependencies: + "@ppstreams/aborted" "^0.1.0" + "@validatem/core" "^0.3.12" + "@validatem/is-function" "^0.1.0" + "@validatem/required" "^0.1.1" + "@validatem/wrap-value-as-option" "^0.1.0" + bluebird "^3.7.2" + +"@validatem/annotate-errors@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@validatem/annotate-errors/-/annotate-errors-0.1.2.tgz#fa9152bb30f4f42b69496b527e38f0c31ff605a9" + integrity sha512-EuX7pzdYI/YpTmZcgdPG481Oi3elAg8JWh/LYXuE1h6MaZk3A8eP5DD33/l7EoKzrysn6y8nCsqNa1ngei562w== + dependencies: + "@validatem/match-validation-error" "^0.1.0" + +"@validatem/any-property@^0.1.0": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@validatem/any-property/-/any-property-0.1.3.tgz#fc7768c1922a8bacff9369ae48913672e5350f52" + integrity sha512-jYWxif5ff9pccu7566LIQ/4+snlApXEJUimBywzAriBgS3r4eDBbz3oZFHuiPmhxNK/NNof5YUS+L6Sk3zaMfg== + dependencies: + "@validatem/annotate-errors" "^0.1.2" + "@validatem/combinator" "^0.1.0" + "@validatem/error" "^1.0.0" + "@validatem/validation-result" "^0.1.1" + "@validatem/virtual-property" "^0.1.0" + default-value "^1.0.0" + +"@validatem/combinator@^0.1.0", "@validatem/combinator@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@validatem/combinator/-/combinator-0.1.2.tgz#eab893d55f1643b9c6857eaf6ff7ed2a728e89ff" + integrity sha512-vE8t1tNXknmN62FlN6LxQmA2c6TwVKZ+fl/Wit3H2unFdOhu7SZj2kRPGjAXdK/ARh/3svYfUBeD75pea0j1Sw== + +"@validatem/core@^0.3.11", "@validatem/core@^0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@validatem/core/-/core-0.3.12.tgz#e4e8a566850571bf55412862e88a3b06e75c8072" + integrity sha512-ngrFk6PT/pPZntpleG6q55SByongNxRk7wJhUiCihyv4yqIqqG+bNGH4wb6yW33IHefreWxkkJ53yM1Yj9srNA== + dependencies: + "@validatem/annotate-errors" "^0.1.2" + "@validatem/any-property" "^0.1.0" + "@validatem/error" "^1.0.0" + "@validatem/match-validation-error" "^0.1.0" + "@validatem/match-versioned-special" "^0.1.0" + "@validatem/match-virtual-property" "^0.1.0" + "@validatem/normalize-rules" "^0.1.0" + "@validatem/required" "^0.1.0" + "@validatem/validation-result" "^0.1.1" + "@validatem/virtual-property" "^0.1.0" + as-expression "^1.0.0" + assure-array "^1.0.0" + create-error "^0.3.1" + default-value "^1.0.0" + execall "^2.0.0" + flatten "^1.0.3" + indent-string "^4.0.0" + is-arguments "^1.0.4" + supports-color "^7.1.0" + syncpipe "^1.0.0" + +"@validatem/default-to@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/default-to/-/default-to-0.1.0.tgz#62766a3ca24d2f61a96c713bcb629a5b3c6427c5" + integrity sha512-UE/mJ6ZcHFlBLUhX75PQHDRYf80GFFhB+vZfIcsEWduh7Nm6lTMDnCPj4MI+jd9E/A7HV5D1yCZhaRSwoWo4vg== + dependencies: + is-callable "^1.1.5" + +"@validatem/either@^0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@validatem/either/-/either-0.1.9.tgz#0d753ef8fe04486d2b7122de3dd3ac51b3acaacf" + integrity sha512-cUqlRjy02qDcZ166/D6duk8lrtqrHynHuSakU0TvMGMBiLzjWpMJ+3beAWHe+kILB5/dlXVyc68ZIjSNhBi8Kw== + dependencies: + "@validatem/combinator" "^0.1.1" + "@validatem/error" "^1.0.0" + "@validatem/match-validation-error" "^0.1.0" + "@validatem/validation-result" "^0.1.2" + flatten "^1.0.3" + +"@validatem/error@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@validatem/error/-/error-1.1.0.tgz#bef46e7066c39761b494ebe3eec2ecdc7348f4ed" + integrity sha512-gZJEoZq1COi/8/5v0fVKQ9uX54x5lb5HbV7mzIOhY6dqjmLNfxdQmpECZPQrCAOpcRkRMJ7zaFhq4UTslpY9yA== + +"@validatem/has-shape@^0.1.0": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@validatem/has-shape/-/has-shape-0.1.8.tgz#dff0f0449c12b96d150091b7a980154d810ae63d" + integrity sha512-x2i8toW1uraFF2Vl6WBl4CScbBeg5alrtoCKMyXbJkHf2B5QxL/ftUh2RQRcBzx6U0i7KUb8vdShcWAa+fehRQ== + dependencies: + "@validatem/annotate-errors" "^0.1.2" + "@validatem/combinator" "^0.1.0" + "@validatem/error" "^1.0.0" + "@validatem/validation-result" "^0.1.1" + array-union "^2.1.0" + as-expression "^1.0.0" + assure-array "^1.0.0" + default-value "^1.0.0" + flatten "^1.0.3" + +"@validatem/is-function@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/is-function/-/is-function-0.1.0.tgz#15a2e95259dc5e32256e8c21872455661437d069" + integrity sha512-UtVrwTGhaIdIJ0mPG5XkAmYZUeWgRoMP1G9ZEHbKvAZJ4+SXf/prC0jPgE0pw+sPjdQG4hblsXSfo/9Bf3PGdQ== + dependencies: + "@validatem/error" "^1.0.0" + is-callable "^1.1.5" + +"@validatem/is-plain-object@^0.1.0", "@validatem/is-plain-object@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@validatem/is-plain-object/-/is-plain-object-0.1.1.tgz#b7a3ef8ef960882c7c41e84ed709fa0bfb932e93" + integrity sha512-aNGbNIbKRpYI0lRBczlTBbiA+nqN52ADAASdySKg2/QeSCVtYS4uOIeCNIJRAgXe/5sUnLTuL4pgq628uAl7Kw== + dependencies: + "@validatem/error" "^1.0.0" + is-plain-obj "^2.1.0" + +"@validatem/match-special@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/match-special/-/match-special-0.1.0.tgz#4e0c28f1aee5bf53c1ef30bbf8c755d4946ae0ff" + integrity sha512-TFiq9Wk/1Hoja4PK85WwNYnwBXk3+Lgoj59ZIMxm2an1qmNYp8j+BnSvkKBflba451yIn6V1laU9NJf+/NYZgw== + +"@validatem/match-validation-error@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/match-validation-error/-/match-validation-error-0.1.0.tgz#fa87f5f1836e7c1d9bf6b75b2addf0a5b21e4c1e" + integrity sha512-6akGTk7DdulOreyqDiGdikwRSixQz/AlvARSX18dcWaTFc79KxCLouL2hyoFcor9IIUhu5RTY4/i756y4T1yxA== + dependencies: + "@validatem/match-versioned-special" "^0.1.0" + +"@validatem/match-versioned-special@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/match-versioned-special/-/match-versioned-special-0.1.0.tgz#2eacc48debecdbbe7e3d02f0c0a665afaea9bedf" + integrity sha512-xoOTY0bdA2ELj+ntcDVJ8YyMEFIJpjZ4HNPL9lGcbnRFwJBhQcHUAhUpZwkMxu02zH9wkNM1FvYGHxPz40745Q== + +"@validatem/match-virtual-property@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/match-virtual-property/-/match-virtual-property-0.1.0.tgz#4de2de1075987b5f3b356d3f2bcf6c0be5b5fb83" + integrity sha512-ssd3coFgwbLuqvZftLZTy3eHN0TFST8oTS2XTViQdXJPXVoJmwEKBpFhXgwnb5Ly1CE037R/KWpjhd1TP/56kQ== + +"@validatem/normalize-rules@^0.1.0": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@validatem/normalize-rules/-/normalize-rules-0.1.3.tgz#59fd6193b1091ff97b5c723b32c9bb1fe2a9dc9c" + integrity sha512-HHPceAP2ce9NWymIZrgLCTzpdwXNRBCCB5H6ZPc5ggOrbmh4STpT83fLazleHtvYNlqgXZ4GjQOvCwrjaM+qEA== + dependencies: + "@validatem/has-shape" "^0.1.0" + "@validatem/is-plain-object" "^0.1.0" + "@validatem/match-special" "^0.1.0" + assure-array "^1.0.0" + default-value "^1.0.0" + flatten "^1.0.3" + is-plain-obj "^2.1.0" + +"@validatem/required@^0.1.0", "@validatem/required@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@validatem/required/-/required-0.1.1.tgz#64f4a87333fc5955511634036b7f8948ed269170" + integrity sha512-vI4NzLfay4RFAzp7xyU34PHb8sAo6w/3frrNh1EY9Xjnw2zxjY5oaxwmbFP1jVevBE6QQEnKogtzUHz/Zuvh6g== + +"@validatem/validation-result@^0.1.1", "@validatem/validation-result@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@validatem/validation-result/-/validation-result-0.1.2.tgz#4e75cfd87305fc78f8d05ac84921a2c99a0348e0" + integrity sha512-okmP8JarIwIgfpaVcvZGuQ1yOsLKT3Egt49Ynz6h1MAeGsP/bGHXkkXtbiWOVsk5Tzku5vDVFSrFnF+5IEHKxw== + dependencies: + default-value "^1.0.0" + +"@validatem/virtual-property@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/virtual-property/-/virtual-property-0.1.0.tgz#880540dfd149f98ecf1095d93912e34443381fe4" + integrity sha512-JUUvWtdqoSkOwlsl20oB3qFHYIL05a/TAfdY4AJcs55QeVTiX5iI1b8IoQW644sIWWooBuLv+XwoxjRsQFczlQ== + +"@validatem/wrap-value-as-option@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/wrap-value-as-option/-/wrap-value-as-option-0.1.0.tgz#57fa8d535f6cdf40cf8c8846ad45f4dd68f44568" + integrity sha512-gWDkfyU0DOsbinE9iqvRSJ+NxuynChyueJsC+AFm3EYbe8+s7V2gRs3qkJ4mq7hOlUbEh8tgCWQfZZvr+IdVFw== + dependencies: + "@validatem/either" "^0.1.9" + "@validatem/is-plain-object" "^0.1.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +as-expression@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/as-expression/-/as-expression-1.0.0.tgz#7bc620ca4cb2fe0ee90d86729bd6add33b8fd831" + integrity sha512-Iqh4GxNUfxbJdGn6b7/XMzc8m1Dz2ZHouBQ9DDTzyMRO3VPPIAXeoY/sucRxxxXKbUtzwzWZSN6jPR3zfpYHHA== + +assure-array@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assure-array/-/assure-array-1.0.0.tgz#4f4ad16a87659d6200a4fb7103462033d216ec1f" + integrity sha1-T0rRaodlnWIApPtxA0YgM9IW7B8= + +bluebird@^3.5.4, bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +clone-regexp@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" + integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q== + dependencies: + is-regexp "^2.0.0" + +create-error@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23" + integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM= + +default-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-value/-/default-value-1.0.0.tgz#8c6f52a5a1193fe78fdc9f86eb71d16c9757c83a" + integrity sha1-jG9SpaEZP+eP3J+G63HRbJdXyDo= + dependencies: + es6-promise-try "0.0.1" + +es6-promise-try@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/es6-promise-try/-/es6-promise-try-0.0.1.tgz#10f140dad27459cef949973e5d21a087f7274b20" + integrity sha1-EPFA2tJ0Wc75SZc+XSGgh/cnSyA= + +execall@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" + integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow== + dependencies: + clone-regexp "^2.1.0" + +flatten@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" + integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + +is-callable@^1.1.5: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-regexp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" + integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== + +split-filter@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/split-filter/-/split-filter-1.1.3.tgz#c68cc598783d88f60d16e7b452dacfe95ba60539" + integrity sha512-2xXwhWeJUFrYE8CL+qoy9mCohu5/E+uglvpqL1FVXz1XbvTwivafVC6oTDeg/9ksOAxg6DvyCF44Dvf5crFU0w== + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +syncpipe@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/syncpipe/-/syncpipe-1.0.0.tgz#170340f813150bc8fcb8878b1b9c71ea0ccd3727" + integrity sha512-cdiAFTnFJRvUaNPDc2n9CqoFvtIL3+JUMJZrC3kA3FzpugHOqu0TvkgNwmnxPZ5/WjAzMcfMS3xm+AO7rg/j/w== + dependencies: + assure-array "^1.0.0"