class StandardFile::V20161215::SyncManager

Public Instance Methods

check_for_conflicts(saved_items, retrieved_items, unsaved_items) click to toggle source
# File lib/standard_file/2016_12_15/sync_manager.rb, line 33
def check_for_conflicts(saved_items, retrieved_items, unsaved_items)
  # conflicts occur when you are trying to save an item for which there is a pending change already
  min_conflict_interval = 20

  if Rails.env.development?
    min_conflict_interval = 1
  end

  saved_ids = saved_items.map{|x| x.uuid }
  retrieved_ids = retrieved_items.map{|x| x.uuid }
  conflicts = saved_ids & retrieved_ids # & is the intersection
  # saved items take precedence, retrieved items are duplicated with a new uuid
  conflicts.each do |conflicted_uuid|
    # if changes are greater than min_conflict_interval seconds apart,
    # push the retrieved item in the unsaved array so that the client can duplicate it
    saved = saved_items.find{|i| i.uuid == conflicted_uuid}
    conflicted = retrieved_items.find{|i| i.uuid == conflicted_uuid}
    if (saved.updated_at - conflicted.updated_at).abs > min_conflict_interval
      unsaved_items.push({
        :item => conflicted,
        :error => {:tag => "sync_conflict"}
      })
    end

    # We remove the item from retrieved items whether or not it satisfies the min_conflict_interval
    # This is because the 'saved' value takes precedence, since that's the current value in the database.
    # So by removing it from retrieved, we are forcing the client to ignore this change.
    retrieved_items.delete(conflicted)
  end
end
sync(item_hashes, options, request) click to toggle source
# File lib/standard_file/2016_12_15/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, unsaved_items = _sync_save(item_hashes, request)
  if saved_items.length > 0
    last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
  end

  check_for_conflicts(saved_items, retrieved_items, unsaved_items)

  # 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,
    :unsaved => unsaved_items,
    :sync_token => sync_token,
    :cursor_token => cursor_token
  }
end

Private Instance Methods

_sync_get(sync_token, input_cursor_token, limit, content_type) click to toggle source
# File lib/standard_file/2016_12_15/sync_manager.rb, line 98
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) click to toggle source
# File lib/standard_file/2016_12_15/sync_manager.rb, line 66
def _sync_save(item_hashes, request)
  if !item_hashes
    return [], []
  end
  saved_items = []
  unsaved = []

  item_hashes.each do |item_hash|
    begin
      item = @user.items.find_or_create_by(:uuid => item_hash[:uuid])
    rescue => error
      unsaved.push({
        :item => item_hash,
        :error => {:message => error.message, :tag => "uuid_conflict"}
        })
      next
    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, unsaved
end