@terreno/rtk 0.19.0 → 0.20.0
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/README.md +6 -0
- package/dist/emptyApi.d.ts +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/openFeatureReactSdkSpike.test.d.ts +2 -0
- package/dist/openFeatureReactSdkSpike.test.d.ts.map +1 -0
- package/dist/openFeatureReactSdkSpike.test.js +29 -0
- package/dist/openFeatureReactSdkSpike.test.js.map +1 -0
- package/dist/useFeatureFlags.d.ts +15 -15
- package/dist/useFeatureFlags.d.ts.map +1 -1
- package/dist/useFeatureFlags.js +65 -78
- package/dist/useFeatureFlags.js.map +1 -1
- package/dist/useFeatureFlags.test.js +119 -53
- package/dist/useFeatureFlags.test.js.map +1 -1
- package/dist/useTerrenoFeatureFlags.d.ts +39 -0
- package/dist/useTerrenoFeatureFlags.d.ts.map +1 -0
- package/dist/useTerrenoFeatureFlags.js +157 -0
- package/dist/useTerrenoFeatureFlags.js.map +1 -0
- package/dist/useTerrenoFeatureFlags.test.d.ts +2 -0
- package/dist/useTerrenoFeatureFlags.test.d.ts.map +1 -0
- package/dist/useTerrenoFeatureFlags.test.js +160 -0
- package/dist/useTerrenoFeatureFlags.test.js.map +1 -0
- package/package.json +7 -2
- package/src/index.ts +1 -0
- package/src/openFeatureReactSdkSpike.test.ts +36 -0
- package/src/useFeatureFlags.test.ts +143 -57
- package/src/useFeatureFlags.ts +91 -95
- package/src/useTerrenoFeatureFlags.test.ts +222 -0
- package/src/useTerrenoFeatureFlags.ts +226 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {NOOP_PROVIDER, OpenFeature, TypedInMemoryProvider} from "@openfeature/web-sdk";
|
|
2
|
+
import type {Api} from "@reduxjs/toolkit/query/react";
|
|
3
|
+
import {useCallback, useEffect, useMemo, useState} from "react";
|
|
4
|
+
|
|
5
|
+
/** Shape returned by `GET .../flagConfiguration` (OpenFeature static flag map). */
|
|
6
|
+
export interface TerrenoFlagDefinition {
|
|
7
|
+
defaultVariant: string;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
variants: Record<string, boolean | string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TerrenoFlagConfiguration = Record<string, TerrenoFlagDefinition>;
|
|
13
|
+
|
|
14
|
+
export interface UseTerrenoFeatureFlagsOptions {
|
|
15
|
+
basePath?: string;
|
|
16
|
+
skip?: boolean;
|
|
17
|
+
/** Current user id — included in RTK cache key and OpenFeature targeting context. */
|
|
18
|
+
userId?: string | null;
|
|
19
|
+
/** Optional socket.io client for live flag refresh. */
|
|
20
|
+
socket?: {
|
|
21
|
+
off: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
22
|
+
on: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
23
|
+
} | null;
|
|
24
|
+
socketEventName?: string;
|
|
25
|
+
/** OpenFeature domain — must match `<OpenFeatureProvider domain>` on the client. */
|
|
26
|
+
domain?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseTerrenoFeatureFlagsResult {
|
|
30
|
+
client: ReturnType<typeof OpenFeature.getClient>;
|
|
31
|
+
error: unknown;
|
|
32
|
+
flags: TerrenoFlagConfiguration;
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
refetch: () => unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: RTK Query API generic typing is intentionally flexible here.
|
|
38
|
+
type FlagsApi = Api<any, any, any, any>;
|
|
39
|
+
|
|
40
|
+
// biome-ignore lint/suspicious/noExplicitAny: Endpoint hook is injected dynamically by RTK Query.
|
|
41
|
+
type EnhancedTerrenoFlagsApi = FlagsApi & {useTerrenoFlagConfigurationQuery: any};
|
|
42
|
+
|
|
43
|
+
const enhancedApiCache = new WeakMap<FlagsApi, Map<string, EnhancedTerrenoFlagsApi>>();
|
|
44
|
+
|
|
45
|
+
const getEnhancedApi = (api: FlagsApi, basePath: string): EnhancedTerrenoFlagsApi => {
|
|
46
|
+
let byBase = enhancedApiCache.get(api);
|
|
47
|
+
if (!byBase) {
|
|
48
|
+
byBase = new Map();
|
|
49
|
+
enhancedApiCache.set(api, byBase);
|
|
50
|
+
}
|
|
51
|
+
const cached = byBase.get(basePath);
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
const enhanced = api.injectEndpoints({
|
|
56
|
+
endpoints: (builder) => ({
|
|
57
|
+
terrenoFlagConfiguration: builder.query<TerrenoFlagConfiguration, {cacheKey: string}>({
|
|
58
|
+
providesTags: (_result, _err, arg) => [{id: arg.cacheKey, type: "feature-flags"}],
|
|
59
|
+
query: () => ({
|
|
60
|
+
method: "GET",
|
|
61
|
+
url: `${basePath}/flagConfiguration`,
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
overrideExisting: false,
|
|
66
|
+
}) as EnhancedTerrenoFlagsApi;
|
|
67
|
+
byBase.set(basePath, enhanced);
|
|
68
|
+
return enhanced;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** In-flight `useTerrenoFeatureFlags` instances per OpenFeature domain (for last-unmount NOOP cleanup). */
|
|
72
|
+
const terrenoOpenFeatureHookRefCounts = new Map<string, number>();
|
|
73
|
+
|
|
74
|
+
/** Last in-flight `setProviderAndWait` per OpenFeature domain (shared across hook instances). */
|
|
75
|
+
const domainProviderSwitchPromises = new Map<string, Promise<void>>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wires Terreno's bulk `/flagConfiguration` fetch into OpenFeature's
|
|
79
|
+
* {@link TypedInMemoryProvider} for the given domain. Prefer OpenFeature React
|
|
80
|
+
* hooks (`useBooleanFlagValue`, etc.) as children of `<OpenFeatureProvider>`.
|
|
81
|
+
*/
|
|
82
|
+
export const useTerrenoFeatureFlags = (
|
|
83
|
+
api: FlagsApi,
|
|
84
|
+
options?: UseTerrenoFeatureFlagsOptions
|
|
85
|
+
): UseTerrenoFeatureFlagsResult => {
|
|
86
|
+
const basePath = options?.basePath ?? "/feature-flags";
|
|
87
|
+
const domain = options?.domain ?? "feature-flags";
|
|
88
|
+
const userId = options?.userId ?? null;
|
|
89
|
+
const skip = options?.skip === true;
|
|
90
|
+
const socket = options?.socket ?? null;
|
|
91
|
+
const socketEventName = options?.socketEventName ?? "featureFlagsChanged";
|
|
92
|
+
|
|
93
|
+
const enhancedApi = useMemo(() => getEnhancedApi(api, basePath), [api, basePath]);
|
|
94
|
+
const useTerrenoFlagConfigurationQuery = enhancedApi.useTerrenoFlagConfigurationQuery;
|
|
95
|
+
const {
|
|
96
|
+
data,
|
|
97
|
+
error,
|
|
98
|
+
isError,
|
|
99
|
+
isFetching,
|
|
100
|
+
isLoading: isQueryLoading,
|
|
101
|
+
isSuccess,
|
|
102
|
+
refetch,
|
|
103
|
+
} = useTerrenoFlagConfigurationQuery({cacheKey: userId ?? ""}, {skip});
|
|
104
|
+
|
|
105
|
+
const [providerReady, setProviderReady] = useState<boolean>(skip);
|
|
106
|
+
const client = useMemo(() => OpenFeature.getClient(domain), [domain]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const prev = terrenoOpenFeatureHookRefCounts.get(domain) ?? 0;
|
|
110
|
+
terrenoOpenFeatureHookRefCounts.set(domain, prev + 1);
|
|
111
|
+
return (): void => {
|
|
112
|
+
const current = terrenoOpenFeatureHookRefCounts.get(domain) ?? 0;
|
|
113
|
+
const next = current - 1;
|
|
114
|
+
if (next <= 0) {
|
|
115
|
+
terrenoOpenFeatureHookRefCounts.delete(domain);
|
|
116
|
+
void (async (): Promise<void> => {
|
|
117
|
+
const pending = domainProviderSwitchPromises.get(domain);
|
|
118
|
+
if (pending) {
|
|
119
|
+
await pending;
|
|
120
|
+
}
|
|
121
|
+
await OpenFeature.setProviderAndWait(domain, NOOP_PROVIDER);
|
|
122
|
+
})();
|
|
123
|
+
} else {
|
|
124
|
+
terrenoOpenFeatureHookRefCounts.set(domain, next);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}, [domain]);
|
|
128
|
+
|
|
129
|
+
useEffect((): void => {
|
|
130
|
+
if (skip) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
void OpenFeature.setContext(domain, {targetingKey: userId ?? ""});
|
|
134
|
+
}, [domain, skip, userId]);
|
|
135
|
+
|
|
136
|
+
useEffect((): void => {
|
|
137
|
+
if (skip) {
|
|
138
|
+
setProviderReady(true);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setProviderReady(false);
|
|
142
|
+
}, [skip, userId]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (skip || !isSuccess || !data) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let cancelled = false;
|
|
150
|
+
|
|
151
|
+
const applyProvider = async (): Promise<void> => {
|
|
152
|
+
const pending = domainProviderSwitchPromises.get(domain);
|
|
153
|
+
const run = (async (): Promise<void> => {
|
|
154
|
+
if (pending) {
|
|
155
|
+
await pending;
|
|
156
|
+
}
|
|
157
|
+
await OpenFeature.setProviderAndWait(domain, new TypedInMemoryProvider(data));
|
|
158
|
+
})();
|
|
159
|
+
domainProviderSwitchPromises.set(domain, run);
|
|
160
|
+
try {
|
|
161
|
+
await run;
|
|
162
|
+
} finally {
|
|
163
|
+
if (domainProviderSwitchPromises.get(domain) === run) {
|
|
164
|
+
domainProviderSwitchPromises.delete(domain);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!cancelled) {
|
|
168
|
+
setProviderReady(true);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
void applyProvider();
|
|
173
|
+
|
|
174
|
+
return (): void => {
|
|
175
|
+
cancelled = true;
|
|
176
|
+
};
|
|
177
|
+
}, [data, domain, isSuccess, skip]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!socket) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const handler = (): void => {
|
|
185
|
+
void refetch();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
socket.on(socketEventName, handler);
|
|
189
|
+
return (): void => {
|
|
190
|
+
socket.off(socketEventName, handler);
|
|
191
|
+
};
|
|
192
|
+
}, [refetch, socket, socketEventName]);
|
|
193
|
+
|
|
194
|
+
const flags = data ?? {};
|
|
195
|
+
|
|
196
|
+
const isLoading = useMemo((): boolean => {
|
|
197
|
+
if (skip) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (isError) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (!isSuccess || !data) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (isQueryLoading || isFetching) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (!providerReady) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}, [data, isError, isFetching, isQueryLoading, isSuccess, providerReady, skip]);
|
|
214
|
+
|
|
215
|
+
const stableRefetch = useCallback((): unknown => {
|
|
216
|
+
return refetch();
|
|
217
|
+
}, [refetch]);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
client,
|
|
221
|
+
error,
|
|
222
|
+
flags,
|
|
223
|
+
isLoading,
|
|
224
|
+
refetch: stableRefetch,
|
|
225
|
+
};
|
|
226
|
+
};
|