@scalebun/react-native 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +0 -1
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
- package/dist/scalebun.full.js +232 -69
- package/dist/scalebun.slim.js +232 -69
- package/ios/Ota/OtaSlotManager.swift +19 -5
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
- package/lib/commonjs/features/ota/OtaTypes.js +4 -0
- package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
- package/lib/commonjs/public/ScaleBunFacade.js +115 -2
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/ota/OtaOrchestrator.js +174 -48
- package/lib/module/features/ota/OtaTypes.js +1 -1
- package/lib/module/features/ota/useOtaUpdate.js +11 -2
- package/lib/module/public/ScaleBunFacade.js +115 -2
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
- package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
- package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
- package/package.json +2 -2
- package/src/core/constants/version.ts +1 -1
- package/src/features/ota/OtaEventEmitter.ts +12 -0
- package/src/features/ota/OtaOrchestrator.ts +209 -62
- package/src/features/ota/OtaTypes.ts +37 -39
- package/src/features/ota/useOtaUpdate.ts +11 -2
- package/src/public/ScaleBunFacade.ts +127 -3
|
@@ -8,42 +8,38 @@
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
-
* OTA-TELEMETRY-SPEC —
|
|
11
|
+
* OTA-TELEMETRY-SPEC — status (spec §84-86)
|
|
12
12
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* types + this spec, not the runtime wiring.
|
|
13
|
+
* Items 1-3 are DONE. All were additive and backward-compatible: old installed
|
|
14
|
+
* clients simply omit the new fields and never emit the new types, and the
|
|
15
|
+
* backend ingests nullable columns. Because they alter device runtime behaviour,
|
|
16
|
+
* verify against a real RN build + device kill-test before release — Metro
|
|
17
|
+
* cannot exercise the boot guard.
|
|
19
18
|
*
|
|
20
|
-
* 1. releaseId end-to-end
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* - set `releaseId: bundle.releaseId` in the delivery mapper
|
|
25
|
-
* OtaOrchestrator.ts deliverOtaEvents (~:105) — currently only bundleId.
|
|
26
|
-
* Then ota_events carry releaseId and the funnel keys by release.
|
|
19
|
+
* 1. releaseId end-to-end — DONE. `recordInstallExpectation` persists the
|
|
20
|
+
* release id (so a rollback reported launches later can still name it),
|
|
21
|
+
* `hydrateCurrentBundleFromSlots` restores it, every emit site passes it, and
|
|
22
|
+
* `deliverOtaEvents` maps it onto the wire item.
|
|
27
23
|
*
|
|
28
|
-
* 2.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* -
|
|
32
|
-
*
|
|
24
|
+
* 2. CHECK + OFFERED — DONE. CHECK is emitted at the top of `sync()` for devices
|
|
25
|
+
* already on an OTA bundle (a device on the factory bundle has no owned
|
|
26
|
+
* bundle id to name, and the server drops rows it cannot resolve — those are
|
|
27
|
+
* counted server-side instead). OFFERED is emitted on `action === 'DOWNLOAD'`
|
|
28
|
+
* before any bytes move.
|
|
33
29
|
*
|
|
34
|
-
* 3.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* add 'VERIFIED' after signature check (~:635).
|
|
30
|
+
* 3. BOOT_SUCCESS — DONE. Emitted from the boot-guard heartbeat once the bundle
|
|
31
|
+
* has actually booted and survived to `healthyAfterMs`. INSTALLED is kept and
|
|
32
|
+
* still means "staged + swapped", but activation should be measured on
|
|
33
|
+
* BOOT_SUCCESS: INSTALLED fires before the bundle has run, so it credits
|
|
34
|
+
* bundles that installed and then crash-reverted.
|
|
40
35
|
*
|
|
41
|
-
* 4. Stamp the running OTA bundle onto session/crash telemetry
|
|
42
|
-
* SessionMetadata.bundleId is the NATIVE app package id, not the OTA bundle
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
36
|
+
* 4. Stamp the running OTA bundle onto session/crash telemetry — NOT DONE.
|
|
37
|
+
* `SessionMetadata.bundleId` is the NATIVE app package id, not the OTA bundle,
|
|
38
|
+
* so crashes cannot currently be attributed to the bundle that produced them
|
|
39
|
+
* (release-health crash impact). Needs optional otaBundleId?/otaBundleVersion?
|
|
40
|
+
* (distinct fields — do NOT overload bundleId) sourced from
|
|
41
|
+
* `otaOrchestrator.getCurrentBundle()` at session start, plus the matching
|
|
42
|
+
* backend columns and a Prisma migration.
|
|
47
43
|
*
|
|
48
44
|
* Do NOT repurpose errorCode (it already collapses failure-error vs rollback-
|
|
49
45
|
* reason); add a new optional field if the two must be distinguished.
|
|
@@ -149,14 +145,16 @@ export interface OtaPatchPayload {
|
|
|
149
145
|
}
|
|
150
146
|
|
|
151
147
|
// ── Adoption telemetry (rides the unified batch envelope as kind:'ota_event') ──
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
//
|
|
149
|
+
// The event vocabulary is declared ONCE, in OtaEventEmitter — the module that
|
|
150
|
+
// emits it. This file used to carry a second, shorter copy under the same name,
|
|
151
|
+
// so `OtaEventItem.type` and the emitter's events could disagree while both
|
|
152
|
+
// type-checked, and adding a funnel step meant remembering to edit two unions.
|
|
153
|
+
// `DOWNLOAD_PROGRESS` is emitted for UI only and never queued for upload, so it
|
|
154
|
+
// is excluded here rather than re-listing the rest.
|
|
155
|
+
import type { OtaEventType as EmittedOtaEventType } from './OtaEventEmitter';
|
|
156
|
+
|
|
157
|
+
export type OtaEventType = Exclude<EmittedOtaEventType, 'DOWNLOAD_PROGRESS'>;
|
|
160
158
|
|
|
161
159
|
export interface OtaEventItem {
|
|
162
160
|
kind: 'ota_event';
|
|
@@ -115,7 +115,13 @@ export function useOtaUpdate(options: UseOtaUpdateOptions): UseOtaUpdateReturn {
|
|
|
115
115
|
setIsSyncing(true);
|
|
116
116
|
setDownloadProgress(0);
|
|
117
117
|
try {
|
|
118
|
-
|
|
118
|
+
// Read through the ref, not the closed-over `options`. The dependency list
|
|
119
|
+
// below cannot name the targeting fields (`attributes` and `segmentIds` are
|
|
120
|
+
// fresh object/array identities on every render, so listing them would
|
|
121
|
+
// rebuild this callback each time), which left a host that changed
|
|
122
|
+
// `channelName` or `lifecycleStage` syncing against the values captured on
|
|
123
|
+
// first render. The ref is assigned on every render, so it is always current.
|
|
124
|
+
const result = await otaOrchestrator.sync(optionsRef.current);
|
|
119
125
|
setSyncResult(result);
|
|
120
126
|
|
|
121
127
|
// Handle mandatory update blocking
|
|
@@ -131,7 +137,10 @@ export function useOtaUpdate(options: UseOtaUpdateOptions): UseOtaUpdateReturn {
|
|
|
131
137
|
} finally {
|
|
132
138
|
setIsSyncing(false);
|
|
133
139
|
}
|
|
134
|
-
|
|
140
|
+
// Empty: everything this reads comes from `optionsRef`, so the callback has a
|
|
141
|
+
// stable identity and never needs rebuilding. A host can safely pass it to a
|
|
142
|
+
// memoised child or an effect dependency list.
|
|
143
|
+
}, []);
|
|
135
144
|
|
|
136
145
|
const restart = useCallback(() => {
|
|
137
146
|
setMandatoryUpdatePending(false);
|
|
@@ -327,6 +327,17 @@ class ScaleBunFacade {
|
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
+
// `healthyAfterMs` is the field BootGuardConfig actually declares.
|
|
331
|
+
// This passed `healthyTimeoutMs`, which nothing reads, so a host that
|
|
332
|
+
// tuned the boot-guard window silently got the 10s default instead.
|
|
333
|
+
// Both spellings are accepted so the older one keeps working.
|
|
334
|
+
const healthyAfterMs =
|
|
335
|
+
typeof ota.healthyAfterMs === 'number'
|
|
336
|
+
? ota.healthyAfterMs
|
|
337
|
+
: typeof ota.healthyTimeoutMs === 'number'
|
|
338
|
+
? ota.healthyTimeoutMs
|
|
339
|
+
: undefined;
|
|
340
|
+
|
|
330
341
|
otaOrchestrator.init({
|
|
331
342
|
...(signingRequested
|
|
332
343
|
? {
|
|
@@ -338,16 +349,129 @@ class ScaleBunFacade {
|
|
|
338
349
|
},
|
|
339
350
|
}
|
|
340
351
|
: {}),
|
|
341
|
-
...(
|
|
342
|
-
? { healthyTimeoutMs: ota.healthyTimeoutMs }
|
|
343
|
-
: {}),
|
|
352
|
+
...(healthyAfterMs !== undefined ? { healthyAfterMs } : {}),
|
|
344
353
|
});
|
|
345
354
|
logger.info('[ScaleBun] OTA enabled from init config.');
|
|
355
|
+
|
|
356
|
+
// Run the checks the config asked for. Until this existed, `ota.enabled`
|
|
357
|
+
// initialised the orchestrator and then never checked anything: the
|
|
358
|
+
// documented `checkOnForeground` and `channelOverride` options were read
|
|
359
|
+
// by no code at all, and an app following the documented config received
|
|
360
|
+
// updates only if it ALSO drove `useOtaUpdate` or the CodePush shim by
|
|
361
|
+
// hand. Nothing logged, because "no update available" and "never asked"
|
|
362
|
+
// look identical from the outside.
|
|
363
|
+
this._startOtaChecks(ota);
|
|
346
364
|
} catch (err: any) {
|
|
347
365
|
logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
|
|
348
366
|
}
|
|
349
367
|
}
|
|
350
368
|
|
|
369
|
+
/** Guards against overlapping config-driven OTA checks. */
|
|
370
|
+
private _otaCheckInFlight = false;
|
|
371
|
+
/** Wall clock of the last config-driven check, for the foreground floor. */
|
|
372
|
+
private _otaLastCheckAt = 0;
|
|
373
|
+
private _otaForegroundListener: ((state: string) => void) | null = null;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Minimum gap between config-driven checks.
|
|
377
|
+
*
|
|
378
|
+
* A foreground transition is cheap to trigger — app switchers, permission
|
|
379
|
+
* dialogs and share sheets all produce one — so an unthrottled check would
|
|
380
|
+
* put a request on the hot path every time the user glanced away. Ten
|
|
381
|
+
* minutes is well below any realistic release cadence and well above that
|
|
382
|
+
* noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
|
|
383
|
+
* which is never throttled.
|
|
384
|
+
*/
|
|
385
|
+
private static readonly OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Drive OTA checks from init config: once at startup, then on each
|
|
389
|
+
* foreground when `checkOnForeground` is on (the schema default).
|
|
390
|
+
*
|
|
391
|
+
* `appVersion` is resolved from the native bridge rather than asked of the
|
|
392
|
+
* integrator, because it gates the server's `targetAppVersion` semver check
|
|
393
|
+
* — sending a wrong or invented value is worse than sending none, and there
|
|
394
|
+
* is no honest default. If it cannot be resolved, the check is skipped with
|
|
395
|
+
* a warning instead of guessing.
|
|
396
|
+
*/
|
|
397
|
+
private _startOtaChecks(ota: any): void {
|
|
398
|
+
// Never in a debug build. `ScaleBunOtaModule.getJSBundleFile()` returns null
|
|
399
|
+
// there on purpose so Metro keeps ownership of the bundle — so a bundle
|
|
400
|
+
// downloaded in dev is installed into a slot that will never be loaded, and
|
|
401
|
+
// the identity check on the next launch then correctly observes that the
|
|
402
|
+
// running code is not what was installed and reports APPLY_FAILED. Checking
|
|
403
|
+
// at all in dev buys nothing and manufactures that false alarm. A developer
|
|
404
|
+
// testing the OTA path drives `useOtaUpdate().sync()` explicitly.
|
|
405
|
+
if (__DEV__) {
|
|
406
|
+
logger.info('[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const runCheck = async (trigger: 'startup' | 'foreground'): Promise<void> => {
|
|
411
|
+
if (this._otaCheckInFlight) return;
|
|
412
|
+
if (
|
|
413
|
+
trigger === 'foreground' &&
|
|
414
|
+
Date.now() - this._otaLastCheckAt < ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS
|
|
415
|
+
) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const clientKey = this._clientKey;
|
|
419
|
+
const apiUrl = this._apiBaseUrl;
|
|
420
|
+
if (!clientKey || !apiUrl) return;
|
|
421
|
+
|
|
422
|
+
this._otaCheckInFlight = true;
|
|
423
|
+
try {
|
|
424
|
+
const info = await bridgeAdapter.getDeviceInfo();
|
|
425
|
+
const appVersion = (info as { appVersion?: string } | null)?.appVersion;
|
|
426
|
+
if (!appVersion) {
|
|
427
|
+
logger.warn(
|
|
428
|
+
'[ScaleBun] OTA check skipped — the app version could not be read from the ' +
|
|
429
|
+
'native bridge. Rebuild the native app, or drive checks yourself with ' +
|
|
430
|
+
'useOtaUpdate({ appVersion }).',
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this._otaLastCheckAt = Date.now();
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
436
|
+
const { otaOrchestrator } = require('../features/ota/OtaOrchestrator');
|
|
437
|
+
await otaOrchestrator.sync({
|
|
438
|
+
apiUrl,
|
|
439
|
+
clientKey,
|
|
440
|
+
appVersion,
|
|
441
|
+
// The documented option, finally connected. Omitted means the
|
|
442
|
+
// server's `default` channel, exactly as before.
|
|
443
|
+
channelName: typeof ota.channelOverride === 'string' ? ota.channelOverride : undefined,
|
|
444
|
+
// Never forced from config: the release's own installMode
|
|
445
|
+
// decides when the app restarts, and yanking the screen out
|
|
446
|
+
// from under a user is not a decision this switch should make.
|
|
447
|
+
});
|
|
448
|
+
} catch (err: any) {
|
|
449
|
+
logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
|
|
450
|
+
} finally {
|
|
451
|
+
this._otaCheckInFlight = false;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
void runCheck('startup');
|
|
456
|
+
|
|
457
|
+
if (ota.checkOnForeground === false) return;
|
|
458
|
+
if (this._otaForegroundListener) return; // idempotent across repeated init()
|
|
459
|
+
try {
|
|
460
|
+
// Through `appLifecycle`, not a second AppState subscription: the SDK
|
|
461
|
+
// already owns one and fanning out from it keeps every consumer on the
|
|
462
|
+
// same transition sequence.
|
|
463
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
464
|
+
const { appLifecycle } = require('../core/lifecycle/appLifecycle');
|
|
465
|
+
this._otaForegroundListener = (state: string) => {
|
|
466
|
+
if (state === 'active') void runCheck('foreground');
|
|
467
|
+
};
|
|
468
|
+
appLifecycle.addListener(this._otaForegroundListener);
|
|
469
|
+
} catch {
|
|
470
|
+
// Lifecycle unavailable (tests, exotic hosts) — the startup check stands.
|
|
471
|
+
this._otaForegroundListener = null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
351
475
|
private _autoEnableDebug(debugConfig: DebugConnectionConfig): void {
|
|
352
476
|
try {
|
|
353
477
|
const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
|