@massalabs/gossip-sdk 0.0.2-dev.20260331143013 → 0.0.2-dev.20260401161307
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.
|
@@ -17,10 +17,14 @@ export declare enum SdkEventType {
|
|
|
17
17
|
SEEKERS_UPDATED = "seekersUpdated",
|
|
18
18
|
SESSION_STATUS_CHANGED = "sessionStatusChanged",
|
|
19
19
|
DISCUSSION_UPDATED = "discussionUpdated",
|
|
20
|
+
WRITE_FAILED = "writeFailed",
|
|
21
|
+
MESSAGE_OPTIMISTIC = "messageOptimistic",
|
|
20
22
|
ERROR = "error"
|
|
21
23
|
}
|
|
22
24
|
export interface SdkEventHandlers {
|
|
23
|
-
[SdkEventType.MESSAGE_RECEIVED]: (message: Message
|
|
25
|
+
[SdkEventType.MESSAGE_RECEIVED]: (message: Omit<Message, 'id'> & {
|
|
26
|
+
id?: number;
|
|
27
|
+
}) => void;
|
|
24
28
|
[SdkEventType.MESSAGE_SENT]: (message: Message) => void;
|
|
25
29
|
[SdkEventType.MESSAGE_READ]: (messageId: number) => void;
|
|
26
30
|
[SdkEventType.MESSAGE_FAILED]: (message: Message, error: Error) => void;
|
|
@@ -31,6 +35,8 @@ export interface SdkEventHandlers {
|
|
|
31
35
|
[SdkEventType.SEEKERS_UPDATED]: (seekers: Uint8Array[]) => void;
|
|
32
36
|
[SdkEventType.SESSION_STATUS_CHANGED]: (contactUserId: string, status: SessionStatus) => void;
|
|
33
37
|
[SdkEventType.DISCUSSION_UPDATED]: (contactUserId: string) => void;
|
|
38
|
+
[SdkEventType.WRITE_FAILED]: (messageId: Uint8Array | undefined, entityType: 'message' | 'discussion' | 'contact', error: Error) => void;
|
|
39
|
+
[SdkEventType.MESSAGE_OPTIMISTIC]: (message: Message) => void;
|
|
34
40
|
[SdkEventType.ERROR]: (error: Error, context: string) => void;
|
|
35
41
|
}
|
|
36
42
|
export declare class SdkEventEmitter {
|
|
@@ -19,6 +19,8 @@ export var SdkEventType;
|
|
|
19
19
|
SdkEventType["SEEKERS_UPDATED"] = "seekersUpdated";
|
|
20
20
|
SdkEventType["SESSION_STATUS_CHANGED"] = "sessionStatusChanged";
|
|
21
21
|
SdkEventType["DISCUSSION_UPDATED"] = "discussionUpdated";
|
|
22
|
+
SdkEventType["WRITE_FAILED"] = "writeFailed";
|
|
23
|
+
SdkEventType["MESSAGE_OPTIMISTIC"] = "messageOptimistic";
|
|
22
24
|
SdkEventType["ERROR"] = "error";
|
|
23
25
|
})(SdkEventType || (SdkEventType = {}));
|
|
24
26
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -42,6 +44,8 @@ export class SdkEventEmitter {
|
|
|
42
44
|
[SdkEventType.SEEKERS_UPDATED]: new Set(),
|
|
43
45
|
[SdkEventType.SESSION_STATUS_CHANGED]: new Set(),
|
|
44
46
|
[SdkEventType.DISCUSSION_UPDATED]: new Set(),
|
|
47
|
+
[SdkEventType.WRITE_FAILED]: new Set(),
|
|
48
|
+
[SdkEventType.MESSAGE_OPTIMISTIC]: new Set(),
|
|
45
49
|
[SdkEventType.ERROR]: new Set(),
|
|
46
50
|
}
|
|
47
51
|
});
|
|
@@ -273,8 +273,10 @@ export class DiscussionService {
|
|
|
273
273
|
*/
|
|
274
274
|
async start(contact, payload) {
|
|
275
275
|
const result = await this.initialize(contact, payload);
|
|
276
|
-
if (result.success)
|
|
276
|
+
if (result.success) {
|
|
277
277
|
await this.refreshService?.stateUpdate();
|
|
278
|
+
this.eventEmitter.emit(SdkEventType.DISCUSSION_UPDATED, contact.userId);
|
|
279
|
+
}
|
|
278
280
|
return result;
|
|
279
281
|
}
|
|
280
282
|
/**
|
|
@@ -45,6 +45,8 @@ export declare class MessageService {
|
|
|
45
45
|
private processingContacts;
|
|
46
46
|
private isFetchingMessages;
|
|
47
47
|
private queries;
|
|
48
|
+
/** Emit MESSAGE_RECEIVED with a Message that may not have a DB id yet */
|
|
49
|
+
private emitMessageReceived;
|
|
48
50
|
constructor(messageProtocol: IMessageProtocol, session: SessionModule, eventEmitter: SdkEventEmitter, config: SdkConfig | undefined, queries: Queries);
|
|
49
51
|
setRefreshService(refreshService: RefreshService): void;
|
|
50
52
|
setQueueManager(queueManager: QueueManager): void;
|
|
@@ -89,8 +91,15 @@ export declare class MessageService {
|
|
|
89
91
|
getVisibleMessages(contactUserId: string): Promise<Message[]>;
|
|
90
92
|
/** Get all non-deleted reaction messages for a contact. */
|
|
91
93
|
getReactions(contactUserId: string): Promise<Message[]>;
|
|
92
|
-
/** Send a message
|
|
94
|
+
/** Send a message and await the full DB write + queue pipeline. */
|
|
93
95
|
send(message: Omit<Message, 'id'>): Promise<SendMessageResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Optimistic send: generates messageId, emits MESSAGE_OPTIMISTIC immediately,
|
|
98
|
+
* and persists in the background. Returns synchronously.
|
|
99
|
+
*/
|
|
100
|
+
send(message: Omit<Message, 'id'>, options: {
|
|
101
|
+
optimistic: true;
|
|
102
|
+
}): SendMessageResult;
|
|
94
103
|
/**
|
|
95
104
|
* Send a text message (simplified).
|
|
96
105
|
* Builds the Message internally, sends it via queue, and triggers state update.
|
package/dist/services/message.js
CHANGED
|
@@ -143,6 +143,10 @@ export function rowToMessage(row) {
|
|
|
143
143
|
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
|
144
144
|
const logger = new Logger('MessageService');
|
|
145
145
|
export class MessageService {
|
|
146
|
+
/** Emit MESSAGE_RECEIVED with a Message that may not have a DB id yet */
|
|
147
|
+
emitMessageReceived(message) {
|
|
148
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_RECEIVED, message);
|
|
149
|
+
}
|
|
146
150
|
constructor(messageProtocol, session, eventEmitter, config = defaultSdkConfig, queries) {
|
|
147
151
|
Object.defineProperty(this, "messageProtocol", {
|
|
148
152
|
enumerable: true,
|
|
@@ -400,11 +404,26 @@ export class MessageService {
|
|
|
400
404
|
});
|
|
401
405
|
continue;
|
|
402
406
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
if (target.type === MessageType.REACTION) {
|
|
408
|
+
// Reaction delete: hard-delete the row, not "[Message deleted]"
|
|
409
|
+
await this.queries.messages.deleteById(target.id);
|
|
410
|
+
this.emitMessageReceived({
|
|
411
|
+
...target,
|
|
412
|
+
type: MessageType.DELETED,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Regular message delete: mark as deleted
|
|
417
|
+
this.emitMessageReceived({
|
|
418
|
+
...target,
|
|
419
|
+
content: '[Message deleted]',
|
|
420
|
+
type: MessageType.DELETED,
|
|
421
|
+
});
|
|
422
|
+
await this.queries.messages.updateById(target.id, {
|
|
423
|
+
content: '[Message deleted]',
|
|
424
|
+
type: MessageType.DELETED,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
408
427
|
continue;
|
|
409
428
|
}
|
|
410
429
|
// Handle edit control messages by updating the referenced message in-place
|
|
@@ -420,6 +439,12 @@ export class MessageService {
|
|
|
420
439
|
...(target.metadata ?? {}),
|
|
421
440
|
edited: true,
|
|
422
441
|
};
|
|
442
|
+
// Emit before DB write so UI updates instantly
|
|
443
|
+
this.emitMessageReceived({
|
|
444
|
+
...target,
|
|
445
|
+
content: message.content,
|
|
446
|
+
metadata: mergedMetadata,
|
|
447
|
+
});
|
|
423
448
|
await this.queries.messages.updateById(target.id, {
|
|
424
449
|
content: message.content,
|
|
425
450
|
metadata: serializeMetadata(mergedMetadata),
|
|
@@ -452,6 +477,18 @@ export class MessageService {
|
|
|
452
477
|
});
|
|
453
478
|
continue;
|
|
454
479
|
}
|
|
480
|
+
// Emit before DB write so UI updates instantly
|
|
481
|
+
this.emitMessageReceived({
|
|
482
|
+
messageId: message.messageId,
|
|
483
|
+
ownerUserId,
|
|
484
|
+
contactUserId: discussion.contactUserId,
|
|
485
|
+
content: message.content,
|
|
486
|
+
type: MessageType.REACTION,
|
|
487
|
+
direction: MessageDirection.INCOMING,
|
|
488
|
+
status: MessageStatus.DELIVERED,
|
|
489
|
+
timestamp: message.sentAt,
|
|
490
|
+
reactionOf: message.reactionOf,
|
|
491
|
+
});
|
|
455
492
|
const id = await this.queries.messages.insert({
|
|
456
493
|
messageId: message.messageId,
|
|
457
494
|
ownerUserId,
|
|
@@ -493,6 +530,20 @@ export class MessageService {
|
|
|
493
530
|
});
|
|
494
531
|
}
|
|
495
532
|
}
|
|
533
|
+
const incomingMsg = {
|
|
534
|
+
messageId: message.messageId,
|
|
535
|
+
ownerUserId,
|
|
536
|
+
contactUserId: discussion.contactUserId,
|
|
537
|
+
content: message.content,
|
|
538
|
+
type: message.type,
|
|
539
|
+
direction: MessageDirection.INCOMING,
|
|
540
|
+
status: MessageStatus.DELIVERED,
|
|
541
|
+
timestamp: message.sentAt,
|
|
542
|
+
replyTo: message.replyTo,
|
|
543
|
+
forwardOf: message.forwardOf,
|
|
544
|
+
};
|
|
545
|
+
// Emit before DB write — UI shows message instantly
|
|
546
|
+
this.emitMessageReceived(incomingMsg);
|
|
496
547
|
const id = await this.queries.messages.insert({
|
|
497
548
|
messageId: message.messageId,
|
|
498
549
|
ownerUserId,
|
|
@@ -517,11 +568,8 @@ export class MessageService {
|
|
|
517
568
|
});
|
|
518
569
|
await this.queries.discussions.incrementUnreadCount(discussion.id);
|
|
519
570
|
storedIds.push(id);
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
if (row) {
|
|
523
|
-
this.eventEmitter.emit(SdkEventType.MESSAGE_RECEIVED, rowToMessage(row));
|
|
524
|
-
}
|
|
571
|
+
// Re-emit with DB id so the store patches the optimistic message
|
|
572
|
+
this.emitMessageReceived({ ...incomingMsg, id });
|
|
525
573
|
}
|
|
526
574
|
return storedIds;
|
|
527
575
|
}
|
|
@@ -591,11 +639,14 @@ export class MessageService {
|
|
|
591
639
|
return { success: false, error: 'Discussion not found' };
|
|
592
640
|
}
|
|
593
641
|
// Generate a random messageId for deduplication (not for keep-alive or retention policy)
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
642
|
+
// Skip if already provided (e.g., from optimistic send)
|
|
643
|
+
if (!message.messageId) {
|
|
644
|
+
const randomMessageId = message.type !== MessageType.KEEP_ALIVE &&
|
|
645
|
+
message.type !== MessageType.RETENTION_POLICY
|
|
646
|
+
? crypto.getRandomValues(new Uint8Array(MESSAGE_ID_SIZE))
|
|
647
|
+
: undefined;
|
|
648
|
+
message.messageId = randomMessageId;
|
|
649
|
+
}
|
|
599
650
|
// Add message as WAITING_SESSION
|
|
600
651
|
let messageId;
|
|
601
652
|
try {
|
|
@@ -986,12 +1037,42 @@ export class MessageService {
|
|
|
986
1037
|
const rows = await this.queries.messages.getReactionsByOwnerAndContact(this.session.userIdEncoded, contactUserId);
|
|
987
1038
|
return rows.map(rowToMessage);
|
|
988
1039
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1040
|
+
send(message, options) {
|
|
1041
|
+
if (!options?.optimistic) {
|
|
1042
|
+
if (this.queueManager) {
|
|
1043
|
+
return this.queueManager.enqueue(message.contactUserId, () => this.sendMessage(message));
|
|
1044
|
+
}
|
|
1045
|
+
return this.sendMessage(message);
|
|
1046
|
+
}
|
|
1047
|
+
const log = logger.forMethod('send:optimistic');
|
|
1048
|
+
const peerId = decodeUserId(message.contactUserId);
|
|
1049
|
+
if (peerId.length !== 32) {
|
|
1050
|
+
return {
|
|
1051
|
+
success: false,
|
|
1052
|
+
error: 'Invalid contact userId (must be 32 bytes)',
|
|
1053
|
+
};
|
|
993
1054
|
}
|
|
994
|
-
|
|
1055
|
+
const messageId = message.type !== MessageType.KEEP_ALIVE &&
|
|
1056
|
+
message.type !== MessageType.RETENTION_POLICY
|
|
1057
|
+
? crypto.getRandomValues(new Uint8Array(MESSAGE_ID_SIZE))
|
|
1058
|
+
: undefined;
|
|
1059
|
+
const optimisticMessage = {
|
|
1060
|
+
...message,
|
|
1061
|
+
messageId,
|
|
1062
|
+
status: MessageStatus.WAITING_SESSION,
|
|
1063
|
+
};
|
|
1064
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_OPTIMISTIC, optimisticMessage);
|
|
1065
|
+
log.info('optimistic send', { messageType: message.type });
|
|
1066
|
+
// Persist in background (non-optimistic path)
|
|
1067
|
+
this.send({ ...message, messageId }).then(result => {
|
|
1068
|
+
if (!result.success) {
|
|
1069
|
+
this.eventEmitter.emit(SdkEventType.WRITE_FAILED, messageId, 'message', new Error(result.error ?? 'Unknown error'));
|
|
1070
|
+
}
|
|
1071
|
+
}, error => {
|
|
1072
|
+
log.error('optimistic send failed', { error });
|
|
1073
|
+
this.eventEmitter.emit(SdkEventType.WRITE_FAILED, messageId, 'message', error instanceof Error ? error : new Error(String(error)));
|
|
1074
|
+
});
|
|
1075
|
+
return { success: true, message: optimisticMessage };
|
|
995
1076
|
}
|
|
996
1077
|
/**
|
|
997
1078
|
* Send a text message (simplified).
|
package/package.json
CHANGED