faye-redis-ng 1.0.2 → 1.0.4
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 +46 -1
 - data/LICENSE +1 -1
 - data/README.md +1 -1
 - data/lib/faye/redis/client_registry.rb +33 -4
 - data/lib/faye/redis/message_queue.rb +34 -95
 - data/lib/faye/redis/pubsub_coordinator.rb +2 -2
 - data/lib/faye/redis/subscription_manager.rb +19 -0
 - data/lib/faye/redis/version.rb +1 -1
 - data/lib/faye/redis.rb +48 -14
 - metadata +2 -2
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: fde6220c9baee883a47eced86a4cd7245ecca48a05a55c906660a5286260c401
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: dd3e0d6190cc019070da513d57ac076537b7d8aa1626e29b17c1f7fb91910b79
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 968761695fa0cc17df7c810a345068bf0de18437fd909eba1ed803f784daff107734f846106f67154cfe1c5a3295905c61f1863de40a59a5304199147144fd80
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 8137b865b357b20024edbbb870ff51b76b44779eebe3db18e77167ef326a64322adae9beec35adad04c54028236f6b30c87736fa67642b2684ac9ef73a951c13
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [1.0.4] - 2025-10-15
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Performance
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **Major Message Delivery Optimization**: Significantly improved message publishing and delivery performance
         
     | 
| 
      
 14 
     | 
    
         
            +
              - Reduced Redis operations for message enqueue from 4 to 2 per message (50% reduction)
         
     | 
| 
      
 15 
     | 
    
         
            +
              - Reduced Redis operations for message dequeue from 2N+1 to 2 atomic operations (90%+ reduction for N messages)
         
     | 
| 
      
 16 
     | 
    
         
            +
              - Changed publish flow from sequential to parallel execution
         
     | 
| 
      
 17 
     | 
    
         
            +
              - Added batch enqueue operation using Redis pipelining for multiple clients
         
     | 
| 
      
 18 
     | 
    
         
            +
              - Reduced network round trips from N to 1 when publishing to multiple clients
         
     | 
| 
      
 19 
     | 
    
         
            +
              - **Overall latency improvement: 60-80% faster message delivery** (depending on subscriber count)
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 22 
     | 
    
         
            +
            - **Message Storage**: Simplified message storage structure
         
     | 
| 
      
 23 
     | 
    
         
            +
              - Messages now stored directly as JSON in Redis lists instead of using separate hash + list
         
     | 
| 
      
 24 
     | 
    
         
            +
              - Maintains message UUID for uniqueness and traceability
         
     | 
| 
      
 25 
     | 
    
         
            +
              - More efficient use of Redis memory and operations
         
     | 
| 
      
 26 
     | 
    
         
            +
            - **Publish Mechanism**: Refactored publish method to execute pub/sub and enqueue operations in parallel
         
     | 
| 
      
 27 
     | 
    
         
            +
              - Eliminates sequential waiting bottleneck
         
     | 
| 
      
 28 
     | 
    
         
            +
              - Uses single Redis pipeline for batch client enqueue operations
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 31 
     | 
    
         
            +
            For 100 subscribers receiving one message:
         
     | 
| 
      
 32 
     | 
    
         
            +
            - Before: 400 Redis operations (sequential), 100 network round trips, ~200-500ms latency
         
     | 
| 
      
 33 
     | 
    
         
            +
            - After: 200 Redis operations (parallel + pipelined), 1 network round trip, ~20-50ms latency
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            ## [1.0.3] - 2025-10-06
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 38 
     | 
    
         
            +
            - **Memory Leak**: Fixed `MessageQueue.clear` to properly delete message data instead of only clearing queue index
         
     | 
| 
      
 39 
     | 
    
         
            +
            - **Resource Cleanup**: Fixed `destroy_client` to clear message queue before destroying client
         
     | 
| 
      
 40 
     | 
    
         
            +
            - **Race Condition**: Fixed `publish` callback timing to wait for all async operations to complete
         
     | 
| 
      
 41 
     | 
    
         
            +
            - **Pattern Cleanup**: Fixed `SubscriptionManager` to properly clean up wildcard patterns when last subscriber unsubscribes
         
     | 
| 
      
 42 
     | 
    
         
            +
            - **Thread Safety**: Fixed `PubSubCoordinator` to use array duplication when iterating subscribers to prevent concurrent modification
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 45 
     | 
    
         
            +
            - **Performance**: Optimized `cleanup_expired` to use batch pipelined operations instead of individual checks
         
     | 
