leptris 1.9.13-aarch64-linux → 1.9.14-aarch64-linux

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48b344b1c1d7d190494ab2f08058006fc70b0487b9c9f21f092e6adc4cfbc06a
4
- data.tar.gz: fc906113ec4e2e65e448d8980d47118d34a4fdc41c4248039085d672999bd8c4
3
+ metadata.gz: c96f59841d0606a790ec372576cdb05f50bbc39c43cdf1188e7a4535068a7a8f
4
+ data.tar.gz: e3396a5ec39b5638f20a2254da0796f6330ca689f62e2fcad939850a42662f64
5
5
  SHA512:
6
- metadata.gz: '05082dfeba52663d829ece629219aef934f7b60bc7380944e2aface77469baca03bc897473d97f5518b76e4fa81ab6c59fa4ce33d940cb5c13a62b19d2309453'
7
- data.tar.gz: 3975c131b215c418307a6f3480eae554d0de4b4d1c69c810aa8d851c603912e6271a198f646acaffdc0d928d763235667061cd1d7274404d48c446edc2e2d94f
6
+ metadata.gz: a6cc9bb4c0ba7b0c39ced816d8435370947d6e051df16d1d03a899c5fbbdd1a7ba1e0c7e0a05f7879874a4b20c5db7b2f7b639138eb0137bbb1276e1288b7b59
7
+ data.tar.gz: 12846d96eec9f9a3d681bdb1bd9a2e29807a21575adc209851bdb4b3067f8c5999732faa9a4bdfad0af1ac6e55cdac6f52c832d886623f137c8b0aa867bc5ba2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.14] - 2026-08-27
9
+
10
+ ### Changed
11
+
12
+ - **Writable documents memoize** — the versioned read cache. A
13
+ per-document mutation version advances at every mutation gate
14
+ (ensure_writable!, root=, add_pi); each memoized field carries
15
+ its own version stamp and recomputes after any bump. Readonly
16
+ semantics unchanged (their version never advances — memos
17
+ forever valid). Measured on writable documents: namespace
18
+ inspection 0.392 s -> 0.013 s (**31x**, readonly parity),
19
+ children 0.154 s -> 0.014 s (**11x**). The read-heavy
20
+ DOM-editing workload (parse, query repeatedly, mutate
21
+ occasionally) now performs like the readonly one between
22
+ mutations. Staleness is test-pinned: a mutation-invalidation
23
+ matrix covers content, attribute, structural, namespace, root=,
24
+ and PI mutations against every memoized read. The shared
25
+ node-level stamp variant was tried and rejected in favor of
26
+ per-field stamps (one field's recompute must not resurrect
27
+ another's stale memo). ADR 0003 extended.
28
+ - NodeSet#[] drops the Ruby-side bounds FFI (the C accessor
29
+ already returns NULL out of range).
30
+
8
31
  ## [1.9.13] - 2026-08-27
9
32
 
10
33
  ### Changed
@@ -1,4 +1,20 @@
1
- # ADR 0003: readonly memoization stays hand-rolled
1
+ # ADR 0003: memoization stays hand-rolled; writable docs memoize via version stamps
2
+
3
+ ## 2026-08-27 extension (leptris-ruby 1.9.14)
4
+
5
+ Writable documents now memoize too: Document carries a mutation
6
+ version advanced at every mutation gate (Node#ensure_writable!,
7
+ root=, add_pi), and each memoized field stores its OWN version
8
+ stamp (`return @x if memo_hit?(@x_version)`). A shared node-level
9
+ stamp was tried and rejected — one field's recompute resurrects
10
+ another field's stale memo. Readonly semantics are unchanged
11
+ (their version never advances). Round X's "version-stamp rejected:
12
+ the compare costs the read" was corrected: the compare is cheap
13
+ against multi-FFI reads.
14
+
15
+ The hand-rolled per-site pattern (below) still stands; only the
16
+ guard gained a version argument.
17
+
2
18
 
3
19
  ## Context
4
20
  Every readonly-mode read repeats the same three lines:
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.13"
4
+ VERSION = "1.9.14"
5
5
  end
@@ -4,11 +4,14 @@ class Leptris::XML::CDATA < Leptris::XML::Text
4
4
  def name; "#cdata-section"; end
5
5
 
6
6
  def content
7
- return @content if readonly_cached?(:@content)
7
+ return @content if memo_hit?(@content_version)
8
8
  ensure_alive!
9
- Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr).tap do |text|
10
- @content = text if @document&.readonly?
9
+ result = Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr)
10
+ if @document
11
+ @content = result
12
+ @content_version = @document.version
11
13
  end
14
+ result
12
15
  end
13
16
 
14
17
  def content=(new_content)
