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.31.2 to 0.31.3

255

__tests__/collab-client.test.ts

@@ -30,6 +30,11 @@ /**

hardResetClient,
queryCollabState,
queryFatalError,
updateServerVersion,
} from '../src/commands';
import { collabMonitorKey } from '../src/common';
import {
collabMonitorKey,
CollabStateName,
FatalErrorCode,
} from '../src/common';

@@ -141,2 +146,3 @@ waitForExpect.defaults.timeout = 500;

const setupServer = ({
instanceDeleteGuardOpts,
managerId = DEFAULT_MANAGER_ID,

@@ -158,2 +164,5 @@ rawDoc = {

}: {
instanceDeleteGuardOpts?: ConstructorParameters<
typeof CollabManager
>[0]['instanceDeleteGuardOpts'];
managerId?: string;

@@ -262,2 +271,3 @@ rawDoc?: {

},
instanceDeleteGuardOpts,
});

@@ -710,6 +720,4 @@

expect(console.error).toBeCalledTimes(1);
expect(console.error).nthCalledWith(
1,
expect.stringContaining('In FatalErrorState message=Document not found'),
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.DocumentNotFound,
);

@@ -804,8 +812,4 @@ });

expect(console.error).toBeCalledTimes(1);
expect(console.error).nthCalledWith(
1,
expect.stringContaining(
'In FatalErrorState message=History/Server not available',
),
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.HistoryNotAvailable,
);

@@ -860,2 +864,3 @@ });

expect(queryFatalError()(client1.view.state)).toEqual({
errorCode: FatalErrorCode.IncorrectManager,
message: 'Incorrect manager',

@@ -866,8 +871,2 @@ });

expect(document.querySelectorAll('.bangle-collab-active').length).toBe(0);
expect(console.error).toBeCalledTimes(1);
expect(console.error).nthCalledWith(
1,
expect.stringContaining('In FatalErrorState message=Incorrect manager'),
);
});

@@ -1042,2 +1041,3 @@

expect(queryFatalError()(client1.view.state)).toStrictEqual({
errorCode: FatalErrorCode.StuckInInfiniteLoop,
message:

@@ -1047,19 +1047,2 @@ 'Stuck in error loop, last failure: CollabFail.ManagerUnresponsive',

expect(console.error).toBeCalledTimes(1);
expect(console.error).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
"@bangle.dev/collab-client: In FatalErrorState message=Stuck in error loop, last failure: CollabFail.ManagerUnresponsive",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`);
server = setupServer();

@@ -1452,1 +1435,205 @@

});
describe('deleting instance', () => {
let server: ReturnType<typeof setupServer>;
beforeEach(() => {
server = setupServer({
managerId: 'manager-test-1',
instanceDeleteGuardOpts: {
deleteWaitTime: 30,
maxDurationToKeepRecord: 150,
},
});
});
test('when an instance is deleted client gets history not available error', async () => {
const client1 = setupClient(server, { clientID: 'client1' });
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('X');
await waitForExpect(() => {
expect(client1.debugString()).toEqual(`doc(paragraph("Xhello world!"))`);
});
server.manager.requestDeleteInstance(docName);
client1.typeText('Y');
await waitForExpect(() => {
expect(queryCollabState()(client1.view.state)?.name).toBe(
CollabStateName.Fatal,
);
});
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.HistoryNotAvailable,
);
});
test('resetting a client should work', async () => {
const client1 = setupClient(server, { clientID: 'client1' });
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('X');
await waitForExpect(() => {
expect(client1.debugString()).toEqual(`doc(paragraph("Xhello world!"))`);
});
server.manager.requestDeleteInstance(docName);
client1.typeText('Y');
await waitForExpect(() => {
expect(queryCollabState()(client1.view.state)?.name).toBe(
CollabStateName.Fatal,
);
});
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.HistoryNotAvailable,
);
await sleep(8);
const sentTimes = Array.from(
new Set((await server.getRequests()).map((r) => r.body.clientCreatedAt)),
);
// should just send one time
expect(sentTimes.length).toBe(1);
hardResetClient()(client1.view.state, client1.view.dispatch);
await waitForExpect(() => {
expect(client1.debugString()).toEqual(`doc(paragraph("Xhello world!"))`);
});
const sentTimes2 = Array.from(
new Set((await server.getRequests()).map((r) => r.body.clientCreatedAt)),
);
// after resetting should be sending new create time
expect(sentTimes2.length).toBe(2);
await sleep(5);
client1.typeText('Z', 1);
await sleep(5);
expect(queryCollabState()(client1.view.state)?.name).toBe(
CollabStateName.Ready,
);
await waitForExpect(() => {
expect(client1.debugString()).toEqual(`doc(paragraph("ZXhello world!"))`);
});
// wait a long time to make instance is not deleted
await sleep(100);
expect(server.manager.getAllDocNames().size).toBe(1);
// now deleting after resetting the instance should still work
server.manager.requestDeleteInstance(docName);
client1.typeText('C');
await waitForExpect(() => {
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.HistoryNotAvailable,
);
});
});
test('after deletion if a newer client connects, server responds ok', async () => {
const client1 = setupClient(server, { clientID: 'client1' });
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('X');
await waitForExpect(() => {
expect(client1.debugString()).toEqual(`doc(paragraph("Xhello world!"))`);
});
server.manager.requestDeleteInstance(docName);
client1.typeText('Y');
await waitForExpect(() => {
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.HistoryNotAvailable,
);
});
// assert that it is deleted
await waitForExpect(() => {
expect(server.manager.getAllDocNames().size).toBe(0);
});
const client2 = setupClient(server, { clientID: 'client2' });
await waitForExpect(async () => {
expect(client2.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client2.typeText('Z');
await waitForExpect(() => {
expect(client2.debugString()).toEqual(`doc(paragraph("Zhello world!"))`);
});
});
test('a newer client cancels any previous deletion', async () => {
const client1 = setupClient(server, { clientID: 'client1' });
await waitForExpect(async () => {
expect(client1.debugString()).toEqual(`doc(paragraph("hello world!"))`);
});
client1.typeText('X');
await waitForExpect(() => {
expect(client1.debugString()).toEqual(`doc(paragraph("Xhello world!"))`);
});
server.manager.requestDeleteInstance(docName);
await sleep(4);
// setup a new client right before the deletion is executed
const client2 = setupClient(server, { clientID: 'client2' });
// type in the old client and get the error
client1.typeText('Y');
await waitForExpect(() => {
expect(queryFatalError()(client1.view.state)?.errorCode).toBe(
FatalErrorCode.HistoryNotAvailable,
);
});
await waitForExpect(async () => {
expect(client2.debugString()).toEqual(`doc(paragraph("Xhello world!"))`);
});
client2.typeText('Z');
// wait plenty of time to make assert instance is not deleted
await sleep(100);
expect(server.manager.getAllDocNames().size).toBe(1);
// should have 'ZX' to signal instance wasn't deleted
await waitForExpect(async () => {
expect(client2.debugString()).toEqual(`doc(paragraph("ZXhello world!"))`);
});
});
});

8

api.md

@@ -27,5 +27,5 @@ ---

InitDocState --> ReadyState: ReadyEvent
InitDocState --> FatalErrorState: FatalErrorEvent
InitDocState --> FatalState: FatalEvent
InitErrorState --> InitState: RestartEvent
InitErrorState --> FatalErrorState: FatalErrorEvent
InitErrorState --> FatalState: FatalEvent
ReadyState --> PushState: PushEvent

@@ -40,4 +40,4 @@ ReadyState --> PullState: PullEvent

PushPullErrorState --> PullState: PullEvent
PushPullErrorState --> FatalErrorState: FatalErrorEvent
FatalErrorState --> [*]
PushPullErrorState --> FatalState: FatalEvent
FatalState --> [*]
```
import { CollabFail, ClientCommunication, CollabMessageBus } from '@bangle.dev/collab-comms';
import { Command, EditorState, EditorView, Node, TextSelection, Plugin } from '@bangle.dev/pm';
import { EditorState, EditorView, Node, TextSelection, Command, Plugin } from '@bangle.dev/pm';

@@ -10,5 +10,6 @@ interface ActionParam {

}
declare type ValidCollabStates = FatalErrorState | InitDocState | InitErrorState | InitState | PullState | PushPullErrorState | PushState | ReadyState;
declare type ValidCollabState = FatalState | InitDocState | InitErrorState | InitState | PullState | PushPullErrorState | PushState | ReadyState;
declare abstract class CollabBaseState {
editingAllowed: boolean;
isEditingBlocked: boolean;
isTaggedError: boolean;
debugInfo?: string;

@@ -21,19 +22,20 @@ createdAt: number;

}): Command;
abstract isErrorState: boolean;
isFatalState(): this is FatalErrorState;
isFatalState(): this is FatalState;
isReadyState(): this is ReadyState;
abstract name: CollabStateName;
abstract runAction(param: ActionParam): Promise<void>;
abstract transition(event: ValidEvents, debugInfo?: string): ValidCollabStates | undefined;
abstract transition(event: ValidEvents, debugInfo?: string): ValidCollabState | undefined;
}
declare class FatalErrorState extends CollabBaseState {
declare class FatalState extends CollabBaseState {
state: {
message: string;
errorCode: FatalErrorCode | undefined;
};
debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
isEditingBlocked: boolean;
isTaggedError: boolean;
name: CollabStateName;
constructor(state: {
message: string;
errorCode: FatalErrorCode | undefined;
}, debugInfo?: string | undefined);

@@ -47,5 +49,5 @@ dispatch(signal: AbortSignal, state: EditorState, dispatch: EditorView['dispatch'] | undefined, event: never, debugInfo?: string): boolean;

debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
isEditingBlocked: boolean;
name: CollabStateName;
clientCreatedAt: number;
constructor(state?: {}, debugInfo?: string | undefined);

@@ -62,6 +64,6 @@ dispatch(signal: AbortSignal, state: EditorState, dispatch: EditorView['dispatch'] | undefined, event: Parameters<InitState['transition']>[0], debugInfo?: string): boolean;

managerId: string;
clientCreatedAt: number;
};
debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
isEditingBlocked: boolean;
name: CollabStateName;

@@ -73,6 +75,7 @@ constructor(state: {

managerId: string;
clientCreatedAt: number;
}, debugInfo?: string | undefined);
dispatch(signal: AbortSignal, state: EditorState, dispatch: EditorView['dispatch'] | undefined, event: Parameters<InitDocState['transition']>[0], debugInfo?: string): boolean;
runAction({ signal, view }: ActionParam): Promise<void>;
transition(event: ReadyEvent | FatalErrorEvent, debugInfo?: string): ReadyState | FatalErrorState | undefined;
transition(event: ReadyEvent | FatalEvent, debugInfo?: string): ReadyState | FatalState | undefined;
}

@@ -84,4 +87,4 @@ declare class InitErrorState extends CollabBaseState {

debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
isEditingBlocked: boolean;
isTaggedError: boolean;
name: CollabStateName;

@@ -93,3 +96,3 @@ constructor(state: {

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

@@ -99,4 +102,2 @@ declare class ReadyState extends CollabBaseState {

debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
name: CollabStateName;

@@ -111,4 +112,2 @@ constructor(state: InitDocState['state'], debugInfo?: string | undefined);

debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
name: CollabStateName;

@@ -123,8 +122,6 @@ constructor(state: InitDocState['state'], debugInfo?: string | undefined);

debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
name: CollabStateName;
constructor(state: InitDocState['state'], debugInfo?: string | undefined);
dispatch(signal: AbortSignal, state: EditorState, dispatch: EditorView['dispatch'] | undefined, event: Parameters<PullState['transition']>[0], debugInfo?: string): boolean;
runAction({ logger, clientInfo, signal, view }: ActionParam): Promise<void>;
runAction({ clientInfo, logger, signal, view }: ActionParam): Promise<void>;
transition(event: ReadyEvent | PushPullErrorEvent, debugInfo?: string): PushPullErrorState | ReadyState | undefined;

@@ -138,4 +135,3 @@ }

debugInfo?: string | undefined;
editingAllowed: boolean;
isErrorState: boolean;
isTaggedError: boolean;
name: CollabStateName;

@@ -148,3 +144,3 @@ constructor(state: {

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

@@ -155,5 +151,5 @@

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

@@ -168,6 +164,15 @@ InitDoc = "INIT_DOC_EVENT",

}
interface FatalErrorEvent {
type: EventType.FatalError;
declare enum FatalErrorCode {
InitialDocLoadFailed = "INITIAL_DOC_LOAD_FAILED",
StuckInInfiniteLoop = "STUCK_IN_INFINITE_LOOP",
IncorrectManager = "INCORRECT_MANAGER",
HistoryNotAvailable = "HISTORY_NOT_AVAILABLE",
DocumentNotFound = "DOCUMENT_NOT_FOUND",
UnexpectedState = "UNEXPECTED_STATE"
}
interface FatalEvent {
type: EventType.Fatal;
payload: {
message: string;
errorCode: FatalErrorCode;
};

@@ -212,3 +217,3 @@ }

declare enum CollabStateName {
FatalError = "FATAL_ERROR_STATE",
Fatal = "FATAL_STATE",
Init = "INIT_STATE",

@@ -223,4 +228,4 @@ InitDoc = "INIT_DOC_STATE",

interface CollabPluginState {
collabState: CollabBaseState;
previousStates: CollabBaseState[];
collabState: ValidCollabState;
previousStates: ValidCollabState[];
infiniteTransitionGuard: {

@@ -241,5 +246,11 @@ counter: number;

/**
* If in fatal state (a terminal state) and returns the error information.
* @returns
*/
declare function queryFatalError(): (state: EditorState) => {
message: string;
errorCode: FatalErrorCode | undefined;
} | undefined;
declare function queryCollabState(): (state: EditorState) => ValidCollabState | undefined;
declare function hardResetClient(): Command;

