Skip to content

Commit b625503

Browse files
committed
feat: update sdks
1 parent 11100fc commit b625503

13 files changed

Lines changed: 158 additions & 65 deletions

File tree

packages/react/src/hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function useReplane<T extends object = UntypedReplaneConfig>(): ReplaneCl
1111
return context.client as ReplaneClient<T>;
1212
}
1313

14-
export function useConfig<T>(name: string, options?: GetConfigOptions): T {
14+
export function useConfig<T>(name: string, options?: GetConfigOptions<T>): T {
1515
const client = useReplane();
1616

1717
const subscribe = useCallback(
@@ -75,7 +75,7 @@ export function createReplaneHook<TConfigs extends object>() {
7575
export function createConfigHook<TConfigs extends object>() {
7676
return function useTypedConfig<K extends keyof TConfigs>(
7777
name: K,
78-
options?: GetConfigOptions
78+
options?: GetConfigOptions<TConfigs[K]>
7979
): TConfigs[K] {
8080
return useConfig<TConfigs[K]>(String(name), options);
8181
};

packages/sdk/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,15 @@ Parameters:
118118
- `name` (K extends keyof T) – config name to fetch. TypeScript will enforce that this is a valid config name from your `Configs` interface.
119119
- `options` (object) – optional configuration:
120120
- `context` (object) – context merged with client-level context for override evaluation.
121+
- `default` (T[K]) – default value to return if the config is not found. When provided, the method will not throw.
121122

122123
Returns the config value of type `T[K]` (synchronous). The return type is automatically inferred from your `Configs` interface.
123124

124125
Notes:
125126

126127
- The Replane client receives realtime updates via SSE in the background.
127-
- If the config is not found, throws a `ReplaneError` with code `not_found`.
128+
- If the config is not found and no default is provided, throws a `ReplaneError` with code `not_found`.
129+
- If the config is not found and a default is provided, returns the default value without throwing.
128130
- Context-based overrides are evaluated automatically based on context.
129131

130132
Example:
@@ -148,6 +150,9 @@ const userEnabled = replane.get("billing-enabled", {
148150
context: { userId: "user-123", plan: "premium" },
149151
});
150152

153+
// Get value with default - won't throw if config doesn't exist
154+
const maxConnections = replane.get("max-connections", { default: 10 });
155+
151156
// Clean up when done
152157
replane.close();
153158
```

packages/sdk/src/client-types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ export interface ReplaneLogger {
1919
/**
2020
* Options for getting a config value
2121
*/
22-
export interface GetConfigOptions {
22+
export interface GetConfigOptions<T> {
2323
/**
2424
* Context for override evaluation (merged with client-level context).
2525
*/
2626
context?: ReplaneContext;
27+
/**
28+
* Default value to return if the config is not found.
29+
* When provided, the method will not throw if the config doesn't exist.
30+
*/
31+
default?: T;
2732
}
2833

2934
/**
@@ -56,7 +61,7 @@ export interface ReplaneSnapshot<_T extends object = object> {
5661
*/
5762
export interface ReplaneClient<T extends object> {
5863
/** Get a config by its name. */
59-
get<K extends keyof T>(configName: K, options?: GetConfigOptions): T[K];
64+
get<K extends keyof T>(configName: K, options?: GetConfigOptions<T[K]>): T[K];
6065
/** Subscribe to config changes.
6166
* @param callback - A function to call when an config is changed. The callback will be called with the new config value.
6267
* @returns A function to unsubscribe from the config changes.

packages/sdk/src/client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,13 @@ function createClientCore<T extends object = Record<string, unknown>>(
9999
}
100100
}
101101

102-
function get<K extends keyof T>(configName: K, getConfigOptions: GetConfigOptions = {}): T[K] {
102+
function get<K extends keyof T>(configName: K, getConfigOptions: GetConfigOptions<T[K]> = {}): T[K] {
103103
const config = configs.get(String(configName));
104104

105105
if (config === undefined) {
106+
if ("default" in getConfigOptions) {
107+
return getConfigOptions.default as T[K];
108+
}
106109
throw new ReplaneError({
107110
message: `Config not found: ${String(configName)}`,
108111
code: ReplaneErrorCode.NotFound,
@@ -221,9 +224,12 @@ export function createInMemoryReplaneClient<T extends object = Record<string, un
221224
initialData: T
222225
): ReplaneClient<T> {
223226
return {
224-
get: (configName) => {
227+
get: (configName, options) => {
225228
const config = initialData[configName];
226229
if (config === undefined) {
230+
if (options && "default" in options) {
231+
return options.default as T[typeof configName];
232+
}
227233
throw new ReplaneError({
228234
message: `Config not found: ${String(configName)}`,
229235
code: ReplaneErrorCode.NotFound,

packages/sdk/tests/index.spec.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,40 @@ describe("createReplaneClient", () => {
103103
expect(() => client.get("nonexistent")).toThrow("Config not found: nonexistent");
104104
});
105105

106+
it("should return default value when config not found and default is provided", async () => {
107+
clientPromise = createClient();
108+
const connection = await mockServer.acceptConnection();
109+
await connection.push({
110+
type: "init",
111+
configs: [{ name: "config1", overrides: [], value: "value1" }],
112+
});
113+
114+
const client = await clientPromise;
115+
await sync();
116+
117+
expect(client.get("nonexistent", { default: "fallback" })).toBe("fallback");
118+
expect(client.get("nonexistent", { default: 42 })).toBe(42);
119+
expect(client.get("nonexistent", { default: null })).toBe(null);
120+
expect(client.get("nonexistent", { default: false })).toBe(false);
121+
expect(client.get("nonexistent", { default: { nested: "value" } })).toEqual({
122+
nested: "value",
123+
});
124+
});
125+
126+
it("should return actual value when config exists even if default is provided", async () => {
127+
clientPromise = createClient();
128+
const connection = await mockServer.acceptConnection();
129+
await connection.push({
130+
type: "init",
131+
configs: [{ name: "config1", overrides: [], value: "actual" }],
132+
});
133+
134+
const client = await clientPromise;
135+
await sync();
136+
137+
expect(client.get("config1", { default: "fallback" })).toBe("actual");
138+
});
139+
106140
it("should handle config values of different types", async () => {
107141
clientPromise = createClient();
108142
const connection = await mockServer.acceptConnection();
@@ -1900,6 +1934,28 @@ describe("createInMemoryReplaneClient", () => {
19001934
expect(() => client.get("nonexistent" as never)).toThrow("Config not found: nonexistent");
19011935
});
19021936

1937+
it("should return default value when config not found and default is provided", () => {
1938+
const client = createInMemoryReplaneClient({
1939+
config1: "value1",
1940+
});
1941+
1942+
expect(client.get("nonexistent" as never, { default: "fallback" as never })).toBe("fallback");
1943+
expect(client.get("nonexistent" as never, { default: 42 as never })).toBe(42);
1944+
expect(client.get("nonexistent" as never, { default: null as never })).toBe(null);
1945+
expect(client.get("nonexistent" as never, { default: false as never })).toBe(false);
1946+
expect(client.get("nonexistent" as never, { default: { nested: "value" } as never })).toEqual({
1947+
nested: "value",
1948+
});
1949+
});
1950+
1951+
it("should return actual value when config exists even if default is provided", () => {
1952+
const client = createInMemoryReplaneClient({
1953+
config1: "actual",
1954+
});
1955+
1956+
expect(client.get("config1", { default: "fallback" })).toBe("actual");
1957+
});
1958+
19031959
it("should handle complex values", () => {
19041960
const client = createInMemoryReplaneClient({
19051961
array: [1, 2, 3],
@@ -2007,6 +2063,32 @@ describe("restoreReplaneClient", () => {
20072063
expect(() => client.get("nonexistent")).toThrow("Config not found: nonexistent");
20082064
});
20092065

2066+
it("should return default value when config not found and default is provided", () => {
2067+
const snapshot: ReplaneSnapshot<Record<string, unknown>> = {
2068+
configs: [{ name: "config1", value: "value1", overrides: [] }],
2069+
};
2070+
2071+
const client = restoreReplaneClient({ snapshot });
2072+
2073+
expect(client.get("nonexistent", { default: "fallback" })).toBe("fallback");
2074+
expect(client.get("nonexistent", { default: 42 })).toBe(42);
2075+
expect(client.get("nonexistent", { default: null })).toBe(null);
2076+
expect(client.get("nonexistent", { default: false })).toBe(false);
2077+
expect(client.get("nonexistent", { default: { nested: "value" } })).toEqual({
2078+
nested: "value",
2079+
});
2080+
});
2081+
2082+
it("should return actual value when config exists even if default is provided", () => {
2083+
const snapshot: ReplaneSnapshot<Record<string, unknown>> = {
2084+
configs: [{ name: "config1", value: "actual", overrides: [] }],
2085+
};
2086+
2087+
const client = restoreReplaneClient({ snapshot });
2088+
2089+
expect(client.get("config1", { default: "fallback" })).toBe("actual");
2090+
});
2091+
20102092
it("should have a no-op close method", () => {
20112093
const snapshot: ReplaneSnapshot<Record<string, unknown>> = {
20122094
configs: [{ name: "config1", value: "value1", overrides: [] }],
@@ -2230,7 +2312,9 @@ describe("restoreReplaneClient", () => {
22302312
overrides: [
22312313
{
22322314
name: "non-na-override",
2233-
conditions: [{ operator: "not_in", property: "country", value: ["US", "CA", "MX"] }],
2315+
conditions: [
2316+
{ operator: "not_in", property: "country", value: ["US", "CA", "MX"] },
2317+
],
22342318
value: "non-na-value",
22352319
},
22362320
],

packages/svelte/README.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ npm install @replanejs/svelte
1919
import { ReplaneContext, config } from '@replanejs/svelte';
2020
import { createReplaneClient } from '@replanejs/svelte';
2121
22-
const client = await createReplaneClient({
22+
const replane = await createReplaneClient({
2323
baseUrl: 'https://your-replane-server.com',
2424
sdkKey: 'your-sdk-key',
2525
});
2626
</script>
2727
28-
<ReplaneContext {client}>
28+
<ReplaneContext client={replane}>
2929
<MyComponent />
3030
</ReplaneContext>
3131
```
@@ -77,10 +77,10 @@ Get direct access to the Replane client from context.
7777
<script>
7878
import { getReplane } from '@replanejs/svelte';
7979
80-
const { client } = getReplane();
80+
const replane = getReplane();
8181
8282
function handleClick() {
83-
const value = client.get('some-config');
83+
const value = replane.get('some-config');
8484
console.log(value);
8585
}
8686
</script>
@@ -90,14 +90,15 @@ Get direct access to the Replane client from context.
9090

9191
### configFrom
9292

93-
Create a reactive store from a client directly (without context).
93+
Create a reactive store from a client directly (without context). Type-safe with full autocomplete for config names.
9494

9595
```svelte
9696
<script>
9797
import { configFrom } from '@replanejs/svelte';
98-
import { client } from './replane-client';
98+
import { replane } from './replane-client';
9999
100-
const featureEnabled = configFrom<boolean>(client, 'featureEnabled');
100+
// Config name is validated against TConfigs, return type is inferred
101+
const featureEnabled = configFrom(replane, 'featureEnabled');
101102
</script>
102103
103104
{#if $featureEnabled}
@@ -117,13 +118,13 @@ Can be used in three ways:
117118
<script>
118119
import { ReplaneContext, createReplaneClient } from '@replanejs/svelte';
119120
120-
const client = await createReplaneClient({
121+
const replane = await createReplaneClient({
121122
baseUrl: 'https://your-replane-server.com',
122123
sdkKey: 'your-sdk-key',
123124
});
124125
</script>
125126
126-
<ReplaneContext {client}>
127+
<ReplaneContext client={replane}>
127128
<App />
128129
</ReplaneContext>
129130
```
@@ -193,11 +194,15 @@ export const getAppReplane = createTypedReplane<AppConfigs>();
193194

194195
```svelte
195196
<script lang="ts">
196-
import { appConfig } from '$lib/replane';
197+
import { appConfig, getAppReplane } from '$lib/replane';
197198
198199
// Config names autocomplete, values are fully typed
199200
const theme = appConfig("theme");
200201
// $theme is { darkMode: boolean; primaryColor: string }
202+
203+
// Direct client access
204+
const replane = getAppReplane();
205+
const features = replane.get("features"); // fully typed
201206
</script>
202207
203208
<div style:color={$theme.primaryColor}>

packages/svelte/examples/basic/src/lib/ConfigDisplay.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
experimentalFeatures: boolean;
1313
}
1414
15-
const { client } = getReplane();
15+
const replane = getReplane();
1616
const theme = config<ThemeConfig>("theme-config");
1717
const features = config<FeatureFlags>("feature-flags");
1818
1919
// Get value with premium context override
2020
const premiumFeatures = $derived(
21-
client.get("feature-flags", { context: { plan: "premium" } })
21+
replane.get("feature-flags", { context: { plan: "premium" } })
2222
);
2323
</script>
2424

@@ -43,7 +43,7 @@
4343
<div class="config-card">
4444
<h3>With Context Override</h3>
4545
<p>You can pass context for user-specific evaluation:</p>
46-
<pre>{`const premiumFeatures = client.get("feature-flags", {
46+
<pre>{`const premiumFeatures = replane.get("feature-flags", {
4747
context: { userId: "123", plan: "premium" }
4848
});`}</pre>
4949
<p>Current value with premium context:</p>

packages/svelte/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export type {
3333
ReplaneContextProps,
3434
ReplaneContextWithClientProps,
3535
ReplaneContextWithOptionsProps,
36-
ConfigOptions,
3736
} from "./types";
3837

3938
// Type guards

0 commit comments

Comments
 (0)