diff --git a/docs/api/cozy-pouch-link/classes/PouchLink.md b/docs/api/cozy-pouch-link/classes/PouchLink.md index 4a112f365f..a396a9c90e 100644 --- a/docs/api/cozy-pouch-link/classes/PouchLink.md +++ b/docs/api/cozy-pouch-link/classes/PouchLink.md @@ -27,6 +27,7 @@ constructor - Initializes a new PouchLink | `opts` | `Object` | - | | `opts.doctypes` | `string`\[] | Doctypes to replicate | | `opts.doctypesReplicationOptions` | `any`\[] | A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote") | +| `opts.platform` | `LinkPlatform` | Platform specific adapters and methods | | `opts.replicationInterval` | `number` | - | *Overrides* @@ -35,7 +36,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:84](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L84) +[CozyPouchLink.js:81](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L81) ## Properties @@ -45,7 +46,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:132](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L132) +[CozyPouchLink.js:134](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L134) *** @@ -55,7 +56,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:94](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L94) +[CozyPouchLink.js:91](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L91) *** @@ -65,7 +66,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:95](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L95) +[CozyPouchLink.js:92](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L92) *** @@ -75,17 +76,17 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:96](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L96) +[CozyPouchLink.js:93](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L93) *** ### options -• **options**: { `replicationInterval`: `number` } & { `doctypes`: `string`\[] ; `doctypesReplicationOptions`: `any`\[] ; `replicationInterval`: `number` } +• **options**: { `replicationInterval`: `number` } & { `doctypes`: `string`\[] ; `doctypesReplicationOptions`: `any`\[] ; `platform`: `LinkPlatform` ; `replicationInterval`: `number` } *Defined in* -[CozyPouchLink.js:88](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L88) +[CozyPouchLink.js:85](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L85) *** @@ -95,7 +96,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:202](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L202) +[CozyPouchLink.js:204](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L204) *** @@ -107,6 +108,16 @@ CozyLink.constructor [CozyPouchLink.js:99](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L99) +*** + +### storage + +• **storage**: `PouchLocalStorage` + +*Defined in* + +[CozyPouchLink.js:94](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L94) + ## Methods ### addReferencesTo @@ -125,7 +136,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:563](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L563) +[CozyPouchLink.js:567](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L567) *** @@ -145,7 +156,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:524](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L524) +[CozyPouchLink.js:528](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L528) *** @@ -166,7 +177,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:567](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L567) +[CozyPouchLink.js:571](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L571) *** @@ -186,7 +197,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:552](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L552) +[CozyPouchLink.js:556](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L556) *** @@ -207,7 +218,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:423](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L423) +[CozyPouchLink.js:427](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L427) *** @@ -229,7 +240,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:495](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L495) +[CozyPouchLink.js:499](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L499) *** @@ -249,7 +260,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:441](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L441) +[CozyPouchLink.js:445](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L445) *** @@ -269,7 +280,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:317](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L317) +[CozyPouchLink.js:321](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L321) *** @@ -289,7 +300,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:112](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L112) +[CozyPouchLink.js:114](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L114) *** @@ -309,7 +320,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:313](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L313) +[CozyPouchLink.js:317](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L317) *** @@ -329,7 +340,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:254](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L254) +[CozyPouchLink.js:258](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L258) *** @@ -349,7 +360,7 @@ CozyLink.constructor *Defined in* -[CozyPouchLink.js:249](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L249) +[CozyPouchLink.js:253](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L253) *** @@ -375,7 +386,7 @@ Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done *Defined in* -[CozyPouchLink.js:235](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L235) +[CozyPouchLink.js:239](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L239) *** @@ -395,7 +406,7 @@ Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done *Defined in* -[CozyPouchLink.js:405](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L405) +[CozyPouchLink.js:409](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L409) *** @@ -416,7 +427,7 @@ Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done *Defined in* -[CozyPouchLink.js:410](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L410) +[CozyPouchLink.js:414](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L414) *** @@ -446,13 +457,13 @@ Migrate the current adapter *Defined in* -[CozyPouchLink.js:146](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L146) +[CozyPouchLink.js:148](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L148) *** ### needsToWaitWarmup -▸ **needsToWaitWarmup**(`doctype`): `boolean` +▸ **needsToWaitWarmup**(`doctype`): `Promise`<`boolean`> Check if there is warmup queries for this doctype and return if those queries are already warmed up or not @@ -465,13 +476,13 @@ and return if those queries are already warmed up or not *Returns* -`boolean` +`Promise`<`boolean`> the need to wait for the warmup *Defined in* -[CozyPouchLink.js:391](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L391) +[CozyPouchLink.js:395](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L395) *** @@ -485,7 +496,7 @@ the need to wait for the warmup *Defined in* -[CozyPouchLink.js:165](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L165) +[CozyPouchLink.js:167](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L167) *** @@ -505,7 +516,7 @@ the need to wait for the warmup *Defined in* -[CozyPouchLink.js:293](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L293) +[CozyPouchLink.js:297](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L297) *** @@ -525,13 +536,13 @@ the need to wait for the warmup *Defined in* -[CozyPouchLink.js:131](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L131) +[CozyPouchLink.js:133](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L133) *** ### request -▸ **request**(`operation`, `result?`, `forward?`): `void` | `Promise`<`any`> +▸ **request**(`operation`, `result?`, `forward?`): `Promise`<`any`> *Parameters* @@ -543,7 +554,7 @@ the need to wait for the warmup *Returns* -`void` | `Promise`<`any`> +`Promise`<`any`> *Overrides* @@ -551,7 +562,7 @@ CozyLink.request *Defined in* -[CozyPouchLink.js:336](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L336) +[CozyPouchLink.js:340](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L340) *** @@ -565,7 +576,7 @@ CozyLink.request *Defined in* -[CozyPouchLink.js:219](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L219) +[CozyPouchLink.js:223](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L223) *** @@ -584,7 +595,7 @@ Emits pouchlink:sync:start event when the replication begins *Defined in* -[CozyPouchLink.js:268](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L268) +[CozyPouchLink.js:272](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L272) *** @@ -603,7 +614,7 @@ Emits pouchlink:sync:stop event *Defined in* -[CozyPouchLink.js:285](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L285) +[CozyPouchLink.js:289](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L289) *** @@ -623,7 +634,7 @@ Emits pouchlink:sync:stop event *Defined in* -[CozyPouchLink.js:321](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L321) +[CozyPouchLink.js:325](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L325) *** @@ -637,7 +648,7 @@ Emits pouchlink:sync:stop event *Defined in* -[CozyPouchLink.js:589](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L589) +[CozyPouchLink.js:593](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L593) *** @@ -657,7 +668,7 @@ Emits pouchlink:sync:stop event *Defined in* -[CozyPouchLink.js:529](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L529) +[CozyPouchLink.js:533](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L533) *** @@ -677,23 +688,29 @@ Emits pouchlink:sync:stop event *Defined in* -[CozyPouchLink.js:534](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L534) +[CozyPouchLink.js:538](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L538) *** ### getPouchAdapterName -▸ `Static` **getPouchAdapterName**(): `string` +▸ `Static` **getPouchAdapterName**(`localStorage`): `Promise`<`string`> Return the PouchDB adapter name. Should be IndexedDB for newest adapters. +*Parameters* + +| Name | Type | Description | +| :------ | :------ | :------ | +| `localStorage` | `LocalStorage` | Methods to access local storage | + *Returns* -`string` +`Promise`<`string`> The adapter name *Defined in* -[CozyPouchLink.js:108](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L108) +[CozyPouchLink.js:109](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L109) diff --git a/packages/cozy-pouch-link/examples/periodic-sync/index.js b/packages/cozy-pouch-link/examples/periodic-sync/index.js index ecfa6b5990..9e84eb7257 100755 --- a/packages/cozy-pouch-link/examples/periodic-sync/index.js +++ b/packages/cozy-pouch-link/examples/periodic-sync/index.js @@ -62,8 +62,9 @@ class App extends React.Component { } componentDidMount() { - this.createManager() - this.displayDocs() + this.createManager().then(() => { + this.displayDocs() + }) } componentWillUnmount() { @@ -83,7 +84,7 @@ class App extends React.Component { }) } - createManager() { + async createManager() { this.manager = new PouchManager([DOCTYPE], { replicationDelay: 2 * 1000, getReplicationURL: this.getReplicationURL, @@ -101,6 +102,7 @@ class App extends React.Component { this.displayDocs() } }) + await this.manager.init() } async displayDocs() { diff --git a/packages/cozy-pouch-link/src/CozyPouchLink.js b/packages/cozy-pouch-link/src/CozyPouchLink.js index cc4dd3f181..9b7f43da49 100644 --- a/packages/cozy-pouch-link/src/CozyPouchLink.js +++ b/packages/cozy-pouch-link/src/CozyPouchLink.js @@ -16,15 +16,11 @@ import { default as helpers } from './helpers' import { getIndexNameFromFields, getIndexFields } from './mango' import * as jsonapi from './jsonapi' import PouchManager from './PouchManager' +import { PouchLocalStorage } from './localStorage' import logger from './logger' import { migratePouch } from './migrations/adapter' +import { platformWeb } from './platformWeb' import { getDatabaseName, getPrefix } from './utils' -import { - getPersistedSyncedDoctypes, - persistAdapterName, - getAdapterName, - destroyWarmedUpQueries -} from './localStorage' PouchDB.plugin(PouchDBFind) @@ -79,6 +75,7 @@ class PouchLink extends CozyLink { * @param {number} [opts.replicationInterval] Milliseconds between replications * @param {string[]} opts.doctypes Doctypes to replicate * @param {object[]} opts.doctypesReplicationOptions A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote") + * @param {import('./types').LinkPlatform} opts.platform Platform specific adapters and methods */ constructor(opts) { @@ -94,6 +91,9 @@ class PouchLink extends CozyLink { this.doctypes = doctypes this.doctypesReplicationOptions = doctypesReplicationOptions this.indexes = {} + this.storage = new PouchLocalStorage( + options.platform?.storage || platformWeb.storage + ) /** @type {Record} - Stores replication states per doctype */ this.replicationStatus = this.replicationStatus || {} @@ -103,10 +103,12 @@ class PouchLink extends CozyLink { * Return the PouchDB adapter name. * Should be IndexedDB for newest adapters. * - * @returns {string} The adapter name + * @param {import('./types').LocalStorage} localStorage Methods to access local storage + * @returns {Promise} The adapter name */ - static getPouchAdapterName = () => { - return getAdapterName() + static getPouchAdapterName = localStorage => { + const storage = new PouchLocalStorage(localStorage || platformWeb.storage) + return storage.getAdapterName() } getReplicationURL(doctype) { @@ -148,15 +150,15 @@ class PouchLink extends CozyLink { for (const plugin of plugins) { PouchDB.plugin(plugin) } - const doctypes = getPersistedSyncedDoctypes() + const doctypes = await this.storage.getPersistedSyncedDoctypes() for (const doctype of Object.keys(doctypes)) { const prefix = getPrefix(url) const dbName = getDatabaseName(prefix, doctype) await migratePouch({ dbName, fromAdapter, toAdapter }) - destroyWarmedUpQueries() // force recomputing indexes + await this.storage.destroyWarmedUpQueries() // force recomputing indexes } - persistAdapterName('indexeddb') + await this.storage.persistAdapterName('indexeddb') } catch (err) { console.error('PouchLink: PouchDB migration failed. ', err) } @@ -194,9 +196,9 @@ class PouchLink extends CozyLink { logger.log('Create pouches with ' + prefix + ' prefix') } - if (!getAdapterName()) { + if (!(await this.storage.getAdapterName())) { const adapter = get(this.options, 'pouch.options.adapter') - persistAdapterName(adapter) + await this.storage.persistAdapterName(adapter) } this.pouches = new PouchManager(this.doctypes, { @@ -208,8 +210,10 @@ class PouchLink extends CozyLink { onDoctypeSyncStart: this.handleDoctypeSyncStart.bind(this), onDoctypeSyncEnd: this.handleDoctypeSyncEnd.bind(this), prefix, - executeQuery: this.executeQuery.bind(this) + executeQuery: this.executeQuery.bind(this), + platform: this.options.platform }) + await this.pouches.init() if (this.client && this.options.initialSync) { this.startReplication() @@ -333,7 +337,7 @@ class PouchLink extends CozyLink { return !!this.getPouch(impactedDoctype) } - request(operation, result = null, forward = doNothing) { + async request(operation, result = null, forward = doNothing) { const doctype = getDoctypeFromOperation(operation) if (!this.pouches) { @@ -355,7 +359,7 @@ class PouchLink extends CozyLink { return forward(operation) } - if (this.needsToWaitWarmup(doctype)) { + if (await this.needsToWaitWarmup(doctype)) { if (process.env.NODE_ENV !== 'production') { logger.info( `Tried to access local ${doctype} but not warmuped yet. Forwarding the operation to next link` @@ -386,18 +390,18 @@ class PouchLink extends CozyLink { * and return if those queries are already warmed up or not * * @param {string} doctype - Doctype to check - * @returns {boolean} the need to wait for the warmup + * @returns {Promise} the need to wait for the warmup */ - needsToWaitWarmup(doctype) { + async needsToWaitWarmup(doctype) { if ( this.doctypesReplicationOptions && this.doctypesReplicationOptions[doctype] && this.doctypesReplicationOptions[doctype].warmupQueries ) { - return !this.pouches.areQueriesWarmedUp( + return !(await this.pouches.areQueriesWarmedUp( doctype, this.doctypesReplicationOptions[doctype].warmupQueries - ) + )) } return false } diff --git a/packages/cozy-pouch-link/src/PouchManager.js b/packages/cozy-pouch-link/src/PouchManager.js index 87eee2017d..b1eaf57ddd 100644 --- a/packages/cozy-pouch-link/src/PouchManager.js +++ b/packages/cozy-pouch-link/src/PouchManager.js @@ -1,4 +1,3 @@ -import PouchDB from 'pouchdb-browser' import fromPairs from 'lodash/fromPairs' import forEach from 'lodash/forEach' import get from 'lodash/get' @@ -7,11 +6,12 @@ import zip from 'lodash/zip' import startsWith from 'lodash/startsWith' import { isMobileApp } from 'cozy-device-helper' +import { PouchLocalStorage } from './localStorage' import Loop from './loop' import logger from './logger' +import { platformWeb } from './platformWeb' import { fetchRemoteLastSequence } from './remote' import { startReplication } from './startReplication' -import * as localStorage from './localStorage' import { getDatabaseName } from './utils' const DEFAULT_DELAY = 30 * 1000 @@ -34,20 +34,35 @@ const getQueryAlias = query => { class PouchManager { constructor(doctypes, options) { this.options = options - const pouchPlugins = get(options, 'pouch.plugins', []) - const pouchOptions = get(options, 'pouch.options', {}) + this.doctypes = doctypes - forEach(pouchPlugins, plugin => PouchDB.plugin(plugin)) + this.storage = new PouchLocalStorage( + options.platform?.storage || platformWeb.storage + ) + this.PouchDB = options.platform?.pouchAdapter || platformWeb.pouchAdapter + this.isOnline = options.platform?.isOnline || platformWeb.isOnline + this.events = options.platform?.events || platformWeb.events + } + + async init() { + const pouchPlugins = get(this.options, 'pouch.plugins', []) + const pouchOptions = get(this.options, 'pouch.options', {}) + + forEach(pouchPlugins, plugin => this.PouchDB.plugin(plugin)) this.pouches = fromPairs( - doctypes.map(doctype => [ + this.doctypes.map(doctype => [ doctype, - new PouchDB(getDatabaseName(options.prefix, doctype), pouchOptions) + new this.PouchDB( + getDatabaseName(this.options.prefix, doctype), + pouchOptions + ) ]) ) - this.syncedDoctypes = localStorage.getPersistedSyncedDoctypes() - this.warmedUpQueries = localStorage.getPersistedWarmedUpQueries() - this.getReplicationURL = options.getReplicationURL - this.doctypesReplicationOptions = options.doctypesReplicationOptions || {} + this.syncedDoctypes = await this.storage.getPersistedSyncedDoctypes() + this.warmedUpQueries = await this.storage.getPersistedWarmedUpQueries() + this.getReplicationURL = this.options.getReplicationURL + this.doctypesReplicationOptions = + this.options.doctypesReplicationOptions || {} this.listenerLaunched = false // We must ensure databases exist on the remote before @@ -63,11 +78,11 @@ class PouchManager { addListeners() { if (!this.listenerLaunched) { if (isMobileApp()) { - document.addEventListener('pause', this.stopReplicationLoop) - document.addEventListener('resume', this.startReplicationLoop) + this.events.addEventListener('pause', this.stopReplicationLoop) + this.events.addEventListener('resume', this.startReplicationLoop) } - document.addEventListener('online', this.startReplicationLoop) - document.addEventListener('offline', this.stopReplicationLoop) + this.events.addEventListener('online', this.startReplicationLoop) + this.events.addEventListener('offline', this.stopReplicationLoop) this.listenerLaunched = true } } @@ -75,22 +90,22 @@ class PouchManager { removeListeners() { if (this.listenerLaunched) { if (isMobileApp()) { - document.removeEventListener('pause', this.stopReplicationLoop) - document.removeEventListener('resume', this.startReplicationLoop) + this.events.removeEventListener('pause', this.stopReplicationLoop) + this.events.removeEventListener('resume', this.startReplicationLoop) } - document.removeEventListener('online', this.startReplicationLoop) - document.removeEventListener('offline', this.stopReplicationLoop) + this.events.removeEventListener('online', this.startReplicationLoop) + this.events.removeEventListener('offline', this.stopReplicationLoop) this.listenerLaunched = false } } - destroy() { + async destroy() { this.stopReplicationLoop() this.removeListeners() - this.clearSyncedDoctypes() - this.clearWarmedUpQueries() - localStorage.destroyAllDoctypeLastSequence() - localStorage.destroyAllLastReplicatedDocID() + await this.clearSyncedDoctypes() + await this.clearWarmedUpQueries() + await this.storage.destroyAllDoctypeLastSequence() + await this.storage.destroyAllLastReplicatedDocID() return Promise.all( Object.values(this.pouches).map(pouch => pouch.destroy()) @@ -161,7 +176,7 @@ class PouchManager { /** Starts replication */ async replicateOnce() { - if (!window.navigator.onLine) { + if (!(await this.isOnline())) { logger.info( 'PouchManager: The device is offline so the replication has been skipped' ) @@ -189,9 +204,9 @@ class PouchManager { // Before the first replication, get the last remote sequence, // which will be used as a checkpoint for the next replication const lastSeq = await fetchRemoteLastSequence(getReplicationURL()) - localStorage.persistDoctypeLastSequence(doctype, lastSeq) + await this.storage.persistDoctypeLastSequence(doctype, lastSeq) } else { - seq = localStorage.getDoctypeLastSequence(doctype) + seq = await this.storage.getDoctypeLastSequence(doctype) } const replicationOptions = get( @@ -210,15 +225,16 @@ class PouchManager { const res = await startReplication( pouch, replicationOptions, - getReplicationURL + getReplicationURL, + this.storage ) if (seq) { // We only need the sequence for the second replication, as PouchDB // will use a local checkpoint for the next runs. - localStorage.destroyDoctypeLastSequence(doctype) + await this.storage.destroyDoctypeLastSequence(doctype) } - this.updateSyncInfo(doctype) + await this.updateSyncInfo(doctype) this.checkToWarmupDoctype(doctype, replicationOptions) if (this.options.onDoctypeSyncEnd) { this.options.onDoctypeSyncEnd(doctype) @@ -281,9 +297,9 @@ class PouchManager { return this.pouches[doctype] } - updateSyncInfo(doctype) { + async updateSyncInfo(doctype) { this.syncedDoctypes[doctype] = { date: new Date().toISOString() } - localStorage.persistSyncedDoctypes(this.syncedDoctypes) + await this.storage.persistSyncedDoctypes(this.syncedDoctypes) } getSyncInfo(doctype) { @@ -295,9 +311,9 @@ class PouchManager { return info ? !!info.date : false } - clearSyncedDoctypes() { + async clearSyncedDoctypes() { this.syncedDoctypes = {} - localStorage.destroySyncedDoctypes() + await this.storage.destroySyncedDoctypes() } async warmupQueries(doctype, queries) { @@ -312,7 +328,7 @@ class PouchManager { } }) ) - localStorage.persistWarmedUpQueries(this.warmedUpQueries) + await this.storage.persistWarmedUpQueries(this.warmedUpQueries) logger.log('PouchManager: warmupQueries for ' + doctype + ' are done') } catch (err) { logger.error( @@ -332,8 +348,8 @@ class PouchManager { } } - areQueriesWarmedUp(doctype, queries) { - const persistWarmedUpQueries = localStorage.getPersistedWarmedUpQueries() + async areQueriesWarmedUp(doctype, queries) { + const persistWarmedUpQueries = await this.storage.getPersistedWarmedUpQueries() return queries.every( query => persistWarmedUpQueries[doctype] && @@ -341,9 +357,9 @@ class PouchManager { ) } - clearWarmedUpQueries() { + async clearWarmedUpQueries() { this.warmedUpQueries = {} - localStorage.destroyWarmedUpQueries() + await this.storage.destroyWarmedUpQueries() } } diff --git a/packages/cozy-pouch-link/src/PouchManager.spec.js b/packages/cozy-pouch-link/src/PouchManager.spec.js index 0fa00a477d..6a5f7187b5 100644 --- a/packages/cozy-pouch-link/src/PouchManager.spec.js +++ b/packages/cozy-pouch-link/src/PouchManager.spec.js @@ -19,10 +19,17 @@ jest.mock('./remote', () => ({ import * as rep from './startReplication' import PouchDB from 'pouchdb-browser' -import * as ls from './localStorage' +import { + LOCALSTORAGE_SYNCED_KEY, + LOCALSTORAGE_WARMUPEDQUERIES_KEY, + PouchLocalStorage +} from './localStorage' +import { platformWeb } from './platformWeb' import { fetchRemoteLastSequence, fetchRemoteInstance } from './remote' +const ls = new PouchLocalStorage(platformWeb.storage) + const sleep = delay => { return new Promise(resolve => { setTimeout(resolve, delay) @@ -63,7 +70,7 @@ describe('PouchManager', () => { getReplicationURL, onSync = jest.fn() - beforeEach(() => { + beforeEach(async () => { getReplicationURL = () => 'http://replicationURL.local' managerOptions = { replicationDelay: 16, @@ -72,6 +79,7 @@ describe('PouchManager', () => { prefix: 'cozy.tools' } manager = new PouchManager(['io.cozy.todos'], managerOptions) + await manager.init() const pouch = manager.getPouch('io.cozy.todos') const replication = mocks.pouchReplication({ direction: 'pull', @@ -133,6 +141,7 @@ describe('PouchManager', () => { 'io.cozy.readonly': { strategy: 'fromRemote' } } }) + await manager.init() const normalPouch = manager.getPouch('io.cozy.todos') const readOnlyPouch = manager.getPouch('io.cozy.readonly') readOnlyPouch.replicate = {} @@ -155,6 +164,7 @@ describe('PouchManager', () => { } } ) + await manager.init() const normalPouch = manager.getPouch('io.cozy.todos') const readOnlyPouch = manager.getPouch('io.cozy.readonly') readOnlyPouch.replicate = {} @@ -231,14 +241,16 @@ describe('PouchManager', () => { it('should add pouch plugin', async () => { const options = { ...managerOptions, pouch: { plugins: ['myPlugin'] } } - new PouchManager(['io.cozy.todos'], options) + const manager = new PouchManager(['io.cozy.todos'], options) + await manager.init() expect(PouchDB.plugin).toHaveBeenCalledTimes(1) }) it('should instanciate pouch with options', async () => { const pouchOptions = { adapter: 'cordova-sqlite', location: 'default' } const options = { ...managerOptions, pouch: { options: pouchOptions } } - new PouchManager(['io.cozy.todos'], options) + const manager = new PouchManager(['io.cozy.todos'], options) + await manager.init() expect(PouchDB).toHaveBeenCalledWith( 'cozy.tools_io.cozy.todos', pouchOptions @@ -246,33 +258,36 @@ describe('PouchManager', () => { }) describe('getPersistedSyncedDoctypes', () => { - it('should return an empty array if local storage is empty', () => { - expect(ls.getPersistedSyncedDoctypes()).toEqual({}) + it('should return an empty array if local storage is empty', async () => { + expect(await ls.getPersistedSyncedDoctypes()).toEqual({}) }) - it('should return an empty array if local storage contains something that is not an array', () => { - localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY] = 'true' - expect(ls.getPersistedSyncedDoctypes()).toEqual({}) + it('should return an empty array if local storage contains something that is not an array', async () => { + localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY] = 'true' + expect(await ls.getPersistedSyncedDoctypes()).toEqual({}) }) - it('should return the list of doctypes if local storage contains one', () => { + it('should return the list of doctypes if local storage contains one', async () => { const persistedSyncedDoctypes = { 'io.cozy.todos': { date: '2021-08-11T13:48:06.085Z' } } - localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY] = JSON.stringify( + localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY] = JSON.stringify( + persistedSyncedDoctypes + ) + expect(await ls.getPersistedSyncedDoctypes()).toEqual( persistedSyncedDoctypes ) - expect(ls.getPersistedSyncedDoctypes()).toEqual(persistedSyncedDoctypes) }) }) describe('persistSyncedDoctypes', () => { - it('should put the list of synced doctypes in localStorage', () => { + it('should put the list of synced doctypes in localStorage', async () => { const manager = new PouchManager(['io.cozy.todos'], managerOptions) + await manager.init() manager.syncedDoctypes = ['io.cozy.todos'] ls.persistSyncedDoctypes(manager.syncedDoctypes) - expect(localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY]).toEqual( + expect(localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY]).toEqual( JSON.stringify(manager.syncedDoctypes) ) }) @@ -287,17 +302,19 @@ describe('PouchManager', () => { MockDate.reset() }) - it('should add the doctype to synced doctypes', () => { + it('should add the doctype to synced doctypes', async () => { const manager = new PouchManager(['io.cozy.todos'], managerOptions) - manager.updateSyncInfo('io.cozy.todos') + await manager.init() + await manager.updateSyncInfo('io.cozy.todos') expect(Object.keys(manager.syncedDoctypes)).toEqual(['io.cozy.todos']) }) - it('should persist the new synced doctypes list', () => { + it('should persist the new synced doctypes list', async () => { const manager = new PouchManager(['io.cozy.todos'], managerOptions) + await manager.init() - manager.updateSyncInfo('io.cozy.todos') - expect(localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY]).toEqual( + await manager.updateSyncInfo('io.cozy.todos') + expect(localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY]).toEqual( JSON.stringify({ 'io.cozy.todos': { date: '2021-08-01T00:00:00.000Z' } }) @@ -308,12 +325,13 @@ describe('PouchManager', () => { describe('isSynced', () => { let manager - beforeEach(() => { + beforeEach(async () => { manager = new PouchManager(['io.cozy.todos'], managerOptions) + await manager.init() }) - it('should return true if the doctype is synced', () => { - manager.updateSyncInfo('io.cozy.todos') + it('should return true if the doctype is synced', async () => { + await manager.updateSyncInfo('io.cozy.todos') expect(manager.isSynced('io.cozy.todos')).toBe(true) }) @@ -323,90 +341,92 @@ describe('PouchManager', () => { }) describe('destroySyncedDoctypes', () => { - it('should destroy the local storage item', () => { - ls.destroySyncedDoctypes() + it('should destroy the local storage item', async () => { + await ls.destroySyncedDoctypes() expect(localStorage.removeItem).toHaveBeenLastCalledWith( - ls.LOCALSTORAGE_SYNCED_KEY + LOCALSTORAGE_SYNCED_KEY ) }) - it('should reset syncedDoctypes', () => { + it('should reset syncedDoctypes', async () => { manager.syncedDoctypes = { 'io.cozy.todos': { date: '2021-08-11T13:48:06.085Z' } } - manager.clearSyncedDoctypes() + await manager.clearSyncedDoctypes() expect(manager.syncedDoctypes).toEqual({}) }) }) describe('getPersistedWarmedUpQueriess', () => { - it('should return an empty object if local storage is empty', () => { - expect(ls.getPersistedWarmedUpQueries()).toEqual({}) + it('should return an empty object if local storage is empty', async () => { + expect(await ls.getPersistedWarmedUpQueries()).toEqual({}) }) - it('should return the list of queries if local storage contains ones', () => { + it('should return the list of queries if local storage contains ones', async () => { const persistedQueries = [query().options.as] - localStorage.__STORE__[ - ls.LOCALSTORAGE_WARMUPEDQUERIES_KEY - ] = JSON.stringify(persistedQueries) - expect(ls.getPersistedWarmedUpQueries()).toEqual(persistedQueries) + localStorage.__STORE__[LOCALSTORAGE_WARMUPEDQUERIES_KEY] = JSON.stringify( + persistedQueries + ) + expect(await ls.getPersistedWarmedUpQueries()).toEqual(persistedQueries) }) }) describe('persistWarmedUpQueries', () => { - it('should put the list of warmedUpQueries in localStorage', () => { + it('should put the list of warmedUpQueries in localStorage', async () => { const manager = new PouchManager(['io.cozy.todos'], managerOptions) + await manager.init() manager.warmedUpQueries = { 'io.cozy.todos': ['query1', 'query2'] } - ls.persistWarmedUpQueries(manager.warmedUpQueries) + await ls.persistWarmedUpQueries(manager.warmedUpQueries) - expect( - localStorage.__STORE__[ls.LOCALSTORAGE_WARMUPEDQUERIES_KEY] - ).toEqual(JSON.stringify(manager.warmedUpQueries)) + expect(localStorage.__STORE__[LOCALSTORAGE_WARMUPEDQUERIES_KEY]).toEqual( + JSON.stringify(manager.warmedUpQueries) + ) }) }) describe('areQueriesWarmedUp', () => { let manager - beforeEach(() => { + beforeEach(async () => { manager = new PouchManager(['io.cozy.todos'], managerOptions) + await manager.init() }) - it('should return true if all the queries are warmuped', () => { + it('should return true if all the queries are warmuped', async () => { manager.warmedUpQueries = { 'io.cozy.todos': [query1().options.as, query2().options.as] } - ls.persistWarmedUpQueries(manager.warmedUpQueries) + await ls.persistWarmedUpQueries(manager.warmedUpQueries) expect( - manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()]) + await manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()]) ).toBe(true) }) - it('should return false if at least one query is not warmuped', () => { + it('should return false if at least one query is not warmuped', async () => { manager.warmedUpQueries = { 'io.cozy.todos': [query2().options.as] } - ls.persistWarmedUpQueries() + await ls.persistWarmedUpQueries() expect( - manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()]) + await manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()]) ).toBe(false) }) - it('should return false if the queries are not been done', () => { + it('should return false if the queries are not been done', async () => { expect( - manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()]) + await manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()]) ).toBe(false) }) }) describe('clearWarmedupQueries', () => { - it('should clear the local storage item', () => { + it('should clear the local storage item', async () => { manager.clearWarmedUpQueries() expect(localStorage.removeItem).toHaveBeenLastCalledWith( - ls.LOCALSTORAGE_WARMUPEDQUERIES_KEY + LOCALSTORAGE_WARMUPEDQUERIES_KEY ) }) it('should reset warmedupQueries', () => { @@ -490,7 +510,7 @@ describe('PouchManager', () => { describe('warmupQueries', () => { let manager const executeMock = jest.fn() - beforeEach(() => { + beforeEach(async () => { let newManagerOptions = { ...managerOptions, executeQuery: executeMock, @@ -502,6 +522,7 @@ describe('PouchManager', () => { } } manager = new PouchManager(['io.cozy.todos'], newManagerOptions) + await manager.init() }) it('should executes warmeupQueries on the first replicationLoop only', async () => { @@ -524,7 +545,7 @@ describe('PouchManager', () => { .definition() .toDefinition() ) - expect(ls.getPersistedWarmedUpQueries()).toEqual({ + expect(await ls.getPersistedWarmedUpQueries()).toEqual({ 'io.cozy.todos': ['query1', 'query2'] }) //Simulation of a loop. Let's replicate again @@ -541,7 +562,7 @@ describe('PouchManager', () => { await manager.replicateOnce() await sleep(10) - expect(ls.getPersistedWarmedUpQueries()).toEqual({}) + expect(await ls.getPersistedWarmedUpQueries()).toEqual({}) expect(manager.warmedUpQueries['io.cozy.todos']).toBeUndefined() }) }) diff --git a/packages/cozy-pouch-link/src/localStorage.js b/packages/cozy-pouch-link/src/localStorage.js index ad05e48d6a..923f45553c 100644 --- a/packages/cozy-pouch-link/src/localStorage.js +++ b/packages/cozy-pouch-link/src/localStorage.js @@ -7,182 +7,216 @@ export const LOCALSTORAGE_LASTREPLICATEDDOCID_KEY = 'cozy-client-pouch-link-lastreplicateddocid' export const LOCALSTORAGE_ADAPTERNAME = 'cozy-client-pouch-link-adaptername' -/** - * Persist the last replicated doc id for a doctype - * - * @param {string} doctype - The replicated doctype - * @param {string} id - The docid - */ -export const persistLastReplicatedDocID = (doctype, id) => { - const docids = getAllLastReplicatedDocID() - docids[doctype] = id - - window.localStorage.setItem( - LOCALSTORAGE_LASTREPLICATEDDOCID_KEY, - JSON.stringify(docids) - ) -} +export class PouchLocalStorage { + constructor(storageEngine) { + this.storageEngine = storageEngine + } -export const getAllLastReplicatedDocID = () => { - const item = window.localStorage.getItem(LOCALSTORAGE_LASTREPLICATEDDOCID_KEY) - return item ? JSON.parse(item) : {} -} + /** + * Persist the last replicated doc id for a doctype + * + * @param {string} doctype - The replicated doctype + * @param {string} id - The docid + * + * @returns {Promise} + */ + async persistLastReplicatedDocID(doctype, id) { + const docids = await this.getAllLastReplicatedDocID() + docids[doctype] = id + + await this.storageEngine.setItem( + LOCALSTORAGE_LASTREPLICATEDDOCID_KEY, + JSON.stringify(docids) + ) + } -/** - * Get the last replicated doc id for a doctype - * - * @param {string} doctype - The doctype - * @returns {string} The last replicated docid - */ -export const getLastReplicatedDocID = doctype => { - const docids = getAllLastSequences() - return docids[doctype] -} + /** + * @returns {Promise>} + */ + async getAllLastReplicatedDocID() { + const item = await this.storageEngine.getItem( + LOCALSTORAGE_LASTREPLICATEDDOCID_KEY + ) + return item ? JSON.parse(item) : {} + } -/** - * Destroy all the replicated doc id - */ -export const destroyAllLastReplicatedDocID = () => { - window.localStorage.removeItem(LOCALSTORAGE_LASTREPLICATEDDOCID_KEY) -} + /** + * Get the last replicated doc id for a doctype + * + * @param {string} doctype - The doctype + * @returns {Promise} The last replicated docid + */ + async getLastReplicatedDocID(doctype) { + const docids = await this.getAllLastSequences() + return docids[doctype] + } -/** - * Persist the synchronized doctypes - * - * @typedef {object} SyncInfo - * @property {string} Date - * - * @param {Record} syncedDoctypes - The sync doctypes - */ -export const persistSyncedDoctypes = syncedDoctypes => { - window.localStorage.setItem( - LOCALSTORAGE_SYNCED_KEY, - JSON.stringify(syncedDoctypes) - ) -} + /** + * Destroy all the replicated doc id + * + * @returns {Promise} + */ + async destroyAllLastReplicatedDocID() { + await this.storageEngine.removeItem(LOCALSTORAGE_LASTREPLICATEDDOCID_KEY) + } -/** - * Get the persisted doctypes - * - * @returns {object} The synced doctypes - */ -export const getPersistedSyncedDoctypes = () => { - const item = window.localStorage.getItem(LOCALSTORAGE_SYNCED_KEY) - const parsed = item ? JSON.parse(item) : {} - if (typeof parsed !== 'object') { - return {} - } - return parsed -} + /** + * Persist the synchronized doctypes + * + * @param {Record} syncedDoctypes - The sync doctypes + * + * @returns {Promise} + */ + async persistSyncedDoctypes(syncedDoctypes) { + await this.storageEngine.setItem( + LOCALSTORAGE_SYNCED_KEY, + JSON.stringify(syncedDoctypes) + ) + } -/** - * Destroy the synced doctypes - * - */ -export const destroySyncedDoctypes = () => { - window.localStorage.removeItem(LOCALSTORAGE_SYNCED_KEY) -} + /** + * Get the persisted doctypes + * + * @returns {Promise} The synced doctypes + */ + async getPersistedSyncedDoctypes() { + const item = await this.storageEngine.getItem(LOCALSTORAGE_SYNCED_KEY) + const parsed = item ? JSON.parse(item) : {} + if (typeof parsed !== 'object') { + return {} + } + return parsed + } -/** - * Persist the last CouchDB sequence for a synced doctype - * - * @param {string} doctype - The synced doctype - * @param {string} sequence - The sequence hash - */ -export const persistDoctypeLastSequence = (doctype, sequence) => { - const seqs = getAllLastSequences() - seqs[doctype] = sequence - - window.localStorage.setItem( - LOCALSTORAGE_LASTSEQUENCES_KEY, - JSON.stringify(seqs) - ) -} + /** + * Destroy the synced doctypes + * + * @returns {Promise} + */ + async destroySyncedDoctypes() { + await this.storageEngine.removeItem(LOCALSTORAGE_SYNCED_KEY) + } -export const getAllLastSequences = () => { - const item = window.localStorage.getItem(LOCALSTORAGE_LASTSEQUENCES_KEY) - return item ? JSON.parse(item) : {} -} + /** + * Persist the last CouchDB sequence for a synced doctype + * + * @param {string} doctype - The synced doctype + * @param {string} sequence - The sequence hash + * + * @returns {Promise} + */ + async persistDoctypeLastSequence(doctype, sequence) { + const seqs = await this.getAllLastSequences() + seqs[doctype] = sequence + + await this.storageEngine.setItem( + LOCALSTORAGE_LASTSEQUENCES_KEY, + JSON.stringify(seqs) + ) + } -/** - * Get the last CouchDB sequence for a doctype - * - * @param {string} doctype - The doctype - * @returns {string} the last sequence - */ -export const getDoctypeLastSequence = doctype => { - const seqs = getAllLastSequences() - return seqs[doctype] -} + /** + * @returns {Promise} + */ + async getAllLastSequences() { + const item = await this.storageEngine.getItem( + LOCALSTORAGE_LASTSEQUENCES_KEY + ) + return item ? JSON.parse(item) : {} + } -/** - * Destroy all the last sequence - */ -export const destroyAllDoctypeLastSequence = () => { - window.localStorage.removeItem(LOCALSTORAGE_LASTSEQUENCES_KEY) -} + /** + * Get the last CouchDB sequence for a doctype + * + * @param {string} doctype - The doctype + * + * @returns {Promise} the last sequence + */ + async getDoctypeLastSequence(doctype) { + const seqs = await this.getAllLastSequences() + return seqs[doctype] + } -/** - * Destroy the last sequence for a doctype - * - * @param {string} doctype - The doctype - */ -export const destroyDoctypeLastSequence = doctype => { - const seqs = getAllLastSequences() - delete seqs[doctype] - window.localStorage.setItem( - LOCALSTORAGE_LASTSEQUENCES_KEY, - JSON.stringify(seqs) - ) -} + /** + * Destroy all the last sequence + * + * @returns {Promise} + */ + async destroyAllDoctypeLastSequence() { + await this.storageEngine.removeItem(LOCALSTORAGE_LASTSEQUENCES_KEY) + } -/** - * Persist the warmed up queries - * - * @param {object} warmedUpQueries - The warmedup queries - */ -export const persistWarmedUpQueries = warmedUpQueries => { - window.localStorage.setItem( - LOCALSTORAGE_WARMUPEDQUERIES_KEY, - JSON.stringify(warmedUpQueries) - ) -} + /** + * Destroy the last sequence for a doctype + * + * @param {string} doctype - The doctype + * + * @returns {Promise} + */ + async destroyDoctypeLastSequence(doctype) { + const seqs = await this.getAllLastSequences() + delete seqs[doctype] + await this.storageEngine.setItem( + LOCALSTORAGE_LASTSEQUENCES_KEY, + JSON.stringify(seqs) + ) + } -/** - * Get the warmed up queries - * - * @returns {object} the warmed up queries - */ -export const getPersistedWarmedUpQueries = () => { - const item = window.localStorage.getItem(LOCALSTORAGE_WARMUPEDQUERIES_KEY) - if (!item) { - return {} - } - return JSON.parse(item) -} + /** + * Persist the warmed up queries + * + * @param {object} warmedUpQueries - The warmedup queries + * + * @returns {Promise} + */ + async persistWarmedUpQueries(warmedUpQueries) { + await this.storageEngine.setItem( + LOCALSTORAGE_WARMUPEDQUERIES_KEY, + JSON.stringify(warmedUpQueries) + ) + } -/** - * Destroy the warmed queries - * - */ -export const destroyWarmedUpQueries = () => { - window.localStorage.removeItem(LOCALSTORAGE_WARMUPEDQUERIES_KEY) -} + /** + * Get the warmed up queries + * + * @returns {Promise} the warmed up queries + */ + async getPersistedWarmedUpQueries() { + const item = await this.storageEngine.getItem( + LOCALSTORAGE_WARMUPEDQUERIES_KEY + ) + if (!item) { + return {} + } + return JSON.parse(item) + } -/** - * Get the adapter name - * - * @returns {string} The adapter name - */ -export const getAdapterName = () => { - return window.localStorage.getItem(LOCALSTORAGE_ADAPTERNAME) -} + /** + * Destroy the warmed queries + * + * @returns {Promise} + */ + async destroyWarmedUpQueries() { + await this.storageEngine.removeItem(LOCALSTORAGE_WARMUPEDQUERIES_KEY) + } + + /** + * Get the adapter name + * + * @returns {Promise} The adapter name + */ + async getAdapterName() { + return await this.storageEngine.getItem(LOCALSTORAGE_ADAPTERNAME) + } -/** - * Persist the adapter name - * - * @param {string} adapter - The adapter name - */ -export const persistAdapterName = adapter => { - window.localStorage.setItem(LOCALSTORAGE_ADAPTERNAME, adapter) + /** + * Persist the adapter name + * + * @param {string} adapter - The adapter name + * + * @returns {Promise} + */ + async persistAdapterName(adapter) { + await this.storageEngine.setItem(LOCALSTORAGE_ADAPTERNAME, adapter) + } } diff --git a/packages/cozy-pouch-link/src/platformWeb.js b/packages/cozy-pouch-link/src/platformWeb.js new file mode 100644 index 0000000000..9f96d78c38 --- /dev/null +++ b/packages/cozy-pouch-link/src/platformWeb.js @@ -0,0 +1,33 @@ +import PouchDB from 'pouchdb-browser' + +const events = { + addEventListener: (eventName, handler) => { + document.addEventListener(eventName, handler) + }, + removeEventListener: (eventName, handler) => { + document.removeEventListener(eventName, handler) + } +} + +const storage = { + getItem: async key => { + return window.localStorage.getItem(key) + }, + setItem: async (key, value) => { + return window.localStorage.setItem(key, value) + }, + removeItem: async key => { + return window.localStorage.removeItem(key) + } +} + +const isOnline = async () => { + return window.navigator.onLine +} + +export const platformWeb = { + storage, + events, + pouchAdapter: PouchDB, + isOnline +} diff --git a/packages/cozy-pouch-link/src/startReplication.js b/packages/cozy-pouch-link/src/startReplication.js index 4ff5d581af..7774f8b13d 100644 --- a/packages/cozy-pouch-link/src/startReplication.js +++ b/packages/cozy-pouch-link/src/startReplication.js @@ -2,10 +2,7 @@ import { default as helpers } from './helpers' import startsWith from 'lodash/startsWith' import logger from './logger' import { fetchRemoteInstance } from './remote' -import { - getLastReplicatedDocID, - persistLastReplicatedDocID -} from './localStorage' + const { isDesignDocument, isDeletedDocument } = helpers const BATCH_SIZE = 1000 // we have mostly small documents @@ -40,13 +37,15 @@ const TIME_UNITS = [['ms', 1000], ['s', 60], ['m', 60], ['h', 24]] * @param {string} replicationOptions.doctype The doctype to replicate * @param {import('cozy-client/types/types').Query[]} replicationOptions.warmupQueries The queries to warmup * @param {Function} getReplicationURL A function that should return the remote replication URL + * @param {import('./localStorage').PouchLocalStorage} storage Methods to access local storage * * @returns {import('./types').CancelablePromise} A cancelable promise that resolves at the end of the replication */ export const startReplication = ( pouch, replicationOptions, - getReplicationURL + getReplicationURL, + storage ) => { let replication let docs = {} @@ -71,7 +70,7 @@ export const startReplication = ( // For the first remote->local replication, we manually replicate all docs // as it avoids to replicate all revs history, which can lead to // performances issues - docs = await replicateAllDocs(pouch, url, doctype) + docs = await replicateAllDocs(pouch, url, doctype, storage) const end = new Date() if (process.env.NODE_ENV !== 'production') { logger.info( @@ -144,13 +143,14 @@ const filterDocs = docs => { * @param {object} db - Pouch instance * @param {string} baseUrl - The remote instance * @param {string} doctype - The doctype to replicate + * @param {import('./localStorage').PouchLocalStorage} storage - Methods to access local storage * @returns {Promise} The retrieved documents */ -export const replicateAllDocs = async (db, baseUrl, doctype) => { +export const replicateAllDocs = async (db, baseUrl, doctype, storage) => { const remoteUrlAllDocs = new URL(`${baseUrl}/_all_docs`) const batchSize = BATCH_SIZE let hasMore = true - let startDocId = getLastReplicatedDocID(doctype) // Get last replicated _id in localStorage + let startDocId = await storage.getLastReplicatedDocID(doctype) // Get last replicated _id in localStorage let docs = [] while (hasMore) { @@ -169,7 +169,7 @@ export const replicateAllDocs = async (db, baseUrl, doctype) => { hasMore = false } await helpers.insertBulkDocs(db, docs) - persistLastReplicatedDocID(doctype, startDocId) + await storage.persistLastReplicatedDocID(doctype, startDocId) } } else { const res = await fetchRemoteInstance(remoteUrlAllDocs, { @@ -184,7 +184,7 @@ export const replicateAllDocs = async (db, baseUrl, doctype) => { filteredDocs.shift() // Remove first element, already included in previous request startDocId = filteredDocs[filteredDocs.length - 1]._id await helpers.insertBulkDocs(db, filteredDocs) - persistLastReplicatedDocID(doctype, startDocId) + await storage.persistLastReplicatedDocID(doctype, startDocId) docs = docs.concat(filteredDocs) if (res.rows.length < batchSize) { diff --git a/packages/cozy-pouch-link/src/startReplication.spec.js b/packages/cozy-pouch-link/src/startReplication.spec.js index 41acd9a503..874aea53af 100644 --- a/packages/cozy-pouch-link/src/startReplication.spec.js +++ b/packages/cozy-pouch-link/src/startReplication.spec.js @@ -1,13 +1,7 @@ import { fetchRemoteLastSequence, fetchRemoteInstance } from './remote' -import { getLastReplicatedDocID } from './localStorage' import { replicateAllDocs } from './startReplication' -jest.mock('./localStorage', () => ({ - getLastReplicatedDocID: jest.fn(), - persistLastReplicatedDocID: jest.fn() -})) - jest.mock('./remote', () => ({ fetchRemoteLastSequence: jest.fn(), fetchRemoteInstance: jest.fn() @@ -27,22 +21,27 @@ const generateDocs = nDocs => { return docs } +const storage = { + getLastReplicatedDocID: jest.fn(), + persistLastReplicatedDocID: jest.fn() +} + describe('replication through _all_docs', () => { beforeEach(() => { fetchRemoteLastSequence.mockResolvedValue('10-xyz') }) it('should replicate all docs', async () => { - getLastReplicatedDocID.mockReturnValue(null) + storage.getLastReplicatedDocID.mockReturnValue(null) const dummyDocs = generateDocs(2) fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs }) - const rep = await replicateAllDocs(null, url) + const rep = await replicateAllDocs(null, url, undefined, storage) const expectedDocs = dummyDocs.map(doc => doc.doc) expect(rep).toEqual(expectedDocs) }) it('should replicate all docs when it gets more docs than the batch limit', async () => { - getLastReplicatedDocID.mockReturnValue(null) + storage.getLastReplicatedDocID.mockReturnValue(null) const dummyDocs = generateDocs(1002) fetchRemoteInstance.mockResolvedValueOnce({ rows: dummyDocs.slice(0, 1001) @@ -51,17 +50,17 @@ describe('replication through _all_docs', () => { rows: dummyDocs.slice(1000, 1002) }) - const rep = await replicateAllDocs(null, url) + const rep = await replicateAllDocs(null, url, undefined, storage) const expectedDocs = dummyDocs.map(doc => doc.doc) expect(rep).toEqual(expectedDocs) }) it('should replicate from the last saved doc id', async () => { - getLastReplicatedDocID.mockReturnValue('5') + storage.getLastReplicatedDocID.mockReturnValue('5') const dummyDocs = generateDocs(10) fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs.slice(5, 11) }) - const rep = await replicateAllDocs(null, url) + const rep = await replicateAllDocs(null, url, undefined, storage) const calledUrl = new URL(`${url}/_all_docs`) expect(fetchRemoteInstance).toHaveBeenCalledWith(calledUrl, { diff --git a/packages/cozy-pouch-link/src/types.js b/packages/cozy-pouch-link/src/types.js index 86fc108772..87f8d17963 100644 --- a/packages/cozy-pouch-link/src/types.js +++ b/packages/cozy-pouch-link/src/types.js @@ -11,4 +11,22 @@ * @typedef {CancelablePromise[] & Cancelable} CancelablePromises */ +/** @typedef {object} SyncInfo + * @property {string} Date + */ + +/** + * @typedef {object} LocalStorage + * @property {function(string): Promise} getItem + * @property {function(string, string): Promise} setItem + * @property {function(string): Promise} removeItem + */ + +/** + * @typedef {object} LinkPlatform + * @property {LocalStorage} storage Methods to access local storage + * @property {any} pouchAdapter PouchDB class (can be pouchdb-core or pouchdb-browser) + * @property {function(): Promise} isOnline Method that check if the app is connected to internet + */ + export default {} diff --git a/packages/cozy-pouch-link/types/CozyPouchLink.d.ts b/packages/cozy-pouch-link/types/CozyPouchLink.d.ts index 2bbfe5e6cc..127c172b69 100644 --- a/packages/cozy-pouch-link/types/CozyPouchLink.d.ts +++ b/packages/cozy-pouch-link/types/CozyPouchLink.d.ts @@ -15,9 +15,10 @@ declare class PouchLink extends CozyLink { * Return the PouchDB adapter name. * Should be IndexedDB for newest adapters. * - * @returns {string} The adapter name + * @param {import('./types').LocalStorage} localStorage Methods to access local storage + * @returns {Promise} The adapter name */ - static getPouchAdapterName: () => string; + static getPouchAdapterName: (localStorage: import('./types').LocalStorage) => Promise; /** * constructor - Initializes a new PouchLink * @@ -25,11 +26,13 @@ declare class PouchLink extends CozyLink { * @param {number} [opts.replicationInterval] Milliseconds between replications * @param {string[]} opts.doctypes Doctypes to replicate * @param {object[]} opts.doctypesReplicationOptions A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote") + * @param {import('./types').LinkPlatform} opts.platform Platform specific adapters and methods */ constructor(opts?: { replicationInterval: number; doctypes: string[]; doctypesReplicationOptions: object[]; + platform: import('./types').LinkPlatform; }); options: { replicationInterval: number; @@ -37,10 +40,12 @@ declare class PouchLink extends CozyLink { replicationInterval?: number; doctypes: string[]; doctypesReplicationOptions: object[]; + platform: import('./types').LinkPlatform; }; doctypes: string[]; doctypesReplicationOptions: any[]; indexes: {}; + storage: PouchLocalStorage; /** @type {Record} - Stores replication states per doctype */ replicationStatus: Record; getReplicationURL(doctype: any): string; @@ -118,9 +123,9 @@ declare class PouchLink extends CozyLink { * and return if those queries are already warmed up or not * * @param {string} doctype - Doctype to check - * @returns {boolean} the need to wait for the warmup + * @returns {Promise} the need to wait for the warmup */ - needsToWaitWarmup(doctype: string): boolean; + needsToWaitWarmup(doctype: string): Promise; hasIndex(name: any): boolean; mergePartialIndexInSelector(selector: any, partialFilter: any): any; ensureIndex(doctype: any, query: any): Promise; @@ -158,4 +163,5 @@ declare class PouchLink extends CozyLink { syncImmediately(): Promise; } import { CozyLink } from "cozy-client"; +import { PouchLocalStorage } from "./localStorage"; import PouchManager from "./PouchManager"; diff --git a/packages/cozy-pouch-link/types/PouchManager.d.ts b/packages/cozy-pouch-link/types/PouchManager.d.ts index a3919c36a1..cbb07a7373 100644 --- a/packages/cozy-pouch-link/types/PouchManager.d.ts +++ b/packages/cozy-pouch-link/types/PouchManager.d.ts @@ -8,6 +8,12 @@ export default PouchManager; declare class PouchManager { constructor(doctypes: any, options: any); options: any; + doctypes: any; + storage: PouchLocalStorage; + PouchDB: any; + isOnline: any; + events: any; + init(): Promise; pouches: import("lodash").Dictionary; syncedDoctypes: any; warmedUpQueries: any; @@ -52,13 +58,14 @@ declare class PouchManager { cancelCurrentReplications(): void; waitForCurrentReplications(): Promise | Promise; getPouch(doctype: any): any; - updateSyncInfo(doctype: any): void; + updateSyncInfo(doctype: any): Promise; getSyncInfo(doctype: any): any; isSynced(doctype: any): boolean; - clearSyncedDoctypes(): void; + clearSyncedDoctypes(): Promise; warmupQueries(doctype: any, queries: any): Promise; checkToWarmupDoctype(doctype: any, replicationOptions: any): void; - areQueriesWarmedUp(doctype: any, queries: any): any; - clearWarmedUpQueries(): void; + areQueriesWarmedUp(doctype: any, queries: any): Promise; + clearWarmedUpQueries(): Promise; } +import { PouchLocalStorage } from "./localStorage"; import Loop from "./loop"; diff --git a/packages/cozy-pouch-link/types/localStorage.d.ts b/packages/cozy-pouch-link/types/localStorage.d.ts index b3295b9c4c..c60f0f36be 100644 --- a/packages/cozy-pouch-link/types/localStorage.d.ts +++ b/packages/cozy-pouch-link/types/localStorage.d.ts @@ -3,26 +3,122 @@ export const LOCALSTORAGE_WARMUPEDQUERIES_KEY: "cozy-client-pouch-link-warmupedq export const LOCALSTORAGE_LASTSEQUENCES_KEY: "cozy-client-pouch-link-lastreplicationsequence"; export const LOCALSTORAGE_LASTREPLICATEDDOCID_KEY: "cozy-client-pouch-link-lastreplicateddocid"; export const LOCALSTORAGE_ADAPTERNAME: "cozy-client-pouch-link-adaptername"; -export function persistLastReplicatedDocID(doctype: string, id: string): void; -export function getAllLastReplicatedDocID(): any; -export function getLastReplicatedDocID(doctype: string): string; -export function destroyAllLastReplicatedDocID(): void; -export function persistSyncedDoctypes(syncedDoctypes: Record): void; -export function getPersistedSyncedDoctypes(): object; -export function destroySyncedDoctypes(): void; -export function persistDoctypeLastSequence(doctype: string, sequence: string): void; -export function getAllLastSequences(): any; -export function getDoctypeLastSequence(doctype: string): string; -export function destroyAllDoctypeLastSequence(): void; -export function destroyDoctypeLastSequence(doctype: string): void; -export function persistWarmedUpQueries(warmedUpQueries: object): void; -export function getPersistedWarmedUpQueries(): object; -export function destroyWarmedUpQueries(): void; -export function getAdapterName(): string; -export function persistAdapterName(adapter: string): void; -/** - * Persist the synchronized doctypes - */ -export type SyncInfo = { - Date: string; -}; +export class PouchLocalStorage { + constructor(storageEngine: any); + storageEngine: any; + /** + * Persist the last replicated doc id for a doctype + * + * @param {string} doctype - The replicated doctype + * @param {string} id - The docid + * + * @returns {Promise} + */ + persistLastReplicatedDocID(doctype: string, id: string): Promise; + /** + * @returns {Promise>} + */ + getAllLastReplicatedDocID(): Promise>; + /** + * Get the last replicated doc id for a doctype + * + * @param {string} doctype - The doctype + * @returns {Promise} The last replicated docid + */ + getLastReplicatedDocID(doctype: string): Promise; + /** + * Destroy all the replicated doc id + * + * @returns {Promise} + */ + destroyAllLastReplicatedDocID(): Promise; + /** + * Persist the synchronized doctypes + * + * @param {Record} syncedDoctypes - The sync doctypes + * + * @returns {Promise} + */ + persistSyncedDoctypes(syncedDoctypes: Record): Promise; + /** + * Get the persisted doctypes + * + * @returns {Promise} The synced doctypes + */ + getPersistedSyncedDoctypes(): Promise; + /** + * Destroy the synced doctypes + * + * @returns {Promise} + */ + destroySyncedDoctypes(): Promise; + /** + * Persist the last CouchDB sequence for a synced doctype + * + * @param {string} doctype - The synced doctype + * @param {string} sequence - The sequence hash + * + * @returns {Promise} + */ + persistDoctypeLastSequence(doctype: string, sequence: string): Promise; + /** + * @returns {Promise} + */ + getAllLastSequences(): Promise; + /** + * Get the last CouchDB sequence for a doctype + * + * @param {string} doctype - The doctype + * + * @returns {Promise} the last sequence + */ + getDoctypeLastSequence(doctype: string): Promise; + /** + * Destroy all the last sequence + * + * @returns {Promise} + */ + destroyAllDoctypeLastSequence(): Promise; + /** + * Destroy the last sequence for a doctype + * + * @param {string} doctype - The doctype + * + * @returns {Promise} + */ + destroyDoctypeLastSequence(doctype: string): Promise; + /** + * Persist the warmed up queries + * + * @param {object} warmedUpQueries - The warmedup queries + * + * @returns {Promise} + */ + persistWarmedUpQueries(warmedUpQueries: object): Promise; + /** + * Get the warmed up queries + * + * @returns {Promise} the warmed up queries + */ + getPersistedWarmedUpQueries(): Promise; + /** + * Destroy the warmed queries + * + * @returns {Promise} + */ + destroyWarmedUpQueries(): Promise; + /** + * Get the adapter name + * + * @returns {Promise} The adapter name + */ + getAdapterName(): Promise; + /** + * Persist the adapter name + * + * @param {string} adapter - The adapter name + * + * @returns {Promise} + */ + persistAdapterName(adapter: string): Promise; +} diff --git a/packages/cozy-pouch-link/types/platformWeb.d.ts b/packages/cozy-pouch-link/types/platformWeb.d.ts new file mode 100644 index 0000000000..9b8b8fdf71 --- /dev/null +++ b/packages/cozy-pouch-link/types/platformWeb.d.ts @@ -0,0 +1,17 @@ +export namespace platformWeb { + export { storage }; + export { events }; + export { PouchDB as pouchAdapter }; + export { isOnline }; +} +declare namespace storage { + function getItem(key: any): Promise; + function setItem(key: any, value: any): Promise; + function removeItem(key: any): Promise; +} +declare namespace events { + function addEventListener(eventName: any, handler: any): void; + function removeEventListener(eventName: any, handler: any): void; +} +declare function isOnline(): Promise; +export {}; diff --git a/packages/cozy-pouch-link/types/startReplication.d.ts b/packages/cozy-pouch-link/types/startReplication.d.ts index c820925f87..44ff8eb07b 100644 --- a/packages/cozy-pouch-link/types/startReplication.d.ts +++ b/packages/cozy-pouch-link/types/startReplication.d.ts @@ -3,5 +3,5 @@ export function startReplication(pouch: object, replicationOptions: { initialReplication: boolean; doctype: string; warmupQueries: import('cozy-client/types/types').Query[]; -}, getReplicationURL: Function): import('./types').CancelablePromise; -export function replicateAllDocs(db: object, baseUrl: string, doctype: string): Promise; +}, getReplicationURL: Function, storage: import('./localStorage').PouchLocalStorage): import('./types').CancelablePromise; +export function replicateAllDocs(db: object, baseUrl: string, doctype: string, storage: import('./localStorage').PouchLocalStorage): Promise; diff --git a/packages/cozy-pouch-link/types/types.d.ts b/packages/cozy-pouch-link/types/types.d.ts index d38702d605..2cb24b393c 100644 --- a/packages/cozy-pouch-link/types/types.d.ts +++ b/packages/cozy-pouch-link/types/types.d.ts @@ -8,3 +8,25 @@ export type Cancelable = { }; export type CancelablePromise = Promise & Cancelable; export type CancelablePromises = CancelablePromise[] & Cancelable; +export type SyncInfo = { + Date: string; +}; +export type LocalStorage = { + getItem: (arg0: string) => Promise; + setItem: (arg0: string, arg1: string) => Promise; + removeItem: (arg0: string) => Promise; +}; +export type LinkPlatform = { + /** + * Methods to access local storage + */ + storage: LocalStorage; + /** + * PouchDB class (can be pouchdb-core or pouchdb-browser) + */ + pouchAdapter: any; + /** + * Method that check if the app is connected to internet + */ + isOnline: () => Promise; +};