summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ide/lib/mirror.js
blob: 6f9cfec9465e7b0e12edc3421b94b66f7c43c813 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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();