@@ -11,6 +11,7 @@ import {
11
11
EppoClient ,
12
12
Flag ,
13
13
HybridConfigurationStore ,
14
+ IAssignmentEvent ,
14
15
IAsyncStore ,
15
16
IPrecomputedConfigurationResponse ,
16
17
VariationType ,
@@ -29,10 +30,18 @@ import {
29
30
validateTestAssignments ,
30
31
} from '../test/testHelpers' ;
31
32
32
- import { IClientConfig } from './i-client-config' ;
33
+ import {
34
+ IApiOptions ,
35
+ IClientConfig ,
36
+ IClientOptions ,
37
+ IPollingOptions ,
38
+ IStorageOptions ,
39
+ } from './i-client-config' ;
33
40
import { ServingStoreUpdateStrategy } from './isolatable-hybrid.store' ;
34
41
35
42
import {
43
+ EppoJSClient ,
44
+ EppoJSClientV2 ,
36
45
EppoPrecomputedJSClient ,
37
46
getConfigUrl ,
38
47
getInstance ,
@@ -138,6 +147,10 @@ const mockObfuscatedUfcFlagConfig: Flag = {
138
147
key : base64Encode ( 'variant-2' ) ,
139
148
value : base64Encode ( 'variant-2' ) ,
140
149
} ,
150
+ [ base64Encode ( 'variant-3' ) ] : {
151
+ key : base64Encode ( 'variant-3' ) ,
152
+ value : base64Encode ( 'variant-3' ) ,
153
+ } ,
141
154
} ,
142
155
allocations : [
143
156
{
@@ -382,6 +395,168 @@ describe('EppoJSClient E2E test', () => {
382
395
} ) ;
383
396
} ) ;
384
397
398
+ describe ( 'decoupled initialization' , ( ) => {
399
+ let mockLogger : IAssignmentLogger ;
400
+ // eslint-disable-next-line @typescript-eslint/ban-types
401
+ let init : ( config : IClientConfig ) => Promise < EppoJSClient > ;
402
+ // eslint-disable-next-line @typescript-eslint/ban-types
403
+ let getInstance : ( ) => EppoJSClient ;
404
+
405
+ beforeEach ( async ( ) => {
406
+ jest . isolateModules ( ( ) => {
407
+ // Isolate and re-require so that the static instance is reset to its default state
408
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
409
+ const reloadedModule = require ( './index' ) ;
410
+ init = reloadedModule . init ;
411
+ getInstance = reloadedModule . getInstance ;
412
+ } ) ;
413
+ } ) ;
414
+
415
+ describe ( 'isolated from the singleton' , ( ) => {
416
+ beforeEach ( ( ) => {
417
+ mockLogger = td . object < IAssignmentLogger > ( ) ;
418
+
419
+ global . fetch = jest . fn ( ( ) => {
420
+ const ufc = { flags : { [ obfuscatedFlagKey ] : mockObfuscatedUfcFlagConfig } } ;
421
+
422
+ return Promise . resolve ( {
423
+ ok : true ,
424
+ status : 200 ,
425
+ json : ( ) => Promise . resolve ( ufc ) ,
426
+ } ) ;
427
+ } ) as jest . Mock ;
428
+ } ) ;
429
+
430
+ afterEach ( ( ) => {
431
+ jest . restoreAllMocks ( ) ;
432
+ } ) ;
433
+
434
+
435
+ it ( 'should be independent of the singleton' , async ( ) => {
436
+ const apiOptions : IApiOptions = { sdkKey : '<MY SDK KEY>' } ;
437
+ const options : IClientOptions = { ...apiOptions , assignmentLogger : mockLogger } ;
438
+ const isolatedClient = new EppoJSClientV2 ( options ) ;
439
+
440
+ expect ( isolatedClient ) . not . toEqual ( getInstance ( ) ) ;
441
+ await isolatedClient . waitForReady ( ) ;
442
+
443
+ expect ( isolatedClient . isInitialized ( ) ) . toBe ( true ) ;
444
+ expect ( isolatedClient . initialized ) . toBe ( true ) ;
445
+ expect ( getInstance ( ) . isInitialized ( ) ) . toBe ( false ) ;
446
+ expect ( getInstance ( ) . initialized ) . toBe ( false ) ;
447
+
448
+ expect ( getInstance ( ) . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
449
+ 'default-value' ,
450
+ ) ;
451
+ expect (
452
+ isolatedClient . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ,
453
+ ) . toEqual ( 'variant-1' ) ;
454
+ } ) ;
455
+ it ( 'initializes on instantiation and notifies when ready' , async ( ) => {
456
+ const apiOptions : IApiOptions = { sdkKey : '<MY SDK KEY>' , baseUrl } ;
457
+ const options : IClientOptions = { ...apiOptions , assignmentLogger : mockLogger } ;
458
+ const client = new EppoJSClientV2 ( options ) ;
459
+
460
+ expect ( client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
461
+ 'default-value' ,
462
+ ) ;
463
+
464
+ await client . waitForReady ( ) ;
465
+
466
+ const assignment = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ;
467
+ expect ( assignment ) . toEqual ( 'variant-1' ) ;
468
+ } ) ;
469
+ } ) ;
470
+
471
+ describe ( 'multiple client instances' , ( ) => {
472
+ const API_KEY_1 = 'my-api-key-1' ;
473
+ const API_KEY_2 = 'my-api-key-2' ;
474
+ const API_KEY_3 = 'my-api-key-3' ;
475
+
476
+ const commonOptions : Omit < IClientOptions , 'sdkKey' > = {
477
+ baseUrl,
478
+ assignmentLogger : mockLogger ,
479
+ } ;
480
+
481
+ let callCount = 0 ;
482
+
483
+ beforeAll ( ( ) => {
484
+ global . fetch = jest . fn ( ( url : string ) => {
485
+ callCount ++ ;
486
+
487
+ const urlParams = new URLSearchParams ( url . split ( '?' ) [ 1 ] ) ;
488
+
489
+ // Get the value of the apiKey parameter and serve a specific variant.
490
+ const apiKey = urlParams . get ( 'apiKey' ) ;
491
+
492
+ // differentiate between the SDK keys by changing the variant that `flagKey` assigns.
493
+ let variant = 'variant-1' ;
494
+ if ( apiKey === API_KEY_2 ) {
495
+ variant = 'variant-2' ;
496
+ } else if ( apiKey === API_KEY_3 ) {
497
+ variant = 'variant-3' ;
498
+ }
499
+
500
+ const encodedVariant = base64Encode ( variant ) ;
501
+
502
+ // deep copy the mock data since we're going to inject a change below.
503
+ const flagConfig : Flag = JSON . parse ( JSON . stringify ( mockObfuscatedUfcFlagConfig ) ) ;
504
+ // Inject the encoded variant as a single split for the flag's only allocation.
505
+ flagConfig . allocations [ 0 ] . splits = [
506
+ {
507
+ variationKey : encodedVariant ,
508
+ shards : [ ] ,
509
+ } ,
510
+ ] ;
511
+
512
+ const ufc = { flags : { [ obfuscatedFlagKey ] : flagConfig } } ;
513
+
514
+ return Promise . resolve ( {
515
+ ok : true ,
516
+ status : 200 ,
517
+ json : ( ) => Promise . resolve ( ufc ) ,
518
+ } ) ;
519
+ } ) as jest . Mock ;
520
+ } ) ;
521
+ afterAll ( ( ) => {
522
+ jest . restoreAllMocks ( ) ;
523
+ } ) ;
524
+
525
+ it ( 'should operate in parallel' , async ( ) => {
526
+ const singleton = await init ( { ...commonOptions , apiKey : API_KEY_1 } ) ;
527
+ expect ( singleton . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
528
+ 'variant-1' ,
529
+ ) ;
530
+ expect ( callCount ) . toBe ( 1 ) ;
531
+
532
+ const myClient2 = new EppoJSClientV2 ( { ...commonOptions , sdkKey : API_KEY_2 } ) ;
533
+ await myClient2 . waitForReady ( ) ;
534
+ expect ( callCount ) . toBe ( 2 ) ;
535
+
536
+ expect ( singleton . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
537
+ 'variant-1' ,
538
+ ) ;
539
+ expect ( myClient2 . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
540
+ 'variant-2' ,
541
+ ) ;
542
+
543
+ const myClient3 = new EppoJSClientV2 ( { ...commonOptions , sdkKey : API_KEY_3 } ) ;
544
+ await myClient3 . waitForReady ( ) ;
545
+
546
+ expect ( singleton . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
547
+ 'variant-1' ,
548
+ ) ;
549
+ expect ( myClient2 . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
550
+ 'variant-2' ,
551
+ ) ;
552
+
553
+ expect ( myClient3 . getStringAssignment ( flagKey , 'subject-10' , { } , 'default-value' ) ) . toEqual (
554
+ 'variant-3' ,
555
+ ) ;
556
+ } ) ;
557
+ } ) ;
558
+ } ) ;
559
+
385
560
describe ( 'sync init' , ( ) => {
386
561
it ( 'initializes with flags in obfuscated mode' , ( ) => {
387
562
const client = offlineInit ( {
@@ -422,9 +597,9 @@ describe('initialization options', () => {
422
597
} as unknown as Record < 'flags' , Record < string , Flag > > ;
423
598
424
599
// eslint-disable-next-line @typescript-eslint/ban-types
425
- let init : ( config : IClientConfig ) => Promise < EppoClient > ;
600
+ let init : ( config : IClientConfig ) => Promise < EppoJSClient > ;
426
601
// eslint-disable-next-line @typescript-eslint/ban-types
427
- let getInstance : ( ) => EppoClient ;
602
+ let getInstance : ( ) => EppoJSClient ;
428
603
429
604
beforeEach ( async ( ) => {
430
605
jest . isolateModules ( ( ) => {
0 commit comments