All files / src/persistence/pouchDbStores PouchDbStore.ts

91.42% Statements 32/35
85.71% Branches 6/7
90.9% Functions 10/11
90.9% Lines 30/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 9342x   42x 42x   42x   42x 24x 24x       24x 24x 24x               18x 18x     18x                     3x 3x 3x           35x     42x 14x 14x           68x 68x 26x                 42x       16x 14x 14x     14x         14x 14x                  
import { EMPTY, Observable, from } from 'rxjs';
import { Logger } from 'ts-log';
import { toPouchDbDoc } from './util';
import PouchDB from 'pouchdb';
 
const FETCH_ALL_PAGE_SIZE = 100;
 
export abstract class PouchDbStore<T extends {}> {
  destroyed = false;
  protected idle: Promise<void> = Promise.resolve();
  protected readonly logger: Logger;
  public readonly db: PouchDB.Database<T>;
 
  constructor(public dbName: string, logger: Logger) {
    this.logger = logger;
    this.db = new PouchDB<T>(dbName, { auto_compaction: true });
  }
 
  /**
   * Only used internally and for cleaning up after tests.
   * If you need to use this for other purposes, consider adding clear() or destroy() to stores interfaces.
   */
  async clearDB(): Promise<void> {
    const docs = await this.fetchAllDocs();
    await this.db.bulkDocs(
      docs.map(
        (row) =>
          ({
            _deleted: true,
            _id: row.id,
            _rev: row.value.rev
          } as unknown as T)
      )
    );
  }
 
  /** Might all destroy other stores, if the underlying PouchDb database is shared. */
  destroy(): Observable<void> {
    if (!this.destroyed) {
      this.destroyed = true;
      return from(this.db.destroy());
    }
    return EMPTY;
  }
 
  protected toPouchDbDoc(obj: T): T {
    return toPouchDbDoc(obj) as T;
  }
 
  async #getRev(docId: string) {
    const existingDoc = await this.db.get(docId).catch(() => void 0);
    return existingDoc?._rev;
  }
 
  async fetchAllDocs(
    options?: Omit<Partial<PouchDB.Core.AllDocsWithinRangeOptions>, 'limit'>
  ): Promise<PouchDB.Core.AllDocsResponse<T>['rows']> {
    const response = await this.db.allDocs({ ...options, limit: FETCH_ALL_PAGE_SIZE });
    if (response && response.rows.length > 0) {
      return [
        ...response.rows,
        ...(await this.fetchAllDocs({
          ...options,
          skip: 1,
          startkey: response.rows[response.rows.length - 1].id
        }))
      ];
    }
    return response.rows || [];
  }
 
  protected forcePut(docId: string, doc: T) {
    if (this.destroyed) return EMPTY;
    const serializableDoc = this.toPouchDbDoc(doc);
    return from(
      (this.idle = this.idle
        .then(async () => {
          const pouchDbDoc = {
            _id: docId,
            _rev: await this.#getRev(docId),
            ...serializableDoc
          };
          try {
            await this.db.put(pouchDbDoc, { force: true });
          } catch (error) {
            this.logger.warn(`PouchDbStore(${this.dbName}): failed to forcePut`, pouchDbDoc, error);
          }
        })
        .catch(() => void 0))
    );
  }
}