@smplkit/sdk 1.1.9 → 1.2.0

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
@@ -68,41 +68,34 @@ var runtime_exports = {};
68
68
  __export(runtime_exports, {
69
69
  ConfigRuntime: () => ConfigRuntime
70
70
  });
71
- var import_ws, BACKOFF_MS, ConfigRuntime;
71
+ var ConfigRuntime;
72
72
  var init_runtime = __esm({
73
73
  "src/config/runtime.ts"() {
74
74
  "use strict";
75
- import_ws = __toESM(require("ws"), 1);
76
75
  init_resolve();
77
- BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
78
76
  ConfigRuntime = class {
79
77
  _cache;
80
78
  _chain;
81
79
  _fetchCount;
82
80
  _lastFetchAt;
83
81
  _closed = false;
84
- _wsStatus = "disconnected";
85
- _ws = null;
86
- _reconnectTimer = null;
87
- _backoffIndex = 0;
88
82
  _listeners = [];
89
- _configId;
90
83
  _environment;
91
- _apiKey;
92
- _baseUrl;
93
84
  _fetchChain;
85
+ _sharedWs = null;
94
86
  /** @internal */
95
87
  constructor(options) {
96
- this._configId = options.configId;
97
88
  this._environment = options.environment;
98
- this._apiKey = options.apiKey;
99
- this._baseUrl = options.baseUrl;
100
89
  this._fetchChain = options.fetchChain;
101
90
  this._chain = options.chain;
102
91
  this._cache = resolveChain(options.chain, options.environment);
103
92
  this._fetchCount = options.chain.length;
104
93
  this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
105
- this._connectWebSocket();
94
+ if (options.sharedWs) {
95
+ this._sharedWs = options.sharedWs;
96
+ this._sharedWs.on("config_changed", this._handleConfigChanged);
97
+ this._sharedWs.on("config_deleted", this._handleConfigDeleted);
98
+ }
106
99
  }
107
100
  // ---- Value access (synchronous, local cache) ----
108
101
  /**
@@ -175,7 +168,10 @@ var init_runtime = __esm({
175
168
  * Return the current WebSocket connection status.
176
169
  */
177
170
  connectionStatus() {
178
- return this._wsStatus;
171
+ if (this._sharedWs) {
172
+ return this._sharedWs.connectionStatus;
173
+ }
174
+ return "disconnected";
179
175
  }
180
176
  // ---- Lifecycle ----
181
177
  /**
@@ -201,19 +197,14 @@ var init_runtime = __esm({
201
197
  /**
202
198
  * Close the runtime connection.
203
199
  *
204
- * Shuts down the WebSocket and cancels any pending reconnect timer.
205
- * Safe to call multiple times.
200
+ * Unregisters from the shared WebSocket. Safe to call multiple times.
206
201
  */
207
202
  async close() {
208
203
  this._closed = true;
209
- this._wsStatus = "disconnected";
210
- if (this._reconnectTimer !== null) {
211
- clearTimeout(this._reconnectTimer);
212
- this._reconnectTimer = null;
213
- }
214
- if (this._ws !== null) {
215
- this._ws.close();
216
- this._ws = null;
204
+ if (this._sharedWs !== null) {
205
+ this._sharedWs.off("config_changed", this._handleConfigChanged);
206
+ this._sharedWs.off("config_deleted", this._handleConfigDeleted);
207
+ this._sharedWs = null;
217
208
  }
218
209
  }
219
210
  /**
@@ -222,94 +213,29 @@ var init_runtime = __esm({
222
213
  async [Symbol.asyncDispose]() {
223
214
  await this.close();
224
215
  }
225
- // ---- WebSocket internals ----
226
- _buildWsUrl() {
227
- let url = this._baseUrl;
228
- if (url.startsWith("https://")) {
229
- url = "wss://" + url.slice("https://".length);
230
- } else if (url.startsWith("http://")) {
231
- url = "ws://" + url.slice("http://".length);
232
- } else {
233
- url = "wss://" + url;
234
- }
235
- url = url.replace(/\/$/, "");
236
- return `${url}/api/ws/v1/configs?api_key=${this._apiKey}`;
237
- }
238
- _connectWebSocket() {
216
+ // ---- Shared WebSocket event handlers ----
217
+ _handleConfigChanged = (data) => {
239
218
  if (this._closed) return;
240
- this._wsStatus = "connecting";
241
- const wsUrl = this._buildWsUrl();
242
- try {
243
- const ws = new import_ws.default(wsUrl);
244
- this._ws = ws;
245
- ws.on("open", () => {
246
- if (this._closed) {
247
- ws.close();
248
- return;
249
- }
250
- this._backoffIndex = 0;
251
- this._wsStatus = "connected";
252
- ws.send(
253
- JSON.stringify({
254
- type: "subscribe",
255
- config_id: this._configId,
256
- environment: this._environment
257
- })
258
- );
259
- });
260
- ws.on("message", (data) => {
261
- try {
262
- const msg = JSON.parse(String(data));
263
- this._handleMessage(msg);
264
- } catch {
265
- }
266
- });
267
- ws.on("close", () => {
268
- if (!this._closed) {
269
- this._wsStatus = "disconnected";
270
- this._scheduleReconnect();
271
- }
219
+ const configId = data.config_id;
220
+ const changes = data.changes;
221
+ if (configId && changes) {
222
+ this._applyChanges(configId, changes);
223
+ } else if (this._fetchChain) {
224
+ void this._fetchChain().then((newChain) => {
225
+ const oldCache = this._cache;
226
+ this._chain = newChain;
227
+ this._cache = resolveChain(newChain, this._environment);
228
+ this._fetchCount += newChain.length;
229
+ this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
230
+ this._diffAndFire(oldCache, this._cache, "websocket");
231
+ }).catch(() => {
272
232
  });
273
- ws.on("error", () => {
274
- });
275
- } catch {
276
- if (!this._closed) {
277
- this._scheduleReconnect();
278
- }
279
- }
280
- }
281
- _scheduleReconnect() {
282
- if (this._closed) return;
283
- const delay = BACKOFF_MS[Math.min(this._backoffIndex, BACKOFF_MS.length - 1)];
284
- this._backoffIndex++;
285
- this._wsStatus = "connecting";
286
- this._reconnectTimer = setTimeout(() => {
287
- this._reconnectTimer = null;
288
- if (this._fetchChain) {
289
- this._fetchChain().then((newChain) => {
290
- const oldCache = this._cache;
291
- this._chain = newChain;
292
- this._cache = resolveChain(newChain, this._environment);
293
- this._fetchCount += newChain.length;
294
- this._lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
295
- this._diffAndFire(oldCache, this._cache, "manual");
296
- }).catch(() => {
297
- }).finally(() => {
298
- this._connectWebSocket();
299
- });
300
- } else {
301
- this._connectWebSocket();
302
- }
303
- }, delay);
304
- }
305
- _handleMessage(msg) {
306
- if (msg.type === "config_changed") {
307
- this._applyChanges(msg.config_id, msg.changes);
308
- } else if (msg.type === "config_deleted") {
309
- this._closed = true;
310
- void this.close();
311
233
  }
312
- }
234
+ };
235
+ _handleConfigDeleted = (_data) => {
236
+ this._closed = true;
237
+ void this.close();
238
+ };
313
239
  _applyChanges(configId, changes) {
314
240
  const chainEntry = this._chain.find((c) => c.id === configId);
315
241
  if (!chainEntry) return;
@@ -360,16 +286,28 @@ var init_runtime = __esm({
360
286
  // src/index.ts
361
287
  var index_exports = {};
362
288
  __export(index_exports, {
289
+ BoolFlagHandle: () => BoolFlagHandle,
363
290
  Config: () => Config,
364
291
  ConfigClient: () => ConfigClient,
365
292
  ConfigRuntime: () => ConfigRuntime,
293
+ Context: () => Context,
294
+ ContextType: () => ContextType,
295
+ Flag: () => Flag,
296
+ FlagChangeEvent: () => FlagChangeEvent,
297
+ FlagStats: () => FlagStats,
298
+ FlagsClient: () => FlagsClient,
299
+ JsonFlagHandle: () => JsonFlagHandle,
300
+ NumberFlagHandle: () => NumberFlagHandle,
301
+ Rule: () => Rule,
302
+ SharedWebSocket: () => SharedWebSocket,
366
303
  SmplClient: () => SmplClient,
367
304
  SmplConflictError: () => SmplConflictError,
368
305
  SmplConnectionError: () => SmplConnectionError,
369
306
  SmplError: () => SmplError,
370
307
  SmplNotFoundError: () => SmplNotFoundError,
371
308
  SmplTimeoutError: () => SmplTimeoutError,
372
- SmplValidationError: () => SmplValidationError
309
+ SmplValidationError: () => SmplValidationError,
310
+ StringFlagHandle: () => StringFlagHandle
373
311
  });
374
312
  module.exports = __toCommonJS(index_exports);
375
313
 
@@ -588,7 +526,8 @@ var Config = class {
588
526
  chain,
589
527
  apiKey: this._client._apiKey,
590
528
  baseUrl: this._client._baseUrl,
591
- fetchChain: () => this._buildChain(timeout)
529
+ fetchChain: () => this._buildChain(timeout),
530
+ sharedWs: this._client._getSharedWs ? this._client._getSharedWs() : null
592
531
  });
593
532
  }
594
533
  /**
@@ -735,6 +674,8 @@ var ConfigClient = class {
735
674
  _baseUrl = BASE_URL;
736
675
  /** @internal */
737
676
  _http;
677
+ /** @internal — returns the shared WebSocket for real-time updates. */
678
+ _getSharedWs;
738
679
  /** @internal */
739
680
  constructor(apiKey, timeout) {
740
681
  this._apiKey = apiKey;
@@ -899,6 +840,1181 @@ var ConfigClient = class {
899
840
  }
900
841
  };
901
842
 
843
+ // src/flags/client.ts
844
+ var import_openapi_fetch2 = __toESM(require("openapi-fetch"), 1);
845
+
846
+ // src/auth.ts
847
+ function buildAuthHeader(apiKey) {
848
+ return `Bearer ${apiKey}`;
849
+ }
850
+
851
+ // src/transport.ts
852
+ var SDK_VERSION = "0.0.0";
853
+ var DEFAULT_TIMEOUT_MS = 3e4;
854
+ var Transport = class {
855
+ apiKey;
856
+ timeout;
857
+ constructor(options) {
858
+ this.apiKey = options.apiKey;
859
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
860
+ }
861
+ /**
862
+ * Send a GET request.
863
+ *
864
+ * @param url - Fully-qualified URL (e.g. `https://config.smplkit.com/api/v1/configs`).
865
+ * @param params - Optional query parameters.
866
+ * @returns Parsed JSON response body.
867
+ */
868
+ async get(url, params) {
869
+ return this.request("GET", url, void 0, params);
870
+ }
871
+ /**
872
+ * Send a POST request with a JSON body.
873
+ *
874
+ * @param url - Fully-qualified URL.
875
+ * @param body - JSON-serializable request body.
876
+ * @returns Parsed JSON response body.
877
+ */
878
+ async post(url, body) {
879
+ return this.request("POST", url, body);
880
+ }
881
+ /**
882
+ * Send a PUT request with a JSON body.
883
+ *
884
+ * @param url - Fully-qualified URL.
885
+ * @param body - JSON-serializable request body.
886
+ * @returns Parsed JSON response body.
887
+ */
888
+ async put(url, body) {
889
+ return this.request("PUT", url, body);
890
+ }
891
+ /**
892
+ * Send a DELETE request.
893
+ *
894
+ * @param url - Fully-qualified URL.
895
+ * @returns Parsed JSON response body (empty object for 204 responses).
896
+ */
897
+ async delete(url) {
898
+ return this.request("DELETE", url);
899
+ }
900
+ /**
901
+ * Core request method. Handles headers, timeouts, and error mapping.
902
+ */
903
+ async request(method, url, body, params) {
904
+ if (params) {
905
+ const searchParams = new URLSearchParams(params);
906
+ url += `?${searchParams.toString()}`;
907
+ }
908
+ const headers = {
909
+ Authorization: buildAuthHeader(this.apiKey),
910
+ "User-Agent": `smplkit-typescript-sdk/${SDK_VERSION}`,
911
+ Accept: "application/vnd.api+json"
912
+ };
913
+ if (body !== void 0) {
914
+ headers["Content-Type"] = "application/vnd.api+json";
915
+ }
916
+ const controller = new AbortController();
917
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
918
+ let response;
919
+ try {
920
+ response = await fetch(url, {
921
+ method,
922
+ headers,
923
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
924
+ signal: controller.signal
925
+ });
926
+ } catch (error) {
927
+ clearTimeout(timeoutId);
928
+ if (error instanceof DOMException && error.name === "AbortError") {
929
+ throw new SmplTimeoutError(`Request timed out after ${this.timeout}ms`);
930
+ }
931
+ if (error instanceof TypeError) {
932
+ throw new SmplConnectionError(`Network error: ${error.message}`);
933
+ }
934
+ throw new SmplConnectionError(
935
+ `Request failed: ${error instanceof Error ? error.message : String(error)}`
936
+ );
937
+ } finally {
938
+ clearTimeout(timeoutId);
939
+ }
940
+ if (response.status === 204) {
941
+ return {};
942
+ }
943
+ const responseText = await response.text();
944
+ if (!response.ok) {
945
+ this.throwForStatus(response.status, responseText);
946
+ }
947
+ try {
948
+ return JSON.parse(responseText);
949
+ } catch {
950
+ throw new SmplError(`Invalid JSON response: ${responseText}`, response.status, responseText);
951
+ }
952
+ }
953
+ /**
954
+ * Map HTTP error status codes to typed SDK exceptions.
955
+ *
956
+ * @throws {SmplNotFoundError} On 404.
957
+ * @throws {SmplConflictError} On 409.
958
+ * @throws {SmplValidationError} On 422.
959
+ * @throws {SmplError} On any other non-2xx status.
960
+ */
961
+ throwForStatus(status, body) {
962
+ switch (status) {
963
+ case 404:
964
+ throw new SmplNotFoundError(body, 404, body);
965
+ case 409:
966
+ throw new SmplConflictError(body, 409, body);
967
+ case 422:
968
+ throw new SmplValidationError(body, 422, body);
969
+ default:
970
+ throw new SmplError(`HTTP ${status}: ${body}`, status, body);
971
+ }
972
+ }
973
+ };
974
+
975
+ // src/flags/models.ts
976
+ var Flag = class {
977
+ /** UUID of the flag. */
978
+ id;
979
+ /** Unique key within the account. */
980
+ key;
981
+ /** Human-readable display name. */
982
+ name;
983
+ /** Value type: BOOLEAN, STRING, NUMERIC, or JSON. */
984
+ type;
985
+ /** Flag-level default value. */
986
+ default;
987
+ /** Closed set of possible values. */
988
+ values;
989
+ /** Optional description. */
990
+ description;
991
+ /** Per-environment configuration. */
992
+ environments;
993
+ /** When the flag was created. */
994
+ createdAt;
995
+ /** When the flag was last updated. */
996
+ updatedAt;
997
+ /** @internal */
998
+ _client;
999
+ /** @internal */
1000
+ constructor(client, fields) {
1001
+ this._client = client;
1002
+ this.id = fields.id;
1003
+ this.key = fields.key;
1004
+ this.name = fields.name;
1005
+ this.type = fields.type;
1006
+ this.default = fields.default;
1007
+ this.values = fields.values;
1008
+ this.description = fields.description;
1009
+ this.environments = fields.environments;
1010
+ this.createdAt = fields.createdAt;
1011
+ this.updatedAt = fields.updatedAt;
1012
+ }
1013
+ /**
1014
+ * Update this flag's attributes on the server.
1015
+ *
1016
+ * Only provided fields are changed; others retain their current values.
1017
+ */
1018
+ async update(options) {
1019
+ const updated = await this._client._updateFlag({
1020
+ flag: this,
1021
+ environments: options.environments,
1022
+ values: options.values,
1023
+ default: options.default,
1024
+ description: options.description,
1025
+ name: options.name
1026
+ });
1027
+ this._apply(updated);
1028
+ }
1029
+ /**
1030
+ * Add a rule to a specific environment.
1031
+ *
1032
+ * The built rule must include an `environment` key (set via
1033
+ * `Rule(...).environment("env_key")`). Re-fetches current state
1034
+ * first to avoid stale data.
1035
+ */
1036
+ async addRule(builtRule) {
1037
+ const envKey = builtRule.environment;
1038
+ if (!envKey) {
1039
+ throw new Error(
1040
+ `Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
1041
+ );
1042
+ }
1043
+ const current = await this._client.get(this.id);
1044
+ this._apply(current);
1045
+ const envs = { ...this.environments };
1046
+ const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
1047
+ const rules = [...envData.rules ?? []];
1048
+ const { environment: _env, ...ruleCopy } = builtRule;
1049
+ rules.push(ruleCopy);
1050
+ envData.rules = rules;
1051
+ envs[envKey] = envData;
1052
+ const updated = await this._client._updateFlag({
1053
+ flag: this,
1054
+ environments: envs
1055
+ });
1056
+ this._apply(updated);
1057
+ }
1058
+ /** @internal */
1059
+ _apply(other) {
1060
+ this.id = other.id;
1061
+ this.key = other.key;
1062
+ this.name = other.name;
1063
+ this.type = other.type;
1064
+ this.default = other.default;
1065
+ this.values = other.values;
1066
+ this.description = other.description;
1067
+ this.environments = other.environments;
1068
+ this.createdAt = other.createdAt;
1069
+ this.updatedAt = other.updatedAt;
1070
+ }
1071
+ toString() {
1072
+ return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
1073
+ }
1074
+ };
1075
+ var ContextType = class {
1076
+ /** UUID. */
1077
+ id;
1078
+ /** Unique key within the account. */
1079
+ key;
1080
+ /** Human-readable display name. */
1081
+ name;
1082
+ /** Known attributes. */
1083
+ attributes;
1084
+ constructor(fields) {
1085
+ this.id = fields.id;
1086
+ this.key = fields.key;
1087
+ this.name = fields.name;
1088
+ this.attributes = fields.attributes;
1089
+ }
1090
+ toString() {
1091
+ return `ContextType(key=${this.key}, name=${this.name})`;
1092
+ }
1093
+ };
1094
+
1095
+ // src/flags/client.ts
1096
+ var import_json_logic_js = __toESM(require("json-logic-js"), 1);
1097
+ var FLAGS_BASE_URL = "https://flags.smplkit.com";
1098
+ var APP_BASE_URL = "https://app.smplkit.com";
1099
+ var CACHE_MAX_SIZE = 1e4;
1100
+ var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
1101
+ var CONTEXT_BATCH_FLUSH_SIZE = 100;
1102
+ async function checkError2(response, context) {
1103
+ const body = await response.text().catch(() => "");
1104
+ switch (response.status) {
1105
+ case 404:
1106
+ throw new SmplNotFoundError(body || context, 404, body);
1107
+ case 409:
1108
+ throw new SmplConflictError(body || context, 409, body);
1109
+ case 422:
1110
+ throw new SmplValidationError(body || context, 422, body);
1111
+ default:
1112
+ throw new SmplError(`HTTP ${response.status}: ${body}`, response.status, body);
1113
+ }
1114
+ }
1115
+ function wrapFetchError2(err) {
1116
+ if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
1117
+ throw err;
1118
+ }
1119
+ if (err instanceof TypeError) {
1120
+ throw new SmplConnectionError(`Network error: ${err.message}`);
1121
+ }
1122
+ throw new SmplConnectionError(
1123
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1124
+ );
1125
+ }
1126
+ function contextsToEvalDict(contexts) {
1127
+ const result = {};
1128
+ for (const ctx of contexts) {
1129
+ result[ctx.type] = { key: ctx.key, ...ctx.attributes };
1130
+ }
1131
+ return result;
1132
+ }
1133
+ function sortedStringify(obj) {
1134
+ if (obj === null || obj === void 0) return "null";
1135
+ if (typeof obj !== "object") return JSON.stringify(obj);
1136
+ if (Array.isArray(obj)) return "[" + obj.map(sortedStringify).join(",") + "]";
1137
+ const keys = Object.keys(obj).sort();
1138
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + sortedStringify(obj[k])).join(",") + "}";
1139
+ }
1140
+ function hashContext(evalDict) {
1141
+ const serialized = sortedStringify(evalDict);
1142
+ let hash = 0;
1143
+ for (let i = 0; i < serialized.length; i++) {
1144
+ const chr = serialized.charCodeAt(i);
1145
+ hash = (hash << 5) - hash + chr | 0;
1146
+ }
1147
+ return hash.toString(36);
1148
+ }
1149
+ function evaluateFlag(flagDef, environment, evalDict) {
1150
+ const flagDefault = flagDef.default;
1151
+ const environments = flagDef.environments ?? {};
1152
+ if (environment === null || !(environment in environments)) {
1153
+ return flagDefault;
1154
+ }
1155
+ const envConfig = environments[environment];
1156
+ const envDefault = envConfig.default;
1157
+ const fallback = envDefault !== void 0 && envDefault !== null ? envDefault : flagDefault;
1158
+ if (!envConfig.enabled) {
1159
+ return fallback;
1160
+ }
1161
+ const rules = envConfig.rules ?? [];
1162
+ for (const rule of rules) {
1163
+ const logic = rule.logic;
1164
+ if (!logic || Object.keys(logic).length === 0) {
1165
+ continue;
1166
+ }
1167
+ try {
1168
+ const result = import_json_logic_js.default.apply(logic, evalDict);
1169
+ if (result) {
1170
+ return rule.value;
1171
+ }
1172
+ } catch {
1173
+ continue;
1174
+ }
1175
+ }
1176
+ return fallback;
1177
+ }
1178
+ var FlagChangeEvent = class {
1179
+ key;
1180
+ source;
1181
+ constructor(key, source) {
1182
+ this.key = key;
1183
+ this.source = source;
1184
+ }
1185
+ };
1186
+ var ResolutionCache = class {
1187
+ _maxSize;
1188
+ _cache = /* @__PURE__ */ new Map();
1189
+ cacheHits = 0;
1190
+ cacheMisses = 0;
1191
+ constructor(maxSize = CACHE_MAX_SIZE) {
1192
+ this._maxSize = maxSize;
1193
+ }
1194
+ get(cacheKey) {
1195
+ if (this._cache.has(cacheKey)) {
1196
+ const value = this._cache.get(cacheKey);
1197
+ this._cache.delete(cacheKey);
1198
+ this._cache.set(cacheKey, value);
1199
+ this.cacheHits++;
1200
+ return [true, value];
1201
+ }
1202
+ this.cacheMisses++;
1203
+ return [false, null];
1204
+ }
1205
+ put(cacheKey, value) {
1206
+ if (this._cache.has(cacheKey)) {
1207
+ this._cache.delete(cacheKey);
1208
+ }
1209
+ this._cache.set(cacheKey, value);
1210
+ if (this._cache.size > this._maxSize) {
1211
+ const firstKey = this._cache.keys().next().value;
1212
+ if (firstKey !== void 0) {
1213
+ this._cache.delete(firstKey);
1214
+ }
1215
+ }
1216
+ }
1217
+ clear() {
1218
+ this._cache.clear();
1219
+ }
1220
+ };
1221
+ var FlagStats = class {
1222
+ cacheHits;
1223
+ cacheMisses;
1224
+ constructor(cacheHits, cacheMisses) {
1225
+ this.cacheHits = cacheHits;
1226
+ this.cacheMisses = cacheMisses;
1227
+ }
1228
+ };
1229
+ var FlagHandleBase = class {
1230
+ /** @internal */
1231
+ _namespace;
1232
+ /** @internal */
1233
+ _key;
1234
+ /** @internal */
1235
+ _default;
1236
+ /** @internal */
1237
+ _listeners = [];
1238
+ constructor(namespace, key, defaultValue) {
1239
+ this._namespace = namespace;
1240
+ this._key = key;
1241
+ this._default = defaultValue;
1242
+ }
1243
+ get key() {
1244
+ return this._key;
1245
+ }
1246
+ get default() {
1247
+ return this._default;
1248
+ }
1249
+ /* v8 ignore next 3 — overridden by all exported subclasses */
1250
+ get(options) {
1251
+ return this._namespace._evaluateHandle(this._key, this._default, options?.context ?? null);
1252
+ }
1253
+ /** Register a flag-specific change listener. Works as a decorator. */
1254
+ onChange(callback) {
1255
+ this._listeners.push(callback);
1256
+ return callback;
1257
+ }
1258
+ };
1259
+ var BoolFlagHandle = class extends FlagHandleBase {
1260
+ get(options) {
1261
+ const value = this._namespace._evaluateHandle(
1262
+ this._key,
1263
+ this._default,
1264
+ options?.context ?? null
1265
+ );
1266
+ if (typeof value === "boolean") {
1267
+ return value;
1268
+ }
1269
+ return this._default;
1270
+ }
1271
+ };
1272
+ var StringFlagHandle = class extends FlagHandleBase {
1273
+ get(options) {
1274
+ const value = this._namespace._evaluateHandle(
1275
+ this._key,
1276
+ this._default,
1277
+ options?.context ?? null
1278
+ );
1279
+ if (typeof value === "string") {
1280
+ return value;
1281
+ }
1282
+ return this._default;
1283
+ }
1284
+ };
1285
+ var NumberFlagHandle = class extends FlagHandleBase {
1286
+ get(options) {
1287
+ const value = this._namespace._evaluateHandle(
1288
+ this._key,
1289
+ this._default,
1290
+ options?.context ?? null
1291
+ );
1292
+ if (typeof value === "number") {
1293
+ return value;
1294
+ }
1295
+ return this._default;
1296
+ }
1297
+ };
1298
+ var JsonFlagHandle = class extends FlagHandleBase {
1299
+ get(options) {
1300
+ const value = this._namespace._evaluateHandle(
1301
+ this._key,
1302
+ this._default,
1303
+ options?.context ?? null
1304
+ );
1305
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1306
+ return value;
1307
+ }
1308
+ return this._default;
1309
+ }
1310
+ };
1311
+ var ContextRegistrationBuffer = class {
1312
+ _seen = /* @__PURE__ */ new Map();
1313
+ _pending = [];
1314
+ observe(contexts) {
1315
+ for (const ctx of contexts) {
1316
+ const cacheKey = `${ctx.type}:${ctx.key}`;
1317
+ if (!this._seen.has(cacheKey)) {
1318
+ if (this._seen.size >= CONTEXT_REGISTRATION_LRU_SIZE) {
1319
+ const firstKey = this._seen.keys().next().value;
1320
+ if (firstKey !== void 0) {
1321
+ this._seen.delete(firstKey);
1322
+ }
1323
+ }
1324
+ this._seen.set(cacheKey, ctx.attributes);
1325
+ this._pending.push({
1326
+ id: `${ctx.type}:${ctx.key}`,
1327
+ name: ctx.name ?? ctx.key,
1328
+ attributes: { ...ctx.attributes }
1329
+ });
1330
+ }
1331
+ }
1332
+ }
1333
+ drain() {
1334
+ const batch = this._pending;
1335
+ this._pending = [];
1336
+ return batch;
1337
+ }
1338
+ get pendingCount() {
1339
+ return this._pending.length;
1340
+ }
1341
+ };
1342
+ var FlagsClient = class {
1343
+ /** @internal */
1344
+ _apiKey;
1345
+ /** @internal */
1346
+ _baseUrl = FLAGS_BASE_URL;
1347
+ /** @internal */
1348
+ _http;
1349
+ /** @internal */
1350
+ _transport;
1351
+ // Runtime state
1352
+ _environment = null;
1353
+ _flagStore = {};
1354
+ _connected = false;
1355
+ _cache = new ResolutionCache();
1356
+ _contextProvider = null;
1357
+ _contextBuffer = new ContextRegistrationBuffer();
1358
+ _handles = {};
1359
+ _globalListeners = [];
1360
+ // Shared WebSocket (set during connect)
1361
+ _wsManager = null;
1362
+ _ensureWs;
1363
+ /** @internal */
1364
+ constructor(apiKey, ensureWs, timeout) {
1365
+ this._apiKey = apiKey;
1366
+ this._ensureWs = ensureWs;
1367
+ const ms = timeout ?? 3e4;
1368
+ this._http = (0, import_openapi_fetch2.default)({
1369
+ baseUrl: FLAGS_BASE_URL,
1370
+ headers: {
1371
+ Authorization: `Bearer ${apiKey}`,
1372
+ Accept: "application/json"
1373
+ },
1374
+ fetch: async (request) => {
1375
+ const controller = new AbortController();
1376
+ const timer = setTimeout(() => controller.abort(), ms);
1377
+ try {
1378
+ return await fetch(new Request(request, { signal: controller.signal }));
1379
+ } catch (err) {
1380
+ if (err instanceof DOMException && err.name === "AbortError") {
1381
+ throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1382
+ }
1383
+ throw err;
1384
+ } finally {
1385
+ clearTimeout(timer);
1386
+ }
1387
+ }
1388
+ });
1389
+ this._transport = new Transport({ apiKey, timeout: ms });
1390
+ }
1391
+ // ------------------------------------------------------------------
1392
+ // Management methods
1393
+ // ------------------------------------------------------------------
1394
+ /** Create a flag. */
1395
+ async create(key, options) {
1396
+ let values = options.values;
1397
+ if (values === void 0 && options.type === "BOOLEAN") {
1398
+ values = [
1399
+ { name: "True", value: true },
1400
+ { name: "False", value: false }
1401
+ ];
1402
+ }
1403
+ const body = {
1404
+ data: {
1405
+ type: "flag",
1406
+ attributes: {
1407
+ key,
1408
+ name: options.name,
1409
+ description: options.description ?? "",
1410
+ type: options.type,
1411
+ default: options.default,
1412
+ values: values ?? []
1413
+ }
1414
+ }
1415
+ };
1416
+ let data;
1417
+ try {
1418
+ const result = await this._http.POST("/api/v1/flags", { body });
1419
+ if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1420
+ data = result.data;
1421
+ } catch (err) {
1422
+ wrapFetchError2(err);
1423
+ }
1424
+ if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1425
+ return this._resourceToModel(data.data);
1426
+ }
1427
+ /** Fetch a flag by UUID. */
1428
+ async get(flagId) {
1429
+ let data;
1430
+ try {
1431
+ const result = await this._http.GET("/api/v1/flags/{id}", {
1432
+ params: { path: { id: flagId } }
1433
+ });
1434
+ if (result.error !== void 0) await checkError2(result.response, `Flag ${flagId} not found`);
1435
+ data = result.data;
1436
+ } catch (err) {
1437
+ wrapFetchError2(err);
1438
+ }
1439
+ if (!data || !data.data) throw new SmplNotFoundError(`Flag ${flagId} not found`);
1440
+ return this._resourceToModel(data.data);
1441
+ }
1442
+ /** List all flags. */
1443
+ async list() {
1444
+ let data;
1445
+ try {
1446
+ const result = await this._http.GET("/api/v1/flags", {});
1447
+ if (result.error !== void 0) await checkError2(result.response, "Failed to list flags");
1448
+ data = result.data;
1449
+ } catch (err) {
1450
+ wrapFetchError2(err);
1451
+ }
1452
+ if (!data) return [];
1453
+ return data.data.map((r) => this._resourceToModel(r));
1454
+ }
1455
+ /** Delete a flag by UUID. */
1456
+ async delete(flagId) {
1457
+ try {
1458
+ const result = await this._http.DELETE("/api/v1/flags/{id}", {
1459
+ params: { path: { id: flagId } }
1460
+ });
1461
+ if (result.error !== void 0 && result.response.status !== 204)
1462
+ await checkError2(result.response, `Failed to delete flag ${flagId}`);
1463
+ } catch (err) {
1464
+ wrapFetchError2(err);
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Internal: PUT a full flag update.
1469
+ * Called by {@link Flag} instance methods.
1470
+ * @internal
1471
+ */
1472
+ async _updateFlag(options) {
1473
+ const { flag } = options;
1474
+ const body = {
1475
+ data: {
1476
+ type: "flag",
1477
+ attributes: {
1478
+ key: flag.key,
1479
+ name: options.name !== void 0 ? options.name : flag.name,
1480
+ type: flag.type,
1481
+ default: options.default !== void 0 ? options.default : flag.default,
1482
+ values: options.values !== void 0 ? options.values : flag.values,
1483
+ description: options.description !== void 0 ? options.description : flag.description ?? "",
1484
+ ...options.environments !== void 0 ? { environments: options.environments } : flag.environments && Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1485
+ }
1486
+ }
1487
+ };
1488
+ let data;
1489
+ try {
1490
+ const result = await this._http.PUT("/api/v1/flags/{id}", {
1491
+ params: { path: { id: flag.id } },
1492
+ body
1493
+ });
1494
+ if (result.error !== void 0)
1495
+ await checkError2(result.response, `Failed to update flag ${flag.id}`);
1496
+ data = result.data;
1497
+ } catch (err) {
1498
+ wrapFetchError2(err);
1499
+ }
1500
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1501
+ return this._resourceToModel(data.data);
1502
+ }
1503
+ // ------------------------------------------------------------------
1504
+ // Context type management (direct HTTP — not in generated spec)
1505
+ // ------------------------------------------------------------------
1506
+ /** Create a context type. */
1507
+ async createContextType(key, options) {
1508
+ const resp = await this._transport.post(`${APP_BASE_URL}/api/v1/context_types`, {
1509
+ data: { type: "context_type", attributes: { key, name: options.name } }
1510
+ });
1511
+ const data = resp.data ?? {};
1512
+ return this._parseContextType(data);
1513
+ }
1514
+ /** Update a context type (merge attributes). */
1515
+ async updateContextType(ctId, options) {
1516
+ const resp = await this._transport.put(`${APP_BASE_URL}/api/v1/context_types/${ctId}`, {
1517
+ data: { type: "context_type", attributes: { attributes: options.attributes } }
1518
+ });
1519
+ const data = resp.data ?? {};
1520
+ return this._parseContextType(data);
1521
+ }
1522
+ /** List all context types. */
1523
+ async listContextTypes() {
1524
+ const resp = await this._transport.get(`${APP_BASE_URL}/api/v1/context_types`);
1525
+ const items = resp.data ?? [];
1526
+ return items.map((item) => this._parseContextType(item));
1527
+ }
1528
+ /** Delete a context type. */
1529
+ async deleteContextType(ctId) {
1530
+ await this._transport.delete(`${APP_BASE_URL}/api/v1/context_types/${ctId}`);
1531
+ }
1532
+ /** List context instances filtered by context type key. */
1533
+ async listContexts(options) {
1534
+ const resp = await this._transport.get(`${APP_BASE_URL}/api/v1/contexts`, {
1535
+ "filter[context_type]": options.contextTypeKey
1536
+ });
1537
+ return resp.data ?? [];
1538
+ }
1539
+ // ------------------------------------------------------------------
1540
+ // Runtime: typed flag handles
1541
+ // ------------------------------------------------------------------
1542
+ /** Declare a boolean flag handle. */
1543
+ boolFlag(key, defaultValue) {
1544
+ const handle = new BoolFlagHandle(this, key, defaultValue);
1545
+ this._handles[key] = handle;
1546
+ return handle;
1547
+ }
1548
+ /** Declare a string flag handle. */
1549
+ stringFlag(key, defaultValue) {
1550
+ const handle = new StringFlagHandle(this, key, defaultValue);
1551
+ this._handles[key] = handle;
1552
+ return handle;
1553
+ }
1554
+ /** Declare a numeric flag handle. */
1555
+ numberFlag(key, defaultValue) {
1556
+ const handle = new NumberFlagHandle(this, key, defaultValue);
1557
+ this._handles[key] = handle;
1558
+ return handle;
1559
+ }
1560
+ /** Declare a JSON flag handle. */
1561
+ jsonFlag(key, defaultValue) {
1562
+ const handle = new JsonFlagHandle(this, key, defaultValue);
1563
+ this._handles[key] = handle;
1564
+ return handle;
1565
+ }
1566
+ // ------------------------------------------------------------------
1567
+ // Runtime: context provider
1568
+ // ------------------------------------------------------------------
1569
+ /**
1570
+ * Register a context provider function.
1571
+ *
1572
+ * Called on every `handle.get()` to supply the current evaluation
1573
+ * context. Can also be used as a decorator:
1574
+ *
1575
+ * ```typescript
1576
+ * client.flags.setContextProvider(() => [
1577
+ * new Context("user", userId, { plan: userPlan }),
1578
+ * ]);
1579
+ * ```
1580
+ */
1581
+ setContextProvider(fn) {
1582
+ this._contextProvider = fn;
1583
+ }
1584
+ /**
1585
+ * Register a context provider — decorator-style alias.
1586
+ *
1587
+ * ```typescript
1588
+ * const provider = client.flags.contextProvider(() => [...]);
1589
+ * ```
1590
+ */
1591
+ contextProvider(fn) {
1592
+ this._contextProvider = fn;
1593
+ return fn;
1594
+ }
1595
+ // ------------------------------------------------------------------
1596
+ // Runtime: connect / disconnect / refresh
1597
+ // ------------------------------------------------------------------
1598
+ /**
1599
+ * Connect to an environment: fetch flag definitions, register on
1600
+ * shared WebSocket, enable local evaluation.
1601
+ */
1602
+ async connect(environment, _options) {
1603
+ this._environment = environment;
1604
+ await this._fetchAllFlags();
1605
+ this._connected = true;
1606
+ this._cache.clear();
1607
+ this._wsManager = this._ensureWs();
1608
+ this._wsManager.on("flag_changed", this._handleFlagChanged);
1609
+ this._wsManager.on("flag_deleted", this._handleFlagDeleted);
1610
+ }
1611
+ /** Disconnect: unregister from WebSocket, flush contexts, clear state. */
1612
+ async disconnect() {
1613
+ if (this._wsManager !== null) {
1614
+ this._wsManager.off("flag_changed", this._handleFlagChanged);
1615
+ this._wsManager.off("flag_deleted", this._handleFlagDeleted);
1616
+ this._wsManager = null;
1617
+ }
1618
+ await this._flushContexts();
1619
+ this._flagStore = {};
1620
+ this._cache.clear();
1621
+ this._connected = false;
1622
+ this._environment = null;
1623
+ }
1624
+ /** Re-fetch all flag definitions and clear cache. */
1625
+ async refresh() {
1626
+ await this._fetchAllFlags();
1627
+ this._cache.clear();
1628
+ this._fireChangeListenersAll("manual");
1629
+ }
1630
+ /** Return the current WebSocket connection status. */
1631
+ connectionStatus() {
1632
+ if (this._wsManager !== null) {
1633
+ return this._wsManager.connectionStatus;
1634
+ }
1635
+ return "disconnected";
1636
+ }
1637
+ /** Return cache statistics. */
1638
+ stats() {
1639
+ return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
1640
+ }
1641
+ // ------------------------------------------------------------------
1642
+ // Runtime: change listeners
1643
+ // ------------------------------------------------------------------
1644
+ /** Register a global change listener that fires for any flag change. */
1645
+ onChangeAny(callback) {
1646
+ this._globalListeners.push(callback);
1647
+ return callback;
1648
+ }
1649
+ /**
1650
+ * Register a global change listener — decorator-style alias.
1651
+ *
1652
+ * ```typescript
1653
+ * const listener = client.flags.onChange((event) => { ... });
1654
+ * ```
1655
+ */
1656
+ onChange(callback) {
1657
+ return this.onChangeAny(callback);
1658
+ }
1659
+ // ------------------------------------------------------------------
1660
+ // Runtime: context registration
1661
+ // ------------------------------------------------------------------
1662
+ /**
1663
+ * Explicitly register context(s) for background batch registration.
1664
+ *
1665
+ * Accepts a single Context or an array. Fire-and-forget — never
1666
+ * blocks. Works before `connect()` is called.
1667
+ */
1668
+ register(context) {
1669
+ if (Array.isArray(context)) {
1670
+ this._contextBuffer.observe(context);
1671
+ } else {
1672
+ this._contextBuffer.observe([context]);
1673
+ }
1674
+ }
1675
+ /** Flush pending context registrations to the server. */
1676
+ async flushContexts() {
1677
+ await this._flushContexts();
1678
+ }
1679
+ // ------------------------------------------------------------------
1680
+ // Runtime: Tier 1 evaluate
1681
+ // ------------------------------------------------------------------
1682
+ /**
1683
+ * Tier 1 explicit evaluation — stateless, no provider or cache.
1684
+ *
1685
+ * Useful for scripts, one-off jobs, and infrastructure code.
1686
+ */
1687
+ async evaluate(key, options) {
1688
+ const evalDict = contextsToEvalDict(options.context);
1689
+ let flagDef = null;
1690
+ if (this._connected && key in this._flagStore) {
1691
+ flagDef = this._flagStore[key];
1692
+ } else {
1693
+ const flags = await this._fetchFlagsList();
1694
+ for (const f of flags) {
1695
+ if (f.key === key) {
1696
+ flagDef = f;
1697
+ break;
1698
+ }
1699
+ }
1700
+ }
1701
+ if (flagDef === null) {
1702
+ return null;
1703
+ }
1704
+ return evaluateFlag(flagDef, options.environment, evalDict);
1705
+ }
1706
+ // ------------------------------------------------------------------
1707
+ // Internal: evaluation
1708
+ // ------------------------------------------------------------------
1709
+ /** @internal */
1710
+ _evaluateHandle(key, defaultValue, context) {
1711
+ if (!this._connected) {
1712
+ return defaultValue;
1713
+ }
1714
+ let evalDict;
1715
+ if (context !== null) {
1716
+ evalDict = contextsToEvalDict(context);
1717
+ } else if (this._contextProvider !== null) {
1718
+ const contexts = this._contextProvider();
1719
+ evalDict = contextsToEvalDict(contexts);
1720
+ this._contextBuffer.observe(contexts);
1721
+ if (this._contextBuffer.pendingCount >= CONTEXT_BATCH_FLUSH_SIZE) {
1722
+ void this._flushContexts();
1723
+ }
1724
+ } else {
1725
+ evalDict = {};
1726
+ }
1727
+ const ctxHash = hashContext(evalDict);
1728
+ const cacheKey = `${key}:${ctxHash}`;
1729
+ const [hit, cachedValue] = this._cache.get(cacheKey);
1730
+ if (hit) {
1731
+ return cachedValue;
1732
+ }
1733
+ const flagDef = this._flagStore[key];
1734
+ if (flagDef === void 0) {
1735
+ this._cache.put(cacheKey, defaultValue);
1736
+ return defaultValue;
1737
+ }
1738
+ let value = evaluateFlag(flagDef, this._environment, evalDict);
1739
+ if (value === null || value === void 0) {
1740
+ value = defaultValue;
1741
+ }
1742
+ this._cache.put(cacheKey, value);
1743
+ return value;
1744
+ }
1745
+ // ------------------------------------------------------------------
1746
+ // Internal: event handlers (called by SharedWebSocket)
1747
+ // ------------------------------------------------------------------
1748
+ _handleFlagChanged = (data) => {
1749
+ const flagKey = data.key;
1750
+ void this._fetchAllFlags().then(() => {
1751
+ this._cache.clear();
1752
+ this._fireChangeListeners(flagKey ?? null, "websocket");
1753
+ });
1754
+ };
1755
+ _handleFlagDeleted = (data) => {
1756
+ const flagKey = data.key;
1757
+ void this._fetchAllFlags().then(() => {
1758
+ this._cache.clear();
1759
+ this._fireChangeListeners(flagKey ?? null, "websocket");
1760
+ });
1761
+ };
1762
+ // ------------------------------------------------------------------
1763
+ // Internal: flag store
1764
+ // ------------------------------------------------------------------
1765
+ async _fetchAllFlags() {
1766
+ const flags = await this._fetchFlagsList();
1767
+ const store = {};
1768
+ for (const f of flags) {
1769
+ store[f.key] = f;
1770
+ }
1771
+ this._flagStore = store;
1772
+ }
1773
+ async _fetchFlagsList() {
1774
+ let data;
1775
+ try {
1776
+ const result = await this._http.GET("/api/v1/flags", {});
1777
+ if (result.error !== void 0) await checkError2(result.response, "Failed to list flags");
1778
+ data = result.data;
1779
+ } catch (err) {
1780
+ wrapFetchError2(err);
1781
+ }
1782
+ if (!data) return [];
1783
+ return data.data.map((r) => this._resourceToPlainDict(r));
1784
+ }
1785
+ // ------------------------------------------------------------------
1786
+ // Internal: change listeners
1787
+ // ------------------------------------------------------------------
1788
+ _fireChangeListeners(flagKey, source) {
1789
+ if (flagKey) {
1790
+ const event = new FlagChangeEvent(flagKey, source);
1791
+ for (const cb of this._globalListeners) {
1792
+ try {
1793
+ cb(event);
1794
+ } catch {
1795
+ }
1796
+ }
1797
+ const handle = this._handles[flagKey];
1798
+ if (handle) {
1799
+ for (const cb of handle._listeners) {
1800
+ try {
1801
+ cb(event);
1802
+ } catch {
1803
+ }
1804
+ }
1805
+ }
1806
+ }
1807
+ }
1808
+ _fireChangeListenersAll(source) {
1809
+ for (const flagKey of Object.keys(this._flagStore)) {
1810
+ this._fireChangeListeners(flagKey, source);
1811
+ }
1812
+ }
1813
+ // ------------------------------------------------------------------
1814
+ // Internal: context flush
1815
+ // ------------------------------------------------------------------
1816
+ async _flushContexts() {
1817
+ const batch = this._contextBuffer.drain();
1818
+ if (batch.length === 0) return;
1819
+ try {
1820
+ await this._transport.put(`${APP_BASE_URL}/api/v1/contexts/bulk`, {
1821
+ contexts: batch
1822
+ });
1823
+ } catch {
1824
+ }
1825
+ }
1826
+ // ------------------------------------------------------------------
1827
+ // Internal: model conversion
1828
+ // ------------------------------------------------------------------
1829
+ _resourceToModel(resource) {
1830
+ const attrs = resource.attributes;
1831
+ return new Flag(this, {
1832
+ id: resource.id ?? "",
1833
+ key: attrs.key,
1834
+ name: attrs.name,
1835
+ type: attrs.type,
1836
+ default: attrs.default,
1837
+ values: (attrs.values ?? []).map((v) => ({ name: v.name, value: v.value })),
1838
+ description: attrs.description ?? null,
1839
+ environments: attrs.environments ?? {},
1840
+ createdAt: attrs.created_at ?? null,
1841
+ updatedAt: attrs.updated_at ?? null
1842
+ });
1843
+ }
1844
+ _resourceToPlainDict(resource) {
1845
+ const attrs = resource.attributes;
1846
+ return {
1847
+ key: attrs.key,
1848
+ name: attrs.name,
1849
+ type: attrs.type,
1850
+ default: attrs.default,
1851
+ values: (attrs.values ?? []).map((v) => ({ name: v.name, value: v.value })),
1852
+ description: attrs.description ?? null,
1853
+ environments: attrs.environments ?? {}
1854
+ };
1855
+ }
1856
+ _parseContextType(data) {
1857
+ const attrs = data.attributes ?? {};
1858
+ return new ContextType({
1859
+ id: data.id ?? "",
1860
+ key: attrs.key ?? "",
1861
+ name: attrs.name ?? "",
1862
+ attributes: attrs.attributes ?? {}
1863
+ });
1864
+ }
1865
+ };
1866
+
1867
+ // src/ws.ts
1868
+ var import_ws = __toESM(require("ws"), 1);
1869
+ var BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
1870
+ var SharedWebSocket = class {
1871
+ _appBaseUrl;
1872
+ _apiKey;
1873
+ _listeners = /* @__PURE__ */ new Map();
1874
+ _connectionStatus = "disconnected";
1875
+ _closed = false;
1876
+ _ws = null;
1877
+ _reconnectTimer = null;
1878
+ _backoffIndex = 0;
1879
+ constructor(appBaseUrl, apiKey) {
1880
+ this._appBaseUrl = appBaseUrl;
1881
+ this._apiKey = apiKey;
1882
+ }
1883
+ // ------------------------------------------------------------------
1884
+ // Listener registration
1885
+ // ------------------------------------------------------------------
1886
+ /** Register a listener for a specific event type. */
1887
+ on(eventName, callback) {
1888
+ if (!this._listeners.has(eventName)) {
1889
+ this._listeners.set(eventName, []);
1890
+ }
1891
+ this._listeners.get(eventName).push(callback);
1892
+ }
1893
+ /** Unregister a listener for a specific event type. */
1894
+ off(eventName, callback) {
1895
+ const list = this._listeners.get(eventName);
1896
+ if (list) {
1897
+ const idx = list.indexOf(callback);
1898
+ if (idx !== -1) {
1899
+ list.splice(idx, 1);
1900
+ }
1901
+ }
1902
+ }
1903
+ _dispatch(eventName, data) {
1904
+ const callbacks = this._listeners.get(eventName);
1905
+ if (callbacks) {
1906
+ for (const cb of [...callbacks]) {
1907
+ try {
1908
+ cb(data);
1909
+ } catch {
1910
+ }
1911
+ }
1912
+ }
1913
+ }
1914
+ // ------------------------------------------------------------------
1915
+ // Connection status
1916
+ // ------------------------------------------------------------------
1917
+ get connectionStatus() {
1918
+ return this._connectionStatus;
1919
+ }
1920
+ // ------------------------------------------------------------------
1921
+ // Lifecycle
1922
+ // ------------------------------------------------------------------
1923
+ /** Start the WebSocket connection. */
1924
+ start() {
1925
+ this._closed = false;
1926
+ this._connect();
1927
+ }
1928
+ /** Stop the WebSocket connection. */
1929
+ stop() {
1930
+ this._closed = true;
1931
+ this._connectionStatus = "disconnected";
1932
+ if (this._reconnectTimer !== null) {
1933
+ clearTimeout(this._reconnectTimer);
1934
+ this._reconnectTimer = null;
1935
+ }
1936
+ if (this._ws !== null) {
1937
+ this._ws.close();
1938
+ this._ws = null;
1939
+ }
1940
+ }
1941
+ // ------------------------------------------------------------------
1942
+ // Connection internals
1943
+ // ------------------------------------------------------------------
1944
+ _buildWsUrl() {
1945
+ let url = this._appBaseUrl;
1946
+ if (url.startsWith("https://")) {
1947
+ url = "wss://" + url.slice("https://".length);
1948
+ } else if (url.startsWith("http://")) {
1949
+ url = "ws://" + url.slice("http://".length);
1950
+ } else {
1951
+ url = "wss://" + url;
1952
+ }
1953
+ url = url.replace(/\/$/, "");
1954
+ return `${url}/api/ws/v1/events?api_key=${this._apiKey}`;
1955
+ }
1956
+ _connect() {
1957
+ if (this._closed) return;
1958
+ this._connectionStatus = "connecting";
1959
+ const wsUrl = this._buildWsUrl();
1960
+ try {
1961
+ const ws = new import_ws.default(wsUrl);
1962
+ this._ws = ws;
1963
+ ws.on("open", () => {
1964
+ if (this._closed) {
1965
+ ws.close();
1966
+ return;
1967
+ }
1968
+ });
1969
+ ws.on("message", (data) => {
1970
+ try {
1971
+ const raw = String(data);
1972
+ if (raw === "ping") {
1973
+ ws.send("pong");
1974
+ return;
1975
+ }
1976
+ const msg = JSON.parse(raw);
1977
+ if (msg.type === "connected") {
1978
+ this._backoffIndex = 0;
1979
+ this._connectionStatus = "connected";
1980
+ return;
1981
+ }
1982
+ if (msg.type === "error") {
1983
+ return;
1984
+ }
1985
+ const eventName = msg.event;
1986
+ if (eventName) {
1987
+ this._dispatch(eventName, msg);
1988
+ }
1989
+ } catch {
1990
+ }
1991
+ });
1992
+ ws.on("close", () => {
1993
+ if (!this._closed) {
1994
+ this._connectionStatus = "disconnected";
1995
+ this._scheduleReconnect();
1996
+ }
1997
+ });
1998
+ ws.on("error", () => {
1999
+ });
2000
+ } catch {
2001
+ if (!this._closed) {
2002
+ this._scheduleReconnect();
2003
+ }
2004
+ }
2005
+ }
2006
+ _scheduleReconnect() {
2007
+ if (this._closed) return;
2008
+ const delay = BACKOFF_MS[Math.min(this._backoffIndex, BACKOFF_MS.length - 1)];
2009
+ this._backoffIndex++;
2010
+ this._connectionStatus = "connecting";
2011
+ this._reconnectTimer = setTimeout(() => {
2012
+ this._reconnectTimer = null;
2013
+ this._connect();
2014
+ }, delay);
2015
+ }
2016
+ };
2017
+
902
2018
  // src/resolve.ts
903
2019
  var import_node_fs = require("fs");
904
2020
  var import_node_os = require("os");
@@ -933,28 +2049,128 @@ function resolveApiKey(explicit) {
933
2049
  }
934
2050
 
935
2051
  // src/client.ts
2052
+ var APP_BASE_URL2 = "https://app.smplkit.com";
936
2053
  var SmplClient = class {
937
2054
  /** Client for config management-plane operations. */
938
2055
  config;
2056
+ /** Client for flags management and runtime operations. */
2057
+ flags;
2058
+ _wsManager = null;
2059
+ _apiKey;
939
2060
  constructor(options = {}) {
940
2061
  const apiKey = resolveApiKey(options.apiKey);
2062
+ this._apiKey = apiKey;
941
2063
  this.config = new ConfigClient(apiKey, options.timeout);
2064
+ this.flags = new FlagsClient(apiKey, () => this._ensureWs(), options.timeout);
2065
+ this.config._getSharedWs = () => this._ensureWs();
2066
+ }
2067
+ /** Lazily create and start the shared WebSocket. @internal */
2068
+ _ensureWs() {
2069
+ if (this._wsManager === null) {
2070
+ this._wsManager = new SharedWebSocket(APP_BASE_URL2, this._apiKey);
2071
+ this._wsManager.start();
2072
+ }
2073
+ return this._wsManager;
2074
+ }
2075
+ /** Close the shared WebSocket and release resources. */
2076
+ close() {
2077
+ if (this._wsManager !== null) {
2078
+ this._wsManager.stop();
2079
+ this._wsManager = null;
2080
+ }
942
2081
  }
943
2082
  };
944
2083
 
945
2084
  // src/index.ts
946
2085
  init_runtime();
2086
+
2087
+ // src/flags/types.ts
2088
+ var Context = class {
2089
+ type;
2090
+ key;
2091
+ name;
2092
+ attributes;
2093
+ constructor(type, key, attributes, options) {
2094
+ this.type = type;
2095
+ this.key = key;
2096
+ this.name = options?.name ?? null;
2097
+ this.attributes = { ...attributes ?? {} };
2098
+ }
2099
+ toString() {
2100
+ return `Context(type=${this.type}, key=${this.key}, name=${this.name})`;
2101
+ }
2102
+ };
2103
+ var Rule = class {
2104
+ _description;
2105
+ _conditions = [];
2106
+ _value = null;
2107
+ _environment = null;
2108
+ constructor(description) {
2109
+ this._description = description;
2110
+ }
2111
+ /** Tag this rule with an environment key (used by `addRule`). */
2112
+ environment(envKey) {
2113
+ this._environment = envKey;
2114
+ return this;
2115
+ }
2116
+ /** Add a condition. Multiple calls are AND'd. */
2117
+ when(variable, op, value) {
2118
+ if (op === "contains") {
2119
+ this._conditions.push({ in: [value, { var: variable }] });
2120
+ } else {
2121
+ this._conditions.push({ [op]: [{ var: variable }, value] });
2122
+ }
2123
+ return this;
2124
+ }
2125
+ /** Set the value returned when this rule matches. */
2126
+ serve(value) {
2127
+ this._value = value;
2128
+ return this;
2129
+ }
2130
+ /** Finalize and return the rule as a plain object. */
2131
+ build() {
2132
+ let logic;
2133
+ if (this._conditions.length === 1) {
2134
+ logic = this._conditions[0];
2135
+ } else if (this._conditions.length > 1) {
2136
+ logic = { and: this._conditions };
2137
+ } else {
2138
+ logic = {};
2139
+ }
2140
+ const result = {
2141
+ description: this._description,
2142
+ logic,
2143
+ value: this._value
2144
+ };
2145
+ if (this._environment !== null) {
2146
+ result.environment = this._environment;
2147
+ }
2148
+ return result;
2149
+ }
2150
+ };
947
2151
  // Annotate the CommonJS export names for ESM import in node:
948
2152
  0 && (module.exports = {
2153
+ BoolFlagHandle,
949
2154
  Config,
950
2155
  ConfigClient,
951
2156
  ConfigRuntime,
2157
+ Context,
2158
+ ContextType,
2159
+ Flag,
2160
+ FlagChangeEvent,
2161
+ FlagStats,
2162
+ FlagsClient,
2163
+ JsonFlagHandle,
2164
+ NumberFlagHandle,
2165
+ Rule,
2166
+ SharedWebSocket,
952
2167
  SmplClient,
953
2168
  SmplConflictError,
954
2169
  SmplConnectionError,
955
2170
  SmplError,
956
2171
  SmplNotFoundError,
957
2172
  SmplTimeoutError,
958
- SmplValidationError
2173
+ SmplValidationError,
2174
+ StringFlagHandle
959
2175
  });
960
2176
  //# sourceMappingURL=index.cjs.map