From 6854ed0da6e077073938e95bdfa6b9dad19d9613 Mon Sep 17 00:00:00 2001 From: Evan Skrukwa Date: Tue, 27 May 2025 15:03:49 -0400 Subject: [PATCH 1/4] async xhr refactor --- src/utils/rest.ts | 126 ++++++++++++++++++---------- src/utils/sharepoint.rest/common.ts | 8 +- src/utils/sharepoint.rest/web.ts | 37 +++++++- 3 files changed, 125 insertions(+), 46 deletions(-) diff --git a/src/utils/rest.ts b/src/utils/rest.ts index ff17866..fd214ba 100644 --- a/src/utils/rest.ts +++ b/src/utils/rest.ts @@ -5,7 +5,7 @@ import { IDictionary } from "../types/common.types"; import { AllRestCacheOptionsKeys, IJsonSyncResult, IRequestBody, IRequestObjects, IRestCacheOptions, IRestError, IRestOptions, IRestRequestOptions, jsonTypes } from "../types/rest.types"; import { ConsoleLogger } from "./consolelogger"; import { getCacheItem, setCacheItem } from "./localstoragecache"; -import { getFormDigest } from "./sharepoint.rest/web"; +import {getFormDigest, getFormDigestSync} from "./sharepoint.rest/web"; var logger = ConsoleLogger.get("kwizcom.rest.module"); const supressDebugMessages = true; @@ -69,7 +69,82 @@ function fillHeaders(xhr: XMLHttpRequest, headers: { [key: string]: string; }) { } } -function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async = true): IRequestObjects { +/** + * Returns an XMLHttpRequest that was opened synchronously (will block on .send()) + */ +function getXhrSync(url: string, body?: IRequestBody, options?: IRestOptions): IRequestObjects { + + let [myOptions, myCacheOptions] = configureXhrHeaders(url, body, options); + + const xhr: XMLHttpRequest = new XMLHttpRequest(); + xhr.open(myOptions.method, url, false); + + fillHeaders(xhr, myOptions.headers); + + if (myOptions.cors) { + xhr.withCredentials = true; + } + + if (myOptions.method === "GET" && myOptions.includeDigestInGet || myOptions.method === "POST" && myOptions.includeDigestInPost) { + const digest = getFormDigestSync(myOptions.spWebUrl); + if (digest) { + xhr.setRequestHeader("X-RequestDigest", digest); + } else { + console.warn("X-RequestDigest header not set due to getFormDigest returning null"); + } + } + + if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { + if (myCacheOptions.allowCache === true && + (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { + logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); + } + xhr.responseType = myOptions.responseType; + } + + return { xhr: xhr, options: myOptions, cacheOptions: myCacheOptions}; +} + +/** + * Returns an XMLHttpRequest that was opened asynchronously (needs to have event listeners set up before .send()) + * + * Should not make synchronous (blocking) calls in the process (i.e. while getting a request digest) + * ^ todo: ensure this behaviour + */ +async function getXhr(url: string, body?: IRequestBody, options?: IRestOptions): Promise { + + let [myOptions, myCacheOptions] = configureXhrHeaders(url, body, options); + + const xhr: XMLHttpRequest = new XMLHttpRequest(); + xhr.open(myOptions.method, url); + + fillHeaders(xhr, myOptions.headers); + + if (myOptions.cors) { + xhr.withCredentials = true; + } + + if (myOptions.method === "GET" && myOptions.includeDigestInGet || myOptions.method === "POST" && myOptions.includeDigestInPost) { + const digest = await getFormDigest(myOptions.spWebUrl); + if (digest) { + xhr.setRequestHeader("X-RequestDigest", digest); + } else { + console.warn("X-RequestDigest header not set due to getFormDigest returning null"); + } + } + + if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { + if (myCacheOptions.allowCache === true && + (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { + logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); + } + xhr.responseType = myOptions.responseType; + } + + return { xhr: xhr, options: myOptions, cacheOptions: myCacheOptions}; +} + +function configureXhrHeaders(url: string, body?: IRequestBody, options?: IRestOptions): [IRestRequestOptions, IRestCacheOptions & { cacheKey?: string; }] { var optionsWithDefaults = assign({}, getDefaultOptions(), options); let myCacheOptions: IRestCacheOptions & { cacheKey?: string; } = {}; Object.keys(AllRestCacheOptionsKeys).forEach(key => { @@ -80,14 +155,8 @@ function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async }); let myOptions: IRestRequestOptions = { ...optionsWithDefaults }; - var xhr: XMLHttpRequest = new XMLHttpRequest(); - let jsonType = myOptions.jsonMetadata || jsonTypes.verbose; - if (myOptions.cors) { - xhr.withCredentials = true; - } - if (isNullOrUndefined(myOptions.headers)) myOptions.headers = {};//issue 660 in case the sender sent headers as null if (isNullOrUndefined(myOptions.headers["Accept"])) { myOptions.headers["Accept"] = jsonType; @@ -97,22 +166,10 @@ function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async if (isNullOrEmptyString(method)) { method = isNullOrUndefined(body) ? "GET" : "POST"; } - myOptions.method = method; - xhr.open(method, url, async !== false); - if (method === "GET") { - if (myOptions.includeDigestInGet === true) {//by default don't add it, unless explicitly asked in options - xhr.setRequestHeader("X-RequestDigest", getFormDigest(myOptions.spWebUrl)); - } - } - else if (method === "POST") { - if (isNullOrUndefined(myOptions.headers["content-type"])) { - myOptions.headers["content-type"] = jsonType; - } - if (myOptions.includeDigestInPost !== false) {//if explicitly set to false - don't include it - xhr.setRequestHeader("X-RequestDigest", getFormDigest(myOptions.spWebUrl)); - } + if (method === "POST" && isNullOrUndefined(myOptions.headers["content-type"])) { + myOptions.headers["content-type"] = jsonType; } if (!isNullOrEmptyString(myOptions.xHttpMethod)) { @@ -123,27 +180,13 @@ function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async } } - fillHeaders(xhr, myOptions.headers); - - if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { - if (myCacheOptions.allowCache === true && - (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { - logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); - } - xhr.responseType = myOptions.responseType; - } - //we do not support cache if there is a request body //postCacheKey - allow cache on post request for stuff like get item by CamlQuery if (isNullOrUndefined(body) || !isNullOrEmptyString(myOptions.postCacheKey)) { myCacheOptions.cacheKey = (url + '|' + JSON.stringify(myOptions)).trim().toLowerCase(); } - return { - xhr: xhr, - options: myOptions, - cacheOptions: myCacheOptions - }; + return [myOptions, myCacheOptions]; } function getCachedResult(objects: IRequestObjects): IJsonSyncResult { @@ -311,7 +354,7 @@ function _canSafelyStringify(result: any) { export function GetJsonSync(url: string, body?: IRequestBody, options?: IRestOptions): IJsonSyncResult { let xhr: XMLHttpRequest = null; let syncResult: IJsonSyncResult = null; - let objects = getXhr(url, body, options, false); + let objects = getXhrSync(url, body, options); try { var cachedResult = getCachedResult(objects); if (!isNullOrUndefined(cachedResult)) { @@ -373,10 +416,9 @@ export function GetJsonSync(url: string, body?: IRequestBody, options?: IRest return syncResult; } -export function GetJson(url: string, body?: IRequestBody, options?: IRestOptions): Promise { +export async function GetJson(url: string, body?: IRequestBody, options?: IRestOptions): Promise { try { - let objects = getXhr(url, body, options); - + let objects = await getXhr(url, body, options); var cachedResult = getCachedResult(objects); if (!isNullOrUndefined(cachedResult)) { if (!supressDebugMessages) { @@ -489,7 +531,7 @@ export function GetJson(url: string, body?: IRequestBody, options?: IRestOpti return xhrPromise; } catch (e) { - return Promise.reject({ message: "an error occured" }); + return Promise.reject({ error: e, message: e.message, stack: e.stack }); } } diff --git a/src/utils/sharepoint.rest/common.ts b/src/utils/sharepoint.rest/common.ts index 989776f..4e9ac5f 100644 --- a/src/utils/sharepoint.rest/common.ts +++ b/src/utils/sharepoint.rest/common.ts @@ -26,14 +26,16 @@ export function hasGlobalContext() { export function GetFileSiteUrl(fileUrl: string): string { let siteUrl: string; let urlParts = fileUrl.split('/'); - if (urlParts[urlParts.length - 1].indexOf('.') > 0)//file name - urlParts.pop();//file name let key = "GetSiteUrl|" + urlParts.join("/").toLowerCase(); siteUrl = getCacheItem(key); if (isNullOrUndefined(siteUrl)) { - while (!isValidGuid(GetWebIdSync(urlParts.join('/')))) + while (!isValidGuid(GetWebIdSync(urlParts.join('/')))) { urlParts.pop(); + if (urlParts.length === 0) { + return ''; + } + } siteUrl = normalizeUrl(urlParts.join('/')); setCacheItem(key, siteUrl, mediumLocalCache.localStorageExpiration);//keep for 15 minutes diff --git a/src/utils/sharepoint.rest/web.ts b/src/utils/sharepoint.rest/web.ts index c7553c1..801517b 100644 --- a/src/utils/sharepoint.rest/web.ts +++ b/src/utils/sharepoint.rest/web.ts @@ -964,6 +964,36 @@ export function GetContextWebInformationSync(siteUrl: string): IContextWebInform } } +export async function GetContextWebInformation(siteUrl: string): Promise { + var siteId: string = null; + if (hasGlobalContext() && _spPageContextInfo && _spPageContextInfo.isAppWeb) { + //inside an app web you can't get the contextinfo for any other site + siteUrl = _spPageContextInfo.webServerRelativeUrl; + siteId = _spPageContextInfo.siteId; + } else { + siteId = await GetSiteId(siteUrl); + + if (isNullOrEmptyString(siteId)) { + return null; + } + } + + try { + let result = await GetJson<{ + d: { GetContextWebInformation: IContextWebInformation; }; + }>(`${GetRestBaseUrl(siteUrl)}/contextinfo`, null, { + method: "POST", + maxAge: 5 * 60, + includeDigestInPost: false, + allowCache: true, + postCacheKey: `GetContextWebInformation_${normalizeGuid(siteId)}` + }); + return result.d.GetContextWebInformation; + } catch { + return null; + } +} + function _getCustomActionsBaseRestUrl(siteUrl?: string, options: { listId?: string, actionId?: string } = {}) { const { listId, actionId } = { ...options }; @@ -1181,11 +1211,16 @@ export async function GetWebPropertyByName(name: string, siteUrl?: string) { return null; } -export function getFormDigest(serverRelativeWebUrl?: string) { +export function getFormDigestSync(serverRelativeWebUrl?: string) { var contextWebInformation = GetContextWebInformationSync(serverRelativeWebUrl); return contextWebInformation && contextWebInformation.FormDigestValue || null; } +export async function getFormDigest(serverRelativeWebUrl?: string) { + var contextWebInformation = await GetContextWebInformation(serverRelativeWebUrl); + return contextWebInformation && contextWebInformation.FormDigestValue || null; +} + export interface spfxContext { legacyPageContext: typeof _spPageContextInfo } export function ensureLegacyProps(pageContext: spfxContext) { try { From 55270d4ff71a238cec352d9ccf7380bbeceb6ca0 Mon Sep 17 00:00:00 2001 From: evan <71612373+skrukwa@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:02:06 -0400 Subject: [PATCH 2/4] update rest.ts --- src/utils/rest.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/utils/rest.ts b/src/utils/rest.ts index fd214ba..cb58eab 100644 --- a/src/utils/rest.ts +++ b/src/utils/rest.ts @@ -162,11 +162,9 @@ function configureXhrHeaders(url: string, body?: IRequestBody, options?: IRestOp myOptions.headers["Accept"] = jsonType; } - let method = myOptions.method; - if (isNullOrEmptyString(method)) { - method = isNullOrUndefined(body) ? "GET" : "POST"; + if (isNullOrEmptyString(myOptions.method)) { + myOptions.method = isNullOrUndefined(body) ? "GET" : "POST"; } - myOptions.method = method; if (method === "POST" && isNullOrUndefined(myOptions.headers["content-type"])) { myOptions.headers["content-type"] = jsonType; @@ -541,4 +539,4 @@ export function GetJsonClearCache() { Object.keys(_cachedResults).forEach(key => { delete _cachedResults[key]; }); -} \ No newline at end of file +} From b1b398b5c02c90aaec1cec7b248187a3f14f3449 Mon Sep 17 00:00:00 2001 From: Evan Skrukwa Date: Tue, 17 Jun 2025 16:33:53 -0400 Subject: [PATCH 3/4] revert GetFileSiteUrl bug fix - fix in other pr --- src/utils/sharepoint.rest/common.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/utils/sharepoint.rest/common.ts b/src/utils/sharepoint.rest/common.ts index 4e9ac5f..989776f 100644 --- a/src/utils/sharepoint.rest/common.ts +++ b/src/utils/sharepoint.rest/common.ts @@ -26,16 +26,14 @@ export function hasGlobalContext() { export function GetFileSiteUrl(fileUrl: string): string { let siteUrl: string; let urlParts = fileUrl.split('/'); + if (urlParts[urlParts.length - 1].indexOf('.') > 0)//file name + urlParts.pop();//file name let key = "GetSiteUrl|" + urlParts.join("/").toLowerCase(); siteUrl = getCacheItem(key); if (isNullOrUndefined(siteUrl)) { - while (!isValidGuid(GetWebIdSync(urlParts.join('/')))) { + while (!isValidGuid(GetWebIdSync(urlParts.join('/')))) urlParts.pop(); - if (urlParts.length === 0) { - return ''; - } - } siteUrl = normalizeUrl(urlParts.join('/')); setCacheItem(key, siteUrl, mediumLocalCache.localStorageExpiration);//keep for 15 minutes From b4bb11fbb88c133d03867c5d74a4b95d46990896 Mon Sep 17 00:00:00 2001 From: Evan Skrukwa Date: Wed, 18 Jun 2025 11:21:54 -0400 Subject: [PATCH 4/4] revert getFormDigest exported signature using ts overloads --- src/utils/rest.ts | 73 ++++++++++++-------------------- src/utils/sharepoint.rest/web.ts | 19 +++++---- 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/utils/rest.ts b/src/utils/rest.ts index cb58eab..e927e9f 100644 --- a/src/utils/rest.ts +++ b/src/utils/rest.ts @@ -5,7 +5,7 @@ import { IDictionary } from "../types/common.types"; import { AllRestCacheOptionsKeys, IJsonSyncResult, IRequestBody, IRequestObjects, IRestCacheOptions, IRestError, IRestOptions, IRestRequestOptions, jsonTypes } from "../types/rest.types"; import { ConsoleLogger } from "./consolelogger"; import { getCacheItem, setCacheItem } from "./localstoragecache"; -import {getFormDigest, getFormDigestSync} from "./sharepoint.rest/web"; +import { getFormDigest } from "./sharepoint.rest/web"; var logger = ConsoleLogger.get("kwizcom.rest.module"); const supressDebugMessages = true; @@ -70,14 +70,18 @@ function fillHeaders(xhr: XMLHttpRequest, headers: { [key: string]: string; }) { } /** - * Returns an XMLHttpRequest that was opened synchronously (will block on .send()) + * Depending on async: + * - Returns an XMLHttpRequest that was opened asynchronously (needs to have event listeners set up before .send()) or + * - Returns an XMLHttpRequest that was opened synchronously (will block on .send()) */ -function getXhrSync(url: string, body?: IRequestBody, options?: IRestOptions): IRequestObjects { +function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async?: true): Promise +function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async?: false): IRequestObjects +function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async: boolean = false): IRequestObjects | Promise { let [myOptions, myCacheOptions] = configureXhrHeaders(url, body, options); const xhr: XMLHttpRequest = new XMLHttpRequest(); - xhr.open(myOptions.method, url, false); + xhr.open(myOptions.method, url, async); fillHeaders(xhr, myOptions.headers); @@ -85,15 +89,6 @@ function getXhrSync(url: string, body?: IRequestBody, options?: IRestOptions): I xhr.withCredentials = true; } - if (myOptions.method === "GET" && myOptions.includeDigestInGet || myOptions.method === "POST" && myOptions.includeDigestInPost) { - const digest = getFormDigestSync(myOptions.spWebUrl); - if (digest) { - xhr.setRequestHeader("X-RequestDigest", digest); - } else { - console.warn("X-RequestDigest header not set due to getFormDigest returning null"); - } - } - if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { if (myCacheOptions.allowCache === true && (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { @@ -102,30 +97,11 @@ function getXhrSync(url: string, body?: IRequestBody, options?: IRestOptions): I xhr.responseType = myOptions.responseType; } - return { xhr: xhr, options: myOptions, cacheOptions: myCacheOptions}; -} - -/** - * Returns an XMLHttpRequest that was opened asynchronously (needs to have event listeners set up before .send()) - * - * Should not make synchronous (blocking) calls in the process (i.e. while getting a request digest) - * ^ todo: ensure this behaviour - */ -async function getXhr(url: string, body?: IRequestBody, options?: IRestOptions): Promise { + const needsDigest = + (myOptions.method === "GET" && myOptions.includeDigestInGet) || + (myOptions.method === "POST" && myOptions.includeDigestInPost); - let [myOptions, myCacheOptions] = configureXhrHeaders(url, body, options); - - const xhr: XMLHttpRequest = new XMLHttpRequest(); - xhr.open(myOptions.method, url); - - fillHeaders(xhr, myOptions.headers); - - if (myOptions.cors) { - xhr.withCredentials = true; - } - - if (myOptions.method === "GET" && myOptions.includeDigestInGet || myOptions.method === "POST" && myOptions.includeDigestInPost) { - const digest = await getFormDigest(myOptions.spWebUrl); + const applyDigest = (digest: string | null) => { if (digest) { xhr.setRequestHeader("X-RequestDigest", digest); } else { @@ -133,15 +109,20 @@ async function getXhr(url: string, body?: IRequestBody, options?: IRestOptions): } } - if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { - if (myCacheOptions.allowCache === true && - (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { - logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); - } - xhr.responseType = myOptions.responseType; + const result: IRequestObjects = { xhr, options: myOptions, cacheOptions: myCacheOptions }; + + if (needsDigest && async) { + return getFormDigest(myOptions.spWebUrl, true).then(digest => { + applyDigest(digest); + return result; + }) + } else if (needsDigest && !async) { + const digest = getFormDigest(myOptions.spWebUrl, false); + applyDigest(digest); + return result; } - return { xhr: xhr, options: myOptions, cacheOptions: myCacheOptions}; + return result; } function configureXhrHeaders(url: string, body?: IRequestBody, options?: IRestOptions): [IRestRequestOptions, IRestCacheOptions & { cacheKey?: string; }] { @@ -166,7 +147,7 @@ function configureXhrHeaders(url: string, body?: IRequestBody, options?: IRestOp myOptions.method = isNullOrUndefined(body) ? "GET" : "POST"; } - if (method === "POST" && isNullOrUndefined(myOptions.headers["content-type"])) { + if (myOptions.method === "POST" && isNullOrUndefined(myOptions.headers["content-type"])) { myOptions.headers["content-type"] = jsonType; } @@ -352,7 +333,7 @@ function _canSafelyStringify(result: any) { export function GetJsonSync(url: string, body?: IRequestBody, options?: IRestOptions): IJsonSyncResult { let xhr: XMLHttpRequest = null; let syncResult: IJsonSyncResult = null; - let objects = getXhrSync(url, body, options); + let objects = getXhr(url, body, options, false); try { var cachedResult = getCachedResult(objects); if (!isNullOrUndefined(cachedResult)) { @@ -416,7 +397,7 @@ export function GetJsonSync(url: string, body?: IRequestBody, options?: IRest export async function GetJson(url: string, body?: IRequestBody, options?: IRestOptions): Promise { try { - let objects = await getXhr(url, body, options); + let objects = await getXhr(url, body, options, true); var cachedResult = getCachedResult(objects); if (!isNullOrUndefined(cachedResult)) { if (!supressDebugMessages) { diff --git a/src/utils/sharepoint.rest/web.ts b/src/utils/sharepoint.rest/web.ts index 801517b..abb8fe7 100644 --- a/src/utils/sharepoint.rest/web.ts +++ b/src/utils/sharepoint.rest/web.ts @@ -1211,14 +1211,17 @@ export async function GetWebPropertyByName(name: string, siteUrl?: string) { return null; } -export function getFormDigestSync(serverRelativeWebUrl?: string) { - var contextWebInformation = GetContextWebInformationSync(serverRelativeWebUrl); - return contextWebInformation && contextWebInformation.FormDigestValue || null; -} - -export async function getFormDigest(serverRelativeWebUrl?: string) { - var contextWebInformation = await GetContextWebInformation(serverRelativeWebUrl); - return contextWebInformation && contextWebInformation.FormDigestValue || null; +export function getFormDigest(serverRelativeWebUrl?: string, async?: true): Promise +export function getFormDigest(serverRelativeWebUrl?: string, async?: false): string | null +export function getFormDigest(serverRelativeWebUrl?: string, async: boolean = false): string | null | Promise { + if (async) { + return GetContextWebInformation(serverRelativeWebUrl).then(contextWebInformation => { + return contextWebInformation && contextWebInformation.FormDigestValue || null; + }); + } else { + let contextWebInformation = GetContextWebInformationSync(serverRelativeWebUrl); + return contextWebInformation && contextWebInformation.FormDigestValue || null; + } } export interface spfxContext { legacyPageContext: typeof _spPageContextInfo }