diff --git a/lib/types/message-types.js b/lib/types/message-types.js index b46985601..7e46bc65d 100644 --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -1,787 +1,786 @@ // @flow import { type ThreadInfo, threadInfoPropType, type ThreadType, threadTypePropType, } from './thread-types'; import { type RelativeUserInfo, relativeUserInfoPropType, type UserInfo, type UserInfos, } from './user-types'; import { type Media, type Image, mediaPropType } from './media-types'; import invariant from 'invariant'; import PropTypes from 'prop-types'; export const messageTypes = Object.freeze({ TEXT: 0, CREATE_THREAD: 1, ADD_MEMBERS: 2, CREATE_SUB_THREAD: 3, CHANGE_SETTINGS: 4, REMOVE_MEMBERS: 5, CHANGE_ROLE: 6, LEAVE_THREAD: 7, JOIN_THREAD: 8, CREATE_ENTRY: 9, EDIT_ENTRY: 10, DELETE_ENTRY: 11, RESTORE_ENTRY: 12, // When the server has a message to deliver that the client can't properly // render because the client is too old, the server will send this message // type instead. Consequently, there is no MessageData for UNSUPPORTED - just // a RawMessageInfo and a MessageInfo. Note that native/persist.js handles // converting these MessageInfos when the client is upgraded. UNSUPPORTED: 13, IMAGES: 14, MULTIMEDIA: 15, }); export type MessageType = $Values; export function assertMessageType(ourMessageType: number): MessageType { invariant( ourMessageType === 0 || ourMessageType === 1 || ourMessageType === 2 || ourMessageType === 3 || ourMessageType === 4 || ourMessageType === 5 || ourMessageType === 6 || ourMessageType === 7 || ourMessageType === 8 || ourMessageType === 9 || ourMessageType === 10 || ourMessageType === 11 || ourMessageType === 12 || ourMessageType === 13 || ourMessageType === 14 || ourMessageType === 15, 'number is not MessageType enum', ); return ourMessageType; } const composableMessageTypes = new Set([ messageTypes.TEXT, messageTypes.IMAGES, messageTypes.MULTIMEDIA, ]); export function isComposableMessageType(ourMessageType: MessageType): boolean { return composableMessageTypes.has(ourMessageType); } export function assertComposableMessageType( ourMessageType: MessageType, ): MessageType { invariant( isComposableMessageType(ourMessageType), 'MessageType is not composed', ); return ourMessageType; } export function messageDataLocalID(messageData: MessageData) { if ( messageData.type !== messageTypes.TEXT && messageData.type !== messageTypes.IMAGES && messageData.type !== messageTypes.MULTIMEDIA ) { return null; } return messageData.localID; } const mediaMessageTypes = new Set([ messageTypes.IMAGES, messageTypes.MULTIMEDIA, ]); export function isMediaMessageType(ourMessageType: MessageType): boolean { return mediaMessageTypes.has(ourMessageType); } export function assetMediaMessageType( ourMessageType: MessageType, ): MessageType { invariant(isMediaMessageType(ourMessageType), 'MessageType is not media'); return ourMessageType; } // *MessageData = passed to createMessages function to insert into database // Raw*MessageInfo = used by server, and contained in client's local store // *MessageInfo = used by client in UI code export type TextMessageData = {| type: 0, localID?: string, // for optimistic creations. included by new clients threadID: string, creatorID: string, time: number, text: string, |}; type CreateThreadMessageData = {| type: 1, threadID: string, creatorID: string, time: number, initialThreadState: {| type: ThreadType, name: ?string, parentThreadID: ?string, color: string, memberIDs: string[], |}, |}; type AddMembersMessageData = {| type: 2, threadID: string, creatorID: string, time: number, addedUserIDs: string[], |}; type CreateSubthreadMessageData = {| type: 3, threadID: string, creatorID: string, time: number, childThreadID: string, |}; type ChangeSettingsMessageData = {| type: 4, threadID: string, creatorID: string, time: number, field: string, value: string | number, |}; type RemoveMembersMessageData = {| type: 5, threadID: string, creatorID: string, time: number, removedUserIDs: string[], |}; type ChangeRoleMessageData = {| type: 6, threadID: string, creatorID: string, time: number, userIDs: string[], newRole: string, |}; type LeaveThreadMessageData = {| type: 7, threadID: string, creatorID: string, time: number, |}; type JoinThreadMessageData = {| type: 8, threadID: string, creatorID: string, time: number, |}; type CreateEntryMessageData = {| type: 9, threadID: string, creatorID: string, time: number, entryID: string, date: string, text: string, |}; type EditEntryMessageData = {| type: 10, threadID: string, creatorID: string, time: number, entryID: string, date: string, text: string, |}; type DeleteEntryMessageData = {| type: 11, threadID: string, creatorID: string, time: number, entryID: string, date: string, text: string, |}; type RestoreEntryMessageData = {| type: 12, threadID: string, creatorID: string, time: number, entryID: string, date: string, text: string, |}; export type ImagesMessageData = {| type: 14, localID?: string, // for optimistic creations. included by new clients threadID: string, creatorID: string, time: number, media: $ReadOnlyArray, |}; export type MediaMessageData = {| type: 15, localID?: string, // for optimistic creations. included by new clients threadID: string, creatorID: string, time: number, media: $ReadOnlyArray, |}; export type MessageData = | TextMessageData | CreateThreadMessageData | AddMembersMessageData | CreateSubthreadMessageData | ChangeSettingsMessageData | RemoveMembersMessageData | ChangeRoleMessageData | LeaveThreadMessageData | JoinThreadMessageData | CreateEntryMessageData | EditEntryMessageData | DeleteEntryMessageData | RestoreEntryMessageData | ImagesMessageData | MediaMessageData; export type MultimediaMessageData = ImagesMessageData | MediaMessageData; export type RawTextMessageInfo = {| ...TextMessageData, id?: string, // null if local copy without ID yet |}; export type RawImagesMessageInfo = {| ...ImagesMessageData, id?: string, // null if local copy without ID yet |}; export type RawMediaMessageInfo = {| ...MediaMessageData, id?: string, // null if local copy without ID yet |}; export type RawMultimediaMessageInfo = | RawImagesMessageInfo | RawMediaMessageInfo; export type RawComposableMessageInfo = | RawTextMessageInfo | RawMultimediaMessageInfo; type RawRobotextMessageInfo = | {| ...CreateThreadMessageData, id: string, |} | {| ...AddMembersMessageData, id: string, |} | {| ...CreateSubthreadMessageData, id: string, |} | {| ...ChangeSettingsMessageData, id: string, |} | {| ...RemoveMembersMessageData, id: string, |} | {| ...ChangeRoleMessageData, id: string, |} | {| ...LeaveThreadMessageData, id: string, |} | {| ...JoinThreadMessageData, id: string, |} | {| ...CreateEntryMessageData, id: string, |} | {| ...EditEntryMessageData, id: string, |} | {| ...DeleteEntryMessageData, id: string, |} | {| ...RestoreEntryMessageData, id: string, |} | {| type: 13, id: string, threadID: string, creatorID: string, time: number, robotext: string, unsupportedMessageInfo: Object, |}; export type RawMessageInfo = RawComposableMessageInfo | RawRobotextMessageInfo; export type LocallyComposedMessageInfo = { localID: string, threadID: string, ... }; export type TextMessageInfo = {| type: 0, id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, creator: RelativeUserInfo, time: number, // millisecond timestamp text: string, |}; export type ImagesMessageInfo = {| type: 14, id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, creator: RelativeUserInfo, time: number, // millisecond timestamp media: $ReadOnlyArray, |}; export type MediaMessageInfo = {| type: 15, id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, creator: RelativeUserInfo, time: number, // millisecond timestamp media: $ReadOnlyArray, |}; export type MultimediaMessageInfo = ImagesMessageInfo | MediaMessageInfo; export type ComposableMessageInfo = TextMessageInfo | MultimediaMessageInfo; export type RobotextMessageInfo = | {| type: 1, id: string, threadID: string, creator: RelativeUserInfo, time: number, initialThreadState: {| type: ThreadType, name: ?string, parentThreadInfo: ?ThreadInfo, color: string, otherMembers: RelativeUserInfo[], |}, |} | {| type: 2, id: string, threadID: string, creator: RelativeUserInfo, time: number, addedMembers: RelativeUserInfo[], |} | {| type: 3, id: string, threadID: string, creator: RelativeUserInfo, time: number, childThreadInfo: ThreadInfo, |} | {| type: 4, id: string, threadID: string, creator: RelativeUserInfo, time: number, field: string, value: string | number, |} | {| type: 5, id: string, threadID: string, creator: RelativeUserInfo, time: number, removedMembers: RelativeUserInfo[], |} | {| type: 6, id: string, threadID: string, creator: RelativeUserInfo, time: number, members: RelativeUserInfo[], newRole: string, |} | {| type: 7, id: string, threadID: string, creator: RelativeUserInfo, time: number, |} | {| type: 8, id: string, threadID: string, creator: RelativeUserInfo, time: number, |} | {| type: 9, id: string, threadID: string, creator: RelativeUserInfo, time: number, entryID: string, date: string, text: string, |} | {| type: 10, id: string, threadID: string, creator: RelativeUserInfo, time: number, entryID: string, date: string, text: string, |} | {| type: 11, id: string, threadID: string, creator: RelativeUserInfo, time: number, entryID: string, date: string, text: string, |} | {| type: 12, id: string, threadID: string, creator: RelativeUserInfo, time: number, entryID: string, date: string, text: string, |} | {| type: 13, id: string, threadID: string, creator: RelativeUserInfo, time: number, robotext: string, unsupportedMessageInfo: Object, |}; export type PreviewableMessageInfo = | RobotextMessageInfo | MultimediaMessageInfo; export type MessageInfo = ComposableMessageInfo | RobotextMessageInfo; export const messageInfoPropType = PropTypes.oneOfType([ PropTypes.shape({ type: PropTypes.oneOf([messageTypes.TEXT]).isRequired, id: PropTypes.string, localID: PropTypes.string, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, text: PropTypes.string.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.CREATE_THREAD]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, initialThreadState: PropTypes.shape({ type: threadTypePropType.isRequired, name: PropTypes.string, parentThreadInfo: threadInfoPropType, color: PropTypes.string.isRequired, otherMembers: PropTypes.arrayOf(relativeUserInfoPropType).isRequired, }).isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.ADD_MEMBERS]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, addedMembers: PropTypes.arrayOf(relativeUserInfoPropType).isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.CREATE_SUB_THREAD]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, childThreadInfo: threadInfoPropType.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.CHANGE_SETTINGS]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, field: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.REMOVE_MEMBERS]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, removedMembers: PropTypes.arrayOf(relativeUserInfoPropType).isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.CHANGE_ROLE]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, members: PropTypes.arrayOf(relativeUserInfoPropType).isRequired, newRole: PropTypes.string.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.LEAVE_THREAD]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.JOIN_THREAD]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.CREATE_ENTRY]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, entryID: PropTypes.string.isRequired, date: PropTypes.string.isRequired, text: PropTypes.string.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.EDIT_ENTRY]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, entryID: PropTypes.string.isRequired, date: PropTypes.string.isRequired, text: PropTypes.string.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.DELETE_ENTRY]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, entryID: PropTypes.string.isRequired, date: PropTypes.string.isRequired, text: PropTypes.string.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.RESTORE_ENTRY]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, entryID: PropTypes.string.isRequired, date: PropTypes.string.isRequired, text: PropTypes.string.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.UNSUPPORTED]).isRequired, id: PropTypes.string.isRequired, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, robotext: PropTypes.string.isRequired, unsupportedMessageInfo: PropTypes.object.isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.IMAGES]).isRequired, id: PropTypes.string, localID: PropTypes.string, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, media: PropTypes.arrayOf(mediaPropType).isRequired, }), PropTypes.shape({ type: PropTypes.oneOf([messageTypes.MULTIMEDIA]).isRequired, id: PropTypes.string, localID: PropTypes.string, threadID: PropTypes.string.isRequired, creator: relativeUserInfoPropType.isRequired, time: PropTypes.number.isRequired, media: PropTypes.arrayOf(mediaPropType).isRequired, }), ]); export type ThreadMessageInfo = {| messageIDs: string[], startReached: boolean, lastNavigatedTo: number, // millisecond timestamp lastPruned: number, // millisecond timestamp |}; // Tracks client-local information about a message that hasn't been assigned an // ID by the server yet. As soon as the client gets an ack from the server for // this message, it will clear the LocalMessageInfo. export type LocalMessageInfo = {| sendFailed?: boolean, |}; export const localMessageInfoPropType = PropTypes.shape({ sendFailed: PropTypes.bool, }); export type MessageStore = {| messages: { [id: string]: RawMessageInfo }, threads: { [threadID: string]: ThreadMessageInfo }, local: { [id: string]: LocalMessageInfo }, currentAsOf: number, |}; export const messageTruncationStatus = Object.freeze({ // EXHAUSTIVE means we've reached the start of the thread. Either the result // set includes the very first message for that thread, or there is nothing // behind the cursor you queried for. Given that the client only ever issues // ranged queries whose range, when unioned with what is in state, represent // the set of all messages for a given thread, we can guarantee that getting // EXHAUSTIVE means the start has been reached. EXHAUSTIVE: 'exhaustive', // TRUNCATED is rare, and means that the server can't guarantee that the // result set for a given thread is contiguous with what the client has in its // state. If the client can't verify the contiguousness itself, it needs to // replace its Redux store's contents with what it is in this payload. // 1) getMessageInfosSince: Result set for thread is equal to max, and the // truncation status isn't EXHAUSTIVE (ie. doesn't include the very first // message). // 2) getMessageInfos: ThreadSelectionCriteria does not specify cursors, the // result set for thread is equal to max, and the truncation status isn't // EXHAUSTIVE. If cursors are specified, we never return truncated, since // the cursor given us guarantees the contiguousness of the result set. // Note that in the reducer, we can guarantee contiguousness if there is any // intersection between messageIDs in the result set and the set currently in // the Redux store. TRUNCATED: 'truncated', // UNCHANGED means the result set is guaranteed to be contiguous with what the // client has in its state, but is not EXHAUSTIVE. Basically, it's anything // that isn't either EXHAUSTIVE or TRUNCATED. UNCHANGED: 'unchanged', }); export type MessageTruncationStatus = $Values; export function assertMessageTruncationStatus( ourMessageTruncationStatus: string, ): MessageTruncationStatus { invariant( ourMessageTruncationStatus === 'truncated' || ourMessageTruncationStatus === 'unchanged' || ourMessageTruncationStatus === 'exhaustive', 'string is not ourMessageTruncationStatus enum', ); return ourMessageTruncationStatus; } export type MessageTruncationStatuses = { [threadID: string]: MessageTruncationStatus, }; export type ThreadCursors = { [threadID: string]: ?string }; export type ThreadSelectionCriteria = {| threadCursors?: ?ThreadCursors, joinedThreads?: ?boolean, |}; export type FetchMessageInfosRequest = {| cursors: ThreadCursors, numberPerThread?: ?number, |}; export type FetchMessageInfosResult = {| rawMessageInfos: RawMessageInfo[], truncationStatuses: MessageTruncationStatuses, userInfos: UserInfos, |}; export type FetchMessageInfosPayload = {| threadID: string, rawMessageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatus, userInfos: UserInfo[], |}; export type MessagesResponse = {| rawMessageInfos: RawMessageInfo[], truncationStatuses: MessageTruncationStatuses, currentAsOf: number, |}; export const defaultNumberPerThread = 20; export type SendMessageResponse = {| newMessageInfo: RawMessageInfo, |}; export type SendMessageResult = {| id: string, time: number, |}; export type SendMessagePayload = {| localID: string, serverID: string, threadID: string, time: number, |}; export type SendTextMessageRequest = {| threadID: string, localID?: string, text: string, |}; export type SendMultimediaMessageRequest = {| threadID: string, localID: string, mediaIDs: $ReadOnlyArray, |}; // Used for the message info included in log-in type actions export type GenericMessagesResult = {| messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: $ReadOnlyArray, currentAsOf: number, |}; export type SaveMessagesPayload = {| rawMessageInfos: $ReadOnlyArray, updatesCurrentAsOf: number, |}; -export type MessagesResultWithUserInfos = {| +export type NewMessagesPayload = {| messagesResult: MessagesResponse, - userInfos: $ReadOnlyArray, |}; export type MessageStorePrunePayload = {| threadIDs: $ReadOnlyArray, |}; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index c2bcf3644..aa4c82f80 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,812 +1,812 @@ // @flow import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, } from './thread-types'; import type { RawEntryInfo, EntryStore, CalendarQuery, SaveEntryPayload, CreateEntryPayload, DeleteEntryResponse, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, } from './entry-types'; import type { LoadingStatus, LoadingInfo } from './loading-types'; import type { BaseNavInfo } from './nav-types'; import type { CurrentUserInfo, UserStore } from './user-types'; import type { LogOutResult, LogInStartingPayload, LogInResult, RegisterResult, } from './account-types'; import type { UserSearchResult } from '../types/search-types'; import type { MessageStore, RawTextMessageInfo, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, SaveMessagesPayload, - MessagesResultWithUserInfos, + NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, } from './message-types'; import type { SetSessionPayload } from './session-types'; import type { ProcessServerRequestsPayload } from './request-types'; import type { ClearDeliveredReportsPayload, ClientReportCreationRequest, QueueReportsPayload, } from './report-types'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types'; import type { SubscriptionUpdateResult } from './subscription-types'; import type { ConnectionInfo, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateDisconnectedBarPayload, } from './socket-types'; import type { UpdatesResultWithUserInfos } from './update-types'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, } from './activity-types'; import type { UpdateMultimediaMessageMediaPayload } from './media-types'; export type BaseAppState = { navInfo: NavInfo, currentUserInfo: ?CurrentUserInfo, entryStore: EntryStore, threadStore: ThreadStore, userStore: UserStore, messageStore: MessageStore, updatesCurrentAsOf: number, // millisecond timestamp loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, calendarFilters: $ReadOnlyArray, urlPrefix: string, connection: ConnectionInfo, watchedThreadIDs: $ReadOnlyArray, foreground: boolean, nextLocalID: number, queuedReports: $ReadOnlyArray, dataLoaded: boolean, }; // Web JS runtime doesn't have access to the cookie for security reasons. // Native JS doesn't have a sessionID because the cookieID is used instead. // Web JS doesn't have a device token because it's not a device... export type NativeAppState = BaseAppState<*> & { sessionID?: void, deviceToken: ?string, cookie: ?string, }; export type WebAppState = BaseAppState<*> & { sessionID: ?string, deviceToken?: void, cookie?: void, }; export type AppState = NativeAppState | WebAppState; export type BaseAction = | {| type: '@@redux/INIT', payload?: void, |} | {| type: 'FETCH_ENTRIES_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_ENTRIES_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_ENTRIES_SUCCESS', payload: FetchEntryInfosResult, loadingInfo: LoadingInfo, |} | {| type: 'LOG_OUT_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'LOG_OUT_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'LOG_OUT_SUCCESS', payload: LogOutResult, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_ACCOUNT_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_ACCOUNT_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_ACCOUNT_SUCCESS', payload: LogOutResult, loadingInfo: LoadingInfo, |} | {| type: 'CREATE_LOCAL_ENTRY', payload: RawEntryInfo, |} | {| type: 'CREATE_ENTRY_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'CREATE_ENTRY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'CREATE_ENTRY_SUCCESS', payload: CreateEntryPayload, loadingInfo: LoadingInfo, |} | {| type: 'SAVE_ENTRY_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'SAVE_ENTRY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'SAVE_ENTRY_SUCCESS', payload: SaveEntryPayload, loadingInfo: LoadingInfo, |} | {| type: 'CONCURRENT_MODIFICATION_RESET', payload: {| id: string, dbText: string, |}, |} | {| type: 'DELETE_ENTRY_STARTED', loadingInfo: LoadingInfo, payload: {| localID: ?string, serverID: ?string, |}, |} | {| type: 'DELETE_ENTRY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_ENTRY_SUCCESS', payload: ?DeleteEntryResponse, loadingInfo: LoadingInfo, |} | {| type: 'LOG_IN_STARTED', loadingInfo: LoadingInfo, payload: LogInStartingPayload, |} | {| type: 'LOG_IN_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'LOG_IN_SUCCESS', payload: LogInResult, loadingInfo: LoadingInfo, |} | {| type: 'REGISTER_STARTED', loadingInfo: LoadingInfo, payload: LogInStartingPayload, |} | {| type: 'REGISTER_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'REGISTER_SUCCESS', payload: RegisterResult, loadingInfo: LoadingInfo, |} | {| type: 'RESET_PASSWORD_STARTED', payload: {| calendarQuery: CalendarQuery |}, loadingInfo: LoadingInfo, |} | {| type: 'RESET_PASSWORD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'RESET_PASSWORD_SUCCESS', payload: LogInResult, loadingInfo: LoadingInfo, |} | {| type: 'FORGOT_PASSWORD_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'FORGOT_PASSWORD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'FORGOT_PASSWORD_SUCCESS', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_USER_SETTINGS_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_USER_SETTINGS_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_USER_SETTINGS_SUCCESS', payload: {| email: string, |}, loadingInfo: LoadingInfo, |} | {| type: 'RESEND_VERIFICATION_EMAIL_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'RESEND_VERIFICATION_EMAIL_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'RESEND_VERIFICATION_EMAIL_SUCCESS', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_THREAD_SETTINGS_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_THREAD_SETTINGS_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_THREAD_SETTINGS_SUCCESS', payload: ChangeThreadSettingsPayload, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_THREAD_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_THREAD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'DELETE_THREAD_SUCCESS', payload: LeaveThreadPayload, loadingInfo: LoadingInfo, |} | {| type: 'NEW_THREAD_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'NEW_THREAD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'NEW_THREAD_SUCCESS', payload: NewThreadResult, loadingInfo: LoadingInfo, |} | {| type: 'REMOVE_USERS_FROM_THREAD_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'REMOVE_USERS_FROM_THREAD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'REMOVE_USERS_FROM_THREAD_SUCCESS', payload: ChangeThreadSettingsPayload, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', payload: ChangeThreadSettingsPayload, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', payload: {| entryID: string, text: string, deleted: boolean, |}, loadingInfo: LoadingInfo, |} | {| type: 'RESTORE_ENTRY_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'RESTORE_ENTRY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'RESTORE_ENTRY_SUCCESS', payload: RestoreEntryPayload, loadingInfo: LoadingInfo, |} | {| type: 'JOIN_THREAD_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'JOIN_THREAD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'JOIN_THREAD_SUCCESS', payload: ThreadJoinPayload, loadingInfo: LoadingInfo, |} | {| type: 'LEAVE_THREAD_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'LEAVE_THREAD_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'LEAVE_THREAD_SUCCESS', payload: LeaveThreadPayload, loadingInfo: LoadingInfo, |} | {| type: 'SET_NEW_SESSION', payload: SetSessionPayload, |} | {| type: 'persist/REHYDRATE', payload: ?BaseAppState<*>, |} | {| type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', payload: FetchMessageInfosPayload, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_MOST_RECENT_MESSAGES_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_MOST_RECENT_MESSAGES_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', payload: FetchMessageInfosPayload, loadingInfo: LoadingInfo, |} | {| type: 'SEND_TEXT_MESSAGE_STARTED', loadingInfo: LoadingInfo, payload: RawTextMessageInfo, |} | {| type: 'SEND_TEXT_MESSAGE_FAILED', error: true, payload: Error & { localID: string, threadID: string, }, loadingInfo: LoadingInfo, |} | {| type: 'SEND_TEXT_MESSAGE_SUCCESS', payload: SendMessagePayload, loadingInfo: LoadingInfo, |} | {| type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', loadingInfo: LoadingInfo, payload: RawMultimediaMessageInfo, |} | {| type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', error: true, payload: Error & { localID: string, threadID: string, }, loadingInfo: LoadingInfo, |} | {| type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', payload: SendMessagePayload, loadingInfo: LoadingInfo, |} | {| type: 'SEARCH_USERS_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'SEARCH_USERS_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'SEARCH_USERS_SUCCESS', payload: UserSearchResult, loadingInfo: LoadingInfo, |} | {| type: 'SAVE_DRAFT', payload: { key: string, draft: string, }, |} | {| type: 'UPDATE_ACTIVITY_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_ACTIVITY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_ACTIVITY_SUCCESS', payload: ActivityUpdateSuccessPayload, loadingInfo: LoadingInfo, |} | {| type: 'SET_DEVICE_TOKEN_STARTED', payload: string, loadingInfo: LoadingInfo, |} | {| type: 'SET_DEVICE_TOKEN_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'SET_DEVICE_TOKEN_SUCCESS', payload: string, loadingInfo: LoadingInfo, |} | {| type: 'HANDLE_VERIFICATION_CODE_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'HANDLE_VERIFICATION_CODE_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'HANDLE_VERIFICATION_CODE_SUCCESS', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'SEND_REPORT_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'SEND_REPORT_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'SEND_REPORT_SUCCESS', payload?: ClearDeliveredReportsPayload, loadingInfo: LoadingInfo, |} | {| type: 'SEND_REPORTS_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'SEND_REPORTS_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'SEND_REPORTS_SUCCESS', payload?: ClearDeliveredReportsPayload, loadingInfo: LoadingInfo, |} | {| type: 'QUEUE_REPORTS', payload: QueueReportsPayload, |} | {| type: 'SET_URL_PREFIX', payload: string, |} | {| type: 'SAVE_MESSAGES', payload: SaveMessagesPayload, |} | {| type: 'UPDATE_CALENDAR_THREAD_FILTER', payload: CalendarThreadFilter, |} | {| type: 'CLEAR_CALENDAR_THREAD_FILTER', payload?: void, |} | {| type: 'SET_CALENDAR_DELETED_FILTER', payload: SetCalendarDeletedFilterPayload, |} | {| type: 'UPDATE_SUBSCRIPTION_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_SUBSCRIPTION_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_SUBSCRIPTION_SUCCESS', payload: SubscriptionUpdateResult, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_CALENDAR_QUERY_STARTED', loadingInfo: LoadingInfo, payload?: CalendarQueryUpdateStartingPayload, |} | {| type: 'UPDATE_CALENDAR_QUERY_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_CALENDAR_QUERY_SUCCESS', payload: CalendarQueryUpdateResult, loadingInfo: LoadingInfo, |} | {| type: 'FULL_STATE_SYNC', payload: StateSyncFullActionPayload, |} | {| type: 'INCREMENTAL_STATE_SYNC', payload: StateSyncIncrementalActionPayload, |} | {| type: 'PROCESS_SERVER_REQUESTS', payload: ProcessServerRequestsPayload, |} | {| type: 'UPDATE_CONNECTION_STATUS', payload: UpdateConnectionStatusPayload, |} | {| type: 'QUEUE_ACTIVITY_UPDATES', payload: QueueActivityUpdatesPayload, |} | {| type: 'FOREGROUND', payload?: void, |} | {| type: 'BACKGROUND', payload?: void, |} | {| type: 'UNSUPERVISED_BACKGROUND', payload?: void, |} | {| type: 'PROCESS_UPDATES', payload: UpdatesResultWithUserInfos, |} | {| type: 'PROCESS_MESSAGES', - payload: MessagesResultWithUserInfos, + payload: NewMessagesPayload, |} | {| type: 'MESSAGE_STORE_PRUNE', payload: MessageStorePrunePayload, |} | {| type: 'SET_LATE_RESPONSE', payload: SetLateResponsePayload, |} | {| type: 'UPDATE_DISCONNECTED_BAR', payload: UpdateDisconnectedBarPayload, |} | {| type: 'REQUEST_ACCESS_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'REQUEST_ACCESS_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'REQUEST_ACCESS_SUCCESS', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', payload: UpdateMultimediaMessageMediaPayload, |} | {| type: 'CREATE_LOCAL_MESSAGE', payload: LocallyComposedMessageInfo, |} | {| type: 'UPDATE_RELATIONSHIPS_STARTED', payload?: void, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_RELATIONSHIPS_FAILED', error: true, payload: Error, loadingInfo: LoadingInfo, |} | {| type: 'UPDATE_RELATIONSHIPS_SUCCESS', payload?: void, loadingInfo: LoadingInfo, |}; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type SuperAction = { type: string, payload?: ActionPayload, loadingInfo?: LoadingInfo, error?: boolean, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; export type Dispatch = ((promisedAction: PromisedAction) => Promise) & ((thunkedAction: ThunkedAction) => void) & ((action: SuperAction) => boolean); // This is lifted from redux-persist/lib/constants.js // I don't want to add redux-persist to the web/server bundles... // import { REHYDRATE } from 'redux-persist'; export const rehydrateActionType = 'persist/REHYDRATE'; diff --git a/lib/types/socket-types.js b/lib/types/socket-types.js index ea0ff0643..c43c93470 100644 --- a/lib/types/socket-types.js +++ b/lib/types/socket-types.js @@ -1,319 +1,316 @@ // @flow import type { SessionState, SessionIdentification } from './session-types'; import type { ServerRequest, ClientResponse, ClientClientResponse, } from './request-types'; import type { RawThreadInfo } from './thread-types'; -import type { - MessagesResponse, - MessagesResultWithUserInfos, -} from './message-types'; +import type { MessagesResponse, NewMessagesPayload } from './message-types'; import type { UpdatesResult, UpdatesResultWithUserInfos } from './update-types'; import type { UserInfo, CurrentUserInfo, LoggedOutUserInfo, } from './user-types'; import { type RawEntryInfo, type CalendarQuery, defaultCalendarQuery, calendarQueryPropType, } from './entry-types'; import { type ActivityUpdate, type UpdateActivityResult, activityUpdatePropType, } from './activity-types'; import type { Platform } from './device-types'; import type { APIRequest } from './endpoints'; import invariant from 'invariant'; import PropTypes from 'prop-types'; // The types of messages that the client sends across the socket export const clientSocketMessageTypes = Object.freeze({ INITIAL: 0, RESPONSES: 1, //ACTIVITY_UPDATES: 2, (DEPRECATED) PING: 3, ACK_UPDATES: 4, API_REQUEST: 5, }); export type ClientSocketMessageType = $Values; export function assertClientSocketMessageType( ourClientSocketMessageType: number, ): ClientSocketMessageType { invariant( ourClientSocketMessageType === 0 || ourClientSocketMessageType === 1 || ourClientSocketMessageType === 3 || ourClientSocketMessageType === 4 || ourClientSocketMessageType === 5, 'number is not ClientSocketMessageType enum', ); return ourClientSocketMessageType; } export type InitialClientSocketMessage = {| type: 0, id: number, payload: {| sessionIdentification: SessionIdentification, sessionState: SessionState, clientResponses: $ReadOnlyArray, |}, |}; export type ResponsesClientSocketMessage = {| type: 1, id: number, payload: {| clientResponses: $ReadOnlyArray, |}, |}; export type PingClientSocketMessage = {| type: 3, id: number, |}; export type AckUpdatesClientSocketMessage = {| type: 4, id: number, payload: {| currentAsOf: number, |}, |}; export type APIRequestClientSocketMessage = {| type: 5, id: number, payload: APIRequest, |}; export type ClientSocketMessage = | InitialClientSocketMessage | ResponsesClientSocketMessage | PingClientSocketMessage | AckUpdatesClientSocketMessage | APIRequestClientSocketMessage; export type ClientInitialClientSocketMessage = {| type: 0, id: number, payload: {| sessionIdentification: SessionIdentification, sessionState: SessionState, clientResponses: $ReadOnlyArray, |}, |}; export type ClientResponsesClientSocketMessage = {| type: 1, id: number, payload: {| clientResponses: $ReadOnlyArray, |}, |}; export type ClientClientSocketMessage = | ClientInitialClientSocketMessage | ClientResponsesClientSocketMessage | PingClientSocketMessage | AckUpdatesClientSocketMessage | APIRequestClientSocketMessage; export type ClientSocketMessageWithoutID = $Diff< ClientClientSocketMessage, { id: number }, >; // The types of messages that the server sends across the socket export const serverSocketMessageTypes = Object.freeze({ STATE_SYNC: 0, REQUESTS: 1, ERROR: 2, AUTH_ERROR: 3, ACTIVITY_UPDATE_RESPONSE: 4, PONG: 5, UPDATES: 6, MESSAGES: 7, API_RESPONSE: 8, }); export type ServerSocketMessageType = $Values; export function assertServerSocketMessageType( ourServerSocketMessageType: number, ): ServerSocketMessageType { invariant( ourServerSocketMessageType === 0 || ourServerSocketMessageType === 1 || ourServerSocketMessageType === 2 || ourServerSocketMessageType === 3 || ourServerSocketMessageType === 4 || ourServerSocketMessageType === 5 || ourServerSocketMessageType === 6 || ourServerSocketMessageType === 7 || ourServerSocketMessageType === 8, 'number is not ServerSocketMessageType enum', ); return ourServerSocketMessageType; } export const stateSyncPayloadTypes = Object.freeze({ FULL: 0, INCREMENTAL: 1, }); export type FullStateSync = {| messagesResult: MessagesResponse, threadInfos: { [id: string]: RawThreadInfo }, currentUserInfo: CurrentUserInfo, rawEntryInfos: $ReadOnlyArray, userInfos: $ReadOnlyArray, updatesCurrentAsOf: number, |}; export type StateSyncFullActionPayload = {| ...FullStateSync, calendarQuery: CalendarQuery, |}; export const fullStateSyncActionType = 'FULL_STATE_SYNC'; export type StateSyncFullSocketPayload = {| ...FullStateSync, type: 0, // Included iff client is using sessionIdentifierTypes.BODY_SESSION_ID sessionID?: string, |}; export type IncrementalStateSync = {| messagesResult: MessagesResponse, updatesResult: UpdatesResult, deltaEntryInfos: $ReadOnlyArray, deletedEntryIDs: $ReadOnlyArray, userInfos: $ReadOnlyArray, |}; export type StateSyncIncrementalActionPayload = {| ...IncrementalStateSync, calendarQuery: CalendarQuery, |}; export const incrementalStateSyncActionType = 'INCREMENTAL_STATE_SYNC'; type StateSyncIncrementalSocketPayload = {| type: 1, ...IncrementalStateSync, |}; export type StateSyncSocketPayload = | StateSyncFullSocketPayload | StateSyncIncrementalSocketPayload; export type StateSyncServerSocketMessage = {| type: 0, responseTo: number, payload: StateSyncSocketPayload, |}; export type RequestsServerSocketMessage = {| type: 1, responseTo?: number, payload: {| serverRequests: $ReadOnlyArray, |}, |}; export type ErrorServerSocketMessage = {| type: 2, responseTo?: number, message: string, payload?: Object, |}; export type AuthErrorServerSocketMessage = {| type: 3, responseTo: number, message: string, // If unspecified, it is because the client is using cookieSources.HEADER, // which means the server can't update the cookie from a socket message. sessionChange?: { cookie: string, currentUserInfo: LoggedOutUserInfo, }, |}; export type ActivityUpdateResponseServerSocketMessage = {| type: 4, responseTo: number, payload: UpdateActivityResult, |}; export type PongServerSocketMessage = {| type: 5, responseTo: number, |}; export type UpdatesServerSocketMessage = {| type: 6, payload: UpdatesResultWithUserInfos, |}; export type MessagesServerSocketMessage = {| type: 7, - payload: MessagesResultWithUserInfos, + payload: NewMessagesPayload, |}; export type APIResponseServerSocketMessage = {| type: 8, responseTo: number, payload: Object, |}; export type ServerSocketMessage = | StateSyncServerSocketMessage | RequestsServerSocketMessage | ErrorServerSocketMessage | AuthErrorServerSocketMessage | ActivityUpdateResponseServerSocketMessage | PongServerSocketMessage | UpdatesServerSocketMessage | MessagesServerSocketMessage | APIResponseServerSocketMessage; export type SocketListener = (message: ServerSocketMessage) => void; export type ConnectionStatus = | 'connecting' | 'connected' | 'reconnecting' | 'disconnecting' | 'forcedDisconnecting' | 'disconnected'; export type ConnectionInfo = {| status: ConnectionStatus, queuedActivityUpdates: $ReadOnlyArray, actualizedCalendarQuery: CalendarQuery, lateResponses: $ReadOnlyArray, showDisconnectedBar: boolean, |}; export const connectionStatusPropType = PropTypes.oneOf([ 'connecting', 'connected', 'reconnecting', 'disconnecting', 'forcedDisconnecting', 'disconnected', ]); export const connectionInfoPropType = PropTypes.shape({ status: connectionStatusPropType.isRequired, queuedActivityUpdates: PropTypes.arrayOf(activityUpdatePropType).isRequired, actualizedCalendarQuery: calendarQueryPropType.isRequired, lateResponses: PropTypes.arrayOf(PropTypes.number).isRequired, showDisconnectedBar: PropTypes.bool.isRequired, }); export const defaultConnectionInfo = ( platform: Platform, timeZone?: ?string, ) => ({ status: 'connecting', queuedActivityUpdates: [], actualizedCalendarQuery: defaultCalendarQuery(platform, timeZone), lateResponses: [], showDisconnectedBar: false, }); export const updateConnectionStatusActionType = 'UPDATE_CONNECTION_STATUS'; export type UpdateConnectionStatusPayload = {| status: ConnectionStatus, |}; export const setLateResponseActionType = 'SET_LATE_RESPONSE'; export type SetLateResponsePayload = {| messageID: number, isLate: boolean, |}; export const updateDisconnectedBarActionType = 'UPDATE_DISCONNECTED_BAR'; export type UpdateDisconnectedBarPayload = {| visible: boolean |}; diff --git a/server/src/socket/socket.js b/server/src/socket/socket.js index 2460b6c0c..f5028c1cf 100644 --- a/server/src/socket/socket.js +++ b/server/src/socket/socket.js @@ -1,791 +1,790 @@ // @flow import type { WebSocket } from 'ws'; import type { $Request } from 'express'; import { type ClientSocketMessage, type InitialClientSocketMessage, type ResponsesClientSocketMessage, type StateSyncFullSocketPayload, type ServerSocketMessage, type ErrorServerSocketMessage, type AuthErrorServerSocketMessage, type PingClientSocketMessage, type AckUpdatesClientSocketMessage, type APIRequestClientSocketMessage, clientSocketMessageTypes, stateSyncPayloadTypes, serverSocketMessageTypes, } from 'lib/types/socket-types'; import { cookieSources, sessionCheckFrequency, stateCheckInactivityActivationInterval, } from 'lib/types/session-types'; import { defaultNumberPerThread } from 'lib/types/message-types'; import { redisMessageTypes, type RedisMessage } from 'lib/types/redis-types'; import { endpointIsSocketSafe } from 'lib/types/endpoints'; import t from 'tcomb'; import invariant from 'invariant'; import _debounce from 'lodash/debounce'; import { ServerError } from 'lib/utils/errors'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils'; import { mostRecentUpdateTimestamp } from 'lib/shared/update-utils'; import { promiseAll } from 'lib/utils/promises'; import { values } from 'lib/utils/objects'; import { serverRequestSocketTimeout } from 'lib/shared/timeouts'; import { Viewer } from '../session/viewer'; import { checkInputValidator, checkClientSupported, tShape, tCookie, } from '../utils/validation-utils'; import { newEntryQueryInputValidator, verifyCalendarQueryThreadIDs, } from '../responders/entry-responders'; import { clientResponseInputValidator, processClientResponses, initializeSession, checkState, } from './session-utils'; import { assertSecureRequest } from '../utils/security-utils'; import { fetchViewerForSocket, extendCookieLifespan, createNewAnonymousCookie, } from '../session/cookies'; import { fetchMessageInfosSince, getMessageFetchResultFromRedisMessages, } from '../fetchers/message-fetchers'; import { fetchThreadInfos } from '../fetchers/thread-fetchers'; import { fetchEntryInfos } from '../fetchers/entry-fetchers'; import { fetchCurrentUserInfo, fetchKnownUserInfos, } from '../fetchers/user-fetchers'; import { updateActivityTime } from '../updaters/activity-updaters'; import { deleteUpdatesBeforeTimeTargettingSession } from '../deleters/update-deleters'; import { fetchUpdateInfos } from '../fetchers/update-fetchers'; import { commitSessionUpdate } from '../updaters/session-updaters'; import { handleAsyncPromise } from '../responders/handlers'; import { deleteCookie } from '../deleters/cookie-deleters'; import { deleteActivityForViewerSession } from '../deleters/activity-deleters'; import { RedisSubscriber } from './redis'; import { fetchUpdateInfosWithRawUpdateInfos } from '../creators/update-creator'; import { jsonEndpoints } from '../endpoints'; const clientSocketMessageInputValidator = t.union([ tShape({ type: t.irreducible( 'clientSocketMessageTypes.INITIAL', x => x === clientSocketMessageTypes.INITIAL, ), id: t.Number, payload: tShape({ sessionIdentification: tShape({ cookie: t.maybe(tCookie), sessionID: t.maybe(t.String), }), sessionState: tShape({ calendarQuery: newEntryQueryInputValidator, messagesCurrentAsOf: t.Number, updatesCurrentAsOf: t.Number, watchedIDs: t.list(t.String), }), clientResponses: t.list(clientResponseInputValidator), }), }), tShape({ type: t.irreducible( 'clientSocketMessageTypes.RESPONSES', x => x === clientSocketMessageTypes.RESPONSES, ), id: t.Number, payload: tShape({ clientResponses: t.list(clientResponseInputValidator), }), }), tShape({ type: t.irreducible( 'clientSocketMessageTypes.PING', x => x === clientSocketMessageTypes.PING, ), id: t.Number, }), tShape({ type: t.irreducible( 'clientSocketMessageTypes.ACK_UPDATES', x => x === clientSocketMessageTypes.ACK_UPDATES, ), id: t.Number, payload: tShape({ currentAsOf: t.Number, }), }), tShape({ type: t.irreducible( 'clientSocketMessageTypes.API_REQUEST', x => x === clientSocketMessageTypes.API_REQUEST, ), id: t.Number, payload: tShape({ endpoint: t.String, input: t.Object, }), }), ]); function onConnection(ws: WebSocket, req: $Request) { assertSecureRequest(req); new Socket(ws, req); } type StateCheckConditions = {| activityRecentlyOccurred: boolean, stateCheckOngoing: boolean, |}; class Socket { ws: WebSocket; httpRequest: $Request; viewer: ?Viewer; redis: ?RedisSubscriber; stateCheckConditions: StateCheckConditions = { activityRecentlyOccurred: true, stateCheckOngoing: false, }; stateCheckTimeoutID: ?TimeoutID; constructor(ws: WebSocket, httpRequest: $Request) { this.ws = ws; this.httpRequest = httpRequest; ws.on('message', this.onMessage); ws.on('close', this.onClose); this.resetTimeout(); } onMessage = async (messageString: string) => { let clientSocketMessage: ?ClientSocketMessage; try { this.resetTimeout(); const message = JSON.parse(messageString); checkInputValidator(clientSocketMessageInputValidator, message); clientSocketMessage = message; if (clientSocketMessage.type === clientSocketMessageTypes.INITIAL) { if (this.viewer) { // This indicates that the user sent multiple INITIAL messages. throw new ServerError('socket_already_initialized'); } this.viewer = await fetchViewerForSocket( this.httpRequest, clientSocketMessage, ); if (!this.viewer) { // This indicates that the cookie was invalid, but the client is using // cookieSources.HEADER and thus can't accept a new cookie over // WebSockets. See comment under catch block for socket_deauthorized. throw new ServerError('socket_deauthorized'); } } const { viewer } = this; if (!viewer) { // This indicates a non-INITIAL message was sent by the client before // the INITIAL message. throw new ServerError('socket_uninitialized'); } if (viewer.sessionChanged) { // This indicates that the cookie was invalid, and we've assigned a new // anonymous one. throw new ServerError('socket_deauthorized'); } if (!viewer.loggedIn) { // This indicates that the specified cookie was an anonymous one. throw new ServerError('not_logged_in'); } await checkClientSupported( viewer, clientSocketMessageInputValidator, clientSocketMessage, ); if (!this.redis) { this.redis = new RedisSubscriber( { userID: viewer.userID, sessionID: viewer.session }, this.onRedisMessage, ); } const serverResponses = await this.handleClientSocketMessage( clientSocketMessage, ); if (viewer.sessionChanged) { // This indicates that something has caused the session to change, which // shouldn't happen from inside a WebSocket since we can't handle cookie // invalidation. throw new ServerError('session_mutated_from_socket'); } handleAsyncPromise(extendCookieLifespan(viewer.cookieID)); for (let response of serverResponses) { this.sendMessage(response); } if (clientSocketMessage.type === clientSocketMessageTypes.INITIAL) { this.onSuccessfulConnection(); } } catch (error) { console.warn(error); if (!(error instanceof ServerError)) { const errorMessage: ErrorServerSocketMessage = { type: serverSocketMessageTypes.ERROR, message: error.message, }; const responseTo = clientSocketMessage ? clientSocketMessage.id : null; if (responseTo !== null) { errorMessage.responseTo = responseTo; } this.markActivityOccurred(); this.sendMessage(errorMessage); return; } invariant(clientSocketMessage, 'should be set'); const responseTo = clientSocketMessage.id; if (error.message === 'socket_deauthorized') { const authErrorMessage: AuthErrorServerSocketMessage = { type: serverSocketMessageTypes.AUTH_ERROR, responseTo, message: error.message, }; if (this.viewer) { // viewer should only be falsey for cookieSources.HEADER (web) // clients. Usually if the cookie is invalid we construct a new // anonymous Viewer with a new cookie, and then pass the cookie down // in the error. But we can't pass HTTP cookies in WebSocket messages. authErrorMessage.sessionChange = { cookie: this.viewer.cookiePairString, currentUserInfo: { id: this.viewer.cookieID, anonymous: true, }, }; } this.sendMessage(authErrorMessage); this.ws.close(4100, error.message); return; } else if (error.message === 'client_version_unsupported') { const { viewer } = this; invariant(viewer, 'should be set'); const promises = {}; promises.deleteCookie = deleteCookie(viewer.cookieID); if (viewer.cookieSource !== cookieSources.BODY) { promises.anonymousViewerData = createNewAnonymousCookie({ platformDetails: error.platformDetails, deviceToken: viewer.deviceToken, }); } const { anonymousViewerData } = await promiseAll(promises); const authErrorMessage: AuthErrorServerSocketMessage = { type: serverSocketMessageTypes.AUTH_ERROR, responseTo, message: error.message, }; if (anonymousViewerData) { // It is normally not safe to pass the result of // createNewAnonymousCookie to the Viewer constructor. That is because // createNewAnonymousCookie leaves several fields of // AnonymousViewerData unset, and consequently Viewer will throw when // access is attempted. It is only safe here because we can guarantee // that only cookiePairString and cookieID are accessed on anonViewer // below. const anonViewer = new Viewer(anonymousViewerData); authErrorMessage.sessionChange = { cookie: anonViewer.cookiePairString, currentUserInfo: { id: anonViewer.cookieID, anonymous: true, }, }; } this.sendMessage(authErrorMessage); this.ws.close(4101, error.message); return; } if (error.payload) { this.sendMessage({ type: serverSocketMessageTypes.ERROR, responseTo, message: error.message, payload: error.payload, }); } else { this.sendMessage({ type: serverSocketMessageTypes.ERROR, responseTo, message: error.message, }); } if (error.message === 'not_logged_in') { this.ws.close(4102, error.message); } else if (error.message === 'session_mutated_from_socket') { this.ws.close(4103, error.message); } else { this.markActivityOccurred(); } } }; onClose = async () => { this.clearStateCheckTimeout(); this.resetTimeout.cancel(); this.debouncedAfterActivity.cancel(); if (this.viewer && this.viewer.hasSessionInfo) { await deleteActivityForViewerSession(this.viewer); } if (this.redis) { this.redis.quit(); this.redis = null; } }; sendMessage(message: ServerSocketMessage) { invariant( this.ws.readyState > 0, "shouldn't send message until connection established", ); if (this.ws.readyState === 1) { this.ws.send(JSON.stringify(message)); } } async handleClientSocketMessage( message: ClientSocketMessage, ): Promise { if (message.type === clientSocketMessageTypes.INITIAL) { this.markActivityOccurred(); return await this.handleInitialClientSocketMessage(message); } else if (message.type === clientSocketMessageTypes.RESPONSES) { this.markActivityOccurred(); return await this.handleResponsesClientSocketMessage(message); } else if (message.type === clientSocketMessageTypes.PING) { return await this.handlePingClientSocketMessage(message); } else if (message.type === clientSocketMessageTypes.ACK_UPDATES) { this.markActivityOccurred(); return await this.handleAckUpdatesClientSocketMessage(message); } else if (message.type === clientSocketMessageTypes.API_REQUEST) { this.markActivityOccurred(); return await this.handleAPIRequestClientSocketMessage(message); } return []; } async handleInitialClientSocketMessage( message: InitialClientSocketMessage, ): Promise { const { viewer } = this; invariant(viewer, 'should be set'); const responses = []; const { sessionState, clientResponses } = message.payload; const { calendarQuery, updatesCurrentAsOf: oldUpdatesCurrentAsOf, messagesCurrentAsOf: oldMessagesCurrentAsOf, watchedIDs, } = sessionState; await verifyCalendarQueryThreadIDs(calendarQuery); const sessionInitializationResult = await initializeSession( viewer, calendarQuery, oldUpdatesCurrentAsOf, ); const threadCursors = {}; for (let watchedThreadID of watchedIDs) { threadCursors[watchedThreadID] = null; } const threadSelectionCriteria = { threadCursors, joinedThreads: true }; const [ fetchMessagesResult, { serverRequests, activityUpdateResult }, ] = await Promise.all([ fetchMessageInfosSince( viewer, threadSelectionCriteria, oldMessagesCurrentAsOf, defaultNumberPerThread, ), processClientResponses(viewer, clientResponses), ]); const messagesResult = { rawMessageInfos: fetchMessagesResult.rawMessageInfos, truncationStatuses: fetchMessagesResult.truncationStatuses, currentAsOf: mostRecentMessageTimestamp( fetchMessagesResult.rawMessageInfos, oldMessagesCurrentAsOf, ), }; if (!sessionInitializationResult.sessionContinued) { const [ threadsResult, entriesResult, currentUserInfo, knownUserInfos, ] = await Promise.all([ fetchThreadInfos(viewer), fetchEntryInfos(viewer, [calendarQuery]), fetchCurrentUserInfo(viewer), fetchKnownUserInfos(viewer), ]); const payload: StateSyncFullSocketPayload = { type: stateSyncPayloadTypes.FULL, messagesResult, threadInfos: threadsResult.threadInfos, currentUserInfo, rawEntryInfos: entriesResult.rawEntryInfos, userInfos: values(knownUserInfos), updatesCurrentAsOf: oldUpdatesCurrentAsOf, }; if (viewer.sessionChanged) { // If initializeSession encounters sessionIdentifierTypes.BODY_SESSION_ID, // but the session is unspecified or expired, it will set a new sessionID // and specify viewer.sessionChanged const { sessionID } = viewer; invariant( sessionID !== null && sessionID !== undefined, 'should be set', ); payload.sessionID = sessionID; viewer.sessionChanged = false; } responses.push({ type: serverSocketMessageTypes.STATE_SYNC, responseTo: message.id, payload, }); } else { const { sessionUpdate, deltaEntryInfoResult, } = sessionInitializationResult; const promises = {}; promises.deleteExpiredUpdates = deleteUpdatesBeforeTimeTargettingSession( viewer, oldUpdatesCurrentAsOf, ); promises.fetchUpdateResult = fetchUpdateInfos( viewer, oldUpdatesCurrentAsOf, calendarQuery, ); promises.sessionUpdate = commitSessionUpdate(viewer, sessionUpdate); const { fetchUpdateResult } = await promiseAll(promises); const { updateInfos, userInfos } = fetchUpdateResult; const newUpdatesCurrentAsOf = mostRecentUpdateTimestamp( [...updateInfos], oldUpdatesCurrentAsOf, ); const updatesResult = { newUpdates: updateInfos, currentAsOf: newUpdatesCurrentAsOf, }; responses.push({ type: serverSocketMessageTypes.STATE_SYNC, responseTo: message.id, payload: { type: stateSyncPayloadTypes.INCREMENTAL, messagesResult, updatesResult, deltaEntryInfos: deltaEntryInfoResult.rawEntryInfos, deletedEntryIDs: deltaEntryInfoResult.deletedEntryIDs, userInfos: values(userInfos), }, }); } if (serverRequests.length > 0 || clientResponses.length > 0) { // We send this message first since the STATE_SYNC triggers the client's // connection status to shift to "connected", and we want to make sure the // client responses are cleared from Redux before that happens responses.unshift({ type: serverSocketMessageTypes.REQUESTS, responseTo: message.id, payload: { serverRequests }, }); } if (activityUpdateResult) { // Same reason for unshifting as above responses.unshift({ type: serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE, responseTo: message.id, payload: activityUpdateResult, }); } return responses; } async handleResponsesClientSocketMessage( message: ResponsesClientSocketMessage, ): Promise { const { viewer } = this; invariant(viewer, 'should be set'); const { clientResponses } = message.payload; const { stateCheckStatus } = await processClientResponses( viewer, clientResponses, ); const serverRequests = []; if (stateCheckStatus && stateCheckStatus.status !== 'state_check') { const { sessionUpdate, checkStateRequest } = await checkState( viewer, stateCheckStatus, viewer.calendarQuery, ); if (sessionUpdate) { await commitSessionUpdate(viewer, sessionUpdate); this.setStateCheckConditions({ stateCheckOngoing: false }); } if (checkStateRequest) { serverRequests.push(checkStateRequest); } } // We send a response message regardless of whether we have any requests, // since we need to ack the client's responses return [ { type: serverSocketMessageTypes.REQUESTS, responseTo: message.id, payload: { serverRequests }, }, ]; } async handlePingClientSocketMessage( message: PingClientSocketMessage, ): Promise { this.updateActivityTime(); return [ { type: serverSocketMessageTypes.PONG, responseTo: message.id, }, ]; } async handleAckUpdatesClientSocketMessage( message: AckUpdatesClientSocketMessage, ): Promise { const { viewer } = this; invariant(viewer, 'should be set'); const { currentAsOf } = message.payload; await Promise.all([ deleteUpdatesBeforeTimeTargettingSession(viewer, currentAsOf), commitSessionUpdate(viewer, { lastUpdate: currentAsOf }), ]); return []; } async handleAPIRequestClientSocketMessage( message: APIRequestClientSocketMessage, ): Promise { if (!endpointIsSocketSafe(message.payload.endpoint)) { throw new ServerError('endpoint_unsafe_for_socket'); } const { viewer } = this; invariant(viewer, 'should be set'); const responder = jsonEndpoints[message.payload.endpoint]; const response = await responder(viewer, message.payload.input); return [ { type: serverSocketMessageTypes.API_RESPONSE, responseTo: message.id, payload: response, }, ]; } onRedisMessage = async (message: RedisMessage) => { try { await this.processRedisMessage(message); } catch (e) { console.warn(e); } }; async processRedisMessage(message: RedisMessage) { if (message.type === redisMessageTypes.START_SUBSCRIPTION) { this.ws.terminate(); } else if (message.type === redisMessageTypes.NEW_UPDATES) { const { viewer } = this; invariant(viewer, 'should be set'); if (message.ignoreSession && message.ignoreSession === viewer.session) { return; } const rawUpdateInfos = message.updates; const { updateInfos, userInfos, } = await fetchUpdateInfosWithRawUpdateInfos(rawUpdateInfos, { viewer }); if (updateInfos.length === 0) { console.warn( 'could not get any UpdateInfos from redisMessageTypes.NEW_UPDATES', ); return; } this.markActivityOccurred(); this.sendMessage({ type: serverSocketMessageTypes.UPDATES, payload: { updatesResult: { currentAsOf: mostRecentUpdateTimestamp([...updateInfos], 0), newUpdates: updateInfos, }, userInfos: values(userInfos), }, }); } else if (message.type === redisMessageTypes.NEW_MESSAGES) { const { viewer } = this; invariant(viewer, 'should be set'); const rawMessageInfos = message.messages; const messageFetchResult = await getMessageFetchResultFromRedisMessages( viewer, rawMessageInfos, ); if (messageFetchResult.rawMessageInfos.length === 0) { console.warn( 'could not get any rawMessageInfos from ' + 'redisMessageTypes.NEW_MESSAGES', ); return; } this.markActivityOccurred(); this.sendMessage({ type: serverSocketMessageTypes.MESSAGES, payload: { messagesResult: { rawMessageInfos: messageFetchResult.rawMessageInfos, truncationStatuses: messageFetchResult.truncationStatuses, currentAsOf: mostRecentMessageTimestamp( messageFetchResult.rawMessageInfos, 0, ), }, - userInfos: values(messageFetchResult.userInfos), }, }); } } onSuccessfulConnection() { if (this.ws.readyState !== 1) { return; } this.handleStateCheckConditionsUpdate(); } updateActivityTime() { const { viewer } = this; invariant(viewer, 'should be set'); handleAsyncPromise(updateActivityTime(viewer)); } // The Socket will timeout by calling this.ws.terminate() // serverRequestSocketTimeout milliseconds after the last // time resetTimeout is called resetTimeout = _debounce( () => this.ws.terminate(), serverRequestSocketTimeout, ); debouncedAfterActivity = _debounce( () => this.setStateCheckConditions({ activityRecentlyOccurred: false }), stateCheckInactivityActivationInterval, ); markActivityOccurred = () => { if (this.ws.readyState !== 1) { return; } this.setStateCheckConditions({ activityRecentlyOccurred: true }); this.debouncedAfterActivity(); }; clearStateCheckTimeout() { const { stateCheckTimeoutID } = this; if (stateCheckTimeoutID) { clearTimeout(stateCheckTimeoutID); this.stateCheckTimeoutID = null; } } setStateCheckConditions(newConditions: $Shape) { this.stateCheckConditions = { ...this.stateCheckConditions, ...newConditions, }; this.handleStateCheckConditionsUpdate(); } get stateCheckCanStart() { return Object.values(this.stateCheckConditions).every(cond => !cond); } handleStateCheckConditionsUpdate() { if (!this.stateCheckCanStart) { this.clearStateCheckTimeout(); return; } if (this.stateCheckTimeoutID) { return; } const { viewer } = this; if (!viewer) { return; } const timeUntilStateCheck = viewer.sessionLastValidated + sessionCheckFrequency - Date.now(); if (timeUntilStateCheck <= 0) { this.initiateStateCheck(); } else { this.stateCheckTimeoutID = setTimeout( this.initiateStateCheck, timeUntilStateCheck, ); } } initiateStateCheck = async () => { this.setStateCheckConditions({ stateCheckOngoing: true }); const { viewer } = this; invariant(viewer, 'should be set'); const { checkStateRequest } = await checkState( viewer, { status: 'state_check' }, viewer.calendarQuery, ); invariant(checkStateRequest, 'should be set'); this.sendMessage({ type: serverSocketMessageTypes.REQUESTS, payload: { serverRequests: [checkStateRequest] }, }); }; } export { onConnection };