module Daybreak # Daybreak::DB contains the public api for Daybreak, you may extend it like # any other Ruby class (i.e. to overwrite serialize and parse). It includes # Enumerable for functional goodies like map, each, reduce and friends. class DB include Enumerable # Create a new Daybreak::DB. The second argument is the default value # to store when accessing a previously unset key, this follows the # Hash standard. # @param [String] file the path to the db file # @param default the default value to store and return when a key is # not yet in the database. # @yield [key] a block that will return the default value to store. # @yieldparam [String] key the key to be stored. def initialize(file, default=nil, &blk) @file_name = file reset! @default = default @default = blk if block_given? read! end # Set a key in the database to be written at some future date. If the data # needs to be persisted immediately, call db.set(key, value, true). # @param [#to_s] key the key of the storage slot in the database # @param value the value to store # @param [Boolean] sync if true, sync this value immediately def []=(key, value, sync = false) key = key.to_s write key, value, sync @table[key] = value end alias_method :set, :"[]=" # set! flushes data immediately to disk. # @param [#to_s] key the key of the storage slot in the database # @param value the value to store def set!(key, value) set key, value, true end # Delete a key from the database # @param [#to_s] key the key of the storage slot in the database # @param [Boolean] sync if true, sync this deletion immediately def delete(key, sync = false) key = key.to_s write key, '', sync, true @table.delete key end # delete! immediately deletes the key on disk. # @param [#to_s] key the key of the storage slot in the database def delete!(key) delete key, true end # Retrieve a value at key from the database. If the default value was specified # when this database was created, that value will be set and returned. Aliased # as get. # @param [#to_s] key the value to retrieve from the database. def [](key) key = key.to_s if @table.has_key? key @table[key] elsif default? if @default.is_a? Proc value = @default.call(key) else value = @default end set key, value end end alias_method :get, :"[]" # Iterate over the key, value pairs in the database. # @yield [key, value] blk the iterator for each key value pair. # @yieldparam [String] key the key. # @yieldparam value the value from the database. def each(&blk) keys.each { |k| blk.call(k, get(k)) } end # Does this db have a default value. def default? !@default.nil? end # Does this db have a value for this key? # @param [key#to_s] key the key to check if the DB has a key. def has_key?(key) @table.has_key? key.to_s end # Return the keys in the db. # @return [Array] def keys @table.keys end # Return the number of stored items. # @return [Integer] def length @table.keys.length end # Serialize the data for writing to disk, if you don't want to use Marshal # overwrite this method. # @param value the value to be serialized # @return [String] def serialize(value) Marshal.dump(value) end # Parse the serialized value from disk, like serialize if you want to use a # different serialization method overwrite this method. # @param value the value to be parsed # @return [String] def parse(value) Marshal.load(value) end # Reset and empty the database file. def empty! @writer.truncate! reset! end alias_method :clear, :empty! # Force all queued commits to be written to disk. def flush! @writer.flush! end # Reset the state of the database, you should call read! after calling this. def reset! @table = {} @writer = Daybreak::Writer.new(@file_name) @reader = Daybreak::Reader.new(@file_name) end # Close the database for reading and writing. def close! @writer.close! @reader.close! end # Compact the database to remove stale commits and reduce the file size. def compact! # Create a new temporary file tmp_file = Tempfile.new File.basename(@file_name) copy_db = self.class.new tmp_file.path # Copy the database key by key into the temporary table each do |key| copy_db.set(key, get(key)) end copy_db.close! # Empty this database empty! close! # Move the copy into place tmp_file.close FileUtils.mv tmp_file.path, @file_name tmp_file.unlink # Reset this database reset! read! end # Read all values from the log file. If you want to check for changed data # call this again. def read! @reader.read do |record| if record.deleted? @table.delete record.key else @table[record.key] = parse(record.data) end end end private def write(key, value, sync = false, delete = false) @writer.write(Record.new(key, serialize(value), delete)) flush! if sync end end end