@onekeyfe/react-native-split-bundle-loader 3.0.64 → 3.0.66

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.
@@ -3,6 +3,7 @@
3
3
  #import <ReactCommon/RCTHost.h>
4
4
  #import <ReactCommon/RCTHost+Internal.h>
5
5
  #import <ReactCommon/RCTInstance.h>
6
+ #import <UIKit/UIKit.h>
6
7
  #import <objc/runtime.h>
7
8
  #import <CommonCrypto/CommonDigest.h>
8
9
  #import <os/lock.h>
@@ -74,6 +75,313 @@ class NSDataJSIBuffer : public facebook::jsi::Buffer {
74
75
  }
75
76
  @end
76
77
 
78
+ // ACTIVE-TIME watchdog for the buffered runtime executor (replaces the bare
79
+ // dispatch_after C1 watchdog).
80
+ //
81
+ // WHY: the old watchdog was a single `dispatch_after(NOW + 30s)`. dispatch_after
82
+ // arms against an ABSOLUTE wall/uptime deadline that keeps elapsing while the
83
+ // app is backgrounded/suspended. If the app is suspended during cold start while
84
+ // a segment's eval is still BUFFERED (waiting for the entry bundle to finish),
85
+ // the 30s deadline can pass entirely off-screen; on FOREGROUND RESUME the stale
86
+ // block fires essentially instantly and wins the SBLSettleGuard race against the
87
+ // buffered executor that was ~1ms from succeeding — false-rejecting the segment
88
+ // as SPLIT_BUNDLE_TIMEOUT and white-screening the app.
89
+ //
90
+ // This watchdog instead accumulates ONLY foreground/active wall-time: suspended
91
+ // time never accrues toward the 30s, and a fresh foreground-grace window after
92
+ // every resume guarantees the buffered executor gets a chance to flush first, so
93
+ // a stale deadline can never fire instantly on resume.
94
+ //
95
+ // Time base: CLOCK_UPTIME_RAW already excludes device sleep; on top of that we
96
+ // only count intervals during which the app is in the active state. All mutable
97
+ // timing state is confined to a single serial queue (_queue) so the timer tick,
98
+ // the start/cancel calls, and the foreground/background notification callbacks
99
+ // never race. The fired/cancelled one-shot flag makes both onTimeout and teardown
100
+ // happen at most once regardless of which thread triggers them.
101
+ @interface SBLActiveWatchdog : NSObject
102
+ - (instancetype)initWithTimeoutMs:(uint64_t)timeoutMs
103
+ foregroundGraceMs:(uint64_t)foregroundGraceMs
104
+ onTimeout:(dispatch_block_t)onTimeout;
105
+ - (void)start;
106
+ - (void)cancel;
107
+ @end
108
+
109
+ @implementation SBLActiveWatchdog {
110
+ // Serial queue that owns ALL mutable state below — every read/write of these
111
+ // ivars happens on _queue, so no additional lock is needed.
112
+ dispatch_queue_t _queue;
113
+ dispatch_source_t _timer;
114
+
115
+ uint64_t _timeoutMs; // active-time budget before firing (e.g. 30000).
116
+ uint64_t _foregroundGraceMs; // post-resume window during which we won't fire.
117
+
118
+ uint64_t _accumulatedActiveMs; // folded active time from completed intervals.
119
+ uint64_t _currentIntervalStartUptimeNs; // CLOCK_UPTIME_RAW at active start.
120
+ uint64_t _graceUntilUptimeNs; // no fire before this uptime (grace window).
121
+ BOOL _isActive; // app currently in active/foreground state.
122
+ BOOL _finished; // one-shot: fired OR cancelled.
123
+
124
+ dispatch_block_t _onTimeout;
125
+ BOOL _observersRegistered;
126
+ }
127
+
128
+ - (instancetype)initWithTimeoutMs:(uint64_t)timeoutMs
129
+ foregroundGraceMs:(uint64_t)foregroundGraceMs
130
+ onTimeout:(dispatch_block_t)onTimeout {
131
+ if (self = [super init]) {
132
+ _queue = dispatch_queue_create("com.onekey.splitbundle.watchdog", DISPATCH_QUEUE_SERIAL);
133
+ _timeoutMs = timeoutMs;
134
+ _foregroundGraceMs = foregroundGraceMs;
135
+ _onTimeout = [onTimeout copy];
136
+ _accumulatedActiveMs = 0;
137
+ _currentIntervalStartUptimeNs = 0;
138
+ _graceUntilUptimeNs = 0;
139
+ _isActive = NO;
140
+ _finished = NO;
141
+ _observersRegistered = NO;
142
+ }
143
+ return self;
144
+ }
145
+
146
+ static uint64_t SBLNowUptimeNs(void) {
147
+ return clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
148
+ }
149
+
150
+ // Current active-interval elapsed (ms) while _isActive; 0 otherwise. _queue only.
151
+ - (uint64_t)currentIntervalMsLocked {
152
+ if (!_isActive || _currentIntervalStartUptimeNs == 0) {
153
+ return 0;
154
+ }
155
+ uint64_t now = SBLNowUptimeNs();
156
+ if (now <= _currentIntervalStartUptimeNs) {
157
+ return 0;
158
+ }
159
+ return (now - _currentIntervalStartUptimeNs) / 1000000ULL;
160
+ }
161
+
162
+ - (void)start {
163
+ // Seed initial app-state + register observers on the MAIN thread:
164
+ // -applicationState and the notification center are both main-thread concerns.
165
+ // We then hop to _queue with the captured `activeNow` to prime timing + timer,
166
+ // so the timing-state ivars are only ever touched on _queue.
167
+ dispatch_async(dispatch_get_main_queue(), ^{
168
+ [self registerObservers];
169
+ // Treat anything other than Background as "active" for timing purposes
170
+ // (Inactive still makes progress toward the wedge).
171
+ UIApplicationState appState = UIApplication.sharedApplication.applicationState;
172
+ BOOL activeNow = (appState != UIApplicationStateBackground);
173
+ dispatch_async(self->_queue, ^{
174
+ [self primeTimerLockedWithActive:activeNow];
175
+ });
176
+ });
177
+ }
178
+
179
+ // Initialize timing state and create+resume the polling timer. _queue only.
180
+ - (void)primeTimerLockedWithActive:(BOOL)activeNow {
181
+ if (_finished) {
182
+ return; // cancelled before we got to prime — nothing to do.
183
+ }
184
+ _isActive = activeNow;
185
+ // Apply an initial grace window so a slow first foreground frame doesn't
186
+ // immediately trip the watchdog the very first tick. Read the clock ONCE so
187
+ // the interval start and the grace deadline share the same base instant.
188
+ if (activeNow) {
189
+ uint64_t now = SBLNowUptimeNs();
190
+ _currentIntervalStartUptimeNs = now;
191
+ _graceUntilUptimeNs = now + _foregroundGraceMs * 1000000ULL;
192
+ } else {
193
+ _currentIntervalStartUptimeNs = 0;
194
+ }
195
+
196
+ // Polling timer: survives pause/resume trivially (unlike a one-shot
197
+ // dispatch_after). Tick every 500ms. Leeway gives the scheduler slack.
198
+ _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
199
+ if (!_timer) {
200
+ // Fail safe: if the timer source can't be created we cannot guard against a
201
+ // wedge. Settle now (as a RETRYABLE timeout) via fireLocked rather than
202
+ // leaving the watchdog half-started (observers registered, _finished=NO,
203
+ // tick never firing) — that stuck state would hang loadSegment: forever.
204
+ // The JS loader re-attempts on SPLIT_BUNDLE_TIMEOUT, and the buffered
205
+ // executor, if it does run, still benefits the module table.
206
+ [self fireLocked];
207
+ return;
208
+ }
209
+ dispatch_source_set_timer(_timer,
210
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)),
211
+ (uint64_t)(500 * NSEC_PER_MSEC),
212
+ (uint64_t)(100 * NSEC_PER_MSEC));
213
+ __weak SBLActiveWatchdog *weakSelf = self;
214
+ dispatch_source_set_event_handler(_timer, ^{
215
+ SBLActiveWatchdog *strongSelf = weakSelf;
216
+ if (strongSelf) {
217
+ [strongSelf tickLocked];
218
+ }
219
+ });
220
+ dispatch_resume(_timer);
221
+ }
222
+
223
+ // Timer tick — runs on _queue (the timer's own queue), so state is consistent.
224
+ - (void)tickLocked {
225
+ if (_finished) {
226
+ return;
227
+ }
228
+ if (!_isActive) {
229
+ return; // only foreground/active time counts toward the timeout.
230
+ }
231
+ uint64_t now = SBLNowUptimeNs();
232
+ if (now < _graceUntilUptimeNs) {
233
+ return; // inside the post-resume grace window — give the executor a chance.
234
+ }
235
+ uint64_t totalActiveMs = _accumulatedActiveMs + [self currentIntervalMsLocked];
236
+ if (totalActiveMs >= _timeoutMs) {
237
+ [self fireLocked];
238
+ }
239
+ }
240
+
241
+ // Fire onTimeout at most once, then tear everything down. _queue only.
242
+ - (void)fireLocked {
243
+ if (_finished) {
244
+ return;
245
+ }
246
+ _finished = YES;
247
+ dispatch_block_t cb = _onTimeout;
248
+ _onTimeout = nil;
249
+ [self teardownTimerLocked];
250
+ // Remove observers on the main thread (where they were registered).
251
+ dispatch_async(dispatch_get_main_queue(), ^{
252
+ [self removeObservers];
253
+ });
254
+ if (cb) {
255
+ cb();
256
+ }
257
+ }
258
+
259
+ // Cancel the timer source exactly once. _queue only.
260
+ - (void)teardownTimerLocked {
261
+ if (_timer) {
262
+ dispatch_source_cancel(_timer);
263
+ _timer = nil; // drop our ref; the source is retained until cancel completes.
264
+ }
265
+ }
266
+
267
+ // Public cancel — thread-safe, may be called from the executor block's success
268
+ // path on a DIFFERENT thread. Hops onto _queue so it serializes with the tick.
269
+ - (void)cancel {
270
+ dispatch_async(_queue, ^{
271
+ if (self->_finished) {
272
+ return;
273
+ }
274
+ self->_finished = YES;
275
+ self->_onTimeout = nil;
276
+ [self teardownTimerLocked];
277
+ dispatch_async(dispatch_get_main_queue(), ^{
278
+ [self removeObservers];
279
+ });
280
+ });
281
+ }
282
+
283
+ // MARK: - App-state observers (main thread)
284
+
285
+ - (void)registerObservers {
286
+ if (_observersRegistered) {
287
+ return;
288
+ }
289
+ _observersRegistered = YES;
290
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
291
+ [nc addObserver:self
292
+ selector:@selector(handleDidBecomeActive)
293
+ name:UIApplicationDidBecomeActiveNotification
294
+ object:nil];
295
+ [nc addObserver:self
296
+ selector:@selector(handleWillResignActive)
297
+ name:UIApplicationWillResignActiveNotification
298
+ object:nil];
299
+ // DidEnterBackground is folded in too: on some transitions WillResignActive
300
+ // and DidEnterBackground both arrive; the resign handler is idempotent (it
301
+ // no-ops when already inactive), so treating background like resign is safe.
302
+ [nc addObserver:self
303
+ selector:@selector(handleWillResignActive)
304
+ name:UIApplicationDidEnterBackgroundNotification
305
+ object:nil];
306
+ }
307
+
308
+ - (void)removeObservers {
309
+ if (!_observersRegistered) {
310
+ return;
311
+ }
312
+ _observersRegistered = NO;
313
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
314
+ }
315
+
316
+ - (void)handleWillResignActive {
317
+ // Sample the resign instant on the main thread BEFORE the app can suspend.
318
+ uint64_t resignAtUptimeNs = SBLNowUptimeNs();
319
+ // Fold + pause SYNCHRONOUSLY (dispatch_sync, not async). The pause must take
320
+ // effect before this lifecycle callback returns — i.e. before the app is
321
+ // suspended. Why sync is required, not just a synchronous timestamp:
322
+ // - the polling timer fires on _queue; a tick may already be PENDING on
323
+ // _queue when we resign. tickLocked recomputes `now` at EXECUTION time, so
324
+ // if that pending tick doesn't run until the app RESUMES (minutes later),
325
+ // it would see _isActive==YES + the old interval start and fold all the
326
+ // suspended-but-awake time (CLOCK_UPTIME_RAW keeps advancing) → fire a
327
+ // false SPLIT_BUNDLE_TIMEOUT before the resume grace is even armed.
328
+ // - dispatch_sync drains the serial queue first: any pending tick runs NOW
329
+ // (at resign time, pre-suspension, with a valid small interval — no false
330
+ // fire), then this fold runs and sets _isActive=NO. So by the time the app
331
+ // suspends, the clock is stopped and no stale tick can fire on resume.
332
+ // Safe from deadlock: _queue blocks never dispatch_sync back to the main
333
+ // thread (observer removal uses dispatch_async), and the queue's blocks are
334
+ // all O(1).
335
+ dispatch_sync(_queue, ^{
336
+ if (self->_finished || !self->_isActive) {
337
+ return; // idempotent: already inactive or already settled.
338
+ }
339
+ // Fold ONLY the active interval up to resignAt into the accumulator, then
340
+ // stop the clock. Suspended time after this point does NOT accrue.
341
+ if (self->_currentIntervalStartUptimeNs != 0 &&
342
+ resignAtUptimeNs > self->_currentIntervalStartUptimeNs) {
343
+ self->_accumulatedActiveMs +=
344
+ (resignAtUptimeNs - self->_currentIntervalStartUptimeNs) / 1000000ULL;
345
+ }
346
+ self->_isActive = NO;
347
+ self->_currentIntervalStartUptimeNs = 0;
348
+ });
349
+ }
350
+
351
+ - (void)handleDidBecomeActive {
352
+ dispatch_async(_queue, ^{
353
+ if (self->_finished || self->_isActive) {
354
+ return;
355
+ }
356
+ // Resume the clock and arm a fresh grace window so the buffered executor
357
+ // gets a chance to flush before the watchdog can fire again. One clock read
358
+ // so the interval start and the grace deadline share the same base instant.
359
+ uint64_t now = SBLNowUptimeNs();
360
+ self->_isActive = YES;
361
+ self->_currentIntervalStartUptimeNs = now;
362
+ self->_graceUntilUptimeNs = now + self->_foregroundGraceMs * 1000000ULL;
363
+ });
364
+ }
365
+
366
+ - (void)dealloc {
367
+ // Defensive: observers are normally removed via cancel/fire, but if this
368
+ // object is released without either (shouldn't happen — the executor block
369
+ // retains it until cancel), make sure we don't leave a dangling observer.
370
+ if (_observersRegistered) {
371
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
372
+ }
373
+ // Make the "timer is nil by dealloc" invariant explicit: cancel/fireLocked
374
+ // both null _timer before the last retain drops, but guard here so a future
375
+ // early-return on those paths can't orphan a running dispatch_source_t. Safe
376
+ // to touch _timer without hopping to _queue — no other thread references a
377
+ // deallocating object.
378
+ if (_timer) {
379
+ dispatch_source_cancel(_timer);
380
+ _timer = nil;
381
+ }
382
+ }
383
+ @end
384
+
77
385
  // Watchdog window for the buffered runtime executor (C1). Post-entry eval is
