togglecraft 1.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.
@@ -0,0 +1,728 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe ToggleCraft::Client do
4
+ let(:sdk_key) { 'test-sdk-key-12345' }
5
+ let(:client) { described_class.new(sdk_key: sdk_key, debug: false) }
6
+
7
+ after do
8
+ client.destroy if client
9
+ ToggleCraft::ConnectionPool.clear(force: true)
10
+ end
11
+
12
+ describe '#initialize' do
13
+ it 'requires sdk_key parameter' do
14
+ expect { described_class.new }.to raise_error(ArgumentError, /sdk_key/)
15
+ end
16
+
17
+ it 'raises error when sdk_key is nil' do
18
+ expect { described_class.new(sdk_key: nil) }.to raise_error(ArgumentError, 'sdk_key is required')
19
+ end
20
+
21
+ it 'raises error when sdk_key is empty string' do
22
+ expect { described_class.new(sdk_key: '') }.to raise_error(ArgumentError, 'sdk_key is required')
23
+ end
24
+
25
+ it 'sets default configuration options' do
26
+ expect(client.config[:url]).to eq('https://sse.togglecraft.io')
27
+ expect(client.config[:enable_cache]).to be true
28
+ expect(client.config[:cache_adapter]).to eq(:memory)
29
+ expect(client.config[:cache_ttl]).to eq(300)
30
+ expect(client.config[:reconnect_interval]).to eq(1)
31
+ expect(client.config[:max_reconnect_interval]).to eq(30)
32
+ expect(client.config[:max_reconnect_attempts]).to eq(10)
33
+ expect(client.config[:slow_reconnect_interval]).to eq(60)
34
+ expect(client.config[:share_connection]).to be true
35
+ expect(client.config[:debug]).to be false
36
+ expect(client.config[:enable_rollout_stage_polling]).to be true
37
+ expect(client.config[:rollout_stage_check_interval]).to eq(60)
38
+ expect(client.config[:fetch_jitter]).to eq(1500)
39
+ end
40
+
41
+ it 'allows custom configuration options' do
42
+ custom_client = described_class.new(
43
+ sdk_key: 'custom-key',
44
+ url: 'https://custom.example.com',
45
+ enable_cache: false,
46
+ cache_ttl: 600,
47
+ reconnect_interval: 5,
48
+ share_connection: false,
49
+ debug: true
50
+ )
51
+
52
+ expect(custom_client.config[:url]).to eq('https://custom.example.com')
53
+ expect(custom_client.config[:enable_cache]).to be false
54
+ expect(custom_client.config[:cache_ttl]).to eq(600)
55
+ expect(custom_client.config[:reconnect_interval]).to eq(5)
56
+ expect(custom_client.config[:share_connection]).to be false
57
+ expect(custom_client.config[:debug]).to be true
58
+
59
+ custom_client.destroy
60
+ end
61
+
62
+ it 'initializes ready state to false' do
63
+ expect(client.ready?).to be false
64
+ end
65
+
66
+ it 'initializes evaluator' do
67
+ evaluator = client.instance_variable_get(:@evaluator)
68
+ expect(evaluator).to be_a(ToggleCraft::Evaluator)
69
+ end
70
+
71
+ it 'initializes cache when enabled' do
72
+ cache = client.instance_variable_get(:@cache)
73
+ expect(cache).to be_a(ToggleCraft::Cache)
74
+ end
75
+
76
+ it 'does not initialize cache when disabled' do
77
+ no_cache_client = described_class.new(sdk_key: 'key', enable_cache: false)
78
+ cache = no_cache_client.instance_variable_get(:@cache)
79
+ expect(cache).to be_nil
80
+ no_cache_client.destroy
81
+ end
82
+
83
+ it 'initializes event listeners map' do
84
+ listeners = client.instance_variable_get(:@listeners)
85
+ expect(listeners.keys).to include(:ready, :flags_updated, :error, :disconnected, :reconnecting, :rollout_stage_changed)
86
+ end
87
+
88
+ it 'initializes connection (shared by default)' do
89
+ connection = client.instance_variable_get(:@connection)
90
+ expect(connection).to be_a(ToggleCraft::SharedSSEConnection)
91
+ end
92
+
93
+ it 'creates dedicated connection when share_connection is false' do
94
+ dedicated_client = described_class.new(sdk_key: 'key', share_connection: false)
95
+ connection = dedicated_client.instance_variable_get(:@connection)
96
+ expect(connection).to be_a(ToggleCraft::SSEConnection)
97
+ dedicated_client.destroy
98
+ end
99
+ end
100
+
101
+ describe '#ready?' do
102
+ it 'returns false initially' do
103
+ expect(client.ready?).to be false
104
+ end
105
+
106
+ it 'returns true after ready state is set' do
107
+ client.instance_variable_get(:@ready).make_true
108
+ expect(client.ready?).to be true
109
+ end
110
+
111
+ it 'returns false after disconnect' do
112
+ client.instance_variable_get(:@ready).make_true
113
+ client.handle_disconnect
114
+ expect(client.ready?).to be false
115
+ end
116
+ end
117
+
118
+ describe '#connected?' do
119
+ it 'returns false when connection is nil' do
120
+ client.instance_variable_set(:@connection, nil)
121
+ expect(client.connected?).to be false
122
+ end
123
+
124
+ it 'returns connection status when connection exists' do
125
+ connection = client.instance_variable_get(:@connection)
126
+ allow(connection).to receive(:connected?).and_return(true)
127
+ expect(client.connected?).to be true
128
+
129
+ allow(connection).to receive(:connected?).and_return(false)
130
+ expect(client.connected?).to be false
131
+ end
132
+ end
133
+
134
+ describe '#wait_for_ready' do
135
+ it 'returns immediately if already ready' do
136
+ client.instance_variable_get(:@ready).make_true
137
+ expect { client.wait_for_ready(timeout: 1) }.not_to raise_error
138
+ end
139
+
140
+ it 'waits for ready event' do
141
+ Thread.new do
142
+ sleep 0.1
143
+ client.instance_variable_get(:@ready).make_true
144
+ client.send(:emit, :ready)
145
+ end
146
+
147
+ expect { client.wait_for_ready(timeout: 2) }.not_to raise_error
148
+ end
149
+
150
+ it 'raises timeout error if not ready within timeout' do
151
+ expect do
152
+ client.wait_for_ready(timeout: 0.1)
153
+ end.to raise_error(Timeout::Error, /Timeout waiting for client to be ready/)
154
+ end
155
+
156
+ it 'attempts to clean up handler after timeout' do
157
+ # Test that timeout occurs without causing issues
158
+ expect do
159
+ client.wait_for_ready(timeout: 0.1)
160
+ end.to raise_error(Timeout::Error)
161
+
162
+ # Client should still be functional after timeout
163
+ expect(client.ready?).to be false
164
+ end
165
+ end
166
+
167
+ describe 'flag evaluation methods' do
168
+ before do
169
+ flags = {
170
+ 'boolean-flag' => {
171
+ type: 'boolean',
172
+ enabled: true,
173
+ value: true
174
+ },
175
+ 'multivariate-flag' => {
176
+ type: 'multivariate',
177
+ enabled: true,
178
+ default_variant: 'control',
179
+ variants: %w[control variant-a],
180
+ weights: { 'control' => 50, 'variant-a' => 50 }
181
+ },
182
+ 'percentage-flag' => {
183
+ type: 'percentage',
184
+ enabled: true,
185
+ percentage: 50
186
+ }
187
+ }
188
+ evaluator = client.instance_variable_get(:@evaluator)
189
+ evaluator.update_flags(flags)
190
+ end
191
+
192
+ describe '#enabled?' do
193
+ it 'evaluates boolean flags' do
194
+ result = client.enabled?('boolean-flag')
195
+ expect(result).to be true
196
+ end
197
+
198
+ it 'returns default value for missing flags' do
199
+ expect(client.enabled?('missing', {}, default: false)).to be false
200
+ expect(client.enabled?('missing', {}, default: true)).to be true
201
+ end
202
+
203
+ it 'accepts context for evaluation' do
204
+ context = { user: { id: '123', role: 'admin' } }
205
+ result = client.enabled?('boolean-flag', context)
206
+ expect([true, false]).to include(result)
207
+ end
208
+
209
+ it 'caches evaluation results when cache is enabled' do
210
+ context = { user: { id: '123' } }
211
+ cache = client.instance_variable_get(:@cache)
212
+
213
+ client.enabled?('boolean-flag', context)
214
+
215
+ # Check cache was called
216
+ expect(cache.instance_variable_get(:@adapter).keys.any? { |k| k.start_with?('eval:') }).to be true
217
+ end
218
+
219
+ it 'does not cache when cache is disabled' do
220
+ no_cache_client = described_class.new(sdk_key: 'key', enable_cache: false)
221
+ evaluator = no_cache_client.instance_variable_get(:@evaluator)
222
+ evaluator.update_flags({ 'test' => { type: 'boolean', enabled: true, value: true } })
223
+
224
+ result = no_cache_client.enabled?('test', { user: { id: '123' } })
225
+ expect(result).to be true
226
+
227
+ no_cache_client.destroy
228
+ end
229
+ end
230
+
231
+ describe '#variant' do
232
+ it 'evaluates multivariate flags' do
233
+ context = { user: { id: '123' } }
234
+ result = client.variant('multivariate-flag', context)
235
+ expect(%w[control variant-a]).to include(result)
236
+ end
237
+
238
+ it 'returns default value for missing flags' do
239
+ expect(client.variant('missing', {}, default: 'default')).to eq('default')
240
+ expect(client.variant('missing', {}, default: nil)).to be_nil
241
+ end
242
+
243
+ it 'returns consistent variant for same user' do
244
+ context = { user: { id: 'user-123' } }
245
+ result1 = client.variant('multivariate-flag', context)
246
+ result2 = client.variant('multivariate-flag', context)
247
+ expect(result1).to eq(result2)
248
+ end
249
+
250
+ it 'caches evaluation results' do
251
+ context = { user: { id: '456' } }
252
+ cache = client.instance_variable_get(:@cache)
253
+
254
+ client.variant('multivariate-flag', context)
255
+
256
+ expect(cache.instance_variable_get(:@adapter).keys.any? { |k| k.start_with?('eval:') }).to be true
257
+ end
258
+ end
259
+
260
+ describe '#in_percentage?' do
261
+ it 'evaluates percentage flags' do
262
+ context = { user: { id: '123' } }
263
+ result = client.in_percentage?('percentage-flag', context)
264
+ expect([true, false]).to include(result)
265
+ end
266
+
267
+ it 'returns default value for missing flags' do
268
+ expect(client.in_percentage?('missing', {}, default: false)).to be false
269
+ expect(client.in_percentage?('missing', {}, default: true)).to be true
270
+ end
271
+
272
+ it 'returns consistent result for same user' do
273
+ context = { user: { id: 'consistent-user' } }
274
+ result1 = client.in_percentage?('percentage-flag', context)
275
+ result2 = client.in_percentage?('percentage-flag', context)
276
+ expect(result1).to eq(result2)
277
+ end
278
+ end
279
+
280
+ describe '#percentage' do
281
+ it 'returns current percentage for percentage flags' do
282
+ result = client.percentage('percentage-flag')
283
+ expect(result).to eq(50)
284
+ end
285
+
286
+ it 'returns nil for non-existent flags' do
287
+ expect(client.percentage('missing')).to be_nil
288
+ end
289
+
290
+ it 'returns nil for non-percentage flags' do
291
+ expect(client.percentage('boolean-flag')).to be_nil
292
+ end
293
+ end
294
+
295
+ describe '#evaluate' do
296
+ it 'evaluates any flag type' do
297
+ expect(client.evaluate('boolean-flag')).to be true
298
+ expect(%w[control variant-a]).to include(client.evaluate('multivariate-flag', { user: { id: '123' } }))
299
+ result = client.evaluate('percentage-flag', { user: { id: '123' } })
300
+ expect([true, false]).to include(result)
301
+ end
302
+
303
+ it 'returns default for missing flags' do
304
+ expect(client.evaluate('missing', {}, default: 'fallback')).to eq('fallback')
305
+ end
306
+ end
307
+
308
+ describe '#all_flag_keys' do
309
+ it 'returns all flag keys' do
310
+ keys = client.all_flag_keys
311
+ expect(keys).to include('boolean-flag', 'multivariate-flag', 'percentage-flag')
312
+ expect(keys.length).to eq(3)
313
+ end
314
+
315
+ it 'returns empty array when no flags' do
316
+ empty_client = described_class.new(sdk_key: 'key')
317
+ expect(empty_client.all_flag_keys).to eq([])
318
+ empty_client.destroy
319
+ end
320
+ end
321
+
322
+ describe '#flag_metadata' do
323
+ it 'returns metadata for existing flags' do
324
+ metadata = client.flag_metadata('boolean-flag')
325
+ expect(metadata[:type]).to eq('boolean')
326
+ expect(metadata[:enabled]).to be true
327
+ end
328
+
329
+ it 'returns nil for non-existent flags' do
330
+ expect(client.flag_metadata('missing')).to be_nil
331
+ end
332
+
333
+ it 'includes type-specific metadata' do
334
+ mv_metadata = client.flag_metadata('multivariate-flag')
335
+ expect(mv_metadata[:variants]).to eq(%w[control variant-a])
336
+ expect(mv_metadata[:default_variant]).to eq('control')
337
+
338
+ pct_metadata = client.flag_metadata('percentage-flag')
339
+ expect(pct_metadata[:percentage]).to eq(50)
340
+ end
341
+ end
342
+
343
+ describe '#has_flag?' do
344
+ it 'returns true for existing flags' do
345
+ expect(client.has_flag?('boolean-flag')).to be true
346
+ end
347
+
348
+ it 'returns false for non-existent flags' do
349
+ expect(client.has_flag?('missing')).to be false
350
+ end
351
+ end
352
+ end
353
+
354
+ describe 'event system' do
355
+ describe '#on' do
356
+ it 'registers event listener' do
357
+ called = false
358
+ client.on(:ready) { called = true }
359
+
360
+ client.send(:emit, :ready)
361
+ expect(called).to be true
362
+ end
363
+
364
+ it 'allows multiple listeners for same event' do
365
+ call_count = 0
366
+ client.on(:ready) { call_count += 1 }
367
+ client.on(:ready) { call_count += 1 }
368
+
369
+ client.send(:emit, :ready)
370
+ expect(call_count).to eq(2)
371
+ end
372
+
373
+ it 'passes arguments to listeners' do
374
+ received_data = nil
375
+ client.on(:flags_updated) { |data| received_data = data }
376
+
377
+ test_data = { 'flag' => 'value' }
378
+ client.send(:emit, :flags_updated, test_data)
379
+ expect(received_data).to eq(test_data)
380
+ end
381
+
382
+ it 'supports all event types' do
383
+ events_received = []
384
+
385
+ client.on(:ready) { events_received << :ready }
386
+ client.on(:flags_updated) { events_received << :flags_updated }
387
+ client.on(:error) { events_received << :error }
388
+ client.on(:disconnected) { events_received << :disconnected }
389
+ client.on(:reconnecting) { events_received << :reconnecting }
390
+ client.on(:rollout_stage_changed) { events_received << :rollout_stage_changed }
391
+
392
+ client.send(:emit, :ready)
393
+ client.send(:emit, :flags_updated, {})
394
+ client.send(:emit, :error, StandardError.new('test'))
395
+ client.send(:emit, :disconnected)
396
+ client.send(:emit, :reconnecting)
397
+ client.send(:emit, :rollout_stage_changed, {})
398
+
399
+ expect(events_received).to eq([:ready, :flags_updated, :error, :disconnected, :reconnecting, :rollout_stage_changed])
400
+ end
401
+
402
+ it 'handles errors in event listeners gracefully' do
403
+ client.on(:ready) { raise 'Listener error' }
404
+ client.on(:ready) { } # This should still be called
405
+
406
+ expect { client.send(:emit, :ready) }.not_to raise_error
407
+ end
408
+ end
409
+
410
+ describe '#once' do
411
+ it 'registers one-time event listener' do
412
+ call_count = 0
413
+ client.once(:ready) { call_count += 1 }
414
+
415
+ client.send(:emit, :ready)
416
+ client.send(:emit, :ready)
417
+
418
+ expect(call_count).to eq(1)
419
+ end
420
+
421
+ it 'receives event data' do
422
+ received = nil
423
+ client.once(:flags_updated) { |data| received = data }
424
+
425
+ client.send(:emit, :flags_updated, { test: 'data' })
426
+ expect(received).to eq({ test: 'data' })
427
+ end
428
+
429
+ it 'removes itself after execution' do
430
+ listeners_before = client.instance_variable_get(:@listeners)[:ready].length
431
+
432
+ client.once(:ready) { }
433
+ listeners_during = client.instance_variable_get(:@listeners)[:ready].length
434
+ expect(listeners_during).to eq(listeners_before + 1)
435
+
436
+ client.send(:emit, :ready)
437
+ listeners_after = client.instance_variable_get(:@listeners)[:ready].length
438
+ expect(listeners_after).to eq(listeners_before)
439
+ end
440
+ end
441
+
442
+ describe '#off' do
443
+ it 'removes specific listener' do
444
+ handler = -> { }
445
+ client.on(:ready, &handler)
446
+
447
+ client.off(:ready, handler)
448
+
449
+ listeners = client.instance_variable_get(:@listeners)[:ready]
450
+ expect(listeners).not_to include(handler)
451
+ end
452
+
453
+ it 'removes all listeners for event when no handler specified' do
454
+ client.on(:ready) { }
455
+ client.on(:ready) { }
456
+
457
+ client.off(:ready)
458
+
459
+ listeners = client.instance_variable_get(:@listeners)[:ready]
460
+ expect(listeners).to be_empty
461
+ end
462
+
463
+ it 'does not affect other events' do
464
+ client.on(:ready) { }
465
+ client.on(:error) { }
466
+
467
+ client.off(:ready)
468
+
469
+ expect(client.instance_variable_get(:@listeners)[:ready]).to be_empty
470
+ expect(client.instance_variable_get(:@listeners)[:error]).not_to be_empty
471
+ end
472
+ end
473
+ end
474
+
475
+ describe '#handle_flags_update' do
476
+ it 'processes full flags payload' do
477
+ payload = {
478
+ flags: {
479
+ 'test-flag' => { type: 'boolean', enabled: true, value: true }
480
+ },
481
+ version: 1
482
+ }
483
+
484
+ client.handle_flags_update(payload)
485
+
486
+ expect(client.has_flag?('test-flag')).to be true
487
+ expect(client.instance_variable_get(:@current_version).get).to eq(1)
488
+ end
489
+
490
+ it 'handles version_update type messages' do
491
+ allow(client).to receive(:handle_version_update)
492
+
493
+ payload = {
494
+ type: 'version_update',
495
+ version: 2,
496
+ project_key: 'project',
497
+ environment_key: 'production'
498
+ }
499
+
500
+ client.handle_flags_update(payload)
501
+
502
+ expect(client).to have_received(:handle_version_update).with(payload)
503
+ end
504
+
505
+ it 'updates cache with new flags' do
506
+ cache = client.instance_variable_get(:@cache)
507
+
508
+ payload = {
509
+ flags: {
510
+ 'cached-flag' => { type: 'boolean', enabled: true }
511
+ }
512
+ }
513
+
514
+ client.handle_flags_update(payload)
515
+
516
+ expect(cache.get('flag:cached-flag')).to eq({ type: 'boolean', enabled: true })
517
+ end
518
+
519
+ it 'emits ready event on first update' do
520
+ ready_called = false
521
+ client.on(:ready) { ready_called = true }
522
+
523
+ payload = {
524
+ flags: { 'flag' => { type: 'boolean', enabled: true } }
525
+ }
526
+
527
+ client.handle_flags_update(payload)
528
+
529
+ expect(ready_called).to be true
530
+ expect(client.ready?).to be true
531
+ end
532
+
533
+ it 'emits flags_updated event' do
534
+ flags_received = nil
535
+ client.on(:flags_updated) { |flags| flags_received = flags }
536
+
537
+ payload = {
538
+ flags: { 'flag' => { type: 'boolean', enabled: true } }
539
+ }
540
+
541
+ # Mark as ready first to avoid ready event
542
+ client.instance_variable_get(:@ready).make_true
543
+
544
+ client.handle_flags_update(payload)
545
+
546
+ expect(flags_received).to eq({ 'flag' => { type: 'boolean', enabled: true } })
547
+ end
548
+ end
549
+
550
+ describe '#handle_connect' do
551
+ it 'logs connection when debug is enabled' do
552
+ debug_client = described_class.new(sdk_key: 'key', debug: true)
553
+
554
+ expect { debug_client.handle_connect }.to output(/SSE connected/).to_stdout
555
+
556
+ debug_client.destroy
557
+ end
558
+
559
+ it 'does not raise errors' do
560
+ expect { client.handle_connect }.not_to raise_error
561
+ end
562
+ end
563
+
564
+ describe '#handle_disconnect' do
565
+ it 'sets ready state to false' do
566
+ client.instance_variable_get(:@ready).make_true
567
+
568
+ client.handle_disconnect
569
+
570
+ expect(client.ready?).to be false
571
+ end
572
+
573
+ it 'emits disconnected event' do
574
+ disconnected = false
575
+ client.on(:disconnected) { disconnected = true }
576
+
577
+ client.handle_disconnect
578
+
579
+ expect(disconnected).to be true
580
+ end
581
+ end
582
+
583
+ describe '#handle_error' do
584
+ it 'emits error event' do
585
+ error_received = nil
586
+ client.on(:error) { |e| error_received = e }
587
+
588
+ test_error = StandardError.new('Test error')
589
+ client.handle_error(test_error)
590
+
591
+ expect(error_received).to eq(test_error)
592
+ end
593
+
594
+ it 'logs error when debug is enabled' do
595
+ debug_client = described_class.new(sdk_key: 'key', debug: true)
596
+
597
+ expect do
598
+ debug_client.handle_error(StandardError.new('Test'))
599
+ end.to output(/Error: Test/).to_stdout
600
+
601
+ debug_client.destroy
602
+ end
603
+ end
604
+
605
+ describe '#destroy' do
606
+ it 'stops rollout stage polling' do
607
+ client.instance_variable_set(:@rollout_stage_timer, Thread.new { sleep })
608
+ timer = client.instance_variable_get(:@rollout_stage_timer)
609
+
610
+ client.destroy
611
+
612
+ sleep 0.1
613
+ expect(timer.alive?).to be false
614
+ end
615
+
616
+ it 'clears all listeners' do
617
+ client.on(:ready) { }
618
+ client.on(:error) { }
619
+
620
+ client.destroy
621
+
622
+ listeners = client.instance_variable_get(:@listeners)
623
+ expect(listeners[:ready]).to be_empty
624
+ expect(listeners[:error]).to be_empty
625
+ end
626
+
627
+ it 'clears flags' do
628
+ evaluator = client.instance_variable_get(:@evaluator)
629
+ evaluator.update_flags({ 'flag' => { type: 'boolean' } })
630
+
631
+ expect(client.all_flag_keys).not_to be_empty
632
+
633
+ flags = client.instance_variable_get(:@flags)
634
+ flags.clear
635
+
636
+ expect(client.instance_variable_get(:@flags)).to be_empty
637
+ end
638
+
639
+ it 'sets ready to false' do
640
+ client.instance_variable_get(:@ready).make_true
641
+
642
+ client.destroy
643
+
644
+ expect(client.ready?).to be false
645
+ end
646
+
647
+ it 'destroys cache when present' do
648
+ cache = client.instance_variable_get(:@cache)
649
+ expect(cache).to receive(:destroy).at_least(:once).and_call_original
650
+
651
+ client.destroy
652
+ end
653
+
654
+ it 'is safe to call multiple times' do
655
+ expect { client.destroy }.not_to raise_error
656
+ expect { client.destroy }.not_to raise_error
657
+ end
658
+ end
659
+
660
+ describe 'thread safety' do
661
+ it 'handles concurrent flag evaluations' do
662
+ evaluator = client.instance_variable_get(:@evaluator)
663
+ evaluator.update_flags({
664
+ 'flag' => { type: 'boolean', enabled: true, value: true }
665
+ })
666
+
667
+ threads = 20.times.map do |i|
668
+ Thread.new do
669
+ 100.times do
670
+ result = client.enabled?('flag', { user: { id: "user-#{i}" } })
671
+ expect(result).to be true
672
+ end
673
+ end
674
+ end
675
+
676
+ threads.each(&:join)
677
+ end
678
+
679
+ it 'handles concurrent listener registration' do
680
+ threads = 10.times.map do
681
+ Thread.new do
682
+ 10.times do
683
+ client.on(:ready) { }
684
+ end
685
+ end
686
+ end
687
+
688
+ threads.each(&:join)
689
+
690
+ listeners = client.instance_variable_get(:@listeners)[:ready]
691
+ expect(listeners.length).to eq(100)
692
+ end
693
+
694
+ it 'handles concurrent event emission' do
695
+ call_count = Concurrent::AtomicFixnum.new(0)
696
+ client.on(:ready) { call_count.increment }
697
+
698
+ threads = 50.times.map do
699
+ Thread.new do
700
+ client.send(:emit, :ready)
701
+ end
702
+ end
703
+
704
+ threads.each(&:join)
705
+ expect(call_count.value).to eq(50)
706
+ end
707
+ end
708
+
709
+ describe 'connection mode' do
710
+ it 'uses shared connection by default' do
711
+ expect(client.instance_variable_get(:@using_shared_connection)).to be true
712
+ expect(client.instance_variable_get(:@connection)).to be_a(ToggleCraft::SharedSSEConnection)
713
+ end
714
+
715
+ it 'uses dedicated connection when share_connection is false' do
716
+ dedicated = described_class.new(sdk_key: 'key', share_connection: false)
717
+ expect(dedicated.instance_variable_get(:@using_shared_connection)).to be false
718
+ expect(dedicated.instance_variable_get(:@connection)).to be_a(ToggleCraft::SSEConnection)
719
+ dedicated.destroy
720
+ end
721
+
722
+ it 'enables debug on connection pool when debug is true and sharing is enabled' do
723
+ expect(ToggleCraft::ConnectionPool).to receive(:set_debug).with(true)
724
+ debug_client = described_class.new(sdk_key: 'key', debug: true, share_connection: true)
725
+ debug_client.destroy
726
+ end
727
+ end
728
+ end