class StandardFile::V20190520::SyncManager

Constants

MIN_CONFLICT_INTERVAL

Ignore differences that are at most this many seconds apart Anything over this threshold will be conflicted.

Public Instance Methods

_sync_get(sync_token, input_cursor_token, limit, content_type) click to toggle source
# File lib/standard_file/2019_05_20/sync_manager.rb, line 122
def _sync_get(sync_token, input_cursor_token, limit, content_type)
  cursor_token = nil
  if limit == nil
    limit = 100000
  end

  # if both are present, cursor_token takes precendence as that would eventually return all results
  # the distinction between getting results for a cursor and a sync token is that cursor results use a
  # >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
  # typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
  # by using >=, we don't miss those results on a subsequent call with a cursor token
  if input_cursor_token
    date = datetime_from_sync_token(input_cursor_token)
    items = @user.items.order(:updated_at).where("updated_at >= ?", date)
  elsif sync_token
    date = datetime_from_sync_token(sync_token)
    items = @user.items.order(:updated_at).where("updated_at > ?", date)
  else
    # if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
    items = @user.items.order(:updated_at).where(:deleted => false)
  end

  if content_type
    items = items.where(:content_type => content_type)
  end

  items = items.sort_by{|m| m.updated_at}

  if items.count > limit
    items = items.slice(0, limit)
    date = items.last.updated_at
    cursor_token = sync_token_from_datetime(date)
  end

  return items, cursor_token
end
_sync_save(item_hashes, request, retrieved_items) click to toggle source
# File lib/standard_file/2019_05_20/sync_manager.rb, line 37
def _sync_save(item_hashes, request, retrieved_items)
  if !item_hashes
    return [], []
  end

  saved_items = []
  conflicts = []

  item_hashes.each do |item_hash|
    is_new_record = false
    begin
      item = @user.items.find_or_create_by(:uuid => item_hash[:uuid]) do |created_item|
        # this block is executed if this is a new record.
        is_new_record = true
      end
    rescue => error
      conflicts.push({
        :unsaved_item => item_hash,
        :type => "uuid_conflict"
      })
      next
    end

    # SFJS did not send updated_at prior to 0.3.59.
    # updated_at value from client will not be saved, as it is not a permitted_param.
    if item_hash['updated_at']
      incoming_updated_at = DateTime.parse(item_hash['updated_at'])
    else
      # Default to epoch
      incoming_updated_at = Time.at(0).to_datetime
    end

    if !is_new_record
      # We want to check if this updated_at value is equal to the item's current updated_at value.
      # If they differ, it means the client is attempting to save an item which hasn't been updated.
      # In this case, if the incoming_item.updated_at < server_item.updated_at, always conflict.
      # We don't want old items overriding newer ones.
      # incoming_item.updated_at > server_item.updated_at would seem to be impossible, as only servers are responsible for setting updated_at.
      # But assuming a rogue client has gotten away with it,
      # we should also conflict in this case if the difference between the dates is greater than MIN_CONFLICT_INTERVAL seconds.

      save_incoming = true

      our_updated_at = item.updated_at
      difference = incoming_updated_at.to_f - our_updated_at.to_f

      if difference < 0
        # incoming is less than ours. This implies stale data. Don't save if greater than interval
        save_incoming = difference.abs < MIN_CONFLICT_INTERVAL
      elsif difference > 0
        # incoming is greater than ours. Should never be the case. If so though, don't save.
        save_incoming = difference.abs < MIN_CONFLICT_INTERVAL
      else
        # incoming is equal to ours (which is desired, healthy behavior), continue with saving.
        save_incoming = true
      end

      if !save_incoming
        # Dont save incoming and send it back. At this point the server item is likely to be included
        # in retrieved_items in a subsequent sync, so when that value comes into the client,
        server_value = item.as_json({})
        conflicts.push({
          :server_item => server_value, # as_json to get values as-is, befor modifying below,
          :type => "sync_conflict"
        })

        retrieved_items.delete(item)
        next
      end
    end

    item.last_user_agent = request.user_agent
    item.update(item_hash.permit(*permitted_params))

    if item.deleted == true
      set_deleted(item)
      item.save
    end

    saved_items.push(item)
  end

  return saved_items, conflicts
end
sync(item_hashes, options, request) click to toggle source
# File lib/standard_file/2019_05_20/sync_manager.rb, line 5
def sync(item_hashes, options, request)
  in_sync_token = options[:sync_token]
  in_cursor_token = options[:cursor_token]
  limit = options[:limit]
  content_type = options[:content_type] # optional, only return items of these type if present

  retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
  last_updated = DateTime.now
  saved_items, conflicts = _sync_save(item_hashes, request, retrieved_items)

  if saved_items.length > 0
    last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
  end

  # add 1 microsecond to avoid returning same object in subsequent sync
  last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
  sync_token = sync_token_from_datetime(last_updated)

  return {
    :retrieved_items => retrieved_items,
    :saved_items => saved_items,
    :conflicts => conflicts,
    :sync_token => sync_token,
    :cursor_token => cursor_token
  }
end