launchdarkly-server-sdk 8.11.2-java → 8.12.0-java

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +69 -9
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +227 -0
  5. data/lib/ldclient-rb/events.rb +34 -19
  6. data/lib/ldclient-rb/flags_state.rb +1 -1
  7. data/lib/ldclient-rb/impl/big_segments.rb +4 -4
  8. data/lib/ldclient-rb/impl/cache_store.rb +44 -0
  9. data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
  10. data/lib/ldclient-rb/impl/data_source/requestor.rb +113 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +83 -0
  12. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  13. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  14. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  15. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  16. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  17. data/lib/ldclient-rb/impl/data_store/status_provider.rb +76 -0
  18. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  19. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  20. data/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +77 -0
  21. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  22. data/lib/ldclient-rb/impl/data_system/fdv2.rb +472 -0
  23. data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
  24. data/lib/ldclient-rb/impl/data_system/polling.rb +628 -0
  25. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  26. data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
  27. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  28. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  29. data/lib/ldclient-rb/impl/event_sender.rb +14 -6
  30. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  31. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  32. data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +460 -0
  33. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +290 -0
  34. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  35. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  36. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  37. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  38. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  39. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  40. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  41. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
  42. data/lib/ldclient-rb/impl/util.rb +71 -0
  43. data/lib/ldclient-rb/impl.rb +1 -2
  44. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  45. data/lib/ldclient-rb/integrations/file_data.rb +67 -0
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  47. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  48. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  49. data/lib/ldclient-rb/integrations/test_data_v2.rb +254 -0
  50. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  51. data/lib/ldclient-rb/interfaces/data_system.rb +704 -0
  52. data/lib/ldclient-rb/interfaces/feature_store.rb +5 -2
  53. data/lib/ldclient-rb/ldclient.rb +66 -132
  54. data/lib/ldclient-rb/util.rb +11 -70
  55. data/lib/ldclient-rb/version.rb +1 -1
  56. data/lib/ldclient-rb.rb +9 -17
  57. metadata +41 -19
  58. data/lib/ldclient-rb/cache_store.rb +0 -45
  59. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  60. data/lib/ldclient-rb/memoized_value.rb +0 -32
  61. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  62. data/lib/ldclient-rb/polling.rb +0 -102
  63. data/lib/ldclient-rb/requestor.rb +0 -102
  64. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  65. data/lib/ldclient-rb/stream.rb +0 -197