@@ -4,11 +4,14 @@ class Leptris::XML::Comment < Leptris::XML::Node
4
4
  def name; "comment"; end
5
5
 
6
6
  def content
7
- return @content if readonly_cached?(:@content)
7
+ return @content if memo_hit?(@content_version)
8
8
  ensure_alive!
9
- Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr).tap do |text|
10
- @content = text if @document&.readonly?
9
+ result = Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr)
10
+ if @document
11
+ @content = result
12
+ @content_version = @document.version
11
13
  end
14
+ result
12
15
  end
13
16
 
14
17
  def content=(new_content)
@@ -14,10 +14,27 @@ class Leptris::XML::Document
14
14
  # `Document#free` was called explicitly and then GC ran.
15
15
  Freed = Struct.new(:state) # state: :alive | :freed
16
16
 
17
+ # Mutation version: advanced by every data mutation (via
18
+ # Node#ensure_writable!, root=, add_pi). Node memos stamp the
19
+ # version they were computed under and recompute after any bump —
20
+ # the invalidation that makes WRITABLE-document memoization sound.
21
+ # Readonly documents never advance it, so their memos are forever
22
+ # valid (ADR 0003 semantics, unchanged).
23
+ def version
24
+ @version
25
+ end
26
+
27
+ # Called by the mutation gates (Node#ensure_writable!, root=,
28
+ # add_pi) — every memo stamped with an older version discards.
29
+ def advance_version
30
+ @version += 1
31
+ end
32
+
17
33
  def initialize(c_ptr = nil, freed = Freed.new(:alive))
18
34
  @c_ptr = c_ptr
19
35
  @freed = freed
20
36
  @readonly = false
37
+ @version = 0
21
38
  # Per-document STRONG cache for Node wrappers, keyed on c_ptr
22
39
  # address. Every wrapper is created through Node.wrap, which is the
23
40
  # single construction path, so the same C node always yields the
