farce 0.1.0-arm-linux-gnu → 0.3.0-arm-linux-gnu

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -1
  3. data/README.md +111 -17
  4. data/docs/gems/dry-types.md +8 -8
  5. data/docs/modes.md +75 -15
  6. data/docs/scopes.md +3 -1
  7. data/docs/variants.md +18 -0
  8. data/ext/farce/atom.c +37 -22
  9. data/ext/farce/bounded_map.c +17 -13
  10. data/ext/farce/containers.h +15 -0
  11. data/ext/farce/counter.c +13 -18
  12. data/ext/farce/exchanger.c +5 -1
  13. data/ext/farce/farce.c +1 -1
  14. data/ext/farce/flag.c +39 -3
  15. data/ext/farce/io.c +3 -3
  16. data/ext/farce/lock.c +17 -23
  17. data/ext/farce/map.c +88 -66
  18. data/ext/farce/priority_queue.c +55 -43
  19. data/ext/farce/queue.c +10 -4
  20. data/ext/farce/reactor.c +52 -25
  21. data/ext/farce/reactor.h +1 -1
  22. data/ext/farce/signal.c +5 -1
  23. data/ext/farce/transaction.c +20 -24
  24. data/ext/farce/tree_map.c +33 -18
  25. data/ext/farce/trie.c +81 -43
  26. data/ext/farce/unshareable.c +168 -19
  27. data/ext/farce/unshared_io_pool.h +21 -12
  28. data/ext/farce/unshared_signal.c +13 -2
  29. data/ext/farce/unshared_wait.h +3 -0
  30. data/ext/farce/vector.c +82 -18
  31. data/ext/farce/weak_map.c +75 -63
  32. data/lib/farce/_yard/macros.rb +18 -3
  33. data/lib/farce/abstract/executor.rb +130 -0
  34. data/lib/farce/abstract/flag.rb +73 -0
  35. data/lib/farce/abstract/pool.rb +334 -0
  36. data/lib/farce/abstract/queue.rb +4 -3
  37. data/lib/farce/abstract/scheduler.rb +539 -93
  38. data/lib/farce/abstract/set.rb +17 -5
  39. data/lib/farce/abstract/sorted_set.rb +2 -2
  40. data/lib/farce/abstract/timer_queue.rb +49 -10
  41. data/lib/farce/abstract/vector.rb +510 -28
  42. data/lib/farce/atom.rb +1 -1
  43. data/lib/farce/class_mirror.rb +20 -7
  44. data/lib/farce/config.rb +7 -1
  45. data/lib/farce/engine/jruby/signal.rb +7 -4
  46. data/lib/farce/engine/jvm/flag.rb +15 -3
  47. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  48. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  49. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  50. data/lib/farce/engine/ruby.rb +7 -0
  51. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  52. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  53. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  54. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  55. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  56. data/lib/farce/engine/shared/vector.rb +8 -0
  57. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  58. data/lib/farce/engine/shared.rb +9 -0
  59. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  60. data/lib/farce/envelope.rb +54 -7
  61. data/lib/farce/exchanger.rb +1 -1
  62. data/lib/farce/flag.rb +2 -0
  63. data/lib/farce/integrations/active_support.rb +1 -1
  64. data/lib/farce/integrations/bson.rb +1 -1
  65. data/lib/farce/integrations/cbor.rb +1 -1
  66. data/lib/farce/integrations/concurrent.rb +3 -3
  67. data/lib/farce/integrations/date.rb +8 -0
  68. data/lib/farce/integrations/dry_types.rb +7 -4
  69. data/lib/farce/integrations/json.rb +2 -2
  70. data/lib/farce/integrations/msgpack.rb +1 -1
  71. data/lib/farce/integrations/oj.rb +3 -4
  72. data/lib/farce/integrations/psych.rb +1 -1
  73. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  74. data/lib/farce/integrations/sorted_set.rb +1 -1
  75. data/lib/farce/integrations/weakref.rb +9 -1
  76. data/lib/farce/integrations/yajl.rb +3 -3
  77. data/lib/farce/integrations.rb +33 -23
  78. data/lib/farce/internal/flag_waiting.rb +30 -0
  79. data/lib/farce/internal/freeze.rb +8 -3
  80. data/lib/farce/internal/lease_initialization.rb +1 -1
  81. data/lib/farce/internal/local_executor_methods.rb +33 -0
  82. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  83. data/lib/farce/internal/map_value_modes.rb +1 -1
  84. data/lib/farce/internal/scheduled_task.rb +1 -1
  85. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  86. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  87. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  88. data/lib/farce/internal.rb +14 -0
  89. data/lib/farce/lazy.rb +4 -4
  90. data/lib/farce/lazy_ref.rb +3 -3
  91. data/lib/farce/lfu_map.rb +1 -1
  92. data/lib/farce/local/flag.rb +4 -0
  93. data/lib/farce/local/pool.rb +111 -0
  94. data/lib/farce/local/scheduler.rb +219 -0
  95. data/lib/farce/local/scoped.rb +1 -1
  96. data/lib/farce/lru_map.rb +1 -1
  97. data/lib/farce/map.rb +2 -2
  98. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  99. data/lib/farce/mode_manager.rb +12 -3
  100. data/lib/farce/molecule.rb +2 -2
  101. data/lib/farce/pool.rb +2 -290
  102. data/lib/farce/port.rb +4 -4
  103. data/lib/farce/priority_queue.rb +1 -1
  104. data/lib/farce/proxy/register.rb +6 -0
  105. data/lib/farce/proxy/supervisor.rb +1 -1
  106. data/lib/farce/proxy.rb +9 -4
  107. data/lib/farce/queue.rb +1 -1
  108. data/lib/farce/scheduler.rb +1 -502
  109. data/lib/farce/set.rb +1 -1
  110. data/lib/farce/sorted_set.rb +1 -1
  111. data/lib/farce/task.rb +285 -0
  112. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  113. data/lib/farce/timer_queue.rb +20 -6
  114. data/lib/farce/transaction/vector.rb +43 -9
  115. data/lib/farce/transaction/wrapper.rb +14 -0
  116. data/lib/farce/tree_map.rb +1 -1
  117. data/lib/farce/unsafe/lfu_map.rb +2 -0
  118. data/lib/farce/unsafe/lru_map.rb +2 -0
  119. data/lib/farce/unsafe/tree_map.rb +2 -0
  120. data/lib/farce/unshareable.rb +8 -19
  121. data/lib/farce/unshared/atom.rb +3 -0
  122. data/lib/farce/unshared/lfu_map.rb +2 -0
  123. data/lib/farce/unshared/lru_map.rb +2 -0
  124. data/lib/farce/unshared/map.rb +3 -0
  125. data/lib/farce/unshared/pool.rb +44 -0
  126. data/lib/farce/unshared/scheduler.rb +89 -0
  127. data/lib/farce/unshared/set.rb +1 -0
  128. data/lib/farce/unshared/sorted_set.rb +1 -0
  129. data/lib/farce/unshared/tree_map.rb +5 -1
  130. data/lib/farce/unshared/vector.rb +3 -0
  131. data/lib/farce/unshared/weak_atom.rb +5 -1
  132. data/lib/farce/unshared/weak_key_map.rb +3 -0
  133. data/lib/farce/unshared/weak_map.rb +3 -0
  134. data/lib/farce/unshared/weak_set.rb +1 -0
  135. data/lib/farce/unshared/weak_value_map.rb +3 -0
  136. data/lib/farce/vector.rb +44 -19
  137. data/lib/farce/version.rb +1 -1
  138. data/lib/farce.rb +20 -25
  139. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5656b13b69823fb23c39da2cd293e9a708c29251d3b2ea5dacd2e152231f53a5
