@smplkit/sdk 3.0.84 → 3.0.85

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
@@ -17486,47 +17486,15 @@ var Config = class {
17486
17486
  };
17487
17487
 
17488
17488
  // src/config/proxy.ts
17489
- function _unflattenDotNotation(flat) {
17490
- const nested = {};
17491
- for (const [key, value] of Object.entries(flat)) {
17492
- const parts = key.split(".");
17493
- let current = nested;
17494
- for (let i = 0; i < parts.length - 1; i++) {
17495
- const part = parts[i];
17496
- if (current[part] === void 0 || typeof current[part] !== "object" || current[part] === null) {
17497
- current[part] = {};
17498
- }
17499
- current = current[part];
17500
- }
17501
- current[parts[parts.length - 1]] = value;
17502
- }
17503
- return nested;
17504
- }
17505
17489
  var LiveConfigProxy = class {
17506
17490
  /** @internal */
17507
17491
  _client;
17508
17492
  /** @internal */
17509
17493
  _key;
17510
- /** @internal */
17511
- _model;
17512
- constructor(client, key, model) {
17494
+ constructor(client, key) {
17513
17495
  this._client = client;
17514
17496
  this._key = key;
17515
- this._model = model;
17516
- const ownMethods = /* @__PURE__ */ new Set([
17517
- "keys",
17518
- "values",
17519
- "items",
17520
- "get",
17521
- "onChange",
17522
- "getBool",
17523
- "getInt",
17524
- "getFloat",
17525
- "getString",
17526
- "getJson",
17527
- "_currentValues",
17528
- "_registerItem"
17529
- ]);
17497
+ const ownMethods = /* @__PURE__ */ new Set(["keys", "values", "items", "get", "onChange", "_currentValues"]);
17530
17498
  return new Proxy(this, {
17531
17499
  get(target, prop, receiver) {
17532
17500
  if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
@@ -17536,17 +17504,9 @@ var LiveConfigProxy = class {
17536
17504
  return Reflect.get(target, prop, receiver);
17537
17505
  }
17538
17506
  const values = target._currentValues();
17539
- if (target._model) {
17540
- const nested = _unflattenDotNotation(values);
17541
- const instance = new target._model(nested);
17542
- return instance[prop];
17543
- }
17544
17507
  return values[prop];
17545
17508
  },
17546
- set(target, prop, value, receiver) {
17547
- if (typeof prop === "string" && prop.startsWith("_")) {
17548
- return Reflect.set(target, prop, value, receiver);
17549
- }
17509
+ set(_target, prop) {
17550
17510
  throw new Error(
17551
17511
  `LiveConfigProxy is read-only; cannot set ${JSON.stringify(String(prop))}. Mutate config values via client.manage.config.*`
17552
17512
  );
@@ -17601,74 +17561,6 @@ var LiveConfigProxy = class {
17601
17561
  const values = this._currentValues();
17602
17562
  return key in values ? values[key] : defaultValue;
17603
17563
  }
17604
- // ------------------------------------------------------------------
17605
- // Typed getters (ADR-037 §2.13)
17606
- //
17607
- // Each registers the item (key, type, default, description) on first
17608
- // call within the process, then returns the resolved value. When the
17609
- // resolved value cannot be coerced to the getter's type — including
17610
- // the "not yet set on the server" case — the in-code default is
17611
- // returned and a structured warning is logged.
17612
- // ------------------------------------------------------------------
17613
- /** @internal */
17614
- _registerItem(itemKey, itemType, defaultValue, description) {
17615
- this._client._observeItemDeclaration(this._key, itemKey, itemType, defaultValue, description);
17616
- }
17617
- /** Read a BOOLEAN item, registering the declaration on first call. */
17618
- getBool(key, defaultValue, options = {}) {
17619
- this._registerItem(key, "BOOLEAN", defaultValue, options.description);
17620
- const values = this._currentValues();
17621
- if (!(key in values)) return defaultValue;
17622
- const value = values[key];
17623
- if (typeof value === "boolean") return value;
17624
- console.warn(
17625
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected BOOLEAN, got ${typeof value}; returning default`
17626
- );
17627
- return defaultValue;
17628
- }
17629
- /** Read a NUMBER item as int, registering the declaration on first call. */
17630
- getInt(key, defaultValue, options = {}) {
17631
- this._registerItem(key, "NUMBER", defaultValue, options.description);
17632
- const values = this._currentValues();
17633
- if (!(key in values)) return defaultValue;
17634
- const value = values[key];
17635
- if (typeof value === "number" && Number.isInteger(value)) return value;
17636
- console.warn(
17637
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected NUMBER (int), got ${typeof value}; returning default`
17638
- );
17639
- return defaultValue;
17640
- }
17641
- /** Read a NUMBER item as float, registering the declaration on first call. */
17642
- getFloat(key, defaultValue, options = {}) {
17643
- this._registerItem(key, "NUMBER", defaultValue, options.description);
17644
- const values = this._currentValues();
17645
- if (!(key in values)) return defaultValue;
17646
- const value = values[key];
17647
- if (typeof value === "number") return value;
17648
- console.warn(
17649
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected NUMBER (float), got ${typeof value}; returning default`
17650
- );
17651
- return defaultValue;
17652
- }
17653
- /** Read a STRING item, registering the declaration on first call. */
17654
- getString(key, defaultValue, options = {}) {
17655
- this._registerItem(key, "STRING", defaultValue, options.description);
17656
- const values = this._currentValues();
17657
- if (!(key in values)) return defaultValue;
17658
- const value = values[key];
17659
- if (typeof value === "string") return value;
17660
- console.warn(
17661
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected STRING, got ${typeof value}; returning default`
17662
- );
17663
- return defaultValue;
17664
- }
17665
- /** Read a JSON item, registering the declaration on first call. */
17666
- getJson(key, defaultValue, options = {}) {
17667
- this._registerItem(key, "JSON", defaultValue, options.description);
17668
- const values = this._currentValues();
17669
- if (!(key in values)) return defaultValue;
17670
- return values[key];
17671
- }
17672
17564
  onChange(callbackOrItemKey, callback) {
17673
17565
  if (typeof callbackOrItemKey === "function") {
17674
17566
  this._client.onChange(this._key, callbackOrItemKey);
@@ -17721,6 +17613,7 @@ var ConfigChangeEvent = class {
17721
17613
  }
17722
17614
  };
17723
17615
  var BASE_URL = "https://config.smplkit.com";
17616
+ var MISSING = /* @__PURE__ */ Symbol("smplkit.config.get.MISSING");
17724
17617
  function extractItemValues(items) {
17725
17618
  if (!items) return {};
17726
17619
  const result = {};
@@ -17760,6 +17653,40 @@ function resourceToConfig(resource) {
17760
17653
  updatedAt: attrs.updated_at ?? null
17761
17654
  });
17762
17655
  }
17656
+ function valueToItemType(value) {
17657
+ if (typeof value === "boolean") return "BOOLEAN";
17658
+ if (typeof value === "number") return "NUMBER";
17659
+ if (typeof value === "string") return "STRING";
17660
+ return "STRING";
17661
+ }
17662
+ function isPlainObject(value) {
17663
+ if (value === null || typeof value !== "object") return false;
17664
+ const proto = Object.getPrototypeOf(value);
17665
+ return proto === Object.prototype || proto === null;
17666
+ }
17667
+ function iterObjectItems(obj, prefix = "") {
17668
+ const out = [];
17669
+ for (const [key, value] of Object.entries(obj)) {
17670
+ const flatKey = `${prefix}${key}`;
17671
+ if (isPlainObject(value)) {
17672
+ out.push(...iterObjectItems(value, `${flatKey}.`));
17673
+ continue;
17674
+ }
17675
+ out.push([flatKey, valueToItemType(value), value]);
17676
+ }
17677
+ return out;
17678
+ }
17679
+ function applyChangeToTarget(target, dottedKey, value) {
17680
+ const parts = dottedKey.split(".");
17681
+ let current = target;
17682
+ for (let i = 0; i < parts.length - 1; i++) {
17683
+ const part = parts[i];
17684
+ if (current === null || typeof current !== "object" || !(part in current)) return;
17685
+ current = current[part];
17686
+ }
17687
+ if (current === null || typeof current !== "object") return;
17688
+ current[parts[parts.length - 1]] = value;
17689
+ }
17763
17690
  var ConfigClient = class {
17764
17691
  /** @internal */
17765
17692
  _apiKey;
@@ -17780,10 +17707,11 @@ var ConfigClient = class {
17780
17707
  * without a full re-list. Mirrors Python's `_raw_config_cache`. */
17781
17708
  _configStore = {};
17782
17709
  /** Cache of LiveConfigProxy instances by config id — ensures repeat
17783
- * `get_or_create(id)` (or `get(id)` after discovery) returns the same
17784
- * handle so callers can reference it as a parent via direct ref.
17785
- * Mirrors Python's `_proxies`. */
17710
+ * `get(id)` calls return the same handle. */
17786
17711
  _proxies = {};
17712
+ /** Bound targets (plain objects or class instances) keyed by config
17713
+ * id. WebSocket dispatch mutates these in place when values change. */
17714
+ _bindings = /* @__PURE__ */ new Map();
17787
17715
  _initialized = false;
17788
17716
  _listeners = [];
17789
17717
  /** @internal */
@@ -17816,66 +17744,126 @@ var ConfigClient = class {
17816
17744
  });
17817
17745
  }
17818
17746
  // ------------------------------------------------------------------
17819
- // Runtime: resolve and subscribe
17747
+ // Public API: bind, get
17820
17748
  // ------------------------------------------------------------------
17821
17749
  /**
17822
- * Return a live, dict-like view of the resolved values for *id*.
17750
+ * Bind an object to a config id; return the same object back, live.
17751
+ *
17752
+ * Declarative, code-first API. The object's keys are the schema; its
17753
+ * values are the in-code defaults. On first boot:
17823
17754
  *
17824
- * Without `model`, returns a {@link LiveConfigProxy} that behaves like a
17825
- * `Record<string, unknown>` (`proxy["key"]`, iteration, `proxy.items()`,
17826
- * `Object.keys(proxy)`) and updates automatically as the server pushes
17827
- * changes.
17755
+ * 1. Every leaf (recursively, through nested plain objects) is
17756
+ * registered with the server as a config item, with its value as
17757
+ * the in-code default and a type inferred from `typeof value`.
17758
+ * 2. After the SDK's cache is populated, any server-side overrides for
17759
+ * this config are applied to the bound object in place.
17828
17760
  *
17829
- * With `model`, the return value type-checks as `model` — attribute
17830
- * access (`cfg.database.host`) walks a model rebuilt from the current
17831
- * values on each read, so the customer sees the model's type signature
17832
- * in their IDE while still tracking live data.
17761
+ * On every WebSocket-delivered change thereafter the bound object is
17762
+ * mutated in place — readers of `obj.foo` and `obj["foo"]` always see
17763
+ * the current resolved value. The returned object is the same one you
17764
+ * passed in (referential identity preserved).
17833
17765
  *
17834
- * Mirrors Python's `client.config.get(id)` / `client.config.get(id, ModelCls)`.
17835
- * There is no `subscribe()` it was unified into `get()`.
17766
+ * Idempotent. Repeated calls with the same id return the originally-
17767
+ * bound object; the new `config` argument is ignored.
17768
+ *
17769
+ * **Plain object literals vs. class instances.** Plain object literals
17770
+ * (e.g., `{ a: 1, b: { c: 2 } }`) are the recommended input shape —
17771
+ * their keys are the explicit override set, and omitted keys inherit
17772
+ * from `parent`. Class instances are also accepted, but every
17773
+ * enumerable property is registered as an explicit override (there is
17774
+ * no JS equivalent of Python's `model_fields_set`); to get omit-to-
17775
+ * inherit semantics, use a plain object literal.
17776
+ *
17777
+ * @param id - The config id to register under.
17778
+ * @param config - A plain object literal (recommended) or class
17779
+ * instance carrying the in-code defaults.
17780
+ * @param options - Optional `parent`: another object previously
17781
+ * returned from a {@link bind} call. Activates parent-chain
17782
+ * inheritance for keys the caller omitted.
17783
+ * @returns The same `config` object, registered and live.
17784
+ * @throws TypeError if `config` is not an object.
17785
+ * @throws Error if `parent` was not previously bound via {@link bind}.
17836
17786
  */
17837
- async get(id, model) {
17787
+ async bind(id, config, options = {}) {
17788
+ if (config === null || typeof config !== "object") {
17789
+ throw new TypeError(`bind() requires an object; got ${typeof config}`);
17790
+ }
17791
+ const existing = this._bindings.get(id);
17792
+ if (existing !== void 0) {
17793
+ return existing;
17794
+ }
17795
+ let parentId = null;
17796
+ if (options.parent !== void 0 && options.parent !== null) {
17797
+ parentId = this._configIdFor(options.parent);
17798
+ if (parentId === null) {
17799
+ throw new Error(
17800
+ "bind(): parent must be an object previously returned from client.config.bind(). Bind the parent first."
17801
+ );
17802
+ }
17803
+ }
17804
+ const ctor = config.constructor;
17805
+ const className = typeof ctor === "function" && ctor !== Object && typeof ctor.name === "string" && ctor.name ? ctor.name : null;
17806
+ this._observeConfigDeclaration(id, parentId, className, null);
17807
+ for (const [itemKey, itemType, value] of iterObjectItems(config)) {
17808
+ this._observeItemDeclaration(id, itemKey, itemType, value, void 0);
17809
+ }
17810
+ this._bindings.set(id, config);
17811
+ await this._ensureInitialized();
17812
+ this._syncTargetFromCache(config, id);
17813
+ return config;
17814
+ }
17815
+ async get(id, key, defaultValue = MISSING) {
17838
17816
  await this._ensureInitialized();
17817
+ if (key === void 0) {
17818
+ if (!(id in this._configCache)) {
17819
+ throw new SmplNotFoundError(`Config with id '${id}' not found in cache`);
17820
+ }
17821
+ const metrics = this._parent?._metrics;
17822
+ if (metrics) {
17823
+ metrics.record("config.resolutions", 1, "resolutions", { config: id });
17824
+ }
17825
+ return this._cachedProxy(id);
17826
+ }
17827
+ const hasDefault = defaultValue !== MISSING;
17828
+ if (hasDefault) {
17829
+ this._observeConfigDeclaration(id, null, null, null);
17830
+ this._observeItemDeclaration(id, key, valueToItemType(defaultValue), defaultValue, void 0);
17831
+ }
17839
17832
  if (!(id in this._configCache)) {
17833
+ if (hasDefault) return defaultValue;
17840
17834
  throw new SmplNotFoundError(`Config with id '${id}' not found in cache`);
17841
17835
  }
17842
- const metrics = this._parent?._metrics;
17843
- if (metrics) {
17844
- metrics.record("config.resolutions", 1, "resolutions", { config: id });
17836
+ const values = this._configCache[id];
17837
+ if (!(key in values)) {
17838
+ if (hasDefault) return defaultValue;
17839
+ throw new SmplNotFoundError(`Config item '${key}' not found in config '${id}'`);
17845
17840
  }
17846
- return this._cachedProxy(id, model);
17841
+ return values[key];
17847
17842
  }
17848
- /**
17849
- * Declare a configuration from code; return a live, dict-like view.
17850
- *
17851
- * Idempotent. Repeated calls with the same `id` return the same
17852
- * {@link LiveConfigProxy} instance. The first call queues a discovery
17853
- * payload (the config and any items declared via typed getters on the
17854
- * returned handle) for upload to `POST /api/v1/configs/bulk` on next
17855
- * flush. If the config already exists server-side, `managed=true`
17856
- * configs are left untouched; `managed=false` configs receive the
17857
- * SDK's items via source-row upsert per ADR-024 §2.9.
17858
- *
17859
- * Unlike {@link get}, this method does NOT raise `NotFoundError` when
17860
- * the id is absent from the cache discovery handles that case.
17861
- *
17862
- * Mirrors Python's `client.config.get_or_create(id, ...)`.
17863
- */
17864
- async getOrCreate(id, options = {}) {
17865
- const parent = options.parent;
17866
- const parentId = parent instanceof LiveConfigProxy ? parent._key : parent ?? null;
17867
- this._observeConfigDeclaration(id, parentId, options.name ?? null, options.description ?? null);
17868
- await this._ensureInitialized();
17869
- return this._cachedProxy(id, options.model);
17843
+ // ------------------------------------------------------------------
17844
+ // Internal: binding helpers
17845
+ // ------------------------------------------------------------------
17846
+ /** @internal return the config_id this object was bound under, or null. */
17847
+ _configIdFor(target) {
17848
+ for (const [cid, bound] of this._bindings) {
17849
+ if (bound === target) return cid;
17850
+ }
17851
+ return null;
17852
+ }
17853
+ /** @internal — apply current cached values to a freshly-bound target. */
17854
+ _syncTargetFromCache(target, configId) {
17855
+ const cache = this._configCache[configId];
17856
+ if (!cache) return;
17857
+ for (const [dottedKey, value] of Object.entries(cache)) {
17858
+ applyChangeToTarget(target, dottedKey, value);
17859
+ }
17870
17860
  }
17871
17861
  /** @internal — return (and cache) the canonical proxy for a config id. */
17872
- _cachedProxy(id, model) {
17862
+ _cachedProxy(id) {
17873
17863
  let proxy = this._proxies[id];
17874
17864
  if (!proxy) {
17875
- proxy = new LiveConfigProxy(this, id, model);
17865
+ proxy = new LiveConfigProxy(this, id);
17876
17866
  this._proxies[id] = proxy;
17877
- } else if (model !== void 0 && proxy._model === void 0) {
17878
- proxy._model = model;
17879
17867
  }
17880
17868
  return proxy;
17881
17869
  }
@@ -17943,7 +17931,7 @@ var ConfigClient = class {
17943
17931
  */
17944
17932
  async refresh() {
17945
17933
  if (!this._initialized) {
17946
- throw new SmplError("Config not initialized. Call get() first.");
17934
+ throw new SmplError("Config not initialized. Call get() or bind() first.");
17947
17935
  }
17948
17936
  const environment = this._parent?._environment;
17949
17937
  if (!environment) {
@@ -17967,10 +17955,6 @@ var ConfigClient = class {
17967
17955
  * (set via `_resolveManagement`) so runtime + management share one HTTP
17968
17956
  * client; falls back to a direct GET when running without `SmplClient`
17969
17957
  * bootstrap (e.g. unit tests that construct `ConfigClient` directly).
17970
- *
17971
- * Pages through the server until a short page (less than the requested
17972
- * size) is returned — accounts with more than 1000 configs would
17973
- * otherwise silently lose everything past page one.
17974
17958
  */
17975
17959
  async _listConfigs() {
17976
17960
  const PAGE_SIZE = 1e3;
@@ -18012,7 +17996,8 @@ var ConfigClient = class {
18012
17996
  * Eagerly initialize the config subclient — fetch all configs, resolve
18013
17997
  * environment-scoped values into the local cache, and subscribe to the
18014
17998
  * shared WebSocket for live updates. Idempotent. Called automatically
18015
- * on first `client.config.get(...)` if not invoked manually.
17999
+ * on first `client.config.get(...)` / `client.config.bind(...)` if not
18000
+ * invoked manually.
18016
18001
  */
18017
18002
  async start() {
18018
18003
  return this._ensureInitialized();
@@ -18148,10 +18133,14 @@ var ConfigClient = class {
18148
18133
  const oldItems = oldCache[cfgKey] ?? {};
18149
18134
  const newItems = newCache[cfgKey] ?? {};
18150
18135
  const allItemKeys = /* @__PURE__ */ new Set([...Object.keys(oldItems), ...Object.keys(newItems)]);
18136
+ const target = this._bindings.get(cfgKey);
18151
18137
  for (const iKey of allItemKeys) {
18152
18138
  const oldVal = iKey in oldItems ? oldItems[iKey] : null;
18153
18139
  const newVal = iKey in newItems ? newItems[iKey] : null;
18154
18140
  if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
18141
+ if (target !== void 0) {
18142
+ applyChangeToTarget(target, iKey, newVal);
18143
+ }
18155
18144
  const metrics = this._parent?._metrics;
18156
18145
  if (metrics) {
18157
18146
  metrics.record("config.changes", 1, "changes", { config: cfgKey });