farce 0.4.1-java → 0.6.0-java

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +32 -0
  14. data/docs/variants.md +9 -0
  15. data/lib/farce/abstract/atom.rb +5 -1
  16. data/lib/farce/abstract/concurrent_map.rb +122 -0
  17. data/lib/farce/abstract/counter.rb +36 -1
  18. data/lib/farce/abstract/executor.rb +30 -7
  19. data/lib/farce/abstract/flag.rb +12 -0
  20. data/lib/farce/abstract/molecule.rb +14 -14
  21. data/lib/farce/abstract/pool.rb +158 -47
  22. data/lib/farce/abstract/priority_queue.rb +1 -1
  23. data/lib/farce/abstract/publisher.rb +189 -0
  24. data/lib/farce/abstract/queue.rb +29 -3
  25. data/lib/farce/abstract/scheduler.rb +41 -14
  26. data/lib/farce/abstract/timer_queue.rb +3 -3
  27. data/lib/farce/abstract/vector.rb +3 -1
  28. data/lib/farce/atom.rb +15 -2
  29. data/lib/farce/clock.rb +20 -7
  30. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  31. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  32. data/lib/farce/concurrent/event.rb +60 -0
  33. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  34. data/lib/farce/concurrent/local_var.rb +65 -0
  35. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  36. data/lib/farce/concurrent/tuple.rb +74 -0
  37. data/lib/farce/concurrent.rb +50 -0
  38. data/lib/farce/config.rb +112 -2
  39. data/lib/farce/counter.rb +5 -15
  40. data/lib/farce/deduper.rb +12 -4
  41. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  42. data/lib/farce/engine/jruby/map.rb +8 -6
  43. data/lib/farce/engine/jruby/signal.rb +2 -2
  44. data/lib/farce/engine/jvm/counter.rb +0 -2
  45. data/lib/farce/engine/jvm/farce.jar +0 -0
  46. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  47. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  48. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  49. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  50. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  51. data/lib/farce/engine/ruby.rb +5 -1
  52. data/lib/farce/engine/shared/exchanger.rb +2 -2
  53. data/lib/farce/engine/shared/queue.rb +43 -5
  54. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  55. data/lib/farce/engine/shared/strict_map.rb +5 -0
  56. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  57. data/lib/farce/engine/shared/trie.rb +14 -1
  58. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  59. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  60. data/lib/farce/engine/shared/unshared_vector.rb +11 -8
  61. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  62. data/lib/farce/engine/shared/vector.rb +2 -0
  63. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  64. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  65. data/lib/farce/engine/shared.rb +2 -0
  66. data/lib/farce/engine/truffleruby/map.rb +8 -6
  67. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  68. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  69. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  70. data/lib/farce/engine/truffleruby.rb +9 -0
  71. data/lib/farce/extension.rb +5 -3
  72. data/lib/farce/integrations/concurrent.rb +16 -3
  73. data/lib/farce/integrations/sorted_set.rb +1 -0
  74. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  75. data/lib/farce/internal/fake_ractor.rb +1 -1
  76. data/lib/farce/internal/flag_waiting.rb +1 -1
  77. data/lib/farce/internal/freeze.rb +1 -5
  78. data/lib/farce/internal/lease_initialization.rb +1 -1
  79. data/lib/farce/internal/lease_pool_state.rb +1 -1
  80. data/lib/farce/internal/lease_state.rb +2 -2
  81. data/lib/farce/internal/local_executor_methods.rb +3 -3
  82. data/lib/farce/internal/map_value_modes.rb +17 -9
  83. data/lib/farce/internal/pool_settings.rb +9 -0
  84. data/lib/farce/internal/pool_worker.rb +34 -21
  85. data/lib/farce/internal/reservation_waiting.rb +1 -1
  86. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  87. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  88. data/lib/farce/internal.rb +19 -8
  89. data/lib/farce/lazy.rb +3 -0
  90. data/lib/farce/lazy_ref.rb +3 -0
  91. data/lib/farce/local/counter.rb +4 -10
  92. data/lib/farce/local/lease.rb +2 -9
  93. data/lib/farce/local/lease_map.rb +2 -9
  94. data/lib/farce/local/pool.rb +38 -5
  95. data/lib/farce/local/publisher.rb +30 -0
  96. data/lib/farce/local/scheduler.rb +18 -5
  97. data/lib/farce/local/vector.rb +3 -2
  98. data/lib/farce/map.rb +8 -1
  99. data/lib/farce/memoize.rb +319 -0
  100. data/lib/farce/pool.rb +5 -0
  101. data/lib/farce/priority_queue.rb +1 -1
  102. data/lib/farce/publisher.rb +57 -0
  103. data/lib/farce/shareable.rb +3 -0
  104. data/lib/farce/signal.rb +1 -1
  105. data/lib/farce/strict/publisher.rb +31 -0
  106. data/lib/farce/strict/vector.rb +3 -2
  107. data/lib/farce/strict/weak_cache.rb +199 -0
  108. data/lib/farce/subscription/definitions.rb +27 -0
  109. data/lib/farce/subscription.rb +121 -0
  110. data/lib/farce/transaction/map.rb +35 -1
  111. data/lib/farce/unshared/pool.rb +1 -1
  112. data/lib/farce/unshared/publisher.rb +25 -0
  113. data/lib/farce/unshared/scheduler.rb +3 -3
  114. data/lib/farce/unshared/vector.rb +3 -2
  115. data/lib/farce/vector.rb +20 -16
  116. data/lib/farce/version.rb +1 -1
  117. data/lib/farce/walker/definitions.rb +31 -2
  118. data/lib/farce/walker/modification.rb +28 -14
  119. data/lib/farce/walker.rb +9 -1
  120. data/lib/farce/weak_atom.rb +5 -2
  121. data/lib/farce/weak_key_map.rb +1 -1
  122. data/lib/farce/weak_map.rb +1 -1
  123. data/lib/farce/weak_value_map.rb +1 -1
  124. data/lib/farce.rb +3 -1
  125. metadata +22 -2
data/lib/farce/map.rb CHANGED
@@ -48,7 +48,14 @@ module Farce
48
48
 
49
49
  private
50
50
 
51
- def new_map(**) = Internal::StrictMap.new(**)
51
+ def new_map(
52
+ compare_by_identity: false,
53
+ compare_keys_by_identity: compare_by_identity,
54
+ compare_values_by_identity: compare_by_identity
55
+ )
56
+ Internal::StrictMap.new(compare_by_identity:, compare_keys_by_identity:, compare_values_by_identity:)
57
+ end
58
+
52
59
  def freeze_backend = @map
53
60
 
54
61
  Internal.prepare_map_access(self, :modes)
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # Ractor-friendly memoization module.
7
+ #
8
+ # Replaces the following pattern:
9
+ #
10
+ # ```ruby
11
+ # class MyClass
12
+ # def time
13
+ # @time ||= Time.now
14
+ # end
15
+ # end
16
+ # ```
17
+ #
18
+ # Using `Farce::Memoize` instead gives you:
19
+ #
20
+ # * Allows memoization after freezing
21
+ # * Is Ractor-safe (with optional scoping or transfer modes) and thread-safe
22
+ # * Adds one instance variable per memoized method (avoids hash lookup and doesn't bust Ruby's data structures)
23
+ # * Is Marshal-dump/load friendly
24
+ # * Allows weak memoization, where the memoized value is kept until it is garbage collected
25
+ # * Does not need to run code on the main Ractor for memoization
26
+ #
27
+ # Methods with arguments cannot be memoized. Explicitly use a {Farce::Abstract::Map map} instead.
28
+ #
29
+ # @example Using `memoize` method
30
+ # class MyClass
31
+ # include Farce::Memoize
32
+ # memoize def time = Time.now
33
+ # end
34
+ #
35
+ # instance = MyClass.new
36
+ # first_time = instance.time
37
+ # second_time = instance.time
38
+ #
39
+ # first_time == second_time # => true
40
+ # first_time == MyClass.new.time # => false
41
+ #
42
+ # @example Scope-specific memoization
43
+ # class MyClass
44
+ # include Farce::Memoize
45
+ #
46
+ # def first = expensive_computation(0)
47
+ # def last = expensive_computation(-1)
48
+ #
49
+ # # Memoize separately per Ractor
50
+ # memoize :first, :last, scope: :ractor
51
+ # end
52
+ #
53
+ # @example Setting a mode
54
+ # class MyClass
55
+ # include Farce::Memoize
56
+ # memoize :expensive, mode: :make_shareable
57
+ # def expensive = expensive_computation
58
+ # end
59
+ #
60
+ # @example Without `memoize` extension
61
+ # class MyClass
62
+ # include Farce::Memoize[:time]
63
+ # def time = Time.now
64
+ # end
65
+ #
66
+ # @see Farce::Lazy
67
+ # @see Farce::LazyRef
68
+ #
69
+ # @!macro memoize_params
70
+ # @overload $0(*methods, scope: :global, mode: :smart, weak: false, visibility: nil)
71
+ # @param methods [Array<Symbol>]
72
+ # The methods to memoize
73
+ #
74
+ # @param scope [Symbol]
75
+ # The scope of the memoization.
76
+ # In addition to the {Farce::SCOPES normal scopes} it may also be set to `:global` or `:unshared`.
77
+ #
78
+ # @param mode [Symbol]
79
+ # The mode of the memoization. Ignored for any scope other than `:global`.
80
+ # Use `:strict` to cache shareable results directly, including with `weak: true`.
81
+ # Defaults to `:raise` with `weak: true`, which also supports `:make_shareable` and `:dedup`.
82
+ #
83
+ # @param weak [Boolean]
84
+ # Whether to use weak memoization.
85
+ # If enabled, memoized values may be garbage collected when no longer referenced.
86
+ # In this case, next access to the memoized method may recompute the value.
87
+ #
88
+ # @param visibility [Symbol, nil]
89
+ # The visibility of the memoized methods.
90
+ # May be one of `:public`, `:protected`, or `:private`.
91
+ # If set to `nil`, {Farce::Memoize#memoize} will attempt to infer the visibility based on the context.
92
+ # Other methods will default to `:public`.
93
+ module Memoize
94
+ # A named module preserves the nil marker's identity across Marshal round trips.
95
+ NIL_VALUE = Module.new.freeze
96
+ private_constant :NIL_VALUE
97
+
98
+ # Mixins used to implement memoization for different methods, scopes, and modes
99
+ # @see Farce::Memoize
100
+ class Mixin < Module
101
+ KNOWN = Strict::Map.new
102
+ private_constant :KNOWN
103
+
104
+ # @!macro memoize_params
105
+ # Creates or retrieves a memoization mixin for the given methods and options.
106
+ # @return [Mixin] the memoization mixin
107
+ def self.new(*methods, **options)
108
+ return super unless self == Mixin
109
+ methods = Farce.dedup(methods.map(&:to_sym).uniq.sort)
110
+ options = Farce.dedup(normalize_options(options))
111
+ map = KNOWN.store_if_absent(options) { Strict::Map.new }
112
+ map.store_if_absent(methods) { super(methods, **options) }
113
+ end
114
+
115
+ # @api private
116
+ def self.normalize_options(options)
117
+ options[:visibility] &&= options[:visibility].to_sym
118
+
119
+ options[:scope] ||= :global
120
+ options[:visibility] ||= :public
121
+ options[:weak] ||= false
122
+
123
+ if options[:scope] == :global
124
+ options[:mode] ||= options[:weak] ? :raise : :smart
125
+ else
126
+ options.delete(:mode)
127
+ end
128
+
129
+ options
130
+ end
131
+ private_class_method :normalize_options
132
+
133
+ # @return [Array<Symbol>] the memoized methods
134
+ attr_reader :memoized_methods
135
+
136
+ # @return [Symbol] the scope of the memoization
137
+ attr_reader :scope
138
+
139
+ # @return [Symbol, nil] the transfer mode, or nil for a scoped cache
140
+ attr_reader :mode
141
+
142
+ # @return [Symbol] the visibility of the memoized methods
143
+ attr_reader :visibility
144
+
145
+ # @api private
146
+ def initialize(methods, scope:, weak:, mode: nil, visibility: :public)
147
+ @memoized_methods = methods
148
+ @scope = scope
149
+ @mode = mode
150
+ @weak = !!weak # rubocop:disable Style/DoubleNegation
151
+ @visibility = visibility
152
+
153
+ super()
154
+
155
+ unless %i[public protected private].include?(visibility)
156
+ raise ArgumentError, "unsupported visibility: #{visibility.inspect}"
157
+ end
158
+
159
+ atom_class = weak ? "WeakAtom" : "Atom"
160
+
161
+ case scope
162
+ when :global
163
+ if mode == :strict
164
+ init = "::Farce::Strict::#{atom_class}.new"
165
+ else
166
+ modes = weak ? Internal::WeakModeManager::MODES : MODES
167
+ raise ArgumentError, "unsupported mode: #{mode.inspect}" unless modes.include?(mode)
168
+ init = "::Farce::#{atom_class}.new(mode: #{mode.inspect})"
169
+ end
170
+ args = methods.join(", ")
171
+ args += ", mode: #{mode.inspect}" unless mode == :smart
172
+ when :unshared
173
+ init = "::Farce::Unshared::#{atom_class}.new"
174
+ args = "#{methods.join(", ")}, scope: :unshared"
175
+ when *SCOPES
176
+ init = "::Farce::Local::#{atom_class}.new(scope: #{scope.inspect})"
177
+ args = "#{methods.join(", ")}, scope: #{scope.inspect}"
178
+ else
179
+ raise ArgumentError, "unsupported scope: #{scope.inspect}"
180
+ end
181
+
182
+ set_temporary_name(-"Farce::Memoize[#{args}]") if instance_of?(Mixin)
183
+ initializer = +""
184
+ # Overlapping mixins need separate slots. Stable names also survive Marshal across processes.
185
+ slot = "#{methods.join(",").unpack1("H*")}_#{scope}_#{mode}_#{@weak}_#{visibility}"
186
+
187
+ methods.each do |method|
188
+ unless method =~ /\A[a-z_]\w*[!?]?\z/i && method != :initialize
189
+ raise ArgumentError, "unsuitable method name: #{method.inspect}"
190
+ end
191
+ ivar = "@_#{method.to_s.unpack1("H*")}_memoize_#{slot}_atom"
192
+ initializer << " #{ivar} = #{init}\n"
193
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
194
+ #{visibility} def #{method}
195
+ value = #{ivar}.value || #{ivar}.store_if_absent do
196
+ result = super()
197
+ nil.equal?(result) ? NIL_VALUE : result
198
+ end
199
+ value unless NIL_VALUE.equal?(value)
200
+ end
201
+ RUBY
202
+ end
203
+
204
+ # Explicit forwarding preserves nested constructor keywords on JRuby.
205
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
206
+ def initialize(*, **, &)
207
+ #{initializer}
208
+ super
209
+ end
210
+ RUBY
211
+ end
212
+
213
+ # @return [Boolean] whether the memoization is weak
214
+ def weak? = @weak
215
+
216
+ # allow `include Farce::Memoize[...]` to work like `prepend`
217
+ private def append_features(base) = base.prepend(self)
218
+ end
219
+
220
+ # @api private
221
+ def self.append_features(base) = base.extend(self)
222
+ private_class_method :append_features
223
+
224
+ # Creates or retrieves the memoization mixin for the given methods, scope, and mode
225
+ # @!macro memoize_params
226
+ # @return [Mixin] memoization mixin for the given methods, scope, and mode
227
+ def self.[](...) = Mixin.new(...)
228
+
229
+ # @!macro memoize_params
230
+ # Creates a memoization mixin for the given arguments and adds it to the current class.
231
+ #
232
+ # Can be used as a prefix to `def`:
233
+ #
234
+ # ```ruby
235
+ # memoize def some_method = expensive_computation
236
+ # ```
237
+ #
238
+ # Or called separately with the method names, useful for passing additional options:
239
+ #
240
+ # ```ruby
241
+ # def some_method = expensive_computation
242
+ #
243
+ # # memoize some_method per thread
244
+ # memoize :some_method, scope: :thread
245
+ # ```
246
+ #
247
+ # ## Visibility of Memoized Methods
248
+ #
249
+ # If no visibility is provided, it will determine the visibility based on the existing method definitions.
250
+ # If none exist, it will default to public. This happens when `memoize` is called.
251
+ # Make sure you set the visibility before calling this method.
252
+ #
253
+ # This works:
254
+ #
255
+ # ```ruby
256
+ # memoize private def some_method = expensive_computation
257
+ # ```
258
+ #
259
+ # As does this:
260
+ #
261
+ # ```ruby
262
+ # private
263
+ # memoize def some_method = expensive_computation
264
+ # ```
265
+ #
266
+ # Or this:
267
+ #
268
+ # ```ruby
269
+ # private def some_method = expensive_computation
270
+ # memoize :some_method
271
+ # ```
272
+ #
273
+ # But this doesn't, as the mixin's method will end up public:
274
+ #
275
+ # ```ruby
276
+ # private memoize def some_method = expensive_computation
277
+ # ```
278
+ #
279
+ # Same for this one, as the mixin's method will also end up public:
280
+ #
281
+ # ```ruby
282
+ # memoize :some_method
283
+ # private def some_method = expensive_computation
284
+ # ```
285
+ #
286
+ # You can of course set visibility explicitly when calling `memoize`:
287
+ #
288
+ # ```ruby
289
+ # memoize :some_method, visibility: :private
290
+ # ```
291
+ #
292
+ # @return [Array<Symbol>] the list of memoized methods
293
+ def memoize(*methods, visibility: nil, warn: true, **)
294
+ if visibility.nil?
295
+ groups = methods.group_by do |method|
296
+ next :protected if protected_method_defined?(method)
297
+ next :private if private_method_defined?(method)
298
+ :public
299
+ end
300
+ return groups.flat_map { memoize(*_2, visibility: _1, warn:, **) }
301
+ end
302
+
303
+ if warn
304
+ methods.each do |name|
305
+ method = instance_method(name)
306
+ next if method.parameters.empty?
307
+ warn "#{method.owner}##{name} expects parameters and may be unsuitable for memoization"
308
+ rescue NameError => e
309
+ raise e unless e.name == name.to_sym
310
+ end
311
+ end
312
+
313
+ mixin = Farce::Memoize[*methods, visibility:, **]
314
+ prepend mixin
315
+
316
+ mixin.memoized_methods
317
+ end
318
+ end
319
+ end
data/lib/farce/pool.rb CHANGED
@@ -11,6 +11,11 @@ module Farce
11
11
  # `min_size` retire after `shrink_after` without queued or running work.
