@sdcorejs/angular 21.1.2 → 21.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.
Files changed (177) hide show
  1. package/README.md +127 -807
  2. package/fesm2022/sdcorejs-angular-components-anchor.mjs +14 -14
  3. package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
  4. package/fesm2022/sdcorejs-angular-components-audit-diff.mjs +332 -0
  5. package/fesm2022/sdcorejs-angular-components-audit-diff.mjs.map +1 -0
  6. package/fesm2022/sdcorejs-angular-components-badge.mjs +1 -0
  7. package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
  8. package/fesm2022/sdcorejs-angular-components-breadcrumb.mjs +121 -0
  9. package/fesm2022/sdcorejs-angular-components-breadcrumb.mjs.map +1 -0
  10. package/fesm2022/sdcorejs-angular-components-data-state.mjs +68 -0
  11. package/fesm2022/sdcorejs-angular-components-data-state.mjs.map +1 -0
  12. package/fesm2022/sdcorejs-angular-components-document-builder.mjs +18 -23
  13. package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +1 -1
  14. package/fesm2022/sdcorejs-angular-components-editor.mjs +6 -4
  15. package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
  16. package/fesm2022/sdcorejs-angular-components-form-generic.mjs +633 -886
  17. package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
  18. package/fesm2022/sdcorejs-angular-components-import-excel.mjs +26 -41
  19. package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
  20. package/fesm2022/sdcorejs-angular-components-job-progress.mjs +97 -0
  21. package/fesm2022/sdcorejs-angular-components-job-progress.mjs.map +1 -0
  22. package/fesm2022/sdcorejs-angular-components-mini-editor.mjs +10 -20
  23. package/fesm2022/sdcorejs-angular-components-mini-editor.mjs.map +1 -1
  24. package/fesm2022/sdcorejs-angular-components-modal.mjs +61 -4
  25. package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
  26. package/fesm2022/sdcorejs-angular-components-preview.mjs +1438 -218
  27. package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
  28. package/fesm2022/sdcorejs-angular-components-query-bar.mjs +9 -17
  29. package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
  30. package/fesm2022/sdcorejs-angular-components-query-builder.mjs +1 -1
  31. package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
  32. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +39 -2
  33. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
  34. package/fesm2022/sdcorejs-angular-components-tab-router.mjs +76 -52
  35. package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
  36. package/fesm2022/sdcorejs-angular-components-tab.mjs +48 -4
  37. package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
  38. package/fesm2022/sdcorejs-angular-components-table.mjs +123 -89
  39. package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
  40. package/fesm2022/sdcorejs-angular-components-tree.mjs +188 -18
  41. package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
  42. package/fesm2022/sdcorejs-angular-components-upload-file.mjs +17 -23
  43. package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
  44. package/fesm2022/sdcorejs-angular-components.mjs +4 -0
  45. package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
  46. package/fesm2022/sdcorejs-angular-directives.mjs +29 -41
  47. package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
  48. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +9 -8
  49. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
  50. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +20 -71
  51. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
  52. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +8 -4
  53. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
  54. package/fesm2022/sdcorejs-angular-forms-chip.mjs +8 -4
  55. package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
  56. package/fesm2022/sdcorejs-angular-forms-date-range.mjs +18 -14
  57. package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
  58. package/fesm2022/sdcorejs-angular-forms-date.mjs +48 -70
  59. package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
  60. package/fesm2022/sdcorejs-angular-forms-datetime.mjs +62 -528
  61. package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
  62. package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs +384 -0
  63. package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs.map +1 -0
  64. package/fesm2022/sdcorejs-angular-forms-input-color.mjs +5 -3
  65. package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
  66. package/fesm2022/sdcorejs-angular-forms-input-number.mjs +16 -11
  67. package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
  68. package/fesm2022/sdcorejs-angular-forms-input.mjs +297 -59
  69. package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
  70. package/fesm2022/sdcorejs-angular-forms-models.mjs +155 -2
  71. package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
  72. package/fesm2022/sdcorejs-angular-forms-radio.mjs +44 -66
  73. package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
  74. package/fesm2022/sdcorejs-angular-forms-select.mjs +8 -21
  75. package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
  76. package/fesm2022/sdcorejs-angular-forms-switch.mjs +19 -64
  77. package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
  78. package/fesm2022/sdcorejs-angular-forms-textarea.mjs +8 -6
  79. package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
  80. package/fesm2022/sdcorejs-angular-forms-time-range.mjs +277 -0
  81. package/fesm2022/sdcorejs-angular-forms-time-range.mjs.map +1 -0
  82. package/fesm2022/sdcorejs-angular-forms-time.mjs +307 -0
  83. package/fesm2022/sdcorejs-angular-forms-time.mjs.map +1 -0
  84. package/fesm2022/sdcorejs-angular-forms-tree-select.mjs +210 -0
  85. package/fesm2022/sdcorejs-angular-forms-tree-select.mjs.map +1 -0
  86. package/fesm2022/sdcorejs-angular-forms.mjs +73 -4
  87. package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -1
  88. package/fesm2022/sdcorejs-angular-i18n.mjs +355 -10
  89. package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
  90. package/fesm2022/sdcorejs-angular-interceptors.mjs +12 -17
  91. package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
  92. package/fesm2022/sdcorejs-angular-modules-icon.mjs.map +1 -1
  93. package/fesm2022/sdcorejs-angular-modules-layout.mjs +861 -80
  94. package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
  95. package/fesm2022/sdcorejs-angular-modules.mjs +0 -1
  96. package/fesm2022/sdcorejs-angular-modules.mjs.map +1 -1
  97. package/fesm2022/sdcorejs-angular-pipes.mjs +5 -7
  98. package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
  99. package/fesm2022/sdcorejs-angular-services-api.mjs +407 -137
  100. package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
  101. package/fesm2022/sdcorejs-angular-services-cache.mjs +515 -105
  102. package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
  103. package/fesm2022/sdcorejs-angular-services-confirm.mjs +5 -7
  104. package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
  105. package/fesm2022/sdcorejs-angular-services-docx.mjs +7 -10
  106. package/fesm2022/sdcorejs-angular-services-docx.mjs.map +1 -1
  107. package/fesm2022/sdcorejs-angular-services-loading.mjs +340 -66
  108. package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
  109. package/fesm2022/sdcorejs-angular-services-notify.mjs +7 -10
  110. package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
  111. package/fesm2022/sdcorejs-angular-services-persistence.mjs +899 -0
  112. package/fesm2022/sdcorejs-angular-services-persistence.mjs.map +1 -0
  113. package/fesm2022/sdcorejs-angular-services-storage.mjs +324 -122
  114. package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
  115. package/fesm2022/sdcorejs-angular-services-task.mjs +389 -0
  116. package/fesm2022/sdcorejs-angular-services-task.mjs.map +1 -0
  117. package/fesm2022/sdcorejs-angular-services-unsaved-changes.mjs +255 -0
  118. package/fesm2022/sdcorejs-angular-services-unsaved-changes.mjs.map +1 -0
  119. package/fesm2022/sdcorejs-angular-services-viewport.mjs +76 -0
  120. package/fesm2022/sdcorejs-angular-services-viewport.mjs.map +1 -0
  121. package/fesm2022/sdcorejs-angular-services.mjs +4 -0
  122. package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
  123. package/package.json +50 -5
  124. package/types/sdcorejs-angular-components-audit-diff.d.ts +74 -0
  125. package/types/sdcorejs-angular-components-badge.d.ts +1 -1
  126. package/types/sdcorejs-angular-components-breadcrumb.d.ts +53 -0
  127. package/types/sdcorejs-angular-components-data-state.d.ts +44 -0
  128. package/types/sdcorejs-angular-components-document-builder.d.ts +5 -5
  129. package/types/sdcorejs-angular-components-form-generic.d.ts +89 -103
  130. package/types/sdcorejs-angular-components-import-excel.d.ts +4 -14
  131. package/types/sdcorejs-angular-components-job-progress.d.ts +38 -0
  132. package/types/sdcorejs-angular-components-mini-editor.d.ts +9 -9
  133. package/types/sdcorejs-angular-components-modal.d.ts +7 -1
  134. package/types/sdcorejs-angular-components-preview.d.ts +181 -28
  135. package/types/sdcorejs-angular-components-side-drawer.d.ts +7 -1
  136. package/types/sdcorejs-angular-components-tab.d.ts +9 -2
  137. package/types/sdcorejs-angular-components-table.d.ts +8 -7
  138. package/types/sdcorejs-angular-components-tree.d.ts +24 -2
  139. package/types/sdcorejs-angular-components-upload-file.d.ts +5 -4
  140. package/types/sdcorejs-angular-components.d.ts +4 -0
  141. package/types/sdcorejs-angular-directives.d.ts +10 -8
  142. package/types/sdcorejs-angular-forms-autocomplete.d.ts +3 -4
  143. package/types/sdcorejs-angular-forms-checkbox.d.ts +4 -8
  144. package/types/sdcorejs-angular-forms-date-range.d.ts +6 -7
  145. package/types/sdcorejs-angular-forms-date.d.ts +3 -3
  146. package/types/sdcorejs-angular-forms-datetime.d.ts +3 -3
  147. package/types/sdcorejs-angular-forms-entity-picker.d.ts +128 -0
  148. package/types/sdcorejs-angular-forms-input-color.d.ts +3 -1
  149. package/types/sdcorejs-angular-forms-input-number.d.ts +4 -2
  150. package/types/sdcorejs-angular-forms-input.d.ts +76 -11
  151. package/types/sdcorejs-angular-forms-models.d.ts +86 -3
  152. package/types/sdcorejs-angular-forms-radio.d.ts +12 -8
  153. package/types/sdcorejs-angular-forms-switch.d.ts +4 -7
  154. package/types/sdcorejs-angular-forms-time-range.d.ts +98 -0
  155. package/types/sdcorejs-angular-forms-time.d.ts +117 -0
  156. package/types/sdcorejs-angular-forms-tree-select.d.ts +76 -0
  157. package/types/sdcorejs-angular-forms.d.ts +15 -5
  158. package/types/sdcorejs-angular-i18n.d.ts +68 -2
  159. package/types/sdcorejs-angular-interceptors.d.ts +5 -6
  160. package/types/sdcorejs-angular-modules-icon.d.ts +2 -3
  161. package/types/sdcorejs-angular-modules-layout.d.ts +288 -15
  162. package/types/sdcorejs-angular-modules.d.ts +0 -1
  163. package/types/sdcorejs-angular-pipes.d.ts +2 -2
  164. package/types/sdcorejs-angular-services-api.d.ts +68 -24
  165. package/types/sdcorejs-angular-services-cache.d.ts +52 -16
  166. package/types/sdcorejs-angular-services-confirm.d.ts +3 -2
  167. package/types/sdcorejs-angular-services-docx.d.ts +2 -3
  168. package/types/sdcorejs-angular-services-loading.d.ts +16 -8
  169. package/types/sdcorejs-angular-services-persistence.d.ts +159 -0
  170. package/types/sdcorejs-angular-services-storage.d.ts +26 -8
  171. package/types/sdcorejs-angular-services-task.d.ts +104 -0
  172. package/types/sdcorejs-angular-services-unsaved-changes.d.ts +83 -0
  173. package/types/sdcorejs-angular-services-viewport.d.ts +42 -0
  174. package/types/sdcorejs-angular-services.d.ts +4 -0
  175. package/fesm2022/sdcorejs-angular-modules-authom.mjs +0 -360
  176. package/fesm2022/sdcorejs-angular-modules-authom.mjs.map +0 -1
  177. package/types/sdcorejs-angular-modules-authom.d.ts +0 -70
@@ -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 { lastValueFrom, from, throwError } from 'rxjs';
7
- import { timeout, map, catchError, shareReplay, switchMap } from 'rxjs/operators';
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
- const SD_API_CONFIGURATION = new InjectionToken('sd-api.configuration');
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; // 60s
14
- #dedupCacheDuration = 1000; // 1s (Deduplication cache duration)
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
- // Thay đổi cấu trúc Cache: Lưu Observable thay vì Subject phức tạp
19
- // Key: hash string -> Value: { stream$: Observable, expiry: number }
20
- #inFlightRequests = new Map();
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
- // Optional: Cơ chế dọn dẹp cache định kỳ (mỗi 1 phút dọn dẹp các key hết hạn)
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
- // Upload thường không cần deduplication cache, set autoCache: false
56
- return await this.post(url, formData, { autoCache: false });
66
+ return this.post(url, formData, { dedupe: false });
57
67
  };
58
- // --- PRIVATE CORE LOGIC ---
59
- /**
60
- * Hàm trung gian xử lý Layer Cache (SdCacheService) trước khi gọi API thực tế
61
- */
62
- #executeWithLayeredCache = async (url, method, body, option) => {
63
- // Layer 1: Persistent Cache (SdCacheService)
64
- if (option?.cacheOption) {
65
- const key = this.#generateKey(url, method, body, option);
66
- const { get, set, has } = this.#cacheService.create(key, option.cacheOption);
67
- if (has()) {
68
- return get();
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
- // Không có cache dài hạn thì gọi trực tiếp logic deduplication
75
- return this.#request(url, method, body, option);
76
- };
77
- /**
78
- * Core Request: Xử lý Deduplication, Timeout, Mapping Response
79
- */
80
- #request = (url, method, body, option) => {
81
- const key = this.#generateKey(url, method, body, option);
82
- // Layer 2: Deduplication Cache (In-Flight Request / Short-term cache)
83
- const now = Date.now();
84
- const cachedItem = this.#inFlightRequests.get(key);
85
- // Nếu đã có request đang chạy hoặc mới chạy xong trong vòng 1s -> Trả về stream đó luôn
86
- if (cachedItem && cachedItem.expiry > now) {
87
- return lastValueFrom(cachedItem.stream$);
88
- }
89
- // Setup request mới
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 apiTimeout = option?.timeout ?? handler?.timeout ?? this.#defaultTimeout;
92
- // Tạo Observable call API
93
- const request$ = this.#httpClient
94
- .request(method, url, {
95
- body,
96
- headers: option?.headers,
97
- params: option?.params,
98
- observe: 'response', // Lấy full response để check status
99
- responseType: option?.responseType,
100
- })
101
- .pipe(timeout(apiTimeout), map(res => {
102
- // Normalize Response Logic
103
- const bodyRes = res.body;
104
- // Logic check response cũ của bạn
105
- if (bodyRes && typeof bodyRes === 'object' && 'ok' in bodyRes && !bodyRes.ok) {
106
- throw bodyRes; // Giả sử structure trả về { ok: false, ... } là lỗi
107
- }
108
- if (handler?.mapResponse) {
109
- return handler.mapResponse(bodyRes);
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
- return bodyRes;
112
- }), catchError(err => {
113
- // Xóa cache ngay lập tức nếu lỗi để user có thể retry
114
- this.#inFlightRequests.delete(key);
115
- throw err;
116
- }),
117
- // QUAN TRỌNG: shareReplay(1) giúp share kết quả cho các subscriber đến sau (trong 1s)
118
- shareReplay(1));
119
- // Lưu vào Map
120
- this.#inFlightRequests.set(key, {
121
- stream$: request$,
122
- expiry: now + this.#dedupCacheDuration,
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
- // Chuyển đổi sang Promise cho đúng return type của bạn
125
- return lastValueFrom(request$);
126
- };
127
- // --- HELPERS ---
128
- #getHandler = (url) => {
129
- const handlers = this.#configurations.flatMap(b => b?.handlers || []);
130
- return handlers.find(e => e.hosts.some(host => url.startsWith(host)));
131
- };
132
- #generateKey = (url, method, body, option) => {
133
- // FormData không hash được nội dung file, luôn generate key mới
134
- if (body instanceof FormData || option?.autoCache === false) {
135
- return Utilities.generateUuid();
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
- params: option?.params,
141
- headers: option?.headers,
142
- body,
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
- // Dọn dẹp memory leak
146
- #cleanupCache() {
147
- const now = Date.now();
148
- this.#inFlightRequests.forEach((value, key) => {
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: "21.2.17", ngImport: i0, type: SdApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
155
428
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdApiService, providedIn: 'root' });
156
429
  }
@@ -162,46 +435,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", 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
- const url = request.url;
168
- if (!url) {
169
- throw new Error(`Invalid URL`);
170
- }
171
- const handlers = this.#configurations.flatMap(configuration => configuration?.handlers || []);
172
- const handler = handlers?.find(e => e.hosts.some(host => url.startsWith(host)));
173
- const intercept = handler?.intercept;
174
- if (intercept) {
175
- request = request.clone(intercept(request));
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 beforeRemoteHandler = handler?.beforeRemote;
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(() => next.handle(request)), map((event) => {
187
- if (event instanceof HttpResponse) {
188
- afterRemoteHandler?.(event);
189
- }
190
- return event;
191
- }), catchError((error) => {
192
- afterRemoteHandler?.(error);
193
- return throwError(() => error);
194
- }));
195
- }
196
- return next.handle(request).pipe(map((event) => {
197
- if (event instanceof HttpResponse) {
198
- afterRemoteHandler?.(event);
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 event;
201
- }), catchError((error) => {
202
- afterRemoteHandler?.(error);
203
- return throwError(() => error);
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: "21.2.17", ngImport: i0, type: SdHttpInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
207
477
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdHttpInterceptor });