@sdcorejs/angular 21.1.3 → 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 (168) hide show
  1. package/README.md +17 -14
  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 +495 -838
  17. package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
  18. package/fesm2022/sdcorejs-angular-components-import-excel.mjs +24 -34
  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 +4 -4
  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 +16 -22
  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 +96 -63
  39. package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
  40. package/fesm2022/sdcorejs-angular-components-tree.mjs +186 -17
  41. package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
  42. package/fesm2022/sdcorejs-angular-components-upload-file.mjs +15 -22
  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 +7 -7
  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 +6 -3
  53. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
  54. package/fesm2022/sdcorejs-angular-forms-chip.mjs +6 -3
  55. package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
  56. package/fesm2022/sdcorejs-angular-forms-date-range.mjs +16 -13
  57. package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
  58. package/fesm2022/sdcorejs-angular-forms-date.mjs +42 -65
  59. package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
  60. package/fesm2022/sdcorejs-angular-forms-datetime.mjs +51 -66
  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 +1 -1
  65. package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
  66. package/fesm2022/sdcorejs-angular-forms-input-number.mjs +7 -5
  67. package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
  68. package/fesm2022/sdcorejs-angular-forms-input.mjs +290 -56
  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 +6 -20
  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 +6 -5
  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-layout.mjs +861 -80
  93. package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
  94. package/fesm2022/sdcorejs-angular-pipes.mjs +5 -7
  95. package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
  96. package/fesm2022/sdcorejs-angular-services-api.mjs +407 -137
  97. package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
  98. package/fesm2022/sdcorejs-angular-services-cache.mjs +515 -105
  99. package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
  100. package/fesm2022/sdcorejs-angular-services-confirm.mjs +5 -7
  101. package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
  102. package/fesm2022/sdcorejs-angular-services-docx.mjs +7 -10
  103. package/fesm2022/sdcorejs-angular-services-docx.mjs.map +1 -1
  104. package/fesm2022/sdcorejs-angular-services-loading.mjs +339 -74
  105. package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
  106. package/fesm2022/sdcorejs-angular-services-notify.mjs +7 -10
  107. package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
  108. package/fesm2022/sdcorejs-angular-services-persistence.mjs +899 -0
  109. package/fesm2022/sdcorejs-angular-services-persistence.mjs.map +1 -0
  110. package/fesm2022/sdcorejs-angular-services-storage.mjs +324 -122
  111. package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
  112. package/fesm2022/sdcorejs-angular-services-task.mjs +389 -0
  113. package/fesm2022/sdcorejs-angular-services-task.mjs.map +1 -0
  114. package/fesm2022/sdcorejs-angular-services-unsaved-changes.mjs +255 -0
  115. package/fesm2022/sdcorejs-angular-services-unsaved-changes.mjs.map +1 -0
  116. package/fesm2022/sdcorejs-angular-services-viewport.mjs +76 -0
  117. package/fesm2022/sdcorejs-angular-services-viewport.mjs.map +1 -0
  118. package/fesm2022/sdcorejs-angular-services.mjs +4 -0
  119. package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
  120. package/package.json +49 -1
  121. package/types/sdcorejs-angular-components-audit-diff.d.ts +74 -0
  122. package/types/sdcorejs-angular-components-badge.d.ts +1 -1
  123. package/types/sdcorejs-angular-components-breadcrumb.d.ts +53 -0
  124. package/types/sdcorejs-angular-components-data-state.d.ts +44 -0
  125. package/types/sdcorejs-angular-components-document-builder.d.ts +5 -5
  126. package/types/sdcorejs-angular-components-form-generic.d.ts +67 -104
  127. package/types/sdcorejs-angular-components-import-excel.d.ts +4 -14
  128. package/types/sdcorejs-angular-components-job-progress.d.ts +38 -0
  129. package/types/sdcorejs-angular-components-mini-editor.d.ts +9 -9
  130. package/types/sdcorejs-angular-components-modal.d.ts +7 -1
  131. package/types/sdcorejs-angular-components-preview.d.ts +181 -28
  132. package/types/sdcorejs-angular-components-side-drawer.d.ts +7 -1
  133. package/types/sdcorejs-angular-components-tab.d.ts +9 -2
  134. package/types/sdcorejs-angular-components-table.d.ts +8 -7
  135. package/types/sdcorejs-angular-components-tree.d.ts +24 -2
  136. package/types/sdcorejs-angular-components-upload-file.d.ts +5 -4
  137. package/types/sdcorejs-angular-components.d.ts +4 -0
  138. package/types/sdcorejs-angular-directives.d.ts +10 -8
  139. package/types/sdcorejs-angular-forms-autocomplete.d.ts +3 -4
  140. package/types/sdcorejs-angular-forms-checkbox.d.ts +4 -8
  141. package/types/sdcorejs-angular-forms-date-range.d.ts +6 -7
  142. package/types/sdcorejs-angular-forms-date.d.ts +2 -2
  143. package/types/sdcorejs-angular-forms-datetime.d.ts +2 -2
  144. package/types/sdcorejs-angular-forms-entity-picker.d.ts +128 -0
  145. package/types/sdcorejs-angular-forms-input-number.d.ts +1 -1
  146. package/types/sdcorejs-angular-forms-input.d.ts +74 -11
  147. package/types/sdcorejs-angular-forms-models.d.ts +86 -3
  148. package/types/sdcorejs-angular-forms-radio.d.ts +12 -8
  149. package/types/sdcorejs-angular-forms-switch.d.ts +4 -7
  150. package/types/sdcorejs-angular-forms-time-range.d.ts +98 -0
  151. package/types/sdcorejs-angular-forms-time.d.ts +117 -0
  152. package/types/sdcorejs-angular-forms-tree-select.d.ts +76 -0
  153. package/types/sdcorejs-angular-forms.d.ts +15 -5
  154. package/types/sdcorejs-angular-i18n.d.ts +68 -2
  155. package/types/sdcorejs-angular-interceptors.d.ts +5 -6
  156. package/types/sdcorejs-angular-modules-layout.d.ts +288 -15
  157. package/types/sdcorejs-angular-pipes.d.ts +2 -2
  158. package/types/sdcorejs-angular-services-api.d.ts +68 -24
  159. package/types/sdcorejs-angular-services-cache.d.ts +52 -16
  160. package/types/sdcorejs-angular-services-confirm.d.ts +3 -2
  161. package/types/sdcorejs-angular-services-docx.d.ts +2 -3
  162. package/types/sdcorejs-angular-services-loading.d.ts +16 -8
  163. package/types/sdcorejs-angular-services-persistence.d.ts +159 -0
  164. package/types/sdcorejs-angular-services-storage.d.ts +26 -8
  165. package/types/sdcorejs-angular-services-task.d.ts +104 -0
  166. package/types/sdcorejs-angular-services-unsaved-changes.d.ts +83 -0
  167. package/types/sdcorejs-angular-services-viewport.d.ts +42 -0
  168. package/types/sdcorejs-angular-services.d.ts +4 -0
