@quonfig/node 0.0.1 → 0.0.3

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.d.cts CHANGED
@@ -56,6 +56,23 @@ interface ConfigEnvelope {
56
56
  configs: ConfigResponse[];
57
57
  meta: Meta;
58
58
  }
59
+ interface WorkspaceEnvironment {
60
+ id: string;
61
+ rules: Rule[];
62
+ }
63
+ interface WorkspaceConfigDocument {
64
+ id?: string;
65
+ key: string;
66
+ type: ConfigTypeString;
67
+ valueType: ValueType;
68
+ sendToClientSdk?: boolean;
69
+ default?: RuleSet;
70
+ environments?: WorkspaceEnvironment[];
71
+ }
72
+ type QuonfigDatadirEnvironments = Record<string, string> | Array<string | {
73
+ id?: string;
74
+ name?: string;
75
+ }>;
59
76
  type ContextValue = string | number | boolean | string[] | null | undefined;
60
77
  type Contexts = {
61
78
  [contextName: string]: {
@@ -67,7 +84,10 @@ type OnNoDefault = "error" | "warn" | "ignore";
67
84
  type ContextUploadMode = "none" | "shapes_only" | "periodic_example";
68
85
  interface QuonfigOptions {
69
86
  sdkKey: string;
87
+ /** @deprecated Use apiUrls instead. If provided, used as a single-element URL list. */
70
88
  apiUrl?: string;
89
+ /** Ordered list of API base URLs to try. Defaults to ["https://primary.quonfig.com", "https://secondary.quonfig.com"]. */
90
+ apiUrls?: string[];
71
91
  /** Base URL for the dedicated telemetry service. Defaults to https://telemetry.quonfig.com. Overridden by QUONFIG_TELEMETRY_URL env var. */
72
92
  telemetryUrl?: string;
73
93
  enableSSE?: boolean;
@@ -80,6 +100,7 @@ interface QuonfigOptions {
80
100
  collectLoggerCounts?: boolean;
81
101
  contextUploadMode?: ContextUploadMode;
82
102
  initTimeout?: number;
103
+ datadir?: string;
83
104
  datafile?: string | object;
84
105
  }
85
106
  interface EvalMatch {
@@ -181,6 +202,7 @@ declare class BoundQuonfig {
181
202
  defaultLevel?: string;
182
203
  contexts?: Contexts;
183
204
  }): boolean;
205
+ flush(): Promise<void>;
184
206
  keys(): string[];
185
207
  inContext(contexts: Contexts): BoundQuonfig;
186
208
  }
@@ -196,7 +218,7 @@ declare class BoundQuonfig {
196
218
  */
197
219
  declare class Quonfig {
198
220
  private readonly sdkKey;
199
- private readonly apiUrl;
221
+ private readonly apiUrls;
200
222
  private readonly telemetryUrl?;
201
223
  private readonly enableSSE;
202
224
  private readonly enablePolling;
@@ -205,6 +227,7 @@ declare class Quonfig {
205
227
  private readonly onNoDefault;
206
228
  private readonly globalContext?;
207
229
  private readonly initTimeout;
230
+ private readonly datadir?;
208
231
  private readonly datafile?;
209
232
  private store;
210
233
  private evaluator;
@@ -221,7 +244,7 @@ declare class Quonfig {
221
244
  private exampleContexts;
222
245
  constructor(options: QuonfigOptions);
223
246
  /**
224
- * Initialize the SDK. Downloads configs from the API (or loads from datafile)
247
+ * Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
225
248
  * and starts background update mechanisms (SSE/polling).
226
249
  *
227
250
  * Must be called before using any get* methods.
@@ -281,13 +304,26 @@ declare class Quonfig {
281
304
  * Create a BoundQuonfig with the given context baked in.
282
305
  */
283
306
  inContext(contexts: Contexts): BoundQuonfig;
307
+ /**
308
+ * Flush pending telemetry data immediately. Useful in serverless environments
309
+ * (Vercel, Lambda) where the process may be frozen before the background
310
+ * timer fires.
311
+ *
312
+ * ```typescript
313
+ * const value = quonfig.get("my-flag", contexts);
314
+ * await quonfig.flush();
315
+ * return NextResponse.json({ value });
316
+ * ```
317
+ */
318
+ flush(): Promise<void>;
284
319
  /**
285
320
  * Close the SDK. Stops SSE, polling, and telemetry.
286
321
  */
287
322
  close(): void;
288
323
  private requireInitialized;
289
324
  private handleNoDefault;
290
- private loadDatafile;
325
+ private loadLocalData;
326
+ private loadLocalEnvelope;
291
327
  private fetchAndInstall;
292
328
  private startSSE;
293
329
  private startPolling;
@@ -466,11 +502,12 @@ interface FetchResult {
466
502
  notChanged: boolean;
467
503
  }
468
504
  declare class Transport {
469
- private baseUrl;
505
+ private baseUrls;
506
+ private activeBaseUrl;
470
507
  private telemetryBaseUrl;
471
508
  private sdkKey;
472
509
  private etag;
473
- constructor(baseUrl: string, sdkKey: string, telemetryBaseUrl?: string);
510
+ constructor(baseUrls: string[], sdkKey: string, telemetryBaseUrl?: string);
474
511
  /**
475
512
  * Build the Basic auth header value.
476
513
  * Uses username "1" like the Go SDK: base64("1:{sdkKey}")
@@ -483,6 +520,7 @@ declare class Transport {
483
520
  /**
484
521
  * Fetch configs from GET /api/v2/configs with ETag caching.
485
522
  *
523
+ * Tries each base URL in order. Returns the first successful result.
486
524
  * Returns `{ notChanged: true }` if the server responds with 304.
487
525
  */
488
526
  fetchConfigs(): Promise<FetchResult>;
@@ -492,6 +530,7 @@ declare class Transport {
492
530
  postTelemetry(data: any): Promise<void>;
493
531
  /**
494
532
  * Get the SSE URL for config streaming.
533
+ * Uses whichever base URL last succeeded for fetchConfigs.
495
534
  */
496
535
  getSSEUrl(): string;
497
536
  /**
@@ -648,7 +687,7 @@ declare class TelemetryReporter {
648
687
  */
649
688
  stop(): void;
650
689
  private scheduleNext;
651
- private sync;
690
+ sync(): Promise<void>;
652
691
  }
653
692
 
654
693
  /**
@@ -752,4 +791,4 @@ declare const encryption: {
752
791
  generateNewHexKey: typeof generateNewHexKey;
753
792
  };
754
793
 
755
- export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
794
+ export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigDatadirEnvironments, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, type WorkspaceConfigDocument, type WorkspaceEnvironment, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
package/dist/index.d.ts CHANGED
@@ -56,6 +56,23 @@ interface ConfigEnvelope {
56
56
  configs: ConfigResponse[];
57
57
  meta: Meta;
58
58
  }
59
+ interface WorkspaceEnvironment {
60
+ id: string;
61
+ rules: Rule[];
62
+ }
63
+ interface WorkspaceConfigDocument {
64
+ id?: string;
65
+ key: string;
66
+ type: ConfigTypeString;
67
+ valueType: ValueType;
68
+ sendToClientSdk?: boolean;
69
+ default?: RuleSet;
70
+ environments?: WorkspaceEnvironment[];
71
+ }
72
+ type QuonfigDatadirEnvironments = Record<string, string> | Array<string | {
73
+ id?: string;
74
+ name?: string;
75
+ }>;
59
76
  type ContextValue = string | number | boolean | string[] | null | undefined;
60
77
  type Contexts = {
61
78
  [contextName: string]: {
@@ -67,7 +84,10 @@ type OnNoDefault = "error" | "warn" | "ignore";
67
84
  type ContextUploadMode = "none" | "shapes_only" | "periodic_example";
68
85
  interface QuonfigOptions {
69
86
  sdkKey: string;
87
+ /** @deprecated Use apiUrls instead. If provided, used as a single-element URL list. */
70
88
  apiUrl?: string;
89
+ /** Ordered list of API base URLs to try. Defaults to ["https://primary.quonfig.com", "https://secondary.quonfig.com"]. */
90
+ apiUrls?: string[];
71
91
  /** Base URL for the dedicated telemetry service. Defaults to https://telemetry.quonfig.com. Overridden by QUONFIG_TELEMETRY_URL env var. */
72
92
  telemetryUrl?: string;
73
93
  enableSSE?: boolean;
@@ -80,6 +100,7 @@ interface QuonfigOptions {
80
100
  collectLoggerCounts?: boolean;
81
101
  contextUploadMode?: ContextUploadMode;
82
102
  initTimeout?: number;
103
+ datadir?: string;
83
104
  datafile?: string | object;
84
105
  }
85
106
  interface EvalMatch {
@@ -181,6 +202,7 @@ declare class BoundQuonfig {
181
202
  defaultLevel?: string;
182
203
  contexts?: Contexts;
183
204
  }): boolean;
205
+ flush(): Promise<void>;
184
206
  keys(): string[];
185
207
  inContext(contexts: Contexts): BoundQuonfig;
186
208
  }
@@ -196,7 +218,7 @@ declare class BoundQuonfig {
196
218
  */
197
219
  declare class Quonfig {
198
220
  private readonly sdkKey;
199
- private readonly apiUrl;
221
+ private readonly apiUrls;
200
222
  private readonly telemetryUrl?;
201
223
  private readonly enableSSE;
202
224
  private readonly enablePolling;
@@ -205,6 +227,7 @@ declare class Quonfig {
205
227
  private readonly onNoDefault;
206
228
  private readonly globalContext?;
207
229
  private readonly initTimeout;
230
+ private readonly datadir?;
208
231
  private readonly datafile?;
209
232
  private store;
210
233
  private evaluator;
@@ -221,7 +244,7 @@ declare class Quonfig {
221
244
  private exampleContexts;
222
245
  constructor(options: QuonfigOptions);
223
246
  /**
224
- * Initialize the SDK. Downloads configs from the API (or loads from datafile)
247
+ * Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
225
248
  * and starts background update mechanisms (SSE/polling).
226
249
  *
227
250
  * Must be called before using any get* methods.
@@ -281,13 +304,26 @@ declare class Quonfig {
281
304
  * Create a BoundQuonfig with the given context baked in.
282
305
  */
283
306
  inContext(contexts: Contexts): BoundQuonfig;
307
+ /**
308
+ * Flush pending telemetry data immediately. Useful in serverless environments
309
+ * (Vercel, Lambda) where the process may be frozen before the background
310
+ * timer fires.
311
+ *
312
+ * ```typescript
313
+ * const value = quonfig.get("my-flag", contexts);
314
+ * await quonfig.flush();
315
+ * return NextResponse.json({ value });
316
+ * ```
317
+ */
318
+ flush(): Promise<void>;
284
319
  /**
285
320
  * Close the SDK. Stops SSE, polling, and telemetry.
286
321
  */
287
322
  close(): void;
288
323
  private requireInitialized;
289
324
  private handleNoDefault;
290
- private loadDatafile;
325
+ private loadLocalData;
326
+ private loadLocalEnvelope;
291
327
  private fetchAndInstall;
292
328
  private startSSE;
293
329
  private startPolling;
@@ -466,11 +502,12 @@ interface FetchResult {
466
502
  notChanged: boolean;
467
503
  }
468
504
  declare class Transport {
469
- private baseUrl;
505
+ private baseUrls;
506
+ private activeBaseUrl;
470
507
  private telemetryBaseUrl;
471
508
  private sdkKey;
472
509
  private etag;
473
- constructor(baseUrl: string, sdkKey: string, telemetryBaseUrl?: string);
510
+ constructor(baseUrls: string[], sdkKey: string, telemetryBaseUrl?: string);
474
511
  /**
475
512
  * Build the Basic auth header value.
476
513
  * Uses username "1" like the Go SDK: base64("1:{sdkKey}")
@@ -483,6 +520,7 @@ declare class Transport {
483
520
  /**
484
521
  * Fetch configs from GET /api/v2/configs with ETag caching.
485
522
  *
523
+ * Tries each base URL in order. Returns the first successful result.
486
524
  * Returns `{ notChanged: true }` if the server responds with 304.
487
525
  */
488
526
  fetchConfigs(): Promise<FetchResult>;
@@ -492,6 +530,7 @@ declare class Transport {
492
530
  postTelemetry(data: any): Promise<void>;
493
531
  /**
494
532
  * Get the SSE URL for config streaming.
533
+ * Uses whichever base URL last succeeded for fetchConfigs.
495
534
  */
496
535
  getSSEUrl(): string;
497
536
  /**
@@ -648,7 +687,7 @@ declare class TelemetryReporter {
648
687
  */
649
688
  stop(): void;
650
689
  private scheduleNext;
651
- private sync;
690
+ sync(): Promise<void>;
652
691
  }
653
692
 
654
693
  /**
@@ -752,4 +791,4 @@ declare const encryption: {
752
791
  generateNewHexKey: typeof generateNewHexKey;
753
792
  };
754
793
 
755
- export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
794
+ export { BoundQuonfig, Client, type ClientOptions, type ConfigEnvelope, type ConfigResponse, ConfigStore, ConfigType, type ConfigTypeString, type ConfigValue, ConfigValueType, ContextShapeCollector, type ContextUploadMode, type ContextValue, type Contexts, type Criterion, type Environment, type EvalMatch, type Evaluation, EvaluationSummaryCollector, Evaluator, ExampleContextCollector, type GetValue, LOG_LEVEL_PREFIX, type LogLevelName, type LogLevelNumber, type Meta, type NodeServerConfigurationAccessor, type NodeServerConfigurationRaw, OP_ALWAYS_TRUE, OP_HIERARCHICAL_MATCH, OP_IN_INT_RANGE, OP_IN_SEG, OP_NOT_IN_SEG, OP_NOT_SET, OP_PROP_AFTER, OP_PROP_BEFORE, OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF, OP_PROP_DOES_NOT_MATCH, OP_PROP_DOES_NOT_START_WITH_ONE_OF, OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL, OP_PROP_IS_NOT_ONE_OF, OP_PROP_IS_ONE_OF, OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL, OP_PROP_MATCHES, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN, OP_PROP_SEMVER_LESS_THAN, OP_PROP_STARTS_WITH_ONE_OF, type OnNoDefault, type ProjectEnvId, type ProvidedData, ProvidedSource, Quonfig, type QuonfigDatadirEnvironments, type QuonfigOptions, Resolver, type Rule, type RuleSet, type SchemaData, type SegmentResolver, type SemanticVersion, TelemetryReporter, Transport, type TypedNodeServerConfigurationAccessor, type TypedNodeServerConfigurationRaw, type Value, type ValueType, type WeightedValue, WeightedValueResolver, type WeightedValuesData, type WorkspaceConfigDocument, type WorkspaceEnvironment, compareSemver, contextLookup, decrypt, durationToMilliseconds, encrypt, encryption, evaluateCriterion, generateNewHexKey, getContextValue, getProjectEnvFromSdkKey, hashZeroToOne, mergeContexts, parseLevel, parseSemver, shouldLog, valueTypeStringForConfig, wordLevelToNumber };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/quonfig.ts
2
2
  import { randomUUID } from "crypto";
3
- import { readFileSync } from "fs";
3
+ import { readFileSync as readFileSync2 } from "fs";
4
4
 
5
5
  // src/store.ts
6
6
  var ConfigStore = class {
@@ -825,12 +825,14 @@ function valueTypeForCoerced(valueType) {
825
825
  var SDK_VERSION = "0.1.0";
826
826
  var DEFAULT_TELEMETRY_URL = "https://telemetry.quonfig.com";
827
827
  var Transport = class {
828
- baseUrl;
828
+ baseUrls;
829
+ activeBaseUrl;
829
830
  telemetryBaseUrl;
830
831
  sdkKey;
831
832
  etag = "";
832
- constructor(baseUrl, sdkKey, telemetryBaseUrl) {
833
- this.baseUrl = baseUrl.replace(/\/$/, "");
833
+ constructor(baseUrls, sdkKey, telemetryBaseUrl) {
834
+ this.baseUrls = baseUrls.map((u) => u.replace(/\/$/, ""));
835
+ this.activeBaseUrl = this.baseUrls[0];
834
836
  const envUrl = process.env.QUONFIG_TELEMETRY_URL;
835
837
  const url = envUrl || telemetryBaseUrl || DEFAULT_TELEMETRY_URL;
836
838
  this.telemetryBaseUrl = url.replace(/\/$/, "");
@@ -857,30 +859,41 @@ var Transport = class {
857
859
  /**
858
860
  * Fetch configs from GET /api/v2/configs with ETag caching.
859
861
  *
862
+ * Tries each base URL in order. Returns the first successful result.
860
863
  * Returns `{ notChanged: true }` if the server responds with 304.
861
864
  */
862
865
  async fetchConfigs() {
863
- const headers = this.getHeaders();
864
- if (this.etag) {
865
- headers["If-None-Match"] = this.etag;
866
- }
867
- const response = await fetch(`${this.baseUrl}/api/v2/configs`, {
868
- method: "GET",
869
- headers
870
- });
871
- if (response.status === 304) {
872
- return { notChanged: true };
873
- }
874
- if (!response.ok) {
875
- const body = await response.text().catch(() => "");
876
- throw new Error(`Unexpected status ${response.status}: ${body}`);
877
- }
878
- const etag = response.headers.get("ETag");
879
- if (etag) {
880
- this.etag = etag;
866
+ let lastError;
867
+ for (const baseUrl of this.baseUrls) {
868
+ try {
869
+ const headers = this.getHeaders();
870
+ if (this.etag) {
871
+ headers["If-None-Match"] = this.etag;
872
+ }
873
+ const response = await fetch(`${baseUrl}/api/v2/configs`, {
874
+ method: "GET",
875
+ headers
876
+ });
877
+ if (response.status === 304) {
878
+ this.activeBaseUrl = baseUrl;
879
+ return { notChanged: true };
880
+ }
881
+ if (!response.ok) {
882
+ const body = await response.text().catch(() => "");
883
+ throw new Error(`Unexpected status ${response.status} from ${baseUrl}: ${body}`);
884
+ }
885
+ const etag = response.headers.get("ETag");
886
+ if (etag) {
887
+ this.etag = etag;
888
+ }
889
+ this.activeBaseUrl = baseUrl;
890
+ const envelope = await response.json();
891
+ return { envelope, notChanged: false };
892
+ } catch (err) {
893
+ lastError = err instanceof Error ? err : new Error(String(err));
894
+ }
881
895
  }
882
- const envelope = await response.json();
883
- return { envelope, notChanged: false };
896
+ throw lastError ?? new Error("All API URLs failed");
884
897
  }
885
898
  /**
886
899
  * Post telemetry data to the telemetry endpoint.
@@ -901,9 +914,10 @@ var Transport = class {
901
914
  }
902
915
  /**
903
916
  * Get the SSE URL for config streaming.
917
+ * Uses whichever base URL last succeeded for fetchConfigs.
904
918
  */
905
919
  getSSEUrl() {
906
- return `${this.baseUrl}/api/v2/sse/config`;
920
+ return `${this.activeBaseUrl}/api/v2/sse/config`;
907
921
  }
908
922
  /**
909
923
  * Get auth headers for SSE connection.
@@ -1008,7 +1022,10 @@ function shouldLog(args) {
1008
1022
  while (loggerNameWithPrefix.includes(".")) {
1009
1023
  const resolvedLevel = getConfig(loggerNameWithPrefix);
1010
1024
  if (resolvedLevel !== void 0) {
1011
- return Number(resolvedLevel) <= desiredLevel;
1025
+ const resolvedLevelNum = parseLevel(resolvedLevel);
1026
+ if (resolvedLevelNum !== void 0) {
1027
+ return resolvedLevelNum <= desiredLevel;
1028
+ }
1012
1029
  }
1013
1030
  loggerNameWithPrefix = loggerNameWithPrefix.slice(
1014
1031
  0,
@@ -1018,6 +1035,87 @@ function shouldLog(args) {
1018
1035
  return defaultLevel <= desiredLevel;
1019
1036
  }
1020
1037
 
1038
+ // src/datadir.ts
1039
+ import { existsSync, readdirSync, readFileSync } from "fs";
1040
+ import { join } from "path";
1041
+ var CONFIG_SUBDIRS = ["configs", "feature-flags", "segments", "schemas", "log-levels"];
1042
+ function loadEnvelopeFromDatadir(datadir) {
1043
+ const environmentId = loadEnvironmentId(join(datadir, "environments.json"));
1044
+ const configs = [];
1045
+ for (const subdir of CONFIG_SUBDIRS) {
1046
+ const dir = join(datadir, subdir);
1047
+ if (!existsSync(dir)) {
1048
+ continue;
1049
+ }
1050
+ const filenames = readdirSync(dir).filter((filename) => filename.endsWith(".json")).sort((a, b) => a.localeCompare(b));
1051
+ for (const filename of filenames) {
1052
+ const raw = JSON.parse(readFileSync(join(dir, filename), "utf-8"));
1053
+ configs.push(toConfigResponse(raw, environmentId));
1054
+ }
1055
+ }
1056
+ return {
1057
+ configs,
1058
+ meta: {
1059
+ version: `datadir:${datadir}`,
1060
+ environment: environmentId
1061
+ }
1062
+ };
1063
+ }
1064
+ function loadEnvironmentId(environmentsPath) {
1065
+ if (!existsSync(environmentsPath)) {
1066
+ throw new Error(`[quonfig] Datadir is missing environments.json: ${environmentsPath}`);
1067
+ }
1068
+ const environments = JSON.parse(readFileSync(environmentsPath, "utf-8"));
1069
+ const candidates = normalizeEnvironmentCandidates(
1070
+ isWrappedEnvironmentList(environments) ? environments.environments : environments
1071
+ );
1072
+ if (candidates.length === 0) {
1073
+ return "";
1074
+ }
1075
+ return candidates[0];
1076
+ }
1077
+ function isWrappedEnvironmentList(value) {
1078
+ return Boolean(
1079
+ value && typeof value === "object" && !Array.isArray(value) && "environments" in value
1080
+ );
1081
+ }
1082
+ function normalizeEnvironmentCandidates(environments) {
1083
+ if (!environments) {
1084
+ return [];
1085
+ }
1086
+ if (Array.isArray(environments)) {
1087
+ return environments.map((entry) => {
1088
+ if (typeof entry === "string") {
1089
+ return entry;
1090
+ }
1091
+ if (entry && typeof entry === "object") {
1092
+ return entry.id ?? entry.name;
1093
+ }
1094
+ return void 0;
1095
+ }).filter((entry) => typeof entry === "string" && entry.length > 0);
1096
+ }
1097
+ if (environments && typeof environments === "object") {
1098
+ const values = Object.values(environments).map((value) => typeof value === "string" && value.length > 0 ? value : void 0).filter((value) => typeof value === "string");
1099
+ if (values.length > 0) {
1100
+ return values;
1101
+ }
1102
+ return Object.keys(environments).filter((key) => key.length > 0);
1103
+ }
1104
+ return [];
1105
+ }
1106
+ function toConfigResponse(raw, environmentId) {
1107
+ const environment = raw.environments?.find((candidate) => candidate.id === environmentId);
1108
+ return {
1109
+ id: raw.id ?? "",
1110
+ key: raw.key,
1111
+ type: raw.type,
1112
+ valueType: raw.valueType,
1113
+ sendToClientSdk: raw.sendToClientSdk ?? false,
1114
+ default: raw.default ?? { rules: [] },
1115
+ environment
1116
+ };
1117
+ }
1118
+
1021
1119
  // src/telemetry/evaluationSummaries.ts
1022
1120
  var EvaluationSummaryCollector = class {
1023
1121
  enabled;
@@ -1287,7 +1385,10 @@ var TelemetryReporter = class {
1287
1385
  };
1288
1386
 
1289
1387
  // src/quonfig.ts
1290
- var DEFAULT_API_URL = "https://api.quonfig.com";
1388
+ var DEFAULT_API_URLS = [
1389
+ "https://primary.quonfig.com",
1390
+ "https://secondary.quonfig.com"
1391
+ ];
1291
1392
  var DEFAULT_POLL_INTERVAL = 6e4;
1292
1393
  var DEFAULT_INIT_TIMEOUT = 1e4;
1293
1394
  var DEFAULT_LOG_LEVEL = 5;
@@ -1328,6 +1429,9 @@ var BoundQuonfig = class _BoundQuonfig {
1328
1429
  contexts: mergeContexts(this.boundContexts, args.contexts)
1329
1430
  });
1330
1431
  }
1432
+ async flush() {
1433
+ return this.client.flush();
1434
+ }
1331
1435
  keys() {
1332
1436
  return this.client.keys();
1333
1437
  }
@@ -1337,7 +1441,7 @@ var BoundQuonfig = class _BoundQuonfig {
1337
1441
  };
1338
1442
  var Quonfig = class {
1339
1443
  sdkKey;
1340
- apiUrl;
1444
+ apiUrls;
1341
1445
  telemetryUrl;
1342
1446
  enableSSE;
1343
1447
  enablePolling;
@@ -1346,6 +1450,7 @@ var Quonfig = class {
1346
1450
  onNoDefault;
1347
1451
  globalContext;
1348
1452
  initTimeout;
1453
+ datadir;
1349
1454
  datafile;
1350
1455
  store;
1351
1456
  evaluator;
@@ -1363,7 +1468,10 @@ var Quonfig = class {
1363
1468
  exampleContexts;
1364
1469
  constructor(options) {
1365
1470
  this.sdkKey = options.sdkKey;
1366
- this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
1471
+ this.apiUrls = options.apiUrls ?? (options.apiUrl ? [options.apiUrl] : DEFAULT_API_URLS);
1472
+ if (this.apiUrls.length === 0) {
1473
+ throw new Error("[quonfig] apiUrls must not be empty");
1474
+ }
1367
1475
  this.telemetryUrl = options.telemetryUrl;
1368
1476
  this.enableSSE = options.enableSSE ?? true;
1369
1477
  this.enablePolling = options.enablePolling ?? false;
@@ -1372,12 +1480,13 @@ var Quonfig = class {
1372
1480
  this.onNoDefault = options.onNoDefault ?? "error";
1373
1481
  this.globalContext = options.globalContext;
1374
1482
  this.initTimeout = options.initTimeout ?? DEFAULT_INIT_TIMEOUT;
1483
+ this.datadir = options.datadir;
1375
1484
  this.datafile = options.datafile;
1376
1485
  this.instanceHash = randomUUID();
1377
1486
  this.store = new ConfigStore();
1378
1487
  this.evaluator = new Evaluator(this.store);
1379
1488
  this.resolver = new Resolver(this.store, this.evaluator);
1380
- this.transport = new Transport(this.apiUrl, this.sdkKey, this.telemetryUrl);
1489
+ this.transport = new Transport(this.apiUrls, this.sdkKey, this.telemetryUrl);
1381
1490
  const contextUploadMode = options.contextUploadMode ?? "periodic_example";
1382
1491
  this.evaluationSummaries = new EvaluationSummaryCollector(
1383
1492
  options.collectEvaluationSummaries ?? true
@@ -1386,14 +1495,14 @@ var Quonfig = class {
1386
1495
  this.exampleContexts = new ExampleContextCollector(contextUploadMode);
1387
1496
  }
1388
1497
  /**
1389
- * Initialize the SDK. Downloads configs from the API (or loads from datafile)
1498
+ * Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
1390
1499
  * and starts background update mechanisms (SSE/polling).
1391
1500
  *
1392
1501
  * Must be called before using any get* methods.
1393
1502
  */
1394
1503
  async init() {
1395
- if (this.datafile) {
1396
- this.loadDatafile();
1504
+ if (this.datadir || this.datafile) {
1505
+ this.loadLocalData();
1397
1506
  this.initialized = true;
1398
1507
  return;
1399
1508
  }
@@ -1561,6 +1670,22 @@ var Quonfig = class {
1561
1670
  inContext(contexts) {
1562
1671
  return new BoundQuonfig(this, mergeContexts(this.globalContext, contexts));
1563
1672
  }
1673
+ /**
1674
+ * Flush pending telemetry data immediately. Useful in serverless environments
1675
+ * (Vercel, Lambda) where the process may be frozen before the background
1676
+ * timer fires.
1677
+ *
1678
+ * ```typescript
1679
+ * const value = quonfig.get("my-flag", contexts);
1680
+ * await quonfig.flush();
1681
+ * return NextResponse.json({ value });
1682
+ * ```
1683
+ */
1684
+ async flush() {
1685
+ if (this.telemetryReporter) {
1686
+ await this.telemetryReporter.sync();
1687
+ }
1688
+ }
1564
1689
  /**
1565
1690
  * Close the SDK. Stops SSE, polling, and telemetry.
1566
1691
  */
@@ -1598,19 +1723,24 @@ var Quonfig = class {
1598
1723
  return void 0;
1599
1724
  }
1600
1725
  }
1601
- loadDatafile() {
1602
- let data;
1603
- if (typeof this.datafile === "string") {
1604
- const raw = readFileSync(this.datafile, "utf-8");
1605
- data = JSON.parse(raw);
1606
- } else if (typeof this.datafile === "object") {
1607
- data = this.datafile;
1608
- } else {
1609
- throw new Error("Invalid datafile option");
1610
- }
1726
+ loadLocalData() {
1727
+ const data = this.loadLocalEnvelope();
1611
1728
  this.store.update(data);
1612
1729
  this.environmentId = data.meta.environment;
1613
1730
  }
1731
+ loadLocalEnvelope() {
1732
+ if (this.datadir) {
1733
+ return loadEnvelopeFromDatadir(this.datadir);
1734
+ }
1735
+ if (typeof this.datafile === "string") {
1736
+ const raw = readFileSync2(this.datafile, "utf-8");
1737
+ return JSON.parse(raw);
1738
+ }
1739
+ if (typeof this.datafile === "object") {
1740
+ return this.datafile;
1741
+ }
1742
+ throw new Error("Invalid local configuration: expected datadir or datafile");
1743
+ }
1614
1744
  async fetchAndInstall() {
1615
1745
  const result = await this.transport.fetchConfigs();
1616
1746
  if (result.notChanged) {
@@ -1704,11 +1834,28 @@ var Client = class {
1704
1834
  }
1705
1835
  async post(path, payload) {
1706
1836
  const url = `${this.apiUrl}${path}`;
1837
+ const isORPC = path.startsWith("/api/v1/");
1707
1838
  this.log("ApiClient", `POST ${url}`);
1708
- return fetch(url, {
1839
+ const body = isORPC ? JSON.stringify({ json: payload }) : JSON.stringify(payload);
1840
+ const raw = await fetch(url, {
1709
1841
  method: "POST",
1710
1842
  headers: this.headers(),
1711
- body: JSON.stringify(payload)
1843
+ body
1844
+ });
1845
+ if (!isORPC) return raw;
1846
+ const text = await raw.text();
1847
+ let unwrapped = text;
1848
+ try {
1849
+ const parsed = JSON.parse(text);
1850
+ if (parsed && typeof parsed === "object" && "json" in parsed) {
1851
+ unwrapped = JSON.stringify(parsed.json);
1852
+ }
1853
+ } catch {
1854
+ }
1855
+ return new Response(unwrapped, {
1856
+ status: raw.status,
1857
+ statusText: raw.statusText,
1858
+ headers: raw.headers
1712
1859
  });
1713
1860
  }
1714
1861
  async put(path, payload) {