@configdirector/react-native-sdk 1.6.0 → 1.7.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 +14 -0
- package/dist/configdirector-react-native-testing.d.mts +3 -2
- package/dist/configdirector-react-native-testing.mjs +3 -2
- package/dist/configdirector-react-native.d.mts +1 -1
- package/dist/configdirector-react-native.mjs +105 -4
- package/dist/{installedTestClient-CxGEAMOE.mjs → installedTestClient-LWv5ji8J.mjs} +144 -37
- package/dist/{types-fcPMBgiX.d.mts → types-P1hoU2Ru.d.mts} +34 -13
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -10,6 +10,16 @@ React Native SDK for [ConfigDirector](https://www.configdirector.com), remote co
|
|
|
10
10
|
npm install --save @configdirector/react-native-sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Install `@react-native-async-storage/async-storage` as well, so the SDK can keep the same client
|
|
14
|
+
session for six hours across app starts. It is an optional peer dependency and works in Expo Go.
|
|
15
|
+
Without it, every app start begins a new client session and the SDK logs a warning.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save @react-native-async-storage/async-storage
|
|
19
|
+
# or in an Expo project
|
|
20
|
+
npx expo install @react-native-async-storage/async-storage
|
|
21
|
+
```
|
|
22
|
+
|
|
13
23
|
## Retrieve a value
|
|
14
24
|
|
|
15
25
|
```tsx
|
|
@@ -74,6 +84,10 @@ package's files are `.mjs` files, which the `jest-expo` and `@react-native/jest-
|
|
|
74
84
|
transform, so a Jest setup must let `transformIgnorePatterns` transform both packages and add a
|
|
75
85
|
`transform` entry for `.mjs` files; the documentation shows the configuration for both presets.
|
|
76
86
|
`jest-expo` requires `@react-native/jest-preset` as a peer dependency, so install it in either case.
|
|
87
|
+
The test client never touches AsyncStorage. A test that builds a real client instead, through
|
|
88
|
+
`createClient` or a provider given an `sdkKey`, logs the warning that AsyncStorage is missing and
|
|
89
|
+
starts a new client session under either preset; add `@babel/plugin-transform-dynamic-import` to the
|
|
90
|
+
Babel test environment for that client to use the AsyncStorage Jest mock instead.
|
|
77
91
|
|
|
78
92
|
## Documentation
|
|
79
93
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as ConfigDirectorContext, n as ConfigDirectorClient, o as ConfigDirectorLogger } from "./types-
|
|
1
|
+
import { i as ConfigDirectorContext, n as ConfigDirectorClient, o as ConfigDirectorLogger } from "./types-P1hoU2Ru.mjs";
|
|
2
2
|
|
|
3
3
|
//#region ../shared/src/errors.d.ts
|
|
4
4
|
declare class ConfigDirectorConnectionError extends Error {
|
|
@@ -102,7 +102,8 @@ interface TestClient {
|
|
|
102
102
|
failContextUpdate(): void;
|
|
103
103
|
/**
|
|
104
104
|
* The context of every `initialize` and `updateContext` call, in call order. `initialize` without
|
|
105
|
-
* a context records an empty context
|
|
105
|
+
* a context records an empty context, and so does a call whose context has an invalid kind, since
|
|
106
|
+
* the client connects without it. Resumes are not recorded.
|
|
106
107
|
*/
|
|
107
108
|
readonly contextUpdates: ConfigDirectorContext[];
|
|
108
109
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as ConfigDirectorConnectionError, d as _defineProperty, l as ConfigDirectorValidationError, o as createDefaultLogger, r as DefaultConfigDirectorClient, t as INSTALLED_TEST_CLIENT_SLOT, u as Emitter } from "./installedTestClient-
|
|
1
|
+
import { c as ConfigDirectorConnectionError, d as _defineProperty, l as ConfigDirectorValidationError, o as createDefaultLogger, r as DefaultConfigDirectorClient, t as INSTALLED_TEST_CLIENT_SLOT, u as Emitter } from "./installedTestClient-LWv5ji8J.mjs";
|
|
2
2
|
//#region ../shared/src/testing/values.ts
|
|
3
3
|
const encodeTestValue$1 = (key, value) => {
|
|
4
4
|
if (key.trim().length == 0) throw new ConfigDirectorValidationError("Invalid config key. The key of a test value cannot be empty.");
|
|
@@ -288,6 +288,7 @@ const TEST_CLIENT_SDK_KEY = "test-client";
|
|
|
288
288
|
const noopTelemetryClient = {
|
|
289
289
|
updateContext: async () => {},
|
|
290
290
|
evaluatedConfig: () => {},
|
|
291
|
+
recordInvalidContext: () => {},
|
|
291
292
|
close: async () => {}
|
|
292
293
|
};
|
|
293
294
|
var InMemoryTestClient = class {
|
|
@@ -391,7 +392,7 @@ const installTestClient = (testClient) => {
|
|
|
391
392
|
const createTestClient = (options) => {
|
|
392
393
|
return createTestClient$1({
|
|
393
394
|
sdkName: "react-native-sdk",
|
|
394
|
-
sdkVersion: "1.
|
|
395
|
+
sdkVersion: "1.7.0"
|
|
395
396
|
}, options);
|
|
396
397
|
};
|
|
397
398
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ConfigDirectorLogMessageDecorator, c as ConfigValueType, i as ConfigDirectorContext, l as ConnectionMode, n as ConfigDirectorClient, o as ConfigDirectorLogger, r as ConfigDirectorClientOptions, s as ConfigDirectorLoggingLevel, t as ClientHooks } from "./types-
|
|
1
|
+
import { a as ConfigDirectorLogMessageDecorator, c as ConfigValueType, i as ConfigDirectorContext, l as ConnectionMode, n as ConfigDirectorClient, o as ConfigDirectorLogger, r as ConfigDirectorClientOptions, s as ConfigDirectorLoggingLevel, t as ClientHooks } from "./types-P1hoU2Ru.mjs";
|
|
2
2
|
import { Component, PropsWithChildren } from "react";
|
|
3
3
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as CLIENT_BASE_URL, d as _defineProperty, i as fetchWithTimeout, n as readInstalledTestClient, o as createDefaultLogger, r as DefaultConfigDirectorClient, s as isFetchErrorFatal } from "./installedTestClient-
|
|
1
|
+
import { a as CLIENT_BASE_URL, d as _defineProperty, i as fetchWithTimeout, n as readInstalledTestClient, o as createDefaultLogger, r as DefaultConfigDirectorClient, s as isFetchErrorFatal } from "./installedTestClient-LWv5ji8J.mjs";
|
|
2
2
|
import React, { Component, useContext as useContext$1 } from "react";
|
|
3
3
|
import { AppState, NativeModules } from "react-native";
|
|
4
4
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -285,6 +285,63 @@ var EvaluatedConfigEventPreprocessor = class {
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
};
|
|
288
|
+
var InvalidContextSubmissions = class {
|
|
289
|
+
constructor() {
|
|
290
|
+
_defineProperty(this, "countsByKindSample", /* @__PURE__ */ new Map());
|
|
291
|
+
_defineProperty(this, "countBeyondSampleLimit", 0);
|
|
292
|
+
_defineProperty(this, "startTime", void 0);
|
|
293
|
+
}
|
|
294
|
+
record(kindSample) {
|
|
295
|
+
var _this$startTime;
|
|
296
|
+
(_this$startTime = this.startTime) !== null && _this$startTime !== void 0 || (this.startTime = /* @__PURE__ */ new Date());
|
|
297
|
+
return this.add(kindSample, 1);
|
|
298
|
+
}
|
|
299
|
+
absorb(entries) {
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (this.startTime === void 0 || entry.startTime < this.startTime) this.startTime = entry.startTime;
|
|
302
|
+
this.add(entry.event.kindSample, entry.count);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
add(kindSample, count) {
|
|
306
|
+
if (kindSample !== void 0) {
|
|
307
|
+
const knownCount = this.countsByKindSample.get(kindSample);
|
|
308
|
+
if (knownCount !== void 0) {
|
|
309
|
+
this.countsByKindSample.set(kindSample, knownCount + count);
|
|
310
|
+
return "known-kind-sample";
|
|
311
|
+
}
|
|
312
|
+
if (this.countsByKindSample.size < 20) {
|
|
313
|
+
this.countsByKindSample.set(kindSample, count);
|
|
314
|
+
return "new-kind-sample";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const firstBeyondSampleLimit = this.countBeyondSampleLimit === 0;
|
|
318
|
+
this.countBeyondSampleLimit += count;
|
|
319
|
+
return firstBeyondSampleLimit ? "first-beyond-sample-limit" : "beyond-sample-limit";
|
|
320
|
+
}
|
|
321
|
+
takeSnapshot() {
|
|
322
|
+
var _this$startTime2;
|
|
323
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
324
|
+
const startTime = (_this$startTime2 = this.startTime) !== null && _this$startTime2 !== void 0 ? _this$startTime2 : endTime;
|
|
325
|
+
const entryOf = (count, kindSample) => ({
|
|
326
|
+
startTime,
|
|
327
|
+
endTime,
|
|
328
|
+
count,
|
|
329
|
+
event: {
|
|
330
|
+
reason: "invalid-context-kind",
|
|
331
|
+
kindSample
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const sampledEntries = Array.from(this.countsByKindSample, ([kindSample, count]) => entryOf(count, kindSample));
|
|
335
|
+
const entries = this.countBeyondSampleLimit > 0 ? [...sampledEntries, entryOf(this.countBeyondSampleLimit)] : sampledEntries;
|
|
336
|
+
this.clear();
|
|
337
|
+
return entries;
|
|
338
|
+
}
|
|
339
|
+
clear() {
|
|
340
|
+
this.countsByKindSample.clear();
|
|
341
|
+
this.countBeyondSampleLimit = 0;
|
|
342
|
+
this.startTime = void 0;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
288
345
|
//#endregion
|
|
289
346
|
//#region ../js-client-core/src/telemetry/ClientTelemetryEventCollector.ts
|
|
290
347
|
var ClientTelemetryEventCollector = class extends TelemetryEventCollector {
|
|
@@ -293,6 +350,7 @@ var ClientTelemetryEventCollector = class extends TelemetryEventCollector {
|
|
|
293
350
|
_defineProperty(this, "evaluationEventSnapshotPreprocessor", void 0);
|
|
294
351
|
_defineProperty(this, "reporter", void 0);
|
|
295
352
|
_defineProperty(this, "_context", void 0);
|
|
353
|
+
_defineProperty(this, "invalidContextSubmissions", new InvalidContextSubmissions());
|
|
296
354
|
this.reporter = new ClientEventReporter(options);
|
|
297
355
|
this.evaluationEventSnapshotPreprocessor = new EvaluatedConfigEventPreprocessor(options.valueIdGenerator);
|
|
298
356
|
}
|
|
@@ -306,6 +364,12 @@ var ClientTelemetryEventCollector = class extends TelemetryEventCollector {
|
|
|
306
364
|
if (!this.collectEvents) return;
|
|
307
365
|
this.evaluationEventQueue.push(event);
|
|
308
366
|
}
|
|
367
|
+
recordInvalidContext(kindSample) {
|
|
368
|
+
this.invalidContextSubmissions.record(kindSample);
|
|
369
|
+
}
|
|
370
|
+
absorbInvalidContextSubmissions(entries) {
|
|
371
|
+
this.invalidContextSubmissions.absorb(entries);
|
|
372
|
+
}
|
|
309
373
|
async forceFlush() {
|
|
310
374
|
this.cancelScheduledFlush();
|
|
311
375
|
await this.flushAndScheduleNext();
|
|
@@ -320,7 +384,10 @@ var ClientTelemetryEventCollector = class extends TelemetryEventCollector {
|
|
|
320
384
|
return await ((_this$reporter = this.reporter) === null || _this$reporter === void 0 ? void 0 : _this$reporter.report({
|
|
321
385
|
context: this._context,
|
|
322
386
|
discreteEvents: {},
|
|
323
|
-
aggregatedEvents: {
|
|
387
|
+
aggregatedEvents: {
|
|
388
|
+
evaluatedConfig: this.aggregator.aggregate(evaluationSnapshot),
|
|
389
|
+
invalidContext: this.invalidContextSubmissions.takeSnapshot()
|
|
390
|
+
},
|
|
324
391
|
droppedEvents: { evaluatedConfig: evaluationSnapshot.droppedCount }
|
|
325
392
|
}));
|
|
326
393
|
}
|
|
@@ -453,6 +520,36 @@ const resolveMetadata = (provided, logger) => {
|
|
|
453
520
|
return metadata;
|
|
454
521
|
};
|
|
455
522
|
//#endregion
|
|
523
|
+
//#region src/instanceIdStorage.ts
|
|
524
|
+
const lookUpAsyncStorage = async () => {
|
|
525
|
+
try {
|
|
526
|
+
const { default: storage } = await import("@react-native-async-storage/async-storage");
|
|
527
|
+
return {
|
|
528
|
+
outcome: "installed",
|
|
529
|
+
storage
|
|
530
|
+
};
|
|
531
|
+
} catch {
|
|
532
|
+
return { outcome: "not installed" };
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
const asyncStorageInstanceIdStorage = (logger) => {
|
|
536
|
+
const asyncStorageLookup = lookUpAsyncStorage().then((lookup) => {
|
|
537
|
+
if (lookup.outcome === "not installed") logger.warn("The ConfigDirector SDK could not find @react-native-async-storage/async-storage, so every app start counts as a new client session. Install it for the SDK to keep the client session for six hours across app starts.");
|
|
538
|
+
return lookup;
|
|
539
|
+
});
|
|
540
|
+
const installedAsyncStorage = async () => {
|
|
541
|
+
const lookup = await asyncStorageLookup;
|
|
542
|
+
switch (lookup.outcome) {
|
|
543
|
+
case "installed": return lookup.storage;
|
|
544
|
+
case "not installed": throw new Error("@react-native-async-storage/async-storage is not installed");
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
getItem: async (key) => (await installedAsyncStorage()).getItem(key),
|
|
549
|
+
setItem: async (key, value) => (await installedAsyncStorage()).setItem(key, value)
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
//#endregion
|
|
456
553
|
//#region src/utf8Encoder.ts
|
|
457
554
|
/**
|
|
458
555
|
* Encodes a string to a UTF-8 byte sequence.
|
|
@@ -607,6 +704,9 @@ var ReactNativeTelemetryClient = class {
|
|
|
607
704
|
evaluatedConfig(event) {
|
|
608
705
|
this.collector.evaluatedConfig(this.sanitizeEvaluatedConfigEvent(event));
|
|
609
706
|
}
|
|
707
|
+
recordInvalidContext(kindSample) {
|
|
708
|
+
this.collector.recordInvalidContext(kindSample);
|
|
709
|
+
}
|
|
610
710
|
async close() {
|
|
611
711
|
await this.collector.close();
|
|
612
712
|
}
|
|
@@ -646,7 +746,7 @@ const createClient = (clientSdkKey, clientOptions) => {
|
|
|
646
746
|
var _clientOptions$logger, _clientOptions$connec;
|
|
647
747
|
const sdkIdentity = {
|
|
648
748
|
sdkName: "react-native-sdk",
|
|
649
|
-
sdkVersion: "1.
|
|
749
|
+
sdkVersion: "1.7.0"
|
|
650
750
|
};
|
|
651
751
|
const logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createConsoleLogger("warn");
|
|
652
752
|
const metadata = resolveMetadata(clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.metadata, logger);
|
|
@@ -663,7 +763,8 @@ const createClient = (clientSdkKey, clientOptions) => {
|
|
|
663
763
|
metadata
|
|
664
764
|
}, {
|
|
665
765
|
fetch: reactNativeStreamingFetch,
|
|
666
|
-
urlFactory
|
|
766
|
+
urlFactory,
|
|
767
|
+
instanceIdStorage: asyncStorageInstanceIdStorage(logger)
|
|
667
768
|
});
|
|
668
769
|
};
|
|
669
770
|
//#endregion
|
|
@@ -438,11 +438,15 @@ var StreamingTransport = class {
|
|
|
438
438
|
_defineProperty(this, "eventEmitter", new Emitter());
|
|
439
439
|
_defineProperty(this, "url", void 0);
|
|
440
440
|
_defineProperty(this, "timeoutTimer", void 0);
|
|
441
|
+
_defineProperty(this, "connectionGeneration", 0);
|
|
441
442
|
this.options = options;
|
|
442
443
|
this.logger = options.logger;
|
|
443
444
|
this.url = options.resolveUrl("client/sse/v1", options.baseUrl);
|
|
444
445
|
}
|
|
445
446
|
async connect(context, timeout) {
|
|
447
|
+
const generation = ++this.connectionGeneration;
|
|
448
|
+
const clientSession = await this.options.clientSession;
|
|
449
|
+
if (generation !== this.connectionGeneration) return this;
|
|
446
450
|
if (this.eventSource) this.close();
|
|
447
451
|
const eventSourcePromise = new Promise((resolve, reject) => {
|
|
448
452
|
this.eventSource = createEventSourceClient({
|
|
@@ -454,7 +458,8 @@ var StreamingTransport = class {
|
|
|
454
458
|
givenContext: context,
|
|
455
459
|
metaContext: this.options.metaContext,
|
|
456
460
|
clientSdkKey: this.options.clientSdkKey,
|
|
457
|
-
instanceId:
|
|
461
|
+
instanceId: clientSession.instanceId,
|
|
462
|
+
ephemeral: clientSession.ephemeral
|
|
458
463
|
}),
|
|
459
464
|
onMessage: ({ data }) => {
|
|
460
465
|
this.dispatchMessage(data);
|
|
@@ -520,6 +525,7 @@ var StreamingTransport = class {
|
|
|
520
525
|
}
|
|
521
526
|
close() {
|
|
522
527
|
var _this$eventSource;
|
|
528
|
+
this.connectionGeneration++;
|
|
523
529
|
(_this$eventSource = this.eventSource) === null || _this$eventSource === void 0 || _this$eventSource.close();
|
|
524
530
|
clearTimeout(this.timeoutTimer);
|
|
525
531
|
}
|
|
@@ -783,10 +789,11 @@ const generateInstanceId = () => {
|
|
|
783
789
|
});
|
|
784
790
|
};
|
|
785
791
|
//#endregion
|
|
786
|
-
//#region ../js-client-core/src/
|
|
792
|
+
//#region ../js-client-core/src/client-session.ts
|
|
787
793
|
const STORAGE_KEY = "configdirector-sdk:instance-id";
|
|
788
794
|
const RECORD_VERSION = 1;
|
|
789
795
|
const MAX_AGE_MS = 360 * 60 * 1e3;
|
|
796
|
+
const STORAGE_DEADLINE_MS = 1e3;
|
|
790
797
|
const CHECKSUM_SALT = "configdirector.instanceId.v1";
|
|
791
798
|
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
792
799
|
const fnv1a = (input, offsetBasis) => {
|
|
@@ -802,7 +809,7 @@ const checksumFor = (id, createdAt) => {
|
|
|
802
809
|
const low = fnv1a(payload, 2166136261);
|
|
803
810
|
return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
|
|
804
811
|
};
|
|
805
|
-
const
|
|
812
|
+
const localStorageIfAvailable = () => {
|
|
806
813
|
try {
|
|
807
814
|
var _localStorage;
|
|
808
815
|
return (_localStorage = globalThis.localStorage) !== null && _localStorage !== void 0 ? _localStorage : void 0;
|
|
@@ -818,34 +825,37 @@ const isUsableRecord = (record) => {
|
|
|
818
825
|
if (!UUID_V4_PATTERN.test(id)) return false;
|
|
819
826
|
return checksum === checksumFor(id, createdAt);
|
|
820
827
|
};
|
|
821
|
-
const readStoredInstanceId = (storage, logger) => {
|
|
828
|
+
const readStoredInstanceId = async (storage, logger) => {
|
|
822
829
|
let raw;
|
|
823
830
|
try {
|
|
824
|
-
raw = storage.getItem(STORAGE_KEY);
|
|
831
|
+
raw = await storage.getItem(STORAGE_KEY);
|
|
825
832
|
} catch (error) {
|
|
826
|
-
logger
|
|
827
|
-
return;
|
|
833
|
+
logger.debug("[ConfigDirectorClient] Could not read the stored instance id, using an ephemeral one", error);
|
|
834
|
+
return { outcome: "failed" };
|
|
828
835
|
}
|
|
829
|
-
if (
|
|
836
|
+
if (typeof raw !== "string" || raw === "") return { outcome: "nothing usable" };
|
|
830
837
|
let record;
|
|
831
838
|
try {
|
|
832
839
|
record = JSON.parse(raw);
|
|
833
840
|
} catch {
|
|
834
|
-
logger
|
|
835
|
-
return;
|
|
841
|
+
logger.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
|
|
842
|
+
return { outcome: "nothing usable" };
|
|
836
843
|
}
|
|
837
844
|
if (!isUsableRecord(record)) {
|
|
838
|
-
logger
|
|
839
|
-
return;
|
|
845
|
+
logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
|
|
846
|
+
return { outcome: "nothing usable" };
|
|
840
847
|
}
|
|
841
848
|
const age = Date.now() - record.createdAt;
|
|
842
849
|
if (age < 0 || age >= MAX_AGE_MS) {
|
|
843
|
-
logger
|
|
844
|
-
return;
|
|
850
|
+
logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
|
|
851
|
+
return { outcome: "nothing usable" };
|
|
845
852
|
}
|
|
846
|
-
return
|
|
853
|
+
return {
|
|
854
|
+
outcome: "stored",
|
|
855
|
+
instanceId: record.id
|
|
856
|
+
};
|
|
847
857
|
};
|
|
848
|
-
const
|
|
858
|
+
const persistInstanceId = async (storage, id, logger) => {
|
|
849
859
|
const createdAt = Date.now();
|
|
850
860
|
const record = {
|
|
851
861
|
v: RECORD_VERSION,
|
|
@@ -854,19 +864,54 @@ const storeInstanceId = (storage, id, logger) => {
|
|
|
854
864
|
checksum: checksumFor(id, createdAt)
|
|
855
865
|
};
|
|
856
866
|
try {
|
|
857
|
-
storage.setItem(STORAGE_KEY, JSON.stringify(record));
|
|
867
|
+
await storage.setItem(STORAGE_KEY, JSON.stringify(record));
|
|
868
|
+
return true;
|
|
858
869
|
} catch (error) {
|
|
859
|
-
logger
|
|
870
|
+
logger.debug("[ConfigDirectorClient] Could not persist the instance id, using it as an ephemeral one", error);
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const settleFromStorage = async (storage, provisionalId, deadline, logger) => {
|
|
875
|
+
const read = await readStoredInstanceId(storage, logger);
|
|
876
|
+
switch (read.outcome) {
|
|
877
|
+
case "stored": return { instanceId: read.instanceId };
|
|
878
|
+
case "failed": return {
|
|
879
|
+
instanceId: provisionalId,
|
|
880
|
+
ephemeral: true
|
|
881
|
+
};
|
|
882
|
+
case "nothing usable": break;
|
|
860
883
|
}
|
|
884
|
+
if (deadline.passed) return {
|
|
885
|
+
instanceId: provisionalId,
|
|
886
|
+
ephemeral: true
|
|
887
|
+
};
|
|
888
|
+
return await persistInstanceId(storage, provisionalId, logger) ? { instanceId: provisionalId } : {
|
|
889
|
+
instanceId: provisionalId,
|
|
890
|
+
ephemeral: true
|
|
891
|
+
};
|
|
861
892
|
};
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
if (!storage)
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
893
|
+
const resolveClientSession = (logger, storage = localStorageIfAvailable()) => {
|
|
894
|
+
const provisionalId = generateInstanceId();
|
|
895
|
+
if (!storage) {
|
|
896
|
+
logger.debug("[ConfigDirectorClient] No instance id storage is available, using an ephemeral instance id");
|
|
897
|
+
return Promise.resolve({
|
|
898
|
+
instanceId: provisionalId,
|
|
899
|
+
ephemeral: true
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const deadline = { passed: false };
|
|
903
|
+
let deadlineTimer;
|
|
904
|
+
const fallbackAtDeadline = new Promise((resolve) => {
|
|
905
|
+
deadlineTimer = setTimeout(() => {
|
|
906
|
+
deadline.passed = true;
|
|
907
|
+
logger.warn(`[ConfigDirectorClient] The instance id storage did not respond within ${STORAGE_DEADLINE_MS}ms, using an ephemeral instance id`);
|
|
908
|
+
resolve({
|
|
909
|
+
instanceId: provisionalId,
|
|
910
|
+
ephemeral: true
|
|
911
|
+
});
|
|
912
|
+
}, STORAGE_DEADLINE_MS);
|
|
913
|
+
});
|
|
914
|
+
return Promise.race([settleFromStorage(storage, provisionalId, deadline, logger), fallbackAtDeadline]).finally(() => clearTimeout(deadlineTimer));
|
|
870
915
|
};
|
|
871
916
|
//#endregion
|
|
872
917
|
//#region ../shared/src/transport/AbstractPollingTransport.ts
|
|
@@ -954,25 +999,33 @@ var PollingTransport = class extends AbstractPollingTransport {
|
|
|
954
999
|
_defineProperty(this, "eventEmitter", new Emitter());
|
|
955
1000
|
_defineProperty(this, "url", void 0);
|
|
956
1001
|
_defineProperty(this, "lastUpdateTimestamp", void 0);
|
|
1002
|
+
_defineProperty(this, "connectionGeneration", 0);
|
|
957
1003
|
this.options = options;
|
|
958
1004
|
this.logger = options.logger;
|
|
959
1005
|
this.url = options.resolveUrl("client/polling/v1", options.baseUrl);
|
|
960
1006
|
this.pollingIntervalSeconds = (_options$pollingInter = options.pollingInterval) !== null && _options$pollingInter !== void 0 ? _options$pollingInter : 60;
|
|
961
1007
|
}
|
|
962
1008
|
async connect(context, timeout) {
|
|
1009
|
+
const generation = ++this.connectionGeneration;
|
|
963
1010
|
this.clearPollingInterval();
|
|
1011
|
+
const clientSession = await this.options.clientSession;
|
|
1012
|
+
if (generation !== this.connectionGeneration) return this;
|
|
964
1013
|
try {
|
|
965
|
-
await this.fetchConfigs(context, timeout);
|
|
1014
|
+
await this.fetchConfigs(context, timeout, clientSession);
|
|
966
1015
|
} finally {
|
|
967
1016
|
if (!this.fatalError) this.schedulePollingInterval(() => {
|
|
968
|
-
this.fetchConfigs(context, timeout).catch((error) => {
|
|
1017
|
+
this.fetchConfigs(context, timeout, clientSession).catch((error) => {
|
|
969
1018
|
this.logger.error("[PollingTransport] Error during polling:", error);
|
|
970
1019
|
});
|
|
971
1020
|
});
|
|
972
1021
|
}
|
|
973
1022
|
return this;
|
|
974
1023
|
}
|
|
975
|
-
|
|
1024
|
+
close() {
|
|
1025
|
+
this.connectionGeneration++;
|
|
1026
|
+
super.close();
|
|
1027
|
+
}
|
|
1028
|
+
async fetchConfigs(context, timeout, clientSession) {
|
|
976
1029
|
if (this.fatalError) {
|
|
977
1030
|
this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
|
|
978
1031
|
return;
|
|
@@ -985,7 +1038,8 @@ var PollingTransport = class extends AbstractPollingTransport {
|
|
|
985
1038
|
givenContext: context,
|
|
986
1039
|
metaContext: this.options.metaContext,
|
|
987
1040
|
clientSdkKey: this.options.clientSdkKey,
|
|
988
|
-
instanceId:
|
|
1041
|
+
instanceId: clientSession.instanceId,
|
|
1042
|
+
ephemeral: clientSession.ephemeral,
|
|
989
1043
|
lastUpdateTimestamp: this.lastUpdateTimestamp
|
|
990
1044
|
})
|
|
991
1045
|
}, this.logger);
|
|
@@ -1032,6 +1086,46 @@ const readHost = () => read((globals) => {
|
|
|
1032
1086
|
return (_globals$location = globals.location) === null || _globals$location === void 0 ? void 0 : _globals$location.host;
|
|
1033
1087
|
});
|
|
1034
1088
|
//#endregion
|
|
1089
|
+
//#region ../shared/src/context-kind.ts
|
|
1090
|
+
const CONTEXT_KIND_MAXIMUM_LENGTH = 64;
|
|
1091
|
+
const KIND_SAMPLE_MAXIMUM_CHARACTERS = 128;
|
|
1092
|
+
const isContextKindCharacter = (character) => character >= "a" && character <= "z" || character >= "0" && character <= "9" || character === "-" || character === "_";
|
|
1093
|
+
const isValidContextKind = (kind) => {
|
|
1094
|
+
if (kind.length < 1 || kind.length > CONTEXT_KIND_MAXIMUM_LENGTH) return false;
|
|
1095
|
+
for (const character of kind) if (!isContextKindCharacter(character)) return false;
|
|
1096
|
+
return true;
|
|
1097
|
+
};
|
|
1098
|
+
const isAcceptedKind = (kind) => kind === void 0 || kind === null || typeof kind === "string" && isValidContextKind(kind);
|
|
1099
|
+
const stringFormOf = (kind) => {
|
|
1100
|
+
if (typeof kind === "string") return kind;
|
|
1101
|
+
try {
|
|
1102
|
+
return String(kind);
|
|
1103
|
+
} catch {
|
|
1104
|
+
return Object.prototype.toString.call(kind);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
const firstCharactersOf = (text, maximumCharacters) => {
|
|
1108
|
+
let sample = "";
|
|
1109
|
+
let characterCount = 0;
|
|
1110
|
+
for (const character of text) {
|
|
1111
|
+
if (characterCount === maximumCharacters) break;
|
|
1112
|
+
sample += character;
|
|
1113
|
+
characterCount += 1;
|
|
1114
|
+
}
|
|
1115
|
+
return sample;
|
|
1116
|
+
};
|
|
1117
|
+
const admitContext = (context) => {
|
|
1118
|
+
const kind = context === null || context === void 0 ? void 0 : context.kind;
|
|
1119
|
+
if (isAcceptedKind(kind)) return {
|
|
1120
|
+
outcome: "admitted",
|
|
1121
|
+
context
|
|
1122
|
+
};
|
|
1123
|
+
return {
|
|
1124
|
+
outcome: "ignored",
|
|
1125
|
+
kindSample: firstCharactersOf(stringFormOf(kind), KIND_SAMPLE_MAXIMUM_CHARACTERS)
|
|
1126
|
+
};
|
|
1127
|
+
};
|
|
1128
|
+
//#endregion
|
|
1035
1129
|
//#region ../js-client-core/src/DefaultConfigDirectorClient.ts
|
|
1036
1130
|
const MAX_EXPONENTIAL_DELAY = 9;
|
|
1037
1131
|
var DefaultConfigDirectorClient = class {
|
|
@@ -1052,9 +1146,9 @@ var DefaultConfigDirectorClient = class {
|
|
|
1052
1146
|
_defineProperty(this, "readyResolve", void 0);
|
|
1053
1147
|
_defineProperty(this, "currentContext", void 0);
|
|
1054
1148
|
_defineProperty(this, "connectionMode", void 0);
|
|
1055
|
-
_defineProperty(this, "
|
|
1149
|
+
_defineProperty(this, "clientSession", void 0);
|
|
1056
1150
|
this.logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createDefaultLogger();
|
|
1057
|
-
this.
|
|
1151
|
+
this.clientSession = resolveClientSession(this.logger, internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.instanceIdStorage);
|
|
1058
1152
|
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;
|
|
1059
1153
|
this.validateSdkKeyPresence(clientSdkKey);
|
|
1060
1154
|
const urlFactory = (_internalClientOption = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.urlFactory) !== null && _internalClientOption !== void 0 ? _internalClientOption : defaultUrlFactory;
|
|
@@ -1111,7 +1205,7 @@ var DefaultConfigDirectorClient = class {
|
|
|
1111
1205
|
buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions) {
|
|
1112
1206
|
var _internalClientOption3, _clientOptions$connec6;
|
|
1113
1207
|
return new (this.getTransportConstructor(this.connectionMode))({
|
|
1114
|
-
|
|
1208
|
+
clientSession: this.clientSession,
|
|
1115
1209
|
clientSdkKey,
|
|
1116
1210
|
baseUrl,
|
|
1117
1211
|
resolveUrl: urlFactory,
|
|
@@ -1138,10 +1232,23 @@ var DefaultConfigDirectorClient = class {
|
|
|
1138
1232
|
}
|
|
1139
1233
|
async initialize(context) {
|
|
1140
1234
|
if (!this.hasReceivedConfigSet) this.initializing = true;
|
|
1141
|
-
await this.
|
|
1235
|
+
await this.submitContext(context, "initialization");
|
|
1142
1236
|
}
|
|
1143
1237
|
async updateContext(context) {
|
|
1144
|
-
await this.
|
|
1238
|
+
await this.submitContext(context, "context update");
|
|
1239
|
+
}
|
|
1240
|
+
async submitContext(context, submission) {
|
|
1241
|
+
await this.connectToTransport(this.contextToConnectWith(context, submission), submission);
|
|
1242
|
+
}
|
|
1243
|
+
contextToConnectWith(suppliedContext, submission) {
|
|
1244
|
+
const admission = admitContext(suppliedContext);
|
|
1245
|
+
switch (admission.outcome) {
|
|
1246
|
+
case "admitted": return admission.context;
|
|
1247
|
+
case "ignored":
|
|
1248
|
+
this.logger.error(`[ConfigDirectorClient] Ignored the context supplied for ${submission} because its kind '${admission.kindSample}' is invalid; connecting as if no context was supplied. A context kind has 1 to 64 characters, using only lowercase letters, digits, '-' and '_'.`);
|
|
1249
|
+
this.telemetryClient.recordInvalidContext(admission.kindSample);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1145
1252
|
}
|
|
1146
1253
|
get context() {
|
|
1147
1254
|
return this.currentContext;
|
|
@@ -1161,7 +1268,7 @@ var DefaultConfigDirectorClient = class {
|
|
|
1161
1268
|
const startTime = (/* @__PURE__ */ new Date()).getTime();
|
|
1162
1269
|
await this.transport.connect(context !== null && context !== void 0 ? context : {}, this.timeout, caller);
|
|
1163
1270
|
this.currentContext = context;
|
|
1164
|
-
this.telemetryClient.updateContext(context);
|
|
1271
|
+
this.telemetryClient.updateContext(context).catch((error) => this.logger.warn("[ConfigDirectorClient] Telemetry could not update the context: ", error));
|
|
1165
1272
|
this.emit("contextUpdated", { context });
|
|
1166
1273
|
const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - startTime;
|
|
1167
1274
|
const remainingTimeout = this.timeout - elapsedTime;
|
|
@@ -1323,7 +1430,7 @@ var DefaultConfigDirectorClient = class {
|
|
|
1323
1430
|
this.logger.debug("[ConfigDirectorClient] close() has been called, closing connection to server");
|
|
1324
1431
|
clearTimeout(this.timeoutTimer);
|
|
1325
1432
|
(_this$readyResolve2 = this.readyResolve) === null || _this$readyResolve2 === void 0 || _this$readyResolve2.call(this, true);
|
|
1326
|
-
this.telemetryClient.close();
|
|
1433
|
+
this.telemetryClient.close().catch((error) => this.logger.warn("[ConfigDirectorClient] Telemetry could not close: ", error));
|
|
1327
1434
|
this.transport.close();
|
|
1328
1435
|
this.ready = false;
|
|
1329
1436
|
this.initializing = false;
|
|
@@ -33,27 +33,40 @@ type ConfigEvaluation = {
|
|
|
33
33
|
context?: ConfigDirectorContext;
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
36
|
+
* The context to be sent to ConfigDirector: the user or other entity, such as a device or an
|
|
37
|
+
* organization, that configs are evaluated for. This context will be used for targeting rules
|
|
38
|
+
* evaluation.
|
|
38
39
|
*/
|
|
39
40
|
type ConfigDirectorContext = {
|
|
40
41
|
/**
|
|
41
|
-
* The
|
|
42
|
-
*
|
|
42
|
+
* The context's identifier. This should be a value that uniquely identifies the user or other
|
|
43
|
+
* entity among contexts of the same {@link kind}.
|
|
43
44
|
* In the case of anonymous users, you could generate a UUID or alternatively not provide
|
|
44
45
|
* the {@link id} and the SDK will generate a random UUID. However, keep in mind that this
|
|
45
|
-
* value is used for segmenting
|
|
46
|
-
* could result in the
|
|
46
|
+
* value is used for segmenting contexts in percentage rollouts, and changes to the {@link id}
|
|
47
|
+
* could result in the context being assigned to a different percentile.
|
|
47
48
|
*/
|
|
48
49
|
id?: string;
|
|
49
50
|
/**
|
|
50
|
-
* The
|
|
51
|
+
* The kind of entity this context represents, such as `user`, `device`, `organization`,
|
|
52
|
+
* `tenant` or `listing`. You can use any other kind that fits your application. A kind has 1
|
|
53
|
+
* to 64 characters, using only lowercase letters (`a`-`z`), digits, `-` and `_`. It is used
|
|
54
|
+
* exactly as given, without trimming or changing its case.
|
|
55
|
+
*
|
|
56
|
+
* A context with an invalid kind is ignored as a whole: the SDK logs an error and evaluates
|
|
57
|
+
* as if no context had been provided.
|
|
58
|
+
*
|
|
59
|
+
* @default "user"
|
|
60
|
+
*/
|
|
61
|
+
kind?: string;
|
|
62
|
+
/**
|
|
63
|
+
* The context's display name. This will be shown in the ConfigDirector dashboard and may be
|
|
51
64
|
* used for targeting rules.
|
|
52
65
|
*/
|
|
53
66
|
name?: string;
|
|
54
67
|
/**
|
|
55
|
-
* Any arbitrary traits for the
|
|
56
|
-
*
|
|
68
|
+
* Any arbitrary traits for the context. They will be shown in the ConfigDirector dashboard
|
|
69
|
+
* and may be used for targeting rules.
|
|
57
70
|
*/
|
|
58
71
|
traits?: {
|
|
59
72
|
[key: string]: unknown;
|
|
@@ -202,17 +215,25 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
|
|
|
202
215
|
* not attempt to re-connect and an error will be logged to the console or the provided
|
|
203
216
|
* logger.
|
|
204
217
|
*
|
|
205
|
-
*
|
|
218
|
+
* A context whose {@link ConfigDirectorContext.kind} is invalid is ignored as a whole: an error is
|
|
219
|
+
* logged, and the client connects as if no context had been provided.
|
|
220
|
+
*
|
|
221
|
+
* @param context The current context to be used for evaluating targeting rules (optional).
|
|
206
222
|
*/
|
|
207
223
|
initialize(context?: ConfigDirectorContext): Promise<void>;
|
|
208
224
|
/**
|
|
209
|
-
* Updates the
|
|
225
|
+
* Updates the context and re-evaluates all config and flag values based on the new context.
|
|
226
|
+
*
|
|
227
|
+
* A context whose {@link ConfigDirectorContext.kind} is invalid is ignored as a whole: an error is
|
|
228
|
+
* logged, the previous context is no longer used, and the client connects as if no context had been
|
|
229
|
+
* provided.
|
|
210
230
|
*
|
|
211
|
-
* @param context The current
|
|
231
|
+
* @param context The current context to be used for evaluating targeting rules (required).
|
|
212
232
|
*/
|
|
213
233
|
updateContext(context: ConfigDirectorContext): Promise<void>;
|
|
214
234
|
/**
|
|
215
|
-
* Returns the current context the client is configured with, or undefined if there is no context
|
|
235
|
+
* Returns the current context the client is configured with, or undefined if there is no context,
|
|
236
|
+
* including after a context with an invalid kind was ignored.
|
|
216
237
|
*
|
|
217
238
|
* Note that the value returned does not updated immediately after calling {@link updateContext}. Only
|
|
218
239
|
* once the underlying connection was successful or timed out, the current context is updated. This is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@configdirector/react-native-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|
|
@@ -71,7 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@babel/core": "^7.29.0",
|
|
74
|
+
"@babel/plugin-transform-dynamic-import": "^7",
|
|
74
75
|
"@eslint/js": "^10.0.1",
|
|
76
|
+
"@react-native-async-storage/async-storage": "^3.1.1",
|
|
75
77
|
"@react-native/babel-preset": "^0.85.0",
|
|
76
78
|
"@react-native/jest-preset": "^0.86.2",
|
|
77
79
|
"@rollup/plugin-replace": "^6.0.3",
|
|
@@ -96,11 +98,15 @@
|
|
|
96
98
|
"wtfnode": "^0.10.1"
|
|
97
99
|
},
|
|
98
100
|
"peerDependencies": {
|
|
101
|
+
"@react-native-async-storage/async-storage": ">=1.0.0",
|
|
99
102
|
"expo": ">=52",
|
|
100
103
|
"react": "*",
|
|
101
104
|
"react-native": ">=0.75"
|
|
102
105
|
},
|
|
103
106
|
"peerDependenciesMeta": {
|
|
107
|
+
"@react-native-async-storage/async-storage": {
|
|
108
|
+
"optional": true
|
|
109
|
+
},
|
|
104
110
|
"expo": {
|
|
105
111
|
"optional": true
|
|
106
112
|
}
|
|
@@ -130,7 +136,7 @@
|
|
|
130
136
|
"^@eventsource/(.*)$": "<rootDir>/../eventsource/src/$1"
|
|
131
137
|
},
|
|
132
138
|
"transformIgnorePatterns": [
|
|
133
|
-
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|@noble)/)"
|
|
139
|
+
"node_modules/(?!((jest-)?react-native|@react-native(-community|-async-storage)?|@noble)/)"
|
|
134
140
|
]
|
|
135
141
|
}
|
|
136
142
|
}
|