UNPKG

git-documentdb

Version:

Offline-first database that syncs with Git

1,064 lines 43.8 kB
"use strict"; /** * GitDocumentDB * Copyright (c) Hidekazu Kubota * * This source code is licensed under the Mozilla Public License Version 2.0 * found in the LICENSE file in the root directory of this source tree. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Sync = exports.syncImpl = exports.syncAndGetResultImpl = exports.encodeToGitRemoteName = void 0; /** * ! Must import both clearInterval and setInterval from 'timers' */ const timers_1 = require("timers"); const crypto_1 = __importDefault(require("crypto")); const isomorphic_git_1 = __importDefault(require("isomorphic-git")); const fs_extra_1 = __importDefault(require("fs-extra")); const remote_isomorphic_git_1 = require("../plugin/remote-isomorphic-git"); const utils_1 = require("../utils"); const error_1 = require("../error"); const sync_worker_1 = require("./sync_worker"); const push_worker_1 = require("./push_worker"); const remote_repository_1 = require("./remote_repository"); const const_1 = require("../const"); const json_diff_1 = require("./json_diff"); const json_patch_ot_1 = require("./json_patch_ot"); const combine_1 = require("./combine"); const validator_1 = require("../validator"); const remote_engine_1 = require("./remote_engine"); /** * encodeToGitRemoteName * * @remarks * The first default name of Git remote is "origin". * * GitDocumentDB adds an alias of "origin", * whose name is generated automatically by this function. * The second and subsequent remotes are also named in the same way. * * A remote name consists of [remote address]_[hash]. * Periods are replaced with underscores. * e.g.) github_com_a0b1c23 * It is human-readable. * * [remote address] is [hostname + domain name] or [ip address]. * [hash] is calculated from remoteURL. * * [hash] is the first seven characters of SHA-1 so that it may collide. * Capitalize one of the remote addresses when hashes collide * because a hostname and a domain name are not case sensitive. * * @throws {@link RemoteErr.InvalidURLFormatError} * * @public */ function encodeToGitRemoteName(remoteURL) { let host; if (/:\/\/.+?@(.+?):/.test(remoteURL)) { // ssh://user@foo.bar:xxx/path/repos.git host = RegExp.$1; } else if (/:\/\/(.+?):/.test(remoteURL)) { // http://foo.bar:xxx/path/repos.git host = RegExp.$1; } else if (/:\/\/.+?@(.+?)\//.test(remoteURL)) { // ssh://user@foo.bar/path/repos.git host = RegExp.$1; } else if (/:\/\/(.+?)\//.test(remoteURL)) { // http://foo.bar/user/repos.git host = RegExp.$1; } else if (/^.+?@(.+?):/.test(remoteURL)) { // user@foo.bar:path/repos.git host = RegExp.$1; } else { throw new remote_engine_1.RemoteErr.InvalidURLFormatError(`URL format is invalid: ${remoteURL}`); } const shortHash = crypto_1.default.createHash('sha1').update(remoteURL).digest('hex').substr(0, 7); // Use toLowerCase() because git.setConfig() and git.addRemote() automatically converts the path to lowercase. return host.toLowerCase().replace(/\./g, '_') + '_' + shortHash; } exports.encodeToGitRemoteName = encodeToGitRemoteName; /** * Implementation of GitDocumentDB#sync(options, get_sync_result) * * @throws {@link Err.DatabaseClosingError} * @throws {@link Err.RepositoryNotOpenError} * * @throws # Errors from constructor of {@link Sync} class. * @throws # Errors from {@link Sync.init} * * @internal */ async function syncAndGetResultImpl(options) { if (this.isClosing) { throw new error_1.Err.DatabaseClosingError(); } if (!this.isOpened) { return Promise.reject(new error_1.Err.RepositoryNotOpenError()); } const sync = new Sync(this, options); const syncResult = await sync.init(); return [sync, syncResult]; } exports.syncAndGetResultImpl = syncAndGetResultImpl; /** * Implementation of GitDocumentDB#sync(options) * * @throws {@link Err.DatabaseClosingError} * @throws {@link Err.RepositoryNotOpenError} * * @throws # Errors from constructor of {@link Sync} class. * @throws # Errors from {@link Sync.init} * * @internal */ async function syncImpl(options) { const [sync, syncResult] = await syncAndGetResultImpl.call(this, options); return sync; } exports.syncImpl = syncImpl; /** * Filter file changes by collectionPath * * @internal */ function filterChanges(syncResult, collectionPath) { if (collectionPath === '') { return syncResult; } const filter = (changedFiles) => { // eslint-disable-next-line complexity return changedFiles.reduce((result, changedFile) => { let oldFatDoc; let newFatDoc; if (changedFile.operation === 'delete' || changedFile.operation === 'update') { oldFatDoc = changedFile.old; } if (changedFile.operation === 'insert' || changedFile.operation === 'update') { // insert newFatDoc = changedFile.new; } if ((oldFatDoc && oldFatDoc.name.startsWith(collectionPath)) || (newFatDoc && newFatDoc.name.startsWith(collectionPath))) { if (oldFatDoc) { oldFatDoc.name = oldFatDoc.name.replace(new RegExp('^' + collectionPath), ''); if (oldFatDoc.type === 'json') { oldFatDoc._id = oldFatDoc._id.replace(new RegExp('^' + collectionPath), ''); oldFatDoc.doc._id = oldFatDoc._id; } } if (newFatDoc) { newFatDoc.name = newFatDoc.name.replace(new RegExp('^' + collectionPath), ''); if (newFatDoc.type === 'json') { newFatDoc._id = newFatDoc._id.replace(new RegExp('^' + collectionPath), ''); newFatDoc.doc._id = newFatDoc._id; } } result.push(changedFile); } return result; }, []); }; if (syncResult.action === 'resolve conflicts and push' || syncResult.action === 'merge and push' || syncResult.action === 'resolve conflicts and push error' || syncResult.action === 'merge and push error' || syncResult.action === 'fast-forward merge') { syncResult.changes.local = filter(syncResult.changes.local); } if (syncResult.action === 'resolve conflicts and push' || syncResult.action === 'merge and push' || syncResult.action === 'push') { syncResult.changes.remote = filter(syncResult.changes.remote); } return syncResult; } /** * Synchronizer class * * @public */ class Sync { /** * constructor * * @throws {@link Err.UndefinedRemoteURLError} * @throws {@link Err.IntervalTooSmallError} * @throws {@link Err.SyncIntervalLessThanOrEqualToRetryIntervalError} * * @throws # Errors from encodeToGitRemoteName * @throws - {@link RemoteErr.InvalidURLFormatError} * * @public */ // eslint-disable-next-line complexity constructor(gitDDB, options) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; var _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; this._retrySyncCounter = 0; // Decremental count this._isClosed = false; /** * Remote Engine */ this._engine = remote_isomorphic_git_1.name; this._remoteName = ''; /*********************************************** * Public properties ***********************************************/ /** * runBeforeLiveSync * * This function is executed just before each automated(live) synchronization event is queued. * Set undefined to stop it. */ this.runBeforeLiveSync = undefined; /** * SyncEvent handlers * * @internal */ this.eventHandlers = { change: [], localChange: [], remoteChange: [], combine: [], pause: [], resume: [], start: [], complete: [], error: [], }; this._gitDDB = gitDDB; options !== null && options !== void 0 ? options : (options = { remoteUrl: undefined, live: undefined, syncDirection: undefined, interval: undefined, retry: undefined, retryInterval: undefined, connection: undefined, combineDbStrategy: undefined, includeCommits: undefined, conflictResolutionStrategy: undefined, }); // Deep clone this._options = JSON.parse(JSON.stringify(options)); // Set function again this._options.conflictResolutionStrategy = options.conflictResolutionStrategy; if (this._options.remoteUrl === undefined || this._options.remoteUrl === '') { throw new error_1.Err.UndefinedRemoteURLError(); } (_a = (_l = this._options).live) !== null && _a !== void 0 ? _a : (_l.live = false); (_b = (_m = this._options).syncDirection) !== null && _b !== void 0 ? _b : (_m.syncDirection = 'both'); (_c = (_o = this._options).interval) !== null && _c !== void 0 ? _c : (_o.interval = const_1.DEFAULT_SYNC_INTERVAL); (_d = (_p = this._options).retryInterval) !== null && _d !== void 0 ? _d : (_p.retryInterval = const_1.NETWORK_RETRY_INTERVAL); if (this._options.interval < const_1.MINIMUM_SYNC_INTERVAL) { throw new error_1.Err.IntervalTooSmallError(const_1.MINIMUM_SYNC_INTERVAL, this._options.interval); } if (this._options.interval <= this._options.retryInterval) { throw new error_1.Err.SyncIntervalLessThanOrEqualToRetryIntervalError(this._options.interval, this._options.retryInterval); } (_e = (_q = this._options).retry) !== null && _e !== void 0 ? _e : (_q.retry = const_1.NETWORK_RETRY); (_f = (_r = this._options).combineDbStrategy) !== null && _f !== void 0 ? _f : (_r.combineDbStrategy = const_1.DEFAULT_COMBINE_DB_STRATEGY); (_g = (_s = this._options).includeCommits) !== null && _g !== void 0 ? _g : (_s.includeCommits = false); (_h = (_t = this._options).conflictResolutionStrategy) !== null && _h !== void 0 ? _h : (_t.conflictResolutionStrategy = const_1.DEFAULT_CONFLICT_RESOLUTION_STRATEGY); this.jsonDiff = new json_diff_1.JsonDiff(gitDDB.schema.json); this.jsonPatch = new json_patch_ot_1.JsonPatchOT(gitDDB.schema.json); (_j = (_u = this._options).connection) !== null && _j !== void 0 ? _j : (_u.connection = { type: 'none' }); (_k = (_v = this._options.connection).engine) !== null && _k !== void 0 ? _k : (_v.engine = remote_isomorphic_git_1.name); this._remoteRepository = new remote_repository_1.RemoteRepository({ remoteUrl: this._options.remoteUrl, connection: this._options.connection, }); this._remoteName = encodeToGitRemoteName(this.remoteURL); this._engine = this._options.connection.engine; } /*********************************************** * Public properties (readonly) ***********************************************/ /** * remoteURL * * @readonly * @public */ get remoteURL() { return this._options.remoteUrl; } get engine() { return this._engine; } /** * Remote repository * * @readonly * @public */ get remoteRepository() { return this._remoteRepository; } /** * Get a clone of remote options * * @readonly * @public */ get options() { const newOptions = JSON.parse(JSON.stringify(this._options)); // options include function. newOptions.conflictResolutionStrategy = this._options.conflictResolutionStrategy; return newOptions; } /** * remoteName * * @readonly * @public */ get remoteName() { return this._remoteName; } /*********************************************** * Public methods ***********************************************/ /** * Initialize remote connection * * @remarks * Call init() once just after creating an instance. * * @throws {@link Err.CannotCreateRemoteRepositoryError} * * @throws # Errors from RemoteEngine[engineName].checkFetch * @throws - {@link RemoteErr.InvalidGitRemoteError} * @throws - {@link RemoteErr.InvalidURLFormatError} * @throws - {@link RemoteErr.NetworkError} * @throws - {@link RemoteErr.HTTPError401AuthorizationRequired} * @throws - {@link RemoteErr.HTTPError404NotFound} * @throws - {@link RemoteErr.CannotConnectError} * @throws - {@link RemoteErr.InvalidURLFormatError} * @throws - {@link RemoteErr.InvalidRepositoryURLError} * @throws - {@link RemoteErr.InvalidSSHKeyPathError} * @throws - {@link RemoteErr.InvalidAuthenticationTypeError} * * @throws Errors from {@link Sync.trySync} * @throws Errors from {@link Sync.tryPush} * * @public */ // eslint-disable-next-line complexity async init() { this._isClosed = false; let isNewRemoteRepository = false; const urlOfRemote = await isomorphic_git_1.default.getConfig({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, path: `remote.${this.remoteName}.url`, }); if (urlOfRemote !== this.remoteURL) { await isomorphic_git_1.default.addRemote({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, remote: this.remoteName, url: this.remoteURL, }); } /** * Set origin if not exist */ const originUrl = await isomorphic_git_1.default.getConfig({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, path: `remote.origin.url`, }); if (originUrl === undefined) { await isomorphic_git_1.default.addRemote({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, remote: 'origin', url: this.remoteURL, }); } let remoteResult; if (this._options.syncDirection === 'push') { // checkFetch will return undefined if succeeds. // Do not download remote. // eslint-disable-next-line no-await-in-loop remoteResult = await remote_engine_1.RemoteEngine[this._engine] .checkFetch(this._gitDDB.workingDir, this._options, this.remoteName, this._gitDDB.tsLogger) .catch(err => err); } else { // fetch will return true if succeeds. // eslint-disable-next-line no-await-in-loop remoteResult = await remote_engine_1.RemoteEngine[this._engine] .fetch(this._gitDDB.workingDir, this._options, this.remoteName, this._gitDDB.defaultBranch, this._gitDDB.defaultBranch, this._gitDDB.tsLogger) .catch(err => err); } /** * Do not use 'instanceof' to compare git-documentdb-remote-errors * because an error from RemoteEngine plugin may not have the same prototype * in its prototype chain. * - https://nodejs.org/en/blog/npm/peer-dependencies/ * - https://stackoverflow.com/questions/46618852/require-and-instanceof/46630766 * Use name property instead. */ if (typeof remoteResult === 'boolean' || remoteResult === undefined) { // nop } else if (remoteResult.name === 'InvalidGitRemoteError') { // checkFetch hardly invoke this error because checkFetch is called just after addRemote. throw (0, remote_engine_1.wrappingRemoteEngineError)(remoteResult); } else if (remoteResult.name === 'InvalidURLFormatError' || remoteResult.name === 'InvalidRepositoryURLError' || remoteResult.name === 'InvalidSSHKeyPathError' || remoteResult.name === 'InvalidAuthenticationTypeError' || remoteResult.name === 'HTTPError401AuthorizationRequired' || remoteResult.name === 'NetworkError' || remoteResult.name === 'CannotConnectError') { throw (0, remote_engine_1.wrappingRemoteEngineError)(remoteResult); } else if (remoteResult.name === 'HTTPError404NotFound') { // Try to create repository by octokit // eslint-disable-next-line no-await-in-loop await this.remoteRepository.create().catch(err => { throw new error_1.Err.CannotCreateRemoteRepositoryError(err.message); }); isNewRemoteRepository = true; } let syncResult = { action: 'nop', }; if (this._options.syncDirection === 'pull') { // Do not create a new remote repository because the direction is 'pull'. /** * TODO: Implement case when sync_direction is 'pull'. */ } else { // push or both if (this._options.syncDirection === 'both') { // Check remote branch after fetching. const remoteCommitOid = await isomorphic_git_1.default .resolveRef({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, ref: `refs/remotes/${this.remoteName}/${this._gitDDB.defaultBranch}`, }) .catch(() => undefined); if (remoteCommitOid === undefined) { // Remote repository is empty. isNewRemoteRepository = true; } } if (isNewRemoteRepository) { this._gitDDB.logger.debug('upstream branch is not set yet. tryPush..'); // Remote repository may not be created yet due to internal delay of GitHub. // Retry if not exist. for (let i = 0; i < this._options.retry + 1; i++) { // eslint-disable-next-line no-await-in-loop const syncResultOrError = await this.tryPush().catch(err => err); if (syncResultOrError instanceof Error) { if (syncResultOrError instanceof remote_engine_1.RemoteErr.HTTPError404NotFound) { // eslint-disable-next-line no-await-in-loop await (0, utils_1.sleep)(this._options.retryInterval); if (i === this._options.retry) { throw syncResultOrError; } continue; } throw syncResultOrError; } syncResult = syncResultOrError; break; } } else if (this._options.syncDirection === 'push') { this._gitDDB.logger.debug('upstream_branch exists. tryPush..'); syncResult = await this.tryPush(); } else if (this._options.syncDirection === 'both') { this._gitDDB.logger.debug('upstream_branch exists. trySync..'); syncResult = await this.trySync(); } } const branchRemote = await isomorphic_git_1.default.getConfig({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, path: `branch.${this._gitDDB.defaultBranch}.remote`, }); if (branchRemote === undefined) { await isomorphic_git_1.default.setConfig({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, path: `branch.${this._gitDDB.defaultBranch}.remote`, value: this.remoteName, }); } const branchMerge = await isomorphic_git_1.default.getConfig({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, path: `branch.${this._gitDDB.defaultBranch}.merge`, }); if (branchMerge === undefined) { await isomorphic_git_1.default.setConfig({ fs: fs_extra_1.default, dir: this._gitDDB.workingDir, path: `branch.${this._gitDDB.defaultBranch}.merge`, value: `refs/heads/${this._gitDDB.defaultBranch}`, }); } if (this._options.live) { if (this._syncTimer === undefined) { this.eventHandlers.resume.forEach(listener => { listener.func(); }); this._syncTimer = (0, timers_1.setInterval)(() => { if (this.runBeforeLiveSync !== undefined) { try { this.runBeforeLiveSync(); } catch (e) { this._gitDDB.logger.debug('Error in runBeforeLiveSync: ' + e); } } if (this._options.syncDirection === 'push') { this.tryPushImpl(true).catch(() => undefined); } else if (this._options.syncDirection === 'both') { this.trySyncImpl(true).catch(() => undefined); } }, this._options.interval); } } return syncResult; } /** * Pause synchronization * * @public */ pause() { if (!this._options.live) return false; // Cancel retrying this._retrySyncCounter = 0; if (this._syncTimer) { (0, timers_1.clearInterval)(this._syncTimer); } this._options.live = false; this.eventHandlers.pause.forEach(listener => { listener.func(); }); return true; } /** * Resume synchronization * * @remarks * Give new settings if needed. * * @throws {@link Err.IntervalTooSmallError} * * @public */ resume(options) { if (this._isClosed) return false; if (this._options.live) return false; options !== null && options !== void 0 ? options : (options = { interval: undefined, retry: undefined, }); if (options.interval !== undefined) { if (options.interval >= const_1.MINIMUM_SYNC_INTERVAL) { this._options.interval = options.interval; } else { throw new error_1.Err.IntervalTooSmallError(const_1.MINIMUM_SYNC_INTERVAL, options.interval); } } if (options.retry !== undefined) { this._options.retry = options.retry; } this._options.live = true; this._syncTimer = (0, timers_1.setInterval)(() => { if (this._options.syncDirection === 'push') { this.tryPushImpl(true).catch(() => undefined); } else if (this._options.syncDirection === 'both') { this.trySyncImpl(true).catch(() => undefined); } }, this._options.interval); this.eventHandlers.resume.forEach(listener => { listener.func(); }); return true; } /** * Stop and clear remote connection * * @public */ close() { this._isClosed = true; this.pause(); this.eventHandlers = { change: [], localChange: [], remoteChange: [], combine: [], pause: [], resume: [], start: [], complete: [], error: [], }; } /** * Try to push * * @throws {@link Err.PushNotAllowedError} * * @throws # Errors from push * @throws - {@link RemoteErr.InvalidGitRemoteError} * @throws - {@link RemoteErr.UnfetchedCommitExistsError} * @throws - {@link RemoteErr.InvalidURLFormatError} * @throws - {@link RemoteErr.NetworkError} * @throws - {@link RemoteErr.HTTPError401AuthorizationRequired} * @throws - {@link RemoteErr.HTTPError404NotFound} * @throws - {@link RemoteErr.HTTPError403Forbidden} * @throws - {@link RemoteErr.CannotConnectError} * @throws - {@link RemoteErr.UnfetchedCommitExistsError} * @throws - {@link RemoteErr.CannotConnectError} * @throws - {@link RemoteErr.InvalidURLFormatError} * @throws - {@link RemoteErr.InvalidRepositoryURLError} * @throws - {@link RemoteErr.InvalidSSHKeyPathError} * @throws - {@link RemoteErr.InvalidAuthenticationTypeError} * * @throws # Errors from getChanges * @throws - {@link Err.InvalidJsonObjectError} * * @public */ async tryPush() { return await this.tryPushImpl(false); } /** * tryPushImpl * * @internal */ // eslint-disable-next-line complexity async tryPushImpl(calledAsPeriodicTask) { if (this._isClosed) return { action: 'canceled' }; if (this._options.syncDirection === 'pull') { throw new error_1.Err.PushNotAllowedError(this._options.syncDirection); } /** * Enqueue pushWorker */ const taskId = this._gitDDB.taskQueue.newTaskId(); const callback = (resolve, reject) => (beforeResolve, beforeReject, taskMetadata) => { if (calledAsPeriodicTask && !this._options.live) { return Promise.resolve().then(() => { const resultCancel = { action: 'canceled', }; beforeResolve(); resolve(resultCancel); }); } return (0, push_worker_1.pushWorker)(this._gitDDB, this, taskMetadata) .then((syncResultPush) => { this._gitDDB.logger.debug(`pushWorker: ${JSON.stringify(syncResultPush)}`, utils_1.CONSOLE_STYLE.bgWhite().fgBlack().tag); if (syncResultPush.action === 'push') { this.eventHandlers.change.forEach(listener => { const filteredSyncResultPush = filterChanges(JSON.parse(JSON.stringify(syncResultPush)), listener.collectionPath); listener.func(filteredSyncResultPush, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); this.eventHandlers.remoteChange.forEach(listener => { const filteredSyncResultPush = filterChanges(JSON.parse(JSON.stringify(syncResultPush)), listener.collectionPath); listener.func(filteredSyncResultPush.changes.remote, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); } if (syncResultPush.action === 'push') { this.eventHandlers.complete.forEach(listener => { listener.func({ ...taskMetadata, collectionPath: listener.collectionPath }); }); } beforeResolve(); resolve(syncResultPush); }) .catch(err => { // console.log(`Error in push_worker: ${err}`); this.eventHandlers.error.forEach(listener => { listener.func(err, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); beforeReject(); reject(err); }); }; const cancel = (resolve) => () => { const result = { action: 'canceled' }; this._gitDDB.logger.debug(`pushWorker: ${JSON.stringify(result)}`, utils_1.CONSOLE_STYLE.bgWhite().fgBlack().tag); resolve(result); }; const task = (resolve, reject) => { return { label: 'push', taskId: taskId, syncRemoteName: this.remoteName, func: callback(resolve, reject), cancel: cancel(resolve), }; }; const resultOrError = await new Promise((resolve, reject) => { this._gitDDB.taskQueue.pushToTaskQueue(task(resolve, reject)); // this._gitDDB.taskQueue.unshiftSyncTaskToTaskQueue(task(resolve, reject)); }).catch((err) => err); if (resultOrError instanceof Error) { if (resultOrError instanceof remote_engine_1.RemoteErr.UnfetchedCommitExistsError) { if (this._options.syncDirection === 'push') { if (this._options.combineDbStrategy === 'replace-with-ours') { // TODO: Exec replace-with-ours instead of throw error } } } // Fatal error. Don't retry? // this.pause(); throw resultOrError; } return resultOrError; } /** * Try to sync with retries * * @throws {@link Err.PushNotAllowedError} * @throws {@link Err.CombineDatabaseError} * * @throws # Errors from syncWorker * @throws - {@link Err.NoMergeBaseFoundError} * @throws - {@link Err.ThreeWayMergeError} * @throws - {@link Err.CannotDeleteDataError} * * @throws # Errors from fetch, pushWorker * @throws - {@link RemoteErr.InvalidGitRemoteError} * @throws - {@link RemoteErr.InvalidURLFormatError} * @throws - {@link RemoteErr.NetworkError} * @throws - {@link RemoteErr.HTTPError401AuthorizationRequired} * @throws - {@link RemoteErr.HTTPError404NotFound} * @throws - {@link RemoteErr.CannotConnectError} * @throws - {@link RemoteErr.InvalidURLFormatError} * @throws - {@link RemoteErr.InvalidRepositoryURLError} * @throws - {@link RemoteErr.InvalidSSHKeyPathError} * @throws - {@link RemoteErr.InvalidAuthenticationTypeError} * * @throws # Errors from pushWorker * @throws - {@link RemoteErr.HTTPError403Forbidden} * @throws - {@link RemoteErr.UnfetchedCommitExistsError} * * @throws # Errors from merge * @throws - {@link Err.InvalidConflictStateError} * @throws - {@link Err.CannotDeleteDataError} * @throws - {@link Err.InvalidDocTypeError} * @throws - {@link Err.InvalidConflictResolutionStrategyError} * @throws - {@link Err.CannotCreateDirectoryError} * @throws - {@link Err.InvalidJsonObjectError} * * @throws # Errors from getChanges * @throws - {@link Err.InvalidJsonObjectError} * * @public */ async trySync() { return await this.trySyncImpl(false); } /** * trySyncImpl * * @internal */ // eslint-disable-next-line complexity async trySyncImpl(calledAsPeriodicTask) { if (this._isClosed) return { action: 'canceled' }; if (this._options.syncDirection === 'pull') { throw new error_1.Err.PushNotAllowedError(this._options.syncDirection); } if (this._retrySyncCounter === 0) { this._retrySyncCounter = this._options.retry + 1; } while (this._retrySyncCounter > 0) { // eslint-disable-next-line no-await-in-loop const resultOrError = await this.enqueueSyncTask(calledAsPeriodicTask).catch((err) => err); let error; let result; if (resultOrError instanceof Error) { error = resultOrError; } else if (resultOrError.action === 'merge and push error' || resultOrError.action === 'resolve conflicts and push error') { result = resultOrError; error = resultOrError.error; } else { result = resultOrError; } if (error instanceof error_1.Err.NoMergeBaseFoundError) { if (this._options.combineDbStrategy === 'throw-error') { // nop } else if (this._options.combineDbStrategy === 'combine-head-with-theirs') { // return SyncResultCombineDatabase // eslint-disable-next-line no-await-in-loop const syncResultCombineDatabase = await (0, combine_1.combineDatabaseWithTheirs)(this._gitDDB, this._options, this.remoteName).catch(err => { if (err) // throw new Err.CombineDatabaseError(err.message); error = new error_1.Err.CombineDatabaseError(err.message); return undefined; }); if (syncResultCombineDatabase !== undefined) { // eslint-disable-next-line no-loop-func this.eventHandlers.combine.forEach(callback => callback.func(syncResultCombineDatabase.duplicates)); return syncResultCombineDatabase; } } } if (error !== undefined) { this._gitDDB.logger.debug('trySync failed: ' + error.message); if (error instanceof remote_engine_1.RemoteErr.UnfetchedCommitExistsError) { this._retrySyncCounter--; if (this._retrySyncCounter === 0) { // this.pause(); throw error; } this._gitDDB.logger.debug(`...retrySync: ${this.currentRetries().toString()}`, utils_1.CONSOLE_STYLE.bgRed().tag); // eslint-disable-next-line no-await-in-loop await (0, utils_1.sleep)(this._options.retryInterval); } else { // Throw error this._retrySyncCounter = 0; // this.pause(); throw error; } } else if (result !== undefined) { // No error this._retrySyncCounter = 0; return result; } } // This line is reached when cancel() set _retrySyncCounter to 0; const cancel = { action: 'canceled' }; this._gitDDB.logger.debug(`syncWorker: ${JSON.stringify(cancel)}`, utils_1.CONSOLE_STYLE.bgWhite().fgBlack().tag); this._retrySyncCounter = 0; return cancel; } /** * Enqueue sync task to TaskQueue * * @public */ enqueueSyncTask(calledAsPeriodicTask) { const taskId = this._gitDDB.taskQueue.newTaskId(); const callback = (resolve, reject) => (beforeResolve, beforeReject, taskMetadata) => { if (calledAsPeriodicTask && !this._options.live) { return Promise.resolve().then(() => { const resultCancel = { action: 'canceled', }; beforeResolve(); resolve(resultCancel); }); } return ((0, sync_worker_1.syncWorker)(this._gitDDB, this, taskMetadata) // eslint-disable-next-line complexity .then((syncResult) => { this._gitDDB.logger.debug(`syncWorker: ${JSON.stringify(syncResult)}`, utils_1.CONSOLE_STYLE.bgWhite().fgBlack().tag); if (syncResult.action === 'resolve conflicts and push' || syncResult.action === 'merge and push' || syncResult.action === 'resolve conflicts and push error' || syncResult.action === 'merge and push error' || syncResult.action === 'fast-forward merge' || syncResult.action === 'push') { this.eventHandlers.change.forEach(listener => { let syncResultForChangeEvent = JSON.parse(JSON.stringify(syncResult)); syncResultForChangeEvent = filterChanges(syncResultForChangeEvent, listener.collectionPath); listener.func(syncResultForChangeEvent, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); } if (syncResult.action === 'resolve conflicts and push' || syncResult.action === 'merge and push' || syncResult.action === 'resolve conflicts and push error' || syncResult.action === 'merge and push error' || syncResult.action === 'fast-forward merge') { this.eventHandlers.localChange.forEach(listener => { let syncResultForLocalChangeEvent = JSON.parse(JSON.stringify(syncResult)); syncResultForLocalChangeEvent = filterChanges(syncResultForLocalChangeEvent, listener.collectionPath); listener.func(syncResultForLocalChangeEvent.changes.local, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); } if (syncResult.action === 'resolve conflicts and push' || syncResult.action === 'merge and push' || syncResult.action === 'push') { this.eventHandlers.remoteChange.forEach(listener => { let syncResultForRemoteChangeEvent = JSON.parse(JSON.stringify(syncResult)); syncResultForRemoteChangeEvent = filterChanges(syncResultForRemoteChangeEvent, listener.collectionPath); listener.func(syncResultForRemoteChangeEvent.changes.remote, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); } this.eventHandlers.complete.forEach(listener => listener.func({ ...taskMetadata, collectionPath: listener.collectionPath, })); beforeResolve(); resolve(syncResult); }) .catch((err) => { // console.log(`Error in syncWorker: ${err}`); this.eventHandlers.error.forEach(listener => { listener.func(err, { ...taskMetadata, collectionPath: listener.collectionPath, }); }); beforeReject(); reject(err); })); }; const cancel = (resolve) => () => { const result = { action: 'canceled' }; this._gitDDB.logger.debug(`syncWorker: ${JSON.stringify(result)}`, utils_1.CONSOLE_STYLE.bgWhite().fgBlack().tag); resolve(result); }; const task = (resolve, reject) => { return { label: 'sync', taskId: taskId, syncRemoteName: this.remoteName, func: callback(resolve, reject), cancel: cancel(resolve), }; }; return new Promise((resolve, reject) => { this._gitDDB.taskQueue.pushToTaskQueue(task(resolve, reject)); }); } /** * Return current retry count (incremental) * * @public */ currentRetries() { let retries = this._options.retry - this._retrySyncCounter + 1; if (this._retrySyncCounter === 0) retries = 0; return retries; } /** * Add SyncEvent handler * * @eventProperty * @public */ // eslint-disable-next-line complexity on(event, callback, collectionPath = '') { if (this._isClosed) return this; collectionPath = validator_1.Validator.normalizeCollectionPath(collectionPath); if (event === 'change') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'localChange') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'remoteChange') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'combine') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'pause') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'resume') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'start') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'complete') this.eventHandlers[event].push({ collectionPath, func: callback, }); if (event === 'error') this.eventHandlers[event].push({ collectionPath, func: callback, }); return this; } /** * Remove SyncEvent handler * * @eventProperty * @public */ off(event, callback) { if (this._isClosed) return this; // @ts-ignore this.eventHandlers[event] = this.eventHandlers[event].filter((listener) => { return listener.func !== callback; }); return this; } } exports.Sync = Sync; //# sourceMappingURL=sync.js.map