@configdirector/react-native-sdk 0.1.0 → 0.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/README.md CHANGED
@@ -10,15 +10,94 @@ Install from NPM:
10
10
  npm install --save @configdirector/react-native-sdk
11
11
  ```
12
12
 
13
+ ### 2. Initialize the provider using your client SDK key
13
14
 
14
- ## Usage
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
+ ```
15
33
 
34
+ ### 3. Retrieve config values
35
+
36
+ Retrieve a config value with the `useConfigValue` hook:
16
37
 
17
38
  ```tsx
18
- import { createClient } from "@configdirector/react-native-sdk";
39
+ import { useConfigValue } from "@configdirector/react-native-sdk";
40
+
41
+ function MyComponent() {
42
+ const { value } = useConfigValue<string>("my-config", "default value");
19
43
 
44
+ return <p>My config value: {value}</p>;
45
+ }
46
+
47
+ export default MyComponent;
20
48
  ```
21
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 `useContext` hook:
74
+
75
+ ```tsx
76
+ import {
77
+ useConfigValue,
78
+ useContext,
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 } = useContext();
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
+ ```
22
101
 
23
102
  ## Requirements
24
103
 
@@ -37,3 +116,7 @@ Expo SDK 48 (RN 0.71) or later is required.
37
116
  ## License
38
117
 
39
118
  MIT
119
+
120
+ ## Getting Help
121
+
122
+ Reach out to us via https://www.configdirector.com/support
@@ -123,6 +123,9 @@ type ClientEvents = {
123
123
  clientReady: {
124
124
  action: ClientConnectAction;
125
125
  };
126
+ contextUpdated: {
127
+ context: ConfigDirectorContext | undefined;
128
+ };
126
129
  };
127
130
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
128
131
  /**
@@ -155,6 +158,15 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
155
158
  * @param context The current user's context to be used for evaluating targeting rules (required).
156
159
  */
157
160
  updateContext(context: ConfigDirectorContext): Promise<void>;
161
+ /**
162
+ * Returns the current context the client is configured with, or undefined if there is no context.
163
+ *
164
+ * Note that the value returned does not updated immediately after calling {@link updateContext}. Only
165
+ * once the underlying connection was successful or timed out, the current context is updated. This is
166
+ * because while the context update is in progress, configs are still evaluated on the previous context.
167
+ *
168
+ */
169
+ get context(): ConfigDirectorContext | undefined;
158
170
  /**
159
171
  * Returns whether or not the client is ready after calling {@link initialize} or {@link updateContext}
160
172
  *
@@ -162,6 +174,11 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
162
174
  * was received.
163
175
  */
164
176
  get isReady(): boolean;
177
+ /**
178
+ * Returns whether or not the client is currently initializing. Upon client creation, it is `false`.
179
+ * It is `true` after calling `initialize` and becomes `false` again once initialization is completed.
180
+ */
181
+ get isInitializing(): boolean;
165
182
  /**
166
183
  * Evaluates a config and returns its value based on the current context and targeting rules
167
184
  *
@@ -287,10 +304,10 @@ declare const useConfigValue: <T extends ConfigValueType>(key: string, defaultVa
287
304
  readyStatus: ClientStatus;
288
305
  loading: boolean;
289
306
  };
290
- declare const useConfigDirectorContext: () => {
307
+ declare const useContext: () => {
291
308
  updateContext: (context: ConfigDirectorContext) => Promise<void>;
292
309
  };
293
- declare const useConfigDirectorClient: () => {
310
+ declare const useClient: () => {
294
311
  client: ConfigDirectorClient;
295
312
  };
296
313
  //#endregion
@@ -306,4 +323,4 @@ declare class ConfigDirectorReactContextError extends Error {
306
323
  //#region src/logger.d.ts
307
324
  declare const createConsoleLogger: (level?: ConfigDirectorLoggingLevel) => DefaultConsoleLogger;
308
325
  //#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 };
326
+ 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
@@ -775,6 +786,7 @@ var DefaultConfigDirectorClient = class {
775
786
  _defineProperty(this, "timeout", void 0);
776
787
  _defineProperty(this, "timeoutTimer", void 0);
777
788
  _defineProperty(this, "ready", false);
789
+ _defineProperty(this, "initializing", false);
778
790
  _defineProperty(this, "readyPromise", void 0);
779
791
  _defineProperty(this, "readyResolve", void 0);
780
792
  _defineProperty(this, "currentContext", void 0);
@@ -822,11 +834,15 @@ var DefaultConfigDirectorClient = class {
822
834
  });
823
835
  }
824
836
  async initialize(context) {
837
+ this.initializing = true;
825
838
  await this.connectToTransport(context, "initialization");
826
839
  }
827
840
  async updateContext(context) {
828
841
  await this.connectToTransport(context, "context update");
829
842
  }
843
+ get context() {
844
+ return this.currentContext;
845
+ }
830
846
  async connectToTransport(context, caller) {
831
847
  try {
832
848
  this.ready = false;
@@ -834,6 +850,7 @@ var DefaultConfigDirectorClient = class {
834
850
  this.readyResolve = resolve;
835
851
  }).then(() => {
836
852
  this.ready = true;
853
+ this.initializing = false;
837
854
  this.eventEmitter.emit("clientReady", { action: caller });
838
855
  this.logger.debug("[ConfigDirectorClient] Received initial payload from the server, client is ready");
839
856
  });
@@ -841,6 +858,7 @@ var DefaultConfigDirectorClient = class {
841
858
  await this.transport.connect(context !== null && context !== void 0 ? context : {}, this.timeout);
842
859
  this.currentContext = context;
843
860
  this.telemetryClient.updateContext(context);
861
+ this.eventEmitter.emit("contextUpdated", { context });
844
862
  const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - startTime;
845
863
  const remainingTimeout = this.timeout - elapsedTime;
846
864
  clearTimeout(this.timeoutTimer);
@@ -936,6 +954,9 @@ var DefaultConfigDirectorClient = class {
936
954
  get isReady() {
937
955
  return this.ready;
938
956
  }
957
+ get isInitializing() {
958
+ return this.initializing;
959
+ }
939
960
  on(eventName, handler) {
940
961
  this.eventEmitter.on(eventName, handler);
941
962
  }
@@ -1243,89 +1264,114 @@ const createConsoleLogger = (level) => createDefaultLogger(level, new MessageDec
1243
1264
  //#endregion
1244
1265
  //#region src/utf8Encoder.ts
1245
1266
  /**
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.
1267
+ * Encodes a string to a UTF-8 byte sequence.
1268
+ *
1269
+ * Uses the native TextEncoder when available (Hermes runtime). For environments
1270
+ * that lack it (JSC), falls back to a manual implementation that follows the
1271
+ * UTF-8 encoding algorithm from the Unicode standard:
1272
+ *
1273
+ * U+0000–U+007F → 1 byte 0xxxxxxx
1274
+ * U+0080–U+07FF → 2 bytes 110xxxxx 10xxxxxx
1275
+ * U+0800–U+FFFF → 3 bytes 1110xxxx 10xxxxxx 10xxxxxx
1276
+ * U+10000–U+10FFFF → 4 bytes 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1277
+ *
1278
+ * JavaScript strings are UTF-16, so code points above U+FFFF are stored as
1279
+ * surrogate pairs and must be recombined before encoding.
1248
1280
  */
1249
1281
  const encodeUtf8 = (str) => {
1250
- if (typeof globalThis["TextEncoder"] === "function") return new TextEncoder().encode(str);
1251
- const uri = encodeURIComponent(str);
1282
+ if (typeof TextEncoder === "function") return new TextEncoder().encode(str);
1283
+ return encodeManually(str);
1284
+ };
1285
+ const encodeManually = (str) => {
1252
1286
  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++;
1287
+ let i = 0;
1288
+ while (i < str.length) {
1289
+ let cp = str.charCodeAt(i++);
1290
+ if (cp >= 55296 && cp <= 56319 && i < str.length) {
1291
+ const low = str.charCodeAt(i);
1292
+ if (low >= 56320 && low <= 57343) {
1293
+ cp = 65536 + (cp - 55296 << 10) + (low - 56320);
1294
+ i++;
1295
+ }
1296
+ }
1297
+ if (cp <= 127) bytes.push(cp);
1298
+ else if (cp <= 2047) bytes.push(192 | cp >> 6, 128 | cp & 63);
1299
+ else if (cp <= 65535) bytes.push(224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63);
1300
+ else bytes.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
1259
1301
  }
1260
1302
  return new Uint8Array(bytes);
1261
1303
  };
1262
1304
  //#endregion
1263
1305
  //#region src/reactNativeStreamingFetch.ts
1264
1306
  /**
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.
1307
+ * A fetch-compatible function for React Native that uses XMLHttpRequest to
1308
+ * support streaming SSE responses. React Native's built-in fetch accumulates
1309
+ * the entire response body before resolving, so persistent SSE connections
1310
+ * never complete. XHR's progress events fire incrementally as data arrives,
1311
+ * which we forward into a ReadableStream.
1269
1312
  */
1270
1313
  const reactNativeStreamingFetch = (url, init) => {
1271
1314
  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"));
1315
+ if ((_init$signal = init.signal) === null || _init$signal === void 0 ? void 0 : _init$signal.aborted) return Promise.reject(new DOMException("Request was aborted before it was opened", "AbortError"));
1273
1316
  return new Promise((resolve, reject) => {
1274
1317
  var _init$method, _ref;
1275
1318
  const xhr = new XMLHttpRequest();
1276
1319
  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;
1320
+ const requestHeaders = init.headers;
1321
+ if (requestHeaders) for (const [name, value] of Object.entries(requestHeaders)) xhr.setRequestHeader(name, value);
1322
+ let responseResolved = false;
1323
+ let bytesForwarded = 0;
1324
+ let streamController;
1325
+ const responseBody = new ReadableStream({ start(ctrl) {
1326
+ streamController = ctrl;
1284
1327
  } });
1328
+ const flushNewBytes = () => {
1329
+ const next = xhr.responseText.slice(bytesForwarded);
1330
+ if (next.length > 0) {
1331
+ bytesForwarded += next.length;
1332
+ streamController.enqueue(encodeUtf8(next));
1333
+ }
1334
+ };
1285
1335
  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
- });
1336
+ if (xhr.readyState >= XMLHttpRequest.HEADERS_RECEIVED && !responseResolved) {
1337
+ responseResolved = true;
1293
1338
  resolve({
1294
1339
  status: xhr.status,
1295
- headers,
1296
- body: stream
1340
+ headers: parseHeaders(xhr.getAllResponseHeaders()),
1341
+ body: responseBody
1297
1342
  });
1298
1343
  }
1299
1344
  };
1300
1345
  xhr.onprogress = () => {
1301
- const newText = xhr.responseText.slice(lastLength);
1302
- lastLength = xhr.responseText.length;
1303
- if (newText) controller.enqueue(encodeUtf8(newText));
1346
+ flushNewBytes();
1304
1347
  };
1305
1348
  xhr.onload = () => {
1306
- const remaining = xhr.responseText.slice(lastLength);
1307
- if (remaining) controller.enqueue(encodeUtf8(remaining));
1308
- controller.close();
1349
+ flushNewBytes();
1350
+ streamController.close();
1309
1351
  };
1310
1352
  xhr.onerror = () => {
1311
- const error = /* @__PURE__ */ new TypeError("Network request failed");
1312
- if (!resolved) reject(error);
1313
- else controller.error(error);
1314
- xhr.abort();
1353
+ const err = /* @__PURE__ */ new TypeError("Network request failed");
1354
+ if (!responseResolved) reject(err);
1355
+ else streamController.error(err);
1315
1356
  };
1316
1357
  if (init.signal) init.signal.addEventListener("abort", () => {
1317
1358
  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();
1359
+ if (!responseResolved) reject(new DOMException("Request was aborted", "AbortError"));
1360
+ else try {
1361
+ streamController.close();
1324
1362
  } catch {}
1325
1363
  });
1326
1364
  xhr.send((_ref = init.body) !== null && _ref !== void 0 ? _ref : null);
1327
1365
  });
1328
1366
  };
1367
+ const parseHeaders = (raw) => {
1368
+ const headers = new Headers();
1369
+ for (const line of raw.trim().split(/\r?\n/)) {
1370
+ const sep = line.indexOf(": ");
1371
+ if (sep > 0) headers.set(line.slice(0, sep), line.slice(sep + 2));
1372
+ }
1373
+ return headers;
1374
+ };
1329
1375
  //#endregion
1330
1376
  //#region src/ReactNativeTelemetryClient.ts
1331
1377
  var ReactNativeTelemetryClient = class {
@@ -1377,7 +1423,7 @@ const createClient = (clientSdkKey, clientOptions) => {
1377
1423
  const logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createConsoleLogger("warn");
1378
1424
  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
1425
  sdkName: "react-native-sdk",
1380
- sdkVersion: "0.1.0"
1426
+ sdkVersion: "0.2.0"
1381
1427
  }, {
1382
1428
  ...clientOptions,
1383
1429
  logger
@@ -1481,7 +1527,7 @@ const noContextError = () => {
1481
1527
  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
1528
  };
1483
1529
  const useConfigValue = (key, defaultValue) => {
1484
- const configDirectorReactContext = useContext(reactContext);
1530
+ const configDirectorReactContext = useContext$1(reactContext);
1485
1531
  if (!(configDirectorReactContext === null || configDirectorReactContext === void 0 ? void 0 : configDirectorReactContext.client)) throw noContextError();
1486
1532
  return {
1487
1533
  value: configDirectorReactContext.client.getValue(key, defaultValue),
@@ -1489,8 +1535,8 @@ const useConfigValue = (key, defaultValue) => {
1489
1535
  loading: configDirectorReactContext.status === "loading"
1490
1536
  };
1491
1537
  };
1492
- const useConfigDirectorContext = () => {
1493
- const configDirectorReactContext = useContext(reactContext);
1538
+ const useContext = () => {
1539
+ const configDirectorReactContext = useContext$1(reactContext);
1494
1540
  if (!(configDirectorReactContext === null || configDirectorReactContext === void 0 ? void 0 : configDirectorReactContext.client)) throw noContextError();
1495
1541
  const updateContext = async (context) => {
1496
1542
  var _configDirectorReactC;
@@ -1498,10 +1544,10 @@ const useConfigDirectorContext = () => {
1498
1544
  };
1499
1545
  return { updateContext };
1500
1546
  };
1501
- const useConfigDirectorClient = () => {
1502
- const configDirectorReactContext = useContext(reactContext);
1547
+ const useClient = () => {
1548
+ const configDirectorReactContext = useContext$1(reactContext);
1503
1549
  if (!(configDirectorReactContext === null || configDirectorReactContext === void 0 ? void 0 : configDirectorReactContext.client)) throw noContextError();
1504
1550
  return { client: configDirectorReactContext.client };
1505
1551
  };
1506
1552
  //#endregion
1507
- export { ConfigDirectorProvider, ConfigDirectorReactContextError, createClient, createConsoleLogger, useConfigDirectorClient, useConfigDirectorContext, useConfigValue };
1553
+ 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.0",
3
+ "version": "0.2.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",