@@ -251,2 +262,3 @@

hardResetClient: typeof hardResetClient;
queryCollabState: typeof queryCollabState;
};

@@ -284,2 +296,2 @@ interface CollabExtensionOptions {

export { collabExtension_d as collabClient };
export { CollabStateName, FatalErrorCode, ValidCollabState, collabExtension_d as collabClient };

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

(function (EventType) {
EventType["FatalError"] = "FATAL_ERROR_EVENT";
EventType["Fatal"] = "FATAL_EVENT";
EventType["HardReset"] = "HARD_RESET_EVENT";

@@ -26,5 +26,14 @@ EventType["InitDoc"] = "INIT_DOC_EVENT";

})(EventType || (EventType = {}));
var FatalErrorCode;
(function (FatalErrorCode) {
FatalErrorCode["InitialDocLoadFailed"] = "INITIAL_DOC_LOAD_FAILED";
FatalErrorCode["StuckInInfiniteLoop"] = "STUCK_IN_INFINITE_LOOP";
FatalErrorCode["IncorrectManager"] = "INCORRECT_MANAGER";
FatalErrorCode["HistoryNotAvailable"] = "HISTORY_NOT_AVAILABLE";
FatalErrorCode["DocumentNotFound"] = "DOCUMENT_NOT_FOUND";
FatalErrorCode["UnexpectedState"] = "UNEXPECTED_STATE";
})(FatalErrorCode || (FatalErrorCode = {}));
var CollabStateName;
(function (CollabStateName) {
CollabStateName["FatalError"] = "FATAL_ERROR_STATE";
CollabStateName["Fatal"] = "FATAL_STATE";
CollabStateName["Init"] = "INIT_STATE";

@@ -122,9 +131,21 @@ CollabStateName["InitDoc"] = "INIT_DOC_STATE";

// If in a fatal error (error which will not be recovered), it returns a fatal error message.
/**
* If in fatal state (a terminal state) and returns the error information.
* @returns
*/
function queryFatalError() {
return (state) => {
const collabState = getCollabState(state);
return (collabState === null || collabState === void 0 ? void 0 : collabState.isFatalState()) ? collabState.state : undefined;
if (collabState === null || collabState === void 0 ? void 0 : collabState.isFatalState()) {
return collabState.state;
}
return undefined;
};
}
function queryCollabState() {
return (state) => {
const collabState = getCollabState(state);
return collabState;
};
}
// Discards any editor changes that have not yet been sent to the server.

@@ -203,3 +224,3 @@ // and sets the editor doc to the one provider by server.

}
return (previousStates.filter((s) => s.isErrorState).length >
return (previousStates.filter((s) => s.isTaggedError).length >
STUCK_IN_ERROR_THRESHOLD);

@@ -213,5 +234,7 @@ };

// Some collab tr's are allowed.
this.editingAllowed = true;
this.isEditingBlocked = false;
this.isTaggedError = false;
this.createdAt = Date.now();
}
// A helper function to dispatch events in correct shape
dispatchCollabPluginEvent(data) {

@@ -227,3 +250,3 @@ return (state, dispatch) => {

isFatalState() {
return this instanceof FatalErrorState;
return this instanceof FatalState;
}

@@ -234,3 +257,3 @@ isReadyState() {

}
class FatalErrorState extends CollabBaseState {
class FatalState extends CollabBaseState {
constructor(state, debugInfo) {

@@ -240,5 +263,5 @@ super();

this.debugInfo = debugInfo;
this.editingAllowed = false;
this.isErrorState = true;
this.name = CollabStateName.FatalError;
this.isEditingBlocked = true;
this.isTaggedError = true;
this.name = CollabStateName.Fatal;
}

@@ -256,3 +279,3 @@ dispatch(signal, state, dispatch, event, debugInfo) {

}
logger(`Freezing document(${clientInfo.docName}) to prevent further edits due to FatalError`);
logger(`Freezing document(${clientInfo.docName}) to prevent further edits due to FatalState`);
return;

@@ -269,5 +292,8 @@ }

this.debugInfo = debugInfo;
this.editingAllowed = false;
this.isErrorState = false;
this.isEditingBlocked = true;
this.name = CollabStateName.Init;
// clientCreatedAt should just be created once at the start of the client
// and not at any state where the value might change during the course of the clients life.
// this field setup order is instrumental to connect with the server.
this.clientCreatedAt = Date.now();
}

@@ -295,2 +321,3 @@ dispatch(signal, state, dispatch, event, debugInfo) {

const result = await clientCom.getDocument({
clientCreatedAt: this.clientCreatedAt,
docName,

@@ -330,2 +357,3 @@ userId,

managerId: payload.managerId,
clientCreatedAt: this.clientCreatedAt,
}, debugInfo);

@@ -349,4 +377,3 @@ }

this.debugInfo = debugInfo;
this.editingAllowed = false;
this.isErrorState = false;
this.isEditingBlocked = true;
this.name = CollabStateName.InitDoc;

@@ -370,5 +397,6 @@ }

this.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Failed to load initial doc',
errorCode: FatalErrorCode.InitialDocLoadFailed,
},

@@ -389,4 +417,4 @@ });

}
else if (type === EventType.FatalError) {
return new FatalErrorState({ message: event.payload.message }, debugInfo);
else if (type === EventType.Fatal) {
return new FatalState({ message: event.payload.message, errorCode: event.payload.errorCode }, debugInfo);
}

@@ -404,4 +432,4 @@ else {

this.debugInfo = debugInfo;
this.editingAllowed = false;
this.isErrorState = true;
this.isEditingBlocked = true;
this.isTaggedError = true;
this.name = CollabStateName.InitError;

@@ -424,4 +452,4 @@ }

}
else if (type === EventType.FatalError) {
return new FatalErrorState({ message: event.payload.message }, debugInfo);
else if (type === EventType.Fatal) {
return new FatalState({ message: event.payload.message, errorCode: event.payload.errorCode }, debugInfo);
}

@@ -439,4 +467,2 @@ else {

this.debugInfo = debugInfo;
this.editingAllowed = true;
this.isErrorState = false;
this.name = CollabStateName.Ready;

@@ -489,4 +515,2 @@ }

this.debugInfo = debugInfo;
this.editingAllowed = true;
this.isErrorState = false;
this.name = CollabStateName.Push;

@@ -516,2 +540,3 @@ }