| 
      
 46 
     | 
    
         
            +
              - Reduces Redis calls from O(n²) to O(n) for n clients
         
     | 
| 
      
 47 
     | 
    
         
            +
              - Returns count of cleaned clients via callback
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            ### Improved
         
     | 
| 
      
 50 
     | 
    
         
            +
            - **Test Coverage**: Increased line coverage from 90% to 95.83% (528/551 lines)
         
     | 
| 
      
 51 
     | 
    
         
            +
            - Added comprehensive tests for cleanup operations and wildcard pattern management
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
       10 
53 
     | 
    
         
             
            ## [1.0.2] - 2025-10-06
         
     | 
| 
       11 
54 
     | 
    
         | 
| 
       12 
55 
     | 
    
         
             
            ### Fixed
         
     | 
| 
         @@ -47,7 +90,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       47 
90 
     | 
    
         
             
            ### Security
         
     | 
| 
       48 
91 
     | 
    
         
             
            - Client and message IDs now use `SecureRandom.uuid` instead of predictable time-based generation
         
     | 
| 
       49 
92 
     | 
    
         | 
| 
       50 
     | 
    
         
            -
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0. 
     | 
| 
      
 93 
     | 
    
         
            +
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.4...HEAD
         
     | 
| 
      
 94 
     | 
    
         
            +
            [1.0.4]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.3...v1.0.4
         
     | 
| 
      
 95 
     | 
    
         
            +
            [1.0.3]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.2...v1.0.3
         
     | 
| 
       51 
96 
     | 
    
         
             
            [1.0.2]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.1...v1.0.2
         
     | 
| 
       52 
97 
     | 
    
         
             
            [1.0.1]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.0...v1.0.1
         
     | 
| 
       53 
98 
     | 
    
         
             
            [1.0.0]: https://github.com/7a6163/faye-redis-ng/releases/tag/v1.0.0
         
     | 
    
        data/LICENSE
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | 
         @@ -268,4 +268,4 @@ MIT License - see LICENSE file for details 
     | 
|
| 
       268 
268 
     | 
    
         
             
            ## Acknowledgments
         
     | 
| 
       269 
269 
     | 
    
         | 
| 
       270 
