@smplkit/sdk 1.1.10 → 1.2.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,178 +1,48 @@
1
1
  /**
2
- * Deep-merge resolution algorithm for config inheritance chains.
2
+ * Shared WebSocket connection to the app service event gateway.
3
3
  *
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 (unwrapped from typed item definitions). */
11
- items: Record<string, unknown>;
12
- /**
13
- * Per-environment overrides.
14
- * Each entry is `{ values: { key: value, ... } }` — values are already
15
- * unwrapped from the server's `{ value: raw }` wrapper by the client layer.
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.
4
+ * A single {@link SharedWebSocket} instance is shared across all product
5
+ * modules (config, flags) within one {@link SmplClient}. Product modules
6
+ * register listeners for specific event types; the shared connection
7
+ * dispatches incoming events to the appropriate listeners.
58
8
  *
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.
62
- *
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.
9
+ * Protocol:
10
+ * - Connect to `wss://app.smplkit.com/api/ws/v1/events?api_key={key}`
11
+ * - Receive `{"type": "connected"}` on success
12
+ * - Receive events: `{"event": "config_changed", ...}`, `{"event": "flag_changed", ...}`
13
+ * - No subscribe message the API key determines the account
14
+ * - Heartbeat: server sends `ping`, client responds with `pong`
15
+ * - Reconnect with exponential backoff
66
16
  */
67
-
68
- /** @internal Options for constructing a ConfigRuntime. */
69
- interface ConfigRuntimeOptions {
70
- configKey: string;
71
- configId: string;
72
- environment: string;
73
- chain: ChainConfig[];
74
- apiKey: string;
75
- baseUrl: string;
76
- fetchChain: (() => Promise<ChainConfig[]>) | null;
77
- }
17
+ type EventCallback = (data: Record<string, any>) => void;
78
18
  /**
79
- * Runtime configuration handle for a specific environment.
19
+ * Manages a single WebSocket connection to the app service event gateway.
80
20
  *
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.
21
+ * Shared across config and flags modules for efficient multiplexing.
85
22
  */
86
- declare class ConfigRuntime {
87
- private _cache;
88
- private _chain;
89
- private _fetchCount;
90
- private _lastFetchAt;
23
+ declare class SharedWebSocket {
24
+ private readonly _appBaseUrl;
25
+ private readonly _apiKey;
26
+ private _listeners;
27
+ private _connectionStatus;
91
28
  private _closed;
92
- private _wsStatus;
93
29
  private _ws;
94
30
  private _reconnectTimer;
95
31
  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);
104
- /**
105
- * Return the resolved value for `key`, or `defaultValue` if absent.
106
- *
107
- * @param key - The config key to look up.
108
- * @param defaultValue - Returned when the key is not present (default: null).
109
- */
110
- get(key: string, defaultValue?: unknown): unknown;
111
- /**
112
- * Return the value as a string, or `defaultValue` if absent or not a string.
113
- */
114
- getString(key: string, defaultValue?: string | null): string | null;
115
- /**
116
- * Return the value as a number, or `defaultValue` if absent or not a number.
117
- */
118
- getInt(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.
129
- */
130
- getAll(): Record<string, unknown>;
131
- /**
132
- * Register a listener that fires when a config value changes.
133
- *
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.
137
- */
138
- onChange(callback: (event: ConfigChangeEvent) => void, options?: {
139
- key?: string;
140
- }): void;
141
- /**
142
- * Return diagnostic statistics for this runtime.
143
- */
144
- stats(): ConfigStats;
145
- /**
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.
160
- *
161
- * Shuts down the WebSocket and cancels any pending reconnect timer.
162
- * Safe to call multiple times.
163
- */
164
- close(): Promise<void>;
165
- /**
166
- * Async dispose support for `await using` (TypeScript 5.2+).
167
- */
168
- [Symbol.asyncDispose](): Promise<void>;
32
+ constructor(appBaseUrl: string, apiKey: string);
33
+ /** Register a listener for a specific event type. */
34
+ on(eventName: string, callback: EventCallback): void;
35
+ /** Unregister a listener for a specific event type. */
36
+ off(eventName: string, callback: EventCallback): void;
37
+ private _dispatch;
38
+ get connectionStatus(): string;
39
+ /** Start the WebSocket connection. */
40
+ start(): void;
41
+ /** Stop the WebSocket connection. */
42
+ stop(): void;
169
43
  private _buildWsUrl;
170
- private _connectWebSocket;
44
+ private _connect;
171
45
  private _scheduleReconnect;
172
- private _handleMessage;
173
- private _applyChanges;
174
- private _diffAndFire;
175
- private _fireListeners;
176
46
  }
177
47
 
178
48
  /**
@@ -182,7 +52,6 @@ declare class ConfigRuntime {
182
52
  * management-plane operations (`update`, `setValues`, `setValue`) as well
183
53
  * as the {@link connect} entry point for runtime value resolution.
184
54
  */
185
-
186
55
  /**
187
56
  * Internal type used by {@link ConfigClient}. Not part of the public API.
188
57
  * @internal
@@ -239,6 +108,7 @@ declare class Config {
239
108
  }): Promise<Config>;
240
109
  readonly _apiKey: string;
241
110
  readonly _baseUrl: string;
111
+ _getSharedWs?: () => SharedWebSocket;
242
112
  }, fields: {
243
113
  id: string;
244
114
  key: string;
@@ -288,37 +158,15 @@ declare class Config {
288
158
  * @param environment - Target environment, or omit for base values.
289
159
  */
290
160
  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
161
  /**
318
162
  * Walk the parent chain and return config data objects, child-to-root.
319
163
  * @internal
320
164
  */
321
- private _buildChain;
165
+ _buildChain(_timeout?: unknown): Promise<Array<{
166
+ id: string;
167
+ items: Record<string, unknown>;
168
+ environments: Record<string, unknown>;
169
+ }>>;
322
170
  toString(): string;
323
171
  }
324
172
  /** Options for creating a new config. */
@@ -342,14 +190,6 @@ interface GetConfigOptions {
342
190
  id?: string;
343
191
  }
344
192
 
345
- /**
346
- * ConfigClient — management-plane operations for configs.
347
- *
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.
351
- */
352
-
353
193
  /**
354
194
  * Client for the smplkit Config API (management plane).
355
195
  *
@@ -365,6 +205,15 @@ declare class ConfigClient {
365
205
  readonly _baseUrl: string;
366
206
  /** @internal */
367
207
  private readonly _http;
208
+ /** @internal — returns the shared WebSocket for real-time updates. */
209
+ _getSharedWs?: () => SharedWebSocket;
210
+ /** @internal — set by SmplClient after construction. */
211
+ _parent: {
212
+ readonly _environment: string;
213
+ readonly _service: string | null;
214
+ } | null;
215
+ private _configCache;
216
+ private _connected;
368
217
  /** @internal */
369
218
  constructor(apiKey: string, timeout?: number);
370
219
  /**
@@ -392,6 +241,23 @@ declare class ConfigClient {
392
241
  * @throws {SmplConflictError} If the config has child configs.
393
242
  */
394
243
  delete(configId: string): Promise<void>;
244
+ /**
245
+ * Fetch all configs, resolve values for the environment, and cache.
246
+ * @internal — called by SmplClient.connect().
247
+ */
248
+ _connectInternal(environment: string): Promise<void>;
249
+ /**
250
+ * Read a resolved config value (prescriptive access).
251
+ *
252
+ * Requires {@link SmplClient.connect} to have been called.
253
+ *
254
+ * @param configKey - The config key to look up.
255
+ * @param itemKey - Optional specific item key. If omitted, returns all values.
256
+ * @param defaultValue - Default value if the key is missing.
257
+ *
258
+ * @throws {SmplNotConnectedError} If connect() has not been called.
259
+ */
260
+ getValue(configKey: string, itemKey?: string, defaultValue?: unknown): unknown;
395
261
  /**
396
262
  * Internal: PUT a full config update and return the updated model.
397
263
  *
@@ -403,6 +269,388 @@ declare class ConfigClient {
403
269
  private _getByKey;
404
270
  }
405
271
 
272
+ /**
273
+ * Flag and ContextType resource models returned by the management API.
274
+ */
275
+
276
+ /**
277
+ * A flag resource returned by {@link FlagsClient} management methods.
278
+ *
279
+ * Provides `update()` for partial updates and `addRule()` for
280
+ * conveniently appending a rule to an environment.
281
+ */
282
+ declare class Flag {
283
+ /** UUID of the flag. */
284
+ id: string;
285
+ /** Unique key within the account. */
286
+ key: string;
287
+ /** Human-readable display name. */
288
+ name: string;
289
+ /** Value type: BOOLEAN, STRING, NUMERIC, or JSON. */
290
+ type: string;
291
+ /** Flag-level default value. */
292
+ default: unknown;
293
+ /** Closed set of possible values. */
294
+ values: Array<{
295
+ name: string;
296
+ value: unknown;
297
+ }>;
298
+ /** Optional description. */
299
+ description: string | null;
300
+ /** Per-environment configuration. */
301
+ environments: Record<string, any>;
302
+ /** When the flag was created. */
303
+ createdAt: string | null;
304
+ /** When the flag was last updated. */
305
+ updatedAt: string | null;
306
+ /** @internal */
307
+ private readonly _client;
308
+ /** @internal */
309
+ constructor(client: FlagsClient, fields: {
310
+ id: string;
311
+ key: string;
312
+ name: string;
313
+ type: string;
314
+ default: unknown;
315
+ values: Array<{
316
+ name: string;
317
+ value: unknown;
318
+ }>;
319
+ description: string | null;
320
+ environments: Record<string, any>;
321
+ createdAt: string | null;
322
+ updatedAt: string | null;
323
+ });
324
+ /**
325
+ * Update this flag's attributes on the server.
326
+ *
327
+ * Only provided fields are changed; others retain their current values.
328
+ */
329
+ update(options: {
330
+ environments?: Record<string, any>;
331
+ values?: Array<{
332
+ name: string;
333
+ value: unknown;
334
+ }>;
335
+ default?: unknown;
336
+ description?: string;
337
+ name?: string;
338
+ }): Promise<void>;
339
+ /**
340
+ * Add a rule to a specific environment.
341
+ *
342
+ * The built rule must include an `environment` key (set via
343
+ * `Rule(...).environment("env_key")`). Re-fetches current state
344
+ * first to avoid stale data.
345
+ */
346
+ addRule(builtRule: Record<string, any>): Promise<void>;
347
+ /** @internal */
348
+ _apply(other: Flag): void;
349
+ toString(): string;
350
+ }
351
+ /** A context type resource returned by management API methods. */
352
+ declare class ContextType {
353
+ /** UUID. */
354
+ id: string;
355
+ /** Unique key within the account. */
356
+ key: string;
357
+ /** Human-readable display name. */
358
+ name: string;
359
+ /** Known attributes. */
360
+ attributes: Record<string, any>;
361
+ constructor(fields: {
362
+ id: string;
363
+ key: string;
364
+ name: string;
365
+ attributes: Record<string, any>;
366
+ });
367
+ toString(): string;
368
+ }
369
+
370
+ /**
371
+ * Public types for the Flags SDK: FlagType, Context, Rule.
372
+ */
373
+ /** The value type of a flag. */
374
+ type FlagType = "BOOLEAN" | "STRING" | "NUMERIC" | "JSON";
375
+ /**
376
+ * A typed evaluation context entity.
377
+ *
378
+ * Represents a single entity (user, account, device, etc.) in the
379
+ * evaluation context. The *type* and *key* identify the entity;
380
+ * *attributes* carry the data that JSON Logic rules target.
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * new Context("user", "user-123", { plan: "enterprise", firstName: "Alice" })
385
+ * new Context("user", "user-123", { plan: "enterprise" }, { name: "Alice Smith" })
386
+ * ```
387
+ */
388
+ declare class Context {
389
+ readonly type: string;
390
+ readonly key: string;
391
+ readonly name: string | null;
392
+ readonly attributes: Record<string, unknown>;
393
+ constructor(type: string, key: string, attributes?: Record<string, unknown>, options?: {
394
+ name?: string;
395
+ });
396
+ toString(): string;
397
+ }
398
+ /**
399
+ * Fluent builder for JSON Logic rule dicts.
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * new Rule("Enable for enterprise users")
404
+ * .when("user.plan", "==", "enterprise")
405
+ * .when("account.region", "==", "us")
406
+ * .serve(true)
407
+ * .build()
408
+ * ```
409
+ *
410
+ * Multiple `.when()` calls are AND'd. `.environment()` tags the
411
+ * built dict with an environment key for use with `Flag.addRule()`.
412
+ */
413
+ declare class Rule {
414
+ private _description;
415
+ private _conditions;
416
+ private _value;
417
+ private _environment;
418
+ constructor(description: string);
419
+ /** Tag this rule with an environment key (used by `addRule`). */
420
+ environment(envKey: string): Rule;
421
+ /** Add a condition. Multiple calls are AND'd. */
422
+ when(variable: string, op: string, value: any): Rule;
423
+ /** Set the value returned when this rule matches. */
424
+ serve(value: any): Rule;
425
+ /** Finalize and return the rule as a plain object. */
426
+ build(): Record<string, any>;
427
+ }
428
+
429
+ /**
430
+ * FlagsClient — management + prescriptive runtime for Smpl Flags.
431
+ *
432
+ * Uses the generated OpenAPI types (`src/generated/flags.d.ts`) via
433
+ * `openapi-fetch` for all HTTP calls. Context type management and
434
+ * context registration use direct HTTP via the Transport class since
435
+ * these endpoints are on the flags service but not in the generated spec.
436
+ */
437
+
438
+ /** Describes a flag definition change. */
439
+ declare class FlagChangeEvent {
440
+ readonly key: string;
441
+ readonly source: string;
442
+ constructor(key: string, source: string);
443
+ }
444
+ /** Cache statistics for the flags runtime. */
445
+ declare class FlagStats {
446
+ readonly cacheHits: number;
447
+ readonly cacheMisses: number;
448
+ constructor(cacheHits: number, cacheMisses: number);
449
+ }
450
+ /** @internal */
451
+ declare class FlagHandleBase {
452
+ /** @internal */ readonly _namespace: FlagsClient;
453
+ /** @internal */ readonly _key: string;
454
+ /** @internal */ readonly _default: any;
455
+ /** @internal */ _listeners: Array<(event: FlagChangeEvent) => void>;
456
+ constructor(namespace: FlagsClient, key: string, defaultValue: any);
457
+ get key(): string;
458
+ get default(): any;
459
+ get(options?: {
460
+ context?: Context[];
461
+ }): any;
462
+ /** Register a flag-specific change listener. Works as a decorator. */
463
+ onChange(callback: (event: FlagChangeEvent) => void): (event: FlagChangeEvent) => void;
464
+ }
465
+ /** Typed handle for a boolean flag. */
466
+ declare class BoolFlagHandle extends FlagHandleBase {
467
+ get(options?: {
468
+ context?: Context[];
469
+ }): boolean;
470
+ }
471
+ /** Typed handle for a string flag. */
472
+ declare class StringFlagHandle extends FlagHandleBase {
473
+ get(options?: {
474
+ context?: Context[];
475
+ }): string;
476
+ }
477
+ /** Typed handle for a numeric flag. */
478
+ declare class NumberFlagHandle extends FlagHandleBase {
479
+ get(options?: {
480
+ context?: Context[];
481
+ }): number;
482
+ }
483
+ /** Typed handle for a JSON flag. */
484
+ declare class JsonFlagHandle extends FlagHandleBase {
485
+ get(options?: {
486
+ context?: Context[];
487
+ }): Record<string, any>;
488
+ }
489
+ /**
490
+ * Client for the smplkit Flags API — management plane + prescriptive runtime.
491
+ *
492
+ * Obtained via `SmplClient.flags`.
493
+ */
494
+ declare class FlagsClient {
495
+ /** @internal */
496
+ readonly _apiKey: string;
497
+ /** @internal */
498
+ readonly _baseUrl: string;
499
+ /** @internal */
500
+ private readonly _http;
501
+ /** @internal */
502
+ private readonly _transport;
503
+ private _environment;
504
+ private _flagStore;
505
+ private _connected;
506
+ private _cache;
507
+ private _contextProvider;
508
+ private _contextBuffer;
509
+ private _handles;
510
+ private _globalListeners;
511
+ private _wsManager;
512
+ private readonly _ensureWs;
513
+ /** @internal — set by SmplClient after construction. */
514
+ _parent: {
515
+ readonly _environment: string;
516
+ readonly _service: string | null;
517
+ } | null;
518
+ /** @internal */
519
+ constructor(apiKey: string, ensureWs: () => SharedWebSocket, timeout?: number);
520
+ /** Create a flag. */
521
+ create(key: string, options: {
522
+ name: string;
523
+ type: FlagType;
524
+ default: unknown;
525
+ description?: string;
526
+ values?: Array<{
527
+ name: string;
528
+ value: unknown;
529
+ }>;
530
+ }): Promise<Flag>;
531
+ /** Fetch a flag by UUID. */
532
+ get(flagId: string): Promise<Flag>;
533
+ /** List all flags. */
534
+ list(): Promise<Flag[]>;
535
+ /** Delete a flag by UUID. */
536
+ delete(flagId: string): Promise<void>;
537
+ /**
538
+ * Internal: PUT a full flag update.
539
+ * Called by {@link Flag} instance methods.
540
+ * @internal
541
+ */
542
+ _updateFlag(options: {
543
+ flag: Flag;
544
+ environments?: Record<string, any>;
545
+ values?: Array<{
546
+ name: string;
547
+ value: unknown;
548
+ }>;
549
+ default?: unknown;
550
+ description?: string;
551
+ name?: string;
552
+ }): Promise<Flag>;
553
+ /** Create a context type. */
554
+ createContextType(key: string, options: {
555
+ name: string;
556
+ }): Promise<ContextType>;
557
+ /** Update a context type (merge attributes). */
558
+ updateContextType(ctId: string, options: {
559
+ attributes: Record<string, any>;
560
+ }): Promise<ContextType>;
561
+ /** List all context types. */
562
+ listContextTypes(): Promise<ContextType[]>;
563
+ /** Delete a context type. */
564
+ deleteContextType(ctId: string): Promise<void>;
565
+ /** List context instances filtered by context type key. */
566
+ listContexts(options: {
567
+ contextTypeKey: string;
568
+ }): Promise<any[]>;
569
+ /** Declare a boolean flag handle. */
570
+ boolFlag(key: string, defaultValue: boolean): BoolFlagHandle;
571
+ /** Declare a string flag handle. */
572
+ stringFlag(key: string, defaultValue: string): StringFlagHandle;
573
+ /** Declare a numeric flag handle. */
574
+ numberFlag(key: string, defaultValue: number): NumberFlagHandle;
575
+ /** Declare a JSON flag handle. */
576
+ jsonFlag(key: string, defaultValue: Record<string, any>): JsonFlagHandle;
577
+ /**
578
+ * Register a context provider function.
579
+ *
580
+ * Called on every `handle.get()` to supply the current evaluation
581
+ * context. Can also be used as a decorator:
582
+ *
583
+ * ```typescript
584
+ * client.flags.setContextProvider(() => [
585
+ * new Context("user", userId, { plan: userPlan }),
586
+ * ]);
587
+ * ```
588
+ */
589
+ setContextProvider(fn: () => Context[]): void;
590
+ /**
591
+ * Register a context provider — decorator-style alias.
592
+ *
593
+ * ```typescript
594
+ * const provider = client.flags.contextProvider(() => [...]);
595
+ * ```
596
+ */
597
+ contextProvider(fn: () => Context[]): () => Context[];
598
+ /**
599
+ * Connect to an environment: fetch flag definitions, register on
600
+ * shared WebSocket, enable local evaluation.
601
+ * @internal — called by SmplClient.connect().
602
+ */
603
+ _connectInternal(environment: string): Promise<void>;
604
+ /** Disconnect: unregister from WebSocket, flush contexts, clear state. */
605
+ disconnect(): Promise<void>;
606
+ /** Re-fetch all flag definitions and clear cache. */
607
+ refresh(): Promise<void>;
608
+ /** Return the current WebSocket connection status. */
609
+ connectionStatus(): string;
610
+ /** Return cache statistics. */
611
+ stats(): FlagStats;
612
+ /** Register a global change listener that fires for any flag change. */
613
+ onChangeAny(callback: (event: FlagChangeEvent) => void): (event: FlagChangeEvent) => void;
614
+ /**
615
+ * Register a global change listener — decorator-style alias.
616
+ *
617
+ * ```typescript
618
+ * const listener = client.flags.onChange((event) => { ... });
619
+ * ```
620
+ */
621
+ onChange(callback: (event: FlagChangeEvent) => void): (event: FlagChangeEvent) => void;
622
+ /**
623
+ * Explicitly register context(s) for background batch registration.
624
+ *
625
+ * Accepts a single Context or an array. Fire-and-forget — never
626
+ * blocks. Works before `connect()` is called.
627
+ */
628
+ register(context: Context | Context[]): void;
629
+ /** Flush pending context registrations to the server. */
630
+ flushContexts(): Promise<void>;
631
+ /**
632
+ * Tier 1 explicit evaluation — stateless, no provider or cache.
633
+ *
634
+ * Useful for scripts, one-off jobs, and infrastructure code.
635
+ */
636
+ evaluate(key: string, options: {
637
+ environment: string;
638
+ context: Context[];
639
+ }): Promise<any>;
640
+ /** @internal */
641
+ _evaluateHandle(key: string, defaultValue: any, context: Context[] | null): any;
642
+ private _handleFlagChanged;
643
+ private _handleFlagDeleted;
644
+ private _fetchAllFlags;
645
+ private _fetchFlagsList;
646
+ private _fireChangeListeners;
647
+ private _fireChangeListenersAll;
648
+ private _flushContexts;
649
+ private _resourceToModel;
650
+ private _resourceToPlainDict;
651
+ private _parseContextType;
652
+ }
653
+
406
654
  /**
407
655
  * Top-level SDK client — SmplClient.
408
656
  *
@@ -418,6 +666,17 @@ interface SmplClientOptions {
418
666
  * environment variable or the `~/.smplkit` configuration file.
419
667
  */
