Skip to content

Commit 291859d

Browse files
authored
Improve Live conflict resolving (codesandbox#681)
* Much better resolving of conflicts * Add Cancel Session button and option to hide notifications * Fix waiting for service worker * Improve service workers * Add more max ages to sw * Fix disconnecting
1 parent c400c5d commit 291859d

File tree

15 files changed

+198
-132
lines changed

15 files changed

+198
-132
lines changed

packages/app/config/webpack.common.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const __PROD__ = NODE_ENV === 'production'; // eslint-disable-line no-underscore
2020
// const __TEST__ = NODE_ENV === 'test'; // eslint-disable-line no-underscore-dangle
2121
const babelConfig = __DEV__ ? babelDev : babelProd;
2222

23-
const publicPath = SANDBOX_ONLY ? '/' : getHost() + '/';
23+
const publicPath = SANDBOX_ONLY || __DEV__ ? '/' : getHost() + '/';
2424

2525
// Shim for `eslint-plugin-vue/lib/index.js`
2626
const ESLINT_PLUGIN_VUE_INDEX = `module.exports = {

packages/app/config/webpack.prod.js

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,6 @@ module.exports = merge(commonConfig, {
7171
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
7272
maximumFileSizeToCacheInBytes: 5242880,
7373

74-
// We need to keep the old assets available until the sandbox has
75-
// loaded them.
76-
skipWaiting: false,
7774
runtimeCaching: [
7875
{
7976
urlPattern: /api\/v1\/sandboxes/,
@@ -139,7 +136,7 @@ module.exports = merge(commonConfig, {
139136
navigateFallbackWhitelist: [/^(?!\/__).*/],
140137
// Don't precache sourcemaps (they're large) and build asset manifest:
141138
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
142-
maximumFileSizeToCacheInBytes: 10485760,
139+
maximumFileSizeToCacheInBytes: 5242880,
143140
runtimeCaching: [
144141
{
145142
urlPattern: /api\/v1\/sandboxes/,
@@ -164,10 +161,12 @@ module.exports = merge(commonConfig, {
164161
{
165162
// These should be dynamic, since it's not loaded from this domain
166163
// But from the root domain
167-
urlPattern: /codesandbox\.io\/static\/js\/(vendor|common|sandbox)/,
168-
handler: 'networkFirst',
164+
urlPattern: /codesandbox\.io\/static\/js\//,
165+
handler: 'fastest',
169166
options: {
170167
cache: {
168+
// A day
169+
maxAgeSeconds: 60 * 60 * 24,
171170
name: 'static-root-cache',
172171
},
173172
},
@@ -184,33 +183,34 @@ module.exports = merge(commonConfig, {
184183
},
185184
},
186185
{
187-
urlPattern: /webpack-dll-prod\.herokuapp\.com/,
186+
urlPattern: /https:\/\/d1jyvh0kxilfa7\.cloudfront\.net/,
188187
handler: 'fastest',
189188
options: {
190189
cache: {
191-
maxEntries: 100,
192-
maxAgeSeconds: 60 * 60 * 24,
193-
name: 'packager-cache',
190+
maxAgeSeconds: 60 * 60 * 24 * 7,
191+
name: 'dependency-files-cache',
194192
},
195193
},
196194
},
197195
{
198-
urlPattern: /https:\/\/d1jyvh0kxilfa7\.cloudfront\.net/,
199-
handler: 'fastest',
196+
urlPattern: /^https:\/\/unpkg\.com/,
197+
handler: 'cacheFirst',
200198
options: {
201199
cache: {
202-
maxEntries: 200,
203-
name: 'dependency-files-cache',
200+
maxEntries: 300,
201+
name: 'unpkg-dep-cache',
202+
maxAgeSeconds: 60 * 60 * 24 * 7,
204203
},
205204
},
206205
},
207206
{
208-
urlPattern: /^https:\/\/unpkg\.com/,
207+
urlPattern: /jsdelivr\.(com|net)/,
209208
handler: 'cacheFirst',
210209
options: {
211210
cache: {
212211
maxEntries: 300,
213-
name: 'unpkg-dep-cache',
212+
name: 'jsdelivr-dep-cache',
213+
maxAgeSeconds: 60 * 60 * 24 * 7,
214214
},
215215
},
216216
},
@@ -219,7 +219,9 @@ module.exports = merge(commonConfig, {
219219
handler: 'cacheFirst',
220220
options: {
221221
cache: {
222+
maxEntries: 50,
222223
name: 'cloudflare-cache',
224+
maxAgeSeconds: 60 * 60 * 24 * 7,
223225
},
224226
},
225227
},

packages/app/src/app/components/CodeEditor/Monaco/index.js

Lines changed: 43 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,11 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
211211

212212
requestAnimationFrame(() => {
213213
this.setupWorkers();
214-
editor.onDidChangeModelContent(({ changes }) => {
214+
editor.onDidChangeModelContent(e => {
215215
const { isLive, sendTransforms } = this.props;
216216

217217
if (isLive && sendTransforms && !this.receivingCode) {
218-
this.addChangesOperation(changes);
218+
this.sendChangeOperations(e);
219219
}
220220

221221
this.handleChange();
@@ -418,6 +418,7 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
418418
// that the changes of code are not sent to live users. We need to reset
419419
// this state when we're doing changing modules
420420
this.props.onCodeReceived();
421+
this.updatedCode = '';
421422
}
422423
});
423424
};
@@ -428,99 +429,53 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
428429
}
429430
};
430431

431-
changes = { moduleShortid: null, code: '', changes: [] };
432-
/**
433-
* Throttle the changes and handle them after a desired amount of time as one array of changes
434-
*/
435-
addChangesOperation = (changes: Array<any>) => {
436-
// Module changed in the meantime
437-
if (
438-
this.changes.moduleShortid &&
439-
this.changes.moduleShortid !== this.currentModule.shortid
440-
) {
441-
this.sendChangeOperations();
442-
}
443-
444-
if (!this.changes.code) {
445-
this.changes.code = this.currentModule.code || '';
446-
}
447-
if (!this.changes.moduleShortid) {
448-
this.changes.moduleShortid = this.currentModule.shortid;
449-
}
450-
451-
changes.forEach(change => {
452-
this.changes.changes.push(change);
453-
});
454-
455-
this.sendChangeOperations();
456-
};
457-
458-
sendChangeOperations = (retry: boolean = false) => {
432+
updatedCode = '';
433+
sendChangeOperations = changeEvent => {
459434
const { sendTransforms, isLive, onCodeReceived } = this.props;
460435

461-
try {
462-
if (
463-
sendTransforms &&
464-
this.changes.changes &&
465-
this.changes.moduleShortid === this.currentModule.shortid
466-
) {
467-
let code = this.changes.code;
468-
const t = this.changes.changes
469-
.map(change => {
470-
const startPos = change.range.getStartPosition();
471-
const lines = code.split('\n');
472-
const totalLength = code.length;
473-
let index = lineAndColumnToIndex(
474-
lines,
475-
startPos.lineNumber,
476-
startPos.column
477-
);
478-
479-
const operation = new TextOperation();
480-
if (index) {
481-
operation.retain(index);
482-
}
483-
484-
if (change.rangeLength > 0) {
485-
// Deletion
486-
operation.delete(change.rangeLength);
487-
488-
index += change.rangeLength;
489-
}
490-
if (change.text) {
491-
// Insertion
492-
operation.insert(change.text);
493-
}
494-
495-
operation.retain(Math.max(0, totalLength - index));
496-
497-
if (this.changes.changes.length > 1) {
498-
code = operation.apply(code);
499-
}
436+
if (sendTransforms && changeEvent.changes) {
437+
const otOperation = new TextOperation();
438+
// TODO: add a comment explaining what "delta" is
439+
let delta = 0;
440+
this.updatedCode = this.updatedCode || this.currentModule.code || '';
441+
// eslint-disable-next-line no-restricted-syntax
442+
for (const change of [...changeEvent.changes].reverse()) {
443+
const cursorStartOffset =
444+
lineAndColumnToIndex(
445+
this.updatedCode.split('\n'),
446+
change.range.startLineNumber,
447+
change.range.startColumn
448+
) + delta;
449+
450+
const retain = cursorStartOffset - otOperation.targetLength;
451+
if (retain > 0) {
452+
otOperation.retain(retain);
453+
}
500454

501-
return operation;
502-
})
503-
.reduce((prev, next) => prev.compose(next));
455+
if (change.rangeLength > 0) {
456+
otOperation.delete(change.rangeLength);
457+
delta -= change.rangeLength;
458+
}
504459

505-
sendTransforms(t);
506-
} else if (!isLive && onCodeReceived) {
507-
onCodeReceived();
460+
if (change.text) {
461+
otOperation.insert(change.text);
462+
delta += change.text.length;
463+
}
508464
}
509-
this.changes = { moduleShortid: null, code: '', changes: [] };
510-
} catch (e) {
511-
if (retry) {
512-
throw e;
465+
466+
const remaining = this.updatedCode.length - otOperation.baseLength;
467+
if (remaining > 0) {
468+
otOperation.retain(remaining);
513469
}
470+
this.updatedCode = otOperation.apply(this.updatedCode);
514471

515-
console.error(e);
516-
// This can happen on undo, Monaco sends a huge list of operations
517-
// that all apply to the same code and causes the `compose` function
518-
// to throw. The solution is to wait for the new code and try again. That's why
519-
// we call this function again in a timeout
472+
sendTransforms(otOperation);
520473

521-
setTimeout(() => {
522-
this.sendChangeOperations(true);
523-
}, 10);
474+
requestAnimationFrame(() => {
475+
this.updatedCode = '';
476+
});
477+
} else if (!isLive && onCodeReceived) {
478+
onCodeReceived();
524479
}
525480
};
526481

@@ -748,6 +703,7 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
748703
};
749704

750705
applyOperation = (operation: any) => {
706+
this.updatedCode = '';
751707
let index = 0;
752708
for (let i = 0; i < operation.ops.length; i++) {
753709
const op = operation.ops[i];

packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveButton.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,12 @@ export default class LiveButton extends React.PureComponent {
7979
};
8080

8181
render() {
82-
const { onClick, isLoading } = this.props;
82+
const {
83+
onClick,
84+
isLoading,
85+
showIcon = true,
86+
message = 'Go Live',
87+
} = this.props;
8388

8489
if (isLoading) {
8590
return <LoadingDiv>Creating Session</LoadingDiv>;
@@ -91,8 +96,12 @@ export default class LiveButton extends React.PureComponent {
9196
onMouseLeave={this.stopHovering}
9297
onClick={onClick}
9398
>
94-
<AnimatedRecordIcon style={{ opacity: this.state.showIcon ? 1 : 0 }} />{' '}
95-
Go Live
99+
{showIcon && (
100+
<AnimatedRecordIcon
101+
style={{ opacity: this.state.showIcon ? 1 : 0 }}
102+
/>
103+
)}{' '}
104+
{message}
96105
</Button>
97106
);
98107
}

packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/LiveInfo.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
77
import Input from 'common/components/Input';
88
import Margin from 'common/components/spacing/Margin';
99
import delay from 'common/utils/animation/delay-effect';
10+
import Switch from 'common/components/Switch';
1011

1112
import User from './User';
1213
import Countdown from './Countdown';
14+
import LiveButton from './LiveButton';
1315

14-
import { Description } from '../../elements';
16+
import { Description, WorkspaceInputContainer } from '../../elements';
1517

1618
const Container = styled.div`
1719
${delay()};
@@ -110,6 +112,20 @@ const ModeSelector = styled.div`
110112
transform: translateY(${props => props.i * 55}px);
111113
`;
112114

115+
const PreferencesContainer = styled.div`
116+
margin: 1rem;
117+
display: flex;
118+
`;
119+
120+
const Preference = styled.div`
121+
flex: 1;
122+
font-weight: 400;
123+
color: rgba(255, 255, 255, 0.8);
124+
align-items: center;
125+
justify-content: center;
126+
font-size: 0.875rem;
127+
`;
128+
113129
class LiveInfo extends React.Component {
114130
select = e => {
115131
e.target.select();
@@ -125,6 +141,9 @@ class LiveInfo extends React.Component {
125141
removeEditor,
126142
currentUserId,
127143
reconnecting,
144+
onSessionCloseClicked,
145+
notificationsHidden,
146+
toggleNotificationsHidden,
128147
} = this.props;
129148

130149
const owner = roomInfo.users.find(u => u.id === ownerId);
@@ -168,6 +187,31 @@ class LiveInfo extends React.Component {
168187
value={`https://codesandbox.io/live/${roomInfo.roomId}`}
169188
/>
170189

190+
{isOwner && (
191+
<WorkspaceInputContainer>
192+
<LiveButton
193+
message="Stop Session"
194+
onClick={onSessionCloseClicked}
195+
showIcon={false}
196+
/>
197+
</WorkspaceInputContainer>
198+
)}
199+
200+
<Margin top={1}>
201+
<SubTitle>Preferences</SubTitle>
202+
203+
<PreferencesContainer>
204+
<Preference>Hide notifications</Preference>
205+
<Switch
206+
right={notificationsHidden}
207+
onClick={toggleNotificationsHidden}
208+
small
209+
offMode
210+
secondary
211+
/>
212+
</PreferencesContainer>
213+
</Margin>
214+
171215
<Margin top={1}>
172216
<SubTitle>Live Mode</SubTitle>
173217
<ModeSelect>

packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Live/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const Live = ({ signals, store }) => (
2424
ownerId={store.live.roomInfo.ownerId}
2525
currentUserId={store.user.id}
2626
reconnecting={store.live.reconnecting}
27+
onSessionCloseClicked={signals.live.onSessionCloseClicked}
28+
notificationsHidden={store.live.notificationsHidden}
29+
toggleNotificationsHidden={signals.live.onToggleNotificationsHidden}
2730
/>
2831
) : (
2932
<React.Fragment>

packages/app/src/app/store/modules/editor/actions.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,11 @@ export function prettifyCode({ utils, state, props, path }) {
298298
}
299299

300300
return utils
301-
.prettify(moduleToPrettify.title, moduleToPrettify.code, config)
301+
.prettify(
302+
moduleToPrettify.title,
303+
() => (moduleToPrettify ? moduleToPrettify.code : ''),
304+
config
305+
)
302306
.then(newCode => path.success({ code: newCode }))
303307
.catch(error => path.error({ error }));
304308
}

0 commit comments

Comments
 (0)