Skip to content

Commit

Permalink
WIP: Implement keyset_cached (unhandled cache in session)
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Dec 2, 2024
1 parent c81dbd2 commit d8d1431
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 32 deletions.
7 changes: 5 additions & 2 deletions gem/apps/keyset_ar.ru
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ end

# Pagy initializer
require 'pagy/extras/limit'
require 'pagy/extras/keyset'
require 'pagy/extras/keyset_cached'
require 'pagy/extras/pagy'
Pagy::DEFAULT[:limit] = 10
Pagy::DEFAULT.freeze
Expand All @@ -41,13 +41,16 @@ Pagy::DEFAULT.freeze
require 'sinatra/base'
# Sinatra application
class PagyKeyset < Sinatra::Base

Check failure on line 44 in gem/apps/keyset_ar.ru

View workflow job for this annotation

GitHub Actions / Ruby 3.2 Test

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body beginning.

Check failure on line 44 in gem/apps/keyset_ar.ru

View workflow job for this annotation

GitHub Actions / Ruby 3.3 Test

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body beginning.
enable :sessions

include Pagy::Backend
# Root route/action
get '/' do
Time.zone = 'UTC'

@order = { animal: :asc, name: :asc, birthdate: :desc, id: :asc }
@pagy, @pets = pagy_keyset(Pet.order(@order))
@pagy, @pets = pagy_keyset_cached(Pet.order(@order))
erb :main
end

Expand Down
29 changes: 29 additions & 0 deletions gem/lib/pagy/extras/keyset_cached.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/keyset/cached
# frozen_string_literal: true

require_relative '../keyset/cached'

class Pagy # :nodoc:
# Add keyset pagination
module KeysetCachedExtra
private

# Return Pagy::Keyset::Cached object and paginated records
def pagy_keyset_cached(set, **vars)
vars[:cache] ||= pagy_keyset_cached_hash(vars)
vars[:page] ||= pagy_get_page(vars) # numeric page
vars[:limit] ||= pagy_get_limit(vars)
pagy = Keyset::Cached.new(set, **vars)
pagy.finalize
[pagy, pagy.records]
end

# Return the cached hash
def pagy_keyset_cached_hash(vars)
key = "pagy-keyset-#{B64.encode(params.slice(vars.delete(:key_params)).to_json)}"
puts key
session[key] ||= {}
end
end
Backend.prepend KeysetCachedExtra
end
61 changes: 33 additions & 28 deletions gem/lib/pagy/keyset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,58 @@ class Keyset
class TypeError < ::TypeError; end

include SharedMethods

# Pick the right adapter for the set
def self.new(set, **vars)
if self == Pagy::Keyset
if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
ActiveRecord
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
Sequel
else
raise TypeError, "expected set to be an instance of ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
end.new(set, **vars)
else
allocate.tap { |instance| instance.send(:initialize, set, **vars) }
end
end

attr_reader :latest # Other readers from SharedMethods

def initialize(set, **vars)
default = DEFAULT.slice(:limit, :page_param, # from pagy
:headers, # from headers extra
:jsonapi, # from jsonapi extra
:limit_param, :limit_max, :limit_extra) # from limit_extra
assign_vars({ **default, page: nil }, vars)
# Extend the instance with the right adapter for the set
if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
extend ActiveRecord
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
extend Sequel
else
raise TypeError, "expected set to be an instance of ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
end
assign_vars(default, vars)
assign_limit
@set = set
@page = @vars[:page]
@keyset = extract_keyset
assign_cursor
raise InternalError, 'the set must be ordered' if @keyset.empty?
return unless @page
return unless @cursor

latest = JSON.parse(B64.urlsafe_decode(@page)).transform_keys(&:to_sym)
latest = JSON.parse(B64.urlsafe_decode(@cursor)).transform_keys(&:to_sym)
@latest = typecast_latest(latest)
raise InternalError, 'page and keyset are not consistent' \
unless @latest.keys == @keyset.keys
end

def default
default = DEFAULT.slice(:limit, :page_param, # from pagy
:headers, # from headers extra
:jsonapi, # from jsonapi extra
:limit_param, :limit_max, :limit_extra) # from limit_extra
{ **default, page: nil }
end

