Skip to content

Commit aabb2ae

Browse files
committed
feat: update docs
1 parent 0c99412 commit aabb2ae

1 file changed

Lines changed: 103 additions & 61 deletions

File tree

README.md

Lines changed: 103 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# Replane JavaScript SDK
22

3-
Small TypeScript client for fetching configuration values from a Replane API.
3+
Small TypeScript client for watching configuration values from a Replane API with realtime updates and context-based override evaluation.
44

55
Part of the Replane project: [replane-dev/replane](https://github.com/replane-dev/replane).
66

77
> Status: early. Minimal surface area on purpose. Expect small breaking tweaks until 0.1.x.
88
99
## Why it exists
1010

11-
You just need: given a token + config name -> get the value. This package does only that:
11+
You need: given a token + config name + optional context -> watch the value with realtime updates. This package does only that:
1212

1313
- Works in ESM and CJS (dual build)
1414
- Zero runtime deps (uses native `fetch` — bring a polyfill if your runtime lacks it)
1515
- Realtime updates via Server-Sent Events (SSE)
16+
- Context-based override evaluation (feature flags, A/B testing, gradual rollouts)
1617
- Tiny bundle footprint
1718
- Strong TypeScript types
1819

@@ -39,86 +40,85 @@ const client = createReplaneClient({
3940
baseUrl: "https://api.my-replane-host.com",
4041
});
4142

42-
// One-off fetch
43+
// Watch a config (receives realtime updates via SSE)
44+
const featureFlag = await client.watchConfig<boolean>("new-onboarding");
4345

44-
const featureFlag = await client
45-
.getConfigValue<boolean>("new-onboarding")
46-
// Ignore errors and use `false` if config is missing or fetch fails
47-
.catch(() => false);
46+
// Get the current value
47+
if (featureFlag.getValue()) {
48+
console.log("New onboarding enabled!");
49+
}
4850

4951
// Typed example
5052
interface PasswordRequirements {
5153
minLength: number;
5254
requireSymbol: boolean;
5355
}
5456

55-
const passwordRequirements = await client
56-
.getConfigValue<PasswordRequirements>("password-requirements")
57-
.catch(() => ({ minLength: 8, requireSymbol: false }));
58-
59-
// Watching a config (initial fetch must succeed)
60-
const billingEnabled = await client.watchConfigValue<boolean>(
61-
"billing-enabled"
57+
const passwordReqs = await client.watchConfig<PasswordRequirements>(
58+
"password-requirements"
6259
);
6360

64-
// Later, read the latest value
65-
if (billingEnabled.get()) {
66-
console.log("Billing enabled!");
67-
}
61+
// Read value anytime (always returns the latest from realtime updates)
62+
const { minLength } = passwordReqs.getValue();
63+
64+
// With context for override evaluation
65+
const billingEnabled = await client.watchConfig<boolean>("billing-enabled");
66+
67+
// Evaluate with user context - overrides apply automatically
68+
const enabled = billingEnabled.getValue({
69+
userId: "user-123",
70+
plan: "premium",
71+
region: "us-east",
72+
});
6873

6974
// When done, clean up resources
75+
featureFlag.close();
76+
passwordReqs.close();
7077
billingEnabled.close();
7178

72-
// Or, if you don't need the client anymore
79+
// Or close all watchers at once
7380
client.close();
7481
```
7582

7683
## API
7784

7885
### `createReplaneClient(options)`
7986

80-
Returns an object: `{ getConfigValue, watchConfigValue, close }`.
87+
Returns an object: `{ watchConfig, close }`.
8188

82-
`close()` stops all active watchers created by this client and marks the client as closed. After calling it, any subsequent call to `getConfigValue` or `watchConfigValue` will throw. It is safe to call multiple times (no‑op after the first call).
89+
`close()` stops all active watchers created by this client and marks the client as closed. After calling it, any subsequent call to `watchConfig` will throw. It is safe to call multiple times (no‑op after the first call).
8390

8491
#### Options
8592

8693
- `baseUrl` (string) – API origin (no trailing slash needed).
8794
- `apiKey` (string) – API key for authorization. Required. **Note:** Each API key is tied to a specific project and can only access configs from that project. To access configs from multiple projects, create multiple API keys and initialize separate client instances.
88-
- `fetchFn` (function) – custom fetch (e.g. `undici.fetch` or mocked fetch in tests).
95+
- `context` (object) – default context for all config evaluations. Can be overridden per-request in `watcher.getValue()`. Optional.
96+
- `fetchFn` (function) – custom fetch (e.g. `undici.fetch` or mocked fetch in tests). Optional.
8997
- `timeoutMs` (number) – abort the request after N ms. Default: 2000.
9098
- `retries` (number) – number of retry attempts on failures (5xx or network errors). Default: 2.
91-
- `retryDelayMs` (number) – base delay between retries in ms (a small jitter is applied). Default: 100.
92-
93-
### `client.getConfigValue(name, overrides?)`
94-
95-
Parameters:
99+
- `retryDelayMs` (number) – base delay between retries in ms (a small jitter is applied). Default: 200.
100+
- `logger` (object) – custom logger with `debug`, `info`, `warn`, `error` methods. Default: `console`.
96101

97-
- `name` (string) – config name to fetch.
98-
- Overrides: same semantics as in `createReplaneClient`.
102+
### `client.watchConfig(name, options?)`
99103

100-
Returns: a promise resolving to the parsed JSON value.
101-
102-
Errors: throws on non-2xx responses (including 404 for missing configs), network errors, or invalid JSON. Catch `ReplaneError` to handle failures.
103-
104-
Retry behavior:
104+
Creates a lightweight watcher that receives realtime updates for the config value via Server-Sent Events (SSE). Useful for long‑lived processes wanting instant updates without manually refetching.
105105

106-
- Transient failures (5xx responses or network errors) are retried up to `retries` times with a base delay of `retryDelayMs` between attempts.
107-
- You can override these per call via the `overrides` argument.
106+
Parameters:
108107

109-
### `client.watchConfigValue(name, overrides?)`
108+
- `name` (string) – config name to watch.
109+
- `options` (object) – optional configuration:
110+
- `context` (object) – context merged with client-level context for override evaluation.
110111

111-
Creates a lightweight watcher that receives realtime updates for the config value via Server-Sent Events (SSE). Useful for long‑lived processes wanting instant updates without manually refetching.
112+
Returns a promise resolving to an object: `{ getValue(context?): T, close(): void }`.
112113

113-
Returns a promise resolving to an object: `{ get(): T, close(): void }`.
114-
115-
- `get()` – returns the most recent value.
116-
- `close()` – stops watching for updates. Further calls to `get()` after `close()` throw.
114+
- `getValue(context?)` – returns the current value with override evaluation based on provided context (merged with client and watcher contexts). The value is always up-to-date thanks to realtime SSE updates.
115+
- `close()` – stops watching for updates. Further calls to `getValue()` after `close()` throw.
117116

118117
Notes:
119118

120119
- The initial fetch must succeed (it will throw on errors).
121120
- Subsequent updates are pushed from the server in realtime via SSE.
121+
- Values are automatically refreshed every 60 seconds as a fallback.
122122

123123
#### Watcher lifecycle
124124

@@ -129,10 +129,18 @@ Notes:
129129
Example:
130130

131131
```ts
132-
const billingEnabled = await client.watchConfigValue("billing-enabled");
133-
if (billingEnabled.get()) {
132+
const billingEnabled = await client.watchConfig<boolean>("billing-enabled");
133+
134+
// Get value without context values
135+
if (billingEnabled.getValue()) {
134136
// ...
135137
}
138+
139+
// Get value with context for override evaluation
140+
if (billingEnabled.getValue({ userId: "user-123", plan: "premium" })) {
141+
// ...
142+
}
143+
136144
// Later, when you no longer need updates:
137145
billingEnabled.close();
138146
```
@@ -145,13 +153,13 @@ Parameters:
145153

146154
- `initialData` (object) – map of config name to value.
147155

148-
Returns the same client shape as `createReplaneClient` (`{ getConfigValue, watchConfigValue, close }`).
156+
Returns the same client shape as `createReplaneClient` (`{ watchConfig, close }`).
149157

150158
Notes:
151159

152-
- `getConfigValue(name)` resolves to the value from `initialData`.
160+
- `watchConfig(name)` resolves to a watcher with the value from `initialData`.
153161
- If a name is missing, it throws a `ReplaneError` (`Config not found: <name>`).
154-
- `watchConfigValue` works as usual, but uses periodic refresh (every 60s) instead of SSE since there's no server connection (values remain whatever is in-memory).
162+
- Watchers work as usual but don't receive SSE updates (values remain whatever is in-memory).
155163

156164
Example:
157165

@@ -160,13 +168,19 @@ import { createInMemoryReplaneClient } from "replane-sdk";
160168

161169
const client = createInMemoryReplaneClient({
162170
"feature-a": true,
163-
"max-items": { value: 10, updatedAt: Date.now() },
171+
"max-items": { value: 10, ttl: 3600 },
164172
});
165173

166-
const enabled = await client.getConfigValue<boolean>("feature-a"); // true
167-
const watcher = await client.watchConfigValue<number>("max-items");
168-
watcher.get(); // { value: 10, updatedAt: ... }
169-
watcher.close();
174+
const featureA = await client.watchConfig<boolean>("feature-a");
175+
console.log(featureA.getValue()); // true
176+
177+
const maxItems = await client.watchConfig<{ value: number; ttl: number }>(
178+
"max-items"
179+
);
180+
console.log(maxItems.getValue()); // { value: 10, ttl: 3600 }
181+
182+
featureA.close();
183+
maxItems.close();
170184
```
171185

172186
### `client.close()`
@@ -180,7 +194,9 @@ client.close();
180194

181195
### Errors
182196

183-
`getConfigValue` throws on non‑2xx HTTP responses (including 404), network errors, and invalid JSON. `watchConfigValue` uses `getConfigValue` for its initial fetch; handle errors accordingly with try/catch when creating a watcher. A `ReplaneError` is thrown for HTTP failures; other errors may be thrown for network/parse issues.
197+
`watchConfig` throws on non‑2xx HTTP responses (including 404), network errors, and invalid JSON during the initial fetch. Handle errors with try/catch when creating a watcher. A `ReplaneError` is thrown for HTTP failures; other errors may be thrown for network/parse issues.
198+
199+
After the initial fetch succeeds, subsequent SSE update errors are logged but don't throw (the watcher continues to work with the last known value).
184200

185201
## Environment notes
186202

@@ -189,23 +205,49 @@ client.close();
189205

190206
## Common patterns
191207

192-
Typed config:
208+
### Typed config
193209

194210
```ts
195211
interface LayoutConfig {
196212
variant: "a" | "b";
197213
ttl: number;
198214
}
199-
const layout = await client.getConfigValue<LayoutConfig>("layout");
215+
const layout = await client.watchConfig<LayoutConfig>("layout");
216+
console.log(layout.getValue()); // { variant: "a", ttl: 3600 }
200217
```
201218

202-
Timeout override:
219+
### Context-based overrides
203220

204221
```ts
205-
await client.getConfigValue("slow-config", { timeoutMs: 3000 });
222+
// Config has base value `false` but override: if `plan === "premium"` then `true`
223+
const featureWatcher = await client.watchConfig<boolean>("advanced-features");
224+
225+
// Free user
226+
const freeUserEnabled = featureWatcher.getValue({ plan: "free" }); // false
227+
228+
// Premium user
229+
const premiumUserEnabled = featureWatcher.getValue({ plan: "premium" }); // true
230+
```
231+
232+
### Client-level context
233+
234+
```ts
235+
const client = createReplaneClient({
236+
apiKey: process.env.REPLANE_API_KEY!,
237+
baseUrl: "https://api.my-replane-host.com",
238+
context: {
239+
environment: "production",
240+
region: "us-east",
241+
},
242+
});
243+
244+
// This context is used for all watchers unless overridden
245+
const watcher = await client.watchConfig("feature-flag");
246+
watcher.getValue(); // Uses client-level context
247+
watcher.getValue({ userId: "123" }); // Merges with client context
206248
```
207249

208-
Custom fetch (tests):
250+
### Custom fetch (tests)
209251

210252
```ts
211253
const client = createReplaneClient({
@@ -215,7 +257,7 @@ const client = createReplaneClient({
215257
});
216258
```
217259

218-
Multiple projects:
260+
### Multiple projects
219261

220262
```ts
221263
// Each project needs its own API key and client instance
@@ -230,8 +272,8 @@ const projectBClient = createReplaneClient({
230272
});
231273

232274
// Each client only accesses configs from its respective project
233-
const featureA = await projectAClient.getConfigValue("feature-flag");
234-
const featureB = await projectBClient.getConfigValue("feature-flag");
275+
const featureA = await projectAClient.watchConfig("feature-flag");
276+
const featureB = await projectBClient.watchConfig("feature-flag");
235277
```
236278

237279
## Roadmap

0 commit comments

Comments
 (0)