repo_description: A fast, local first, reactive Database for JavaScript Applications https://rxdb.info/
repo_topics: angular browser-database couchdb crdt database firebase graphql indexeddb ionic local-first localstorage nodejs nosql postgres pouchdb react-native realtime realtime-database rxdb sqlite
code: import type {
      RxDocument,
      RxDocumentData,
      RxStorageChangeEvent,
      WeakRef,
      FinalizationRegistry
  } from './types/index.d.ts';
  import {
      getFromMapOrThrow,
      overwriteGetterForCaching,
      requestIdlePromiseNoQueue
  } from './plugins/utils/index.ts';
  import {
      overwritable
  } from './overwritable.ts';
  import { Observable } from 'rxjs';
  
  /**
   * Because we have to create many cache items,
   * we use an array instead of an object with properties
   * for better performance and less memory usage.
   * @link https://stackoverflow.com/questions/17295056/array-vs-object-efficiency-in-javascript
   */
  declare type CacheItem<RxDocType, OrmMethods> = [
      /**
       * Store the different document states of time
       * based on their [revisionHeight+_meta.lwt] .
       * We store WeakRefs so that we can later clean up
       * document states that are no longer needed.
       *
       * Notice that we can not only rely on the revisionHeight
       * because when cleanup is used, two document states can end up with
       * the same revision but different _meta.lwt.
       */
      Map<string, WeakRef<RxDocument<RxDocType, OrmMethods>>>,
  
      /**
       * Store the latest known document state.
       * As long as any state of the document is in the cache,
       * we observe the changestream and update the latestDoc accordingly.
       * This makes it easier to optimize performance on other parts
       * because for each known document we can always get the current state
       * in the storage.
       * Also it makes it possible to call RxDocument.latest() in a non-async way
       * to retrieve the latest document state or to observe$ some property.
       *
       * To not prevent the whole cacheItem from being garbage collected,
       * we store only the document data here, but not the RxDocument.
       */
      RxDocumentData<RxDocType>
  ];
  
  
  /**
   * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
   */
  declare type FinalizationRegistryValue = {
      docId: string;
      rev: string;
      lwt: number;
  };
  
  /**
   * The DocumentCache stores RxDocument objects
   * by their primary key and revision.
   * This is useful on client side applications where
   * it is not known how much memory can be used, so
   * we de-duplicate RxDocument states to save memory.
   * To not fill up the memory with old document states, the DocumentCache
   * only contains weak references to the RxDocuments themself.
   * @link https://caniuse.com/?search=weakref
   */
  export class DocumentCache<RxDocType, OrmMethods> {
      public readonly cacheItemByDocId = new Map<string, CacheItem<RxDocType, OrmMethods>>();
  
      /**
       * Process stuff lazy to not block the CPU
       * on critical paths.
       */
      public readonly tasks = new Set<Function>();
  
      /**
       * Some JavaScript runtimes like QuickJS,
       * so not have a FinalizationRegistry or WeakRef.
       * Therefore we need a workaround which might waste a lot of memory,
       * but at least works.
       */
      public readonly registry?: FinalizationRegistry<FinalizationRegistryValue> = typeof FinalizationRegistry === 'function' ?
          new FinalizationRegistry<FinalizationRegistryValue>(docMeta => {
              const docId = docMeta.docId;
              const cacheItem = this.cacheItemByDocId.get(docId);
              if (cacheItem) {
                  cacheItem[0].delete(docMeta.rev + docMeta.lwt);
                  if (cacheItem[0].size === 0) {
                      /**
                       * No state of the document is cached anymore,
                       * so we can clean up.
                       */
                      this.cacheItemByDocId.delete(docId);
                  }
              }
          }) as any :
          undefined;
  
      constructor(
          public readonly primaryPath: string,
          public readonly changes$: Observable<RxStorageChangeEvent<RxDocType>[]>,
          /**
           * A method that can create a RxDocument by the given document data.
           */
          public documentCreator: (docData: RxDocumentData<RxDocType>) => RxDocument<RxDocType, OrmMethods>
      ) {
          changes$.subscribe((events: RxStorageChangeEvent<RxDocType>[]) => {
              this.tasks.add(() => {
                  const cacheItemByDocId = this.cacheItemByDocId;
                  for (let index = 0; index < events.length; index++) {
                      const event = events[index];
                      const cacheItem = cacheItemByDocId.get(event.documentId);
                      if (cacheItem) {
                          let documentData = event.documentData;
                          if (!documentData) {
                              documentData = event.previousDocumentData as any;
                          }
                          cacheItem[1] = documentData;
                      }
                  }
              });
              if (this.tasks.size <= 1) {
                  requestIdlePromiseNoQueue().then(() => {
                      this.processTasks();
                  });
              }
          });
      }
  
      public processTasks() {
          if (this.tasks.size === 0) {
              return;
          }
          this.tasks.forEach(task => task());
          this.tasks.clear();
      }
  
      /**
       * Get the RxDocument from the cache
       * and create a new one if not exits before.
       * @overwrites itself with the actual function
       * because this is @performance relevant.
       * It is called on each document row for each write and read.
       */
      get getCachedRxDocuments(): (docsData: RxDocumentData<RxDocType>[]) => RxDocument<RxDocType, OrmMethods>[] {
          const fn = getCachedRxDocumentMonad(this);
          return overwriteGetterForCaching(
              this,
              'getCachedRxDocuments',
              fn
          );
      }
  
      get getCachedRxDocument(): (docData: RxDocumentData<RxDocType>) => RxDocument<RxDocType, OrmMethods> {
          const fn = getCachedRxDocumentSingle(this);
          return overwriteGetterForCaching(
              this,
              'getCachedRxDocument',
              fn
          );
      }
  
      /**
       * Throws if not exists
       */
      public getLatestDocumentData(docId: string): RxDocumentData<RxDocType> {
          this.processTasks();
          const cacheItem = getFromMapOrThrow(this.cacheItemByDocId, docId);
          return cacheItem[1];
      }
  
      public getLatestDocumentDataIfExists(docId: string): RxDocumentData<RxDocType> | undefined {
          this.processTasks();
          const cacheItem = this.cacheItemByDocId.get(docId);
          if (cacheItem) {
              return cacheItem[1];
          }
      }
  }
  
  /**
   * @hotPath Dedicated single-document function that avoids array allocations.
   * Used by getCachedRxDocument which is called from many call sites.
   */
  function getCachedRxDocumentSingle<RxDocType, OrmMethods>(
      docCache: DocumentCache<RxDocType, OrmMethods>
  ): (docData: RxDocumentData<RxDocType>) => RxDocument<RxDocType, OrmMethods> {
      const primaryPath = docCache.primaryPath;
      const cacheItemByDocId = docCache.cacheItemByDocId;
      const registry = docCache.registry;
      const deepFreezeWhenDevMode = overwritable.deepFreezeWhenDevMode;
      const documentCreator = docCache.documentCreator;
  
      return (docData: RxDocumentData<RxDocType>): RxDocument<RxDocType, OrmMethods> => {
          const docId: string = (docData as any)[primaryPath];
          const rev = docData._rev;
          const lwt = docData._meta.lwt;
          const cacheKey = rev + lwt;
  
          const cacheItem = cacheItemByDocId.get(docId);
          if (!cacheItem) {
              docData = deepFreezeWhenDevMode(docData) as any;
              const newDoc = documentCreator(docData) as RxDocument<RxDocType, OrmMethods>;
              const newByRev = new Map<string, WeakRef<RxDocument<RxDocType, OrmMethods>>>();
              newByRev.set(cacheKey, createWeakRefWithFallback(newDoc));
              cacheItemByDocId.set(docId, [newByRev, docData]);
              if (registry) {
                  docCache.tasks.add(() => {
                      registry.register(newDoc, {
                          docId: newDoc.primary,
                          rev,
                          lwt
                      });
                  });
                  if (docCache.tasks.size <= 1) {
                      requestIdlePromiseNoQueue().then(() => {
                          docCache.processTasks();
                      });
                  }
              }
              return newDoc;
          }
  
          const byRev = cacheItem[0];
          const cachedRxDocumentWeakRef = byRev.get(cacheKey);
          let cachedRxDocument = cachedRxDocumentWeakRef ? cachedRxDocumentWeakRef.deref() : undefined;
          if (!cachedRxDocument) {
              docData = deepFreezeWhenDevMode(docData) as any;
              cachedRxDocument = documentCreator(docData) as RxDocument<RxDocType, OrmMethods>;
              byRev.set(cacheKey, createWeakRefWithFallback(cachedRxDocument));
              if (registry) {
                  const registeredDoc = cachedRxDocument;
                  docCache.tasks.add(() => {
                      registry.register(registeredDoc, {
                          docId: registeredDoc.primary,
                          rev,
                          lwt
                      });
                  });
                  if (docCache.tasks.size <= 1) {
                      requestIdlePromiseNoQueue().then(() => {
                          docCache.processTasks();
                      });
                  }
              }
          }
          return cachedRxDocument;
      };
  }
  
  
  /**
   * This function is called very very often.
   * @hotPath This is one of the most important methods for performance.
   * It is used in many places to transform the raw document data into RxDocuments.
   */
  function getCachedRxDocumentMonad<RxDocType, OrmMethods>(
      docCache: DocumentCache<RxDocType, OrmMethods>
  ): (docsData: RxDocumentData<RxDocType>[]) => RxDocument<RxDocType, OrmMethods>[] {
      const primaryPath = docCache.primaryPath;
      const cacheItemByDocId = docCache.cacheItemByDocId;
      const registry = docCache.registry;
      const deepFreezeWhenDevMode = overwritable.deepFreezeWhenDevMode;
      const documentCreator = docCache.documentCreator;
      const fn: (docsData: RxDocumentData<RxDocType>[]) => RxDocument<RxDocType, OrmMethods>[] = (docsData: RxDocumentData<RxDocType>[]) => {
          const ret: RxDocument<RxDocType, OrmMethods>[] = new Array(docsData.length);
          let registryTasks: { doc: RxDocument<RxDocType, OrmMethods>; rev: string; lwt: number; }[] | undefined;
          for (let index = 0; index < docsData.length; index++) {
              let docData = docsData[index];
              const docId: string = (docData as any)[primaryPath];
  
              const rev = docData._rev;
              const lwt = docData._meta.lwt;
              const cacheKey = rev + lwt;
  
              const cacheItem = cacheItemByDocId.get(docId);
              if (!cacheItem) {
                  /**
                   * New document - no need for WeakRef lookup.
                   * Create cache item directly.
                   */
                  docData = deepFreezeWhenDevMode(docData) as any;
                  const cachedRxDocument = documentCreator(docData) as RxDocument<RxDocType, OrmMethods>;
                  const byRev = new Map<string, WeakRef<RxDocument<RxDocType, OrmMethods>>>();
                  byRev.set(cacheKey, createWeakRefWithFallback(cachedRxDocument));
                  cacheItemByDocId.set(docId, [byRev, docData]);
                  ret[index] = cachedRxDocument;
                  if (registry) {
                      if (!registryTasks) {
                          registryTasks = [];
                      }
                      registryTasks.push({ doc: cachedRxDocument, rev, lwt });
                  }
              } else {
                  const byRev = cacheItem[0];
                  const cachedRxDocumentWeakRef = byRev.get(cacheKey);
                  let cachedRxDocument = cachedRxDocumentWeakRef ? cachedRxDocumentWeakRef.deref() : undefined;
                  if (!cachedRxDocument) {
                      docData = deepFreezeWhenDevMode(docData) as any;
                      cachedRxDocument = documentCreator(docData) as RxDocument<RxDocType, OrmMethods>;
                      byRev.set(cacheKey, createWeakRefWithFallback(cachedRxDocument));
                      if (registry) {
                          if (!registryTasks) {
                              registryTasks = [];
                          }
                          registryTasks.push({ doc: cachedRxDocument, rev, lwt });
                      }
                  }
                  ret[index] = cachedRxDocument;
              }
          }
          if (registryTasks && registry) {
              /**
               * Calling registry.register() has shown to have
               * really bad performance. So we add the cached documents
               * lazily.
               */
              const tasks = registryTasks;
              docCache.tasks.add(() => {
                  for (let index = 0; index < tasks.length; index++) {
                      const task = tasks[index];
                      registry.register(task.doc, {
                          docId: task.doc.primary,
                          rev: task.rev,
                          lwt: task.lwt
                      });
                  }
              });
              if (docCache.tasks.size <= 1) {
                  requestIdlePromiseNoQueue().then(() => {
                      docCache.processTasks();
                  });
              }
          }
          return ret;
      };
      return fn;
  }
  
  export function mapDocumentsDataToCacheDocs<RxDocType, OrmMethods>(
      docCache: DocumentCache<RxDocType, OrmMethods>,
      docsData: RxDocumentData<RxDocType>[]
  ) {
      const getCachedRxDocuments = docCache.getCachedRxDocuments;
      return getCachedRxDocuments(docsData);
  }
  
  /**
   * Fallback for JavaScript runtimes that do not support WeakRef.
   * The fallback will keep the items in cache forever,
   * but at least works.
   */
  const HAS_WEAK_REF = typeof WeakRef === 'function';
  const createWeakRefWithFallback = HAS_WEAK_REF ? createWeakRef : createWeakRefFallback;
  function createWeakRef<T extends object>(obj: T): WeakRef<T> {
      return new WeakRef(obj) as any;
  }
  function createWeakRefFallback<T extends object>(obj: T): WeakRef<T> {
      return {
          deref() {
              return obj;
          }
      } as any;
  }
  