# Assign the cursor from the cache
def assign_cursor
@cursor = @vars[:page]
end

# Return the next page
def next
records
return unless @more

@next ||= begin
hash = keyset_attributes_from(@records.last)
json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json
B64.urlsafe_encode(json)
end
@next ||= next_cursor
end

# Return the next cursor
def next_cursor
hash = keyset_attributes_from(@records.last)
json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json
B64.urlsafe_encode(json)
end

# Fetch the array of records for the current page
Expand Down
2 changes: 1 addition & 1 deletion gem/lib/pagy/keyset/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class Pagy
class Keyset
# Keyset adapter for ActiveRecord
class ActiveRecord < Keyset
module ActiveRecord
protected

# Get the keyset attributes from the record
Expand Down
64 changes: 64 additions & 0 deletions gem/lib/pagy/keyset/cached.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# See Pagy::Countless API documentation: https://ddnexus.github.io/pagy/docs/api/keyset/cached
# frozen_string_literal: true

require_relative '../keyset'

class Pagy # :nodoc:
# No need to know the count to paginate
class Keyset
class Cached < Keyset

Check failure on line 9 in gem/lib/pagy/keyset/cached.rb

View workflow job for this annotation

GitHub Actions / Ruby 3.2 Test

Style/Documentation: Missing top-level documentation comment for `class Pagy::Keyset::Cached`.

Check failure on line 9 in gem/lib/pagy/keyset/cached.rb

View workflow job for this annotation

GitHub Actions / Ruby 3.3 Test

Style/Documentation: Missing top-level documentation comment for `class Pagy::Keyset::Cached`.

Check failure on line 10 in gem/lib/pagy/keyset/cached.rb

View workflow job for this annotation

GitHub Actions / Ruby 3.2 Test

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body beginning.

Check failure on line 10 in gem/lib/pagy/keyset/cached.rb

View workflow job for this annotation

GitHub Actions / Ruby 3.3 Test

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body beginning.
def self.new(set, **vars)
allocate.tap do |instance|
instance.instance_variable_set(:@cache, vars[:cache])
instance.send(:initialize, set, **vars)
end
end

# Override adding default variables required by UI
def default
{ **super, **DEFAULT.slice(:ends, :page, :size) }
end

# Override the assign_cursor
def assign_cursor
@cursor = @cache[@page]
end

# Return the next page from cache or chache the next_page/next_cursor pair
# It must be called before finalize
def next
next_page = @page + 1
records
return if !@more || (@vars[:max_pages] && @last > vars[:max_pages])

@cache[next_page] = @cache[next_page] || next_cursor
next_page
end

# Finalize the instance variables based on the fetched size
def finalize
# Ensure next is called, so we know the actual last page
@next = self.next
@prev = (@page - 1 unless @page == 1)
@last = @cache.keys.last
raise OverflowError.new(self, :page, "in 1..#{@last}", @page) if @page > @last

@in = @records.size
@offset = @limit * (@page - 1) # may not be accurate
@from = @in.zero? ? 0 : @offset + 1
@to = @offset + @in
self
end
end
end

module SeriesOverride

Check failure on line 56 in gem/lib/pagy/keyset/cached.rb

View workflow job for this annotation

GitHub Actions / Ruby 3.2 Test

Style/Documentation: Missing top-level documentation comment for `module Pagy::SeriesOverride`.

Check failure on line 56 in gem/lib/pagy/keyset/cached.rb

View workflow job for this annotation

GitHub Actions / Ruby 3.3 Test

Style/Documentation: Missing top-level documentation comment for `module Pagy::SeriesOverride`.
# Override the original series.
# Return nil if :keyset_numeric_minimal is enabled
def series(**)
super unless @vars[:keyset_numeric_minimal]
end
end
prepend SeriesOverride
end
2 changes: 1 addition & 1 deletion gem/lib/pagy/keyset/sequel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class Pagy
class Keyset
# Keyset adapter for sequel
class Sequel < Keyset
module Sequel
protected

# Get the keyset attributes from the record
Expand Down

0 comments on commit d8d1431

Please sign in to comment.