diff --git a/CHANGELOG.md b/CHANGELOG.md index 51641aa..e8267c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +- Correct `docs/reminders.md`, which said that Solid Objects has no + `unschedule`. The gem has `unschedule` and `unschedule_all`, and the guide + now has a section on how to cancel a reminder. The agent guide shows the + cancel call. A new test fails when the reminder guide does not name a public + reminder method of an actor. + ## 1.0.0 - 2026-10-13 - Version 1.0 freezes the public API under semantic versioning. diff --git a/docs/agents.md b/docs/agents.md index 8ea6af5..5695d1a 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -169,6 +169,7 @@ Obey these rules in actor code: - Keep state in `attribute` values. State must be JSON-compatible. - Use `schedule(at:, key:)` for delayed work. A reminder is one named alarm for each actor and key. A new `schedule` with the same key moves the alarm. + `unschedule(:expire, key: buyer)` cancels it. - Use `reject(code, message)` for a business rule failure that must not retry. It takes a code and a message, for example `reject(:room_full, "The room is full")`. @@ -191,6 +192,7 @@ Avoid these mistakes: | `schedule(at: deadline)` with no operation after it | `schedule(at: deadline, key: buyer).expire(buyer:)`. A `schedule` call with no operation after it fails the turn with `SolidObjects::ScheduleWithoutOperation` | | `reject "room full"` | `reject(:room_full, "The room is full")` | | `id` inside an actor | `actor_id`. An actor has no `id` method | +| A guessed cancel method, such as `cancel_reminder` | `unschedule(:expire, key: buyer)`. `unschedule_all(:expire)` cancels every key of one operation | | `register_effect(:name) { \|context, arguments\| ... }` | `register_effect(:name) { \|arguments, context\| ... }`. The arguments come first | [Reminders](reminders.md) and the [architecture guide](architecture.md) give diff --git a/docs/reminders.md b/docs/reminders.md index 5c97fd9..6d47814 100644 --- a/docs/reminders.md +++ b/docs/reminders.md @@ -129,8 +129,35 @@ cannot strand an entry because the handler drains by time rather than by alarm. Prefer it when the queue is large and the items are interchangeable; prefer `key:` when an item needs its own alarm that can be moved on its own. -Solid Objects has no `unschedule`. A reminder stops when its handler does not -re-arm it, and destroying an actor removes its reminders. +## Cancel a reminder + +`unschedule` cancels one reminder: by operation, by operation and key, or by +the handle that the operation call on the `schedule` result returns. +`unschedule_all` cancels every key of one operation. + +```ruby +def confirm(buyer:) + self.holds = holds.except(buyer) + unschedule(:expire, key: buyer) +end + +def close_sale + unschedule_all(:expire) +end +``` + +A cancellation is staged like a schedule. It commits with the state change of +the same turn, and a turn that raises cancels nothing. A cancellation of a +reminder that does not exist is not an error. An unknown operation raises +`UnknownMessage`. A cancellation cannot recall an occurrence that the scheduler +already turned into a message. + +A reminder also stops when its handler does not re-arm it, and the destruction +of an actor removes its reminders. + +`reminder` returns the status of one reminder, or `nil`. `reminders` lists +every key of one operation. Both include the changes that the current turn +staged. Self-scheduling actors should also have a low-frequency application reconciler. It may read `SolidObjects::Instance.states_for`, `.without_pending_work`, and diff --git a/test/unit/reminder_documents_test.rb b/test/unit/reminder_documents_test.rb new file mode 100644 index 0000000..ee48f88 --- /dev/null +++ b/test/unit/reminder_documents_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "test_helper" + +class ReminderDocumentsTest < ActiveSupport::TestCase + ROOT = File.expand_path("../..", __dir__) + REMINDER_METHODS = %i[schedule unschedule unschedule_all reminder reminders].freeze + + def guide(path) + File.read(File.join(ROOT, path)) + end + + test "the reminder guide names each public reminder method of an actor" do + REMINDER_METHODS.each do |method| + assert SolidObjects::Actor.public_method_defined?(method), "SolidObjects::Actor has no public #{method}" + assert_includes guide("docs/reminders.md"), "`#{method}`", "docs/reminders.md does not name #{method}" + end + end + + test "the reminder guide does not deny a method that an actor has" do + refute_match(/has no `unschedule`/, guide("docs/reminders.md")) + end + + test "the agent guide shows how to cancel a reminder" do + assert_includes guide("docs/agents.md"), "unschedule(:expire, key: buyer)" + end +end