All files / src/persistence/pouchDbStores PouchDbStore.ts

91.17% Statements 31/34
85.71% Branches 6/7
90.9% Functions 10/11
90.62% Lines 29/32

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 9343x   43x 43x   43x   43x 25x 25x       25x 25x 25x               18x 18x     18x                     3x 3x 3x           36x       17x 17x           68x 68x 26x                 42x       17x 15x 15x     15x         15x 15x                  
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;
  }
 
  protected 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))
    );
  }
}