@sdcorejs/angular 20.1.3 → 20.1.4
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/README.md +17 -14
- package/components/audit-diff/index.d.ts +74 -0
- package/components/badge/index.d.ts +1 -1
- package/components/breadcrumb/index.d.ts +53 -0
- package/components/data-state/index.d.ts +44 -0
- package/components/document-builder/index.d.ts +5 -5
- package/components/form-generic/index.d.ts +67 -104
- package/components/import-excel/index.d.ts +4 -14
- package/components/index.d.ts +4 -0
- package/components/job-progress/index.d.ts +38 -0
- package/components/mini-editor/index.d.ts +9 -9
- package/components/modal/index.d.ts +7 -1
- package/components/preview/index.d.ts +181 -28
- package/components/side-drawer/index.d.ts +7 -1
- package/components/tab/index.d.ts +9 -2
- package/components/table/index.d.ts +8 -7
- package/components/tree/index.d.ts +24 -2
- package/components/upload-file/index.d.ts +5 -4
- package/directives/index.d.ts +10 -8
- package/fesm2022/sdcorejs-angular-components-anchor.mjs +14 -14
- package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-audit-diff.mjs +332 -0
- package/fesm2022/sdcorejs-angular-components-audit-diff.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-components-badge.mjs +1 -0
- package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-breadcrumb.mjs +121 -0
- package/fesm2022/sdcorejs-angular-components-breadcrumb.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-components-data-state.mjs +68 -0
- package/fesm2022/sdcorejs-angular-components-data-state.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs +18 -23
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-editor.mjs +6 -4
- package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs +495 -838
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs +24 -34
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-job-progress.mjs +97 -0
- package/fesm2022/sdcorejs-angular-components-job-progress.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-components-mini-editor.mjs +10 -20
- package/fesm2022/sdcorejs-angular-components-mini-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal.mjs +61 -4
- package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-preview.mjs +1438 -218
- package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs +4 -4
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs +1 -1
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +39 -2
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs +16 -22
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab.mjs +48 -4
- package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-table.mjs +96 -63
- package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tree.mjs +186 -17
- package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs +15 -22
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components.mjs +4 -0
- package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-directives.mjs +29 -41
- package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +7 -7
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +20 -83
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +6 -3
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip.mjs +6 -3
- package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs +16 -13
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date.mjs +42 -65
- package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs +51 -66
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs +384 -0
- package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs +7 -5
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input.mjs +290 -56
- package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-models.mjs +155 -2
- package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-radio.mjs +44 -78
- package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-select.mjs +6 -20
- package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-switch.mjs +19 -76
- package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs +6 -5
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-time-range.mjs +280 -0
- package/fesm2022/sdcorejs-angular-forms-time-range.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-forms-time.mjs +310 -0
- package/fesm2022/sdcorejs-angular-forms-time.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-forms-tree-select.mjs +210 -0
- package/fesm2022/sdcorejs-angular-forms-tree-select.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-forms.mjs +73 -4
- package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-i18n.mjs +355 -10
- package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-interceptors.mjs +12 -17
- package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-layout.mjs +861 -80
- package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-pipes.mjs +5 -7
- package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-api.mjs +407 -137
- package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-cache.mjs +515 -105
- package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-confirm.mjs +5 -7
- package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-docx.mjs +7 -10
- package/fesm2022/sdcorejs-angular-services-docx.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-loading.mjs +339 -74
- package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-notify.mjs +7 -10
- package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-persistence.mjs +899 -0
- package/fesm2022/sdcorejs-angular-services-persistence.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-services-storage.mjs +324 -122
- package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-task.mjs +389 -0
- package/fesm2022/sdcorejs-angular-services-task.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-services-unsaved-changes.mjs +255 -0
- package/fesm2022/sdcorejs-angular-services-unsaved-changes.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-services-viewport.mjs +76 -0
- package/fesm2022/sdcorejs-angular-services-viewport.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-services.mjs +4 -0
- package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
- package/forms/autocomplete/index.d.ts +3 -4
- package/forms/checkbox/index.d.ts +4 -8
- package/forms/date/index.d.ts +2 -2
- package/forms/date-range/index.d.ts +6 -7
- package/forms/datetime/index.d.ts +2 -2
- package/forms/entity-picker/index.d.ts +128 -0
- package/forms/index.d.ts +15 -5
- package/forms/input/index.d.ts +74 -11
- package/forms/input-number/index.d.ts +1 -1
- package/forms/models/index.d.ts +86 -3
- package/forms/radio/index.d.ts +12 -8
- package/forms/switch/index.d.ts +4 -7
- package/forms/time/index.d.ts +117 -0
- package/forms/time-range/index.d.ts +98 -0
- package/forms/tree-select/index.d.ts +76 -0
- package/i18n/index.d.ts +68 -2
- package/interceptors/index.d.ts +5 -6
- package/modules/layout/index.d.ts +288 -15
- package/package.json +49 -1
- package/pipes/index.d.ts +2 -2
- package/services/api/index.d.ts +68 -24
- package/services/cache/index.d.ts +52 -16
- package/services/confirm/index.d.ts +3 -2
- package/services/docx/index.d.ts +2 -3
- package/services/index.d.ts +4 -0
- package/services/loading/index.d.ts +16 -8
- package/services/persistence/index.d.ts +159 -0
- package/services/storage/index.d.ts +26 -8
- package/services/task/index.d.ts +104 -0
- package/services/unsaved-changes/index.d.ts +83 -0
- package/services/viewport/index.d.ts +42 -0
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Injectable, NgModule } from '@angular/core';
|
|
3
|
-
import { HttpClient, HttpResponse, HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
|
2
|
+
import { InjectionToken, inject, DestroyRef, Injectable, NgModule } from '@angular/core';
|
|
3
|
+
import { HttpClient, HttpRequest, HttpResponse, HttpErrorResponse, HttpHeaders, HttpParams, HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
|
4
4
|
import { SdCacheService } from '@sdcorejs/angular/services/cache';
|
|
5
5
|
import { BrowserUtilities, Utilities } from '@sdcorejs/utils/fns';
|
|
6
|
-
import {
|
|
7
|
-
import { timeout,
|
|
6
|
+
import { Subscription, throwError, timer, of, TimeoutError, from, defer } from 'rxjs';
|
|
7
|
+
import { timeout, filter, map, retry, switchMap, materialize, concatMap, catchError, dematerialize } from 'rxjs/operators';
|
|
8
8
|
|
|
9
9
|
const SD_API_CONFIG = new InjectionToken('sd-api.configuration');
|
|
10
|
-
|
|
10
|
+
/** @deprecated Use `SD_API_CONFIG`. This is the same injection token. */
|
|
11
|
+
const SD_API_CONFIGURATION = SD_API_CONFIG;
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Response generics intentionally default to `unknown` for type safety.
|
|
15
|
+
* Callers should specify `T`; migration guidance is tracked for Task 13.
|
|
16
|
+
*/
|
|
12
17
|
class SdApiService {
|
|
13
|
-
#defaultTimeout = 60000;
|
|
14
|
-
#
|
|
18
|
+
#defaultTimeout = 60000;
|
|
19
|
+
#defaultDedupeWindowMs = 1000;
|
|
20
|
+
#maxTimerMs = 2_147_483_647;
|
|
21
|
+
#maxRetryAttempts = 10;
|
|
15
22
|
#httpClient = inject(HttpClient);
|
|
16
|
-
#configurations = inject(SD_API_CONFIG, { optional: true })
|
|
23
|
+
#configurations = inject(SD_API_CONFIG, { optional: true }) ?? [];
|
|
17
24
|
#cacheService = inject(SdCacheService);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
#
|
|
25
|
+
#destroyRef = inject(DestroyRef);
|
|
26
|
+
#sharedRequests = new Map();
|
|
27
|
+
#activeRequests = new Set();
|
|
28
|
+
#objectIds = new WeakMap();
|
|
29
|
+
#nextObjectId = 1;
|
|
30
|
+
#destroyed = false;
|
|
21
31
|
constructor() {
|
|
22
|
-
|
|
23
|
-
setInterval(() => this.#cleanupCache(), 60000);
|
|
32
|
+
this.#destroyRef.onDestroy(() => this.#destroy());
|
|
24
33
|
}
|
|
25
34
|
get http() {
|
|
26
35
|
return this.#httpClient;
|
|
27
36
|
}
|
|
28
|
-
// --- PUBLIC METHODS ---
|
|
29
37
|
get = (url, option) => {
|
|
30
38
|
return this.#executeWithLayeredCache(url, 'GET', undefined, option);
|
|
31
39
|
};
|
|
@@ -35,15 +43,18 @@ class SdApiService {
|
|
|
35
43
|
put = (url, body, option) => {
|
|
36
44
|
return this.#executeWithLayeredCache(url, 'PUT', body, option);
|
|
37
45
|
};
|
|
46
|
+
patch = (url, body, option) => {
|
|
47
|
+
return this.#executeWithLayeredCache(url, 'PATCH', body, option);
|
|
48
|
+
};
|
|
38
49
|
delete = (url, option) => {
|
|
39
50
|
return this.#executeWithLayeredCache(url, 'DELETE', undefined, option);
|
|
40
51
|
};
|
|
41
|
-
// Upload file logic giữ nguyên nhưng refactor nhẹ
|
|
42
52
|
upload = async (url, option) => {
|
|
43
53
|
const file = await BrowserUtilities.upload(option);
|
|
44
54
|
if (!Array.isArray(file) && file) {
|
|
45
55
|
return this.uploadFile(url, file);
|
|
46
56
|
}
|
|
57
|
+
return undefined;
|
|
47
58
|
};
|
|
48
59
|
uploadFile = async (url, file) => {
|
|
49
60
|
if (!file)
|
|
@@ -52,105 +63,367 @@ class SdApiService {
|
|
|
52
63
|
throw new Error('Invalid file extension');
|
|
53
64
|
const formData = new FormData();
|
|
54
65
|
formData.append('file', file, file.name);
|
|
55
|
-
|
|
56
|
-
return await this.post(url, formData, { autoCache: false });
|
|
66
|
+
return this.post(url, formData, { dedupe: false });
|
|
57
67
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
async #executeWithLayeredCache(url, method, body, option) {
|
|
69
|
+
if (this.#destroyed || option?.signal?.aborted) {
|
|
70
|
+
throw this.#createAbortError();
|
|
71
|
+
}
|
|
72
|
+
if (!option?.cacheOption) {
|
|
73
|
+
return this.#request(url, method, body, option);
|
|
74
|
+
}
|
|
75
|
+
const key = this.#generateKey(url, method, body, option);
|
|
76
|
+
const cache = this.#cacheService.create(key, option.cacheOption);
|
|
77
|
+
try {
|
|
78
|
+
const cached = cache.snapshot();
|
|
79
|
+
if (cached.present)
|
|
80
|
+
return cached.value;
|
|
70
81
|
const result = await this.#request(url, method, body, option);
|
|
71
|
-
set(result);
|
|
82
|
+
cache.set(result);
|
|
72
83
|
return result;
|
|
73
84
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
finally {
|
|
86
|
+
cache.release();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
#request(url, method, body, option) {
|
|
90
|
+
if (this.#destroyed || option?.signal?.aborted) {
|
|
91
|
+
return Promise.reject(this.#createAbortError());
|
|
92
|
+
}
|
|
93
|
+
const shouldDedupe = option?.dedupe ?? option?.autoCache ?? method === 'GET';
|
|
94
|
+
const key = shouldDedupe ? this.#generateRequestKey(url, method, body, option) : undefined;
|
|
95
|
+
let request = key ? this.#sharedRequests.get(key) : undefined;
|
|
96
|
+
if (!request) {
|
|
97
|
+
const prepared = this.#prepareRequest(url, method, body, option, key);
|
|
98
|
+
request = prepared.request;
|
|
99
|
+
if (key)
|
|
100
|
+
this.#sharedRequests.set(key, request);
|
|
101
|
+
this.#startRequest(request, prepared.source);
|
|
102
|
+
}
|
|
103
|
+
return this.#attachCaller(request, option?.signal);
|
|
104
|
+
}
|
|
105
|
+
#prepareRequest(url, method, body, option, key) {
|
|
106
|
+
let resolve;
|
|
107
|
+
let reject;
|
|
108
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
109
|
+
resolve = resolvePromise;
|
|
110
|
+
reject = rejectPromise;
|
|
111
|
+
});
|
|
112
|
+
const request = {
|
|
113
|
+
key,
|
|
114
|
+
replayWindowMs: this.#normalizeDedupeWindow(option?.dedupeWindowMs),
|
|
115
|
+
promise,
|
|
116
|
+
resolve,
|
|
117
|
+
reject,
|
|
118
|
+
subscription: Subscription.EMPTY,
|
|
119
|
+
owners: 0,
|
|
120
|
+
state: 'pending',
|
|
121
|
+
};
|
|
122
|
+
this.#activeRequests.add(request);
|
|
90
123
|
const handler = this.#getHandler(url);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
const httpRequest = new HttpRequest(method, url, body ?? null, {
|
|
125
|
+
headers: this.#toHttpHeaders(option?.headers),
|
|
126
|
+
context: option?.context,
|
|
127
|
+
params: this.#toHttpParams(option?.params),
|
|
128
|
+
reportProgress: option?.reportProgress,
|
|
129
|
+
responseType: option?.responseType ?? 'json',
|
|
130
|
+
withCredentials: option?.withCredentials,
|
|
131
|
+
transferCache: option?.transferCache,
|
|
132
|
+
});
|
|
133
|
+
const source = this.#withRetry(this.#httpClient.request(httpRequest).pipe(timeout(this.#resolveTimeout(url, option?.timeout)), filter((event) => event instanceof HttpResponse), map(response => this.#mapResponse(response.body, handler))), method, option?.retry);
|
|
134
|
+
return { request, source };
|
|
135
|
+
}
|
|
136
|
+
#startRequest(request, source) {
|
|
137
|
+
request.subscription = source.subscribe({
|
|
138
|
+
next: value => this.#completeSuccess(request, value),
|
|
139
|
+
error: error => this.#completeError(request, error),
|
|
140
|
+
complete: () => {
|
|
141
|
+
if (request.state === 'pending') {
|
|
142
|
+
this.#completeError(request, new Error('HTTP request completed without a response'));
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
#attachCaller(request, signal) {
|
|
148
|
+
request.owners++;
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
let settled = false;
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
if (signal)
|
|
153
|
+
signal.removeEventListener('abort', abort);
|
|
154
|
+
};
|
|
155
|
+
const release = () => {
|
|
156
|
+
request.owners = Math.max(0, request.owners - 1);
|
|
157
|
+
if (request.state === 'pending' && request.owners === 0) {
|
|
158
|
+
this.#cancelRequest(request);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const abort = () => {
|
|
162
|
+
if (settled)
|
|
163
|
+
return;
|
|
164
|
+
settled = true;
|
|
165
|
+
cleanup();
|
|
166
|
+
release();
|
|
167
|
+
reject(this.#createAbortError());
|
|
168
|
+
};
|
|
169
|
+
if (signal) {
|
|
170
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
171
|
+
if (signal.aborted) {
|
|
172
|
+
abort();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
110
175
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
176
|
+
request.promise.then(value => {
|
|
177
|
+
if (settled)
|
|
178
|
+
return;
|
|
179
|
+
settled = true;
|
|
180
|
+
cleanup();
|
|
181
|
+
release();
|
|
182
|
+
resolve(value);
|
|
183
|
+
}, error => {
|
|
184
|
+
if (settled)
|
|
185
|
+
return;
|
|
186
|
+
settled = true;
|
|
187
|
+
cleanup();
|
|
188
|
+
release();
|
|
189
|
+
reject(error);
|
|
190
|
+
});
|
|
123
191
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
192
|
+
}
|
|
193
|
+
#completeSuccess(request, value) {
|
|
194
|
+
if (request.state !== 'pending')
|
|
195
|
+
return;
|
|
196
|
+
request.state = 'succeeded';
|
|
197
|
+
request.resolve(value);
|
|
198
|
+
if (!request.key) {
|
|
199
|
+
this.#activeRequests.delete(request);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (request.replayWindowMs === 0) {
|
|
203
|
+
this.#evictRequest(request);
|
|
204
|
+
return;
|
|
136
205
|
}
|
|
206
|
+
request.replayTimer = setTimeout(() => this.#evictRequest(request), request.replayWindowMs);
|
|
207
|
+
}
|
|
208
|
+
#completeError(request, error) {
|
|
209
|
+
if (request.state !== 'pending')
|
|
210
|
+
return;
|
|
211
|
+
request.state = 'failed';
|
|
212
|
+
this.#removeRequest(request);
|
|
213
|
+
request.reject(error);
|
|
214
|
+
}
|
|
215
|
+
#cancelRequest(request) {
|
|
216
|
+
if (request.state !== 'pending')
|
|
217
|
+
return;
|
|
218
|
+
request.state = 'cancelled';
|
|
219
|
+
request.subscription.unsubscribe();
|
|
220
|
+
this.#removeRequest(request);
|
|
221
|
+
request.reject(this.#createAbortError());
|
|
222
|
+
}
|
|
223
|
+
#evictRequest(request) {
|
|
224
|
+
if (request.replayTimer !== undefined) {
|
|
225
|
+
clearTimeout(request.replayTimer);
|
|
226
|
+
request.replayTimer = undefined;
|
|
227
|
+
}
|
|
228
|
+
this.#removeRequest(request);
|
|
229
|
+
}
|
|
230
|
+
#removeRequest(request) {
|
|
231
|
+
if (request.key && this.#sharedRequests.get(request.key) === request) {
|
|
232
|
+
this.#sharedRequests.delete(request.key);
|
|
233
|
+
}
|
|
234
|
+
this.#activeRequests.delete(request);
|
|
235
|
+
}
|
|
236
|
+
#withRetry(source, method, option) {
|
|
237
|
+
const attempts = this.#normalizeRetryAttempts(option?.attempts);
|
|
238
|
+
const mutation = method !== 'GET';
|
|
239
|
+
if (attempts === 0 || (mutation && option?.mutations !== true)) {
|
|
240
|
+
return source;
|
|
241
|
+
}
|
|
242
|
+
return source.pipe(retry({
|
|
243
|
+
count: attempts,
|
|
244
|
+
delay: (error, attempt) => {
|
|
245
|
+
if (this.#isAbortError(error) || !this.#shouldRetry(error, attempt, option)) {
|
|
246
|
+
return throwError(() => error);
|
|
247
|
+
}
|
|
248
|
+
const baseDelay = this.#normalizeRetryDelay(option?.delayMs);
|
|
249
|
+
const backoff = this.#normalizeRetryBackoff(option?.backoff);
|
|
250
|
+
const computedDelay = baseDelay * Math.pow(backoff, attempt - 1);
|
|
251
|
+
const delayMs = Number.isFinite(computedDelay) ? Math.min(this.#maxTimerMs, computedDelay) : this.#maxTimerMs;
|
|
252
|
+
return delayMs > 0 ? timer(delayMs) : of(undefined);
|
|
253
|
+
},
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
#shouldRetry(error, attempt, option) {
|
|
257
|
+
if (option?.retryWhen) {
|
|
258
|
+
return option.retryWhen(error, attempt);
|
|
259
|
+
}
|
|
260
|
+
if (error instanceof TimeoutError)
|
|
261
|
+
return true;
|
|
262
|
+
if (!(error instanceof HttpErrorResponse))
|
|
263
|
+
return false;
|
|
264
|
+
return error.status === 0 || [408, 425, 429, 500, 502, 503, 504].includes(error.status);
|
|
265
|
+
}
|
|
266
|
+
#mapResponse(body, handler) {
|
|
267
|
+
if (body !== null && typeof body === 'object' && 'ok' in body && body.ok === false) {
|
|
268
|
+
throw body;
|
|
269
|
+
}
|
|
270
|
+
return handler?.mapResponse ? handler.mapResponse(body) : body;
|
|
271
|
+
}
|
|
272
|
+
#getHandler(url) {
|
|
273
|
+
const handlers = this.#configurations.flatMap(configuration => configuration.handlers ?? []);
|
|
274
|
+
return handlers.find(handler => handler.hosts.some(host => url.startsWith(host)));
|
|
275
|
+
}
|
|
276
|
+
#generateKey(url, method, body, option) {
|
|
137
277
|
return Utilities.hash({
|
|
138
|
-
url,
|
|
139
278
|
method,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
279
|
+
url,
|
|
280
|
+
body: this.#normalizeBody(body),
|
|
281
|
+
headers: this.#normalizeHeaders(option?.headers),
|
|
282
|
+
params: this.#normalizeParams(option?.params),
|
|
283
|
+
context: option?.context ? this.#getObjectId(option.context) : undefined,
|
|
284
|
+
reportProgress: option?.reportProgress ?? false,
|
|
285
|
+
responseType: option?.responseType ?? 'json',
|
|
286
|
+
withCredentials: option?.withCredentials ?? false,
|
|
287
|
+
transferCache: option?.transferCache,
|
|
288
|
+
timeout: this.#resolveTimeout(url, option?.timeout),
|
|
289
|
+
retry: option?.retry
|
|
290
|
+
? {
|
|
291
|
+
attempts: this.#normalizeRetryAttempts(option.retry.attempts),
|
|
292
|
+
delayMs: this.#normalizeRetryDelay(option.retry.delayMs),
|
|
293
|
+
backoff: this.#normalizeRetryBackoff(option.retry.backoff),
|
|
294
|
+
mutations: option.retry.mutations,
|
|
295
|
+
retryWhen: option.retry.retryWhen ? this.#getObjectId(option.retry.retryWhen) : undefined,
|
|
296
|
+
}
|
|
297
|
+
: undefined,
|
|
143
298
|
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (value.expiry < now) {
|
|
150
|
-
this.#inFlightRequests.delete(key);
|
|
151
|
-
}
|
|
299
|
+
}
|
|
300
|
+
#generateRequestKey(url, method, body, option) {
|
|
301
|
+
return Utilities.hash({
|
|
302
|
+
responseKey: this.#generateKey(url, method, body, option),
|
|
303
|
+
dedupeWindowMs: this.#normalizeDedupeWindow(option?.dedupeWindowMs),
|
|
152
304
|
});
|
|
153
305
|
}
|
|
306
|
+
#normalizeDedupeWindow(dedupeWindowMs) {
|
|
307
|
+
return this.#normalizeFiniteNumber(dedupeWindowMs, this.#defaultDedupeWindowMs, 0, this.#maxTimerMs);
|
|
308
|
+
}
|
|
309
|
+
#resolveTimeout(url, timeoutMs) {
|
|
310
|
+
const configuredTimeout = timeoutMs === undefined ? this.#getHandler(url)?.timeout : timeoutMs;
|
|
311
|
+
return this.#normalizeFiniteNumber(configuredTimeout, this.#defaultTimeout, 0, this.#maxTimerMs);
|
|
312
|
+
}
|
|
313
|
+
#normalizeRetryAttempts(attempts) {
|
|
314
|
+
return Math.floor(this.#normalizeFiniteNumber(attempts, 0, 0, this.#maxRetryAttempts));
|
|
315
|
+
}
|
|
316
|
+
#normalizeRetryDelay(delayMs) {
|
|
317
|
+
return this.#normalizeFiniteNumber(delayMs, 0, 0, this.#maxTimerMs);
|
|
318
|
+
}
|
|
319
|
+
#normalizeRetryBackoff(backoff) {
|
|
320
|
+
const normalized = this.#normalizeFiniteNumber(backoff, 1, 0, this.#maxTimerMs);
|
|
321
|
+
return normalized > 0 ? normalized : 1;
|
|
322
|
+
}
|
|
323
|
+
#normalizeFiniteNumber(value, defaultValue, minimum, maximum) {
|
|
324
|
+
if (value === undefined || !Number.isFinite(value))
|
|
325
|
+
return defaultValue;
|
|
326
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
327
|
+
}
|
|
328
|
+
#normalizeBody(body, ancestors = new WeakSet()) {
|
|
329
|
+
if (body === null || typeof body !== 'object')
|
|
330
|
+
return body;
|
|
331
|
+
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
332
|
+
return { urlSearchParams: body.toString() };
|
|
333
|
+
}
|
|
334
|
+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
335
|
+
return { objectType: 'FormData', objectId: this.#getObjectId(body) };
|
|
336
|
+
}
|
|
337
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
338
|
+
return { objectType: 'Blob', objectId: this.#getObjectId(body) };
|
|
339
|
+
}
|
|
340
|
+
if (typeof ArrayBuffer !== 'undefined' && (body instanceof ArrayBuffer || ArrayBuffer.isView(body))) {
|
|
341
|
+
return { objectType: 'ArrayBuffer', objectId: this.#getObjectId(body) };
|
|
342
|
+
}
|
|
343
|
+
if (body instanceof Date) {
|
|
344
|
+
return { date: body.getTime() };
|
|
345
|
+
}
|
|
346
|
+
if (ancestors.has(body)) {
|
|
347
|
+
return { circularObjectId: this.#getObjectId(body) };
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(body)) {
|
|
350
|
+
ancestors.add(body);
|
|
351
|
+
const normalized = body.map(value => this.#normalizeBody(value, ancestors));
|
|
352
|
+
ancestors.delete(body);
|
|
353
|
+
return normalized;
|
|
354
|
+
}
|
|
355
|
+
const prototype = Object.getPrototypeOf(body);
|
|
356
|
+
if (prototype === Object.prototype || prototype === null) {
|
|
357
|
+
ancestors.add(body);
|
|
358
|
+
const normalized = Object.fromEntries(Object.entries(body).map(([key, value]) => [key, this.#normalizeBody(value, ancestors)]));
|
|
359
|
+
ancestors.delete(body);
|
|
360
|
+
return normalized;
|
|
361
|
+
}
|
|
362
|
+
return { objectType: prototype?.constructor?.name ?? 'Object', objectId: this.#getObjectId(body) };
|
|
363
|
+
}
|
|
364
|
+
#normalizeHeaders(headers) {
|
|
365
|
+
if (headers instanceof HttpHeaders) {
|
|
366
|
+
return headers
|
|
367
|
+
.keys()
|
|
368
|
+
.sort()
|
|
369
|
+
.map(key => [key.toLowerCase(), headers.getAll(key)]);
|
|
370
|
+
}
|
|
371
|
+
return headers;
|
|
372
|
+
}
|
|
373
|
+
#normalizeParams(params) {
|
|
374
|
+
if (params instanceof HttpParams) {
|
|
375
|
+
return params
|
|
376
|
+
.keys()
|
|
377
|
+
.sort()
|
|
378
|
+
.map(key => [key, params.getAll(key)]);
|
|
379
|
+
}
|
|
380
|
+
return params;
|
|
381
|
+
}
|
|
382
|
+
#toHttpHeaders(headers) {
|
|
383
|
+
if (!headers || headers instanceof HttpHeaders)
|
|
384
|
+
return headers;
|
|
385
|
+
return new HttpHeaders(headers);
|
|
386
|
+
}
|
|
387
|
+
#toHttpParams(params) {
|
|
388
|
+
if (!params || params instanceof HttpParams)
|
|
389
|
+
return params;
|
|
390
|
+
return new HttpParams({ fromObject: params });
|
|
391
|
+
}
|
|
392
|
+
#getObjectId(value) {
|
|
393
|
+
const existing = this.#objectIds.get(value);
|
|
394
|
+
if (existing !== undefined)
|
|
395
|
+
return existing;
|
|
396
|
+
const id = this.#nextObjectId++;
|
|
397
|
+
this.#objectIds.set(value, id);
|
|
398
|
+
return id;
|
|
399
|
+
}
|
|
400
|
+
#createAbortError() {
|
|
401
|
+
const error = new Error('The operation was aborted');
|
|
402
|
+
error.name = 'AbortError';
|
|
403
|
+
return error;
|
|
404
|
+
}
|
|
405
|
+
#isAbortError(error) {
|
|
406
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
407
|
+
}
|
|
408
|
+
#destroy() {
|
|
409
|
+
if (this.#destroyed)
|
|
410
|
+
return;
|
|
411
|
+
this.#destroyed = true;
|
|
412
|
+
for (const request of [...this.#activeRequests]) {
|
|
413
|
+
if (request.replayTimer !== undefined) {
|
|
414
|
+
clearTimeout(request.replayTimer);
|
|
415
|
+
request.replayTimer = undefined;
|
|
416
|
+
}
|
|
417
|
+
if (request.state === 'pending') {
|
|
418
|
+
request.state = 'cancelled';
|
|
419
|
+
request.subscription.unsubscribe();
|
|
420
|
+
request.reject(this.#createAbortError());
|
|
421
|
+
}
|
|
422
|
+
this.#removeRequest(request);
|
|
423
|
+
}
|
|
424
|
+
this.#sharedRequests.clear();
|
|
425
|
+
this.#activeRequests.clear();
|
|
426
|
+
}
|
|
154
427
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
155
428
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdApiService, providedIn: 'root' });
|
|
156
429
|
}
|
|
@@ -162,46 +435,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
162
435
|
}], ctorParameters: () => [] });
|
|
163
436
|
|
|
164
437
|
class SdHttpInterceptor {
|
|
165
|
-
#configurations = inject(SD_API_CONFIG, { optional: true })
|
|
438
|
+
#configurations = inject(SD_API_CONFIG, { optional: true }) ?? [];
|
|
166
439
|
intercept(request, next) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
request = request.clone(
|
|
176
|
-
}
|
|
177
|
-
if (request.body instanceof FormData) {
|
|
178
|
-
request = request.clone({
|
|
179
|
-
headers: request.headers.delete('Content-Type'),
|
|
180
|
-
});
|
|
440
|
+
if (!request.url)
|
|
441
|
+
throw new Error('Invalid URL');
|
|
442
|
+
const handler = this.#findHandler(request.url);
|
|
443
|
+
if (handler?.intercept) {
|
|
444
|
+
const interceptedRequest = handler.intercept(request);
|
|
445
|
+
request = interceptedRequest instanceof HttpRequest ? interceptedRequest : request.clone(interceptedRequest);
|
|
446
|
+
}
|
|
447
|
+
if (typeof FormData !== 'undefined' && request.body instanceof FormData) {
|
|
448
|
+
request = request.clone({ headers: request.headers.delete('Content-Type') });
|
|
181
449
|
}
|
|
182
|
-
const
|
|
183
|
-
const afterRemoteHandler = handler?.afterRemote;
|
|
184
|
-
const beforeRemote = beforeRemoteHandler?.(request);
|
|
450
|
+
const beforeRemote = handler?.beforeRemote?.(request);
|
|
185
451
|
if (beforeRemote instanceof Promise) {
|
|
186
|
-
return from(beforeRemote).pipe(switchMap(() =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
452
|
+
return from(beforeRemote).pipe(switchMap(() => this.#handleRemote(request, next, handler)));
|
|
453
|
+
}
|
|
454
|
+
return this.#handleRemote(request, next, handler);
|
|
455
|
+
}
|
|
456
|
+
#handleRemote(request, next, handler) {
|
|
457
|
+
return next.handle(request).pipe(materialize(), concatMap(notification => {
|
|
458
|
+
const afterValue = notification.kind === 'N' && notification.value instanceof HttpResponse
|
|
459
|
+
? notification.value
|
|
460
|
+
: notification.kind === 'E' && (notification.error instanceof Error || notification.error instanceof HttpErrorResponse)
|
|
461
|
+
? notification.error
|
|
462
|
+
: undefined;
|
|
463
|
+
if (!afterValue || !handler?.afterRemote) {
|
|
464
|
+
return of(notification);
|
|
199
465
|
}
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}));
|
|
466
|
+
return defer(() => {
|
|
467
|
+
const result = handler.afterRemote?.(afterValue);
|
|
468
|
+
return result instanceof Promise ? result : Promise.resolve();
|
|
469
|
+
}).pipe(map(() => notification), catchError(hookError => (notification.kind === 'E' ? of(notification) : throwError(() => hookError))));
|
|
470
|
+
}), dematerialize());
|
|
471
|
+
}
|
|
472
|
+
#findHandler(url) {
|
|
473
|
+
const handlers = this.#configurations.flatMap(configuration => configuration.handlers ?? []);
|
|
474
|
+
return handlers.find(handler => handler.hosts.some(host => url.startsWith(host)));
|
|
205
475
|
}
|
|
206
476
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdHttpInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
207
477
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdHttpInterceptor });
|