Skip to content

Commit

Permalink
fix(collaboration): fix debouncing of onSendableReceived handler (#…
Browse files Browse the repository at this point in the history
…1727)

* chore(core): expose the return type of the throttle and debounce helpers

* fix(collaboration): fix the debounce of onSendableReceived handler so it works as intended

* feat(collaboration): add the ability to cancel and flush debounced sendable steps

* test(collaboration): add tests for debounced onSendableReceived handler
  • Loading branch information
whawker committed Jun 17, 2022
1 parent d514b9b commit 35c0098
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-eyes-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@remirror/core-helpers': patch
---

Expose the return type of the throttle and debounce helpers
7 changes: 7 additions & 0 deletions .changeset/hip-hounds-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@remirror/extension-collaboration': minor
---

Fix `onSendableReceived` handler so it is actually debounced as intended.

Add two new commands `cancelSendableSteps` and `flushSendableSteps` which more control over the debounced functionality
4 changes: 4 additions & 0 deletions packages/remirror__core-helpers/src/core-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,5 +1053,9 @@ export {
snakeCase,
spaceCase,
} from 'case-anything';
export type {
debounce as DebouncedFunction,
throttle as ThrottledFunction,
} from 'throttle-debounce';
export { debounce, throttle } from 'throttle-debounce';
export { omit, pick };
Original file line number Diff line number Diff line change
@@ -1,5 +1,134 @@
import { extensionValidityTest } from 'jest-remirror';
import { extensionValidityTest, renderEditor } from 'jest-remirror';

import { CollaborationExtension } from '../';
import { CollaborationExtension, CollaborationOptions } from '../';

extensionValidityTest(CollaborationExtension, { clientID: 'abc' });

jest.useFakeTimers('modern');

function create(options: Partial<CollaborationOptions> = {}) {
const collabExtension = new CollaborationExtension({
clientID: 1,
...options,
});

if (options.onSendableReceived) {
collabExtension.addHandler('onSendableReceived', options.onSendableReceived);
}

return renderEditor([collabExtension]);
}

describe('getSendableSteps', () => {
afterAll(() => {
jest.useRealTimers();
});

const expectedSteps = [
{
from: 0,
slice: {
content: [
{
content: [
{
text: 'Initial state',
type: 'text',
},
],
type: 'paragraph',
},
],
},
stepType: 'replace',
to: 2,
},
{
from: 14,
slice: {
content: [
{
text: ' update 1',
type: 'text',
},
],
},
stepType: 'replace',
to: 14,
},
{
from: 23,
slice: {
content: [
{
text: ' update 2',
type: 'text',
},
],
},
stepType: 'replace',
to: 23,
},
];

it('should debounce calls to the onSendableReceived handler', () => {
const handleSendableReceived = jest.fn();

const {
nodes: { doc, p },
add,
commands,
} = create({ onSendableReceived: handleSendableReceived });

add(doc(p('Initial state<cursor>')));

jest.advanceTimersByTime(100);
commands.insertText(' update 1');

jest.advanceTimersByTime(100);
commands.insertText(' update 2');

jest.advanceTimersByTime(1000);

expect(handleSendableReceived).toHaveBeenCalledOnce();
expect(handleSendableReceived).toHaveBeenCalledWith({
sendable: expect.any(Object),
jsonSendable: {
clientID: 1,
version: 0,
steps: expectedSteps,
},
});
});

it('should allow me to flush the steps on demand', () => {
const handleSendableReceived = jest.fn();

const {
nodes: { doc, p },
add,
commands,
} = create({ onSendableReceived: handleSendableReceived });

add(doc(p('Initial state<cursor>')));

jest.advanceTimersByTime(100);
commands.insertText(' update 1');

jest.advanceTimersByTime(100);
commands.insertText(' update 2');

expect(handleSendableReceived).not.toHaveBeenCalled();

commands.flushSendableSteps();
expect(handleSendableReceived).toHaveBeenCalledOnce();
expect(handleSendableReceived).toHaveBeenCalledWith({
sendable: expect.any(Object),
jsonSendable: {
clientID: 1,
version: 0,
steps: expectedSteps,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
command,
CommandFunction,
debounce,
DebouncedFunction,
EditorState,
extension,
Handler,
Expand Down Expand Up @@ -39,8 +40,10 @@ export class CollaborationExtension extends PlainExtension<CollaborationOptions>
return 'collaboration' as const;
}

private _getSendableSteps?: DebouncedFunction<(state: EditorState) => void>;

protected init(): void {
this.getSendableSteps = debounce(this.options.debounceMs, this.getSendableSteps);
this._getSendableSteps = debounce(this.options.debounceMs, this.getSendableSteps.bind(this));
}

/**
Expand Down Expand Up @@ -73,6 +76,23 @@ export class CollaborationExtension extends PlainExtension<CollaborationOptions>
};
}

@command()
cancelSendableSteps(): CommandFunction {
return () => {
this._getSendableSteps?.cancel();
return true;
};
}

@command()
flushSendableSteps(): CommandFunction {
return ({ state }) => {
this._getSendableSteps?.cancel();
this.getSendableSteps(state);
return true;
};
}

createExternalPlugins(): ProsemirrorPlugin[] {
const { version, clientID } = this.options;

Expand All @@ -85,14 +105,18 @@ export class CollaborationExtension extends PlainExtension<CollaborationOptions>
}

onStateUpdate(props: StateUpdateLifecycleProps): void {
this.getSendableSteps(props.state);
this._getSendableSteps?.(props.state);
}

onDestroy(): void {
this.store.commands.flushSendableSteps();
}

/**
* This passes the sendable steps into the `onSendableReceived` handler defined in the
* options when there is something to send.
*/
private getSendableSteps = (state: EditorState) => {
private getSendableSteps(state: EditorState) {
const sendable = sendableSteps(state);

if (sendable) {
Expand All @@ -103,7 +127,7 @@ export class CollaborationExtension extends PlainExtension<CollaborationOptions>
};
this.options.onSendableReceived({ sendable, jsonSendable });
}
};
}
}

export interface Sendable {
Expand Down Expand Up @@ -168,11 +192,15 @@ export interface CollaborationOptions {
onSendableReceived: Handler<(props: OnSendableReceivedProps) => void>;
}

export interface StepWithClientId extends Step {
clientID: number | string;
}

export type CollaborationAttributes = ProsemirrorAttributes<{
/**
* TODO give this some better types
* The steps to confirm, combined with the clientID of the user who created the change
*/
steps: any[];
steps: StepWithClientId[];

/**
* The version of the document that these steps were added to.
Expand Down

0 comments on commit 35c0098

Please sign in to comment.