@smplkit/sdk 1.3.12 → 1.3.13
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 +1204 -613
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +728 -407
- package/dist/index.d.ts +728 -407
- package/dist/index.js +1195 -607
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,33 +30,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
33
|
+
BooleanFlag: () => BooleanFlag,
|
|
34
34
|
Config: () => Config,
|
|
35
35
|
ConfigClient: () => ConfigClient,
|
|
36
36
|
Context: () => Context,
|
|
37
|
-
ContextType: () => ContextType,
|
|
38
37
|
Flag: () => Flag,
|
|
39
38
|
FlagChangeEvent: () => FlagChangeEvent,
|
|
40
39
|
FlagStats: () => FlagStats,
|
|
41
40
|
FlagsClient: () => FlagsClient,
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
JsonFlag: () => JsonFlag,
|
|
42
|
+
LiveConfigProxy: () => LiveConfigProxy,
|
|
43
|
+
LogGroup: () => LogGroup,
|
|
44
|
+
LogLevel: () => LogLevel,
|
|
45
|
+
Logger: () => Logger,
|
|
46
|
+
LoggingClient: () => LoggingClient,
|
|
47
|
+
NumberFlag: () => NumberFlag,
|
|
44
48
|
Rule: () => Rule,
|
|
45
49
|
SharedWebSocket: () => SharedWebSocket,
|
|
46
50
|
SmplClient: () => SmplClient,
|
|
47
51
|
SmplConflictError: () => SmplConflictError,
|
|
48
52
|
SmplConnectionError: () => SmplConnectionError,
|
|
49
53
|
SmplError: () => SmplError,
|
|
50
|
-
SmplNotConnectedError: () => SmplNotConnectedError,
|
|
51
54
|
SmplNotFoundError: () => SmplNotFoundError,
|
|
52
55
|
SmplTimeoutError: () => SmplTimeoutError,
|
|
53
56
|
SmplValidationError: () => SmplValidationError,
|
|
54
|
-
|
|
57
|
+
StringFlag: () => StringFlag
|
|
55
58
|
});
|
|
56
59
|
module.exports = __toCommonJS(index_exports);
|
|
57
60
|
|
|
58
61
|
// src/client.ts
|
|
59
|
-
var
|
|
62
|
+
var import_openapi_fetch4 = __toESM(require("openapi-fetch"), 1);
|
|
60
63
|
|
|
61
64
|
// src/config/client.ts
|
|
62
65
|
var import_openapi_fetch = __toESM(require("openapi-fetch"), 1);
|
|
@@ -119,13 +122,6 @@ var SmplConflictError = class extends SmplError {
|
|
|
119
122
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
120
123
|
}
|
|
121
124
|
};
|
|
122
|
-
var SmplNotConnectedError = class extends SmplError {
|
|
123
|
-
constructor(message) {
|
|
124
|
-
super(message);
|
|
125
|
-
this.name = "SmplNotConnectedError";
|
|
126
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
125
|
var SmplValidationError = class extends SmplError {
|
|
130
126
|
constructor(message, statusCode, responseBody, errors) {
|
|
131
127
|
super(message, statusCode ?? 422, responseBody, errors);
|
|
@@ -205,9 +201,9 @@ function resolveChain(chain, environment) {
|
|
|
205
201
|
|
|
206
202
|
// src/config/types.ts
|
|
207
203
|
var Config = class {
|
|
208
|
-
/** UUID of the config. */
|
|
204
|
+
/** UUID of the config, or `null` if unsaved. */
|
|
209
205
|
id;
|
|
210
|
-
/** Human-readable key (e.g. `"
|
|
206
|
+
/** Human-readable key (e.g. `"user-service"`). */
|
|
211
207
|
key;
|
|
212
208
|
/** Display name. */
|
|
213
209
|
name;
|
|
@@ -227,10 +223,7 @@ var Config = class {
|
|
|
227
223
|
createdAt;
|
|
228
224
|
/** When the config was last updated, or null if unavailable. */
|
|
229
225
|
updatedAt;
|
|
230
|
-
/**
|
|
231
|
-
* Internal reference to the parent client.
|
|
232
|
-
* @internal
|
|
233
|
-
*/
|
|
226
|
+
/** @internal */
|
|
234
227
|
_client;
|
|
235
228
|
/** @internal */
|
|
236
229
|
constructor(client, fields) {
|
|
@@ -246,100 +239,31 @@ var Config = class {
|
|
|
246
239
|
this.updatedAt = fields.updatedAt;
|
|
247
240
|
}
|
|
248
241
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* Builds the request from current attribute values, overriding with any
|
|
252
|
-
* provided options. Updates local attributes in place on success.
|
|
253
|
-
*
|
|
254
|
-
* @param options.name - New display name.
|
|
255
|
-
* @param options.description - New description (pass empty string to clear).
|
|
256
|
-
* @param options.items - New base values (replaces entirely).
|
|
257
|
-
* @param options.environments - New environments dict (replaces entirely).
|
|
258
|
-
*/
|
|
259
|
-
async update(options) {
|
|
260
|
-
const updated = await this._client._updateConfig({
|
|
261
|
-
configId: this.id,
|
|
262
|
-
name: options.name ?? this.name,
|
|
263
|
-
key: this.key,
|
|
264
|
-
description: options.description !== void 0 ? options.description : this.description,
|
|
265
|
-
parent: this.parent,
|
|
266
|
-
items: options.items ?? this.items,
|
|
267
|
-
environments: options.environments ?? this.environments
|
|
268
|
-
});
|
|
269
|
-
this.name = updated.name;
|
|
270
|
-
this.description = updated.description;
|
|
271
|
-
this.items = updated.items;
|
|
272
|
-
this.environments = updated.environments;
|
|
273
|
-
this.updatedAt = updated.updatedAt;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Replace base or environment-specific values.
|
|
277
|
-
*
|
|
278
|
-
* When `environment` is provided, replaces that environment's `values`
|
|
279
|
-
* sub-dict (other environments are preserved). When omitted, replaces
|
|
280
|
-
* the base `items`.
|
|
281
|
-
*
|
|
282
|
-
* @param values - The complete set of values to set.
|
|
283
|
-
* @param environment - Target environment, or omit for base values.
|
|
284
|
-
*/
|
|
285
|
-
async setValues(values, environment) {
|
|
286
|
-
let newItems;
|
|
287
|
-
let newEnvs;
|
|
288
|
-
if (environment === void 0) {
|
|
289
|
-
newItems = values;
|
|
290
|
-
newEnvs = this.environments;
|
|
291
|
-
} else {
|
|
292
|
-
newItems = this.items;
|
|
293
|
-
const existingEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? { ...this.environments[environment] } : {};
|
|
294
|
-
existingEntry.values = values;
|
|
295
|
-
newEnvs = { ...this.environments, [environment]: existingEntry };
|
|
296
|
-
}
|
|
297
|
-
const updated = await this._client._updateConfig({
|
|
298
|
-
configId: this.id,
|
|
299
|
-
name: this.name,
|
|
300
|
-
key: this.key,
|
|
301
|
-
description: this.description,
|
|
302
|
-
parent: this.parent,
|
|
303
|
-
items: newItems,
|
|
304
|
-
environments: newEnvs
|
|
305
|
-
});
|
|
306
|
-
this.items = updated.items;
|
|
307
|
-
this.environments = updated.environments;
|
|
308
|
-
this.updatedAt = updated.updatedAt;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Set a single key within base or environment-specific values.
|
|
242
|
+
* Persist this config to the server.
|
|
312
243
|
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* @param key - The config key to set.
|
|
316
|
-
* @param value - The value to assign.
|
|
317
|
-
* @param environment - Target environment, or omit for base values.
|
|
244
|
+
* POST if `id` is null (new config), PUT if `id` is set (update).
|
|
245
|
+
* Updates this instance in-place with the server response.
|
|
318
246
|
*/
|
|
319
|
-
async
|
|
320
|
-
if (
|
|
321
|
-
const
|
|
322
|
-
|
|
247
|
+
async save() {
|
|
248
|
+
if (this.id === null) {
|
|
249
|
+
const created = await this._client._createConfig(this);
|
|
250
|
+
this._apply(created);
|
|
323
251
|
} else {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
...typeof envEntry.values === "object" && envEntry.values !== null ? envEntry.values : {}
|
|
327
|
-
};
|
|
328
|
-
existing[key] = value;
|
|
329
|
-
await this.setValues(existing, environment);
|
|
252
|
+
const updated = await this._client._updateConfig(this);
|
|
253
|
+
this._apply(updated);
|
|
330
254
|
}
|
|
331
255
|
}
|
|
332
256
|
/**
|
|
333
257
|
* Walk the parent chain and return config data objects, child-to-root.
|
|
334
258
|
* @internal
|
|
335
259
|
*/
|
|
336
|
-
async _buildChain(
|
|
337
|
-
const chain = [{ id: this.id, items: this.items, environments: this.environments }];
|
|
260
|
+
async _buildChain() {
|
|
261
|
+
const chain = [{ id: this.id ?? "", items: this.items, environments: this.environments }];
|
|
338
262
|
let parentId = this.parent;
|
|
339
263
|
while (parentId !== null) {
|
|
340
|
-
const parentConfig = await this._client.
|
|
264
|
+
const parentConfig = await this._client._getById(parentId);
|
|
341
265
|
chain.push({
|
|
342
|
-
id: parentConfig.id,
|
|
266
|
+
id: parentConfig.id ?? "",
|
|
343
267
|
items: parentConfig.items,
|
|
344
268
|
environments: parentConfig.environments
|
|
345
269
|
});
|
|
@@ -347,11 +271,82 @@ var Config = class {
|
|
|
347
271
|
}
|
|
348
272
|
return chain;
|
|
349
273
|
}
|
|
274
|
+
/** @internal — copy all fields from another Config instance. */
|
|
275
|
+
_apply(other) {
|
|
276
|
+
this.id = other.id;
|
|
277
|
+
this.key = other.key;
|
|
278
|
+
this.name = other.name;
|
|
279
|
+
this.description = other.description;
|
|
280
|
+
this.parent = other.parent;
|
|
281
|
+
this.items = other.items;
|
|
282
|
+
this.environments = other.environments;
|
|
283
|
+
this.createdAt = other.createdAt;
|
|
284
|
+
this.updatedAt = other.updatedAt;
|
|
285
|
+
}
|
|
350
286
|
toString() {
|
|
351
287
|
return `Config(id=${this.id}, key=${this.key}, name=${this.name})`;
|
|
352
288
|
}
|
|
353
289
|
};
|
|
354
290
|
|
|
291
|
+
// src/config/proxy.ts
|
|
292
|
+
var LiveConfigProxy = class {
|
|
293
|
+
/** @internal */
|
|
294
|
+
_client;
|
|
295
|
+
/** @internal */
|
|
296
|
+
_key;
|
|
297
|
+
/** @internal */
|
|
298
|
+
_model;
|
|
299
|
+
constructor(client, key, model) {
|
|
300
|
+
this._client = client;
|
|
301
|
+
this._key = key;
|
|
302
|
+
this._model = model;
|
|
303
|
+
return new Proxy(this, {
|
|
304
|
+
get(target, prop, receiver) {
|
|
305
|
+
if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
|
|
306
|
+
return Reflect.get(target, prop, receiver);
|
|
307
|
+
}
|
|
308
|
+
const values = target._currentValues();
|
|
309
|
+
if (target._model) {
|
|
310
|
+
const instance = new target._model(values);
|
|
311
|
+
return instance[prop];
|
|
312
|
+
}
|
|
313
|
+
return values[prop];
|
|
314
|
+
},
|
|
315
|
+
has(target, prop) {
|
|
316
|
+
if (typeof prop === "symbol") return Reflect.has(target, prop);
|
|
317
|
+
const values = target._currentValues();
|
|
318
|
+
return prop in values;
|
|
319
|
+
},
|
|
320
|
+
ownKeys(target) {
|
|
321
|
+
const values = target._currentValues();
|
|
322
|
+
return Object.keys(values);
|
|
323
|
+
},
|
|
324
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
325
|
+
if (typeof prop === "symbol") return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
326
|
+
const values = target._currentValues();
|
|
327
|
+
if (prop in values) {
|
|
328
|
+
return {
|
|
329
|
+
configurable: true,
|
|
330
|
+
enumerable: true,
|
|
331
|
+
value: values[prop],
|
|
332
|
+
writable: false
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** @internal */
|
|
340
|
+
_currentValues() {
|
|
341
|
+
return this._client._getCachedConfig(this._key) ?? {};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/helpers.ts
|
|
346
|
+
function keyToDisplayName(key) {
|
|
347
|
+
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
348
|
+
}
|
|
349
|
+
|
|
355
350
|
// src/config/client.ts
|
|
356
351
|
var BASE_URL = "https://config.smplkit.com";
|
|
357
352
|
function extractItemValues(items) {
|
|
@@ -381,7 +376,7 @@ function extractEnvironments(environments) {
|
|
|
381
376
|
function resourceToConfig(resource, client) {
|
|
382
377
|
const attrs = resource.attributes;
|
|
383
378
|
return new Config(client, {
|
|
384
|
-
id: resource.id ??
|
|
379
|
+
id: resource.id ?? null,
|
|
385
380
|
key: attrs.key ?? "",
|
|
386
381
|
name: attrs.name,
|
|
387
382
|
description: attrs.description ?? null,
|
|
@@ -390,8 +385,8 @@ function resourceToConfig(resource, client) {
|
|
|
390
385
|
environments: extractEnvironments(
|
|
391
386
|
attrs.environments
|
|
392
387
|
),
|
|
393
|
-
createdAt: attrs.created_at
|
|
394
|
-
updatedAt: attrs.updated_at
|
|
388
|
+
createdAt: attrs.created_at ?? null,
|
|
389
|
+
updatedAt: attrs.updated_at ?? null
|
|
395
390
|
});
|
|
396
391
|
}
|
|
397
392
|
async function checkError(response, _context) {
|
|
@@ -458,7 +453,7 @@ function buildRequestBody(options) {
|
|
|
458
453
|
};
|
|
459
454
|
}
|
|
460
455
|
var ConfigClient = class {
|
|
461
|
-
/** @internal
|
|
456
|
+
/** @internal */
|
|
462
457
|
_apiKey;
|
|
463
458
|
/** @internal */
|
|
464
459
|
_baseUrl = BASE_URL;
|
|
@@ -469,7 +464,9 @@ var ConfigClient = class {
|
|
|
469
464
|
/** @internal — set by SmplClient after construction. */
|
|
470
465
|
_parent = null;
|
|
471
466
|
_configCache = {};
|
|
472
|
-
|
|
467
|
+
/* v8 ignore next — bookkeeping for future use */
|
|
468
|
+
_configStore = [];
|
|
469
|
+
_initialized = false;
|
|
473
470
|
_listeners = [];
|
|
474
471
|
/** @internal */
|
|
475
472
|
constructor(apiKey, timeout) {
|
|
@@ -481,7 +478,6 @@ var ConfigClient = class {
|
|
|
481
478
|
Authorization: `Bearer ${apiKey}`,
|
|
482
479
|
Accept: "application/json"
|
|
483
480
|
},
|
|
484
|
-
// openapi-fetch custom fetch receives a pre-built Request object
|
|
485
481
|
fetch: async (request) => {
|
|
486
482
|
const controller = new AbortController();
|
|
487
483
|
const timer = setTimeout(() => controller.abort(), ms);
|
|
@@ -498,23 +494,31 @@ var ConfigClient = class {
|
|
|
498
494
|
}
|
|
499
495
|
});
|
|
500
496
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
497
|
+
// ------------------------------------------------------------------
|
|
498
|
+
// Management: factory method
|
|
499
|
+
// ------------------------------------------------------------------
|
|
500
|
+
/** Create an unsaved config. Call `.save()` to persist. */
|
|
501
|
+
new(key, options) {
|
|
502
|
+
return new Config(this, {
|
|
503
|
+
id: null,
|
|
504
|
+
key,
|
|
505
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
506
|
+
description: options?.description ?? null,
|
|
507
|
+
parent: options?.parent ?? null,
|
|
508
|
+
items: {},
|
|
509
|
+
environments: {},
|
|
510
|
+
createdAt: null,
|
|
511
|
+
updatedAt: null
|
|
512
|
+
});
|
|
514
513
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
514
|
+
// ------------------------------------------------------------------
|
|
515
|
+
// Management: CRUD
|
|
516
|
+
// ------------------------------------------------------------------
|
|
517
|
+
/** Fetch a config by key. */
|
|
518
|
+
async get(key) {
|
|
519
|
+
return this._getByKey(key);
|
|
520
|
+
}
|
|
521
|
+
/** List all configs. */
|
|
518
522
|
async list() {
|
|
519
523
|
let data;
|
|
520
524
|
try {
|
|
@@ -527,18 +531,31 @@ var ConfigClient = class {
|
|
|
527
531
|
if (!data) return [];
|
|
528
532
|
return data.data.map((r) => resourceToConfig(r, this));
|
|
529
533
|
}
|
|
530
|
-
/**
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
534
|
+
/** Delete a config by key. */
|
|
535
|
+
async delete(key) {
|
|
536
|
+
const config = await this.get(key);
|
|
537
|
+
try {
|
|
538
|
+
const result = await this._http.DELETE("/api/v1/configs/{id}", {
|
|
539
|
+
params: { path: { id: config.id } }
|
|
540
|
+
});
|
|
541
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
542
|
+
await checkError(result.response, `Failed to delete config '${key}'`);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
wrapFetchError(err);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// ------------------------------------------------------------------
|
|
548
|
+
// Management: internal save methods (called by Config.save())
|
|
549
|
+
// ------------------------------------------------------------------
|
|
550
|
+
/** @internal — POST a new config. */
|
|
551
|
+
async _createConfig(config) {
|
|
536
552
|
const body = buildRequestBody({
|
|
537
|
-
name:
|
|
538
|
-
key:
|
|
539
|
-
description:
|
|
540
|
-
parent:
|
|
541
|
-
items:
|
|
553
|
+
name: config.name,
|
|
554
|
+
key: config.key,
|
|
555
|
+
description: config.description,
|
|
556
|
+
parent: config.parent,
|
|
557
|
+
items: config.items,
|
|
558
|
+
environments: config.environments
|
|
542
559
|
});
|
|
543
560
|
let data;
|
|
544
561
|
try {
|
|
@@ -551,127 +568,191 @@ var ConfigClient = class {
|
|
|
551
568
|
if (!data || !data.data) throw new SmplValidationError("Failed to create config");
|
|
552
569
|
return resourceToConfig(data.data, this);
|
|
553
570
|
}
|
|
554
|
-
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
571
|
+
/** @internal — PUT a config update. */
|
|
572
|
+
async _updateConfig(config) {
|
|
573
|
+
const body = buildRequestBody({
|
|
574
|
+
id: config.id,
|
|
575
|
+
name: config.name,
|
|
576
|
+
key: config.key,
|
|
577
|
+
description: config.description,
|
|
578
|
+
parent: config.parent,
|
|
579
|
+
items: config.items,
|
|
580
|
+
environments: config.environments
|
|
581
|
+
});
|
|
582
|
+
let data;
|
|
561
583
|
try {
|
|
562
|
-
const result = await this._http.
|
|
563
|
-
params: { path: { id:
|
|
584
|
+
const result = await this._http.PUT("/api/v1/configs/{id}", {
|
|
585
|
+
params: { path: { id: config.id } },
|
|
586
|
+
body
|
|
564
587
|
});
|
|
565
|
-
if (result.error !== void 0
|
|
566
|
-
await checkError(result.response, `Failed to
|
|
588
|
+
if (result.error !== void 0)
|
|
589
|
+
await checkError(result.response, `Failed to update config ${config.id}`);
|
|
590
|
+
data = result.data;
|
|
567
591
|
} catch (err) {
|
|
568
592
|
wrapFetchError(err);
|
|
569
593
|
}
|
|
594
|
+
if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
|
|
595
|
+
return resourceToConfig(data.data, this);
|
|
570
596
|
}
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
597
|
+
/** @internal — fetch a config by UUID. */
|
|
598
|
+
async _getById(configId) {
|
|
599
|
+
let data;
|
|
600
|
+
try {
|
|
601
|
+
const result = await this._http.GET("/api/v1/configs/{id}", {
|
|
602
|
+
params: { path: { id: configId } }
|
|
603
|
+
});
|
|
604
|
+
if (result.error !== void 0)
|
|
605
|
+
await checkError(result.response, `Config ${configId} not found`);
|
|
606
|
+
data = result.data;
|
|
607
|
+
} catch (err) {
|
|
608
|
+
wrapFetchError(err);
|
|
581
609
|
}
|
|
582
|
-
|
|
583
|
-
|
|
610
|
+
if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
|
|
611
|
+
return resourceToConfig(data.data, this);
|
|
584
612
|
}
|
|
613
|
+
// ------------------------------------------------------------------
|
|
614
|
+
// Runtime: resolve and subscribe
|
|
615
|
+
// ------------------------------------------------------------------
|
|
585
616
|
/**
|
|
586
|
-
*
|
|
617
|
+
* Resolve a config's values for the current environment.
|
|
587
618
|
*
|
|
588
|
-
*
|
|
619
|
+
* Returns a flat dict of resolved key-value pairs, walking the
|
|
620
|
+
* parent chain and applying environment overrides.
|
|
589
621
|
*
|
|
590
|
-
*
|
|
591
|
-
* @param itemKey - Optional specific item key. If omitted, returns all values.
|
|
592
|
-
* @param defaultValue - Default value if the key is missing.
|
|
593
|
-
*
|
|
594
|
-
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
622
|
+
* Optionally pass a model class to map the resolved values.
|
|
595
623
|
*/
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (resolved === void 0) {
|
|
602
|
-
return defaultValue ?? null;
|
|
624
|
+
async resolve(key, model) {
|
|
625
|
+
await this._ensureInitialized();
|
|
626
|
+
const values = this._configCache[key];
|
|
627
|
+
if (values === void 0) {
|
|
628
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
|
|
603
629
|
}
|
|
604
|
-
if (
|
|
605
|
-
return
|
|
630
|
+
if (model) {
|
|
631
|
+
return new model(values);
|
|
606
632
|
}
|
|
607
|
-
return
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Return a config value as a string, or `defaultValue` if absent or not a string.
|
|
611
|
-
*
|
|
612
|
-
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
613
|
-
*/
|
|
614
|
-
getString(configKey, itemKey, defaultValue = null) {
|
|
615
|
-
const value = this.getValue(configKey, itemKey);
|
|
616
|
-
return typeof value === "string" ? value : defaultValue;
|
|
633
|
+
return values;
|
|
617
634
|
}
|
|
618
635
|
/**
|
|
619
|
-
*
|
|
636
|
+
* Subscribe to a config's values — returns a live proxy that
|
|
637
|
+
* auto-updates when the underlying config changes.
|
|
620
638
|
*
|
|
621
|
-
*
|
|
639
|
+
* Optionally pass a model class to map the resolved values.
|
|
622
640
|
*/
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
641
|
+
async subscribe(key, model) {
|
|
642
|
+
await this._ensureInitialized();
|
|
643
|
+
if (!(key in this._configCache)) {
|
|
644
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
|
|
645
|
+
}
|
|
646
|
+
return new LiveConfigProxy(this, key, model);
|
|
626
647
|
}
|
|
648
|
+
// ------------------------------------------------------------------
|
|
649
|
+
// Runtime: change listeners (3-level overloads)
|
|
650
|
+
// ------------------------------------------------------------------
|
|
627
651
|
/**
|
|
628
|
-
*
|
|
652
|
+
* Register a change listener.
|
|
629
653
|
*
|
|
630
|
-
*
|
|
654
|
+
* - `onChange(callback)` — fires for any config change (global).
|
|
655
|
+
* - `onChange(configKey, callback)` — fires for changes to a specific config.
|
|
656
|
+
* - `onChange(configKey, itemKey, callback)` — fires for a specific item.
|
|
631
657
|
*/
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
658
|
+
onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
|
|
659
|
+
if (typeof callbackOrConfigKey === "function") {
|
|
660
|
+
this._listeners.push({
|
|
661
|
+
callback: callbackOrConfigKey,
|
|
662
|
+
configKey: null,
|
|
663
|
+
itemKey: null
|
|
664
|
+
});
|
|
665
|
+
} else if (typeof callbackOrItemKey === "function") {
|
|
666
|
+
this._listeners.push({
|
|
667
|
+
callback: callbackOrItemKey,
|
|
668
|
+
configKey: callbackOrConfigKey,
|
|
669
|
+
itemKey: null
|
|
670
|
+
});
|
|
671
|
+
} else if (typeof callbackOrItemKey === "string" && callback) {
|
|
672
|
+
this._listeners.push({
|
|
673
|
+
callback,
|
|
674
|
+
configKey: callbackOrConfigKey,
|
|
675
|
+
itemKey: callbackOrItemKey
|
|
676
|
+
});
|
|
677
|
+
}
|
|
635
678
|
}
|
|
679
|
+
// ------------------------------------------------------------------
|
|
680
|
+
// Runtime: refresh
|
|
681
|
+
// ------------------------------------------------------------------
|
|
636
682
|
/**
|
|
637
683
|
* Re-fetch all configs, re-resolve values, and update the cache.
|
|
638
|
-
*
|
|
639
|
-
* Fires change listeners for any values that differ from the previous cache.
|
|
640
|
-
*
|
|
641
|
-
* @throws {SmplNotConnectedError} If connect() has not been called.
|
|
684
|
+
* Fires change listeners for any values that differ.
|
|
642
685
|
*/
|
|
643
686
|
async refresh() {
|
|
644
|
-
if (!this.
|
|
645
|
-
throw new
|
|
687
|
+
if (!this._initialized) {
|
|
688
|
+
throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
|
|
646
689
|
}
|
|
647
690
|
const environment = this._parent?._environment;
|
|
648
691
|
if (!environment) {
|
|
649
692
|
throw new SmplError("No environment set.");
|
|
650
693
|
}
|
|
651
694
|
const configs = await this.list();
|
|
695
|
+
this._configStore = configs;
|
|
652
696
|
const newCache = {};
|
|
653
697
|
for (const cfg of configs) {
|
|
654
|
-
const chain = await cfg._buildChain(
|
|
698
|
+
const chain = await cfg._buildChain();
|
|
655
699
|
newCache[cfg.key] = resolveChain(chain, environment);
|
|
656
700
|
}
|
|
657
701
|
const oldCache = this._configCache;
|
|
658
702
|
this._configCache = newCache;
|
|
659
703
|
this._diffAndFire(oldCache, newCache, "manual");
|
|
660
704
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
705
|
+
// ------------------------------------------------------------------
|
|
706
|
+
// Runtime: lazy initialization
|
|
707
|
+
// ------------------------------------------------------------------
|
|
708
|
+
/** @internal */
|
|
709
|
+
async _ensureInitialized() {
|
|
710
|
+
if (this._initialized) return;
|
|
711
|
+
const environment = this._parent?._environment;
|
|
712
|
+
if (!environment) {
|
|
713
|
+
throw new SmplError("No environment set. Ensure SmplClient is configured.");
|
|
714
|
+
}
|
|
715
|
+
const configs = await this.list();
|
|
716
|
+
this._configStore = configs;
|
|
717
|
+
const cache = {};
|
|
718
|
+
for (const cfg of configs) {
|
|
719
|
+
const chain = await cfg._buildChain();
|
|
720
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
721
|
+
}
|
|
722
|
+
this._configCache = cache;
|
|
723
|
+
this._initialized = true;
|
|
724
|
+
if (this._getSharedWs) {
|
|
725
|
+
const ws = this._getSharedWs();
|
|
726
|
+
ws.on("config_changed", this._handleConfigChanged);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/** @internal — called by SmplClient for backward compat. */
|
|
730
|
+
async _connectInternal(environment) {
|
|
731
|
+
if (this._initialized) return;
|
|
732
|
+
const configs = await this.list();
|
|
733
|
+
this._configStore = configs;
|
|
734
|
+
const cache = {};
|
|
735
|
+
for (const cfg of configs) {
|
|
736
|
+
const chain = await cfg._buildChain();
|
|
737
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
738
|
+
}
|
|
739
|
+
this._configCache = cache;
|
|
740
|
+
this._initialized = true;
|
|
674
741
|
}
|
|
742
|
+
/** @internal — get resolved config from cache. Used by LiveConfigProxy. */
|
|
743
|
+
_getCachedConfig(key) {
|
|
744
|
+
return this._configCache[key];
|
|
745
|
+
}
|
|
746
|
+
// ------------------------------------------------------------------
|
|
747
|
+
// Internal: WebSocket handler
|
|
748
|
+
// ------------------------------------------------------------------
|
|
749
|
+
_handleConfigChanged = (_data) => {
|
|
750
|
+
void this.refresh().catch(() => {
|
|
751
|
+
});
|
|
752
|
+
};
|
|
753
|
+
// ------------------------------------------------------------------
|
|
754
|
+
// Internal: change detection
|
|
755
|
+
// ------------------------------------------------------------------
|
|
675
756
|
/** @internal */
|
|
676
757
|
_diffAndFire(oldCache, newCache, source) {
|
|
677
758
|
const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
|
|
@@ -702,54 +783,9 @@ var ConfigClient = class {
|
|
|
702
783
|
}
|
|
703
784
|
}
|
|
704
785
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
* Called by {@link Config} instance methods.
|
|
709
|
-
* @internal
|
|
710
|
-
*/
|
|
711
|
-
async _updateConfig(payload) {
|
|
712
|
-
const body = buildRequestBody({
|
|
713
|
-
id: payload.configId,
|
|
714
|
-
name: payload.name,
|
|
715
|
-
key: payload.key,
|
|
716
|
-
description: payload.description,
|
|
717
|
-
parent: payload.parent,
|
|
718
|
-
items: payload.items,
|
|
719
|
-
environments: payload.environments
|
|
720
|
-
});
|
|
721
|
-
let data;
|
|
722
|
-
try {
|
|
723
|
-
const result = await this._http.PUT("/api/v1/configs/{id}", {
|
|
724
|
-
params: { path: { id: payload.configId } },
|
|
725
|
-
body
|
|
726
|
-
});
|
|
727
|
-
if (result.error !== void 0)
|
|
728
|
-
await checkError(result.response, `Failed to update config ${payload.configId}`);
|
|
729
|
-
data = result.data;
|
|
730
|
-
} catch (err) {
|
|
731
|
-
wrapFetchError(err);
|
|
732
|
-
}
|
|
733
|
-
if (!data || !data.data)
|
|
734
|
-
throw new SmplValidationError(`Failed to update config ${payload.configId}`);
|
|
735
|
-
return resourceToConfig(data.data, this);
|
|
736
|
-
}
|
|
737
|
-
// ---- Private helpers ----
|
|
738
|
-
async _getById(configId) {
|
|
739
|
-
let data;
|
|
740
|
-
try {
|
|
741
|
-
const result = await this._http.GET("/api/v1/configs/{id}", {
|
|
742
|
-
params: { path: { id: configId } }
|
|
743
|
-
});
|
|
744
|
-
if (result.error !== void 0)
|
|
745
|
-
await checkError(result.response, `Config ${configId} not found`);
|
|
746
|
-
data = result.data;
|
|
747
|
-
} catch (err) {
|
|
748
|
-
wrapFetchError(err);
|
|
749
|
-
}
|
|
750
|
-
if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
|
|
751
|
-
return resourceToConfig(data.data, this);
|
|
752
|
-
}
|
|
786
|
+
// ------------------------------------------------------------------
|
|
787
|
+
// Internal: fetch by key
|
|
788
|
+
// ------------------------------------------------------------------
|
|
753
789
|
async _getByKey(key) {
|
|
754
790
|
let data;
|
|
755
791
|
try {
|
|
@@ -774,7 +810,7 @@ var import_openapi_fetch2 = __toESM(require("openapi-fetch"), 1);
|
|
|
774
810
|
|
|
775
811
|
// src/flags/models.ts
|
|
776
812
|
var Flag = class {
|
|
777
|
-
/** UUID of the flag. */
|
|
813
|
+
/** UUID of the flag, or `null` if unsaved. */
|
|
778
814
|
id;
|
|
779
815
|
/** Unique key within the account. */
|
|
780
816
|
key;
|
|
@@ -811,37 +847,35 @@ var Flag = class {
|
|
|
811
847
|
this.updatedAt = fields.updatedAt;
|
|
812
848
|
}
|
|
813
849
|
/**
|
|
814
|
-
*
|
|
850
|
+
* Persist this flag to the server.
|
|
815
851
|
*
|
|
816
|
-
*
|
|
852
|
+
* POST if `id` is null (new flag), PUT if `id` is set (update).
|
|
853
|
+
* Updates this instance in-place with the server response.
|
|
817
854
|
*/
|
|
818
|
-
async
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
});
|
|
827
|
-
this._apply(updated);
|
|
855
|
+
async save() {
|
|
856
|
+
if (this.id === null) {
|
|
857
|
+
const created = await this._client._createFlag(this);
|
|
858
|
+
this._apply(created);
|
|
859
|
+
} else {
|
|
860
|
+
const updated = await this._client._updateFlag(this);
|
|
861
|
+
this._apply(updated);
|
|
862
|
+
}
|
|
828
863
|
}
|
|
829
864
|
/**
|
|
830
|
-
* Add a rule to a specific environment.
|
|
865
|
+
* Add a rule to a specific environment (sync local mutation).
|
|
831
866
|
*
|
|
832
867
|
* The built rule must include an `environment` key (set via
|
|
833
|
-
* `Rule(...).environment("env_key")`).
|
|
834
|
-
*
|
|
868
|
+
* `Rule(...).environment("env_key")`). No HTTP call is made.
|
|
869
|
+
*
|
|
870
|
+
* @returns `this` for chaining.
|
|
835
871
|
*/
|
|
836
|
-
|
|
872
|
+
addRule(builtRule) {
|
|
837
873
|
const envKey = builtRule.environment;
|
|
838
874
|
if (!envKey) {
|
|
839
875
|
throw new Error(
|
|
840
876
|
`Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
|
|
841
877
|
);
|
|
842
878
|
}
|
|
843
|
-
const current = await this._client.get(this.id);
|
|
844
|
-
this._apply(current);
|
|
845
879
|
const envs = { ...this.environments };
|
|
846
880
|
const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
|
|
847
881
|
const rules = [...envData.rules ?? []];
|
|
@@ -849,13 +883,44 @@ var Flag = class {
|
|
|
849
883
|
rules.push(ruleCopy);
|
|
850
884
|
envData.rules = rules;
|
|
851
885
|
envs[envKey] = envData;
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
environments: envs
|
|
855
|
-
});
|
|
856
|
-
this._apply(updated);
|
|
886
|
+
this.environments = envs;
|
|
887
|
+
return this;
|
|
857
888
|
}
|
|
858
|
-
/**
|
|
889
|
+
/** Enable or disable a flag in a specific environment (sync local mutation). */
|
|
890
|
+
setEnvironmentEnabled(envKey, enabled) {
|
|
891
|
+
const envs = { ...this.environments };
|
|
892
|
+
const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
|
|
893
|
+
envData.enabled = enabled;
|
|
894
|
+
envs[envKey] = envData;
|
|
895
|
+
this.environments = envs;
|
|
896
|
+
}
|
|
897
|
+
/** Set the default value for a specific environment (sync local mutation). */
|
|
898
|
+
setEnvironmentDefault(envKey, defaultValue) {
|
|
899
|
+
const envs = { ...this.environments };
|
|
900
|
+
const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
|
|
901
|
+
envData.default = defaultValue;
|
|
902
|
+
envs[envKey] = envData;
|
|
903
|
+
this.environments = envs;
|
|
904
|
+
}
|
|
905
|
+
/** Clear all rules for a specific environment (sync local mutation). */
|
|
906
|
+
clearRules(envKey) {
|
|
907
|
+
const envs = { ...this.environments };
|
|
908
|
+
const envData = envs[envKey];
|
|
909
|
+
if (envData) {
|
|
910
|
+
envs[envKey] = { ...envData, rules: [] };
|
|
911
|
+
this.environments = envs;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Evaluate the flag locally (sync, no HTTP).
|
|
916
|
+
*
|
|
917
|
+
* Requires `initialize()` to have been called.
|
|
918
|
+
*/
|
|
919
|
+
/* v8 ignore next 3 — overridden by all exported subclasses */
|
|
920
|
+
get(options) {
|
|
921
|
+
return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
922
|
+
}
|
|
923
|
+
/** @internal — copy all fields from another Flag instance. */
|
|
859
924
|
_apply(other) {
|
|
860
925
|
this.id = other.id;
|
|
861
926
|
this.key = other.key;
|
|
@@ -872,35 +937,52 @@ var Flag = class {
|
|
|
872
937
|
return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
|
|
873
938
|
}
|
|
874
939
|
};
|
|
875
|
-
var
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
/** Known attributes. */
|
|
883
|
-
attributes;
|
|
884
|
-
constructor(fields) {
|
|
885
|
-
this.id = fields.id;
|
|
886
|
-
this.key = fields.key;
|
|
887
|
-
this.name = fields.name;
|
|
888
|
-
this.attributes = fields.attributes;
|
|
940
|
+
var BooleanFlag = class extends Flag {
|
|
941
|
+
get(options) {
|
|
942
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
943
|
+
if (typeof value === "boolean") {
|
|
944
|
+
return value;
|
|
945
|
+
}
|
|
946
|
+
return this.default;
|
|
889
947
|
}
|
|
890
|
-
|
|
891
|
-
|
|
948
|
+
};
|
|
949
|
+
var StringFlag = class extends Flag {
|
|
950
|
+
get(options) {
|
|
951
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
952
|
+
if (typeof value === "string") {
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
return this.default;
|
|
892
956
|
}
|
|
893
957
|
};
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
958
|
+
var NumberFlag = class extends Flag {
|
|
959
|
+
get(options) {
|
|
960
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
961
|
+
if (typeof value === "number") {
|
|
962
|
+
return value;
|
|
963
|
+
}
|
|
964
|
+
return this.default;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
var JsonFlag = class extends Flag {
|
|
968
|
+
get(options) {
|
|
969
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
970
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
971
|
+
return value;
|
|
972
|
+
}
|
|
973
|
+
return this.default;
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// src/flags/client.ts
|
|
978
|
+
var import_json_logic_js = __toESM(require("json-logic-js"), 1);
|
|
979
|
+
var FLAGS_BASE_URL = "https://flags.smplkit.com";
|
|
980
|
+
var APP_BASE_URL = "https://app.smplkit.com";
|
|
981
|
+
var CACHE_MAX_SIZE = 1e4;
|
|
982
|
+
var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
|
|
983
|
+
var CONTEXT_BATCH_FLUSH_SIZE = 100;
|
|
984
|
+
async function checkError2(response, _context) {
|
|
985
|
+
const body = await response.text().catch(() => "");
|
|
904
986
|
throwForStatus(response.status, body);
|
|
905
987
|
}
|
|
906
988
|
function wrapFetchError2(err) {
|
|
@@ -1017,88 +1099,6 @@ var FlagStats = class {
|
|
|
1017
1099
|
this.cacheMisses = cacheMisses;
|
|
1018
1100
|
}
|
|
1019
1101
|
};
|
|
1020
|
-
var FlagHandleBase = class {
|
|
1021
|
-
/** @internal */
|
|
1022
|
-
_namespace;
|
|
1023
|
-
/** @internal */
|
|
1024
|
-
_key;
|
|
1025
|
-
/** @internal */
|
|
1026
|
-
_default;
|
|
1027
|
-
/** @internal */
|
|
1028
|
-
_listeners = [];
|
|
1029
|
-
constructor(namespace, key, defaultValue) {
|
|
1030
|
-
this._namespace = namespace;
|
|
1031
|
-
this._key = key;
|
|
1032
|
-
this._default = defaultValue;
|
|
1033
|
-
}
|
|
1034
|
-
get key() {
|
|
1035
|
-
return this._key;
|
|
1036
|
-
}
|
|
1037
|
-
get default() {
|
|
1038
|
-
return this._default;
|
|
1039
|
-
}
|
|
1040
|
-
/* v8 ignore next 3 — overridden by all exported subclasses */
|
|
1041
|
-
get(options) {
|
|
1042
|
-
return this._namespace._evaluateHandle(this._key, this._default, options?.context ?? null);
|
|
1043
|
-
}
|
|
1044
|
-
/** Register a flag-specific change listener. Works as a decorator. */
|
|
1045
|
-
onChange(callback) {
|
|
1046
|
-
this._listeners.push(callback);
|
|
1047
|
-
return callback;
|
|
1048
|
-
}
|
|
1049
|
-
};
|
|
1050
|
-
var BoolFlagHandle = class extends FlagHandleBase {
|
|
1051
|
-
get(options) {
|
|
1052
|
-
const value = this._namespace._evaluateHandle(
|
|
1053
|
-
this._key,
|
|
1054
|
-
this._default,
|
|
1055
|
-
options?.context ?? null
|
|
1056
|
-
);
|
|
1057
|
-
if (typeof value === "boolean") {
|
|
1058
|
-
return value;
|
|
1059
|
-
}
|
|
1060
|
-
return this._default;
|
|
1061
|
-
}
|
|
1062
|
-
};
|
|
1063
|
-
var StringFlagHandle = class extends FlagHandleBase {
|
|
1064
|
-
get(options) {
|
|
1065
|
-
const value = this._namespace._evaluateHandle(
|
|
1066
|
-
this._key,
|
|
1067
|
-
this._default,
|
|
1068
|
-
options?.context ?? null
|
|
1069
|
-
);
|
|
1070
|
-
if (typeof value === "string") {
|
|
1071
|
-
return value;
|
|
1072
|
-
}
|
|
1073
|
-
return this._default;
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
var NumberFlagHandle = class extends FlagHandleBase {
|
|
1077
|
-
get(options) {
|
|
1078
|
-
const value = this._namespace._evaluateHandle(
|
|
1079
|
-
this._key,
|
|
1080
|
-
this._default,
|
|
1081
|
-
options?.context ?? null
|
|
1082
|
-
);
|
|
1083
|
-
if (typeof value === "number") {
|
|
1084
|
-
return value;
|
|
1085
|
-
}
|
|
1086
|
-
return this._default;
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
var JsonFlagHandle = class extends FlagHandleBase {
|
|
1090
|
-
get(options) {
|
|
1091
|
-
const value = this._namespace._evaluateHandle(
|
|
1092
|
-
this._key,
|
|
1093
|
-
this._default,
|
|
1094
|
-
options?.context ?? null
|
|
1095
|
-
);
|
|
1096
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1097
|
-
return value;
|
|
1098
|
-
}
|
|
1099
|
-
return this._default;
|
|
1100
|
-
}
|
|
1101
|
-
};
|
|
1102
1102
|
var ContextRegistrationBuffer = class {
|
|
1103
1103
|
_seen = /* @__PURE__ */ new Map();
|
|
1104
1104
|
_pending = [];
|
|
@@ -1142,13 +1142,14 @@ var FlagsClient = class {
|
|
|
1142
1142
|
// Runtime state
|
|
1143
1143
|
_environment = null;
|
|
1144
1144
|
_flagStore = {};
|
|
1145
|
-
|
|
1145
|
+
_initialized = false;
|
|
1146
1146
|
_cache = new ResolutionCache();
|
|
1147
1147
|
_contextProvider = null;
|
|
1148
1148
|
_contextBuffer = new ContextRegistrationBuffer();
|
|
1149
1149
|
_handles = {};
|
|
1150
1150
|
_globalListeners = [];
|
|
1151
|
-
|
|
1151
|
+
_keyListeners = /* @__PURE__ */ new Map();
|
|
1152
|
+
// Shared WebSocket (set during initialize)
|
|
1152
1153
|
_wsManager = null;
|
|
1153
1154
|
_ensureWs;
|
|
1154
1155
|
/** @internal — set by SmplClient after construction. */
|
|
@@ -1190,55 +1191,91 @@ var FlagsClient = class {
|
|
|
1190
1191
|
});
|
|
1191
1192
|
}
|
|
1192
1193
|
// ------------------------------------------------------------------
|
|
1193
|
-
// Management methods
|
|
1194
|
+
// Management: factory methods (return unsaved flags)
|
|
1194
1195
|
// ------------------------------------------------------------------
|
|
1195
|
-
/** Create
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1196
|
+
/** Create an unsaved boolean flag. Call `.save()` to persist. */
|
|
1197
|
+
newBooleanFlag(key, options) {
|
|
1198
|
+
return new BooleanFlag(this, {
|
|
1199
|
+
id: null,
|
|
1200
|
+
key,
|
|
1201
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1202
|
+
type: "BOOLEAN",
|
|
1203
|
+
default: options.default,
|
|
1204
|
+
values: [
|
|
1200
1205
|
{ name: "True", value: true },
|
|
1201
1206
|
{ name: "False", value: false }
|
|
1202
|
-
]
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
return this
|
|
1207
|
+
],
|
|
1208
|
+
description: options.description ?? null,
|
|
1209
|
+
environments: {},
|
|
1210
|
+
createdAt: null,
|
|
1211
|
+
updatedAt: null
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
/** Create an unsaved string flag. Call `.save()` to persist. */
|
|
1215
|
+
newStringFlag(key, options) {
|
|
1216
|
+
return new StringFlag(this, {
|
|
1217
|
+
id: null,
|
|
1218
|
+
key,
|
|
1219
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1220
|
+
type: "STRING",
|
|
1221
|
+
default: options.default,
|
|
1222
|
+
values: options.values ?? [],
|
|
1223
|
+
description: options.description ?? null,
|
|
1224
|
+
environments: {},
|
|
1225
|
+
createdAt: null,
|
|
1226
|
+
updatedAt: null
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
/** Create an unsaved number flag. Call `.save()` to persist. */
|
|
1230
|
+
newNumberFlag(key, options) {
|
|
1231
|
+
return new NumberFlag(this, {
|
|
1232
|
+
id: null,
|
|
1233
|
+
key,
|
|
1234
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1235
|
+
type: "NUMERIC",
|
|
1236
|
+
default: options.default,
|
|
1237
|
+
values: options.values ?? [],
|
|
1238
|
+
description: options.description ?? null,
|
|
1239
|
+
environments: {},
|
|
1240
|
+
createdAt: null,
|
|
1241
|
+
updatedAt: null
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
/** Create an unsaved JSON flag. Call `.save()` to persist. */
|
|
1245
|
+
newJsonFlag(key, options) {
|
|
1246
|
+
return new JsonFlag(this, {
|
|
1247
|
+
id: null,
|
|
1248
|
+
key,
|
|
1249
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1250
|
+
type: "JSON",
|
|
1251
|
+
default: options.default,
|
|
1252
|
+
values: options.values ?? [],
|
|
1253
|
+
description: options.description ?? null,
|
|
1254
|
+
environments: {},
|
|
1255
|
+
createdAt: null,
|
|
1256
|
+
updatedAt: null
|
|
1257
|
+
});
|
|
1227
1258
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1259
|
+
// ------------------------------------------------------------------
|
|
1260
|
+
// Management: CRUD
|
|
1261
|
+
// ------------------------------------------------------------------
|
|
1262
|
+
/** Fetch a flag by key. */
|
|
1263
|
+
async get(key) {
|
|
1230
1264
|
let data;
|
|
1231
1265
|
try {
|
|
1232
|
-
const result = await this._http.GET("/api/v1/flags
|
|
1233
|
-
params: {
|
|
1266
|
+
const result = await this._http.GET("/api/v1/flags", {
|
|
1267
|
+
params: { query: { "filter[key]": key } }
|
|
1234
1268
|
});
|
|
1235
|
-
if (result.error !== void 0)
|
|
1269
|
+
if (result.error !== void 0)
|
|
1270
|
+
await checkError2(result.response, `Flag with key '${key}' not found`);
|
|
1236
1271
|
data = result.data;
|
|
1237
1272
|
} catch (err) {
|
|
1238
1273
|
wrapFetchError2(err);
|
|
1239
1274
|
}
|
|
1240
|
-
if (!data || !data.data
|
|
1241
|
-
|
|
1275
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
1276
|
+
throw new SmplNotFoundError(`Flag with key '${key}' not found`);
|
|
1277
|
+
}
|
|
1278
|
+
return this._resourceToModel(data.data[0]);
|
|
1242
1279
|
}
|
|
1243
1280
|
/** List all flags. */
|
|
1244
1281
|
async list() {
|
|
@@ -1253,161 +1290,148 @@ var FlagsClient = class {
|
|
|
1253
1290
|
if (!data) return [];
|
|
1254
1291
|
return data.data.map((r) => this._resourceToModel(r));
|
|
1255
1292
|
}
|
|
1256
|
-
/** Delete a flag by
|
|
1257
|
-
async delete(
|
|
1293
|
+
/** Delete a flag by key. */
|
|
1294
|
+
async delete(key) {
|
|
1295
|
+
const flag = await this.get(key);
|
|
1258
1296
|
try {
|
|
1259
1297
|
const result = await this._http.DELETE("/api/v1/flags/{id}", {
|
|
1260
|
-
params: { path: { id:
|
|
1298
|
+
params: { path: { id: flag.id } }
|
|
1261
1299
|
});
|
|
1262
1300
|
if (result.error !== void 0 && result.response.status !== 204)
|
|
1263
|
-
await checkError2(result.response, `Failed to delete flag ${
|
|
1301
|
+
await checkError2(result.response, `Failed to delete flag '${key}'`);
|
|
1264
1302
|
} catch (err) {
|
|
1265
1303
|
wrapFetchError2(err);
|
|
1266
1304
|
}
|
|
1267
1305
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
async _updateFlag(options) {
|
|
1274
|
-
const { flag } = options;
|
|
1306
|
+
// ------------------------------------------------------------------
|
|
1307
|
+
// Management: internal save methods (called by Flag.save())
|
|
1308
|
+
// ------------------------------------------------------------------
|
|
1309
|
+
/** @internal — POST a new flag. */
|
|
1310
|
+
async _createFlag(flag) {
|
|
1275
1311
|
const body = {
|
|
1276
1312
|
data: {
|
|
1277
1313
|
type: "flag",
|
|
1278
1314
|
attributes: {
|
|
1279
1315
|
key: flag.key,
|
|
1280
|
-
name:
|
|
1316
|
+
name: flag.name,
|
|
1317
|
+
description: flag.description ?? "",
|
|
1281
1318
|
type: flag.type,
|
|
1282
|
-
default:
|
|
1283
|
-
values:
|
|
1284
|
-
|
|
1285
|
-
...options.environments !== void 0 ? { environments: options.environments } : flag.environments && Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1319
|
+
default: flag.default,
|
|
1320
|
+
values: flag.values,
|
|
1321
|
+
...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1286
1322
|
}
|
|
1287
1323
|
}
|
|
1288
1324
|
};
|
|
1289
1325
|
let data;
|
|
1290
1326
|
try {
|
|
1291
|
-
const result = await this._http.
|
|
1292
|
-
|
|
1293
|
-
body
|
|
1294
|
-
});
|
|
1295
|
-
if (result.error !== void 0)
|
|
1296
|
-
await checkError2(result.response, `Failed to update flag ${flag.id}`);
|
|
1327
|
+
const result = await this._http.POST("/api/v1/flags", { body });
|
|
1328
|
+
if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
|
|
1297
1329
|
data = result.data;
|
|
1298
1330
|
} catch (err) {
|
|
1299
1331
|
wrapFetchError2(err);
|
|
1300
1332
|
}
|
|
1301
|
-
if (!data || !data.data) throw new SmplValidationError(
|
|
1333
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
|
|
1302
1334
|
return this._resourceToModel(data.data);
|
|
1303
1335
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1336
|
+
/** @internal — PUT a flag update. */
|
|
1337
|
+
async _updateFlag(flag) {
|
|
1338
|
+
const body = {
|
|
1339
|
+
data: {
|
|
1340
|
+
type: "flag",
|
|
1341
|
+
attributes: {
|
|
1342
|
+
key: flag.key,
|
|
1343
|
+
name: flag.name,
|
|
1344
|
+
type: flag.type,
|
|
1345
|
+
default: flag.default,
|
|
1346
|
+
values: flag.values,
|
|
1347
|
+
description: flag.description ?? "",
|
|
1348
|
+
...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1314
1349
|
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
await checkError2(result.response, "Failed to create context type");
|
|
1318
|
-
data = result.data;
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
wrapFetchError2(err);
|
|
1321
|
-
}
|
|
1322
|
-
if (!data || !data.data) throw new SmplValidationError("Failed to create context type");
|
|
1323
|
-
return this._parseContextType(data.data);
|
|
1324
|
-
}
|
|
1325
|
-
/** Update a context type (merge attributes). */
|
|
1326
|
-
async updateContextType(ctId, options) {
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1327
1352
|
let data;
|
|
1328
1353
|
try {
|
|
1329
|
-
const result = await this.
|
|
1330
|
-
params: { path: { id:
|
|
1331
|
-
body
|
|
1332
|
-
data: {
|
|
1333
|
-
type: "context_type",
|
|
1334
|
-
attributes: { key: options.key, name: options.name, attributes: options.attributes }
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1354
|
+
const result = await this._http.PUT("/api/v1/flags/{id}", {
|
|
1355
|
+
params: { path: { id: flag.id } },
|
|
1356
|
+
body
|
|
1337
1357
|
});
|
|
1338
1358
|
if (result.error !== void 0)
|
|
1339
|
-
await checkError2(result.response, `Failed to update
|
|
1340
|
-
data = result.data;
|
|
1341
|
-
} catch (err) {
|
|
1342
|
-
wrapFetchError2(err);
|
|
1343
|
-
}
|
|
1344
|
-
if (!data || !data.data) throw new SmplValidationError(`Failed to update context type ${ctId}`);
|
|
1345
|
-
return this._parseContextType(data.data);
|
|
1346
|
-
}
|
|
1347
|
-
/** List all context types. */
|
|
1348
|
-
async listContextTypes() {
|
|
1349
|
-
let data;
|
|
1350
|
-
try {
|
|
1351
|
-
const result = await this._appHttp.GET("/api/v1/context_types");
|
|
1352
|
-
if (result.error !== void 0)
|
|
1353
|
-
await checkError2(result.response, "Failed to list context types");
|
|
1354
|
-
data = result.data;
|
|
1355
|
-
} catch (err) {
|
|
1356
|
-
wrapFetchError2(err);
|
|
1357
|
-
}
|
|
1358
|
-
if (!data || !data.data) throw new SmplValidationError("Failed to list context types");
|
|
1359
|
-
return data.data.map((item) => this._parseContextType(item));
|
|
1360
|
-
}
|
|
1361
|
-
/** Delete a context type. */
|
|
1362
|
-
async deleteContextType(ctId) {
|
|
1363
|
-
try {
|
|
1364
|
-
const result = await this._appHttp.DELETE("/api/v1/context_types/{id}", {
|
|
1365
|
-
params: { path: { id: ctId } }
|
|
1366
|
-
});
|
|
1367
|
-
if (result.error !== void 0 && result.response.status !== 204)
|
|
1368
|
-
await checkError2(result.response, `Failed to delete context type ${ctId}`);
|
|
1369
|
-
} catch (err) {
|
|
1370
|
-
wrapFetchError2(err);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
/** List context instances filtered by context type key. */
|
|
1374
|
-
async listContexts(options) {
|
|
1375
|
-
let data;
|
|
1376
|
-
try {
|
|
1377
|
-
const result = await this._appHttp.GET("/api/v1/contexts", {
|
|
1378
|
-
params: { query: { "filter[context_type_id]": options.contextTypeKey } }
|
|
1379
|
-
});
|
|
1380
|
-
if (result.error !== void 0) await checkError2(result.response, "Failed to list contexts");
|
|
1359
|
+
await checkError2(result.response, `Failed to update flag ${flag.id}`);
|
|
1381
1360
|
data = result.data;
|
|
1382
1361
|
} catch (err) {
|
|
1383
1362
|
wrapFetchError2(err);
|
|
1384
1363
|
}
|
|
1385
|
-
|
|
1364
|
+
if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
|
|
1365
|
+
return this._resourceToModel(data.data);
|
|
1386
1366
|
}
|
|
1387
1367
|
// ------------------------------------------------------------------
|
|
1388
1368
|
// Runtime: typed flag handles
|
|
1389
1369
|
// ------------------------------------------------------------------
|
|
1390
|
-
/** Declare a boolean flag handle. */
|
|
1391
|
-
|
|
1392
|
-
const handle = new
|
|
1370
|
+
/** Declare a boolean flag handle for runtime evaluation. */
|
|
1371
|
+
booleanFlag(key, defaultValue) {
|
|
1372
|
+
const handle = new BooleanFlag(this, {
|
|
1373
|
+
id: null,
|
|
1374
|
+
key,
|
|
1375
|
+
name: key,
|
|
1376
|
+
type: "BOOLEAN",
|
|
1377
|
+
default: defaultValue,
|
|
1378
|
+
values: [],
|
|
1379
|
+
description: null,
|
|
1380
|
+
environments: {},
|
|
1381
|
+
createdAt: null,
|
|
1382
|
+
updatedAt: null
|
|
1383
|
+
});
|
|
1393
1384
|
this._handles[key] = handle;
|
|
1394
1385
|
return handle;
|
|
1395
1386
|
}
|
|
1396
|
-
/** Declare a string flag handle. */
|
|
1387
|
+
/** Declare a string flag handle for runtime evaluation. */
|
|
1397
1388
|
stringFlag(key, defaultValue) {
|
|
1398
|
-
const handle = new
|
|
1389
|
+
const handle = new StringFlag(this, {
|
|
1390
|
+
id: null,
|
|
1391
|
+
key,
|
|
1392
|
+
name: key,
|
|
1393
|
+
type: "STRING",
|
|
1394
|
+
default: defaultValue,
|
|
1395
|
+
values: [],
|
|
1396
|
+
description: null,
|
|
1397
|
+
environments: {},
|
|
1398
|
+
createdAt: null,
|
|
1399
|
+
updatedAt: null
|
|
1400
|
+
});
|
|
1399
1401
|
this._handles[key] = handle;
|
|
1400
1402
|
return handle;
|
|
1401
1403
|
}
|
|
1402
|
-
/** Declare a numeric flag handle. */
|
|
1404
|
+
/** Declare a numeric flag handle for runtime evaluation. */
|
|
1403
1405
|
numberFlag(key, defaultValue) {
|
|
1404
|
-
const handle = new
|
|
1406
|
+
const handle = new NumberFlag(this, {
|
|
1407
|
+
id: null,
|
|
1408
|
+
key,
|
|
1409
|
+
name: key,
|
|
1410
|
+
type: "NUMERIC",
|
|
1411
|
+
default: defaultValue,
|
|
1412
|
+
values: [],
|
|
1413
|
+
description: null,
|
|
1414
|
+
environments: {},
|
|
1415
|
+
createdAt: null,
|
|
1416
|
+
updatedAt: null
|
|
1417
|
+
});
|
|
1405
1418
|
this._handles[key] = handle;
|
|
1406
1419
|
return handle;
|
|
1407
1420
|
}
|
|
1408
|
-
/** Declare a JSON flag handle. */
|
|
1421
|
+
/** Declare a JSON flag handle for runtime evaluation. */
|
|
1409
1422
|
jsonFlag(key, defaultValue) {
|
|
1410
|
-
const handle = new
|
|
1423
|
+
const handle = new JsonFlag(this, {
|
|
1424
|
+
id: null,
|
|
1425
|
+
key,
|
|
1426
|
+
name: key,
|
|
1427
|
+
type: "JSON",
|
|
1428
|
+
default: defaultValue,
|
|
1429
|
+
values: [],
|
|
1430
|
+
description: null,
|
|
1431
|
+
environments: {},
|
|
1432
|
+
createdAt: null,
|
|
1433
|
+
updatedAt: null
|
|
1434
|
+
});
|
|
1411
1435
|
this._handles[key] = handle;
|
|
1412
1436
|
return handle;
|
|
1413
1437
|
}
|
|
@@ -1417,41 +1441,32 @@ var FlagsClient = class {
|
|
|
1417
1441
|
/**
|
|
1418
1442
|
* Register a context provider function.
|
|
1419
1443
|
*
|
|
1420
|
-
* Called on every `handle.get()` to supply the current evaluation
|
|
1421
|
-
* context. Can also be used as a decorator:
|
|
1422
|
-
*
|
|
1423
|
-
* ```typescript
|
|
1424
|
-
* client.flags.setContextProvider(() => [
|
|
1425
|
-
* new Context("user", userId, { plan: userPlan }),
|
|
1426
|
-
* ]);
|
|
1427
|
-
* ```
|
|
1444
|
+
* Called on every `handle.get()` to supply the current evaluation context.
|
|
1428
1445
|
*/
|
|
1429
1446
|
setContextProvider(fn) {
|
|
1430
1447
|
this._contextProvider = fn;
|
|
1431
1448
|
}
|
|
1432
1449
|
/**
|
|
1433
1450
|
* Register a context provider — decorator-style alias.
|
|
1434
|
-
*
|
|
1435
|
-
* ```typescript
|
|
1436
|
-
* const provider = client.flags.contextProvider(() => [...]);
|
|
1437
|
-
* ```
|
|
1438
1451
|
*/
|
|
1439
1452
|
contextProvider(fn) {
|
|
1440
1453
|
this._contextProvider = fn;
|
|
1441
1454
|
return fn;
|
|
1442
1455
|
}
|
|
1443
1456
|
// ------------------------------------------------------------------
|
|
1444
|
-
// Runtime:
|
|
1457
|
+
// Runtime: initialize / disconnect / refresh
|
|
1445
1458
|
// ------------------------------------------------------------------
|
|
1446
1459
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1460
|
+
* Initialize the flags runtime: fetch definitions and wire WebSocket.
|
|
1461
|
+
*
|
|
1462
|
+
* Idempotent — safe to call multiple times. Must be called (and awaited)
|
|
1463
|
+
* before using `.get()` on flag handles.
|
|
1450
1464
|
*/
|
|
1451
|
-
async
|
|
1452
|
-
this.
|
|
1465
|
+
async initialize() {
|
|
1466
|
+
if (this._initialized) return;
|
|
1467
|
+
this._environment = this._parent?._environment ?? null;
|
|
1453
1468
|
await this._fetchAllFlags();
|
|
1454
|
-
this.
|
|
1469
|
+
this._initialized = true;
|
|
1455
1470
|
this._cache.clear();
|
|
1456
1471
|
this._wsManager = this._ensureWs();
|
|
1457
1472
|
this._wsManager.on("flag_changed", this._handleFlagChanged);
|
|
@@ -1467,7 +1482,7 @@ var FlagsClient = class {
|
|
|
1467
1482
|
await this._flushContexts();
|
|
1468
1483
|
this._flagStore = {};
|
|
1469
1484
|
this._cache.clear();
|
|
1470
|
-
this.
|
|
1485
|
+
this._initialized = false;
|
|
1471
1486
|
this._environment = null;
|
|
1472
1487
|
}
|
|
1473
1488
|
/** Re-fetch all flag definitions and clear cache. */
|
|
@@ -1488,22 +1503,27 @@ var FlagsClient = class {
|
|
|
1488
1503
|
return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
|
|
1489
1504
|
}
|
|
1490
1505
|
// ------------------------------------------------------------------
|
|
1491
|
-
// Runtime: change listeners
|
|
1506
|
+
// Runtime: change listeners (dual-mode)
|
|
1492
1507
|
// ------------------------------------------------------------------
|
|
1493
|
-
/** Register a global change listener that fires for any flag change. */
|
|
1494
|
-
onChangeAny(callback) {
|
|
1495
|
-
this._globalListeners.push(callback);
|
|
1496
|
-
return callback;
|
|
1497
|
-
}
|
|
1498
1508
|
/**
|
|
1499
|
-
* Register a
|
|
1509
|
+
* Register a change listener.
|
|
1500
1510
|
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
* ```
|
|
1511
|
+
* - `onChange(callback)` — fires for any flag change (global).
|
|
1512
|
+
* - `onChange(key, callback)` — fires only for the specified flag key.
|
|
1504
1513
|
*/
|
|
1505
|
-
onChange(callback) {
|
|
1506
|
-
|
|
1514
|
+
onChange(callbackOrKey, callback) {
|
|
1515
|
+
if (typeof callbackOrKey === "function") {
|
|
1516
|
+
this._globalListeners.push(callbackOrKey);
|
|
1517
|
+
} else {
|
|
1518
|
+
const key = callbackOrKey;
|
|
1519
|
+
if (!callback) {
|
|
1520
|
+
throw new SmplError("onChange(key, callback) requires a callback function.");
|
|
1521
|
+
}
|
|
1522
|
+
if (!this._keyListeners.has(key)) {
|
|
1523
|
+
this._keyListeners.set(key, []);
|
|
1524
|
+
}
|
|
1525
|
+
this._keyListeners.get(key).push(callback);
|
|
1526
|
+
}
|
|
1507
1527
|
}
|
|
1508
1528
|
// ------------------------------------------------------------------
|
|
1509
1529
|
// Runtime: context registration
|
|
@@ -1512,7 +1532,7 @@ var FlagsClient = class {
|
|
|
1512
1532
|
* Explicitly register context(s) for background batch registration.
|
|
1513
1533
|
*
|
|
1514
1534
|
* Accepts a single Context or an array. Fire-and-forget — never
|
|
1515
|
-
* blocks. Works before `
|
|
1535
|
+
* blocks. Works before `initialize()` is called.
|
|
1516
1536
|
*/
|
|
1517
1537
|
register(context) {
|
|
1518
1538
|
if (Array.isArray(context)) {
|
|
@@ -1530,8 +1550,6 @@ var FlagsClient = class {
|
|
|
1530
1550
|
// ------------------------------------------------------------------
|
|
1531
1551
|
/**
|
|
1532
1552
|
* Tier 1 explicit evaluation — stateless, no provider or cache.
|
|
1533
|
-
*
|
|
1534
|
-
* Useful for scripts, one-off jobs, and infrastructure code.
|
|
1535
1553
|
*/
|
|
1536
1554
|
async evaluate(key, options) {
|
|
1537
1555
|
const evalDict = contextsToEvalDict(options.context);
|
|
@@ -1539,7 +1557,7 @@ var FlagsClient = class {
|
|
|
1539
1557
|
evalDict["service"] = { key: this._parent._service };
|
|
1540
1558
|
}
|
|
1541
1559
|
let flagDef = null;
|
|
1542
|
-
if (this.
|
|
1560
|
+
if (this._initialized && key in this._flagStore) {
|
|
1543
1561
|
flagDef = this._flagStore[key];
|
|
1544
1562
|
} else {
|
|
1545
1563
|
const flags = await this._fetchFlagsList();
|
|
@@ -1560,8 +1578,8 @@ var FlagsClient = class {
|
|
|
1560
1578
|
// ------------------------------------------------------------------
|
|
1561
1579
|
/** @internal */
|
|
1562
1580
|
_evaluateHandle(key, defaultValue, context) {
|
|
1563
|
-
if (!this.
|
|
1564
|
-
throw new
|
|
1581
|
+
if (!this._initialized) {
|
|
1582
|
+
throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
|
|
1565
1583
|
}
|
|
1566
1584
|
let evalDict;
|
|
1567
1585
|
if (context !== null) {
|
|
@@ -1598,6 +1616,19 @@ var FlagsClient = class {
|
|
|
1598
1616
|
return value;
|
|
1599
1617
|
}
|
|
1600
1618
|
// ------------------------------------------------------------------
|
|
1619
|
+
// Internal: _connectInternal (called by SmplClient for backward compat)
|
|
1620
|
+
// ------------------------------------------------------------------
|
|
1621
|
+
/** @internal — called by SmplClient constructor / lazy init. */
|
|
1622
|
+
async _connectInternal(environment) {
|
|
1623
|
+
this._environment = environment;
|
|
1624
|
+
await this._fetchAllFlags();
|
|
1625
|
+
this._initialized = true;
|
|
1626
|
+
this._cache.clear();
|
|
1627
|
+
this._wsManager = this._ensureWs();
|
|
1628
|
+
this._wsManager.on("flag_changed", this._handleFlagChanged);
|
|
1629
|
+
this._wsManager.on("flag_deleted", this._handleFlagDeleted);
|
|
1630
|
+
}
|
|
1631
|
+
// ------------------------------------------------------------------
|
|
1601
1632
|
// Internal: event handlers (called by SharedWebSocket)
|
|
1602
1633
|
// ------------------------------------------------------------------
|
|
1603
1634
|
_handleFlagChanged = (data) => {
|
|
@@ -1649,9 +1680,9 @@ var FlagsClient = class {
|
|
|
1649
1680
|
} catch {
|
|
1650
1681
|
}
|
|
1651
1682
|
}
|
|
1652
|
-
const
|
|
1653
|
-
if (
|
|
1654
|
-
for (const cb of
|
|
1683
|
+
const keyCallbacks = this._keyListeners.get(flagKey);
|
|
1684
|
+
if (keyCallbacks) {
|
|
1685
|
+
for (const cb of keyCallbacks) {
|
|
1655
1686
|
try {
|
|
1656
1687
|
cb(event);
|
|
1657
1688
|
} catch {
|
|
@@ -1687,10 +1718,11 @@ var FlagsClient = class {
|
|
|
1687
1718
|
// ------------------------------------------------------------------
|
|
1688
1719
|
// Internal: model conversion
|
|
1689
1720
|
// ------------------------------------------------------------------
|
|
1721
|
+
/** @internal */
|
|
1690
1722
|
_resourceToModel(resource) {
|
|
1691
1723
|
const attrs = resource.attributes;
|
|
1692
1724
|
return new Flag(this, {
|
|
1693
|
-
id: resource.id ??
|
|
1725
|
+
id: resource.id ?? null,
|
|
1694
1726
|
key: attrs.key,
|
|
1695
1727
|
name: attrs.name,
|
|
1696
1728
|
type: attrs.type,
|
|
@@ -1714,13 +1746,567 @@ var FlagsClient = class {
|
|
|
1714
1746
|
environments: attrs.environments ?? {}
|
|
1715
1747
|
};
|
|
1716
1748
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// src/logging/client.ts
|
|
1752
|
+
var import_openapi_fetch3 = __toESM(require("openapi-fetch"), 1);
|
|
1753
|
+
|
|
1754
|
+
// src/logging/models.ts
|
|
1755
|
+
var Logger = class {
|
|
1756
|
+
/** UUID of the logger, or `null` if unsaved. */
|
|
1757
|
+
id;
|
|
1758
|
+
/** Unique key (dot-separated hierarchy). */
|
|
1759
|
+
key;
|
|
1760
|
+
/** Human-readable display name. */
|
|
1761
|
+
name;
|
|
1762
|
+
/** Base log level, or null if inherited. */
|
|
1763
|
+
level;
|
|
1764
|
+
/** UUID of the parent log group, or null. */
|
|
1765
|
+
group;
|
|
1766
|
+
/** Whether this logger is managed by the platform. */
|
|
1767
|
+
managed;
|
|
1768
|
+
/** Observed sources (services that report this logger). */
|
|
1769
|
+
sources;
|
|
1770
|
+
/** Per-environment level overrides. */
|
|
1771
|
+
environments;
|
|
1772
|
+
/** When the logger was created. */
|
|
1773
|
+
createdAt;
|
|
1774
|
+
/** When the logger was last updated. */
|
|
1775
|
+
updatedAt;
|
|
1776
|
+
/** @internal */
|
|
1777
|
+
_client;
|
|
1778
|
+
/** @internal */
|
|
1779
|
+
constructor(client, fields) {
|
|
1780
|
+
this._client = client;
|
|
1781
|
+
this.id = fields.id;
|
|
1782
|
+
this.key = fields.key;
|
|
1783
|
+
this.name = fields.name;
|
|
1784
|
+
this.level = fields.level;
|
|
1785
|
+
this.group = fields.group;
|
|
1786
|
+
this.managed = fields.managed;
|
|
1787
|
+
this.sources = fields.sources;
|
|
1788
|
+
this.environments = fields.environments;
|
|
1789
|
+
this.createdAt = fields.createdAt;
|
|
1790
|
+
this.updatedAt = fields.updatedAt;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Persist this logger to the server.
|
|
1794
|
+
*
|
|
1795
|
+
* POST if `id` is null (new), PUT if `id` is set (update).
|
|
1796
|
+
*/
|
|
1797
|
+
async save() {
|
|
1798
|
+
const saved = await this._client._saveLogger(this);
|
|
1799
|
+
this._apply(saved);
|
|
1800
|
+
}
|
|
1801
|
+
/** Set the base log level (sync local mutation). */
|
|
1802
|
+
setLevel(level) {
|
|
1803
|
+
this.level = level;
|
|
1804
|
+
}
|
|
1805
|
+
/** Clear the base log level (sync local mutation). */
|
|
1806
|
+
clearLevel() {
|
|
1807
|
+
this.level = null;
|
|
1808
|
+
}
|
|
1809
|
+
/** Set an environment-specific log level (sync local mutation). */
|
|
1810
|
+
setEnvironmentLevel(env, level) {
|
|
1811
|
+
const envs = { ...this.environments };
|
|
1812
|
+
envs[env] = { ...envs[env] ?? {}, level };
|
|
1813
|
+
this.environments = envs;
|
|
1814
|
+
}
|
|
1815
|
+
/** Clear an environment-specific log level (sync local mutation). */
|
|
1816
|
+
clearEnvironmentLevel(env) {
|
|
1817
|
+
const envs = { ...this.environments };
|
|
1818
|
+
if (envs[env]) {
|
|
1819
|
+
const entry = { ...envs[env] };
|
|
1820
|
+
delete entry.level;
|
|
1821
|
+
envs[env] = entry;
|
|
1822
|
+
this.environments = envs;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
/** Clear all environment-specific log levels (sync local mutation). */
|
|
1826
|
+
clearAllEnvironmentLevels() {
|
|
1827
|
+
this.environments = {};
|
|
1828
|
+
}
|
|
1829
|
+
/** @internal — copy all fields from another Logger instance. */
|
|
1830
|
+
_apply(other) {
|
|
1831
|
+
this.id = other.id;
|
|
1832
|
+
this.key = other.key;
|
|
1833
|
+
this.name = other.name;
|
|
1834
|
+
this.level = other.level;
|
|
1835
|
+
this.group = other.group;
|
|
1836
|
+
this.managed = other.managed;
|
|
1837
|
+
this.sources = other.sources;
|
|
1838
|
+
this.environments = other.environments;
|
|
1839
|
+
this.createdAt = other.createdAt;
|
|
1840
|
+
this.updatedAt = other.updatedAt;
|
|
1841
|
+
}
|
|
1842
|
+
toString() {
|
|
1843
|
+
return `Logger(key=${this.key}, level=${this.level})`;
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
var LogGroup = class {
|
|
1847
|
+
/** UUID of the log group, or `null` if unsaved. */
|
|
1848
|
+
id;
|
|
1849
|
+
/** Unique key. */
|
|
1850
|
+
key;
|
|
1851
|
+
/** Human-readable display name. */
|
|
1852
|
+
name;
|
|
1853
|
+
/** Base log level, or null if inherited. */
|
|
1854
|
+
level;
|
|
1855
|
+
/** UUID of the parent log group, or null. */
|
|
1856
|
+
group;
|
|
1857
|
+
/** Per-environment level overrides. */
|
|
1858
|
+
environments;
|
|
1859
|
+
/** When the log group was created. */
|
|
1860
|
+
createdAt;
|
|
1861
|
+
/** When the log group was last updated. */
|
|
1862
|
+
updatedAt;
|
|
1863
|
+
/** @internal */
|
|
1864
|
+
_client;
|
|
1865
|
+
/** @internal */
|
|
1866
|
+
constructor(client, fields) {
|
|
1867
|
+
this._client = client;
|
|
1868
|
+
this.id = fields.id;
|
|
1869
|
+
this.key = fields.key;
|
|
1870
|
+
this.name = fields.name;
|
|
1871
|
+
this.level = fields.level;
|
|
1872
|
+
this.group = fields.group;
|
|
1873
|
+
this.environments = fields.environments;
|
|
1874
|
+
this.createdAt = fields.createdAt;
|
|
1875
|
+
this.updatedAt = fields.updatedAt;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Persist this log group to the server.
|
|
1879
|
+
*
|
|
1880
|
+
* POST if `id` is null (new), PUT if `id` is set (update).
|
|
1881
|
+
*/
|
|
1882
|
+
async save() {
|
|
1883
|
+
const saved = await this._client._saveLogGroup(this);
|
|
1884
|
+
this._apply(saved);
|
|
1885
|
+
}
|
|
1886
|
+
/** Set the base log level (sync local mutation). */
|
|
1887
|
+
setLevel(level) {
|
|
1888
|
+
this.level = level;
|
|
1889
|
+
}
|
|
1890
|
+
/** Clear the base log level (sync local mutation). */
|
|
1891
|
+
clearLevel() {
|
|
1892
|
+
this.level = null;
|
|
1893
|
+
}
|
|
1894
|
+
/** Set an environment-specific log level (sync local mutation). */
|
|
1895
|
+
setEnvironmentLevel(env, level) {
|
|
1896
|
+
const envs = { ...this.environments };
|
|
1897
|
+
envs[env] = { ...envs[env] ?? {}, level };
|
|
1898
|
+
this.environments = envs;
|
|
1899
|
+
}
|
|
1900
|
+
/** Clear an environment-specific log level (sync local mutation). */
|
|
1901
|
+
clearEnvironmentLevel(env) {
|
|
1902
|
+
const envs = { ...this.environments };
|
|
1903
|
+
if (envs[env]) {
|
|
1904
|
+
const entry = { ...envs[env] };
|
|
1905
|
+
delete entry.level;
|
|
1906
|
+
envs[env] = entry;
|
|
1907
|
+
this.environments = envs;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
/** Clear all environment-specific log levels (sync local mutation). */
|
|
1911
|
+
clearAllEnvironmentLevels() {
|
|
1912
|
+
this.environments = {};
|
|
1913
|
+
}
|
|
1914
|
+
/** @internal — copy all fields from another LogGroup instance. */
|
|
1915
|
+
_apply(other) {
|
|
1916
|
+
this.id = other.id;
|
|
1917
|
+
this.key = other.key;
|
|
1918
|
+
this.name = other.name;
|
|
1919
|
+
this.level = other.level;
|
|
1920
|
+
this.group = other.group;
|
|
1921
|
+
this.environments = other.environments;
|
|
1922
|
+
this.createdAt = other.createdAt;
|
|
1923
|
+
this.updatedAt = other.updatedAt;
|
|
1924
|
+
}
|
|
1925
|
+
toString() {
|
|
1926
|
+
return `LogGroup(key=${this.key}, level=${this.level})`;
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/logging/client.ts
|
|
1931
|
+
var LOGGING_BASE_URL = "https://logging.smplkit.com";
|
|
1932
|
+
async function checkError3(response, _context) {
|
|
1933
|
+
const body = await response.text().catch(() => "");
|
|
1934
|
+
throwForStatus(response.status, body);
|
|
1935
|
+
}
|
|
1936
|
+
function wrapFetchError3(err) {
|
|
1937
|
+
if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
|
|
1938
|
+
throw err;
|
|
1939
|
+
}
|
|
1940
|
+
if (err instanceof TypeError) {
|
|
1941
|
+
throw new SmplConnectionError(`Network error: ${err.message}`);
|
|
1942
|
+
}
|
|
1943
|
+
throw new SmplConnectionError(
|
|
1944
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
var LoggingClient = class {
|
|
1948
|
+
/** @internal */
|
|
1949
|
+
_apiKey;
|
|
1950
|
+
/** @internal */
|
|
1951
|
+
_baseUrl = LOGGING_BASE_URL;
|
|
1952
|
+
/** @internal */
|
|
1953
|
+
_http;
|
|
1954
|
+
/** @internal — set by SmplClient after construction. */
|
|
1955
|
+
_parent = null;
|
|
1956
|
+
_ensureWs;
|
|
1957
|
+
_wsManager = null;
|
|
1958
|
+
_started = false;
|
|
1959
|
+
_globalListeners = [];
|
|
1960
|
+
_keyListeners = /* @__PURE__ */ new Map();
|
|
1961
|
+
/** @internal */
|
|
1962
|
+
constructor(apiKey, ensureWs, timeout) {
|
|
1963
|
+
this._apiKey = apiKey;
|
|
1964
|
+
this._ensureWs = ensureWs;
|
|
1965
|
+
const ms = timeout ?? 3e4;
|
|
1966
|
+
this._http = (0, import_openapi_fetch3.default)({
|
|
1967
|
+
baseUrl: LOGGING_BASE_URL,
|
|
1968
|
+
headers: {
|
|
1969
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1970
|
+
Accept: "application/json"
|
|
1971
|
+
},
|
|
1972
|
+
fetch: async (request) => {
|
|
1973
|
+
const controller = new AbortController();
|
|
1974
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
1975
|
+
try {
|
|
1976
|
+
return await fetch(new Request(request, { signal: controller.signal }));
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1979
|
+
throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
|
|
1980
|
+
}
|
|
1981
|
+
throw err;
|
|
1982
|
+
} finally {
|
|
1983
|
+
clearTimeout(timer);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
// ------------------------------------------------------------------
|
|
1989
|
+
// Management: Logger factory
|
|
1990
|
+
// ------------------------------------------------------------------
|
|
1991
|
+
/** Create an unsaved logger. Call `.save()` to persist. */
|
|
1992
|
+
new(key, options) {
|
|
1993
|
+
return new Logger(this, {
|
|
1994
|
+
id: null,
|
|
1995
|
+
key,
|
|
1996
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
1997
|
+
level: null,
|
|
1998
|
+
group: null,
|
|
1999
|
+
managed: options?.managed ?? false,
|
|
2000
|
+
sources: [],
|
|
2001
|
+
environments: {},
|
|
2002
|
+
createdAt: null,
|
|
2003
|
+
updatedAt: null
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
// ------------------------------------------------------------------
|
|
2007
|
+
// Management: Logger CRUD
|
|
2008
|
+
// ------------------------------------------------------------------
|
|
2009
|
+
/** Fetch a logger by key. */
|
|
2010
|
+
async get(key) {
|
|
2011
|
+
let data;
|
|
2012
|
+
try {
|
|
2013
|
+
const result = await this._http.GET("/api/v1/loggers", {
|
|
2014
|
+
params: { query: { "filter[key]": key } }
|
|
2015
|
+
});
|
|
2016
|
+
if (result.error !== void 0)
|
|
2017
|
+
await checkError3(result.response, `Logger with key '${key}' not found`);
|
|
2018
|
+
data = result.data;
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
wrapFetchError3(err);
|
|
2021
|
+
}
|
|
2022
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
2023
|
+
throw new SmplNotFoundError(`Logger with key '${key}' not found`);
|
|
2024
|
+
}
|
|
2025
|
+
return this._loggerToModel(data.data[0]);
|
|
2026
|
+
}
|
|
2027
|
+
/** List all loggers. */
|
|
2028
|
+
async list() {
|
|
2029
|
+
let data;
|
|
2030
|
+
try {
|
|
2031
|
+
const result = await this._http.GET("/api/v1/loggers", {});
|
|
2032
|
+
if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
|
|
2033
|
+
data = result.data;
|
|
2034
|
+
} catch (err) {
|
|
2035
|
+
wrapFetchError3(err);
|
|
2036
|
+
}
|
|
2037
|
+
if (!data) return [];
|
|
2038
|
+
return data.data.map((r) => this._loggerToModel(r));
|
|
2039
|
+
}
|
|
2040
|
+
/** Delete a logger by key. */
|
|
2041
|
+
async delete(key) {
|
|
2042
|
+
const logger = await this.get(key);
|
|
2043
|
+
try {
|
|
2044
|
+
const result = await this._http.DELETE("/api/v1/loggers/{id}", {
|
|
2045
|
+
params: { path: { id: logger.id } }
|
|
2046
|
+
});
|
|
2047
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
2048
|
+
await checkError3(result.response, `Failed to delete logger '${key}'`);
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
wrapFetchError3(err);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
// ------------------------------------------------------------------
|
|
2054
|
+
// Management: LogGroup factory
|
|
2055
|
+
// ------------------------------------------------------------------
|
|
2056
|
+
/** Create an unsaved log group. Call `.save()` to persist. */
|
|
2057
|
+
newGroup(key, options) {
|
|
2058
|
+
return new LogGroup(this, {
|
|
2059
|
+
id: null,
|
|
2060
|
+
key,
|
|
2061
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
2062
|
+
level: null,
|
|
2063
|
+
group: options?.group ?? null,
|
|
2064
|
+
environments: {},
|
|
2065
|
+
createdAt: null,
|
|
2066
|
+
updatedAt: null
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
// ------------------------------------------------------------------
|
|
2070
|
+
// Management: LogGroup CRUD
|
|
2071
|
+
// ------------------------------------------------------------------
|
|
2072
|
+
/** Fetch a log group by key. */
|
|
2073
|
+
async getGroup(key) {
|
|
2074
|
+
const groups = await this.listGroups();
|
|
2075
|
+
const match = groups.find((g) => g.key === key);
|
|
2076
|
+
if (!match) {
|
|
2077
|
+
throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
|
|
2078
|
+
}
|
|
2079
|
+
return match;
|
|
2080
|
+
}
|
|
2081
|
+
/** List all log groups. */
|
|
2082
|
+
async listGroups() {
|
|
2083
|
+
let data;
|
|
2084
|
+
try {
|
|
2085
|
+
const result = await this._http.GET("/api/v1/log_groups", {});
|
|
2086
|
+
if (result.error !== void 0)
|
|
2087
|
+
await checkError3(result.response, "Failed to list log groups");
|
|
2088
|
+
data = result.data;
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
wrapFetchError3(err);
|
|
2091
|
+
}
|
|
2092
|
+
if (!data) return [];
|
|
2093
|
+
return data.data.map((r) => this._groupToModel(r));
|
|
2094
|
+
}
|
|
2095
|
+
/** Delete a log group by key. */
|
|
2096
|
+
async deleteGroup(key) {
|
|
2097
|
+
const group = await this.getGroup(key);
|
|
2098
|
+
try {
|
|
2099
|
+
const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
|
|
2100
|
+
params: { path: { id: group.id } }
|
|
2101
|
+
});
|
|
2102
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
2103
|
+
await checkError3(result.response, `Failed to delete log group '${key}'`);
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
wrapFetchError3(err);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
// ------------------------------------------------------------------
|
|
2109
|
+
// Management: internal save methods
|
|
2110
|
+
// ------------------------------------------------------------------
|
|
2111
|
+
/** @internal — POST or PUT a logger. */
|
|
2112
|
+
async _saveLogger(logger) {
|
|
2113
|
+
const body = {
|
|
2114
|
+
data: {
|
|
2115
|
+
type: "logger",
|
|
2116
|
+
attributes: {
|
|
2117
|
+
key: logger.key,
|
|
2118
|
+
name: logger.name,
|
|
2119
|
+
level: logger.level,
|
|
2120
|
+
group: logger.group,
|
|
2121
|
+
managed: logger.managed,
|
|
2122
|
+
environments: logger.environments
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
if (logger.id === null) {
|
|
2127
|
+
let data;
|
|
2128
|
+
try {
|
|
2129
|
+
const result = await this._http.POST("/api/v1/loggers", { body });
|
|
2130
|
+
if (result.error !== void 0)
|
|
2131
|
+
await checkError3(result.response, "Failed to create logger");
|
|
2132
|
+
data = result.data;
|
|
2133
|
+
} catch (err) {
|
|
2134
|
+
wrapFetchError3(err);
|
|
2135
|
+
}
|
|
2136
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
|
|
2137
|
+
return this._loggerToModel(data.data);
|
|
2138
|
+
} else {
|
|
2139
|
+
let data;
|
|
2140
|
+
try {
|
|
2141
|
+
const result = await this._http.PUT("/api/v1/loggers/{id}", {
|
|
2142
|
+
params: { path: { id: logger.id } },
|
|
2143
|
+
body
|
|
2144
|
+
});
|
|
2145
|
+
if (result.error !== void 0)
|
|
2146
|
+
await checkError3(result.response, `Failed to update logger ${logger.id}`);
|
|
2147
|
+
data = result.data;
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
wrapFetchError3(err);
|
|
2150
|
+
}
|
|
2151
|
+
if (!data || !data.data)
|
|
2152
|
+
throw new SmplValidationError(`Failed to update logger ${logger.id}`);
|
|
2153
|
+
return this._loggerToModel(data.data);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
/** @internal — POST or PUT a log group. */
|
|
2157
|
+
async _saveLogGroup(group) {
|
|
2158
|
+
const body = {
|
|
2159
|
+
data: {
|
|
2160
|
+
type: "log_group",
|
|
2161
|
+
attributes: {
|
|
2162
|
+
key: group.key,
|
|
2163
|
+
name: group.name,
|
|
2164
|
+
level: group.level,
|
|
2165
|
+
group: group.group,
|
|
2166
|
+
environments: group.environments
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
if (group.id === null) {
|
|
2171
|
+
let data;
|
|
2172
|
+
try {
|
|
2173
|
+
const result = await this._http.POST("/api/v1/log_groups", { body });
|
|
2174
|
+
if (result.error !== void 0)
|
|
2175
|
+
await checkError3(result.response, "Failed to create log group");
|
|
2176
|
+
data = result.data;
|
|
2177
|
+
} catch (err) {
|
|
2178
|
+
wrapFetchError3(err);
|
|
2179
|
+
}
|
|
2180
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
|
|
2181
|
+
return this._groupToModel(data.data);
|
|
2182
|
+
} else {
|
|
2183
|
+
let data;
|
|
2184
|
+
try {
|
|
2185
|
+
const result = await this._http.PUT("/api/v1/log_groups/{id}", {
|
|
2186
|
+
params: { path: { id: group.id } },
|
|
2187
|
+
body
|
|
2188
|
+
});
|
|
2189
|
+
if (result.error !== void 0)
|
|
2190
|
+
await checkError3(result.response, `Failed to update log group ${group.id}`);
|
|
2191
|
+
data = result.data;
|
|
2192
|
+
} catch (err) {
|
|
2193
|
+
wrapFetchError3(err);
|
|
2194
|
+
}
|
|
2195
|
+
if (!data || !data.data)
|
|
2196
|
+
throw new SmplValidationError(`Failed to update log group ${group.id}`);
|
|
2197
|
+
return this._groupToModel(data.data);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
// ------------------------------------------------------------------
|
|
2201
|
+
// Runtime: start (scaffolded)
|
|
2202
|
+
// ------------------------------------------------------------------
|
|
2203
|
+
/**
|
|
2204
|
+
* Start the logging runtime.
|
|
2205
|
+
*
|
|
2206
|
+
* Fetches existing loggers/groups and wires WebSocket listeners for
|
|
2207
|
+
* live updates. Idempotent — safe to call multiple times.
|
|
2208
|
+
*
|
|
2209
|
+
* Note: Node.js auto-discovery (equivalent to Python's logging module
|
|
2210
|
+
* monkey-patching) is deferred. Management methods work without start().
|
|
2211
|
+
*/
|
|
2212
|
+
async start() {
|
|
2213
|
+
if (this._started) return;
|
|
2214
|
+
this._wsManager = this._ensureWs();
|
|
2215
|
+
this._wsManager.on("logger_changed", this._handleLoggerChanged);
|
|
2216
|
+
this._started = true;
|
|
2217
|
+
}
|
|
2218
|
+
// ------------------------------------------------------------------
|
|
2219
|
+
// Runtime: change listeners (dual-mode)
|
|
2220
|
+
// ------------------------------------------------------------------
|
|
2221
|
+
/**
|
|
2222
|
+
* Register a change listener.
|
|
2223
|
+
*
|
|
2224
|
+
* - `onChange(callback)` — fires for any logger change (global).
|
|
2225
|
+
* - `onChange(key, callback)` — fires only for the specified logger key.
|
|
2226
|
+
*/
|
|
2227
|
+
onChange(callbackOrKey, callback) {
|
|
2228
|
+
if (typeof callbackOrKey === "function") {
|
|
2229
|
+
this._globalListeners.push(callbackOrKey);
|
|
2230
|
+
} else {
|
|
2231
|
+
const key = callbackOrKey;
|
|
2232
|
+
if (!callback) {
|
|
2233
|
+
throw new SmplError("onChange(key, callback) requires a callback function.");
|
|
2234
|
+
}
|
|
2235
|
+
if (!this._keyListeners.has(key)) {
|
|
2236
|
+
this._keyListeners.set(key, []);
|
|
2237
|
+
}
|
|
2238
|
+
this._keyListeners.get(key).push(callback);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
// ------------------------------------------------------------------
|
|
2242
|
+
// Internal: close
|
|
2243
|
+
// ------------------------------------------------------------------
|
|
2244
|
+
/** @internal */
|
|
2245
|
+
_close() {
|
|
2246
|
+
if (this._wsManager !== null) {
|
|
2247
|
+
this._wsManager.off("logger_changed", this._handleLoggerChanged);
|
|
2248
|
+
this._wsManager = null;
|
|
2249
|
+
}
|
|
2250
|
+
this._started = false;
|
|
2251
|
+
}
|
|
2252
|
+
// ------------------------------------------------------------------
|
|
2253
|
+
// Internal: WebSocket handler
|
|
2254
|
+
// ------------------------------------------------------------------
|
|
2255
|
+
_handleLoggerChanged = (data) => {
|
|
2256
|
+
const key = data.key;
|
|
2257
|
+
if (key) {
|
|
2258
|
+
const level = data.level ?? null;
|
|
2259
|
+
const event = {
|
|
2260
|
+
key,
|
|
2261
|
+
level,
|
|
2262
|
+
source: "websocket"
|
|
2263
|
+
};
|
|
2264
|
+
for (const cb of this._globalListeners) {
|
|
2265
|
+
try {
|
|
2266
|
+
cb(event);
|
|
2267
|
+
} catch {
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
const keyCallbacks = this._keyListeners.get(key);
|
|
2271
|
+
if (keyCallbacks) {
|
|
2272
|
+
for (const cb of keyCallbacks) {
|
|
2273
|
+
try {
|
|
2274
|
+
cb(event);
|
|
2275
|
+
} catch {
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
// ------------------------------------------------------------------
|
|
2282
|
+
// Internal: model conversion
|
|
2283
|
+
// ------------------------------------------------------------------
|
|
2284
|
+
_loggerToModel(resource) {
|
|
2285
|
+
const attrs = resource.attributes;
|
|
2286
|
+
return new Logger(this, {
|
|
2287
|
+
id: resource.id ?? null,
|
|
1721
2288
|
key: attrs.key ?? "",
|
|
1722
|
-
name: attrs.name
|
|
1723
|
-
|
|
2289
|
+
name: attrs.name,
|
|
2290
|
+
level: attrs.level ?? null,
|
|
2291
|
+
group: attrs.group ?? null,
|
|
2292
|
+
managed: attrs.managed ?? false,
|
|
2293
|
+
sources: attrs.sources ?? [],
|
|
2294
|
+
environments: attrs.environments ?? {},
|
|
2295
|
+
createdAt: attrs.created_at ?? null,
|
|
2296
|
+
updatedAt: attrs.updated_at ?? null
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
_groupToModel(resource) {
|
|
2300
|
+
const attrs = resource.attributes;
|
|
2301
|
+
return new LogGroup(this, {
|
|
2302
|
+
id: resource.id ?? null,
|
|
2303
|
+
key: attrs.key ?? "",
|
|
2304
|
+
name: attrs.name,
|
|
2305
|
+
level: attrs.level ?? null,
|
|
2306
|
+
group: attrs.group ?? null,
|
|
2307
|
+
environments: attrs.environments ?? {},
|
|
2308
|
+
createdAt: attrs.created_at ?? null,
|
|
2309
|
+
updatedAt: attrs.updated_at ?? null
|
|
1724
2310
|
});
|
|
1725
2311
|
}
|
|
1726
2312
|
};
|
|
@@ -1936,17 +2522,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
|
|
|
1936
2522
|
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";
|
|
1937
2523
|
var NO_SERVICE_MESSAGE = "No service provided. Set one of:\n 1. Pass service in options\n 2. Set the SMPLKIT_SERVICE environment variable";
|
|
1938
2524
|
var SmplClient = class {
|
|
1939
|
-
/** Client for config management
|
|
2525
|
+
/** Client for config management and runtime. */
|
|
1940
2526
|
config;
|
|
1941
|
-
/** Client for flags management and runtime
|
|
2527
|
+
/** Client for flags management and runtime. */
|
|
1942
2528
|
flags;
|
|
2529
|
+
/** Client for logging management and runtime. */
|
|
2530
|
+
logging;
|
|
1943
2531
|
_wsManager = null;
|
|
1944
2532
|
_apiKey;
|
|
1945
2533
|
/** @internal */
|
|
1946
2534
|
_environment;
|
|
1947
2535
|
/** @internal */
|
|
1948
2536
|
_service;
|
|
1949
|
-
_connected = false;
|
|
1950
2537
|
_timeout;
|
|
1951
2538
|
_appHttp;
|
|
1952
2539
|
constructor(options = {}) {
|
|
@@ -1964,7 +2551,7 @@ var SmplClient = class {
|
|
|
1964
2551
|
this._apiKey = apiKey;
|
|
1965
2552
|
this._timeout = options.timeout ?? 3e4;
|
|
1966
2553
|
const ms = this._timeout;
|
|
1967
|
-
this._appHttp = (0,
|
|
2554
|
+
this._appHttp = (0, import_openapi_fetch4.default)({
|
|
1968
2555
|
baseUrl: APP_BASE_URL2,
|
|
1969
2556
|
headers: {
|
|
1970
2557
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -1987,24 +2574,12 @@ var SmplClient = class {
|
|
|
1987
2574
|
});
|
|
1988
2575
|
this.config = new ConfigClient(apiKey, this._timeout);
|
|
1989
2576
|
this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
2577
|
+
this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
1990
2578
|
this.config._getSharedWs = () => this._ensureWs();
|
|
1991
2579
|
this.flags._parent = this;
|
|
1992
2580
|
this.config._parent = this;
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
* Connect to the smplkit platform.
|
|
1996
|
-
*
|
|
1997
|
-
* Fetches initial flag and config data, opens the shared WebSocket,
|
|
1998
|
-
* and registers the service as a context instance (if provided).
|
|
1999
|
-
*
|
|
2000
|
-
* This method is idempotent — calling it multiple times is safe.
|
|
2001
|
-
*/
|
|
2002
|
-
async connect() {
|
|
2003
|
-
if (this._connected) return;
|
|
2004
|
-
await this._registerServiceContext();
|
|
2005
|
-
await this.flags._connectInternal(this._environment);
|
|
2006
|
-
await this.config._connectInternal(this._environment);
|
|
2007
|
-
this._connected = true;
|
|
2581
|
+
this.logging._parent = this;
|
|
2582
|
+
void this._registerServiceContext();
|
|
2008
2583
|
}
|
|
2009
2584
|
/** @internal */
|
|
2010
2585
|
async _registerServiceContext() {
|
|
@@ -2033,6 +2608,7 @@ var SmplClient = class {
|
|
|
2033
2608
|
}
|
|
2034
2609
|
/** Close the shared WebSocket and release resources. */
|
|
2035
2610
|
close() {
|
|
2611
|
+
this.logging._close();
|
|
2036
2612
|
if (this._wsManager !== null) {
|
|
2037
2613
|
this._wsManager.stop();
|
|
2038
2614
|
this._wsManager = null;
|
|
@@ -2104,29 +2680,44 @@ var Rule = class {
|
|
|
2104
2680
|
return result;
|
|
2105
2681
|
}
|
|
2106
2682
|
};
|
|
2683
|
+
|
|
2684
|
+
// src/logging/types.ts
|
|
2685
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
2686
|
+
LogLevel2["TRACE"] = "TRACE";
|
|
2687
|
+
LogLevel2["DEBUG"] = "DEBUG";
|
|
2688
|
+
LogLevel2["INFO"] = "INFO";
|
|
2689
|
+
LogLevel2["WARN"] = "WARN";
|
|
2690
|
+
LogLevel2["ERROR"] = "ERROR";
|
|
2691
|
+
LogLevel2["FATAL"] = "FATAL";
|
|
2692
|
+
LogLevel2["SILENT"] = "SILENT";
|
|
2693
|
+
return LogLevel2;
|
|
2694
|
+
})(LogLevel || {});
|
|
2107
2695
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2108
2696
|
0 && (module.exports = {
|
|
2109
|
-
|
|
2697
|
+
BooleanFlag,
|
|
2110
2698
|
Config,
|
|
2111
2699
|
ConfigClient,
|
|
2112
2700
|
Context,
|
|
2113
|
-
ContextType,
|
|
2114
2701
|
Flag,
|
|
2115
2702
|
FlagChangeEvent,
|
|
2116
2703
|
FlagStats,
|
|
2117
2704
|
FlagsClient,
|
|
2118
|
-
|
|
2119
|
-
|
|
2705
|
+
JsonFlag,
|
|
2706
|
+
LiveConfigProxy,
|
|
2707
|
+
LogGroup,
|
|
2708
|
+
LogLevel,
|
|
2709
|
+
Logger,
|
|
2710
|
+
LoggingClient,
|
|
2711
|
+
NumberFlag,
|
|
2120
2712
|
Rule,
|
|
2121
2713
|
SharedWebSocket,
|
|
2122
2714
|
SmplClient,
|
|
2123
2715
|
SmplConflictError,
|
|
2124
2716
|
SmplConnectionError,
|
|
2125
2717
|
SmplError,
|
|
2126
|
-
SmplNotConnectedError,
|
|
2127
2718
|
SmplNotFoundError,
|
|
2128
2719
|
SmplTimeoutError,
|
|
2129
2720
|
SmplValidationError,
|
|
2130
|
-
|
|
2721
|
+
StringFlag
|
|
2131
2722
|
});
|
|
2132
2723
|
//# sourceMappingURL=index.cjs.map
|