Javascript 如何交错流(带背压)
假设我有两条可能无限的流:Javascript 如何交错流(带背压),javascript,rxjs,bacon.js,Javascript,Rxjs,Bacon.js,假设我有两条可能无限的流: s1 = a..b..c..d..e... s2 = 1.2.3.4.5.6.7... 我想合并这些流,然后将合并的流映射到slowish异步操作(例如,在Bacon中使用fromPromise和flatMapConcat) 我可以将它们与合并: me = a12b3.c45d6.7e... 然后是地图 s1 = a..b..c..d..e... s2 = 1.2.3.4.5.6.7... me = a12b3.c45d6.7e... mm = a..1..2..
s1 = a..b..c..d..e...
s2 = 1.2.3.4.5.6.7...
我想合并这些流,然后将合并的流映射到slowish异步操作(例如,在Bacon中使用fromPromise
和flatMapConcat
)
我可以将它们与合并:
me = a12b3.c45d6.7e...
然后是地图
s1 = a..b..c..d..e...
s2 = 1.2.3.4.5.6.7...
me = a12b3.c45d6.7e...
mm = a..1..2..b..3..c..4..5..
正如您所看到的,从长远来看,greediers2
streams具有优势这是不受欢迎的行为
合并行为是不好的,因为我想有一些背压,以有更多的交错,“公平”,“循环”合并。期望的行为的几个例子:
s1 = a.....b..............c...
s2 = ..1.2.3..................
mm = a...1...b...2...3....c...
s1 = a.........b..........c...
s2 = ..1.2.3..................
mm = a...1...2...b...3....c...
一种想法是s1
和s2
将任务发送给工人,而工人一次只能处理一项任务。使用merge
和flatMapConcat
我会得到一个贪婪的任务管理器,但我想要一个更公平的
我想找到一个简单而优雅的解决方案。如果它很容易推广到任意数量的流,那就太好了:
// roundRobinPromiseMap(streams: [Stream a], f: a -> Promise b): Stream b
var mm = roundRobinPromiseMap([s1, s2], slowAsyncFunc);
使用RxJS或其他Rx库的解决方案也不错
澄清
不是zipAsArray
我不想:
function roundRobinPromiseMap(streams, f) {
return Bacon.zipAsArray.apply(null, streams)
.flatMap(Bacon.fromArray)
.flatMapConcat(function (x) {
return Bacon.fromPromise(f(x));
});
}
比较示例大理石图:
s1 = a.....b..............c.......
s2 = ..1.2.3......................
mm = a...1...b...2...3....c....... // wanted
zip = a...1...b...2........c...3... // zipAsArray based
是的,我会遇到缓冲问题
。。。但我也会这么说:
大理石图
s1 = a.........b..........c...
s2 = ..1.2.3..................
mm = a...1...2...b...3....c...
merge = a...1...2...3...b....c...
这里有一段疯狂的代码可能会有所帮助
它将输入流转换为单个“值”事件流,然后将它们与“发送”事件(以及用于记账的“结束”事件)合并。然后,使用状态机,它在“值”事件之外建立队列,并在“发送”事件上分派值
起初我写了一篇文章,但我把它改成了一篇要点
这是一张非常相似的圆形RobinPromiseMap。gist中的代码已经过测试,但这不是
# roundRobinPromiseMap :: (a -> Promise b) -> [EventStream] -> EventStream
roundRobinPromiseMap = (promiser, streams) ->
# A bus to trigger new sends based on promise fulfillment
promiseFulfilled = new Bacon.Bus()
# Merge the input streams into a single, keyed stream
theStream = Bacon.mergeAll(streams.map((s, idx) ->
s.map((val) -> {
type: 'value'
index: idx
value: val
})
))
# Merge in 'end' events
.merge(Bacon.mergeAll(streams.map((s) ->
s.mapEnd(-> {
type: 'end'
})
)))
# Merge in 'send' events that fire when the promise is fulfilled.
.merge(promiseFulfilled.map({ type: 'send' }))
# Feed into a state machine that keeps queues and only creates
# output events on 'send' input events.
.withStateMachine(
{
queues: streams.map(-> [])
toPush: 0
ended: 0
}
handleState
)
# Feed this output to the promiser
theStream.onValue((value) ->
Bacon.fromPromise(promiser(value)).onValue(->
promiseFulfilled.push()
))
handleState = (state, baconEvent) ->
outEvents = []
if baconEvent.hasValue()
# Handle a round robin event of 'value', 'send', or 'end'
outEvents = handleRoundRobinEvent(state, baconEvent.value())
else
outEvents = [baconEvent]
[state, outEvents]
handleRoundRobinEvent = (state, rrEvent) ->
outEvents = []
# 'value' : push onto queue
if rrEvent.type == 'value'
state.queues[rrEvent.index].push(rrEvent.value)
# 'send' : send the next value by round-robin selection
else if rrEvent.type == 'send'
# Here's a sentinel for empty queues
noValue = {}
nextValue = noValue
triedQueues = 0
while nextValue == noValue && triedQueues < state.queues.length
if state.queues[state.toPush].length > 0
nextValue = state.queues[state.toPush].shift()
state.toPush = (state.toPush + 1) % state.queues.length
triedQueues++
if nextValue != noValue
outEvents.push(new Bacon.Next(nextValue))
# 'end': Keep track of ended streams
else if rrEvent.type == 'end'
state.ended++
# End the round-robin stream if all inputs have ended
if roundRobinEnded(state)
outEvents.push(new Bacon.End())
outEvents
roundRobinEnded = (state) ->
emptyQueues = allEmpty(state.queues)
emptyQueues && state.ended == state.queues.length
allEmpty = (arrays) ->
for a in arrays
return false if a.length > 0
return true
#roundRobinPromiseMap::(a->Promise b)->[EventStream]->EventStream
roundRobinPromiseMap=(promiser,streams)->
#基于承诺履行触发新发送的总线
PromiseCompleted=新培根
#将输入流合并为单个键控流
theStream=Bacon.mergeAll(streams.map((s,idx)->
s、 地图((val)->{
类型:“值”
索引:idx
值:val
})
))
#在“结束”事件中合并
.merge(Bacon.mergeAll(streams.map)(s)->
s、 mapEnd(->{
键入:“结束”
})
)))
#合并在“发送”事件中,该事件在承诺履行时触发。
.merge(promisemplied.map({type:'send'}))
#馈送到一个状态机,该状态机保持队列并只创建
#“发送”输入事件上的输出事件。
.带状态机(
{
队列:streams.map(->[])
toPush:0
完:0
}
扶手
)
#将此输出馈送到promiser
theStream.onValue((value)->
培根.fromPromise(promiser(value)).onValue(->
履行诺言
))
handleState=(州,baconEvent)->
outEvents=[]
如果baconEvent.hasValue()
#处理“value”、“send”或“end”的循环事件
outEvents=handleRoundRobinEvent(状态,baconEvent.value())
其他的
outEvents=[baconEvent]
[州,州]
handleRoundRobinEvent=(状态,rrEvent)->
outEvents=[]
#“值”:推送到队列上
如果rrEvent.type=='value'
state.queues[rrEvent.index].push(rrEvent.value)
#“发送”:通过循环选择发送下一个值
如果rrEvent.type=='send'
#这里有一个空队哨兵
noValue={}
下一个值=新值
triedQueues=0
而nextValue==noValue&&triedQueues0
nextValue=state.queues[state.toPush].shift()
state.toPush=(state.toPush+1)%state.queues.length
搜索队列++
如果下一个值!=诺瓦卢
outEvents.push(新培根,下一个(下一个值))
#“结束”:跟踪结束的流
如果rEvent.type=='end',则为else
结束++
#如果所有输入都已结束,则结束循环流
如果为圆形(州)
outEvents.push(new Bacon.End())
外衣
RoundRobineded=(州)->
emptyQueues=allEmpty(state.queues)
emptyQueues&&state.ended==state.queues.length
allEmpty=(数组)->
对于数组中的
如果a.length>0,则返回false
返回真值
这里的核心挑战是理解如何将公平正式化。在问题中,我已经提到了工人类比。事实证明,明显的公平性标准是选择一个比其他流生成的事件更少的流,或者更进一步:谁生成的流等待的时间更少
在此之后,使用指称语义将所需输出形式化是非常简单的:
我没有时间开发从Bacon.js中包含和stateMachine
的指称组合词,因此下一步是直接用Bacon.js在JavaScript中重新实现它。整个可运行的解决方案是
这个想法是用
- 每流成本和队列状态
- 流和附加反馈流作为输入
当整个系统的输出被反馈时,我们可以在前一个平面映射流结束时将下一个事件出列
为此,我不得不制作一个有点难看的rec
combinator
function rec(f) {
var bus = new Bacon.Bus();
var result = f(bus);
bus.plug(result);
return result;
}
它的类型是(EventStream a->EventStream a)->EventStream a
-该类型类似于其他递归组合符,例如fix
当总线中断取消订阅传播时,它可以具有更好的系统范围行为。我们必须在这方面努力
第二个辅助函数是stateMachine
,它接受一组流并将它们转换为单个状态机。本质上,它是。带状态机的∘ 合并所有∘ zipWithIndex
function stateMachine(inputs, initState, f) {
var mapped = inputs.map(function (input, i) {
return input.map(function (x) {
return [i, x];
})
});
return Bacon.mergeAll(mapped).withStateMachine(initState, function (state, p) {
if (p.hasValue()) {
p = p.value();
return f(state, p[0], p[1]);
} else {
return [state, p];
}
});
}
使用这两个助手
function stateMachine(inputs, initState, f) {
var mapped = inputs.map(function (input, i) {
return input.map(function (x) {
return [i, x];
})
});
return Bacon.mergeAll(mapped).withStateMachine(initState, function (state, p) {
if (p.hasValue()) {
p = p.value();
return f(state, p[0], p[1]);
} else {
return [state, p];
}
});
}
function fairScheduler(streams, fn) {
var streamsCount = streams.length;
return rec(function (res) {
return stateMachine(append(streams, res), initialFairState(streamsCount), function (state, i, x) {
// console.log("FAIR: " + JSON.stringify(state), i, x);
// END event
if (i == streamsCount && x.end) {
var additionalCost = new Date().getTime() - x.started;
// add cost to input stream cost center
var updatedState = _.extend({}, state, {
costs: updateArray(
state.costs,
x.idx, function (cost) { return cost + additionalCost; }),
});
if (state.queues.every(function (q) { return q.length === 0; })) {
// if queues are empty, set running: false and don't emit any events
return [_.extend({}, updatedState, { running: false }), []];
} else {
// otherwise pick a stream with
// - non-empty queue
// - minimal cost
var minQueueIdx = _.chain(state.queues)
.map(function (q, i) {
return [q, i];
})
.filter(function (p) {
return p[0].length !== 0;
})
.sortBy(function (p) {
return state.costs[p[1]];
})
.value()[0][1];
// emit an event from that stream
return [
_.extend({}, updatedState, {
queues: updateArray(state.queues, minQueueIdx, function (q) { return q.slice(1); }),
running: true,
}),
[new Bacon.Next({
value: state.queues[minQueueIdx][0],
idx: minQueueIdx,
})],
];
}
} else if (i < streamsCount) {
// event from input stream
if (state.running) {
// if worker is running, just enquee the event
return [
_.extend({}, state, {
queues: updateArray(state.queues, i, function (q) { return q .concat([x]); }),
}),
[],
];
} else {
// if worker isn't running, start it right away
return [
_.extend({}, state, {
running: true,
}),
[new Bacon.Next({ value: x, idx: i})],
]
}
} else {
return [state, []];
}
})
.flatMapConcat(function (x) {
// map passed thru events,
// and append special "end" event
return fn(x).concat(Bacon.once({
end: true,
idx: x.idx,
started: new Date().getTime(),
}));
});
})
.filter(function (x) {
// filter out END events
return !x.end;
})
.map(".value"); // and return only value field
}