import { Config } from "./config"; import Loader from "./loader"; import TelemetryUploader from "./telemetry/uploader"; import type { ConfigValue, Contexts, Duration, EvaluationCallback, EvaluationDetails, EvaluationPayload, InitOptions } from "./types"; type PollStatus = { status: "not-started"; } | { status: "pending"; } | { status: "stopped"; } | { status: "running"; frequencyInMs: number; }; export interface QuonfigBootstrap { evaluations: EvaluationPayload["evaluations"]; context: Contexts; } export declare class Quonfig { private _configs; private _telemetryUploader; private _pollCount; private _pollStatus; private _pollTimeoutId; private _instanceHash; private _collectEvaluationSummaries; private evaluationSummaryAggregator; private _contexts; private _loggerKey; private _loadedContextSig; private _dataVersion; private _heldGeneration; private _configInstalls; private _stale; private _subscribers; private _bootstrapConsumed; clientName: string; clientVersion: string; loaded: boolean; loader: Loader | undefined; afterEvaluationCallback: EvaluationCallback; /** * Initialize the SDK. Must be called before any other methods. */ init({ sdkKey, context, domain, apiUrls, apiUrl, telemetryUrl, timeout, hedgeDelay, afterEvaluationCallback, collectEvaluationSummaries, collectContextMode, loggerKey, }: InitOptions): Promise; get configs(): { [key: string]: Config; }; get contexts(): Contexts; get instanceHash(): string; get pollTimeoutId(): NodeJS.Timeout | undefined; get pollCount(): number; get pollStatus(): PollStatus; get telemetryUploader(): TelemetryUploader | undefined; /** The init-time `loggerKey` used by the `shouldLog({loggerPath, ...})` overload. */ get loggerKey(): string | undefined; /** * Monotonic version counter that increments every time the in-memory config * changes (via `setConfig` or `hydrate`). Pair with `subscribe()` and * React's `useSyncExternalStore` to drive re-renders on poll updates. */ get dataVersion(): number; /** * Meta.generation of the config the client is currently holding (0 before the * first install, or when the server predates the watermark / is the depth-1 * secondary's gen=1 floor). A higher generation is strictly newer; the * reject-older guard compares against it on every network install path * (qfg-7h5d.2.1, spec 5f). */ get heldGeneration(): number; /** * Whether the currently-served config came from the last-known-good * localStorage cache because every API URL was unreachable (spec 5h). When * true the values are non-authoritative; the next successful network load * heals this back to false. `getDetails()` also reports reason "STALE". */ get stale(): boolean; /** * Register a listener invoked synchronously after every config mutation * (poll fetch, `setConfig`, `hydrate`). Returns an unsubscribe function. * * Listeners must not throw — exceptions are swallowed so one bad subscriber * cannot break the others. */ subscribe(listener: () => void): () => void; private notifySubscribers; /** * Internal: load configs from server. */ private load; /** * Apply a loader result to the in-memory config, enforcing the reject-older * install guard (qfg-7h5d.2.1, spec 5f). Shared by `load()` and `poll()` so * the guard lives in exactly one place. `sig` is the encoded signature of the * context that was requested. * * Two orthogonal conditions gate an install: * 1. Did the server return data for us to apply? A 200 (or a 304 whose * cached payload is for a DIFFERENT context than `_configs` currently * holds, after an `updateContext`) does; a 304 for the already-held * context is a no-op. * 2. Is the data new enough to install? A SAME-context refresh runs through * `shouldInstall` — the reject-older watermark check that stops a slow * primary or a failover to the stale (gen=1) secondary from regressing or * flapping an established client. A context SWITCH is a different query * whose generation is not comparable to the held one, so it always * installs (the "fresh for this context" case — a stale secondary may * seed it, bounded, exactly as the spec allows; the guard must never * strand `updateContext` on the held generation). */ private applyLoaderResult; /** * Canonical reject-older rule for a SAME-context network install (mirrors * sdk-node/src/quonfig.ts:1251 `shouldInstall`). Reject-older is the whole * rule — there is no source ranking (spec 5f point 2): * * - A fresh client (nothing installed yet) accepts the first snapshot, even * an unversioned or stale-secondary one — it has nothing to regress. * - An incoming generation <= 0 (absent, or a server that predates the * watermark) carries no ordering information, so it cannot be rejected as * "older": install it (the carve-out). This is mandatory from day one so * the frontend never repeats the backend's 5-of-6 miss where established * clients froze against gen=0 servers (qfg-7h5d.1.18). * - Otherwise install iff the incoming generation strictly exceeds the held * one. Equal-or-lower is a no-op, so a late failover can't move an * established client backward, an equal second leg can't flap, and a later * newer leg heals forward. * * The depth-1 secondary's generation is a positive 1, so it is rejected by the * strict-greater check (an established client holds a far higher primary * generation, so `1 > held` is false — spec 5f.1), NOT by the carve-out. Only * a truly unversioned (<= 0) payload takes the carve-out. */ private shouldInstall; /** * Update the context and re-fetch evaluated configs from the server. */ updateContext(context: Contexts, skipLoad?: boolean): Promise; /** * Start polling the server for config updates at the given frequency. */ poll({ frequencyInMs }: { frequencyInMs: number; }): Promise; private doPolling; /** * Stop polling for config updates. */ stopPolling(): void; /** * Drain in-memory telemetry counters by POSTing them to the telemetry * endpoint. Use this when you want to ensure counters are shipped without * tearing down the SDK (e.g. before a context swap in a long-lived SPA). */ flush(): Promise; /** * Tear down the SDK: drain telemetry, then stop polling and telemetry timers. */ close(): Promise; /** * Stop telemetry aggregator timers without draining. Prefer `close()` or * `flush()` for normal teardown — those drain pending counters first. */ stopTelemetry(): void; /** * Set configs from a raw evaluation payload. This is the single install * mutation every network path funnels through; it stamps the held generation * watermark and bumps the install count so the reject-older guard * (`shouldInstall`) can order the next install. Callers on a network path go * through `applyLoaderResult`, which enforces the guard first; `setConfig` * itself installs unconditionally (a local SSR `hydrate`/bootstrap seed is a * source of truth and intentionally skips the guard). */ setConfig(rawValues: EvaluationPayload): void; /** * Seed the client with pre-evaluated flags (e.g. for SSR hydration). * Flags are flat key-value pairs: { flagKey: value }. */ hydrate(flags: Record): void; /** * Extract the current evaluated config as a flat key-value map. */ extract(): Record; /** * Check if a feature flag is enabled. Returns false for any non-true value. */ isEnabled(key: string): boolean; /** * Get the evaluated value for a config key. */ get(key: string): ConfigValue; /** * Return the evaluated value plus OpenFeature-style resolution details * (reason, variant, flagMetadata) for the given key. * * Mirrors sdk-node's `getBoolDetails` / `getStringDetails` etc. so the * openfeature-web provider can populate `variant` and `flagMetadata` per * `project/plans/openfeature-resolution-details.md`. */ getDetails(key: string): EvaluationDetails; /** * Get the evaluated value for a key, asserting it is a Duration. */ getDuration(key: string): Duration | undefined; /** * Determine whether a log message at the given level should be emitted. * * Two shapes are supported: * * 1. `{configKey, ...}` — primitive shape. Evaluates the named config as a * log level. The caller is responsible for any per-logger routing. * * 2. `{loggerPath, ...}` — convenience shape. Requires `loggerKey` on * `init()`. The SDK uses `loggerKey` as the underlying config key and * injects `contexts["quonfig-sdk-logging"] = { key: loggerPath }` into * the live client contexts so the logger path is auto-captured by the * existing example-context telemetry. `loggerPath` is passed through * without normalization. */ shouldLog(args: { configKey: string; desiredLevel: string; defaultLevel: string; }): boolean; shouldLog(args: { loggerPath: string; desiredLevel: string; defaultLevel?: string; }): boolean; /** * Whether evaluation summary telemetry is being collected. */ isCollectingEvaluationSummaries(): boolean; } /** Singleton instance for convenience. */ export declare const quonfig: Quonfig; export {};