@open-mercato/ui 0.6.6-develop.6089.1.a7ed560528 → 0.6.6-develop.6121.1.5797a901b3
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
|
-
|
|
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": ";
|
|
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.
|
|
3
|
+
"version": "0.6.6-develop.6121.1.5797a901b3",
|
|
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.
|
|
158
|
+
"@open-mercato/shared": "0.6.6-develop.6121.1.5797a901b3",
|
|
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.
|
|
164
|
+
"@open-mercato/shared": "0.6.6-develop.6121.1.5797a901b3",
|
|
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
|
+
})
|