Initial commit
commit
2abd52f919
@ -0,0 +1 @@
|
||||
node_modules
|
@ -0,0 +1,69 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
|
||||
const pipe = require("@promistream/pipe");
|
||||
const map = require("@promistream/map");
|
||||
const spy = require("@promistream/spy");
|
||||
const collect = require("@promistream/collect");
|
||||
const fromIterable = require("@promistream/from-iterable");
|
||||
const forkMirror = require("./");
|
||||
|
||||
return Promise.try(() => {
|
||||
let [ streamA, streamB ] = pipe([
|
||||
fromIterable([ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k" ]),
|
||||
forkMirror(2, { bufferSize: Infinity, leaderMode: false }) // NOTE: Change `leaderMode` to see the output order change
|
||||
]).read();
|
||||
|
||||
return Promise.all([
|
||||
pipe([
|
||||
streamA,
|
||||
map((string) => {
|
||||
return Promise
|
||||
.resolve(`[stream A] ${string}`)
|
||||
.delay(20); // Emulate some slow processing step
|
||||
}),
|
||||
spy((value) => console.log(value)),
|
||||
collect()
|
||||
]).read(),
|
||||
pipe([
|
||||
streamB,
|
||||
map((string) => `[stream B] ${string}`),
|
||||
spy((value) => console.log(value)),
|
||||
collect()
|
||||
]).read(),
|
||||
]);
|
||||
}).then((results) => {
|
||||
console.log(results);
|
||||
|
||||
/*
|
||||
[
|
||||
[
|
||||
'[stream A] a',
|
||||
'[stream A] b',
|
||||
'[stream A] c',
|
||||
'[stream A] d',
|
||||
'[stream A] e',
|
||||
'[stream A] f',
|
||||
'[stream A] g',
|
||||
'[stream A] h',
|
||||
'[stream A] i',
|
||||
'[stream A] j',
|
||||
'[stream A] k'
|
||||
],
|
||||
[
|
||||
'[stream B] a',
|
||||
'[stream B] b',
|
||||
'[stream B] c',
|
||||
'[stream B] d',
|
||||
'[stream B] e',
|
||||
'[stream B] f',
|
||||
'[stream B] g',
|
||||
'[stream B] h',
|
||||
'[stream B] i',
|
||||
'[stream B] j',
|
||||
'[stream B] k'
|
||||
]
|
||||
]
|
||||
*/
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
|
||||
const range = require("range").range;
|
||||
|
||||
const derivedStream = require("@promistream/derived-stream");
|
||||
|
||||
const { validateArguments } = require("@validatem/core");
|
||||
const required = require("@validatem/required");
|
||||
const isInteger = require("@validatem/is-integer");
|
||||
const isPositive = require("@validatem/is-positive");
|
||||
const defaultTo = require("@validatem/default-to");
|
||||
const either = require("@validatem/either");
|
||||
const isValue = require("@validatem/is-value");
|
||||
const isBoolean = require("@validatem/is-boolean");
|
||||
|
||||
const mirroredBuffer = require("./mirrored-buffer");
|
||||
const notifiedWhen = require("./notified-when");
|
||||
|
||||
// FIXME: Ensure that the consume-peek-on-read model makes coherent sense
|
||||
|
||||
let positiveOrInfinity = [
|
||||
either([ isInteger, isValue(Infinity) ]),
|
||||
isPositive
|
||||
];
|
||||
|
||||
module.exports = function createMirrorFork(_streamCount, _options) {
|
||||
let [ streamCount, options ] = validateArguments(arguments, {
|
||||
streamCount: [ required, isInteger, isPositive ],
|
||||
options: [ defaultTo({}), {
|
||||
bufferSize: [ required, positiveOrInfinity ],
|
||||
peekBufferSize: [ defaultTo(Infinity), positiveOrInfinity ],
|
||||
leaderMode: [ defaultTo(false), isBoolean ]
|
||||
}]
|
||||
});
|
||||
|
||||
let peekBuffer = mirroredBuffer(streamCount, options.leaderMode);
|
||||
let readBuffer = mirroredBuffer(streamCount, options.leaderMode);
|
||||
let when = notifiedWhen();
|
||||
|
||||
// FIXME: Label as 'Mirrored stream fork'
|
||||
return derivedStream((source) => {
|
||||
return range(0, streamCount).map((streamIndex) => {
|
||||
return {
|
||||
_promistreamVersion: 0,
|
||||
_promistreamIsSource: true,
|
||||
description: `Mirrored stream fork (${streamIndex})`,
|
||||
abort: function (reason) {
|
||||
return source.abort(reason);
|
||||
},
|
||||
peek: function () {
|
||||
return when(() => peekBuffer.getBiggestBufferLength() < options.peekBufferSize, () => {
|
||||
let resultPromise = peekBuffer.getItem(streamIndex, () => source.peek());
|
||||
|
||||
when.notify();
|
||||
|
||||
return resultPromise;
|
||||
});
|
||||
},
|
||||
read: function () {
|
||||
return when(() => readBuffer.getBiggestBufferLength() < options.bufferSize, () => {
|
||||
let resultPromise = readBuffer.getItem(streamIndex, () => source.read());
|
||||
|
||||
// Throw away the peek corresponding to this read, if there are any
|
||||
peekBuffer.maybeDiscardItem(streamIndex);
|
||||
|
||||
when.notify();
|
||||
|
||||
return resultPromise;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
const range = require("range").range;
|
||||
const asExpression = require("as-expression");
|
||||
const pDefer = require("p-defer");
|
||||
|
||||
// FIXME: Move into separate package
|
||||
|
||||
module.exports = function createMirroredBuffer(slots, leaderMode = false) {
|
||||
let buffers = range(0, slots).map(() => []);
|
||||
let bufferLengths = range(0, slots).map(() => []);
|
||||
|
||||
return {
|
||||
getItem: function (slotIndex, callback) {
|
||||
let slotBuffer = buffers[slotIndex];
|
||||
|
||||
if (slotBuffer.length === 0) {
|
||||
let value = asExpression(() => {
|
||||
if (leaderMode) {
|
||||
// NOTE: In leader mode, we don't immediately start the operation, but instead store a placeholder defer. Then whenever the stream with index 0 picks out the item, *it* will start the operation (see further down below) and attach its promise to the existing defer, thereby resolving it for every stream.
|
||||
let { promise, resolve } = pDefer();
|
||||
|
||||
return {
|
||||
promise: promise,
|
||||
resolve: resolve
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
promise: callback(),
|
||||
resolve: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
buffers.forEach((buffer, i) => {
|
||||
buffer.push(value);
|
||||
bufferLengths[i] += 1;
|
||||
});
|
||||
}
|
||||
|
||||
bufferLengths[slotIndex] -= 1;
|
||||
let nextItem = slotBuffer.shift();
|
||||
|
||||
if (leaderMode && slotIndex === 0) {
|
||||
nextItem.resolve(callback());
|
||||
}
|
||||
|
||||
return nextItem.promise;
|
||||
},
|
||||
// FIXME: Add note to README asking anyone who needs a strict discardItem, to file an issue to that effect
|
||||
maybeDiscardItem: function (slotIndex) {
|
||||
let slotBuffer = buffers[slotIndex];
|
||||
|
||||
if (slotBuffer.length > 0) {
|
||||
slotBuffer.shift();
|
||||
bufferLengths[slotIndex] -= 1;
|
||||
}
|
||||
},
|
||||
getBiggestBufferLength: function () {
|
||||
// TODO: Pre-calculate this on changes instead? Using bufferLengths. Figure out how to make that work with external buffer changes...
|
||||
return Math.max(... buffers.map((buffer) => buffer.length));
|
||||
}
|
||||
};
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
const pDefer = require("p-defer");
|
||||
|
||||
// FIXME: Move into separate package, and see if this can be used in other stream implementations too
|
||||
|
||||
module.exports = function createPromiseNotifier() {
|
||||
let promise, resolve;
|
||||
|
||||
return {
|
||||
wait: function () {
|
||||
if (promise == null) {
|
||||
let defer = pDefer();
|
||||
promise = defer.promise;
|
||||
resolve = defer.resolve;
|
||||
}
|
||||
|
||||
return promise;
|
||||
},
|
||||
notify: function (value) {
|
||||
if (resolve != null) {
|
||||
resolve(value);
|
||||
|
||||
promise = null;
|
||||
resolve = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const notifiedPromise = require("./notified-promise");
|
||||
|
||||
// FIXME: Move into separate package
|
||||
|
||||
module.exports = function createNotifiedWhen() {
|
||||
let notifier = notifiedPromise();
|
||||
|
||||
function when(condition, callback) {
|
||||
if (condition()) {
|
||||
return callback();
|
||||
} else {
|
||||
return Promise.try(() => {
|
||||
return notifier.wait();
|
||||
}).then(() => {
|
||||
return when(condition, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
when.notify = function notifyWhen() {
|
||||
return notifier.notify();
|
||||
};
|
||||
|
||||
return when;
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@promistream/fork-mirror",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"keywords": [
|
||||
"promistream"
|
||||
],
|
||||
"repository": "http://git.cryto.net/promistream/fork-mirror.git",
|
||||
"author": "Sven Slootweg <admin@cryto.net>",
|
||||
"license": "WTFPL OR CC0-1.0",
|
||||
"devDependencies": {
|
||||
"@joepie91/eslint-config": "^1.1.0",
|
||||
"@promistream/collect": "^0.1.0",
|
||||
"@promistream/from-iterable": "^0.1.0",
|
||||
"@promistream/map": "^0.1.0",
|
||||
"@promistream/pipe": "^0.1.1",
|
||||
"@promistream/spy": "^0.1.0",
|
||||
"eslint": "^6.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@promistream/derived-stream": "^0.1.0",
|
||||
"@validatem/core": "^0.3.15",
|
||||
"@validatem/default-to": "^0.1.0",
|
||||
"@validatem/either": "^0.1.9",
|
||||
"@validatem/is-boolean": "^0.1.1",
|
||||
"@validatem/is-integer": "^0.1.0",
|
||||
"@validatem/is-positive": "^1.0.0",
|
||||
"@validatem/is-value": "^0.1.0",
|
||||
"@validatem/required": "^0.1.1",
|
||||
"as-expression": "^1.0.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"p-defer": "3",
|
||||
"range": "^0.0.3"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue