@kupola/kupola 1.2.0 → 1.4.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +330 -286
  3. package/adapters/axios.d.ts +34 -0
  4. package/adapters/axios.js +122 -0
  5. package/adapters/navios-http.d.ts +110 -0
  6. package/adapters/navios-http.js +151 -0
  7. package/css/components-ext.css +157 -0
  8. package/css/components.css +69 -0
  9. package/css/theme-dark.css +5 -5
  10. package/css/utilities.css +119 -1
  11. package/dist/css/accessibility.css +119 -0
  12. package/dist/css/animations.css +224 -0
  13. package/dist/css/brand-themes.css +300 -0
  14. package/dist/css/colors_and_type.css +441 -0
  15. package/dist/css/components-ext.css +4165 -0
  16. package/dist/css/components.css +1483 -0
  17. package/dist/css/kupola.css +1 -12
  18. package/dist/css/kupola.min.css +1 -0
  19. package/dist/css/responsive.css +697 -0
  20. package/dist/css/scaffold.css +145 -0
  21. package/dist/css/states.css +316 -0
  22. package/dist/css/theme-dark.css +296 -0
  23. package/dist/css/theme-light.css +296 -0
  24. package/dist/css/utilities.css +171 -0
  25. package/dist/icons.svg +284 -0
  26. package/dist/kupola.cjs.js +17409 -257
  27. package/dist/kupola.cjs.js.map +1 -1
  28. package/dist/kupola.css +9000 -0
  29. package/dist/kupola.esm.js +17183 -7597
  30. package/dist/kupola.esm.js.map +1 -1
  31. package/dist/kupola.min.css +1 -0
  32. package/dist/kupola.min.js +2 -0
  33. package/dist/kupola.min.js.map +1 -0
  34. package/dist/kupola.umd.js +17415 -257
  35. package/dist/kupola.umd.js.map +1 -1
  36. package/dist/plugins/vite-plugin-kupola.js +120 -0
  37. package/dist/types/kupola.d.ts +421 -323
  38. package/js/calendar.js +334 -25
  39. package/js/carousel.js +182 -48
  40. package/js/collapse.js +148 -34
  41. package/js/color-picker.js +416 -108
  42. package/js/component.js +8 -19
  43. package/js/countdown.js +9 -8
  44. package/js/data-bind.js +73 -16
  45. package/js/datepicker.js +488 -110
  46. package/js/depends.js +710 -0
  47. package/js/dialog.js +4 -2
  48. package/js/drawer.js +172 -8
  49. package/js/dropdown.js +272 -17
  50. package/js/dynamic-tags.js +156 -40
  51. package/js/fileupload.js +9 -8
  52. package/js/form.js +280 -254
  53. package/js/global-events.js +281 -188
  54. package/js/heatmap.js +10 -7
  55. package/js/i18n.js +18 -10
  56. package/js/icons.js +141 -161
  57. package/js/image-preview.js +146 -2
  58. package/js/initializer.js +151 -71
  59. package/js/kupola-core.js +123 -45
  60. package/js/kupola-lifecycle.js +13 -11
  61. package/js/message.js +8 -1
  62. package/js/modal.js +207 -59
  63. package/js/notification.js +8 -1
  64. package/js/numberinput.js +9 -8
  65. package/js/pagination.js +263 -0
  66. package/js/registry.js +29 -12
  67. package/js/select.js +482 -27
  68. package/js/slide-captcha.js +11 -2
  69. package/js/slider.js +442 -25
  70. package/js/statcard.js +9 -7
  71. package/js/table.js +1210 -0
  72. package/js/tag.js +268 -14
  73. package/js/theme-standalone.js +88 -0
  74. package/js/theme.js +14 -43
  75. package/js/timepicker.js +335 -66
  76. package/js/tooltip.js +317 -86
  77. package/js/utils.js +6 -2
  78. package/js/validation.js +6 -2
  79. package/js/virtual-list.js +11 -7
  80. package/js/web-components.js +288 -0
  81. package/package.json +81 -67
  82. package/plugins/vite-plugin-kupola.js +120 -0
  83. package/scripts/build-css.cjs +113 -0
  84. package/scripts/build-svg-sprite.cjs +107 -0
  85. package/types/kupola.d.ts +421 -323
  86. package/CHANGELOG.md +0 -130
  87. package/INTEGRATION.md +0 -440
  88. package/PROJECT_SUMMARY.md +0 -312
  89. package/SKILL.md +0 -572
  90. package/dist/utils/utils/Kupola.cs +0 -77
  91. package/dist/utils/utils/Kupola.java +0 -104
  92. package/dist/utils/utils/kupola.go +0 -120
  93. package/dist/utils/utils/kupola.js +0 -63
  94. package/dist/utils/utils/kupola.py +0 -1392
  95. package/dist/utils/utils/kupola.rb +0 -69
  96. package/js/composition-api.js +0 -458
  97. package/js/error-handler.js +0 -181
  98. package/js/http.js +0 -419
  99. package/js/kupola-devtools.js +0 -598
  100. package/js/performance.js +0 -250
  101. package/js/router.js +0 -396
  102. package/js/security.js +0 -189
  103. package/js/test-utils.js +0 -251
  104. package/templates/base.html +0 -30
  105. package/templates/base_dashboard.html +0 -99
  106. package/utils/Kupola.cs +0 -77
  107. package/utils/Kupola.java +0 -104
  108. package/utils/kupola.go +0 -120
  109. package/utils/kupola.js +0 -63
  110. package/utils/kupola.py +0 -1392
  111. package/utils/kupola.rb +0 -69
