@teardown/react-native 2.0.23 → 2.0.24
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/package.json
CHANGED
|
@@ -259,7 +259,7 @@ describe("ForceUpdateClient", () => {
|
|
|
259
259
|
});
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
-
describe("
|
|
262
|
+
describe("checkIntervalMs", () => {
|
|
263
263
|
test("skips version check when identify returns null", async () => {
|
|
264
264
|
const mockIdentity = createMockIdentityClient();
|
|
265
265
|
const mockLogging = createMockLoggingClient();
|
|
@@ -268,8 +268,7 @@ describe("ForceUpdateClient", () => {
|
|
|
268
268
|
mockIdentity.setNextIdentifyResult(null);
|
|
269
269
|
|
|
270
270
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
271
|
-
|
|
272
|
-
checkCooldownMs: 0,
|
|
271
|
+
checkIntervalMs: 0,
|
|
273
272
|
});
|
|
274
273
|
client.initialize();
|
|
275
274
|
|
|
@@ -287,14 +286,13 @@ describe("ForceUpdateClient", () => {
|
|
|
287
286
|
client.shutdown();
|
|
288
287
|
});
|
|
289
288
|
|
|
290
|
-
test("
|
|
289
|
+
test("checkIntervalMs -1 disables version checking entirely", async () => {
|
|
291
290
|
const mockIdentity = createMockIdentityClient();
|
|
292
291
|
const mockLogging = createMockLoggingClient();
|
|
293
292
|
const mockStorage = createMockStorageClient();
|
|
294
293
|
|
|
295
294
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
296
|
-
|
|
297
|
-
checkCooldownMs: -1,
|
|
295
|
+
checkIntervalMs: -1,
|
|
298
296
|
});
|
|
299
297
|
client.initialize();
|
|
300
298
|
|
|
@@ -310,47 +308,46 @@ describe("ForceUpdateClient", () => {
|
|
|
310
308
|
client.shutdown();
|
|
311
309
|
});
|
|
312
310
|
|
|
313
|
-
test("
|
|
311
|
+
test("interval prevents checks too soon after successful check", async () => {
|
|
314
312
|
const mockIdentity = createMockIdentityClient();
|
|
315
313
|
const mockLogging = createMockLoggingClient();
|
|
316
314
|
const mockStorage = createMockStorageClient();
|
|
317
315
|
|
|
318
316
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
319
|
-
checkOnForeground:
|
|
320
|
-
|
|
321
|
-
checkCooldownMs: 0,
|
|
317
|
+
checkOnForeground: true,
|
|
318
|
+
checkIntervalMs: 100_000,
|
|
322
319
|
});
|
|
323
320
|
client.initialize();
|
|
324
321
|
|
|
325
322
|
const foregroundHandler = mockAppStateListeners[0];
|
|
326
323
|
|
|
327
|
-
// First foreground
|
|
324
|
+
// First foreground - should trigger check
|
|
328
325
|
await foregroundHandler("active");
|
|
329
326
|
await new Promise((r) => setTimeout(r, 10));
|
|
330
327
|
|
|
331
328
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
332
329
|
|
|
333
|
-
// Second foreground immediately (within
|
|
330
|
+
// Second foreground immediately (within interval window)
|
|
334
331
|
await foregroundHandler("active");
|
|
335
332
|
await new Promise((r) => setTimeout(r, 10));
|
|
336
333
|
|
|
337
334
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
338
335
|
|
|
339
|
-
// Only first call should have triggered identify (
|
|
336
|
+
// Only first call should have triggered identify (interval blocks second)
|
|
340
337
|
expect(callsAfterFirst).toBe(1);
|
|
341
338
|
expect(callsAfterSecond).toBe(1);
|
|
342
339
|
|
|
343
340
|
client.shutdown();
|
|
344
341
|
});
|
|
345
342
|
|
|
346
|
-
test("checkOnForeground: true
|
|
343
|
+
test("checkOnForeground: true respects interval", async () => {
|
|
347
344
|
const mockIdentity = createMockIdentityClient();
|
|
348
345
|
const mockLogging = createMockLoggingClient();
|
|
349
346
|
const mockStorage = createMockStorageClient();
|
|
350
347
|
|
|
351
348
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
352
349
|
checkOnForeground: true,
|
|
353
|
-
|
|
350
|
+
checkIntervalMs: 100_000,
|
|
354
351
|
});
|
|
355
352
|
client.initialize();
|
|
356
353
|
|
|
@@ -362,28 +359,77 @@ describe("ForceUpdateClient", () => {
|
|
|
362
359
|
|
|
363
360
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
364
361
|
|
|
365
|
-
// Second foreground immediately (within
|
|
362
|
+
// Second foreground immediately (within interval window)
|
|
366
363
|
await foregroundHandler("active");
|
|
367
364
|
await new Promise((r) => setTimeout(r, 10));
|
|
368
365
|
|
|
369
366
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
370
367
|
|
|
371
|
-
//
|
|
368
|
+
// Only first should trigger - interval blocks second even with checkOnForeground: true
|
|
372
369
|
expect(callsAfterFirst).toBe(1);
|
|
373
|
-
expect(callsAfterSecond).toBe(
|
|
370
|
+
expect(callsAfterSecond).toBe(1);
|
|
374
371
|
|
|
375
372
|
client.shutdown();
|
|
376
373
|
});
|
|
377
374
|
|
|
378
|
-
test("
|
|
375
|
+
test("checkOnForeground: false disables foreground checking entirely", async () => {
|
|
379
376
|
const mockIdentity = createMockIdentityClient();
|
|
380
377
|
const mockLogging = createMockLoggingClient();
|
|
381
378
|
const mockStorage = createMockStorageClient();
|
|
382
379
|
|
|
383
380
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
384
381
|
checkOnForeground: false,
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
checkIntervalMs: 0, // Would normally allow every check
|
|
383
|
+
});
|
|
384
|
+
client.initialize();
|
|
385
|
+
|
|
386
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
387
|
+
|
|
388
|
+
// Foreground should not trigger check when checkOnForeground is false
|
|
389
|
+
await foregroundHandler("active");
|
|
390
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
391
|
+
|
|
392
|
+
expect(mockIdentity.getIdentifyCallCount()).toBe(0);
|
|
393
|
+
|
|
394
|
+
client.shutdown();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("checkIntervalMs 0 checks on every foreground", async () => {
|
|
398
|
+
const mockIdentity = createMockIdentityClient();
|
|
399
|
+
const mockLogging = createMockLoggingClient();
|
|
400
|
+
const mockStorage = createMockStorageClient();
|
|
401
|
+
|
|
402
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
403
|
+
checkOnForeground: true,
|
|
404
|
+
checkIntervalMs: 0,
|
|
405
|
+
});
|
|
406
|
+
client.initialize();
|
|
407
|
+
|
|
408
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
409
|
+
|
|
410
|
+
// First foreground
|
|
411
|
+
await foregroundHandler("active");
|
|
412
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
413
|
+
|
|
414
|
+
// Second foreground immediately
|
|
415
|
+
await foregroundHandler("active");
|
|
416
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
417
|
+
|
|
418
|
+
// Both should trigger since interval is 0
|
|
419
|
+
expect(mockIdentity.getIdentifyCallCount()).toBe(2);
|
|
420
|
+
|
|
421
|
+
client.shutdown();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("values below 30s are clamped to 30s minimum", async () => {
|
|
425
|
+
const mockIdentity = createMockIdentityClient();
|
|
426
|
+
const mockLogging = createMockLoggingClient();
|
|
427
|
+
const mockStorage = createMockStorageClient();
|
|
428
|
+
|
|
429
|
+
// Set interval to 10ms (below 30s minimum)
|
|
430
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
431
|
+
checkOnForeground: true,
|
|
432
|
+
checkIntervalMs: 10,
|
|
387
433
|
});
|
|
388
434
|
client.initialize();
|
|
389
435
|
|
|
@@ -395,13 +441,13 @@ describe("ForceUpdateClient", () => {
|
|
|
395
441
|
|
|
396
442
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
397
443
|
|
|
398
|
-
// Second foreground immediately
|
|
444
|
+
// Second foreground immediately - should be blocked by 30s minimum
|
|
399
445
|
await foregroundHandler("active");
|
|
400
446
|
await new Promise((r) => setTimeout(r, 10));
|
|
401
447
|
|
|
402
448
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
403
449
|
|
|
404
|
-
// Only first call should have triggered
|
|
450
|
+
// Only first call should have triggered (30s minimum enforced)
|
|
405
451
|
expect(callsAfterFirst).toBe(1);
|
|
406
452
|
expect(callsAfterSecond).toBe(1);
|
|
407
453
|
|
|
@@ -69,41 +69,30 @@ export interface VersionStatusChangeEvents {
|
|
|
69
69
|
|
|
70
70
|
export type ForceUpdateClientOptions = {
|
|
71
71
|
/**
|
|
72
|
-
* Minimum time (ms) between
|
|
73
|
-
* Measured from the last time the app came to foreground.
|
|
74
|
-
* Prevents checking when user quickly switches apps back and forth.
|
|
75
|
-
* Default: 30000 (30 seconds)
|
|
76
|
-
*
|
|
77
|
-
* Special values:
|
|
78
|
-
* - -1: Disable throttling, check on every foreground (respects checkCooldownMs)
|
|
79
|
-
*
|
|
80
|
-
* Example: If throttleMs is 30s and user backgrounds then foregrounds the app
|
|
81
|
-
* twice within 20s, only the first transition triggers a check.
|
|
82
|
-
*/
|
|
83
|
-
throttleMs?: number;
|
|
84
|
-
/**
|
|
85
|
-
* Minimum time (ms) since the last successful version check before checking again.
|
|
86
|
-
* Measured from when the last check completed successfully (not when it started).
|
|
87
|
-
* Prevents unnecessary API calls after we already have fresh version data.
|
|
72
|
+
* Minimum time (ms) between version checks.
|
|
88
73
|
* Default: 300000 (5 minutes)
|
|
89
74
|
*
|
|
75
|
+
* Values below 30 seconds are clamped to 30 seconds to prevent excessive API calls.
|
|
76
|
+
*
|
|
90
77
|
* Special values:
|
|
91
|
-
* - 0:
|
|
92
|
-
* - -1: Disable
|
|
78
|
+
* - 0: Check on every foreground (no interval)
|
|
79
|
+
* - -1: Disable automatic version checking entirely
|
|
93
80
|
*
|
|
94
|
-
* Example: If
|
|
81
|
+
* Example: If checkIntervalMs is 5min and a check completes at 12:00pm,
|
|
95
82
|
* no new checks occur until 12:05pm, even if user foregrounds the app multiple times.
|
|
96
83
|
*/
|
|
97
|
-
|
|
98
|
-
/**
|
|
84
|
+
checkIntervalMs?: number;
|
|
85
|
+
/** Check version when app comes to foreground, respecting checkIntervalMs (default: true) */
|
|
99
86
|
checkOnForeground?: boolean;
|
|
100
87
|
/** If true, check version even when not identified by using anonymous device identification (default: false) */
|
|
101
88
|
identifyAnonymousDevice?: boolean;
|
|
102
89
|
};
|
|
103
90
|
|
|
91
|
+
/** Hard minimum interval between checks to prevent excessive API calls */
|
|
92
|
+
const MIN_CHECK_INTERVAL_MS = 30_000; // 30 seconds
|
|
93
|
+
|
|
104
94
|
const DEFAULT_OPTIONS: Required<ForceUpdateClientOptions> = {
|
|
105
|
-
|
|
106
|
-
checkCooldownMs: 300_000, // 5 minutes
|
|
95
|
+
checkIntervalMs: 300_000, // 5 minutes
|
|
107
96
|
checkOnForeground: true,
|
|
108
97
|
identifyAnonymousDevice: false,
|
|
109
98
|
};
|
|
@@ -116,7 +105,6 @@ export class ForceUpdateClient {
|
|
|
116
105
|
private unsubscribe: (() => void) | null = null;
|
|
117
106
|
private appStateSubscription: NativeEventSubscription | null = null;
|
|
118
107
|
private lastCheckTime: number | null = null;
|
|
119
|
-
private lastForegroundTime: number | null = null;
|
|
120
108
|
private initialized = false;
|
|
121
109
|
|
|
122
110
|
private readonly logger: Logger;
|
|
@@ -132,7 +120,6 @@ export class ForceUpdateClient {
|
|
|
132
120
|
this.logger = logging.createLogger({ name: "ForceUpdateClient" });
|
|
133
121
|
this.storage = storage.createStorage("version");
|
|
134
122
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
135
|
-
// Don't initialize here - defer to initialize()
|
|
136
123
|
}
|
|
137
124
|
|
|
138
125
|
initialize(): void {
|
|
@@ -142,7 +129,6 @@ export class ForceUpdateClient {
|
|
|
142
129
|
}
|
|
143
130
|
this.initialized = true;
|
|
144
131
|
|
|
145
|
-
// Load from storage, subscribe to events, and sync with current identity state
|
|
146
132
|
this.versionStatus = this.getVersionStatusFromStorage();
|
|
147
133
|
this.logger.debug(`Initialized with version status: ${this.versionStatus.type}`);
|
|
148
134
|
this.subscribeToIdentity();
|
|
@@ -164,25 +150,30 @@ export class ForceUpdateClient {
|
|
|
164
150
|
}
|
|
165
151
|
|
|
166
152
|
private getVersionStatusFromStorage(): VersionStatus {
|
|
167
|
-
|
|
153
|
+
try {
|
|
154
|
+
const stored = this.storage.getItem(VERSION_STATUS_STORAGE_KEY);
|
|
168
155
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
156
|
+
if (stored == null) {
|
|
157
|
+
this.logger.debug("No stored version status, returning initializing");
|
|
158
|
+
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
159
|
+
}
|
|
173
160
|
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
const parsed = VersionStatusSchema.parse(JSON.parse(stored));
|
|
162
|
+
this.logger.debug(`Parsed version status from storage: ${parsed.type}`);
|
|
163
|
+
|
|
164
|
+
// "checking" and "initializing" are transient states - if we restore them, reset to initializing
|
|
165
|
+
// This can happen if the app was killed during a version check
|
|
166
|
+
if (parsed.type === "checking" || parsed.type === "initializing") {
|
|
167
|
+
this.logger.debug(`Found stale '${parsed.type}' state in storage, resetting to initializing`);
|
|
168
|
+
this.storage.removeItem(VERSION_STATUS_STORAGE_KEY);
|
|
169
|
+
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
170
|
+
}
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.logger.debug(`Found stale '${parsed.type}' state in storage, resetting to initializing`);
|
|
181
|
-
this.storage.removeItem(VERSION_STATUS_STORAGE_KEY);
|
|
172
|
+
return parsed;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.logger.debugError("Error getting version status from storage", { error });
|
|
182
175
|
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
183
176
|
}
|
|
184
|
-
|
|
185
|
-
return parsed;
|
|
186
177
|
}
|
|
187
178
|
|
|
188
179
|
private saveVersionStatusToStorage(status: VersionStatus): void {
|
|
@@ -239,30 +230,22 @@ export class ForceUpdateClient {
|
|
|
239
230
|
if (nextState === "active") {
|
|
240
231
|
this.logger.debug("App state changed to active");
|
|
241
232
|
|
|
242
|
-
// If
|
|
243
|
-
if (this.options.
|
|
244
|
-
this.logger.debug("Version checking disabled (
|
|
233
|
+
// If checkIntervalMs is -1, disable checking entirely
|
|
234
|
+
if (this.options.checkIntervalMs === -1) {
|
|
235
|
+
this.logger.debug("Version checking disabled (checkIntervalMs = -1)");
|
|
245
236
|
return;
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
const now = Date.now();
|
|
249
240
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Otherwise, check if enough time has passed since last successful check
|
|
259
|
-
const cooldownOk =
|
|
260
|
-
this.options.checkCooldownMs === 0 ||
|
|
261
|
-
!this.lastCheckTime ||
|
|
262
|
-
now - this.lastCheckTime >= this.options.checkCooldownMs;
|
|
263
|
-
|
|
264
|
-
if (this.options.checkOnForeground || (throttleOk && cooldownOk)) {
|
|
265
|
-
this.lastForegroundTime = now;
|
|
241
|
+
// Calculate effective interval (clamp to minimum unless 0 for "always check")
|
|
242
|
+
const effectiveInterval =
|
|
243
|
+
this.options.checkIntervalMs === 0 ? 0 : Math.max(this.options.checkIntervalMs, MIN_CHECK_INTERVAL_MS);
|
|
244
|
+
|
|
245
|
+
// Check if enough time has passed since last successful check
|
|
246
|
+
const canCheck = effectiveInterval === 0 || !this.lastCheckTime || now - this.lastCheckTime >= effectiveInterval;
|
|
247
|
+
|
|
248
|
+
if (this.options.checkOnForeground && canCheck) {
|
|
266
249
|
this.checkVersionOnForeground();
|
|
267
250
|
}
|
|
268
251
|
}
|
|
@@ -25,6 +25,8 @@ function createMockLoggingClient() {
|
|
|
25
25
|
warn: (message: string, ...args: unknown[]) => logs.push({ level: "warn", message, args }),
|
|
26
26
|
error: (message: string, ...args: unknown[]) => logs.push({ level: "error", message, args }),
|
|
27
27
|
debug: (message: string, ...args: unknown[]) => logs.push({ level: "debug", message, args }),
|
|
28
|
+
debugInfo: (message: string, ...args: unknown[]) => logs.push({ level: "debugInfo", message, args }),
|
|
29
|
+
debugError: (message: string, ...args: unknown[]) => logs.push({ level: "debugError", message, args }),
|
|
28
30
|
}),
|
|
29
31
|
getLogs: () => logs,
|
|
30
32
|
clearLogs: () => {
|
|
@@ -256,20 +258,26 @@ describe("IdentityClient", () => {
|
|
|
256
258
|
expect(client.getIdentifyState().type).toBe("unidentified");
|
|
257
259
|
});
|
|
258
260
|
|
|
259
|
-
test("
|
|
261
|
+
test("gracefully handles invalid stored state (corrupt JSON)", () => {
|
|
260
262
|
const mockStorage = createMockStorageClient();
|
|
261
263
|
mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, "not-valid-json{{{");
|
|
262
264
|
|
|
263
265
|
const { client } = createTestClient({ storage: mockStorage });
|
|
264
|
-
|
|
266
|
+
loadStateFromStorage(client);
|
|
267
|
+
|
|
268
|
+
// Should gracefully fall back to unidentified state
|
|
269
|
+
expect(client.getIdentifyState().type).toBe("unidentified");
|
|
265
270
|
});
|
|
266
271
|
|
|
267
|
-
test("
|
|
272
|
+
test("gracefully handles invalid stored state (schema mismatch)", () => {
|
|
268
273
|
const mockStorage = createMockStorageClient();
|
|
269
274
|
mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, JSON.stringify({ type: "invalid_type" }));
|
|
270
275
|
|
|
271
276
|
const { client } = createTestClient({ storage: mockStorage });
|
|
272
|
-
|
|
277
|
+
loadStateFromStorage(client);
|
|
278
|
+
|
|
279
|
+
// Should gracefully fall back to unidentified state
|
|
280
|
+
expect(client.getIdentifyState().type).toBe("unidentified");
|
|
273
281
|
});
|
|
274
282
|
|
|
275
283
|
test("creates logger with correct name", () => {
|
|
@@ -627,10 +635,10 @@ describe("IdentityClient", () => {
|
|
|
627
635
|
|
|
628
636
|
// Should have debug logs about state transitions
|
|
629
637
|
// When already identified, identify() will transition: identified -> identifying -> identified
|
|
630
|
-
const debugLogs = mockLogging.getLogs().filter((l) => l.level === "debug");
|
|
638
|
+
const debugLogs = mockLogging.getLogs().filter((l) => l.level === "debug" || l.level === "debugInfo");
|
|
631
639
|
expect(debugLogs.length).toBeGreaterThan(0);
|
|
632
640
|
// Check that state transitions are logged
|
|
633
|
-
expect(debugLogs.some((l) => l.message.includes("Identify state
|
|
641
|
+
expect(debugLogs.some((l) => l.message.includes("Identify state"))).toBe(true);
|
|
634
642
|
});
|
|
635
643
|
});
|
|
636
644
|
|