@smplkit/sdk 3.0.84 → 3.0.86

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.js CHANGED
@@ -17416,47 +17416,15 @@ var Config = class {
17416
17416
  };
17417
17417
 
17418
17418
  // src/config/proxy.ts
17419
- function _unflattenDotNotation(flat) {
17420
- const nested = {};
17421
- for (const [key, value] of Object.entries(flat)) {
17422
- const parts = key.split(".");
17423
- let current = nested;
17424
- for (let i = 0; i < parts.length - 1; i++) {
17425
- const part = parts[i];
17426
- if (current[part] === void 0 || typeof current[part] !== "object" || current[part] === null) {
17427
- current[part] = {};
17428
- }
17429
- current = current[part];
17430
- }
17431
- current[parts[parts.length - 1]] = value;
17432
- }
17433
- return nested;
17434
- }
17435
17419
  var LiveConfigProxy = class {
17436
17420
  /** @internal */
17437
17421
  _client;
17438
17422
  /** @internal */
17439
17423
  _key;
17440
- /** @internal */
17441
- _model;
17442
- constructor(client, key, model) {
17424
+ constructor(client, key) {
17443
17425
  this._client = client;
17444
17426
  this._key = key;
17445
- this._model = model;
17446
- const ownMethods = /* @__PURE__ */ new Set([
17447
- "keys",
17448
- "values",
17449
- "items",
17450
- "get",
17451
- "onChange",
17452
- "getBool",
17453
- "getInt",
17454
- "getFloat",
17455
- "getString",
17456
- "getJson",
17457
- "_currentValues",
17458
- "_registerItem"
17459
- ]);
17427
+ const ownMethods = /* @__PURE__ */ new Set(["keys", "values", "items", "get", "onChange", "_currentValues"]);
17460
17428
  return new Proxy(this, {
17461
17429
  get(target, prop, receiver) {
17462
17430
  if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
@@ -17466,17 +17434,9 @@ var LiveConfigProxy = class {
17466
17434
  return Reflect.get(target, prop, receiver);
17467
17435
  }
17468
17436
  const values = target._currentValues();
17469
- if (target._model) {
17470
- const nested = _unflattenDotNotation(values);
17471
- const instance = new target._model(nested);
17472
- return instance[prop];
17473
- }
17474
17437
  return values[prop];
17475
17438
  },
17476
- set(target, prop, value, receiver) {
17477
- if (typeof prop === "string" && prop.startsWith("_")) {
17478
- return Reflect.set(target, prop, value, receiver);
17479
- }
17439
+ set(_target, prop) {
17480
17440
  throw new Error(
17481
17441
  `LiveConfigProxy is read-only; cannot set ${JSON.stringify(String(prop))}. Mutate config values via client.manage.config.*`
17482
17442
  );
@@ -17531,74 +17491,6 @@ var LiveConfigProxy = class {
17531
17491
  const values = this._currentValues();
17532
17492
  return key in values ? values[key] : defaultValue;
17533
17493
  }
17534
- // ------------------------------------------------------------------
17535
- // Typed getters (ADR-037 §2.13)
17536
- //
17537
- // Each registers the item (key, type, default, description) on first
17538
- // call within the process, then returns the resolved value. When the
17539
- // resolved value cannot be coerced to the getter's type — including
17540
- // the "not yet set on the server" case — the in-code default is
17541
- // returned and a structured warning is logged.
17542
- // ------------------------------------------------------------------
17543
- /** @internal */
17544
- _registerItem(itemKey, itemType, defaultValue, description) {
17545
- this._client._observeItemDeclaration(this._key, itemKey, itemType, defaultValue, description);
17546
- }
17547
- /** Read a BOOLEAN item, registering the declaration on first call. */
17548
- getBool(key, defaultValue, options = {}) {
17549
- this._registerItem(key, "BOOLEAN", defaultValue, options.description);
17550
- const values = this._currentValues();
17551
- if (!(key in values)) return defaultValue;
17552
- const value = values[key];
17553
- if (typeof value === "boolean") return value;
17554
- console.warn(
17555
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected BOOLEAN, got ${typeof value}; returning default`
17556
- );
17557
- return defaultValue;
17558
- }
17559
- /** Read a NUMBER item as int, registering the declaration on first call. */
17560
- getInt(key, defaultValue, options = {}) {
17561
- this._registerItem(key, "NUMBER", defaultValue, options.description);
17562
- const values = this._currentValues();
17563
- if (!(key in values)) return defaultValue;
17564
- const value = values[key];
17565
- if (typeof value === "number" && Number.isInteger(value)) return value;
17566
- console.warn(
17567
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected NUMBER (int), got ${typeof value}; returning default`
17568
- );
17569
- return defaultValue;
17570
- }
17571
- /** Read a NUMBER item as float, registering the declaration on first call. */
17572
- getFloat(key, defaultValue, options = {}) {
17573
- this._registerItem(key, "NUMBER", defaultValue, options.description);
17574
- const values = this._currentValues();
17575
- if (!(key in values)) return defaultValue;
17576
- const value = values[key];
17577
- if (typeof value === "number") return value;
17578
- console.warn(
17579
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected NUMBER (float), got ${typeof value}; returning default`
17580
- );
17581
- return defaultValue;
17582
- }
17583
- /** Read a STRING item, registering the declaration on first call. */
17584
- getString(key, defaultValue, options = {}) {
17585
- this._registerItem(key, "STRING", defaultValue, options.description);
17586
- const values = this._currentValues();
17587
- if (!(key in values)) return defaultValue;
17588
- const value = values[key];
17589
- if (typeof value === "string") return value;
17590
- console.warn(
17591
- `[smplkit] config ${JSON.stringify(this._key)} item ${JSON.stringify(key)}: expected STRING, got ${typeof value}; returning default`
17592
- );
17593
- return defaultValue;
17594
- }
17595
- /** Read a JSON item, registering the declaration on first call. */
17596
- getJson(key, defaultValue, options = {}) {
17597
- this._registerItem(key, "JSON", defaultValue, options.description);
17598
- const values = this._currentValues();
17599
- if (!(key in values)) return defaultValue;
17600
- return values[key];
17601
- }
17602
17494
  onChange(callbackOrItemKey, callback) {
17603
17495
  if (typeof callbackOrItemKey === "function") {
17604
17496
  this._client.onChange(this._key, callbackOrItemKey);
@@ -17651,6 +17543,7 @@ var ConfigChangeEvent = class {
17651
17543
  }
17652
17544
  };
17653
17545
  var BASE_URL = "https://config.smplkit.com";
17546
+ var MISSING = /* @__PURE__ */ Symbol("smplkit.config.get.MISSING");
17654
17547
  function extractItemValues(items) {
17655
17548
  if (!items) return {};
17656
17549
  const result = {};
@@ -17690,6 +17583,40 @@ function resourceToConfig(resource) {
17690
17583
  updatedAt: attrs.updated_at ?? null
17691
17584
  });
17692
17585
  }
17586
+ function valueToItemType(value) {
17587
+ if (typeof value === "boolean") return "BOOLEAN";
17588
+ if (typeof value === "number") return "NUMBER";
17589
+ if (typeof value === "string") return "STRING";
17590
+ return "STRING";
17591
+ }
17592
+ function isPlainObject(value) {
17593
+ if (value === null || typeof value !== "object") return false;
17594
+ const proto = Object.getPrototypeOf(value);
17595
+ return proto === Object.prototype || proto === null;
17596
+ }
17597
+ function iterObjectItems(obj, prefix = "") {
17598
+ const out = [];
17599
+ for (const [key, value] of Object.entries(obj)) {
17600
+ const flatKey = `${prefix}${key}`;
17601
+ if (isPlainObject(value)) {
17602
+ out.push(...iterObjectItems(value, `${flatKey}.`));
17603
+ continue;
17604
+ }
17605
+ out.push([flatKey, valueToItemType(value), value]);
17606
+ }
17607
+ return out;
17608
+ }
17609
+ function applyChangeToTarget(target, dottedKey, value) {
17610
+ const parts = dottedKey.split(".");
17611
+ let current = target;
17612
+ for (let i = 0; i < parts.length - 1; i++) {
17613
+ const part = parts[i];
17614
+ if (current === null || typeof current !== "object" || !(part in current)) return;
17615
+ current = current[part];
17616
+ }
17617
+ if (current === null || typeof current !== "object") return;
17618
+ current[parts[parts.length - 1]] = value;
17619
+ }
17693
17620
  var ConfigClient = class {
17694
17621
  /** @internal */
17695
17622
  _apiKey;
@@ -17710,10 +17637,11 @@ var ConfigClient = class {
17710
17637
  * without a full re-list. Mirrors Python's `_raw_config_cache`. */
17711
17638
  _configStore = {};
17712
17639
  /** Cache of LiveConfigProxy instances by config id — ensures repeat
17713
- * `get_or_create(id)` (or `get(id)` after discovery) returns the same
17714
- * handle so callers can reference it as a parent via direct ref.
17715
- * Mirrors Python's `_proxies`. */
17640
+ * `get(id)` calls return the same handle. */
17716
17641
  _proxies = {};
17642
+ /** Bound targets (plain objects or class instances) keyed by config
17643
+ * id. WebSocket dispatch mutates these in place when values change. */
17644
+ _bindings = /* @__PURE__ */ new Map();
17717
17645
  _initialized = false;
17718
17646
  _listeners = [];
17719
17647
  /** @internal */
@@ -17746,66 +17674,126 @@ var ConfigClient = class {
17746
17674
  });
17747
17675
  }
17748
17676
  // ------------------------------------------------------------------
17749
- // Runtime: resolve and subscribe
17677
+ // Public API: bind, get
17750
17678
  // ------------------------------------------------------------------
17751
17679
  /**
17752
- * Return a live, dict-like view of the resolved values for *id*.
17680
+ * Bind an object to a config id; return the same object back, live.
17681
+ *
17682
+ * Declarative, code-first API. The object's keys are the schema; its
17683
+ * values are the in-code defaults. On first boot:
17753
17684
  *
17754
- * Without `model`, returns a {@link LiveConfigProxy} that behaves like a
17755
- * `Record<string, unknown>` (`proxy["key"]`, iteration, `proxy.items()`,
17756
- * `Object.keys(proxy)`) and updates automatically as the server pushes
17757
- * changes.
17685
+ * 1. Every leaf (recursively, through nested plain objects) is
17686
+ * registered with the server as a config item, with its value as
17687
+ * the in-code default and a type inferred from `typeof value`.
17688
+ * 2. After the SDK's cache is populated, any server-side overrides for
17689
+ * this config are applied to the bound object in place.
17758
17690
  *
17759
- * With `model`, the return value type-checks as `model` — attribute
17760
- * access (`cfg.database.host`) walks a model rebuilt from the current
17761
- * values on each read, so the customer sees the model's type signature
17762
- * in their IDE while still tracking live data.
17691
+ * On every WebSocket-delivered change thereafter the bound object is
17692
+ * mutated in place — readers of `obj.foo` and `obj["foo"]` always see
17693
+ * the current resolved value. The returned object is the same one you
17694
+ * passed in (referential identity preserved).
17763
17695
  *
17764
- * Mirrors Python's `client.config.get(id)` / `client.config.get(id, ModelCls)`.
17765
- * There is no `subscribe()` it was unified into `get()`.
17696
+ * Idempotent. Repeated calls with the same id return the originally-
17697
+ * bound object; the new `config` argument is ignored.
17698
+ *
17699
+ * **Plain object literals vs. class instances.** Plain object literals
17700
+ * (e.g., `{ a: 1, b: { c: 2 } }`) are the recommended input shape —
17701
+ * their keys are the explicit override set, and omitted keys inherit
17702
+ * from `parent`. Class instances are also accepted, but every
17703
+ * enumerable property is registered as an explicit override (there is
17704
+ * no JS equivalent of Python's `model_fields_set`); to get omit-to-
17705
+ * inherit semantics, use a plain object literal.
17706
+ *
17707
+ * @param id - The config id to register under.
17708
+ * @param config - A plain object literal (recommended) or class
17709
+ * instance carrying the in-code defaults.
17710
+ * @param options - Optional `parent`: another object previously
17711
+ * returned from a {@link bind} call. Activates parent-chain
17712
+ * inheritance for keys the caller omitted.
17713
+ * @returns The same `config` object, registered and live.
17714
+ * @throws TypeError if `config` is not an object.
17715
+ * @throws Error if `parent` was not previously bound via {@link bind}.
17766
17716
  */
17767
- async get(id, model) {
17717
+ async bind(id, config, options = {}) {
17718
+ if (config === null || typeof config !== "object") {
17719
+ throw new TypeError(`bind() requires an object; got ${typeof config}`);
17720
+ }
17721
+ const existing = this._bindings.get(id);
17722
+ if (existing !== void 0) {
17723
+ return existing;
17724
+ }
17725
+ let parentId = null;
17726
+ if (options.parent !== void 0 && options.parent !== null) {
17727
+ parentId = this._configIdFor(options.parent);
17728
+ if (parentId === null) {
17729
+ throw new Error(
17730
+ "bind(): parent must be an object previously returned from client.config.bind(). Bind the parent first."
17731
+ );
17732
+ }
17733
+ }
17734
+ const ctor = config.constructor;
17735
+ const className = typeof ctor === "function" && ctor !== Object && typeof ctor.name === "string" && ctor.name ? ctor.name : null;
17736
+ this._observeConfigDeclaration(id, parentId, className, null);
17737
+ for (const [itemKey, itemType, value] of iterObjectItems(config)) {
17738
+ this._observeItemDeclaration(id, itemKey, itemType, value, void 0);
17739
+ }
17740
+ this._bindings.set(id, config);
17741
+ await this._ensureInitialized();
17742
+ this._syncTargetFromCache(config, id);
17743
+ return config;
17744
+ }
17745
+ async get(id, key, defaultValue = MISSING) {
17768
17746
  await this._ensureInitialized();
17747
+ if (key === void 0) {
17748
+ if (!(id in this._configCache)) {
17749
+ throw new SmplNotFoundError(`Config with id '${id}' not found in cache`);
17750
+ }
17751
+ const metrics = this._parent?._metrics;
17752
+ if (metrics) {
17753
+ metrics.record("config.resolutions", 1, "resolutions", { config: id });
17754
+ }
17755
+ return this._cachedProxy(id);
17756
+ }
17757
+ const hasDefault = defaultValue !== MISSING;
17758
+ if (hasDefault) {
17759
+ this._observeConfigDeclaration(id, null, null, null);
17760
+ this._observeItemDeclaration(id, key, valueToItemType(defaultValue), defaultValue, void 0);
17761
+ }
17769
17762
  if (!(id in this._configCache)) {
17763
+ if (hasDefault) return defaultValue;
17770
17764
  throw new SmplNotFoundError(`Config with id '${id}' not found in cache`);
17771
17765
  }
17772
- const metrics = this._parent?._metrics;
17773
- if (metrics) {
17774
- metrics.record("config.resolutions", 1, "resolutions", { config: id });
17766
+ const values = this._configCache[id];
17767
+ if (!(key in values)) {
17768
+ if (hasDefault) return defaultValue;
17769
+ throw new SmplNotFoundError(`Config item '${key}' not found in config '${id}'`);
17775
17770
  }
17776
- return this._cachedProxy(id, model);
17771
+ return values[key];
17777
17772
  }
17778
- /**
17779
- * Declare a configuration from code; return a live, dict-like view.
17780
- *
17781
- * Idempotent. Repeated calls with the same `id` return the same
17782
- * {@link LiveConfigProxy} instance. The first call queues a discovery
17783
- * payload (the config and any items declared via typed getters on the
17784
- * returned handle) for upload to `POST /api/v1/configs/bulk` on next
17785
- * flush. If the config already exists server-side, `managed=true`
17786
- * configs are left untouched; `managed=false` configs receive the
17787
- * SDK's items via source-row upsert per ADR-024 §2.9.
17788
- *
17789
- * Unlike {@link get}, this method does NOT raise `NotFoundError` when
17790
- * the id is absent from the cache discovery handles that case.
17791
- *
17792
- * Mirrors Python's `client.config.get_or_create(id, ...)`.
17793
- */
17794
- async getOrCreate(id, options = {}) {
17795
- const parent = options.parent;
17796
- const parentId = parent instanceof LiveConfigProxy ? parent._key : parent ?? null;
17797
- this._observeConfigDeclaration(id, parentId, options.name ?? null, options.description ?? null);
17798
- await this._ensureInitialized();
17799
- return this._cachedProxy(id, options.model);
17773
+ // ------------------------------------------------------------------
17774
+ // Internal: binding helpers
17775
+ // ------------------------------------------------------------------
17776
+ /** @internal return the config_id this object was bound under, or null. */
17777
+ _configIdFor(target) {
17778
+ for (const [cid, bound] of this._bindings) {
17779
+ if (bound === target) return cid;
17780
+ }
17781
+ return null;
17782
+ }
17783
+ /** @internal — apply current cached values to a freshly-bound target. */
17784
+ _syncTargetFromCache(target, configId) {
17785
+ const cache = this._configCache[configId];
17786
+ if (!cache) return;
17787
+ for (const [dottedKey, value] of Object.entries(cache)) {
17788
+ applyChangeToTarget(target, dottedKey, value);
17789
+ }
17800
17790
  }
17801
17791
  /** @internal — return (and cache) the canonical proxy for a config id. */
17802
- _cachedProxy(id, model) {
17792
+ _cachedProxy(id) {
17803
17793
  let proxy = this._proxies[id];
17804
17794
  if (!proxy) {
17805
- proxy = new LiveConfigProxy(this, id, model);
17795
+ proxy = new LiveConfigProxy(this, id);
17806
17796
  this._proxies[id] = proxy;
17807
- } else if (model !== void 0 && proxy._model === void 0) {
17808
- proxy._model = model;
17809
17797
  }
17810
17798
  return proxy;
17811
17799
  }
@@ -17873,7 +17861,7 @@ var ConfigClient = class {
17873
17861
  */
17874
17862
  async refresh() {
17875
17863
  if (!this._initialized) {
17876
- throw new SmplError("Config not initialized. Call get() first.");
17864
+ throw new SmplError("Config not initialized. Call get() or bind() first.");
17877
17865
  }
17878
17866
  const environment = this._parent?._environment;
17879
17867
  if (!environment) {
@@ -17897,10 +17885,6 @@ var ConfigClient = class {
17897
17885
  * (set via `_resolveManagement`) so runtime + management share one HTTP
17898
17886
  * client; falls back to a direct GET when running without `SmplClient`
17899
17887
  * bootstrap (e.g. unit tests that construct `ConfigClient` directly).
17900
- *
17901
- * Pages through the server until a short page (less than the requested
17902
- * size) is returned — accounts with more than 1000 configs would
17903
- * otherwise silently lose everything past page one.
17904
17888
  */
17905
17889
  async _listConfigs() {
17906
17890
  const PAGE_SIZE = 1e3;
@@ -17942,7 +17926,8 @@ var ConfigClient = class {
17942
17926
  * Eagerly initialize the config subclient — fetch all configs, resolve
17943
17927
  * environment-scoped values into the local cache, and subscribe to the
17944
17928
  * shared WebSocket for live updates. Idempotent. Called automatically
17945
- * on first `client.config.get(...)` if not invoked manually.
17929
+ * on first `client.config.get(...)` / `client.config.bind(...)` if not
17930
+ * invoked manually.
17946
17931
  */
17947
17932
  async start() {
17948
17933
  return this._ensureInitialized();
@@ -18078,10 +18063,14 @@ var ConfigClient = class {
18078
18063
  const oldItems = oldCache[cfgKey] ?? {};
18079
18064
  const newItems = newCache[cfgKey] ?? {};
18080
18065
  const allItemKeys = /* @__PURE__ */ new Set([...Object.keys(oldItems), ...Object.keys(newItems)]);
18066
+ const target = this._bindings.get(cfgKey);
18081
18067
  for (const iKey of allItemKeys) {
18082
18068
  const oldVal = iKey in oldItems ? oldItems[iKey] : null;
18083
18069
  const newVal = iKey in newItems ? newItems[iKey] : null;
18084
18070
  if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
18071
+ if (target !== void 0) {
18072
+ applyChangeToTarget(target, iKey, newVal);
18073
+ }
18085
18074
  const metrics = this._parent?._metrics;
18086
18075
  if (metrics) {
18087
18076
  metrics.record("config.changes", 1, "changes", { config: cfgKey });