lib/io/stream/writable.rb



# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

require_relative "readable"

module IO::Stream
	# The minimum write size before flushing. Defaults to 64KB.
	MINIMUM_WRITE_SIZE = ENV.fetch("IO_STREAM_MINIMUM_WRITE_SIZE", BLOCK_SIZE).to_i
	
	# A module providing writable stream functionality.
	#
	# You must implement the `syswrite` method to write data to the underlying IO.
	module Writable
		# Initialize writable stream functionality.
		# @parameter minimum_write_size [Integer] The minimum buffer size before flushing.
		def initialize(minimum_write_size: MINIMUM_WRITE_SIZE, **, &block)
			@writing = ::Thread::Mutex.new
			@write_buffer = StringBuffer.new
			@minimum_write_size = minimum_write_size
			
			super(**, &block) if defined?(super)
		end
		
		attr_accessor :minimum_write_size
		
		# Flushes buffered data to the stream.
		def flush
			return if @write_buffer.empty?
			
			@writing.synchronize do
				self.drain(@write_buffer)
			end
		end
		
		# Writes `string` to the buffer. When the buffer is full or #sync is true the
		# buffer is flushed to the underlying `io`.
		# @parameter string [String] the string to write to the buffer.
		# @returns [Integer] the number of bytes appended to the buffer.
		def write(string, flush: false)
			@writing.synchronize do
				@write_buffer << string
				
				flush |= (@write_buffer.bytesize >= @minimum_write_size)
				
				if flush
					self.drain(@write_buffer)
				end
			end
			
			return string.bytesize
		end
		
		# Appends `string` to the buffer and returns self for method chaining.
		# @parameter string [String] the string to write to the stream.
		def <<(string)
			write(string)
			
			return self
		end
		
		# Write arguments to the stream followed by a separator and flush immediately.
		# @parameter arguments [Array] The arguments to write to the stream.
		# @parameter separator [String] The separator to append after each argument.
		def puts(*arguments, separator: $/)
			return if arguments.empty?
			
			@writing.synchronize do
				arguments.each do |argument|
					@write_buffer << argument << separator
				end
				
				self.drain(@write_buffer)
			end
		end
		
		# Close the write end of the stream by flushing any remaining data.
		def close_write
			flush
		end
		
		private def drain(buffer)
			begin
				syswrite(buffer)
			ensure
				# If the write operation fails, we still need to clear this buffer, and the data is essentially lost.
				buffer.clear
			end
		end
	end
end