UNPKG

ts-stream

Version:

Type-safe object streams with seamless support for backpressure, ending, and error handling

298 lines 13.8 kB
"use strict"; /** * Promise-based object stream with seamless support for back-pressure and error * handling, written in Typescript. * * Copyright (C) 2015 Martin Poelstra * License: MIT */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.batch = exports.filter = exports.map = exports.compose = void 0; var Stream_1 = require("./Stream"); var util_1 = require("./util"); function compose(t1, t2) { return function (readable, writable) { var stream = new Stream_1.Stream(); t1(readable, stream); t2(stream, writable); }; } exports.compose = compose; // Return an ender callback that first runs an optional user-supplied ender, // followed by the default ender that always ends the stream. // It's refactored out, because it's currently a bit tricky and exact behavior // may change, see TODO in implementation. function composeEnders(ender, defaultEnder) { if (!ender) { return defaultEnder; } return function (error) { // TODO: an error returned from ender is currently passed on to next // stream, if stream was not ended with an error yet. // It'd maybe be better to not have the next stream be ended when an // error occurred in this ender, but there's no way to send another // end(), so we have to close it somehow... return Promise.resolve(error) .then(ender) .then(function () { return defaultEnder(error); }, function (enderError) { // ender callback failed, but in order to let final stream fail, // we need to pass 'something' on, and to wait for that to come // back. // Finally, make sure to return the enderError. return Promise.resolve(defaultEnder(error !== null && error !== void 0 ? error : enderError)).then(function () { return Promise.reject(enderError); }, function () { return Promise.reject(enderError); }); }); }; } function map(readable, writable, mapper, ender, aborter) { writable.aborted().catch(function (err) { return readable.abort(err); }); readable.aborted().catch(function (err) { return writable.abort(err); }); readable.forEach(function (v) { return writable.write(mapper(v)); }, composeEnders(ender, function (error) { return writable.end(error, readable.result()); }), aborter); } exports.map = map; function filter(readable, writable, filterer, ender, aborter) { writable.aborted().catch(function (err) { return readable.abort(err); }); readable.aborted().catch(function (err) { return writable.abort(err); }); readable.forEach(function (v) { var b = filterer(v); if (!b) { return; } else if (b === true) { // note: not just `if (b)`! return writable.write(v); } else { // more complex return type, probably a PromiseLike return Promise.resolve(b).then(function (resolvedB) { if (resolvedB) { return writable.write(v); } }); } }, composeEnders(ender, function (error) { return writable.end(error, readable.result()); }), aborter); } exports.filter = filter; function batch(readable, writable, maxBatchSize, _a) { var _this = this; var _b = _a === void 0 ? {} : _a, _c = _b.minBatchSize, minBatchSize = _c === void 0 ? maxBatchSize : _c, flushTimeout = _b.flushTimeout, handleError = _b.handleError; writable.aborted().catch(function (err) { return readable.abort(err); }); readable.aborted().catch(function (err) { return writable.abort(err); }); var queue = []; var pendingWrite; var timeout; function flush() { return __awaiter(this, void 0, void 0, function () { var peeled_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!queue.length) return [3 /*break*/, 2]; peeled_1 = queue; queue = []; return [4 /*yield*/, writable .write(peeled_1) .catch(handleError && (function (e) { return handleError(e, peeled_1); }))]; case 1: _a.sent(); _a.label = 2; case 2: return [2 /*return*/]; } }); }); } function earlyFlush() { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (!(queue.length >= minBatchSize && queue.length < maxBatchSize)) return [3 /*break*/, 2]; pendingWrite = util_1.track(flush()); util_1.swallowErrors(pendingWrite.promise); return [4 /*yield*/, pendingWrite.promise]; case 1: _a.sent(); return [3 /*break*/, 0]; case 2: // Won't be reached if the above throws, leaving the error to be handled // by forEach() or end() pendingWrite = undefined; return [2 /*return*/]; } }); }); } function clearFlushTimeout() { if (timeout !== undefined) { clearTimeout(timeout); timeout = undefined; } } function startFlushTimeout() { if (typeof flushTimeout === "number") { clearFlushTimeout(); timeout = setTimeout(function () { if (!(pendingWrite === null || pendingWrite === void 0 ? void 0 : pendingWrite.isPending) && queue.length > 0) { // NOTE If a normal flush() operation is in progress when this // fires, this will slightly pressure the downstream reader. // We could prevent this by tracking the promise of a normal // flush. pendingWrite = util_1.track(flush()); util_1.swallowErrors(pendingWrite.promise); } }, flushTimeout); } } function consumeEarlyFlushError() { if (pendingWrite === null || pendingWrite === void 0 ? void 0 : pendingWrite.isRejected) { var reason = pendingWrite.reason; pendingWrite = undefined; return reason; } } function settleEarlyFlush() { return __awaiter(this, void 0, void 0, function () { var e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, (pendingWrite === null || pendingWrite === void 0 ? void 0 : pendingWrite.promise)]; case 1: _a.sent(); return [3 /*break*/, 3]; case 2: e_1 = _a.sent(); return [2 /*return*/, consumeEarlyFlushError()]; case 3: return [2 /*return*/]; } }); }); } function throwIfThrowable(e) { if (e) { throw e; } } readable.forEach(function (v) { return __awaiter(_this, void 0, void 0, function () { var flushFailureError, earlyFlushError, e_2, toThrow; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: startFlushTimeout(); queue.push(v); if (!(queue.length >= maxBatchSize)) return [3 /*break*/, 6]; _b.label = 1; case 1: _b.trys.push([1, 4, , 5]); return [4 /*yield*/, settleEarlyFlush()]; case 2: // backpressure earlyFlushError = _b.sent(); return [4 /*yield*/, flush()]; case 3: _b.sent(); return [3 /*break*/, 5]; case 4: e_2 = _b.sent(); flushFailureError = e_2; return [3 /*break*/, 5]; case 5: return [3 /*break*/, 7]; case 6: if (!pendingWrite) { // no backpressure yet (until new queue fills to maxBatchSize) util_1.swallowErrors(earlyFlush()); } _b.label = 7; case 7: toThrow = (_a = earlyFlushError !== null && earlyFlushError !== void 0 ? earlyFlushError : // If there was an error in earlyFlush() and we weren't awaiting it, try to capture it here. // If we didn't do this, the error would be captured anyway in the next call to write() // or end(), but it's better to get it earlier if we can. consumeEarlyFlushError()) !== null && _a !== void 0 ? _a : // Default to any error that occurred in the normal, backpressured flush(). // NOTE: Errors from earlyFlush() will shadow this one. If this is a concern for the caller, // they can instead use the handleError() parameter, which is guaranteed to run on every error. flushFailureError; throwIfThrowable(toThrow); return [2 /*return*/]; } }); }); }, function (error) { return __awaiter(_this, void 0, void 0, function () { var flushError, earlyFlushError, e_3, toThrow; return __generator(this, function (_a) { switch (_a.label) { case 0: clearFlushTimeout(); return [4 /*yield*/, settleEarlyFlush()]; case 1: earlyFlushError = _a.sent(); _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, flush()]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: e_3 = _a.sent(); flushError = e_3; return [3 /*break*/, 5]; case 5: toThrow = earlyFlushError !== null && earlyFlushError !== void 0 ? earlyFlushError : flushError; return [4 /*yield*/, writable.end(error, readable.result())]; case 6: _a.sent(); throwIfThrowable(toThrow); return [2 /*return*/]; } }); }); }, function () { if (!pendingWrite) { // Trigger write errors on what is already in the queue, as early as possible util_1.swallowErrors(earlyFlush()); } }); } exports.batch = batch; //# sourceMappingURL=Transform.js.map