Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bangle.dev/collab-client

Package Overview
Dependencies
Maintainers
1
Versions
112
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bangle.dev/collab-client - npm Package Compare versions

Comparing version 0.30.0-alpha.7 to 0.30.0-alpha.8

482

__tests__/collab-client.test.ts

@@ -12,2 +12,3 @@ /**

CollabRequestType,
CollabServerState,
ManagerRequest,

@@ -22,3 +23,4 @@ MAX_STEP_HISTORY,

import { plugins } from '../src/collab-extension';
import { onUpstreamChanges } from '../src/commands';
import { hardResetClient, onUpstreamChanges } from '../src/commands';
import { collabMonitorKey } from '../src/common';

@@ -34,4 +36,10 @@ waitForExpect.defaults.timeout = 500;

) => {
const { manager, docChangeEmitter } = server;
let ref: { manager?: CollabManager; docChangeEmitter?: Emitter } = {
manager: undefined,
docChangeEmitter: undefined,
};
ref.manager = server.manager;
ref.docChangeEmitter = server.docChangeEmitter;
const { editor, debugString } = renderTestEditor({

@@ -45,3 +53,5 @@ specRegistry: specRegistry,

retryWaitTime: 0,
sendManagerRequest: manager.handleRequest.bind(manager),
sendManagerRequest: (...args) => {
return ref.manager!.handleRequest(...args);
},
}),

@@ -61,6 +71,8 @@ ],

docChangeEmitter.on('doc_changed', ({ version }: { version: number }) => {
const cb = ({ version }: { version: number }) => {
onUpstreamChanges(version)(editor.view.state, editor.view.dispatch);
});
};
ref.docChangeEmitter.on('doc_changed', cb);
return {

@@ -73,2 +85,8 @@ docName: _docName,

typeText,
changeServer(server: ReturnType<typeof setupServer>) {
ref.docChangeEmitter?.off('doc_changed', cb);
ref.docChangeEmitter = server.docChangeEmitter;
ref.manager = server.manager;
ref.docChangeEmitter.on('doc_changed', cb);
},
};

@@ -78,2 +96,3 @@ };

const setupServer = ({
managerId = 'test-manager-id',
rawDoc = {

@@ -94,2 +113,3 @@ type: 'doc',

}: {
managerId?: string;
rawDoc?: {

@@ -100,2 +120,6 @@ type: 'doc';

} = {}) => {
let ref: { rawDoc: any } = {
rawDoc,
};
let emitDocChangeEvent = true;

@@ -106,11 +130,13 @@

const manager = new CollabManager({
managerId,
schema: specRegistry.schema,
getDoc: async (dName) => {
getInitialState: async (dName) => {
if (dName === docName) {
return specRegistry.schema.nodeFromJSON(rawDoc);
return new CollabServerState(
specRegistry.schema.nodeFromJSON(ref.rawDoc),
);
}
return undefined;
},
applyCollabState: (docName, newCollabState) => {
applyState: (docName, newCollabState) => {
queueMicrotask(() => {

@@ -221,2 +247,5 @@ if (emitDocChangeEvent) {

},
changeRawDoc(rawDoc: any) {
ref.rawDoc = rawDoc;
},
};

@@ -466,2 +495,61 @@ };

test('hard reset', async () => {
let server = setupServer({ managerId: 'manager-test-1' });
const client1 = setupClient(server, 'client1');
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('one');
client1.typeText('two');
client1.typeText('three');
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.version).toEqual(3);
});
server = setupServer({
managerId: 'manager-test-1',
rawDoc: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'reset bro!',
},
],
},
],
},
});
client1.changeServer(server);
hardResetClient()(client1.view.state, client1.view.dispatch);
// should be reset
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("reset bro!"))`);
});
expect(collabMonitorKey.getState(client1.view.state)?.serverVersion).toBe(
undefined,
);
// should continue to sync
client1.typeText('hi again');
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
`doc(paragraph("reset bro!hi again"))`,
);
});
expect(client1.debugString()).toEqual(`doc(paragraph("reset bro!hi again"))`);
expect(collabMonitorKey.getState(client1.view.state)?.serverVersion).toBe(1);
});
describe('failures', () => {

@@ -804,89 +892,323 @@ test('handles ApplyFailed', async () => {

});
});
test('local apply steps fails', async () => {
console.error = jest.fn();
const server = setupServer();
test('handles ManagerDestroyed', async () => {
let server = setupServer();
const client1 = setupClient(server, 'client1');
const client1 = setupClient(server, 'client1');
const client2 = setupClient(server, 'client2');
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
client1.typeText('wow ');
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
'doc(paragraph("wow hello world!"))',
);
});
const { manager } = server;
manager.destroy();
client1.typeText('bow ');
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
'doc(paragraph("wow hello world!"))',
);
});
expect(await server.getNotOkayRequests()).toEqual([
{
userId: 'user-client1',
result: {
body: CollabFail.ManagerDestroyed,
ok: false,
},
},
]);
expect(
(await server.getCalls()).filter((r) =>
r.payload.userId.includes('client1'),
),
).toEqual([
{
payload: expect.anything(),
type: 'CollabRequestType.GetDocument',
},
{
payload: expect.anything(),
type: 'CollabRequestType.PushEvents',
},
{
payload: expect.anything(),
type: 'CollabRequestType.PullEvents',
},
{
payload: expect.anything(),
type: 'CollabRequestType.PushEvents',
},
]);
server = setupServer();
client1.changeServer(server);
client1.typeText('pow ');
await sleep(10);
// should get invalid version since server was changed
expect(await server.getNotOkayRequests()).toEqual([
{
result: {
body: 'CollabFail.InvalidVersion',
ok: false,
},
userId: 'user-client1',
},
]);
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
'doc(paragraph("hello world!"))',
);
});
expect(
(await server.getCalls()).filter((r) =>
r.payload.userId.includes('client1'),
),
).toEqual([
{
payload: expect.anything(),
type: 'CollabRequestType.PullEvents',
},
{
payload: expect.anything(),
type: 'CollabRequestType.GetDocument',
},
]);
});
test('server restarts with different managerId and version', async () => {
console.error = jest.fn();
let server = setupServer();
let done = false;
server.alterResponse((req, resp) => {
if (
req.type === CollabRequestType.PullEvents &&
!done &&
resp.ok &&
req.payload.userId.includes('client1')
) {
done = true;
let body = resp.body as PullEventResponse;
return {
...resp,
body: {
...body,
steps: [
{
stepType: 'replace',
from: 1,
to: 10000,
slice: {
content: [
{
type: 'text',
text: 'very ',
},
],
},
},
],
const client1 = setupClient(server, 'client1');
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('wow ');
await sleep(10);
expect(await server.getNotOkayRequests()).toEqual([]);
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
'doc(paragraph("wow hello world!"))',
);
});
server = setupServer({ managerId: 'test-manager-new-id' });
client1.changeServer(server);
await sleep(10);
client1.typeText('bye ');
await sleep(10);
expect(await server.getNotOkayRequests()).toEqual([
{
userId: 'user-client1',
result: {
body: CollabFail.IncorrectManager,
ok: false,
},
};
}
return resp;
},
]);
expect(
(await server.getCalls()).filter((r) =>
r.payload.userId.includes('client1'),
),
).toEqual([
{
payload: expect.anything(),
type: 'CollabRequestType.PushEvents',
},
{
payload: expect.anything(),
type: 'CollabRequestType.GetDocument',
},
]);
// TODO: Currently if it restarts, it ends up resetting everything
// this is not desirable.
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('new type ');
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(
`doc(paragraph("hello wonew type rld!"))`,
);
});
});
client2.typeText('wow ');
test('local apply steps fails', async () => {
console.error = jest.fn();
const server = setupServer();
await sleep(10);
const client1 = setupClient(server, 'client1');
const client2 = setupClient(server, 'client2');
expect(await server.getNotOkayRequests()).toEqual([]);
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
await sleep(10);
let done = false;
server.alterResponse((req, resp) => {
if (
req.type === CollabRequestType.PullEvents &&
!done &&
resp.ok &&
req.payload.userId.includes('client1')
) {
done = true;
let body = resp.body as PullEventResponse;
return {
...resp,
body: {
...body,
steps: [
{
stepType: 'replace',
from: 1,
to: 10000,
slice: {
content: [
{
type: 'text',
text: 'very ',
},
],
},
},
],
},
};
}
return resp;
});
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
'doc(paragraph("wow hello world!"))',
client2.typeText('wow ');
await sleep(10);
expect(await server.getNotOkayRequests()).toEqual([]);
await sleep(10);
await waitForExpect(async () => {
expect(server.manager.getCollabState(docName)?.doc.toString()).toEqual(
'doc(paragraph("wow hello world!"))',
);
});
expect(console.error).toBeCalledTimes(1);
expect(console.error).nthCalledWith(
1,
new RangeError('Position 10000 out of range'),
);
expect(
(await server.getCalls()).filter((r) =>
r.payload.userId.includes('client1'),
),
).toEqual([
{
payload: expect.anything(),
type: 'CollabRequestType.GetDocument',
},
// two pulls because of the apply error
{
payload: expect.anything(),
type: 'CollabRequestType.PullEvents',
},
{
payload: expect.anything(),
type: 'CollabRequestType.PullEvents',
},
]);
// after recovery client should be syncing correctly
client1.typeText('new type ');
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(
'doc(paragraph("wow new type hello world!"))',
);
});
});
expect(console.error).toBeCalledTimes(1);
expect(console.error).nthCalledWith(
1,
new RangeError('Position 10000 out of range'),
);
test('resets collab-monitor on restart', async () => {
const server = setupServer();
expect(
(await server.getCalls()).filter((r) =>
r.payload.userId.includes('client1'),
),
).toEqual([
{
payload: expect.anything(),
type: 'CollabRequestType.GetDocument',
},
// two pulls because of the apply error
{
payload: expect.anything(),
type: 'CollabRequestType.PullEvents',
},
{
payload: expect.anything(),
type: 'CollabRequestType.PullEvents',
},
]);
const client1 = setupClient(server, 'client1');
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
// send an invalid version to trigger client to restart
server.alterRequest((req) => {
if (req.type === CollabRequestType.PushEvents) {
return {
...req,
payload: {
...req.payload,
version: 100,
},
};
}
return req;
});
client1.typeText('wow ');
expect(client1.debugString()).toEqual(`doc(paragraph("wow hello world!"))`);
onUpstreamChanges(783)(client1.view.state, client1.view.dispatch);
expect(collabMonitorKey.getState(client1.view.state)?.serverVersion).toBe(
783,
);
await sleep(10);
expect(await server.getNotOkayRequests()).toEqual([
{
userId: 'user-client1',
result: {
body: CollabFail.InvalidVersion,
ok: false,
},
},
]);
// Editor should reset
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
// server version should be reset upon restart
expect(collabMonitorKey.getState(client1.view.state)?.serverVersion).toBe(
undefined,
);
});
});

32

dist/index.d.ts
import { CollabFail, CollabManager } from '@bangle.dev/collab-server';
import { EditorState, EditorView, Node, TextSelection, Command } from '@bangle.dev/pm';
import { Command, EditorState, EditorView, Node, TextSelection } from '@bangle.dev/pm';
import * as prosemirror_state from 'prosemirror-state';

@@ -15,3 +15,3 @@

debugInfo?: string;
protected dispatchCollabPluginEvent(data: {
dispatchCollabPluginEvent(data: {
collabEvent?: ValidEvents;

@@ -24,3 +24,3 @@ debugInfo?: string;

abstract runAction(param: ActionParam): Promise<void>;
abstract transition(event: ValidEvents, debugInfo?: string): ValidCollabStates;
abstract transition(event: ValidEvents, debugInfo?: string): ValidCollabStates | undefined;
}

@@ -49,3 +49,3 @@ declare class FatalErrorState extends CollabBaseState {

runAction({ clientInfo, signal, view }: ActionParam): Promise<void>;
transition(event: InitDocEvent | InitErrorEvent, debugInfo?: string): InitDocState | InitErrorState;
transition(event: InitDocEvent | InitErrorEvent, debugInfo?: string): InitDocState | InitErrorState | undefined;
}

@@ -70,3 +70,3 @@ declare class InitDocState extends CollabBaseState {

runAction({ signal, view }: ActionParam): Promise<void>;
transition(event: ReadyEvent | FatalErrorEvent, debugInfo?: string): FatalErrorState | ReadyState;
transition(event: ReadyEvent | FatalErrorEvent, debugInfo?: string): FatalErrorState | ReadyState | undefined;
}

@@ -84,3 +84,3 @@ declare class InitErrorState extends CollabBaseState {

runAction(param: ActionParam): Promise<void>;
transition(event: RestartEvent | FatalErrorEvent, debugInfo?: string): FatalErrorState | InitState;
transition(event: RestartEvent | FatalErrorEvent, debugInfo?: string): FatalErrorState | InitState | undefined;
}

@@ -94,3 +94,3 @@ declare class ReadyState extends CollabBaseState {

runAction({ signal, view }: ActionParam): Promise<void>;
transition(event: Parameters<ReadyState['dispatch']>[2], debugInfo?: string): PullState | PushState;
transition(event: Parameters<ReadyState['dispatch']>[2], debugInfo?: string): PullState | PushState | undefined;
}

@@ -104,3 +104,3 @@ declare class PushState extends CollabBaseState {

runAction({ clientInfo, signal, view }: ActionParam): Promise<void>;
transition(event: ReadyEvent | PullEvent | PushPullErrorEvent, debugInfo?: string): PullState | PushPullErrorState | ReadyState;
transition(event: ReadyEvent | PullEvent | PushPullErrorEvent, debugInfo?: string): PullState | PushPullErrorState | ReadyState | undefined;
}

@@ -114,3 +114,3 @@ declare class PullState extends CollabBaseState {

runAction({ logger, clientInfo, signal, view }: ActionParam): Promise<void>;
transition(event: ReadyEvent | PushPullErrorEvent, debugInfo?: string): PushPullErrorState | ReadyState;
transition(event: ReadyEvent | PushPullErrorEvent, debugInfo?: string): ReadyState | PushPullErrorState | undefined;
}

@@ -130,3 +130,3 @@ declare class PushPullErrorState extends CollabBaseState {

runAction(param: ActionParam): Promise<void>;
transition(event: RestartEvent | PullEvent | FatalErrorEvent, debugInfo?: string): FatalErrorState | InitState | PullState;
transition(event: RestartEvent | PullEvent | FatalErrorEvent, debugInfo?: string): FatalErrorState | InitState | PullState | undefined;
}

@@ -137,5 +137,6 @@

}
declare type ValidEvents = FatalErrorEvent | InitDocEvent | InitErrorEvent | PullEvent | PushEvent | PushPullErrorEvent | ReadyEvent | RestartEvent;
declare type ValidEvents = FatalErrorEvent | HardResetEvent | InitDocEvent | InitErrorEvent | PullEvent | PushEvent | PushPullErrorEvent | ReadyEvent | RestartEvent;
declare enum EventType {
FatalError = "FATAL_ERROR_EVENT",
HardReset = "HARD_RESET_EVENT",
InitDoc = "INIT_DOC_EVENT",

@@ -188,2 +189,5 @@ InitError = "INIT_ERROR_EVENT",

}
interface HardResetEvent {
type: EventType.HardReset;
}
declare enum CollabStateName {

@@ -200,3 +204,3 @@ FatalError = "FATAL_ERROR_STATE",

interface CollabPluginState {
collabState: ValidCollabStates;
collabState: CollabBaseState;
}

@@ -214,3 +218,4 @@ interface ClientInfo {

} | undefined;
declare function onUpstreamChanges(version: number): Command;
declare function hardResetClient(): Command;
declare function onUpstreamChanges(serverVersion: number | undefined): Command;

@@ -221,2 +226,3 @@ declare const plugins: typeof pluginsFactory;

queryFatalError: typeof queryFatalError;
hardResetClient: typeof hardResetClient;
};

@@ -223,0 +229,0 @@ declare const RECOVERY_BACK_OFF = 50;

@@ -11,2 +11,3 @@ import { getVersion, receiveTransaction, sendableSteps, collab } from 'prosemirror-collab';

EventType["FatalError"] = "FATAL_ERROR_EVENT";
EventType["HardReset"] = "HARD_RESET_EVENT";
EventType["InitDoc"] = "INIT_DOC_EVENT";

@@ -117,2 +118,3 @@ EventType["InitError"] = "INIT_ERROR_EVENT";

// If in a fatal error (error which will not be recovered), it returns a fatal error message.
function queryFatalError() {

@@ -124,5 +126,19 @@ return (state) => {

}
// Saves the server version of the document. Rest of the code
// Discards any editor changes that have not yet been sent to the server.
// and sets the editor doc to the one provider by server.
function hardResetClient() {
return (state, dispatch) => {
const collabState = getCollabState(state);
collabState === null || collabState === void 0 ? void 0 : collabState.dispatchCollabPluginEvent({
collabEvent: {
type: EventType.HardReset,
},
debugInfo: 'hard-reset',
})(state, dispatch);
return true;
};
}
// Saves the server version of the document. Rest of the extension
// then uses this information to determine whether to pull from server or not.
function onUpstreamChanges(version) {
function onUpstreamChanges(serverVersion) {
return (state, dispatch) => {

@@ -133,4 +149,4 @@ const pluginState = collabMonitorKey.getState(state);

}
if (pluginState.serverVersion !== version) {
const meta = { serverVersion: version };
if (pluginState.serverVersion !== serverVersion) {
const meta = { serverVersion: serverVersion };
dispatch === null || dispatch === void 0 ? void 0 : dispatch(state.tr.setMeta(collabMonitorKey, meta));

@@ -164,2 +180,6 @@ return true;

const collabState = getCollabState(state);
// only dispatch if in ready state, so as to trigger a state transition
// from ready-state -> whatever.
// If state is not in ready state, whenever it eventually transitions to
// ready state it's own action will dispatch these events automatically.
if (collabState === null || collabState === void 0 ? void 0 : collabState.isReadyState()) {

@@ -285,3 +305,4 @@ collabState.dispatch(state, dispatch, {

else {
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -335,3 +356,4 @@ }

else {
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -365,3 +387,4 @@ }

else {
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -411,3 +434,4 @@ }

else {
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -488,3 +512,4 @@ }

else {
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -559,3 +584,3 @@ }

else {
throw new Error('Invalid event ' + type);
return;
}

@@ -592,3 +617,4 @@ }

else {
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -603,3 +629,3 @@ }

logger('Handling failure=', failure, 'currentState=', collabState.name);
const debugSource = `pushPullErrorStateAction:${failure}:`;
const debugSource = `pushPullErrorStateAction(${failure}):`;
switch (failure) {

@@ -654,2 +680,20 @@ case CollabFail.InvalidVersion:

// 500
case CollabFail.ManagerDestroyed: {
abortableSetTimeout(() => {
if (!signal.aborted) {
if (collabState instanceof PushPullErrorState) {
collabState.dispatch(view.state, view.dispatch, {
type: EventType.Pull,
}, debugSource);
}
else if (collabState instanceof InitErrorState) {
collabState.dispatch(view.state, view.dispatch, {
type: EventType.Restart,
}, debugSource);
}
else ;
}
}, signal, clientInfo.retryWaitTime);
return;
}
case CollabFail.ApplyFailed: {

@@ -685,2 +729,5 @@ if (collabState instanceof PushPullErrorState) {

: () => { };
const collabMonitorInitialState = {
serverVersion: undefined,
};
function collabClientPlugin(clientInfo) {

@@ -702,5 +749,5 @@ const logger = (state) => (...args) => {

}
// prevent any other tr until state is in one of the no-edit state
// prevent any other tr while state is in one of the no-edit state
if (tr.docChanged && ((_a = getCollabState(state)) === null || _a === void 0 ? void 0 : _a.editingAllowed) === false) {
logger(state)('skipping transaction');
console.debug('@bangle.dev/collab-client blocking transaction');
return false;

@@ -718,19 +765,24 @@ }

const meta = tr.getMeta(collabClientKey);
if (meta === undefined) {
if (meta === undefined || !meta.collabEvent) {
return value;
}
let result = {
...value,
};
if (meta.collabEvent) {
const newPluginState = value.collabState.transition(meta.collabEvent, meta.debugInfo);
if (newPluginState.name !== value.collabState.name) {
result.collabState = newPluginState;
}
else {
logger(newState)(`applyState IGNORE EVENT ${meta.collabEvent.type}`, `debugInfo=${result.collabState.debugInfo}`);
}
if (meta.collabEvent.type === EventType.HardReset) {
logger(newState)('apply state HARD RESET, newStateName=', CollabStateName.Init, 'oldStateName=', value.collabState.name);
return {
collabState: new InitState(undefined, '(HardReset)'),
};
}
logger(newState)('apply state, newStateName=', result.collabState.name, `debugInfo=${result.collabState.debugInfo}`, 'oldStateName=', value.collabState.name);
return result;
const newCollabState = value.collabState.transition(meta.collabEvent, meta.debugInfo);
if (!newCollabState) {
return value;
}
if (newCollabState.name !== value.collabState.name) {
logger(newState)('apply state, newStateName=', newCollabState.name, `debugInfo=${newCollabState.debugInfo}`, 'oldStateName=', value.collabState.name);
return {
...value,
collabState: newCollabState,
};
}
logger(newState)(`applyState IGNORE EVENT ${meta.collabEvent.type} due to self transition`, `debugInfo=${meta.debugInfo}`);
return value;
},

@@ -779,7 +831,6 @@ },

init: (_, _state) => {
return {
serverVersion: undefined,
};
return collabMonitorInitialState;
},
apply: (tr, value, oldState, newState) => {
var _a;
const meta = tr.getMeta(collabMonitorKey);

@@ -793,2 +844,6 @@ if (meta) {

}
// Reset collab monitors state if collab state is restarted
if (((_a = getCollabState(newState)) === null || _a === void 0 ? void 0 : _a.name) === CollabStateName.Init) {
return collabMonitorInitialState;
}
return value;

@@ -799,2 +854,9 @@ },

const check = (view) => {
// There are two ways different ways this extension keeps a check on outdated version (needs to pull)
// and sendable steps (needs to push):
// 1. the ready state action which gets triggered when collabClientKey plugin transitions
// to the ready state.
// 2. this plugin (collabMonitorKey) which runs `check` every time the view is updated and
// checks if we need a push or pull.
// outdated version gets a priority over local changes
if (isOutdatedVersion()(view.state)) {

@@ -820,3 +882,3 @@ onOutdatedVersion()(view.state, view.dispatch);

const plugins = pluginsFactory;
const commands = { onUpstreamChanges, queryFatalError };
const commands = { onUpstreamChanges, queryFatalError, hardResetClient };
const RECOVERY_BACK_OFF = 50;

@@ -823,0 +885,0 @@ function pluginsFactory({ clientID = 'client-' + uuid(), docName, sendManagerRequest, retryWaitTime = 100, }) {

{
"name": "@bangle.dev/collab-client",
"version": "0.30.0-alpha.7",
"version": "0.30.0-alpha.8",
"homepage": "https://bangle.dev",

@@ -43,8 +43,8 @@ "authors": [

"devDependencies": {
"@bangle.dev/all-base-components": "0.30.0-alpha.7",
"@bangle.dev/collab-server": "0.30.0-alpha.7",
"@bangle.dev/core": "0.30.0-alpha.7",
"@bangle.dev/disk": "0.30.0-alpha.7",
"@bangle.dev/pm": "0.30.0-alpha.7",
"@bangle.dev/test-helpers": "0.30.0-alpha.7",
"@bangle.dev/all-base-components": "0.30.0-alpha.8",
"@bangle.dev/collab-server": "0.30.0-alpha.8",
"@bangle.dev/core": "0.30.0-alpha.8",
"@bangle.dev/disk": "0.30.0-alpha.8",
"@bangle.dev/pm": "0.30.0-alpha.8",
"@bangle.dev/test-helpers": "0.30.0-alpha.8",
"@types/node": "^17.0.43",

@@ -56,3 +56,3 @@ "localforage": "^1.9.0",

"dependencies": {
"@bangle.dev/utils": "0.30.0-alpha.7",
"@bangle.dev/utils": "0.30.0-alpha.8",
"@types/jest": "^27.5.2",

@@ -59,0 +59,0 @@ "prosemirror-collab": "^1.3.0",

@@ -18,2 +18,4 @@ import { getVersion, sendableSteps } from 'prosemirror-collab';

CollabPluginState,
CollabStateName,
EventType,
} from './common';

@@ -28,2 +30,6 @@ import { getCollabState } from './helpers';

const collabMonitorInitialState = {
serverVersion: undefined,
};
export function collabClientPlugin(clientInfo: ClientInfo) {

@@ -53,5 +59,5 @@ const logger =

// prevent any other tr until state is in one of the no-edit state
// prevent any other tr while state is in one of the no-edit state
if (tr.docChanged && getCollabState(state)?.editingAllowed === false) {
logger(state)('skipping transaction');
console.debug('@bangle.dev/collab-client blocking transaction');
return false;

@@ -73,35 +79,48 @@ }

if (meta === undefined) {
if (meta === undefined || !meta.collabEvent) {
return value;
}
let result: CollabPluginState = {
...value,
};
if (meta.collabEvent.type === EventType.HardReset) {
logger(newState)(
'apply state HARD RESET, newStateName=',
CollabStateName.Init,
'oldStateName=',
value.collabState.name,
);
return {
collabState: new InitState(undefined, '(HardReset)'),
};
}
if (meta.collabEvent) {
const newPluginState = value.collabState.transition(
meta.collabEvent,
meta.debugInfo,
const newCollabState = value.collabState.transition(
meta.collabEvent,
meta.debugInfo,
);
if (!newCollabState) {
return value;
}
if (newCollabState.name !== value.collabState.name) {
logger(newState)(
'apply state, newStateName=',
newCollabState.name,
`debugInfo=${newCollabState.debugInfo}`,
'oldStateName=',
value.collabState.name,
);
if (newPluginState.name !== value.collabState.name) {
result.collabState = newPluginState;
} else {
logger(newState)(
`applyState IGNORE EVENT ${meta.collabEvent.type}`,
`debugInfo=${result.collabState.debugInfo}`,
);
}
return {
...value,
collabState: newCollabState,
};
}
logger(newState)(
'apply state, newStateName=',
result.collabState.name,
`debugInfo=${result.collabState.debugInfo}`,
'oldStateName=',
value.collabState.name,
`applyState IGNORE EVENT ${meta.collabEvent.type} due to self transition`,
`debugInfo=${meta.debugInfo}`,
);
return result;
return value;
},

@@ -155,5 +174,3 @@ },

init: (_, _state): CollabMonitor => {
return {
serverVersion: undefined,
};
return collabMonitorInitialState;
},

@@ -170,2 +187,8 @@ apply: (tr, value, oldState, newState): CollabMonitor => {

}
// Reset collab monitors state if collab state is restarted
if (getCollabState(newState)?.name === CollabStateName.Init) {
return collabMonitorInitialState;
}
return value;

@@ -176,2 +199,10 @@ },

const check = (view: EditorView) => {
// There are two ways different ways this extension keeps a check on outdated version (needs to pull)
// and sendable steps (needs to push):
// 1. the ready state action which gets triggered when collabClientKey plugin transitions
// to the ready state.
// 2. this plugin (collabMonitorKey) which runs `check` every time the view is updated and
// checks if we need a push or pull.
// outdated version gets a priority over local changes
if (isOutdatedVersion()(view.state)) {

@@ -178,0 +209,0 @@ onOutdatedVersion()(view.state, view.dispatch);

@@ -7,3 +7,7 @@ import { collab } from 'prosemirror-collab';

import { collabClientPlugin } from './collab-client';
import { onUpstreamChanges, queryFatalError } from './commands';
import {
hardResetClient,
onUpstreamChanges,
queryFatalError,
} from './commands';

@@ -19,3 +23,3 @@ const LOG = false;

export const plugins = pluginsFactory;
export const commands = { onUpstreamChanges, queryFatalError };
export const commands = { onUpstreamChanges, queryFatalError, hardResetClient };

@@ -22,0 +26,0 @@ export const RECOVERY_BACK_OFF = 50;

@@ -8,2 +8,3 @@ import { getVersion } from 'prosemirror-collab';

// If in a fatal error (error which will not be recovered), it returns a fatal error message.
export function queryFatalError() {

@@ -15,5 +16,21 @@ return (state: EditorState) => {

}
// Saves the server version of the document. Rest of the code
// Discards any editor changes that have not yet been sent to the server.
// and sets the editor doc to the one provider by server.
export function hardResetClient(): Command {
return (state, dispatch) => {
const collabState = getCollabState(state);
collabState?.dispatchCollabPluginEvent({
collabEvent: {
type: EventType.HardReset,
},
debugInfo: 'hard-reset',
})(state, dispatch);
return true;
};
}
// Saves the server version of the document. Rest of the extension
// then uses this information to determine whether to pull from server or not.
export function onUpstreamChanges(version: number): Command {
export function onUpstreamChanges(serverVersion: number | undefined): Command {
return (state, dispatch) => {

@@ -25,4 +42,4 @@ const pluginState = collabMonitorKey.getState(state);

if (pluginState.serverVersion !== version) {
const meta: CollabMonitorTrMeta = { serverVersion: version };
if (pluginState.serverVersion !== serverVersion) {
const meta: CollabMonitorTrMeta = { serverVersion: serverVersion };
dispatch?.(state.tr.setMeta(collabMonitorKey, meta));

@@ -66,2 +83,6 @@ return true;

const collabState = getCollabState(state);
// only dispatch if in ready state, so as to trigger a state transition
// from ready-state -> whatever.
// If state is not in ready state, whenever it eventually transitions to
// ready state it's own action will dispatch these events automatically.
if (collabState?.isReadyState()) {

@@ -68,0 +89,0 @@ collabState.dispatch(

import { CollabFail, CollabManager } from '@bangle.dev/collab-server';
import { Node, PluginKey, TextSelection } from '@bangle.dev/pm';
import type { ValidCollabStates } from './state';
import type { CollabBaseState } from './state';

@@ -17,3 +17,3 @@ export const collabClientKey = new PluginKey<CollabPluginState>(

export interface CollabMonitorTrMeta {
serverVersion: number;
serverVersion: number | undefined;
}

@@ -24,2 +24,3 @@

| FatalErrorEvent
| HardResetEvent
| InitDocEvent

@@ -35,2 +36,3 @@ | InitErrorEvent

FatalError = 'FATAL_ERROR_EVENT',
HardReset = 'HARD_RESET_EVENT',
InitDoc = 'INIT_DOC_EVENT',

@@ -90,2 +92,6 @@ InitError = 'INIT_ERROR_EVENT',

export interface HardResetEvent {
type: EventType.HardReset;
}
export enum CollabStateName {

@@ -103,3 +109,3 @@ FatalError = 'FATAL_ERROR_STATE',

export interface CollabPluginState {
collabState: ValidCollabStates;
collabState: CollabBaseState;
}

@@ -106,0 +112,0 @@

@@ -54,3 +54,3 @@ import { getVersion, sendableSteps } from 'prosemirror-collab';

protected dispatchCollabPluginEvent(data: {
public dispatchCollabPluginEvent(data: {
collabEvent?: ValidEvents;

@@ -80,3 +80,3 @@ debugInfo?: string;

debugInfo?: string,
): ValidCollabStates;
): ValidCollabStates | undefined;
}

@@ -216,4 +216,7 @@

} else {
// This should not get called by any statically findable .transition() . However
// dynamic code can possibly call it and it should be safe to ignore.
let val: never = type;
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -293,3 +296,4 @@ }

let val: never = type;
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -335,3 +339,4 @@ }

let val: never = type;
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -398,3 +403,4 @@ }

let val: never = type;
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -508,3 +514,4 @@ }

let val: never = type;
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -589,3 +596,6 @@ }

transition(event: ReadyEvent | PushPullErrorEvent, debugInfo?: string) {
transition(
event: ReadyEvent | PushPullErrorEvent,
debugInfo?: string,
): ReadyState | PushPullErrorState | undefined {
const type = event.type;

@@ -604,3 +614,3 @@ if (type === EventType.Ready) {

let val: never = type;
throw new Error('Invalid event ' + type);
return;
}

@@ -652,3 +662,4 @@ }

let val: never = type;
throw new Error('Invalid event');
console.debug('@bangle.dev/collab-client Ignoring event' + type);
return;
}

@@ -672,3 +683,3 @@ }

const debugSource = `pushPullErrorStateAction:${failure}:`;
const debugSource = `pushPullErrorStateAction(${failure}):`;

@@ -754,2 +765,34 @@ switch (failure) {

// 500
case CollabFail.ManagerDestroyed: {
abortableSetTimeout(
() => {
if (!signal.aborted) {
if (collabState instanceof PushPullErrorState) {
collabState.dispatch(
view.state,
view.dispatch,
{
type: EventType.Pull,
},
debugSource,
);
} else if (collabState instanceof InitErrorState) {
collabState.dispatch(
view.state,
view.dispatch,
{
type: EventType.Restart,
},
debugSource,
);
} else {
let val: never = collabState;
}
}
},
signal,
clientInfo.retryWaitTime,
);
return;
}
case CollabFail.ApplyFailed: {

@@ -756,0 +799,0 @@ if (collabState instanceof PushPullErrorState) {

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc