@xh/hoist 76.0.0-SNAPSHOT.1758217044052 → 76.0.0-SNAPSHOT.1758310971360
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 +5 -3
- package/admin/tabs/userData/roles/graph/RoleGraph.ts +1 -2
- package/appcontainer/AppContainerModel.ts +1 -2
- package/appcontainer/ExceptionDialogModel.ts +2 -1
- package/build/types/appcontainer/ExceptionDialogModel.d.ts +2 -1
- package/build/types/cmp/grid/Grid.d.ts +3 -3
- package/build/types/core/{exception/ExceptionHandler.d.ts → ExceptionHandler.d.ts} +2 -1
- package/build/types/core/XH.d.ts +3 -2
- package/build/types/core/index.d.ts +1 -3
- package/build/types/core/types/Types.d.ts +0 -10
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +5 -5
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +5 -5
- package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
- package/build/types/exception/Exception.d.ts +24 -0
- package/build/types/{core/exception → exception}/Types.d.ts +1 -18
- package/build/types/exception/index.d.ts +2 -0
- package/build/types/kit/swiper/index.d.ts +1 -1
- package/build/types/svc/FetchService.d.ts +55 -0
- package/build/types/utils/js/LangUtils.d.ts +1 -27
- package/build/types/utils/{log → js}/LogUtils.d.ts +37 -1
- package/build/types/utils/js/index.d.ts +2 -0
- package/cmp/ag-grid/AgGrid.ts +1 -1
- package/cmp/chart/Chart.ts +1 -2
- package/cmp/chart/impl/ChartContextMenuItems.ts +1 -1
- package/cmp/grid/columns/Column.ts +1 -2
- package/cmp/grid/helpers/GridCountLabel.ts +1 -2
- package/cmp/grid/impl/ColumnWidthCalculator.ts +1 -2
- package/cmp/grid/impl/Utils.ts +1 -1
- package/cmp/relativetimestamp/RelativeTimestamp.ts +1 -2
- package/cmp/treemap/TreeMap.ts +1 -2
- package/core/{exception/ExceptionHandler.ts → ExceptionHandler.ts} +3 -4
- package/core/HoistBase.ts +10 -2
- package/core/HoistBaseDecorators.ts +1 -2
- package/core/HoistComponent.ts +1 -2
- package/core/XH.ts +3 -4
- package/core/index.ts +1 -3
- package/core/load/LoadSupport.ts +1 -2
- package/core/persist/PersistenceProvider.ts +1 -2
- package/core/types/Types.ts +0 -6
- package/data/filter/Utils.ts +1 -1
- package/data/impl/RecordSet.ts +1 -2
- package/desktop/cmp/button/grid/ColAutosizeButton.ts +1 -2
- package/desktop/cmp/button/grid/ColChooserButton.ts +1 -2
- package/desktop/cmp/button/grid/ExpandToLevelButton.ts +1 -2
- package/desktop/cmp/button/grid/ExportButton.ts +1 -2
- package/desktop/cmp/button/panel/ModalToggleButton.ts +1 -2
- package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -2
- package/desktop/cmp/dash/container/impl/DashContainerUtils.ts +1 -2
- package/desktop/cmp/form/FormField.ts +1 -2
- package/desktop/cmp/grid/editors/BooleanEditor.ts +1 -1
- package/desktop/cmp/panel/Panel.ts +1 -1
- package/desktop/hooks/UseContextMenu.ts +1 -1
- package/exception/Exception.ts +81 -0
- package/{core/exception → exception}/Types.ts +1 -22
- package/{utils/log → exception}/index.ts +2 -2
- package/kit/ag-grid/index.ts +1 -2
- package/kit/highcharts/index.ts +1 -2
- package/mobile/cmp/button/grid/ColAutosizeButton.ts +1 -2
- package/mobile/cmp/button/grid/ColChooserButton.ts +1 -2
- package/mobile/cmp/button/grid/ExpandCollapseButton.ts +1 -2
- package/mobile/cmp/button/grid/ExpandToLevelButton.ts +1 -2
- package/mobile/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -2
- package/mobile/cmp/panel/Panel.ts +1 -1
- package/mobx/overrides.ts +1 -1
- package/package.json +1 -1
- package/promise/Promise.ts +3 -3
- package/security/BaseOAuthClient.ts +3 -3
- package/security/msal/MsalClient.ts +1 -2
- package/svc/ChangelogService.ts +1 -1
- package/svc/EnvironmentService.ts +1 -2
- package/svc/FetchService.ts +208 -16
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/async/Timer.ts +1 -2
- package/utils/js/Decorators.ts +4 -4
- package/utils/js/LangUtils.ts +3 -67
- package/utils/{log → js}/LogUtils.ts +78 -19
- package/utils/js/index.ts +2 -0
- package/build/types/core/exception/Exception.d.ts +0 -61
- package/build/types/utils/log/index.d.ts +0 -1
- package/build/types/utils/version/index.d.ts +0 -1
- package/core/exception/Exception.ts +0 -256
- package/utils/version/index.ts +0 -8
- /package/build/types/utils/{version → js}/VersionUtils.d.ts +0 -0
- /package/utils/{version → js}/VersionUtils.ts +0 -0
package/svc/FetchService.ts
CHANGED
|
@@ -4,20 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
Exception,
|
|
10
|
-
HoistService,
|
|
11
|
-
LoadSpec,
|
|
12
|
-
PlainObject,
|
|
13
|
-
TrackOptions,
|
|
14
|
-
XH
|
|
15
|
-
} from '@xh/hoist/core';
|
|
7
|
+
import {Awaitable, HoistService, LoadSpec, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
|
|
8
|
+
import {Exception, HoistException, TimeoutException} from '@xh/hoist/exception';
|
|
16
9
|
import {PromiseTimeoutSpec} from '@xh/hoist/promise';
|
|
17
10
|
import {isLocalDate, SECONDS} from '@xh/hoist/utils/datetime';
|
|
18
11
|
import {warnIf} from '@xh/hoist/utils/js';
|
|
19
12
|
import {StatusCodes} from 'http-status-codes';
|
|
20
|
-
import {isDate, isFunction, isNil, isObject, isString, omit, omitBy} from 'lodash';
|
|
13
|
+
import {isDate, isFunction, isNil, isObject, isString, omit, omitBy, truncate} from 'lodash';
|
|
21
14
|
import {IStringifyOptions, stringify} from 'qs';
|
|
22
15
|
import ShortUniqueId from 'short-unique-id';
|
|
23
16
|
|
|
@@ -198,7 +191,7 @@ export class FetchService extends HoistService {
|
|
|
198
191
|
// Apply tracking
|
|
199
192
|
const {correlationId, loadSpec, track} = opts;
|
|
200
193
|
if (track) {
|
|
201
|
-
const trackOptions = isString(track) ? {message: track} : track;
|
|
194
|
+
const trackOptions: TrackOptions = isString(track) ? {message: track} : track;
|
|
202
195
|
warnIf(
|
|
203
196
|
trackOptions.correlationId || trackOptions.loadSpec,
|
|
204
197
|
'Neither Correlation ID nor LoadSpec should be set in `FetchOptions.track`. Use `FetchOptions` top-level properties instead.'
|
|
@@ -296,14 +289,14 @@ export class FetchService extends HoistService {
|
|
|
296
289
|
? timeout.message
|
|
297
290
|
: // Exception.timeout() message already includes interval - add URL here.
|
|
298
291
|
e.message + ` loading '${opts.url}'`;
|
|
299
|
-
throw
|
|
292
|
+
throw this.timeoutException(opts, e, msg);
|
|
300
293
|
}
|
|
301
294
|
|
|
302
295
|
if (!e.isHoistException) {
|
|
303
296
|
// Just two other cases where we expect this to *throw* -- Typically we get a fail status
|
|
304
297
|
throw e.name === 'AbortError'
|
|
305
|
-
?
|
|
306
|
-
:
|
|
298
|
+
? this.abortedException(opts, e)
|
|
299
|
+
: this.serverUnavailableException(opts, e);
|
|
307
300
|
}
|
|
308
301
|
throw e;
|
|
309
302
|
} finally {
|
|
@@ -359,7 +352,8 @@ export class FetchService extends HoistService {
|
|
|
359
352
|
// 4) Await underlying fetch and post-process response.
|
|
360
353
|
const ret = await fetch(url, fetchOpts);
|
|
361
354
|
|
|
362
|
-
if (!ret.ok)
|
|
355
|
+
if (!ret.ok)
|
|
356
|
+
throw this.exceptionFromResponse(opts, ret, await this.safeResponseTextAsync(ret));
|
|
363
357
|
|
|
364
358
|
return ret;
|
|
365
359
|
}
|
|
@@ -367,7 +361,7 @@ export class FetchService extends HoistService {
|
|
|
367
361
|
private async parseJsonAsync(opts: FetchOptions, r: Response): Promise<any> {
|
|
368
362
|
if (this.NO_JSON_RESPONSES.includes(r.status)) return null;
|
|
369
363
|
return r.json().catchWhen('SyntaxError', e => {
|
|
370
|
-
throw
|
|
364
|
+
throw this.jsonParseException(opts, e);
|
|
371
365
|
});
|
|
372
366
|
}
|
|
373
367
|
|
|
@@ -384,6 +378,184 @@ export class FetchService extends HoistService {
|
|
|
384
378
|
if (isLocalDate(value)) return value.isoString;
|
|
385
379
|
return value;
|
|
386
380
|
};
|
|
381
|
+
|
|
382
|
+
//---------------------
|
|
383
|
+
// Exception Handling
|
|
384
|
+
//--------------------
|
|
385
|
+
/**
|
|
386
|
+
* Create an Error to throw when a fetch call returns a !ok response.
|
|
387
|
+
* @param fetchOptions - original options passed to FetchService.
|
|
388
|
+
* @param response - return value of native fetch.
|
|
389
|
+
* @param responseText - optional additional details from the server.
|
|
390
|
+
*/
|
|
391
|
+
private exceptionFromResponse(
|
|
392
|
+
fetchOptions: FetchOptions,
|
|
393
|
+
response: Response,
|
|
394
|
+
responseText: string = null
|
|
395
|
+
): FetchException {
|
|
396
|
+
const {headers, status, statusText} = response,
|
|
397
|
+
defaults = {
|
|
398
|
+
name: 'HTTP Error ' + (status || ''),
|
|
399
|
+
message: statusText,
|
|
400
|
+
httpStatus: status,
|
|
401
|
+
serverDetails: responseText,
|
|
402
|
+
fetchOptions
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (status === 401) {
|
|
406
|
+
return this.createException({
|
|
407
|
+
...defaults,
|
|
408
|
+
name: 'Unauthorized',
|
|
409
|
+
message: 'Your session may have timed out and you may need to log in again.'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Try to "smart" decode as server provided JSON Exception (with a name)
|
|
414
|
+
try {
|
|
415
|
+
const cType = headers.get('Content-Type');
|
|
416
|
+
if (cType?.includes('application/json')) {
|
|
417
|
+
const parsedResp = this.safeParseJson(responseText);
|
|
418
|
+
return this.createException({
|
|
419
|
+
...defaults,
|
|
420
|
+
name: parsedResp?.name ?? defaults.name,
|
|
421
|
+
message: this.extractMessage(parsedResp, responseText, statusText),
|
|
422
|
+
isRoutine: parsedResp?.isRoutine ?? false,
|
|
423
|
+
serverDetails: parsedResp ?? responseText
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
} catch (ignored) {}
|
|
427
|
+
|
|
428
|
+
// Fall back to raw defaults
|
|
429
|
+
return this.createException(defaults);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Create an Error to throw when a fetchJson call encounters a SyntaxError.
|
|
434
|
+
* @param fetchOptions - original options passed to FetchService.
|
|
435
|
+
* @param cause - object thrown by native {@link response.json}.
|
|
436
|
+
*/
|
|
437
|
+
private jsonParseException(fetchOptions: FetchOptions, cause: any): FetchException {
|
|
438
|
+
return this.createException({
|
|
439
|
+
name: 'JSON Parsing Error',
|
|
440
|
+
message:
|
|
441
|
+
'Error parsing the response body as JSON. The server may have returned an invalid ' +
|
|
442
|
+
'or empty response. Use "XH.fetch()" to process the response manually.',
|
|
443
|
+
fetchOptions,
|
|
444
|
+
cause
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Create an Error to throw when a fetch call is aborted.
|
|
450
|
+
* @param fetchOptions - original options passed to FetchService.
|
|
451
|
+
* @param cause - object thrown by native fetch
|
|
452
|
+
*/
|
|
453
|
+
private abortedException(fetchOptions: FetchOptions, cause: any): FetchException {
|
|
454
|
+
return this.createException({
|
|
455
|
+
name: 'Fetch Aborted',
|
|
456
|
+
message: `Fetch request aborted, url: "${fetchOptions.url}"`,
|
|
457
|
+
isRoutine: true,
|
|
458
|
+
isFetchAborted: true,
|
|
459
|
+
fetchOptions,
|
|
460
|
+
cause
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Create an Error to throw when a fetch call times out.
|
|
466
|
+
* @param fetchOptions - original options the app passed when calling FetchService.
|
|
467
|
+
* @param cause - underlying timeout exception
|
|
468
|
+
* @param message - optional custom message
|
|
469
|
+
*
|
|
470
|
+
* @returns an exception that is both a TimeoutException, and a FetchException, with the
|
|
471
|
+
* underlying TimeoutException as its cause.
|
|
472
|
+
*/
|
|
473
|
+
private timeoutException(
|
|
474
|
+
fetchOptions: FetchOptions,
|
|
475
|
+
cause: TimeoutException,
|
|
476
|
+
message: string
|
|
477
|
+
): FetchException & TimeoutException {
|
|
478
|
+
return this.createException({
|
|
479
|
+
name: 'Fetch Timeout',
|
|
480
|
+
message,
|
|
481
|
+
isFetchTimeout: true,
|
|
482
|
+
isTimeout: true,
|
|
483
|
+
interval: cause.interval,
|
|
484
|
+
fetchOptions,
|
|
485
|
+
cause
|
|
486
|
+
}) as FetchException & TimeoutException;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Create an Error for when the server called by fetch does not respond
|
|
491
|
+
* @param fetchOptions - original options the app passed to FetchService.fetch
|
|
492
|
+
* @param cause - object thrown by native fetch
|
|
493
|
+
*/
|
|
494
|
+
private serverUnavailableException(fetchOptions: FetchOptions, cause: any): FetchException {
|
|
495
|
+
const protocolPattern = /^[a-z]+:\/\//i,
|
|
496
|
+
originPattern = /^[a-z]+:\/\/[^/]+/i,
|
|
497
|
+
match = fetchOptions.url.match(originPattern),
|
|
498
|
+
origin = match
|
|
499
|
+
? match[0]
|
|
500
|
+
: protocolPattern.test(XH.baseUrl)
|
|
501
|
+
? XH.baseUrl
|
|
502
|
+
: window.location.origin;
|
|
503
|
+
|
|
504
|
+
return this.createException({
|
|
505
|
+
name: 'Server Unavailable',
|
|
506
|
+
message: `Unable to contact the server at ${origin}`,
|
|
507
|
+
isServerUnavailable: true,
|
|
508
|
+
fetchOptions,
|
|
509
|
+
cause
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private createException(attributes: PlainObject) {
|
|
514
|
+
let correlationId: string = null;
|
|
515
|
+
const correlationIdHeaderKey = XH?.fetchService?.correlationIdHeaderKey;
|
|
516
|
+
if (correlationIdHeaderKey) {
|
|
517
|
+
correlationId = attributes.fetchOptions?.headers?.[correlationIdHeaderKey];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return Exception.create({
|
|
521
|
+
isFetchAborted: false,
|
|
522
|
+
httpStatus: 0, // native fetch doesn't put status on its Error
|
|
523
|
+
serverDetails: null,
|
|
524
|
+
stack: null, // server-sourced exceptions do not include, neither should client, not relevant
|
|
525
|
+
correlationId,
|
|
526
|
+
...attributes
|
|
527
|
+
}) as FetchException;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private safeParseJson(txt: string): PlainObject {
|
|
531
|
+
try {
|
|
532
|
+
return JSON.parse(txt);
|
|
533
|
+
} catch (ignored) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private extractMessage(
|
|
539
|
+
parsedResp: PlainObject,
|
|
540
|
+
responseText: string,
|
|
541
|
+
statusText: string
|
|
542
|
+
): string {
|
|
543
|
+
let ret: string;
|
|
544
|
+
if (parsedResp) {
|
|
545
|
+
// From parsed response, including cause if provided (e.g. ExternalHttpException)
|
|
546
|
+
ret = parsedResp.message;
|
|
547
|
+
if (isString(parsedResp.cause)) {
|
|
548
|
+
const cause = truncate(parsedResp.cause, {length: 255});
|
|
549
|
+
ret = ret ? `${ret} (Caused by: ${cause})` : cause;
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
// Use raw text if not JSON parseable
|
|
553
|
+
ret = truncate(responseText?.trim(), {length: 255});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Fallback to statusText if we have nothing else.
|
|
557
|
+
return ret || statusText;
|
|
558
|
+
}
|
|
387
559
|
}
|
|
388
560
|
|
|
389
561
|
/** Headers to be applied to all requests. Specified as object, or dynamic function to create. */
|
|
@@ -473,3 +645,23 @@ export interface FetchOptions {
|
|
|
473
645
|
*/
|
|
474
646
|
track?: string | TrackOptions;
|
|
475
647
|
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Exception thrown to indicate an HTTP error resulting from a call to FetchService.
|
|
651
|
+
*/
|
|
652
|
+
export interface FetchException extends HoistException {
|
|
653
|
+
/** Http Status code associated with exception. 0 if no response received. */
|
|
654
|
+
httpStatus: number;
|
|
655
|
+
|
|
656
|
+
/** Rich object or string containing details about the exception as sent by server. */
|
|
657
|
+
serverDetails: string | PlainObject;
|
|
658
|
+
|
|
659
|
+
/** Options of underlying fetch call. */
|
|
660
|
+
fetchOptions: FetchOptions;
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* True if exception resulted from the fetch being aborted by fetchService, or the application.
|
|
664
|
+
* @see FetchService.abort and FetchOptions.autoAbortKey.
|
|
665
|
+
*/
|
|
666
|
+
isFetchAborted: boolean;
|
|
667
|
+
}
|