core/remote/change.js
/** A remote change to be send to Prep/Merge.
*
* @module core/remote/change
* @flow
*/
/*::
import type { RemoteDoc, CouchDBDeletion } from './document'
import type { Metadata, SavedMetadata } from '../metadata'
*/
const path = require('path')
const metadata = require('../metadata')
/*::
export type RemoteFileAddition = {
sideName: 'remote',
type: 'FileAddition',
doc: Metadata
}
export type RemoteFileDeletion = {
sideName: 'remote',
type: 'FileDeletion',
doc: SavedMetadata
}
export type RemoteFileMove = {
sideName: 'remote',
type: 'FileMove',
doc: Metadata,
was: SavedMetadata,
needRefetch?: true,
update?: boolean
}
export type RemoteFileRestoration = {
sideName: 'remote',
type: 'FileRestoration',
doc: Metadata,
was: Metadata
}
export type RemoteFileTrashing = {
sideName: 'remote',
type: 'FileTrashing',
doc: Metadata,
was: SavedMetadata
}
export type RemoteFileUpdate = {
sideName: 'remote',
type: 'FileUpdate',
doc: Metadata
}
export type RemoteDirAddition = {
sideName: 'remote',
type: 'DirAddition',
doc: Metadata
}
export type RemoteDirDeletion = {
sideName: 'remote',
type: 'DirDeletion',
doc: SavedMetadata
}
export type RemoteDirMove = {
sideName: 'remote',
type: 'DirMove',
doc: Metadata,
was: SavedMetadata,
needRefetch?: true,
descendantMoves: RemoteDescendantChange[]
}
export type RemoteDirRestoration = {
sideName: 'remote',
type: 'DirRestoration',
doc: Metadata,
was: Metadata
}
export type RemoteDirTrashing = {
sideName: 'remote',
type: 'DirTrashing',
doc: Metadata,
was: SavedMetadata
}
export type RemoteDirUpdate = {
sideName: 'remote',
type: 'DirUpdate',
doc: Metadata
}
export type RemoteIgnoredChange = {
sideName: 'remote',
type: 'IgnoredChange',
doc: *,
was?: SavedMetadata,
detail: string
}
export type RemoteInvalidChange = {
sideName: 'remote',
type: 'InvalidChange',
doc: *,
was?: SavedMetadata,
error: Error
}
export type RemoteUpToDate = {
sideName: 'remote',
type: 'UpToDate',
doc: Metadata,
was: SavedMetadata
}
export type RemoteDescendantChange = {
sideName: 'remote',
type: 'DescendantChange',
doc: Metadata,
was: SavedMetadata,
ancestor: RemoteDirMove|RemoteDescendantChange,
descendantMoves: RemoteDescendantChange[],
update?: boolean
}
export type RemoteChange =
| RemoteDirAddition
| RemoteDirDeletion
| RemoteDirMove
| RemoteDirRestoration
| RemoteDirTrashing
| RemoteDirUpdate
| RemoteFileAddition
| RemoteFileDeletion
| RemoteFileMove
| RemoteFileRestoration
| RemoteFileTrashing
| RemoteFileUpdate
| RemoteIgnoredChange
| RemoteDescendantChange
| RemoteInvalidChange
| RemoteUpToDate
*/
module.exports = {
added,
trashed,
deleted,
upToDate,
updated,
isChildSource,
isChildDestination,
isChildMove,
isOnlyChildMove,
includeDescendant,
applyMoveInsideMove,
sort,
sortByPath
}
const sideName = 'remote'
// FIXME: return types
function added(
doc /*: Metadata */
) /*: RemoteFileAddition | RemoteDirAddition */ {
if (metadata.isFile(doc)) {
return {
sideName,
type: 'FileAddition',
doc
}
} else {
return {
sideName,
type: 'DirAddition',
doc
}
}
}
function trashed(
doc /*: Metadata */,
was /*: SavedMetadata */
) /*: RemoteFileTrashing | RemoteDirTrashing */ {
if (metadata.isFile(doc)) {
return {
sideName,
type: 'FileTrashing',
doc,
was
}
} else {
return {
sideName,
type: 'DirTrashing',
doc,
was
}
}
}
function deleted(
doc /*: SavedMetadata */
) /*: RemoteFileDeletion | RemoteDirDeletion */ {
if (metadata.isFile(doc)) {
return {
sideName,
type: 'FileDeletion',
doc
}
} else {
return {
sideName,
type: 'DirDeletion',
doc
}
}
}
function upToDate(
doc /*: Metadata */,
was /*: SavedMetadata */
) /*: RemoteUpToDate */ {
return { sideName, type: 'UpToDate', doc, was }
}
function updated(
doc /*: Metadata */
) /*: RemoteFileUpdate | RemoteDirUpdate */ {
if (metadata.isFile(doc)) {
return {
sideName,
type: 'FileUpdate',
doc
}
} else {
return {
sideName,
type: 'DirUpdate',
doc
}
}
}
function isChildMove(
p /*: RemoteChange */,
c /*: RemoteChange */
) /*: boolean %checks */ {
return (
isFolderMove(p) &&
(c.type === 'DirMove' || c.type === 'FileMove') &&
(isChildDestination(p, c) || isChildSource(p, c))
)
}
function isChildDestination(
p /*: RemoteChange */,
c /*: RemoteChange */
) /*: boolean %checks */ {
return (
isFolderMove(p) &&
isMove(c) &&
metadata.samePath(path.dirname(c.doc.path), p.doc.path)
)
}
function isChildSource(
p /*: RemoteChange */,
c /*: RemoteChange */
) /*: boolean %checks */ {
return (
isFolderMove(p) &&
isMove(c) &&
!!p.was &&
!!c.was &&
metadata.samePath(path.dirname(c.was.path), p.was.path)
)
}
/**
* was doc
* p /p -> /p2
* c /p/c -> /p2/c
*/
function isOnlyChildMove(
p /*: RemoteChange */,
c /*: RemoteChange */
) /*: boolean %checks */ {
return (
isChildSource(p, c) &&
isChildDestination(p, c) &&
metadata.samePath(path.basename(c.doc.path), path.basename(c.was.path))
)
}
function applyMoveInsideMove(
parentMove /*: RemoteDirMove|RemoteDescendantChange */,
childMove /*: RemoteDirMove | RemoteFileMove */
) {
childMove.needRefetch = true
childMove.was.path = metadata.newChildPath(
childMove.was.path,
parentMove.was.path,
parentMove.doc.path
)
}
const isDelete = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'DirDeletion' || a.type === 'FileDeletion'
const isAdd = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'DirAddition' || a.type === 'FileAddition'
const isDescendant = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'DescendantChange'
const isMove = (a /*: RemoteChange */) /*: boolean %checks */ =>
isFileMove(a) || isFolderMove(a)
const isFileMove = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'FileMove' || (isDescendant(a) && a.doc.docType === metadata.FILE)
const isFolderMove = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'DirMove' || (isDescendant(a) && a.doc.docType === metadata.FOLDER)
const isTrash = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'DirTrashing' || a.type === 'FileTrashing'
const isRestore = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'DirRestoration' || a.type === 'FileRestoration'
const isIgnore = (a /*: RemoteChange */) /*: boolean %checks */ =>
a.type === 'IgnoredChange'
function includeDescendant(
parent /*: RemoteDirMove|RemoteDescendantChange */,
e /*: RemoteDescendantChange */
) {
if (isDescendant(parent) && parent.ancestor) {
includeDescendant(parent.ancestor, e)
} else {
parent.descendantMoves.push(e, ...e.descendantMoves)
}
e.descendantMoves = []
}
const createdPath = (a /*: RemoteChange */) /*: ?string */ =>
isAdd(a) || isMove(a) || isRestore(a) ? a.doc.path : null
const createdId = (a /*: RemoteChange */) /*: ?string */ =>
isAdd(a) || isMove(a) || isRestore(a) ? metadata.id(a.doc.path) : null
const deletedPath = (a /*: RemoteChange */) /*: ?string */ =>
isDelete(a) ? a.doc.path : isMove(a) || isTrash(a) ? a.was.path : null
const deletedId = (a /*: RemoteChange */) /*: ?string */ =>
isDelete(a)
? metadata.id(a.doc.path)
: isMove(a) || isTrash(a)
? metadata.id(a.was.path)
: null
const ignoredPath = (a /*: RemoteChange */) /*: ?string */ =>
isIgnore(a) && typeof a.doc.path === 'string' ? a.doc.path : null
const areParentChild = (p /*: ?string */, c /*: ?string */) /*: boolean */ =>
!!p && !!c && metadata.areParentChildPaths(p, c)
const areEqual = (a /*: ?string */, b /*: ?string */) /*: boolean */ =>
!!a && !!b && a === b
const lower = (p1 /*: ?string */, p2 /*: ?string */) /*: boolean */ =>
!!p1 && !!p2 && p1 < p2
const aFirst = -1
const bFirst = 1
const sortForDelete = (del, b, delFirst) => {
if (isDelete(b) || isTrash(b)) {
if (lower(deletedPath(del), deletedPath(b))) return delFirst
if (lower(deletedPath(b), deletedPath(del))) return -delFirst
return 0
}
if (isMove(b) && areParentChild(deletedPath(del), deletedPath(b)))
return -delFirst
return delFirst
}
const sortForDescendant = (desc, b, descFirst) => {
if (areParentChild(deletedPath(desc), createdPath(b))) return descFirst
if (areParentChild(deletedPath(b), createdPath(desc))) return -descFirst
if (areParentChild(createdPath(b), deletedPath(desc))) return descFirst
if (areParentChild(createdPath(desc), deletedPath(b))) return -descFirst
if (areEqual(deletedId(desc), createdId(b))) return descFirst
if (areEqual(deletedId(b), createdId(desc))) return descFirst
return -descFirst
}
const sortForMove = (move, b, moveFirst) => {
if (isMove(b)) {
if (isDescendant(move) && isDescendant(b)) {
if (areParentChild(deletedPath(move), deletedPath(b))) return moveFirst
if (areParentChild(deletedPath(b), deletedPath(move))) return -moveFirst
if (areEqual(deletedId(move), createdId(b))) return moveFirst
if (areEqual(deletedId(b), createdId(move))) return -moveFirst
if (lower(deletedPath(move), deletedPath(b))) return moveFirst
if (lower(deletedPath(b), deletedPath(move))) return -moveFirst
return 0
}
if (isDescendant(move) && !isDescendant(b))
return sortForDescendant(move, b, moveFirst)
if (!isDescendant(move) && isDescendant(b))
return sortForDescendant(b, move, -moveFirst)
if (areParentChild(deletedPath(b), deletedPath(move))) return moveFirst
if (areParentChild(deletedPath(move), deletedPath(b))) return -moveFirst
if (areParentChild(deletedPath(b), createdPath(move))) return moveFirst
if (areParentChild(deletedPath(move), createdPath(b))) return -moveFirst
if (areParentChild(createdPath(move), createdPath(b))) return moveFirst
if (areParentChild(createdPath(b), createdPath(move))) return -moveFirst
if (areParentChild(createdPath(move), deletedPath(b))) return moveFirst
if (areParentChild(createdPath(b), deletedPath(move))) return -moveFirst
// Both orders would be "valid" but if there already is a document at this
// path, processing `created` first would lead to a conflict.
// On the other hand, if there aren't any documents at this path,
// processing `deleted` first will lead to an error that can be recovered
// from via a retry.
//
// We use the `*Id` methods here since multiple paths can replace the same
// path on macOS and Windows.
if (areEqual(deletedId(move), createdId(b))) return moveFirst
if (areEqual(deletedId(b), createdId(move))) return -moveFirst
if (lower(createdPath(move), createdPath(b))) return moveFirst
if (lower(createdPath(b), createdPath(move))) return -moveFirst
return 0
}
return moveFirst
}
const sortForAdd = (add, b, addFirst) => {
if (isRestore(b) || isAdd(b)) {
if (areParentChild(createdPath(add), createdPath(b))) return addFirst
if (areParentChild(createdPath(b), createdPath(add))) return -addFirst
if (lower(createdPath(add), createdPath(b))) return addFirst
if (lower(createdPath(b), createdPath(add))) return -addFirst
return 0
}
return addFirst
}
// Priorities:
// isDelete > isTrash > isMove > isDescendant > isRestore > isAdd > isIgnore
const sortChanges = (a, b) => {
if (isDelete(a) || isTrash(a)) return sortForDelete(a, b, aFirst)
if (isDelete(b) || isTrash(b)) return sortForDelete(b, a, bFirst)
if (isMove(a)) return sortForMove(a, b, aFirst)
if (isMove(b)) return sortForMove(b, a, bFirst)
if (isRestore(a) || isAdd(a)) return sortForAdd(a, b, aFirst)
if (isRestore(b) || isAdd(b)) return sortForAdd(b, a, bFirst)
if (lower(ignoredPath(a), ignoredPath(b))) return aFirst
if (lower(ignoredPath(b), ignoredPath(a))) return bFirst
return 0
}
function sort(changes /*: Array<RemoteChange> */) /*: Array<RemoteChange> */ {
// return changes.sort(sortByPath).sort(sortByAction)
return changes.sort(sortChanges)
}
function sortByPath(
changes /*: Array<RemoteChange> */
) /*: Array<RemoteChange> */ {
return changes.sort(
(
{ doc: aDoc /*: { path: string } */ },
{ doc: bDoc /*: { path: string } */ }
) => {
if (!aDoc.path) return 1
if (!bDoc.path) return -1
const aPath = aDoc.path.normalize()
const bPath = bDoc.path.normalize()
if (aPath < bPath) return -1
if (aPath > bPath) return 1
return 0
}
)
}