270 
     | 
    
         
             
            - Built for the [Faye](https://faye.jcoglan.com/) messaging system
         
     | 
| 
       271 
     | 
    
         
            -
            - Inspired by the original faye-redis gem
         
     | 
| 
      
 271 
     | 
    
         
            +
            - Inspired by the original [faye-redis](https://github.com/faye/faye-redis-ruby) gem
         
     | 
| 
         @@ -110,14 +110,43 @@ module Faye 
     | 
|
| 
       110 
110 
     | 
    
         
             
                  end
         
     | 
| 
       111 
111 
     | 
    
         | 
| 
       112 
112 
     | 
    
         
             
                  # Clean up expired clients
         
     | 
| 
       113 
     | 
    
         
            -
                  def cleanup_expired
         
     | 
| 
      
 113 
     | 
    
         
            +
                  def cleanup_expired(&callback)
         
     | 
| 
       114 
114 
     | 
    
         
             
                    all do |client_ids|
         
     | 
| 
       115 
     | 
    
         
            -
                       
     | 
| 
       116 
     | 
    
         
            -
             
     | 
| 
       117 
     | 
    
         
            -
             
     | 
| 
      
 115 
     | 
    
         
            +
                      # Check existence in batch using pipelined commands
         
     | 
| 
      
 116 
     | 
    
         
            +
                      results = @connection.with_redis do |redis|
         
     | 
| 
      
 117 
     | 
    
         
            +
                        redis.pipelined do |pipeline|
         
     | 
| 
      
 118 
     | 
    
         
            +
                          client_ids.each do |client_id|
         
     | 
| 
      
 119 
     | 
    
         
            +
                            pipeline.exists?(client_key(client_id))
         
     | 
| 
      
 120 
     | 
    
         
            +
                          end
         
     | 
| 
       118 
121 
     | 
    
         
             
                        end
         
     | 
| 
       119 
122 
     | 
    
         
             
                      end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                      # Collect expired client IDs
         
     | 
| 
      
 125 
     | 
    
         
            +
                      expired_clients = []
         
     | 
| 
      
 126 
     | 
    
         
            +
                      client_ids.each_with_index do |client_id, index|
         
     | 
| 
      
 127 
     | 
    
         
            +
                        result = results[index]
         
     | 
| 
      
 128 
     | 
    
         
            +
                        # Redis 5.x returns boolean, older versions return integer
         
     | 
| 
      
 129 
     | 
    
         
            +
                        exists = result.is_a?(Integer) ? result > 0 : result
         
     | 
| 
      
 130 
     | 
    
         
            +
                        expired_clients << client_id unless exists
         
     | 
| 
      
 131 
     | 
    
         
            +
                      end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                      # Batch delete expired clients
         
     | 
| 
      
 134 
     | 
    
         
            +
                      if expired_clients.any?
         
     | 
| 
      
 135 
     | 
    
         
            +
                        @connection.with_redis do |redis|
         
     | 
| 
      
 136 
     | 
    
         
            +
                          redis.pipelined do |pipeline|
         
     | 
| 
      
 137 
     | 
    
         
            +
                            expired_clients.each do |client_id|
         
     | 
| 
      
 138 
     | 
    
         
            +
                              pipeline.del(client_key(client_id))
         
     | 
| 
      
 139 
     | 
    
         
            +
                              pipeline.srem?(clients_index_key, client_id)
         
     | 
| 
      
 140 
     | 
    
         
            +
                            end
         
     | 
| 
      
 141 
     | 
    
         
            +
                          end
         
     | 
| 
      
 142 
     | 
    
         
            +
                        end
         
     | 
| 
      
 143 
     | 
    
         
            +
                      end
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                      EventMachine.next_tick { callback.call(expired_clients.size) } if callback
         
     | 
| 
       120 
146 
     | 
    
         
             
                    end
         
     | 
| 
      
 147 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 148 
     | 
    
         
            +
                    log_error("Failed to cleanup expired clients: #{e.message}")
         
     | 
| 
      
 149 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(0) } if callback
         
     | 
| 
       121 
150 
     | 
    
         
             
                  end
         
     | 
| 
       122 
151 
     | 
    
         | 
| 
       123 
152 
     | 
    
         
             
                  private
         
     | 
| 
         @@ -13,30 +13,20 @@ module Faye 
     | 
|
| 
       13 
13 
     | 
    
         | 
| 
       14 
14 
     | 
    
         
             
                  # Enqueue a message for a client
         
     | 
| 
       15 
15 
     | 
    
         
             
                  def enqueue(client_id, message, &callback)
         
     | 
| 
       16 
     | 
    
         
            -
                     
     | 
| 
       17 
     | 
    
         
            -
                     
     | 
| 
      
 16 
     | 
    
         
            +
                    # Add unique ID if not present (for message deduplication)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    message_with_id = message.dup
         
     | 
| 
      
 18 
     | 
    
         
            +
                    message_with_id['id'] ||= generate_message_id
         
     | 
| 
       18 
19 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
                     
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
                      channel: message['channel'],
         
     | 
| 
       22 
     | 
    
         
            -
                      data: message['data'],
         
     | 
| 
       23 
     | 
    
         
            -
                      client_id: message['clientId'],
         
     | 
| 
       24 
     | 
    
         
            -
                      timestamp: timestamp
         
     | 
| 
       25 
     | 
    
         
            -
                    }
         
     | 
| 
      
 20 
     | 
    
         
            +
                    # Store message directly as JSON
         
     | 
| 
      
 21 
     | 
    
         
            +
                    message_json = message_with_id.to_json
         
     | 
| 
       26 
22 
     | 
    
         | 
| 
       27 
23 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
| 
       28 
     | 
    
         
            -
                       
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
                        multi.hset(message_key(message_id), message_data.transform_keys(&:to_s).transform_values { |v| v.to_json })
         
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
      
 24 
     | 
    
         
            +
                      # Use RPUSH with EXPIRE in a single pipeline
         
     | 
| 
      
 25 
     | 
    
         
            +
                      redis.pipelined do |pipeline|
         
     | 
| 
       32 
26 
     | 
    
         
             
                        # Add message to client's queue
         
     | 
| 
       33 
     | 
    
         
            -
                         
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
                         
     | 
| 
       36 
     | 
    
         
            -
                        multi.expire(message_key(message_id), message_ttl)
         
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
                        # Set TTL on queue
         
     | 
| 
       39 
     | 
    
         
            -
                        multi.expire(queue_key(client_id), message_ttl)
         
     | 
| 
      
 27 
     | 
    
         
            +
                        pipeline.rpush(queue_key(client_id), message_json)
         
     | 
| 
      
 28 
     | 
    
         
            +
                        # Set TTL on queue (only if it doesn't already have one)
         
     | 
| 
      
 29 
     | 
    
         
            +
                        pipeline.expire(queue_key(client_id), message_ttl)
         
     | 
| 
       40 
30 
     | 
    
         
             
                      end
         
     | 
| 
       41 
31 
     | 
    
         
             
                    end
         
     | 
| 
       42 
32 
     | 
    
         | 
| 
         @@ -48,50 +38,25 @@ module Faye 
     | 
|
| 
       48 
38 
     | 
    
         | 
| 
       49 
39 
     | 
    
         
             
                  # Dequeue all messages for a client
         
     | 
| 
       50 
40 
     | 
    
         
             
                  def dequeue_all(client_id, &callback)
         
     | 
| 
       51 
     | 
    
         
            -
                    # Get all  
     | 
| 
       52 
     | 
    
         
            -
                     
     | 
| 
       53 
     | 
    
         
            -
                      redis.lrange(queue_key(client_id), 0, -1)
         
     | 
| 
       54 
     | 
    
         
            -
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
                    # Get all messages and delete queue in a single atomic operation
         
     | 
| 
      
 42 
     | 
    
         
            +
                    key = queue_key(client_id)
         
     | 
| 
       55 
43 
     | 
    
         | 
| 
       56 
     | 
    
         
            -
                     
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
             
     | 
| 
       60 
     | 
    
         
            -
                         
     | 
| 
       61 
     | 
    
         
            -
                          message_ids.each do |message_id|
         
     | 
| 
       62 
     | 
    
         
            -
                            pipeline.hgetall(message_key(message_id))
         
     | 
| 
       63 
     | 
    
         
            -
                          end
         
     | 
| 
       64 
     | 
    
         
            -
                        end.each do |data|
         
     | 
| 
       65 
     | 
    
         
            -
                          next if data.nil? || data.empty?
         
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
                          # Parse JSON values
         
     | 
| 
       68 
     | 
    
         
            -
                          parsed_data = data.transform_values do |v|
         
     | 
| 
       69 
     | 
    
         
            -
                            begin
         
     | 
| 
       70 
     | 
    
         
            -
                              JSON.parse(v)
         
     | 
| 
       71 
     | 
    
         
            -
                            rescue JSON::ParserError
         
     | 
| 
       72 
     | 
    
         
            -
                              v
         
     | 
| 
       73 
     | 
    
         
            -
                            end
         
     | 
| 
       74 
     | 
    
         
            -
                          end
         
     | 
| 
       75 
     | 
    
         
            -
             
     | 
| 
       76 
     | 
    
         
            -
                          # Convert to Faye message format
         
     | 
| 
       77 
     | 
    
         
            -
                          messages << {
         
     | 
| 
       78 
     | 
    
         
            -
                            'channel' => parsed_data['channel'],
         
     | 
| 
       79 
     | 
    
         
            -
                            'data' => parsed_data['data'],
         
     | 
| 
       80 
     | 
    
         
            -
                            'clientId' => parsed_data['client_id'],
         
     | 
| 
       81 
     | 
    
         
            -
                            'id' => parsed_data['id']
         
     | 
| 
       82 
     | 
    
         
            -
                          }
         
     | 
| 
       83 
     | 
    
         
            -
                        end
         
     | 
| 
      
 44 
     | 
    
         
            +
                    json_messages = @connection.with_redis do |redis|
         
     | 
| 
      
 45 
     | 
    
         
            +
                      # Use MULTI/EXEC to atomically get and delete
         
     | 
| 
      
 46 
     | 
    
         
            +
                      redis.multi do |multi|
         
     | 
| 
      
 47 
     | 
    
         
            +
                        multi.lrange(key, 0, -1)
         
     | 
| 
      
 48 
     | 
    
         
            +
                        multi.del(key)
         
     | 
| 
       84 
49 
     | 
    
         
             
                      end
         
     | 
| 
       85 
50 
     | 
    
         
             
                    end
         
     | 
| 
       86 
51 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                    #  
     | 
| 
       88 
     | 
    
         
            -
                     
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
                           
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
                           
     | 
| 
      
 52 
     | 
    
         
            +
                    # Parse messages from JSON
         
     | 
| 
      
 53 
     | 
    
         
            +
                    messages = []
         
     | 
| 
      
 54 
     | 
    
         
            +
                    if json_messages && json_messages[0]
         
     | 
| 
      
 55 
     | 
    
         
            +
                      json_messages[0].each do |json|
         
     | 
| 
      
 56 
     | 
    
         
            +
                        begin
         
     | 
| 
      
 57 
     | 
    
         
            +
                          messages << JSON.parse(json)
         
     | 
| 
      
 58 
     | 
    
         
            +
                        rescue JSON::ParserError => e
         
     | 
| 
      
 59 
     | 
    
         
            +
                          log_error("Failed to parse message JSON: #{e.message}")
         
     | 
| 
       95 
60 
     | 
    
         
             
                        end
         
     | 
| 
       96 
61 
     | 
    
         
             
                      end
         
     | 
| 
       97 
62 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -106,12 +71,17 @@ module Faye 
     | 
|
| 
       106 
71 
     | 
    
         | 
| 
       107 
72 
     | 
    
         
             
                  # Peek at messages without removing them
         
     | 
| 
       108 
73 
     | 
    
         
             
                  def peek(client_id, limit = 10, &callback)
         
     | 
| 
       109 
     | 
    
         
            -
                     
     | 
| 
      
 74 
     | 
    
         
            +
                    json_messages = @connection.with_redis do |redis|
         
     | 
| 
       110 
75 
     | 
    
         
             
                      redis.lrange(queue_key(client_id), 0, limit - 1)
         
     | 
| 
       111 
76 
     | 
    
         
             
                    end
         
     | 
| 
       112 
77 
     | 
    
         | 
| 
       113 
     | 
    
         
            -
                    messages =  
     | 
| 
       114 
     | 
    
         
            -
                       
     | 
| 
      
 78 
     | 
    
         
            +
                    messages = json_messages.map do |json|
         
     | 
| 
      
 79 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 80 
     | 
    
         
            +
                        JSON.parse(json)
         
     | 
| 
      
 81 
     | 
    
         
            +
                      rescue JSON::ParserError => e
         
     | 
| 
      
 82 
     | 
    
         
            +
                        log_error("Failed to parse message JSON: #{e.message}")
         
     | 
| 
      
 83 
     | 
    
         
            +
                        nil
         
     | 
| 
      
 84 
     | 
    
         
            +
                      end
         
     | 
| 
       115 
85 
     | 
    
         
             
                    end.compact
         
     | 
| 
       116 
86 
     | 
    
         | 
| 
       117 
87 
     | 
    
         
             
                    EventMachine.next_tick { callback.call(messages) } if callback
         
     | 
| 
         @@ -138,6 +108,7 @@ module Faye 
     | 
|
| 
       138 
108 
     | 
    
         | 
| 
       139 
109 
     | 
    
         
             
                  # Clear a client's message queue
         
     | 
| 
       140 
110 
     | 
    
         
             
                  def clear(client_id, &callback)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    # Simply delete the queue
         
     | 
| 
       141 
112 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
| 
       142 
113 
     | 
    
         
             
                      redis.del(queue_key(client_id))
         
     | 
| 
       143 
114 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -150,42 +121,10 @@ module Faye 
     | 
|
| 
       150 
121 
     | 
    
         | 
| 
       151 
122 
     | 
    
         
             
                  private
         
     | 
| 
       152 
123 
     | 
    
         | 
| 
       153 
     | 
    
         
            -
                  def fetch_message(message_id)
         
     | 
| 
       154 
     | 
    
         
            -
                    data = @connection.with_redis do |redis|
         
     | 
| 
       155 
     | 
    
         
            -
                      redis.hgetall(message_key(message_id))
         
     | 
| 
       156 
     | 
    
         
            -
                    end
         
     | 
| 
       157 
     | 
    
         
            -
             
     | 
| 
       158 
     | 
    
         
            -
                    return nil if data.empty?
         
     | 
| 
       159 
     | 
    
         
            -
             
     | 
| 
       160 
     | 
    
         
            -
                    # Parse JSON values
         
     | 
| 
       161 
     | 
    
         
            -
                    parsed_data = data.transform_values do |v|
         
     | 
| 
       162 
     | 
    
         
            -
                      begin
         
     | 
| 
       163 
     | 
    
         
            -
                        JSON.parse(v)
         
     | 
| 
       164 
     | 
    
         
            -
                      rescue JSON::ParserError
         
     | 
| 
       165 
     | 
    
         
            -
                        v
         
     | 
| 
       166 
     | 
    
         
            -
                      end
         
     | 
| 
       167 
     | 
    
         
            -
                    end
         
     | 
| 
       168 
     | 
    
         
            -
             
     | 
| 
       169 
     | 
    
         
            -
                    # Convert to Faye message format
         
     | 
| 
       170 
     | 
    
         
            -
                    {
         
     | 
| 
       171 
     | 
    
         
            -
                      'channel' => parsed_data['channel'],
         
     | 
| 
       172 
     | 
    
         
            -
                      'data' => parsed_data['data'],
         
     | 
| 
       173 
     | 
    
         
            -
                      'clientId' => parsed_data['client_id'],
         
     | 
| 
       174 
     | 
    
         
            -
                      'id' => parsed_data['id']
         
     | 
| 
       175 
     | 
    
         
            -
                    }
         
     | 
| 
       176 
     | 
    
         
            -
                  rescue => e
         
     | 
| 
       177 
     | 
    
         
            -
                    log_error("Failed to fetch message #{message_id}: #{e.message}")
         
     | 
| 
       178 
     | 
    
         
            -
                    nil
         
     | 
| 
       179 
     | 
    
         
            -
                  end
         
     | 
| 
       180 
     | 
    
         
            -
             
     | 
| 
       181 
124 
     | 
    
         
             
                  def queue_key(client_id)
         
     | 
| 
       182 
125 
     | 
    
         
             
                    namespace_key("messages:#{client_id}")
         
     | 
| 
       183 
126 
     | 
    
         
             
                  end
         
     | 
| 
       184 
127 
     | 
    
         | 
| 
       185 
     | 
    
         
            -
                  def message_key(message_id)
         
     | 
| 
       186 
     | 
    
         
            -
                    namespace_key("message:#{message_id}")
         
     | 
| 
       187 
     | 
    
         
            -
                  end
         
     | 
| 
       188 
     | 
    
         
            -
             
     | 
| 
       189 
128 
     | 
    
         
             
                  def namespace_key(key)
         
     | 
| 
       190 
129 
     | 
    
         
             
                    namespace = @options[:namespace] || 'faye'
         
     | 
| 
       191 
130 
     | 
    
         
             
                    "#{namespace}:#{key}"
         
     | 
| 
         @@ -166,9 +166,9 @@ module Faye 
     | 
|
| 
       166 
166 
     | 
    
         
             
                    begin
         
     | 
| 
       167 
167 
     | 
    
         
             
                      message = JSON.parse(message_json)
         
     | 
| 
       168 
168 
     | 
    
         | 
| 
       169 
     | 
    
         
            -
                      # Notify all subscribers
         
     | 
| 
      
 169 
     | 
    
         
            +
                      # Notify all subscribers (use dup to avoid concurrent modification)
         
     | 
| 
       170 
170 
     | 
    
         
             
                      EventMachine.next_tick do
         
     | 
| 
       171 
     | 
    
         
            -
                        @subscribers.each do |subscriber|
         
     | 
| 
      
 171 
     | 
    
         
            +
                        @subscribers.dup.each do |subscriber|
         
     | 
| 
       172 
172 
     | 
    
         
             
                          subscriber.call(channel, message)
         
     | 
| 
       173 
173 
     | 
    
         
             
                        end
         
     | 
| 
       174 
174 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -56,6 +56,11 @@ module Faye 
     | 
|
| 
       56 
56 
     | 
    
         
             
                      end
         
     | 
| 
       57 
57 
     | 
    
         
             
                    end
         
     | 
| 
       58 
58 
     | 
    
         | 
| 
      
 59 
     | 
    
         
            +
                    # Clean up wildcard pattern if no more subscribers
         
     | 
| 
      
 60 
     | 
    
         
            +
                    if channel.include?('*')
         
     | 
| 
      
 61 
     | 
    
         
            +
                      cleanup_pattern_if_unused(channel)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
       59 
64 
     | 
    
         
             
                    EventMachine.next_tick { callback.call(true) } if callback
         
     | 
| 
       60 
65 
     | 
    
         
             
                  rescue => e
         
     | 
| 
       61 
66 
     | 
    
         
             
                    log_error("Failed to unsubscribe client #{client_id} from #{channel}: #{e.message}")
         
     | 
| 
         @@ -160,6 +165,20 @@ module Faye 
     | 
|
| 
       160 
165 
     | 
    
         | 
| 
       161 
166 
     | 
    
         
             
                  private
         
     | 
| 
       162 
167 
     | 
    
         | 
| 
      
 168 
     | 
    
         
            +
                  def cleanup_pattern_if_unused(pattern)
         
     | 
| 
      
 169 
     | 
    
         
            +
                    subscribers = @connection.with_redis do |redis|
         
     | 
| 
      
 170 
     | 
    
         
            +
                      redis.smembers(channel_subscribers_key(pattern))
         
     | 
| 
      
 171 
     | 
    
         
            +
                    end
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                    if subscribers.empty?
         
     | 
