zero-query 0.9.1 → 0.9.5

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/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.9.1
2
+ * zQuery (zeroQuery) v0.9.5
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -1218,6 +1218,8 @@ query.children = (parentId) => {
1218
1218
  const p = document.getElementById(parentId);
1219
1219
  return new ZQueryCollection(p ? Array.from(p.children) : []);
1220
1220
  };
1221
+ query.qs = (sel, ctx = document) => ctx.querySelector(sel);
1222
+ query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
1221
1223
 
1222
1224
  // Create element shorthand — returns ZQueryCollection for chaining
1223
1225
  query.create = (tag, attrs = {}, ...children) => {
@@ -1225,7 +1227,7 @@ query.create = (tag, attrs = {}, ...children) => {
1225
1227
  for (const [k, v] of Object.entries(attrs)) {
1226
1228
  if (k === 'class') el.className = v;
1227
1229
  else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
1228
- else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
1230
+ else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
1229
1231
  else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
1230
1232
  else el.setAttribute(k, v);
1231
1233
  }
@@ -1445,6 +1447,11 @@ function tokenize(expr) {
1445
1447
  tokens.push({ t: T.OP, v: ch });
1446
1448
  i++; continue;
1447
1449
  }
1450
+ // Spread operator: ...
1451
+ if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
1452
+ tokens.push({ t: T.OP, v: '...' });
1453
+ i += 3; continue;
1454
+ }
1448
1455
  if ('()[]{},.?:'.includes(ch)) {
1449
1456
  tokens.push({ t: T.PUNC, v: ch });
1450
1457
  i++; continue;
@@ -1508,7 +1515,7 @@ class Parser {
1508
1515
  this.next(); // consume ?
1509
1516
  const truthy = this.parseExpression(0);
1510
1517
  this.expect(T.PUNC, ':');
1511
- const falsy = this.parseExpression(1);
1518
+ const falsy = this.parseExpression(0);
1512
1519
  left = { type: 'ternary', cond: left, truthy, falsy };
1513
1520
  continue;
1514
1521
  }
@@ -1649,7 +1656,12 @@ class Parser {
1649
1656
  this.expect(T.PUNC, '(');
1650
1657
  const args = [];
1651
1658
  while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
1652
- args.push(this.parseExpression(0));
1659
+ if (this.peek().t === T.OP && this.peek().v === '...') {
1660
+ this.next();
1661
+ args.push({ type: 'spread', arg: this.parseExpression(0) });
1662
+ } else {
1663
+ args.push(this.parseExpression(0));
1664
+ }
1653
1665
  if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
1654
1666
  }
1655
1667
  this.expect(T.PUNC, ')');
@@ -1725,7 +1737,12 @@ class Parser {
1725
1737
  this.next();
1726
1738
  const elements = [];
1727
1739
  while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
1728
- elements.push(this.parseExpression(0));
1740
+ if (this.peek().t === T.OP && this.peek().v === '...') {
1741
+ this.next();
1742
+ elements.push({ type: 'spread', arg: this.parseExpression(0) });
1743
+ } else {
1744
+ elements.push(this.parseExpression(0));
1745
+ }
1729
1746
  if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
1730
1747
  }
1731
1748
  this.expect(T.PUNC, ']');
@@ -1737,6 +1754,14 @@ class Parser {
1737
1754
  this.next();
1738
1755
  const properties = [];
1739
1756
  while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
1757
+ // Spread in object: { ...obj }
1758
+ if (this.peek().t === T.OP && this.peek().v === '...') {
1759
+ this.next();
1760
+ properties.push({ spread: true, value: this.parseExpression(0) });
1761
+ if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
1762
+ continue;
1763
+ }
1764
+
1740
1765
  const keyTok = this.next();
1741
1766
  let key;
1742
1767
  if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
@@ -1768,7 +1793,13 @@ class Parser {
1768
1793
 
1769
1794
  // new keyword
1770
1795
  if (tok.v === 'new') {
1771
- const classExpr = this.parsePostfix();
1796
+ let classExpr = this.parsePrimary();
1797
+ // Handle member access (e.g. ns.MyClass) without consuming call args
1798
+ while (this.peek().t === T.PUNC && this.peek().v === '.') {
1799
+ this.next();
1800
+ const prop = this.next();
1801
+ classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
1802
+ }
1772
1803
  let args = [];
1773
1804
  if (this.peek().t === T.PUNC && this.peek().v === '(') {
1774
1805
  args = this._parseArgs();
@@ -1880,6 +1911,12 @@ function evaluate(node, scope) {
1880
1911
  if (name === 'encodeURIComponent') return encodeURIComponent;
1881
1912
  if (name === 'decodeURIComponent') return decodeURIComponent;
1882
1913
  if (name === 'console') return console;
1914
+ if (name === 'Map') return Map;
1915
+ if (name === 'Set') return Set;
1916
+ if (name === 'RegExp') return RegExp;
1917
+ if (name === 'Error') return Error;
1918
+ if (name === 'URL') return URL;
1919
+ if (name === 'URLSearchParams') return URLSearchParams;
1883
1920
  return undefined;
1884
1921
  }
1885
1922
 
@@ -1918,10 +1955,21 @@ function evaluate(node, scope) {
1918
1955
  }
1919
1956
 
1920
1957
  case 'optional_call': {
1921
- const callee = evaluate(node.callee, scope);
1958
+ const calleeNode = node.callee;
1959
+ const args = _evalArgs(node.args, scope);
1960
+ // Method call: obj?.method() — bind `this` to obj
1961
+ if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
1962
+ const obj = evaluate(calleeNode.obj, scope);
1963
+ if (obj == null) return undefined;
1964
+ const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
1965
+ if (!_isSafeAccess(obj, prop)) return undefined;
1966
+ const fn = obj[prop];
1967
+ if (typeof fn !== 'function') return undefined;
1968
+ return fn.apply(obj, args);
1969
+ }
1970
+ const callee = evaluate(calleeNode, scope);
1922
1971
  if (callee == null) return undefined;
1923
1972
  if (typeof callee !== 'function') return undefined;
1924
- const args = node.args.map(a => evaluate(a, scope));
1925
1973
  return callee(...args);
1926
1974
  }
1927
1975
 
@@ -1931,7 +1979,7 @@ function evaluate(node, scope) {
1931
1979
  // Only allow safe constructors
1932
1980
  if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
1933
1981
  Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
1934
- const args = node.args.map(a => evaluate(a, scope));
1982
+ const args = _evalArgs(node.args, scope);
1935
1983
  return new Ctor(...args);
1936
1984
  }
1937
1985
  return undefined;
@@ -1961,13 +2009,32 @@ function evaluate(node, scope) {
1961
2009
  return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
1962
2010
  }
1963
2011
 
1964
- case 'array':
1965
- return node.elements.map(e => evaluate(e, scope));
2012
+ case 'array': {
2013
+ const arr = [];
2014
+ for (const e of node.elements) {
2015
+ if (e.type === 'spread') {
2016
+ const iterable = evaluate(e.arg, scope);
2017
+ if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
2018
+ for (const v of iterable) arr.push(v);
2019
+ }
2020
+ } else {
2021
+ arr.push(evaluate(e, scope));
2022
+ }
2023
+ }
2024
+ return arr;
2025
+ }
1966
2026
 
1967
2027
  case 'object': {
1968
2028
  const obj = {};
1969
- for (const { key, value } of node.properties) {
1970
- obj[key] = evaluate(value, scope);
2029
+ for (const prop of node.properties) {
2030
+ if (prop.spread) {
2031
+ const source = evaluate(prop.value, scope);
2032
+ if (source != null && typeof source === 'object') {
2033
+ Object.assign(obj, source);
2034
+ }
2035
+ } else {
2036
+ obj[prop.key] = evaluate(prop.value, scope);
2037
+ }
1971
2038
  }
1972
2039
  return obj;
1973
2040
  }
@@ -1988,12 +2055,30 @@ function evaluate(node, scope) {
1988
2055
  }
1989
2056
  }
1990
2057
 
2058
+ /**
2059
+ * Evaluate a list of argument AST nodes, flattening any spread elements.
2060
+ */
2061
+ function _evalArgs(argNodes, scope) {
2062
+ const result = [];
2063
+ for (const a of argNodes) {
2064
+ if (a.type === 'spread') {
2065
+ const iterable = evaluate(a.arg, scope);
2066
+ if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
2067
+ for (const v of iterable) result.push(v);
2068
+ }
2069
+ } else {
2070
+ result.push(evaluate(a, scope));
2071
+ }
2072
+ }
2073
+ return result;
2074
+ }
2075
+
1991
2076
  /**
1992
2077
  * Resolve and execute a function call safely.
1993
2078
  */
1994
2079
  function _resolveCall(node, scope) {
1995
2080
  const callee = node.callee;
1996
- const args = node.args.map(a => evaluate(a, scope));
2081
+ const args = _evalArgs(node.args, scope);
1997
2082
 
1998
2083
  // Method call: obj.method() — bind `this` to obj
1999
2084
  if (callee.type === 'member' || callee.type === 'optional_member') {
@@ -2067,8 +2152,9 @@ function _evalBinary(node, scope) {
2067
2152
  * @returns {*} — evaluation result, or undefined on error
2068
2153
  */
2069
2154
 
2070
- // AST cache — avoids re-tokenizing and re-parsing the same expression string.
2071
- // Bounded to prevent unbounded memory growth in long-lived apps.
2155
+ // AST cache (LRU) — avoids re-tokenizing and re-parsing the same expression.
2156
+ // Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
2157
+ // Eviction removes the least-recently-used (first) entry when at capacity.
2072
2158
  const _astCache = new Map();
2073
2159
  const _AST_CACHE_MAX = 512;
2074
2160
 
@@ -2088,9 +2174,12 @@ function safeEval(expr, scope) {
2088
2174
  // Fall through to full parser for built-in globals (Math, JSON, etc.)
2089
2175
  }
2090
2176
 
2091
- // Check AST cache
2177
+ // Check AST cache (LRU: move to end on hit)
2092
2178
  let ast = _astCache.get(trimmed);
2093
- if (!ast) {
2179
+ if (ast) {
2180
+ _astCache.delete(trimmed);
2181
+ _astCache.set(trimmed, ast);
2182
+ } else {
2094
2183
  const tokens = tokenize(trimmed);
2095
2184
  const parser = new Parser(tokens, scope);
2096
2185
  ast = parser.parse();
@@ -3220,15 +3309,44 @@ class Component {
3220
3309
  const handler = (e) => {
3221
3310
  // Read bindings from live map — always up to date after re-renders
3222
3311
  const currentBindings = this._eventBindings?.get(event) || [];
3223
- for (const { selector, methodExpr, modifiers, el } of currentBindings) {
3224
- if (!e.target.closest(selector)) continue;
3312
+
3313
+ // Collect matching bindings with their matched elements, then sort
3314
+ // deepest-first so .stop correctly prevents ancestor handlers
3315
+ // (mimics real DOM bubbling order within delegated events).
3316
+ const hits = [];
3317
+ for (const binding of currentBindings) {
3318
+ const matched = e.target.closest(binding.selector);
3319
+ if (!matched) continue;
3320
+ hits.push({ ...binding, matched });
3321
+ }
3322
+ hits.sort((a, b) => {
3323
+ if (a.matched === b.matched) return 0;
3324
+ return a.matched.contains(b.matched) ? 1 : -1;
3325
+ });
3326
+
3327
+ let stoppedAt = null; // Track elements that called .stop
3328
+ for (const { selector, methodExpr, modifiers, el, matched } of hits) {
3329
+
3330
+ // In delegated events, .stop should prevent ancestor bindings from
3331
+ // firing — stopPropagation alone only stops real DOM bubbling.
3332
+ if (stoppedAt) {
3333
+ let blocked = false;
3334
+ for (const stopped of stoppedAt) {
3335
+ if (matched.contains(stopped) && matched !== stopped) { blocked = true; break; }
3336
+ }
3337
+ if (blocked) continue;
3338
+ }
3225
3339
 
3226
3340
  // .self — only fire if target is the element itself
3227
3341
  if (modifiers.includes('self') && e.target !== el) continue;
3228
3342
 
3229
3343
  // Handle modifiers
3230
3344
  if (modifiers.includes('prevent')) e.preventDefault();
3231
- if (modifiers.includes('stop')) e.stopPropagation();
3345
+ if (modifiers.includes('stop')) {
3346
+ e.stopPropagation();
3347
+ if (!stoppedAt) stoppedAt = [];
3348
+ stoppedAt.push(matched);
3349
+ }
3232
3350
 
3233
3351
  // Build the invocation function
3234
3352
  const invoke = (evt) => {
@@ -4529,7 +4647,12 @@ class Router {
4529
4647
  if (typeof matched.component === 'string') {
4530
4648
  const container = document.createElement(matched.component);
4531
4649
  this._el.appendChild(container);
4532
- this._instance = mount(container, matched.component, props);
4650
+ try {
4651
+ this._instance = mount(container, matched.component, props);
4652
+ } catch (err) {
4653
+ reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
4654
+ return;
4655
+ }
4533
4656
  if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', matched.component);
4534
4657
  }
4535
4658
  // If component is a render function
@@ -4862,8 +4985,9 @@ async function request(method, url, data, options = {}) {
4862
4985
  } else {
4863
4986
  fetchOpts.signal = controller.signal;
4864
4987
  }
4988
+ let _timedOut = false;
4865
4989
  if (timeout > 0) {
4866
- timer = setTimeout(() => controller.abort(), timeout);
4990
+ timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
4867
4991
  }
4868
4992
 
4869
4993
  // Run request interceptors
@@ -4923,7 +5047,10 @@ async function request(method, url, data, options = {}) {
4923
5047
  } catch (err) {
4924
5048
  if (timer) clearTimeout(timer);
4925
5049
  if (err.name === 'AbortError') {
4926
- throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
5050
+ if (_timedOut) {
5051
+ throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
5052
+ }
5053
+ throw new Error(`Request aborted: ${method} ${fullURL}`);
4927
5054
  }
4928
5055
  throw err;
4929
5056
  }
@@ -5060,6 +5187,10 @@ function escapeHtml(str) {
5060
5187
  return String(str).replace(/[&<>"']/g, c => map[c]);
5061
5188
  }
5062
5189
 
5190
+ function stripHtml(str) {
5191
+ return String(str).replace(/<[^>]*>/g, '');
5192
+ }
5193
+
5063
5194
  /**
5064
5195
  * Template tag for auto-escaping interpolated values
5065
5196
  * Usage: $.html`<div>${userInput}</div>`
@@ -5105,7 +5236,10 @@ function camelCase(str) {
5105
5236
  * CamelCase to kebab-case
5106
5237
  */
5107
5238
  function kebabCase(str) {
5108
- return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
5239
+ return str
5240
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
5241
+ .replace(/([a-z\d])([A-Z])/g, '$1-$2')
5242
+ .toLowerCase();
5109
5243
  }
5110
5244
 
5111
5245
 
@@ -5146,15 +5280,19 @@ function deepMerge(target, ...sources) {
5146
5280
  /**
5147
5281
  * Simple object equality check
5148
5282
  */
5149
- function isEqual(a, b) {
5283
+ function isEqual(a, b, _seen) {
5150
5284
  if (a === b) return true;
5151
5285
  if (typeof a !== typeof b) return false;
5152
5286
  if (typeof a !== 'object' || a === null || b === null) return false;
5153
5287
  if (Array.isArray(a) !== Array.isArray(b)) return false;
5288
+ // Guard against circular references
5289
+ if (!_seen) _seen = new Set();
5290
+ if (_seen.has(a)) return true;
5291
+ _seen.add(a);
5154
5292
  const keysA = Object.keys(a);
5155
5293
  const keysB = Object.keys(b);
5156
5294
  if (keysA.length !== keysB.length) return false;
5157
- return keysA.every(k => isEqual(a[k], b[k]));
5295
+ return keysA.every(k => isEqual(a[k], b[k], _seen));
5158
5296
  }
5159
5297
 
5160
5298
 
@@ -5255,7 +5393,182 @@ class EventBus {
5255
5393
  clear() { this._handlers.clear(); }
5256
5394
  }
5257
5395
 
5258
- const bus = new EventBus();
5396
+ const bus = new EventBus();
5397
+
5398
+
5399
+ // ---------------------------------------------------------------------------
5400
+ // Array utilities
5401
+ // ---------------------------------------------------------------------------
5402
+
5403
+ function range(startOrEnd, end, step) {
5404
+ let s, e, st;
5405
+ if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
5406
+ else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
5407
+ if (st === 0) return [];
5408
+ const result = [];
5409
+ if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
5410
+ else { for (let i = s; i > e; i += st) result.push(i); }
5411
+ return result;
5412
+ }
5413
+
5414
+ function unique(arr, keyFn) {
5415
+ if (!keyFn) return [...new Set(arr)];
5416
+ const seen = new Set();
5417
+ return arr.filter(item => {
5418
+ const k = keyFn(item);
5419
+ if (seen.has(k)) return false;
5420
+ seen.add(k);
5421
+ return true;
5422
+ });
5423
+ }
5424
+
5425
+ function chunk(arr, size) {
5426
+ const result = [];
5427
+ for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
5428
+ return result;
5429
+ }
5430
+
5431
+ function groupBy(arr, keyFn) {
5432
+ const result = {};
5433
+ for (const item of arr) {
5434
+ const k = keyFn(item);
5435
+ (result[k] ??= []).push(item);
5436
+ }
5437
+ return result;
5438
+ }
5439
+
5440
+
5441
+ // ---------------------------------------------------------------------------
5442
+ // Object utilities
5443
+ // ---------------------------------------------------------------------------
5444
+
5445
+ function pick(obj, keys) {
5446
+ const result = {};
5447
+ for (const k of keys) { if (k in obj) result[k] = obj[k]; }
5448
+ return result;
5449
+ }
5450
+
5451
+ function omit(obj, keys) {
5452
+ const exclude = new Set(keys);
5453
+ const result = {};
5454
+ for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
5455
+ return result;
5456
+ }
5457
+
5458
+ function getPath(obj, path, fallback) {
5459
+ const keys = path.split('.');
5460
+ let cur = obj;
5461
+ for (const k of keys) {
5462
+ if (cur == null || typeof cur !== 'object') return fallback;
5463
+ cur = cur[k];
5464
+ }
5465
+ return cur === undefined ? fallback : cur;
5466
+ }
5467
+
5468
+ function setPath(obj, path, value) {
5469
+ const keys = path.split('.');
5470
+ let cur = obj;
5471
+ for (let i = 0; i < keys.length - 1; i++) {
5472
+ const k = keys[i];
5473
+ if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
5474
+ cur = cur[k];
5475
+ }
5476
+ cur[keys[keys.length - 1]] = value;
5477
+ return obj;
5478
+ }
5479
+
5480
+ function isEmpty(val) {
5481
+ if (val == null) return true;
5482
+ if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
5483
+ if (val instanceof Map || val instanceof Set) return val.size === 0;
5484
+ if (typeof val === 'object') return Object.keys(val).length === 0;
5485
+ return false;
5486
+ }
5487
+
5488
+
5489
+ // ---------------------------------------------------------------------------
5490
+ // String utilities
5491
+ // ---------------------------------------------------------------------------
5492
+
5493
+ function capitalize(str) {
5494
+ if (!str) return '';
5495
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
5496
+ }
5497
+
5498
+ function truncate(str, maxLen, suffix = '…') {
5499
+ if (str.length <= maxLen) return str;
5500
+ const end = Math.max(0, maxLen - suffix.length);
5501
+ return str.slice(0, end) + suffix;
5502
+ }
5503
+
5504
+
5505
+ // ---------------------------------------------------------------------------
5506
+ // Number utilities
5507
+ // ---------------------------------------------------------------------------
5508
+
5509
+ function clamp(val, min, max) {
5510
+ return val < min ? min : val > max ? max : val;
5511
+ }
5512
+
5513
+
5514
+ // ---------------------------------------------------------------------------
5515
+ // Function utilities
5516
+ // ---------------------------------------------------------------------------
5517
+
5518
+ function memoize(fn, keyFnOrOpts) {
5519
+ let keyFn, maxSize = 0;
5520
+ if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
5521
+ else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
5522
+
5523
+ const cache = new Map();
5524
+
5525
+ const memoized = (...args) => {
5526
+ const key = keyFn ? keyFn(...args) : args[0];
5527
+ if (cache.has(key)) return cache.get(key);
5528
+ const result = fn(...args);
5529
+ cache.set(key, result);
5530
+ if (maxSize > 0 && cache.size > maxSize) {
5531
+ cache.delete(cache.keys().next().value);
5532
+ }
5533
+ return result;
5534
+ };
5535
+
5536
+ memoized.clear = () => cache.clear();
5537
+ return memoized;
5538
+ }
5539
+
5540
+
5541
+ // ---------------------------------------------------------------------------
5542
+ // Async utilities
5543
+ // ---------------------------------------------------------------------------
5544
+
5545
+ function retry(fn, opts = {}) {
5546
+ const { attempts = 3, delay = 1000, backoff = 1 } = opts;
5547
+ return new Promise((resolve, reject) => {
5548
+ let attempt = 0, currentDelay = delay;
5549
+ const tryOnce = () => {
5550
+ attempt++;
5551
+ fn(attempt).then(resolve, (err) => {
5552
+ if (attempt >= attempts) return reject(err);
5553
+ const d = currentDelay;
5554
+ currentDelay *= backoff;
5555
+ setTimeout(tryOnce, d);
5556
+ });
5557
+ };
5558
+ tryOnce();
5559
+ });
5560
+ }
5561
+
5562
+ function timeout(promise, ms, message) {
5563
+ let timer;
5564
+ const race = Promise.race([
5565
+ promise,
5566
+ new Promise((_, reject) => {
5567
+ timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
5568
+ })
5569
+ ]);
5570
+ return race.finally(() => clearTimeout(timer));
5571
+ }
5259
5572
 
5260
5573
  // --- index.js (assembly) ------------------------------------------
5261
5574
  /**
@@ -5314,6 +5627,8 @@ Object.defineProperty($, 'name', {
5314
5627
  value: query.name, writable: true, configurable: true
5315
5628
  });
5316
5629
  $.children = query.children;
5630
+ $.qs = query.qs;
5631
+ $.qsa = query.qsa;
5317
5632
 
5318
5633
  // --- Collection selector ---------------------------------------------------
5319
5634
  /**
@@ -5382,8 +5697,10 @@ $.pipe = pipe;
5382
5697
  $.once = once;
5383
5698
  $.sleep = sleep;
5384
5699
  $.escapeHtml = escapeHtml;
5700
+ $.stripHtml = stripHtml;
5385
5701
  $.html = html;
5386
5702
  $.trust = trust;
5703
+ $.TrustedHTML = TrustedHTML;
5387
5704
  $.uuid = uuid;
5388
5705
  $.camelCase = camelCase;
5389
5706
  $.kebabCase = kebabCase;
@@ -5394,7 +5711,23 @@ $.param = param;
5394
5711
  $.parseQuery = parseQuery;
5395
5712
  $.storage = storage;
5396
5713
  $.session = session;
5714
+ $.EventBus = EventBus;
5397
5715
  $.bus = bus;
5716
+ $.range = range;
5717
+ $.unique = unique;
5718
+ $.chunk = chunk;
5719
+ $.groupBy = groupBy;
5720
+ $.pick = pick;
5721
+ $.omit = omit;
5722
+ $.getPath = getPath;
5723
+ $.setPath = setPath;
5724
+ $.isEmpty = isEmpty;
5725
+ $.capitalize = capitalize;
5726
+ $.truncate = truncate;
5727
+ $.clamp = clamp;
5728
+ $.memoize = memoize;
5729
+ $.retry = retry;
5730
+ $.timeout = timeout;
5398
5731
 
5399
5732
  // --- Error handling --------------------------------------------------------
5400
5733
  $.onError = onError;
@@ -5404,8 +5737,8 @@ $.guardCallback = guardCallback;
5404
5737
  $.validate = validate;
5405
5738
 
5406
5739
  // --- Meta ------------------------------------------------------------------
5407
- $.version = '0.9.1';
5408
- $.libSize = '~92 KB';
5740
+ $.version = '0.9.5';
5741
+ $.libSize = '~98 KB';
5409
5742
  $.meta = {}; // populated at build time by CLI bundler
5410
5743
 
5411
5744
  $.noConflict = () => {