philiprehberger-queue_stack 0.1.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff50a60ddd8a9bd5d462120f2030251c948659105edfe76ee9a87646d16f3a7d
4
- data.tar.gz: e34e36ff80548f16e92aa9fd2b604161f74140caff317efec05ae48648bf732e
3
+ metadata.gz: 61b9b755594ffadcc05a80b942c0424a29043da9e01c81bc41045c5d440156e1
4
+ data.tar.gz: 823d4982ec931ecf197a42e37ceb7439939a4e6e06070d22ceb437eb1985f2e4
5
5
  SHA512:
6
- metadata.gz: bb6a2bade86267597f71fe64a549fd82b85f5376a3c4e57b4f2ecc96a5f944abc9f63112c0c50407c185ee41ffa8336a6f1328c866ee70ee21ebbd5eff0eac25
7
- data.tar.gz: 0ba54d2272dee5f4fa95ce8e614ef4a7bde2f1db07c3887adf4caee7495395fb387ec76288cd0fc59ed87b7af1fa9d137e4ea866c777a06aa510b147255bdfc7
6
+ metadata.gz: 0c2f235cd8a6f31bafd5d94af63806e3caabc0fff8743b82815ff90df255f74dbf6d924edccb57e5d73f0fef65958b1c7231e60e96bc9dde9aee3e94718ce0fd
7
+ data.tar.gz: 35d25847836f39bbd80fd7aa8ff07005c126cec030be1d1e9908a4d428d8f96cf2b662167cce8428314aefd7a1ce2384179dd8489e132f1635333e2ffd1dec48
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-04-09
11
+
12
+ ### Added
13
+ - `Queue#try_enqueue(item, timeout: nil)` and `Stack#try_push(item, timeout: nil)` non-blocking insertion variants
14
+ - `Queue#clear` and `Stack#clear` to discard all items and wake blocked producers
15
+
16
+ ## [0.2.0] - 2026-04-03
17
+
18
+ ### Added
19
+ - `drain` method to remove and return all items at once
20
+ - `each` and `to_a` for non-destructive iteration
21
+ - `close` / `closed?` for graceful shutdown semantics
22
+ - `ClosedError` raised when adding to a closed container
23
+
24
+ ## [0.1.5] - 2026-03-31
25
+
26
+ ### Added
27
+ - Add GitHub issue templates, dependabot config, and PR template
28
+
10
29
  ## [0.1.4] - 2026-03-31
11
30
 
12
31
  ### Changed
data/README.md CHANGED
@@ -66,6 +66,66 @@ s = Philiprehberger::QueueStack::Stack.new
66
66
  item = s.try_pop(timeout: 5) # waits up to 5 seconds
67
67
  ```
68
68
 
69
+ ### Drain
70
+
71
+ ```ruby
72
+ q = Philiprehberger::QueueStack::Queue.new
73
+ q.enqueue('a')
74
+ q.enqueue('b')
75
+ q.enqueue('c')
76
+ q.drain # => ['a', 'b', 'c'] (queue is now empty)
77
+
78
+ s = Philiprehberger::QueueStack::Stack.new
79
+ s.push('a')
80
+ s.push('b')
81
+ s.push('c')
82
+ s.drain # => ['c', 'b', 'a'] (stack is now empty)
83
+ ```
84
+
85
+ ### Iteration
86
+
87
+ ```ruby
88
+ q = Philiprehberger::QueueStack::Queue.new
89
+ q.enqueue('a')
90
+ q.enqueue('b')
91
+ q.each { |item| puts item } # prints 'a', 'b'
92
+ q.to_a # => ['a', 'b'] (queue unchanged)
93
+ ```
94
+
95
+ ### Non-Blocking Insertion
96
+
97
+ ```ruby
98
+ q = Philiprehberger::QueueStack::Queue.new(capacity: 1)
99
+ q.enqueue('a')
100
+ q.try_enqueue('b') # => false (full, no wait)
101
+ q.try_enqueue('b', timeout: 0.5) # => false after waiting up to 0.5s
102
+
103
+ s = Philiprehberger::QueueStack::Stack.new(capacity: 1)
104
+ s.push('a')
105
+ s.try_push('b') # => false (full, no wait)
106
+ ```
107
+
108
+ ### Clear
109
+
110
+ ```ruby
111
+ q = Philiprehberger::QueueStack::Queue.new
112
+ q.enqueue('a'); q.enqueue('b')
113
+ q.clear
114
+ q.empty? # => true
115
+ ```
116
+
117
+ ### Close / Shutdown
118
+
119
+ ```ruby
120
+ q = Philiprehberger::QueueStack::Queue.new
121
+ q.enqueue('a')
122
+ q.close
123
+ q.closed? # => true
124
+ q.dequeue # => 'a'
125
+ q.dequeue # => nil (closed and empty)
126
+ q.enqueue('b') # raises Philiprehberger::QueueStack::ClosedError
127
+ ```
128
+
69
129
  ### Capacity Limits
70
130
 
71
131
  ```ruby
