@workos-inc/widgets 1.21.0 → 1.21.1-next.1794250769562
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/CHANGELOG.md +6 -0
- package/dist/cjs/api/api-provider.cjs +41 -7
- package/dist/cjs/api/api-provider.cjs.map +1 -1
- package/dist/cjs/api/api-provider.d.cts +5 -0
- package/dist/cjs/api/api-provider.d.cts.map +1 -1
- package/dist/esm/api/api-provider.d.ts +5 -0
- package/dist/esm/api/api-provider.d.ts.map +1 -1
- package/dist/esm/api/api-provider.js +41 -7
- package/dist/esm/api/api-provider.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api-provider.tsx +71 -10
package/CHANGELOG.md
CHANGED
|
@@ -39,8 +39,7 @@ const ApiProvider = ({ authToken, baseUrl, children, widgetType }) => {
|
|
|
39
39
|
});
|
|
40
40
|
react.useEffect(() => {
|
|
41
41
|
if (typeof authToken !== "string") return;
|
|
42
|
-
|
|
43
|
-
if (cached?.authToken === authToken) return;
|
|
42
|
+
if (queryClient.getQueryData(queryKey)?.authToken === authToken) return;
|
|
44
43
|
queryClient.cancelQueries({ queryKey });
|
|
45
44
|
const claims = require_api_claims.getClaims(authToken);
|
|
46
45
|
queryClient.setQueryData(queryKey, {
|
|
@@ -48,12 +47,38 @@ const ApiProvider = ({ authToken, baseUrl, children, widgetType }) => {
|
|
|
48
47
|
permissions: claims.permissions,
|
|
49
48
|
exp: claims.exp
|
|
50
49
|
});
|
|
51
|
-
if (cached && !isSameSubject(require_api_claims.getClaims(cached.authToken), claims)) queryClient.invalidateQueries({ predicate: (query) => require_api_client_graphql_query_keys.isClientGraphqlQueryKey(query.queryKey) });
|
|
52
50
|
}, [
|
|
53
51
|
authToken,
|
|
54
52
|
queryClient,
|
|
55
53
|
queryKey
|
|
56
54
|
]);
|
|
55
|
+
const currentToken = typeof authToken === "string" ? authToken : authTokenQuery.data?.authToken;
|
|
56
|
+
const currentIdentityKey = react.useMemo(() => currentToken ? getIdentityKey(require_api_claims.getClaims(currentToken)) : void 0, [currentToken]);
|
|
57
|
+
const [tokenIdentity, setTokenIdentity] = react.useState({
|
|
58
|
+
key: currentIdentityKey,
|
|
59
|
+
generation: 0
|
|
60
|
+
});
|
|
61
|
+
if (currentIdentityKey !== tokenIdentity.key) setTokenIdentity({
|
|
62
|
+
key: currentIdentityKey,
|
|
63
|
+
generation: tokenIdentity.key === void 0 ? tokenIdentity.generation : tokenIdentity.generation + 1
|
|
64
|
+
});
|
|
65
|
+
const [settledGeneration, setSettledGeneration] = react.useState(0);
|
|
66
|
+
const identityGeneration = tokenIdentity.generation;
|
|
67
|
+
const isIdentityPending = settledGeneration !== identityGeneration;
|
|
68
|
+
react.useEffect(() => {
|
|
69
|
+
if (!isIdentityPending) return;
|
|
70
|
+
queryClient.invalidateQueries({ predicate: isClientGraphqlQuery });
|
|
71
|
+
const settleWhenRefetched = () => {
|
|
72
|
+
if (!queryClient.getQueryCache().findAll({ predicate: isClientGraphqlQuery }).some((query) => query.state.fetchStatus === "fetching" || query.isActive() && query.state.isInvalidated)) setSettledGeneration(identityGeneration);
|
|
73
|
+
};
|
|
74
|
+
const unsubscribe = queryClient.getQueryCache().subscribe(settleWhenRefetched);
|
|
75
|
+
settleWhenRefetched();
|
|
76
|
+
return unsubscribe;
|
|
77
|
+
}, [
|
|
78
|
+
identityGeneration,
|
|
79
|
+
isIdentityPending,
|
|
80
|
+
queryClient
|
|
81
|
+
]);
|
|
57
82
|
const exp = authTokenQuery.data?.exp;
|
|
58
83
|
react.useEffect(() => {
|
|
59
84
|
if (!exp) return;
|
|
@@ -89,21 +114,30 @@ const ApiProvider = ({ authToken, baseUrl, children, widgetType }) => {
|
|
|
89
114
|
baseUrl,
|
|
90
115
|
elevatedAccess,
|
|
91
116
|
setElevatedAccess,
|
|
92
|
-
widgetType
|
|
117
|
+
widgetType,
|
|
118
|
+
isIdentityPending
|
|
93
119
|
}), [
|
|
94
120
|
authTokenQuery.data,
|
|
95
121
|
baseUrl,
|
|
96
122
|
elevatedAccess,
|
|
97
123
|
setElevatedAccess,
|
|
98
|
-
widgetType
|
|
124
|
+
widgetType,
|
|
125
|
+
isIdentityPending
|
|
99
126
|
]);
|
|
100
127
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ApiContext.Provider, {
|
|
101
128
|
value,
|
|
102
129
|
children
|
|
103
130
|
});
|
|
104
131
|
};
|
|
105
|
-
function
|
|
106
|
-
return
|
|
132
|
+
function isClientGraphqlQuery(query) {
|
|
133
|
+
return require_api_client_graphql_query_keys.isClientGraphqlQueryKey(query.queryKey);
|
|
134
|
+
}
|
|
135
|
+
function getIdentityKey(claims) {
|
|
136
|
+
return JSON.stringify([
|
|
137
|
+
claims.sid,
|
|
138
|
+
claims.sub,
|
|
139
|
+
claims.org_id
|
|
140
|
+
]);
|
|
107
141
|
}
|
|
108
142
|
const useApi = () => {
|
|
109
143
|
const context = react.useContext(ApiContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-provider.cjs","names":["React","useQueryClient","useQuery","getClaims","isClientGraphqlQueryKey","useIsHydrated"],"sources":["../../../src/api/api-provider.tsx"],"sourcesContent":["import { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport * as React from \"react\";\nimport { useIsHydrated } from \"../lib/use-is-hydrated.js\";\nimport { getClaims } from \"./claims.js\";\nimport type { Claims } from \"./claims.js\";\nimport { isClientGraphqlQueryKey } from \"./client-graphql/query-keys.js\";\n\ntype WidgetType =\n | \"admin-portal-audit-log-streaming\"\n | \"admin-portal-domain-verification\"\n | \"user-management\"\n | \"user-api-keys\"\n | \"organization-switcher\"\n | \"user-sessions\"\n | \"user-security\"\n | \"user-profile\"\n | \"admin-portal-sso-connection\"\n | \"api-keys\"\n | \"pipes\"\n | \"pipes-admin\"\n | \"directory-sync\";\n\ntype AuthToken = string | (() => Promise<string>);\n\ninterface ElevatedAccess {\n token: string;\n expiresAt: string;\n}\n\ninterface ApiContextValue {\n authToken: string | undefined;\n permissions: string[] | undefined;\n exp: number | undefined;\n baseUrl: string;\n widgetType: WidgetType;\n elevatedAccess?: ElevatedAccess;\n setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;\n}\n\ninterface AuthTokenQueryData {\n authToken: string;\n permissions: string[] | undefined;\n exp: number | undefined;\n}\n\nconst authTokenQueryKey = (widgetType: WidgetType) =>\n [\"authToken\", widgetType] as const;\n\n// Refetch this many ms before the JWT expires so data queries always see a\n// fresh token.\nconst TOKEN_REFRESH_BUFFER_MS = 30_000;\n\n// Cap setTimeout delays at ~24 days to stay well below the 32-bit signed int\n// limit (~24.8 days), which some runtimes silently coerce to 1ms.\nconst MAX_TIMEOUT_MS = 2_147_483_000;\n\nconst ApiContext = React.createContext<ApiContextValue | undefined>(undefined);\nApiContext.displayName = \"ApiContext\";\n\ninterface ApiProviderProps {\n authToken: AuthToken;\n baseUrl: string;\n children: React.ReactNode;\n widgetType: WidgetType;\n}\n\nconst ApiProvider: React.FC<ApiProviderProps> = ({\n authToken,\n baseUrl,\n children,\n widgetType,\n}) => {\n const queryClient = useQueryClient();\n const queryKey = authTokenQueryKey(widgetType);\n const authTokenQuery = useQuery<AuthTokenQueryData>({\n initialData: () => {\n if (typeof authToken !== \"string\") {\n return undefined;\n }\n\n const claims = getClaims(authToken);\n return {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryFn: async () => {\n const resolvedToken =\n typeof authToken === \"string\" ? authToken : await authToken();\n const claims = getClaims(resolvedToken);\n\n return {\n authToken: resolvedToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryKey,\n });\n\n // `initialData` only seeds the cache on first mount, so a new string token\n // has to be written to the cache explicitly. A token for a different session,\n // user or organization (e.g. after switching organizations) also invalidates\n // data fetched with the previous token.\n React.useEffect(() => {\n if (typeof authToken !== \"string\") {\n return;\n }\n\n const cached = queryClient.getQueryData<AuthTokenQueryData>(queryKey);\n if (cached?.authToken === authToken) {\n return;\n }\n\n // Keep an in-flight fetch holding the previous token from overwriting the\n // new one.\n void queryClient.cancelQueries({ queryKey });\n const claims = getClaims(authToken);\n queryClient.setQueryData<AuthTokenQueryData>(queryKey, {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n });\n\n if (cached && !isSameSubject(getClaims(cached.authToken), claims)) {\n void queryClient.invalidateQueries({\n predicate: (query) => isClientGraphqlQueryKey(query.queryKey),\n });\n }\n }, [authToken, queryClient, queryKey]);\n\n // Keep the token \"fresh\" until ~30s before it expires, so window-focus\n // re-fetches re-mint before data queries use a stale Bearer.\n //\n // TODO(v2): prefer the function-form `staleTime` on `useQuery` above once we\n // can bump the `@tanstack/react-query` peer dep to ^5.44.0 (the version that\n // introduced it). That lets react-query own the freshness window and avoids\n // the extra timer + window-focus nonsense this effect introduces.\n const exp = authTokenQuery.data?.exp;\n React.useEffect(() => {\n if (!exp) {\n return;\n }\n\n const delay = Math.min(\n MAX_TIMEOUT_MS,\n exp * 1000 - Date.now() - TOKEN_REFRESH_BUFFER_MS,\n );\n if (delay <= 0) {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n return;\n }\n const timeout = window.setTimeout(() => {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n }, delay);\n return () => window.clearTimeout(timeout);\n }, [exp, queryClient, widgetType]);\n\n const [elevatedAccess, setElevatedAccess] = React.useState<ElevatedAccess>();\n const elevatedAccessTimeout = React.useRef<number | undefined>(undefined);\n\n // This effect manages the expiration of elevated access tokens\n // When an elevated access token is present, it checks every 30 seconds if the token has expired\n // If the token has expired (current time > expiration time), it clears the elevated access\n React.useEffect(() => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n\n if (elevatedAccess) {\n elevatedAccessTimeout.current = window.setInterval(() => {\n const now = new Date();\n const expiresAtDate = new Date(elevatedAccess.expiresAt);\n\n // Reset the elevated access if it has expired\n if (now > expiresAtDate) {\n setElevatedAccess(undefined);\n }\n }, 30_000); // every 30 seconds\n }\n\n return () => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n };\n }, [elevatedAccess]);\n\n const value = React.useMemo(\n () => ({\n authToken: authTokenQuery.data?.authToken,\n permissions: authTokenQuery.data?.permissions,\n exp: authTokenQuery.data?.exp,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n }),\n [\n authTokenQuery.data,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n ],\n );\n\n return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;\n};\n\nfunction isSameSubject(a: Claims, b: Claims) {\n return a.sid === b.sid && a.sub === b.sub && a.org_id === b.org_id;\n}\n\nconst useApi = () => {\n const context = React.useContext(ApiContext);\n\n if (context === undefined) {\n throw new Error(\"useApi must be used within an ApiProvider\");\n }\n\n return context;\n};\n\nconst useElevatedAccessToken = () => {\n const { elevatedAccess, setElevatedAccess } = useApi();\n\n return { elevatedAccess, setElevatedAccess };\n};\n\nconst useApiReady = () => {\n const { authToken } = useApi();\n const isHydrated = useIsHydrated();\n\n return isHydrated && authToken !== undefined;\n};\nexport type { WidgetType, AuthToken, AuthTokenQueryData };\nexport {\n authTokenQueryKey,\n ApiProvider,\n useApi,\n useElevatedAccessToken,\n useApiReady,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-provider.cjs","names":["React","useQueryClient","useQuery","getClaims","isClientGraphqlQueryKey","useIsHydrated"],"sources":["../../../src/api/api-provider.tsx"],"sourcesContent":["import { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport type { Query } from \"@tanstack/react-query\";\nimport * as React from \"react\";\nimport { useIsHydrated } from \"../lib/use-is-hydrated.js\";\nimport { getClaims } from \"./claims.js\";\nimport type { Claims } from \"./claims.js\";\nimport { isClientGraphqlQueryKey } from \"./client-graphql/query-keys.js\";\n\ntype WidgetType =\n | \"admin-portal-audit-log-streaming\"\n | \"admin-portal-domain-verification\"\n | \"user-management\"\n | \"user-api-keys\"\n | \"organization-switcher\"\n | \"user-sessions\"\n | \"user-security\"\n | \"user-profile\"\n | \"admin-portal-sso-connection\"\n | \"api-keys\"\n | \"pipes\"\n | \"pipes-admin\"\n | \"directory-sync\";\n\ntype AuthToken = string | (() => Promise<string>);\n\ninterface ElevatedAccess {\n token: string;\n expiresAt: string;\n}\n\ninterface ApiContextValue {\n authToken: string | undefined;\n permissions: string[] | undefined;\n exp: number | undefined;\n baseUrl: string;\n widgetType: WidgetType;\n elevatedAccess?: ElevatedAccess;\n setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;\n /**\n * The token now belongs to a different session, user or organization, but\n * the Client GraphQL data on screen may still belong to the previous one.\n */\n isIdentityPending: boolean;\n}\n\ninterface AuthTokenQueryData {\n authToken: string;\n permissions: string[] | undefined;\n exp: number | undefined;\n}\n\nconst authTokenQueryKey = (widgetType: WidgetType) =>\n [\"authToken\", widgetType] as const;\n\n// Refetch this many ms before the JWT expires so data queries always see a\n// fresh token.\nconst TOKEN_REFRESH_BUFFER_MS = 30_000;\n\n// Cap setTimeout delays at ~24 days to stay well below the 32-bit signed int\n// limit (~24.8 days), which some runtimes silently coerce to 1ms.\nconst MAX_TIMEOUT_MS = 2_147_483_000;\n\nconst ApiContext = React.createContext<ApiContextValue | undefined>(undefined);\nApiContext.displayName = \"ApiContext\";\n\ninterface ApiProviderProps {\n authToken: AuthToken;\n baseUrl: string;\n children: React.ReactNode;\n widgetType: WidgetType;\n}\n\nconst ApiProvider: React.FC<ApiProviderProps> = ({\n authToken,\n baseUrl,\n children,\n widgetType,\n}) => {\n const queryClient = useQueryClient();\n const queryKey = authTokenQueryKey(widgetType);\n const authTokenQuery = useQuery<AuthTokenQueryData>({\n initialData: () => {\n if (typeof authToken !== \"string\") {\n return undefined;\n }\n\n const claims = getClaims(authToken);\n return {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryFn: async () => {\n const resolvedToken =\n typeof authToken === \"string\" ? authToken : await authToken();\n const claims = getClaims(resolvedToken);\n\n return {\n authToken: resolvedToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryKey,\n });\n\n // `initialData` only seeds the cache on first mount, so a new string token\n // has to be written to the cache explicitly.\n React.useEffect(() => {\n if (typeof authToken !== \"string\") {\n return;\n }\n\n const cached = queryClient.getQueryData<AuthTokenQueryData>(queryKey);\n if (cached?.authToken === authToken) {\n return;\n }\n\n // Keep an in-flight fetch holding the previous token from overwriting the\n // new one.\n void queryClient.cancelQueries({ queryKey });\n const claims = getClaims(authToken);\n queryClient.setQueryData<AuthTokenQueryData>(queryKey, {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n });\n }, [authToken, queryClient, queryKey]);\n\n const currentToken =\n typeof authToken === \"string\" ? authToken : authTokenQuery.data?.authToken;\n const currentIdentityKey = React.useMemo(\n () => (currentToken ? getIdentityKey(getClaims(currentToken)) : undefined),\n [currentToken],\n );\n // Counts changes to the token's session, user or organization (e.g. after\n // switching organizations). Switching back to an earlier identity counts too,\n // since requests made for the one in between may still be in flight.\n const [tokenIdentity, setTokenIdentity] = React.useState({\n key: currentIdentityKey,\n generation: 0,\n });\n if (currentIdentityKey !== tokenIdentity.key) {\n setTokenIdentity({\n key: currentIdentityKey,\n generation:\n tokenIdentity.key === undefined\n ? tokenIdentity.generation\n : tokenIdentity.generation + 1,\n });\n }\n // The identity generation whose Client GraphQL data is on screen. The\n // previous identity's data stays on screen until it has been refetched with\n // the new token.\n const [settledGeneration, setSettledGeneration] = React.useState(0);\n const identityGeneration = tokenIdentity.generation;\n const isIdentityPending = settledGeneration !== identityGeneration;\n React.useEffect(() => {\n if (!isIdentityPending) {\n return;\n }\n\n // Cancels refetches still in flight for an earlier identity.\n void queryClient.invalidateQueries({ predicate: isClientGraphqlQuery });\n // A failed refetch leaves its query invalidated, so the previous identity's\n // data stays locked until a refetch succeeds.\n const settleWhenRefetched = () => {\n const isRefetching = queryClient\n .getQueryCache()\n .findAll({ predicate: isClientGraphqlQuery })\n .some(\n (query) =>\n query.state.fetchStatus === \"fetching\" ||\n (query.isActive() && query.state.isInvalidated),\n );\n if (!isRefetching) {\n setSettledGeneration(identityGeneration);\n }\n };\n const unsubscribe = queryClient\n .getQueryCache()\n .subscribe(settleWhenRefetched);\n settleWhenRefetched();\n return unsubscribe;\n }, [identityGeneration, isIdentityPending, queryClient]);\n\n // Keep the token \"fresh\" until ~30s before it expires, so window-focus\n // re-fetches re-mint before data queries use a stale Bearer.\n //\n // TODO(v2): prefer the function-form `staleTime` on `useQuery` above once we\n // can bump the `@tanstack/react-query` peer dep to ^5.44.0 (the version that\n // introduced it). That lets react-query own the freshness window and avoids\n // the extra timer + window-focus nonsense this effect introduces.\n const exp = authTokenQuery.data?.exp;\n React.useEffect(() => {\n if (!exp) {\n return;\n }\n\n const delay = Math.min(\n MAX_TIMEOUT_MS,\n exp * 1000 - Date.now() - TOKEN_REFRESH_BUFFER_MS,\n );\n if (delay <= 0) {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n return;\n }\n const timeout = window.setTimeout(() => {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n }, delay);\n return () => window.clearTimeout(timeout);\n }, [exp, queryClient, widgetType]);\n\n const [elevatedAccess, setElevatedAccess] = React.useState<ElevatedAccess>();\n const elevatedAccessTimeout = React.useRef<number | undefined>(undefined);\n\n // This effect manages the expiration of elevated access tokens\n // When an elevated access token is present, it checks every 30 seconds if the token has expired\n // If the token has expired (current time > expiration time), it clears the elevated access\n React.useEffect(() => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n\n if (elevatedAccess) {\n elevatedAccessTimeout.current = window.setInterval(() => {\n const now = new Date();\n const expiresAtDate = new Date(elevatedAccess.expiresAt);\n\n // Reset the elevated access if it has expired\n if (now > expiresAtDate) {\n setElevatedAccess(undefined);\n }\n }, 30_000); // every 30 seconds\n }\n\n return () => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n };\n }, [elevatedAccess]);\n\n const value = React.useMemo(\n () => ({\n authToken: authTokenQuery.data?.authToken,\n permissions: authTokenQuery.data?.permissions,\n exp: authTokenQuery.data?.exp,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n isIdentityPending,\n }),\n [\n authTokenQuery.data,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n isIdentityPending,\n ],\n );\n\n return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;\n};\n\nfunction isClientGraphqlQuery(query: Query) {\n return isClientGraphqlQueryKey(query.queryKey);\n}\n\nfunction getIdentityKey(claims: Claims) {\n return JSON.stringify([claims.sid, claims.sub, claims.org_id]);\n}\n\nconst useApi = () => {\n const context = React.useContext(ApiContext);\n\n if (context === undefined) {\n throw new Error(\"useApi must be used within an ApiProvider\");\n }\n\n return context;\n};\n\nconst useElevatedAccessToken = () => {\n const { elevatedAccess, setElevatedAccess } = useApi();\n\n return { elevatedAccess, setElevatedAccess };\n};\n\nconst useApiReady = () => {\n const { authToken } = useApi();\n const isHydrated = useIsHydrated();\n\n return isHydrated && authToken !== undefined;\n};\nexport type { WidgetType, AuthToken, AuthTokenQueryData };\nexport {\n authTokenQueryKey,\n ApiProvider,\n useApi,\n useElevatedAccessToken,\n useApiReady,\n};\n"],"mappings":"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"}
|
|
@@ -14,6 +14,11 @@ interface ApiContextValue {
|
|
|
14
14
|
widgetType: WidgetType;
|
|
15
15
|
elevatedAccess?: ElevatedAccess;
|
|
16
16
|
setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;
|
|
17
|
+
/**
|
|
18
|
+
* The token now belongs to a different session, user or organization, but
|
|
19
|
+
* the Client GraphQL data on screen may still belong to the previous one.
|
|
20
|
+
*/
|
|
21
|
+
isIdentityPending: boolean;
|
|
17
22
|
}
|
|
18
23
|
interface AuthTokenQueryData {
|
|
19
24
|
authToken: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-provider.d.cts","names":[],"sources":["../../../src/api/api-provider.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"api-provider.d.cts","names":[],"sources":["../../../src/api/api-provider.tsx"],"mappings":";;KAQK;KAeA,4BAA4B;UAEvB;EACR;EACA;;UAGQ;EACR;EACA;EACA;EACA;EACA,YAAY;EACZ,iBAAiB;EACjB,oBAAoB,iBAAiB;;;;;EAKrC;;UAGQ;EACR;EACA;EACA;;cAGI,oBAAiB,YAAgB,sCACzB;UAaJ;EACR,WAAW;EACX;EACA,UAAU,MAAM;EAChB,YAAY;;cAGR,aAAa,MAAM,GAAG;cAgNtB,cAAM;cAUN;kBAAA;EA7PiC,oBAAA,iBAAA;;cAmQjC"}
|
|
@@ -14,6 +14,11 @@ interface ApiContextValue {
|
|
|
14
14
|
widgetType: WidgetType;
|
|
15
15
|
elevatedAccess?: ElevatedAccess;
|
|
16
16
|
setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;
|
|
17
|
+
/**
|
|
18
|
+
* The token now belongs to a different session, user or organization, but
|
|
19
|
+
* the Client GraphQL data on screen may still belong to the previous one.
|
|
20
|
+
*/
|
|
21
|
+
isIdentityPending: boolean;
|
|
17
22
|
}
|
|
18
23
|
interface AuthTokenQueryData {
|
|
19
24
|
authToken: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-provider.d.ts","names":[],"sources":["../../../src/api/api-provider.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"api-provider.d.ts","names":[],"sources":["../../../src/api/api-provider.tsx"],"mappings":";;KAQK;KAeA,4BAA4B;UAEvB;EACR;EACA;;UAGQ;EACR;EACA;EACA;EACA;EACA,YAAY;EACZ,iBAAiB;EACjB,oBAAoB,iBAAiB;;;;;EAKrC;;UAGQ;EACR;EACA;EACA;;cAGI,oBAAiB,YAAgB,sCACzB;UAaJ;EACR,WAAW;EACX;EACA,UAAU,MAAM;EAChB,YAAY;;cAGR,aAAa,MAAM,GAAG;cAgNtB,cAAM;cAUN;kBAAA;EA7PiC,oBAAA,iBAAA;;cAmQjC"}
|
|
@@ -36,8 +36,7 @@ const ApiProvider = ({ authToken, baseUrl, children, widgetType }) => {
|
|
|
36
36
|
});
|
|
37
37
|
React.useEffect(() => {
|
|
38
38
|
if (typeof authToken !== "string") return;
|
|
39
|
-
|
|
40
|
-
if (cached?.authToken === authToken) return;
|
|
39
|
+
if (queryClient.getQueryData(queryKey)?.authToken === authToken) return;
|
|
41
40
|
queryClient.cancelQueries({ queryKey });
|
|
42
41
|
const claims = getClaims(authToken);
|
|
43
42
|
queryClient.setQueryData(queryKey, {
|
|
@@ -45,12 +44,38 @@ const ApiProvider = ({ authToken, baseUrl, children, widgetType }) => {
|
|
|
45
44
|
permissions: claims.permissions,
|
|
46
45
|
exp: claims.exp
|
|
47
46
|
});
|
|
48
|
-
if (cached && !isSameSubject(getClaims(cached.authToken), claims)) queryClient.invalidateQueries({ predicate: (query) => isClientGraphqlQueryKey(query.queryKey) });
|
|
49
47
|
}, [
|
|
50
48
|
authToken,
|
|
51
49
|
queryClient,
|
|
52
50
|
queryKey
|
|
53
51
|
]);
|
|
52
|
+
const currentToken = typeof authToken === "string" ? authToken : authTokenQuery.data?.authToken;
|
|
53
|
+
const currentIdentityKey = React.useMemo(() => currentToken ? getIdentityKey(getClaims(currentToken)) : void 0, [currentToken]);
|
|
54
|
+
const [tokenIdentity, setTokenIdentity] = React.useState({
|
|
55
|
+
key: currentIdentityKey,
|
|
56
|
+
generation: 0
|
|
57
|
+
});
|
|
58
|
+
if (currentIdentityKey !== tokenIdentity.key) setTokenIdentity({
|
|
59
|
+
key: currentIdentityKey,
|
|
60
|
+
generation: tokenIdentity.key === void 0 ? tokenIdentity.generation : tokenIdentity.generation + 1
|
|
61
|
+
});
|
|
62
|
+
const [settledGeneration, setSettledGeneration] = React.useState(0);
|
|
63
|
+
const identityGeneration = tokenIdentity.generation;
|
|
64
|
+
const isIdentityPending = settledGeneration !== identityGeneration;
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
if (!isIdentityPending) return;
|
|
67
|
+
queryClient.invalidateQueries({ predicate: isClientGraphqlQuery });
|
|
68
|
+
const settleWhenRefetched = () => {
|
|
69
|
+
if (!queryClient.getQueryCache().findAll({ predicate: isClientGraphqlQuery }).some((query) => query.state.fetchStatus === "fetching" || query.isActive() && query.state.isInvalidated)) setSettledGeneration(identityGeneration);
|
|
70
|
+
};
|
|
71
|
+
const unsubscribe = queryClient.getQueryCache().subscribe(settleWhenRefetched);
|
|
72
|
+
settleWhenRefetched();
|
|
73
|
+
return unsubscribe;
|
|
74
|
+
}, [
|
|
75
|
+
identityGeneration,
|
|
76
|
+
isIdentityPending,
|
|
77
|
+
queryClient
|
|
78
|
+
]);
|
|
54
79
|
const exp = authTokenQuery.data?.exp;
|
|
55
80
|
React.useEffect(() => {
|
|
56
81
|
if (!exp) return;
|
|
@@ -86,21 +111,30 @@ const ApiProvider = ({ authToken, baseUrl, children, widgetType }) => {
|
|
|
86
111
|
baseUrl,
|
|
87
112
|
elevatedAccess,
|
|
88
113
|
setElevatedAccess,
|
|
89
|
-
widgetType
|
|
114
|
+
widgetType,
|
|
115
|
+
isIdentityPending
|
|
90
116
|
}), [
|
|
91
117
|
authTokenQuery.data,
|
|
92
118
|
baseUrl,
|
|
93
119
|
elevatedAccess,
|
|
94
120
|
setElevatedAccess,
|
|
95
|
-
widgetType
|
|
121
|
+
widgetType,
|
|
122
|
+
isIdentityPending
|
|
96
123
|
]);
|
|
97
124
|
return /* @__PURE__ */ jsx(ApiContext.Provider, {
|
|
98
125
|
value,
|
|
99
126
|
children
|
|
100
127
|
});
|
|
101
128
|
};
|
|
102
|
-
function
|
|
103
|
-
return
|
|
129
|
+
function isClientGraphqlQuery(query) {
|
|
130
|
+
return isClientGraphqlQueryKey(query.queryKey);
|
|
131
|
+
}
|
|
132
|
+
function getIdentityKey(claims) {
|
|
133
|
+
return JSON.stringify([
|
|
134
|
+
claims.sid,
|
|
135
|
+
claims.sub,
|
|
136
|
+
claims.org_id
|
|
137
|
+
]);
|
|
104
138
|
}
|
|
105
139
|
const useApi = () => {
|
|
106
140
|
const context = React.useContext(ApiContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-provider.js","names":[],"sources":["../../../src/api/api-provider.tsx"],"sourcesContent":["import { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport * as React from \"react\";\nimport { useIsHydrated } from \"../lib/use-is-hydrated.js\";\nimport { getClaims } from \"./claims.js\";\nimport type { Claims } from \"./claims.js\";\nimport { isClientGraphqlQueryKey } from \"./client-graphql/query-keys.js\";\n\ntype WidgetType =\n | \"admin-portal-audit-log-streaming\"\n | \"admin-portal-domain-verification\"\n | \"user-management\"\n | \"user-api-keys\"\n | \"organization-switcher\"\n | \"user-sessions\"\n | \"user-security\"\n | \"user-profile\"\n | \"admin-portal-sso-connection\"\n | \"api-keys\"\n | \"pipes\"\n | \"pipes-admin\"\n | \"directory-sync\";\n\ntype AuthToken = string | (() => Promise<string>);\n\ninterface ElevatedAccess {\n token: string;\n expiresAt: string;\n}\n\ninterface ApiContextValue {\n authToken: string | undefined;\n permissions: string[] | undefined;\n exp: number | undefined;\n baseUrl: string;\n widgetType: WidgetType;\n elevatedAccess?: ElevatedAccess;\n setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;\n}\n\ninterface AuthTokenQueryData {\n authToken: string;\n permissions: string[] | undefined;\n exp: number | undefined;\n}\n\nconst authTokenQueryKey = (widgetType: WidgetType) =>\n [\"authToken\", widgetType] as const;\n\n// Refetch this many ms before the JWT expires so data queries always see a\n// fresh token.\nconst TOKEN_REFRESH_BUFFER_MS = 30_000;\n\n// Cap setTimeout delays at ~24 days to stay well below the 32-bit signed int\n// limit (~24.8 days), which some runtimes silently coerce to 1ms.\nconst MAX_TIMEOUT_MS = 2_147_483_000;\n\nconst ApiContext = React.createContext<ApiContextValue | undefined>(undefined);\nApiContext.displayName = \"ApiContext\";\n\ninterface ApiProviderProps {\n authToken: AuthToken;\n baseUrl: string;\n children: React.ReactNode;\n widgetType: WidgetType;\n}\n\nconst ApiProvider: React.FC<ApiProviderProps> = ({\n authToken,\n baseUrl,\n children,\n widgetType,\n}) => {\n const queryClient = useQueryClient();\n const queryKey = authTokenQueryKey(widgetType);\n const authTokenQuery = useQuery<AuthTokenQueryData>({\n initialData: () => {\n if (typeof authToken !== \"string\") {\n return undefined;\n }\n\n const claims = getClaims(authToken);\n return {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryFn: async () => {\n const resolvedToken =\n typeof authToken === \"string\" ? authToken : await authToken();\n const claims = getClaims(resolvedToken);\n\n return {\n authToken: resolvedToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryKey,\n });\n\n // `initialData` only seeds the cache on first mount, so a new string token\n // has to be written to the cache explicitly. A token for a different session,\n // user or organization (e.g. after switching organizations) also invalidates\n // data fetched with the previous token.\n React.useEffect(() => {\n if (typeof authToken !== \"string\") {\n return;\n }\n\n const cached = queryClient.getQueryData<AuthTokenQueryData>(queryKey);\n if (cached?.authToken === authToken) {\n return;\n }\n\n // Keep an in-flight fetch holding the previous token from overwriting the\n // new one.\n void queryClient.cancelQueries({ queryKey });\n const claims = getClaims(authToken);\n queryClient.setQueryData<AuthTokenQueryData>(queryKey, {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n });\n\n if (cached && !isSameSubject(getClaims(cached.authToken), claims)) {\n void queryClient.invalidateQueries({\n predicate: (query) => isClientGraphqlQueryKey(query.queryKey),\n });\n }\n }, [authToken, queryClient, queryKey]);\n\n // Keep the token \"fresh\" until ~30s before it expires, so window-focus\n // re-fetches re-mint before data queries use a stale Bearer.\n //\n // TODO(v2): prefer the function-form `staleTime` on `useQuery` above once we\n // can bump the `@tanstack/react-query` peer dep to ^5.44.0 (the version that\n // introduced it). That lets react-query own the freshness window and avoids\n // the extra timer + window-focus nonsense this effect introduces.\n const exp = authTokenQuery.data?.exp;\n React.useEffect(() => {\n if (!exp) {\n return;\n }\n\n const delay = Math.min(\n MAX_TIMEOUT_MS,\n exp * 1000 - Date.now() - TOKEN_REFRESH_BUFFER_MS,\n );\n if (delay <= 0) {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n return;\n }\n const timeout = window.setTimeout(() => {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n }, delay);\n return () => window.clearTimeout(timeout);\n }, [exp, queryClient, widgetType]);\n\n const [elevatedAccess, setElevatedAccess] = React.useState<ElevatedAccess>();\n const elevatedAccessTimeout = React.useRef<number | undefined>(undefined);\n\n // This effect manages the expiration of elevated access tokens\n // When an elevated access token is present, it checks every 30 seconds if the token has expired\n // If the token has expired (current time > expiration time), it clears the elevated access\n React.useEffect(() => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n\n if (elevatedAccess) {\n elevatedAccessTimeout.current = window.setInterval(() => {\n const now = new Date();\n const expiresAtDate = new Date(elevatedAccess.expiresAt);\n\n // Reset the elevated access if it has expired\n if (now > expiresAtDate) {\n setElevatedAccess(undefined);\n }\n }, 30_000); // every 30 seconds\n }\n\n return () => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n };\n }, [elevatedAccess]);\n\n const value = React.useMemo(\n () => ({\n authToken: authTokenQuery.data?.authToken,\n permissions: authTokenQuery.data?.permissions,\n exp: authTokenQuery.data?.exp,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n }),\n [\n authTokenQuery.data,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n ],\n );\n\n return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;\n};\n\nfunction isSameSubject(a: Claims, b: Claims) {\n return a.sid === b.sid && a.sub === b.sub && a.org_id === b.org_id;\n}\n\nconst useApi = () => {\n const context = React.useContext(ApiContext);\n\n if (context === undefined) {\n throw new Error(\"useApi must be used within an ApiProvider\");\n }\n\n return context;\n};\n\nconst useElevatedAccessToken = () => {\n const { elevatedAccess, setElevatedAccess } = useApi();\n\n return { elevatedAccess, setElevatedAccess };\n};\n\nconst useApiReady = () => {\n const { authToken } = useApi();\n const isHydrated = useIsHydrated();\n\n return isHydrated && authToken !== undefined;\n};\nexport type { WidgetType, AuthToken, AuthTokenQueryData };\nexport {\n authTokenQueryKey,\n ApiProvider,\n useApi,\n useElevatedAccessToken,\n useApiReady,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-provider.js","names":[],"sources":["../../../src/api/api-provider.tsx"],"sourcesContent":["import { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport type { Query } from \"@tanstack/react-query\";\nimport * as React from \"react\";\nimport { useIsHydrated } from \"../lib/use-is-hydrated.js\";\nimport { getClaims } from \"./claims.js\";\nimport type { Claims } from \"./claims.js\";\nimport { isClientGraphqlQueryKey } from \"./client-graphql/query-keys.js\";\n\ntype WidgetType =\n | \"admin-portal-audit-log-streaming\"\n | \"admin-portal-domain-verification\"\n | \"user-management\"\n | \"user-api-keys\"\n | \"organization-switcher\"\n | \"user-sessions\"\n | \"user-security\"\n | \"user-profile\"\n | \"admin-portal-sso-connection\"\n | \"api-keys\"\n | \"pipes\"\n | \"pipes-admin\"\n | \"directory-sync\";\n\ntype AuthToken = string | (() => Promise<string>);\n\ninterface ElevatedAccess {\n token: string;\n expiresAt: string;\n}\n\ninterface ApiContextValue {\n authToken: string | undefined;\n permissions: string[] | undefined;\n exp: number | undefined;\n baseUrl: string;\n widgetType: WidgetType;\n elevatedAccess?: ElevatedAccess;\n setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;\n /**\n * The token now belongs to a different session, user or organization, but\n * the Client GraphQL data on screen may still belong to the previous one.\n */\n isIdentityPending: boolean;\n}\n\ninterface AuthTokenQueryData {\n authToken: string;\n permissions: string[] | undefined;\n exp: number | undefined;\n}\n\nconst authTokenQueryKey = (widgetType: WidgetType) =>\n [\"authToken\", widgetType] as const;\n\n// Refetch this many ms before the JWT expires so data queries always see a\n// fresh token.\nconst TOKEN_REFRESH_BUFFER_MS = 30_000;\n\n// Cap setTimeout delays at ~24 days to stay well below the 32-bit signed int\n// limit (~24.8 days), which some runtimes silently coerce to 1ms.\nconst MAX_TIMEOUT_MS = 2_147_483_000;\n\nconst ApiContext = React.createContext<ApiContextValue | undefined>(undefined);\nApiContext.displayName = \"ApiContext\";\n\ninterface ApiProviderProps {\n authToken: AuthToken;\n baseUrl: string;\n children: React.ReactNode;\n widgetType: WidgetType;\n}\n\nconst ApiProvider: React.FC<ApiProviderProps> = ({\n authToken,\n baseUrl,\n children,\n widgetType,\n}) => {\n const queryClient = useQueryClient();\n const queryKey = authTokenQueryKey(widgetType);\n const authTokenQuery = useQuery<AuthTokenQueryData>({\n initialData: () => {\n if (typeof authToken !== \"string\") {\n return undefined;\n }\n\n const claims = getClaims(authToken);\n return {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryFn: async () => {\n const resolvedToken =\n typeof authToken === \"string\" ? authToken : await authToken();\n const claims = getClaims(resolvedToken);\n\n return {\n authToken: resolvedToken,\n permissions: claims.permissions,\n exp: claims.exp,\n };\n },\n queryKey,\n });\n\n // `initialData` only seeds the cache on first mount, so a new string token\n // has to be written to the cache explicitly.\n React.useEffect(() => {\n if (typeof authToken !== \"string\") {\n return;\n }\n\n const cached = queryClient.getQueryData<AuthTokenQueryData>(queryKey);\n if (cached?.authToken === authToken) {\n return;\n }\n\n // Keep an in-flight fetch holding the previous token from overwriting the\n // new one.\n void queryClient.cancelQueries({ queryKey });\n const claims = getClaims(authToken);\n queryClient.setQueryData<AuthTokenQueryData>(queryKey, {\n authToken,\n permissions: claims.permissions,\n exp: claims.exp,\n });\n }, [authToken, queryClient, queryKey]);\n\n const currentToken =\n typeof authToken === \"string\" ? authToken : authTokenQuery.data?.authToken;\n const currentIdentityKey = React.useMemo(\n () => (currentToken ? getIdentityKey(getClaims(currentToken)) : undefined),\n [currentToken],\n );\n // Counts changes to the token's session, user or organization (e.g. after\n // switching organizations). Switching back to an earlier identity counts too,\n // since requests made for the one in between may still be in flight.\n const [tokenIdentity, setTokenIdentity] = React.useState({\n key: currentIdentityKey,\n generation: 0,\n });\n if (currentIdentityKey !== tokenIdentity.key) {\n setTokenIdentity({\n key: currentIdentityKey,\n generation:\n tokenIdentity.key === undefined\n ? tokenIdentity.generation\n : tokenIdentity.generation + 1,\n });\n }\n // The identity generation whose Client GraphQL data is on screen. The\n // previous identity's data stays on screen until it has been refetched with\n // the new token.\n const [settledGeneration, setSettledGeneration] = React.useState(0);\n const identityGeneration = tokenIdentity.generation;\n const isIdentityPending = settledGeneration !== identityGeneration;\n React.useEffect(() => {\n if (!isIdentityPending) {\n return;\n }\n\n // Cancels refetches still in flight for an earlier identity.\n void queryClient.invalidateQueries({ predicate: isClientGraphqlQuery });\n // A failed refetch leaves its query invalidated, so the previous identity's\n // data stays locked until a refetch succeeds.\n const settleWhenRefetched = () => {\n const isRefetching = queryClient\n .getQueryCache()\n .findAll({ predicate: isClientGraphqlQuery })\n .some(\n (query) =>\n query.state.fetchStatus === \"fetching\" ||\n (query.isActive() && query.state.isInvalidated),\n );\n if (!isRefetching) {\n setSettledGeneration(identityGeneration);\n }\n };\n const unsubscribe = queryClient\n .getQueryCache()\n .subscribe(settleWhenRefetched);\n settleWhenRefetched();\n return unsubscribe;\n }, [identityGeneration, isIdentityPending, queryClient]);\n\n // Keep the token \"fresh\" until ~30s before it expires, so window-focus\n // re-fetches re-mint before data queries use a stale Bearer.\n //\n // TODO(v2): prefer the function-form `staleTime` on `useQuery` above once we\n // can bump the `@tanstack/react-query` peer dep to ^5.44.0 (the version that\n // introduced it). That lets react-query own the freshness window and avoids\n // the extra timer + window-focus nonsense this effect introduces.\n const exp = authTokenQuery.data?.exp;\n React.useEffect(() => {\n if (!exp) {\n return;\n }\n\n const delay = Math.min(\n MAX_TIMEOUT_MS,\n exp * 1000 - Date.now() - TOKEN_REFRESH_BUFFER_MS,\n );\n if (delay <= 0) {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n return;\n }\n const timeout = window.setTimeout(() => {\n void queryClient.refetchQueries({\n queryKey: authTokenQueryKey(widgetType),\n });\n }, delay);\n return () => window.clearTimeout(timeout);\n }, [exp, queryClient, widgetType]);\n\n const [elevatedAccess, setElevatedAccess] = React.useState<ElevatedAccess>();\n const elevatedAccessTimeout = React.useRef<number | undefined>(undefined);\n\n // This effect manages the expiration of elevated access tokens\n // When an elevated access token is present, it checks every 30 seconds if the token has expired\n // If the token has expired (current time > expiration time), it clears the elevated access\n React.useEffect(() => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n\n if (elevatedAccess) {\n elevatedAccessTimeout.current = window.setInterval(() => {\n const now = new Date();\n const expiresAtDate = new Date(elevatedAccess.expiresAt);\n\n // Reset the elevated access if it has expired\n if (now > expiresAtDate) {\n setElevatedAccess(undefined);\n }\n }, 30_000); // every 30 seconds\n }\n\n return () => {\n if (elevatedAccessTimeout.current) {\n window.clearInterval(elevatedAccessTimeout.current);\n }\n };\n }, [elevatedAccess]);\n\n const value = React.useMemo(\n () => ({\n authToken: authTokenQuery.data?.authToken,\n permissions: authTokenQuery.data?.permissions,\n exp: authTokenQuery.data?.exp,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n isIdentityPending,\n }),\n [\n authTokenQuery.data,\n baseUrl,\n elevatedAccess,\n setElevatedAccess,\n widgetType,\n isIdentityPending,\n ],\n );\n\n return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;\n};\n\nfunction isClientGraphqlQuery(query: Query) {\n return isClientGraphqlQueryKey(query.queryKey);\n}\n\nfunction getIdentityKey(claims: Claims) {\n return JSON.stringify([claims.sid, claims.sub, claims.org_id]);\n}\n\nconst useApi = () => {\n const context = React.useContext(ApiContext);\n\n if (context === undefined) {\n throw new Error(\"useApi must be used within an ApiProvider\");\n }\n\n return context;\n};\n\nconst useElevatedAccessToken = () => {\n const { elevatedAccess, setElevatedAccess } = useApi();\n\n return { elevatedAccess, setElevatedAccess };\n};\n\nconst useApiReady = () => {\n const { authToken } = useApi();\n const isHydrated = useIsHydrated();\n\n return isHydrated && authToken !== undefined;\n};\nexport type { WidgetType, AuthToken, AuthTokenQueryData };\nexport {\n authTokenQueryKey,\n ApiProvider,\n useApi,\n useElevatedAccessToken,\n useApiReady,\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/api/api-provider.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { Query } from "@tanstack/react-query";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
import { useIsHydrated } from "../lib/use-is-hydrated.js";
|
|
4
5
|
import { getClaims } from "./claims.js";
|
|
@@ -35,6 +36,11 @@ interface ApiContextValue {
|
|
|
35
36
|
widgetType: WidgetType;
|
|
36
37
|
elevatedAccess?: ElevatedAccess;
|
|
37
38
|
setElevatedAccess: (elevatedAccess?: ElevatedAccess) => void;
|
|
39
|
+
/**
|
|
40
|
+
* The token now belongs to a different session, user or organization, but
|
|
41
|
+
* the Client GraphQL data on screen may still belong to the previous one.
|
|
42
|
+
*/
|
|
43
|
+
isIdentityPending: boolean;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
interface AuthTokenQueryData {
|
|
@@ -100,9 +106,7 @@ const ApiProvider: React.FC<ApiProviderProps> = ({
|
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
// `initialData` only seeds the cache on first mount, so a new string token
|
|
103
|
-
// has to be written to the cache explicitly.
|
|
104
|
-
// user or organization (e.g. after switching organizations) also invalidates
|
|
105
|
-
// data fetched with the previous token.
|
|
109
|
+
// has to be written to the cache explicitly.
|
|
106
110
|
React.useEffect(() => {
|
|
107
111
|
if (typeof authToken !== "string") {
|
|
108
112
|
return;
|
|
@@ -122,13 +126,64 @@ const ApiProvider: React.FC<ApiProviderProps> = ({
|
|
|
122
126
|
permissions: claims.permissions,
|
|
123
127
|
exp: claims.exp,
|
|
124
128
|
});
|
|
129
|
+
}, [authToken, queryClient, queryKey]);
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
const currentToken =
|
|
132
|
+
typeof authToken === "string" ? authToken : authTokenQuery.data?.authToken;
|
|
133
|
+
const currentIdentityKey = React.useMemo(
|
|
134
|
+
() => (currentToken ? getIdentityKey(getClaims(currentToken)) : undefined),
|
|
135
|
+
[currentToken],
|
|
136
|
+
);
|
|
137
|
+
// Counts changes to the token's session, user or organization (e.g. after
|
|
138
|
+
// switching organizations). Switching back to an earlier identity counts too,
|
|
139
|
+
// since requests made for the one in between may still be in flight.
|
|
140
|
+
const [tokenIdentity, setTokenIdentity] = React.useState({
|
|
141
|
+
key: currentIdentityKey,
|
|
142
|
+
generation: 0,
|
|
143
|
+
});
|
|
144
|
+
if (currentIdentityKey !== tokenIdentity.key) {
|
|
145
|
+
setTokenIdentity({
|
|
146
|
+
key: currentIdentityKey,
|
|
147
|
+
generation:
|
|
148
|
+
tokenIdentity.key === undefined
|
|
149
|
+
? tokenIdentity.generation
|
|
150
|
+
: tokenIdentity.generation + 1,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// The identity generation whose Client GraphQL data is on screen. The
|
|
154
|
+
// previous identity's data stays on screen until it has been refetched with
|
|
155
|
+
// the new token.
|
|
156
|
+
const [settledGeneration, setSettledGeneration] = React.useState(0);
|
|
157
|
+
const identityGeneration = tokenIdentity.generation;
|
|
158
|
+
const isIdentityPending = settledGeneration !== identityGeneration;
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
if (!isIdentityPending) {
|
|
161
|
+
return;
|
|
130
162
|
}
|
|
131
|
-
|
|
163
|
+
|
|
164
|
+
// Cancels refetches still in flight for an earlier identity.
|
|
165
|
+
void queryClient.invalidateQueries({ predicate: isClientGraphqlQuery });
|
|
166
|
+
// A failed refetch leaves its query invalidated, so the previous identity's
|
|
167
|
+
// data stays locked until a refetch succeeds.
|
|
168
|
+
const settleWhenRefetched = () => {
|
|
169
|
+
const isRefetching = queryClient
|
|
170
|
+
.getQueryCache()
|
|
171
|
+
.findAll({ predicate: isClientGraphqlQuery })
|
|
172
|
+
.some(
|
|
173
|
+
(query) =>
|
|
174
|
+
query.state.fetchStatus === "fetching" ||
|
|
175
|
+
(query.isActive() && query.state.isInvalidated),
|
|
176
|
+
);
|
|
177
|
+
if (!isRefetching) {
|
|
178
|
+
setSettledGeneration(identityGeneration);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const unsubscribe = queryClient
|
|
182
|
+
.getQueryCache()
|
|
183
|
+
.subscribe(settleWhenRefetched);
|
|
184
|
+
settleWhenRefetched();
|
|
185
|
+
return unsubscribe;
|
|
186
|
+
}, [identityGeneration, isIdentityPending, queryClient]);
|
|
132
187
|
|
|
133
188
|
// Keep the token "fresh" until ~30s before it expires, so window-focus
|
|
134
189
|
// re-fetches re-mint before data queries use a stale Bearer.
|
|
@@ -200,6 +255,7 @@ const ApiProvider: React.FC<ApiProviderProps> = ({
|
|
|
200
255
|
elevatedAccess,
|
|
201
256
|
setElevatedAccess,
|
|
202
257
|
widgetType,
|
|
258
|
+
isIdentityPending,
|
|
203
259
|
}),
|
|
204
260
|
[
|
|
205
261
|
authTokenQuery.data,
|
|
@@ -207,14 +263,19 @@ const ApiProvider: React.FC<ApiProviderProps> = ({
|
|
|
207
263
|
elevatedAccess,
|
|
208
264
|
setElevatedAccess,
|
|
209
265
|
widgetType,
|
|
266
|
+
isIdentityPending,
|
|
210
267
|
],
|
|
211
268
|
);
|
|
212
269
|
|
|
213
270
|
return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;
|
|
214
271
|
};
|
|
215
272
|
|
|
216
|
-
function
|
|
217
|
-
return
|
|
273
|
+
function isClientGraphqlQuery(query: Query) {
|
|
274
|
+
return isClientGraphqlQueryKey(query.queryKey);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getIdentityKey(claims: Claims) {
|
|
278
|
+
return JSON.stringify([claims.sid, claims.sub, claims.org_id]);
|
|
218
279
|
}
|
|
219
280
|
|
|
220
281
|
const useApi = () => {
|