@smplkit/sdk 1.0.1 → 1.1.1

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.d.ts CHANGED
@@ -1,91 +1,214 @@
1
1
  /**
2
- * Internal HTTP client wrapper.
2
+ * Deep-merge resolution algorithm for config inheritance chains.
3
3
  *
4
- * Uses native `fetch` with `AbortController` for timeouts. Maps network
5
- * errors and HTTP status codes to typed SDK exceptions.
4
+ * Mirrors the Python SDK's `_resolver.py` (ADR-024 §2.5–2.6).
5
+ */
6
+ /** A single entry in a config inheritance chain (child-to-root ordering). */
7
+ interface ChainConfig {
8
+ /** Config UUID. */
9
+ id: string;
10
+ /** Base key-value pairs. */
11
+ values: Record<string, unknown>;
12
+ /**
13
+ * Per-environment overrides.
14
+ * Each entry is `{ values: { key: value, ... } }` — the server wraps
15
+ * environment-specific values in a nested `values` key.
16
+ */
17
+ environments: Record<string, unknown>;
18
+ }
19
+
20
+ /**
21
+ * Types for the config runtime plane.
22
+ */
23
+ /** Describes a single value change pushed by the server or detected on refresh. */
24
+ interface ConfigChangeEvent {
25
+ /** The config key that changed. */
26
+ key: string;
27
+ /** The previous value (null if the key was absent). */
28
+ oldValue: unknown;
29
+ /** The updated value (null if the key was removed). */
30
+ newValue: unknown;
31
+ /** How the change was delivered. */
32
+ source: "websocket" | "poll" | "manual";
33
+ }
34
+ /** Diagnostic statistics for a {@link ConfigRuntime} instance. */
35
+ interface ConfigStats {
36
+ /**
37
+ * Total number of HTTP fetches performed, including the initial connect
38
+ * and any reconnection re-syncs or manual refreshes. Incremented by the
39
+ * chain length (number of configs fetched) on each fetch.
40
+ */
41
+ fetchCount: number;
42
+ /** ISO-8601 timestamp of the most recent fetch, or null if none yet. */
43
+ lastFetchAt: string | null;
44
+ }
45
+ /** WebSocket connection status. */
46
+ type ConnectionStatus = "connected" | "connecting" | "disconnected";
47
+ /** Options for {@link Config.connect}. */
48
+ interface ConnectOptions {
49
+ /**
50
+ * Maximum milliseconds to wait for the initial fetch.
51
+ * @default 30000
52
+ */
53
+ timeout?: number;
54
+ }
55
+
56
+ /**
57
+ * ConfigRuntime — runtime-plane value resolution with WebSocket updates.
58
+ *
59
+ * Holds a fully resolved local cache of config values for a specific
60
+ * environment. All value-access methods are synchronous (local reads);
61
+ * only {@link refresh} and {@link close} are async.
6
62
  *
7
- * @internal This module is not part of the public API.
63
+ * A background WebSocket connection is maintained for real-time updates.
64
+ * If the WebSocket fails, the runtime operates in cache-only mode and
65
+ * reconnects automatically with exponential backoff.
8
66
  */
