@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.
- package/LICENSE +21 -0
- package/README.md +330 -286
- package/adapters/axios.d.ts +34 -0
- package/adapters/axios.js +122 -0
- package/adapters/navios-http.d.ts +110 -0
- package/adapters/navios-http.js +151 -0
- package/dist/css/accessibility.css +119 -0
- package/dist/css/animations.css +224 -0
- package/dist/css/brand-themes.css +300 -0
- package/dist/css/colors_and_type.css +441 -0
- package/dist/css/components-ext.css +4165 -0
- package/dist/css/components.css +1483 -0
- package/dist/css/responsive.css +697 -0
- package/dist/css/scaffold.css +145 -0
- package/dist/css/states.css +316 -0
- package/dist/css/theme-dark.css +296 -0
- package/dist/css/theme-light.css +296 -0
- package/dist/css/utilities.css +171 -0
- package/dist/kupola.cjs.js +219 -161
- package/dist/kupola.cjs.js.map +1 -1
- package/dist/kupola.esm.js +6643 -5709
- package/dist/kupola.esm.js.map +1 -1
- package/dist/kupola.umd.js +219 -161
- package/dist/kupola.umd.js.map +1 -1
- package/dist/plugins/vite-plugin-kupola.js +120 -0
- package/dist/types/kupola.d.ts +421 -323
- package/js/calendar.js +334 -25
- package/js/carousel.js +182 -48
- package/js/collapse.js +148 -34
- package/js/color-picker.js +416 -108
- package/js/component.js +8 -19
- package/js/countdown.js +9 -8
- package/js/data-bind.js +73 -16
- package/js/datepicker.js +488 -110
- package/js/depends.js +710 -0
- package/js/dialog.js +4 -2
- package/js/drawer.js +172 -8
- package/js/dropdown.js +272 -17
- package/js/dynamic-tags.js +156 -40
- package/js/fileupload.js +9 -8
- package/js/form.js +280 -254
- package/js/global-events.js +281 -188
- package/js/heatmap.js +10 -7
- package/js/i18n.js +18 -10
- package/js/icons.js +141 -161
- package/js/image-preview.js +146 -2
- package/js/initializer.js +113 -71
- package/js/kupola-core.js +123 -45
- package/js/kupola-lifecycle.js +13 -11
- package/js/message.js +8 -1
- package/js/modal.js +207 -59
- package/js/notification.js +8 -1
- package/js/numberinput.js +9 -8
- package/js/pagination.js +263 -0
- package/js/registry.js +29 -12
- package/js/select.js +482 -27
- package/js/slide-captcha.js +11 -2
- package/js/slider.js +442 -25
- package/js/statcard.js +9 -7
- package/js/table.js +1210 -0
- package/js/tag.js +268 -14
- package/js/theme.js +14 -43
- package/js/timepicker.js +335 -66
- package/js/tooltip.js +317 -86
- package/js/utils.js +6 -2
- package/js/validation.js +6 -2
- package/js/virtual-list.js +11 -7
- package/js/web-components.js +288 -0
- package/package.json +77 -67
- package/plugins/vite-plugin-kupola.js +120 -0
- package/types/kupola.d.ts +421 -323
- package/CHANGELOG.md +0 -130
- package/INTEGRATION.md +0 -440
- package/PROJECT_SUMMARY.md +0 -312
- package/SKILL.md +0 -572
- package/dist/utils/utils/Kupola.cs +0 -77
- package/dist/utils/utils/Kupola.java +0 -104
- package/dist/utils/utils/kupola.go +0 -120
- package/dist/utils/utils/kupola.js +0 -63
- package/dist/utils/utils/kupola.py +0 -1392
- package/dist/utils/utils/kupola.rb +0 -69
- package/js/composition-api.js +0 -458
- package/js/error-handler.js +0 -181
- package/js/http.js +0 -419
- package/js/kupola-devtools.js +0 -598
- package/js/performance.js +0 -250
- package/js/router.js +0 -396
- package/js/security.js +0 -189
- package/js/test-utils.js +0 -251
- package/templates/base.html +0 -30
- package/templates/base_dashboard.html +0 -99
- package/utils/Kupola.cs +0 -77
- package/utils/Kupola.java +0 -104
- package/utils/kupola.go +0 -120
- package/utils/kupola.js +0 -63
- package/utils/kupola.py +0 -1392
- 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
|
+
}
|