yiffspace 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/yiffspace/concerns/active_record_extensions.rb +45 -0
  3. data/lib/yiffspace/concerns/api_methods.rb +101 -0
  4. data/lib/yiffspace/concerns/attribute_matchers.rb +100 -0
  5. data/lib/yiffspace/concerns/attribute_methods.rb +39 -0
  6. data/lib/yiffspace/concerns/concurrency_methods.rb +20 -0
  7. data/lib/yiffspace/concerns/conditional_includes.rb +43 -0
  8. data/lib/yiffspace/concerns/current_methods.rb +60 -0
  9. data/lib/yiffspace/concerns/has_bit_flags.rb +59 -0
  10. data/lib/yiffspace/concerns/user_methods.rb +77 -0
  11. data/lib/yiffspace/concerns/user_name_methods.rb +53 -0
  12. data/lib/yiffspace/configuration.rb +70 -10
  13. data/lib/yiffspace/core_ext/all.rb +0 -1
  14. data/lib/yiffspace/include/all.rb +16 -0
  15. data/lib/yiffspace/include/cache.rb +5 -0
  16. data/lib/yiffspace/include/current.rb +5 -0
  17. data/lib/yiffspace/include/duration_parser.rb +5 -0
  18. data/lib/yiffspace/include/helpers.rb +5 -0
  19. data/lib/yiffspace/{core_ext → include}/open_hash.rb +2 -0
  20. data/lib/yiffspace/include/parameter_builder.rb +5 -0
  21. data/lib/yiffspace/include/parse_value.rb +5 -0
  22. data/lib/yiffspace/include/query_builder.rb +5 -0
  23. data/lib/yiffspace/include/query_dsl.rb +5 -0
  24. data/lib/yiffspace/include/query_helper.rb +5 -0
  25. data/lib/yiffspace/include/routes.rb +5 -0
  26. data/lib/yiffspace/include/table_builder.rb +5 -0
  27. data/lib/yiffspace/include/trace_logger.rb +5 -0
  28. data/lib/yiffspace/include/user_attribute.rb +5 -0
  29. data/lib/yiffspace/search/query_builder.rb +83 -0
  30. data/lib/yiffspace/search/query_dsl.rb +119 -0
  31. data/lib/yiffspace/search/query_helper.rb +49 -0
  32. data/lib/yiffspace/utils/cache.rb +40 -0
  33. data/lib/yiffspace/utils/current.rb +43 -0
  34. data/lib/yiffspace/utils/duration_parser.rb +24 -0
  35. data/lib/yiffspace/utils/helpers.rb +20 -0
  36. data/lib/yiffspace/utils/parameter_builder.rb +121 -0
  37. data/lib/yiffspace/utils/parse_value.rb +174 -0
  38. data/lib/yiffspace/utils/routes.rb +28 -0
  39. data/lib/yiffspace/utils/table_builder.rb +136 -0
  40. data/lib/yiffspace/utils/trace_logger.rb +91 -0
  41. data/lib/yiffspace/utils/user_attribute.rb +271 -0
  42. data/lib/yiffspace/version.rb +1 -1
  43. data/lib/yiffspace.rb +11 -1
  44. metadata +68 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d76f09af3606e844dc35e1d97ce80a14eb6d99a989d421ec0e8a81ba6a3735f7
4
- data.tar.gz: b81ec5a4ad23d5e0606fd8247f5b09537078fb9fe271c3a72ac3442963b3554f
3
+ metadata.gz: aca3308a25bbf6b426ea3a8d52a9bfe7a41452cc7e500332accab52ee0d2fde0
4
+ data.tar.gz: 2cac0c503de3fa0141b0a923c0fadeb4225b01640b74dac33c8f743127a5273e
5
5
  SHA512:
6
- metadata.gz: 39fcd8410ca585b80edd98ce465d62c188c54a967db2cd98158306c313deaf6b8ea73e3ab3208b2e3cf69100fad641a763a25eecefbbf4a1cc31474851619b3d
7
- data.tar.gz: 776dd1d3c6c53d1746fd7f671a2ce9667263ee031a34a7329054e753d4415bb0f1cf3d5432c34c258059aeab3bc652fccd57c1b7ed6e6226aaddee94fc93b5cc
6
+ metadata.gz: 62946e5dfb3a8593ae125882c845652babac22b5d4b55cb421a8de1cf6503b1940ada9b1e00b640b3d3d0af4f8f8867be832c0600e4f2de18f5c7acff71b8182
7
+ data.tar.gz: bb2435f4fd6d6f7ab4b4d6d4110e90b4bf1aceb36109db94af99078f652a59acfdc569fbe28b57367de014aa5443d9b413ad3ecdceced070321979a2b5537ccd
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module ActiveRecordExtensions
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ def without_timeout
10
+ original = statement_timeout
11
+ connection.execute("SET STATEMENT_TIMEOUT = 0") unless Rails.env.test?
12
+ yield
13
+ ensure
14
+ connection.execute("SET STATEMENT_TIMEOUT = #{original}") unless Rails.env.test?
15
+ end
16
+
17
+ def with_timeout(time, default_value = nil)
18
+ original = statement_timeout
19
+ connection.execute("SET STATEMENT_TIMEOUT = #{time}") unless Rails.env.test?
20
+ yield
21
+ rescue ::ActiveRecord::StatementInvalid => e
22
+ Rails.logger.warn("Statement timeout exceeded: #{e.message}")
23
+ default_value
24
+ ensure
25
+ connection.execute("SET STATEMENT_TIMEOUT = #{original}") unless Rails.env.test?
26
+ end
27
+
28
+ def statement_timeout
29
+ ApplicationRecord.connection.select_one("SELECT setting FROM pg_settings WHERE name = 'statement_timeout'")["setting"].to_i
30
+ end
31
+
32
+ # CrossJoinLateral, LeftJoinLateral, nil
33
+ def unnest(column, name: column.singularize, type: Arel::Nodes::LeftJoinLateral)
34
+ function = Arel::Nodes::NamedFunction.new("unnest", [arel(column)], name)
35
+ return function if type.nil?
36
+
37
+ joins(type.new(
38
+ function,
39
+ Arel::Nodes::On.new(Arel.sql("TRUE")),
40
+ ))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module ApiMethods
6
+ class NotVisibleError < StandardError; end
7
+ extend(ActiveSupport::Concern)
8
+
9
+ module ClassMethods
10
+ def available_includes
11
+ []
12
+ end
13
+
14
+ def multiple_includes
15
+ reflections.select { |_, v| v.macro == :has_many }.keys.map(&:to_sym)
16
+ end
17
+
18
+ def associated_models(name)
19
+ if reflections[name].options[:polymorphic]
20
+ reflections[name].active_record.try(:model_types) || []
21
+ else
22
+ [reflections[name].class_name]
23
+ end
24
+ end
25
+ end
26
+
27
+ delegate(:available_includes, to: :class)
28
+
29
+ # XXX deprecated, shouldn't expose this as an instance method.
30
+ def api_attributes(user)
31
+ policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
32
+ policy.api_attributes
33
+ end
34
+
35
+ # XXX deprecated, shouldn't expose this as an instance method.
36
+ def html_data_attributes(user)
37
+ policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
38
+ policy.html_data_attributes
39
+ end
40
+
41
+ def process_api_attributes(options, underscore: false)
42
+ options[:methods] ||= []
43
+ attributes, methods = api_attributes(options[:user]).partition { |attr| has_attribute?(attr) }
44
+ methods += options[:methods]
45
+ if underscore && options[:only].blank?
46
+ options[:only] = attributes + methods
47
+ else
48
+ options[:only] ||= attributes + methods
49
+ end
50
+
51
+ attributes &= options[:only]
52
+ methods &= options[:only]
53
+
54
+ options[:only] = attributes
55
+ options[:methods] = methods
56
+
57
+ options.delete(:methods) if options[:methods].empty?
58
+ options
59
+ end
60
+
61
+ def serializable_hash(options = {})
62
+ options ||= {}
63
+ options[:user] ||= CurrentUser.user || User.anonymous
64
+ return :not_visible unless visible?(options[:user])
65
+
66
+ if options[:only].is_a?(String)
67
+ options.delete(:methods)
68
+ options.delete(:include)
69
+ options.merge!(ParameterBuilder.serial_parameters(options[:only], self, options))
70
+ if options[:only].include?("_")
71
+ options[:only].delete("_")
72
+ options = process_api_attributes(options, underscore: true)
73
+ end
74
+ else
75
+ options = process_api_attributes(options)
76
+ end
77
+ options[:only] += [SecureRandom.hex(6)]
78
+
79
+ hash = super
80
+ hash.transform_keys! { |key| key.delete("?").delete_prefix("apionly_") }
81
+ deep_reject_hash(hash) { |_, v| v == :not_visible }
82
+ end
83
+
84
+ def visible?(_user)
85
+ true
86
+ end
87
+
88
+ def deep_reject_hash(hash, &block)
89
+ hash.each_with_object({}) do |(key, value), result|
90
+ if value.is_a?(Hash)
91
+ result[key] = deep_reject_hash(value, &block)
92
+ elsif value.is_a?(Array)
93
+ result[key] = value.map { |v| v.is_a?(Hash) ? deep_reject_hash(v, &block) : v }.reject { |i| block.call(nil, i) }
94
+ elsif !block.call(key, value)
95
+ result[key] = value
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module AttributeMatchers
6
+ def boolean_attribute_matches(attribute, value, &)
7
+ return all if value.nil?
8
+
9
+ if value.to_s.truthy?
10
+ value = true
11
+ elsif value.to_s.falsy?
12
+ value = false
13
+ else
14
+ raise(ArgumentError, "value must be truthy or falsy")
15
+ end
16
+
17
+ q = block_given? ? yield : all
18
+ q.where(attribute => value)
19
+ end
20
+
21
+ # range: "5", ">5", "<5", ">=5", "<=5", "5..10", "5,6,7"
22
+ def numeric_attribute_matches(attribute, value, &)
23
+ return all if value.nil?
24
+
25
+ value = value.to_s.strip
26
+ column = QueryHelper.get_column(attribute, table_name)
27
+ parsed_range = ParseValue.range(value, column.type)
28
+
29
+ add_range_relation(parsed_range, attribute, &)
30
+ end
31
+
32
+ # datetime columns return ActiveSupport::TimeWithZone
33
+ def datetime_attribute_matches(attribute, range, &)
34
+ numeric_attribute_matches(attribute, range, &)
35
+ end
36
+
37
+ def add_range_relation(arr, field, &)
38
+ return all if arr.nil?
39
+
40
+ q = block_given? ? yield : all
41
+ case arr[0]
42
+ when :eq
43
+ if arr[1].is_a?(Time)
44
+ q.where.between(field => arr[1].all_day)
45
+ else
46
+ q.where(field => arr[1])
47
+ end
48
+ when :gt
49
+ q.where.gt(field => arr[1])
50
+ when :gte
51
+ q.where.gte(field => arr[1])
52
+ when :lt
53
+ q.where.lt(field => arr[1])
54
+ when :lte
55
+ q.where.lte(field => arr[1])
56
+ when :in
57
+ q.where.in(field => arr[1])
58
+ when :between
59
+ q.where.between(field => arr[1]..arr[2])
60
+ else
61
+ q.none
62
+ end
63
+ end
64
+
65
+ def text_attribute_matches(attribute, value, convert_to_wildcard: false, &)
66
+ return all if value.nil?
67
+
68
+ value = "*#{value}*" if convert_to_wildcard && value.exclude?("*")
69
+ q = block_given? ? yield : all
70
+ if value.is_a?(Array)
71
+ q.where.ilike_any(attribute => value)
72
+ elsif value =~ /\*/
73
+ q.where.ilike(attribute => value)
74
+ else
75
+ q.where.tsquery(attribute => value)
76
+ end
77
+ end
78
+
79
+ def ip_attribute_matches(attribute, value, &)
80
+ return all if value.nil?
81
+
82
+ q = block_given? ? yield : all
83
+ q.where.inet_contained_within_or_equals(attribute => value)
84
+ end
85
+
86
+ def attribute_present(attribute, value, &)
87
+ return all if value.nil?
88
+
89
+ q = block_given? ? yield : all
90
+ if value.to_s.truthy?
91
+ q.where.not(attribute => nil)
92
+ elsif value.to_s.falsy?
93
+ q.where(attribute => nil)
94
+ else
95
+ q.none
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module AttributeMethods
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ # Defines `<attribute>_string`, `<attribute>_string=`, and `<attribute>=`
10
+ # methods for converting an array attribute to or from a string.
11
+ #
12
+ # The `<attribute>=` setter parses strings into an array using the
13
+ # `parse` regex. The resulting strings can be converted to another type
14
+ # with the `cast` option.
15
+ def array_attribute(name, parse: /[^[:space:]]+/, join_character: " ", cast: :itself)
16
+ define_method("#{name}_string") do
17
+ send(name).join(join_character)
18
+ end
19
+
20
+ define_method("#{name}_string=") do |value|
21
+ raise(ArgumentError, "#{name} must be a String") unless value.respond_to?(:to_str)
22
+
23
+ send("#{name}=", value)
24
+ end
25
+
26
+ define_method("#{name}=") do |value|
27
+ if value.respond_to?(:to_str)
28
+ super(value.to_str.scan(parse).flatten.map(&cast))
29
+ elsif value.respond_to?(:to_a)
30
+ super(value.to_a)
31
+ else
32
+ raise(ArgumentError, "#{name} must be a String or an Array")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module ConcurrencyMethods
6
+ extend(ActiveSupport::Concern)
7
+
8
+ class_methods do
9
+ def parallel_find_each(**, &)
10
+ # XXX We may deadlock if a transaction is open; do a non-parallel each.
11
+ return find_each(&) if connection.transaction_open?
12
+
13
+ find_in_batches(error_on_ignore: true, **) do |batch|
14
+ batch.parallel_each(&)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module ConditionalIncludes
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ def html_includes(request, *)
10
+ includes_if(request.format.html?, *)
11
+ end
12
+
13
+ def includes_if(condition, *)
14
+ return all unless condition
15
+
16
+ includes(*)
17
+ end
18
+
19
+ def includes_unless(condition, *)
20
+ return all if condition
21
+
22
+ includes(*)
23
+ end
24
+
25
+ def html_preload(request, *)
26
+ preload_if(request.format.html?, *)
27
+ end
28
+
29
+ def preload_if(condition, *)
30
+ return all unless condition
31
+
32
+ preload(*)
33
+ end
34
+
35
+ def preload_unless(condition, *)
36
+ return all if condition
37
+
38
+ preload(*)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module CurrentMethods
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ def search_current(params, ...)
10
+ search(params, YiffSpace.config.current_class.user, ...)
11
+ end
12
+
13
+ def with_current(instance, method, *args, **, &)
14
+ attr = args.grep(Symbol)
15
+ other = args - attr
16
+ hashes = other.grep(Hash)
17
+ params = other.grep(ActionController::Parameters)
18
+ other -= hashes + params
19
+ other.compact_blank!
20
+ options = [attr.index_with(YiffSpace.config.current_class.user), *hashes, *params.map(&:to_hash)].inject(:merge)
21
+ instance.send(method, *other, **options, **, &)
22
+ end
23
+
24
+ def new_with_current(...)
25
+ with_current(self, :new, ...)
26
+ end
27
+
28
+ def create_with_current(...)
29
+ with_current(self, :create, ...)
30
+ end
31
+
32
+ def create_with_current!(...)
33
+ with_current(self, :create!, ...)
34
+ end
35
+ end
36
+
37
+ def update_with_current(...)
38
+ self.class.with_current(self, :update, ...)
39
+ end
40
+
41
+ def update_with_current!(...)
42
+ self.class.with_current(self, :update!, ...)
43
+ end
44
+
45
+ def destroy_with_current(*attrs)
46
+ attrs.each do |attr|
47
+ send("#{attr}=", YiffSpace.config.current_class.user)
48
+ end
49
+ destroy
50
+ end
51
+
52
+ def destroy_with_current!(*attrs)
53
+ attrs.each do |attr|
54
+ send("#{attr}=", YiffSpace.config.current_class.user)
55
+ end
56
+ destroy!
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module HasBitFlags
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ # NOTE: the ordering of attributes has to be fixed
10
+ # new attributes should be appended to the end.
11
+ def has_bit_flags(attributes, options = {})
12
+ field = options[:field] || :bit_flags
13
+
14
+ define_singleton_method("flag_value_for") do |key|
15
+ value = attributes[key.to_s]
16
+ return value if value
17
+
18
+ raise(ArgumentError, "Invalid flag: #{key}")
19
+ end
20
+
21
+ attributes.each do |name, value|
22
+ scope(name.to_sym, -> { where.has_bits(field => value) })
23
+ define_method(name) do
24
+ send(field) & value == value
25
+ end
26
+
27
+ define_method("#{name}=") do |val|
28
+ if val.to_s =~ /[t1y]/
29
+ send("#{field}=", send(field) | value)
30
+ else
31
+ send("#{field}=", send(field) & ~value)
32
+ end
33
+ end
34
+
35
+ define_method("#{name}_was") do
36
+ send("#{field}_was") & value == value
37
+ end
38
+
39
+ define_method("#{name}_before_last_save") do
40
+ send("#{field}_before_last_save") & value == value
41
+ end
42
+
43
+ alias_method("#{name}?", name)
44
+ alias_method("#{name}_before_last_save?", "#{name}_before_last_save")
45
+ alias_method("#{name}_was?", "#{name}_was")
46
+
47
+ define_method("#{name}_changed?") do
48
+ send("#{name}_was") != send(name)
49
+ end
50
+
51
+ define_method("saved_change_to_#{name}?") do
52
+ send("#{name}_before_last_save") != send(name)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module UserMethods
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ def belongs_to_user(*, **)
10
+ UserAttribute.new(self, *, **, db: true)
11
+ end
12
+
13
+ def resolvable(*, **)
14
+ UserAttribute.new(self, *, **, db: false)
15
+ end
16
+
17
+ def with_user(instance, method, user, *args, **, &)
18
+ default = { new: :creator, create: :creator, create!: :creator, update: :updater, update!: :updater, destroy: :destroyer, destroy!: :destroyer }.fetch(method)
19
+ attrs = args.grep(Symbol)
20
+ attrs = [default] if default && attrs.empty?
21
+ other = args - attrs
22
+ hashes = other.grep(Hash)
23
+ params = other.grep(ActionController::Parameters)
24
+ other -= hashes + params
25
+ other.compact_blank!
26
+ options = [attrs.index_with(user), *hashes, *params.map(&:to_hash)].inject(:merge)
27
+ if %i[destroy destroy!].include?(method)
28
+ options.each do |attr, value|
29
+ instance.send("#{attr}=", value)
30
+ end
31
+ instance.send(method)
32
+ else
33
+ instance.send(method, *other, **options, **, &)
34
+ end
35
+ end
36
+
37
+ def new_with(...)
38
+ with_user(self, :new, ...)
39
+ end
40
+
41
+ def create_with(...)
42
+ new_with(...).tap(&:save)
43
+ end
44
+
45
+ def create_with!(...)
46
+ new_with(...).tap(&:save!)
47
+ end
48
+ end
49
+
50
+ def update_with(...)
51
+ self.class.with_user(self, :update, ...)
52
+ end
53
+
54
+ def update_with!(...)
55
+ self.class.with_user(self, :update!, ...)
56
+ end
57
+
58
+ def destroy_with(...)
59
+ self.class.with_user(self, :destroy, ...)
60
+ end
61
+
62
+ def destroy_with!(...)
63
+ self.class.with_user(self, :destroy!, ...)
64
+ end
65
+
66
+ private
67
+
68
+ def put(attr, value, overwrite: false)
69
+ if respond_to?(:"#{attr}_id")
70
+ send(:"#{attr}=", value) if overwrite || send(:"#{attr}_id").blank?
71
+ elsif respond_to?(attr)
72
+ send(:"#{attr}=", value) if overwrite || send(attr).blank?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Concerns
5
+ module UserNameMethods
6
+ extend(ActiveSupport::Concern)
7
+
8
+ module ClassMethods
9
+ def name_to_id(name)
10
+ normalized_name = normalize_name(name)
11
+ Cache.fetch("uni:#{normalized_name}", expires_in: 4.hours) do
12
+ ::User.where("lower(name) = ?", normalized_name).pick(:id)
13
+ end
14
+ end
15
+
16
+ # @param arguments [Array<String, Array<String>>] a list of names
17
+ # @return [Hash{String => Integer, nil}] a hash of normalized names to user IDs
18
+ def bulk_name_to_id(*arguments)
19
+ names = arguments.flatten.map { |n| normalize_name(n) }
20
+ results = names.index_with { |name| Utils::Cache.fetch("uni:#{name}") }.compact_blank
21
+ missing = names - results.keys
22
+ fetched = ::User.where("lower(name) IN (?)", missing).select(:id, :name).to_h { |u| [normalize_name(u.name), u.id] }
23
+ not_found = (missing - fetched.keys).index_with { nil }
24
+ fetched.each { |name, id| Utils::Cache.write("uni:#{name}", id, expires_in: 4.hours) }
25
+ { **results, **fetched, **not_found }
26
+ end
27
+
28
+ def name_or_id_to_id(name)
29
+ return name[1..].to_i if name =~ /\A!\d+\z/
30
+
31
+ ::User.name_to_id(name)
32
+ end
33
+
34
+ def name_or_id_to_id_forced(name)
35
+ return name.to_i if name =~ /\A\d+\z/
36
+
37
+ ::User.name_to_id(name)
38
+ end
39
+
40
+ def id_to_name(user_id)
41
+ RequestStore[:id_name_cache] ||= {}
42
+ return RequestStore[:id_name_cache][user_id] if RequestStore[:id_name_cache].key?(user_id)
43
+
44
+ name = Cache.fetch("uin:#{user_id}", expires_in: 4.hours) do
45
+ ::User.where(id: user_id).pick(:name) || YiffSpace.config.anonymous_user_name.call
46
+ end
47
+ RequestStore[:id_name_cache][user_id] = name
48
+ name
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end