| 
      
 174 
     | 
    
         
            +
                      @connection.with_redis do |redis|
         
     | 
| 
      
 175 
     | 
    
         
            +
                        redis.srem(patterns_key, pattern)
         
     | 
| 
      
 176 
     | 
    
         
            +
                      end
         
     | 
| 
      
 177 
     | 
    
         
            +
                    end
         
     | 
| 
      
 178 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 179 
     | 
    
         
            +
                    log_error("Failed to cleanup pattern #{pattern}: #{e.message}")
         
     | 
| 
      
 180 
     | 
    
         
            +
                  end
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
       163 
182 
     | 
    
         
             
                  def client_subscriptions_key(client_id)
         
     | 
| 
       164 
183 
     | 
    
         
             
                    namespace_key("subscriptions:#{client_id}")
         
     | 
| 
       165 
184 
     | 
    
         
             
                  end
         
     | 
    
        data/lib/faye/redis/version.rb
    CHANGED
    
    
    
        data/lib/faye/redis.rb
    CHANGED
    
    | 
         @@ -66,7 +66,9 @@ module Faye 
     | 
|
| 
       66 
66 
     | 
    
         
             
                # Destroy a client
         
     | 
| 
       67 
67 
     | 
    
         
             
                def destroy_client(client_id, &callback)
         
     | 
| 
       68 
68 
     | 
    
         
             
                  @subscription_manager.unsubscribe_all(client_id) do
         
     | 
| 
       69 
     | 
    
         
            -
                    @ 
     | 
| 
      
 69 
     | 
    
         
            +
                    @message_queue.clear(client_id) do
         
     | 
| 
      
 70 
     | 
    
         
            +
                      @client_registry.destroy(client_id, &callback)
         
     | 
| 
      
 71 
     | 
    
         
            +
                    end
         
     | 
| 
       70 
72 
     | 
    
         
             
                  end
         
     | 
| 
       71 
73 
     | 
    
         
             
                end
         
     | 
| 
       72 
74 
     | 
    
         | 
| 
         @@ -93,26 +95,33 @@ module Faye 
     | 
|
| 
       93 
95 
     | 
    
         
             
                # Publish a message to channels
         
     | 
| 
       94 
96 
     | 
    
         
             
                def publish(message, channels, &callback)
         
     | 
| 
       95 
97 
     | 
    
         
             
                  channels = [channels] unless channels.is_a?(Array)
         
     | 
| 
       96 
     | 
    
         
            -
                  success = true
         
     | 
| 
       97 
98 
     | 
    
         | 
| 
       98 
99 
     | 
    
         
             
                  begin
         
     | 
| 
      
 100 
     | 
    
         
            +
                    remaining_operations = channels.size
         
     | 
| 
      
 101 
     | 
    
         
            +
                    success = true
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
       99 
103 
     | 
    
         
             
                    channels.each do |channel|
         
     | 
| 
       100 
     | 
    
         
            -
                      #  
     | 
| 
      
 104 
     | 
    
         
            +
                      # Get subscribers and process in parallel
         
     | 
| 
       101 
105 
     | 
    
         
             
                      @subscription_manager.get_subscribers(channel) do |client_ids|
         
     | 
| 
       102 
     | 
    
         
            -
                         
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
      
 106 
     | 
    
         
            +
                        # Immediately publish to pub/sub (don't wait for enqueue)
         
     | 
| 
      
 107 
     | 
    
         
            +
                        @pubsub_coordinator.publish(channel, message) do |published|
         
     | 
| 
      
 108 
     | 
    
         
            +
                          success &&= published
         
     | 
| 
      
 109 
     | 
    
         
            +
                        end
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                        # Enqueue for all subscribed clients in parallel (batch operation)
         
     | 
| 
      
 112 
     | 
    
         
            +
                        if client_ids.any?
         
     | 
| 
      
 113 
     | 
    
         
            +
                          enqueue_messages_batch(client_ids, message) do |enqueued|
         
     | 
| 
       104 
114 
     | 
    
         
             
                            success &&= enqueued
         
     | 
| 
       105 
115 
     | 
    
         
             
                          end
         
     | 
| 
       106 
116 
     | 
    
         
             
                        end
         
     | 
| 
       107 
     | 
    
         
            -
                      end
         
     | 
| 
       108 
117 
     | 
    
         | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
             
     | 
| 
       111 
     | 
    
         
            -
                         
     | 
| 
      
 118 
     | 
    
         
            +
                        # Track completion
         
     | 
| 
      
 119 
     | 
    
         
            +
                        remaining_operations -= 1
         
     | 
