@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,530 @@
1
+ import type { StorageCapabilities, ParentConfig } from '../types'
2
+ import type { CacheInfo, EntityManagerInternal } from './EntityManager.types'
3
+
4
+ type Self = EntityManagerInternal<any>
5
+
6
+ /**
7
+ * Patch EntityManager prototype with cache-related getters and methods.
8
+ */
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export function applyCacheMethods(EntityManagerClass: { prototype: any }): void {
11
+ const proto = EntityManagerClass.prototype as Self
12
+
13
+ // ============ THRESHOLD & TTL (getters) ============
14
+
15
+ Object.defineProperty(proto, 'effectiveThreshold', {
16
+ get(this: Self): number {
17
+ return this.localFilterThreshold ?? 100
18
+ },
19
+ configurable: true,
20
+ })
21
+
22
+ Object.defineProperty(proto, 'effectiveCacheTtlMs', {
23
+ get(this: Self): number {
24
+ // 1. Storage capabilities (can be set dynamically from API headers)
25
+ const storageCaps =
26
+ (this.storage as unknown as { capabilities?: StorageCapabilities })?.capabilities ||
27
+ (this.storage?.constructor as { capabilities?: StorageCapabilities })?.capabilities
28
+ if (storageCaps?.cacheTtlMs !== undefined) {
29
+ return storageCaps.cacheTtlMs
30
+ }
31
+
32
+ // 2. Entity-level config
33
+ if (this._cacheTtlMs !== null) {
34
+ return this._cacheTtlMs
35
+ }
36
+
37
+ // 3. Global default from kernel options
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const globalTtl = (this._orchestrator as any)?.kernelOptions?.defaultEntityCacheTtlMs
40
+ if (globalTtl !== undefined) {
41
+ return globalTtl
42
+ }
43
+
44
+ // 4. Default: infinite (no expiration)
45
+ return -1
46
+ },
47
+ configurable: true,
48
+ })
49
+
50
+ Object.defineProperty(proto, 'isCacheTtlDisabled', {
51
+ get(this: Self): boolean {
52
+ return this.effectiveCacheTtlMs === 0
53
+ },
54
+ configurable: true,
55
+ })
56
+
57
+ Object.defineProperty(proto, 'storageSupportsTotal', {
58
+ get(this: Self): boolean {
59
+ const caps = (this.storage?.constructor as { capabilities?: Partial<StorageCapabilities> })
60
+ ?.capabilities
61
+ return caps?.supportsTotal ?? false
62
+ },
63
+ configurable: true,
64
+ })
65
+
66
+ Object.defineProperty(proto, 'storageSearchFields', {
67
+ get(this: Self): string[] | undefined {
68
+ const caps = (this.storage?.constructor as { capabilities?: Partial<StorageCapabilities> })
69
+ ?.capabilities
70
+ return caps?.searchFields
71
+ },
72
+ configurable: true,
73
+ })
74
+
75
+ Object.defineProperty(proto, 'isCacheEnabled', {
76
+ get(this: Self): boolean {
77
+ if (this.effectiveThreshold <= 0) return false
78
+ if (this.isCacheTtlDisabled) return false // TTL=0 disables cache
79
+ // Check capabilities (instance getter or static)
80
+ const caps =
81
+ (this.storage as unknown as { capabilities?: StorageCapabilities })?.capabilities ||
82
+ (this.storage?.constructor as { capabilities?: StorageCapabilities })?.capabilities
83
+ if (caps?.supportsCaching === false) return false
84
+ if (!this.storageSupportsTotal) return false
85
+ return true
86
+ },
87
+ configurable: true,
88
+ })
89
+
90
+ Object.defineProperty(proto, 'overflow', {
91
+ get(this: Self): boolean {
92
+ const cache = this._cache
93
+ return cache.valid && cache.total > cache.items.length
94
+ },
95
+ configurable: true,
96
+ })
97
+
98
+ Object.defineProperty(proto, 'warmupEnabled', {
99
+ get(this: Self): boolean {
100
+ return this._warmup && this.isCacheEnabled
101
+ },
102
+ configurable: true,
103
+ })
104
+
105
+ // ============ ASYMMETRIC MODE ============
106
+
107
+ Object.defineProperty(proto, 'isAsymmetric', {
108
+ get(this: Self): boolean {
109
+ if (this._asymmetric) return true
110
+ // Fallback to storage capabilities
111
+ const caps =
112
+ (this.storage as unknown as { capabilities?: StorageCapabilities })?.capabilities ||
113
+ (this.storage?.constructor as { capabilities?: StorageCapabilities })?.capabilities
114
+ return caps?.asymmetric ?? false
115
+ },
116
+ configurable: true,
117
+ })
118
+
119
+ Object.defineProperty(proto, 'effectiveDetailCacheTtlMs', {
120
+ get(this: Self): number {
121
+ return this._detailCacheTtlMs
122
+ },
123
+ configurable: true,
124
+ })
125
+
126
+ Object.defineProperty(proto, 'effectiveDetailCacheMaxSize', {
127
+ get(this: Self): number {
128
+ return this._detailCacheMaxSize
129
+ },
130
+ configurable: true,
131
+ })
132
+
133
+ Object.defineProperty(proto, 'isDetailCacheEnabled', {
134
+ get(this: Self): boolean {
135
+ return this.isAsymmetric && this._detailCacheTtlMs !== 0
136
+ },
137
+ configurable: true,
138
+ })
139
+
140
+ // ============ INTERNAL METHODS ============
141
+
142
+ proto._isCacheExpired = function (this: Self): boolean {
143
+ const cache = this._cache
144
+ if (!cache.valid || !cache.loadedAt) return false
145
+ const ttl = this.effectiveCacheTtlMs
146
+ if (ttl <= 0) return false // 0 = disabled (handled elsewhere), -1 = infinite
147
+ return Date.now() - cache.loadedAt > ttl
148
+ }
149
+
150
+ proto._isDetailCacheEntryExpired = function (this: Self, loadedAt: number): boolean {
151
+ const ttl = this.effectiveDetailCacheTtlMs
152
+ if (ttl < 0) return false // -1 = infinite
153
+ if (ttl === 0) return true // 0 = disabled
154
+ return Date.now() - loadedAt > ttl
155
+ }
156
+
157
+ proto._evictDetailCache = function (this: Self): void {
158
+ const maxSize = this._detailCacheMaxSize
159
+ if (maxSize <= 0) return // 0 = unlimited
160
+ const items = this._detailCache.items
161
+ if (items.size <= maxSize) return
162
+
163
+ // Evict oldest entries by loadedAt
164
+ const entries = [...items.entries()].sort((a, b) => a[1].loadedAt - b[1].loadedAt)
165
+ const toRemove = entries.length - maxSize
166
+ for (let i = 0; i < toRemove; i++) {
167
+ items.delete(entries[i][0])
168
+ }
169
+ }
170
+
171
+ proto._getStorageRequiresAuth = function (this: Self): boolean {
172
+ // Check instance capabilities first (may have dynamic requiresAuth)
173
+ const instanceCaps = (this.storage as unknown as { capabilities?: Partial<StorageCapabilities> })
174
+ ?.capabilities
175
+ if (instanceCaps?.requiresAuth !== undefined) {
176
+ return instanceCaps.requiresAuth
177
+ }
178
+ // Fallback to static capabilities
179
+ const caps = (this.storage?.constructor as { capabilities?: Partial<StorageCapabilities> })
180
+ ?.capabilities
181
+ return caps?.requiresAuth ?? false
182
+ }
183
+
184
+ proto._parseSearchFields = function (
185
+ this: Self,
186
+ overrideSearchFields: string[] | null = null
187
+ ): { ownFields: string[]; parentFields: Record<string, string[]> } {
188
+ const search = overrideSearchFields ?? this.storageSearchFields ?? []
189
+ const ownFields: string[] = []
190
+ const parentFields: Record<string, string[]> = {}
191
+
192
+ const parents: Record<string, ParentConfig> = this._parents
193
+
194
+ for (const field of search) {
195
+ if (!field.includes('.')) {
196
+ ownFields.push(field)
197
+ continue
198
+ }
199
+
200
+ // Split on first dot only
201
+ const parts = field.split('.', 2)
202
+ const parentKey = parts[0]
203
+ const fieldName = parts[1]
204
+
205
+ if (!parentKey || !fieldName) {
206
+ continue
207
+ }
208
+
209
+ // Validate parentKey exists in parents config
210
+ if (!parents[parentKey]) {
211
+ console.warn(
212
+ `[EntityManager:${this.name}] Unknown parent '${parentKey}' in searchFields '${field}', skipping`
213
+ )
214
+ continue
215
+ }
216
+
217
+ // Group by parentKey
218
+ if (!parentFields[parentKey]) {
219
+ parentFields[parentKey] = []
220
+ }
221
+ parentFields[parentKey].push(fieldName)
222
+ }
223
+
224
+ return { ownFields, parentFields }
225
+ }
226
+
227
+ proto._resolveSearchFields = async function (this: Self, items: unknown[]): Promise<void> {
228
+ const { parentFields } = this._parseSearchFields()
229
+
230
+ // No parent fields to resolve
231
+ if (Object.keys(parentFields).length === 0) return
232
+
233
+ const orchestrator = this._orchestrator
234
+ const parents: Record<string, ParentConfig> = this._parents
235
+
236
+ // Need orchestrator to access other managers
237
+ if (!orchestrator) {
238
+ console.warn(
239
+ `[EntityManager:${this.name}] No orchestrator, cannot resolve parent fields`
240
+ )
241
+ return
242
+ }
243
+
244
+ // Process each parent type
245
+ for (const [parentKey, fields] of Object.entries(parentFields)) {
246
+ const config = parents[parentKey]
247
+
248
+ if (!config) {
249
+ console.warn(
250
+ `[EntityManager:${this.name}] Missing parent config for '${parentKey}'`
251
+ )
252
+ continue
253
+ }
254
+
255
+ // Extract unique parent IDs (deduplicated)
256
+ const ids = [
257
+ ...new Set(
258
+ items
259
+ .map((i) => (i as Record<string, unknown>)[config.foreignKey as string] as string | number | undefined)
260
+ .filter((id): id is string | number => id != null)
261
+ ),
262
+ ]
263
+ if (ids.length === 0) continue
264
+
265
+ // Batch fetch parent entities
266
+ const manager = orchestrator.get(config.entity)
267
+ if (!manager) {
268
+ console.warn(
269
+ `[EntityManager:${this.name}] Manager not found for '${config.entity}'`
270
+ )
271
+ continue
272
+ }
273
+
274
+ const parentItems = await manager.getMany(ids)
275
+ const parentMap = new Map(
276
+ parentItems.map((p) => [p[manager.idField as keyof typeof p], p])
277
+ )
278
+
279
+ // Cache resolved values in _search (non-enumerable)
280
+ for (const item of items) {
281
+ // Initialize _search as non-enumerable if needed
282
+ const itemWithSearch = item as { _search?: Record<string, string>; [k: string]: unknown }
283
+ if (!itemWithSearch._search) {
284
+ Object.defineProperty(item, '_search', {
285
+ value: {},
286
+ enumerable: false,
287
+ writable: true,
288
+ configurable: true,
289
+ })
290
+ }
291
+
292
+ const parent = parentMap.get(itemWithSearch[config.foreignKey as string])
293
+ for (const field of fields) {
294
+ itemWithSearch._search![`${parentKey}.${field}`] =
295
+ (parent?.[field as keyof typeof parent] as string) ?? ''
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ // ============ SIGNAL LISTENERS ============
302
+
303
+ proto._setupCacheListeners = function (this: Self): void {
304
+ const signals = this._signals
305
+ if (!signals) return
306
+
307
+ // Clean up existing listeners if any
308
+ const existingCleanup = this._signalCleanup
309
+ if (existingCleanup) {
310
+ existingCleanup()
311
+ this._signalCleanup = null
312
+ }
313
+
314
+ const cleanups: Array<() => void> = []
315
+ const parents: Record<string, ParentConfig> = this._parents
316
+
317
+ // Listen for parent data changes (if parents defined)
318
+ if (parents && Object.keys(parents).length > 0) {
319
+ const parentEntities = Object.values(parents).map((p) => p.entity)
320
+ cleanups.push(
321
+ signals.on(
322
+ 'entity:data-invalidate',
323
+ (event: { name: string; data: unknown }) => {
324
+ const { entity } = (event.data || {}) as { entity?: string }
325
+ if (entity && parentEntities.includes(entity)) {
326
+ this._clearSearchCache()
327
+ }
328
+ }
329
+ )
330
+ )
331
+ }
332
+
333
+ // Listen for own entity data changes to invalidate detail cache
334
+ if (this.isAsymmetric && this.isDetailCacheEnabled) {
335
+ cleanups.push(
336
+ signals.on(
337
+ 'entity:data-invalidate',
338
+ (event: { name: string; data: unknown }) => {
339
+ const { entity } = (event.data || {}) as { entity?: string }
340
+ if (entity === this.name) {
341
+ this.invalidateDetailCache()
342
+ }
343
+ }
344
+ )
345
+ )
346
+ }
347
+
348
+ // Listen for datalayer invalidation
349
+ cleanups.push(
350
+ signals.on(
351
+ 'entity:datalayer-invalidate',
352
+ (event: { name: string; data: unknown }) => {
353
+ const { entity, actuator } = (event.data || {}) as {
354
+ entity?: string
355
+ actuator?: string
356
+ }
357
+ // Auth-triggered: only react if authSensitive
358
+ if (actuator === 'auth' && !this._authSensitive) return
359
+
360
+ // Check entity match (global or targeted)
361
+ if (!entity || entity === '*' || entity === this.name) {
362
+ this.invalidateDataLayer()
363
+ }
364
+ }
365
+ )
366
+ )
367
+
368
+ // Combined cleanup function
369
+ this._signalCleanup = () => {
370
+ cleanups.forEach((cleanup) => cleanup())
371
+ }
372
+ }
373
+
374
+ proto._clearSearchCache = function (this: Self): void {
375
+ const cache = this._cache
376
+ if (!cache.valid) return
377
+
378
+ for (const item of cache.items) {
379
+ const itemWithSearch = item as { _search?: Record<string, string> }
380
+ if (itemWithSearch._search) {
381
+ itemWithSearch._search = {}
382
+ }
383
+ }
384
+
385
+ // Invalidate cache so next list() refetches
386
+ this.invalidateCache()
387
+ }
388
+
389
+ // ============ CACHE STATE ============
390
+
391
+ proto.invalidateDetailCache = function (this: Self): void {
392
+ this._detailCache.items.clear()
393
+ this._detailInflight.clear()
394
+ }
395
+
396
+ proto.invalidateCache = function (this: Self): void {
397
+ const cache = this._cache
398
+ cache.valid = false
399
+ cache.items = []
400
+ cache.total = 0
401
+ cache.loadedAt = null
402
+ this._cacheLoading = null
403
+ // Also clear detail cache
404
+ this.invalidateDetailCache()
405
+ }
406
+
407
+ proto.invalidateDataLayer = function (this: Self): void {
408
+ this.invalidateCache()
409
+
410
+ // Reset storage if it supports it (e.g., clear auth tokens, cached responses)
411
+ if (typeof this.storage?.reset === 'function') {
412
+ this.storage.reset()
413
+ }
414
+ }
415
+
416
+ // ============ CACHE LOADING ============
417
+
418
+ proto.ensureCache = async function (this: Self): Promise<boolean> {
419
+ if (!this.isCacheEnabled) return false
420
+ if (this._cache.valid) return true
421
+
422
+ // Avoid concurrent cache loads
423
+ if (this._cacheLoading) {
424
+ await this._cacheLoading
425
+ return this._cache.valid
426
+ }
427
+
428
+ this._cacheLoading = this._loadCache()
429
+ const result = await this._cacheLoading
430
+ this._cacheLoading = null
431
+ return result
432
+ }
433
+
434
+ proto._loadCacheInBackground = function (this: Self): void {
435
+ this._cacheLoading = this._loadCache()
436
+ this._cacheLoading
437
+ .catch((err: unknown) =>
438
+ console.error(
439
+ `[EntityManager:${this.name}] Background cache load failed:`,
440
+ err
441
+ )
442
+ )
443
+ .finally(() => {
444
+ this._cacheLoading = null
445
+ })
446
+ }
447
+
448
+ proto._loadCache = async function (this: Self): Promise<boolean> {
449
+ const threshold = this.effectiveThreshold
450
+
451
+ // First, check total count with minimal request
452
+ // Use _internal to skip stats counting
453
+ const probe = await this.list({ page_size: 1, _internal: true })
454
+
455
+ if (probe.total > threshold) {
456
+ // Too many items, don't cache
457
+ this._cache.valid = false
458
+ return false
459
+ }
460
+
461
+ // Load all items (internal operation, skip stats)
462
+ const result = await this.list({
463
+ page_size: probe.total || threshold,
464
+ _internal: true,
465
+ })
466
+ const cache = this._cache
467
+ cache.items = result.items || []
468
+ cache.total = result.total
469
+ cache.loadedAt = Date.now()
470
+ cache.valid = true
471
+ // Resolve parent fields for search (book.title, user.username, etc.)
472
+ await this._resolveSearchFields(cache.items)
473
+ return true
474
+ }
475
+
476
+ // ============ WARMUP ============
477
+
478
+ proto.warmup = async function (this: Self): Promise<boolean | null> {
479
+ // Skip if warmup disabled or caching not applicable
480
+ if (!this._warmup) return null
481
+ if (!this.isCacheEnabled) return null
482
+
483
+ const deferred = this._orchestrator?.deferred
484
+
485
+ // Wait for auth if app uses authentication (user context affects cache)
486
+ if (deferred?.has('auth:ready')) {
487
+ await deferred.await('auth:ready')
488
+ }
489
+
490
+ const key = `entity:${this.name}:cache`
491
+
492
+ if (!deferred) {
493
+ // Fallback: direct cache load without DeferredRegistry
494
+ return this.ensureCache()
495
+ }
496
+
497
+ // Register in DeferredRegistry for loose coupling
498
+ return deferred.queue(key, () => this.ensureCache())
499
+ }
500
+
501
+ proto.getCacheInfo = function (this: Self): CacheInfo {
502
+ const ttlMs = this.effectiveCacheTtlMs
503
+ const cache = this._cache
504
+ const asymmetric = this.isAsymmetric
505
+ return {
506
+ enabled: this.isCacheEnabled,
507
+ storageSupportsTotal: this.storageSupportsTotal,
508
+ threshold: this.effectiveThreshold,
509
+ valid: cache.valid,
510
+ overflow: this.overflow,
511
+ itemCount: cache.items.length,
512
+ total: cache.total,
513
+ loadedAt: cache.loadedAt,
514
+ ttlMs,
515
+ expiresAt: cache.loadedAt && ttlMs > 0
516
+ ? cache.loadedAt + ttlMs
517
+ : null,
518
+ expired: this._isCacheExpired(),
519
+ asymmetric,
520
+ detailCache: asymmetric && this.isDetailCacheEnabled
521
+ ? {
522
+ enabled: true,
523
+ ttlMs: this.effectiveDetailCacheTtlMs,
524
+ size: this._detailCache.items.size,
525
+ maxSize: this._detailCacheMaxSize,
526
+ }
527
+ : null,
528
+ }
529
+ }
530
+ }