@massalabs/gossip-sdk 0.0.1 → 0.0.2-dev.20260128111120
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/dist/api/messageProtocol/index.d.ts +19 -0
- package/dist/api/messageProtocol/index.js +26 -0
- package/dist/api/messageProtocol/mock.d.ts +12 -0
- package/{src/api/messageProtocol/mock.ts → dist/api/messageProtocol/mock.js} +2 -3
- package/dist/api/messageProtocol/rest.d.ts +22 -0
- package/dist/api/messageProtocol/rest.js +161 -0
- package/dist/api/messageProtocol/types.d.ts +61 -0
- package/dist/api/messageProtocol/types.js +6 -0
- package/dist/assets/generated/wasm/README.md +281 -0
- package/dist/assets/generated/wasm/gossip_wasm.d.ts +498 -0
- package/dist/assets/generated/wasm/gossip_wasm.js +1399 -0
- package/dist/assets/generated/wasm/gossip_wasm_bg.wasm +0 -0
- package/dist/assets/generated/wasm/gossip_wasm_bg.wasm.d.ts +68 -0
- package/dist/assets/generated/wasm/package.json +15 -0
- package/dist/config/protocol.d.ts +36 -0
- package/dist/config/protocol.js +77 -0
- package/dist/config/sdk.d.ts +82 -0
- package/dist/config/sdk.js +55 -0
- package/{src/contacts.ts → dist/contacts.d.ts} +10 -94
- package/dist/contacts.js +166 -0
- package/dist/core/SdkEventEmitter.d.ts +36 -0
- package/dist/core/SdkEventEmitter.js +59 -0
- package/dist/core/SdkPolling.d.ts +35 -0
- package/dist/core/SdkPolling.js +100 -0
- package/{src/core/index.ts → dist/core/index.d.ts} +0 -2
- package/dist/core/index.js +5 -0
- package/dist/crypto/bip39.d.ts +34 -0
- package/dist/crypto/bip39.js +62 -0
- package/dist/crypto/encryption.d.ts +37 -0
- package/dist/crypto/encryption.js +46 -0
- package/dist/db.d.ts +190 -0
- package/dist/db.js +311 -0
- package/dist/gossipSdk.d.ts +274 -0
- package/dist/gossipSdk.js +690 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +77 -0
- package/dist/services/announcement.d.ts +43 -0
- package/dist/services/announcement.js +491 -0
- package/dist/services/auth.d.ts +37 -0
- package/dist/services/auth.js +76 -0
- package/dist/services/discussion.d.ts +63 -0
- package/dist/services/discussion.js +297 -0
- package/dist/services/message.d.ts +74 -0
- package/dist/services/message.js +826 -0
- package/dist/services/refresh.d.ts +41 -0
- package/dist/services/refresh.js +205 -0
- package/{src/sw.ts → dist/sw.d.ts} +1 -8
- package/dist/sw.js +10 -0
- package/dist/types/events.d.ts +80 -0
- package/dist/types/events.js +7 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/utils/base64.d.ts +10 -0
- package/dist/utils/base64.js +30 -0
- package/dist/utils/contacts.d.ts +42 -0
- package/dist/utils/contacts.js +113 -0
- package/dist/utils/discussions.d.ts +24 -0
- package/dist/utils/discussions.js +38 -0
- package/dist/utils/logs.d.ts +19 -0
- package/dist/utils/logs.js +89 -0
- package/dist/utils/messageSerialization.d.ts +64 -0
- package/dist/utils/messageSerialization.js +184 -0
- package/dist/utils/queue.d.ts +50 -0
- package/dist/utils/queue.js +110 -0
- package/dist/utils/type.d.ts +10 -0
- package/dist/utils/type.js +4 -0
- package/dist/utils/userId.d.ts +40 -0
- package/dist/utils/userId.js +90 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +112 -0
- package/dist/utils.d.ts +30 -0
- package/{src/utils.ts → dist/utils.js} +9 -19
- package/dist/wasm/encryption.d.ts +56 -0
- package/{src/wasm/encryption.ts → dist/wasm/encryption.js} +22 -51
- package/dist/wasm/index.d.ts +10 -0
- package/{src/wasm/index.ts → dist/wasm/index.js} +1 -8
- package/dist/wasm/loader.d.ts +21 -0
- package/dist/wasm/loader.js +103 -0
- package/dist/wasm/session.d.ts +85 -0
- package/dist/wasm/session.js +226 -0
- package/dist/wasm/userKeys.d.ts +17 -0
- package/{src/wasm/userKeys.ts → dist/wasm/userKeys.js} +6 -13
- package/package.json +5 -1
- package/src/api/messageProtocol/index.ts +0 -53
- package/src/api/messageProtocol/rest.ts +0 -209
- package/src/api/messageProtocol/types.ts +0 -70
- package/src/config/protocol.ts +0 -97
- package/src/config/sdk.ts +0 -131
- package/src/core/SdkEventEmitter.ts +0 -91
- package/src/core/SdkPolling.ts +0 -134
- package/src/crypto/bip39.ts +0 -84
- package/src/crypto/encryption.ts +0 -77
- package/src/db.ts +0 -465
- package/src/gossipSdk.ts +0 -994
- package/src/index.ts +0 -211
- package/src/services/announcement.ts +0 -653
- package/src/services/auth.ts +0 -95
- package/src/services/discussion.ts +0 -380
- package/src/services/message.ts +0 -1055
- package/src/services/refresh.ts +0 -234
- package/src/types/events.ts +0 -108
- package/src/types.ts +0 -70
- package/src/utils/base64.ts +0 -39
- package/src/utils/contacts.ts +0 -161
- package/src/utils/discussions.ts +0 -55
- package/src/utils/logs.ts +0 -86
- package/src/utils/messageSerialization.ts +0 -257
- package/src/utils/queue.ts +0 -106
- package/src/utils/type.ts +0 -7
- package/src/utils/userId.ts +0 -114
- package/src/utils/validation.ts +0 -144
- package/src/wasm/loader.ts +0 -123
- package/src/wasm/session.ts +0 -276
- package/test/config/protocol.spec.ts +0 -31
- package/test/config/sdk.spec.ts +0 -163
- package/test/db/helpers.spec.ts +0 -142
- package/test/db/operations.spec.ts +0 -128
- package/test/db/states.spec.ts +0 -535
- package/test/integration/discussion-flow.spec.ts +0 -422
- package/test/integration/messaging-flow.spec.ts +0 -708
- package/test/integration/sdk-lifecycle.spec.ts +0 -325
- package/test/mocks/index.ts +0 -9
- package/test/mocks/mockMessageProtocol.ts +0 -100
- package/test/services/auth.spec.ts +0 -311
- package/test/services/discussion.spec.ts +0 -279
- package/test/services/message-deduplication.spec.ts +0 -299
- package/test/services/message-startup.spec.ts +0 -331
- package/test/services/message.spec.ts +0 -817
- package/test/services/refresh.spec.ts +0 -199
- package/test/services/session-status.spec.ts +0 -349
- package/test/session/wasm.spec.ts +0 -227
- package/test/setup.ts +0 -52
- package/test/utils/contacts.spec.ts +0 -156
- package/test/utils/discussions.spec.ts +0 -66
- package/test/utils/queue.spec.ts +0 -52
- package/test/utils/serialization.spec.ts +0 -120
- package/test/utils/userId.spec.ts +0 -120
- package/test/utils/validation.spec.ts +0 -223
- package/test/utils.ts +0 -212
- package/tsconfig.json +0 -26
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -28
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discussion Service
|
|
3
|
+
*
|
|
4
|
+
* Class-based service for initializing, accepting, and managing discussions.
|
|
5
|
+
*/
|
|
6
|
+
import { DiscussionStatus, MessageDirection, MessageStatus, DiscussionDirection, } from '../db';
|
|
7
|
+
import { UserPublicKeys, SessionStatus, } from '../assets/generated/wasm/gossip_wasm';
|
|
8
|
+
import { EstablishSessionError } from './announcement';
|
|
9
|
+
import { sessionStatusToString } from '../wasm/session';
|
|
10
|
+
import { decodeUserId } from '../utils/userId';
|
|
11
|
+
import { Logger } from '../utils/logs';
|
|
12
|
+
const logger = new Logger('DiscussionService');
|
|
13
|
+
/**
|
|
14
|
+
* Service for managing discussions between users.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const discussionService = new DiscussionService(db, announcementService, session);
|
|
19
|
+
*
|
|
20
|
+
* // Initialize a new discussion
|
|
21
|
+
* const result = await discussionService.initialize(contact, 'Hello!');
|
|
22
|
+
*
|
|
23
|
+
* // Accept a discussion request
|
|
24
|
+
* await discussionService.accept(discussion);
|
|
25
|
+
*
|
|
26
|
+
* // Renew a broken discussion
|
|
27
|
+
* await discussionService.renew(contactUserId);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class DiscussionService {
|
|
31
|
+
constructor(db, announcementService, session, events = {}) {
|
|
32
|
+
Object.defineProperty(this, "db", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: void 0
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "announcementService", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "session", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: void 0
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(this, "events", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: void 0
|
|
55
|
+
});
|
|
56
|
+
this.db = db;
|
|
57
|
+
this.announcementService = announcementService;
|
|
58
|
+
this.session = session;
|
|
59
|
+
this.events = events;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Initialize a discussion with a contact using SessionManager
|
|
63
|
+
* @param contact - The contact to start a discussion with
|
|
64
|
+
* @param message - Optional message to include in the announcement
|
|
65
|
+
* @returns The discussion ID and the created announcement
|
|
66
|
+
*/
|
|
67
|
+
async initialize(contact, message) {
|
|
68
|
+
const log = logger.forMethod('initialize');
|
|
69
|
+
try {
|
|
70
|
+
const userId = this.session.userIdEncoded;
|
|
71
|
+
// Encode message as UTF-8 if provided
|
|
72
|
+
const userData = message
|
|
73
|
+
? new TextEncoder().encode(message)
|
|
74
|
+
: new Uint8Array(0);
|
|
75
|
+
log.info(`${userId} is establishing session with contact ${contact.name}`);
|
|
76
|
+
const result = await this.announcementService.establishSession(UserPublicKeys.from_bytes(contact.publicKeys), userData);
|
|
77
|
+
let status = DiscussionStatus.PENDING;
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
log.error(`Failed to establish session with contact ${contact.name}, got error: ${result.error}`);
|
|
80
|
+
// if the error is due to the session manager failed to establish outgoing session, throw the error
|
|
81
|
+
if (result.error && result.error.includes(EstablishSessionError))
|
|
82
|
+
throw new Error(EstablishSessionError);
|
|
83
|
+
status = DiscussionStatus.SEND_FAILED;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
log.info(`session established with contact and announcement sent: ${result.announcement.length}... bytes`);
|
|
87
|
+
}
|
|
88
|
+
// Parse announcement message to extract only the actual message content.
|
|
89
|
+
// The message parameter may be JSON format: {"u":"username","m":"message"}
|
|
90
|
+
// We only want to store the "m" (message) field, not the full JSON.
|
|
91
|
+
let parsedAnnouncementMessage;
|
|
92
|
+
if (message) {
|
|
93
|
+
if (message.startsWith('{')) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(message);
|
|
96
|
+
parsedAnnouncementMessage = parsed.m?.trim() || undefined;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Invalid JSON, treat as plain text
|
|
100
|
+
parsedAnnouncementMessage = message;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
parsedAnnouncementMessage = message;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Persist discussion immediately with the announcement for reliable retry
|
|
108
|
+
const discussionId = await this.db.discussions.add({
|
|
109
|
+
ownerUserId: userId,
|
|
110
|
+
contactUserId: contact.userId,
|
|
111
|
+
direction: DiscussionDirection.INITIATED,
|
|
112
|
+
status: status,
|
|
113
|
+
nextSeeker: undefined,
|
|
114
|
+
initiationAnnouncement: result.announcement,
|
|
115
|
+
announcementMessage: parsedAnnouncementMessage,
|
|
116
|
+
unreadCount: 0,
|
|
117
|
+
createdAt: new Date(),
|
|
118
|
+
updatedAt: new Date(),
|
|
119
|
+
});
|
|
120
|
+
log.info(`discussion created with id: ${discussionId}`);
|
|
121
|
+
// Emit status change event
|
|
122
|
+
const discussion = await this.db.discussions.get(discussionId);
|
|
123
|
+
if (discussion) {
|
|
124
|
+
this.events.onDiscussionStatusChanged?.(discussion);
|
|
125
|
+
}
|
|
126
|
+
return { discussionId, announcement: result.announcement };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
log.error(`Failed to initialize discussion, error: ${error}`);
|
|
130
|
+
throw new Error('Discussion initialization failed, error: ' + error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Accept a discussion request from a contact using SessionManager
|
|
135
|
+
* @param discussion - The discussion to accept
|
|
136
|
+
*/
|
|
137
|
+
async accept(discussion) {
|
|
138
|
+
const log = logger.forMethod('accept');
|
|
139
|
+
try {
|
|
140
|
+
const contact = await this.db.getContactByOwnerAndUserId(discussion.ownerUserId, discussion.contactUserId);
|
|
141
|
+
if (!contact)
|
|
142
|
+
throw new Error(`Contact ${discussion.contactUserId} not found for ownerUserId ${discussion.ownerUserId}`);
|
|
143
|
+
const result = await this.announcementService.establishSession(UserPublicKeys.from_bytes(contact.publicKeys));
|
|
144
|
+
let status = DiscussionStatus.ACTIVE;
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
log.error(`Failed to establish session with contact ${contact.name}, got error: ${result.error}`);
|
|
147
|
+
// if the error is due to the session manager failed to establish outgoing session, throw the error
|
|
148
|
+
if (result.error && result.error.includes(EstablishSessionError))
|
|
149
|
+
throw new Error(EstablishSessionError);
|
|
150
|
+
status = DiscussionStatus.SEND_FAILED;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
log.info(`session established with contact and announcement sent: ${result.announcement.length}... bytes`);
|
|
154
|
+
}
|
|
155
|
+
// update discussion status
|
|
156
|
+
await this.db.discussions.update(discussion.id, {
|
|
157
|
+
status: status,
|
|
158
|
+
initiationAnnouncement: result.announcement,
|
|
159
|
+
updatedAt: new Date(),
|
|
160
|
+
});
|
|
161
|
+
log.info(`discussion updated in db with status: ${status}`);
|
|
162
|
+
// Emit status change event
|
|
163
|
+
const updatedDiscussion = await this.db.discussions.get(discussion.id);
|
|
164
|
+
if (updatedDiscussion) {
|
|
165
|
+
this.events.onDiscussionStatusChanged?.(updatedDiscussion);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
log.error(`Failed to accept pending discussion, error: ${error}`);
|
|
171
|
+
throw new Error('Failed to accept pending discussion, error: ' + error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Renew a discussion by resetting sent outgoing messages and sending a new announcement.
|
|
176
|
+
* @param contactUserId - The user ID of the contact whose discussion should be renewed.
|
|
177
|
+
*/
|
|
178
|
+
async renew(contactUserId) {
|
|
179
|
+
const log = logger.forMethod('renew');
|
|
180
|
+
const ownerUserId = this.session.userIdEncoded;
|
|
181
|
+
const contact = await this.db.getContactByOwnerAndUserId(ownerUserId, contactUserId);
|
|
182
|
+
if (!contact)
|
|
183
|
+
throw new Error('Contact not found');
|
|
184
|
+
const existingDiscussion = await this.db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
|
|
185
|
+
if (!existingDiscussion)
|
|
186
|
+
throw new Error('Discussion with contact ' + contact.name + ' not found');
|
|
187
|
+
log.info(`renewing discussion between ${ownerUserId} and ${contactUserId}`);
|
|
188
|
+
// reset session by creating and sending a new announcement
|
|
189
|
+
const result = await this.announcementService.establishSession(UserPublicKeys.from_bytes(contact.publicKeys));
|
|
190
|
+
// if the error is due to the session manager failed to establish outgoing session, throw the error
|
|
191
|
+
if (result.error && result.error.includes(EstablishSessionError))
|
|
192
|
+
throw new Error(EstablishSessionError);
|
|
193
|
+
// get the new session status
|
|
194
|
+
const sessionStatus = this.session.peerSessionStatus(decodeUserId(contactUserId));
|
|
195
|
+
log.info(`session status for discussion between ${ownerUserId} and ${contactUserId} after reinitiation is ${sessionStatusToString(sessionStatus)}`);
|
|
196
|
+
// Determine discussion status based on send result and session state:
|
|
197
|
+
// - SEND_FAILED: announcement couldn't be sent
|
|
198
|
+
// - ACTIVE: session fully established (peer responded)
|
|
199
|
+
// - RECONNECTING: true renewal, waiting for peer's response
|
|
200
|
+
// - PENDING: first contact retry, waiting for peer's response
|
|
201
|
+
let status;
|
|
202
|
+
if (!result.success) {
|
|
203
|
+
status = DiscussionStatus.SEND_FAILED;
|
|
204
|
+
}
|
|
205
|
+
else if (sessionStatus === SessionStatus.Active) {
|
|
206
|
+
// Session fully established (peer already responded)
|
|
207
|
+
status = DiscussionStatus.ACTIVE;
|
|
208
|
+
}
|
|
209
|
+
else if (existingDiscussion.status === DiscussionStatus.ACTIVE) {
|
|
210
|
+
// True renewal: had working session before, now recovering
|
|
211
|
+
status = DiscussionStatus.RECONNECTING;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// First contact retry: never had working session
|
|
215
|
+
status = DiscussionStatus.PENDING;
|
|
216
|
+
}
|
|
217
|
+
await this.db.transaction('rw', [this.db.discussions, this.db.messages], async () => {
|
|
218
|
+
await this.db.discussions.update(existingDiscussion.id, {
|
|
219
|
+
status: status,
|
|
220
|
+
direction: DiscussionDirection.INITIATED,
|
|
221
|
+
initiationAnnouncement: result.announcement,
|
|
222
|
+
updatedAt: new Date(),
|
|
223
|
+
});
|
|
224
|
+
log.info(`discussion updated with status: ${status}`);
|
|
225
|
+
/* Reset outgoing messages that haven't been acknowledged by the peer.
|
|
226
|
+
* When session is renewed, messages encrypted with the old session
|
|
227
|
+
* may not be decryptable by the peer with the new session.
|
|
228
|
+
*
|
|
229
|
+
* Messages to reset (not acknowledged):
|
|
230
|
+
* - SENDING: Was in progress, needs re-encryption with new session
|
|
231
|
+
* - FAILED: Previous send failed, needs re-encryption
|
|
232
|
+
* - SENT: On network but not acknowledged - peer may not have received
|
|
233
|
+
*
|
|
234
|
+
* Messages to keep (acknowledged by peer):
|
|
235
|
+
* - DELIVERED: Peer confirmed receipt
|
|
236
|
+
* - READ: Peer read it
|
|
237
|
+
*/
|
|
238
|
+
const messagesToReset = await this.db.messages
|
|
239
|
+
.where('[ownerUserId+contactUserId]')
|
|
240
|
+
.equals([ownerUserId, contactUserId])
|
|
241
|
+
.and(message => message.direction === MessageDirection.OUTGOING &&
|
|
242
|
+
(message.status === MessageStatus.SENDING ||
|
|
243
|
+
message.status === MessageStatus.FAILED ||
|
|
244
|
+
message.status === MessageStatus.SENT))
|
|
245
|
+
.modify({
|
|
246
|
+
status: MessageStatus.WAITING_SESSION,
|
|
247
|
+
encryptedMessage: undefined,
|
|
248
|
+
seeker: undefined,
|
|
249
|
+
});
|
|
250
|
+
log.info(`reset ${messagesToReset} messages to WAITING_SESSION`);
|
|
251
|
+
});
|
|
252
|
+
// Emit events after transaction completes
|
|
253
|
+
const updatedDiscussion = await this.db.discussions.get(existingDiscussion.id);
|
|
254
|
+
if (updatedDiscussion) {
|
|
255
|
+
this.events.onDiscussionStatusChanged?.(updatedDiscussion);
|
|
256
|
+
this.events.onSessionRenewed?.(updatedDiscussion);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if new messages can be sent to session manager for encryption.
|
|
261
|
+
* Returns false if the discussion is broken or if there are failed messages
|
|
262
|
+
* that have not been encrypted.
|
|
263
|
+
*
|
|
264
|
+
* @param ownerUserId - The owner user ID
|
|
265
|
+
* @param contactUserId - The contact user ID
|
|
266
|
+
* @returns true if discussion is in stable state for sending messages
|
|
267
|
+
*/
|
|
268
|
+
async isStableState(ownerUserId, contactUserId) {
|
|
269
|
+
const log = logger.forMethod('isStableState');
|
|
270
|
+
const discussion = await this.db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
|
|
271
|
+
if (!discussion)
|
|
272
|
+
throw new Error('Discussion not found');
|
|
273
|
+
if (discussion.status === DiscussionStatus.BROKEN) {
|
|
274
|
+
log.info(`Discussion with ownerUserId ${ownerUserId} and contactUserId ${contactUserId} is broken`);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const messages = await this.db.messages
|
|
278
|
+
.where('[ownerUserId+contactUserId+direction]')
|
|
279
|
+
.equals([
|
|
280
|
+
discussion.ownerUserId,
|
|
281
|
+
discussion.contactUserId,
|
|
282
|
+
MessageDirection.OUTGOING,
|
|
283
|
+
])
|
|
284
|
+
.sortBy('id');
|
|
285
|
+
/* If the discussion has been broken, all non delivered messages have been marked as failed and
|
|
286
|
+
their encryptedMessage field has been deleted.
|
|
287
|
+
If there are some unencrypted unsent messages in the conversation, the discussion is not stable
|
|
288
|
+
i.e. we should not encrypt any new message via session manager before these messages are not resent */
|
|
289
|
+
if (messages.length > 0 &&
|
|
290
|
+
!messages[messages.length - 1].encryptedMessage &&
|
|
291
|
+
messages[messages.length - 1].status === MessageStatus.FAILED) {
|
|
292
|
+
log.info(`Discussion with ownerUserId ${ownerUserId} and contactUserId ${contactUserId} has no encryptedMessage failed messages`);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Reception Service
|
|
3
|
+
*
|
|
4
|
+
* Handles fetching encrypted messages from the protocol and decrypting them.
|
|
5
|
+
* This service works both in host app contexts and SDK/automation context.
|
|
6
|
+
*/
|
|
7
|
+
import { type Message, type GossipDatabase } from '../db';
|
|
8
|
+
import { IMessageProtocol } from '../api/messageProtocol';
|
|
9
|
+
import { SessionModule } from '../wasm';
|
|
10
|
+
import { DiscussionService } from './discussion';
|
|
11
|
+
import { GossipSdkEvents } from '../types/events';
|
|
12
|
+
import { SdkConfig } from '../config/sdk';
|
|
13
|
+
export interface MessageResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
newMessagesCount: number;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SendMessageResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
message?: Message;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class MessageService {
|
|
24
|
+
private db;
|
|
25
|
+
private messageProtocol;
|
|
26
|
+
private session;
|
|
27
|
+
private discussionService;
|
|
28
|
+
private events;
|
|
29
|
+
private config;
|
|
30
|
+
constructor(db: GossipDatabase, messageProtocol: IMessageProtocol, session: SessionModule, discussionService: DiscussionService, events?: GossipSdkEvents, config?: SdkConfig);
|
|
31
|
+
fetchMessages(): Promise<MessageResult>;
|
|
32
|
+
private decryptMessages;
|
|
33
|
+
private storeDecryptedMessages;
|
|
34
|
+
findMessageBySeeker(seeker: Uint8Array, ownerUserId: string): Promise<Message | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Check if a message is a duplicate based on content and timestamp.
|
|
37
|
+
*
|
|
38
|
+
* A message is considered duplicate if:
|
|
39
|
+
* - Same sender (contactUserId)
|
|
40
|
+
* - Same content
|
|
41
|
+
* - Incoming direction
|
|
42
|
+
* - Timestamp within deduplication window (default 30 seconds)
|
|
43
|
+
*
|
|
44
|
+
* This handles the edge case where:
|
|
45
|
+
* 1. Sender sends message successfully to network
|
|
46
|
+
* 2. Sender app crashes before updating DB status to SENT
|
|
47
|
+
* 3. On restart, message is reset to WAITING_SESSION and re-sent
|
|
48
|
+
* 4. Receiver gets the same message twice with different seekers
|
|
49
|
+
*
|
|
50
|
+
* @param ownerUserId - The owner's user ID
|
|
51
|
+
* @param contactUserId - The sender's user ID
|
|
52
|
+
* @param content - The message content
|
|
53
|
+
* @param timestamp - The message timestamp
|
|
54
|
+
* @returns true if a duplicate exists
|
|
55
|
+
*/
|
|
56
|
+
private isDuplicateMessage;
|
|
57
|
+
private acknowledgeMessages;
|
|
58
|
+
sendMessage(message: Message): Promise<SendMessageResult>;
|
|
59
|
+
private serializeMessage;
|
|
60
|
+
resendMessages(messages: Map<string, Message[]>): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Process messages that are waiting for an active session.
|
|
63
|
+
* Called when a session becomes Active to send queued messages.
|
|
64
|
+
* Per spec: when session becomes Active, encrypt and send WAITING_SESSION messages.
|
|
65
|
+
*
|
|
66
|
+
* @param contactUserId - The contact whose session became active
|
|
67
|
+
* @returns Number of messages successfully sent
|
|
68
|
+
*/
|
|
69
|
+
processWaitingMessages(contactUserId: string): Promise<number>;
|
|
70
|
+
/**
|
|
71
|
+
* Get count of messages waiting for session with a specific contact.
|
|
72
|
+
*/
|
|
73
|
+
getWaitingMessageCount(contactUserId: string): Promise<number>;
|
|
74
|
+
}
|