@@ -6,8 +6,62 @@ import { SemverVersioning } from './versioning/SemverVersioning'
66
77let NativeCodePush = require ( "react-native" ) . NativeModules . CodePush ;
88const PackageMixins = require ( "./package-mixins" ) ( NativeCodePush ) ;
9+ const RolloutStorage = require ( "react-native" ) . NativeModules . RolloutStorage ;
910
10- const DEPLOYMENT_KEY = 'deprecated_deployment_key' ;
11+ const DEPLOYMENT_KEY = 'deprecated_deployment_key' ,
12+ ROLLOUT_CACHE_PREFIX = 'CodePushRolloutDecision_' ,
13+ ROLLOUT_CACHE_KEY = 'CodePushRolloutKey' ;
14+
15+ function hashDeviceId ( deviceId ) {
16+ let hash = 0 ;
17+ for ( let i = 0 ; i < deviceId . length ; i ++ ) {
18+ hash = ( ( hash << 5 ) - hash ) + deviceId . charCodeAt ( i ) ;
19+ hash |= 0 ; // Convert to 32bit int
20+ }
21+ return Math . abs ( hash ) ;
22+ }
23+
24+ function getRolloutKey ( label , rollout ) {
25+ return `${ ROLLOUT_CACHE_PREFIX } ${ label } _rollout_${ rollout ?? 100 } ` ;
26+ }
27+
28+ function getBucket ( clientId , packageHash ) {
29+ const hash = hashDeviceId ( `${ clientId ?? '' } _${ packageHash ?? '' } ` ) ;
30+ return ( Math . abs ( hash ) % 100 ) ;
31+ }
32+
33+ export async function shouldApplyCodePushUpdate ( remotePackage , clientId , onRolloutSkipped ) {
34+ if ( remotePackage . rollout === undefined || remotePackage . rollout >= 100 ) {
35+ return true ;
36+ }
37+
38+ const rolloutKey = getRolloutKey ( remotePackage . label , remotePackage . rollout ) ,
39+ cachedDecision = await RolloutStorage . getItem ( rolloutKey ) ;
40+
41+ if ( cachedDecision != null ) {
42+ // should apply if cachedDecision is true
43+ return cachedDecision === 'true' ;
44+ }
45+
46+ const bucket = getBucket ( clientId , remotePackage . packageHash ) ,
47+ inRollout = bucket < remotePackage . rollout ,
48+ prevRolloutCacheKey = await RolloutStorage . getItem ( ROLLOUT_CACHE_KEY ) ;
49+
50+ console . log ( `[CodePush] Bucket: ${ bucket } , rollout: ${ remotePackage . rollout } → ${ inRollout ? 'IN' : 'OUT' } ` ) ;
51+
52+ if ( prevRolloutCacheKey )
53+ await RolloutStorage . removeItem ( prevRolloutCacheKey ) ;
54+
55+ await RolloutStorage . setItem ( ROLLOUT_CACHE_KEY , rolloutKey ) ;
56+ await RolloutStorage . setItem ( rolloutKey , inRollout . toString ( ) ) ;
57+
58+ if ( ! inRollout ) {
59+ console . log ( `[CodePush] Skipping update due to rollout. Bucket ${ bucket } >= rollout ${ remotePackage . rollout } ` ) ;
60+ onRolloutSkipped ?. ( remotePackage . label ) ;
61+ }
62+
63+ return inRollout ;
64+ }
1165
1266async function checkForUpdate ( handleBinaryVersionMismatchCallback = null ) {
1367 /*
@@ -121,6 +175,7 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
121175 package_size : 0 ,
122176 // not used at runtime.
123177 should_run_binary_version : false ,
178+ rollout : latestReleaseInfo . rollout
124179 } ;
125180
126181 return mapToRemotePackageMetadata ( updateInfo ) ;
@@ -164,6 +219,13 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
164219 return null ;
165220 } else {
166221 const remotePackage = { ...update , ...PackageMixins . remote ( ) } ;
222+
223+ // Rollout filtering
224+ const shouldApply = await shouldApplyCodePushUpdate ( remotePackage , nativeConfig . clientUniqueId , sharedCodePushOptions ?. onRolloutSkipped ) ;
225+
226+ if ( ! shouldApply )
227+ return { skipRollout : true } ;
228+
167229 remotePackage . failedInstall = await NativeCodePush . isFailedUpdate ( remotePackage . packageHash ) ;
168230 return remotePackage ;
169231 }
@@ -193,6 +255,7 @@ function mapToRemotePackageMetadata(updateInfo) {
193255 packageHash : updateInfo . package_hash ?? '' ,
194256 packageSize : updateInfo . package_size ?? 0 ,
195257 downloadUrl : updateInfo . download_url ?? '' ,
258+ rollout : updateInfo . rollout ?? 100 ,
196259 } ;
197260}
198261
@@ -493,6 +556,11 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
493556 return CodePush . SyncStatus . UPDATE_INSTALLED ;
494557 } ;
495558
559+ if ( remotePackage ?. skipRollout ) {
560+ syncStatusChangeCallback ( CodePush . SyncStatus . UP_TO_DATE ) ;
561+ return CodePush . SyncStatus . UP_TO_DATE ;
562+ }
563+
496564 const updateShouldBeIgnored = await shouldUpdateBeIgnored ( remotePackage , syncOptions ) ;
497565
498566 if ( ! remotePackage || updateShouldBeIgnored ) {
@@ -609,6 +677,9 @@ let CodePush;
609677 *
610678 * onSyncError: (label: string, error: Error) => void | undefined,
611679 * setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | undefined): void,
680+ *
681+ * onRolloutSkipped: (label: string, error: Error) => void | undefined,
682+ * setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
612683 * }}
613684 */
614685const sharedCodePushOptions = {
@@ -653,6 +724,12 @@ const sharedCodePushOptions = {
653724 if ( typeof onSyncErrorFunction !== 'function' ) throw new Error ( 'Please pass a function to onSyncError' ) ;
654725 this . onSyncError = onSyncErrorFunction ;
655726 } ,
727+ onRolloutSkipped : undefined ,
728+ setOnRolloutSkipped ( onRolloutSkippedFunction ) {
729+ if ( ! onRolloutSkippedFunction ) return ;
730+ if ( typeof onRolloutSkippedFunction !== 'function' ) throw new Error ( 'Please pass a function to onRolloutSkipped' ) ;
731+ this . onRolloutSkipped = onRolloutSkippedFunction ;
732+ }
656733}
657734
658735function codePushify ( options = { } ) {
@@ -688,6 +765,7 @@ function codePushify(options = {}) {
688765 sharedCodePushOptions . setOnDownloadStart ( options . onDownloadStart ) ;
689766 sharedCodePushOptions . setOnDownloadSuccess ( options . onDownloadSuccess ) ;
690767 sharedCodePushOptions . setOnSyncError ( options . onSyncError ) ;
768+ sharedCodePushOptions . setOnRolloutSkipped ( options . onRolloutSkipped ) ;
691769
692770 const decorator = ( RootComponent ) => {
693771 class CodePushComponent extends React . Component {
0 commit comments