const response = await clientCom.pushEvents({
clientCreatedAt: this.state.clientCreatedAt,
version: getVersion(view.state),

@@ -568,4 +593,2 @@ steps: steps ? steps.steps.map((s) => s.toJSON()) : [],

this.debugInfo = debugInfo;
this.editingAllowed = true;
this.isErrorState = false;
this.name = CollabStateName.Pull;

@@ -580,3 +603,3 @@ }

}
async runAction({ logger, clientInfo, signal, view }) {
async runAction({ clientInfo, logger, signal, view }) {
if (signal.aborted) {

@@ -588,2 +611,3 @@ return;

const response = await clientCom.pullEvents({
clientCreatedAt: this.state.clientCreatedAt,
version: getVersion(view.state),

@@ -643,4 +667,3 @@ docName: docName,

this.debugInfo = debugInfo;
this.editingAllowed = true;
this.isErrorState = true;
this.isTaggedError = true;
this.name = CollabStateName.PushPullError;

@@ -666,4 +689,4 @@ }

}
else if (type === EventType.FatalError) {
return new FatalErrorState({ message: event.payload.message }, debugInfo);
else if (type === EventType.Fatal) {
return new FatalState({ message: event.payload.message, errorCode: event.payload.errorCode }, debugInfo);
}

@@ -685,5 +708,6 @@ else {

collabState.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Stuck in error loop, last failure: ' + failure,
errorCode: FatalErrorCode.StuckInInfiniteLoop,
},

@@ -706,5 +730,6 @@ }, debugSource);

collabState.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Incorrect manager',
errorCode: FatalErrorCode.IncorrectManager,
},

@@ -716,5 +741,6 @@ }, debugSource);