4
- data.tar.gz: 9d431ff8db40caca5cc43390beb6d21c14dc6e4757d8413963c3fc0a8621d106
3
+ metadata.gz: d8295409e81c0cbabd7868166bfbf29d456d99babab6dee561feb8d3676a865d
4
+ data.tar.gz: 4fdf1d5d80980fca658668abf24ca9395524e65d30e4e4ce20745463c157117f
5
5
  SHA512:
6
- metadata.gz: ac6c5d52fa76b742fdfa00b8175f8f07576711a2010e2ead4a35558f562f66b39b12f803f8ac0b62cf8b69faded767ce0259fcb90c6e40bdeec644fcbcf6ff6c
7
- data.tar.gz: 941781e468218ee1ac53bcfa1c9f8ea70d30b00e5734ac3af79b4582a0a545ea56aee8807d1a8e7dd05159ac29874fd79e015a732e1f7874ae8d29da0bfc3138
6
+ metadata.gz: 86ebdae86e939f495543480a53b7df19aed571cadd1207a605122737424d14f6033584e0d96869ceafb9bf9383d34020b8ff388f4569b523f8d5cfa8f73b665f
7
+ data.tar.gz: 65d5fbdf1843f628c60f5e07e49b551ae4f1354c34afa72e02b95b18615a0d86d1a97c6bf4203c9933ce27ff34c433b07be01baa5721e907edcef22d8a241178
data/CHANGELOG.md CHANGED
@@ -4,6 +4,132 @@ Farce follows [Semantic Versioning 2.0](http://semver.org/). Any public method o
4
4
 
5
5
  ## Development Releases
6
6
 
7
+ ### Farce 0.3.0 (2026-10-13)
8
+
9
+ #### New Features
10
+
11
+ ##### Breaking Changes
12
+
13
+ * `Abstract::Scheduler` is no longer a superclass of `Pool`. Use `Abstract::Executor` instead.
14
+
15
+ ##### Executor Improvements
16
+
17
+ `Scheduler` and `Pool` have the following improvements:
18
+
19
+ * They now support `try_schedule`, which discards tasks and returns `nil` when their internal queues are at capacity.
20
+ * They now have `Unshared` and `Local` variants, running in a Ractor-local context (Pool will manage a Thread pool instead of a Ractor pool).
21
+
22
+ Creates a thread pool with a maximum size of 2 and a queue capacity of 10:
23
+
24
+ ```ruby
25
+ # A thread pool local to the current Ractor
26
+ thread_pool = Farce::Unshared::Pool.new(max_size: 2, capacity: 10)
27
+
28
+ # this should only run approximately 10 tasks on CRuby
29
+ 500.times { thread_pool.try_schedule { puts "Task executed" } }
30
+ ```
31
+
32
+ ##### Tasks
33
+
34
+ A new class, `Task`, has been introduced to represent asynchronous tasks that can be scheduled and executed within Farce's executor framework,
35
+ executed manually, or handed off to any other executor supporting procs or callable objects.
36
+
37
+ ```ruby
38
+ task = Farce::Task.schedule do
39
+ puts "Task started"
40
+ sleep 1
41
+ :success
42
+ ensure
43
+ puts "Task finished"
44
+ end
45
+
46
+ task.alive? # => true
47
+ task.value # => :success (blocks until the task completes)
48
+ ```
49
+
50
+ #### Bug Fixes
51
+
52
+ * Calling `wait_*` methods on atoms on JRuby no longer blocks a fiber scheduler.
53
+
54
+ ### Farce 0.2.0 (2026-10-11)
55
+
56
+ #### New Features
57
+
58
+ ##### Waiting on Flags
59
+
60
+ You can now wait for a `Farce::Flag` or `Farce::Local::Flag` to change its value:
61
+
62
+ ```ruby
63
+ flag = Farce::Flag.new
64
+
65
+ Thread.new do
66
+ flag.wait_until_set
67
+ puts "Flag has been activated!"
68
+ end
69
+
70
+ flag.set
71
+ ```
72
+
73
+ ##### Timer Queue Entries
74
+
75
+ `TimerQueue#push` and `TimerQueue#try_push` now return a `TimerQueue::Entry` object on a successful push.
76
+ This allows you to cancel a scheduled value using the returned entry:
77
+
78
+ ```ruby
79
+ queue = Farce::TimerQueue.new
80
+ entry = queue.push(:work, delay: 60)
81
+ entry.delete # => true
82
+ ```
83
+
84
+ ##### Expanded Vector Functionality
85
+
86
+ Improved Array compatibility for `Vector`, `Strict::Vector`, `Unshared::Vector`, and `Local::Vector`:
87
+
88
+ * Added `to_ary`, `delete`, and `delete_if`.
89
+ * Added range and start/length indexing, slice assignment, multiple-value `push` and `append`, and `pop(count)`.
90
+ * Added `shift`, `unshift`, `prepend`, `insert`, `delete_at`, `slice!`, `replace`, and `fill`.
91
+ * Added `map!`, `collect!`, `keep_if`, `select!`, `filter!`, `reject!`, `compact!`, and `uniq!`.
92
+ * Added `reverse!`, `rotate!`, `shuffle!`, `sort!`, `sort_by!`, and `flatten!`.
93
+ * Added `flatten`, `transpose`, `product`, `combination`, `repeated_combination`, `permutation`, and `repeated_permutation`.
94
+ * Added lexicographic `<=>`. Equality and hashing continue to use object identity.
95
+
96
+ ##### Miscellaneous
97
+
98
+ * Unshareable objects may now be frozen.
99
+ * Trying to make an unshareable object shareable raises a better error message now (`Ractor::Error` with a descriptive message, instead of a `NoMethodError`).
100
+ * Added a new transfer mode: `:marshal`, which will copy objects between Ractors using Ruby's [Marshal](https://docs.ruby-lang.org/en/4.0/Marshal.html) serialization.
101
+
102
+ #### Bug Fixes
103
+
104
+ * Improve compatibility with other gems modifying `Kernel#require`, work around an issue with Ruby 4.0's runtime ractor require patch and alias methods chains not working nicely with `Module#prepend`.
105
+
106
+ #### Performance Improvements
107
+
108
+ ##### Rework CRuby native extension
109
+
110
+ Incorporated the advice from ["Tips for updating Ruby C extensions for efficiently using Ruby 4 VM APIs"](https://github.com/ruby/ruby/blob/master/doc/efficient_extensions.md).
111
+
112
+ With this change, all long-living Farce objects can be compacted and moved to old GC generations. This was not the case previously for maps and vectors that had snapshots taken of them (either for iteration or for transactions). These ended up being permanently kept in young generations. A long-living map or vector would therefore cause frequent GC runs to be quite expensive.
113
+
114
+ | Operation | Before | After | Change |
115
+ | --------------------------------------------------- | ------- | ------- | ----------- |
116
+ | `Map#each` over 1024 entries | 34.1 μs | 35.2 μs | 3% slower |
117
+ | Transaction with a map with 16 entries | 25.2 μs | 25.3 μs | 0.4% slower |
118
+ | Transaction with a map with 1024 entries | 26.7 μs | 29.2 μs | 9% slower |
119
+ | Transaction with a vector with 10,000 entries | 26.1 μs | 29.4 μs | 13% slower |
120
+ | GC while keeping a vector with 10,000 entries alive | 67.6 μs | 28.6 μs | 58% faster |
121
+
122
+ Beyond these specific cases:
123
+ * Other iteration methods will have a similar performance impact to `Map#each` (for instance, `Map#transform_values`).
124
+ * Methods not involving iterations, like reading or writing a specific map entry, do not show any measurable performance impact.
125
+
126
+ Measurements were taken on CRuby 4.0.7 (arm64-darwin23) with YJIT enabled. Measurements taken against Ruby's main branch show similar results.
127
+
128
+ #### Other changes
129
+
130
+ * Avoid circular require warnings when automatically loading third-party integrations.
131
+ * Do not install a require hook if automatic integration loading is disabled.
132
+
7
133
  ### Farce 0.1.0 (2026-10-09)
8
134
 
9
- * Initial release
135
+ Initial release.
data/README.md CHANGED
@@ -117,6 +117,7 @@ puts output
117
117
  - [`Farce.schedule`](#farceschedule)
118
118
  - [Schedulers](#schedulers)
119
119
  - [Ractor Pools](#ractor-pools)
120
+ - [Tasks](#tasks)
120
121
  - [Third-Party Fiber Schedulers](#third-party-fiber-schedulers)
121
122
  - [Integrations](#integrations)
122
123
  - [Active Support](#active-support)
@@ -133,6 +134,7 @@ puts output
133
134
  - [Constants](#constants)
134
135
  - [Compatibility and Dependencies](#compatibility-and-dependencies)
135
136
  - [Ruby](#ruby)
137
+ - [Optional Dependencies](#optional-dependencies)
136
138
  - [Similar Projects](#similar-projects)
137
139
  - [Installation](#installation)
138
140
  - [Globally](#globally)
@@ -415,6 +417,23 @@ list.to_a # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
415
417
  list.sum # => 45
416
418
  ```
417
419
 
420
+ There is one important difference to arrays: Bulk mutations, such as `Vector#map!` are atomic. Either the block succeeds for all entries, or if it raises an exception then none of the changes are applied. Such mutations may also not write to the same vector from within the block.
421
+
422
+ ```ruby
423
+ list = Farce::Vector[1, 2, 3, 4]
424
+
425
+ begin
426
+ list.map! do |value|
427
+ raise "error" if value == 2
428
+ value * 2
429
+ end
430
+ rescue => e
431
+ end
432
+
433
+ # This would return 2 if list was an array
434
+ list.first # => 1
435
+ ```
436
+
418
437
  #### Sets
419
438
 
420
439
  `Farce::Set` is to `Set` what `Farce::Map` is to `Hash`. It implements the same interface, with additional atomic operations.
@@ -536,7 +555,7 @@ end
536
555
 
537
556
  #### Queues
538
557
 
539
- `Farce::Queue` is a drop-in replacement for Ruby's `Queue` and `SizedQueue`. It provides a blocking and a non-blocking API.
558
+ `Farce::Queue` is an alternative to Ruby's `Queue` and `SizedQueue`. It provides a superset of their API, offering both blocking and a non-blocking API, while implementing Ractor-sharing via [sharing modes](#sharing-modes).
540
559
 
541
560
  ```ruby
542
561
  queue = Farce::Queue.new
@@ -569,7 +588,7 @@ queue.pop # => :b
569
588
 
570
589
  ##### Timer Queues
571
590
 
572
- Timer queues are similar to priority queues, sorted by float values, but these values are timestamps (based on [Farce::Clock](#additional-classes), not on Ruby's Time, which isn't monotonic). Any `pull` will block until the next timestamp has been reached.
591
+ Timer queues are similar to priority queues, sorted by float values, but these values are timestamps (based on [Farce::Clock](#additional-classes), not on Ruby's Time, which isn't monotonic). Any `pop` will block until the next timestamp has been reached.
573
592
 
574
593
  ```ruby
575
594
  queue = Farce::TimerQueue.new
@@ -579,7 +598,16 @@ queue.push(:ok, in: 1.5)
579
598
  queue.pop # => :ok
580
599
  ```
581
600
 
582
- Timer queues are useful for implementing schedulers (which is what Farce uses them for under the hood), so they have one additional feature: You can delete an entry if you know its timestamp.
601
+ Timer queues are useful for implementing schedulers (which is what Farce uses them for under the hood). Both `push` and `try_push` return a cancellation handle when they succeed. Use `entry.delete` or `queue.delete(entry)` to cancel a scheduled value. The handle exposes its `queue` and scheduled `clock` time.
602
+
603
+ ```ruby
604
+ queue = Farce::TimerQueue.new
605
+ entry = queue.push(:work, in: 60)
606
+
607
+ entry.delete # => true
608
+ ```
609
+
610
+ You can also delete a value if you know its timestamp:
583
611
 
584
612
  ```ruby
585
613
  queue = Farce::TimerQueue.new
@@ -589,6 +617,9 @@ queue.push(:first, at: timestamp - 0.1)
589
617
  queue.push(:second, at: timestamp)
590
618
  queue.push(:third, at: timestamp + 0.1)
591
619
 
620
+ fourth = queue.push(:fourth, at: timestamp + 0.2)
621
+ queue.delete(fourth)
622
+
592
623
  queue.delete(:second, at: timestamp)
593
624
 
594
625
  result = []
@@ -601,7 +632,7 @@ result # => [:first, :third]
601
632
  Queues support setting a maximum capacity. If the capacity has been reached it will block any pushes to create back pressure.
602
633
 
603
634
  ```ruby
604
- queue = Farce::Queue.new(2)
635
+ queue = Farce::Queue.new(capacity: 2)
605
636
 
606
637
  queue.try_push(:a) # => true
607
638
  queue.try_push(:b) # => true
@@ -683,17 +714,22 @@ The [default variants](#variants-and-scopes) of most [data structures](#data-str
683
714
 
684
715
  These modes are:
685
716
 
686
- | Mode | What happens to non-shareable data | A typical use |
687
- | ----------------- | ------------------------------------------------------------------------------------ | ------------------------------------------- |
688
- | `:copy` (default) | Transfers a copy and leaves the original usable. This is the default. | Send a snapshot of a request. |
689
- | `:move` | Transfers ownership and makes the original inaccessible. | Hand a completed batch to a consumer. |
690
- | `:local` | Keeps the same object in its originating Ractor. | Pass work between local threads or fibers. |
691
- | `:make_shareable` | Calls `Ractor.make_shareable` on the original. | Publish finished configuration. |
692
- | `:mutable` | Copies non-shareable objects into a [`Farce::Mutable`](#mutables). | Synchronize mutations across Ractors. |
693
- | `:shareable_copy` | Makes a shareable copy and leaves the original alone. | Publish a snapshot of an editable document. |
694
- | `:dedup` | Deduplicates the value, then makes it shareable. May update and freeze the original. | Reuse repeated message contents. |
695
- | `:proxy` | Creates a [`Farce::Proxy`](#proxies) that executes calls in the original Ractor. | Share access to a mutable object. |
696
- | `:raise` | Raises `Ractor::IsolationError`. | Enforce a shareable-data boundary. |
717
+ | Mode | What happens to non-shareable data | A typical use |
718
+ | --- | --- | --- |
719
+ | `:smart` (default) | Applies smart rules or a dedicated proxy, raising for unsupported values. | Share supported values with less copying. |
720
+ | `:smart_raise` | Applies smart rules or a dedicated proxy, with a raising fallback. | Reject unsupported transfers. |
721
+ | `:smart_copy` | Applies smart rules or a dedicated proxy, then tries copying and Marshal. Default for `Farce::Port`. | Send values with a copying fallback. |
722
+ | `:smart_move` | Applies smart rules or a dedicated proxy, then tries moving. | Hand off unsupported values. |
723
+ | `:copy` | Transfers a copy and leaves the original usable. | Send a snapshot of a request. |
724
+ | `:marshal` | Serializes a snapshot and reconstructs it once per receiving Ractor. | Transfer an Unshared container.|
725
+ | `:move` | Transfers ownership and makes the original inaccessible. | Hand a completed batch to a consumer. |
726
+ | `:local` | Keeps the same object in its originating Ractor. | Pass work between local threads or fibers. |
727
+ | `:make_shareable` | Calls `Ractor.make_shareable` on the original. | Publish finished configuration. |
728
+ | `:mutable` | Copies non-shareable objects into a [`Farce::Mutable`](#mutables). | Synchronize mutations across Ractors. |
729
+ | `:shareable_copy` | Makes a shareable copy and leaves the original alone. | Publish a snapshot of an editable document. |
730
+ | `:dedup` | Deduplicates the value, then makes it shareable. May update and freeze the original. | Reuse repeated message contents. |
731
+ | `:proxy` | Creates a [`Farce::Proxy`](#proxies) that executes calls in the original Ractor. | Share access to a mutable object. |
732
+ | `:raise` | Raises `Ractor::IsolationError`. | Enforce a shareable-data boundary. |
697
733
 
698
734
  Here's an example:
699
735
 
@@ -974,7 +1010,7 @@ list = Farce::Vector.new
974
1010
  list[0] = 42
975
1011
 
976
1012
  # Atomically replace list[0] with 256 if the value is still 42
977
- list.compare_and_swap(0, 42, 256)
1013
+ list.compare_and_set(0, 42, 256)
978
1014
  ```
979
1015
 
980
1016
  #### Observability and Signaling
@@ -1187,6 +1223,43 @@ pool = Farce::Pool.new(min_size: 4, max_size: 10, grow_after: 0.1)
1187
1223
  pool.schedule { puts "Hello from the pool!" }
1188
1224
  ```
1189
1225
 
1226
+ #### Tasks
1227
+
1228
+ If you want to retrieve the result of an asynchronous operation, or monitor if it is still running, you can use `Farce::Task`:
1229
+
1230
+ ```ruby
1231
+ task = Farce::Task.schedule(1, 2) do |a, b|
1232
+ sleep 1 # simulate intense computation
1233
+ a + b
1234
+ end
1235
+
1236
+ task.alive? # => true
1237
+ task.value # => 3 (after approximately 1 second)
1238
+ task.join # would block, but doesn't, as the task has already completed
1239
+ ```
1240
+
1241
+ Tasks can also be created without immediately scheduling them, allowing you to control when and how they are executed:
1242
+
1243
+ ```ruby
1244
+ task = Farce::Task.new { :success }
1245
+ task.alive? # => true
1246
+ task.value(0.1) # => nil (after 100 milliseconds)
1247
+
1248
+ Farce.schedule(&task)
1249
+ task.value # => :success
1250
+ ```
1251
+
1252
+ This allows you to use third-party executors:
1253
+
1254
+ ```ruby
1255
+ require "concurrent"
1256
+ executor = Concurrent.global_fast_executor
1257
+ task = Farce::Task.new { :success }
1258
+ executor.post(&task)
1259
+
1260
+ task.value # => :success
1261
+ ```
1262
+
1190
1263
  #### Third-Party Fiber Schedulers
1191
1264
 
1192
1265
  Both `Farce::Scheduler` and `Farce::Pool` run a Fiber scheduler under the hood to execute tasks. This is a Farce-internal scheduler by default, but you can replace it with your own if you want:
@@ -1385,7 +1458,7 @@ Other classes Farce provides include:
1385
1458
  * `Exchanger`: A synchronization point for two-way data swapping between Threads, Ractors, and/or Fibers. Drop-in replacement for concurrent-ruby's exchanger.
1386
1459
  * `Lazy`: Lazily initialized value.
1387
1460
  * `LazyRef`: A [reference](#references) for a lazily initialized value.
1388
- * `ThreadScheduler`: An alternative [scheduler](#scheduling-code) creating a new thread for each unit of work. Used on TruffleRuby.
1461
+ * `ThreadExecutor`: A [task executor](#scheduling-code) creating a new thread for each unit of work. Used on TruffleRuby.
1389
1462
  * `Walker`: A tool for walking a Ruby object tree.
1390
1463
  * `WeakValue`: A value object version of [`WeakRef`](#weakref). Allows handling references more explicitly, without automatic method delegation.
1391
1464
 
@@ -1433,6 +1506,27 @@ Moreover:
1433
1506
  * Dropping support for a CRuby version is only done in major releases.
1434
1507
  * If support for an older CRuby version is dropped, Farce will still backport security fixes for at least as long as that CRuby version is [still receiving security fixes](https://www.ruby-lang.org/en/downloads/branches/).
1435
1508
 
1509
+ ### Optional Dependencies
1510
+
1511
+ Farce's optional [integrations](#integrations) depend on external libraries. Automated tests only run against recent versions of these libraries, but the code is written to be compatible with as wide a range as possible.
1512
+
1513
+ Farce should be compatible with these versions:
1514
+
1515
+ | Gem | Minimum Version | Release Date |
1516
+ |-------------------|-----------------|--------------|
1517
+ | `ractor-tmvar` | 0.3.0 | 2026-10-06 |
1518
+ | `msgpack` | 1.7.0 | 2023-03-29 |
1519
+ | `concurrent-ruby` | 1.1.8 | 2021-01-20 |
1520
+ | `dry-types` | 1.0.0 | 2019-04-23 |
1521
+ | `activesupport` | 5.1.0 | 2017-04-27 |
1522
+ | `oj` | 2.8.1 | 2014-04-21 |
1523
+ | `bson` | 2.0.0 | 2013-12-02 |
1524
+ | `psych` | 1.1.0 | 2011-03-30 |
1525
+
1526
+ Any version of `cbor`, `json`, `ractor-sharing`, `sorted_set`, and `weakref` should be supported
1527
+
1528
+ If you want to use any of these in a version that is incompatible with Farce, you can [disable automatic integration loading](#disable-automatic-loading).
1529
+
1436
1530
  ### Similar Projects
1437
1531
 
1438
1532
  * [ractor-shim](https://github.com/eregon/ractor-shim/) provides similar functionality to `Farce::Ractor`. See [the comparison document](docs/gems/ractor-shim.md) for more details.
@@ -49,7 +49,7 @@ end
49
49
  numbers = Types::IntegerVector[["1", 2]]
50
50
  numbers.class # => Farce::Vector
51
51
  numbers.to_a # => [1, 2]
52
- numbers.mode # => :copy
52
+ numbers.mode # => :smart
53
53
  ```
54
54
 
55
55
  The bare `Vector` accepts any Array members. Imported dry namespaces control
@@ -243,8 +243,7 @@ and dry-types' `Strict` namespace configure separate parts of the conversion.
243
243
  | `:local` | `Farce::Local::Vector`, `Farce::Local::Map`, `Farce::Local::Set`, `Farce::Local::Counter`, `Farce::Local::Flag`, `Farce::Local::Atom` |
244
244
 
245
245
  The integration imports only classes that Farce provides for the selected
246
- variant. Strict has an Atom but no Counter or Flag. Unshared has none of these
247
- three scalar-backed types.
246
+ variant.
248
247
 
249
248
  For a single shared collection type, use `.with(mode: ...)`:
250
249
 
@@ -258,10 +257,11 @@ vector = Types::LocalValueVector[[payload]]
258
257
  vector[0].equal?(payload) # => true
259
258
  ```
260
259
 
261
- The supported shared modes are `:copy`, `:local`, `:make_shareable`,
262
- `:shareable_copy`, and `:raise`. They apply to collections and Atom. Counter and
263
- Flag have no transfer mode. `:move` is rejected because dry-types creates and
264
- examines intermediate values during coercion.
260
+ The supported shared modes are `:smart`, `:smart_raise`, `:smart_copy`,
261
+ `:copy`, `:local`, `:make_shareable`, `:shareable_copy`, and `:raise`.
262
+ The default is `:smart`. They apply to collections and Atom. Counter and Flag have
263
+ no transfer mode. `:move` and `:smart_move` are rejected because dry-types creates
264
+ and examines intermediate values during coercion.
265
265
 
266
266
  ## Farce input and ownership
267
267
 
@@ -278,7 +278,7 @@ converted.to_a # => [1]
278
278
  ```
279
279
 
280
280
  The explicit read establishes where transfer and ownership happen. This rule
281
- also applies when the container's default mode is `:copy` because an individual
281
+ also applies when the container's default mode is `:smart` because an individual
282
282
  entry might have been inserted with `mode: :move`.
283
283
 
284
284
  Vector materialization uses its normal snapshot. Map and set materialization
data/docs/modes.md CHANGED
@@ -14,7 +14,9 @@ This improves over the default control via `Ractor::Port`'s `move:` option.
14
14
  - [Introduction: What Ruby gives you](#introduction-what-ruby-gives-you)
15
15
  - [What `move: true` does on `Ractor::Port`](#what-move-true-does-on-ractorport)
16
16
  - [Farce's Modes](#farces-modes)
17
+ - [Smart modes: apply rules before transferring](#smart-modes-apply-rules-before-transferring)
17
18
  - [`:copy`: keep working with the original](#copy-keep-working-with-the-original)
19
+ - [`:marshal`: reconstruct a serialized snapshot](#marshal-reconstruct-a-serialized-snapshot)
18
20
  - [`:move`: hand off a finished batch](#move-hand-off-a-finished-batch)
19
21
  - [`:local`: preserve object identity](#local-preserve-object-identity)
20
22
  - [`:make_shareable`: publish the original](#make_shareable-publish-the-original)
@@ -76,7 +78,7 @@ port.receive # => [4, 5]
76
78
  port.close
77
79
  ```
78
80
 
79
- Farce extends this choice with nine named modes.
81
+ Farce extends this choice with fourteen named modes.
80
82
 
81
83
  ## Farce's Modes
82
84
 
@@ -84,7 +86,12 @@ The following modes are accepted by `Farce::Port`. They apply to non-shareable v
84
86
 
85
87
  | Mode | What happens to non-shareable data | A typical use |
86
88
  | --- | --- | --- |
87
- | `:copy` (default) | Transfers a copy and leaves the original usable. This is the default. | Send a snapshot of a request. |
89
+ | `:smart` | Applies smart rules or a dedicated proxy. Raises for unsupported values. Default for most mode-backed classes. | Share supported values with less copying. |
90
+ | `:smart_raise` | Applies smart rules or a dedicated proxy, with a raising fallback. | Reject unsupported transfers. |
91
+ | `:smart_copy` | Applies smart rules or a dedicated proxy, then tries copying and Marshal. Default for `Farce::Port`. | Send values with a copying fallback. |
92
+ | `:smart_move` | Applies smart rules or a dedicated proxy, then tries moving. | Hand off unsupported values. |
93
+ | `:copy` | Transfers a copy and leaves the original usable. | Send a snapshot of a request. |
94
+ | `:marshal` | Serializes a snapshot and reconstructs it once per receiving Ractor. | Transfer an Unshared container with fresh storage. |
88
95
  | `:move` | Transfers ownership and makes the original inaccessible. | Hand a completed batch to a consumer. |
89
96
  | `:local` | Keeps the same object in its originating Ractor. | Pass work between local threads or fibers. |
90
97
  | `:make_shareable` | Calls `Ractor.make_shareable` on the original. | Publish finished configuration. |
@@ -94,12 +101,38 @@ The following modes are accepted by `Farce::Port`. They apply to non-shareable v
94
101
  | `:proxy` | Creates a `Farce::Proxy` that executes calls in the original Ractor. | Share access to a mutable object. |
95
102
  | `:raise` | Raises `Ractor::IsolationError`. | Enforce a shareable-data boundary. |
96
103
 
104
+ ### Smart modes: apply rules before transferring
105
+
106
+ Most mode-backed classes default to `:smart`. `Farce::Port` defaults to `:smart_copy` so values without a suitable rule or dedicated proxy can still be copied. Classes with restricted weak-value modes keep their `:raise` default.
107
+
108
+ Smart modes pass already-shareable values through unchanged. Mutable strings become frozen, deduplicated strings without freezing the source string. Time values are made shareable in place. The Date integration does the same for Date and DateTime values.
109
+
110
+ With `:smart`, arrays and Ruby sets containing only shareable elements are made shareable in place. Symbol-keyed hashes containing only shareable values are made shareable in every smart mode. This can freeze the original, including with `:smart_copy` and `:smart_move`.
111
+
112
+ For arrays and Ruby sets, `:smart_copy` makes a shallow copy and tries to prepare each element with `:smart_raise`. If all results are directly usable, it makes that copy shareable. Otherwise, it copies the original graph, preserving aliases and cycles. If native copying raises a standard error, it tries Marshal reconstruction. If both fail, it raises the original copying error. It does not recursively insert managed envelopes into the container. Identity-based Farce sets use the prepared element for membership and reject a copying envelope fallback.
113
+
114
+ A dedicated proxy applies only to the exact registered class. Smart rules inherit through the source class hierarchy and can customize individual smart modes. Without applicable behavior, `:smart` and `:smart_raise` raise `Ractor::IsolationError`, `:smart_copy` tries copying and Marshal, and `:smart_move` tries moving.
115
+
116
+ ```ruby
117
+ class Job
118
+ attr_reader :id
119
+ def initialize(id) = @id = id
120
+ end
121
+
122
+ Farce::ModeManager::SmartRule.define(Job, :marshal)
123
+ queue = Farce::Queue.new
124
+ queue << Job.new(42)
125
+ queue.pop.id # => 42
126
+ ```
127
+
128
+ Use an explicit `:copy` mode when callers need independently mutable copies even for values that smart rules could make shareable. On JRuby and TruffleRuby, ordinary values are already shareable and pass through unchanged.
129
+
97
130
  ### `:copy`: keep working with the original
98
131
 
99
132
  Copying is a good starting point for ordinary hashes, arrays, and strings. The receiver can change its copy without changing the sender's data. Shareable parts of the message can still be reused.
100
133
 
101
134
  ```ruby
102
- port = Farce::Port.new
135
+ port = Farce::Port.new(mode: :copy)
103
136
  request = { ids: [10, 20] }
104
137
  port.send(request)
105
138
  request[:ids] << 30
@@ -113,6 +146,27 @@ port.close
113
146
 
114
147
  Copying is not a way to serialize every Ruby object. Some objects cannot be copied across Ractors. Choose a mode that fits the value, or send ordinary data from which the receiver can build what it needs.
115
148
 
149
+ ### `:marshal`: reconstruct a serialized snapshot
150
+
151
+ Use `:marshal` for values with a Marshal representation, including `Farce::Unshared` containers whose backends cannot be copied directly between native Ractors. Farce calls `Marshal.dump` when preparing the value. A shareable envelope holds the frozen bytes. The first read in each Ractor calls `Marshal.load`, then caches that Ractor's reconstructed value.
152
+
153
+ ```ruby
154
+ port = Farce::Port.new(mode: :marshal)
155
+ source = Farce::Unshared::Map.new({ jobs: [] })
156
+ port.send(source)
157
+ source[:jobs] << :sender_change
158
+
159
+ received = port.receive
160
+ received[:jobs] # => []
161
+ received[:jobs] << :receiver_change
162
+ source[:jobs] # => [:sender_change]
163
+ port.close
164
+ ```
165
+
166
+ Farce's Marshal hooks rebuild containers from logical contents and settings. The receiver gets fresh backend storage and synchronization state. Repeated references and supported cycles within the serialized graph are preserved. A reconstructed Unshared container remains unshareable.
167
+
168
+ Contents must support Marshal. Procs, anonymous classes, and live coordination objects such as queues and leases cannot be serialized. A serialization failure raises `TypeError` before the value is sent or stored. Unlike native Ractor copying, Marshal may also reconstruct nested shareable objects. Top-level shareable values still pass through unchanged.
169
+
116
170
  ### `:move`: hand off a finished batch
117
171
 
118
172
  Moving is useful when a producer has finished building a message and will never use it again. It avoids copying the payload. Do not retain a plan to reuse the original or its moved nested objects after sending.
@@ -262,7 +316,7 @@ proxy.length # => 1
262
316
  port.close
263
317
  ```
264
318
 
265
- Each delegated call involves a request and response. Non-shareable arguments and return values are copied by default, while methods that return the original object return its proxy. Keep the owning Ractor alive while callers use the proxy. Calls raise `Farce::Ractor::RemoteError` after the owner exits. Access through other references to the original object still needs its own coordination.
319
+ Each delegated call involves a request and response. Arguments and return values use `:smart` rules by default. Unsupported values raise unless a wrapper selects another mode. Methods that return the original object return its proxy. Keep the owning Ractor alive while callers use the proxy. Calls raise `Farce::Ractor::RemoteError` after the owner exits. Access through other references to the original object still needs its own coordination.
266
320
 
267
321
  On JRuby and TruffleRuby, ordinary values are already shareable, so this mode passes them through without creating a proxy.
268
322
 
@@ -326,7 +380,7 @@ Automatic local transfer takes precedence even over an explicit `mode:` or `move
326
380
 
327
381
  ## Other classes that support modes
328
382
 
329
- The same choices appear in several Farce APIs. Containers normally default to `:copy` and unwrap their managed values when you read them. Setting a container's mode does not make the values it returns shareable.
383
+ The same choices appear in several Farce APIs. Containers normally default to `:smart` and unwrap their managed values when you read them. Setting a container's mode does not make the values it returns shareable.
330
384
 
331
385
  | Class | Where to select a mode | What it controls |
332
386
  | --- | --- | --- |
@@ -337,6 +391,7 @@ The same choices appear in several Farce APIs. Containers normally default to `:
337
391
  | `Farce::WeakAtom` | `new`, `store`, `update`, and other replacement operations | Weakly held values. Supports only `:raise`, `:make_shareable`, and `:dedup`. Defaults to `:raise`. |
338
392
  | `Farce::Atom` | `new`, `store`, `swap`, `update`, and other replacement operations | The stored value. |
339
393
  | `Farce::Lazy`, `Farce::LazyRef` | `new` | The result computed once by the factory. |
394
+ | `Farce::Task` | `new`, `schedule`, `try_schedule`, `in_parallel`, `on_main`, `add_argument` | Arguments and the successful result. `add_argument` overrides the mode for one argument. Scheduling can select local mode automatically. |
340
395
  | `Farce::Molecule` | `define`, `new` | Newly created field atoms. |
341
396
  | `Farce::Vector` | `new`, `push`, `store`, `update`, and other replacement operations | Element values. |
342
397
  | `Farce::WeakSet` | `new`, `add`, `add?` | Weakly held elements. Supports only `:raise`, `:make_shareable`, and `:dedup`. Defaults to `:raise`. |
@@ -346,9 +401,11 @@ The same choices appear in several Farce APIs. Containers normally default to `:
346
401
  | `Farce::TreeMap` | `new` | Values only. Keys follow the tree map's own rules. |
347
402
  | `Farce::LRUMap` | `new` | Values only. Individual value hits and writes update eviction order. |
348
403
  | `Farce::LFUMap` | `new` | Values only. Individual value hits and writes update eviction frequency. |
349
- | `Farce::Scheduler` | `schedule` | Task arguments, with automatic local transfer enabled by default. |
350
- | `Farce::ThreadScheduler` | `schedule`, `execute` | Accepts scheduler options but always keeps arguments local. |
351
- | `Farce::Pool` | `schedule` | Task arguments. `:local` is rejected. |
404
+ | `Farce::Scheduler` | `schedule`, `try_schedule` | Task arguments, with automatic local transfer enabled by default. |
405
+ | `Farce::Unshared::Scheduler`, `Farce::Local::Scheduler` | `schedule`, `try_schedule`, `execute` | Always local. With `auto_local: false`, only `:local` and smart modes are accepted. |
406
+ | `Farce::ThreadExecutor` | `schedule`, `try_schedule`, `execute` | Accepts scheduler options but always keeps arguments local. |
407
+ | `Farce::Pool` | `schedule`, `try_schedule` | Task arguments. `:local` is rejected. |
408
+ | `Farce::Unshared::Pool`, `Farce::Local::Pool` | `schedule`, `try_schedule`, `execute` | Always local. With `auto_local: false`, only `:local` and smart modes are accepted. |
352
409
 
353
410
  `Farce::Lazy` runs its shareable factory once and applies the mode to the result. With
354
411
  `:copy`, each Ractor receives its own cached copy. Use `Farce::Strict::Lazy` when the
@@ -394,7 +451,7 @@ retries.push({ attempt: 2 }, at: Farce::Clock.now, mode: :make_shareable)
394
451
  Farce::Ractor.shareable?(retries.pop) # => true
395
452
  ```
396
453
 
397
- Wrapping happens before waiting for queue space or an exchange partner. A failed `try_push` or a timeout can therefore leave a value moved or frozen. Use copying when you need to retain the original for a retry. Also, reading a moved payload can claim it: `TimerQueue#peek` opens the payload even though it leaves the item queued.
454
+ Wrapping happens before waiting for queue space or an exchange partner. A failed `try_push` or a timeout can therefore leave a value moved or frozen. Use copying when you need to retain the original for a retry. Also, reading a moved payload can claim it: `TimerQueue#peek` opens the payload even though it leaves the item queued. Cancelling a timer through the handle returned by `push` or `try_push` does not open or claim the payload.
398
455
 
399
456
  ### Exchange mutable messages with a partner
400
457
 
@@ -487,7 +544,7 @@ For containers that retain values, `:copy` gives each Ractor its own copy of a s
487
544
 
488
545
  ### Pass task data as arguments
489
546
 
490
- `Scheduler#schedule` and `Pool#schedule` accept `mode:` for task arguments. Pass mutable data as arguments instead of capturing it from the surrounding scope. A non-local task's block must be convertible to a shareable proc.
547
+ Schedulers and pools accept `mode:` for task arguments in `schedule` and `try_schedule`. Pass mutable data as arguments instead of capturing it from the surrounding scope. A non-local task's block must be convertible to a shareable proc.
491
548
 
492
549
  ```ruby
493
550
  pool = Farce::Pool.new(max_size: 2)
@@ -503,7 +560,7 @@ pool.close
503
560
  results.close
504
561
  ```
505
562
 
506
- A scheduler defaults to `auto_local: true`, preserving the block and its arguments when scheduling from its owning Ractor. Set `auto_local: false` to enforce the requested mode there. A pool may choose another Ractor for any task, so it rejects `:local` and does not apply automatic local transfer.
563
+ A scheduler defaults to `auto_local: true`, preserving the block and its arguments when scheduling from its owning Ractor. Set `auto_local: false` to enforce the requested mode there. `Farce::Pool` may choose another Ractor for any task, so it rejects `:local` and does not apply automatic local transfer. If you want a Pool with `local` support, use `Farce::Unshared::Pool` or `Farce::Local::Pool` instead.
507
564
 
508
565
  ## Under the hood
509
566
 
@@ -521,20 +578,23 @@ envelope.value[:ids] # => [1, 2]
521
578
  envelope.value.equal?(envelope.value) # => true
522
579
  ```
523
580
 
524
- `Envelope.new` defaults to `:copy` and accepts `:copy`, `:move`, or `:local`. These are envelope types, not the full set of manager modes. A shareable payload produces an `Envelope::Share`. The other manager modes either prepare shareable data directly or raise an error.
581
+ `Envelope.new` defaults to `:copy` and accepts `:copy`, `:marshal`, `:move`, or `:local`. These are envelope types, not the full set of manager modes. A shareable payload produces an `Envelope::Share`. The other manager modes either prepare shareable data directly or raise an error.
525
582
 
526
583
  ### When copying and moving happen
527
584
 
528
585
  A copy envelope copies its non-shareable payload into internal storage when created. Each Ractor that opens it gets another copy, cached for that Ractor. A move envelope moves the payload into storage immediately, then moves it out when the winning Ractor first opens it. Forwarding either envelope does not add a payload copy or move at every hop.
529
586
 
587
+ A Marshal envelope serializes its payload when created. Each Ractor that opens it reconstructs one value from the same frozen bytes and caches it. Forwarding the envelope does not serialize or reconstruct its payload again.
588
+
530
589
  | Envelope | Who can open it? | What repeated reads return |
531
590
  | --- | --- | --- |
532
591
  | `Envelope::Copy` | Any Ractor. | That Ractor's cached copy. |
592
+ | `Envelope::Marshal` | Any Ractor. | That Ractor's cached reconstruction. |
533
593
  | `Envelope::Move` | The first Ractor to claim it. | The owning Ractor's received object. |
534
594
  | `Envelope::Local` | Its creating Ractor. | The original object. |
535
595
  | `Envelope::Share` | Any Ractor. | The shared payload. |
536
596
 
537
- A claim belongs to a Ractor, not a thread or fiber, and cannot be revoked. `claim` returns the envelope on success or `nil` if another Ractor owns it. `claim!` and `value` raise `Farce::Envelope::AlreadyClaimed` on failure. Use `claimed?` to ask whether it has an owner and `owned?` to ask whether the current Ractor can open it. Copy and share envelopes report both as true because every Ractor can open them.
597
+ A claim belongs to a Ractor, not a thread or fiber, and cannot be revoked. `claim` returns the envelope on success or `nil` if another Ractor owns it. `claim!` and `value` raise `Farce::Envelope::AlreadyClaimed` on failure. Use `claimed?` to ask whether it has an owner and `owned?` to ask whether the current Ractor can open it. Copy, Marshal, and share envelopes report both as true because every Ractor can open them.
538
598
 
539
599
  ### Forward envelopes without opening them
540
600
 
@@ -573,7 +633,7 @@ Both queues accept the envelope in `:raise` mode because the wrapper is shareabl
573
633
 
574
634
  ### Mode managers prepare values and open their own envelopes
575
635
 
576
- `Farce::ModeManager` provides two core operations: `wrap` prepares a value for shared storage, and `unwrap` retrieves values from envelopes that this manager created. It passes already-shareable values through unchanged. For non-shareable values, `:copy`, `:move`, and `:local` create managed envelopes. The remaining modes prepare shareable data directly, create a `Farce::Mutable` or `Farce::Proxy`, or raise. Mutable and proxy wrappers remain wrapped when read.
636
+ `Farce::ModeManager` provides two core operations: `wrap` prepares a value for shared storage, and `unwrap` retrieves values from envelopes that this manager created. It passes already-shareable values through unchanged. For non-shareable values, `:copy`, `:marshal`, `:move`, and `:local` create managed envelopes. Smart modes choose their representation through rules, dedicated proxies, and their fallback behavior. The remaining modes prepare shareable data directly, create a `Farce::Mutable` or `Farce::Proxy`, or raise. Mutable and proxy wrappers remain wrapped when read.
577
637
 
578
638
  ```ruby
579
639
  manager = Farce::ModeManager.new(mode: :copy)
@@ -602,7 +662,7 @@ Keep one manager per instance when building an abstraction around shareable stor
602
662
  class WorkInbox
603
663
  include Farce::Shareable
604
664
 
605
- def initialize(mode: :copy)
665
+ def initialize(mode: :smart)
606
666
  @manager = Farce::ModeManager.new(mode: mode)
607
667
  @storage = Farce::Queue.new(mode: :raise)
608
668
  super()
data/docs/scopes.md CHANGED
@@ -238,7 +238,7 @@ Pass the needed data as task arguments when crossing an execution boundary. Use
238
238
 
239
239
  ## Classes that support scopes
240
240
 
241
- These classes accept the same five scopes. The scope determines which backing container or resource an operation uses. Each class retains its own behavior, such as queue ordering or weak references.
241
+ The classes in this table accept the same five scopes. The scope determines which backing container or resource an operation uses. Each class retains its own behavior, such as queue ordering or weak references.
242
242
 
243
243
  | Class | What each scope gets |
244
244
  | --- | --- |
@@ -266,6 +266,8 @@ These classes accept the same five scopes. The scope determines which backing co
266
266
  | `Farce::Local::Lease` | An independently initialized resource to borrow. |
267
267
  | `Farce::Local::LeaseMap` | An independently initialized set of named resources. |
268
268
  | `Farce::Local::LeasePool` | A pool with its own resources and capacity. |
269
+ | `Farce::Local::Scheduler` | A task scheduler with its own worker and lifecycle. |
270
+ | `Farce::Local::Pool` | An elastic pool of thread workers with independent scaling and lifecycle. |
269
271
 
270
272
  ### Count within each scope
271
273