Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.

Commit 19851f5

Browse files
Goamandmo-odoo
authored andcommitted
[IMP] Allow concurency for DomLayout - DomEditable
1 parent c03da81 commit 19851f5

File tree

14 files changed

+305
-203
lines changed

14 files changed

+305
-203
lines changed

packages/core/src/ContextManager.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import JWEditor, { Commands, CommandParamsType } from './JWEditor';
1+
import JWEditor from './JWEditor';
22
import { VRange } from './VRange';
33
import { Predicate, VNode } from './VNodes/VNode';
4-
import { JWPlugin } from './JWPlugin';
4+
import { ExecutionContext } from './JWEditor';
55

6-
export interface Context {
6+
export interface Context extends Partial<ExecutionContext> {
77
range?: VRange;
8-
execCommand: <P extends JWPlugin, C extends Commands<P> = Commands<P>>(
9-
commandName: C | (() => Promise<void> | void),
10-
params?: CommandParamsType<P, C>,
11-
) => Promise<void>;
128
}
13-
149
export interface CheckingContext extends Context {
1510
selector?: VNode[];
1611
}

packages/core/src/Dispatcher.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export class Dispatcher {
5252

5353
await this.dispatchHooks(commandId, args);
5454

55+
if (params.context?.range?.temporary) {
56+
params.context.range.remove();
57+
}
58+
5559
return result;
5660
}
5761
}

packages/core/src/JWEditor.ts

Lines changed: 118 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ export interface CommitParams extends CommandParams {
4040
commandNames: string[];
4141
}
4242

