Skip to content

fix: Improve client quality and align with Go client#28

Closed
goloroden wants to merge 7 commits into
mainfrom
fix/client-quality-improvements
Closed

fix: Improve client quality and align with Go client#28
goloroden wants to merge 7 commits into
mainfrom
fix/client-quality-improvements

Conversation

@goloroden

Copy link
Copy Markdown
Member

Summary

  • Stream error handling: Replace invalid {:error, reason} returns in Stream.resource/3 with raise(reason) for proper exception propagation
  • Type safety: Split FromLatestEventOptions into ReadFromLatestEventOptions and ObserveFromLatestEventOptions with operation-specific valid values
  • Idiomatic naming: Rename Event fields to snake_case (predecessor_hash, data_content_type, spec_version)
  • Typespec fix: Correct write_events return type from Event.t() to [Event.t()]
  • Default values: Change recursive option from enforce: true to default: false, aligning with Go client
  • Documentation: Add aborting sections for all streaming operations in README and @doc

Test plan

  • All existing tests pass (field renames are reflected in test utils)
  • FromLatestEventOptions split correctly prevents invalid combinations at type level
  • Stream error handling raises exceptions cleanly
  • README examples are consistent with code changes

🤖 Generated with Claude Code

goloroden and others added 6 commits March 24, 2026 15:05
…nvalid tuples

Stream.resource/3 only accepts {list, acc} or {:halt, acc} as return values.
Returning {:error, reason} was invalid and would crash the stream consumer
with an unclear error. Now raises the exception directly, which propagates
cleanly to the consumer and allows Stream.resource's cleanup function to
properly close the HTTP connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
write_events returns a list of Event structs, not a single Event.
The typespec now correctly reflects response([Event.t()]).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aligns with Go client behavior where Recursive defaults to false
(zero value). Users no longer need to explicitly set recursive
when creating options structs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces ReadFromLatestEventOptions (read_everything | read_nothing)
and ObserveFromLatestEventOptions (read_everything | wait_for_event)
to prevent invalid combinations at the type level. Aligns with Go client
which uses separate ReadFromLatestEvent and ObserveFromLatestEvent types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renames predecessorhash to predecessor_hash, datacontenttype to
data_content_type, and specversion to spec_version. Adds key mapping
in Event.new/1 and ManagementEvent.new/1 to translate JSON field names
to snake_case atoms. Updates all test assertions accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds "Aborting" sections to README and @doc annotations for read_events,
observe_events, run_eventql_query, read_subjects, and read_event_types.
Documents how to stop consuming lazy streams using Enum.take/2, stopping
iteration, or terminating the consuming process. Aligns with Go client
which documents context cancellation for the same operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@goloroden goloroden requested a review from a team as a code owner March 24, 2026 14:14
…_atom

Map.get evaluates the default value eagerly, causing
String.to_existing_atom("datacontenttype") to fail even when the key
exists in the mapping. Switch to Map.get_lazy which only evaluates
the fallback when the key is not found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@goloroden goloroden self-assigned this Mar 24, 2026

@gossi gossi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separating request options for read and observe from latest event is the only meaningful change in this PR.

One cosmetic one, that does extra work.

The stream handling notice is unnecessary (they better go). The error handling wrong at best.

Comment thread lib/eventsourcingdb.ex
Comment on lines +306 to +309
### Aborting Reading

Since `read_events` returns a lazy stream, you can stop consuming it at any time. Use `Enum.take/2` to limit the number of events, or simply stop iterating. If you are consuming the stream in a separate process, you can abort by terminating that process.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's implicit. And I consider it general knowledge about elixir when handling stream vs list. It's one of the first things you learn.

The comment is stating the obvious and isn't needed.

Comment thread lib/eventsourcingdb.ex

*Note that `from_latest_event` and `lower_bound` can not be provided at the same time.*

### Aborting Observing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for all the others here...

Comment thread lib/eventsourcingdb.ex

{:error, reason} ->
{:error, reason}
raise(reason)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is no good here and unexpected. There is read_event_types() and read_event_types!() - the latter with the bang is expected to throw an arror, the former is expected to return the error.

Please don't throw in here, leave that to the bang versions of that functions (that's the reason for their existence).

Comment thread lib/eventsourcingdb.ex

{:error, reason} ->
{:error, reason}
raise(reason)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in this case (and the one above). Is the proper return code to Req.parse_message() iirc.

typedstruct do
field :data, any(), enforce: true
field :datacontenttype, String.t(), enforce: true
field :data_content_type, String.t(), enforce: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took rust impl as reference which keeps the name wo/ underscore. In the end eases handling those fields.

You can underscore name them to introduce extra work.

@goloroden

Copy link
Copy Markdown
Member Author

Closing in favor of a focused PR that only contains the FromLatestEventOptions split.

@goloroden goloroden closed this Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants