@@ -27,7 +27,7 @@ import utils from '../utils';
2727import type { StorageKeyValuePair } from './providers/types' ;
2828import type BufferStore from './BufferStore/types' ;
2929
30- type EntryType = 'set' | 'merge' ;
30+ type EntryType = 'set' | 'merge' | 'remove' ;
3131
3232type BufferEntry = {
3333 key : OnyxKey ;
@@ -36,10 +36,11 @@ type BufferEntry = {
3636 replaceNullPatches ?: FastMergeReplaceNullPatch [ ] ;
3737} ;
3838
39- /** Flush handlers for the two entry types. */
39+ /** Flush handlers for the three entry types. */
4040type FlushHandlers = {
4141 multiSet : ( pairs : StorageKeyValuePair [ ] ) => Promise < void > ;
4242 multiMerge : ( pairs : StorageKeyValuePair [ ] ) => Promise < void > ;
43+ multiRemove : ( keys : OnyxKey [ ] ) => Promise < void > ;
4344} ;
4445
4546/**
@@ -73,7 +74,9 @@ const FLUSH_TIMEOUT_MS = 200;
7374/** Default web flush scheduler using requestIdleCallback with setTimeout fallback. */
7475function defaultScheduleFlush ( doFlush : ( ) => void ) : number | null {
7576 if ( typeof requestIdleCallback === 'function' ) {
76- return requestIdleCallback ( doFlush , { timeout : FLUSH_TIMEOUT_MS } ) as unknown as number ;
77+ return requestIdleCallback ( doFlush , {
78+ timeout : FLUSH_TIMEOUT_MS ,
79+ } ) as unknown as number ;
7780 }
7881 return setTimeout ( doFlush , FLUSH_TIMEOUT_MS ) as unknown as number ;
7982}
@@ -144,20 +147,38 @@ class WriteBuffer {
144147 * - No existing entry: create a MERGE entry with just the patch
145148 * - Existing SET entry: apply patch to the full value in-memory, keep as SET
146149 * - Existing MERGE entry: merge patches together, keep as MERGE
150+ * - Existing REMOVE (tombstone): the underlying value will be null after flush,
151+ * so merging into null is just the patch itself -- replace tombstone with SET
147152 */
148153 merge ( key : OnyxKey , patch : OnyxValue < OnyxKey > , replaceNullPatches ?: FastMergeReplaceNullPatch [ ] ) : void {
149154 const existing = this . store . get ( key ) ;
150155
151156 if ( ! existing ) {
152157 // No pending write -- stage as a MERGE entry (just the patch)
153- this . store . set ( key , { key, value : patch , entryType : 'merge' , replaceNullPatches} ) ;
158+ this . store . set ( key , {
159+ key,
160+ value : patch ,
161+ entryType : 'merge' ,
162+ replaceNullPatches,
163+ } ) ;
164+ } else if ( existing . entryType === 'remove' ) {
165+ // Pending REMOVE -- merging into null is just the patch itself, replace with SET
166+ this . store . set ( key , {
167+ key,
168+ value : patch ,
169+ entryType : 'set' ,
170+ } ) ;
154171 } else if ( existing . entryType === 'set' ) {
155172 // Pending SET -- apply patch to the full value, stay as SET
156173 const { result : merged } = utils . fastMerge ( existing . value as Record < string , unknown > , patch as Record < string , unknown > , {
157174 shouldRemoveNestedNulls : true ,
158175 objectRemovalMode : 'replace' ,
159176 } ) ;
160- this . store . set ( key , { key, value : merged as OnyxValue < OnyxKey > , entryType : 'set' } ) ;
177+ this . store . set ( key , {
178+ key,
179+ value : merged as OnyxValue < OnyxKey > ,
180+ entryType : 'set' ,
181+ } ) ;
161182 } else {
162183 // Pending MERGE -- merge patches together, stay as MERGE
163184 const { result : mergedPatch } = utils . fastMerge ( existing . value as Record < string , unknown > , patch as Record < string , unknown > , {
@@ -176,20 +197,32 @@ class WriteBuffer {
176197 }
177198
178199 /**
179- * Remove a key from the write buffer. This is used when a key is being
180- * removed from storage entirely (not just set to null).
200+ * Stage a tombstone for a key (REMOVE entry). A pending SET or MERGE is
201+ * discarded; the tombstone wins and is flushed via multiRemove. Scheduling
202+ * a flush mirrors set/merge so deletes are batched off the worker hot path.
181203 */
182204 remove ( key : OnyxKey ) : void {
183- this . store . delete ( key ) ;
205+ this . store . set ( key , {
206+ key,
207+ value : null as OnyxValue < OnyxKey > ,
208+ entryType : 'remove' ,
209+ } ) ;
210+ this . scheduleFlush ( ) ;
184211 }
185212
186213 /**
187- * Remove multiple keys from the write buffer.
214+ * Stage tombstones for multiple keys (all REMOVE entries). A single flush
215+ * is scheduled regardless of how many keys are removed.
188216 */
189217 removeMany ( keys : OnyxKey [ ] ) : void {
190218 for ( const key of keys ) {
191- this . store . delete ( key ) ;
219+ this . store . set ( key , {
220+ key,
221+ value : null as OnyxValue < OnyxKey > ,
222+ entryType : 'remove' ,
223+ } ) ;
192224 }
225+ this . scheduleFlush ( ) ;
193226 }
194227
195228 /**
@@ -217,12 +250,21 @@ class WriteBuffer {
217250 }
218251
219252 /**
220- * Check whether the key has any pending entry (SET or MERGE ).
253+ * Check whether the key has any pending entry (SET, MERGE, or REMOVE ).
221254 */
222255 hasAny ( key : OnyxKey ) : boolean {
223256 return this . store . has ( key ) ;
224257 }
225258
259+ /**
260+ * Inspect the pending entry type for a key without touching it. Used by the
261+ * storage facade to disambiguate reads: SET serves from buffer, REMOVE
262+ * returns null immediately, MERGE forces a flush before reading the provider.
263+ */
264+ peekEntryType ( key : OnyxKey ) : EntryType | undefined {
265+ return this . store . get ( key ) ?. entryType ;
266+ }
267+
226268 /**
227269 * Returns the number of pending entries (both SET and MERGE).
228270 */
@@ -277,11 +319,16 @@ class WriteBuffer {
277319 const setSnapshot = new Map < OnyxKey , BufferEntry > ( ) ;
278320 const mergePairs : StorageKeyValuePair [ ] = [ ] ;
279321 const mergeKeys : OnyxKey [ ] = [ ] ;
322+ const removeKeys : OnyxKey [ ] = [ ] ;
323+ const removeSnapshot = new Map < OnyxKey , BufferEntry > ( ) ;
280324
281325 for ( const [ key , entry ] of this . store . entries ( ) ) {
282326 if ( entry . entryType === 'set' ) {
283327 setPairs . push ( [ entry . key , entry . value ] ) ;
284328 setSnapshot . set ( key , entry ) ;
329+ } else if ( entry . entryType === 'remove' ) {
330+ removeKeys . push ( entry . key ) ;
331+ removeSnapshot . set ( key , entry ) ;
285332 } else {
286333 mergePairs . push ( [ entry . key , entry . value , entry . replaceNullPatches ] ) ;
287334 mergeKeys . push ( key ) ;
@@ -294,14 +341,17 @@ class WriteBuffer {
294341 this . store . delete ( key ) ;
295342 }
296343
297- // Flush both types concurrently
344+ // Flush all three types concurrently
298345 const promises : Array < Promise < void > > = [ ] ;
299346 if ( setPairs . length > 0 ) {
300347 promises . push ( this . handlers . multiSet ( setPairs ) ) ;
301348 }
302349 if ( mergePairs . length > 0 ) {
303350 promises . push ( this . handlers . multiMerge ( mergePairs ) ) ;
304351 }
352+ if ( removeKeys . length > 0 ) {
353+ promises . push ( this . handlers . multiRemove ( removeKeys ) ) ;
354+ }
305355
306356 await Promise . all ( promises ) ;
307357
@@ -312,6 +362,14 @@ class WriteBuffer {
312362 this . store . delete ( key ) ;
313363 }
314364 }
365+
366+ // Same reference-identity check for REMOVE tombstones: a set/merge
367+ // arriving during flush replaces the entry and must not be erased.
368+ for ( const [ key , flushedEntry ] of removeSnapshot ) {
369+ if ( this . store . get ( key ) === flushedEntry ) {
370+ this . store . delete ( key ) ;
371+ }
372+ }
315373 } finally {
316374 this . isFlushing = false ;
317375 }
0 commit comments