nl 0.2.4 → 0.4.1

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Rakefile +7 -0
  4. data/lib/nl/async/dispatcher.rb +199 -0
  5. data/lib/nl/async/driver.rb +37 -0
  6. data/lib/nl/async/mailbox.rb +81 -0
  7. data/lib/nl/async/operation.rb +277 -0
  8. data/lib/nl/async.rb +13 -0
  9. data/lib/nl/attribute_set.rb +173 -0
  10. data/lib/nl/bitfield32.rb +86 -0
  11. data/lib/nl/blocking_transport.rb +131 -0
  12. data/lib/nl/connection.rb +61 -0
  13. data/lib/nl/datagram.rb +22 -0
  14. data/lib/nl/datatypes.rb +454 -0
  15. data/lib/nl/decoder.rb +13 -1
  16. data/lib/nl/endian.rb +10 -0
  17. data/lib/nl/error.rb +10 -0
  18. data/lib/nl/exchange.rb +124 -0
  19. data/lib/nl/family.rb +117 -23
  20. data/lib/nl/genl/client.rb +104 -0
  21. data/lib/nl/genl/protocol.rb +59 -0
  22. data/lib/nl/genl/wire.rb +94 -0
  23. data/lib/nl/genl.rb +60 -73
  24. data/lib/nl/notification.rb +112 -0
  25. data/lib/nl/notification_router.rb +92 -0
  26. data/lib/nl/raw/client.rb +83 -0
  27. data/lib/nl/raw/protocol.rb +112 -0
  28. data/lib/nl/raw/wire.rb +146 -0
  29. data/lib/nl/raw.rb +104 -0
  30. data/lib/nl/sequence_allocator.rb +34 -0
  31. data/lib/nl/socket.rb +15 -12
  32. data/lib/nl/structured_payload.rb +61 -0
  33. data/lib/nl/sub_message.rb +94 -0
  34. data/lib/nl/version.rb +1 -1
  35. data/lib/nl.rb +10 -12
  36. data/sig/generated/nl/async/dispatcher.rbs +53 -0
  37. data/sig/generated/nl/async/driver.rbs +21 -0
  38. data/sig/generated/nl/async/mailbox.rbs +32 -0
  39. data/sig/generated/nl/async/operation.rbs +123 -0
  40. data/sig/generated/nl/async.rbs +9 -0
  41. data/sig/generated/nl/attribute_set.rbs +50 -0
  42. data/sig/generated/nl/bitfield32.rbs +41 -0
  43. data/sig/generated/nl/blocking_transport.rbs +33 -0
  44. data/sig/generated/nl/connection.rbs +26 -0
  45. data/sig/generated/nl/datagram.rbs +11 -0
  46. data/sig/generated/nl/datatypes.rbs +181 -0
  47. data/sig/generated/nl/decoder.rbs +38 -0
  48. data/sig/generated/nl/encoder.rbs +29 -0
  49. data/sig/generated/nl/endian.rbs +28 -0
  50. data/sig/generated/nl/error.rbs +27 -0
  51. data/sig/generated/nl/exchange.rbs +75 -0
  52. data/sig/generated/nl/family.rbs +71 -0
  53. data/sig/generated/nl/genl/client.rbs +74 -0
  54. data/sig/generated/nl/genl/protocol.rbs +37 -0
  55. data/sig/generated/nl/genl/wire.rbs +113 -0
  56. data/sig/generated/nl/genl.rbs +50 -0
  57. data/sig/generated/nl/notification.rbs +53 -0
  58. data/sig/generated/nl/notification_router.rbs +30 -0
  59. data/sig/generated/nl/raw/client.rbs +50 -0
  60. data/sig/generated/nl/raw/protocol.rbs +122 -0
  61. data/sig/generated/nl/raw/wire.rbs +151 -0
  62. data/sig/generated/nl/raw.rbs +70 -0
  63. data/sig/generated/nl/sequence_allocator.rbs +15 -0
  64. data/sig/generated/nl/socket.rbs +40 -0
  65. data/sig/generated/nl/structured_payload.rbs +25 -0
  66. data/sig/generated/nl/sub_message.rbs +74 -0
  67. data/sig/generated/nl/version.rbs +5 -0
  68. data/sig/generated/nl.rbs +2 -0
  69. metadata +65 -9
  70. data/lib/nl/core.rb +0 -94
  71. data/lib/nl/protocols/genl.rb +0 -55
  72. data/lib/nl/protocols/raw.rb +0 -395
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 125daafb96b4c4e1147421c6482219e9158ad31d0827859a61c86d97f506c62a
4
- data.tar.gz: ed544d00ae8fa14525e75e913bf3e6e36931c520d9dbdc86d79a10dcbf859310
3
+ metadata.gz: 8fbd6064a908ddacbadbb65eaa2f7c7ab862baf91478804f1915ba988271b759
4
+ data.tar.gz: f49043a50d47c82da9f1f818f02948a12f386df091b7041e4ae1b7d755780b23
5
5
  SHA512:
