@@ -539,6 +539,12 @@ describe('initialization options', () => {
539
539
} ) ;
540
540
541
541
it ( 'only fetches/does initialization workload once per API key if init is called multiple times concurrently' , async ( ) => {
542
+ // IMPORTANT NOTE
543
+ // Initializing the SDK with multiple different SDK Keys is undefined behaviour as the EppoClient is accessed via
544
+ // `getInstance` which returns a singleton. Initializing with multiple keys is _not yet supported_, so in this test,
545
+ // we just use the same key over and over. The more intricate parts of configuration loading and storing will
546
+ // silently break and fail the tests in unexpected ways if we use multiple keys.
547
+
542
548
let callCount = 0 ;
543
549
544
550
global . fetch = jest . fn ( ( ) => {
@@ -553,15 +559,15 @@ describe('initialization options', () => {
553
559
const inits : Promise < EppoClient > [ ] = [ ] ;
554
560
[
555
561
'KEY_1' ,
556
- 'KEY_2' ,
557
562
'KEY_1' ,
558
- 'KEY_2' ,
559
563
'KEY_1' ,
560
- 'KEY_2' ,
561
- 'KEY_3' ,
562
564
'KEY_1' ,
563
- 'KEY_2' ,
564
- 'KEY_3' ,
565
+ 'KEY_1' ,
566
+ 'KEY_1' ,
567
+ 'KEY_1' ,
568
+ 'KEY_1' ,
569
+ 'KEY_1' ,
570
+ 'KEY_1' ,
565
571
] . forEach ( ( varyingAPIKey ) => {
566
572
inits . push (
567
573
init ( {
@@ -580,12 +586,12 @@ describe('initialization options', () => {
580
586
const client = await Promise . race ( inits ) ;
581
587
await Promise . all ( inits ) ;
582
588
583
- expect ( callCount ) . toBe ( 3 ) ;
589
+ expect ( callCount ) . toBe ( 1 ) ;
584
590
callCount = 0 ;
585
591
expect ( client . getStringAssignment ( flagKey , 'subject' , { } , 'default-value' ) ) . toBe ( 'control' ) ;
586
592
587
593
const reInits : Promise < EppoClient > [ ] = [ ] ;
588
- [ 'KEY_1' , 'KEY_2 ' , 'KEY_3 ' , 'KEY_4 ' ] . forEach ( ( varyingAPIKey ) => {
594
+ [ 'KEY_1' , 'KEY_1 ' , 'KEY_1 ' , 'KEY_1 ' ] . forEach ( ( varyingAPIKey ) => {
589
595
reInits . push (
590
596
init ( {
591
597
apiKey : varyingAPIKey ,
@@ -598,7 +604,7 @@ describe('initialization options', () => {
598
604
599
605
await Promise . all ( reInits ) ;
600
606
601
- expect ( callCount ) . toBe ( 4 ) ;
607
+ expect ( callCount ) . toBe ( 1 ) ;
602
608
expect ( client . getStringAssignment ( flagKey , 'subject' , { } , 'default-value' ) ) . toBe ( 'control' ) ;
603
609
} ) ;
604
610
@@ -764,6 +770,9 @@ describe('initialization options', () => {
764
770
expect ( client . getStringAssignment ( flagKey , 'subject' , { } , 'default-value' ) ) . toBe ( 'control' ) ;
765
771
} ) ;
766
772
773
+ // This test sets up the EppoClient to fail to load a configuration.
774
+ // If init is told to skip the initial request and there is no persistent store with data,
775
+ // the client throws an error (unless throwOnFailedInitialization = false).
767
776
it ( 'skips initial request' , async ( ) => {
768
777
let callCount = 0 ;
769
778
@@ -780,6 +789,7 @@ describe('initialization options', () => {
780
789
await init ( {
781
790
apiKey,
782
791
baseUrl,
792
+ throwOnFailedInitialization : false ,
783
793
assignmentLogger : mockLogger ,
784
794
skipInitialRequest : true ,
785
795
} ) ;
@@ -1137,6 +1147,69 @@ describe('initialization options', () => {
1137
1147
) ;
1138
1148
} ) ;
1139
1149
} ) ;
1150
+
1151
+ describe ( 'advanced initialization conditions' , ( ) => {
1152
+ it ( 'skips the fetch and uses the persistent store when unexpired' , async ( ) => {
1153
+ // This test sets up a case where the persistent store has unexpired entries, but fails to load them into memory
1154
+ // before the fetching code checks to see whether the entries are expired. In this case, the fetch can abort but
1155
+ // we want the initialization routine to now wait for the config to finish loading.
1156
+ const entriesPromise = new DeferredPromise < Record < string , Flag > > ( ) ;
1157
+
1158
+ const mockStore : IAsyncStore < Flag > = {
1159
+ isInitialized ( ) {
1160
+ return false ; // mock that entries have not been save from a fetch.
1161
+ } ,
1162
+ async isExpired ( ) {
1163
+ return false ; // prevents a fetch
1164
+ } ,
1165
+ async entries ( ) {
1166
+ return entriesPromise . promise ;
1167
+ } ,
1168
+ async setEntries ( entries ) {
1169
+ // pass
1170
+ } ,
1171
+ } ;
1172
+
1173
+ let callCount = 0 ;
1174
+ const mockStoreEntries = { flags : { } } as unknown as Record < string , Flag > ;
1175
+ global . fetch = jest . fn ( ( ) => {
1176
+ callCount ++ ;
1177
+ return Promise . resolve ( {
1178
+ ok : true ,
1179
+ status : 200 ,
1180
+ json : ( ) => Promise . resolve ( { flags : mockStoreEntries } ) ,
1181
+ } ) ;
1182
+ } ) as jest . Mock ;
1183
+
1184
+ const mockLogger = td . object < IAssignmentLogger > ( ) ;
1185
+ let clientInitialized = false ;
1186
+ const initPromise = init ( {
1187
+ apiKey,
1188
+ baseUrl,
1189
+ persistentStore : mockStore ,
1190
+ forceReinitialize : true ,
1191
+ assignmentLogger : mockLogger ,
1192
+ } ) . then ( ( client ) => {
1193
+ clientInitialized = true ;
1194
+ return client ;
1195
+ } ) ;
1196
+
1197
+ expect ( callCount ) . toBe ( 0 ) ;
1198
+ expect ( clientInitialized ) . toBe ( false ) ;
1199
+
1200
+ // Complete the "load from cache"
1201
+ if ( entriesPromise . resolve ) {
1202
+ entriesPromise . resolve ( mockConfigResponse . flags ) ;
1203
+ } else {
1204
+ throw 'Error running test' ;
1205
+ }
1206
+
1207
+ // Await so it can finish its initialization before this test proceeds
1208
+ const client = await initPromise ;
1209
+ expect ( callCount ) . toBe ( 0 ) ;
1210
+ expect ( client . getStringAssignment ( flagKey , 'subject' , { } , 'default-value' ) ) . toBe ( 'control' ) ;
1211
+ } ) ;
1212
+ } ) ;
1140
1213
} ) ;
1141
1214
1142
1215
describe ( 'getConfigUrl function' , ( ) => {
@@ -1331,7 +1404,7 @@ describe('EppoClient config', () => {
1331
1404
return Promise . resolve ( {
1332
1405
ok : true ,
1333
1406
status : 200 ,
1334
- json : ( ) => Promise . resolve ( { } ) ,
1407
+ json : ( ) => Promise . resolve ( { flags : { } } ) ,
1335
1408
} ) ;
1336
1409
} ) as jest . Mock ;
1337
1410
const client = await init ( {
@@ -1378,3 +1451,20 @@ describe('EppoClient config', () => {
1378
1451
} ) ;
1379
1452
} ) ;
1380
1453
} ) ;
1454
+
1455
+ /**
1456
+ * A wrapper for a promise which allows for later resolution.
1457
+ */
1458
+ class DeferredPromise < T > {
1459
+ promise : Promise < T > ;
1460
+ resolve ?: ( value : PromiseLike < T > | T ) => void ;
1461
+ reject ?: ( reason ?: never ) => void ;
1462
+ constructor ( ) {
1463
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
1464
+ const self = this ;
1465
+ this . promise = new Promise < T > ( ( resolve , reject ) => {
1466
+ self . resolve = resolve ;
1467
+ self . reject = reject ;
1468
+ } ) ;
1469
+ }
1470
+ }
0 commit comments