@smplkit/sdk 1.3.12 → 1.3.14
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 +1199 -629
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +727 -407
- package/dist/index.d.ts +727 -407
- package/dist/index.js +1190 -623
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
-
import
|
|
2
|
+
import createClient4 from "openapi-fetch";
|
|
3
3
|
|
|
4
4
|
// src/config/client.ts
|
|
5
5
|
import createClient from "openapi-fetch";
|
|
@@ -62,13 +62,6 @@ var SmplConflictError = class extends SmplError {
|
|
|
62
62
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
|
-
var SmplNotConnectedError = class extends SmplError {
|
|
66
|
-
constructor(message) {
|
|
67
|
-
super(message);
|
|
68
|
-
this.name = "SmplNotConnectedError";
|
|
69
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
65
|
var SmplValidationError = class extends SmplError {
|
|
73
66
|
constructor(message, statusCode, responseBody, errors) {
|
|
74
67
|
super(message, statusCode ?? 422, responseBody, errors);
|
|
@@ -148,9 +141,9 @@ function resolveChain(chain, environment) {
|
|
|
148
141
|
|
|
149
142
|
// src/config/types.ts
|
|
150
143
|
var Config = class {
|
|
151
|
-
/** UUID of the config. */
|
|
144
|
+
/** UUID of the config, or `null` if unsaved. */
|
|
152
145
|
id;
|
|
153
|
-
/** Human-readable key (e.g. `"
|
|
146
|
+
/** Human-readable key (e.g. `"user-service"`). */
|
|
154
147
|
key;
|
|
155
148
|
/** Display name. */
|
|
156
149
|
name;
|
|
@@ -170,10 +163,7 @@ var Config = class {
|
|
|
170
163
|
createdAt;
|
|
171
164
|
/** When the config was last updated, or null if unavailable. */
|
|
172
165
|
updatedAt;
|
|
173
|
-
/**
|
|
174
|
-
* Internal reference to the parent client.
|
|
175
|
-
* @internal
|
|
176
|
-
*/
|
|
166
|
+
/** @internal */
|
|
177
167
|
_client;
|
|
178
168
|
/** @internal */
|
|
179
169
|
constructor(client, fields) {
|
|
@@ -189,100 +179,31 @@ var Config = class {
|
|
|
189
179
|
this.updatedAt = fields.updatedAt;
|
|
190
180
|
}
|
|
191
181
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* Builds the request from current attribute values, overriding with any
|
|
195
|
-
* provided options. Updates local attributes in place on success.
|
|
196
|
-
*
|
|
197
|
-
* @param options.name - New display name.
|
|
198
|
-
* @param options.description - New description (pass empty string to clear).
|
|
199
|
-
* @param options.items - New base values (replaces entirely).
|
|
200
|
-
* @param options.environments - New environments dict (replaces entirely).
|
|
201
|
-
*/
|
|
202
|
-
async update(options) {
|
|
203
|
-
const updated = await this._client._updateConfig({
|
|
204
|
-
configId: this.id,
|
|
205
|
-
name: options.name ?? this.name,
|
|
206
|
-
key: this.key,
|
|
207
|
-
description: options.description !== void 0 ? options.description : this.description,
|
|
208
|
-
parent: this.parent,
|
|
209
|
-
items: options.items ?? this.items,
|
|
210
|
-
environments: options.environments ?? this.environments
|
|
211
|
-
});
|
|
212
|
-
this.name = updated.name;
|
|
213
|
-
this.description = updated.description;
|
|
214
|
-
this.items = updated.items;
|
|
215
|
-
this.environments = updated.environments;
|
|
216
|
-
this.updatedAt = updated.updatedAt;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Replace base or environment-specific values.
|
|
220
|
-
*
|
|
221
|
-
* When `environment` is provided, replaces that environment's `values`
|
|
222
|
-
* sub-dict (other environments are preserved). When omitted, replaces
|
|
223
|
-
* the base `items`.
|
|
224
|
-
*
|
|
225
|
-
* @param values - The complete set of values to set.
|
|
226
|
-
* @param environment - Target environment, or omit for base values.
|
|
227
|
-
*/
|
|
228
|
-
async setValues(values, environment) {
|
|
229
|
-
let newItems;
|
|
230
|
-
let newEnvs;
|
|
231
|
-
if (environment === void 0) {
|
|
232
|
-
newItems = values;
|
|
233
|
-
newEnvs = this.environments;
|
|
234
|
-
} else {
|
|
235
|
-
newItems = this.items;
|
|
236
|
-
const existingEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? { ...this.environments[environment] } : {};
|
|
237
|
-
existingEntry.values = values;
|
|
238
|
-
newEnvs = { ...this.environments, [environment]: existingEntry };
|
|
239
|
-
}
|
|
240
|
-
const updated = await this._client._updateConfig({
|
|
241
|
-
configId: this.id,
|
|
242
|
-
name: this.name,
|
|
243
|
-
key: this.key,
|
|
244
|
-
description: this.description,
|
|
245
|
-
parent: this.parent,
|
|
246
|
-
items: newItems,
|
|
247
|
-
environments: newEnvs
|
|
248
|
-
});
|
|
249
|
-
this.items = updated.items;
|
|
250
|
-
this.environments = updated.environments;
|
|
251
|
-
this.updatedAt = updated.updatedAt;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Set a single key within base or environment-specific values.
|
|
182
|
+
* Persist this config to the server.
|
|
255
183
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* @param key - The config key to set.
|
|
259
|
-
* @param value - The value to assign.
|
|
260
|
-
* @param environment - Target environment, or omit for base values.
|
|
184
|
+
* POST if `id` is null (new config), PUT if `id` is set (update).
|
|
185
|
+
* Updates this instance in-place with the server response.
|
|
261
186
|
*/
|
|
262
|
-
async
|
|
263
|
-
if (
|
|
264
|
-
const
|
|
265
|
-
|
|
187
|
+
async save() {
|
|
188
|
+
if (this.id === null) {
|
|
189
|
+
const created = await this._client._createConfig(this);
|
|
190
|
+
this._apply(created);
|
|
266
191
|
} else {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
...typeof envEntry.values === "object" && envEntry.values !== null ? envEntry.values : {}
|
|
270
|
-
};
|
|
271
|
-
existing[key] = value;
|
|
272
|
-
await this.setValues(existing, environment);
|
|
192
|
+
const updated = await this._client._updateConfig(this);
|
|
193
|
+
this._apply(updated);
|
|
273
194
|
}
|
|
274
195
|
}
|
|
275
196
|
/**
|
|
276
197
|
* Walk the parent chain and return config data objects, child-to-root.
|
|
277
198
|
* @internal
|
|
278
199
|
*/
|
|
279
|
-
async _buildChain(
|
|
280
|
-
const chain = [{ id: this.id, items: this.items, environments: this.environments }];
|
|
200
|
+
async _buildChain() {
|
|
201
|
+
const chain = [{ id: this.id ?? "", items: this.items, environments: this.environments }];
|
|
281
202
|
let parentId = this.parent;
|
|
282
203
|
while (parentId !== null) {
|
|
283
|
-
const parentConfig = await this._client.
|
|
204
|
+
const parentConfig = await this._client._getById(parentId);
|
|
284
205
|
chain.push({
|
|
285
|
-
id: parentConfig.id,
|
|
206
|
+
id: parentConfig.id ?? "",
|
|
286
207
|
items: parentConfig.items,
|
|
287
208
|
environments: parentConfig.environments
|
|
288
209
|
});
|
|
@@ -290,11 +211,82 @@ var Config = class {
|
|
|
290
211
|
}
|
|
291
212
|
return chain;
|
|
292
213
|
}
|
|
214
|
+
/** @internal — copy all fields from another Config instance. */
|
|
215
|
+
_apply(other) {
|
|
216
|
+
this.id = other.id;
|
|
217
|
+
this.key = other.key;
|
|
218
|
+
this.name = other.name;
|
|
219
|
+
this.description = other.description;
|
|
220
|
+
this.parent = other.parent;
|
|
221
|
+
this.items = other.items;
|
|
222
|
+
this.environments = other.environments;
|
|
223
|
+
this.createdAt = other.createdAt;
|
|
224
|
+
this.updatedAt = other.updatedAt;
|
|
225
|
+
}
|
|
293
226
|
toString() {
|
|
294
227
|
return `Config(id=${this.id}, key=${this.key}, name=${this.name})`;
|
|
295
228
|
}
|
|
296
229
|
};
|
|
297
230
|
|
|
231
|
+
// src/config/proxy.ts
|
|
232
|
+
var LiveConfigProxy = class {
|
|
233
|
+
/** @internal */
|
|
234
|
+
_client;
|
|
235
|
+
/** @internal */
|
|
236
|
+
_key;
|
|
237
|
+
/** @internal */
|
|
238
|
+
_model;
|
|
239
|
+
constructor(client, key, model) {
|
|
240
|
+
this._client = client;
|
|
241
|
+
this._key = key;
|
|
242
|
+
this._model = model;
|
|
243
|
+
return new Proxy(this, {
|
|
244
|
+
get(target, prop, receiver) {
|
|
245
|
+
if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
|
|
246
|
+
return Reflect.get(target, prop, receiver);
|
|
247
|
+
}
|
|
248
|
+
const values = target._currentValues();
|
|
249
|
+
if (target._model) {
|
|
250
|
+
const instance = new target._model(values);
|
|
251
|
+
return instance[prop];
|
|
252
|
+
}
|
|
253
|
+
return values[prop];
|
|
254
|
+
},
|
|
255
|
+
has(target, prop) {
|
|
256
|
+
if (typeof prop === "symbol") return Reflect.has(target, prop);
|
|
257
|
+
const values = target._currentValues();
|
|
258
|
+
return prop in values;
|
|
259
|
+
},
|
|
260
|
+
ownKeys(target) {
|
|
261
|
+
const values = target._currentValues();
|
|
262
|
+
return Object.keys(values);
|
|
263
|
+
},
|
|
264
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
265
|
+
if (typeof prop === "symbol") return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
266
|
+
const values = target._currentValues();
|
|
267
|
+
if (prop in values) {
|
|
268
|
+
return {
|
|
269
|
+
configurable: true,
|
|
270
|
+
enumerable: true,
|
|
271
|
+
value: values[prop],
|
|
272
|
+
writable: false
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** @internal */
|
|
280
|
+
_currentValues() {
|
|
281
|
+
return this._client._getCachedConfig(this._key) ?? {};
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/helpers.ts
|
|
286
|
+
function keyToDisplayName(key) {
|
|
287
|
+
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
288
|
+
}
|
|
289
|
+
|
|
298
290
|
// src/config/client.ts
|
|
299
291
|
var BASE_URL = "https://config.smplkit.com";
|
|
300
292
|
function extractItemValues(items) {
|
|
@@ -324,7 +316,7 @@ function extractEnvironments(environments) {
|
|
|
324
316
|
function resourceToConfig(resource, client) {
|
|
325
317
|
const attrs = resource.attributes;
|
|
326
318
|
return new Config(client, {
|
|
327
|
-
id: resource.id ??
|
|
319
|
+
id: resource.id ?? null,
|
|
328
320
|
key: attrs.key ?? "",
|
|
329
321
|
name: attrs.name,
|
|
330
322
|
description: attrs.description ?? null,
|
|
@@ -333,8 +325,8 @@ function resourceToConfig(resource, client) {
|
|
|
333
325
|
environments: extractEnvironments(
|
|
334
326
|
attrs.environments
|
|
335
327
|
),
|
|
336
|
-
createdAt: attrs.created_at
|
|
337
|
-
updatedAt: attrs.updated_at
|
|
328
|
+
createdAt: attrs.created_at ?? null,
|
|
329
|
+
updatedAt: attrs.updated_at ?? null
|
|
338
330
|
});
|
|
339
331
|
}
|
|
340
332
|
async function checkError(response, _context) {
|
|
@@ -401,7 +393,7 @@ function buildRequestBody(options) {
|
|
|
401
393
|
};
|
|
402
394
|
}
|
|
403
395
|
var ConfigClient = class {
|
|
404
|
-
/** @internal
|
|
396
|
+
/** @internal */
|
|
405
397
|
_apiKey;
|
|
406
398
|
/** @internal */
|
|
407
399
|
_baseUrl = BASE_URL;
|
|
@@ -412,7 +404,7 @@ var ConfigClient = class {
|
|
|
412
404
|
/** @internal — set by SmplClient after construction. */
|
|
413
405
|
_parent = null;
|
|
414
406
|
_configCache = {};
|
|
415
|
-
|
|
407
|
+
_initialized = false;
|
|
416
408
|
_listeners = [];
|
|
417
409
|
/** @internal */
|
|
418
410
|
constructor(apiKey, timeout) {
|
|
@@ -424,7 +416,6 @@ var ConfigClient = class {
|
|
|
424
416
|
Authorization: `Bearer ${apiKey}`,
|
|
425
417
|
Accept: "application/json"
|
|
426
418
|
},
|
|
427
|
-
// openapi-fetch custom fetch receives a pre-built Request object
|
|
428
419
|
fetch: async (request) => {
|
|
429
420
|
const controller = new AbortController();
|
|
430
421
|
const timer = setTimeout(() => controller.abort(), ms);
|
|
@@ -441,23 +432,31 @@ var ConfigClient = class {
|
|
|
441
432
|
}
|
|
442
433
|
});
|
|
443
434
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
435
|
+
// ------------------------------------------------------------------
|
|
436
|
+
// Management: factory method
|
|
437
|
+
// ------------------------------------------------------------------
|
|
438
|
+
/** Create an unsaved config. Call `.save()` to persist. */
|
|
439
|
+
new(key, options) {
|
|
440
|
+
return new Config(this, {
|
|
441
|
+
id: null,
|
|
442
|
+
key,
|
|
443
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
444
|
+
description: options?.description ?? null,
|
|
445
|
+
parent: options?.parent ?? null,
|
|
446
|
+
items: {},
|
|
447
|
+
environments: {},
|
|
448
|
+
createdAt: null,
|
|
449
|
+
updatedAt: null
|
|
450
|
+
});
|
|
457
451
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
452
|
+
// ------------------------------------------------------------------
|
|
453
|
+
// Management: CRUD
|
|
454
|
+
// ------------------------------------------------------------------
|
|
455
|
+
/** Fetch a config by key. */
|
|
456
|
+
async get(key) {
|
|
457
|
+
return this._getByKey(key);
|
|
458
|
+
}
|
|
459
|
+
/** List all configs. */
|
|
461
460
|
async list() {
|
|
462
461
|
let data;
|
|
463
462
|
try {
|
|
@@ -470,18 +469,31 @@ var ConfigClient = class {
|
|
|
470
469
|
if (!data) return [];
|
|
471
470
|
return data.data.map((r) => resourceToConfig(r, this));
|
|
472
471
|
}
|
|
473
|
-
/**
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
472
|
+
/** Delete a config by key. */
|
|
473
|
+
async delete(key) {
|
|
474
|
+
const config = await this.get(key);
|
|
475
|
+
try {
|
|
476
|
+
const result = await this._http.DELETE("/api/v1/configs/{id}", {
|
|
477
|
+
params: { path: { id: config.id } }
|
|
478
|
+
});
|
|
479
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
480
|
+
await checkError(result.response, `Failed to delete config '${key}'`);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
wrapFetchError(err);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// ------------------------------------------------------------------
|
|
486
|
+
// Management: internal save methods (called by Config.save())
|
|
487
|
+
// ------------------------------------------------------------------
|
|
488
|
+
/** @internal — POST a new config. */
|
|
489
|
+
async _createConfig(config) {
|
|
479
490
|
const body = buildRequestBody({
|
|
480
|
-
name:
|
|
481
|
-
key:
|
|
482
|
-
description:
|
|
483
|
-
parent:
|
|
484
|
-
items:
|
|
491
|
+
name: config.name,
|
|
492
|
+
key: config.key,
|
|
493
|
+
description: config.description,
|
|
494
|
+
parent: config.parent,
|
|
495
|
+
items: config.items,
|
|
496
|
+
environments: config.environments
|
|
485
497
|
});
|
|
486
498
|
let data;
|
|
487
499
|
try {
|
|
@@ -494,98 +506,124 @@ var ConfigClient = class {
|
|
|
494
506
|
if (!data || !data.data) throw new SmplValidationError("Failed to create config");
|
|
495
507
|
return resourceToConfig(data.data, this);
|
|
496
508
|
}
|
|
497
|
-
/**
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
509
|
+
/** @internal — PUT a config update. */
|
|
510
|
+
async _updateConfig(config) {
|
|
511
|
+
const body = buildRequestBody({
|
|
512
|
+
id: config.id,
|
|
513
|
+
name: config.name,
|
|
514
|
+
key: config.key,
|
|
515
|
+
description: config.description,
|
|
516
|
+
parent: config.parent,
|
|
517
|
+
items: config.items,
|
|
518
|
+
environments: config.environments
|
|
519
|
+
});
|
|
520
|
+
let data;
|
|
504
521
|
try {
|
|
505
|
-
const result = await this._http.
|
|
506
|
-
params: { path: { id:
|
|
522
|
+
const result = await this._http.PUT("/api/v1/configs/{id}", {
|
|
523
|
+
params: { path: { id: config.id } },
|
|
524
|
+
body
|
|
507
525
|
});
|
|
508
|
-
if (result.error !== void 0
|
|
509
|
-
await checkError(result.response, `Failed to
|
|
526
|
+
if (result.error !== void 0)
|
|
527
|
+
await checkError(result.response, `Failed to update config ${config.id}`);
|
|
528
|
+
data = result.data;
|
|
510
529
|
} catch (err) {
|
|
511
530
|
wrapFetchError(err);
|
|
512
531
|
}
|
|
532
|
+
if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
|
|
533
|
+
return resourceToConfig(data.data, this);
|
|
513
534
|
}
|
|
514
|
-
/**
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
535
|
+
/** @internal — fetch a config by UUID. */
|
|
536
|
+
async _getById(configId) {
|
|
537
|
+
let data;
|
|
538
|
+
try {
|
|
539
|
+
const result = await this._http.GET("/api/v1/configs/{id}", {
|
|
540
|
+
params: { path: { id: configId } }
|
|
541
|
+
});
|
|
542
|
+
if (result.error !== void 0)
|
|
543
|
+
await checkError(result.response, `Config ${configId} not found`);
|
|
544
|
+
data = result.data;
|
|
545
|
+
} catch (err) {
|
|
546
|
+
wrapFetchError(err);
|
|
524
547
|
}
|
|
525
|
-
|
|
526
|
-
|
|
548
|
+
if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
|
|
549
|
+
return resourceToConfig(data.data, this);
|
|
527
550
|
}
|
|
551
|
+
// ------------------------------------------------------------------
|
|
552
|
+
// Runtime: resolve and subscribe
|
|
553
|
+
// ------------------------------------------------------------------
|
|
528
554
|
/**
|
|
529
|
-
*
|
|
555
|
+
* Resolve a config's values for the current environment.
|
|
530
556
|
*
|
|
531
|
-
*
|
|
557
|
+
* Returns a flat dict of resolved key-value pairs, walking the
|
|
558
|
+
* parent chain and applying environment overrides.
|
|
532
559
|
*
|
|
533
|
-
*
|
|
534
|
-
* @param itemKey - Optional specific item key. If omitted, returns all values.
|
|
535
|
-
* @param defaultValue - Default value if the key is missing.
|
|
536
|
-
*
|
|
537
|
-
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
560
|
+
* Optionally pass a model class to map the resolved values.
|
|
538
561
|
*/
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (resolved === void 0) {
|
|
545
|
-
return defaultValue ?? null;
|
|
562
|
+
async resolve(key, model) {
|
|
563
|
+
await this._ensureInitialized();
|
|
564
|
+
const values = this._configCache[key];
|
|
565
|
+
if (values === void 0) {
|
|
566
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
|
|
546
567
|
}
|
|
547
|
-
if (
|
|
548
|
-
return
|
|
568
|
+
if (model) {
|
|
569
|
+
return new model(values);
|
|
549
570
|
}
|
|
550
|
-
return
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Return a config value as a string, or `defaultValue` if absent or not a string.
|
|
554
|
-
*
|
|
555
|
-
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
556
|
-
*/
|
|
557
|
-
getString(configKey, itemKey, defaultValue = null) {
|
|
558
|
-
const value = this.getValue(configKey, itemKey);
|
|
559
|
-
return typeof value === "string" ? value : defaultValue;
|
|
571
|
+
return values;
|
|
560
572
|
}
|
|
561
573
|
/**
|
|
562
|
-
*
|
|
574
|
+
* Subscribe to a config's values — returns a live proxy that
|
|
575
|
+
* auto-updates when the underlying config changes.
|
|
563
576
|
*
|
|
564
|
-
*
|
|
577
|
+
* Optionally pass a model class to map the resolved values.
|
|
565
578
|
*/
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
579
|
+
async subscribe(key, model) {
|
|
580
|
+
await this._ensureInitialized();
|
|
581
|
+
if (!(key in this._configCache)) {
|
|
582
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
|
|
583
|
+
}
|
|
584
|
+
return new LiveConfigProxy(this, key, model);
|
|
569
585
|
}
|
|
586
|
+
// ------------------------------------------------------------------
|
|
587
|
+
// Runtime: change listeners (3-level overloads)
|
|
588
|
+
// ------------------------------------------------------------------
|
|
570
589
|
/**
|
|
571
|
-
*
|
|
590
|
+
* Register a change listener.
|
|
572
591
|
*
|
|
573
|
-
*
|
|
592
|
+
* - `onChange(callback)` — fires for any config change (global).
|
|
593
|
+
* - `onChange(configKey, callback)` — fires for changes to a specific config.
|
|
594
|
+
* - `onChange(configKey, itemKey, callback)` — fires for a specific item.
|
|
574
595
|
*/
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
596
|
+
onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
|
|
597
|
+
if (typeof callbackOrConfigKey === "function") {
|
|
598
|
+
this._listeners.push({
|
|
599
|
+
callback: callbackOrConfigKey,
|
|
600
|
+
configKey: null,
|
|
601
|
+
itemKey: null
|
|
602
|
+
});
|
|
603
|
+
} else if (typeof callbackOrItemKey === "function") {
|
|
604
|
+
this._listeners.push({
|
|
605
|
+
callback: callbackOrItemKey,
|
|
606
|
+
configKey: callbackOrConfigKey,
|
|
607
|
+
itemKey: null
|
|
608
|
+
});
|
|
609
|
+
} else if (typeof callbackOrItemKey === "string" && callback) {
|
|
610
|
+
this._listeners.push({
|
|
611
|
+
callback,
|
|
612
|
+
configKey: callbackOrConfigKey,
|
|
613
|
+
itemKey: callbackOrItemKey
|
|
614
|
+
});
|
|
615
|
+
}
|
|
578
616
|
}
|
|
617
|
+
// ------------------------------------------------------------------
|
|
618
|
+
// Runtime: refresh
|
|
619
|
+
// ------------------------------------------------------------------
|
|
579
620
|
/**
|
|
580
621
|
* Re-fetch all configs, re-resolve values, and update the cache.
|
|
581
|
-
*
|
|
582
|
-
* Fires change listeners for any values that differ from the previous cache.
|
|
583
|
-
*
|
|
584
|
-
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
622
|
+
* Fires change listeners for any values that differ.
|
|
585
623
|
*/
|
|
586
624
|
async refresh() {
|
|
587
|
-
if (!this.
|
|
588
|
-
throw new
|
|
625
|
+
if (!this._initialized) {
|
|
626
|
+
throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
|
|
589
627
|
}
|
|
590
628
|
const environment = this._parent?._environment;
|
|
591
629
|
if (!environment) {
|
|
@@ -594,27 +632,62 @@ var ConfigClient = class {
|
|
|
594
632
|
const configs = await this.list();
|
|
595
633
|
const newCache = {};
|
|
596
634
|
for (const cfg of configs) {
|
|
597
|
-
const chain = await cfg._buildChain(
|
|
635
|
+
const chain = await cfg._buildChain();
|
|
598
636
|
newCache[cfg.key] = resolveChain(chain, environment);
|
|
599
637
|
}
|
|
600
638
|
const oldCache = this._configCache;
|
|
601
639
|
this._configCache = newCache;
|
|
602
640
|
this._diffAndFire(oldCache, newCache, "manual");
|
|
603
641
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
642
|
+
// ------------------------------------------------------------------
|
|
643
|
+
// Runtime: lazy initialization
|
|
644
|
+
// ------------------------------------------------------------------
|
|
645
|
+
/** @internal */
|
|
646
|
+
async _ensureInitialized() {
|
|
647
|
+
if (this._initialized) return;
|
|
648
|
+
const environment = this._parent?._environment;
|
|
649
|
+
if (!environment) {
|
|
650
|
+
throw new SmplError("No environment set. Ensure SmplClient is configured.");
|
|
651
|
+
}
|
|
652
|
+
const configs = await this.list();
|
|
653
|
+
const cache = {};
|
|
654
|
+
for (const cfg of configs) {
|
|
655
|
+
const chain = await cfg._buildChain();
|
|
656
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
657
|
+
}
|
|
658
|
+
this._configCache = cache;
|
|
659
|
+
this._initialized = true;
|
|
660
|
+
if (this._getSharedWs) {
|
|
661
|
+
const ws = this._getSharedWs();
|
|
662
|
+
ws.on("config_changed", this._handleConfigChanged);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/** @internal — called by SmplClient for backward compat. */
|
|
666
|
+
async _connectInternal(environment) {
|
|
667
|
+
if (this._initialized) return;
|
|
668
|
+
const configs = await this.list();
|
|
669
|
+
const cache = {};
|
|
670
|
+
for (const cfg of configs) {
|
|
671
|
+
const chain = await cfg._buildChain();
|
|
672
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
673
|
+
}
|
|
674
|
+
this._configCache = cache;
|
|
675
|
+
this._initialized = true;
|
|
617
676
|
}
|
|
677
|
+
/** @internal — get resolved config from cache. Used by LiveConfigProxy. */
|
|
678
|
+
_getCachedConfig(key) {
|
|
679
|
+
return this._configCache[key];
|
|
680
|
+
}
|
|
681
|
+
// ------------------------------------------------------------------
|
|
682
|
+
// Internal: WebSocket handler
|
|
683
|
+
// ------------------------------------------------------------------
|
|
684
|
+
_handleConfigChanged = (_data) => {
|
|
685
|
+
void this.refresh().catch(() => {
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
// ------------------------------------------------------------------
|
|
689
|
+
// Internal: change detection
|
|
690
|
+
// ------------------------------------------------------------------
|
|
618
691
|
/** @internal */
|
|
619
692
|
_diffAndFire(oldCache, newCache, source) {
|
|
620
693
|
const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
|
|
@@ -645,54 +718,9 @@ var ConfigClient = class {
|
|
|
645
718
|
}
|
|
646
719
|
}
|
|
647
720
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
* Called by {@link Config} instance methods.
|
|
652
|
-
* @internal
|
|
653
|
-
*/
|
|
654
|
-
async _updateConfig(payload) {
|
|
655
|
-
const body = buildRequestBody({
|
|
656
|
-
id: payload.configId,
|
|
657
|
-
name: payload.name,
|
|
658
|
-
key: payload.key,
|
|
659
|
-
description: payload.description,
|
|
660
|
-
parent: payload.parent,
|
|
661
|
-
items: payload.items,
|
|
662
|
-
environments: payload.environments
|
|
663
|
-
});
|
|
664
|
-
let data;
|
|
665
|
-
try {
|
|
666
|
-
const result = await this._http.PUT("/api/v1/configs/{id}", {
|
|
667
|
-
params: { path: { id: payload.configId } },
|
|
668
|
-
body
|
|
669
|
-
});
|
|
670
|
-
if (result.error !== void 0)
|
|
671
|
-
await checkError(result.response, `Failed to update config ${payload.configId}`);
|
|
672
|
-
data = result.data;
|
|
673
|
-
} catch (err) {
|
|
674
|
-
wrapFetchError(err);
|
|
675
|
-
}
|
|
676
|
-
if (!data || !data.data)
|
|
677
|
-
throw new SmplValidationError(`Failed to update config ${payload.configId}`);
|
|
678
|
-
return resourceToConfig(data.data, this);
|
|
679
|
-
}
|
|
680
|
-
// ---- Private helpers ----
|
|
681
|
-
async _getById(configId) {
|
|
682
|
-
let data;
|
|
683
|
-
try {
|
|
684
|
-
const result = await this._http.GET("/api/v1/configs/{id}", {
|
|
685
|
-
params: { path: { id: configId } }
|
|
686
|
-
});
|
|
687
|
-
if (result.error !== void 0)
|
|
688
|
-
await checkError(result.response, `Config ${configId} not found`);
|
|
689
|
-
data = result.data;
|
|
690
|
-
} catch (err) {
|
|
691
|
-
wrapFetchError(err);
|
|
692
|
-
}
|
|
693
|
-
if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
|
|
694
|
-
return resourceToConfig(data.data, this);
|
|
695
|
-
}
|
|
721
|
+
// ------------------------------------------------------------------
|
|
722
|
+
// Internal: fetch by key
|
|
723
|
+
// ------------------------------------------------------------------
|
|
696
724
|
async _getByKey(key) {
|
|
697
725
|
let data;
|
|
698
726
|
try {
|
|
@@ -717,7 +745,7 @@ import createClient2 from "openapi-fetch";
|
|
|
717
745
|
|
|
718
746
|
// src/flags/models.ts
|
|
719
747
|
var Flag = class {
|
|
720
|
-
/** UUID of the flag. */
|
|
748
|
+
/** UUID of the flag, or `null` if unsaved. */
|
|
721
749
|
id;
|
|
722
750
|
/** Unique key within the account. */
|
|
723
751
|
key;
|
|
@@ -754,37 +782,35 @@ var Flag = class {
|
|
|
754
782
|
this.updatedAt = fields.updatedAt;
|
|
755
783
|
}
|
|
756
784
|
/**
|
|
757
|
-
*
|
|
785
|
+
* Persist this flag to the server.
|
|
758
786
|
*
|
|
759
|
-
*
|
|
787
|
+
* POST if `id` is null (new flag), PUT if `id` is set (update).
|
|
788
|
+
* Updates this instance in-place with the server response.
|
|
760
789
|
*/
|
|
761
|
-
async
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
});
|
|
770
|
-
this._apply(updated);
|
|
790
|
+
async save() {
|
|
791
|
+
if (this.id === null) {
|
|
792
|
+
const created = await this._client._createFlag(this);
|
|
793
|
+
this._apply(created);
|
|
794
|
+
} else {
|
|
795
|
+
const updated = await this._client._updateFlag(this);
|
|
796
|
+
this._apply(updated);
|
|
797
|
+
}
|
|
771
798
|
}
|
|
772
799
|
/**
|
|
773
|
-
* Add a rule to a specific environment.
|
|
800
|
+
* Add a rule to a specific environment (sync local mutation).
|
|
774
801
|
*
|
|
775
802
|
* The built rule must include an `environment` key (set via
|
|
776
|
-
* `Rule(...).environment("env_key")`).
|
|
777
|
-
*
|
|
803
|
+
* `Rule(...).environment("env_key")`). No HTTP call is made.
|
|
804
|
+
*
|
|
805
|
+
* @returns `this` for chaining.
|
|
778
806
|
*/
|
|
779
|
-
|
|
807
|
+
addRule(builtRule) {
|
|
780
808
|
const envKey = builtRule.environment;
|
|
781
809
|
if (!envKey) {
|
|
782
810
|
throw new Error(
|
|
783
811
|
`Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
|
|
784
812
|
);
|
|
785
813
|
}
|
|
786
|
-
const current = await this._client.get(this.id);
|
|
787
|
-
this._apply(current);
|
|
788
814
|
const envs = { ...this.environments };
|
|
789
815
|
const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
|
|
790
816
|
const rules = [...envData.rules ?? []];
|
|
@@ -792,13 +818,43 @@ var Flag = class {
|
|
|
792
818
|
rules.push(ruleCopy);
|
|
793
819
|
envData.rules = rules;
|
|
794
820
|
envs[envKey] = envData;
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
environments: envs
|
|
798
|
-
});
|
|
799
|
-
this._apply(updated);
|
|
821
|
+
this.environments = envs;
|
|
822
|
+
return this;
|
|
800
823
|
}
|
|
801
|
-
/**
|
|
824
|
+
/** Enable or disable a flag in a specific environment (sync local mutation). */
|
|
825
|
+
setEnvironmentEnabled(envKey, enabled) {
|
|
826
|
+
const envs = { ...this.environments };
|
|
827
|
+
const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
|
|
828
|
+
envData.enabled = enabled;
|
|
829
|
+
envs[envKey] = envData;
|
|
830
|
+
this.environments = envs;
|
|
831
|
+
}
|
|
832
|
+
/** Set the default value for a specific environment (sync local mutation). */
|
|
833
|
+
setEnvironmentDefault(envKey, defaultValue) {
|
|
834
|
+
const envs = { ...this.environments };
|
|
835
|
+
const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
|
|
836
|
+
envData.default = defaultValue;
|
|
837
|
+
envs[envKey] = envData;
|
|
838
|
+
this.environments = envs;
|
|
839
|
+
}
|
|
840
|
+
/** Clear all rules for a specific environment (sync local mutation). */
|
|
841
|
+
clearRules(envKey) {
|
|
842
|
+
const envs = { ...this.environments };
|
|
843
|
+
const envData = envs[envKey];
|
|
844
|
+
if (envData) {
|
|
845
|
+
envs[envKey] = { ...envData, rules: [] };
|
|
846
|
+
this.environments = envs;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Evaluate the flag locally (sync, no HTTP).
|
|
851
|
+
*
|
|
852
|
+
* Requires `initialize()` to have been called.
|
|
853
|
+
*/
|
|
854
|
+
get(options) {
|
|
855
|
+
return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
856
|
+
}
|
|
857
|
+
/** @internal — copy all fields from another Flag instance. */
|
|
802
858
|
_apply(other) {
|
|
803
859
|
this.id = other.id;
|
|
804
860
|
this.key = other.key;
|
|
@@ -815,36 +871,53 @@ var Flag = class {
|
|
|
815
871
|
return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
|
|
816
872
|
}
|
|
817
873
|
};
|
|
818
|
-
var
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
/** Known attributes. */
|
|
826
|
-
attributes;
|
|
827
|
-
constructor(fields) {
|
|
828
|
-
this.id = fields.id;
|
|
829
|
-
this.key = fields.key;
|
|
830
|
-
this.name = fields.name;
|
|
831
|
-
this.attributes = fields.attributes;
|
|
874
|
+
var BooleanFlag = class extends Flag {
|
|
875
|
+
get(options) {
|
|
876
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
877
|
+
if (typeof value === "boolean") {
|
|
878
|
+
return value;
|
|
879
|
+
}
|
|
880
|
+
return this.default;
|
|
832
881
|
}
|
|
833
|
-
|
|
834
|
-
|
|
882
|
+
};
|
|
883
|
+
var StringFlag = class extends Flag {
|
|
884
|
+
get(options) {
|
|
885
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
886
|
+
if (typeof value === "string") {
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
return this.default;
|
|
835
890
|
}
|
|
836
891
|
};
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
892
|
+
var NumberFlag = class extends Flag {
|
|
893
|
+
get(options) {
|
|
894
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
895
|
+
if (typeof value === "number") {
|
|
896
|
+
return value;
|
|
897
|
+
}
|
|
898
|
+
return this.default;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
var JsonFlag = class extends Flag {
|
|
902
|
+
get(options) {
|
|
903
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
904
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
905
|
+
return value;
|
|
906
|
+
}
|
|
907
|
+
return this.default;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// src/flags/client.ts
|
|
912
|
+
import jsonLogic from "json-logic-js";
|
|
913
|
+
var FLAGS_BASE_URL = "https://flags.smplkit.com";
|
|
914
|
+
var APP_BASE_URL = "https://app.smplkit.com";
|
|
915
|
+
var CACHE_MAX_SIZE = 1e4;
|
|
916
|
+
var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
|
|
917
|
+
var CONTEXT_BATCH_FLUSH_SIZE = 100;
|
|
918
|
+
async function checkError2(response, _context) {
|
|
919
|
+
const body = await response.text().catch(() => "");
|
|
920
|
+
throwForStatus(response.status, body);
|
|
848
921
|
}
|
|
849
922
|
function wrapFetchError2(err) {
|
|
850
923
|
if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
|
|
@@ -960,88 +1033,6 @@ var FlagStats = class {
|
|
|
960
1033
|
this.cacheMisses = cacheMisses;
|
|
961
1034
|
}
|
|
962
1035
|
};
|
|
963
|
-
var FlagHandleBase = class {
|
|
964
|
-
/** @internal */
|
|
965
|
-
_namespace;
|
|
966
|
-
/** @internal */
|
|
967
|
-
_key;
|
|
968
|
-
/** @internal */
|
|
969
|
-
_default;
|
|
970
|
-
/** @internal */
|
|
971
|
-
_listeners = [];
|
|
972
|
-
constructor(namespace, key, defaultValue) {
|
|
973
|
-
this._namespace = namespace;
|
|
974
|
-
this._key = key;
|
|
975
|
-
this._default = defaultValue;
|
|
976
|
-
}
|
|
977
|
-
get key() {
|
|
978
|
-
return this._key;
|
|
979
|
-
}
|
|
980
|
-
get default() {
|
|
981
|
-
return this._default;
|
|
982
|
-
}
|
|
983
|
-
/* v8 ignore next 3 — overridden by all exported subclasses */
|
|
984
|
-
get(options) {
|
|
985
|
-
return this._namespace._evaluateHandle(this._key, this._default, options?.context ?? null);
|
|
986
|
-
}
|
|
987
|
-
/** Register a flag-specific change listener. Works as a decorator. */
|
|
988
|
-
onChange(callback) {
|
|
989
|
-
this._listeners.push(callback);
|
|
990
|
-
return callback;
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
var BoolFlagHandle = class extends FlagHandleBase {
|
|
994
|
-
get(options) {
|
|
995
|
-
const value = this._namespace._evaluateHandle(
|
|
996
|
-
this._key,
|
|
997
|
-
this._default,
|
|
998
|
-
options?.context ?? null
|
|
999
|
-
);
|
|
1000
|
-
if (typeof value === "boolean") {
|
|
1001
|
-
return value;
|
|
1002
|
-
}
|
|
1003
|
-
return this._default;
|
|
1004
|
-
}
|
|
1005
|
-
};
|
|
1006
|
-
var StringFlagHandle = class extends FlagHandleBase {
|
|
1007
|
-
get(options) {
|
|
1008
|
-
const value = this._namespace._evaluateHandle(
|
|
1009
|
-
this._key,
|
|
1010
|
-
this._default,
|
|
1011
|
-
options?.context ?? null
|
|
1012
|
-
);
|
|
1013
|
-
if (typeof value === "string") {
|
|
1014
|
-
return value;
|
|
1015
|
-
}
|
|
1016
|
-
return this._default;
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
var NumberFlagHandle = class extends FlagHandleBase {
|
|
1020
|
-
get(options) {
|
|
1021
|
-
const value = this._namespace._evaluateHandle(
|
|
1022
|
-
this._key,
|
|
1023
|
-
this._default,
|
|
1024
|
-
options?.context ?? null
|
|
1025
|
-
);
|
|
1026
|
-
if (typeof value === "number") {
|
|
1027
|
-
return value;
|
|
1028
|
-
}
|
|
1029
|
-
return this._default;
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
var JsonFlagHandle = class extends FlagHandleBase {
|
|
1033
|
-
get(options) {
|
|
1034
|
-
const value = this._namespace._evaluateHandle(
|
|
1035
|
-
this._key,
|
|
1036
|
-
this._default,
|
|
1037
|
-
options?.context ?? null
|
|
1038
|
-
);
|
|
1039
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1040
|
-
return value;
|
|
1041
|
-
}
|
|
1042
|
-
return this._default;
|
|
1043
|
-
}
|
|
1044
|
-
};
|
|
1045
1036
|
var ContextRegistrationBuffer = class {
|
|
1046
1037
|
_seen = /* @__PURE__ */ new Map();
|
|
1047
1038
|
_pending = [];
|
|
@@ -1085,13 +1076,14 @@ var FlagsClient = class {
|
|
|
1085
1076
|
// Runtime state
|
|
1086
1077
|
_environment = null;
|
|
1087
1078
|
_flagStore = {};
|
|
1088
|
-
|
|
1079
|
+
_initialized = false;
|
|
1089
1080
|
_cache = new ResolutionCache();
|
|
1090
1081
|
_contextProvider = null;
|
|
1091
1082
|
_contextBuffer = new ContextRegistrationBuffer();
|
|
1092
1083
|
_handles = {};
|
|
1093
1084
|
_globalListeners = [];
|
|
1094
|
-
|
|
1085
|
+
_keyListeners = /* @__PURE__ */ new Map();
|
|
1086
|
+
// Shared WebSocket (set during initialize)
|
|
1095
1087
|
_wsManager = null;
|
|
1096
1088
|
_ensureWs;
|
|
1097
1089
|
/** @internal — set by SmplClient after construction. */
|
|
@@ -1133,55 +1125,91 @@ var FlagsClient = class {
|
|
|
1133
1125
|
});
|
|
1134
1126
|
}
|
|
1135
1127
|
// ------------------------------------------------------------------
|
|
1136
|
-
// Management methods
|
|
1128
|
+
// Management: factory methods (return unsaved flags)
|
|
1137
1129
|
// ------------------------------------------------------------------
|
|
1138
|
-
/** Create
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1130
|
+
/** Create an unsaved boolean flag. Call `.save()` to persist. */
|
|
1131
|
+
newBooleanFlag(key, options) {
|
|
1132
|
+
return new BooleanFlag(this, {
|
|
1133
|
+
id: null,
|
|
1134
|
+
key,
|
|
1135
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1136
|
+
type: "BOOLEAN",
|
|
1137
|
+
default: options.default,
|
|
1138
|
+
values: [
|
|
1143
1139
|
{ name: "True", value: true },
|
|
1144
1140
|
{ name: "False", value: false }
|
|
1145
|
-
]
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
}
|
|
1166
|
-
wrapFetchError2(err);
|
|
1167
|
-
}
|
|
1168
|
-
if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
|
|
1169
|
-
return this._resourceToModel(data.data);
|
|
1141
|
+
],
|
|
1142
|
+
description: options.description ?? null,
|
|
1143
|
+
environments: {},
|
|
1144
|
+
createdAt: null,
|
|
1145
|
+
updatedAt: null
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
/** Create an unsaved string flag. Call `.save()` to persist. */
|
|
1149
|
+
newStringFlag(key, options) {
|
|
1150
|
+
return new StringFlag(this, {
|
|
1151
|
+
id: null,
|
|
1152
|
+
key,
|
|
1153
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1154
|
+
type: "STRING",
|
|
1155
|
+
default: options.default,
|
|
1156
|
+
values: options.values ?? [],
|
|
1157
|
+
description: options.description ?? null,
|
|
1158
|
+
environments: {},
|
|
1159
|
+
createdAt: null,
|
|
1160
|
+
updatedAt: null
|
|
1161
|
+
});
|
|
1170
1162
|
}
|
|
1171
|
-
/**
|
|
1172
|
-
|
|
1163
|
+
/** Create an unsaved number flag. Call `.save()` to persist. */
|
|
1164
|
+
newNumberFlag(key, options) {
|
|
1165
|
+
return new NumberFlag(this, {
|
|
1166
|
+
id: null,
|
|
1167
|
+
key,
|
|
1168
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1169
|
+
type: "NUMERIC",
|
|
1170
|
+
default: options.default,
|
|
1171
|
+
values: options.values ?? [],
|
|
1172
|
+
description: options.description ?? null,
|
|
1173
|
+
environments: {},
|
|
1174
|
+
createdAt: null,
|
|
1175
|
+
updatedAt: null
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
/** Create an unsaved JSON flag. Call `.save()` to persist. */
|
|
1179
|
+
newJsonFlag(key, options) {
|
|
1180
|
+
return new JsonFlag(this, {
|
|
1181
|
+
id: null,
|
|
1182
|
+
key,
|
|
1183
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1184
|
+
type: "JSON",
|
|
1185
|
+
default: options.default,
|
|
1186
|
+
values: options.values ?? [],
|
|
1187
|
+
description: options.description ?? null,
|
|
1188
|
+
environments: {},
|
|
1189
|
+
createdAt: null,
|
|
1190
|
+
updatedAt: null
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
// ------------------------------------------------------------------
|
|
1194
|
+
// Management: CRUD
|
|
1195
|
+
// ------------------------------------------------------------------
|
|
1196
|
+
/** Fetch a flag by key. */
|
|
1197
|
+
async get(key) {
|
|
1173
1198
|
let data;
|
|
1174
1199
|
try {
|
|
1175
|
-
const result = await this._http.GET("/api/v1/flags
|
|
1176
|
-
params: {
|
|
1200
|
+
const result = await this._http.GET("/api/v1/flags", {
|
|
1201
|
+
params: { query: { "filter[key]": key } }
|
|
1177
1202
|
});
|
|
1178
|
-
if (result.error !== void 0)
|
|
1203
|
+
if (result.error !== void 0)
|
|
1204
|
+
await checkError2(result.response, `Flag with key '${key}' not found`);
|
|
1179
1205
|
data = result.data;
|
|
1180
1206
|
} catch (err) {
|
|
1181
1207
|
wrapFetchError2(err);
|
|
1182
1208
|
}
|
|
1183
|
-
if (!data || !data.data
|
|
1184
|
-
|
|
1209
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
1210
|
+
throw new SmplNotFoundError(`Flag with key '${key}' not found`);
|
|
1211
|
+
}
|
|
1212
|
+
return this._resourceToModel(data.data[0]);
|
|
1185
1213
|
}
|
|
1186
1214
|
/** List all flags. */
|
|
1187
1215
|
async list() {
|
|
@@ -1196,161 +1224,148 @@ var FlagsClient = class {
|
|
|
1196
1224
|
if (!data) return [];
|
|
1197
1225
|
return data.data.map((r) => this._resourceToModel(r));
|
|
1198
1226
|
}
|
|
1199
|
-
/** Delete a flag by
|
|
1200
|
-
async delete(
|
|
1227
|
+
/** Delete a flag by key. */
|
|
1228
|
+
async delete(key) {
|
|
1229
|
+
const flag = await this.get(key);
|
|
1201
1230
|
try {
|
|
1202
1231
|
const result = await this._http.DELETE("/api/v1/flags/{id}", {
|
|
1203
|
-
params: { path: { id:
|
|
1232
|
+
params: { path: { id: flag.id } }
|
|
1204
1233
|
});
|
|
1205
1234
|
if (result.error !== void 0 && result.response.status !== 204)
|
|
1206
|
-
await checkError2(result.response, `Failed to delete flag ${
|
|
1235
|
+
await checkError2(result.response, `Failed to delete flag '${key}'`);
|
|
1207
1236
|
} catch (err) {
|
|
1208
1237
|
wrapFetchError2(err);
|
|
1209
1238
|
}
|
|
1210
1239
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
async _updateFlag(options) {
|
|
1217
|
-
const { flag } = options;
|
|
1240
|
+
// ------------------------------------------------------------------
|
|
1241
|
+
// Management: internal save methods (called by Flag.save())
|
|
1242
|
+
// ------------------------------------------------------------------
|
|
1243
|
+
/** @internal — POST a new flag. */
|
|
1244
|
+
async _createFlag(flag) {
|
|
1218
1245
|
const body = {
|
|
1219
1246
|
data: {
|
|
1220
1247
|
type: "flag",
|
|
1221
1248
|
attributes: {
|
|
1222
1249
|
key: flag.key,
|
|
1223
|
-
name:
|
|
1250
|
+
name: flag.name,
|
|
1251
|
+
description: flag.description ?? "",
|
|
1224
1252
|
type: flag.type,
|
|
1225
|
-
default:
|
|
1226
|
-
values:
|
|
1227
|
-
|
|
1228
|
-
...options.environments !== void 0 ? { environments: options.environments } : flag.environments && Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1253
|
+
default: flag.default,
|
|
1254
|
+
values: flag.values,
|
|
1255
|
+
...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1229
1256
|
}
|
|
1230
1257
|
}
|
|
1231
1258
|
};
|
|
1232
1259
|
let data;
|
|
1233
1260
|
try {
|
|
1234
|
-
const result = await this._http.
|
|
1235
|
-
|
|
1236
|
-
body
|
|
1237
|
-
});
|
|
1238
|
-
if (result.error !== void 0)
|
|
1239
|
-
await checkError2(result.response, `Failed to update flag ${flag.id}`);
|
|
1261
|
+
const result = await this._http.POST("/api/v1/flags", { body });
|
|
1262
|
+
if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
|
|
1240
1263
|
data = result.data;
|
|
1241
1264
|
} catch (err) {
|
|
1242
1265
|
wrapFetchError2(err);
|
|
1243
1266
|
}
|
|
1244
|
-
if (!data || !data.data) throw new SmplValidationError(
|
|
1267
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
|
|
1245
1268
|
return this._resourceToModel(data.data);
|
|
1246
1269
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1270
|
+
/** @internal — PUT a flag update. */
|
|
1271
|
+
async _updateFlag(flag) {
|
|
1272
|
+
const body = {
|
|
1273
|
+
data: {
|
|
1274
|
+
type: "flag",
|
|
1275
|
+
attributes: {
|
|
1276
|
+
key: flag.key,
|
|
1277
|
+
name: flag.name,
|
|
1278
|
+
type: flag.type,
|
|
1279
|
+
default: flag.default,
|
|
1280
|
+
values: flag.values,
|
|
1281
|
+
description: flag.description ?? "",
|
|
1282
|
+
...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1257
1283
|
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
await checkError2(result.response, "Failed to create context type");
|
|
1261
|
-
data = result.data;
|
|
1262
|
-
} catch (err) {
|
|
1263
|
-
wrapFetchError2(err);
|
|
1264
|
-
}
|
|
1265
|
-
if (!data || !data.data) throw new SmplValidationError("Failed to create context type");
|
|
1266
|
-
return this._parseContextType(data.data);
|
|
1267
|
-
}
|
|
1268
|
-
/** Update a context type (merge attributes). */
|
|
1269
|
-
async updateContextType(ctId, options) {
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1270
1286
|
let data;
|
|
1271
1287
|
try {
|
|
1272
|
-
const result = await this.
|
|
1273
|
-
params: { path: { id:
|
|
1274
|
-
body
|
|
1275
|
-
data: {
|
|
1276
|
-
type: "context_type",
|
|
1277
|
-
attributes: { key: options.key, name: options.name, attributes: options.attributes }
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1288
|
+
const result = await this._http.PUT("/api/v1/flags/{id}", {
|
|
1289
|
+
params: { path: { id: flag.id } },
|
|
1290
|
+
body
|
|
1280
1291
|
});
|
|
1281
1292
|
if (result.error !== void 0)
|
|
1282
|
-
await checkError2(result.response, `Failed to update
|
|
1283
|
-
data = result.data;
|
|
1284
|
-
} catch (err) {
|
|
1285
|
-
wrapFetchError2(err);
|
|
1286
|
-
}
|
|
1287
|
-
if (!data || !data.data) throw new SmplValidationError(`Failed to update context type ${ctId}`);
|
|
1288
|
-
return this._parseContextType(data.data);
|
|
1289
|
-
}
|
|
1290
|
-
/** List all context types. */
|
|
1291
|
-
async listContextTypes() {
|
|
1292
|
-
let data;
|
|
1293
|
-
try {
|
|
1294
|
-
const result = await this._appHttp.GET("/api/v1/context_types");
|
|
1295
|
-
if (result.error !== void 0)
|
|
1296
|
-
await checkError2(result.response, "Failed to list context types");
|
|
1297
|
-
data = result.data;
|
|
1298
|
-
} catch (err) {
|
|
1299
|
-
wrapFetchError2(err);
|
|
1300
|
-
}
|
|
1301
|
-
if (!data || !data.data) throw new SmplValidationError("Failed to list context types");
|
|
1302
|
-
return data.data.map((item) => this._parseContextType(item));
|
|
1303
|
-
}
|
|
1304
|
-
/** Delete a context type. */
|
|
1305
|
-
async deleteContextType(ctId) {
|
|
1306
|
-
try {
|
|
1307
|
-
const result = await this._appHttp.DELETE("/api/v1/context_types/{id}", {
|
|
1308
|
-
params: { path: { id: ctId } }
|
|
1309
|
-
});
|
|
1310
|
-
if (result.error !== void 0 && result.response.status !== 204)
|
|
1311
|
-
await checkError2(result.response, `Failed to delete context type ${ctId}`);
|
|
1312
|
-
} catch (err) {
|
|
1313
|
-
wrapFetchError2(err);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
/** List context instances filtered by context type key. */
|
|
1317
|
-
async listContexts(options) {
|
|
1318
|
-
let data;
|
|
1319
|
-
try {
|
|
1320
|
-
const result = await this._appHttp.GET("/api/v1/contexts", {
|
|
1321
|
-
params: { query: { "filter[context_type_id]": options.contextTypeKey } }
|
|
1322
|
-
});
|
|
1323
|
-
if (result.error !== void 0) await checkError2(result.response, "Failed to list contexts");
|
|
1293
|
+
await checkError2(result.response, `Failed to update flag ${flag.id}`);
|
|
1324
1294
|
data = result.data;
|
|
1325
1295
|
} catch (err) {
|
|
1326
1296
|
wrapFetchError2(err);
|
|
1327
1297
|
}
|
|
1328
|
-
|
|
1298
|
+
if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
|
|
1299
|
+
return this._resourceToModel(data.data);
|
|
1329
1300
|
}
|
|
1330
1301
|
// ------------------------------------------------------------------
|
|
1331
1302
|
// Runtime: typed flag handles
|
|
1332
1303
|
// ------------------------------------------------------------------
|
|
1333
|
-
/** Declare a boolean flag handle. */
|
|
1334
|
-
|
|
1335
|
-
const handle = new
|
|
1304
|
+
/** Declare a boolean flag handle for runtime evaluation. */
|
|
1305
|
+
booleanFlag(key, defaultValue) {
|
|
1306
|
+
const handle = new BooleanFlag(this, {
|
|
1307
|
+
id: null,
|
|
1308
|
+
key,
|
|
1309
|
+
name: key,
|
|
1310
|
+
type: "BOOLEAN",
|
|
1311
|
+
default: defaultValue,
|
|
1312
|
+
values: [],
|
|
1313
|
+
description: null,
|
|
1314
|
+
environments: {},
|
|
1315
|
+
createdAt: null,
|
|
1316
|
+
updatedAt: null
|
|
1317
|
+
});
|
|
1336
1318
|
this._handles[key] = handle;
|
|
1337
1319
|
return handle;
|
|
1338
1320
|
}
|
|
1339
|
-
/** Declare a string flag handle. */
|
|
1321
|
+
/** Declare a string flag handle for runtime evaluation. */
|
|
1340
1322
|
stringFlag(key, defaultValue) {
|
|
1341
|
-
const handle = new
|
|
1323
|
+
const handle = new StringFlag(this, {
|
|
1324
|
+
id: null,
|
|
1325
|
+
key,
|
|
1326
|
+
name: key,
|
|
1327
|
+
type: "STRING",
|
|
1328
|
+
default: defaultValue,
|
|
1329
|
+
values: [],
|
|
1330
|
+
description: null,
|
|
1331
|
+
environments: {},
|
|
1332
|
+
createdAt: null,
|
|
1333
|
+
updatedAt: null
|
|
1334
|
+
});
|
|
1342
1335
|
this._handles[key] = handle;
|
|
1343
1336
|
return handle;
|
|
1344
1337
|
}
|
|
1345
|
-
/** Declare a numeric flag handle. */
|
|
1338
|
+
/** Declare a numeric flag handle for runtime evaluation. */
|
|
1346
1339
|
numberFlag(key, defaultValue) {
|
|
1347
|
-
const handle = new
|
|
1340
|
+
const handle = new NumberFlag(this, {
|
|
1341
|
+
id: null,
|
|
1342
|
+
key,
|
|
1343
|
+
name: key,
|
|
1344
|
+
type: "NUMERIC",
|
|
1345
|
+
default: defaultValue,
|
|
1346
|
+
values: [],
|
|
1347
|
+
description: null,
|
|
1348
|
+
environments: {},
|
|
1349
|
+
createdAt: null,
|
|
1350
|
+
updatedAt: null
|
|
1351
|
+
});
|
|
1348
1352
|
this._handles[key] = handle;
|
|
1349
1353
|
return handle;
|
|
1350
1354
|
}
|
|
1351
|
-
/** Declare a JSON flag handle. */
|
|
1355
|
+
/** Declare a JSON flag handle for runtime evaluation. */
|
|
1352
1356
|
jsonFlag(key, defaultValue) {
|
|
1353
|
-
const handle = new
|
|
1357
|
+
const handle = new JsonFlag(this, {
|
|
1358
|
+
id: null,
|
|
1359
|
+
key,
|
|
1360
|
+
name: key,
|
|
1361
|
+
type: "JSON",
|
|
1362
|
+
default: defaultValue,
|
|
1363
|
+
values: [],
|
|
1364
|
+
description: null,
|
|
1365
|
+
environments: {},
|
|
1366
|
+
createdAt: null,
|
|
1367
|
+
updatedAt: null
|
|
1368
|
+
});
|
|
1354
1369
|
this._handles[key] = handle;
|
|
1355
1370
|
return handle;
|
|
1356
1371
|
}
|
|
@@ -1360,41 +1375,32 @@ var FlagsClient = class {
|
|
|
1360
1375
|
/**
|
|
1361
1376
|
* Register a context provider function.
|
|
1362
1377
|
*
|
|
1363
|
-
* Called on every `handle.get()` to supply the current evaluation
|
|
1364
|
-
* context. Can also be used as a decorator:
|
|
1365
|
-
*
|
|
1366
|
-
* ```typescript
|
|
1367
|
-
* client.flags.setContextProvider(() => [
|
|
1368
|
-
* new Context("user", userId, { plan: userPlan }),
|
|
1369
|
-
* ]);
|
|
1370
|
-
* ```
|
|
1378
|
+
* Called on every `handle.get()` to supply the current evaluation context.
|
|
1371
1379
|
*/
|
|
1372
1380
|
setContextProvider(fn) {
|
|
1373
1381
|
this._contextProvider = fn;
|
|
1374
1382
|
}
|
|
1375
1383
|
/**
|
|
1376
1384
|
* Register a context provider — decorator-style alias.
|
|
1377
|
-
*
|
|
1378
|
-
* ```typescript
|
|
1379
|
-
* const provider = client.flags.contextProvider(() => [...]);
|
|
1380
|
-
* ```
|
|
1381
1385
|
*/
|
|
1382
1386
|
contextProvider(fn) {
|
|
1383
1387
|
this._contextProvider = fn;
|
|
1384
1388
|
return fn;
|
|
1385
1389
|
}
|
|
1386
1390
|
// ------------------------------------------------------------------
|
|
1387
|
-
// Runtime:
|
|
1391
|
+
// Runtime: initialize / disconnect / refresh
|
|
1388
1392
|
// ------------------------------------------------------------------
|
|
1389
1393
|
/**
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1394
|
+
* Initialize the flags runtime: fetch definitions and wire WebSocket.
|
|
1395
|
+
*
|
|
1396
|
+
* Idempotent — safe to call multiple times. Must be called (and awaited)
|
|
1397
|
+
* before using `.get()` on flag handles.
|
|
1393
1398
|
*/
|
|
1394
|
-
async
|
|
1395
|
-
this.
|
|
1399
|
+
async initialize() {
|
|
1400
|
+
if (this._initialized) return;
|
|
1401
|
+
this._environment = this._parent?._environment ?? null;
|
|
1396
1402
|
await this._fetchAllFlags();
|
|
1397
|
-
this.
|
|
1403
|
+
this._initialized = true;
|
|
1398
1404
|
this._cache.clear();
|
|
1399
1405
|
this._wsManager = this._ensureWs();
|
|
1400
1406
|
this._wsManager.on("flag_changed", this._handleFlagChanged);
|
|
@@ -1410,7 +1416,7 @@ var FlagsClient = class {
|
|
|
1410
1416
|
await this._flushContexts();
|
|
1411
1417
|
this._flagStore = {};
|
|
1412
1418
|
this._cache.clear();
|
|
1413
|
-
this.
|
|
1419
|
+
this._initialized = false;
|
|
1414
1420
|
this._environment = null;
|
|
1415
1421
|
}
|
|
1416
1422
|
/** Re-fetch all flag definitions and clear cache. */
|
|
@@ -1431,22 +1437,27 @@ var FlagsClient = class {
|
|
|
1431
1437
|
return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
|
|
1432
1438
|
}
|
|
1433
1439
|
// ------------------------------------------------------------------
|
|
1434
|
-
// Runtime: change listeners
|
|
1440
|
+
// Runtime: change listeners (dual-mode)
|
|
1435
1441
|
// ------------------------------------------------------------------
|
|
1436
|
-
/** Register a global change listener that fires for any flag change. */
|
|
1437
|
-
onChangeAny(callback) {
|
|
1438
|
-
this._globalListeners.push(callback);
|
|
1439
|
-
return callback;
|
|
1440
|
-
}
|
|
1441
1442
|
/**
|
|
1442
|
-
* Register a
|
|
1443
|
+
* Register a change listener.
|
|
1443
1444
|
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
* ```
|
|
1445
|
+
* - `onChange(callback)` — fires for any flag change (global).
|
|
1446
|
+
* - `onChange(key, callback)` — fires only for the specified flag key.
|
|
1447
1447
|
*/
|
|
1448
|
-
onChange(callback) {
|
|
1449
|
-
|
|
1448
|
+
onChange(callbackOrKey, callback) {
|
|
1449
|
+
if (typeof callbackOrKey === "function") {
|
|
1450
|
+
this._globalListeners.push(callbackOrKey);
|
|
1451
|
+
} else {
|
|
1452
|
+
const key = callbackOrKey;
|
|
1453
|
+
if (!callback) {
|
|
1454
|
+
throw new SmplError("onChange(key, callback) requires a callback function.");
|
|
1455
|
+
}
|
|
1456
|
+
if (!this._keyListeners.has(key)) {
|
|
1457
|
+
this._keyListeners.set(key, []);
|
|
1458
|
+
}
|
|
1459
|
+
this._keyListeners.get(key).push(callback);
|
|
1460
|
+
}
|
|
1450
1461
|
}
|
|
1451
1462
|
// ------------------------------------------------------------------
|
|
1452
1463
|
// Runtime: context registration
|
|
@@ -1455,7 +1466,7 @@ var FlagsClient = class {
|
|
|
1455
1466
|
* Explicitly register context(s) for background batch registration.
|
|
1456
1467
|
*
|
|
1457
1468
|
* Accepts a single Context or an array. Fire-and-forget — never
|
|
1458
|
-
* blocks. Works before `
|
|
1469
|
+
* blocks. Works before `initialize()` is called.
|
|
1459
1470
|
*/
|
|
1460
1471
|
register(context) {
|
|
1461
1472
|
if (Array.isArray(context)) {
|
|
@@ -1473,8 +1484,6 @@ var FlagsClient = class {
|
|
|
1473
1484
|
// ------------------------------------------------------------------
|
|
1474
1485
|
/**
|
|
1475
1486
|
* Tier 1 explicit evaluation — stateless, no provider or cache.
|
|
1476
|
-
*
|
|
1477
|
-
* Useful for scripts, one-off jobs, and infrastructure code.
|
|
1478
1487
|
*/
|
|
1479
1488
|
async evaluate(key, options) {
|
|
1480
1489
|
const evalDict = contextsToEvalDict(options.context);
|
|
@@ -1482,7 +1491,7 @@ var FlagsClient = class {
|
|
|
1482
1491
|
evalDict["service"] = { key: this._parent._service };
|
|
1483
1492
|
}
|
|
1484
1493
|
let flagDef = null;
|
|
1485
|
-
if (this.
|
|
1494
|
+
if (this._initialized && key in this._flagStore) {
|
|
1486
1495
|
flagDef = this._flagStore[key];
|
|
1487
1496
|
} else {
|
|
1488
1497
|
const flags = await this._fetchFlagsList();
|
|
@@ -1503,8 +1512,8 @@ var FlagsClient = class {
|
|
|
1503
1512
|
// ------------------------------------------------------------------
|
|
1504
1513
|
/** @internal */
|
|
1505
1514
|
_evaluateHandle(key, defaultValue, context) {
|
|
1506
|
-
if (!this.
|
|
1507
|
-
throw new
|
|
1515
|
+
if (!this._initialized) {
|
|
1516
|
+
throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
|
|
1508
1517
|
}
|
|
1509
1518
|
let evalDict;
|
|
1510
1519
|
if (context !== null) {
|
|
@@ -1541,6 +1550,19 @@ var FlagsClient = class {
|
|
|
1541
1550
|
return value;
|
|
1542
1551
|
}
|
|
1543
1552
|
// ------------------------------------------------------------------
|
|
1553
|
+
// Internal: _connectInternal (called by SmplClient for backward compat)
|
|
1554
|
+
// ------------------------------------------------------------------
|
|
1555
|
+
/** @internal — called by SmplClient constructor / lazy init. */
|
|
1556
|
+
async _connectInternal(environment) {
|
|
1557
|
+
this._environment = environment;
|
|
1558
|
+
await this._fetchAllFlags();
|
|
1559
|
+
this._initialized = true;
|
|
1560
|
+
this._cache.clear();
|
|
1561
|
+
this._wsManager = this._ensureWs();
|
|
1562
|
+
this._wsManager.on("flag_changed", this._handleFlagChanged);
|
|
1563
|
+
this._wsManager.on("flag_deleted", this._handleFlagDeleted);
|
|
1564
|
+
}
|
|
1565
|
+
// ------------------------------------------------------------------
|
|
1544
1566
|
// Internal: event handlers (called by SharedWebSocket)
|
|
1545
1567
|
// ------------------------------------------------------------------
|
|
1546
1568
|
_handleFlagChanged = (data) => {
|
|
@@ -1592,9 +1614,9 @@ var FlagsClient = class {
|
|
|
1592
1614
|
} catch {
|
|
1593
1615
|
}
|
|
1594
1616
|
}
|
|
1595
|
-
const
|
|
1596
|
-
if (
|
|
1597
|
-
for (const cb of
|
|
1617
|
+
const keyCallbacks = this._keyListeners.get(flagKey);
|
|
1618
|
+
if (keyCallbacks) {
|
|
1619
|
+
for (const cb of keyCallbacks) {
|
|
1598
1620
|
try {
|
|
1599
1621
|
cb(event);
|
|
1600
1622
|
} catch {
|
|
@@ -1630,10 +1652,11 @@ var FlagsClient = class {
|
|
|
1630
1652
|
// ------------------------------------------------------------------
|
|
1631
1653
|
// Internal: model conversion
|
|
1632
1654
|
// ------------------------------------------------------------------
|
|
1655
|
+
/** @internal */
|
|
1633
1656
|
_resourceToModel(resource) {
|
|
1634
1657
|
const attrs = resource.attributes;
|
|
1635
1658
|
return new Flag(this, {
|
|
1636
|
-
id: resource.id ??
|
|
1659
|
+
id: resource.id ?? null,
|
|
1637
1660
|
key: attrs.key,
|
|
1638
1661
|
name: attrs.name,
|
|
1639
1662
|
type: attrs.type,
|
|
@@ -1657,13 +1680,567 @@ var FlagsClient = class {
|
|
|
1657
1680
|
environments: attrs.environments ?? {}
|
|
1658
1681
|
};
|
|
1659
1682
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// src/logging/client.ts
|
|
1686
|
+
import createClient3 from "openapi-fetch";
|
|
1687
|
+
|
|
1688
|
+
// src/logging/models.ts
|
|
1689
|
+
var Logger = class {
|
|
1690
|
+
/** UUID of the logger, or `null` if unsaved. */
|
|
1691
|
+
id;
|
|
1692
|
+
/** Unique key (dot-separated hierarchy). */
|
|
1693
|
+
key;
|
|
1694
|
+
/** Human-readable display name. */
|
|
1695
|
+
name;
|
|
1696
|
+
/** Base log level, or null if inherited. */
|
|
1697
|
+
level;
|
|
1698
|
+
/** UUID of the parent log group, or null. */
|
|
1699
|
+
group;
|
|
1700
|
+
/** Whether this logger is managed by the platform. */
|
|
1701
|
+
managed;
|
|
1702
|
+
/** Observed sources (services that report this logger). */
|
|
1703
|
+
sources;
|
|
1704
|
+
/** Per-environment level overrides. */
|
|
1705
|
+
environments;
|
|
1706
|
+
/** When the logger was created. */
|
|
1707
|
+
createdAt;
|
|
1708
|
+
/** When the logger was last updated. */
|
|
1709
|
+
updatedAt;
|
|
1710
|
+
/** @internal */
|
|
1711
|
+
_client;
|
|
1712
|
+
/** @internal */
|
|
1713
|
+
constructor(client, fields) {
|
|
1714
|
+
this._client = client;
|
|
1715
|
+
this.id = fields.id;
|
|
1716
|
+
this.key = fields.key;
|
|
1717
|
+
this.name = fields.name;
|
|
1718
|
+
this.level = fields.level;
|
|
1719
|
+
this.group = fields.group;
|
|
1720
|
+
this.managed = fields.managed;
|
|
1721
|
+
this.sources = fields.sources;
|
|
1722
|
+
this.environments = fields.environments;
|
|
1723
|
+
this.createdAt = fields.createdAt;
|
|
1724
|
+
this.updatedAt = fields.updatedAt;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Persist this logger to the server.
|
|
1728
|
+
*
|
|
1729
|
+
* POST if `id` is null (new), PUT if `id` is set (update).
|
|
1730
|
+
*/
|
|
1731
|
+
async save() {
|
|
1732
|
+
const saved = await this._client._saveLogger(this);
|
|
1733
|
+
this._apply(saved);
|
|
1734
|
+
}
|
|
1735
|
+
/** Set the base log level (sync local mutation). */
|
|
1736
|
+
setLevel(level) {
|
|
1737
|
+
this.level = level;
|
|
1738
|
+
}
|
|
1739
|
+
/** Clear the base log level (sync local mutation). */
|
|
1740
|
+
clearLevel() {
|
|
1741
|
+
this.level = null;
|
|
1742
|
+
}
|
|
1743
|
+
/** Set an environment-specific log level (sync local mutation). */
|
|
1744
|
+
setEnvironmentLevel(env, level) {
|
|
1745
|
+
const envs = { ...this.environments };
|
|
1746
|
+
envs[env] = { ...envs[env] ?? {}, level };
|
|
1747
|
+
this.environments = envs;
|
|
1748
|
+
}
|
|
1749
|
+
/** Clear an environment-specific log level (sync local mutation). */
|
|
1750
|
+
clearEnvironmentLevel(env) {
|
|
1751
|
+
const envs = { ...this.environments };
|
|
1752
|
+
if (envs[env]) {
|
|
1753
|
+
const entry = { ...envs[env] };
|
|
1754
|
+
delete entry.level;
|
|
1755
|
+
envs[env] = entry;
|
|
1756
|
+
this.environments = envs;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
/** Clear all environment-specific log levels (sync local mutation). */
|
|
1760
|
+
clearAllEnvironmentLevels() {
|
|
1761
|
+
this.environments = {};
|
|
1762
|
+
}
|
|
1763
|
+
/** @internal — copy all fields from another Logger instance. */
|
|
1764
|
+
_apply(other) {
|
|
1765
|
+
this.id = other.id;
|
|
1766
|
+
this.key = other.key;
|
|
1767
|
+
this.name = other.name;
|
|
1768
|
+
this.level = other.level;
|
|
1769
|
+
this.group = other.group;
|
|
1770
|
+
this.managed = other.managed;
|
|
1771
|
+
this.sources = other.sources;
|
|
1772
|
+
this.environments = other.environments;
|
|
1773
|
+
this.createdAt = other.createdAt;
|
|
1774
|
+
this.updatedAt = other.updatedAt;
|
|
1775
|
+
}
|
|
1776
|
+
toString() {
|
|
1777
|
+
return `Logger(key=${this.key}, level=${this.level})`;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
var LogGroup = class {
|
|
1781
|
+
/** UUID of the log group, or `null` if unsaved. */
|
|
1782
|
+
id;
|
|
1783
|
+
/** Unique key. */
|
|
1784
|
+
key;
|
|
1785
|
+
/** Human-readable display name. */
|
|
1786
|
+
name;
|
|
1787
|
+
/** Base log level, or null if inherited. */
|
|
1788
|
+
level;
|
|
1789
|
+
/** UUID of the parent log group, or null. */
|
|
1790
|
+
group;
|
|
1791
|
+
/** Per-environment level overrides. */
|
|
1792
|
+
environments;
|
|
1793
|
+
/** When the log group was created. */
|
|
1794
|
+
createdAt;
|
|
1795
|
+
/** When the log group was last updated. */
|
|
1796
|
+
updatedAt;
|
|
1797
|
+
/** @internal */
|
|
1798
|
+
_client;
|
|
1799
|
+
/** @internal */
|
|
1800
|
+
constructor(client, fields) {
|
|
1801
|
+
this._client = client;
|
|
1802
|
+
this.id = fields.id;
|
|
1803
|
+
this.key = fields.key;
|
|
1804
|
+
this.name = fields.name;
|
|
1805
|
+
this.level = fields.level;
|
|
1806
|
+
this.group = fields.group;
|
|
1807
|
+
this.environments = fields.environments;
|
|
1808
|
+
this.createdAt = fields.createdAt;
|
|
1809
|
+
this.updatedAt = fields.updatedAt;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Persist this log group to the server.
|
|
1813
|
+
*
|
|
1814
|
+
* POST if `id` is null (new), PUT if `id` is set (update).
|
|
1815
|
+
*/
|
|
1816
|
+
async save() {
|
|
1817
|
+
const saved = await this._client._saveLogGroup(this);
|
|
1818
|
+
this._apply(saved);
|
|
1819
|
+
}
|
|
1820
|
+
/** Set the base log level (sync local mutation). */
|
|
1821
|
+
setLevel(level) {
|
|
1822
|
+
this.level = level;
|
|
1823
|
+
}
|
|
1824
|
+
/** Clear the base log level (sync local mutation). */
|
|
1825
|
+
clearLevel() {
|
|
1826
|
+
this.level = null;
|
|
1827
|
+
}
|
|
1828
|
+
/** Set an environment-specific log level (sync local mutation). */
|
|
1829
|
+
setEnvironmentLevel(env, level) {
|
|
1830
|
+
const envs = { ...this.environments };
|
|
1831
|
+
envs[env] = { ...envs[env] ?? {}, level };
|
|
1832
|
+
this.environments = envs;
|
|
1833
|
+
}
|
|
1834
|
+
/** Clear an environment-specific log level (sync local mutation). */
|
|
1835
|
+
clearEnvironmentLevel(env) {
|
|
1836
|
+
const envs = { ...this.environments };
|
|
1837
|
+
if (envs[env]) {
|
|
1838
|
+
const entry = { ...envs[env] };
|
|
1839
|
+
delete entry.level;
|
|
1840
|
+
envs[env] = entry;
|
|
1841
|
+
this.environments = envs;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
/** Clear all environment-specific log levels (sync local mutation). */
|
|
1845
|
+
clearAllEnvironmentLevels() {
|
|
1846
|
+
this.environments = {};
|
|
1847
|
+
}
|
|
1848
|
+
/** @internal — copy all fields from another LogGroup instance. */
|
|
1849
|
+
_apply(other) {
|
|
1850
|
+
this.id = other.id;
|
|
1851
|
+
this.key = other.key;
|
|
1852
|
+
this.name = other.name;
|
|
1853
|
+
this.level = other.level;
|
|
1854
|
+
this.group = other.group;
|
|
1855
|
+
this.environments = other.environments;
|
|
1856
|
+
this.createdAt = other.createdAt;
|
|
1857
|
+
this.updatedAt = other.updatedAt;
|
|
1858
|
+
}
|
|
1859
|
+
toString() {
|
|
1860
|
+
return `LogGroup(key=${this.key}, level=${this.level})`;
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
// src/logging/client.ts
|
|
1865
|
+
var LOGGING_BASE_URL = "https://logging.smplkit.com";
|
|
1866
|
+
async function checkError3(response, _context) {
|
|
1867
|
+
const body = await response.text().catch(() => "");
|
|
1868
|
+
throwForStatus(response.status, body);
|
|
1869
|
+
}
|
|
1870
|
+
function wrapFetchError3(err) {
|
|
1871
|
+
if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
|
|
1872
|
+
throw err;
|
|
1873
|
+
}
|
|
1874
|
+
if (err instanceof TypeError) {
|
|
1875
|
+
throw new SmplConnectionError(`Network error: ${err.message}`);
|
|
1876
|
+
}
|
|
1877
|
+
throw new SmplConnectionError(
|
|
1878
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
var LoggingClient = class {
|
|
1882
|
+
/** @internal */
|
|
1883
|
+
_apiKey;
|
|
1884
|
+
/** @internal */
|
|
1885
|
+
_baseUrl = LOGGING_BASE_URL;
|
|
1886
|
+
/** @internal */
|
|
1887
|
+
_http;
|
|
1888
|
+
/** @internal — set by SmplClient after construction. */
|
|
1889
|
+
_parent = null;
|
|
1890
|
+
_ensureWs;
|
|
1891
|
+
_wsManager = null;
|
|
1892
|
+
_started = false;
|
|
1893
|
+
_globalListeners = [];
|
|
1894
|
+
_keyListeners = /* @__PURE__ */ new Map();
|
|
1895
|
+
/** @internal */
|
|
1896
|
+
constructor(apiKey, ensureWs, timeout) {
|
|
1897
|
+
this._apiKey = apiKey;
|
|
1898
|
+
this._ensureWs = ensureWs;
|
|
1899
|
+
const ms = timeout ?? 3e4;
|
|
1900
|
+
this._http = createClient3({
|
|
1901
|
+
baseUrl: LOGGING_BASE_URL,
|
|
1902
|
+
headers: {
|
|
1903
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1904
|
+
Accept: "application/json"
|
|
1905
|
+
},
|
|
1906
|
+
fetch: async (request) => {
|
|
1907
|
+
const controller = new AbortController();
|
|
1908
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
1909
|
+
try {
|
|
1910
|
+
return await fetch(new Request(request, { signal: controller.signal }));
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1913
|
+
throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
|
|
1914
|
+
}
|
|
1915
|
+
throw err;
|
|
1916
|
+
} finally {
|
|
1917
|
+
clearTimeout(timer);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
// ------------------------------------------------------------------
|
|
1923
|
+
// Management: Logger factory
|
|
1924
|
+
// ------------------------------------------------------------------
|
|
1925
|
+
/** Create an unsaved logger. Call `.save()` to persist. */
|
|
1926
|
+
new(key, options) {
|
|
1927
|
+
return new Logger(this, {
|
|
1928
|
+
id: null,
|
|
1929
|
+
key,
|
|
1930
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
1931
|
+
level: null,
|
|
1932
|
+
group: null,
|
|
1933
|
+
managed: options?.managed ?? false,
|
|
1934
|
+
sources: [],
|
|
1935
|
+
environments: {},
|
|
1936
|
+
createdAt: null,
|
|
1937
|
+
updatedAt: null
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
// ------------------------------------------------------------------
|
|
1941
|
+
// Management: Logger CRUD
|
|
1942
|
+
// ------------------------------------------------------------------
|
|
1943
|
+
/** Fetch a logger by key. */
|
|
1944
|
+
async get(key) {
|
|
1945
|
+
let data;
|
|
1946
|
+
try {
|
|
1947
|
+
const result = await this._http.GET("/api/v1/loggers", {
|
|
1948
|
+
params: { query: { "filter[key]": key } }
|
|
1949
|
+
});
|
|
1950
|
+
if (result.error !== void 0)
|
|
1951
|
+
await checkError3(result.response, `Logger with key '${key}' not found`);
|
|
1952
|
+
data = result.data;
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
wrapFetchError3(err);
|
|
1955
|
+
}
|
|
1956
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
1957
|
+
throw new SmplNotFoundError(`Logger with key '${key}' not found`);
|
|
1958
|
+
}
|
|
1959
|
+
return this._loggerToModel(data.data[0]);
|
|
1960
|
+
}
|
|
1961
|
+
/** List all loggers. */
|
|
1962
|
+
async list() {
|
|
1963
|
+
let data;
|
|
1964
|
+
try {
|
|
1965
|
+
const result = await this._http.GET("/api/v1/loggers", {});
|
|
1966
|
+
if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
|
|
1967
|
+
data = result.data;
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
wrapFetchError3(err);
|
|
1970
|
+
}
|
|
1971
|
+
if (!data) return [];
|
|
1972
|
+
return data.data.map((r) => this._loggerToModel(r));
|
|
1973
|
+
}
|
|
1974
|
+
/** Delete a logger by key. */
|
|
1975
|
+
async delete(key) {
|
|
1976
|
+
const logger = await this.get(key);
|
|
1977
|
+
try {
|
|
1978
|
+
const result = await this._http.DELETE("/api/v1/loggers/{id}", {
|
|
1979
|
+
params: { path: { id: logger.id } }
|
|
1980
|
+
});
|
|
1981
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
1982
|
+
await checkError3(result.response, `Failed to delete logger '${key}'`);
|
|
1983
|
+
} catch (err) {
|
|
1984
|
+
wrapFetchError3(err);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
// ------------------------------------------------------------------
|
|
1988
|
+
// Management: LogGroup factory
|
|
1989
|
+
// ------------------------------------------------------------------
|
|
1990
|
+
/** Create an unsaved log group. Call `.save()` to persist. */
|
|
1991
|
+
newGroup(key, options) {
|
|
1992
|
+
return new LogGroup(this, {
|
|
1993
|
+
id: null,
|
|
1994
|
+
key,
|
|
1995
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
1996
|
+
level: null,
|
|
1997
|
+
group: options?.group ?? null,
|
|
1998
|
+
environments: {},
|
|
1999
|
+
createdAt: null,
|
|
2000
|
+
updatedAt: null
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
// ------------------------------------------------------------------
|
|
2004
|
+
// Management: LogGroup CRUD
|
|
2005
|
+
// ------------------------------------------------------------------
|
|
2006
|
+
/** Fetch a log group by key. */
|
|
2007
|
+
async getGroup(key) {
|
|
2008
|
+
const groups = await this.listGroups();
|
|
2009
|
+
const match = groups.find((g) => g.key === key);
|
|
2010
|
+
if (!match) {
|
|
2011
|
+
throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
|
|
2012
|
+
}
|
|
2013
|
+
return match;
|
|
2014
|
+
}
|
|
2015
|
+
/** List all log groups. */
|
|
2016
|
+
async listGroups() {
|
|
2017
|
+
let data;
|
|
2018
|
+
try {
|
|
2019
|
+
const result = await this._http.GET("/api/v1/log_groups", {});
|
|
2020
|
+
if (result.error !== void 0)
|
|
2021
|
+
await checkError3(result.response, "Failed to list log groups");
|
|
2022
|
+
data = result.data;
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
wrapFetchError3(err);
|
|
2025
|
+
}
|
|
2026
|
+
if (!data) return [];
|
|
2027
|
+
return data.data.map((r) => this._groupToModel(r));
|
|
2028
|
+
}
|
|
2029
|
+
/** Delete a log group by key. */
|
|
2030
|
+
async deleteGroup(key) {
|
|
2031
|
+
const group = await this.getGroup(key);
|
|
2032
|
+
try {
|
|
2033
|
+
const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
|
|
2034
|
+
params: { path: { id: group.id } }
|
|
2035
|
+
});
|
|
2036
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
2037
|
+
await checkError3(result.response, `Failed to delete log group '${key}'`);
|
|
2038
|
+
} catch (err) {
|
|
2039
|
+
wrapFetchError3(err);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
// ------------------------------------------------------------------
|
|
2043
|
+
// Management: internal save methods
|
|
2044
|
+
// ------------------------------------------------------------------
|
|
2045
|
+
/** @internal — POST or PUT a logger. */
|
|
2046
|
+
async _saveLogger(logger) {
|
|
2047
|
+
const body = {
|
|
2048
|
+
data: {
|
|
2049
|
+
type: "logger",
|
|
2050
|
+
attributes: {
|
|
2051
|
+
key: logger.key,
|
|
2052
|
+
name: logger.name,
|
|
2053
|
+
level: logger.level,
|
|
2054
|
+
group: logger.group,
|
|
2055
|
+
managed: logger.managed,
|
|
2056
|
+
environments: logger.environments
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
if (logger.id === null) {
|
|
2061
|
+
let data;
|
|
2062
|
+
try {
|
|
2063
|
+
const result = await this._http.POST("/api/v1/loggers", { body });
|
|
2064
|
+
if (result.error !== void 0)
|
|
2065
|
+
await checkError3(result.response, "Failed to create logger");
|
|
2066
|
+
data = result.data;
|
|
2067
|
+
} catch (err) {
|
|
2068
|
+
wrapFetchError3(err);
|
|
2069
|
+
}
|
|
2070
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
|
|
2071
|
+
return this._loggerToModel(data.data);
|
|
2072
|
+
} else {
|
|
2073
|
+
let data;
|
|
2074
|
+
try {
|
|
2075
|
+
const result = await this._http.PUT("/api/v1/loggers/{id}", {
|
|
2076
|
+
params: { path: { id: logger.id } },
|
|
2077
|
+
body
|
|
2078
|
+
});
|
|
2079
|
+
if (result.error !== void 0)
|
|
2080
|
+
await checkError3(result.response, `Failed to update logger ${logger.id}`);
|
|
2081
|
+
data = result.data;
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
wrapFetchError3(err);
|
|
2084
|
+
}
|
|
2085
|
+
if (!data || !data.data)
|
|
2086
|
+
throw new SmplValidationError(`Failed to update logger ${logger.id}`);
|
|
2087
|
+
return this._loggerToModel(data.data);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
/** @internal — POST or PUT a log group. */
|
|
2091
|
+
async _saveLogGroup(group) {
|
|
2092
|
+
const body = {
|
|
2093
|
+
data: {
|
|
2094
|
+
type: "log_group",
|
|
2095
|
+
attributes: {
|
|
2096
|
+
key: group.key,
|
|
2097
|
+
name: group.name,
|
|
2098
|
+
level: group.level,
|
|
2099
|
+
group: group.group,
|
|
2100
|
+
environments: group.environments
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
if (group.id === null) {
|
|
2105
|
+
let data;
|
|
2106
|
+
try {
|
|
2107
|
+
const result = await this._http.POST("/api/v1/log_groups", { body });
|
|
2108
|
+
if (result.error !== void 0)
|
|
2109
|
+
await checkError3(result.response, "Failed to create log group");
|
|
2110
|
+
data = result.data;
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
wrapFetchError3(err);
|
|
2113
|
+
}
|
|
2114
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
|
|
2115
|
+
return this._groupToModel(data.data);
|
|
2116
|
+
} else {
|
|
2117
|
+
let data;
|
|
2118
|
+
try {
|
|
2119
|
+
const result = await this._http.PUT("/api/v1/log_groups/{id}", {
|
|
2120
|
+
params: { path: { id: group.id } },
|
|
2121
|
+
body
|
|
2122
|
+
});
|
|
2123
|
+
if (result.error !== void 0)
|
|
2124
|
+
await checkError3(result.response, `Failed to update log group ${group.id}`);
|
|
2125
|
+
data = result.data;
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
wrapFetchError3(err);
|
|
2128
|
+
}
|
|
2129
|
+
if (!data || !data.data)
|
|
2130
|
+
throw new SmplValidationError(`Failed to update log group ${group.id}`);
|
|
2131
|
+
return this._groupToModel(data.data);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
// ------------------------------------------------------------------
|
|
2135
|
+
// Runtime: start (scaffolded)
|
|
2136
|
+
// ------------------------------------------------------------------
|
|
2137
|
+
/**
|
|
2138
|
+
* Start the logging runtime.
|
|
2139
|
+
*
|
|
2140
|
+
* Fetches existing loggers/groups and wires WebSocket listeners for
|
|
2141
|
+
* live updates. Idempotent — safe to call multiple times.
|
|
2142
|
+
*
|
|
2143
|
+
* Note: Node.js auto-discovery (equivalent to Python's logging module
|
|
2144
|
+
* monkey-patching) is deferred. Management methods work without start().
|
|
2145
|
+
*/
|
|
2146
|
+
async start() {
|
|
2147
|
+
if (this._started) return;
|
|
2148
|
+
this._wsManager = this._ensureWs();
|
|
2149
|
+
this._wsManager.on("logger_changed", this._handleLoggerChanged);
|
|
2150
|
+
this._started = true;
|
|
2151
|
+
}
|
|
2152
|
+
// ------------------------------------------------------------------
|
|
2153
|
+
// Runtime: change listeners (dual-mode)
|
|
2154
|
+
// ------------------------------------------------------------------
|
|
2155
|
+
/**
|
|
2156
|
+
* Register a change listener.
|
|
2157
|
+
*
|
|
2158
|
+
* - `onChange(callback)` — fires for any logger change (global).
|
|
2159
|
+
* - `onChange(key, callback)` — fires only for the specified logger key.
|
|
2160
|
+
*/
|
|
2161
|
+
onChange(callbackOrKey, callback) {
|
|
2162
|
+
if (typeof callbackOrKey === "function") {
|
|
2163
|
+
this._globalListeners.push(callbackOrKey);
|
|
2164
|
+
} else {
|
|
2165
|
+
const key = callbackOrKey;
|
|
2166
|
+
if (!callback) {
|
|
2167
|
+
throw new SmplError("onChange(key, callback) requires a callback function.");
|
|
2168
|
+
}
|
|
2169
|
+
if (!this._keyListeners.has(key)) {
|
|
2170
|
+
this._keyListeners.set(key, []);
|
|
2171
|
+
}
|
|
2172
|
+
this._keyListeners.get(key).push(callback);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
// ------------------------------------------------------------------
|
|
2176
|
+
// Internal: close
|
|
2177
|
+
// ------------------------------------------------------------------
|
|
2178
|
+
/** @internal */
|
|
2179
|
+
_close() {
|
|
2180
|
+
if (this._wsManager !== null) {
|
|
2181
|
+
this._wsManager.off("logger_changed", this._handleLoggerChanged);
|
|
2182
|
+
this._wsManager = null;
|
|
2183
|
+
}
|
|
2184
|
+
this._started = false;
|
|
2185
|
+
}
|
|
2186
|
+
// ------------------------------------------------------------------
|
|
2187
|
+
// Internal: WebSocket handler
|
|
2188
|
+
// ------------------------------------------------------------------
|
|
2189
|
+
_handleLoggerChanged = (data) => {
|
|
2190
|
+
const key = data.key;
|
|
2191
|
+
if (key) {
|
|
2192
|
+
const level = data.level ?? null;
|
|
2193
|
+
const event = {
|
|
2194
|
+
key,
|
|
2195
|
+
level,
|
|
2196
|
+
source: "websocket"
|
|
2197
|
+
};
|
|
2198
|
+
for (const cb of this._globalListeners) {
|
|
2199
|
+
try {
|
|
2200
|
+
cb(event);
|
|
2201
|
+
} catch {
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
const keyCallbacks = this._keyListeners.get(key);
|
|
2205
|
+
if (keyCallbacks) {
|
|
2206
|
+
for (const cb of keyCallbacks) {
|
|
2207
|
+
try {
|
|
2208
|
+
cb(event);
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
// ------------------------------------------------------------------
|
|
2216
|
+
// Internal: model conversion
|
|
2217
|
+
// ------------------------------------------------------------------
|
|
2218
|
+
_loggerToModel(resource) {
|
|
2219
|
+
const attrs = resource.attributes;
|
|
2220
|
+
return new Logger(this, {
|
|
2221
|
+
id: resource.id ?? null,
|
|
1664
2222
|
key: attrs.key ?? "",
|
|
1665
|
-
name: attrs.name
|
|
1666
|
-
|
|
2223
|
+
name: attrs.name,
|
|
2224
|
+
level: attrs.level ?? null,
|
|
2225
|
+
group: attrs.group ?? null,
|
|
2226
|
+
managed: attrs.managed ?? false,
|
|
2227
|
+
sources: attrs.sources ?? [],
|
|
2228
|
+
environments: attrs.environments ?? {},
|
|
2229
|
+
createdAt: attrs.created_at ?? null,
|
|
2230
|
+
updatedAt: attrs.updated_at ?? null
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
_groupToModel(resource) {
|
|
2234
|
+
const attrs = resource.attributes;
|
|
2235
|
+
return new LogGroup(this, {
|
|
2236
|
+
id: resource.id ?? null,
|
|
2237
|
+
key: attrs.key ?? "",
|
|
2238
|
+
name: attrs.name,
|
|
2239
|
+
level: attrs.level ?? null,
|
|
2240
|
+
group: attrs.group ?? null,
|
|
2241
|
+
environments: attrs.environments ?? {},
|
|
2242
|
+
createdAt: attrs.created_at ?? null,
|
|
2243
|
+
updatedAt: attrs.updated_at ?? null
|
|
1667
2244
|
});
|
|
1668
2245
|
}
|
|
1669
2246
|
};
|
|
@@ -1879,17 +2456,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
|
|
|
1879
2456
|
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";
|
|
1880
2457
|
var NO_SERVICE_MESSAGE = "No service provided. Set one of:\n 1. Pass service in options\n 2. Set the SMPLKIT_SERVICE environment variable";
|
|
1881
2458
|
var SmplClient = class {
|
|
1882
|
-
/** Client for config management
|
|
2459
|
+
/** Client for config management and runtime. */
|
|
1883
2460
|
config;
|
|
1884
|
-
/** Client for flags management and runtime
|
|
2461
|
+
/** Client for flags management and runtime. */
|
|
1885
2462
|
flags;
|
|
2463
|
+
/** Client for logging management and runtime. */
|
|
2464
|
+
logging;
|
|
1886
2465
|
_wsManager = null;
|
|
1887
2466
|
_apiKey;
|
|
1888
2467
|
/** @internal */
|
|
1889
2468
|
_environment;
|
|
1890
2469
|
/** @internal */
|
|
1891
2470
|
_service;
|
|
1892
|
-
_connected = false;
|
|
1893
2471
|
_timeout;
|
|
1894
2472
|
_appHttp;
|
|
1895
2473
|
constructor(options = {}) {
|
|
@@ -1906,48 +2484,21 @@ var SmplClient = class {
|
|
|
1906
2484
|
const apiKey = resolveApiKey(options.apiKey, environment);
|
|
1907
2485
|
this._apiKey = apiKey;
|
|
1908
2486
|
this._timeout = options.timeout ?? 3e4;
|
|
1909
|
-
|
|
1910
|
-
this._appHttp = createClient3({
|
|
2487
|
+
this._appHttp = createClient4({
|
|
1911
2488
|
baseUrl: APP_BASE_URL2,
|
|
1912
2489
|
headers: {
|
|
1913
2490
|
Authorization: `Bearer ${apiKey}`,
|
|
1914
2491
|
Accept: "application/json"
|
|
1915
|
-
},
|
|
1916
|
-
fetch: async (request) => {
|
|
1917
|
-
const controller = new AbortController();
|
|
1918
|
-
const timer = setTimeout(() => controller.abort(), ms);
|
|
1919
|
-
try {
|
|
1920
|
-
return await fetch(new Request(request, { signal: controller.signal }));
|
|
1921
|
-
} catch (err) {
|
|
1922
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1923
|
-
throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
|
|
1924
|
-
}
|
|
1925
|
-
throw err;
|
|
1926
|
-
} finally {
|
|
1927
|
-
clearTimeout(timer);
|
|
1928
|
-
}
|
|
1929
2492
|
}
|
|
1930
2493
|
});
|
|
1931
2494
|
this.config = new ConfigClient(apiKey, this._timeout);
|
|
1932
2495
|
this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
2496
|
+
this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
1933
2497
|
this.config._getSharedWs = () => this._ensureWs();
|
|
1934
2498
|
this.flags._parent = this;
|
|
1935
2499
|
this.config._parent = this;
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
* Connect to the smplkit platform.
|
|
1939
|
-
*
|
|
1940
|
-
* Fetches initial flag and config data, opens the shared WebSocket,
|
|
1941
|
-
* and registers the service as a context instance (if provided).
|
|
1942
|
-
*
|
|
1943
|
-
* This method is idempotent — calling it multiple times is safe.
|
|
1944
|
-
*/
|
|
1945
|
-
async connect() {
|
|
1946
|
-
if (this._connected) return;
|
|
1947
|
-
await this._registerServiceContext();
|
|
1948
|
-
await this.flags._connectInternal(this._environment);
|
|
1949
|
-
await this.config._connectInternal(this._environment);
|
|
1950
|
-
this._connected = true;
|
|
2500
|
+
this.logging._parent = this;
|
|
2501
|
+
void this._registerServiceContext();
|
|
1951
2502
|
}
|
|
1952
2503
|
/** @internal */
|
|
1953
2504
|
async _registerServiceContext() {
|
|
@@ -1976,6 +2527,7 @@ var SmplClient = class {
|
|
|
1976
2527
|
}
|
|
1977
2528
|
/** Close the shared WebSocket and release resources. */
|
|
1978
2529
|
close() {
|
|
2530
|
+
this.logging._close();
|
|
1979
2531
|
if (this._wsManager !== null) {
|
|
1980
2532
|
this._wsManager.stop();
|
|
1981
2533
|
this._wsManager = null;
|
|
@@ -2047,28 +2599,43 @@ var Rule = class {
|
|
|
2047
2599
|
return result;
|
|
2048
2600
|
}
|
|
2049
2601
|
};
|
|
2602
|
+
|
|
2603
|
+
// src/logging/types.ts
|
|
2604
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
2605
|
+
LogLevel2["TRACE"] = "TRACE";
|
|
2606
|
+
LogLevel2["DEBUG"] = "DEBUG";
|
|
2607
|
+
LogLevel2["INFO"] = "INFO";
|
|
2608
|
+
LogLevel2["WARN"] = "WARN";
|
|
2609
|
+
LogLevel2["ERROR"] = "ERROR";
|
|
2610
|
+
LogLevel2["FATAL"] = "FATAL";
|
|
2611
|
+
LogLevel2["SILENT"] = "SILENT";
|
|
2612
|
+
return LogLevel2;
|
|
2613
|
+
})(LogLevel || {});
|
|
2050
2614
|
export {
|
|
2051
|
-
|
|
2615
|
+
BooleanFlag,
|
|
2052
2616
|
Config,
|
|
2053
2617
|
ConfigClient,
|
|
2054
2618
|
Context,
|
|
2055
|
-
ContextType,
|
|
2056
2619
|
Flag,
|
|
2057
2620
|
FlagChangeEvent,
|
|
2058
2621
|
FlagStats,
|
|
2059
2622
|
FlagsClient,
|
|
2060
|
-
|
|
2061
|
-
|
|
2623
|
+
JsonFlag,
|
|
2624
|
+
LiveConfigProxy,
|
|
2625
|
+
LogGroup,
|
|
2626
|
+
LogLevel,
|
|
2627
|
+
Logger,
|
|
2628
|
+
LoggingClient,
|
|
2629
|
+
NumberFlag,
|
|
2062
2630
|
Rule,
|
|
2063
2631
|
SharedWebSocket,
|
|
2064
2632
|
SmplClient,
|
|
2065
2633
|
SmplConflictError,
|
|
2066
2634
|
SmplConnectionError,
|
|
2067
2635
|
SmplError,
|
|
2068
|
-
SmplNotConnectedError,
|
|
2069
2636
|
SmplNotFoundError,
|
|
2070
2637
|
SmplTimeoutError,
|
|
2071
2638
|
SmplValidationError,
|
|
2072
|
-
|
|
2639
|
+
StringFlag
|
|
2073
2640
|
};
|
|
2074
2641
|
//# sourceMappingURL=index.js.map
|