@namiml/sdk-core 3.4.12-dev.202609252349 → 3.4.12-dev.202609281806
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 +511 -63
- package/dist/index.d.ts +153 -14
- package/dist/index.mjs +512 -64
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -104,7 +104,7 @@ const {
|
|
|
104
104
|
// version — stamped by scripts/version.sh
|
|
105
105
|
NAMI_SDK_VERSION = "3.4.12",
|
|
106
106
|
// full package version including dev suffix — stamped by scripts/version.sh
|
|
107
|
-
NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.
|
|
107
|
+
NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609281806",
|
|
108
108
|
// environments
|
|
109
109
|
PRODUCTION = "production", DEVELOPMENT = "development",
|
|
110
110
|
// error messages
|
|
@@ -117,7 +117,13 @@ API_VERSION = "v3", BASE_URL_PATH = `sdk/${API_VERSION}/platform`, BASE_URL = "h
|
|
|
117
117
|
EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY = "startupTelemetry",
|
|
118
118
|
// Test hook: every platform-config fetch attempt fails before the request leaves.
|
|
119
119
|
// Same namiCommand as Apple's and Android's, so the shared first-run flows drive it.
|
|
120
|
-
SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure",
|
|
120
|
+
SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure",
|
|
121
|
+
// Test hook: every per-paywall body fetch fails before the request leaves, so the body cache's
|
|
122
|
+
// failure rows can be exercised without a proxy. Same namiCommand as Apple's and Android's (NAM-4357).
|
|
123
|
+
SIMULATE_PAYWALL_FETCH_FAILURE = "simulatePaywallFetchFailure",
|
|
124
|
+
// `campaignCacheTTL=<seconds>` overrides the config's campaign_cache_ttl — Apple's command name.
|
|
125
|
+
// It lets the paywall-cache e2e suite pin TTL 3600 / 1 / 0 per load (NAM-4357).
|
|
126
|
+
CAMPAIGN_CACHE_TTL_PREFIX = "campaignCacheTTL=", EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT = 2000,
|
|
121
127
|
// Platform-config attempts when no initial config was supplied (Android's
|
|
122
128
|
// CONFIG_MAX_ATTEMPTS; Apple's retry(maxAttempts: 3)).
|
|
123
129
|
CONFIG_MAX_ATTEMPTS = 3,
|
|
@@ -3057,6 +3063,206 @@ function uniqBy(array, iteratee) {
|
|
|
3057
3063
|
return (array && array.length) ? baseUniq(array, baseIteratee(iteratee)) : [];
|
|
3058
3064
|
}
|
|
3059
3065
|
|
|
3066
|
+
/** Storage key of an entry's raw body; the exact URL follows the prefix. */
|
|
3067
|
+
const PAYWALL_BODY_KEY_PREFIX = "api_paywall_body:";
|
|
3068
|
+
/**
|
|
3069
|
+
* Storage key of an entry's metadata. Written AFTER the body, so it is the commit point: an entry
|
|
3070
|
+
* is visible only once both are durable, and a body with no metadata is an interrupted write.
|
|
3071
|
+
*/
|
|
3072
|
+
const PAYWALL_BODY_META_KEY_PREFIX = "api_paywall_body_meta:";
|
|
3073
|
+
const DEFAULT_MAX_BODY_ENTRIES = 500;
|
|
3074
|
+
class PaywallBodyCache {
|
|
3075
|
+
constructor(storage = () => getPlatformAdapters().storage, maxEntries = DEFAULT_MAX_BODY_ENTRIES) {
|
|
3076
|
+
this.storage = storage;
|
|
3077
|
+
this.maxEntries = maxEntries;
|
|
3078
|
+
/** Set under `campaign_cache_ttl == 0`: entries live here, in this process only. */
|
|
3079
|
+
this.memory = null;
|
|
3080
|
+
/** Metadata of every persisted entry, loaded once from the storage adapter. */
|
|
3081
|
+
this.index = null;
|
|
3082
|
+
}
|
|
3083
|
+
get count() {
|
|
3084
|
+
return this.memory ? this.memory.size : this.loadIndex().size;
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Applies `campaign_cache_ttl` at the start of a resolution. `null` keeps everything, `n > 0`
|
|
3088
|
+
* purges entries whose own age is `>= n` seconds, and `0` purges the persistent store and keeps
|
|
3089
|
+
* this process's entries in memory only.
|
|
3090
|
+
*/
|
|
3091
|
+
evaluateTtl(ttl, now = Date.now()) {
|
|
3092
|
+
if (ttl === 0) {
|
|
3093
|
+
if (this.memory)
|
|
3094
|
+
return;
|
|
3095
|
+
this.dropPersisted([...this.loadIndex().keys()]);
|
|
3096
|
+
this.memory = new Map();
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
// Leaving TTL 0 goes back to the persistent store; the process-local entries are dropped.
|
|
3100
|
+
this.memory = null;
|
|
3101
|
+
if (ttl === null)
|
|
3102
|
+
return;
|
|
3103
|
+
const expired = [...this.loadIndex().values()].filter((meta) => isExpired(meta, ttl, now));
|
|
3104
|
+
this.dropPersisted(expired.map((meta) => meta.url));
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* The entry stored under this exact URL, or null when there is none, it is past `ttl`, or its
|
|
3108
|
+
* body is missing (a half-written entry, which is deleted here).
|
|
3109
|
+
*/
|
|
3110
|
+
validEntry(url, ttl, now = Date.now()) {
|
|
3111
|
+
if (this.memory)
|
|
3112
|
+
return this.memory.get(url) ?? null;
|
|
3113
|
+
const meta = this.loadIndex().get(url);
|
|
3114
|
+
if (!meta)
|
|
3115
|
+
return null;
|
|
3116
|
+
if (ttl !== null && ttl > 0 && isExpired(meta, ttl, now)) {
|
|
3117
|
+
this.dropPersisted([url]);
|
|
3118
|
+
return null;
|
|
3119
|
+
}
|
|
3120
|
+
const body = this.read(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3121
|
+
if (body === null) {
|
|
3122
|
+
this.dropPersisted([url]);
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
3125
|
+
return { meta, body };
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* The stored body for this exact URL, read straight from storage rather than through the index,
|
|
3129
|
+
* with no TTL check. This is what rebuilds the aggregate `api_paywalls`, so it also sees an entry
|
|
3130
|
+
* a host wrote after the index was loaded (the web test apps inject local test paywalls that way).
|
|
3131
|
+
*/
|
|
3132
|
+
bodyFor(url) {
|
|
3133
|
+
if (this.memory)
|
|
3134
|
+
return this.memory.get(url)?.body ?? null;
|
|
3135
|
+
return this.read(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3136
|
+
}
|
|
3137
|
+
/** Stores a body under its exact URL. Returns false when the write did not land (quota, I/O). */
|
|
3138
|
+
store(url, contentHash, body, now = Date.now()) {
|
|
3139
|
+
const meta = { url, savedAt: now, ...(contentHash ? { contentHash } : {}) };
|
|
3140
|
+
if (this.memory) {
|
|
3141
|
+
this.memory.set(url, { meta, body });
|
|
3142
|
+
return true;
|
|
3143
|
+
}
|
|
3144
|
+
const index = this.loadIndex();
|
|
3145
|
+
const metaJson = JSON.stringify(meta);
|
|
3146
|
+
// Body first, metadata last. Each write is verified by reading it back: the web adapter
|
|
3147
|
+
// swallows a quota error rather than throwing, so a silent no-op is the common failure.
|
|
3148
|
+
if (this.write(PAYWALL_BODY_KEY_PREFIX + url, body) &&
|
|
3149
|
+
this.write(PAYWALL_BODY_META_KEY_PREFIX + url, metaJson)) {
|
|
3150
|
+
index.set(url, meta);
|
|
3151
|
+
return true;
|
|
3152
|
+
}
|
|
3153
|
+
logger.warn(`Paywall body cache: could not store ${url}; it will be fetched next time`);
|
|
3154
|
+
this.dropPersisted([url]);
|
|
3155
|
+
return false;
|
|
3156
|
+
}
|
|
3157
|
+
/** Stores a bundled body unless an entry already exists — a body the SDK fetched always wins. */
|
|
3158
|
+
seedIfAbsent(url, contentHash, body, now = Date.now()) {
|
|
3159
|
+
const exists = this.memory ? this.memory.has(url) : this.loadIndex().has(url);
|
|
3160
|
+
return exists ? false : this.store(url, contentHash, body, now);
|
|
3161
|
+
}
|
|
3162
|
+
/** Deletes one entry — used for an undecodable body. Not eviction. */
|
|
3163
|
+
remove(url) {
|
|
3164
|
+
if (this.memory)
|
|
3165
|
+
this.memory.delete(url);
|
|
3166
|
+
else
|
|
3167
|
+
this.dropPersisted([url]);
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Eviction, run once per resolution after a complete `campaign_rules` fetch: delete every entry
|
|
3171
|
+
* whose URL this resolution did not name, then cap the store at `maxEntries`, oldest first.
|
|
3172
|
+
*/
|
|
3173
|
+
reconcile(currentUrls) {
|
|
3174
|
+
const metas = this.memory
|
|
3175
|
+
? [...this.memory.values()].map((entry) => entry.meta)
|
|
3176
|
+
: [...this.loadIndex().values()];
|
|
3177
|
+
const unnamed = metas.filter((meta) => !currentUrls.has(meta.url)).map((meta) => meta.url);
|
|
3178
|
+
const kept = metas.filter((meta) => currentUrls.has(meta.url)).sort((a, b) => a.savedAt - b.savedAt);
|
|
3179
|
+
const overCap = kept.slice(0, Math.max(0, kept.length - this.maxEntries)).map((meta) => meta.url);
|
|
3180
|
+
[...unnamed, ...overCap].forEach((url) => this.remove(url));
|
|
3181
|
+
}
|
|
3182
|
+
/** Empties the store — persisted and in-memory entries alike. Called by `Nami.reset()`. */
|
|
3183
|
+
purgeAll() {
|
|
3184
|
+
this.dropPersisted([...this.loadIndex().keys()]);
|
|
3185
|
+
this.memory?.clear();
|
|
3186
|
+
}
|
|
3187
|
+
/** Builds the index from the metadata keys, clearing away half-written and unreadable entries. */
|
|
3188
|
+
loadIndex() {
|
|
3189
|
+
if (this.index)
|
|
3190
|
+
return this.index;
|
|
3191
|
+
this.index = new Map();
|
|
3192
|
+
let keys;
|
|
3193
|
+
try {
|
|
3194
|
+
keys = this.storage().getAllKeys();
|
|
3195
|
+
}
|
|
3196
|
+
catch (error) {
|
|
3197
|
+
logger.warn(`Paywall body cache: storage unavailable, caching nothing (${error})`);
|
|
3198
|
+
return this.index;
|
|
3199
|
+
}
|
|
3200
|
+
const bodies = new Set(keys.filter((key) => key.startsWith(PAYWALL_BODY_KEY_PREFIX)).map((key) => key.slice(PAYWALL_BODY_KEY_PREFIX.length)));
|
|
3201
|
+
const stale = [];
|
|
3202
|
+
for (const key of keys) {
|
|
3203
|
+
if (!key.startsWith(PAYWALL_BODY_META_KEY_PREFIX))
|
|
3204
|
+
continue;
|
|
3205
|
+
const url = key.slice(PAYWALL_BODY_META_KEY_PREFIX.length);
|
|
3206
|
+
const meta = parseMeta(this.read(key));
|
|
3207
|
+
if (meta?.url === url && bodies.has(url))
|
|
3208
|
+
this.index.set(url, meta);
|
|
3209
|
+
else
|
|
3210
|
+
stale.push(url);
|
|
3211
|
+
bodies.delete(url);
|
|
3212
|
+
}
|
|
3213
|
+
// Bodies whose metadata never landed: interrupted writes.
|
|
3214
|
+
this.dropPersisted([...stale, ...bodies]);
|
|
3215
|
+
return this.index;
|
|
3216
|
+
}
|
|
3217
|
+
dropPersisted(urls) {
|
|
3218
|
+
for (const url of urls) {
|
|
3219
|
+
this.index?.delete(url);
|
|
3220
|
+
this.removeKey(PAYWALL_BODY_META_KEY_PREFIX + url);
|
|
3221
|
+
this.removeKey(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
read(key) {
|
|
3225
|
+
try {
|
|
3226
|
+
return this.storage().getItem(key);
|
|
3227
|
+
}
|
|
3228
|
+
catch {
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
write(key, value) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.storage().setItem(key, value);
|
|
3235
|
+
}
|
|
3236
|
+
catch {
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
return this.read(key) === value;
|
|
3240
|
+
}
|
|
3241
|
+
removeKey(key) {
|
|
3242
|
+
try {
|
|
3243
|
+
this.storage().removeItem(key);
|
|
3244
|
+
}
|
|
3245
|
+
catch {
|
|
3246
|
+
// Nothing more to do: an entry that cannot be deleted is re-validated on the next read.
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
function isExpired(meta, ttlSeconds, now) {
|
|
3251
|
+
return (now - meta.savedAt) / 1000 >= ttlSeconds;
|
|
3252
|
+
}
|
|
3253
|
+
function parseMeta(raw) {
|
|
3254
|
+
if (raw === null)
|
|
3255
|
+
return null;
|
|
3256
|
+
try {
|
|
3257
|
+
const meta = JSON.parse(raw);
|
|
3258
|
+
return typeof meta?.url === "string" && typeof meta?.savedAt === "number" ? meta : null;
|
|
3259
|
+
}
|
|
3260
|
+
catch {
|
|
3261
|
+
return null;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
const paywallBodyCache = new PaywallBodyCache();
|
|
3265
|
+
|
|
3060
3266
|
class StorageService {
|
|
3061
3267
|
constructor() {
|
|
3062
3268
|
this.memoryStore = {};
|
|
@@ -3139,6 +3345,20 @@ class StorageService {
|
|
|
3139
3345
|
}
|
|
3140
3346
|
this.setItem(key, paywalls);
|
|
3141
3347
|
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Writes the aggregate `api_paywalls` as the list of URLs a per-URL resolution produced, rather
|
|
3350
|
+
* than a second copy of their bodies (NAM-4357): the bodies already live in the per-URL body
|
|
3351
|
+
* cache, and `getPaywalls(API_PAYWALLS)` rebuilds the paywall objects from it. The legacy bulk
|
|
3352
|
+
* path still writes whole objects through `setPaywalls`.
|
|
3353
|
+
*/
|
|
3354
|
+
setApiPaywallsFromBodyCache(urls) {
|
|
3355
|
+
const index = { bodyCacheUrls: urls };
|
|
3356
|
+
if (this.getCampaignCacheTtl() === 0) {
|
|
3357
|
+
this.memoryStore[API_PAYWALLS] = JSON.stringify(index);
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
this.setItem(API_PAYWALLS, index);
|
|
3361
|
+
}
|
|
3142
3362
|
getPaywalls(key) {
|
|
3143
3363
|
if (key === INITIAL_PAYWALLS) {
|
|
3144
3364
|
return this.memoryStore[INITIAL_PAYWALLS]
|
|
@@ -3147,8 +3367,10 @@ class StorageService {
|
|
|
3147
3367
|
}
|
|
3148
3368
|
if (key === API_PAYWALLS) {
|
|
3149
3369
|
const memoryValue = this.memoryStore[API_PAYWALLS];
|
|
3150
|
-
if (memoryValue != null)
|
|
3151
|
-
|
|
3370
|
+
if (memoryValue != null) {
|
|
3371
|
+
const parsed = JSON.parse(memoryValue);
|
|
3372
|
+
return isBodyCacheAggregate(parsed) ? paywallsFromBodyCache(parsed.bodyCacheUrls) : parsed;
|
|
3373
|
+
}
|
|
3152
3374
|
let stored = this.getItem(API_PAYWALLS);
|
|
3153
3375
|
if (stored === null)
|
|
3154
3376
|
return null;
|
|
@@ -3160,6 +3382,8 @@ class StorageService {
|
|
|
3160
3382
|
return stored;
|
|
3161
3383
|
}
|
|
3162
3384
|
}
|
|
3385
|
+
if (isBodyCacheAggregate(stored))
|
|
3386
|
+
return paywallsFromBodyCache(stored.bodyCacheUrls);
|
|
3163
3387
|
return this.isLegacyTimestampedCache(stored) ? stored.data : stored;
|
|
3164
3388
|
}
|
|
3165
3389
|
return this.getItem(key);
|
|
@@ -3328,7 +3552,18 @@ class StorageService {
|
|
|
3328
3552
|
this.resetItem(API_CAMPAIGN_RULES);
|
|
3329
3553
|
this.resetItem(API_CAMPAIGN_SESSION_TIMESTAMP);
|
|
3330
3554
|
}
|
|
3555
|
+
/**
|
|
3556
|
+
* The effective `campaign_cache_ttl`: a `campaignCacheTTL=<n>` namiCommand first, then the API
|
|
3557
|
+
* config, then the initial config. Lives here rather than in `utils/config` because this service
|
|
3558
|
+
* needs it to decide where the aggregate caches go, and `utils/config` already imports this one.
|
|
3559
|
+
*/
|
|
3331
3560
|
getCampaignCacheTtl() {
|
|
3561
|
+
const override = this.getNamiConfig()?.namiCommands
|
|
3562
|
+
?.filter((cmd) => cmd.startsWith(CAMPAIGN_CACHE_TTL_PREFIX))
|
|
3563
|
+
.map((cmd) => cmd.slice(CAMPAIGN_CACHE_TTL_PREFIX.length))
|
|
3564
|
+
.find((value) => /^\d+$/.test(value));
|
|
3565
|
+
if (override !== undefined)
|
|
3566
|
+
return parseInt(override, 10);
|
|
3332
3567
|
const config = this.getAppConfig(API_CONFIG)
|
|
3333
3568
|
|| this.getAppConfig(INITIAL_APP_CONFIG);
|
|
3334
3569
|
return config?.campaign_cache_ttl ?? null;
|
|
@@ -3420,6 +3655,28 @@ class StorageService {
|
|
|
3420
3655
|
this.memoryStore = {};
|
|
3421
3656
|
}
|
|
3422
3657
|
}
|
|
3658
|
+
function isBodyCacheAggregate(value) {
|
|
3659
|
+
return typeof value === "object" && value !== null && Array.isArray(value.bodyCacheUrls);
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3662
|
+
* The paywalls a per-URL resolution produced, decoded from the body cache. A URL whose entry has
|
|
3663
|
+
* since gone (evicted, purged, or a write that never landed) is simply absent.
|
|
3664
|
+
*/
|
|
3665
|
+
function paywallsFromBodyCache(urls) {
|
|
3666
|
+
const paywalls = [];
|
|
3667
|
+
for (const url of urls) {
|
|
3668
|
+
const body = paywallBodyCache.bodyFor(url);
|
|
3669
|
+
if (body === null)
|
|
3670
|
+
continue;
|
|
3671
|
+
try {
|
|
3672
|
+
paywalls.push(JSON.parse(body));
|
|
3673
|
+
}
|
|
3674
|
+
catch {
|
|
3675
|
+
paywallBodyCache.remove(url);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
return paywalls;
|
|
3679
|
+
}
|
|
3423
3680
|
const storageService = new StorageService();
|
|
3424
3681
|
|
|
3425
3682
|
class RetryLimitExceededError extends Error {
|
|
@@ -6797,11 +7054,8 @@ const hasCapability = (capability) => {
|
|
|
6797
7054
|
|| storageService.getAppConfig(INITIAL_APP_CONFIG);
|
|
6798
7055
|
return appConfig?.capabilities?.includes(capability) ?? false;
|
|
6799
7056
|
};
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|| storageService.getAppConfig(INITIAL_APP_CONFIG);
|
|
6803
|
-
return config?.campaign_cache_ttl ?? null;
|
|
6804
|
-
};
|
|
7057
|
+
/** The effective `campaign_cache_ttl`, `campaignCacheTTL=<n>` namiCommand first. */
|
|
7058
|
+
const getCampaignCacheTtl = () => storageService.getCampaignCacheTtl();
|
|
6805
7059
|
/**
|
|
6806
7060
|
* The effective global journey settings from the current config, coalescing a missing
|
|
6807
7061
|
* `journey_settings` object — or any missing field — to {@link DEFAULT_JOURNEY_SETTINGS}.
|
|
@@ -57508,22 +57762,23 @@ class CampaignRuleRepository {
|
|
|
57508
57762
|
static hasPaywallUrls(campaigns) {
|
|
57509
57763
|
return campaigns.some((c) => c.paywall_url || c.page_urls || c.flow?.page_urls);
|
|
57510
57764
|
}
|
|
57765
|
+
/**
|
|
57766
|
+
* The campaign rules for this form factor, unvalidated, plus whether they came from the network.
|
|
57767
|
+
* `fromNetwork` is false when the stored rules stood in for a failed fetch (or updates are
|
|
57768
|
+
* disabled): the per-URL body cache must then neither resolve nor evict (NAM-4357).
|
|
57769
|
+
*/
|
|
57511
57770
|
async fetchCampaignRulesRaw() {
|
|
57512
|
-
const authDevice = storageService.getDevice();
|
|
57513
57771
|
if (this.disableCampaignUpdates) {
|
|
57514
57772
|
logger.info("Campaign updates disabled by configuration. Skipping fetch.");
|
|
57515
|
-
return [];
|
|
57516
|
-
}
|
|
57517
|
-
let campaignRules;
|
|
57518
|
-
if (!authDevice?.id) {
|
|
57519
|
-
campaignRules = await this.getAnonymousCampaigns();
|
|
57520
|
-
}
|
|
57521
|
-
else {
|
|
57522
|
-
campaignRules = await this.getCampaigns(authDevice.id);
|
|
57773
|
+
return { campaigns: [], fromNetwork: false };
|
|
57523
57774
|
}
|
|
57524
|
-
|
|
57775
|
+
const authDevice = storageService.getDevice();
|
|
57776
|
+
const fetched = authDevice?.id
|
|
57777
|
+
? await this.fetchCampaigns(authDevice.id)
|
|
57778
|
+
: await this.fetchAnonymousCampaigns();
|
|
57779
|
+
const campaigns = fetched.campaigns?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
|
|
57525
57780
|
(cRule.flow && cRule.form_factors?.some((f) => f.form_factor === this.currentFormFactor)));
|
|
57526
|
-
return
|
|
57781
|
+
return { campaigns, fromNetwork: fetched.fromNetwork };
|
|
57527
57782
|
}
|
|
57528
57783
|
finalizeCampaignRules(campaignRules, paywalls) {
|
|
57529
57784
|
// NAM-2457: a server flow rule is kept only when BOTH assets are satisfied —
|
|
@@ -57559,10 +57814,19 @@ class CampaignRuleRepository {
|
|
|
57559
57814
|
listeners.forEach((handler) => handler(campaigns));
|
|
57560
57815
|
}
|
|
57561
57816
|
async getAnonymousCampaigns(sdkVersion = NAMI_SDK_VERSION) {
|
|
57817
|
+
return (await this.fetchAnonymousCampaigns(sdkVersion)).campaigns;
|
|
57818
|
+
}
|
|
57819
|
+
async getCampaigns(deviceId) {
|
|
57820
|
+
return (await this.fetchCampaigns(deviceId)).campaigns;
|
|
57821
|
+
}
|
|
57822
|
+
async fetchAnonymousCampaigns(sdkVersion = NAMI_SDK_VERSION) {
|
|
57562
57823
|
const path = `campaign_rules/?sdk_version=${sdkVersion}`;
|
|
57563
57824
|
try {
|
|
57564
57825
|
const resp = await NamiAPI.instance.fetchAPI(path);
|
|
57565
|
-
return
|
|
57826
|
+
return {
|
|
57827
|
+
campaigns: mapAnonymousCampaigns(resp.results ?? [], this.splitPosition, this.currentFormFactor),
|
|
57828
|
+
fromNetwork: true,
|
|
57829
|
+
};
|
|
57566
57830
|
}
|
|
57567
57831
|
catch (error) {
|
|
57568
57832
|
const data = this.fallbackData();
|
|
@@ -57570,23 +57834,23 @@ class CampaignRuleRepository {
|
|
|
57570
57834
|
logger.error(handleErrors(error.status, path));
|
|
57571
57835
|
throw error;
|
|
57572
57836
|
}
|
|
57573
|
-
return data.results;
|
|
57837
|
+
return { campaigns: data.results, fromNetwork: false };
|
|
57574
57838
|
}
|
|
57575
57839
|
}
|
|
57576
|
-
async
|
|
57840
|
+
async fetchCampaigns(deviceId) {
|
|
57577
57841
|
const path = `device/${deviceId}/campaign_rules/`;
|
|
57578
|
-
let data;
|
|
57579
57842
|
try {
|
|
57580
|
-
data = await NamiAPI.instance.fetchAPI(path);
|
|
57843
|
+
const data = await NamiAPI.instance.fetchAPI(path);
|
|
57844
|
+
return { campaigns: data?.results || [], fromNetwork: true };
|
|
57581
57845
|
}
|
|
57582
57846
|
catch (error) {
|
|
57583
|
-
data = this.fallbackData();
|
|
57847
|
+
const data = this.fallbackData();
|
|
57584
57848
|
if (!data) {
|
|
57585
57849
|
logger.error(handleErrors(error.status, path));
|
|
57586
57850
|
throw error;
|
|
57587
57851
|
}
|
|
57852
|
+
return { campaigns: data.results || [], fromNetwork: false };
|
|
57588
57853
|
}
|
|
57589
|
-
return data?.results || [];
|
|
57590
57854
|
}
|
|
57591
57855
|
fallbackData() {
|
|
57592
57856
|
const storedData = storageService.getCampaignRules(API_CAMPAIGN_RULES)
|
|
@@ -57869,6 +58133,15 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
57869
58133
|
* RetryExhaustedError when all attempts and/or the budget are spent.
|
|
57870
58134
|
*/
|
|
57871
58135
|
async function fetchWithCdnRetry(url, opts = {}) {
|
|
58136
|
+
const { data, retryCount } = await fetchTextWithCdnRetry(url, opts);
|
|
58137
|
+
return { data: JSON.parse(data), retryCount };
|
|
58138
|
+
}
|
|
58139
|
+
/**
|
|
58140
|
+
* {@link fetchWithCdnRetry}'s retry loop, returning the raw 2xx body text instead of parsed JSON.
|
|
58141
|
+
* The per-URL paywall body cache stores those exact bytes, so a cached body is decoded by the
|
|
58142
|
+
* same `JSON.parse` a fetched one is (NAM-4357).
|
|
58143
|
+
*/
|
|
58144
|
+
async function fetchTextWithCdnRetry(url, opts = {}) {
|
|
57872
58145
|
const maxAttempts = opts.maxAttempts ?? DEFAULTS.maxAttempts;
|
|
57873
58146
|
const baseDelayMs = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
|
|
57874
58147
|
const maxDelayMs = opts.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
@@ -57918,8 +58191,7 @@ async function fetchWithCdnRetry(url, opts = {}) {
|
|
|
57918
58191
|
const responseBody = await response.clone().text();
|
|
57919
58192
|
logger.debug(`[HTTP] Response ${response.status}: ${responseBody}`);
|
|
57920
58193
|
}
|
|
57921
|
-
|
|
57922
|
-
return { data, retryCount: attempt };
|
|
58194
|
+
return { data: await response.text(), retryCount: attempt };
|
|
57923
58195
|
}
|
|
57924
58196
|
catch (err) {
|
|
57925
58197
|
if (err instanceof NonRetryableHttpError)
|
|
@@ -57940,7 +58212,43 @@ async function fetchWithCdnRetry(url, opts = {}) {
|
|
|
57940
58212
|
throw new RetryExhaustedError(url, attempt, lastStatus);
|
|
57941
58213
|
}
|
|
57942
58214
|
|
|
58215
|
+
/**
|
|
58216
|
+
* Reads the parts of a `campaign_rules` paywall URL the per-URL body cache needs (NAM-4357).
|
|
58217
|
+
*
|
|
58218
|
+
* Both helpers scan the string rather than build a `URL`: the cache is keyed on the exact bytes
|
|
58219
|
+
* the server sent, so nothing here may normalise, re-encode or reject a URL — a malformed one is
|
|
58220
|
+
* simply un-hashed.
|
|
58221
|
+
*/
|
|
58222
|
+
/**
|
|
58223
|
+
* The `content_hash` query value, or undefined when it is missing or empty. When the item appears
|
|
58224
|
+
* more than once the first value wins. The value is never parsed or validated: it is a
|
|
58225
|
+
* cache-validity token, not a content selector.
|
|
58226
|
+
*/
|
|
58227
|
+
function contentHashOf(url) {
|
|
58228
|
+
const query = url.split("#")[0].split("?")[1];
|
|
58229
|
+
if (!query)
|
|
58230
|
+
return undefined;
|
|
58231
|
+
for (const item of query.split("&")) {
|
|
58232
|
+
const eq = item.indexOf("=");
|
|
58233
|
+
const name = eq === -1 ? item : item.slice(0, eq);
|
|
58234
|
+
if (name !== "content_hash")
|
|
58235
|
+
continue;
|
|
58236
|
+
const value = eq === -1 ? "" : item.slice(eq + 1);
|
|
58237
|
+
return value || undefined;
|
|
58238
|
+
}
|
|
58239
|
+
return undefined;
|
|
58240
|
+
}
|
|
58241
|
+
/** The path segment after `/paywall/` — the paywall id the URL names — or undefined. */
|
|
58242
|
+
function paywallIdOf(url) {
|
|
58243
|
+
const path = url.split(/[?#]/)[0];
|
|
58244
|
+
const match = /\/paywall\/([^/]+)/.exec(path);
|
|
58245
|
+
return match ? match[1] : undefined;
|
|
58246
|
+
}
|
|
58247
|
+
|
|
57943
58248
|
class PaywallRepository {
|
|
58249
|
+
constructor(bodyCache = paywallBodyCache) {
|
|
58250
|
+
this.bodyCache = bodyCache;
|
|
58251
|
+
}
|
|
57944
58252
|
async fetchPaywalls() {
|
|
57945
58253
|
const authDevice = storageService.getDevice();
|
|
57946
58254
|
let paywalls;
|
|
@@ -57989,34 +58297,131 @@ class PaywallRepository {
|
|
|
57989
58297
|
}
|
|
57990
58298
|
return data?.results || [];
|
|
57991
58299
|
}
|
|
57992
|
-
|
|
57993
|
-
|
|
57994
|
-
|
|
57995
|
-
|
|
58300
|
+
/**
|
|
58301
|
+
* Resolves the URL set named by `campaign_rules` into paywall objects — the decision table of
|
|
58302
|
+
* `api-resilience-and-caching.md` § 2.2, run identically at startup and on `refresh()`:
|
|
58303
|
+
*
|
|
58304
|
+
* | URL | entry for the exact URL within TTL | action | provenance |
|
|
58305
|
+
* |-----------|------------------------------------|-------------------------------|--------------------------|
|
|
58306
|
+
* | hashed | yes | serve it, no request | `cacheHit` |
|
|
58307
|
+
* | hashed | no | fetch; store the 2xx body | `fetched` |
|
|
58308
|
+
* | un-hashed | not consulted | fetch; store the 2xx body | `fetched` |
|
|
58309
|
+
* | either | fetch failed, entry within TTL | serve it, keep it | `cacheAfterFetchFailure` |
|
|
58310
|
+
* | either | fetch failed, no entry | no body | counts `failed` |
|
|
58311
|
+
*
|
|
58312
|
+
* Writes the aggregate `api_paywalls` — as the URLs of every body this resolution produced, since
|
|
58313
|
+
* the bodies themselves are already in the body cache — unless it produced none, so an outage
|
|
58314
|
+
* cannot overwrite a good aggregate with an empty one. Eviction is the caller's: it runs only
|
|
58315
|
+
* after a complete `campaign_rules` fetch.
|
|
58316
|
+
*/
|
|
58317
|
+
async resolvePaywallsByUrls(urls) {
|
|
57996
58318
|
const uniqueUrls = [...new Set(urls)];
|
|
57997
|
-
const
|
|
57998
|
-
|
|
57999
|
-
|
|
58000
|
-
|
|
58001
|
-
|
|
58002
|
-
|
|
58003
|
-
|
|
58004
|
-
|
|
58005
|
-
|
|
58006
|
-
|
|
58007
|
-
|
|
58008
|
-
|
|
58009
|
-
|
|
58319
|
+
const ttl = getCampaignCacheTtl();
|
|
58320
|
+
this.bodyCache.evaluateTtl(ttl);
|
|
58321
|
+
const resolutions = await Promise.all(uniqueUrls.map((url) => this.resolvePaywall(url, ttl)));
|
|
58322
|
+
const outcome = {
|
|
58323
|
+
paywalls: [],
|
|
58324
|
+
urls: uniqueUrls.length,
|
|
58325
|
+
hashed: uniqueUrls.filter((url) => contentHashOf(url) !== undefined).length,
|
|
58326
|
+
cacheHits: 0,
|
|
58327
|
+
fetched: 0,
|
|
58328
|
+
cacheAfterFetchFailure: 0,
|
|
58329
|
+
failed: 0,
|
|
58330
|
+
retryCount: 0,
|
|
58331
|
+
};
|
|
58332
|
+
const resolvedUrls = new Map();
|
|
58333
|
+
resolutions.forEach((resolution, i) => {
|
|
58334
|
+
outcome.retryCount += resolution.retryCount;
|
|
58335
|
+
if (!resolution.paywall) {
|
|
58336
|
+
outcome.failed += 1;
|
|
58337
|
+
return;
|
|
58010
58338
|
}
|
|
58339
|
+
resolvedUrls.set(resolution.paywall, uniqueUrls[i]);
|
|
58340
|
+
if (resolution.paywall.fetchSource === "cacheHit")
|
|
58341
|
+
outcome.cacheHits += 1;
|
|
58342
|
+
else if (resolution.paywall.fetchSource === "cacheAfterFetchFailure")
|
|
58343
|
+
outcome.cacheAfterFetchFailure += 1;
|
|
58344
|
+
else
|
|
58345
|
+
outcome.fetched += 1;
|
|
58346
|
+
});
|
|
58347
|
+
outcome.paywalls = this.validatePaywalls([...resolvedUrls.keys()]);
|
|
58348
|
+
if (outcome.paywalls.length > 0) {
|
|
58349
|
+
storageService.setApiPaywallsFromBodyCache(outcome.paywalls.map((paywall) => resolvedUrls.get(paywall)));
|
|
58011
58350
|
}
|
|
58012
|
-
|
|
58013
|
-
|
|
58014
|
-
|
|
58015
|
-
|
|
58351
|
+
return outcome;
|
|
58352
|
+
}
|
|
58353
|
+
/**
|
|
58354
|
+
* Seeds the body cache from the initial-config bundle (§ 2.2 "Seeding"): every **hashed** bundled
|
|
58355
|
+
* URL whose paywall id matches a bundled body is stored under that exact URL unless an entry
|
|
58356
|
+
* already exists. A hash in a bundled URL was minted against the bundled body, so the pair is
|
|
58357
|
+
* exact. Runs at configure, before the first resolution.
|
|
58358
|
+
*/
|
|
58359
|
+
seedFromInitialConfig(campaignRules, paywalls) {
|
|
58360
|
+
const bundled = new Map(paywalls.map((paywall) => [paywall.id, paywall]));
|
|
58361
|
+
this.bodyCache.evaluateTtl(getCampaignCacheTtl());
|
|
58362
|
+
let candidates = 0;
|
|
58363
|
+
let seeded = 0;
|
|
58364
|
+
for (const url of new Set(bundledPaywallUrls(campaignRules))) {
|
|
58365
|
+
const contentHash = contentHashOf(url);
|
|
58366
|
+
const id = paywallIdOf(url);
|
|
58367
|
+
const paywall = id ? bundled.get(id) : undefined;
|
|
58368
|
+
if (!contentHash || !paywall)
|
|
58369
|
+
continue;
|
|
58370
|
+
candidates += 1;
|
|
58371
|
+
if (this.bodyCache.seedIfAbsent(url, contentHash, JSON.stringify(paywall)))
|
|
58372
|
+
seeded += 1;
|
|
58373
|
+
}
|
|
58374
|
+
logger.info(`Paywall body cache seeded from initial config: seeded=${seeded}, candidates=${candidates}`);
|
|
58375
|
+
}
|
|
58376
|
+
/** One row of the decision table. Never throws. */
|
|
58377
|
+
async resolvePaywall(url, ttl) {
|
|
58378
|
+
const contentHash = contentHashOf(url);
|
|
58379
|
+
if (contentHash) {
|
|
58380
|
+
const hit = this.decodeCached(url, ttl);
|
|
58381
|
+
if (hit)
|
|
58382
|
+
return { paywall: withProvenance(hit, "cacheHit", contentHash), retryCount: 0 };
|
|
58383
|
+
}
|
|
58384
|
+
let retryCount = 0;
|
|
58385
|
+
try {
|
|
58386
|
+
const response = await this.fetchPaywallText(url);
|
|
58387
|
+
retryCount = response.retryCount;
|
|
58388
|
+
const paywall = decodePaywall(response.data);
|
|
58389
|
+
if (paywall) {
|
|
58390
|
+
this.bodyCache.store(url, contentHash, response.data);
|
|
58391
|
+
return { paywall: withProvenance(paywall, "fetched", contentHash), retryCount };
|
|
58016
58392
|
}
|
|
58017
|
-
|
|
58393
|
+
logger.error(`Paywall body from ${url} is not a decodable paywall`);
|
|
58018
58394
|
}
|
|
58019
|
-
|
|
58395
|
+
catch (error) {
|
|
58396
|
+
// Note: an exhausted-failure path contributed retries we can't see from the thrown error,
|
|
58397
|
+
// so retry_count under-reports failures. Acceptable for the telemetry signal.
|
|
58398
|
+
logger.error(`Failed to fetch individual paywall: ${error}`);
|
|
58399
|
+
}
|
|
58400
|
+
const fallback = this.decodeCached(url, ttl);
|
|
58401
|
+
if (fallback) {
|
|
58402
|
+
logger.warn(`Serving the cached body for ${url} after fetch failure (within campaign_cache_ttl)`);
|
|
58403
|
+
return { paywall: withProvenance(fallback, "cacheAfterFetchFailure", contentHash), retryCount };
|
|
58404
|
+
}
|
|
58405
|
+
return { retryCount };
|
|
58406
|
+
}
|
|
58407
|
+
/** The stored body for this exact URL, decoded; an undecodable one is deleted (not eviction). */
|
|
58408
|
+
decodeCached(url, ttl) {
|
|
58409
|
+
const entry = this.bodyCache.validEntry(url, ttl);
|
|
58410
|
+
if (!entry)
|
|
58411
|
+
return undefined;
|
|
58412
|
+
const paywall = decodePaywall(entry.body);
|
|
58413
|
+
if (!paywall) {
|
|
58414
|
+
logger.warn(`Deleting an undecodable cached paywall body for ${url}`);
|
|
58415
|
+
this.bodyCache.remove(url);
|
|
58416
|
+
}
|
|
58417
|
+
return paywall;
|
|
58418
|
+
}
|
|
58419
|
+
fetchPaywallText(url) {
|
|
58420
|
+
if (storageService.getNamiConfig()?.namiCommands?.includes(SIMULATE_PAYWALL_FETCH_FAILURE)) {
|
|
58421
|
+
logger.warn(`${SIMULATE_PAYWALL_FETCH_FAILURE}: failing the fetch for ${url} before the network`);
|
|
58422
|
+
return Promise.reject(new RetryExhaustedError(url, 0));
|
|
58423
|
+
}
|
|
58424
|
+
return fetchTextWithCdnRetry(url);
|
|
58020
58425
|
}
|
|
58021
58426
|
validatePaywalls(paywalls) {
|
|
58022
58427
|
return paywalls.filter((paywall) => {
|
|
@@ -58041,6 +58446,33 @@ class PaywallRepository {
|
|
|
58041
58446
|
}
|
|
58042
58447
|
}
|
|
58043
58448
|
PaywallRepository.instance = new PaywallRepository();
|
|
58449
|
+
function decodePaywall(text) {
|
|
58450
|
+
try {
|
|
58451
|
+
const value = JSON.parse(text);
|
|
58452
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
58453
|
+
}
|
|
58454
|
+
catch {
|
|
58455
|
+
return undefined;
|
|
58456
|
+
}
|
|
58457
|
+
}
|
|
58458
|
+
function withProvenance(paywall, fetchSource, contentHash) {
|
|
58459
|
+
return { ...paywall, fetchSource, ...(contentHash ? { contentHash } : {}) };
|
|
58460
|
+
}
|
|
58461
|
+
/** Every paywall URL a bundled (anonymous-format) rule names, across all of its segments. */
|
|
58462
|
+
function bundledPaywallUrls(campaignRules) {
|
|
58463
|
+
const urls = [];
|
|
58464
|
+
for (const rule of campaignRules) {
|
|
58465
|
+
for (const segment of rule.segments ?? []) {
|
|
58466
|
+
if (segment.paywall_url)
|
|
58467
|
+
urls.push(segment.paywall_url);
|
|
58468
|
+
if (segment.page_urls)
|
|
58469
|
+
urls.push(...Object.values(segment.page_urls));
|
|
58470
|
+
if (segment.flow?.page_urls)
|
|
58471
|
+
urls.push(...Object.values(segment.flow.page_urls));
|
|
58472
|
+
}
|
|
58473
|
+
}
|
|
58474
|
+
return urls;
|
|
58475
|
+
}
|
|
58044
58476
|
|
|
58045
58477
|
class ConfigRepository {
|
|
58046
58478
|
/**
|
|
@@ -61841,7 +62273,10 @@ class NamiRefs {
|
|
|
61841
62273
|
}
|
|
61842
62274
|
}
|
|
61843
62275
|
if (initconfig?.paywalls) {
|
|
61844
|
-
storageService.setPaywalls(INITIAL_PAYWALLS, initconfig.paywalls);
|
|
62276
|
+
storageService.setPaywalls(INITIAL_PAYWALLS, initconfig.paywalls.map((paywall) => ({ ...paywall, fetchSource: "initialConfig" })));
|
|
62277
|
+
if (initconfig.campaign_rules?.length) {
|
|
62278
|
+
PaywallRepository.instance.seedFromInitialConfig(initconfig.campaign_rules, initconfig.paywalls);
|
|
62279
|
+
}
|
|
61845
62280
|
}
|
|
61846
62281
|
if (initconfig?.products) {
|
|
61847
62282
|
storageService.setProducts(INITIAL_PRODUCTS, initconfig.products);
|
|
@@ -61908,7 +62343,7 @@ class NamiRefs {
|
|
|
61908
62343
|
const paywallRepo = PaywallRepository.instance;
|
|
61909
62344
|
const startTime = Date.now();
|
|
61910
62345
|
const campaignRulesStartTime = Date.now();
|
|
61911
|
-
const rawCampaigns = await campaignRepo.fetchCampaignRulesRaw();
|
|
62346
|
+
const { campaigns: rawCampaigns, fromNetwork } = await campaignRepo.fetchCampaignRulesRaw();
|
|
61912
62347
|
const campaignRulesDuration = Date.now() - campaignRulesStartTime;
|
|
61913
62348
|
// Hydrate flow objects: fetch flow JSON from flow.url for campaigns that
|
|
61914
62349
|
// use the URL-based format (4.0.0+ since NAM-3493) instead of inline flow.object
|
|
@@ -61929,24 +62364,35 @@ class NamiRefs {
|
|
|
61929
62364
|
const useIndividual = !campaignRepo.useLegacyPaywallFetch &&
|
|
61930
62365
|
CampaignRuleRepository.hasPaywallUrls(rawCampaigns);
|
|
61931
62366
|
let paywalls;
|
|
61932
|
-
let
|
|
62367
|
+
let outcome = null;
|
|
61933
62368
|
const paywallsStartTime = Date.now();
|
|
61934
|
-
if (useIndividual) {
|
|
62369
|
+
if (useIndividual && !fromNetwork) {
|
|
62370
|
+
// campaign_rules came from the stored copy, so this is not a complete page set: no
|
|
62371
|
+
// resolution and no eviction (api-resilience-and-caching.md § 2.2). The aggregate serves.
|
|
62372
|
+
logger.info("Paywall resolution skipped: campaign_rules were served from the stored copy");
|
|
62373
|
+
paywalls = getApiPaywalls();
|
|
62374
|
+
}
|
|
62375
|
+
else if (useIndividual) {
|
|
61935
62376
|
const urls = CampaignRuleRepository.extractPaywallUrls(rawCampaigns);
|
|
61936
|
-
|
|
61937
|
-
paywalls =
|
|
61938
|
-
|
|
62377
|
+
outcome = await paywallRepo.resolvePaywallsByUrls(urls);
|
|
62378
|
+
paywalls = outcome.paywalls;
|
|
62379
|
+
paywallBodyCache.reconcile(new Set(urls));
|
|
61939
62380
|
}
|
|
61940
62381
|
else {
|
|
61941
62382
|
paywalls = await paywallRepo.fetchPaywalls();
|
|
61942
62383
|
}
|
|
61943
62384
|
const paywallsDuration = Date.now() - paywallsStartTime;
|
|
61944
62385
|
const totalDuration = Date.now() - startTime;
|
|
61945
|
-
if (
|
|
61946
|
-
|
|
61947
|
-
|
|
62386
|
+
if (!useIndividual || outcome) {
|
|
62387
|
+
// The line every SDK emits once per resolution; the paywall-cache e2e suites parse it.
|
|
62388
|
+
const cacheFields = outcome
|
|
62389
|
+
? `, urls=${outcome.urls}, hashed=${outcome.hashed}, cache_hits=${outcome.cacheHits}, ` +
|
|
62390
|
+
`fetched=${outcome.fetched}, cache_after_fetch_failure=${outcome.cacheAfterFetchFailure}, failed=${outcome.failed}`
|
|
62391
|
+
: "";
|
|
62392
|
+
logger.debug(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
|
|
62393
|
+
`count=${paywalls.length}${cacheFields}, total=${totalDuration}ms, ` +
|
|
61948
62394
|
`campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms` +
|
|
61949
|
-
`${
|
|
62395
|
+
`${outcome ? `, retry_count=${outcome.retryCount}` : ""}`);
|
|
61950
62396
|
}
|
|
61951
62397
|
return campaignRepo.finalizeCampaignRules(rawCampaigns, paywalls);
|
|
61952
62398
|
}
|
|
@@ -62120,6 +62566,8 @@ class Nami {
|
|
|
62120
62566
|
static async reset() {
|
|
62121
62567
|
var _a;
|
|
62122
62568
|
storageService.clearAll();
|
|
62569
|
+
// The per-URL paywall body cache keeps its own prefixed keys and an in-memory index (NAM-4357).
|
|
62570
|
+
paywallBodyCache.purgeAll();
|
|
62123
62571
|
clearInMemoryAnonymousMode();
|
|
62124
62572
|
__classPrivateFieldSet(Nami.instance, _Nami_isInitialized, false, "f");
|
|
62125
62573
|
__classPrivateFieldGet((_a = Nami.instance), _Nami_removeForegroundListener, "f")?.call(_a);
|
|
@@ -66221,4 +66669,4 @@ var internal = /*#__PURE__*/Object.freeze({
|
|
|
66221
66669
|
NamiPurchaseManager: NamiPurchaseManager
|
|
66222
66670
|
});
|
|
66223
66671
|
|
|
66224
|
-
export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_TRAFFIC_OBSERVER, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_NOT_AVAILABLE, CONFIG_API_TIMEOUT_LIMIT, CONFIG_MAX_ATTEMPTS, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEFAULT_JOURNEY_SETTINGS, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$1 as NamiPurchaseManager, NamiRefs, NamiReservedActions, NotFoundError, PAYWALL_ACTION_EVENT, PLATFORM_ID_REQUIRED, PRODUCTION, PaywallManagerEvents, PaywallRepository, PaywallState, PlacementLabelResolver, PlatformIDRequiredError, ProductRepository, RECONFIG_SUCCESS, RESUME_THROTTLE_MS, RetryLimitExceededError, SDKNotInitializedError, SDK_NOT_INITIALIZED, SERVER_NAMI_ENTITLEMENTS, SESSION_REQUIRED, SHOULD_SHOW_LOADING_INDICATOR, SIMULATE_CONFIG_FETCH_FAILURE, SKU_TEXT_REGEX, SMART_TEXT_PATTERN, STARTUP_TELEMETRY, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, TRANSPORT_FAILURE_STATUS, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, NamiProfileManager$1 as _NamiProfileManager, internal as _internal, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, didReceiveResponse, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getJourneySettings, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerAPITrafficObserver, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };
|
|
66672
|
+
export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_TRAFFIC_OBSERVER, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_CACHE_TTL_PREFIX, CAMPAIGN_NOT_AVAILABLE, CONFIG_API_TIMEOUT_LIMIT, CONFIG_MAX_ATTEMPTS, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEFAULT_JOURNEY_SETTINGS, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$1 as NamiPurchaseManager, NamiRefs, NamiReservedActions, NotFoundError, PAYWALL_ACTION_EVENT, PLATFORM_ID_REQUIRED, PRODUCTION, PaywallManagerEvents, PaywallRepository, PaywallState, PlacementLabelResolver, PlatformIDRequiredError, ProductRepository, RECONFIG_SUCCESS, RESUME_THROTTLE_MS, RetryLimitExceededError, SDKNotInitializedError, SDK_NOT_INITIALIZED, SERVER_NAMI_ENTITLEMENTS, SESSION_REQUIRED, SHOULD_SHOW_LOADING_INDICATOR, SIMULATE_CONFIG_FETCH_FAILURE, SIMULATE_PAYWALL_FETCH_FAILURE, SKU_TEXT_REGEX, SMART_TEXT_PATTERN, STARTUP_TELEMETRY, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, TRANSPORT_FAILURE_STATUS, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, NamiProfileManager$1 as _NamiProfileManager, internal as _internal, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, didReceiveResponse, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getJourneySettings, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerAPITrafficObserver, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };
|