Skip to content

Commit d7d0102

Browse files
Store connected users in binding class
1 parent edd2151 commit d7d0102

File tree

4 files changed

+127
-121
lines changed

4 files changed

+127
-121
lines changed

source/sharedb-ace-binding.ts

Lines changed: 77 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,17 @@
55
* @license MIT
66
*/
77

8-
// TODO: support reconnects with same id
9-
10-
import { WebSocket } from 'partysocket';
118
import Logdown from 'logdown';
12-
import sharedb from 'sharedb/lib/sharedb';
139
import type {
1410
AceMultiCursorManager,
1511
AceMultiSelectionManager,
16-
AceRadarView,
17-
IRangeData
12+
AceRadarView
1813
} from '@convergencelabs/ace-collab-ext';
19-
import { IIndexRange } from '@convergencelabs/ace-collab-ext/dist/types/IndexRange';
2014
import { AceViewportUtil, AceRangeUtil } from '@convergencelabs/ace-collab-ext';
2115
import type { Ace, EditSession } from 'ace-builds';
2216
import type { IAceEditor } from 'react-ace/lib/types';
23-
import type { SharedbAcePlugin, SharedbAceUser } from './types';
17+
import sharedb from 'sharedb/lib/sharedb';
18+
import type { PresenceUpdate, SharedbAceUser } from './types';
2419