9
- /** Options for constructing a {@link Transport} instance. */
10
- interface TransportOptions {
11
- /** The API key used for Bearer token authentication. */
67
+
68
+ /** @internal Options for constructing a ConfigRuntime. */
69
+ interface ConfigRuntimeOptions {
70
+ configKey: string;
71
+ configId: string;
72
+ environment: string;
73
+ chain: ChainConfig[];
12
74
  apiKey: string;
13
- /** Base URL for all API requests. Must not have a trailing slash. */
14
75
  baseUrl: string;
15
- /** Request timeout in milliseconds. Defaults to 30 000. */
16
- timeout?: number;
76
+ fetchChain: (() => Promise<ChainConfig[]>) | null;
17
77
  }
18
- /** Parsed JSON response from the API. */
19
- type JsonBody = Record<string, any>;
20
78
  /**
21
- * Low-level HTTP transport that handles auth, timeouts, and error mapping.
79
+ * Runtime configuration handle for a specific environment.
22
80
  *
23
- * @internal
81
+ * Obtained by calling {@link Config.connect}. All value-access methods
82
+ * are synchronous and served entirely from a local in-process cache.
83
+ * The cache is populated eagerly on construction and kept current via
84
+ * a background WebSocket connection.
24
85
  */
25
- declare class Transport {
26
- private readonly apiKey;
27
- private readonly baseUrl;
28
- private readonly timeout;
29
- constructor(options: TransportOptions);
86
+ declare class ConfigRuntime {
87
+ private _cache;
88
+ private _chain;
89
+ private _fetchCount;
90
+ private _lastFetchAt;
91
+ private _closed;
92
+ private _wsStatus;
93
+ private _ws;
94
+ private _reconnectTimer;
95
+ private _backoffIndex;
96
+ private _listeners;
97
+ private readonly _configId;
98
+ private readonly _environment;
99
+ private readonly _apiKey;
100
+ private readonly _baseUrl;
101
+ private readonly _fetchChain;
102
+ /** @internal */
103
+ constructor(options: ConfigRuntimeOptions);
30
104
  /**
31
- * Send a GET request.
105
+ * Return the resolved value for `key`, or `defaultValue` if absent.
32
106
  *
33
- * @param path - URL path relative to `baseUrl` (e.g. `/api/v1/configs`).
34
- * @param params - Optional query parameters.
35
- * @returns Parsed JSON response body.
107
+ * @param key - The config key to look up.
108
+ * @param defaultValue - Returned when the key is not present (default: null).
36
109
  */
37
- get(path: string, params?: Record<string, string>): Promise<JsonBody>;
110
+ get(key: string, defaultValue?: unknown): unknown;
38
111
  /**
39
- * Send a POST request with a JSON body.
40
- *
41
- * @param path - URL path relative to `baseUrl`.
42
- * @param body - JSON-serializable request body.
43
- * @returns Parsed JSON response body.
112
+ * Return the value as a string, or `defaultValue` if absent or not a string.
44
113
  */
45
- post(path: string, body: JsonBody): Promise<JsonBody>;
114
+ getString(key: string, defaultValue?: string | null): string | null;
46
115
  /**
47
- * Send a PUT request with a JSON body.
48
- *
49
- * @param path - URL path relative to `baseUrl`.
50
- * @param body - JSON-serializable request body.
51
- * @returns Parsed JSON response body.
116
+ * Return the value as a number, or `defaultValue` if absent or not a number.
117
+ */
118
+ getNumber(key: string, defaultValue?: number | null): number | null;
119
+ /**
120
+ * Return the value as a boolean, or `defaultValue` if absent or not a boolean.
121
+ */
122
+ getBool(key: string, defaultValue?: boolean | null): boolean | null;
123
+ /**
124
+ * Return whether `key` is present in the resolved configuration.
125
+ */
126
+ exists(key: string): boolean;
127
+ /**
128
+ * Return a shallow copy of the full resolved configuration.
52
129
  */
53
- put(path: string, body: JsonBody): Promise<JsonBody>;
130
+ getAll(): Record<string, unknown>;
54
131
  /**
55
- * Send a DELETE request.
132
+ * Register a listener that fires when a config value changes.
56
133
  *
57
- * @param path - URL path relative to `baseUrl`.
58
- * @returns Parsed JSON response body (empty object for 204 responses).
134
+ * @param callback - Called with a {@link ConfigChangeEvent} on each change.
135
+ * @param options.key - If provided, the listener fires only for this key.
136
+ * If omitted, the listener fires for all changes.
59
137
  */
60
- delete(path: string): Promise<JsonBody>;
138
+ onChange(callback: (event: ConfigChangeEvent) => void, options?: {
139
+ key?: string;
140
+ }): void;
61
141
  /**
62
- * Core request method. Handles headers, timeouts, and error mapping.
142
+ * Return diagnostic statistics for this runtime.
63
143
  */
64
- private request;
144
+ stats(): ConfigStats;
65
145
  /**
66
- * Map HTTP error status codes to typed SDK exceptions.
146
+ * Return the current WebSocket connection status.
147
+ */
148
+ connectionStatus(): ConnectionStatus;
149
+ /**
150
+ * Force a manual refresh of the cached configuration.
151
+ *
152
+ * Re-fetches the full config chain via HTTP, re-resolves values, updates
153
+ * the local cache, and fires listeners for any detected changes.
154
+ *
155
+ * @throws {Error} If no `fetchChain` function was provided on construction.
156
+ */
157
+ refresh(): Promise<void>;
158
+ /**
159
+ * Close the runtime connection.
67
160
  *
68
- * @throws {SmplNotFoundError} On 404.
69
- * @throws {SmplConflictError} On 409.
70
- * @throws {SmplValidationError} On 422.
71
- * @throws {SmplError} On any other non-2xx status.
161
+ * Shuts down the WebSocket and cancels any pending reconnect timer.
162
+ * Safe to call multiple times.
72
163
  */
73
- private throwForStatus;
164
+ close(): Promise<void>;
165
+ /**
166
+ * Async dispose support for `await using` (TypeScript 5.2+).
167
+ */
168
+ [Symbol.asyncDispose](): Promise<void>;
169
+ private _buildWsUrl;
170
+ private _connectWebSocket;
171
+ private _scheduleReconnect;
172
+ private _handleMessage;
173
+ private _applyChanges;
174
+ private _diffAndFire;
175
+ private _fireListeners;
74
176
  }
75
177
 
76
178
  /**
77
- * Config resource types.
179
+ * Config resource — management-plane model with runtime connect support.
180
+ *
181
+ * Instances are returned by {@link ConfigClient} methods and provide
182
+ * management-plane operations (`update`, `setValues`, `setValue`) as well
183
+ * as the {@link connect} entry point for runtime value resolution.
184
+ */
185
+
186
+ /**
187
+ * Internal type used by {@link ConfigClient}. Not part of the public API.
188
+ * @internal
189
+ */
190
+ interface ConfigUpdatePayload {
191
+ configId: string;
192
+ name: string;
193
+ key: string | null;
194
+ description: string | null;
195
+ parent: string | null;
196
+ values: Record<string, unknown>;
197
+ environments: Record<string, unknown>;
198
+ }
199
+ /**
200
+ * A configuration resource fetched from the smplkit Config service.
78
201
  *
79
- * These types represent the domain model for config resources after
80
- * unwrapping the JSON:API envelope.
202
+ * Instances are returned by {@link ConfigClient} methods and provide
203
+ * management-plane operations as well as the {@link connect} entry point
204
+ * for runtime value resolution.
81
205
  */
82
- /** A config resource as returned by the smplkit Config API. */
83
- interface Config {
206
+ declare class Config {
84
207
  /** UUID of the config. */
85
208
  id: string;
86
209
  /** Human-readable key (e.g. `"user_service"`). */
87
210
  key: string;
88
- /** Display name for the config. */
211
+ /** Display name. */
89
212
  name: string;
90
213
  /** Optional description. */
91
214
  description: string | null;
@@ -93,12 +216,110 @@ interface Config {
93
216
  parent: string | null;
94
217
  /** Base key-value pairs. */
95
218
  values: Record<string, unknown>;
96
- /** Per-environment overrides, keyed by environment name. */
97
- environments: Record<string, Record<string, unknown>>;
219
+ /**
220
+ * Per-environment overrides.
221
+ * Stored as `{ env_name: { values: { key: value } } }` to match the
222
+ * server's format.
223
+ */
224
+ environments: Record<string, unknown>;
98
225
  /** When the config was created, or null if unavailable. */
99
226
  createdAt: Date | null;
100
227
  /** When the config was last updated, or null if unavailable. */
101
228
  updatedAt: Date | null;
229
+ /**
230
+ * Internal reference to the parent client.
231
+ * @internal
232
+ */
233
+ private readonly _client;
234
+ /** @internal */
235
+ constructor(client: {
236
+ _updateConfig(payload: ConfigUpdatePayload): Promise<Config>;
237
+ get(options: {
238
+ id: string;
239
+ }): Promise<Config>;
240
+ readonly _apiKey: string;
241
+ readonly _baseUrl: string;
242
+ }, fields: {
243
+ id: string;
244
+ key: string;
245
+ name: string;
246
+ description: string | null;
247
+ parent: string | null;
248
+ values: Record<string, unknown>;
249
+ environments: Record<string, unknown>;
250
+ createdAt: Date | null;
251
+ updatedAt: Date | null;
252
+ });
253
+ /**
254
+ * Update this config's attributes on the server.
255
+ *
256
+ * Builds the request from current attribute values, overriding with any
257
+ * provided options. Updates local attributes in place on success.
258
+ *
259
+ * @param options.name - New display name.
260
+ * @param options.description - New description (pass empty string to clear).
261
+ * @param options.values - New base values (replaces entirely).
262
+ * @param options.environments - New environments dict (replaces entirely).
263
+ */
264
+ update(options: {
265
+ name?: string;
266
+ description?: string;
267
+ values?: Record<string, unknown>;
268
+ environments?: Record<string, unknown>;
269
+ }): Promise<void>;
270
+ /**
271
+ * Replace base or environment-specific values.
272
+ *
273
+ * When `environment` is provided, replaces that environment's `values`
274
+ * sub-dict (other environments are preserved). When omitted, replaces
275
+ * the base `values`.
276
+ *
277
+ * @param values - The complete set of values to set.
278
+ * @param environment - Target environment, or omit for base values.
279
+ */
280
+ setValues(values: Record<string, unknown>, environment?: string): Promise<void>;
281
+ /**
282
+ * Set a single key within base or environment-specific values.
283
+ *
284
+ * Merges the key into existing values rather than replacing all values.
285
+ *
286
+ * @param key - The config key to set.
287
+ * @param value - The value to assign.
288
+ * @param environment - Target environment, or omit for base values.
289
+ */
290
+ setValue(key: string, value: unknown, environment?: string): Promise<void>;
291
+ /**
292
+ * Connect to this config for runtime value resolution.
293
+ *
294
+ * Eagerly fetches this config and its full parent chain, resolves values
295
+ * for the given environment via deep merge, and returns a
296
+ * {@link ConfigRuntime} with a fully populated local cache.
297
+ *
298
+ * A background WebSocket connection is started for real-time updates.
299
+ * If the WebSocket fails to connect, the runtime operates in cache-only
300
+ * mode and reconnects automatically.
301
+ *
302
+ * Supports both `await` and `await using` (TypeScript 5.2+)::
303
+ *
304
+ * ```typescript
305
+ * // Simple await
306
+ * const runtime = await config.connect("production");
307
+ * try { ... } finally { await runtime.close(); }
308
+ *
309
+ * // await using (auto-close)
310
+ * await using runtime = await config.connect("production");
311
+ * ```
312
+ *
313
+ * @param environment - The environment to resolve for (e.g. `"production"`).
314
+ * @param options.timeout - Milliseconds to wait for the initial fetch.
315
+ */
316
+ connect(environment: string, options?: ConnectOptions): Promise<ConfigRuntime>;
317
+ /**
318
+ * Walk the parent chain and return config data objects, child-to-root.
319
+ * @internal
320
+ */
321
+ private _buildChain;
322
+ toString(): string;
102
323
  }
103
324
  /** Options for creating a new config. */
104
325
  interface CreateConfigOptions {
@@ -108,7 +329,7 @@ interface CreateConfigOptions {
108
329
  key?: string;
109
330
  /** Optional description. */
110
331
  description?: string;
111
- /** Parent config UUID. */
332
+ /** Parent config UUID. Defaults to the account's `common` config if omitted. */
112
333
  parent?: string;
113
334
  /** Initial base values. */
114
335
  values?: Record<string, unknown>;
@@ -124,65 +345,62 @@ interface GetConfigOptions {
124
345
  /**
125
346
  * ConfigClient — management-plane operations for configs.
126
347
  *
127
- * Provides CRUD operations on config resources. Obtained via
128
- * `SmplkitClient.config`.
348
+ * Uses the generated OpenAPI types (`src/generated/config.d.ts`) via
349
+ * `openapi-fetch` for all HTTP calls, keeping the client layer fully
350
+ * type-safe without hand-coded request/response shapes.
129
351
  */
130
352
 
131
353
  /**
132
- * Client for the smplkit Config API.
354
+ * Client for the smplkit Config API (management plane).
133
355
  *
134
356
  * All methods are async and return `Promise<T>`. Network and server
135
357
  * errors are mapped to typed SDK exceptions.
358
+ *
359
+ * Obtained via `SmplkitClient.config`.
136
360
  */
137
361
  declare class ConfigClient {
362
+ /** @internal — used by Config instances for reconnecting and WebSocket auth. */
363
+ readonly _apiKey: string;
364
+ /** @internal */
365
+ readonly _baseUrl: string;
138
366
  /** @internal */
139
- private readonly transport;
367
+ private readonly _http;
140
368
  /** @internal */
141
- constructor(transport: Transport);
369
+ constructor(apiKey: string, timeout?: number);
142
370
  /**
143
371
  * Fetch a single config by key or UUID.
144
372
  *
145
373
  * Exactly one of `key` or `id` must be provided.
146
374
  *
147
- * @param options - Lookup options.
148
- * @returns The matching config.
149
375
  * @throws {SmplNotFoundError} If no matching config exists.
150
- * @throws {Error} If neither or both of `key` and `id` are provided.
151
376
  */
152
377
  get(options: GetConfigOptions): Promise<Config>;
153
378
  /**
154
379
  * List all configs for the account.
155
- *
156
- * @returns An array of config objects.
157
380
  */
158
381
  list(): Promise<Config[]>;
159
382
  /**
160
383
  * Create a new config.
161
384
  *
162
- * @param options - Config creation options.
163
- * @returns The created config.
164
385
  * @throws {SmplValidationError} If the server rejects the request.
165
386
  */
166
387
  create(options: CreateConfigOptions): Promise<Config>;
167
388
  /**
168
389
  * Delete a config by UUID.
169
390
  *
170
- * @param configId - The UUID of the config to delete.
171
391
  * @throws {SmplNotFoundError} If the config does not exist.
172
- * @throws {SmplConflictError} If the config has children.
392
+ * @throws {SmplConflictError} If the config has child configs.
173
393
  */
174
394
  delete(configId: string): Promise<void>;
175
- /** Fetch a config by UUID. */
176
- private getById;
177
- /** Fetch a config by key using the list endpoint with a filter. */
178
- private getByKey;
179
395
  /**
180
- * Convert a JSON:API resource to a Config domain model.
396
+ * Internal: PUT a full config update and return the updated model.
397
+ *
398
+ * Called by {@link Config} instance methods.
181
399
  * @internal
182
400
  */
183
- private resourceToModel;
184
- /** Build a JSON:API request body for create operations. */
185
- private buildRequestBody;
401
+ _updateConfig(payload: ConfigUpdatePayload): Promise<Config>;
402
+ private _getById;
403
+ private _getByKey;
186
404
  }
187
405
 
188
406
  /**
@@ -196,11 +414,6 @@ declare class ConfigClient {
196
414
  interface SmplkitClientOptions {
197
415
  /** API key for authenticating with the smplkit platform. */
198
416
  apiKey: string;
199
- /**
200
- * Base URL for all API requests.
201
- * @default "https://config.smplkit.com"
202
- */
203
- baseUrl?: string;
204
417
  /**
205
418
  * Request timeout in milliseconds.
206
419
  * @default 30000
@@ -221,8 +434,6 @@ interface SmplkitClientOptions {
221
434
  declare class SmplkitClient {
222
435
  /** Client for config management-plane operations. */
223
436
  readonly config: ConfigClient;
224
- /** @internal */
225
- private readonly transport;
226
437
  constructor(options: SmplkitClientOptions);
227
438
  }
228
439
 
@@ -262,4 +473,4 @@ declare class SmplValidationError extends SmplError {
262
473
  constructor(message: string, statusCode?: number, responseBody?: string);
263
474
  }
264
475
 
265
- export { type Config, ConfigClient, type CreateConfigOptions, type GetConfigOptions, SmplConflictError, SmplConnectionError, SmplError, SmplNotFoundError, SmplTimeoutError, SmplValidationError, SmplkitClient, type SmplkitClientOptions };
476
+ export { Config, type ConfigChangeEvent, ConfigClient, ConfigRuntime, type ConfigStats, type ConnectOptions, type ConnectionStatus, type CreateConfigOptions, type GetConfigOptions, SmplConflictError, SmplConnectionError, SmplError, SmplNotFoundError, SmplTimeoutError, SmplValidationError, SmplkitClient, type SmplkitClientOptions };