@replanejs/sdk 0.5.12 → 0.6.2
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 +10 -609
- package/dist/index.d.ts +201 -46
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +569 -345
- package/dist/index.js.map +1 -0
- package/package.json +6 -13
- package/LICENSE +0 -21
- package/README.md +0 -479
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["params: { message: string; code: string; cause?: unknown }","ms: number","averageDelay: number","signals: Array<AbortSignal | undefined | null>","input: string | URL | Request","init: RequestInit","timeoutMs: number","fetchFn: typeof fetch","response: Response","message: string","body: unknown","params: FetchSseOptions","dataLines: string[]","comment: string | null","options: StartReplicationStreamOptions","error: unknown","inactivityTimer: ReturnType<typeof setTimeout> | null","options: ReplaneFinalOptions","path: string","input: string","baseValue: T","overrides: RenderedOverride[]","context: ReplaneContext","logger: ReplaneLogger","overrideResult: EvaluationResult","condition: RenderedCondition","contextValue","value: never","message: string","expectedValue: unknown","contextValue: unknown","options: ClientCoreOptions","configs: Map<string, ConfigDto>","updatedConfigs: ConfigDto[]","configName: K","getConfigOptions: GetConfigOptions","callbackOrConfigName: keyof T | ((config: MapConfig<T>) => void)","callbackOrUndefined?: (config: MapConfig<T>) => void","configName: keyof T | undefined","callback: (config: MapConfig<T>) => void","client: ReplaneClient<T>","sdkOptions: ReplaneClientOptions<T>","initialData: T","options: RestoreReplaneClientOptions<T>","initialConfigs: ConfigDto[]","storage: ReplaneRemoteStorage | null","streamOptions: ReplaneFinalOptions | null","sdkOptions: ReplaneFinalOptions","storage: ReplaneStorage","missingRequiredConfigs: string[]","defaults: ReplaneClientOptions<T>"],"sources":["../src/error.ts","../src/utils.ts","../src/sse.ts","../src/storage.ts","../src/hash.ts","../src/evaluation.ts","../src/client.ts"],"sourcesContent":["/**\n * Error codes for ReplaneError\n */\nexport enum ReplaneErrorCode {\n NotFound = \"not_found\",\n Timeout = \"timeout\",\n NetworkError = \"network_error\",\n AuthError = \"auth_error\",\n Forbidden = \"forbidden\",\n ServerError = \"server_error\",\n ClientError = \"client_error\",\n Closed = \"closed\",\n NotInitialized = \"not_initialized\",\n Unknown = \"unknown\",\n}\n\n/**\n * Custom error class for Replane SDK errors\n */\nexport class ReplaneError extends Error {\n code: string;\n\n constructor(params: { message: string; code: string; cause?: unknown }) {\n super(params.message, { cause: params.cause });\n this.name = \"ReplaneError\";\n this.code = params.code;\n }\n}\n","/**\n * Returns a promise that resolves after the specified delay\n *\n * @param ms - Delay in milliseconds\n */\nexport async function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Returns a promise that resolves after a delay with jitter.\n * The actual delay is the average delay ± 10% (jitter = averageDelay/5).\n *\n * @param averageDelay - The average delay in milliseconds\n */\nexport async function retryDelay(averageDelay: number): Promise<void> {\n const jitter = averageDelay / 5;\n const delayMs = averageDelay + Math.random() * jitter - jitter / 2;\n\n await delay(delayMs);\n}\n\n/**\n * Combines multiple abort signals into one.\n * When any of the input signals is aborted, the combined signal will also be aborted.\n *\n * @param signals - Array of AbortSignal instances (can contain undefined/null)\n * @returns An object containing the combined signal and a cleanup function\n */\nexport function combineAbortSignals(signals: Array<AbortSignal | undefined | null>): {\n signal: AbortSignal;\n cleanUpSignals: () => void;\n} {\n const controller = new AbortController();\n const onAbort = () => {\n controller.abort();\n cleanUpSignals();\n };\n\n const cleanUpSignals = () => {\n for (const s of signals) {\n s?.removeEventListener(\"abort\", onAbort);\n }\n };\n\n for (const s of signals) {\n s?.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n if (signals.some((s) => s?.aborted)) {\n onAbort();\n }\n\n return { signal: controller.signal, cleanUpSignals };\n}\n\n/**\n * A deferred promise that can be resolved or rejected from outside.\n * Useful for coordinating async operations.\n */\nexport class Deferred<T> {\n public readonly promise: Promise<T>;\n public resolve!: (value: T) => void;\n public reject!: (error: unknown) => void;\n\n constructor() {\n this.promise = new Promise((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n}\n","import { ReplaneError, ReplaneErrorCode } from \"./error\";\nimport { combineAbortSignals } from \"./utils\";\n\nconst SSE_DATA_PREFIX = \"data:\";\n\n/**\n * Parsed SSE event\n */\nexport type SseEvent = { type: \"comment\"; comment: string } | { type: \"data\"; data: string };\n\n/**\n * Options for fetchSse\n */\nexport interface FetchSseOptions {\n fetchFn: typeof fetch;\n url: string;\n timeoutMs: number;\n body?: string;\n headers?: Record<string, string>;\n method?: string;\n signal?: AbortSignal;\n onConnect?: () => void;\n}\n\n/**\n * Fetch with timeout support\n */\nexport async function fetchWithTimeout(\n input: string | URL | Request,\n init: RequestInit,\n timeoutMs: number,\n fetchFn: typeof fetch\n): Promise<Response> {\n if (!fetchFn) {\n throw new Error(\"Global fetch is not available. Provide options.fetchFn.\");\n }\n if (!timeoutMs) return fetchFn(input, init);\n\n const timeoutController = new AbortController();\n const t = setTimeout(() => timeoutController.abort(), timeoutMs);\n // Note: We intentionally don't call cleanUpSignals() here because for streaming\n // responses (like SSE), the connection remains open after the response headers\n // are received. The abort signal needs to remain connected so that close() can\n // propagate the abort through the signal chain.\n const { signal } = combineAbortSignals([init.signal, timeoutController.signal]);\n try {\n return await fetchFn(input, {\n ...init,\n signal,\n });\n } finally {\n clearTimeout(t);\n }\n}\n\n/**\n * Ensures the response is successful, throwing ReplaneError if not\n */\nexport async function ensureSuccessfulResponse(response: Response, message: string): Promise<void> {\n if (response.status === 404) {\n throw new ReplaneError({\n message: `Not found: ${message}`,\n code: ReplaneErrorCode.NotFound,\n });\n }\n\n if (response.status === 401) {\n throw new ReplaneError({\n message: `Unauthorized access: ${message}`,\n code: ReplaneErrorCode.AuthError,\n });\n }\n\n if (response.status === 403) {\n throw new ReplaneError({\n message: `Forbidden access: ${message}`,\n code: ReplaneErrorCode.Forbidden,\n });\n }\n\n if (!response.ok) {\n let body: unknown;\n try {\n body = await response.text();\n } catch {\n body = \"<unable to read response body>\";\n }\n\n const code =\n response.status >= 500\n ? ReplaneErrorCode.ServerError\n : response.status >= 400\n ? ReplaneErrorCode.ClientError\n : ReplaneErrorCode.Unknown;\n\n throw new ReplaneError({\n message: `Fetch response isn't successful (${message}): ${response.status} ${response.statusText} - ${body}`,\n code,\n });\n }\n}\n\n/**\n * Fetches a Server-Sent Events (SSE) stream and yields parsed events.\n *\n * @param params - Options for the SSE fetch\n * @yields SseEvent objects containing either data or comment events\n */\nexport async function* fetchSse(params: FetchSseOptions): AsyncGenerator<SseEvent> {\n const abortController = new AbortController();\n const { signal, cleanUpSignals } = params.signal\n ? combineAbortSignals([params.signal, abortController.signal])\n : { signal: abortController.signal, cleanUpSignals: () => {} };\n\n try {\n const res = await fetchWithTimeout(\n params.url,\n {\n method: params.method ?? \"GET\",\n headers: { Accept: \"text/event-stream\", ...(params.headers ?? {}) },\n body: params.body,\n signal,\n },\n params.timeoutMs,\n params.fetchFn\n );\n\n await ensureSuccessfulResponse(res, `SSE ${params.url}`);\n const responseContentType = res.headers.get(\"content-type\") ?? \"\";\n\n if (!responseContentType.includes(\"text/event-stream\")) {\n throw new ReplaneError({\n message: `Expected text/event-stream, got \"${responseContentType}\"`,\n code: ReplaneErrorCode.ServerError,\n });\n }\n\n if (!res.body) {\n throw new ReplaneError({\n message: `Failed to fetch SSE ${params.url}: body is empty`,\n code: ReplaneErrorCode.Unknown,\n });\n }\n\n if (params.onConnect) {\n params.onConnect();\n }\n\n const decoded = res.body.pipeThrough(new TextDecoderStream());\n const reader = decoded.getReader();\n\n let buffer = \"\";\n\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += value!;\n\n // Split on blank line; handle both \\n\\n and \\r\\n\\r\\n\n const frames = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = frames.pop() ?? \"\";\n\n for (const frame of frames) {\n // Parse lines inside a single SSE event frame\n const dataLines: string[] = [];\n let comment: string | null = null;\n\n for (const rawLine of frame.split(/\\r?\\n/)) {\n if (!rawLine) continue;\n if (rawLine.startsWith(\":\")) {\n // comment/keepalive\n comment = rawLine.slice(1);\n continue;\n }\n\n if (rawLine.startsWith(SSE_DATA_PREFIX)) {\n // Keep leading space after \"data:\" if present per spec\n const line = rawLine.slice(SSE_DATA_PREFIX.length).replace(/^\\s/, \"\");\n dataLines.push(line);\n }\n }\n\n if (dataLines.length) {\n const data = dataLines.join(\"\\n\");\n yield { type: \"data\", data };\n } else if (comment !== null) {\n yield { type: \"comment\", comment };\n }\n }\n }\n } finally {\n try {\n await reader.cancel();\n } catch {\n // ignore error\n }\n abortController.abort();\n }\n } finally {\n cleanUpSignals();\n }\n}\n","import type { ReplicationStreamRecord, StartReplicationStreamBody } from \"./types\";\nimport type { ReplaneFinalOptions } from \"./client-types\";\nimport { fetchSse } from \"./sse\";\nimport { combineAbortSignals, retryDelay } from \"./utils\";\n\nconst SUPPORTED_REPLICATION_STREAM_RECORD_TYPES = Object.keys({\n config_change: true,\n init: true,\n} satisfies Record<ReplicationStreamRecord[\"type\"], true>);\n\n/**\n * Options for starting a replication stream\n */\nexport interface StartReplicationStreamOptions extends ReplaneFinalOptions {\n // getBody is a function to get the latest configs when we are trying\n // to reestablish the replication stream\n getBody: () => StartReplicationStreamBody;\n signal?: AbortSignal;\n onConnect?: () => void;\n}\n\n/**\n * Interface for storage implementations\n */\nexport interface ReplaneStorage {\n startReplicationStream(\n options: StartReplicationStreamOptions\n ): AsyncIterable<ReplicationStreamRecord>;\n close(): void;\n}\n\n/**\n * Remote storage implementation that connects to the Replane server\n * and streams config updates via SSE.\n */\nexport class ReplaneRemoteStorage implements ReplaneStorage {\n private closeController = new AbortController();\n\n /**\n * Start a replication stream that yields config updates.\n * This method never throws - it retries on failure with exponential backoff.\n */\n async *startReplicationStream(\n options: StartReplicationStreamOptions\n ): AsyncIterable<ReplicationStreamRecord> {\n const { signal, cleanUpSignals } = combineAbortSignals([\n this.closeController.signal,\n options.signal,\n ]);\n try {\n let failedAttempts = 0;\n while (!signal.aborted) {\n try {\n for await (const event of this.startReplicationStreamImpl({\n ...options,\n signal,\n onConnect: () => {\n failedAttempts = 0;\n },\n })) {\n yield event;\n }\n } catch (error: unknown) {\n failedAttempts++;\n const retryDelayMs = Math.min(options.retryDelayMs * 2 ** (failedAttempts - 1), 10_000);\n if (!signal.aborted) {\n options.logger.error(\n `Failed to fetch project events, retrying in ${retryDelayMs}ms...`,\n error\n );\n\n await retryDelay(retryDelayMs);\n }\n }\n }\n } finally {\n cleanUpSignals();\n }\n }\n\n private async *startReplicationStreamImpl(\n options: StartReplicationStreamOptions\n ): AsyncIterable<ReplicationStreamRecord> {\n // Create an abort controller for inactivity timeout\n const inactivityAbortController = new AbortController();\n const { signal: combinedSignal, cleanUpSignals } = options.signal\n ? combineAbortSignals([options.signal, inactivityAbortController.signal])\n : { signal: inactivityAbortController.signal, cleanUpSignals: () => {} };\n\n let inactivityTimer: ReturnType<typeof setTimeout> | null = null;\n\n const resetInactivityTimer = () => {\n if (inactivityTimer) clearTimeout(inactivityTimer);\n inactivityTimer = setTimeout(() => {\n inactivityAbortController.abort();\n }, options.inactivityTimeoutMs);\n };\n\n try {\n const rawEvents = fetchSse({\n fetchFn: options.fetchFn,\n headers: {\n Authorization: this.getAuthHeader(options),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(options.getBody()),\n timeoutMs: options.requestTimeoutMs,\n method: \"POST\",\n signal: combinedSignal,\n url: this.getApiEndpoint(`/sdk/v1/replication/stream`, options),\n onConnect: () => {\n resetInactivityTimer();\n options.onConnect?.();\n },\n });\n\n for await (const sseEvent of rawEvents) {\n resetInactivityTimer();\n\n if (sseEvent.type === \"comment\") continue;\n\n const event = JSON.parse(sseEvent.data);\n if (\n typeof event === \"object\" &&\n event !== null &&\n \"type\" in event &&\n typeof event.type === \"string\" &&\n (SUPPORTED_REPLICATION_STREAM_RECORD_TYPES as unknown as string[]).includes(event.type)\n ) {\n yield event as ReplicationStreamRecord;\n }\n }\n } finally {\n if (inactivityTimer) clearTimeout(inactivityTimer);\n cleanUpSignals();\n }\n }\n\n /**\n * Close the storage and abort any active connections\n */\n close(): void {\n this.closeController.abort();\n }\n\n private getAuthHeader(options: ReplaneFinalOptions): string {\n return `Bearer ${options.sdkKey}`;\n }\n\n private getApiEndpoint(path: string, options: ReplaneFinalOptions): string {\n return `${options.baseUrl}/api${path}`;\n }\n}\n","/**\n * FNV-1a 32-bit hash function\n *\n * FNV (Fowler–Noll–Vo) is a non-cryptographic hash function known for its\n * speed and good distribution. This implementation uses the FNV-1a variant\n * which XORs before multiplying for better avalanche characteristics.\n *\n * @param input - The string to hash\n * @returns A 32-bit unsigned integer hash value\n */\nexport function fnv1a32(input: string): number {\n // Convert string to bytes (UTF-8)\n const encoder = new TextEncoder();\n const bytes = encoder.encode(input);\n\n // FNV-1a core\n let hash = 0x811c9dc5 >>> 0; // 2166136261, force uint32\n\n for (let i = 0; i < bytes.length; i++) {\n hash ^= bytes[i]; // XOR with byte\n hash = Math.imul(hash, 0x01000193) >>> 0; // * 16777619 mod 2^32\n }\n\n return hash >>> 0; // ensure unsigned 32-bit\n}\n\n/**\n * Convert FNV-1a hash to [0, 1) for bucketing.\n *\n * This is useful for percentage-based segmentation where you need\n * to deterministically assign a value to a bucket based on a string input.\n *\n * @param input - The string to hash\n * @returns A number in the range [0, 1)\n */\nexport function fnv1a32ToUnit(input: string): number {\n const h = fnv1a32(input);\n return h / 2 ** 32; // double in [0, 1)\n}\n","import type { RenderedCondition, RenderedOverride } from \"./types\";\nimport type { ReplaneContext, ReplaneLogger } from \"./client-types\";\nimport { fnv1a32ToUnit } from \"./hash\";\n\n/**\n * Result of evaluating a condition\n */\nexport type EvaluationResult = \"matched\" | \"not_matched\" | \"unknown\";\n\n/**\n * Evaluate config overrides based on context.\n * Returns the first matching override's value, or the base value if no override matches.\n *\n * @param baseValue - The default value to return if no override matches\n * @param overrides - Array of overrides to evaluate\n * @param context - The context to evaluate conditions against\n * @param logger - Logger for warnings\n * @returns The evaluated value\n */\nexport function evaluateOverrides<T>(\n baseValue: T,\n overrides: RenderedOverride[],\n context: ReplaneContext,\n logger: ReplaneLogger\n): T {\n // Find first matching override\n for (const override of overrides) {\n // All conditions must match (implicit AND)\n let overrideResult: EvaluationResult = \"matched\";\n const results = override.conditions.map((c) => evaluateCondition(c, context, logger));\n // AND: false > unknown > true\n if (results.some((r) => r === \"not_matched\")) {\n overrideResult = \"not_matched\";\n } else if (results.some((r) => r === \"unknown\")) {\n overrideResult = \"unknown\";\n }\n\n // Only use override if all conditions matched (not unknown)\n if (overrideResult === \"matched\") {\n return override.value as T;\n }\n }\n\n return baseValue;\n}\n\n/**\n * Evaluate a single condition against a context.\n *\n * @param condition - The condition to evaluate\n * @param context - The context to evaluate against\n * @param logger - Logger for warnings\n * @returns The evaluation result\n */\nexport function evaluateCondition(\n condition: RenderedCondition,\n context: ReplaneContext,\n logger: ReplaneLogger\n): EvaluationResult {\n const operator = condition.operator;\n\n // Composite conditions\n if (operator === \"and\") {\n const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));\n // AND: false > unknown > true\n if (results.some((r) => r === \"not_matched\")) return \"not_matched\";\n if (results.some((r) => r === \"unknown\")) return \"unknown\";\n return \"matched\";\n }\n\n if (operator === \"or\") {\n const results = condition.conditions.map((c) => evaluateCondition(c, context, logger));\n // OR: true > unknown > false\n if (results.some((r) => r === \"matched\")) return \"matched\";\n if (results.some((r) => r === \"unknown\")) return \"unknown\";\n return \"not_matched\";\n }\n\n if (operator === \"not\") {\n const result = evaluateCondition(condition.condition, context, logger);\n if (result === \"matched\") return \"not_matched\";\n if (result === \"not_matched\") return \"matched\";\n return \"unknown\"; // NOT unknown = unknown\n }\n\n // Segmentation\n if (operator === \"segmentation\") {\n const contextValue = context[condition.property];\n if (contextValue === undefined || contextValue === null) {\n return \"unknown\";\n }\n\n // FNV-1a hash to bucket [0, 100)\n const hashInput = String(contextValue) + condition.seed;\n const unitValue = fnv1a32ToUnit(hashInput);\n return unitValue >= condition.fromPercentage / 100 && unitValue < condition.toPercentage / 100\n ? \"matched\"\n : \"not_matched\";\n }\n\n // Property-based conditions\n const property = condition.property;\n const contextValue = context[property];\n const expectedValue = condition.value;\n\n if (contextValue === undefined) {\n return \"unknown\";\n }\n\n // Type casting\n const castedValue = castToContextType(expectedValue, contextValue);\n\n switch (operator) {\n case \"equals\":\n return contextValue === castedValue ? \"matched\" : \"not_matched\";\n\n case \"in\":\n if (!Array.isArray(castedValue)) return \"unknown\";\n return castedValue.includes(contextValue) ? \"matched\" : \"not_matched\";\n\n case \"not_in\":\n if (!Array.isArray(castedValue)) return \"unknown\";\n return !castedValue.includes(contextValue) ? \"matched\" : \"not_matched\";\n\n case \"less_than\":\n if (typeof contextValue === \"number\" && typeof castedValue === \"number\") {\n return contextValue < castedValue ? \"matched\" : \"not_matched\";\n }\n if (typeof contextValue === \"string\" && typeof castedValue === \"string\") {\n return contextValue < castedValue ? \"matched\" : \"not_matched\";\n }\n return \"not_matched\";\n\n case \"less_than_or_equal\":\n if (typeof contextValue === \"number\" && typeof castedValue === \"number\") {\n return contextValue <= castedValue ? \"matched\" : \"not_matched\";\n }\n if (typeof contextValue === \"string\" && typeof castedValue === \"string\") {\n return contextValue <= castedValue ? \"matched\" : \"not_matched\";\n }\n return \"not_matched\";\n\n case \"greater_than\":\n if (typeof contextValue === \"number\" && typeof castedValue === \"number\") {\n return contextValue > castedValue ? \"matched\" : \"not_matched\";\n }\n if (typeof contextValue === \"string\" && typeof castedValue === \"string\") {\n return contextValue > castedValue ? \"matched\" : \"not_matched\";\n }\n return \"not_matched\";\n\n case \"greater_than_or_equal\":\n if (typeof contextValue === \"number\" && typeof castedValue === \"number\") {\n return contextValue >= castedValue ? \"matched\" : \"not_matched\";\n }\n if (typeof contextValue === \"string\" && typeof castedValue === \"string\") {\n return contextValue >= castedValue ? \"matched\" : \"not_matched\";\n }\n return \"not_matched\";\n\n default:\n warnNever(operator, logger, `Unexpected operator: ${operator}`);\n return \"unknown\";\n }\n}\n\n/**\n * Helper to warn about exhaustive check failures\n */\nfunction warnNever(value: never, logger: ReplaneLogger, message: string): void {\n logger.warn(message, { value });\n}\n\n/**\n * Cast expected value to match context value type.\n * This enables loose matching between different types (e.g., \"25\" matches 25).\n *\n * @param expectedValue - The value from the condition\n * @param contextValue - The value from the context\n * @returns The expected value cast to match the context value's type\n */\nexport function castToContextType(expectedValue: unknown, contextValue: unknown): unknown {\n if (typeof contextValue === \"number\") {\n if (typeof expectedValue === \"string\") {\n const num = Number(expectedValue);\n return isNaN(num) ? expectedValue : num;\n }\n return expectedValue;\n }\n\n if (typeof contextValue === \"boolean\") {\n if (typeof expectedValue === \"string\") {\n if (expectedValue === \"true\") return true;\n if (expectedValue === \"false\") return false;\n }\n if (typeof expectedValue === \"number\") {\n return expectedValue !== 0;\n }\n return expectedValue;\n }\n\n if (typeof contextValue === \"string\") {\n if (typeof expectedValue === \"number\" || typeof expectedValue === \"boolean\") {\n return String(expectedValue);\n }\n return expectedValue;\n }\n\n return expectedValue;\n}\n","import type { ConfigDto, RenderedOverride } from \"./types\";\nimport type {\n Configs,\n ReplaneContext,\n ReplaneLogger,\n GetConfigOptions,\n MapConfig,\n ReplaneSnapshot,\n ReplaneClient,\n ReplaneClientOptions,\n RestoreReplaneClientOptions,\n ReplaneFinalOptions,\n} from \"./client-types\";\nimport type { ReplaneStorage } from \"./storage\";\nimport { ReplaneRemoteStorage } from \"./storage\";\nimport { ReplaneError, ReplaneErrorCode } from \"./error\";\nimport { evaluateOverrides } from \"./evaluation\";\nimport { Deferred } from \"./utils\";\n\n/**\n * Internal options for creating the client core\n */\ninterface ClientCoreOptions {\n initialConfigs: ConfigDto[];\n context: ReplaneContext;\n logger: ReplaneLogger;\n storage: ReplaneStorage | null;\n streamOptions: ReplaneFinalOptions | null;\n requiredConfigs: string[];\n}\n\n/**\n * Result from creating the client core\n */\ninterface ClientCoreResult<T extends Configs> {\n client: ReplaneClient<T>;\n configs: Map<string, ConfigDto>;\n startStreaming: () => Promise<void>;\n clientReady: Deferred<void>;\n}\n\n/**\n * Creates the core client logic shared between createReplaneClient and restoreReplaneClient\n */\nfunction createClientCore<T extends Configs = Record<string, unknown>>(\n options: ClientCoreOptions\n): ClientCoreResult<T> {\n const { initialConfigs, context, logger, storage, streamOptions, requiredConfigs } = options;\n\n const configs: Map<string, ConfigDto> = new Map(\n initialConfigs.map((config) => [config.name, config])\n );\n\n const clientReady = new Deferred<void>();\n const configSubscriptions = new Map<keyof T, Set<(config: MapConfig<T>) => void>>();\n const clientSubscriptions = new Set<(config: MapConfig<T>) => void>();\n\n function processConfigUpdates(updatedConfigs: ConfigDto[]) {\n for (const config of updatedConfigs) {\n configs.set(config.name, {\n name: config.name,\n overrides: config.overrides,\n value: config.value,\n });\n for (const callback of clientSubscriptions) {\n callback({ name: config.name as keyof T, value: config.value as T[keyof T] });\n }\n for (const callback of configSubscriptions.get(config.name as keyof T) ?? []) {\n callback({ name: config.name as keyof T, value: config.value as T[keyof T] });\n }\n }\n }\n\n async function startStreaming(): Promise<void> {\n if (!storage || !streamOptions) return;\n\n try {\n const replicationStream = storage.startReplicationStream({\n ...streamOptions,\n getBody: () => ({\n currentConfigs: [...configs.values()].map((config) => ({\n name: config.name,\n overrides: config.overrides,\n value: config.value,\n })),\n requiredConfigs,\n }),\n });\n\n for await (const event of replicationStream) {\n const updatedConfigs: ConfigDto[] =\n event.type === \"config_change\" ? [event.config] : event.configs;\n processConfigUpdates(updatedConfigs);\n clientReady.resolve();\n }\n } catch (error) {\n logger.error(\"Replane: error in SSE connection:\", error);\n clientReady.reject(error);\n throw error;\n }\n }\n\n function get<K extends keyof T>(configName: K, getConfigOptions: GetConfigOptions = {}): T[K] {\n const config = configs.get(String(configName));\n\n if (config === undefined) {\n throw new ReplaneError({\n message: `Config not found: ${String(configName)}`,\n code: ReplaneErrorCode.NotFound,\n });\n }\n\n try {\n return evaluateOverrides<T[K]>(\n config.value as T[K],\n config.overrides,\n { ...context, ...(getConfigOptions?.context ?? {}) },\n logger\n );\n } catch (error) {\n logger.error(`Replane: error evaluating overrides for config ${String(configName)}:`, error);\n return config.value as T[K];\n }\n }\n\n const subscribe = (\n callbackOrConfigName: keyof T | ((config: MapConfig<T>) => void),\n callbackOrUndefined?: (config: MapConfig<T>) => void\n ) => {\n let configName: keyof T | undefined = undefined;\n let callback: (config: MapConfig<T>) => void;\n if (typeof callbackOrConfigName === \"function\") {\n callback = callbackOrConfigName;\n } else {\n configName = callbackOrConfigName as keyof T;\n if (callbackOrUndefined === undefined) {\n throw new Error(\"callback is required when config name is provided\");\n }\n callback = callbackOrUndefined!;\n }\n\n // Wrap the callback to ensure that we have a unique reference\n const originalCallback = callback;\n callback = (...args: Parameters<typeof callback>) => {\n originalCallback(...args);\n };\n\n if (configName === undefined) {\n clientSubscriptions.add(callback);\n return () => {\n clientSubscriptions.delete(callback);\n };\n }\n\n if (!configSubscriptions.has(configName)) {\n configSubscriptions.set(configName, new Set());\n }\n configSubscriptions.get(configName)!.add(callback);\n return () => {\n configSubscriptions.get(configName)?.delete(callback);\n if (configSubscriptions.get(configName)?.size === 0) {\n configSubscriptions.delete(configName);\n }\n };\n };\n\n const getSnapshot = (): ReplaneSnapshot<T> => ({\n configs: [...configs.values()].map((config) => ({\n name: config.name,\n value: config.value,\n overrides: config.overrides.map((override) => ({\n name: override.name,\n conditions: override.conditions,\n value: override.value,\n })),\n })),\n context,\n });\n\n const close = () => storage?.close();\n\n const client: ReplaneClient<T> = {\n get,\n subscribe: subscribe as ReplaneClient<T>[\"subscribe\"],\n getSnapshot,\n close,\n };\n\n return { client, configs, startStreaming, clientReady };\n}\n\n/**\n * Create a Replane client bound to an SDK key.\n *\n * @example\n * ```typescript\n * const client = await createReplaneClient({\n * sdkKey: 'your-sdk-key',\n * baseUrl: 'https://app.replane.dev'\n * });\n * const value = client.get('my-config');\n * ```\n */\nexport async function createReplaneClient<T extends Configs = Record<string, unknown>>(\n sdkOptions: ReplaneClientOptions<T>\n): Promise<ReplaneClient<T>> {\n const storage = new ReplaneRemoteStorage();\n return await createReplaneClientInternal(toFinalOptions(sdkOptions), storage);\n}\n\n/**\n * Create a Replane client that uses in-memory storage.\n * Useful for testing or when you have static config values.\n *\n * @example\n * ```typescript\n * const client = createInMemoryReplaneClient({ 'my-config': 123 });\n * const value = client.get('my-config'); // 123\n * ```\n */\nexport function createInMemoryReplaneClient<T extends Configs = Record<string, unknown>>(\n initialData: T\n): ReplaneClient<T> {\n return {\n get: (configName) => {\n const config = initialData[configName];\n if (config === undefined) {\n throw new ReplaneError({\n message: `Config not found: ${String(configName)}`,\n code: ReplaneErrorCode.NotFound,\n });\n }\n return config;\n },\n subscribe: () => {\n return () => {};\n },\n getSnapshot: () => ({\n configs: Object.entries(initialData).map(([name, value]) => ({\n name,\n value,\n overrides: [],\n })),\n }),\n close: () => {},\n };\n}\n\n/**\n * Restore a Replane client from a snapshot.\n * This is useful for SSR/hydration scenarios where the server has already fetched configs.\n *\n * @example\n * ```typescript\n * // On the server:\n * const serverClient = await createReplaneClient({ ... });\n * const snapshot = serverClient.getSnapshot();\n * // Pass snapshot to client via props/serialization\n *\n * // On the client:\n * const client = restoreReplaneClient({\n * snapshot,\n * connection: { sdkKey, baseUrl }\n * });\n * const value = client.get('my-config');\n * ```\n */\nexport function restoreReplaneClient<T extends Configs = Record<string, unknown>>(\n options: RestoreReplaneClientOptions<T>\n): ReplaneClient<T> {\n const { snapshot, connection } = options;\n const context = options.context ?? snapshot.context ?? {};\n const logger = connection?.logger ?? console;\n\n // Initialize configs from snapshot\n const initialConfigs: ConfigDto[] = snapshot.configs.map((config) => ({\n name: config.name,\n value: config.value,\n overrides: config.overrides as RenderedOverride[],\n }));\n\n let storage: ReplaneRemoteStorage | null = null;\n let streamOptions: ReplaneFinalOptions | null = null;\n\n if (connection) {\n storage = new ReplaneRemoteStorage();\n streamOptions = {\n sdkKey: connection.sdkKey,\n baseUrl: connection.baseUrl.replace(/\\/+$/, \"\"),\n fetchFn: connection.fetchFn ?? globalThis.fetch.bind(globalThis),\n requestTimeoutMs: connection.requestTimeoutMs ?? 2000,\n initializationTimeoutMs: 5000, // Not used for restore\n inactivityTimeoutMs: connection.inactivityTimeoutMs ?? 30_000,\n logger,\n retryDelayMs: connection.retryDelayMs ?? 200,\n context,\n requiredConfigs: [],\n fallbacks: [],\n };\n }\n\n const { client, startStreaming } = createClientCore<T>({\n initialConfigs,\n context,\n logger,\n storage,\n streamOptions,\n requiredConfigs: [],\n });\n\n // Start streaming in background (non-blocking) if connection is provided\n if (storage && streamOptions) {\n startStreaming().catch((error) => {\n logger.error(\"Replane: error in restored client SSE connection:\", error);\n });\n }\n\n return client;\n}\n\n/**\n * Internal function to create a Replane client with the given options and storage\n */\nasync function createReplaneClientInternal<T extends Configs = Record<string, unknown>>(\n sdkOptions: ReplaneFinalOptions,\n storage: ReplaneStorage\n): Promise<ReplaneClient<T>> {\n if (!sdkOptions.sdkKey) throw new Error(\"SDK key is required\");\n\n const { client, configs, startStreaming, clientReady } = createClientCore<T>({\n initialConfigs: sdkOptions.fallbacks,\n context: sdkOptions.context,\n logger: sdkOptions.logger,\n storage,\n streamOptions: sdkOptions,\n requiredConfigs: sdkOptions.requiredConfigs,\n });\n\n // Start streaming in background\n startStreaming().catch((error) => {\n sdkOptions.logger.error(\"Replane: error initializing client:\", error);\n });\n\n const initializationTimeoutId = setTimeout(() => {\n if (sdkOptions.fallbacks.length === 0) {\n // no fallbacks, we have nothing to work with\n client.close();\n\n clientReady.reject(\n new ReplaneError({\n message: \"Replane client initialization timed out\",\n code: ReplaneErrorCode.Timeout,\n })\n );\n\n return;\n }\n\n const missingRequiredConfigs: string[] = [];\n for (const requiredConfigName of sdkOptions.requiredConfigs) {\n if (!configs.has(requiredConfigName)) {\n missingRequiredConfigs.push(requiredConfigName);\n }\n }\n\n if (missingRequiredConfigs.length > 0) {\n client.close();\n clientReady.reject(\n new ReplaneError({\n message: `Required configs are missing: ${missingRequiredConfigs.join(\", \")}`,\n code: ReplaneErrorCode.NotFound,\n })\n );\n\n return;\n }\n\n clientReady.resolve();\n }, sdkOptions.initializationTimeoutMs);\n\n clientReady.promise.then(() => clearTimeout(initializationTimeoutId));\n\n await clientReady.promise;\n\n return client;\n}\n\n/**\n * Convert user options to final options with defaults\n */\nfunction toFinalOptions<T extends Configs>(defaults: ReplaneClientOptions<T>): ReplaneFinalOptions {\n return {\n sdkKey: defaults.sdkKey,\n baseUrl: defaults.baseUrl.replace(/\\/+$/, \"\"),\n fetchFn:\n defaults.fetchFn ??\n // some browsers require binding the fetch function to window\n globalThis.fetch.bind(globalThis),\n requestTimeoutMs: defaults.requestTimeoutMs ?? 2000,\n initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5000,\n inactivityTimeoutMs: defaults.inactivityTimeoutMs ?? 30_000,\n logger: defaults.logger ?? console,\n retryDelayMs: defaults.retryDelayMs ?? 200,\n context: {\n ...(defaults.context ?? {}),\n },\n requiredConfigs: Array.isArray(defaults.required)\n ? defaults.required.map((name) => String(name))\n : Object.entries(defaults.required ?? {})\n .filter(([_, value]) => value !== undefined)\n .map(([name]) => name),\n fallbacks: Object.entries(defaults.fallbacks ?? {})\n .filter(([_, value]) => value !== undefined)\n .map(([name, value]) => ({\n name,\n overrides: [],\n version: -1,\n value,\n })),\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replanejs/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Dynamic configuration SDK for browser and server environments (Node.js, Deno, Bun). Powered by Replane.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/replane-dev/replane-javascript.git"
|
|
13
|
+
"url": "git+https://github.com/replane-dev/replane-javascript.git",
|
|
14
|
+
"directory": "packages/sdk"
|
|
14
15
|
},
|
|
15
16
|
"author": "Dmitry Tilyupo <tilyupo@gmail.com>",
|
|
16
17
|
"files": [
|
|
@@ -46,26 +47,18 @@
|
|
|
46
47
|
"dev": "tsdown --watch",
|
|
47
48
|
"test": "vitest run",
|
|
48
49
|
"typecheck": "tsc --noEmit",
|
|
49
|
-
"lint": "eslint src tests --max-warnings 0",
|
|
50
|
-
"lint:fix": "eslint src tests --fix",
|
|
51
|
-
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"",
|
|
52
|
-
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"README.md\"",
|
|
53
50
|
"release": "pnpm run build && bumpp && npm publish"
|
|
54
51
|
},
|
|
55
52
|
"devDependencies": {
|
|
56
|
-
"@eslint/js": "^9.18.0",
|
|
57
53
|
"@types/node": "^22.15.17",
|
|
58
|
-
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
59
|
-
"@typescript-eslint/parser": "^8.20.0",
|
|
60
54
|
"async-channel": "^0.2.0",
|
|
61
55
|
"bumpp": "^10.1.0",
|
|
62
56
|
"esbuild": "^0.23.1",
|
|
63
|
-
"eslint": "^9.18.0",
|
|
64
|
-
"prettier": "^3.4.2",
|
|
65
57
|
"tsdown": "^0.11.9",
|
|
66
58
|
"typescript": "^5.8.3",
|
|
67
|
-
"typescript-eslint": "^8.20.0",
|
|
68
59
|
"vitest": "^3.1.3"
|
|
69
60
|
},
|
|
70
|
-
"
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
}
|
|
71
64
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Dmitry Tilyupo
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
# Replane JavaScript SDK
|
|
2
|
-
|
|
3
|
-
Small TypeScript client for watching configuration values from a Replane API with realtime updates and context-based override evaluation.
|
|
4
|
-
|
|
5
|
-
Part of the Replane project: [replane-dev/replane](https://github.com/replane-dev/replane).
|
|
6
|
-
|
|
7
|
-
> Status: early. Minimal surface area on purpose. Expect small breaking tweaks until 0.1.x.
|
|
8
|
-
|
|
9
|
-
## Why it exists
|
|
10
|
-
|
|
11
|
-
You need: given a token + config name + optional context -> watch the value with realtime updates. This package does only that:
|
|
12
|
-
|
|
13
|
-
- Works in ESM and CJS (dual build)
|
|
14
|
-
- Zero runtime deps (uses native `fetch` — bring a polyfill if your runtime lacks it)
|
|
15
|
-
- Realtime updates via Server-Sent Events (SSE)
|
|
16
|
-
- Context-based override evaluation (feature flags, A/B testing, gradual rollouts)
|
|
17
|
-
- Tiny bundle footprint
|
|
18
|
-
- Strong TypeScript types
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @replanejs/sdk
|
|
24
|
-
# or
|
|
25
|
-
pnpm add @replanejs/sdk
|
|
26
|
-
# or
|
|
27
|
-
yarn add @replanejs/sdk
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Quick start
|
|
31
|
-
|
|
32
|
-
> **Important:** Each SDK key is tied to a specific project. The client can only access configs from the project that the SDK key belongs to. If you need configs from multiple projects, create separate SDK keys and initialize separate clients—one per project.
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { createReplaneClient } from "@replanejs/sdk";
|
|
36
|
-
|
|
37
|
-
// Define your config types
|
|
38
|
-
interface Configs {
|
|
39
|
-
"new-onboarding": boolean;
|
|
40
|
-
"password-requirements": PasswordRequirements;
|
|
41
|
-
"billing-enabled": boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface PasswordRequirements {
|
|
45
|
-
minLength: number;
|
|
46
|
-
requireSymbol: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const replane = await createReplaneClient<Configs>({
|
|
50
|
-
// Each SDK key belongs to one project only
|
|
51
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
52
|
-
baseUrl: "https://replane.my-hosting.com",
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Get a config value (knows about latest updates via SSE)
|
|
56
|
-
const featureFlag = replane.get("new-onboarding"); // Typed as boolean
|
|
57
|
-
|
|
58
|
-
if (featureFlag) {
|
|
59
|
-
console.log("New onboarding enabled!");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Typed config - no need to specify type again
|
|
63
|
-
const passwordReqs = replane.get("password-requirements");
|
|
64
|
-
|
|
65
|
-
// Use the value directly
|
|
66
|
-
const { minLength } = passwordReqs; // TypeScript knows this is PasswordRequirements
|
|
67
|
-
|
|
68
|
-
// With context for override evaluation
|
|
69
|
-
const enabled = replane.get("billing-enabled", {
|
|
70
|
-
context: {
|
|
71
|
-
userId: "user-123",
|
|
72
|
-
plan: "premium",
|
|
73
|
-
region: "us-east",
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (enabled) {
|
|
78
|
-
console.log("Billing enabled for this user!");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// When done, clean up resources
|
|
82
|
-
replane.close();
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## API
|
|
86
|
-
|
|
87
|
-
### `createReplaneClient<T>(options)`
|
|
88
|
-
|
|
89
|
-
Returns a promise resolving to an object: `{ get, subscribe, close }`.
|
|
90
|
-
|
|
91
|
-
Type parameter `T` defines the shape of your configs (a mapping of config names to their value types).
|
|
92
|
-
|
|
93
|
-
`close()` stops the configs client and cleans up resources. It is safe to call multiple times (no‑op after the first call).
|
|
94
|
-
|
|
95
|
-
#### Options
|
|
96
|
-
|
|
97
|
-
- `baseUrl` (string) – Replane origin (no trailing slash needed).
|
|
98
|
-
- `sdkKey` (string) – SDK key for authorization. Required. **Note:** Each SDK key is tied to a specific project and can only access configs from that project. To access configs from multiple projects, create multiple SDK keys and initialize separate client instances.
|
|
99
|
-
- `required` (object or array) – mark specific configs as required. If any required config is missing, the client will throw an error during initialization. Can be an object with boolean values or an array of config names. Optional.
|
|
100
|
-
- `fallbacks` (object) – fallback values to use if the initial request to fetch configs fails. Allows the client to start even when the API is unavailable. Optional.
|
|
101
|
-
- `context` (object) – default context for all config evaluations. Can be overridden per-request in `get()`. Optional.
|
|
102
|
-
- `fetchFn` (function) – custom fetch (e.g. `undici.fetch` or mocked fetch in tests). Optional.
|
|
103
|
-
- `timeoutMs` (number) – abort the request after N ms. Default: 2000.
|
|
104
|
-
- `retries` (number) – number of retry attempts on failures (5xx or network errors). Default: 2.
|
|
105
|
-
- `retryDelayMs` (number) – base delay between retries in ms (a small jitter is applied). Default: 200.
|
|
106
|
-
- `logger` (object) – custom logger with `debug`, `info`, `warn`, `error` methods. Default: `console`.
|
|
107
|
-
|
|
108
|
-
### `replane.get<K>(name, options?)`
|
|
109
|
-
|
|
110
|
-
Gets the current config value. The configs client maintains an up-to-date cache that receives realtime updates via Server-Sent Events (SSE) in the background.
|
|
111
|
-
|
|
112
|
-
Parameters:
|
|
113
|
-
|
|
114
|
-
- `name` (K extends keyof T) – config name to fetch. TypeScript will enforce that this is a valid config name from your `Configs` interface.
|
|
115
|
-
- `options` (object) – optional configuration:
|
|
116
|
-
- `context` (object) – context merged with client-level context for override evaluation.
|
|
117
|
-
|
|
118
|
-
Returns the config value of type `T[K]` (synchronous). The return type is automatically inferred from your `Configs` interface.
|
|
119
|
-
|
|
120
|
-
Notes:
|
|
121
|
-
|
|
122
|
-
- The Replane client receives realtime updates via SSE in the background.
|
|
123
|
-
- If the config is not found, throws a `ReplaneError` with code `not_found`.
|
|
124
|
-
- Context-based overrides are evaluated automatically based on context.
|
|
125
|
-
|
|
126
|
-
Example:
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
interface Configs {
|
|
130
|
-
"billing-enabled": boolean;
|
|
131
|
-
"max-connections": number;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const replane = await createReplaneClient<Configs>({
|
|
135
|
-
sdkKey: "your-sdk-key",
|
|
136
|
-
baseUrl: "https://replane.my-host.com",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Get value without context - TypeScript knows this is boolean
|
|
140
|
-
const enabled = replane.get("billing-enabled");
|
|
141
|
-
|
|
142
|
-
// Get value with context for override evaluation
|
|
143
|
-
const userEnabled = replane.get("billing-enabled", {
|
|
144
|
-
context: { userId: "user-123", plan: "premium" },
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Clean up when done
|
|
148
|
-
replane.close();
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### `replane.subscribe(callback)` or `replane.subscribe(configName, callback)`
|
|
152
|
-
|
|
153
|
-
Subscribe to config changes and receive real-time updates when configs are modified.
|
|
154
|
-
|
|
155
|
-
**Two overloads:**
|
|
156
|
-
|
|
157
|
-
1. **Subscribe to all config changes:**
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const unsubscribe = replane.subscribe((config) => {
|
|
161
|
-
console.log(`Config ${config.name} changed to:`, config.value);
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
2. **Subscribe to a specific config:**
|
|
166
|
-
```ts
|
|
167
|
-
const unsubscribe = replane.subscribe("billing-enabled", (config) => {
|
|
168
|
-
console.log(`billing-enabled changed to:`, config.value);
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Parameters:
|
|
173
|
-
|
|
174
|
-
- `callback` (function) – Function called when any config changes. Receives an object with `{ name, value }`.
|
|
175
|
-
- `configName` (K extends keyof T) – Optional. If provided, only changes to this specific config will trigger the callback.
|
|
176
|
-
|
|
177
|
-
Returns a function to unsubscribe from the config changes.
|
|
178
|
-
|
|
179
|
-
Example:
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
interface Configs {
|
|
183
|
-
"feature-flag": boolean;
|
|
184
|
-
"max-connections": number;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const replane = await createReplaneClient<Configs>({
|
|
188
|
-
sdkKey: "your-sdk-key",
|
|
189
|
-
baseUrl: "https://replane.my-host.com",
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Subscribe to all config changes
|
|
193
|
-
const unsubscribeAll = replane.subscribe((config) => {
|
|
194
|
-
console.log(`Config ${config.name} updated:`, config.value);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Subscribe to a specific config
|
|
198
|
-
const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
|
|
199
|
-
console.log("Feature flag changed:", config.value);
|
|
200
|
-
// config.value is typed as boolean
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Later: unsubscribe when done
|
|
204
|
-
unsubscribeAll();
|
|
205
|
-
unsubscribeFeature();
|
|
206
|
-
|
|
207
|
-
// Clean up when done
|
|
208
|
-
replane.close();
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### `createInMemoryReplaneClient(initialData)`
|
|
212
|
-
|
|
213
|
-
Creates a client backed by an in-memory store instead of making HTTP requests. Handy for unit tests or local development where you want deterministic config values without a server.
|
|
214
|
-
|
|
215
|
-
Parameters:
|
|
216
|
-
|
|
217
|
-
- `initialData` (object) – map of config name to value.
|
|
218
|
-
|
|
219
|
-
Returns the same client shape as `createReplaneClient` (`{ get, subscribe, close }`).
|
|
220
|
-
|
|
221
|
-
Notes:
|
|
222
|
-
|
|
223
|
-
- `get(name)` resolves to the value from `initialData`.
|
|
224
|
-
- If a name is missing, it throws a `ReplaneError` (`Config not found: <name>`).
|
|
225
|
-
- The client works as usual but doesn't receive SSE updates (values remain whatever is in-memory).
|
|
226
|
-
|
|
227
|
-
Example:
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
import { createInMemoryReplaneClient } from "@replanejs/sdk";
|
|
231
|
-
|
|
232
|
-
interface Configs {
|
|
233
|
-
"feature-a": boolean;
|
|
234
|
-
"max-items": { value: number; ttl: number };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const replane = createInMemoryReplaneClient<Configs>({
|
|
238
|
-
"feature-a": true,
|
|
239
|
-
"max-items": { value: 10, ttl: 3600 },
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
const featureA = replane.get("feature-a"); // TypeScript knows this is boolean
|
|
243
|
-
console.log(featureA); // true
|
|
244
|
-
|
|
245
|
-
const maxItems = replane.get("max-items"); // TypeScript knows the type
|
|
246
|
-
console.log(maxItems); // { value: 10, ttl: 3600 }
|
|
247
|
-
|
|
248
|
-
replane.close();
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### `replane.close()`
|
|
252
|
-
|
|
253
|
-
Gracefully shuts down the Replane client and cleans up resources. Subsequent method calls will throw. Use this in environments where you manage resource lifecycles explicitly (e.g. shutting down a server or worker).
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
// During shutdown
|
|
257
|
-
replane.close();
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Errors
|
|
261
|
-
|
|
262
|
-
`createReplaneClient` throws if the initial request to fetch configs fails with non‑2xx HTTP responses and network errors. A `ReplaneError` is thrown for HTTP failures; other errors may be thrown for network/parse issues.
|
|
263
|
-
|
|
264
|
-
The Replane client receives realtime updates via SSE in the background. SSE connection errors are logged and automatically retried, but don't affect `get` calls (which return the last known value).
|
|
265
|
-
|
|
266
|
-
## Environment notes
|
|
267
|
-
|
|
268
|
-
- Node 18+ has global `fetch`; for older Node versions supply `fetchFn`.
|
|
269
|
-
- Edge runtimes / Workers: provide a compatible `fetch` + `AbortController` if not built‑in.
|
|
270
|
-
|
|
271
|
-
## Common patterns
|
|
272
|
-
|
|
273
|
-
### Typed config
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
interface LayoutConfig {
|
|
277
|
-
variant: "a" | "b";
|
|
278
|
-
ttl: number;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
interface Configs {
|
|
282
|
-
layout: LayoutConfig;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const replane = await createReplaneClient<Configs>({
|
|
286
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
287
|
-
baseUrl: "https://replane.my-host.com",
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const layout = replane.get("layout"); // TypeScript knows this is LayoutConfig
|
|
291
|
-
console.log(layout); // { variant: "a", ttl: 3600 }
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### Context-based overrides
|
|
295
|
-
|
|
296
|
-
```ts
|
|
297
|
-
interface Configs {
|
|
298
|
-
"advanced-features": boolean;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const replane = await createReplaneClient<Configs>({
|
|
302
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
303
|
-
baseUrl: "https://replane.my-host.com",
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// Config has base value `false` but override: if `plan === "premium"` then `true`
|
|
307
|
-
|
|
308
|
-
// Free user
|
|
309
|
-
const freeUserEnabled = replane.get("advanced-features", {
|
|
310
|
-
context: { plan: "free" },
|
|
311
|
-
}); // false
|
|
312
|
-
|
|
313
|
-
// Premium user
|
|
314
|
-
const premiumUserEnabled = replane.get("advanced-features", {
|
|
315
|
-
context: { plan: "premium" },
|
|
316
|
-
}); // true
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Client-level context
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
interface Configs {
|
|
323
|
-
"feature-flag": boolean;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const replane = await createReplaneClient<Configs>({
|
|
327
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
328
|
-
baseUrl: "https://replane.my-host.com",
|
|
329
|
-
context: {
|
|
330
|
-
userId: "user-123",
|
|
331
|
-
region: "us-east",
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// This context is used for all configs unless overridden
|
|
336
|
-
const value1 = replane.get("feature-flag"); // Uses client-level context
|
|
337
|
-
const value2 = replane.get("feature-flag", {
|
|
338
|
-
context: { userId: "user-321" },
|
|
339
|
-
}); // Merges with client context
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### Custom fetch (tests)
|
|
343
|
-
|
|
344
|
-
```ts
|
|
345
|
-
const replane = await createReplaneClient({
|
|
346
|
-
sdkKey: "TKN",
|
|
347
|
-
baseUrl: "https://api",
|
|
348
|
-
fetchFn: mockFetch,
|
|
349
|
-
});
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Required configs
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
interface Configs {
|
|
356
|
-
"api-key": string;
|
|
357
|
-
"database-url": string;
|
|
358
|
-
"optional-feature": boolean;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const replane = await createReplaneClient<Configs>({
|
|
362
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
363
|
-
baseUrl: "https://replane.my-host.com",
|
|
364
|
-
required: {
|
|
365
|
-
"api-key": true,
|
|
366
|
-
"database-url": true,
|
|
367
|
-
"optional-feature": false, // Not required
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Alternative: use an array
|
|
372
|
-
// required: ["api-key", "database-url"]
|
|
373
|
-
|
|
374
|
-
// If any required config is missing, initialization will throw
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### Fallback configs
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
interface Configs {
|
|
381
|
-
"feature-flag": boolean;
|
|
382
|
-
"max-connections": number;
|
|
383
|
-
"timeout-ms": number;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const replane = await createReplaneClient<Configs>({
|
|
387
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
388
|
-
baseUrl: "https://replane.my-host.com",
|
|
389
|
-
fallbacks: {
|
|
390
|
-
"feature-flag": false, // Use false if fetch fails
|
|
391
|
-
"max-connections": 10, // Use 10 if fetch fails
|
|
392
|
-
"timeout-ms": 5000, // Use 5s if fetch fails
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// If the initial fetch fails, fallback values are used
|
|
397
|
-
// Once the configs client connects, it will receive realtime updates
|
|
398
|
-
const maxConnections = replane.get("max-connections"); // 10 (or real value)
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### Multiple projects
|
|
402
|
-
|
|
403
|
-
```ts
|
|
404
|
-
interface ProjectAConfigs {
|
|
405
|
-
"feature-flag": boolean;
|
|
406
|
-
"max-users": number;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
interface ProjectBConfigs {
|
|
410
|
-
"feature-flag": boolean;
|
|
411
|
-
"api-rate-limit": number;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Each project needs its own SDK key and Replane client instance
|
|
415
|
-
const projectAConfigs = await createReplaneClient<ProjectAConfigs>({
|
|
416
|
-
sdkKey: process.env.PROJECT_A_SDK_KEY!,
|
|
417
|
-
baseUrl: "https://replane.my-host.com",
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const projectBConfigs = await createReplaneClient<ProjectBConfigs>({
|
|
421
|
-
sdkKey: process.env.PROJECT_B_SDK_KEY!,
|
|
422
|
-
baseUrl: "https://replane.my-host.com",
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// Each Replane client only accesses configs from its respective project
|
|
426
|
-
const featureA = projectAConfigs.get("feature-flag"); // boolean
|
|
427
|
-
const featureB = projectBConfigs.get("feature-flag"); // boolean
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### Subscriptions
|
|
431
|
-
|
|
432
|
-
```ts
|
|
433
|
-
interface Configs {
|
|
434
|
-
"feature-flag": boolean;
|
|
435
|
-
"max-users": number;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const replane = await createReplaneClient<Configs>({
|
|
439
|
-
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
440
|
-
baseUrl: "https://replane.my-host.com",
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// Subscribe to all config changes
|
|
444
|
-
const unsubscribeAll = replane.subscribe((config) => {
|
|
445
|
-
console.log(`Config ${config.name} changed:`, config.value);
|
|
446
|
-
|
|
447
|
-
// React to specific config changes
|
|
448
|
-
if (config.name === "feature-flag") {
|
|
449
|
-
console.log("Feature flag updated:", config.value);
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// Subscribe to a specific config only
|
|
454
|
-
const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
|
|
455
|
-
console.log("Feature flag changed:", config.value);
|
|
456
|
-
// config.value is automatically typed as boolean
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// Subscribe to multiple specific configs
|
|
460
|
-
const unsubscribeMaxUsers = replane.subscribe("max-users", (config) => {
|
|
461
|
-
console.log("Max users changed:", config.value);
|
|
462
|
-
// config.value is automatically typed as number
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Cleanup
|
|
466
|
-
unsubscribeAll();
|
|
467
|
-
unsubscribeFeature();
|
|
468
|
-
unsubscribeMaxUsers();
|
|
469
|
-
replane.close();
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
## Roadmap
|
|
473
|
-
|
|
474
|
-
- Config caching
|
|
475
|
-
- Config invalidation
|
|
476
|
-
|
|
477
|
-
## License
|
|
478
|
-
|
|
479
|
-
MIT
|