@@ -21,6 +21,7 @@ import {
2121 takeRight ,
2222 drop ,
2323 take ,
24+ unionBy ,
2425} from "lodash" ;
2526import {
2627 DbIndexFTSFromRangeQueries ,
@@ -403,14 +404,17 @@ class InMemoryStore implements DbStore {
403404 * an optional 3rd parameter on it) so that the normal, always-safe `put()` required by the shared
404405 * `DbStore` interface can never accidentally be called with scoping semantics.
405406 *
406- * @param indexNames Scoping hint: items are only removed from / re-written into the primary key
407- * plus the listed index(es) -- every other index on the store is left completely untouched. This
408- * lets callers who fetched data through a single index (e.g. a ranged read served from that
409- * index) cache the results without seeding "islands" of items into unrelated indexes that were
410- * never actually queried/loaded for those items, and without evicting an already-cached item from
411- * indexes this call didn't touch. Note that an index left untouched here can only go stale in
412- * content if the item's data changes without ever going through the real (unscoped) `put()` path --
413- * real writes always resync every index.
407+ * @param indexNames Scoping hint for *newly seen* items only: an item this store has never
408+ * cached before is only written into the primary key plus the listed index(es) -- every other
409+ * index on the store is left untouched, so callers who fetched data through a single index (e.g.
410+ * a ranged read served from that index) can cache the results without seeding "islands" of items
411+ * into unrelated indexes that were never actually queried/loaded for those items.
412+ *
413+ * If the item was already cached, the stale copy is removed from -- and the fresh copy is
414+ * re-written into -- every index it was actually present in (not just the listed one(s)), so an
415+ * index this call didn't ask for can never end up holding stale content. The listed index(es) are
416+ * additionally guaranteed to receive the fresh copy even if the item wasn't previously tracked by
417+ * them, so the ranged read that triggered this call still gets a correctly populated cache there.
414418 */
415419 putInIndexAfterGet_DoNotUse (
416420 itemOrItems : ItemType | ItemType [ ] ,
@@ -434,28 +438,43 @@ class InMemoryStore implements DbStore {
434438 ) ! ! ! ;
435439 const existingItem = this . _mergedData . get ( pk ) ;
436440
437- // Scope both the removal (of the stale copy) and the re-add (of the new copy) to the
438- // same index list, so an index this call didn't ask for is never touched either way.
439- const indexesToTouch = indexNames
441+ // Scoping hint from the caller: for a brand-new item (never cached before) this is where
442+ // the fresh copy is written. Left undefined (i.e. every index) for the normal, unscoped
443+ // put() path.
444+ const requestedIndexes = indexNames
440445 ? filter ( this . _storeSchema . indexes , ( index ) =>
441446 includes ( indexNames , index . name )
442447 )
443448 : this . _storeSchema . indexes ;
444449
450+ let indexesToPopulate = requestedIndexes ;
451+
445452 if ( existingItem ) {
446- // We're going to overwrite the PK anyways - don't remove PK
447- this . _removeFromIndices (
453+ // Always remove the stale copy from every index it's actually present in -- each index
454+ // holds its own copy of the item, so scoping the removal to just the requested index(es)
455+ // would leave an un-refreshed, stale copy behind in any other index the item was already
456+ // tracked by (see PR #87 discussion).
457+ const indexesThatHadItem = this . _removeFromIndices (
448458 pk ,
449459 existingItem ,
450460 /** RemovePrimaryKey */ false ,
451- indexesToTouch
461+ this . _storeSchema . indexes ?? [ ]
452462 ) ;
463+
464+ // Re-populate every index the item was actually already cached by (so it's refreshed,
465+ // never left stale) plus whichever index(es) this call is scoped to (so a ranged read
466+ // still gets a correctly populated cache there, even for an index the item wasn't
467+ // previously tracked by).
468+ indexesToPopulate = indexNames
469+ ? unionBy ( indexesThatHadItem , requestedIndexes ?? [ ] , "name" )
470+ : this . _storeSchema . indexes ;
453471 }
472+
454473 this . _mergedData . set ( pk , item ) ;
455474 ( this . openPrimaryKey ( ) as InMemoryIndex ) . put ( item ) ;
456475
457- if ( indexesToTouch ) {
458- for ( const index of indexesToTouch ) {
476+ if ( indexesToPopulate ) {
477+ for ( const index of indexesToPopulate ) {
459478 ( this . openIndex ( index . name ) as InMemoryIndex ) . put ( item ) ;
460479 }
461480 }
@@ -605,29 +624,44 @@ class InMemoryStore implements DbStore {
605624 item : ItemType ,
606625 removePrimaryKey : boolean ,
607626 indexesToRemoveFrom : IndexSchema [ ] = this . _storeSchema . indexes ?? [ ]
608- ) {
627+ ) : IndexSchema [ ] {
609628 // Don't need to remove from primary key on Puts because set is enough
610629 // 1. If it's an existing key then it will get overwritten
611630 // 2. If it's a new key then we need to add it
612631 if ( removePrimaryKey ) {
613632 ( this . openPrimaryKey ( ) as InMemoryIndex ) . remove ( key ) ;
614633 }
615634
635+ const indexesThatHadItem : IndexSchema [ ] = [ ] ;
636+
616637 each ( indexesToRemoveFrom , ( index : IndexSchema ) => {
617638 const ind = this . openIndex ( index . name ) as InMemoryIndex ;
618639 const indexKeys = ind . internal_getKeysFromItem ( item ) ;
619640
620641 // when it's a unique index, value is the item.
621642 // in case of a non-unique index, value is an array of items,
622643 // and we want to only remove items that have the same primary key
644+ let removedFromThisIndex = false ;
623645 if ( ind . isUniqueIndex ( ) ) {
624- each ( indexKeys , ( indexKey : string ) => ind . remove ( indexKey ) ) ;
646+ each ( indexKeys , ( indexKey : string ) => {
647+ if ( ind . remove ( indexKey ) ) {
648+ removedFromThisIndex = true ;
649+ }
650+ } ) ;
625651 } else {
626- each ( indexKeys , ( idxKey : string ) =>
627- ind . remove ( { idxKey, primaryKey : key } )
628- ) ;
652+ each ( indexKeys , ( idxKey : string ) => {
653+ if ( ind . remove ( { idxKey, primaryKey : key } ) ) {
654+ removedFromThisIndex = true ;
655+ }
656+ } ) ;
657+ }
658+
659+ if ( removedFromThisIndex ) {
660+ indexesThatHadItem . push ( index ) ;
629661 }
630662 } ) ;
663+
664+ return indexesThatHadItem ;
631665 }
632666}
633667
@@ -739,22 +773,24 @@ class InMemoryIndex extends DbIndexFTSFromRangeQueries {
739773 * Removes item from index. For non-unique indices, a pair of index value and a primary key is required.
740774 * @param key a string, if it's a unique index, a pair of key value and a primary key, if it's a non-unique index
741775 * @param skipTransactionOnCreation
742- * @returns
776+ * @returns Whether an entry was actually found and removed.
743777 */
744778 public remove (
745779 key : string | { primaryKey : string ; idxKey : string } ,
746780 skipTransactionOnCreation ?: boolean
747- ) {
781+ ) : boolean {
748782 if ( ! skipTransactionOnCreation && ! this . _trans ! . internal_isOpen ( ) ) {
749783 throw new Error ( "InMemoryTransaction already closed" ) ;
750784 }
751785
752786 if ( typeof key === "string" ) {
787+ const hadKey = this . _indexTree . has ( key ) ;
753788 this . _indexTree . delete ( key ) ;
789+ return hadKey ;
754790 } else {
755791 const idxItems = this . _indexTree . get ( key . idxKey ) ;
756792 if ( ! idxItems ) {
757- return ;
793+ return false ;
758794 }
759795
760796 const idxItemsWithoutItem = idxItems . filter ( ( idxItem ) => {
@@ -765,6 +801,11 @@ class InMemoryIndex extends DbIndexFTSFromRangeQueries {
765801 return idxItemPrimaryKeyVal !== key . primaryKey ;
766802 } ) ;
767803
804+ if ( idxItemsWithoutItem . length === idxItems . length ) {
805+ // Nothing matched key.primaryKey -- no-op.
806+ return false ;
807+ }
808+
768809 // if we removed all items, remove the index tree node.
769810 // otherwise, update the index value with the new array
770811 // sans the primary key item
@@ -773,6 +814,7 @@ class InMemoryIndex extends DbIndexFTSFromRangeQueries {
773814 } else {
774815 this . _indexTree . set ( key . idxKey , idxItemsWithoutItem ) ;
775816 }
817+ return true ;
776818 }
777819 }
778820
0 commit comments