@ops-ai/react-router-toggly 1.0.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/client.cjs +832 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +603 -0
- package/dist/client.d.ts +603 -0
- package/dist/client.js +787 -0
- package/dist/client.js.map +1 -0
- package/dist/server.cjs +2324 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +645 -0
- package/dist/server.d.ts +645 -0
- package/dist/server.js +2294 -0
- package/dist/server.js.map +1 -0
- package/dist/telemetry/grpc.cjs +159 -0
- package/dist/telemetry/grpc.cjs.map +1 -0
- package/dist/telemetry/grpc.d.cts +29 -0
- package/dist/telemetry/grpc.d.ts +29 -0
- package/dist/telemetry/grpc.js +149 -0
- package/dist/telemetry/grpc.js.map +1 -0
- package/package.json +107 -0
- package/proto/google/protobuf/timestamp.proto +17 -0
- package/proto/metrics.proto +54 -0
- package/proto/usage.proto +49 -0
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var togglyHooksTypes = require('@ops-ai/toggly-hooks-types');
|
|
4
|
+
require('@ops-ai/toggly-eval');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var togglyLocalGates = require('@ops-ai/toggly-local-gates');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
var reactRouter = require('react-router');
|
|
9
|
+
|
|
10
|
+
// src/core/index.ts
|
|
11
|
+
|
|
12
|
+
// src/core/types.ts
|
|
13
|
+
var TogglyError = class extends Error {
|
|
14
|
+
constructor(message, code, cause) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
this.name = "TogglyError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var TogglyNetworkError = class extends TogglyError {
|
|
22
|
+
constructor(message, cause) {
|
|
23
|
+
super(message, "NETWORK_ERROR", cause);
|
|
24
|
+
this.name = "TogglyNetworkError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var TogglyConfigError = class extends TogglyError {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message, "CONFIG_ERROR");
|
|
30
|
+
this.name = "TogglyConfigError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var TogglyTimeoutError = class extends TogglyError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message, "TIMEOUT_ERROR");
|
|
36
|
+
this.name = "TogglyTimeoutError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var DEFAULT_CONFIG = {
|
|
40
|
+
baseUrl: "https://definitions.toggly.io",
|
|
41
|
+
environment: "Production",
|
|
42
|
+
timeout: 1e4,
|
|
43
|
+
debug: false,
|
|
44
|
+
evaluationMode: "remote"
|
|
45
|
+
};
|
|
46
|
+
function mergeConfig(config) {
|
|
47
|
+
return {
|
|
48
|
+
...DEFAULT_CONFIG,
|
|
49
|
+
...config
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildDefinitionsUrl(config, context) {
|
|
53
|
+
const { baseUrl, appKey, environment, groups, claims, evaluationMode } = mergeConfig(config);
|
|
54
|
+
if (!appKey) {
|
|
55
|
+
throw new Error("appKey is required");
|
|
56
|
+
}
|
|
57
|
+
const mode = evaluationMode ?? "remote";
|
|
58
|
+
const pathSegment = mode === "local" ? "definitions-signed" : "evaluated-signed";
|
|
59
|
+
const url = new URL(`${baseUrl}/${pathSegment}/${appKey}/${environment}`);
|
|
60
|
+
if (mode === "local") {
|
|
61
|
+
return url.toString();
|
|
62
|
+
}
|
|
63
|
+
const fromParam = typeof context === "string" ? { identity: context } : context;
|
|
64
|
+
togglyHooksTypes.appendEvaluationContext(
|
|
65
|
+
url,
|
|
66
|
+
{
|
|
67
|
+
identity: fromParam?.identity,
|
|
68
|
+
groups: fromParam?.groups ?? groups,
|
|
69
|
+
claims: fromParam?.claims ?? claims
|
|
70
|
+
},
|
|
71
|
+
"evaluated"
|
|
72
|
+
);
|
|
73
|
+
return url.toString();
|
|
74
|
+
}
|
|
75
|
+
function isFeatureEnabled(flags, featureKey, defaultValue = false, entityContext) {
|
|
76
|
+
if (!flags || Object.keys(flags).length === 0) {
|
|
77
|
+
return defaultValue;
|
|
78
|
+
}
|
|
79
|
+
const value = flags[featureKey];
|
|
80
|
+
if (value === void 0) {
|
|
81
|
+
return defaultValue;
|
|
82
|
+
}
|
|
83
|
+
return togglyHooksTypes.resolveEvaluatedDefinition(value, entityContext);
|
|
84
|
+
}
|
|
85
|
+
function createLogger(debug) {
|
|
86
|
+
return {
|
|
87
|
+
debug: (...args) => {
|
|
88
|
+
if (debug) {
|
|
89
|
+
console.debug("[Toggly]", ...args);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
info: (...args) => {
|
|
93
|
+
if (debug) {
|
|
94
|
+
console.info("[Toggly]", ...args);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
warn: (...args) => {
|
|
98
|
+
console.warn("[Toggly]", ...args);
|
|
99
|
+
},
|
|
100
|
+
error: (...args) => {
|
|
101
|
+
console.error("[Toggly]", ...args);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function fetchWithTimeout(url, options = {}, timeout = 1e4) {
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(url, {
|
|
110
|
+
...options,
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
});
|
|
113
|
+
return response;
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/core/constants.ts
|
|
120
|
+
var STORAGE_KEYS = {
|
|
121
|
+
/** Identity cookie/storage key */
|
|
122
|
+
IDENTITY: "toggly_identity",
|
|
123
|
+
/** Feature flags cache key */
|
|
124
|
+
FLAGS: "toggly_flags",
|
|
125
|
+
/** Config cache key */
|
|
126
|
+
CONFIG: "toggly_config",
|
|
127
|
+
/** Last fetch timestamp key */
|
|
128
|
+
LAST_FETCH: "toggly_last_fetch"
|
|
129
|
+
};
|
|
130
|
+
var HEADERS = {
|
|
131
|
+
/** Identity header */
|
|
132
|
+
IDENTITY: "x-toggly-identity",
|
|
133
|
+
/** Feature flags header */
|
|
134
|
+
FLAGS: "x-toggly-flags",
|
|
135
|
+
/** Cache control */
|
|
136
|
+
CACHE_CONTROL: "cache-control"
|
|
137
|
+
};
|
|
138
|
+
var TOGGLY_LOADER_KEY = "__toggly";
|
|
139
|
+
|
|
140
|
+
// src/client/sdk-identity.ts
|
|
141
|
+
var SDK_ID = "react-router";
|
|
142
|
+
var SDK_VERSION = "1.0.0";
|
|
143
|
+
function appendSdkQueryParams(params) {
|
|
144
|
+
params.set("sdk", SDK_ID);
|
|
145
|
+
params.set("sdkVersion", SDK_VERSION);
|
|
146
|
+
}
|
|
147
|
+
var TogglyContext = react.createContext(void 0);
|
|
148
|
+
function TogglyProvider({
|
|
149
|
+
children,
|
|
150
|
+
serverContext,
|
|
151
|
+
config,
|
|
152
|
+
enableRefresh = false,
|
|
153
|
+
refreshInterval = 6e4,
|
|
154
|
+
onFlagsChange
|
|
155
|
+
}) {
|
|
156
|
+
const mergedConfig = react.useMemo(
|
|
157
|
+
() => config ? mergeConfig(config) : void 0,
|
|
158
|
+
[config]
|
|
159
|
+
);
|
|
160
|
+
const logger = react.useMemo(
|
|
161
|
+
() => createLogger(mergedConfig?.debug ?? false),
|
|
162
|
+
[mergedConfig?.debug]
|
|
163
|
+
);
|
|
164
|
+
const [flags, setFlags] = react.useState(
|
|
165
|
+
serverContext?.flags ?? mergedConfig?.featureDefaults ?? {}
|
|
166
|
+
);
|
|
167
|
+
const [identity, setIdentity] = react.useState(
|
|
168
|
+
serverContext ? serverContext.identity : mergedConfig?.identity
|
|
169
|
+
);
|
|
170
|
+
const [isReady, setIsReady] = react.useState(!!serverContext);
|
|
171
|
+
const [hooks, setHooks] = react.useState([]);
|
|
172
|
+
const [localGatesRevision, setLocalGatesRevision] = react.useState(0);
|
|
173
|
+
const localGatesRef = react.useRef(mergedConfig?.localGates ?? []);
|
|
174
|
+
const localGateIndexRef = react.useRef(
|
|
175
|
+
togglyLocalGates.buildFlagGateIndex(localGatesRef.current)
|
|
176
|
+
);
|
|
177
|
+
const localGatesListenersRef = react.useRef(/* @__PURE__ */ new Set());
|
|
178
|
+
const getEffectiveFlag = react.useCallback(
|
|
179
|
+
(featureKey, defaultValue = false, entityContext) => {
|
|
180
|
+
const remote = isFeatureEnabled(
|
|
181
|
+
flags,
|
|
182
|
+
featureKey,
|
|
183
|
+
defaultValue,
|
|
184
|
+
entityContext
|
|
185
|
+
);
|
|
186
|
+
return togglyLocalGates.applyLocalGate(
|
|
187
|
+
remote,
|
|
188
|
+
featureKey,
|
|
189
|
+
localGatesRef.current,
|
|
190
|
+
localGateIndexRef.current
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
[flags, localGatesRevision]
|
|
194
|
+
);
|
|
195
|
+
const registerContext2 = react.useCallback(
|
|
196
|
+
(kind, mapper) => {
|
|
197
|
+
togglyHooksTypes.registerContext(kind, mapper);
|
|
198
|
+
},
|
|
199
|
+
[]
|
|
200
|
+
);
|
|
201
|
+
const setLocalGates = react.useCallback((gates) => {
|
|
202
|
+
localGatesRef.current = [...gates];
|
|
203
|
+
localGateIndexRef.current = togglyLocalGates.buildFlagGateIndex(localGatesRef.current);
|
|
204
|
+
}, []);
|
|
205
|
+
const notifyLocalGatesChanged = react.useCallback(() => {
|
|
206
|
+
setLocalGatesRevision((revision) => revision + 1);
|
|
207
|
+
localGatesListenersRef.current.forEach((listener) => {
|
|
208
|
+
try {
|
|
209
|
+
listener();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error("Local gate listener error:", error);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}, [logger]);
|
|
215
|
+
const subscribeLocalGatesChanged = react.useCallback(
|
|
216
|
+
(listener) => {
|
|
217
|
+
localGatesListenersRef.current.add(listener);
|
|
218
|
+
return () => {
|
|
219
|
+
localGatesListenersRef.current.delete(listener);
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
[]
|
|
223
|
+
);
|
|
224
|
+
react.useEffect(() => {
|
|
225
|
+
if (mergedConfig?.localGates) {
|
|
226
|
+
setLocalGates(mergedConfig.localGates);
|
|
227
|
+
}
|
|
228
|
+
}, [mergedConfig?.localGates, setLocalGates]);
|
|
229
|
+
const executeAfterRefresh = react.useCallback(
|
|
230
|
+
async (newFlags) => {
|
|
231
|
+
for (const hook of hooks) {
|
|
232
|
+
if (hook.afterRefresh) {
|
|
233
|
+
try {
|
|
234
|
+
await hook.afterRefresh(newFlags);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.error(
|
|
237
|
+
`Error in hook "${hook.getMetadata().name}.afterRefresh":`,
|
|
238
|
+
error
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
[hooks, logger]
|
|
245
|
+
);
|
|
246
|
+
const fetchFlags = react.useCallback(
|
|
247
|
+
async (userIdentity) => {
|
|
248
|
+
if (!mergedConfig?.appKey) {
|
|
249
|
+
logger.debug("No appKey, using current flags.");
|
|
250
|
+
return flags;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const url = buildDefinitionsUrl(mergedConfig, userIdentity);
|
|
254
|
+
logger.debug(`Fetching flags from: ${url}`);
|
|
255
|
+
const response = await fetchWithTimeout(url, {}, mergedConfig.timeout);
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
258
|
+
}
|
|
259
|
+
const payload = await response.json();
|
|
260
|
+
const newFlags = payload && typeof payload === "object" ? payload : {};
|
|
261
|
+
logger.debug(`Fetched ${Object.keys(newFlags).length} flags.`);
|
|
262
|
+
return newFlags;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
logger.warn("Failed to fetch flags:", error);
|
|
265
|
+
return flags;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
[mergedConfig, flags, logger]
|
|
269
|
+
);
|
|
270
|
+
const updateFlags = react.useCallback(
|
|
271
|
+
async (newFlags) => {
|
|
272
|
+
setFlags(newFlags);
|
|
273
|
+
await executeAfterRefresh(newFlags);
|
|
274
|
+
onFlagsChange?.(newFlags);
|
|
275
|
+
},
|
|
276
|
+
[executeAfterRefresh, onFlagsChange]
|
|
277
|
+
);
|
|
278
|
+
const isEnabled = react.useCallback(
|
|
279
|
+
(featureKey, defaultValue = false, entity, kind) => {
|
|
280
|
+
return getEffectiveFlag(
|
|
281
|
+
featureKey,
|
|
282
|
+
defaultValue,
|
|
283
|
+
togglyHooksTypes.normalizeEntityContext(entity, kind)
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
[getEffectiveFlag]
|
|
287
|
+
);
|
|
288
|
+
const isDisabled = react.useCallback(
|
|
289
|
+
(featureKey, defaultValue = true, entity, kind) => {
|
|
290
|
+
return !isEnabled(featureKey, !defaultValue, entity, kind);
|
|
291
|
+
},
|
|
292
|
+
[isEnabled]
|
|
293
|
+
);
|
|
294
|
+
const evaluateGate = react.useCallback(
|
|
295
|
+
(featureKeys, requirement = "all", negate = false, entity, kind) => {
|
|
296
|
+
if (featureKeys.length === 0) {
|
|
297
|
+
return !negate;
|
|
298
|
+
}
|
|
299
|
+
const entityContext = togglyHooksTypes.normalizeEntityContext(entity, kind);
|
|
300
|
+
let result;
|
|
301
|
+
if (requirement === "any") {
|
|
302
|
+
result = featureKeys.some(
|
|
303
|
+
(key) => getEffectiveFlag(key, false, entityContext)
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
result = featureKeys.every(
|
|
307
|
+
(key) => getEffectiveFlag(key, false, entityContext)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return negate ? !result : result;
|
|
311
|
+
},
|
|
312
|
+
[getEffectiveFlag]
|
|
313
|
+
);
|
|
314
|
+
const identify = react.useCallback(
|
|
315
|
+
async (newIdentity, _context) => {
|
|
316
|
+
logger.debug(`Identifying user: ${newIdentity}`);
|
|
317
|
+
for (const hook of hooks) {
|
|
318
|
+
if (hook.beforeIdentify) {
|
|
319
|
+
try {
|
|
320
|
+
await hook.beforeIdentify(newIdentity);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logger.error(
|
|
323
|
+
`Error in hook "${hook.getMetadata().name}.beforeIdentify":`,
|
|
324
|
+
error
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
setIdentity(newIdentity);
|
|
330
|
+
const newFlags = await fetchFlags(newIdentity);
|
|
331
|
+
await updateFlags(newFlags);
|
|
332
|
+
for (let i = hooks.length - 1; i >= 0; i--) {
|
|
333
|
+
const hook = hooks[i];
|
|
334
|
+
if (hook.afterIdentify) {
|
|
335
|
+
try {
|
|
336
|
+
await hook.afterIdentify(newIdentity, void 0);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
logger.error(
|
|
339
|
+
`Error in hook "${hook.getMetadata().name}.afterIdentify":`,
|
|
340
|
+
error
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
setIsReady(true);
|
|
346
|
+
},
|
|
347
|
+
[hooks, fetchFlags, updateFlags, logger]
|
|
348
|
+
);
|
|
349
|
+
const reset = react.useCallback(async () => {
|
|
350
|
+
logger.debug("Resetting identity");
|
|
351
|
+
setIdentity(void 0);
|
|
352
|
+
const newFlags = await fetchFlags();
|
|
353
|
+
await updateFlags(newFlags);
|
|
354
|
+
}, [fetchFlags, updateFlags, logger]);
|
|
355
|
+
const refresh = react.useCallback(async () => {
|
|
356
|
+
logger.debug("Refreshing flags");
|
|
357
|
+
const newFlags = await fetchFlags(identity);
|
|
358
|
+
await updateFlags(newFlags);
|
|
359
|
+
}, [identity, fetchFlags, updateFlags, logger]);
|
|
360
|
+
const addHook = react.useCallback(
|
|
361
|
+
(hook) => {
|
|
362
|
+
setHooks((currentHooks) => {
|
|
363
|
+
const metadata = hook.getMetadata();
|
|
364
|
+
const exists = currentHooks.find(
|
|
365
|
+
(h) => h.getMetadata().name === metadata.name
|
|
366
|
+
);
|
|
367
|
+
if (exists) {
|
|
368
|
+
logger.warn(`Hook "${metadata.name}" already registered. Skipping.`);
|
|
369
|
+
return currentHooks;
|
|
370
|
+
}
|
|
371
|
+
logger.debug(`Hook "${metadata.name}" registered.`);
|
|
372
|
+
return [...currentHooks, hook];
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
[logger]
|
|
376
|
+
);
|
|
377
|
+
const removeHook = react.useCallback(
|
|
378
|
+
(name) => {
|
|
379
|
+
let removed = false;
|
|
380
|
+
setHooks((currentHooks) => {
|
|
381
|
+
const index = currentHooks.findIndex(
|
|
382
|
+
(h) => h.getMetadata().name === name
|
|
383
|
+
);
|
|
384
|
+
if (index > -1) {
|
|
385
|
+
logger.debug(`Hook "${name}" removed.`);
|
|
386
|
+
removed = true;
|
|
387
|
+
return [
|
|
388
|
+
...currentHooks.slice(0, index),
|
|
389
|
+
...currentHooks.slice(index + 1)
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
return currentHooks;
|
|
393
|
+
});
|
|
394
|
+
return removed;
|
|
395
|
+
},
|
|
396
|
+
[logger]
|
|
397
|
+
);
|
|
398
|
+
const wsRef = react.useRef(null);
|
|
399
|
+
const wsReconnectTimerRef = react.useRef(null);
|
|
400
|
+
const wsConnectedRef = react.useRef(false);
|
|
401
|
+
const lastFallbackRefreshRef = react.useRef(0);
|
|
402
|
+
const FALLBACK_REFRESH_INTERVAL = 20 * 60 * 1e3;
|
|
403
|
+
const WS_RECONNECT_DELAY = 5e3;
|
|
404
|
+
const refreshRef = react.useRef(refresh);
|
|
405
|
+
react.useEffect(() => {
|
|
406
|
+
refreshRef.current = refresh;
|
|
407
|
+
}, [refresh]);
|
|
408
|
+
react.useEffect(() => {
|
|
409
|
+
if (!enableRefresh || !mergedConfig?.appKey) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
logger.debug(`Setting up refresh interval: ${refreshInterval}ms`);
|
|
413
|
+
const intervalId = setInterval(() => {
|
|
414
|
+
if (wsConnectedRef.current) {
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
if (now - lastFallbackRefreshRef.current < FALLBACK_REFRESH_INTERVAL) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
lastFallbackRefreshRef.current = now;
|
|
420
|
+
}
|
|
421
|
+
refresh();
|
|
422
|
+
}, refreshInterval);
|
|
423
|
+
return () => {
|
|
424
|
+
clearInterval(intervalId);
|
|
425
|
+
};
|
|
426
|
+
}, [enableRefresh, refreshInterval, refresh, mergedConfig?.appKey, logger]);
|
|
427
|
+
react.useEffect(() => {
|
|
428
|
+
if (typeof window === "undefined") {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!mergedConfig?.appKey || !mergedConfig?.baseUrl) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (typeof WebSocket === "undefined") {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
function buildWebSocketUrl() {
|
|
438
|
+
const baseUrl = mergedConfig.baseUrl;
|
|
439
|
+
const wsUrl = baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
440
|
+
const params = new URLSearchParams();
|
|
441
|
+
appendSdkQueryParams(params);
|
|
442
|
+
const query = params.toString();
|
|
443
|
+
return `${wsUrl.replace(/\/$/, "")}/${mergedConfig.appKey}/ws${query ? `?${query}` : ""}`;
|
|
444
|
+
}
|
|
445
|
+
function connect() {
|
|
446
|
+
if (wsRef.current) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const wsUrl = buildWebSocketUrl();
|
|
450
|
+
logger.debug(`WebSocket connecting to: ${wsUrl}`);
|
|
451
|
+
try {
|
|
452
|
+
const socket = new WebSocket(wsUrl);
|
|
453
|
+
wsRef.current = socket;
|
|
454
|
+
socket.onopen = () => {
|
|
455
|
+
wsConnectedRef.current = true;
|
|
456
|
+
lastFallbackRefreshRef.current = Date.now();
|
|
457
|
+
logger.debug("WebSocket connected");
|
|
458
|
+
};
|
|
459
|
+
socket.onmessage = (event) => {
|
|
460
|
+
const text = typeof event.data === "string" ? event.data : "";
|
|
461
|
+
try {
|
|
462
|
+
const msg = JSON.parse(text);
|
|
463
|
+
if (msg.type === "ping") {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (msg.type === "flags-updated" || msg.type === "update") {
|
|
467
|
+
logger.debug("WebSocket: definitions updated, refreshing");
|
|
468
|
+
refreshRef.current();
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
if (text === "update" || text === "flags-updated") {
|
|
472
|
+
refreshRef.current();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
socket.onclose = () => {
|
|
477
|
+
wsConnectedRef.current = false;
|
|
478
|
+
wsRef.current = null;
|
|
479
|
+
logger.debug("WebSocket disconnected, reconnecting in 5s");
|
|
480
|
+
scheduleReconnect();
|
|
481
|
+
};
|
|
482
|
+
socket.onerror = () => {
|
|
483
|
+
logger.warn("WebSocket error");
|
|
484
|
+
};
|
|
485
|
+
} catch (error) {
|
|
486
|
+
logger.warn("Failed to create WebSocket:", error);
|
|
487
|
+
wsRef.current = null;
|
|
488
|
+
scheduleReconnect();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function scheduleReconnect() {
|
|
492
|
+
if (wsReconnectTimerRef.current) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
wsReconnectTimerRef.current = setTimeout(() => {
|
|
496
|
+
wsReconnectTimerRef.current = null;
|
|
497
|
+
connect();
|
|
498
|
+
}, WS_RECONNECT_DELAY);
|
|
499
|
+
}
|
|
500
|
+
connect();
|
|
501
|
+
return () => {
|
|
502
|
+
if (wsReconnectTimerRef.current) {
|
|
503
|
+
clearTimeout(wsReconnectTimerRef.current);
|
|
504
|
+
wsReconnectTimerRef.current = null;
|
|
505
|
+
}
|
|
506
|
+
if (wsRef.current) {
|
|
507
|
+
wsRef.current.onclose = null;
|
|
508
|
+
wsRef.current.onerror = null;
|
|
509
|
+
wsRef.current.onmessage = null;
|
|
510
|
+
wsRef.current.onopen = null;
|
|
511
|
+
wsRef.current.close();
|
|
512
|
+
wsRef.current = null;
|
|
513
|
+
wsConnectedRef.current = false;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}, [mergedConfig?.appKey, mergedConfig?.baseUrl, logger]);
|
|
517
|
+
react.useEffect(() => {
|
|
518
|
+
if (!serverContext && mergedConfig?.appKey) {
|
|
519
|
+
logger.debug("No server context, initializing client-side");
|
|
520
|
+
fetchFlags(identity).then((newFlags) => {
|
|
521
|
+
setFlags(newFlags);
|
|
522
|
+
setIsReady(true);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}, []);
|
|
526
|
+
const contextValue = react.useMemo(
|
|
527
|
+
() => ({
|
|
528
|
+
flags,
|
|
529
|
+
isReady,
|
|
530
|
+
identity,
|
|
531
|
+
isEnabled,
|
|
532
|
+
isDisabled,
|
|
533
|
+
evaluateGate,
|
|
534
|
+
registerContext: registerContext2,
|
|
535
|
+
identify,
|
|
536
|
+
reset,
|
|
537
|
+
refresh,
|
|
538
|
+
addHook,
|
|
539
|
+
removeHook,
|
|
540
|
+
setLocalGates,
|
|
541
|
+
notifyLocalGatesChanged,
|
|
542
|
+
subscribeLocalGatesChanged
|
|
543
|
+
}),
|
|
544
|
+
[
|
|
545
|
+
flags,
|
|
546
|
+
isReady,
|
|
547
|
+
identity,
|
|
548
|
+
isEnabled,
|
|
549
|
+
isDisabled,
|
|
550
|
+
evaluateGate,
|
|
551
|
+
registerContext2,
|
|
552
|
+
identify,
|
|
553
|
+
reset,
|
|
554
|
+
refresh,
|
|
555
|
+
addHook,
|
|
556
|
+
removeHook,
|
|
557
|
+
setLocalGates,
|
|
558
|
+
notifyLocalGatesChanged,
|
|
559
|
+
subscribeLocalGatesChanged
|
|
560
|
+
]
|
|
561
|
+
);
|
|
562
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TogglyContext.Provider, { value: contextValue, children });
|
|
563
|
+
}
|
|
564
|
+
function useTogglyContext() {
|
|
565
|
+
const context = react.useContext(TogglyContext);
|
|
566
|
+
if (!context) {
|
|
567
|
+
throw new Error("useTogglyContext must be used within a TogglyProvider");
|
|
568
|
+
}
|
|
569
|
+
return context;
|
|
570
|
+
}
|
|
571
|
+
function useToggly() {
|
|
572
|
+
return useTogglyContext();
|
|
573
|
+
}
|
|
574
|
+
function useFeature(featureKey, defaultValue = false, entity, kind) {
|
|
575
|
+
const { isEnabled } = useTogglyContext();
|
|
576
|
+
return isEnabled(featureKey, defaultValue, entity, kind);
|
|
577
|
+
}
|
|
578
|
+
function useFeatureDisabled(featureKey, defaultValue = true, entity, kind) {
|
|
579
|
+
const { isDisabled } = useTogglyContext();
|
|
580
|
+
return isDisabled(featureKey, defaultValue, entity, kind);
|
|
581
|
+
}
|
|
582
|
+
function useFeatureGate(featureKeys, requirement = "all", negate = false, entity, kind) {
|
|
583
|
+
const { evaluateGate } = useTogglyContext();
|
|
584
|
+
return evaluateGate(featureKeys, requirement, negate, entity, kind);
|
|
585
|
+
}
|
|
586
|
+
function useFeatureFlags() {
|
|
587
|
+
const { flags } = useTogglyContext();
|
|
588
|
+
return flags;
|
|
589
|
+
}
|
|
590
|
+
function useFeatures(featureKeys, defaultValue = false) {
|
|
591
|
+
const { isEnabled } = useTogglyContext();
|
|
592
|
+
return react.useMemo(() => {
|
|
593
|
+
const result = {};
|
|
594
|
+
for (const key of featureKeys) {
|
|
595
|
+
result[key] = isEnabled(key, defaultValue);
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}, [featureKeys, isEnabled, defaultValue]);
|
|
599
|
+
}
|
|
600
|
+
function useFeatureCallback(featureKey, enabledCallback, disabledCallback, defaultValue = false) {
|
|
601
|
+
const enabled = useFeature(featureKey, defaultValue);
|
|
602
|
+
return enabled ? enabledCallback() : disabledCallback();
|
|
603
|
+
}
|
|
604
|
+
function useFeatureValue(featureKey, enabledValue, disabledValue, defaultValue = false) {
|
|
605
|
+
const enabled = useFeature(featureKey, defaultValue);
|
|
606
|
+
return enabled ? enabledValue : disabledValue;
|
|
607
|
+
}
|
|
608
|
+
function useFeatureChange(featureKey, _onChange) {
|
|
609
|
+
const enabled = useFeature(featureKey);
|
|
610
|
+
return enabled;
|
|
611
|
+
}
|
|
612
|
+
function useIdentity() {
|
|
613
|
+
const { identity, identify, reset } = useTogglyContext();
|
|
614
|
+
return {
|
|
615
|
+
identity,
|
|
616
|
+
identify,
|
|
617
|
+
reset
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function useTogglyReady() {
|
|
621
|
+
const { isReady } = useTogglyContext();
|
|
622
|
+
return isReady;
|
|
623
|
+
}
|
|
624
|
+
function useRefreshFlags() {
|
|
625
|
+
const { refresh } = useTogglyContext();
|
|
626
|
+
return refresh;
|
|
627
|
+
}
|
|
628
|
+
function useFeatureRender(featureKey, enabled, disabled, defaultValue = false) {
|
|
629
|
+
const isEnabled = useFeature(featureKey, defaultValue);
|
|
630
|
+
return isEnabled ? enabled : disabled;
|
|
631
|
+
}
|
|
632
|
+
function useABTest(featureKey, variantA, variantB, defaultVariant = "A") {
|
|
633
|
+
const enabled = useFeature(featureKey, defaultVariant === "B");
|
|
634
|
+
return enabled ? variantB : variantA;
|
|
635
|
+
}
|
|
636
|
+
function useFeatureWithLoading(featureKey, defaultValue = false) {
|
|
637
|
+
const { isReady, isEnabled } = useTogglyContext();
|
|
638
|
+
return {
|
|
639
|
+
enabled: isEnabled(featureKey, defaultValue),
|
|
640
|
+
isLoading: !isReady
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function Feature({
|
|
644
|
+
featureKey,
|
|
645
|
+
featureKeys,
|
|
646
|
+
requirement = "all",
|
|
647
|
+
negate = false,
|
|
648
|
+
defaultValue = false,
|
|
649
|
+
children,
|
|
650
|
+
render,
|
|
651
|
+
context,
|
|
652
|
+
contextKind
|
|
653
|
+
}) {
|
|
654
|
+
const keys = featureKeys ?? (featureKey ? [featureKey] : []);
|
|
655
|
+
const singleEnabled = useFeature(keys[0] ?? "", defaultValue, context, contextKind);
|
|
656
|
+
const gateEnabled = useFeatureGate(keys, requirement, false, context, contextKind);
|
|
657
|
+
let enabled;
|
|
658
|
+
if (keys.length === 0) {
|
|
659
|
+
enabled = defaultValue;
|
|
660
|
+
} else if (keys.length === 1) {
|
|
661
|
+
enabled = singleEnabled;
|
|
662
|
+
} else {
|
|
663
|
+
enabled = gateEnabled;
|
|
664
|
+
}
|
|
665
|
+
if (negate) {
|
|
666
|
+
enabled = !enabled;
|
|
667
|
+
}
|
|
668
|
+
if (render) {
|
|
669
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: render(enabled) });
|
|
670
|
+
}
|
|
671
|
+
return enabled ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children }) : null;
|
|
672
|
+
}
|
|
673
|
+
function FeatureEnabled({
|
|
674
|
+
featureKey,
|
|
675
|
+
defaultValue = false,
|
|
676
|
+
children
|
|
677
|
+
}) {
|
|
678
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Feature, { featureKey, defaultValue, children });
|
|
679
|
+
}
|
|
680
|
+
function FeatureDisabled({
|
|
681
|
+
featureKey,
|
|
682
|
+
defaultValue = true,
|
|
683
|
+
children
|
|
684
|
+
}) {
|
|
685
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Feature, { featureKey, defaultValue: !defaultValue, negate: true, children });
|
|
686
|
+
}
|
|
687
|
+
function FeatureSwitch({
|
|
688
|
+
featureKey,
|
|
689
|
+
defaultValue = false,
|
|
690
|
+
enabled,
|
|
691
|
+
disabled
|
|
692
|
+
}) {
|
|
693
|
+
const isEnabled = useFeature(featureKey, defaultValue);
|
|
694
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: isEnabled ? enabled : disabled });
|
|
695
|
+
}
|
|
696
|
+
function FeatureGate({
|
|
697
|
+
featureKeys,
|
|
698
|
+
requirement = "all",
|
|
699
|
+
negate = false,
|
|
700
|
+
children,
|
|
701
|
+
context,
|
|
702
|
+
contextKind
|
|
703
|
+
}) {
|
|
704
|
+
const enabled = useFeatureGate(featureKeys, requirement, negate, context, contextKind);
|
|
705
|
+
return enabled ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children }) : null;
|
|
706
|
+
}
|
|
707
|
+
function RouterTogglyProvider({
|
|
708
|
+
children,
|
|
709
|
+
routeId,
|
|
710
|
+
fallbackContext,
|
|
711
|
+
config,
|
|
712
|
+
...props
|
|
713
|
+
}) {
|
|
714
|
+
let serverContext;
|
|
715
|
+
try {
|
|
716
|
+
if (routeId) {
|
|
717
|
+
const routeData = reactRouter.useRouteLoaderData(routeId);
|
|
718
|
+
serverContext = routeData?.[TOGGLY_LOADER_KEY];
|
|
719
|
+
} else {
|
|
720
|
+
const loaderData = reactRouter.useLoaderData();
|
|
721
|
+
serverContext = loaderData?.[TOGGLY_LOADER_KEY];
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
serverContext = fallbackContext;
|
|
725
|
+
}
|
|
726
|
+
serverContext = serverContext ?? fallbackContext;
|
|
727
|
+
const mergedConfig = config ?? (serverContext ? {
|
|
728
|
+
appKey: serverContext.appKey,
|
|
729
|
+
environment: serverContext.environment
|
|
730
|
+
} : void 0);
|
|
731
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
732
|
+
TogglyProvider,
|
|
733
|
+
{
|
|
734
|
+
serverContext,
|
|
735
|
+
config: mergedConfig,
|
|
736
|
+
...props,
|
|
737
|
+
children
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
function useTogglyLoaderData() {
|
|
742
|
+
const loaderData = reactRouter.useLoaderData();
|
|
743
|
+
return {
|
|
744
|
+
data: loaderData,
|
|
745
|
+
toggly: loaderData[TOGGLY_LOADER_KEY]
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function useTogglyRouteLoaderData(routeId) {
|
|
749
|
+
const routeData = reactRouter.useRouteLoaderData(routeId);
|
|
750
|
+
return {
|
|
751
|
+
data: routeData,
|
|
752
|
+
toggly: routeData?.[TOGGLY_LOADER_KEY]
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function extractServerContext(loaderData) {
|
|
756
|
+
return loaderData[TOGGLY_LOADER_KEY];
|
|
757
|
+
}
|
|
758
|
+
function hasTogglyContext(loaderData) {
|
|
759
|
+
const data = loaderData;
|
|
760
|
+
return TOGGLY_LOADER_KEY in data && data[TOGGLY_LOADER_KEY] !== void 0;
|
|
761
|
+
}
|
|
762
|
+
function TogglyScript({
|
|
763
|
+
serverContext,
|
|
764
|
+
nonce
|
|
765
|
+
}) {
|
|
766
|
+
if (!serverContext) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
const script = `window.__TOGGLY_DATA__=${togglyHooksTypes.serializeJsonForInlineScript(serverContext)};`;
|
|
770
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
771
|
+
"script",
|
|
772
|
+
{
|
|
773
|
+
nonce,
|
|
774
|
+
dangerouslySetInnerHTML: { __html: script },
|
|
775
|
+
suppressHydrationWarning: true
|
|
776
|
+
}
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function getWindowTogglyData() {
|
|
780
|
+
if (typeof window === "undefined") {
|
|
781
|
+
return void 0;
|
|
782
|
+
}
|
|
783
|
+
return window.__TOGGLY_DATA__;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
Object.defineProperty(exports, "normalizeEntityContext", {
|
|
787
|
+
enumerable: true,
|
|
788
|
+
get: function () { return togglyHooksTypes.normalizeEntityContext; }
|
|
789
|
+
});
|
|
790
|
+
Object.defineProperty(exports, "registerContext", {
|
|
791
|
+
enumerable: true,
|
|
792
|
+
get: function () { return togglyHooksTypes.registerContext; }
|
|
793
|
+
});
|
|
794
|
+
exports.Feature = Feature;
|
|
795
|
+
exports.FeatureDisabled = FeatureDisabled;
|
|
796
|
+
exports.FeatureEnabled = FeatureEnabled;
|
|
797
|
+
exports.FeatureGate = FeatureGate;
|
|
798
|
+
exports.FeatureSwitch = FeatureSwitch;
|
|
799
|
+
exports.HEADERS = HEADERS;
|
|
800
|
+
exports.RouterTogglyProvider = RouterTogglyProvider;
|
|
801
|
+
exports.STORAGE_KEYS = STORAGE_KEYS;
|
|
802
|
+
exports.TOGGLY_LOADER_KEY = TOGGLY_LOADER_KEY;
|
|
803
|
+
exports.TogglyConfigError = TogglyConfigError;
|
|
804
|
+
exports.TogglyContext = TogglyContext;
|
|
805
|
+
exports.TogglyError = TogglyError;
|
|
806
|
+
exports.TogglyNetworkError = TogglyNetworkError;
|
|
807
|
+
exports.TogglyProvider = TogglyProvider;
|
|
808
|
+
exports.TogglyScript = TogglyScript;
|
|
809
|
+
exports.TogglyTimeoutError = TogglyTimeoutError;
|
|
810
|
+
exports.extractServerContext = extractServerContext;
|
|
811
|
+
exports.getWindowTogglyData = getWindowTogglyData;
|
|
812
|
+
exports.hasTogglyContext = hasTogglyContext;
|
|
813
|
+
exports.useABTest = useABTest;
|
|
814
|
+
exports.useFeature = useFeature;
|
|
815
|
+
exports.useFeatureCallback = useFeatureCallback;
|
|
816
|
+
exports.useFeatureChange = useFeatureChange;
|
|
817
|
+
exports.useFeatureDisabled = useFeatureDisabled;
|
|
818
|
+
exports.useFeatureFlags = useFeatureFlags;
|
|
819
|
+
exports.useFeatureGate = useFeatureGate;
|
|
820
|
+
exports.useFeatureRender = useFeatureRender;
|
|
821
|
+
exports.useFeatureValue = useFeatureValue;
|
|
822
|
+
exports.useFeatureWithLoading = useFeatureWithLoading;
|
|
823
|
+
exports.useFeatures = useFeatures;
|
|
824
|
+
exports.useIdentity = useIdentity;
|
|
825
|
+
exports.useRefreshFlags = useRefreshFlags;
|
|
826
|
+
exports.useToggly = useToggly;
|
|
827
|
+
exports.useTogglyContext = useTogglyContext;
|
|
828
|
+
exports.useTogglyLoaderData = useTogglyLoaderData;
|
|
829
|
+
exports.useTogglyReady = useTogglyReady;
|
|
830
|
+
exports.useTogglyRouteLoaderData = useTogglyRouteLoaderData;
|
|
831
|
+
//# sourceMappingURL=client.cjs.map
|
|
832
|
+
//# sourceMappingURL=client.cjs.map
|