zero-query 1.0.9 → 1.2.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/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -0
  5. package/cli/commands/build.js +254 -216
  6. package/cli/commands/bundle.js +1228 -1183
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -167
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +7264 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6252
  81. package/dist/zquery.min.js +8 -601
  82. package/index.d.ts +570 -365
  83. package/index.js +311 -232
  84. package/package.json +76 -69
  85. package/src/component.js +1709 -1454
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -254
  93. package/src/router.js +843 -773
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -272
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1023
  115. package/tests/compare.test.js +497 -0
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -0
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -0
  121. package/tests/electron-features.test.js +864 -0
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -145
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
package/src/utils.js CHANGED
@@ -1,515 +1,515 @@
1
- /**
2
- * zQuery Utils - Common utility functions
3
- *
4
- * Quality-of-life helpers that every frontend project needs.
5
- * Attached to $ namespace for convenience.
6
- */
7
-
8
- // ---------------------------------------------------------------------------
9
- // Function utilities
10
- // ---------------------------------------------------------------------------
11
-
12
- /**
13
- * Debounce - delays execution until after `ms` of inactivity
14
- */
15
- export function debounce(fn, ms = 250) {
16
- let timer;
17
- const debounced = (...args) => {
18
- clearTimeout(timer);
19
- timer = setTimeout(() => fn(...args), ms);
20
- };
21
- debounced.cancel = () => clearTimeout(timer);
22
- return debounced;
23
- }
24
-
25
- /**
26
- * Throttle - limits execution to once per `ms`
27
- */
28
- export function throttle(fn, ms = 250) {
29
- let last = 0;
30
- let timer;
31
- return (...args) => {
32
- const now = Date.now();
33
- const remaining = ms - (now - last);
34
- clearTimeout(timer);
35
- if (remaining <= 0) {
36
- last = now;
37
- fn(...args);
38
- } else {
39
- timer = setTimeout(() => { last = Date.now(); fn(...args); }, remaining);
40
- }
41
- };
42
- }
43
-
44
- /**
45
- * Pipe - compose functions left-to-right
46
- */
47
- export function pipe(...fns) {
48
- return (input) => fns.reduce((val, fn) => fn(val), input);
49
- }
50
-
51
- /**
52
- * Once - function that only runs once
53
- */
54
- export function once(fn) {
55
- let called = false, result;
56
- return (...args) => {
57
- if (!called) { called = true; result = fn(...args); }
58
- return result;
59
- };
60
- }
61
-
62
- /**
63
- * Sleep - promise-based delay
64
- */
65
- export function sleep(ms) {
66
- return new Promise(resolve => setTimeout(resolve, ms));
67
- }
68
-
69
-
70
- // ---------------------------------------------------------------------------
71
- // String utilities
72
- // ---------------------------------------------------------------------------
73
-
74
- /**
75
- * Escape HTML entities
76
- */
77
- export function escapeHtml(str) {
78
- const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
79
- return String(str).replace(/[&<>"']/g, c => map[c]);
80
- }
81
-
82
- export function stripHtml(str) {
83
- return String(str).replace(/<[^>]*>/g, '');
84
- }
85
-
86
- /**
87
- * Template tag for auto-escaping interpolated values
88
- * Usage: $.html`<div>${userInput}</div>`
89
- */
90
- export function html(strings, ...values) {
91
- return strings.reduce((result, str, i) => {
92
- const val = values[i - 1];
93
- const escaped = (val instanceof TrustedHTML) ? val.toString() : escapeHtml(val ?? '');
94
- return result + escaped + str;
95
- });
96
- }
97
-
98
- /**
99
- * Mark HTML as trusted (skip escaping in $.html template)
100
- */
101
- export class TrustedHTML {
102
- constructor(html) { this._html = html; }
103
- toString() { return this._html; }
104
- }
105
-
106
- export function trust(htmlStr) {
107
- return new TrustedHTML(htmlStr);
108
- }
109
-
110
- /**
111
- * Generate UUID v4
112
- */
113
- export function uuid() {
114
- if (crypto?.randomUUID) return crypto.randomUUID();
115
- // Fallback using crypto.getRandomValues (wider support than randomUUID)
116
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
117
- const buf = new Uint8Array(1);
118
- crypto.getRandomValues(buf);
119
- const r = buf[0] & 15;
120
- return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
121
- });
122
- }
123
-
124
- /**
125
- * Kebab-case to camelCase
126
- */
127
- export function camelCase(str) {
128
- return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
129
- }
130
-
131
- /**
132
- * CamelCase to kebab-case
133
- */
134
- export function kebabCase(str) {
135
- return str
136
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
137
- .replace(/([a-z\d])([A-Z])/g, '$1-$2')
138
- .toLowerCase();
139
- }
140
-
141
-
142
- // ---------------------------------------------------------------------------
143
- // Object utilities
144
- // ---------------------------------------------------------------------------
145
-
146
- /**
147
- * Deep clone via structuredClone (handles circular refs, Dates, etc.).
148
- * Falls back to a manual deep clone that preserves Date, RegExp, Map, Set,
149
- * ArrayBuffer, TypedArrays, undefined values, and circular references.
150
- */
151
- export function deepClone(obj) {
152
- if (typeof structuredClone === 'function') return structuredClone(obj);
153
-
154
- const seen = new Map();
155
- function clone(val) {
156
- if (val === null || typeof val !== 'object') return val;
157
- if (seen.has(val)) return seen.get(val);
158
- if (val instanceof Date) return new Date(val.getTime());
159
- if (val instanceof RegExp) return new RegExp(val.source, val.flags);
160
- if (val instanceof Map) {
161
- const m = new Map();
162
- seen.set(val, m);
163
- val.forEach((v, k) => m.set(clone(k), clone(v)));
164
- return m;
165
- }
166
- if (val instanceof Set) {
167
- const s = new Set();
168
- seen.set(val, s);
169
- val.forEach(v => s.add(clone(v)));
170
- return s;
171
- }
172
- if (ArrayBuffer.isView(val)) return new val.constructor(val.buffer.slice(0));
173
- if (val instanceof ArrayBuffer) return val.slice(0);
174
- if (Array.isArray(val)) {
175
- const arr = [];
176
- seen.set(val, arr);
177
- for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
178
- return arr;
179
- }
180
- const result = Object.create(Object.getPrototypeOf(val));
181
- seen.set(val, result);
182
- for (const key of Object.keys(val)) result[key] = clone(val[key]);
183
- return result;
184
- }
185
- return clone(obj);
186
- }
187
-
188
- // Keys that must never be written through data-merge or path-set operations
189
- const _UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
190
-
191
- /**
192
- * Deep merge objects
193
- */
194
- export function deepMerge(target, ...sources) {
195
- const seen = new WeakSet();
196
- function merge(tgt, src) {
197
- if (seen.has(src)) return tgt;
198
- seen.add(src);
199
- for (const key of Object.keys(src)) {
200
- if (_UNSAFE_KEYS.has(key)) continue;
201
- if (src[key] && typeof src[key] === 'object' && !Array.isArray(src[key])) {
202
- if (!tgt[key] || typeof tgt[key] !== 'object') tgt[key] = {};
203
- merge(tgt[key], src[key]);
204
- } else {
205
- tgt[key] = src[key];
206
- }
207
- }
208
- return tgt;
209
- }
210
- for (const source of sources) merge(target, source);
211
- return target;
212
- }
213
-
214
- /**
215
- * Simple object equality check
216
- */
217
- export function isEqual(a, b, _seen) {
218
- if (a === b) return true;
219
- if (typeof a !== typeof b) return false;
220
- if (typeof a !== 'object' || a === null || b === null) return false;
221
- if (Array.isArray(a) !== Array.isArray(b)) return false;
222
- // Guard against circular references
223
- if (!_seen) _seen = new Set();
224
- if (_seen.has(a)) return true;
225
- _seen.add(a);
226
- const keysA = Object.keys(a);
227
- const keysB = Object.keys(b);
228
- if (keysA.length !== keysB.length) return false;
229
- return keysA.every(k => isEqual(a[k], b[k], _seen));
230
- }
231
-
232
-
233
- // ---------------------------------------------------------------------------
234
- // URL utilities
235
- // ---------------------------------------------------------------------------
236
-
237
- /**
238
- * Serialize object to URL query string
239
- */
240
- export function param(obj) {
241
- return new URLSearchParams(obj).toString();
242
- }
243
-
244
- /**
245
- * Parse URL query string to object
246
- */
247
- export function parseQuery(str) {
248
- return Object.fromEntries(new URLSearchParams(str));
249
- }
250
-
251
-
252
- // ---------------------------------------------------------------------------
253
- // Storage helpers (localStorage wrapper with JSON support)
254
- // ---------------------------------------------------------------------------
255
- export const storage = {
256
- get(key, fallback = null) {
257
- try {
258
- const raw = localStorage.getItem(key);
259
- return raw !== null ? JSON.parse(raw) : fallback;
260
- } catch {
261
- return fallback;
262
- }
263
- },
264
-
265
- set(key, value) {
266
- localStorage.setItem(key, JSON.stringify(value));
267
- },
268
-
269
- remove(key) {
270
- localStorage.removeItem(key);
271
- },
272
-
273
- clear() {
274
- localStorage.clear();
275
- },
276
- };
277
-
278
- export const session = {
279
- get(key, fallback = null) {
280
- try {
281
- const raw = sessionStorage.getItem(key);
282
- return raw !== null ? JSON.parse(raw) : fallback;
283
- } catch {
284
- return fallback;
285
- }
286
- },
287
-
288
- set(key, value) {
289
- sessionStorage.setItem(key, JSON.stringify(value));
290
- },
291
-
292
- remove(key) {
293
- sessionStorage.removeItem(key);
294
- },
295
-
296
- clear() {
297
- sessionStorage.clear();
298
- },
299
- };
300
-
301
-
302
- // ---------------------------------------------------------------------------
303
- // Event bus (pub/sub)
304
- // ---------------------------------------------------------------------------
305
- export class EventBus {
306
- constructor() { this._handlers = new Map(); }
307
-
308
- on(event, fn) {
309
- if (!this._handlers.has(event)) this._handlers.set(event, new Set());
310
- this._handlers.get(event).add(fn);
311
- return () => this.off(event, fn);
312
- }
313
-
314
- off(event, fn) {
315
- this._handlers.get(event)?.delete(fn);
316
- }
317
-
318
- emit(event, ...args) {
319
- this._handlers.get(event)?.forEach(fn => fn(...args));
320
- }
321
-
322
- once(event, fn) {
323
- const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
324
- return this.on(event, wrapper);
325
- }
326
-
327
- clear() { this._handlers.clear(); }
328
- }
329
-
330
- export const bus = new EventBus();
331
-
332
-
333
- // ---------------------------------------------------------------------------
334
- // Array utilities
335
- // ---------------------------------------------------------------------------
336
-
337
- export function range(startOrEnd, end, step) {
338
- let s, e, st;
339
- if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
340
- else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
341
- if (st === 0) return [];
342
- const result = [];
343
- if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
344
- else { for (let i = s; i > e; i += st) result.push(i); }
345
- return result;
346
- }
347
-
348
- export function unique(arr, keyFn) {
349
- if (!keyFn) return [...new Set(arr)];
350
- const seen = new Set();
351
- return arr.filter(item => {
352
- const k = keyFn(item);
353
- if (seen.has(k)) return false;
354
- seen.add(k);
355
- return true;
356
- });
357
- }
358
-
359
- export function chunk(arr, size) {
360
- const result = [];
361
- for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
362
- return result;
363
- }
364
-
365
- export function groupBy(arr, keyFn) {
366
- const result = {};
367
- for (const item of arr) {
368
- const k = keyFn(item);
369
- (result[k] ??= []).push(item);
370
- }
371
- return result;
372
- }
373
-
374
-
375
- // ---------------------------------------------------------------------------
376
- // Object utilities
377
- // ---------------------------------------------------------------------------
378
-
379
- export function pick(obj, keys) {
380
- const result = {};
381
- for (const k of keys) { if (k in obj) result[k] = obj[k]; }
382
- return result;
383
- }
384
-
385
- export function omit(obj, keys) {
386
- const exclude = new Set(keys);
387
- const result = {};
388
- for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
389
- return result;
390
- }
391
-
392
- export function getPath(obj, path, fallback) {
393
- const keys = path.split('.');
394
- let cur = obj;
395
- for (const k of keys) {
396
- if (cur == null || typeof cur !== 'object') return fallback;
397
- cur = cur[k];
398
- }
399
- return cur === undefined ? fallback : cur;
400
- }
401
-
402
- export function setPath(obj, path, value) {
403
- const keys = path.split('.');
404
- let cur = obj;
405
- for (let i = 0; i < keys.length - 1; i++) {
406
- const k = keys[i];
407
- if (_UNSAFE_KEYS.has(k)) return obj;
408
- if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
409
- cur = cur[k];
410
- }
411
- const lastKey = keys[keys.length - 1];
412
- if (_UNSAFE_KEYS.has(lastKey)) return obj;
413
- cur[lastKey] = value;
414
- return obj;
415
- }
416
-
417
- export function isEmpty(val) {
418
- if (val == null) return true;
419
- if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
420
- if (val instanceof Map || val instanceof Set) return val.size === 0;
421
- if (typeof val === 'object') return Object.keys(val).length === 0;
422
- return false;
423
- }
424
-
425
-
426
- // ---------------------------------------------------------------------------
427
- // String utilities
428
- // ---------------------------------------------------------------------------
429
-
430
- export function capitalize(str) {
431
- if (!str) return '';
432
- return str[0].toUpperCase() + str.slice(1).toLowerCase();
433
- }
434
-
435
- export function truncate(str, maxLen, suffix = '…') {
436
- if (str.length <= maxLen) return str;
437
- const end = Math.max(0, maxLen - suffix.length);
438
- return str.slice(0, end) + suffix;
439
- }
440
-
441
-
442
- // ---------------------------------------------------------------------------
443
- // Number utilities
444
- // ---------------------------------------------------------------------------
445
-
446
- export function clamp(val, min, max) {
447
- return val < min ? min : val > max ? max : val;
448
- }
449
-
450
-
451
- // ---------------------------------------------------------------------------
452
- // Function utilities
453
- // ---------------------------------------------------------------------------
454
-
455
- export function memoize(fn, keyFnOrOpts) {
456
- let keyFn, maxSize = 0;
457
- if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
458
- else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
459
-
460
- const cache = new Map();
461
-
462
- const memoized = (...args) => {
463
- const key = keyFn ? keyFn(...args) : args[0];
464
- if (cache.has(key)) {
465
- // LRU: promote to newest by re-inserting
466
- const value = cache.get(key);
467
- cache.delete(key);
468
- cache.set(key, value);
469
- return value;
470
- }
471
- const result = fn(...args);
472
- cache.set(key, result);
473
- // LRU eviction: drop the least-recently-used entry
474
- if (maxSize > 0 && cache.size > maxSize) {
475
- cache.delete(cache.keys().next().value);
476
- }
477
- return result;
478
- };
479
-
480
- memoized.clear = () => cache.clear();
481
- return memoized;
482
- }
483
-
484
-
485
- // ---------------------------------------------------------------------------
486
- // Async utilities
487
- // ---------------------------------------------------------------------------
488
-
489
- export function retry(fn, opts = {}) {
490
- const { attempts = 3, delay = 1000, backoff = 1 } = opts;
491
- return new Promise((resolve, reject) => {
492
- let attempt = 0, currentDelay = delay;
493
- const tryOnce = () => {
494
- attempt++;
495
- fn(attempt).then(resolve, (err) => {
496
- if (attempt >= attempts) return reject(err);
497
- const d = currentDelay;
498
- currentDelay *= backoff;
499
- setTimeout(tryOnce, d);
500
- });
501
- };
502
- tryOnce();
503
- });
504
- }
505
-
506
- export function timeout(promise, ms, message) {
507
- let timer;
508
- const race = Promise.race([
509
- promise,
510
- new Promise((_, reject) => {
511
- timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
512
- })
513
- ]);
514
- return race.finally(() => clearTimeout(timer));
515
- }
1
+ /**
2
+ * zQuery Utils - Common utility functions
3
+ *
4
+ * Quality-of-life helpers that every frontend project needs.
5
+ * Attached to $ namespace for convenience.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Function utilities
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /**
13
+ * Debounce - delays execution until after `ms` of inactivity
14
+ */
15
+ export function debounce(fn, ms = 250) {
16
+ let timer;
17
+ const debounced = (...args) => {
18
+ clearTimeout(timer);
19
+ timer = setTimeout(() => fn(...args), ms);
20
+ };
21
+ debounced.cancel = () => clearTimeout(timer);
22
+ return debounced;
23
+ }
24
+
25
+ /**
26
+ * Throttle - limits execution to once per `ms`
27
+ */
28
+ export function throttle(fn, ms = 250) {
29
+ let last = 0;
30
+ let timer;
31
+ return (...args) => {
32
+ const now = Date.now();
33
+ const remaining = ms - (now - last);
34
+ clearTimeout(timer);
35
+ if (remaining <= 0) {
36
+ last = now;
37
+ fn(...args);
38
+ } else {
39
+ timer = setTimeout(() => { last = Date.now(); fn(...args); }, remaining);
40
+ }
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Pipe - compose functions left-to-right
46
+ */
47
+ export function pipe(...fns) {
48
+ return (input) => fns.reduce((val, fn) => fn(val), input);
49
+ }
50
+
51
+ /**
52
+ * Once - function that only runs once
53
+ */
54
+ export function once(fn) {
55
+ let called = false, result;
56
+ return (...args) => {
57
+ if (!called) { called = true; result = fn(...args); }
58
+ return result;
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Sleep - promise-based delay
64
+ */
65
+ export function sleep(ms) {
66
+ return new Promise(resolve => setTimeout(resolve, ms));
67
+ }
68
+
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // String utilities
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /**
75
+ * Escape HTML entities
76
+ */
77
+ export function escapeHtml(str) {
78
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
79
+ return String(str).replace(/[&<>"']/g, c => map[c]);
80
+ }
81
+
82
+ export function stripHtml(str) {
83
+ return String(str).replace(/<[^>]*>/g, '');
84
+ }
85
+
86
+ /**
87
+ * Template tag for auto-escaping interpolated values
88
+ * Usage: $.html`<div>${userInput}</div>`
89
+ */
90
+ export function html(strings, ...values) {
91
+ return strings.reduce((result, str, i) => {
92
+ const val = values[i - 1];
93
+ const escaped = (val instanceof TrustedHTML) ? val.toString() : escapeHtml(val ?? '');
94
+ return result + escaped + str;
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Mark HTML as trusted (skip escaping in $.html template)
100
+ */
101
+ export class TrustedHTML {
102
+ constructor(html) { this._html = html; }
103
+ toString() { return this._html; }
104
+ }
105
+
106
+ export function trust(htmlStr) {
107
+ return new TrustedHTML(htmlStr);
108
+ }
109
+
110
+ /**
111
+ * Generate UUID v4
112
+ */
113
+ export function uuid() {
114
+ if (crypto?.randomUUID) return crypto.randomUUID();
115
+ // Fallback using crypto.getRandomValues (wider support than randomUUID)
116
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
117
+ const buf = new Uint8Array(1);
118
+ crypto.getRandomValues(buf);
119
+ const r = buf[0] & 15;
120
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Kebab-case to camelCase
126
+ */
127
+ export function camelCase(str) {
128
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
129
+ }
130
+
131
+ /**
132
+ * CamelCase to kebab-case
133
+ */
134
+ export function kebabCase(str) {
135
+ return str
136
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
137
+ .replace(/([a-z\d])([A-Z])/g, '$1-$2')
138
+ .toLowerCase();
139
+ }
140
+
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Object utilities
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /**
147
+ * Deep clone via structuredClone (handles circular refs, Dates, etc.).
148
+ * Falls back to a manual deep clone that preserves Date, RegExp, Map, Set,
149
+ * ArrayBuffer, TypedArrays, undefined values, and circular references.
150
+ */
151
+ export function deepClone(obj) {
152
+ if (typeof structuredClone === 'function') return structuredClone(obj);
153
+
154
+ const seen = new Map();
155
+ function clone(val) {
156
+ if (val === null || typeof val !== 'object') return val;
157
+ if (seen.has(val)) return seen.get(val);
158
+ if (val instanceof Date) return new Date(val.getTime());
159
+ if (val instanceof RegExp) return new RegExp(val.source, val.flags);
160
+ if (val instanceof Map) {
161
+ const m = new Map();
162
+ seen.set(val, m);
163
+ val.forEach((v, k) => m.set(clone(k), clone(v)));
164
+ return m;
165
+ }
166
+ if (val instanceof Set) {
167
+ const s = new Set();
168
+ seen.set(val, s);
169
+ val.forEach(v => s.add(clone(v)));
170
+ return s;
171
+ }
172
+ if (ArrayBuffer.isView(val)) return new val.constructor(val.buffer.slice(0));
173
+ if (val instanceof ArrayBuffer) return val.slice(0);
174
+ if (Array.isArray(val)) {
175
+ const arr = [];
176
+ seen.set(val, arr);
177
+ for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
178
+ return arr;
179
+ }
180
+ const result = Object.create(Object.getPrototypeOf(val));
181
+ seen.set(val, result);
182
+ for (const key of Object.keys(val)) result[key] = clone(val[key]);
183
+ return result;
184
+ }
185
+ return clone(obj);
186
+ }
187
+
188
+ // Keys that must never be written through data-merge or path-set operations
189
+ const _UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
190
+
191
+ /**
192
+ * Deep merge objects
193
+ */
194
+ export function deepMerge(target, ...sources) {
195
+ const seen = new WeakSet();
196
+ function merge(tgt, src) {
197
+ if (seen.has(src)) return tgt;
198
+ seen.add(src);
199
+ for (const key of Object.keys(src)) {
200
+ if (_UNSAFE_KEYS.has(key)) continue;
201
+ if (src[key] && typeof src[key] === 'object' && !Array.isArray(src[key])) {
202
+ if (!tgt[key] || typeof tgt[key] !== 'object') tgt[key] = {};
203
+ merge(tgt[key], src[key]);
204
+ } else {
205
+ tgt[key] = src[key];
206
+ }
207
+ }
208
+ return tgt;
209
+ }
210
+ for (const source of sources) merge(target, source);
211
+ return target;
212
+ }
213
+
214
+ /**
215
+ * Simple object equality check
216
+ */
217
+ export function isEqual(a, b, _seen) {
218
+ if (a === b) return true;
219
+ if (typeof a !== typeof b) return false;
220
+ if (typeof a !== 'object' || a === null || b === null) return false;
221
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
222
+ // Guard against circular references
223
+ if (!_seen) _seen = new Set();
224
+ if (_seen.has(a)) return true;
225
+ _seen.add(a);
226
+ const keysA = Object.keys(a);
227
+ const keysB = Object.keys(b);
228
+ if (keysA.length !== keysB.length) return false;
229
+ return keysA.every(k => isEqual(a[k], b[k], _seen));
230
+ }
231
+
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // URL utilities
235
+ // ---------------------------------------------------------------------------
236
+
237
+ /**
238
+ * Serialize object to URL query string
239
+ */
240
+ export function param(obj) {
241
+ return new URLSearchParams(obj).toString();
242
+ }
243
+
244
+ /**
245
+ * Parse URL query string to object
246
+ */
247
+ export function parseQuery(str) {
248
+ return Object.fromEntries(new URLSearchParams(str));
249
+ }
250
+
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Storage helpers (localStorage wrapper with JSON support)
254
+ // ---------------------------------------------------------------------------
255
+ export const storage = {
256
+ get(key, fallback = null) {
257
+ try {
258
+ const raw = localStorage.getItem(key);
259
+ return raw !== null ? JSON.parse(raw) : fallback;
260
+ } catch {
261
+ return fallback;
262
+ }
263
+ },
264
+
265
+ set(key, value) {
266
+ localStorage.setItem(key, JSON.stringify(value));
267
+ },
268
+
269
+ remove(key) {
270
+ localStorage.removeItem(key);
271
+ },
272
+
273
+ clear() {
274
+ localStorage.clear();
275
+ },
276
+ };
277
+
278
+ export const session = {
279
+ get(key, fallback = null) {
280
+ try {
281
+ const raw = sessionStorage.getItem(key);
282
+ return raw !== null ? JSON.parse(raw) : fallback;
283
+ } catch {
284
+ return fallback;
285
+ }
286
+ },
287
+
288
+ set(key, value) {
289
+ sessionStorage.setItem(key, JSON.stringify(value));
290
+ },
291
+
292
+ remove(key) {
293
+ sessionStorage.removeItem(key);
294
+ },
295
+
296
+ clear() {
297
+ sessionStorage.clear();
298
+ },
299
+ };
300
+
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // Event bus (pub/sub)
304
+ // ---------------------------------------------------------------------------
305
+ export class EventBus {
306
+ constructor() { this._handlers = new Map(); }
307
+
308
+ on(event, fn) {
309
+ if (!this._handlers.has(event)) this._handlers.set(event, new Set());
310
+ this._handlers.get(event).add(fn);
311
+ return () => this.off(event, fn);
312
+ }
313
+
314
+ off(event, fn) {
315
+ this._handlers.get(event)?.delete(fn);
316
+ }
317
+
318
+ emit(event, ...args) {
319
+ this._handlers.get(event)?.forEach(fn => fn(...args));
320
+ }
321
+
322
+ once(event, fn) {
323
+ const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
324
+ return this.on(event, wrapper);
325
+ }
326
+
327
+ clear() { this._handlers.clear(); }
328
+ }
329
+
330
+ export const bus = new EventBus();
331
+
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Array utilities
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export function range(startOrEnd, end, step) {
338
+ let s, e, st;
339
+ if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
340
+ else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
341
+ if (st === 0) return [];
342
+ const result = [];
343
+ if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
344
+ else { for (let i = s; i > e; i += st) result.push(i); }
345
+ return result;
346
+ }
347
+
348
+ export function unique(arr, keyFn) {
349
+ if (!keyFn) return [...new Set(arr)];
350
+ const seen = new Set();
351
+ return arr.filter(item => {
352
+ const k = keyFn(item);
353
+ if (seen.has(k)) return false;
354
+ seen.add(k);
355
+ return true;
356
+ });
357
+ }
358
+
359
+ export function chunk(arr, size) {
360
+ const result = [];
361
+ for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
362
+ return result;
363
+ }
364
+
365
+ export function groupBy(arr, keyFn) {
366
+ const result = {};
367
+ for (const item of arr) {
368
+ const k = keyFn(item);
369
+ (result[k] ??= []).push(item);
370
+ }
371
+ return result;
372
+ }
373
+
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // Object utilities
377
+ // ---------------------------------------------------------------------------
378
+
379
+ export function pick(obj, keys) {
380
+ const result = {};
381
+ for (const k of keys) { if (k in obj) result[k] = obj[k]; }
382
+ return result;
383
+ }
384
+
385
+ export function omit(obj, keys) {
386
+ const exclude = new Set(keys);
387
+ const result = {};
388
+ for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
389
+ return result;
390
+ }
391
+
392
+ export function getPath(obj, path, fallback) {
393
+ const keys = path.split('.');
394
+ let cur = obj;
395
+ for (const k of keys) {
396
+ if (cur == null || typeof cur !== 'object') return fallback;
397
+ cur = cur[k];
398
+ }
399
+ return cur === undefined ? fallback : cur;
400
+ }
401
+
402
+ export function setPath(obj, path, value) {
403
+ const keys = path.split('.');
404
+ let cur = obj;
405
+ for (let i = 0; i < keys.length - 1; i++) {
406
+ const k = keys[i];
407
+ if (_UNSAFE_KEYS.has(k)) return obj;
408
+ if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
409
+ cur = cur[k];
410
+ }
411
+ const lastKey = keys[keys.length - 1];
412
+ if (_UNSAFE_KEYS.has(lastKey)) return obj;
413
+ cur[lastKey] = value;
414
+ return obj;
415
+ }
416
+
417
+ export function isEmpty(val) {
418
+ if (val == null) return true;
419
+ if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
420
+ if (val instanceof Map || val instanceof Set) return val.size === 0;
421
+ if (typeof val === 'object') return Object.keys(val).length === 0;
422
+ return false;
423
+ }
424
+
425
+
426
+ // ---------------------------------------------------------------------------
427
+ // String utilities
428
+ // ---------------------------------------------------------------------------
429
+
430
+ export function capitalize(str) {
431
+ if (!str) return '';
432
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
433
+ }
434
+
435
+ export function truncate(str, maxLen, suffix = '…') {
436
+ if (str.length <= maxLen) return str;
437
+ const end = Math.max(0, maxLen - suffix.length);
438
+ return str.slice(0, end) + suffix;
439
+ }
440
+
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // Number utilities
444
+ // ---------------------------------------------------------------------------
445
+
446
+ export function clamp(val, min, max) {
447
+ return val < min ? min : val > max ? max : val;
448
+ }
449
+
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // Function utilities
453
+ // ---------------------------------------------------------------------------
454
+
455
+ export function memoize(fn, keyFnOrOpts) {
456
+ let keyFn, maxSize = 0;
457
+ if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
458
+ else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
459
+
460
+ const cache = new Map();
461
+
462
+ const memoized = (...args) => {
463
+ const key = keyFn ? keyFn(...args) : args[0];
464
+ if (cache.has(key)) {
465
+ // LRU: promote to newest by re-inserting
466
+ const value = cache.get(key);
467
+ cache.delete(key);
468
+ cache.set(key, value);
469
+ return value;
470
+ }
471
+ const result = fn(...args);
472
+ cache.set(key, result);
473
+ // LRU eviction: drop the least-recently-used entry
474
+ if (maxSize > 0 && cache.size > maxSize) {
475
+ cache.delete(cache.keys().next().value);
476
+ }
477
+ return result;
478
+ };
479
+
480
+ memoized.clear = () => cache.clear();
481
+ return memoized;
482
+ }
483
+
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Async utilities
487
+ // ---------------------------------------------------------------------------
488
+
489
+ export function retry(fn, opts = {}) {
490
+ const { attempts = 3, delay = 1000, backoff = 1 } = opts;
491
+ return new Promise((resolve, reject) => {
492
+ let attempt = 0, currentDelay = delay;
493
+ const tryOnce = () => {
494
+ attempt++;
495
+ fn(attempt).then(resolve, (err) => {
496
+ if (attempt >= attempts) return reject(err);
497
+ const d = currentDelay;
498
+ currentDelay *= backoff;
499
+ setTimeout(tryOnce, d);
500
+ });
501
+ };
502
+ tryOnce();
503
+ });
504
+ }
505
+
506
+ export function timeout(promise, ms, message) {
507
+ let timer;
508
+ const race = Promise.race([
509
+ promise,
510
+ new Promise((_, reject) => {
511
+ timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
512
+ })
513
+ ]);
514
+ return race.finally(() => clearTimeout(timer));
515
+ }