package/js/depends.js ADDED
@@ -0,0 +1,710 @@
1
+ import { ref } from './data-bind.js';
2
+
3
+ // ============================================================
4
+ // Scheduler - 批量更新、去重、微任务调度
5
+ // ============================================================
6
+
7
+ class Scheduler {
8
+ constructor() {
9
+ this._queue = new Set();
10
+ this._scheduled = false;
11
+ }
12
+
13
+ schedule(fn) {
14
+ this._queue.add(fn);
15
+ if (!this._scheduled) {
16
+ this._scheduled = true;
17
+ queueMicrotask(() => this._flush());
18
+ }
19
+ }
20
+
21
+ _flush() {
22
+ const tasks = Array.from(this._queue);
23
+ this._queue.clear();
24
+ this._scheduled = false;
25
+ // Deduplicate: same function only runs once
26
+ const seen = new Set();
27
+ for (const task of tasks) {
28
+ if (!seen.has(task)) {
29
+ seen.add(task);
30
+ try { task(); } catch (e) { console.error('[DependsScheduler]', e); }
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ const globalScheduler = new Scheduler();
37
+
38
+ // ============================================================
39
+ // Cache Manager - TTL + Stale-While-Revalidate
40
+ // ============================================================
41
+
42
+ class CacheEntry {
43
+ constructor(data, ttl) {
44
+ this.data = data;
45
+ this.createdAt = Date.now();
46
+ this.ttl = ttl; // ms
47
+ }
48
+
49
+ get isFresh() {
50
+ return Date.now() - this.createdAt < this.ttl;
51
+ }
52
+
53
+ get isStale() {
54
+ return !this.isFresh;
55
+ }
56
+ }
57
+
58
+ class CacheManager {
59
+ constructor() {
60
+ this._store = new Map();
61
+ }
62
+
63
+ get(key) {
64
+ const entry = this._store.get(key);
65
+ if (!entry) return null;
66
+ return entry;
67
+ }
68
+
69
+ set(key, data, ttl = 60000) {
70
+ this._store.set(key, new CacheEntry(data, ttl));
71
+ }
72
+
73
+ has(key) {
74
+ return this._store.has(key);
75
+ }
76
+
77
+ delete(key) {
78
+ this._store.delete(key);
79
+ }
80
+
81
+ clear() {
82
+ this._store.clear();
83
+ }
84
+
85
+ // Get data regardless of freshness (for stale-while-revalidate)
86
+ getStale(key) {
87
+ const entry = this._store.get(key);
88
+ return entry ? entry.data : null;
89
+ }
90
+ }
91
+
92
+ // ============================================================
93
+ // Error types
94
+ // ============================================================
95
+
96
+ class DependsError extends Error {
97
+ constructor(message, code, cause) {
98
+ super(message);
99
+ this.name = 'DependsError';
100
+ this.code = code;
101
+ this.cause = cause;
102
+ this.timestamp = Date.now();
103
+ }
104
+ }
105
+
106
+ // ============================================================
107
+ // HTTP Client Plugin System
108
+ // ============================================================
109
+
110
+ // Default: native fetch. Stored as direct function reference for performance
111
+ // (avoids property lookup on every request).
112
+ let _httpClientFetch = typeof globalThis !== 'undefined' && globalThis.fetch
113
+ ? globalThis.fetch.bind(globalThis)
114
+ : (typeof window !== 'undefined' && window.fetch ? window.fetch.bind(window) : null);
115
+
116
+ /**
117
+ * Configure a custom HTTP client for all useDeps/useQuery requests.
118
+ *
119
+ * The client must provide a `fetch` function that returns a Response-like object:
120
+ * { ok: boolean, status: number, headers: object, json(): Promise, text(): Promise }
121
+ *
122
+ * @param {Object} client - HTTP client object
123
+ * @param {Function} client.fetch - fetch function (url, options) => Promise<Response>
124
+ *
125
+ * @example
126
+ * // Use Axios
127
+ * import { configureHttpClient } from 'kupola';
128
+ * import { createAxiosAdapter } from 'kupola/adapters/axios';
129
+ * import axios from 'axios';
130
+ * configureHttpClient(createAxiosAdapter(axios));
131
+ */
132
+ export function configureHttpClient(client) {
133
+ if (!client || typeof client.fetch !== 'function') {
134
+ throw new TypeError('[Kupola] configureHttpClient: client must provide a fetch function');
135
+ }
136
+ _httpClientFetch = client.fetch.bind(client);
137
+ }
138
+
139
+ /**
140
+ * Get the current HTTP client fetch function.
141
+ * @returns {Function}
142
+ */
143
+ export function getHttpClient() {
144
+ return _httpClientFetch;
145
+ }
146
+
147
+ /**
148
+ * Reset HTTP client to native fetch (useful for testing).
149
+ */
150
+ export function resetHttpClient() {
151
+ _httpClientFetch = typeof globalThis !== 'undefined' && globalThis.fetch
152
+ ? globalThis.fetch.bind(globalThis)
153
+ : (typeof window !== 'undefined' && window.fetch ? window.fetch.bind(window) : null);
154
+ }
155
+
156
+ // ============================================================
157
+ // Data Source Adapters
158
+ // ============================================================
159
+
160
+ class DependsSource {
161
+ constructor(config, cacheManager) {
162
+ this.config = config;
163
+ this.cacheKey = config.cacheKey || String(config.source);
164
+ this.staleTime = config.staleTime ?? 60000;
165
+ this.cache = cacheManager;
166
+ this.subscribers = [];
167
+ this.pending = null;
168
+ this.retryCount = config.retry ?? 0;
169
+ this.retryDelay = config.retryDelay ?? 1000;
170
+ this.onError = config.onError || null;
171
+ }
172
+
173
+ subscribe(callback) {
174
+ this.subscribers.push(callback);
175
+ return () => {
176
+ const idx = this.subscribers.indexOf(callback);
177
+ if (idx > -1) this.subscribers.splice(idx, 1);
178
+ };
179
+ }
180
+
181
+ notify() {
182
+ // Use scheduler to batch notifications
183
+ globalScheduler.schedule(() => {
184
+ this.subscribers.forEach(cb => {
185
+ try { cb(); } catch (e) { console.error('[DependsSource.notify]', e); }
186
+ });
187
+ });
188
+ }
189
+
190
+ async fetch(params) {
191
+ throw new DependsError('Source fetch not implemented', 'NOT_IMPLEMENTED');
192
+ }
193
+
194
+ async getValue(params) {
195
+ // 1. Check fresh cache
196
+ const cached = this.cache.get(this.cacheKey);
197
+ if (cached && cached.isFresh) {
198
+ return cached.data;
199
+ }
200
+
201
+ // 2. Stale-while-revalidate: return stale data immediately, re-fetch in background
202
+ if (cached && cached.isStale) {
203
+ this._revalidate(params); // fire and forget
204
+ return cached.data;
205
+ }
206
+
207
+ // 3. No cache - fetch with retry
208
+ return this._fetchWithRetry(params);
209
+ }
210
+
211
+ async _fetchWithRetry(params, attempt = 0) {
212
+ try {
213
+ const result = await this.fetch(params);
214
+ this.cache.set(this.cacheKey, result, this.staleTime);
215
+ this.notify();
216
+ return result;
217
+ } catch (error) {
218
+ if (attempt < this.retryCount) {
219
+ const delay = this.retryDelay * Math.pow(2, attempt);
220
+ await new Promise(resolve => setTimeout(resolve, delay));
221
+ return this._fetchWithRetry(params, attempt + 1);
222
+ }
223
+ const depError = error instanceof DependsError
224
+ ? error
225
+ : new DependsError(error.message || 'Fetch failed', 'FETCH_ERROR', error);
226
+ if (this.onError) {
227
+ try { this.onError(depError); } catch (_) { /* ignore handler errors */ }
228
+ }
229
+ throw depError;
230
+ }
231
+ }
232
+
233
+ async _revalidate(params) {
234
+ try {
235
+ await this._fetchWithRetry(params);
236
+ } catch (_) {
237
+ // Silently fail for background revalidation
238
+ }
239
+ }
240
+
241
+ invalidate() {
242
+ this.cache.delete(this.cacheKey);
243
+ this.pending = null;
244
+ }
245
+
246
+ destroy() {
247
+ this.subscribers = [];
248
+ this.pending = null;
249
+ }
250
+ }
251
+
252
+ // --- HTTP Source ---
253
+ class FetchedSource extends DependsSource {
254
+ constructor(config, cacheManager) {
255
+ super(config, cacheManager);
256
+ this.method = config.method || 'GET';
257
+ this.headers = config.headers || {};
258
+ this.queryParams = config.query || {};
259
+ }
260
+
261
+ async fetch(params) {
262
+ let url = this.config.source;
263
+
264
+ // Replace path params (:id -> value)
265
+ for (const key in params) {
266
+ url = url.replace(`:${key}`, encodeURIComponent(params[key]));
267
+ }
268
+
269
+ // Build query string
270
+ const queryParts = [];
271
+ for (const [k, v] of Object.entries(this.queryParams || {})) {
272
+ queryParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
273
+ }
274
+ for (const key in params) {
275
+ if (!this.config.source.includes(`:${key}`)) {
276
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
277
+ }
278
+ }
279
+ if (queryParts.length > 0) {
280
+ url += (url.includes('?') ? '&' : '?') + queryParts.join('&');
281
+ }
282
+
283
+ const fetchOptions = {
284
+ method: this.method.toUpperCase(),
285
+ headers: { 'Content-Type': 'application/json', ...this.headers }
286
+ };
287
+
288
+ if (['POST', 'PUT', 'PATCH'].includes(fetchOptions.method)) {
289
+ fetchOptions.body = JSON.stringify(params);
290
+ }
291
+
292
+ // Use configurable HTTP client (defaults to native fetch)
293
+ const clientFetch = _httpClientFetch;
294
+ if (!clientFetch) {
295
+ throw new DependsError('No HTTP client available. Use configureHttpClient() to set one.', 'NO_HTTP_CLIENT');
296
+ }
297
+
298
+ const response = await clientFetch(url, fetchOptions);
299
+
300
+ // Normalize response: support both native Response and adapter format
301
+ const ok = typeof response.ok === 'boolean' ? response.ok : (response.status >= 200 && response.status < 300);
302
+ const status = typeof response.status === 'number' ? response.status : 0;
303
+
304
+ if (!ok) {
305
+ throw new DependsError(`HTTP ${status}`, 'HTTP_ERROR');
306
+ }
307
+
308
+ // Support both .json() method and plain objects (for adapters that pre-parse)
309
+ const data = typeof response.json === 'function'
310
+ ? await response.json()
311
+ : response.data !== undefined ? response.data : response;
312
+ return data;
313
+ }
314
+ }
315
+
316
+ // --- localStorage Source ---
317
+ class StorageSource extends DependsSource {
318
+ constructor(config, cacheManager) {
319
+ super(config, cacheManager);
320
+ this.storageKey = config.source.replace('localStorage:', '');
321
+ this.defaultValue = config.default;
322
+ this.sync = config.sync !== false; // multi-tab sync by default
323
+
324
+ if (this.sync && typeof window !== 'undefined') {
325
+ this._storageHandler = (e) => {
326
+ if (e.key === this.storageKey) {
327
+ this.cache.delete(this.cacheKey);
328
+ this.notify();
329
+ }
330
+ };
331
+ window.addEventListener('storage', this._storageHandler);
332
+ }
333
+ }
334
+
335
+ async fetch() {
336
+ try {
337
+ const value = localStorage.getItem(this.storageKey);
338
+ if (value === null) return this.defaultValue;
339
+ // Try to parse JSON
340
+ try { return JSON.parse(value); } catch (_) { return value; }
341
+ } catch (e) {
342
+ return this.defaultValue;
343
+ }
344
+ }
345
+
346
+ setValue(value) {
347
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
348
+ localStorage.setItem(this.storageKey, serialized);
349
+ this.cache.delete(this.cacheKey);
350
+ this.notify();
351
+ }
352
+
353
+ destroy() {
354
+ super.destroy();
355
+ if (this._storageHandler) {
356
+ window.removeEventListener('storage', this._storageHandler);
357
+ }
358
+ }
359
+ }
360
+
361
+ // --- Route Source ---
362
+ class RouteSource extends DependsSource {
363
+ constructor(config, cacheManager) {
364
+ super(config, cacheManager);
365
+ this.paramName = config.source.replace('route:', '');
366
+ }
367
+
368
+ async fetch() {
369
+ if (typeof window === 'undefined') return '';
370
+ // Support both hash and history mode
371
+ const hash = location.hash.slice(1);
372
+ const match = hash.match(new RegExp(`/${this.paramName}/([^/]+)`));
373
+ if (match) return decodeURIComponent(match[1]);
374
+
375
+ // Fallback to search params
376
+ const urlParams = new URLSearchParams(location.search);
377
+ return urlParams.get(this.paramName) || '';
378
+ }
379
+ }
380
+
381
+ // --- Function Source ---
382
+ class FunctionSource extends DependsSource {
383
+ async fetch(params) {
384
+ const result = await this.config.source(params);
385
+ return result;
386
+ }
387
+ }
388
+
389
+ // --- Static Source ---
390
+ class StaticSource extends DependsSource {
391
+ async fetch() {
392
+ return this.config.source;
393
+ }
394
+ }
395
+
396
+ // --- WebSocket Source ---
397
+ class WebSocketSource extends DependsSource {
398
+ constructor(config, cacheManager) {
399
+ super(config, cacheManager);
400
+ this.ws = null;
401
+ this.reconnect = config.reconnect !== false;
402
+ this.reconnectDelay = config.reconnectDelay || 3000;
403
+ this.messageHandler = null;
404
+ this._connected = false;
405
+ }
406
+
407
+ async fetch() {
408
+ return new Promise((resolve, reject) => {
409
+ try {
410
+ this.ws = new WebSocket(this.config.source);
411
+
412
+ this.ws.onopen = () => {
413
+ this._connected = true;
414
+ // Resolve with current cached data or null
415
+ resolve(this.cache.getStale(this.cacheKey));
416
+ };
417
+
418
+ this.messageHandler = (event) => {
419
+ let data;
420
+ try { data = JSON.parse(event.data); } catch (_) { data = event.data; }
421
+ this.cache.set(this.cacheKey, data, this.staleTime);
422
+ this.notify();
423
+ };
424
+
425
+ this.ws.onmessage = this.messageHandler;
426
+
427
+ this.ws.onerror = (error) => {
428
+ if (!this._connected) {
429
+ reject(new DependsError('WebSocket connection failed', 'WS_ERROR', error));
430
+ }
431
+ };
432
+
433
+ this.ws.onclose = () => {
434
+ this._connected = false;
435
+ if (this.reconnect) {
436
+ setTimeout(() => {
437
+ this.fetch().catch(() => {});
438
+ }, this.reconnectDelay);
439
+ }
440
+ };
441
+ } catch (e) {
442
+ reject(new DependsError('WebSocket creation failed', 'WS_ERROR', e));
443
+ }
444
+ });
445
+ }
446
+
447
+ send(data) {
448
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
449
+ this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
450
+ }
451
+ }
452
+
453
+ destroy() {
454
+ super.destroy();
455
+ if (this.ws) {
456
+ this.ws.onmessage = null;
457
+ this.ws.onclose = null;
458
+ this.ws.close();
459
+ this.ws = null;
460
+ }
461
+ }
462
+ }
463
+
464
+ // ============================================================
465
+ // Source Factory
466
+ // ============================================================
467
+
468
+ function createSource(config, cacheManager) {
469
+ const source = config.source;
470
+ if (typeof source === 'function') {
471
+ return new FunctionSource(config, cacheManager);
472
+ } else if (typeof source === 'string' && (source.startsWith('ws://') || source.startsWith('wss://'))) {
473
+ return new WebSocketSource(config, cacheManager);
474
+ } else if (typeof source === 'string' && (source.startsWith('/') || source.startsWith('http'))) {
475
+ return new FetchedSource(config, cacheManager);
476
+ } else if (typeof source === 'string' && source.startsWith('localStorage:')) {
477
+ return new StorageSource(config, cacheManager);
478
+ } else if (typeof source === 'string' && source.startsWith('route:')) {
479
+ return new RouteSource(config, cacheManager);
480
+ } else {
481
+ return new StaticSource(config, cacheManager);
482
+ }
483
+ }
484
+
485
+ // ============================================================
486
+ // useDeps - Core API
487
+ // ============================================================
488
+
489
+ export function useDeps(props, depsConfig) {
490
+ const result = {};
491
+ const cacheManager = new CacheManager();
492
+ const unsubscribers = [];
493
+
494
+ for (const key in depsConfig) {
495
+ let config = depsConfig[key];
496
+ if (typeof config === 'string') {
497
+ config = { source: config };
498
+ }
499
+
500
+ const depConfig = {
501
+ ...config,
502
+ cacheKey: config.cacheKey || `${key}-${JSON.stringify(_extractPropValues(props))}`
503
+ };
504
+
505
+ const source = createSource(depConfig, cacheManager);
506
+ const data = ref(null);
507
+ const loading = ref(true);
508
+ const error = ref(null);
509
+ const lastUpdated = ref(null);
510
+ let _loadId = 0; // dedup concurrent loads
511
+
512
+ function getParams() {
513
+ return _extractPropValues(props);
514
+ }
515
+
516
+ async function load() {
517
+ const thisLoadId = ++_loadId;
518
+ loading.value = true;
519
+ error.value = null;
520
+ try {
521
+ const value = await source.getValue(getParams());
522
+ if (thisLoadId !== _loadId) return; // stale load
523
+ data.value = value;
524
+ lastUpdated.value = Date.now();
525
+ } catch (e) {
526
+ if (thisLoadId !== _loadId) return;
527
+ error.value = e.message || 'Unknown error';
528
+ } finally {
529
+ if (thisLoadId === _loadId) {
530
+ loading.value = false;
531
+ }
532
+ }
533
+ }
534
+
535
+ // Initial load
536
+ load();
537
+
538
+ // Subscribe to source notifications (e.g. WebSocket messages, storage events)
539
+ const unsubscribeSource = source.subscribe(() => {
540
+ const cached = cacheManager.getStale(source.cacheKey);
541
+ if (cached !== null && cached !== undefined) {
542
+ data.value = cached;
543
+ lastUpdated.value = Date.now();
544
+ }
545
+ });
546
+ unsubscribers.push(unsubscribeSource);
547
+
548
+ // Watch prop changes -> invalidate & reload
549
+ // Use direct ref subscription (works outside setup context)
550
+ const propKeys = Object.keys(props);
551
+ if (propKeys.length > 0) {
552
+ propKeys.forEach(k => {
553
+ const prop = props[k];
554
+ if (prop && typeof prop === 'object' && 'value' in prop && prop._subscribers) {
555
+ const handler = () => {
556
+ source.invalidate();
557
+ load();
558
+ };
559
+ prop._subscribers.add(handler);
560
+ unsubscribers.push(() => prop._subscribers.delete(handler));
561
+ }
562
+ });
563
+ }
564
+
565
+ result[key] = {
566
+ data,
567
+ loading,
568
+ error,
569
+ lastUpdated,
570
+ refresh() {
571
+ source.invalidate();
572
+ return load();
573
+ },
574
+ // For StorageSource
575
+ setValue(value) {
576
+ if (source instanceof StorageSource) {
577
+ source.setValue(value);
578
+ // Also update data ref immediately
579
+ data.value = value;
580
+ }
581
+ },
582
+ // For WebSocketSource
583
+ send(msg) {
584
+ if (source instanceof WebSocketSource) {
585
+ source.send(msg);
586
+ }
587
+ },
588
+ _source: source
589
+ };
590
+ }
591
+
592
+ // Cleanup helper
593
+ result._dispose = () => {
594
+ unsubscribers.forEach(fn => fn());
595
+ // Remove from DevTools tracking
596
+ if (window.__kupolaDepInstances) {
597
+ const idx = window.__kupolaDepInstances.indexOf(result);
598
+ if (idx !== -1) window.__kupolaDepInstances.splice(idx, 1);
599
+ }
600
+ };
601
+
602
+ // Register for DevTools tracking
603
+ if (!window.__kupolaDepInstances) window.__kupolaDepInstances = [];
604
+ window.__kupolaDepInstances.push(result);
605
+
606
+ return result;
607
+ }
608
+
609
+ // ============================================================
610
+ // useQuery - Single dependency convenience API
611
+ // ============================================================
612
+
613
+ export function useQuery(config) {
614
+ const cacheManager = new CacheManager();
615
+ const source = createSource(config, cacheManager);
616
+ const data = ref(null);
617
+ const loading = ref(true);
618
+ const error = ref(null);
619
+ let _loadId = 0;
620
+
621
+ async function load() {
622
+ const thisLoadId = ++_loadId;
623
+ loading.value = true;
624
+ error.value = null;
625
+ try {
626
+ const value = await source.getValue(config.params || {});
627
+ if (thisLoadId !== _loadId) return;
628
+ data.value = value;
629
+ } catch (e) {
630
+ if (thisLoadId !== _loadId) return;
631
+ error.value = e.message || 'Unknown error';
632
+ } finally {
633
+ if (thisLoadId === _loadId) loading.value = false;
634
+ }
635
+ }
636
+
637
+ // Load immediately (works outside setup context)
638
+ load();
639
+
640
+ source.subscribe(() => {
641
+ const cached = cacheManager.getStale(source.cacheKey);
642
+ if (cached !== null && cached !== undefined) {
643
+ data.value = cached;
644
+ }
645
+ });
646
+
647
+ return {
648
+ data,
649
+ loading,
650
+ error,
651
+ refresh() {
652
+ source.invalidate();
653
+ return load();
654
+ }
655
+ };
656
+ }
657
+
658
+ // ============================================================
659
+ // Utilities
660
+ // ============================================================
661
+
662
+ function _extractPropValues(props) {
663
+ const result = {};
664
+ for (const key in props) {
665
+ const value = props[key];
666
+ if (value && typeof value === 'object' && 'value' in value) {
667
+ result[key] = value.value;
668
+ } else {
669
+ result[key] = value;
670
+ }
671
+ }
672
+ return result;
673
+ }
674
+
675
+ export function clearCache() {
676
+ // Global cache clear is no longer applicable; each useDeps has its own cache
677
+ }
678
+
679
+ // ============================================================
680
+ // Exports
681
+ // ============================================================
682
+
683
+ export {
684
+ Scheduler,
685
+ CacheManager,
686
+ CacheEntry,
687
+ DependsError,
688
+ DependsSource,
689
+ FetchedSource,
690
+ StorageSource,
691
+ RouteSource,
692
+ FunctionSource,
693
+ StaticSource,
694
+ WebSocketSource,
695
+ createSource
696
+ };
697
+
698
+ // HTTP Client Plugin System exports are above (configureHttpClient, getHttpClient, resetHttpClient)
699
+
700
+ if (typeof window !== 'undefined') {
701
+ window.useDeps = useDeps;
702
+ window.useQuery = useQuery;
703
+ window.clearCache = clearCache;
704
+ window.DependsError = DependsError;
705
+ window.CacheManager = CacheManager;
706
+ window.Scheduler = Scheduler;
707
+ window.configureHttpClient = configureHttpClient;
708
+ window.getHttpClient = getHttpClient;
709
+ window.resetHttpClient = resetHttpClient;
710
+ }