@@ -84,9 +144,16 @@ q.full? # => true
84
144
  |--------|-------------|
85
145
  | `.new(capacity:)` | Create a queue with optional capacity limit |
86
146
  | `#enqueue(item)` | Add item to back (blocks if full) |
147
+ | `#try_enqueue(item, timeout: nil)` | Non-blocking enqueue, returns true/false (waits up to timeout if given) |
87
148
  | `#dequeue` | Remove and return front item (blocks if empty) |
88
149
  | `#try_dequeue(timeout:)` | Dequeue with timeout, returns nil on timeout |
150
+ | `#clear` | Remove all items without returning them |
89
151
  | `#peek` | View front item without removing |
152
+ | `#drain` | Remove and return all items as array (FIFO order) |
153
+ | `#each` | Iterate items without removing (returns Enumerator if no block) |
154
+ | `#to_a` | Snapshot as array (FIFO order) |
155
+ | `#close` | Mark as closed (new enqueues raise `ClosedError`) |
156
+ | `#closed?` | Whether the queue has been closed |
90
157
  | `#size` | Number of items |
91
158
  | `#empty?` | Whether the queue is empty |
92
159
  | `#full?` | Whether the queue is at capacity |
@@ -97,9 +164,16 @@ q.full? # => true
97
164
  |--------|-------------|
98
165
  | `.new(capacity:)` | Create a stack with optional capacity limit |
99
166
  | `#push(item)` | Push item on top (blocks if full) |
167
+ | `#try_push(item, timeout: nil)` | Non-blocking push, returns true/false (waits up to timeout if given) |
100
168
  | `#pop` | Remove and return top item (blocks if empty) |
101
169
  | `#try_pop(timeout:)` | Pop with timeout, returns nil on timeout |
170
+ | `#clear` | Remove all items without returning them |
102
171
  | `#peek` | View top item without removing |
172
+ | `#drain` | Remove and return all items as array (LIFO order) |
173
+ | `#each` | Iterate items without removing (returns Enumerator if no block) |
174
+ | `#to_a` | Snapshot as array (LIFO order) |
175
+ | `#close` | Mark as closed (new pushes raise `ClosedError`) |
176
+ | `#closed?` | Whether the stack has been closed |
103
177
  | `#size` | Number of items |
104
178
  | `#empty?` | Whether the stack is empty |
105
179
  | `#full?` | Whether the stack is at capacity |
@@ -9,12 +9,15 @@ module Philiprehberger
9
9
  # q.enqueue('item')
10
10
  # q.dequeue # => 'item'
11
11
  class Queue
12
+ include Enumerable
13
+
12
14
  # Create a new queue.
13
15
  #
14
16
  # @param capacity [Integer, nil] maximum number of items (nil for unlimited)
15
17
  def initialize(capacity: nil)
16
18
  @items = []
17
19
  @capacity = capacity
20
+ @closed = false
18
21
  @mutex = Mutex.new
19
22
  @not_empty = ConditionVariable.new
20
23
  @not_full = ConditionVariable.new
@@ -24,26 +27,75 @@ module Philiprehberger
24
27
  #
25
28
  # @param item [Object] the item to enqueue
26
29
  # @return [void]
30
+ # @raise [ClosedError] if the queue has been closed
27
31
  def enqueue(item)
28
32
  @mutex.synchronize do
33
+ raise ClosedError, 'cannot enqueue on a closed queue' if @closed
34
+
29
35
  @not_full.wait(@mutex) while @capacity && @items.length >= @capacity
30
36
  @items.push(item)
31
37
  @not_empty.signal
32
38
  end
33
39
  end
34
40
 
35
- # Remove and return the front item. Blocks if empty.
41
+ # Remove and return the front item. Blocks if empty (returns nil if closed and empty).
36
42
  #
37
- # @return [Object] the dequeued item
43
+ # @return [Object, nil] the dequeued item or nil if closed and empty
38
44
  def dequeue
39
45
  @mutex.synchronize do
40
- @not_empty.wait(@mutex) while @items.empty?
46
+ while @items.empty?
47
+ return nil if @closed
48
+
49
+ @not_empty.wait(@mutex)
50
+ end
41
51
  item = @items.shift
42
52
  @not_full.signal
43
53
  item
44
54
  end
45
55
  end
46
56
 
57
+ # Try to enqueue an item without blocking indefinitely.
58
+ #
59
+ # With timeout: nil, returns immediately. With a numeric timeout, waits up to
60
+ # that many seconds for space to become available.
61
+ #
62
+ # @param item [Object] the item to enqueue
63
+ # @param timeout [Numeric, nil] seconds to wait, or nil for non-blocking
64
+ # @return [Boolean] true if enqueued, false if full (or timed out)
65
+ # @raise [ClosedError] if the queue has been closed
66
+ def try_enqueue(item, timeout: nil)
67
+ @mutex.synchronize do
68
+ raise ClosedError, 'cannot enqueue on a closed queue' if @closed
69
+
70
+ if @capacity && @items.length >= @capacity
71
+ return false if timeout.nil? || timeout <= 0
72
+
73
+ deadline = Time.now + timeout
74
+ while @items.length >= @capacity
75
+ remaining = deadline - Time.now
76
+ return false if remaining <= 0
77
+
78
+ @not_full.wait(@mutex, remaining)
79
+ raise ClosedError, 'cannot enqueue on a closed queue' if @closed
80
+ end
81
+ end
82
+
83
+ @items.push(item)
84
+ @not_empty.signal
85
+ true
86
+ end
87
+ end
88
+
89
+ # Remove all items without returning them. Signals any blocked producers.
90
+ #
91
+ # @return [void]
92
+ def clear
93
+ @mutex.synchronize do
94
+ @items.clear
95
+ @not_full.broadcast
96
+ end
97
+ end
98
+
47
99
  # Try to dequeue an item with a timeout.
48
100
  #
49
101
  # @param timeout [Numeric] seconds to wait
@@ -52,6 +104,8 @@ module Philiprehberger
52
104
  deadline = Time.now + timeout
53
105
  @mutex.synchronize do
54
106
  while @items.empty?
107
+ return nil if @closed
108
+
55
109
  remaining = deadline - Time.now
56
110
  return nil if remaining <= 0
57
111
 
@@ -63,6 +117,57 @@ module Philiprehberger
63
117
  end
64
118
  end
65
119
 
120
+ # Remove and return all items as an array (FIFO order). Non-blocking.
121
+ #
122
+ # @return [Array] all items in FIFO order
123
+ def drain
124
+ @mutex.synchronize do
125
+ result = @items.dup
126
+ @items.clear
127
+ @not_full.broadcast
128
+ result
129
+ end
130
+ end
131
+
132
+ # Iterate items without removing them (snapshot of current state, FIFO order).
133
+ # Returns an Enumerator if no block is given.
134
+ #
135
+ # @yield [item] each item in FIFO order
136
+ # @return [Enumerator, self]
137
+ def each(&block)
138
+ snapshot = @mutex.synchronize { @items.dup }
139
+ return snapshot.each unless block
140
+
141
+ snapshot.each(&block)
142
+ self
143
+ end
144
+
145
+ # Return a snapshot of items as an array (FIFO order).
146
+ #
147
+ # @return [Array]
148
+ def to_a
149
+ @mutex.synchronize { @items.dup }
150
+ end
151
+
152
+ # Mark the queue as closed. New enqueue calls will raise ClosedError.
153
+ # Existing items can still be dequeued. Wakes all waiting threads.
154
+ #
155
+ # @return [void]
156
+ def close
157
+ @mutex.synchronize do
158
+ @closed = true
159
+ @not_empty.broadcast
160
+ @not_full.broadcast
161
+ end
162
+ end
163
+
164
+ # Whether the queue has been closed.
165
+ #
166
+ # @return [Boolean]
167
+ def closed?
168
+ @mutex.synchronize { @closed }
169
+ end
170
+
66
171
  # Peek at the front item without removing it.
67
172
  #
68
173
  # @return [Object, nil] the front item or nil if empty
@@ -9,12 +9,15 @@ module Philiprehberger
9
9
  # s.push('item')
10
10
  # s.pop # => 'item'
11
11
  class Stack
12
+ include Enumerable
13
+
12
14
  # Create a new stack.
13
15
  #
14
16
  # @param capacity [Integer, nil] maximum number of items (nil for unlimited)
15
17
  def initialize(capacity: nil)
16
18
  @items = []
17
19
  @capacity = capacity
20
+ @closed = false
18
21
  @mutex = Mutex.new
19
22
  @not_empty = ConditionVariable.new
20
23
  @not_full = ConditionVariable.new
@@ -24,26 +27,75 @@ module Philiprehberger
24
27
  #
25
28
  # @param item [Object] the item to push
26
29
  # @return [void]
30
+ # @raise [ClosedError] if the stack has been closed
27
31
  def push(item)
28
32
  @mutex.synchronize do
33
+ raise ClosedError, 'cannot push on a closed stack' if @closed
34
+
29
35
  @not_full.wait(@mutex) while @capacity && @items.length >= @capacity
30
36
  @items.push(item)
31
37
  @not_empty.signal
32
38
  end
33
39
  end
34
40
 
35
- # Pop and return the top item. Blocks if empty.
41
+ # Pop and return the top item. Blocks if empty (returns nil if closed and empty).
36
42
  #
37
- # @return [Object] the popped item
43
+ # @return [Object, nil] the popped item or nil if closed and empty
38
44
  def pop
39
45
  @mutex.synchronize do
40
- @not_empty.wait(@mutex) while @items.empty?
46
+ while @items.empty?
47
+ return nil if @closed
48
+
49
+ @not_empty.wait(@mutex)
50
+ end
41
51
  item = @items.pop
42
52
  @not_full.signal
43
53
  item
44
54
  end
45
55
  end
46
56
 
57
+ # Try to push an item without blocking indefinitely.
58
+ #
59
+ # With timeout: nil, returns immediately. With a numeric timeout, waits up to
60
+ # that many seconds for space to become available.
61
+ #
62
+ # @param item [Object] the item to push
63
+ # @param timeout [Numeric, nil] seconds to wait, or nil for non-blocking
64
+ # @return [Boolean] true if pushed, false if full (or timed out)
65
+ # @raise [ClosedError] if the stack has been closed
66
+ def try_push(item, timeout: nil)
67
+ @mutex.synchronize do
68
+ raise ClosedError, 'cannot push on a closed stack' if @closed
69
+
70
+ if @capacity && @items.length >= @capacity
71
+ return false if timeout.nil? || timeout <= 0
72
+
73
+ deadline = Time.now + timeout
74
+ while @items.length >= @capacity
75
+ remaining = deadline - Time.now
76
+ return false if remaining <= 0
77
+
78
+ @not_full.wait(@mutex, remaining)
79
+ raise ClosedError, 'cannot push on a closed stack' if @closed
80
+ end
81
+ end
82
+
83
+ @items.push(item)
84
+ @not_empty.signal
85
+ true
86
+ end
87
+ end
88
+
89
+ # Remove all items without returning them. Signals any blocked producers.
90
+ #
91
+ # @return [void]
92
+ def clear
93
+ @mutex.synchronize do
94
+ @items.clear
95
+ @not_full.broadcast
96
+ end
97
+ end
98
+
47
99
  # Try to pop an item with a timeout.
48
100
  #
49
101
  # @param timeout [Numeric] seconds to wait
@@ -52,6 +104,8 @@ module Philiprehberger
52
104
  deadline = Time.now + timeout
53
105
  @mutex.synchronize do
54
106
  while @items.empty?
107
+ return nil if @closed
108
+
55
109
  remaining = deadline - Time.now
56
110
  return nil if remaining <= 0
57
111
 
@@ -63,6 +117,57 @@ module Philiprehberger
63
117
  end
64
118
  end
65
119
 
120
+ # Remove and return all items as an array (LIFO order, top first). Non-blocking.
121
+ #
122
+ # @return [Array] all items in LIFO order (top first)
123
+ def drain
124
+ @mutex.synchronize do
125
+ result = @items.reverse
126
+ @items.clear
127
+ @not_full.broadcast
128
+ result
129
+ end
130
+ end
131
+
132
+ # Iterate items without removing them (snapshot of current state, LIFO order).
133
+ # Returns an Enumerator if no block is given.
134
+ #
135
+ # @yield [item] each item in LIFO order (top first)
136
+ # @return [Enumerator, self]
137
+ def each(&block)
138
+ snapshot = @mutex.synchronize { @items.reverse }
139
+ return snapshot.each unless block
140
+
141
+ snapshot.each(&block)
142
+ self
143
+ end
144
+
145
+ # Return a snapshot of items as an array (LIFO order, top first).
146
+ #
147
+ # @return [Array]
148
+ def to_a
149
+ @mutex.synchronize { @items.reverse }
150
+ end
151
+
152
+ # Mark the stack as closed. New push calls will raise ClosedError.
153
+ # Existing items can still be popped. Wakes all waiting threads.
154
+ #
155
+ # @return [void]
156
+ def close
157
+ @mutex.synchronize do
158
+ @closed = true
159
+ @not_empty.broadcast
160
+ @not_full.broadcast
161
+ end
162
+ end
163
+
164
+ # Whether the stack has been closed.
165
+ #
166
+ # @return [Boolean]
167
+ def closed?
168
+ @mutex.synchronize { @closed }
169
+ end
170
+
66
171
  # Peek at the top item without removing it.
67
172
  #
68
173
  # @return [Object, nil] the top item or nil if empty
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module QueueStack
5
- VERSION = '0.1.4'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -7,5 +7,6 @@ require_relative 'queue_stack/stack'
7
7
  module Philiprehberger
8
8
  module QueueStack
9
9
  class Error < StandardError; end
10
+ class ClosedError < Error; end
10
11
  end
11
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-queue_stack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-31 00:00:00.000000000 Z
11
+ date: 2026-04-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Thread-safe queue and stack data structures with configurable capacity
14
14
  limits, blocking enqueue/dequeue with timeouts, and peek operations. Uses Mutex
@@ -26,11 +26,11 @@ files:
26
26
  - lib/philiprehberger/queue_stack/queue.rb
27
27
  - lib/philiprehberger/queue_stack/stack.rb
28
28
  - lib/philiprehberger/queue_stack/version.rb
29
- homepage: https://github.com/philiprehberger/rb-queue-stack
29
+ homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-queue_stack
30
30
  licenses:
31
31
  - MIT
32
32
  metadata:
33
- homepage_uri: https://github.com/philiprehberger/rb-queue-stack
33
+ homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-queue_stack
34
34
  source_code_uri: https://github.com/philiprehberger/rb-queue-stack
35
35
  changelog_uri: https://github.com/philiprehberger/rb-queue-stack/blob/main/CHANGELOG.md
36
36
  bug_tracker_uri: https://github.com/philiprehberger/rb-queue-stack/issues