@patterninc/pattern-ui 0.0.0-main.202610080441.d12ae32dfdcb → 0.0.0-main.202610080442.f227f6fcab21

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,7 +1,7 @@
1
- import { detectEnvironment, fetchToggles } from "./fetchToggles.js";
1
+ import { detectEnvironment, fetchToggles, requestToggles } from "./fetchToggles.js";
2
2
  import { c } from "react/compiler-runtime";
3
3
  import { jsx } from "react/jsx-runtime";
4
- import React, { createContext, useEffect, useState } from "react";
4
+ import React, { createContext, useEffect, useEffectEvent, useState } from "react";
5
5
 
6
6
  //#region src/components/ToggleProvider/ToggleProvider.tsx
7
7
  const ToggleProviderContext = createContext({
@@ -118,7 +118,7 @@ function setToggleCookie(name, value) {
118
118
  * ```
119
119
  */
120
120
  const ToggleProvider = (t0) => {
121
- const $ = c(25);
121
+ const $ = c(33);
122
122
  const { children, distributionKey, environment, initialToggles, initialToggleOverrides, errorCallback, finishedLoadingCallback, toggleApiUrl: t1 } = t0;
123
123
  const toggleApiUrl = t1 === void 0 ? DEFAULT_TOGGLE_API_URL : t1;
124
124
  let t2;
@@ -166,87 +166,107 @@ const ToggleProvider = (t0) => {
166
166
  } else t6 = $[7];
167
167
  const setToggleOverride = t6;
168
168
  let t7;
169
+ if ($[8] !== finishedLoadingCallback) {
170
+ t7 = (isLoaded) => {
171
+ finishedLoadingCallback?.(isLoaded);
172
+ };
173
+ $[8] = finishedLoadingCallback;
174
+ $[9] = t7;
175
+ } else t7 = $[9];
176
+ const onFinishedLoading = useEffectEvent(t7);
169
177
  let t8;
170
- if ($[8] !== distributionKey || $[9] !== errorCallback || $[10] !== finishedLoadingCallback || $[11] !== initialToggles || $[12] !== resolvedEnvironment || $[13] !== toggleApiUrl) {
171
- t7 = () => {
178
+ if ($[10] !== errorCallback) {
179
+ t8 = (error) => {
180
+ errorCallback?.(error);
181
+ };
182
+ $[10] = errorCallback;
183
+ $[11] = t8;
184
+ } else t8 = $[11];
185
+ const onError = useEffectEvent(t8);
186
+ let t9;
187
+ if ($[12] !== distributionKey || $[13] !== initialToggles || $[14] !== onError || $[15] !== onFinishedLoading || $[16] !== resolvedEnvironment || $[17] !== toggleApiUrl) {
188
+ t9 = () => {
172
189
  if (initialToggles !== void 0) {
173
- finishedLoadingCallback?.(true);
190
+ onFinishedLoading(true);
174
191
  return;
175
192
  }
176
193
  const controller = new AbortController();
177
194
  const { signal } = controller;
178
- fetchToggles(distributionKey, resolvedEnvironment, toggleApiUrl, signal).then((toggles) => {
195
+ requestToggles(distributionKey, resolvedEnvironment, toggleApiUrl, signal).then((toggles) => {
179
196
  if (!signal.aborted) {
180
197
  setTogglesFromCDN(toggles);
181
- finishedLoadingCallback?.(true);
198
+ onFinishedLoading(true);
182
199
  }
183
- }).catch((error) => {
184
- if (error?.name === "AbortError") return;
185
- if (errorCallback) errorCallback(error);
186
- if (initialToggles === void 0) setTogglesFromCDN([]);
187
- if (!error?.message?.toLowerCase().includes("canceled")) finishedLoadingCallback?.(true);
200
+ }, (error_0) => {
201
+ if (signal.aborted || error_0 instanceof Error && error_0.name === "AbortError") return;
202
+ console.error("Error fetching toggles:", error_0);
203
+ onError(error_0 instanceof Error ? error_0.message : String(error_0));
204
+ setTogglesFromCDN([]);
205
+ onFinishedLoading(true);
188
206
  });
189
207
  return () => {
190
208
  controller.abort();
191
209
  };
192
210
  };
193
- t8 = [
211
+ $[12] = distributionKey;
212
+ $[13] = initialToggles;
213
+ $[14] = onError;
214
+ $[15] = onFinishedLoading;
215
+ $[16] = resolvedEnvironment;
216
+ $[17] = toggleApiUrl;
217
+ $[18] = t9;
218
+ } else t9 = $[18];
219
+ let t10;
220
+ if ($[19] !== distributionKey || $[20] !== initialToggles || $[21] !== resolvedEnvironment || $[22] !== toggleApiUrl) {
221
+ t10 = [
194
222
  distributionKey,
195
223
  resolvedEnvironment,
196
- errorCallback,
197
- finishedLoadingCallback,
198
224
  initialToggles,
199
225
  toggleApiUrl
200
226
  ];
201
- $[8] = distributionKey;
202
- $[9] = errorCallback;
203
- $[10] = finishedLoadingCallback;
204
- $[11] = initialToggles;
205
- $[12] = resolvedEnvironment;
206
- $[13] = toggleApiUrl;
207
- $[14] = t7;
208
- $[15] = t8;
209
- } else {
210
- t7 = $[14];
211
- t8 = $[15];
212
- }
213
- useEffect(t7, t8);
214
- let t9;
215
- if ($[16] !== toggleOverrides || $[17] !== togglesFromCDN) {
216
- t9 = togglesFromCDN?.map((toggle) => {
227
+ $[19] = distributionKey;
228
+ $[20] = initialToggles;
229
+ $[21] = resolvedEnvironment;
230
+ $[22] = toggleApiUrl;
231
+ $[23] = t10;
232
+ } else t10 = $[23];
233
+ useEffect(t9, t10);
234
+ let t11;
235
+ if ($[24] !== toggleOverrides || $[25] !== togglesFromCDN) {
236
+ t11 = togglesFromCDN?.map((toggle) => {
217
237
  const overrideValue = toggleOverrides[toggle.key];
218
238
  return {
219
239
  ...toggle,
220
240
  enabled: overrideValue !== void 0 ? overrideValue : toggle.enabled
221
241
  };
222
242
  });
223
- $[16] = toggleOverrides;
224
- $[17] = togglesFromCDN;
225
- $[18] = t9;
226
- } else t9 = $[18];
227
- const mergedToggles = t9;
228
- let t10;
229
- if ($[19] !== mergedToggles || $[20] !== togglesFromCDN) {
230
- t10 = {
243
+ $[24] = toggleOverrides;
244
+ $[25] = togglesFromCDN;
245
+ $[26] = t11;
246
+ } else t11 = $[26];
247
+ const mergedToggles = t11;
248
+ let t12;
249
+ if ($[27] !== mergedToggles || $[28] !== togglesFromCDN) {
250
+ t12 = {
231
251
  toggles: mergedToggles,
232
252
  originalToggles: togglesFromCDN,
233
253
  setToggleOverride
234
254
  };
235
- $[19] = mergedToggles;
236
- $[20] = togglesFromCDN;
237
- $[21] = t10;
238
- } else t10 = $[21];
239
- let t11;
240
- if ($[22] !== children || $[23] !== t10) {
241
- t11 = /* @__PURE__ */ jsx(ToggleProviderContext.Provider, {
242
- value: t10,
255
+ $[27] = mergedToggles;
256
+ $[28] = togglesFromCDN;
257
+ $[29] = t12;
258
+ } else t12 = $[29];
259
+ let t13;
260
+ if ($[30] !== children || $[31] !== t12) {
261
+ t13 = /* @__PURE__ */ jsx(ToggleProviderContext.Provider, {
262
+ value: t12,
243
263
  children
244
264
  });
245
- $[22] = children;
246
- $[23] = t10;
247
- $[24] = t11;
248
- } else t11 = $[24];
249
- return t11;
265
+ $[30] = children;
266
+ $[31] = t12;
267
+ $[32] = t13;
268
+ } else t13 = $[32];
269
+ return t13;
250
270
  };
251
271
 
252
272
  //#endregion
@@ -41,17 +41,24 @@ function detectEnvironment() {
41
41
  * @returns Promise resolving to an array of toggles
42
42
  */
43
43
  async function fetchToggles(distributionKey, environment, toggleApiUrl = DEFAULT_TOGGLE_API_URL, signal) {
44
- const resolvedEnvironment = environment || detectEnvironment();
45
44
  try {
46
- const response = await fetch(`${toggleApiUrl}/cdn/${distributionKey}/${resolvedEnvironment}`, { signal });
47
- if (!response.ok) throw new Error(`Failed to fetch toggles: ${response.status} ${response.statusText}`);
48
- return await response.json();
45
+ return await requestToggles(distributionKey, environment, toggleApiUrl, signal);
49
46
  } catch (error) {
50
47
  if (error instanceof Error && error.name === "AbortError") throw error;
51
48
  console.error("Error fetching toggles:", error);
52
49
  return [];
53
50
  }
54
51
  }
52
+ /**
53
+ * Same request as {@link fetchToggles}, but rejects on every failure instead of resolving to `[]`.
54
+ * Used by `ToggleProvider` so fetch failures reach its `errorCallback`.
55
+ */
56
+ async function requestToggles(distributionKey, environment, toggleApiUrl = DEFAULT_TOGGLE_API_URL, signal) {
57
+ const resolvedEnvironment = environment || detectEnvironment();
58
+ const response = await fetch(`${toggleApiUrl}/cdn/${distributionKey}/${resolvedEnvironment}`, { signal });
59
+ if (!response.ok) throw new Error(`Failed to fetch toggles: ${response.status} ${response.statusText}`);
60
+ return await response.json();
61
+ }
55
62
 
56
63
  //#endregion
57
- export { detectEnvironment, fetchToggles };
64
+ export { detectEnvironment, fetchToggles, requestToggles };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterninc/pattern-ui",
3
- "version": "0.0.0-main.202610080441.d12ae32dfdcb",
3
+ "version": "0.0.0-main.202610080442.f227f6fcab21",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/patterninc/pattern-exp.git",
@@ -1,6 +1,10 @@
1
- import React, { createContext, useEffect, useState } from 'react'
1
+ import React, { createContext, useEffect, useEffectEvent, useState } from 'react'
2
2
 
3
- import { detectEnvironment, fetchToggles as fetchTogglesFromCDN } from './fetchToggles'
3
+ import {
4
+ detectEnvironment,
5
+ fetchToggles as fetchTogglesFromCDN,
6
+ requestToggles,
7
+ } from './fetchToggles'
4
8
 
5
9
  /** Toggle interface representing a feature toggle from the API */
6
10
  export interface Toggle {
@@ -247,56 +251,53 @@ export const ToggleProvider = ({
247
251
  }))
248
252
  }
249
253
 
254
+ // Effect Events keep the latest callbacks without listing them as effect deps, so inline
255
+ // callbacks from consumers don't abort and refetch on every render.
256
+ const onFinishedLoading = useEffectEvent((isLoaded: boolean) => {
257
+ finishedLoadingCallback?.(isLoaded)
258
+ })
259
+ const onError = useEffectEvent((error: string | Record<string, unknown>) => {
260
+ errorCallback?.(error)
261
+ })
262
+
250
263
  // Server-provided initialToggles is the catalog for this render. Skip the client
251
264
  // CDN fetch so hydration stays stable. `undefined` means the server miss — fetch.
252
265
  useEffect(() => {
253
266
  if (initialToggles !== undefined) {
254
- finishedLoadingCallback?.(true)
267
+ onFinishedLoading(true)
255
268
  return
256
269
  }
257
270
 
258
271
  const controller = new AbortController()
259
272
  const { signal } = controller
260
273
 
261
- fetchTogglesFromCDN(distributionKey, resolvedEnvironment, toggleApiUrl, signal)
262
- .then((toggles) => {
274
+ // requestToggles (unlike the public fetchToggles) rejects on failure so errors reach onError.
275
+ // The rejection handler is passed to then() (not catch()) so a throw from the success path,
276
+ // e.g. a consumer's finishedLoadingCallback, isn't mistaken for a fetch failure.
277
+ requestToggles(distributionKey, resolvedEnvironment, toggleApiUrl, signal).then(
278
+ (toggles) => {
263
279
  // Only update state if not aborted
264
280
  if (!signal.aborted) {
265
281
  setTogglesFromCDN(toggles)
266
- finishedLoadingCallback?.(true)
282
+ onFinishedLoading(true)
267
283
  }
268
- })
269
- .catch((error) => {
284
+ },
285
+ (error: unknown) => {
270
286
  // Don't handle abort errors - they're expected during cleanup
271
- if (error?.name === 'AbortError') return
287
+ if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) return
272
288
 
273
- // Handle other errors
274
- if (errorCallback) {
275
- errorCallback(error)
276
- }
277
- // If client-side fetch fails but we have initialToggles from server, keep those
278
- // Otherwise set to empty array
279
- if (initialToggles === undefined) {
280
- setTogglesFromCDN([])
281
- }
282
-
283
- // If the query is cancelled, don't set finished loading to true. This fixes issues in strict mode
284
- if (!error?.message?.toLowerCase().includes('canceled')) {
285
- finishedLoadingCallback?.(true)
286
- }
287
- })
289
+ console.error('Error fetching toggles:', error)
290
+ onError(error instanceof Error ? error.message : String(error))
291
+ // initialToggles is undefined on this path (the effect returned early otherwise)
292
+ setTogglesFromCDN([])
293
+ onFinishedLoading(true)
294
+ },
295
+ )
288
296
 
289
297
  return () => {
290
298
  controller.abort()
291
299
  }
292
- }, [
293
- distributionKey,
294
- resolvedEnvironment,
295
- errorCallback,
296
- finishedLoadingCallback,
297
- initialToggles,
298
- toggleApiUrl,
299
- ])
300
+ }, [distributionKey, resolvedEnvironment, initialToggles, toggleApiUrl])
300
301
 
301
302
  // Merge backend toggles with local overrides
302
303
  const mergedToggles = togglesFromCDN?.map((toggle) => {
@@ -63,18 +63,8 @@ export async function fetchToggles(
63
63
  toggleApiUrl: string = DEFAULT_TOGGLE_API_URL,
64
64
  signal?: AbortSignal,
65
65
  ): Promise<Toggle[]> {
66
- const resolvedEnvironment = environment || detectEnvironment()
67
66
  try {
68
- const response = await fetch(`${toggleApiUrl}/cdn/${distributionKey}/${resolvedEnvironment}`, {
69
- signal,
70
- })
71
-
72
- if (!response.ok) {
73
- throw new Error(`Failed to fetch toggles: ${response.status} ${response.statusText}`)
74
- }
75
-
76
- const toggles = (await response.json()) as Toggle[]
77
- return toggles
67
+ return await requestToggles(distributionKey, environment, toggleApiUrl, signal)
78
68
  } catch (error) {
79
69
  // Don't log or return empty array for abort errors - let caller handle it
80
70
  if (error instanceof Error && error.name === 'AbortError') {
@@ -87,3 +77,25 @@ export async function fetchToggles(
87
77
  return []
88
78
  }
89
79
  }
80
+
81
+ /**
82
+ * Same request as {@link fetchToggles}, but rejects on every failure instead of resolving to `[]`.
83
+ * Used by `ToggleProvider` so fetch failures reach its `errorCallback`.
84
+ */
85
+ export async function requestToggles(
86
+ distributionKey: string,
87
+ environment?: ToggleEnvironment,
88
+ toggleApiUrl: string = DEFAULT_TOGGLE_API_URL,
89
+ signal?: AbortSignal,
90
+ ): Promise<Toggle[]> {
91
+ const resolvedEnvironment = environment || detectEnvironment()
92
+ const response = await fetch(`${toggleApiUrl}/cdn/${distributionKey}/${resolvedEnvironment}`, {
93
+ signal,
94
+ })
95
+
96
+ if (!response.ok) {
97
+ throw new Error(`Failed to fetch toggles: ${response.status} ${response.statusText}`)
98
+ }
99
+
100
+ return (await response.json()) as Toggle[]
101
+ }