featurevisor 3.1.0 → 4.0.0
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/README.md +300 -182
- data/bin/cli.rb +84 -17
- data/bin/commands/assess_distribution.rb +10 -52
- data/bin/commands/benchmark.rb +10 -48
- data/bin/commands/node_cli.rb +20 -0
- data/bin/commands/runtime_project.rb +63 -0
- data/bin/commands/test.rb +735 -260
- data/bin/commands.rb +1 -0
- data/bin/segment_tests.rb +89 -0
- data/bin/test_checks.rb +767 -0
- data/lib/featurevisor/child_instance.rb +15 -2
- data/lib/featurevisor/compare_versions.rb +18 -36
- data/lib/featurevisor/conditions.rb +91 -9
- data/lib/featurevisor/dependencies.rb +150 -0
- data/lib/featurevisor/emitter.rb +4 -0
- data/lib/featurevisor/evaluate.rb +64 -115
- data/lib/featurevisor/evaluation_data_provider.rb +100 -51
- data/lib/featurevisor/evaluation_session.rb +191 -0
- data/lib/featurevisor/events.rb +39 -100
- data/lib/featurevisor/instance.rb +258 -59
- data/lib/featurevisor/modules.rb +6 -46
- data/lib/featurevisor/mutations.rb +213 -0
- data/lib/featurevisor/normalize.rb +105 -0
- data/lib/featurevisor/unified_conditions.rb +79 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +7 -0
- metadata +10 -1
|
@@ -10,9 +10,13 @@ module Featurevisor
|
|
|
10
10
|
@sticky_variables = options[:sticky_variables] || {}
|
|
11
11
|
@emitter = Featurevisor::Emitter.new
|
|
12
12
|
@parent_unsubscribers = []
|
|
13
|
+
@closed = false
|
|
14
|
+
@evaluation_generation = 0
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def on(event_name, callback = nil, &block)
|
|
18
|
+
return -> {} if @closed
|
|
19
|
+
|
|
16
20
|
callback = block if block_given?
|
|
17
21
|
if %w[context_set sticky_features_set sticky_variables_set].include?(event_name)
|
|
18
22
|
return @emitter.on(event_name, callback)
|
|
@@ -33,6 +37,9 @@ module Featurevisor
|
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def close
|
|
40
|
+
return if @closed
|
|
41
|
+
|
|
42
|
+
@closed = true
|
|
36
43
|
@parent_unsubscribers.dup.each(&:call)
|
|
37
44
|
@parent_unsubscribers.clear
|
|
38
45
|
@emitter.clear_all
|
|
@@ -40,6 +47,7 @@ module Featurevisor
|
|
|
40
47
|
|
|
41
48
|
def set_context(context, replace = false)
|
|
42
49
|
@context = replace ? context : { **@context, **context }
|
|
50
|
+
@evaluation_generation += 1
|
|
43
51
|
@emitter.trigger("context_set", context: @context, replaced: replace)
|
|
44
52
|
end
|
|
45
53
|
|
|
@@ -50,6 +58,7 @@ module Featurevisor
|
|
|
50
58
|
def set_sticky_features(sticky, replace = false)
|
|
51
59
|
previous = @sticky_features
|
|
52
60
|
@sticky_features = replace ? sticky : { **@sticky_features, **sticky }
|
|
61
|
+
@evaluation_generation += 1
|
|
53
62
|
params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous, @sticky_features, replace)
|
|
54
63
|
@emitter.trigger("sticky_features_set", params)
|
|
55
64
|
end
|
|
@@ -57,6 +66,7 @@ module Featurevisor
|
|
|
57
66
|
def set_sticky_variables(sticky, replace = false)
|
|
58
67
|
previous = @sticky_variables
|
|
59
68
|
@sticky_variables = replace ? sticky : { **@sticky_variables, **sticky }
|
|
69
|
+
@evaluation_generation += 1
|
|
60
70
|
@emitter.trigger(
|
|
61
71
|
"sticky_variables_set",
|
|
62
72
|
Featurevisor::Events.get_params_for_sticky_variables_set_event(previous, @sticky_variables, replace)
|
|
@@ -87,7 +97,7 @@ module Featurevisor
|
|
|
87
97
|
delegate_variable(:evaluate_variable, feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
88
98
|
end
|
|
89
99
|
|
|
90
|
-
%i[boolean string integer
|
|
100
|
+
%i[boolean string integer number array object json].each do |type|
|
|
91
101
|
define_method("get_variable_#{type}") do |feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {}|
|
|
92
102
|
delegate_variable("get_variable_#{type}".to_sym, feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
93
103
|
end
|
|
@@ -111,7 +121,10 @@ module Featurevisor
|
|
|
111
121
|
{
|
|
112
122
|
**options,
|
|
113
123
|
__featurevisor_child_sticky_features: @sticky_features,
|
|
114
|
-
__featurevisor_child_sticky_variables: @sticky_variables
|
|
124
|
+
__featurevisor_child_sticky_variables: @sticky_variables,
|
|
125
|
+
__featurevisor_sticky_features_source: -> { @sticky_features },
|
|
126
|
+
__featurevisor_sticky_variables_source: -> { @sticky_variables },
|
|
127
|
+
__featurevisor_child_generation: -> { @evaluation_generation }
|
|
115
128
|
}
|
|
116
129
|
end
|
|
117
130
|
|
|
@@ -19,7 +19,7 @@ module Featurevisor
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
match = version.match(SEMVER_REGEX)
|
|
22
|
-
unless match
|
|
22
|
+
unless match && version.ascii_only? && match[0] == version
|
|
23
23
|
raise ArgumentError, "Invalid argument not valid semver ('#{version}' received)"
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -34,50 +34,29 @@ module Featurevisor
|
|
|
34
34
|
s == "*" || s.downcase == "x"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Forces types to be the same for comparison
|
|
38
|
-
# @param a [String, Integer] First value
|
|
39
|
-
# @param b [String, Integer] Second value
|
|
40
|
-
# @return [Array] Array with both values converted to same type
|
|
41
|
-
def self.force_type(a, b)
|
|
42
|
-
if a.is_a?(Integer) != b.is_a?(Integer)
|
|
43
|
-
[a.to_s, b.to_s]
|
|
44
|
-
else
|
|
45
|
-
[a, b]
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Tries to parse a string as an integer
|
|
50
|
-
# @param v [String] String to parse
|
|
51
|
-
# @return [Integer, String] Parsed integer or original string
|
|
52
|
-
def self.try_parse(v)
|
|
53
|
-
Integer(v, 10)
|
|
54
|
-
rescue ArgumentError
|
|
55
|
-
v
|
|
56
|
-
end
|
|
57
|
-
|
|
58
37
|
# Compares two strings for version comparison
|
|
59
38
|
# @param a [String] First string
|
|
60
39
|
# @param b [String] Second string
|
|
61
40
|
# @return [Integer] -1 if a < b, 0 if equal, 1 if a > b
|
|
62
|
-
def self.compare_strings(a, b)
|
|
63
|
-
return 0 if wildcard?(a) || wildcard?(b)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
0
|
|
41
|
+
def self.compare_strings(a, b, prerelease = false)
|
|
42
|
+
return 0 if !prerelease && (wildcard?(a) || wildcard?(b))
|
|
43
|
+
an = /\A[0-9]+\z/.match?(a)
|
|
44
|
+
bn = /\A[0-9]+\z/.match?(b)
|
|
45
|
+
if an && bn
|
|
46
|
+
a = a.sub(/\A0+/, "")
|
|
47
|
+
b = b.sub(/\A0+/, "")
|
|
48
|
+
return a.length <=> b.length if a.length != b.length
|
|
49
|
+
elsif prerelease && an != bn
|
|
50
|
+
return an ? -1 : 1
|
|
73
51
|
end
|
|
52
|
+
a <=> b
|
|
74
53
|
end
|
|
75
54
|
|
|
76
55
|
# Compares version segments
|
|
77
56
|
# @param a [Array<String>, MatchData] First version segments
|
|
78
57
|
# @param b [Array<String>, MatchData] Second version segments
|
|
79
58
|
# @return [Integer] -1 if a < b, 0 if equal, 1 if a > b
|
|
80
|
-
def self.compare_segments(a, b)
|
|
59
|
+
def self.compare_segments(a, b, prerelease = false)
|
|
81
60
|
# Convert to arrays if needed
|
|
82
61
|
a_array = a.is_a?(MatchData) ? a.to_a[1..-1] : a.to_a
|
|
83
62
|
b_array = b.is_a?(MatchData) ? b.to_a[1..-1] : b.to_a
|
|
@@ -85,10 +64,13 @@ module Featurevisor
|
|
|
85
64
|
max_length = [a_array.length, b_array.length].max
|
|
86
65
|
|
|
87
66
|
(0...max_length).each do |i|
|
|
67
|
+
if prerelease && (i == a_array.length || i == b_array.length)
|
|
68
|
+
return a_array.length <=> b_array.length
|
|
69
|
+
end
|
|
88
70
|
a_val = a_array[i] || "0"
|
|
89
71
|
b_val = b_array[i] || "0"
|
|
90
72
|
|
|
91
|
-
result = compare_strings(a_val, b_val)
|
|
73
|
+
result = compare_strings(a_val, b_val, prerelease)
|
|
92
74
|
return result unless result == 0
|
|
93
75
|
end
|
|
94
76
|
|
|
@@ -116,7 +98,7 @@ module Featurevisor
|
|
|
116
98
|
|
|
117
99
|
# Validate pre-release
|
|
118
100
|
if p1 && p2
|
|
119
|
-
compare_segments(p1.split("."), p2.split("."))
|
|
101
|
+
compare_segments(p1.split("."), p2.split("."), true)
|
|
120
102
|
elsif p1 || p2
|
|
121
103
|
p1 ? -1 : 1
|
|
122
104
|
else
|
|
@@ -23,6 +23,16 @@ module Featurevisor
|
|
|
23
23
|
def self.get_value_with_presence(obj, path)
|
|
24
24
|
return MISSING unless obj.is_a?(Hash) && path.is_a?(String)
|
|
25
25
|
|
|
26
|
+
# Most attributes are direct keys. Avoid allocating a path array for
|
|
27
|
+
# every condition while retaining the existing nested path behaviour.
|
|
28
|
+
unless path.empty? || path.include?(".")
|
|
29
|
+
symbol = path.to_sym
|
|
30
|
+
return obj[symbol] if obj.key?(symbol)
|
|
31
|
+
return obj[path] if obj.key?(path)
|
|
32
|
+
|
|
33
|
+
return MISSING
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
path.split(".").reduce(obj) do |current, key|
|
|
27
37
|
break MISSING unless current.is_a?(Hash)
|
|
28
38
|
|
|
@@ -39,7 +49,7 @@ module Featurevisor
|
|
|
39
49
|
|
|
40
50
|
def self.strict_equal?(left, right)
|
|
41
51
|
return true if left.nil? && right.nil?
|
|
42
|
-
return left
|
|
52
|
+
return left == right if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
43
53
|
return left == right if left.is_a?(String) && right.is_a?(String)
|
|
44
54
|
return left == right if (left == true || left == false) && (right == true || right == false)
|
|
45
55
|
|
|
@@ -47,6 +57,15 @@ module Featurevisor
|
|
|
47
57
|
end
|
|
48
58
|
private_class_method :strict_equal?
|
|
49
59
|
|
|
60
|
+
def self.missing_value
|
|
61
|
+
MISSING
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.value_is_matched(raw_value, condition, get_regex)
|
|
65
|
+
context = raw_value.equal?(MISSING) ? {} : { value: raw_value }
|
|
66
|
+
condition_is_matched(condition.merge(attribute: "value"), context, get_regex)
|
|
67
|
+
end
|
|
68
|
+
|
|
50
69
|
# Check if a condition is matched against context
|
|
51
70
|
# @param condition [Hash] Condition to evaluate
|
|
52
71
|
# @param context [Hash] Context to evaluate against
|
|
@@ -55,7 +74,11 @@ module Featurevisor
|
|
|
55
74
|
def self.condition_is_matched(condition, context, get_regex)
|
|
56
75
|
attribute = condition["attribute"] || condition[:attribute]
|
|
57
76
|
operator = condition["operator"] || condition[:operator]
|
|
58
|
-
value =
|
|
77
|
+
value = if condition.key?(:value)
|
|
78
|
+
condition[:value]
|
|
79
|
+
elsif condition.key?("value")
|
|
80
|
+
condition["value"]
|
|
81
|
+
end
|
|
59
82
|
regex_flags = condition["regexFlags"] || condition[:regexFlags]
|
|
60
83
|
|
|
61
84
|
raw_context_value = get_value_with_presence(context, attribute)
|
|
@@ -63,6 +86,18 @@ module Featurevisor
|
|
|
63
86
|
attribute_exists = !raw_context_value.equal?(MISSING)
|
|
64
87
|
|
|
65
88
|
case operator
|
|
89
|
+
when "empty", "notEmpty"
|
|
90
|
+
return false unless attribute_exists && !context_value_from_path.nil?
|
|
91
|
+
|
|
92
|
+
size = if context_value_from_path.is_a?(String) || context_value_from_path.is_a?(Array) ||
|
|
93
|
+
context_value_from_path.is_a?(Hash)
|
|
94
|
+
context_value_from_path.size
|
|
95
|
+
end
|
|
96
|
+
!size.nil? && (operator == "empty" ? size.zero? : size.positive?)
|
|
97
|
+
when "isNull"
|
|
98
|
+
attribute_exists && context_value_from_path.nil?
|
|
99
|
+
when "notNull"
|
|
100
|
+
attribute_exists && !context_value_from_path.nil?
|
|
66
101
|
when "equals"
|
|
67
102
|
attribute_exists && strict_equal?(context_value_from_path, value)
|
|
68
103
|
when "notEquals"
|
|
@@ -148,6 +183,12 @@ module Featurevisor
|
|
|
148
183
|
attribute_exists
|
|
149
184
|
when "notExists"
|
|
150
185
|
!attribute_exists
|
|
186
|
+
when "includesAny", "notIncludesAny"
|
|
187
|
+
return false unless context_value_from_path.is_a?(Array) && value.is_a?(Array)
|
|
188
|
+
return false unless value.all? { |candidate| candidate.nil? || candidate.is_a?(String) || candidate.is_a?(Integer) || candidate.is_a?(Float) || candidate == true || candidate == false }
|
|
189
|
+
|
|
190
|
+
matched = value.any? { |candidate| context_value_from_path.any? { |actual| strict_equal?(actual, candidate) } }
|
|
191
|
+
operator == "includesAny" ? matched : !matched
|
|
151
192
|
when "includes", "notIncludes"
|
|
152
193
|
# includes / notIncludes (where context value is an array)
|
|
153
194
|
if context_value_from_path.is_a?(Array) &&
|
|
@@ -167,15 +208,56 @@ module Featurevisor
|
|
|
167
208
|
end
|
|
168
209
|
|
|
169
210
|
def self.portable_date(value)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
if value.is_a?(String)
|
|
212
|
+
match = /\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?(?:Z|([+\-])([0-9]{2}):([0-9]{2}))\z/.match(value)
|
|
213
|
+
return nil unless match
|
|
214
|
+
return nil if value.start_with?("0000-")
|
|
215
|
+
if match[2] && (match[2].to_i > 14 || match[3].to_i > 59 || (match[2] == "14" && match[3] != "00"))
|
|
216
|
+
return nil
|
|
217
|
+
end
|
|
218
|
+
parsed = Time.iso8601(value)
|
|
219
|
+
# Time.iso8601 must not normalize an invalid calendar date or clock time.
|
|
220
|
+
return nil unless parsed.getlocal(match[1] ? "#{match[1]}#{match[2]}:#{match[3]}" : "+00:00").strftime("%Y-%m-%dT%H:%M:%S") == value[0, 19]
|
|
221
|
+
elsif value.is_a?(Time) || value.is_a?(DateTime)
|
|
222
|
+
return nil unless (1..9999).cover?(value.year)
|
|
223
|
+
offset_seconds = value.is_a?(DateTime) ? value.offset * 86400 : value.utc_offset
|
|
224
|
+
return nil if offset_seconds.abs > 14 * 3600
|
|
225
|
+
parsed = value.to_time
|
|
226
|
+
else
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
milliseconds = (parsed.to_r * 1000).floor
|
|
230
|
+
result = Time.at(Rational(milliseconds, 1000)).utc
|
|
231
|
+
return nil if result.year < 1 || result.year > 9999
|
|
232
|
+
result
|
|
233
|
+
rescue ArgumentError, RangeError
|
|
177
234
|
nil
|
|
178
235
|
end
|
|
179
236
|
private_class_method :portable_date
|
|
237
|
+
|
|
238
|
+
def self.normalize_portable_regex(pattern)
|
|
239
|
+
inside = {
|
|
240
|
+
'd' => '0-9', 'D' => '\\u{0}-\\u{2F}\\u{3A}-\\u{10FFFF}',
|
|
241
|
+
'w' => 'A-Za-z0-9_', 'W' => '\\u{0}-\\u{2F}\\u{3A}-\\u{40}\\u{5B}-\\u{5E}\\u{60}\\u{7B}-\\u{10FFFF}'
|
|
242
|
+
}
|
|
243
|
+
outside = { 'd' => '[0-9]', 'D' => '[^0-9]', 'w' => '(?-i:[A-Za-z0-9_])', 'W' => '(?-i:[^A-Za-z0-9_])' }
|
|
244
|
+
output = +''
|
|
245
|
+
in_class = false
|
|
246
|
+
index = 0
|
|
247
|
+
while index < pattern.length
|
|
248
|
+
char = pattern[index]
|
|
249
|
+
if char == '\\' && index + 1 < pattern.length
|
|
250
|
+
code = pattern[index + 1]
|
|
251
|
+
output << (in_class ? inside : outside).fetch(code, pattern[index, 2])
|
|
252
|
+
index += 2
|
|
253
|
+
next
|
|
254
|
+
end
|
|
255
|
+
in_class = true if char == '[' && !in_class
|
|
256
|
+
in_class = false if char == ']' && in_class
|
|
257
|
+
output << char
|
|
258
|
+
index += 1
|
|
259
|
+
end
|
|
260
|
+
output
|
|
261
|
+
end
|
|
180
262
|
end
|
|
181
263
|
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Featurevisor
|
|
6
|
+
module Dependencies
|
|
7
|
+
def self.extract_entity_dependencies(entry)
|
|
8
|
+
sources = {}
|
|
9
|
+
add = lambda do |kind, key, dependent|
|
|
10
|
+
source = [kind, key.to_sym]
|
|
11
|
+
sources[source] = true
|
|
12
|
+
end
|
|
13
|
+
conditions = nil
|
|
14
|
+
conditions = lambda do |value, dependent|
|
|
15
|
+
case value
|
|
16
|
+
when String
|
|
17
|
+
next if value == "*"
|
|
18
|
+
if value.lstrip.match?(/\A[\{\[\"]/)
|
|
19
|
+
begin
|
|
20
|
+
conditions.call(JSON.parse(value, symbolize_names: true), dependent)
|
|
21
|
+
rescue JSON::ParserError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
add.call(:segment, value, dependent)
|
|
26
|
+
end
|
|
27
|
+
when Array
|
|
28
|
+
value.each { |child| conditions.call(child, dependent) }
|
|
29
|
+
when Hash
|
|
30
|
+
if value.key?(:variable)
|
|
31
|
+
add.call(value[:feature] ? :feature : :variable, value[:feature] || value[:variable], dependent)
|
|
32
|
+
elsif value.key?(:feature)
|
|
33
|
+
add.call(:feature, value[:feature], dependent)
|
|
34
|
+
elsif !value.key?(:attribute)
|
|
35
|
+
%i[and or not].each { |group| conditions.call(value[group], dependent) if value.key?(group) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
visit = nil
|
|
40
|
+
visit = lambda do |entry, dependent|
|
|
41
|
+
conditions.call(entry[:conditions], dependent) if entry.key?(:conditions)
|
|
42
|
+
%i[force traffic variations overrides overrideProgram].each do |name|
|
|
43
|
+
(entry[name] || []).each { |child| visit.call(child, dependent) }
|
|
44
|
+
end
|
|
45
|
+
(entry[:variableOverrides] || {}).each_value do |overrides|
|
|
46
|
+
overrides.each { |child| visit.call(child, dependent) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
visit.call(entry, nil)
|
|
50
|
+
compose = entry[:compose]
|
|
51
|
+
if compose.is_a?(String)
|
|
52
|
+
add.call(:variable, compose, nil)
|
|
53
|
+
elsif compose.is_a?(Array)
|
|
54
|
+
compose.each do |reference|
|
|
55
|
+
add.call(reference[:feature] ? :feature : :variable, reference[:feature] || reference[:variable], nil)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
sources.keys
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
COLLECTIONS = { segments: :segment, features: :feature, variables: :variable }.freeze
|
|
62
|
+
|
|
63
|
+
# Edge identities do not retain old providers, definitions or caller values.
|
|
64
|
+
class Index
|
|
65
|
+
EMPTY = [].freeze
|
|
66
|
+
|
|
67
|
+
def initialize
|
|
68
|
+
@reverse = {}
|
|
69
|
+
@owners = {}
|
|
70
|
+
@order = {}
|
|
71
|
+
@next_order = 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def update(datafile, incoming, previous = nil)
|
|
75
|
+
updates = []
|
|
76
|
+
COLLECTIONS.each do |name, kind|
|
|
77
|
+
definitions = datafile[name] || {}
|
|
78
|
+
(incoming[name] || {}).each_key do |key|
|
|
79
|
+
owner = [kind, key.to_sym]
|
|
80
|
+
entry = definitions[key.to_sym]
|
|
81
|
+
# Hashes do not identify edges. Reuse only an identical definition.
|
|
82
|
+
next if previous && @owners.key?(owner) && entry && entry == (previous[name] || {})[key.to_sym]
|
|
83
|
+
updates << [owner, entry && Dependencies.extract_entity_dependencies(entry)]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
updates.each do |owner, sources|
|
|
87
|
+
(@owners.delete(owner) || []).each do |source|
|
|
88
|
+
dependents = @reverse[source]
|
|
89
|
+
dependents.delete(owner)
|
|
90
|
+
@reverse.delete(source) if dependents.empty?
|
|
91
|
+
end
|
|
92
|
+
unless sources
|
|
93
|
+
@order.delete(owner)
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
unless @order.key?(owner)
|
|
97
|
+
@order[owner] = @next_order
|
|
98
|
+
@next_order += 1
|
|
99
|
+
end
|
|
100
|
+
@owners[owner] = sources
|
|
101
|
+
sources.each { |source| (@reverse[source] ||= {})[owner] = true }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def reorder(datafile)
|
|
106
|
+
@order = {}
|
|
107
|
+
COLLECTIONS.each do |name, kind|
|
|
108
|
+
(datafile[name] || {}).each_key.with_index { |key, rank| @order[[kind, key.to_sym]] = rank }
|
|
109
|
+
end
|
|
110
|
+
@next_order = (@order.values.max || -1) + 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dependents(identity)
|
|
114
|
+
dependents = @reverse[identity]
|
|
115
|
+
dependents ? dependents.keys.sort_by { |owner| @order.fetch(owner) } : EMPTY
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.create_index(datafile)
|
|
120
|
+
index = Index.new
|
|
121
|
+
index.update(datafile, datafile)
|
|
122
|
+
index
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.add_changes(features, variables, changed_segments, index)
|
|
126
|
+
kinds = %i[feature variable segment]
|
|
127
|
+
affected = [features, variables, changed_segments].map { |keys| keys.to_h { |key| [key.to_sym, true] } }
|
|
128
|
+
queues = affected.map(&:keys)
|
|
129
|
+
cursors = [0, 0, 0]
|
|
130
|
+
loop do
|
|
131
|
+
advanced = false
|
|
132
|
+
kinds.each_with_index do |kind, source|
|
|
133
|
+
next if cursors[source] >= queues[source].length
|
|
134
|
+
advanced = true
|
|
135
|
+
key = queues[source][cursors[source]]
|
|
136
|
+
cursors[source] += 1
|
|
137
|
+
index.dependents([kind, key]).each do |target_kind, target_key|
|
|
138
|
+
target = kinds.index(target_kind)
|
|
139
|
+
next if affected[target].key?(target_key)
|
|
140
|
+
affected[target][target_key] = true
|
|
141
|
+
queues[target] << target_key
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
break unless advanced
|
|
145
|
+
end
|
|
146
|
+
features.replace(affected[0].keys)
|
|
147
|
+
variables.replace(affected[1].keys)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
data/lib/featurevisor/emitter.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Featurevisor
|
|
|
31
31
|
is_active = false
|
|
32
32
|
index = listeners.index(callback)
|
|
33
33
|
listeners.delete_at(index) if index && index >= 0
|
|
34
|
+
callback = nil
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -54,6 +55,9 @@ module Featurevisor
|
|
|
54
55
|
|
|
55
56
|
# Clear all event listeners
|
|
56
57
|
def clear_all
|
|
58
|
+
# Cleanup functions may still reference the previous listener arrays.
|
|
59
|
+
# Empty those arrays too, rather than retaining unrelated callbacks.
|
|
60
|
+
@listeners.each_value(&:clear)
|
|
57
61
|
@listeners = {}
|
|
58
62
|
end
|
|
59
63
|
end
|