2520
function traverse(object: any, path: string[]) {
2621
for (const key of path) {
@@ -33,22 +28,12 @@ interface SharedbAceBindingOptions {
3328
ace: IAceEditor;
3429
doc: sharedb.Doc;
3530
user: SharedbAceUser;
36-
cursorManager: AceMultiCursorManager;
37-
selectionManager: AceMultiSelectionManager;
38-
radarManager: AceRadarView;
39-
usersPresence: sharedb.Presence;
40-
pluginWS?: WebSocket;
31+
cursorManager?: AceMultiCursorManager;
32+
selectionManager?: AceMultiSelectionManager;
33+
radarManager?: AceRadarView;
34+
usersPresence: sharedb.Presence<PresenceUpdate>;
4135
path: string[];
42-
plugins?: SharedbAcePlugin[];
43-
onError: (err: unknown) => unknown;
44-
}
45-
46-
interface PresenceUpdate {
47-
user: SharedbAceUser;
48-
cursorPos?: Ace.Point;
49-
selectionRange?: IRangeData[];
50-
radarViewRows?: IIndexRange;
51-
radarCursorRow?: number;
36+
onError?: (err: unknown) => unknown;
5237
}
5338

5439
class SharedbAceBinding {
@@ -60,11 +45,11 @@ class SharedbAceBinding {
6045

6146
path: string[];
6247

63-
cursorManager: AceMultiCursorManager;
64-
selectionManager: AceMultiSelectionManager;
65-
radarManager: AceRadarView;
48+
cursorManager?: AceMultiCursorManager;
49+
selectionManager?: AceMultiSelectionManager;
50+
radarManager?: AceRadarView;
6651

67-
usersPresence: sharedb.Presence;
52+
usersPresence: sharedb.Presence<PresenceUpdate>;
6853

6954
logger: Logdown.Logger;
7055

@@ -74,7 +59,18 @@ class SharedbAceBinding {
7459

7560
localPresence?: sharedb.LocalPresence<PresenceUpdate>;
7661

77-
onError: (err: unknown) => unknown;
62+
connectedUsers: Record<string, SharedbAceUser> = {};
63+
64+
docSubmitted: sharedb.Callback = (err) => {
65+
if (err) {
66+
this.onError?.(err);
67+
this.logger.log(`*local*: op error: ${err}`);
68+
} else {
69+
this.logger.log('*local*: op submitted');
70+
}
71+
};
72+
73+
onError?: (err: unknown) => unknown;
7874

7975
/**
8076
* Constructs the binding object.
@@ -93,11 +89,8 @@ class SharedbAceBinding {
9389
* the selections in the editor
9490
* @param {Object} options.usersPresence - ShareDB presence channel
9591
* containing information of the users, including cursor positions
96-
* @param {Object} options.pluginWS - WebSocket connection for
97-
* sharedb-ace plugins
9892
* @param {string[]} options.path - A lens, describing the nesting
9993
* to the JSON document. It should point to a string.
100-
* @param {Object[]} options.plugins - array of sharedb-ace plugins
10194
* @param {?function} options.onError - a callback on error
10295
* @example
10396
* const binding = new SharedbAceBinding({
@@ -108,8 +101,6 @@ class SharedbAceBinding {
108101
* selectionManager: selectionManager,
109102
* usersPresence: usersPresence,
110103
* path: ["path"],
111-
* plugins: [ SharedbAceMultipleCursors ],
112-
* pluginWS: "http://localhost:3108/ws",
113104
* })
114105
*/
115106
constructor(options: SharedbAceBindingOptions) {
@@ -125,12 +116,6 @@ class SharedbAceBinding {
125116
this.onError = options.onError;
126117
this.logger = Logdown('shareace');
127118

128-
// Initialize plugins
129-
if (options.pluginWS && options.plugins) {
130-
const { pluginWS } = options;
131-
options.plugins.forEach((plugin) => plugin(pluginWS, this.editor));
132-
}
133-
134119
// Set value of ace document to ShareDB document value
135120
this.setInitialValue();
136121

@@ -146,10 +131,12 @@ class SharedbAceBinding {
146131
this.session.setValue(traverse(this.doc.data, this.path));
147132
this.suppress = false;
148133

149-
this.cursorManager.removeAll();
150-
this.selectionManager.removeAll();
151-
// TODO: Remove all views for radarManager
152-
// this.radarManager.removeView();
134+
this.cursorManager?.removeAll();
135+
this.selectionManager?.removeAll();
136+
137+
// @ts-expect-error hotfix to remove all views in radarManager
138+
this.radarManager?.removeAllViews();
139+
153140
this.initializeLocalPresence();
154141
for (const [id, update] of Object.entries(this.usersPresence.remotePresences)) {
155142
this.updatePresence(id, update);
@@ -160,30 +147,35 @@ class SharedbAceBinding {
160147
* Listens to the changes
161148
*/
162149
listen = () => {
163-
// TODO: Also update view on window resize
164-
// TODO: Clicking on radar indicator is not exactly accurate
165-
this.session.on('change', this.onLocalChange);
166-
this.session.on('changeScrollTop', this.onLocalChangeScrollTop);
167150
this.doc.on('op', this.onRemoteChange);
168151
this.doc.on('load', this.onRemoteReload);
169152

153+
this.session.on('change', this.onLocalChange);
170154
this.usersPresence.on('receive', this.updatePresence);
171155
this.session.selection.on('changeCursor', this.onLocalCursorChange);
172156
this.session.selection.on('changeSelection', this.onLocalSelectionChange);
157+
158+
// Hotfix for clicking on radar indicator to update local presence
159+
// because editor.scrollToLine does not trigger changeScrollTop
160+
// Generates a decent amount of traffic but it's ok for now
161+
this.editor.renderer.on('afterRender', this.onLocalViewChange);
162+
163+
this.session.on('changeMode', this.onLocalModeChange);
173164
};
174165

175166
/**
176167
* Stop listening to changes
177168
*/
178169
unlisten = () => {
179-
this.session.removeListener('change', this.onLocalChange);
180-
this.session.off('changeScrollTop', this.onLocalChangeScrollTop);
181170
this.doc.off('op', this.onRemoteChange);
182171
this.doc.off('load', this.onRemoteReload);
183172

173+
this.session.removeListener('change', this.onLocalChange);
184174
this.usersPresence.off('receive', this.updatePresence);
185175
this.session.selection.off('changeCursor', this.onLocalCursorChange);
186176
this.session.selection.off('changeSelection', this.onLocalSelectionChange);
177+
this.editor.renderer.off('afterRender', this.onLocalViewChange);
178+
this.session.off('changeMode', this.onLocalModeChange);
187179
};
188180

189181
/**
@@ -194,7 +186,6 @@ class SharedbAceBinding {
194186
* @throws {Error} throws error if delta is malformed
195187
*/
196188
deltaTransform = (delta: Ace.Delta): sharedb.Op => {
197-
// TODO: Use SubtypeOp to declare new operations
198189
const aceDoc = this.session.getDocument();
199190
const start = aceDoc.positionToIndex(delta.start);
200191
const end = aceDoc.positionToIndex(delta.end);
@@ -225,10 +216,9 @@ class SharedbAceBinding {
225216
* @throws {Error} throws error on malformed op
226217
*/
227218
opTransform = (ops: sharedb.Op[]): Ace.Delta[] => {
228-
const self = this;
229219
const opToDelta = (op: sharedb.Op): Ace.Delta => {
230220
const index = op.p.at(-1) as number;
231-
const pos = self.session.doc.indexToPosition(index, 0);
221+
const pos = this.session.doc.indexToPosition(index, 0);
232222
const start = pos;
233223
let action: 'remove' | 'insert';
234224
let lines: string[];
@@ -238,7 +228,7 @@ class SharedbAceBinding {
238228
action = 'remove';
239229
lines = op.sd.split('\n');
240230
const count = lines.reduce((total, line) => total + line.length, lines.length - 1);
241-
end = self.session.doc.indexToPosition(index + count, 0);
231+
end = this.session.doc.indexToPosition(index + count, 0);
242232
} else if ('si' in op) {
243233
action = 'insert';
244234
lines = op.si.split('\n');
@@ -287,35 +277,38 @@ class SharedbAceBinding {
287277
const op = this.deltaTransform(delta);
288278
this.logger.log(`*local*: transformed op: ${JSON.stringify(op)}`);
289279

290-
const docSubmitted: sharedb.Callback = (err) => {
291-
if (err) {
292-
this.onError && this.onError(err);
293-
this.logger.log(`*local*: op error: ${err}`);
294-
} else {
295-
this.logger.log('*local*: op submitted');
296-
}
297-
};
298-
299280
if (!this.doc.type) {
300281
// likely previous operation failed, we're out of sync
301282
// don't submitOp now
302283
return;
303284
}
304285

305-
this.doc.submitOp(op, { source: this }, docSubmitted);
286+
this.doc.submitOp(op, { source: this }, this.docSubmitted);
306287
} catch (err) {
307-
this.onError && this.onError(err);
288+
this.onError?.(err);
308289
}
309290
};
310291

292+
onLocalModeChange = () => {
293+
// TODO: This is wrong
294+
// @ts-ignore
295+
const modeString: string = this.session.getMode().$id;
296+
const mode = modeString.substring(modeString.lastIndexOf('/') + 1);
297+
298+
this.localPresence?.submit({
299+
user: this.user,
300+
newMode: mode
301+
});
302+
};
303+
311304
/**
312305
* Event Listener for remote events (ShareDB)
313306
*
314307
* @param {Object[]} ops - array of ShareDB ops
315308
* @param {Object} source - which sharedb-ace-binding instance
316309
* created the op. If self, don't apply the op.
317310
*/
318-
onRemoteChange = (ops: sharedb.Op[], source: this | false, clientId?: string) => {
311+
onRemoteChange = (ops: sharedb.Op[], source: this | false) => {
319312
try {
320313
this.logger.log(`*remote*: fired ${Date.now()}`);
321314

@@ -341,41 +334,46 @@ class SharedbAceBinding {
341334
this.logger.log(JSON.stringify(this.session.getValue()));
342335
this.logger.log('*remote*: delta applied');
343336
} catch (err) {
344-
this.onError && this.onError(err);
337+
this.onError?.(err);
345338
}
346339
};
347340

348341
updatePresence = (id: string, update: PresenceUpdate) => {
349342
// TODO: logger and error handling
350-
// TODO: separate into multiple handlers
351343
if (update === null) {
352344
try {
353-
this.cursorManager.removeCursor(id);
345+
this.cursorManager?.removeCursor(id);
354346
// eslint-disable-next-line no-empty
355347
} catch {}
356348

357349
try {
358-
this.selectionManager.removeSelection(id);
350+
this.selectionManager?.removeSelection(id);
359351
// eslint-disable-next-line no-empty
360352
} catch {}
361353

362354
try {
363-
this.radarManager.removeView(id);
355+
this.radarManager?.removeView(id);
364356
// eslint-disable-next-line no-empty
365357
} catch {}
366358

359+
if (id in this.connectedUsers) {
360+
delete this.connectedUsers[id];
361+
}
362+
367363
return;
368364
}
369365

370-
if (update.cursorPos) {
366+
this.connectedUsers[id] = update.user;
367+
368+
if (this.cursorManager && update.cursorPos) {
371369
try {
372370
this.cursorManager.setCursor(id, update.cursorPos);
373371
} catch {
374372
this.cursorManager.addCursor(id, update.user.name, update.user.color, update.cursorPos);
375373
}
376374
}
377375

378-
if (update.selectionRange) {
376+
if (this.selectionManager && update.selectionRange) {
379377
const ranges = AceRangeUtil.fromJson(update.selectionRange);
380378
try {
381379
this.selectionManager.setSelection(id, ranges);
@@ -384,7 +382,7 @@ class SharedbAceBinding {
384382
}
385383
}
386384

387-
if (update.radarViewRows) {
385+
if (this.radarManager && update.radarViewRows) {
388386
const rows = AceViewportUtil.indicesToRows(
389387
this.editor,
390388
update.radarViewRows.start,
@@ -402,9 +400,14 @@ class SharedbAceBinding {
402400
);
403401
}
404402
}
403+
404+
// TODO: This is not the right way
405+
if (update.newMode) {
406+
this.session.setMode(update.newMode);
407+
}
405408
};
406409

407-
onLocalChangeScrollTop = (scrollTop: number) => {
410+
onLocalViewChange = () => {
408411
// TODO: logger and error handling
409412
const viewportIndices = AceViewportUtil.getVisibleIndexRange(this.editor);
410413
this.localPresence?.submit({

0 commit comments

Comments
 (0)