Skip to content

Commit

Permalink
Improve docs for async query methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Jcparkyn committed Jun 8, 2024
1 parent a55774d commit 06a3758
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

## Changed
- Some minor improvements to type definitions for `Query` methods (non-breaking).
- Improved docs for async `Query` methods.

## Development
- More tests.
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/invoking-queries-manually.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The `Query` class contains four different methods for manually invoking queries,
1. **`Trigger`**: This always runs the query using the passed argument, regardless of whether cached data is available. Importantly, this will **not** share cached data with other components. This is the recommended way to call endpoints with side-effects (e.g. PUT, POST, or DELETE endpoints) in most cases, and works a lot like mutations in React Query.
1. **`Invoke`**: This simply calls the original query function, completely ignoring all Phetch functionality (caching and state management).

Each of these (except for `Invoke`) has an `Async` variant, which will **return the result of the query, or re-throw the exception if the query failed**. Unless you specifically need to `await` the query, you should usually use the non-async versions listed above.

> :information_source: If you are coming from React Query, you may be used to "queries" and "mutations" being different things.
In Phetch, these have been combined, so that everything is just a query.
To get the same behavior as a mutation in React Query or RTK Query, use the `query.Trigger()` method.
71 changes: 61 additions & 10 deletions src/Phetch.Core/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,28 +167,45 @@ public interface IQuery<TArg, TResult> : IQuery
public Task<TResult> Invoke(TArg arg, CancellationToken ct = default);

/// <summary>
/// Re-runs the query using the most recent argument and returns the result asynchronously.
/// Re-runs the query using the most recent argument and returns the result asynchronously. If
/// the query fails, this will <b>re-throw the error</b> from the query function.
/// </summary>
/// <remarks>
/// If you do not need to <c>await</c> the completion of the query, use <see
/// cref="QueryExtensions.Refetch">Refetch</see> instead.
/// </remarks>
/// <returns>The value returned by the query function</returns>
/// <exception cref="InvalidOperationException">Thrown if no argument has been provided to the query</exception>
/// <exception cref="InvalidOperationException">
/// Thrown if no argument has been provided to the query
/// </exception>
/// <exception cref="Exception">Thrown if the query function throws an exception</exception>
public Task<TResult> RefetchAsync();

/// <summary>
/// Updates the argument for this query, and re-run the query if the argument has changed.
/// Updates the argument for this query, and re-run the query if the argument has changed. If
/// the query fails, this will <b>re-throw the error</b> from the query function.
/// </summary>
/// <remarks>
/// If you do not need to <c>await</c> the completion of the query, use <see cref="QueryExtensions.SetArg"/> instead.
/// If you do not need to <c>await</c> the completion of the query, use <see
/// cref="QueryExtensions.SetArg"/> instead.
/// </remarks>
/// <returns>
/// A <see cref="Task"/> which completes when the query returns, or immediately if there is a
/// non-stale cached value for this argument.
/// </returns>
/// <exception cref="Exception">Thrown if the query function throws an exception</exception>
public Task<TResult> SetArgAsync(TArg arg);

/// <summary>
/// Run the query function without sharing state or cache with other queries.
/// Run the query function without sharing state or cache with other queries. If the query
/// fails, this will <b>re-throw the error</b> from the query function.
/// </summary>
/// <remarks>
/// If you do not need to <c>await</c> the completion of the query, use <see
/// cref="QueryExtensions.Trigger{TArg, TResult}(IQuery{TArg, TResult}, TArg,
/// Action{QuerySuccessEventArgs{TArg, TResult}}?,
/// Action{QueryFailureEventArgs{TArg}}?)">Trigger</see> instead.
/// <para/>
/// This is typically used for queries that have side effects (e.g., POST requests). This has
/// the following differences from <see cref="SetArgAsync(TArg)"/>:
/// <list type="bullet">
Expand All @@ -203,6 +220,7 @@ public interface IQuery<TArg, TResult> : IQuery
/// </remarks>
/// <param name="arg">The argument to pass to the query function</param>
/// <returns>The value returned by the query function</returns>
/// <exception cref="Exception">Thrown if the query function throws an exception</exception>
public Task<TResult> TriggerAsync(TArg arg);
}

Expand Down Expand Up @@ -509,7 +527,7 @@ public static void Refetch<TArg, TResult>(this IQuery<TArg, TResult> self)
_ = self.RefetchAsync();
}

/// <inheritdoc cref="Query{TArg, TResult}.TriggerAsync(TArg)"/>
/// <inheritdoc cref="IQuery{TArg, TResult}.TriggerAsync(TArg)"/>
/// <param name="self">The query to trigger.</param>
/// <param name="arg">The argument to pass to the query.</param>
/// <param name="onFailure">
Expand Down Expand Up @@ -538,10 +556,31 @@ public static async Task<TResult> TriggerAsync<TArg, TResult>(
}
}

/// <summary>
/// Run the query function without sharing state or cache with other queries.
/// </summary>
/// <remarks>
/// <inheritdoc cref="Query{TArg, TResult}.TriggerAsync(TArg)"/>
/// To also return the result of the query, use <see cref="Query{TArg, TResult}.TriggerAsync(TArg)"/>.
/// <para/>
/// This is typically used for queries that have side effects (e.g., POST requests). This has
/// the following differences from <see cref="SetArg{TArg, TResult}(IQuery{TArg, TResult}, TArg)">SetArg</see>:
/// <list type="bullet">
/// <item>
/// This will always run the query function, even if it was previously run with the same query argument.
/// </item>
/// <item>
/// The state of this query (including the cached return value) will not be shared with other
/// queries that use the same query argument.
/// </item>
/// </list>
/// </remarks>
/// <param name="self">The query to trigger.</param>
/// <param name="arg">The argument to pass to the query.</param>
/// <param name="onFailure">
/// An optional callback which will be fired if the query fails. This is not fired if the query
/// is cancelled.
/// </param>
/// <param name="onSuccess">An optional callback which will be fired if the query succeeds.</param>
[ExcludeFromCodeCoverage]
public static void Trigger<TArg, TResult>(
this IQuery<TArg, TResult> self,
Expand All @@ -557,7 +596,7 @@ public static void Trigger<TArg, TResult>(
/// Causes this query to fetch if it has not already.
/// </summary>
/// <remarks>
/// This is equivalent to <see cref="Query{TArg, TResult}.SetArgAsync(TArg)"/>, but for parameterless queries.
/// This is equivalent to <see cref="SetArg{TArg, TResult}(IQuery{TArg, TResult}, TArg)"/>, but for parameterless queries.
/// </remarks>
[ExcludeFromCodeCoverage]
public static void Fetch<TResult>(this IQuery<Unit, TResult> self)
Expand All @@ -566,15 +605,27 @@ public static void Fetch<TResult>(this IQuery<Unit, TResult> self)
_ = self.SetArgAsync(default);
}

/// <inheritdoc cref="Fetch"/>
/// <summary>
/// Causes this query to fetch if it has not already. If the query fails, this will <b>re-throw
/// the error</b> from the query function.
/// </summary>
/// <remarks>
/// If you do not need to <c>await</c> the completion of the query, use <see
/// cref="Fetch"/> instead.
/// <para/>
/// This is equivalent to <see cref="Query{TArg,TResult}.SetArgAsync(TArg)"/>, but for
/// parameterless queries.
/// </remarks>
/// <returns>The value returned by the query function</returns>
/// <exception cref="Exception">Thrown if the query function throws an exception</exception>
[ExcludeFromCodeCoverage]
public static Task<TResult> FetchAsync<TResult>(this IQuery<Unit, TResult> self)
{
_ = self ?? throw new ArgumentNullException(nameof(self));
return self.SetArgAsync(default);
}

/// <inheritdoc cref="Query{TArg, TResult}.TriggerAsync(TArg)"/>
/// <inheritdoc cref="Trigger{TArg, TResult}(IQuery{TArg, TResult}, TArg, Action{QuerySuccessEventArgs{TArg, TResult}}?, Action{QueryFailureEventArgs{TArg}}?)"/>
[ExcludeFromCodeCoverage]
public static void Trigger<TResult>(
this IQuery<Unit, TResult> self,
Expand Down
10 changes: 10 additions & 0 deletions test/Phetch.Tests/Query/QueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ public async Task SetArg_should_reset_state_after_cancel_with_uncancelable_query
AssertIsIdleState(query);
}

[UIFact]
public void SetArg_should_not_throw_when_query_fails_synchronously()
{
var query = new Endpoint<int, string>(
val => throw new IndexOutOfRangeException("BOOM!")
).Use();
query.SetArg(1); // Should not throw
query.Error!.Message.Should().Be("BOOM!");
}

[UIFact]
public async Task Should_handle_query_error()
{
Expand Down

0 comments on commit 06a3758

Please sign in to comment.