Skip to content

Commit a90227e

Browse files
Mashrul HaqueMashrul Haque
authored andcommitted
docs: fix README examples to match real APIs (tab-sync signing key, options-based persistence, query focus/reconnect refetch)
- Cross-tab sync example used nonexistent fluent methods; now uses real properties and documents the shared-signing-key requirement - Options-based persistence example would throw under the new integrity-check validation; now shows EnableIntegrityCheck/SigningKey explicitly - Query docs cover RefetchOnWindowFocus/RefetchOnReconnect (now implemented), client-wide defaults, and scoped per-circuit caching - README is also the NuGet package readme (PackageReadmeFile)
1 parent 2bfae22 commit a90227e

2 files changed

Lines changed: 38 additions & 11 deletions

File tree

README.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,18 @@ builder.Services.AddStoreWithHistory(
604604

605605
```csharp
606606
builder.Services.AddQueryClient();
607+
608+
// Or with client-wide defaults (per-query options always win):
609+
builder.Services.AddQueryClient(opts =>
610+
{
611+
opts.DefaultStaleTime = TimeSpan.FromSeconds(30);
612+
opts.DefaultRetry = 1;
613+
opts.DefaultRefetchOnWindowFocus = true;
614+
});
607615
```
608616

617+
The query client is **scoped**: on Blazor Server each circuit (user) gets its own isolated cache; in WebAssembly this is effectively app-wide.
618+
609619
### Why This Matters
610620

611621
Without a query system, every component manages its own loading states, error handling, and caching. With it:
@@ -614,6 +624,7 @@ Without a query system, every component manages its own loading states, error ha
614624
|---------|----------------------|
615625
| Duplicate API calls | Automatic request deduplication |
616626
| Stale data after mutations | `InvalidateQueries(k => k.StartsWith("user-"))` |
627+
| Stale data after tab switch / reconnect | `RefetchOnWindowFocus` / `RefetchOnReconnect` (on by default) |
617628
| Loading spinners everywhere | Centralized loading/error states |
618629
| Manual retry logic | Built-in with exponential backoff |
619630
| Cache invalidation headaches | Configurable stale/cache times |
@@ -636,7 +647,9 @@ Inherit from `QueryComponent` — it injects `IQueryClient` for you (available a
636647
QueryFn = async ct => await api.GetUserAsync(123, ct), // Fetch function
637648
StaleTime = TimeSpan.FromMinutes(5), // Fresh for 5 min
638649
CacheTime = TimeSpan.FromHours(1), // Cache for 1 hour
639-
Retry = 3 // Retry 3 times
650+
Retry = 3, // Retry 3 times
651+
RefetchOnWindowFocus = true, // Refetch stale data on tab focus
652+
RefetchOnReconnect = true // ...and when the browser comes back online
640653
});
641654
}
642655
}
@@ -689,14 +702,19 @@ builder.Services.AddStore(
689702
new CartState(),
690703
(store, sp) => store
691704
.WithDefaults(sp, "Cart")
692-
.WithTabSync(sp, opts => opts
693-
.Channel("shopping-cart")
694-
.EnableMessageSigning() // HMAC-SHA256 security
695-
.MaxMessageAgeSeconds(30) // Replay attack prevention
696-
.ExcludeActions("HOVER", "FOCUS")) // Don't sync transient state
705+
.WithTabSync(sp, opts =>
706+
{
707+
opts.Channel("shopping-cart")
708+
.ExcludeActions("HOVER", "FOCUS"); // Don't sync transient state
709+
opts.EnableMessageSigning = true; // HMAC-SHA256 security
710+
opts.DeriveKeyFromOrigin = true; // Shared signing key across same-origin tabs
711+
opts.MaxMessageAgeSeconds = 30; // Replay attack prevention
712+
})
697713
);
698714
```
699715

716+
> **Note:** When `EnableMessageSigning` is true, supply a key all tabs share — `DeriveKeyFromOrigin = true` or a `SigningKey` from `MessageSigner.DeriveKeyFromSeed(...)`. Without one, each tab generates its own random key and signed messages from other tabs fail verification (the middleware logs a warning).
717+
700718
That's all the code needed. Components don't change. Sync is automatic.
701719

702720
```
@@ -709,10 +727,11 @@ Tab 2: Receives → Store syncs → UI updates
709727

710728
| Option | Purpose |
711729
|--------|---------|
712-
| `EnableMessageSigning()` | HMAC-SHA256 signatures |
713-
| `MaxMessageAgeSeconds(30)` | Reject old messages (replay attacks) |
714-
| `MaxMessageSizeBytes(1MB)` | Prevent DoS |
715-
| `RequireValidSignature(true)` | Reject unsigned messages |
730+
| `EnableMessageSigning = true` | HMAC-SHA256 signatures |
731+
| `SigningKey` / `DeriveKeyFromOrigin` | Shared key so all tabs can verify each other |
732+
| `MaxMessageAgeSeconds = 30` | Reject old messages (replay attacks) |
733+
| `MaxMessageSizeBytes` | Prevent DoS |
734+
| `RequireValidSignature = true` | Reject unsigned messages |
716735

717736
---
718737

@@ -994,6 +1013,10 @@ Use `TransformOnSave` to exclude sensitive fields from localStorage:
9941013
.WithPersistence(sp, new PersistenceOptions<UserState>
9951014
{
9961015
Key = "user-state",
1016+
// Integrity checking is on by default and requires a stable key —
1017+
// supply one (e.g. MessageSigner.DeriveKeyFromSeed("your-app-seed"))
1018+
// or disable it explicitly:
1019+
EnableIntegrityCheck = false,
9971020
TransformOnSave = state => state with
9981021
{
9991022
Password = null,
@@ -1003,6 +1026,8 @@ Use `TransformOnSave` to exclude sensitive fields from localStorage:
10031026
})
10041027
```
10051028

1029+
> **Note:** Options-based persistence enables HMAC integrity checking by default. Since a random per-session key can never verify state across reloads, the store now **throws at startup** if `EnableIntegrityCheck` is true and no `SigningKey` is provided — supply a stable key via `MessageSigner.DeriveKeyFromSeed(...)` / `DeriveKeyFromPassphrase(...)`, or set `EnableIntegrityCheck = false`.
1030+
10061031
### Security Gotchas
10071032

10081033
| Mistake | Solution |

tests/EasyAppDev.Blazor.Store.Tests/Query/ReadmeQueryExamplesTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ protected override void OnInitialized()
5050
QueryFn = async ct => await Api.GetUserAsync(123, ct),
5151
StaleTime = TimeSpan.FromMinutes(5),
5252
CacheTime = TimeSpan.FromHours(1),
53-
Retry = 3
53+
Retry = 3,
54+
RefetchOnWindowFocus = true,
55+
RefetchOnReconnect = true
5456
});
5557

5658
Mutation = UseMutation(new MutationOptions<User, UpdateUserRequest>

0 commit comments

Comments
 (0)