@layers/amba-react-native 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +414 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +54 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +414 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1824,6 +1824,296 @@ function logListenerError(err) {
|
|
|
1824
1824
|
console.warn("[amba] onExperimentAssigned listener threw", err);
|
|
1825
1825
|
}
|
|
1826
1826
|
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region src/remote-values.ts
|
|
1829
|
+
/** Prefix of the per-project AsyncStorage record key. */
|
|
1830
|
+
const CACHE_KEY_PREFIX = "amba_rc_v1_";
|
|
1831
|
+
/** Slot for values evaluated without a signed-in user. */
|
|
1832
|
+
const ANON_SLOT = "anon";
|
|
1833
|
+
/** Most user slots one project record keeps. */
|
|
1834
|
+
const MAX_SLOTS = 5;
|
|
1835
|
+
const TWO_32 = 4294967296;
|
|
1836
|
+
/**
|
|
1837
|
+
* 64-bit FNV-1a over the string's UTF-16 code units (low byte, then high
|
|
1838
|
+
* byte), as 16 hex digits. Computed in two 32-bit halves: some React Native
|
|
1839
|
+
* engines (JavaScriptCore on older Android builds) have no BigInt.
|
|
1840
|
+
*/
|
|
1841
|
+
function fnv1a64Hex(text) {
|
|
1842
|
+
let hi = 3421674724;
|
|
1843
|
+
let lo = 2216829733;
|
|
1844
|
+
const step = (byte) => {
|
|
1845
|
+
lo = (lo ^ byte) >>> 0;
|
|
1846
|
+
const low = lo * 435;
|
|
1847
|
+
const carry = Math.floor(low / TWO_32);
|
|
1848
|
+
const high = hi * 435 + carry + (lo << 8 >>> 0);
|
|
1849
|
+
lo = low % TWO_32;
|
|
1850
|
+
hi = high % TWO_32;
|
|
1851
|
+
};
|
|
1852
|
+
for (let i = 0; i < text.length; i++) {
|
|
1853
|
+
const unit = text.charCodeAt(i);
|
|
1854
|
+
step(unit & 255);
|
|
1855
|
+
step(unit >>> 8);
|
|
1856
|
+
}
|
|
1857
|
+
return hi.toString(16).padStart(8, "0") + lo.toString(16).padStart(8, "0");
|
|
1858
|
+
}
|
|
1859
|
+
/** The record key for a project: a hash of the base URL and client key. */
|
|
1860
|
+
function recordKey(baseUrl, clientKey) {
|
|
1861
|
+
return `${CACHE_KEY_PREFIX}${fnv1a64Hex(`${baseUrl.replace(/\/+$/, "")}\n${clientKey}`)}`;
|
|
1862
|
+
}
|
|
1863
|
+
/** The slot key for an identity: `u` + a hash of the user id, or `anon`. */
|
|
1864
|
+
function slotKey(appUserId) {
|
|
1865
|
+
return appUserId ? `u${fnv1a64Hex(appUserId)}` : ANON_SLOT;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* The user a config response was evaluated for, from its `ETag`:
|
|
1869
|
+
* `"<version>:<app user id>:<segments>"` for a signed-in caller, `"<version>"`
|
|
1870
|
+
* for every other one (no session, or an expired or revoked token, which the
|
|
1871
|
+
* server answers with each key's default value). `undefined` without an
|
|
1872
|
+
* `ETag`; `null` for an evaluation for nobody.
|
|
1873
|
+
*/
|
|
1874
|
+
function etagEvaluatedFor(etag) {
|
|
1875
|
+
if (!etag) return void 0;
|
|
1876
|
+
const parts = etag.trim().replace(/^W\//, "").replace(/^"|"$/g, "").split(":");
|
|
1877
|
+
return parts.length > 1 && parts[1] ? parts[1] : null;
|
|
1878
|
+
}
|
|
1879
|
+
function isPlainObject(value) {
|
|
1880
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1881
|
+
}
|
|
1882
|
+
function validSlot(raw) {
|
|
1883
|
+
if (!isPlainObject(raw)) return null;
|
|
1884
|
+
const slot = { saved_at: typeof raw["saved_at"] === "number" ? raw["saved_at"] : 0 };
|
|
1885
|
+
const config = raw["config"];
|
|
1886
|
+
if (isPlainObject(config) && isPlainObject(config["values"])) slot.config = {
|
|
1887
|
+
version: typeof config["version"] === "string" ? config["version"] : null,
|
|
1888
|
+
values: config["values"]
|
|
1889
|
+
};
|
|
1890
|
+
const flags = raw["flags"];
|
|
1891
|
+
if (Array.isArray(flags)) slot.flags = flags.filter((f) => isPlainObject(f) && typeof f["name"] === "string" && typeof f["enabled"] === "boolean");
|
|
1892
|
+
return slot.config || slot.flags ? slot : null;
|
|
1893
|
+
}
|
|
1894
|
+
function upsert(list, flag) {
|
|
1895
|
+
const i = list.findIndex((f) => f.name === flag.name);
|
|
1896
|
+
if (i >= 0) list[i] = flag;
|
|
1897
|
+
else list.push(flag);
|
|
1898
|
+
}
|
|
1899
|
+
var RemoteValuesCache = class {
|
|
1900
|
+
record = {
|
|
1901
|
+
v: 1,
|
|
1902
|
+
slots: {}
|
|
1903
|
+
};
|
|
1904
|
+
/** The slot key the held values were resolved for. */
|
|
1905
|
+
viewFor = null;
|
|
1906
|
+
config = null;
|
|
1907
|
+
flags = null;
|
|
1908
|
+
/** Bumped by every sign-out; an older ticket's response is not cached. */
|
|
1909
|
+
generation = 0;
|
|
1910
|
+
writes = Promise.resolve();
|
|
1911
|
+
constructor(store, key, defaults, currentUser) {
|
|
1912
|
+
this.store = store;
|
|
1913
|
+
this.key = key;
|
|
1914
|
+
this.defaults = defaults;
|
|
1915
|
+
this.currentUser = currentUser;
|
|
1916
|
+
}
|
|
1917
|
+
/** Read the project's record from storage. `configure()` awaits it. */
|
|
1918
|
+
async load() {
|
|
1919
|
+
let raw = null;
|
|
1920
|
+
try {
|
|
1921
|
+
raw = await this.store.read(this.key);
|
|
1922
|
+
} catch {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (!raw) return;
|
|
1926
|
+
try {
|
|
1927
|
+
const parsed = JSON.parse(raw);
|
|
1928
|
+
const slots = {};
|
|
1929
|
+
if (isPlainObject(parsed) && isPlainObject(parsed["slots"])) for (const [k, v] of Object.entries(parsed["slots"])) {
|
|
1930
|
+
const slot = validSlot(v);
|
|
1931
|
+
if (slot) slots[k] = slot;
|
|
1932
|
+
}
|
|
1933
|
+
this.record = {
|
|
1934
|
+
v: 1,
|
|
1935
|
+
slots
|
|
1936
|
+
};
|
|
1937
|
+
} catch {
|
|
1938
|
+
this.record = {
|
|
1939
|
+
v: 1,
|
|
1940
|
+
slots: {}
|
|
1941
|
+
};
|
|
1942
|
+
this.persist();
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
/** Take a ticket for a request about to be sent for the current user. */
|
|
1946
|
+
ticket() {
|
|
1947
|
+
return {
|
|
1948
|
+
generation: this.generation,
|
|
1949
|
+
requestedFor: this.currentUser()
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
/** The value for config `key` (fetched, cached, then default); `undefined` when none. */
|
|
1953
|
+
configValue(key) {
|
|
1954
|
+
this.ensureView();
|
|
1955
|
+
const values = this.config?.values;
|
|
1956
|
+
if (values && Object.prototype.hasOwnProperty.call(values, key)) {
|
|
1957
|
+
const v = values[key];
|
|
1958
|
+
if (v !== null && v !== void 0) return v;
|
|
1959
|
+
}
|
|
1960
|
+
return this.defaultFor(key);
|
|
1961
|
+
}
|
|
1962
|
+
/** The held assignment for flag `key`, or `null`. */
|
|
1963
|
+
flag(key) {
|
|
1964
|
+
this.ensureView();
|
|
1965
|
+
return this.flags?.find((f) => f.name === key) ?? null;
|
|
1966
|
+
}
|
|
1967
|
+
/** Whether flag `key` is on: the held assignment, then a boolean default. */
|
|
1968
|
+
flagEnabled(key) {
|
|
1969
|
+
const flag = this.flag(key);
|
|
1970
|
+
if (flag) return flag.enabled;
|
|
1971
|
+
const d = this.defaultFor(key);
|
|
1972
|
+
return typeof d === "boolean" ? d : void 0;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Hold and persist a fetched bundle; returns it. A bundle the server
|
|
1976
|
+
* evaluated for nobody although the request carried a user (an expired or
|
|
1977
|
+
* revoked session) goes to the `anon` slot and leaves the user's values
|
|
1978
|
+
* alone; one evaluated for another user is not cached.
|
|
1979
|
+
*/
|
|
1980
|
+
storeConfig(ticket, bundle, etag) {
|
|
1981
|
+
if (!isPlainObject(bundle.values)) return bundle;
|
|
1982
|
+
const evaluatedFor = etagEvaluatedFor(etag);
|
|
1983
|
+
let target;
|
|
1984
|
+
if (ticket.requestedFor && evaluatedFor === null) target = ANON_SLOT;
|
|
1985
|
+
else if (typeof evaluatedFor === "string" && evaluatedFor !== ticket.requestedFor) return bundle;
|
|
1986
|
+
this.write(ticket, target, (slot, held) => {
|
|
1987
|
+
slot.config = bundle;
|
|
1988
|
+
if (held) this.config = bundle;
|
|
1989
|
+
});
|
|
1990
|
+
return bundle;
|
|
1991
|
+
}
|
|
1992
|
+
/** Hold and persist a fetched flag list (it replaces the held one). */
|
|
1993
|
+
storeFlags(ticket, flags) {
|
|
1994
|
+
this.write(ticket, void 0, (slot, held) => {
|
|
1995
|
+
slot.flags = [...flags];
|
|
1996
|
+
if (held) this.flags = [...flags];
|
|
1997
|
+
});
|
|
1998
|
+
return flags;
|
|
1999
|
+
}
|
|
2000
|
+
/** Hold and persist one flag from a single-flag lookup; returns it. */
|
|
2001
|
+
upsertFlag(ticket, flag) {
|
|
2002
|
+
this.write(ticket, void 0, (slot, held) => {
|
|
2003
|
+
this.adoptHeldFlags(slot, held);
|
|
2004
|
+
slot.flags = slot.flags ?? [];
|
|
2005
|
+
upsert(slot.flags, flag);
|
|
2006
|
+
if (held) {
|
|
2007
|
+
this.flags = this.flags ?? [];
|
|
2008
|
+
upsert(this.flags, flag);
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
return flag;
|
|
2012
|
+
}
|
|
2013
|
+
/** Drop flag `key` after a single-flag lookup found it off or unknown. */
|
|
2014
|
+
removeFlag(ticket, key) {
|
|
2015
|
+
this.write(ticket, void 0, (slot, held) => {
|
|
2016
|
+
this.adoptHeldFlags(slot, held);
|
|
2017
|
+
if (slot.flags) slot.flags = slot.flags.filter((f) => f.name !== key);
|
|
2018
|
+
if (held && this.flags) this.flags = this.flags.filter((f) => f.name !== key);
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Sign-out: delete the signed-out user's slot and drop the held values;
|
|
2023
|
+
* later lookups serve the `anon` slot, then the defaults.
|
|
2024
|
+
*/
|
|
2025
|
+
clearForSignOut(signedOutUser) {
|
|
2026
|
+
this.generation += 1;
|
|
2027
|
+
this.config = null;
|
|
2028
|
+
this.flags = null;
|
|
2029
|
+
this.viewFor = null;
|
|
2030
|
+
if (!signedOutUser) return;
|
|
2031
|
+
const doomed = slotKey(signedOutUser);
|
|
2032
|
+
if (this.record.slots[doomed]) {
|
|
2033
|
+
delete this.record.slots[doomed];
|
|
2034
|
+
this.persist();
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
/** Resolves when every write so far reached storage. */
|
|
2038
|
+
flushed() {
|
|
2039
|
+
return this.writes;
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Before a single-flag change to the current user's slot: when the slot
|
|
2043
|
+
* holds no flag list and the served list is the signed-out one, the
|
|
2044
|
+
* user's slot starts from that list, so after a relaunch the user reads
|
|
2045
|
+
* the same flags the change was made on.
|
|
2046
|
+
*/
|
|
2047
|
+
adoptHeldFlags(slot, held) {
|
|
2048
|
+
if (held && !slot.flags && this.flags) slot.flags = [...this.flags];
|
|
2049
|
+
}
|
|
2050
|
+
defaultFor(key) {
|
|
2051
|
+
if (!Object.prototype.hasOwnProperty.call(this.defaults, key)) return void 0;
|
|
2052
|
+
const v = this.defaults[key];
|
|
2053
|
+
return v === null ? void 0 : v;
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* `held` is `true` when the slot written is the one the current identity
|
|
2057
|
+
* reads, so the served values change with it.
|
|
2058
|
+
*/
|
|
2059
|
+
write(ticket, targetOverride, apply) {
|
|
2060
|
+
const current = slotKey(this.currentUser());
|
|
2061
|
+
if (ticket.generation !== this.generation || slotKey(ticket.requestedFor) !== current) return;
|
|
2062
|
+
this.ensureView();
|
|
2063
|
+
const target = targetOverride ?? current;
|
|
2064
|
+
const existing = this.record.slots[target];
|
|
2065
|
+
const slot = existing ? {
|
|
2066
|
+
saved_at: existing.saved_at,
|
|
2067
|
+
...existing.config ? { config: existing.config } : {},
|
|
2068
|
+
...existing.flags ? { flags: [...existing.flags] } : {}
|
|
2069
|
+
} : { saved_at: 0 };
|
|
2070
|
+
apply(slot, target === current);
|
|
2071
|
+
if (!slot.config && !slot.flags) {
|
|
2072
|
+
if (!existing) return;
|
|
2073
|
+
delete this.record.slots[target];
|
|
2074
|
+
} else {
|
|
2075
|
+
slot.saved_at = Date.now();
|
|
2076
|
+
this.record.slots[target] = slot;
|
|
2077
|
+
this.prune(target);
|
|
2078
|
+
}
|
|
2079
|
+
this.persist();
|
|
2080
|
+
if (target !== current) this.viewFor = null;
|
|
2081
|
+
}
|
|
2082
|
+
/** Serve the current identity's own slot, the `anon` slot for the rest. */
|
|
2083
|
+
ensureView() {
|
|
2084
|
+
const current = slotKey(this.currentUser());
|
|
2085
|
+
if (this.viewFor === current) return;
|
|
2086
|
+
const own = this.record.slots[current];
|
|
2087
|
+
const anon = current === ANON_SLOT ? void 0 : this.record.slots[ANON_SLOT];
|
|
2088
|
+
const config = own?.config ?? anon?.config;
|
|
2089
|
+
const flags = own?.flags ?? anon?.flags;
|
|
2090
|
+
this.config = config ?? null;
|
|
2091
|
+
this.flags = flags ? [...flags] : null;
|
|
2092
|
+
this.viewFor = current;
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Keep the MAX_SLOTS most recently written user slots; `keep` and the
|
|
2096
|
+
* signed-out slot (read before sign-in and for every user's gaps) stay.
|
|
2097
|
+
*/
|
|
2098
|
+
prune(keep) {
|
|
2099
|
+
const keys = Object.keys(this.record.slots).filter((k) => k !== ANON_SLOT);
|
|
2100
|
+
while (keys.length > MAX_SLOTS) {
|
|
2101
|
+
let oldest = null;
|
|
2102
|
+
for (const k of keys) {
|
|
2103
|
+
if (k === keep) continue;
|
|
2104
|
+
if (oldest === null || this.record.slots[k].saved_at < this.record.slots[oldest].saved_at) oldest = k;
|
|
2105
|
+
}
|
|
2106
|
+
if (oldest === null) break;
|
|
2107
|
+
delete this.record.slots[oldest];
|
|
2108
|
+
keys.splice(keys.indexOf(oldest), 1);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
persist() {
|
|
2112
|
+
const text = Object.keys(this.record.slots).length === 0 ? null : JSON.stringify(this.record);
|
|
2113
|
+
this.writes = this.writes.then(() => text === null ? this.store.delete(this.key) : this.store.write(this.key, text)).catch(() => {});
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
|
|
1827
2117
|
//#endregion
|
|
1828
2118
|
//#region src/client-info.ts
|
|
1829
2119
|
/**
|
|
@@ -2206,7 +2496,7 @@ function delay(ms) {
|
|
|
2206
2496
|
//#endregion
|
|
2207
2497
|
//#region src/index.ts
|
|
2208
2498
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2209
|
-
const SDK_VERSION = "4.
|
|
2499
|
+
const SDK_VERSION = "4.2.0";
|
|
2210
2500
|
/**
|
|
2211
2501
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2212
2502
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -2250,6 +2540,62 @@ const authSubscribers = /* @__PURE__ */ new Set();
|
|
|
2250
2540
|
* start a new session.
|
|
2251
2541
|
*/
|
|
2252
2542
|
const experimentReporter = new ExperimentAssignmentReporter();
|
|
2543
|
+
/**
|
|
2544
|
+
* `configDefaults` from the latest `configure()` call, recorded before
|
|
2545
|
+
* configure reads the AsyncStorage cache. `config.getCached` and
|
|
2546
|
+
* `flags.isEnabled` serve them on a first render that runs while
|
|
2547
|
+
* `configure()` is still in flight.
|
|
2548
|
+
*/
|
|
2549
|
+
let _pendingConfigDefaults = null;
|
|
2550
|
+
/** Validate `AmbaConfig.configDefaults`: absent, or a plain object. */
|
|
2551
|
+
function readConfigDefaults(value) {
|
|
2552
|
+
if (value === void 0 || value === null) return null;
|
|
2553
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new AmbaApiError("INVALID_CONFIG", "Amba.configure: configDefaults must be an object of config key to value");
|
|
2554
|
+
return value;
|
|
2555
|
+
}
|
|
2556
|
+
/** The pending `configDefaults` entry for `key`, ignoring inherited properties. */
|
|
2557
|
+
function pendingDefault(key) {
|
|
2558
|
+
const defaults = _pendingConfigDefaults;
|
|
2559
|
+
if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, key)) return;
|
|
2560
|
+
const value = defaults[key];
|
|
2561
|
+
return value === null ? void 0 : value;
|
|
2562
|
+
}
|
|
2563
|
+
/** `"array"` for arrays, else `typeof` — the JSON kind of a value. */
|
|
2564
|
+
function jsonKind(value) {
|
|
2565
|
+
return Array.isArray(value) ? "array" : typeof value;
|
|
2566
|
+
}
|
|
2567
|
+
function configGetCached(key, defaultValue) {
|
|
2568
|
+
if (state) refreshOnFirstRead(state, "config");
|
|
2569
|
+
const raw = state ? state.remoteValues.configValue(key) : pendingDefault(key);
|
|
2570
|
+
if (raw === void 0 || raw === null) return defaultValue;
|
|
2571
|
+
if (defaultValue !== void 0 && defaultValue !== null && jsonKind(raw) !== jsonKind(defaultValue)) return defaultValue;
|
|
2572
|
+
return raw;
|
|
2573
|
+
}
|
|
2574
|
+
/** `Amba.flags.isEnabled`. */
|
|
2575
|
+
function flagsIsEnabled(key, defaultValue = false) {
|
|
2576
|
+
if (state) refreshOnFirstRead(state, "flags");
|
|
2577
|
+
const held = state ? state.remoteValues.flagEnabled(key) : pendingDefault(key);
|
|
2578
|
+
return typeof held === "boolean" ? held : defaultValue;
|
|
2579
|
+
}
|
|
2580
|
+
/** `Amba.flags.getCached`. */
|
|
2581
|
+
function flagsGetCached(key) {
|
|
2582
|
+
if (!state) return null;
|
|
2583
|
+
refreshOnFirstRead(state, "flags");
|
|
2584
|
+
return state.remoteValues.flag(key);
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* The first on-device read of config (or flags) for each identity after
|
|
2588
|
+
* `configure()` starts one background fetch that replaces the cached values:
|
|
2589
|
+
* once at launch, and once more after a sign-in or sign-out changes who is
|
|
2590
|
+
* reading. Apps that never read cached values make no extra request. A
|
|
2591
|
+
* failure (offline) leaves the cached values in place.
|
|
2592
|
+
*/
|
|
2593
|
+
function refreshOnFirstRead(s, kind) {
|
|
2594
|
+
const identity = s.identity.getAppUserId() ?? "";
|
|
2595
|
+
if (s.refreshedFor[kind] === identity) return;
|
|
2596
|
+
s.refreshedFor[kind] = identity;
|
|
2597
|
+
Promise.resolve().then(() => kind === "config" ? Amba.config.fetch() : Amba.flags.fetch()).catch(() => {});
|
|
2598
|
+
}
|
|
2253
2599
|
function snapshotSession(user) {
|
|
2254
2600
|
if (!user) return null;
|
|
2255
2601
|
return {
|
|
@@ -2872,6 +3218,8 @@ const Amba = {
|
|
|
2872
3218
|
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
2873
3219
|
const secureStorage = config.secureStorage;
|
|
2874
3220
|
if (secureStorage !== void 0 && (typeof secureStorage?.getItem !== "function" || typeof secureStorage?.setItem !== "function" || typeof secureStorage?.removeItem !== "function")) throw new Error("Amba.configure: secureStorage must provide getItem, setItem and removeItem");
|
|
3221
|
+
const configDefaults = readConfigDefaults(config.configDefaults);
|
|
3222
|
+
_pendingConfigDefaults = configDefaults;
|
|
2875
3223
|
const storagePrefix = config.storagePrefix ?? "amba";
|
|
2876
3224
|
const persistence = new AsyncStoragePersistence(storagePrefix);
|
|
2877
3225
|
if (config.events?.batching) await persistence.assertDurable();
|
|
@@ -2879,6 +3227,8 @@ const Amba = {
|
|
|
2879
3227
|
const vault = await resolveTokenVault(tokenStorage, storagePrefix, secureStorage);
|
|
2880
3228
|
const identity = await IdentityStore.create(persistence, vault);
|
|
2881
3229
|
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
3230
|
+
const remoteValues = new RemoteValuesCache(persistence, recordKey(baseUrl, config.clientKey), configDefaults ?? {}, () => identity.getAppUserId());
|
|
3231
|
+
await remoteValues.load();
|
|
2882
3232
|
const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
|
|
2883
3233
|
const http = new AmbaHttpClient({
|
|
2884
3234
|
apiKey: config.clientKey,
|
|
@@ -2912,6 +3262,11 @@ const Amba = {
|
|
|
2912
3262
|
resolvedFunctionsProjectId: null,
|
|
2913
3263
|
functionsProjectIdInflight: null,
|
|
2914
3264
|
persistence,
|
|
3265
|
+
remoteValues,
|
|
3266
|
+
refreshedFor: {
|
|
3267
|
+
config: null,
|
|
3268
|
+
flags: null
|
|
3269
|
+
},
|
|
2915
3270
|
offlineEnabled: false,
|
|
2916
3271
|
offlineQueue: null,
|
|
2917
3272
|
eventQueue: null
|
|
@@ -3162,7 +3517,10 @@ const Amba = {
|
|
|
3162
3517
|
} catch (err) {
|
|
3163
3518
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
3164
3519
|
}
|
|
3520
|
+
const signedOutUser = s.identity.getAppUserId();
|
|
3165
3521
|
await s.identity.clearSession(rotateAnonymousId);
|
|
3522
|
+
s.remoteValues.clearForSignOut(signedOutUser);
|
|
3523
|
+
await s.remoteValues.flushed();
|
|
3166
3524
|
try {
|
|
3167
3525
|
await getOfflineQueue(s).purge();
|
|
3168
3526
|
} catch (err) {
|
|
@@ -3543,54 +3901,69 @@ const Amba = {
|
|
|
3543
3901
|
return text;
|
|
3544
3902
|
}
|
|
3545
3903
|
} },
|
|
3546
|
-
config: {
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
response
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
const
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
parsed =
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3904
|
+
config: {
|
|
3905
|
+
fetch: async () => {
|
|
3906
|
+
const s = getState();
|
|
3907
|
+
const ticket = s.remoteValues.ticket();
|
|
3908
|
+
const url = s.http.url(P.config);
|
|
3909
|
+
const headers = s.http.buildHeaders();
|
|
3910
|
+
let response;
|
|
3911
|
+
try {
|
|
3912
|
+
response = await (globalThis.fetch ?? fetch)(url, {
|
|
3913
|
+
method: "GET",
|
|
3914
|
+
headers
|
|
3915
|
+
});
|
|
3916
|
+
} catch (err) {
|
|
3917
|
+
throw new AmbaApiError("NETWORK_ERROR", `Network error: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
3918
|
+
}
|
|
3919
|
+
const text = await response.text();
|
|
3920
|
+
if (!response.ok) {
|
|
3921
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
3922
|
+
throw mapHttpError(response, text, ct.includes("application/json"));
|
|
3923
|
+
}
|
|
3924
|
+
let parsed = {};
|
|
3925
|
+
if (text) try {
|
|
3926
|
+
parsed = JSON.parse(text);
|
|
3927
|
+
} catch {
|
|
3928
|
+
parsed = {};
|
|
3929
|
+
}
|
|
3930
|
+
const etag = response.headers.get("etag");
|
|
3931
|
+
let version = null;
|
|
3932
|
+
if (etag) {
|
|
3933
|
+
const cleaned = etag.replace(/^"|"$/g, "");
|
|
3934
|
+
const m = cleaned.match(/^(v\d+)/);
|
|
3935
|
+
version = m ? m[1] ?? null : cleaned || null;
|
|
3936
|
+
}
|
|
3937
|
+
return s.remoteValues.storeConfig(ticket, {
|
|
3938
|
+
version,
|
|
3939
|
+
values: parsed.data ?? {}
|
|
3940
|
+
}, etag);
|
|
3941
|
+
},
|
|
3942
|
+
getCached: configGetCached
|
|
3943
|
+
},
|
|
3582
3944
|
flags: {
|
|
3583
3945
|
fetch: async () => {
|
|
3584
|
-
|
|
3946
|
+
const s = getState();
|
|
3947
|
+
const ticket = s.remoteValues.ticket();
|
|
3948
|
+
const env = await s.http.get(P.flags);
|
|
3949
|
+
return s.remoteValues.storeFlags(ticket, env.data);
|
|
3585
3950
|
},
|
|
3586
3951
|
get: async (key) => {
|
|
3952
|
+
const s = getState();
|
|
3953
|
+
const ticket = s.remoteValues.ticket();
|
|
3587
3954
|
try {
|
|
3588
|
-
|
|
3955
|
+
const env = await s.http.get(`${P.flags}/${encodePathSegment(key)}`);
|
|
3956
|
+
return s.remoteValues.upsertFlag(ticket, env.data);
|
|
3589
3957
|
} catch (err) {
|
|
3590
|
-
if (err instanceof AmbaApiError && err.code === "NOT_FOUND")
|
|
3958
|
+
if (err instanceof AmbaApiError && err.code === "NOT_FOUND") {
|
|
3959
|
+
s.remoteValues.removeFlag(ticket, key);
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3591
3962
|
throw err;
|
|
3592
3963
|
}
|
|
3593
|
-
}
|
|
3964
|
+
},
|
|
3965
|
+
getCached: flagsGetCached,
|
|
3966
|
+
isEnabled: flagsIsEnabled
|
|
3594
3967
|
},
|
|
3595
3968
|
experiments: {
|
|
3596
3969
|
getVariant: async (key) => {
|