43+
export type ExecCommandFunction = <P extends JWPlugin, C extends Commands<P> = Commands<P>>(
44+
commandName: C | ((context: Context) => Promise<void> | void),
45+
params?: CommandParamsType<P, C>,
46+
) => Promise<void>;
47+
48+
export interface ExecutionContext {
49+
execCommand: ExecCommandFunction;
50+
}
51+
4352
export interface JWEditorConfig {
4453
/**
4554
* The modes that the editor will support.
@@ -322,6 +331,12 @@ export class JWEditor {
322331
}
323332
}
324333

334+
async nextEventMutex(next: (execCommand: ExecCommandFunction) => void): Promise<void> {
335+
return (this._mutex = this._mutex.then(
336+
next.bind(undefined, this._withOpenMemory.bind(this)),
337+
));
338+
}
339+
325340
/**
326341
* Execute arbitrary code in `callback`, then dispatch the commit event.
327342
* The callback receives an `execCommand` parameter which it can use to
@@ -355,103 +370,9 @@ export class JWEditor {
355370
commandName: C | ((context: Context) => Promise<void> | void),
356371
params?: CommandParamsType<P, C>,
357372
): Promise<void> {
358-
this._mutex = this._mutex.then(async () => {
359-
if (!this.memory.isFrozen()) {
360-
console.error(
361-
'You are trying to call the external editor' +
362-
' execCommand method from within an execCommand. ' +
363-
'Use the `execCommand` method of your plugin instead.',
364-
);
365-
return;
366-
}
367-
const timeout = setTimeout(() => {
368-
console.warn(
369-
'An execCommand call is taking more than 10 seconds to finish. It might be caused by a deadlock.\n' +
370-
'Verify that you do not call editor.execCommand inside another editor.execCommand.',
371-
);
372-
}, 10000);
373-
374-
// Switch to the next memory slice (unfreeze the memory).
375-
const origin = this.memory.sliceKey;
376-
const memorySlice = this._memoryID.toString();
377-
this.memory.switchTo(memorySlice);
378-
this.memoryInfo.commandNames = new VersionableArray();
379-
this.memoryInfo.uiCommand = false;
380-
let commandNames = this.memoryInfo.commandNames;
381-
382-
try {
383-
// Execute command.
384-
if (typeof commandName === 'function') {
385-
const name = '@custom' + (commandName.name ? ':' + commandName.name : '');
386-
this.memoryInfo.commandNames.push(name);
387-
await commandName(this.contextManager.defaultContext);
388-
if (this.memory.sliceKey !== memorySlice) {
389-
// Override by the current commandName if the slice changed.
390-
commandNames = [name];
391-
}
392-
} else {
393-
this.memoryInfo.commandNames.push(commandName);
394-
await this.dispatcher.dispatch(commandName, params);
395-
if (this.memory.sliceKey !== memorySlice) {
396-
// Override by the current commandName if the slice changed.
397-
commandNames = [commandName];
398-
}
399-
}
400-
401-
// Prepare nex slice and freeze the memory.
402-
this._memoryID++;
403-
const nextMemorySlice = this._memoryID.toString();
404-
this.memory.create(nextMemorySlice);
405-
406-
// Send the commit message with a frozen memory.
407-
const changesLocations = this.memory.getChangesLocations(
408-
memorySlice,
409-
this.memory.sliceKey,
410-
);
411-
await this.dispatcher.dispatchHooks('@commit', {
412-
changesLocations: changesLocations,
413-
commandNames: commandNames,
414-
});
415-
clearTimeout(timeout);
416-
} catch (error) {
417-
clearTimeout(timeout);
418-
if (this._stage !== EditorStage.EDITION) {
419-
throw error;
420-
}
421-
console.error(error);
422-
423-
await this.dispatcher.dispatchHooks('@error', {
424-
message: error.message,
425-
stack: error.stack,
426-
});
427-
428-
const failedSlice = this.memory.sliceKey;
429-
430-
// When an error occurs, we go back to part of the functional memory.
431-
this.memory.switchTo(origin);
432-
433-
try {
434-
// Send the commit message with a frozen memory.
435-
const changesLocations = this.memory.getChangesLocations(failedSlice, origin);
436-
await this.dispatcher.dispatchHooks('@commit', {
437-
changesLocations: changesLocations,
438-
commandNames: commandNames,
439-
});
440-
} catch (revertError) {
441-
if (this._stage !== EditorStage.EDITION) {
442-
throw revertError;
443-
}
444-
445-
await this.dispatcher.dispatchHooks('@error', {
446-
message: error.message,
447-
stack: error.stack,
448-
});
449-
450-
console.error(revertError);
451-
}
452-
}
373+
return this.nextEventMutex(async () => {
374+
return this._withOpenMemory(commandName, params);
453375
});
454-
return this._mutex;
455376
}
456377

457378
/**
@@ -492,13 +413,13 @@ export class JWEditor {
492413
this.memoryInfo.commandNames.push('@withRange');
493414
let range: VRange;
494415
if (typeof commandName === 'function') {
495-
range = new VRange(this, bounds, params as Mode);
416+
range = new VRange(this, bounds, { mode: params as Mode });
496417
this.memoryInfo.commandNames.push(
497418
'@custom' + (commandName.name ? ':' + commandName.name : ''),
498419
);
499420
await commandName({ ...this.contextManager.defaultContext, range });
500421
} else {
501-
range = new VRange(this, bounds, mode);
422+
range = new VRange(this, bounds, { mode });
502423
this.memoryInfo.commandNames.push(commandName);
503424
const newParam = Object.assign({ context: {} }, params as CommandParamsType<P, C>);
504425
newParam.context.range = range;
@@ -538,6 +459,105 @@ export class JWEditor {
538459
this._stage = EditorStage.CONFIGURATION;
539460
}
540461

462+
private async _withOpenMemory<P extends JWPlugin, C extends Commands<P> = Commands<P>>(
463+
commandName: C | ((context: Context) => Promise<void> | void),
464+
params?: CommandParamsType<P, C>,
465+
): Promise<void> {
466+
if (!this.memory.isFrozen()) {
467+
console.error(
468+
'You are trying to call the external editor' +
469+
' execCommand method from within an execCommand. ' +
470+
'Use the `execCommand` method of your plugin instead.',
471+
);
472+
return;
473+
}
474+
const timeout = setTimeout(() => {
475+
console.warn(
476+
'An execCommand call is taking more than 10 seconds to finish. It might be caused by a deadlock.\n' +
477+
'Verify that you do not call editor.execCommand inside another editor.execCommand.',
478+
);
479+
}, 10000);
480+
481+
// Switch to the next memory slice (unfreeze the memory).
482+
const origin = this.memory.sliceKey;
483+
const memorySlice = this._memoryID.toString();
484+
this.memory.switchTo(memorySlice);
485+
this.memoryInfo.commandNames = new VersionableArray();
486+
this.memoryInfo.uiCommand = false;
487+
let commandNames = this.memoryInfo.commandNames;
488+
489+
try {
490+
// Execute command.
491+
if (typeof commandName === 'function') {
492+
const name = '@custom' + (commandName.name ? ':' + commandName.name : '');
493+
this.memoryInfo.commandNames.push(name);
494+
await commandName(this.contextManager.defaultContext);
495+
if (this.memory.sliceKey !== memorySlice) {
496+
// Override by the current commandName if the slice changed.
497+
commandNames = [name];
498+
}
499+
} else {
500+
this.memoryInfo.commandNames.push(commandName);
501+
await this.dispatcher.dispatch(commandName, params);
502+
if (this.memory.sliceKey !== memorySlice) {
503+
// Override by the current commandName if the slice changed.
504+
commandNames = [commandName];
505+
}
506+
}
507+
508+
// Prepare nex slice and freeze the memory.
509+
this._memoryID++;
510+
const nextMemorySlice = this._memoryID.toString();
511+
this.memory.create(nextMemorySlice);
512+
513+
// Send the commit message with a frozen memory.
514+
const changesLocations = this.memory.getChangesLocations(
515+
memorySlice,
516+
this.memory.sliceKey,
517+
);
518+
await this.dispatcher.dispatchHooks('@commit', {
519+
changesLocations: changesLocations,
520+
commandNames: commandNames,
521+
});
522+
clearTimeout(timeout);
523+
} catch (error) {
524+
clearTimeout(timeout);
525+
if (this._stage !== EditorStage.EDITION) {
526+
throw error;
527+
}
528+
console.error(error);
529+
530+
await this.dispatcher.dispatchHooks('@error', {
531+
message: error.message,
532+
stack: error.stack,
533+
});
534+
535+
const failedSlice = this.memory.sliceKey;
536+
537+
// When an error occurs, we go back to part of the functional memory.
538+
this.memory.switchTo(origin);
539+
540+
try {
541+
// Send the commit message with a frozen memory.
542+
const changesLocations = this.memory.getChangesLocations(failedSlice, origin);
543+
await this.dispatcher.dispatchHooks('@commit', {
544+
changesLocations: changesLocations,
545+
commandNames: commandNames,
546+
});
547+
} catch (revertError) {
548+
if (this._stage !== EditorStage.EDITION) {
549+
throw revertError;
550+
}
551+
552+
await this.dispatcher.dispatchHooks('@error', {
553+
message: error.message,
554+
stack: error.stack,
555+
});
556+
557+
console.error(revertError);
558+
}
559+
}
560+
}
541561
/**
542562
* Execute the command or arbitrary code in `callback` in memory.
543563
*

packages/core/src/VRange.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@ import { TableCellNode } from '../../plugin-table/src/TableCellNode';
99
import { Mode, RuleProperty } from './Mode';
1010
import JWEditor from './JWEditor';
1111

12+
interface VRangeOptions {
13+
temporary?: boolean;
14+
mode?: Mode;
15+
}
16+
1217
export class VRange {
1318
readonly start = new MarkerNode();
1419
readonly end = new MarkerNode();
20+
/**
21+
* Mark the range as being temporary to signal the system to remove it
22+
* when necessary.
23+
*/
24+
temporary: boolean;
25+
private readonly _mode: Mode | undefined;
1526

1627
/**
1728
* Return the context of a collapsed range at the given location, targetting
@@ -75,8 +86,10 @@ export class VRange {
7586
constructor(
7687
public readonly editor: JWEditor,
7788
boundaryPoints?: [Point, Point],
78-
private readonly _mode?: Mode,
89+
options: VRangeOptions = {},
7990
) {
91+
this.temporary = !!options.temporary;
92+
this._mode = options.mode;
8093
// If a range context is given, adapt this range to match it.
8194
if (boundaryPoints) {
8295
const [start, end] = boundaryPoints;

packages/core/test/JWeditor.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { StageError } from '../../utils/src/errors';
55
import { testEditor } from '../../utils/src/testUtils';
66
import { BasicEditor } from '../../bundle-basic-editor/BasicEditor';
77
import { Layout } from '../../plugin-layout/src/Layout';
8+
import { VRange } from '../src/VRange';
9+
import { Char } from '../../plugin-char/src/Char';
810

911
describe('core', () => {
1012
describe('JWEditor', () => {
@@ -229,7 +231,7 @@ describe('core', () => {
229231
});
230232
});
231233
});
232-
describe('execCustomCommand', () => {
234+
describe('execCommand', () => {
233235
it('should execute a custom command and trigger plugins', async () => {
234236
await testEditor(BasicEditor, {
235237
contentBefore: '<div>ab[]</div>',
@@ -243,6 +245,28 @@ describe('core', () => {
243245
contentAfter: '<div>b[]</div>',
244246
});
245247
});
248+
it('should remove a temporaryVRange after an execCommand', async () => {
249+
await testEditor(BasicEditor, {
250+
contentBefore: '<div>ab[]</div>',
251+
stepFunction: async editor => {
252+
await editor.execCommand(async context => {
253+
const layout = editor.plugins.get(Layout);
254+
const domLayout = layout.engines.dom;
255+
const divider = domLayout.components.editable[0].firstChild();
256+
const range = new VRange(editor, VRange.at(divider), {
257+
temporary: true,
258+
});
259+
expect(range.start.parent).to.equal(divider);
260+
await context.execCommand<Char>('insertText', {
261+
text: 'c',
262+
context: { range },
263+
});
264+
expect(range.start.parent).to.equal(undefined);
265+
});
266+
},
267+
contentAfter: '<div>cab[]</div>',
268+
});
269+
});
246270
});
247271
});
248272
});

0 commit comments

Comments
 (0)