@smplkit/sdk 3.0.77 → 3.0.79

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
@@ -17511,7 +17511,20 @@ var LiveConfigProxy = class {
17511
17511
  this._client = client;
17512
17512
  this._key = key;
17513
17513
  this._model = model;
17514
- const ownMethods = /* @__PURE__ */ new Set(["keys", "values", "items", "get", "onChange", "_currentValues"]);
17514
+ const ownMethods = /* @__PURE__ */ new Set([
17515
+ "keys",
17516
+ "values",
17517
+ "items",
17518
+ "get",
17519
+ "onChange",
17520
+ "getBool",
17521
+ "getInt",
17522
+ "getFloat",
17523
+ "getString",
17524
+ "getJson",
17525
+ "_currentValues",
17526
+ "_registerItem"
17527
+ ]);
17515
17528
  return new Proxy(this, {
17516
17529
  get(target, prop, receiver) {
17517
17530
  if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
@@ -17528,7 +17541,10 @@ var LiveConfigProxy = class {
17528
17541
  }
17529
17542
  return values[prop];
17530
17543
  },
17531
- set(_target, prop, _value) {
17544
+ set(target, prop, value, receiver) {
17545
+ if (typeof prop === "string" && prop.startsWith("_")) {
17546
+ return Reflect.set(target, prop, value, receiver);
17547
+ }
17532
17548
  throw new Error(
17533
17549
  `LiveConfigProxy is read-only; cannot set ${JSON.stringify(String(prop))}. Mutate config values via client.manage.config.*`
17534
17550
  );
@@ -17583,6 +17599,74 @@ var LiveConfigProxy = class {
17583
17599
  const values = this._currentValues();
17584
17600
  return key in values ? values[key] : defaultValue;
17585
17601
  }
17602
+ // ------------------------------------------------------------------
17603
+ // Typed getters (ADR-037 §2.13)
17604
+ //
17605
+ // Each registers the item (key, type, default, description) on first
17606
+ // call within the process, then returns the resolved value. When the
17607
+ // resolved value cannot be coerced to the getter's type — including
17608
+ // the "not yet set on the server" case — the in-code default is
17609
+ // returned and a structured warning is logged.
17610
+ // ------------------------------------------------------------------
17611
+ /** @internal */
17612
+ _registerItem(itemKey, itemType, defaultValue, description) {
17613
+ this._client._observeItemDeclaration(this._key, itemKey, itemType, defaultValue, description);
17614
+ }
17615
+ /** Read a BOOLEAN item, registering the declaration on first call. */
17616
+ getBool(key, defaultValue, options = {}) {
17617
+ this._registerItem(key, "BOOLEAN", defaultValue, options.description);
17618
+ const values = this._currentValues();
17619
+ if (!(key in values)) return defaultValue;
17620
+ const value = values[key];
17621
+ if (typeof value === "boolean") return value;
17622
+ console.warn(
17623
+ `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected BOOLEAN, got ${typeof value}; returning default`
17624
+ );
17625
+ return defaultValue;
17626
+ }
17627
+ /** Read a NUMBER item as int, registering the declaration on first call. */
17628
+ getInt(key, defaultValue, options = {}) {
17629
+ this._registerItem(key, "NUMBER", defaultValue, options.description);
17630
+ const values = this._currentValues();
17631
+ if (!(key in values)) return defaultValue;
17632
+ const value = values[key];
17633
+ if (typeof value === "number" && Number.isInteger(value)) return value;
17634
+ console.warn(
17635
+ `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected NUMBER (int), got ${typeof value}; returning default`
17636
+ );
17637
+ return defaultValue;
17638
+ }
17639
+ /** Read a NUMBER item as float, registering the declaration on first call. */
17640
+ getFloat(key, defaultValue, options = {}) {
17641
+ this._registerItem(key, "NUMBER", defaultValue, options.description);
17642
+ const values = this._currentValues();
17643
+ if (!(key in values)) return defaultValue;
17644
+ const value = values[key];
17645
+ if (typeof value === "number") return value;
17646
+ console.warn(
17647
+ `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected NUMBER (float), got ${typeof value}; returning default`
17648
+ );
17649
+ return defaultValue;
17650
+ }
17651
+ /** Read a STRING item, registering the declaration on first call. */
17652
+ getString(key, defaultValue, options = {}) {
17653
+ this._registerItem(key, "STRING", defaultValue, options.description);
17654
+ const values = this._currentValues();
17655
+ if (!(key in values)) return defaultValue;
17656
+ const value = values[key];
17657
+ if (typeof value === "string") return value;
17658
+ console.warn(
17659
+ `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected STRING, got ${typeof value}; returning default`
17660
+ );
17661
+ return defaultValue;
17662
+ }
17663
+ /** Read a JSON item, registering the declaration on first call. */
17664
+ getJson(key, defaultValue, options = {}) {
17665
+ this._registerItem(key, "JSON", defaultValue, options.description);
17666
+ const values = this._currentValues();
17667
+ if (!(key in values)) return defaultValue;
17668
+ return values[key];
17669
+ }
17586
17670
  onChange(callbackOrItemKey, callback) {
17587
17671
  if (typeof callbackOrItemKey === "function") {
17588
17672
  this._client.onChange(this._key, callbackOrItemKey);
@@ -17693,6 +17777,11 @@ var ConfigClient = class {
17693
17777
  * cache for everyone (including descendants that inherit from it)
17694
17778
  * without a full re-list. Mirrors Python's `_raw_config_cache`. */
17695
17779
  _configStore = {};
17780
+ /** Cache of LiveConfigProxy instances by config id — ensures repeat
17781
+ * `get_or_create(id)` (or `get(id)` after discovery) returns the same
17782
+ * handle so callers can reference it as a parent via direct ref.
17783
+ * Mirrors Python's `_proxies`. */
17784
+ _proxies = {};
17696
17785
  _initialized = false;
17697
17786
  _listeners = [];
17698
17787
  /** @internal */
@@ -17752,7 +17841,65 @@ var ConfigClient = class {
17752
17841
  if (metrics) {
17753
17842
  metrics.record("config.resolutions", 1, "resolutions", { config: id });
17754
17843
  }
17755
- return new LiveConfigProxy(this, id, model);
17844
+ return this._cachedProxy(id, model);
17845
+ }
17846
+ /**
17847
+ * Declare a configuration from code; return a live, dict-like view.
17848
+ *
17849
+ * Idempotent. Repeated calls with the same `id` return the same
17850
+ * {@link LiveConfigProxy} instance. The first call queues a discovery
17851
+ * payload (the config and any items declared via typed getters on the
17852
+ * returned handle) for upload to `POST /api/v1/configs/bulk` on next
17853
+ * flush. If the config already exists server-side, `managed=true`
17854
+ * configs are left untouched; `managed=false` configs receive the
17855
+ * SDK's items via source-row upsert per ADR-024 §2.9.
17856
+ *
17857
+ * Unlike {@link get}, this method does NOT raise `NotFoundError` when
17858
+ * the id is absent from the cache — discovery handles that case.
17859
+ *
17860
+ * Mirrors Python's `client.config.get_or_create(id, ...)`.
17861
+ */
17862
+ async getOrCreate(id, options = {}) {
17863
+ const parent = options.parent;
17864
+ const parentId = parent instanceof LiveConfigProxy ? parent._key : parent ?? null;
17865
+ this._observeConfigDeclaration(id, parentId, options.name ?? null, options.description ?? null);
17866
+ await this._ensureInitialized();
17867
+ return this._cachedProxy(id, options.model);
17868
+ }
17869
+ /** @internal — return (and cache) the canonical proxy for a config id. */
17870
+ _cachedProxy(id, model) {
17871
+ let proxy = this._proxies[id];
17872
+ if (!proxy) {
17873
+ proxy = new LiveConfigProxy(this, id, model);
17874
+ this._proxies[id] = proxy;
17875
+ } else if (model !== void 0 && proxy._model === void 0) {
17876
+ proxy._model = model;
17877
+ }
17878
+ return proxy;
17879
+ }
17880
+ /** @internal — queue a config declaration with the management buffer. */
17881
+ _observeConfigDeclaration(configId, parent, name, description) {
17882
+ const manage = this._resolveManagement?.();
17883
+ if (!manage) return;
17884
+ manage.config.registerConfig(configId, {
17885
+ service: this._parent?._service ?? null,
17886
+ environment: this._parent?._environment ?? "",
17887
+ parent,
17888
+ name,
17889
+ description
17890
+ });
17891
+ }
17892
+ /** @internal — queue a config item declaration with the management buffer. */
17893
+ _observeItemDeclaration(configId, itemKey, itemType, defaultValue, description) {
17894
+ const manage = this._resolveManagement?.();
17895
+ if (!manage) return;
17896
+ manage.config.registerConfigItem(
17897
+ configId,
17898
+ itemKey,
17899
+ itemType,
17900
+ defaultValue,
17901
+ description ?? null
17902
+ );
17756
17903
  }
17757
17904
  // ------------------------------------------------------------------
17758
17905
  // Runtime: change listeners (3-level overloads)
@@ -17875,6 +18022,17 @@ var ConfigClient = class {
17875
18022
  if (!environment) {
17876
18023
  throw new SmplError("No environment set. Ensure SmplClient is configured.");
17877
18024
  }
18025
+ const manage = this._resolveManagement?.();
18026
+ if (manage) {
18027
+ try {
18028
+ await manage.config.flush();
18029
+ } catch (err) {
18030
+ debug(
18031
+ "config",
18032
+ `pre-start discovery flush failed: ${err instanceof Error ? err.message : String(err)}`
18033
+ );
18034
+ }
18035
+ }
17878
18036
  const configs = await this._listConfigs();
17879
18037
  const cache = {};
17880
18038
  const store = {};
@@ -18828,11 +18986,99 @@ function resourceToConfig2(resource, client) {
18828
18986
  updatedAt: attrs.updated_at ?? null
18829
18987
  });
18830
18988
  }
18989
+ var CONFIG_REGISTRATION_FLUSH_SIZE = 50;
18990
+ var ConfigRegistrationBuffer = class {
18991
+ _pending = /* @__PURE__ */ new Map();
18992
+ _meta = /* @__PURE__ */ new Map();
18993
+ _sentItems = /* @__PURE__ */ new Set();
18994
+ declare(configId, meta) {
18995
+ if (this._meta.has(configId)) return;
18996
+ this._meta.set(configId, meta);
18997
+ this._pending.set(configId, this._buildEntry(configId, {}));
18998
+ }
18999
+ addItem(configId, itemKey, itemType, defaultValue, description) {
19000
+ if (!this._meta.has(configId)) return;
19001
+ const sentKey = `${configId}::${itemKey}`;
19002
+ if (this._sentItems.has(sentKey)) return;
19003
+ let entry = this._pending.get(configId);
19004
+ if (!entry) {
19005
+ entry = this._buildEntry(configId, {});
19006
+ this._pending.set(configId, entry);
19007
+ }
19008
+ if (itemKey in entry.items) return;
19009
+ const def = { value: defaultValue, type: itemType };
19010
+ if (description !== null) def.description = description;
19011
+ entry.items[itemKey] = def;
19012
+ }
19013
+ _buildEntry(configId, items) {
19014
+ const meta = this._meta.get(configId);
19015
+ const entry = { id: configId, items };
19016
+ if (meta.service !== null) entry.service = meta.service;
19017
+ if (meta.environment !== null) entry.environment = meta.environment;
19018
+ if (meta.parent !== null) entry.parent = meta.parent;
19019
+ if (meta.name !== null) entry.name = meta.name;
19020
+ if (meta.description !== null) entry.description = meta.description;
19021
+ return entry;
19022
+ }
19023
+ /** Destructive drain — records sent items so they aren't re-queued. */
19024
+ drain() {
19025
+ const batch = Array.from(this._pending.values());
19026
+ for (const entry of batch) {
19027
+ for (const itemKey of Object.keys(entry.items)) {
19028
+ this._sentItems.add(`${entry.id}::${itemKey}`);
19029
+ }
19030
+ }
19031
+ this._pending.clear();
19032
+ return batch;
19033
+ }
19034
+ get pendingCount() {
19035
+ return this._pending.size;
19036
+ }
19037
+ };
18831
19038
  var ManagementConfigClient = class {
18832
19039
  /** @internal */
18833
19040
  constructor(_http) {
18834
19041
  this._http = _http;
18835
19042
  }
19043
+ /** @internal */
19044
+ _buffer = new ConfigRegistrationBuffer();
19045
+ /** @internal — queue a configuration declaration for bulk-discovery upload. */
19046
+ registerConfig(configId, meta) {
19047
+ this._buffer.declare(configId, {
19048
+ service: meta.service,
19049
+ environment: meta.environment,
19050
+ parent: meta.parent ?? null,
19051
+ name: meta.name ?? null,
19052
+ description: meta.description ?? null
19053
+ });
19054
+ if (this._buffer.pendingCount >= CONFIG_REGISTRATION_FLUSH_SIZE) {
19055
+ void this.flush();
19056
+ }
19057
+ }
19058
+ /** @internal — queue a config item declaration. */
19059
+ registerConfigItem(configId, itemKey, itemType, defaultValue, description) {
19060
+ this._buffer.addItem(configId, itemKey, itemType, defaultValue, description);
19061
+ if (this._buffer.pendingCount >= CONFIG_REGISTRATION_FLUSH_SIZE) {
19062
+ void this.flush();
19063
+ }
19064
+ }
19065
+ /** Send any pending config declarations to `POST /api/v1/configs/bulk`. */
19066
+ async flush() {
19067
+ const batch = this._buffer.drain();
19068
+ if (batch.length === 0) return;
19069
+ try {
19070
+ const result = await this._http.POST("/api/v1/configs/bulk", {
19071
+ body: { configs: batch }
19072
+ });
19073
+ if (!result.response.ok) {
19074
+ }
19075
+ } catch {
19076
+ }
19077
+ }
19078
+ /** Number of pending config declarations awaiting flush. */
19079
+ get pendingCount() {
19080
+ return this._buffer.pendingCount;
19081
+ }
18836
19082
  /** Construct an unsaved {@link Config}. Call `.save()` to persist. */
18837
19083
  new(id, options = {}) {
18838
19084
  const parent = options.parent;
@@ -21094,6 +21340,7 @@ var SmplManagementClient = class {
21094
21340
  await this.contexts.flush();
21095
21341
  await this.flags.flush();
21096
21342
  await this.loggers.flush();
21343
+ await this.config.flush();
21097
21344
  } catch {
21098
21345
  }
21099
21346
  }