farce 0.1.0-arm-linux-gnu → 0.2.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -1
  3. data/README.md +72 -16
  4. data/docs/gems/dry-types.md +8 -8
  5. data/docs/modes.md +67 -10
  6. data/ext/farce/atom.c +37 -22
  7. data/ext/farce/bounded_map.c +17 -13
  8. data/ext/farce/containers.h +15 -0
  9. data/ext/farce/counter.c +13 -18
  10. data/ext/farce/exchanger.c +5 -1
  11. data/ext/farce/farce.c +1 -1
  12. data/ext/farce/flag.c +39 -3
  13. data/ext/farce/io.c +3 -3
  14. data/ext/farce/lock.c +17 -23
  15. data/ext/farce/map.c +88 -66
  16. data/ext/farce/priority_queue.c +55 -43
  17. data/ext/farce/queue.c +10 -4
  18. data/ext/farce/reactor.c +52 -25
  19. data/ext/farce/reactor.h +1 -1
  20. data/ext/farce/signal.c +5 -1
  21. data/ext/farce/transaction.c +20 -24
  22. data/ext/farce/tree_map.c +33 -18
  23. data/ext/farce/trie.c +81 -43
  24. data/ext/farce/unshareable.c +168 -19
  25. data/ext/farce/unshared_io_pool.h +21 -12
  26. data/ext/farce/unshared_signal.c +13 -2
  27. data/ext/farce/unshared_wait.h +3 -0
  28. data/ext/farce/vector.c +82 -18
  29. data/ext/farce/weak_map.c +75 -63
  30. data/lib/farce/_yard/macros.rb +18 -3
  31. data/lib/farce/abstract/flag.rb +73 -0
  32. data/lib/farce/abstract/queue.rb +4 -3
  33. data/lib/farce/abstract/scheduler.rb +3 -3
  34. data/lib/farce/abstract/set.rb +17 -5
  35. data/lib/farce/abstract/sorted_set.rb +2 -2
  36. data/lib/farce/abstract/timer_queue.rb +49 -10
  37. data/lib/farce/abstract/vector.rb +510 -28
  38. data/lib/farce/atom.rb +1 -1
  39. data/lib/farce/class_mirror.rb +20 -7
  40. data/lib/farce/config.rb +7 -1
  41. data/lib/farce/engine/jvm/flag.rb +15 -3
  42. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  43. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  44. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  45. data/lib/farce/engine/ruby.rb +7 -0
  46. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  47. data/lib/farce/engine/shared/vector.rb +8 -0
  48. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  49. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  50. data/lib/farce/envelope.rb +54 -7
  51. data/lib/farce/exchanger.rb +1 -1
  52. data/lib/farce/flag.rb +2 -0
  53. data/lib/farce/integrations/active_support.rb +1 -1
  54. data/lib/farce/integrations/bson.rb +1 -1
  55. data/lib/farce/integrations/cbor.rb +1 -1
  56. data/lib/farce/integrations/concurrent.rb +3 -3
  57. data/lib/farce/integrations/date.rb +8 -0
  58. data/lib/farce/integrations/dry_types.rb +7 -4
  59. data/lib/farce/integrations/json.rb +2 -2
  60. data/lib/farce/integrations/msgpack.rb +1 -1
  61. data/lib/farce/integrations/oj.rb +3 -4
  62. data/lib/farce/integrations/psych.rb +1 -1
  63. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  64. data/lib/farce/integrations/sorted_set.rb +1 -1
  65. data/lib/farce/integrations/weakref.rb +9 -1
  66. data/lib/farce/integrations/yajl.rb +3 -3
  67. data/lib/farce/integrations.rb +33 -23
  68. data/lib/farce/internal/flag_waiting.rb +30 -0
  69. data/lib/farce/internal/freeze.rb +8 -3
  70. data/lib/farce/internal/map_value_modes.rb +1 -1
  71. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  72. data/lib/farce/lazy.rb +4 -4
  73. data/lib/farce/lazy_ref.rb +3 -3
  74. data/lib/farce/lfu_map.rb +1 -1
  75. data/lib/farce/local/flag.rb +4 -0
  76. data/lib/farce/local/scoped.rb +1 -1
  77. data/lib/farce/lru_map.rb +1 -1
  78. data/lib/farce/map.rb +2 -2
  79. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  80. data/lib/farce/mode_manager.rb +12 -3
  81. data/lib/farce/molecule.rb +2 -2
  82. data/lib/farce/pool.rb +1 -1
  83. data/lib/farce/port.rb +4 -4
  84. data/lib/farce/priority_queue.rb +1 -1
  85. data/lib/farce/proxy/register.rb +6 -0
  86. data/lib/farce/proxy/supervisor.rb +1 -1
  87. data/lib/farce/proxy.rb +7 -2
  88. data/lib/farce/queue.rb +1 -1
  89. data/lib/farce/scheduler.rb +2 -2
  90. data/lib/farce/set.rb +1 -1
  91. data/lib/farce/sorted_set.rb +1 -1
  92. data/lib/farce/timer_queue.rb +20 -6
  93. data/lib/farce/transaction/vector.rb +43 -9
  94. data/lib/farce/transaction/wrapper.rb +14 -0
  95. data/lib/farce/tree_map.rb +1 -1
  96. data/lib/farce/unsafe/lfu_map.rb +2 -0
  97. data/lib/farce/unsafe/lru_map.rb +2 -0
  98. data/lib/farce/unsafe/tree_map.rb +2 -0
  99. data/lib/farce/unshareable.rb +8 -19
  100. data/lib/farce/unshared/atom.rb +3 -0
  101. data/lib/farce/unshared/lfu_map.rb +2 -0
  102. data/lib/farce/unshared/lru_map.rb +2 -0
  103. data/lib/farce/unshared/map.rb +3 -0
  104. data/lib/farce/unshared/set.rb +1 -0
  105. data/lib/farce/unshared/sorted_set.rb +1 -0
  106. data/lib/farce/unshared/tree_map.rb +5 -1
  107. data/lib/farce/unshared/vector.rb +3 -0
  108. data/lib/farce/unshared/weak_atom.rb +5 -1
  109. data/lib/farce/unshared/weak_key_map.rb +3 -0
  110. data/lib/farce/unshared/weak_map.rb +3 -0
  111. data/lib/farce/unshared/weak_set.rb +1 -0
  112. data/lib/farce/unshared/weak_value_map.rb +3 -0
  113. data/lib/farce/vector.rb +44 -19
  114. data/lib/farce/version.rb +1 -1
  115. data/lib/farce.rb +11 -8
  116. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5656b13b69823fb23c39da2cd293e9a708c29251d3b2ea5dacd2e152231f53a5
