@kupola/kupola 1.2.0 → 1.3.1

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 (97) 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/dist/css/accessibility.css +119 -0
  8. package/dist/css/animations.css +224 -0
  9. package/dist/css/brand-themes.css +300 -0
  10. package/dist/css/colors_and_type.css +441 -0
  11. package/dist/css/components-ext.css +4165 -0
  12. package/dist/css/components.css +1483 -0
  13. package/dist/css/responsive.css +697 -0
  14. package/dist/css/scaffold.css +145 -0
  15. package/dist/css/states.css +316 -0
  16. package/dist/css/theme-dark.css +296 -0
  17. package/dist/css/theme-light.css +296 -0
  18. package/dist/css/utilities.css +171 -0
  19. package/dist/kupola.cjs.js +219 -161
  20. package/dist/kupola.cjs.js.map +1 -1
  21. package/dist/kupola.esm.js +6643 -5709
  22. package/dist/kupola.esm.js.map +1 -1
  23. package/dist/kupola.umd.js +219 -161
  24. package/dist/kupola.umd.js.map +1 -1
  25. package/dist/plugins/vite-plugin-kupola.js +120 -0
  26. package/dist/types/kupola.d.ts +421 -323
  27. package/js/calendar.js +334 -25
  28. package/js/carousel.js +182 -48
  29. package/js/collapse.js +148 -34
  30. package/js/color-picker.js +416 -108
  31. package/js/component.js +8 -19
  32. package/js/countdown.js +9 -8
  33. package/js/data-bind.js +73 -16
  34. package/js/datepicker.js +488 -110
  35. package/js/depends.js +710 -0
  36. package/js/dialog.js +4 -2
  37. package/js/drawer.js +172 -8
  38. package/js/dropdown.js +272 -17
  39. package/js/dynamic-tags.js +156 -40
  40. package/js/fileupload.js +9 -8
  41. package/js/form.js +280 -254
  42. package/js/global-events.js +281 -188
  43. package/js/heatmap.js +10 -7
  44. package/js/i18n.js +18 -10
  45. package/js/icons.js +141 -161
  46. package/js/image-preview.js +146 -2
  47. package/js/initializer.js +113 -71
  48. package/js/kupola-core.js +123 -45
  49. package/js/kupola-lifecycle.js +13 -11
  50. package/js/message.js +8 -1
  51. package/js/modal.js +207 -59
  52. package/js/notification.js +8 -1
  53. package/js/numberinput.js +9 -8
  54. package/js/pagination.js +263 -0
  55. package/js/registry.js +29 -12
  56. package/js/select.js +482 -27
  57. package/js/slide-captcha.js +11 -2
  58. package/js/slider.js +442 -25
  59. package/js/statcard.js +9 -7
  60. package/js/table.js +1210 -0
  61. package/js/tag.js +268 -14
  62. package/js/theme.js +14 -43
  63. package/js/timepicker.js +335 -66
  64. package/js/tooltip.js +317 -86
  65. package/js/utils.js +6 -2
  66. package/js/validation.js +6 -2
  67. package/js/virtual-list.js +11 -7
  68. package/js/web-components.js +288 -0
  69. package/package.json +77 -67
  70. package/plugins/vite-plugin-kupola.js +120 -0
  71. package/types/kupola.d.ts +421 -323
  72. package/CHANGELOG.md +0 -130
  73. package/INTEGRATION.md +0 -440
  74. package/PROJECT_SUMMARY.md +0 -312
  75. package/SKILL.md +0 -572
  76. package/dist/utils/utils/Kupola.cs +0 -77
  77. package/dist/utils/utils/Kupola.java +0 -104
  78. package/dist/utils/utils/kupola.go +0 -120
  79. package/dist/utils/utils/kupola.js +0 -63
  80. package/dist/utils/utils/kupola.py +0 -1392
  81. package/dist/utils/utils/kupola.rb +0 -69
  82. package/js/composition-api.js +0 -458
  83. package/js/error-handler.js +0 -181
  84. package/js/http.js +0 -419
  85. package/js/kupola-devtools.js +0 -598
  86. package/js/performance.js +0 -250
  87. package/js/router.js +0 -396
  88. package/js/security.js +0 -189
  89. package/js/test-utils.js +0 -251
  90. package/templates/base.html +0 -30
  91. package/templates/base_dashboard.html +0 -99
  92. package/utils/Kupola.cs +0 -77
  93. package/utils/Kupola.java +0 -104
  94. package/utils/kupola.go +0 -120
  95. package/utils/kupola.js +0 -63
  96. package/utils/kupola.py +0 -1392
  97. 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
+ }