vzcode 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-176d73ed.js +115 -0
- package/dist/assets/worker-aa634fce.js +237 -0
- package/dist/index.html +1 -1
- package/package.json +12 -11
- package/src/client/App.tsx +32 -25
- package/src/client/usePrettier/index.ts +161 -0
- package/src/client/usePrettier/worker.ts +93 -0
- package/src/server/index.js +33 -8
- package/src/types.ts +20 -1
- package/dist/assets/index-8a130b74.js +0 -89
package/dist/index.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
19
19
|
rel="stylesheet"
|
|
20
20
|
/>
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-176d73ed.js"></script>
|
|
22
22
|
<link rel="stylesheet" href="/assets/index-ccd52a89.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@codemirror/lang-css": "^6.2.1",
|
|
50
50
|
"@codemirror/lang-html": "^6.4.5",
|
|
51
|
-
"@codemirror/lang-javascript": "^6.
|
|
51
|
+
"@codemirror/lang-javascript": "^6.2.0",
|
|
52
52
|
"@codemirror/lang-markdown": "^6.2.0",
|
|
53
53
|
"@codemirror/state": "^6.2.1",
|
|
54
54
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
55
|
-
"@replit/codemirror-interact": "^6.
|
|
55
|
+
"@replit/codemirror-interact": "^6.3.0",
|
|
56
56
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
57
|
-
"@uiw/codemirror-themes-all": "^4.21.
|
|
57
|
+
"@uiw/codemirror-themes-all": "^4.21.11",
|
|
58
58
|
"codemirror": "^6.0.1",
|
|
59
|
-
"codemirror-ot": "^4.
|
|
59
|
+
"codemirror-ot": "^4.1.0",
|
|
60
60
|
"d3-array": "^3.2.4",
|
|
61
61
|
"diff-match-patch": "^1.0.5",
|
|
62
62
|
"dotenv": "^16.3.1",
|
|
@@ -69,17 +69,18 @@
|
|
|
69
69
|
"react-dom": "^18.2.0",
|
|
70
70
|
"react-select": "^5.7.4",
|
|
71
71
|
"sharedb": "^4.0.0",
|
|
72
|
-
"sharedb-client-browser": "^4.
|
|
72
|
+
"sharedb-client-browser": "^4.3.0",
|
|
73
73
|
"ws": "^8.13.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@types/react": "^18.2.
|
|
76
|
+
"@types/react": "^18.2.21",
|
|
77
77
|
"@types/react-dom": "^18.2.7",
|
|
78
78
|
"@vitejs/plugin-react": "^4.0.4",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
79
|
+
"ot-json1": "^1.0.2",
|
|
80
|
+
"prettier": "^3.0.2",
|
|
81
|
+
"sass": "^1.66.1",
|
|
82
|
+
"typescript": "^5.2.2",
|
|
82
83
|
"vite": "^4.4.9",
|
|
83
|
-
"vitest": "^0.34.
|
|
84
|
+
"vitest": "^0.34.3"
|
|
84
85
|
}
|
|
85
86
|
}
|
package/src/client/App.tsx
CHANGED
|
@@ -6,12 +6,13 @@ import { CodeEditor } from './CodeEditor';
|
|
|
6
6
|
import { diff } from './diff';
|
|
7
7
|
import { Settings } from './Settings';
|
|
8
8
|
import { Sidebar } from './Sidebar';
|
|
9
|
-
import { FileId, Files } from '../types';
|
|
9
|
+
import { FileId, Files, ShareDBDoc, VZCodeContent } from '../types';
|
|
10
10
|
import { TabList } from './TabList';
|
|
11
11
|
import { useOpenDirectories } from './useOpenDirectories';
|
|
12
12
|
import { useTabsState } from './useTabsState';
|
|
13
13
|
import { defaultTheme } from './themes';
|
|
14
14
|
import './style.scss';
|
|
15
|
+
import { usePrettier } from './usePrettier';
|
|
15
16
|
|
|
16
17
|
// Register our custom JSON1 OT type that supports presence.
|
|
17
18
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -24,15 +25,27 @@ const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
|
24
25
|
const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
|
|
25
26
|
const connection = new Connection(socket);
|
|
26
27
|
|
|
27
|
-
// The ShareDB document type.
|
|
28
|
-
type Document = {
|
|
29
|
-
files: Files;
|
|
30
|
-
isInteracting: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
28
|
function App() {
|
|
34
29
|
// The ShareDB document.
|
|
35
|
-
const [shareDBDoc, setShareDBDoc] =
|
|
30
|
+
const [shareDBDoc, setShareDBDoc] =
|
|
31
|
+
useState<ShareDBDoc<VZCodeContent> | null>(null);
|
|
32
|
+
|
|
33
|
+
// A helper function to submit operations to the ShareDB document
|
|
34
|
+
const submitOperation: (
|
|
35
|
+
next: (content: VZCodeContent) => VZCodeContent,
|
|
36
|
+
) => void = useCallback(
|
|
37
|
+
(next) => {
|
|
38
|
+
const content: VZCodeContent = shareDBDoc.data;
|
|
39
|
+
const op = diff(content, next(content));
|
|
40
|
+
if (op && shareDBDoc) {
|
|
41
|
+
shareDBDoc.submitOp(op);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
[shareDBDoc],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Auto-run Pretter after local changes.
|
|
48
|
+
usePrettier(shareDBDoc, submitOperation);
|
|
36
49
|
|
|
37
50
|
// Local ShareDB presence, for broadcasting our cursor position
|
|
38
51
|
// so other clients can see it.
|
|
@@ -46,7 +59,7 @@ function App() {
|
|
|
46
59
|
// The `doc.data` part of the ShareDB document,
|
|
47
60
|
// updated on each change to decouple rendering from ShareDB.
|
|
48
61
|
// Starts out as `null` until the document is loaded.
|
|
49
|
-
const [
|
|
62
|
+
const [content, setContent] = useState<VZCodeContent | null>(null);
|
|
50
63
|
|
|
51
64
|
// The id of the currently open file tab.
|
|
52
65
|
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
@@ -90,13 +103,13 @@ function App() {
|
|
|
90
103
|
setShareDBDoc(shareDBDoc);
|
|
91
104
|
|
|
92
105
|
// Set initial data.
|
|
93
|
-
|
|
106
|
+
setContent(shareDBDoc.data);
|
|
94
107
|
|
|
95
108
|
// Listen for all changes and update `data`.
|
|
96
109
|
// This decouples rendering logic from ShareDB.
|
|
97
110
|
// This callback gets called on each change.
|
|
98
111
|
shareDBDoc.on('op', () => {
|
|
99
|
-
|
|
112
|
+
setContent(shareDBDoc.data);
|
|
100
113
|
});
|
|
101
114
|
|
|
102
115
|
// Set up presence.
|
|
@@ -123,16 +136,6 @@ function App() {
|
|
|
123
136
|
};
|
|
124
137
|
}, []);
|
|
125
138
|
|
|
126
|
-
// A helper function to submit operations to the ShareDB document
|
|
127
|
-
const submitOperation = useCallback(
|
|
128
|
-
(updateFunction: (document: Document) => Document) => {
|
|
129
|
-
const currentDocument = shareDBDoc.data;
|
|
130
|
-
const nextDocument = updateFunction(currentDocument);
|
|
131
|
-
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
132
|
-
},
|
|
133
|
-
[shareDBDoc],
|
|
134
|
-
);
|
|
135
|
-
|
|
136
139
|
// Called when a file in the sidebar is double-clicked.
|
|
137
140
|
const handleRenameFileClick = useCallback(
|
|
138
141
|
(fileId: FileId) => {
|
|
@@ -193,27 +196,31 @@ function App() {
|
|
|
193
196
|
}, []);
|
|
194
197
|
|
|
195
198
|
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
196
|
-
// via interactive code widgets, and `false` when they are not.
|
|
199
|
+
// via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
|
|
197
200
|
const interactTimeoutRef = useRef(null);
|
|
198
201
|
const handleInteract = useCallback(() => {
|
|
202
|
+
// Set `isInteracting: true` if not already set.
|
|
199
203
|
if (!interactTimeoutRef.current) {
|
|
200
204
|
submitOperation((document) => ({ ...document, isInteracting: true }));
|
|
201
205
|
} else {
|
|
202
206
|
clearTimeout(interactTimeoutRef.current);
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
// Set `isInteracting: false` after a delay.
|
|
205
210
|
interactTimeoutRef.current = setTimeout(() => {
|
|
206
211
|
interactTimeoutRef.current = null;
|
|
207
212
|
submitOperation((document) => ({ ...document, isInteracting: false }));
|
|
208
213
|
}, 800);
|
|
209
214
|
}, [submitOperation]);
|
|
210
215
|
|
|
216
|
+
const files: Files | null = content ? content.files : null;
|
|
217
|
+
|
|
211
218
|
return (
|
|
212
219
|
<div className="app">
|
|
213
220
|
<div className="left">
|
|
214
221
|
<Sidebar
|
|
215
222
|
createFile={createFile}
|
|
216
|
-
files={
|
|
223
|
+
files={files}
|
|
217
224
|
handleRenameFileClick={handleRenameFileClick}
|
|
218
225
|
handleDeleteFileClick={handleDeleteFileClick}
|
|
219
226
|
handleFileClick={openTab}
|
|
@@ -229,13 +236,13 @@ function App() {
|
|
|
229
236
|
</div>
|
|
230
237
|
<div className="right">
|
|
231
238
|
<TabList
|
|
232
|
-
files={
|
|
239
|
+
files={files}
|
|
233
240
|
tabList={tabList}
|
|
234
241
|
activeFileId={activeFileId}
|
|
235
242
|
setActiveFileId={setActiveFileId}
|
|
236
243
|
closeTab={closeTab}
|
|
237
244
|
/>
|
|
238
|
-
{
|
|
245
|
+
{content && activeFileId ? (
|
|
239
246
|
<CodeEditor
|
|
240
247
|
shareDBDoc={shareDBDoc}
|
|
241
248
|
localPresence={localPresence}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { JSONOp } from 'ot-json1';
|
|
2
|
+
import { FileId, ShareDBDoc, VZCodeContent } from '../../types';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import PrettierWorker from './worker?worker';
|
|
6
|
+
|
|
7
|
+
// The time in milliseconds by which auto-saving is debounced.
|
|
8
|
+
const autoPrettierDebounceTimeMS = 1200;
|
|
9
|
+
|
|
10
|
+
// Computes a file extension from a file name.
|
|
11
|
+
// Example: 'foo.js' => '.js'
|
|
12
|
+
const extension = (fileName: string) => {
|
|
13
|
+
const match = fileName.match(/\.([^.]+)$/);
|
|
14
|
+
if (match) {
|
|
15
|
+
return match[1];
|
|
16
|
+
} else {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const usePrettier = (
|
|
22
|
+
shareDBDoc: ShareDBDoc<VZCodeContent> | null,
|
|
23
|
+
submitOperation,
|
|
24
|
+
) => {
|
|
25
|
+
// The set of files that have been modified
|
|
26
|
+
// since the last Prettier run.
|
|
27
|
+
const dirtyFiles: Set<FileId> = new Set<FileId>();
|
|
28
|
+
|
|
29
|
+
// A ref to keep track of whether an op is being applied.
|
|
30
|
+
// This is used to prevent Prettier from running
|
|
31
|
+
// when the op is coming from Prettier itself.
|
|
32
|
+
const isApplyingOpRef = useRef(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!shareDBDoc) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const prettierWorker = new PrettierWorker();
|
|
40
|
+
|
|
41
|
+
// Get the message that comes back from the worker.
|
|
42
|
+
// This is the prettified text for a specific file.
|
|
43
|
+
const handleMessage = (event) => {
|
|
44
|
+
const { fileId, error, fileTextPrettified } = event.data;
|
|
45
|
+
if (error) {
|
|
46
|
+
console.log(error);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isApplyingOpRef.current = true;
|
|
51
|
+
submitOperation((document) => ({
|
|
52
|
+
...document,
|
|
53
|
+
files: {
|
|
54
|
+
...document.files,
|
|
55
|
+
[fileId]: {
|
|
56
|
+
...document.files[fileId],
|
|
57
|
+
text: fileTextPrettified,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
isApplyingOpRef.current = false;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Listen for messages from the worker.
|
|
65
|
+
prettierWorker.addEventListener('message', handleMessage);
|
|
66
|
+
|
|
67
|
+
const runPrettier = async () => {
|
|
68
|
+
// Get the content of the document
|
|
69
|
+
const content = shareDBDoc.data;
|
|
70
|
+
|
|
71
|
+
// Get the files
|
|
72
|
+
const files = content.files;
|
|
73
|
+
|
|
74
|
+
// Get the dirty files
|
|
75
|
+
const dirtyFileIds = Array.from(dirtyFiles);
|
|
76
|
+
|
|
77
|
+
// Clear the set of dirty files
|
|
78
|
+
dirtyFiles.clear();
|
|
79
|
+
|
|
80
|
+
// Run Prettier on each dirty file
|
|
81
|
+
for (const fileId of dirtyFileIds) {
|
|
82
|
+
// Get the file
|
|
83
|
+
const file = files[fileId];
|
|
84
|
+
|
|
85
|
+
// Craft the data to send to the worker
|
|
86
|
+
const data = {
|
|
87
|
+
fileText: file.text,
|
|
88
|
+
fileExtension: extension(file.name),
|
|
89
|
+
fileId,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Run Prettier for this file
|
|
93
|
+
prettierWorker.postMessage(data);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Function to debounce running Prettier
|
|
98
|
+
let debounceTimeout;
|
|
99
|
+
function debouncePrettier() {
|
|
100
|
+
clearTimeout(debounceTimeout);
|
|
101
|
+
debounceTimeout = setTimeout(runPrettier, autoPrettierDebounceTimeMS);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Listen for changes
|
|
105
|
+
shareDBDoc.on('op batch', (op: JSONOp, isLocal: boolean) => {
|
|
106
|
+
// Only act on changes coming from the client
|
|
107
|
+
if (isLocal) {
|
|
108
|
+
// If the op is coming from Prettier itself,
|
|
109
|
+
// then do nothing.
|
|
110
|
+
if (isApplyingOpRef.current) {
|
|
111
|
+
// console.log('ignoring op from Prettier');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Example op that we want to act on:
|
|
116
|
+
// op [
|
|
117
|
+
// "files",
|
|
118
|
+
// "2514857669",
|
|
119
|
+
// "text",
|
|
120
|
+
// {
|
|
121
|
+
// "es": [
|
|
122
|
+
// 18,
|
|
123
|
+
// "d"
|
|
124
|
+
// ]
|
|
125
|
+
// }
|
|
126
|
+
// ]
|
|
127
|
+
|
|
128
|
+
// Check if the path of this op is the text content of a file
|
|
129
|
+
if (
|
|
130
|
+
op &&
|
|
131
|
+
op.length === 4 &&
|
|
132
|
+
op[0] === 'files' &&
|
|
133
|
+
typeof op[1] === 'string' &&
|
|
134
|
+
op[2] === 'text'
|
|
135
|
+
) {
|
|
136
|
+
// Get the file id
|
|
137
|
+
const fileId: FileId = op[1];
|
|
138
|
+
|
|
139
|
+
// Add the file id to the set of dirty files
|
|
140
|
+
dirtyFiles.add(fileId);
|
|
141
|
+
|
|
142
|
+
// Debounce running Prettier
|
|
143
|
+
debouncePrettier();
|
|
144
|
+
|
|
145
|
+
// console.log('Op is for file', fileId);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// console.log('ignoring op from remote', op, isLocal);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// In the event that the component unmounts,
|
|
152
|
+
return () => {
|
|
153
|
+
// Remove the event listener
|
|
154
|
+
prettierWorker.removeEventListener('message', handleMessage);
|
|
155
|
+
|
|
156
|
+
// Clear the timeout
|
|
157
|
+
clearTimeout(debounceTimeout);
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}, [shareDBDoc]);
|
|
161
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { format } from 'prettier/standalone';
|
|
2
|
+
import * as prettierPluginBabel from 'prettier/plugins/babel';
|
|
3
|
+
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
4
|
+
import * as prettierPluginHtml from 'prettier/plugins/html';
|
|
5
|
+
import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
|
|
6
|
+
import * as prettierPluginCSS from 'prettier/plugins/postcss';
|
|
7
|
+
// import * as prettierPluginYaml from 'prettier/plugins/yaml';
|
|
8
|
+
// import * as prettierPluginGraphql from 'prettier/plugins/graphql';
|
|
9
|
+
// import * as prettierPluginAngular from 'prettier/plugins/angular';
|
|
10
|
+
import * as prettierPluginTypescript from 'prettier/plugins/typescript';
|
|
11
|
+
|
|
12
|
+
import { FileId } from '../../types';
|
|
13
|
+
|
|
14
|
+
const parsers = {
|
|
15
|
+
js: 'babel',
|
|
16
|
+
jsx: 'babel',
|
|
17
|
+
mjs: 'babel',
|
|
18
|
+
cjs: 'babel',
|
|
19
|
+
ts: 'typescript',
|
|
20
|
+
tsx: 'typescript',
|
|
21
|
+
css: 'css',
|
|
22
|
+
// '.less': 'less',
|
|
23
|
+
// '.scss': 'scss',
|
|
24
|
+
html: 'html',
|
|
25
|
+
json: 'json',
|
|
26
|
+
json5: 'json5',
|
|
27
|
+
// '.graphql': 'graphql',
|
|
28
|
+
// '.gql': 'graphql',
|
|
29
|
+
md: 'markdown',
|
|
30
|
+
markdown: 'markdown',
|
|
31
|
+
// '.yaml': 'yaml',
|
|
32
|
+
// '.yml': 'yaml',
|
|
33
|
+
// '.vue': 'vue',
|
|
34
|
+
// '.component.html': 'angular',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const plugins = [
|
|
38
|
+
prettierPluginBabel,
|
|
39
|
+
prettierPluginEstree,
|
|
40
|
+
prettierPluginHtml,
|
|
41
|
+
prettierPluginMarkdown,
|
|
42
|
+
prettierPluginTypescript,
|
|
43
|
+
prettierPluginCSS,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
onmessage = async ({
|
|
47
|
+
data,
|
|
48
|
+
}: {
|
|
49
|
+
data: {
|
|
50
|
+
// The text content of the file
|
|
51
|
+
fileText: string;
|
|
52
|
+
|
|
53
|
+
// The file extension
|
|
54
|
+
// Supported extensions: https://prettier.io/docs/en/options.html#parser
|
|
55
|
+
// VizHub only supports
|
|
56
|
+
// - JavaScript
|
|
57
|
+
// - TypeScript
|
|
58
|
+
// - HTML
|
|
59
|
+
// - CSS
|
|
60
|
+
// - JSON
|
|
61
|
+
// - Markdown
|
|
62
|
+
|
|
63
|
+
fileExtension: string;
|
|
64
|
+
|
|
65
|
+
// The file id
|
|
66
|
+
fileId: FileId;
|
|
67
|
+
};
|
|
68
|
+
}) => {
|
|
69
|
+
const { fileExtension, fileText, fileId } = data;
|
|
70
|
+
const parser = parsers[fileExtension];
|
|
71
|
+
|
|
72
|
+
if (!parser) {
|
|
73
|
+
postMessage({
|
|
74
|
+
fileId,
|
|
75
|
+
error: `Unsupported file extension for Prettier: ${fileExtension}`,
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const fileTextPrettified = await format(fileText, { parser, plugins });
|
|
82
|
+
|
|
83
|
+
postMessage({
|
|
84
|
+
fileId,
|
|
85
|
+
fileTextPrettified,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
postMessage({
|
|
89
|
+
fileId,
|
|
90
|
+
error,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
package/src/server/index.js
CHANGED
|
@@ -18,6 +18,10 @@ dotenv.config({ path: '../../.env' });
|
|
|
18
18
|
// The time in milliseconds by which auto-saving is debounced.
|
|
19
19
|
const autoSaveDebounceTimeMS = 800;
|
|
20
20
|
|
|
21
|
+
// The time in milliseconds by which auto-saving is throttled
|
|
22
|
+
// when the user is interacting with the widgets in the editor.
|
|
23
|
+
const throttleTimeMS = 100;
|
|
24
|
+
|
|
21
25
|
// Server port.
|
|
22
26
|
const port = 3030;
|
|
23
27
|
|
|
@@ -49,7 +53,23 @@ const shareDBBackend = new ShareDB({
|
|
|
49
53
|
const server = http.createServer(app);
|
|
50
54
|
const wss = new WebSocketServer({ server });
|
|
51
55
|
wss.on('connection', (ws) => {
|
|
52
|
-
|
|
56
|
+
const clientStream = new WebSocketJSONStream(ws);
|
|
57
|
+
shareDBBackend.listen(clientStream);
|
|
58
|
+
|
|
59
|
+
// Prevent server crashes on errors.
|
|
60
|
+
clientStream.on('error', (error) => {
|
|
61
|
+
console.log('clientStream error: ' + error.message);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Handle errors
|
|
65
|
+
ws.on('error', (error) => {
|
|
66
|
+
console.log('ws error: ' + error.message);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Handle disconnections
|
|
70
|
+
ws.on('close', (code) => {
|
|
71
|
+
clientStream.end();
|
|
72
|
+
});
|
|
53
73
|
});
|
|
54
74
|
|
|
55
75
|
// Serve static files
|
|
@@ -69,7 +89,6 @@ let previousDocument = initialDocument;
|
|
|
69
89
|
|
|
70
90
|
// Saves the files that changed.
|
|
71
91
|
const save = () => {
|
|
72
|
-
console.log('saving');
|
|
73
92
|
const currentDocument = shareDBDoc.data;
|
|
74
93
|
|
|
75
94
|
// Take a look at each file (key) previously and currently.
|
|
@@ -119,31 +138,37 @@ const save = () => {
|
|
|
119
138
|
// });
|
|
120
139
|
// });
|
|
121
140
|
|
|
122
|
-
const throttleTimeMS = 200;
|
|
123
|
-
|
|
124
141
|
let lastExecutedTime = Date.now();
|
|
142
|
+
let lastChangeTimeout; // This timeout ensures the last change is saved
|
|
125
143
|
|
|
126
144
|
// Function to throttle the saving
|
|
127
145
|
function throttleSave() {
|
|
128
|
-
console.log('throttling');
|
|
129
146
|
const now = Date.now();
|
|
130
|
-
|
|
147
|
+
const timeSinceLastExecution = now - lastExecutedTime;
|
|
148
|
+
|
|
149
|
+
if (timeSinceLastExecution > throttleTimeMS) {
|
|
131
150
|
save();
|
|
132
151
|
lastExecutedTime = now;
|
|
152
|
+
} else {
|
|
153
|
+
// Clear the previous timeout and set a new one to save the last change
|
|
154
|
+
clearTimeout(lastChangeTimeout);
|
|
155
|
+
lastChangeTimeout = setTimeout(() => {
|
|
156
|
+
save();
|
|
157
|
+
lastExecutedTime = now;
|
|
158
|
+
}, throttleTimeMS - timeSinceLastExecution);
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
// Function to debounce the saving
|
|
137
163
|
let debounceTimeout;
|
|
138
164
|
function debounceSave() {
|
|
139
|
-
console.log('debouncing');
|
|
140
165
|
clearTimeout(debounceTimeout);
|
|
141
166
|
debounceTimeout = setTimeout(save, autoSaveDebounceTimeMS);
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
// Subscribe to listen for modifications
|
|
145
170
|
shareDBDoc.subscribe(() => {
|
|
146
|
-
shareDBDoc.on('op', () => {
|
|
171
|
+
shareDBDoc.on('op', (op) => {
|
|
147
172
|
if (shareDBDoc.data.isInteracting) {
|
|
148
173
|
throttleSave();
|
|
149
174
|
} else {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// FileId
|
|
2
2
|
// * A unique ID for a file.
|
|
3
|
+
|
|
4
|
+
import { JSONOp } from 'ot-json1';
|
|
5
|
+
|
|
3
6
|
// * This is a random string.
|
|
4
7
|
export type FileId = string;
|
|
5
8
|
|
|
@@ -64,6 +67,22 @@ export type ShareDBDoc<T> = {
|
|
|
64
67
|
data: T;
|
|
65
68
|
ingestSnapshot: (snapshot: any, callback) => void;
|
|
66
69
|
subscribe: (callback) => void;
|
|
67
|
-
on: (event: string, callback: () => void) => void;
|
|
70
|
+
on: (event: string, callback: (op: JSONOp, source: boolean) => void) => void;
|
|
68
71
|
off: (event: string, callback: () => void) => void;
|
|
72
|
+
submitOp: (op: JSONOp, options?: any, callback?: () => void) => void;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// The ShareDB document type for VZCode.
|
|
76
|
+
export type VZCodeContent = {
|
|
77
|
+
// `files`
|
|
78
|
+
// * The files in the VZCode instance.
|
|
79
|
+
files: Files;
|
|
80
|
+
|
|
81
|
+
// `isInteracting`
|
|
82
|
+
// * `true` when the user is interacting
|
|
83
|
+
// via interactive code widgets (e.g. Alt+drag)
|
|
84
|
+
// * Hot reloading is throttled when this is `true`.
|
|
85
|
+
// * `false` when they are not (e.g. normal typing)
|
|
86
|
+
// * Hot reloading is debounced when this is `false`.
|
|
87
|
+
isInteracting: boolean;
|
|
69
88
|
};
|