diff options
Diffstat (limited to 'app/assets/javascripts/ide/lib')
-rw-r--r-- | app/assets/javascripts/ide/lib/common/model.js | 35 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/create_diff.js | 85 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/create_file_diff.js | 112 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/diff/controller.js | 9 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/diff/diff.js | 9 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editor.js | 32 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editor_options.js | 22 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editorconfig/parser.js | 55 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js | 33 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/files.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/languages/README.md | 21 | ||||
-rw-r--r-- | app/assets/javascripts/ide/lib/mirror.js | 154 |
12 files changed, 549 insertions, 23 deletions
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index a15f04075d9..c5bb00c3dee 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,6 +1,8 @@ import { editor as monacoEditor, Uri } from 'monaco-editor'; import Disposable from './disposable'; import eventHub from '../../eventhub'; +import { trimTrailingWhitespace, insertFinalNewline } from '../../utils'; +import { defaultModelOptions } from '../editor_options'; export default class Model { constructor(file, head = null) { @@ -8,6 +10,7 @@ export default class Model { this.file = file; this.head = head; this.content = file.content !== '' || file.deleted ? file.content : file.raw; + this.options = { ...defaultModelOptions }; this.disposable.add( (this.originalModel = monacoEditor.createModel( @@ -50,10 +53,6 @@ export default class Model { return this.model.getModeId(); } - get eol() { - return this.model.getEOL() === '\n' ? 'LF' : 'CRLF'; - } - get path() { return this.file.key; } @@ -94,8 +93,32 @@ export default class Model { this.getModel().setValue(content); } + updateOptions(obj = {}) { + Object.assign(this.options, obj); + this.model.updateOptions(obj); + this.applyCustomOptions(); + } + + applyCustomOptions() { + this.updateNewContent( + Object.entries(this.options).reduce((content, [key, value]) => { + switch (key) { + case 'endOfLine': + this.model.pushEOL(value); + return this.model.getValue(); + case 'insertFinalNewline': + return value ? insertFinalNewline(content) : content; + case 'trimTrailingWhitespace': + return value ? trimTrailingWhitespace(content) : content; + default: + return content; + } + }, this.model.getValue()), + ); + } + dispose() { - this.disposable.dispose(); + if (!this.model.isDisposed()) this.applyCustomOptions(); this.events.forEach(cb => { if (typeof cb === 'function') cb(); @@ -106,5 +129,7 @@ export default class Model { eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose); eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent); eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent); + + this.disposable.dispose(); } } diff --git a/app/assets/javascripts/ide/lib/create_diff.js b/app/assets/javascripts/ide/lib/create_diff.js new file mode 100644 index 00000000000..3e915afdbcb --- /dev/null +++ b/app/assets/javascripts/ide/lib/create_diff.js @@ -0,0 +1,85 @@ +import { commitActionForFile } from '~/ide/stores/utils'; +import { commitActionTypes } from '~/ide/constants'; +import createFileDiff from './create_file_diff'; + +const getDeletedParents = (entries, file) => { + const parent = file.parentPath && entries[file.parentPath]; + + if (parent && parent.deleted) { + return [parent, ...getDeletedParents(entries, parent)]; + } + + return []; +}; + +const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} }) => { + // We need changed files to overwrite staged, so put them at the end. + const changes = stagedFiles.concat(changedFiles).reduce((acc, file) => { + const key = file.path; + const action = commitActionForFile(file); + const prev = acc[key]; + + // If a file was deleted, which was previously added, then we should do nothing. + if (action === commitActionTypes.delete && prev && prev.action === commitActionTypes.create) { + delete acc[key]; + } else { + acc[key] = { action, file }; + } + + return acc; + }, {}); + + // We need to clean "move" actions, because we can only support 100% similarity moves at the moment. + // This is because the previous file's content might not be loaded. + Object.values(changes) + .filter(change => change.action === commitActionTypes.move) + .forEach(change => { + const prev = changes[change.file.prevPath]; + + if (!prev) { + return; + } + + if (change.file.content === prev.file.content) { + // If content is the same, continue with the move but don't do the prevPath's delete. + delete changes[change.file.prevPath]; + } else { + // Otherwise, treat the move as a delete / create. + Object.assign(change, { action: commitActionTypes.create }); + } + }); + + // Next, we need to add deleted directories by looking at the parents + Object.values(changes) + .filter(change => change.action === commitActionTypes.delete && change.file.parentPath) + .forEach(({ file }) => { + // Do nothing if we've already visited this directory. + if (changes[file.parentPath]) { + return; + } + + getDeletedParents(entries, file).forEach(parent => { + changes[parent.path] = { action: commitActionTypes.delete, file: parent }; + }); + }); + + return Object.values(changes); +}; + +const createDiff = state => { + const changes = filesWithChanges(state); + + const toDelete = changes.filter(x => x.action === commitActionTypes.delete).map(x => x.file.path); + + const patch = changes + .filter(x => x.action !== commitActionTypes.delete) + .map(({ file, action }) => createFileDiff(file, action)) + .join(''); + + return { + patch, + toDelete, + }; +}; + +export default createDiff; diff --git a/app/assets/javascripts/ide/lib/create_file_diff.js b/app/assets/javascripts/ide/lib/create_file_diff.js new file mode 100644 index 00000000000..5ae4993321c --- /dev/null +++ b/app/assets/javascripts/ide/lib/create_file_diff.js @@ -0,0 +1,112 @@ +/* eslint-disable @gitlab/require-i18n-strings */ +import { createTwoFilesPatch } from 'diff'; +import { commitActionTypes } from '~/ide/constants'; + +const DEV_NULL = '/dev/null'; +const DEFAULT_MODE = '100644'; +const NO_NEW_LINE = '\\ No newline at end of file'; +const NEW_LINE = '\n'; + +/** + * Cleans patch generated by `diff` package. + * + * - Removes "=======" separator added at the beginning + */ +const cleanTwoFilesPatch = text => text.replace(/^(=+\s*)/, ''); + +const endsWithNewLine = val => !val || val[val.length - 1] === NEW_LINE; + +const addEndingNewLine = val => (endsWithNewLine(val) ? val : val + NEW_LINE); + +const removeEndingNewLine = val => (endsWithNewLine(val) ? val.substr(0, val.length - 1) : val); + +const diffHead = (prevPath, newPath = '') => + `diff --git "a/${prevPath}" "b/${newPath || prevPath}"`; + +const createDiffBody = (path, content, isCreate) => { + if (!content) { + return ''; + } + + const prefix = isCreate ? '+' : '-'; + const fromPath = isCreate ? DEV_NULL : `a/${path}`; + const toPath = isCreate ? `b/${path}` : DEV_NULL; + + const hasNewLine = endsWithNewLine(content); + const lines = removeEndingNewLine(content).split(NEW_LINE); + + const chunkHead = isCreate ? `@@ -0,0 +1,${lines.length} @@` : `@@ -1,${lines.length} +0,0 @@`; + const chunk = lines + .map(line => `${prefix}${line}`) + .concat(!hasNewLine ? [NO_NEW_LINE] : []) + .join(NEW_LINE); + + return `--- ${fromPath} ++++ ${toPath} +${chunkHead} +${chunk}`; +}; + +const createMoveFileDiff = (prevPath, newPath) => `${diffHead(prevPath, newPath)} +rename from ${prevPath} +rename to ${newPath}`; + +const createNewFileDiff = (path, content) => { + const diff = createDiffBody(path, content, true); + + return `${diffHead(path)} +new file mode ${DEFAULT_MODE} +${diff}`; +}; + +const createDeleteFileDiff = (path, content) => { + const diff = createDiffBody(path, content, false); + + return `${diffHead(path)} +deleted file mode ${DEFAULT_MODE} +${diff}`; +}; + +const createUpdateFileDiff = (path, oldContent, newContent) => { + const patch = createTwoFilesPatch(`a/${path}`, `b/${path}`, oldContent, newContent); + + return `${diffHead(path)} +${cleanTwoFilesPatch(patch)}`; +}; + +const createFileDiffRaw = (file, action) => { + switch (action) { + case commitActionTypes.move: + return createMoveFileDiff(file.prevPath, file.path); + case commitActionTypes.create: + return createNewFileDiff(file.path, file.content); + case commitActionTypes.delete: + return createDeleteFileDiff(file.path, file.content); + case commitActionTypes.update: + return createUpdateFileDiff(file.path, file.raw || '', file.content); + default: + return ''; + } +}; + +/** + * Create a git diff for a single IDE file. + * + * ## Notes: + * When called with `commitActionType.move`, it assumes that the move + * is a 100% similarity move. No diff will be generated. This is because + * generating a move with changes is not support by the current IDE, since + * the source file might not have it's content loaded yet. + * + * When called with `commitActionType.delete`, it does not support + * deleting files with a mode different than 100644. For the IDE mirror, this + * isn't needed because deleting is handled outside the unified patch. + * + * ## References: + * - https://git-scm.com/docs/git-diff#_generating_patches_with_p + */ +const createFileDiff = (file, action) => + // It's important that the file diff ends in a new line - git expects this. + addEndingNewLine(createFileDiffRaw(file, action)); + +export default createFileDiff; diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index 234a7f903a1..35fcda6a6c5 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -50,10 +50,15 @@ export default class DirtyDiffController { } computeDiff(model) { + const originalModel = model.getOriginalModel(); + const newModel = model.getModel(); + + if (originalModel.isDisposed() || newModel.isDisposed()) return; + this.dirtyDiffWorker.postMessage({ path: model.path, - originalContent: model.getOriginalModel().getValue(), - newContent: model.getModel().getValue(), + originalContent: originalModel.getValue(), + newContent: newModel.getValue(), }); } diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js index 29e29d7fcd3..3a456b7c4d6 100644 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -1,8 +1,15 @@ import { diffLines } from 'diff'; +import { defaultDiffOptions } from '../editor_options'; +// See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20 // eslint-disable-next-line import/prefer-default-export export const computeDiff = (originalContent, newContent) => { - const changes = diffLines(originalContent, newContent); + // prevent EOL changes from highlighting the entire file + const changes = diffLines( + originalContent.replace(/\r\n/g, '\n'), + newContent.replace(/\r\n/g, '\n'), + defaultDiffOptions, + ); let lineNumber = 1; return changes.reduce((acc, change) => { diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 25224abd77c..4dfc27117c0 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,11 +1,11 @@ import { debounce } from 'lodash'; -import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor'; +import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor'; import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; -import editorOptions, { defaultEditorOptions } from './editor_options'; +import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options'; import { themes } from './themes'; import languages from './languages'; import keymap from './keymap.json'; @@ -37,6 +37,10 @@ export default class Editor { ...defaultEditorOptions, ...options, }; + this.diffOptions = { + ...defaultDiffEditorOptions, + ...options, + }; setupThemes(); registerLanguages(...languages); @@ -66,19 +70,14 @@ export default class Editor { } } - createDiffInstance(domElement, readOnly = true) { + createDiffInstance(domElement) { if (!this.instance) { clearDomElement(domElement); this.disposable.add( (this.instance = monacoEditor.createDiffEditor(domElement, { - ...this.options, - quickSuggestions: false, - occurrencesHighlight: false, + ...this.diffOptions, renderSideBySide: Editor.renderSideBySide(domElement), - readOnly, - renderLineHighlight: readOnly ? 'all' : 'none', - hideCursorInOverviewRuler: !readOnly, })), ); @@ -187,6 +186,21 @@ export default class Editor { }); } + replaceSelectedText(text) { + let selection = this.instance.getSelection(); + const range = new Range( + selection.startLineNumber, + selection.startColumn, + selection.endLineNumber, + selection.endColumn, + ); + + this.instance.executeEdits('', [{ range, text }]); + + selection = this.instance.getSelection(); + this.instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn }); + } + get isDiffEditorType() { return this.instance.getEditorType() === 'vs.editor.IDiffEditor'; } diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index dac2a8e8b51..f182a1ec50e 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -9,7 +9,27 @@ export const defaultEditorOptions = { wordWrap: 'on', }; -export default [ +export const defaultDiffOptions = { + ignoreWhitespace: false, +}; + +export const defaultDiffEditorOptions = { + ...defaultEditorOptions, + quickSuggestions: false, + occurrencesHighlight: false, + ignoreTrimWhitespace: false, + readOnly: false, + renderLineHighlight: 'none', + hideCursorInOverviewRuler: true, +}; + +export const defaultModelOptions = { + endOfLine: 0, + insertFinalNewline: true, + trimTrailingWhitespace: false, +}; + +export const editorOptions = [ { readOnly: model => Boolean(model.file.file_lock), quickSuggestions: model => !(model.language === 'markdown'), diff --git a/app/assets/javascripts/ide/lib/editorconfig/parser.js b/app/assets/javascripts/ide/lib/editorconfig/parser.js new file mode 100644 index 00000000000..a30a8cb868d --- /dev/null +++ b/app/assets/javascripts/ide/lib/editorconfig/parser.js @@ -0,0 +1,55 @@ +import { parseString } from 'editorconfig/src/lib/ini'; +import minimatch from 'minimatch'; +import { getPathParents } from '../../utils'; + +const dirname = path => path.replace(/\.editorconfig$/, ''); + +function isRootConfig(config) { + return config.some(([pattern, rules]) => !pattern && rules?.root === 'true'); +} + +function getRulesForSection(path, [pattern, rules]) { + if (!pattern) { + return {}; + } + if (minimatch(path, pattern, { matchBase: true })) { + return rules; + } + + return {}; +} + +function getRulesWithConfigs(filePath, configFiles = [], rules = {}) { + if (!configFiles.length) return rules; + + const [{ content, path: configPath }, ...nextConfigs] = configFiles; + const configDir = dirname(configPath); + + if (!filePath.startsWith(configDir)) return rules; + + const parsed = parseString(content); + const isRoot = isRootConfig(parsed); + const relativeFilePath = filePath.slice(configDir.length); + + const sectionRules = parsed.reduce( + (acc, section) => Object.assign(acc, getRulesForSection(relativeFilePath, section)), + {}, + ); + + // prefer existing rules by overwriting to section rules + const result = Object.assign(sectionRules, rules); + + return isRoot ? result : getRulesWithConfigs(filePath, nextConfigs, result); +} + +// eslint-disable-next-line import/prefer-default-export +export function getRulesWithTraversal(filePath, getFileContent) { + const editorconfigPaths = [ + ...getPathParents(filePath).map(x => `${x}/.editorconfig`), + '.editorconfig', + ]; + + return Promise.all( + editorconfigPaths.map(path => getFileContent(path).then(content => ({ path, content }))), + ).then(results => getRulesWithConfigs(filePath, results.filter(x => x.content))); +} diff --git a/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js b/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js new file mode 100644 index 00000000000..f9d5579511a --- /dev/null +++ b/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js @@ -0,0 +1,33 @@ +import { isBoolean, isNumber } from 'lodash'; + +const map = (key, validValues) => value => + value in validValues ? { [key]: validValues[value] } : {}; + +const bool = key => value => (isBoolean(value) ? { [key]: value } : {}); + +const int = (key, isValid) => value => + isNumber(value) && isValid(value) ? { [key]: Math.trunc(value) } : {}; + +const rulesMapper = { + indent_style: map('insertSpaces', { tab: false, space: true }), + indent_size: int('tabSize', n => n > 0), + tab_width: int('tabSize', n => n > 0), + trim_trailing_whitespace: bool('trimTrailingWhitespace'), + end_of_line: map('endOfLine', { crlf: 1, lf: 0 }), + insert_final_newline: bool('insertFinalNewline'), +}; + +const parseValue = x => { + let value = typeof x === 'string' ? x.toLowerCase() : x; + if (/^[0-9.-]+$/.test(value)) value = Number(value); + if (value === 'true') value = true; + if (value === 'false') value = false; + + return value; +}; + +export default function mapRulesToMonaco(rules) { + return Object.entries(rules).reduce((obj, [key, value]) => { + return Object.assign(obj, rulesMapper[key]?.(parseValue(value)) || {}); + }, {}); +} diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index 26518a2abac..6d85e225fd5 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -19,7 +19,6 @@ export const decorateFiles = ({ branchId, tempFile = false, content = '', - base64 = false, binary = false, rawPath = '', }) => { @@ -49,7 +48,6 @@ export const decorateFiles = ({ path, url: `/${projectId}/tree/${branchId}/-/${path}/`, type: 'tree', - parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, changed: tempFile, opened: tempFile, @@ -86,14 +84,11 @@ export const decorateFiles = ({ path, url: `/${projectId}/blob/${branchId}/-/${path}`, type: 'blob', - parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, changed: tempFile, content, - base64, binary: (previewMode && previewMode.binary) || binary, rawPath, - previewMode, parentPath, }); diff --git a/app/assets/javascripts/ide/lib/languages/README.md b/app/assets/javascripts/ide/lib/languages/README.md new file mode 100644 index 00000000000..e4d1a4c7818 --- /dev/null +++ b/app/assets/javascripts/ide/lib/languages/README.md @@ -0,0 +1,21 @@ +# Web IDE Languages + +The Web IDE uses the [Monaco editor](https://microsoft.github.io/monaco-editor/) which uses the [Monarch library](https://microsoft.github.io/monaco-editor/monarch.html) for syntax highlighting. +The Web IDE currently supports all langauges defined in the [monaco-languages](https://github.com/microsoft/monaco-languages/tree/master/src) repository. + +## Adding New Languages + +While Monaco supports a wide variety of languages, there's always the chance that it's missing something. +You'll find a list of [unsupported languages in this epic](https://gitlab.com/groups/gitlab-org/-/epics/1474), which is the right place to add more if needed. + +Should you be willing to help us and add support to GitLab for any missing languages, here are the steps to do so: + +1. Create a new issue and add it to [this epic](https://gitlab.com/groups/gitlab-org/-/epics/1474), if it doesn't already exist. +2. Create a new file in this folder called `{languageName}.js`, where `{languageName}` is the name of the language you want to add support for. +3. Follow the [Monarch documentation](https://microsoft.github.io/monaco-editor/monarch.html) to add a configuration for the new language. + - Example: The [`vue.js`](./vue.js) file in the current directory adds support for Vue.js Syntax Highlighting. +4. Add tests for the new langauge implementation in `spec/frontend/ide/lib/languages/{langaugeName}.js`. + - Example: See [`vue_spec.js`](spec/frontend/ide/lib/languages/vue_spec.js). +5. Create a [Merge Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) with your newly added language. + +Thank you! diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js new file mode 100644 index 00000000000..a516c28ad7a --- /dev/null +++ b/app/assets/javascripts/ide/lib/mirror.js @@ -0,0 +1,154 @@ +import createDiff from './create_diff'; +import { getWebSocketUrl, mergeUrlParams } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; + +export const SERVICE_NAME = 'webide-file-sync'; +export const PROTOCOL = 'webfilesync.gitlab.com'; +export const MSG_CONNECTION_ERROR = __('Could not connect to Web IDE file mirror service.'); + +// Before actually connecting to the service, we must delay a bit +// so that the service has sufficiently started. + +const noop = () => {}; +export const SERVICE_DELAY = 8000; + +const cancellableWait = time => { + let timeoutId = 0; + + const cancel = () => clearTimeout(timeoutId); + + const promise = new Promise(resolve => { + timeoutId = setTimeout(resolve, time); + }); + + return [promise, cancel]; +}; + +const isErrorResponse = error => error && error.code !== 0; + +const isErrorPayload = payload => payload && payload.status_code !== 200; + +const getErrorFromResponse = data => { + if (isErrorResponse(data.error)) { + return { message: data.error.Message }; + } else if (isErrorPayload(data.payload)) { + return { message: data.payload.error_message }; + } + + return null; +}; + +const getFullPath = path => mergeUrlParams({ service: SERVICE_NAME }, getWebSocketUrl(path)); + +const createWebSocket = fullPath => + new Promise((resolve, reject) => { + const socket = new WebSocket(fullPath, [PROTOCOL]); + const resetCallbacks = () => { + socket.onopen = null; + socket.onerror = null; + }; + + socket.onopen = () => { + resetCallbacks(); + resolve(socket); + }; + + socket.onerror = () => { + resetCallbacks(); + reject(new Error(MSG_CONNECTION_ERROR)); + }; + }); + +export const canConnect = ({ services = [] }) => services.some(name => name === SERVICE_NAME); + +export const createMirror = () => { + let socket = null; + let cancelHandler = noop; + let nextMessageHandler = noop; + + const cancelConnect = () => { + cancelHandler(); + cancelHandler = noop; + }; + + const onCancelConnect = fn => { + cancelHandler = fn; + }; + + const receiveMessage = ev => { + const handle = nextMessageHandler; + nextMessageHandler = noop; + handle(JSON.parse(ev.data)); + }; + + const onNextMessage = fn => { + nextMessageHandler = fn; + }; + + const waitForNextMessage = () => + new Promise((resolve, reject) => { + onNextMessage(data => { + const err = getErrorFromResponse(data); + + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + const uploadDiff = ({ toDelete, patch }) => { + if (!socket) { + return Promise.resolve(); + } + + const response = waitForNextMessage(); + + const msg = { + code: 'EVENT', + namespace: '/files', + event: 'PATCH', + payload: { diff: patch, delete_files: toDelete }, + }; + + socket.send(JSON.stringify(msg)); + + return response; + }; + + return { + upload(state) { + return uploadDiff(createDiff(state)); + }, + connect(path) { + if (socket) { + this.disconnect(); + } + + const fullPath = getFullPath(path); + const [wait, cancelWait] = cancellableWait(SERVICE_DELAY); + + onCancelConnect(cancelWait); + + return wait + .then(() => createWebSocket(fullPath)) + .then(newSocket => { + socket = newSocket; + socket.onmessage = receiveMessage; + }); + }, + disconnect() { + cancelConnect(); + + if (!socket) { + return; + } + + socket.close(); + socket = null; + }, + }; +}; + +export default createMirror(); |