leptris 1.9.23 → 1.9.25
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 +4 -4
- data/CHANGELOG.md +56 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +63 -29
- data/lib/leptris/xml/node.rb +10 -4
- data/lib/leptris/xml/sax/parser.rb +60 -24
- data/lib/leptris/xml/sax/recorder.rb +33 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afa52b007ec23d222a3562207fcef3f9975f62c11378da91de2d238eff7a74be
|
|
4
|
+
data.tar.gz: 878468ec897ba3f9b60aca28e1379ac712e261c4500c4cc51c2a7379545730ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8caa0b2d6052ff1e72a1d23c59d1f283b887c852e7cb669fa8449d85438f8f89a2005726f96644f6bc46c880d717e54c922210fb9b638c2d890d6d0cdc57fbe
|
|
7
|
+
data.tar.gz: 19fab1e638971c2402b69e70447657c9dfe4d140ba55e588f09b4a394690b34c2e43ba46433615209f66be1660ee6755b83827e7b51e84516c5793b67f44d0d2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,62 @@ 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.
|
|
8
|
+
## [1.9.25] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Cold full-tree walk regression (1.9.1 → 1.9.24, ~1.4-1.5x)
|
|
13
|
+
recovered to parity**: the batch-fetch era (1.9.11-1.9.17) bought
|
|
14
|
+
its warm-path wins with per-call machinery the cold branch paid
|
|
15
|
+
on every children() — reproduced, bisected across every published
|
|
16
|
+
release, and profiled end to end this round. Raw cold walk of a
|
|
17
|
+
~4,200-node catalog (interleaved best-of-5): 1.9.24 5,441 µs ->
|
|
18
|
+
**4,519 µs (-17%)**, vs 1.9.1's 4,369 µs = parity. Through moxml
|
|
19
|
+
the same shape improves 20.1 ms -> 16.5 ms (-18%).
|
|
20
|
+
- `FFI.fetch_children` (and the XPath result batch) now use
|
|
21
|
+
thread-local scratch buffers that grow to the largest family
|
|
22
|
+
seen and are reused — no MemoryPointer allocation/free per
|
|
23
|
+
call, and materially less GC. The count query is paid only when
|
|
24
|
+
the buffer fills exactly (possible truncation): a family that
|
|
25
|
+
fits costs ONE dispatch, not two.
|
|
26
|
+
- `Node.wrap`'s miss path resolves the wrapper cache and pointer
|
|
27
|
+
address once instead of twice — a cold walk wraps every node
|
|
28
|
+
exactly once, so the cache is 100% misses and every redundant
|
|
29
|
+
resolution was pure cost. Identity semantics unchanged.
|
|
30
|
+
- Kept: the per-field version-stamped memo stores on writable
|
|
31
|
+
documents (ADR-0003's writable extension — the residual ~3% vs
|
|
32
|
+
1.9.1 is that feature working) and the batch architecture
|
|
33
|
+
itself (never more FFI dispatches than the linked-list walk,
|
|
34
|
+
and 2 vs 701 for the 700-child root).
|
|
35
|
+
|
|
36
|
+
## [1.9.24] - 2026-08-27
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **Interest-proportional SAX (both transports)**: the handler's
|
|
41
|
+
declared interest — which event methods it overrides — now decides
|
|
42
|
+
what the transport dispatches or drains.
|
|
43
|
+
- `SAX::Parser` attaches a callback only when the handler defines
|
|
44
|
+
the method beyond `SAX::Document`'s no-ops; the C engine already
|
|
45
|
+
skips NULL callbacks, so unwanted events cost nothing on either
|
|
46
|
+
side. Measured per parse of a 1.9 MB / 250k-event document:
|
|
47
|
+
text-only handler 151 ms -> 22 ms (**6.9x**); a handler that
|
|
48
|
+
overrides nothing runs at the C floor, 151 ms -> 7 ms
|
|
49
|
+
(**22x**); elements-only 151 ms -> 121 ms (-20%, the remainder
|
|
50
|
+
is attribute walking). Handlers overriding most kinds stay at
|
|
51
|
+
parity. Duck-typed handlers attach exactly what they define.
|
|
52
|
+
- `SAX::Recorder#each_event(*kinds)` (and `Recorder.parse(...,
|
|
53
|
+
kinds:)`, `#feed_stream(..., kinds:)`) skips records of other
|
|
54
|
+
kinds BEFORE slicing any strings from the arena — an unwanted
|
|
55
|
+
event costs one Array read. Filtered `:characters` drain 201 ms
|
|
56
|
+
-> 119 ms (**-41%**); unknown kinds raise ArgumentError. The
|
|
57
|
+
kind lookup table is now an Array indexed by the event code.
|
|
58
|
+
- Transport choice, by measurement: the pruned callback transport
|
|
59
|
+
beats the filtered recorder for selective consumers (C-side
|
|
60
|
+
emission is skipped entirely), so callbacks remain the default;
|
|
61
|
+
the recorder keeps its niche — bulk raw-event drains per chunk.
|
|
62
|
+
|
|
63
|
+
## [1.9.23] - 2026-08-27
|
|
9
64
|
|
|
10
65
|
### Added
|
|
11
66
|
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -817,16 +817,33 @@ module Leptris
|
|
|
817
817
|
end
|
|
818
818
|
end
|
|
819
819
|
|
|
820
|
-
# All-kind child handle batch (libleptris 1.7.0)
|
|
821
|
-
#
|
|
822
|
-
#
|
|
820
|
+
# All-kind child handle batch (libleptris 1.7.0). Opportunistic
|
|
821
|
+
# single dispatch: the C call copies up to the capacity handed
|
|
822
|
+
# it and returns what fit, so the count query is paid only when
|
|
823
|
+
# the buffer fills exactly (a possible truncation) — families
|
|
824
|
+
# that fit the scratch buffer cost ONE dispatch, not two.
|
|
825
|
+
FETCH_INITIAL = 32
|
|
826
|
+
private_constant :FETCH_INITIAL
|
|
827
|
+
|
|
823
828
|
def self.fetch_children(c_ptr)
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
830
|
+
buffer = scratch[:pointers]
|
|
831
|
+
if buffer.nil?
|
|
832
|
+
buffer = scratch[:pointers] =
|
|
833
|
+
::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
|
|
834
|
+
end
|
|
835
|
+
cap = buffer.size / PTR_BYTES
|
|
836
|
+
copied = leptris_node_children(c_ptr, buffer, cap)
|
|
837
|
+
if copied == cap
|
|
838
|
+
total = leptris_node_children(c_ptr, nil, 0)
|
|
839
|
+
if total > cap
|
|
840
|
+
buffer.free
|
|
841
|
+
buffer = scratch[:pointers] =
|
|
842
|
+
::FFI::MemoryPointer.new(:pointer, total)
|
|
843
|
+
copied = leptris_node_children(c_ptr, buffer, total)
|
|
844
|
+
end
|
|
829
845
|
end
|
|
846
|
+
buffer.get_array_of_pointer(0, copied)
|
|
830
847
|
end
|
|
831
848
|
|
|
832
849
|
# XPath result-set batch (leptris_xpath_result_get_nodes_ex):
|
|
@@ -840,18 +857,13 @@ module Leptris
|
|
|
840
857
|
|
|
841
858
|
def self.fetch_result_nodes(result_ptr, count)
|
|
842
859
|
return [[], nil] if count.zero?
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
[buffer.get_array_of_pointer(0, copied), hints]
|
|
851
|
-
ensure
|
|
852
|
-
kinds.free
|
|
853
|
-
end
|
|
854
|
-
end
|
|
860
|
+
buffer = scratch_pointers(count)
|
|
861
|
+
kinds = scratch_ints(count)
|
|
862
|
+
copied = leptris_xpath_result_get_nodes_ex(
|
|
863
|
+
result_ptr, buffer, kinds, count)
|
|
864
|
+
hints = kinds.get_array_of_int(0, copied)
|
|
865
|
+
.map { |k| XPATH_KIND_HINT[k] }
|
|
866
|
+
[buffer.get_array_of_pointer(0, copied), hints]
|
|
855
867
|
end
|
|
856
868
|
|
|
857
869
|
# Fragment parse with the status out-param read: returns
|
|
@@ -863,16 +875,38 @@ module Leptris
|
|
|
863
875
|
[raw, status.read_int]
|
|
864
876
|
end
|
|
865
877
|
|
|
866
|
-
#
|
|
867
|
-
#
|
|
868
|
-
#
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
878
|
+
# Thread-local scratch buffers for batch handle fetches (round
|
|
879
|
+
# XX): the per-call MemoryPointer new/free was pure cold-path
|
|
880
|
+
# overhead — two allocations and a GC visit per children() /
|
|
881
|
+
# result-set batch, ~15% of a cold full-tree walk between
|
|
882
|
+
# 1.9.1 and 1.9.24. The buffers grow to the largest request
|
|
883
|
+
# seen (bounded by the widest element or result set) and are
|
|
884
|
+
# reused. Safe: no Ruby reentrancy during the C call, and
|
|
885
|
+
# per-thread storage keeps concurrent fetches independent.
|
|
886
|
+
# Never freed — a few KB pinned per thread.
|
|
887
|
+
PTR_BYTES = ::FFI.type_size(:pointer)
|
|
888
|
+
private_constant :PTR_BYTES
|
|
889
|
+
INT_BYTES = ::FFI.type_size(:int)
|
|
890
|
+
private_constant :INT_BYTES
|
|
891
|
+
|
|
892
|
+
def self.scratch_pointers(count)
|
|
893
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
894
|
+
ptr = scratch[:pointers]
|
|
895
|
+
if ptr.nil? || ptr.size / PTR_BYTES < count
|
|
896
|
+
ptr&.free
|
|
897
|
+
ptr = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, count)
|
|
898
|
+
end
|
|
899
|
+
ptr
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def self.scratch_ints(count)
|
|
903
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
904
|
+
ptr = scratch[:ints]
|
|
905
|
+
if ptr.nil? || ptr.size / INT_BYTES < count
|
|
906
|
+
ptr&.free
|
|
907
|
+
ptr = scratch[:ints] = ::FFI::MemoryPointer.new(:int, count)
|
|
875
908
|
end
|
|
909
|
+
ptr
|
|
876
910
|
end
|
|
877
911
|
end
|
|
878
912
|
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -19,9 +19,15 @@ class Leptris::XML::Node
|
|
|
19
19
|
# Per-document weak-ref cache. Returns the existing wrapper when the
|
|
20
20
|
# same c_ptr is wrapped twice (common in children/sibling walks,
|
|
21
21
|
# repeated xpath queries, traverse-then-access patterns). The cache
|
|
22
|
-
# dies with the document so no stale entries.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# dies with the document so no stale entries. The miss path
|
|
23
|
+
# resolves the cache and address once — a cold walk wraps every
|
|
24
|
+
# node exactly once and pays both only on the store.
|
|
25
|
+
if document
|
|
26
|
+
cache = document.wrapper_cache
|
|
27
|
+
address = c_ptr.address
|
|
28
|
+
if (cached = cache[address])
|
|
29
|
+
return cached
|
|
30
|
+
end
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
node_type ||= Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
@@ -41,7 +47,7 @@ class Leptris::XML::Node
|
|
|
41
47
|
new(c_ptr, document, parent: parent, node_type: node_type)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
cache[address] = node if document
|
|
45
51
|
node
|
|
46
52
|
end
|
|
47
53
|
|
|
@@ -83,59 +83,95 @@ class Leptris::XML::SAX::Parser
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
85
|
# Build a LeptrisSAXHandler struct populated with FFI::Function callbacks
|
|
86
|
-
# that dispatch to the Ruby handler.
|
|
86
|
+
# that dispatch to the Ruby handler. Only the callbacks the handler
|
|
87
|
+
# actually overrides are attached: the C engine skips NULL callbacks,
|
|
88
|
+
# so cost scales with the handler's declared interest, not with the
|
|
89
|
+
# document's event mix (measured 4.9x for an elements-only handler on
|
|
90
|
+
# a 1.9 MB stream).
|
|
87
91
|
def build_handler_struct
|
|
88
92
|
s = Leptris::XML::FFI::SAXHandler.new
|
|
89
93
|
handler = @document # capture in closures
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
if overridden?(:start_document)
|
|
96
|
+
s[:start_document] = callback(:void, [:pointer]) do |_|
|
|
97
|
+
handler.start_document
|
|
98
|
+
end
|
|
93
99
|
end
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if overridden?(:end_document)
|
|
102
|
+
s[:end_document] = callback(:void, [:pointer]) do |_|
|
|
103
|
+
handler.end_document
|
|
104
|
+
end
|
|
97
105
|
end
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
if overridden?(:start_element)
|
|
108
|
+
s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
|
|
109
|
+
attrs = walk_attr_array(attrs_ptr)
|
|
110
|
+
handler.start_element(utf8(name), attrs)
|
|
111
|
+
end
|
|
102
112
|
end
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
if overridden?(:end_element)
|
|
115
|
+
s[:end_element] = callback(:void, [:pointer, :string]) do |_, name|
|
|
116
|
+
handler.end_element(utf8(name))
|
|
117
|
+
end
|
|
106
118
|
end
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
if overridden?(:characters)
|
|
121
|
+
s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
|
|
122
|
+
handler.characters(text_ptr.read_bytes(len).force_encoding(Encoding::UTF_8))
|
|
123
|
+
end
|
|
110
124
|
end
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
if overridden?(:comment)
|
|
127
|
+
s[:comment] = callback(:void, [:pointer, :string]) do |_, comment|
|
|
128
|
+
handler.comment(utf8(comment))
|
|
129
|
+
end
|
|
114
130
|
end
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
if overridden?(:cdata_block)
|
|
133
|
+
s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata|
|
|
134
|
+
handler.cdata_block(utf8(cdata))
|
|
135
|
+
end
|
|
118
136
|
end
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
if overridden?(:processing_instruction)
|
|
139
|
+
s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data|
|
|
140
|
+
handler.processing_instruction(utf8(target), utf8(data))
|
|
141
|
+
end
|
|
122
142
|
end
|
|
123
143
|
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
if overridden?(:start_prefix_mapping)
|
|
145
|
+
s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri|
|
|
146
|
+
handler.start_prefix_mapping(utf8(prefix), utf8(uri))
|
|
147
|
+
end
|
|
126
148
|
end
|
|
127
149
|
|
|
128
|
-
|
|
129
|
-
|
|
150
|
+
if overridden?(:end_prefix_mapping)
|
|
151
|
+
s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix|
|
|
152
|
+
handler.end_prefix_mapping(utf8(prefix))
|
|
153
|
+
end
|
|
130
154
|
end
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
if overridden?(:error)
|
|
157
|
+
s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
|
|
158
|
+
handler.error(utf8(msg), line, col)
|
|
159
|
+
end
|
|
134
160
|
end
|
|
135
161
|
|
|
136
162
|
s
|
|
137
163
|
end
|
|
138
164
|
|
|
165
|
+
# True when the handler defines the event method beyond the no-op
|
|
166
|
+
# every SAX::Document carries — subclasses, included modules, and
|
|
167
|
+
# duck-typed handlers all qualify; a handler that overrides nothing
|
|
168
|
+
# attaches nothing and the parse runs at the C floor.
|
|
169
|
+
def overridden?(name)
|
|
170
|
+
@document.method(name).owner != Leptris::XML::SAX::Document
|
|
171
|
+
rescue NameError
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
|
|
139
175
|
def callback(return_type, params, blocking: true, &block)
|
|
140
176
|
::FFI::Function.new(return_type, params, blocking: blocking, &block)
|
|
141
177
|
end
|
|
@@ -29,6 +29,10 @@ class Leptris::XML::SAX::Recorder
|
|
|
29
29
|
Leptris::XML::FFI::SAX_EVENT_ERROR => :error,
|
|
30
30
|
}.freeze
|
|
31
31
|
|
|
32
|
+
# Kind symbol per code byte — Array indexing in the drain loop,
|
|
33
|
+
# not a Hash lookup per event.
|
|
34
|
+
KIND_BY_CODE = Array.new(KINDS.size) { |code| KINDS[code] }.freeze
|
|
35
|
+
|
|
32
36
|
def self.open
|
|
33
37
|
raw = Leptris::XML::FFI.leptris_sax_recorder_new
|
|
34
38
|
if raw.null?
|
|
@@ -38,14 +42,17 @@ class Leptris::XML::SAX::Recorder
|
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
# One-shot parse over a complete document, yielding Record structs.
|
|
41
|
-
|
|
45
|
+
# +kinds+ filters the drain (see #each_event) — pass e.g.
|
|
46
|
+
# `kinds: [:start_element]` to slice strings only for the events
|
|
47
|
+
# you consume.
|
|
48
|
+
def self.parse(xml_or_io, kinds: nil)
|
|
42
49
|
recorder = open
|
|
43
50
|
begin
|
|
44
51
|
if xml_or_io.respond_to?(:read)
|
|
45
|
-
recorder.feed_stream(xml_or_io) { |*args| yield(*args) }
|
|
52
|
+
recorder.feed_stream(xml_or_io, kinds: kinds) { |*args| yield(*args) }
|
|
46
53
|
else
|
|
47
54
|
recorder.feed(xml_or_io.to_s, final: true)
|
|
48
|
-
recorder.each_event { |*args| yield(*args) }
|
|
55
|
+
recorder.each_event(*Array(kinds)) { |*args| yield(*args) }
|
|
49
56
|
end
|
|
50
57
|
ensure
|
|
51
58
|
recorder.free
|
|
@@ -72,19 +79,23 @@ class Leptris::XML::SAX::Recorder
|
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
# Feed an IO in chunks, draining events after each chunk.
|
|
75
|
-
def feed_stream(io, chunk_size = 65_536, &block)
|
|
82
|
+
def feed_stream(io, chunk_size = 65_536, kinds: nil, &block)
|
|
76
83
|
while (chunk = io.read(chunk_size))
|
|
77
84
|
feed(chunk)
|
|
78
|
-
each_event(&block)
|
|
85
|
+
each_event(*Array(kinds), &block)
|
|
79
86
|
end
|
|
80
87
|
feed("", final: true)
|
|
81
|
-
each_event(&block)
|
|
88
|
+
each_event(*Array(kinds), &block)
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
# Drains the current chunk's records: bulk-reads the record array
|
|
85
92
|
# and the packed arena (two FFI calls), then slices every string
|
|
86
|
-
# from the arena in Ruby — no per-string FFI.
|
|
87
|
-
|
|
93
|
+
# from the arena in Ruby — no per-string FFI. With +kinds+, records
|
|
94
|
+
# of other kinds are skipped BEFORE any string is sliced: an
|
|
95
|
+
# unwanted event costs one Array read, and cost scales with what
|
|
96
|
+
# the consumer asked for, not with the document's event mix.
|
|
97
|
+
def each_event(*kinds)
|
|
98
|
+
wanted = kinds.empty? ? nil : wanted_set(kinds)
|
|
88
99
|
count_ptr = ::FFI::MemoryPointer.new(:size_t)
|
|
89
100
|
len_ptr = ::FFI::MemoryPointer.new(:size_t)
|
|
90
101
|
begin
|
|
@@ -108,8 +119,8 @@ class Leptris::XML::SAX::Recorder
|
|
|
108
119
|
|
|
109
120
|
i = 0
|
|
110
121
|
while i < count
|
|
111
|
-
kind =
|
|
112
|
-
if kind
|
|
122
|
+
kind = KIND_BY_CODE[fields[i * 9]]
|
|
123
|
+
if kind && (wanted.nil? || wanted[kind])
|
|
113
124
|
base = i * 9 + 1
|
|
114
125
|
yield kind,
|
|
115
126
|
slice(arena, fields[base], fields[base + 1]),
|
|
@@ -128,6 +139,18 @@ class Leptris::XML::SAX::Recorder
|
|
|
128
139
|
|
|
129
140
|
private
|
|
130
141
|
|
|
142
|
+
def wanted_set(kinds)
|
|
143
|
+
unknown = kinds.each_with_object([]) do |kind, acc|
|
|
144
|
+
acc << kind unless KINDS.value?(kind)
|
|
145
|
+
end
|
|
146
|
+
unless unknown.empty?
|
|
147
|
+
raise ArgumentError,
|
|
148
|
+
"unknown event kinds #{unknown.inspect} " \
|
|
149
|
+
"(known: #{KINDS.values.inspect})"
|
|
150
|
+
end
|
|
151
|
+
kinds.each_with_object({}) { |kind, set| set[kind] = true }
|
|
152
|
+
end
|
|
153
|
+
|
|
131
154
|
# Zero-length slices are empty strings, not nil — the default
|
|
132
155
|
# namespace prefix is legitimately "" (START_PREFIX: name may be
|
|
133
156
|
# ""), matching the callback transport's delivery.
|