@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.
Files changed (60) hide show
  1. package/README.md +2 -2
  2. package/dist/config/sdk.js +1 -1
  3. package/dist/contacts.d.ts +11 -17
  4. package/dist/contacts.js +20 -26
  5. package/dist/core/SdkEventEmitter.d.ts +2 -0
  6. package/dist/core/SdkEventEmitter.js +2 -0
  7. package/dist/core/SdkPolling.d.ts +2 -5
  8. package/dist/core/SdkPolling.js +1 -4
  9. package/dist/core/index.d.ts +1 -1
  10. package/dist/core/index.js +1 -0
  11. package/dist/db.d.ts +34 -56
  12. package/dist/db.js +39 -229
  13. package/dist/gossip.d.ts +54 -23
  14. package/dist/gossip.js +149 -120
  15. package/dist/index.d.ts +13 -4
  16. package/dist/index.js +13 -4
  17. package/dist/queries/activeSeekers.d.ts +2 -0
  18. package/dist/queries/activeSeekers.js +18 -0
  19. package/dist/queries/announcementCursors.d.ts +2 -0
  20. package/dist/queries/announcementCursors.js +20 -0
  21. package/dist/queries/contacts.d.ts +13 -0
  22. package/dist/queries/contacts.js +43 -0
  23. package/dist/queries/discussions.d.ts +21 -0
  24. package/dist/queries/discussions.js +73 -0
  25. package/dist/queries/index.d.ts +7 -0
  26. package/dist/queries/index.js +7 -0
  27. package/dist/queries/messages.d.ts +27 -0
  28. package/dist/queries/messages.js +120 -0
  29. package/dist/queries/pendingAnnouncements.d.ts +4 -0
  30. package/dist/queries/pendingAnnouncements.js +11 -0
  31. package/dist/queries/userProfile.d.ts +22 -0
  32. package/dist/queries/userProfile.js +116 -0
  33. package/dist/schema.d.ts +1280 -0
  34. package/dist/schema.js +164 -0
  35. package/dist/services/announcement.d.ts +8 -7
  36. package/dist/services/announcement.js +95 -121
  37. package/dist/services/auth.d.ts +1 -3
  38. package/dist/services/auth.js +4 -11
  39. package/dist/services/discussion.d.ts +8 -15
  40. package/dist/services/discussion.js +62 -62
  41. package/dist/services/message.d.ts +15 -26
  42. package/dist/services/message.js +282 -270
  43. package/dist/services/refresh.d.ts +3 -5
  44. package/dist/services/refresh.js +10 -13
  45. package/dist/sqlite-worker.d.ts +12 -0
  46. package/dist/sqlite-worker.js +106 -0
  47. package/dist/sqlite.d.ts +79 -0
  48. package/dist/sqlite.js +448 -0
  49. package/dist/utils/contacts.d.ts +2 -5
  50. package/dist/utils/contacts.js +23 -39
  51. package/dist/utils/discussions.d.ts +7 -2
  52. package/dist/utils/discussions.js +24 -5
  53. package/dist/utils/logs.js +1 -3
  54. package/dist/utils/validation.d.ts +2 -4
  55. package/dist/utils/validation.js +5 -10
  56. package/package.json +5 -7
  57. package/dist/api/messageProtocol/mock.d.ts +0 -12
  58. package/dist/api/messageProtocol/mock.js +0 -12
  59. package/dist/types/events.d.ts +0 -80
  60. package/dist/types/events.js +0 -7
