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