In my app I have this resource (some parts omitted):
defmodule MyApp.Vote do
create :cast do
argument :poll_id, :uuid, allow_nil?: false
argument :option_id, :uuid, allow_nil?: false
change manage_relationship(:poll_id, :poll, type: :append_and_remove)
change manage_relationship(:option_id, :option, type: :append_and_remove)
belongs_to :poll, Poll do
belongs_to :option, Option do
belongs_to :voter, Voter do
identity :unique_voter_per_poll, [:voter_id, :poll_id]
define :cast, args: [:poll_id, :option_id]
If a voter casts their vote for another option in the same poll, this is an upsert so the new option id overwrites the old one.
Now, I want to publish updates on a
pubsub because I want to display (and update) the count of votes for each option in a Live View. This means that when I cast a vote, I want to receive a
vote event for the
option_id, but I also want to receive an
unvote event for the previous
What is the recommended way to do this?
I think what you’d need to do is handle that yourself with a custom notifier. You can write a notifier as a module with a
notify/1 function and then reference it in your resource:
Then you can publish a message for vote and unvote respectively. Although, thinking about it, there isn’t a good way to select old attributes when doing an upsert right now. You may need to implement a manual action and use ecto directly to effectively handle this case.
I think I’ve found a workaround: I’ve added a custom change that reads the current option.
defmodule Tabemono.Voting.Vote.Changes.StorePreviousOption do
def change(changeset, _opts, ctx) do
poll_id = Ash.Changeset.get_argument(changeset, :poll_id)
voter_id = ctx.actor.id
|> Ash.Query.lock("FOR UPDATE NOWAIT")
|> Ash.Query.filter(poll_id: poll_id, voter_id: voter_id)
if previous_vote do
Ash.Changeset.put_context(changeset, :old_option_id, previous_vote.option_id)
To be extra careful, I’ve added a row lock. This way, in the (unlikely) event that the same user tries to update the vote from two different locations, one of them should fail, ensuring a single notification is sent for that old option id.
After that, it’s just a matter of reading the changeset context in the notification that gets sent. I would’ve preferred to put this information in the notification metadata instead, but I’m not sure how to control it.
Is this solutions reasonable or does it have any problem?
Sounds like a good workaround to me