1+ import { deepEqual } from "fast-equals" ;
2+
13import {
24 AutoFeedback ,
35 Feedback ,
@@ -17,7 +19,7 @@ import {
1719import { ToolbarPosition } from "./ui/types" ;
1820import { API_BASE_URL , APP_BASE_URL , SSE_REALTIME_BASE_URL } from "./config" ;
1921import { ReflagContext } from "./context" ;
20- import { HookArgs , HooksManager } from "./hooksManager" ;
22+ import { HookArgs , HooksManager , State } from "./hooksManager" ;
2123import { HttpClient } from "./httpClient" ;
2224import { Logger , loggerWithPrefix , quietConsoleLogger } from "./logger" ;
2325import { showToolbarToggle } from "./toolbar" ;
@@ -176,11 +178,6 @@ export interface Config {
176178 * Whether the client is bootstrapped.
177179 */
178180 bootstrapped : boolean ;
179-
180- /**
181- * Whether the client is initialized.
182- */
183- initialized : boolean ;
184181}
185182
186183/**
@@ -318,7 +315,6 @@ const defaultConfig: Config = {
318315 enableTracking : true ,
319316 offline : false ,
320317 bootstrapped : false ,
321- initialized : false ,
322318} ;
323319
324320/**
@@ -379,18 +375,19 @@ export interface Flag {
379375
380376function shouldShowToolbar ( opts : InitOptions ) {
381377 const toolbarOpts = opts . toolbar ;
378+ if ( typeof window === "undefined" ) return false ;
382379 if ( typeof toolbarOpts === "boolean" ) return toolbarOpts ;
383380 if ( typeof toolbarOpts ?. show === "boolean" ) return toolbarOpts . show ;
384-
385- return window ?. location ?. hostname === "localhost" ;
381+ return window . location . hostname === "localhost" ;
386382}
387383
388384/**
389385 * ReflagClient lets you interact with the Reflag API.
390386 */
391387export class ReflagClient {
388+ private state : State = "idle" ;
392389 private readonly publishableKey : string ;
393- private readonly context : ReflagContext ;
390+ private context : ReflagContext ;
394391 private config : Config ;
395392 private requestFeedbackOptions : Partial < RequestFeedbackOptions > ;
396393 private readonly httpClient : HttpClient ;
@@ -413,7 +410,7 @@ export class ReflagClient {
413410 this . context = {
414411 user : opts ?. user ?. id ? opts . user : undefined ,
415412 company : opts ?. company ?. id ? opts . company : undefined ,
416- otherContext : opts ?. otherContext ,
413+ other : { ... opts ?. otherContext , ... opts ?. other } ,
417414 } ;
418415
419416 this . config = {
@@ -424,7 +421,6 @@ export class ReflagClient {
424421 offline : opts ?. offline ?? defaultConfig . offline ,
425422 bootstrapped :
426423 opts && "bootstrappedFlags" in opts && ! ! opts . bootstrappedFlags ,
427- initialized : false ,
428424 } ;
429425
430426 this . requestFeedbackOptions = {
@@ -440,11 +436,10 @@ export class ReflagClient {
440436
441437 this . flagsClient = new FlagsClient (
442438 this . httpClient ,
443- // API expects `other` and we have `otherContext`.
444439 {
445440 user : this . context . user ,
446441 company : this . context . company ,
447- other : this . context . otherContext ,
442+ other : { ... this . context . otherContext , ... this . context . other } ,
448443 } ,
449444 this . logger ,
450445 isBootstrapped ( opts )
@@ -455,6 +450,7 @@ export class ReflagClient {
455450 : {
456451 expireTimeMs : opts . expireTimeMs ,
457452 staleTimeMs : opts . staleTimeMs ,
453+ staleWhileRevalidate : opts . staleWhileRevalidate ,
458454 timeoutMs : opts . timeoutMs ,
459455 fallbackFlags : opts . fallbackFlags ,
460456 offline : this . config . offline ,
@@ -506,10 +502,11 @@ export class ReflagClient {
506502 * Must be called before calling other SDK methods.
507503 */
508504 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 } ` ) ;
511507 return ;
512508 }
509+ this . setState ( "initializing" ) ;
513510
514511 const start = Date . now ( ) ;
515512 if ( this . autoFeedback ) {
@@ -542,7 +539,27 @@ export class ReflagClient {
542539 "ms" +
543540 ( this . config . offline ? " (offline mode)" : "" ) ,
544541 ) ;
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 ;
546563 }
547564
548565 /**
@@ -584,67 +601,120 @@ export class ReflagClient {
584601 /**
585602 * Update the user context.
586603 * 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 .
588605 *
589606 * @param user
590607 */
591608 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 = {
600611 ...this . context . user ,
601612 ...user ,
602613 id : user . id ?? this . context . user ?. id ,
603614 } ;
615+
616+ // Nothing has changed, skipping update
617+ if ( deepEqual ( this . context . user , newUserContext ) ) return ;
618+ this . context . user = newUserContext ;
604619 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+
605626 await this . flagsClient . setContext ( this . context ) ;
606627 }
607628
608629 /**
609630 * Update the company context.
610631 * 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 .
612633 *
613634 * @param company The company details.
614635 */
615636 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 = {
623638 ...this . context . company ,
624639 ...company ,
625640 id : company . id ?? this . context . company ?. id ,
626641 } ;
642+
643+ // Nothing has changed, skipping update
644+ if ( deepEqual ( this . context . company , newCompanyContext ) ) return ;
645+ this . context . company = newCompanyContext ;
627646 void this . company ( ) ;
647+
628648 await this . flagsClient . setContext ( this . context ) ;
629649 }
630650
631651 /**
632652 * Update the company context.
633653 * 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 .
635655 *
636656 * @param otherContext Additional context.
637657 */
638658 async updateOtherContext ( otherContext : {
639659 [ key : string ] : string | number | undefined ;
640660 } ) {
641- this . context . otherContext = {
642- ...this . context . otherContext ,
661+ const newOtherContext = {
662+ ...this . context . other ,
643663 ...otherContext ,
644664 } ;
665+
666+ // Nothing has changed, skipping update
667+ if ( deepEqual ( this . context . other , newOtherContext ) ) return ;
668+ this . context . other = newOtherContext ;
669+
645670 await this . flagsClient . setContext ( this . context ) ;
646671 }
647672
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+
648718 /**
649719 * Track an event in Reflag.
650720 *
@@ -876,27 +946,17 @@ export class ReflagClient {
876946 } ;
877947 }
878948
949+ private setState ( state : State ) {
950+ this . state = state ;
951+ this . hooks . trigger ( "stateUpdated" , state ) ;
952+ }
953+
879954 private sendCheckEvent ( checkEvent : CheckEvent ) {
880955 return this . flagsClient . sendCheckEvent ( checkEvent , ( ) => {
881956 this . hooks . trigger ( "check" , checkEvent ) ;
882957 } ) ;
883958 }
884959
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-
900960 /**
901961 * Send attributes to Reflag for the current user
902962 */
@@ -958,4 +1018,13 @@ export class ReflagClient {
9581018 this . hooks . trigger ( "company" , this . context . company ) ;
9591019 return res ;
9601020 }
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+ }
9611030}
0 commit comments