@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.
- package/README.md +2 -2
- package/dist/config/sdk.js +1 -1
- package/dist/contacts.d.ts +11 -17
- package/dist/contacts.js +20 -26
- package/dist/core/SdkEventEmitter.d.ts +2 -0
- package/dist/core/SdkEventEmitter.js +2 -0
- package/dist/core/SdkPolling.d.ts +2 -5
- package/dist/core/SdkPolling.js +1 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -0
- package/dist/db.d.ts +34 -56
- package/dist/db.js +39 -229
- package/dist/gossip.d.ts +54 -23
- package/dist/gossip.js +149 -120
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -4
- package/dist/queries/activeSeekers.d.ts +2 -0
- package/dist/queries/activeSeekers.js +18 -0
- package/dist/queries/announcementCursors.d.ts +2 -0
- package/dist/queries/announcementCursors.js +20 -0
- package/dist/queries/contacts.d.ts +13 -0
- package/dist/queries/contacts.js +43 -0
- package/dist/queries/discussions.d.ts +21 -0
- package/dist/queries/discussions.js +73 -0
- package/dist/queries/index.d.ts +7 -0
- package/dist/queries/index.js +7 -0
- package/dist/queries/messages.d.ts +27 -0
- package/dist/queries/messages.js +120 -0
- package/dist/queries/pendingAnnouncements.d.ts +4 -0
- package/dist/queries/pendingAnnouncements.js +11 -0
- package/dist/queries/userProfile.d.ts +22 -0
- package/dist/queries/userProfile.js +116 -0
- package/dist/schema.d.ts +1280 -0
- package/dist/schema.js +164 -0
- package/dist/services/announcement.d.ts +8 -7
- package/dist/services/announcement.js +95 -121
- package/dist/services/auth.d.ts +1 -3
- package/dist/services/auth.js +4 -11
- package/dist/services/discussion.d.ts +8 -15
- package/dist/services/discussion.js +62 -62
- package/dist/services/message.d.ts +15 -26
- package/dist/services/message.js +282 -270
- package/dist/services/refresh.d.ts +3 -5
- package/dist/services/refresh.js +10 -13
- package/dist/sqlite-worker.d.ts +12 -0
- package/dist/sqlite-worker.js +106 -0
- package/dist/sqlite.d.ts +79 -0
- package/dist/sqlite.js +448 -0
- package/dist/utils/contacts.d.ts +2 -5
- package/dist/utils/contacts.js +23 -39
- package/dist/utils/discussions.d.ts +7 -2
- package/dist/utils/discussions.js +24 -5
- package/dist/utils/logs.js +1 -3
- package/dist/utils/validation.d.ts +2 -4
- package/dist/utils/validation.js +5 -10
- package/package.json +5 -7
- package/dist/api/messageProtocol/mock.d.ts +0 -12
- package/dist/api/messageProtocol/mock.js +0 -12
- package/dist/types/events.d.ts +0 -80
- 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 {
|
|
7
|
-
*
|
|
8
|
-
* const sdk = createGossipSdk();
|
|
6
|
+
* import { gossipSdk } from '@massalabs/gossip-sdk';
|
|
9
7
|
*
|
|
10
8
|
* // Initialize once at app startup
|
|
11
|
-
* await
|
|
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
|
|
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
|
|
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
|
|
31
|
-
* await
|
|
32
|
-
* const contacts = await
|
|
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
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* gossipSdk.on('message', (msg) => { ... });
|
|
35
|
+
* gossipSdk.on('discussionRequest', (discussion, contact) => { ... });
|
|
37
36
|
*
|
|
38
37
|
* // Logout
|
|
39
|
-
* await
|
|
38
|
+
* await gossipSdk.closeSession();
|
|
40
39
|
* ```
|
|
41
40
|
*/
|
|
42
|
-
import {
|
|
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
|
|
169
|
+
const config = mergeConfig(options.config);
|
|
169
170
|
// Configure protocol URL (prefer explicit option, then config)
|
|
170
|
-
const baseUrl = options
|
|
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(
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
this.
|
|
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
|
|
237
|
-
config
|
|
249
|
+
messageProtocol,
|
|
250
|
+
config,
|
|
238
251
|
session,
|
|
239
252
|
userKeys,
|
|
240
|
-
|
|
253
|
+
persistEncryptionKey: options.persistEncryptionKey,
|
|
241
254
|
onPersist: options.onPersist,
|
|
242
255
|
};
|
|
243
256
|
// Create cached service API wrappers
|
|
244
|
-
this.createServiceAPIWrappers(session
|
|
257
|
+
this.createServiceAPIWrappers(session);
|
|
245
258
|
// Auto-start polling if enabled in config
|
|
246
|
-
if (
|
|
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
|
|
267
|
+
createServiceAPIWrappers(session) {
|
|
255
268
|
this._messagesAPI = {
|
|
256
|
-
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
|
-
|
|
260
|
-
|
|
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,
|
|
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:
|
|
276
|
-
|
|
277
|
-
|
|
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 =>
|
|
299
|
-
|
|
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:
|
|
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
|
|
312
|
-
get: (ownerUserId, contactUserId) => getContact(ownerUserId, contactUserId
|
|
313
|
-
add: (ownerUserId, userId, name, publicKeys) => addContact(ownerUserId, userId, name, publicKeys
|
|
314
|
-
updateName: (ownerUserId, contactUserId, newName) => updateContactName(ownerUserId, contactUserId, newName
|
|
315
|
-
delete: (ownerUserId, contactUserId) => deleteContact(ownerUserId, contactUserId,
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
530
|
-
if (!onPersist || !
|
|
528
|
+
const { onPersist, persistEncryptionKey, session } = this.state;
|
|
529
|
+
if (!onPersist || !persistEncryptionKey)
|
|
531
530
|
return;
|
|
532
531
|
try {
|
|
533
|
-
const blob = session.toEncryptedBlob(
|
|
532
|
+
const blob = session.toEncryptedBlob(persistEncryptionKey);
|
|
534
533
|
console.log(`[SessionPersist] Saving session blob (${blob.length} bytes)`);
|
|
535
|
-
await onPersist(blob,
|
|
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
|
-
//
|
|
573
|
+
// Singleton Export
|
|
544
574
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
545
|
-
/**
|
|
546
|
-
export
|
|
547
|
-
|
|
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 {
|
|
13
|
+
* import { gossipSdk } from '@massalabs/gossip-sdk';
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
* await
|
|
10
|
-
* await
|
|
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 {
|
|
13
|
+
* import { gossipSdk } from '@massalabs/gossip-sdk';
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
* await
|
|
10
|
-
* await
|
|
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,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,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 {};
|