12
12
  # By default, the pool starts with no workers and can shrink back to zero.
13
13
  #
14
+ # Worker limits, task limits, scaling delays, and queue capacity can change while
15
+ # the pool runs. Lowering `max_size` retires extra workers as they become idle.
16
+ # Busy workers keep queued work runnable. Raising `min_size` starts workers
17
+ # immediately. Queued tasks are retained.
18
+ #
14
19
  # Most importantly, it exposes a {#schedule} method compatible with {Scheduler#schedule}.
15
20
  #
16
21
  # @example
@@ -42,7 +42,7 @@ module Farce
42
42
  result = @reverse_order ? @queue.pop_last { empty = true } : @queue.pop { empty = true }
43
43
 
44
44
  return @manager.unwrap(result) unless empty
45
- remaining = remaining_timeout(deadline)
45
+ remaining = Clock.remaining(deadline)
46
46
  return block_given? ? yield : nil unless wait_pop(timeout: remaining)
47
47
  end
48
48
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # A message multiplexer.
7
+ # Implements the publisher-subscriber protocol.
8
+ #
9
+ # @example
10
+ # publisher = Farce::Publisher.new
11
+ #
12
+ # # Count how many messages have been received
13
+ # counter = Farce::Counter.new
14
+ # publisher.subscribe(counter)
15
+ #
16
+ # # Trigger a callback whenever a message is published
17
+ # publisher.subscribe do |message|
18
+ # puts "Received message: #{message}"
19
+ # end
20
+ #
21
+ # # Send messages to a queue
22
+ # queue = Farce::Queue.new
23
+ # publisher.subscribe(queue)
24
+ #
25
+ # # Keep track of the last published value
26
+ # atom = Farce::Atom.new
27
+ # publisher.subscribe(atom)
28
+ #
29
+ # # Send messages to a Ractor
30
+ # ractor = Ractor.new do
31
+ # while message = Ractor.receive
32
+ # puts "Received message in Ractor: #{message}"
33
+ # end
34
+ # end
35
+ # publisher.subscribe(ractor)
36
+ #
37
+ # # Publish some messages
38
+ # publisher.publish("Hello, world!")
39
+ # publisher.publish("Another message")
40
+ #
41
+ # queue.pop # => "Hello, world!"
42
+ # atom.value # => "Another message"
43
+ # counter.value # => 2
44
+ class Publisher < Farce::Abstract::Publisher
45
+ include Shareable::Unfreezable
46
+
47
+ # @overload initialize(*subscribers)
48
+ # (see Farce::Abstract#initialize)
49
+ def initialize(*subscribers)
50
+ @map = Strict::Map.new
51
+ super()
52
+ subscribers.each { subscribe(it) }
53
+ end
54
+
55
+ private attr_reader :map
56
+ end
57
+ end
@@ -13,6 +13,9 @@ module Farce
13
13
  # object = MyClass.new
