@open-mercato/ui 0.6.6-develop.6094.1.28b081ea16 → 0.6.6-develop.6126.1.0b9392fdae

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.
@@ -2,6 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
5
6
  import { redirectToSessionRefresh, notifyForbiddenAccess, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from "../backend/utils/api.js";
6
7
  function ensureGlobalFetchInterception() {
7
8
  if (typeof window === "undefined") return;
@@ -11,7 +12,33 @@ function ensureGlobalFetchInterception() {
11
12
  w.__omOriginalFetch = window.fetch;
12
13
  window.fetch = ((input, init) => apiFetch(input, init));
13
14
  }
15
+ const DEFAULT_QUERY_STALE_TIME_MS = 3e4;
16
+ const QUERY_RETRY_LIMIT = 2;
17
+ function readErrorStatus(error) {
18
+ const status = error?.status;
19
+ if (typeof status === "number") return status;
20
+ const responseStatus = error?.response?.status;
21
+ return typeof responseStatus === "number" ? responseStatus : null;
22
+ }
23
+ function shouldRetryQuery(failureCount, error) {
24
+ const status = readErrorStatus(error);
25
+ if (status !== null && status >= 400 && status <= 404) return false;
26
+ return failureCount < QUERY_RETRY_LIMIT;
27
+ }
28
+ function buildDefaultQueryOptions() {
29
+ const enableDefaultStaleTime = parseBooleanWithDefault(
30
+ process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED,
31
+ false
32
+ );
33
+ return {
34
+ retry: shouldRetryQuery,
35
+ ...enableDefaultStaleTime ? { staleTime: DEFAULT_QUERY_STALE_TIME_MS } : {}
36
+ };
37
+ }
14
38
  const client = new QueryClient({
39
+ defaultOptions: {
40
+ queries: buildDefaultQueryOptions()
41
+ },
15
42
  queryCache: new QueryCache({
16
43
  onError: (error) => {
17
44
  if (error instanceof UnauthorizedError) redirectToSessionRefresh();
@@ -39,6 +66,9 @@ function QueryProvider({ children, defaultForbiddenRoles }) {
39
66
  return /* @__PURE__ */ jsx(QueryClientProvider, { client, children });
40
67
  }
41
68
  export {
42
- QueryProvider
69
+ DEFAULT_QUERY_STALE_TIME_MS,
70
+ QueryProvider,
71
+ buildDefaultQueryOptions,
72
+ shouldRetryQuery
43
73
  };
44
74
  //# sourceMappingURL=QueryProvider.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/theme/QueryProvider.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { redirectToSessionRefresh, notifyForbiddenAccess, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from '../backend/utils/api'\n\n// Ensure global fetch calls also flow through apiFetch so 401 session-refresh\n// and 403 access-denied flash banners fire consistently.\nfunction ensureGlobalFetchInterception() {\n if (typeof window === 'undefined') return\n const w = window as any\n if (w.__omFetchPatched) return\n w.__omFetchPatched = true\n w.__omOriginalFetch = window.fetch\n window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => apiFetch(input, init)) as any\n}\n\nconst client = new QueryClient({\n queryCache: new QueryCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) notifyForbiddenAccess()\n // As a fallback, try to detect common cases\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) notifyForbiddenAccess()\n },\n }),\n mutationCache: new MutationCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) notifyForbiddenAccess()\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) notifyForbiddenAccess()\n },\n }),\n})\n\ntype QueryProviderProps = { children: React.ReactNode; defaultForbiddenRoles?: string[] }\n\nexport function QueryProvider({ children, defaultForbiddenRoles }: QueryProviderProps) {\n React.useEffect(() => {\n ensureGlobalFetchInterception()\n if (defaultForbiddenRoles && defaultForbiddenRoles.length) {\n setAuthRedirectConfig({ defaultForbiddenRoles })\n }\n }, [])\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>\n}\n"],
5
- "mappings": ";AA6CS;AA5CT,YAAY,WAAW;AACvB,SAAS,eAAe,YAAY,aAAa,2BAA2B;AAC5E,SAAS,0BAA0B,uBAAuB,mBAAmB,gBAAgB,UAAU,6BAA6B;AAIpI,SAAS,gCAAgC;AACvC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,IAAI;AACV,MAAI,EAAE,iBAAkB;AACxB,IAAE,mBAAmB;AACrB,IAAE,oBAAoB,OAAO;AAC7B,SAAO,SAAS,CAAC,OAA0B,SAAuB,SAAS,OAAO,IAAI;AACxF;AAEA,MAAM,SAAS,IAAI,YAAY;AAAA,EAC7B,YAAY,IAAI,WAAW;AAAA,IACzB,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,uBAAsB;AAAA,eAEtD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,uBAAsB;AAAA,IACjE;AAAA,EACF,CAAC;AAAA,EACD,eAAe,IAAI,cAAc;AAAA,IAC/B,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,uBAAsB;AAAA,eACtD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,uBAAsB;AAAA,IACjE;AAAA,EACF,CAAC;AACH,CAAC;AAIM,SAAS,cAAc,EAAE,UAAU,sBAAsB,GAAuB;AACrF,QAAM,UAAU,MAAM;AACpB,kCAA8B;AAC9B,QAAI,yBAAyB,sBAAsB,QAAQ;AACzD,4BAAsB,EAAE,sBAAsB,CAAC;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO,oBAAC,uBAAoB,QAAiB,UAAS;AACxD;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { redirectToSessionRefresh, notifyForbiddenAccess, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from '../backend/utils/api'\n\n// Ensure global fetch calls also flow through apiFetch so 401 session-refresh\n// and 403 access-denied flash banners fire consistently.\nfunction ensureGlobalFetchInterception() {\n if (typeof window === 'undefined') return\n const w = window as any\n if (w.__omFetchPatched) return\n w.__omFetchPatched = true\n w.__omOriginalFetch = window.fetch\n window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => apiFetch(input, init)) as any\n}\n\nexport const DEFAULT_QUERY_STALE_TIME_MS = 30_000\nconst QUERY_RETRY_LIMIT = 2\n\nfunction readErrorStatus(error: unknown): number | null {\n const status = (error as { status?: unknown })?.status\n if (typeof status === 'number') return status\n const responseStatus = (error as { response?: { status?: unknown } })?.response?.status\n return typeof responseStatus === 'number' ? responseStatus : null\n}\n\nexport function shouldRetryQuery(failureCount: number, error: unknown): boolean {\n const status = readErrorStatus(error)\n if (status !== null && status >= 400 && status <= 404) return false\n return failureCount < QUERY_RETRY_LIMIT\n}\n\nexport function buildDefaultQueryOptions() {\n const enableDefaultStaleTime = parseBooleanWithDefault(\n process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED,\n false,\n )\n return {\n retry: shouldRetryQuery,\n ...(enableDefaultStaleTime ? { staleTime: DEFAULT_QUERY_STALE_TIME_MS } : {}),\n }\n}\n\nconst client = new QueryClient({\n defaultOptions: {\n queries: buildDefaultQueryOptions(),\n },\n queryCache: new QueryCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) notifyForbiddenAccess()\n // As a fallback, try to detect common cases\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) notifyForbiddenAccess()\n },\n }),\n mutationCache: new MutationCache({\n onError: (error) => {\n if (error instanceof UnauthorizedError) redirectToSessionRefresh()\n else if (error instanceof ForbiddenError) notifyForbiddenAccess()\n else if ((error as any)?.status === 401) redirectToSessionRefresh()\n else if ((error as any)?.status === 403) notifyForbiddenAccess()\n },\n }),\n})\n\ntype QueryProviderProps = { children: React.ReactNode; defaultForbiddenRoles?: string[] }\n\nexport function QueryProvider({ children, defaultForbiddenRoles }: QueryProviderProps) {\n React.useEffect(() => {\n ensureGlobalFetchInterception()\n if (defaultForbiddenRoles && defaultForbiddenRoles.length) {\n setAuthRedirectConfig({ defaultForbiddenRoles })\n }\n }, [])\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>\n}\n"],
5
+ "mappings": ";AA4ES;AA3ET,YAAY,WAAW;AACvB,SAAS,eAAe,YAAY,aAAa,2BAA2B;AAC5E,SAAS,+BAA+B;AACxC,SAAS,0BAA0B,uBAAuB,mBAAmB,gBAAgB,UAAU,6BAA6B;AAIpI,SAAS,gCAAgC;AACvC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,IAAI;AACV,MAAI,EAAE,iBAAkB;AACxB,IAAE,mBAAmB;AACrB,IAAE,oBAAoB,OAAO;AAC7B,SAAO,SAAS,CAAC,OAA0B,SAAuB,SAAS,OAAO,IAAI;AACxF;AAEO,MAAM,8BAA8B;AAC3C,MAAM,oBAAoB;AAE1B,SAAS,gBAAgB,OAA+B;AACtD,QAAM,SAAU,OAAgC;AAChD,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,QAAM,iBAAkB,OAA+C,UAAU;AACjF,SAAO,OAAO,mBAAmB,WAAW,iBAAiB;AAC/D;AAEO,SAAS,iBAAiB,cAAsB,OAAyB;AAC9E,QAAM,SAAS,gBAAgB,KAAK;AACpC,MAAI,WAAW,QAAQ,UAAU,OAAO,UAAU,IAAK,QAAO;AAC9D,SAAO,eAAe;AACxB;AAEO,SAAS,2BAA2B;AACzC,QAAM,yBAAyB;AAAA,IAC7B,QAAQ,IAAI;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,GAAI,yBAAyB,EAAE,WAAW,4BAA4B,IAAI,CAAC;AAAA,EAC7E;AACF;AAEA,MAAM,SAAS,IAAI,YAAY;AAAA,EAC7B,gBAAgB;AAAA,IACd,SAAS,yBAAyB;AAAA,EACpC;AAAA,EACA,YAAY,IAAI,WAAW;AAAA,IACzB,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,uBAAsB;AAAA,eAEtD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,uBAAsB;AAAA,IACjE;AAAA,EACF,CAAC;AAAA,EACD,eAAe,IAAI,cAAc;AAAA,IAC/B,SAAS,CAAC,UAAU;AAClB,UAAI,iBAAiB,kBAAmB,0BAAyB;AAAA,eACxD,iBAAiB,eAAgB,uBAAsB;AAAA,eACtD,OAAe,WAAW,IAAK,0BAAyB;AAAA,eACxD,OAAe,WAAW,IAAK,uBAAsB;AAAA,IACjE;AAAA,EACF,CAAC;AACH,CAAC;AAIM,SAAS,cAAc,EAAE,UAAU,sBAAsB,GAAuB;AACrF,QAAM,UAAU,MAAM;AACpB,kCAA8B;AAC9B,QAAI,yBAAyB,sBAAsB,QAAQ;AACzD,4BAAsB,EAAE,sBAAsB,CAAC;AAAA,IACjD;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO,oBAAC,uBAAoB,QAAiB,UAAS;AACxD;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.6-develop.6094.1.28b081ea16",
3
+ "version": "0.6.6-develop.6126.1.0b9392fdae",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,13 +155,13 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.6-develop.6094.1.28b081ea16",
158
+ "@open-mercato/shared": "0.6.6-develop.6126.1.0b9392fdae",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
- "@open-mercato/shared": "0.6.6-develop.6094.1.28b081ea16",
164
+ "@open-mercato/shared": "0.6.6-develop.6126.1.0b9392fdae",
165
165
  "@testing-library/dom": "^10.4.1",
166
166
  "@testing-library/jest-dom": "^6.9.1",
167
167
  "@testing-library/react": "^16.3.1",
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
4
5
  import { redirectToSessionRefresh, notifyForbiddenAccess, UnauthorizedError, ForbiddenError, apiFetch, setAuthRedirectConfig } from '../backend/utils/api'
5
6
 
6
7
  // Ensure global fetch calls also flow through apiFetch so 401 session-refresh
@@ -14,7 +15,37 @@ function ensureGlobalFetchInterception() {
14
15
  window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => apiFetch(input, init)) as any
15
16
  }
16
17
 
18
+ export const DEFAULT_QUERY_STALE_TIME_MS = 30_000
19
+ const QUERY_RETRY_LIMIT = 2
20
+
21
+ function readErrorStatus(error: unknown): number | null {
22
+ const status = (error as { status?: unknown })?.status
23
+ if (typeof status === 'number') return status
24
+ const responseStatus = (error as { response?: { status?: unknown } })?.response?.status
25
+ return typeof responseStatus === 'number' ? responseStatus : null
26
+ }
27
+
28
+ export function shouldRetryQuery(failureCount: number, error: unknown): boolean {
29
+ const status = readErrorStatus(error)
30
+ if (status !== null && status >= 400 && status <= 404) return false
31
+ return failureCount < QUERY_RETRY_LIMIT
32
+ }
33
+
34
+ export function buildDefaultQueryOptions() {
35
+ const enableDefaultStaleTime = parseBooleanWithDefault(
36
+ process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED,
37
+ false,
38
+ )
39
+ return {
40
+ retry: shouldRetryQuery,
41
+ ...(enableDefaultStaleTime ? { staleTime: DEFAULT_QUERY_STALE_TIME_MS } : {}),
42
+ }
43
+ }
44
+
17
45
  const client = new QueryClient({
46
+ defaultOptions: {
47
+ queries: buildDefaultQueryOptions(),
48
+ },
18
49
  queryCache: new QueryCache({
19
50
  onError: (error) => {
20
51
  if (error instanceof UnauthorizedError) redirectToSessionRefresh()
@@ -0,0 +1,37 @@
1
+ import {
2
+ DEFAULT_QUERY_STALE_TIME_MS,
3
+ buildDefaultQueryOptions,
4
+ shouldRetryQuery,
5
+ } from '../QueryProvider'
6
+ import { UnauthorizedError } from '../../backend/utils/api'
7
+
8
+ describe('QueryProvider defaults', () => {
9
+ const originalStaleTimeFlag = process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED
10
+
11
+ afterEach(() => {
12
+ process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED = originalStaleTimeFlag
13
+ })
14
+
15
+ it('does not retry deterministic 4xx query failures', () => {
16
+ expect(shouldRetryQuery(0, new UnauthorizedError())).toBe(false)
17
+ expect(shouldRetryQuery(0, { status: 400 })).toBe(false)
18
+ expect(shouldRetryQuery(0, { status: 404 })).toBe(false)
19
+ })
20
+
21
+ it('keeps transient query failures retryable with a two-retry cap', () => {
22
+ expect(shouldRetryQuery(0, { status: 408 })).toBe(true)
23
+ expect(shouldRetryQuery(1, { status: 429 })).toBe(true)
24
+ expect(shouldRetryQuery(1, { status: 503 })).toBe(true)
25
+ expect(shouldRetryQuery(1, new Error('network'))).toBe(true)
26
+
27
+ expect(shouldRetryQuery(2, { status: 503 })).toBe(false)
28
+ })
29
+
30
+ it('gates the shared staleTime default behind the public env flag', () => {
31
+ process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED = '0'
32
+ expect(buildDefaultQueryOptions().staleTime).toBeUndefined()
33
+
34
+ process.env.NEXT_PUBLIC_OM_QUERY_DEFAULT_STALE_TIME_ENABLED = '1'
35
+ expect(buildDefaultQueryOptions().staleTime).toBe(DEFAULT_QUERY_STALE_TIME_MS)
36
+ })
37
+ })