@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(
|
|
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 ($[
|
|
171
|
-
|
|
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
|
-
|
|
190
|
+
onFinishedLoading(true);
|
|
174
191
|
return;
|
|
175
192
|
}
|
|
176
193
|
const controller = new AbortController();
|
|
177
194
|
const { signal } = controller;
|
|
178
|
-
|
|
195
|
+
requestToggles(distributionKey, resolvedEnvironment, toggleApiUrl, signal).then((toggles) => {
|
|
179
196
|
if (!signal.aborted) {
|
|
180
197
|
setTogglesFromCDN(toggles);
|
|
181
|
-
|
|
198
|
+
onFinishedLoading(true);
|
|
182
199
|
}
|
|
183
|
-
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
$[
|
|
202
|
-
$[
|
|
203
|
-
$[
|
|
204
|
-
$[
|
|
205
|
-
$[
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
$[
|
|
224
|
-
$[
|
|
225
|
-
$[
|
|
226
|
-
} else
|
|
227
|
-
const mergedToggles =
|
|
228
|
-
let
|
|
229
|
-
if ($[
|
|
230
|
-
|
|
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
|
-
$[
|
|
236
|
-
$[
|
|
237
|
-
$[
|
|
238
|
-
} else
|
|
239
|
-
let
|
|
240
|
-
if ($[
|
|
241
|
-
|
|
242
|
-
value:
|
|
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
|
-
$[
|
|
246
|
-
$[
|
|
247
|
-
$[
|
|
248
|
-
} else
|
|
249
|
-
return
|
|
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
|
-
|
|
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,10 @@
|
|
|
1
|
-
import React, { createContext, useEffect, useState } from 'react'
|
|
1
|
+
import React, { createContext, useEffect, useEffectEvent, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
267
|
+
onFinishedLoading(true)
|
|
255
268
|
return
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
const controller = new AbortController()
|
|
259
272
|
const { signal } = controller
|
|
260
273
|
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
282
|
+
onFinishedLoading(true)
|
|
267
283
|
}
|
|
268
|
-
}
|
|
269
|
-
|
|
284
|
+
},
|
|
285
|
+
(error: unknown) => {
|
|
270
286
|
// Don't handle abort errors - they're expected during cleanup
|
|
271
|
-
if (error
|
|
287
|
+
if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) return
|
|
272
288
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
+
}
|