You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
3.8 KiB
JavaScript
121 lines
3.8 KiB
JavaScript
"use strict";
|
|
|
|
const Promise = require("bluebird");
|
|
const consumable = require("@joepie91/consumable");
|
|
const pPause = require("p-pause");
|
|
const debug = require("debug")("promistream:last-will");
|
|
|
|
const isEndOfStream = require("@promistream/is-end-of-stream");
|
|
const isAborted = require("@promistream/is-aborted");
|
|
|
|
const { validateOptions } = require("@validatem/core");
|
|
const isFunction = require("@validatem/is-function");
|
|
const wrapValueAsOption = require("@validatem/wrap-value-as-option");
|
|
const requireEither = require("@validatem/require-either");
|
|
|
|
let NoValue = Symbol();
|
|
|
|
// MARKER: Test this in batchOn, then finalize the refactoring
|
|
|
|
// FIXME: Make NoValue its own reusable promistream package? For use as a marker in different types of streams which need to deal with multiple possibly-matching handlers, or eg. map-filter
|
|
// FIXME: Re-understand and refactor the logic, finish packaging it up
|
|
module.exports = function lastWill() {
|
|
let { onEndOfStream, onAborted, onAllEnds } = validateOptions(arguments, [
|
|
wrapValueAsOption("onEndOfStream"), {
|
|
onEndOfStream: [ isFunction ],
|
|
onAborted: [ isFunction ],
|
|
onAllEnds: [ isFunction ],
|
|
},
|
|
requireEither([ "onEndOfStream", "onAborted", "onAllEnds" ], { allowMultiple: true })
|
|
]);
|
|
|
|
let errorBuffer = consumable();
|
|
let hasExecutedLastWill = false;
|
|
|
|
// NOTE: We use a resettable pauser to ensure that when reading in parallel mode, all concurrent reads get held up while a stream end marker is being processed, but parallelization is otherwise permitted. Think of it as a conditional `sequentialize`.
|
|
// FIXME: Verify that this doesn't ever introduce race conditions
|
|
let pauser = pPause();
|
|
|
|
function throwBufferedError() {
|
|
// Run after all custom handlers have run out
|
|
throw errorBuffer.consume();
|
|
}
|
|
|
|
function processUntilValue(handlers) {
|
|
return Promise.reduce(handlers, (lastResult, handler, i) => {
|
|
if (lastResult === NoValue) {
|
|
debug(`Calling handler ${i}`);
|
|
return handler(errorBuffer.peek());
|
|
} else {
|
|
debug(`Skipping handler ${i}`);
|
|
return lastResult;
|
|
}
|
|
}, NoValue);
|
|
}
|
|
|
|
function handleMarker(marker, markerHandler) {
|
|
pauser.pause();
|
|
|
|
return Promise.try(() => {
|
|
let relevantHandlers = [ markerHandler, onAllEnds, throwBufferedError ]
|
|
.filter((handler) => handler != null);
|
|
|
|
debug(`${relevantHandlers.length - 1} handlers to try, excluding default handler`);
|
|
|
|
if (hasExecutedLastWill || relevantHandlers.length === 0) {
|
|
debug("Bailing");
|
|
throw marker;
|
|
} else {
|
|
errorBuffer.set(marker);
|
|
hasExecutedLastWill = true;
|
|
|
|
return processUntilValue(relevantHandlers);
|
|
}
|
|
}).finally(() => {
|
|
pauser.unpause();
|
|
});
|
|
}
|
|
|
|
return {
|
|
_promistreamVersion: 0,
|
|
description: `last-will handler`,
|
|
abort: function abort_lastWill(source, reason) {
|
|
// FIXME: propagate module
|
|
return source.abort(reason);
|
|
},
|
|
peek: function peek_lastWill(source) {
|
|
return Promise.try(() => {
|
|
return pauser.await();
|
|
}).then(() => {
|
|
if (errorBuffer.isSet()) {
|
|
/* We've reached the end of the source stream in some way; we'll have at least *some* response ready, whether it is an error or some value produced by a last-will handler */
|
|
return true;
|
|
} else {
|
|
return source.peek();
|
|
}
|
|
});
|
|
},
|
|
read: function produceValue_lastWill(source) {
|
|
return Promise.try(() => {
|
|
return pauser.await();
|
|
}).then(() => {
|
|
if (errorBuffer.isSet()) {
|
|
return throwBufferedError();
|
|
} else {
|
|
return Promise.try(() => {
|
|
return source.read();
|
|
}).catch(isEndOfStream, (err) => {
|
|
debug("Got EndOfStream marker");
|
|
return handleMarker(err, onEndOfStream);
|
|
}).catch(isAborted, (err) => {
|
|
debug("Got Aborted marker");
|
|
return handleMarker(err, onAborted);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
module.exports.NoValue = NoValue;
|