420
668
  apiKey?: string;
669
+ /**
670
+ * The environment to connect to (e.g. `"production"`, `"staging"`).
671
+ * When omitted, resolved from the `SMPLKIT_ENVIRONMENT` environment variable.
672
+ */
673
+ environment?: string;
674
+ /**
675
+ * Optional service name. When set, the SDK automatically registers
676
+ * the service as a context instance and includes it in flag
677
+ * evaluation context.
678
+ */
679
+ service?: string;
421
680
  /**
422
681
  * Request timeout in milliseconds.
423
682
  * @default 30000
@@ -431,14 +690,208 @@ interface SmplClientOptions {
431
690
  * ```typescript
432
691
  * import { SmplClient } from "@smplkit/sdk";
433
692
  *
434
- * const client = new SmplClient({ apiKey: "sk_api_..." });
435
- * const cfg = await client.config.get({ key: "common" });
693
+ * const client = new SmplClient({ apiKey: "sk_api_...", environment: "production" });
694
+ * await client.connect();
436
695
  * ```
437
696
  */
438
697
  declare class SmplClient {
439
698
  /** Client for config management-plane operations. */
440
699
  readonly config: ConfigClient;
700
+ /** Client for flags management and runtime operations. */
701
+ readonly flags: FlagsClient;
702
+ private _wsManager;
703
+ private readonly _apiKey;
704
+ /** @internal */
705
+ readonly _environment: string;
706
+ /** @internal */
707
+ readonly _service: string | null;
708
+ private _connected;
709
+ private readonly _timeout;
441
710
  constructor(options?: SmplClientOptions);
711
+ /**
712
+ * Connect to the smplkit platform.
713
+ *
714
+ * Fetches initial flag and config data, opens the shared WebSocket,
715
+ * and registers the service as a context instance (if provided).
716
+ *
717
+ * This method is idempotent — calling it multiple times is safe.
718
+ */
719
+ connect(): Promise<void>;
720
+ /** @internal */
721
+ private _registerServiceContext;
722
+ /** Lazily create and start the shared WebSocket. @internal */
723
+ private _ensureWs;
724
+ /** Close the shared WebSocket and release resources. */
725
+ close(): void;
726
+ }
727
+
728
+ /**
729
+ * Deep-merge resolution algorithm for config inheritance chains.
730
+ *
731
+ * Mirrors the Python SDK's `_resolver.py` (ADR-024 §2.5–2.6).
732
+ */
733
+ /** A single entry in a config inheritance chain (child-to-root ordering). */
734
+ interface ChainConfig {
735
+ /** Config UUID. */
736
+ id: string;
737
+ /** Base key-value pairs (unwrapped from typed item definitions). */
738
+ items: Record<string, unknown>;
739
+ /**
740
+ * Per-environment overrides.
741
+ * Each entry is `{ values: { key: value, ... } }` — values are already
742
+ * unwrapped from the server's `{ value: raw }` wrapper by the client layer.
743
+ */
744
+ environments: Record<string, unknown>;
745
+ }
746
+
747
+ /**
748
+ * Types for the config runtime plane.
749
+ */
750
+ /** Describes a single value change pushed by the server or detected on refresh. */
751
+ interface ConfigChangeEvent {
752
+ /** The config key that changed. */
753
+ key: string;
754
+ /** The previous value (null if the key was absent). */
755
+ oldValue: unknown;
756
+ /** The updated value (null if the key was removed). */
757
+ newValue: unknown;
758
+ /** How the change was delivered. */
759
+ source: "websocket" | "poll" | "manual";
760
+ }
761
+ /** Diagnostic statistics for a {@link ConfigRuntime} instance. */
762
+ interface ConfigStats {
763
+ /**
764
+ * Total number of HTTP fetches performed, including the initial connect
765
+ * and any reconnection re-syncs or manual refreshes. Incremented by the
766
+ * chain length (number of configs fetched) on each fetch.
767
+ */
768
+ fetchCount: number;
769
+ /** ISO-8601 timestamp of the most recent fetch, or null if none yet. */
770
+ lastFetchAt: string | null;
771
+ }
772
+ /** WebSocket connection status. */
773
+ type ConnectionStatus = "connected" | "connecting" | "disconnected";
774
+ /** Options for {@link Config.connect}. */
775
+ interface ConnectOptions {
776
+ /**
777
+ * Maximum milliseconds to wait for the initial fetch.
778
+ * @default 30000
779
+ */
780
+ timeout?: number;
781
+ }
782
+
783
+ /**
784
+ * ConfigRuntime — runtime-plane value resolution with WebSocket updates.
785
+ *
786
+ * Holds a fully resolved local cache of config values for a specific
787
+ * environment. All value-access methods are synchronous (local reads);
788
+ * only {@link refresh} and {@link close} are async.
789
+ *
790
+ * A background WebSocket connection is maintained for real-time updates.
791
+ * If the WebSocket fails, the runtime operates in cache-only mode and
792
+ * reconnects automatically with exponential backoff.
793
+ */
794
+
795
+ /** @internal Options for constructing a ConfigRuntime. */
796
+ interface ConfigRuntimeOptions {
797
+ configKey: string;
798
+ configId: string;
799
+ environment: string;
800
+ chain: ChainConfig[];
801
+ apiKey: string;
802
+ baseUrl: string;
803
+ fetchChain: (() => Promise<ChainConfig[]>) | null;
804
+ sharedWs?: SharedWebSocket | null;
805
+ }
806
+ /**
807
+ * Runtime configuration handle for a specific environment.
808
+ *
809
+ * Obtained by calling {@link Config.connect}. All value-access methods
810
+ * are synchronous and served entirely from a local in-process cache.
811
+ * The cache is populated eagerly on construction and kept current via
812
+ * a background WebSocket connection.
813
+ */
814
+ declare class ConfigRuntime {
815
+ private _cache;
816
+ private _chain;
817
+ private _fetchCount;
818
+ private _lastFetchAt;
819
+ private _closed;
820
+ private _listeners;
821
+ private readonly _environment;
822
+ private readonly _fetchChain;
823
+ private _sharedWs;
824
+ /** @internal */
825
+ constructor(options: ConfigRuntimeOptions);
826
+ /**
827
+ * Return the resolved value for `key`, or `defaultValue` if absent.
828
+ *
829
+ * @param key - The config key to look up.
830
+ * @param defaultValue - Returned when the key is not present (default: null).
831
+ */
832
+ get(key: string, defaultValue?: unknown): unknown;
833
+ /**
834
+ * Return the value as a string, or `defaultValue` if absent or not a string.
835
+ */
836
+ getString(key: string, defaultValue?: string | null): string | null;
837
+ /**
838
+ * Return the value as a number, or `defaultValue` if absent or not a number.
839
+ */
840
+ getInt(key: string, defaultValue?: number | null): number | null;
841
+ /**
842
+ * Return the value as a boolean, or `defaultValue` if absent or not a boolean.
843
+ */
844
+ getBool(key: string, defaultValue?: boolean | null): boolean | null;
845
+ /**
846
+ * Return whether `key` is present in the resolved configuration.
847
+ */
848
+ exists(key: string): boolean;
849
+ /**
850
+ * Return a shallow copy of the full resolved configuration.
851
+ */
852
+ getAll(): Record<string, unknown>;
853
+ /**
854
+ * Register a listener that fires when a config value changes.
855
+ *
856
+ * @param callback - Called with a {@link ConfigChangeEvent} on each change.
857
+ * @param options.key - If provided, the listener fires only for this key.
858
+ * If omitted, the listener fires for all changes.
859
+ */
860
+ onChange(callback: (event: ConfigChangeEvent) => void, options?: {
861
+ key?: string;
862
+ }): void;
863
+ /**
864
+ * Return diagnostic statistics for this runtime.
865
+ */
866
+ stats(): ConfigStats;
867
+ /**
868
+ * Return the current WebSocket connection status.
869
+ */
870
+ connectionStatus(): ConnectionStatus;
871
+ /**
872
+ * Force a manual refresh of the cached configuration.
873
+ *
874
+ * Re-fetches the full config chain via HTTP, re-resolves values, updates
875
+ * the local cache, and fires listeners for any detected changes.
876
+ *
877
+ * @throws {Error} If no `fetchChain` function was provided on construction.
878
+ */
879
+ refresh(): Promise<void>;
880
+ /**
881
+ * Close the runtime connection.
882
+ *
883
+ * Unregisters from the shared WebSocket. Safe to call multiple times.
884
+ */
885
+ close(): Promise<void>;
886
+ /**
887
+ * Async dispose support for `await using` (TypeScript 5.2+).
888
+ */
889
+ [Symbol.asyncDispose](): Promise<void>;
890
+ private _handleConfigChanged;
891
+ private _handleConfigDeleted;
892
+ private _applyChanges;
893
+ private _diffAndFire;
894
+ private _fireListeners;
442
895
  }
