hackle-ruby-sdk 1.0.0 → 2.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hackle/client.rb +186 -87
  3. data/lib/hackle/config.rb +59 -17
  4. data/lib/hackle/decision.rb +113 -0
  5. data/lib/hackle/event.rb +89 -0
  6. data/lib/hackle/internal/clock/clock.rb +47 -0
  7. data/lib/hackle/internal/concurrent/executors.rb +20 -0
  8. data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
  9. data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
  10. data/lib/hackle/internal/config/parameter_config.rb +50 -0
  11. data/lib/hackle/internal/core/hackle_core.rb +182 -0
  12. data/lib/hackle/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
  13. data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
  14. data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
  15. data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
  16. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
  17. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
  18. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
  19. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
  20. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
  21. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
  22. data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
  23. data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
  24. data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
  25. data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
  26. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
  27. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
  28. data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
  29. data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
  30. data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
  31. data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
  32. data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
  33. data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
  34. data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
  35. data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
  36. data/lib/hackle/internal/event/user_event.rb +187 -0
  37. data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
  38. data/lib/hackle/internal/event/user_event_factory.rb +58 -0
  39. data/lib/hackle/internal/event/user_event_processor.rb +181 -0
  40. data/lib/hackle/internal/http/http.rb +28 -0
  41. data/lib/hackle/internal/http/http_client.rb +48 -0
  42. data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
  43. data/lib/hackle/internal/logger/logger.rb +31 -0
  44. data/lib/hackle/internal/model/action.rb +57 -0
  45. data/lib/hackle/internal/model/bucket.rb +58 -0
  46. data/lib/hackle/internal/model/container.rb +47 -0
  47. data/lib/hackle/internal/model/decision_reason.rb +31 -0
  48. data/lib/hackle/{models → internal/model}/event_type.rb +5 -8
  49. data/lib/hackle/internal/model/experiment.rb +194 -0
  50. data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
  51. data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
  52. data/lib/hackle/internal/model/sdk.rb +23 -0
  53. data/lib/hackle/internal/model/segment.rb +61 -0
  54. data/lib/hackle/internal/model/target.rb +203 -0
  55. data/lib/hackle/internal/model/target_rule.rb +19 -0
  56. data/lib/hackle/internal/model/targeting.rb +45 -0
  57. data/lib/hackle/internal/model/value_type.rb +75 -0
  58. data/lib/hackle/internal/model/variation.rb +27 -0
  59. data/lib/hackle/internal/model/version.rb +153 -0
  60. data/lib/hackle/internal/properties/properties_builder.rb +101 -0
  61. data/lib/hackle/internal/user/hackle_user.rb +74 -0
  62. data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
  63. data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
  64. data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
  65. data/lib/hackle/internal/workspace/workspace.rb +353 -0
  66. data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
  67. data/lib/hackle/remote_config.rb +55 -0
  68. data/lib/hackle/user.rb +124 -0
  69. data/lib/hackle/version.rb +1 -11
  70. data/lib/hackle.rb +4 -69
  71. metadata +123 -53
  72. data/.gitignore +0 -11
  73. data/.rspec +0 -2
  74. data/.travis.yml +0 -7
  75. data/Gemfile +0 -6
  76. data/README.md +0 -33
  77. data/Rakefile +0 -6
  78. data/hackle-ruby-sdk.gemspec +0 -29
  79. data/lib/hackle/decision/decider.rb +0 -69
  80. data/lib/hackle/events/event_dispatcher.rb +0 -96
  81. data/lib/hackle/events/event_processor.rb +0 -126
  82. data/lib/hackle/events/user_event.rb +0 -61
  83. data/lib/hackle/http/http.rb +0 -37
  84. data/lib/hackle/models/bucket.rb +0 -26
  85. data/lib/hackle/models/event.rb +0 -26
  86. data/lib/hackle/models/experiment.rb +0 -69
  87. data/lib/hackle/models/slot.rb +0 -22
  88. data/lib/hackle/models/user.rb +0 -24
  89. data/lib/hackle/models/variation.rb +0 -21
  90. data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
  91. data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
  92. data/lib/hackle/workspaces/workspace.rb +0 -100
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/target'
4
+
5
+ module Hackle
6
+ class TargetingType
7
+
8
+ # @!attribute [r] supported_key_types
9
+ # @return [Array<TargetKeyType>]
10
+ attr_reader :supported_key_types
11
+
12
+ # @param supported_key_types [Array<TargetKeyType>]
13
+ def initialize(supported_key_types)
14
+ # @type [Array<TargetKeyType>]
15
+ @supported_key_types = supported_key_types
16
+ end
17
+
18
+ # @param key_type [TargetKeyType]
19
+ def supports?(key_type)
20
+ @supported_key_types.include?(key_type)
21
+ end
22
+
23
+ IDENTIFIER = new(
24
+ [
25
+ TargetKeyType::SEGMENT
26
+ ]
27
+ )
28
+ PROPERTY = new(
29
+ [
30
+ TargetKeyType::SEGMENT,
31
+ TargetKeyType::USER_PROPERTY,
32
+ TargetKeyType::HACKLE_PROPERTY,
33
+ TargetKeyType::AB_TEST,
34
+ TargetKeyType::FEATURE_FLAG
35
+ ]
36
+ )
37
+ SEGMENT = new(
38
+ [
39
+ TargetKeyType::USER_ID,
40
+ TargetKeyType::USER_PROPERTY,
41
+ TargetKeyType::HACKLE_PROPERTY
42
+ ]
43
+ )
44
+ end
45
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class ValueType
5
+ # @!attribute [r] name
6
+ # @return [String]
7
+ attr_reader :name
8
+
9
+ # @param name [String]
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+
14
+ def to_s
15
+ name
16
+ end
17
+
18
+ NULL = new('NULL')
19
+ UNKNOWN = new('UNKNOWN')
20
+ STRING = new('STRING')
21
+ NUMBER = new('NUMBER')
22
+ BOOLEAN = new('BOOLEAN')
23
+ VERSION = new('VERSION')
24
+ JSON = new('JSON')
25
+
26
+ @types = {
27
+ 'NULL' => NULL,
28
+ 'UNKNOWN' => UNKNOWN,
29
+ 'STRING' => STRING,
30
+ 'NUMBER' => NUMBER,
31
+ 'BOOLEAN' => BOOLEAN,
32
+ 'VERSION' => VERSION,
33
+ 'JSON' => JSON
34
+ }.freeze
35
+
36
+ # @param name [String]
37
+ # @return [ValueType, nil]
38
+ def self.from_or_nil(name)
39
+ @types[name.upcase]
40
+ end
41
+
42
+ # @return [Array<ValueType>]
43
+ def self.values
44
+ @types.values
45
+ end
46
+
47
+ class << self
48
+ def string?(value)
49
+ return false if value.nil?
50
+
51
+ value.is_a?(String)
52
+ end
53
+
54
+ def empty_string?(value)
55
+ string?(value) && value.empty?
56
+ end
57
+
58
+ def not_empty_string?(value)
59
+ string?(value) && !value.empty?
60
+ end
61
+
62
+ def number?(value)
63
+ return false if value.nil?
64
+
65
+ value.is_a?(Numeric)
66
+ end
67
+
68
+ def boolean?(value)
69
+ return false if value.nil?
70
+
71
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class Variation
5
+
6
+ # @!attribute [r] id
7
+ # @return [Integer]
8
+ # @!attribute [r] key
9
+ # @return [String]
10
+ # @!attribute [r] is_dropped
11
+ # @return [boolean]
12
+ # @!attribute [r] parameter_configuration_id
13
+ # @return [Integer]
14
+ attr_reader :id, :key, :is_dropped, :parameter_configuration_id
15
+
16
+ # @param id [Integer]
17
+ # @param key [String]
18
+ # @param is_dropped [boolean]
19
+ # @param parameter_configuration_id [Integer, nil]
20
+ def initialize(id:, key:, is_dropped:, parameter_configuration_id:)
21
+ @id = id
22
+ @key = key
23
+ @is_dropped = is_dropped
24
+ @parameter_configuration_id = parameter_configuration_id
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class Version
5
+ include Comparable
6
+
7
+ # @!attribute [r] core_version
8
+ # @return [CoreVersion]
9
+ # @!attribute [r] prerelease
10
+ # @return [MetadataVersion]
11
+ # @!attribute [r] build
12
+ # @return [MetadataVersion]
13
+ attr_reader :core_version, :prerelease, :build
14
+
15
+ # @param major [Integer]
16
+ # @param minor [Integer]
17
+ # @param patch [Integer]
18
+ # @param prerelease [MetadataVersion]
19
+ # @param build [MetadataVersion]
20
+
21
+ def initialize(core_version:, prerelease:, build:)
22
+ @core_version = core_version
23
+ @prerelease = prerelease
24
+ @build = build
25
+ end
26
+
27
+ VERSION_REGEX = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(\S+))?$/.freeze
28
+
29
+ # @param version [Object]
30
+ # @return [Version, nil]
31
+ def self.parse_or_nil(value)
32
+ return nil unless value.is_a?(String)
33
+
34
+ match = VERSION_REGEX.match(value)
35
+ return nil unless match
36
+
37
+ core_version = CoreVersion.new(major: match[1].to_i, minor: (match[2] || 0).to_i, patch: (match[3] || 0).to_i)
38
+ prerelease = MetadataVersion.parse(match[4])
39
+ build = MetadataVersion.parse(match[5])
40
+ new(core_version: core_version, prerelease: prerelease, build: build)
41
+ end
42
+
43
+ def <=>(other)
44
+ core_comparison = core_version <=> other.core_version
45
+ return core_comparison unless core_comparison.zero?
46
+
47
+ prerelease <=> other.prerelease
48
+ end
49
+
50
+ def ==(other)
51
+ other.is_a?(Version) && (self <=> other).zero?
52
+ end
53
+
54
+ def to_s
55
+ str = core_version.to_s
56
+ str += "-#{prerelease}" unless prerelease.empty?
57
+ str += "+#{build}" unless build.empty?
58
+ str
59
+ end
60
+ end
61
+
62
+ class CoreVersion
63
+ include Comparable
64
+
65
+ # @!attribute [r] major
66
+ # @return [Integer]
67
+ # @!attribute [r] minor
68
+ # @return [Integer]
69
+ # @!attribute [r] patch
70
+ # @return [Integer]
71
+ attr_reader :major, :minor, :patch
72
+
73
+ # @param major [Integer]
74
+ # @param minor [Integer]
75
+ # @param patch [Integer]
76
+ def initialize(major:, minor:, patch:)
77
+ @major = major
78
+ @minor = minor
79
+ @patch = patch
80
+ end
81
+
82
+ def <=>(other)
83
+ return nil unless other.is_a?(CoreVersion)
84
+
85
+ [major, minor, patch] <=> [other.major, other.minor, other.patch]
86
+ end
87
+
88
+ def ==(other)
89
+ other.is_a?(CoreVersion) && (major == other.major && minor == other.minor && patch == other.patch)
90
+ end
91
+
92
+ def to_s
93
+ "#{major}.#{minor}.#{patch}"
94
+ end
95
+ end
96
+
97
+ class MetadataVersion
98
+ include Comparable
99
+
100
+ # @!attribute [r] identifiers
101
+ # @return [Array<String>]
102
+ attr_reader :identifiers
103
+
104
+ def initialize(identifiers)
105
+ @identifiers = identifiers
106
+ end
107
+
108
+ def self.parse(value)
109
+ return new([]) unless value
110
+
111
+ new(value.split('.'))
112
+ end
113
+
114
+ def ==(other)
115
+ other.is_a?(MetadataVersion) && identifiers == other.identifiers
116
+ end
117
+
118
+ def <=>(other)
119
+ return 0 if empty? && other.empty?
120
+ return -1 if empty?
121
+ return 1 if other.empty?
122
+
123
+ compare_identifiers(other)
124
+ end
125
+
126
+ def compare_identifiers(other)
127
+ min_length = [identifiers.length, other.identifiers.length].min
128
+ min_length.times do |i|
129
+ result = compare_single_identifier(identifiers[i], other.identifiers[i])
130
+ return result if result != 0
131
+ end
132
+ identifiers.length <=> other.identifiers.length
133
+ end
134
+
135
+ def compare_single_identifier(id1, id2)
136
+ num1 = id1.to_i
137
+ num2 = id2.to_i
138
+ if id1.to_i.to_s == id1 && id2.to_i.to_s == id2
139
+ num1 <=> num2
140
+ else
141
+ id1 <=> id2
142
+ end
143
+ end
144
+
145
+ def to_s
146
+ identifiers.join('.')
147
+ end
148
+
149
+ def empty?
150
+ identifiers.empty?
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/logger/logger'
4
+
5
+ module Hackle
6
+ class PropertiesBuilder
7
+ def initialize
8
+ # @type [Hash{String => Object}]
9
+ @properties = {}
10
+ end
11
+
12
+ # @param key [String]
13
+ # @param value [Object, nil]
14
+ # @return [Hackle::PropertiesBuilder]
15
+ def add(key, value)
16
+ return self if @properties.length >= MAX_PROPERTIES_COUNT
17
+
18
+ unless valid_key?(key)
19
+ Log.get.warn { "Invalid property key: #{key} (expected: string[1..128])" }
20
+ return self
21
+ end
22
+
23
+ sanitized_value = sanitize_value_or_nil(key, value)
24
+ @properties[key] = sanitized_value unless sanitized_value.nil?
25
+ self
26
+ end
27
+
28
+ # @param properties [Hash{String => Object}]
29
+ # @return [Hackle::PropertiesBuilder]
30
+ def add_all(properties)
31
+ if properties.nil? || !properties.is_a?(Hash)
32
+ Log.get.warn { "Invalid properties: #{properties} (expected: Hash{String => Object})" }
33
+ return self
34
+ end
35
+
36
+ properties.each { |key, value| add(key, value) }
37
+ self
38
+ end
39
+
40
+ # @return [Hash{String (frozen)->Object}]
41
+ def build
42
+ @properties.dup
43
+ end
44
+
45
+ private
46
+
47
+ SYSTEM_PROPERTY_KEY_PREFIX = '$'
48
+ MAX_PROPERTIES_COUNT = 128
49
+ MAX_PROPERTY_KEY_LENGTH = 128
50
+ MAX_PROPERTY_VALUE_LENGTH = 1024
51
+
52
+ # @param key [String]
53
+ # @param value [Object, nil]
54
+ # @return [Object, nil]
55
+ def sanitize_value_or_nil(key, value)
56
+ return nil if value.nil?
57
+ return value.filter { |it| valid_element?(it) } if value.is_a?(Array)
58
+ return value if valid_value?(value)
59
+ return value if key.start_with?(SYSTEM_PROPERTY_KEY_PREFIX)
60
+
61
+ nil
62
+ end
63
+
64
+ # @param key [String]
65
+ # @return [boolean]
66
+ def valid_key?(key)
67
+ return false if key.nil?
68
+ return false unless key.is_a?(String)
69
+ return false if key.empty?
70
+ return false if key.length > MAX_PROPERTY_KEY_LENGTH
71
+
72
+ true
73
+ end
74
+
75
+ # @param value [Object]
76
+ # @return [boolean]
77
+ def valid_value?(value)
78
+ case value
79
+ when String
80
+ value.length <= MAX_PROPERTY_VALUE_LENGTH
81
+ when Numeric, TrueClass, FalseClass
82
+ true
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ # @param element [Object]
89
+ # @return [boolean]
90
+ def valid_element?(element)
91
+ case element
92
+ when String
93
+ element.length <= MAX_PROPERTY_VALUE_LENGTH
94
+ when Numeric
95
+ true
96
+ else
97
+ false
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/identifiers/identifier_builder'
4
+ require 'hackle/internal/properties/properties_builder'
5
+
6
+ module Hackle
7
+ class HackleUser
8
+
9
+ # @return [Hash{String => String}]
10
+ attr_reader :identifiers
11
+
12
+ # @return [Hash{String => Object}]
13
+ attr_reader :properties
14
+
15
+ # @param identifiers [Hash{String => String}]
16
+ # @param properties [Hash{String => Object}]
17
+ def initialize(identifiers:, properties:)
18
+ @identifiers = identifiers
19
+ @properties = properties
20
+ end
21
+
22
+ def ==(other)
23
+ other.is_a?(Hackle::HackleUser) && identifiers == other.identifiers && properties == other.properties
24
+ end
25
+
26
+ def self.builder
27
+ Builder.new
28
+ end
29
+
30
+ class Builder
31
+ def initialize
32
+ @identifiers = IdentifiersBuilder.new
33
+ @properties = PropertiesBuilder.new
34
+ end
35
+
36
+ # @param type [String]
37
+ # @param value [String, nil]
38
+ # @return [Builder]
39
+ def identifier(type, value)
40
+ @identifiers.add(type, value)
41
+ self
42
+ end
43
+
44
+ # @param identifiers [Hash{String => String}]
45
+ # @return [Builder]
46
+ def identifiers(identifiers)
47
+ @identifiers.add_all(identifiers)
48
+ self
49
+ end
50
+
51
+ # @param key [String]
52
+ # @param value [Object, nil]
53
+ # @return [Builder]
54
+ def property(key, value)
55
+ @properties.add(key, value)
56
+ self
57
+ end
58
+
59
+ # @param properties [Hash{String => Object}]
60
+ # @return [Builder]
61
+ def properties(properties)
62
+ @properties.add_all(properties)
63
+ self
64
+ end
65
+
66
+ def build
67
+ HackleUser.new(
68
+ identifiers: @identifiers.build,
69
+ properties: @properties.build
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/user'
4
+ require 'hackle/internal/user/hackle_user'
5
+
6
+ module Hackle
7
+ class HackleUserResolver
8
+ # @param user [User]
9
+ # @return [HackleUser]
10
+ def resolve_or_nil(user)
11
+ return nil if user.nil?
12
+ return nil unless user.is_a?(User)
13
+
14
+ builder = HackleUser.builder
15
+ builder.identifiers(user.identifiers)
16
+ builder.identifier('$id', user.id) unless user.id.nil?
17
+ builder.identifier('$deviceId', user.device_id) unless user.device_id.nil?
18
+ builder.identifier('$userId', user.user_id) unless user.user_id.nil?
19
+ builder.properties(user.properties)
20
+ hackle_user = builder.build
21
+
22
+ return nil if hackle_user.identifiers.empty?
23
+
24
+ hackle_user
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'hackle/internal/http/http'
5
+ require 'hackle/internal/workspace/workspace'
6
+
7
+ module Hackle
8
+ class HttpWorkspaceFetcher
9
+ # @param http_client [HttpClient]
10
+ # @param sdk [Sdk]
11
+ def initialize(http_client:, sdk:)
12
+
13
+ # @type [String]
14
+ @url = "/api/v2/workspaces/#{sdk.key}/config"
15
+
16
+ # @type [HttpClient]
17
+ @http_client = http_client
18
+
19
+ # @type [String, nil]
20
+ @last_modified = nil
21
+ end
22
+
23
+ # @return [Hackle::Workspace, nil]
24
+ def fetch_if_modified
25
+ request = create_request
26
+ response = @http_client.execute(request)
27
+ handle_response(response)
28
+ end
29
+
30
+ private
31
+
32
+ # @return [Net::HTTPRequest]
33
+ def create_request
34
+ request = Net::HTTP::Get.new(@url)
35
+ request['If-Modified-Since'] = @last_modified unless @last_modified.nil?
36
+ request
37
+ end
38
+
39
+ # @param response [Net::HTTPResponse]
40
+ # @return [Workspace, nil]
41
+ def handle_response(response)
42
+ return nil if HTTP.not_modified?(response)
43
+ raise "http status code: #{response.code}" unless HTTP.successful?(response)
44
+
45
+ @last_modified = response.header['Last-Modified']
46
+ response_body = JSON.parse(response.body, symbolize_names: true)
47
+ Workspace.from_hash(response_body)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/logger/logger'
4
+ require 'hackle/internal/workspace/workspace_fetcher'
5
+
6
+ module Hackle
7
+ class PollingWorkspaceFetcher
8
+ include WorkspaceFetcher
9
+ # @param http_workspace_fetcher [HttpWorkspaceFetcher]
10
+ # @param scheduler [Scheduler]
11
+ # @param polling_interval_seconds [Float]
12
+ def initialize(http_workspace_fetcher:, scheduler:, polling_interval_seconds:)
13
+ # @type [HttpWorkspaceFetcher]
14
+ @http_workspace_fetcher = http_workspace_fetcher
15
+
16
+ # @type [Scheduler]
17
+ @scheduler = scheduler
18
+
19
+ # @type [Float]
20
+ @polling_interval_seconds = polling_interval_seconds
21
+
22
+ # @type [ScheduledJob, nil]
23
+ @polling_job = nil
24
+
25
+ # @type [Workspace, nil]
26
+ @workspace = nil
27
+ end
28
+
29
+ # @return [Workspace, nil]
30
+ def fetch
31
+ @workspace
32
+ end
33
+
34
+ def start
35
+ return unless @polling_job.nil?
36
+
37
+ poll
38
+ @polling_job = @scheduler.schedule_periodically(@polling_interval_seconds, -> { poll })
39
+ end
40
+
41
+ def stop
42
+ @polling_job&.cancel
43
+ @polling_job = nil
44
+ end
45
+
46
+ def resume
47
+ @polling_job&.cancel
48
+ @polling_job = @scheduler.schedule_periodically(@polling_interval_seconds, -> { poll })
49
+ end
50
+
51
+ private
52
+
53
+ def poll
54
+ workspace = @http_workspace_fetcher.fetch_if_modified
55
+ return if workspace.nil?
56
+
57
+ @workspace = workspace
58
+ rescue => e
59
+ Log.get.error { "Failed to poll Workspace: #{e.inspect}" }
60
+ end
61
+ end
62
+ end