0.3.0 Release

This commit is contained in:
Sami Turcotte 2018-11-25 20:00:37 -05:00
parent 5074a991fc
commit e1796d5896
14 changed files with 4079 additions and 91 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
.vscode .vscode
node_modules node_modules
**/*.js dist
**/*.js.map sample_output

View File

@ -1,5 +0,0 @@
.vscode
node_modules
**/*.ts
tsconfig.json
tslint.json

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"tabWidth": 4,
"trailingComma": "all"
}

View File

@ -1,6 +1,79 @@
# mysah # mysah
ES6 Promise library
## State of the project Promise, Stream and EventEmitter utils for Node.js
**mysah** is in its infancy. More development to come! ## Installation
```sh
yarn add mysah
```
## Basic Usage
```js
const stream = require("mysah/stream");
const { once, sleep } = require("mysah");
async function main() {
const collector = stream
.concat(
stream.fromArray(["a", "b", "c"]),
stream.fromArray(["d", "e"])
)
.pipe(stream.collect({ objectMode: true }));
const collected = await once(collector, "data");
console.log(collected); // [ 'a', 'b', 'c', 'd', 'e' ]
await sleep(1000); // Resolve after one second
}
main();
```
## API
### mysah/stream
```ts
/**
* Convert an array into a readable stream of its elements
* @param array The array of elements to stream
*/
export declare function fromArray(array: any[]): NodeJS.ReadableStream;
/**
* Return a ReadWrite stream that collects streamed objects or bytes into an array or buffer
* @param options
* @param options.objectMode Whether this stream should behave as a stream of objects
*/
export declare function collect({
objectMode,
}?: {
objectMode?: boolean | undefined;
}): NodeJS.ReadWriteStream;
/**
* Return a stream of readable streams concatenated together
* @param streams The readable streams to concatenate
*/
export declare function concat(
...streams: NodeJS.ReadableStream[]
): NodeJS.ReadableStream;
```
### mysah
```ts
/**
* Resolve after the given delay in milliseconds
*
* @param ms - The number of milliseconds to wait
*/
export declare function sleep(ms: number): Promise<{}>;
/**
* Resolve once the given event emitter emits the specified event
*
* @param emitter - The event emitter to watch
* @param event - The event to watch
*/
export declare function once<T>(emitter: NodeJS.EventEmitter, event: string): Promise<T>;
```

View File

@ -1,31 +0,0 @@
const es6Promisify = require('es6-promisify');
/**
* Return a promise that resolves once the given event emitter emits the specified event
*
* @param {NodeJS.EventEmitter} emitter - The event emitter to watch
* @param {string} event - The event to watch
* @returns {Promise<{}>} - The promise that resolves once the given emitter emits the specified evnet
*/
export function once(emitter: NodeJS.EventEmitter, event: string) {
return new Promise((resolve) => {
emitter.once(event, result => {
resolve(result);
});
});
}
/**
* Transform callback-based function -- func(arg1, arg2 .. argN, callback) -- into
* an ES6-compatible Promise. Promisify provides a default callback of the form (error, result)
* and rejects when `error` is truthy. You can also supply settings object as the second argument.
*
* @param {function} original - The function to promisify
* @param {object} [settings] - Settings object
* @param {object} settings.thisArg - A `this` context to use. If not set, assume `settings` _is_ `thisArg`
* @param {bool} settings.multiArgs - Should multiple arguments be returned as an array?
* @returns {function} A promisified version of `original`
*/
export function promisify(original: Function, settings?: Object): Function {
return es6Promisify(original, settings);
};

View File

@ -1,22 +1,60 @@
{ {
"name": "mysah", "name": "mysah",
"version": "0.1.0", "version": "0.3.0",
"description": "ES6 Promise library", "description": "Promise, Stream and EventEmitter utils for Node.js",
"main": "index.js", "keywords": [
"scripts": { "promise",
"test": "echo \"Error: no test specified\" && exit 1" "stream",
}, "event emitter",
"keywords": [ "utils"
"ES6", ],
"Promise", "author": {
"library" "name": "Sami Turcotte",
], "email": "samiturcotte@gmail.com"
"author": "Sami Turcotte", },
"license": "MIT", "license": "MIT",
"devDependencies": { "main": "index.js",
"@types/node": "^7.0.8" "types": "dist/**/*.d.ts",
}, "files": [
"dependencies": { "dist"
"es6-promisify": "^5.0.0" ],
} "repository": {
"url": "git@github.com:Wenzil/mysah.git",
"type": "git"
},
"scripts": {
"test": "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": "^10.12.10",
"ava": "^1.0.0-rc.2",
"chai": "^4.2.0",
"prettier": "^1.14.3",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.16.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.1.6"
},
"ava": {
"files": [
"src/**/*.spec.ts"
],
"sources": [
"src/**/*.ts"
],
"compileEnhancements": false,
"failWithoutAssertions": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register/transpile-only"
]
}
} }

21
samples/concat_files.js Normal file
View File

@ -0,0 +1,21 @@
const fs = require("fs");
const path = require("path");
const stream = require("mysah/stream");
const sourceFile1 = path.join(process.cwd(), "package.json");
const sourceFile2 = path.join(process.cwd(), "README.md");
const outputDir = path.join(process.cwd(), "sample_output");
const outputFile = path.join(outputDir, "concat_files.txt");
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Concat two source files together into one
stream
.concat(
fs.createReadStream(sourceFile1),
stream.fromArray(["\n"]),
fs.createReadStream(sourceFile2),
)
.pipe(fs.createWriteStream(outputFile));

29
src/index.spec.ts Normal file
View File

@ -0,0 +1,29 @@
import test from "ava";
import { expect } from "chai";
import { once, sleep } from "./";
import { EventEmitter } from "events";
const TimingErrorMarginMs = 50;
test("sleep() resolves after the specified delay in milliseconds", async t => {
const before = Date.now();
await sleep(200);
const after = Date.now();
expect(after - before).gte(200);
expect(after - before).closeTo(200, TimingErrorMarginMs);
});
test("once() resolves only after the specified event is emitted", async t => {
const emitter = new EventEmitter();
const before = Date.now();
emitter.emit("noise", "is ignored");
setTimeout(() => emitter.emit("done", "some-result"), 200);
const result = await once(emitter, "done");
const after = Date.now();
expect(result).to.equal("some-result");
expect(after - before).gte(200);
expect(after - before).closeTo(200, TimingErrorMarginMs);
});

27
src/index.ts Normal file
View File

@ -0,0 +1,27 @@
/**
* Resolve after the given delay in milliseconds
*
* @param ms - The number of milliseconds to wait
*/
export function sleep(ms: number) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
/**
* Resolve once the given event emitter emits the specified event
*
* @param emitter - The event emitter to watch
* @param event - The event to watch
*/
export function once<T>(
emitter: NodeJS.EventEmitter,
event: string,
): Promise<T> {
return new Promise(resolve => {
emitter.once(event, result => {
resolve(result);
});
});
}

350
src/stream.spec.ts Normal file
View File

@ -0,0 +1,350 @@
import test from "ava";
import { expect } from "chai";
import { fromArray, collect, concat } from "./stream";
import { Readable } from "stream";
test.cb("fromArray() streams array elements in flowing mode", t => {
t.plan(3);
const elements = ["a", "b", "c"];
const stream = fromArray(elements);
let i = 0;
stream
.on("data", element => {
expect(element).to.equal(elements[i]);
t.pass();
i++;
})
.on("error", t.end)
.on("end", t.end);
});
test.cb("fromArray() streams array elements in paused mode", t => {
t.plan(3);
const elements = ["a", "b", "c"];
const stream = fromArray(elements);
let i = 0;
stream
.on("readable", () => {
let element = stream.read();
while (element !== null) {
expect(element).to.equal(elements[i]);
t.pass();
i++;
element = stream.read();
}
})
.on("error", t.end)
.on("end", t.end);
});
test.cb("fromArray() ends immediately if there are no array elements", t => {
t.plan(0);
fromArray([])
.on("data", () => t.fail())
.on("error", t.end)
.on("end", t.end);
});
test.cb(
"collect() collects streamed elements into an array (object, flowing mode)",
t => {
t.plan(1);
const source = new Readable({ objectMode: true });
source
.pipe(collect({ objectMode: true }))
.on("data", collected => {
expect(collected).to.deep.equal(["a", "b", "c"]);
t.pass();
})
.on("error", t.end)
.on("end", t.end);
source.push("a");
source.push("b");
source.push("c");
source.push(null);
},
);
test.cb(
"collect() collects streamed elements into an array (object, paused mode)",
t => {
t.plan(1);
const source = new Readable({ objectMode: true });
const collector = source.pipe(collect({ objectMode: true }));
collector
.on("readable", () => {
let collected = collector.read();
while (collected !== null) {
expect(collected).to.deep.equal(["a", "b", "c"]);
t.pass();
collected = collector.read();
}
})
.on("error", t.end)
.on("end", t.end);
source.push("a");
source.push("b");
source.push("c");
source.push(null);
},
);
test.cb(
"collect() collects streamed bytes into a buffer (non-object, flowing mode)",
t => {
t.plan(1);
const source = new Readable({ objectMode: false });
source
.pipe(collect())
.on("data", collected => {
expect(collected).to.deep.equal(Buffer.from("abc"));
t.pass();
})
.on("error", t.end)
.on("end", t.end);
source.push("a");
source.push("b");
source.push("c");
source.push(null);
},
);
test.cb(
"collect() collects streamed bytes into a buffer (non-object, paused mode)",
t => {
t.plan(1);
const source = new Readable({ objectMode: false });
const collector = source.pipe(collect({ objectMode: false }));
collector
.on("readable", () => {
let collected = collector.read();
while (collected !== null) {
expect(collected).to.deep.equal(Buffer.from("abc"));
t.pass();
collected = collector.read();
}
})
.on("error", t.end)
.on("end", t.end);
source.push("a");
source.push("b");
source.push("c");
source.push(null);
},
);
test.cb(
"collect() emits an empty array if the source was empty (object mode)",
t => {
t.plan(1);
const source = new Readable({ objectMode: true });
const collector = source.pipe(collect({ objectMode: true }));
collector
.on("data", collected => {
expect(collected).to.deep.equal([]);
t.pass();
})
.on("error", t.end)
.on("end", t.end);
source.push(null);
},
);
test.cb(
"collect() emits nothing if the source was empty (non-object mode)",
t => {
t.plan(0);
const source = new Readable({ objectMode: false });
const collector = source.pipe(collect({ objectMode: false }));
collector
.on("data", () => t.fail())
.on("error", t.end)
.on("end", t.end);
source.push(null);
},
);
test.cb(
"concat() concatenates multiple readable streams (object, flowing mode)",
t => {
t.plan(6);
const source1 = new Readable({ objectMode: true });
const source2 = new Readable({ objectMode: true });
const expectedElements = ["a", "b", "c", "d", "e", "f"];
let i = 0;
concat(source1, source2)
.on("data", element => {
expect(element).to.equal(expectedElements[i]);
t.pass();
i++;
})
.on("error", t.end)
.on("end", t.end);
source1.push("a");
source2.push("d");
source1.push("b");
source2.push("e");
source1.push("c");
source2.push("f");
source2.push(null);
source1.push(null);
},
);
test.cb(
"concat() concatenates multiple readable streams (object, paused mode)",
t => {
t.plan(6);
const source1 = new Readable({ objectMode: true });
const source2 = new Readable({ objectMode: true });
const expectedElements = ["a", "b", "c", "d", "e", "f"];
let i = 0;
const concatenation = concat(source1, source2)
.on("readable", () => {
let element = concatenation.read();
while (element !== null) {
expect(element).to.equal(expectedElements[i]);
t.pass();
i++;
element = concatenation.read();
}
})
.on("error", t.end)
.on("end", t.end);
source1.push("a");
source2.push("d");
source1.push("b");
source2.push("e");
source1.push("c");
source2.push("f");
source2.push(null);
source1.push(null);
},
);
test.cb(
"concat() concatenates multiple readable streams (non-object, flowing mode)",
t => {
t.plan(6);
const source1 = new Readable({ objectMode: false });
const source2 = new Readable({ objectMode: false });
const expectedElements = ["a", "b", "c", "d", "e", "f"];
let i = 0;
concat(source1, source2)
.on("data", element => {
expect(element).to.deep.equal(Buffer.from(expectedElements[i]));
t.pass();
i++;
})
.on("error", t.end)
.on("end", t.end);
source1.push("a");
source2.push("d");
source1.push("b");
source2.push("e");
source1.push("c");
source2.push("f");
source2.push(null);
source1.push(null);
},
);
test.cb(
"concat() concatenates multiple readable streams (non-object, paused mode)",
t => {
t.plan(6);
const source1 = new Readable({ objectMode: false });
const source2 = new Readable({ objectMode: false });
const expectedElements = ["a", "b", "c", "d", "e", "f"];
let i = 0;
const concatenation = concat(source1, source2)
.on("readable", () => {
let element = concatenation.read();
while (element !== null) {
expect(element).to.deep.equal(
Buffer.from(expectedElements[i]),
);
t.pass();
i++;
element = concatenation.read();
}
})
.on("error", t.end)
.on("end", t.end);
source1.push("a");
source2.push("d");
source1.push("b");
source2.push("e");
source1.push("c");
source2.push("f");
source2.push(null);
source1.push(null);
},
);
test.cb("concat() concatenates a single readable stream (object mode)", t => {
t.plan(3);
const source = new Readable({ objectMode: true });
const expectedElements = ["a", "b", "c", "d", "e", "f"];
let i = 0;
concat(source)
.on("data", element => {
expect(element).to.equal(expectedElements[i]);
t.pass();
i++;
})
.on("error", t.end)
.on("end", t.end);
source.push("a");
source.push("b");
source.push("c");
source.push(null);
});
test.cb(
"concat() concatenates a single readable stream (non-object mode)",
t => {
t.plan(3);
const source = new Readable({ objectMode: false });
const expectedElements = ["a", "b", "c", "d", "e", "f"];
let i = 0;
concat(source)
.on("data", element => {
expect(element).to.deep.equal(Buffer.from(expectedElements[i]));
t.pass();
i++;
})
.on("error", t.end)
.on("end", t.end);
source.push("a");
source.push("b");
source.push("c");
source.push(null);
},
);
test.cb("concat() concatenates empty list of readable streams", t => {
t.plan(0);
concat()
.pipe(collect())
.on("data", _ => {
t.fail();
})
.on("error", t.end)
.on("end", t.end);
});

83
src/stream.ts Normal file
View File

@ -0,0 +1,83 @@
import { Transform, Readable } from "stream";
/**
* Convert an array into a readable stream of its elements
* @param array The array of elements to stream
*/
export function fromArray(array: any[]): NodeJS.ReadableStream {
let cursor = 0;
return new Readable({
objectMode: true,
read() {
if (cursor < array.length) {
this.push(array[cursor]);
cursor++;
} else {
this.push(null);
}
},
});
}
/**
* Return a ReadWrite stream that collects streamed objects or bytes into an array or buffer
* @param options
* @param options.objectMode Whether this stream should behave as a stream of objects
*/
export function collect({ objectMode = false } = {}): NodeJS.ReadWriteStream {
const collected: any[] = [];
return new Transform({
readableObjectMode: objectMode,
writableObjectMode: objectMode,
transform(data, encoding, callback) {
collected.push(data);
callback();
},
flush(callback) {
this.push(objectMode ? collected : Buffer.concat(collected));
callback();
},
});
}
/**
* Return a stream of readable streams concatenated together
* @param streams The readable streams to concatenate
*/
export function concat(
...streams: NodeJS.ReadableStream[]
): NodeJS.ReadableStream {
let isStarted = false;
let currentStreamIndex = 0;
const startCurrentStream = () => {
if (currentStreamIndex >= streams.length) {
wrapper.push(null);
} else {
streams[currentStreamIndex]
.on("data", chunk => {
if (!wrapper.push(chunk)) {
streams[currentStreamIndex].pause();
}
})
.on("error", err => wrapper.emit("error", err))
.on("end", () => {
currentStreamIndex++;
startCurrentStream();
});
}
};
const wrapper = new Readable({
objectMode: true,
read() {
if (!isStarted) {
isStarted = true;
startCurrentStream();
}
if (currentStreamIndex < streams.length) {
streams[currentStreamIndex].resume();
}
},
});
return wrapper;
}

View File

@ -1,13 +1,18 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "noImplicitAny": true,
"module": "commonjs", "strictNullChecks": true,
"sourceMap": true, "noImplicitReturns": true,
"watch": false, "noUnusedLocals": true,
"noUnusedParameters": true, "noImplicitThis": true,
"noUnusedLocals": true "forceConsistentCasingInFileNames": true,
}, "suppressImplicitAnyIndexErrors": true,
"include": [ "outDir": "./dist",
"**/*.ts" "module": "commonjs",
] "target": "es5",
} "lib": ["es2017"],
"sourceMap": true,
"declaration": true
},
"include": ["src/**/*.ts"]
}

View File

@ -1,19 +1,13 @@
{ {
"rules": { "extends": [
"no-unused-expression": true, "tslint:latest",
"no-duplicate-variable": true, "tslint-plugin-prettier",
"curly": true, "tslint-config-prettier"
"class-name": true, ],
"semicolon": [true, "always"], "rules": {
"triple-equals": true, "no-console": false,
"trailing-comma": [true, {"multiline": "never", "singleline": "never"}], "no-implicit-dependencies": [true, ["ava"]],
"only-arrow-functions": [true, "allow-declarations"], "prettier": true,
"no-eval": true, "ordered-imports": false
"no-invalid-this": true, }
"switch-default": true, }
"prefer-const": true,
"arrow-return-shorthand": [true],
"jsdoc-format": true,
"no-consecutive-blank-lines": [true]
}
}

3400
yarn.lock Normal file

File diff suppressed because it is too large Load Diff