@namiml/sdk-core 3.4.12-dev.202609252349 → 3.4.12-dev.202609281859
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 +732 -268
- package/dist/index.d.ts +159 -14
- package/dist/index.mjs +733 -269
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -106,7 +106,7 @@ const {
|
|
|
106
106
|
// version — stamped by scripts/version.sh
|
|
107
107
|
NAMI_SDK_VERSION: exports.NAMI_SDK_VERSION = "3.4.12",
|
|
108
108
|
// full package version including dev suffix — stamped by scripts/version.sh
|
|
109
|
-
NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.
|
|
109
|
+
NAMI_SDK_PACKAGE_VERSION: exports.NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609281859",
|
|
110
110
|
// environments
|
|
111
111
|
PRODUCTION: exports.PRODUCTION = "production", DEVELOPMENT: exports.DEVELOPMENT = "development",
|
|
112
112
|
// error messages
|
|
@@ -119,7 +119,13 @@ API_VERSION: exports.API_VERSION = "v3", BASE_URL_PATH: exports.BASE_URL_PATH =
|
|
|
119
119
|
EXTENDED_CLIENT_INFO_PREFIX: exports.EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER: exports.EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS: exports.VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS: exports.LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC: exports.LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER: exports.API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY: exports.STARTUP_TELEMETRY = "startupTelemetry",
|
|
120
120
|
// Test hook: every platform-config fetch attempt fails before the request leaves.
|
|
121
121
|
// Same namiCommand as Apple's and Android's, so the shared first-run flows drive it.
|
|
122
|
-
SIMULATE_CONFIG_FETCH_FAILURE: exports.SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure",
|
|
122
|
+
SIMULATE_CONFIG_FETCH_FAILURE: exports.SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure",
|
|
123
|
+
// Test hook: every per-paywall body fetch fails before the request leaves, so the body cache's
|
|
124
|
+
// failure rows can be exercised without a proxy. Same namiCommand as Apple's and Android's (NAM-4357).
|
|
125
|
+
SIMULATE_PAYWALL_FETCH_FAILURE: exports.SIMULATE_PAYWALL_FETCH_FAILURE = "simulatePaywallFetchFailure",
|
|
126
|
+
// `campaignCacheTTL=<seconds>` overrides the config's campaign_cache_ttl — Apple's command name.
|
|
127
|
+
// It lets the paywall-cache e2e suite pin TTL 3600 / 1 / 0 per load (NAM-4357).
|
|
128
|
+
CAMPAIGN_CACHE_TTL_PREFIX: exports.CAMPAIGN_CACHE_TTL_PREFIX = "campaignCacheTTL=", EXTENDED_PLATFORM: exports.EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION: exports.EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT: exports.API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC: exports.API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT: exports.API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT: exports.DEVICE_API_TIMEOUT_LIMIT = 2000,
|
|
123
129
|
// Platform-config attempts when no initial config was supplied (Android's
|
|
124
130
|
// CONFIG_MAX_ATTEMPTS; Apple's retry(maxAttempts: 3)).
|
|
125
131
|
CONFIG_MAX_ATTEMPTS: exports.CONFIG_MAX_ATTEMPTS = 3,
|
|
@@ -3059,6 +3065,206 @@ function uniqBy(array, iteratee) {
|
|
|
3059
3065
|
return (array && array.length) ? baseUniq(array, baseIteratee(iteratee)) : [];
|
|
3060
3066
|
}
|
|
3061
3067
|
|
|
3068
|
+
/** Storage key of an entry's raw body; the exact URL follows the prefix. */
|
|
3069
|
+
const PAYWALL_BODY_KEY_PREFIX = "api_paywall_body:";
|
|
3070
|
+
/**
|
|
3071
|
+
* Storage key of an entry's metadata. Written AFTER the body, so it is the commit point: an entry
|
|
3072
|
+
* is visible only once both are durable, and a body with no metadata is an interrupted write.
|
|
3073
|
+
*/
|
|
3074
|
+
const PAYWALL_BODY_META_KEY_PREFIX = "api_paywall_body_meta:";
|
|
3075
|
+
const DEFAULT_MAX_BODY_ENTRIES = 500;
|
|
3076
|
+
class PaywallBodyCache {
|
|
3077
|
+
constructor(storage = () => getPlatformAdapters().storage, maxEntries = DEFAULT_MAX_BODY_ENTRIES) {
|
|
3078
|
+
this.storage = storage;
|
|
3079
|
+
this.maxEntries = maxEntries;
|
|
3080
|
+
/** Set under `campaign_cache_ttl == 0`: entries live here, in this process only. */
|
|
3081
|
+
this.memory = null;
|
|
3082
|
+
/** Metadata of every persisted entry, loaded once from the storage adapter. */
|
|
3083
|
+
this.index = null;
|
|
3084
|
+
}
|
|
3085
|
+
get count() {
|
|
3086
|
+
return this.memory ? this.memory.size : this.loadIndex().size;
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Applies `campaign_cache_ttl` at the start of a resolution. `null` keeps everything, `n > 0`
|
|
3090
|
+
* purges entries whose own age is `>= n` seconds, and `0` purges the persistent store and keeps
|
|
3091
|
+
* this process's entries in memory only.
|
|
3092
|
+
*/
|
|
3093
|
+
evaluateTtl(ttl, now = Date.now()) {
|
|
3094
|
+
if (ttl === 0) {
|
|
3095
|
+
if (this.memory)
|
|
3096
|
+
return;
|
|
3097
|
+
this.dropPersisted([...this.loadIndex().keys()]);
|
|
3098
|
+
this.memory = new Map();
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
// Leaving TTL 0 goes back to the persistent store; the process-local entries are dropped.
|
|
3102
|
+
this.memory = null;
|
|
3103
|
+
if (ttl === null)
|
|
3104
|
+
return;
|
|
3105
|
+
const expired = [...this.loadIndex().values()].filter((meta) => isExpired(meta, ttl, now));
|
|
3106
|
+
this.dropPersisted(expired.map((meta) => meta.url));
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* The entry stored under this exact URL, or null when there is none, it is past `ttl`, or its
|
|
3110
|
+
* body is missing (a half-written entry, which is deleted here).
|
|
3111
|
+
*/
|
|
3112
|
+
validEntry(url, ttl, now = Date.now()) {
|
|
3113
|
+
if (this.memory)
|
|
3114
|
+
return this.memory.get(url) ?? null;
|
|
3115
|
+
const meta = this.loadIndex().get(url);
|
|
3116
|
+
if (!meta)
|
|
3117
|
+
return null;
|
|
3118
|
+
if (ttl !== null && ttl > 0 && isExpired(meta, ttl, now)) {
|
|
3119
|
+
this.dropPersisted([url]);
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
const body = this.read(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3123
|
+
if (body === null) {
|
|
3124
|
+
this.dropPersisted([url]);
|
|
3125
|
+
return null;
|
|
3126
|
+
}
|
|
3127
|
+
return { meta, body };
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* The stored body for this exact URL, read straight from storage rather than through the index,
|
|
3131
|
+
* with no TTL check. This is what rebuilds the aggregate `api_paywalls`, so it also sees an entry
|
|
3132
|
+
* a host wrote after the index was loaded (the web test apps inject local test paywalls that way).
|
|
3133
|
+
*/
|
|
3134
|
+
bodyFor(url) {
|
|
3135
|
+
if (this.memory)
|
|
3136
|
+
return this.memory.get(url)?.body ?? null;
|
|
3137
|
+
return this.read(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3138
|
+
}
|
|
3139
|
+
/** Stores a body under its exact URL. Returns false when the write did not land (quota, I/O). */
|
|
3140
|
+
store(url, contentHash, body, now = Date.now()) {
|
|
3141
|
+
const meta = { url, savedAt: now, ...(contentHash ? { contentHash } : {}) };
|
|
3142
|
+
if (this.memory) {
|
|
3143
|
+
this.memory.set(url, { meta, body });
|
|
3144
|
+
return true;
|
|
3145
|
+
}
|
|
3146
|
+
const index = this.loadIndex();
|
|
3147
|
+
const metaJson = JSON.stringify(meta);
|
|
3148
|
+
// Body first, metadata last. Each write is verified by reading it back: the web adapter
|
|
3149
|
+
// swallows a quota error rather than throwing, so a silent no-op is the common failure.
|
|
3150
|
+
if (this.write(PAYWALL_BODY_KEY_PREFIX + url, body) &&
|
|
3151
|
+
this.write(PAYWALL_BODY_META_KEY_PREFIX + url, metaJson)) {
|
|
3152
|
+
index.set(url, meta);
|
|
3153
|
+
return true;
|
|
3154
|
+
}
|
|
3155
|
+
logger.warn(`Paywall body cache: could not store ${url}; it will be fetched next time`);
|
|
3156
|
+
this.dropPersisted([url]);
|
|
3157
|
+
return false;
|
|
3158
|
+
}
|
|
3159
|
+
/** Stores a bundled body unless an entry already exists — a body the SDK fetched always wins. */
|
|
3160
|
+
seedIfAbsent(url, contentHash, body, now = Date.now()) {
|
|
3161
|
+
const exists = this.memory ? this.memory.has(url) : this.loadIndex().has(url);
|
|
3162
|
+
return exists ? false : this.store(url, contentHash, body, now);
|
|
3163
|
+
}
|
|
3164
|
+
/** Deletes one entry — used for an undecodable body. Not eviction. */
|
|
3165
|
+
remove(url) {
|
|
3166
|
+
if (this.memory)
|
|
3167
|
+
this.memory.delete(url);
|
|
3168
|
+
else
|
|
3169
|
+
this.dropPersisted([url]);
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Eviction, run once per resolution after a complete `campaign_rules` fetch: delete every entry
|
|
3173
|
+
* whose URL this resolution did not name, then cap the store at `maxEntries`, oldest first.
|
|
3174
|
+
*/
|
|
3175
|
+
reconcile(currentUrls) {
|
|
3176
|
+
const metas = this.memory
|
|
3177
|
+
? [...this.memory.values()].map((entry) => entry.meta)
|
|
3178
|
+
: [...this.loadIndex().values()];
|
|
3179
|
+
const unnamed = metas.filter((meta) => !currentUrls.has(meta.url)).map((meta) => meta.url);
|
|
3180
|
+
const kept = metas.filter((meta) => currentUrls.has(meta.url)).sort((a, b) => a.savedAt - b.savedAt);
|
|
3181
|
+
const overCap = kept.slice(0, Math.max(0, kept.length - this.maxEntries)).map((meta) => meta.url);
|
|
3182
|
+
[...unnamed, ...overCap].forEach((url) => this.remove(url));
|
|
3183
|
+
}
|
|
3184
|
+
/** Empties the store — persisted and in-memory entries alike. Called by `Nami.reset()`. */
|
|
3185
|
+
purgeAll() {
|
|
3186
|
+
this.dropPersisted([...this.loadIndex().keys()]);
|
|
3187
|
+
this.memory?.clear();
|
|
3188
|
+
}
|
|
3189
|
+
/** Builds the index from the metadata keys, clearing away half-written and unreadable entries. */
|
|
3190
|
+
loadIndex() {
|
|
3191
|
+
if (this.index)
|
|
3192
|
+
return this.index;
|
|
3193
|
+
this.index = new Map();
|
|
3194
|
+
let keys;
|
|
3195
|
+
try {
|
|
3196
|
+
keys = this.storage().getAllKeys();
|
|
3197
|
+
}
|
|
3198
|
+
catch (error) {
|
|
3199
|
+
logger.warn(`Paywall body cache: storage unavailable, caching nothing (${error})`);
|
|
3200
|
+
return this.index;
|
|
3201
|
+
}
|
|
3202
|
+
const bodies = new Set(keys.filter((key) => key.startsWith(PAYWALL_BODY_KEY_PREFIX)).map((key) => key.slice(PAYWALL_BODY_KEY_PREFIX.length)));
|
|
3203
|
+
const stale = [];
|
|
3204
|
+
for (const key of keys) {
|
|
3205
|
+
if (!key.startsWith(PAYWALL_BODY_META_KEY_PREFIX))
|
|
3206
|
+
continue;
|
|
3207
|
+
const url = key.slice(PAYWALL_BODY_META_KEY_PREFIX.length);
|
|
3208
|
+
const meta = parseMeta(this.read(key));
|
|
3209
|
+
if (meta?.url === url && bodies.has(url))
|
|
3210
|
+
this.index.set(url, meta);
|
|
3211
|
+
else
|
|
3212
|
+
stale.push(url);
|
|
3213
|
+
bodies.delete(url);
|
|
3214
|
+
}
|
|
3215
|
+
// Bodies whose metadata never landed: interrupted writes.
|
|
3216
|
+
this.dropPersisted([...stale, ...bodies]);
|
|
3217
|
+
return this.index;
|
|
3218
|
+
}
|
|
3219
|
+
dropPersisted(urls) {
|
|
3220
|
+
for (const url of urls) {
|
|
3221
|
+
this.index?.delete(url);
|
|
3222
|
+
this.removeKey(PAYWALL_BODY_META_KEY_PREFIX + url);
|
|
3223
|
+
this.removeKey(PAYWALL_BODY_KEY_PREFIX + url);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
read(key) {
|
|
3227
|
+
try {
|
|
3228
|
+
return this.storage().getItem(key);
|
|
3229
|
+
}
|
|
3230
|
+
catch {
|
|
3231
|
+
return null;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
write(key, value) {
|
|
3235
|
+
try {
|
|
3236
|
+
this.storage().setItem(key, value);
|
|
3237
|
+
}
|
|
3238
|
+
catch {
|
|
3239
|
+
return false;
|
|
3240
|
+
}
|
|
3241
|
+
return this.read(key) === value;
|
|
3242
|
+
}
|
|
3243
|
+
removeKey(key) {
|
|
3244
|
+
try {
|
|
3245
|
+
this.storage().removeItem(key);
|
|
3246
|
+
}
|
|
3247
|
+
catch {
|
|
3248
|
+
// Nothing more to do: an entry that cannot be deleted is re-validated on the next read.
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
function isExpired(meta, ttlSeconds, now) {
|
|
3253
|
+
return (now - meta.savedAt) / 1000 >= ttlSeconds;
|
|
3254
|
+
}
|
|
3255
|
+
function parseMeta(raw) {
|
|
3256
|
+
if (raw === null)
|
|
3257
|
+
return null;
|
|
3258
|
+
try {
|
|
3259
|
+
const meta = JSON.parse(raw);
|
|
3260
|
+
return typeof meta?.url === "string" && typeof meta?.savedAt === "number" ? meta : null;
|
|
3261
|
+
}
|
|
3262
|
+
catch {
|
|
3263
|
+
return null;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const paywallBodyCache = new PaywallBodyCache();
|
|
3267
|
+
|
|
3062
3268
|
class StorageService {
|
|
3063
3269
|
constructor() {
|
|
3064
3270
|
this.memoryStore = {};
|
|
@@ -3141,6 +3347,20 @@ class StorageService {
|
|
|
3141
3347
|
}
|
|
3142
3348
|
this.setItem(key, paywalls);
|
|
3143
3349
|
}
|
|
3350
|
+
/**
|
|
3351
|
+
* Writes the aggregate `api_paywalls` as the list of URLs a per-URL resolution produced, rather
|
|
3352
|
+
* than a second copy of their bodies (NAM-4357): the bodies already live in the per-URL body
|
|
3353
|
+
* cache, and `getPaywalls(API_PAYWALLS)` rebuilds the paywall objects from it. The legacy bulk
|
|
3354
|
+
* path still writes whole objects through `setPaywalls`.
|
|
3355
|
+
*/
|
|
3356
|
+
setApiPaywallsFromBodyCache(urls) {
|
|
3357
|
+
const index = { bodyCacheUrls: urls };
|
|
3358
|
+
if (this.getCampaignCacheTtl() === 0) {
|
|
3359
|
+
this.memoryStore[exports.API_PAYWALLS] = JSON.stringify(index);
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
this.setItem(exports.API_PAYWALLS, index);
|
|
3363
|
+
}
|
|
3144
3364
|
getPaywalls(key) {
|
|
3145
3365
|
if (key === exports.INITIAL_PAYWALLS) {
|
|
3146
3366
|
return this.memoryStore[exports.INITIAL_PAYWALLS]
|
|
@@ -3149,8 +3369,10 @@ class StorageService {
|
|
|
3149
3369
|
}
|
|
3150
3370
|
if (key === exports.API_PAYWALLS) {
|
|
3151
3371
|
const memoryValue = this.memoryStore[exports.API_PAYWALLS];
|
|
3152
|
-
if (memoryValue != null)
|
|
3153
|
-
|
|
3372
|
+
if (memoryValue != null) {
|
|
3373
|
+
const parsed = JSON.parse(memoryValue);
|
|
3374
|
+
return isBodyCacheAggregate(parsed) ? paywallsFromBodyCache(parsed.bodyCacheUrls) : parsed;
|
|
3375
|
+
}
|
|
3154
3376
|
let stored = this.getItem(exports.API_PAYWALLS);
|
|
3155
3377
|
if (stored === null)
|
|
3156
3378
|
return null;
|
|
@@ -3162,6 +3384,8 @@ class StorageService {
|
|
|
3162
3384
|
return stored;
|
|
3163
3385
|
}
|
|
3164
3386
|
}
|
|
3387
|
+
if (isBodyCacheAggregate(stored))
|
|
3388
|
+
return paywallsFromBodyCache(stored.bodyCacheUrls);
|
|
3165
3389
|
return this.isLegacyTimestampedCache(stored) ? stored.data : stored;
|
|
3166
3390
|
}
|
|
3167
3391
|
return this.getItem(key);
|
|
@@ -3330,7 +3554,18 @@ class StorageService {
|
|
|
3330
3554
|
this.resetItem(exports.API_CAMPAIGN_RULES);
|
|
3331
3555
|
this.resetItem(exports.API_CAMPAIGN_SESSION_TIMESTAMP);
|
|
3332
3556
|
}
|
|
3557
|
+
/**
|
|
3558
|
+
* The effective `campaign_cache_ttl`: a `campaignCacheTTL=<n>` namiCommand first, then the API
|
|
3559
|
+
* config, then the initial config. Lives here rather than in `utils/config` because this service
|
|
3560
|
+
* needs it to decide where the aggregate caches go, and `utils/config` already imports this one.
|
|
3561
|
+
*/
|
|
3333
3562
|
getCampaignCacheTtl() {
|
|
3563
|
+
const override = this.getNamiConfig()?.namiCommands
|
|
3564
|
+
?.filter((cmd) => cmd.startsWith(exports.CAMPAIGN_CACHE_TTL_PREFIX))
|
|
3565
|
+
.map((cmd) => cmd.slice(exports.CAMPAIGN_CACHE_TTL_PREFIX.length))
|
|
3566
|
+
.find((value) => /^\d+$/.test(value));
|
|
3567
|
+
if (override !== undefined)
|
|
3568
|
+
return parseInt(override, 10);
|
|
3334
3569
|
const config = this.getAppConfig(exports.API_CONFIG)
|
|
3335
3570
|
|| this.getAppConfig(exports.INITIAL_APP_CONFIG);
|
|
3336
3571
|
return config?.campaign_cache_ttl ?? null;
|
|
@@ -3422,6 +3657,28 @@ class StorageService {
|
|
|
3422
3657
|
this.memoryStore = {};
|
|
3423
3658
|
}
|
|
3424
3659
|
}
|
|
3660
|
+
function isBodyCacheAggregate(value) {
|
|
3661
|
+
return typeof value === "object" && value !== null && Array.isArray(value.bodyCacheUrls);
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* The paywalls a per-URL resolution produced, decoded from the body cache. A URL whose entry has
|
|
3665
|
+
* since gone (evicted, purged, or a write that never landed) is simply absent.
|
|
3666
|
+
*/
|
|
3667
|
+
function paywallsFromBodyCache(urls) {
|
|
3668
|
+
const paywalls = [];
|
|
3669
|
+
for (const url of urls) {
|
|
3670
|
+
const body = paywallBodyCache.bodyFor(url);
|
|
3671
|
+
if (body === null)
|
|
3672
|
+
continue;
|
|
3673
|
+
try {
|
|
3674
|
+
paywalls.push(JSON.parse(body));
|
|
3675
|
+
}
|
|
3676
|
+
catch {
|
|
3677
|
+
paywallBodyCache.remove(url);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
return paywalls;
|
|
3681
|
+
}
|
|
3425
3682
|
const storageService = new StorageService();
|
|
3426
3683
|
|
|
3427
3684
|
class RetryLimitExceededError extends Error {
|
|
@@ -6799,11 +7056,8 @@ const hasCapability = (capability) => {
|
|
|
6799
7056
|
|| storageService.getAppConfig(exports.INITIAL_APP_CONFIG);
|
|
6800
7057
|
return appConfig?.capabilities?.includes(capability) ?? false;
|
|
6801
7058
|
};
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|| storageService.getAppConfig(exports.INITIAL_APP_CONFIG);
|
|
6805
|
-
return config?.campaign_cache_ttl ?? null;
|
|
6806
|
-
};
|
|
7059
|
+
/** The effective `campaign_cache_ttl`, `campaignCacheTTL=<n>` namiCommand first. */
|
|
7060
|
+
const getCampaignCacheTtl = () => storageService.getCampaignCacheTtl();
|
|
6807
7061
|
/**
|
|
6808
7062
|
* The effective global journey settings from the current config, coalescing a missing
|
|
6809
7063
|
* `journey_settings` object — or any missing field — to {@link DEFAULT_JOURNEY_SETTINGS}.
|
|
@@ -7116,211 +7370,6 @@ async function timeoutRequest$1(url, options = {}, timeout) {
|
|
|
7116
7370
|
return response;
|
|
7117
7371
|
}
|
|
7118
7372
|
|
|
7119
|
-
function requirePlatformID(target, propertyKey, descriptor) {
|
|
7120
|
-
const originalMethod = descriptor.value;
|
|
7121
|
-
descriptor.value = function (...args) {
|
|
7122
|
-
if (!this.platformID) {
|
|
7123
|
-
throw new PlatformIDRequiredError();
|
|
7124
|
-
}
|
|
7125
|
-
return originalMethod.apply(this, args);
|
|
7126
|
-
};
|
|
7127
|
-
return descriptor;
|
|
7128
|
-
}
|
|
7129
|
-
function requireDeviceID(target, propertyKey, descriptor) {
|
|
7130
|
-
const originalMethod = descriptor.value;
|
|
7131
|
-
descriptor.value = function (...args) {
|
|
7132
|
-
const device = storageService.getDevice();
|
|
7133
|
-
if (!device?.id) {
|
|
7134
|
-
throw new DeviceIDRequiredError();
|
|
7135
|
-
}
|
|
7136
|
-
return originalMethod.apply(this, args);
|
|
7137
|
-
};
|
|
7138
|
-
return descriptor;
|
|
7139
|
-
}
|
|
7140
|
-
|
|
7141
|
-
class NamiAPI {
|
|
7142
|
-
get deviceID() {
|
|
7143
|
-
return storageService.getDevice()?.id;
|
|
7144
|
-
}
|
|
7145
|
-
constructor() {
|
|
7146
|
-
const config = storageService.getNamiConfig();
|
|
7147
|
-
if (!this.platformID && config) {
|
|
7148
|
-
this.configure(config);
|
|
7149
|
-
}
|
|
7150
|
-
}
|
|
7151
|
-
static configure(config) {
|
|
7152
|
-
this.instance.configure(config);
|
|
7153
|
-
}
|
|
7154
|
-
configure(config) {
|
|
7155
|
-
this.baseURL = getBaseUrl(config.namiCommands);
|
|
7156
|
-
this.platformID = config.appPlatformID;
|
|
7157
|
-
}
|
|
7158
|
-
/**
|
|
7159
|
-
* Clear cached baseURL and platformID so the next `configure()` call
|
|
7160
|
-
* re-derives them from fresh storage. Called from `Nami.reset()`.
|
|
7161
|
-
*
|
|
7162
|
-
* Without this, the NamiAPI singleton holds the previous lifecycle's
|
|
7163
|
-
* platform/base URL even after storage has been wiped — any in-flight
|
|
7164
|
-
* or queued request after reset would hit the old endpoint.
|
|
7165
|
-
*/
|
|
7166
|
-
static reset() {
|
|
7167
|
-
this.instance.reset();
|
|
7168
|
-
}
|
|
7169
|
-
reset() {
|
|
7170
|
-
this.baseURL = undefined;
|
|
7171
|
-
this.platformID = undefined;
|
|
7172
|
-
}
|
|
7173
|
-
async login(externalId) {
|
|
7174
|
-
if (!externalId) {
|
|
7175
|
-
throw new ExternalIDRequiredError();
|
|
7176
|
-
}
|
|
7177
|
-
const path = `device/${this.deviceID}/login/`;
|
|
7178
|
-
const data = { external_id: externalId };
|
|
7179
|
-
return await this.requestBodyAPI(path, data, 'PUT');
|
|
7180
|
-
}
|
|
7181
|
-
async logout() {
|
|
7182
|
-
const path = `device/${this.deviceID}/logout/`;
|
|
7183
|
-
return await this.requestBodyAPI(path, {}, 'PUT');
|
|
7184
|
-
}
|
|
7185
|
-
async startSession(sessionStartTime) {
|
|
7186
|
-
const path = `device/${this.deviceID}/session/`;
|
|
7187
|
-
const resp = await this.requestBodyAPI(path, {
|
|
7188
|
-
start_time: sessionStartTime,
|
|
7189
|
-
app_env: "production",
|
|
7190
|
-
});
|
|
7191
|
-
storageService.setSessionCounter();
|
|
7192
|
-
if (resp.id) {
|
|
7193
|
-
storageService.setSessionId(resp.id);
|
|
7194
|
-
}
|
|
7195
|
-
}
|
|
7196
|
-
async postImpression(options) {
|
|
7197
|
-
const path = `device/${this.deviceID}/impression/`;
|
|
7198
|
-
const resp = await this.requestBodyAPI(path, options, "POST", true);
|
|
7199
|
-
if (resp.id) {
|
|
7200
|
-
storageService.setLastImpressionId(resp.id);
|
|
7201
|
-
// Notify the flow-completion engine (no-op unless a flow is open) so it
|
|
7202
|
-
// can bind this impression to the current screen's completion context.
|
|
7203
|
-
NamiAPI.impressionListener?.(resp.id);
|
|
7204
|
-
}
|
|
7205
|
-
}
|
|
7206
|
-
/**
|
|
7207
|
-
* Reports a flow completion result against a prior impression
|
|
7208
|
-
* (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
|
|
7209
|
-
* impression must already exist. `keepalive` so a dismiss-triggered PATCH
|
|
7210
|
-
* survives page unload on web.
|
|
7211
|
-
*/
|
|
7212
|
-
async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
|
|
7213
|
-
const path = `device/${this.deviceID}/impression/${impressionId}/`;
|
|
7214
|
-
await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
|
|
7215
|
-
}
|
|
7216
|
-
async postConversion(options) {
|
|
7217
|
-
const path = `device/${this.deviceID}/transaction/`;
|
|
7218
|
-
await this.requestBodyAPI(path, options, "POST", true);
|
|
7219
|
-
}
|
|
7220
|
-
async purchaseValidation(options) {
|
|
7221
|
-
const path = `device/${this.deviceID}/purchase_event/`;
|
|
7222
|
-
await this.requestBodyAPI(path, options, "PUT");
|
|
7223
|
-
}
|
|
7224
|
-
async fetchAPI(path, timeout, retries) {
|
|
7225
|
-
const url = `${this.baseURL}/${this.platformID}/` + path;
|
|
7226
|
-
return await withRetry(url, undefined, timeout, retries);
|
|
7227
|
-
}
|
|
7228
|
-
async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
|
|
7229
|
-
const url = `${this.baseURL}/${this.platformID}/` + path;
|
|
7230
|
-
const options = {
|
|
7231
|
-
method,
|
|
7232
|
-
headers: {
|
|
7233
|
-
"Content-Type": "application/json",
|
|
7234
|
-
},
|
|
7235
|
-
body: JSON.stringify(bodyData),
|
|
7236
|
-
keepalive,
|
|
7237
|
-
};
|
|
7238
|
-
return await withRetry(url, options, timeout, retries);
|
|
7239
|
-
}
|
|
7240
|
-
}
|
|
7241
|
-
NamiAPI.instance = new NamiAPI();
|
|
7242
|
-
__decorate([
|
|
7243
|
-
requireDeviceID
|
|
7244
|
-
], NamiAPI.prototype, "login", null);
|
|
7245
|
-
__decorate([
|
|
7246
|
-
requireDeviceID
|
|
7247
|
-
], NamiAPI.prototype, "logout", null);
|
|
7248
|
-
__decorate([
|
|
7249
|
-
requireDeviceID
|
|
7250
|
-
], NamiAPI.prototype, "startSession", null);
|
|
7251
|
-
__decorate([
|
|
7252
|
-
requireDeviceID
|
|
7253
|
-
], NamiAPI.prototype, "postImpression", null);
|
|
7254
|
-
__decorate([
|
|
7255
|
-
requireDeviceID
|
|
7256
|
-
], NamiAPI.prototype, "patchImpressionCompletion", null);
|
|
7257
|
-
__decorate([
|
|
7258
|
-
requireDeviceID
|
|
7259
|
-
], NamiAPI.prototype, "postConversion", null);
|
|
7260
|
-
__decorate([
|
|
7261
|
-
requireDeviceID
|
|
7262
|
-
], NamiAPI.prototype, "purchaseValidation", null);
|
|
7263
|
-
__decorate([
|
|
7264
|
-
requirePlatformID
|
|
7265
|
-
], NamiAPI.prototype, "fetchAPI", null);
|
|
7266
|
-
__decorate([
|
|
7267
|
-
requirePlatformID
|
|
7268
|
-
], NamiAPI.prototype, "requestBodyAPI", null);
|
|
7269
|
-
|
|
7270
|
-
function isNamiFlowCampaign(campaign) {
|
|
7271
|
-
return campaign?.flow != null;
|
|
7272
|
-
}
|
|
7273
|
-
/**
|
|
7274
|
-
* @enum NamiCampaignRuleType
|
|
7275
|
-
* Types of Nami campaigns
|
|
7276
|
-
*/
|
|
7277
|
-
exports.NamiCampaignRuleType = void 0;
|
|
7278
|
-
(function (NamiCampaignRuleType) {
|
|
7279
|
-
NamiCampaignRuleType["DEFAULT"] = "default";
|
|
7280
|
-
NamiCampaignRuleType["LABEL"] = "label";
|
|
7281
|
-
NamiCampaignRuleType["UNKNOWN"] = "unknown";
|
|
7282
|
-
NamiCampaignRuleType["URL"] = "url";
|
|
7283
|
-
})(exports.NamiCampaignRuleType || (exports.NamiCampaignRuleType = {}));
|
|
7284
|
-
// NamiCampaignManagerStatic interface + const proxy live in
|
|
7285
|
-
// `src/managers/campaign.ts`. They must stay out of this file: PaywallState
|
|
7286
|
-
// type-imports from here, and pulling in the manager class triggers a
|
|
7287
|
-
// constructor chain that runs before PaywallState finishes loading. See NAM-1207.
|
|
7288
|
-
|
|
7289
|
-
/**
|
|
7290
|
-
* Get initial paywalls from storage service.
|
|
7291
|
-
* @returns An array of initial paywalls from storage service.
|
|
7292
|
-
*/
|
|
7293
|
-
const getInitialPaywalls = () => {
|
|
7294
|
-
return storageService.getPaywalls(exports.INITIAL_PAYWALLS) ?? [];
|
|
7295
|
-
};
|
|
7296
|
-
/**
|
|
7297
|
-
* Get all API paywalls from storage service.
|
|
7298
|
-
* @returns An array of API paywalls from storage service.
|
|
7299
|
-
*/
|
|
7300
|
-
const getApiPaywalls = () => {
|
|
7301
|
-
return storageService.getPaywalls(exports.API_PAYWALLS) ?? [];
|
|
7302
|
-
};
|
|
7303
|
-
/**
|
|
7304
|
-
* Get all paywalls from both API and Initial sources, ensuring uniqueness.
|
|
7305
|
-
* This function combines paywalls from both sources and filters out duplicates.
|
|
7306
|
-
* API Data takes precedence over Initial Data.
|
|
7307
|
-
*
|
|
7308
|
-
* @returns A combined list of unique paywalls based on both API and Initial data.
|
|
7309
|
-
*/
|
|
7310
|
-
const allPaywalls = () => {
|
|
7311
|
-
const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
|
|
7312
|
-
return uniqBy(paywalls, "id");
|
|
7313
|
-
};
|
|
7314
|
-
const getPaywall = (paywallId) => {
|
|
7315
|
-
const paywalls = allPaywalls();
|
|
7316
|
-
return paywalls.find((paywall) => paywall.id === paywallId);
|
|
7317
|
-
};
|
|
7318
|
-
const hasAllPaywalls = (paywallIds) => {
|
|
7319
|
-
const paywalls = allPaywalls();
|
|
7320
|
-
const paywallIdSet = new Set(paywalls.map(p => p.id));
|
|
7321
|
-
return paywallIds.every(id => paywallIdSet.has(id));
|
|
7322
|
-
};
|
|
7323
|
-
|
|
7324
7373
|
const globalScope = typeof globalThis !== 'undefined' ? globalThis :
|
|
7325
7374
|
typeof self !== 'undefined' ? self :
|
|
7326
7375
|
typeof global !== 'undefined' ? global :
|
|
@@ -7544,6 +7593,227 @@ function uuidFromSplitPosition(namiCommands) {
|
|
|
7544
7593
|
return value && !isNaN(numericalValue) ? generateUuidForSplitPosition(numericalValue) : undefined;
|
|
7545
7594
|
}
|
|
7546
7595
|
|
|
7596
|
+
function requirePlatformID(target, propertyKey, descriptor) {
|
|
7597
|
+
const originalMethod = descriptor.value;
|
|
7598
|
+
descriptor.value = function (...args) {
|
|
7599
|
+
if (!this.platformID) {
|
|
7600
|
+
throw new PlatformIDRequiredError();
|
|
7601
|
+
}
|
|
7602
|
+
return originalMethod.apply(this, args);
|
|
7603
|
+
};
|
|
7604
|
+
return descriptor;
|
|
7605
|
+
}
|
|
7606
|
+
function requireDeviceID(target, propertyKey, descriptor) {
|
|
7607
|
+
const originalMethod = descriptor.value;
|
|
7608
|
+
descriptor.value = function (...args) {
|
|
7609
|
+
const device = storageService.getDevice();
|
|
7610
|
+
if (!device?.id) {
|
|
7611
|
+
throw new DeviceIDRequiredError();
|
|
7612
|
+
}
|
|
7613
|
+
return originalMethod.apply(this, args);
|
|
7614
|
+
};
|
|
7615
|
+
return descriptor;
|
|
7616
|
+
}
|
|
7617
|
+
|
|
7618
|
+
class NamiAPI {
|
|
7619
|
+
get deviceID() {
|
|
7620
|
+
return storageService.getDevice()?.id;
|
|
7621
|
+
}
|
|
7622
|
+
constructor() {
|
|
7623
|
+
const config = storageService.getNamiConfig();
|
|
7624
|
+
if (!this.platformID && config) {
|
|
7625
|
+
this.configure(config);
|
|
7626
|
+
}
|
|
7627
|
+
}
|
|
7628
|
+
static configure(config) {
|
|
7629
|
+
this.instance.configure(config);
|
|
7630
|
+
}
|
|
7631
|
+
configure(config) {
|
|
7632
|
+
this.baseURL = getBaseUrl(config.namiCommands);
|
|
7633
|
+
this.platformID = config.appPlatformID;
|
|
7634
|
+
}
|
|
7635
|
+
/**
|
|
7636
|
+
* Clear cached baseURL and platformID so the next `configure()` call
|
|
7637
|
+
* re-derives them from fresh storage. Called from `Nami.reset()`.
|
|
7638
|
+
*
|
|
7639
|
+
* Without this, the NamiAPI singleton holds the previous lifecycle's
|
|
7640
|
+
* platform/base URL even after storage has been wiped — any in-flight
|
|
7641
|
+
* or queued request after reset would hit the old endpoint.
|
|
7642
|
+
*/
|
|
7643
|
+
static reset() {
|
|
7644
|
+
this.instance.reset();
|
|
7645
|
+
}
|
|
7646
|
+
reset() {
|
|
7647
|
+
this.baseURL = undefined;
|
|
7648
|
+
this.platformID = undefined;
|
|
7649
|
+
}
|
|
7650
|
+
async login(externalId) {
|
|
7651
|
+
if (!externalId) {
|
|
7652
|
+
throw new ExternalIDRequiredError();
|
|
7653
|
+
}
|
|
7654
|
+
const path = `device/${this.deviceID}/login/`;
|
|
7655
|
+
const data = { external_id: externalId };
|
|
7656
|
+
return await this.requestBodyAPI(path, data, 'PUT');
|
|
7657
|
+
}
|
|
7658
|
+
async logout() {
|
|
7659
|
+
const path = `device/${this.deviceID}/logout/`;
|
|
7660
|
+
return await this.requestBodyAPI(path, {}, 'PUT');
|
|
7661
|
+
}
|
|
7662
|
+
async startSession(sessionStartTime) {
|
|
7663
|
+
const path = `device/${this.deviceID}/session/`;
|
|
7664
|
+
const resp = await this.requestBodyAPI(path, {
|
|
7665
|
+
start_time: sessionStartTime,
|
|
7666
|
+
app_env: "production",
|
|
7667
|
+
});
|
|
7668
|
+
storageService.setSessionCounter();
|
|
7669
|
+
if (resp.id) {
|
|
7670
|
+
storageService.setSessionId(resp.id);
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
async postImpression(options) {
|
|
7674
|
+
const path = `device/${this.deviceID}/impression/`;
|
|
7675
|
+
// Declare the id rather than taking it from the response. Three things depended on that
|
|
7676
|
+
// response before NAM-4421:
|
|
7677
|
+
//
|
|
7678
|
+
// 1. Retries were not idempotent. `withRetry` re-sends on any non-ok response, so a backend
|
|
7679
|
+
// (or a proxy) that created the impression and then returned 5xx got a *second* impression
|
|
7680
|
+
// per retry — up to three rows for one page view, inflating the denominator of paywall
|
|
7681
|
+
// conversion rate.
|
|
7682
|
+
// 2. Until the response landed, `getLastImpressionId()` still held the previous page's id, so
|
|
7683
|
+
// anything snapshotting in that window — including `setPurchaseImpression()` on a buy tap —
|
|
7684
|
+
// copied the wrong page.
|
|
7685
|
+
// 3. When every retry failed, the id was never published at all, so storage kept the previous
|
|
7686
|
+
// page's id and purchases here were attributed to the page before this one.
|
|
7687
|
+
const impressionId = options.id ?? generateUUID();
|
|
7688
|
+
const body = {
|
|
7689
|
+
...options,
|
|
7690
|
+
id: impressionId,
|
|
7691
|
+
impression_date: options.impression_date ?? new Date().toISOString(),
|
|
7692
|
+
};
|
|
7693
|
+
storageService.setLastImpressionId(impressionId);
|
|
7694
|
+
// Notify the flow-completion engine (no-op unless a flow is open) so it
|
|
7695
|
+
// can bind this impression to the current screen's completion context.
|
|
7696
|
+
NamiAPI.impressionListener?.(impressionId);
|
|
7697
|
+
await this.requestBodyAPI(path, body, "POST", true);
|
|
7698
|
+
}
|
|
7699
|
+
/**
|
|
7700
|
+
* Reports a flow completion result against a prior impression
|
|
7701
|
+
* (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
|
|
7702
|
+
* impression must already exist. `keepalive` so a dismiss-triggered PATCH
|
|
7703
|
+
* survives page unload on web.
|
|
7704
|
+
*/
|
|
7705
|
+
async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
|
|
7706
|
+
const path = `device/${this.deviceID}/impression/${impressionId}/`;
|
|
7707
|
+
await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
|
|
7708
|
+
}
|
|
7709
|
+
async postConversion(options) {
|
|
7710
|
+
const path = `device/${this.deviceID}/transaction/`;
|
|
7711
|
+
await this.requestBodyAPI(path, options, "POST", true);
|
|
7712
|
+
}
|
|
7713
|
+
async purchaseValidation(options) {
|
|
7714
|
+
const path = `device/${this.deviceID}/purchase_event/`;
|
|
7715
|
+
await this.requestBodyAPI(path, options, "PUT");
|
|
7716
|
+
}
|
|
7717
|
+
async fetchAPI(path, timeout, retries) {
|
|
7718
|
+
const url = `${this.baseURL}/${this.platformID}/` + path;
|
|
7719
|
+
return await withRetry(url, undefined, timeout, retries);
|
|
7720
|
+
}
|
|
7721
|
+
async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
|
|
7722
|
+
const url = `${this.baseURL}/${this.platformID}/` + path;
|
|
7723
|
+
const options = {
|
|
7724
|
+
method,
|
|
7725
|
+
headers: {
|
|
7726
|
+
"Content-Type": "application/json",
|
|
7727
|
+
},
|
|
7728
|
+
body: JSON.stringify(bodyData),
|
|
7729
|
+
keepalive,
|
|
7730
|
+
};
|
|
7731
|
+
return await withRetry(url, options, timeout, retries);
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
NamiAPI.instance = new NamiAPI();
|
|
7735
|
+
__decorate([
|
|
7736
|
+
requireDeviceID
|
|
7737
|
+
], NamiAPI.prototype, "login", null);
|
|
7738
|
+
__decorate([
|
|
7739
|
+
requireDeviceID
|
|
7740
|
+
], NamiAPI.prototype, "logout", null);
|
|
7741
|
+
__decorate([
|
|
7742
|
+
requireDeviceID
|
|
7743
|
+
], NamiAPI.prototype, "startSession", null);
|
|
7744
|
+
__decorate([
|
|
7745
|
+
requireDeviceID
|
|
7746
|
+
], NamiAPI.prototype, "postImpression", null);
|
|
7747
|
+
__decorate([
|
|
7748
|
+
requireDeviceID
|
|
7749
|
+
], NamiAPI.prototype, "patchImpressionCompletion", null);
|
|
7750
|
+
__decorate([
|
|
7751
|
+
requireDeviceID
|
|
7752
|
+
], NamiAPI.prototype, "postConversion", null);
|
|
7753
|
+
__decorate([
|
|
7754
|
+
requireDeviceID
|
|
7755
|
+
], NamiAPI.prototype, "purchaseValidation", null);
|
|
7756
|
+
__decorate([
|
|
7757
|
+
requirePlatformID
|
|
7758
|
+
], NamiAPI.prototype, "fetchAPI", null);
|
|
7759
|
+
__decorate([
|
|
7760
|
+
requirePlatformID
|
|
7761
|
+
], NamiAPI.prototype, "requestBodyAPI", null);
|
|
7762
|
+
|
|
7763
|
+
function isNamiFlowCampaign(campaign) {
|
|
7764
|
+
return campaign?.flow != null;
|
|
7765
|
+
}
|
|
7766
|
+
/**
|
|
7767
|
+
* @enum NamiCampaignRuleType
|
|
7768
|
+
* Types of Nami campaigns
|
|
7769
|
+
*/
|
|
7770
|
+
exports.NamiCampaignRuleType = void 0;
|
|
7771
|
+
(function (NamiCampaignRuleType) {
|
|
7772
|
+
NamiCampaignRuleType["DEFAULT"] = "default";
|
|
7773
|
+
NamiCampaignRuleType["LABEL"] = "label";
|
|
7774
|
+
NamiCampaignRuleType["UNKNOWN"] = "unknown";
|
|
7775
|
+
NamiCampaignRuleType["URL"] = "url";
|
|
7776
|
+
})(exports.NamiCampaignRuleType || (exports.NamiCampaignRuleType = {}));
|
|
7777
|
+
// NamiCampaignManagerStatic interface + const proxy live in
|
|
7778
|
+
// `src/managers/campaign.ts`. They must stay out of this file: PaywallState
|
|
7779
|
+
// type-imports from here, and pulling in the manager class triggers a
|
|
7780
|
+
// constructor chain that runs before PaywallState finishes loading. See NAM-1207.
|
|
7781
|
+
|
|
7782
|
+
/**
|
|
7783
|
+
* Get initial paywalls from storage service.
|
|
7784
|
+
* @returns An array of initial paywalls from storage service.
|
|
7785
|
+
*/
|
|
7786
|
+
const getInitialPaywalls = () => {
|
|
7787
|
+
return storageService.getPaywalls(exports.INITIAL_PAYWALLS) ?? [];
|
|
7788
|
+
};
|
|
7789
|
+
/**
|
|
7790
|
+
* Get all API paywalls from storage service.
|
|
7791
|
+
* @returns An array of API paywalls from storage service.
|
|
7792
|
+
*/
|
|
7793
|
+
const getApiPaywalls = () => {
|
|
7794
|
+
return storageService.getPaywalls(exports.API_PAYWALLS) ?? [];
|
|
7795
|
+
};
|
|
7796
|
+
/**
|
|
7797
|
+
* Get all paywalls from both API and Initial sources, ensuring uniqueness.
|
|
7798
|
+
* This function combines paywalls from both sources and filters out duplicates.
|
|
7799
|
+
* API Data takes precedence over Initial Data.
|
|
7800
|
+
*
|
|
7801
|
+
* @returns A combined list of unique paywalls based on both API and Initial data.
|
|
7802
|
+
*/
|
|
7803
|
+
const allPaywalls = () => {
|
|
7804
|
+
const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
|
|
7805
|
+
return uniqBy(paywalls, "id");
|
|
7806
|
+
};
|
|
7807
|
+
const getPaywall = (paywallId) => {
|
|
7808
|
+
const paywalls = allPaywalls();
|
|
7809
|
+
return paywalls.find((paywall) => paywall.id === paywallId);
|
|
7810
|
+
};
|
|
7811
|
+
const hasAllPaywalls = (paywallIds) => {
|
|
7812
|
+
const paywalls = allPaywalls();
|
|
7813
|
+
const paywallIdSet = new Set(paywalls.map(p => p.id));
|
|
7814
|
+
return paywallIds.every(id => paywallIdSet.has(id));
|
|
7815
|
+
};
|
|
7816
|
+
|
|
7547
7817
|
const formatDistanceLocale$1j = {
|
|
7548
7818
|
lessThanXSeconds: {
|
|
7549
7819
|
one: "minder as 'n sekonde",
|
|
@@ -57510,22 +57780,23 @@ class CampaignRuleRepository {
|
|
|
57510
57780
|
static hasPaywallUrls(campaigns) {
|
|
57511
57781
|
return campaigns.some((c) => c.paywall_url || c.page_urls || c.flow?.page_urls);
|
|
57512
57782
|
}
|
|
57783
|
+
/**
|
|
57784
|
+
* The campaign rules for this form factor, unvalidated, plus whether they came from the network.
|
|
57785
|
+
* `fromNetwork` is false when the stored rules stood in for a failed fetch (or updates are
|
|
57786
|
+
* disabled): the per-URL body cache must then neither resolve nor evict (NAM-4357).
|
|
57787
|
+
*/
|
|
57513
57788
|
async fetchCampaignRulesRaw() {
|
|
57514
|
-
const authDevice = storageService.getDevice();
|
|
57515
57789
|
if (this.disableCampaignUpdates) {
|
|
57516
57790
|
logger.info("Campaign updates disabled by configuration. Skipping fetch.");
|
|
57517
|
-
return [];
|
|
57791
|
+
return { campaigns: [], fromNetwork: false };
|
|
57518
57792
|
}
|
|
57519
|
-
|
|
57520
|
-
|
|
57521
|
-
|
|
57522
|
-
|
|
57523
|
-
|
|
57524
|
-
campaignRules = await this.getCampaigns(authDevice.id);
|
|
57525
|
-
}
|
|
57526
|
-
campaignRules = campaignRules?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
|
|
57793
|
+
const authDevice = storageService.getDevice();
|
|
57794
|
+
const fetched = authDevice?.id
|
|
57795
|
+
? await this.fetchCampaigns(authDevice.id)
|
|
57796
|
+
: await this.fetchAnonymousCampaigns();
|
|
57797
|
+
const campaigns = fetched.campaigns?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
|
|
57527
57798
|
(cRule.flow && cRule.form_factors?.some((f) => f.form_factor === this.currentFormFactor)));
|
|
57528
|
-
return
|
|
57799
|
+
return { campaigns, fromNetwork: fetched.fromNetwork };
|
|
57529
57800
|
}
|
|
57530
57801
|
finalizeCampaignRules(campaignRules, paywalls) {
|
|
57531
57802
|
// NAM-2457: a server flow rule is kept only when BOTH assets are satisfied —
|
|
@@ -57561,10 +57832,19 @@ class CampaignRuleRepository {
|
|
|
57561
57832
|
listeners.forEach((handler) => handler(campaigns));
|
|
57562
57833
|
}
|
|
57563
57834
|
async getAnonymousCampaigns(sdkVersion = exports.NAMI_SDK_VERSION) {
|
|
57835
|
+
return (await this.fetchAnonymousCampaigns(sdkVersion)).campaigns;
|
|
57836
|
+
}
|
|
57837
|
+
async getCampaigns(deviceId) {
|
|
57838
|
+
return (await this.fetchCampaigns(deviceId)).campaigns;
|
|
57839
|
+
}
|
|
57840
|
+
async fetchAnonymousCampaigns(sdkVersion = exports.NAMI_SDK_VERSION) {
|
|
57564
57841
|
const path = `campaign_rules/?sdk_version=${sdkVersion}`;
|
|
57565
57842
|
try {
|
|
57566
57843
|
const resp = await NamiAPI.instance.fetchAPI(path);
|
|
57567
|
-
return
|
|
57844
|
+
return {
|
|
57845
|
+
campaigns: mapAnonymousCampaigns(resp.results ?? [], this.splitPosition, this.currentFormFactor),
|
|
57846
|
+
fromNetwork: true,
|
|
57847
|
+
};
|
|
57568
57848
|
}
|
|
57569
57849
|
catch (error) {
|
|
57570
57850
|
const data = this.fallbackData();
|
|
@@ -57572,23 +57852,23 @@ class CampaignRuleRepository {
|
|
|
57572
57852
|
logger.error(handleErrors(error.status, path));
|
|
57573
57853
|
throw error;
|
|
57574
57854
|
}
|
|
57575
|
-
return data.results;
|
|
57855
|
+
return { campaigns: data.results, fromNetwork: false };
|
|
57576
57856
|
}
|
|
57577
57857
|
}
|
|
57578
|
-
async
|
|
57858
|
+
async fetchCampaigns(deviceId) {
|
|
57579
57859
|
const path = `device/${deviceId}/campaign_rules/`;
|
|
57580
|
-
let data;
|
|
57581
57860
|
try {
|
|
57582
|
-
data = await NamiAPI.instance.fetchAPI(path);
|
|
57861
|
+
const data = await NamiAPI.instance.fetchAPI(path);
|
|
57862
|
+
return { campaigns: data?.results || [], fromNetwork: true };
|
|
57583
57863
|
}
|
|
57584
57864
|
catch (error) {
|
|
57585
|
-
data = this.fallbackData();
|
|
57865
|
+
const data = this.fallbackData();
|
|
57586
57866
|
if (!data) {
|
|
57587
57867
|
logger.error(handleErrors(error.status, path));
|
|
57588
57868
|
throw error;
|
|
57589
57869
|
}
|
|
57870
|
+
return { campaigns: data.results || [], fromNetwork: false };
|
|
57590
57871
|
}
|
|
57591
|
-
return data?.results || [];
|
|
57592
57872
|
}
|
|
57593
57873
|
fallbackData() {
|
|
57594
57874
|
const storedData = storageService.getCampaignRules(exports.API_CAMPAIGN_RULES)
|
|
@@ -57871,6 +58151,15 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
57871
58151
|
* RetryExhaustedError when all attempts and/or the budget are spent.
|
|
57872
58152
|
*/
|
|
57873
58153
|
async function fetchWithCdnRetry(url, opts = {}) {
|
|
58154
|
+
const { data, retryCount } = await fetchTextWithCdnRetry(url, opts);
|
|
58155
|
+
return { data: JSON.parse(data), retryCount };
|
|
58156
|
+
}
|
|
58157
|
+
/**
|
|
58158
|
+
* {@link fetchWithCdnRetry}'s retry loop, returning the raw 2xx body text instead of parsed JSON.
|
|
58159
|
+
* The per-URL paywall body cache stores those exact bytes, so a cached body is decoded by the
|
|
58160
|
+
* same `JSON.parse` a fetched one is (NAM-4357).
|
|
58161
|
+
*/
|
|
58162
|
+
async function fetchTextWithCdnRetry(url, opts = {}) {
|
|
57874
58163
|
const maxAttempts = opts.maxAttempts ?? DEFAULTS.maxAttempts;
|
|
57875
58164
|
const baseDelayMs = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
|
|
57876
58165
|
const maxDelayMs = opts.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
@@ -57920,8 +58209,7 @@ async function fetchWithCdnRetry(url, opts = {}) {
|
|
|
57920
58209
|
const responseBody = await response.clone().text();
|
|
57921
58210
|
logger.debug(`[HTTP] Response ${response.status}: ${responseBody}`);
|
|
57922
58211
|
}
|
|
57923
|
-
|
|
57924
|
-
return { data, retryCount: attempt };
|
|
58212
|
+
return { data: await response.text(), retryCount: attempt };
|
|
57925
58213
|
}
|
|
57926
58214
|
catch (err) {
|
|
57927
58215
|
if (err instanceof NonRetryableHttpError)
|
|
@@ -57942,7 +58230,43 @@ async function fetchWithCdnRetry(url, opts = {}) {
|
|
|
57942
58230
|
throw new RetryExhaustedError(url, attempt, lastStatus);
|
|
57943
58231
|
}
|
|
57944
58232
|
|
|
58233
|
+
/**
|
|
58234
|
+
* Reads the parts of a `campaign_rules` paywall URL the per-URL body cache needs (NAM-4357).
|
|
58235
|
+
*
|
|
58236
|
+
* Both helpers scan the string rather than build a `URL`: the cache is keyed on the exact bytes
|
|
58237
|
+
* the server sent, so nothing here may normalise, re-encode or reject a URL — a malformed one is
|
|
58238
|
+
* simply un-hashed.
|
|
58239
|
+
*/
|
|
58240
|
+
/**
|
|
58241
|
+
* The `content_hash` query value, or undefined when it is missing or empty. When the item appears
|
|
58242
|
+
* more than once the first value wins. The value is never parsed or validated: it is a
|
|
58243
|
+
* cache-validity token, not a content selector.
|
|
58244
|
+
*/
|
|
58245
|
+
function contentHashOf(url) {
|
|
58246
|
+
const query = url.split("#")[0].split("?")[1];
|
|
58247
|
+
if (!query)
|
|
58248
|
+
return undefined;
|
|
58249
|
+
for (const item of query.split("&")) {
|
|
58250
|
+
const eq = item.indexOf("=");
|
|
58251
|
+
const name = eq === -1 ? item : item.slice(0, eq);
|
|
58252
|
+
if (name !== "content_hash")
|
|
58253
|
+
continue;
|
|
58254
|
+
const value = eq === -1 ? "" : item.slice(eq + 1);
|
|
58255
|
+
return value || undefined;
|
|
58256
|
+
}
|
|
58257
|
+
return undefined;
|
|
58258
|
+
}
|
|
58259
|
+
/** The path segment after `/paywall/` — the paywall id the URL names — or undefined. */
|
|
58260
|
+
function paywallIdOf(url) {
|
|
58261
|
+
const path = url.split(/[?#]/)[0];
|
|
58262
|
+
const match = /\/paywall\/([^/]+)/.exec(path);
|
|
58263
|
+
return match ? match[1] : undefined;
|
|
58264
|
+
}
|
|
58265
|
+
|
|
57945
58266
|
class PaywallRepository {
|
|
58267
|
+
constructor(bodyCache = paywallBodyCache) {
|
|
58268
|
+
this.bodyCache = bodyCache;
|
|
58269
|
+
}
|
|
57946
58270
|
async fetchPaywalls() {
|
|
57947
58271
|
const authDevice = storageService.getDevice();
|
|
57948
58272
|
let paywalls;
|
|
@@ -57991,34 +58315,131 @@ class PaywallRepository {
|
|
|
57991
58315
|
}
|
|
57992
58316
|
return data?.results || [];
|
|
57993
58317
|
}
|
|
57994
|
-
|
|
57995
|
-
|
|
57996
|
-
|
|
57997
|
-
|
|
58318
|
+
/**
|
|
58319
|
+
* Resolves the URL set named by `campaign_rules` into paywall objects — the decision table of
|
|
58320
|
+
* `api-resilience-and-caching.md` § 2.2, run identically at startup and on `refresh()`:
|
|
58321
|
+
*
|
|
58322
|
+
* | URL | entry for the exact URL within TTL | action | provenance |
|
|
58323
|
+
* |-----------|------------------------------------|-------------------------------|--------------------------|
|
|
58324
|
+
* | hashed | yes | serve it, no request | `cacheHit` |
|
|
58325
|
+
* | hashed | no | fetch; store the 2xx body | `fetched` |
|
|
58326
|
+
* | un-hashed | not consulted | fetch; store the 2xx body | `fetched` |
|
|
58327
|
+
* | either | fetch failed, entry within TTL | serve it, keep it | `cacheAfterFetchFailure` |
|
|
58328
|
+
* | either | fetch failed, no entry | no body | counts `failed` |
|
|
58329
|
+
*
|
|
58330
|
+
* Writes the aggregate `api_paywalls` — as the URLs of every body this resolution produced, since
|
|
58331
|
+
* the bodies themselves are already in the body cache — unless it produced none, so an outage
|
|
58332
|
+
* cannot overwrite a good aggregate with an empty one. Eviction is the caller's: it runs only
|
|
58333
|
+
* after a complete `campaign_rules` fetch.
|
|
58334
|
+
*/
|
|
58335
|
+
async resolvePaywallsByUrls(urls) {
|
|
57998
58336
|
const uniqueUrls = [...new Set(urls)];
|
|
57999
|
-
const
|
|
58000
|
-
|
|
58001
|
-
|
|
58002
|
-
|
|
58003
|
-
|
|
58004
|
-
|
|
58005
|
-
|
|
58006
|
-
|
|
58007
|
-
|
|
58008
|
-
|
|
58009
|
-
|
|
58010
|
-
|
|
58011
|
-
|
|
58337
|
+
const ttl = getCampaignCacheTtl();
|
|
58338
|
+
this.bodyCache.evaluateTtl(ttl);
|
|
58339
|
+
const resolutions = await Promise.all(uniqueUrls.map((url) => this.resolvePaywall(url, ttl)));
|
|
58340
|
+
const outcome = {
|
|
58341
|
+
paywalls: [],
|
|
58342
|
+
urls: uniqueUrls.length,
|
|
58343
|
+
hashed: uniqueUrls.filter((url) => contentHashOf(url) !== undefined).length,
|
|
58344
|
+
cacheHits: 0,
|
|
58345
|
+
fetched: 0,
|
|
58346
|
+
cacheAfterFetchFailure: 0,
|
|
58347
|
+
failed: 0,
|
|
58348
|
+
retryCount: 0,
|
|
58349
|
+
};
|
|
58350
|
+
const resolvedUrls = new Map();
|
|
58351
|
+
resolutions.forEach((resolution, i) => {
|
|
58352
|
+
outcome.retryCount += resolution.retryCount;
|
|
58353
|
+
if (!resolution.paywall) {
|
|
58354
|
+
outcome.failed += 1;
|
|
58355
|
+
return;
|
|
58012
58356
|
}
|
|
58357
|
+
resolvedUrls.set(resolution.paywall, uniqueUrls[i]);
|
|
58358
|
+
if (resolution.paywall.fetchSource === "cacheHit")
|
|
58359
|
+
outcome.cacheHits += 1;
|
|
58360
|
+
else if (resolution.paywall.fetchSource === "cacheAfterFetchFailure")
|
|
58361
|
+
outcome.cacheAfterFetchFailure += 1;
|
|
58362
|
+
else
|
|
58363
|
+
outcome.fetched += 1;
|
|
58364
|
+
});
|
|
58365
|
+
outcome.paywalls = this.validatePaywalls([...resolvedUrls.keys()]);
|
|
58366
|
+
if (outcome.paywalls.length > 0) {
|
|
58367
|
+
storageService.setApiPaywallsFromBodyCache(outcome.paywalls.map((paywall) => resolvedUrls.get(paywall)));
|
|
58013
58368
|
}
|
|
58014
|
-
|
|
58015
|
-
|
|
58016
|
-
|
|
58017
|
-
|
|
58369
|
+
return outcome;
|
|
58370
|
+
}
|
|
58371
|
+
/**
|
|
58372
|
+
* Seeds the body cache from the initial-config bundle (§ 2.2 "Seeding"): every **hashed** bundled
|
|
58373
|
+
* URL whose paywall id matches a bundled body is stored under that exact URL unless an entry
|
|
58374
|
+
* already exists. A hash in a bundled URL was minted against the bundled body, so the pair is
|
|
58375
|
+
* exact. Runs at configure, before the first resolution.
|
|
58376
|
+
*/
|
|
58377
|
+
seedFromInitialConfig(campaignRules, paywalls) {
|
|
58378
|
+
const bundled = new Map(paywalls.map((paywall) => [paywall.id, paywall]));
|
|
58379
|
+
this.bodyCache.evaluateTtl(getCampaignCacheTtl());
|
|
58380
|
+
let candidates = 0;
|
|
58381
|
+
let seeded = 0;
|
|
58382
|
+
for (const url of new Set(bundledPaywallUrls(campaignRules))) {
|
|
58383
|
+
const contentHash = contentHashOf(url);
|
|
58384
|
+
const id = paywallIdOf(url);
|
|
58385
|
+
const paywall = id ? bundled.get(id) : undefined;
|
|
58386
|
+
if (!contentHash || !paywall)
|
|
58387
|
+
continue;
|
|
58388
|
+
candidates += 1;
|
|
58389
|
+
if (this.bodyCache.seedIfAbsent(url, contentHash, JSON.stringify(paywall)))
|
|
58390
|
+
seeded += 1;
|
|
58391
|
+
}
|
|
58392
|
+
logger.info(`Paywall body cache seeded from initial config: seeded=${seeded}, candidates=${candidates}`);
|
|
58393
|
+
}
|
|
58394
|
+
/** One row of the decision table. Never throws. */
|
|
58395
|
+
async resolvePaywall(url, ttl) {
|
|
58396
|
+
const contentHash = contentHashOf(url);
|
|
58397
|
+
if (contentHash) {
|
|
58398
|
+
const hit = this.decodeCached(url, ttl);
|
|
58399
|
+
if (hit)
|
|
58400
|
+
return { paywall: withProvenance(hit, "cacheHit", contentHash), retryCount: 0 };
|
|
58401
|
+
}
|
|
58402
|
+
let retryCount = 0;
|
|
58403
|
+
try {
|
|
58404
|
+
const response = await this.fetchPaywallText(url);
|
|
58405
|
+
retryCount = response.retryCount;
|
|
58406
|
+
const paywall = decodePaywall(response.data);
|
|
58407
|
+
if (paywall) {
|
|
58408
|
+
this.bodyCache.store(url, contentHash, response.data);
|
|
58409
|
+
return { paywall: withProvenance(paywall, "fetched", contentHash), retryCount };
|
|
58018
58410
|
}
|
|
58019
|
-
|
|
58411
|
+
logger.error(`Paywall body from ${url} is not a decodable paywall`);
|
|
58020
58412
|
}
|
|
58021
|
-
|
|
58413
|
+
catch (error) {
|
|
58414
|
+
// Note: an exhausted-failure path contributed retries we can't see from the thrown error,
|
|
58415
|
+
// so retry_count under-reports failures. Acceptable for the telemetry signal.
|
|
58416
|
+
logger.error(`Failed to fetch individual paywall: ${error}`);
|
|
58417
|
+
}
|
|
58418
|
+
const fallback = this.decodeCached(url, ttl);
|
|
58419
|
+
if (fallback) {
|
|
58420
|
+
logger.warn(`Serving the cached body for ${url} after fetch failure (within campaign_cache_ttl)`);
|
|
58421
|
+
return { paywall: withProvenance(fallback, "cacheAfterFetchFailure", contentHash), retryCount };
|
|
58422
|
+
}
|
|
58423
|
+
return { retryCount };
|
|
58424
|
+
}
|
|
58425
|
+
/** The stored body for this exact URL, decoded; an undecodable one is deleted (not eviction). */
|
|
58426
|
+
decodeCached(url, ttl) {
|
|
58427
|
+
const entry = this.bodyCache.validEntry(url, ttl);
|
|
58428
|
+
if (!entry)
|
|
58429
|
+
return undefined;
|
|
58430
|
+
const paywall = decodePaywall(entry.body);
|
|
58431
|
+
if (!paywall) {
|
|
58432
|
+
logger.warn(`Deleting an undecodable cached paywall body for ${url}`);
|
|
58433
|
+
this.bodyCache.remove(url);
|
|
58434
|
+
}
|
|
58435
|
+
return paywall;
|
|
58436
|
+
}
|
|
58437
|
+
fetchPaywallText(url) {
|
|
58438
|
+
if (storageService.getNamiConfig()?.namiCommands?.includes(exports.SIMULATE_PAYWALL_FETCH_FAILURE)) {
|
|
58439
|
+
logger.warn(`${exports.SIMULATE_PAYWALL_FETCH_FAILURE}: failing the fetch for ${url} before the network`);
|
|
58440
|
+
return Promise.reject(new RetryExhaustedError(url, 0));
|
|
58441
|
+
}
|
|
58442
|
+
return fetchTextWithCdnRetry(url);
|
|
58022
58443
|
}
|
|
58023
58444
|
validatePaywalls(paywalls) {
|
|
58024
58445
|
return paywalls.filter((paywall) => {
|
|
@@ -58043,6 +58464,33 @@ class PaywallRepository {
|
|
|
58043
58464
|
}
|
|
58044
58465
|
}
|
|
58045
58466
|
PaywallRepository.instance = new PaywallRepository();
|
|
58467
|
+
function decodePaywall(text) {
|
|
58468
|
+
try {
|
|
58469
|
+
const value = JSON.parse(text);
|
|
58470
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
58471
|
+
}
|
|
58472
|
+
catch {
|
|
58473
|
+
return undefined;
|
|
58474
|
+
}
|
|
58475
|
+
}
|
|
58476
|
+
function withProvenance(paywall, fetchSource, contentHash) {
|
|
58477
|
+
return { ...paywall, fetchSource, ...(contentHash ? { contentHash } : {}) };
|
|
58478
|
+
}
|
|
58479
|
+
/** Every paywall URL a bundled (anonymous-format) rule names, across all of its segments. */
|
|
58480
|
+
function bundledPaywallUrls(campaignRules) {
|
|
58481
|
+
const urls = [];
|
|
58482
|
+
for (const rule of campaignRules) {
|
|
58483
|
+
for (const segment of rule.segments ?? []) {
|
|
58484
|
+
if (segment.paywall_url)
|
|
58485
|
+
urls.push(segment.paywall_url);
|
|
58486
|
+
if (segment.page_urls)
|
|
58487
|
+
urls.push(...Object.values(segment.page_urls));
|
|
58488
|
+
if (segment.flow?.page_urls)
|
|
58489
|
+
urls.push(...Object.values(segment.flow.page_urls));
|
|
58490
|
+
}
|
|
58491
|
+
}
|
|
58492
|
+
return urls;
|
|
58493
|
+
}
|
|
58046
58494
|
|
|
58047
58495
|
class ConfigRepository {
|
|
58048
58496
|
/**
|
|
@@ -61843,7 +62291,10 @@ class NamiRefs {
|
|
|
61843
62291
|
}
|
|
61844
62292
|
}
|
|
61845
62293
|
if (initconfig?.paywalls) {
|
|
61846
|
-
storageService.setPaywalls(exports.INITIAL_PAYWALLS, initconfig.paywalls);
|
|
62294
|
+
storageService.setPaywalls(exports.INITIAL_PAYWALLS, initconfig.paywalls.map((paywall) => ({ ...paywall, fetchSource: "initialConfig" })));
|
|
62295
|
+
if (initconfig.campaign_rules?.length) {
|
|
62296
|
+
PaywallRepository.instance.seedFromInitialConfig(initconfig.campaign_rules, initconfig.paywalls);
|
|
62297
|
+
}
|
|
61847
62298
|
}
|
|
61848
62299
|
if (initconfig?.products) {
|
|
61849
62300
|
storageService.setProducts(exports.INITIAL_PRODUCTS, initconfig.products);
|
|
@@ -61910,7 +62361,7 @@ class NamiRefs {
|
|
|
61910
62361
|
const paywallRepo = PaywallRepository.instance;
|
|
61911
62362
|
const startTime = Date.now();
|
|
61912
62363
|
const campaignRulesStartTime = Date.now();
|
|
61913
|
-
const rawCampaigns = await campaignRepo.fetchCampaignRulesRaw();
|
|
62364
|
+
const { campaigns: rawCampaigns, fromNetwork } = await campaignRepo.fetchCampaignRulesRaw();
|
|
61914
62365
|
const campaignRulesDuration = Date.now() - campaignRulesStartTime;
|
|
61915
62366
|
// Hydrate flow objects: fetch flow JSON from flow.url for campaigns that
|
|
61916
62367
|
// use the URL-based format (4.0.0+ since NAM-3493) instead of inline flow.object
|
|
@@ -61931,24 +62382,35 @@ class NamiRefs {
|
|
|
61931
62382
|
const useIndividual = !campaignRepo.useLegacyPaywallFetch &&
|
|
61932
62383
|
CampaignRuleRepository.hasPaywallUrls(rawCampaigns);
|
|
61933
62384
|
let paywalls;
|
|
61934
|
-
let
|
|
62385
|
+
let outcome = null;
|
|
61935
62386
|
const paywallsStartTime = Date.now();
|
|
61936
|
-
if (useIndividual) {
|
|
62387
|
+
if (useIndividual && !fromNetwork) {
|
|
62388
|
+
// campaign_rules came from the stored copy, so this is not a complete page set: no
|
|
62389
|
+
// resolution and no eviction (api-resilience-and-caching.md § 2.2). The aggregate serves.
|
|
62390
|
+
logger.info("Paywall resolution skipped: campaign_rules were served from the stored copy");
|
|
62391
|
+
paywalls = getApiPaywalls();
|
|
62392
|
+
}
|
|
62393
|
+
else if (useIndividual) {
|
|
61937
62394
|
const urls = CampaignRuleRepository.extractPaywallUrls(rawCampaigns);
|
|
61938
|
-
|
|
61939
|
-
paywalls =
|
|
61940
|
-
|
|
62395
|
+
outcome = await paywallRepo.resolvePaywallsByUrls(urls);
|
|
62396
|
+
paywalls = outcome.paywalls;
|
|
62397
|
+
paywallBodyCache.reconcile(new Set(urls));
|
|
61941
62398
|
}
|
|
61942
62399
|
else {
|
|
61943
62400
|
paywalls = await paywallRepo.fetchPaywalls();
|
|
61944
62401
|
}
|
|
61945
62402
|
const paywallsDuration = Date.now() - paywallsStartTime;
|
|
61946
62403
|
const totalDuration = Date.now() - startTime;
|
|
61947
|
-
if (
|
|
61948
|
-
|
|
61949
|
-
|
|
62404
|
+
if (!useIndividual || outcome) {
|
|
62405
|
+
// The line every SDK emits once per resolution; the paywall-cache e2e suites parse it.
|
|
62406
|
+
const cacheFields = outcome
|
|
62407
|
+
? `, urls=${outcome.urls}, hashed=${outcome.hashed}, cache_hits=${outcome.cacheHits}, ` +
|
|
62408
|
+
`fetched=${outcome.fetched}, cache_after_fetch_failure=${outcome.cacheAfterFetchFailure}, failed=${outcome.failed}`
|
|
62409
|
+
: "";
|
|
62410
|
+
logger.debug(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
|
|
62411
|
+
`count=${paywalls.length}${cacheFields}, total=${totalDuration}ms, ` +
|
|
61950
62412
|
`campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms` +
|
|
61951
|
-
`${
|
|
62413
|
+
`${outcome ? `, retry_count=${outcome.retryCount}` : ""}`);
|
|
61952
62414
|
}
|
|
61953
62415
|
return campaignRepo.finalizeCampaignRules(rawCampaigns, paywalls);
|
|
61954
62416
|
}
|
|
@@ -62122,6 +62584,8 @@ class Nami {
|
|
|
62122
62584
|
static async reset() {
|
|
62123
62585
|
var _a;
|
|
62124
62586
|
storageService.clearAll();
|
|
62587
|
+
// The per-URL paywall body cache keeps its own prefixed keys and an in-memory index (NAM-4357).
|
|
62588
|
+
paywallBodyCache.purgeAll();
|
|
62125
62589
|
clearInMemoryAnonymousMode();
|
|
62126
62590
|
__classPrivateFieldSet(Nami.instance, _Nami_isInitialized, false, "f");
|
|
62127
62591
|
__classPrivateFieldGet((_a = Nami.instance), _Nami_removeForegroundListener, "f")?.call(_a);
|