ezfw-core 1.0.0

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 (154) hide show
  1. package/components/EzBaseComponent.ts +648 -0
  2. package/components/EzComponent.ts +89 -0
  3. package/components/EzInput.module.scss +183 -0
  4. package/components/EzInput.ts +104 -0
  5. package/components/EzLabel.ts +22 -0
  6. package/components/EzOutlet.ts +181 -0
  7. package/components/HtmlWrapper.ts +305 -0
  8. package/components/avatar/EzAvatar.module.scss +200 -0
  9. package/components/avatar/EzAvatar.ts +130 -0
  10. package/components/badge/EzBadge.module.scss +202 -0
  11. package/components/badge/EzBadge.ts +77 -0
  12. package/components/button/EzButton.module.scss +402 -0
  13. package/components/button/EzButton.ts +175 -0
  14. package/components/button/EzButtonGroup.ts +48 -0
  15. package/components/card/EzCard.module.scss +71 -0
  16. package/components/card/EzCard.ts +120 -0
  17. package/components/chart/EzBarChart.ts +47 -0
  18. package/components/chart/EzChart.module.scss +14 -0
  19. package/components/chart/EzChart.ts +279 -0
  20. package/components/chart/EzDoughnutChart.ts +47 -0
  21. package/components/chart/EzLineChart.ts +53 -0
  22. package/components/checkbox/EzCheckbox.module.scss +145 -0
  23. package/components/checkbox/EzCheckbox.ts +115 -0
  24. package/components/dataview/EzDataView.module.scss +115 -0
  25. package/components/dataview/EzDataView.ts +355 -0
  26. package/components/dataview/modes/EzDataViewCards.ts +322 -0
  27. package/components/dataview/modes/EzDataViewGrid.ts +76 -0
  28. package/components/datepicker/EzDatePicker.module.scss +348 -0
  29. package/components/datepicker/EzDatePicker.ts +519 -0
  30. package/components/dialog/EzDialog.module.scss +180 -0
  31. package/components/dropdown/EzDropdown.module.scss +107 -0
  32. package/components/dropdown/EzDropdown.ts +235 -0
  33. package/components/feed/EzActivityFeed.module.scss +90 -0
  34. package/components/feed/EzActivityFeed.ts +78 -0
  35. package/components/form/EzForm.ts +364 -0
  36. package/components/form/EzValidators.test.js +421 -0
  37. package/components/form/EzValidators.ts +202 -0
  38. package/components/grid/EzGrid.scss +88 -0
  39. package/components/grid/EzGrid.ts +1085 -0
  40. package/components/grid/EzGridContainer.ts +104 -0
  41. package/components/grid/body/EzGridBody.scss +283 -0
  42. package/components/grid/body/EzGridBody.ts +549 -0
  43. package/components/grid/body/EzGridCell.ts +211 -0
  44. package/components/grid/body/EzGridRow.ts +196 -0
  45. package/components/grid/filter/EzGridFilters.scss +78 -0
  46. package/components/grid/filter/EzGridFilters.ts +285 -0
  47. package/components/grid/footer/EzGridFooter.scss +136 -0
  48. package/components/grid/footer/EzGridFooter.ts +448 -0
  49. package/components/grid/header/EzGridHeader.scss +199 -0
  50. package/components/grid/header/EzGridHeader.ts +430 -0
  51. package/components/grid/query/EzGridQuery.ts +81 -0
  52. package/components/grid/state/EzGridColumns.ts +155 -0
  53. package/components/grid/state/EzGridController.ts +470 -0
  54. package/components/grid/state/EzGridLifecycle.ts +136 -0
  55. package/components/grid/state/EzGridNormalizers.test.js +273 -0
  56. package/components/grid/state/EzGridNormalizers.ts +162 -0
  57. package/components/grid/state/EzGridParts.ts +233 -0
  58. package/components/grid/state/EzGridPersistence.ts +140 -0
  59. package/components/grid/state/EzGridRemote.test.js +573 -0
  60. package/components/grid/state/EzGridRemote.ts +335 -0
  61. package/components/grid/state/EzGridSelection.ts +231 -0
  62. package/components/grid/state/EzGridSort.ts +286 -0
  63. package/components/grid/title/EzGridActionBar.ts +98 -0
  64. package/components/grid/title/EzGridTitle.ts +114 -0
  65. package/components/grid/title/EzGridTitleBar.scss +65 -0
  66. package/components/grid/title/EzGridTitleBar.ts +87 -0
  67. package/components/grid/types.ts +607 -0
  68. package/components/panel/EzPanel.module.scss +133 -0
  69. package/components/panel/EzPanel.ts +147 -0
  70. package/components/radio/EzRadio.module.scss +190 -0
  71. package/components/radio/EzRadio.ts +149 -0
  72. package/components/select/EzSelect.module.scss +153 -0
  73. package/components/select/EzSelect.ts +238 -0
  74. package/components/skeleton/EzSkeleton.module.scss +95 -0
  75. package/components/skeleton/EzSkeleton.ts +70 -0
  76. package/components/store/EzStore.ts +344 -0
  77. package/components/switch/EzSwitch.module.scss +164 -0
  78. package/components/switch/EzSwitch.ts +117 -0
  79. package/components/tabs/EzTabPanel.module.scss +181 -0
  80. package/components/tabs/EzTabPanel.ts +402 -0
  81. package/components/textarea/EzTextarea.module.scss +131 -0
  82. package/components/textarea/EzTextarea.ts +161 -0
  83. package/components/timepicker/EzTimePicker.module.scss +282 -0
  84. package/components/timepicker/EzTimePicker.ts +540 -0
  85. package/components/toast/EzToast.module.scss +291 -0
  86. package/components/tooltip/EzTooltip.module.scss +124 -0
  87. package/components/tooltip/EzTooltip.ts +153 -0
  88. package/core/EzComponentTypes.ts +693 -0
  89. package/core/EzError.ts +63 -0
  90. package/core/EzModel.ts +268 -0
  91. package/core/EzTypes.ts +328 -0
  92. package/core/eventBus.ts +284 -0
  93. package/core/ez.ts +617 -0
  94. package/core/loader.ts +725 -0
  95. package/core/renderer.ts +1010 -0
  96. package/core/router.ts +490 -0
  97. package/core/services.ts +124 -0
  98. package/core/state.ts +142 -0
  99. package/core/utils.ts +81 -0
  100. package/package.json +51 -0
  101. package/services/RouteUI.js +17 -0
  102. package/services/crypto.js +64 -0
  103. package/services/dialog.js +222 -0
  104. package/services/fetchApi.js +63 -0
  105. package/services/firebase.js +30 -0
  106. package/services/toast.js +214 -0
  107. package/template/doc/EzDocs.js +15 -0
  108. package/template/doc/EzDocs.module.scss +627 -0
  109. package/template/doc/EzDocsController.js +164 -0
  110. package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
  111. package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
  112. package/template/doc/data/badge/EzBadgeDoc.js +92 -0
  113. package/template/doc/data/button/EzButtonDoc.js +77 -0
  114. package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
  115. package/template/doc/data/card/EzCardDoc.js +39 -0
  116. package/template/doc/data/chart/EzChartDoc.js +60 -0
  117. package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
  118. package/template/doc/data/component/EzComponentDoc.js +34 -0
  119. package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
  120. package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
  121. package/template/doc/data/dialog/EzDialogDoc.js +217 -0
  122. package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
  123. package/template/doc/data/form/EzFormDoc.js +90 -0
  124. package/template/doc/data/grid/EzGridDoc.js +99 -0
  125. package/template/doc/data/input/EzInputDoc.js +92 -0
  126. package/template/doc/data/label/EzLabelDoc.js +40 -0
  127. package/template/doc/data/model/EzModelDoc.js +53 -0
  128. package/template/doc/data/outlet/EzOutletDoc.js +63 -0
  129. package/template/doc/data/panel/EzPanelDoc.js +214 -0
  130. package/template/doc/data/radio/EzRadioDoc.js +174 -0
  131. package/template/doc/data/router/EzRouterDoc.js +75 -0
  132. package/template/doc/data/select/EzSelectDoc.js +37 -0
  133. package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
  134. package/template/doc/data/switch/EzSwitchDoc.js +82 -0
  135. package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
  136. package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
  137. package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
  138. package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
  139. package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
  140. package/template/doc/sidebar/EzDocsSidebar.js +32 -0
  141. package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
  142. package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
  143. package/template/doc/viewer/EzDocsViewer.js +18 -0
  144. package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
  145. package/template/doc/viewer/content/EzDocsContent.js +315 -0
  146. package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
  147. package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
  148. package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
  149. package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
  150. package/template/doc/welcome/EzDocsWelcome.js +48 -0
  151. package/themes/ez-theme.scss +179 -0
  152. package/themes/nature-fresh.scss +169 -0
  153. package/types/global.d.ts +21 -0
  154. package/utils/cssModules.js +81 -0
package/core/loader.ts ADDED
@@ -0,0 +1,725 @@
1
+ import { EzError } from './EzError.js';
2
+
3
+ export interface ModuleMeta {
4
+ path: string;
5
+ defines: string[];
6
+ }
7
+
8
+ export type ModuleMap = Record<string, () => Promise<unknown>>;
9
+
10
+ export type LoadPriority = 'high' | 'low' | 'idle';
11
+
12
+ export type NetworkStrategy = 'fast' | 'medium' | 'slow' | 'offline';
13
+
14
+ export interface PriorityQueueItem {
15
+ eztype: string;
16
+ priority: LoadPriority;
17
+ resolve: () => void;
18
+ reject: (err: Error) => void;
19
+ }
20
+
21
+ export interface RetryConfig {
22
+ maxRetries: number;
23
+ baseDelay: number;
24
+ maxDelay: number;
25
+ }
26
+
27
+ interface NetworkConnection {
28
+ effectiveType?: string;
29
+ saveData?: boolean;
30
+ addEventListener(event: string, handler: () => void): void;
31
+ }
32
+
33
+ interface NavigatorWithConnection extends Navigator {
34
+ connection?: NetworkConnection;
35
+ mozConnection?: NetworkConnection;
36
+ webkitConnection?: NetworkConnection;
37
+ }
38
+
39
+ interface RouteChainItem {
40
+ path: string;
41
+ view: string;
42
+ params: Record<string, string>;
43
+ }
44
+
45
+ interface RouteMatch {
46
+ chain: RouteChainItem[];
47
+ params: Record<string, string>;
48
+ fullPath: string;
49
+ }
50
+
51
+ interface EzRouter {
52
+ _routes: unknown[];
53
+ _matchRoute(path: string): RouteMatch | null;
54
+ }
55
+
56
+ interface EzInstance {
57
+ _registry: Record<string, unknown>;
58
+ _controllers: Record<string, unknown>;
59
+ _gridBehaviors: Record<string, unknown>;
60
+ _models: Record<string, unknown>;
61
+ _stores: Record<string, unknown>;
62
+ _context: {
63
+ route?: string | null;
64
+ view?: string | null;
65
+ };
66
+ _router?: EzRouter;
67
+ registerComponent(name: string, component: unknown): void;
68
+ }
69
+
70
+ export class EzLoader {
71
+ private ez: EzInstance;
72
+ private _modules: ModuleMap = {};
73
+ private _moduleIndex: Record<string, string[]> = {};
74
+ private _moduleMeta: Record<string, ModuleMeta> = {};
75
+ private _currentModulePath: string | null = null;
76
+ private _prefetched: Set<string> = new Set();
77
+ private _prefetchPromises: Map<string, Promise<void>> = new Map();
78
+ private _lowPriorityQueue: PriorityQueueItem[] = [];
79
+ private _idlePriorityQueue: PriorityQueueItem[] = [];
80
+ private _processingQueue: boolean = false;
81
+ private _idleCallbackId: number | null = null;
82
+ private _networkStrategy: NetworkStrategy = 'fast';
83
+ private _networkAware: boolean = false;
84
+ private _retryConfig: RetryConfig = {
85
+ maxRetries: 3,
86
+ baseDelay: 1000,
87
+ maxDelay: 10000
88
+ };
89
+ private _retryAttempts: Map<string, number> = new Map();
90
+ private _failedModules: Set<string> = new Set();
91
+
92
+ constructor(ez: EzInstance) {
93
+ this.ez = ez;
94
+ this._initNetworkDetection();
95
+ }
96
+
97
+ private _initNetworkDetection(): void {
98
+ this._updateNetworkStrategy();
99
+
100
+ const nav = navigator as NavigatorWithConnection;
101
+ const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
102
+
103
+ if (connection) {
104
+ connection.addEventListener('change', () => {
105
+ this._updateNetworkStrategy();
106
+ });
107
+ }
108
+
109
+ window.addEventListener('online', () => this._updateNetworkStrategy());
110
+ window.addEventListener('offline', () => this._updateNetworkStrategy());
111
+ }
112
+
113
+ private _updateNetworkStrategy(): void {
114
+ if (!navigator.onLine) {
115
+ this._networkStrategy = 'offline';
116
+ return;
117
+ }
118
+
119
+ const nav = navigator as NavigatorWithConnection;
120
+ const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
121
+
122
+ if (!connection) {
123
+ this._networkStrategy = 'fast';
124
+ return;
125
+ }
126
+
127
+ const effectiveType = connection.effectiveType;
128
+ const saveData = connection.saveData;
129
+
130
+ if (saveData) {
131
+ this._networkStrategy = 'slow';
132
+ return;
133
+ }
134
+
135
+ switch (effectiveType) {
136
+ case 'slow-2g':
137
+ case '2g':
138
+ this._networkStrategy = 'slow';
139
+ break;
140
+ case '3g':
141
+ this._networkStrategy = 'medium';
142
+ break;
143
+ case '4g':
144
+ default:
145
+ this._networkStrategy = 'fast';
146
+ break;
147
+ }
148
+ }
149
+
150
+ setNetworkAware(enabled: boolean): void {
151
+ this._networkAware = enabled;
152
+ }
153
+
154
+ getNetworkStrategy(): NetworkStrategy {
155
+ return this._networkStrategy;
156
+ }
157
+
158
+ canPrefetch(): boolean {
159
+ if (!this._networkAware) return true;
160
+ return this._networkStrategy === 'fast' || this._networkStrategy === 'medium';
161
+ }
162
+
163
+ adjustPriorityForNetwork(requestedPriority: LoadPriority): LoadPriority {
164
+ if (!this._networkAware) return requestedPriority;
165
+
166
+ switch (this._networkStrategy) {
167
+ case 'offline':
168
+ return requestedPriority;
169
+
170
+ case 'slow':
171
+ if (requestedPriority === 'low') return 'idle';
172
+ return requestedPriority;
173
+
174
+ case 'medium':
175
+ return requestedPriority;
176
+
177
+ case 'fast':
178
+ default:
179
+ return requestedPriority;
180
+ }
181
+ }
182
+
183
+ getNetworkInfo(): { strategy: NetworkStrategy; online: boolean; effectiveType: string | null; saveData: boolean } {
184
+ const nav = navigator as NavigatorWithConnection;
185
+ const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
186
+
187
+ return {
188
+ strategy: this._networkStrategy,
189
+ online: navigator.onLine,
190
+ effectiveType: connection?.effectiveType || null,
191
+ saveData: connection?.saveData || false
192
+ };
193
+ }
194
+
195
+ installModules(mods: ModuleMap): void {
196
+ this._modules = mods;
197
+ this._moduleIndex = {};
198
+
199
+ for (const path of Object.keys(mods)) {
200
+ this._moduleMeta[path] = {
201
+ path,
202
+ defines: []
203
+ };
204
+ const file = path.split('/').pop();
205
+ if (!file || (!file.endsWith('.js') && !file.endsWith('.ts'))) continue;
206
+
207
+ const eztype = file.replace(/\.(js|ts)$/, '');
208
+
209
+ this._moduleIndex[eztype] ??= [];
210
+ this._moduleIndex[eztype].push(path);
211
+ }
212
+
213
+ for (const eztype in this._moduleIndex) {
214
+ const paths = this._moduleIndex[eztype];
215
+ if (paths.length > 1) {
216
+ throw new EzError({
217
+ code: 'EZ_MODULE_DUPLICATE',
218
+ source: 'module',
219
+ message: `Multiple modules resolve to eztype "${eztype}"`,
220
+ details: {
221
+ eztype,
222
+ modules: paths
223
+ }
224
+ });
225
+ }
226
+ }
227
+ }
228
+
229
+ async loadModule(path: string): Promise<unknown> {
230
+ this._currentModulePath = path;
231
+ const mod = await this._modules[path]();
232
+ this._currentModulePath = null;
233
+ return mod;
234
+ }
235
+
236
+ getCurrentModulePath(): string | null {
237
+ return this._currentModulePath;
238
+ }
239
+
240
+ getModules(): ModuleMap {
241
+ return this._modules;
242
+ }
243
+
244
+ getModuleMeta(): Record<string, ModuleMeta> {
245
+ return this._moduleMeta;
246
+ }
247
+
248
+ async resolveEzType(eztype: string): Promise<void> {
249
+ if (this.ez._registry[eztype]) return;
250
+
251
+ const lowerEztype = eztype.toLowerCase();
252
+ const keys = Object.keys(this._modules);
253
+
254
+ const path = keys.find(p => {
255
+ const filename = p.split(/[/\\]/).pop()?.toLowerCase() || '';
256
+ return filename === `${lowerEztype}.js` || filename === `${lowerEztype}.ts`;
257
+ });
258
+
259
+ if (!path) {
260
+ return;
261
+ }
262
+
263
+ const module = await this.loadModuleWithRetry(path) as Record<string, unknown>;
264
+
265
+ let exported = module.default || Object.values(module).find(v => typeof v === 'function');
266
+
267
+ if (!exported) {
268
+ exported = this.ez._registry[eztype] as Record<string, unknown> | undefined;
269
+ }
270
+
271
+ if (!exported) {
272
+ throw new Error(`[ez] Module loaded for ${eztype} but exports no class/function or ez.define`);
273
+ }
274
+
275
+ this.ez.registerComponent(eztype, exported);
276
+ }
277
+
278
+ async resolveController(name: string | object | null): Promise<unknown> {
279
+ if (!name) return null;
280
+
281
+ if (typeof name === 'object') {
282
+ return name;
283
+ }
284
+
285
+ if (this.ez._controllers[name]) return this.ez._controllers[name];
286
+
287
+ const controllerName = name.endsWith('Controller') ? name : `${name}Controller`;
288
+
289
+ if (this.ez._controllers[controllerName]) {
290
+ if (name !== controllerName) {
291
+ this.ez._controllers[name] = this.ez._controllers[controllerName];
292
+ }
293
+ return this.ez._controllers[controllerName];
294
+ }
295
+
296
+ const lower = controllerName.toLowerCase();
297
+
298
+ const path = Object.keys(this._modules).find(p => {
299
+ const filename = p.split('/').pop()?.toLowerCase() || '';
300
+ return filename === `${lower}.js` || filename === `${lower}.ts`;
301
+ });
302
+
303
+ if (!path) {
304
+ throw new EzError({
305
+ code: 'EZ_CONTROLLER_404',
306
+ source: 'controller',
307
+ message: `Controller "${controllerName}" not found`,
308
+ route: this.ez._context.route,
309
+ view: this.ez._context.view,
310
+ details: {
311
+ requested: name,
312
+ inferred: controllerName,
313
+ available: Object.keys(this.ez._controllers)
314
+ }
315
+ });
316
+ }
317
+
318
+ await this.loadModuleWithRetry(path);
319
+
320
+ if (name !== controllerName && this.ez._controllers[controllerName]) {
321
+ this.ez._controllers[name] = this.ez._controllers[controllerName];
322
+ }
323
+
324
+ return this.ez._controllers[controllerName];
325
+ }
326
+
327
+ async resolveGridBehavior(name: string): Promise<void> {
328
+ if (!name) return;
329
+ if (this.ez._gridBehaviors[name]) return;
330
+
331
+ const lower = name.toLowerCase();
332
+
333
+ const path = Object.keys(this._modules).find(
334
+ p => p.toLowerCase().includes(lower)
335
+ );
336
+
337
+ if (!path) {
338
+ throw new EzError({
339
+ code: 'EZ_GRID_BEHAVIOR_404',
340
+ source: 'grid',
341
+ message: `GridBehavior "${name}" not found`,
342
+ route: this.ez._context?.route,
343
+ view: this.ez._context?.view
344
+ });
345
+ }
346
+
347
+ await this.loadModuleWithRetry(path);
348
+ }
349
+
350
+ async resolveModel(name: string): Promise<void> {
351
+ if (!name) return;
352
+ if (this.ez._models[name]) return;
353
+
354
+ const lower = name.toLowerCase();
355
+
356
+ const path = Object.keys(this._modules).find(p => {
357
+ const filename = p.split('/').pop()?.toLowerCase() || '';
358
+ return filename === `${lower}.js` || filename === `${lower}.ts`;
359
+ });
360
+
361
+ if (!path) {
362
+ throw new EzError({
363
+ code: 'EZ_MODEL_404',
364
+ source: 'model',
365
+ message: `Model "${name}" not found`,
366
+ route: this.ez._context?.route,
367
+ view: this.ez._context?.view,
368
+ details: {
369
+ requested: name,
370
+ available: Object.keys(this.ez._models)
371
+ }
372
+ });
373
+ }
374
+
375
+ await this.loadModuleWithRetry(path);
376
+ }
377
+
378
+ async resolveStore(name: string): Promise<void> {
379
+ if (!name) return;
380
+ if (this.ez._stores[name]) return;
381
+
382
+ const lower = name.toLowerCase();
383
+
384
+ const path = Object.keys(this._modules).find(p => {
385
+ const filename = p.split('/').pop()?.toLowerCase() || '';
386
+ return filename === `${lower}.js` || filename === `${lower}.ts`;
387
+ });
388
+
389
+ if (!path) {
390
+ throw new EzError({
391
+ code: 'EZ_STORE_404',
392
+ source: 'store',
393
+ message: `Store "${name}" not found`,
394
+ route: this.ez._context?.route,
395
+ view: this.ez._context?.view,
396
+ details: {
397
+ requested: name,
398
+ available: Object.keys(this.ez._stores)
399
+ }
400
+ });
401
+ }
402
+
403
+ await this.loadModuleWithRetry(path);
404
+ }
405
+
406
+ async loadCss(path: string): Promise<void> {
407
+ if (!path) return;
408
+
409
+ if (document.querySelector(`link[data-ez-css="${path}"]`)) return;
410
+
411
+ const link = document.createElement('link');
412
+ link.rel = 'stylesheet';
413
+ link.href = path;
414
+ link.setAttribute('data-ez-css', path);
415
+ document.head.appendChild(link);
416
+ }
417
+
418
+ async prefetch(eztype: string): Promise<void> {
419
+ if (!this.canPrefetch()) return;
420
+
421
+ if (this.ez._registry[eztype]) return;
422
+
423
+ const path = this._findModulePath(eztype);
424
+ if (!path) return;
425
+
426
+ if (this._prefetched.has(path)) return;
427
+
428
+ if (this._prefetchPromises.has(path)) {
429
+ return this._prefetchPromises.get(path);
430
+ }
431
+
432
+ const idle = window.requestIdleCallback || ((cb: () => void) => setTimeout(cb, 1) as unknown as number);
433
+
434
+ const promise = new Promise<void>((resolve) => {
435
+ idle(async () => {
436
+ try {
437
+ await this.loadModule(path);
438
+ this._prefetched.add(path);
439
+ } catch {
440
+ // Silently fail - prefetch is best-effort
441
+ }
442
+ this._prefetchPromises.delete(path);
443
+ resolve();
444
+ });
445
+ });
446
+
447
+ this._prefetchPromises.set(path, promise);
448
+ return promise;
449
+ }
450
+
451
+ async prefetchAll(eztypes: string[]): Promise<void> {
452
+ await Promise.all(eztypes.map(t => this.prefetch(t)));
453
+ }
454
+
455
+ private _findModulePath(eztype: string): string | null {
456
+ const lowerEztype = eztype.toLowerCase();
457
+ const keys = Object.keys(this._modules);
458
+
459
+ return keys.find(p => {
460
+ const filename = p.split(/[/\\]/).pop()?.toLowerCase() || '';
461
+ return filename === `${lowerEztype}.js` || filename === `${lowerEztype}.ts`;
462
+ }) || null;
463
+ }
464
+
465
+ isPrefetched(eztype: string): boolean {
466
+ const path = this._findModulePath(eztype);
467
+ return path ? this._prefetched.has(path) : false;
468
+ }
469
+
470
+ async loadWithPriority(eztype: string, priority: LoadPriority = 'high'): Promise<void> {
471
+ if (this.ez._registry[eztype]) return;
472
+
473
+ const adjustedPriority = this.adjustPriorityForNetwork(priority);
474
+
475
+ if (adjustedPriority === 'high') {
476
+ return this.resolveEzType(eztype);
477
+ }
478
+
479
+ return new Promise((resolve, reject) => {
480
+ const item: PriorityQueueItem = { eztype, priority: adjustedPriority, resolve, reject };
481
+
482
+ if (adjustedPriority === 'low') {
483
+ this._lowPriorityQueue.push(item);
484
+ this._scheduleLowPriorityProcessing();
485
+ } else if (adjustedPriority === 'idle') {
486
+ this._idlePriorityQueue.push(item);
487
+ this._scheduleIdleProcessing();
488
+ }
489
+ });
490
+ }
491
+
492
+ private _scheduleLowPriorityProcessing(): void {
493
+ if (this._processingQueue) return;
494
+
495
+ setTimeout(() => this._processLowPriorityQueue(), 0);
496
+ }
497
+
498
+ private async _processLowPriorityQueue(): Promise<void> {
499
+ if (this._processingQueue) return;
500
+ if (this._lowPriorityQueue.length === 0) return;
501
+
502
+ this._processingQueue = true;
503
+
504
+ while (this._lowPriorityQueue.length > 0) {
505
+ const item = this._lowPriorityQueue.shift();
506
+ if (!item) continue;
507
+
508
+ try {
509
+ await this.resolveEzType(item.eztype);
510
+ item.resolve();
511
+ } catch (err) {
512
+ item.reject(err as Error);
513
+ }
514
+ }
515
+
516
+ this._processingQueue = false;
517
+ }
518
+
519
+ private _scheduleIdleProcessing(): void {
520
+ if (this._idleCallbackId !== null) return;
521
+
522
+ const idle = window.requestIdleCallback || ((cb: (deadline: IdleDeadline) => void) => setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 50) as unknown as number);
523
+
524
+ this._idleCallbackId = idle((deadline: IdleDeadline) => {
525
+ this._idleCallbackId = null;
526
+ this._processIdleQueue(deadline);
527
+ });
528
+ }
529
+
530
+ private async _processIdleQueue(deadline: IdleDeadline): Promise<void> {
531
+ while (this._idlePriorityQueue.length > 0 && deadline.timeRemaining() > 5) {
532
+ const item = this._idlePriorityQueue.shift();
533
+ if (!item) continue;
534
+
535
+ try {
536
+ await this.resolveEzType(item.eztype);
537
+ item.resolve();
538
+ } catch (err) {
539
+ item.reject(err as Error);
540
+ }
541
+ }
542
+
543
+ if (this._idlePriorityQueue.length > 0) {
544
+ this._scheduleIdleProcessing();
545
+ }
546
+ }
547
+
548
+ getPendingCount(): { low: number; idle: number } {
549
+ return {
550
+ low: this._lowPriorityQueue.length,
551
+ idle: this._idlePriorityQueue.length
552
+ };
553
+ }
554
+
555
+ setRetryConfig(config: Partial<RetryConfig>): void {
556
+ this._retryConfig = { ...this._retryConfig, ...config };
557
+ }
558
+
559
+ async loadModuleWithRetry(path: string): Promise<unknown> {
560
+ if (this._failedModules.has(path)) {
561
+ throw new Error(`Module "${path}" has permanently failed to load`);
562
+ }
563
+
564
+ const { maxRetries, baseDelay, maxDelay } = this._retryConfig;
565
+ let lastError: Error | undefined;
566
+
567
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
568
+ try {
569
+ this._retryAttempts.set(path, attempt);
570
+
571
+ const module = await this.loadModule(path);
572
+
573
+ this._retryAttempts.delete(path);
574
+ return module;
575
+
576
+ } catch (err) {
577
+ lastError = err as Error;
578
+
579
+ if (!navigator.onLine) {
580
+ throw err;
581
+ }
582
+
583
+ if (attempt >= maxRetries) {
584
+ this._failedModules.add(path);
585
+ this._retryAttempts.delete(path);
586
+ throw err;
587
+ }
588
+
589
+ const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
590
+
591
+ const jitter = delay * 0.2 * (Math.random() - 0.5);
592
+ const finalDelay = Math.round(delay + jitter);
593
+
594
+ await this._sleep(finalDelay);
595
+ }
596
+ }
597
+
598
+ throw lastError;
599
+ }
600
+
601
+ private _sleep(ms: number): Promise<void> {
602
+ return new Promise(resolve => setTimeout(resolve, ms));
603
+ }
604
+
605
+ getRetryStatus(path: string): { attempts: number; failed: boolean } | null {
606
+ if (this._failedModules.has(path)) {
607
+ return { attempts: this._retryConfig.maxRetries + 1, failed: true };
608
+ }
609
+
610
+ const attempts = this._retryAttempts.get(path);
611
+ if (attempts !== undefined) {
612
+ return { attempts, failed: false };
613
+ }
614
+
615
+ return null;
616
+ }
617
+
618
+ clearFailedModule(path: string): void {
619
+ this._failedModules.delete(path);
620
+ this._retryAttempts.delete(path);
621
+ }
622
+
623
+ clearAllFailedModules(): void {
624
+ this._failedModules.clear();
625
+ this._retryAttempts.clear();
626
+ }
627
+
628
+ getFailedModules(): string[] {
629
+ return [...this._failedModules];
630
+ }
631
+
632
+ addPreloadHint(eztype: string): boolean {
633
+ if (!this.canPrefetch()) return false;
634
+
635
+ if (this.ez._registry[eztype]) return false;
636
+
637
+ const path = this._findModulePath(eztype);
638
+ if (!path) return false;
639
+
640
+ return this._injectPreloadLink(path);
641
+ }
642
+
643
+ addPreloadHints(eztypes: string[]): number {
644
+ let count = 0;
645
+ for (const eztype of eztypes) {
646
+ if (this.addPreloadHint(eztype)) count++;
647
+ }
648
+ return count;
649
+ }
650
+
651
+ private _injectPreloadLink(path: string): boolean {
652
+ const selector = `link[rel="modulepreload"][href="${path}"]`;
653
+ if (document.querySelector(selector)) return false;
654
+
655
+ const link = document.createElement('link');
656
+ link.rel = 'modulepreload';
657
+ link.href = path;
658
+ link.setAttribute('data-ez-preload', 'true');
659
+
660
+ document.head.appendChild(link);
661
+ return true;
662
+ }
663
+
664
+ async addPreloadHintsForRoute(routePath: string): Promise<number> {
665
+ if (!this.canPrefetch()) return 0;
666
+
667
+ const router = this.ez._router;
668
+ if (!router) return 0;
669
+
670
+ const match = router._matchRoute(routePath.toLowerCase());
671
+ if (!match || !match.chain.length) return 0;
672
+
673
+ const views = match.chain.map(r => r.view).filter(Boolean);
674
+ return this.addPreloadHints(views);
675
+ }
676
+
677
+ async addPreloadHintsForAdjacentRoutes(): Promise<number> {
678
+ if (!this.canPrefetch()) return 0;
679
+
680
+ const router = this.ez._router;
681
+ if (!router || !router._routes) return 0;
682
+
683
+ const currentPath = window.location.pathname;
684
+ let count = 0;
685
+
686
+ interface RouteNode {
687
+ path: string;
688
+ view?: string;
689
+ children?: RouteNode[];
690
+ }
691
+
692
+ const collectAdjacent = (routes: RouteNode[], parentPath: string = ''): void => {
693
+ for (const route of routes) {
694
+ const fullPath = parentPath + route.path;
695
+
696
+ if (fullPath === currentPath) {
697
+ if (route.children) {
698
+ for (const child of route.children) {
699
+ if (child.view) {
700
+ if (this.addPreloadHint(child.view)) count++;
701
+ }
702
+ }
703
+ }
704
+ continue;
705
+ }
706
+
707
+ if (route.view) {
708
+ if (this.addPreloadHint(route.view)) count++;
709
+ }
710
+ }
711
+ };
712
+
713
+ collectAdjacent(router._routes as RouteNode[]);
714
+ return count;
715
+ }
716
+
717
+ clearPreloadHints(): void {
718
+ const links = document.querySelectorAll('link[data-ez-preload="true"]');
719
+ links.forEach(link => link.remove());
720
+ }
721
+
722
+ getPreloadHintCount(): number {
723
+ return document.querySelectorAll('link[data-ez-preload="true"]').length;
724
+ }
725
+ }