78
386
  // sub-millisecond, so this only ever elapses on a genuine wedge (entry bundle
79
387
  // never finished evaluating) — never on a healthy slow device.
@@ -362,11 +670,30 @@ typedef NS_ENUM(NSInteger, ESegmentEvalError) {
362
670
  // either order, on a wedge).
363
671
  SBLSettleGuard *settleGuard = [[SBLSettleGuard alloc] init];
364
672
 
673
+ // C1 active-time watchdog. Constructed BEFORE the executor block so the block
674
+ // can capture (retain) it and tear it down on the happy path. It fires only
675
+ // on a genuine wedge AND only after kSegmentEvalWatchdogSeconds of
676
+ // FOREGROUND/active time has accrued — backgrounded/suspended time never
677
+ // counts, so a stale deadline can never fire instantly on a foreground
678
+ // resume (the white-screen bug this replaces). On fire it settles the SAME
679
+ // SBLSettleGuard via tryClaim, so there is still exactly one settle.
680
+ SBLActiveWatchdog *watchdog = [[SBLActiveWatchdog alloc]
681
+ initWithTimeoutMs:(uint64_t)(kSegmentEvalWatchdogSeconds * 1000.0)
682
+ foregroundGraceMs:500
683
+ onTimeout:^{
684
+ if ([settleGuard tryClaim]) {
685
+ [SBLLogger error:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ (key=%@) WATCHDOG fired after %.0fs active time — runtime executor never ran (entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, segmentKey, kSegmentEvalWatchdogSeconds]];
686
+ onEvaluated([NSError errorWithDomain:@"SplitBundleLoader"
687
+ code:ESegmentEvalErrorTimeout
688
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment %@ eval timed out after %.0fs active time (buffered runtime executor never ran)", segmentKey, kSegmentEvalWatchdogSeconds]}]);
689
+ }
690
+ }];
691
+
365
692
  [instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) {
366
693
  @autoreleasepool {
367
- // If the watchdog already fired (entry took >30s then unwedged),
368
- // the JS promise is already rejected — still evaluate the segment
369
- // (the module table benefits) but don't double-settle.
694
+ // If the watchdog already fired (entry took >30s active then
695
+ // unwedged), the JS promise is already rejected — still evaluate the
696
+ // segment (the module table benefits) but don't double-settle.
370
697
  BOOL won = [settleGuard tryClaim];
371
698
  NSError *evalError = nil;
372
699
  CFAbsoluteTime evalStart = CFAbsoluteTimeGetCurrent();
@@ -389,33 +716,31 @@ typedef NS_ENUM(NSInteger, ESegmentEvalError) {
389
716
  userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment evaluation failed for %@ (unknown C++ exception)", sourceURL]}];
390
717
  [SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluation threw an unknown exception", sourceURL]];
391
718
  }
719
+ // Tear down the active-time watchdog (timer + observers) regardless
720
+ // of who won the settle: the buffered executor has now run, so the
721
+ // watchdog has no further job. Capturing `watchdog` here is also what
722
+ // RETAINS it for the lifetime of this async block (until cancel). It
723
+ // is safe to cancel from this (different) thread — cancel hops onto
724
+ // the watchdog's own serial queue and is a one-shot.
725
+ [watchdog cancel];
392
726
  if (won) {
393
727
  // Resolve/reject the JS promise from INSIDE this same block,
394
728
  // strictly AFTER the segment eval above — the ordering guarantee
395
729
  // that fixes the "Requiring unknown module" race (see method doc).
396
730
  onEvaluated(evalError);
397
731
  } else {
398
- [SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluated AFTER watchdog already settled (entry was wedged >%.0fs)", sourceURL, kSegmentEvalWatchdogSeconds]];
732
+ [SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluated AFTER watchdog already settled (entry was wedged >%.0fs active time)", sourceURL, kSegmentEvalWatchdogSeconds]];
399
733
  }
400
734
  }
401
735
  }];
402
736
 
403
- // C1 watchdog. Fires only on a genuine wedge: in steady state the entry
404
- // bundle is long done and the buffered block runs sub-millisecond, so the
405
- // guard is already claimed (tryClaim returns NO) long before this elapses.
406
- // On a real wedge it settles the promise with a distinct, RETRYABLE timeout
407
- // error so the JS loader can re-attempt instead of hanging inflightSegments
408
- // forever. The block retains settleGuard, so the lock stays valid even if
409
- // the executor block later runs (entry finally evaluates).
410
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kSegmentEvalWatchdogSeconds * NSEC_PER_SEC)),
411
- dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
412
- if ([settleGuard tryClaim]) {
413
- [SBLLogger error:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ (key=%@) WATCHDOG fired after %.0fs — runtime executor never ran (entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, segmentKey, kSegmentEvalWatchdogSeconds]];
414
- onEvaluated([NSError errorWithDomain:@"SplitBundleLoader"
415
- code:ESegmentEvalErrorTimeout
416
- userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment %@ eval timed out after %.0fs (buffered runtime executor never ran)", segmentKey, kSegmentEvalWatchdogSeconds]}]);
417
- }
418
- });
737
+ // Arm the watchdog AFTER scheduling the executor. Fires only on a genuine
738
+ // wedge: in steady state the entry bundle is long done and the buffered block
739
+ // runs sub-millisecond, so it cancels the watchdog (and the guard is already
740
+ // claimed) long before kSegmentEvalWatchdogSeconds of ACTIVE time elapses.
741
+ // Because only foreground/active time is counted, a suspend-during-cold-start
742
+ // can no longer let a stale deadline false-fire on resume.
743
+ [watchdog start];
419
744
 
420
745
  double dispatchMs = (CFAbsoluteTimeGetCurrent() - dispatchStart) * 1000.0;
421
746
  [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ dispatched in %.1fms (resolve fires after eval; watchdog %.0fs)", sourceURL, dispatchMs, kSegmentEvalWatchdogSeconds]];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-split-bundle-loader",
3
- "version": "3.0.64",
3
+ "version": "3.0.66",
4
4
  "description": "react-native-split-bundle-loader",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",