feathersjs-offline/owndata-ownnet

View on GitHub
packages/client/src/owndata/index.js

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
import { stripSlashes } from '@feathersjs/commons';
import errors from '@feathersjs/errors';
import { to } from '../common';
import OwnClass from '../own-common';

const debug = require('debug')('@feathersjs-offline:owndata:service-wrapper');
const BOT = new Date(0).toISOString();

class OwndataClass extends OwnClass {
  constructor (opts) {
    debug(`Constructor started, opts = ${JSON.stringify(opts)}`);
    super(opts);

    this.type = 'own-data';
    this.__forTestingOnly = super._processQueuedEvents;

    debug('  Done.');
    return this;
  }

  async _processQueuedEvents () {
    debug(`processQueuedEvents (${this.type}) entered`);
    if(!this.internalProcessingAllowed() || this.pQActive) {
      // console.log(`processingQueuedEvents: internalProcessing (aIP=${this.aIP}), pQActive=${this.pQActive}`);
      return false;
    }
    this.pQActive = true;

    let [err, store] = await to(this.localQueue.getEntries({query:{$sort: {[this.id]: 1}}}));
    if (!store || store === []) {
      this.pQActive = false;
      return true;
    }

    debug(`  processing ${store.length} queued entries...`);
    let self = this;
    let stop = false;
    while (store.length && !stop) {
      let el = store.shift();
      let { eventName, record, arg1, arg2, arg3 = null, id } = el;
      ({eventName, el, arg1, arg2, arg3, id} = _accEvent(this.id, eventName, record, arg1, arg2, arg3, id));

      debug(`    processing: event=${eventName}, arg1=${JSON.stringify(arg1)}, arg2=${JSON.stringify(arg2)}, arg3=${JSON.stringify(arg3)}`)
      await self.remoteService[eventName](arg1, arg2, arg3)
        .then(async res => {
          await to(self.localQueue.remove(id));
          if (eventName !== 'remove') {
            // Let any updates to the document/item on server reflect on the local DB
            await to(self.localService.patch(res[self.id], res));
          }
        })
        .catch(async err => {
          debug(`catch: record=${JSON.stringify(record)}, BOT=${JSON.stringify(BOT)}, event=${eventName}, id=${id}`);
          if (record.onServerAt === BOT  &&  eventName === 'remove') {
            // This record has probably never been on server (=remoteService), so we silently ignore the error
            await to(self.localQueue.remove(id));
          }
          else {
            // The connection to the server has probably been cut - let's continue at another time
            stop = true;
          }
        });
    }

    debug(`  processing ended, stop=${stop}`);

    this.pQActive = false;

    return !stop;
  }

};

function init (options) {
  return new OwndataClass(options);
}

let Owndata = init;

/**
 * A owndataWrapper is a CLIENT adapter wrapping for FeathersJS services extending them to
 * implement the offline own-data principle (**LINK-TO-DOC**).
 *
 * @example ```
 * import feathers from '(at)feathersjs/feathers';
 * import memory from 'feathers-memory';
 * import { owndataWrapper } from '(at)feathersjs-offline/client';
 * const app = feathers();
 * app.use('/testpath', memory({id: 'uuid'}));
 * owndataWrapper(app, '/testpath');
 * app.service('testpath').create({givenName: 'James', familyName: 'Bond'})
 * // ...
 * ```
 *
 * It works in co-existence with it's SERVER counterpart, RealtimeServiceWrapper.
 *
 * @param {object} app  The application handle
 * @param {object} path The service path (as used in ```app.use(path, serviceAdapter)```)
 * @param {object} options The options for the serviceAdaptor AND the OwndataWrapper
 *
 */
function owndataWrapper (app, path, options = {}) {
  debug(`owndataWrapper started on path '${path}'`)
  if (!(app && app.version && app.service && app.services) ) {
    throw new errors.Unavailable(`The FeathersJS app must be supplied as first argument`);
  }

  let location = stripSlashes(path);

  let old = app.service(location);
  if (typeof old === 'undefined') {
    throw new errors.Unavailable(`No prior service registered on path '${location}'`);
  }

  let opts = Object.assign({}, old.options, options);
  app.use(location, Owndata(opts));
  let service = app.service(location);
  service.options = opts;
  service._listenOptions();

  return service;
}

module.exports = { init, Owndata, owndataWrapper };

init.Service = OwndataClass;

// Helper

function _accEvent (idName, eventName, el, arg1, arg2, arg3, id) {
  let elId = el[idName];
  switch (eventName) {
    case 'create':
      return { eventName, el, arg1: el, arg2: arg2, arg3: {}, id };
    case 'update':
      return { eventName, el, arg1, arg2, arg3: arg3, id };
    case 'patch':
      return { eventName, el, arg1: elId, arg2: el, arg3: arg3, id };
    case 'remove':
      return { eventName, el, arg1: elId, arg2: {}, arg3: arg3, id };
  }
}