@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 +4 -113
- package/dist/configdirector-react-native.d.mts +38 -12
- package/dist/configdirector-react-native.mjs +310 -195
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,121 +1,12 @@
|
|
|
1
1
|
# ConfigDirector React Native SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is the React Native SDK for [ConfigDirector](https://www.configdirector.com).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Documentation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Refer to the [official documentation for the React Native SDK](https://docs.configdirector.com/sdks/mobile/react-native).
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
*
|
|
80
|
-
* If set to
|
|
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
|
|
83
|
-
* initialization, and
|
|
84
|
-
*
|
|
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
|
|
88
|
+
* Defaults to `streaming`
|
|
87
89
|
*/
|
|
88
|
-
|
|
90
|
+
mode?: ConnectionMode;
|
|
89
91
|
/**
|
|
90
|
-
* The
|
|
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
|
-
|
|
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
|
|
316
|
+
declare const useContext: () => {
|
|
291
317
|
updateContext: (context: ConfigDirectorContext) => Promise<void>;
|
|
292
318
|
};
|
|
293
|
-
declare const
|
|
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,
|
|
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, "
|
|
147
|
-
_defineProperty(this, "currentEvent",
|
|
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(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
198
|
+
this.emitEvent();
|
|
173
199
|
return;
|
|
174
200
|
}
|
|
175
|
-
|
|
176
|
-
this.processContent(content);
|
|
201
|
+
this.applyField(this.parseField(line));
|
|
177
202
|
}
|
|
178
|
-
|
|
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
|
-
|
|
188
|
-
switch (
|
|
212
|
+
applyField(parsed) {
|
|
213
|
+
switch (parsed.field) {
|
|
189
214
|
case "event":
|
|
190
|
-
this.currentEvent.type =
|
|
215
|
+
this.currentEvent.type = parsed.value;
|
|
191
216
|
break;
|
|
192
217
|
case "data":
|
|
193
|
-
this.currentEvent.data +=
|
|
218
|
+
this.currentEvent.data += parsed.value + "\n";
|
|
194
219
|
break;
|
|
195
220
|
case "id":
|
|
196
|
-
if (!
|
|
221
|
+
if (!parsed.value.includes("\0")) this.lastEventId = parsed.value;
|
|
197
222
|
break;
|
|
198
223
|
case "retry":
|
|
199
|
-
if (/^\d+$/.test(
|
|
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(
|
|
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
|
-
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
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,
|
|
215
|
-
value: this.
|
|
239
|
+
field: line.slice(0, colon),
|
|
240
|
+
value: this.extractValue(line, colon + 1)
|
|
216
241
|
};
|
|
217
242
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
277
|
-
this._onConnect = this.
|
|
278
|
-
this._onDisconnect = this.
|
|
279
|
-
this._onMessage = this.
|
|
280
|
-
this._onComment = this.
|
|
281
|
-
this._shouldReconnect = this.
|
|
282
|
-
this._calculateReconnectDelay = this.
|
|
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.
|
|
297
|
+
this._onError = this.resolveCallback(value, () => {}, "onError");
|
|
286
298
|
}
|
|
287
299
|
set onMessage(value) {
|
|
288
|
-
this._onMessage = this.
|
|
300
|
+
this._onMessage = this.resolveCallback(value, () => {}, "onMessage");
|
|
289
301
|
}
|
|
290
302
|
set onComment(value) {
|
|
291
|
-
this._onComment = this.
|
|
303
|
+
this._onComment = this.resolveCallback(value, () => {}, "onComment");
|
|
292
304
|
}
|
|
293
305
|
set onConnect(value) {
|
|
294
|
-
this._onConnect = this.
|
|
306
|
+
this._onConnect = this.resolveCallback(value, () => {}, "onConnect");
|
|
295
307
|
}
|
|
296
308
|
set onDisconnect(value) {
|
|
297
|
-
this._onDisconnect = this.
|
|
309
|
+
this._onDisconnect = this.resolveCallback(value, () => {}, "onDisconnect");
|
|
298
310
|
}
|
|
299
311
|
set shouldReconnect(value) {
|
|
300
|
-
this._shouldReconnect = this.
|
|
312
|
+
this._shouldReconnect = this.resolveCallback(value, this.DEFAULT_SHOULD_RECONNECT, "shouldReconnect");
|
|
301
313
|
}
|
|
302
314
|
set calculateReconnectDelay(value) {
|
|
303
|
-
this._calculateReconnectDelay = this.
|
|
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.
|
|
335
|
+
headers: this.buildRequestHeaders(),
|
|
319
336
|
signal: this.abortController.signal,
|
|
320
337
|
cache: "no-store"
|
|
321
|
-
}).then(this.handleFetchResponse
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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/
|
|
693
|
-
var
|
|
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, "
|
|
767
|
+
_defineProperty(this, "lastUpdateTimestamp", void 0);
|
|
700
768
|
this.options = options;
|
|
701
769
|
this.logger = options.logger;
|
|
702
|
-
this.url = options.resolveUrl("client/
|
|
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("[
|
|
707
|
-
return
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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, "
|
|
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.
|
|
787
|
-
const transportConstructor = this.
|
|
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
|
|
1247
|
-
*
|
|
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
|
|
1251
|
-
|
|
1346
|
+
if (typeof TextEncoder === "function") return new TextEncoder().encode(str);
|
|
1347
|
+
return encodeManually(str);
|
|
1348
|
+
};
|
|
1349
|
+
const encodeManually = (str) => {
|
|
1252
1350
|
const bytes = [];
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
|
1266
|
-
* streaming SSE responses. React Native's built-in fetch
|
|
1267
|
-
* body before resolving,
|
|
1268
|
-
* XHR's
|
|
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(
|
|
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
|
|
1278
|
-
if (
|
|
1279
|
-
let
|
|
1280
|
-
let
|
|
1281
|
-
let
|
|
1282
|
-
const
|
|
1283
|
-
|
|
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
|
|
1287
|
-
|
|
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:
|
|
1409
|
+
headers: parseHeaders(xhr.getAllResponseHeaders()),
|
|
1410
|
+
body: responseBody
|
|
1297
1411
|
});
|
|
1298
1412
|
}
|
|
1299
1413
|
};
|
|
1300
1414
|
xhr.onprogress = () => {
|
|
1301
|
-
|
|
1302
|
-
lastLength = xhr.responseText.length;
|
|
1303
|
-
if (newText) controller.enqueue(encodeUtf8(newText));
|
|
1415
|
+
flushNewBytes();
|
|
1304
1416
|
};
|
|
1305
1417
|
xhr.onload = () => {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
controller.close();
|
|
1418
|
+
flushNewBytes();
|
|
1419
|
+
streamController.close();
|
|
1309
1420
|
};
|
|
1310
1421
|
xhr.onerror = () => {
|
|
1311
|
-
const
|
|
1312
|
-
if (!
|
|
1313
|
-
else
|
|
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 (!
|
|
1319
|
-
|
|
1320
|
-
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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": "
|
|
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": ">=
|
|
81
|
+
"expo": ">=52",
|
|
82
82
|
"react": "*",
|
|
83
|
-
"react-native": ">=0.
|
|
83
|
+
"react-native": ">=0.75"
|
|
84
84
|
},
|
|
85
85
|
"peerDependenciesMeta": {
|
|
86
86
|
"expo": {
|