@configdirector/react-native-sdk 0.1.1 → 0.3.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/README.md CHANGED
@@ -1,121 +1,12 @@
1
1
  # ConfigDirector React Native SDK
2
2
 
3
- ## Getting started
3
+ This is the React Native SDK for [ConfigDirector](https://www.configdirector.com).
4
4
 
5
- ### 1. Install
5
+ ## Documentation
6
6
 
7
- Install from NPM:
7
+ Refer to the [official documentation for the React Native SDK](https://docs.configdirector.com/sdks/mobile/react-native).
8
8
 
9
- ```bash
10
- npm install --save @configdirector/react-native-sdk
11
- ```
12
-
13
- ### 2. Initialize the provider using your client SDK key
14
-
15
- ```tsx
16
- import { View } from "react-native";
17
- import AwesomeApp from "./AwesomeApp";
18
- import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";
19
-
20
- export default function App() {
21
- return (
22
- <ConfigDirectorProvider
23
- sdkKey={"YOUR-CLIENT-SDK-KEY"}
24
- appName="MyAwesomeApp"
25
- appVersion="1.0.0">
26
- <View>
27
- <AwesomeApp />
28
- </View>
29
- </ConfigDirectorProvider>
30
- );
31
- }
32
- ```
33
-
34
- ### 3. Retrieve config values
35
-
36
- Retrieve a config value with the `useConfigValue` hook:
37
-
38
- ```tsx
39
- import { useConfigValue } from "@configdirector/react-native-sdk";
40
-
41
- function MyComponent() {
42
- const { value } = useConfigValue<string>("my-config", "default value");
43
-
44
- return <p>My config value: {value}</p>;
45
- }
46
-
47
- export default MyComponent;
48
- ```
49
-
50
- You can also determine if the client is still initializing, so rather than transition from the default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated:
51
-
52
- ```tsx
53
- import { useConfigValue } from "@configdirector/react-native-sdk";
54
-
55
- function MyComponent() {
56
- const { value, loading } = useConfigValue<string>(
57
- "my-config",
58
- "default value",
59
- );
60
-
61
- if (loading) {
62
- return <p>Loading...</p>;
63
- } else {
64
- return <p>My config value: {value}</p>;
65
- }
66
- }
67
-
68
- export default MyComponent;
69
- ```
70
-
71
- ### 4. Updating the user context
72
-
73
- Update the user context with the `useConfigDirectorContext` hook:
74
-
75
- ```tsx
76
- import {
77
- useConfigValue,
78
- useConfigDirectorContext,
79
- } from "@configdirector/react-native-sdk";
80
-
81
- function MyComponent() {
82
- const { value: myConfigValue } = useConfigValue<string>(
83
- "my-config",
84
- "default value",
85
- );
86
- const { updateContext } = useConfigDirectorContext();
87
-
88
- useEffect(() => {
89
- updateContext({
90
- id: "54321",
91
- name: "Another User",
92
- traits: { customProperty: "customValue" },
93
- });
94
- }, []);
95
-
96
- return <p>My config value: {myConfigValue}</p>;
97
- }
98
-
99
- export default MyComponent;
100
- ```
101
-
102
- ## Requirements
103
-
104
- - React Native **0.71 or later** (requires `ReadableStream` support)
105
- - **Hermes** is strongly recommended. JavaScriptCore (JSC) is supported via a built-in UTF-8 fallback, but Hermes is the default engine in RN 0.70+ and provides better performance.
106
-
107
- ## Expo
108
-
109
- This SDK is compatible with both **Expo managed workflow** and **bare workflow**.
110
-
111
- - In the **managed workflow**, Hermes is the default engine — no additional setup needed.
112
- - In the **bare workflow**, ensure Hermes is enabled in your `Podfile` (iOS) and `gradle.properties` (Android), which is the default in Expo SDK 47+.
113
-
114
- Expo SDK 48 (RN 0.71) or later is required.
115
-
116
- ## License
117
-
118
- MIT
9
+ There is also [a quickstart guide for ConfigDirector and any of our SDKs](https://docs.configdirector.com/getting-started/quickstart).
119
10
 
120
11
  ## Getting Help
121
12
 
@@ -61,6 +61,7 @@ type ConfigDirectorMetaContext = {
61
61
  appVersion?: string;
62
62
  appName?: string;
63
63
  };
64
+ type ConnectionMode = "streaming" | "polling" | "one-time";
64
65
  //#endregion
65
66
  //#region ../js-client-core/src/types.d.ts
66
67
  /**
@@ -76,18 +77,26 @@ type ConfigDirectorClientOptions = {
76
77
  */
77
78
  connection?: {
78
79
  /**
79
- * Whether to open a streaming connection or use a one-time pull of configuration state.
80
- * If set to true, the streaming connection will remain open and receive updates whenever
80
+ * The connection mode to be used, one of `streaming` (default), `polling`, or `one-time`.
81
+ * If set to `streaming`, the connection will remain open and receive updates whenever
81
82
  * config state is updated on the ConfigDirector dashboard.
82
- * When set to false, there will be an initial request to retrieve config state during
83
- * initialization, and an additional request whenever {@link ConfigDirectorClient.updateContext}
84
- * is called. But not updates will be received after those requests.
83
+ * When set to `polling`, there will be an initial request to retrieve config state during
84
+ * initialization, and additional requests on a `pollingInterval`.
85
+ * The `one-time` connection mode will only retrieve config state during initialization and
86
+ * context updates. It will not poll for regular updates.
85
87
  *
86
- * Defaults to true (streaming connection)
88
+ * Defaults to `streaming`
87
89
  */
88
- streaming?: boolean;
90
+ mode?: ConnectionMode;
89
91
  /**
90
- * The timeout, in milliseconds, to be used in initialization and when updating the context.
92
+ * The polling interval in _seconds_ when the `mode` is set to `polling`. This option has no
93
+ * effect when the `mode` is set to `streaming` or `one-time`.
94
+ *
95
+ * Defaults to 60 seconds
96
+ */
97
+ pollingInterval?: number;
98
+ /**
99
+ * The timeout, in _milliseconds_, to be used in initialization and when updating the context.
91
100
  * If streaming is enabled, the operation (initialization or context update) may still succeed
92
101
  * after it times out if no unrecoverable errors are encountered (like an invalid SDK key).
93
102
  * If streaming is disabled, if the operation times out, it will not be retried.
@@ -123,6 +132,9 @@ type ClientEvents = {
123
132
  clientReady: {
124
133
  action: ClientConnectAction;
125
134
  };
135
+ contextUpdated: {
136
+ context: ConfigDirectorContext | undefined;
137
+ };
126
138
  };
127
139
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
128
140
  /**
@@ -155,6 +167,15 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
155
167
  * @param context The current user's context to be used for evaluating targeting rules (required).
156
168
  */
157
169
  updateContext(context: ConfigDirectorContext): Promise<void>;
170
+ /**
171
+ * Returns the current context the client is configured with, or undefined if there is no context.
172
+ *
173
+ * Note that the value returned does not updated immediately after calling {@link updateContext}. Only
174
+ * once the underlying connection was successful or timed out, the current context is updated. This is
175
+ * because while the context update is in progress, configs are still evaluated on the previous context.
176
+ *
177
+ */
178
+ get context(): ConfigDirectorContext | undefined;
158
179
  /**
159
180
  * Returns whether or not the client is ready after calling {@link initialize} or {@link updateContext}
160
181
  *
@@ -162,6 +183,11 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
162
183
  * was received.
163
184
  */
164
185
  get isReady(): boolean;
186
+ /**
187
+ * Returns whether or not the client is currently initializing. Upon client creation, it is `false`.
188
+ * It is `true` after calling `initialize` and becomes `false` again once initialization is completed.
189
+ */
190
+ get isInitializing(): boolean;
165
191
  /**
166
192
  * Evaluates a config and returns its value based on the current context and targeting rules
167
193
  *
@@ -229,7 +255,7 @@ declare class DefaultConsoleLogger implements ConfigDirectorLogger {
229
255
  info(message: string, ...args: any): void;
230
256
  warn(message: string, ...args: any): void;
231
257
  error(message: string, ...args: any): void;
232
- private log;
258
+ protected log(loggerFunction: (...args: any) => void, level: ConfigDirectorLoggingLevel, message: string, ...args: any): void;
233
259
  }
234
260
  //#endregion
235
261
  //#region src/types.d.ts
@@ -287,10 +313,10 @@ declare const useConfigValue: <T extends ConfigValueType>(key: string, defaultVa
287
313
  readyStatus: ClientStatus;
288
314
  loading: boolean;
289
315
  };
290
- declare const useConfigDirectorContext: () => {
316
+ declare const useContext: () => {
291
317
  updateContext: (context: ConfigDirectorContext) => Promise<void>;
292
318
  };
293
- declare const useConfigDirectorClient: () => {
319
+ declare const useClient: () => {
294
320
  client: ConfigDirectorClient;
295
321
  };
296
322
  //#endregion
@@ -306,4 +332,4 @@ declare class ConfigDirectorReactContextError extends Error {
306
332
  //#region src/logger.d.ts
307
333
  declare const createConsoleLogger: (level?: ConfigDirectorLoggingLevel) => DefaultConsoleLogger;
308
334
  //#endregion
309
- export { type ClientStatus, type ConfigDirectorClient, type ConfigDirectorClientOptions, type ConfigDirectorContext, type ConfigDirectorLogger, type ConfigDirectorLoggingLevel, ConfigDirectorProvider, type ConfigDirectorProviderOptions, ConfigDirectorReactContextError, type NetInfoSubscribe, createClient, createConsoleLogger, useConfigDirectorClient, useConfigDirectorContext, useConfigValue };
335
+ export { type ClientStatus, type ConfigDirectorClient, type ConfigDirectorClientOptions, type ConfigDirectorContext, type ConfigDirectorLogger, type ConfigDirectorLoggingLevel, ConfigDirectorProvider, type ConfigDirectorProviderOptions, ConfigDirectorReactContextError, type NetInfoSubscribe, createClient, createConsoleLogger, useClient, useConfigValue, useContext };
@@ -1,4 +1,4 @@
1
- import React, { Component, useContext } from "react";
1
+ import React, { Component, useContext as useContext$1 } from "react";
2
2
  import { AppState } from "react-native";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region \0@oxc-project+runtime@0.122.0/helpers/typeof.js
@@ -143,8 +143,8 @@ var EventSourceParser = class {
143
143
  constructor(options) {
144
144
  var _options$onEvent;
145
145
  _defineProperty(this, "isFirstChunk", true);
146
- _defineProperty(this, "previousIncompleteLine", "");
147
- _defineProperty(this, "currentEvent", void 0);
146
+ _defineProperty(this, "bufferedInput", "");
147
+ _defineProperty(this, "currentEvent", { data: "" });
148
148
  _defineProperty(this, "lastEventId", void 0);
149
149
  _defineProperty(this, "onEvent", void 0);
150
150
  _defineProperty(this, "onRetry", void 0);
@@ -154,83 +154,95 @@ var EventSourceParser = class {
154
154
  this.onRetry = options.onRetry;
155
155
  this.onComment = options.onComment;
156
156
  }
157
- parse(input) {
158
- const chunk = this.prepareChunk(input);
159
- const lines = this.splitLines(chunk);
160
- for (const line of lines) this.parseAndProcessLine(line);
157
+ parse(chunk) {
158
+ let input = chunk;
159
+ if (this.isFirstChunk) {
160
+ this.isFirstChunk = false;
161
+ if (input.startsWith("")) input = input.slice(1);
162
+ else if (input.startsWith("")) input = input.slice(3);
163
+ }
164
+ const lines = this.scanLines(this.bufferedInput + input);
165
+ for (const line of lines) this.dispatchLine(line);
161
166
  }
162
167
  finish() {
163
- if (this.previousIncompleteLine !== "") this.parseAndProcessLine(this.previousIncompleteLine);
168
+ this.bufferedInput = "";
169
+ }
170
+ /**
171
+ * Scans `text` character by character to extract complete lines, recognizing
172
+ * CR, LF, and CRLF as line terminators per the SSE spec. Any unterminated
173
+ * trailing text is buffered and prepended to the next chunk.
174
+ */
175
+ scanLines(text) {
176
+ const lines = [];
177
+ let lineStart = 0;
178
+ let i = 0;
179
+ while (i < text.length) {
180
+ const ch = text[i];
181
+ if (ch === "\r" || ch === "\n") {
182
+ lines.push(text.slice(lineStart, i));
183
+ if (ch === "\r" && text[i + 1] === "\n") i++;
184
+ i++;
185
+ lineStart = i;
186
+ } else i++;
187
+ }
188
+ this.bufferedInput = text.slice(lineStart);
189
+ return lines;
164
190
  }
165
- parseAndProcessLine(line) {
191
+ dispatchLine(line) {
166
192
  if (line.startsWith(":")) {
167
193
  var _this$onComment;
168
- (_this$onComment = this.onComment) === null || _this$onComment === void 0 || _this$onComment.call(this, this.getValueFromLine(0, line));
194
+ (_this$onComment = this.onComment) === null || _this$onComment === void 0 || _this$onComment.call(this, this.extractValue(line, 1));
169
195
  return;
170
196
  }
171
197
  if (line === "") {
172
- this.publishEvent();
198
+ this.emitEvent();
173
199
  return;
174
200
  }
175
- const content = this.parseContentLine(line);
176
- this.processContent(content);
201
+ this.applyField(this.parseField(line));
177
202
  }
178
- publishEvent() {
203
+ emitEvent() {
179
204
  const data = this.currentEvent.data.endsWith("\n") ? this.currentEvent.data.slice(0, -1) : this.currentEvent.data;
180
- if (data) this.onEvent({
205
+ if (data !== "") this.onEvent({
181
206
  ...this.currentEvent,
182
207
  id: this.lastEventId,
183
208
  data
184
209
  });
185
210
  this.currentEvent = { data: "" };
186
211
  }
187
- processContent(content) {
188
- switch (content.field) {
212
+ applyField(parsed) {
213
+ switch (parsed.field) {
189
214
  case "event":
190
- this.currentEvent.type = content.value;
215
+ this.currentEvent.type = parsed.value;
191
216
  break;
192
217
  case "data":
193
- this.currentEvent.data += content.value + "\n";
218
+ this.currentEvent.data += parsed.value + "\n";
194
219
  break;
195
220
  case "id":
196
- if (!content.value.includes("\0")) this.lastEventId = content.value;
221
+ if (!parsed.value.includes("\0")) this.lastEventId = parsed.value;
197
222
  break;
198
223
  case "retry":
199
- if (/^\d+$/.test(content.value)) {
224
+ if (/^\d+$/.test(parsed.value)) {
200
225
  var _this$onRetry;
201
- (_this$onRetry = this.onRetry) === null || _this$onRetry === void 0 || _this$onRetry.call(this, Number.parseInt(content.value, 10));
226
+ (_this$onRetry = this.onRetry) === null || _this$onRetry === void 0 || _this$onRetry.call(this, Number.parseInt(parsed.value, 10));
202
227
  }
203
228
  break;
204
229
  default: break;
205
230
  }
206
231
  }
207
- parseContentLine(line) {
208
- const delimiterIndex = line.indexOf(":");
209
- if (delimiterIndex === -1) return {
232
+ parseField(line) {
233
+ const colon = line.indexOf(":");
234
+ if (colon === -1) return {
210
235
  field: line,
211
236
  value: ""
212
237
  };
213
238
  return {
214
- field: line.slice(0, delimiterIndex),
215
- value: this.getValueFromLine(delimiterIndex, line)
239
+ field: line.slice(0, colon),
240
+ value: this.extractValue(line, colon + 1)
216
241
  };
217
242
  }
218
- splitLines(chunk) {
219
- var _lines$pop;
220
- const lines = chunk.split(/\r\n|\r|\n/);
221
- this.previousIncompleteLine = (_lines$pop = lines.pop()) !== null && _lines$pop !== void 0 ? _lines$pop : "";
222
- return lines;
223
- }
224
- getValueFromLine(delimiterIndex, line) {
225
- if (delimiterIndex === -1) return "";
226
- const valueStartIndex = line[delimiterIndex + 1] === " " ? delimiterIndex + 2 : delimiterIndex + 1;
227
- return line.slice(valueStartIndex);
228
- }
229
- prepareChunk(chunk) {
230
- if (this.isFirstChunk) {
231
- this.isFirstChunk = false;
232
- return this.previousIncompleteLine + chunk.replace(/^\xEF\xBB\xBF/, "").replace(/^\uFEFF/, "");
233
- } else return this.previousIncompleteLine + chunk;
243
+ /** Returns the value portion of a line, stripping a single leading space per spec. */
244
+ extractValue(line, from) {
245
+ return line[from] === " " ? line.slice(from + 1) : line.slice(from);
234
246
  }
235
247
  };
236
248
  //#endregion
@@ -273,34 +285,34 @@ var EventSourceClient = class {
273
285
  referrerPolicy: options.referrerPolicy
274
286
  };
275
287
  this._fetch = (_options$fetch = options.fetch) !== null && _options$fetch !== void 0 ? _options$fetch : ((url, init) => fetch(url, init));
276
- this._onError = this.getValidatedFunctionOrNoOp(options.onError, "onError");
277
- this._onConnect = this.getValidatedFunctionOrNoOp(options.onConnect, "onConnect");
278
- this._onDisconnect = this.getValidatedFunctionOrNoOp(options.onDisconnect, "onDisconnect");
279
- this._onMessage = this.getValidatedFunctionOrNoOp(options.onMessage, "onMessage");
280
- this._onComment = this.getValidatedFunctionOrNoOp(options.onComment, "onComment");
281
- this._shouldReconnect = this.getValidatedFunctionOr(options.shouldReconnect, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
282
- this._calculateReconnectDelay = this.getValidatedFunctionOr(options.calculateReconnectDelay, this.DEFAULT_CALCULATE_RECONNECT_DELAY, "calculateReconnectDelay");
288
+ this._onError = this.resolveCallback(options.onError, () => {}, "onError");
289
+ this._onConnect = this.resolveCallback(options.onConnect, () => {}, "onConnect");
290
+ this._onDisconnect = this.resolveCallback(options.onDisconnect, () => {}, "onDisconnect");
291
+ this._onMessage = this.resolveCallback(options.onMessage, () => {}, "onMessage");
292
+ this._onComment = this.resolveCallback(options.onComment, () => {}, "onComment");
293
+ this._shouldReconnect = this.resolveCallback(options.shouldReconnect, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
294
+ this._calculateReconnectDelay = this.resolveCallback(options.calculateReconnectDelay, this.DEFAULT_CALCULATE_RECONNECT_DELAY, "calculateReconnectDelay");
283
295
  }
284
296
  set onError(value) {
285
- this._onError = this.getValidatedFunctionOrNoOp(value, "onError");
297
+ this._onError = this.resolveCallback(value, () => {}, "onError");
286
298
  }
287
299
  set onMessage(value) {
288
- this._onMessage = this.getValidatedFunctionOrNoOp(value, "onMessage");
300
+ this._onMessage = this.resolveCallback(value, () => {}, "onMessage");
289
301
  }
290
302
  set onComment(value) {
291
- this._onComment = this.getValidatedFunctionOrNoOp(value, "onComment");
303
+ this._onComment = this.resolveCallback(value, () => {}, "onComment");
292
304
  }
293
305
  set onConnect(value) {
294
- this._onConnect = this.getValidatedFunctionOrNoOp(value, "onConnect");
306
+ this._onConnect = this.resolveCallback(value, () => {}, "onConnect");
295
307
  }
296
308
  set onDisconnect(value) {
297
- this._onDisconnect = this.getValidatedFunctionOrNoOp(value, "onDisconnect");
309
+ this._onDisconnect = this.resolveCallback(value, () => {}, "onDisconnect");
298
310
  }
299
311
  set shouldReconnect(value) {
300
- this._shouldReconnect = this.getValidatedFunctionOr(value, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
312
+ this._shouldReconnect = this.resolveCallback(value, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
301
313
  }
302
314
  set calculateReconnectDelay(value) {
303
- this._calculateReconnectDelay = this.getValidatedFunctionOr(value, this.DEFAULT_CALCULATE_RECONNECT_DELAY, "shouldReconnect");
315
+ this._calculateReconnectDelay = this.resolveCallback(value, this.DEFAULT_CALCULATE_RECONNECT_DELAY, "shouldReconnect");
304
316
  }
305
317
  get readyState() {
306
318
  return this._readyState;
@@ -310,15 +322,20 @@ var EventSourceClient = class {
310
322
  this.reconnectAttempt = 0;
311
323
  this.initiateConnection();
312
324
  }
325
+ close() {
326
+ clearTimeout(this.reconnectTimeout);
327
+ this._readyState = ReadyState.CLOSED;
328
+ this.abortController.abort();
329
+ }
313
330
  initiateConnection() {
314
331
  this.abortController = new AbortController();
315
332
  this._readyState = ReadyState.CONNECTING;
316
333
  this._fetch(this.url, {
317
334
  ...this.requestOptions,
318
- headers: this.buildHeaders(),
335
+ headers: this.buildRequestHeaders(),
319
336
  signal: this.abortController.signal,
320
337
  cache: "no-store"
321
- }).then(this.handleFetchResponse.bind(this)).catch((error) => {
338
+ }).then((response) => this.handleFetchResponse(response)).catch((error) => {
322
339
  if (error.name === "AbortError" || this.abortController.signal.aborted) {
323
340
  this.close();
324
341
  return;
@@ -327,23 +344,6 @@ var EventSourceClient = class {
327
344
  this.scheduleReconnect({ error });
328
345
  });
329
346
  }
330
- close() {
331
- clearTimeout(this.reconnectTimeout);
332
- this._readyState = ReadyState.CLOSED;
333
- this.abortController.abort();
334
- }
335
- disconnected() {
336
- clearTimeout(this.reconnectTimeout);
337
- this._readyState = ReadyState.CLOSED;
338
- this._onDisconnect();
339
- }
340
- buildHeaders() {
341
- const lastEventIdHeader = this.lastEventId ? { "Last-Event-ID": this.lastEventId } : void 0;
342
- return {
343
- ...this.headers,
344
- ...lastEventIdHeader
345
- };
346
- }
347
347
  async handleFetchResponse(response) {
348
348
  const { body, status } = response;
349
349
  if (status === 204) {
@@ -358,8 +358,6 @@ var EventSourceClient = class {
358
358
  this._readyState = ReadyState.OPEN;
359
359
  this._onConnect();
360
360
  this.reconnectAttempt = 0;
361
- const reader = body.getReader();
362
- const decoder = new TextDecoder();
363
361
  const parser = new EventSourceParser({
364
362
  onEvent: (event) => {
365
363
  if (typeof event.id === "string") this.lastEventId = event.id;
@@ -368,18 +366,35 @@ var EventSourceClient = class {
368
366
  onComment: (comment) => this._onComment(comment),
369
367
  onRetry: (retryDelay) => this.serverReconnectionTime = retryDelay
370
368
  });
371
- let done = false;
372
- while (!done) {
373
- const { done: readerDone, value } = await reader.read();
374
- if (value) parser.parse(decoder.decode(value, { stream: true }));
375
- done = readerDone;
376
- }
377
- parser.finish();
369
+ await this.readResponseStream(body, parser);
378
370
  this.scheduleReconnect({
379
371
  status,
380
372
  error: new StreamClosedError("The server response stream was closed")
381
373
  });
382
374
  }
375
+ async readResponseStream(body, parser) {
376
+ const reader = body.getReader();
377
+ const decoder = new TextDecoder();
378
+ let done = false;
379
+ while (!done) {
380
+ const result = await reader.read();
381
+ if (result.value) parser.parse(decoder.decode(result.value, { stream: true }));
382
+ done = result.done;
383
+ }
384
+ parser.finish();
385
+ }
386
+ disconnected() {
387
+ clearTimeout(this.reconnectTimeout);
388
+ this._readyState = ReadyState.CLOSED;
389
+ this._onDisconnect();
390
+ }
391
+ buildRequestHeaders() {
392
+ const lastEventIdHeader = this.lastEventId ? { "Last-Event-ID": this.lastEventId } : void 0;
393
+ return {
394
+ ...this.headers,
395
+ ...lastEventIdHeader
396
+ };
397
+ }
383
398
  scheduleReconnect(state) {
384
399
  if (this.abortController.signal.aborted) return;
385
400
  clearTimeout(this.reconnectTimeout);
@@ -390,26 +405,22 @@ var EventSourceClient = class {
390
405
  error: state.error,
391
406
  serverReconnectionTime: this.serverReconnectionTime
392
407
  };
393
- if (this._shouldReconnect(reconnectionState)) {
394
- this._readyState = ReadyState.CONNECTING;
395
- let delay = this._calculateReconnectDelay(reconnectionState);
396
- if (!Number.isFinite(delay) || delay < 1 || delay > 1e3 * 60 * 60) {
397
- this._onError(new ValueOutOfRangeError(`The calculated reconnect delay is out of range: ${delay}. Defaulting to ${this.serverReconnectionTime}`));
398
- delay = this.serverReconnectionTime;
399
- }
400
- this.reconnectTimeout = setTimeout(() => this.initiateConnection(), delay);
401
- } else {
408
+ if (!this._shouldReconnect(reconnectionState)) {
402
409
  this.disconnected();
403
410
  return;
404
411
  }
412
+ this._readyState = ReadyState.CONNECTING;
413
+ let delay = this._calculateReconnectDelay(reconnectionState);
414
+ if (!Number.isFinite(delay) || delay < 1 || delay > 1e3 * 60 * 60) {
415
+ this._onError(new ValueOutOfRangeError(`The calculated reconnect delay is out of range: ${delay}. Defaulting to ${this.serverReconnectionTime}`));
416
+ delay = this.serverReconnectionTime;
417
+ }
418
+ this.reconnectTimeout = setTimeout(() => this.initiateConnection(), delay);
405
419
  }
406
- getValidatedFunctionOrNoOp(func, name) {
407
- return this.getValidatedFunctionOr(func, () => {}, name);
408
- }
409
- getValidatedFunctionOr(func, defaultFunc, name) {
410
- if (func == null) return defaultFunc;
411
- else if (typeof func === "function") return func;
412
- else throw new InvalidOptionError(`${name} must be a function`);
420
+ resolveCallback(provided, fallback, optionName) {
421
+ if (provided == null) return fallback;
422
+ if (typeof provided !== "function") throw new InvalidOptionError(`${optionName} must be a function`);
423
+ return provided;
413
424
  }
414
425
  };
415
426
  //#endregion
@@ -668,6 +679,61 @@ const createDefaultLogger = (level, messageDecorator) => {
668
679
  return new DefaultConsoleLogger(level !== null && level !== void 0 ? level : "warn", messageDecorator !== null && messageDecorator !== void 0 ? messageDecorator : new LogMessageDecorator());
669
680
  };
670
681
  //#endregion
682
+ //#region ../shared/src/transport/AbstractPollingTransport.ts
683
+ var AbstractPollingTransport = class {
684
+ constructor() {
685
+ _defineProperty(this, "fatalError", false);
686
+ _defineProperty(this, "pollingInterval", void 0);
687
+ _defineProperty(this, "pollingIntervalSeconds", 60);
688
+ }
689
+ clearPollingInterval() {
690
+ clearInterval(this.pollingInterval);
691
+ }
692
+ schedulePollingInterval(callback) {
693
+ clearInterval(this.pollingInterval);
694
+ if (this.pollingIntervalSeconds <= 0) return;
695
+ this.pollingInterval = setInterval(callback, this.pollingIntervalSeconds * 1e3);
696
+ }
697
+ async handleNonOkResponse(response) {
698
+ if (!response.ok) if (this.isStatusFatal(response.status)) {
699
+ this.fatalError = true;
700
+ throw this.prepareFatalResponseStatusError(response.status, await response.text());
701
+ } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
702
+ }
703
+ handleFetchError(fetchError) {
704
+ if (this.isFetchErrorFatal(fetchError)) {
705
+ this.fatalError = true;
706
+ this.close();
707
+ throw new ConfigDirectorConnectionError(`Connection failed with fatal error: ${fetchError}. This is an unrecoverable error, retry attempts will be ignored.`);
708
+ } else if (fetchError instanceof SyntaxError) throw new ConfigDirectorConnectionError(`Failed to parse the response from the server: ${fetchError}`);
709
+ else throw new ConfigDirectorConnectionError(`Connection failed with error: ${fetchError}.`);
710
+ }
711
+ prepareFatalResponseStatusError(responseStatus, errorBody) {
712
+ var _errorBody$trim$lengt, _errorBody$trim;
713
+ const status = responseStatus !== null && responseStatus !== void 0 ? responseStatus : 0;
714
+ return new ConfigDirectorConnectionError(`${`Connection failed with status: ${responseStatus !== null && responseStatus !== void 0 ? responseStatus : "unknown"}`}${((_errorBody$trim$lengt = errorBody === null || errorBody === void 0 || (_errorBody$trim = errorBody.trim()) === null || _errorBody$trim === void 0 ? void 0 : _errorBody$trim.length) !== null && _errorBody$trim$lengt !== void 0 ? _errorBody$trim$lengt : 0) > 0 ? ` (${errorBody})` : ""}. This is an unrecoverable error, retry attempts will be ignored.`, status);
715
+ }
716
+ isStatusFatal(status) {
717
+ return !!status && status >= 400 && status < 500;
718
+ }
719
+ isFetchErrorFatal(fetchError) {
720
+ if ((fetchError === null || fetchError === void 0 ? void 0 : fetchError.name) === "NotAllowedError") return true;
721
+ else if (fetchError instanceof TypeError) return true;
722
+ return false;
723
+ }
724
+ get isConnected() {
725
+ return this.pollingInterval !== void 0;
726
+ }
727
+ close() {
728
+ clearInterval(this.pollingInterval);
729
+ this.pollingInterval = void 0;
730
+ }
731
+ dispose() {
732
+ this.close();
733
+ this.clear();
734
+ }
735
+ };
736
+ //#endregion
671
737
  //#region ../shared/src/fetchWithTimeout.ts
672
738
  const fetchWithTimeout = async (timeout, resource, options, logger) => {
673
739
  const abortController = new AbortController();
@@ -689,22 +755,35 @@ const fetchWithTimeout = async (timeout, resource, options, logger) => {
689
755
  }
690
756
  };
691
757
  //#endregion
692
- //#region ../js-client-core/src/PullTransport.ts
693
- var PullTransport = class {
758
+ //#region ../js-client-core/src/PollingTransport.ts
759
+ var PollingTransport = class extends AbstractPollingTransport {
694
760
  constructor(options) {
761
+ var _options$pollingInter;
762
+ super();
695
763
  this.options = options;
696
764
  _defineProperty(this, "logger", void 0);
697
765
  _defineProperty(this, "eventEmitter", new Emitter());
698
766
  _defineProperty(this, "url", void 0);
699
- _defineProperty(this, "fatalError", false);
767
+ _defineProperty(this, "lastUpdateTimestamp", void 0);
700
768
  this.options = options;
701
769
  this.logger = options.logger;
702
- this.url = options.resolveUrl("client/pull/v1", options.baseUrl);
770
+ this.url = options.resolveUrl("client/polling/v1", options.baseUrl);
771
+ this.pollingIntervalSeconds = (_options$pollingInter = options.pollingInterval) !== null && _options$pollingInter !== void 0 ? _options$pollingInter : 60;
703
772
  }
704
773
  async connect(context, timeout) {
774
+ this.clearPollingInterval();
775
+ await this.fetchConfigs(context, timeout);
776
+ this.schedulePollingInterval(() => {
777
+ this.fetchConfigs(context, timeout).catch((error) => {
778
+ this.logger.error("[PollingTransport] Error during polling:", error);
779
+ });
780
+ });
781
+ return this;
782
+ }
783
+ async fetchConfigs(context, timeout) {
705
784
  if (this.fatalError) {
706
- this.logger.warn("[PullTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
707
- return this;
785
+ this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
786
+ return;
708
787
  }
709
788
  try {
710
789
  const response = await fetchWithTimeout(timeout, this.url, {
@@ -713,32 +792,20 @@ var PullTransport = class {
713
792
  body: JSON.stringify({
714
793
  givenContext: context,
715
794
  metaContext: this.options.metaContext,
716
- clientSdkKey: this.options.clientSdkKey
795
+ clientSdkKey: this.options.clientSdkKey,
796
+ lastUpdateTimestamp: this.lastUpdateTimestamp
717
797
  })
718
798
  }, this.logger);
719
- if (!response.ok) if (this.isStatusFatal(response.status)) {
720
- this.fatalError = true;
721
- throw this.prepareFatalResponseStatusError(response.status, await response.text());
722
- } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
723
- const json = JSON.parse(await response.text());
724
- this.eventEmitter.emit("configSetReceived", json);
725
- return this;
799
+ await this.handleNonOkResponse(response);
800
+ if (response.status == 200) {
801
+ const json = JSON.parse(await response.text());
802
+ this.lastUpdateTimestamp = json.timestamp;
803
+ this.eventEmitter.emit("configSetReceived", json);
804
+ }
726
805
  } catch (fetchError) {
727
- if (isFetchErrorFatal(fetchError)) {
728
- this.fatalError = true;
729
- throw new ConfigDirectorConnectionError(`Connection failed with fatal error: ${fetchError}. This is an unrecoverable error, retry attempts will be ignored.`);
730
- } else if (fetchError instanceof SyntaxError) throw new ConfigDirectorConnectionError(`Failed to parse the response from the server: ${fetchError}`);
731
- else throw new ConfigDirectorConnectionError(`Connection failed with error: ${fetchError}.`);
806
+ this.handleFetchError(fetchError);
732
807
  }
733
808
  }
734
- prepareFatalResponseStatusError(responseStatus, errorBody) {
735
- var _errorBody$trim$lengt, _errorBody$trim;
736
- const status = responseStatus !== null && responseStatus !== void 0 ? responseStatus : 0;
737
- return new ConfigDirectorConnectionError(`${`Connection failed with status: ${responseStatus !== null && responseStatus !== void 0 ? responseStatus : "unknown"}`}${((_errorBody$trim$lengt = errorBody === null || errorBody === void 0 || (_errorBody$trim = errorBody.trim()) === null || _errorBody$trim === void 0 ? void 0 : _errorBody$trim.length) !== null && _errorBody$trim$lengt !== void 0 ? _errorBody$trim$lengt : 0) > 0 ? ` (${errorBody})` : ""}. This is an unrecoverable error, retry attempts will be ignored.`, status);
738
- }
739
- isStatusFatal(status) {
740
- return !!status && status >= 400 && status < 500;
741
- }
742
809
  on(name, handler) {
743
810
  this.eventEmitter.on(name, handler);
744
811
  }
@@ -748,10 +815,13 @@ var PullTransport = class {
748
815
  clear() {
749
816
  this.eventEmitter.clear();
750
817
  }
751
- close() {}
752
- dispose() {
753
- this.close();
754
- this.clear();
818
+ };
819
+ //#endregion
820
+ //#region ../js-client-core/src/OneTimeTransport.ts
821
+ var OneTimeTransport = class extends PollingTransport {
822
+ constructor(options) {
823
+ super(options);
824
+ this.pollingIntervalSeconds = 0;
755
825
  }
756
826
  };
757
827
  //#endregion
@@ -765,7 +835,7 @@ const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
765
835
  const MAX_EXPONENTIAL_DELAY = 9;
766
836
  var DefaultConfigDirectorClient = class {
767
837
  constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
768
- var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _this$parseUrl, _clientOptions$connec3, _clientOptions$connec4, _navigator, _internalClientOption2;
838
+ var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _this$parseUrl, _clientOptions$connec3, _clientOptions$connec4, _clientOptions$connec5, _navigator, _internalClientOption2, _clientOptions$connec6;
769
839
  _defineProperty(this, "logger", void 0);
770
840
  _defineProperty(this, "telemetryClient", void 0);
771
841
  _defineProperty(this, "configSet", void 0);
@@ -775,16 +845,17 @@ var DefaultConfigDirectorClient = class {
775
845
  _defineProperty(this, "timeout", void 0);
776
846
  _defineProperty(this, "timeoutTimer", void 0);
777
847
  _defineProperty(this, "ready", false);
848
+ _defineProperty(this, "initializing", false);
778
849
  _defineProperty(this, "readyPromise", void 0);
779
850
  _defineProperty(this, "readyResolve", void 0);
780
851
  _defineProperty(this, "currentContext", void 0);
781
- _defineProperty(this, "streaming", void 0);
852
+ _defineProperty(this, "connectionMode", void 0);
782
853
  this.logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createDefaultLogger();
783
854
  this.timeout = (_clientOptions$connec = clientOptions === null || clientOptions === void 0 || (_clientOptions$connec2 = clientOptions.connection) === null || _clientOptions$connec2 === void 0 ? void 0 : _clientOptions$connec2.timeout) !== null && _clientOptions$connec !== void 0 ? _clientOptions$connec : 3e3;
784
855
  const urlFactory = (_internalClientOption = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.urlFactory) !== null && _internalClientOption !== void 0 ? _internalClientOption : defaultUrlFactory;
785
856
  const baseUrl = (_this$parseUrl = this.parseUrl(clientOptions === null || clientOptions === void 0 || (_clientOptions$connec3 = clientOptions.connection) === null || _clientOptions$connec3 === void 0 ? void 0 : _clientOptions$connec3.url, urlFactory)) !== null && _this$parseUrl !== void 0 ? _this$parseUrl : CLIENT_BASE_URL;
786
- this.streaming = (clientOptions === null || clientOptions === void 0 || (_clientOptions$connec4 = clientOptions.connection) === null || _clientOptions$connec4 === void 0 ? void 0 : _clientOptions$connec4.streaming) === false ? false : true;
787
- const transportConstructor = this.streaming ? StreamingTransport : PullTransport;
857
+ this.connectionMode = (_clientOptions$connec4 = clientOptions === null || clientOptions === void 0 || (_clientOptions$connec5 = clientOptions.connection) === null || _clientOptions$connec5 === void 0 ? void 0 : _clientOptions$connec5.mode) !== null && _clientOptions$connec4 !== void 0 ? _clientOptions$connec4 : "streaming";
858
+ const transportConstructor = this.getTransportConstructor(this.connectionMode);
788
859
  this.telemetryClient = telemetryClient;
789
860
  this.transport = new transportConstructor({
790
861
  clientSdkKey,
@@ -800,7 +871,8 @@ var DefaultConfigDirectorClient = class {
800
871
  fetch: internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.fetch,
801
872
  connectionRetryDelay: (_internalClientOption2 = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.connectionRetryDelay) !== null && _internalClientOption2 !== void 0 ? _internalClientOption2 : ((attempt) => {
802
873
  return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
803
- })
874
+ }),
875
+ pollingInterval: clientOptions === null || clientOptions === void 0 || (_clientOptions$connec6 = clientOptions.connection) === null || _clientOptions$connec6 === void 0 ? void 0 : _clientOptions$connec6.pollingInterval
804
876
  });
805
877
  this.transport.on("configSetReceived", (configSet) => {
806
878
  var _this$readyResolve;
@@ -821,12 +893,23 @@ var DefaultConfigDirectorClient = class {
821
893
  this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
822
894
  });
823
895
  }
896
+ getTransportConstructor(connectionMode) {
897
+ switch (connectionMode) {
898
+ case "one-time": return OneTimeTransport;
899
+ case "polling": return PollingTransport;
900
+ default: return StreamingTransport;
901
+ }
902
+ }
824
903
  async initialize(context) {
904
+ this.initializing = true;
825
905
  await this.connectToTransport(context, "initialization");
826
906
  }
827
907
  async updateContext(context) {
828
908
  await this.connectToTransport(context, "context update");
829
909
  }
910
+ get context() {
911
+ return this.currentContext;
912
+ }
830
913
  async connectToTransport(context, caller) {
831
914
  try {
832
915
  this.ready = false;
@@ -834,6 +917,7 @@ var DefaultConfigDirectorClient = class {
834
917
  this.readyResolve = resolve;
835
918
  }).then(() => {
836
919
  this.ready = true;
920
+ this.initializing = false;
837
921
  this.eventEmitter.emit("clientReady", { action: caller });
838
922
  this.logger.debug("[ConfigDirectorClient] Received initial payload from the server, client is ready");
839
923
  });
@@ -841,6 +925,7 @@ var DefaultConfigDirectorClient = class {
841
925
  await this.transport.connect(context !== null && context !== void 0 ? context : {}, this.timeout);
842
926
  this.currentContext = context;
843
927
  this.telemetryClient.updateContext(context);
928
+ this.eventEmitter.emit("contextUpdated", { context });
844
929
  const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - startTime;
845
930
  const remainingTimeout = this.timeout - elapsedTime;
846
931
  clearTimeout(this.timeoutTimer);
@@ -848,10 +933,7 @@ var DefaultConfigDirectorClient = class {
848
933
  if (remainingTimeout > 0) await Promise.race([this.readyPromise, new Promise((resolve) => {
849
934
  this.timeoutTimer = setTimeout(() => resolve(), remainingTimeout);
850
935
  })]).finally(() => clearTimeout(this.timeoutTimer));
851
- if (!this.ready) {
852
- const warningDetails = this.streaming ? "The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds." : "Since the client was configured without streaming, configs may not update and always return the default value.";
853
- this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. ${warningDetails}`);
854
- }
936
+ if (!this.ready) this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`);
855
937
  } catch (error) {
856
938
  this.logger.error(`[ConfigDirectorClient] An error occurred during ${caller}: `, error);
857
939
  }
@@ -936,6 +1018,9 @@ var DefaultConfigDirectorClient = class {
936
1018
  get isReady() {
937
1019
  return this.ready;
938
1020
  }
1021
+ get isInitializing() {
1022
+ return this.initializing;
1023
+ }
939
1024
  on(eventName, handler) {
940
1025
  this.eventEmitter.on(eventName, handler);
941
1026
  }
@@ -1243,89 +1328,119 @@ const createConsoleLogger = (level) => createDefaultLogger(level, new MessageDec
1243
1328
  //#endregion
1244
1329
  //#region src/utf8Encoder.ts
1245
1330
  /**
1246
- * Encodes a string to UTF-8 bytes. Uses the native TextEncoder when available
1247
- * (Hermes), and falls back to an encodeURIComponent-based approach for JSC.
1331
+ * Encodes a string to a UTF-8 byte sequence.
1332
+ *
1333
+ * Uses the native TextEncoder when available (Hermes runtime). For environments
1334
+ * that lack it (JSC), falls back to a manual implementation that follows the
1335
+ * UTF-8 encoding algorithm from the Unicode standard:
1336
+ *
1337
+ * U+0000–U+007F → 1 byte 0xxxxxxx
1338
+ * U+0080–U+07FF → 2 bytes 110xxxxx 10xxxxxx
1339
+ * U+0800–U+FFFF → 3 bytes 1110xxxx 10xxxxxx 10xxxxxx
1340
+ * U+10000–U+10FFFF → 4 bytes 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1341
+ *
1342
+ * JavaScript strings are UTF-16, so code points above U+FFFF are stored as
1343
+ * surrogate pairs and must be recombined before encoding.
1248
1344
  */
1249
1345
  const encodeUtf8 = (str) => {
1250
- if (typeof globalThis["TextEncoder"] === "function") return new TextEncoder().encode(str);
1251
- const uri = encodeURIComponent(str);
1346
+ if (typeof TextEncoder === "function") return new TextEncoder().encode(str);
1347
+ return encodeManually(str);
1348
+ };
1349
+ const encodeManually = (str) => {
1252
1350
  const bytes = [];
1253
- for (let i = 0; i < uri.length;) if (uri[i] === "%") {
1254
- bytes.push(parseInt(uri.slice(i + 1, i + 3), 16));
1255
- i += 3;
1256
- } else {
1257
- bytes.push(uri.charCodeAt(i));
1258
- i++;
1351
+ let i = 0;
1352
+ while (i < str.length) {
1353
+ let cp = str.charCodeAt(i++);
1354
+ if (cp >= 55296 && cp <= 56319 && i < str.length) {
1355
+ const low = str.charCodeAt(i);
1356
+ if (low >= 56320 && low <= 57343) {
1357
+ cp = 65536 + (cp - 55296 << 10) + (low - 56320);
1358
+ i++;
1359
+ }
1360
+ }
1361
+ if (cp <= 127) bytes.push(cp);
1362
+ else if (cp <= 2047) bytes.push(192 | cp >> 6, 128 | cp & 63);
1363
+ else if (cp <= 65535) bytes.push(224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63);
1364
+ else bytes.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
1259
1365
  }
1260
1366
  return new Uint8Array(bytes);
1261
1367
  };
1262
1368
  //#endregion
1263
1369
  //#region src/reactNativeStreamingFetch.ts
1370
+ const abortError = (message) => {
1371
+ const err = new Error(message);
1372
+ err.name = "AbortError";
1373
+ return err;
1374
+ };
1264
1375
  /**
1265
- * A fetch-compatible function for React Native that uses XMLHttpRequest to support
1266
- * streaming SSE responses. React Native's built-in fetch buffers the entire response
1267
- * body before resolving, which means it never resolves for persistent SSE connections.
1268
- * XHR's onprogress callback fires incrementally as data arrives.
1376
+ * A fetch-compatible function for React Native that uses XMLHttpRequest to
1377
+ * support streaming SSE responses. React Native's built-in fetch accumulates
1378
+ * the entire response body before resolving, so persistent SSE connections
1379
+ * never complete. XHR's progress events fire incrementally as data arrives,
1380
+ * which we forward into a ReadableStream.
1269
1381
  */
1270
1382
  const reactNativeStreamingFetch = (url, init) => {
1271
1383
  var _init$signal;
1272
- if ((_init$signal = init.signal) === null || _init$signal === void 0 ? void 0 : _init$signal.aborted) return Promise.reject(new DOMException("The operation was aborted.", "AbortError"));
1384
+ if ((_init$signal = init.signal) === null || _init$signal === void 0 ? void 0 : _init$signal.aborted) return Promise.reject(abortError("Request was aborted before it was opened"));
1273
1385
  return new Promise((resolve, reject) => {
1274
1386
  var _init$method, _ref;
1275
1387
  const xhr = new XMLHttpRequest();
1276
1388
  xhr.open((_init$method = init.method) !== null && _init$method !== void 0 ? _init$method : "GET", url);
1277
- const headers = init.headers;
1278
- if (headers) for (const [key, value] of Object.entries(headers)) xhr.setRequestHeader(key, value);
1279
- let resolved = false;
1280
- let lastLength = 0;
1281
- let controller;
1282
- const stream = new ReadableStream({ start(c) {
1283
- controller = c;
1389
+ const requestHeaders = init.headers;
1390
+ if (requestHeaders) for (const [name, value] of Object.entries(requestHeaders)) xhr.setRequestHeader(name, value);
1391
+ let responseResolved = false;
1392
+ let bytesForwarded = 0;
1393
+ let streamController;
1394
+ const responseBody = new ReadableStream({ start(ctrl) {
1395
+ streamController = ctrl;
1284
1396
  } });
1397
+ const flushNewBytes = () => {
1398
+ const next = xhr.responseText.slice(bytesForwarded);
1399
+ if (next.length > 0) {
1400
+ bytesForwarded += next.length;
1401
+ streamController.enqueue(encodeUtf8(next));
1402
+ }
1403
+ };
1285
1404
  xhr.onreadystatechange = () => {
1286
- if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED && !resolved) {
1287
- resolved = true;
1288
- const headers = new Headers();
1289
- xhr.getAllResponseHeaders().trim().split(/\r?\n/).forEach((line) => {
1290
- const [key, ...rest] = line.split(": ");
1291
- if (key) headers.set(key, rest.join(": "));
1292
- });
1405
+ if (xhr.readyState >= XMLHttpRequest.HEADERS_RECEIVED && !responseResolved) {
1406
+ responseResolved = true;
1293
1407
  resolve({
1294
1408
  status: xhr.status,
1295
- headers,
1296
- body: stream
1409
+ headers: parseHeaders(xhr.getAllResponseHeaders()),
1410
+ body: responseBody
1297
1411
  });
1298
1412
  }
1299
1413
  };
1300
1414
  xhr.onprogress = () => {
1301
- const newText = xhr.responseText.slice(lastLength);
1302
- lastLength = xhr.responseText.length;
1303
- if (newText) controller.enqueue(encodeUtf8(newText));
1415
+ flushNewBytes();
1304
1416
  };
1305
1417
  xhr.onload = () => {
1306
- const remaining = xhr.responseText.slice(lastLength);
1307
- if (remaining) controller.enqueue(encodeUtf8(remaining));
1308
- controller.close();
1418
+ flushNewBytes();
1419
+ streamController.close();
1309
1420
  };
1310
1421
  xhr.onerror = () => {
1311
- const error = /* @__PURE__ */ new TypeError("Network request failed");
1312
- if (!resolved) reject(error);
1313
- else controller.error(error);
1314
- xhr.abort();
1422
+ const err = /* @__PURE__ */ new TypeError("Network request failed");
1423
+ if (!responseResolved) reject(err);
1424
+ else streamController.error(err);
1315
1425
  };
1316
1426
  if (init.signal) init.signal.addEventListener("abort", () => {
1317
1427
  xhr.abort();
1318
- if (!resolved) {
1319
- const abortError = /* @__PURE__ */ new Error("The operation was aborted.");
1320
- abortError.name = "AbortError";
1321
- reject(abortError);
1322
- } else try {
1323
- controller.close();
1428
+ if (!responseResolved) reject(abortError("Request was aborted"));
1429
+ else try {
1430
+ streamController.close();
1324
1431
  } catch {}
1325
1432
  });
1326
1433
  xhr.send((_ref = init.body) !== null && _ref !== void 0 ? _ref : null);
1327
1434
  });
1328
1435
  };
1436
+ const parseHeaders = (raw) => {
1437
+ const headers = new Headers();
1438
+ for (const line of raw.trim().split(/\r?\n/)) {
1439
+ const sep = line.indexOf(": ");
1440
+ if (sep > 0) headers.set(line.slice(0, sep), line.slice(sep + 2));
1441
+ }
1442
+ return headers;
1443
+ };
1329
1444
  //#endregion
1330
1445
  //#region src/ReactNativeTelemetryClient.ts
1331
1446
  var ReactNativeTelemetryClient = class {
@@ -1377,7 +1492,7 @@ const createClient = (clientSdkKey, clientOptions) => {
1377
1492
  const logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createConsoleLogger("warn");
1378
1493
  return new DefaultConfigDirectorClient(new ReactNativeTelemetryClient(clientSdkKey, (clientOptions === null || clientOptions === void 0 || (_clientOptions$connec = clientOptions.connection) === null || _clientOptions$connec === void 0 ? void 0 : _clientOptions$connec.url) ? urlFactory(clientOptions.connection.url) : CLIENT_BASE_URL, logger, urlFactory), clientSdkKey, {
1379
1494
  sdkName: "react-native-sdk",
1380
- sdkVersion: "0.1.1"
1495
+ sdkVersion: "0.3.0"
1381
1496
  }, {
1382
1497
  ...clientOptions,
1383
1498
  logger
@@ -1481,7 +1596,7 @@ const noContextError = () => {
1481
1596
  return new ConfigDirectorReactContextError("The ConfigDirector client was not found in the React context. Please make sure the component using this hook is inside the context of a ConfigDirectorProvider.");
1482
1597
  };
1483
1598
  const useConfigValue = (key, defaultValue) => {
1484
- const configDirectorReactContext = useContext(reactContext);
1599
+ const configDirectorReactContext = useContext$1(reactContext);
1485
1600
  if (!(configDirectorReactContext === null || configDirectorReactContext === void 0 ? void 0 : configDirectorReactContext.client)) throw noContextError();
1486
1601
  return {
1487
1602
  value: configDirectorReactContext.client.getValue(key, defaultValue),
@@ -1489,8 +1604,8 @@ const useConfigValue = (key, defaultValue) => {
1489
1604
  loading: configDirectorReactContext.status === "loading"
1490
1605
  };
1491
1606
  };
1492
- const useConfigDirectorContext = () => {
1493
- const configDirectorReactContext = useContext(reactContext);
1607
+ const useContext = () => {
1608
+ const configDirectorReactContext = useContext$1(reactContext);
1494
1609
  if (!(configDirectorReactContext === null || configDirectorReactContext === void 0 ? void 0 : configDirectorReactContext.client)) throw noContextError();
1495
1610
  const updateContext = async (context) => {
1496
1611
  var _configDirectorReactC;
@@ -1498,10 +1613,10 @@ const useConfigDirectorContext = () => {
1498
1613
  };
1499
1614
  return { updateContext };
1500
1615
  };
1501
- const useConfigDirectorClient = () => {
1502
- const configDirectorReactContext = useContext(reactContext);
1616
+ const useClient = () => {
1617
+ const configDirectorReactContext = useContext$1(reactContext);
1503
1618
  if (!(configDirectorReactContext === null || configDirectorReactContext === void 0 ? void 0 : configDirectorReactContext.client)) throw noContextError();
1504
1619
  return { client: configDirectorReactContext.client };
1505
1620
  };
1506
1621
  //#endregion
1507
- export { ConfigDirectorProvider, ConfigDirectorReactContextError, createClient, createConsoleLogger, useConfigDirectorClient, useConfigDirectorContext, useConfigValue };
1622
+ export { ConfigDirectorProvider, ConfigDirectorReactContextError, createClient, createConsoleLogger, useClient, useConfigValue, useContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/react-native-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "React Native client SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",
@@ -21,7 +21,7 @@
21
21
  "homepage": "https://www.configdirector.com",
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "https://github.com/ConfigDirector/js-sdks"
24
+ "url": "git+https://github.com/ConfigDirector/js-sdks.git"
25
25
  },
26
26
  "main": "./dist/configdirector-react-native.mjs",
27
27
  "types": "./dist/configdirector-react-native.d.mts",
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "scripts": {
41
41
  "example": "yarn workspace @configdirector/react-native-sdk-example",
42
- "clean": "del-cli dist",
42
+ "clean": "rm -fr dist",
43
43
  "build": "tsdown",
44
44
  "prepare": "yarn build",
45
45
  "typecheck": "tsc",
@@ -78,9 +78,9 @@
78
78
  "wtfnode": "^0.10.1"
79
79
  },
80
80
  "peerDependencies": {
81
- "expo": ">=48",
81
+ "expo": ">=52",
82
82
  "react": "*",
83
- "react-native": ">=0.71"
83
+ "react-native": ">=0.75"
84
84
  },
85
85
  "peerDependenciesMeta": {
86
86
  "expo": {