package/dist/gossip.js CHANGED
@@ -1,25 +1,24 @@
1
1
  /**
2
- * GossipSdk
2
+ * GossipSdk - Singleton SDK with clean lifecycle API
3
3
  *
4
4
  * @example
5
5
  * ```typescript
6
- * import { createGossipSdk } from '@massalabs/gossip-sdk';
7
- *
8
- * const sdk = createGossipSdk();
6
+ * import { gossipSdk } from '@massalabs/gossip-sdk';
9
7
  *
10
8
  * // Initialize once at app startup
11
- * await sdk.init({
9
+ * await gossipSdk.init({
10
+ * db,
12
11
  * protocolBaseUrl: 'https://api.example.com',
13
12
  * });
14
13
  *
15
14
  * // Open session (login) - SDK handles keys/session internally
16
- * await sdk.openSession({
15
+ * await gossipSdk.openSession({
17
16
  * mnemonic: 'word1 word2 ...',
18
17
  * onPersist: async (blob) => { /* save to db *\/ },
19
18
  * });
20
19
  *
21
20
  * // Or restore existing session
22
- * await sdk.openSession({
21
+ * await gossipSdk.openSession({
23
22
  * mnemonic: 'word1 word2 ...',
24
23
  * encryptedSession: savedBlob,
25
24
  * encryptionKey: key,
@@ -27,36 +26,37 @@
27
26
  * });
28
27
  *
29
28
  * // Use clean API
30
- * await sdk.messages.send(contactId, 'Hello!');
31
- * await sdk.discussions.start(contact);
32
- * const contacts = await sdk.contacts.list(ownerUserId);
29
+ * await gossipSdk.messages.send(contactId, 'Hello!');
30
+ * await gossipSdk.discussions.start(contact);
31
+ * const contacts = await gossipSdk.contacts.list(ownerUserId);
33
32
  *
34
33
  * // Events
35
- * sdk.on('message', (msg) => { ... });
36
- * sdk.on('discussionRequest', (discussion, contact) => { ... });
34
+ * gossipSdk.on('message', (msg) => { ... });
35
+ * gossipSdk.on('discussionRequest', (discussion, contact) => { ... });
37
36
  *
38
37
  * // Logout
39
- * await sdk.closeSession();
38
+ * await gossipSdk.closeSession();
40
39
  * ```
41
40
  */
42
- import { gossipDb, } from './db';
41
+ import { MessageStatus, } from './db';
42
+ import { toDiscussion, toSortedDiscussions } from './utils/discussions';
43
43
  import { createMessageProtocol } from './api/messageProtocol';
44
44
  import { setProtocolBaseUrl } from './config/protocol';
45
45
  import { defaultSdkConfig, mergeConfig, } from './config/sdk';
46
46
  import { startWasmInitialization, ensureWasmInitialized } from './wasm/loader';
47
47
  import { generateUserKeys } from './wasm/userKeys';
48
48
  import { SessionModule } from './wasm/session';
49
- import { generateEncryptionKeyFromSeed, } from './wasm/encryption';
50
49
  import { AnnouncementService, } from './services/announcement';
51
50
  import { DiscussionService, } from './services/discussion';
52
- import { MessageService, } from './services/message';
51
+ import { MessageService, rowToMessage, } from './services/message';
53
52
  import { RefreshService } from './services/refresh';
54
53
  import { AuthService } from './services/auth';
55
54
  import { validateUserIdFormat, validateUsernameFormat, } from './utils/validation';
56
55
  import { QueueManager } from './utils/queue';
57
56
  import { encodeUserId, decodeUserId } from './utils/userId';
57
+ import { initDb } from './sqlite';
58
+ import { getMessageById as queryGetMessageById, getMessagesByOwnerAndContact, getMessagesByStatus, updateMessageById, getDiscussionsByOwner, getDiscussionByOwnerAndContact, } from './queries';
58
59
  import { getContacts, getContact, addContact, updateContactName, deleteContact, } from './contacts';
59
- import { SessionManagerWrapper, } from './wasm/bindings';
60
60
  import { SdkEventEmitter, SdkEventType, } from './core/SdkEventEmitter';
61
61
  import { SdkPolling } from './core/SdkPolling';
62
62
  export { SdkEventType };
@@ -164,19 +164,24 @@ class GossipSdk {
164
164
  console.warn('[GossipSdk] Already initialized');
165
165
  return;
166
166
  }
167
+ console.log('[GossipSdk] Initializing SDK');
167
168
  // Merge config with defaults
168
- const config = mergeConfig(options?.config);
169
+ const config = mergeConfig(options.config);
169
170
  // Configure protocol URL (prefer explicit option, then config)
170
- const baseUrl = options?.protocolBaseUrl ?? config.protocol.baseUrl;
171
+ const baseUrl = options.protocolBaseUrl ?? config.protocol.baseUrl;
171
172
  if (baseUrl) {
172
173
  setProtocolBaseUrl(baseUrl);
173
174
  }
174
175
  // Start WASM initialization
175
176
  startWasmInitialization();
177
+ console.log('[GossipSdk] Initializing SQLite');
178
+ // Initialize SQLite (idempotent — no-op if already initialized).
179
+ await initDb({ wasmUrl: options.wasmUrl, opfsPath: options.opfsPath });
180
+ console.log('[GossipSdk] SQLite initialized');
176
181
  // Create message protocol