6
- metadata.gz: d9af220cb3e60bb8a30124c50304a8916679ca5f2b559ecc17828312a522ad73244e5ab9ff22b337641be93a9907f86790b0c6a9e2118533abd853e687d77349
7
- data.tar.gz: d35efcdb26611ca0eba7d422280c96caafee84b39e988a453dd3c3ef23a05294a5d767e1b9f0cd515e0d3224ba04ff8a345b7edd759348dbd095d20d47cc2506
6
+ metadata.gz: 0f162b7c19214639fdd277bea5fdedb1ef4661a8747de0458b08dd85fefa94ec36dfbbd71b039ef5143069ca903e54d133029528dd4a75cee865d6b409db7251
7
+ data.tar.gz: c538d2a693926047576fc49bd82fd0bf435cbe19e3c61c21536826a0d873f260b87f2b6544dec4dac73e5385c11baa1da741c901bd81f2711eadb827cf34f356
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.4.1 (2026-09-08)
6
+
7
+ ## v0.4.0 (2026-09-08)
8
+
9
+ - Represent `bitfield32` values with mutable `Bitfield32` objects.
10
+ - Support fixed-length binary values and structured binary values.
11
+ - Encode Generic Netlink requests with the protocol version.
12
+ - Support building and reading attributes declared as repeatable.
13
+ - Add `Raw::Client` for using raw families with the same protocol number over one connection.
14
+ - Rename `Genl::Connection` to `Genl::Client`.
15
+ - Remove `UnknownNotification`; unsolicited messages without a registered decoder are now ignored.
16
+ - Add context-aware encoding and decoding support for polymorphic sub-messages.
17
+
18
+ ## v0.3.0 (2026-09-01)
19
+
20
+ - Fix encoding of nested attributes and mark them with `NLA_F_NESTED`.
21
+ - Support encoding and decoding of nested type-value attributes.
22
+ - Support multiplexing requests via `Family.async`
23
+ - Support receiving notifications (unsolicited messages).
24
+ - Rename `Genl::Connection#open` to `Genl::Connection#family`.
25
+
5
26
  ## v0.2.4 (2026-08-22)
6
27
 
7
28
  ## v0.2.3 (2026-03-24)
data/Rakefile CHANGED
@@ -2,4 +2,11 @@ require 'rspec/core/rake_task'
2
2
 
3
3
  RSpec::Core::RakeTask.new(:spec)
4
4
 
5
+ desc 'Generate RBS signatures from inline annotations'
6
+ task :rbs do
7
+ sh '../bin/rbs-inline', '--output=sig/generated', '--opt-out', 'lib'
8
+ end
9
+
10
+ task :build => :rbs
11
+
5
12
  task default: :spec