@@ -1,155 +1,565 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken } from '@angular/core';
3
- import { DateUtilities } from '@sdcorejs/angular/utilities/extensions';
2
+ import { InjectionToken, inject, DestroyRef, Injectable } from '@angular/core';
3
+ import { SD_PERSISTENCE_STORAGE_ADAPTER, SdGraphSerializer, SdGraphIdentityCanonicalizer, canonicalizeSdPersistenceValue, buildSdPersistenceKey, digestSdPersistenceKey, readSdPersistenceStorageItem, parseSdPersistenceEnvelope, stringifySdPersistenceValueEnvelope, stringifySdPersistenceTombstoneEnvelope } from '@sdcorejs/angular/services/persistence';
4
4
  import { Utilities } from '@sdcorejs/utils/fns';
5
5
  import { BehaviorSubject } from 'rxjs';
6
- import { map } from 'rxjs/operators';
6
+
7
+ const SD_CACHE_CONFIG = new InjectionToken('sd-cache.configuration');
8
+ /** @deprecated Prefer globally safe unknown callbacks on `ISdCacheConfiguration`. */
9
+ function adaptLegacySdCacheCallbacks(callbacks) {
10
+ return {
11
+ set: callbacks.set
12
+ ? (key, value, option) => {
13
+ if (!callbacks.matches(key, option))
14
+ return;
15
+ if (!callbacks.isValue(value.data))
16
+ throw new TypeError(`Legacy cache value did not match key ${key}`);
17
+ return callbacks.set?.(key, { data: value.data, createdOn: new Date(value.createdOn) }, option);
18
+ }
19
+ : undefined,
20
+ get: callbacks.get
21
+ ? async (key, option) => {
22
+ if (!callbacks.matches(key, option))
23
+ return undefined;
24
+ const value = await callbacks.get?.(key, option);
25
+ if (!value)
26
+ return undefined;
27
+ if (!callbacks.isValue(value.data))
28
+ throw new TypeError(`Legacy cache value did not match key ${key}`);
29
+ return { data: value.data, createdOn: new Date(value.createdOn) };
30
+ }
31
+ : undefined,
32
+ remove: callbacks.remove
33
+ ? (key, option) => {
34
+ if (!callbacks.matches(key, option))
35
+ return;
36
+ return callbacks.remove?.(key, option);
37
+ }
38
+ : undefined,
39
+ };
40
+ }
7
41
 