14
14
  # Ractor.shareable?(object) # => true
15
15
  module Shareable
16
+ # @api private
17
+ def self.included(base) = Internal.prepare_shareable_class(base)
18
+
16
19
  # A structurally frozen wrapper whose logical frozen state is owned by its backend.
17
20
  # The including class must implement a private `freeze_backend` method.
18
21
  #
data/lib/farce/signal.rb CHANGED
@@ -80,7 +80,7 @@ module Farce
80
80
  result = yield
81
81
  return result if result
82
82
 
83
- remaining = deadline - Clock.now if deadline
83
+ remaining = Clock.remaining(deadline)
84
84
  return if remaining && !remaining.positive?
85
85
  return unless wait(observed, timeout: remaining)
86
86
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Strict
7
+ # Strict variant of {Farce::Publisher}.
8
+ class Publisher < Farce::Abstract::Publisher
9
+ include Shareable::Unfreezable
10
+
11
+ # @overload initialize(*subscribers)
12
+ # (see Farce::Abstract#initialize)
13
+ def initialize(*subscribers)
14
+ @map = Strict::Map.new
15
+ super()
16
+ subscribers.each { subscribe(it) }
17
+ end
18
+
19
+ # (see Farce::Abstract::Publisher#publish)
20
+ def publish(message = nil)
21
+ return super if Ractor.shareable?(message)
22
+ raise Ractor::IsolationError, "value must be Ractor-shareable"
23
+ end
24
+ alias send publish
25
+ alias << publish
26
+ alias broadcast publish
27
+
28
+ private attr_reader :map
29
+ end
30
+ end
31
+ end
@@ -8,10 +8,11 @@ module Farce
8
8
  class Vector < Abstract::Vector
9
9
  include Shareable::Delegated
10
10
 
11
- # @param source [Array, nil] Initial values. The source array is not retained.
11
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil. The source array is not retained.
12
12
  # @param compare_by_identity [Boolean] Whether values are compared by identity.
13
+ # @raise [ArgumentError] If the size is negative.
13
14
  def initialize(source = nil, compare_by_identity: false)
14
- @vector = Internal::Vector.new(source, compare_by_identity:)
15
+ @vector = Internal::Vector.new(normalize_vector_source(source), compare_by_identity:)
15
16
  super()
16
17
  end
17
18
 
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Strict
7
+ # A weak cache that allows reusing previously generated values without holding strong references to them.
8
+ # Only shareable keys and values can be used.
9
+ #
10
+ # @example
11
+ # cache = Farce::Strict::WeakCache.new
12
+ # value = cache.store_if_absent(:key) { ExpensiveClass.new.freeze }
13
+ class WeakCache
14
+ include Shareable::Delegated
15
+ include Internal::Inspect
16
+
17
+ # Creates a new WeakCache instance if it outperforms {Strict::WeakValueMap} on the current platform.
18
+ # Otherwise it creates a standard WeakValueMap instance.
19
+ # @return [WeakCache, Strict::WeakValueMap] The created cache instance.
20
+ def self.create(...) = Internal.native_weak_maps? ? Strict::WeakValueMap.new(...) : WeakCache.new(...)
21
+
22
+ # @param initial_mapping [Hash, Farce::Abstract::Map, #each, nil]
23
+ # The entries to store initially. Each iteration must yield a key-value pair.
24
+ # Nil creates an empty map.
25
+ # @param compare_by_identity [Boolean]
26
+ # Whether keys use object identity instead of `hash` and `eql?`.
27
+ def initialize(initial_mapping = nil, compare_by_identity: false)
28
+ @map = Strict::WeakValueMap.new(initial_mapping, compare_keys_by_identity: compare_by_identity)
29
+ initialize_cache
30
+ super()
31
+ end
32
+
33
+ # @api private
34
+ def marshal_dump = [1, @map]
35
+
36
+ # @api private
37
+ def marshal_load(data)
38
+ @map, = Internal::MarshalSupport.payload(data, 1)
39
+ initialize_cache
40
+ publish_shareable
41
+ end
42
+
43
+ # Retrieves or stores a value for the given key.
44
+ # The block is called to generate the value if the key is absent.
45
+ # Concurrent calls for the same key reuse the generated value.
46
+ # Once that value is collected, a later call generates it again.
47
+ #
48
+ # @param key [BasicObject] The key to look up.
49
+ # @yield Called without arguments when the key is absent.
50
+ # @yieldreturn [BasicObject] The value to store and return.
51
+ # @return [BasicObject] The associated value.
52
+ def store_if_absent(key, &)
53
+ raise LocalJumpError, "no block given" unless block_given?
54
+ @cache.store_if_absent(key, &)
55
+ end
56
+
57
+ alias compute_if_absent store_if_absent
58
+
59
+ # Store a value only when the key is absent.
60
+ # Existing nil and false values count as present.
61
+ # @param key [BasicObject] The key to look up or store.
62
+ # @param value [BasicObject] The value to store if the key is absent.
63
+ # @return [BasicObject, nil] The existing value, or nil when a value was inserted.
64
+ def put_if_absent(key, value)
65
+ inserted = false
66
+ previous = store_if_absent(key) do
67
+ inserted = true
68
+ value
69
+ end
70
+ inserted ? nil : previous
71
+ end
72
+
73
+ # Look up a value without generating it.
74
+ # @param key [BasicObject] The key to look up.
75
+ # @return [BasicObject, nil] The associated value, or nil if the key is absent.
76
+ def [](key) = @cache.fetch(key) { nil } # rubocop:disable Style/RedundantFetchBlock
77
+
78
+ # Return a lookup key and its associated value if present.
79
+ # @param key [BasicObject] The key to look up.
80
+ # @return [Array(BasicObject, BasicObject), nil] The pair `[key, value]`, or nil if the key is absent.
81
+ def assoc(key)
82
+ value = fetch(key) { return nil }
83
+ [key, value]
84
+ end
85
+
86
+ # Return whether a live entry exists, including entries with nil or false values.
87
+ # @param key [BasicObject] The key to look up.
88
+ # @return [Boolean]
89
+ def key?(key)
90
+ @cache.fetch(key) { return false }
91
+ true
92
+ end
93
+
94
+ alias has_key? key?
95
+ alias include? key?
96
+ alias member? key?
97
+
98
+ # Look up a value in nested maps or arrays.
99
+ # @param key [BasicObject] The key to look up.
100
+ # @param rest [Array<BasicObject>] Additional keys or indices for the nested value.
101
+ # @return [BasicObject, nil] The nested value, or nil if an intermediate value is absent or nil.
102
+ def dig(key, *rest)
103
+ value = self[key]
104
+ return value if rest.empty? || value.nil?
105
+ value.dig(*rest)
106
+ end
107
+
108
+ # Return the stored key matching a lookup key.
109
+ # @param key [BasicObject] The key to look up.
110
+ # @return [BasicObject, nil] The stored key, or nil if the key is absent.
111
+ def getkey(key) = @map.getkey(key)
112
+
113
+ # Fetch the value associated with a key, using the same missing-key behavior as Hash#fetch.
114
+ # If both a default and a block are provided, the block takes precedence.
115
+ #
116
+ # @overload fetch(key)
117
+ # @param key [BasicObject] The key to look up.
118
+ # @return [BasicObject] The associated value.
119
+ # @raise [KeyError] If the key is absent.
120
+ #
121
+ # @overload fetch(key, default)
122
+ # @param key [BasicObject] The key to look up.
123
+ # @param default [BasicObject] The value to return if the key is absent.
124
+ # @return [BasicObject] The associated value or `default`.
125
+ #
126
+ # @overload fetch(key)
127
+ # @param key [BasicObject] The key to look up.
128
+ # @yield [key] Called if the key is absent.
129
+ # @yieldparam key [BasicObject] The missing key.
130
+ # @yieldreturn [BasicObject] The value to return.
131
+ # @return [BasicObject] The associated value or the block result.
132
+ def fetch(*arguments)
133
+ unless arguments.length.between?(1, 2)
134
+ raise ArgumentError, "wrong number of arguments (given #{arguments.length}, expected 1..2)"
135
+ end
136
+
137
+ key, default = arguments
138
+ warn "block supersedes default value argument", uplevel: 1 if block_given? && arguments.length == 2
139
+ @cache.fetch(key) do
140
+ return yield(key) if block_given?
141
+ return default if arguments.length == 2
142
+ raise KeyError.new("key not found: #{key.inspect}", receiver: self, key: key)
143
+ end
144
+ end
145
+
146
+ # Fetch values in the requested order, using {#fetch} for missing keys.
147
+ # @param keys [Array<BasicObject>] The keys to look up.
148
+ # @yield [key] Called for each missing key.
149
+ # @yieldparam key [BasicObject] The missing key.
150
+ # @yieldreturn [BasicObject] The fallback value, which is not stored.
151
+ # @return [Array<BasicObject>] The associated values or block results.
152
+ def fetch_values(*keys, &) = keys.map { fetch(it, &) }
153
+
154
+ # Look up values in the requested order, returning nil for missing keys.
155
+ # @param keys [Array<BasicObject>] The keys to look up.
156
+ # @return [Array<BasicObject>] The associated values, with nil for missing keys.
157
+ def values_at(*keys) = keys.map { self[it] }
158
+
159
+ # @return [Boolean] Whether keys are compared by identity instead of `hash` and `eql?`.
160
+ def compare_keys_by_identity? = @map.compare_keys_by_identity?
161
+
162
+ # (see #compare_keys_by_identity?)
163
+ def compare_by_identity? = compare_keys_by_identity?
164
+
165
+ # @return [Boolean] Whether keys must be Ractor-shareable, including copied mutable strings.
166
+ def shareable_keys? = true
167
+
168
+ # @return [Boolean] Whether values must be Ractor-shareable.
169
+ def shareable_values? = true
170
+
171
+ # @return [Boolean] Whether keys are referenced weakly.
172
+ def weak_keys? = false
173
+
174
+ # @return [Boolean] Whether values are referenced weakly.
175
+ def weak_values? = true
176
+
177
+ # @api private
178
+ def inspect_with(inspector)
179
+ super do
180
+ yield if block_given?
181
+ inspector.breakable
182
+ inspector.group("{", "}") do
183
+ inspector.breakable ""
184
+ inspector.seplist(@map, nil, :each) do |key, value|
185
+ inspector.group { inspector.hash_pair(key, value) }
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ alias to_s inspect
192
+
193
+ private
194
+
195
+ def initialize_cache = @cache = Internal.native_weak_maps? ? @map : Internal::WeakCacheMirror.new(@map)
196
+ def freeze_backend = @map
197
+ end
198
+ end
199
+ end