Migrate message ids to UUIDv7

This commit is contained in:
Fedor Indutny 2024-10-07 20:17:03 -07:00 committed by GitHub
parent c1b5811c39
commit 60d7cbff3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 203 additions and 147 deletions

View file

@ -6,7 +6,7 @@ import { render } from 'react-dom';
import { batch as batchDispatch } from 'react-redux';
import PQueue from 'p-queue';
import pMap from 'p-map';
import { v4 as generateUuid } from 'uuid';
import { v7 as generateUuid } from 'uuid';
import * as Registration from './util/registration';
import MessageReceiver from './textsecure/MessageReceiver';
@ -169,6 +169,7 @@ import {
incrementMessageCounter,
initializeMessageCounter,
} from './util/incrementMessageCounter';
import { generateMessageId } from './util/generateMessageId';
import { RetryPlaceholders } from './util/retryPlaceholders';
import { setBatchingStrategy } from './util/messageBatcher';
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
@ -2694,7 +2695,8 @@ export async function startApp(): Promise<void> {
}
const partialMessage: MessageAttributesType = {
id: generateUuid(),
...generateMessageId(data.receivedAtCounter),
canReplyToStory: data.message.isStory
? data.message.canReplyToStory
: undefined,
@ -2705,7 +2707,6 @@ export async function startApp(): Promise<void> {
),
readStatus: ReadStatus.Read,
received_at_ms: data.receivedAtDate,
received_at: data.receivedAtCounter,
seenStatus: SeenStatus.NotApplicable,
sendStateByConversationId,
sent_at: timestamp,
@ -2957,13 +2958,13 @@ export async function startApp(): Promise<void> {
`Did not receive receivedAtCounter for message: ${data.timestamp}`
);
const partialMessage: MessageAttributesType = {
id: generateUuid(),
...generateMessageId(data.receivedAtCounter),
canReplyToStory: data.message.isStory
? data.message.canReplyToStory
: undefined,
conversationId: descriptor.id,
readStatus: ReadStatus.Unread,
received_at: data.receivedAtCounter,
received_at_ms: data.receivedAtDate,
seenStatus: SeenStatus.Unseen,
sent_at: data.timestamp,

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { FormEvent } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import { z } from 'zod';
import { Modal } from './Modal';
import type { LocalizerType } from '../types/I18N';

View file

@ -3,7 +3,7 @@
import classNames from 'classnames';
import React, { useMemo } from 'react';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import { getClassNamesFor } from '../util/getClassNamesFor';
import { CircleCheckbox } from './CircleCheckbox';

View file

@ -12,7 +12,7 @@ import React, {
} from 'react';
import { noop, partition } from 'lodash';
import classNames from 'classnames';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling';

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { UIEvent } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import type { LocalizerType } from '../types/I18N';
import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button';

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo, useState } from 'react';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import type { LocalizerType } from '../../../types/Util';
import { getMuteOptions } from '../../../util/getMuteOptions';

View file

@ -12,7 +12,6 @@ import {
} from 'lodash';
import Long from 'long';
import type { ClientZkGroupCipher } from '@signalapp/libsignal-client/zkgroup';
import { v4 as getGuid } from 'uuid';
import LRU from 'lru-cache';
import * as log from './logging/log';
import {
@ -101,6 +100,7 @@ import {
decodeGroupSendEndorsementResponse,
isValidGroupSendEndorsementsExpiration,
} from './util/groupSendEndorsements';
import { generateMessageId } from './util/generateMessageId';
type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
@ -293,7 +293,7 @@ type UploadedAvatarType = {
type BasicMessageType = Pick<
MessageAttributesType,
'id' | 'schemaVersion' | 'readStatus' | 'seenStatus'
'readStatus' | 'seenStatus'
>;
type GroupV2ChangeMessageType = {
@ -335,14 +335,6 @@ const SUPPORTED_CHANGE_EPOCH = 5;
export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR';
const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16;
function generateBasicMessage(): BasicMessageType {
return {
id: getGuid(),
schemaVersion: MAX_MESSAGE_SCHEMA,
// this is missing most properties to fulfill this type
};
}
// Group Links
export function generateGroupInviteLinkPassword(): Uint8Array {
@ -2024,12 +2016,13 @@ export async function createGroupV2(
});
const createdTheGroupMessage: MessageAttributesType = {
...generateBasicMessage(),
...generateMessageId(incrementMessageCounter()),
schemaVersion: MAX_MESSAGE_SCHEMA,
type: 'group-v2-change',
sourceServiceId: ourAci,
conversationId: conversation.id,
readStatus: ReadStatus.Read,
received_at: incrementMessageCounter(),
received_at_ms: timestamp,
timestamp,
seenStatus: SeenStatus.Seen,
@ -2468,7 +2461,6 @@ export async function initiateMigrationToGroupV2(
const groupChangeMessages: Array<GroupChangeMessageType> = [];
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v1-migration',
groupMigration: {
areWeInvited: false,
@ -2609,7 +2601,6 @@ export function buildMigrationBubble(
);
return {
...generateBasicMessage(),
type: 'group-v1-migration',
groupMigration: {
areWeInvited,
@ -2623,7 +2614,6 @@ export function buildMigrationBubble(
export function getBasicMigrationBubble(): GroupChangeMessageType {
return {
...generateBasicMessage(),
type: 'group-v1-migration',
groupMigration: {
areWeInvited: false,
@ -2697,7 +2687,6 @@ export async function joinGroupV2ViaLinkAndMigrate({
};
const groupChangeMessages: Array<GroupChangeMessageType> = [
{
...generateBasicMessage(),
type: 'group-v1-migration',
groupMigration: {
areWeInvited: false,
@ -2865,7 +2854,6 @@ export async function respondToGroupV2Migration({
seenStatus: SeenStatus.Seen,
},
{
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
details: [
@ -2946,7 +2934,6 @@ export async function respondToGroupV2Migration({
if (!areWeInvited && !areWeMember) {
// Add a message to the timeline saying the user was removed. This shouldn't happen.
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
details: [
@ -3184,8 +3171,9 @@ async function updateGroup(
return {
...changeMessage,
...generateMessageId(finalReceivedAt),
schemaVersion: MAX_MESSAGE_SCHEMA,
conversationId: conversation.id,
received_at: finalReceivedAt,
received_at_ms: syntheticSentAt,
sent_at: syntheticSentAt,
timestamp,
@ -3895,7 +3883,6 @@ async function updateGroupViaSingleChange({
catchupMessages.length > 0
) {
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
details: [
@ -4901,7 +4888,6 @@ function extractDiffs({
if (firstUpdate && serviceIdKindInvitedToGroup !== undefined) {
// Note, we will add 'you were invited' to group even if dropInitialJoinMessage = true
message = {
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
from: whoInvitedUsUserId || from,
@ -4919,7 +4905,6 @@ function extractDiffs({
};
} else if (firstUpdate && areWePendingApproval) {
message = {
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
from: ourAci,
@ -4940,7 +4925,6 @@ function extractDiffs({
sourceServiceId === ourAci
) {
message = {
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
from,
@ -4962,7 +4946,6 @@ function extractDiffs({
);
message = {
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
from,
@ -4973,7 +4956,6 @@ function extractDiffs({
};
} else if (firstUpdate && current.revision === 0) {
message = {
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
from,
@ -5002,7 +4984,6 @@ function extractDiffs({
}
message = {
...generateBasicMessage(),
type: 'group-v2-change',
sourceServiceId,
groupV2Change: {
@ -5014,7 +4995,6 @@ function extractDiffs({
};
} else if (details.length > 0) {
message = {
...generateBasicMessage(),
type: 'group-v2-change',
sourceServiceId,
groupV2Change: {
@ -5041,7 +5021,6 @@ function extractDiffs({
`extractDiffs/${logId}: generating change notification for new ${expireTimer} timer`
);
timerNotification = {
...generateBasicMessage(),
type: 'timer-notification',
sourceServiceId: isReJoin ? undefined : sourceServiceId,
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import PQueue from 'p-queue';
import { v4 as uuid } from 'uuid';
import { v7 as uuid } from 'uuid';
import { noop } from 'lodash';
import { Job } from './Job';

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import * as Errors from '../../types/errors';
import { strictAssert } from '../../util/assert';
@ -32,6 +31,7 @@ import type { AciString, ServiceIdString } from '../../types/ServiceId';
import { isAciString } from '../../util/isAciString';
import { handleMultipleSendErrors } from './handleMultipleSendErrors';
import { incrementMessageCounter } from '../../util/incrementMessageCounter';
import { generateMessageId } from '../../util/generateMessageId';
import type {
ConversationQueueJobBundle,
@ -159,11 +159,10 @@ export async function sendReaction(
remove: !emoji,
};
const ephemeralMessageForReactionSend = new window.Whisper.Message({
id: generateUuid(),
...generateMessageId(incrementMessageCounter()),
type: 'outgoing',
conversationId: conversation.get('id'),
sent_at: pendingReaction.timestamp,
received_at: incrementMessageCounter(),
received_at_ms: pendingReaction.timestamp,
timestamp: pendingReaction.timestamp,
sendStateByConversationId: zipObject(

View file

@ -174,6 +174,7 @@ import { ReceiptType } from '../types/Receipt';
import { getQuoteAttachment } from '../util/makeQuote';
import { deriveProfileKeyVersion } from '../util/zkgroup';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateMessageId } from '../util/generateMessageId';
import { getMessageAuthorText } from '../util/getMessageAuthorText';
import { downscaleOutgoingAttachment } from '../util/attachments';
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
@ -2328,11 +2329,10 @@ export class ConversationModel extends window.Backbone
: lastMessageTimestamp;
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
type: 'message-request-response-event',
sent_at: maybeLastMessageTimestamp,
received_at: incrementMessageCounter(),
received_at_ms: maybeLastMessageTimestamp,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.NotApplicable,
@ -3085,12 +3085,11 @@ export class ConversationModel extends window.Backbone
});
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(receivedAtCounter),
conversationId: this.id,
type: 'chat-session-refreshed',
timestamp: receivedAt,
sent_at: receivedAt,
received_at: receivedAtCounter,
received_at_ms: receivedAt,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
@ -3133,12 +3132,11 @@ export class ConversationModel extends window.Backbone
}
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(receivedAtCounter),
conversationId: this.id,
type: 'delivery-issue',
sourceServiceId: senderAci,
sent_at: receivedAt,
received_at: receivedAtCounter,
received_at_ms: receivedAt,
timestamp: receivedAt,
readStatus: ReadStatus.Unread,
@ -3183,12 +3181,11 @@ export class ConversationModel extends window.Backbone
const timestamp = Date.now();
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
type: 'keychange',
sent_at: timestamp,
timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp,
key_changed: keyChangedId,
readStatus: ReadStatus.Read,
@ -3245,12 +3242,11 @@ export class ConversationModel extends window.Backbone
const timestamp = Date.now();
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
type: 'conversation-merge',
sent_at: timestamp,
timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp,
conversationMerge: {
renderInfo,
@ -3294,12 +3290,11 @@ export class ConversationModel extends window.Backbone
log.info(`${logId}: adding notification`);
const timestamp = Date.now();
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
type: 'phone-number-discovery',
sent_at: timestamp,
timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp,
phoneNumberDiscovery: {
e164,
@ -3343,12 +3338,11 @@ export class ConversationModel extends window.Backbone
const timestamp = Date.now();
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
local: Boolean(options.local),
readStatus: ReadStatus.Read,
received_at_ms: timestamp,
received_at: incrementMessageCounter(),
seenStatus: options.local ? SeenStatus.Seen : SeenStatus.Unseen,
sent_at: lastMessage,
timestamp,
@ -3388,11 +3382,10 @@ export class ConversationModel extends window.Backbone
): Promise<void> {
const now = Date.now();
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
type: 'profile-change',
sent_at: now,
received_at: incrementMessageCounter(),
received_at_ms: now,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.NotApplicable,
@ -3432,11 +3425,10 @@ export class ConversationModel extends window.Backbone
): Promise<string> {
const now = Date.now();
const message: MessageAttributesType = {
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: this.id,
type,
sent_at: now,
received_at: incrementMessageCounter(),
received_at_ms: now,
timestamp: now,
@ -4102,7 +4094,7 @@ export class ConversationModel extends window.Backbone
// Here we move attachments to disk
const attributes = await upgradeMessageSchema({
id: generateGuid(),
...generateMessageId(incrementMessageCounter()),
timestamp: now,
type: 'outgoing',
body,
@ -4112,7 +4104,6 @@ export class ConversationModel extends window.Backbone
preview,
attachments: attachmentsToSend,
sent_at: now,
received_at: incrementMessageCounter(),
received_at_ms: now,
expirationStartTimestamp,
expireTimer,
@ -4734,9 +4725,9 @@ export class ConversationModel extends window.Backbone
const shouldBeRead =
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
const id = generateGuid();
const counter = receivedAt ?? incrementMessageCounter();
const attributes = {
id,
...generateMessageId(counter),
conversationId: this.id,
expirationTimerUpdate: {
expireTimer,
@ -4747,7 +4738,6 @@ export class ConversationModel extends window.Backbone
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
readStatus: shouldBeRead ? ReadStatus.Read : ReadStatus.Unread,
received_at_ms: receivedAtMS,
received_at: receivedAt ?? incrementMessageCounter(),
seenStatus: shouldBeRead ? SeenStatus.Seen : SeenStatus.Unseen,
sent_at: sentAt,
timestamp: sentAt,
@ -4760,7 +4750,7 @@ export class ConversationModel extends window.Backbone
});
window.MessageCache.__DEPRECATED$register(
id,
attributes.id,
attributes,
'updateExpirationTimer'
);

View file

@ -11,7 +11,6 @@ import {
pick,
union,
} from 'lodash';
import { v4 as generateUuid } from 'uuid';
import type {
CustomError,
@ -26,6 +25,7 @@ import { isNotNil } from '../util/isNotNil';
import { isNormalNumber } from '../util/isNormalNumber';
import { strictAssert } from '../util/assert';
import { hydrateStoryContext } from '../util/hydrateStoryContext';
import { generateMessageId } from '../util/generateMessageId';
import { drop } from '../util/drop';
import type { ConversationModel } from './conversations';
import type {
@ -1642,7 +1642,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
}
const messageId = message.get('id') || generateUuid();
const messageId =
message.get('id') || generateMessageId(this.get('received_at')).id;
// Send delivery receipts, but only for non-story sealed sender messages
// and not for messages from unaccepted conversations

View file

@ -2,9 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import noop from 'lodash/noop';
import { v4 as generateUuid } from 'uuid';
import { v7 as generateUuid } from 'uuid';
import { DataWriter } from '../sql/Client';
import type { MessageModel } from '../models/messages';
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
import { ReactionSource } from './ReactionSource';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
@ -12,6 +13,7 @@ import { getSourceServiceId, isStory } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import { isDirectConversation } from '../util/whatTypeOfConversation';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateMessageId } from '../util/generateMessageId';
import { repeat, zipObject } from '../util/iterables';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { isAciString } from '../util/isAciString';
@ -87,31 +89,31 @@ export async function enqueueReactionForSend({
: undefined;
// Only used in story scenarios, where we use a whole message to represent the reaction
const storyReactionMessage = storyMessage
? new window.Whisper.Message({
id: generateUuid(),
type: 'outgoing',
conversationId: targetConversation.id,
sent_at: timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp,
timestamp,
expireTimer,
sendStateByConversationId: zipObject(
targetConversation.getMemberConversationIds(),
repeat({
status: SendStatus.Pending,
updatedAt: Date.now(),
})
),
storyId: message.id,
storyReaction: {
emoji,
targetAuthorAci,
targetTimestamp,
},
})
: undefined;
let storyReactionMessage: MessageModel | undefined;
if (storyMessage) {
storyReactionMessage = new window.Whisper.Message({
...generateMessageId(incrementMessageCounter()),
type: 'outgoing',
conversationId: targetConversation.id,
sent_at: timestamp,
received_at_ms: timestamp,
timestamp,
expireTimer,
sendStateByConversationId: zipObject(
targetConversation.getMemberConversationIds(),
repeat({
status: SendStatus.Pending,
updatedAt: Date.now(),
})
),
storyId: message.id,
storyReaction: {
emoji,
targetAuthorAci,
targetTimestamp,
},
});
}
const reaction: ReactionAttributesType = {
envelopeId: generateUuid(),

View file

@ -3,7 +3,7 @@
import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client';
import { ReceiptCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
import { v4 as generateUuid } from 'uuid';
import { v7 as generateUuid } from 'uuid';
import pMap from 'p-map';
import { Writable } from 'stream';
import { isNumber } from 'lodash';
@ -63,6 +63,7 @@ import {
deriveGroupPublicParams,
} from '../../util/zkgroup';
import { incrementMessageCounter } from '../../util/incrementMessageCounter';
import { generateMessageId } from '../../util/generateMessageId';
import { isAciString } from '../../util/isAciString';
import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from '../../util/phoneNumberSharingMode';
@ -487,6 +488,26 @@ export class BackupImportStream extends Writable {
const batch = Array.from(this.saveMessageBatch);
this.saveMessageBatch.clear();
// There are a few indexes that start with message id, and many more that
// start with conversationId. Sort messages by both to make sure that we
// are not doing random insertions into the database file.
// This improves bulk insert performance >2x.
batch.sort((a, b) => {
if (a.conversationId > b.conversationId) {
return -1;
}
if (a.conversationId < b.conversationId) {
return 1;
}
if (a.id < b.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
return 0;
});
await DataWriter.saveMessages(batch, {
forceSave: true,
ourAci,
@ -1235,9 +1256,8 @@ export class BackupImportStream extends Writable {
}
let attributes: MessageAttributesType = {
id: generateUuid(),
...generateMessageId(incrementMessageCounter()),
conversationId: chatConvo.id,
received_at: incrementMessageCounter(),
sent_at: timestamp,
source: authorConvo?.e164,
sourceServiceId: authorConvo?.serviceId,

View file

@ -18,7 +18,7 @@ import { cleanDataForIpc } from './cleanDataForIpc';
import type { AciString, ServiceIdString } from '../types/ServiceId';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import * as log from '../logging/log';
import { isValidUuid } from '../util/isValidUuid';
import { isValidUuidV7 } from '../util/isValidUuid';
import * as Errors from '../types/errors';
import type { StoredJob } from '../jobs/types';
@ -603,7 +603,7 @@ async function saveMessage(
jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert),
});
softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
softAssert(isValidUuidV7(id), 'saveMessage: messageId is not a UUID');
void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update();

View file

@ -9,7 +9,6 @@ import rimraf from 'rimraf';
import { randomBytes } from 'crypto';
import type { Database, Statement } from '@signalapp/better-sqlite3';
import SQL from '@signalapp/better-sqlite3';
import { v4 as generateUuid } from 'uuid';
import { z } from 'zod';
import type { ReadonlyDeep } from 'type-fest';
@ -49,6 +48,7 @@ import { isNormalNumber } from '../util/isNormalNumber';
import { isNotNil } from '../util/isNotNil';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import * as durations from '../util/durations';
import { generateMessageId } from '../util/generateMessageId';
import { formatCountForLogging } from '../logging/formatCountForLogging';
import type { ConversationColorType, CustomColorType } from '../types/Colors';
import type { BadgeType, BadgeImageType } from '../badges/types';
@ -2328,7 +2328,7 @@ export function saveMessage(
const toCreate = {
...data,
id: id || generateUuid(),
id: id || generateMessageId(data.received_at).id,
};
prepare(
@ -6813,23 +6813,30 @@ function pageMessages(
writable.exec(
`
CREATE TEMP TABLE tmp_${runId}_updated_messages
(rowid INTEGER PRIMARY KEY ASC);
(rowid INTEGER PRIMARY KEY, received_at INTEGER, sent_at INTEGER);
INSERT INTO tmp_${runId}_updated_messages (rowid)
SELECT rowid FROM messages;
CREATE INDEX tmp_${runId}_updated_messages_received_at
ON tmp_${runId}_updated_messages (received_at ASC, sent_at ASC);
INSERT INTO tmp_${runId}_updated_messages
(rowid, received_at, sent_at)
SELECT rowid, received_at, sent_at FROM messages
ORDER BY received_at ASC, sent_at ASC;
CREATE TEMP TRIGGER tmp_${runId}_message_updates
UPDATE OF json ON messages
BEGIN
INSERT OR IGNORE INTO tmp_${runId}_updated_messages (rowid)
VALUES (NEW.rowid);
INSERT OR IGNORE INTO tmp_${runId}_updated_messages
(rowid, received_at, sent_at)
VALUES (NEW.rowid, NEW.received_at, NEW.sent_at);
END;
CREATE TEMP TRIGGER tmp_${runId}_message_inserts
AFTER INSERT ON messages
BEGIN
INSERT OR IGNORE INTO tmp_${runId}_updated_messages (rowid)
VALUES (NEW.rowid);
INSERT OR IGNORE INTO tmp_${runId}_updated_messages
(rowid, received_at, sent_at)
VALUES (NEW.rowid, NEW.received_at, NEW.sent_at);
END;
`
);
@ -6840,10 +6847,11 @@ function pageMessages(
const rowids: Array<number> = writable
.prepare<Query>(
`
DELETE FROM tmp_${runId}_updated_messages
RETURNING rowid
LIMIT $chunkSize;
`
DELETE FROM tmp_${runId}_updated_messages
RETURNING rowid
ORDER BY received_at ASC, sent_at ASC
LIMIT $chunkSize;
`
)
.pluck()
.all({ chunkSize });

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import { assert } from 'chai';
import { type AciString, generateAci } from '../types/ServiceId';

View file

@ -3,7 +3,7 @@
import { assert } from 'chai';
import type { PeekInfo } from '@signalapp/ringrtc';
import uuid from 'uuid';
import { v4 as uuid } from 'uuid';
import {
getPeerIdFromConversation,
getCallIdFromEra,

View file

@ -0,0 +1,47 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { stringify } from 'uuid';
import { getRandomBytes } from '../Crypto';
export type GeneratedMessageIdType = Readonly<{
id: string;
received_at: number;
}>;
// See https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.7
export function generateMessageId(counter: number): GeneratedMessageIdType {
const uuid = getRandomBytes(16);
/* eslint-disable no-bitwise */
// We compose uuid out of 48 bits (6 bytes of) timestamp-like counter:
// `incrementMessageCounter`. Note big-endian encoding (which ensures proper
// lexicographical order), and floating point divisions (because `&` operator
// coerces to 32bit integers)
uuid[0] = (counter / 0x10000000000) & 0xff;
uuid[1] = (counter / 0x00100000000) & 0xff;
uuid[2] = (counter / 0x00001000000) & 0xff;
uuid[3] = (counter / 0x00000010000) & 0xff;
uuid[4] = (counter / 0x00000000100) & 0xff;
uuid[5] = (counter / 0x00000000001) & 0xff;
// Mask out 4 bits of version number
uuid[6] &= 0x0f;
// And set the version to 7
uuid[6] |= 0x70;
// Mask out 2 bits of variant
uuid[8] &= 0x3f;
// And set it to "2"
uuid[8] |= 0x80;
/* eslint-enable no-bitwise */
return {
id: stringify(uuid),
received_at: counter,
};
}

View file

@ -4,6 +4,7 @@
import { debounce, isNumber } from 'lodash';
import { strictAssert } from './assert';
import { safeParseInteger } from './numbers';
import { DataReader } from '../sql/Client';
import * as log from '../logging/log';
@ -15,7 +16,9 @@ export async function initializeMessageCounter(): Promise<void> {
'incrementMessageCounter: already initialized'
);
const storedCounter = Number(localStorage.getItem('lastReceivedAtCounter'));
const storedCounter = safeParseInteger(
localStorage.getItem('lastReceivedAtCounter') ?? ''
);
const dbCounter = await DataReader.getMaxMessageCounter();
if (isNumber(dbCounter) && isNumber(storedCounter)) {

View file

@ -16,3 +16,19 @@ export const isValidUuid = (value: unknown): value is string => {
return UUID_REGEXP.test(value);
};
const UUID_V7_REGEXP =
/^[0-9A-F]{8}-[0-9A-F]{4}-7[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
export const isValidUuidV7 = (value: unknown): value is string => {
if (typeof value !== 'string') {
return false;
}
// Zero UUID is a valid uuid.
if (value === '00000000-0000-0000-0000-000000000000') {
return true;
}
return UUID_V7_REGEXP.test(value);
};

View file

@ -5,7 +5,7 @@ import { ipcRenderer } from 'electron';
import { isString, isTypedArray } from 'lodash';
import { join, normalize, basename } from 'path';
import fse from 'fs-extra';
import getGuid from 'uuid/v4';
import { v4 as getGuid } from 'uuid';
import { isPathInside } from '../util/isPathInside';
import { writeWindowsZoneIdentifier } from '../util/windowsZoneIdentifier';