8
42
  class SdCacheService {
9
- #memoryCache = new Map();
10
- #subjects = new Map();
11
- constructor() { }
12
- create(key, option) {
43
+ #configuration = inject(SD_CACHE_CONFIG, { optional: true });
44
+ #adapter = inject(SD_PERSISTENCE_STORAGE_ADAPTER);
45
+ #destroyRef = inject(DestroyRef);
46
+ #defaultSerializer = new SdGraphSerializer();
47
+ #defaultIdentityCanonicalizer = new SdGraphIdentityCanonicalizer();
48
+ #states = new Map();
49
+ #remoteQueues = new Map();
50
+ #memoryEntries = new Map();
51
+ #failedOwners = new Map();
52
+ #maxFailedOwners = 256;
53
+ #destroyed = false;
54
+ constructor() {
55
+ this.#destroyRef.onDestroy(() => {
56
+ this.#destroyed = true;
57
+ for (const state of this.#states.values()) {
58
+ state.disposed = true;
59
+ state.generation += 1;
60
+ state.entry = undefined;
61
+ state.inFlight = undefined;
62
+ for (const handle of state.handles)
63
+ handle.complete();
64
+ state.handles.clear();
65
+ }
66
+ this.#states.clear();
67
+ this.#remoteQueues.clear();
68
+ this.#memoryEntries.clear();
69
+ this.#failedOwners.clear();
70
+ });
71
+ }
72
+ create(key, option = {}) {
73
+ if (this.#destroyed)
74
+ throw new Error('Cache service has been destroyed');
13
75
  if (!key)
14
76
  throw new Error('Key is required');
15
- const hashKey = Utilities.hash({ key });
16
- // 1. Init Subject: Load data có sẵn nếu có
17
- if (!this.#subjects.has(hashKey)) {
18
- const existingData = this.#internalGet(hashKey, option);
19
- this.#subjects.set(hashKey, new BehaviorSubject(existingData ?? option?.default));
20
- }
21
- const subject = this.#subjects.get(hashKey);
22
- // 2. Define helpers
77
+ const normalized = this.#normalize(key, option);
78
+ const state = this.#getOrCreateState(normalized);
79
+ if (Object.prototype.hasOwnProperty.call(option, 'default')) {
80
+ return this.#createDefaultHandle(state, normalized, this.#cloneAs(normalized.serializer, option.default));
81
+ }
82
+ return this.#createOptionalHandle(state, normalized);
83
+ }
84
+ #getOrCreateState(normalized) {
85
+ const existing = this.#states.get(normalized.ownerDigest);
86
+ if (existing)
87
+ return existing;
88
+ const state = {
89
+ hydrated: false,
90
+ handles: new Set(),
91
+ generation: 0,
92
+ disposed: false,
93
+ };
94
+ this.#states.set(normalized.ownerDigest, state);
95
+ this.#hydrateState(state, normalized);
96
+ return state;
97
+ }
98
+ #createOptionalHandle(state, normalized) {
99
+ const initial = this.#readOptional(state, normalized);
100
+ const subject = new BehaviorSubject(initial);
101
+ const facade = {
102
+ emit: (present, value) => subject.next(present ? this.#cloneAs(normalized.serializer, value) : undefined),
103
+ complete: () => subject.complete(),
104
+ };
105
+ const operations = this.#attachHandle(state, normalized, facade, () => this.#readOptional(state, normalized));
106
+ return { ...operations, observer: subject.asObservable() };
107
+ }
108
+ #createDefaultHandle(state, normalized, defaultSnapshot) {
109
+ const read = () => this.#readWithDefault(state, normalized, defaultSnapshot);
110
+ const subject = new BehaviorSubject(read());
111
+ const facade = {
112
+ emit: (present, value) => subject.next(present ? this.#cloneAs(normalized.serializer, value) : this.#cloneAs(normalized.serializer, defaultSnapshot)),
113
+ complete: () => subject.complete(),
114
+ };
115
+ const operations = this.#attachHandle(state, normalized, facade, read);
116
+ return { ...operations, get: read, observer: subject.asObservable() };
117
+ }
118
+ #attachHandle(state, normalized, facade, read) {
119
+ state.handles.add(facade);
120
+ let active = true;
121
+ const assertActive = () => {
122
+ if (!active || state.disposed || this.#destroyed)
123
+ throw new Error('Cache handle has been destroyed');
124
+ };
23
125
  const get = () => {
24
- const val = this.#internalGet(hashKey, option);
25
- // Fix strict type: Ép kiểu về T (chấp nhận rủi ro undefined nếu user không check)
26
- return (val ?? option?.default);
126
+ assertActive();
127
+ return read();
128
+ };
129
+ const snapshot = () => {
130
+ assertActive();
131
+ const entry = this.#getValidEntry(state, normalized);
132
+ return entry === undefined ? { present: false } : { present: true, value: this.#cloneAs(normalized.serializer, entry.data) };
27
133
  };
28
134
  const set = (data) => {
29
- this.#internalSet(hashKey, data, option);
30
- subject.next(data);
135
+ assertActive();
136
+ this.#setEntry(state, normalized, data, true);
137
+ this.#broadcast(state);
31
138
  };
32
139
  const has = () => {
33
- return this.#internalGet(hashKey, option) !== undefined;
140
+ assertActive();
141
+ return this.#getValidEntry(state, normalized) !== undefined;
34
142
  };
35
143
  const remove = () => {
36
- this.#internalRemove(hashKey, option);
37
- subject.next(undefined);
144
+ assertActive();
145
+ this.#removeEntry(state, normalized, true);
146
+ this.#broadcast(state);
147
+ };
148
+ const releaseFacade = () => {
149
+ if (!active)
150
+ return;
151
+ active = false;
152
+ state.handles.delete(facade);
153
+ facade.complete();
154
+ if (state.handles.size > 0)
155
+ return;
156
+ state.disposed = true;
157
+ state.generation += 1;
158
+ state.entry = undefined;
159
+ state.inFlight = undefined;
160
+ this.#states.delete(normalized.ownerDigest);
38
161
  };
162
+ const release = () => releaseFacade();
39
163
  const destroy = () => {
40
- subject.complete();
41
- this.#subjects.delete(hashKey);
42
- this.#memoryCache.delete(hashKey);
164
+ if (!active)
165
+ return;
166
+ releaseFacade();
167
+ if (normalized.area !== 'memory')
168
+ return;
169
+ this.#memoryEntries.delete(normalized.ownerDigest);
170
+ if (state.disposed)
171
+ return;
172
+ state.entry = undefined;
173
+ state.hydrated = true;
174
+ this.#broadcast(state);
43
175
  };
44
- // 3. IMPLEMENT LOAD (Get-Or-Set Pattern)
45
- const load = async (callback) => {
46
- // Check cache trước
47
- if (has()) {
48
- return get();
176
+ const load = (callback) => {
177
+ try {
178
+ assertActive();
49
179
  }
50
- // Cache miss -> Gọi API
51
- const result = await callback();
52
- // Chỉ lưu nếu kết quả hợp lệ (khác undefined/null)
53
- if (result !== undefined && result !== null) {
54
- set(result);
180
+ catch (error) {
181
+ return Promise.reject(error);
55
182
  }
56
- return result;
183
+ const entry = this.#getValidEntry(state, normalized);
184
+ if (entry)
185
+ return Promise.resolve(this.#cloneAs(normalized.serializer, entry.data));
186
+ const generation = state.generation;
187
+ if (state.inFlight?.generation === generation) {
188
+ return state.inFlight.promise.then(value => this.#cloneAs(normalized.serializer, value));
189
+ }
190
+ const promise = this.#loadMiss(state, normalized, callback, generation);
191
+ state.inFlight = { generation, promise };
192
+ void promise
193
+ .finally(() => {
194
+ if (state.inFlight?.promise === promise)
195
+ state.inFlight = undefined;
196
+ })
197
+ .catch(() => undefined);
198
+ return promise;
57
199
  };
200
+ return { get, snapshot, set, has, remove, release, destroy, load };
201
+ }
202
+ async #loadMiss(state, normalized, callback, generation) {
203
+ const remote = await this.#readRemote(normalized);
204
+ if (remote) {
205
+ if (!state.disposed && state.generation === generation) {
206
+ state.entry = this.#cloneEntry(remote, normalized.serializer);
207
+ state.hydrated = true;
208
+ state.generation += 1;
209
+ this.#writePersistent(normalized, state.entry);
210
+ this.#broadcast(state);
211
+ }
212
+ return this.#cloneAs(normalized.serializer, remote.data);
213
+ }
214
+ const result = await callback();
215
+ if (result !== null && result !== undefined && !state.disposed && state.generation === generation) {
216
+ this.#setEntry(state, normalized, result, true);
217
+ this.#broadcast(state);
218
+ }
219
+ if (result === null || result === undefined)
220
+ return result;
221
+ return this.#cloneAs(normalized.serializer, result);
222
+ }
223
+ #normalize(key, option) {
224
+ const serializer = option.serializer ?? this.#configuration?.serializer ?? this.#defaultSerializer;
225
+ const identityCanonicalizer = option.identityCanonicalizer ?? this.#configuration?.identityCanonicalizer ?? this.#defaultIdentityCanonicalizer;
226
+ const canonicalKey = canonicalizeSdPersistenceValue(identityCanonicalizer, key, 'cache key');
227
+ const legacyStorageKey = this.#legacyStorageKey(key);
228
+ const namespace = option.namespace ?? this.#configuration?.namespace;
229
+ const version = option.version ?? this.#configuration?.version;
230
+ const area = option.type ?? 'memory';
231
+ const ttlMs = this.#normalizeTtl(option);
232
+ const serializerFormat = this.#serializerFormat(serializer);
233
+ const hasArgs = Object.prototype.hasOwnProperty.call(option, 'args');
234
+ const hasDefault = Object.prototype.hasOwnProperty.call(option, 'default');
235
+ const fields = [
236
+ { tag: 'area', value: canonicalizeSdPersistenceValue(identityCanonicalizer, area, 'cache area') },
237
+ { tag: 'namespace', value: this.#optionalIdentity(identityCanonicalizer, namespace !== undefined, namespace, 'cache namespace') },
238
+ { tag: 'version', value: this.#optionalIdentity(identityCanonicalizer, version !== undefined, version, 'cache version') },
239
+ { tag: 'ttlMs', value: this.#optionalIdentity(identityCanonicalizer, ttlMs !== undefined, ttlMs, 'cache TTL') },
240
+ { tag: 'args', value: this.#optionalIdentity(identityCanonicalizer, hasArgs, option.args, 'cache args') },
241
+ { tag: 'default', value: this.#optionalIdentity(identityCanonicalizer, hasDefault, option.default, 'cache default') },
242
+ { tag: 'serializer', value: serializerFormat },
243
+ { tag: 'identityCanonicalizer', value: this.#identityCanonicalizerFormat(identityCanonicalizer) },
244
+ ];
245
+ const identity = buildSdPersistenceKey(canonicalKey, fields);
246
+ const identityDigest = digestSdPersistenceKey(identity);
247
+ const storageKey = this.#convertKey(`sdcorejs.cache@1:${identityDigest}`);
248
+ const ownerDigest = digestSdPersistenceKey(buildSdPersistenceKey(storageKey, [{ tag: 'area', value: area }]));
58
249
  return {
59
- get,
60
- set,
61
- has,
62
- remove,
63
- destroy,
64
- load,
65
- observer: subject.asObservable().pipe(map(() => get())),
250
+ publicOption: option,
251
+ area,
252
+ storageKey,
253
+ legacyStorageKey,
254
+ ownerDigest,
255
+ identityDigest,
256
+ ttlMs,
257
+ serializer,
66
258
  };
67
259
  }
68
- // --- PRIVATE HELPERS ---
69
- #getStorage(option) {
70
- if (option?.type === 'local')
71
- return localStorage;
72
- if (option?.type === 'session')
73
- return sessionStorage;
74
- return null;
75
- }
76
- #internalGet(key, option) {
77
- let entry = this.#memoryCache.get(key);
78
- if (!entry) {
79
- const storage = this.#getStorage(option);
80
- if (storage) {
81
- const raw = storage.getItem(key);
82
- if (raw) {
83
- try {
84
- const parsed = JSON.parse(raw);
85
- entry = {
86
- data: parsed.data,
87
- createdOn: new Date(parsed.createdOn),
88
- };
89
- this.#memoryCache.set(key, entry);
90
- }
91
- catch (e) {
92
- this.#internalRemove(key, option);
93
- }
94
- }
95
- }
260
+ #convertKey(baseKey) {
261
+ try {
262
+ return this.#configuration?.convertKey?.(baseKey) ?? baseKey;
263
+ }
264
+ catch {
265
+ return baseKey;
266
+ }
267
+ }
268
+ #legacyStorageKey(key) {
269
+ try {
270
+ return this.#convertKey(Utilities.hash({ key }));
96
271
  }
97
- if (!entry)
272
+ catch {
273
+ return undefined;
274
+ }
275
+ }
276
+ #normalizeTtl(option) {
277
+ const candidate = option.ttlMs ?? (option.hours === undefined ? undefined : option.hours * 3_600_000);
278
+ if (candidate === undefined || !Number.isFinite(candidate) || candidate <= 0)
279
+ return undefined;
280
+ return Math.min(candidate, Number.MAX_SAFE_INTEGER);
281
+ }
282
+ #optionalIdentity(canonicalizer, present, value, label) {
283
+ return present ? `1${canonicalizeSdPersistenceValue(canonicalizer, value, label)}` : '0';
284
+ }
285
+ #identityCanonicalizerFormat(canonicalizer) {
286
+ if (typeof canonicalizer.format !== 'string' || canonicalizer.format.length === 0) {
287
+ throw new Error('Persistence identity canonicalizer format must be a non-empty string');
288
+ }
289
+ return canonicalizer.format;
290
+ }
291
+ #serializerFormat(serializer) {
292
+ if (typeof serializer.format !== 'string' || serializer.format.length === 0) {
293
+ throw new Error('Persistence serializer format must be a non-empty string');
294
+ }
295
+ return serializer.format;
296
+ }
297
+ #readOptional(state, normalized) {
298
+ const entry = this.#getValidEntry(state, normalized);
299
+ return entry ? this.#cloneAs(normalized.serializer, entry.data) : undefined;
300
+ }
301
+ #readWithDefault(state, normalized, defaultSnapshot) {
302
+ const entry = this.#getValidEntry(state, normalized);
303
+ return entry ? this.#cloneAs(normalized.serializer, entry.data) : this.#cloneAs(normalized.serializer, defaultSnapshot);
304
+ }
305
+ #getValidEntry(state, normalized) {
306
+ if (!state.hydrated) {
307
+ this.#hydrateState(state, normalized);
308
+ }
309
+ if (!state.entry)
310
+ return undefined;
311
+ if (this.#isExpired(state.entry, normalized.ttlMs)) {
312
+ this.#removeEntry(state, normalized, true);
313
+ this.#broadcast(state);
98
314
  return undefined;
99
- if (option?.hours && entry.createdOn) {
100
- const expiredOn = DateUtilities.addHours(entry.createdOn, option.hours);
101
- if (expiredOn && expiredOn < new Date()) {
102
- this.#internalRemove(key, option);
103
- return undefined;
104
- }
105
315
  }
106
- return this.#deepClone(entry.data);
316
+ return state.entry;
317
+ }
318
+ #isExpired(entry, ttlMs) {
319
+ return ttlMs !== undefined && entry.createdOn.getTime() + ttlMs <= Date.now();
107
320
  }
108
- #internalSet(key, data, option) {
109
- // FIX LỖI TẠI ĐÂY: Bỏ "?? null" để giữ nguyên kiểu T
110
- const clonedData = this.#deepClone(data);
321
+ #hydrateState(state, normalized) {
322
+ const read = this.#readEntry(normalized);
323
+ state.entry = read.status === 'found' ? read.entry : undefined;
324
+ state.hydrated = read.status !== 'unavailable';
325
+ }
326
+ #setEntry(state, normalized, data, notifyRemote) {
111
327
  const entry = {
112
- data: clonedData,
328
+ data: normalized.serializer.clone(data),
113
329
  createdOn: new Date(),
114
330
  };
