@massalabs/gossip-sdk 0.0.2-dev.20260128094509 → 0.0.2-dev.20260128160824
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/dist/api/messageProtocol/index.d.ts +19 -0
- package/dist/api/messageProtocol/index.js +26 -0
- package/dist/api/messageProtocol/mock.d.ts +12 -0
- package/{src/api/messageProtocol/mock.ts → dist/api/messageProtocol/mock.js} +2 -3
- package/dist/api/messageProtocol/rest.d.ts +22 -0
- package/dist/api/messageProtocol/rest.js +161 -0
- package/dist/api/messageProtocol/types.d.ts +61 -0
- package/dist/api/messageProtocol/types.js +6 -0
- package/dist/assets/generated/wasm/README.md +281 -0
- package/dist/assets/generated/wasm/gossip_wasm.d.ts +638 -0
- package/dist/assets/generated/wasm/gossip_wasm.js +1557 -0
- package/dist/assets/generated/wasm/gossip_wasm_bg.wasm +0 -0
- package/dist/assets/generated/wasm/gossip_wasm_bg.wasm.d.ts +164 -0
- package/dist/assets/generated/wasm/package.json +15 -0
- package/dist/assets/generated/wasm-node/README.md +281 -0
- package/dist/assets/generated/wasm-node/gossip_wasm.d.ts +443 -0
- package/dist/assets/generated/wasm-node/gossip_wasm.js +1488 -0
- package/dist/assets/generated/wasm-node/gossip_wasm_bg.wasm +0 -0
- package/dist/assets/generated/wasm-node/gossip_wasm_bg.wasm.d.ts +164 -0
- package/dist/assets/generated/wasm-node/package.json +11 -0
- package/dist/config/protocol.d.ts +36 -0
- package/dist/config/protocol.js +77 -0
- package/dist/config/sdk.d.ts +82 -0
- package/dist/config/sdk.js +55 -0
- package/{src/contacts.ts → dist/contacts.d.ts} +11 -95
- package/dist/contacts.js +166 -0
- package/dist/core/SdkEventEmitter.d.ts +36 -0
- package/dist/core/SdkEventEmitter.js +59 -0
- package/dist/core/SdkPolling.d.ts +35 -0
- package/dist/core/SdkPolling.js +100 -0
- package/{src/core/index.ts → dist/core/index.d.ts} +0 -2
- package/dist/core/index.js +5 -0
- package/dist/crypto/bip39.d.ts +34 -0
- package/dist/crypto/bip39.js +62 -0
- package/dist/crypto/encryption.d.ts +37 -0
- package/dist/crypto/encryption.js +46 -0
- package/dist/db.d.ts +190 -0
- package/dist/db.js +311 -0
- package/dist/gossipSdk.d.ts +274 -0
- package/dist/gossipSdk.js +690 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +61 -0
- package/dist/services/announcement.d.ts +43 -0
- package/dist/services/announcement.js +491 -0
- package/dist/services/auth.d.ts +37 -0
- package/dist/services/auth.js +76 -0
- package/dist/services/discussion.d.ts +63 -0
- package/dist/services/discussion.js +297 -0
- package/dist/services/message.d.ts +74 -0
- package/dist/services/message.js +826 -0
- package/dist/services/refresh.d.ts +41 -0
- package/dist/services/refresh.js +205 -0
- package/{src/sw.ts → dist/sw.d.ts} +1 -8
- package/dist/sw.js +10 -0
- package/dist/types/events.d.ts +80 -0
- package/dist/types/events.js +7 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/utils/base64.d.ts +10 -0
- package/dist/utils/base64.js +30 -0
- package/dist/utils/contacts.d.ts +42 -0
- package/dist/utils/contacts.js +113 -0
- package/dist/utils/discussions.d.ts +24 -0
- package/dist/utils/discussions.js +38 -0
- package/dist/utils/logs.d.ts +19 -0
- package/dist/utils/logs.js +89 -0
- package/dist/utils/messageSerialization.d.ts +64 -0
- package/dist/utils/messageSerialization.js +184 -0
- package/dist/utils/queue.d.ts +50 -0
- package/dist/utils/queue.js +110 -0
- package/dist/utils/type.d.ts +10 -0
- package/dist/utils/type.js +4 -0
- package/dist/utils/userId.d.ts +40 -0
- package/dist/utils/userId.js +90 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +112 -0
- package/dist/utils.d.ts +30 -0
- package/{src/utils.ts → dist/utils.js} +9 -19
- package/dist/wasm/encryption.d.ts +56 -0
- package/{src/wasm/encryption.ts → dist/wasm/encryption.js} +22 -51
- package/dist/wasm/index.d.ts +10 -0
- package/{src/wasm/index.ts → dist/wasm/index.js} +1 -8
- package/dist/wasm/loader.d.ts +22 -0
- package/dist/wasm/loader.js +78 -0
- package/dist/wasm/session.d.ts +85 -0
- package/dist/wasm/session.js +226 -0
- package/dist/wasm/userKeys.d.ts +17 -0
- package/{src/wasm/userKeys.ts → dist/wasm/userKeys.js} +6 -13
- package/package.json +15 -2
- package/src/api/messageProtocol/index.ts +0 -53
- package/src/api/messageProtocol/rest.ts +0 -209
- package/src/api/messageProtocol/types.ts +0 -70
- package/src/config/protocol.ts +0 -97
- package/src/config/sdk.ts +0 -131
- package/src/core/SdkEventEmitter.ts +0 -91
- package/src/core/SdkPolling.ts +0 -134
- package/src/crypto/bip39.ts +0 -84
- package/src/crypto/encryption.ts +0 -77
- package/src/db.ts +0 -465
- package/src/gossipSdk.ts +0 -994
- package/src/index.ts +0 -211
- package/src/services/announcement.ts +0 -653
- package/src/services/auth.ts +0 -95
- package/src/services/discussion.ts +0 -380
- package/src/services/message.ts +0 -1055
- package/src/services/refresh.ts +0 -234
- package/src/types/events.ts +0 -108
- package/src/types.ts +0 -70
- package/src/utils/base64.ts +0 -39
- package/src/utils/contacts.ts +0 -161
- package/src/utils/discussions.ts +0 -55
- package/src/utils/logs.ts +0 -86
- package/src/utils/messageSerialization.ts +0 -257
- package/src/utils/queue.ts +0 -106
- package/src/utils/type.ts +0 -7
- package/src/utils/userId.ts +0 -114
- package/src/utils/validation.ts +0 -144
- package/src/wasm/loader.ts +0 -123
- package/src/wasm/session.ts +0 -276
- package/test/config/protocol.spec.ts +0 -31
- package/test/config/sdk.spec.ts +0 -163
- package/test/db/helpers.spec.ts +0 -142
- package/test/db/operations.spec.ts +0 -128
- package/test/db/states.spec.ts +0 -535
- package/test/integration/discussion-flow.spec.ts +0 -422
- package/test/integration/messaging-flow.spec.ts +0 -708
- package/test/integration/sdk-lifecycle.spec.ts +0 -325
- package/test/mocks/index.ts +0 -9
- package/test/mocks/mockMessageProtocol.ts +0 -100
- package/test/services/auth.spec.ts +0 -311
- package/test/services/discussion.spec.ts +0 -279
- package/test/services/message-deduplication.spec.ts +0 -299
- package/test/services/message-startup.spec.ts +0 -331
- package/test/services/message.spec.ts +0 -817
- package/test/services/refresh.spec.ts +0 -199
- package/test/services/session-status.spec.ts +0 -349
- package/test/session/wasm.spec.ts +0 -227
- package/test/setup.ts +0 -52
- package/test/utils/contacts.spec.ts +0 -156
- package/test/utils/discussions.spec.ts +0 -66
- package/test/utils/queue.spec.ts +0 -52
- package/test/utils/serialization.spec.ts +0 -120
- package/test/utils/userId.spec.ts +0 -120
- package/test/utils/validation.spec.ts +0 -223
- package/test/utils.ts +0 -212
- package/tsconfig.json +0 -26
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -28
package/src/gossipSdk.ts
DELETED
|
@@ -1,994 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GossipSdk - Singleton SDK with clean lifecycle API
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* import { gossipSdk } from 'gossip-sdk';
|
|
7
|
-
*
|
|
8
|
-
* // Initialize once at app startup
|
|
9
|
-
* await gossipSdk.init({
|
|
10
|
-
* db,
|
|
11
|
-
* protocolBaseUrl: 'https://api.example.com',
|
|
12
|
-
* });
|
|
13
|
-
*
|
|
14
|
-
* // Open session (login) - SDK handles keys/session internally
|
|
15
|
-
* await gossipSdk.openSession({
|
|
16
|
-
* mnemonic: 'word1 word2 ...',
|
|
17
|
-
* onPersist: async (blob) => { /* save to db *\/ },
|
|
18
|
-
* });
|
|
19
|
-
*
|
|
20
|
-
* // Or restore existing session
|
|
21
|
-
* await gossipSdk.openSession({
|
|
22
|
-
* mnemonic: 'word1 word2 ...',
|
|
23
|
-
* encryptedSession: savedBlob,
|
|
24
|
-
* encryptionKey: key,
|
|
25
|
-
* onPersist: async (blob) => { /* save to db *\/ },
|
|
26
|
-
* });
|
|
27
|
-
*
|
|
28
|
-
* // Use clean API
|
|
29
|
-
* await gossipSdk.messages.send(contactId, 'Hello!');
|
|
30
|
-
* await gossipSdk.discussions.start(contact);
|
|
31
|
-
* const contacts = await gossipSdk.contacts.list(ownerUserId);
|
|
32
|
-
*
|
|
33
|
-
* // Events
|
|
34
|
-
* gossipSdk.on('message', (msg) => { ... });
|
|
35
|
-
* gossipSdk.on('discussionRequest', (discussion, contact) => { ... });
|
|
36
|
-
*
|
|
37
|
-
* // Logout
|
|
38
|
-
* await gossipSdk.closeSession();
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
import {
|
|
43
|
-
GossipDatabase,
|
|
44
|
-
type Contact,
|
|
45
|
-
type Discussion,
|
|
46
|
-
type Message,
|
|
47
|
-
type UserProfile,
|
|
48
|
-
MessageStatus,
|
|
49
|
-
} from './db';
|
|
50
|
-
import { setDb } from './db';
|
|
51
|
-
import { IMessageProtocol, createMessageProtocol } from './api/messageProtocol';
|
|
52
|
-
import { setProtocolBaseUrl } from './config/protocol';
|
|
53
|
-
import {
|
|
54
|
-
type SdkConfig,
|
|
55
|
-
type DeepPartial,
|
|
56
|
-
defaultSdkConfig,
|
|
57
|
-
mergeConfig,
|
|
58
|
-
} from './config/sdk';
|
|
59
|
-
import { startWasmInitialization, ensureWasmInitialized } from './wasm/loader';
|
|
60
|
-
import { generateUserKeys, UserKeys } from './wasm/userKeys';
|
|
61
|
-
import { SessionModule } from './wasm/session';
|
|
62
|
-
import { EncryptionKey } from './wasm/encryption';
|
|
63
|
-
import {
|
|
64
|
-
AnnouncementService,
|
|
65
|
-
type AnnouncementReceptionResult,
|
|
66
|
-
} from './services/announcement';
|
|
67
|
-
import { DiscussionService } from './services/discussion';
|
|
68
|
-
import {
|
|
69
|
-
MessageService,
|
|
70
|
-
type MessageResult,
|
|
71
|
-
type SendMessageResult,
|
|
72
|
-
} from './services/message';
|
|
73
|
-
import { RefreshService } from './services/refresh';
|
|
74
|
-
import { AuthService } from './services/auth';
|
|
75
|
-
import type {
|
|
76
|
-
DeleteContactResult,
|
|
77
|
-
UpdateContactNameResult,
|
|
78
|
-
} from './utils/contacts';
|
|
79
|
-
import {
|
|
80
|
-
validateUserIdFormat,
|
|
81
|
-
validateUsernameFormat,
|
|
82
|
-
type ValidationResult,
|
|
83
|
-
} from './utils/validation';
|
|
84
|
-
import { QueueManager } from './utils/queue';
|
|
85
|
-
import { encodeUserId, decodeUserId } from './utils/userId';
|
|
86
|
-
import type { GossipSdkEvents } from './types/events';
|
|
87
|
-
import {
|
|
88
|
-
getContacts,
|
|
89
|
-
getContact,
|
|
90
|
-
addContact,
|
|
91
|
-
updateContactName,
|
|
92
|
-
deleteContact,
|
|
93
|
-
} from './contacts';
|
|
94
|
-
import type { UserPublicKeys } from './assets/generated/wasm/gossip_wasm';
|
|
95
|
-
import {
|
|
96
|
-
SdkEventEmitter,
|
|
97
|
-
type SdkEventType,
|
|
98
|
-
type SdkEventHandlers,
|
|
99
|
-
} from './core/SdkEventEmitter';
|
|
100
|
-
import { SdkPolling } from './core/SdkPolling';
|
|
101
|
-
|
|
102
|
-
// Re-export event types
|
|
103
|
-
export type { SdkEventType, SdkEventHandlers };
|
|
104
|
-
|
|
105
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
-
// Types
|
|
107
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
export interface GossipSdkInitOptions {
|
|
110
|
-
/** Database instance */
|
|
111
|
-
db: GossipDatabase;
|
|
112
|
-
/** Protocol API base URL (shorthand for config.protocol.baseUrl) */
|
|
113
|
-
protocolBaseUrl?: string;
|
|
114
|
-
/** SDK configuration (optional - uses defaults if not provided) */
|
|
115
|
-
config?: DeepPartial<SdkConfig>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface OpenSessionOptions {
|
|
119
|
-
/** BIP39 mnemonic phrase */
|
|
120
|
-
mnemonic: string;
|
|
121
|
-
/** Existing encrypted session blob (for restoring session) */
|
|
122
|
-
encryptedSession?: Uint8Array;
|
|
123
|
-
/** Encryption key for decrypting session */
|
|
124
|
-
encryptionKey?: EncryptionKey;
|
|
125
|
-
/** Callback when session state changes (for persistence) */
|
|
126
|
-
onPersist?: (
|
|
127
|
-
encryptedBlob: Uint8Array,
|
|
128
|
-
encryptionKey: EncryptionKey
|
|
129
|
-
) => Promise<void>;
|
|
130
|
-
/** Encryption key for persisting session (required if onPersist is provided) */
|
|
131
|
-
persistEncryptionKey?: EncryptionKey;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
-
// SDK State
|
|
136
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
type SdkStateUninitialized = { status: 'uninitialized' };
|
|
139
|
-
|
|
140
|
-
type SdkStateInitialized = {
|
|
141
|
-
status: 'initialized';
|
|
142
|
-
db: GossipDatabase;
|
|
143
|
-
messageProtocol: IMessageProtocol;
|
|
144
|
-
config: SdkConfig;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
type SdkStateSessionOpen = {
|
|
148
|
-
status: 'session_open';
|
|
149
|
-
db: GossipDatabase;
|
|
150
|
-
messageProtocol: IMessageProtocol;
|
|
151
|
-
config: SdkConfig;
|
|
152
|
-
session: SessionModule;
|
|
153
|
-
userKeys: UserKeys;
|
|
154
|
-
persistEncryptionKey?: EncryptionKey;
|
|
155
|
-
onPersist?: (
|
|
156
|
-
encryptedBlob: Uint8Array,
|
|
157
|
-
encryptionKey: EncryptionKey
|
|
158
|
-
) => Promise<void>;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
type SdkState =
|
|
162
|
-
| SdkStateUninitialized
|
|
163
|
-
| SdkStateInitialized
|
|
164
|
-
| SdkStateSessionOpen;
|
|
165
|
-
|
|
166
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
-
// SDK Class
|
|
168
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
class GossipSdkImpl {
|
|
171
|
-
private state: SdkState = { status: 'uninitialized' };
|
|
172
|
-
|
|
173
|
-
// Core components
|
|
174
|
-
private eventEmitter = new SdkEventEmitter();
|
|
175
|
-
private pollingManager = new SdkPolling();
|
|
176
|
-
private messageQueues = new QueueManager();
|
|
177
|
-
|
|
178
|
-
// Services (created when session opens)
|
|
179
|
-
private _auth: AuthService | null = null;
|
|
180
|
-
private _announcement: AnnouncementService | null = null;
|
|
181
|
-
private _discussion: DiscussionService | null = null;
|
|
182
|
-
private _message: MessageService | null = null;
|
|
183
|
-
private _refresh: RefreshService | null = null;
|
|
184
|
-
|
|
185
|
-
// Cached service API wrappers (created in openSession)
|
|
186
|
-
private _messagesAPI: MessageServiceAPI | null = null;
|
|
187
|
-
private _discussionsAPI: DiscussionServiceAPI | null = null;
|
|
188
|
-
private _announcementsAPI: AnnouncementServiceAPI | null = null;
|
|
189
|
-
private _contactsAPI: ContactsAPI | null = null;
|
|
190
|
-
private _refreshAPI: RefreshServiceAPI | null = null;
|
|
191
|
-
|
|
192
|
-
// ─────────────────────────────────────────────────────────────────
|
|
193
|
-
// Lifecycle
|
|
194
|
-
// ─────────────────────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Initialize the SDK. Call once at app startup.
|
|
198
|
-
*/
|
|
199
|
-
async init(options: GossipSdkInitOptions): Promise<void> {
|
|
200
|
-
if (this.state.status !== 'uninitialized') {
|
|
201
|
-
console.warn('[GossipSdk] Already initialized');
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Merge config with defaults
|
|
206
|
-
const config = mergeConfig(options.config);
|
|
207
|
-
|
|
208
|
-
// Configure database
|
|
209
|
-
setDb(options.db);
|
|
210
|
-
|
|
211
|
-
// Configure protocol URL (prefer explicit option, then config)
|
|
212
|
-
const baseUrl = options.protocolBaseUrl ?? config.protocol.baseUrl;
|
|
213
|
-
if (baseUrl) {
|
|
214
|
-
setProtocolBaseUrl(baseUrl);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Start WASM initialization
|
|
218
|
-
startWasmInitialization();
|
|
219
|
-
|
|
220
|
-
// Create message protocol
|
|
221
|
-
const messageProtocol = createMessageProtocol();
|
|
222
|
-
|
|
223
|
-
// Create auth service (doesn't need session)
|
|
224
|
-
this._auth = new AuthService(options.db, messageProtocol);
|
|
225
|
-
|
|
226
|
-
this.state = {
|
|
227
|
-
status: 'initialized',
|
|
228
|
-
db: options.db,
|
|
229
|
-
messageProtocol,
|
|
230
|
-
config,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Open a session (login).
|
|
236
|
-
* Generates keys from mnemonic and initializes session.
|
|
237
|
-
*/
|
|
238
|
-
async openSession(options: OpenSessionOptions): Promise<void> {
|
|
239
|
-
if (this.state.status === 'uninitialized') {
|
|
240
|
-
throw new Error('SDK not initialized. Call init() first.');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (this.state.status === 'session_open') {
|
|
244
|
-
throw new Error('Session already open. Call closeSession() first.');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (options.encryptedSession && !options.encryptionKey) {
|
|
248
|
-
throw new Error(
|
|
249
|
-
'encryptionKey is required when encryptedSession is provided.'
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (options.onPersist && !options.persistEncryptionKey) {
|
|
254
|
-
throw new Error(
|
|
255
|
-
'persistEncryptionKey is required when onPersist is provided.'
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const { db, messageProtocol } = this.state;
|
|
260
|
-
|
|
261
|
-
// Validate session restore options - must have both or neither
|
|
262
|
-
if (options.encryptedSession && !options.encryptionKey) {
|
|
263
|
-
throw new Error(
|
|
264
|
-
'encryptedSession provided without encryptionKey. Session restore requires both.'
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
if (options.encryptionKey && !options.encryptedSession) {
|
|
268
|
-
console.warn(
|
|
269
|
-
'[GossipSdk] encryptionKey provided without encryptedSession - key will be ignored'
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Validate persistence options - warn if incomplete
|
|
274
|
-
if (options.onPersist && !options.persistEncryptionKey) {
|
|
275
|
-
console.warn(
|
|
276
|
-
'[GossipSdk] onPersist provided without persistEncryptionKey - session will not be persisted'
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
if (options.persistEncryptionKey && !options.onPersist) {
|
|
280
|
-
console.warn(
|
|
281
|
-
'[GossipSdk] persistEncryptionKey provided without onPersist callback - key will be unused'
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Ensure WASM is ready
|
|
286
|
-
await ensureWasmInitialized();
|
|
287
|
-
|
|
288
|
-
// Generate keys from mnemonic
|
|
289
|
-
const userKeys = await generateUserKeys(options.mnemonic);
|
|
290
|
-
|
|
291
|
-
// Create session with persistence callback
|
|
292
|
-
// IMPORTANT: This callback is awaited by the session module before network sends
|
|
293
|
-
const session = new SessionModule(userKeys, async () => {
|
|
294
|
-
await this.handleSessionPersist();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// Restore existing session state if provided
|
|
298
|
-
if (options.encryptedSession && options.encryptionKey) {
|
|
299
|
-
// Create a minimal profile-like object for load()
|
|
300
|
-
const profileForLoad = {
|
|
301
|
-
session: options.encryptedSession,
|
|
302
|
-
} as UserProfile;
|
|
303
|
-
session.load(profileForLoad, options.encryptionKey);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Create event handlers that wire to our event system
|
|
307
|
-
const serviceEvents: GossipSdkEvents = {
|
|
308
|
-
onMessageReceived: (message: Message) => {
|
|
309
|
-
this.eventEmitter.emit('message', message);
|
|
310
|
-
},
|
|
311
|
-
onMessageSent: (message: Message) => {
|
|
312
|
-
this.eventEmitter.emit('messageSent', message);
|
|
313
|
-
},
|
|
314
|
-
onMessageFailed: (message: Message, error: Error) => {
|
|
315
|
-
this.eventEmitter.emit('messageFailed', message, error);
|
|
316
|
-
},
|
|
317
|
-
onDiscussionRequest: (discussion: Discussion, contact: Contact) => {
|
|
318
|
-
this.eventEmitter.emit('discussionRequest', discussion, contact);
|
|
319
|
-
},
|
|
320
|
-
onDiscussionStatusChanged: (discussion: Discussion) => {
|
|
321
|
-
this.eventEmitter.emit('discussionStatusChanged', discussion);
|
|
322
|
-
},
|
|
323
|
-
onSessionBroken: (discussion: Discussion) => {
|
|
324
|
-
this.eventEmitter.emit('sessionBroken', discussion);
|
|
325
|
-
},
|
|
326
|
-
onSessionRenewed: (discussion: Discussion) => {
|
|
327
|
-
this.eventEmitter.emit('sessionRenewed', discussion);
|
|
328
|
-
},
|
|
329
|
-
onError: (error: Error, context: string) => {
|
|
330
|
-
this.eventEmitter.emit('error', error, context);
|
|
331
|
-
},
|
|
332
|
-
// Auto-renewal: when session is lost, automatically renew it
|
|
333
|
-
onSessionRenewalNeeded: (contactUserId: string) => {
|
|
334
|
-
console.log('[GossipSdk] Session renewal needed for', contactUserId);
|
|
335
|
-
this.handleSessionRenewal(contactUserId);
|
|
336
|
-
},
|
|
337
|
-
// Auto-accept: when peer sent us an announcement, accept/respond to establish session
|
|
338
|
-
onSessionAcceptNeeded: (contactUserId: string) => {
|
|
339
|
-
console.log('[GossipSdk] Session accept needed for', contactUserId);
|
|
340
|
-
this.handleSessionAccept(contactUserId);
|
|
341
|
-
},
|
|
342
|
-
// Session became active: peer accepted our announcement, process waiting messages
|
|
343
|
-
onSessionBecameActive: (contactUserId: string) => {
|
|
344
|
-
console.log('[GossipSdk] Session became active for', contactUserId);
|
|
345
|
-
this.handleSessionBecameActive(contactUserId);
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// Get config from initialized state
|
|
350
|
-
const { config } = this.state;
|
|
351
|
-
|
|
352
|
-
// Create services with config
|
|
353
|
-
this._announcement = new AnnouncementService(
|
|
354
|
-
db,
|
|
355
|
-
messageProtocol,
|
|
356
|
-
session,
|
|
357
|
-
serviceEvents,
|
|
358
|
-
config
|
|
359
|
-
);
|
|
360
|
-
this._discussion = new DiscussionService(
|
|
361
|
-
db,
|
|
362
|
-
this._announcement,
|
|
363
|
-
session,
|
|
364
|
-
serviceEvents
|
|
365
|
-
);
|
|
366
|
-
this._message = new MessageService(
|
|
367
|
-
db,
|
|
368
|
-
messageProtocol,
|
|
369
|
-
session,
|
|
370
|
-
this._discussion,
|
|
371
|
-
serviceEvents,
|
|
372
|
-
config
|
|
373
|
-
);
|
|
374
|
-
this._refresh = new RefreshService(
|
|
375
|
-
db,
|
|
376
|
-
this._message,
|
|
377
|
-
session,
|
|
378
|
-
serviceEvents
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
// Reset any messages stuck in SENDING status to FAILED
|
|
382
|
-
// This handles app crash/close during message send
|
|
383
|
-
await this.resetStuckSendingMessages(db);
|
|
384
|
-
|
|
385
|
-
this.state = {
|
|
386
|
-
status: 'session_open',
|
|
387
|
-
db,
|
|
388
|
-
messageProtocol,
|
|
389
|
-
config,
|
|
390
|
-
session,
|
|
391
|
-
userKeys,
|
|
392
|
-
persistEncryptionKey: options.persistEncryptionKey,
|
|
393
|
-
onPersist: options.onPersist,
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
// Create cached service API wrappers
|
|
397
|
-
this.createServiceAPIWrappers(db, session);
|
|
398
|
-
|
|
399
|
-
// Auto-start polling if enabled in config
|
|
400
|
-
if (config.polling.enabled) {
|
|
401
|
-
this.startPolling();
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Create cached service API wrappers.
|
|
407
|
-
* Called once during openSession to avoid creating new objects on each getter access.
|
|
408
|
-
*/
|
|
409
|
-
private createServiceAPIWrappers(
|
|
410
|
-
db: GossipDatabase,
|
|
411
|
-
session: SessionModule
|
|
412
|
-
): void {
|
|
413
|
-
this._messagesAPI = {
|
|
414
|
-
send: message =>
|
|
415
|
-
this.messageQueues.enqueue(message.contactUserId, () =>
|
|
416
|
-
this._message!.sendMessage(message)
|
|
417
|
-
),
|
|
418
|
-
fetch: () => this._message!.fetchMessages(),
|
|
419
|
-
resend: async messages => {
|
|
420
|
-
const promises: Promise<void>[] = [];
|
|
421
|
-
for (const [contactId, contactMessages] of messages.entries()) {
|
|
422
|
-
const singleContactMap = new Map([[contactId, contactMessages]]);
|
|
423
|
-
promises.push(
|
|
424
|
-
this.messageQueues.enqueue(contactId, () =>
|
|
425
|
-
this._message!.resendMessages(singleContactMap)
|
|
426
|
-
)
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
await Promise.all(promises);
|
|
430
|
-
},
|
|
431
|
-
findBySeeker: (seeker, ownerUserId) =>
|
|
432
|
-
this._message!.findMessageBySeeker(seeker, ownerUserId),
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
this._discussionsAPI = {
|
|
436
|
-
start: (contact, message) =>
|
|
437
|
-
this._discussion!.initialize(contact, message),
|
|
438
|
-
accept: discussion => this._discussion!.accept(discussion),
|
|
439
|
-
renew: contactUserId => this._discussion!.renew(contactUserId),
|
|
440
|
-
isStable: (ownerUserId, contactUserId) =>
|
|
441
|
-
this._discussion!.isStableState(ownerUserId, contactUserId),
|
|
442
|
-
list: ownerUserId => db.getDiscussionsByOwner(ownerUserId),
|
|
443
|
-
get: (ownerUserId, contactUserId) =>
|
|
444
|
-
db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId),
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
this._announcementsAPI = {
|
|
448
|
-
fetch: () => this._announcement!.fetchAndProcessAnnouncements(),
|
|
449
|
-
resend: failedDiscussions =>
|
|
450
|
-
this._announcement!.resendAnnouncements(failedDiscussions),
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
this._contactsAPI = {
|
|
454
|
-
list: ownerUserId => getContacts(ownerUserId, db),
|
|
455
|
-
get: (ownerUserId, contactUserId) =>
|
|
456
|
-
getContact(ownerUserId, contactUserId, db),
|
|
457
|
-
add: (ownerUserId, userId, name, publicKeys) =>
|
|
458
|
-
addContact(ownerUserId, userId, name, publicKeys, db),
|
|
459
|
-
updateName: (ownerUserId, contactUserId, newName) =>
|
|
460
|
-
updateContactName(ownerUserId, contactUserId, newName, db),
|
|
461
|
-
delete: (ownerUserId, contactUserId) =>
|
|
462
|
-
deleteContact(ownerUserId, contactUserId, db, session),
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
this._refreshAPI = {
|
|
466
|
-
handleSessionRefresh: activeDiscussions =>
|
|
467
|
-
this._refresh!.handleSessionRefresh(activeDiscussions),
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Close the current session (logout).
|
|
473
|
-
*/
|
|
474
|
-
async closeSession(): Promise<void> {
|
|
475
|
-
if (this.state.status !== 'session_open') {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Stop polling first
|
|
480
|
-
this.pollingManager.stop();
|
|
481
|
-
|
|
482
|
-
// Cleanup session
|
|
483
|
-
this.state.session.cleanup();
|
|
484
|
-
|
|
485
|
-
// Clear services
|
|
486
|
-
this._announcement = null;
|
|
487
|
-
this._discussion = null;
|
|
488
|
-
this._message = null;
|
|
489
|
-
this._refresh = null;
|
|
490
|
-
|
|
491
|
-
// Clear cached API wrappers
|
|
492
|
-
this._messagesAPI = null;
|
|
493
|
-
this._discussionsAPI = null;
|
|
494
|
-
this._announcementsAPI = null;
|
|
495
|
-
this._contactsAPI = null;
|
|
496
|
-
this._refreshAPI = null;
|
|
497
|
-
|
|
498
|
-
// Clear message queues
|
|
499
|
-
this.messageQueues.clear();
|
|
500
|
-
|
|
501
|
-
// Reset to initialized state
|
|
502
|
-
this.state = {
|
|
503
|
-
status: 'initialized',
|
|
504
|
-
db: this.state.db,
|
|
505
|
-
messageProtocol: this.state.messageProtocol,
|
|
506
|
-
config: this.state.config,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// ─────────────────────────────────────────────────────────────────
|
|
511
|
-
// Session Info
|
|
512
|
-
// ─────────────────────────────────────────────────────────────────
|
|
513
|
-
|
|
514
|
-
/** Current user ID (encoded). Throws if no session is open. */
|
|
515
|
-
get userId(): string {
|
|
516
|
-
const state = this.requireSession();
|
|
517
|
-
return state.session.userIdEncoded;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/** Current user ID (raw bytes). Throws if no session is open. */
|
|
521
|
-
get userIdBytes(): Uint8Array {
|
|
522
|
-
const state = this.requireSession();
|
|
523
|
-
return state.session.userId;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** User's public keys. Throws if no session is open. */
|
|
527
|
-
get publicKeys(): UserPublicKeys {
|
|
528
|
-
const state = this.requireSession();
|
|
529
|
-
return state.session.ourPk;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/** Whether a session is currently open */
|
|
533
|
-
get isSessionOpen(): boolean {
|
|
534
|
-
return this.state.status === 'session_open';
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/** Whether SDK is initialized */
|
|
538
|
-
get isInitialized(): boolean {
|
|
539
|
-
return this.state.status !== 'uninitialized';
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Get encrypted session blob for persistence.
|
|
544
|
-
* Throws if no session is open.
|
|
545
|
-
*/
|
|
546
|
-
getEncryptedSession(encryptionKey: EncryptionKey): Uint8Array {
|
|
547
|
-
const state = this.requireSession();
|
|
548
|
-
return state.session.toEncryptedBlob(encryptionKey);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Configure session persistence after session is opened.
|
|
553
|
-
* Use this when you need to set up persistence after account creation.
|
|
554
|
-
*
|
|
555
|
-
* @param encryptionKey - Key to encrypt session blob
|
|
556
|
-
* @param onPersist - Callback to save encrypted session blob
|
|
557
|
-
*/
|
|
558
|
-
configurePersistence(
|
|
559
|
-
encryptionKey: EncryptionKey,
|
|
560
|
-
onPersist: (
|
|
561
|
-
encryptedBlob: Uint8Array,
|
|
562
|
-
encryptionKey: EncryptionKey
|
|
563
|
-
) => Promise<void>
|
|
564
|
-
): void {
|
|
565
|
-
if (this.state.status !== 'session_open') {
|
|
566
|
-
throw new Error('No session open. Call openSession() first.');
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Update state with persistence config
|
|
570
|
-
this.state = {
|
|
571
|
-
...this.state,
|
|
572
|
-
persistEncryptionKey: encryptionKey,
|
|
573
|
-
onPersist,
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
console.log('[GossipSdk] Session persistence configured');
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// ─────────────────────────────────────────────────────────────────
|
|
580
|
-
// Services (accessible only when session is open)
|
|
581
|
-
// ─────────────────────────────────────────────────────────────────
|
|
582
|
-
|
|
583
|
-
/** Auth service (available after init, before session) */
|
|
584
|
-
get auth(): AuthService {
|
|
585
|
-
if (!this._auth) {
|
|
586
|
-
throw new Error('SDK not initialized');
|
|
587
|
-
}
|
|
588
|
-
return this._auth;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/** Message service */
|
|
592
|
-
get messages(): MessageServiceAPI {
|
|
593
|
-
this.requireSession();
|
|
594
|
-
if (!this._messagesAPI) {
|
|
595
|
-
throw new Error('Messages API not initialized');
|
|
596
|
-
}
|
|
597
|
-
return this._messagesAPI;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** Discussion service */
|
|
601
|
-
get discussions(): DiscussionServiceAPI {
|
|
602
|
-
this.requireSession();
|
|
603
|
-
if (!this._discussionsAPI) {
|
|
604
|
-
throw new Error('Discussions API not initialized');
|
|
605
|
-
}
|
|
606
|
-
return this._discussionsAPI;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/** Announcement service */
|
|
610
|
-
get announcements(): AnnouncementServiceAPI {
|
|
611
|
-
this.requireSession();
|
|
612
|
-
if (!this._announcementsAPI) {
|
|
613
|
-
throw new Error('Announcements API not initialized');
|
|
614
|
-
}
|
|
615
|
-
return this._announcementsAPI;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/** Contact management */
|
|
619
|
-
get contacts(): ContactsAPI {
|
|
620
|
-
this.requireSession();
|
|
621
|
-
if (!this._contactsAPI) {
|
|
622
|
-
throw new Error('Contacts API not initialized');
|
|
623
|
-
}
|
|
624
|
-
return this._contactsAPI;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** Refresh/sync service */
|
|
628
|
-
get refresh(): RefreshServiceAPI {
|
|
629
|
-
this.requireSession();
|
|
630
|
-
if (!this._refreshAPI) {
|
|
631
|
-
throw new Error('Refresh API not initialized');
|
|
632
|
-
}
|
|
633
|
-
return this._refreshAPI;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/** Utility functions */
|
|
637
|
-
get utils(): SdkUtils {
|
|
638
|
-
return {
|
|
639
|
-
validateUserId: validateUserIdFormat,
|
|
640
|
-
validateUsername: validateUsernameFormat,
|
|
641
|
-
encodeUserId,
|
|
642
|
-
decodeUserId,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/** Current SDK configuration (read-only) */
|
|
647
|
-
get config(): SdkConfig {
|
|
648
|
-
if (this.state.status === 'uninitialized') {
|
|
649
|
-
return defaultSdkConfig;
|
|
650
|
-
}
|
|
651
|
-
return this.state.config;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/** Polling control API */
|
|
655
|
-
get polling(): PollingAPI {
|
|
656
|
-
return {
|
|
657
|
-
start: () => this.startPolling(),
|
|
658
|
-
stop: () => this.pollingManager.stop(),
|
|
659
|
-
isRunning: this.pollingManager.isRunning(),
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// ─────────────────────────────────────────────────────────────────
|
|
664
|
-
// Polling
|
|
665
|
-
// ─────────────────────────────────────────────────────────────────
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Start polling for messages, announcements, and session refresh.
|
|
669
|
-
* Uses intervals from config.polling.
|
|
670
|
-
*/
|
|
671
|
-
private startPolling(): void {
|
|
672
|
-
if (this.state.status !== 'session_open') {
|
|
673
|
-
console.warn('[GossipSdk] Cannot start polling - no session open');
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const { config, db, session } = this.state;
|
|
678
|
-
|
|
679
|
-
this.pollingManager.start(config, {
|
|
680
|
-
fetchMessages: async () => {
|
|
681
|
-
await this._message?.fetchMessages();
|
|
682
|
-
},
|
|
683
|
-
fetchAnnouncements: async () => {
|
|
684
|
-
await this._announcement?.fetchAndProcessAnnouncements();
|
|
685
|
-
},
|
|
686
|
-
handleSessionRefresh: async discussions => {
|
|
687
|
-
await this._refresh?.handleSessionRefresh(discussions);
|
|
688
|
-
},
|
|
689
|
-
getActiveDiscussions: async () => {
|
|
690
|
-
return db.getActiveDiscussionsByOwner(session.userIdEncoded);
|
|
691
|
-
},
|
|
692
|
-
onError: (error, context) => {
|
|
693
|
-
this.eventEmitter.emit('error', error, context);
|
|
694
|
-
},
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// ─────────────────────────────────────────────────────────────────
|
|
699
|
-
// Events
|
|
700
|
-
// ─────────────────────────────────────────────────────────────────
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Register an event handler
|
|
704
|
-
*/
|
|
705
|
-
on<K extends SdkEventType>(event: K, handler: SdkEventHandlers[K]): void {
|
|
706
|
-
this.eventEmitter.on(event, handler);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Remove an event handler
|
|
711
|
-
*/
|
|
712
|
-
off<K extends SdkEventType>(event: K, handler: SdkEventHandlers[K]): void {
|
|
713
|
-
this.eventEmitter.off(event, handler);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// ─────────────────────────────────────────────────────────────────
|
|
717
|
-
// Private Helpers
|
|
718
|
-
// ─────────────────────────────────────────────────────────────────
|
|
719
|
-
|
|
720
|
-
private requireSession(): SdkStateSessionOpen {
|
|
721
|
-
if (this.state.status !== 'session_open') {
|
|
722
|
-
throw new Error('No session open. Call openSession() first.');
|
|
723
|
-
}
|
|
724
|
-
return this.state;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Handle automatic session renewal when session is lost.
|
|
729
|
-
* Called by onSessionRenewalNeeded event.
|
|
730
|
-
*/
|
|
731
|
-
private async handleSessionRenewal(contactUserId: string): Promise<void> {
|
|
732
|
-
if (this.state.status !== 'session_open') return;
|
|
733
|
-
|
|
734
|
-
try {
|
|
735
|
-
await this._discussion!.renew(contactUserId);
|
|
736
|
-
console.log('[GossipSdk] Session renewed for', contactUserId);
|
|
737
|
-
|
|
738
|
-
// After successful renewal, process any waiting messages
|
|
739
|
-
const sentCount =
|
|
740
|
-
await this._message!.processWaitingMessages(contactUserId);
|
|
741
|
-
if (sentCount > 0) {
|
|
742
|
-
console.log(
|
|
743
|
-
`[GossipSdk] Sent ${sentCount} waiting messages after renewal`
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
} catch (error) {
|
|
747
|
-
console.error('[GossipSdk] Session renewal failed:', error);
|
|
748
|
-
this.eventEmitter.emit(
|
|
749
|
-
'error',
|
|
750
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
751
|
-
'session_renewal'
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* Handle automatic session accept when peer has sent us an announcement.
|
|
758
|
-
* Called by onSessionAcceptNeeded event.
|
|
759
|
-
* This is different from renewal - we respond to their session request.
|
|
760
|
-
*/
|
|
761
|
-
private async handleSessionAccept(contactUserId: string): Promise<void> {
|
|
762
|
-
if (this.state.status !== 'session_open') return;
|
|
763
|
-
|
|
764
|
-
try {
|
|
765
|
-
const ownerUserId = this.state.session.userIdEncoded;
|
|
766
|
-
const discussion = await this.state.db.getDiscussionByOwnerAndContact(
|
|
767
|
-
ownerUserId,
|
|
768
|
-
contactUserId
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
if (!discussion) {
|
|
772
|
-
console.warn(
|
|
773
|
-
'[GossipSdk] No discussion found for accept, contactUserId:',
|
|
774
|
-
contactUserId
|
|
775
|
-
);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// Accept the discussion (sends our announcement back to establish session)
|
|
780
|
-
await this._discussion!.accept(discussion);
|
|
781
|
-
console.log('[GossipSdk] Session accepted for', contactUserId);
|
|
782
|
-
|
|
783
|
-
// After successful accept, process any waiting messages
|
|
784
|
-
const sentCount =
|
|
785
|
-
await this._message!.processWaitingMessages(contactUserId);
|
|
786
|
-
if (sentCount > 0) {
|
|
787
|
-
console.log(
|
|
788
|
-
`[GossipSdk] Sent ${sentCount} waiting messages after accept`
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
} catch (error) {
|
|
792
|
-
console.error('[GossipSdk] Session accept failed:', error);
|
|
793
|
-
this.eventEmitter.emit(
|
|
794
|
-
'error',
|
|
795
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
796
|
-
'session_accept'
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Handle session becoming Active after peer accepts our announcement.
|
|
803
|
-
* Called by onSessionBecameActive event.
|
|
804
|
-
*
|
|
805
|
-
* This is different from handleSessionAccept:
|
|
806
|
-
* - handleSessionAccept: WE accept a session (peer initiated)
|
|
807
|
-
* - handleSessionBecameActive: PEER accepts our session (we initiated)
|
|
808
|
-
*/
|
|
809
|
-
private async handleSessionBecameActive(
|
|
810
|
-
contactUserId: string
|
|
811
|
-
): Promise<void> {
|
|
812
|
-
if (this.state.status !== 'session_open') return;
|
|
813
|
-
|
|
814
|
-
try {
|
|
815
|
-
// Process any messages that were queued as WAITING_SESSION
|
|
816
|
-
const sentCount =
|
|
817
|
-
await this._message!.processWaitingMessages(contactUserId);
|
|
818
|
-
if (sentCount > 0) {
|
|
819
|
-
console.log(
|
|
820
|
-
`[GossipSdk] Sent ${sentCount} waiting messages after session became active`
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
} catch (error) {
|
|
824
|
-
console.error('[GossipSdk] Processing waiting messages failed:', error);
|
|
825
|
-
this.eventEmitter.emit(
|
|
826
|
-
'error',
|
|
827
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
828
|
-
'session_became_active'
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
private async handleSessionPersist(): Promise<void> {
|
|
834
|
-
if (this.state.status !== 'session_open') return;
|
|
835
|
-
|
|
836
|
-
const { onPersist, persistEncryptionKey, session } = this.state;
|
|
837
|
-
if (!onPersist || !persistEncryptionKey) return;
|
|
838
|
-
|
|
839
|
-
try {
|
|
840
|
-
const blob = session.toEncryptedBlob(persistEncryptionKey);
|
|
841
|
-
console.log(
|
|
842
|
-
`[SessionPersist] Saving session blob (${blob.length} bytes)`
|
|
843
|
-
);
|
|
844
|
-
await onPersist(blob, persistEncryptionKey);
|
|
845
|
-
} catch (error) {
|
|
846
|
-
console.error('[GossipSdk] Session persistence failed:', error);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* Reset any messages stuck in SENDING status to FAILED.
|
|
852
|
-
* This handles the case where the app crashed or was closed during message send.
|
|
853
|
-
* Per spec: SENDING should never be persisted - if we find it on startup, it failed.
|
|
854
|
-
*/
|
|
855
|
-
/**
|
|
856
|
-
* Reset messages stuck in SENDING status to WAITING_SESSION.
|
|
857
|
-
*
|
|
858
|
-
* Per spec: SENDING is a transient state that should never be persisted.
|
|
859
|
-
* If the app crashes/closes during a send, the message would be stuck forever.
|
|
860
|
-
*
|
|
861
|
-
* By resetting to WAITING_SESSION:
|
|
862
|
-
* - Message will be re-encrypted with current session keys
|
|
863
|
-
* - Message will be automatically sent when session is active
|
|
864
|
-
* - No manual user intervention required
|
|
865
|
-
*
|
|
866
|
-
* We also clear encryptedMessage and seeker since they may be stale.
|
|
867
|
-
*/
|
|
868
|
-
private async resetStuckSendingMessages(db: GossipDatabase): Promise<void> {
|
|
869
|
-
try {
|
|
870
|
-
const count = await db.messages
|
|
871
|
-
.where('status')
|
|
872
|
-
.equals(MessageStatus.SENDING)
|
|
873
|
-
.modify({
|
|
874
|
-
status: MessageStatus.WAITING_SESSION,
|
|
875
|
-
encryptedMessage: undefined,
|
|
876
|
-
seeker: undefined,
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
if (count > 0) {
|
|
880
|
-
console.log(
|
|
881
|
-
`[GossipSdk] Reset ${count} stuck SENDING message(s) to WAITING_SESSION for auto-retry`
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
} catch (error) {
|
|
885
|
-
console.error('[GossipSdk] Failed to reset stuck messages:', error);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
891
|
-
// Service API Types
|
|
892
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
893
|
-
|
|
894
|
-
interface MessageServiceAPI {
|
|
895
|
-
/** Send a message */
|
|
896
|
-
send(message: Omit<Message, 'id'>): Promise<SendMessageResult>;
|
|
897
|
-
/** Fetch and decrypt messages from the protocol */
|
|
898
|
-
fetch(): Promise<MessageResult>;
|
|
899
|
-
/** Resend failed messages */
|
|
900
|
-
resend(messages: Map<string, Message[]>): Promise<void>;
|
|
901
|
-
/** Find a message by its seeker */
|
|
902
|
-
findBySeeker(
|
|
903
|
-
seeker: Uint8Array,
|
|
904
|
-
ownerUserId: string
|
|
905
|
-
): Promise<Message | undefined>;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
interface DiscussionServiceAPI {
|
|
909
|
-
/** Start a new discussion with a contact */
|
|
910
|
-
start(
|
|
911
|
-
contact: Contact,
|
|
912
|
-
message?: string
|
|
913
|
-
): Promise<{ discussionId: number; announcement: Uint8Array }>;
|
|
914
|
-
/** Accept an incoming discussion request */
|
|
915
|
-
accept(discussion: Discussion): Promise<void>;
|
|
916
|
-
/** Renew a broken discussion */
|
|
917
|
-
renew(contactUserId: string): Promise<void>;
|
|
918
|
-
/** Check if a discussion is in a stable state */
|
|
919
|
-
isStable(ownerUserId: string, contactUserId: string): Promise<boolean>;
|
|
920
|
-
/** List all discussions for the owner */
|
|
921
|
-
list(ownerUserId: string): Promise<Discussion[]>;
|
|
922
|
-
/** Get a specific discussion */
|
|
923
|
-
get(
|
|
924
|
-
ownerUserId: string,
|
|
925
|
-
contactUserId: string
|
|
926
|
-
): Promise<Discussion | undefined>;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
interface AnnouncementServiceAPI {
|
|
930
|
-
/** Fetch and process announcements from the protocol */
|
|
931
|
-
fetch(): Promise<AnnouncementReceptionResult>;
|
|
932
|
-
/** Resend failed announcements */
|
|
933
|
-
resend(failedDiscussions: Discussion[]): Promise<void>;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
interface ContactsAPI {
|
|
937
|
-
/** List all contacts for the owner */
|
|
938
|
-
list(ownerUserId: string): Promise<Contact[]>;
|
|
939
|
-
/** Get a specific contact */
|
|
940
|
-
get(ownerUserId: string, contactUserId: string): Promise<Contact | null>;
|
|
941
|
-
/** Add a new contact */
|
|
942
|
-
add(
|
|
943
|
-
ownerUserId: string,
|
|
944
|
-
userId: string,
|
|
945
|
-
name: string,
|
|
946
|
-
publicKeys: UserPublicKeys
|
|
947
|
-
): Promise<{ success: boolean; error?: string; contact?: Contact }>;
|
|
948
|
-
/** Update a contact's name */
|
|
949
|
-
updateName(
|
|
950
|
-
ownerUserId: string,
|
|
951
|
-
contactUserId: string,
|
|
952
|
-
newName: string
|
|
953
|
-
): Promise<UpdateContactNameResult>;
|
|
954
|
-
/** Delete a contact and all related data */
|
|
955
|
-
delete(
|
|
956
|
-
ownerUserId: string,
|
|
957
|
-
contactUserId: string
|
|
958
|
-
): Promise<DeleteContactResult>;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
interface RefreshServiceAPI {
|
|
962
|
-
/** Handle session refresh (keep-alive, broken sessions, etc.) */
|
|
963
|
-
handleSessionRefresh(activeDiscussions: Discussion[]): Promise<void>;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
interface SdkUtils {
|
|
967
|
-
/** Validate a user ID format */
|
|
968
|
-
validateUserId(userId: string): ValidationResult;
|
|
969
|
-
/** Validate a username format */
|
|
970
|
-
validateUsername(username: string): ValidationResult;
|
|
971
|
-
/** Encode raw bytes to user ID string */
|
|
972
|
-
encodeUserId(rawId: Uint8Array): string;
|
|
973
|
-
/** Decode user ID string to raw bytes */
|
|
974
|
-
decodeUserId(encodedId: string): Uint8Array;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
interface PollingAPI {
|
|
978
|
-
/** Start polling for messages, announcements, and session refresh */
|
|
979
|
-
start(): void;
|
|
980
|
-
/** Stop all polling */
|
|
981
|
-
stop(): void;
|
|
982
|
-
/** Whether polling is currently running */
|
|
983
|
-
isRunning: boolean;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
987
|
-
// Singleton Export
|
|
988
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
989
|
-
|
|
990
|
-
/** The singleton GossipSdk instance */
|
|
991
|
-
export const gossipSdk = new GossipSdkImpl();
|
|
992
|
-
|
|
993
|
-
// Also export the class for testing
|
|
994
|
-
export { GossipSdkImpl };
|