diff --git a/keyserver/src/creators/update-creator.js b/keyserver/src/creators/update-creator.js index 67129fe44..6cebe321b 100644 --- a/keyserver/src/creators/update-creator.js +++ b/keyserver/src/creators/update-creator.js @@ -1,803 +1,778 @@ // @flow import invariant from 'invariant'; import { nonThreadCalendarFilters } from 'lib/selectors/calendar-filter-selectors.js'; import { keyForUpdateData, keyForUpdateInfo, rawUpdateInfoFromUpdateData, } from 'lib/shared/update-utils.js'; +import { updateSpecs } from 'lib/shared/updates/update-specs.js'; import { type RawEntryInfos, type FetchEntryInfosBase, type CalendarQuery, defaultCalendarQuery, } from 'lib/types/entry-types.js'; import { defaultNumberPerThread, type FetchMessageInfosResult, type MessageSelectionCriteria, } from 'lib/types/message-types.js'; import { type UpdateTarget, redisMessageTypes, type NewUpdatesRedisMessage, } from 'lib/types/redis-types.js'; import type { RawThreadInfo } from 'lib/types/thread-types.js'; import { updateTypes } from 'lib/types/update-types-enum.js'; import { type ServerUpdateInfo, type UpdateData, type RawUpdateInfo, type CreateUpdatesResult, } from 'lib/types/update-types.js'; import type { UserInfos, LoggedInUserInfo } from 'lib/types/user-types.js'; import { promiseAll } from 'lib/utils/promises.js'; import createIDs from './id-creator.js'; import { dbQuery, SQL, mergeAndConditions } from '../database/database.js'; import type { SQLStatementType } from '../database/types.js'; import { deleteUpdatesByConditions } from '../deleters/update-deleters.js'; import { fetchEntryInfos, fetchEntryInfosByID, } from '../fetchers/entry-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { fetchThreadInfos, type FetchThreadInfosResult, } from '../fetchers/thread-fetchers.js'; import { fetchKnownUserInfos, fetchCurrentUserInfo, } from '../fetchers/user-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { channelNameForUpdateTarget, publisher } from '../socket/redis.js'; export type UpdatesForCurrentSession = // This is the default if no Viewer is passed, or if an isSocket Viewer is // passed in. We will broadcast to all valid sessions via Redis and return // nothing to the caller, relying on the current session's Redis listener to // pick up the updates and deliver them asynchronously. | 'broadcast' // This is the default if a non-isSocket Viewer is passed in. We avoid // broadcasting the update to the current session, and instead return the // update to the caller, who will handle delivering it to the client. | 'return' // This means we ignore any updates destined for the current session. // Presumably the caller knows what they are doing and has a different way of // communicating the relevant information to the client. | 'ignore'; type DeleteCondition = { +userID: string, +target: ?string, +types: 'all_types' | $ReadOnlySet, }; export type ViewerInfo = | { viewer: Viewer, calendarQuery?: ?CalendarQuery, updatesForCurrentSession?: UpdatesForCurrentSession, } | { viewer: Viewer, calendarQuery: ?CalendarQuery, updatesForCurrentSession?: UpdatesForCurrentSession, threadInfos: { +[id: string]: RawThreadInfo }, }; const defaultUpdateCreationResult = { viewerUpdates: [], userInfos: {} }; const sortFunction = ( a: UpdateData | ServerUpdateInfo, b: UpdateData | ServerUpdateInfo, ) => a.time - b.time; const deleteUpdatesBatchSize = 500; // Creates rows in the updates table based on the inputed updateDatas. Returns // UpdateInfos pertaining to the provided viewerInfo, as well as related // UserInfos. If no viewerInfo is provided, no UpdateInfos will be returned. And // the update row won't have an updater column, meaning no session will be // excluded from the update. async function createUpdates( updateDatas: $ReadOnlyArray, passedViewerInfo?: ?ViewerInfo, ): Promise { if (updateDatas.length === 0) { return defaultUpdateCreationResult; } // viewer.session will throw for a script Viewer let viewerInfo = passedViewerInfo; if ( viewerInfo && (viewerInfo.viewer.isScriptViewer || !viewerInfo.viewer.loggedIn) ) { viewerInfo = null; } const sortedUpdateDatas = [...updateDatas].sort(sortFunction); const filteredUpdateDatas: UpdateData[] = []; const keyedUpdateDatas: Map = new Map(); for (const updateData of sortedUpdateDatas) { const key = keyForUpdateData(updateData); if (!key) { filteredUpdateDatas.push(updateData); continue; } const conditionKey = `${updateData.userID}|${key}`; const deleteCondition = getDeleteCondition(updateData); invariant( deleteCondition, `updateData of type ${updateData.type} has conditionKey ` + `${conditionKey} but no deleteCondition`, ); const curUpdateDatas = keyedUpdateDatas.get(conditionKey); if (!curUpdateDatas) { keyedUpdateDatas.set(conditionKey, [updateData]); continue; } const filteredCurrent = curUpdateDatas.filter(curUpdateData => filterOnDeleteCondition(curUpdateData, deleteCondition), ); if (filteredCurrent.length === 0) { keyedUpdateDatas.set(conditionKey, [updateData]); continue; } const isNewUpdateDataFiltered = !filteredCurrent.every(curUpdateData => { const curDeleteCondition = getDeleteCondition(curUpdateData); invariant( curDeleteCondition, `updateData of type ${curUpdateData.type} is in keyedUpdateDatas ` + "but doesn't have a deleteCondition", ); return filterOnDeleteCondition(updateData, curDeleteCondition); }); if (!isNewUpdateDataFiltered) { filteredCurrent.push(updateData); } keyedUpdateDatas.set(conditionKey, filteredCurrent); } for (const keyUpdateDatas of keyedUpdateDatas.values()) { filteredUpdateDatas.push(...keyUpdateDatas); } const ids = await createIDs('updates', filteredUpdateDatas.length); let updatesForCurrentSession = viewerInfo && viewerInfo.updatesForCurrentSession; if (!updatesForCurrentSession && viewerInfo) { updatesForCurrentSession = viewerInfo.viewer.isSocket ? 'broadcast' : 'return'; } else if (!updatesForCurrentSession) { updatesForCurrentSession = 'broadcast'; } const dontBroadcastSession = updatesForCurrentSession !== 'broadcast' && viewerInfo ? viewerInfo.viewer.session : null; const publishInfos: Map = new Map(); const viewerRawUpdateInfos: RawUpdateInfo[] = []; const insertRows: (?(number | string))[][] = []; const earliestTime: Map = new Map(); for (let i = 0; i < filteredUpdateDatas.length; i++) { const updateData = filteredUpdateDatas[i]; - let content; - if (updateData.type === updateTypes.DELETE_ACCOUNT) { - content = JSON.stringify({ deletedUserID: updateData.deletedUserID }); - } else if (updateData.type === updateTypes.UPDATE_THREAD) { - content = JSON.stringify({ threadID: updateData.threadID }); - } else if (updateData.type === updateTypes.UPDATE_THREAD_READ_STATUS) { - const { threadID, unread } = updateData; - content = JSON.stringify({ threadID, unread }); - } else if ( - updateData.type === updateTypes.DELETE_THREAD || - updateData.type === updateTypes.JOIN_THREAD - ) { - const { threadID } = updateData; - content = JSON.stringify({ threadID }); - } else if (updateData.type === updateTypes.BAD_DEVICE_TOKEN) { - const { deviceToken } = updateData; - content = JSON.stringify({ deviceToken }); - } else if (updateData.type === updateTypes.UPDATE_ENTRY) { - const { entryID } = updateData; - content = JSON.stringify({ entryID }); - } else if (updateData.type === updateTypes.UPDATE_CURRENT_USER) { - // user column contains all the info we need to construct the UpdateInfo - content = null; - } else if (updateData.type === updateTypes.UPDATE_USER) { - const { updatedUserID } = updateData; - content = JSON.stringify({ updatedUserID }); - } else { - invariant(false, `unrecognized updateType ${updateData.type}`); - } const target = getTargetFromUpdateData(updateData); const rawUpdateInfo = rawUpdateInfoFromUpdateData(updateData, ids[i]); if (!target || !dontBroadcastSession || target !== dontBroadcastSession) { const updateTarget = target ? { userID: updateData.userID, sessionID: target } : { userID: updateData.userID }; const channelName = channelNameForUpdateTarget(updateTarget); let publishInfo = publishInfos.get(channelName); if (!publishInfo) { publishInfo = { updateTarget, rawUpdateInfos: [] }; publishInfos.set(channelName, publishInfo); } publishInfo.rawUpdateInfos.push(rawUpdateInfo); } if ( updatesForCurrentSession === 'return' && viewerInfo && updateData.userID === viewerInfo.viewer.id && (!target || target === viewerInfo.viewer.session) ) { viewerRawUpdateInfos.push(rawUpdateInfo); } if (viewerInfo && target && viewerInfo.viewer.session === target) { // In the case where this update is being created only for the current // session, there's no reason to insert a row into the updates table continue; } + const content = + updateSpecs[updateData.type].updateContentForServerDB(updateData); + const key = keyForUpdateData(updateData); if (key) { const conditionKey = `${updateData.userID}|${key}`; const currentEarliestTime = earliestTime.get(conditionKey); if (!currentEarliestTime || updateData.time < currentEarliestTime) { earliestTime.set(conditionKey, updateData.time); } } const insertRow = [ ids[i], updateData.userID, updateData.type, key, content, updateData.time, dontBroadcastSession, target, ]; insertRows.push(insertRow); } type DeleteUpdatesConditions = { key: string, target?: string, types?: number[], time?: number, }; const usersByConditions: Map< string, { conditions: DeleteUpdatesConditions, users: Set, }, > = new Map(); for (const [conditionKey, keyUpdateDatas] of keyedUpdateDatas) { const deleteConditionByTarget: Map = new Map(); for (const updateData of keyUpdateDatas) { const deleteCondition = getDeleteCondition(updateData); invariant( deleteCondition, `updateData of type ${updateData.type} is in keyedUpdateDatas but ` + "doesn't have a deleteCondition", ); const { target, types } = deleteCondition; const existingDeleteCondition = deleteConditionByTarget.get(target); if (!existingDeleteCondition) { deleteConditionByTarget.set(target, deleteCondition); continue; } const existingTypes = existingDeleteCondition.types; if (existingTypes === 'all_types') { continue; } else if (types === 'all_types') { deleteConditionByTarget.set(target, deleteCondition); continue; } const mergedTypes = new Set([...types, ...existingTypes]); deleteConditionByTarget.set(target, { ...deleteCondition, types: mergedTypes, }); } for (const deleteCondition of deleteConditionByTarget.values()) { const { userID, target, types } = deleteCondition; const key = conditionKey.split('|')[1]; const conditions: DeleteUpdatesConditions = { key }; if (target) { conditions.target = target; } if (types !== 'all_types') { invariant(types.size > 0, 'deleteCondition had empty types set'); conditions.types = [...types]; } const earliestTimeForCondition = earliestTime.get(conditionKey); if (earliestTimeForCondition) { conditions.time = earliestTimeForCondition; } const conditionsKey = JSON.stringify(conditions); if (!usersByConditions.has(conditionsKey)) { usersByConditions.set(conditionsKey, { conditions, users: new Set(), }); } usersByConditions.get(conditionsKey)?.users.add(userID); } } const deleteSQLConditions: SQLStatementType[] = []; for (const { conditions, users } of usersByConditions.values()) { const sqlConditions = [ SQL`u.user IN (${[...users]})`, SQL`u.key = ${conditions.key}`, ]; if (conditions.target) { sqlConditions.push(SQL`u.target = ${conditions.target}`); } if (conditions.types) { sqlConditions.push(SQL`u.type IN (${conditions.types})`); } if (conditions.time) { sqlConditions.push(SQL`u.time < ${conditions.time}`); } deleteSQLConditions.push(mergeAndConditions(sqlConditions)); } const promises = {}; if (insertRows.length > 0) { const insertQuery = SQL` INSERT INTO updates(id, user, type, \`key\`, content, time, updater, target) `; insertQuery.append(SQL`VALUES ${insertRows}`); promises.insert = dbQuery(insertQuery); } if (publishInfos.size > 0) { promises.redis = redisPublish(publishInfos.values(), dontBroadcastSession); } if (deleteSQLConditions.length > 0) { promises.delete = (async () => { while (deleteSQLConditions.length > 0) { const batch = deleteSQLConditions.splice(0, deleteUpdatesBatchSize); await deleteUpdatesByConditions(batch); } })(); } if (viewerRawUpdateInfos.length > 0) { invariant(viewerInfo, 'should be set'); promises.updatesResult = fetchUpdateInfosWithRawUpdateInfos( viewerRawUpdateInfos, viewerInfo, ); } const { updatesResult } = await promiseAll(promises); if (!updatesResult) { return defaultUpdateCreationResult; } const { updateInfos, userInfos } = updatesResult; return { viewerUpdates: updateInfos, userInfos }; } export type FetchUpdatesResult = { +updateInfos: $ReadOnlyArray, +userInfos: UserInfos, }; async function fetchUpdateInfosWithRawUpdateInfos( rawUpdateInfos: $ReadOnlyArray, viewerInfo: ViewerInfo, ): Promise { const { viewer } = viewerInfo; const threadIDsNeedingFetch = new Set(); const entryIDsNeedingFetch = new Set(); let currentUserNeedsFetch = false; const threadIDsNeedingDetailedFetch = new Set(); // entries and messages for (const rawUpdateInfo of rawUpdateInfos) { if ( !viewerInfo.threadInfos && (rawUpdateInfo.type === updateTypes.UPDATE_THREAD || rawUpdateInfo.type === updateTypes.JOIN_THREAD) ) { threadIDsNeedingFetch.add(rawUpdateInfo.threadID); } if (rawUpdateInfo.type === updateTypes.JOIN_THREAD) { threadIDsNeedingDetailedFetch.add(rawUpdateInfo.threadID); } else if (rawUpdateInfo.type === updateTypes.UPDATE_ENTRY) { entryIDsNeedingFetch.add(rawUpdateInfo.entryID); } else if (rawUpdateInfo.type === updateTypes.UPDATE_CURRENT_USER) { currentUserNeedsFetch = true; } } const promises = {}; if (!viewerInfo.threadInfos && threadIDsNeedingFetch.size > 0) { promises.threadResult = fetchThreadInfos(viewer, { threadIDs: threadIDsNeedingFetch, }); } let calendarQuery: ?CalendarQuery = viewerInfo.calendarQuery ? viewerInfo.calendarQuery : null; if (!calendarQuery && viewer.hasSessionInfo) { // This should only ever happen for "legacy" clients who call in without // providing this information. These clients wouldn't know how to deal with // the corresponding UpdateInfos anyways, so no reason to be worried. calendarQuery = viewer.calendarQuery; } else if (!calendarQuery) { calendarQuery = defaultCalendarQuery(viewer.platform, viewer.timeZone); } if (threadIDsNeedingDetailedFetch.size > 0) { const threadCursors: { [string]: ?string } = {}; for (const threadID of threadIDsNeedingDetailedFetch) { threadCursors[threadID] = null; } const messageSelectionCriteria: MessageSelectionCriteria = { threadCursors, }; promises.messageInfosResult = fetchMessageInfos( viewer, messageSelectionCriteria, defaultNumberPerThread, ); const threadCalendarQuery = { ...calendarQuery, filters: [ ...nonThreadCalendarFilters(calendarQuery.filters), { type: 'threads', threadIDs: [...threadIDsNeedingDetailedFetch] }, ], }; promises.calendarResult = fetchEntryInfos(viewer, [threadCalendarQuery]); } if (entryIDsNeedingFetch.size > 0) { promises.entryInfosResult = fetchEntryInfosByID( viewer, entryIDsNeedingFetch, ); } if (currentUserNeedsFetch) { promises.currentUserInfoResult = (async () => { const currentUserInfo = await fetchCurrentUserInfo(viewer); invariant(currentUserInfo.anonymous === undefined, 'should be logged in'); return currentUserInfo; })(); } const { threadResult, messageInfosResult, calendarResult, entryInfosResult, currentUserInfoResult, } = await promiseAll(promises); let threadInfosResult; if (viewerInfo.threadInfos) { const { threadInfos } = viewerInfo; threadInfosResult = { threadInfos }; } else if (threadResult) { threadInfosResult = threadResult; } else { threadInfosResult = { threadInfos: {} }; } return await updateInfosFromRawUpdateInfos(viewer, rawUpdateInfos, { threadInfosResult, messageInfosResult, calendarResult, entryInfosResult, currentUserInfoResult, }); } export type UpdateInfosRawData = { threadInfosResult: FetchThreadInfosResult, messageInfosResult: ?FetchMessageInfosResult, calendarResult: ?FetchEntryInfosBase, entryInfosResult: ?RawEntryInfos, currentUserInfoResult: LoggedInUserInfo, }; async function updateInfosFromRawUpdateInfos( viewer: Viewer, rawUpdateInfos: $ReadOnlyArray, rawData: UpdateInfosRawData, ): Promise { const { threadInfosResult, messageInfosResult, calendarResult, entryInfosResult, currentUserInfoResult, } = rawData; const updateInfos = []; const userIDsToFetch = new Set(); const rawEntryInfosByThreadID = {}; for (const entryInfo of calendarResult?.rawEntryInfos ?? []) { if (!rawEntryInfosByThreadID[entryInfo.threadID]) { rawEntryInfosByThreadID[entryInfo.threadID] = []; } rawEntryInfosByThreadID[entryInfo.threadID].push(entryInfo); } const rawMessageInfosByThreadID = {}; for (const messageInfo of messageInfosResult?.rawMessageInfos ?? []) { if (!rawMessageInfosByThreadID[messageInfo.threadID]) { rawMessageInfosByThreadID[messageInfo.threadID] = []; } rawMessageInfosByThreadID[messageInfo.threadID].push(messageInfo); } for (const rawUpdateInfo of rawUpdateInfos) { if (rawUpdateInfo.type === updateTypes.DELETE_ACCOUNT) { updateInfos.push({ type: updateTypes.DELETE_ACCOUNT, id: rawUpdateInfo.id, time: rawUpdateInfo.time, deletedUserID: rawUpdateInfo.deletedUserID, }); } else if (rawUpdateInfo.type === updateTypes.UPDATE_THREAD) { const threadInfo = threadInfosResult.threadInfos[rawUpdateInfo.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.UPDATE_THREAD because we couldn't " + `fetch RawThreadInfo for ${rawUpdateInfo.threadID}`, ); continue; } updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: rawUpdateInfo.id, time: rawUpdateInfo.time, threadInfo, }); } else if (rawUpdateInfo.type === updateTypes.UPDATE_THREAD_READ_STATUS) { updateInfos.push({ type: updateTypes.UPDATE_THREAD_READ_STATUS, id: rawUpdateInfo.id, time: rawUpdateInfo.time, threadID: rawUpdateInfo.threadID, unread: rawUpdateInfo.unread, }); } else if (rawUpdateInfo.type === updateTypes.DELETE_THREAD) { updateInfos.push({ type: updateTypes.DELETE_THREAD, id: rawUpdateInfo.id, time: rawUpdateInfo.time, threadID: rawUpdateInfo.threadID, }); } else if (rawUpdateInfo.type === updateTypes.JOIN_THREAD) { const threadInfo = threadInfosResult.threadInfos[rawUpdateInfo.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.JOIN_THREAD because we couldn't " + `fetch RawThreadInfo for ${rawUpdateInfo.threadID}`, ); continue; } invariant(calendarResult, 'should be set'); const rawEntryInfos = rawEntryInfosByThreadID[rawUpdateInfo.threadID] ?? []; invariant(messageInfosResult, 'should be set'); const rawMessageInfos = rawMessageInfosByThreadID[rawUpdateInfo.threadID] ?? []; updateInfos.push({ type: updateTypes.JOIN_THREAD, id: rawUpdateInfo.id, time: rawUpdateInfo.time, threadInfo, rawMessageInfos, truncationStatus: messageInfosResult.truncationStatuses[rawUpdateInfo.threadID], rawEntryInfos, }); } else if (rawUpdateInfo.type === updateTypes.BAD_DEVICE_TOKEN) { updateInfos.push({ type: updateTypes.BAD_DEVICE_TOKEN, id: rawUpdateInfo.id, time: rawUpdateInfo.time, deviceToken: rawUpdateInfo.deviceToken, }); } else if (rawUpdateInfo.type === updateTypes.UPDATE_ENTRY) { invariant(entryInfosResult, 'should be set'); const entryInfo = entryInfosResult[rawUpdateInfo.entryID]; if (!entryInfo) { console.warn( "failed to hydrate updateTypes.UPDATE_ENTRY because we couldn't " + `fetch RawEntryInfo for ${rawUpdateInfo.entryID}`, ); continue; } updateInfos.push({ type: updateTypes.UPDATE_ENTRY, id: rawUpdateInfo.id, time: rawUpdateInfo.time, entryInfo, }); } else if (rawUpdateInfo.type === updateTypes.UPDATE_CURRENT_USER) { invariant(currentUserInfoResult, 'should be set'); updateInfos.push({ type: updateTypes.UPDATE_CURRENT_USER, id: rawUpdateInfo.id, time: rawUpdateInfo.time, currentUserInfo: currentUserInfoResult, }); } else if (rawUpdateInfo.type === updateTypes.UPDATE_USER) { updateInfos.push({ type: updateTypes.UPDATE_USER, id: rawUpdateInfo.id, time: rawUpdateInfo.time, updatedUserID: rawUpdateInfo.updatedUserID, }); userIDsToFetch.add(rawUpdateInfo.updatedUserID); } else { invariant(false, `unrecognized updateType ${rawUpdateInfo.type}`); } } let userInfos = {}; if (userIDsToFetch.size > 0) { userInfos = await fetchKnownUserInfos(viewer, [...userIDsToFetch]); } updateInfos.sort(sortFunction); // Now we'll attempt to merge UpdateInfos so that we only have one per key const updateForKey: Map = new Map(); const mergedUpdates: ServerUpdateInfo[] = []; for (const updateInfo of updateInfos) { const key = keyForUpdateInfo(updateInfo); if (!key) { mergedUpdates.push(updateInfo); continue; } else if ( updateInfo.type === updateTypes.DELETE_THREAD || updateInfo.type === updateTypes.JOIN_THREAD || updateInfo.type === updateTypes.DELETE_ACCOUNT ) { updateForKey.set(key, updateInfo); continue; } const currentUpdateInfo = updateForKey.get(key); if (!currentUpdateInfo) { updateForKey.set(key, updateInfo); } else if ( updateInfo.type === updateTypes.UPDATE_THREAD && currentUpdateInfo.type === updateTypes.UPDATE_THREAD_READ_STATUS ) { // UPDATE_THREAD trumps UPDATE_THREAD_READ_STATUS // Note that we keep the oldest UPDATE_THREAD updateForKey.set(key, updateInfo); } else if ( updateInfo.type === updateTypes.UPDATE_THREAD_READ_STATUS && currentUpdateInfo.type === updateTypes.UPDATE_THREAD_READ_STATUS ) { // If we only have UPDATE_THREAD_READ_STATUS, keep the most recent updateForKey.set(key, updateInfo); } else if (updateInfo.type === updateTypes.UPDATE_ENTRY) { updateForKey.set(key, updateInfo); } else if (updateInfo.type === updateTypes.UPDATE_CURRENT_USER) { updateForKey.set(key, updateInfo); } } for (const [, updateInfo] of updateForKey) { mergedUpdates.push(updateInfo); } mergedUpdates.sort(sortFunction); return { updateInfos: mergedUpdates, userInfos }; } type PublishInfo = { updateTarget: UpdateTarget, rawUpdateInfos: RawUpdateInfo[], }; async function redisPublish( publishInfos: Iterator, dontBroadcastSession: ?string, ): Promise { for (const publishInfo of publishInfos) { const { updateTarget, rawUpdateInfos } = publishInfo; const redisMessage: NewUpdatesRedisMessage = { type: redisMessageTypes.NEW_UPDATES, updates: rawUpdateInfos, }; if (!updateTarget.sessionID && dontBroadcastSession) { redisMessage.ignoreSession = dontBroadcastSession; } publisher.sendMessage(updateTarget, redisMessage); } } function getTargetFromUpdateData(updateData: UpdateData): ?string { if (updateData.targetSession) { return updateData.targetSession; } else if (updateData.targetCookie) { return updateData.targetCookie; } else { return null; } } function getDeleteCondition(updateData: UpdateData): ?DeleteCondition { let types; if (updateData.type === updateTypes.DELETE_ACCOUNT) { types = new Set([updateTypes.DELETE_ACCOUNT, updateTypes.UPDATE_USER]); } else if (updateData.type === updateTypes.UPDATE_THREAD) { types = new Set([ updateTypes.UPDATE_THREAD, updateTypes.UPDATE_THREAD_READ_STATUS, ]); } else if (updateData.type === updateTypes.UPDATE_THREAD_READ_STATUS) { types = new Set([updateTypes.UPDATE_THREAD_READ_STATUS]); } else if ( updateData.type === updateTypes.DELETE_THREAD || updateData.type === updateTypes.JOIN_THREAD ) { types = 'all_types'; } else if (updateData.type === updateTypes.UPDATE_ENTRY) { types = 'all_types'; } else if (updateData.type === updateTypes.UPDATE_CURRENT_USER) { types = new Set([updateTypes.UPDATE_CURRENT_USER]); } else if (updateData.type === updateTypes.UPDATE_USER) { types = new Set([updateTypes.UPDATE_USER]); } else { return null; } const target = getTargetFromUpdateData(updateData); const { userID } = updateData; return { userID, target, types }; } function filterOnDeleteCondition( updateData: UpdateData, deleteCondition: DeleteCondition, ): boolean { invariant( updateData.userID === deleteCondition.userID, `updateData of type ${updateData.type} being compared to wrong userID`, ); if (deleteCondition.target) { const target = getTargetFromUpdateData(updateData); if (target !== deleteCondition.target) { return true; } } if (deleteCondition.types === 'all_types') { return false; } return !deleteCondition.types.has(updateData.type); } export { createUpdates, fetchUpdateInfosWithRawUpdateInfos }; diff --git a/lib/shared/updates/bad-device-token-spec.js b/lib/shared/updates/bad-device-token-spec.js index e4002059d..c429f512a 100644 --- a/lib/shared/updates/bad-device-token-spec.js +++ b/lib/shared/updates/bad-device-token-spec.js @@ -1,23 +1,29 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { BadDeviceTokenRawUpdateInfo, + BadDeviceTokenUpdateData, BadDeviceTokenUpdateInfo, } from '../../types/update-types.js'; export const badDeviceTokenSpec: UpdateSpec< BadDeviceTokenUpdateInfo, BadDeviceTokenRawUpdateInfo, + BadDeviceTokenUpdateData, > = Object.freeze({ rawUpdateInfoFromRow(row: Object) { const { deviceToken } = JSON.parse(row.content); return { type: updateTypes.BAD_DEVICE_TOKEN, id: row.id.toString(), time: row.time, deviceToken, }; }, + updateContentForServerDB(data: BadDeviceTokenUpdateData) { + const { deviceToken } = data; + return JSON.stringify({ deviceToken }); + }, }); diff --git a/lib/shared/updates/delete-account-spec.js b/lib/shared/updates/delete-account-spec.js index 7dc6c7a63..d2c235b25 100644 --- a/lib/shared/updates/delete-account-spec.js +++ b/lib/shared/updates/delete-account-spec.js @@ -1,59 +1,64 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { AccountDeletionRawUpdateInfo, + AccountDeletionUpdateData, AccountDeletionUpdateInfo, } from '../../types/update-types.js'; import type { UserInfos } from '../../types/user-types.js'; export const deleteAccountSpec: UpdateSpec< AccountDeletionUpdateInfo, AccountDeletionRawUpdateInfo, + AccountDeletionUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: AccountDeletionUpdateInfo, ) { const operations = []; for (const threadID in storeThreadInfos) { const threadInfo = storeThreadInfos[threadID]; const newMembers = threadInfo.members.filter( member => member.id !== update.deletedUserID, ); if (newMembers.length < threadInfo.members.length) { const updatedThread = { ...threadInfo, members: newMembers, }; operations.push({ type: 'replace', payload: { id: threadID, threadInfo: updatedThread, }, }); } } return operations; }, reduceUserInfos(state: UserInfos, update: AccountDeletionUpdateInfo) { const { deletedUserID } = update; if (!state[deletedUserID]) { return state; } const { [deletedUserID]: deleted, ...rest } = state; return rest; }, rawUpdateInfoFromRow(row: Object) { const content = JSON.parse(row.content); return { type: updateTypes.DELETE_ACCOUNT, id: row.id.toString(), time: row.time, deletedUserID: content.deletedUserID, }; }, + updateContentForServerDB(data: AccountDeletionUpdateData) { + return JSON.stringify({ deletedUserID: data.deletedUserID }); + }, }); diff --git a/lib/shared/updates/delete-thread-spec.js b/lib/shared/updates/delete-thread-spec.js index 96946c4a7..e99d7efcc 100644 --- a/lib/shared/updates/delete-thread-spec.js +++ b/lib/shared/updates/delete-thread-spec.js @@ -1,49 +1,55 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadDeletionRawUpdateInfo, + ThreadDeletionUpdateData, ThreadDeletionUpdateInfo, } from '../../types/update-types.js'; export const deleteThreadSpec: UpdateSpec< ThreadDeletionUpdateInfo, ThreadDeletionRawUpdateInfo, + ThreadDeletionUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadDeletionUpdateInfo, ) { if (storeThreadInfos[update.threadID]) { return [ { type: 'remove', payload: { ids: [update.threadID], }, }, ]; } return null; }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadDeletionUpdateInfo, ) { if (!filteredThreadIDs.has(update.threadID)) { return filteredThreadIDs; } return new Set([...filteredThreadIDs].filter(id => id !== update.threadID)); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.DELETE_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, + updateContentForServerDB(data: ThreadDeletionUpdateData) { + const { threadID } = data; + return JSON.stringify({ threadID }); + }, }); diff --git a/lib/shared/updates/join-thread-spec.js b/lib/shared/updates/join-thread-spec.js index 1715ec25f..b8aa52e70 100644 --- a/lib/shared/updates/join-thread-spec.js +++ b/lib/shared/updates/join-thread-spec.js @@ -1,99 +1,105 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import type { UpdateSpec } from './update-spec.js'; import type { RawEntryInfo } from '../../types/entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../../types/message-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, + ThreadJoinUpdateData, } from '../../types/update-types.js'; import { combineTruncationStatuses } from '../message-utils.js'; import { threadInFilterList } from '../thread-utils.js'; export const joinThreadSpec: UpdateSpec< ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, + ThreadJoinUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadJoinUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, mergeEntryInfos( entryIDs: Set, mergedEntryInfos: Array, update: ThreadJoinUpdateInfo, ) { for (const entryInfo of update.rawEntryInfos) { const entryID = entryInfo.id; if (!entryID || entryIDs.has(entryID)) { continue; } mergedEntryInfos.push(entryInfo); entryIDs.add(entryID); } }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadJoinUpdateInfo, ) { if ( !threadInFilterList(update.threadInfo) || filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set([...filteredThreadIDs, update.threadInfo.id]); }, getRawMessageInfos(update: ThreadJoinUpdateInfo) { return update.rawMessageInfos; }, mergeMessageInfosAndTruncationStatuses( messageIDs: Set, messageInfos: Array, truncationStatuses: MessageTruncationStatuses, update: ThreadJoinUpdateInfo, ) { for (const messageInfo of update.rawMessageInfos) { const messageID = messageInfo.id; if (!messageID || messageIDs.has(messageID)) { continue; } messageInfos.push(messageInfo); messageIDs.add(messageID); } truncationStatuses[update.threadInfo.id] = combineTruncationStatuses( update.truncationStatus, truncationStatuses[update.threadInfo.id], ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.JOIN_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, + updateContentForServerDB(data: ThreadJoinUpdateData) { + const { threadID } = data; + return JSON.stringify({ threadID }); + }, }); diff --git a/lib/shared/updates/update-current-user-spec.js b/lib/shared/updates/update-current-user-spec.js index 0033f3238..1f52fdbb7 100644 --- a/lib/shared/updates/update-current-user-spec.js +++ b/lib/shared/updates/update-current-user-spec.js @@ -1,30 +1,36 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import type { UpdateSpec } from './update-spec.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { CurrentUserUpdateInfo, CurrentUserRawUpdateInfo, + CurrentUserUpdateData, } from '../../types/update-types.js'; import type { CurrentUserInfo } from '../../types/user-types.js'; export const updateCurrentUserSpec: UpdateSpec< CurrentUserUpdateInfo, CurrentUserRawUpdateInfo, + CurrentUserUpdateData, > = Object.freeze({ reduceCurrentUser(state: ?CurrentUserInfo, update: CurrentUserUpdateInfo) { if (!_isEqual(update.currentUserInfo)(state)) { return update.currentUserInfo; } return state; }, rawUpdateInfoFromRow(row: Object) { return { type: updateTypes.UPDATE_CURRENT_USER, id: row.id.toString(), time: row.time, }; }, + updateContentForServerDB() { + // user column contains all the info we need to construct the UpdateInfo + return null; + }, }); diff --git a/lib/shared/updates/update-entry-spec.js b/lib/shared/updates/update-entry-spec.js index 85aeb5aae..ee4a3ada6 100644 --- a/lib/shared/updates/update-entry-spec.js +++ b/lib/shared/updates/update-entry-spec.js @@ -1,35 +1,43 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import type { RawEntryInfo } from '../../types/entry-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo, EntryRawUpdateInfo, + EntryUpdateData, } from '../../types/update-types.js'; -export const updateEntrySpec: UpdateSpec = - Object.freeze({ - mergeEntryInfos( - entryIDs: Set, - mergedEntryInfos: Array, - update: EntryUpdateInfo, - ) { - const { entryInfo } = update; - const entryID = entryInfo.id; - if (!entryID || entryIDs.has(entryID)) { - return; - } - mergedEntryInfos.push(entryInfo); - entryIDs.add(entryID); - }, - rawUpdateInfoFromRow(row: Object) { - const { entryID } = JSON.parse(row.content); - return { - type: updateTypes.UPDATE_ENTRY, - id: row.id.toString(), - time: row.time, - entryID, - }; - }, - }); +export const updateEntrySpec: UpdateSpec< + EntryUpdateInfo, + EntryRawUpdateInfo, + EntryUpdateData, +> = Object.freeze({ + mergeEntryInfos( + entryIDs: Set, + mergedEntryInfos: Array, + update: EntryUpdateInfo, + ) { + const { entryInfo } = update; + const entryID = entryInfo.id; + if (!entryID || entryIDs.has(entryID)) { + return; + } + mergedEntryInfos.push(entryInfo); + entryIDs.add(entryID); + }, + rawUpdateInfoFromRow(row: Object) { + const { entryID } = JSON.parse(row.content); + return { + type: updateTypes.UPDATE_ENTRY, + id: row.id.toString(), + time: row.time, + entryID, + }; + }, + updateContentForServerDB(data: EntryUpdateData) { + const { entryID } = data; + return JSON.stringify({ entryID }); + }, +}); diff --git a/lib/shared/updates/update-spec.js b/lib/shared/updates/update-spec.js index 292e0dab2..43daf09a9 100644 --- a/lib/shared/updates/update-spec.js +++ b/lib/shared/updates/update-spec.js @@ -1,43 +1,49 @@ // @flow import type { ThreadStoreOperation } from '../../ops/thread-store-ops.js'; import type { RawEntryInfo } from '../../types/entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../../types/message-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import type { ClientUpdateInfo, RawUpdateInfo, + UpdateData, } from '../../types/update-types.js'; import type { CurrentUserInfo, UserInfos } from '../../types/user-types.js'; -export type UpdateSpec = { +export type UpdateSpec< + UpdateInfo: ClientUpdateInfo, + RawInfo: RawUpdateInfo, + Data: UpdateData, +> = { +generateOpsForThreadUpdates?: ( storeThreadInfos: RawThreadInfos, update: UpdateInfo, ) => ?$ReadOnlyArray, +mergeEntryInfos?: ( entryIDs: Set, mergedEntryInfos: Array, update: UpdateInfo, ) => void, +reduceCurrentUser?: ( state: ?CurrentUserInfo, update: UpdateInfo, ) => ?CurrentUserInfo, +reduceUserInfos?: (state: UserInfos, update: UpdateInfo) => UserInfos, +reduceCalendarThreadFilters?: ( filteredThreadIDs: $ReadOnlySet, update: UpdateInfo, ) => $ReadOnlySet, +getRawMessageInfos?: (update: UpdateInfo) => $ReadOnlyArray, +mergeMessageInfosAndTruncationStatuses?: ( messageIDs: Set, messageInfos: Array, truncationStatuses: MessageTruncationStatuses, update: UpdateInfo, ) => void, +rawUpdateInfoFromRow: (row: Object) => RawInfo, + +updateContentForServerDB: (data: Data) => ?string, }; diff --git a/lib/shared/updates/update-specs.js b/lib/shared/updates/update-specs.js index b0a0db3d5..1d7d36128 100644 --- a/lib/shared/updates/update-specs.js +++ b/lib/shared/updates/update-specs.js @@ -1,27 +1,27 @@ // @flow import { badDeviceTokenSpec } from './bad-device-token-spec.js'; import { deleteAccountSpec } from './delete-account-spec.js'; import { deleteThreadSpec } from './delete-thread-spec.js'; import { joinThreadSpec } from './join-thread-spec.js'; import { updateCurrentUserSpec } from './update-current-user-spec.js'; import { updateEntrySpec } from './update-entry-spec.js'; import type { UpdateSpec } from './update-spec.js'; import { updateThreadReadStatusSpec } from './update-thread-read-status-spec.js'; import { updateThreadSpec } from './update-thread-spec.js'; import { updateUserSpec } from './update-user-spec.js'; import { updateTypes, type UpdateType } from '../../types/update-types-enum.js'; export const updateSpecs: { - +[UpdateType]: UpdateSpec<*, *>, + +[UpdateType]: UpdateSpec<*, *, *>, } = Object.freeze({ [updateTypes.DELETE_ACCOUNT]: deleteAccountSpec, [updateTypes.UPDATE_THREAD]: updateThreadSpec, [updateTypes.UPDATE_THREAD_READ_STATUS]: updateThreadReadStatusSpec, [updateTypes.DELETE_THREAD]: deleteThreadSpec, [updateTypes.JOIN_THREAD]: joinThreadSpec, [updateTypes.BAD_DEVICE_TOKEN]: badDeviceTokenSpec, [updateTypes.UPDATE_ENTRY]: updateEntrySpec, [updateTypes.UPDATE_CURRENT_USER]: updateCurrentUserSpec, [updateTypes.UPDATE_USER]: updateUserSpec, }); diff --git a/lib/shared/updates/update-thread-read-status-spec.js b/lib/shared/updates/update-thread-read-status-spec.js index fc0e897bc..e1426da73 100644 --- a/lib/shared/updates/update-thread-read-status-spec.js +++ b/lib/shared/updates/update-thread-read-status-spec.js @@ -1,56 +1,62 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import type { RawThreadInfo, RawThreadInfos, } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadReadStatusUpdateInfo, ThreadReadStatusRawUpdateInfo, + ThreadReadStatusUpdateData, } from '../../types/update-types.js'; export const updateThreadReadStatusSpec: UpdateSpec< ThreadReadStatusUpdateInfo, ThreadReadStatusRawUpdateInfo, + ThreadReadStatusUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadReadStatusUpdateInfo, ) { const storeThreadInfo: ?RawThreadInfo = storeThreadInfos[update.threadID]; if ( !storeThreadInfo || storeThreadInfo.currentUser.unread === update.unread ) { return null; } const updatedThread = { ...storeThreadInfo, currentUser: { ...storeThreadInfo.currentUser, unread: update.unread, }, }; return [ { type: 'replace', payload: { id: update.threadID, threadInfo: updatedThread, }, }, ]; }, rawUpdateInfoFromRow(row: Object) { const { threadID, unread } = JSON.parse(row.content); return { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: row.id.toString(), time: row.time, threadID, unread, }; }, + updateContentForServerDB(data: ThreadReadStatusUpdateData) { + const { threadID, unread } = data; + return JSON.stringify({ threadID, unread }); + }, }); diff --git a/lib/shared/updates/update-thread-spec.js b/lib/shared/updates/update-thread-spec.js index 3db83d6da..aab62a93b 100644 --- a/lib/shared/updates/update-thread-spec.js +++ b/lib/shared/updates/update-thread-spec.js @@ -1,58 +1,63 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import type { UpdateSpec } from './update-spec.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadUpdateInfo, ThreadRawUpdateInfo, + ThreadUpdateData, } from '../../types/update-types.js'; import { threadInFilterList } from '../thread-utils.js'; export const updateThreadSpec: UpdateSpec< ThreadUpdateInfo, ThreadRawUpdateInfo, + ThreadUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadUpdateInfo, ) { if ( threadInFilterList(update.threadInfo) || !filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set( [...filteredThreadIDs].filter(id => id !== update.threadInfo.id), ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.UPDATE_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, + updateContentForServerDB(data: ThreadUpdateData) { + return JSON.stringify({ threadID: data.threadID }); + }, }); diff --git a/lib/shared/updates/update-user-spec.js b/lib/shared/updates/update-user-spec.js index 07a272d80..412a1effc 100644 --- a/lib/shared/updates/update-user-spec.js +++ b/lib/shared/updates/update-user-spec.js @@ -1,21 +1,29 @@ // @flow import type { UpdateSpec } from './update-spec.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { UserUpdateInfo, UserRawUpdateInfo, + UserUpdateData, } from '../../types/update-types.js'; -export const updateUserSpec: UpdateSpec = - Object.freeze({ - rawUpdateInfoFromRow(row: Object) { - const content = JSON.parse(row.content); - return { - type: updateTypes.UPDATE_USER, - id: row.id.toString(), - time: row.time, - updatedUserID: content.updatedUserID, - }; - }, - }); +export const updateUserSpec: UpdateSpec< + UserUpdateInfo, + UserRawUpdateInfo, + UserUpdateData, +> = Object.freeze({ + rawUpdateInfoFromRow(row: Object) { + const content = JSON.parse(row.content); + return { + type: updateTypes.UPDATE_USER, + id: row.id.toString(), + time: row.time, + updatedUserID: content.updatedUserID, + }; + }, + updateContentForServerDB(data: UserUpdateData) { + const { updatedUserID } = data; + return JSON.stringify({ updatedUserID }); + }, +}); diff --git a/lib/types/update-types.js b/lib/types/update-types.js index a2c5c10d1..82f3c6248 100644 --- a/lib/types/update-types.js +++ b/lib/types/update-types.js @@ -1,399 +1,399 @@ // @flow import t, { type TUnion, type TInterface } from 'tcomb'; import { type RawEntryInfo, rawEntryInfoValidator } from './entry-types.js'; import { type RawMessageInfo, rawMessageInfoValidator, type MessageTruncationStatus, messageTruncationStatusValidator, } from './message-types.js'; import { type RawThreadInfo, rawThreadInfoValidator } from './thread-types.js'; import { updateTypes } from './update-types-enum.js'; import { type UserInfo, userInfoValidator, type UserInfos, userInfosValidator, type LoggedInUserInfo, loggedInUserInfoValidator, } from './user-types.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; type AccountDeletionData = { +deletedUserID: string, }; type ThreadData = { +threadID: string, }; type ThreadReadStatusData = { +threadID: string, +unread: boolean, }; type ThreadDeletionData = { +threadID: string, }; type ThreadJoinData = { +threadID: string, }; type BadDeviceTokenData = { +deviceToken: string, }; type EntryData = { +entryID: string, }; type CurrentUserData = {}; type UserData = { // ID of the UserInfo being updated +updatedUserID: string, }; type SharedUpdateData = { +userID: string, +time: number, }; -type AccountDeletionUpdateData = { +export type AccountDeletionUpdateData = { ...SharedUpdateData, ...AccountDeletionData, +type: 0, }; -type ThreadUpdateData = { +export type ThreadUpdateData = { ...SharedUpdateData, ...ThreadData, +type: 1, +targetSession?: string, }; -type ThreadReadStatusUpdateData = { +export type ThreadReadStatusUpdateData = { ...SharedUpdateData, ...ThreadReadStatusData, +type: 2, }; -type ThreadDeletionUpdateData = { +export type ThreadDeletionUpdateData = { ...SharedUpdateData, ...ThreadDeletionData, +type: 3, }; -type ThreadJoinUpdateData = { +export type ThreadJoinUpdateData = { ...SharedUpdateData, ...ThreadJoinData, +type: 4, +targetSession?: string, }; -type BadDeviceTokenUpdateData = { +export type BadDeviceTokenUpdateData = { ...SharedUpdateData, ...BadDeviceTokenData, +type: 5, +targetCookie: string, }; -type EntryUpdateData = { +export type EntryUpdateData = { ...SharedUpdateData, ...EntryData, +type: 6, +targetSession: string, }; -type CurrentUserUpdateData = { +export type CurrentUserUpdateData = { ...SharedUpdateData, ...CurrentUserData, +type: 7, }; -type UserUpdateData = { +export type UserUpdateData = { ...SharedUpdateData, ...UserData, +type: 8, +targetSession?: string, }; export type UpdateData = | AccountDeletionUpdateData | ThreadUpdateData | ThreadReadStatusUpdateData | ThreadDeletionUpdateData | ThreadJoinUpdateData | BadDeviceTokenUpdateData | EntryUpdateData | CurrentUserUpdateData | UserUpdateData; type SharedRawUpdateInfo = { +id: string, +time: number, }; export type AccountDeletionRawUpdateInfo = { ...SharedRawUpdateInfo, ...AccountDeletionData, +type: 0, }; export type ThreadRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadData, +type: 1, }; export type ThreadReadStatusRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadReadStatusData, +type: 2, }; export type ThreadDeletionRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadDeletionData, +type: 3, }; export type ThreadJoinRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadJoinData, +type: 4, }; export type BadDeviceTokenRawUpdateInfo = { ...SharedRawUpdateInfo, ...BadDeviceTokenData, +type: 5, }; export type EntryRawUpdateInfo = { ...SharedRawUpdateInfo, ...EntryData, +type: 6, }; export type CurrentUserRawUpdateInfo = { ...SharedRawUpdateInfo, ...CurrentUserData, +type: 7, }; export type UserRawUpdateInfo = { ...SharedRawUpdateInfo, ...UserData, +type: 8, }; export type RawUpdateInfo = | AccountDeletionRawUpdateInfo | ThreadRawUpdateInfo | ThreadReadStatusRawUpdateInfo | ThreadDeletionRawUpdateInfo | ThreadJoinRawUpdateInfo | BadDeviceTokenRawUpdateInfo | EntryRawUpdateInfo | CurrentUserRawUpdateInfo | UserRawUpdateInfo; export type AccountDeletionUpdateInfo = { +type: 0, +id: string, +time: number, +deletedUserID: string, }; export const accountDeletionUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.DELETE_ACCOUNT), id: t.String, time: t.Number, deletedUserID: t.String, }); export type ThreadUpdateInfo = { +type: 1, +id: string, +time: number, +threadInfo: RawThreadInfo, }; export const threadUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_THREAD), id: t.String, time: t.Number, threadInfo: rawThreadInfoValidator, }); export type ThreadReadStatusUpdateInfo = { +type: 2, +id: string, +time: number, +threadID: string, +unread: boolean, }; export const threadReadStatusUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_THREAD_READ_STATUS), id: t.String, time: t.Number, threadID: tID, unread: t.Boolean, }); export type ThreadDeletionUpdateInfo = { +type: 3, +id: string, +time: number, +threadID: string, }; export const threadDeletionUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.DELETE_THREAD), id: t.String, time: t.Number, threadID: tID, }); export type ThreadJoinUpdateInfo = { +type: 4, +id: string, +time: number, +threadInfo: RawThreadInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatus: MessageTruncationStatus, +rawEntryInfos: $ReadOnlyArray, }; export const threadJoinUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.JOIN_THREAD), id: t.String, time: t.Number, threadInfo: rawThreadInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatus: messageTruncationStatusValidator, rawEntryInfos: t.list(rawEntryInfoValidator), }); export type BadDeviceTokenUpdateInfo = { +type: 5, +id: string, +time: number, +deviceToken: string, }; export const badDeviceTokenUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.BAD_DEVICE_TOKEN), id: t.String, time: t.Number, deviceToken: t.String, }); export type EntryUpdateInfo = { +type: 6, +id: string, +time: number, +entryInfo: RawEntryInfo, }; export const entryUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_ENTRY), id: t.String, time: t.Number, entryInfo: rawEntryInfoValidator, }); export type CurrentUserUpdateInfo = { +type: 7, +id: string, +time: number, +currentUserInfo: LoggedInUserInfo, }; export type UserUpdateInfo = { +type: 8, +id: string, +time: number, // Updated UserInfo is already contained within the UpdatesResultWithUserInfos +updatedUserID: string, }; export const userUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_USER), id: t.String, time: t.Number, updatedUserID: t.String, }); export type ClientUpdateInfo = | AccountDeletionUpdateInfo | ThreadUpdateInfo | ThreadReadStatusUpdateInfo | ThreadDeletionUpdateInfo | ThreadJoinUpdateInfo | BadDeviceTokenUpdateInfo | EntryUpdateInfo | CurrentUserUpdateInfo | UserUpdateInfo; type ServerCurrentUserUpdateInfo = { +type: 7, +id: string, +time: number, +currentUserInfo: LoggedInUserInfo, }; export const serverCurrentUserUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_CURRENT_USER), id: t.String, time: t.Number, currentUserInfo: loggedInUserInfoValidator, }); export type ServerUpdateInfo = | AccountDeletionUpdateInfo | ThreadUpdateInfo | ThreadReadStatusUpdateInfo | ThreadDeletionUpdateInfo | ThreadJoinUpdateInfo | BadDeviceTokenUpdateInfo | EntryUpdateInfo | ServerCurrentUserUpdateInfo | UserUpdateInfo; export const serverUpdateInfoValidator: TUnion = t.union([ accountDeletionUpdateInfoValidator, threadUpdateInfoValidator, threadReadStatusUpdateInfoValidator, threadDeletionUpdateInfoValidator, threadJoinUpdateInfoValidator, badDeviceTokenUpdateInfoValidator, entryUpdateInfoValidator, serverCurrentUserUpdateInfoValidator, userUpdateInfoValidator, ]); export type ServerUpdatesResult = { +currentAsOf: number, +newUpdates: $ReadOnlyArray, }; export const serverUpdatesResultValidator: TInterface = tShape({ currentAsOf: t.Number, newUpdates: t.list(serverUpdateInfoValidator), }); export type ServerUpdatesResultWithUserInfos = { +updatesResult: ServerUpdatesResult, +userInfos: $ReadOnlyArray, }; export const serverUpdatesResultWithUserInfosValidator: TInterface = tShape({ updatesResult: serverUpdatesResultValidator, userInfos: t.list(userInfoValidator), }); export type ClientUpdatesResult = { +currentAsOf: number, +newUpdates: $ReadOnlyArray, }; export type ClientUpdatesResultWithUserInfos = { +updatesResult: ClientUpdatesResult, +userInfos: $ReadOnlyArray, }; export type CreateUpdatesResult = { +viewerUpdates: $ReadOnlyArray, +userInfos: UserInfos, }; export const createUpdatesResultValidator: TInterface = tShape({ viewerUpdates: t.list(serverUpdateInfoValidator), userInfos: userInfosValidator, }); export type ServerCreateUpdatesResponse = { +viewerUpdates: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export const serverCreateUpdatesResponseValidator: TInterface = tShape({ viewerUpdates: t.list(serverUpdateInfoValidator), userInfos: t.list(userInfoValidator), }); export type ClientCreateUpdatesResponse = { +viewerUpdates: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export const processUpdatesActionType = 'PROCESS_UPDATES';