Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c097336

Browse files
committed
feat: rewrite to new provider pattern and cookie overrides
1 parent 4640f2f commit c097336

File tree

19 files changed

+707
-479
lines changed

19 files changed

+707
-479
lines changed

packages/browser-sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"dependencies": {
3939
"@floating-ui/dom": "^1.6.8",
4040
"canonical-json": "^0.0.4",
41+
"fast-equals": "^5.2.2",
4142
"js-cookie": "^3.0.5",
4243
"preact": "^10.22.1"
4344
},

packages/browser-sdk/src/client.ts

Lines changed: 121 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { deepEqual } from "fast-equals";
2+
13
import {
24
AutoFeedback,
35
Feedback,
@@ -17,7 +19,7 @@ import {
1719
import { ToolbarPosition } from "./ui/types";
1820
import { API_BASE_URL, APP_BASE_URL, SSE_REALTIME_BASE_URL } from "./config";
1921
import { ReflagContext } from "./context";
20-
import { HookArgs, HooksManager } from "./hooksManager";
22+
import { HookArgs, HooksManager, State } from "./hooksManager";
2123
import { HttpClient } from "./httpClient";
2224
import { Logger, loggerWithPrefix, quietConsoleLogger } from "./logger";
2325
import { 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

380376
function 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
*/
391387
export 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
}

packages/browser-sdk/src/context.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ export interface ReflagContext {
5555
user?: UserContext;
5656

5757
/**
58-
* Context which is not related to a user or a company
58+
* Context which is not related to a user or a company.
59+
*/
60+
other?: Record<string, string | number | undefined>;
61+
62+
/**
63+
* Context which is not related to a user or a company.
64+
* @deprecated Use `other` instead
5965
*/
6066
otherContext?: Record<string, string | number | undefined>;
6167
}

0 commit comments

Comments
 (0)