@@ -16,11 +16,21 @@ type InputBatch = {
1616 lastTimestamp : number ;
1717} ;
1818
19+ type AutofillStreak = {
20+ firstTimestamp : number ;
21+ lastTimestamp : number ;
22+ } ;
23+
1924const BIG_INPUT_DELTA_THRESHOLD = 3 ;
25+ const AUTOFILL_STREAK_LENGTH = 5 ;
26+ const AUTOFILL_FAST_INTERVAL_MS = 80 ;
27+ const AUTOFILL_UNIFORM_SPREAD_MS = 25 ;
2028
2129const useLoginInputEventLow = ( { inputRef, connectService, enabled } : Props ) => {
2230 const inputBatchRef = useRef < InputBatch | null > ( null ) ;
2331 const previousLengthRef = useRef ( 0 ) ;
32+ const autofillTimestampsRef = useRef < number [ ] > ( [ ] ) ;
33+ const autofillStreakRef = useRef < AutofillStreak | null > ( null ) ;
2434
2535 useEffect ( ( ) => {
2636 if ( ! enabled ) {
@@ -33,6 +43,8 @@ const useLoginInputEventLow = ({ inputRef, connectService, enabled }: Props) =>
3343 }
3444
3545 previousLengthRef . current = input . value . length ;
46+ autofillTimestampsRef . current = [ ] ;
47+ autofillStreakRef . current = null ;
3648
3749 const isInputFocused = ( ) => document . activeElement === input ;
3850
@@ -66,7 +78,27 @@ const useLoginInputEventLow = ({ inputRef, connectService, enabled }: Props) =>
6678 focusProbeTimer = window . setTimeout ( runEnvProbe , 250 ) ;
6779 } ;
6880
81+ const extendInputBatch = ( timestamp : number ) => {
82+ const batch = inputBatchRef . current ;
83+ if ( ! batch ) {
84+ inputBatchRef . current = { firstTimestamp : timestamp , lastTimestamp : timestamp } ;
85+ return ;
86+ }
87+
88+ batch . lastTimestamp = timestamp ;
89+ } ;
90+
91+ const drainAutofillRingToBatch = ( ) => {
92+ const ring = autofillTimestampsRef . current ;
93+ for ( const ts of ring ) {
94+ extendInputBatch ( ts ) ;
95+ }
96+ ring . length = 0 ;
97+ } ;
98+
6999 const flushInputBatch = ( ) => {
100+ drainAutofillRingToBatch ( ) ;
101+
70102 const batch = inputBatchRef . current ;
71103 if ( ! batch ) {
72104 return ;
@@ -80,8 +112,23 @@ const useLoginInputEventLow = ({ inputRef, connectService, enabled }: Props) =>
80112 inputBatchRef . current = null ;
81113 } ;
82114
115+ const flushAutofillStreak = ( ) => {
116+ const streak = autofillStreakRef . current ;
117+ if ( ! streak ) {
118+ return ;
119+ }
120+
121+ connectService . enqueueLowEvent ( {
122+ eventType : 'big-input-add' ,
123+ timestamp : streak . firstTimestamp ,
124+ durationMs : streak . lastTimestamp - streak . firstTimestamp ,
125+ } ) ;
126+ autofillStreakRef . current = null ;
127+ } ;
128+
83129 const enqueueNonInputEvent = ( eventType : string ) => {
84130 flushInputBatch ( ) ;
131+ flushAutofillStreak ( ) ;
85132 connectService . enqueueLowEvent ( {
86133 eventType,
87134 timestamp : Date . now ( ) ,
@@ -111,6 +158,30 @@ const useLoginInputEventLow = ({ inputRef, connectService, enabled }: Props) =>
111158 enqueueNonInputEvent ( 'click' ) ;
112159 } ;
113160
161+ const detectAutofillStreak = ( ) : boolean => {
162+ const ring = autofillTimestampsRef . current ;
163+ if ( ring . length < AUTOFILL_STREAK_LENGTH ) {
164+ return false ;
165+ }
166+
167+ let minInterval = Infinity ;
168+ let maxInterval = - Infinity ;
169+ for ( let i = 1 ; i < ring . length ; i ++ ) {
170+ const interval = ring [ i ] - ring [ i - 1 ] ;
171+ if ( interval > AUTOFILL_FAST_INTERVAL_MS ) {
172+ return false ;
173+ }
174+ if ( interval < minInterval ) {
175+ minInterval = interval ;
176+ }
177+ if ( interval > maxInterval ) {
178+ maxInterval = interval ;
179+ }
180+ }
181+
182+ return maxInterval - minInterval <= AUTOFILL_UNIFORM_SPREAD_MS ;
183+ } ;
184+
114185 const handleInput = ( ) => {
115186 const timestamp = Date . now ( ) ;
116187
@@ -119,26 +190,55 @@ const useLoginInputEventLow = ({ inputRef, connectService, enabled }: Props) =>
119190 previousLengthRef . current = newLength ;
120191
121192 if ( delta >= BIG_INPUT_DELTA_THRESHOLD ) {
193+ drainAutofillRingToBatch ( ) ;
194+ flushAutofillStreak ( ) ;
122195 connectService . enqueueLowEvent ( { eventType : 'big-input-add' , timestamp } ) ;
123196 return ;
124197 }
125198
126199 if ( delta <= - BIG_INPUT_DELTA_THRESHOLD ) {
200+ drainAutofillRingToBatch ( ) ;
201+ flushAutofillStreak ( ) ;
127202 connectService . enqueueLowEvent ( { eventType : 'big-input-rem' , timestamp } ) ;
128203 return ;
129204 }
130205
131- const currentBatch = inputBatchRef . current ;
206+ const activeStreak = autofillStreakRef . current ;
207+ if ( activeStreak ) {
208+ if ( delta === 1 && timestamp - activeStreak . lastTimestamp <= AUTOFILL_FAST_INTERVAL_MS ) {
209+ activeStreak . lastTimestamp = timestamp ;
210+ return ;
211+ }
212+
213+ flushAutofillStreak ( ) ;
214+ }
215+
216+ if ( delta !== 1 ) {
217+ drainAutofillRingToBatch ( ) ;
218+ extendInputBatch ( timestamp ) ;
219+ return ;
220+ }
221+
222+ const ring = autofillTimestampsRef . current ;
223+ ring . push ( timestamp ) ;
224+ if ( ring . length > AUTOFILL_STREAK_LENGTH ) {
225+ const graduated = ring . shift ( ) as number ;
226+ extendInputBatch ( graduated ) ;
227+ }
132228
133- if ( ! currentBatch ) {
134- inputBatchRef . current = {
135- firstTimestamp : timestamp ,
136- lastTimestamp : timestamp ,
137- } ;
229+ if ( ! detectAutofillStreak ( ) ) {
138230 return ;
139231 }
140232
141- currentBatch . lastTimestamp = timestamp ;
233+ const streakStart = ring [ 0 ] ;
234+ const streakLast = ring [ ring . length - 1 ] ;
235+ ring . length = 0 ;
236+
237+ flushInputBatch ( ) ;
238+ autofillStreakRef . current = {
239+ firstTimestamp : streakStart ,
240+ lastTimestamp : streakLast ,
241+ } ;
142242 } ;
143243
144244 const handleKeyUp = ( event : KeyboardEvent ) => {
@@ -195,6 +295,7 @@ const useLoginInputEventLow = ({ inputRef, connectService, enabled }: Props) =>
195295
196296 const flushForTeardown = ( ) => {
197297 flushInputBatch ( ) ;
298+ flushAutofillStreak ( ) ;
198299 resizeBatcher . flush ( ) ;
199300 scrollBatcher . flush ( ) ;
200301 connectService . flushLowEventsKeepalive ( ) ;
0 commit comments