@funnelsgrove/runtime 0.7.18 → 0.7.20

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.
@@ -19,7 +19,6 @@ export type GeneratedOfferSetPlanModeConfig = {
19
19
  export type GeneratedOfferSetPlanItem = {
20
20
  readonly funnelPlanKey: string;
21
21
  readonly runtimePlanKey?: string | null;
22
- readonly projectBillingPlanId?: string | null;
23
22
  readonly testProvider?: 'stripe' | 'solidgate' | null;
24
23
  readonly liveProvider?: 'stripe' | 'solidgate' | null;
25
24
  readonly testProviderPlanId?: string | null;
@@ -82,7 +81,6 @@ export type OfferSetRuntimePlanOverrides = {
82
81
  offerSetId?: string;
83
82
  offerSetKey?: string;
84
83
  funnelPlanKey?: string;
85
- projectPlanId?: string;
86
84
  providerPlanId?: string;
87
85
  paymentMode?: 'one_time';
88
86
  analyticsPurchaseValue?: number;
@@ -155,12 +155,11 @@ const readGeneratedPlanNumberValue = (item, mode, itemKeys, metadataKeys = itemK
155
155
  return ((_c = (_b = (_a = readFiniteNumberField(modeConfig, metadataKeys)) !== null && _a !== void 0 ? _a : readFiniteNumberField(fallbackModeConfig, metadataKeys)) !== null && _b !== void 0 ? _b : itemValue) !== null && _c !== void 0 ? _c : readFiniteNumberField(metadata, metadataKeys));
156
156
  };
157
157
  const buildGeneratedPlanOverrides = (item, mode, isDefault) => {
158
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
158
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
159
159
  const metadata = readOfferItemMetadata(item);
160
160
  const providerPlanId = readProviderPlanId(item, mode);
161
161
  const followUpProviderPlanId = readFollowUpProviderPlanId(item, mode);
162
- const projectPlanId = (_a = readString(item.projectBillingPlanId)) !== null && _a !== void 0 ? _a : readStringField(metadata, ['projectBillingPlanId', 'projectPlanId', 'project_billing_plan_id']);
163
- const title = (_c = (_b = readString(item.title)) !== null && _b !== void 0 ? _b : readString(item.displayNameOverride)) !== null && _c !== void 0 ? _c : readStringField(metadata, ['title', 'displayName', 'publicPlanName', 'public_plan_name']);
162
+ const title = (_b = (_a = readString(item.title)) !== null && _a !== void 0 ? _a : readString(item.displayNameOverride)) !== null && _b !== void 0 ? _b : readStringField(metadata, ['title', 'displayName', 'publicPlanName', 'public_plan_name']);
164
163
  const description = readGeneratedPlanStringValue(item, mode, ['description']);
165
164
  const priceDisplayFallbackMode = mode === 'live' ? 'test' : undefined;
166
165
  const readPriceString = (key) => (readGeneratedPlanStringValue(item, mode, [key], [String(key)], priceDisplayFallbackMode));
@@ -171,17 +170,17 @@ const buildGeneratedPlanOverrides = (item, mode, isDefault) => {
171
170
  const checkoutSummaryLabel = readPriceString('checkoutSummaryLabel');
172
171
  const billingInterval = readPriceString('billingInterval');
173
172
  const billingIntervalCount = readPriceNumber('billingIntervalCount');
174
- const followUpPlanTitle = (_e = (_d = readString(item.followUpPlanTitle)) !== null && _d !== void 0 ? _d : readString(item.followUpTitle)) !== null && _e !== void 0 ? _e : readStringField(metadata, ['followUpPlanTitle', 'followUpTitle', 'follow_up_plan_title']);
173
+ const followUpPlanTitle = (_d = (_c = readString(item.followUpPlanTitle)) !== null && _c !== void 0 ? _c : readString(item.followUpTitle)) !== null && _d !== void 0 ? _d : readStringField(metadata, ['followUpPlanTitle', 'followUpTitle', 'follow_up_plan_title']);
175
174
  const followUpBillingInterval = readGeneratedPlanStringValue(item, mode, ['followUpBillingInterval'], ['followUpBillingInterval', 'follow_up_billing_interval']);
176
175
  const followUpBillingIntervalCount = readGeneratedPlanNumberValue(item, mode, ['followUpBillingIntervalCount'], ['followUpBillingIntervalCount', 'follow_up_billing_interval_count']);
177
176
  const introIterations = readGeneratedPlanNumberValue(item, mode, ['introIterations'], ['introIterations', 'intro_iterations']);
178
177
  const amountCents = readPriceNumber('amountCents');
179
178
  const modeConfig = readGeneratedModeConfig(item, mode);
180
- const paymentMode = (_m = (_l = (_k = (_j = (_h = (_g = (_f = readOneTimePaymentMode(modeConfig.paymentMode)) !== null && _f !== void 0 ? _f : readOneTimePaymentMode(modeConfig.payment_mode)) !== null && _g !== void 0 ? _g : readOneTimePaymentMode(modeConfig.checkoutMode)) !== null && _h !== void 0 ? _h : readOneTimePaymentMode(modeConfig.checkout_mode)) !== null && _j !== void 0 ? _j : readOneTimePaymentMode(metadata.paymentMode)) !== null && _k !== void 0 ? _k : readOneTimePaymentMode(metadata.payment_mode)) !== null && _l !== void 0 ? _l : readOneTimePaymentMode(metadata.checkoutMode)) !== null && _m !== void 0 ? _m : readOneTimePaymentMode(metadata.checkout_mode);
181
- const analyticsPurchaseValue = (_o = readFiniteNumber(item.analyticsPurchaseValue)) !== null && _o !== void 0 ? _o : readFiniteNumberField(metadata, ['analyticsPurchaseValue']);
182
- const featuredTag = (_p = readString(item.featuredTag)) !== null && _p !== void 0 ? _p : readStringField(metadata, ['featuredTag']);
183
- const oldPriceLabel = (_q = readString(item.oldPriceLabel)) !== null && _q !== void 0 ? _q : readStringField(metadata, ['oldPriceLabel']);
184
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey: item.funnelPlanKey }, (projectPlanId ? { projectPlanId } : {})), (providerPlanId ? { providerPlanId } : {})), (paymentMode ? { paymentMode } : {})), (analyticsPurchaseValue !== null ? { analyticsPurchaseValue } : {})), (title ? { title } : {})), (description ? { description } : {})), (priceLabel ? { priceLabel } : {})), (perDayAmount ? { perDayAmount } : {})), (perDayLabel ? { perDayLabel } : {})), (amountCents !== null ? { amountCents: Math.max(0, Math.round(amountCents)) } : {})), (checkoutSummaryLabel ? { checkoutSummaryLabel } : {})), (billingInterval ? { billingInterval } : {})), (billingIntervalCount !== null ? { billingIntervalCount: Math.max(1, Math.round(billingIntervalCount)) } : {})), (followUpProviderPlanId ? { followUpProviderPlanId } : {})), (followUpPlanTitle ? { followUpPlanTitle } : {})), (followUpBillingInterval ? { followUpBillingInterval } : {})), (followUpBillingIntervalCount !== null ? { followUpBillingIntervalCount: Math.max(1, Math.round(followUpBillingIntervalCount)) } : {})), (introIterations !== null ? { introIterations: Math.max(1, Math.round(introIterations)) } : {})), (featuredTag ? { featuredTag } : {})), (oldPriceLabel ? { oldPriceLabel } : {})), (isDefault ? { isDefault: true } : {}));
179
+ const paymentMode = (_l = (_k = (_j = (_h = (_g = (_f = (_e = readOneTimePaymentMode(modeConfig.paymentMode)) !== null && _e !== void 0 ? _e : readOneTimePaymentMode(modeConfig.payment_mode)) !== null && _f !== void 0 ? _f : readOneTimePaymentMode(modeConfig.checkoutMode)) !== null && _g !== void 0 ? _g : readOneTimePaymentMode(modeConfig.checkout_mode)) !== null && _h !== void 0 ? _h : readOneTimePaymentMode(metadata.paymentMode)) !== null && _j !== void 0 ? _j : readOneTimePaymentMode(metadata.payment_mode)) !== null && _k !== void 0 ? _k : readOneTimePaymentMode(metadata.checkoutMode)) !== null && _l !== void 0 ? _l : readOneTimePaymentMode(metadata.checkout_mode);
180
+ const analyticsPurchaseValue = (_m = readFiniteNumber(item.analyticsPurchaseValue)) !== null && _m !== void 0 ? _m : readFiniteNumberField(metadata, ['analyticsPurchaseValue']);
181
+ const featuredTag = (_o = readString(item.featuredTag)) !== null && _o !== void 0 ? _o : readStringField(metadata, ['featuredTag']);
182
+ const oldPriceLabel = (_p = readString(item.oldPriceLabel)) !== null && _p !== void 0 ? _p : readStringField(metadata, ['oldPriceLabel']);
183
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey: item.funnelPlanKey }, (providerPlanId ? { providerPlanId } : {})), (paymentMode ? { paymentMode } : {})), (analyticsPurchaseValue !== null ? { analyticsPurchaseValue } : {})), (title ? { title } : {})), (description ? { description } : {})), (priceLabel ? { priceLabel } : {})), (perDayAmount ? { perDayAmount } : {})), (perDayLabel ? { perDayLabel } : {})), (amountCents !== null ? { amountCents: Math.max(0, Math.round(amountCents)) } : {})), (checkoutSummaryLabel ? { checkoutSummaryLabel } : {})), (billingInterval ? { billingInterval } : {})), (billingIntervalCount !== null ? { billingIntervalCount: Math.max(1, Math.round(billingIntervalCount)) } : {})), (followUpProviderPlanId ? { followUpProviderPlanId } : {})), (followUpPlanTitle ? { followUpPlanTitle } : {})), (followUpBillingInterval ? { followUpBillingInterval } : {})), (followUpBillingIntervalCount !== null ? { followUpBillingIntervalCount: Math.max(1, Math.round(followUpBillingIntervalCount)) } : {})), (introIterations !== null ? { introIterations: Math.max(1, Math.round(introIterations)) } : {})), (featuredTag ? { featuredTag } : {})), (oldPriceLabel ? { oldPriceLabel } : {})), (isDefault ? { isDefault: true } : {}));
185
184
  };
