@smplkit/sdk 1.0.0 → 1.1.0
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 +3 -4
- package/dist/chunk-PZD5PSQY.js +317 -0
- package/dist/chunk-PZD5PSQY.js.map +1 -0
- package/dist/index.cjs +874 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +476 -0
- package/dist/index.d.ts +299 -88
- package/dist/index.js +372 -236
- package/dist/index.js.map +1 -1
- package/dist/runtime-CCRTBKED.js +7 -0
- package/dist/runtime-CCRTBKED.js.map +1 -0
- package/package.json +7 -1
- package/dist/index.d.mts +0 -265
- package/dist/index.mjs +0 -329
- package/dist/index.mjs.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/config/resolve.ts
|
|
34
|
+
function deepMerge(base, override) {
|
|
35
|
+
const result = { ...base };
|
|
36
|
+
for (const [key, value] of Object.entries(override)) {
|
|
37
|
+
if (key in result && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key]) && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
38
|
+
result[key] = deepMerge(
|
|
39
|
+
result[key],
|
|
40
|
+
value
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
result[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function resolveChain(chain, environment) {
|
|
49
|
+
let accumulated = {};
|
|
50
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
51
|
+
const config = chain[i];
|
|
52
|
+
const baseValues = config.values ?? {};
|
|
53
|
+
const envEntry = (config.environments ?? {})[environment];
|
|
54
|
+
const envValues = envEntry !== null && envEntry !== void 0 && typeof envEntry === "object" && !Array.isArray(envEntry) ? envEntry.values ?? {} : {};
|
|
55
|
+
const configResolved = deepMerge(baseValues, envValues);
|
|
56
|
+
accumulated = deepMerge(accumulated, configResolved);
|
|
57
|
+
}
|
|
58
|
+
return accumulated;
|
|
59
|
+
}
|
|
60
|
+
var init_resolve = __esm({
|
|
61
|
+
"src/config/resolve.ts"() {
|
|
62
|
+
"use strict";
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/config/runtime.ts
|
|
67
|
+
var runtime_exports = {};
|
|
68
|
+
__export(runtime_exports, {
|
|
69
|
+
ConfigRuntime: () => ConfigRuntime
|
|
70
|
+
});
|
|
71
|
+
var import_ws, BACKOFF_MS, ConfigRuntime;
|
|
72
|
+
var init_runtime = __esm({
|
|
73
|
+
"src/config/runtime.ts"() {
|
|
74
|
+
"use strict";
|
|
75
|
+
import_ws = __toESM(require("ws"), 1);
|
|
76
|
+
init_resolve();
|
|
77
|
+
BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
|
|
78
|
+
ConfigRuntime = class {
|
|
79
|
+
_cache;
|
|
80
|
+
_chain;
|
|
81
|
+
_fetchCount;
|
|
82
|
+
_lastFetchAt;
|
|
83
|
+
_closed = false;
|
|
84
|
+
_wsStatus = "disconnected";
|
|
85
|
+
_ws = null;
|
|
86
|
+
_reconnectTimer = null;
|
|
87
|
+
_backoffIndex = 0;
|
|
88
|
+
_listeners = [];
|
|
89
|
+
_configId;
|
|
90
|
+
_environment;
|
|
91
|
+
_apiKey;
|
|
92
|
+
_baseUrl;
|
|
93
|
+
_fetchChain;
|
|
94
|
+
/** @internal */
|
|
95
|
+
constructor(options) {
|
|
96
|
+
this._configId = options.configId;
|
|
97
|
+
this._environment = options.environment;
|
|
98
|
+
this._apiKey = options.apiKey;
|
|
99
|
+
this._baseUrl = options.baseUrl;
|
|
100
|
+
this._fetchChain = options.fetchChain;
|
|
101
|
+
this._chain = options.chain;
|
|
102
|
+
this._cache = resolveChain(options.chain, options.environment);
|
|
103
|
+
this._fetchCount = options.chain.length;
|
|
104
|
+
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
105
|
+
this._connectWebSocket();
|
|
106
|
+
}
|
|
107
|
+
// ---- Value access (synchronous, local cache) ----
|
|
108
|
+
/**
|
|
109
|
+
* Return the resolved value for `key`, or `defaultValue` if absent.
|
|
110
|
+
*
|
|
111
|
+
* @param key - The config key to look up.
|
|
112
|
+
* @param defaultValue - Returned when the key is not present (default: null).
|
|
113
|
+
*/
|
|
114
|
+
get(key, defaultValue = null) {
|
|
115
|
+
return key in this._cache ? this._cache[key] : defaultValue;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Return the value as a string, or `defaultValue` if absent or not a string.
|
|
119
|
+
*/
|
|
120
|
+
getString(key, defaultValue = null) {
|
|
121
|
+
const value = this._cache[key];
|
|
122
|
+
return typeof value === "string" ? value : defaultValue;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Return the value as a number, or `defaultValue` if absent or not a number.
|
|
126
|
+
*/
|
|
127
|
+
getNumber(key, defaultValue = null) {
|
|
128
|
+
const value = this._cache[key];
|
|
129
|
+
return typeof value === "number" ? value : defaultValue;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Return the value as a boolean, or `defaultValue` if absent or not a boolean.
|
|
133
|
+
*/
|
|
134
|
+
getBool(key, defaultValue = null) {
|
|
135
|
+
const value = this._cache[key];
|
|
136
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Return whether `key` is present in the resolved configuration.
|
|
140
|
+
*/
|
|
141
|
+
exists(key) {
|
|
142
|
+
return key in this._cache;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Return a shallow copy of the full resolved configuration.
|
|
146
|
+
*/
|
|
147
|
+
getAll() {
|
|
148
|
+
return { ...this._cache };
|
|
149
|
+
}
|
|
150
|
+
// ---- Change listeners ----
|
|
151
|
+
/**
|
|
152
|
+
* Register a listener that fires when a config value changes.
|
|
153
|
+
*
|
|
154
|
+
* @param callback - Called with a {@link ConfigChangeEvent} on each change.
|
|
155
|
+
* @param options.key - If provided, the listener fires only for this key.
|
|
156
|
+
* If omitted, the listener fires for all changes.
|
|
157
|
+
*/
|
|
158
|
+
onChange(callback, options) {
|
|
159
|
+
this._listeners.push({
|
|
160
|
+
callback,
|
|
161
|
+
key: options?.key ?? null
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ---- Diagnostics ----
|
|
165
|
+
/**
|
|
166
|
+
* Return diagnostic statistics for this runtime.
|
|
167
|
+
*/
|
|
168
|
+
stats() {
|
|
169
|
+
return {
|
|
170
|
+
fetchCount: this._fetchCount,
|
|
171
|
+
lastFetchAt: this._lastFetchAt
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Return the current WebSocket connection status.
|
|
176
|
+
*/
|
|
177
|
+
connectionStatus() {
|
|
178
|
+
return this._wsStatus;
|
|
179
|
+
}
|
|
180
|
+
// ---- Lifecycle ----
|
|
181
|
+
/**
|
|
182
|
+
* Force a manual refresh of the cached configuration.
|
|
183
|
+
*
|
|
184
|
+
* Re-fetches the full config chain via HTTP, re-resolves values, updates
|
|
185
|
+
* the local cache, and fires listeners for any detected changes.
|
|
186
|
+
*
|
|
187
|
+
* @throws {Error} If no `fetchChain` function was provided on construction.
|
|
188
|
+
*/
|
|
189
|
+
async refresh() {
|
|
190
|
+
if (!this._fetchChain) {
|
|
191
|
+
throw new Error("No fetchChain function provided; cannot refresh.");
|
|
192
|
+
}
|
|
193
|
+
const newChain = await this._fetchChain();
|
|
194
|
+
const oldCache = this._cache;
|
|
195
|
+
this._chain = newChain;
|
|
196
|
+
this._cache = resolveChain(newChain, this._environment);
|
|
197
|
+
this._fetchCount += newChain.length;
|
|
198
|
+
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
this._diffAndFire(oldCache, this._cache, "manual");
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Close the runtime connection.
|
|
203
|
+
*
|
|
204
|
+
* Shuts down the WebSocket and cancels any pending reconnect timer.
|
|
205
|
+
* Safe to call multiple times.
|
|
206
|
+
*/
|
|
207
|
+
async close() {
|
|
208
|
+
this._closed = true;
|
|
209
|
+
this._wsStatus = "disconnected";
|
|
210
|
+
if (this._reconnectTimer !== null) {
|
|
211
|
+
clearTimeout(this._reconnectTimer);
|
|
212
|
+
this._reconnectTimer = null;
|
|
213
|
+
}
|
|
214
|
+
if (this._ws !== null) {
|
|
215
|
+
this._ws.close();
|
|
216
|
+
this._ws = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Async dispose support for `await using` (TypeScript 5.2+).
|
|
221
|
+
*/
|
|
222
|
+
async [Symbol.asyncDispose]() {
|
|
223
|
+
await this.close();
|
|
224
|
+
}
|
|
225
|
+
// ---- WebSocket internals ----
|
|
226
|
+
_buildWsUrl() {
|
|
227
|
+
let url = this._baseUrl;
|
|
228
|
+
if (url.startsWith("https://")) {
|
|
229
|
+
url = "wss://" + url.slice("https://".length);
|
|
230
|
+
} else if (url.startsWith("http://")) {
|
|
231
|
+
url = "ws://" + url.slice("http://".length);
|
|
232
|
+
} else {
|
|
233
|
+
url = "wss://" + url;
|
|
234
|
+
}
|
|
235
|
+
url = url.replace(/\/$/, "");
|
|
236
|
+
return `${url}/api/ws/v1/configs?api_key=${this._apiKey}`;
|
|
237
|
+
}
|
|
238
|
+
_connectWebSocket() {
|
|
239
|
+
if (this._closed) return;
|
|
240
|
+
this._wsStatus = "connecting";
|
|
241
|
+
const wsUrl = this._buildWsUrl();
|
|
242
|
+
try {
|
|
243
|
+
const ws = new import_ws.default(wsUrl);
|
|
244
|
+
this._ws = ws;
|
|
245
|
+
ws.on("open", () => {
|
|
246
|
+
if (this._closed) {
|
|
247
|
+
ws.close();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this._backoffIndex = 0;
|
|
251
|
+
this._wsStatus = "connected";
|
|
252
|
+
ws.send(
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
type: "subscribe",
|
|
255
|
+
config_id: this._configId,
|
|
256
|
+
environment: this._environment
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
ws.on("message", (data) => {
|
|
261
|
+
try {
|
|
262
|
+
const msg = JSON.parse(String(data));
|
|
263
|
+
this._handleMessage(msg);
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
ws.on("close", () => {
|
|
268
|
+
if (!this._closed) {
|
|
269
|
+
this._wsStatus = "disconnected";
|
|
270
|
+
this._scheduleReconnect();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
ws.on("error", () => {
|
|
274
|
+
});
|
|
275
|
+
} catch {
|
|
276
|
+
if (!this._closed) {
|
|
277
|
+
this._scheduleReconnect();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
_scheduleReconnect() {
|
|
282
|
+
if (this._closed) return;
|
|
283
|
+
const delay = BACKOFF_MS[Math.min(this._backoffIndex, BACKOFF_MS.length - 1)];
|
|
284
|
+
this._backoffIndex++;
|
|
285
|
+
this._wsStatus = "connecting";
|
|
286
|
+
this._reconnectTimer = setTimeout(() => {
|
|
287
|
+
this._reconnectTimer = null;
|
|
288
|
+
if (this._fetchChain) {
|
|
289
|
+
this._fetchChain().then((newChain) => {
|
|
290
|
+
const oldCache = this._cache;
|
|
291
|
+
this._chain = newChain;
|
|
292
|
+
this._cache = resolveChain(newChain, this._environment);
|
|
293
|
+
this._fetchCount += newChain.length;
|
|
294
|
+
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
295
|
+
this._diffAndFire(oldCache, this._cache, "manual");
|
|
296
|
+
}).catch(() => {
|
|
297
|
+
}).finally(() => {
|
|
298
|
+
this._connectWebSocket();
|
|
299
|
+
});
|
|
300
|
+
} else {
|
|
301
|
+
this._connectWebSocket();
|
|
302
|
+
}
|
|
303
|
+
}, delay);
|
|
304
|
+
}
|
|
305
|
+
_handleMessage(msg) {
|
|
306
|
+
if (msg.type === "config_changed") {
|
|
307
|
+
this._applyChanges(msg.config_id, msg.changes);
|
|
308
|
+
} else if (msg.type === "config_deleted") {
|
|
309
|
+
this._closed = true;
|
|
310
|
+
void this.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
_applyChanges(configId, changes) {
|
|
314
|
+
const chainEntry = this._chain.find((c) => c.id === configId);
|
|
315
|
+
if (!chainEntry) return;
|
|
316
|
+
for (const change of changes) {
|
|
317
|
+
const { key, new_value } = change;
|
|
318
|
+
const envEntry = chainEntry.environments[this._environment] !== void 0 && chainEntry.environments[this._environment] !== null ? chainEntry.environments[this._environment] : null;
|
|
319
|
+
const envValues = envEntry !== null && typeof envEntry === "object" ? envEntry.values ?? {} : null;
|
|
320
|
+
if (new_value === null || new_value === void 0) {
|
|
321
|
+
delete chainEntry.values[key];
|
|
322
|
+
if (envValues) delete envValues[key];
|
|
323
|
+
} else if (envValues && key in envValues) {
|
|
324
|
+
envValues[key] = new_value;
|
|
325
|
+
} else if (key in chainEntry.values) {
|
|
326
|
+
chainEntry.values[key] = new_value;
|
|
327
|
+
} else {
|
|
328
|
+
chainEntry.values[key] = new_value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const oldCache = this._cache;
|
|
332
|
+
this._cache = resolveChain(this._chain, this._environment);
|
|
333
|
+
this._diffAndFire(oldCache, this._cache, "websocket");
|
|
334
|
+
}
|
|
335
|
+
_diffAndFire(oldCache, newCache, source) {
|
|
336
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
|
|
337
|
+
for (const key of allKeys) {
|
|
338
|
+
const oldVal = key in oldCache ? oldCache[key] : null;
|
|
339
|
+
const newVal = key in newCache ? newCache[key] : null;
|
|
340
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
341
|
+
const event = { key, oldValue: oldVal, newValue: newVal, source };
|
|
342
|
+
this._fireListeners(event);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
_fireListeners(event) {
|
|
347
|
+
for (const listener of this._listeners) {
|
|
348
|
+
if (listener.key === null || listener.key === event.key) {
|
|
349
|
+
try {
|
|
350
|
+
listener.callback(event);
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// src/index.ts
|
|
361
|
+
var index_exports = {};
|
|
362
|
+
__export(index_exports, {
|
|
363
|
+
Config: () => Config,
|
|
364
|
+
ConfigClient: () => ConfigClient,
|
|
365
|
+
ConfigRuntime: () => ConfigRuntime,
|
|
366
|
+
SmplConflictError: () => SmplConflictError,
|
|
367
|
+
SmplConnectionError: () => SmplConnectionError,
|
|
368
|
+
SmplError: () => SmplError,
|
|
369
|
+
SmplNotFoundError: () => SmplNotFoundError,
|
|
370
|
+
SmplTimeoutError: () => SmplTimeoutError,
|
|
371
|
+
SmplValidationError: () => SmplValidationError,
|
|
372
|
+
SmplkitClient: () => SmplkitClient
|
|
373
|
+
});
|
|
374
|
+
module.exports = __toCommonJS(index_exports);
|
|
375
|
+
|
|
376
|
+
// src/config/client.ts
|
|
377
|
+
var import_openapi_fetch = __toESM(require("openapi-fetch"), 1);
|
|
378
|
+
|
|
379
|
+
// src/errors.ts
|
|
380
|
+
var SmplError = class extends Error {
|
|
381
|
+
/** The HTTP status code, if the error originated from an HTTP response. */
|
|
382
|
+
statusCode;
|
|
383
|
+
/** The raw response body, if available. */
|
|
384
|
+
responseBody;
|
|
385
|
+
constructor(message, statusCode, responseBody) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.name = "SmplError";
|
|
388
|
+
this.statusCode = statusCode;
|
|
389
|
+
this.responseBody = responseBody;
|
|
390
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
var SmplConnectionError = class extends SmplError {
|
|
394
|
+
constructor(message, statusCode, responseBody) {
|
|
395
|
+
super(message, statusCode, responseBody);
|
|
396
|
+
this.name = "SmplConnectionError";
|
|
397
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var SmplTimeoutError = class extends SmplError {
|
|
401
|
+
constructor(message, statusCode, responseBody) {
|
|
402
|
+
super(message, statusCode, responseBody);
|
|
403
|
+
this.name = "SmplTimeoutError";
|
|
404
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var SmplNotFoundError = class extends SmplError {
|
|
408
|
+
constructor(message, statusCode, responseBody) {
|
|
409
|
+
super(message, statusCode ?? 404, responseBody);
|
|
410
|
+
this.name = "SmplNotFoundError";
|
|
411
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var SmplConflictError = class extends SmplError {
|
|
415
|
+
constructor(message, statusCode, responseBody) {
|
|
416
|
+
super(message, statusCode ?? 409, responseBody);
|
|
417
|
+
this.name = "SmplConflictError";
|
|
418
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
var SmplValidationError = class extends SmplError {
|
|
422
|
+
constructor(message, statusCode, responseBody) {
|
|
423
|
+
super(message, statusCode ?? 422, responseBody);
|
|
424
|
+
this.name = "SmplValidationError";
|
|
425
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// src/config/types.ts
|
|
430
|
+
var Config = class {
|
|
431
|
+
/** UUID of the config. */
|
|
432
|
+
id;
|
|
433
|
+
/** Human-readable key (e.g. `"user_service"`). */
|
|
434
|
+
key;
|
|
435
|
+
/** Display name. */
|
|
436
|
+
name;
|
|
437
|
+
/** Optional description. */
|
|
438
|
+
description;
|
|
439
|
+
/** Parent config UUID, or null if this is a root config. */
|
|
440
|
+
parent;
|
|
441
|
+
/** Base key-value pairs. */
|
|
442
|
+
values;
|
|
443
|
+
/**
|
|
444
|
+
* Per-environment overrides.
|
|
445
|
+
* Stored as `{ env_name: { values: { key: value } } }` to match the
|
|
446
|
+
* server's format.
|
|
447
|
+
*/
|
|
448
|
+
environments;
|
|
449
|
+
/** When the config was created, or null if unavailable. */
|
|
450
|
+
createdAt;
|
|
451
|
+
/** When the config was last updated, or null if unavailable. */
|
|
452
|
+
updatedAt;
|
|
453
|
+
/**
|
|
454
|
+
* Internal reference to the parent client.
|
|
455
|
+
* @internal
|
|
456
|
+
*/
|
|
457
|
+
_client;
|
|
458
|
+
/** @internal */
|
|
459
|
+
constructor(client, fields) {
|
|
460
|
+
this._client = client;
|
|
461
|
+
this.id = fields.id;
|
|
462
|
+
this.key = fields.key;
|
|
463
|
+
this.name = fields.name;
|
|
464
|
+
this.description = fields.description;
|
|
465
|
+
this.parent = fields.parent;
|
|
466
|
+
this.values = fields.values;
|
|
467
|
+
this.environments = fields.environments;
|
|
468
|
+
this.createdAt = fields.createdAt;
|
|
469
|
+
this.updatedAt = fields.updatedAt;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Update this config's attributes on the server.
|
|
473
|
+
*
|
|
474
|
+
* Builds the request from current attribute values, overriding with any
|
|
475
|
+
* provided options. Updates local attributes in place on success.
|
|
476
|
+
*
|
|
477
|
+
* @param options.name - New display name.
|
|
478
|
+
* @param options.description - New description (pass empty string to clear).
|
|
479
|
+
* @param options.values - New base values (replaces entirely).
|
|
480
|
+
* @param options.environments - New environments dict (replaces entirely).
|
|
481
|
+
*/
|
|
482
|
+
async update(options) {
|
|
483
|
+
const updated = await this._client._updateConfig({
|
|
484
|
+
configId: this.id,
|
|
485
|
+
name: options.name ?? this.name,
|
|
486
|
+
key: this.key,
|
|
487
|
+
description: options.description !== void 0 ? options.description : this.description,
|
|
488
|
+
parent: this.parent,
|
|
489
|
+
values: options.values ?? this.values,
|
|
490
|
+
environments: options.environments ?? this.environments
|
|
491
|
+
});
|
|
492
|
+
this.name = updated.name;
|
|
493
|
+
this.description = updated.description;
|
|
494
|
+
this.values = updated.values;
|
|
495
|
+
this.environments = updated.environments;
|
|
496
|
+
this.updatedAt = updated.updatedAt;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Replace base or environment-specific values.
|
|
500
|
+
*
|
|
501
|
+
* When `environment` is provided, replaces that environment's `values`
|
|
502
|
+
* sub-dict (other environments are preserved). When omitted, replaces
|
|
503
|
+
* the base `values`.
|
|
504
|
+
*
|
|
505
|
+
* @param values - The complete set of values to set.
|
|
506
|
+
* @param environment - Target environment, or omit for base values.
|
|
507
|
+
*/
|
|
508
|
+
async setValues(values, environment) {
|
|
509
|
+
let newValues;
|
|
510
|
+
let newEnvs;
|
|
511
|
+
if (environment === void 0) {
|
|
512
|
+
newValues = values;
|
|
513
|
+
newEnvs = this.environments;
|
|
514
|
+
} else {
|
|
515
|
+
newValues = this.values;
|
|
516
|
+
const existingEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? { ...this.environments[environment] } : {};
|
|
517
|
+
existingEntry.values = values;
|
|
518
|
+
newEnvs = { ...this.environments, [environment]: existingEntry };
|
|
519
|
+
}
|
|
520
|
+
const updated = await this._client._updateConfig({
|
|
521
|
+
configId: this.id,
|
|
522
|
+
name: this.name,
|
|
523
|
+
key: this.key,
|
|
524
|
+
description: this.description,
|
|
525
|
+
parent: this.parent,
|
|
526
|
+
values: newValues,
|
|
527
|
+
environments: newEnvs
|
|
528
|
+
});
|
|
529
|
+
this.values = updated.values;
|
|
530
|
+
this.environments = updated.environments;
|
|
531
|
+
this.updatedAt = updated.updatedAt;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Set a single key within base or environment-specific values.
|
|
535
|
+
*
|
|
536
|
+
* Merges the key into existing values rather than replacing all values.
|
|
537
|
+
*
|
|
538
|
+
* @param key - The config key to set.
|
|
539
|
+
* @param value - The value to assign.
|
|
540
|
+
* @param environment - Target environment, or omit for base values.
|
|
541
|
+
*/
|
|
542
|
+
async setValue(key, value, environment) {
|
|
543
|
+
if (environment === void 0) {
|
|
544
|
+
const merged = { ...this.values, [key]: value };
|
|
545
|
+
await this.setValues(merged);
|
|
546
|
+
} else {
|
|
547
|
+
const envEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? this.environments[environment] : {};
|
|
548
|
+
const existing = {
|
|
549
|
+
...typeof envEntry.values === "object" && envEntry.values !== null ? envEntry.values : {}
|
|
550
|
+
};
|
|
551
|
+
existing[key] = value;
|
|
552
|
+
await this.setValues(existing, environment);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Connect to this config for runtime value resolution.
|
|
557
|
+
*
|
|
558
|
+
* Eagerly fetches this config and its full parent chain, resolves values
|
|
559
|
+
* for the given environment via deep merge, and returns a
|
|
560
|
+
* {@link ConfigRuntime} with a fully populated local cache.
|
|
561
|
+
*
|
|
562
|
+
* A background WebSocket connection is started for real-time updates.
|
|
563
|
+
* If the WebSocket fails to connect, the runtime operates in cache-only
|
|
564
|
+
* mode and reconnects automatically.
|
|
565
|
+
*
|
|
566
|
+
* Supports both `await` and `await using` (TypeScript 5.2+)::
|
|
567
|
+
*
|
|
568
|
+
* ```typescript
|
|
569
|
+
* // Simple await
|
|
570
|
+
* const runtime = await config.connect("production");
|
|
571
|
+
* try { ... } finally { await runtime.close(); }
|
|
572
|
+
*
|
|
573
|
+
* // await using (auto-close)
|
|
574
|
+
* await using runtime = await config.connect("production");
|
|
575
|
+
* ```
|
|
576
|
+
*
|
|
577
|
+
* @param environment - The environment to resolve for (e.g. `"production"`).
|
|
578
|
+
* @param options.timeout - Milliseconds to wait for the initial fetch.
|
|
579
|
+
*/
|
|
580
|
+
async connect(environment, options) {
|
|
581
|
+
const { ConfigRuntime: ConfigRuntime2 } = await Promise.resolve().then(() => (init_runtime(), runtime_exports));
|
|
582
|
+
const timeout = options?.timeout ?? 3e4;
|
|
583
|
+
const chain = await this._buildChain(timeout);
|
|
584
|
+
return new ConfigRuntime2({
|
|
585
|
+
configKey: this.key,
|
|
586
|
+
configId: this.id,
|
|
587
|
+
environment,
|
|
588
|
+
chain,
|
|
589
|
+
apiKey: this._client._apiKey,
|
|
590
|
+
baseUrl: this._client._baseUrl,
|
|
591
|
+
fetchChain: () => this._buildChain(timeout)
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Walk the parent chain and return config data objects, child-to-root.
|
|
596
|
+
* @internal
|
|
597
|
+
*/
|
|
598
|
+
async _buildChain(_timeout) {
|
|
599
|
+
const chain = [{ id: this.id, values: this.values, environments: this.environments }];
|
|
600
|
+
let parentId = this.parent;
|
|
601
|
+
while (parentId !== null) {
|
|
602
|
+
const parentConfig = await this._client.get({ id: parentId });
|
|
603
|
+
chain.push({
|
|
604
|
+
id: parentConfig.id,
|
|
605
|
+
values: parentConfig.values,
|
|
606
|
+
environments: parentConfig.environments
|
|
607
|
+
});
|
|
608
|
+
parentId = parentConfig.parent;
|
|
609
|
+
}
|
|
610
|
+
return chain;
|
|
611
|
+
}
|
|
612
|
+
toString() {
|
|
613
|
+
return `Config(id=${this.id}, key=${this.key}, name=${this.name})`;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// src/config/client.ts
|
|
618
|
+
var BASE_URL = "https://config.smplkit.com";
|
|
619
|
+
function resourceToConfig(resource, client) {
|
|
620
|
+
const attrs = resource.attributes;
|
|
621
|
+
return new Config(client, {
|
|
622
|
+
id: resource.id ?? "",
|
|
623
|
+
key: attrs.key ?? "",
|
|
624
|
+
name: attrs.name,
|
|
625
|
+
description: attrs.description ?? null,
|
|
626
|
+
parent: attrs.parent ?? null,
|
|
627
|
+
values: attrs.values ?? {},
|
|
628
|
+
environments: attrs.environments ?? {},
|
|
629
|
+
createdAt: attrs.created_at ? new Date(attrs.created_at) : null,
|
|
630
|
+
updatedAt: attrs.updated_at ? new Date(attrs.updated_at) : null
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async function checkError(response, context) {
|
|
634
|
+
const body = await response.text().catch(() => "");
|
|
635
|
+
switch (response.status) {
|
|
636
|
+
case 404:
|
|
637
|
+
throw new SmplNotFoundError(body || context, 404, body);
|
|
638
|
+
case 409:
|
|
639
|
+
throw new SmplConflictError(body || context, 409, body);
|
|
640
|
+
case 422:
|
|
641
|
+
throw new SmplValidationError(body || context, 422, body);
|
|
642
|
+
default:
|
|
643
|
+
throw new SmplError(`HTTP ${response.status}: ${body}`, response.status, body);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function wrapFetchError(err) {
|
|
647
|
+
if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
if (err instanceof TypeError) {
|
|
651
|
+
throw new SmplConnectionError(`Network error: ${err.message}`);
|
|
652
|
+
}
|
|
653
|
+
throw new SmplConnectionError(
|
|
654
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
function buildRequestBody(options) {
|
|
658
|
+
const attrs = {
|
|
659
|
+
name: options.name
|
|
660
|
+
};
|
|
661
|
+
if (options.key !== void 0) attrs.key = options.key;
|
|
662
|
+
if (options.description !== void 0) attrs.description = options.description;
|
|
663
|
+
if (options.parent !== void 0) attrs.parent = options.parent;
|
|
664
|
+
if (options.values !== void 0)
|
|
665
|
+
attrs.values = options.values;
|
|
666
|
+
if (options.environments !== void 0)
|
|
667
|
+
attrs.environments = options.environments;
|
|
668
|
+
return {
|
|
669
|
+
data: {
|
|
670
|
+
id: options.id ?? null,
|
|
671
|
+
type: "config",
|
|
672
|
+
attributes: attrs
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
var ConfigClient = class {
|
|
677
|
+
/** @internal — used by Config instances for reconnecting and WebSocket auth. */
|
|
678
|
+
_apiKey;
|
|
679
|
+
/** @internal */
|
|
680
|
+
_baseUrl = BASE_URL;
|
|
681
|
+
/** @internal */
|
|
682
|
+
_http;
|
|
683
|
+
/** @internal */
|
|
684
|
+
constructor(apiKey, timeout) {
|
|
685
|
+
this._apiKey = apiKey;
|
|
686
|
+
const ms = timeout ?? 3e4;
|
|
687
|
+
this._http = (0, import_openapi_fetch.default)({
|
|
688
|
+
baseUrl: BASE_URL,
|
|
689
|
+
headers: {
|
|
690
|
+
Authorization: `Bearer ${apiKey}`,
|
|
691
|
+
Accept: "application/json"
|
|
692
|
+
},
|
|
693
|
+
// openapi-fetch custom fetch receives a pre-built Request object
|
|
694
|
+
fetch: async (request) => {
|
|
695
|
+
const controller = new AbortController();
|
|
696
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
697
|
+
try {
|
|
698
|
+
return await fetch(new Request(request, { signal: controller.signal }));
|
|
699
|
+
} catch (err) {
|
|
700
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
701
|
+
throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
|
|
702
|
+
}
|
|
703
|
+
throw err;
|
|
704
|
+
} finally {
|
|
705
|
+
clearTimeout(timer);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Fetch a single config by key or UUID.
|
|
712
|
+
*
|
|
713
|
+
* Exactly one of `key` or `id` must be provided.
|
|
714
|
+
*
|
|
715
|
+
* @throws {SmplNotFoundError} If no matching config exists.
|
|
716
|
+
*/
|
|
717
|
+
async get(options) {
|
|
718
|
+
const { key, id } = options;
|
|
719
|
+
if (key === void 0 === (id === void 0)) {
|
|
720
|
+
throw new Error("Exactly one of 'key' or 'id' must be provided.");
|
|
721
|
+
}
|
|
722
|
+
return id !== void 0 ? this._getById(id) : this._getByKey(key);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* List all configs for the account.
|
|
726
|
+
*/
|
|
727
|
+
async list() {
|
|
728
|
+
let data;
|
|
729
|
+
try {
|
|
730
|
+
const result = await this._http.GET("/api/v1/configs", {});
|
|
731
|
+
if (result.error !== void 0) await checkError(result.response, "Failed to list configs");
|
|
732
|
+
data = result.data;
|
|
733
|
+
} catch (err) {
|
|
734
|
+
wrapFetchError(err);
|
|
735
|
+
}
|
|
736
|
+
if (!data) return [];
|
|
737
|
+
return data.data.map((r) => resourceToConfig(r, this));
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Create a new config.
|
|
741
|
+
*
|
|
742
|
+
* @throws {SmplValidationError} If the server rejects the request.
|
|
743
|
+
*/
|
|
744
|
+
async create(options) {
|
|
745
|
+
const body = buildRequestBody({
|
|
746
|
+
name: options.name,
|
|
747
|
+
key: options.key,
|
|
748
|
+
description: options.description,
|
|
749
|
+
parent: options.parent,
|
|
750
|
+
values: options.values
|
|
751
|
+
});
|
|
752
|
+
let data;
|
|
753
|
+
try {
|
|
754
|
+
const result = await this._http.POST("/api/v1/configs", { body });
|
|
755
|
+
if (result.error !== void 0) await checkError(result.response, "Failed to create config");
|
|
756
|
+
data = result.data;
|
|
757
|
+
} catch (err) {
|
|
758
|
+
wrapFetchError(err);
|
|
759
|
+
}
|
|
760
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create config");
|
|
761
|
+
return resourceToConfig(data.data, this);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Delete a config by UUID.
|
|
765
|
+
*
|
|
766
|
+
* @throws {SmplNotFoundError} If the config does not exist.
|
|
767
|
+
* @throws {SmplConflictError} If the config has child configs.
|
|
768
|
+
*/
|
|
769
|
+
async delete(configId) {
|
|
770
|
+
try {
|
|
771
|
+
const result = await this._http.DELETE("/api/v1/configs/{id}", {
|
|
772
|
+
params: { path: { id: configId } }
|
|
773
|
+
});
|
|
774
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
775
|
+
await checkError(result.response, `Failed to delete config ${configId}`);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
wrapFetchError(err);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Internal: PUT a full config update and return the updated model.
|
|
782
|
+
*
|
|
783
|
+
* Called by {@link Config} instance methods.
|
|
784
|
+
* @internal
|
|
785
|
+
*/
|
|
786
|
+
async _updateConfig(payload) {
|
|
787
|
+
const body = buildRequestBody({
|
|
788
|
+
id: payload.configId,
|
|
789
|
+
name: payload.name,
|
|
790
|
+
key: payload.key,
|
|
791
|
+
description: payload.description,
|
|
792
|
+
parent: payload.parent,
|
|
793
|
+
values: payload.values,
|
|
794
|
+
environments: payload.environments
|
|
795
|
+
});
|
|
796
|
+
let data;
|
|
797
|
+
try {
|
|
798
|
+
const result = await this._http.PUT("/api/v1/configs/{id}", {
|
|
799
|
+
params: { path: { id: payload.configId } },
|
|
800
|
+
body
|
|
801
|
+
});
|
|
802
|
+
if (result.error !== void 0)
|
|
803
|
+
await checkError(result.response, `Failed to update config ${payload.configId}`);
|
|
804
|
+
data = result.data;
|
|
805
|
+
} catch (err) {
|
|
806
|
+
wrapFetchError(err);
|
|
807
|
+
}
|
|
808
|
+
if (!data || !data.data)
|
|
809
|
+
throw new SmplValidationError(`Failed to update config ${payload.configId}`);
|
|
810
|
+
return resourceToConfig(data.data, this);
|
|
811
|
+
}
|
|
812
|
+
// ---- Private helpers ----
|
|
813
|
+
async _getById(configId) {
|
|
814
|
+
let data;
|
|
815
|
+
try {
|
|
816
|
+
const result = await this._http.GET("/api/v1/configs/{id}", {
|
|
817
|
+
params: { path: { id: configId } }
|
|
818
|
+
});
|
|
819
|
+
if (result.error !== void 0)
|
|
820
|
+
await checkError(result.response, `Config ${configId} not found`);
|
|
821
|
+
data = result.data;
|
|
822
|
+
} catch (err) {
|
|
823
|
+
wrapFetchError(err);
|
|
824
|
+
}
|
|
825
|
+
if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
|
|
826
|
+
return resourceToConfig(data.data, this);
|
|
827
|
+
}
|
|
828
|
+
async _getByKey(key) {
|
|
829
|
+
let data;
|
|
830
|
+
try {
|
|
831
|
+
const result = await this._http.GET("/api/v1/configs", {
|
|
832
|
+
params: { query: { "filter[key]": key } }
|
|
833
|
+
});
|
|
834
|
+
if (result.error !== void 0)
|
|
835
|
+
await checkError(result.response, `Config with key '${key}' not found`);
|
|
836
|
+
data = result.data;
|
|
837
|
+
} catch (err) {
|
|
838
|
+
wrapFetchError(err);
|
|
839
|
+
}
|
|
840
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
841
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found`);
|
|
842
|
+
}
|
|
843
|
+
return resourceToConfig(data.data[0], this);
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// src/client.ts
|
|
848
|
+
var SmplkitClient = class {
|
|
849
|
+
/** Client for config management-plane operations. */
|
|
850
|
+
config;
|
|
851
|
+
constructor(options) {
|
|
852
|
+
if (!options.apiKey) {
|
|
853
|
+
throw new Error("apiKey is required");
|
|
854
|
+
}
|
|
855
|
+
this.config = new ConfigClient(options.apiKey, options.timeout);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// src/index.ts
|
|
860
|
+
init_runtime();
|
|
861
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
862
|
+
0 && (module.exports = {
|
|
863
|
+
Config,
|
|
864
|
+
ConfigClient,
|
|
865
|
+
ConfigRuntime,
|
|
866
|
+
SmplConflictError,
|
|
867
|
+
SmplConnectionError,
|
|
868
|
+
SmplError,
|
|
869
|
+
SmplNotFoundError,
|
|
870
|
+
SmplTimeoutError,
|
|
871
|
+
SmplValidationError,
|
|
872
|
+
SmplkitClient
|
|
873
|
+
});
|
|
874
|
+
//# sourceMappingURL=index.cjs.map
|