@@ -0,0 +1,628 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ldclient-rb/interfaces"
4
+ require "ldclient-rb/interfaces/data_system"
5
+ require "ldclient-rb/impl/data_system"
6
+ require "ldclient-rb/impl/data_system/protocolv2"
7
+ require "ldclient-rb/impl/data_system/data_source_builder_common"
8
+ require "ldclient-rb/impl/data_source/requestor"
9
+ require "ldclient-rb/impl/util"
10
+ require "concurrent"
11
+ require "json"
12
+ require "uri"
13
+ require "http"
14
+
15
+ module LaunchDarkly
16
+ module Impl
17
+ module DataSystem
18
+ FDV2_POLLING_ENDPOINT = "/sdk/poll"
19
+ FDV1_POLLING_ENDPOINT = "/sdk/latest-all"
20
+
21
+ LD_ENVID_HEADER = "X-LD-EnvID"
22
+ LD_FD_FALLBACK_HEADER = "X-LD-FD-Fallback"
23
+
24
+ #
25
+ # Requester protocol for polling data source
26
+ #
27
+ module Requester
28
+ #
29
+ # Fetches the data for the given selector.
30
+ # Returns a Result containing a tuple of [ChangeSet, headers],
31
+ # or an error if the data could not be retrieved.
32
+ #
33
+ # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
34
+ # @return [Result]
35
+ #
36
+ def fetch(selector)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ #
41
+ # Closes any persistent connections and releases resources.
42
+ # This method should be called when the requester is no longer needed.
43
+ # Implementations should handle being called multiple times gracefully.
44
+ #
45
+ def stop
46
+ # Optional - implementations may override if they need cleanup
47
+ end
48
+ end
49
+
50
+ #
51
+ # PollingDataSource is a data source that can retrieve information from
52
+ # LaunchDarkly either as an Initializer or as a Synchronizer.
53
+ #
54
+ class PollingDataSource
55
+ include LaunchDarkly::Interfaces::DataSystem::Initializer
56
+ include LaunchDarkly::Interfaces::DataSystem::Synchronizer
57
+
58
+ attr_reader :name
59
+
60
+ #
61
+ # @param poll_interval [Float] Polling interval in seconds
62
+ # @param requester [Requester] The requester to use for fetching data
63
+ # @param logger [Logger] The logger
64
+ #
65
+ def initialize(poll_interval, requester, logger)
66
+ @requester = requester
67
+ @poll_interval = poll_interval
68
+ @logger = logger
69
+ @interrupt_event = Concurrent::Event.new
70
+ @stop = Concurrent::Event.new
71
+ @name = "PollingDataSourceV2"
72
+ end
73
+
74
+ #
75
+ # Fetch returns a Basis, or an error if the Basis could not be retrieved.
76
+ #
77
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
78
+ # @return [LaunchDarkly::Interfaces::DataSystem::Basis, nil]
79
+ #
80
+ def fetch(ss)
81
+ poll(ss)
82
+ ensure
83
+ # Ensure the requester is stopped to avoid leaving open connections.
84
+ @requester.stop if @requester.respond_to?(:stop)
85
+ end
86
+
87
+ #
88
+ # sync begins the synchronization process for the data source, yielding
89
+ # Update objects until the connection is closed or an unrecoverable error
90
+ # occurs.
91
+ #
92
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
93
+ # @yieldparam update [LaunchDarkly::Interfaces::DataSystem::Update]
94
+ #
95
+ def sync(ss)
96
+ @logger.info { "[LDClient] Starting PollingDataSourceV2 synchronizer" }
97
+
98
+ until @stop.set?
99
+ result = @requester.fetch(ss.selector)
100
+
101
+ if !result.success?
102
+ fallback = false
103
+ envid = nil
104
+
105
+ if result.headers
106
+ fallback = result.headers[LD_FD_FALLBACK_HEADER] == 'true'
107
+ envid = result.headers[LD_ENVID_HEADER]
108
+ end
109
+
110
+ if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
111
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
112
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE,
113
+ result.exception.status,
114
+ Impl::Util.http_error_message(
115
+ result.exception.status, "polling request", "will retry"
116
+ ),
117
+ Time.now
118
+ )
119
+
120
+ status_code = result.exception.status
121
+ if Impl::Util.http_error_recoverable?(status_code)
122
+ # If fallback is requested, send OFF status to signal shutdown
123
+ if fallback
124
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
125
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
126
+ error: error_info,
127
+ environment_id: envid,
128
+ revert_to_fdv1: true
129
+ )
130
+ break
131
+ end
132
+
133
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
134
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
135
+ error: error_info,
136
+ environment_id: envid,
137
+ revert_to_fdv1: false
138
+ )
139
+ @interrupt_event.wait(@poll_interval)
140
+ next
141
+ end
142
+
143
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
144
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
145
+ error: error_info,
146
+ environment_id: envid,
147
+ revert_to_fdv1: fallback
148
+ )
149
+ break
150
+ end
151
+
152
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
153
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR,
154
+ 0,
155
+ result.error,
156
+ Time.now
157
+ )
158
+
159
+ # If fallback is requested, send OFF status to signal shutdown
160
+ if fallback
161
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
162
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
163
+ error: error_info,
164
+ environment_id: envid,
165
+ revert_to_fdv1: true
166
+ )
167
+ break
168
+ end
169
+
170
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
171
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
172
+ error: error_info,
173
+ environment_id: envid,
174
+ revert_to_fdv1: false
175
+ )
176
+ else
177
+ change_set, headers = result.value
178
+ fallback = headers[LD_FD_FALLBACK_HEADER] == 'true'
179
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
180
+ state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
181
+ change_set: change_set,
182
+ environment_id: headers[LD_ENVID_HEADER],
183
+ revert_to_fdv1: fallback
184
+ )
185
+ end
186
+
187
+ break if fallback
188
+ break if @interrupt_event.wait(@poll_interval)
189
+ end
190
+ ensure
191
+ # Ensure the requester is stopped to avoid leaving open connections.
192
+ @requester.stop if @requester.respond_to?(:stop)
193
+ end
194
+
195
+ #
196
+ # Stops the synchronizer.
197
+ #
198
+ def stop
199
+ @logger.info { "[LDClient] Stopping PollingDataSourceV2 synchronizer" }
200
+ @interrupt_event.set
201
+ @stop.set
202
+ end
203
+
204
+ #
205
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
206
+ # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::Basis, String>]
207
+ #
208
+ private def poll(ss)
209
+ result = @requester.fetch(ss.selector)
210
+
211
+ unless result.success?
212
+ if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
213
+ status_code = result.exception.status
214
+ http_error_message_result = Impl::Util.http_error_message(
215
+ status_code, "polling request", "will retry"
216
+ )
217
+ @logger.warn { "[LDClient] #{http_error_message_result}" } if Impl::Util.http_error_recoverable?(status_code)
218
+ return LaunchDarkly::Result.fail(http_error_message_result, result.exception)
219
+ end
220
+
221
+ return LaunchDarkly::Result.fail(result.error || 'Failed to request payload', result.exception)
222
+ end
223
+
224
+ change_set, headers = result.value
225
+
226
+ env_id = headers[LD_ENVID_HEADER]
227
+ env_id = nil unless env_id.is_a?(String)
228
+
229
+ basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
230
+ change_set: change_set,
231
+ persist: change_set.selector.defined?,
232
+ environment_id: env_id
233
+ )
234
+
235
+ LaunchDarkly::Result.success(basis)
236
+ rescue => e
237
+ msg = "Error: Exception encountered when updating flags. #{e}"
238
+ @logger.error { "[LDClient] #{msg}" }
239
+ @logger.debug { "[LDClient] Exception trace: #{e.backtrace}" }
240
+ LaunchDarkly::Result.fail(msg, e)
241
+ end
242
+ end
243
+
244
+ #
245
+ # HTTPPollingRequester is a Requester that uses HTTP to make
246
+ # requests to the FDv2 polling endpoint.
247
+ #
248
+ class HTTPPollingRequester
249
+ include Requester
250
+
251
+ #
252
+ # @param sdk_key [String]
253
+ # @param http_config [HttpConfigOptions] HTTP connection settings
254
+ # @param config [LaunchDarkly::Config] Used for global header settings
255
+ #
256
+ def initialize(sdk_key, http_config, config)
257
+ @etag = nil
258
+ @config = config
259
+ @sdk_key = sdk_key
260
+ @poll_uri = http_config.base_uri + FDV2_POLLING_ENDPOINT
261
+ @http_client = Impl::Util.new_http_client(http_config)
262
+ .use(:auto_inflate)
263
+ .headers("Accept-Encoding" => "gzip")
264
+ end
265
+
266
+ #
267
+ # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
268
+ # @return [Result]
269
+ #
270
+ def fetch(selector)
271
+ query_params = []
272
+ query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
273
+
274
+ if selector && selector.defined?
275
+ query_params << ["selector", selector.state]
276
+ end
277
+
278
+ uri = @poll_uri
279
+ if query_params.any?
280
+ filter_query = URI.encode_www_form(query_params)
281
+ uri = "#{uri}?#{filter_query}"
282
+ end
283
+
284
+ headers = {}
285
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
286
+ headers["If-None-Match"] = @etag unless @etag.nil?
287
+
288
+ begin
289
+ response = @http_client.request("GET", uri, headers: headers)
290
+ status = response.status.code
291
+ response_headers = response.headers.to_h.transform_keys(&:downcase)
292
+
293
+ if status >= 400
294
+ return LaunchDarkly::Result.fail(
295
+ "HTTP error #{status}",
296
+ LaunchDarkly::Impl::DataSource::UnexpectedResponseError.new(status),
297
+ response_headers
298
+ )
299
+ end
300
+
301
+ if status == 304
302
+ return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
303
+ end
304
+
305
+ body = response.to_s
306
+ data = JSON.parse(body, symbolize_names: true)
307
+ etag = response_headers["etag"]
308
+ @etag = etag unless etag.nil?
309
+
310
+ @config.logger.debug { "[LDClient] #{uri} response status:[#{status}] ETag:[#{etag}]" }
311
+
312
+ changeset_result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(data)
313
+ if changeset_result.success?
314
+ LaunchDarkly::Result.success([changeset_result.value, response_headers])
315
+ else
316
+ LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
317
+ end
318
+ rescue JSON::ParserError => e
319
+ LaunchDarkly::Result.fail("Failed to parse JSON: #{e.message}", e, response_headers)
320
+ rescue => e
321
+ LaunchDarkly::Result.fail("Network error: #{e.message}", e)
322
+ end
323
+ end
324
+
325
+ #
326
+ # Closes the HTTP client and releases any persistent connections.
327
+ #
328
+ def stop
329
+ begin
330
+ @http_client.close if @http_client
331
+ rescue
332
+ end
333
+ end
334
+ end
335
+
336
+ #
337
+ # HTTPFDv1PollingRequester is a Requester that uses HTTP to make
338
+ # requests to the FDv1 polling endpoint.
339
+ #
340
+ class HTTPFDv1PollingRequester
341
+ include Requester
342
+
343
+ #
344
+ # @param sdk_key [String]
345
+ # @param http_config [HttpConfigOptions] HTTP connection settings
346
+ # @param config [LaunchDarkly::Config] Used for global header settings and payload_filter_key
347
+ #
348
+ def initialize(sdk_key, http_config, config)
349
+ @etag = nil
350
+ @config = config
351
+ @sdk_key = sdk_key
352
+ @poll_uri = http_config.base_uri + FDV1_POLLING_ENDPOINT
353
+ @http_client = Impl::Util.new_http_client(http_config)
354
+ .use(:auto_inflate)
355
+ .headers("Accept-Encoding" => "gzip")
356
+ end
357
+
358
+ #
359
+ # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
360
+ # @return [Result]
361
+ #
362
+ def fetch(selector)
363
+ query_params = []
364
+ query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
365
+
366
+ uri = @poll_uri
367
+ if query_params.any?
368
+ filter_query = URI.encode_www_form(query_params)
369
+ uri = "#{uri}?#{filter_query}"
370
+ end
371
+
372
+ headers = {}
373
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
374
+ headers["If-None-Match"] = @etag unless @etag.nil?
375
+
376
+ begin
377
+ response = @http_client.request("GET", uri, headers: headers)
378
+ status = response.status.code
379
+ response_headers = response.headers.to_h.transform_keys(&:downcase)
380
+
381
+ if status >= 400
382
+ return LaunchDarkly::Result.fail(
383
+ "HTTP error #{status}",
384
+ LaunchDarkly::Impl::DataSource::UnexpectedResponseError.new(status),
385
+ response_headers
386
+ )
387
+ end
388
+
389
+ if status == 304
390
+ return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
391
+ end
392
+
393
+ body = response.to_s
394
+ data = JSON.parse(body, symbolize_names: true)
395
+ etag = response_headers["etag"]
396
+ @etag = etag unless etag.nil?
397
+
398
+ @config.logger.debug { "[LDClient] #{uri} response status:[#{status}] ETag:[#{etag}]" }
399
+
400
+ changeset_result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data)
401
+ if changeset_result.success?
402
+ LaunchDarkly::Result.success([changeset_result.value, response_headers])
403
+ else
404
+ LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
405
+ end
406
+ rescue JSON::ParserError => e
407
+ LaunchDarkly::Result.fail("Failed to parse JSON: #{e.message}", e, response_headers)
408
+ rescue => e
409
+ LaunchDarkly::Result.fail("Network error: #{e.message}", e)
410
+ end
411
+ end
412
+
413
+ #
414
+ # Closes the HTTP client and releases any persistent connections.
415
+ #
416
+ def stop
417
+ begin
418
+ @http_client.close if @http_client
419
+ rescue
420
+ end
421
+ end
422
+ end
423
+
424
+ #
425
+ # Converts a polling payload into a ChangeSet.
426
+ #
427
+ # @param data [Hash] The polling payload
428
+ # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::ChangeSet, String>] Result containing ChangeSet on success, or error message on failure
429
+ #
430
+ def self.polling_payload_to_changeset(data)
431
+ unless data[:events].is_a?(Array)
432
+ return LaunchDarkly::Result.fail("Invalid payload: 'events' key is missing or not a list")
433
+ end
434
+
435
+ builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
436
+
437
+ data[:events].each do |event|
438
+ unless event.is_a?(Hash)
439
+ return LaunchDarkly::Result.fail("Invalid payload: 'events' must be a list of objects")
440
+ end
441
+
442
+ next unless event[:event]
443
+
444
+ case event[:event].to_sym
445
+ when LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT
446
+ begin
447
+ server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.from_h(event[:data])
448
+ rescue ArgumentError => e
449
+ return LaunchDarkly::Result.fail("Invalid JSON in server intent", e)
450
+ end
451
+
452
+ if server_intent.payload.code == LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_NONE
453
+ return LaunchDarkly::Result.success(LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes)
454
+ end
455
+
456
+ builder.start(server_intent.payload.code)
457
+
458
+ when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
459
+ begin
460
+ put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(event[:data])
461
+ rescue ArgumentError => e
462
+ return LaunchDarkly::Result.fail("Invalid JSON in put object", e)
463
+ end
464
+
465
+ builder.add_put(put.kind, put.key, put.version, put.object)
466
+
467
+ when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
468
+ begin
469
+ delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(event[:data])
470
+ rescue ArgumentError => e
471
+ return LaunchDarkly::Result.fail("Invalid JSON in delete object", e)
472
+ end
473
+
474
+ builder.add_delete(delete_object.kind, delete_object.key, delete_object.version)
475
+
476
+ when LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED
477
+ begin
478
+ selector = LaunchDarkly::Interfaces::DataSystem::Selector.from_h(event[:data])
479
+ changeset = builder.finish(selector)
480
+ return LaunchDarkly::Result.success(changeset)
481
+ rescue ArgumentError, RuntimeError => e
482
+ return LaunchDarkly::Result.fail("Invalid JSON in payload transferred object", e)
483
+ end
484
+ end
485
+ end
486
+
487
+ LaunchDarkly::Result.fail("didn't receive any known protocol events in polling payload")
488
+ end
489
+
490
+ #
491
+ # Converts an FDv1 polling payload into a ChangeSet.
492
+ #
493
+ # @param data [Hash] The FDv1 polling payload
494
+ # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::ChangeSet, String>] Result containing ChangeSet on success, or error message on failure
495
+ #
496
+ def self.fdv1_polling_payload_to_changeset(data)
497
+ builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
498
+ builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
499
+ selector = LaunchDarkly::Interfaces::DataSystem::Selector.no_selector
500
+
501
+ kind_mappings = [
502
+ [LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, :flags],
503
+ [LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, :segments],
504
+ ]
505
+
506
+ kind_mappings.each do |kind, fdv1_key|
507
+ kind_data = data[fdv1_key]
508
+ next if kind_data.nil?
509
+
510
+ unless kind_data.is_a?(Hash)
511
+ return LaunchDarkly::Result.fail("Invalid format: #{fdv1_key} is not an object")
512
+ end
513
+
514
+ kind_data.each do |key, flag_or_segment|
515
+ unless flag_or_segment.is_a?(Hash)
516
+ return LaunchDarkly::Result.fail("Invalid format: #{key} is not an object")
517
+ end
518
+
519
+ version = flag_or_segment[:version]
520
+ return LaunchDarkly::Result.fail("Invalid format: #{key} does not have a version set") if version.nil?
521
+
522
+ builder.add_put(kind, key, version, flag_or_segment)
523
+ end
524
+ end
525
+
526
+ LaunchDarkly::Result.success(builder.finish(selector))
527
+ end
528
+
529
+ #
530
+ # Builder for a PollingDataSource.
531
+ #
532
+ class PollingDataSourceBuilder
533
+ include DataSourceBuilderCommon
534
+
535
+ DEFAULT_BASE_URI = "https://sdk.launchdarkly.com"
536
+ DEFAULT_POLL_INTERVAL = 30
537
+
538
+ def initialize
539
+ @requester = nil
540
+ end
541
+
542
+ #
543
+ # Sets the polling interval in seconds.
544
+ #
545
+ # @param secs [Float] Polling interval in seconds
546
+ # @return [PollingDataSourceBuilder]
547
+ #
548
+ def poll_interval(secs)
549
+ @poll_interval = secs
550
+ self
551
+ end
552
+
553
+ #
554
+ # Sets a custom Requester for the PollingDataSource.
555
+ #
556
+ # @param requester [Requester]
557
+ # @return [PollingDataSourceBuilder]
558
+ #
559
+ def requester(requester)
560
+ @requester = requester
561
+ self
562
+ end
563
+
564
+ #
565
+ # Builds the PollingDataSource with the configured parameters.
566
+ #
567
+ # @param sdk_key [String]
568
+ # @param config [LaunchDarkly::Config]
569
+ # @return [PollingDataSource]
570
+ #
571
+ def build(sdk_key, config)
572
+ http_opts = build_http_config
573
+ requester = @requester || HTTPPollingRequester.new(sdk_key, http_opts, config)
574
+ PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger)
575
+ end
576
+ end
577
+
578
+ #
579
+ # Builder for an FDv1 PollingDataSource.
580
+ #
581
+ class FDv1PollingDataSourceBuilder
582
+ include DataSourceBuilderCommon
583
+
584
+ DEFAULT_BASE_URI = "https://sdk.launchdarkly.com"
585
+ DEFAULT_POLL_INTERVAL = 30
586
+
587
+ def initialize
588
+ @requester = nil
589
+ end
590
+
591
+ #
592
+ # Sets the polling interval in seconds.
593
+ #
594
+ # @param secs [Float] Polling interval in seconds
595
+ # @return [FDv1PollingDataSourceBuilder]
596
+ #
597
+ def poll_interval(secs)
598
+ @poll_interval = secs
599
+ self
600
+ end
601
+
602
+ #
603
+ # Sets a custom Requester for the PollingDataSource.
604
+ #
605
+ # @param requester [Requester]
606
+ # @return [FDv1PollingDataSourceBuilder]
607
+ #
608
+ def requester(requester)
609
+ @requester = requester
610
+ self
611
+ end
612
+
613
+ #
614
+ # Builds the PollingDataSource with the configured parameters.
615
+ #
616
+ # @param sdk_key [String]
617
+ # @param config [LaunchDarkly::Config]
618
+ # @return [PollingDataSource]
619
+ #
620
+ def build(sdk_key, config)
621
+ http_opts = build_http_config
622
+ requester = @requester || HTTPFDv1PollingRequester.new(sdk_key, http_opts, config)
623
+ PollingDataSource.new(@poll_interval || DEFAULT_POLL_INTERVAL, requester, config.logger)
624
+ end
625
+ end
626
+ end
627
+ end
628
+ end