186
185
  export const applyGeneratedOfferSetToPlanCatalog = (baseCatalog, mode, offerSet) => {
187
186
  var _a, _b, _c, _d;
@@ -204,7 +203,9 @@ export const applyGeneratedOfferSetToPlanCatalog = (baseCatalog, mode, offerSet)
204
203
  if (!key) {
205
204
  continue;
206
205
  }
207
- nextCatalog[key] = Object.assign(Object.assign(Object.assign(Object.assign({}, ((_c = nextCatalog[key]) !== null && _c !== void 0 ? _c : {})), (((_d = offerSet.id) === null || _d === void 0 ? void 0 : _d.trim()) ? { offerSetId: offerSet.id.trim() } : {})), { offerSetKey: offerSet.key }), buildGeneratedPlanOverrides(item, mode, getOfferSetItemKind(item) === 'plan' && key === defaultPlanKey));
206
+ const basePlan = Object.assign({}, ((_c = nextCatalog[key]) !== null && _c !== void 0 ? _c : {}));
207
+ delete basePlan.projectPlanId;
208
+ nextCatalog[key] = Object.assign(Object.assign(Object.assign(Object.assign({}, basePlan), (((_d = offerSet.id) === null || _d === void 0 ? void 0 : _d.trim()) ? { offerSetId: offerSet.id.trim() } : {})), { offerSetKey: offerSet.key }), buildGeneratedPlanOverrides(item, mode, getOfferSetItemKind(item) === 'plan' && key === defaultPlanKey));
208
209
  }
