toga-ai 1.0.1003 → 1.0.1004
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga2-commerce/architecture.md +6 -6
- package/knowledge/2.0/apps/toga2-commerce/features/catalog-cache-freshness.md +106 -120
- package/knowledge/2.0/apps/toga2-commerce/features/force-logout-on-deployment.md +7 -6
- package/knowledge/2.0/apps/toga25-supply/features/force-logout-on-deployment.md +4 -3
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| [Cart Order For user — Add New User duplicate guard + approving-manager rule](features/cart-order-for-user-and-approving-manager.md) | Cart "Order For" user section: the Add New User duplicate check (username OR email), who the Order For list can find, and the config-driven rule that blocks che |
|
|
10
10
|
| [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | How the cart total (subtotal/shipping/tax/total) is computed and where shipping cost flows from; open when a cart total or shipping charge looks wrong. |
|
|
11
11
|
| [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | Current state of the Cart form (field-config-driven, section-hardcoded, two dual-mode forms) plus the DECIDED-but-unbuilt config-driven refactor to the toga2.5 |
|
|
12
|
-
| [Catalog cache freshness — the persisted query cache
|
|
12
|
+
| [Catalog cache freshness — the persisted query cache, per-query staleTime overrides, and focus refetch](features/catalog-cache-freshness.md) | The persisted React Query cache (`localStorage["commerce"]`), the per-query `staleTime` overrides that served prices up to 24h old, focus refetch, how to opt a |
|
|
13
13
|
| [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | Storefront category tile order is one DB column (`AssortmentItems.sortOrder` ASC) with a NULL-sorts-first trap; open when asked to reorder items on a category p |
|
|
14
14
|
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Per-tenant/language/role JSON config that drives almost all commerce UI text, fields, and login fetches (Layer A global + Layer B FIELDS registry); open before |
|
|
15
15
|
| [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | Expedited shipping options are config-gated to show only when the cart holds a computer kit; open when touching cart shipping-method options or the gate config. |
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-13
|
|
10
10
|
owners: ["apeterson", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- src/main.tsx
|
|
@@ -39,7 +39,7 @@ Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM
|
|
|
39
39
|
|
|
40
40
|
**Multi-tenant:** one codebase serves several clients (**COMPASS**, **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from the **hostname** and drives three independent systems — API base URL, theme, per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and [client-fields](features/client-fields.md).
|
|
41
41
|
|
|
42
|
-
**Critical rules:** Tenant is derived from hostname only (`hostname.toUpperCase().split(".")[0]`) — wrong host = wrong tenant, and `localhost` falls back (fields→COMPASS, theme→DEFAULT). The single `QueryClient`
|
|
42
|
+
**Critical rules:** Tenant is derived from hostname only (`hostname.toUpperCase().split(".")[0]`) — wrong host = wrong tenant, and `localhost` falls back (fields→COMPASS, theme→DEFAULT). The single `QueryClient` (`src/lib/queryClient.ts`) has default `staleTime: 0` but persists the whole cache to **`localStorage["commerce"]`**, so a returning user paints the old value first, and a per-query `staleTime` override keeps it stale (up to 24h on `_production`; removed on `TRUE-82427`, pending merge) — see [catalog cache freshness](features/catalog-cache-freshness.md); verify any data/copy change after `localStorage.removeItem('commerce')` or logout, never on a warm browser. `AuthLayout` never remounts, so `refetchOnMount` fires once per session — route-change refreshes must be wired explicitly. The registry French language key is **`fr-CA`**, not `fr` — branch on `startsWith("fr")`. api2 returns the standard `{success,data,errors}` envelope; do not break the shape.
|
|
43
43
|
|
|
44
44
|
## Tech stack (verified versions, package.json)
|
|
45
45
|
|
|
@@ -63,7 +63,7 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
|
63
63
|
## Bootstrap & provider stack
|
|
64
64
|
|
|
65
65
|
- `src/main.tsx` — initializes **Sentry** (browser tracing + replay), mounts `<App/>` in React `StrictMode`, imports global CSS.
|
|
66
|
-
- `src/App.tsx` —
|
|
66
|
+
- `src/App.tsx` — wraps the single `QueryClient` from `src/lib/queryClient.ts` (kept there so non-React code such as the 401 handler can reach it) with a `createSyncStoragePersister` (localStorage key **`"commerce"`**, build-id `buster`, `throttleTime: 0`). Provider nesting, **outermost → innermost**:
|
|
67
67
|
|
|
68
68
|
```
|
|
69
69
|
PersistQueryClientProvider (client=queryClient, persister → localStorage "commerce")
|
|
@@ -75,7 +75,7 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
- `App` runs a mount effect managing **edit-order mode**: reads a `localStorage.synced` flag and calls `exitEditOrderModeGlobalSyncReset()` when an edit-order session was abandoned outside `/cart`; subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
|
|
78
|
-
- The cache is **persisted**: query results survive refreshes
|
|
78
|
+
- The cache is **persisted**: query results survive refreshes and paint before the refetch. Invalidate deliberately with `queryClient.invalidateQueries(...)` when freshness matters; freshness rules live in [catalog cache freshness](features/catalog-cache-freshness.md).
|
|
79
79
|
|
|
80
80
|
## Routing (`src/routes.tsx`, createBrowserRouter)
|
|
81
81
|
|
|
@@ -135,7 +135,7 @@ Most use the `persist` middleware (localStorage). Verified files:
|
|
|
135
135
|
| `useSettingsErrorStore` | Settings validation errors (suppresses data queries when set) |
|
|
136
136
|
|
|
137
137
|
### React Query (`src/queries/`, `src/hooks/useApiQuery.ts`, `useApiMutation.ts`)
|
|
138
|
-
- Global default `staleTime`
|
|
138
|
+
- Global default `staleTime: 0`. `refetchOnWindowFocus` is `false` on `_production`, React Query's default `true` on `TRUE-82427` (pending merge). A per-query `staleTime` / `refetchOnWindowFocus` beats the default — see [catalog cache freshness](features/catalog-cache-freshness.md).
|
|
139
139
|
- Cache persisted to localStorage (`"commerce"`); rehydrated on load.
|
|
140
140
|
- Page queries are typically `enabled` on `isAuthenticated && user?.uuid && !settingsModalEnabled`.
|
|
141
141
|
|
|
@@ -192,7 +192,7 @@ The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors
|
|
|
192
192
|
## Gotchas
|
|
193
193
|
|
|
194
194
|
- **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit invalidation for data that must refresh on navigation.
|
|
195
|
-
- **
|
|
195
|
+
- **Persisted cache paints old data first** → catalog, pricing and `FIELDS` copy (served through the `["clientFields", …]` query) come from `localStorage["commerce"]` until the refetch lands; per-query `staleTime` overrides stretch that (see [catalog cache freshness](features/catalog-cache-freshness.md)). The build-id `buster` is live on `_production` (`e546db3d`) and drops the persisted cache on a new build. Verify any FIELDS or catalog change after `localStorage.removeItem('commerce')` or a logout, never on a warm browser.
|
|
196
196
|
- **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS, theme → DEFAULT). Use the per-tenant dev scripts.
|
|
197
197
|
- **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase (`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as `en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"` comparison silently never matches. See the client-fields doc.
|
|
198
198
|
- **Tailwind: not every `<n>px` name is a token.** `max-h-300px` does not exist (300px is only on the height/width scales) — write `max-h-[300px]`. `w-345px`, `h-300px`, `border-1` and `shadow-subMenu` do exist. A non-existent class silently renders unstyled.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Catalog cache freshness — the persisted query cache
|
|
2
|
+
title: Catalog cache freshness — the persisted query cache, per-query staleTime overrides, and focus refetch
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: toga2-commerce
|
|
5
5
|
project: TOGa Commerce
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-13
|
|
10
10
|
owners: ["bala", "apeterson", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/src/lib/queryClient.ts
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
- toga2-commerce/src/components/NavIcons/NavIconList.tsx
|
|
19
19
|
- toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts
|
|
20
20
|
- toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts
|
|
21
|
+
- toga2-commerce/src/pages/Home/HomePage.tsx
|
|
21
22
|
- toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts
|
|
22
23
|
- toga2-commerce/src/pages/Filter/viewModel/useFilterViewModel.ts
|
|
23
24
|
- toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts
|
|
@@ -25,188 +26,173 @@ files:
|
|
|
25
26
|
- toga2-commerce/src/queries/queries.ts
|
|
26
27
|
- toga2-commerce/src/hooks/useDeploymentGuard.tsx
|
|
27
28
|
- toga2-commerce/src/stores/useBundleBuilderZu.ts
|
|
29
|
+
- toga2-commerce/src/pages/ItemsView/ItemViewPage.tsx
|
|
30
|
+
- toga2-commerce/src/pages/OrderDetails/hooks/useOrderFormatting.ts
|
|
31
|
+
- toga2-commerce/src/pages/BundleView/hooks/applyBundleFormatting.ts
|
|
32
|
+
- toga2-commerce/src/pages/GetSupport/GetSupportPage.tsx
|
|
28
33
|
related:
|
|
29
34
|
- ./inactive-item-purchase-gating.md
|
|
30
35
|
- ./force-logout-on-deployment.md
|
|
36
|
+
- ./sso-redirect-and-session-gating.md
|
|
31
37
|
- ../../../standards/frontend.md
|
|
32
38
|
- ../architecture.md
|
|
33
39
|
- ../../toga25-supply/features/persisted-query-cache.md
|
|
34
40
|
- ../../toga25-supply/features/force-logout-on-deployment.md
|
|
35
41
|
---
|
|
36
42
|
|
|
37
|
-
The persisted React Query cache (`localStorage["commerce"]`)
|
|
43
|
+
The persisted React Query cache (`localStorage["commerce"]`), the per-query `staleTime` overrides that served prices up to 24h old, focus refetch, how to opt a query out, and the stacked causes behind "stale data / cache won't clear / stuck login"; open for any freshness or forced-logout bug.
|
|
38
44
|
|
|
39
45
|
## Summary
|
|
40
46
|
|
|
41
|
-
> **Status 2026-10-
|
|
47
|
+
> **Status 2026-10-13 — branch-dependent.** `TRUE-82427` (`93f12265`, **pending merge, not on `_production`**) removes the per-query `staleTime` overrides on item detail, filter/search, cart shipping methods, Home and BundleView, and turns `refetchOnWindowFocus` back on app-wide. On `_production` the 24h overrides in [the inventory](#where-the-staleness-lives) still apply. Confirm the branch before reasoning about freshness.
|
|
42
48
|
|
|
43
|
-
The whole React Query cache is persisted to **`localStorage["commerce"]`** via `PersistQueryClientProvider`.
|
|
49
|
+
The whole React Query cache is persisted to **`localStorage["commerce"]`** via `PersistQueryClientProvider`. With a long `staleTime` it serves catalogue and price state hours out of date **across full page reloads**, in both directions (a switched-off item stays buyable; a switched-on item stays hidden).
|
|
44
50
|
|
|
45
|
-
Any query whose answer **gates an action** (can this be bought? is
|
|
51
|
+
Any query whose answer **gates an action** (can this be bought? is it available?) must be opted out of *both* `staleTime` and persistence. One is not enough.
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
Three production complaints ("users don't see updated data", "clearing cache doesn't work", "stuck on a broken login page after forced logout") trace to **four causes** (below); 1-3 fixed, 4 open.
|
|
48
54
|
|
|
49
55
|
## How it works
|
|
50
56
|
|
|
51
|
-
### The default
|
|
57
|
+
### The default (`src/lib/queryClient.ts`)
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
| | `_production` | `TRUE-82427` (pending merge) |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `staleTime` | `0` | `0` |
|
|
62
|
+
| `refetchOnWindowFocus` | `false` | React Query default `true` (key removed) |
|
|
63
|
+
| per-query overrides | yes — see inventory | only the out-of-scope ones |
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
Cache persisted via `createSyncStoragePersister` with a build-id **`buster`** and `throttleTime: 0`.
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
> **⚠ Do not "fix" freshness by setting the global `staleTime` to 0 — it already is.** A per-query `staleTime` always beats the default (including `["clientFields", …]`, which carries per-tenant `FIELDS` copy). A per-query `refetchOnWindowFocus: false` likewise blocks the app-wide `true` — `TRUE-82427` removed those too (Home ×3, BundleView ×2).
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
`useDeploymentGuard.tsx` no longer sets `refetchOnWindowFocus: true` on `TRUE-82427`; it inherits the default. See [force logout on deployment](./force-logout-on-deployment.md). TOGa 2.5 Supply has the same default — see [supply's persisted query cache](../../toga25-supply/features/persisted-query-cache.md).
|
|
60
70
|
|
|
61
|
-
###
|
|
71
|
+
### Where the staleness lives
|
|
62
72
|
|
|
63
|
-
`
|
|
73
|
+
`_production` column inventoried 2026-10-01; `TRUE-82427` column verified 2026-10-13.
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
| Query | `_production` | `TRUE-82427` |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `useItemDetailsViewModel.ts` item detail | 24h | removed → 0 |
|
|
78
|
+
| `useFilterViewModel.ts` (both) filter/search | 24h | removed → 0 |
|
|
79
|
+
| `useCartViewModel.ts` shipping methods | 24h | removed → 0 |
|
|
80
|
+
| `useHomeViewModel.ts` (3) | 30s + `gcTime` 1h + focus `false` | `gcTime` 1h only |
|
|
81
|
+
| `useBundleQueryData.ts` (2) | 30s + `gcTime` 1h + focus `false` | `gcTime` 1h only |
|
|
82
|
+
| `useEditAddressFromCartForm.ts:75` countries | 24h | 24h — out of scope (dev decision) |
|
|
83
|
+
| `queries/queries.ts:39,46,68,78` login/prefetch | 5 min | 5 min — out of scope (dev decision) |
|
|
66
84
|
|
|
67
|
-
|
|
85
|
+
**On `_production`:** a price/availability edit can take **up to 24h** to reach the item page, and a refresh does not fix it (the copy is in `localStorage["commerce"]`).
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
### What "fresh" means after `TRUE-82427`
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
const NON_PERSISTED_QUERY_KEYS: string[] = ["fetchItemDetails", "fetchItemDetailsViaBundle"];
|
|
73
|
-
|
|
74
|
-
const shouldDehydrateQuery = (query) => {
|
|
75
|
-
const queryName = query.queryKey?.[0];
|
|
76
|
-
if (typeof queryName === "string" && NON_PERSISTED_QUERY_KEYS.includes(queryName)) return false;
|
|
77
|
-
return query.state.status === "success";
|
|
78
|
-
};
|
|
79
|
-
// … persistOptions: { dehydrateOptions: { shouldDehydrateQuery } }
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
> **⚠ Branch state (verified 2026-10-08) — two different persistence filters exist; neither is on `_production`.**
|
|
83
|
-
> - `_production`: `buster` + `throttleTime: 0`, **no `dehydrateOptions`** — nothing is excluded from persistence.
|
|
84
|
-
> - `_beta` / `#sprint84`: the `NON_PERSISTED_QUERY_KEYS` filter above, in `App.tsx`.
|
|
85
|
-
> - `TRUE-81382` / `_sandbox-client`: `shouldPersistQuery` in `src/queries/queries.ts`, excluding `BUNDLE_QUERY_KEYS` (`fetchBundlesByPersona`, `bundleData`, `bundleImagesData`, `fetchItemDetailsViaBundle`).
|
|
86
|
-
>
|
|
87
|
-
> When these meet, merge into **one** filter holding both key lists. Confirm the branch before relying on this section.
|
|
88
|
-
|
|
89
|
-
**Bundle queries are uncached (TRUE-81382).** `bundleData`, `bundleImagesData`, `fetchBundlesByPersona` spread `BUNDLE_NO_CACHE_QUERY_OPTIONS` (`{ staleTime: 0, gcTime: 0 }`, `src/queries/queries.ts`) and are excluded by `shouldPersistQuery`. `useBundleBuilderZu` no longer persists the raw parent-bundle API payload. Cause: an inactive item was orderable from a stale persisted bundle. Merging with `e546db3d` (TRUE-81991): in `App.tsx` keep **both** `buster` and `dehydrateOptions`; in the bundle queries keep `BUNDLE_NO_CACHE_QUERY_OPTIONS` + `refetchOnWindowFocus: false` and drop the 30s/1h values.
|
|
90
|
-
|
|
91
|
-
**Why the exclusion is required:** `gcTime: 0` only evicts a query once it becomes **inactive**. An **active** query — exactly the state the item page is in while the user is looking at it — is still dehydrated and written to `localStorage`, and is rehydrated on the next load. So `gcTime: 0` alone leaves the stale answer on disk.
|
|
92
|
-
|
|
93
|
-
The default branch (`query.state.status === "success"`) preserves TanStack's normal behaviour for every other query, so this is an allowlist-shaped opt-out with no app-wide side effects.
|
|
94
|
-
|
|
95
|
-
**Listings were deliberately left on the 24h cache.** Only the item-detail path gates a purchase decision; making every listing uncached would trade a real performance characteristic for no correctness gain.
|
|
96
|
-
|
|
97
|
-
### The four stacked causes behind the production complaints
|
|
89
|
+
A change reaches the shopper on: navigating to the page (mount), refresh / fresh URL, the tab becoming visible again, network reconnect, or a mutation that invalidates the query. Nothing polls — it does **not** update while the shopper sits still on one page. `AuthLayout`-owned queries (header, notifications, `FIELDS`) never remount, so focus refetch is their only in-session refresh.
|
|
98
90
|
|
|
99
|
-
|
|
91
|
+
Limits to state to the business:
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
93
|
+
1. **Prices already in the cart do not update** — the cart is Zustand + `localStorage`, not React Query.
|
|
94
|
+
2. **Alt-tabbing to another application is not a focus event** — see [frontend standard §4](../../../standards/frontend.md).
|
|
95
|
+
3. **On a cold load the old value paints first** from `localStorage["commerce"]`, then swaps.
|
|
104
96
|
|
|
105
|
-
|
|
97
|
+
### Home page must accept refetched data (`TRUE-82427`)
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
|---|---|
|
|
109
|
-
| `src/contexts/AuthContext.tsx:180-183` | logout |
|
|
110
|
-
| `src/components/NavIcons/NavIconList.tsx:162-170` | view-as / persona switch (language-specific data) |
|
|
111
|
-
| `src/api/axiosInstance.ts:46` | 401 forced logout |
|
|
99
|
+
`useHomeViewModel` copied query data into `homeDataState` only while `status === "fields-ready"`; after first paint (`"ready"`) a background refetch never reached the screen until Home remounted. Fix:
|
|
112
100
|
|
|
113
|
-
|
|
101
|
+
- the effect runs in `"fields-ready"` **or** `"ready"`;
|
|
102
|
+
- it uses functional `setHomeDataState(prev => …)` spreading `prev.data`, so `bundlesData` survives when the bundles, categories and items blocks all run in one pass;
|
|
103
|
+
- it depends on `homeDataState.status`, not the whole object (avoids a render loop);
|
|
104
|
+
- the view model no longer sets the `*ToDisplay` lists — `HomePage.tsx` rebuilds them in a **`useLayoutEffect`** on `data` via `handleSearchSubmit(inputValue)`, keeping the shopper's search text. A plain `useEffect` caused a one-frame "no results" flash.
|
|
114
105
|
|
|
115
|
-
|
|
116
|
-
2. **cancels in-flight queries** — otherwise they settle after the clear and are persisted again,
|
|
117
|
-
3. clears; an **epoch counter** makes a superseded clear a no-op.
|
|
106
|
+
### Item detail Add to Cart must use the refetched price (`TRUE-82427`)
|
|
118
107
|
|
|
119
|
-
|
|
108
|
+
`ItemViewPage.tsx` copies `itemToAddToCart` (price, lease, Item) into `itemForCart` in an effect. It depended on `[selectedItem?.partNumber, currencySymbol]`, so a refetch with a new price for the same part updated the screen but **not** what Add to Cart sent. Fix: depend on `[selectedItem, currencySymbol]`; quantity is kept.
|
|
120
109
|
|
|
121
|
-
|
|
110
|
+
- No loop: `selectedItem` is view-model state, and React Query structural sharing keeps its reference when data is unchanged.
|
|
111
|
+
- Never add `itemToAddToCart` as a dependency — it is rebuilt every render.
|
|
112
|
+
- **Open:** after Add to Cart, `handleAddItemToCart` resets `itemForCart` to price `0` / uuid `""` until `selectedItem` or currency changes; a second click in that window can add the item at price 0. Fix direction: rebuild price/lease from `itemToAddToCart` inside the reset.
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
- On **any** core-parameter change it calls `logout()` + `window.location.href = "/"` with **no notice** to the user. On an SSO client `/` re-triggers the IdP.
|
|
125
|
-
- It **monkey-patches `window.history.pushState` and `replaceState`** (`:216-226`) to refetch on every navigation. react-router uses those same methods.
|
|
114
|
+
### Opting a single query out — `staleTime: 0` + `gcTime: 0` is not enough
|
|
126
115
|
|
|
127
|
-
|
|
116
|
+
Item-detail queries (`fetchItemDetails`, `fetchItemDetailsViaBundle`) also need excluding from persistence:
|
|
128
117
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
Inventory verified read-only on 2026-10-01. Every value below is a **per-query override** that wins over the global `staleTime: 0`.
|
|
132
|
-
|
|
133
|
-
| staleTime | Where |
|
|
134
|
-
|---|---|
|
|
135
|
-
| **24h** | `pages/ItemsView/viewModel/useItemDetailsViewModel.ts:76` (item detail) · `pages/Filter/viewModel/useFilterViewModel.ts:106,134` (filter/search results) · `pages/Cart/viewModel/useCartViewModel.ts:310` (shipping methods) · `pages/Cart/viewModel/useEditAddressFromCartForm.ts:75` |
|
|
136
|
-
| **5 min** | `queries/queries.ts:39,46,68,78` (login / prefetch) |
|
|
137
|
-
| **30s** (+ `gcTime: 1h`) | `pages/Home/viewModel/useHomeViewModel.ts:150,175,201` · `pages/BundleView/hooks/useBundleQueryData.ts:26,42` — the **bundle** queries among these become `0`/`0` with TRUE-81382 (above) |
|
|
138
|
-
|
|
139
|
-
**Consequence — the answer to "why doesn't a price change show up":** a price or availability edit can take **up to 24 hours** to reach a shopper on the item page, and **a browser refresh does not fix it**, because the cache is persisted to `localStorage["commerce"]` and survives reload and browser restart.
|
|
118
|
+
```ts
|
|
119
|
+
const NON_PERSISTED_QUERY_KEYS: string[] = ["fetchItemDetails", "fetchItemDetailsViaBundle"];
|
|
140
120
|
|
|
141
|
-
|
|
121
|
+
const shouldDehydrateQuery = (query) => {
|
|
122
|
+
const queryName = query.queryKey?.[0];
|
|
123
|
+
if (typeof queryName === "string" && NON_PERSISTED_QUERY_KEYS.includes(queryName)) return false;
|
|
124
|
+
return query.state.status === "success";
|
|
125
|
+
};
|
|
126
|
+
// … persistOptions: { dehydrateOptions: { shouldDehydrateQuery } }
|
|
127
|
+
```
|
|
142
128
|
|
|
143
|
-
|
|
144
|
-
2. Remove `refetchOnWindowFocus: false` from `src/lib/queryClient.ts`, so React Query's default `true` applies app-wide.
|
|
145
|
-
3. Then delete the now-redundant `refetchOnWindowFocus: true` (and its comment) at `src/hooks/useDeploymentGuard.tsx:93`.
|
|
129
|
+
**Why:** `gcTime: 0` only evicts an **inactive** query. The page the user is on is **active**, so it is still dehydrated to `localStorage` and rehydrated next load.
|
|
146
130
|
|
|
147
|
-
|
|
131
|
+
> **⚠ Branch state (2026-10-08) — two persistence filters exist; neither is on `_production`.**
|
|
132
|
+
> - `_production`: `buster` + `throttleTime: 0`, **no `dehydrateOptions`**.
|
|
133
|
+
> - `_beta` / `#sprint84`: `NON_PERSISTED_QUERY_KEYS` above, in `App.tsx`.
|
|
134
|
+
> - `TRUE-81382` / `_sandbox-client`: `shouldPersistQuery` in `src/queries/queries.ts`, excluding `BUNDLE_QUERY_KEYS` (`fetchBundlesByPersona`, `bundleData`, `bundleImagesData`, `fetchItemDetailsViaBundle`).
|
|
135
|
+
>
|
|
136
|
+
> When they meet, merge into **one** filter holding both key lists.
|
|
148
137
|
|
|
149
|
-
|
|
138
|
+
**Bundle queries uncached — `TRUE-81382` branch only.** There, `bundleData`, `bundleImagesData`, `fetchBundlesByPersona` spread `BUNDLE_NO_CACHE_QUERY_OPTIONS` (`{ staleTime: 0, gcTime: 0 }`) and are excluded by `shouldPersistQuery`; `useBundleBuilderZu` no longer persists the raw parent-bundle payload (cause: an inactive item was orderable from a stale persisted bundle). The constant does **not** exist on `_production` or `TRUE-82427`. **Merge decision open:** `TRUE-82427` removed `refetchOnWindowFocus: false` from these queries; `TRUE-81382` keeps it — keeping it opts bundles out of focus refetch. In `App.tsx` keep **both** `buster` and `dehydrateOptions`.
|
|
150
139
|
|
|
151
|
-
|
|
140
|
+
### Decision — `gcTime` is the speed knob, `staleTime` is the correctness knob
|
|
152
141
|
|
|
153
|
-
|
|
142
|
+
Decided 2026-09-03; revised 2026-10-13.
|
|
154
143
|
|
|
155
|
-
|
|
144
|
+
- React Query paints cached data instantly regardless of `staleTime`; `staleTime` only decides whether it also refetches.
|
|
145
|
+
- Commerce gates loading on `isLoading`, never `isFetching`; `isLoading` is `false` whenever cached data exists. So **a lower `staleTime` shows no extra spinner**.
|
|
146
|
+
- `gcTime` (default 5 min) decides how long unused data stays in memory — that is the real speed win. **Keep `gcTime: 1h` on Home and BundleView.**
|
|
156
147
|
|
|
157
|
-
|
|
158
|
-
2. **Alt-tabbing to another application and back is not a focus event** — see [frontend standard §4](../../../standards/frontend.md) and [force logout on deployment](./force-logout-on-deployment.md).
|
|
159
|
-
3. **On a cold load the old value paints first** from `localStorage["commerce"]` and swaps when the response lands.
|
|
148
|
+
**Revised 2026-10-13 (`TRUE-82427`):** the heavy hooks no longer carry `staleTime: 30_000` + `refetchOnWindowFocus: false`; global `0` + focus refetch applies, `gcTime: 1h` stays. Supersedes the 2026-09-22 "30s for heavy queries" shape.
|
|
160
149
|
|
|
161
|
-
|
|
150
|
+
**Still open:** a query that drives a **price or stock count** should gate its spinner on `isFetching`, not `isLoading`. Which commerce queries those are is not answered.
|
|
162
151
|
|
|
163
|
-
|
|
152
|
+
**Rejected:** a 24h `staleTime` "for speed" (buys none).
|
|
164
153
|
|
|
165
|
-
|
|
166
|
-
- Commerce gates **every** loading state on `isLoading`, never `isFetching` (`useBundleQueryData.ts:22,35`; `useHomeViewModel.ts:136,157,177`). `isLoading` is `false` whenever cached data exists. So **lowering `staleTime` shows no extra spinner** — the 24h value only suppresses the refetch.
|
|
167
|
-
- **`gcTime` is set nowhere in commerce**, so it is the 5-minute default. Navigating away from Home for 6+ minutes and back drops the query from memory and **does** show a real spinner today, despite the 24h `staleTime`. The persisted cache only helps a full reload, not in-session navigation. **Fixing `gcTime` is the actual speed win.**
|
|
154
|
+
### The four stacked causes behind the production complaints
|
|
168
155
|
|
|
169
|
-
|
|
156
|
+
Pinned 2026-09-03 on `TRUE-81610`.
|
|
170
157
|
|
|
171
|
-
|
|
172
|
-
staleTime: 30_000, // stops a refetch storm on Home → bundle → Home clicks;
|
|
173
|
-
// never survives a session or a deploy
|
|
174
|
-
gcTime: 1000 * 60 * 60, // the real perceived-speed knob
|
|
175
|
-
refetchOnWindowFocus: false,
|
|
176
|
-
```
|
|
158
|
+
**1. Long `staleTime` + persisted cache → daily stale data.** Global default fixed 2026-09-22 (TRUE-81991, `e546db3d`). Per-query overrides removed on `TRUE-82427` (pending merge).
|
|
177
159
|
|
|
178
|
-
|
|
160
|
+
**2. Cache clears not gated on restoration → silently undone.** `PersistQueryClientProvider` renders children while still restoring; a clear that runs first is overwritten by the pending restore. ✅ Fixed 2026-09-22: every clear goes through **`clearPersistedQueryCache()`** (`src/lib/queryCacheRestoration.ts`), which waits for restore (capped **1.5s**), **cancels in-flight queries**, then clears; an epoch counter makes a superseded clear a no-op. Sites: logout (`AuthContext.tsx`), view-as switch (`NavIconList.tsx`), 401 (`axiosInstance.ts`). Supply's equivalent: `awaitRestoration()` from `useIsRestoring()`.
|
|
179
161
|
|
|
180
|
-
**
|
|
162
|
+
**3. Two auth gates read two different localStorage keys → stuck login.** ✅ Fixed on `TRUE-82172` — see [SSO redirect & session gating](./sso-redirect-and-session-gating.md).
|
|
181
163
|
|
|
182
|
-
**
|
|
164
|
+
**4. Forced logout too aggressive.** `AuthContext.tsx` `["fetchCoreParameters"]` string-compares the **whole** Core parameters JSON against a login baseline and on **any** difference calls `logout()` + `window.location.href = "/"` with no notice (on SSO, `/` re-triggers the IdP). It also **monkey-patches `history.pushState`/`replaceState`** to refetch on navigation. **Open.** The login-never-clears-cache half was fixed 2026-09-22 (`resetClientState()` on login and logout).
|
|
183
165
|
|
|
184
166
|
## Gotchas
|
|
185
167
|
|
|
186
|
-
- **⚠
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
- **⚠
|
|
192
|
-
- **⚠
|
|
193
|
-
- **⚠ A
|
|
194
|
-
- **⚠
|
|
195
|
-
- **⚠
|
|
196
|
-
-
|
|
197
|
-
-
|
|
168
|
+
- **⚠ A page that copies query data into local state behind a first-load guard (status, ref, or narrow effect deps) silently ignores refetches** — now visible because focus refetch is on. Fixed on `TRUE-82427`: Home, item detail Add to Cart. **Still open (found 2026-10-13):**
|
|
169
|
+
- OrderDetails `useOrderFormatting.ts` — effect deps lack `["trackingNumbers", uuid]` and `["currentApprovalStage", uuid]`, so their refetches are ignored.
|
|
170
|
+
- BundleView `applyBundleFormatting.ts` — the image effect is blocked by the `hasRunImageFormatting` ref, so an images-only refetch is ignored.
|
|
171
|
+
- Filter — search results come from a `useMutation` `onSuccess` on search-text change, so focus refetch never refreshes them (category browsing is fine).
|
|
172
|
+
- `GetSupportPage.tsx` — calls `reset()` on every `userData` change, so a refetch with changed data can wipe a message being typed.
|
|
173
|
+
- **⚠ A per-query `refetchOnWindowFocus: false` or `staleTime` beats the global default.** Grep for them before blaming `queryClient.ts`.
|
|
174
|
+
- **⚠ Never verify a catalogue or FIELDS change on a warm browser.** Run `localStorage.removeItem('commerce')` or log out first.
|
|
175
|
+
- **⚠ A cart line's price never refetches** (Zustand, outside React Query).
|
|
176
|
+
- **⚠ `gcTime: 0` does not stop persistence.** Also exclude the key via `dehydrateOptions.shouldDehydrateQuery`.
|
|
177
|
+
- **⚠ Nothing is excluded from persistence on `_production`** (2026-10-08).
|
|
178
|
+
- **⚠ The core-parameters watcher is not a targeted invalidation** — any Core parameter change logs out every user. Use `queryClient.invalidateQueries` for anything narrower.
|
|
179
|
+
- **⚠ Never call `queryClient.clear()` directly** — use `clearPersistedQueryCache()`. Keep persister `throttleTime` at `0`.
|
|
180
|
+
- **⚠ Gate auth on `user`, never `accessToken`** — see [SSO redirect & session gating](./sso-redirect-and-session-gating.md).
|
|
181
|
+
- **`buster = __BUILD_ID__` is live on `_production`** (`e546db3d`); it cannot fix a browser-cached JS bundle — see [supply's buster gap](../../toga25-supply/features/persisted-query-cache.md#the-buster-gap).
|
|
182
|
+
- **`AuthLayout` never remounts**, so `refetchOnMount` fires once per session for queries it owns.
|
|
198
183
|
|
|
199
184
|
## Change history
|
|
200
|
-
- 2026-10-
|
|
201
|
-
- 2026-10-
|
|
202
|
-
- 2026-
|
|
203
|
-
- 2026-09-
|
|
204
|
-
- 2026-09-
|
|
185
|
+
- 2026-10-13 — `TRUE-82427` (`93f12265`, pending merge): per-query `staleTime` / `refetchOnWindowFocus: false` overrides removed, focus refetch on app-wide, guard's explicit opt-in removed, Home refetch fix, item-detail Add to Cart uses refetched price; four other refetch-ignoring pages logged as open. Countries + 5-min login queries left out of scope. 30s heavy-query shape superseded. (tcox)
|
|
186
|
+
- 2026-10-08 — Bundle queries uncached + excluded from persistence (TRUE-81382); branch map of the two persistence filters; buster live on `_production`. (tcox)
|
|
187
|
+
- 2026-10-01 — Global default already `staleTime: 0`; per-query 24h overrides inventoried; ticket scope agreed. (apeterson)
|
|
188
|
+
- 2026-09-22 — Causes 1 (default) and 2 fixed; login resets client state. TRUE-81991 (`e546db3d`). (apeterson)
|
|
189
|
+
- 2026-09-21 — Cause 3 fixed on `TRUE-82172`, moved to its own doc. (tcox)
|
|
190
|
+
- 2026-09-03 — Four causes pinned; `gcTime`-vs-`staleTime` decision. (bala, apeterson)
|
|
205
191
|
|
|
206
192
|
## Related
|
|
207
193
|
|
|
208
194
|
- [inactive-item-purchase-gating](./inactive-item-purchase-gating.md)
|
|
209
195
|
- [sso-redirect-and-session-gating](./sso-redirect-and-session-gating.md)
|
|
196
|
+
- [force-logout-on-deployment](./force-logout-on-deployment.md)
|
|
210
197
|
- [architecture](../architecture.md)
|
|
211
198
|
- [supply persisted-query-cache](../../toga25-supply/features/persisted-query-cache.md)
|
|
212
|
-
- [supply force-logout-on-deployment](../../toga25-supply/features/force-logout-on-deployment.md)
|
|
@@ -6,9 +6,8 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
10
|
-
owners: [apeterson]
|
|
11
|
-
owners: [apeterson, bala]
|
|
9
|
+
updated: 2026-10-13
|
|
10
|
+
owners: [apeterson, bala, tcox]
|
|
12
11
|
files:
|
|
13
12
|
- toga2-commerce/src/hooks/useDeploymentGuard.tsx
|
|
14
13
|
- toga2-commerce/src/lib/deploymentVersion.ts
|
|
@@ -38,7 +37,7 @@ Reached commerce `_production` only in merge `26e2fae7` (2026-09-30, TRUE-81991)
|
|
|
38
37
|
|
|
39
38
|
## How it works
|
|
40
39
|
|
|
41
|
-
1. One query on `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, `
|
|
40
|
+
1. One query on `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, focus refetch on (explicit `true` on `_production`; inherited from the app default on `TRUE-82427`), `refetchInterval` **5 minutes**.
|
|
42
41
|
2. First observation writes the baseline (`action: "establish-baseline"`) and never signs out.
|
|
43
42
|
3. A later differing value → toaster → 2.5s timer → `logout()` + reload.
|
|
44
43
|
4. `logout()` clears `AUTH_STORAGE_KEYS`, including the baseline, so the next login just re-baselines.
|
|
@@ -62,9 +61,10 @@ Exactly three things, verified 2026-10-01:
|
|
|
62
61
|
| | app-wide `refetchOnWindowFocus` | guard query |
|
|
63
62
|
|---|---|---|
|
|
64
63
|
| toga25-supply (`App.tsx`) | default `true` — every on-screen query refetches on focus | inherits |
|
|
65
|
-
| toga2-commerce (`lib/queryClient.ts`) | `false` | opts back in individually |
|
|
64
|
+
| toga2-commerce `_production` (`lib/queryClient.ts`) | `false` | opts back in individually |
|
|
65
|
+
| toga2-commerce `TRUE-82427` (pending merge, 2026-10-13) | default `true` — matches supply | inherits (explicit opt-in removed) |
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Sign-out timing is **identical** in every row. `TRUE-82427` turned focus refetch on for price freshness, not logout — see [catalog cache freshness](./catalog-cache-freshness.md). Commerce `origin/_production` and `origin/_sandbox-client` are byte-identical for `useDeploymentGuard.tsx`, `App.tsx`, and `queryClient.ts` — sandbox-vs-prod differences were timing, not build.
|
|
68
68
|
|
|
69
69
|
## Gotchas
|
|
70
70
|
|
|
@@ -79,6 +79,7 @@ Because the guard opts in, sign-out timing is **identical** in both apps. Deleti
|
|
|
79
79
|
To also fire on an in-app navigation: a `useLocation()` effect calling `refetch()` on `pathname` change, with `refetch` held in a **ref** (its identity changes every render; depending on it directly loops). Commerce can do this as-is — `<DeploymentGuard />` is inside `AuthLayout`, inside the router. **Supply cannot**: its guard sits in `App.tsx` **above** `<AppRouter />`, so `useLocation()` would throw; the guard must move inside the router first. Not implemented — deferred 2026-10-01.
|
|
80
80
|
|
|
81
81
|
## Change history
|
|
82
|
+
- 2026-10-13 — Commerce app-wide focus refetch on and guard opt-in removed on `TRUE-82427` (pending merge); query-config table updated. (tcox)
|
|
82
83
|
- 2026-10-01 — Documented the exact refetch triggers (mount / tab-visibility / 5-min poll; **not** route change), the background-pause behavior, the first-bump no-op, the ignored `key=` param, and the commerce-vs-supply query config difference. Route-change refetch deferred. (apeterson)
|
|
83
84
|
- 2026-10-01 — Trigger is now the tools Force Logout page (TRUE-82414). (bala)
|
|
84
85
|
- 2026-09-25 — Built for commerce: moved out of `AuthContext` into `<DeploymentGuard />` so a toaster can be shown; added 5-min poll + focus refetch; baseline key added to `AUTH_STORAGE_KEYS`. TRUE-81991. (apeterson)
|
|
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
10
|
-
owners: [apeterson, bala]
|
|
9
|
+
updated: 2026-10-13
|
|
10
|
+
owners: [apeterson, bala, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/hooks/useDeploymentGuard.tsx
|
|
13
13
|
- toga25-supply/src/App.tsx
|
|
@@ -32,9 +32,10 @@ On large deploys the backend bumps Core parameter `META_LAST_REFRESH_DATETIME`.
|
|
|
32
32
|
4. **Reset UX.** Non-dismissing info toast ("A new version is available — signing you out briefly…"), wait **2.5s**, then `AuthContext` `logout()`.
|
|
33
33
|
5. **Centralized logout does the wipe.** `logout()` runs `performLogout()` + `clearUser()` + `localStorage.clear()` (tokens, `zu-user`, `zu-hostname`, the persisted query cache, and the `deployment-version` baseline) + `queryClient.clear()`, then hard-redirects to `/`. Clearing the baseline is what prevents a logout loop.
|
|
34
34
|
|
|
35
|
-
**When it actually fires, the first-bump no-op, and the ignored `key=` param are identical here** — documented once in [commerce's force-logout doc](../../toga2-commerce/features/force-logout-on-deployment.md) (sections *What actually triggers a refetch* and *Gotchas*).
|
|
35
|
+
**When it actually fires, the first-bump no-op, and the ignored `key=` param are identical here** — documented once in [commerce's force-logout doc](../../toga2-commerce/features/force-logout-on-deployment.md) (sections *What actually triggers a refetch* and *Gotchas*). `App.tsx` leaves `refetchOnWindowFocus` at React Query's default `true` app-wide, so every on-screen query refetches on tab visibility. Commerce `_production` still sets `false` app-wide; commerce `TRUE-82427` (pending merge, 2026-10-13) matches supply. Sign-out timing is the same in both apps.
|
|
36
36
|
|
|
37
37
|
## Change history
|
|
38
|
+
- 2026-10-13 — Noted commerce `TRUE-82427` now matches supply on app-wide focus refetch. (tcox)
|
|
38
39
|
- 2026-10-01 — Trigger is now the tools Force Logout page (TRUE-82414). (bala)
|
|
39
40
|
- 2026-10-01 — Recorded that the guard sits above the router (blocks `useLocation()`); moved the shared trigger/gotcha detail to the commerce doc rather than duplicating it. (apeterson)
|
|
40
41
|
- 2026-09-16 — Initial implementation. (apeterson)
|
package/package.json
CHANGED