@ops-ai/react-router-toggly 1.1.0 → 1.2.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.1] - 2026-09-27
4
+
5
+ ### Fixed
6
+ - Keep `RouterTogglyProvider` hook order stable when route selection changes while retaining loader and fallback context hydration [OPS-1451].
7
+ - Surface invalid React Router context errors to the application's Error Boundary instead of intercepting loader hooks [OPS-1451].
8
+
9
+ ## [1.2.0] - 2026-09-26
10
+
11
+ ### Added
12
+ - Opt-in `enableVariants` on `TogglyConfig`. When set, the browser client fetches
13
+ `/evaluated-variants-signed` instead of `/evaluated-signed`, and the server
14
+ client switches from `definitions-signed` local eval to remote variant
15
+ assignment. Exposes `getVariant` / `getVariantValue` on the provider context
16
+ and `TogglyServerClient` [OPS-1425].
17
+ - Typed soft-null `getVariantValue<T>(…, isT?)` and exported `decodeVariantValue`
18
+ matching the JS/Vue SDK contract.
3
19
 
4
20
  ## [1.1.0] - 2026-09-18
5
21
 
package/dist/client.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var togglyHooksTypes = require('@ops-ai/toggly-hooks-types');
4
4
  require('@ops-ai/toggly-eval');
5
+ var togglySignedDefs = require('@ops-ai/toggly-signed-defs');
5
6
  var react = require('react');
6
7
  var togglyClientTelemetry = require('@ops-ai/toggly-client-telemetry');
7
8
  var browser = require('@ops-ai/toggly-client-telemetry/browser');
@@ -52,12 +53,18 @@ function mergeConfig(config) {
52
53
  };
53
54
  }
54
55
  function buildDefinitionsUrl(config, context) {
55
- const { baseUrl, appKey, environment, groups, claims, evaluationMode } = mergeConfig(config);
56
+ const { baseUrl, appKey, environment, groups, claims, evaluationMode, enableVariants } = mergeConfig(config);
56
57
  if (!appKey) {
57
58
  throw new Error("appKey is required");
58
59
  }
59
60
  const mode = evaluationMode ?? "remote";
60
- const pathSegment = mode === "local" ? "definitions-signed" : "evaluated-signed";
61
+ const useVariants = mode !== "local" && enableVariants === true;
62
+ let pathSegment = "evaluated-signed";
63
+ if (mode === "local") {
64
+ pathSegment = "definitions-signed";
65
+ } else if (useVariants) {
66
+ pathSegment = "evaluated-variants-signed";
67
+ }
61
68
  const url = new URL(`${baseUrl}/${pathSegment}/${appKey}/${environment}`);
62
69
  if (mode === "local") {
63
70
  return url.toString();
@@ -70,10 +77,25 @@ function buildDefinitionsUrl(config, context) {
70
77
  groups: fromParam?.groups ?? groups,
71
78
  claims: fromParam?.claims ?? claims
72
79
  },
73
- "evaluated"
80
+ useVariants ? "variants" : "evaluated"
74
81
  );
75
82
  return url.toString();
76
83
  }
84
+ function parseVariantDefsPayload(parsed) {
85
+ return togglySignedDefs.asVariantDefsRecord(togglySignedDefs.unwrapDefsPayload(parsed));
86
+ }
87
+ function variantDefsToFlags(defs) {
88
+ const out = {};
89
+ for (const key of Object.keys(defs)) {
90
+ out[key] = defs[key]?.enabled === true;
91
+ }
92
+ return out;
93
+ }
94
+ function decodeVariantValue(value, isT) {
95
+ if (value === null || value === void 0) return null;
96
+ if (isT) return isT(value) ? value : null;
97
+ return value;
98
+ }
77
99
  function isFeatureEnabled(flags, featureKey, defaultValue = false, entityContext) {
78
100
  if (!flags || Object.keys(flags).length === 0) {
79
101
  return defaultValue;
@@ -138,6 +160,12 @@ var HEADERS = {
138
160
  CACHE_CONTROL: "cache-control"
139
161
  };
140
162
  var TOGGLY_LOADER_KEY = "__toggly";
163
+ function checkVariantLabel(result) {
164
+ if (typeof result === "string") {
165
+ return result;
166
+ }
167
+ return result ? "enabled" : "disabled";
168
+ }
141
169
  function createBrowserTelemetry(config) {
142
170
  const enabled = globalThis.window !== void 0 && typeof document !== "undefined" && !!config.appKey?.trim() && config.enableTelemetry !== false;
143
171
  const usage = enabled && config.enableUsageTracking !== false;
@@ -266,13 +294,13 @@ function createBrowserTelemetry(config) {
266
294
  };
267
295
  if (active) {
268
296
  const record = getReporter().captureCheck();
269
- return (key, result) => record(key, result ? "enabled" : "disabled");
297
+ return (key, result) => record(key, checkVariantLabel(result));
270
298
  }
271
299
  const attribution = context;
272
300
  const capturedPartition = partition;
273
- return (key, result) => feature(key, result ? "enabled" : "disabled", 0, attribution, capturedPartition);
301
+ return (key, result) => feature(key, checkVariantLabel(result), 0, attribution, capturedPartition);
274
302
  },
275
- recordCheck: (key, result) => feature(key, result ? "enabled" : "disabled", 0),
303
+ recordCheck: (key, result) => feature(key, checkVariantLabel(result), 0),
276
304
  activate() {
277
305
  if (disposed || active) return;
278
306
  active = true;
@@ -292,7 +320,7 @@ function createBrowserTelemetry(config) {
292
320
 
293
321
  // src/client/sdk-identity.ts
294
322
  var SDK_ID = "react-router";
295
- var SDK_VERSION = "1.1.0";
323
+ var SDK_VERSION = "1.2.1";
296
324
  function appendSdkQueryParams(params) {
297
325
  params.set("sdk", SDK_ID);
298
326
  params.set("sdkVersion", SDK_VERSION);
@@ -346,6 +374,7 @@ function TogglyProviderOwner({
346
374
  claims: { ...mergedConfig?.claims }
347
375
  });
348
376
  const flagsRef = react.useRef(serverContext?.flags ?? mergedConfig?.featureDefaults ?? {});
377
+ const variantsRef = react.useRef(null);
349
378
  const generationRef = react.useRef(0);
350
379
  const updateRevisionRef = react.useRef(0);
351
380
  const [telemetry] = react.useState(() => createBrowserTelemetry({ ...config, ...contextRef.current }));
@@ -483,6 +512,14 @@ function TogglyProviderOwner({
483
512
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
484
513
  }
485
514
  const payload = await response.json();
515
+ if (mergedConfig.enableVariants === true) {
516
+ const defs = parseVariantDefsPayload(payload);
517
+ variantsRef.current = defs;
518
+ const newFlags2 = variantDefsToFlags(defs);
519
+ logger.debug(`Fetched ${Object.keys(newFlags2).length} flags (variants).`);
520
+ return newFlags2;
521
+ }
522
+ variantsRef.current = null;
486
523
  const newFlags = payload && typeof payload === "object" ? payload : {};
487
524
  logger.debug(`Fetched ${Object.keys(newFlags).length} flags.`);
488
525
  return newFlags;
@@ -542,10 +579,34 @@ function TogglyProviderOwner({
542
579
  },
543
580
  [captureEvaluation]
544
581
  );
582
+ const getVariant = react.useCallback(
583
+ (featureKey) => {
584
+ if (!mergedConfig?.enableVariants) return null;
585
+ const entry = variantsRef.current?.[featureKey];
586
+ const enabled = togglyLocalGates.applyLocalGate(
587
+ entry?.enabled === true,
588
+ featureKey,
589
+ localGatesRef.current,
590
+ localGateIndexRef.current
591
+ );
592
+ const record = telemetry.captureCheck();
593
+ record(featureKey, enabled && entry?.variant ? entry.variant : "disabled");
594
+ if (!enabled || !entry?.variant) return null;
595
+ return { name: entry.variant, configurationValue: entry.configurationValue };
596
+ },
597
+ [mergedConfig?.enableVariants, localGatesRevision, telemetry, flags]
598
+ );
599
+ const getVariantValue = react.useCallback(
600
+ (featureKey, isT) => {
601
+ return decodeVariantValue(getVariant(featureKey)?.configurationValue, isT);
602
+ },
603
+ [getVariant]
604
+ );
545
605
  const installContext = react.useCallback((target) => {
546
606
  contextRef.current = target;
547
607
  telemetry.setContext(target);
548
608
  flagsRef.current = mergedConfig?.featureDefaults ?? {};
609
+ variantsRef.current = null;
549
610
  setFlags(flagsRef.current);
550
611
  setIdentity(target.identity);
551
612
  }, [mergedConfig?.featureDefaults, telemetry]);
@@ -816,6 +877,8 @@ function TogglyProviderOwner({
816
877
  isDisabled,
817
878
  evaluateGate,
818
879
  registerContext: registerContext2,
880
+ getVariant,
881
+ getVariantValue,
819
882
  identify,
820
883
  reset,
821
884
  refresh,
@@ -834,6 +897,8 @@ function TogglyProviderOwner({
834
897
  isDisabled,
835
898
  evaluateGate,
836
899
  registerContext2,
900
+ getVariant,
901
+ getVariantValue,
837
902
  identify,
838
903
  reset,
839
904
  refresh,
@@ -995,27 +1060,19 @@ function RouterTogglyProvider({
995
1060
  config,
996
1061
  ...props
997
1062
  }) {
998
- let serverContext;
999
- try {
1000
- if (routeId) {
1001
- const routeData = reactRouter.useRouteLoaderData(routeId);
1002
- serverContext = routeData?.[TOGGLY_LOADER_KEY];
1003
- } else {
1004
- const loaderData = reactRouter.useLoaderData();
1005
- serverContext = loaderData?.[TOGGLY_LOADER_KEY];
1006
- }
1007
- } catch {
1008
- serverContext = fallbackContext;
1009
- }
1010
- serverContext = serverContext ?? fallbackContext;
1011
- const mergedConfig = config ?? (serverContext ? {
1012
- appKey: serverContext.appKey,
1013
- environment: serverContext.environment
1063
+ const routeData = reactRouter.useRouteLoaderData(routeId ?? "");
1064
+ const loaderData = reactRouter.useLoaderData();
1065
+ const data = routeId ? routeData : loaderData;
1066
+ const serverContext = data?.[TOGGLY_LOADER_KEY];
1067
+ const resolvedServerContext = serverContext ?? fallbackContext;
1068
+ const mergedConfig = config ?? (resolvedServerContext ? {
1069
+ appKey: resolvedServerContext.appKey,
1070
+ environment: resolvedServerContext.environment
1014
1071
  } : void 0);
1015
1072
  return /* @__PURE__ */ jsxRuntime.jsx(
1016
1073
  TogglyProvider,
1017
1074
  {
1018
- serverContext,
1075
+ serverContext: resolvedServerContext,
1019
1076
  config: mergedConfig,
1020
1077
  ...props,
1021
1078
  children