diff --git a/app/models/cache/procedure_dossier_pagination.rb b/app/models/cache/procedure_dossier_pagination.rb new file mode 100644 index 000000000..0bcee802c --- /dev/null +++ b/app/models/cache/procedure_dossier_pagination.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +class Cache::ProcedureDossierPagination + HALF_WINDOW = 50 + TRESHOLD_BEFORE_REFRESH = 1 + CACHE_EXPIRACY = 8.hours + + attr_reader :procedure_presentation, :statut, :cache + delegate :procedure, :instructeur, to: :procedure_presentation + + def initialize(procedure_presentation:, statut:) + @procedure_presentation = procedure_presentation + @statut = statut + @cache = Kredis.json(cache_key, expires_in: CACHE_EXPIRACY) + end + + def save_context(ids:, incoming_page:) + value = { ids: } + value[:incoming_page] = incoming_page if incoming_page + write_cache(value) + end + + def next_dossier_id(from_id:) + index = ids&.index(from_id.to_i) + + return nil if index.nil? # not found + + if refresh_cache_after?(from_id:) + renew_ids(from_id:) + index = ids.index(from_id.to_i) + end + return nil if index.blank? + return nil if index + 1 > ids.size # out of bound end + + ids[index + 1] + end + + def previous_dossier_id(from_id:) + index = ids&.index(from_id.to_i) + + return nil if index.nil? # not found + + if refresh_cache_before?(from_id:) + renew_ids(from_id:) + index = ids.index(from_id.to_i) + end + return nil if index.blank? + return nil if index - 1 < 0 # out of bound start + + ids[index - 1] + end + + def incoming_page + read_cache[:incoming_page] + end + + # test only + def raw + read_cache + end + + private + + def cache_key + [procedure.id, instructeur.id, statut].join(":") + end + + def write_cache(value) + cache.value = value + @read_cache = nil + end + + def read_cache + @read_cache ||= Hash(cache.value).with_indifferent_access + end + + def ids = read_cache[:ids] + + def refresh_cache_after?(from_id:) = from_id.in?(ids.last(TRESHOLD_BEFORE_REFRESH)) + + def refresh_cache_before?(from_id:) = from_id.in?(ids.first(TRESHOLD_BEFORE_REFRESH)) + + def renew_ids(from_id:) + value = read_cache + value[:ids] = fetch_ids_around(from_id:) + + write_cache(value) + end + + def fetch_all_ids + dossiers = Dossier.where(groupe_instructeur_id: GroupeInstructeur.joins(:instructeurs, :procedure).where(procedure: procedure, instructeurs: [instructeur]).select(:id)) + DossierFilterService.filtered_sorted_ids(dossiers, statut, procedure_presentation.filters_for(statut), procedure_presentation.sorted_column, instructeur, count: 0) + end + + def fetch_ids_around(from_id:) + all_ids = fetch_all_ids + from_id_at = all_ids.index(from_id) + + if from_id_at.present? + new_page_starts_at = [0, from_id_at - HALF_WINDOW].max # avoid index below 0 + new_page_ends_at = [from_id_at + HALF_WINDOW, all_ids.size].min # avoid index above all_ids.size + all_ids.slice(new_page_starts_at, new_page_ends_at) + else + [] + end + end +end diff --git a/spec/models/cache/procedure_dossier_pagination_spec.rb b/spec/models/cache/procedure_dossier_pagination_spec.rb new file mode 100644 index 000000000..a179d0372 --- /dev/null +++ b/spec/models/cache/procedure_dossier_pagination_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +describe Cache::ProcedureDossierPagination do + let(:instructeur) { double(id: 1) } + let(:procedure) { double(id: 1) } + let(:procedure_presentation) { double(instructeur:, procedure:) } + let(:instance) { described_class.new(procedure_presentation:, statut: 'a-suivre') } + + before do + instance.save_context(ids: cached_ids, incoming_page: nil) + end + + describe 'next_dossier_id' do + context 'when procedure.dossiers.by_statut has only one page' do + let(:cached_ids) { [3, 4] } + before do + allow(instance).to receive(:fetch_all_ids).and_return(cached_ids) + end + + it 'find next until the end' do + expect(instance.next_dossier_id(from_id: cached_ids.last)).to eq(nil) + expect(instance.next_dossier_id(from_id: cached_ids.first)).to eq(cached_ids.last) + end + end + + context 'when procedure.dossiers.by_statut has more than one page' do + let(:cached_ids) { [2, 3, 4] } + let(:next_page_ids) { (0..10).to_a } + + subject { instance.next_dossier_id(from_id: cached_ids.last) } + before do + allow(instance).to receive(:fetch_all_ids).and_return(next_page_ids) + end + + it 'refreshes paginated_ids' do + expect { subject }.to change { instance.send(:ids) }.from(cached_ids).to(next_page_ids) + end + end + + context 'when procedure.dossiers.by_statut does not include searched dossiers anymore' do + let(:cached_ids) { [] } + let(:next_page_ids) { [] } + before { allow(instance).to receive(:fetch_all_ids).and_return(next_page_ids) } + + it 'works' do + expect(instance.next_dossier_id(from_id: 50)).to eq(nil) + end + end + end + + describe 'previous_dossier_id' do + context 'when procedure.dossiers.by_statut has only one page' do + let(:cached_ids) { [3, 4] } + before do + allow(instance).to receive(:fetch_all_ids).and_return(cached_ids) + end + + it 'find next until the end' do + expect(instance.previous_dossier_id(from_id: cached_ids.last)).to eq(cached_ids.first) + expect(instance.previous_dossier_id(from_id: cached_ids.first)).to eq(nil) + end + end + + context 'when procedure.dossiers.by_statut has more than one page' do + let(:cached_ids) { [11, 12, 13] } + subject { instance.previous_dossier_id(from_id: cached_ids.first) } + let(:next_page_ids) { (11..20).to_a } + before do + allow(instance).to receive(:fetch_all_ids).and_return(next_page_ids) + end + + it 'works' do + expect { subject }.to change { instance.send(:ids) }.from(cached_ids).to(next_page_ids) + end + end + + context 'when procedure.dossiers.by_statut does not include searched dossiers anymore' do + let(:cached_ids) { [] } + before { allow(instance).to receive(:fetch_all_ids).and_return([]) } + + it 'works' do + expect(instance.previous_dossier_id(from_id: 50)).to eq(nil) + end + end + end +end