@@ -142,6 +159,7 @@ class Leptris::XML::Document
142
159
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
143
160
  Leptris::XML::FFI.check_status(
144
161
  Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
162
+ @version += 1
145
163
  element
146
164
  end
147
165
 
@@ -253,15 +271,14 @@ class Leptris::XML::Document
253
271
  # Document-level processing instructions (not tree nodes):
254
272
  # an array of [target, data] pairs in document order.
255
273
  def processing_instructions
256
- if readonly? && instance_variable_defined?(:@processing_instructions)
257
- return @processing_instructions
258
- end
274
+ return @processing_instructions if @pi_version == @version
259
275
  count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
260
276
  result = count.times.map do |i|
261
277
  [Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
262
278
  Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
263
279
  end
264
- @processing_instructions = result if readonly?
280
+ @processing_instructions = result
281
+ @pi_version = @version
265
282
  result
266
283
  end
267
284
 
@@ -270,6 +287,7 @@ class Leptris::XML::Document
270
287
  witness = Leptris::XML::FFI.leptris_document_add_pi(
271
288
  @c_ptr, target.to_s, data.to_s)
272
289
  raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
290
+ @version += 1
273
291
  self
274
292
  end
275
293
 
@@ -17,11 +17,14 @@ class Leptris::XML::Element < Leptris::XML::Node
17
17
  alias_method :node_name=, :name=
18
18
 
19
19
  def content
20
- return @content if readonly_cached?(:@content)
20
+ return @content if memo_hit?(@content_version)
21
21
  ensure_alive!
22
- Leptris::XML::FFI.leptris_element_text(@c_ptr).tap do |text|
23
- @content = text if @document&.readonly?
22
+ result = Leptris::XML::FFI.leptris_element_text(@c_ptr)
23
+ if @document
24
+ @content = result
25
+ @content_version = @document.version
24
26
  end
27
+ result
25
28
  end
26
29
 
27
30
  def content=(new_content)
@@ -104,31 +107,43 @@ class Leptris::XML::Element < Leptris::XML::Node
104
107
  end
105
108
 
106
109
  def keys
107
- return @keys if readonly_cached?(:@keys)
110
+ return @keys if memo_hit?(@keys_version)
108
111
  result = each_attribute.to_a.map(&:name)
109
- @keys = result if @document&.readonly?
112
+ if @document
113
+ @keys = result
114
+ @keys_version = @document.version
115
+ end
110
116
  result
111
117
  end
112
118
 
113
119
  def values
114
- return @values if readonly_cached?(:@values)
120
+ return @values if memo_hit?(@values_version)
115
121
  result = each_attribute.to_a.map(&:value)
116
- @values = result if @document&.readonly?
122
+ if @document
123
+ @values = result
124
+ @values_version = @document.version
125
+ end
117
126
  result
118
127
  end
119
128
 
120
129
  def attributes
121
- return @attributes if readonly_cached?(:@attributes)
130
+ return @attributes if memo_hit?(@attributes_version)
122
131
  result = {}
123
132
  each_attribute { |attr| result[attr.name] = attr }
124
- @attributes = result if @document&.readonly?
133
+ if @document
134
+ @attributes = result
135
+ @attributes_version = @document.version
136
+ end
125
137
  result
126
138
  end
127
139
 
128
140
  def attribute_nodes
129
- return @attribute_nodes if readonly_cached?(:@attribute_nodes)
141
+ return @attribute_nodes if memo_hit?(@attribute_nodes_version)
130
142
  result = each_attribute.to_a
131
- @attribute_nodes = result if @document&.readonly?
143
+ if @document
144
+ @attribute_nodes = result
145
+ @attribute_nodes_version = @document.version
146
+ end
132
147
  result
133
148
  end
134
149
 
@@ -247,7 +262,7 @@ class Leptris::XML::Element < Leptris::XML::Node
247
262
  alias_method :<<, :add_child
248
263
 
249
264
  def namespace
250
- return @namespace if readonly_cached?(:@namespace)
265
+ return @namespace if memo_hit?(@namespace_version)
251
266
  ensure_alive!
252
267
  uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
253
268
  result =
@@ -261,12 +276,15 @@ class Leptris::XML::Element < Leptris::XML::Node
261
276
  prefix = nil if prefix.nil? || prefix.empty?
262
277
  Leptris::XML::Namespace.new(self, uri, prefix: prefix)
263
278
  end
264
- @namespace = result if @document&.readonly?
279
+ if @document
280
+ @namespace = result
281
+ @namespace_version = @document.version
282
+ end
265
283
  result
266
284
  end
267
285
 
268
286
  def namespace_definitions
269
- return @namespace_definitions if readonly_cached?(:@namespace_definitions)
287
+ return @namespace_definitions if memo_hit?(@namespace_definitions_version)
270
288
  ensure_alive!
271
289
  count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
272
290
  result = count.times.map do |i|
@@ -274,12 +292,15 @@ class Leptris::XML::Element < Leptris::XML::Node
274
292
  uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
275
293
  Leptris::XML::Namespace.new(self, uri, prefix: prefix)
276
294
  end
277
- @namespace_definitions = result if @document&.readonly?
295
+ if @document
296
+ @namespace_definitions = result
297
+ @namespace_definitions_version = @document.version
298
+ end
278
299
  result
279
300
  end
280
301
 
281
302
  def namespaces
282
- return @namespaces if readonly_cached?(:@namespaces)
303
+ return @namespaces if memo_hit?(@namespaces_version)
283
304
  scopes = {}
284
305
  node = self
285
306
  while node.is_a?(Leptris::XML::Element)
@@ -289,7 +310,10 @@ class Leptris::XML::Element < Leptris::XML::Node
289
310
  end
290
311
  node = node.parent
291
312
  end
292
- @namespaces = scopes if @document&.readonly?
313
+ if @document
314
+ @namespaces = scopes
315
+ @namespaces_version = @document.version
316
+ end
293
317
  scopes
294
318
  end
295
319
 
@@ -101,13 +101,19 @@ class Leptris::XML::Node
101
101
  end
102
102
 
103
103
  # Raises ReadOnlyError when the owning document was marked readonly,
104
- # UseAfterFreeError when it was freed.
104
+ # UseAfterFreeError when it was freed. Every node-level mutation
105
+ # passes through this gate, so it is where the document's mutation
106
+ # version advances — the invalidation behind writable-document
107
+ # memoization. Bumping before the C call is conservative: a failed
108
+ # mutation merely discards memos.
105
109
  def ensure_writable!
106
110
  ensure_alive!
107
111
  if readonly_document?
108
112
  raise Leptris::XML::ReadOnlyError,
109
113
  "document is readonly — mutation attempted on #{inspect}"
110
114
  end
115
+ @document.advance_version
116
+ nil
111
117
  end
112
118
 
113
119
  # Readonly is one-way, so caching TRUE is sound: once observed,
@@ -142,14 +148,17 @@ class Leptris::XML::Node
142
148
  def children
143
149
  # Immutable in readonly mode: the batch fetch plus wrapper
144
150
  # construction is paid once.
145
- return @children if readonly_cached?(:@children)
151
+ return @children if memo_hit?(@children_version)
146
152
  ensure_alive!
147
153
  parent = as_element_or_self
148
154
  nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
149
155
  Leptris::XML::Node.wrap(ptr, @document, parent: parent)
150
156
  end
151
157
  result = Leptris::XML::NodeSet.new(@document, nodes)
152
- @children = result if @document&.readonly?
158
+ if @document
159
+ @children = result
160
+ @children_version = @document.version
161
+ end
153
162
  result
154
163
  end
155
164
 
@@ -185,9 +194,12 @@ class Leptris::XML::Node
185
194
  end
186
195
 
187
196
  def element_children
188
- return @element_children if readonly_cached?(:@element_children)
197
+ return @element_children if memo_hit?(@element_children_version)
189
198
  result = children.select(&:element?)
190
- @element_children = result if @document&.readonly?
199
+ if @document
200
+ @element_children = result
201
+ @element_children_version = @document.version
202
+ end
191
203
  result
192
204
  end
193
205
  alias_method :elements, :element_children
@@ -230,16 +242,19 @@ class Leptris::XML::Node
230
242
  end
231
243
 
232
244
  def path
233
- return @path if readonly_cached?(:@path)
245
+ return @path if memo_hit?(@path_version)
234
246
  ensure_alive!
235
247
  str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
236
248
  result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
237
- @path = result if @document&.readonly?
249
+ if @document
250
+ @path = result
251
+ @path_version = @document.version
252
+ end
238
253
  result
239
254
  end
240
255
 
241
256
  def css_path
242
- return @css_path if readonly_cached?(:@css_path)
257
+ return @css_path if memo_hit?(@css_path_version)
243
258
  result =
244
259
  if path.nil?
245
260
  nil
@@ -249,7 +264,10 @@ class Leptris::XML::Node
249
264
  part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
250
265
  end.join(" > ")
251
266
  end
252
- @css_path = result if @document&.readonly?
267
+ if @document
268
+ @css_path = result
269
+ @css_path_version = @document.version
270
+ end
253
271
  result
254
272
  end
255
273
 
@@ -274,11 +292,15 @@ class Leptris::XML::Node
274
292
 
275
293
  protected
276
294
 
277
- # Memo presence alone proves readonly: every memo site assigns
278
- # only under readonly, and readonly is one-way so the guard
279
- # saves the document round-trip on every memoized read.
280
- def readonly_cached?(ivar)
281
- instance_variable_defined?(ivar)
295
+ # A memo is valid while the document's mutation version has not
296
+ # advanced since that memo was stored. Each memoized field carries
297
+ # its OWN stamp — a shared node-level stamp would let one field's
298
+ # recompute resurrect another field's stale memo. Readonly
299
+ # documents never advance the version, so their memos are forever
300
+ # valid (ADR 0003 semantics); writable documents gain memos
301
+ # between mutations.
302
+ def memo_hit?(stamp)
303
+ @document && stamp == @document.version
282
304
  end
283
305
 
284
306
  def as_element_or_self
@@ -60,7 +60,6 @@ class Leptris::XML::NodeSet
60
60
  # inconsistently with an eager set.
61
61
  return to_a[idx] if idx.negative?
62
62
  if @result_ptr
63
- return nil if idx < 0 || idx >= length
64
63
  ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
65
64
  return nil if ptr.null?
66
65
  Leptris::XML::Node.wrap(ptr, @document)
@@ -2,20 +2,26 @@
2
2
 
3
3
  class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
4
4
  def name
5
- return @name if readonly_cached?(:@name)
5
+ return @name if memo_hit?(@name_version)
6
6
  ensure_alive!
7
- Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr).tap do |target|
8
- @name = target if @document&.readonly?
7
+ result = Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr)
8
+ if @document
9
+ @name = result
10
+ @name_version = @document.version
9
11
  end
12
+ result
10
13
  end
11
14
  alias_method :target, :name
12
15
 
13
16
  def content
14
- return @content if readonly_cached?(:@content)
17
+ return @content if memo_hit?(@content_version)
15
18
  ensure_alive!
16
- Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s.tap do |data|
17
- @content = data if @document&.readonly?
19
+ result = Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s
20
+ if @document
21
+ @content = result
22
+ @content_version = @document.version
18
23
  end
24
+ result
19
25
  end
20
26
 
21
27
  def target=(new_target)
@@ -4,11 +4,14 @@ class Leptris::XML::Text < Leptris::XML::Node
4
4
  def name; "text"; end
5
5
 
6
6
  def content
7
- return @content if readonly_cached?(:@content)
7
+ return @content if memo_hit?(@content_version)
8
8
  ensure_alive!
9
- Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr).tap do |text|
10
- @content = text if @document&.readonly?
9
+ result = Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr)
10
+ if @document
11
+ @content = result
12
+ @content_version = @document.version
11
13
  end
14
+ result
12
15
  end
13
16
 
14
17
  def content=(new_content)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.13
4
+ version: 1.9.14
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi