Skip to content

Commit f6fc5ac

Browse files
authored
Add live typing and fix bug (codesandbox#3711)
1 parent 65d87b3 commit f6fc5ac

File tree

8 files changed

+93
-46
lines changed

8 files changed

+93
-46
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"license": "GPL-3.0",
1010
"workspaces": {
1111
"packages": [
12+
"packages/@types/ot",
1213
"packages/app",
1314
"packages/homepage",
1415
"packages/common",

packages/@types/ot/index.d.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
declare module 'ot' {
2+
export type SerializedTextOperation = (string | number)[];
3+
4+
class TextOperation {
5+
delete(length: number): TextOperation;
6+
insert(str: string): TextOperation;
7+
retain(length: number): TextOperation;
8+
9+
baseLength: number;
10+
targetLength: number;
11+
12+
apply(code: string): string;
13+
compose(operation: TextOperation): TextOperation;
14+
15+
static transform(left: TextOperation, right: TextOperation): TextOperation;
16+
static isRetain(operation: TextOperation): boolean;
17+
static isInsert(operation: TextOperation): boolean;
18+
static isDelete(operation: TextOperation): boolean;
19+
20+
static fromJSON(operation: SerializedTextOperation): TextOperation;
21+
toJSON(): SerializedTextOperation;
22+
}
23+
24+
export { TextOperation };
25+
}

packages/@types/ot/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "@types/ot",
3+
"version": "1.0.0",
4+
"description": "",
5+
"typings": "./index.d.ts",
6+
"scripts": {},
7+
"keywords": [],
8+
"author": "Ives van Hoorne",
9+
"license": "MIT"
10+
}

packages/app/src/app/overmind/effects/live/clients.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TextOperation } from 'ot';
12
import { logBreadcrumb } from '@codesandbox/common/lib/utils/analytics/sentry';
23
import { Blocker, blocker } from 'app/utils/blocker';
34

@@ -6,10 +7,13 @@ import { OTClient, synchronized_ } from './ot/client';
67
export type SendOperation = (
78
moduleShortid: string,
89
revision: number,
9-
operation: any
10+
operation: TextOperation
1011
) => Promise<unknown>;
1112

12-
export type ApplyOperation = (moduleShortid: string, operation: any) => void;
13+
export type ApplyOperation = (
14+
moduleShortid: string,
15+
operation: TextOperation
16+
) => void;
1317

1418
export class CodeSandboxOTClient extends OTClient {
1519
/*
@@ -19,14 +23,21 @@ export class CodeSandboxOTClient extends OTClient {
1923
*/
2024
public awaitSynchronized: Blocker<void> | null;
2125
moduleShortid: string;
22-
onSendOperation: (revision: number, operation: any) => Promise<unknown>;
23-
onApplyOperation: (operation: any) => void;
26+
onSendOperation: (
27+
revision: number,
28+
operation: TextOperation
29+
) => Promise<unknown>;
30+
31+
onApplyOperation: (operation: TextOperation) => void;
2432

2533
constructor(
2634
revision: number,
2735
moduleShortid: string,
28-
onSendOperation: (revision: number, operation: any) => Promise<unknown>,
29-
onApplyOperation: (operation: any) => void
36+
onSendOperation: (
37+
revision: number,
38+
operation: TextOperation
39+
) => Promise<unknown>,
40+
onApplyOperation: (operation: TextOperation) => void
3041
) {
3142
super(revision);
3243
this.moduleShortid = moduleShortid;
@@ -35,7 +46,7 @@ export class CodeSandboxOTClient extends OTClient {
3546
}
3647

3748
lastAcknowledgedRevision: number = -1;
38-
sendOperation(revision, operation) {
49+
sendOperation(revision: number, operation: TextOperation) {
3950
// Whenever we send an operation we enable the blocker
4051
// that lets us wait for its resolvment when moving back
4152
// to synchronized state
@@ -72,7 +83,7 @@ export class CodeSandboxOTClient extends OTClient {
7283
});
7384
}
7485

75-
applyOperation(operation) {
86+
applyOperation(operation: TextOperation) {
7687
this.onApplyOperation(operation);
7788
}
7889

@@ -93,11 +104,11 @@ export class CodeSandboxOTClient extends OTClient {
93104
}
94105
}
95106

96-
applyClient(operation: any) {
107+
applyClient(operation: TextOperation) {
97108
super.applyClient(operation);
98109
}
99110

100-
applyServer(operation: any) {
111+
applyServer(operation: TextOperation) {
101112
super.applyServer(operation);
102113
}
103114

packages/app/src/app/overmind/effects/live/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ import {
1111
import _debug from '@codesandbox/common/lib/utils/debug';
1212
import { Blocker, blocker } from 'app/utils/blocker';
1313
import { camelizeKeys } from 'humps';
14-
import { TextOperation } from 'ot';
14+
import { TextOperation, SerializedTextOperation } from 'ot';
1515
import { Channel, Presence, Socket } from 'phoenix';
1616
import uuid from 'uuid';
1717

1818
import { OPTIMISTIC_ID_PREFIX } from '../utils';
1919
import clientsFactory from './clients';
2020

2121
type Options = {
22-
onApplyOperation(args: { moduleShortid: string; operation: any }): void;
22+
onApplyOperation(args: {
23+
moduleShortid: string;
24+
operation: TextOperation;
25+
}): void;
2326
provideJwtToken(): string;
2427
isLiveBlockerExperiement(): boolean;
2528
onOperationError(payload: {
@@ -93,7 +96,11 @@ class Live {
9396
awaitSend.resolve();
9497
}
9598

96-
private onSendOperation = async (moduleShortid, revision, operation) => {
99+
private onSendOperation = async (
100+
moduleShortid: string,
101+
revision: number,
102+
operation: TextOperation
103+
) => {
97104
logBreadcrumb({
98105
type: 'ot',
99106
message: `Sending ${JSON.stringify({
@@ -332,7 +339,7 @@ class Live {
332339
});
333340
}
334341

335-
sendCodeUpdate(moduleShortid: string, operation: any) {
342+
sendCodeUpdate(moduleShortid: string, operation: TextOperation) {
336343
if (!operation) {
337344
return;
338345
}
@@ -465,13 +472,7 @@ class Live {
465472
return this.clients.getAll();
466473
}
467474

468-
applyClient(moduleShortid: string, operation: any) {
469-
return this.clients
470-
.get(moduleShortid)
471-
.applyClient(TextOperation.fromJSON(operation));
472-
}
473-
474-
applyServer(moduleShortid: string, operation: any) {
475+
applyServer(moduleShortid: string, operation: SerializedTextOperation) {
475476
return this.clients
476477
.get(moduleShortid)
477478
.applyServer(TextOperation.fromJSON(operation));

packages/app/src/app/overmind/effects/live/ot/client.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
22
/* eslint-disable react/no-access-state-in-setstate */
33

4-
type Operation = any;
4+
import { TextOperation } from 'ot';
55

66
interface IState {
7-
applyClient(client: OTClient, operation: Operation): IState;
8-
applyServer(client: OTClient, operation: Operation): IState;
7+
applyClient(client: OTClient, operation: TextOperation): IState;
8+
applyServer(client: OTClient, operation: TextOperation): IState;
99
serverAck(client: OTClient): IState;
1010
transformSelection<T>(selection: T): T;
1111
resend?(client: OTClient): void;
@@ -14,14 +14,14 @@ interface IState {
1414
// In the 'Synchronized' state, there is no pending operation that the client
1515
// has sent to the server.
1616
class Synchronized implements IState {
17-
applyClient(client: OTClient, operation: Operation) {
17+
applyClient(client: OTClient, operation: TextOperation) {
1818
// When the user makes an edit, send the operation to the server and
1919
// switch to the 'AwaitingConfirm' state
2020
client.sendOperation(client.revision, operation);
2121
return new AwaitingConfirm(operation);
2222
}
2323

24-
applyServer(client: OTClient, operation: Operation) {
24+
applyServer(client: OTClient, operation: TextOperation) {
2525
// When we receive a new operation from the server, the operation can be
2626
// simply applied to the current document
2727
client.applyOperation(operation);
@@ -44,20 +44,20 @@ export const synchronized_ = new Synchronized();
4444
// In the 'AwaitingConfirm' state, there's one operation the client has sent
4545
// to the server and is still waiting for an acknowledgement.
4646
class AwaitingConfirm implements IState {
47-
outstanding: Operation;
47+
outstanding: TextOperation;
4848

49-
constructor(outstanding: Operation) {
49+
constructor(outstanding: TextOperation) {
5050
// Save the pending operation
5151
this.outstanding = outstanding;
5252
}
5353

54-
applyClient(client: OTClient, operation: Operation) {
54+
applyClient(client: OTClient, operation: TextOperation) {
5555
// When the user makes an edit, don't send the operation immediately,
5656
// instead switch to 'AwaitingWithBuffer' state
5757
return new AwaitingWithBuffer(this.outstanding, operation);
5858
}
5959

60-
applyServer(client: OTClient, operation: Operation) {
60+
applyServer(client: OTClient, operation: TextOperation) {
6161
// This is another client's operation. Visualization:
6262
//
6363
// /\
@@ -68,7 +68,7 @@ class AwaitingConfirm implements IState {
6868
// (can be applied \/
6969
// to the client's
7070
// current document)
71-
const pair = operation.constructor.transform(this.outstanding, operation);
71+
const pair = TextOperation.transform(this.outstanding, operation);
7272
client.applyOperation(pair[1]);
7373
return new AwaitingConfirm(pair[0]);
7474
}
@@ -93,22 +93,22 @@ class AwaitingConfirm implements IState {
9393
// In the 'AwaitingWithBuffer' state, the client is waiting for an operation
9494
// to be acknowledged by the server while buffering the edits the user makes
9595
class AwaitingWithBuffer implements IState {
96-
outstanding: Operation;
97-
buffer: Operation;
96+
outstanding: TextOperation;
97+
buffer: TextOperation;
9898

99-
constructor(outstanding: Operation, buffer: Operation) {
99+
constructor(outstanding: TextOperation, buffer: TextOperation) {
100100
// Save the pending operation and the user's edits since then
101101
this.outstanding = outstanding;
102102
this.buffer = buffer;
103103
}
104104

105-
applyClient(client: OTClient, operation: Operation) {
105+
applyClient(client: OTClient, operation: TextOperation) {
106106
// Compose the user's changes onto the buffer
107107
const newBuffer = this.buffer.compose(operation);
108108
return new AwaitingWithBuffer(this.outstanding, newBuffer);
109109
}
110110

111-
applyServer(client: OTClient, operation: Operation) {
111+
applyServer(client: OTClient, operation: TextOperation) {
112112
// Operation comes from another client
113113
//
114114
// /\
@@ -126,7 +126,7 @@ class AwaitingWithBuffer implements IState {
126126
// document
127127
//
128128
// * pair1[1]
129-
const transform = operation.constructor.transform;
129+
const transform = TextOperation.transform;
130130
const pair1 = transform(this.outstanding, operation);
131131
const pair2 = transform(this.buffer, pair1[1]);
132132
client.applyOperation(pair2[1]);
@@ -165,12 +165,12 @@ export abstract class OTClient {
165165
}
166166

167167
// Call this method when the user changes the document.
168-
applyClient(operation: Operation) {
168+
applyClient(operation: TextOperation) {
169169
this.setState(this.state.applyClient(this, operation));
170170
}
171171

172172
// Call this method with a new operation from the server
173-
applyServer(operation: Operation) {
173+
applyServer(operation: TextOperation) {
174174
this.revision++;
175175
this.setState(this.state.applyServer(this, operation));
176176
}
@@ -196,7 +196,7 @@ export abstract class OTClient {
196196
return this.state.transformSelection(selection);
197197
}
198198

199-
abstract sendOperation(revision: number, operation: Operation): void;
199+
abstract sendOperation(revision: number, operation: TextOperation): void;
200200

201-
abstract applyOperation(operation: Operation): void;
201+
abstract applyOperation(operation: TextOperation): void;
202202
}

packages/app/src/app/overmind/namespaces/editor/actions.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ModuleTab,
99
WindowOrientation,
1010
} from '@codesandbox/common/lib/types';
11+
import { TextOperation } from 'ot';
1112
import { getTextOperation } from '@codesandbox/common/lib/utils/diff';
1213
import { COMMENTS } from '@codesandbox/common/lib/utils/feature-flags';
1314
import { convertTypeToStatus } from '@codesandbox/common/lib/utils/notifications';
@@ -321,13 +322,11 @@ export const codeChanged: Action<{
321322
// never want to send that code update, since this actual code change goes through this
322323
// specific code flow already.
323324
if (state.live.isLive && module.code !== code) {
324-
let operation;
325+
let operation: TextOperation;
325326
if (event) {
326327
operation = eventToTransform(event, module.code).operation;
327328
} else {
328-
const transform = getTextOperation(module.code, code);
329-
330-
operation = transform.operation;
329+
operation = getTextOperation(module.code, code);
331330
}
332331

333332
effects.live.sendCodeUpdate(moduleShortid, operation);
@@ -437,7 +436,7 @@ export const forkSandboxClicked: AsyncAction = async ({
437436
if (!state.editor.currentSandbox) {
438437
return;
439438
}
440-
439+
441440
await actions.editor.internal.forkSandbox({
442441
sandboxId: state.editor.currentSandbox.id,
443442
});

0 commit comments

Comments
 (0)