@@ -0,0 +1,199 @@
1
+ require_relative '../datagram'
2
+ require_relative '../error'
3
+ require_relative '../exchange'
4
+ require_relative '../notification_router'
5
+ require_relative '../sequence_allocator'
6
+
7
+ module Nl
8
+ module Async
9
+ # Owns the single receive loop for a socket and routes replies by sequence.
10
+ class Dispatcher
11
+ Pending = Data.define(:exchange, :endpoint, :reply_class, :sink)
12
+ private_constant :Pending
13
+
14
+ def initialize(socket, protocol:, executor: :thread, notifications:)
15
+ @socket = socket
16
+ @protocol = protocol
17
+ @sequences = SequenceAllocator.new
18
+ @notifications = notifications
19
+ @driver = executor.respond_to?(:start) ? executor : Async.driver(executor)
20
+ @mutex = Mutex.new
21
+ @send_mutex = Mutex.new
22
+ @pending = {}
23
+ @closed = false
24
+ @task = @driver.start { receive_loop }
25
+ end
26
+
27
+ def exchange(endpoint, kind, request_class, reply_class, args, &block)
28
+ operation = exchange_async(endpoint, kind, request_class, reply_class, args)
29
+ if kind == :dump
30
+ if block
31
+ operation.each(&block)
32
+ nil
33
+ else
34
+ operation.to_a
35
+ end
36
+ else
37
+ operation.await
38
+ end
39
+ end
40
+
41
+ def exchange_async(endpoint, kind, request_class, reply_class, args, stream_capacity: nil)
42
+ request = @protocol.build_request(endpoint, kind, request_class, args)
43
+
44
+ key = nil
45
+ seq = pid = nil
46
+ operation = sink = nil
47
+ @send_mutex.synchronize do
48
+ @mutex.synchronize do
49
+ raise ClosedError, 'dispatcher is closed' if @closed
50
+
51
+ pid = @socket.local_port_id
52
+ seq = @sequences.next { @pending.key?([it, pid]) }
53
+ key = [seq, pid]
54
+ mailbox = Mailbox.new(capacity: stream_capacity)
55
+ operation, sink = if kind == :dump
56
+ Stream.build(mailbox:, on_close: -> { discard(key) })
57
+ else
58
+ Future.build(mailbox:, on_close: -> { discard(key) })
59
+ end
60
+ mode = kind == :dump ? :dump : (reply_class ? :reply : :no_reply)
61
+ exchange = Exchange.new(mode:)
62
+ @pending[key] = Pending.new(exchange:, endpoint:, reply_class:, sink:)
63
+ end
64
+
65
+ begin
66
+ @protocol.send_message(@socket, endpoint, request, seq:, pid:)
67
+ rescue Exception => error
68
+ fail_pending(key, error)
69
+ raise
70
+ end
71
+ end
72
+ operation
73
+ end
74
+
75
+ def async_capable? #: true
76
+ true
77
+ end
78
+
79
+ def receive_notification(endpoint, timeout: nil)
80
+ @notifications.channel(endpoint).pop(timeout:)
81
+ end
82
+
83
+ def close
84
+ task = @mutex.synchronize do
85
+ next if @closed
86
+
87
+ @closed = true
88
+ @socket.close unless @socket.closed?
89
+ @task
90
+ end
91
+ fail_all(ClosedError.new('dispatcher is closed'))
92
+ @driver.stop(task) if task && !task.equal?(::Thread.current)
93
+ nil
94
+ end
95
+
96
+ private def receive_loop
97
+ loop do
98
+ break if @mutex.synchronize { @closed }
99
+
100
+ @socket.wait_readable
101
+ data = @socket.recvmsg_nonblock(exception: false)
102
+ next if data == :wait_readable
103
+
104
+ dispatch_datagram(IO::Buffer.for(data.first))
105
+ rescue Errno::ENOBUFS => e
106
+ @notifications.lose_all(NotificationLossError.new('kernel receive buffer overflowed'))
107
+ fail_all(e)
108
+ end
109
+ rescue Exception => e
110
+ fail_receive_loop(e)
111
+ end
112
+
113
+ private def fail_receive_loop(error)
114
+ failed = @mutex.synchronize do
115
+ next false if @closed
116
+
117
+ @closed = true
118
+ @socket.close unless @socket.closed?
119
+ true
120
+ end
121
+ return unless failed
122
+
123
+ fail_all(error)
124
+ @notifications.close
125
+ end
126
+
127
+ private def dispatch_datagram(buffer)
128
+ Datagram.each_frame(buffer) do |header, payload|
129
+ key = [header.seq, header.pid]
130
+ pending = @mutex.synchronize { @pending[key] }
131
+ unless pending
132
+ @notifications.route(header, payload) if header.seq.zero?
133
+ next
134
+ end
135
+
136
+ begin
137
+ message = @protocol.decode_frame(pending.endpoint, header, payload, pending.reply_class)
138
+ dispatch_outcome(key, pending, pending.exchange.accept(message))
139
+ rescue Exception => error
140
+ fail_pending(key, error) if pending
141
+ end
142
+ end
143
+ end
144
+
145
+ private def dispatch_outcome(key, pending, outcome)
146
+ return unless outcome
147
+
148
+ case outcome
149
+ when Exchange::Item
150
+ begin
151
+ pending.sink.push(outcome.value)
152
+ rescue Mailbox::FullError
153
+ fail_pending(key, StreamOverflowError.new("reply buffer exceeded for sequence #{key.first}"))
154
+ end
155
+ when Exchange::Complete
156
+ complete_pending(key, pending, pending.exchange.result)
157
+ when Exchange::Failure
158
+ fail_pending(key, outcome.exception)
159
+ end
160
+ end
161
+
162
+ private def complete_pending(key, pending, value)
163
+ removed = @mutex.synchronize { @pending.delete(key) }
164
+ return unless removed
165
+ return if pending.exchange.cancelled?
166
+
167
+ if pending.exchange.mode == :dump
168
+ pending.sink.finish
169
+ elsif pending.exchange.mode == :reply
170
+ pending.sink.succeed(value)
171
+ else
172
+ pending.sink.finish
173
+ end
174
+ end
175
+
176
+ private def discard(key)
177
+ @mutex.synchronize do
178
+ if pending = @pending[key]
179
+ pending.exchange.cancel
180
+ end
181
+ end
182
+ end
183
+
184
+ private def fail_pending(key, error)
185
+ pending = @mutex.synchronize { @pending.delete(key) }
186
+ pending&.sink&.fail(error) unless pending&.exchange&.cancelled?
187
+ end
188
+
189
+ private def fail_all(error)
190
+ pending = @mutex.synchronize do
191
+ old = @pending.values
192
+ @pending.clear
193
+ old
194
+ end
195
+ pending.each { it.sink.fail(error) unless it.exchange.cancelled? }
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,37 @@
1
+ module Nl
2
+ module Async
3
+ module Drivers
4
+ class Thread
5
+ def start(&block)
6
+ ::Thread.new(&block)
7
+ end
8
+
9
+ def stop(task)
10
+ task.join
11
+ end
12
+ end
13
+
14
+ class Fiber
15
+ def start(&block)
16
+ raise ArgumentError, 'Fiber.scheduler is not installed' unless ::Fiber.scheduler
17
+
18
+ ::Fiber.schedule(&block)
19
+ end
20
+
21
+ def stop(_task)
22
+ # Scheduled fibers have no general join operation. Closing the socket
23
+ # makes the receive loop terminate at its next scheduling point.
24
+ end
25
+ end
26
+ end
27
+
28
+ def self.driver(name)
29
+ case name
30
+ when :thread then Drivers::Thread.new
31
+ when :fiber then Drivers::Fiber.new
32
+ else
33
+ raise ArgumentError, "unknown async executor: #{name.inspect}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,81 @@
1
+ require_relative '../error'
2
+
3
+ module Nl
4
+ module Async
5
+ # A thread-safe, scheduler-aware, single-consumer mailbox.
6
+ class Mailbox
7
+ class FullError < StandardError; end
8
+
9
+ def initialize(capacity: nil)
10
+ raise ArgumentError, 'capacity must be positive' if capacity && capacity <= 0
11
+
12
+ @mutex = Mutex.new
13
+ @condition = ConditionVariable.new
14
+ @queue = []
15
+ @closed = false
16
+ @capacity = capacity
17
+ end
18
+
19
+ # Adds a value without executing consumer code.
20
+ # @rbs (untyped) -> bool
21
+ def push(value)
22
+ push_value(value, enforce_capacity: true)
23
+ end
24
+
25
+ # Adds a completion/error event even when the value capacity is full.
26
+ def push_terminal(value)
27
+ push_value(value, enforce_capacity: false)
28
+ end
29
+
30
+ private def push_value(value, enforce_capacity:)
31
+ @mutex.synchronize do
32
+ return false if @closed
33
+ if enforce_capacity && @capacity && @queue.length >= @capacity
34
+ raise FullError, 'mailbox capacity exceeded'
35
+ end
36
+
37
+ wake = @queue.empty?
38
+ @queue << value
39
+ @condition.signal if wake
40
+ end
41
+ true
42
+ end
43
+
44
+ # Removes the next value, suspending through the current thread or Fiber scheduler if empty.
45
+ # @rbs (?timeout: Numeric?) -> untyped
46
+ def pop(timeout: nil)
47
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
48
+
49
+ @mutex.synchronize do
50
+ loop do
51
+ return @queue.shift unless @queue.empty?
52
+ raise ClosedError if @closed
53
+
54
+ remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
55
+ raise TimeoutError if remaining && remaining <= 0
56
+
57
+ @condition.wait(@mutex, remaining)
58
+ end
59
+ end
60
+ end
61
+
62
+ def close
63
+ @mutex.synchronize do
64
+ return nil if @closed
65
+
66
+ @closed = true
67
+ @condition.broadcast
68
+ end
69
+ nil
70
+ end
71
+
72
+ def closed?
73
+ @mutex.synchronize { @closed }
74
+ end
75
+
76
+ def empty?
77
+ @mutex.synchronize { @queue.empty? }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,277 @@
1
+ require_relative '../error'
2
+
3
+ module Nl
4
+ module Async
5
+ module Events
6
+ Item = Data.define(:value)
7
+ Done = Data.define
8
+ Error = Data.define(:exception)
9
+ end
10
+ private_constant :Events
11
+
12
+ class ConcurrentConsumptionError < StandardError; end
13
+ class StreamOverflowError < StandardError; end
14
+
15
+ # Future for the result of an already-started, single-reply operation.
16
+ # @rbs generic out Result
17
+ class Future
18
+ def self.build(mailbox: Mailbox.new, on_close: nil)
19
+ operation = new(mailbox:, on_close:)
20
+ [operation, Sink.new(mailbox)]
21
+ end
22
+
23
+ def initialize(mailbox:, on_close:)
24
+ @mailbox = mailbox
25
+ @on_close = on_close
26
+ @mutex = Mutex.new
27
+ @state = :open
28
+ @value = nil
29
+ @error = nil
30
+ end
31
+
32
+ # @rbs (?timeout: Numeric?) -> Result
33
+ def await(timeout: nil)
34
+ return terminal_result unless @mutex.synchronize { @state == :open }
35
+
36
+ event = @mailbox.pop(timeout:)
37
+ case event
38
+ when Events::Item
39
+ settle_done(event.value)
40
+ when Events::Done
41
+ settle_done(nil)
42
+ when Events::Error
43
+ settle_failed(event.exception)
44
+ end
45
+ @mailbox.close
46
+ terminal_result
47
+ rescue ClosedError
48
+ terminal_result
49
+ end
50
+ alias value await
51
+
52
+ def ready? #: bool
53
+ @mutex.synchronize { @state != :open } || !@mailbox.empty?
54
+ end
55
+
56
+ # Stops local delivery. The dispatcher still drains the Netlink exchange.
57
+ def close #: nil
58
+ closed, callback = @mutex.synchronize do
59
+ next [false, nil] if @state != :open
60
+
61
+ @state = :closed
62
+ [true, @on_close]
63
+ end
64
+ if closed
65
+ callback&.call
66
+ @mailbox.close
67
+ end
68
+ nil
69
+ end
70
+
71
+ private def settle_done(value)
72
+ @mutex.synchronize do
73
+ if @state == :open
74
+ @state = :done
75
+ @value = value
76
+ end
77
+ end
78
+ end
79
+
80
+ private def settle_failed(error)
81
+ @mutex.synchronize do
82
+ if @state == :open
83
+ @state = :failed
84
+ @error = error
85
+ end
86
+ end
87
+ end
88
+
89
+ private def terminal_result
90
+ state, value, error = @mutex.synchronize { [@state, @value, @error] }
91
+ case state
92
+ when :done then value
93
+ when :failed then raise error
94
+ else raise ClosedError, 'future is closed'
95
+ end
96
+ end
97
+
98
+ class Sink
99
+ def initialize(mailbox)
100
+ @mailbox = mailbox
101
+ end
102
+
103
+ def succeed(value) = @mailbox.push(Events::Item.new(value))
104
+ def finish = @mailbox.push_terminal(Events::Done.new)
105
+ def fail(exception) = @mailbox.push_terminal(Events::Error.new(exception))
106
+ end
107
+ end
108
+
109
+ # Single-pass consumer handle for a multipart Netlink operation.
110
+ # @rbs generic out Item
111
+ class Stream
112
+ include Enumerable #[Item]
113
+
114
+ def self.build(mailbox: Mailbox.new, on_close: nil)
115
+ operation = new(mailbox:, on_close:)
116
+ [operation, Sink.new(mailbox)]
117
+ end
118
+
119
+ def initialize(mailbox:, on_close:)
120
+ @mailbox = mailbox
121
+ @on_close = on_close
122
+ @mutex = Mutex.new
123
+ @owner = nil
124
+ @state = :open
125
+ @error = nil
126
+ end
127
+
128
+ # @rbs () { (Item) -> void } -> self
129
+ # | () -> Enumerator[Item, self]
130
+ def each
131
+ return enum_for(__method__) unless block_given?
132
+
133
+ state, error = current_state
134
+ case state
135
+ when :completed then return self
136
+ when :closed then raise ClosedError, 'stream is closed'
137
+ when :failed then raise error
138
+ end
139
+
140
+ claim_consumer!
141
+ begin
142
+ loop do
143
+ case event = @mailbox.pop
144
+ when Events::Item
145
+ yield event.value
146
+ when Events::Done
147
+ completed = mark_completed
148
+ @mailbox.close
149
+ terminal_result unless completed
150
+ break
151
+ when Events::Error
152
+ failed = mark_failed(event.exception)
153
+ @mailbox.close
154
+ raise event.exception if failed
155
+
156
+ terminal_result
157
+ end
158
+ end
159
+ self
160
+ rescue ClosedError
161
+ terminal_result
162
+ ensure
163
+ close if open?
164
+ end
165
+ end
166
+
167
+ # @rbs (?timeout: Numeric?) -> Item
168
+ def next(timeout: nil)
169
+ state, error = current_state
170
+ case state
171
+ when :completed then raise StopIteration
172
+ when :closed then raise ClosedError, 'stream is closed'
173
+ when :failed then raise error
174
+ end
175
+
176
+ claim_consumer!
177
+ case event = @mailbox.pop(timeout:)
178
+ when Events::Item
179
+ event.value
180
+ when Events::Done
181
+ completed = mark_completed
182
+ @mailbox.close
183
+ raise StopIteration if completed
184
+
185
+ terminal_result(next_item: true)
186
+ when Events::Error
187
+ failed = mark_failed(event.exception)
188
+ @mailbox.close
189
+ raise event.exception if failed
190
+
191
+ terminal_result(next_item: true)
192
+ end
193
+ rescue ClosedError
194
+ terminal_result(next_item: true)
195
+ end
196
+
197
+ # Stops local delivery. The dispatcher still drains the Netlink exchange.
198
+ def close #: nil
199
+ closed, callback = @mutex.synchronize do
200
+ next [false, nil] if @state != :open
201
+
202
+ @state = :closed
203
+ [true, @on_close]
204
+ end
205
+ if closed
206
+ callback&.call
207
+ @mailbox.close
208
+ end
209
+ nil
210
+ end
211
+
212
+ def closed? #: bool
213
+ @mutex.synchronize { @state != :open }
214
+ end
215
+
216
+ private def current_state
217
+ @mutex.synchronize { [@state, @error] }
218
+ end
219
+
220
+ private def open?
221
+ @mutex.synchronize { @state == :open }
222
+ end
223
+
224
+ private def claim_consumer!
225
+ owner = Fiber.current
226
+ @mutex.synchronize do
227
+ @owner ||= owner
228
+ unless @owner.equal?(owner)
229
+ raise ConcurrentConsumptionError, 'a stream can only be consumed by one fiber'
230
+ end
231
+ end
232
+ end
233
+
234
+ private def mark_completed
235
+ @mutex.synchronize do
236
+ next false unless @state == :open
237
+
238
+ @state = :completed
239
+ true
240
+ end
241
+ end
242
+
243
+ private def mark_failed(error)
244
+ @mutex.synchronize do
245
+ next false unless @state == :open
246
+
247
+ @state = :failed
248
+ @error = error
249
+ true
250
+ end
251
+ end
252
+
253
+ private def terminal_result(next_item: false)
254
+ state, error = current_state
255
+ case state
256
+ when :completed
257
+ raise StopIteration if next_item
258
+ self
259
+ when :failed
260
+ raise error
261
+ else
262
+ raise ClosedError, 'stream is closed'
263
+ end
264
+ end
265
+
266
+ class Sink
267
+ def initialize(mailbox)
268
+ @mailbox = mailbox
269
+ end
270
+
271
+ def push(value) = @mailbox.push(Events::Item.new(value))
272
+ def finish = @mailbox.push_terminal(Events::Done.new)
273
+ def fail(exception) = @mailbox.push_terminal(Events::Error.new(exception))
274
+ end
275
+ end
276
+ end
277
+ end
data/lib/nl/async.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'error'
2
+
3
+ module Nl
4
+ # Asynchronous request handling support.
5
+ module Async
6
+ class UnavailableError < StandardError; end
7
+ end
8
+ end
9
+
10
+ require_relative 'async/mailbox'
11
+ require_relative 'async/operation'
12
+ require_relative 'async/driver'
13
+ require_relative 'async/dispatcher'