@savvagent/solid 1.0.0 → 1.1.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/dist/index.d.mts +232 -16
- package/dist/index.d.ts +232 -16
- package/dist/index.js +294 -19
- package/dist/index.mjs +290 -27
- package/package.json +16 -7
package/dist/index.mjs
CHANGED
|
@@ -1,31 +1,79 @@
|
|
|
1
1
|
// src/index.tsx
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
useContext,
|
|
5
|
-
createSignal,
|
|
6
|
-
createResource,
|
|
7
|
-
createEffect,
|
|
8
|
-
onCleanup
|
|
9
|
-
} from "solid-js";
|
|
2
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
3
|
+
import { createContext, useContext, createSignal, createResource, createEffect, createMemo, onCleanup } from "solid-js";
|
|
10
4
|
import { FlagClient } from "@savvagent/sdk";
|
|
11
5
|
import { FlagClient as FlagClient2 } from "@savvagent/sdk";
|
|
12
6
|
var SavvagentContext = createContext();
|
|
13
7
|
function SavvagentProvider(props) {
|
|
14
|
-
const
|
|
8
|
+
const [isReady, setIsReady] = createSignal(false);
|
|
9
|
+
let client;
|
|
10
|
+
try {
|
|
11
|
+
client = new FlagClient(props.config);
|
|
12
|
+
setIsReady(true);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("[Savvagent] Failed to initialize client:", error);
|
|
15
|
+
props.config.onError?.(error);
|
|
16
|
+
client = new FlagClient({
|
|
17
|
+
...props.config,
|
|
18
|
+
apiKey: ""
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const normalizedDefaultContext = createMemo(() => ({
|
|
22
|
+
application_id: props.defaultContext?.applicationId,
|
|
23
|
+
environment: props.defaultContext?.environment,
|
|
24
|
+
organization_id: props.defaultContext?.organizationId,
|
|
25
|
+
user_id: props.defaultContext?.userId,
|
|
26
|
+
anonymous_id: props.defaultContext?.anonymousId,
|
|
27
|
+
session_id: props.defaultContext?.sessionId,
|
|
28
|
+
language: props.defaultContext?.language,
|
|
29
|
+
attributes: props.defaultContext?.attributes
|
|
30
|
+
}));
|
|
31
|
+
if (props.defaultContext?.userId) {
|
|
32
|
+
client.setUserId(props.defaultContext.userId);
|
|
33
|
+
}
|
|
15
34
|
onCleanup(() => {
|
|
16
35
|
client.close();
|
|
17
36
|
});
|
|
18
|
-
|
|
37
|
+
const contextValue = {
|
|
38
|
+
client,
|
|
39
|
+
isReady,
|
|
40
|
+
defaultContext: normalizedDefaultContext
|
|
41
|
+
};
|
|
42
|
+
return _$createComponent(SavvagentContext.Provider, {
|
|
43
|
+
value: contextValue,
|
|
44
|
+
get children() {
|
|
45
|
+
return props.children;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
19
48
|
}
|
|
20
49
|
function useSavvagent() {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
50
|
+
const context = useContext(SavvagentContext);
|
|
51
|
+
if (!context) {
|
|
23
52
|
throw new Error("useSavvagent must be used within a SavvagentProvider");
|
|
24
53
|
}
|
|
25
|
-
return
|
|
54
|
+
return context;
|
|
55
|
+
}
|
|
56
|
+
function deepEqual(a, b) {
|
|
57
|
+
if (a === b) return true;
|
|
58
|
+
if (a === null || b === null) return a === b;
|
|
59
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
60
|
+
const keysA = Object.keys(a);
|
|
61
|
+
const keysB = Object.keys(b);
|
|
62
|
+
if (keysA.length !== keysB.length) return false;
|
|
63
|
+
for (const key of keysA) {
|
|
64
|
+
if (!keysB.includes(key)) return false;
|
|
65
|
+
if (!deepEqual(a[key], b[key])) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
26
70
|
}
|
|
27
71
|
function createFlag(flagKey, options = {}) {
|
|
28
|
-
const
|
|
72
|
+
const {
|
|
73
|
+
client,
|
|
74
|
+
isReady,
|
|
75
|
+
defaultContext
|
|
76
|
+
} = useSavvagent();
|
|
29
77
|
const {
|
|
30
78
|
context,
|
|
31
79
|
defaultValue = false,
|
|
@@ -33,23 +81,39 @@ function createFlag(flagKey, options = {}) {
|
|
|
33
81
|
onError
|
|
34
82
|
} = options;
|
|
35
83
|
const [trigger, setTrigger] = createSignal(0);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
throw error2;
|
|
84
|
+
const mergedContext = createMemo(() => {
|
|
85
|
+
const def = defaultContext();
|
|
86
|
+
return {
|
|
87
|
+
...def,
|
|
88
|
+
...context,
|
|
89
|
+
attributes: {
|
|
90
|
+
...def?.attributes,
|
|
91
|
+
...context?.attributes
|
|
45
92
|
}
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
let prevContext;
|
|
96
|
+
const [result] = createResource(() => ({
|
|
97
|
+
trigger: trigger(),
|
|
98
|
+
context: mergedContext(),
|
|
99
|
+
ready: isReady()
|
|
100
|
+
}), async (source) => {
|
|
101
|
+
if (!source.ready) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return await client.evaluate(flagKey, source.context);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const error2 = err;
|
|
108
|
+
onError?.(error2);
|
|
109
|
+
throw error2;
|
|
46
110
|
}
|
|
47
|
-
);
|
|
111
|
+
});
|
|
48
112
|
const value = () => result()?.value ?? defaultValue;
|
|
49
113
|
const loading = () => result.loading;
|
|
50
114
|
const error = () => result.error ?? null;
|
|
51
115
|
createEffect(() => {
|
|
52
|
-
if (!realtime) return;
|
|
116
|
+
if (!realtime || !isReady()) return;
|
|
53
117
|
const unsubscribe = client.subscribe(flagKey, () => {
|
|
54
118
|
setTrigger((t) => t + 1);
|
|
55
119
|
});
|
|
@@ -57,6 +121,22 @@ function createFlag(flagKey, options = {}) {
|
|
|
57
121
|
unsubscribe();
|
|
58
122
|
});
|
|
59
123
|
});
|
|
124
|
+
createEffect(() => {
|
|
125
|
+
if (!isReady()) return;
|
|
126
|
+
const unsubscribe = client.onOverrideChange(() => {
|
|
127
|
+
setTrigger((t) => t + 1);
|
|
128
|
+
});
|
|
129
|
+
onCleanup(() => {
|
|
130
|
+
unsubscribe();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
createEffect(() => {
|
|
134
|
+
const currentContext = mergedContext();
|
|
135
|
+
if (prevContext !== void 0 && !deepEqual(prevContext, currentContext)) {
|
|
136
|
+
setTrigger((t) => t + 1);
|
|
137
|
+
}
|
|
138
|
+
prevContext = currentContext;
|
|
139
|
+
});
|
|
60
140
|
return {
|
|
61
141
|
value,
|
|
62
142
|
loading,
|
|
@@ -69,8 +149,176 @@ function createFlagValue(flagKey, options = {}) {
|
|
|
69
149
|
const flag = createFlag(flagKey, options);
|
|
70
150
|
return flag.value;
|
|
71
151
|
}
|
|
152
|
+
function createFlags(flagKeys, options = {}) {
|
|
153
|
+
const {
|
|
154
|
+
client,
|
|
155
|
+
isReady,
|
|
156
|
+
defaultContext
|
|
157
|
+
} = useSavvagent();
|
|
158
|
+
const {
|
|
159
|
+
context,
|
|
160
|
+
defaultValues = {},
|
|
161
|
+
realtime = true,
|
|
162
|
+
onError
|
|
163
|
+
} = options;
|
|
164
|
+
const [trigger, setTrigger] = createSignal(0);
|
|
165
|
+
const mergedContext = createMemo(() => {
|
|
166
|
+
const def = defaultContext();
|
|
167
|
+
return {
|
|
168
|
+
...def,
|
|
169
|
+
...context,
|
|
170
|
+
attributes: {
|
|
171
|
+
...def?.attributes,
|
|
172
|
+
...context?.attributes
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
const initialValues = {};
|
|
177
|
+
const initialErrors = {};
|
|
178
|
+
const initialResults = {};
|
|
179
|
+
for (const key of flagKeys) {
|
|
180
|
+
initialValues[key] = defaultValues[key] ?? false;
|
|
181
|
+
initialErrors[key] = null;
|
|
182
|
+
initialResults[key] = null;
|
|
183
|
+
}
|
|
184
|
+
const [values, setValues] = createSignal(initialValues);
|
|
185
|
+
const [errors, setErrors] = createSignal(initialErrors);
|
|
186
|
+
const [results, setResults] = createSignal(initialResults);
|
|
187
|
+
const [loading, setLoading] = createSignal(true);
|
|
188
|
+
let prevContext;
|
|
189
|
+
const evaluateFlags = async () => {
|
|
190
|
+
if (!isReady()) return;
|
|
191
|
+
setLoading(true);
|
|
192
|
+
const newValues = {};
|
|
193
|
+
const newErrors = {};
|
|
194
|
+
const newResults = {};
|
|
195
|
+
const ctx = mergedContext();
|
|
196
|
+
await Promise.all(flagKeys.map(async (flagKey) => {
|
|
197
|
+
try {
|
|
198
|
+
const evalResult = await client.evaluate(flagKey, ctx);
|
|
199
|
+
newValues[flagKey] = evalResult.value;
|
|
200
|
+
newErrors[flagKey] = null;
|
|
201
|
+
newResults[flagKey] = evalResult;
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const error = err;
|
|
204
|
+
newValues[flagKey] = defaultValues[flagKey] ?? false;
|
|
205
|
+
newErrors[flagKey] = error;
|
|
206
|
+
newResults[flagKey] = null;
|
|
207
|
+
onError?.(error, flagKey);
|
|
208
|
+
}
|
|
209
|
+
}));
|
|
210
|
+
setValues(newValues);
|
|
211
|
+
setErrors(newErrors);
|
|
212
|
+
setResults(newResults);
|
|
213
|
+
setLoading(false);
|
|
214
|
+
};
|
|
215
|
+
createEffect(() => {
|
|
216
|
+
trigger();
|
|
217
|
+
if (isReady()) {
|
|
218
|
+
evaluateFlags();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
createEffect(() => {
|
|
222
|
+
if (!realtime || !isReady()) return;
|
|
223
|
+
const unsubscribes = flagKeys.map((flagKey) => client.subscribe(flagKey, () => {
|
|
224
|
+
setTrigger((t) => t + 1);
|
|
225
|
+
}));
|
|
226
|
+
onCleanup(() => {
|
|
227
|
+
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
createEffect(() => {
|
|
231
|
+
if (!isReady()) return;
|
|
232
|
+
const unsubscribe = client.onOverrideChange(() => {
|
|
233
|
+
setTrigger((t) => t + 1);
|
|
234
|
+
});
|
|
235
|
+
onCleanup(() => {
|
|
236
|
+
unsubscribe();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
createEffect(() => {
|
|
240
|
+
const currentContext = mergedContext();
|
|
241
|
+
if (prevContext !== void 0 && !deepEqual(prevContext, currentContext)) {
|
|
242
|
+
setTrigger((t) => t + 1);
|
|
243
|
+
}
|
|
244
|
+
prevContext = currentContext;
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
values,
|
|
248
|
+
loading,
|
|
249
|
+
errors,
|
|
250
|
+
results,
|
|
251
|
+
refetch: () => setTrigger((t) => t + 1)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function createWithFlag(flagKey, callback, options = {}) {
|
|
255
|
+
const {
|
|
256
|
+
client,
|
|
257
|
+
isReady
|
|
258
|
+
} = useSavvagent();
|
|
259
|
+
const {
|
|
260
|
+
context,
|
|
261
|
+
onError
|
|
262
|
+
} = options;
|
|
263
|
+
createEffect(() => {
|
|
264
|
+
if (!isReady()) return;
|
|
265
|
+
client.withFlag(flagKey, callback, context).catch((error) => {
|
|
266
|
+
console.error(`[Savvagent] Error in withFlag callback for ${flagKey}:`, error);
|
|
267
|
+
onError?.(error);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function createUser() {
|
|
272
|
+
const {
|
|
273
|
+
client
|
|
274
|
+
} = useSavvagent();
|
|
275
|
+
const [userId, setUserIdSignal] = createSignal(client.getUserId());
|
|
276
|
+
const [anonymousId, setAnonymousIdSignal] = createSignal(client.getAnonymousId());
|
|
277
|
+
const setUserId = (id) => {
|
|
278
|
+
client.setUserId(id);
|
|
279
|
+
setUserIdSignal(id);
|
|
280
|
+
};
|
|
281
|
+
const getUserId = () => {
|
|
282
|
+
return client.getUserId();
|
|
283
|
+
};
|
|
284
|
+
const setAnonymousId = (id) => {
|
|
285
|
+
client.setAnonymousId(id);
|
|
286
|
+
setAnonymousIdSignal(id);
|
|
287
|
+
};
|
|
288
|
+
const getAnonymousId = () => {
|
|
289
|
+
return client.getAnonymousId();
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
userId,
|
|
293
|
+
setUserId,
|
|
294
|
+
getUserId,
|
|
295
|
+
anonymousId,
|
|
296
|
+
setAnonymousId,
|
|
297
|
+
getAnonymousId
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function createEnvironment() {
|
|
301
|
+
const {
|
|
302
|
+
client
|
|
303
|
+
} = useSavvagent();
|
|
304
|
+
const [environment, setEnvironmentSignal] = createSignal(client.getEnvironment());
|
|
305
|
+
const setEnvironment = (env) => {
|
|
306
|
+
client.setEnvironment(env);
|
|
307
|
+
setEnvironmentSignal(env);
|
|
308
|
+
};
|
|
309
|
+
const getEnvironment = () => {
|
|
310
|
+
return client.getEnvironment();
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
environment,
|
|
314
|
+
setEnvironment,
|
|
315
|
+
getEnvironment
|
|
316
|
+
};
|
|
317
|
+
}
|
|
72
318
|
function createUserSignals() {
|
|
73
|
-
const
|
|
319
|
+
const {
|
|
320
|
+
client
|
|
321
|
+
} = useSavvagent();
|
|
74
322
|
const [userId, setUserIdSignal] = createSignal(client.getUserId());
|
|
75
323
|
const setUserId = (id) => {
|
|
76
324
|
client.setUserId(id);
|
|
@@ -78,16 +326,31 @@ function createUserSignals() {
|
|
|
78
326
|
};
|
|
79
327
|
return [userId, setUserId];
|
|
80
328
|
}
|
|
329
|
+
function createTrackError(flagKey, context) {
|
|
330
|
+
const {
|
|
331
|
+
client
|
|
332
|
+
} = useSavvagent();
|
|
333
|
+
return (error) => {
|
|
334
|
+
client.trackError(flagKey, error, context);
|
|
335
|
+
};
|
|
336
|
+
}
|
|
81
337
|
function trackError(flagKey, error, context) {
|
|
82
|
-
const
|
|
338
|
+
const {
|
|
339
|
+
client
|
|
340
|
+
} = useSavvagent();
|
|
83
341
|
client.trackError(flagKey, error, context);
|
|
84
342
|
}
|
|
85
343
|
export {
|
|
86
344
|
FlagClient2 as FlagClient,
|
|
87
345
|
SavvagentProvider,
|
|
346
|
+
createEnvironment,
|
|
88
347
|
createFlag,
|
|
89
348
|
createFlagValue,
|
|
349
|
+
createFlags,
|
|
350
|
+
createTrackError,
|
|
351
|
+
createUser,
|
|
90
352
|
createUserSignals,
|
|
353
|
+
createWithFlag,
|
|
91
354
|
trackError,
|
|
92
355
|
useSavvagent
|
|
93
356
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvagent/solid",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "SolidJS SDK for Savvagent feature flags with reactive primitives",
|
|
5
5
|
"author": "Savvagent",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,12 +21,21 @@
|
|
|
21
21
|
"solid-js": ">=1.0.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@savvagent/sdk": "1.
|
|
24
|
+
"@savvagent/sdk": "1.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"typescript": "^
|
|
27
|
+
"@solidjs/testing-library": "0.8.10",
|
|
28
|
+
"@testing-library/jest-dom": "6.9.1",
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
30
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
31
|
+
"esbuild-plugin-solid": "0.6.0",
|
|
32
|
+
"eslint": "^8.57.0",
|
|
33
|
+
"jsdom": "27.2.0",
|
|
34
|
+
"solid-js": "^1.9.10",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vite-plugin-solid": "^2.11.10",
|
|
38
|
+
"vitest": "4.0.14"
|
|
30
39
|
},
|
|
31
40
|
"keywords": [
|
|
32
41
|
"savvagent",
|
|
@@ -50,8 +59,8 @@
|
|
|
50
59
|
"access": "public"
|
|
51
60
|
},
|
|
52
61
|
"scripts": {
|
|
53
|
-
"build": "tsup
|
|
54
|
-
"dev": "tsup
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"dev": "tsup --watch",
|
|
55
64
|
"test": "vitest",
|
|
56
65
|
"lint": "eslint src --ext .ts,.tsx",
|
|
57
66
|
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|