Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/StrawberryShake/Client/src/Core/IOperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,27 @@ Task<IOperationResult<TResultData>> ExecuteAsync(
IObservable<IOperationResult<TResultData>> Watch(
OperationRequest request,
ExecutionStrategy? strategy = null);

/// <summary>
/// Registers a request and subscribes to updates on the request results, optionally
/// seeding the store from a previously persisted transport payload so that the operation
/// is not re-executed. This is used to rehydrate prerendered Blazor components.
/// </summary>
/// <param name="request">
/// The operation request.
/// </param>
/// <param name="persistedState">
/// The UTF-8 encoded JSON of the GraphQL response "data" object captured during a server
/// prerender, or <c>null</c> to execute the operation normally.
/// </param>
/// <param name="strategy">
/// The request execution strategy.
/// </param>
/// <returns>
/// The observable that can be used to subscribe to results.
/// </returns>
IObservable<IOperationResult<TResultData>> Watch(
OperationRequest request,
ReadOnlyMemory<byte>? persistedState,
ExecutionStrategy? strategy = null);
}
15 changes: 15 additions & 0 deletions src/StrawberryShake/Client/src/Core/IOperationResultBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,19 @@ public interface IOperationResultBuilder<TResponseBody, TResultData>
/// </returns>
IOperationResult<TResultData> Build(
Response<TResponseBody> response);

/// <summary>
/// Builds a runtime operation result from a previously persisted transport "data"
/// payload, without executing the operation. This is used to rehydrate state that was
/// captured during a server prerender.
/// </summary>
/// <param name="persistedData">
/// The UTF-8 encoded JSON of the GraphQL response "data" object.
/// </param>
/// <returns>
/// Returns the runtime result.
/// </returns>
IOperationResult<TResultData> BuildFromPersistedData(ReadOnlyMemory<byte> persistedData)
=> throw new NotSupportedException(
"This operation result builder does not support rehydrating from persisted data.");
}
43 changes: 43 additions & 0 deletions src/StrawberryShake/Client/src/Core/OperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,47 @@ public IObservable<IOperationResult<TResult>> Watch(
request,
strategy ?? _strategy);
}

/// <summary>
/// Registers a request and subscribes to updates on the request results, optionally
/// seeding the store from a previously persisted transport payload so that the operation
/// is not re-executed. This is used to rehydrate prerendered Blazor components.
/// </summary>
/// <param name="request">
/// The operation request.
/// </param>
/// <param name="persistedState">
/// The UTF-8 encoded JSON of the GraphQL response "data" object captured during a server
/// prerender, or <c>null</c> to execute the operation normally.
/// </param>
/// <param name="strategy">
/// The request execution strategy.
/// </param>
/// <returns>
/// The observable that can be used to subscribe to results.
/// </returns>
public IObservable<IOperationResult<TResult>> Watch(
OperationRequest request,
ReadOnlyMemory<byte>? persistedState,
ExecutionStrategy? strategy = null)
{
ArgumentNullException.ThrowIfNull(request);

if (persistedState is { } state)
{
// Rehydrate the persisted payload through the existing deserialization path so
// the entity store and operation store are seeded, then serve it from the cache.
var result = _resultBuilder().BuildFromPersistedData(state);
_operationStore.Set(request, result);
strategy ??= ExecutionStrategy.CacheFirst;
}

return new OperationExecutorObservable(
_connection,
_operationStore,
_resultBuilder,
_resultPatcher,
request,
strategy ?? ExecutionStrategy.CacheFirst);
}
}
54 changes: 53 additions & 1 deletion src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Text.Json;
using StrawberryShake.Json;
using static StrawberryShake.ResultFields;
Expand All @@ -24,6 +25,7 @@ public IOperationResult<TResultData> Build(
IOperationResultDataInfo? dataInfo = null;
IReadOnlyList<IClientError>? errors = null;
IReadOnlyDictionary<string, object?>? extensions = null;
JsonElement? persistedData = null;

try
{
Expand All @@ -34,6 +36,11 @@ public IOperationResult<TResultData> Build(
{
dataInfo = BuildData(dataProp);
data = ResultDataFactory.Create(dataInfo);

if (CapturePersistedData)
{
persistedData = dataProp.Clone();
}
}

if (body.RootElement.TryGetProperty(Errors, out var errorsProp)
Expand Down Expand Up @@ -90,13 +97,58 @@ public IOperationResult<TResultData> Build(
};
}

var contextData = response.ContextData;

if (persistedData is { } captured)
{
var augmented = contextData is null
? new Dictionary<string, object?>()
: new Dictionary<string, object?>(contextData);
augmented[WellKnownContextData.PersistedData] = captured;
contextData = augmented;
}

return new OperationResult<TResultData>(
data,
dataInfo,
ResultDataFactory,
errors,
extensions,
response.ContextData);
contextData);
}

/// <summary>
/// When overridden to return <c>true</c>, the raw transport "data" payload is captured
/// into <see cref="IOperationResult.ContextData"/> under
/// <see cref="WellKnownContextData.PersistedData"/> so it can be persisted and later
/// rehydrated via <see cref="BuildFromPersistedData"/>.
/// </summary>
protected virtual bool CapturePersistedData => false;

/// <summary>
/// Builds a runtime operation result from a previously persisted transport "data"
/// payload, without executing the operation.
/// </summary>
/// <param name="persistedData">
/// The UTF-8 encoded JSON of the GraphQL response "data" object.
/// </param>
/// <returns>
/// Returns the runtime result.
/// </returns>
public IOperationResult<TResultData> BuildFromPersistedData(ReadOnlyMemory<byte> persistedData)
{
var bufferWriter = new ArrayBufferWriter<byte>();

using (var writer = new Utf8JsonWriter(bufferWriter))
{
writer.WriteStartObject();
writer.WritePropertyName(Data);
writer.WriteRawValue(persistedData.Span, skipInputValidation: true);
writer.WriteEndObject();
}

using var document = JsonDocument.Parse(bufferWriter.WrittenMemory);
return Build(new Response<JsonDocument>(document, null));
}

protected abstract IOperationResultDataInfo BuildData(JsonElement obj);
Expand Down
12 changes: 12 additions & 0 deletions src/StrawberryShake/Client/src/Core/StorelessOperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,16 @@ public IObservable<IOperationResult<TResult>> Watch(

return new StorelessOperationExecutorObservable(_connection, _resultBuilder, request);
}

/// <summary>
/// Not supported by the storeless executor. Persisted component state requires a store
/// to rehydrate into.
/// </summary>
public IObservable<IOperationResult<TResult>> Watch(
OperationRequest request,
ReadOnlyMemory<byte>? persistedState,
ExecutionStrategy? strategy = null)
=> throw new NotSupportedException(
"Persisted component state requires a client with a store. "
+ "Disable NoStore to use persisted Razor components.");
}
15 changes: 15 additions & 0 deletions src/StrawberryShake/Client/src/Core/WellKnownContextData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace StrawberryShake;

/// <summary>
/// Well known keys for <see cref="IOperationResult.ContextData"/>.
/// </summary>
public static class WellKnownContextData
{
/// <summary>
/// The key under which the raw transport "data" payload of an operation result is
/// captured. This allows the payload to be persisted (for example via Blazor's
/// <c>PersistentComponentState</c>) and later rehydrated without re-executing the
/// operation.
/// </summary>
public const string PersistedData = "StrawberryShake.PersistedData";
}
Loading