@servicetitan/launchdarkly-service 38.3.0 → 39.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  import { LDClient } from 'launchdarkly-js-client-sdk';
2
- import { ProviderConfig } from 'launchdarkly-react-client-sdk';
3
- export declare function getLDClient({ clientSideID, context, options }: ProviderConfig): LDClient;
2
+ import { ClientConfig } from './types';
3
+ export declare function getLDClient({ clientSideID, context, options }: ClientConfig): LDClient;
4
4
  //# sourceMappingURL=get-ld-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-ld-client.d.ts","sourceRoot":"","sources":["../src/get-ld-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAc,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAI/D,wBAAgB,WAAW,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,cAAc,YAO7E"}
1
+ {"version":3,"file":"get-ld-client.d.ts","sourceRoot":"","sources":["../src/get-ld-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAc,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvC,wBAAgB,WAAW,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,QAAQ,CAOtF"}
@@ -3,10 +3,7 @@ const ldClients = new Map();
3
3
  export function getLDClient({ clientSideID, context, options }) {
4
4
  let client = ldClients.get(clientSideID);
5
5
  if (!client) {
6
- client = initialize(clientSideID, context !== null && context !== void 0 ? context : {
7
- kind: 'user',
8
- anonymous: true
9
- }, options);
6
+ client = initialize(clientSideID, context, options);
10
7
  ldClients.set(clientSideID, client);
11
8
  }
12
9
  return client;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/get-ld-client.ts"],"sourcesContent":["import { LDClient, initialize } from 'launchdarkly-js-client-sdk';\nimport { ProviderConfig } from 'launchdarkly-react-client-sdk';\n\nconst ldClients: Map<string, LDClient> = new Map();\n\nexport function getLDClient({ clientSideID, context, options }: ProviderConfig) {\n let client = ldClients.get(clientSideID);\n if (!client) {\n client = initialize(clientSideID, context ?? { kind: 'user', anonymous: true }, options);\n ldClients.set(clientSideID, client);\n }\n return client;\n}\n"],"names":["initialize","ldClients","Map","getLDClient","clientSideID","context","options","client","get","kind","anonymous","set"],"mappings":"AAAA,SAAmBA,UAAU,QAAQ,6BAA6B;AAGlE,MAAMC,YAAmC,IAAIC;AAE7C,OAAO,SAASC,YAAY,EAAEC,YAAY,EAAEC,OAAO,EAAEC,OAAO,EAAkB;IAC1E,IAAIC,SAASN,UAAUO,GAAG,CAACJ;IAC3B,IAAI,CAACG,QAAQ;QACTA,SAASP,WAAWI,cAAcC,oBAAAA,qBAAAA,UAAW;YAAEI,MAAM;YAAQC,WAAW;QAAK,GAAGJ;QAChFL,UAAUU,GAAG,CAACP,cAAcG;IAChC;IACA,OAAOA;AACX"}
1
+ {"version":3,"sources":["../src/get-ld-client.ts"],"sourcesContent":["import { LDClient, initialize } from 'launchdarkly-js-client-sdk';\nimport { ClientConfig } from './types';\n\nconst ldClients: Map<string, LDClient> = new Map();\n\nexport function getLDClient({ clientSideID, context, options }: ClientConfig): LDClient {\n let client = ldClients.get(clientSideID);\n if (!client) {\n client = initialize(clientSideID, context, options);\n ldClients.set(clientSideID, client);\n }\n return client;\n}\n"],"names":["initialize","ldClients","Map","getLDClient","clientSideID","context","options","client","get","set"],"mappings":"AAAA,SAAmBA,UAAU,QAAQ,6BAA6B;AAGlE,MAAMC,YAAmC,IAAIC;AAE7C,OAAO,SAASC,YAAY,EAAEC,YAAY,EAAEC,OAAO,EAAEC,OAAO,EAAgB;IACxE,IAAIC,SAASN,UAAUO,GAAG,CAACJ;IAC3B,IAAI,CAACG,QAAQ;QACTA,SAASP,WAAWI,cAAcC,SAASC;QAC3CL,UAAUQ,GAAG,CAACL,cAAcG;IAChC;IACA,OAAOA;AACX"}
@@ -1,6 +1,9 @@
1
+ import { LDContext } from 'launchdarkly-js-client-sdk';
1
2
  import { ProviderConfig as BaseProviderConfig } from 'launchdarkly-react-client-sdk';
2
3
  import { FC, PropsWithChildren } from 'react';
3
- export type ProviderConfig = Pick<BaseProviderConfig, 'clientSideID' | 'context'> & {
4
+ export type ProviderConfig = Pick<BaseProviderConfig, 'clientSideID'> & {
5
+ /** Pass `null` to defer client init until a real context is available. */
6
+ context: LDContext | null;
4
7
  projectName: string;
5
8
  waitForInitialization?: boolean;
6
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ld-provider.d.ts","sourceRoot":"","sources":["../src/ld-provider.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEH,cAAc,IAAI,kBAAkB,EACvC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAmC,MAAM,OAAO,CAAC;AAM/E,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,kBAAkB,EAAE,cAAc,GAAG,SAAS,CAAC,GAAG;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACnC,CAAC;AAaF,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAgD3D,CAAC"}
1
+ {"version":3,"file":"ld-provider.d.ts","sourceRoot":"","sources":["../src/ld-provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAEH,cAAc,IAAI,kBAAkB,EACvC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAmC,MAAM,OAAO,CAAC;AAM/E,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC,GAAG;IACpE,0EAA0E;IAC1E,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACnC,CAAC;AAaF,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,cAAc,CAAC,CA8D3D,CAAC"}
@@ -29,6 +29,11 @@ export const LDProvider = provide({
29
29
  const [ldService, log] = useOptionalDependencies(LD_SERVICE_TOKEN, Log);
30
30
  const [counter, forceUpdate] = useReducer((x)=>x + 1, 0);
31
31
  useEffect(()=>{
32
+ if (context === null) {
33
+ setLDClient(undefined);
34
+ return;
35
+ }
36
+ let cancelled = false;
32
37
  (async function initializeClient() {
33
38
  var _ref;
34
39
  const options = mergeOptions(ldService);
@@ -50,12 +55,19 @@ export const LDProvider = provide({
50
55
  });
51
56
  }
52
57
  }
53
- setLDClient(client);
58
+ if (!cancelled) {
59
+ setLDClient(client);
60
+ }
54
61
  })();
62
+ return ()=>{
63
+ cancelled = true;
64
+ };
65
+ // Presence only — avoid re-init when callers pass a new context object each render.
55
66
  // eslint-disable-next-line react-hooks/exhaustive-deps
56
67
  }, [
57
- clientSideID
58
- ]); // only reload if clientSideID changes
68
+ clientSideID,
69
+ context === null
70
+ ]);
59
71
  useEffect(()=>{
60
72
  if (!waitForInitialization && ldClient) {
61
73
  ldClient.on('initialized', forceUpdate);
@@ -67,7 +79,7 @@ export const LDProvider = provide({
67
79
  ldClient,
68
80
  waitForInitialization
69
81
  ]);
70
- if (!ldClient) {
82
+ if (context === null || !ldClient) {
71
83
  return null;
72
84
  }
73
85
  return /*#__PURE__*/ _jsx(BaseLDProvider, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ld-provider.tsx"],"sourcesContent":["import { Log } from '@servicetitan/log-service';\nimport { provide, useOptionalDependencies } from '@servicetitan/react-ioc';\nimport { LDClient } from 'launchdarkly-js-client-sdk';\nimport {\n LDProvider as BaseLDProvider,\n ProviderConfig as BaseProviderConfig,\n} from 'launchdarkly-react-client-sdk';\nimport { FC, PropsWithChildren, useEffect, useReducer, useState } from 'react';\nimport { FeatureFlagProvider } from './feature-flag-provider';\nimport { FeatureFlagStore } from './feature-flag-store';\nimport { getLDClient } from './get-ld-client';\nimport { LDService, LD_SERVICE_TOKEN } from './types';\n\nexport type ProviderConfig = Pick<BaseProviderConfig, 'clientSideID' | 'context'> & {\n projectName: string;\n waitForInitialization?: boolean;\n};\n\nfunction mergeOptions(ldService: LDService | undefined): BaseProviderConfig['options'] {\n const { application, options } = ldService ?? {};\n if (!options && application === undefined) {\n return undefined;\n }\n return {\n ...options,\n ...(application !== undefined ? { application } : {}),\n };\n}\n\nexport const LDProvider: FC<PropsWithChildren<ProviderConfig>> = provide({\n singletons: [FeatureFlagStore],\n})(props => {\n const { children, clientSideID, context, projectName, waitForInitialization = true } = props;\n const [ldClient, setLDClient] = useState<LDClient>();\n const [ldService, log] = useOptionalDependencies(LD_SERVICE_TOKEN, Log);\n const [counter, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n useEffect(() => {\n (async function initializeClient() {\n const options = mergeOptions(ldService);\n const config: BaseProviderConfig = { clientSideID, context, options };\n const client = ldService?.getClient(config) ?? getLDClient(config);\n if (waitForInitialization) {\n try {\n await client.waitForInitialization(5);\n } catch (error) {\n log?.error({\n category: 'LaunchDarkly',\n code: 'InitializationFailed',\n error,\n message: `${projectName} client failed to initialize`,\n });\n }\n }\n setLDClient(client);\n })();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [clientSideID]); // only reload if clientSideID changes\n\n useEffect(() => {\n if (!waitForInitialization && ldClient) {\n ldClient.on('initialized', forceUpdate);\n return () => {\n ldClient.off('initialized', forceUpdate);\n };\n }\n }, [ldClient, waitForInitialization]);\n\n if (!ldClient) {\n return null;\n }\n\n return (\n <BaseLDProvider key={counter.toString()} clientSideID={clientSideID} ldClient={ldClient}>\n <FeatureFlagProvider>{children}</FeatureFlagProvider>\n </BaseLDProvider>\n );\n});\n"],"names":["Log","provide","useOptionalDependencies","LDProvider","BaseLDProvider","useEffect","useReducer","useState","FeatureFlagProvider","FeatureFlagStore","getLDClient","LD_SERVICE_TOKEN","mergeOptions","ldService","application","options","undefined","singletons","props","children","clientSideID","context","projectName","waitForInitialization","ldClient","setLDClient","log","counter","forceUpdate","x","initializeClient","config","client","getClient","error","category","code","message","on","off","toString"],"mappings":";AAAA,SAASA,GAAG,QAAQ,4BAA4B;AAChD,SAASC,OAAO,EAAEC,uBAAuB,QAAQ,0BAA0B;AAE3E,SACIC,cAAcC,cAAc,QAEzB,gCAAgC;AACvC,SAAgCC,SAAS,EAAEC,UAAU,EAAEC,QAAQ,QAAQ,QAAQ;AAC/E,SAASC,mBAAmB,QAAQ,0BAA0B;AAC9D,SAASC,gBAAgB,QAAQ,uBAAuB;AACxD,SAASC,WAAW,QAAQ,kBAAkB;AAC9C,SAAoBC,gBAAgB,QAAQ,UAAU;AAOtD,SAASC,aAAaC,SAAgC;IAClD,MAAM,EAAEC,WAAW,EAAEC,OAAO,EAAE,GAAGF,sBAAAA,uBAAAA,YAAa,CAAC;IAC/C,IAAI,CAACE,WAAWD,gBAAgBE,WAAW;QACvC,OAAOA;IACX;IACA,OAAO;QACH,GAAGD,OAAO;QACV,GAAID,gBAAgBE,YAAY;YAAEF;QAAY,IAAI,CAAC,CAAC;IACxD;AACJ;AAEA,OAAO,MAAMX,aAAoDF,QAAQ;IACrEgB,YAAY;QAACR;KAAiB;AAClC,GAAGS,CAAAA;IACC,MAAM,EAAEC,QAAQ,EAAEC,YAAY,EAAEC,OAAO,EAAEC,WAAW,EAAEC,wBAAwB,IAAI,EAAE,GAAGL;IACvF,MAAM,CAACM,UAAUC,YAAY,GAAGlB;IAChC,MAAM,CAACM,WAAWa,IAAI,GAAGxB,wBAAwBS,kBAAkBX;IACnE,MAAM,CAAC2B,SAASC,YAAY,GAAGtB,WAAW,CAACuB,IAAcA,IAAI,GAAG;IAEhExB,UAAU;QACL,CAAA,eAAeyB;;YACZ,MAAMf,UAAUH,aAAaC;YAC7B,MAAMkB,SAA6B;gBAAEX;gBAAcC;gBAASN;YAAQ;YACpE,MAAMiB,iBAASnB,sBAAAA,gCAAAA,UAAWoB,SAAS,CAACF,8CAAWrB,YAAYqB;YAC3D,IAAIR,uBAAuB;gBACvB,IAAI;oBACA,MAAMS,OAAOT,qBAAqB,CAAC;gBACvC,EAAE,OAAOW,OAAO;oBACZR,gBAAAA,0BAAAA,IAAKQ,KAAK,CAAC;wBACPC,UAAU;wBACVC,MAAM;wBACNF;wBACAG,SAAS,GAAGf,YAAY,4BAA4B,CAAC;oBACzD;gBACJ;YACJ;YACAG,YAAYO;QAChB,CAAA;IACA,uDAAuD;IAC3D,GAAG;QAACZ;KAAa,GAAG,sCAAsC;IAE1Df,UAAU;QACN,IAAI,CAACkB,yBAAyBC,UAAU;YACpCA,SAASc,EAAE,CAAC,eAAeV;YAC3B,OAAO;gBACHJ,SAASe,GAAG,CAAC,eAAeX;YAChC;QACJ;IACJ,GAAG;QAACJ;QAAUD;KAAsB;IAEpC,IAAI,CAACC,UAAU;QACX,OAAO;IACX;IAEA,qBACI,KAACpB;QAAwCgB,cAAcA;QAAcI,UAAUA;kBAC3E,cAAA,KAAChB;sBAAqBW;;OADLQ,QAAQa,QAAQ;AAI7C,GAAG"}
1
+ {"version":3,"sources":["../src/ld-provider.tsx"],"sourcesContent":["import { Log } from '@servicetitan/log-service';\nimport { provide, useOptionalDependencies } from '@servicetitan/react-ioc';\nimport { LDClient, LDContext } from 'launchdarkly-js-client-sdk';\nimport {\n LDProvider as BaseLDProvider,\n ProviderConfig as BaseProviderConfig,\n} from 'launchdarkly-react-client-sdk';\nimport { FC, PropsWithChildren, useEffect, useReducer, useState } from 'react';\nimport { FeatureFlagProvider } from './feature-flag-provider';\nimport { FeatureFlagStore } from './feature-flag-store';\nimport { getLDClient } from './get-ld-client';\nimport { ClientConfig, LDService, LD_SERVICE_TOKEN } from './types';\n\nexport type ProviderConfig = Pick<BaseProviderConfig, 'clientSideID'> & {\n /** Pass `null` to defer client init until a real context is available. */\n context: LDContext | null;\n projectName: string;\n waitForInitialization?: boolean;\n};\n\nfunction mergeOptions(ldService: LDService | undefined): BaseProviderConfig['options'] {\n const { application, options } = ldService ?? {};\n if (!options && application === undefined) {\n return undefined;\n }\n return {\n ...options,\n ...(application !== undefined ? { application } : {}),\n };\n}\n\nexport const LDProvider: FC<PropsWithChildren<ProviderConfig>> = provide({\n singletons: [FeatureFlagStore],\n})(props => {\n const { children, clientSideID, context, projectName, waitForInitialization = true } = props;\n const [ldClient, setLDClient] = useState<LDClient>();\n const [ldService, log] = useOptionalDependencies(LD_SERVICE_TOKEN, Log);\n const [counter, forceUpdate] = useReducer((x: number) => x + 1, 0);\n\n useEffect(() => {\n if (context === null) {\n setLDClient(undefined);\n return;\n }\n\n let cancelled = false;\n\n (async function initializeClient() {\n const options = mergeOptions(ldService);\n const config: ClientConfig = { clientSideID, context, options };\n const client = ldService?.getClient(config) ?? getLDClient(config);\n if (waitForInitialization) {\n try {\n await client.waitForInitialization(5);\n } catch (error) {\n log?.error({\n category: 'LaunchDarkly',\n code: 'InitializationFailed',\n error,\n message: `${projectName} client failed to initialize`,\n });\n }\n }\n if (!cancelled) {\n setLDClient(client);\n }\n })();\n\n return () => {\n cancelled = true;\n };\n // Presence only — avoid re-init when callers pass a new context object each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [clientSideID, context === null]);\n\n useEffect(() => {\n if (!waitForInitialization && ldClient) {\n ldClient.on('initialized', forceUpdate);\n return () => {\n ldClient.off('initialized', forceUpdate);\n };\n }\n }, [ldClient, waitForInitialization]);\n\n if (context === null || !ldClient) {\n return null;\n }\n\n return (\n <BaseLDProvider key={counter.toString()} clientSideID={clientSideID} ldClient={ldClient}>\n <FeatureFlagProvider>{children}</FeatureFlagProvider>\n </BaseLDProvider>\n );\n});\n"],"names":["Log","provide","useOptionalDependencies","LDProvider","BaseLDProvider","useEffect","useReducer","useState","FeatureFlagProvider","FeatureFlagStore","getLDClient","LD_SERVICE_TOKEN","mergeOptions","ldService","application","options","undefined","singletons","props","children","clientSideID","context","projectName","waitForInitialization","ldClient","setLDClient","log","counter","forceUpdate","x","cancelled","initializeClient","config","client","getClient","error","category","code","message","on","off","toString"],"mappings":";AAAA,SAASA,GAAG,QAAQ,4BAA4B;AAChD,SAASC,OAAO,EAAEC,uBAAuB,QAAQ,0BAA0B;AAE3E,SACIC,cAAcC,cAAc,QAEzB,gCAAgC;AACvC,SAAgCC,SAAS,EAAEC,UAAU,EAAEC,QAAQ,QAAQ,QAAQ;AAC/E,SAASC,mBAAmB,QAAQ,0BAA0B;AAC9D,SAASC,gBAAgB,QAAQ,uBAAuB;AACxD,SAASC,WAAW,QAAQ,kBAAkB;AAC9C,SAAkCC,gBAAgB,QAAQ,UAAU;AASpE,SAASC,aAAaC,SAAgC;IAClD,MAAM,EAAEC,WAAW,EAAEC,OAAO,EAAE,GAAGF,sBAAAA,uBAAAA,YAAa,CAAC;IAC/C,IAAI,CAACE,WAAWD,gBAAgBE,WAAW;QACvC,OAAOA;IACX;IACA,OAAO;QACH,GAAGD,OAAO;QACV,GAAID,gBAAgBE,YAAY;YAAEF;QAAY,IAAI,CAAC,CAAC;IACxD;AACJ;AAEA,OAAO,MAAMX,aAAoDF,QAAQ;IACrEgB,YAAY;QAACR;KAAiB;AAClC,GAAGS,CAAAA;IACC,MAAM,EAAEC,QAAQ,EAAEC,YAAY,EAAEC,OAAO,EAAEC,WAAW,EAAEC,wBAAwB,IAAI,EAAE,GAAGL;IACvF,MAAM,CAACM,UAAUC,YAAY,GAAGlB;IAChC,MAAM,CAACM,WAAWa,IAAI,GAAGxB,wBAAwBS,kBAAkBX;IACnE,MAAM,CAAC2B,SAASC,YAAY,GAAGtB,WAAW,CAACuB,IAAcA,IAAI,GAAG;IAEhExB,UAAU;QACN,IAAIgB,YAAY,MAAM;YAClBI,YAAYT;YACZ;QACJ;QAEA,IAAIc,YAAY;QAEf,CAAA,eAAeC;;YACZ,MAAMhB,UAAUH,aAAaC;YAC7B,MAAMmB,SAAuB;gBAAEZ;gBAAcC;gBAASN;YAAQ;YAC9D,MAAMkB,iBAASpB,sBAAAA,gCAAAA,UAAWqB,SAAS,CAACF,8CAAWtB,YAAYsB;YAC3D,IAAIT,uBAAuB;gBACvB,IAAI;oBACA,MAAMU,OAAOV,qBAAqB,CAAC;gBACvC,EAAE,OAAOY,OAAO;oBACZT,gBAAAA,0BAAAA,IAAKS,KAAK,CAAC;wBACPC,UAAU;wBACVC,MAAM;wBACNF;wBACAG,SAAS,GAAGhB,YAAY,4BAA4B,CAAC;oBACzD;gBACJ;YACJ;YACA,IAAI,CAACQ,WAAW;gBACZL,YAAYQ;YAChB;QACJ,CAAA;QAEA,OAAO;YACHH,YAAY;QAChB;IACA,oFAAoF;IACpF,uDAAuD;IAC3D,GAAG;QAACV;QAAcC,YAAY;KAAK;IAEnChB,UAAU;QACN,IAAI,CAACkB,yBAAyBC,UAAU;YACpCA,SAASe,EAAE,CAAC,eAAeX;YAC3B,OAAO;gBACHJ,SAASgB,GAAG,CAAC,eAAeZ;YAChC;QACJ;IACJ,GAAG;QAACJ;QAAUD;KAAsB;IAEpC,IAAIF,YAAY,QAAQ,CAACG,UAAU;QAC/B,OAAO;IACX;IAEA,qBACI,KAACpB;QAAwCgB,cAAcA;QAAcI,UAAUA;kBAC3E,cAAA,KAAChB;sBAAqBW;;OADLQ,QAAQc,QAAQ;AAI7C,GAAG"}
@@ -71,6 +71,10 @@ describe('[launchdarkly-service] flag-used inspector integration', ()=>{
71
71
  children: /*#__PURE__*/ _jsx(LDProvider, {
72
72
  clientSideID: "test",
73
73
  projectName: "test",
74
+ context: {
75
+ kind: 'user',
76
+ key: 'test'
77
+ },
74
78
  children: /*#__PURE__*/ _jsx(Reader, {})
75
79
  })
76
80
  }));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tests/flag-used-inspectors.test.tsx"],"sourcesContent":["import { Provider, useDependencies } from '@servicetitan/react-ioc';\nimport '@testing-library/jest-dom';\nimport { render, screen } from '@testing-library/react';\nimport { LDClient, LDInspection } from 'launchdarkly-js-client-sdk';\nimport { observer } from 'mobx-react';\nimport { FC } from 'react';\nimport { FeatureFlagStore } from '../feature-flag-store';\nimport { LDProvider } from '../ld-provider';\nimport { LD_SERVICE_TOKEN, LDService } from '../types';\n\nconst FLAG_KEY = 'my-flag';\nconst CAMEL_KEY = 'myFlag';\n\nfunction createFakeClient(\n flagValues: Record<string, unknown>,\n inspectors: LDInspection[]\n): LDClient {\n return {\n allFlags: () => ({ ...flagValues }),\n variation: (key: string, defaultValue: unknown) => {\n const value = key in flagValues ? flagValues[key] : defaultValue;\n inspectors\n .filter(i => i.type === 'flag-used')\n .forEach(i => i.method!(key, { value }, { kind: 'user', key: 'test' }));\n return value;\n },\n waitForInitialization: () => Promise.resolve(),\n on: () => {},\n off: () => {},\n } as Partial<LDClient> as LDClient;\n}\n\nconst Reader: FC = observer(() => {\n const [{ flags }] = useDependencies(FeatureFlagStore);\n return <div data-testid=\"flag-value\">{String(flags[CAMEL_KEY])}</div>;\n});\n\ndescribe('[launchdarkly-service] flag-used inspector integration', () => {\n const flagValues: Record<string, unknown> = { [FLAG_KEY]: true };\n let inspectorMethod: jest.Mock;\n let ldService: LDService;\n\n beforeEach(() => {\n jest.clearAllMocks();\n\n inspectorMethod = jest.fn();\n const inspector: LDInspection = {\n type: 'flag-used',\n name: 'test-inspector',\n method: inspectorMethod,\n };\n\n ldService = {\n getClient: ({ options }) => createFakeClient(flagValues, options?.inspectors ?? []),\n options: { inspectors: [inspector] },\n };\n });\n\n const subject = () =>\n render(\n <Provider singletons={[{ provide: LD_SERVICE_TOKEN, useValue: ldService }]}>\n <LDProvider clientSideID=\"test\" projectName=\"test\">\n <Reader />\n </LDProvider>\n </Provider>\n );\n\n describe('when a flag is read through FeatureFlagStore', () => {\n beforeEach(async () => {\n subject();\n // Wait for <Reader> to mount and read flag\n await screen.findByTestId('flag-value');\n });\n\n test('fires the flag-used inspector', () => {\n expect(inspectorMethod).toHaveBeenCalledWith(\n FLAG_KEY,\n expect.objectContaining({ value: true }),\n expect.anything()\n );\n });\n });\n});\n"],"names":["Provider","useDependencies","render","screen","observer","FeatureFlagStore","LDProvider","LD_SERVICE_TOKEN","FLAG_KEY","CAMEL_KEY","createFakeClient","flagValues","inspectors","allFlags","variation","key","defaultValue","value","filter","i","type","forEach","method","kind","waitForInitialization","Promise","resolve","on","off","Reader","flags","div","data-testid","String","describe","inspectorMethod","ldService","beforeEach","jest","clearAllMocks","fn","inspector","name","getClient","options","subject","singletons","provide","useValue","clientSideID","projectName","findByTestId","test","expect","toHaveBeenCalledWith","objectContaining","anything"],"mappings":";AAAA,SAASA,QAAQ,EAAEC,eAAe,QAAQ,0BAA0B;AACpE,OAAO,4BAA4B;AACnC,SAASC,MAAM,EAAEC,MAAM,QAAQ,yBAAyB;AAExD,SAASC,QAAQ,QAAQ,aAAa;AAEtC,SAASC,gBAAgB,QAAQ,wBAAwB;AACzD,SAASC,UAAU,QAAQ,iBAAiB;AAC5C,SAASC,gBAAgB,QAAmB,WAAW;AAEvD,MAAMC,WAAW;AACjB,MAAMC,YAAY;AAElB,SAASC,iBACLC,UAAmC,EACnCC,UAA0B;IAE1B,OAAO;QACHC,UAAU,IAAO,CAAA;gBAAE,GAAGF,UAAU;YAAC,CAAA;QACjCG,WAAW,CAACC,KAAaC;YACrB,MAAMC,QAAQF,OAAOJ,aAAaA,UAAU,CAACI,IAAI,GAAGC;YACpDJ,WACKM,MAAM,CAACC,CAAAA,IAAKA,EAAEC,IAAI,KAAK,aACvBC,OAAO,CAACF,CAAAA,IAAKA,EAAEG,MAAM,CAAEP,KAAK;oBAAEE;gBAAM,GAAG;oBAAEM,MAAM;oBAAQR,KAAK;gBAAO;YACxE,OAAOE;QACX;QACAO,uBAAuB,IAAMC,QAAQC,OAAO;QAC5CC,IAAI,KAAO;QACXC,KAAK,KAAO;IAChB;AACJ;AAEA,MAAMC,SAAazB,SAAS;IACxB,MAAM,CAAC,EAAE0B,KAAK,EAAE,CAAC,GAAG7B,gBAAgBI;IACpC,qBAAO,KAAC0B;QAAIC,eAAY;kBAAcC,OAAOH,KAAK,CAACrB,UAAU;;AACjE;AAEAyB,SAAS,0DAA0D;IAC/D,MAAMvB,aAAsC;QAAE,CAACH,SAAS,EAAE;IAAK;IAC/D,IAAI2B;IACJ,IAAIC;IAEJC,WAAW;QACPC,KAAKC,aAAa;QAElBJ,kBAAkBG,KAAKE,EAAE;QACzB,MAAMC,YAA0B;YAC5BrB,MAAM;YACNsB,MAAM;YACNpB,QAAQa;QACZ;QAEAC,YAAY;YACRO,WAAW,CAAC,EAAEC,OAAO,EAAE;;uBAAKlC,iBAAiBC,oBAAYiC,oBAAAA,8BAAAA,QAAShC,UAAU,uCAAI,EAAE;;YAClFgC,SAAS;gBAAEhC,YAAY;oBAAC6B;iBAAU;YAAC;QACvC;IACJ;IAEA,MAAMI,UAAU,IACZ3C,qBACI,KAACF;YAAS8C,YAAY;gBAAC;oBAAEC,SAASxC;oBAAkByC,UAAUZ;gBAAU;aAAE;sBACtE,cAAA,KAAC9B;gBAAW2C,cAAa;gBAAOC,aAAY;0BACxC,cAAA,KAACrB;;;IAKjBK,SAAS,gDAAgD;QACrDG,WAAW;YACPQ;YACA,2CAA2C;YAC3C,MAAM1C,OAAOgD,YAAY,CAAC;QAC9B;QAEAC,KAAK,iCAAiC;YAClCC,OAAOlB,iBAAiBmB,oBAAoB,CACxC9C,UACA6C,OAAOE,gBAAgB,CAAC;gBAAEtC,OAAO;YAAK,IACtCoC,OAAOG,QAAQ;QAEvB;IACJ;AACJ"}
1
+ {"version":3,"sources":["../../src/tests/flag-used-inspectors.test.tsx"],"sourcesContent":["import { Provider, useDependencies } from '@servicetitan/react-ioc';\nimport '@testing-library/jest-dom';\nimport { render, screen } from '@testing-library/react';\nimport { LDClient, LDInspection } from 'launchdarkly-js-client-sdk';\nimport { observer } from 'mobx-react';\nimport { FC } from 'react';\nimport { FeatureFlagStore } from '../feature-flag-store';\nimport { LDProvider } from '../ld-provider';\nimport { LD_SERVICE_TOKEN, LDService } from '../types';\n\nconst FLAG_KEY = 'my-flag';\nconst CAMEL_KEY = 'myFlag';\n\nfunction createFakeClient(\n flagValues: Record<string, unknown>,\n inspectors: LDInspection[]\n): LDClient {\n return {\n allFlags: () => ({ ...flagValues }),\n variation: (key: string, defaultValue: unknown) => {\n const value = key in flagValues ? flagValues[key] : defaultValue;\n inspectors\n .filter(i => i.type === 'flag-used')\n .forEach(i => i.method!(key, { value }, { kind: 'user', key: 'test' }));\n return value;\n },\n waitForInitialization: () => Promise.resolve(),\n on: () => {},\n off: () => {},\n } as Partial<LDClient> as LDClient;\n}\n\nconst Reader: FC = observer(() => {\n const [{ flags }] = useDependencies(FeatureFlagStore);\n return <div data-testid=\"flag-value\">{String(flags[CAMEL_KEY])}</div>;\n});\n\ndescribe('[launchdarkly-service] flag-used inspector integration', () => {\n const flagValues: Record<string, unknown> = { [FLAG_KEY]: true };\n let inspectorMethod: jest.Mock;\n let ldService: LDService;\n\n beforeEach(() => {\n jest.clearAllMocks();\n\n inspectorMethod = jest.fn();\n const inspector: LDInspection = {\n type: 'flag-used',\n name: 'test-inspector',\n method: inspectorMethod,\n };\n\n ldService = {\n getClient: ({ options }) => createFakeClient(flagValues, options?.inspectors ?? []),\n options: { inspectors: [inspector] },\n };\n });\n\n const subject = () =>\n render(\n <Provider singletons={[{ provide: LD_SERVICE_TOKEN, useValue: ldService }]}>\n <LDProvider\n clientSideID=\"test\"\n projectName=\"test\"\n context={{ kind: 'user', key: 'test' }}\n >\n <Reader />\n </LDProvider>\n </Provider>\n );\n\n describe('when a flag is read through FeatureFlagStore', () => {\n beforeEach(async () => {\n subject();\n // Wait for <Reader> to mount and read flag\n await screen.findByTestId('flag-value');\n });\n\n test('fires the flag-used inspector', () => {\n expect(inspectorMethod).toHaveBeenCalledWith(\n FLAG_KEY,\n expect.objectContaining({ value: true }),\n expect.anything()\n );\n });\n });\n});\n"],"names":["Provider","useDependencies","render","screen","observer","FeatureFlagStore","LDProvider","LD_SERVICE_TOKEN","FLAG_KEY","CAMEL_KEY","createFakeClient","flagValues","inspectors","allFlags","variation","key","defaultValue","value","filter","i","type","forEach","method","kind","waitForInitialization","Promise","resolve","on","off","Reader","flags","div","data-testid","String","describe","inspectorMethod","ldService","beforeEach","jest","clearAllMocks","fn","inspector","name","getClient","options","subject","singletons","provide","useValue","clientSideID","projectName","context","findByTestId","test","expect","toHaveBeenCalledWith","objectContaining","anything"],"mappings":";AAAA,SAASA,QAAQ,EAAEC,eAAe,QAAQ,0BAA0B;AACpE,OAAO,4BAA4B;AACnC,SAASC,MAAM,EAAEC,MAAM,QAAQ,yBAAyB;AAExD,SAASC,QAAQ,QAAQ,aAAa;AAEtC,SAASC,gBAAgB,QAAQ,wBAAwB;AACzD,SAASC,UAAU,QAAQ,iBAAiB;AAC5C,SAASC,gBAAgB,QAAmB,WAAW;AAEvD,MAAMC,WAAW;AACjB,MAAMC,YAAY;AAElB,SAASC,iBACLC,UAAmC,EACnCC,UAA0B;IAE1B,OAAO;QACHC,UAAU,IAAO,CAAA;gBAAE,GAAGF,UAAU;YAAC,CAAA;QACjCG,WAAW,CAACC,KAAaC;YACrB,MAAMC,QAAQF,OAAOJ,aAAaA,UAAU,CAACI,IAAI,GAAGC;YACpDJ,WACKM,MAAM,CAACC,CAAAA,IAAKA,EAAEC,IAAI,KAAK,aACvBC,OAAO,CAACF,CAAAA,IAAKA,EAAEG,MAAM,CAAEP,KAAK;oBAAEE;gBAAM,GAAG;oBAAEM,MAAM;oBAAQR,KAAK;gBAAO;YACxE,OAAOE;QACX;QACAO,uBAAuB,IAAMC,QAAQC,OAAO;QAC5CC,IAAI,KAAO;QACXC,KAAK,KAAO;IAChB;AACJ;AAEA,MAAMC,SAAazB,SAAS;IACxB,MAAM,CAAC,EAAE0B,KAAK,EAAE,CAAC,GAAG7B,gBAAgBI;IACpC,qBAAO,KAAC0B;QAAIC,eAAY;kBAAcC,OAAOH,KAAK,CAACrB,UAAU;;AACjE;AAEAyB,SAAS,0DAA0D;IAC/D,MAAMvB,aAAsC;QAAE,CAACH,SAAS,EAAE;IAAK;IAC/D,IAAI2B;IACJ,IAAIC;IAEJC,WAAW;QACPC,KAAKC,aAAa;QAElBJ,kBAAkBG,KAAKE,EAAE;QACzB,MAAMC,YAA0B;YAC5BrB,MAAM;YACNsB,MAAM;YACNpB,QAAQa;QACZ;QAEAC,YAAY;YACRO,WAAW,CAAC,EAAEC,OAAO,EAAE;;uBAAKlC,iBAAiBC,oBAAYiC,oBAAAA,8BAAAA,QAAShC,UAAU,uCAAI,EAAE;;YAClFgC,SAAS;gBAAEhC,YAAY;oBAAC6B;iBAAU;YAAC;QACvC;IACJ;IAEA,MAAMI,UAAU,IACZ3C,qBACI,KAACF;YAAS8C,YAAY;gBAAC;oBAAEC,SAASxC;oBAAkByC,UAAUZ;gBAAU;aAAE;sBACtE,cAAA,KAAC9B;gBACG2C,cAAa;gBACbC,aAAY;gBACZC,SAAS;oBAAE5B,MAAM;oBAAQR,KAAK;gBAAO;0BAErC,cAAA,KAACc;;;IAKjBK,SAAS,gDAAgD;QACrDG,WAAW;YACPQ;YACA,2CAA2C;YAC3C,MAAM1C,OAAOiD,YAAY,CAAC;QAC9B;QAEAC,KAAK,iCAAiC;YAClCC,OAAOnB,iBAAiBoB,oBAAoB,CACxC/C,UACA8C,OAAOE,gBAAgB,CAAC;gBAAEvC,OAAO;YAAK,IACtCqC,OAAOG,QAAQ;QAEvB;IACJ;AACJ"}
@@ -11,35 +11,23 @@ describe(`[launchdarkly-service] ${getLDClient.name}`, ()=>{
11
11
  jest.clearAllMocks();
12
12
  jest.mocked(initialize).mockImplementation(()=>mockClient);
13
13
  config = {
14
- clientSideID: makeId()
14
+ clientSideID: makeId(),
15
+ context: {
16
+ kind: 'multi'
17
+ }
15
18
  };
16
19
  });
17
- function anonymousUserContext() {
18
- return {
19
- kind: 'user',
20
- anonymous: true
21
- };
22
- }
23
- test('returns new LaunchDarkly client with anonymous user context', ()=>{
20
+ test('initializes client with the given context', ()=>{
24
21
  expect(subject()).toBe(mockClient);
25
- expect(initialize).toHaveBeenCalledWith(config.clientSideID, anonymousUserContext(), undefined);
26
- });
27
- describe('when given a context', ()=>{
28
- beforeEach(()=>config.context = {
29
- kind: 'multi'
30
- });
31
- test('initializes client with context', ()=>{
32
- subject();
33
- expect(initialize).toHaveBeenCalledWith(config.clientSideID, config.context, undefined);
34
- });
22
+ expect(initialize).toHaveBeenCalledWith(config.clientSideID, config.context, config.options);
35
23
  });
36
24
  describe('when given options', ()=>{
37
25
  beforeEach(()=>config.options = {
38
26
  hash: 'foo'
39
27
  });
40
- test('initializes client with options', ()=>{
28
+ test('passes options through unchanged', ()=>{
41
29
  subject();
42
- expect(initialize).toHaveBeenCalledWith(config.clientSideID, anonymousUserContext(), config.options);
30
+ expect(initialize).toHaveBeenCalledWith(config.clientSideID, config.context, config.options);
43
31
  });
44
32
  });
45
33
  describe('when client already exists', ()=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tests/get-ld-client.test.ts"],"sourcesContent":["import { initialize } from 'launchdarkly-js-client-sdk';\nimport { getLDClient } from '../get-ld-client';\n\njest.mock('launchdarkly-js-client-sdk', () => ({\n initialize: jest.fn(),\n}));\n\ndescribe(`[launchdarkly-service] ${getLDClient.name}`, () => {\n const mockClient: any = Symbol('mockClient');\n let config: Parameters<typeof getLDClient>[0];\n\n const subject = () => getLDClient(config);\n\n beforeEach(() => {\n jest.clearAllMocks();\n jest.mocked(initialize).mockImplementation(() => mockClient);\n config = { clientSideID: makeId() };\n });\n\n function anonymousUserContext() {\n return { kind: 'user', anonymous: true };\n }\n\n test('returns new LaunchDarkly client with anonymous user context', () => {\n expect(subject()).toBe(mockClient);\n expect(initialize).toHaveBeenCalledWith(\n config.clientSideID,\n anonymousUserContext(),\n undefined\n );\n });\n\n describe('when given a context', () => {\n beforeEach(() => (config.context = { kind: 'multi' }));\n\n test('initializes client with context', () => {\n subject();\n\n expect(initialize).toHaveBeenCalledWith(config.clientSideID, config.context, undefined);\n });\n });\n\n describe('when given options', () => {\n beforeEach(() => (config.options = { hash: 'foo' }));\n\n test('initializes client with options', () => {\n subject();\n\n expect(initialize).toHaveBeenCalledWith(\n config.clientSideID,\n anonymousUserContext(),\n config.options\n );\n });\n });\n\n describe('when client already exists', () => {\n const existingClient: any = Symbol('existingClient');\n\n beforeEach(() => {\n jest.mocked(initialize).mockImplementationOnce(() => existingClient);\n subject();\n });\n\n test('returns existing client', () => {\n expect(subject()).toBe(existingClient);\n expect(initialize).toHaveBeenCalledTimes(1);\n });\n });\n});\n\nfunction makeId() {\n return Math.round(1e6 * Math.random()).toString(36);\n}\n"],"names":["initialize","getLDClient","jest","mock","fn","describe","name","mockClient","Symbol","config","subject","beforeEach","clearAllMocks","mocked","mockImplementation","clientSideID","makeId","anonymousUserContext","kind","anonymous","test","expect","toBe","toHaveBeenCalledWith","undefined","context","options","hash","existingClient","mockImplementationOnce","toHaveBeenCalledTimes","Math","round","random","toString"],"mappings":"AAAA,SAASA,UAAU,QAAQ,6BAA6B;AACxD,SAASC,WAAW,QAAQ,mBAAmB;AAE/CC,KAAKC,IAAI,CAAC,8BAA8B,IAAO,CAAA;QAC3CH,YAAYE,KAAKE,EAAE;IACvB,CAAA;AAEAC,SAAS,CAAC,uBAAuB,EAAEJ,YAAYK,IAAI,EAAE,EAAE;IACnD,MAAMC,aAAkBC,OAAO;IAC/B,IAAIC;IAEJ,MAAMC,UAAU,IAAMT,YAAYQ;IAElCE,WAAW;QACPT,KAAKU,aAAa;QAClBV,KAAKW,MAAM,CAACb,YAAYc,kBAAkB,CAAC,IAAMP;QACjDE,SAAS;YAAEM,cAAcC;QAAS;IACtC;IAEA,SAASC;QACL,OAAO;YAAEC,MAAM;YAAQC,WAAW;QAAK;IAC3C;IAEAC,KAAK,+DAA+D;QAChEC,OAAOX,WAAWY,IAAI,CAACf;QACvBc,OAAOrB,YAAYuB,oBAAoB,CACnCd,OAAOM,YAAY,EACnBE,wBACAO;IAER;IAEAnB,SAAS,wBAAwB;QAC7BM,WAAW,IAAOF,OAAOgB,OAAO,GAAG;gBAAEP,MAAM;YAAQ;QAEnDE,KAAK,mCAAmC;YACpCV;YAEAW,OAAOrB,YAAYuB,oBAAoB,CAACd,OAAOM,YAAY,EAAEN,OAAOgB,OAAO,EAAED;QACjF;IACJ;IAEAnB,SAAS,sBAAsB;QAC3BM,WAAW,IAAOF,OAAOiB,OAAO,GAAG;gBAAEC,MAAM;YAAM;QAEjDP,KAAK,mCAAmC;YACpCV;YAEAW,OAAOrB,YAAYuB,oBAAoB,CACnCd,OAAOM,YAAY,EACnBE,wBACAR,OAAOiB,OAAO;QAEtB;IACJ;IAEArB,SAAS,8BAA8B;QACnC,MAAMuB,iBAAsBpB,OAAO;QAEnCG,WAAW;YACPT,KAAKW,MAAM,CAACb,YAAY6B,sBAAsB,CAAC,IAAMD;YACrDlB;QACJ;QAEAU,KAAK,2BAA2B;YAC5BC,OAAOX,WAAWY,IAAI,CAACM;YACvBP,OAAOrB,YAAY8B,qBAAqB,CAAC;QAC7C;IACJ;AACJ;AAEA,SAASd;IACL,OAAOe,KAAKC,KAAK,CAAC,MAAMD,KAAKE,MAAM,IAAIC,QAAQ,CAAC;AACpD"}
1
+ {"version":3,"sources":["../../src/tests/get-ld-client.test.ts"],"sourcesContent":["import { initialize } from 'launchdarkly-js-client-sdk';\nimport { getLDClient } from '../get-ld-client';\n\njest.mock('launchdarkly-js-client-sdk', () => ({\n initialize: jest.fn(),\n}));\n\ndescribe(`[launchdarkly-service] ${getLDClient.name}`, () => {\n const mockClient: any = Symbol('mockClient');\n let config: Parameters<typeof getLDClient>[0];\n\n const subject = () => getLDClient(config);\n\n beforeEach(() => {\n jest.clearAllMocks();\n jest.mocked(initialize).mockImplementation(() => mockClient);\n config = { clientSideID: makeId(), context: { kind: 'multi' } };\n });\n\n test('initializes client with the given context', () => {\n expect(subject()).toBe(mockClient);\n expect(initialize).toHaveBeenCalledWith(\n config.clientSideID,\n config.context,\n config.options\n );\n });\n\n describe('when given options', () => {\n beforeEach(() => (config.options = { hash: 'foo' }));\n\n test('passes options through unchanged', () => {\n subject();\n\n expect(initialize).toHaveBeenCalledWith(\n config.clientSideID,\n config.context,\n config.options\n );\n });\n });\n\n describe('when client already exists', () => {\n const existingClient: any = Symbol('existingClient');\n\n beforeEach(() => {\n jest.mocked(initialize).mockImplementationOnce(() => existingClient);\n subject();\n });\n\n test('returns existing client', () => {\n expect(subject()).toBe(existingClient);\n expect(initialize).toHaveBeenCalledTimes(1);\n });\n });\n});\n\nfunction makeId() {\n return Math.round(1e6 * Math.random()).toString(36);\n}\n"],"names":["initialize","getLDClient","jest","mock","fn","describe","name","mockClient","Symbol","config","subject","beforeEach","clearAllMocks","mocked","mockImplementation","clientSideID","makeId","context","kind","test","expect","toBe","toHaveBeenCalledWith","options","hash","existingClient","mockImplementationOnce","toHaveBeenCalledTimes","Math","round","random","toString"],"mappings":"AAAA,SAASA,UAAU,QAAQ,6BAA6B;AACxD,SAASC,WAAW,QAAQ,mBAAmB;AAE/CC,KAAKC,IAAI,CAAC,8BAA8B,IAAO,CAAA;QAC3CH,YAAYE,KAAKE,EAAE;IACvB,CAAA;AAEAC,SAAS,CAAC,uBAAuB,EAAEJ,YAAYK,IAAI,EAAE,EAAE;IACnD,MAAMC,aAAkBC,OAAO;IAC/B,IAAIC;IAEJ,MAAMC,UAAU,IAAMT,YAAYQ;IAElCE,WAAW;QACPT,KAAKU,aAAa;QAClBV,KAAKW,MAAM,CAACb,YAAYc,kBAAkB,CAAC,IAAMP;QACjDE,SAAS;YAAEM,cAAcC;YAAUC,SAAS;gBAAEC,MAAM;YAAQ;QAAE;IAClE;IAEAC,KAAK,6CAA6C;QAC9CC,OAAOV,WAAWW,IAAI,CAACd;QACvBa,OAAOpB,YAAYsB,oBAAoB,CACnCb,OAAOM,YAAY,EACnBN,OAAOQ,OAAO,EACdR,OAAOc,OAAO;IAEtB;IAEAlB,SAAS,sBAAsB;QAC3BM,WAAW,IAAOF,OAAOc,OAAO,GAAG;gBAAEC,MAAM;YAAM;QAEjDL,KAAK,oCAAoC;YACrCT;YAEAU,OAAOpB,YAAYsB,oBAAoB,CACnCb,OAAOM,YAAY,EACnBN,OAAOQ,OAAO,EACdR,OAAOc,OAAO;QAEtB;IACJ;IAEAlB,SAAS,8BAA8B;QACnC,MAAMoB,iBAAsBjB,OAAO;QAEnCG,WAAW;YACPT,KAAKW,MAAM,CAACb,YAAY0B,sBAAsB,CAAC,IAAMD;YACrDf;QACJ;QAEAS,KAAK,2BAA2B;YAC5BC,OAAOV,WAAWW,IAAI,CAACI;YACvBL,OAAOpB,YAAY2B,qBAAqB,CAAC;QAC7C;IACJ;AACJ;AAEA,SAASX;IACL,OAAOY,KAAKC,KAAK,CAAC,MAAMD,KAAKE,MAAM,IAAIC,QAAQ,CAAC;AACpD"}
@@ -63,18 +63,21 @@ describe(`[launchdarkly-service] ${LDProvider.name}`, ()=>{
63
63
  singletons = [];
64
64
  props = {
65
65
  clientSideID: 'foo',
66
- projectName: 'bar'
66
+ projectName: 'bar',
67
+ context: {
68
+ kind: 'user',
69
+ key: 'user-1'
70
+ }
67
71
  };
68
72
  });
69
- const subject = ()=>{
70
- return render(/*#__PURE__*/ _jsx(Provider, {
73
+ const tree = ()=>/*#__PURE__*/ _jsx(Provider, {
71
74
  singletons: singletons,
72
75
  children: /*#__PURE__*/ _jsx(LDProvider, {
73
76
  ...props,
74
77
  children: /*#__PURE__*/ _jsx(Component, {})
75
78
  })
76
- }));
77
- };
79
+ });
80
+ const subject = ()=>render(tree());
78
81
  function expectLDProvider(props) {
79
82
  if (props) {
80
83
  expect(screen.getByText(`<LDProvider ${formatProps(props)} />`)).toBeInTheDocument();
@@ -153,6 +156,51 @@ describe(`[launchdarkly-service] ${LDProvider.name}`, ()=>{
153
156
  });
154
157
  });
155
158
  });
159
+ describe('when no context is provided', ()=>{
160
+ beforeEach(()=>{
161
+ props.context = null;
162
+ });
163
+ test('does not initialize client', ()=>{
164
+ subject();
165
+ expect(getLDClient).not.toHaveBeenCalled();
166
+ });
167
+ test('does not render children', ()=>{
168
+ subject();
169
+ expect(screen.queryByText('<Component />')).not.toBeInTheDocument();
170
+ });
171
+ describe('when context becomes available after mount', ()=>{
172
+ let rerender;
173
+ beforeEach(async ()=>{
174
+ rerender = subject().rerender;
175
+ props.context = {
176
+ kind: 'user',
177
+ key: 'user-1'
178
+ };
179
+ rerender(tree());
180
+ await waitFor(()=>expectLDProvider({
181
+ clientSideID: props.clientSideID,
182
+ ldClient
183
+ }));
184
+ });
185
+ test('initializes client', ()=>{
186
+ expect(getLDClient).toHaveBeenCalledWith(expect.objectContaining({
187
+ context: props.context
188
+ }));
189
+ });
190
+ test('renders children', ()=>{
191
+ expect(screen.queryByText('<Component />')).toBeInTheDocument();
192
+ });
193
+ describe('when context is removed', ()=>{
194
+ beforeEach(()=>{
195
+ props.context = null;
196
+ rerender(tree());
197
+ });
198
+ test('removes children', ()=>{
199
+ expect(screen.queryByText('<Component />')).not.toBeInTheDocument();
200
+ });
201
+ });
202
+ });
203
+ });
156
204
  describe('when LDService is provided', ()=>{
157
205
  const ldServiceClient = {
158
206
  waitUntilReady: jest.fn().mockResolvedValue(undefined),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tests/ld-provider.test.tsx"],"sourcesContent":["import { Log } from '@servicetitan/log-service';\nimport { Provider, useDependencies } from '@servicetitan/react-ioc';\nimport '@testing-library/jest-dom';\nimport { render, screen, waitFor } from '@testing-library/react';\nimport { LDClient } from 'launchdarkly-js-client-sdk';\nimport { act, FC, Fragment } from 'react';\nimport { FeatureFlagStore } from '../feature-flag-store';\nimport { getLDClient } from '../get-ld-client';\nimport { LDProvider } from '../ld-provider';\nimport { LD_SERVICE_TOKEN, LDService } from '../types';\n\nfunction formatProps(props: Record<string, any>) {\n return Object.entries(props).map(([key, value]) => {\n return `${key}=${value.toString()}`;\n });\n}\n\nfunction mockComponent(name: string) {\n return {\n [name]: ({ children, ...props }: any) => (\n <Fragment>\n <div>{`<${name} ${formatProps(props)} />`}</div>\n {children}\n </Fragment>\n ),\n };\n}\n\njest.mock('launchdarkly-react-client-sdk', () => ({\n ...mockComponent('LDProvider'),\n}));\njest.mock('../feature-flag-provider', () => ({\n ...mockComponent('FeatureFlagProvider'),\n}));\njest.mock('../get-ld-client', () => ({\n getLDClient: jest.fn(),\n}));\n\ndescribe(`[launchdarkly-service] ${LDProvider.name}`, () => {\n const log: Partial<Log> = {\n error: jest.fn(),\n };\n const ldClient: Partial<LDClient> & { toString: () => string } = {\n on: jest.fn(),\n off: jest.fn(),\n waitUntilReady: jest.fn().mockResolvedValue(undefined),\n waitForInitialization: jest.fn().mockResolvedValue(undefined),\n toString: () => 'ldClient',\n };\n let singletons: Required<Parameters<typeof Provider>[0]>['singletons'];\n let props: Parameters<typeof LDProvider>[0];\n let providedFeatureFlagStore: FeatureFlagStore;\n let renderCount: number;\n\n const Component: FC = () => {\n [providedFeatureFlagStore] = useDependencies(FeatureFlagStore);\n renderCount += 1;\n return <div>{'<Component />'}</div>;\n };\n\n beforeEach(() => {\n jest.clearAllMocks();\n jest.mocked(getLDClient).mockReturnValue(ldClient as LDClient);\n renderCount = 0;\n singletons = [];\n props = { clientSideID: 'foo', projectName: 'bar' };\n });\n\n const subject = () => {\n return render(\n <Provider singletons={singletons}>\n <LDProvider {...props}>\n <Component />\n </LDProvider>\n </Provider>\n );\n };\n\n function expectLDProvider(props?: Record<string, any>) {\n if (props) {\n expect(screen.getByText(`<LDProvider ${formatProps(props)} />`)).toBeInTheDocument();\n } else {\n expect(screen.getByText(/<LDProvider .* \\/>/)).toBeInTheDocument();\n }\n }\n\n test('renders base LDProvider and FeatureFlagProvider after client is ready and initialized', async () => {\n subject();\n\n await waitFor(() => expectLDProvider({ clientSideID: props.clientSideID, ldClient }));\n\n expect(screen.getByText('<FeatureFlagProvider />')).toBeInTheDocument();\n expect(screen.getByText('<Component />')).toBeInTheDocument();\n expect(ldClient.waitForInitialization).toHaveBeenCalled();\n });\n\n test('provides FeatureFlagStore', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(providedFeatureFlagStore).toBeDefined();\n });\n\n describe('when waitForInitialization is false', () => {\n let initializedCallback: Function;\n\n beforeEach(() => {\n props.waitForInitialization = false;\n jest.mocked(ldClient.on)?.mockImplementation((event, callback) => {\n if (event === 'initialized') {\n initializedCallback = callback;\n }\n });\n });\n\n test('does not wait for client to initialize', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(ldClient.waitForInitialization).not.toHaveBeenCalled();\n });\n\n test('re-renders when client is initialized', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n expect(renderCount).toBe(1);\n\n act(() => initializedCallback?.());\n expect(renderCount).toBe(2);\n });\n\n test('deregisters listener when component is unmounted', async () => {\n const { unmount } = subject();\n\n await waitFor(() => expectLDProvider());\n\n unmount();\n\n expect(jest.mocked(ldClient.off)).toHaveBeenCalledWith(\n 'initialized',\n initializedCallback\n );\n });\n });\n\n describe('when initialization fails', () => {\n beforeEach(() => {\n jest.mocked(ldClient.waitForInitialization)?.mockRejectedValue('Oops!');\n });\n\n test('renders base LDProvider', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n });\n\n describe('when log service is provided', () => {\n beforeEach(() => singletons.push({ provide: Log, useValue: log }));\n\n test('logs error', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(log.error).toHaveBeenCalledWith({\n category: 'LaunchDarkly',\n code: 'InitializationFailed',\n error: 'Oops!',\n message: `${props.projectName} client failed to initialize`,\n });\n });\n });\n });\n\n describe('when LDService is provided', () => {\n const ldServiceClient: Partial<LDClient> & { toString: () => string } = {\n waitUntilReady: jest.fn().mockResolvedValue(undefined),\n waitForInitialization: jest.fn().mockResolvedValue(undefined),\n toString: () => 'ldServiceClient',\n };\n let ldService: LDService;\n\n beforeEach(() => {\n ldService = { getClient: jest.fn().mockReturnValue(ldServiceClient) };\n singletons?.push({ provide: LD_SERVICE_TOKEN, useValue: ldService });\n });\n\n test('uses LDService client', async () => {\n subject();\n\n await waitFor(() =>\n expectLDProvider({ clientSideID: props.clientSideID, ldClient: ldServiceClient })\n );\n\n expect(ldServiceClient.waitForInitialization).toHaveBeenCalled();\n });\n\n describe('when LDService has options', () => {\n const options: LDService['options'] = { hash: 'abc123' };\n\n beforeEach(() => (ldService.options = options));\n\n test('forwards options to LDClient', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(ldService.getClient).toHaveBeenCalledWith(\n expect.objectContaining({ options })\n );\n });\n });\n\n describe('when application is provided', () => {\n const application = { id: 'some-application', version: '1.2.3' };\n\n beforeEach(() => (ldService.application = application));\n\n test('passes application to LDClient', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(ldService.getClient).toHaveBeenCalledWith(\n expect.objectContaining({\n options: { application },\n })\n );\n });\n });\n });\n});\n"],"names":["Log","Provider","useDependencies","render","screen","waitFor","act","Fragment","FeatureFlagStore","getLDClient","LDProvider","LD_SERVICE_TOKEN","formatProps","props","Object","entries","map","key","value","toString","mockComponent","name","children","div","jest","mock","fn","describe","log","error","ldClient","on","off","waitUntilReady","mockResolvedValue","undefined","waitForInitialization","singletons","providedFeatureFlagStore","renderCount","Component","beforeEach","clearAllMocks","mocked","mockReturnValue","clientSideID","projectName","subject","expectLDProvider","expect","getByText","toBeInTheDocument","test","toHaveBeenCalled","toBeDefined","initializedCallback","mockImplementation","event","callback","not","toBe","unmount","toHaveBeenCalledWith","mockRejectedValue","push","provide","useValue","category","code","message","ldServiceClient","ldService","getClient","options","hash","objectContaining","application","id","version"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tests/ld-provider.test.tsx"],"sourcesContent":["import { Log } from '@servicetitan/log-service';\nimport { Provider, useDependencies } from '@servicetitan/react-ioc';\nimport '@testing-library/jest-dom';\nimport { render, screen, waitFor } from '@testing-library/react';\nimport { LDClient } from 'launchdarkly-js-client-sdk';\nimport { act, FC, Fragment, ReactNode } from 'react';\nimport { FeatureFlagStore } from '../feature-flag-store';\nimport { getLDClient } from '../get-ld-client';\nimport { LDProvider } from '../ld-provider';\nimport { LD_SERVICE_TOKEN, LDService } from '../types';\n\nfunction formatProps(props: Record<string, any>) {\n return Object.entries(props).map(([key, value]) => {\n return `${key}=${value.toString()}`;\n });\n}\n\nfunction mockComponent(name: string) {\n return {\n [name]: ({ children, ...props }: any) => (\n <Fragment>\n <div>{`<${name} ${formatProps(props)} />`}</div>\n {children}\n </Fragment>\n ),\n };\n}\n\njest.mock('launchdarkly-react-client-sdk', () => ({\n ...mockComponent('LDProvider'),\n}));\njest.mock('../feature-flag-provider', () => ({\n ...mockComponent('FeatureFlagProvider'),\n}));\njest.mock('../get-ld-client', () => ({\n getLDClient: jest.fn(),\n}));\n\ndescribe(`[launchdarkly-service] ${LDProvider.name}`, () => {\n const log: Partial<Log> = {\n error: jest.fn(),\n };\n const ldClient: Partial<LDClient> & { toString: () => string } = {\n on: jest.fn(),\n off: jest.fn(),\n waitUntilReady: jest.fn().mockResolvedValue(undefined),\n waitForInitialization: jest.fn().mockResolvedValue(undefined),\n toString: () => 'ldClient',\n };\n let singletons: Required<Parameters<typeof Provider>[0]>['singletons'];\n let props: Parameters<typeof LDProvider>[0];\n let providedFeatureFlagStore: FeatureFlagStore;\n let renderCount: number;\n\n const Component: FC = () => {\n [providedFeatureFlagStore] = useDependencies(FeatureFlagStore);\n renderCount += 1;\n return <div>{'<Component />'}</div>;\n };\n\n beforeEach(() => {\n jest.clearAllMocks();\n jest.mocked(getLDClient).mockReturnValue(ldClient as LDClient);\n renderCount = 0;\n singletons = [];\n props = {\n clientSideID: 'foo',\n projectName: 'bar',\n context: { kind: 'user', key: 'user-1' },\n };\n });\n\n const tree = () => (\n <Provider singletons={singletons}>\n <LDProvider {...props}>\n <Component />\n </LDProvider>\n </Provider>\n );\n\n const subject = () => render(tree());\n\n function expectLDProvider(props?: Record<string, any>) {\n if (props) {\n expect(screen.getByText(`<LDProvider ${formatProps(props)} />`)).toBeInTheDocument();\n } else {\n expect(screen.getByText(/<LDProvider .* \\/>/)).toBeInTheDocument();\n }\n }\n\n test('renders base LDProvider and FeatureFlagProvider after client is ready and initialized', async () => {\n subject();\n\n await waitFor(() => expectLDProvider({ clientSideID: props.clientSideID, ldClient }));\n\n expect(screen.getByText('<FeatureFlagProvider />')).toBeInTheDocument();\n expect(screen.getByText('<Component />')).toBeInTheDocument();\n expect(ldClient.waitForInitialization).toHaveBeenCalled();\n });\n\n test('provides FeatureFlagStore', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(providedFeatureFlagStore).toBeDefined();\n });\n\n describe('when waitForInitialization is false', () => {\n let initializedCallback: Function;\n\n beforeEach(() => {\n props.waitForInitialization = false;\n jest.mocked(ldClient.on)?.mockImplementation((event, callback) => {\n if (event === 'initialized') {\n initializedCallback = callback;\n }\n });\n });\n\n test('does not wait for client to initialize', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(ldClient.waitForInitialization).not.toHaveBeenCalled();\n });\n\n test('re-renders when client is initialized', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n expect(renderCount).toBe(1);\n\n act(() => initializedCallback?.());\n expect(renderCount).toBe(2);\n });\n\n test('deregisters listener when component is unmounted', async () => {\n const { unmount } = subject();\n\n await waitFor(() => expectLDProvider());\n\n unmount();\n\n expect(jest.mocked(ldClient.off)).toHaveBeenCalledWith(\n 'initialized',\n initializedCallback\n );\n });\n });\n\n describe('when initialization fails', () => {\n beforeEach(() => {\n jest.mocked(ldClient.waitForInitialization)?.mockRejectedValue('Oops!');\n });\n\n test('renders base LDProvider', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n });\n\n describe('when log service is provided', () => {\n beforeEach(() => singletons.push({ provide: Log, useValue: log }));\n\n test('logs error', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(log.error).toHaveBeenCalledWith({\n category: 'LaunchDarkly',\n code: 'InitializationFailed',\n error: 'Oops!',\n message: `${props.projectName} client failed to initialize`,\n });\n });\n });\n });\n\n describe('when no context is provided', () => {\n beforeEach(() => {\n props.context = null;\n });\n\n test('does not initialize client', () => {\n subject();\n\n expect(getLDClient).not.toHaveBeenCalled();\n });\n\n test('does not render children', () => {\n subject();\n\n expect(screen.queryByText('<Component />')).not.toBeInTheDocument();\n });\n\n describe('when context becomes available after mount', () => {\n let rerender: (ui: ReactNode) => void;\n\n beforeEach(async () => {\n rerender = subject().rerender;\n props.context = { kind: 'user', key: 'user-1' };\n rerender(tree());\n\n await waitFor(() =>\n expectLDProvider({ clientSideID: props.clientSideID, ldClient })\n );\n });\n\n test('initializes client', () => {\n expect(getLDClient).toHaveBeenCalledWith(\n expect.objectContaining({ context: props.context })\n );\n });\n\n test('renders children', () => {\n expect(screen.queryByText('<Component />')).toBeInTheDocument();\n });\n\n describe('when context is removed', () => {\n beforeEach(() => {\n props.context = null;\n rerender(tree());\n });\n\n test('removes children', () => {\n expect(screen.queryByText('<Component />')).not.toBeInTheDocument();\n });\n });\n });\n });\n\n describe('when LDService is provided', () => {\n const ldServiceClient: Partial<LDClient> & { toString: () => string } = {\n waitUntilReady: jest.fn().mockResolvedValue(undefined),\n waitForInitialization: jest.fn().mockResolvedValue(undefined),\n toString: () => 'ldServiceClient',\n };\n let ldService: LDService;\n\n beforeEach(() => {\n ldService = { getClient: jest.fn().mockReturnValue(ldServiceClient) };\n singletons?.push({ provide: LD_SERVICE_TOKEN, useValue: ldService });\n });\n\n test('uses LDService client', async () => {\n subject();\n\n await waitFor(() =>\n expectLDProvider({ clientSideID: props.clientSideID, ldClient: ldServiceClient })\n );\n\n expect(ldServiceClient.waitForInitialization).toHaveBeenCalled();\n });\n\n describe('when LDService has options', () => {\n const options: LDService['options'] = { hash: 'abc123' };\n\n beforeEach(() => (ldService.options = options));\n\n test('forwards options to LDClient', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(ldService.getClient).toHaveBeenCalledWith(\n expect.objectContaining({ options })\n );\n });\n });\n\n describe('when application is provided', () => {\n const application = { id: 'some-application', version: '1.2.3' };\n\n beforeEach(() => (ldService.application = application));\n\n test('passes application to LDClient', async () => {\n subject();\n\n await waitFor(() => expectLDProvider());\n\n expect(ldService.getClient).toHaveBeenCalledWith(\n expect.objectContaining({\n options: { application },\n })\n );\n });\n });\n });\n});\n"],"names":["Log","Provider","useDependencies","render","screen","waitFor","act","Fragment","FeatureFlagStore","getLDClient","LDProvider","LD_SERVICE_TOKEN","formatProps","props","Object","entries","map","key","value","toString","mockComponent","name","children","div","jest","mock","fn","describe","log","error","ldClient","on","off","waitUntilReady","mockResolvedValue","undefined","waitForInitialization","singletons","providedFeatureFlagStore","renderCount","Component","beforeEach","clearAllMocks","mocked","mockReturnValue","clientSideID","projectName","context","kind","tree","subject","expectLDProvider","expect","getByText","toBeInTheDocument","test","toHaveBeenCalled","toBeDefined","initializedCallback","mockImplementation","event","callback","not","toBe","unmount","toHaveBeenCalledWith","mockRejectedValue","push","provide","useValue","category","code","message","queryByText","rerender","objectContaining","ldServiceClient","ldService","getClient","options","hash","application","id","version"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare const LD_SERVICE_TOKEN: import("@servicetitan/react-ioc").SymbolT
3
3
  export type LDApplication = LDOptions['application'];
4
4
  export interface ClientConfig {
5
5
  clientSideID: string;
6
- context?: LDContext;
6
+ context: LDContext;
7
7
  options?: LDOptions;
8
8
  }
9
9
  export interface LDService {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAEjF,eAAO,MAAM,gBAAgB,0DAA6C,CAAC;AAE3E,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC,CAAC;AAErD,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,QAAQ,CAAC;IAC9C,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;CAC5C;AAED,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,CAAC,GAAG;IAC5C,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;CAClB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAEjF,eAAO,MAAM,gBAAgB,0DAA6C,CAAC;AAE3E,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC,CAAC;AAErD,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,QAAQ,CAAC;IAC9C,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;CAC5C;AAED,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,CAAC,GAAG;IAC5C,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;CAClB,CAAC"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import { symbolToken } from '@servicetitan/react-ioc';\nimport type { LDClient, LDContext, LDOptions } from 'launchdarkly-js-client-sdk';\n\nexport const LD_SERVICE_TOKEN = symbolToken<LDService>('LD_SERVICE_TOKEN');\n\nexport type LDApplication = LDOptions['application'];\n\nexport interface ClientConfig {\n clientSideID: string;\n context?: LDContext;\n options?: LDOptions;\n}\n\nexport interface LDService {\n getClient: (config: ClientConfig) => LDClient;\n application?: LDApplication;\n options?: Omit<LDOptions, 'application'>;\n}\n\nexport type FlagKey<S, K extends keyof S> = K & {\n typeRef?: S[K];\n};\n"],"names":["symbolToken","LD_SERVICE_TOKEN"],"mappings":"AAAA,SAASA,WAAW,QAAQ,0BAA0B;AAGtD,OAAO,MAAMC,mBAAmBD,YAAuB,oBAAoB"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import { symbolToken } from '@servicetitan/react-ioc';\nimport type { LDClient, LDContext, LDOptions } from 'launchdarkly-js-client-sdk';\n\nexport const LD_SERVICE_TOKEN = symbolToken<LDService>('LD_SERVICE_TOKEN');\n\nexport type LDApplication = LDOptions['application'];\n\nexport interface ClientConfig {\n clientSideID: string;\n context: LDContext;\n options?: LDOptions;\n}\n\nexport interface LDService {\n getClient: (config: ClientConfig) => LDClient;\n application?: LDApplication;\n options?: Omit<LDOptions, 'application'>;\n}\n\nexport type FlagKey<S, K extends keyof S> = K & {\n typeRef?: S[K];\n};\n"],"names":["symbolToken","LD_SERVICE_TOKEN"],"mappings":"AAAA,SAASA,WAAW,QAAQ,0BAA0B;AAGtD,OAAO,MAAMC,mBAAmBD,YAAuB,oBAAoB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/launchdarkly-service",
3
- "version": "38.3.0",
3
+ "version": "39.0.0",
4
4
  "description": "Service for creating and reusing client connections for LaunchDarkly",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/uikit/launchdarkly-service",
6
6
  "repository": {
@@ -27,8 +27,8 @@
27
27
  ],
28
28
  "devDependencies": {
29
29
  "@datadog/browser-rum": "^7.5.0",
30
- "@servicetitan/log-service": "38.3.0",
31
- "@servicetitan/react-ioc": "38.3.0",
30
+ "@servicetitan/log-service": "39.0.0",
31
+ "@servicetitan/react-ioc": "39.0.0",
32
32
  "@testing-library/dom": "^10.4.1",
33
33
  "@testing-library/jest-dom": "^7.0.0",
34
34
  "@testing-library/react": "^16.3.2",
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@datadog/browser-rum": ">=6.19.0",
43
- "@servicetitan/log-service": "38.3.0",
44
- "@servicetitan/react-ioc": "38.3.0",
43
+ "@servicetitan/log-service": "39.0.0",
44
+ "@servicetitan/react-ioc": "39.0.0",
45
45
  "launchdarkly-js-client-sdk": "3.5.0",
46
46
  "launchdarkly-react-client-sdk": "3.6.1",
47
47
  "mobx": ">= 6.13.5",
@@ -59,5 +59,5 @@
59
59
  "cli": {
60
60
  "webpack": false
61
61
  },
62
- "gitHead": "64614330b773bffc08a39ff53f700e8be94796a7"
62
+ "gitHead": "9138e3fcda061941b89620b9004426f5c13a89ae"
63
63
  }
@@ -1,12 +1,12 @@
1
1
  import { LDClient, initialize } from 'launchdarkly-js-client-sdk';
2
- import { ProviderConfig } from 'launchdarkly-react-client-sdk';
2
+ import { ClientConfig } from './types';
3
3
 
4
4
  const ldClients: Map<string, LDClient> = new Map();
5
5
 
6
- export function getLDClient({ clientSideID, context, options }: ProviderConfig) {
6
+ export function getLDClient({ clientSideID, context, options }: ClientConfig): LDClient {
7
7
  let client = ldClients.get(clientSideID);
8
8
  if (!client) {
9
- client = initialize(clientSideID, context ?? { kind: 'user', anonymous: true }, options);
9
+ client = initialize(clientSideID, context, options);
10
10
  ldClients.set(clientSideID, client);
11
11
  }
12
12
  return client;
@@ -1,6 +1,6 @@
1
1
  import { Log } from '@servicetitan/log-service';
2
2
  import { provide, useOptionalDependencies } from '@servicetitan/react-ioc';
3
- import { LDClient } from 'launchdarkly-js-client-sdk';
3
+ import { LDClient, LDContext } from 'launchdarkly-js-client-sdk';
4
4
  import {
5
5
  LDProvider as BaseLDProvider,
6
6
  ProviderConfig as BaseProviderConfig,
@@ -9,9 +9,11 @@ import { FC, PropsWithChildren, useEffect, useReducer, useState } from 'react';
9
9
  import { FeatureFlagProvider } from './feature-flag-provider';
10
10
  import { FeatureFlagStore } from './feature-flag-store';
11
11
  import { getLDClient } from './get-ld-client';
12
- import { LDService, LD_SERVICE_TOKEN } from './types';
12
+ import { ClientConfig, LDService, LD_SERVICE_TOKEN } from './types';
13
13
 
14
- export type ProviderConfig = Pick<BaseProviderConfig, 'clientSideID' | 'context'> & {
14
+ export type ProviderConfig = Pick<BaseProviderConfig, 'clientSideID'> & {
15
+ /** Pass `null` to defer client init until a real context is available. */
16
+ context: LDContext | null;
15
17
  projectName: string;
16
18
  waitForInitialization?: boolean;
17
19
  };
@@ -36,9 +38,16 @@ export const LDProvider: FC<PropsWithChildren<ProviderConfig>> = provide({
36
38
  const [counter, forceUpdate] = useReducer((x: number) => x + 1, 0);
37
39
 
38
40
  useEffect(() => {
41
+ if (context === null) {
42
+ setLDClient(undefined);
43
+ return;
44
+ }
45
+
46
+ let cancelled = false;
47
+
39
48
  (async function initializeClient() {
40
49
  const options = mergeOptions(ldService);
41
- const config: BaseProviderConfig = { clientSideID, context, options };
50
+ const config: ClientConfig = { clientSideID, context, options };
42
51
  const client = ldService?.getClient(config) ?? getLDClient(config);
43
52
  if (waitForInitialization) {
44
53
  try {
@@ -52,10 +61,17 @@ export const LDProvider: FC<PropsWithChildren<ProviderConfig>> = provide({
52
61
  });
53
62
  }
54
63
  }
55
- setLDClient(client);
64
+ if (!cancelled) {
65
+ setLDClient(client);
66
+ }
56
67
  })();
68
+
69
+ return () => {
70
+ cancelled = true;
71
+ };
72
+ // Presence only — avoid re-init when callers pass a new context object each render.
57
73
  // eslint-disable-next-line react-hooks/exhaustive-deps
58
- }, [clientSideID]); // only reload if clientSideID changes
74
+ }, [clientSideID, context === null]);
59
75
 
60
76
  useEffect(() => {
61
77
  if (!waitForInitialization && ldClient) {
@@ -66,7 +82,7 @@ export const LDProvider: FC<PropsWithChildren<ProviderConfig>> = provide({
66
82
  }
67
83
  }, [ldClient, waitForInitialization]);
68
84
 
69
- if (!ldClient) {
85
+ if (context === null || !ldClient) {
70
86
  return null;
71
87
  }
72
88
 
@@ -59,7 +59,11 @@ describe('[launchdarkly-service] flag-used inspector integration', () => {
59
59
  const subject = () =>
60
60
  render(
61
61
  <Provider singletons={[{ provide: LD_SERVICE_TOKEN, useValue: ldService }]}>
62
- <LDProvider clientSideID="test" projectName="test">
62
+ <LDProvider
63
+ clientSideID="test"
64
+ projectName="test"
65
+ context={{ kind: 'user', key: 'test' }}
66
+ >
63
67
  <Reader />
64
68
  </LDProvider>
65
69
  </Provider>
@@ -14,41 +14,27 @@ describe(`[launchdarkly-service] ${getLDClient.name}`, () => {
14
14
  beforeEach(() => {
15
15
  jest.clearAllMocks();
16
16
  jest.mocked(initialize).mockImplementation(() => mockClient);
17
- config = { clientSideID: makeId() };
17
+ config = { clientSideID: makeId(), context: { kind: 'multi' } };
18
18
  });
19
19
 
20
- function anonymousUserContext() {
21
- return { kind: 'user', anonymous: true };
22
- }
23
-
24
- test('returns new LaunchDarkly client with anonymous user context', () => {
20
+ test('initializes client with the given context', () => {
25
21
  expect(subject()).toBe(mockClient);
26
22
  expect(initialize).toHaveBeenCalledWith(
27
23
  config.clientSideID,
28
- anonymousUserContext(),
29
- undefined
24
+ config.context,
25
+ config.options
30
26
  );
31
27
  });
32
28
 
33
- describe('when given a context', () => {
34
- beforeEach(() => (config.context = { kind: 'multi' }));
35
-
36
- test('initializes client with context', () => {
37
- subject();
38
-
39
- expect(initialize).toHaveBeenCalledWith(config.clientSideID, config.context, undefined);
40
- });
41
- });
42
-
43
29
  describe('when given options', () => {
44
30
  beforeEach(() => (config.options = { hash: 'foo' }));
45
31
 
46
- test('initializes client with options', () => {
32
+ test('passes options through unchanged', () => {
47
33
  subject();
48
34
 
49
35
  expect(initialize).toHaveBeenCalledWith(
50
36
  config.clientSideID,
51
- anonymousUserContext(),
37
+ config.context,
52
38
  config.options
53
39
  );
54
40
  });
@@ -3,7 +3,7 @@ import { Provider, useDependencies } from '@servicetitan/react-ioc';
3
3
  import '@testing-library/jest-dom';
4
4
  import { render, screen, waitFor } from '@testing-library/react';
5
5
  import { LDClient } from 'launchdarkly-js-client-sdk';
6
- import { act, FC, Fragment } from 'react';
6
+ import { act, FC, Fragment, ReactNode } from 'react';
7
7
  import { FeatureFlagStore } from '../feature-flag-store';
8
8
  import { getLDClient } from '../get-ld-client';
9
9
  import { LDProvider } from '../ld-provider';
@@ -63,18 +63,22 @@ describe(`[launchdarkly-service] ${LDProvider.name}`, () => {
63
63
  jest.mocked(getLDClient).mockReturnValue(ldClient as LDClient);
64
64
  renderCount = 0;
65
65
  singletons = [];
66
- props = { clientSideID: 'foo', projectName: 'bar' };
66
+ props = {
67
+ clientSideID: 'foo',
68
+ projectName: 'bar',
69
+ context: { kind: 'user', key: 'user-1' },
70
+ };
67
71
  });
68
72
 
69
- const subject = () => {
70
- return render(
71
- <Provider singletons={singletons}>
72
- <LDProvider {...props}>
73
- <Component />
74
- </LDProvider>
75
- </Provider>
76
- );
77
- };
73
+ const tree = () => (
74
+ <Provider singletons={singletons}>
75
+ <LDProvider {...props}>
76
+ <Component />
77
+ </LDProvider>
78
+ </Provider>
79
+ );
80
+
81
+ const subject = () => render(tree());
78
82
 
79
83
  function expectLDProvider(props?: Record<string, any>) {
80
84
  if (props) {
@@ -175,6 +179,59 @@ describe(`[launchdarkly-service] ${LDProvider.name}`, () => {
175
179
  });
176
180
  });
177
181
 
182
+ describe('when no context is provided', () => {
183
+ beforeEach(() => {
184
+ props.context = null;
185
+ });
186
+
187
+ test('does not initialize client', () => {
188
+ subject();
189
+
190
+ expect(getLDClient).not.toHaveBeenCalled();
191
+ });
192
+
193
+ test('does not render children', () => {
194
+ subject();
195
+
196
+ expect(screen.queryByText('<Component />')).not.toBeInTheDocument();
197
+ });
198
+
199
+ describe('when context becomes available after mount', () => {
200
+ let rerender: (ui: ReactNode) => void;
201
+
202
+ beforeEach(async () => {
203
+ rerender = subject().rerender;
204
+ props.context = { kind: 'user', key: 'user-1' };
205
+ rerender(tree());
206
+
207
+ await waitFor(() =>
208
+ expectLDProvider({ clientSideID: props.clientSideID, ldClient })
209
+ );
210
+ });
211
+
212
+ test('initializes client', () => {
213
+ expect(getLDClient).toHaveBeenCalledWith(
214
+ expect.objectContaining({ context: props.context })
215
+ );
216
+ });
217
+
218
+ test('renders children', () => {
219
+ expect(screen.queryByText('<Component />')).toBeInTheDocument();
220
+ });
221
+
222
+ describe('when context is removed', () => {
223
+ beforeEach(() => {
224
+ props.context = null;
225
+ rerender(tree());
226
+ });
227
+
228
+ test('removes children', () => {
229
+ expect(screen.queryByText('<Component />')).not.toBeInTheDocument();
230
+ });
231
+ });
232
+ });
233
+ });
234
+
178
235
  describe('when LDService is provided', () => {
179
236
  const ldServiceClient: Partial<LDClient> & { toString: () => string } = {
180
237
  waitUntilReady: jest.fn().mockResolvedValue(undefined),
package/src/types.ts CHANGED
@@ -7,7 +7,7 @@ export type LDApplication = LDOptions['application'];
7
7
 
8
8
  export interface ClientConfig {
9
9
  clientSideID: string;
10
- context?: LDContext;
10
+ context: LDContext;
11
11
  options?: LDOptions;
12
12
  }
13
13