4
- data.tar.gz: 9d431ff8db40caca5cc43390beb6d21c14dc6e4757d8413963c3fc0a8621d106
3
+ metadata.gz: b3b75367e471eaa033e832d3dce0e50d7286b2b4f1da9873a0b5fa55f9d9ce0f
4
+ data.tar.gz: 5f2531148719ccd407e5e5451069b2df80e290607c8e4f2dc65300aa21246fb8
5
5
  SHA512:
6
- metadata.gz: ac6c5d52fa76b742fdfa00b8175f8f07576711a2010e2ead4a35558f562f66b39b12f803f8ac0b62cf8b69faded767ce0259fcb90c6e40bdeec644fcbcf6ff6c
7
- data.tar.gz: 941781e468218ee1ac53bcfa1c9f8ea70d30b00e5734ac3af79b4582a0a545ea56aee8807d1a8e7dd05159ac29874fd79e015a732e1f7874ae8d29da0bfc3138
6
+ metadata.gz: 35cbb56604adae3e1ca7105a3bd0a7c72d4d5e63126988767c4235c87a4bd4b04e3335714bb4bdcab696588c317398cfe07a5ca5bc357ddb9f4b796164283cd1
7
+ data.tar.gz: f8d44d695172b5994d1e75da5b7f4a850717d88356451a932ce9fb347ddb6a3ffcbe01e624ce2a8b70d9b8c632c2eb03ac18a32c105114c74f8b7c9a6e98ad89
data/CHANGELOG.md CHANGED
@@ -4,6 +4,85 @@ Farce follows [Semantic Versioning 2.0](http://semver.org/). Any public method o
4
4
 
5
5
  ## Development Releases
6
6
 
7
+ ### Farce 0.2.0 (2026-10-11)
8
+
9
+ #### New Features
10
+
11
+ ##### Waiting on Flags
12
+
13
+ You can now wait for a `Farce::Flag` or `Farce::Local::Flag` to change its value:
14
+
15
+ ```ruby
16
+ flag = Farce::Flag.new
17
+
18
+ Thread.new do
19
+ flag.wait_until_set
20
+ puts "Flag has been activated!"
21
+ end
22
+
23
+ flag.set
24
+ ```
25
+
26
+ ##### Timer Queue Entries
27
+
28
+ `TimerQueue#push` and `TimerQueue#try_push` now return a `TimerQueue::Entry` object on a successful push.
29
+ This allows you to cancel a scheduled value using the returned entry:
30
+
31
+ ```ruby
32
+ queue = Farce::TimerQueue.new
33
+ entry = queue.push(:work, delay: 60)
34
+ entry.delete # => true
35
+ ```
36
+
37
+ ##### Expanded Vector Functionality
38
+
39
+ Improved Array compatibility for `Vector`, `Strict::Vector`, `Unshared::Vector`, and `Local::Vector`:
40
+
41
+ * Added `to_ary`, `delete`, and `delete_if`.
42
+ * Added range and start/length indexing, slice assignment, multiple-value `push` and `append`, and `pop(count)`.
43
+ * Added `shift`, `unshift`, `prepend`, `insert`, `delete_at`, `slice!`, `replace`, and `fill`.
44
+ * Added `map!`, `collect!`, `keep_if`, `select!`, `filter!`, `reject!`, `compact!`, and `uniq!`.
45
+ * Added `reverse!`, `rotate!`, `shuffle!`, `sort!`, `sort_by!`, and `flatten!`.
46
+ * Added `flatten`, `transpose`, `product`, `combination`, `repeated_combination`, `permutation`, and `repeated_permutation`.
47
+ * Added lexicographic `<=>`. Equality and hashing continue to use object identity.
48
+
49
+ ##### Miscellaneous
50
+
51
+ * Unshareable objects may now be frozen.
52
+ * Trying to make an unshareable object shareable raises a better error message now (`Ractor::Error` with a descriptive message, instead of a `NoMethodError`).
53
+ * 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.
54
+
55
+ #### Bug Fixes
56
+
57
+ * 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`.
58
+
59
+ #### Performance Improvements
60
+
61
+ ##### Rework CRuby native extension
62
+
63
+ 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).
64
+
65
+ 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.
66
+
67
+ | Operation | Before | After | Change |
68
+ | --------------------------------------------------- | ------- | ------- | ----------- |
69
+ | `Map#each` over 1024 entries | 34.1 μs | 35.2 μs | 3% slower |
70
+ | Transaction with a map with 16 entries | 25.2 μs | 25.3 μs | 0.4% slower |
71
+ | Transaction with a map with 1024 entries | 26.7 μs | 29.2 μs | 9% slower |
72
+ | Transaction with a vector with 10,000 entries | 26.1 μs | 29.4 μs | 13% slower |
73
+ | GC while keeping a vector with 10,000 entries alive | 67.6 μs | 28.6 μs | 58% faster |
74
+
75
+ Beyond these specific cases:
76
+ * Other iteration methods will have a similar performance impact to `Map#each` (for instance, `Map#transform_values`).
77
+ * Methods not involving iterations, like reading or writing a specific map entry, do not show any measurable performance impact.
78
+
79
+ Measurements were taken on CRuby 4.0.7 (arm64-darwin23) with YJIT enabled. Measurements taken against Ruby's main branch show similar results.
80
+
81
+ #### Other changes
82
+
83
+ * Avoid circular require warnings when automatically loading third-party integrations.
84
+ * Do not install a require hook if automatic integration loading is disabled.
85
+
7
86
  ### Farce 0.1.0 (2026-10-09)
8
87
 
9
- * Initial release
88
+ Initial release.
data/README.md CHANGED
@@ -133,6 +133,7 @@ puts output
133
133
  - [Constants](#constants)
134
134
  - [Compatibility and Dependencies](#compatibility-and-dependencies)
135
135
  - [Ruby](#ruby)
136
+ - [Optional Dependencies](#optional-dependencies)
136
137
  - [Similar Projects](#similar-projects)
137
138
  - [Installation](#installation)
138
139
  - [Globally](#globally)
@@ -415,6 +416,23 @@ list.to_a # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
415
416
  list.sum # => 45
416
417
  ```
417
418
 
419
+ 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.
420
+
421
+ ```ruby
422
+ list = Farce::Vector[1, 2, 3, 4]
423
+
424
+ begin
425
+ list.map! do |value|
426
+ raise "error" if value == 2
427
+ value * 2
428
+ end
429
+ rescue => e
430
+ end
431
+
432
+ # This would return 2 if list was an array
433
+ list.first # => 1
434
+ ```
435
+
418
436
  #### Sets
419
437
 
420
438
  `Farce::Set` is to `Set` what `Farce::Map` is to `Hash`. It implements the same interface, with additional atomic operations.
@@ -536,7 +554,7 @@ end
536
554
 
537
555
  #### Queues
538
556
 
539
- `Farce::Queue` is a drop-in replacement for Ruby's `Queue` and `SizedQueue`. It provides a blocking and a non-blocking API.
557
+ `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
558
 
541
559
  ```ruby
542
560
  queue = Farce::Queue.new
@@ -569,7 +587,7 @@ queue.pop # => :b
569
587
 
570
588
  ##### Timer Queues
571
589
 
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.
590
+ 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
591
 
574
592
  ```ruby
575
593
  queue = Farce::TimerQueue.new
@@ -579,7 +597,16 @@ queue.push(:ok, in: 1.5)
579
597
  queue.pop # => :ok
580
598
  ```
581
599
 
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.
600
+ 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.
601
+
602
+ ```ruby
603
+ queue = Farce::TimerQueue.new
604
+ entry = queue.push(:work, in: 60)
605
+
606
+ entry.delete # => true
607
+ ```
608
+
609
+ You can also delete a value if you know its timestamp:
583
610
 
584
611
  ```ruby
585
612
  queue = Farce::TimerQueue.new
@@ -589,6 +616,9 @@ queue.push(:first, at: timestamp - 0.1)
589
616
  queue.push(:second, at: timestamp)
590
617
  queue.push(:third, at: timestamp + 0.1)
591
618
 
619
+ fourth = queue.push(:fourth, at: timestamp + 0.2)
620
+ queue.delete(fourth)
621
+
592
622
  queue.delete(:second, at: timestamp)
593
623
 
594
624
  result = []
@@ -601,7 +631,7 @@ result # => [:first, :third]
601
631
  Queues support setting a maximum capacity. If the capacity has been reached it will block any pushes to create back pressure.
602
632
 
603
633
  ```ruby
604
- queue = Farce::Queue.new(2)
634
+ queue = Farce::Queue.new(capacity: 2)
605
635
 
606
636
  queue.try_push(:a) # => true
607
637
  queue.try_push(:b) # => true
@@ -683,17 +713,22 @@ The [default variants](#variants-and-scopes) of most [data structures](#data-str
683
713
 
684
714
  These modes are:
685
715
 
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. |
716
+ | Mode | What happens to non-shareable data | A typical use |
717
+ | --- | --- | --- |
718
+ | `:smart` (default) | Applies smart rules or a dedicated proxy, raising for unsupported values. | Share supported values with less copying. |
719
+ | `:smart_raise` | Applies smart rules or a dedicated proxy, with a raising fallback. | Reject unsupported transfers. |
720
+ | `:smart_copy` | Applies smart rules or a dedicated proxy, then tries copying and Marshal. Default for `Farce::Port`. | Send values with a copying fallback. |
721
+ | `:smart_move` | Applies smart rules or a dedicated proxy, then tries moving. | Hand off unsupported values. |
722
+ | `:copy` | Transfers a copy and leaves the original usable. | Send a snapshot of a request. |
723
+ | `:marshal` | Serializes a snapshot and reconstructs it once per receiving Ractor. | Transfer an Unshared container.|
724
+ | `:move` | Transfers ownership and makes the original inaccessible. | Hand a completed batch to a consumer. |
725
+ | `:local` | Keeps the same object in its originating Ractor. | Pass work between local threads or fibers. |
726
+ | `:make_shareable` | Calls `Ractor.make_shareable` on the original. | Publish finished configuration. |
727
+ | `:mutable` | Copies non-shareable objects into a [`Farce::Mutable`](#mutables). | Synchronize mutations across Ractors. |
728
+ | `:shareable_copy` | Makes a shareable copy and leaves the original alone. | Publish a snapshot of an editable document. |
729
+ | `:dedup` | Deduplicates the value, then makes it shareable. May update and freeze the original. | Reuse repeated message contents. |
730
+ | `:proxy` | Creates a [`Farce::Proxy`](#proxies) that executes calls in the original Ractor. | Share access to a mutable object. |
731
+ | `:raise` | Raises `Ractor::IsolationError`. | Enforce a shareable-data boundary. |
697
732
 
698
733
  Here's an example:
699
734
 
@@ -974,7 +1009,7 @@ list = Farce::Vector.new
974
1009
  list[0] = 42
975
1010
 
976
1011
  # Atomically replace list[0] with 256 if the value is still 42
977
- list.compare_and_swap(0, 42, 256)
1012
+ list.compare_and_set(0, 42, 256)
978
1013
  ```
979
1014
 
980
1015
  #### Observability and Signaling
@@ -1433,6 +1468,27 @@ Moreover:
1433
1468
  * Dropping support for a CRuby version is only done in major releases.
1434
1469
  * 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
1470
 
1471
+ ### Optional Dependencies
1472
+
1473
+ 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.
1474
+
1475
+ Farce should be compatible with these versions:
1476
+
1477
+ | Gem | Minimum Version | Release Date |
1478
+ |-------------------|-----------------|--------------|
1479
+ | `ractor-tmvar` | 0.3.0 | 2026-10-06 |
1480
+ | `msgpack` | 1.7.0 | 2023-03-29 |
1481
+ | `concurrent-ruby` | 1.1.8 | 2021-01-20 |
1482
+ | `dry-types` | 1.0.0 | 2019-04-23 |
1483
+ | `activesupport` | 5.1.0 | 2017-04-27 |
1484
+ | `oj` | 2.8.1 | 2014-04-21 |
1485
+ | `bson` | 2.0.0 | 2013-12-02 |
1486
+ | `psych` | 1.1.0 | 2011-03-30 |
1487
+
1488
+ Any version of `cbor`, `json`, `ractor-sharing`, `sorted_set`, and `weakref` should be supported
1489
+
1490
+ 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).
1491
+
1436
1492
  ### Similar Projects
1437
1493
 
1438
1494
  * [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
  | --- | --- | --- |
@@ -394,7 +448,7 @@ retries.push({ attempt: 2 }, at: Farce::Clock.now, mode: :make_shareable)
394
448
  Farce::Ractor.shareable?(retries.pop) # => true
395
449
  ```
396
450
 
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.
451
+ 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
452
 
399
453
  ### Exchange mutable messages with a partner
400
454
 
@@ -521,20 +575,23 @@ envelope.value[:ids] # => [1, 2]
521
575
  envelope.value.equal?(envelope.value) # => true
522
576
  ```
523
577
 
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.
578
+ `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
579
 
526
580
  ### When copying and moving happen
527
581
 
528
582
  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
583
 
584
+ 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.
585
+
530
586
  | Envelope | Who can open it? | What repeated reads return |
531
587
  | --- | --- | --- |
532
588
  | `Envelope::Copy` | Any Ractor. | That Ractor's cached copy. |
589
+ | `Envelope::Marshal` | Any Ractor. | That Ractor's cached reconstruction. |
533
590
  | `Envelope::Move` | The first Ractor to claim it. | The owning Ractor's received object. |
534
591
  | `Envelope::Local` | Its creating Ractor. | The original object. |
535
592
  | `Envelope::Share` | Any Ractor. | The shared payload. |
536
593
 
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.
594
+ 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
595
 
539
596
  ### Forward envelopes without opening them
540
597
 
@@ -573,7 +630,7 @@ Both queues accept the envelope in `:raise` mode because the wrapper is shareabl
573
630
 
574
631
  ### Mode managers prepare values and open their own envelopes
575
632
 
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.
633
+ `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
634
 
578
635
  ```ruby
579
636
  manager = Farce::ModeManager.new(mode: :copy)
@@ -602,7 +659,7 @@ Keep one manager per instance when building an abstraction around shareable stor
602
659
  class WorkInbox
603
660
  include Farce::Shareable
604
661
 
605
- def initialize(mode: :copy)
662
+ def initialize(mode: :smart)
606
663
  @manager = Farce::ModeManager.new(mode: mode)
607
664
  @storage = Farce::Queue.new(mode: :raise)
608
665
  super()