@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/README.md +2 -1
- package/dist/index.cjs +192 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -7
- package/dist/index.d.ts +46 -7
- package/dist/index.js +192 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
505
|
+
private baseUrls;
|
|
506
|
+
private activeBaseUrl;
|
|
470
507
|
private telemetryBaseUrl;
|
|
471
508
|
private sdkKey;
|
|
472
509
|
private etag;
|
|
473
|
-
constructor(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
505
|
+
private baseUrls;
|
|
506
|
+
private activeBaseUrl;
|
|
470
507
|
private telemetryBaseUrl;
|
|
471
508
|
private sdkKey;
|
|
472
509
|
private etag;
|
|
473
|
-
constructor(
|
|
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
|
-
|
|
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
|
-
|
|
828
|
+
baseUrls;
|
|
829
|
+
activeBaseUrl;
|
|
829
830
|
telemetryBaseUrl;
|
|
830
831
|
sdkKey;
|
|
831
832
|
etag = "";
|
|
832
|
-
constructor(
|
|
833
|
-
this.
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|