@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.mjs CHANGED
@@ -1,31 +1,79 @@
1
1
  // src/index.tsx
2
- import {
3
- createContext,
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 client = new FlagClient(props.config);
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
- return /* @__PURE__ */ React.createElement(SavvagentContext.Provider, { value: client }, props.children);
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 client = useContext(SavvagentContext);
22
- if (!client) {
50
+ const context = useContext(SavvagentContext);
51
+ if (!context) {
23
52
  throw new Error("useSavvagent must be used within a SavvagentProvider");
24
53
  }
25
- return client;
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 client = useSavvagent();
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 [result] = createResource(
37
- trigger,
38
- async () => {
39
- try {
40
- return await client.evaluate(flagKey, context);
41
- } catch (err) {
42
- const error2 = err;
43
- onError?.(error2);
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 client = useSavvagent();
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 client = useSavvagent();
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.0.0",
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.0.0"
24
+ "@savvagent/sdk": "1.1.0"
25
25
  },
26
26
  "devDependencies": {
27
- "solid-js": "^1.8.0",
28
- "tsup": "^8.0.0",
29
- "typescript": "^5.4.0"
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 src/index.tsx --format cjs,esm --dts --external solid-js",
54
- "dev": "tsup src/index.tsx --format cjs,esm --dts --external solid-js --watch",
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}\""