@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.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,7 @@ var ConfigClient = class {
|
|
|
469
464
|
/** @internal — set by SmplClient after construction. */
|
|
470
465
|
_parent = null;
|
|
471
466
|
_configCache = {};
|
|
472
|
-
|
|
467
|
+
_initialized = false;
|
|
473
468
|
_listeners = [];
|
|
474
469
|
/** @internal */
|
|
475
470
|
constructor(apiKey, timeout) {
|
|
@@ -481,7 +476,6 @@ var ConfigClient = class {
|
|
|
481
476
|
Authorization: `Bearer ${apiKey}`,
|
|
482
477
|
Accept: "application/json"
|
|
483
478
|
},
|
|
484
|
-
// openapi-fetch custom fetch receives a pre-built Request object
|
|
485
479
|
fetch: async (request) => {
|
|
486
480
|
const controller = new AbortController();
|
|
487
481
|
const timer = setTimeout(() => controller.abort(), ms);
|
|
@@ -498,23 +492,31 @@ var ConfigClient = class {
|
|
|
498
492
|
}
|
|
499
493
|
});
|
|
500
494
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
495
|
+
// ------------------------------------------------------------------
|
|
496
|
+
// Management: factory method
|
|
497
|
+
// ------------------------------------------------------------------
|
|
498
|
+
/** Create an unsaved config. Call `.save()` to persist. */
|
|
499
|
+
new(key, options) {
|
|
500
|
+
return new Config(this, {
|
|
501
|
+
id: null,
|
|
502
|
+
key,
|
|
503
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
504
|
+
description: options?.description ?? null,
|
|
505
|
+
parent: options?.parent ?? null,
|
|
506
|
+
items: {},
|
|
507
|
+
environments: {},
|
|
508
|
+
createdAt: null,
|
|
509
|
+
updatedAt: null
|
|
510
|
+
});
|
|
514
511
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
512
|
+
// ------------------------------------------------------------------
|
|
513
|
+
// Management: CRUD
|
|
514
|
+
// ------------------------------------------------------------------
|
|
515
|
+
/** Fetch a config by key. */
|
|
516
|
+
async get(key) {
|
|
517
|
+
return this._getByKey(key);
|
|
518
|
+
}
|
|
519
|
+
/** List all configs. */
|
|
518
520
|
async list() {
|
|
519
521
|
let data;
|
|
520
522
|
try {
|
|
@@ -527,18 +529,31 @@ var ConfigClient = class {
|
|
|
527
529
|
if (!data) return [];
|
|
528
530
|
return data.data.map((r) => resourceToConfig(r, this));
|
|
529
531
|
}
|
|
530
|
-
/**
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
532
|
+
/** Delete a config by key. */
|
|
533
|
+
async delete(key) {
|
|
534
|
+
const config = await this.get(key);
|
|
535
|
+
try {
|
|
536
|
+
const result = await this._http.DELETE("/api/v1/configs/{id}", {
|
|
537
|
+
params: { path: { id: config.id } }
|
|
538
|
+
});
|
|
539
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
540
|
+
await checkError(result.response, `Failed to delete config '${key}'`);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
wrapFetchError(err);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// ------------------------------------------------------------------
|
|
546
|
+
// Management: internal save methods (called by Config.save())
|
|
547
|
+
// ------------------------------------------------------------------
|
|
548
|
+
/** @internal — POST a new config. */
|
|
549
|
+
async _createConfig(config) {
|
|
536
550
|
const body = buildRequestBody({
|
|
537
|
-
name:
|
|
538
|
-
key:
|
|
539
|
-
description:
|
|
540
|
-
parent:
|
|
541
|
-
items:
|
|
551
|
+
name: config.name,
|
|
552
|
+
key: config.key,
|
|
553
|
+
description: config.description,
|
|
554
|
+
parent: config.parent,
|
|
555
|
+
items: config.items,
|
|
556
|
+
environments: config.environments
|
|
542
557
|
});
|
|
543
558
|
let data;
|
|
544
559
|
try {
|
|
@@ -551,98 +566,124 @@ var ConfigClient = class {
|
|
|
551
566
|
if (!data || !data.data) throw new SmplValidationError("Failed to create config");
|
|
552
567
|
return resourceToConfig(data.data, this);
|
|
553
568
|
}
|
|
554
|
-
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
569
|
+
/** @internal — PUT a config update. */
|
|
570
|
+
async _updateConfig(config) {
|
|
571
|
+
const body = buildRequestBody({
|
|
572
|
+
id: config.id,
|
|
573
|
+
name: config.name,
|
|
574
|
+
key: config.key,
|
|
575
|
+
description: config.description,
|
|
576
|
+
parent: config.parent,
|
|
577
|
+
items: config.items,
|
|
578
|
+
environments: config.environments
|
|
579
|
+
});
|
|
580
|
+
let data;
|
|
561
581
|
try {
|
|
562
|
-
const result = await this._http.
|
|
563
|
-
params: { path: { id:
|
|
582
|
+
const result = await this._http.PUT("/api/v1/configs/{id}", {
|
|
583
|
+
params: { path: { id: config.id } },
|
|
584
|
+
body
|
|
564
585
|
});
|
|
565
|
-
if (result.error !== void 0
|
|
566
|
-
await checkError(result.response, `Failed to
|
|
586
|
+
if (result.error !== void 0)
|
|
587
|
+
await checkError(result.response, `Failed to update config ${config.id}`);
|
|
588
|
+
data = result.data;
|
|
567
589
|
} catch (err) {
|
|
568
590
|
wrapFetchError(err);
|
|
569
591
|
}
|
|
592
|
+
if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
|
|
593
|
+
return resourceToConfig(data.data, this);
|
|
570
594
|
}
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
595
|
+
/** @internal — fetch a config by UUID. */
|
|
596
|
+
async _getById(configId) {
|
|
597
|
+
let data;
|
|
598
|
+
try {
|
|
599
|
+
const result = await this._http.GET("/api/v1/configs/{id}", {
|
|
600
|
+
params: { path: { id: configId } }
|
|
601
|
+
});
|
|
602
|
+
if (result.error !== void 0)
|
|
603
|
+
await checkError(result.response, `Config ${configId} not found`);
|
|
604
|
+
data = result.data;
|
|
605
|
+
} catch (err) {
|
|
606
|
+
wrapFetchError(err);
|
|
581
607
|
}
|
|
582
|
-
|
|
583
|
-
|
|
608
|
+
if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
|
|
609
|
+
return resourceToConfig(data.data, this);
|
|
584
610
|
}
|
|
611
|
+
// ------------------------------------------------------------------
|
|
612
|
+
// Runtime: resolve and subscribe
|
|
613
|
+
// ------------------------------------------------------------------
|
|
585
614
|
/**
|
|
586
|
-
*
|
|
615
|
+
* Resolve a config's values for the current environment.
|
|
587
616
|
*
|
|
588
|
-
*
|
|
617
|
+
* Returns a flat dict of resolved key-value pairs, walking the
|
|
618
|
+
* parent chain and applying environment overrides.
|
|
589
619
|
*
|
|
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.
|
|
620
|
+
* Optionally pass a model class to map the resolved values.
|
|
595
621
|
*/
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (resolved === void 0) {
|
|
602
|
-
return defaultValue ?? null;
|
|
622
|
+
async resolve(key, model) {
|
|
623
|
+
await this._ensureInitialized();
|
|
624
|
+
const values = this._configCache[key];
|
|
625
|
+
if (values === void 0) {
|
|
626
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
|
|
603
627
|
}
|
|
604
|
-
if (
|
|
605
|
-
return
|
|
628
|
+
if (model) {
|
|
629
|
+
return new model(values);
|
|
606
630
|
}
|
|
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;
|
|
631
|
+
return values;
|
|
617
632
|
}
|
|
618
633
|
/**
|
|
619
|
-
*
|
|
634
|
+
* Subscribe to a config's values — returns a live proxy that
|
|
635
|
+
* auto-updates when the underlying config changes.
|
|
620
636
|
*
|
|
621
|
-
*
|
|
637
|
+
* Optionally pass a model class to map the resolved values.
|
|
622
638
|
*/
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
639
|
+
async subscribe(key, model) {
|
|
640
|
+
await this._ensureInitialized();
|
|
641
|
+
if (!(key in this._configCache)) {
|
|
642
|
+
throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
|
|
643
|
+
}
|
|
644
|
+
return new LiveConfigProxy(this, key, model);
|
|
626
645
|
}
|
|
646
|
+
// ------------------------------------------------------------------
|
|
647
|
+
// Runtime: change listeners (3-level overloads)
|
|
648
|
+
// ------------------------------------------------------------------
|
|
627
649
|
/**
|
|
628
|
-
*
|
|
650
|
+
* Register a change listener.
|
|
629
651
|
*
|
|
630
|
-
*
|
|
652
|
+
* - `onChange(callback)` — fires for any config change (global).
|
|
653
|
+
* - `onChange(configKey, callback)` — fires for changes to a specific config.
|
|
654
|
+
* - `onChange(configKey, itemKey, callback)` — fires for a specific item.
|
|
631
655
|
*/
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
656
|
+
onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
|
|
657
|
+
if (typeof callbackOrConfigKey === "function") {
|
|
658
|
+
this._listeners.push({
|
|
659
|
+
callback: callbackOrConfigKey,
|
|
660
|
+
configKey: null,
|
|
661
|
+
itemKey: null
|
|
662
|
+
});
|
|
663
|
+
} else if (typeof callbackOrItemKey === "function") {
|
|
664
|
+
this._listeners.push({
|
|
665
|
+
callback: callbackOrItemKey,
|
|
666
|
+
configKey: callbackOrConfigKey,
|
|
667
|
+
itemKey: null
|
|
668
|
+
});
|
|
669
|
+
} else if (typeof callbackOrItemKey === "string" && callback) {
|
|
670
|
+
this._listeners.push({
|
|
671
|
+
callback,
|
|
672
|
+
configKey: callbackOrConfigKey,
|
|
673
|
+
itemKey: callbackOrItemKey
|
|
674
|
+
});
|
|
675
|
+
}
|
|
635
676
|
}
|
|
677
|
+
// ------------------------------------------------------------------
|
|
678
|
+
// Runtime: refresh
|
|
679
|
+
// ------------------------------------------------------------------
|
|
636
680
|
/**
|
|
637
681
|
* 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.
|
|
682
|
+
* Fires change listeners for any values that differ.
|
|
642
683
|
*/
|
|
643
684
|
async refresh() {
|
|
644
|
-
if (!this.
|
|
645
|
-
throw new
|
|
685
|
+
if (!this._initialized) {
|
|
686
|
+
throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
|
|
646
687
|
}
|
|
647
688
|
const environment = this._parent?._environment;
|
|
648
689
|
if (!environment) {
|
|
@@ -651,27 +692,62 @@ var ConfigClient = class {
|
|
|
651
692
|
const configs = await this.list();
|
|
652
693
|
const newCache = {};
|
|
653
694
|
for (const cfg of configs) {
|
|
654
|
-
const chain = await cfg._buildChain(
|
|
695
|
+
const chain = await cfg._buildChain();
|
|
655
696
|
newCache[cfg.key] = resolveChain(chain, environment);
|
|
656
697
|
}
|
|
657
698
|
const oldCache = this._configCache;
|
|
658
699
|
this._configCache = newCache;
|
|
659
700
|
this._diffAndFire(oldCache, newCache, "manual");
|
|
660
701
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
702
|
+
// ------------------------------------------------------------------
|
|
703
|
+
// Runtime: lazy initialization
|
|
704
|
+
// ------------------------------------------------------------------
|
|
705
|
+
/** @internal */
|
|
706
|
+
async _ensureInitialized() {
|
|
707
|
+
if (this._initialized) return;
|
|
708
|
+
const environment = this._parent?._environment;
|
|
709
|
+
if (!environment) {
|
|
710
|
+
throw new SmplError("No environment set. Ensure SmplClient is configured.");
|
|
711
|
+
}
|
|
712
|
+
const configs = await this.list();
|
|
713
|
+
const cache = {};
|
|
714
|
+
for (const cfg of configs) {
|
|
715
|
+
const chain = await cfg._buildChain();
|
|
716
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
717
|
+
}
|
|
718
|
+
this._configCache = cache;
|
|
719
|
+
this._initialized = true;
|
|
720
|
+
if (this._getSharedWs) {
|
|
721
|
+
const ws = this._getSharedWs();
|
|
722
|
+
ws.on("config_changed", this._handleConfigChanged);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** @internal — called by SmplClient for backward compat. */
|
|
726
|
+
async _connectInternal(environment) {
|
|
727
|
+
if (this._initialized) return;
|
|
728
|
+
const configs = await this.list();
|
|
729
|
+
const cache = {};
|
|
730
|
+
for (const cfg of configs) {
|
|
731
|
+
const chain = await cfg._buildChain();
|
|
732
|
+
cache[cfg.key] = resolveChain(chain, environment);
|
|
733
|
+
}
|
|
734
|
+
this._configCache = cache;
|
|
735
|
+
this._initialized = true;
|
|
674
736
|
}
|
|
737
|
+
/** @internal — get resolved config from cache. Used by LiveConfigProxy. */
|
|
738
|
+
_getCachedConfig(key) {
|
|
739
|
+
return this._configCache[key];
|
|
740
|
+
}
|
|
741
|
+
// ------------------------------------------------------------------
|
|
742
|
+
// Internal: WebSocket handler
|
|
743
|
+
// ------------------------------------------------------------------
|
|
744
|
+
_handleConfigChanged = (_data) => {
|
|
745
|
+
void this.refresh().catch(() => {
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
// ------------------------------------------------------------------
|
|
749
|
+
// Internal: change detection
|
|
750
|
+
// ------------------------------------------------------------------
|
|
675
751
|
/** @internal */
|
|
676
752
|
_diffAndFire(oldCache, newCache, source) {
|
|
677
753
|
const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
|
|
@@ -702,54 +778,9 @@ var ConfigClient = class {
|
|
|
702
778
|
}
|
|
703
779
|
}
|
|
704
780
|
}
|
|
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
|
-
}
|
|
781
|
+
// ------------------------------------------------------------------
|
|
782
|
+
// Internal: fetch by key
|
|
783
|
+
// ------------------------------------------------------------------
|
|
753
784
|
async _getByKey(key) {
|
|
754
785
|
let data;
|
|
755
786
|
try {
|
|
@@ -774,7 +805,7 @@ var import_openapi_fetch2 = __toESM(require("openapi-fetch"), 1);
|
|
|
774
805
|
|
|
775
806
|
// src/flags/models.ts
|
|
776
807
|
var Flag = class {
|
|
777
|
-
/** UUID of the flag. */
|
|
808
|
+
/** UUID of the flag, or `null` if unsaved. */
|
|
778
809
|
id;
|
|
779
810
|
/** Unique key within the account. */
|
|
780
811
|
key;
|
|
@@ -811,37 +842,35 @@ var Flag = class {
|
|
|
811
842
|
this.updatedAt = fields.updatedAt;
|
|
812
843
|
}
|
|
813
844
|
/**
|
|
814
|
-
*
|
|
845
|
+
* Persist this flag to the server.
|
|
815
846
|
*
|
|
816
|
-
*
|
|
847
|
+
* POST if `id` is null (new flag), PUT if `id` is set (update).
|
|
848
|
+
* Updates this instance in-place with the server response.
|
|
817
849
|
*/
|
|
818
|
-
async
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
});
|
|
827
|
-
this._apply(updated);
|
|
850
|
+
async save() {
|
|
851
|
+
if (this.id === null) {
|
|
852
|
+
const created = await this._client._createFlag(this);
|
|
853
|
+
this._apply(created);
|
|
854
|
+
} else {
|
|
855
|
+
const updated = await this._client._updateFlag(this);
|
|
856
|
+
this._apply(updated);
|
|
857
|
+
}
|
|
828
858
|
}
|
|
829
859
|
/**
|
|
830
|
-
* Add a rule to a specific environment.
|
|
860
|
+
* Add a rule to a specific environment (sync local mutation).
|
|
831
861
|
*
|
|
832
862
|
* The built rule must include an `environment` key (set via
|
|
833
|
-
* `Rule(...).environment("env_key")`).
|
|
834
|
-
*
|
|
863
|
+
* `Rule(...).environment("env_key")`). No HTTP call is made.
|
|
864
|
+
*
|
|
865
|
+
* @returns `this` for chaining.
|
|
835
866
|
*/
|
|
836
|
-
|
|
867
|
+
addRule(builtRule) {
|
|
837
868
|
const envKey = builtRule.environment;
|
|
838
869
|
if (!envKey) {
|
|
839
870
|
throw new Error(
|
|
840
871
|
`Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
|
|
841
872
|
);
|
|
842
873
|
}
|
|
843
|
-
const current = await this._client.get(this.id);
|
|
844
|
-
this._apply(current);
|
|
845
874
|
const envs = { ...this.environments };
|
|
846
875
|
const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
|
|
847
876
|
const rules = [...envData.rules ?? []];
|
|
@@ -849,13 +878,43 @@ var Flag = class {
|
|
|
849
878
|
rules.push(ruleCopy);
|
|
850
879
|
envData.rules = rules;
|
|
851
880
|
envs[envKey] = envData;
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
environments: envs
|
|
855
|
-
});
|
|
856
|
-
this._apply(updated);
|
|
881
|
+
this.environments = envs;
|
|
882
|
+
return this;
|
|
857
883
|
}
|
|
858
|
-
/**
|
|
884
|
+
/** Enable or disable a flag in a specific environment (sync local mutation). */
|
|
885
|
+
setEnvironmentEnabled(envKey, enabled) {
|
|
886
|
+
const envs = { ...this.environments };
|
|
887
|
+
const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
|
|
888
|
+
envData.enabled = enabled;
|
|
889
|
+
envs[envKey] = envData;
|
|
890
|
+
this.environments = envs;
|
|
891
|
+
}
|
|
892
|
+
/** Set the default value for a specific environment (sync local mutation). */
|
|
893
|
+
setEnvironmentDefault(envKey, defaultValue) {
|
|
894
|
+
const envs = { ...this.environments };
|
|
895
|
+
const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
|
|
896
|
+
envData.default = defaultValue;
|
|
897
|
+
envs[envKey] = envData;
|
|
898
|
+
this.environments = envs;
|
|
899
|
+
}
|
|
900
|
+
/** Clear all rules for a specific environment (sync local mutation). */
|
|
901
|
+
clearRules(envKey) {
|
|
902
|
+
const envs = { ...this.environments };
|
|
903
|
+
const envData = envs[envKey];
|
|
904
|
+
if (envData) {
|
|
905
|
+
envs[envKey] = { ...envData, rules: [] };
|
|
906
|
+
this.environments = envs;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Evaluate the flag locally (sync, no HTTP).
|
|
911
|
+
*
|
|
912
|
+
* Requires `initialize()` to have been called.
|
|
913
|
+
*/
|
|
914
|
+
get(options) {
|
|
915
|
+
return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
916
|
+
}
|
|
917
|
+
/** @internal — copy all fields from another Flag instance. */
|
|
859
918
|
_apply(other) {
|
|
860
919
|
this.id = other.id;
|
|
861
920
|
this.key = other.key;
|
|
@@ -872,36 +931,53 @@ var Flag = class {
|
|
|
872
931
|
return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
|
|
873
932
|
}
|
|
874
933
|
};
|
|
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;
|
|
934
|
+
var BooleanFlag = class extends Flag {
|
|
935
|
+
get(options) {
|
|
936
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
937
|
+
if (typeof value === "boolean") {
|
|
938
|
+
return value;
|
|
939
|
+
}
|
|
940
|
+
return this.default;
|
|
889
941
|
}
|
|
890
|
-
|
|
891
|
-
|
|
942
|
+
};
|
|
943
|
+
var StringFlag = class extends Flag {
|
|
944
|
+
get(options) {
|
|
945
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
946
|
+
if (typeof value === "string") {
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
return this.default;
|
|
892
950
|
}
|
|
893
951
|
};
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
952
|
+
var NumberFlag = class extends Flag {
|
|
953
|
+
get(options) {
|
|
954
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
955
|
+
if (typeof value === "number") {
|
|
956
|
+
return value;
|
|
957
|
+
}
|
|
958
|
+
return this.default;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
var JsonFlag = class extends Flag {
|
|
962
|
+
get(options) {
|
|
963
|
+
const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
|
|
964
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
967
|
+
return this.default;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// src/flags/client.ts
|
|
972
|
+
var import_json_logic_js = __toESM(require("json-logic-js"), 1);
|
|
973
|
+
var FLAGS_BASE_URL = "https://flags.smplkit.com";
|
|
974
|
+
var APP_BASE_URL = "https://app.smplkit.com";
|
|
975
|
+
var CACHE_MAX_SIZE = 1e4;
|
|
976
|
+
var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
|
|
977
|
+
var CONTEXT_BATCH_FLUSH_SIZE = 100;
|
|
978
|
+
async function checkError2(response, _context) {
|
|
979
|
+
const body = await response.text().catch(() => "");
|
|
980
|
+
throwForStatus(response.status, body);
|
|
905
981
|
}
|
|
906
982
|
function wrapFetchError2(err) {
|
|
907
983
|
if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
|
|
@@ -1017,88 +1093,6 @@ var FlagStats = class {
|
|
|
1017
1093
|
this.cacheMisses = cacheMisses;
|
|
1018
1094
|
}
|
|
1019
1095
|
};
|
|
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
1096
|
var ContextRegistrationBuffer = class {
|
|
1103
1097
|
_seen = /* @__PURE__ */ new Map();
|
|
1104
1098
|
_pending = [];
|
|
@@ -1142,13 +1136,14 @@ var FlagsClient = class {
|
|
|
1142
1136
|
// Runtime state
|
|
1143
1137
|
_environment = null;
|
|
1144
1138
|
_flagStore = {};
|
|
1145
|
-
|
|
1139
|
+
_initialized = false;
|
|
1146
1140
|
_cache = new ResolutionCache();
|
|
1147
1141
|
_contextProvider = null;
|
|
1148
1142
|
_contextBuffer = new ContextRegistrationBuffer();
|
|
1149
1143
|
_handles = {};
|
|
1150
1144
|
_globalListeners = [];
|
|
1151
|
-
|
|
1145
|
+
_keyListeners = /* @__PURE__ */ new Map();
|
|
1146
|
+
// Shared WebSocket (set during initialize)
|
|
1152
1147
|
_wsManager = null;
|
|
1153
1148
|
_ensureWs;
|
|
1154
1149
|
/** @internal — set by SmplClient after construction. */
|
|
@@ -1190,55 +1185,91 @@ var FlagsClient = class {
|
|
|
1190
1185
|
});
|
|
1191
1186
|
}
|
|
1192
1187
|
// ------------------------------------------------------------------
|
|
1193
|
-
// Management methods
|
|
1188
|
+
// Management: factory methods (return unsaved flags)
|
|
1194
1189
|
// ------------------------------------------------------------------
|
|
1195
|
-
/** Create
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1190
|
+
/** Create an unsaved boolean flag. Call `.save()` to persist. */
|
|
1191
|
+
newBooleanFlag(key, options) {
|
|
1192
|
+
return new BooleanFlag(this, {
|
|
1193
|
+
id: null,
|
|
1194
|
+
key,
|
|
1195
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1196
|
+
type: "BOOLEAN",
|
|
1197
|
+
default: options.default,
|
|
1198
|
+
values: [
|
|
1200
1199
|
{ name: "True", value: true },
|
|
1201
1200
|
{ 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
|
-
wrapFetchError2(err);
|
|
1224
|
-
}
|
|
1225
|
-
if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
|
|
1226
|
-
return this._resourceToModel(data.data);
|
|
1201
|
+
],
|
|
1202
|
+
description: options.description ?? null,
|
|
1203
|
+
environments: {},
|
|
1204
|
+
createdAt: null,
|
|
1205
|
+
updatedAt: null
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
/** Create an unsaved string flag. Call `.save()` to persist. */
|
|
1209
|
+
newStringFlag(key, options) {
|
|
1210
|
+
return new StringFlag(this, {
|
|
1211
|
+
id: null,
|
|
1212
|
+
key,
|
|
1213
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1214
|
+
type: "STRING",
|
|
1215
|
+
default: options.default,
|
|
1216
|
+
values: options.values ?? [],
|
|
1217
|
+
description: options.description ?? null,
|
|
1218
|
+
environments: {},
|
|
1219
|
+
createdAt: null,
|
|
1220
|
+
updatedAt: null
|
|
1221
|
+
});
|
|
1227
1222
|
}
|
|
1228
|
-
/**
|
|
1229
|
-
|
|
1223
|
+
/** Create an unsaved number flag. Call `.save()` to persist. */
|
|
1224
|
+
newNumberFlag(key, options) {
|
|
1225
|
+
return new NumberFlag(this, {
|
|
1226
|
+
id: null,
|
|
1227
|
+
key,
|
|
1228
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1229
|
+
type: "NUMERIC",
|
|
1230
|
+
default: options.default,
|
|
1231
|
+
values: options.values ?? [],
|
|
1232
|
+
description: options.description ?? null,
|
|
1233
|
+
environments: {},
|
|
1234
|
+
createdAt: null,
|
|
1235
|
+
updatedAt: null
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
/** Create an unsaved JSON flag. Call `.save()` to persist. */
|
|
1239
|
+
newJsonFlag(key, options) {
|
|
1240
|
+
return new JsonFlag(this, {
|
|
1241
|
+
id: null,
|
|
1242
|
+
key,
|
|
1243
|
+
name: options.name ?? keyToDisplayName(key),
|
|
1244
|
+
type: "JSON",
|
|
1245
|
+
default: options.default,
|
|
1246
|
+
values: options.values ?? [],
|
|
1247
|
+
description: options.description ?? null,
|
|
1248
|
+
environments: {},
|
|
1249
|
+
createdAt: null,
|
|
1250
|
+
updatedAt: null
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
// ------------------------------------------------------------------
|
|
1254
|
+
// Management: CRUD
|
|
1255
|
+
// ------------------------------------------------------------------
|
|
1256
|
+
/** Fetch a flag by key. */
|
|
1257
|
+
async get(key) {
|
|
1230
1258
|
let data;
|
|
1231
1259
|
try {
|
|
1232
|
-
const result = await this._http.GET("/api/v1/flags
|
|
1233
|
-
params: {
|
|
1260
|
+
const result = await this._http.GET("/api/v1/flags", {
|
|
1261
|
+
params: { query: { "filter[key]": key } }
|
|
1234
1262
|
});
|
|
1235
|
-
if (result.error !== void 0)
|
|
1263
|
+
if (result.error !== void 0)
|
|
1264
|
+
await checkError2(result.response, `Flag with key '${key}' not found`);
|
|
1236
1265
|
data = result.data;
|
|
1237
1266
|
} catch (err) {
|
|
1238
1267
|
wrapFetchError2(err);
|
|
1239
1268
|
}
|
|
1240
|
-
if (!data || !data.data
|
|
1241
|
-
|
|
1269
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
1270
|
+
throw new SmplNotFoundError(`Flag with key '${key}' not found`);
|
|
1271
|
+
}
|
|
1272
|
+
return this._resourceToModel(data.data[0]);
|
|
1242
1273
|
}
|
|
1243
1274
|
/** List all flags. */
|
|
1244
1275
|
async list() {
|
|
@@ -1253,161 +1284,148 @@ var FlagsClient = class {
|
|
|
1253
1284
|
if (!data) return [];
|
|
1254
1285
|
return data.data.map((r) => this._resourceToModel(r));
|
|
1255
1286
|
}
|
|
1256
|
-
/** Delete a flag by
|
|
1257
|
-
async delete(
|
|
1287
|
+
/** Delete a flag by key. */
|
|
1288
|
+
async delete(key) {
|
|
1289
|
+
const flag = await this.get(key);
|
|
1258
1290
|
try {
|
|
1259
1291
|
const result = await this._http.DELETE("/api/v1/flags/{id}", {
|
|
1260
|
-
params: { path: { id:
|
|
1292
|
+
params: { path: { id: flag.id } }
|
|
1261
1293
|
});
|
|
1262
1294
|
if (result.error !== void 0 && result.response.status !== 204)
|
|
1263
|
-
await checkError2(result.response, `Failed to delete flag ${
|
|
1295
|
+
await checkError2(result.response, `Failed to delete flag '${key}'`);
|
|
1264
1296
|
} catch (err) {
|
|
1265
1297
|
wrapFetchError2(err);
|
|
1266
1298
|
}
|
|
1267
1299
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
async _updateFlag(options) {
|
|
1274
|
-
const { flag } = options;
|
|
1300
|
+
// ------------------------------------------------------------------
|
|
1301
|
+
// Management: internal save methods (called by Flag.save())
|
|
1302
|
+
// ------------------------------------------------------------------
|
|
1303
|
+
/** @internal — POST a new flag. */
|
|
1304
|
+
async _createFlag(flag) {
|
|
1275
1305
|
const body = {
|
|
1276
1306
|
data: {
|
|
1277
1307
|
type: "flag",
|
|
1278
1308
|
attributes: {
|
|
1279
1309
|
key: flag.key,
|
|
1280
|
-
name:
|
|
1310
|
+
name: flag.name,
|
|
1311
|
+
description: flag.description ?? "",
|
|
1281
1312
|
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 } : {}
|
|
1313
|
+
default: flag.default,
|
|
1314
|
+
values: flag.values,
|
|
1315
|
+
...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1286
1316
|
}
|
|
1287
1317
|
}
|
|
1288
1318
|
};
|
|
1289
1319
|
let data;
|
|
1290
1320
|
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}`);
|
|
1321
|
+
const result = await this._http.POST("/api/v1/flags", { body });
|
|
1322
|
+
if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
|
|
1297
1323
|
data = result.data;
|
|
1298
1324
|
} catch (err) {
|
|
1299
1325
|
wrapFetchError2(err);
|
|
1300
1326
|
}
|
|
1301
|
-
if (!data || !data.data) throw new SmplValidationError(
|
|
1327
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
|
|
1302
1328
|
return this._resourceToModel(data.data);
|
|
1303
1329
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1330
|
+
/** @internal — PUT a flag update. */
|
|
1331
|
+
async _updateFlag(flag) {
|
|
1332
|
+
const body = {
|
|
1333
|
+
data: {
|
|
1334
|
+
type: "flag",
|
|
1335
|
+
attributes: {
|
|
1336
|
+
key: flag.key,
|
|
1337
|
+
name: flag.name,
|
|
1338
|
+
type: flag.type,
|
|
1339
|
+
default: flag.default,
|
|
1340
|
+
values: flag.values,
|
|
1341
|
+
description: flag.description ?? "",
|
|
1342
|
+
...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
|
|
1314
1343
|
}
|
|
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) {
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1327
1346
|
let data;
|
|
1328
1347
|
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
|
-
}
|
|
1348
|
+
const result = await this._http.PUT("/api/v1/flags/{id}", {
|
|
1349
|
+
params: { path: { id: flag.id } },
|
|
1350
|
+
body
|
|
1337
1351
|
});
|
|
1338
1352
|
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");
|
|
1353
|
+
await checkError2(result.response, `Failed to update flag ${flag.id}`);
|
|
1381
1354
|
data = result.data;
|
|
1382
1355
|
} catch (err) {
|
|
1383
1356
|
wrapFetchError2(err);
|
|
1384
1357
|
}
|
|
1385
|
-
|
|
1358
|
+
if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
|
|
1359
|
+
return this._resourceToModel(data.data);
|
|
1386
1360
|
}
|
|
1387
1361
|
// ------------------------------------------------------------------
|
|
1388
1362
|
// Runtime: typed flag handles
|
|
1389
1363
|
// ------------------------------------------------------------------
|
|
1390
|
-
/** Declare a boolean flag handle. */
|
|
1391
|
-
|
|
1392
|
-
const handle = new
|
|
1364
|
+
/** Declare a boolean flag handle for runtime evaluation. */
|
|
1365
|
+
booleanFlag(key, defaultValue) {
|
|
1366
|
+
const handle = new BooleanFlag(this, {
|
|
1367
|
+
id: null,
|
|
1368
|
+
key,
|
|
1369
|
+
name: key,
|
|
1370
|
+
type: "BOOLEAN",
|
|
1371
|
+
default: defaultValue,
|
|
1372
|
+
values: [],
|
|
1373
|
+
description: null,
|
|
1374
|
+
environments: {},
|
|
1375
|
+
createdAt: null,
|
|
1376
|
+
updatedAt: null
|
|
1377
|
+
});
|
|
1393
1378
|
this._handles[key] = handle;
|
|
1394
1379
|
return handle;
|
|
1395
1380
|
}
|
|
1396
|
-
/** Declare a string flag handle. */
|
|
1381
|
+
/** Declare a string flag handle for runtime evaluation. */
|
|
1397
1382
|
stringFlag(key, defaultValue) {
|
|
1398
|
-
const handle = new
|
|
1383
|
+
const handle = new StringFlag(this, {
|
|
1384
|
+
id: null,
|
|
1385
|
+
key,
|
|
1386
|
+
name: key,
|
|
1387
|
+
type: "STRING",
|
|
1388
|
+
default: defaultValue,
|
|
1389
|
+
values: [],
|
|
1390
|
+
description: null,
|
|
1391
|
+
environments: {},
|
|
1392
|
+
createdAt: null,
|
|
1393
|
+
updatedAt: null
|
|
1394
|
+
});
|
|
1399
1395
|
this._handles[key] = handle;
|
|
1400
1396
|
return handle;
|
|
1401
1397
|
}
|
|
1402
|
-
/** Declare a numeric flag handle. */
|
|
1398
|
+
/** Declare a numeric flag handle for runtime evaluation. */
|
|
1403
1399
|
numberFlag(key, defaultValue) {
|
|
1404
|
-
const handle = new
|
|
1400
|
+
const handle = new NumberFlag(this, {
|
|
1401
|
+
id: null,
|
|
1402
|
+
key,
|
|
1403
|
+
name: key,
|
|
1404
|
+
type: "NUMERIC",
|
|
1405
|
+
default: defaultValue,
|
|
1406
|
+
values: [],
|
|
1407
|
+
description: null,
|
|
1408
|
+
environments: {},
|
|
1409
|
+
createdAt: null,
|
|
1410
|
+
updatedAt: null
|
|
1411
|
+
});
|
|
1405
1412
|
this._handles[key] = handle;
|
|
1406
1413
|
return handle;
|
|
1407
1414
|
}
|
|
1408
|
-
/** Declare a JSON flag handle. */
|
|
1415
|
+
/** Declare a JSON flag handle for runtime evaluation. */
|
|
1409
1416
|
jsonFlag(key, defaultValue) {
|
|
1410
|
-
const handle = new
|
|
1417
|
+
const handle = new JsonFlag(this, {
|
|
1418
|
+
id: null,
|
|
1419
|
+
key,
|
|
1420
|
+
name: key,
|
|
1421
|
+
type: "JSON",
|
|
1422
|
+
default: defaultValue,
|
|
1423
|
+
values: [],
|
|
1424
|
+
description: null,
|
|
1425
|
+
environments: {},
|
|
1426
|
+
createdAt: null,
|
|
1427
|
+
updatedAt: null
|
|
1428
|
+
});
|
|
1411
1429
|
this._handles[key] = handle;
|
|
1412
1430
|
return handle;
|
|
1413
1431
|
}
|
|
@@ -1417,41 +1435,32 @@ var FlagsClient = class {
|
|
|
1417
1435
|
/**
|
|
1418
1436
|
* Register a context provider function.
|
|
1419
1437
|
*
|
|
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
|
-
* ```
|
|
1438
|
+
* Called on every `handle.get()` to supply the current evaluation context.
|
|
1428
1439
|
*/
|
|
1429
1440
|
setContextProvider(fn) {
|
|
1430
1441
|
this._contextProvider = fn;
|
|
1431
1442
|
}
|
|
1432
1443
|
/**
|
|
1433
1444
|
* Register a context provider — decorator-style alias.
|
|
1434
|
-
*
|
|
1435
|
-
* ```typescript
|
|
1436
|
-
* const provider = client.flags.contextProvider(() => [...]);
|
|
1437
|
-
* ```
|
|
1438
1445
|
*/
|
|
1439
1446
|
contextProvider(fn) {
|
|
1440
1447
|
this._contextProvider = fn;
|
|
1441
1448
|
return fn;
|
|
1442
1449
|
}
|
|
1443
1450
|
// ------------------------------------------------------------------
|
|
1444
|
-
// Runtime:
|
|
1451
|
+
// Runtime: initialize / disconnect / refresh
|
|
1445
1452
|
// ------------------------------------------------------------------
|
|
1446
1453
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1454
|
+
* Initialize the flags runtime: fetch definitions and wire WebSocket.
|
|
1455
|
+
*
|
|
1456
|
+
* Idempotent — safe to call multiple times. Must be called (and awaited)
|
|
1457
|
+
* before using `.get()` on flag handles.
|
|
1450
1458
|
*/
|
|
1451
|
-
async
|
|
1452
|
-
this.
|
|
1459
|
+
async initialize() {
|
|
1460
|
+
if (this._initialized) return;
|
|
1461
|
+
this._environment = this._parent?._environment ?? null;
|
|
1453
1462
|
await this._fetchAllFlags();
|
|
1454
|
-
this.
|
|
1463
|
+
this._initialized = true;
|
|
1455
1464
|
this._cache.clear();
|
|
1456
1465
|
this._wsManager = this._ensureWs();
|
|
1457
1466
|
this._wsManager.on("flag_changed", this._handleFlagChanged);
|
|
@@ -1467,7 +1476,7 @@ var FlagsClient = class {
|
|
|
1467
1476
|
await this._flushContexts();
|
|
1468
1477
|
this._flagStore = {};
|
|
1469
1478
|
this._cache.clear();
|
|
1470
|
-
this.
|
|
1479
|
+
this._initialized = false;
|
|
1471
1480
|
this._environment = null;
|
|
1472
1481
|
}
|
|
1473
1482
|
/** Re-fetch all flag definitions and clear cache. */
|
|
@@ -1488,22 +1497,27 @@ var FlagsClient = class {
|
|
|
1488
1497
|
return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
|
|
1489
1498
|
}
|
|
1490
1499
|
// ------------------------------------------------------------------
|
|
1491
|
-
// Runtime: change listeners
|
|
1500
|
+
// Runtime: change listeners (dual-mode)
|
|
1492
1501
|
// ------------------------------------------------------------------
|
|
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
1502
|
/**
|
|
1499
|
-
* Register a
|
|
1503
|
+
* Register a change listener.
|
|
1500
1504
|
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
* ```
|
|
1505
|
+
* - `onChange(callback)` — fires for any flag change (global).
|
|
1506
|
+
* - `onChange(key, callback)` — fires only for the specified flag key.
|
|
1504
1507
|
*/
|
|
1505
|
-
onChange(callback) {
|
|
1506
|
-
|
|
1508
|
+
onChange(callbackOrKey, callback) {
|
|
1509
|
+
if (typeof callbackOrKey === "function") {
|
|
1510
|
+
this._globalListeners.push(callbackOrKey);
|
|
1511
|
+
} else {
|
|
1512
|
+
const key = callbackOrKey;
|
|
1513
|
+
if (!callback) {
|
|
1514
|
+
throw new SmplError("onChange(key, callback) requires a callback function.");
|
|
1515
|
+
}
|
|
1516
|
+
if (!this._keyListeners.has(key)) {
|
|
1517
|
+
this._keyListeners.set(key, []);
|
|
1518
|
+
}
|
|
1519
|
+
this._keyListeners.get(key).push(callback);
|
|
1520
|
+
}
|
|
1507
1521
|
}
|
|
1508
1522
|
// ------------------------------------------------------------------
|
|
1509
1523
|
// Runtime: context registration
|
|
@@ -1512,7 +1526,7 @@ var FlagsClient = class {
|
|
|
1512
1526
|
* Explicitly register context(s) for background batch registration.
|
|
1513
1527
|
*
|
|
1514
1528
|
* Accepts a single Context or an array. Fire-and-forget — never
|
|
1515
|
-
* blocks. Works before `
|
|
1529
|
+
* blocks. Works before `initialize()` is called.
|
|
1516
1530
|
*/
|
|
1517
1531
|
register(context) {
|
|
1518
1532
|
if (Array.isArray(context)) {
|
|
@@ -1530,8 +1544,6 @@ var FlagsClient = class {
|
|
|
1530
1544
|
// ------------------------------------------------------------------
|
|
1531
1545
|
/**
|
|
1532
1546
|
* Tier 1 explicit evaluation — stateless, no provider or cache.
|
|
1533
|
-
*
|
|
1534
|
-
* Useful for scripts, one-off jobs, and infrastructure code.
|
|
1535
1547
|
*/
|
|
1536
1548
|
async evaluate(key, options) {
|
|
1537
1549
|
const evalDict = contextsToEvalDict(options.context);
|
|
@@ -1539,7 +1551,7 @@ var FlagsClient = class {
|
|
|
1539
1551
|
evalDict["service"] = { key: this._parent._service };
|
|
1540
1552
|
}
|
|
1541
1553
|
let flagDef = null;
|
|
1542
|
-
if (this.
|
|
1554
|
+
if (this._initialized && key in this._flagStore) {
|
|
1543
1555
|
flagDef = this._flagStore[key];
|
|
1544
1556
|
} else {
|
|
1545
1557
|
const flags = await this._fetchFlagsList();
|
|
@@ -1560,8 +1572,8 @@ var FlagsClient = class {
|
|
|
1560
1572
|
// ------------------------------------------------------------------
|
|
1561
1573
|
/** @internal */
|
|
1562
1574
|
_evaluateHandle(key, defaultValue, context) {
|
|
1563
|
-
if (!this.
|
|
1564
|
-
throw new
|
|
1575
|
+
if (!this._initialized) {
|
|
1576
|
+
throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
|
|
1565
1577
|
}
|
|
1566
1578
|
let evalDict;
|
|
1567
1579
|
if (context !== null) {
|
|
@@ -1598,6 +1610,19 @@ var FlagsClient = class {
|
|
|
1598
1610
|
return value;
|
|
1599
1611
|
}
|
|
1600
1612
|
// ------------------------------------------------------------------
|
|
1613
|
+
// Internal: _connectInternal (called by SmplClient for backward compat)
|
|
1614
|
+
// ------------------------------------------------------------------
|
|
1615
|
+
/** @internal — called by SmplClient constructor / lazy init. */
|
|
1616
|
+
async _connectInternal(environment) {
|
|
1617
|
+
this._environment = environment;
|
|
1618
|
+
await this._fetchAllFlags();
|
|
1619
|
+
this._initialized = true;
|
|
1620
|
+
this._cache.clear();
|
|
1621
|
+
this._wsManager = this._ensureWs();
|
|
1622
|
+
this._wsManager.on("flag_changed", this._handleFlagChanged);
|
|
1623
|
+
this._wsManager.on("flag_deleted", this._handleFlagDeleted);
|
|
1624
|
+
}
|
|
1625
|
+
// ------------------------------------------------------------------
|
|
1601
1626
|
// Internal: event handlers (called by SharedWebSocket)
|
|
1602
1627
|
// ------------------------------------------------------------------
|
|
1603
1628
|
_handleFlagChanged = (data) => {
|
|
@@ -1649,9 +1674,9 @@ var FlagsClient = class {
|
|
|
1649
1674
|
} catch {
|
|
1650
1675
|
}
|
|
1651
1676
|
}
|
|
1652
|
-
const
|
|
1653
|
-
if (
|
|
1654
|
-
for (const cb of
|
|
1677
|
+
const keyCallbacks = this._keyListeners.get(flagKey);
|
|
1678
|
+
if (keyCallbacks) {
|
|
1679
|
+
for (const cb of keyCallbacks) {
|
|
1655
1680
|
try {
|
|
1656
1681
|
cb(event);
|
|
1657
1682
|
} catch {
|
|
@@ -1687,10 +1712,11 @@ var FlagsClient = class {
|
|
|
1687
1712
|
// ------------------------------------------------------------------
|
|
1688
1713
|
// Internal: model conversion
|
|
1689
1714
|
// ------------------------------------------------------------------
|
|
1715
|
+
/** @internal */
|
|
1690
1716
|
_resourceToModel(resource) {
|
|
1691
1717
|
const attrs = resource.attributes;
|
|
1692
1718
|
return new Flag(this, {
|
|
1693
|
-
id: resource.id ??
|
|
1719
|
+
id: resource.id ?? null,
|
|
1694
1720
|
key: attrs.key,
|
|
1695
1721
|
name: attrs.name,
|
|
1696
1722
|
type: attrs.type,
|
|
@@ -1714,13 +1740,567 @@ var FlagsClient = class {
|
|
|
1714
1740
|
environments: attrs.environments ?? {}
|
|
1715
1741
|
};
|
|
1716
1742
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
// src/logging/client.ts
|
|
1746
|
+
var import_openapi_fetch3 = __toESM(require("openapi-fetch"), 1);
|
|
1747
|
+
|
|
1748
|
+
// src/logging/models.ts
|
|
1749
|
+
var Logger = class {
|
|
1750
|
+
/** UUID of the logger, or `null` if unsaved. */
|
|
1751
|
+
id;
|
|
1752
|
+
/** Unique key (dot-separated hierarchy). */
|
|
1753
|
+
key;
|
|
1754
|
+
/** Human-readable display name. */
|
|
1755
|
+
name;
|
|
1756
|
+
/** Base log level, or null if inherited. */
|
|
1757
|
+
level;
|
|
1758
|
+
/** UUID of the parent log group, or null. */
|
|
1759
|
+
group;
|
|
1760
|
+
/** Whether this logger is managed by the platform. */
|
|
1761
|
+
managed;
|
|
1762
|
+
/** Observed sources (services that report this logger). */
|
|
1763
|
+
sources;
|
|
1764
|
+
/** Per-environment level overrides. */
|
|
1765
|
+
environments;
|
|
1766
|
+
/** When the logger was created. */
|
|
1767
|
+
createdAt;
|
|
1768
|
+
/** When the logger was last updated. */
|
|
1769
|
+
updatedAt;
|
|
1770
|
+
/** @internal */
|
|
1771
|
+
_client;
|
|
1772
|
+
/** @internal */
|
|
1773
|
+
constructor(client, fields) {
|
|
1774
|
+
this._client = client;
|
|
1775
|
+
this.id = fields.id;
|
|
1776
|
+
this.key = fields.key;
|
|
1777
|
+
this.name = fields.name;
|
|
1778
|
+
this.level = fields.level;
|
|
1779
|
+
this.group = fields.group;
|
|
1780
|
+
this.managed = fields.managed;
|
|
1781
|
+
this.sources = fields.sources;
|
|
1782
|
+
this.environments = fields.environments;
|
|
1783
|
+
this.createdAt = fields.createdAt;
|
|
1784
|
+
this.updatedAt = fields.updatedAt;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Persist this logger to the server.
|
|
1788
|
+
*
|
|
1789
|
+
* POST if `id` is null (new), PUT if `id` is set (update).
|
|
1790
|
+
*/
|
|
1791
|
+
async save() {
|
|
1792
|
+
const saved = await this._client._saveLogger(this);
|
|
1793
|
+
this._apply(saved);
|
|
1794
|
+
}
|
|
1795
|
+
/** Set the base log level (sync local mutation). */
|
|
1796
|
+
setLevel(level) {
|
|
1797
|
+
this.level = level;
|
|
1798
|
+
}
|
|
1799
|
+
/** Clear the base log level (sync local mutation). */
|
|
1800
|
+
clearLevel() {
|
|
1801
|
+
this.level = null;
|
|
1802
|
+
}
|
|
1803
|
+
/** Set an environment-specific log level (sync local mutation). */
|
|
1804
|
+
setEnvironmentLevel(env, level) {
|
|
1805
|
+
const envs = { ...this.environments };
|
|
1806
|
+
envs[env] = { ...envs[env] ?? {}, level };
|
|
1807
|
+
this.environments = envs;
|
|
1808
|
+
}
|
|
1809
|
+
/** Clear an environment-specific log level (sync local mutation). */
|
|
1810
|
+
clearEnvironmentLevel(env) {
|
|
1811
|
+
const envs = { ...this.environments };
|
|
1812
|
+
if (envs[env]) {
|
|
1813
|
+
const entry = { ...envs[env] };
|
|
1814
|
+
delete entry.level;
|
|
1815
|
+
envs[env] = entry;
|
|
1816
|
+
this.environments = envs;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
/** Clear all environment-specific log levels (sync local mutation). */
|
|
1820
|
+
clearAllEnvironmentLevels() {
|
|
1821
|
+
this.environments = {};
|
|
1822
|
+
}
|
|
1823
|
+
/** @internal — copy all fields from another Logger instance. */
|
|
1824
|
+
_apply(other) {
|
|
1825
|
+
this.id = other.id;
|
|
1826
|
+
this.key = other.key;
|
|
1827
|
+
this.name = other.name;
|
|
1828
|
+
this.level = other.level;
|
|
1829
|
+
this.group = other.group;
|
|
1830
|
+
this.managed = other.managed;
|
|
1831
|
+
this.sources = other.sources;
|
|
1832
|
+
this.environments = other.environments;
|
|
1833
|
+
this.createdAt = other.createdAt;
|
|
1834
|
+
this.updatedAt = other.updatedAt;
|
|
1835
|
+
}
|
|
1836
|
+
toString() {
|
|
1837
|
+
return `Logger(key=${this.key}, level=${this.level})`;
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
var LogGroup = class {
|
|
1841
|
+
/** UUID of the log group, or `null` if unsaved. */
|
|
1842
|
+
id;
|
|
1843
|
+
/** Unique key. */
|
|
1844
|
+
key;
|
|
1845
|
+
/** Human-readable display name. */
|
|
1846
|
+
name;
|
|
1847
|
+
/** Base log level, or null if inherited. */
|
|
1848
|
+
level;
|
|
1849
|
+
/** UUID of the parent log group, or null. */
|
|
1850
|
+
group;
|
|
1851
|
+
/** Per-environment level overrides. */
|
|
1852
|
+
environments;
|
|
1853
|
+
/** When the log group was created. */
|
|
1854
|
+
createdAt;
|
|
1855
|
+
/** When the log group was last updated. */
|
|
1856
|
+
updatedAt;
|
|
1857
|
+
/** @internal */
|
|
1858
|
+
_client;
|
|
1859
|
+
/** @internal */
|
|
1860
|
+
constructor(client, fields) {
|
|
1861
|
+
this._client = client;
|
|
1862
|
+
this.id = fields.id;
|
|
1863
|
+
this.key = fields.key;
|
|
1864
|
+
this.name = fields.name;
|
|
1865
|
+
this.level = fields.level;
|
|
1866
|
+
this.group = fields.group;
|
|
1867
|
+
this.environments = fields.environments;
|
|
1868
|
+
this.createdAt = fields.createdAt;
|
|
1869
|
+
this.updatedAt = fields.updatedAt;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Persist this log group to the server.
|
|
1873
|
+
*
|
|
1874
|
+
* POST if `id` is null (new), PUT if `id` is set (update).
|
|
1875
|
+
*/
|
|
1876
|
+
async save() {
|
|
1877
|
+
const saved = await this._client._saveLogGroup(this);
|
|
1878
|
+
this._apply(saved);
|
|
1879
|
+
}
|
|
1880
|
+
/** Set the base log level (sync local mutation). */
|
|
1881
|
+
setLevel(level) {
|
|
1882
|
+
this.level = level;
|
|
1883
|
+
}
|
|
1884
|
+
/** Clear the base log level (sync local mutation). */
|
|
1885
|
+
clearLevel() {
|
|
1886
|
+
this.level = null;
|
|
1887
|
+
}
|
|
1888
|
+
/** Set an environment-specific log level (sync local mutation). */
|
|
1889
|
+
setEnvironmentLevel(env, level) {
|
|
1890
|
+
const envs = { ...this.environments };
|
|
1891
|
+
envs[env] = { ...envs[env] ?? {}, level };
|
|
1892
|
+
this.environments = envs;
|
|
1893
|
+
}
|
|
1894
|
+
/** Clear an environment-specific log level (sync local mutation). */
|
|
1895
|
+
clearEnvironmentLevel(env) {
|
|
1896
|
+
const envs = { ...this.environments };
|
|
1897
|
+
if (envs[env]) {
|
|
1898
|
+
const entry = { ...envs[env] };
|
|
1899
|
+
delete entry.level;
|
|
1900
|
+
envs[env] = entry;
|
|
1901
|
+
this.environments = envs;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
/** Clear all environment-specific log levels (sync local mutation). */
|
|
1905
|
+
clearAllEnvironmentLevels() {
|
|
1906
|
+
this.environments = {};
|
|
1907
|
+
}
|
|
1908
|
+
/** @internal — copy all fields from another LogGroup instance. */
|
|
1909
|
+
_apply(other) {
|
|
1910
|
+
this.id = other.id;
|
|
1911
|
+
this.key = other.key;
|
|
1912
|
+
this.name = other.name;
|
|
1913
|
+
this.level = other.level;
|
|
1914
|
+
this.group = other.group;
|
|
1915
|
+
this.environments = other.environments;
|
|
1916
|
+
this.createdAt = other.createdAt;
|
|
1917
|
+
this.updatedAt = other.updatedAt;
|
|
1918
|
+
}
|
|
1919
|
+
toString() {
|
|
1920
|
+
return `LogGroup(key=${this.key}, level=${this.level})`;
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
// src/logging/client.ts
|
|
1925
|
+
var LOGGING_BASE_URL = "https://logging.smplkit.com";
|
|
1926
|
+
async function checkError3(response, _context) {
|
|
1927
|
+
const body = await response.text().catch(() => "");
|
|
1928
|
+
throwForStatus(response.status, body);
|
|
1929
|
+
}
|
|
1930
|
+
function wrapFetchError3(err) {
|
|
1931
|
+
if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
|
|
1932
|
+
throw err;
|
|
1933
|
+
}
|
|
1934
|
+
if (err instanceof TypeError) {
|
|
1935
|
+
throw new SmplConnectionError(`Network error: ${err.message}`);
|
|
1936
|
+
}
|
|
1937
|
+
throw new SmplConnectionError(
|
|
1938
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
var LoggingClient = class {
|
|
1942
|
+
/** @internal */
|
|
1943
|
+
_apiKey;
|
|
1944
|
+
/** @internal */
|
|
1945
|
+
_baseUrl = LOGGING_BASE_URL;
|
|
1946
|
+
/** @internal */
|
|
1947
|
+
_http;
|
|
1948
|
+
/** @internal — set by SmplClient after construction. */
|
|
1949
|
+
_parent = null;
|
|
1950
|
+
_ensureWs;
|
|
1951
|
+
_wsManager = null;
|
|
1952
|
+
_started = false;
|
|
1953
|
+
_globalListeners = [];
|
|
1954
|
+
_keyListeners = /* @__PURE__ */ new Map();
|
|
1955
|
+
/** @internal */
|
|
1956
|
+
constructor(apiKey, ensureWs, timeout) {
|
|
1957
|
+
this._apiKey = apiKey;
|
|
1958
|
+
this._ensureWs = ensureWs;
|
|
1959
|
+
const ms = timeout ?? 3e4;
|
|
1960
|
+
this._http = (0, import_openapi_fetch3.default)({
|
|
1961
|
+
baseUrl: LOGGING_BASE_URL,
|
|
1962
|
+
headers: {
|
|
1963
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1964
|
+
Accept: "application/json"
|
|
1965
|
+
},
|
|
1966
|
+
fetch: async (request) => {
|
|
1967
|
+
const controller = new AbortController();
|
|
1968
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
1969
|
+
try {
|
|
1970
|
+
return await fetch(new Request(request, { signal: controller.signal }));
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1973
|
+
throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
|
|
1974
|
+
}
|
|
1975
|
+
throw err;
|
|
1976
|
+
} finally {
|
|
1977
|
+
clearTimeout(timer);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
// ------------------------------------------------------------------
|
|
1983
|
+
// Management: Logger factory
|
|
1984
|
+
// ------------------------------------------------------------------
|
|
1985
|
+
/** Create an unsaved logger. Call `.save()` to persist. */
|
|
1986
|
+
new(key, options) {
|
|
1987
|
+
return new Logger(this, {
|
|
1988
|
+
id: null,
|
|
1989
|
+
key,
|
|
1990
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
1991
|
+
level: null,
|
|
1992
|
+
group: null,
|
|
1993
|
+
managed: options?.managed ?? false,
|
|
1994
|
+
sources: [],
|
|
1995
|
+
environments: {},
|
|
1996
|
+
createdAt: null,
|
|
1997
|
+
updatedAt: null
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
// ------------------------------------------------------------------
|
|
2001
|
+
// Management: Logger CRUD
|
|
2002
|
+
// ------------------------------------------------------------------
|
|
2003
|
+
/** Fetch a logger by key. */
|
|
2004
|
+
async get(key) {
|
|
2005
|
+
let data;
|
|
2006
|
+
try {
|
|
2007
|
+
const result = await this._http.GET("/api/v1/loggers", {
|
|
2008
|
+
params: { query: { "filter[key]": key } }
|
|
2009
|
+
});
|
|
2010
|
+
if (result.error !== void 0)
|
|
2011
|
+
await checkError3(result.response, `Logger with key '${key}' not found`);
|
|
2012
|
+
data = result.data;
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
wrapFetchError3(err);
|
|
2015
|
+
}
|
|
2016
|
+
if (!data || !data.data || data.data.length === 0) {
|
|
2017
|
+
throw new SmplNotFoundError(`Logger with key '${key}' not found`);
|
|
2018
|
+
}
|
|
2019
|
+
return this._loggerToModel(data.data[0]);
|
|
2020
|
+
}
|
|
2021
|
+
/** List all loggers. */
|
|
2022
|
+
async list() {
|
|
2023
|
+
let data;
|
|
2024
|
+
try {
|
|
2025
|
+
const result = await this._http.GET("/api/v1/loggers", {});
|
|
2026
|
+
if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
|
|
2027
|
+
data = result.data;
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
wrapFetchError3(err);
|
|
2030
|
+
}
|
|
2031
|
+
if (!data) return [];
|
|
2032
|
+
return data.data.map((r) => this._loggerToModel(r));
|
|
2033
|
+
}
|
|
2034
|
+
/** Delete a logger by key. */
|
|
2035
|
+
async delete(key) {
|
|
2036
|
+
const logger = await this.get(key);
|
|
2037
|
+
try {
|
|
2038
|
+
const result = await this._http.DELETE("/api/v1/loggers/{id}", {
|
|
2039
|
+
params: { path: { id: logger.id } }
|
|
2040
|
+
});
|
|
2041
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
2042
|
+
await checkError3(result.response, `Failed to delete logger '${key}'`);
|
|
2043
|
+
} catch (err) {
|
|
2044
|
+
wrapFetchError3(err);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
// ------------------------------------------------------------------
|
|
2048
|
+
// Management: LogGroup factory
|
|
2049
|
+
// ------------------------------------------------------------------
|
|
2050
|
+
/** Create an unsaved log group. Call `.save()` to persist. */
|
|
2051
|
+
newGroup(key, options) {
|
|
2052
|
+
return new LogGroup(this, {
|
|
2053
|
+
id: null,
|
|
2054
|
+
key,
|
|
2055
|
+
name: options?.name ?? keyToDisplayName(key),
|
|
2056
|
+
level: null,
|
|
2057
|
+
group: options?.group ?? null,
|
|
2058
|
+
environments: {},
|
|
2059
|
+
createdAt: null,
|
|
2060
|
+
updatedAt: null
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
// ------------------------------------------------------------------
|
|
2064
|
+
// Management: LogGroup CRUD
|
|
2065
|
+
// ------------------------------------------------------------------
|
|
2066
|
+
/** Fetch a log group by key. */
|
|
2067
|
+
async getGroup(key) {
|
|
2068
|
+
const groups = await this.listGroups();
|
|
2069
|
+
const match = groups.find((g) => g.key === key);
|
|
2070
|
+
if (!match) {
|
|
2071
|
+
throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
|
|
2072
|
+
}
|
|
2073
|
+
return match;
|
|
2074
|
+
}
|
|
2075
|
+
/** List all log groups. */
|
|
2076
|
+
async listGroups() {
|
|
2077
|
+
let data;
|
|
2078
|
+
try {
|
|
2079
|
+
const result = await this._http.GET("/api/v1/log_groups", {});
|
|
2080
|
+
if (result.error !== void 0)
|
|
2081
|
+
await checkError3(result.response, "Failed to list log groups");
|
|
2082
|
+
data = result.data;
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
wrapFetchError3(err);
|
|
2085
|
+
}
|
|
2086
|
+
if (!data) return [];
|
|
2087
|
+
return data.data.map((r) => this._groupToModel(r));
|
|
2088
|
+
}
|
|
2089
|
+
/** Delete a log group by key. */
|
|
2090
|
+
async deleteGroup(key) {
|
|
2091
|
+
const group = await this.getGroup(key);
|
|
2092
|
+
try {
|
|
2093
|
+
const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
|
|
2094
|
+
params: { path: { id: group.id } }
|
|
2095
|
+
});
|
|
2096
|
+
if (result.error !== void 0 && result.response.status !== 204)
|
|
2097
|
+
await checkError3(result.response, `Failed to delete log group '${key}'`);
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
wrapFetchError3(err);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
// ------------------------------------------------------------------
|
|
2103
|
+
// Management: internal save methods
|
|
2104
|
+
// ------------------------------------------------------------------
|
|
2105
|
+
/** @internal — POST or PUT a logger. */
|
|
2106
|
+
async _saveLogger(logger) {
|
|
2107
|
+
const body = {
|
|
2108
|
+
data: {
|
|
2109
|
+
type: "logger",
|
|
2110
|
+
attributes: {
|
|
2111
|
+
key: logger.key,
|
|
2112
|
+
name: logger.name,
|
|
2113
|
+
level: logger.level,
|
|
2114
|
+
group: logger.group,
|
|
2115
|
+
managed: logger.managed,
|
|
2116
|
+
environments: logger.environments
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
if (logger.id === null) {
|
|
2121
|
+
let data;
|
|
2122
|
+
try {
|
|
2123
|
+
const result = await this._http.POST("/api/v1/loggers", { body });
|
|
2124
|
+
if (result.error !== void 0)
|
|
2125
|
+
await checkError3(result.response, "Failed to create logger");
|
|
2126
|
+
data = result.data;
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
wrapFetchError3(err);
|
|
2129
|
+
}
|
|
2130
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
|
|
2131
|
+
return this._loggerToModel(data.data);
|
|
2132
|
+
} else {
|
|
2133
|
+
let data;
|
|
2134
|
+
try {
|
|
2135
|
+
const result = await this._http.PUT("/api/v1/loggers/{id}", {
|
|
2136
|
+
params: { path: { id: logger.id } },
|
|
2137
|
+
body
|
|
2138
|
+
});
|
|
2139
|
+
if (result.error !== void 0)
|
|
2140
|
+
await checkError3(result.response, `Failed to update logger ${logger.id}`);
|
|
2141
|
+
data = result.data;
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
wrapFetchError3(err);
|
|
2144
|
+
}
|
|
2145
|
+
if (!data || !data.data)
|
|
2146
|
+
throw new SmplValidationError(`Failed to update logger ${logger.id}`);
|
|
2147
|
+
return this._loggerToModel(data.data);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
/** @internal — POST or PUT a log group. */
|
|
2151
|
+
async _saveLogGroup(group) {
|
|
2152
|
+
const body = {
|
|
2153
|
+
data: {
|
|
2154
|
+
type: "log_group",
|
|
2155
|
+
attributes: {
|
|
2156
|
+
key: group.key,
|
|
2157
|
+
name: group.name,
|
|
2158
|
+
level: group.level,
|
|
2159
|
+
group: group.group,
|
|
2160
|
+
environments: group.environments
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
if (group.id === null) {
|
|
2165
|
+
let data;
|
|
2166
|
+
try {
|
|
2167
|
+
const result = await this._http.POST("/api/v1/log_groups", { body });
|
|
2168
|
+
if (result.error !== void 0)
|
|
2169
|
+
await checkError3(result.response, "Failed to create log group");
|
|
2170
|
+
data = result.data;
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
wrapFetchError3(err);
|
|
2173
|
+
}
|
|
2174
|
+
if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
|
|
2175
|
+
return this._groupToModel(data.data);
|
|
2176
|
+
} else {
|
|
2177
|
+
let data;
|
|
2178
|
+
try {
|
|
2179
|
+
const result = await this._http.PUT("/api/v1/log_groups/{id}", {
|
|
2180
|
+
params: { path: { id: group.id } },
|
|
2181
|
+
body
|
|
2182
|
+
});
|
|
2183
|
+
if (result.error !== void 0)
|
|
2184
|
+
await checkError3(result.response, `Failed to update log group ${group.id}`);
|
|
2185
|
+
data = result.data;
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
wrapFetchError3(err);
|
|
2188
|
+
}
|
|
2189
|
+
if (!data || !data.data)
|
|
2190
|
+
throw new SmplValidationError(`Failed to update log group ${group.id}`);
|
|
2191
|
+
return this._groupToModel(data.data);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
// ------------------------------------------------------------------
|
|
2195
|
+
// Runtime: start (scaffolded)
|
|
2196
|
+
// ------------------------------------------------------------------
|
|
2197
|
+
/**
|
|
2198
|
+
* Start the logging runtime.
|
|
2199
|
+
*
|
|
2200
|
+
* Fetches existing loggers/groups and wires WebSocket listeners for
|
|
2201
|
+
* live updates. Idempotent — safe to call multiple times.
|
|
2202
|
+
*
|
|
2203
|
+
* Note: Node.js auto-discovery (equivalent to Python's logging module
|
|
2204
|
+
* monkey-patching) is deferred. Management methods work without start().
|
|
2205
|
+
*/
|
|
2206
|
+
async start() {
|
|
2207
|
+
if (this._started) return;
|
|
2208
|
+
this._wsManager = this._ensureWs();
|
|
2209
|
+
this._wsManager.on("logger_changed", this._handleLoggerChanged);
|
|
2210
|
+
this._started = true;
|
|
2211
|
+
}
|
|
2212
|
+
// ------------------------------------------------------------------
|
|
2213
|
+
// Runtime: change listeners (dual-mode)
|
|
2214
|
+
// ------------------------------------------------------------------
|
|
2215
|
+
/**
|
|
2216
|
+
* Register a change listener.
|
|
2217
|
+
*
|
|
2218
|
+
* - `onChange(callback)` — fires for any logger change (global).
|
|
2219
|
+
* - `onChange(key, callback)` — fires only for the specified logger key.
|
|
2220
|
+
*/
|
|
2221
|
+
onChange(callbackOrKey, callback) {
|
|
2222
|
+
if (typeof callbackOrKey === "function") {
|
|
2223
|
+
this._globalListeners.push(callbackOrKey);
|
|
2224
|
+
} else {
|
|
2225
|
+
const key = callbackOrKey;
|
|
2226
|
+
if (!callback) {
|
|
2227
|
+
throw new SmplError("onChange(key, callback) requires a callback function.");
|
|
2228
|
+
}
|
|
2229
|
+
if (!this._keyListeners.has(key)) {
|
|
2230
|
+
this._keyListeners.set(key, []);
|
|
2231
|
+
}
|
|
2232
|
+
this._keyListeners.get(key).push(callback);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
// ------------------------------------------------------------------
|
|
2236
|
+
// Internal: close
|
|
2237
|
+
// ------------------------------------------------------------------
|
|
2238
|
+
/** @internal */
|
|
2239
|
+
_close() {
|
|
2240
|
+
if (this._wsManager !== null) {
|
|
2241
|
+
this._wsManager.off("logger_changed", this._handleLoggerChanged);
|
|
2242
|
+
this._wsManager = null;
|
|
2243
|
+
}
|
|
2244
|
+
this._started = false;
|
|
2245
|
+
}
|
|
2246
|
+
// ------------------------------------------------------------------
|
|
2247
|
+
// Internal: WebSocket handler
|
|
2248
|
+
// ------------------------------------------------------------------
|
|
2249
|
+
_handleLoggerChanged = (data) => {
|
|
2250
|
+
const key = data.key;
|
|
2251
|
+
if (key) {
|
|
2252
|
+
const level = data.level ?? null;
|
|
2253
|
+
const event = {
|
|
2254
|
+
key,
|
|
2255
|
+
level,
|
|
2256
|
+
source: "websocket"
|
|
2257
|
+
};
|
|
2258
|
+
for (const cb of this._globalListeners) {
|
|
2259
|
+
try {
|
|
2260
|
+
cb(event);
|
|
2261
|
+
} catch {
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const keyCallbacks = this._keyListeners.get(key);
|
|
2265
|
+
if (keyCallbacks) {
|
|
2266
|
+
for (const cb of keyCallbacks) {
|
|
2267
|
+
try {
|
|
2268
|
+
cb(event);
|
|
2269
|
+
} catch {
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
// ------------------------------------------------------------------
|
|
2276
|
+
// Internal: model conversion
|
|
2277
|
+
// ------------------------------------------------------------------
|
|
2278
|
+
_loggerToModel(resource) {
|
|
2279
|
+
const attrs = resource.attributes;
|
|
2280
|
+
return new Logger(this, {
|
|
2281
|
+
id: resource.id ?? null,
|
|
1721
2282
|
key: attrs.key ?? "",
|
|
1722
|
-
name: attrs.name
|
|
1723
|
-
|
|
2283
|
+
name: attrs.name,
|
|
2284
|
+
level: attrs.level ?? null,
|
|
2285
|
+
group: attrs.group ?? null,
|
|
2286
|
+
managed: attrs.managed ?? false,
|
|
2287
|
+
sources: attrs.sources ?? [],
|
|
2288
|
+
environments: attrs.environments ?? {},
|
|
2289
|
+
createdAt: attrs.created_at ?? null,
|
|
2290
|
+
updatedAt: attrs.updated_at ?? null
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
_groupToModel(resource) {
|
|
2294
|
+
const attrs = resource.attributes;
|
|
2295
|
+
return new LogGroup(this, {
|
|
2296
|
+
id: resource.id ?? null,
|
|
2297
|
+
key: attrs.key ?? "",
|
|
2298
|
+
name: attrs.name,
|
|
2299
|
+
level: attrs.level ?? null,
|
|
2300
|
+
group: attrs.group ?? null,
|
|
2301
|
+
environments: attrs.environments ?? {},
|
|
2302
|
+
createdAt: attrs.created_at ?? null,
|
|
2303
|
+
updatedAt: attrs.updated_at ?? null
|
|
1724
2304
|
});
|
|
1725
2305
|
}
|
|
1726
2306
|
};
|
|
@@ -1936,17 +2516,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
|
|
|
1936
2516
|
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
2517
|
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
2518
|
var SmplClient = class {
|
|
1939
|
-
/** Client for config management
|
|
2519
|
+
/** Client for config management and runtime. */
|
|
1940
2520
|
config;
|
|
1941
|
-
/** Client for flags management and runtime
|
|
2521
|
+
/** Client for flags management and runtime. */
|
|
1942
2522
|
flags;
|
|
2523
|
+
/** Client for logging management and runtime. */
|
|
2524
|
+
logging;
|
|
1943
2525
|
_wsManager = null;
|
|
1944
2526
|
_apiKey;
|
|
1945
2527
|
/** @internal */
|
|
1946
2528
|
_environment;
|
|
1947
2529
|
/** @internal */
|
|
1948
2530
|
_service;
|
|
1949
|
-
_connected = false;
|
|
1950
2531
|
_timeout;
|
|
1951
2532
|
_appHttp;
|
|
1952
2533
|
constructor(options = {}) {
|
|
@@ -1963,48 +2544,21 @@ var SmplClient = class {
|
|
|
1963
2544
|
const apiKey = resolveApiKey(options.apiKey, environment);
|
|
1964
2545
|
this._apiKey = apiKey;
|
|
1965
2546
|
this._timeout = options.timeout ?? 3e4;
|
|
1966
|
-
|
|
1967
|
-
this._appHttp = (0, import_openapi_fetch3.default)({
|
|
2547
|
+
this._appHttp = (0, import_openapi_fetch4.default)({
|
|
1968
2548
|
baseUrl: APP_BASE_URL2,
|
|
1969
2549
|
headers: {
|
|
1970
2550
|
Authorization: `Bearer ${apiKey}`,
|
|
1971
2551
|
Accept: "application/json"
|
|
1972
|
-
},
|
|
1973
|
-
fetch: async (request) => {
|
|
1974
|
-
const controller = new AbortController();
|
|
1975
|
-
const timer = setTimeout(() => controller.abort(), ms);
|
|
1976
|
-
try {
|
|
1977
|
-
return await fetch(new Request(request, { signal: controller.signal }));
|
|
1978
|
-
} catch (err) {
|
|
1979
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1980
|
-
throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
|
|
1981
|
-
}
|
|
1982
|
-
throw err;
|
|
1983
|
-
} finally {
|
|
1984
|
-
clearTimeout(timer);
|
|
1985
|
-
}
|
|
1986
2552
|
}
|
|
1987
2553
|
});
|
|
1988
2554
|
this.config = new ConfigClient(apiKey, this._timeout);
|
|
1989
2555
|
this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
2556
|
+
this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
|
|
1990
2557
|
this.config._getSharedWs = () => this._ensureWs();
|
|
1991
2558
|
this.flags._parent = this;
|
|
1992
2559
|
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;
|
|
2560
|
+
this.logging._parent = this;
|
|
2561
|
+
void this._registerServiceContext();
|
|
2008
2562
|
}
|
|
2009
2563
|
/** @internal */
|
|
2010
2564
|
async _registerServiceContext() {
|
|
@@ -2033,6 +2587,7 @@ var SmplClient = class {
|
|
|
2033
2587
|
}
|
|
2034
2588
|
/** Close the shared WebSocket and release resources. */
|
|
2035
2589
|
close() {
|
|
2590
|
+
this.logging._close();
|
|
2036
2591
|
if (this._wsManager !== null) {
|
|
2037
2592
|
this._wsManager.stop();
|
|
2038
2593
|
this._wsManager = null;
|
|
@@ -2104,29 +2659,44 @@ var Rule = class {
|
|
|
2104
2659
|
return result;
|
|
2105
2660
|
}
|
|
2106
2661
|
};
|
|
2662
|
+
|
|
2663
|
+
// src/logging/types.ts
|
|
2664
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
2665
|
+
LogLevel2["TRACE"] = "TRACE";
|
|
2666
|
+
LogLevel2["DEBUG"] = "DEBUG";
|
|
2667
|
+
LogLevel2["INFO"] = "INFO";
|
|
2668
|
+
LogLevel2["WARN"] = "WARN";
|
|
2669
|
+
LogLevel2["ERROR"] = "ERROR";
|
|
2670
|
+
LogLevel2["FATAL"] = "FATAL";
|
|
2671
|
+
LogLevel2["SILENT"] = "SILENT";
|
|
2672
|
+
return LogLevel2;
|
|
2673
|
+
})(LogLevel || {});
|
|
2107
2674
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2108
2675
|
0 && (module.exports = {
|
|
2109
|
-
|
|
2676
|
+
BooleanFlag,
|
|
2110
2677
|
Config,
|
|
2111
2678
|
ConfigClient,
|
|
2112
2679
|
Context,
|
|
2113
|
-
ContextType,
|
|
2114
2680
|
Flag,
|
|
2115
2681
|
FlagChangeEvent,
|
|
2116
2682
|
FlagStats,
|
|
2117
2683
|
FlagsClient,
|
|
2118
|
-
|
|
2119
|
-
|
|
2684
|
+
JsonFlag,
|
|
2685
|
+
LiveConfigProxy,
|
|
2686
|
+
LogGroup,
|
|
2687
|
+
LogLevel,
|
|
2688
|
+
Logger,
|
|
2689
|
+
LoggingClient,
|
|
2690
|
+
NumberFlag,
|
|
2120
2691
|
Rule,
|
|
2121
2692
|
SharedWebSocket,
|
|
2122
2693
|
SmplClient,
|
|
2123
2694
|
SmplConflictError,
|
|
2124
2695
|
SmplConnectionError,
|
|
2125
2696
|
SmplError,
|
|
2126
|
-
SmplNotConnectedError,
|
|
2127
2697
|
SmplNotFoundError,
|
|
2128
2698
|
SmplTimeoutError,
|
|
2129
2699
|
SmplValidationError,
|
|
2130
|
-
|
|
2700
|
+
StringFlag
|
|
2131
2701
|
});
|
|
2132
2702
|
//# sourceMappingURL=index.cjs.map
|