fiber_stream 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +283 -12
- data/examples/README.md +10 -0
- data/examples/async_http_streaming_body.rb +115 -0
- data/examples/ractor_merge_ports_and_map.rb +116 -0
- data/lib/fiber_stream/errors.rb +4 -1
- data/lib/fiber_stream/flow.rb +74 -1
- data/lib/fiber_stream/pull/async_boundary.rb +28 -11
- data/lib/fiber_stream/pull/buffer_boundary.rb +28 -10
- data/lib/fiber_stream/pull/concat.rb +103 -0
- data/lib/fiber_stream/pull/drop.rb +58 -0
- data/lib/fiber_stream/pull/drop_while.rb +61 -0
- data/lib/fiber_stream/pull/grouped.rb +46 -0
- data/lib/fiber_stream/pull/merge.rb +230 -0
- data/lib/fiber_stream/pull/parallel_map_boundary.rb +28 -24
- data/lib/fiber_stream/pull/ractor_map_boundary.rb +103 -79
- data/lib/fiber_stream/pull/ractor_merge_ports_source.rb +358 -0
- data/lib/fiber_stream/pull/ractor_port_source.rb +14 -14
- data/lib/fiber_stream/pull/split.rb +134 -0
- data/lib/fiber_stream/pull/take_while.rb +42 -0
- data/lib/fiber_stream/pull/zip.rb +83 -0
- data/lib/fiber_stream/pull.rb +48 -3
- data/lib/fiber_stream/ractor_port.rb +3 -1
- data/lib/fiber_stream/running_pipeline.rb +18 -8
- data/lib/fiber_stream/sink.rb +24 -0
- data/lib/fiber_stream/source.rb +190 -7
- data/lib/fiber_stream/version.rb +1 -1
- data/sig/fiber_stream.rbs +18 -1
- metadata +27 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Pull stream for `Source.ractor_merge_ports`.
|
|
6
|
+
#
|
|
7
|
+
# A coordinator thread owns blocking Ractor waits and forwards producer
|
|
8
|
+
# messages through a bounded result mailbox. Each producer receives at most
|
|
9
|
+
# one outstanding ack, and downstream demand replenishes only the producer
|
|
10
|
+
# that emitted the previous value.
|
|
11
|
+
class RactorMergePortsSource
|
|
12
|
+
PortPair = Data.define(:side, :port, :ack_port)
|
|
13
|
+
StartCommand = Data.define
|
|
14
|
+
RequestAckCommand = Data.define(:side)
|
|
15
|
+
ShutdownCommand = Data.define
|
|
16
|
+
ValueResult = Data.define(:side, :value)
|
|
17
|
+
DoneResult = Data.define(:side)
|
|
18
|
+
ErrorResult = Data.define(:side, :error)
|
|
19
|
+
private_constant :PortPair, :StartCommand, :RequestAckCommand, :ShutdownCommand
|
|
20
|
+
private_constant :ValueResult, :DoneResult, :ErrorResult
|
|
21
|
+
|
|
22
|
+
def initialize(port_pairs, ack_transfer, cancel)
|
|
23
|
+
@pairs = port_pairs.each_with_index.map do |pair, side|
|
|
24
|
+
PortPair.new(side:, port: pair.port, ack_port: pair.ack_port)
|
|
25
|
+
end.freeze
|
|
26
|
+
@ack_transfer = ack_transfer
|
|
27
|
+
@cancel_enabled = cancel
|
|
28
|
+
@result_mailbox = RactorMergeResultMailbox.new(@pairs.size)
|
|
29
|
+
@control_port = nil
|
|
30
|
+
@coordinator = nil
|
|
31
|
+
@state_mutex = Mutex.new
|
|
32
|
+
@producer_terminal = @pairs.to_h { |pair| [pair.side, false] }
|
|
33
|
+
@side_done = @pairs.to_h { |pair| [pair.side, false] }
|
|
34
|
+
@cancel_sent = @pairs.to_h { |pair| [pair.side, false] }
|
|
35
|
+
@pending_ack_sides = {}
|
|
36
|
+
@started = false
|
|
37
|
+
@closed = false
|
|
38
|
+
@done = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def next
|
|
42
|
+
return DONE if closed_or_done?
|
|
43
|
+
|
|
44
|
+
start
|
|
45
|
+
request_pending_acks
|
|
46
|
+
next_result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
started = mark_closed
|
|
51
|
+
return if started.nil?
|
|
52
|
+
return unless started
|
|
53
|
+
|
|
54
|
+
close_result_mailbox
|
|
55
|
+
wake_coordinator
|
|
56
|
+
wait_for_coordinator
|
|
57
|
+
cancel_error = cancel_non_terminal_producers
|
|
58
|
+
raise cancel_error if cancel_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def closed_or_done?
|
|
64
|
+
@state_mutex.synchronize { @closed || @done }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def mark_closed
|
|
68
|
+
already_closed = false
|
|
69
|
+
started = false
|
|
70
|
+
|
|
71
|
+
@state_mutex.synchronize do
|
|
72
|
+
already_closed = @closed
|
|
73
|
+
started = @started
|
|
74
|
+
@closed = true
|
|
75
|
+
@done = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
already_closed ? nil : started
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start
|
|
82
|
+
started_now = false
|
|
83
|
+
|
|
84
|
+
@state_mutex.synchronize do
|
|
85
|
+
return if @started || @closed
|
|
86
|
+
|
|
87
|
+
@control_port = Ractor::Port.new
|
|
88
|
+
@started = true
|
|
89
|
+
@coordinator = Thread.new { run_coordinator }
|
|
90
|
+
started_now = true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
send_control_command(StartCommand.new) if started_now && !closed_or_done?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def request_pending_acks
|
|
97
|
+
sides =
|
|
98
|
+
@state_mutex.synchronize do
|
|
99
|
+
pending = @pending_ack_sides.keys
|
|
100
|
+
@pending_ack_sides.clear
|
|
101
|
+
pending
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sides.each { |side| send_control_command(RequestAckCommand.new(side:)) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def next_result
|
|
108
|
+
loop do
|
|
109
|
+
result = @result_mailbox.pop
|
|
110
|
+
return complete if result.nil?
|
|
111
|
+
|
|
112
|
+
case result
|
|
113
|
+
in ValueResult[side:, value:]
|
|
114
|
+
record_pending_ack(side)
|
|
115
|
+
return value
|
|
116
|
+
in DoneResult[side:]
|
|
117
|
+
mark_side_done(side)
|
|
118
|
+
return complete if all_done?
|
|
119
|
+
in ErrorResult[error:]
|
|
120
|
+
mark_done
|
|
121
|
+
raise_error(error)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
rescue RactorMergeResultMailbox::Closed
|
|
125
|
+
complete
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def record_pending_ack(side)
|
|
129
|
+
@state_mutex.synchronize do
|
|
130
|
+
@pending_ack_sides[side] = true unless @closed || @producer_terminal.fetch(side)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def mark_side_done(side)
|
|
135
|
+
@state_mutex.synchronize { @side_done[side] = true }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def all_done?
|
|
139
|
+
@state_mutex.synchronize { @side_done.values.all? }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def mark_done
|
|
143
|
+
@state_mutex.synchronize { @done = true }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def complete
|
|
147
|
+
mark_done
|
|
148
|
+
DONE
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def run_coordinator
|
|
152
|
+
outstanding_ack = @pairs.to_h { |pair| [pair.side, false] }
|
|
153
|
+
active_ports = @pairs.map(&:port)
|
|
154
|
+
pair_by_port = @pairs.to_h { |pair| [pair.port, pair] }
|
|
155
|
+
|
|
156
|
+
loop do
|
|
157
|
+
break if active_ports.empty?
|
|
158
|
+
|
|
159
|
+
selected, message = Ractor.select(@control_port, *active_ports)
|
|
160
|
+
if selected == @control_port
|
|
161
|
+
break if handle_control_message(message, outstanding_ack)
|
|
162
|
+
else
|
|
163
|
+
pair = pair_by_port.fetch(selected)
|
|
164
|
+
outstanding_ack[pair.side] = false
|
|
165
|
+
result = build_result(pair, message)
|
|
166
|
+
active_ports.delete(pair.port) if producer_terminal?(pair.side)
|
|
167
|
+
deliver_result(result)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
rescue StandardError => error
|
|
171
|
+
deliver_result(ErrorResult.new(side: nil, error: build_error(:receive, error)))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def handle_control_message(message, outstanding_ack)
|
|
175
|
+
case message
|
|
176
|
+
in StartCommand
|
|
177
|
+
@pairs.each { |pair| ack_pair(pair, outstanding_ack) }
|
|
178
|
+
false
|
|
179
|
+
in RequestAckCommand[side:]
|
|
180
|
+
pair = @pairs.fetch(side)
|
|
181
|
+
ack_pair(pair, outstanding_ack)
|
|
182
|
+
false
|
|
183
|
+
in ShutdownCommand
|
|
184
|
+
true
|
|
185
|
+
else
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def ack_pair(pair, outstanding_ack)
|
|
191
|
+
return if outstanding_ack.fetch(pair.side)
|
|
192
|
+
return if producer_terminal?(pair.side)
|
|
193
|
+
|
|
194
|
+
ack_error = send_ack(pair)
|
|
195
|
+
if ack_error
|
|
196
|
+
deliver_result(ErrorResult.new(side: pair.side, error: ack_error))
|
|
197
|
+
else
|
|
198
|
+
outstanding_ack[pair.side] = true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def send_ack(pair)
|
|
203
|
+
send_control(pair.ack_port, RactorPort::Ack.new)
|
|
204
|
+
nil
|
|
205
|
+
rescue StandardError => error
|
|
206
|
+
build_error(:ack_transfer, error)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_result(pair, message)
|
|
210
|
+
side = pair.side
|
|
211
|
+
|
|
212
|
+
case message
|
|
213
|
+
in RactorPort::Element[value]
|
|
214
|
+
ValueResult.new(side:, value:)
|
|
215
|
+
in RactorPort::Complete
|
|
216
|
+
mark_producer_terminal(side)
|
|
217
|
+
DoneResult.new(side:)
|
|
218
|
+
in RactorPort::Failure[String => cause_class_name, String => cause_message]
|
|
219
|
+
mark_producer_terminal(side)
|
|
220
|
+
ErrorResult.new(
|
|
221
|
+
side:,
|
|
222
|
+
error: RactorPortSourceError.new(
|
|
223
|
+
kind: :producer_failure,
|
|
224
|
+
cause_class_name: cause_class_name,
|
|
225
|
+
cause_message: cause_message
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
in RactorPort::Failure
|
|
229
|
+
ErrorResult.new(side:, error: invalid_message_error(message, "Failure payloads must be Strings"))
|
|
230
|
+
else
|
|
231
|
+
ErrorResult.new(side:, error: invalid_message_error(message, "invalid RactorPort message"))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def invalid_message_error(message, cause_message)
|
|
236
|
+
RactorPortSourceError.new(
|
|
237
|
+
kind: :invalid_message,
|
|
238
|
+
cause_class_name: message.class.name,
|
|
239
|
+
cause_message: cause_message
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def build_error(kind, error)
|
|
244
|
+
RactorPortSourceError.new(
|
|
245
|
+
kind: kind,
|
|
246
|
+
cause_class_name: error.class.name,
|
|
247
|
+
cause_message: error.message,
|
|
248
|
+
cause: error
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def mark_producer_terminal(side)
|
|
253
|
+
@state_mutex.synchronize { @producer_terminal[side] = true }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def producer_terminal?(side)
|
|
257
|
+
@state_mutex.synchronize { @producer_terminal.fetch(side) }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def cancel_non_terminal_producers
|
|
261
|
+
first_error = nil
|
|
262
|
+
|
|
263
|
+
@pairs.each do |pair|
|
|
264
|
+
next unless should_cancel_pair?(pair)
|
|
265
|
+
|
|
266
|
+
cancel_error = send_cancel(pair)
|
|
267
|
+
first_error ||= cancel_error
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
first_error
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def should_cancel_pair?(pair)
|
|
274
|
+
@state_mutex.synchronize do
|
|
275
|
+
return false unless @cancel_enabled
|
|
276
|
+
return false if @producer_terminal.fetch(pair.side)
|
|
277
|
+
return false if @cancel_sent.fetch(pair.side)
|
|
278
|
+
|
|
279
|
+
@cancel_sent[pair.side] = true
|
|
280
|
+
true
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def send_cancel(pair)
|
|
285
|
+
send_control(pair.ack_port, RactorPort::Cancel.new(:closed))
|
|
286
|
+
nil
|
|
287
|
+
rescue StandardError => error
|
|
288
|
+
build_error(:cancel_transfer, error)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def send_control(port, message)
|
|
292
|
+
if @ack_transfer == :move
|
|
293
|
+
port.send(message, move: true)
|
|
294
|
+
else
|
|
295
|
+
port.send(message)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def raise_error(error)
|
|
300
|
+
if error.is_a?(RactorPortSourceError) && error.original_cause
|
|
301
|
+
raise error, cause: error.original_cause
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
raise error
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def deliver_result(result)
|
|
308
|
+
@result_mailbox.push(result)
|
|
309
|
+
rescue RactorMergeResultMailbox::Closed
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def send_control_command(command)
|
|
314
|
+
@control_port&.send(command)
|
|
315
|
+
rescue StandardError
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def wake_coordinator
|
|
320
|
+
send_control_command(ShutdownCommand.new)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def wait_for_coordinator
|
|
324
|
+
return unless @coordinator
|
|
325
|
+
|
|
326
|
+
@coordinator.join
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def close_result_mailbox
|
|
330
|
+
@result_mailbox.close
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
class RactorMergeResultMailbox
|
|
334
|
+
Closed = Class.new(StandardError)
|
|
335
|
+
|
|
336
|
+
def initialize(capacity)
|
|
337
|
+
@queue = Thread::SizedQueue.new(capacity)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def push(result)
|
|
341
|
+
@queue << result
|
|
342
|
+
rescue ClosedQueueError
|
|
343
|
+
raise Closed
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def pop
|
|
347
|
+
@queue.pop
|
|
348
|
+
rescue ClosedQueueError
|
|
349
|
+
raise Closed
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def close
|
|
353
|
+
@queue.close
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
@@ -9,7 +9,10 @@ module FiberStream
|
|
|
9
9
|
# a coordinator thread so scheduler-managed fibers do not call Ractor wait
|
|
10
10
|
# APIs directly.
|
|
11
11
|
class RactorPortSource
|
|
12
|
-
|
|
12
|
+
ProtocolMessage = Data.define(:message)
|
|
13
|
+
ErrorMessage = Data.define(:error)
|
|
14
|
+
ClosedMessage = Data.define
|
|
15
|
+
private_constant :ProtocolMessage, :ErrorMessage, :ClosedMessage
|
|
13
16
|
|
|
14
17
|
def initialize(port, ack_port, ack_transfer, cancel)
|
|
15
18
|
@port = port
|
|
@@ -81,15 +84,13 @@ module FiberStream
|
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
def handle_result(result)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
handle_protocol_message(result.fetch(1))
|
|
89
|
-
when :error
|
|
87
|
+
case result
|
|
88
|
+
in ProtocolMessage[message:]
|
|
89
|
+
handle_protocol_message(message)
|
|
90
|
+
in ErrorMessage[error:]
|
|
90
91
|
mark_done
|
|
91
|
-
raise_error(
|
|
92
|
-
|
|
92
|
+
raise_error(error)
|
|
93
|
+
in ClosedMessage
|
|
93
94
|
DONE
|
|
94
95
|
end
|
|
95
96
|
end
|
|
@@ -151,19 +152,19 @@ module FiberStream
|
|
|
151
152
|
|
|
152
153
|
ack_error = send_ack
|
|
153
154
|
if ack_error
|
|
154
|
-
deliver_result(
|
|
155
|
+
deliver_result(ErrorMessage.new(error: ack_error))
|
|
155
156
|
break
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
selected, message = select_message
|
|
159
160
|
break if selected == @shutdown_port || closed?
|
|
160
161
|
|
|
161
|
-
deliver_result(
|
|
162
|
+
deliver_result(ProtocolMessage.new(message:))
|
|
162
163
|
end
|
|
163
164
|
rescue StandardError => error
|
|
164
|
-
deliver_result(
|
|
165
|
+
deliver_result(ErrorMessage.new(error: build_error(:receive, error)))
|
|
165
166
|
ensure
|
|
166
|
-
deliver_result(
|
|
167
|
+
deliver_result(ClosedMessage.new) if closed?
|
|
167
168
|
end
|
|
168
169
|
|
|
169
170
|
def select_message
|
|
@@ -218,7 +219,6 @@ module FiberStream
|
|
|
218
219
|
def wait_for_coordinator
|
|
219
220
|
return unless @coordinator
|
|
220
221
|
|
|
221
|
-
sleep WAIT_INTERVAL while @coordinator.alive?
|
|
222
222
|
@coordinator.join
|
|
223
223
|
end
|
|
224
224
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Delimiter-framing stage for `Flow.split`.
|
|
6
|
+
#
|
|
7
|
+
# The stage keeps an internal byte buffer because frames and separators can
|
|
8
|
+
# cross chunk boundaries. Length checks are per frame body, not against the
|
|
9
|
+
# aggregate buffer, so already complete valid frames can be emitted before a
|
|
10
|
+
# later over-limit frame fails.
|
|
11
|
+
class Split
|
|
12
|
+
def initialize(upstream, separator, keep_separator, max_length)
|
|
13
|
+
@upstream = upstream
|
|
14
|
+
@separator = separator.b.freeze
|
|
15
|
+
@keep_separator = keep_separator
|
|
16
|
+
@max_length = max_length
|
|
17
|
+
@buffer = +"".b
|
|
18
|
+
@closed = false
|
|
19
|
+
@upstream_done = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def next
|
|
23
|
+
return DONE if @closed
|
|
24
|
+
|
|
25
|
+
loop do
|
|
26
|
+
frame = next_buffered_frame
|
|
27
|
+
return frame if frame
|
|
28
|
+
|
|
29
|
+
validate_pending_frame_length!
|
|
30
|
+
return complete_from_buffer if @upstream_done
|
|
31
|
+
|
|
32
|
+
append_next_chunk
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def close
|
|
37
|
+
return if @closed
|
|
38
|
+
|
|
39
|
+
@closed = true
|
|
40
|
+
@buffer.clear
|
|
41
|
+
@upstream.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def next_buffered_frame
|
|
47
|
+
separator_index = @buffer.index(@separator)
|
|
48
|
+
return nil unless separator_index
|
|
49
|
+
|
|
50
|
+
frame = @buffer.slice!(0, separator_index)
|
|
51
|
+
@buffer.slice!(0, @separator.bytesize)
|
|
52
|
+
validate_frame_length!(frame)
|
|
53
|
+
format_frame(frame)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def complete_from_buffer
|
|
57
|
+
return DONE if @buffer.empty?
|
|
58
|
+
|
|
59
|
+
frame = @buffer
|
|
60
|
+
@buffer = +"".b
|
|
61
|
+
validate_frame_length!(frame)
|
|
62
|
+
frame
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def append_next_chunk
|
|
66
|
+
chunk = @upstream.next
|
|
67
|
+
if Pull.done?(chunk)
|
|
68
|
+
@upstream_done = true
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
unless chunk.is_a?(String)
|
|
73
|
+
raise TypeError, "Flow.split elements must be String"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@buffer << chunk.b
|
|
77
|
+
validate_pending_frame_length!
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_pending_frame_length!
|
|
81
|
+
return unless @max_length
|
|
82
|
+
return if pending_frame_body_bytesize <= @max_length
|
|
83
|
+
|
|
84
|
+
fail_frame_too_long
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_frame_length!(frame)
|
|
88
|
+
return unless @max_length
|
|
89
|
+
return if frame.bytesize <= @max_length
|
|
90
|
+
|
|
91
|
+
fail_frame_too_long
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pending_frame_body_bytesize
|
|
95
|
+
separator_index = @buffer.index(@separator)
|
|
96
|
+
return separator_index if separator_index
|
|
97
|
+
|
|
98
|
+
@buffer.bytesize - partial_separator_suffix_bytesize
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def partial_separator_suffix_bytesize
|
|
102
|
+
max_suffix_bytesize = [@separator.bytesize - 1, @buffer.bytesize].min
|
|
103
|
+
return 0 if max_suffix_bytesize.zero?
|
|
104
|
+
|
|
105
|
+
max_suffix_bytesize.downto(1) do |bytesize|
|
|
106
|
+
suffix = @buffer.byteslice(@buffer.bytesize - bytesize, bytesize)
|
|
107
|
+
return bytesize if @separator.start_with?(suffix)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def fail_frame_too_long
|
|
114
|
+
@closed = true
|
|
115
|
+
close_upstream
|
|
116
|
+
error = FrameTooLongError.new("frame exceeded max_length #{@max_length}")
|
|
117
|
+
raise error
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def close_upstream
|
|
121
|
+
@upstream.close
|
|
122
|
+
nil
|
|
123
|
+
rescue StandardError => error
|
|
124
|
+
error
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_frame(frame)
|
|
128
|
+
return frame unless @keep_separator
|
|
129
|
+
|
|
130
|
+
frame + @separator
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Predicate-based limiting stage.
|
|
6
|
+
#
|
|
7
|
+
# It forwards the leading prefix whose predicate results are truthy. The
|
|
8
|
+
# first falsey predicate result is consumed, not emitted, and closes
|
|
9
|
+
# upstream immediately.
|
|
10
|
+
class TakeWhile
|
|
11
|
+
def initialize(upstream, predicate)
|
|
12
|
+
@upstream = upstream
|
|
13
|
+
@predicate = predicate
|
|
14
|
+
@closed = false
|
|
15
|
+
@done = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def next
|
|
19
|
+
return DONE if @closed || @done
|
|
20
|
+
|
|
21
|
+
value = @upstream.next
|
|
22
|
+
if Pull.done?(value)
|
|
23
|
+
@done = true
|
|
24
|
+
return DONE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return value if @predicate.call(value)
|
|
28
|
+
|
|
29
|
+
@done = true
|
|
30
|
+
close
|
|
31
|
+
DONE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
return if @closed
|
|
36
|
+
|
|
37
|
+
@closed = true
|
|
38
|
+
@upstream.close
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Pull stream that emits pairs from two source definitions.
|
|
6
|
+
#
|
|
7
|
+
# The receiver side is materialized on first downstream demand. The other
|
|
8
|
+
# side is materialized only after the receiver produces a value for a pair.
|
|
9
|
+
class Zip
|
|
10
|
+
def initialize(left_materializer, right_materializer)
|
|
11
|
+
@left_materializer = left_materializer
|
|
12
|
+
@right_materializer = right_materializer
|
|
13
|
+
@left = nil
|
|
14
|
+
@right = nil
|
|
15
|
+
@closed = false
|
|
16
|
+
@done = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next
|
|
20
|
+
return DONE if @closed || @done
|
|
21
|
+
|
|
22
|
+
left = materialize_left
|
|
23
|
+
left_value = left.next
|
|
24
|
+
if Pull.done?(left_value)
|
|
25
|
+
@done = true
|
|
26
|
+
close_materialized_streams
|
|
27
|
+
return DONE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
right = materialize_right
|
|
31
|
+
right_value = right.next
|
|
32
|
+
if Pull.done?(right_value)
|
|
33
|
+
@done = true
|
|
34
|
+
close_materialized_streams
|
|
35
|
+
return DONE
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[left_value, right_value]
|
|
39
|
+
rescue StandardError
|
|
40
|
+
@done = true
|
|
41
|
+
close_materialized_streams(raise_error: false)
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def close
|
|
46
|
+
return if @closed
|
|
47
|
+
|
|
48
|
+
@closed = true
|
|
49
|
+
close_materialized_streams
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def materialize_left
|
|
55
|
+
@left ||= @left_materializer.call
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def materialize_right
|
|
59
|
+
@right ||= @right_materializer.call
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def close_materialized_streams(raise_error: true)
|
|
63
|
+
streams = [@left, @right]
|
|
64
|
+
@left = nil
|
|
65
|
+
@right = nil
|
|
66
|
+
|
|
67
|
+
first_error = nil
|
|
68
|
+
|
|
69
|
+
streams.each do |stream|
|
|
70
|
+
next unless stream
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
stream.close
|
|
74
|
+
rescue StandardError => error
|
|
75
|
+
first_error ||= error
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise first_error if raise_error && first_error
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|