@smplkit/sdk 1.2.0 → 1.2.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/dist/index.cjs +355 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -199
- package/dist/index.d.ts +244 -199
- package/dist/index.js +355 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2VYY5OMH.js +0 -241
- package/dist/chunk-2VYY5OMH.js.map +0 -1
- package/dist/runtime-MIIY5ZNG.js +0 -7
- package/dist/runtime-MIIY5ZNG.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -5,9 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
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
8
|
var __export = (target, all) => {
|
|
12
9
|
for (var name in all)
|
|
13
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -30,259 +27,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
27
|
));
|
|
31
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
29
|
|
|
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.items ?? {};
|
|
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 ConfigRuntime;
|
|
72
|
-
var init_runtime = __esm({
|
|
73
|
-
"src/config/runtime.ts"() {
|
|
74
|
-
"use strict";
|
|
75
|
-
init_resolve();
|
|
76
|
-
ConfigRuntime = class {
|
|
77
|
-
_cache;
|
|
78
|
-
_chain;
|
|
79
|
-
_fetchCount;
|
|
80
|
-
_lastFetchAt;
|
|
81
|
-
_closed = false;
|
|
82
|
-
_listeners = [];
|
|
83
|
-
_environment;
|
|
84
|
-
_fetchChain;
|
|
85
|
-
_sharedWs = null;
|
|
86
|
-
/** @internal */
|
|
87
|
-
constructor(options) {
|
|
88
|
-
this._environment = options.environment;
|
|
89
|
-
this._fetchChain = options.fetchChain;
|
|
90
|
-
this._chain = options.chain;
|
|
91
|
-
this._cache = resolveChain(options.chain, options.environment);
|
|
92
|
-
this._fetchCount = options.chain.length;
|
|
93
|
-
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
94
|
-
if (options.sharedWs) {
|
|
95
|
-
this._sharedWs = options.sharedWs;
|
|
96
|
-
this._sharedWs.on("config_changed", this._handleConfigChanged);
|
|
97
|
-
this._sharedWs.on("config_deleted", this._handleConfigDeleted);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// ---- Value access (synchronous, local cache) ----
|
|
101
|
-
/**
|
|
102
|
-
* Return the resolved value for `key`, or `defaultValue` if absent.
|
|
103
|
-
*
|
|
104
|
-
* @param key - The config key to look up.
|
|
105
|
-
* @param defaultValue - Returned when the key is not present (default: null).
|
|
106
|
-
*/
|
|
107
|
-
get(key, defaultValue = null) {
|
|
108
|
-
return key in this._cache ? this._cache[key] : defaultValue;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Return the value as a string, or `defaultValue` if absent or not a string.
|
|
112
|
-
*/
|
|
113
|
-
getString(key, defaultValue = null) {
|
|
114
|
-
const value = this._cache[key];
|
|
115
|
-
return typeof value === "string" ? value : defaultValue;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Return the value as a number, or `defaultValue` if absent or not a number.
|
|
119
|
-
*/
|
|
120
|
-
getInt(key, defaultValue = null) {
|
|
121
|
-
const value = this._cache[key];
|
|
122
|
-
return typeof value === "number" ? value : defaultValue;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Return the value as a boolean, or `defaultValue` if absent or not a boolean.
|
|
126
|
-
*/
|
|
127
|
-
getBool(key, defaultValue = null) {
|
|
128
|
-
const value = this._cache[key];
|
|
129
|
-
return typeof value === "boolean" ? value : defaultValue;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Return whether `key` is present in the resolved configuration.
|
|
133
|
-
*/
|
|
134
|
-
exists(key) {
|
|
135
|
-
return key in this._cache;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Return a shallow copy of the full resolved configuration.
|
|
139
|
-
*/
|
|
140
|
-
getAll() {
|
|
141
|
-
return { ...this._cache };
|
|
142
|
-
}
|
|
143
|
-
// ---- Change listeners ----
|
|
144
|
-
/**
|
|
145
|
-
* Register a listener that fires when a config value changes.
|
|
146
|
-
*
|
|
147
|
-
* @param callback - Called with a {@link ConfigChangeEvent} on each change.
|
|
148
|
-
* @param options.key - If provided, the listener fires only for this key.
|
|
149
|
-
* If omitted, the listener fires for all changes.
|
|
150
|
-
*/
|
|
151
|
-
onChange(callback, options) {
|
|
152
|
-
this._listeners.push({
|
|
153
|
-
callback,
|
|
154
|
-
key: options?.key ?? null
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
// ---- Diagnostics ----
|
|
158
|
-
/**
|
|
159
|
-
* Return diagnostic statistics for this runtime.
|
|
160
|
-
*/
|
|
161
|
-
stats() {
|
|
162
|
-
return {
|
|
163
|
-
fetchCount: this._fetchCount,
|
|
164
|
-
lastFetchAt: this._lastFetchAt
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Return the current WebSocket connection status.
|
|
169
|
-
*/
|
|
170
|
-
connectionStatus() {
|
|
171
|
-
if (this._sharedWs) {
|
|
172
|
-
return this._sharedWs.connectionStatus;
|
|
173
|
-
}
|
|
174
|
-
return "disconnected";
|
|
175
|
-
}
|
|
176
|
-
// ---- Lifecycle ----
|
|
177
|
-
/**
|
|
178
|
-
* Force a manual refresh of the cached configuration.
|
|
179
|
-
*
|
|
180
|
-
* Re-fetches the full config chain via HTTP, re-resolves values, updates
|
|
181
|
-
* the local cache, and fires listeners for any detected changes.
|
|
182
|
-
*
|
|
183
|
-
* @throws {Error} If no `fetchChain` function was provided on construction.
|
|
184
|
-
*/
|
|
185
|
-
async refresh() {
|
|
186
|
-
if (!this._fetchChain) {
|
|
187
|
-
throw new Error("No fetchChain function provided; cannot refresh.");
|
|
188
|
-
}
|
|
189
|
-
const newChain = await this._fetchChain();
|
|
190
|
-
const oldCache = this._cache;
|
|
191
|
-
this._chain = newChain;
|
|
192
|
-
this._cache = resolveChain(newChain, this._environment);
|
|
193
|
-
this._fetchCount += newChain.length;
|
|
194
|
-
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
195
|
-
this._diffAndFire(oldCache, this._cache, "manual");
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Close the runtime connection.
|
|
199
|
-
*
|
|
200
|
-
* Unregisters from the shared WebSocket. Safe to call multiple times.
|
|
201
|
-
*/
|
|
202
|
-
async close() {
|
|
203
|
-
this._closed = true;
|
|
204
|
-
if (this._sharedWs !== null) {
|
|
205
|
-
this._sharedWs.off("config_changed", this._handleConfigChanged);
|
|
206
|
-
this._sharedWs.off("config_deleted", this._handleConfigDeleted);
|
|
207
|
-
this._sharedWs = null;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Async dispose support for `await using` (TypeScript 5.2+).
|
|
212
|
-
*/
|
|
213
|
-
async [Symbol.asyncDispose]() {
|
|
214
|
-
await this.close();
|
|
215
|
-
}
|
|
216
|
-
// ---- Shared WebSocket event handlers ----
|
|
217
|
-
_handleConfigChanged = (data) => {
|
|
218
|
-
if (this._closed) return;
|
|
219
|
-
const configId = data.config_id;
|
|
220
|
-
const changes = data.changes;
|
|
221
|
-
if (configId && changes) {
|
|
222
|
-
this._applyChanges(configId, changes);
|
|
223
|
-
} else if (this._fetchChain) {
|
|
224
|
-
void this._fetchChain().then((newChain) => {
|
|
225
|
-
const oldCache = this._cache;
|
|
226
|
-
this._chain = newChain;
|
|
227
|
-
this._cache = resolveChain(newChain, this._environment);
|
|
228
|
-
this._fetchCount += newChain.length;
|
|
229
|
-
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
230
|
-
this._diffAndFire(oldCache, this._cache, "websocket");
|
|
231
|
-
}).catch(() => {
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
_handleConfigDeleted = (_data) => {
|
|
236
|
-
this._closed = true;
|
|
237
|
-
void this.close();
|
|
238
|
-
};
|
|
239
|
-
_applyChanges(configId, changes) {
|
|
240
|
-
const chainEntry = this._chain.find((c) => c.id === configId);
|
|
241
|
-
if (!chainEntry) return;
|
|
242
|
-
for (const change of changes) {
|
|
243
|
-
const { key, new_value } = change;
|
|
244
|
-
const envEntry = chainEntry.environments[this._environment] !== void 0 && chainEntry.environments[this._environment] !== null ? chainEntry.environments[this._environment] : null;
|
|
245
|
-
const envValues = envEntry !== null && typeof envEntry === "object" ? envEntry.values ?? {} : null;
|
|
246
|
-
if (new_value === null || new_value === void 0) {
|
|
247
|
-
delete chainEntry.items[key];
|
|
248
|
-
if (envValues) delete envValues[key];
|
|
249
|
-
} else if (envValues && key in envValues) {
|
|
250
|
-
envValues[key] = new_value;
|
|
251
|
-
} else if (key in chainEntry.items) {
|
|
252
|
-
chainEntry.items[key] = new_value;
|
|
253
|
-
} else {
|
|
254
|
-
chainEntry.items[key] = new_value;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const oldCache = this._cache;
|
|
258
|
-
this._cache = resolveChain(this._chain, this._environment);
|
|
259
|
-
this._diffAndFire(oldCache, this._cache, "websocket");
|
|
260
|
-
}
|
|
261
|
-
_diffAndFire(oldCache, newCache, source) {
|
|
262
|
-
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
|
|
263
|
-
for (const key of allKeys) {
|
|
264
|
-
const oldVal = key in oldCache ? oldCache[key] : null;
|
|
265
|
-
const newVal = key in newCache ? newCache[key] : null;
|
|
266
|
-
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
267
|
-
const event = { key, oldValue: oldVal, newValue: newVal, source };
|
|
268
|
-
this._fireListeners(event);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
_fireListeners(event) {
|
|
273
|
-
for (const listener of this._listeners) {
|
|
274
|
-
if (listener.key === null || listener.key === event.key) {
|
|
275
|
-
try {
|
|
276
|
-
listener.callback(event);
|
|
277
|
-
} catch {
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
30
|
// src/index.ts
|
|
287
31
|
var index_exports = {};
|
|
288
32
|
__export(index_exports, {
|
|
@@ -304,6 +48,7 @@ __export(index_exports, {
|
|
|
304
48
|
SmplConflictError: () => SmplConflictError,
|
|
305
49
|
SmplConnectionError: () => SmplConnectionError,
|
|
306
50
|
SmplError: () => SmplError,
|
|
51
|
+
SmplNotConnectedError: () => SmplNotConnectedError,
|
|
307
52
|
SmplNotFoundError: () => SmplNotFoundError,
|
|
308
53
|
SmplTimeoutError: () => SmplTimeoutError,
|
|
309
54
|
SmplValidationError: () => SmplValidationError,
|
|
@@ -356,6 +101,13 @@ var SmplConflictError = class extends SmplError {
|
|
|
356
101
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
357
102
|
}
|
|
358
103
|
};
|
|
104
|
+
var SmplNotConnectedError = class extends SmplError {
|
|
105
|
+
constructor(message) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "SmplNotConnectedError";
|
|
108
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
359
111
|
var SmplValidationError = class extends SmplError {
|
|
360
112
|
constructor(message, statusCode, responseBody) {
|
|
361
113
|
super(message, statusCode ?? 422, responseBody);
|
|
@@ -364,6 +116,34 @@ var SmplValidationError = class extends SmplError {
|
|
|
364
116
|
}
|
|
365
117
|
};
|
|
366
118
|
|
|
119
|
+
// src/config/resolve.ts
|
|
120
|
+
function deepMerge(base, override) {
|
|
121
|
+
const result = { ...base };
|
|
122
|
+
for (const [key, value] of Object.entries(override)) {
|
|
123
|
+
if (key in result && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key]) && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
124
|
+
result[key] = deepMerge(
|
|
125
|
+
result[key],
|
|
126
|
+
value
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
result[key] = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function resolveChain(chain, environment) {
|
|
135
|
+
let accumulated = {};
|
|
136
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
137
|
+
const config = chain[i];
|
|
138
|
+
const baseValues = config.items ?? {};
|
|
139
|
+
const envEntry = (config.environments ?? {})[environment];
|
|
140
|
+
const envValues = envEntry !== null && envEntry !== void 0 && typeof envEntry === "object" && !Array.isArray(envEntry) ? envEntry.values ?? {} : {};
|
|
141
|
+
const configResolved = deepMerge(baseValues, envValues);
|
|
142
|
+
accumulated = deepMerge(accumulated, configResolved);
|
|
143
|
+
}
|
|
144
|
+
return accumulated;
|
|
145
|
+
}
|
|
146
|
+
|
|
367
147
|
// src/config/types.ts
|
|
368
148
|
var Config = class {
|
|
369
149
|
/** UUID of the config. */
|
|
@@ -490,46 +270,6 @@ var Config = class {
|
|
|
490
270
|
await this.setValues(existing, environment);
|
|
491
271
|
}
|
|
492
272
|
}
|
|
493
|
-
/**
|
|
494
|
-
* Connect to this config for runtime value resolution.
|
|
495
|
-
*
|
|
496
|
-
* Eagerly fetches this config and its full parent chain, resolves values
|
|
497
|
-
* for the given environment via deep merge, and returns a
|
|
498
|
-
* {@link ConfigRuntime} with a fully populated local cache.
|
|
499
|
-
*
|
|
500
|
-
* A background WebSocket connection is started for real-time updates.
|
|
501
|
-
* If the WebSocket fails to connect, the runtime operates in cache-only
|
|
502
|
-
* mode and reconnects automatically.
|
|
503
|
-
*
|
|
504
|
-
* Supports both `await` and `await using` (TypeScript 5.2+)::
|
|
505
|
-
*
|
|
506
|
-
* ```typescript
|
|
507
|
-
* // Simple await
|
|
508
|
-
* const runtime = await config.connect("production");
|
|
509
|
-
* try { ... } finally { await runtime.close(); }
|
|
510
|
-
*
|
|
511
|
-
* // await using (auto-close)
|
|
512
|
-
* await using runtime = await config.connect("production");
|
|
513
|
-
* ```
|
|
514
|
-
*
|
|
515
|
-
* @param environment - The environment to resolve for (e.g. `"production"`).
|
|
516
|
-
* @param options.timeout - Milliseconds to wait for the initial fetch.
|
|
517
|
-
*/
|
|
518
|
-
async connect(environment, options) {
|
|
519
|
-
const { ConfigRuntime: ConfigRuntime2 } = await Promise.resolve().then(() => (init_runtime(), runtime_exports));
|
|
520
|
-
const timeout = options?.timeout ?? 3e4;
|
|
521
|
-
const chain = await this._buildChain(timeout);
|
|
522
|
-
return new ConfigRuntime2({
|
|
523
|
-
configKey: this.key,
|
|
524
|
-
configId: this.id,
|
|
525
|
-
environment,
|
|
526
|
-
chain,
|
|
527
|
-
apiKey: this._client._apiKey,
|
|
528
|
-
baseUrl: this._client._baseUrl,
|
|
529
|
-
fetchChain: () => this._buildChain(timeout),
|
|
530
|
-
sharedWs: this._client._getSharedWs ? this._client._getSharedWs() : null
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
273
|
/**
|
|
534
274
|
* Walk the parent chain and return config data objects, child-to-root.
|
|
535
275
|
* @internal
|
|
@@ -676,6 +416,10 @@ var ConfigClient = class {
|
|
|
676
416
|
_http;
|
|
677
417
|
/** @internal — returns the shared WebSocket for real-time updates. */
|
|
678
418
|
_getSharedWs;
|
|
419
|
+
/** @internal — set by SmplClient after construction. */
|
|
420
|
+
_parent = null;
|
|
421
|
+
_configCache = {};
|
|
422
|
+
_connected = false;
|
|
679
423
|
/** @internal */
|
|
680
424
|
constructor(apiKey, timeout) {
|
|
681
425
|
this._apiKey = apiKey;
|
|
@@ -773,6 +517,44 @@ var ConfigClient = class {
|
|
|
773
517
|
wrapFetchError(err);
|
|
774
518
|
}
|
|
775
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Fetch all configs, resolve values for the environment, and cache.
|
|
522
|
+
* @internal — called by SmplClient.connect().
|
|
523
|
+
*/
|
|
524
|
+
async _connectInternal(environment) {
|
|
525
|
+
const configs = await this.list();
|
|
526
|
+
const cache = {};
|
|
527
|
+
for (const cfg of configs) {
|
|
528
|
+
const chain = await cfg._buildChain(this._http);
|
|
529
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
530
|
+
}
|
|
531
|
+
this._configCache = cache;
|
|
532
|
+
this._connected = true;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Read a resolved config value (prescriptive access).
|
|
536
|
+
*
|
|
537
|
+
* Requires {@link SmplClient.connect} to have been called.
|
|
538
|
+
*
|
|
539
|
+
* @param configKey - The config key to look up.
|
|
540
|
+
* @param itemKey - Optional specific item key. If omitted, returns all values.
|
|
541
|
+
* @param defaultValue - Default value if the key is missing.
|
|
542
|
+
*
|
|
543
|
+
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
544
|
+
*/
|
|
545
|
+
getValue(configKey, itemKey, defaultValue) {
|
|
546
|
+
if (!this._connected) {
|
|
547
|
+
throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
|
|
548
|
+
}
|
|
549
|
+
const resolved = this._configCache[configKey];
|
|
550
|
+
if (resolved === void 0) {
|
|
551
|
+
return defaultValue ?? null;
|
|
552
|
+
}
|
|
553
|
+
if (itemKey === void 0) {
|
|
554
|
+
return { ...resolved };
|
|
555
|
+
}
|
|
556
|
+
return itemKey in resolved ? resolved[itemKey] : defaultValue ?? null;
|
|
557
|
+
}
|
|
776
558
|
/**
|
|
777
559
|
* Internal: PUT a full config update and return the updated model.
|
|
778
560
|
*
|
|
@@ -1360,6 +1142,8 @@ var FlagsClient = class {
|
|
|
1360
1142
|
// Shared WebSocket (set during connect)
|
|
1361
1143
|
_wsManager = null;
|
|
1362
1144
|
_ensureWs;
|
|
1145
|
+
/** @internal — set by SmplClient after construction. */
|
|
1146
|
+
_parent = null;
|
|
1363
1147
|
/** @internal */
|
|
1364
1148
|
constructor(apiKey, ensureWs, timeout) {
|
|
1365
1149
|
this._apiKey = apiKey;
|
|
@@ -1598,8 +1382,9 @@ var FlagsClient = class {
|
|
|
1598
1382
|
/**
|
|
1599
1383
|
* Connect to an environment: fetch flag definitions, register on
|
|
1600
1384
|
* shared WebSocket, enable local evaluation.
|
|
1385
|
+
* @internal — called by SmplClient.connect().
|
|
1601
1386
|
*/
|
|
1602
|
-
async
|
|
1387
|
+
async _connectInternal(environment) {
|
|
1603
1388
|
this._environment = environment;
|
|
1604
1389
|
await this._fetchAllFlags();
|
|
1605
1390
|
this._connected = true;
|
|
@@ -1686,6 +1471,9 @@ var FlagsClient = class {
|
|
|
1686
1471
|
*/
|
|
1687
1472
|
async evaluate(key, options) {
|
|
1688
1473
|
const evalDict = contextsToEvalDict(options.context);
|
|
1474
|
+
if (this._parent?._service && !("service" in evalDict)) {
|
|
1475
|
+
evalDict["service"] = { key: this._parent._service };
|
|
1476
|
+
}
|
|
1689
1477
|
let flagDef = null;
|
|
1690
1478
|
if (this._connected && key in this._flagStore) {
|
|
1691
1479
|
flagDef = this._flagStore[key];
|
|
@@ -1709,7 +1497,7 @@ var FlagsClient = class {
|
|
|
1709
1497
|
/** @internal */
|
|
1710
1498
|
_evaluateHandle(key, defaultValue, context) {
|
|
1711
1499
|
if (!this._connected) {
|
|
1712
|
-
|
|
1500
|
+
throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
|
|
1713
1501
|
}
|
|
1714
1502
|
let evalDict;
|
|
1715
1503
|
if (context !== null) {
|
|
@@ -1724,6 +1512,9 @@ var FlagsClient = class {
|
|
|
1724
1512
|
} else {
|
|
1725
1513
|
evalDict = {};
|
|
1726
1514
|
}
|
|
1515
|
+
if (this._parent?._service && !("service" in evalDict)) {
|
|
1516
|
+
evalDict["service"] = { key: this._parent._service };
|
|
1517
|
+
}
|
|
1727
1518
|
const ctxHash = hashContext(evalDict);
|
|
1728
1519
|
const cacheKey = `${key}:${ctxHash}`;
|
|
1729
1520
|
const [hit, cachedValue] = this._cache.get(cacheKey);
|
|
@@ -2050,6 +1841,7 @@ function resolveApiKey(explicit) {
|
|
|
2050
1841
|
|
|
2051
1842
|
// src/client.ts
|
|
2052
1843
|
var APP_BASE_URL2 = "https://app.smplkit.com";
|
|
1844
|
+
var NO_ENVIRONMENT_MESSAGE = "No environment provided. Set one of:\n 1. Pass environment to the constructor\n 2. Set the SMPLKIT_ENVIRONMENT environment variable";
|
|
2053
1845
|
var SmplClient = class {
|
|
2054
1846
|
/** Client for config management-plane operations. */
|
|
2055
1847
|
config;
|
|
@@ -2057,12 +1849,66 @@ var SmplClient = class {
|
|
|
2057
1849
|
flags;
|
|
2058
1850
|
_wsManager = null;
|
|
2059
1851
|
_apiKey;
|
|
1852
|
+
/** @internal */
|
|
1853
|
+
_environment;
|
|
1854
|
+
/** @internal */
|
|
1855
|
+
_service;
|
|
1856
|
+
_connected = false;
|
|
1857
|
+
_timeout;
|
|
2060
1858
|
constructor(options = {}) {
|
|
2061
1859
|
const apiKey = resolveApiKey(options.apiKey);
|
|
2062
1860
|
this._apiKey = apiKey;
|
|
2063
|
-
|
|
2064
|
-
|
|
1861
|
+
const environment = options.environment || process.env.SMPLKIT_ENVIRONMENT;
|
|
1862
|
+
if (!environment) {
|
|
1863
|
+
throw new SmplError(NO_ENVIRONMENT_MESSAGE);
|
|
1864
|
+
}
|
|
1865
|
+
this._environment = environment;
|
|
1866
|
+
this._service = options.service || process.env.SMPLKIT_SERVICE || null;
|
|
1867
|
+
this._timeout = options.timeout ?? 3e4;
|
|
1868
|
+
this.config = new ConfigClient(apiKey, this._timeout);
|
|
1869
|
+
this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
2065
1870
|
this.config._getSharedWs = () => this._ensureWs();
|
|
1871
|
+
this.flags._parent = this;
|
|
1872
|
+
this.config._parent = this;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Connect to the smplkit platform.
|
|
1876
|
+
*
|
|
1877
|
+
* Fetches initial flag and config data, opens the shared WebSocket,
|
|
1878
|
+
* and registers the service as a context instance (if provided).
|
|
1879
|
+
*
|
|
1880
|
+
* This method is idempotent — calling it multiple times is safe.
|
|
1881
|
+
*/
|
|
1882
|
+
async connect() {
|
|
1883
|
+
if (this._connected) return;
|
|
1884
|
+
if (this._service) {
|
|
1885
|
+
await this._registerServiceContext();
|
|
1886
|
+
}
|
|
1887
|
+
await this.flags._connectInternal(this._environment);
|
|
1888
|
+
await this.config._connectInternal(this._environment);
|
|
1889
|
+
this._connected = true;
|
|
1890
|
+
}
|
|
1891
|
+
/** @internal */
|
|
1892
|
+
async _registerServiceContext() {
|
|
1893
|
+
try {
|
|
1894
|
+
await fetch(`${APP_BASE_URL2}/api/v1/contexts/bulk`, {
|
|
1895
|
+
method: "PUT",
|
|
1896
|
+
headers: {
|
|
1897
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
1898
|
+
"Content-Type": "application/json"
|
|
1899
|
+
},
|
|
1900
|
+
body: JSON.stringify({
|
|
1901
|
+
contexts: [
|
|
1902
|
+
{
|
|
1903
|
+
type: "service",
|
|
1904
|
+
key: this._service,
|
|
1905
|
+
attributes: { name: this._service }
|
|
1906
|
+
}
|
|
1907
|
+
]
|
|
1908
|
+
})
|
|
1909
|
+
});
|
|
1910
|
+
} catch {
|
|
1911
|
+
}
|
|
2066
1912
|
}
|
|
2067
1913
|
/** Lazily create and start the shared WebSocket. @internal */
|
|
2068
1914
|
_ensureWs() {
|
|
@@ -2081,8 +1927,214 @@ var SmplClient = class {
|
|
|
2081
1927
|
}
|
|
2082
1928
|
};
|
|
2083
1929
|
|
|
2084
|
-
// src/
|
|
2085
|
-
|
|
1930
|
+
// src/config/runtime.ts
|
|
1931
|
+
var ConfigRuntime = class {
|
|
1932
|
+
_cache;
|
|
1933
|
+
_chain;
|
|
1934
|
+
_fetchCount;
|
|
1935
|
+
_lastFetchAt;
|
|
1936
|
+
_closed = false;
|
|
1937
|
+
_listeners = [];
|
|
1938
|
+
_environment;
|
|
1939
|
+
_fetchChain;
|
|
1940
|
+
_sharedWs = null;
|
|
1941
|
+
/** @internal */
|
|
1942
|
+
constructor(options) {
|
|
1943
|
+
this._environment = options.environment;
|
|
1944
|
+
this._fetchChain = options.fetchChain;
|
|
1945
|
+
this._chain = options.chain;
|
|
1946
|
+
this._cache = resolveChain(options.chain, options.environment);
|
|
1947
|
+
this._fetchCount = options.chain.length;
|
|
1948
|
+
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1949
|
+
if (options.sharedWs) {
|
|
1950
|
+
this._sharedWs = options.sharedWs;
|
|
1951
|
+
this._sharedWs.on("config_changed", this._handleConfigChanged);
|
|
1952
|
+
this._sharedWs.on("config_deleted", this._handleConfigDeleted);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
// ---- Value access (synchronous, local cache) ----
|
|
1956
|
+
/**
|
|
1957
|
+
* Return the resolved value for `key`, or `defaultValue` if absent.
|
|
1958
|
+
*
|
|
1959
|
+
* @param key - The config key to look up.
|
|
1960
|
+
* @param defaultValue - Returned when the key is not present (default: null).
|
|
1961
|
+
*/
|
|
1962
|
+
get(key, defaultValue = null) {
|
|
1963
|
+
return key in this._cache ? this._cache[key] : defaultValue;
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Return the value as a string, or `defaultValue` if absent or not a string.
|
|
1967
|
+
*/
|
|
1968
|
+
getString(key, defaultValue = null) {
|
|
1969
|
+
const value = this._cache[key];
|
|
1970
|
+
return typeof value === "string" ? value : defaultValue;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Return the value as a number, or `defaultValue` if absent or not a number.
|
|
1974
|
+
*/
|
|
1975
|
+
getInt(key, defaultValue = null) {
|
|
1976
|
+
const value = this._cache[key];
|
|
1977
|
+
return typeof value === "number" ? value : defaultValue;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Return the value as a boolean, or `defaultValue` if absent or not a boolean.
|
|
1981
|
+
*/
|
|
1982
|
+
getBool(key, defaultValue = null) {
|
|
1983
|
+
const value = this._cache[key];
|
|
1984
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Return whether `key` is present in the resolved configuration.
|
|
1988
|
+
*/
|
|
1989
|
+
exists(key) {
|
|
1990
|
+
return key in this._cache;
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Return a shallow copy of the full resolved configuration.
|
|
1994
|
+
*/
|
|
1995
|
+
getAll() {
|
|
1996
|
+
return { ...this._cache };
|
|
1997
|
+
}
|
|
1998
|
+
// ---- Change listeners ----
|
|
1999
|
+
/**
|
|
2000
|
+
* Register a listener that fires when a config value changes.
|
|
2001
|
+
*
|
|
2002
|
+
* @param callback - Called with a {@link ConfigChangeEvent} on each change.
|
|
2003
|
+
* @param options.key - If provided, the listener fires only for this key.
|
|
2004
|
+
* If omitted, the listener fires for all changes.
|
|
2005
|
+
*/
|
|
2006
|
+
onChange(callback, options) {
|
|
2007
|
+
this._listeners.push({
|
|
2008
|
+
callback,
|
|
2009
|
+
key: options?.key ?? null
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
// ---- Diagnostics ----
|
|
2013
|
+
/**
|
|
2014
|
+
* Return diagnostic statistics for this runtime.
|
|
2015
|
+
*/
|
|
2016
|
+
stats() {
|
|
2017
|
+
return {
|
|
2018
|
+
fetchCount: this._fetchCount,
|
|
2019
|
+
lastFetchAt: this._lastFetchAt
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Return the current WebSocket connection status.
|
|
2024
|
+
*/
|
|
2025
|
+
connectionStatus() {
|
|
2026
|
+
if (this._sharedWs) {
|
|
2027
|
+
return this._sharedWs.connectionStatus;
|
|
2028
|
+
}
|
|
2029
|
+
return "disconnected";
|
|
2030
|
+
}
|
|
2031
|
+
// ---- Lifecycle ----
|
|
2032
|
+
/**
|
|
2033
|
+
* Force a manual refresh of the cached configuration.
|
|
2034
|
+
*
|
|
2035
|
+
* Re-fetches the full config chain via HTTP, re-resolves values, updates
|
|
2036
|
+
* the local cache, and fires listeners for any detected changes.
|
|
2037
|
+
*
|
|
2038
|
+
* @throws {Error} If no `fetchChain` function was provided on construction.
|
|
2039
|
+
*/
|
|
2040
|
+
async refresh() {
|
|
2041
|
+
if (!this._fetchChain) {
|
|
2042
|
+
throw new Error("No fetchChain function provided; cannot refresh.");
|
|
2043
|
+
}
|
|
2044
|
+
const newChain = await this._fetchChain();
|
|
2045
|
+
const oldCache = this._cache;
|
|
2046
|
+
this._chain = newChain;
|
|
2047
|
+
this._cache = resolveChain(newChain, this._environment);
|
|
2048
|
+
this._fetchCount += newChain.length;
|
|
2049
|
+
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2050
|
+
this._diffAndFire(oldCache, this._cache, "manual");
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Close the runtime connection.
|
|
2054
|
+
*
|
|
2055
|
+
* Unregisters from the shared WebSocket. Safe to call multiple times.
|
|
2056
|
+
*/
|
|
2057
|
+
async close() {
|
|
2058
|
+
this._closed = true;
|
|
2059
|
+
if (this._sharedWs !== null) {
|
|
2060
|
+
this._sharedWs.off("config_changed", this._handleConfigChanged);
|
|
2061
|
+
this._sharedWs.off("config_deleted", this._handleConfigDeleted);
|
|
2062
|
+
this._sharedWs = null;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Async dispose support for `await using` (TypeScript 5.2+).
|
|
2067
|
+
*/
|
|
2068
|
+
async [Symbol.asyncDispose]() {
|
|
2069
|
+
await this.close();
|
|
2070
|
+
}
|
|
2071
|
+
// ---- Shared WebSocket event handlers ----
|
|
2072
|
+
_handleConfigChanged = (data) => {
|
|
2073
|
+
if (this._closed) return;
|
|
2074
|
+
const configId = data.config_id;
|
|
2075
|
+
const changes = data.changes;
|
|
2076
|
+
if (configId && changes) {
|
|
2077
|
+
this._applyChanges(configId, changes);
|
|
2078
|
+
} else if (this._fetchChain) {
|
|
2079
|
+
void this._fetchChain().then((newChain) => {
|
|
2080
|
+
const oldCache = this._cache;
|
|
2081
|
+
this._chain = newChain;
|
|
2082
|
+
this._cache = resolveChain(newChain, this._environment);
|
|
2083
|
+
this._fetchCount += newChain.length;
|
|
2084
|
+
this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2085
|
+
this._diffAndFire(oldCache, this._cache, "websocket");
|
|
2086
|
+
}).catch(() => {
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
_handleConfigDeleted = (_data) => {
|
|
2091
|
+
this._closed = true;
|
|
2092
|
+
void this.close();
|
|
2093
|
+
};
|
|
2094
|
+
_applyChanges(configId, changes) {
|
|
2095
|
+
const chainEntry = this._chain.find((c) => c.id === configId);
|
|
2096
|
+
if (!chainEntry) return;
|
|
2097
|
+
for (const change of changes) {
|
|
2098
|
+
const { key, new_value } = change;
|
|
2099
|
+
const envEntry = chainEntry.environments[this._environment] !== void 0 && chainEntry.environments[this._environment] !== null ? chainEntry.environments[this._environment] : null;
|
|
2100
|
+
const envValues = envEntry !== null && typeof envEntry === "object" ? envEntry.values ?? {} : null;
|
|
2101
|
+
if (new_value === null || new_value === void 0) {
|
|
2102
|
+
delete chainEntry.items[key];
|
|
2103
|
+
if (envValues) delete envValues[key];
|
|
2104
|
+
} else if (envValues && key in envValues) {
|
|
2105
|
+
envValues[key] = new_value;
|
|
2106
|
+
} else if (key in chainEntry.items) {
|
|
2107
|
+
chainEntry.items[key] = new_value;
|
|
2108
|
+
} else {
|
|
2109
|
+
chainEntry.items[key] = new_value;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
const oldCache = this._cache;
|
|
2113
|
+
this._cache = resolveChain(this._chain, this._environment);
|
|
2114
|
+
this._diffAndFire(oldCache, this._cache, "websocket");
|
|
2115
|
+
}
|
|
2116
|
+
_diffAndFire(oldCache, newCache, source) {
|
|
2117
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
|
|
2118
|
+
for (const key of allKeys) {
|
|
2119
|
+
const oldVal = key in oldCache ? oldCache[key] : null;
|
|
2120
|
+
const newVal = key in newCache ? newCache[key] : null;
|
|
2121
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
2122
|
+
const event = { key, oldValue: oldVal, newValue: newVal, source };
|
|
2123
|
+
this._fireListeners(event);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
_fireListeners(event) {
|
|
2128
|
+
for (const listener of this._listeners) {
|
|
2129
|
+
if (listener.key === null || listener.key === event.key) {
|
|
2130
|
+
try {
|
|
2131
|
+
listener.callback(event);
|
|
2132
|
+
} catch {
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2086
2138
|
|
|
2087
2139
|
// src/flags/types.ts
|
|
2088
2140
|
var Context = class {
|
|
@@ -2168,6 +2220,7 @@ var Rule = class {
|
|
|
2168
2220
|
SmplConflictError,
|
|
2169
2221
|
SmplConnectionError,
|
|
2170
2222
|
SmplError,
|
|
2223
|
+
SmplNotConnectedError,
|
|
2171
2224
|
SmplNotFoundError,
|
|
2172
2225
|
SmplTimeoutError,
|
|
2173
2226
|
SmplValidationError,
|