lib/wcc/contentful/store/lazy_cache_store.rb in wcc-contentful-0.2.2 vs lib/wcc/contentful/store/lazy_cache_store.rb in wcc-contentful-0.3.0.pre.rc

- old
+ new

@@ -1,37 +1,58 @@ # frozen_string_literal: true module WCC::Contentful::Store class LazyCacheStore - delegate :find_all, to: :@cdn - - # TODO: https://zube.io/watermarkchurch/development/c/2265 - # figure out how to cache the results of a find_by query, ex: - # `find_by('slug' => '/about')` - delegate :find_by, to: :@cdn - def initialize(client, cache: nil) @cdn = CDNAdapter.new(client) @cache = cache || ActiveSupport::Cache::MemoryStore.new + @client = client end - def find(key) + def find(key, **options) found = @cache.fetch(key) do # if it's not a contentful ID don't hit the API. # Store a nil object if we can't find the object on the CDN. - (@cdn.find(key) || nil_obj(key)) if key =~ /^\w+$/ + (@cdn.find(key, options) || nil_obj(key)) if key =~ /^\w+$/ end case found.try(:dig, 'sys', 'type') when 'Nil', 'DeletedEntry', 'DeletedAsset' nil else found end end + # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18 + # figure out how to cache the results of a find_by query, ex: + # `find_by('slug' => '/about')` + def find_by(content_type:, filter: nil, options: nil) + if filter.keys == ['sys.id'] + # Direct ID lookup, like what we do in `WCC::Contentful::ModelMethods.resolve` + # We can return just this item. Stores are not required to implement :include option. + if found = @cache.read(filter['sys.id']) + return found + end + end + + q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {})) + q = q.apply(filter) if filter + q.first + end + + def find_all(content_type:, options: nil) + Query.new( + store: self, + client: @client, + relation: { content_type: content_type }, + cache: @cache, + options: options + ) + end + # #index is called whenever the sync API comes back with more data. def index(json) id = json.dig('sys', 'id') return unless prev = @cache.read(id) @@ -39,10 +60,11 @@ return prev if next_rev < prev_rev end # we also set deletes in the cache - no need to go hit the API when we know # this is a nil object + ensure_hash json @cache.write(id, json) case json.dig('sys', 'type') when 'DeletedEntry', 'DeletedAsset' nil @@ -50,10 +72,11 @@ json end end def set(key, value) + ensure_hash value old = @cache.read(key) @cache.write(key, value) old end @@ -69,8 +92,70 @@ 'id' => id, 'type' => 'Nil', 'revision' => 1 } } + end + + def ensure_hash(val) + raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash) + end + + class Query < CDNAdapter::Query + def initialize(cache:, **extra) + super(cache: cache, **extra) + @cache = cache + end + + private + + def response + # Disabling because the superclass already took `@response` + # rubocop:disable Naming/MemoizedInstanceVariableName + @wrapped_response ||= ResponseWrapper.new(super, @cache) + # rubocop:enable Naming/MemoizedInstanceVariableName + end + + ResponseWrapper = + Struct.new(:response, :cache) do + delegate :count, to: :response + + def items + @items ||= + response.items.map do |item| + id = item.dig('sys', 'id') + prev = cache.read(id) + unless (prev_rev = prev&.dig('sys', 'revision')) && + (next_rev = item.dig('sys', 'revision')) && + next_rev < prev_rev + + cache.write(id, item) + end + + item + end + end + + def includes + @includes ||= IncludesWrapper.new(response, cache) + end + end + + IncludesWrapper = + Struct.new(:response, :cache) do + def [](id) + return unless item = response.includes[id] + + prev = cache.read(id) + unless (prev_rev = prev&.dig('sys', 'revision')) && + (next_rev = item.dig('sys', 'revision')) && + next_rev < prev_rev + + cache.write(id, item) + end + + item + end + end end end end