@@ -15,6 +15,22 @@ let isInitialized = false;
1515// by other browser tabs (merge into memory).
1616const knownRequestIDs = new Set < number > ( ) ;
1717let crossTabRequestsCallback : ( ( ) => void ) | undefined ;
18+ // Tracks the number of unresolved Onyx.set()/Onyx.multiSet() promises initiated
19+ // by this tab. While any own writes are pending, the Onyx callback must NOT
20+ // reconcile deletions, because the callback may have been triggered synchronously
21+ // by broadcastUpdate during our own Onyx.set() call — and that value may become
22+ // stale if a later Onyx.set() has already been called (Issue 4 protection).
23+ // When the counter is 0, any callback must be from a cross-tab storage event,
24+ // so it is safe to reconcile deletions from the leader tab.
25+ let pendingOnyxWrites = 0 ;
26+
27+ function trackOnyxWrite < T > ( promise : Promise < T > ) : Promise < T > {
28+ pendingOnyxWrites ++ ;
29+ return promise . finally ( ( ) => {
30+ pendingOnyxWrites -- ;
31+ } ) ;
32+ }
33+
1834let initializationCallback : ( ) => void ;
1935function triggerInitializationCallback ( ) {
2036 if ( typeof initializationCallback !== 'function' ) {
@@ -56,8 +72,31 @@ Onyx.connectWithoutView({
5672 }
5773 }
5874 persistedRequests = [ ...persistedRequests , ...newFromOtherTabs ] ;
59- Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , persistedRequests ) ;
75+ trackOnyxWrite ( Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , persistedRequests ) ) ;
6076 crossTabRequestsCallback ?.( ) ;
77+ return ;
78+ }
79+
80+ // Reconcile deletions from the leader tab. When pendingOnyxWrites > 0,
81+ // the callback may be a stale own-write (broadcastUpdate fires synchronously
82+ // inside Onyx.set) — skip to preserve Issue 4 protection. When counter is 0,
83+ // the callback is from a cross-tab storage event, safe to remove from memory
84+ // any requests the leader already processed.
85+ if ( pendingOnyxWrites === 0 ) {
86+ const diskIDs = new Set < number > ( ) ;
87+ for ( const r of val ) {
88+ if ( r . requestID != null ) {
89+ diskIDs . add ( r . requestID ) ;
90+ }
91+ }
92+ const previousLength = persistedRequests . length ;
93+ persistedRequests = persistedRequests . filter ( ( r ) => r . requestID == null || diskIDs . has ( r . requestID ) ) ;
94+ if ( persistedRequests . length !== previousLength ) {
95+ Log . info ( '[PersistedRequests] Reconciled deletions from leader tab' , false , {
96+ removedCount : previousLength - persistedRequests . length ,
97+ remainingCount : persistedRequests . length ,
98+ } ) ;
99+ }
61100 }
62101 return ;
63102 }
@@ -92,7 +131,7 @@ Onyx.connectWithoutView({
92131 }
93132 const requests = [ ...persistedRequests , ...pendingSaveOperations ] ;
94133 persistedRequests = requests ;
95- Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , requests ) ;
134+ trackOnyxWrite ( Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , requests ) ) ;
96135 pendingSaveOperations = [ ] ;
97136 }
98137
@@ -144,6 +183,7 @@ function clear() {
144183 persistedRequests = [ ] ;
145184 pendingSaveOperations = [ ] ;
146185 knownRequestIDs . clear ( ) ;
186+ pendingOnyxWrites = 0 ;
147187 Onyx . set ( ONYXKEYS . PERSISTED_ONGOING_REQUESTS , null ) ;
148188 return Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , [ ] ) ;
149189}
@@ -183,7 +223,7 @@ function save<TKey extends OnyxKey>(requestToPersist: Request<TKey>): Promise<vo
183223 newQueueLength : requests . length ,
184224 } ) ;
185225
186- return Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , requests as AnyRequest [ ] )
226+ return trackOnyxWrite ( Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , requests as AnyRequest [ ] ) )
187227 . then ( ( ) => {
188228 Log . info ( '[PersistedRequests] Request successfully persisted to disk' , false , {
189229 command : requestToPersist . command ,
@@ -243,10 +283,12 @@ function endRequestAndRemoveFromQueue<TKey extends OnyxKey>(requestToRemove: Req
243283 newQueueLength : persistedRequests . length ,
244284 } ) ;
245285
246- Onyx . multiSet ( {
247- [ ONYXKEYS . PERSISTED_REQUESTS ] : persistedRequests ,
248- [ ONYXKEYS . PERSISTED_ONGOING_REQUESTS ] : null ,
249- } ) . then ( ( ) => {
286+ trackOnyxWrite (
287+ Onyx . multiSet ( {
288+ [ ONYXKEYS . PERSISTED_REQUESTS ] : persistedRequests ,
289+ [ ONYXKEYS . PERSISTED_ONGOING_REQUESTS ] : null ,
290+ } ) ,
291+ ) . then ( ( ) => {
250292 Log . info ( '[PersistedRequests] Successfully persisted request removal to disk' , false , {
251293 command : requestToRemove . command ,
252294 newQueueLength : getLength ( ) ,
@@ -262,7 +304,7 @@ function deleteRequestsByIndices(indices: number[]): Promise<void> {
262304 persistedRequests = persistedRequests . filter ( ( _ , index ) => ! indicesSet . has ( index ) ) ;
263305
264306 // Update the persisted requests in storage or state as necessary
265- return Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , persistedRequests ) . then ( ( ) => {
307+ return trackOnyxWrite ( Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , persistedRequests ) ) . then ( ( ) => {
266308 Log . info ( `Multiple (${ indices . length } ) requests removed from the queue. Queue length is ${ persistedRequests . length } ` ) ;
267309 } ) ;
268310}
@@ -276,7 +318,7 @@ function update<TKey extends OnyxKey>(oldRequestIndex: number, newRequest: Reque
276318 if ( newRequest . requestID != null ) {
277319 knownRequestIDs . add ( newRequest . requestID ) ;
278320 }
279- return Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , requests ) ;
321+ return trackOnyxWrite ( Onyx . set ( ONYXKEYS . PERSISTED_REQUESTS , requests ) ) ;
280322}
281323
282324function updateOngoingRequest < TKey extends OnyxKey > ( newRequest : Request < TKey > ) {
@@ -333,10 +375,12 @@ function processNextRequest(): AnyRequest | null {
333375 // native File objects (DataCloneError). These requests cannot survive a crash
334376 // anyway since File references are lost on restart.
335377 const hasNonSerializableData = ongoingRequest ?. data && Object . values ( ongoingRequest . data ) . some ( ( v ) => v instanceof File || v instanceof Blob ) ;
336- Onyx . multiSet ( {
337- [ ONYXKEYS . PERSISTED_REQUESTS ] : persistedRequests ,
338- ...( ongoingRequest && ! hasNonSerializableData ? { [ ONYXKEYS . PERSISTED_ONGOING_REQUESTS ] : ongoingRequest } : { } ) ,
339- } ) ;
378+ trackOnyxWrite (
379+ Onyx . multiSet ( {
380+ [ ONYXKEYS . PERSISTED_REQUESTS ] : persistedRequests ,
381+ ...( ongoingRequest && ! hasNonSerializableData ? { [ ONYXKEYS . PERSISTED_ONGOING_REQUESTS ] : ongoingRequest } : { } ) ,
382+ } ) ,
383+ ) ;
340384
341385 // Return the local reference, not `ongoingRequest`. The Onyx.multiSet above
342386 // triggers a synchronous callback (Onyx 3.0.46+) that overwrites `ongoingRequest`
@@ -378,10 +422,12 @@ function rollbackOngoingRequest() {
378422
379423 // Persist both changes to disk so a crash after rollback doesn't lose
380424 // the rolled-back request or leave a stale ongoingRequest on disk.
381- Onyx . multiSet ( {
382- [ ONYXKEYS . PERSISTED_REQUESTS ] : persistedRequests ,
383- [ ONYXKEYS . PERSISTED_ONGOING_REQUESTS ] : null ,
384- } ) ;
425+ trackOnyxWrite (
426+ Onyx . multiSet ( {
427+ [ ONYXKEYS . PERSISTED_REQUESTS ] : persistedRequests ,
428+ [ ONYXKEYS . PERSISTED_ONGOING_REQUESTS ] : null ,
429+ } ) ,
430+ ) ;
385431}
386432
387433function getAll ( ) : AnyRequest [ ] {
0 commit comments