@massalabs/gossip-sdk 0.0.2-dev.20260220052333 → 0.0.2-dev.20260220053835
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/config/sdk.js +1 -1
- package/dist/contacts.d.ts +11 -17
- package/dist/contacts.js +20 -26
- package/dist/core/SdkEventEmitter.d.ts +2 -0
- package/dist/core/SdkEventEmitter.js +2 -0
- package/dist/core/SdkPolling.d.ts +2 -5
- package/dist/core/SdkPolling.js +1 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -0
- package/dist/db.d.ts +34 -56
- package/dist/db.js +39 -229
- package/dist/gossip.d.ts +54 -23
- package/dist/gossip.js +149 -120
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -4
- package/dist/queries/activeSeekers.d.ts +2 -0
- package/dist/queries/activeSeekers.js +18 -0
- package/dist/queries/announcementCursors.d.ts +2 -0
- package/dist/queries/announcementCursors.js +20 -0
- package/dist/queries/contacts.d.ts +13 -0
- package/dist/queries/contacts.js +43 -0
- package/dist/queries/discussions.d.ts +21 -0
- package/dist/queries/discussions.js +73 -0
- package/dist/queries/index.d.ts +7 -0
- package/dist/queries/index.js +7 -0
- package/dist/queries/messages.d.ts +27 -0
- package/dist/queries/messages.js +120 -0
- package/dist/queries/pendingAnnouncements.d.ts +4 -0
- package/dist/queries/pendingAnnouncements.js +11 -0
- package/dist/queries/userProfile.d.ts +22 -0
- package/dist/queries/userProfile.js +116 -0
- package/dist/schema.d.ts +1280 -0
- package/dist/schema.js +164 -0
- package/dist/services/announcement.d.ts +8 -7
- package/dist/services/announcement.js +95 -121
- package/dist/services/auth.d.ts +1 -3
- package/dist/services/auth.js +4 -11
- package/dist/services/discussion.d.ts +8 -15
- package/dist/services/discussion.js +62 -62
- package/dist/services/message.d.ts +15 -26
- package/dist/services/message.js +282 -270
- package/dist/services/refresh.d.ts +3 -5
- package/dist/services/refresh.js +10 -13
- package/dist/sqlite-worker.d.ts +12 -0
- package/dist/sqlite-worker.js +106 -0
- package/dist/sqlite.d.ts +79 -0
- package/dist/sqlite.js +448 -0
- package/dist/utils/contacts.d.ts +2 -5
- package/dist/utils/contacts.js +23 -39
- package/dist/utils/discussions.d.ts +7 -2
- package/dist/utils/discussions.js +24 -5
- package/dist/utils/logs.js +1 -3
- package/dist/utils/validation.d.ts +2 -4
- package/dist/utils/validation.js +5 -10
- package/package.json +5 -7
- package/dist/api/messageProtocol/mock.d.ts +0 -12
- package/dist/api/messageProtocol/mock.js +0 -12
- package/dist/types/events.d.ts +0 -80
- package/dist/types/events.js +0 -7
package/dist/services/message.js
CHANGED
|
@@ -5,24 +5,98 @@
|
|
|
5
5
|
* This service works both in host app contexts and SDK/automation context.
|
|
6
6
|
*/
|
|
7
7
|
import { MessageDirection, MessageStatus, MessageType, MESSAGE_ID_SIZE, } from '../db';
|
|
8
|
+
import { getMessageById, getMessageByOwnerAndSeeker, findMessageByMessageId, insertMessage, updateMessageById, deleteDeliveredKeepAliveMessages, getOutgoingSentMessagesByOwner, getWaitingMessageCount as getWaitingCount, getSendQueueMessages, } from '../queries';
|
|
9
|
+
import { getDiscussionByOwnerAndContact, updateDiscussionById, incrementUnreadCount, decrementUnreadCount, } from '../queries';
|
|
10
|
+
import { replaceActiveSeekers } from '../queries';
|
|
8
11
|
import { decodeUserId, encodeUserId } from '../utils/userId';
|
|
9
12
|
import { SessionStatus } from '../wasm/bindings';
|
|
10
13
|
import { serializeRegularMessage, serializeReplyMessage, serializeForwardMessage, serializeKeepAliveMessage, deserializeMessage, } from '../utils/messageSerialization';
|
|
11
|
-
import { encodeToBase64 } from '../utils/base64';
|
|
14
|
+
import { encodeToBase64, decodeFromBase64 } from '../utils/base64';
|
|
12
15
|
import { sessionStatusToString } from '../wasm/session';
|
|
13
16
|
import { Logger } from '../utils/logs';
|
|
14
17
|
import { defaultSdkConfig } from '../config/sdk';
|
|
15
18
|
import { SdkEventType } from '../core/SdkEventEmitter';
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// JSON serialization helpers for message fields stored as text in SQLite
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Serialize replyTo to JSON string for SQLite storage.
|
|
23
|
+
* Uint8Array fields are base64-encoded. */
|
|
24
|
+
function serializeReplyTo(replyTo) {
|
|
25
|
+
if (!replyTo)
|
|
26
|
+
return null;
|
|
27
|
+
return JSON.stringify({
|
|
28
|
+
originalMsgId: encodeToBase64(replyTo.originalMsgId),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Deserialize replyTo from JSON string. */
|
|
32
|
+
function deserializeReplyTo(json) {
|
|
33
|
+
if (!json)
|
|
34
|
+
return undefined;
|
|
35
|
+
const parsed = JSON.parse(json);
|
|
36
|
+
return {
|
|
37
|
+
originalMsgId: decodeFromBase64(parsed.originalMsgId),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Serialize forwardOf to JSON string for SQLite storage. */
|
|
41
|
+
function serializeForwardOf(forwardOf) {
|
|
42
|
+
if (!forwardOf)
|
|
43
|
+
return null;
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
originalContent: forwardOf.originalContent,
|
|
46
|
+
originalContactId: forwardOf.originalContactId
|
|
47
|
+
? encodeToBase64(forwardOf.originalContactId)
|
|
48
|
+
: undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Deserialize forwardOf from JSON string. */
|
|
52
|
+
function deserializeForwardOf(json) {
|
|
53
|
+
if (!json)
|
|
54
|
+
return undefined;
|
|
55
|
+
const parsed = JSON.parse(json);
|
|
56
|
+
return {
|
|
57
|
+
originalContent: parsed.originalContent ?? undefined,
|
|
58
|
+
originalContactId: parsed.originalContactId
|
|
59
|
+
? decodeFromBase64(parsed.originalContactId)
|
|
60
|
+
: undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Serialize metadata to JSON string. */
|
|
64
|
+
function serializeMetadata(metadata) {
|
|
65
|
+
if (!metadata)
|
|
66
|
+
return null;
|
|
67
|
+
return JSON.stringify(metadata);
|
|
68
|
+
}
|
|
69
|
+
/** Deserialize metadata from JSON string. */
|
|
70
|
+
function deserializeMetadata(json) {
|
|
71
|
+
if (!json)
|
|
72
|
+
return undefined;
|
|
73
|
+
return JSON.parse(json);
|
|
74
|
+
}
|
|
75
|
+
/** Convert a SQLite row from the messages table to a Message object. */
|
|
76
|
+
export function rowToMessage(row) {
|
|
77
|
+
return {
|
|
78
|
+
id: row.id,
|
|
79
|
+
messageId: row.messageId ?? undefined,
|
|
80
|
+
ownerUserId: row.ownerUserId,
|
|
81
|
+
contactUserId: row.contactUserId,
|
|
82
|
+
content: row.content,
|
|
83
|
+
serializedContent: row.serializedContent ?? undefined,
|
|
84
|
+
type: row.type,
|
|
85
|
+
direction: row.direction,
|
|
86
|
+
status: row.status,
|
|
87
|
+
timestamp: row.timestamp,
|
|
88
|
+
metadata: deserializeMetadata(row.metadata),
|
|
89
|
+
seeker: row.seeker ?? undefined,
|
|
90
|
+
replyTo: deserializeReplyTo(row.replyTo),
|
|
91
|
+
forwardOf: deserializeForwardOf(row.forwardOf),
|
|
92
|
+
encryptedMessage: row.encryptedMessage ?? undefined,
|
|
93
|
+
whenToSend: row.whenToSend ?? undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
16
96
|
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
|
17
97
|
const logger = new Logger('MessageService');
|
|
18
98
|
export class MessageService {
|
|
19
|
-
constructor(
|
|
20
|
-
Object.defineProperty(this, "db", {
|
|
21
|
-
enumerable: true,
|
|
22
|
-
configurable: true,
|
|
23
|
-
writable: true,
|
|
24
|
-
value: void 0
|
|
25
|
-
});
|
|
99
|
+
constructor(messageProtocol, session, eventEmitter, config = defaultSdkConfig) {
|
|
26
100
|
Object.defineProperty(this, "messageProtocol", {
|
|
27
101
|
enumerable: true,
|
|
28
102
|
configurable: true,
|
|
@@ -35,19 +109,19 @@ export class MessageService {
|
|
|
35
109
|
writable: true,
|
|
36
110
|
value: void 0
|
|
37
111
|
});
|
|
38
|
-
Object.defineProperty(this, "
|
|
112
|
+
Object.defineProperty(this, "eventEmitter", {
|
|
39
113
|
enumerable: true,
|
|
40
114
|
configurable: true,
|
|
41
115
|
writable: true,
|
|
42
116
|
value: void 0
|
|
43
117
|
});
|
|
44
|
-
Object.defineProperty(this, "
|
|
118
|
+
Object.defineProperty(this, "config", {
|
|
45
119
|
enumerable: true,
|
|
46
120
|
configurable: true,
|
|
47
121
|
writable: true,
|
|
48
122
|
value: void 0
|
|
49
123
|
});
|
|
50
|
-
Object.defineProperty(this, "
|
|
124
|
+
Object.defineProperty(this, "refreshService", {
|
|
51
125
|
enumerable: true,
|
|
52
126
|
configurable: true,
|
|
53
127
|
writable: true,
|
|
@@ -65,13 +139,13 @@ export class MessageService {
|
|
|
65
139
|
writable: true,
|
|
66
140
|
value: false
|
|
67
141
|
});
|
|
68
|
-
this.db = db;
|
|
69
142
|
this.messageProtocol = messageProtocol;
|
|
70
143
|
this.session = session;
|
|
71
|
-
this.discussionService = discussionService;
|
|
72
144
|
this.eventEmitter = eventEmitter;
|
|
73
145
|
this.config = config;
|
|
74
|
-
|
|
146
|
+
}
|
|
147
|
+
setRefreshService(refreshService) {
|
|
148
|
+
this.refreshService = refreshService;
|
|
75
149
|
}
|
|
76
150
|
async fetchMessages() {
|
|
77
151
|
const log = logger.forMethod('fetchMessages');
|
|
@@ -129,7 +203,7 @@ export class MessageService {
|
|
|
129
203
|
await sleep(this.config.messages.fetchDelayMs);
|
|
130
204
|
}
|
|
131
205
|
try {
|
|
132
|
-
await
|
|
206
|
+
await replaceActiveSeekers(seekers);
|
|
133
207
|
}
|
|
134
208
|
catch (error) {
|
|
135
209
|
log.error('failed to update active seekers', error);
|
|
@@ -151,6 +225,41 @@ export class MessageService {
|
|
|
151
225
|
this.isFetchingMessages = false;
|
|
152
226
|
}
|
|
153
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Add a message to SQLite and update the corresponding discussion.
|
|
230
|
+
*/
|
|
231
|
+
async addMessageAndUpdateDiscussion(message) {
|
|
232
|
+
const messageId = await insertMessage({
|
|
233
|
+
messageId: message.messageId,
|
|
234
|
+
ownerUserId: message.ownerUserId,
|
|
235
|
+
contactUserId: message.contactUserId,
|
|
236
|
+
content: message.content,
|
|
237
|
+
serializedContent: message.serializedContent,
|
|
238
|
+
type: message.type,
|
|
239
|
+
direction: message.direction,
|
|
240
|
+
status: message.status,
|
|
241
|
+
timestamp: message.timestamp,
|
|
242
|
+
metadata: serializeMetadata(message.metadata),
|
|
243
|
+
seeker: message.seeker,
|
|
244
|
+
replyTo: serializeReplyTo(message.replyTo),
|
|
245
|
+
forwardOf: serializeForwardOf(message.forwardOf),
|
|
246
|
+
encryptedMessage: message.encryptedMessage,
|
|
247
|
+
whenToSend: message.whenToSend,
|
|
248
|
+
});
|
|
249
|
+
const discussion = await getDiscussionByOwnerAndContact(message.ownerUserId, message.contactUserId);
|
|
250
|
+
if (discussion) {
|
|
251
|
+
await updateDiscussionById(discussion.id, {
|
|
252
|
+
lastMessageId: messageId,
|
|
253
|
+
lastMessageContent: message.content,
|
|
254
|
+
lastMessageTimestamp: message.timestamp,
|
|
255
|
+
updatedAt: new Date(),
|
|
256
|
+
});
|
|
257
|
+
if (message.direction === MessageDirection.INCOMING) {
|
|
258
|
+
await incrementUnreadCount(discussion.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return messageId;
|
|
262
|
+
}
|
|
154
263
|
async decryptMessages(encrypted) {
|
|
155
264
|
const log = logger.forMethod('decryptMessages');
|
|
156
265
|
const decrypted = [];
|
|
@@ -207,187 +316,117 @@ export class MessageService {
|
|
|
207
316
|
const log = logger.forMethod('storeDecryptedMessages');
|
|
208
317
|
const storedIds = [];
|
|
209
318
|
for (const message of decrypted) {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
preview: message.content.slice(0, 50),
|
|
216
|
-
});
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
// If received msg has same messageId as a previously received msg
|
|
220
|
-
const isDuplicate = await this.handleDuplicateMessageId(message, ownerUserId);
|
|
221
|
-
if (isDuplicate) {
|
|
222
|
-
log.info('Duplicate message received, skipping', {
|
|
223
|
-
senderId: message.senderId,
|
|
224
|
-
preview: message.content.slice(0, 30),
|
|
225
|
-
});
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
if (message.replyTo?.originalMsgId) {
|
|
229
|
-
const original = await this.findMessageByMsgId(message.replyTo.originalMsgId, ownerUserId, message.senderId);
|
|
230
|
-
if (!original) {
|
|
231
|
-
log.warn('reply target not found', {
|
|
232
|
-
originalMsgId: encodeToBase64(message.replyTo.originalMsgId),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const id = await this.db.messages.add({
|
|
237
|
-
ownerUserId,
|
|
238
|
-
contactUserId: discussion.contactUserId,
|
|
239
|
-
content: message.content,
|
|
240
|
-
type: message.type,
|
|
241
|
-
direction: MessageDirection.INCOMING,
|
|
242
|
-
status: MessageStatus.DELIVERED,
|
|
243
|
-
timestamp: message.sentAt,
|
|
244
|
-
metadata: {},
|
|
245
|
-
messageId: message.messageId,
|
|
246
|
-
replyTo: message.replyTo
|
|
247
|
-
? {
|
|
248
|
-
originalMsgId: message.replyTo.originalMsgId,
|
|
249
|
-
}
|
|
250
|
-
: undefined,
|
|
251
|
-
forwardOf: message.forwardOf
|
|
252
|
-
? {
|
|
253
|
-
originalContent: message.forwardOf.originalContent,
|
|
254
|
-
originalContactId: message.forwardOf.originalContactId,
|
|
255
|
-
}
|
|
256
|
-
: undefined,
|
|
319
|
+
const discussion = await getDiscussionByOwnerAndContact(ownerUserId, message.senderId);
|
|
320
|
+
if (!discussion) {
|
|
321
|
+
log.error('no discussion for incoming message', {
|
|
322
|
+
senderId: message.senderId,
|
|
323
|
+
preview: message.content.slice(0, 50),
|
|
257
324
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// If received msg has same messageId as a previously received msg
|
|
328
|
+
const isDuplicate = await this.handleDuplicateMessageId(message, ownerUserId);
|
|
329
|
+
if (isDuplicate) {
|
|
330
|
+
log.info('Duplicate message received, skipping', {
|
|
331
|
+
senderId: message.senderId,
|
|
332
|
+
preview: message.content.slice(0, 30),
|
|
266
333
|
});
|
|
267
|
-
return id;
|
|
268
|
-
});
|
|
269
|
-
if (result === null) {
|
|
270
334
|
continue;
|
|
271
335
|
}
|
|
272
|
-
|
|
336
|
+
if (message.replyTo?.originalMsgId) {
|
|
337
|
+
const original = await this.findMessageByMsgId(message.replyTo.originalMsgId, ownerUserId, message.senderId);
|
|
338
|
+
if (!original) {
|
|
339
|
+
log.warn('reply target not found', {
|
|
340
|
+
originalMsgId: encodeToBase64(message.replyTo.originalMsgId),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const id = await insertMessage({
|
|
345
|
+
messageId: message.messageId,
|
|
346
|
+
ownerUserId,
|
|
347
|
+
contactUserId: discussion.contactUserId,
|
|
348
|
+
content: message.content,
|
|
349
|
+
type: message.type,
|
|
350
|
+
direction: MessageDirection.INCOMING,
|
|
351
|
+
status: MessageStatus.DELIVERED,
|
|
352
|
+
timestamp: message.sentAt,
|
|
353
|
+
metadata: serializeMetadata({}),
|
|
354
|
+
replyTo: serializeReplyTo(message.replyTo),
|
|
355
|
+
forwardOf: serializeForwardOf(message.forwardOf),
|
|
356
|
+
});
|
|
357
|
+
// Update discussion in SQLite
|
|
358
|
+
const now = new Date();
|
|
359
|
+
await updateDiscussionById(discussion.id, {
|
|
360
|
+
lastMessageId: id,
|
|
361
|
+
lastMessageContent: message.content,
|
|
362
|
+
lastMessageTimestamp: message.sentAt,
|
|
363
|
+
updatedAt: now,
|
|
364
|
+
lastSyncTimestamp: now,
|
|
365
|
+
});
|
|
366
|
+
await incrementUnreadCount(discussion.id);
|
|
273
367
|
storedIds.push(id);
|
|
274
368
|
// Emit event for new message
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
this.eventEmitter.emit(SdkEventType.MESSAGE_RECEIVED,
|
|
369
|
+
const row = await getMessageById(id);
|
|
370
|
+
if (row) {
|
|
371
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_RECEIVED, rowToMessage(row));
|
|
278
372
|
}
|
|
279
373
|
}
|
|
280
374
|
return storedIds;
|
|
281
375
|
}
|
|
282
376
|
/**
|
|
283
|
-
* Checks for duplicate incoming messages
|
|
284
|
-
*
|
|
285
|
-
* A message is considered a duplicate if:
|
|
286
|
-
* - It has the same messageId
|
|
287
|
-
* - It is from the same sender (contactUserId)
|
|
288
|
-
* - Belongs to the same conversation (ownerUserId)
|
|
289
|
-
*
|
|
290
|
-
* This protects against scenarios where:
|
|
291
|
-
* 1. A sender successfully delivers a message to the network,
|
|
292
|
-
* 2. Their app crashes or resets before marking the message as SENT in the local DB,
|
|
293
|
-
* 3. On restart, the sender's app re-sends the message (possibly with a different seeker),
|
|
294
|
-
* 4. The receiver gets the same message twice,
|
|
295
|
-
* 5. Duplicates are prevented by matching messageId byte-for-byte.
|
|
296
|
-
*
|
|
297
|
-
* This method uses messageId (usually a 12-byte random value) for exact match detection.
|
|
298
|
-
* If a duplicate is found, it updates certain fields (e.g., content, replyTo, forwardOf)
|
|
299
|
-
* on the existing message and returns true.
|
|
300
|
-
*
|
|
301
|
-
* @param message - The incoming decrypted message object.
|
|
302
|
-
* @param ownerUserId - The userId of the message database owner (the recipient).
|
|
303
|
-
* @returns true if a duplicate message (by messageId) was found; otherwise false.
|
|
377
|
+
* Checks for duplicate incoming messages based on messageId (12-byte random).
|
|
378
|
+
* Returns true if a message with the same messageId already exists (skips storage).
|
|
304
379
|
*/
|
|
305
380
|
async handleDuplicateMessageId(message, ownerUserId) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
.where('[ownerUserId+contactUserId]')
|
|
310
|
-
.equals([ownerUserId, message.senderId])
|
|
311
|
-
.and(m => {
|
|
312
|
-
if (!m.messageId || !message.messageId)
|
|
313
|
-
return false;
|
|
314
|
-
if (m.messageId.length !== message.messageId.length)
|
|
315
|
-
return false;
|
|
316
|
-
for (let i = 0; i < m.messageId.length; i++) {
|
|
317
|
-
if (m.messageId[i] !== message.messageId[i])
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
return true;
|
|
321
|
-
})
|
|
322
|
-
.first();
|
|
323
|
-
if (existingWithMessageId) {
|
|
324
|
-
await this.db.messages.update(existingWithMessageId.id, {
|
|
325
|
-
content: message.content,
|
|
326
|
-
replyTo: message.replyTo,
|
|
327
|
-
forwardOf: message.forwardOf,
|
|
328
|
-
});
|
|
381
|
+
if (message.messageId && message.messageId.length > 0) {
|
|
382
|
+
const existing = await findMessageByMessageId(ownerUserId, message.senderId, message.messageId);
|
|
383
|
+
if (existing) {
|
|
329
384
|
return true;
|
|
330
385
|
}
|
|
331
386
|
}
|
|
332
387
|
return false;
|
|
333
388
|
}
|
|
334
389
|
async findMessageByMsgId(messageId, ownerUserId, contactUserId) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (m.messageId[i] !== messageId[i])
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
return true;
|
|
350
|
-
})
|
|
351
|
-
.first();
|
|
390
|
+
if (!contactUserId) {
|
|
391
|
+
// Without contactUserId, fall back to seeker-based lookup is not possible
|
|
392
|
+
// with messageId. Return undefined.
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
const row = await findMessageByMessageId(ownerUserId, contactUserId, messageId);
|
|
396
|
+
return row ? rowToMessage(row) : undefined;
|
|
397
|
+
}
|
|
398
|
+
async findMessageBySeeker(seeker, ownerUserId) {
|
|
399
|
+
const row = await getMessageByOwnerAndSeeker(ownerUserId, seeker);
|
|
400
|
+
return row ? rowToMessage(row) : undefined;
|
|
352
401
|
}
|
|
353
402
|
async acknowledgeMessages(seekers, userId) {
|
|
354
403
|
if (seekers.size === 0)
|
|
355
404
|
return;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
seekers.has(encodeToBase64(message.seeker)))
|
|
363
|
-
.modify({
|
|
405
|
+
// Find SENT outgoing messages, then filter by seeker match
|
|
406
|
+
const candidates = await getOutgoingSentMessagesByOwner(userId);
|
|
407
|
+
const toUpdate = candidates.filter(m => m.seeker != null && seekers.has(encodeToBase64(m.seeker)));
|
|
408
|
+
// Mark matching OUTGOING SENT messages as DELIVERED and clear encrypted data
|
|
409
|
+
for (const m of toUpdate) {
|
|
410
|
+
await updateMessageById(m.id, {
|
|
364
411
|
status: MessageStatus.DELIVERED,
|
|
365
|
-
|
|
366
|
-
|
|
412
|
+
encryptedMessage: null,
|
|
413
|
+
serializedContent: null,
|
|
414
|
+
seeker: null,
|
|
415
|
+
whenToSend: null,
|
|
367
416
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
status: MessageStatus.DELIVERED,
|
|
373
|
-
type: MessageType.KEEP_ALIVE,
|
|
374
|
-
})
|
|
375
|
-
.delete();
|
|
376
|
-
return count;
|
|
377
|
-
});
|
|
378
|
-
if (updatedCount > 0) {
|
|
417
|
+
}
|
|
418
|
+
// After marking as DELIVERED, clean up DELIVERED keep-alive messages
|
|
419
|
+
await deleteDeliveredKeepAliveMessages(userId);
|
|
420
|
+
if (toUpdate.length > 0) {
|
|
379
421
|
logger
|
|
380
422
|
.forMethod('acknowledgeMessages')
|
|
381
|
-
.info(`acknowledged ${
|
|
423
|
+
.info(`acknowledged ${toUpdate.length} messages`);
|
|
382
424
|
}
|
|
383
425
|
}
|
|
384
426
|
async sendMessage(message) {
|
|
385
427
|
const log = logger.forMethod('sendMessage');
|
|
386
428
|
log.info('queueing message', {
|
|
387
|
-
messageContent: message.content,
|
|
388
429
|
messageType: message.type,
|
|
389
|
-
messageReplyTo: message.replyTo,
|
|
390
|
-
messageForwardOf: message.forwardOf,
|
|
391
430
|
});
|
|
392
431
|
const peerId = decodeUserId(message.contactUserId);
|
|
393
432
|
if (peerId.length !== 32) {
|
|
@@ -396,37 +435,40 @@ export class MessageService {
|
|
|
396
435
|
error: 'Invalid contact userId (must be 32 bytes)',
|
|
397
436
|
};
|
|
398
437
|
}
|
|
399
|
-
//
|
|
400
|
-
|
|
438
|
+
// Look up discussion
|
|
439
|
+
const discussion = await getDiscussionByOwnerAndContact(message.ownerUserId, message.contactUserId);
|
|
440
|
+
if (!discussion) {
|
|
441
|
+
return { success: false, error: 'Discussion not found' };
|
|
442
|
+
}
|
|
443
|
+
// Generate a random messageId for deduplication (not for keep-alive)
|
|
444
|
+
const randomMessageId = message.type !== MessageType.KEEP_ALIVE
|
|
445
|
+
? crypto.getRandomValues(new Uint8Array(MESSAGE_ID_SIZE))
|
|
446
|
+
: undefined;
|
|
447
|
+
message.messageId = randomMessageId;
|
|
448
|
+
// Add message as WAITING_SESSION
|
|
401
449
|
let messageId;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
messageId = await this.db.addMessage({
|
|
414
|
-
...message,
|
|
415
|
-
status: MessageStatus.WAITING_SESSION,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
catch (error) {
|
|
419
|
-
return new Error('Failed to add message to database, got error: ' + error);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
if (error) {
|
|
423
|
-
return { success: false, error: error.message };
|
|
450
|
+
try {
|
|
451
|
+
messageId = await this.addMessageAndUpdateDiscussion({
|
|
452
|
+
...message,
|
|
453
|
+
status: MessageStatus.WAITING_SESSION,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
error: 'Failed to add message to database, got error: ' + error,
|
|
460
|
+
};
|
|
424
461
|
}
|
|
425
462
|
const queuedMessage = {
|
|
426
463
|
...message,
|
|
427
464
|
id: messageId,
|
|
428
465
|
status: MessageStatus.WAITING_SESSION,
|
|
429
466
|
};
|
|
467
|
+
/*
|
|
468
|
+
Trigger a state update to send the new message.
|
|
469
|
+
If the stateUpdate function is already running, it will be skipped.
|
|
470
|
+
*/
|
|
471
|
+
await this.refreshService?.stateUpdate();
|
|
430
472
|
return {
|
|
431
473
|
success: true,
|
|
432
474
|
message: queuedMessage,
|
|
@@ -490,11 +532,11 @@ export class MessageService {
|
|
|
490
532
|
for (const msg of retryMessages) {
|
|
491
533
|
if (!msg.id)
|
|
492
534
|
continue;
|
|
493
|
-
await
|
|
535
|
+
await updateMessageById(msg.id, {
|
|
494
536
|
status: MessageStatus.WAITING_SESSION,
|
|
495
|
-
encryptedMessage:
|
|
496
|
-
seeker:
|
|
497
|
-
whenToSend:
|
|
537
|
+
encryptedMessage: null,
|
|
538
|
+
seeker: null,
|
|
539
|
+
whenToSend: null,
|
|
498
540
|
});
|
|
499
541
|
}
|
|
500
542
|
await this.processSendQueueForContact(contactId);
|
|
@@ -517,7 +559,7 @@ export class MessageService {
|
|
|
517
559
|
}
|
|
518
560
|
this.processingContacts.add(contactUserId);
|
|
519
561
|
try {
|
|
520
|
-
const discussion = await
|
|
562
|
+
const discussion = await getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
|
|
521
563
|
const weAccepted = discussion?.weAccepted;
|
|
522
564
|
if (!discussion || !weAccepted) {
|
|
523
565
|
return {
|
|
@@ -537,13 +579,8 @@ export class MessageService {
|
|
|
537
579
|
error: new Error('Session not active or self requested'),
|
|
538
580
|
};
|
|
539
581
|
}
|
|
540
|
-
// retrieve all
|
|
541
|
-
const pendingMessages = await
|
|
542
|
-
.where('[ownerUserId+contactUserId]')
|
|
543
|
-
.equals([ownerUserId, contactUserId])
|
|
544
|
-
.and(msg => msg.direction === MessageDirection.OUTGOING &&
|
|
545
|
-
[MessageStatus.WAITING_SESSION, MessageStatus.READY].includes(msg.status))
|
|
546
|
-
.sortBy('timestamp');
|
|
582
|
+
// retrieve all messages in send queue that need to be updated for this contact
|
|
583
|
+
const pendingMessages = (await getSendQueueMessages(ownerUserId, contactUserId)).map(rowToMessage);
|
|
547
584
|
log.debug('pending messages', {
|
|
548
585
|
pendingMessages: pendingMessages.map(msg => ({
|
|
549
586
|
id: msg.id,
|
|
@@ -592,7 +629,7 @@ export class MessageService {
|
|
|
592
629
|
encryptedMessage = sendOutput.data;
|
|
593
630
|
seeker = sendOutput.seeker;
|
|
594
631
|
whenToSend = new Date();
|
|
595
|
-
await
|
|
632
|
+
await updateMessageById(msg.id, {
|
|
596
633
|
status: MessageStatus.READY,
|
|
597
634
|
encryptedMessage,
|
|
598
635
|
seeker,
|
|
@@ -622,11 +659,11 @@ export class MessageService {
|
|
|
622
659
|
continue;
|
|
623
660
|
}
|
|
624
661
|
if (!encryptedMessage || !seeker) {
|
|
625
|
-
await
|
|
662
|
+
await updateMessageById(msg.id, {
|
|
626
663
|
status: MessageStatus.WAITING_SESSION,
|
|
627
|
-
encryptedMessage:
|
|
628
|
-
seeker:
|
|
629
|
-
whenToSend:
|
|
664
|
+
encryptedMessage: null,
|
|
665
|
+
seeker: null,
|
|
666
|
+
whenToSend: null,
|
|
630
667
|
});
|
|
631
668
|
log.debug('message has no encryptedMessage or seeker, updated to WAITING_SESSION', {
|
|
632
669
|
messageId: msg.id,
|
|
@@ -646,30 +683,20 @@ export class MessageService {
|
|
|
646
683
|
seeker,
|
|
647
684
|
ciphertext: encryptedMessage,
|
|
648
685
|
});
|
|
649
|
-
// update the db
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
catch (error) {
|
|
665
|
-
log.error('failed to update message status to SENT', {
|
|
666
|
-
messageId: msg.id,
|
|
667
|
-
error,
|
|
668
|
-
});
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
return false;
|
|
672
|
-
});
|
|
686
|
+
// update the db — check latest state to avoid race conditions
|
|
687
|
+
const latestRow = await getMessageById(msg.id);
|
|
688
|
+
let sent = false;
|
|
689
|
+
if (latestRow && // ensure the discussion has not been deleted
|
|
690
|
+
latestRow.status === MessageStatus.READY // ensure the discussion has not been reset with all pending messages reset to WAITING_SESSION
|
|
691
|
+
) {
|
|
692
|
+
await updateMessageById(msg.id, {
|
|
693
|
+
status: MessageStatus.SENT,
|
|
694
|
+
encryptedMessage: null,
|
|
695
|
+
serializedContent: null,
|
|
696
|
+
whenToSend: null,
|
|
697
|
+
});
|
|
698
|
+
sent = true;
|
|
699
|
+
}
|
|
673
700
|
if (sent) {
|
|
674
701
|
sentCount++;
|
|
675
702
|
log.debug('message sent', {
|
|
@@ -698,16 +725,14 @@ export class MessageService {
|
|
|
698
725
|
messageId: msg.id,
|
|
699
726
|
error,
|
|
700
727
|
});
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
});
|
|
728
|
+
const latestRow = await getMessageById(msg.id);
|
|
729
|
+
if (latestRow && // ensure the discussion has not been deleted
|
|
730
|
+
latestRow.status === MessageStatus.READY // ensure the discussion has not been reset with all pending messages reset to WAITING_SESSION
|
|
731
|
+
) {
|
|
732
|
+
await updateMessageById(msg.id, {
|
|
733
|
+
whenToSend: new Date(Date.now() + this.config.messages.retryDelayMs),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
711
736
|
}
|
|
712
737
|
}
|
|
713
738
|
}
|
|
@@ -722,12 +747,8 @@ export class MessageService {
|
|
|
722
747
|
*/
|
|
723
748
|
async getPendingSendCount(contactUserId) {
|
|
724
749
|
const ownerUserId = this.session.userIdEncoded;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
.equals([ownerUserId, contactUserId])
|
|
728
|
-
.and(msg => msg.direction === MessageDirection.OUTGOING &&
|
|
729
|
-
[MessageStatus.WAITING_SESSION, MessageStatus.READY].includes(msg.status))
|
|
730
|
-
.count();
|
|
750
|
+
const rows = await getSendQueueMessages(ownerUserId, contactUserId);
|
|
751
|
+
return rows.length;
|
|
731
752
|
}
|
|
732
753
|
/**
|
|
733
754
|
* Send a keep-alive message without adding it to the message history.
|
|
@@ -765,34 +786,25 @@ export class MessageService {
|
|
|
765
786
|
*/
|
|
766
787
|
async getWaitingMessageCount(contactUserId) {
|
|
767
788
|
const ownerUserId = this.session.userIdEncoded;
|
|
768
|
-
return
|
|
769
|
-
.where('[ownerUserId+contactUserId+status]')
|
|
770
|
-
.equals([ownerUserId, contactUserId, MessageStatus.WAITING_SESSION])
|
|
771
|
-
.count();
|
|
789
|
+
return getWaitingCount(ownerUserId, contactUserId);
|
|
772
790
|
}
|
|
773
791
|
// Mark a message as read. Returns true if the message has been marked as read, false if it was already marked as read or doesn't exist.
|
|
774
792
|
async markAsRead(id) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
await
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (discussion.unreadCount > 0) {
|
|
792
|
-
discussion.unreadCount -= 1;
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
return true;
|
|
796
|
-
});
|
|
793
|
+
// Check current message status from DB to avoid race conditions
|
|
794
|
+
const row = await getMessageById(id);
|
|
795
|
+
if (!row || row.status !== MessageStatus.DELIVERED) {
|
|
796
|
+
// Message was already marked as read or doesn't exist
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
const message = rowToMessage(row);
|
|
800
|
+
// Update message status
|
|
801
|
+
await updateMessageById(id, { status: MessageStatus.READ });
|
|
802
|
+
// Atomically decrement discussion unread count
|
|
803
|
+
const discussion = await getDiscussionByOwnerAndContact(message.ownerUserId, message.contactUserId);
|
|
804
|
+
if (discussion) {
|
|
805
|
+
await decrementUnreadCount(discussion.id);
|
|
806
|
+
}
|
|
807
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_READ, id);
|
|
808
|
+
return true;
|
|
797
809
|
}
|
|
798
810
|
}
|