ts-stream
Version:
Type-safe object streams with seamless support for backpressure, ending, and error handling
298 lines • 13.8 kB
JavaScript
/**
* 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
;