@replanejs/sdk 0.5.6

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.cjs ADDED
@@ -0,0 +1,565 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var src_exports = {};
20
+ __export(src_exports, {
21
+ ReplaneError: () => ReplaneError,
22
+ createInMemoryReplaneClient: () => createInMemoryReplaneClient,
23
+ createReplaneClient: () => createReplaneClient
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+ const SUPPORTED_REPLICATION_STREAM_RECORD_TYPES = Object.keys({
27
+ config_change: true,
28
+ init: true
29
+ });
30
+ function fnv1a32(input) {
31
+ const encoder = new TextEncoder();
32
+ const bytes = encoder.encode(input);
33
+ let hash = 2166136261 >>> 0;
34
+ for (let i = 0; i < bytes.length; i++) {
35
+ hash ^= bytes[i];
36
+ hash = Math.imul(hash, 16777619) >>> 0;
37
+ }
38
+ return hash >>> 0;
39
+ }
40
+ function fnv1a32ToUnit(input) {
41
+ const h = fnv1a32(input);
42
+ return h / 2 ** 32;
43
+ }
44
+ function evaluateOverrides(baseValue, overrides, context, logger) {
45
+ for (const override of overrides) {
46
+ let overrideResult = "matched";
47
+ const results = override.conditions.map((c) => evaluateCondition(c, context, logger));
48
+ if (results.some((r) => r === "not_matched")) {
49
+ overrideResult = "not_matched";
50
+ } else if (results.some((r) => r === "unknown")) {
51
+ overrideResult = "unknown";
52
+ }
53
+ if (overrideResult === "matched") {
54
+ return override.value;
55
+ }
56
+ }
57
+ return baseValue;
58
+ }
59
+ function evaluateCondition(condition, context, logger) {
60
+ const operator = condition.operator;
61
+ if (operator === "and") {
62
+ const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));
63
+ if (results.some((r) => r === "not_matched")) return "not_matched";
64
+ if (results.some((r) => r === "unknown")) return "unknown";
65
+ return "matched";
66
+ }
67
+ if (operator === "or") {
68
+ const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));
69
+ if (results.some((r) => r === "matched")) return "matched";
70
+ if (results.some((r) => r === "unknown")) return "unknown";
71
+ return "not_matched";
72
+ }
73
+ if (operator === "not") {
74
+ const result = evaluateCondition(condition.condition, context, logger);
75
+ if (result === "matched") return "not_matched";
76
+ if (result === "not_matched") return "matched";
77
+ return "unknown";
78
+ }
79
+ if (operator === "segmentation") {
80
+ const contextValue2 = context[condition.property];
81
+ if (contextValue2 === void 0 || contextValue2 === null) {
82
+ return "unknown";
83
+ }
84
+ const hashInput = String(contextValue2) + condition.seed;
85
+ const unitValue = fnv1a32ToUnit(hashInput);
86
+ return unitValue >= condition.fromPercentage / 100 && unitValue < condition.toPercentage / 100 ? "matched" : "not_matched";
87
+ }
88
+ const property = condition.property;
89
+ const contextValue = context[property];
90
+ const expectedValue = condition.value;
91
+ if (contextValue === void 0) {
92
+ return "unknown";
93
+ }
94
+ const castedValue = castToContextType(expectedValue, contextValue);
95
+ switch (operator) {
96
+ case "equals":
97
+ return contextValue === castedValue ? "matched" : "not_matched";
98
+ case "in":
99
+ if (!Array.isArray(castedValue)) return "unknown";
100
+ return castedValue.includes(contextValue) ? "matched" : "not_matched";
101
+ case "not_in":
102
+ if (!Array.isArray(castedValue)) return "unknown";
103
+ return !castedValue.includes(contextValue) ? "matched" : "not_matched";
104
+ case "less_than":
105
+ if (typeof contextValue === "number" && typeof castedValue === "number") {
106
+ return contextValue < castedValue ? "matched" : "not_matched";
107
+ }
108
+ if (typeof contextValue === "string" && typeof castedValue === "string") {
109
+ return contextValue < castedValue ? "matched" : "not_matched";
110
+ }
111
+ return "not_matched";
112
+ case "less_than_or_equal":
113
+ if (typeof contextValue === "number" && typeof castedValue === "number") {
114
+ return contextValue <= castedValue ? "matched" : "not_matched";
115
+ }
116
+ if (typeof contextValue === "string" && typeof castedValue === "string") {
117
+ return contextValue <= castedValue ? "matched" : "not_matched";
118
+ }
119
+ return "not_matched";
120
+ case "greater_than":
121
+ if (typeof contextValue === "number" && typeof castedValue === "number") {
122
+ return contextValue > castedValue ? "matched" : "not_matched";
123
+ }
124
+ if (typeof contextValue === "string" && typeof castedValue === "string") {
125
+ return contextValue > castedValue ? "matched" : "not_matched";
126
+ }
127
+ return "not_matched";
128
+ case "greater_than_or_equal":
129
+ if (typeof contextValue === "number" && typeof castedValue === "number") {
130
+ return contextValue >= castedValue ? "matched" : "not_matched";
131
+ }
132
+ if (typeof contextValue === "string" && typeof castedValue === "string") {
133
+ return contextValue >= castedValue ? "matched" : "not_matched";
134
+ }
135
+ return "not_matched";
136
+ default:
137
+ warnNever(operator, logger, `Unexpected operator: ${operator}`);
138
+ return "unknown";
139
+ }
140
+ }
141
+ function warnNever(value, logger, message) {
142
+ logger.warn(message, { value });
143
+ }
144
+ function castToContextType(expectedValue, contextValue) {
145
+ if (typeof contextValue === "number") {
146
+ if (typeof expectedValue === "string") {
147
+ const num = Number(expectedValue);
148
+ return isNaN(num) ? expectedValue : num;
149
+ }
150
+ return expectedValue;
151
+ }
152
+ if (typeof contextValue === "boolean") {
153
+ if (typeof expectedValue === "string") {
154
+ if (expectedValue === "true") return true;
155
+ if (expectedValue === "false") return false;
156
+ }
157
+ if (typeof expectedValue === "number") {
158
+ return expectedValue !== 0;
159
+ }
160
+ return expectedValue;
161
+ }
162
+ if (typeof contextValue === "string") {
163
+ if (typeof expectedValue === "number" || typeof expectedValue === "boolean") {
164
+ return String(expectedValue);
165
+ }
166
+ return expectedValue;
167
+ }
168
+ return expectedValue;
169
+ }
170
+ async function delay(ms) {
171
+ return new Promise((resolve) => setTimeout(resolve, ms));
172
+ }
173
+ async function retryDelay(averageDelay) {
174
+ const jitter = averageDelay / 5;
175
+ const delayMs = averageDelay + Math.random() * jitter - jitter / 2;
176
+ await delay(delayMs);
177
+ }
178
+ class ReplaneRemoteStorage {
179
+ closeController = new AbortController();
180
+ // never throws
181
+ async *startReplicationStream(options) {
182
+ const { signal, cleanUpSignals } = combineAbortSignals([
183
+ this.closeController.signal,
184
+ options.signal
185
+ ]);
186
+ try {
187
+ let failedAttempts = 0;
188
+ while (!signal.aborted) {
189
+ try {
190
+ for await (const event of this.startReplicationStreamImpl({
191
+ ...options,
192
+ signal,
193
+ onConnect: () => {
194
+ failedAttempts = 0;
195
+ }
196
+ })) {
197
+ yield event;
198
+ }
199
+ } catch (error) {
200
+ failedAttempts++;
201
+ const retryDelayMs = Math.min(options.retryDelayMs * 2 ** (failedAttempts - 1), 1e4);
202
+ if (!signal.aborted) {
203
+ options.logger.error(
204
+ `Failed to fetch project events, retrying in ${retryDelayMs}ms...`,
205
+ error
206
+ );
207
+ await retryDelay(retryDelayMs);
208
+ }
209
+ }
210
+ }
211
+ } finally {
212
+ cleanUpSignals();
213
+ }
214
+ }
215
+ async *startReplicationStreamImpl(options) {
216
+ const rawEvents = fetchSse({
217
+ fetchFn: options.fetchFn,
218
+ headers: {
219
+ Authorization: this.getAuthHeader(options),
220
+ "Content-Type": "application/json"
221
+ },
222
+ body: JSON.stringify(options.getBody()),
223
+ timeoutMs: options.requestTimeoutMs,
224
+ method: "POST",
225
+ signal: options.signal,
226
+ url: this.getApiEndpoint(`/sdk/v1/replication/stream`, options),
227
+ onConnect: options.onConnect
228
+ });
229
+ for await (const rawEvent of rawEvents) {
230
+ const event = JSON.parse(rawEvent);
231
+ if (typeof event === "object" && event !== null && "type" in event && typeof event.type === "string" && SUPPORTED_REPLICATION_STREAM_RECORD_TYPES.includes(event.type)) {
232
+ yield event;
233
+ }
234
+ }
235
+ }
236
+ close() {
237
+ this.closeController.abort();
238
+ }
239
+ getAuthHeader(options) {
240
+ return `Bearer ${options.sdkKey}`;
241
+ }
242
+ getApiEndpoint(path, options) {
243
+ return `${options.baseUrl}/api${path}`;
244
+ }
245
+ }
246
+ var ReplaneErrorCode = /* @__PURE__ */ ((ReplaneErrorCode2) => {
247
+ ReplaneErrorCode2["NotFound"] = "not_found";
248
+ ReplaneErrorCode2["Timeout"] = "timeout";
249
+ ReplaneErrorCode2["NetworkError"] = "network_error";
250
+ ReplaneErrorCode2["AuthError"] = "auth_error";
251
+ ReplaneErrorCode2["Forbidden"] = "forbidden";
252
+ ReplaneErrorCode2["ServerError"] = "server_error";
253
+ ReplaneErrorCode2["ClientError"] = "client_error";
254
+ ReplaneErrorCode2["Closed"] = "closed";
255
+ ReplaneErrorCode2["NotInitialized"] = "not_initialized";
256
+ ReplaneErrorCode2["Unknown"] = "unknown";
257
+ return ReplaneErrorCode2;
258
+ })(ReplaneErrorCode || {});
259
+ class ReplaneError extends Error {
260
+ code;
261
+ constructor(params) {
262
+ super(params.message, { cause: params.cause });
263
+ this.name = "ReplaneError";
264
+ this.code = params.code;
265
+ }
266
+ }
267
+ async function createReplaneClient(sdkOptions) {
268
+ const storage = new ReplaneRemoteStorage();
269
+ return await _createReplaneClient(toFinalOptions(sdkOptions), storage);
270
+ }
271
+ function createInMemoryReplaneClient(initialData) {
272
+ return {
273
+ getConfig: (configName) => {
274
+ const config = initialData[configName];
275
+ if (config === void 0) {
276
+ throw new ReplaneError({
277
+ message: `Config not found: ${String(configName)}`,
278
+ code: "not_found" /* NotFound */
279
+ });
280
+ }
281
+ return config;
282
+ },
283
+ close: () => {
284
+ }
285
+ };
286
+ }
287
+ async function _createReplaneClient(sdkOptions, storage) {
288
+ if (!sdkOptions.sdkKey) throw new Error("SDK key is required");
289
+ let configs = new Map(
290
+ sdkOptions.fallbacks.map((config) => [config.name, config])
291
+ );
292
+ const clientReady = new Deferred();
293
+ (async () => {
294
+ try {
295
+ const replicationStream = storage.startReplicationStream({
296
+ ...sdkOptions,
297
+ getBody: () => ({
298
+ currentConfigs: [...configs.values()].map((config) => ({
299
+ name: config.name,
300
+ overrides: config.overrides,
301
+ version: config.version,
302
+ value: config.value
303
+ })),
304
+ requiredConfigs: sdkOptions.requiredConfigs
305
+ })
306
+ });
307
+ for await (const event of replicationStream) {
308
+ if (event.type === "init") {
309
+ configs = new Map(event.configs.map((config) => [config.name, config]));
310
+ clientReady.resolve();
311
+ } else if (event.type === "config_change") {
312
+ configs.set(event.configName, {
313
+ name: event.configName,
314
+ overrides: event.overrides,
315
+ version: event.version,
316
+ value: event.value
317
+ });
318
+ } else {
319
+ warnNever(event, sdkOptions.logger, "Replane: unknown event type in event stream");
320
+ }
321
+ }
322
+ } catch (error) {
323
+ sdkOptions.logger.error("Replane: error initializing client:", error);
324
+ clientReady.reject(error);
325
+ }
326
+ })();
327
+ function getConfig(configName, getConfigOptions = {}) {
328
+ const config = configs.get(String(configName));
329
+ if (config === void 0) {
330
+ throw new ReplaneError({
331
+ message: `Config not found: ${String(configName)}`,
332
+ code: "not_found" /* NotFound */
333
+ });
334
+ }
335
+ try {
336
+ return evaluateOverrides(
337
+ config.value,
338
+ config.overrides,
339
+ { ...sdkOptions.context, ...getConfigOptions?.context ?? {} },
340
+ sdkOptions.logger
341
+ );
342
+ } catch (error) {
343
+ sdkOptions.logger.error(
344
+ `Replane: error evaluating overrides for config ${String(configName)}:`,
345
+ error
346
+ );
347
+ return config.value;
348
+ }
349
+ }
350
+ const close = () => storage.close();
351
+ const initializationTimeoutId = setTimeout(() => {
352
+ if (sdkOptions.fallbacks.length === 0) {
353
+ close();
354
+ clientReady.reject(
355
+ new ReplaneError({
356
+ message: "Replane client initialization timed out",
357
+ code: "timeout" /* Timeout */
358
+ })
359
+ );
360
+ return;
361
+ }
362
+ const missingRequiredConfigs = [];
363
+ for (const requiredConfigName of sdkOptions.requiredConfigs) {
364
+ if (!configs.has(requiredConfigName)) {
365
+ missingRequiredConfigs.push(requiredConfigName);
366
+ }
367
+ }
368
+ if (missingRequiredConfigs.length > 0) {
369
+ close();
370
+ clientReady.reject(
371
+ new ReplaneError({
372
+ message: `Required configs are missing: ${missingRequiredConfigs.join(", ")}`,
373
+ code: "not_found" /* NotFound */
374
+ })
375
+ );
376
+ return;
377
+ }
378
+ clientReady.resolve();
379
+ }, sdkOptions.sdkInitializationTimeoutMs);
380
+ clientReady.promise.then(() => clearTimeout(initializationTimeoutId));
381
+ await clientReady.promise;
382
+ return {
383
+ getConfig,
384
+ close
385
+ };
386
+ }
387
+ function toFinalOptions(defaults) {
388
+ return {
389
+ sdkKey: defaults.sdkKey,
390
+ baseUrl: defaults.baseUrl.replace(/\/+$/, ""),
391
+ fetchFn: defaults.fetchFn ?? // some browsers require binding the fetch function to window
392
+ globalThis.fetch.bind(globalThis),
393
+ requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
394
+ sdkInitializationTimeoutMs: defaults.sdkInitializationTimeoutMs ?? 5e3,
395
+ logger: defaults.logger ?? console,
396
+ retryDelayMs: defaults.retryDelayMs ?? 200,
397
+ context: {
398
+ ...defaults.context ?? {}
399
+ },
400
+ requiredConfigs: Array.isArray(defaults.required) ? defaults.required.map((name) => String(name)) : Object.entries(defaults.required ?? {}).filter(([_, value]) => value !== void 0).map(([name]) => name),
401
+ fallbacks: Object.entries(defaults.fallbacks ?? {}).filter(([_, value]) => value !== void 0).map(([name, value]) => ({
402
+ name,
403
+ overrides: [],
404
+ version: -1,
405
+ value
406
+ }))
407
+ };
408
+ }
409
+ async function fetchWithTimeout(input, init, timeoutMs, fetchFn) {
410
+ if (!fetchFn) {
411
+ throw new Error("Global fetch is not available. Provide options.fetchFn.");
412
+ }
413
+ if (!timeoutMs) return fetchFn(input, init);
414
+ const timeoutController = new AbortController();
415
+ const t = setTimeout(() => timeoutController.abort(), timeoutMs);
416
+ const { signal } = combineAbortSignals([init.signal, timeoutController.signal]);
417
+ try {
418
+ return await fetchFn(input, {
419
+ ...init,
420
+ signal
421
+ });
422
+ } finally {
423
+ clearTimeout(t);
424
+ }
425
+ }
426
+ const SSE_DATA_PREFIX = "data:";
427
+ async function* fetchSse(params) {
428
+ const abortController = new AbortController();
429
+ const { signal, cleanUpSignals } = params.signal ? combineAbortSignals([params.signal, abortController.signal]) : { signal: abortController.signal, cleanUpSignals: () => {
430
+ } };
431
+ try {
432
+ const res = await fetchWithTimeout(
433
+ params.url,
434
+ {
435
+ method: params.method ?? "GET",
436
+ headers: { Accept: "text/event-stream", ...params.headers ?? {} },
437
+ body: params.body,
438
+ signal
439
+ },
440
+ params.timeoutMs,
441
+ params.fetchFn
442
+ );
443
+ await ensureSuccessfulResponse(res, `SSE ${params.url}`);
444
+ const responseContentType = res.headers.get("content-type") ?? "";
445
+ if (!responseContentType.includes("text/event-stream")) {
446
+ throw new ReplaneError({
447
+ message: `Expected text/event-stream, got "${responseContentType}"`,
448
+ code: "server_error" /* ServerError */
449
+ });
450
+ }
451
+ if (!res.body) {
452
+ throw new ReplaneError({
453
+ message: `Failed to fetch SSE ${params.url}: body is empty`,
454
+ code: "unknown" /* Unknown */
455
+ });
456
+ }
457
+ if (params.onConnect) {
458
+ params.onConnect();
459
+ }
460
+ const decoded = res.body.pipeThrough(new TextDecoderStream());
461
+ const reader = decoded.getReader();
462
+ let buffer = "";
463
+ try {
464
+ while (true) {
465
+ const { value, done } = await reader.read();
466
+ if (done) break;
467
+ buffer += value;
468
+ const frames = buffer.split(/\r?\n\r?\n/);
469
+ buffer = frames.pop() ?? "";
470
+ for (const frame of frames) {
471
+ const dataLines = [];
472
+ for (const rawLine of frame.split(/\r?\n/)) {
473
+ if (!rawLine) continue;
474
+ if (rawLine.startsWith(":")) continue;
475
+ if (rawLine.startsWith(SSE_DATA_PREFIX)) {
476
+ const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\s/, "");
477
+ dataLines.push(line);
478
+ }
479
+ }
480
+ if (dataLines.length) {
481
+ const payload = dataLines.join("\n");
482
+ yield payload;
483
+ }
484
+ }
485
+ }
486
+ } finally {
487
+ try {
488
+ await reader.cancel();
489
+ } catch {
490
+ }
491
+ abortController.abort();
492
+ }
493
+ } finally {
494
+ cleanUpSignals();
495
+ }
496
+ }
497
+ async function ensureSuccessfulResponse(response, message) {
498
+ if (response.status === 404) {
499
+ throw new ReplaneError({
500
+ message: `Not found: ${message}`,
501
+ code: "not_found" /* NotFound */
502
+ });
503
+ }
504
+ if (response.status === 401) {
505
+ throw new ReplaneError({
506
+ message: `Unauthorized access: ${message}`,
507
+ code: "auth_error" /* AuthError */
508
+ });
509
+ }
510
+ if (response.status === 403) {
511
+ throw new ReplaneError({
512
+ message: `Forbidden access: ${message}`,
513
+ code: "forbidden" /* Forbidden */
514
+ });
515
+ }
516
+ if (!response.ok) {
517
+ let body;
518
+ try {
519
+ body = await response.text();
520
+ } catch {
521
+ body = "<unable to read response body>";
522
+ }
523
+ const code = response.status >= 500 ? "server_error" /* ServerError */ : response.status >= 400 ? "client_error" /* ClientError */ : "unknown" /* Unknown */;
524
+ throw new ReplaneError({
525
+ message: `Fetch response isn't successful (${message}): ${response.status} ${response.statusText} - ${body}`,
526
+ code
527
+ });
528
+ }
529
+ }
530
+ function combineAbortSignals(signals) {
531
+ const controller = new AbortController();
532
+ const onAbort = () => {
533
+ controller.abort();
534
+ cleanUpSignals();
535
+ };
536
+ const cleanUpSignals = () => {
537
+ for (const s of signals) {
538
+ s?.removeEventListener("abort", onAbort);
539
+ }
540
+ };
541
+ for (const s of signals) {
542
+ s?.addEventListener("abort", onAbort, { once: true });
543
+ }
544
+ if (signals.some((s) => s?.aborted)) {
545
+ onAbort();
546
+ }
547
+ return { signal: controller.signal, cleanUpSignals };
548
+ }
549
+ class Deferred {
550
+ promise;
551
+ resolve;
552
+ reject;
553
+ constructor() {
554
+ this.promise = new Promise((resolve, reject) => {
555
+ this.resolve = resolve;
556
+ this.reject = reject;
557
+ });
558
+ }
559
+ }
560
+ // Annotate the CommonJS export names for ESM import in node:
561
+ 0 && (module.exports = {
562
+ ReplaneError,
563
+ createInMemoryReplaneClient,
564
+ createReplaneClient
565
+ });
@@ -0,0 +1,118 @@
1
+ //#region src/index.d.ts
2
+ type Configs = object;
3
+ type ReplaneContext = Record<string, unknown>;
4
+ interface ReplaneClientOptions<T extends Configs> {
5
+ /**
6
+ * Base URL of the Replane API (no trailing slash).
7
+ */
8
+ baseUrl: string;
9
+ /**
10
+ * Project SDK key for authorization.
11
+ */
12
+ sdkKey: string;
13
+ /**
14
+ * Custom fetch implementation (useful for tests / polyfills).
15
+ */
16
+ fetchFn?: typeof fetch;
17
+ /**
18
+ * Optional timeout in ms for the request.
19
+ * @default 2000
20
+ */
21
+ requestTimeoutMs?: number;
22
+ /**
23
+ * Optional timeout in ms for the SDK initialization.
24
+ * @default 5000
25
+ */
26
+ sdkInitializationTimeoutMs?: number;
27
+ /**
28
+ * Delay between retries in ms.
29
+ * @default 100
30
+ */
31
+ retryDelayMs?: number;
32
+ /**
33
+ * Optional logger (defaults to console).
34
+ */
35
+ logger?: ReplaneLogger;
36
+ /**
37
+ * Default context for all config evaluations.
38
+ * Can be overridden per-request in `client.watchConfig()` and `watcher.getValue()`.
39
+ */
40
+ context?: ReplaneContext;
41
+ /**
42
+ * Required configs for the client.
43
+ * If a config is not present, the client will throw an error during initialization.
44
+ * @example
45
+ * {
46
+ * required: {
47
+ * config1: true,
48
+ * config2: true,
49
+ * config3: false,
50
+ * },
51
+ * }
52
+ *
53
+ * @example
54
+ * {
55
+ * required: ["config1", "config2", "config3"],
56
+ * }
57
+ */
58
+ required?: { [K in keyof T]: boolean } | Array<keyof T>;
59
+ /**
60
+ * Fallback configs to use if the initial request to fetch configs fails.
61
+ * @example
62
+ * {
63
+ * fallbacks: {
64
+ * config1: "value1",
65
+ * config2: 42,
66
+ * },
67
+ * }
68
+ */
69
+ fallbacks?: { [K in keyof T]: T[K] };
70
+ }
71
+ interface ReplaneLogger {
72
+ debug(...args: unknown[]): void;
73
+ info(...args: unknown[]): void;
74
+ warn(...args: unknown[]): void;
75
+ error(...args: unknown[]): void;
76
+ }
77
+ interface GetConfigOptions {
78
+ /**
79
+ * Context for override evaluation (merged with client-level context).
80
+ */
81
+ context?: ReplaneContext;
82
+ }
83
+ interface ConfigWatcher<T> {
84
+ /** Current config value (or fallback if not found). */
85
+ getValue(context?: ReplaneContext): T;
86
+ /** Stop watching for changes. */
87
+ close(): void;
88
+ }
89
+ interface ReplaneClient<T extends Configs> {
90
+ /** Get a config by its name. */
91
+ getConfig<K extends keyof T>(configName: K, options?: GetConfigOptions): T[K];
92
+ /** Close the client and clean up resources. */
93
+ close(): void;
94
+ }
95
+ declare class ReplaneError extends Error {
96
+ code: string;
97
+ constructor(params: {
98
+ message: string;
99
+ code: string;
100
+ cause?: unknown;
101
+ });
102
+ }
103
+ /**
104
+ * Create a Replane client bound to an SDK key.
105
+ * Usage:
106
+ * const client = await createReplaneClient({ sdkKey: 'your-sdk-key', baseUrl: 'https://app.replane.dev' })
107
+ * const value = client.getConfig('my-config')
108
+ */
109
+ declare function createReplaneClient<T extends Configs = Record<string, unknown>>(sdkOptions: ReplaneClientOptions<T>): Promise<ReplaneClient<T>>;
110
+ /**
111
+ * Create a Replane client that uses in-memory storage.
112
+ * Usage:
113
+ * const client = createInMemoryReplaneClient({ 'my-config': 123 })
114
+ * const value = client.getConfig('my-config') // 123
115
+ */
116
+ declare function createInMemoryReplaneClient<T extends Configs = Record<string, unknown>>(initialData: T): ReplaneClient<T>;
117
+ //#endregion
118
+ export { ConfigWatcher, GetConfigOptions, ReplaneClient, ReplaneClientOptions, ReplaneContext, ReplaneError, ReplaneLogger, createInMemoryReplaneClient, createReplaneClient };