115
- this.#memoryCache.set(key, entry);
116
- const storage = this.#getStorage(option);
117
- if (storage) {
331
+ state.entry = entry;
332
+ state.hydrated = true;
333
+ state.generation += 1;
334
+ this.#writePersistent(normalized, entry);
335
+ if (notifyRemote)
336
+ this.#safeRemoteSet(normalized, entry);
337
+ }
338
+ #removeEntry(state, normalized, notifyRemote) {
339
+ state.entry = undefined;
340
+ state.hydrated = true;
341
+ state.generation += 1;
342
+ if (normalized.area !== 'memory')
343
+ this.#writeTombstone(normalized);
344
+ if (notifyRemote)
345
+ void this.#safeRemoteRemove(normalized);
346
+ }
347
+ #broadcast(state) {
348
+ const present = state.entry !== undefined;
349
+ const value = state.entry?.data;
350
+ for (const handle of state.handles)
351
+ handle.emit(present, value);
352
+ }
353
+ #readEntry(normalized) {
354
+ if (normalized.area === 'memory') {
355
+ const entry = this.#memoryEntries.get(normalized.ownerDigest);
356
+ return entry ? { status: 'found', entry: this.#cloneEntry(entry, normalized.serializer) } : { status: 'absent' };
357
+ }
358
+ if (this.#failedOwners.has(normalized.ownerDigest))
359
+ return { status: 'unavailable' };
360
+ const primary = readSdPersistenceStorageItem(this.#adapter, normalized.area, normalized.storageKey);
361
+ if (primary.status === 'found') {
362
+ let envelope;
363
+ try {
364
+ envelope = parseSdPersistenceEnvelope(primary.value, normalized.identityDigest, normalized.serializer.format);
365
+ }
366
+ catch {
367
+ this.#writeTombstone(normalized);
368
+ return { status: 'absent' };
369
+ }
370
+ if (envelope?.kind === 'tombstone')
371
+ return { status: 'absent' };
372
+ const entry = envelope?.kind === 'value' ? this.#parseCurrentEntry(envelope.payload, normalized.serializer) : undefined;
373
+ if (entry)
374
+ return { status: 'found', entry };
375
+ this.#writeTombstone(normalized);
376
+ return { status: 'absent' };
377
+ }
378
+ if (primary.status === 'unavailable')
379
+ return { status: 'unavailable' };
380
+ if (normalized.legacyStorageKey === undefined)
381
+ return { status: 'absent' };
382
+ const legacyRead = readSdPersistenceStorageItem(this.#adapter, normalized.area, normalized.legacyStorageKey);
383
+ if (legacyRead.status === 'unavailable')
384
+ return { status: 'unavailable' };
385
+ if (legacyRead.status === 'absent')
386
+ return { status: 'absent' };
387
+ const legacy = this.#parseLegacy(legacyRead.value);
388
+ if (!legacy) {
389
+ this.#writeTombstone(normalized);
390
+ return { status: 'absent' };
391
+ }
392
+ this.#writePersistent(normalized, legacy);
393
+ return { status: 'found', entry: legacy };
394
+ }
395
+ #parseCurrentEntry(payload, serializer) {
396
+ try {
397
+ return this.#validateEntry(serializer.parse(payload));
398
+ }
399
+ catch {
400
+ return undefined;
401
+ }
402
+ }
403
+ #parseLegacy(raw) {
404
+ try {
405
+ const parsed = JSON.parse(raw);
406
+ if (!this.#hasExactEntryKeys(parsed))
407
+ return undefined;
408
+ const createdOn = this.#toValidDate(parsed['createdOn']);
409
+ return createdOn ? { data: parsed['data'], createdOn } : undefined;
410
+ }
411
+ catch {
412
+ return undefined;
413
+ }
414
+ }
415
+ #validateEntry(value) {
416
+ if (!this.#hasExactEntryKeys(value))
417
+ return undefined;
418
+ const createdOn = this.#toValidDate(value['createdOn']);
419
+ return createdOn ? { data: value['data'], createdOn } : undefined;
420
+ }
421
+ #toValidDate(value) {
422
+ const date = value instanceof Date ? value : typeof value === 'string' || typeof value === 'number' ? new Date(value) : undefined;
423
+ return date && Number.isFinite(date.getTime()) ? date : undefined;
424
+ }
425
+ #writePersistent(normalized, entry) {
426
+ if (normalized.area === 'memory') {
427
+ this.#memoryEntries.set(normalized.ownerDigest, this.#cloneEntry(entry, normalized.serializer));
428
+ return true;
429
+ }
430
+ try {
431
+ const payload = normalized.serializer.stringify(entry);
432
+ if (this.#adapter.setItem(normalized.area, normalized.storageKey, stringifySdPersistenceValueEnvelope(normalized.identityDigest, normalized.serializer.format, payload))) {
433
+ this.#clearFailedOwner(normalized.ownerDigest);
434
+ return true;
435
+ }
436
+ }
437
+ catch {
438
+ // Fall through to canonical quarantine.
439
+ }
440
+ this.#safeAdapterRemove(normalized.area, normalized.storageKey);
441
+ if (this.#tryPersistTombstone(normalized)) {
442
+ this.#clearFailedOwner(normalized.ownerDigest);
443
+ return false;
444
+ }
445
+ this.#recordFailedOwner(normalized.ownerDigest);
446
+ return false;
447
+ }
448
+ #readRemote(normalized) {
449
+ return this.#enqueueRemote(normalized, async () => {
450
+ const get = this.#configuration?.get;
451
+ if (!get)
452
+ return undefined;
453
+ const entry = this.#validateEntry(await get(normalized.storageKey, normalized.publicOption));
454
+ if (!entry || !this.#isExpired(entry, normalized.ttlMs))
455
+ return entry;
456
+ const remove = this.#configuration?.remove;
457
+ if (remove)
458
+ await remove(normalized.storageKey, normalized.publicOption);
459
+ return undefined;
460
+ });
461
+ }
462
+ #safeRemoteSet(normalized, entry) {
463
+ const set = this.#configuration?.set;
464
+ if (!set)
465
+ return;
466
+ void this.#enqueueRemote(normalized, () => set(normalized.storageKey, this.#cloneEntry(entry, normalized.serializer), normalized.publicOption));
467
+ }
468
+ #safeRemoteRemove(normalized) {
469
+ const remove = this.#configuration?.remove;
470
+ if (!remove)
471
+ return Promise.resolve();
472
+ return this.#enqueueRemote(normalized, () => remove(normalized.storageKey, normalized.publicOption));
473
+ }
474
+ #enqueueRemote(normalized, operation) {
475
+ const previous = this.#remoteQueues.get(normalized.ownerDigest) ?? Promise.resolve();
476
+ const result = previous.then(async () => {
118
477
  try {
119
- storage.setItem(key, JSON.stringify(entry));
478
+ return await operation();
120
479
  }
121
- catch (e) {
122
- console.error('Storage quota exceeded', e);
480
+ catch {
481
+ // A failed remote operation cannot poison later work for this identity.
482
+ return undefined;
123
483
  }
484
+ });
485
+ const tail = result.then(() => undefined);
486
+ this.#remoteQueues.set(normalized.ownerDigest, tail);
487
+ void tail.then(() => {
488
+ if (this.#remoteQueues.get(normalized.ownerDigest) === tail)
489
+ this.#remoteQueues.delete(normalized.ownerDigest);
490
+ });
491
+ return result;
492
+ }
493
+ #safeAdapterRemove(area, key) {
494
+ try {
495
+ return this.#adapter.removeItem(area, key);
124
496
  }
497
+ catch {
498
+ return false;
499
+ }
500
+ }
501
+ #writeTombstone(normalized) {
502
+ if (normalized.area === 'memory') {
503
+ this.#memoryEntries.delete(normalized.ownerDigest);
504
+ return true;
505
+ }
506
+ if (this.#tryPersistTombstone(normalized)) {
507
+ this.#clearFailedOwner(normalized.ownerDigest);
508
+ return true;
509
+ }
510
+ this.#safeAdapterRemove(normalized.area, normalized.storageKey);
511
+ this.#recordFailedOwner(normalized.ownerDigest);
512
+ return false;
125
513
  }
