gristlabs_grist-core/app/common/CommTypes.ts
Dmitry S a91d493ffc (core) Fix issue with 'UNEXPECTED ORDER OF CALLBACKS' in Client.ts.
Summary:
- Substantial refactoring of the logic when the server fails to send some
  messages to a client.
- Add seqId numbers to server messages to ensure reliable order.
- Add a needReload flag in clientConnect for a clear indication whent the
  browser client needs to reload the app.
- Reproduce some potential failure scenarios in a test case (some of which
  previously could have led to incorrectly ordered messages).
- Convert other Comm tests to typescript.
- Tweak logging of Comm and Client to be slightly more concise (in particular,
  avoid logging sessionId)

Note that despite the big refactoring, this only addresses a fairly rare
situation, with websocket failures while server is trying to send to the
client. It includes no improvements for failures while the client is sending to
the server.

(I looked for an existing library that would take care of these issues. A relevant article I found is https://docs.microsoft.com/en-us/azure/azure-web-pubsub/howto-develop-reliable-clients, but it doesn't include a library for both ends, and is still in review. Other libraries with similar purposes did not inspire enough confidence.)

Test Plan: New test cases, which reproduce some previously problematic scenarios.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3470
2022-06-16 23:51:14 -04:00

152 lines
4.8 KiB
TypeScript

import {ActionGroup} from 'app/common/ActionGroup';
import {DocAction} from 'app/common/DocActions';
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
import {Product} from 'app/common/Features';
import {StringUnion} from 'app/common/StringUnion';
import {UserProfile} from 'app/common/LoginSessionAPI';
export const ValidEvent = StringUnion('docListAction', 'docUserAction', 'docShutdown', 'docError',
'docUsage', 'clientConnect');
export type ValidEvent = typeof ValidEvent.type;
/**
* A request in the appropriate form for sending to the server.
*/
export interface CommRequest {
reqId: number;
method: string;
args: any[];
}
/**
* A regular, successful response from the server.
*/
export interface CommResponse {
reqId: number;
data: any;
error?: null; // TODO: keep until sure server never sets this on regular responses.
}
/**
* An exceptional response from the server when there is an error.
*/
export interface CommResponseError {
reqId: number;
error: string;
errorCode?: string;
shouldFork?: boolean; // if set, the server suggests forking the document.
details?: any; // if set, error has extra details available. TODO - the treatment of
// details could do with some harmonisation between rest API and ws API,
// and between front-end and back-end types.
}
/**
* A message pushed from the server, not in response to a request.
*/
export interface CommMessageBase {
type: ValidEvent;
docFD?: number;
data?: unknown;
}
export type CommDocMessage = CommDocUserAction | CommDocUsage | CommDocShutdown | CommDocError;
export type CommMessage = CommDocMessage | CommDocListAction | CommClientConnect;
export type CommResponseBase = CommResponse | CommResponseError | CommMessage;
export type CommDocEventType = CommDocMessage['type']
/**
* Event for a change to the document list.
* These are sent to all connected clients, regardless of which documents they have open.
* TODO: This is entirely unused at the moment.
*/
export interface CommDocListAction extends CommMessageBase {
type: 'docListAction';
addDocs?: string[]; // names of documents to add to the docList.
removeDocs?: string[]; // names of documents that got removed.
renameDocs?: string[]; // [oldName, newName] pairs for renamed docs.
addInvites?: string[]; // document invite names to add.
removeInvites?: string[]; // documents invite names to remove.
}
/**
* Event for a user action on a document, or part of one. Sent to all clients that have this
* document open.
*/
export interface CommDocUserAction extends CommMessageBase {
type: 'docUserAction';
docFD: number; // The file descriptor of the open document, specific to each client.
fromSelf?: boolean; // Flag to indicate whether the action originated from this client.
// ActionGroup object containing user action, and doc actions.
data: {
docActions: DocAction[];
actionGroup: ActionGroup;
docUsage: FilteredDocUsageSummary;
error?: string;
};
}
/**
* Event for a change to document usage. Sent to all clients that have this document open.
*/
export interface CommDocUsage extends CommMessageBase {
type: 'docUsage';
docFD: number; // The file descriptor of the open document, specific to each client.
data: {
docUsage: FilteredDocUsageSummary; // Document usage summary.
product?: Product; //Product that was used to compute `data.docUsage`
};
}
/**
* Event for when a document is forcibly shutdown, and requires the client to re-open it.
*/
export interface CommDocShutdown extends CommMessageBase {
type: 'docShutdown';
docFD: number;
data: null;
}
/**
* Event that signals an error while opening a doc.
*/
export interface CommDocError extends CommMessageBase {
type: 'docError';
docFD: number;
data: {
when: string;
message: string;
}
}
/**
* Event sent by server received when a client first connects.
*/
export interface CommClientConnect extends CommMessageBase {
type: 'clientConnect';
// ID for the client, which may be reused if a client reconnects to reattach to its state on
// the server.
clientId: string;
// If set, the reconnecting client cannot be sent all missed messages, and needs to reload.
needReload?: boolean;
// Array of serialized messages missed from the server while disconnected.
missedMessages?: string[];
// Which version the server reports for itself.
serverVersion?: string;
// Object containing server settings and features which should be used to initialize the client.
settings?: {[key: string]: unknown};
// Object containing session profile information if the user is signed in, or null otherwise.
profile: UserProfile|null;
dup?: boolean; // Flag that's set to true when it's a duplicate clientConnect message.
}