react-native-smooth-clip-view 0.2.5 → 0.2.7
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/README.md +68 -26
- package/android/CMakeLists.txt +0 -1
- package/android/build.gradle +15 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +12 -8
- package/android/src/main/cpp/SmoothClipBindings.cpp +39 -15
- package/android/src/main/cpp/SmoothClipRegistry.cpp +288 -285
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +28 -4
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +180 -42
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +1 -0
- package/cpp/SmoothClipAnimationCurve.h +560 -0
- package/cpp/SmoothClipAnimationId.h +32 -0
- package/cpp/SmoothClipRegistry.h +11 -1
- package/cpp/SmoothClipVelocityTracker.h +30 -10
- package/ios/SmoothClipRegistry.mm +398 -150
- package/ios/SmoothClipTurboModule.cpp +4 -2
- package/ios/SmoothClipTurboModule.h +2 -1
- package/ios/SmoothClipView.mm +85 -140
- package/ios/SmoothClipViewRegistry.h +4 -1
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/driverState.js +8 -0
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.native.js +122 -32
- package/lib/module/drivers.native.js.map +1 -1
- package/lib/module/reactRequests.js +9 -3
- package/lib/module/reactRequests.js.map +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/driverState.d.ts.map +1 -1
- package/lib/typescript/src/driverTypes.d.ts +3 -2
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts.map +1 -1
- package/lib/typescript/src/reactRequests.d.ts +3 -1
- package/lib/typescript/src/reactRequests.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverState.ts +8 -0
- package/src/driverTypes.ts +3 -2
- package/src/drivers.native.ts +160 -29
- package/src/reactRequests.ts +14 -2
package/src/drivers.native.ts
CHANGED
|
@@ -46,9 +46,25 @@ const NATIVE = 1;
|
|
|
46
46
|
|
|
47
47
|
const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
|
|
48
48
|
const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
|
|
50
|
+
// The animate* host functions take one argument beyond the TurboModule spec:
|
|
51
|
+
// the Reanimated-rule start stamp in milliseconds (see animateOnUI). Android's
|
|
52
|
+
// JSI bindings read it as an optional trailing argument; the iOS TurboModule
|
|
53
|
+
// reads its declared parameters positionally and ignores extras, so the
|
|
54
|
+
// codegen spec stays untouched and both platforms take the same call. A
|
|
55
|
+
// narrower function is assignable to the widened type, so no cast is needed.
|
|
56
|
+
type WithStartStamp<F> = F extends (...args: infer A) => infer R
|
|
57
|
+
? (...args: [...A, startedAtMs: number]) => R
|
|
58
|
+
: never;
|
|
59
|
+
const animateTimingHostFunction: WithStartStamp<
|
|
60
|
+
typeof NativeSmoothClipModule.animateTiming
|
|
61
|
+
> = NativeSmoothClipModule.animateTiming;
|
|
62
|
+
const animateSpringHostFunction: WithStartStamp<
|
|
63
|
+
typeof NativeSmoothClipModule.animateSpring
|
|
64
|
+
> = NativeSmoothClipModule.animateSpring;
|
|
65
|
+
const animateKeyframesHostFunction: WithStartStamp<
|
|
66
|
+
typeof NativeSmoothClipModule.animateKeyframes
|
|
67
|
+
> = NativeSmoothClipModule.animateKeyframes;
|
|
52
68
|
const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
|
|
53
69
|
const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
|
|
54
70
|
const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
|
|
@@ -91,9 +107,19 @@ function animationIsFinite(
|
|
|
91
107
|
return false;
|
|
92
108
|
}
|
|
93
109
|
if (animation.type === 'timing') {
|
|
110
|
+
const [x1, , x2] = animation.controlPoints;
|
|
94
111
|
return (
|
|
95
112
|
Number.isFinite(animation.duration) &&
|
|
96
|
-
animation.controlPoints.every(Number.isFinite)
|
|
113
|
+
animation.controlPoints.every(Number.isFinite) &&
|
|
114
|
+
// A cubic-bezier easing is only defined for x within [0,1] (the CSS /
|
|
115
|
+
// CoreAnimation / Reanimated Easing.bezier contract). Out-of-range x
|
|
116
|
+
// makes the Android parameter solve meaningless while CoreAnimation
|
|
117
|
+
// silently clamps — reject like any other invalid input instead of
|
|
118
|
+
// diverging per platform.
|
|
119
|
+
x1 >= 0 &&
|
|
120
|
+
x1 <= 1 &&
|
|
121
|
+
x2 >= 0 &&
|
|
122
|
+
x2 <= 1
|
|
97
123
|
);
|
|
98
124
|
}
|
|
99
125
|
if (animation.type === 'spring') {
|
|
@@ -195,6 +221,14 @@ export function useSmoothClipDriver(
|
|
|
195
221
|
// so animateTo must start from the native registry's latest value instead
|
|
196
222
|
// of snapping back to the stale SharedValue.
|
|
197
223
|
const scalarsStale = useSharedValue(0);
|
|
224
|
+
// Non-zero between the effect cleanup's native teardown and the next effect
|
|
225
|
+
// run. Native cannot tell "this driver was never seeded" from "this driver
|
|
226
|
+
// was destroyed and erased" — both are a missing registry entry — so the
|
|
227
|
+
// animation entry points would happily create a fresh state for a call that
|
|
228
|
+
// arrives after the hook is gone, leaving a latch that no view starts and no
|
|
229
|
+
// destroy cancels. Only the UI side knows the difference, so it is decided
|
|
230
|
+
// here.
|
|
231
|
+
const disposed = useSharedValue(0);
|
|
198
232
|
const callbackRef = useRef(options.onAnimationComplete);
|
|
199
233
|
const driverRef = useRef<SmoothClipDriver | null>(null);
|
|
200
234
|
callbackRef.current = options.onAnimationComplete;
|
|
@@ -205,14 +239,21 @@ export function useSmoothClipDriver(
|
|
|
205
239
|
|
|
206
240
|
const seedPresentation = (next: SmoothClipPresentation) => {
|
|
207
241
|
'worklet';
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
242
|
+
if (clipPresentationEquals(presentation.value, next)) return;
|
|
243
|
+
suppressDeliveries.value += 1;
|
|
244
|
+
presentation.value = next;
|
|
245
|
+
// The listener runs synchronously inside that assignment, so the credit
|
|
246
|
+
// is either spent by now or was never spendable. deliver() returns before
|
|
247
|
+
// the decrement whenever the value dedupes against `last` — which happens
|
|
248
|
+
// exactly when native already holds it, e.g. freezing a latch back to the
|
|
249
|
+
// value it started from. An unspent credit would then swallow the next
|
|
250
|
+
// real declarative write and leave native a geometry behind for good.
|
|
251
|
+
if (suppressDeliveries.value > 0) suppressDeliveries.value -= 1;
|
|
212
252
|
};
|
|
213
253
|
|
|
214
254
|
const beginOnUI = (): SmoothClipPresentation => {
|
|
215
255
|
'worklet';
|
|
256
|
+
if (disposed.value !== 0) return presentation.value;
|
|
216
257
|
const current = presentationFromNative(
|
|
217
258
|
beginInteractionHostFunction(driverId),
|
|
218
259
|
presentation.value
|
|
@@ -226,7 +267,7 @@ export function useSmoothClipDriver(
|
|
|
226
267
|
|
|
227
268
|
const setOnUI = (next: SmoothClipPresentation): void => {
|
|
228
269
|
'worklet';
|
|
229
|
-
if (!isFiniteClipPresentation(next)) return;
|
|
270
|
+
if (disposed.value !== 0 || !isFiniteClipPresentation(next)) return;
|
|
230
271
|
activeAnimationId.value = 0;
|
|
231
272
|
ownership.value = INTERACTIVE;
|
|
232
273
|
scalarsStale.value = 0;
|
|
@@ -240,7 +281,8 @@ export function useSmoothClipDriver(
|
|
|
240
281
|
clip.radius,
|
|
241
282
|
contentTranslateX,
|
|
242
283
|
contentTranslateY,
|
|
243
|
-
true
|
|
284
|
+
true,
|
|
285
|
+
false
|
|
244
286
|
);
|
|
245
287
|
seedPresentation(next);
|
|
246
288
|
};
|
|
@@ -252,10 +294,12 @@ export function useSmoothClipDriver(
|
|
|
252
294
|
height: number,
|
|
253
295
|
radius: number,
|
|
254
296
|
contentTranslateX: number,
|
|
255
|
-
contentTranslateY: number
|
|
297
|
+
contentTranslateY: number,
|
|
298
|
+
overridePendingAnimation = false
|
|
256
299
|
): void => {
|
|
257
300
|
'worklet';
|
|
258
301
|
if (
|
|
302
|
+
disposed.value !== 0 ||
|
|
259
303
|
!Number.isFinite(x) ||
|
|
260
304
|
!Number.isFinite(y) ||
|
|
261
305
|
!Number.isFinite(width) ||
|
|
@@ -278,7 +322,8 @@ export function useSmoothClipDriver(
|
|
|
278
322
|
radius,
|
|
279
323
|
contentTranslateX,
|
|
280
324
|
contentTranslateY,
|
|
281
|
-
true
|
|
325
|
+
true,
|
|
326
|
+
overridePendingAnimation
|
|
282
327
|
);
|
|
283
328
|
};
|
|
284
329
|
|
|
@@ -287,13 +332,22 @@ export function useSmoothClipDriver(
|
|
|
287
332
|
animation: SmoothClipAnimation
|
|
288
333
|
): number => {
|
|
289
334
|
'worklet';
|
|
335
|
+
// Cleanup owns the terminal state. Check it before validation so even an
|
|
336
|
+
// invalid stale call cannot mint a native rejection id/completion against
|
|
337
|
+
// a driver whose listener and registry entry are already gone.
|
|
338
|
+
if (disposed.value !== 0) return 0;
|
|
290
339
|
if (
|
|
291
340
|
!isFiniteClipPresentation(target) ||
|
|
292
341
|
!animationIsFinite(target, animation)
|
|
293
342
|
) {
|
|
294
343
|
return rejectAnimationHostFunction(driverId);
|
|
295
344
|
}
|
|
296
|
-
|
|
345
|
+
// Past the hook's cleanup the native entry is gone for good, and an
|
|
346
|
+
// interactive start here would recreate it as a latch nothing can ever
|
|
347
|
+
// start or cancel. Reject before any side effect — not via
|
|
348
|
+
// rejectAnimation, which would mint an id and a completion for a driver
|
|
349
|
+
// whose JS state is already detached. This is the "unsupported dispatch"
|
|
350
|
+
// arm of the documented 0 contract.
|
|
297
351
|
const from = animation.from;
|
|
298
352
|
if (from !== undefined) {
|
|
299
353
|
// Fused hot write: exactly setScalars(from…) issued immediately
|
|
@@ -308,7 +362,8 @@ export function useSmoothClipDriver(
|
|
|
308
362
|
from.clip.height,
|
|
309
363
|
from.clip.radius,
|
|
310
364
|
from.contentTranslateX,
|
|
311
|
-
from.contentTranslateY
|
|
365
|
+
from.contentTranslateY,
|
|
366
|
+
true
|
|
312
367
|
);
|
|
313
368
|
}
|
|
314
369
|
|
|
@@ -325,6 +380,26 @@ export function useSmoothClipDriver(
|
|
|
325
380
|
scalarsStale.value = 0;
|
|
326
381
|
const startClip = start.clip;
|
|
327
382
|
const targetClip = target.clip;
|
|
383
|
+
// Reanimated stamps a parallel animation's t0 on this same runtime as
|
|
384
|
+
// `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
|
|
385
|
+
// Capturing the identical value here and handing it to the integrator
|
|
386
|
+
// keeps clip and content phase-locked in every branch — including a
|
|
387
|
+
// start issued from CALLBACK_INPUT (batched gesture moves), where the
|
|
388
|
+
// dispatching frame's stamp is EARLIER than the call: the native min()
|
|
389
|
+
// anchor alone would adopt that frame stamp while Reanimated, outside
|
|
390
|
+
// its rAF flush where __frameTimestamp is cleared, keeps the wall
|
|
391
|
+
// clock — a lead lasting the whole animation. NaN when the globals are
|
|
392
|
+
// absent (tests, non-worklets runtimes): native then falls back to its
|
|
393
|
+
// own clock plus the min() anchor, the pre-stamp behavior.
|
|
394
|
+
const workletGlobal = globalThis as {
|
|
395
|
+
__frameTimestamp?: number;
|
|
396
|
+
_getAnimationTimestamp?: () => number;
|
|
397
|
+
};
|
|
398
|
+
const startedAtMs =
|
|
399
|
+
workletGlobal.__frameTimestamp ||
|
|
400
|
+
(typeof workletGlobal._getAnimationTimestamp === 'function'
|
|
401
|
+
? workletGlobal._getAnimationTimestamp()
|
|
402
|
+
: Number.NaN);
|
|
328
403
|
let animationId: number;
|
|
329
404
|
|
|
330
405
|
if (animation.type === 'timing') {
|
|
@@ -351,7 +426,8 @@ export function useSmoothClipDriver(
|
|
|
351
426
|
y1,
|
|
352
427
|
x2,
|
|
353
428
|
y2,
|
|
354
|
-
reduceMotion
|
|
429
|
+
reduceMotion,
|
|
430
|
+
startedAtMs
|
|
355
431
|
);
|
|
356
432
|
} else if (animation.type === 'spring') {
|
|
357
433
|
const inheritVelocity =
|
|
@@ -379,7 +455,8 @@ export function useSmoothClipDriver(
|
|
|
379
455
|
Math.max(0, animation.damping ?? 10),
|
|
380
456
|
inheritVelocity ? 0 : (animation.initialVelocity as number),
|
|
381
457
|
inheritVelocity,
|
|
382
|
-
reduceMotion
|
|
458
|
+
reduceMotion,
|
|
459
|
+
startedAtMs
|
|
383
460
|
);
|
|
384
461
|
} else {
|
|
385
462
|
animationId = animateKeyframesHostFunction(
|
|
@@ -401,16 +478,19 @@ export function useSmoothClipDriver(
|
|
|
401
478
|
target.contentTranslateY,
|
|
402
479
|
Math.max(0, animation.duration),
|
|
403
480
|
flattenedKeyframes(animation),
|
|
404
|
-
reduceMotion
|
|
481
|
+
reduceMotion,
|
|
482
|
+
startedAtMs
|
|
405
483
|
);
|
|
406
484
|
}
|
|
407
485
|
if (animationId === 0) {
|
|
408
|
-
// The 0 sentinel:
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
486
|
+
// The 0 sentinel: off-main, invalid-id, or otherwise unsupported
|
|
487
|
+
// dispatch. A missing driver is accepted when this call carries the
|
|
488
|
+
// authoritative interactive start above; a start-less missing-state
|
|
489
|
+
// request remains unsupported rather than inventing zero geometry.
|
|
490
|
+
// Validation failures never get here: they reject before any side
|
|
491
|
+
// effect, with a fresh id and one finished:false completion once the
|
|
492
|
+
// native entry exists — in the pre-seed window rejectAnimation has no
|
|
493
|
+
// entry and returns the bare 0 sentinel with no completion instead.
|
|
414
494
|
ownership.value = INTERACTIVE;
|
|
415
495
|
presentation.value = start;
|
|
416
496
|
if (scalarsWereStale) scalarsStale.value = 1;
|
|
@@ -426,6 +506,10 @@ export function useSmoothClipDriver(
|
|
|
426
506
|
behavior: 'current' | 'target' = 'current'
|
|
427
507
|
): SmoothClipPresentation => {
|
|
428
508
|
'worklet';
|
|
509
|
+
// Past the hook's cleanup the native entry is gone; the call would fail
|
|
510
|
+
// defined (handled=false) but it is the last driver.ui path that still
|
|
511
|
+
// crosses into native after the tombstone. Guard it like the rest.
|
|
512
|
+
if (disposed.value !== 0) return presentation.value;
|
|
429
513
|
const values = cancelAnimationHostFunction(
|
|
430
514
|
driverId,
|
|
431
515
|
animationId,
|
|
@@ -498,7 +582,14 @@ export function useSmoothClipDriver(
|
|
|
498
582
|
return promise;
|
|
499
583
|
},
|
|
500
584
|
set(next) {
|
|
501
|
-
|
|
585
|
+
// Teardown resolves (undefined) rather than rejects: `set` is a
|
|
586
|
+
// documented fire-and-forget call, and a promise discarded with
|
|
587
|
+
// `void` must not become an unhandled rejection at unmount.
|
|
588
|
+
const { requestId, promise } = createReactRequest<void>(
|
|
589
|
+
driverId,
|
|
590
|
+
false,
|
|
591
|
+
{ value: undefined }
|
|
592
|
+
);
|
|
502
593
|
scheduleOnUI(() => {
|
|
503
594
|
'worklet';
|
|
504
595
|
setOnUI(next);
|
|
@@ -507,9 +598,12 @@ export function useSmoothClipDriver(
|
|
|
507
598
|
return promise;
|
|
508
599
|
},
|
|
509
600
|
animateTo(target, animation) {
|
|
601
|
+
// Teardown resolves with the documented 0 rejection sentinel (see
|
|
602
|
+
// `set` above): the README's own example voids this promise.
|
|
510
603
|
const { requestId, promise } = createReactRequest<number>(
|
|
511
604
|
driverId,
|
|
512
|
-
true
|
|
605
|
+
true,
|
|
606
|
+
{ value: 0 }
|
|
513
607
|
);
|
|
514
608
|
scheduleOnUI(() => {
|
|
515
609
|
'worklet';
|
|
@@ -591,9 +685,14 @@ export function useSmoothClipDriver(
|
|
|
591
685
|
suppressed: SharedValue<number>,
|
|
592
686
|
active: SharedValue<number>,
|
|
593
687
|
stale: SharedValue<number>,
|
|
688
|
+
gone: SharedValue<number>,
|
|
594
689
|
setter: typeof setPresentationHostFunction
|
|
595
690
|
) => {
|
|
596
691
|
'worklet';
|
|
692
|
+
// First, before the listener or the seed: this effect run owns the
|
|
693
|
+
// driver again, so a StrictMode/<Activity> replay must clear the
|
|
694
|
+
// previous cleanup's tombstone or every later write would be rejected.
|
|
695
|
+
gone.value = 0;
|
|
597
696
|
let last: SmoothClipPresentation | null = null;
|
|
598
697
|
const deliver = (next: SmoothClipPresentation) => {
|
|
599
698
|
if (
|
|
@@ -623,15 +722,19 @@ export function useSmoothClipDriver(
|
|
|
623
722
|
clip.radius,
|
|
624
723
|
contentTranslateX,
|
|
625
724
|
contentTranslateY,
|
|
725
|
+
false,
|
|
626
726
|
false
|
|
627
727
|
);
|
|
628
728
|
};
|
|
629
729
|
source.addListener(listenerId, deliver);
|
|
630
730
|
// Authoritative take-ownership seed. Creates the native entry before
|
|
631
|
-
// any view mounts on the
|
|
632
|
-
// an effect-replay cleanup (StrictMode/<Activity>) on later runs.
|
|
731
|
+
// any view mounts on the ordinary path, and revives a driver destroyed
|
|
732
|
+
// by an effect-replay cleanup (StrictMode/<Activity>) on later runs.
|
|
733
|
+
// An animation worklet can win the scheduling race and create a latch
|
|
734
|
+
// first; Native ownership then marks that intent as newer, so this
|
|
735
|
+
// passive seed must not clear its active id or replay the target value.
|
|
633
736
|
const current = source.value;
|
|
634
|
-
if (isFiniteClipPresentation(current)) {
|
|
737
|
+
if (owner.value === INTERACTIVE && isFiniteClipPresentation(current)) {
|
|
635
738
|
active.value = 0;
|
|
636
739
|
owner.value = INTERACTIVE;
|
|
637
740
|
stale.value = 0;
|
|
@@ -645,7 +748,8 @@ export function useSmoothClipDriver(
|
|
|
645
748
|
current.clip.radius,
|
|
646
749
|
current.contentTranslateX,
|
|
647
750
|
current.contentTranslateY,
|
|
648
|
-
true
|
|
751
|
+
true,
|
|
752
|
+
false
|
|
649
753
|
);
|
|
650
754
|
}
|
|
651
755
|
},
|
|
@@ -656,6 +760,7 @@ export function useSmoothClipDriver(
|
|
|
656
760
|
suppressDeliveries,
|
|
657
761
|
activeAnimationId,
|
|
658
762
|
scalarsStale,
|
|
763
|
+
disposed,
|
|
659
764
|
setPresentationHostFunction
|
|
660
765
|
);
|
|
661
766
|
|
|
@@ -668,20 +773,46 @@ export function useSmoothClipDriver(
|
|
|
668
773
|
source: SharedValue<SmoothClipPresentation>,
|
|
669
774
|
listenerId: number,
|
|
670
775
|
nativeDriverId: number,
|
|
776
|
+
owner: SharedValue<number>,
|
|
777
|
+
active: SharedValue<number>,
|
|
778
|
+
stale: SharedValue<number>,
|
|
779
|
+
suppressed: SharedValue<number>,
|
|
780
|
+
gone: SharedValue<number>,
|
|
671
781
|
destroy: typeof destroyDriverHostFunction
|
|
672
782
|
) => {
|
|
673
783
|
'worklet';
|
|
674
784
|
source.removeListener(listenerId);
|
|
675
785
|
destroy(nativeDriverId);
|
|
786
|
+
// StrictMode replays the effect with these same SharedValues. Reset
|
|
787
|
+
// the UI-side tombstone only after native teardown so the next setup
|
|
788
|
+
// can seed or animate without inheriting stale Native ownership.
|
|
789
|
+
active.value = 0;
|
|
790
|
+
owner.value = INTERACTIVE;
|
|
791
|
+
stale.value = 0;
|
|
792
|
+
// Defensive: seedPresentation now reclaims its own unspent credit at
|
|
793
|
+
// the point of issue, so this should already be 0. Kept because the
|
|
794
|
+
// failure mode — one declarative write silently dropped for the rest
|
|
795
|
+
// of the next hook's life — is invisible until someone reports a
|
|
796
|
+
// stale clip rect.
|
|
797
|
+
suppressed.value = 0;
|
|
798
|
+
// Last: the native entry is now erased, so until the next effect run
|
|
799
|
+
// clears this, any call that would recreate it is rejected instead.
|
|
800
|
+
gone.value = 1;
|
|
676
801
|
},
|
|
677
802
|
presentation,
|
|
678
803
|
driverId,
|
|
679
804
|
driverId,
|
|
805
|
+
ownership,
|
|
806
|
+
activeAnimationId,
|
|
807
|
+
scalarsStale,
|
|
808
|
+
suppressDeliveries,
|
|
809
|
+
disposed,
|
|
680
810
|
destroyDriverHostFunction
|
|
681
811
|
);
|
|
682
812
|
};
|
|
683
813
|
}, [
|
|
684
814
|
activeAnimationId,
|
|
815
|
+
disposed,
|
|
685
816
|
ownership,
|
|
686
817
|
presentation,
|
|
687
818
|
scalarsStale,
|
package/src/reactRequests.ts
CHANGED
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
type PendingRequest = {
|
|
7
7
|
resolve(value: unknown): void;
|
|
8
8
|
reject(error: Error): void;
|
|
9
|
+
// Set for calls whose documented usage is fire-and-forget (`void
|
|
10
|
+
// driver.react.animateTo(...)`): teardown resolves them with this benign
|
|
11
|
+
// sentinel instead of rejecting, so a discarded promise cannot surface as
|
|
12
|
+
// an unhandled rejection during ordinary unmount.
|
|
13
|
+
teardownResolution?: { value: unknown };
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
const requests = new Map<number, PendingRequest>();
|
|
@@ -14,7 +19,8 @@ let nextRequestId = 0;
|
|
|
14
19
|
|
|
15
20
|
export function createReactRequest<T>(
|
|
16
21
|
driverId: number,
|
|
17
|
-
deferCompletions = false
|
|
22
|
+
deferCompletions = false,
|
|
23
|
+
teardownResolution?: { value: unknown }
|
|
18
24
|
): { requestId: number; promise: Promise<T> } {
|
|
19
25
|
nextRequestId = (nextRequestId % 0x7ffffffe) + 1;
|
|
20
26
|
const requestId = nextRequestId;
|
|
@@ -27,6 +33,7 @@ export function createReactRequest<T>(
|
|
|
27
33
|
requests.set(requestId, {
|
|
28
34
|
resolve: resolveRequest as (value: unknown) => void,
|
|
29
35
|
reject: rejectRequest,
|
|
36
|
+
teardownResolution,
|
|
30
37
|
});
|
|
31
38
|
const driverRequests = requestsByDriver.get(driverId) ?? new Set<number>();
|
|
32
39
|
driverRequests.add(requestId);
|
|
@@ -58,7 +65,12 @@ export function rejectDriverRequests(driverId: number): void {
|
|
|
58
65
|
for (const requestId of driverRequests) {
|
|
59
66
|
const request = requests.get(requestId);
|
|
60
67
|
requests.delete(requestId);
|
|
61
|
-
|
|
68
|
+
if (!request) continue;
|
|
69
|
+
if (request.teardownResolution) {
|
|
70
|
+
request.resolve(request.teardownResolution.value);
|
|
71
|
+
} else {
|
|
72
|
+
request.reject(new Error('[SmoothClipView] Driver was destroyed.'));
|
|
73
|
+
}
|
|
62
74
|
}
|
|
63
75
|
requestsByDriver.delete(driverId);
|
|
64
76
|
}
|