@quonfig/node 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.cjs +169 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -6
- package/dist/index.d.ts +32 -6
- package/dist/index.js +169 -44
- 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.
|
|
@@ -1111,6 +1125,87 @@ function shouldLog(args) {
|
|
|
1111
1125
|
return defaultLevel <= desiredLevel;
|
|
1112
1126
|
}
|
|
1113
1127
|
|
|
1128
|
+
// src/datadir.ts
|
|
1129
|
+
var import_fs = require("fs");
|
|
1130
|
+
var import_path = require("path");
|
|
1131
|
+
var CONFIG_SUBDIRS = ["configs", "feature-flags", "segments", "schemas", "log-levels"];
|
|
1132
|
+
function loadEnvelopeFromDatadir(datadir) {
|
|
1133
|
+
const environmentId = loadEnvironmentId((0, import_path.join)(datadir, "environments.json"));
|
|
1134
|
+
const configs = [];
|
|
1135
|
+
for (const subdir of CONFIG_SUBDIRS) {
|
|
1136
|
+
const dir = (0, import_path.join)(datadir, subdir);
|
|
1137
|
+
if (!(0, import_fs.existsSync)(dir)) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const filenames = (0, import_fs.readdirSync)(dir).filter((filename) => filename.endsWith(".json")).sort((a, b) => a.localeCompare(b));
|
|
1141
|
+
for (const filename of filenames) {
|
|
1142
|
+
const raw = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)(dir, filename), "utf-8"));
|
|
1143
|
+
configs.push(toConfigResponse(raw, environmentId));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
configs,
|
|
1148
|
+
meta: {
|
|
1149
|
+
version: `datadir:${datadir}`,
|
|
1150
|
+
environment: environmentId
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function loadEnvironmentId(environmentsPath) {
|
|
1155
|
+
if (!(0, import_fs.existsSync)(environmentsPath)) {
|
|
1156
|
+
throw new Error(`[quonfig] Datadir is missing environments.json: ${environmentsPath}`);
|
|
1157
|
+
}
|
|
1158
|
+
const environments = JSON.parse((0, import_fs.readFileSync)(environmentsPath, "utf-8"));
|
|
1159
|
+
const candidates = normalizeEnvironmentCandidates(
|
|
1160
|
+
isWrappedEnvironmentList(environments) ? environments.environments : environments
|
|
1161
|
+
);
|
|
1162
|
+
if (candidates.length === 0) {
|
|
1163
|
+
return "";
|
|
1164
|
+
}
|
|
1165
|
+
return candidates[0];
|
|
1166
|
+
}
|
|
1167
|
+
function isWrappedEnvironmentList(value) {
|
|
1168
|
+
return Boolean(
|
|
1169
|
+
value && typeof value === "object" && !Array.isArray(value) && "environments" in value
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
function normalizeEnvironmentCandidates(environments) {
|
|
1173
|
+
if (!environments) {
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
if (Array.isArray(environments)) {
|
|
1177
|
+
return environments.map((entry) => {
|
|
1178
|
+
if (typeof entry === "string") {
|
|
1179
|
+
return entry;
|
|
1180
|
+
}
|
|
1181
|
+
if (entry && typeof entry === "object") {
|
|
1182
|
+
return entry.id ?? entry.name;
|
|
1183
|
+
}
|
|
1184
|
+
return void 0;
|
|
1185
|
+
}).filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
1186
|
+
}
|
|
1187
|
+
if (environments && typeof environments === "object") {
|
|
1188
|
+
const values = Object.values(environments).map((value) => typeof value === "string" && value.length > 0 ? value : void 0).filter((value) => typeof value === "string");
|
|
1189
|
+
if (values.length > 0) {
|
|
1190
|
+
return values;
|
|
1191
|
+
}
|
|
1192
|
+
return Object.keys(environments).filter((key) => key.length > 0);
|
|
1193
|
+
}
|
|
1194
|
+
return [];
|
|
1195
|
+
}
|
|
1196
|
+
function toConfigResponse(raw, environmentId) {
|
|
1197
|
+
const environment = raw.environments?.find((candidate) => candidate.id === environmentId);
|
|
1198
|
+
return {
|
|
1199
|
+
id: raw.id ?? "",
|
|
1200
|
+
key: raw.key,
|
|
1201
|
+
type: raw.type,
|
|
1202
|
+
valueType: raw.valueType,
|
|
1203
|
+
sendToClientSdk: raw.sendToClientSdk ?? false,
|
|
1204
|
+
default: raw.default ?? { rules: [] },
|
|
1205
|
+
environment
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1114
1209
|
// src/telemetry/evaluationSummaries.ts
|
|
1115
1210
|
var EvaluationSummaryCollector = class {
|
|
1116
1211
|
enabled;
|
|
@@ -1380,7 +1475,10 @@ var TelemetryReporter = class {
|
|
|
1380
1475
|
};
|
|
1381
1476
|
|
|
1382
1477
|
// src/quonfig.ts
|
|
1383
|
-
var
|
|
1478
|
+
var DEFAULT_API_URLS = [
|
|
1479
|
+
"https://primary.quonfig.com",
|
|
1480
|
+
"https://secondary.quonfig.com"
|
|
1481
|
+
];
|
|
1384
1482
|
var DEFAULT_POLL_INTERVAL = 6e4;
|
|
1385
1483
|
var DEFAULT_INIT_TIMEOUT = 1e4;
|
|
1386
1484
|
var DEFAULT_LOG_LEVEL = 5;
|
|
@@ -1430,7 +1528,7 @@ var BoundQuonfig = class _BoundQuonfig {
|
|
|
1430
1528
|
};
|
|
1431
1529
|
var Quonfig = class {
|
|
1432
1530
|
sdkKey;
|
|
1433
|
-
|
|
1531
|
+
apiUrls;
|
|
1434
1532
|
telemetryUrl;
|
|
1435
1533
|
enableSSE;
|
|
1436
1534
|
enablePolling;
|
|
@@ -1439,6 +1537,7 @@ var Quonfig = class {
|
|
|
1439
1537
|
onNoDefault;
|
|
1440
1538
|
globalContext;
|
|
1441
1539
|
initTimeout;
|
|
1540
|
+
datadir;
|
|
1442
1541
|
datafile;
|
|
1443
1542
|
store;
|
|
1444
1543
|
evaluator;
|
|
@@ -1456,7 +1555,10 @@ var Quonfig = class {
|
|
|
1456
1555
|
exampleContexts;
|
|
1457
1556
|
constructor(options) {
|
|
1458
1557
|
this.sdkKey = options.sdkKey;
|
|
1459
|
-
this.
|
|
1558
|
+
this.apiUrls = options.apiUrls ?? (options.apiUrl ? [options.apiUrl] : DEFAULT_API_URLS);
|
|
1559
|
+
if (this.apiUrls.length === 0) {
|
|
1560
|
+
throw new Error("[quonfig] apiUrls must not be empty");
|
|
1561
|
+
}
|
|
1460
1562
|
this.telemetryUrl = options.telemetryUrl;
|
|
1461
1563
|
this.enableSSE = options.enableSSE ?? true;
|
|
1462
1564
|
this.enablePolling = options.enablePolling ?? false;
|
|
@@ -1465,12 +1567,13 @@ var Quonfig = class {
|
|
|
1465
1567
|
this.onNoDefault = options.onNoDefault ?? "error";
|
|
1466
1568
|
this.globalContext = options.globalContext;
|
|
1467
1569
|
this.initTimeout = options.initTimeout ?? DEFAULT_INIT_TIMEOUT;
|
|
1570
|
+
this.datadir = options.datadir;
|
|
1468
1571
|
this.datafile = options.datafile;
|
|
1469
1572
|
this.instanceHash = (0, import_crypto3.randomUUID)();
|
|
1470
1573
|
this.store = new ConfigStore();
|
|
1471
1574
|
this.evaluator = new Evaluator(this.store);
|
|
1472
1575
|
this.resolver = new Resolver(this.store, this.evaluator);
|
|
1473
|
-
this.transport = new Transport(this.
|
|
1576
|
+
this.transport = new Transport(this.apiUrls, this.sdkKey, this.telemetryUrl);
|
|
1474
1577
|
const contextUploadMode = options.contextUploadMode ?? "periodic_example";
|
|
1475
1578
|
this.evaluationSummaries = new EvaluationSummaryCollector(
|
|
1476
1579
|
options.collectEvaluationSummaries ?? true
|
|
@@ -1479,14 +1582,14 @@ var Quonfig = class {
|
|
|
1479
1582
|
this.exampleContexts = new ExampleContextCollector(contextUploadMode);
|
|
1480
1583
|
}
|
|
1481
1584
|
/**
|
|
1482
|
-
* Initialize the SDK. Downloads configs from the API (or loads from datafile)
|
|
1585
|
+
* Initialize the SDK. Downloads configs from the API (or loads from datadir/datafile)
|
|
1483
1586
|
* and starts background update mechanisms (SSE/polling).
|
|
1484
1587
|
*
|
|
1485
1588
|
* Must be called before using any get* methods.
|
|
1486
1589
|
*/
|
|
1487
1590
|
async init() {
|
|
1488
|
-
if (this.datafile) {
|
|
1489
|
-
this.
|
|
1591
|
+
if (this.datadir || this.datafile) {
|
|
1592
|
+
this.loadLocalData();
|
|
1490
1593
|
this.initialized = true;
|
|
1491
1594
|
return;
|
|
1492
1595
|
}
|
|
@@ -1691,19 +1794,24 @@ var Quonfig = class {
|
|
|
1691
1794
|
return void 0;
|
|
1692
1795
|
}
|
|
1693
1796
|
}
|
|
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
|
-
}
|
|
1797
|
+
loadLocalData() {
|
|
1798
|
+
const data = this.loadLocalEnvelope();
|
|
1704
1799
|
this.store.update(data);
|
|
1705
1800
|
this.environmentId = data.meta.environment;
|
|
1706
1801
|
}
|
|
1802
|
+
loadLocalEnvelope() {
|
|
1803
|
+
if (this.datadir) {
|
|
1804
|
+
return loadEnvelopeFromDatadir(this.datadir);
|
|
1805
|
+
}
|
|
1806
|
+
if (typeof this.datafile === "string") {
|
|
1807
|
+
const raw = (0, import_fs2.readFileSync)(this.datafile, "utf-8");
|
|
1808
|
+
return JSON.parse(raw);
|
|
1809
|
+
}
|
|
1810
|
+
if (typeof this.datafile === "object") {
|
|
1811
|
+
return this.datafile;
|
|
1812
|
+
}
|
|
1813
|
+
throw new Error("Invalid local configuration: expected datadir or datafile");
|
|
1814
|
+
}
|
|
1707
1815
|
async fetchAndInstall() {
|
|
1708
1816
|
const result = await this.transport.fetchConfigs();
|
|
1709
1817
|
if (result.notChanged) {
|
|
@@ -1797,11 +1905,28 @@ var Client = class {
|
|
|
1797
1905
|
}
|
|
1798
1906
|
async post(path, payload) {
|
|
1799
1907
|
const url = `${this.apiUrl}${path}`;
|
|
1908
|
+
const isORPC = path.startsWith("/api/v1/");
|
|
1800
1909
|
this.log("ApiClient", `POST ${url}`);
|
|
1801
|
-
|
|
1910
|
+
const body = isORPC ? JSON.stringify({ json: payload }) : JSON.stringify(payload);
|
|
1911
|
+
const raw = await fetch(url, {
|
|
1802
1912
|
method: "POST",
|
|
1803
1913
|
headers: this.headers(),
|
|
1804
|
-
body
|
|
1914
|
+
body
|
|
1915
|
+
});
|
|
1916
|
+
if (!isORPC) return raw;
|
|
1917
|
+
const text = await raw.text();
|
|
1918
|
+
let unwrapped = text;
|
|
1919
|
+
try {
|
|
1920
|
+
const parsed = JSON.parse(text);
|
|
1921
|
+
if (parsed && typeof parsed === "object" && "json" in parsed) {
|
|
1922
|
+
unwrapped = JSON.stringify(parsed.json);
|
|
1923
|
+
}
|
|
1924
|
+
} catch {
|
|
1925
|
+
}
|
|
1926
|
+
return new Response(unwrapped, {
|
|
1927
|
+
status: raw.status,
|
|
1928
|
+
statusText: raw.statusText,
|
|
1929
|
+
headers: raw.headers
|
|
1805
1930
|
});
|
|
1806
1931
|
}
|
|
1807
1932
|
async put(path, payload) {
|