@massalabs/gossip-sdk 0.0.2-dev.20260217132139 → 0.0.2-dev.20260218140506

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.
@@ -15,6 +15,7 @@ export declare class RestMessageProtocol implements IMessageProtocol {
15
15
  sendMessage(message: EncryptedMessage): Promise<void>;
16
16
  sendAnnouncement(announcement: Uint8Array): Promise<string>;
17
17
  fetchAnnouncements(limit?: number, cursor?: string): Promise<BulletinItem[]>;
18
+ fetchBulletinCounter(): Promise<string>;
18
19
  fetchPublicKeyByUserId(userId: Uint8Array): Promise<string>;
19
20
  postPublicKey(base64PublicKeys: string): Promise<string>;
20
21
  private makeRequest;
@@ -91,6 +91,16 @@ export class RestMessageProtocol {
91
91
  data: decodeFromBase64(item.data),
92
92
  }));
93
93
  }
94
+ async fetchBulletinCounter() {
95
+ const url = `${this.baseUrl}${BULLETIN_ENDPOINT}/counter`;
96
+ const response = await this.makeRequest(url, {
97
+ method: 'GET',
98
+ });
99
+ if (!response.success || !response.data) {
100
+ throw new Error(response.error || 'Failed to fetch bulletin counter');
101
+ }
102
+ return response.data.counter;
103
+ }
94
104
  async fetchPublicKeyByUserId(userId) {
95
105
  const response = await this.makeRequest(`${this.baseUrl}/auth/retrieve`, {
96
106
  method: 'POST',
@@ -40,6 +40,10 @@ export interface IMessageProtocol {
40
40
  * @param cursor - Optional cursor (counter) to fetch announcements after this value
41
41
  */
42
42
  fetchAnnouncements(limit?: number, cursor?: string): Promise<BulletinItem[]>;
43
+ /**
44
+ * Fetch the latest bulletin counter from the API without downloading announcement data.
45
+ */
46
+ fetchBulletinCounter(): Promise<string>;
43
47
  /**
44
48
  * Fetch public key by userId hash (base64 string)
45
49
  * @param userId - Decoded userId bytes
package/dist/db.js CHANGED
@@ -185,7 +185,8 @@ export class GossipDatabase extends Dexie {
185
185
  lastMessageId: messageId,
186
186
  lastMessageContent: message.content,
187
187
  lastMessageTimestamp: message.timestamp,
188
- unreadCount: message.direction === MessageDirection.INCOMING
188
+ unreadCount: message.direction === MessageDirection.INCOMING &&
189
+ ![MessageType.ANNOUNCEMENT, MessageType.KEEP_ALIVE].includes(message.type)
189
190
  ? discussion.unreadCount + 1
190
191
  : discussion.unreadCount,
191
192
  updatedAt: new Date(),
@@ -201,7 +202,10 @@ export class GossipDatabase extends Dexie {
201
202
  lastMessageId: messageId,
202
203
  lastMessageContent: message.content,
203
204
  lastMessageTimestamp: message.timestamp,
204
- unreadCount: message.direction === MessageDirection.INCOMING ? 1 : 0,
205
+ unreadCount: message.direction === MessageDirection.INCOMING &&
206
+ ![MessageType.ANNOUNCEMENT, MessageType.KEEP_ALIVE].includes(message.type)
207
+ ? 1
208
+ : 0,
205
209
  updatedAt: new Date(),
206
210
  });
207
211
  }
package/dist/gossip.d.ts CHANGED
@@ -196,6 +196,8 @@ interface DiscussionServiceAPI {
196
196
  interface AnnouncementServiceAPI {
197
197
  /** Fetch and process announcements from the protocol */
198
198
  fetch(): Promise<AnnouncementReceptionResult>;
199
+ /** Skip historical announcements for a new account. Call after profile creation. */
200
+ skipHistorical(): Promise<void>;
199
201
  }
200
202
  interface ContactsAPI {
201
203
  /** List all contacts for the owner */
package/dist/gossip.js CHANGED
@@ -230,6 +230,7 @@ class GossipSdk {
230
230
  this._refresh = new RefreshService(db, this._message, this._discussion, this._announcement, session, this.eventEmitter);
231
231
  // Publish gossip ID so the user is discoverable (fire-and-forget — don't block login)
232
232
  this._auth.ensurePublicKeyPublished(session.ourPk, session.userIdEncoded).catch(err => console.warn('[GossipSdk] Failed to publish public key:', err));
233
+ // Update SDK state to reflect the newly opened session.
233
234
  this.state = {
234
235
  status: SdkStatus.SESSION_OPEN,
235
236
  messageProtocol: this.state.messageProtocol,
@@ -304,6 +305,7 @@ class GossipSdk {
304
305
  await this._refresh?.stateUpdate();
305
306
  return result;
306
307
  },
308
+ skipHistorical: () => this._announcement.skipHistoricalAnnouncements(),
307
309
  };
308
310
  this._contactsAPI = {
309
311
  list: ownerUserId => getContacts(ownerUserId, db),
@@ -35,10 +35,16 @@ export declare class AnnouncementService {
35
35
  fetchAndProcessAnnouncements(): Promise<AnnouncementReceptionResult>;
36
36
  /**
37
37
  * Persist lastBulletinCounter for the current user.
38
- * Uses put() so the cursor is saved even when no profile row exists yet
39
- * (e.g. headless bot that never created a profile via app UI).
38
+ * Only updates an existing profile row never creates a partial one,
39
+ * since a row without `security`/`session` would crash downstream code.
40
40
  */
41
41
  private _upsertLastBulletinCounter;
42
+ /**
43
+ * Fetch the latest bulletin counter from the API and persist it so that
44
+ * historical announcements (undecryptable by a new account) are skipped.
45
+ * No-op if a counter already exists or if the profile hasn't been created yet.
46
+ */
47
+ skipHistoricalAnnouncements(): Promise<void>;
42
48
  private _fetchAnnouncements;
43
49
  private _generateTemporaryContactName;
44
50
  private _processIncomingAnnouncement;
@@ -267,30 +267,47 @@ export class AnnouncementService {
267
267
  }
268
268
  /**
269
269
  * Persist lastBulletinCounter for the current user.
270
- * Uses put() so the cursor is saved even when no profile row exists yet
271
- * (e.g. headless bot that never created a profile via app UI).
270
+ * Only updates an existing profile row never creates a partial one,
271
+ * since a row without `security`/`session` would crash downstream code.
272
272
  */
273
273
  async _upsertLastBulletinCounter(nextCounter) {
274
274
  const userId = this.session.userIdEncoded;
275
275
  const existing = await this.db.userProfile.get(userId);
276
- if (existing) {
277
- await this.db.userProfile.update(userId, {
278
- lastBulletinCounter: nextCounter,
279
- updatedAt: new Date(),
280
- });
276
+ if (!existing) {
277
+ logger
278
+ .forMethod('_upsertLastBulletinCounter')
279
+ .debug('no profile row yet — skipping counter write');
281
280
  return;
282
281
  }
283
- // Minimal profile so cursor persists; headless bots may never create a full profile
284
- await this.db.userProfile.put({
285
- userId,
286
- username: '',
287
- status: 'offline',
288
- lastSeen: new Date(),
289
- createdAt: new Date(),
290
- updatedAt: new Date(),
282
+ await this.db.userProfile.update(userId, {
291
283
  lastBulletinCounter: nextCounter,
284
+ updatedAt: new Date(),
292
285
  });
293
286
  }
287
+ /**
288
+ * Fetch the latest bulletin counter from the API and persist it so that
289
+ * historical announcements (undecryptable by a new account) are skipped.
290
+ * No-op if a counter already exists or if the profile hasn't been created yet.
291
+ */
292
+ async skipHistoricalAnnouncements() {
293
+ const log = logger.forMethod('skipHistoricalAnnouncements');
294
+ const existing = await this.db.userProfile.get(this.session.userIdEncoded);
295
+ if (!existing) {
296
+ log.debug('no profile row yet — skipping');
297
+ return;
298
+ }
299
+ if (existing.lastBulletinCounter !== undefined)
300
+ return;
301
+ try {
302
+ const counter = await this.messageProtocol.fetchBulletinCounter();
303
+ await this._upsertLastBulletinCounter(counter);
304
+ log.info('set initial bulletin counter for new account', { counter });
305
+ }
306
+ catch (err) {
307
+ // Non-critical — on failure the first fetch starts from the beginning.
308
+ log.warn('failed to initialize bulletin counter', { err });
309
+ }
310
+ }
294
311
  async _fetchAnnouncements(cursor, limit) {
295
312
  const fetchLimit = limit ?? this.config.announcements.fetchLimit;
296
313
  const log = logger.forMethod('_fetchAnnouncements');
@@ -383,7 +400,7 @@ export class AnnouncementService {
383
400
  content: message,
384
401
  type: MessageType.ANNOUNCEMENT,
385
402
  direction: MessageDirection.INCOMING,
386
- status: MessageStatus.DELIVERED,
403
+ status: MessageStatus.READ,
387
404
  timestamp,
388
405
  });
389
406
  }
@@ -113,7 +113,7 @@ export class DiscussionService {
113
113
  content: payload.message,
114
114
  type: MessageType.ANNOUNCEMENT,
115
115
  direction: MessageDirection.OUTGOING,
116
- status: MessageStatus.DELIVERED, // Announcement message are not like other msg. they are set as delivered to prevent them from being sent again if session is renewed
116
+ status: MessageStatus.READ, // Announcement message are not like other msg. they are set as read to prevent them from being sent again if session is renewed
117
117
  timestamp: new Date(),
118
118
  });
119
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260217132139",
3
+ "version": "0.0.2-dev.20260218140506",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",