@funnelsgrove/runtime 0.7.19 → 0.7.21

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.
@@ -118,10 +118,6 @@ type UseFunnelFlowControllerInput<StepId extends string> = {
118
118
  stepById: Record<string, FunnelStepMeta>;
119
119
  stepComponentById: StepComponentRegistry;
120
120
  funnelExperiments: readonly FunnelFlowControllerExperiment<StepId>[];
121
- sessionReplay?: {
122
- enabled: boolean;
123
- sampleRate: number;
124
- };
125
121
  getPathForStep: (stepId: StepId) => string;
126
122
  getStepIdFromPath: (pathname: string) => StepId | null;
127
123
  getSequentialNextStepId: (stepId: StepId) => StepId | null;
@@ -183,5 +179,5 @@ export declare function resolveRenderedExperimentStepId(input: {
183
179
  resolveRenderableStepId: (stepId: string | null | undefined) => string | null;
184
180
  safeActiveStepId: string;
185
181
  }): string;
186
- export declare function useFunnelFlowController<StepId extends string>({ api, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, sessionReplay, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
182
+ export declare function useFunnelFlowController<StepId extends string>({ api, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
187
183
  export {};
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { useFunnelRuntimeConfig } from '../components/FunnelRuntimeConfigBoundary.js';
3
4
  import { writeStepChoice } from '../sdk/userAnswers.js';
4
5
  import { apiService } from '../services/api.service.js';
5
6
  import { logger } from '../services/logger.js';
@@ -278,8 +279,9 @@ export function resolveRenderedExperimentStepId(input) {
278
279
  const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
279
280
  return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
280
281
  }
281
- export function useFunnelFlowController({ api = apiService, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, sessionReplay, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
282
- var _a, _b, _c;
282
+ export function useFunnelFlowController({ api = apiService, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
283
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
284
+ const publishedRuntime = useFunnelRuntimeConfig();
283
285
  const [runtimeMode] = useRuntimeMode();
284
286
  const [editorModeEnabled, setEditorModeEnabled] = useState(() => isEditorEnabled());
285
287
  const [editorPanelExpanded, setEditorPanelExpanded] = useState(false);
@@ -287,8 +289,8 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
287
289
  const [editorVariantOverrides, setEditorVariantOverrides] = useState({});
288
290
  const shouldLockToInitialStep = lockToInitialStep || isPreviewStepLockRequested();
289
291
  const isPreviewRuntime = useMemo(() => isPreviewFrameRuntime(), []);
290
- const sessionReplayEnabled = sessionReplay === null || sessionReplay === void 0 ? void 0 : sessionReplay.enabled;
291
- const sessionReplaySampleRate = sessionReplay === null || sessionReplay === void 0 ? void 0 : sessionReplay.sampleRate;
292
+ const sessionReplayEnabled = (_c = (_b = (_a = publishedRuntime.config) === null || _a === void 0 ? void 0 : _a.sessionReplay) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : false;
293
+ const sessionReplaySampleRate = (_f = (_e = (_d = publishedRuntime.config) === null || _d === void 0 ? void 0 : _d.sessionReplay) === null || _e === void 0 ? void 0 : _e.sampleRate) !== null && _f !== void 0 ? _f : 10;
292
294
  const resolveRenderableStepId = useCallback((stepId) => {
293
295
  if (!stepId || !isFunnelStepId(stepId)) {
294
296
  return null;
@@ -318,7 +320,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
318
320
  });
319
321
  return resolveFallbackStepId(requestedStepId);
320
322
  }, [initialStepId, resolveFallbackStepId, resolveRuntimeInitialStepId]);
321
- const isSubscriptionManagementRuntime = ((_a = stepById[safeInitialStepId]) === null || _a === void 0 ? void 0 : _a.kind) === 'manage-subscription';
323
+ const isSubscriptionManagementRuntime = ((_g = stepById[safeInitialStepId]) === null || _g === void 0 ? void 0 : _g.kind) === 'manage-subscription';
322
324
  const lifecycleEventsSuppressed = isPreviewRuntime || editorModeEnabled || isSubscriptionManagementRuntime;
323
325
  const [activeStepId, setActiveStepId] = useState(safeInitialStepId);
324
326
  const [attributes, setAttributes] = useState(() => (Object.assign({}, (initialAttributes || {}))));
@@ -542,12 +544,10 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
542
544
  distinctId: localUserId,
543
545
  projectId: PROJECT_ID,
544
546
  funnelId: FUNNEL_ID,
545
- sessionReplay: sessionReplayEnabled !== undefined && sessionReplaySampleRate !== undefined
546
- ? {
547
- enabled: sessionReplayEnabled,
548
- sampleRate: sessionReplaySampleRate,
549
- }
550
- : undefined,
547
+ sessionReplay: {
548
+ enabled: sessionReplayEnabled,
549
+ sampleRate: sessionReplaySampleRate,
550
+ },
551
551
  })
552
552
  .then(() => setPostHogReady(true))
553
553
  .catch((error) => {
@@ -1232,10 +1232,10 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
1232
1232
  safeInitialStepId,
1233
1233
  ]);
1234
1234
  const choiceTargets = getChoiceTargetsForStep(renderedStepId);
1235
- const actionBar = (_b = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.actionBar) !== null && _b !== void 0 ? _b : activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.actionBar;
1235
+ const actionBar = (_h = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.actionBar) !== null && _h !== void 0 ? _h : activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.actionBar;
1236
1236
  const isAutoAdvanceStep = typeof (actionBar === null || actionBar === void 0 ? void 0 : actionBar.autoAdvanceMs) === 'number';
1237
1237
  const autoAdvanceMs = actionBar === null || actionBar === void 0 ? void 0 : actionBar.autoAdvanceMs;
1238
- const buttonVariant = (_c = actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonVariant) !== null && _c !== void 0 ? _c : null;
1238
+ const buttonVariant = (_j = actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonVariant) !== null && _j !== void 0 ? _j : null;
1239
1239
  const buttonText = (actionBar === null || actionBar === void 0 ? void 0 : actionBar.buttonText) || 'Continue';
1240
1240
  const hideActionBar = (actionBar === null || actionBar === void 0 ? void 0 : actionBar.hidden) === true;
1241
1241
  useEffect(() => {
@@ -1,4 +1,7 @@
1
1
  const inflightByFunnelId = new Map();
2
+ const TRANSIENT_RUNTIME_CONFIG_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
3
+ const RUNTIME_CONFIG_RETRY_DELAYS_MS = [250, 750];
4
+ const wait = (delayMs) => (new Promise((resolve) => setTimeout(resolve, delayMs)));
2
5
  const isRecord = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value));
3
6
  const readString = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
4
7
  const readNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
@@ -297,19 +300,38 @@ export const loadFunnelRuntimeConfig = (funnelId, options = {}) => {
297
300
  }
298
301
  const fetcher = options.fetcher || globalThis.fetch;
299
302
  const basePath = (options.basePath || '/api/funnel-config').replace(/\/$/, '');
300
- const request = fetcher(`${basePath}/${encodeURIComponent(normalizedFunnelId)}?versionId=${encodeURIComponent(versionId)}`, {
301
- cache: 'no-store',
302
- headers: { accept: 'application/json', 'cache-control': 'no-cache' },
303
- }).then(async (response) => {
304
- if (!response.ok) {
305
- throw new Error(`Unable to load funnel runtime config (${response.status})`);
306
- }
307
- const published = parsePublishedRuntimeConfig(await response.json());
308
- if (published.config.funnelId !== normalizedFunnelId) {
309
- throw new Error('Funnel runtime config belongs to a different funnel');
303
+ const requestUrl = `${basePath}/${encodeURIComponent(normalizedFunnelId)}?versionId=${encodeURIComponent(versionId)}`;
304
+ const request = (async () => {
305
+ for (let attempt = 0;; attempt += 1) {
306
+ let response;
307
+ try {
308
+ response = await fetcher(requestUrl, {
309
+ cache: 'no-store',
310
+ headers: { accept: 'application/json', 'cache-control': 'no-cache' },
311
+ });
312
+ }
313
+ catch (error) {
314
+ const retryDelay = RUNTIME_CONFIG_RETRY_DELAYS_MS[attempt];
315
+ if (retryDelay === undefined)
316
+ throw error;
317
+ await wait(retryDelay);
318
+ continue;
319
+ }
320
+ if (!response.ok) {
321
+ const retryDelay = RUNTIME_CONFIG_RETRY_DELAYS_MS[attempt];
322
+ if (retryDelay !== undefined && TRANSIENT_RUNTIME_CONFIG_STATUSES.has(response.status)) {
323
+ await wait(retryDelay);
324
+ continue;
325
+ }
326
+ throw new Error(`Unable to load funnel runtime config (${response.status})`);
327
+ }
328
+ const published = parsePublishedRuntimeConfig(await response.json());
329
+ if (published.config.funnelId !== normalizedFunnelId) {
330
+ throw new Error('Funnel runtime config belongs to a different funnel');
331
+ }
332
+ return published;
310
333
  }
311
- return published;
312
- }).finally(() => {
334
+ })().finally(() => {
313
335
  inflightByFunnelId.delete(cacheKey);
314
336
  });
315
337
  inflightByFunnelId.set(cacheKey, request);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",