@massalabs/gossip-sdk 0.0.2-dev.20260331143013 → 0.0.2-dev.20260401160204

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) => void;
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, queued via QueueManager if available */
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.
@@ -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
- await this.queries.messages.updateById(target.id, {
404
- content: '[Message deleted]',
405
- type: MessageType.DELETED,
406
- });
407
- // Do not insert a new message row for delete control messages
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
- // Emit event for new message
521
- const row = await this.queries.messages.getById(id);
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
- const randomMessageId = message.type !== MessageType.KEEP_ALIVE &&
595
- message.type !== MessageType.RETENTION_POLICY
596
- ? crypto.getRandomValues(new Uint8Array(MESSAGE_ID_SIZE))
597
- : undefined;
598
- message.messageId = randomMessageId;
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
- /** Send a message, queued via QueueManager if available */
990
- async send(message) {
991
- if (this.queueManager) {
992
- return this.queueManager.enqueue(message.contactUserId, () => this.sendMessage(message));
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
- return this.sendMessage(message);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260331143013",
3
+ "version": "0.0.2-dev.20260401160204",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",