@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/schema.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Drizzle ORM schema for the Gossip SDK database.
3
+ *
4
+ * IMPORTANT: The raw DDL in sqlite.ts must stay in sync with these definitions.
5
+ * When adding/removing/renaming columns, tables, or indexes, update BOTH files.
6
+ *
7
+ * Type mappings from SQLite:
8
+ * Date → integer (epoch milliseconds)
9
+ * boolean → integer (0/1)
10
+ * Uint8Array → blob (wa-sqlite handles natively)
11
+ * JSON → text (JSON.stringify/parse in service layer)
12
+ */
13
+ import { sqliteTable, text, integer, index, uniqueIndex, customType, } from 'drizzle-orm/sqlite-core';
14
+ // Custom blob type — wa-sqlite returns Uint8Array natively for BLOB columns.
15
+ // Drizzle's built-in blob mode converts to/from hex strings, which breaks
16
+ // with wa-sqlite's direct Uint8Array handling. This custom type passes through.
17
+ const bytes = customType({
18
+ dataType() {
19
+ return 'blob';
20
+ },
21
+ fromDriver(value) {
22
+ return value;
23
+ },
24
+ toDriver(value) {
25
+ return value;
26
+ },
27
+ });
28
+ // ---------------------------------------------------------------------------
29
+ // contacts
30
+ // ---------------------------------------------------------------------------
31
+ export const contacts = sqliteTable('contacts', {
32
+ id: integer('id').primaryKey({ autoIncrement: true }),
33
+ ownerUserId: text('ownerUserId').notNull(),
34
+ userId: text('userId').notNull(),
35
+ name: text('name').notNull(),
36
+ avatar: text('avatar'),
37
+ publicKeys: bytes('publicKeys').notNull(),
38
+ isOnline: integer('isOnline', { mode: 'boolean' }).notNull(),
39
+ lastSeen: integer('lastSeen', { mode: 'timestamp_ms' }).notNull(),
40
+ createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
41
+ }, table => [
42
+ index('contacts_owner_user_idx').on(table.ownerUserId, table.userId),
43
+ index('contacts_owner_name_idx').on(table.ownerUserId, table.name),
44
+ ]);
45
+ // ---------------------------------------------------------------------------
46
+ // messages
47
+ // ---------------------------------------------------------------------------
48
+ export const messages = sqliteTable('messages', {
49
+ id: integer('id').primaryKey({ autoIncrement: true }),
50
+ ownerUserId: text('ownerUserId').notNull(),
51
+ contactUserId: text('contactUserId').notNull(),
52
+ messageId: bytes('messageId'), // 12-byte random ID for deduplication
53
+ content: text('content').notNull(),
54
+ serializedContent: bytes('serializedContent'),
55
+ type: text('type').$type().notNull(),
56
+ direction: text('direction').$type().notNull(),
57
+ status: text('status').$type().notNull(),
58
+ timestamp: integer('timestamp', { mode: 'timestamp_ms' }).notNull(),
59
+ metadata: text('metadata'), // JSON — service layer handles stringify/parse
60
+ seeker: bytes('seeker'),
61
+ replyTo: text('replyTo'), // JSON — contains Uint8Array fields (base64 encoded)
62
+ forwardOf: text('forwardOf'), // JSON — contains Uint8Array fields (base64 encoded)
63
+ encryptedMessage: bytes('encryptedMessage'),
64
+ whenToSend: integer('whenToSend', { mode: 'timestamp_ms' }),
65
+ }, table => [
66
+ index('messages_owner_contact_idx').on(table.ownerUserId, table.contactUserId),
67
+ index('messages_owner_status_idx').on(table.ownerUserId, table.status),
68
+ index('messages_owner_contact_status_idx').on(table.ownerUserId, table.contactUserId, table.status),
69
+ index('messages_owner_seeker_idx').on(table.ownerUserId, table.seeker),
70
+ index('messages_owner_contact_dir_idx').on(table.ownerUserId, table.contactUserId, table.direction),
71
+ index('messages_owner_dir_status_idx').on(table.ownerUserId, table.direction, table.status),
72
+ index('messages_timestamp_idx').on(table.timestamp),
73
+ ]);
74
+ // ---------------------------------------------------------------------------
75
+ // userProfile
76
+ // ---------------------------------------------------------------------------
77
+ export const userProfile = sqliteTable('userProfile', {
78
+ userId: text('userId').primaryKey(),
79
+ username: text('username').notNull(),
80
+ avatar: text('avatar'),
81
+ bio: text('bio'),
82
+ status: text('status').notNull(),
83
+ lastSeen: integer('lastSeen', { mode: 'timestamp_ms' }).notNull(),
84
+ createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
85
+ updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(),
86
+ lastPublicKeyPush: integer('lastPublicKeyPush', {
87
+ mode: 'timestamp_ms',
88
+ }),
89
+ security: text('security').notNull(), // JSON — contains Uint8Array fields (base64 encoded)
90
+ session: bytes('session').notNull(),
91
+ }, table => [
92
+ index('userProfile_username_idx').on(table.username),
93
+ index('userProfile_status_idx').on(table.status),
94
+ ]);
95
+ // ---------------------------------------------------------------------------
96
+ // discussions
97
+ // ---------------------------------------------------------------------------
98
+ export const discussions = sqliteTable('discussions', {
99
+ id: integer('id').primaryKey({ autoIncrement: true }),
100
+ ownerUserId: text('ownerUserId').notNull(),
101
+ contactUserId: text('contactUserId').notNull(),
102
+ weAccepted: integer('weAccepted', { mode: 'boolean' })
103
+ .notNull()
104
+ .default(false),
105
+ sendAnnouncement: text('sendAnnouncement'), // JSON — nullable
106
+ direction: text('direction').$type().notNull(),
107
+ status: text('status').$type().notNull(),
108
+ nextSeeker: bytes('nextSeeker'),
109
+ initiationAnnouncement: bytes('initiationAnnouncement'),
110
+ announcementMessage: text('announcementMessage'),
111
+ lastSyncTimestamp: integer('lastSyncTimestamp', {
112
+ mode: 'timestamp_ms',
113
+ }),
114
+ customName: text('customName'),
115
+ lastMessageId: integer('lastMessageId'),
116
+ lastMessageContent: text('lastMessageContent'),
117
+ lastMessageTimestamp: integer('lastMessageTimestamp', {
118
+ mode: 'timestamp_ms',
119
+ }),
120
+ unreadCount: integer('unreadCount').notNull().default(0),
121
+ createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
122
+ updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(),
123
+ }, table => [
124
+ uniqueIndex('discussions_owner_contact_idx').on(table.ownerUserId, table.contactUserId),
125
+ index('discussions_owner_status_idx').on(table.ownerUserId, table.status),
126
+ ]);
127
+ // ---------------------------------------------------------------------------
128
+ // pendingEncryptedMessages
129
+ // ---------------------------------------------------------------------------
130
+ export const pendingEncryptedMessages = sqliteTable('pendingEncryptedMessages', {
131
+ id: integer('id').primaryKey({ autoIncrement: true }),
132
+ seeker: bytes('seeker').notNull(),
133
+ ciphertext: bytes('ciphertext').notNull(),
134
+ fetchedAt: integer('fetchedAt', { mode: 'timestamp_ms' }).notNull(),
135
+ }, table => [
136
+ index('pending_encrypted_seeker_idx').on(table.seeker),
137
+ index('pending_encrypted_fetchedAt_idx').on(table.fetchedAt),
138
+ ]);
139
+ // ---------------------------------------------------------------------------
140
+ // pendingAnnouncements
141
+ // ---------------------------------------------------------------------------
142
+ export const pendingAnnouncements = sqliteTable('pendingAnnouncements', {
143
+ id: integer('id').primaryKey({ autoIncrement: true }),
144
+ announcement: bytes('announcement').notNull(),
145
+ fetchedAt: integer('fetchedAt', { mode: 'timestamp_ms' }).notNull(),
146
+ counter: text('counter'),
147
+ }, table => [
148
+ uniqueIndex('pending_announcements_announcement_idx').on(table.announcement),
149
+ index('pending_announcements_fetchedAt_idx').on(table.fetchedAt),
150
+ ]);
151
+ // ---------------------------------------------------------------------------
152
+ // activeSeekers
153
+ // ---------------------------------------------------------------------------
154
+ export const activeSeekers = sqliteTable('activeSeekers', {
155
+ id: integer('id').primaryKey({ autoIncrement: true }),
156
+ seeker: bytes('seeker').notNull(),
157
+ }, table => [index('active_seekers_seeker_idx').on(table.seeker)]);
158
+ // ---------------------------------------------------------------------------
159
+ // announcementCursors
160
+ // ---------------------------------------------------------------------------
161
+ export const announcementCursors = sqliteTable('announcementCursors', {
162
+ userId: text('userId').primaryKey(),
163
+ counter: text('counter').notNull(),
164
+ });
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Handles broadcasting and processing of session announcements.
5
5
  */
