@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/dist/client.d.ts CHANGED
@@ -65,6 +65,14 @@ interface TogglyConfig {
65
65
  localGates?: LocalGate[];
66
66
  /** Optional SDK error callback for reporting fetch/evaluation failures. */
67
67
  onError?: (message: string, error?: unknown) => void;
68
+ /**
69
+ * Use `/evaluated-variants-signed` and expose {@link TogglyContextValue.getVariant} /
70
+ * `getVariantValue` (client) or the equivalent {@link TogglyServerClient} methods
71
+ * (server). Only applies in `remote` evaluationMode client-side; the server
72
+ * package forces `remote` when this is set (mirrors the Nuxt/Astro OPS-825
73
+ * override — server otherwise defaults to `definitions-signed` + local eval).
74
+ */
75
+ enableVariants?: boolean;
68
76
  /** Host-minted browser capability; takes precedence over client identity targeting. */
69
77
  instanceId?: string;
70
78
  /** Initial browser identity; hydrated server identity takes precedence. */
@@ -144,6 +152,13 @@ interface IdentityContext {
144
152
  * Feature flags state (boolean or entity gate per flag)
145
153
  */
146
154
  type FeatureFlags = EvaluatedDefinitions;
155
+ /**
156
+ * Assigned variant for a feature (aligned with @ops-ai/feature-flags-toggly).
157
+ */
158
+ interface VariantResult {
159
+ name: string;
160
+ configurationValue?: unknown;
161
+ }
147
162
  /**
148
163
  * Feature evaluation result
149
164
  */
@@ -276,6 +291,17 @@ interface TogglyContextValue extends FrontendTelemetry {
276
291
  evaluateGate: (featureKeys: string[], requirement?: 'all' | 'any', negate?: boolean, entity?: TogglyEntityContext | Record<string, unknown> | null, kind?: string) => boolean;
277
292
  /** Register a domain-object mapper for entity-context evaluation */
278
293
  registerContext: <T>(kind: string, mapper: (entity: T) => TogglyEntityContext) => void;
294
+ /**
295
+ * Current variant assignment for a feature (requires `enableVariants` in the
296
+ * provider config). Soft-null when variants are disabled, unassigned, or the
297
+ * effective flag is off — never throws.
298
+ */
299
+ getVariant: (featureKey: string) => VariantResult | null;
300
+ /**
301
+ * Configuration payload for the assigned variant, if any.
302
+ * Optional `isT` type guard soft-fails to null on mismatch.
303
+ */
304
+ getVariantValue: <T = unknown>(featureKey: string, isT?: (v: unknown) => v is T) => T | null;
279
305
  /** Set user identity */
280
306
  identify: (identity: string, context?: IdentityContext) => Promise<void>;
281
307
  /** Clear user identity */
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { registerContext, normalizeEntityContext, serializeJsonForInlineScript, resolveEvaluatedDefinition, appendEvaluationContext } from '@ops-ai/toggly-hooks-types';
2
2
  export { normalizeEntityContext, registerContext } from '@ops-ai/toggly-hooks-types';
3
3
  import '@ops-ai/toggly-eval';
4
+ import { asVariantDefsRecord, unwrapDefsPayload } from '@ops-ai/toggly-signed-defs';
4
5
  import { createContext, useRef, useMemo, useState, useEffect, useCallback, useContext, useLayoutEffect } from 'react';
5
6
  import { createTelemetryReporter } from '@ops-ai/toggly-client-telemetry';
6
7
  import { attachBrowserLifecycle } from '@ops-ai/toggly-client-telemetry/browser';
@@ -51,12 +52,18 @@ function mergeConfig(config) {
51
52
  };
52
53
  }
53
54
  function buildDefinitionsUrl(config, context) {
54
- const { baseUrl, appKey, environment, groups, claims, evaluationMode } = mergeConfig(config);
55
+ const { baseUrl, appKey, environment, groups, claims, evaluationMode, enableVariants } = mergeConfig(config);
55
56
  if (!appKey) {
56
57
  throw new Error("appKey is required");
57
58
  }
58
59
  const mode = evaluationMode ?? "remote";
59
- const pathSegment = mode === "local" ? "definitions-signed" : "evaluated-signed";
60
+ const useVariants = mode !== "local" && enableVariants === true;
61
+ let pathSegment = "evaluated-signed";
62
+ if (mode === "local") {
63
+ pathSegment = "definitions-signed";
64
+ } else if (useVariants) {
65
+ pathSegment = "evaluated-variants-signed";
66
+ }
60
67
  const url = new URL(`${baseUrl}/${pathSegment}/${appKey}/${environment}`);
61
68
  if (mode === "local") {
62
69
  return url.toString();
@@ -69,10 +76,25 @@ function buildDefinitionsUrl(config, context) {
69
76
  groups: fromParam?.groups ?? groups,
70
77
  claims: fromParam?.claims ?? claims
71
78
  },
72
- "evaluated"
79
+ useVariants ? "variants" : "evaluated"
73
80
  );
74
81
  return url.toString();
75
82
  }
83
+ function parseVariantDefsPayload(parsed) {
84
+ return asVariantDefsRecord(unwrapDefsPayload(parsed));
85
+ }
86
+ function variantDefsToFlags(defs) {
87
+ const out = {};
88
+ for (const key of Object.keys(defs)) {
89
+ out[key] = defs[key]?.enabled === true;
90
+ }
91
+ return out;
92
+ }
93
+ function decodeVariantValue(value, isT) {
94
+ if (value === null || value === void 0) return null;
95
+ if (isT) return isT(value) ? value : null;
96
+ return value;
97
+ }
76
98
  function isFeatureEnabled(flags, featureKey, defaultValue = false, entityContext) {
77
99
  if (!flags || Object.keys(flags).length === 0) {
78
100
  return defaultValue;
@@ -137,6 +159,12 @@ var HEADERS = {
137
159
  CACHE_CONTROL: "cache-control"
138
160
  };
139
161
  var TOGGLY_LOADER_KEY = "__toggly";
162
+ function checkVariantLabel(result) {
163
+ if (typeof result === "string") {
164
+ return result;
165
+ }
166
+ return result ? "enabled" : "disabled";
167
+ }
140
168
  function createBrowserTelemetry(config) {
141
169
  const enabled = globalThis.window !== void 0 && typeof document !== "undefined" && !!config.appKey?.trim() && config.enableTelemetry !== false;
142
170
  const usage = enabled && config.enableUsageTracking !== false;
@@ -265,13 +293,13 @@ function createBrowserTelemetry(config) {
265
293
  };
266
294
  if (active) {
267
295
  const record = getReporter().captureCheck();
268
- return (key, result) => record(key, result ? "enabled" : "disabled");
296
+ return (key, result) => record(key, checkVariantLabel(result));
269
297
  }
270
298
  const attribution = context;
271
299
  const capturedPartition = partition;
272
- return (key, result) => feature(key, result ? "enabled" : "disabled", 0, attribution, capturedPartition);
300
+ return (key, result) => feature(key, checkVariantLabel(result), 0, attribution, capturedPartition);
273
301
  },
274
- recordCheck: (key, result) => feature(key, result ? "enabled" : "disabled", 0),
302
+ recordCheck: (key, result) => feature(key, checkVariantLabel(result), 0),
275
303
  activate() {
276
304
  if (disposed || active) return;
277
305
  active = true;
@@ -291,7 +319,7 @@ function createBrowserTelemetry(config) {
291
319
 
292
320
  // src/client/sdk-identity.ts
293
321
  var SDK_ID = "react-router";
294
- var SDK_VERSION = "1.1.0";
322
+ var SDK_VERSION = "1.2.1";
295
323
  function appendSdkQueryParams(params) {
296
324
  params.set("sdk", SDK_ID);
297
325
  params.set("sdkVersion", SDK_VERSION);
@@ -345,6 +373,7 @@ function TogglyProviderOwner({
345
373
  claims: { ...mergedConfig?.claims }
346
374
  });
347
375
  const flagsRef = useRef(serverContext?.flags ?? mergedConfig?.featureDefaults ?? {});
376
+ const variantsRef = useRef(null);
348
377
  const generationRef = useRef(0);
349
378
  const updateRevisionRef = useRef(0);
350
379
  const [telemetry] = useState(() => createBrowserTelemetry({ ...config, ...contextRef.current }));
@@ -482,6 +511,14 @@ function TogglyProviderOwner({
482
511
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
483
512
  }
484
513
  const payload = await response.json();
514
+ if (mergedConfig.enableVariants === true) {
515
+ const defs = parseVariantDefsPayload(payload);
516
+ variantsRef.current = defs;
517
+ const newFlags2 = variantDefsToFlags(defs);
518
+ logger.debug(`Fetched ${Object.keys(newFlags2).length} flags (variants).`);
519
+ return newFlags2;
520
+ }
521
+ variantsRef.current = null;
485
522
  const newFlags = payload && typeof payload === "object" ? payload : {};
486
523
  logger.debug(`Fetched ${Object.keys(newFlags).length} flags.`);
487
524
  return newFlags;
@@ -541,10 +578,34 @@ function TogglyProviderOwner({
541
578
  },
542
579
  [captureEvaluation]
543
580
  );
581
+ const getVariant = useCallback(
582
+ (featureKey) => {
583
+ if (!mergedConfig?.enableVariants) return null;
584
+ const entry = variantsRef.current?.[featureKey];
585
+ const enabled = applyLocalGate(
586
+ entry?.enabled === true,
587
+ featureKey,
588
+ localGatesRef.current,
589
+ localGateIndexRef.current
590
+ );
591
+ const record = telemetry.captureCheck();
592
+ record(featureKey, enabled && entry?.variant ? entry.variant : "disabled");
593
+ if (!enabled || !entry?.variant) return null;
594
+ return { name: entry.variant, configurationValue: entry.configurationValue };
595
+ },
596
+ [mergedConfig?.enableVariants, localGatesRevision, telemetry, flags]
597
+ );
598
+ const getVariantValue = useCallback(
599
+ (featureKey, isT) => {
600
+ return decodeVariantValue(getVariant(featureKey)?.configurationValue, isT);
601
+ },
602
+ [getVariant]
603
+ );
544
604
  const installContext = useCallback((target) => {
545
605
  contextRef.current = target;
546
606
  telemetry.setContext(target);
547
607
  flagsRef.current = mergedConfig?.featureDefaults ?? {};
608
+ variantsRef.current = null;
548
609
  setFlags(flagsRef.current);
549
610
  setIdentity(target.identity);
550
611
  }, [mergedConfig?.featureDefaults, telemetry]);
@@ -815,6 +876,8 @@ function TogglyProviderOwner({
815
876
  isDisabled,
816
877
  evaluateGate,
817
878
  registerContext: registerContext2,
879
+ getVariant,
880
+ getVariantValue,
818
881
  identify,
819
882
  reset,
820
883
  refresh,
@@ -833,6 +896,8 @@ function TogglyProviderOwner({
833
896
  isDisabled,
834
897
  evaluateGate,
835
898
  registerContext2,
899
+ getVariant,
900
+ getVariantValue,
836
901
  identify,
837
902
  reset,
838
903
  refresh,
@@ -994,27 +1059,19 @@ function RouterTogglyProvider({
994
1059
  config,
995
1060
  ...props
996
1061
  }) {
997
- let serverContext;
998
- try {
999
- if (routeId) {
1000
- const routeData = useRouteLoaderData(routeId);
1001
- serverContext = routeData?.[TOGGLY_LOADER_KEY];
1002
- } else {
1003
- const loaderData = useLoaderData();
1004
- serverContext = loaderData?.[TOGGLY_LOADER_KEY];
1005
- }
1006
- } catch {
1007
- serverContext = fallbackContext;
1008
- }
1009
- serverContext = serverContext ?? fallbackContext;
1010
- const mergedConfig = config ?? (serverContext ? {
1011
- appKey: serverContext.appKey,
1012
- environment: serverContext.environment
1062
+ const routeData = useRouteLoaderData(routeId ?? "");
1063
+ const loaderData = useLoaderData();
1064
+ const data = routeId ? routeData : loaderData;
1065
+ const serverContext = data?.[TOGGLY_LOADER_KEY];
1066
+ const resolvedServerContext = serverContext ?? fallbackContext;
1067
+ const mergedConfig = config ?? (resolvedServerContext ? {
1068
+ appKey: resolvedServerContext.appKey,
1069
+ environment: resolvedServerContext.environment
1013
1070
  } : void 0);
1014
1071
  return /* @__PURE__ */ jsx(
1015
1072
  TogglyProvider,
1016
1073
  {
1017
- serverContext,
1074
+ serverContext: resolvedServerContext,
1018
1075
  config: mergedConfig,
1019
1076
  ...props,
1020
1077
  children