| 
      
 120 
     | 
    
         
            +
                        if remaining_operations == 0 && callback
         
     | 
| 
      
 121 
     | 
    
         
            +
                          EventMachine.next_tick { callback.call(success) }
         
     | 
| 
      
 122 
     | 
    
         
            +
                        end
         
     | 
| 
       112 
123 
     | 
    
         
             
                      end
         
     | 
| 
       113 
124 
     | 
    
         
             
                    end
         
     | 
| 
       114 
     | 
    
         
            -
             
     | 
| 
       115 
     | 
    
         
            -
                    EventMachine.next_tick { callback.call(success) } if callback
         
     | 
| 
       116 
125 
     | 
    
         
             
                  rescue => e
         
     | 
| 
       117 
126 
     | 
    
         
             
                    log_error("Failed to publish message to channels #{channels}: #{e.message}")
         
     | 
| 
       118 
127 
     | 
    
         
             
                    EventMachine.next_tick { callback.call(false) } if callback
         
     | 
| 
         @@ -136,13 +145,38 @@ module Faye 
     | 
|
| 
       136 
145 
     | 
    
         
             
                  SecureRandom.uuid
         
     | 
| 
       137 
146 
     | 
    
         
             
                end
         
     | 
| 
       138 
147 
     | 
    
         | 
| 
      
 148 
     | 
    
         
            +
                # Batch enqueue messages to multiple clients using a single Redis pipeline
         
     | 
| 
      
 149 
     | 
    
         
            +
                def enqueue_messages_batch(client_ids, message, &callback)
         
     | 
| 
      
 150 
     | 
    
         
            +
                  return EventMachine.next_tick { callback.call(true) } if client_ids.empty? || !callback
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
                  message_json = message.to_json
         
     | 
| 
      
 153 
     | 
    
         
            +
                  message_ttl = @options[:message_ttl] || 3600
         
     | 
| 
      
 154 
     | 
    
         
            +
                  namespace = @options[:namespace] || 'faye'
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 157 
     | 
    
         
            +
                    @connection.with_redis do |redis|
         
     | 
| 
      
 158 
     | 
    
         
            +
                      redis.pipelined do |pipeline|
         
     | 
| 
      
 159 
     | 
    
         
            +
                        client_ids.each do |client_id|
         
     | 
| 
      
 160 
     | 
    
         
            +
                          queue_key = "#{namespace}:messages:#{client_id}"
         
     | 
| 
      
 161 
     | 
    
         
            +
                          pipeline.rpush(queue_key, message_json)
         
     | 
| 
      
 162 
     | 
    
         
            +
                          pipeline.expire(queue_key, message_ttl)
         
     | 
| 
      
 163 
     | 
    
         
            +
                        end
         
     | 
| 
      
 164 
     | 
    
         
            +
                      end
         
     | 
| 
      
 165 
     | 
    
         
            +
                    end
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(true) } if callback
         
     | 
| 
      
 168 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 169 
     | 
    
         
            +
                    log_error("Failed to batch enqueue messages: #{e.message}")
         
     | 
| 
      
 170 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(false) } if callback
         
     | 
| 
      
 171 
     | 
    
         
            +
                  end
         
     | 
| 
      
 172 
     | 
    
         
            +
                end
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
       139 
174 
     | 
    
         
             
                def setup_message_routing
         
     | 
| 
       140 
175 
     | 
    
         
             
                  # Subscribe to message events from other servers
         
     | 
| 
       141 
176 
     | 
    
         
             
                  @pubsub_coordinator.on_message do |channel, message|
         
     | 
| 
       142 
177 
     | 
    
         
             
                    @subscription_manager.get_subscribers(channel) do |client_ids|
         
     | 
| 
       143 
     | 
    
         
            -
                       
     | 
| 
       144 
     | 
    
         
            -
             
     | 
| 
       145 
     | 
    
         
            -
                      end
         
     | 
| 
      
 178 
     | 
    
         
            +
                      # Use batch enqueue for better performance
         
     | 
| 
      
 179 
     | 
    
         
            +
                      enqueue_messages_batch(client_ids, message) if client_ids.any?
         
     | 
| 
       146 
180 
     | 
    
         
             
                    end
         
     | 
| 
       147 
181 
     | 
    
         
             
                  end
         
     | 
| 
       148 
182 
     | 
    
         
             
                end
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: faye-redis-ng
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 1.0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.0.4
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Zac
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire:
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date: 2025-10- 
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2025-10-15 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: redis
         
     |