1
+ import { deepEqual } from "fast-equals" ;
2
+
1
3
import {
2
4
AutoFeedback ,
3
5
Feedback ,
@@ -17,7 +19,7 @@ import {
17
19
import { ToolbarPosition } from "./ui/types" ;
18
20
import { API_BASE_URL , APP_BASE_URL , SSE_REALTIME_BASE_URL } from "./config" ;
19
21
import { ReflagContext } from "./context" ;
20
- import { HookArgs , HooksManager } from "./hooksManager" ;
22
+ import { HookArgs , HooksManager , State } from "./hooksManager" ;
21
23
import { HttpClient } from "./httpClient" ;
22
24
import { Logger , loggerWithPrefix , quietConsoleLogger } from "./logger" ;
23
25
import { showToolbarToggle } from "./toolbar" ;
@@ -176,11 +178,6 @@ export interface Config {
176
178
* Whether the client is bootstrapped.
177
179
*/
178
180
bootstrapped : boolean ;
179
-
180
- /**
181
- * Whether the client is initialized.
182
- */
183
- initialized : boolean ;
184
181
}
185
182
186
183
/**
@@ -318,7 +315,6 @@ const defaultConfig: Config = {
318
315
enableTracking : true ,
319
316
offline : false ,
320
317
bootstrapped : false ,
321
- initialized : false ,
322
318
} ;
323
319
324
320
/**
@@ -379,18 +375,19 @@ export interface Flag {
379
375
380
376
function shouldShowToolbar ( opts : InitOptions ) {
381
377
const toolbarOpts = opts . toolbar ;
378
+ if ( typeof window === "undefined" ) return false ;
382
379
if ( typeof toolbarOpts === "boolean" ) return toolbarOpts ;
383
380
if ( typeof toolbarOpts ?. show === "boolean" ) return toolbarOpts . show ;
384
-
385
- return window ?. location ?. hostname === "localhost" ;
381
+ return window . location . hostname === "localhost" ;
386
382
}
387
383
388
384
/**
389
385
* ReflagClient lets you interact with the Reflag API.
390
386
*/
391
387
export class ReflagClient {
388
+ private state : State = "idle" ;
392
389
private readonly publishableKey : string ;
393
- private readonly context : ReflagContext ;
390
+ private context : ReflagContext ;
394
391
private config : Config ;
395
392
private requestFeedbackOptions : Partial < RequestFeedbackOptions > ;
396
393
private readonly httpClient : HttpClient ;
@@ -413,7 +410,7 @@ export class ReflagClient {
413
410
this . context = {
414
411
user : opts ?. user ?. id ? opts . user : undefined ,
415
412
company : opts ?. company ?. id ? opts . company : undefined ,
416
- otherContext : opts ?. otherContext ,
413
+ other : { ... opts ?. otherContext , ... opts ?. other } ,
417
414
} ;
418
415
419
416
this . config = {
@@ -424,7 +421,6 @@ export class ReflagClient {
424
421
offline : opts ?. offline ?? defaultConfig . offline ,
425
422
bootstrapped :
426
423
opts && "bootstrappedFlags" in opts && ! ! opts . bootstrappedFlags ,
427
- initialized : false ,
428
424
} ;
429
425
430
426
this . requestFeedbackOptions = {
@@ -440,11 +436,10 @@ export class ReflagClient {
440
436
441
437
this . flagsClient = new FlagsClient (
442
438
this . httpClient ,
443
- // API expects `other` and we have `otherContext`.
444
439
{
445
440
user : this . context . user ,
446
441
company : this . context . company ,
447
- other : this . context . otherContext ,
442
+ other : { ... this . context . otherContext , ... this . context . other } ,
448
443
} ,
449
444
this . logger ,
450
445
isBootstrapped ( opts )
@@ -455,6 +450,7 @@ export class ReflagClient {
455
450
: {
456
451
expireTimeMs : opts . expireTimeMs ,
457
452
staleTimeMs : opts . staleTimeMs ,
453
+ staleWhileRevalidate : opts . staleWhileRevalidate ,
458
454
timeoutMs : opts . timeoutMs ,
459
455
fallbackFlags : opts . fallbackFlags ,
460
456
offline : this . config . offline ,
@@ -506,10 +502,11 @@ export class ReflagClient {
506
502
* Must be called before calling other SDK methods.
507
503
*/
508
504
async initialize ( ) {
509
- if ( this . config . initialized ) {
510
- this . logger . warn ( "Reflag client already initialized" ) ;
505
+ if ( this . state === "initializing" || this . state === " initialized" ) {
506
+ this . logger . warn ( ` "Reflag client already ${ this . state } ` ) ;
511
507
return ;
512
508
}
509
+ this . setState ( "initializing" ) ;
513
510
514
511
const start = Date . now ( ) ;
515
512
if ( this . autoFeedback ) {
@@ -542,7 +539,27 @@ export class ReflagClient {
542
539
"ms" +
543
540
( this . config . offline ? " (offline mode)" : "" ) ,
544
541
) ;
545
- this . config . initialized = true ;
542
+ this . setState ( "initialized" ) ;
543
+ }
544
+
545
+ /**
546
+ * Stop the SDK.
547
+ * This will stop any automated feedback surveys.
548
+ *
549
+ **/
550
+ async stop ( ) {
551
+ if ( this . autoFeedback ) {
552
+ // ensure fully initialized before stopping
553
+ await this . autoFeedbackInit ;
554
+ this . autoFeedback . stop ( ) ;
555
+ }
556
+
557
+ this . flagsClient . stop ( ) ;
558
+ this . setState ( "stopped" ) ;
559
+ }
560
+
561
+ getState ( ) {
562
+ return this . state ;
546
563
}
547
564
548
565
/**
@@ -584,67 +601,120 @@ export class ReflagClient {
584
601
/**
585
602
* Update the user context.
586
603
* Performs a shallow merge with the existing user context.
587
- * Attempting to update the user ID will log a warning and be ignored .
604
+ * It will not update the context if nothing has changed .
588
605
*
589
606
* @param user
590
607
*/
591
608
async updateUser ( user : { [ key : string ] : string | number | undefined } ) {
592
- if ( user . id && user . id !== this . context . user ?. id ) {
593
- this . logger . warn (
594
- "ignoring attempt to update the user ID. Re-initialize the ReflagClient with a new user ID instead." ,
595
- ) ;
596
- return ;
597
- }
598
-
599
- this . context . user = {
609
+ const userIdChanged = user . id && user . id !== this . context . user ?. id ;
610
+ const newUserContext = {
600
611
...this . context . user ,
601
612
...user ,
602
613
id : user . id ?? this . context . user ?. id ,
603
614
} ;
615
+
616
+ // Nothing has changed, skipping update
617
+ if ( deepEqual ( this . context . user , newUserContext ) ) return ;
618
+ this . context . user = newUserContext ;
604
619
void this . user ( ) ;
620
+
621
+ // Update the feedback user if the user ID has changed
622
+ if ( userIdChanged ) {
623
+ void this . updateAutoFeedbackUser ( String ( user . id ) ) ;
624
+ }
625
+
605
626
await this . flagsClient . setContext ( this . context ) ;
606
627
}
607
628
608
629
/**
609
630
* Update the company context.
610
631
* Performs a shallow merge with the existing company context.
611
- * Attempting to update the company ID will log a warning and be ignored .
632
+ * It will not update the context if nothing has changed .
612
633
*
613
634
* @param company The company details.
614
635
*/
615
636
async updateCompany ( company : { [ key : string ] : string | number | undefined } ) {
616
- if ( company . id && company . id !== this . context . company ?. id ) {
617
- this . logger . warn (
618
- "ignoring attempt to update the company ID. Re-initialize the ReflagClient with a new company ID instead." ,
619
- ) ;
620
- return ;
621
- }
622
- this . context . company = {
637
+ const newCompanyContext = {
623
638
...this . context . company ,
624
639
...company ,
625
640
id : company . id ?? this . context . company ?. id ,
626
641
} ;
642
+
643
+ // Nothing has changed, skipping update
644
+ if ( deepEqual ( this . context . company , newCompanyContext ) ) return ;
645
+ this . context . company = newCompanyContext ;
627
646
void this . company ( ) ;
647
+
628
648
await this . flagsClient . setContext ( this . context ) ;
629
649
}
630
650
631
651
/**
632
652
* Update the company context.
633
653
* Performs a shallow merge with the existing company context.
634
- * Updates to the company ID will be ignored .
654
+ * It will not update the context if nothing has changed .
635
655
*
636
656
* @param otherContext Additional context.
637
657
*/
638
658
async updateOtherContext ( otherContext : {
639
659
[ key : string ] : string | number | undefined ;
640
660
} ) {
641
- this . context . otherContext = {
642
- ...this . context . otherContext ,
661
+ const newOtherContext = {
662
+ ...this . context . other ,
643
663
...otherContext ,
644
664
} ;
665
+
666
+ // Nothing has changed, skipping update
667
+ if ( deepEqual ( this . context . other , newOtherContext ) ) return ;
668
+ this . context . other = newOtherContext ;
669
+
645
670
await this . flagsClient . setContext ( this . context ) ;
646
671
}
647
672
673
+ /**
674
+ * Update the context.
675
+ * Performs a shallow merge with the existing context.
676
+ * It will not update the context if nothing has changed.
677
+ *
678
+ * @param context The context to update.
679
+ */
680
+ async updateContext ( { otherContext, ...context } : ReflagContext ) {
681
+ const userIdChanged =
682
+ context . user ?. id && context . user . id !== this . context . user ?. id ;
683
+ const newContext = {
684
+ ...this . context ,
685
+ ...context ,
686
+ other : { ...this . context . other , ...otherContext , ...context . other } ,
687
+ } ;
688
+
689
+ // Nothing has changed, skipping update
690
+ if ( deepEqual ( this . context , newContext ) ) return ;
691
+ this . context = newContext ;
692
+
693
+ if ( context . company ) {
694
+ void this . company ( ) ;
695
+ }
696
+
697
+ if ( context . user ) {
698
+ void this . user ( ) ;
699
+ // Update the automatic feedback user if the user ID has changed
700
+ if ( userIdChanged ) {
701
+ void this . updateAutoFeedbackUser ( String ( context . user . id ) ) ;
702
+ }
703
+ }
704
+
705
+ await this . flagsClient . setContext ( this . context ) ;
706
+ }
707
+
708
+ /**
709
+ * Update the flags.
710
+ *
711
+ * @param flags The flags to update.
712
+ * @param triggerEvent Whether to trigger the `flagsUpdated` event.
713
+ */
714
+ updateFlags ( flags : FetchedFlags , triggerEvent = true ) {
715
+ this . flagsClient . setFetchedFlags ( flags , triggerEvent ) ;
716
+ }
717
+
648
718
/**
649
719
* Track an event in Reflag.
650
720
*
@@ -876,27 +946,17 @@ export class ReflagClient {
876
946
} ;
877
947
}
878
948
949
+ private setState ( state : State ) {
950
+ this . state = state ;
951
+ this . hooks . trigger ( "stateUpdated" , state ) ;
952
+ }
953
+
879
954
private sendCheckEvent ( checkEvent : CheckEvent ) {
880
955
return this . flagsClient . sendCheckEvent ( checkEvent , ( ) => {
881
956
this . hooks . trigger ( "check" , checkEvent ) ;
882
957
} ) ;
883
958
}
884
959
885
- /**
886
- * Stop the SDK.
887
- * This will stop any automated feedback surveys.
888
- *
889
- **/
890
- async stop ( ) {
891
- if ( this . autoFeedback ) {
892
- // ensure fully initialized before stopping
893
- await this . autoFeedbackInit ;
894
- this . autoFeedback . stop ( ) ;
895
- }
896
-
897
- this . flagsClient . stop ( ) ;
898
- }
899
-
900
960
/**
901
961
* Send attributes to Reflag for the current user
902
962
*/
@@ -958,4 +1018,13 @@ export class ReflagClient {
958
1018
this . hooks . trigger ( "company" , this . context . company ) ;
959
1019
return res ;
960
1020
}
1021
+
1022
+ private async updateAutoFeedbackUser ( userId : string ) {
1023
+ if ( ! this . autoFeedback ) {
1024
+ return ;
1025
+ }
1026
+ // Ensure fully initialized before updating the user
1027
+ await this . autoFeedbackInit ;
1028
+ await this . autoFeedback . setUser ( userId ) ;
1029
+ }
961
1030
}
0 commit comments