@miaskiewicz/turbo-dom 0.1.26 → 0.1.27

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/index.d.ts CHANGED
@@ -34,23 +34,13 @@ export declare function parseRaw(html: string): number
34
34
  * objects lazily from this — no eager full-tree allocation. The fast path.
35
35
  */
36
36
  export interface JsSoa {
37
- nodeType: Uint8Array
38
- ns: Uint8Array
39
- tagId: Uint32Array
40
- parent: Int32Array
41
- firstChild: Int32Array
42
- nextSib: Int32Array
43
- textId: Int32Array
44
- pubId: Int32Array
45
- sysId: Int32Array
46
- attrStart: Int32Array
47
- attrCount: Uint16Array
48
- attrNameId: Uint32Array
49
- attrValue: Array<string>
50
- attrPrefixId: Uint32Array
37
+ packed: Uint8Array
38
+ n: number
39
+ m: number
51
40
  tagNames: Array<string>
52
41
  attrNames: Array<string>
53
42
  attrPrefixes: Array<string>
43
+ attrValues: Array<string>
54
44
  strings: Array<string>
55
45
  }
56
46
  /** Parse a document into the SoA flat buffer (the fast runtime path). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
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",
package/src/core.rs CHANGED
@@ -89,12 +89,13 @@ pub struct Soa {
89
89
  pub attr_count: Vec<u16>, // attrs for this node
90
90
  // flat attr tables — names/prefixes interned (highly repetitive), values pooled
91
91
  pub attr_name_id: Vec<u32>, // index into attr_names
92
- pub attr_value: Vec<String>,
92
+ pub attr_value_id: Vec<u32>, // index into attr_values (interned, deduped)
93
93
  pub attr_prefix_id: Vec<u32>, // index into attr_prefixes
94
94
  // string tables (interned, deduped)
95
95
  pub tag_names: Vec<String>,
96
96
  pub attr_names: Vec<String>,
97
97
  pub attr_prefixes: Vec<String>,
98
+ pub attr_values: Vec<String>, // interned attr value dictionary
98
99
  pub strings: Vec<String>, // text/comment/doctype data, pooled
99
100
  }
100
101
 
@@ -103,6 +104,7 @@ struct SoaBuilder {
103
104
  tag_map: std::collections::HashMap<String, u32>,
104
105
  attr_name_map: std::collections::HashMap<String, u32>,
105
106
  attr_prefix_map: std::collections::HashMap<String, u32>,
107
+ attr_value_map: std::collections::HashMap<String, u32>,
106
108
  }
107
109
 
108
110
  impl SoaBuilder {
@@ -133,6 +135,15 @@ impl SoaBuilder {
133
135
  self.attr_prefix_map.insert(prefix.to_string(), id);
134
136
  id
135
137
  }
138
+ fn intern_attr_value(&mut self, value: &str) -> u32 {
139
+ if let Some(&id) = self.attr_value_map.get(value) {
140
+ return id;
141
+ }
142
+ let id = self.soa.attr_values.len() as u32;
143
+ self.soa.attr_values.push(value.to_string());
144
+ self.attr_value_map.insert(value.to_string(), id);
145
+ id
146
+ }
136
147
  fn push_string(&mut self, s: &str) -> i32 {
137
148
  let id = self.soa.strings.len() as i32;
138
149
  self.soa.strings.push(s.to_string());
@@ -184,12 +195,13 @@ impl SoaBuilder {
184
195
  self.soa.tag_id[idx] = self.intern_tag(&name.local);
185
196
  let borrowed = attrs.borrow();
186
197
  if !borrowed.is_empty() {
187
- self.soa.attr_start[idx] = self.soa.attr_value.len() as i32;
198
+ self.soa.attr_start[idx] = self.soa.attr_name_id.len() as i32;
188
199
  self.soa.attr_count[idx] = borrowed.len() as u16;
189
200
  for attr in borrowed.iter() {
190
201
  let nid = self.intern_attr_name(&attr.name.local);
191
202
  self.soa.attr_name_id.push(nid);
192
- self.soa.attr_value.push(attr.value.to_string());
203
+ let vid = self.intern_attr_value(&attr.value);
204
+ self.soa.attr_value_id.push(vid);
193
205
  let pfx = attr.name.prefix.as_ref().map(|p| p.to_string()).unwrap_or_default();
194
206
  let pid = self.intern_attr_prefix(&pfx);
195
207
  self.soa.attr_prefix_id.push(pid);
@@ -252,6 +264,7 @@ pub fn parse_html_soa(html: &str) -> Soa {
252
264
  tag_map: std::collections::HashMap::new(),
253
265
  attr_name_map: std::collections::HashMap::new(),
254
266
  attr_prefix_map: std::collections::HashMap::new(),
267
+ attr_value_map: std::collections::HashMap::new(),
255
268
  };
256
269
  b.alloc(&dom.document, -1);
257
270
  b.soa
package/src/lib.rs CHANGED
@@ -71,48 +71,47 @@ mod napi_front {
71
71
 
72
72
  /// SoA flat buffer: structure as typed arrays, crossed once. JS inflates node
73
73
  /// objects lazily from this — no eager full-tree allocation. The fast path.
74
+ // All numeric columns packed into ONE little-endian byte blob (1 ArrayBuffer +
75
+ // zero-copy views in JS, vs 13 separate addon buffers + finalizers per parse).
76
+ // Layout — 4-byte block first (keeps every Int32/Uint32 view 4-aligned), then
77
+ // u16, then u8. Length-N: tag_id,parent,first_child,next_sib,text_id,pub_id,
78
+ // sys_id,attr_start | Length-M: attr_name_id,attr_value_id,attr_prefix_id |
79
+ // u16 N: attr_count | u8 N: node_type, ns. JS unpack must mirror this order.
74
80
  #[napi(object)]
75
81
  pub struct JsSoa {
76
- pub node_type: Uint8Array,
77
- pub ns: Uint8Array,
78
- pub tag_id: Uint32Array,
79
- pub parent: Int32Array,
80
- pub first_child: Int32Array,
81
- pub next_sib: Int32Array,
82
- pub text_id: Int32Array,
83
- pub pub_id: Int32Array,
84
- pub sys_id: Int32Array,
85
- pub attr_start: Int32Array,
86
- pub attr_count: Uint16Array,
87
- pub attr_name_id: Uint32Array,
88
- pub attr_value: Vec<String>,
89
- pub attr_prefix_id: Uint32Array,
82
+ pub packed: Uint8Array,
83
+ pub n: u32,
84
+ pub m: u32,
90
85
  pub tag_names: Vec<String>,
91
86
  pub attr_names: Vec<String>,
92
87
  pub attr_prefixes: Vec<String>,
88
+ pub attr_values: Vec<String>,
93
89
  pub strings: Vec<String>,
94
90
  }
95
91
 
96
92
  impl From<core::Soa> for JsSoa {
97
93
  fn from(s: core::Soa) -> Self {
94
+ let n = s.node_type.len();
95
+ let m = s.attr_name_id.len();
96
+ let mut buf: Vec<u8> = Vec::with_capacity(36 * n + 12 * m);
97
+ for col in [&s.tag_id] { for v in col.iter() { buf.extend_from_slice(&v.to_le_bytes()); } }
98
+ for col in [&s.parent, &s.first_child, &s.next_sib, &s.text_id, &s.pub_id, &s.sys_id, &s.attr_start] {
99
+ for v in col.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
100
+ }
101
+ for col in [&s.attr_name_id, &s.attr_value_id, &s.attr_prefix_id] {
102
+ for v in col.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
103
+ }
104
+ for v in s.attr_count.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
105
+ buf.extend_from_slice(&s.node_type);
106
+ buf.extend_from_slice(&s.ns);
98
107
  JsSoa {
99
- node_type: Uint8Array::new(s.node_type),
100
- ns: Uint8Array::new(s.ns),
101
- tag_id: Uint32Array::new(s.tag_id),
102
- parent: Int32Array::new(s.parent),
103
- first_child: Int32Array::new(s.first_child),
104
- next_sib: Int32Array::new(s.next_sib),
105
- text_id: Int32Array::new(s.text_id),
106
- pub_id: Int32Array::new(s.pub_id),
107
- sys_id: Int32Array::new(s.sys_id),
108
- attr_start: Int32Array::new(s.attr_start),
109
- attr_count: Uint16Array::new(s.attr_count),
110
- attr_name_id: Uint32Array::new(s.attr_name_id),
111
- attr_value: s.attr_value,
112
- attr_prefix_id: Uint32Array::new(s.attr_prefix_id),
108
+ packed: Uint8Array::new(buf),
109
+ n: n as u32,
110
+ m: m as u32,
113
111
  tag_names: s.tag_names,
114
112
  attr_names: s.attr_names,
115
113
  attr_prefixes: s.attr_prefixes,
114
+ attr_values: s.attr_values,
116
115
  strings: s.strings,
117
116
  }
118
117
  }
@@ -3,10 +3,32 @@
3
3
 
4
4
  const NS_SHORT = ['', 'svg', 'math'];
5
5
 
6
+ // Inflate the packed byte blob into named typed-array views (zero-copy).
7
+ // Mirrors the column order in lib.rs JsSoa::from. One ArrayBuffer, ~13 views.
8
+ function unpack(soa) {
9
+ const u8 = soa.packed, ab = u8.buffer, base = u8.byteOffset, n = soa.n, m = soa.m;
10
+ let off = base;
11
+ const i32 = () => { const a = new Int32Array(ab, off, n); off += n * 4; return a; };
12
+ const u32n = () => { const a = new Uint32Array(ab, off, n); off += n * 4; return a; };
13
+ const u32m = () => { const a = new Uint32Array(ab, off, m); off += m * 4; return a; };
14
+ const tagId = u32n(), parent = i32(), firstChild = i32(), nextSib = i32(),
15
+ textId = i32(), pubId = i32(), sysId = i32(), attrStart = i32();
16
+ const attrNameId = u32m(), attrValueId = u32m(), attrPrefixId = u32m();
17
+ const attrCount = new Uint16Array(ab, off, n); off += n * 2;
18
+ const nodeType = new Uint8Array(ab, off, n); off += n;
19
+ const ns = new Uint8Array(ab, off, n); off += n;
20
+ return {
21
+ nodeType, ns, tagId, parent, firstChild, nextSib, textId, pubId, sysId,
22
+ attrStart, attrCount, attrNameId, attrValueId, attrPrefixId,
23
+ tagNames: soa.tagNames, attrNames: soa.attrNames, attrPrefixes: soa.attrPrefixes,
24
+ attrValues: soa.attrValues, strings: soa.strings,
25
+ };
26
+ }
27
+
6
28
  export class Buffer {
7
29
  constructor(soa) {
8
- this.soa = soa;
9
- this.length = soa.nodeType.length;
30
+ this.soa = soa.packed ? unpack(soa) : soa;
31
+ this.length = this.soa.nodeType.length;
10
32
  }
11
33
  nodeType(i) { return this.soa.nodeType[i]; }
12
34
  ns(i) { return NS_SHORT[this.soa.ns[i]] || ''; }
@@ -17,16 +39,35 @@ export class Buffer {
17
39
  text(i) { const t = this.soa.textId[i]; return t < 0 ? '' : this.soa.strings[t]; }
18
40
  publicId(i) { const t = this.soa.pubId[i]; return t < 0 ? '' : this.soa.strings[t]; }
19
41
  systemId(i) { const t = this.soa.sysId[i]; return t < 0 ? '' : this.soa.strings[t]; }
42
+ // Read a single attr value / presence straight from the columns — no array,
43
+ // no {name,value,prefix} objects. The hot read path (selectors hammer
44
+ // getAttribute('class')/('id')) never materializes the attr list.
45
+ attrGet(i, name) {
46
+ const start = this.soa.attrStart[i];
47
+ if (start < 0) return null;
48
+ const count = this.soa.attrCount[i];
49
+ const { attrNameId, attrNames, attrValueId, attrValues } = this.soa;
50
+ for (let k = 0; k < count; k++) if (attrNames[attrNameId[start + k]] === name) return attrValues[attrValueId[start + k]];
51
+ return null;
52
+ }
53
+ attrHas(i, name) {
54
+ const start = this.soa.attrStart[i];
55
+ if (start < 0) return false;
56
+ const count = this.soa.attrCount[i];
57
+ const { attrNameId, attrNames } = this.soa;
58
+ for (let k = 0; k < count; k++) if (attrNames[attrNameId[start + k]] === name) return true;
59
+ return false;
60
+ }
20
61
  attrs(i) {
21
62
  const start = this.soa.attrStart[i];
22
63
  if (start < 0) return [];
23
64
  const count = this.soa.attrCount[i];
24
65
  const out = new Array(count);
25
- const { attrNameId, attrNames, attrValue, attrPrefixId, attrPrefixes } = this.soa;
66
+ const { attrNameId, attrNames, attrValueId, attrValues, attrPrefixId, attrPrefixes } = this.soa;
26
67
  for (let k = 0; k < count; k++) {
27
68
  out[k] = {
28
69
  name: attrNames[attrNameId[start + k]],
29
- value: attrValue[start + k],
70
+ value: attrValues[attrValueId[start + k]],
30
71
  prefix: attrPrefixes[attrPrefixId[start + k]] || '',
31
72
  };
32
73
  }
@@ -354,8 +354,18 @@ export class Element extends Node {
354
354
  // builds the array from the SoA only when an attribute is first touched (many
355
355
  // elements are inflated for traversal but never have attrs read).
356
356
  __buildAttrs() { const doc = this.ownerDocument, buf = doc && doc.__buf; return (this.__attrIdx >= 0 && buf) ? buf.attrs(this.__attrIdx) : []; }
357
- getAttribute(name) { const at = this.__attrs ?? (this.__attrs = this.__buildAttrs()); for (let i = 0; i < at.length; i++) if (at[i].name === name) return at[i].value; return null; }
358
- hasAttribute(name) { const at = this.__attrs ?? (this.__attrs = this.__buildAttrs()); for (let i = 0; i < at.length; i++) if (at[i].name === name) return true; return false; }
357
+ getAttribute(name) {
358
+ const at = this.__attrs;
359
+ if (at !== undefined) { for (let i = 0; i < at.length; i++) if (at[i].name === name) return at[i].value; return null; }
360
+ const doc = this.ownerDocument, buf = doc && doc.__buf; // lazy: read column, don't materialize
361
+ return (this.__attrIdx >= 0 && buf) ? buf.attrGet(this.__attrIdx, name) : null;
362
+ }
363
+ hasAttribute(name) {
364
+ const at = this.__attrs;
365
+ if (at !== undefined) { for (let i = 0; i < at.length; i++) if (at[i].name === name) return true; return false; }
366
+ const doc = this.ownerDocument, buf = doc && doc.__buf;
367
+ return (this.__attrIdx >= 0 && buf) ? buf.attrHas(this.__attrIdx, name) : false;
368
+ }
359
369
  getAttributeNames() { return (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => a.name); }
360
370
  setAttribute(name, value) {
361
371
  if (this.__attrs === undefined) this.__attrs = this.__buildAttrs();
Binary file
Binary file