collabState.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'History/Server not available',
errorCode: FatalErrorCode.HistoryNotAvailable,
},

@@ -727,5 +753,6 @@ }, debugSource);

collabState.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Document not found',
errorCode: FatalErrorCode.DocumentNotFound,
},

@@ -744,5 +771,7 @@ }, debugSource);

collabState.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: `Cannot handle ${failure} in state=${collabState.name}`,
// TODO: is this the right error code?
errorCode: FatalErrorCode.UnexpectedState,
},

@@ -784,5 +813,6 @@ }, debugSource);

collabState.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: `Cannot handle ${failure} in state=${collabState.name}`,
errorCode: FatalErrorCode.UnexpectedState,
},

@@ -843,3 +873,3 @@ }, debugSource);

// 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) {
if (tr.docChanged && ((_a = getCollabState(state)) === null || _a === void 0 ? void 0 : _a.isEditingBlocked) === true) {
console.debug('@bangle.dev/collab-client blocking transaction');

@@ -863,2 +893,3 @@ return false;

}
// By pass any logic, if we receive this event and set the state to Init
if (meta.collabEvent.type === EventType.HardReset) {

@@ -887,3 +918,6 @@ logger(newState)('apply state HARD RESET, newStateName=', CollabStateName.Init, 'oldStateName=', value.collabState.name);

return {
collabState: new FatalErrorState({ message: 'Infinite transitions' }, '(stuck in infinite transitions)'),
collabState: new FatalState({
message: 'Infinite transitions',
errorCode: FatalErrorCode.StuckInInfiniteLoop,
}, '(stuck in infinite transitions)'),
previousStates: [value.collabState, ...value.previousStates],

@@ -906,5 +940,2 @@ infiniteTransitionGuard: { counter: 0, lastChecked: 0 },

}
if (newCollabState.isFatalState()) {
console.error(`@bangle.dev/collab-client: In FatalErrorState message=${newCollabState.state.message}`);
}
return {

@@ -987,7 +1018,7 @@ ...value,

var _a;
const editingAllowed = (_a = getCollabState(state)) === null || _a === void 0 ? void 0 : _a.editingAllowed;
const editingBlocked = (_a = getCollabState(state)) === null || _a === void 0 ? void 0 : _a.isEditingBlocked;
return {
class: editingAllowed
? 'bangle-collab-active'
: 'bangle-collab-frozen',
class: editingBlocked
? 'bangle-collab-frozen'
: 'bangle-collab-active',
};

@@ -1046,3 +1077,3 @@ },

const plugins = pluginsFactory;
const commands = { queryFatalError, hardResetClient };
const commands = { queryFatalError, hardResetClient, queryCollabState };
/**

@@ -1049,0 +1080,0 @@ *

{
"name": "@bangle.dev/collab-client",
"version": "0.31.2",
"version": "0.31.3",
"homepage": "https://bangle.dev",

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

"devDependencies": {
"@bangle.dev/all-base-components": "0.31.2",
"@bangle.dev/collab-manager": "0.31.2",
"@bangle.dev/core": "0.31.2",
"@bangle.dev/disk": "0.31.2",
"@bangle.dev/pm": "0.31.2",
"@bangle.dev/test-helpers": "0.31.2",
"@bangle.dev/all-base-components": "0.31.3",
"@bangle.dev/collab-manager": "0.31.3",
"@bangle.dev/core": "0.31.3",
"@bangle.dev/disk": "0.31.3",
"@bangle.dev/pm": "0.31.3",
"@bangle.dev/test-helpers": "0.31.3",
"@types/node": "^17.0.43",

@@ -55,7 +55,6 @@ "localforage": "^1.9.0",

"dependencies": {
"@bangle.dev/collab-comms": "0.31.2",
"@bangle.dev/utils": "0.31.2",
"@bangle.dev/collab-comms": "0.31.3",
"@bangle.dev/utils": "0.31.3",
"@types/jest": "^27.5.2",
"prosemirror-collab": "^1.3.0",
"xstate": "^4.32.1"
"prosemirror-collab": "^1.3.0"
},

@@ -62,0 +61,0 @@ "exports": {

@@ -26,6 +26,7 @@ import { getVersion, sendableSteps } from 'prosemirror-collab';

EventType,
FatalErrorCode,
MAX_STATES_TO_KEEP,
} from './common';
import { getCollabState } from './helpers';
import { CollabBaseState, FatalErrorState, InitState } from './state';
import { CollabBaseState, FatalState, InitState } from './state';

@@ -37,3 +38,3 @@ const LOG = true;

const collabMonitorInitialState = {
const collabMonitorInitialState: CollabMonitor = {
serverVersion: undefined,

@@ -88,3 +89,3 @@ };

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

@@ -113,2 +114,3 @@ return false;

// By pass any logic, if we receive this event and set the state to Init
if (meta.collabEvent.type === EventType.HardReset) {

@@ -121,2 +123,3 @@ logger(newState)(

);
return {

@@ -154,4 +157,7 @@ collabState: new InitState(undefined, '(HardReset)'),

return {
collabState: new FatalErrorState(
{ message: 'Infinite transitions' },
collabState: new FatalState(
{
message: 'Infinite transitions',
errorCode: FatalErrorCode.StuckInInfiniteLoop,
},
'(stuck in infinite transitions)',

@@ -191,8 +197,2 @@ ),

if (newCollabState.isFatalState()) {
console.error(
`@bangle.dev/collab-client: In FatalErrorState message=${newCollabState.state.message}`,
);
}
return {

@@ -217,2 +217,3 @@ ...value,

let clientComController = new AbortController();
let clientCom = new ClientCommunication({

@@ -287,7 +288,7 @@ clientId: clientID,

attributes: (state: any) => {
const editingAllowed = getCollabState(state)?.editingAllowed;
const editingBlocked = getCollabState(state)?.isEditingBlocked;
return {
class: editingAllowed
? 'bangle-collab-active'
: 'bangle-collab-frozen',
class: editingBlocked
? 'bangle-collab-frozen'
: 'bangle-collab-active',
};

@@ -294,0 +295,0 @@ },

@@ -8,6 +8,6 @@ import { collab } from 'prosemirror-collab';

import { collabClientPlugin } from './collab-client';
import { hardResetClient, queryFatalError } from './commands';
import { hardResetClient, queryCollabState, queryFatalError } from './commands';
export const plugins = pluginsFactory;
export const commands = { queryFatalError, hardResetClient };
export const commands = { queryFatalError, hardResetClient, queryCollabState };

@@ -14,0 +14,0 @@ export interface CollabExtensionOptions {

@@ -14,10 +14,23 @@ import { getVersion } from 'prosemirror-collab';

// If in a fatal error (error which will not be recovered), it returns a fatal error message.
/**
* If in fatal state (a terminal state) and returns the error information.
* @returns
*/
export function queryFatalError() {
return (state: EditorState) => {
const collabState = getCollabState(state);
return collabState?.isFatalState() ? collabState.state : undefined;
if (collabState?.isFatalState()) {
return collabState.state;
}
return undefined;
};
}
export function queryCollabState() {
return (state: EditorState) => {
const collabState = getCollabState(state);
return collabState;
};
}
// Discards any editor changes that have not yet been sent to the server.

@@ -120,3 +133,3 @@ // and sets the editor doc to the one provider by server.

return (
previousStates.filter((s) => s.isErrorState).length >
previousStates.filter((s) => s.isTaggedError).length >
STUCK_IN_ERROR_THRESHOLD

@@ -123,0 +136,0 @@ );

import { ClientCommunication, CollabFail } from '@bangle.dev/collab-comms';
import { Node, PluginKey, TextSelection } from '@bangle.dev/pm';
import type { CollabBaseState } from './state';
import type { ValidCollabState } from './state';

@@ -28,3 +28,3 @@ export const MAX_STATES_TO_KEEP = 15;

export type ValidEvents =
| FatalErrorEvent
| FatalEvent
| HardResetEvent

@@ -40,3 +40,3 @@ | InitDocEvent

export enum EventType {
FatalError = 'FATAL_ERROR_EVENT',
Fatal = 'FATAL_EVENT',
HardReset = 'HARD_RESET_EVENT',

@@ -52,6 +52,16 @@ InitDoc = 'INIT_DOC_EVENT',

export interface FatalErrorEvent {
type: EventType.FatalError;
export enum FatalErrorCode {
InitialDocLoadFailed = 'INITIAL_DOC_LOAD_FAILED',
StuckInInfiniteLoop = 'STUCK_IN_INFINITE_LOOP',
IncorrectManager = 'INCORRECT_MANAGER',
HistoryNotAvailable = 'HISTORY_NOT_AVAILABLE',
DocumentNotFound = 'DOCUMENT_NOT_FOUND',
UnexpectedState = 'UNEXPECTED_STATE',
}
export interface FatalEvent {
type: EventType.Fatal;
payload: {
message: string;
errorCode: FatalErrorCode;
};

@@ -103,3 +113,3 @@ }

export enum CollabStateName {
FatalError = 'FATAL_ERROR_STATE',
Fatal = 'FATAL_STATE',
Init = 'INIT_STATE',

@@ -115,4 +125,4 @@ InitDoc = 'INIT_DOC_STATE',

export interface CollabPluginState {
collabState: CollabBaseState;
previousStates: CollabBaseState[];
collabState: ValidCollabState;
previousStates: ValidCollabState[];
infiniteTransitionGuard: { counter: number; lastChecked: number };

@@ -119,0 +129,0 @@ }

import * as collabClient from './collab-extension';
export type { CollabStateName, FatalErrorCode } from './common';
export { collabClient };
export type { ValidCollabState } from './state';

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

EventType,
FatalErrorEvent,
FatalErrorCode,
FatalEvent,
InitDocEvent,

@@ -43,4 +44,4 @@ InitErrorEvent,

export type ValidCollabStates =
| FatalErrorState
export type ValidCollabState =
| FatalState
| InitDocState

@@ -57,5 +58,9 @@ | InitErrorState

// Some collab tr's are allowed.
editingAllowed: boolean = true;
isEditingBlocked: boolean = false;
isTaggedError: boolean = false;
debugInfo?: string;
createdAt = Date.now();
// A helper function to dispatch events in correct shape
public dispatchCollabPluginEvent(data: {

@@ -75,6 +80,4 @@ signal: AbortSignal;

abstract isErrorState: boolean;
public isFatalState(): this is FatalErrorState {
return this instanceof FatalErrorState;
public isFatalState(): this is FatalState {
return this instanceof FatalState;
}

@@ -93,13 +96,14 @@

debugInfo?: string,
): ValidCollabStates | undefined;
): ValidCollabState | undefined;
}
export class FatalErrorState extends CollabBaseState {
editingAllowed = false;
isErrorState = true;
name = CollabStateName.FatalError;
export class FatalState extends CollabBaseState {
isEditingBlocked = true;
isTaggedError = true;
name = CollabStateName.Fatal;
constructor(
public state: {
message: string;
errorCode: FatalErrorCode | undefined;
},

@@ -130,3 +134,3 @@ public debugInfo?: string,

logger(
`Freezing document(${clientInfo.docName}) to prevent further edits due to FatalError`,
`Freezing document(${clientInfo.docName}) to prevent further edits due to FatalState`,
);

@@ -142,6 +146,10 @@ return;

export class InitState extends CollabBaseState {
editingAllowed = false;
isErrorState = false;
isEditingBlocked = true;
name = CollabStateName.Init;
// clientCreatedAt should just be created once at the start of the client
// and not at any state where the value might change during the course of the clients life.
// this field setup order is instrumental to connect with the server.
clientCreatedAt = Date.now();
constructor(public state = {}, public debugInfo?: string) {

@@ -181,2 +189,3 @@ super();

const result = await clientCom.getDocument({
clientCreatedAt: this.clientCreatedAt,
docName,

@@ -237,2 +246,3 @@ userId,

managerId: payload.managerId,
clientCreatedAt: this.clientCreatedAt,
},

@@ -259,4 +269,3 @@ debugInfo,

export class InitDocState extends CollabBaseState {
editingAllowed = false;
isErrorState = false;
isEditingBlocked = true;
name = CollabStateName.InitDoc;

@@ -270,2 +279,3 @@

managerId: string;
clientCreatedAt: number;
},

@@ -307,5 +317,6 @@ public debugInfo?: string,

this.dispatch(signal, view.state, view.dispatch, {
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Failed to load initial doc',
errorCode: FatalErrorCode.InitialDocLoadFailed,
},

@@ -328,10 +339,13 @@ });

transition(
event: ReadyEvent | FatalErrorEvent,
event: ReadyEvent | FatalEvent,
debugInfo?: string,
): ReadyState | FatalErrorState | undefined {
): ReadyState | FatalState | undefined {
const type = event.type;
if (type === EventType.Ready) {
return new ReadyState(this.state, debugInfo);
} else if (type === EventType.FatalError) {
return new FatalErrorState({ message: event.payload.message }, debugInfo);
} else if (type === EventType.Fatal) {
return new FatalState(
{ message: event.payload.message, errorCode: event.payload.errorCode },
debugInfo,
);
} else {

@@ -346,4 +360,4 @@ let val: never = type;

export class InitErrorState extends CollabBaseState {
editingAllowed = false;
isErrorState = true;
isEditingBlocked = true;
isTaggedError = true;
name = CollabStateName.InitError;

@@ -378,8 +392,11 @@

transition(event: RestartEvent | FatalErrorEvent, debugInfo?: string) {
transition(event: RestartEvent | FatalEvent, debugInfo?: string) {
const type = event.type;
if (type === EventType.Restart) {
return new InitState(undefined, debugInfo);
} else if (type === EventType.FatalError) {
return new FatalErrorState({ message: event.payload.message }, debugInfo);
} else if (type === EventType.Fatal) {
return new FatalState(
{ message: event.payload.message, errorCode: event.payload.errorCode },
debugInfo,
);
} else {

@@ -394,4 +411,2 @@ let val: never = type;

export class ReadyState extends CollabBaseState {
editingAllowed = true;
isErrorState = false;
name = CollabStateName.Ready;

@@ -464,4 +479,2 @@

export class PushState extends CollabBaseState {
editingAllowed = true;
isErrorState = false;
name = CollabStateName.Push;

@@ -512,2 +525,3 @@

const response = await clientCom.pushEvents({
clientCreatedAt: this.state.clientCreatedAt,
version: getVersion(view.state),

@@ -579,4 +593,2 @@ steps: steps ? steps.steps.map((s) => s.toJSON()) : [],

export class PullState extends CollabBaseState {
editingAllowed = true;
isErrorState = false;
name = CollabStateName.Pull;

@@ -602,3 +614,3 @@

async runAction({ logger, clientInfo, signal, view }: ActionParam) {
async runAction({ clientInfo, logger, signal, view }: ActionParam) {
if (signal.aborted) {

@@ -610,2 +622,3 @@ return;

const response = await clientCom.pullEvents({
clientCreatedAt: this.state.clientCreatedAt,
version: getVersion(view.state),

@@ -683,4 +696,4 @@ docName: docName,

export class PushPullErrorState extends CollabBaseState {
editingAllowed = true;
isErrorState = true;
isTaggedError = true;
name = CollabStateName.PushPullError;

@@ -716,6 +729,3 @@

transition(
event: RestartEvent | PullEvent | FatalErrorEvent,
debugInfo?: string,
) {
transition(event: RestartEvent | PullEvent | FatalEvent, debugInfo?: string) {
const type = event.type;

@@ -726,4 +736,7 @@ if (type === EventType.Restart) {

return new PullState(this.state.initDocState, debugInfo);
} else if (type === EventType.FatalError) {
return new FatalErrorState({ message: event.payload.message }, debugInfo);
} else if (type === EventType.Fatal) {
return new FatalState(
{ message: event.payload.message, errorCode: event.payload.errorCode },
debugInfo,
);
} else {

@@ -759,5 +772,6 @@ let val: never = type;

{
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Stuck in error loop, last failure: ' + failure,
errorCode: FatalErrorCode.StuckInInfiniteLoop,
},

@@ -798,5 +812,6 @@ },

{
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Incorrect manager',
errorCode: FatalErrorCode.IncorrectManager,
},

@@ -814,5 +829,6 @@ },

{
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'History/Server not available',
errorCode: FatalErrorCode.HistoryNotAvailable,
},

@@ -831,5 +847,6 @@ },

{
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: 'Document not found',
errorCode: FatalErrorCode.DocumentNotFound,
},

@@ -859,5 +876,7 @@ },

{
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: `Cannot handle ${failure} in state=${collabState.name}`,
// TODO: is this the right error code?
errorCode: FatalErrorCode.UnexpectedState,
},

@@ -932,5 +951,6 @@ },

{
type: EventType.FatalError,
type: EventType.Fatal,
payload: {
message: `Cannot handle ${failure} in state=${collabState.name}`,
errorCode: FatalErrorCode.UnexpectedState,
},

@@ -937,0 +957,0 @@ },

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