126
- #internalRemove(key, option) {
127
- this.#memoryCache.delete(key);
128
- const storage = this.#getStorage(option);
129
- if (storage) {
130
- storage.removeItem(key);
514
+ #tryPersistTombstone(normalized) {
515
+ if (normalized.area === 'memory')
516
+ return false;
517
+ try {
518
+ return this.#adapter.setItem(normalized.area, normalized.storageKey, stringifySdPersistenceTombstoneEnvelope(normalized.identityDigest, normalized.serializer.format));
519
+ }
520
+ catch {
521
+ return false;
131
522
  }
132
523
  }
133
- #deepClone(val) {
134
- if (val === undefined || val === null)
135
- return val;
136
- return JSON.parse(JSON.stringify(val));
524
+ #clearFailedOwner(ownerDigest) {
525
+ this.#failedOwners.delete(ownerDigest);
526
+ }
527
+ #recordFailedOwner(ownerDigest) {
528
+ this.#failedOwners.delete(ownerDigest);
529
+ this.#failedOwners.set(ownerDigest, true);
530
+ if (this.#failedOwners.size <= this.#maxFailedOwners)
531
+ return;
532
+ const oldest = this.#failedOwners.keys().next().value;
533
+ if (oldest !== undefined)
534
+ this.#failedOwners.delete(oldest);
535
+ }
536
+ #cloneEntry(entry, serializer) {
537
+ return { data: serializer.clone(entry.data), createdOn: new Date(entry.createdOn) };
538
+ }
539
+ #cloneAs(serializer, value) {
540
+ const entry = serializer.parse(serializer.stringify({ data: value, createdOn: new Date(0) }));
541
+ return entry.data;
542
+ }
543
+ #isRecord(value) {
544
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
545
+ }
546
+ #hasExactEntryKeys(value) {
547
+ return (this.#isRecord(value) &&
548
+ Reflect.ownKeys(value).length === 2 &&
549
+ Object.prototype.hasOwnProperty.call(value, 'data') &&
550
+ Object.prototype.hasOwnProperty.call(value, 'createdOn'));
137
551
  }
138
552
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
139
553
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdCacheService, providedIn: 'root' });
140
554
  }
141
555
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdCacheService, decorators: [{
142
556
  type: Injectable,
143
- args: [{
144
- providedIn: 'root',
145
- }]
557
+ args: [{ providedIn: 'root' }]
146
558
  }], ctorParameters: () => [] });
147
559
 
148
- const SD_CACHE_CONFIG = new InjectionToken('sd-cache.configuration');
149
-
150
560
  /**
151
561
  * Generated bundle index. Do not edit.
152
562
  */
153
563
 
154
- export { SD_CACHE_CONFIG, SdCacheService };
564
+ export { SD_CACHE_CONFIG, SdCacheService, adaptLegacySdCacheCallbacks };
155
565
  //# sourceMappingURL=sdcorejs-angular-services-cache.mjs.map