209
210
  return nextCatalog;
210
211
  };
@@ -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);
@@ -112,7 +115,6 @@ const normalizePublishedOfferSets = (config) => {
112
115
  if (!funnelPlanKey) {
113
116
  throw new Error(`Published runtime offer set "${key}" has an item without a plan key`);
114
117
  }
115
- const projectBillingPlanId = pickString(item, 'projectBillingPlanId', 'project_billing_plan_id');
116
118
  const testProvider = parsePaymentProvider(pick(item, 'testProvider', 'test_provider'), `Published runtime offer set "${key}"`);
117
119
  const liveProvider = parsePaymentProvider(pick(item, 'liveProvider', 'live_provider'), `Published runtime offer set "${key}"`);
118
120
  const testMapping = buildPlanModeConfig(item, 'test');
@@ -121,9 +123,9 @@ const normalizePublishedOfferSets = (config) => {
121
123
  || pickString(metadata, 'title', 'publicPlanName', 'public_plan_name');
122
124
  const description = pickString(item, 'description')
123
125
  || pickString(metadata, 'description');
124
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey }, (pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey')
126
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey }, (pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey')
125
127
  ? { runtimePlanKey: pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey') }
126
- : {})), (projectBillingPlanId ? { projectBillingPlanId } : {})), (testProvider ? { testProvider } : {})), (liveProvider ? { liveProvider } : {})), (pickString(item, 'testProviderPlanId', 'test_provider_plan_id')
128
+ : {})), (testProvider ? { testProvider } : {})), (liveProvider ? { liveProvider } : {})), (pickString(item, 'testProviderPlanId', 'test_provider_plan_id')
127
129
  ? { testProviderPlanId: pickString(item, 'testProviderPlanId', 'test_provider_plan_id') }
128
130
  : {})), (pickString(item, 'liveProviderPlanId', 'live_provider_plan_id')
129
131
  ? { liveProviderPlanId: pickString(item, 'liveProviderPlanId', 'live_provider_plan_id') }
@@ -298,19 +300,38 @@ export const loadFunnelRuntimeConfig = (funnelId, options = {}) => {
298
300
  }
299
301
  const fetcher = options.fetcher || globalThis.fetch;
300
302
  const basePath = (options.basePath || '/api/funnel-config').replace(/\/$/, '');
301
- const request = fetcher(`${basePath}/${encodeURIComponent(normalizedFunnelId)}?versionId=${encodeURIComponent(versionId)}`, {
302
- cache: 'no-store',
303
- headers: { accept: 'application/json', 'cache-control': 'no-cache' },
304
- }).then(async (response) => {
305
- if (!response.ok) {
306
- throw new Error(`Unable to load funnel runtime config (${response.status})`);
307
- }
308
- const published = parsePublishedRuntimeConfig(await response.json());
309
- if (published.config.funnelId !== normalizedFunnelId) {
310
- 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;
311
333
  }
312
- return published;
313
- }).finally(() => {
334
+ })().finally(() => {
314
335
  inflightByFunnelId.delete(cacheKey);
315
336
  });
316
337
  inflightByFunnelId.set(cacheKey, request);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",