@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/README.md
CHANGED
|
@@ -55,7 +55,8 @@ new Quonfig({
|
|
|
55
55
|
initTimeout: 10000, // Init timeout in ms (default: 10000)
|
|
56
56
|
onNoDefault: "error", // "error" | "warn" | "ignore" (default: "error")
|
|
57
57
|
globalContext: { ... }, // Context applied to all evaluations
|
|
58
|
-
|
|
58
|
+
datadir: "./workspace-data", // Load local workspace directories instead of API
|
|
59
|
+
datafile: "./config.json", // Legacy local envelope path
|
|
59
60
|
});
|
|
60
61
|
```
|
|
61
62
|
|
package/dist/index.cjs
CHANGED
|
@@ -93,7 +93,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
93
93
|
|
|
94
94
|
// src/quonfig.ts
|
|
95
95
|
var import_crypto3 = require("crypto");
|
|
96
|
-
var
|
|
96
|
+
var import_fs2 = require("fs");
|
|
97
97
|
|
|
98
98
|
// src/store.ts
|
|
99
99
|
var ConfigStore = class {
|
|
@@ -918,12 +918,14 @@ function valueTypeForCoerced(valueType) {
|
|
|
918
918
|
var SDK_VERSION = "0.1.0";
|
|
919
919
|
var DEFAULT_TELEMETRY_URL = "https://telemetry.quonfig.com";
|
|
920
920
|
var Transport = class {
|
|
921
|
-
|
|
921
|
+
baseUrls;
|
|
922
|
+
activeBaseUrl;
|
|
922
923
|
telemetryBaseUrl;
|
|
923
924
|
sdkKey;
|
|
924
925
|
etag = "";
|
|
925
|
-
constructor(
|
|
926
|
-
this.
|
|
926
|
+
constructor(baseUrls, sdkKey, telemetryBaseUrl) {
|
|
927
|
+
this.baseUrls = baseUrls.map((u) => u.replace(/\/$/, ""));
|
|
928
|
+
this.activeBaseUrl = this.baseUrls[0];
|
|
927
929
|
const envUrl = process.env.QUONFIG_TELEMETRY_URL;
|
|
928
930
|
const url = envUrl || telemetryBaseUrl || DEFAULT_TELEMETRY_URL;
|
|
929
931
|
this.telemetryBaseUrl = url.replace(/\/$/, "");
|
|
@@ -950,30 +952,41 @@ var Transport = class {
|
|
|
950
952
|
/**
|
|
951
953
|
* Fetch configs from GET /api/v2/configs with ETag caching.
|
|
952
954
|
*
|
|
955
|
+
* Tries each base URL in order. Returns the first successful result.
|
|
953
956
|
* Returns `{ notChanged: true }` if the server responds with 304.
|
|
954
957
|
*/
|
|
955
958
|
async fetchConfigs() {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
959
|
+
let lastError;
|
|
960
|
+
for (const baseUrl of this.baseUrls) {
|
|
961
|
+
try {
|
|
962
|
+
const headers = this.getHeaders();
|
|
963
|
+
if (this.etag) {
|
|
964
|
+
headers["If-None-Match"] = this.etag;
|
|
965
|
+
}
|
|
966
|
+
const response = await fetch(`${baseUrl}/api/v2/configs`, {
|
|
967
|
+
method: "GET",
|
|
968
|
+
headers
|
|
969
|
+
});
|
|
970
|
+
if (response.status === 304) {
|
|
971
|
+
this.activeBaseUrl = baseUrl;
|
|
972
|
+
return { notChanged: true };
|
|
973
|
+
}
|
|
974
|
+
if (!response.ok) {
|
|
975
|
+
const body = await response.text().catch(() => "");
|
|
976
|
+
throw new Error(`Unexpected status ${response.status} from ${baseUrl}: ${body}`);
|
|
977
|
+
}
|
|
978
|
+
const etag = response.headers.get("ETag");
|
|
979
|
+
if (etag) {
|
|
980
|
+
this.etag = etag;
|
|
981
|
+
}
|
|
982
|
+
this.activeBaseUrl = baseUrl;
|
|
983
|
+
const envelope = await response.json();
|
|
984
|
+
return { envelope, notChanged: false };
|
|
985
|
+
} catch (err) {
|
|
986
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
987
|
+
}
|
|
974
988
|
}
|
|
975
|
-
|
|
976
|
-
return { envelope, notChanged: false };
|
|
989
|
+
throw lastError ?? new Error("All API URLs failed");
|
|
977
990
|
}
|
|
978
991
|
/**
|
|
979
992
|
* Post telemetry data to the telemetry endpoint.
|
|
@@ -994,9 +1007,10 @@ var Transport = class {
|
|
|
994
1007
|
}
|
|
995
1008
|
/**
|
|
996
1009
|
* Get the SSE URL for config streaming.
|
|
1010
|
+
* Uses whichever base URL last succeeded for fetchConfigs.
|
|
997
1011
|
*/
|
|
998
1012
|
getSSEUrl() {
|
|
999
|
-
return `${this.
|
|
1013
|
+
return `${this.activeBaseUrl}/api/v2/sse/config`;
|
|
1000
1014
|
}
|
|
1001
1015
|
/**
|
|
1002
1016
|
* Get auth headers for SSE connection.
|
|
@@ -1101,7 +1115,10 @@ function shouldLog(args) {
|
|
|
1101
1115
|
while (loggerNameWithPrefix.includes(".")) {
|
|
1102
1116
|
const resolvedLevel = getConfig(loggerNameWithPrefix);
|
|
1103
1117
|
if (resolvedLevel !== void 0) {
|
|
1104
|
-
|
|
1118
|
+
const resolvedLevelNum = parseLevel(resolvedLevel);
|
|
1119
|
+
if (resolvedLevelNum !== void 0) {
|
|
1120
|
+
return resolvedLevelNum <= desiredLevel;
|
|
1121
|
+
}
|
|
1105
1122
|
}
|
|
1106
1123
|
loggerNameWithPrefix = loggerNameWithPrefix.slice(
|
|
1107
1124
|
0,
|
|
@@ -1111,6 +1128,87 @@ function shouldLog(args) {
|
|
|
1111
1128
|
return defaultLevel <= desiredLevel;
|
|
1112
1129
|
}
|
|
1113
1130
|
|
|
1131
|
+
// src/datadir.ts
|
|
1132
|
+
var import_fs = require("fs");
|
|
1133
|
+
var import_path = require("path");
|
|
1134
|
+
var CONFIG_SUBDIRS = ["configs", "feature-flags", "segments", "schemas", "log-levels"];
|
|
1135
|
+
function loadEnvelopeFromDatadir(datadir) {
|
|
1136
|
+
const environmentId = loadEnvironmentId((0, import_path.join)(datadir, "environments.json"));
|
|
1137
|
+
const configs = [];
|
|
1138
|
+
for (const subdir of CONFIG_SUBDIRS) {
|
|
1139
|
+
const dir = (0, import_path.join)(datadir, subdir);
|
|
1140
|
+
if (!(0, import_fs.existsSync)(dir)) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const filenames = (0, import_fs.readdirSync)(dir).filter((filename) => filename.endsWith(".json")).sort((a, b) => a.localeCompare(b));
|
|
1144
|
+
for (const filename of filenames) {
|
|
1145
|
+
const raw = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)(dir, filename), "utf-8"));
|
|
1146
|
+
configs.push(toConfigResponse(raw, environmentId));
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
configs,
|
|
1151
|
+
meta: {
|
|
1152
|
+
version: `datadir:${datadir}`,
|
|
1153
|
+
environment: environmentId
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function loadEnvironmentId(environmentsPath) {
|
|
1158
|
+
if (!(0, import_fs.existsSync)(environmentsPath)) {
|
|
1159
|
+
throw new Error(`[quonfig] Datadir is missing environments.json: ${environmentsPath}`);
|
|
1160
|
+
}
|
|
1161
|
+
const environments = JSON.parse((0, import_fs.readFileSync)(environmentsPath, "utf-8"));
|
|
1162
|
+
const candidates = normalizeEnvironmentCandidates(
|
|
1163
|
+
isWrappedEnvironmentList(environments) ? environments.environments : environments
|
|
1164
|
+
);
|
|
1165
|
+
if (candidates.length === 0) {
|
|
1166
|
+
return "";
|
|
1167
|
+
}
|
|
1168
|
+
return candidates[0];
|
|
1169
|
+
}
|
|
1170
|
+
function isWrappedEnvironmentList(value) {
|
|
1171
|
+
return Boolean(
|
|
1172
|
+
value && typeof value === "object" && !Array.isArray(value) && "environments" in value
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
function normalizeEnvironmentCandidates(environments) {
|
|
1176
|
+
if (!environments) {
|
|
1177
|
+
return [];
|
|
1178
|
+
}
|
|
1179
|
+
if (Array.isArray(environments)) {
|
|
1180
|
+
return environments.map((entry) => {
|
|
1181
|
+
if (typeof entry === "string") {
|
|
1182
|
+
return entry;
|
|
1183
|
+
}
|
|
1184
|
+
if (entry && typeof entry === "object") {
|
|
1185
|
+
return entry.id ?? entry.name;
|
|
1186
|
+
}
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}).filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
1189
|
+
}
|
|
1190
|
+
if (environments && typeof environments === "object") {
|
|
1191
|
+
const values = Object.values(environments).map((value) => typeof value === "string" && value.length > 0 ? value : void 0).filter((value) => typeof value === "string");
|
|
1192
|
+
if (values.length > 0) {
|
|
1193
|
+
return values;
|
|
1194
|
+
}
|
|
1195
|
+
return Object.keys(environments).filter((key) => key.length > 0);
|
|
1196
|
+
}
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
function toConfigResponse(raw, environmentId) {
|
|
1200
|
+
const environment = raw.environments?.find((candidate) => candidate.id === environmentId);
|
|
1201
|
+
return {
|
|
1202
|
+
id: raw.id ?? "",
|
|
1203
|
+
key: raw.key,
|
|
1204
|
+
type: raw.type,
|
|
1205
|
+
valueType: raw.valueType,
|
|
1206
|
+
sendToClientSdk: raw.sendToClientSdk ?? false,
|
|
1207
|
+
default: raw.default ?? { rules: [] },
|
|
1208
|
+
environment
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1114
1212
|
// src/telemetry/evaluationSummaries.ts
|
|
1115
1213
|
var EvaluationSummaryCollector = class {
|
|
1116
1214
|
enabled;
|
|
@@ -1380,7 +1478,10 @@ var TelemetryReporter = class {
|
|
|
1380
1478
|
};
|
|
1381
1479
|
|
|
1382
1480
|
// src/quonfig.ts
|
|
1383
|
-
var
|
|
1481
|
+
var DEFAULT_API_URLS = [
|
|
1482
|
+
"https://primary.quonfig.com",
|
|
1483
|
+
"https://secondary.quonfig.com"
|
|
1484
|
+
];
|
|
1384
1485
|
var DEFAULT_POLL_INTERVAL = 6e4;
|
|
1385
1486
|
var DEFAULT_INIT_TIMEOUT = 1e4;
|
|
1386
1487
|
var DEFAULT_LOG_LEVEL = 5;
|
|
@@ -1421,6 +1522,9 @@ var BoundQuonfig = class _BoundQuonfig {
|
|
|
1421
1522
|
contexts: mergeContexts(this.boundContexts, args.contexts)
|
|
1422
1523
|
});
|
|
1423
1524
|
}
|
|
1525
|
+
async flush() {
|
|
1526
|
+
return this.client.flush();
|
|
1527
|
+
}
|
|
1424
1528
|
keys() {
|
|
1425
1529
|
return this.client.keys();
|
|
1426
1530
|
}
|
|
@@ -1430,7 +1534,7 @@ var BoundQuonfig = class _BoundQuonfig {
|
|
|
1430
1534
|
};
|
|
1431
1535
|
var Quonfig = class {
|
|
1432
1536
|
sdkKey;
|
|
1433
|
-
|
|
1537
|
+
apiUrls;
|
|
1434
1538
|
telemetryUrl;
|
|
1435
1539
|
enableSSE;
|
|
1436
1540
|
enablePolling;
|
|
@@ -1439,6 +1543,7 @@ var Quonfig = class {
|
|
|
1439
1543
|
onNoDefault;
|
|
1440
1544
|
globalContext;
|
|
1441
1545
|
initTimeout;
|
|
1546
|
+
datadir;
|
|
1442
1547
|
datafile;
|
|
1443
1548
|
store;
|
|
1444
1549
|
evaluator;
|
|
@@ -1456,7 +1561,10 @@ var Quonfig = class {
|
|
|
1456
1561
|
exampleContexts;
|
|
1457
1562
|
constructor(options) {
|
|
1458
1563
|
this.sdkKey = options.sdkKey;
|
|
1459
|
-
this.
|
|
1564
|
+
this.apiUrls = options.apiUrls ?? (options.apiUrl ? [options.apiUrl] : DEFAULT_API_URLS);
|
|
1565
|
+
if (this.apiUrls.length === 0) {
|
|
1566
|
+
throw new Error("[quonfig] apiUrls must not be empty");
|
|
1567
|
+
}
|
|
1460
1568
|
this.telemetryUrl = options.telemetryUrl;
|
|
1461
1569
|
this.enableSSE = options.enableSSE ?? true;
|
|
1462
1570
|
this.enablePolling = options.enablePolling ?? false;
|
|
@@ -1465,12 +1573,13 @@ var Quonfig = class {
|
|
|
1465
1573
|
this.onNoDefault = options.onNoDefault ?? "error";
|
|
1466
1574
|
this.globalContext = options.globalContext;
|
|
1467
1575
|
this.initTimeout = options.initTimeout ?? DEFAULT_INIT_TIMEOUT;
|
|
1576
|
+
this.datadir = options.datadir;
|
|
1468
1577
|
this.datafile = options.datafile;
|
|
1469
1578
|
this.instanceHash = (0, import_crypto3.randomUUID)();
|
|
1470
1579
|
this.store = new ConfigStore();
|
|
1471
1580
|
this.evaluator = new Evaluator(this.store);
|
|
1472
1581
|
this.resolver = new Resolver(this.store, this.evaluator);
|
|
1473
|
-
this.transport = new Transport(this.
|
|
1582
|
+
this.transport = new Transport(this.apiUrls, this.sdkKey, this.telemetryUrl);
|
|
1474
1583
|
const contextUploadMode = options.contextUploadMode ?? "periodic_example";
|
|
1475
1584
|
this.evaluationSummaries = new EvaluationSummaryCollector(
|
|
1476
1585
|
options.collectEvaluationSummaries ?? true
|
|
@@ -1479,14 +1588,14 @@ var Quonfig = class {
|
|
|
1479
1588
|
this.exampleContexts = new ExampleContextCollector(contextUploadMode);
|
|
1480
1589
|
}
|
|
1481
1590
|
/**
|
|
1482
|
-
* Initialize the SDK. Downloads configs from the API (or loads from datafile)
|
|
1591
|
+
* Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
|
|
1483
1592
|
* and starts background update mechanisms (SSE/polling).
|
|
1484
1593
|
*
|
|
1485
1594
|
* Must be called before using any get* methods.
|
|
1486
1595
|
*/
|
|
1487
1596
|
async init() {
|
|
1488
|
-
if (this.datafile) {
|
|
1489
|
-
this.
|
|
1597
|
+
if (this.datadir || this.datafile) {
|
|
1598
|
+
this.loadLocalData();
|
|
1490
1599
|
this.initialized = true;
|
|
1491
1600
|
return;
|
|
1492
1601
|
}
|
|
@@ -1654,6 +1763,22 @@ var Quonfig = class {
|
|
|
1654
1763
|
inContext(contexts) {
|
|
1655
1764
|
return new BoundQuonfig(this, mergeContexts(this.globalContext, contexts));
|
|
1656
1765
|
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Flush pending telemetry data immediately. Useful in serverless environments
|
|
1768
|
+
* (Vercel, Lambda) where the process may be frozen before the background
|
|
1769
|
+
* timer fires.
|
|
1770
|
+
*
|
|
1771
|
+
* ```typescript
|
|
1772
|
+
* const value = quonfig.get("my-flag", contexts);
|
|
1773
|
+
* await quonfig.flush();
|
|
1774
|
+
* return NextResponse.json({ value });
|
|
1775
|
+
* ```
|
|
1776
|
+
*/
|
|
1777
|
+
async flush() {
|
|
1778
|
+
if (this.telemetryReporter) {
|
|
1779
|
+
await this.telemetryReporter.sync();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1657
1782
|
/**
|
|
1658
1783
|
* Close the SDK. Stops SSE, polling, and telemetry.
|
|
1659
1784
|
*/
|
|
@@ -1691,19 +1816,24 @@ var Quonfig = class {
|
|
|
1691
1816
|
return void 0;
|
|
1692
1817
|
}
|
|
1693
1818
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
if (typeof this.datafile === "string") {
|
|
1697
|
-
const raw = (0, import_fs.readFileSync)(this.datafile, "utf-8");
|
|
1698
|
-
data = JSON.parse(raw);
|
|
1699
|
-
} else if (typeof this.datafile === "object") {
|
|
1700
|
-
data = this.datafile;
|
|
1701
|
-
} else {
|
|
1702
|
-
throw new Error("Invalid datafile option");
|
|
1703
|
-
}
|
|
1819
|
+
loadLocalData() {
|
|
1820
|
+
const data = this.loadLocalEnvelope();
|
|
1704
1821
|
this.store.update(data);
|
|
1705
1822
|
this.environmentId = data.meta.environment;
|
|
1706
1823
|
}
|
|
1824
|
+
loadLocalEnvelope() {
|
|
1825
|
+
if (this.datadir) {
|
|
1826
|
+
return loadEnvelopeFromDatadir(this.datadir);
|
|
1827
|
+
}
|
|
1828
|
+
if (typeof this.datafile === "string") {
|
|
1829
|
+
const raw = (0, import_fs2.readFileSync)(this.datafile, "utf-8");
|
|
1830
|
+
return JSON.parse(raw);
|
|
1831
|
+
}
|
|
1832
|
+
if (typeof this.datafile === "object") {
|
|
1833
|
+
return this.datafile;
|
|
1834
|
+
}
|
|
1835
|
+
throw new Error("Invalid local configuration: expected datadir or datafile");
|
|
1836
|
+
}
|
|
1707
1837
|
async fetchAndInstall() {
|
|
1708
1838
|
const result = await this.transport.fetchConfigs();
|
|
1709
1839
|
if (result.notChanged) {
|
|
@@ -1797,11 +1927,28 @@ var Client = class {
|
|
|
1797
1927
|
}
|
|
1798
1928
|
async post(path, payload) {
|
|
1799
1929
|
const url = `${this.apiUrl}${path}`;
|
|
1930
|
+
const isORPC = path.startsWith("/api/v1/");
|
|
1800
1931
|
this.log("ApiClient", `POST ${url}`);
|
|
1801
|
-
|
|
1932
|
+
const body = isORPC ? JSON.stringify({ json: payload }) : JSON.stringify(payload);
|
|
1933
|
+
const raw = await fetch(url, {
|
|
1802
1934
|
method: "POST",
|
|
1803
1935
|
headers: this.headers(),
|
|
1804
|
-
body
|
|
1936
|
+
body
|
|
1937
|
+
});
|
|
1938
|
+
if (!isORPC) return raw;
|
|
1939
|
+
const text = await raw.text();
|
|
1940
|
+
let unwrapped = text;
|
|
1941
|
+
try {
|
|
1942
|
+
const parsed = JSON.parse(text);
|
|
1943
|
+
if (parsed && typeof parsed === "object" && "json" in parsed) {
|
|
1944
|
+
unwrapped = JSON.stringify(parsed.json);
|
|
1945
|
+
}
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
return new Response(unwrapped, {
|
|
1949
|
+
status: raw.status,
|
|
1950
|
+
statusText: raw.statusText,
|
|
1951
|
+
headers: raw.headers
|
|
1805
1952
|
});
|
|
1806
1953
|
}
|
|
1807
1954
|
async put(path, payload) {
|