@smplkit/sdk 3.0.1 → 3.0.3

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 CHANGED
@@ -16903,6 +16903,13 @@ function environmentsToWire(environments) {
16903
16903
  }
16904
16904
  return out;
16905
16905
  }
16906
+ function environmentsForResolver(environments) {
16907
+ const out = {};
16908
+ for (const [envId, env] of Object.entries(environments)) {
16909
+ out[envId] = { values: env.values };
16910
+ }
16911
+ return out;
16912
+ }
16906
16913
  var Config = class {
16907
16914
  id;
16908
16915
  name;
@@ -17092,8 +17099,8 @@ var Config = class {
17092
17099
  const chain = [
17093
17100
  {
17094
17101
  id: this.id,
17095
- items: this._itemsRaw,
17096
- environments: environmentsToWire(this._environments)
17102
+ items: this.items,
17103
+ environments: environmentsForResolver(this._environments)
17097
17104
  }
17098
17105
  ];
17099
17106
  let current = this;
@@ -17115,8 +17122,8 @@ var Config = class {
17115
17122
  }
17116
17123
  chain.push({
17117
17124
  id: parentConfig.id,
17118
- items: parentConfig._itemsRaw,
17119
- environments: environmentsToWire(parentConfig._environments)
17125
+ items: parentConfig.items,
17126
+ environments: environmentsForResolver(parentConfig._environments)
17120
17127
  });
17121
17128
  current = parentConfig;
17122
17129
  }
@@ -17128,6 +17135,22 @@ var Config = class {
17128
17135
  };
17129
17136
 
17130
17137
  // src/config/proxy.ts
17138
+ function _unflattenDotNotation(flat) {
17139
+ const nested = {};
17140
+ for (const [key, value] of Object.entries(flat)) {
17141
+ const parts = key.split(".");
17142
+ let current = nested;
17143
+ for (let i = 0; i < parts.length - 1; i++) {
17144
+ const part = parts[i];
17145
+ if (current[part] === void 0 || typeof current[part] !== "object" || current[part] === null) {
17146
+ current[part] = {};
17147
+ }
17148
+ current = current[part];
17149
+ }
17150
+ current[parts[parts.length - 1]] = value;
17151
+ }
17152
+ return nested;
17153
+ }
17131
17154
  var LiveConfigProxy = class {
17132
17155
  /** @internal */
17133
17156
  _client;
@@ -17150,7 +17173,8 @@ var LiveConfigProxy = class {
17150
17173
  }
17151
17174
  const values = target._currentValues();
17152
17175
  if (target._model) {
17153
- const instance = new target._model(values);
17176
+ const nested = _unflattenDotNotation(values);
17177
+ const instance = new target._model(nested);
17154
17178
  return instance[prop];
17155
17179
  }
17156
17180
  return values[prop];
@@ -17312,6 +17336,11 @@ var ConfigClient = class {
17312
17336
  /** @internal — resolves the management config sub-client used by lazy-init/refresh. */
17313
17337
  _resolveManagement;
17314
17338
  _configCache = {};
17339
+ /** Raw Config objects keyed by id, kept around so a single-config
17340
+ * change (WS event) can refetch one config and rebuild the resolved
17341
+ * cache for everyone (including descendants that inherit from it)
17342
+ * without a full re-list. Mirrors Python's `_raw_config_cache`. */
17343
+ _configStore = {};
17315
17344
  _initialized = false;
17316
17345
  _listeners = [];
17317
17346
  /** @internal */
@@ -17420,12 +17449,15 @@ var ConfigClient = class {
17420
17449
  }
17421
17450
  const configs = await this._listConfigs();
17422
17451
  const newCache = {};
17452
+ const newStore = {};
17423
17453
  for (const cfg of configs) {
17424
17454
  const chain = await cfg._buildChain(configs);
17425
17455
  newCache[cfg.id] = resolveChain(chain, environment);
17456
+ newStore[cfg.id] = cfg;
17426
17457
  }
17427
17458
  const oldCache = this._configCache;
17428
17459
  this._configCache = newCache;
17460
+ this._configStore = newStore;
17429
17461
  this._diffAndFire(oldCache, newCache, "manual");
17430
17462
  }
17431
17463
  /**
@@ -17467,11 +17499,14 @@ var ConfigClient = class {
17467
17499
  }
17468
17500
  const configs = await this._listConfigs();
17469
17501
  const cache = {};
17502
+ const store = {};
17470
17503
  for (const cfg of configs) {
17471
17504
  const chain = await cfg._buildChain(configs);
17472
17505
  cache[cfg.id] = resolveChain(chain, environment);
17506
+ store[cfg.id] = cfg;
17473
17507
  }
17474
17508
  this._configCache = cache;
17509
+ this._configStore = store;
17475
17510
  this._initialized = true;
17476
17511
  if (this._getSharedWs) {
17477
17512
  const ws = this._getSharedWs();
@@ -17485,11 +17520,14 @@ var ConfigClient = class {
17485
17520
  if (this._initialized) return;
17486
17521
  const configs = await this._listConfigs();
17487
17522
  const cache = {};
17523
+ const store = {};
17488
17524
  for (const cfg of configs) {
17489
17525
  const chain = await cfg._buildChain(configs);
17490
17526
  cache[cfg.id] = resolveChain(chain, environment);
17527
+ store[cfg.id] = cfg;
17491
17528
  }
17492
17529
  this._configCache = cache;
17530
+ this._configStore = store;
17493
17531
  this._initialized = true;
17494
17532
  }
17495
17533
  /** @internal — get resolved config from cache. Used by LiveConfigProxy. */
@@ -17503,26 +17541,29 @@ var ConfigClient = class {
17503
17541
  debug("websocket", `config_changed event received: ${JSON.stringify(data)}`);
17504
17542
  const configKey = data.id;
17505
17543
  if (!configKey) return;
17544
+ const environment = this._parent?._environment;
17545
+ if (!environment) return;
17506
17546
  void this._fetchSingleConfig(configKey).then((newConfig) => {
17507
- const environment = this._parent?._environment;
17508
- if (!environment) return;
17509
- const oldValues = this._configCache[configKey];
17510
- let newValues;
17511
- if (newConfig !== null) {
17512
- newValues = this._resolveConfigValues(newConfig, environment);
17513
- } else {
17514
- newValues = {};
17515
- }
17516
- const oldJson = JSON.stringify(oldValues ?? {});
17517
- const newJson = JSON.stringify(newValues);
17518
- if (oldJson === newJson) return;
17519
- const oldCache = { ...this._configCache };
17520
- if (newConfig !== null) {
17521
- this._configCache[configKey] = newValues;
17547
+ const newStore = { ...this._configStore };
17548
+ if (newConfig === null) {
17549
+ delete newStore[configKey];
17522
17550
  } else {
17523
- delete this._configCache[configKey];
17524
- }
17525
- this._diffAndFire(oldCache, this._configCache, "websocket");
17551
+ newStore[configKey] = newConfig;
17552
+ }
17553
+ const allConfigs = Object.values(newStore);
17554
+ return Promise.all(
17555
+ allConfigs.map(async (cfg) => {
17556
+ const chain = await cfg._buildChain(allConfigs);
17557
+ return [cfg.id, resolveChain(chain, environment)];
17558
+ })
17559
+ ).then((entries) => ({ entries, newStore }));
17560
+ }).then(({ entries, newStore }) => {
17561
+ const newCache = {};
17562
+ for (const [id, values] of entries) newCache[id] = values;
17563
+ const oldCache = this._configCache;
17564
+ this._configCache = newCache;
17565
+ this._configStore = newStore;
17566
+ this._diffAndFire(oldCache, newCache, "websocket");
17526
17567
  }).catch((err) => {
17527
17568
  debug(
17528
17569
  "websocket",
@@ -17530,6 +17571,20 @@ var ConfigClient = class {
17530
17571
  );
17531
17572
  });
17532
17573
  };
17574
+ /** Fetch a single config by key. Returns null if not found. @internal */
17575
+ async _fetchSingleConfig(key) {
17576
+ debug("api", `GET /api/v1/configs/${key}`);
17577
+ try {
17578
+ const result = await this._http.GET("/api/v1/configs/{id}", {
17579
+ params: { path: { id: key } }
17580
+ });
17581
+ if (!result.response.ok) return null;
17582
+ if (!result.data?.data) return null;
17583
+ return resourceToConfig(result.data.data);
17584
+ } catch {
17585
+ return null;
17586
+ }
17587
+ }
17533
17588
  _handleConfigDeleted = (data) => {
17534
17589
  debug("websocket", `config_deleted event received: ${JSON.stringify(data)}`);
17535
17590
  const configKey = data.id;
@@ -17548,27 +17603,6 @@ var ConfigClient = class {
17548
17603
  // ------------------------------------------------------------------
17549
17604
  // Internal: change detection
17550
17605
  // ------------------------------------------------------------------
17551
- /** Fetch a single config by key. Returns null if not found. @internal */
17552
- async _fetchSingleConfig(key) {
17553
- debug("api", `GET /api/v1/configs/${key}`);
17554
- try {
17555
- const result = await this._http.GET("/api/v1/configs/{id}", {
17556
- params: { path: { id: key } }
17557
- });
17558
- if (!result.response.ok) return null;
17559
- if (!result.data?.data) return null;
17560
- return resourceToConfig(result.data.data);
17561
- } catch {
17562
- return null;
17563
- }
17564
- }
17565
- /** Resolve a config's values for an environment (no parent chain). @internal */
17566
- _resolveConfigValues(config, environment) {
17567
- const base = config.items ?? {};
17568
- const envEntry = config.environments?.[environment];
17569
- if (!envEntry?.values) return { ...base };
17570
- return { ...base, ...envEntry.values };
17571
- }
17572
17606
  /** @internal */
17573
17607
  _diffAndFire(oldCache, newCache, source) {
17574
17608
  const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
@@ -20521,6 +20555,30 @@ var FlagsClient = class {
20521
20555
  this._flagFlushTimer = setInterval(() => {
20522
20556
  void this._flushFlags();
20523
20557
  }, FLAG_REGISTRATION_FLUSH_INTERVAL_MS);
20558
+ if (typeof this._flagFlushTimer === "object" && "unref" in this._flagFlushTimer) {
20559
+ this._flagFlushTimer.unref();
20560
+ }
20561
+ }
20562
+ /**
20563
+ * Synchronous teardown — clears the flag-flush interval and unsubscribes
20564
+ * from WebSocket events. Called by `SmplClient.close()`. Does not flush
20565
+ * pending context observations (use {@link disconnect} for that).
20566
+ * @internal
20567
+ */
20568
+ _close() {
20569
+ if (this._flagFlushTimer !== null) {
20570
+ clearInterval(this._flagFlushTimer);
20571
+ this._flagFlushTimer = null;
20572
+ }
20573
+ if (this._wsManager !== null) {
20574
+ this._wsManager.off("flag_changed", this._handleFlagChanged);
20575
+ this._wsManager.off("flag_deleted", this._handleFlagDeleted);
20576
+ this._wsManager.off("flags_changed", this._handleFlagsChanged);
20577
+ this._wsManager = null;
20578
+ }
20579
+ this._cache.clear();
20580
+ this._initialized = false;
20581
+ this._environment = null;
20524
20582
  }
20525
20583
  /** Disconnect the flags runtime and release resources. */
20526
20584
  async disconnect() {
@@ -20677,6 +20735,9 @@ var FlagsClient = class {
20677
20735
  this._flagFlushTimer = setInterval(() => {
20678
20736
  void this._flushFlags();
20679
20737
  }, FLAG_REGISTRATION_FLUSH_INTERVAL_MS);
20738
+ if (typeof this._flagFlushTimer === "object" && "unref" in this._flagFlushTimer) {
20739
+ this._flagFlushTimer.unref();
20740
+ }
20680
20741
  }
20681
20742
  // ------------------------------------------------------------------
20682
20743
  // Internal: event handlers (called by SharedWebSocket)
@@ -21024,10 +21085,39 @@ var LoggingClient = class {
21024
21085
  * `install()`.
21025
21086
  *
21026
21087
  * Mirrors Python's `client.logging.install()`. There is no `stop()`.
21088
+ *
21089
+ * Adapter coverage:
21090
+ * - **winston**: pre-existing named loggers (`winston.loggers.*`) and the
21091
+ * default logger are auto-discovered.
21092
+ * - **pino**: pino has no global registry, so only loggers created
21093
+ * through `pino()` / `logger.child()` *after* `install()` runs are
21094
+ * tracked. To bring pre-existing pino loggers under management, recreate
21095
+ * them after install or register them explicitly via
21096
+ * `client.manage.loggers.register([...])`.
21097
+ *
21098
+ * After the initial pass, call {@link refresh} to re-fetch managed levels
21099
+ * from the server and re-apply them onto the native loggers (e.g. after
21100
+ * suspecting drift, or to force a manual sync outside the WebSocket).
21027
21101
  */
21028
21102
  async install() {
21029
21103
  return this._installInternal();
21030
21104
  }
21105
+ /**
21106
+ * Re-fetch logger and group levels from the server and re-apply them
21107
+ * onto the registered adapters.
21108
+ *
21109
+ * Diff-based: change listeners only fire for loggers whose level
21110
+ * actually changed (added, removed, or different level), with
21111
+ * `source: "manual"`. Mirrors Python's `client.logging.refresh()`.
21112
+ *
21113
+ * @throws SmplError if `install()` has not been called.
21114
+ */
21115
+ async refresh() {
21116
+ if (!this._started) {
21117
+ throw new SmplError("Logging not installed. Call install() first.");
21118
+ }
21119
+ await this._resolveAndFire("manual");
21120
+ }
21031
21121
  /**
21032
21122
  * @deprecated Use {@link LoggingClient.install}. Retained as a backwards-
21033
21123
  * compatible alias.
@@ -21100,6 +21190,9 @@ var LoggingClient = class {
21100
21190
  this._loggerFlushTimer = setInterval(() => {
21101
21191
  void this._flushLoggerBuffer();
21102
21192
  }, 3e4);
21193
+ if (typeof this._loggerFlushTimer === "object" && "unref" in this._loggerFlushTimer) {
21194
+ this._loggerFlushTimer.unref();
21195
+ }
21103
21196
  this._started = true;
21104
21197
  }
21105
21198
  // ------------------------------------------------------------------
@@ -21371,63 +21464,75 @@ var LoggingClient = class {
21371
21464
  };
21372
21465
  _handleLoggersChanged = (_data) => {
21373
21466
  debug("websocket", `loggers_changed event received`);
21374
- void Promise.all([this._listLoggers(), this._listLogGroups()]).then(([serverLoggers, serverGroups]) => {
21375
- debug("resolution", `resolution pass (trigger: loggers_changed event)`);
21376
- const changedLoggerIds = /* @__PURE__ */ new Set();
21377
- const newLoggerKeys = new Set(serverLoggers.map((l) => l.id));
21378
- for (const logger of serverLoggers) {
21379
- const key = logger.id;
21380
- const oldLevel = this._loggerStore[key] ?? null;
21381
- const newLevel = logger.level ?? null;
21382
- if (oldLevel !== newLevel || !(key in this._loggerStore)) {
21383
- changedLoggerIds.add(key);
21384
- this._loggerStore[key] = newLevel;
21385
- }
21386
- }
21387
- for (const key of Object.keys(this._loggerStore)) {
21388
- if (!newLoggerKeys.has(key)) {
21389
- changedLoggerIds.add(key);
21390
- delete this._loggerStore[key];
21391
- }
21392
- }
21393
- for (const group of serverGroups) {
21394
- this._groupStore[group.id] = group.level ?? null;
21395
- }
21396
- this._applyLevels(serverLoggers);
21397
- if (changedLoggerIds.size === 0) return;
21398
- const [firstKey] = changedLoggerIds;
21399
- const firstLogger = serverLoggers.find((l) => l.id === firstKey);
21400
- const globalEvent = new LoggerChangeEvent({
21401
- id: firstKey,
21402
- level: firstLogger?.level ?? null,
21403
- source: "websocket"
21404
- });
21405
- for (const cb of this._globalListeners) {
21406
- try {
21407
- cb(globalEvent);
21408
- } catch {
21409
- }
21467
+ void this._resolveAndFire("websocket").catch(() => {
21468
+ });
21469
+ };
21470
+ /**
21471
+ * Full refetch of loggers + log_groups, apply resolved levels to
21472
+ * adapters, diff against local stores and fire change listeners
21473
+ * (global once, per-key for each changed id). Shared between the
21474
+ * `loggers_changed` WS handler and the public `refresh()` method.
21475
+ * @internal
21476
+ */
21477
+ async _resolveAndFire(source) {
21478
+ const [serverLoggers, serverGroups] = await Promise.all([
21479
+ this._listLoggers(),
21480
+ this._listLogGroups()
21481
+ ]);
21482
+ debug("resolution", `resolution pass (trigger: ${source})`);
21483
+ const changedLoggerIds = /* @__PURE__ */ new Set();
21484
+ const newLoggerKeys = new Set(serverLoggers.map((l) => l.id));
21485
+ for (const logger of serverLoggers) {
21486
+ const key = logger.id;
21487
+ const oldLevel = this._loggerStore[key] ?? null;
21488
+ const newLevel = logger.level ?? null;
21489
+ if (oldLevel !== newLevel || !(key in this._loggerStore)) {
21490
+ changedLoggerIds.add(key);
21491
+ this._loggerStore[key] = newLevel;
21492
+ }
21493
+ }
21494
+ for (const key of Object.keys(this._loggerStore)) {
21495
+ if (!newLoggerKeys.has(key)) {
21496
+ changedLoggerIds.add(key);
21497
+ delete this._loggerStore[key];
21498
+ }
21499
+ }
21500
+ for (const group of serverGroups) {
21501
+ this._groupStore[group.id] = group.level ?? null;
21502
+ }
21503
+ this._applyLevels(serverLoggers);
21504
+ if (changedLoggerIds.size === 0) return;
21505
+ const [firstKey] = changedLoggerIds;
21506
+ const firstLogger = serverLoggers.find((l) => l.id === firstKey);
21507
+ const globalEvent = new LoggerChangeEvent({
21508
+ id: firstKey,
21509
+ level: firstLogger?.level ?? null,
21510
+ source
21511
+ });
21512
+ for (const cb of this._globalListeners) {
21513
+ try {
21514
+ cb(globalEvent);
21515
+ } catch {
21410
21516
  }
21411
- for (const key of changedLoggerIds) {
21412
- const keyCallbacks = this._keyListeners.get(key);
21413
- if (keyCallbacks) {
21414
- const l = serverLoggers.find((x) => x.id === key);
21415
- const keyEvent = new LoggerChangeEvent({
21416
- id: key,
21417
- level: l?.level ?? null,
21418
- source: "websocket"
21419
- });
21420
- for (const cb of keyCallbacks) {
21421
- try {
21422
- cb(keyEvent);
21423
- } catch {
21424
- }
21517
+ }
21518
+ for (const key of changedLoggerIds) {
21519
+ const keyCallbacks = this._keyListeners.get(key);
21520
+ if (keyCallbacks) {
21521
+ const l = serverLoggers.find((x) => x.id === key);
21522
+ const keyEvent = new LoggerChangeEvent({
21523
+ id: key,
21524
+ level: l?.level ?? null,
21525
+ source
21526
+ });
21527
+ for (const cb of keyCallbacks) {
21528
+ try {
21529
+ cb(keyEvent);
21530
+ } catch {
21425
21531
  }
21426
21532
  }
21427
21533
  }
21428
- }).catch(() => {
21429
- });
21430
- };
21534
+ }
21535
+ }
21431
21536
  // ------------------------------------------------------------------
21432
21537
  // Internal: single-resource fetchers
21433
21538
  // ------------------------------------------------------------------
@@ -21970,6 +22075,7 @@ var SmplClient = class {
21970
22075
  if (this._metrics !== null) {
21971
22076
  this._metrics.close();
21972
22077
  }
22078
+ this.flags._close();
21973
22079
  this.logging._close();
21974
22080
  if (this._wsManager !== null) {
21975
22081
  this._wsManager.stop();