443
896
 
444
897
  /**
@@ -472,9 +925,13 @@ declare class SmplNotFoundError extends SmplError {
472
925
  declare class SmplConflictError extends SmplError {
473
926
  constructor(message: string, statusCode?: number, responseBody?: string);
474
927
  }
928
+ /** Raised when a method requiring connect() is called before connecting. */
929
+ declare class SmplNotConnectedError extends SmplError {
930
+ constructor(message: string);
931
+ }
475
932
  /** Raised when the server rejects a request due to validation errors (HTTP 422). */
476
933
  declare class SmplValidationError extends SmplError {
477
934
  constructor(message: string, statusCode?: number, responseBody?: string);
478
935
  }
479
936
 
480
- export { Config, type ConfigChangeEvent, ConfigClient, ConfigRuntime, type ConfigStats, type ConnectOptions, type ConnectionStatus, type CreateConfigOptions, type GetConfigOptions, SmplClient, type SmplClientOptions, SmplConflictError, SmplConnectionError, SmplError, SmplNotFoundError, SmplTimeoutError, SmplValidationError };
937
+ export { BoolFlagHandle, Config, type ConfigChangeEvent, ConfigClient, ConfigRuntime, type ConfigStats, type ConnectOptions, type ConnectionStatus, Context, ContextType, type CreateConfigOptions, Flag, FlagChangeEvent, FlagStats, type FlagType, FlagsClient, type GetConfigOptions, JsonFlagHandle, NumberFlagHandle, Rule, SharedWebSocket, SmplClient, type SmplClientOptions, SmplConflictError, SmplConnectionError, SmplError, SmplNotConnectedError, SmplNotFoundError, SmplTimeoutError, SmplValidationError, StringFlagHandle };