@miaskiewicz/turbo-dom 0.1.18 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -158,7 +158,7 @@ export class Node extends EventTarget {
158
158
  else if (kids[i].nodeType === ELEMENT_NODE) kids[i].normalize();
159
159
  }
160
160
  }
161
- replaceChildren(...nodes) { this.__kids = []; for (const n of nodes) this.appendChild(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n); }
161
+ replaceChildren(...nodes) { this.__kids = []; this.__touch(); for (const n of nodes) this.appendChild(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n); }
162
162
  // bitmask: 1 DISCONNECTED, 2 PRECEDING, 4 FOLLOWING, 8 CONTAINS, 16 CONTAINED_BY
163
163
  compareDocumentPosition(other) {
164
164
  if (other === this) return 0;
@@ -189,9 +189,15 @@ export class Node extends EventTarget {
189
189
  }
190
190
  set textContent(value) {
191
191
  this.__kids = [];
192
+ this.__touch();
192
193
  if (value !== '') this.appendChild(this.ownerDocument.createTextNode(String(value)));
193
194
  }
194
195
 
196
+ // bump the document version so getElementsBy* caches invalidate after direct
197
+ // __kids reassignments (innerHTML/textContent/replaceChildren) that bypass
198
+ // insertBefore/removeChild.
199
+ __touch() { const d = this.ownerDocument; if (d) d.__version = (d.__version || 0) + 1; }
200
+
195
201
  // window/document path for event propagation past the document
196
202
  get __owner() { return null; }
197
203
  }
@@ -505,7 +511,7 @@ export class Element extends Node {
505
511
  before(...nodes) { const p = this.parentNode; if (!p) return; for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), this); }
506
512
  after(...nodes) { const p = this.parentNode; if (!p) return; const ref = this.nextSibling; for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), ref); }
507
513
  replaceWith(...nodes) { const p = this.parentNode; if (!p) return; const ref = this.nextSibling; this.remove(); for (const n of nodes) p.insertBefore(toNode(this.ownerDocument, n), ref); }
508
- replaceChildren(...nodes) { this.__kids = []; for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
514
+ replaceChildren(...nodes) { this.__kids = []; this.__touch(); for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
509
515
 
510
516
  // ---- queries ----
511
517
  matches(sel) { return matchesSelector(this, sel); }
@@ -520,6 +526,7 @@ export class Element extends Node {
520
526
  set innerHTML(html) {
521
527
  const frag = native.parseFragment(String(html), this.__ns ? `${this.__ns} ${this.localName}` : this.localName);
522
528
  this.__kids = [];
529
+ this.__touch();
523
530
  for (const rawChild of frag.children) {
524
531
  if (rawChild.nodeType === DOCUMENT_FRAGMENT_NODE && rawChild.name === 'content') continue;
525
532
  const child = this.ownerDocument.__inflateNested(rawChild);
@@ -956,7 +963,11 @@ function isDescendant(node, ancestor) {
956
963
  }
957
964
  function notifyMutation(target, record) {
958
965
  const doc = target.ownerDocument;
959
- if (!doc || !doc.__mo || doc.__mo.length === 0) return;
966
+ if (!doc) return;
967
+ // bump the DOM version on every structural/attribute mutation — invalidates
968
+ // the document's getElementsBy* caches. Unconditional (independent of observers).
969
+ doc.__version = (doc.__version || 0) + 1;
970
+ if (!doc.__mo || doc.__mo.length === 0) return;
960
971
  for (const reg of doc.__mo) {
961
972
  const { obs, target: obsTarget, options } = reg;
962
973
  const onTarget = record.target === obsTarget;
@@ -1109,6 +1120,9 @@ export class Document extends Node {
1109
1120
  this.__active = null;
1110
1121
  this.__mo = []; // drop observers
1111
1122
  this.__moPending = null;
1123
+ this.__version = (this.__version || 0) + 1; // invalidate getElementsBy* caches
1124
+ this.__tagCache = null;
1125
+ this.__classCache = null;
1112
1126
  }
1113
1127
 
1114
1128
  get documentElement() { return this.__children().find((n) => n.nodeType === ELEMENT_NODE && n.localName === 'html') ?? null; }
@@ -1191,8 +1205,28 @@ export class Document extends Node {
1191
1205
  }
1192
1206
  querySelector(sel) { return qsel(this, sel); }
1193
1207
  querySelectorAll(sel) { return qselAll(this, sel); }
1194
- getElementsByTagName(tag) { const self = this; return liveHTMLCollection(() => collectByTag(self, tag.toLowerCase())); }
1195
- getElementsByClassName(cls) { const self = this; const classes = cls.split(/\s+/).filter(Boolean); return liveHTMLCollection(() => collectByClass(self, classes)); }
1208
+ // version-keyed cache: getElementsBy* is called repeatedly within a single
1209
+ // query (e.g. RTL getByLabelText calls element.labels per element, each doing
1210
+ // document.getElementsByTagName('label')). Without caching that's O(n²) tree
1211
+ // walks. The cache invalidates whenever the DOM version bumps (any mutation).
1212
+ __byTag(t) {
1213
+ const v = this.__version || 0;
1214
+ const c = this.__tagCache && this.__tagCache.get(t);
1215
+ if (c && c.v === v) return c.arr;
1216
+ const arr = collectByTag(this, t);
1217
+ (this.__tagCache || (this.__tagCache = new Map())).set(t, { v, arr });
1218
+ return arr;
1219
+ }
1220
+ __byClass(key, classes) {
1221
+ const v = this.__version || 0;
1222
+ const c = this.__classCache && this.__classCache.get(key);
1223
+ if (c && c.v === v) return c.arr;
1224
+ const arr = collectByClass(this, classes);
1225
+ (this.__classCache || (this.__classCache = new Map())).set(key, { v, arr });
1226
+ return arr;
1227
+ }
1228
+ getElementsByTagName(tag) { const self = this; const t = tag.toLowerCase(); return liveHTMLCollection(() => self.__byTag(t)); }
1229
+ getElementsByClassName(cls) { const self = this; const classes = cls.split(/\s+/).filter(Boolean); return liveHTMLCollection(() => self.__byClass(cls, classes)); }
1196
1230
  contains(node) { return Node.prototype.contains.call(this, node); }
1197
1231
 
1198
1232
  // cookie jar: store name=value, strip attributes (path/Secure/SameSite/…),