import { gql , InMemoryCache , Cache , NormalizedCacheObject , Reference , StoreObject , makeReference , } from "@apollo/client/core" ; import { compact , every , pick , isFunction } from "lodash-es" ; import InvalidationPolicyManager from "../policies/InvalidationPolicyManager" ; import { EntityStoreWatcher , EntityTypeMap } from "../entity-store" ; import { makeEntityId , isQuery , maybeDeepClone , fieldNameFromStoreName } from "../helpers" ; import { FragmentWhereFilter , InvalidationPolicyCacheConfig } from "./types" ; import { CacheResultProcessor , ReadResultStatus } from "./CacheResultProcessor" ; import { InvalidationPolicyEvent , ReadFieldOptions } from "../policies/types" ; import { FragmentDefinitionNode } from 'graphql' ; import { cacheExtensionsCollectionTypename , collectionEntityIdForType } from './utils' ; /**
 * Extension of Apollo in-memory cache which adds support for cache policies.
 */ export default class InvalidationPolicyCache extends InMemoryCache { protected entityTypeMap : EntityTypeMap ; protected entityStoreWatcher : EntityStoreWatcher ; protected invalidationPolicyManager : InvalidationPolicyManager ; protected cacheResultProcessor : CacheResultProcessor ; protected entityStoreRoot : any ; protected isBroadcasting : boolean ; protected enableCollections : boolean ; constructor ( config = { } ) { const { invalidationPolicies = { } , enableCollections = false , ... inMemoryCacheConfig } = config ; super ( inMemoryCacheConfig ) ; this . enableCollections = enableCollections ; this . isBroadcasting = false ; const { entityTypeMap } = this ; this . invalidationPolicyManager = new InvalidationPolicyManager ( { policies : invalidationPolicies , entityTypeMap : entityTypeMap , cacheOperations : { evict : ( ... args ) => this . evict ( ... args ) , modify : ( ... args ) => this . modify ( ... args ) , readField : ( ... args ) => this . readField ( ... args ) , } , } ) ; this . cacheResultProcessor = new CacheResultProcessor ( { invalidationPolicyManager : this . invalidationPolicyManager , entityTypeMap : this . entityTypeMap , cache : this , } ) ; } private init ( ) { super . init ( ) ; this . entityStoreRoot = this . data ; this . entityTypeMap = new EntityTypeMap ( ) ; this . entityStoreWatcher = new EntityStoreWatcher ( { entityStore : this . entityStoreRoot , entityTypeMap : this . entityTypeMap , policies : this . policies , updateCollectionField : this . updateCollectionField . bind ( this ) , } ) ; this . cacheResultProcessor = new CacheResultProcessor ( { invalidationPolicyManager : this . invalidationPolicyManager , entityTypeMap : this . entityTypeMap , cache : this , } ) ; } protected readField < T > ( fieldNameOrOptions ? , from ? ) { if ( ! fieldNameOrOptions ) { return ; } const options = typeof fieldNameOrOptions === "string" ? { fieldName : fieldNameOrOptions , from , } : fieldNameOrOptions ; if ( void 0 === options . from ) { options . from = { __ref : 'ROOT_QUERY' } ; } return this . policies . readField < T > ( options , { store : this . entityStoreRoot , } ) ; } protected broadcastWatches ( ) { this . isBroadcasting = true ; super . broadcastWatches ( ) ; this . isBroadcasting = false ; } isOperatingOnRootData ( ) { return this . data === this . entityStoreRoot ; } modify ( options ) { const modifyResult = super . modify ( options ) ; if ( ! this . invalidationPolicyManager . isPolicyEventActive ( InvalidationPolicyEvent . Write ) || ! modifyResult ) { return modifyResult ; } const { id = "ROOT_QUERY" , fields } = options ; if ( isQuery ( id ) ) { Object . keys ( fields ) . forEach ( ( storeFieldName ) => { const fieldName = fieldNameFromStoreName ( storeFieldName ) ; const typename = this . entityTypeMap . readEntityById ( makeEntityId ( id , fieldName ) ) ?. typename ; if ( ! typename ) { return ; } this . invalidationPolicyManager . runWritePolicy ( typename , { parent : { id , fieldName , storeFieldName , ref : makeReference ( id ) , } , } ) ; } ) ; } else { const typename = this . entityTypeMap . readEntityById ( id ) ?. typename ; if ( ! typename ) { return modifyResult ; } this . invalidationPolicyManager . runWritePolicy ( typename , { parent : { id , ref : makeReference ( id ) , } , } ) ; } if ( options . broadcast ) { this . broadcastWatches ( ) ; } return modifyResult ; } write ( options ) { const writeResult = super . write ( options ) ; if ( ( ! this . invalidationPolicyManager . isPolicyEventActive ( InvalidationPolicyEvent . Write ) && ! this . invalidationPolicyManager . isPolicyEventActive ( InvalidationPolicyEvent . Read ) ) || ! this . isOperatingOnRootData ( ) ) { return writeResult ; } this . cacheResultProcessor . processWriteResult ( options ) ; if ( options . broadcast ) { this . broadcastWatches ( ) ; } return writeResult ; } evictWhere < EntityType > ( filters ) { const { __typename , filter } = filters ; const references = this . readReferenceWhere ( { __typename , filter , } ) ; references . forEach ( ( ref ) => this . evict ( { id : ref . __ref , broadcast : false } ) ) ; this . broadcastWatches ( ) ; return references ; } evict ( options ) { const { fieldName , args } = options ; let { id } = options ; if ( ! id ) { if ( Object . prototype . hasOwnProperty . call ( options , "id" ) ) { return false ; } id = "ROOT_QUERY" ; } if ( this . invalidationPolicyManager . isPolicyEventActive ( InvalidationPolicyEvent . Evict ) ) { const { typename } = this . entityTypeMap . readEntityById ( makeEntityId ( id , fieldName ) ) ?? { } ; if ( typename ) { const storeFieldName = isQuery ( id ) && fieldName ? this . policies . getStoreFieldName ( { typename , fieldName , args , } ) : undefined ; this . invalidationPolicyManager . runEvictPolicy ( typename , { parent : { id , fieldName , storeFieldName , variables : args , ref : makeReference ( id ) , } , } ) ; } } return super . evict ( options ) ; } protected updateCollectionField ( typename , dataId ) { if ( ! this . enableCollections ) { return ; } const collectionEntityId = collectionEntityIdForType ( typename ) ; const collectionFieldExists = ! ! this . readField < Record < string , any [ ] > > ( 'id' , makeReference ( collectionEntityId ) ) ; if ( ! collectionFieldExists ) { this . writeFragment ( { id : collectionEntityId , fragment : gql `
          fragment InitializeCollectionEntity on CacheExtensionsCollectionEntity {
            data
            id
          }
        ` , data : { __typename : cacheExtensionsCollectionTypename , id : typename , data : [ ] , } , } ) ; } if ( ! this . entityTypeMap . readEntityById ( dataId ) ) { this . modify ( { broadcast : false , id : collectionEntityId , fields : { data : ( existing , { canRead } ) => { return [ ... existing . filter ( ( ref ) => canRead ( ref ) ) , makeReference ( dataId ) , ] ; } } } ) ; } } private _expire ( reportOnly = false ) { const { entitiesById } = this . entityTypeMap . extract ( ) ; const expiredEntityIds = [ ] ; Object . keys ( entitiesById ) . forEach ( ( entityId ) => { const entity = entitiesById [ entityId ] ; const { storeFieldNames , dataId , fieldName , typename } = entity ; if ( isQuery ( dataId ) && storeFieldNames ) { Object . keys ( storeFieldNames . entries ) . forEach ( ( storeFieldName ) => { const isExpired = this . invalidationPolicyManager . runReadPolicy ( { typename , dataId , fieldName , storeFieldName , reportOnly , } ) ; if ( isExpired ) { expiredEntityIds . push ( makeEntityId ( dataId , storeFieldName ) ) ; } } ) ; } else { const isExpired = this . invalidationPolicyManager . runReadPolicy ( { typename , dataId , fieldName , reportOnly , } ) ; if ( isExpired ) { expiredEntityIds . push ( makeEntityId ( dataId ) ) ; } } } ) ; if ( expiredEntityIds . length > 0 ) { this . broadcastWatches ( ) ; } return expiredEntityIds ; } expire ( ) { return this . _expire ( false ) ; } expiredEntities ( ) { return this . _expire ( true ) ; } activatePolicyEvents ( ... policyEvents ) { if ( policyEvents . length > 0 ) { this . invalidationPolicyManager . activatePolicies ( ... policyEvents ) ; } else { this . invalidationPolicyManager . activatePolicies ( InvalidationPolicyEvent . Read , InvalidationPolicyEvent . Write , InvalidationPolicyEvent . Evict ) ; } } deactivatePolicyEvents ( ... policyEvents ) { if ( policyEvents . length > 0 ) { this . invalidationPolicyManager . deactivatePolicies ( ... policyEvents ) ; } else { this . invalidationPolicyManager . deactivatePolicies ( InvalidationPolicyEvent . Read , InvalidationPolicyEvent . Write , InvalidationPolicyEvent . Evict ) ; } } activePolicyEvents ( ) { return [ InvalidationPolicyEvent . Read , InvalidationPolicyEvent . Write , InvalidationPolicyEvent . Evict ] . filter ( policyEvent => this . invalidationPolicyManager . isPolicyEventActive ( policyEvent ) ) ; } read < T > ( options ) { const result = super . read < T > ( options ) ; if ( ! this . invalidationPolicyManager . isPolicyEventActive ( InvalidationPolicyEvent . Read ) ) { return result ; } const processedResult = maybeDeepClone ( result ) ; const processedResultStatus = this . cacheResultProcessor . processReadResult ( processedResult , options ) ; if ( processedResultStatus === ReadResultStatus . Complete ) { return result ; } this . broadcastWatches ( ) ; return processedResultStatus === ReadResultStatus . Evicted ? null : processedResult ; } diff < T > ( options ) { const cacheDiff = super . diff < T > ( options ) ; if ( ! this . invalidationPolicyManager . isPolicyEventActive ( InvalidationPolicyEvent . Read ) || this . isBroadcasting ) { return cacheDiff ; } const { result } = cacheDiff ; const processedResult = maybeDeepClone ( result ) ; const processedResultStatus = this . cacheResultProcessor . processReadResult ( processedResult , options ) ; if ( processedResultStatus === ReadResultStatus . Complete ) { return cacheDiff ; } this . broadcastWatches ( ) ; cacheDiff . complete = false ; cacheDiff . result = processedResultStatus === ReadResultStatus . Evicted ? undefined : processedResult ; return cacheDiff ; } extract ( optimistic = false , withInvalidation = true ) { const extractedCache = super . extract ( optimistic ) ; if ( withInvalidation ) { extractedCache . invalidation = pick ( this . entityTypeMap . extract ( ) , "entitiesById" ) ; } return extractedCache ; } readFragmentWhere < FragmentType , TVariables = any > ( options ) { const { fragment , filter , ... restOptions } = options ; const fragmentDefinition = fragment . definitions [ 0 ] as FragmentDefinitionNode ; const __typename = fragmentDefinition . typeCondition . name . value ; const matchingRefs = this . readReferenceWhere ( { __typename , filter } ) ; const matchingFragments = matchingRefs . map ( ref => this . readFragment ( { ... restOptions , fragment , id : ref . __ref , } ) ) ; return compact ( matchingFragments ) ; } readReferenceWhere < T > ( options ) { const { __typename , filter } = options ; const collectionEntityName = collectionEntityIdForType ( __typename ) ; const entityReferences = this . readField < Reference [ ] > ( 'data' , makeReference ( collectionEntityName ) ) ; if ( ! entityReferences ) { return [ ] ; } if ( ! filter ) { return entityReferences ; } return entityReferences . filter ( ref => { if ( isFunction ( filter ) ) { return filter ( ref , this . readField . bind ( this ) ) ; } const entityFilterResults = Object . keys ( filter ) . map ( filterField => { const filterValue = filter [ filterField ] ; const entityValueForFilter = this . readField ( filterField , ref ) ; return filterValue === entityValueForFilter ; } ) ; return every ( entityFilterResults , Boolean ) ; } ) ; } }