177
182
  const messageProtocol = createMessageProtocol();
178
183
  // Create auth service (doesn't need session)
179
- this._auth = new AuthService(this.db, messageProtocol);
184
+ this._auth = new AuthService(messageProtocol);
180
185
  this.state = {
181
186
  status: SdkStatus.INITIALIZED,
182
187
  messageProtocol,
@@ -194,24 +199,23 @@ class GossipSdk {
194
199
  if (this.state.status === SdkStatus.SESSION_OPEN) {
195
200
  throw new Error('Session already open. Call closeSession() first.');
196
201
  }
197
- if (!options.encryptionKey) {
198
- console.warn('[GossipSdk] No encryptionKey provided. Creating a new one from mnemonic.');
202
+ // Validate session restore options - must have both or neither
203
+ if (options.encryptedSession && !options.encryptionKey) {
204
+ throw new Error('encryptionKey is required when encryptedSession is provided.');
199
205
  }
200
- const encryptionKey = options.encryptionKey ??
201
- (await generateEncryptionKeyFromSeed(options.mnemonic, new Uint8Array(32).fill(0)));
202
- // Ensure WASM is ready before using any WASM-backed helpers
203
- await ensureWasmInitialized();
204
- // Validate that encryptedSession can be decrypted with the provided key
205
- if (options.encryptedSession) {
206
- try {
207
- const sessionManager = SessionManagerWrapper.from_encrypted_blob(options.encryptedSession, encryptionKey);
208
- // We only create this wrapper for validation, free it immediately
209
- sessionManager.free();
210
- }
211
- catch {
212
- throw new Error('[GossipSdk] Failed to load encrypted session. Please provide a valid encryptedSession and encryptionKey.');
213
- }
206
+ if (options.encryptionKey && !options.encryptedSession) {
207
+ console.warn('[GossipSdk] encryptionKey provided without encryptedSession - key will be ignored');
208
+ }
209
+ // Validate persistence options
210
+ if (options.onPersist && !options.persistEncryptionKey) {
211
+ throw new Error('persistEncryptionKey is required when onPersist is provided.');
214
212
  }
213
+ if (options.persistEncryptionKey && !options.onPersist) {
214
+ console.warn('[GossipSdk] persistEncryptionKey provided without onPersist callback - key will be unused');
215
+ }
216
+ const { messageProtocol } = this.state;
217
+ // Ensure WASM is ready
218
+ await ensureWasmInitialized();
215
219
  // Generate keys from mnemonic
216
220
  const userKeys = await generateUserKeys(options.mnemonic);
217
221
  // Create session with persistence callback
@@ -220,30 +224,39 @@ class GossipSdk {
220
224
  await this.handleSessionPersist();
221
225
  }, options.sessionConfig);
222
226
  // Restore existing session state if provided
223
- if (options.encryptedSession) {
224
- session.load(options.encryptedSession, encryptionKey);
227
+ if (options.encryptedSession && options.encryptionKey) {
228
+ session.load(options.encryptedSession, options.encryptionKey);
225
229
  }
226
- const db = this.db;
227
- this._announcement = new AnnouncementService(db, this.state.messageProtocol, session, this.eventEmitter, this.config);
228
- this._discussion = new DiscussionService(db, this._announcement, session, this.eventEmitter);
229
- this._message = new MessageService(db, this.state.messageProtocol, session, this._discussion, this.eventEmitter, this.config);
230
- this._refresh = new RefreshService(db, this._message, this._discussion, this._announcement, session, this.eventEmitter);
231
- // Publish gossip ID so the user is discoverable (fire-and-forget don't block login)
232
- this._auth.ensurePublicKeyPublished(session.ourPk, session.userIdEncoded).catch(err => console.warn('[GossipSdk] Failed to publish public key:', err));
230
+ // Get config from initialized state
231
+ const { config } = this.state;
232
+ // Create services with config (refreshService will be set after creation)
233
+ this._announcement = new AnnouncementService(messageProtocol, session, this.eventEmitter, config);
234
+ this._discussion = new DiscussionService(this._announcement, session, this.eventEmitter);
235
+ this._message = new MessageService(messageProtocol, session, this.eventEmitter, config);
236
+ this._refresh = new RefreshService(this._message, this._discussion, this._announcement, session, this.eventEmitter);
237
+ // Publish gossip ID (public key) on messageProtocol so the user is discoverable
238
+ await this._auth.ensurePublicKeyPublished(session.ourPk, session.userIdEncoded);
239
+ // Now set refreshService on services (circular dependency resolved via setter)
240
+ this._discussion.setRefreshService(this._refresh);
241
+ this._message.setRefreshService(this._refresh);
242
+ this._announcement.setRefreshService(this._refresh);
243
+ // Reset any messages stuck in SENDING status to WAITING_SESSION
244
+ // This handles app crash/close during message send
245
+ await this.resetStuckSendingMessages();
233
246
  // Update SDK state to reflect the newly opened session.
234
247
  this.state = {
235
248
  status: SdkStatus.SESSION_OPEN,
236
- messageProtocol: this.state.messageProtocol,
237
- config: this.state.config,
249
+ messageProtocol,
250
+ config,
238
251
  session,
239
252
  userKeys,
240
- encryptionKey,
253
+ persistEncryptionKey: options.persistEncryptionKey,
241
254
  onPersist: options.onPersist,
242
255
  };
243
256
  // Create cached service API wrappers
244
- this.createServiceAPIWrappers(session, db);
257
+ this.createServiceAPIWrappers(session);
245
258
  // Auto-start polling if enabled in config
246
- if (this.config.polling.enabled) {
259
+ if (config.polling.enabled) {
247
260
  this.startPolling();
248
261
  }
249
262
  }
@@ -251,68 +264,50 @@ class GossipSdk {
251
264
  * Create cached service API wrappers.
252
265
  * Called once during openSession to avoid creating new objects on each getter access.
253
266
  */
254
- createServiceAPIWrappers(session, db) {
267
+ createServiceAPIWrappers(session) {
255
268
  this._messagesAPI = {
256
- get: id => db.messages.get(id),
269
+ get: async (id) => {
270
+ const row = await queryGetMessageById(id);
271
+ return row ? rowToMessage(row) : undefined;
272
+ },
257
273
  getMessages: async (contactUserId) => {
258
274
  const state = this.requireSession();
259
- return await db.messages
260
- .where('[ownerUserId+contactUserId]')
261
- .equals([state.session.userIdEncoded, contactUserId])
262
- .toArray();
275
+ const rows = await getMessagesByOwnerAndContact(state.session.userIdEncoded, contactUserId);
276
+ return rows.map(rowToMessage);
263
277
  },
264
- send: message => this.messageQueues.enqueue(message.contactUserId, async () => {
265
- const result = await this._message.sendMessage(message);
266
- if (result.success)
267
- await this._refresh?.stateUpdate();
268
- return result;
269
- }),
278
+ send: message => this.messageQueues.enqueue(message.contactUserId, () => this._message.sendMessage(message)),
270
279
  fetch: () => this._message.fetchMessages(),
271
280
  findByMsgId: (messageId, ownerUserId, contactUserId) => this._message.findMessageByMsgId(messageId, ownerUserId, contactUserId),
272
281
  markAsRead: id => this._message.markAsRead(id),
273
282
  };
274
283
  this._discussionsAPI = {
275
- start: async (contact, payload) => {
276
- const result = await this._discussion.initialize(contact, payload);
277
- if (result.success)
278
- await this._refresh?.stateUpdate();
279
- return result;
280
- },
281
- accept: async (discussion) => {
282
- const result = await this._discussion.accept(discussion);
283
- if (result.success)
284
- await this._refresh?.stateUpdate();
285
- return result;
286
- },
287
- renew: async (contactUserId) => {
288
- const result = await this._discussion.createSessionForContact(contactUserId, new Uint8Array(0));
289
- if (result.success)
290
- await this._refresh?.stateUpdate();
291
- return result;
292
- },
284
+ start: (contact, payload) => this._discussion.initialize(contact, payload),
285
+ accept: (discussion) => this._discussion.accept(discussion),
286
+ renew: (contactUserId) => this._discussion.createSessionForContact(contactUserId, new Uint8Array(0)),
293
287
  getStatus: (contactUserId) => {
294
288
  if (this.state.status !== SdkStatus.SESSION_OPEN)
295
289
  throw new Error('No session open. Call openSession() first.');
296
290
  return this.state.session.peerSessionStatus(decodeUserId(contactUserId));
297
291
  },
298
- list: ownerUserId => db.getDiscussionsByOwner(ownerUserId),
299
- get: (ownerUserId, contactUserId) => db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId),
292
+ list: async (ownerUserId) => {
293
+ const all = await getDiscussionsByOwner(ownerUserId);
294
+ return toSortedDiscussions(all);
295
+ },
296
+ get: async (ownerUserId, contactUserId) => {
297
+ const row = await getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
298
+ return row ? toDiscussion(row) : undefined;
299
+ },
300
300
  };
301
301
  this._announcementsAPI = {
302
- fetch: async () => {
303
- const result = await this._announcement.fetchAndProcessAnnouncements();
304
- if (result.newAnnouncementsCount)
305
- await this._refresh?.stateUpdate();
306
- return result;
307
- },
302
+ fetch: () => this._announcement.fetchAndProcessAnnouncements(),
308
303
  skipHistorical: () => this._announcement.skipHistoricalAnnouncements(),
309
304
  };
310
305
  this._contactsAPI = {
311
- list: ownerUserId => getContacts(ownerUserId, db),
312
- get: (ownerUserId, contactUserId) => getContact(ownerUserId, contactUserId, db),
313
- add: (ownerUserId, userId, name, publicKeys) => addContact(ownerUserId, userId, name, publicKeys, db),
314
- updateName: (ownerUserId, contactUserId, newName) => updateContactName(ownerUserId, contactUserId, newName, db),
315
- delete: (ownerUserId, contactUserId) => deleteContact(ownerUserId, contactUserId, db, session),
306
+ list: ownerUserId => getContacts(ownerUserId),
307
+ get: (ownerUserId, contactUserId) => getContact(ownerUserId, contactUserId),
308
+ add: (ownerUserId, userId, name, publicKeys) => addContact(ownerUserId, userId, name, publicKeys),
309
+ updateName: (ownerUserId, contactUserId, newName) => updateContactName(ownerUserId, contactUserId, newName),
310
+ delete: (ownerUserId, contactUserId) => deleteContact(ownerUserId, contactUserId, session),
316
311
  };
317
312
  }
318
313
  /**
@@ -375,12 +370,28 @@ class GossipSdk {
375
370
  * Get encrypted session blob for persistence.
376
371
  * Throws if no session is open.
377
372
  */
378
- getEncryptedSession() {
373
+ getEncryptedSession(encryptionKey) {
379
374
  const state = this.requireSession();
380
- if (!state.encryptionKey) {
381
- throw new Error('No encryption key found. Call openSession() first.');
375
+ return state.session.toEncryptedBlob(encryptionKey);
376
+ }
377
+ /**
378
+ * Configure session persistence after session is opened.
379
+ * Use this when you need to set up persistence after account creation.
380
+ *
381
+ * @param encryptionKey - Key to encrypt session blob
382
+ * @param onPersist - Callback to save encrypted session blob
383
+ */
384
+ configurePersistence(encryptionKey, onPersist) {
385
+ if (this.state.status !== SdkStatus.SESSION_OPEN) {
386
+ throw new Error('No session open. Call openSession() first.');
382
387
  }
383
- return state.session.toEncryptedBlob(state.encryptionKey);
388
+ // Update state with persistence config
389
+ this.state = {
390
+ ...this.state,
391
+ persistEncryptionKey: encryptionKey,
392
+ onPersist,
393
+ };
394
+ console.log('[GossipSdk] Session persistence configured');
384
395
  }
385
396
  // ─────────────────────────────────────────────────────────────────
386
397
  // Services (accessible only when session is open)
@@ -424,9 +435,6 @@ class GossipSdk {
424
435
  }
425
436
  return this._contactsAPI;
426
437
  }
427
- get db() {
428
- return gossipDb();
429
- }
430
438
  /**
431
439
  * Update state for all discussions:
432
440
  * - Cleanup orphaned peers
@@ -477,26 +485,17 @@ class GossipSdk {
477
485
  console.warn('[GossipSdk] Cannot start polling - no session open');
478
486
  return;
479
487
  }
480
- const { config, session } = this.state;
488
+ const { config } = this.state;
481
489
  this.pollingManager.start(config, {
482
490
  fetchMessages: async () => {
483
491
  await this._message?.fetchMessages();
484
492
  },
485
493
  fetchAnnouncements: async () => {
486
- const result = await this._announcement?.fetchAndProcessAnnouncements();
487
- if (result?.newAnnouncementsCount)
488
- this._refresh?.stateUpdate();
494
+ await this._announcement?.fetchAndProcessAnnouncements();
489
495
  },
490
496
  handleSessionRefresh: async () => {
491
- // Retry public key publish if it failed during openSession()
492
- this._auth
493
- ?.ensurePublicKeyPublished(session.ourPk, session.userIdEncoded)
494
- .catch(() => { });
495
497
  await this.updateState();
496
498
  },
497
- getActiveDiscussions: async () => {
498
- return this.db.getDiscussionsByOwner(session.userIdEncoded);
499
- },
500
499
  }, this.eventEmitter);
501
500
  }
502
501
  // ─────────────────────────────────────────────────────────────────
@@ -526,24 +525,54 @@ class GossipSdk {
526
525
  async handleSessionPersist() {
527
526
  if (this.state.status !== SdkStatus.SESSION_OPEN)
528
527
  return;
529
- const { onPersist, encryptionKey, session } = this.state;
530
- if (!onPersist || !encryptionKey)
528
+ const { onPersist, persistEncryptionKey, session } = this.state;
529
+ if (!onPersist || !persistEncryptionKey)
531
530
  return;
532
531
  try {
533
- const blob = session.toEncryptedBlob(encryptionKey);
532
+ const blob = session.toEncryptedBlob(persistEncryptionKey);
534
533
  console.log(`[SessionPersist] Saving session blob (${blob.length} bytes)`);
535
- await onPersist(blob, encryptionKey);
534
+ await onPersist(blob, persistEncryptionKey);
536
535
  }
537
536
  catch (error) {
538
537
  this.eventEmitter.emit(SdkEventType.ERROR, error instanceof Error ? error : new Error(String(error)), 'session_persist');
539
538
  }
540
539
  }
540
+ /**
541
+ * Reset messages stuck in SENDING status to WAITING_SESSION.
542
+ *
543
+ * Per spec: SENDING is a transient state that should never be persisted.
544
+ * If the app crashes/closes during a send, the message would be stuck forever.
545
+ *
546
+ * By resetting to WAITING_SESSION:
547
+ * - Message will be re-encrypted with current session keys
548
+ * - Message will be automatically sent when session is active
549
+ * - No manual user intervention required
550
+ *
551
+ * We also clear encryptedMessage and seeker since they may be stale.
552
+ */
553
+ async resetStuckSendingMessages() {
554
+ try {
555
+ const stuck = await getMessagesByStatus(MessageStatus.SENDING);
556
+ for (const m of stuck) {
557
+ await updateMessageById(m.id, {
558
+ status: MessageStatus.WAITING_SESSION,
559
+ encryptedMessage: null,
560
+ seeker: null,
561
+ });
562
+ }
563
+ if (stuck.length > 0) {
564
+ console.log(`[GossipSdk] Reset ${stuck.length} stuck SENDING message(s) to WAITING_SESSION for auto-retry`);
565
+ }
566
+ }
567
+ catch (error) {
568
+ console.error('[GossipSdk] Failed to reset stuck messages:', error);
569
+ }
570
+ }
541
571
  }
542
572
  // ─────────────────────────────────────────────────────────────────────────────
543
- // Factory & Class Export
573
+ // Singleton Export
544
574
  // ─────────────────────────────────────────────────────────────────────────────
545
- /** Create a new GossipSdk instance. The consumer manages the instance lifecycle. */
546
- export function createGossipSdk() {
547
- return new GossipSdk();
548
- }
575
+ /** The singleton GossipSdk instance */
576
+ export const gossipSdk = new GossipSdk();
577
+ // Also export the class for testing
549
578
  export { GossipSdk };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,20 @@
1
1
  /**
2
2
  * Gossip SDK
3
3
  *
4
+ * Main entry point for the Gossip SDK.
5
+ * Works in both browser and Node.js environments.
6
+ *
7
+ * WASM is loaded via the web target build with runtime detection:
8
+ * - Browser: init() uses import.meta.url + fetch
9
+ * - Node.js / Jiti: init(bytes) reads the .wasm file from disk
10
+ *
4
11
  * @example
5
12
  * ```typescript
6
- * import { createGossipSdk } from '@massalabs/gossip-sdk';
13
+ * import { gossipSdk } from '@massalabs/gossip-sdk';
7
14
  *
8
- * const sdk = createGossipSdk();
9
- * await sdk.init();
10
- * await sdk.openSession({ mnemonic: '...' });
15
+ * await gossipSdk.init({ db, protocolBaseUrl: 'https://api.example.com' });
16
+ * await gossipSdk.openSession({ mnemonic: '...' });
17
+ * await gossipSdk.messages.send(contactId, 'Hello!');
11
18
  * ```
12
19
  *
13
20
  * @packageDocumentation
@@ -16,5 +23,7 @@ export * from './api';
16
23
  export * from './crypto';
17
24
  export * from './db';
18
25
  export * from './gossip';
26
+ export * from './queries';
19
27
  export * from './utils';
20
28
  export * from './wasm';
29
+ export { initDb, closeSqlite, getSqliteDb, clearAllTables, clearConversationTables, isSqliteOpen, } from './sqlite';
package/dist/index.js CHANGED
@@ -1,13 +1,20 @@
1
1
  /**
2
2
  * Gossip SDK
3
3
  *
4
+ * Main entry point for the Gossip SDK.
5
+ * Works in both browser and Node.js environments.
6
+ *
7
+ * WASM is loaded via the web target build with runtime detection:
8
+ * - Browser: init() uses import.meta.url + fetch
9
+ * - Node.js / Jiti: init(bytes) reads the .wasm file from disk
10
+ *
4
11
  * @example
5
12
  * ```typescript
6
- * import { createGossipSdk } from '@massalabs/gossip-sdk';
13
+ * import { gossipSdk } from '@massalabs/gossip-sdk';
7
14
  *
8
- * const sdk = createGossipSdk();
9
- * await sdk.init();
10
- * await sdk.openSession({ mnemonic: '...' });
15
+ * await gossipSdk.init({ db, protocolBaseUrl: 'https://api.example.com' });
16
+ * await gossipSdk.openSession({ mnemonic: '...' });
17
+ * await gossipSdk.messages.send(contactId, 'Hello!');
11
18
  * ```
12
19
  *
13
20
  * @packageDocumentation
@@ -16,5 +23,7 @@ export * from './api';
16
23
  export * from './crypto';
17
24
  export * from './db';
18
25
  export * from './gossip';
26
+ export * from './queries';
19
27
  export * from './utils';
20
28
  export * from './wasm';
29
+ export { initDb, closeSqlite, getSqliteDb, clearAllTables, clearConversationTables, isSqliteOpen, } from './sqlite';
@@ -0,0 +1,2 @@
1
+ export declare function setOnSeekersUpdated(cb: ((seekers: Uint8Array[]) => void) | null): void;
2
+ export declare function replaceActiveSeekers(seekers: Uint8Array[]): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import * as schema from '../schema';
2
+ import { getSqliteDb, withTransaction } from '../sqlite';
3
+ let onSeekersUpdated = null;
4
+ export function setOnSeekersUpdated(cb) {
5
+ onSeekersUpdated = cb;
6
+ }
7
+ export async function replaceActiveSeekers(seekers) {
8
+ await withTransaction(async () => {
9
+ const db = getSqliteDb();
10
+ await db.delete(schema.activeSeekers);
11
+ if (seekers.length > 0) {
12
+ await db
13
+ .insert(schema.activeSeekers)
14
+ .values(seekers.map(seeker => ({ seeker })));
15
+ }
16
+ });
17
+ onSeekersUpdated?.(seekers);
18
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getAnnouncementCursor(userId: string): Promise<string | undefined>;
2
+ export declare function upsertAnnouncementCursor(userId: string, counter: string): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import * as schema from '../schema';
3
+ import { getSqliteDb } from '../sqlite';
4
+ export async function getAnnouncementCursor(userId) {
5
+ const row = await getSqliteDb()
6
+ .select({ counter: schema.announcementCursors.counter })
7
+ .from(schema.announcementCursors)
8
+ .where(eq(schema.announcementCursors.userId, userId))
9
+ .get();
10
+ return row?.counter;
11
+ }
12
+ export async function upsertAnnouncementCursor(userId, counter) {
13
+ await getSqliteDb()
14
+ .insert(schema.announcementCursors)
15
+ .values({ userId, counter })
16
+ .onConflictDoUpdate({
17
+ target: schema.announcementCursors.userId,
18
+ set: { counter },
19
+ });
20
+ }
@@ -0,0 +1,13 @@
1
+ import * as schema from '../schema';
2
+ import { Contact } from '../db';
3
+ export type ContactRow = typeof schema.contacts.$inferSelect;
4
+ type ContactInsert = typeof schema.contacts.$inferInsert;
5
+ export declare function getContactByOwnerAndUser(ownerUserId: string, userId: string): Promise<ContactRow | undefined>;
6
+ export declare function getContactsByOwner(ownerUserId: string): Promise<Contact[]>;
7
+ export declare function insertContact(values: ContactInsert): Promise<number>;
8
+ export declare function updateContactByOwnerAndUser(ownerUserId: string, userId: string, data: Partial<ContactInsert>): Promise<void>;
9
+ export declare function deleteContactByOwnerAndUser(ownerUserId: string, userId: string): Promise<void>;
10
+ export declare function getContactNamesByPrefix(ownerUserId: string, prefix: string): Promise<{
11
+ name: string;
12
+ }[]>;
13
+ export {};
@@ -0,0 +1,43 @@
1
+ import { eq, and, sql } from 'drizzle-orm';
2
+ import * as schema from '../schema';
3
+ import { getSqliteDb, getLastInsertRowId } from '../sqlite';
4
+ export async function getContactByOwnerAndUser(ownerUserId, userId) {
5
+ return getSqliteDb()
6
+ .select()
7
+ .from(schema.contacts)
8
+ .where(and(eq(schema.contacts.ownerUserId, ownerUserId), eq(schema.contacts.userId, userId)))
9
+ .get();
10
+ }
11
+ export async function getContactsByOwner(ownerUserId) {
12
+ return getSqliteDb()
13
+ .select()
14
+ .from(schema.contacts)
15
+ .where(eq(schema.contacts.ownerUserId, ownerUserId))
16
+ .all();
17
+ }
18
+ export async function insertContact(values) {
19
+ await getSqliteDb().insert(schema.contacts).values(values);
20
+ return getLastInsertRowId();
21
+ }
22
+ export async function updateContactByOwnerAndUser(ownerUserId, userId, data) {
23
+ await getSqliteDb()
24
+ .update(schema.contacts)
25
+ .set(data)
26
+ .where(and(eq(schema.contacts.ownerUserId, ownerUserId), eq(schema.contacts.userId, userId)));
27
+ }
28
+ export async function deleteContactByOwnerAndUser(ownerUserId, userId) {
29
+ await getSqliteDb()
30
+ .delete(schema.contacts)
31
+ .where(and(eq(schema.contacts.ownerUserId, ownerUserId), eq(schema.contacts.userId, userId)));
32
+ }
33
+ /** Escape LIKE wildcard characters so they match literally. */
34
+ function escapeLike(value) {
35
+ return value.replace(/[%_]/g, '\\$&');
36
+ }
37
+ export async function getContactNamesByPrefix(ownerUserId, prefix) {
38
+ return getSqliteDb()
39
+ .select({ name: schema.contacts.name })
40
+ .from(schema.contacts)
41
+ .where(and(eq(schema.contacts.ownerUserId, ownerUserId), sql `${schema.contacts.name} LIKE ${escapeLike(prefix) + '%'} ESCAPE '\\'`))
42
+ .all();
43
+ }
@@ -0,0 +1,21 @@
1
+ import * as schema from '../schema';
2
+ import type { DiscussionStatus } from '../db';
3
+ export type DiscussionRow = typeof schema.discussions.$inferSelect;
4
+ type DiscussionInsert = typeof schema.discussions.$inferInsert;
5
+ export declare function getDiscussionByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<DiscussionRow | undefined>;
6
+ export declare function getDiscussionsByOwner(ownerUserId: string): Promise<DiscussionRow[]>;
7
+ export declare function getDiscussionById(id: number): Promise<DiscussionRow | undefined>;
8
+ export declare function insertDiscussion(values: DiscussionInsert): Promise<number>;
9
+ export declare function updateDiscussionById(id: number, data: Partial<DiscussionInsert>): Promise<void>;
10
+ export declare function deleteDiscussionById(id: number): Promise<void>;
11
+ export declare function deleteDiscussionsByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<void>;
12
+ /**
13
+ * Atomically increment unreadCount by 1.
14
+ */
15
+ export declare function incrementUnreadCount(discussionId: number): Promise<void>;
16
+ /**
17
+ * Atomically decrement unreadCount by 1, ensuring it never goes below 0.
18
+ */
19
+ export declare function decrementUnreadCount(discussionId: number): Promise<void>;
20
+ export declare function getDiscussionsByOwnerAndStatus(ownerUserId: string, status: DiscussionStatus): Promise<DiscussionRow[]>;
21
+ export {};