6
- import { type Discussion, type GossipDatabase } from '../db';
6
+ import { type Discussion } from '../db';
7
7
  import { IMessageProtocol } from '../api/messageProtocol';
8
8
  import { UserPublicKeys } from '../wasm/bindings';
9
9
  import { SessionModule } from '../wasm/session';
10
10
  import { SdkConfig } from '../config/sdk';
11
11
  import { Result } from '../utils/type';
12
12
  import { SdkEventEmitter } from '../core/SdkEventEmitter';
13
+ import type { RefreshService } from './refresh';
13
14
  export interface AnnouncementReceptionResult {
14
15
  success: boolean;
15
16
  newAnnouncementsCount: number;
@@ -17,13 +18,14 @@ export interface AnnouncementReceptionResult {
17
18
  }
18
19
  export declare const EstablishSessionError = "Session manager failed to establish outgoing session";
19
20
  export declare class AnnouncementService {
20
- private db;
21
21
  private messageProtocol;
22
22
  private session;
23
23
  private isProcessingAnnouncements;
24
24
  private eventEmitter;
25
25
  private config;
26
- constructor(db: GossipDatabase, messageProtocol: IMessageProtocol, session: SessionModule, eventEmitter: SdkEventEmitter, config?: SdkConfig);
26
+ private refreshService?;
27
+ constructor(messageProtocol: IMessageProtocol, session: SessionModule, eventEmitter: SdkEventEmitter, config?: SdkConfig);
28
+ setRefreshService(refreshService: RefreshService): void;
27
29
  setMessageProtocol(messageProtocol: IMessageProtocol): void;
28
30
  sendAnnouncement(announcement: Uint8Array): Promise<{
29
31
  success: boolean;
@@ -34,15 +36,14 @@ export declare class AnnouncementService {
34
36
  processOutgoingAnnouncements(discussions: Discussion[]): Promise<void>;
35
37
  fetchAndProcessAnnouncements(): Promise<AnnouncementReceptionResult>;
36
38
  /**
37
- * Persist lastBulletinCounter for the current user.
38
- * Only updates an existing profile row — never creates a partial one,
39
- * since a row without `security`/`session` would crash downstream code.
39
+ * Persist the bulletin polling cursor for the current user.
40
+ * Stored in a dedicated announcementCursors table (not userProfile).
40
41
  */
41
42
  private _upsertLastBulletinCounter;
42
43
  /**
43
44
  * Fetch the latest bulletin counter from the API and persist it so that
44
45
  * 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
+ * No-op if a counter already exists.
46
47
  */
47
48
  skipHistoricalAnnouncements(): Promise<void>;
48
49
  private _fetchAnnouncements;
@@ -3,25 +3,20 @@
3
3
  *
4
4
  * Handles broadcasting and processing of session announcements.
5
5
  */
6
- import { DiscussionDirection, MessageDirection, MessageStatus, MessageType, } from '../db';
6
+ import { DiscussionDirection, DiscussionStatus, MessageDirection, MessageStatus, MessageType, serializeSendAnnouncement, } from '../db';
7
+ import { toDiscussion } from '../utils/discussions';
7
8
  import { decodeUserId, encodeUserId } from '../utils/userId';
8
9
  import { SessionStatus } from '../wasm/bindings';
9
10
  import { sessionStatusToString } from '../wasm/session';
10
11
  import { Logger } from '../utils/logs';
11
12
  import { defaultSdkConfig } from '../config/sdk';
12
13
  import { decodeAnnouncementPayload } from '../utils/announcementPayload';
13
- import { resetSendQueue } from './discussion';
14
14
  import { SdkEventType } from '../core/SdkEventEmitter';
15
+ import { getContactByOwnerAndUser, insertContact, getContactNamesByPrefix, getDiscussionByOwnerAndContact, getDiscussionById, insertDiscussion, updateDiscussionById, insertMessage, getAnnouncementMessagesByContact, resetSendQueueMessages, getAllPendingAnnouncements, deletePendingAnnouncementsByIds, getAnnouncementCursor, upsertAnnouncementCursor, } from '../queries';
15
16
  const logger = new Logger('AnnouncementService');
16
17
  export const EstablishSessionError = 'Session manager failed to establish outgoing session';
17
18
  export class AnnouncementService {
18
- constructor(db, messageProtocol, session, eventEmitter, config = defaultSdkConfig) {
19
- Object.defineProperty(this, "db", {
20
- enumerable: true,
21
- configurable: true,
22
- writable: true,
23
- value: void 0
24
- });
19
+ constructor(messageProtocol, session, eventEmitter, config = defaultSdkConfig) {
25
20
  Object.defineProperty(this, "messageProtocol", {
26
21
  enumerable: true,
27
22
  configurable: true,
@@ -52,12 +47,20 @@ export class AnnouncementService {
52
47
  writable: true,
53
48
  value: void 0
54
49
  });
55
- this.db = db;
50
+ Object.defineProperty(this, "refreshService", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: void 0
55
+ });
56
56
  this.messageProtocol = messageProtocol;
57
57
  this.session = session;
58
58
  this.eventEmitter = eventEmitter;
59
59
  this.config = config;
60
60
  }
61
+ setRefreshService(refreshService) {
62
+ this.refreshService = refreshService;
63
+ }
61
64
  setMessageProtocol(messageProtocol) {
62
65
  this.messageProtocol = messageProtocol;
63
66
  }
@@ -100,7 +103,7 @@ export class AnnouncementService {
100
103
  for (const discussion of discussions) {
101
104
  if (!discussion.id)
102
105
  continue;
103
- // ensure the announcement is not null
106
+ // sendAnnouncement is deserialized by toDiscussion() before reaching here
104
107
  if (discussion.sendAnnouncement === null)
105
108
  continue;
106
109
  const { announcement_bytes, when_to_send } = discussion.sendAnnouncement;
@@ -109,7 +112,7 @@ export class AnnouncementService {
109
112
  discussionId: discussion.id,
110
113
  contactUserId: discussion.contactUserId,
111
114
  });
112
- await this.db.discussions.update(discussion.id, {
115
+ await updateDiscussionById(discussion.id, {
113
116
  sendAnnouncement: null,
114
117
  updatedAt: new Date(),
115
118
  });
@@ -126,28 +129,26 @@ export class AnnouncementService {
126
129
  }
127
130
  // Send the announcement and handle updates after transaction
128
131
  const result = await this.sendAnnouncement(announcement_bytes);
129
- // update discussion state after sending, inside a new transaction
130
- await this.db.transaction('rw', [this.db.discussions], async () => {
131
- const latest = await this.db.discussions.get(discussion.id);
132
- if (!latest || latest.sendAnnouncement === null) {
133
- return;
134
- }
135
- if (result.success) {
136
- await this.db.discussions.update(discussion.id, {
137
- sendAnnouncement: null,
138
- updatedAt: new Date(),
139
- });
140
- }
141
- else {
142
- await this.db.discussions.update(discussion.id, {
143
- sendAnnouncement: {
144
- announcement_bytes,
145
- when_to_send: new Date(Date.now() + this.config.announcements.retryDelayMs),
146
- },
147
- updatedAt: new Date(),
148
- });
149
- }
150
- });
132
+ // update discussion state after sending
133
+ const latest = await getDiscussionById(discussion.id);
134
+ if (!latest || latest.sendAnnouncement === null) {
135
+ continue;
136
+ }
137
+ if (result.success) {
138
+ await updateDiscussionById(discussion.id, {
139
+ sendAnnouncement: null,
140
+ updatedAt: new Date(),
141
+ });
142
+ }
143
+ else {
144
+ await updateDiscussionById(discussion.id, {
145
+ sendAnnouncement: serializeSendAnnouncement({
146
+ announcement_bytes,
147
+ when_to_send: new Date(Date.now() + this.config.announcements.retryDelayMs),
148
+ }),
149
+ updatedAt: new Date(),
150
+ });
151
+ }
151
152
  }
152
153
  }
153
154
  async fetchAndProcessAnnouncements() {
@@ -161,10 +162,10 @@ export class AnnouncementService {
161
162
  let fetchedCounters = [];
162
163
  this.isProcessingAnnouncements = true;
163
164
  try {
164
- const pending = await this.db.pendingAnnouncements.toArray();
165
+ const pending = await getAllPendingAnnouncements();
165
166
  const successfullyProcessedPendingIds = [];
166
167
  if (pending.length > 0) {
167
- log.info(`processing ${pending.length} pending announcements from IndexedDB`);
168
+ log.info(`processing ${pending.length} pending announcements from SQLite`);
168
169
  // Process pending announcements one by one, tracking successes
169
170
  let newAnnouncementsCount = 0;
170
171
  for (const item of pending) {
@@ -197,7 +198,7 @@ export class AnnouncementService {
197
198
  }
198
199
  // Delete only successfully processed pending announcements
199
200
  if (successfullyProcessedPendingIds.length > 0) {
200
- await this.db.pendingAnnouncements.bulkDelete(successfullyProcessedPendingIds);
201
+ await deletePendingAnnouncementsByIds(successfullyProcessedPendingIds);
201
202
  log.info(`deleted ${successfullyProcessedPendingIds.length} processed pending announcements`);
202
203
  }
203
204
  if (fetchedCounters.length > 0) {
@@ -212,8 +213,7 @@ export class AnnouncementService {
212
213
  };
213
214
  }
214
215
  // No pending - fetch from API
215
- const cursor = (await this.db.userProfile.get(this.session.userIdEncoded))
216
- ?.lastBulletinCounter;
216
+ const cursor = await getAnnouncementCursor(this.session.userIdEncoded);
217
217
  // fetch from node all announcements since the last retrieved announcement
218
218
  const fetched = await this._fetchAnnouncements(cursor);
219
219
  announcements = fetched.map(a => a.data);
@@ -266,37 +266,21 @@ export class AnnouncementService {
266
266
  }
267
267
  }
268
268
  /**
269
- * Persist lastBulletinCounter for the current user.
270
- * Only updates an existing profile row — never creates a partial one,
271
- * since a row without `security`/`session` would crash downstream code.
269
+ * Persist the bulletin polling cursor for the current user.
270
+ * Stored in a dedicated announcementCursors table (not userProfile).
272
271
  */
273
272
  async _upsertLastBulletinCounter(nextCounter) {
274
- const userId = this.session.userIdEncoded;
275
- const existing = await this.db.userProfile.get(userId);
276
- if (!existing) {
277
- logger
278
- .forMethod('_upsertLastBulletinCounter')
279
- .debug('no profile row yet — skipping counter write');
280
- return;
281
- }
282
- await this.db.userProfile.update(userId, {
283
- lastBulletinCounter: nextCounter,
284
- updatedAt: new Date(),
285
- });
273
+ await upsertAnnouncementCursor(this.session.userIdEncoded, nextCounter);
286
274
  }
287
275
  /**
288
276
  * Fetch the latest bulletin counter from the API and persist it so that
289
277
  * 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.
278
+ * No-op if a counter already exists.
291
279
  */
292
280
  async skipHistoricalAnnouncements() {
293
281
  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)
282
+ const existing = await getAnnouncementCursor(this.session.userIdEncoded);
283
+ if (existing !== undefined)
300
284
  return;
301
285
  try {
302
286
  const counter = await this.messageProtocol.fetchBulletinCounter();
@@ -321,11 +305,7 @@ export class AnnouncementService {
321
305
  }
322
306
  }
323
307
  async _generateTemporaryContactName(ownerUserId) {
324
- const newRequestContacts = await this.db.contacts
325
- .where('ownerUserId')
326
- .equals(ownerUserId)
327
- .filter(contact => contact.name.startsWith('New Request'))
328
- .toArray();
308
+ const newRequestContacts = await getContactNamesByPrefix(ownerUserId, 'New Request');
329
309
  const numbers = newRequestContacts
330
310
  .map(contact => {
331
311
  const match = contact.name.match(/^New Request (\d+)$/);
@@ -349,29 +329,26 @@ export class AnnouncementService {
349
329
  const contactUserIdRaw = announcerPkeys.derive_id();
350
330
  const contactUserId = encodeUserId(contactUserIdRaw);
351
331
  const sessionStatus = this.session.peerSessionStatus(contactUserIdRaw);
352
- // Log clearly for debugging
353
- console.log(`[Announcement] Received from ${contactUserId.slice(0, 12)}... -> session status: ${sessionStatusToString(sessionStatus)}`);
354
- log.info('session updated', {
332
+ log.info('announcement received', {
355
333
  contactUserId,
356
334
  status: sessionStatusToString(sessionStatus),
357
335
  });
358
- let contact = await this.db.getContactByOwnerAndUserId(this.session.userIdEncoded, contactUserId);
336
+ let contact = await getContactByOwnerAndUser(this.session.userIdEncoded, contactUserId);
359
337
  const isNewContact = !contact;
360
338
  if (isNewContact) {
361
339
  // Use extracted username if present, otherwise generate temporary name
362
340
  const name = username ||
363
341
  (await this._generateTemporaryContactName(this.session.userIdEncoded));
364
- await this.db.contacts.add({
342
+ await insertContact({
365
343
  ownerUserId: this.session.userIdEncoded,
366
344
  userId: contactUserId,
367
345
  name,
368
346
  publicKeys: announcerPkeys.to_bytes(),
369
- avatar: undefined,
370
347
  isOnline: false,
371
348
  lastSeen: new Date(),
372
349
  createdAt: new Date(),
373
350
  });
374
- contact = await this.db.getContactByOwnerAndUserId(this.session.userIdEncoded, contactUserId);
351
+ contact = await getContactByOwnerAndUser(this.session.userIdEncoded, contactUserId);
375
352
  log.info('created new contact', { contactUserId, name });
376
353
  }
377
354
  if (!contact) {
@@ -384,17 +361,13 @@ export class AnnouncementService {
384
361
  const timestamp = new Date(result.timestamp);
385
362
  const windowStart = new Date(timestamp.getTime() - 1000);
386
363
  const windowEnd = new Date(timestamp.getTime() + 1000);
387
- const existing = await this.db.messages
388
- .where('[ownerUserId+contactUserId]')
389
- .equals([this.session.userIdEncoded, contactUserId])
390
- .and(msg => msg.direction === MessageDirection.INCOMING &&
391
- msg.type === MessageType.ANNOUNCEMENT &&
392
- msg.content === message &&
364
+ const existing = await getAnnouncementMessagesByContact(this.session.userIdEncoded, contactUserId);
365
+ // Filter in JS for content match and timestamp window (SQLite timestamp_ms comparison)
366
+ const duplicate = existing.find(msg => msg.content === message &&
393
367
  msg.timestamp >= windowStart &&
394
- msg.timestamp <= windowEnd)
395
- .first();
396
- if (!existing) {
397
- await this.db.addMessage({
368
+ msg.timestamp <= windowEnd);
369
+ if (!duplicate) {
370
+ await insertMessage({
398
371
  ownerUserId: this.session.userIdEncoded,
399
372
  contactUserId,
400
373
  content: message,
@@ -405,6 +378,7 @@ export class AnnouncementService {
405
378
  });
406
379
  }
407
380
  }
381
+ await this.refreshService?.stateUpdate();
408
382
  return {
409
383
  success: true,
410
384
  discussionId,
@@ -413,47 +387,47 @@ export class AnnouncementService {
413
387
  }
414
388
  async _handleReceivedDiscussion(ownerUserId, contactUserId, message) {
415
389
  const log = logger.forMethod('handleReceivedDiscussion');
416
- const discussionId = await this.db.transaction('rw', this.db.discussions, this.db.messages, this.db.contacts, async () => {
417
- const existing = await this.db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
418
- if (existing) {
419
- const updateData = { updatedAt: new Date() };
420
- if (message)
421
- updateData.lastAnnouncementMessage = message;
422
- log.info('updating existing discussion', {
423
- discussionId: existing.id,
424
- contactUserId,
425
- });
426
- // Group update and queue reset in the same transaction to ensure atomicity
427
- await this.db.discussions.update(existing.id, updateData);
428
- // reset all messages in send queue to WAITING_SESSION for this contact
429
- await resetSendQueue(this.db, this.session.userIdEncoded, contactUserId);
430
- const newDiscussion = await this.db.discussions.get(existing.id);
431
- const contact = await this.db.getContactByOwnerAndUserId(ownerUserId, contactUserId);
432
- if (newDiscussion &&
433
- contact &&
434
- this.session.peerSessionStatus(decodeUserId(contactUserId)) ===
435
- SessionStatus.PeerRequested) {
436
- this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, newDiscussion, contact);
437
- }
438
- return existing.id;
439
- }
440
- log.info('creating new discussion', { contactUserId });
441
- return await this.db.discussions.add({
442
- ownerUserId,
390
+ const existing = await getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
391
+ if (existing) {
392
+ const updateData = { updatedAt: new Date() };
393
+ if (message)
394
+ updateData.announcementMessage = message;
395
+ log.info('updating existing discussion', {
396
+ discussionId: existing.id,
443
397
  contactUserId,
444
- weAccepted: false,
445
- sendAnnouncement: null,
446
- direction: DiscussionDirection.RECEIVED,
447
- lastAnnouncementMessage: message,
448
- unreadCount: 0,
449
- createdAt: new Date(),
450
- updatedAt: new Date(),
451
398
  });
399
+ await updateDiscussionById(existing.id, updateData);
400
+ // reset pending outgoing messages to WAITING_SESSION for this contact
401
+ // Only reset READY and SENT — DELIVERED messages should not be resent.
402
+ await resetSendQueueMessages(this.session.userIdEncoded, contactUserId, [
403
+ MessageStatus.READY,
404
+ MessageStatus.SENT,
405
+ ]);
406
+ const newDiscussion = await getDiscussionById(existing.id);
407
+ const contactRow = await getContactByOwnerAndUser(ownerUserId, contactUserId);
408
+ if (newDiscussion &&
409
+ contactRow &&
410
+ this.session.peerSessionStatus(decodeUserId(contactUserId)) ===
411
+ SessionStatus.PeerRequested) {
412
+ this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, toDiscussion(newDiscussion), contactRow);
413
+ }
414
+ return { discussionId: existing.id };
415
+ }
416
+ log.info('creating new discussion', { contactUserId });
417
+ const discussionId = await insertDiscussion({
418
+ ownerUserId,
419
+ contactUserId,
420
+ direction: DiscussionDirection.RECEIVED,
421
+ status: DiscussionStatus.PENDING,
422
+ announcementMessage: message,
423
+ unreadCount: 0,
424
+ createdAt: new Date(),
425
+ updatedAt: new Date(),
452
426
  });
453
- const discussion = await this.db.discussions.get(discussionId);
454
- const contact = await this.db.getContactByOwnerAndUserId(ownerUserId, contactUserId);
455
- if (discussion && contact) {
456
- this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, discussion, contact);
427
+ const discussion = await getDiscussionById(discussionId);
428
+ const contactRow = await getContactByOwnerAndUser(ownerUserId, contactUserId);
429
+ if (discussion && contactRow) {
430
+ this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, toDiscussion(discussion), contactRow);
457
431
  }
458
432
  return { discussionId };
459
433
  }
@@ -5,11 +5,9 @@
5
5
  */
6
6
  import { UserPublicKeys } from '../wasm/bindings';
7
7
  import { IMessageProtocol } from '../api/messageProtocol/types';
8
- import { type GossipDatabase } from '../db';
9
8
  export declare class AuthService {
10
- private db;
11
9
  messageProtocol: IMessageProtocol;
12
- constructor(db: GossipDatabase, messageProtocol: IMessageProtocol);
10
+ constructor(messageProtocol: IMessageProtocol);
13
11
  /**
14
12
  * Fetch public key by userId
15
13
  * @param userId - Bech32-encoded userId (e.g., "gossip1...")
@@ -6,14 +6,9 @@
6
6
  import { UserPublicKeys } from '../wasm/bindings';
7
7
  import { decodeUserId } from '../utils/userId';
8
8
  import { encodeToBase64, decodeFromBase64 } from '../utils/base64';
9
+ import { getUserProfileField, updateUserProfileById } from '../queries';
9
10
  export class AuthService {
10
- constructor(db, messageProtocol) {
11
- Object.defineProperty(this, "db", {
12
- enumerable: true,
13
- configurable: true,
14
- writable: true,
15
- value: db
16
- });
11
+ constructor(messageProtocol) {
17
12
  Object.defineProperty(this, "messageProtocol", {
18
13
  enumerable: true,
19
14
  configurable: true,
@@ -41,7 +36,7 @@ export class AuthService {
41
36
  * @param userId - Bech32-encoded userId (e.g., "gossip1...")
42
37
  */
43
38
  async ensurePublicKeyPublished(publicKeys, userId) {
44
- const profile = await this.db.userProfile.get(userId);
39
+ const profile = await getUserProfileField(userId);
45
40
  if (profile) {
46
41
  const lastPush = profile.lastPublicKeyPush;
47
42
  if (lastPush && !moreThanOneWeekAgo(lastPush)) {
@@ -50,9 +45,7 @@ export class AuthService {
50
45
  }
51
46
  await this.messageProtocol.postPublicKey(encodeToBase64(publicKeys.to_bytes()));
52
47
  if (profile) {
53
- await this.db.userProfile.update(userId, {
54
- lastPublicKeyPush: new Date(),
55
- });
48
+ await updateUserProfileById(userId, { lastPublicKeyPush: new Date() });
56
49
  }
57
50
  }
58
51
  }