@massalabs/gossip-sdk 0.0.1
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 +484 -0
- package/package.json +41 -0
- package/src/api/messageProtocol/index.ts +53 -0
- package/src/api/messageProtocol/mock.ts +13 -0
- package/src/api/messageProtocol/rest.ts +209 -0
- package/src/api/messageProtocol/types.ts +70 -0
- package/src/config/protocol.ts +97 -0
- package/src/config/sdk.ts +131 -0
- package/src/contacts.ts +210 -0
- package/src/core/SdkEventEmitter.ts +91 -0
- package/src/core/SdkPolling.ts +134 -0
- package/src/core/index.ts +9 -0
- package/src/crypto/bip39.ts +84 -0
- package/src/crypto/encryption.ts +77 -0
- package/src/db.ts +465 -0
- package/src/gossipSdk.ts +994 -0
- package/src/index.ts +211 -0
- package/src/services/announcement.ts +653 -0
- package/src/services/auth.ts +95 -0
- package/src/services/discussion.ts +380 -0
- package/src/services/message.ts +1055 -0
- package/src/services/refresh.ts +234 -0
- package/src/sw.ts +17 -0
- package/src/types/events.ts +108 -0
- package/src/types.ts +70 -0
- package/src/utils/base64.ts +39 -0
- package/src/utils/contacts.ts +161 -0
- package/src/utils/discussions.ts +55 -0
- package/src/utils/logs.ts +86 -0
- package/src/utils/messageSerialization.ts +257 -0
- package/src/utils/queue.ts +106 -0
- package/src/utils/type.ts +7 -0
- package/src/utils/userId.ts +114 -0
- package/src/utils/validation.ts +144 -0
- package/src/utils.ts +47 -0
- package/src/wasm/encryption.ts +108 -0
- package/src/wasm/index.ts +20 -0
- package/src/wasm/loader.ts +123 -0
- package/src/wasm/session.ts +276 -0
- package/src/wasm/userKeys.ts +31 -0
- package/test/config/protocol.spec.ts +31 -0
- package/test/config/sdk.spec.ts +163 -0
- package/test/db/helpers.spec.ts +142 -0
- package/test/db/operations.spec.ts +128 -0
- package/test/db/states.spec.ts +535 -0
- package/test/integration/discussion-flow.spec.ts +422 -0
- package/test/integration/messaging-flow.spec.ts +708 -0
- package/test/integration/sdk-lifecycle.spec.ts +325 -0
- package/test/mocks/index.ts +9 -0
- package/test/mocks/mockMessageProtocol.ts +100 -0
- package/test/services/auth.spec.ts +311 -0
- package/test/services/discussion.spec.ts +279 -0
- package/test/services/message-deduplication.spec.ts +299 -0
- package/test/services/message-startup.spec.ts +331 -0
- package/test/services/message.spec.ts +817 -0
- package/test/services/refresh.spec.ts +199 -0
- package/test/services/session-status.spec.ts +349 -0
- package/test/session/wasm.spec.ts +227 -0
- package/test/setup.ts +52 -0
- package/test/utils/contacts.spec.ts +156 -0
- package/test/utils/discussions.spec.ts +66 -0
- package/test/utils/queue.spec.ts +52 -0
- package/test/utils/serialization.spec.ts +120 -0
- package/test/utils/userId.spec.ts +120 -0
- package/test/utils/validation.spec.ts +223 -0
- package/test/utils.ts +212 -0
- package/tsconfig.json +26 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +28 -0
package/src/db.ts
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gossip Database
|
|
3
|
+
*
|
|
4
|
+
* IndexedDB database implementation using Dexie for the Gossip messenger.
|
|
5
|
+
* Provides tables for contacts, messages, discussions, user profiles, and more.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Dexie, { Table } from 'dexie';
|
|
9
|
+
|
|
10
|
+
// Define authentication method type
|
|
11
|
+
export type AuthMethod = 'capacitor' | 'webauthn' | 'password';
|
|
12
|
+
|
|
13
|
+
// Define interfaces for data models
|
|
14
|
+
export interface Contact {
|
|
15
|
+
id?: number;
|
|
16
|
+
ownerUserId: string; // The current user's userId owning this contact
|
|
17
|
+
userId: string; // 32-byte user ID (gossip Bech32 encoded) - primary key
|
|
18
|
+
name: string;
|
|
19
|
+
avatar?: string;
|
|
20
|
+
publicKeys: Uint8Array; // Serialized UserPublicKeys bytes (from UserPublicKeys.to_bytes())
|
|
21
|
+
isOnline: boolean;
|
|
22
|
+
lastSeen: Date;
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Message {
|
|
27
|
+
id?: number;
|
|
28
|
+
ownerUserId: string; // The current user's userId owning this message
|
|
29
|
+
contactUserId: string; // Reference to Contact.userId
|
|
30
|
+
content: string;
|
|
31
|
+
serializedContent?: Uint8Array; // Serialized message content
|
|
32
|
+
type: MessageType;
|
|
33
|
+
direction: MessageDirection;
|
|
34
|
+
status: MessageStatus;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
seeker?: Uint8Array; // Seeker for this message (stored when sending or receiving)
|
|
38
|
+
replyTo?: {
|
|
39
|
+
originalContent?: string;
|
|
40
|
+
originalSeeker: Uint8Array; // Seeker of the original message (required for replies)
|
|
41
|
+
};
|
|
42
|
+
forwardOf?: {
|
|
43
|
+
originalContent?: string;
|
|
44
|
+
originalSeeker: Uint8Array;
|
|
45
|
+
};
|
|
46
|
+
encryptedMessage?: Uint8Array; // Ciphertext of the message
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UserProfile {
|
|
50
|
+
userId: string; // 32-byte user ID (gossip Bech32 encoded) - primary key
|
|
51
|
+
username: string;
|
|
52
|
+
avatar?: string;
|
|
53
|
+
security: {
|
|
54
|
+
encKeySalt: Uint8Array;
|
|
55
|
+
|
|
56
|
+
// Authentication method used to create the account
|
|
57
|
+
authMethod: AuthMethod;
|
|
58
|
+
|
|
59
|
+
// WebAuthn/FIDO2 (biometric) details when used
|
|
60
|
+
webauthn?: {
|
|
61
|
+
credentialId?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// iCloud Keychain sync preference (iOS only)
|
|
65
|
+
iCloudSync?: boolean;
|
|
66
|
+
|
|
67
|
+
// Mnemonic backup details
|
|
68
|
+
mnemonicBackup: {
|
|
69
|
+
encryptedMnemonic: Uint8Array;
|
|
70
|
+
createdAt: Date;
|
|
71
|
+
backedUp: boolean;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
session: Uint8Array;
|
|
75
|
+
bio?: string;
|
|
76
|
+
status: 'online' | 'away' | 'busy' | 'offline';
|
|
77
|
+
lastSeen: Date;
|
|
78
|
+
createdAt: Date;
|
|
79
|
+
updatedAt: Date;
|
|
80
|
+
lastPublicKeyPush?: Date;
|
|
81
|
+
lastBulletinCounter?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Unified discussion interface combining protocol state and UI metadata
|
|
85
|
+
|
|
86
|
+
export enum DiscussionStatus {
|
|
87
|
+
PENDING = 'pending',
|
|
88
|
+
ACTIVE = 'active',
|
|
89
|
+
CLOSED = 'closed', // closed by the user
|
|
90
|
+
BROKEN = 'broken', // The session is killed. Need to be reinitiated
|
|
91
|
+
SEND_FAILED = 'sendFailed', // The discussion was initiated by the session manager but could not be broadcasted on network
|
|
92
|
+
RECONNECTING = 'reconnecting', // Session recovery in progress, waiting for peer's response
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export enum MessageDirection {
|
|
96
|
+
INCOMING = 'incoming',
|
|
97
|
+
OUTGOING = 'outgoing',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export enum MessageStatus {
|
|
101
|
+
WAITING_SESSION = 'waiting_session', // Waiting for active session with peer
|
|
102
|
+
SENDING = 'sending',
|
|
103
|
+
SENT = 'sent',
|
|
104
|
+
DELIVERED = 'delivered',
|
|
105
|
+
READ = 'read',
|
|
106
|
+
FAILED = 'failed', // Only for unrecoverable errors (network down, blocked, etc.)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export enum DiscussionDirection {
|
|
110
|
+
INITIATED = 'initiated',
|
|
111
|
+
RECEIVED = 'received',
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export enum MessageType {
|
|
115
|
+
TEXT = 'text',
|
|
116
|
+
KEEP_ALIVE = 'keep_alive',
|
|
117
|
+
IMAGE = 'image',
|
|
118
|
+
FILE = 'file',
|
|
119
|
+
AUDIO = 'audio',
|
|
120
|
+
VIDEO = 'video',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface Discussion {
|
|
124
|
+
id?: number;
|
|
125
|
+
ownerUserId: string; // The current user's userId owning this discussion
|
|
126
|
+
contactUserId: string; // Reference to Contact.userId - unique per contact
|
|
127
|
+
|
|
128
|
+
// Protocol/Encryption fields
|
|
129
|
+
direction: DiscussionDirection; // Whether this user initiated or received the discussion
|
|
130
|
+
status: DiscussionStatus;
|
|
131
|
+
nextSeeker?: Uint8Array; // The next seeker for sending messages (from SendMessageOutput)
|
|
132
|
+
initiationAnnouncement?: Uint8Array; // Outgoing announcement bytes when we initiate
|
|
133
|
+
announcementMessage?: string; // Optional message from incoming announcement (user_data)
|
|
134
|
+
lastSyncTimestamp?: Date; // Last time messages were synced from protocol
|
|
135
|
+
|
|
136
|
+
// UI/Display fields
|
|
137
|
+
customName?: string; // Optional custom name for the discussion (overrides contact name)
|
|
138
|
+
lastMessageId?: number;
|
|
139
|
+
lastMessageContent?: string;
|
|
140
|
+
lastMessageTimestamp?: Date;
|
|
141
|
+
unreadCount: number;
|
|
142
|
+
|
|
143
|
+
// Timestamps
|
|
144
|
+
createdAt: Date;
|
|
145
|
+
updatedAt: Date;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface PendingEncryptedMessage {
|
|
149
|
+
id?: number;
|
|
150
|
+
seeker: Uint8Array;
|
|
151
|
+
ciphertext: Uint8Array;
|
|
152
|
+
fetchedAt: Date;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface PendingAnnouncement {
|
|
156
|
+
id?: number;
|
|
157
|
+
announcement: Uint8Array;
|
|
158
|
+
fetchedAt: Date;
|
|
159
|
+
counter?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ActiveSeeker {
|
|
163
|
+
id?: number;
|
|
164
|
+
seeker: Uint8Array;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Define the database class
|
|
168
|
+
export class GossipDatabase extends Dexie {
|
|
169
|
+
// Define tables
|
|
170
|
+
contacts!: Table<Contact>;
|
|
171
|
+
messages!: Table<Message>;
|
|
172
|
+
userProfile!: Table<UserProfile>;
|
|
173
|
+
discussions!: Table<Discussion>;
|
|
174
|
+
pendingEncryptedMessages!: Table<PendingEncryptedMessage>;
|
|
175
|
+
pendingAnnouncements!: Table<PendingAnnouncement>;
|
|
176
|
+
activeSeekers!: Table<ActiveSeeker>;
|
|
177
|
+
|
|
178
|
+
constructor() {
|
|
179
|
+
super('GossipDatabase');
|
|
180
|
+
|
|
181
|
+
this.version(13).stores({
|
|
182
|
+
contacts:
|
|
183
|
+
'++id, ownerUserId, userId, name, isOnline, lastSeen, createdAt, [ownerUserId+userId] , [ownerUserId+name]',
|
|
184
|
+
messages:
|
|
185
|
+
'++id, ownerUserId, contactUserId, type, direction, status, timestamp, seeker, [ownerUserId+contactUserId], [ownerUserId+status], [ownerUserId+contactUserId+status], [ownerUserId+seeker], [ownerUserId+contactUserId+direction], [ownerUserId+direction+status]',
|
|
186
|
+
userProfile: 'userId, username, status, lastSeen',
|
|
187
|
+
discussions:
|
|
188
|
+
'++id, ownerUserId, &[ownerUserId+contactUserId], status, [ownerUserId+status], lastSyncTimestamp, unreadCount, lastMessageTimestamp, createdAt, updatedAt',
|
|
189
|
+
pendingEncryptedMessages: '++id, fetchedAt, seeker',
|
|
190
|
+
pendingAnnouncements: '++id, fetchedAt, &announcement',
|
|
191
|
+
activeSeekers: '++id, seeker',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Add hooks for automatic timestamps
|
|
195
|
+
this.contacts.hook('creating', function (_primKey, obj, _trans) {
|
|
196
|
+
obj.createdAt = new Date();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.userProfile.hook('creating', function (_primKey, obj, _trans) {
|
|
200
|
+
obj.createdAt = new Date();
|
|
201
|
+
obj.updatedAt = new Date();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.userProfile.hook(
|
|
205
|
+
'updating',
|
|
206
|
+
function (modifications, _primKey, _obj, _trans) {
|
|
207
|
+
(modifications as Record<string, unknown>).updatedAt = new Date();
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
this.discussions.hook('creating', function (_primKey, obj, _trans) {
|
|
212
|
+
obj.createdAt = new Date();
|
|
213
|
+
obj.updatedAt = new Date();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
this.discussions.hook(
|
|
217
|
+
'updating',
|
|
218
|
+
function (modifications, _primKey, _obj, _trans) {
|
|
219
|
+
(modifications as Record<string, unknown>).updatedAt = new Date();
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Helper methods for common operations
|
|
225
|
+
|
|
226
|
+
/** CONTACTS */
|
|
227
|
+
async getContactsByOwner(ownerUserId: string): Promise<Contact[]> {
|
|
228
|
+
return await this.contacts
|
|
229
|
+
.where('ownerUserId')
|
|
230
|
+
.equals(ownerUserId)
|
|
231
|
+
.toArray();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getContactByOwnerAndUserId(
|
|
235
|
+
ownerUserId: string,
|
|
236
|
+
userId: string
|
|
237
|
+
): Promise<Contact | undefined> {
|
|
238
|
+
return await this.contacts
|
|
239
|
+
.where('[ownerUserId+userId]')
|
|
240
|
+
.equals([ownerUserId, userId])
|
|
241
|
+
.first();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** DISCUSSIONS */
|
|
245
|
+
async getDiscussionsByOwner(ownerUserId: string): Promise<Discussion[]> {
|
|
246
|
+
const all = await this.discussions
|
|
247
|
+
.where('ownerUserId')
|
|
248
|
+
.equals(ownerUserId)
|
|
249
|
+
.toArray();
|
|
250
|
+
return all.sort((a, b) => {
|
|
251
|
+
if (a.lastMessageTimestamp && b.lastMessageTimestamp) {
|
|
252
|
+
return (
|
|
253
|
+
b.lastMessageTimestamp.getTime() - a.lastMessageTimestamp.getTime()
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (a.lastMessageTimestamp) return -1;
|
|
257
|
+
if (b.lastMessageTimestamp) return 1;
|
|
258
|
+
return b.createdAt.getTime() - a.createdAt.getTime();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async getUnreadCountByOwner(ownerUserId: string): Promise<number> {
|
|
263
|
+
const discussions = await this.discussions
|
|
264
|
+
.where('ownerUserId')
|
|
265
|
+
.equals(ownerUserId)
|
|
266
|
+
.toArray();
|
|
267
|
+
return discussions.reduce((total, d) => total + d.unreadCount, 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getDiscussionByOwnerAndContact(
|
|
271
|
+
ownerUserId: string,
|
|
272
|
+
contactUserId: string
|
|
273
|
+
): Promise<Discussion | undefined> {
|
|
274
|
+
if (!ownerUserId || !contactUserId) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
return await this.discussions
|
|
278
|
+
.where('[ownerUserId+contactUserId]')
|
|
279
|
+
.equals([ownerUserId, contactUserId])
|
|
280
|
+
.first();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get all active discussions with their sync status
|
|
285
|
+
* @returns Array of active discussions
|
|
286
|
+
*/
|
|
287
|
+
async getActiveDiscussionsByOwner(
|
|
288
|
+
ownerUserId: string
|
|
289
|
+
): Promise<Discussion[]> {
|
|
290
|
+
return await this.discussions
|
|
291
|
+
.where('[ownerUserId+status]')
|
|
292
|
+
.equals([ownerUserId, DiscussionStatus.ACTIVE])
|
|
293
|
+
.toArray();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async markMessagesAsRead(
|
|
297
|
+
ownerUserId: string,
|
|
298
|
+
contactUserId: string
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
await this.messages
|
|
301
|
+
.where('[ownerUserId+contactUserId+status]')
|
|
302
|
+
.equals([ownerUserId, contactUserId, MessageStatus.DELIVERED])
|
|
303
|
+
.and(msg => msg.direction === MessageDirection.INCOMING)
|
|
304
|
+
.modify({ status: MessageStatus.READ });
|
|
305
|
+
|
|
306
|
+
await this.discussions
|
|
307
|
+
.where('[ownerUserId+contactUserId]')
|
|
308
|
+
.equals([ownerUserId, contactUserId])
|
|
309
|
+
.modify({ unreadCount: 0 });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async getMessagesForContactByOwner(
|
|
313
|
+
ownerUserId: string,
|
|
314
|
+
contactUserId: string,
|
|
315
|
+
limit = 50
|
|
316
|
+
): Promise<Message[]> {
|
|
317
|
+
return await this.messages
|
|
318
|
+
.where('[ownerUserId+contactUserId]')
|
|
319
|
+
.equals([ownerUserId, contactUserId])
|
|
320
|
+
.reverse()
|
|
321
|
+
.limit(limit)
|
|
322
|
+
.toArray();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async addMessage(message: Omit<Message, 'id'>): Promise<number> {
|
|
326
|
+
const messageId = await this.messages.add(message);
|
|
327
|
+
|
|
328
|
+
// Get existing discussion
|
|
329
|
+
const discussion = await this.getDiscussionByOwnerAndContact(
|
|
330
|
+
message.ownerUserId,
|
|
331
|
+
message.contactUserId
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (discussion) {
|
|
335
|
+
await this.discussions.update(discussion.id!, {
|
|
336
|
+
lastMessageId: messageId,
|
|
337
|
+
lastMessageContent: message.content,
|
|
338
|
+
lastMessageTimestamp: message.timestamp,
|
|
339
|
+
unreadCount:
|
|
340
|
+
message.direction === MessageDirection.INCOMING
|
|
341
|
+
? discussion.unreadCount + 1
|
|
342
|
+
: discussion.unreadCount,
|
|
343
|
+
updatedAt: new Date(),
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
// Note: For new messages, a discussion should already exist from the protocol
|
|
347
|
+
// If not, we'll create a minimal one (this shouldn't normally happen)
|
|
348
|
+
console.log(
|
|
349
|
+
'Warning: Creating discussion for contact without protocol setup:',
|
|
350
|
+
message.contactUserId
|
|
351
|
+
);
|
|
352
|
+
await this.discussions.put({
|
|
353
|
+
ownerUserId: message.ownerUserId,
|
|
354
|
+
contactUserId: message.contactUserId,
|
|
355
|
+
direction:
|
|
356
|
+
message.direction === MessageDirection.INCOMING
|
|
357
|
+
? DiscussionDirection.RECEIVED
|
|
358
|
+
: DiscussionDirection.INITIATED,
|
|
359
|
+
status: DiscussionStatus.PENDING,
|
|
360
|
+
nextSeeker: undefined,
|
|
361
|
+
lastMessageId: messageId,
|
|
362
|
+
lastMessageContent: message.content,
|
|
363
|
+
lastMessageTimestamp: message.timestamp,
|
|
364
|
+
unreadCount: message.direction === MessageDirection.INCOMING ? 1 : 0,
|
|
365
|
+
createdAt: new Date(),
|
|
366
|
+
updatedAt: new Date(),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return messageId;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Update the last sync timestamp for a discussion
|
|
375
|
+
* @param discussionId - The discussion ID
|
|
376
|
+
* @param timestamp - The sync timestamp
|
|
377
|
+
*/
|
|
378
|
+
async updateLastSyncTimestamp(
|
|
379
|
+
discussionId: number,
|
|
380
|
+
timestamp: Date
|
|
381
|
+
): Promise<void> {
|
|
382
|
+
await this.discussions.update(discussionId, {
|
|
383
|
+
lastSyncTimestamp: timestamp,
|
|
384
|
+
updatedAt: new Date(),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async deleteDb(): Promise<void> {
|
|
389
|
+
await this.close();
|
|
390
|
+
await this.delete();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Set all active seekers, replacing any existing ones
|
|
395
|
+
* @param seekers - Array of seeker Uint8Arrays to store
|
|
396
|
+
*/
|
|
397
|
+
async setActiveSeekers(seekers: Uint8Array[]): Promise<void> {
|
|
398
|
+
await this.transaction('rw', this.activeSeekers, async () => {
|
|
399
|
+
// Clear all existing seekers
|
|
400
|
+
await this.activeSeekers.clear();
|
|
401
|
+
|
|
402
|
+
// Bulk add all new seekers
|
|
403
|
+
if (seekers.length > 0) {
|
|
404
|
+
await this.activeSeekers.bulkAdd(
|
|
405
|
+
seekers.map(seeker => ({
|
|
406
|
+
seeker,
|
|
407
|
+
}))
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get all active seekers from the database
|
|
415
|
+
* @returns Array of seeker Uint8Arrays
|
|
416
|
+
*/
|
|
417
|
+
async getActiveSeekers(): Promise<Uint8Array[]> {
|
|
418
|
+
const activeSeekers = await this.activeSeekers.toArray();
|
|
419
|
+
return activeSeekers.map(item => item.seeker);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Database instance - initialized lazily or via setDb()
|
|
424
|
+
let _db: GossipDatabase | null = null;
|
|
425
|
+
let _warnedGlobalDbAccess = false;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the database instance.
|
|
429
|
+
* Creates a default instance if none was set via setDb().
|
|
430
|
+
*/
|
|
431
|
+
export function getDb(): GossipDatabase {
|
|
432
|
+
if (!_db) {
|
|
433
|
+
_db = new GossipDatabase();
|
|
434
|
+
}
|
|
435
|
+
return _db;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Set the database instance.
|
|
440
|
+
* Call this before using any SDK functions if you need a custom db instance.
|
|
441
|
+
*/
|
|
442
|
+
export function setDb(database: GossipDatabase): void {
|
|
443
|
+
_db = database;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get the database instance.
|
|
448
|
+
* Creates a default instance if none was set via setDb().
|
|
449
|
+
*/
|
|
450
|
+
export const db: GossipDatabase = new Proxy({} as GossipDatabase, {
|
|
451
|
+
get(_target, prop) {
|
|
452
|
+
if (!_warnedGlobalDbAccess) {
|
|
453
|
+
_warnedGlobalDbAccess = true;
|
|
454
|
+
console.warn(
|
|
455
|
+
'[GossipSdk] Global db access is deprecated. Use createGossipSdk() or setDb().'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const target = getDb();
|
|
459
|
+
const value = Reflect.get(target, prop);
|
|
460
|
+
if (typeof value === 'function') {
|
|
461
|
+
return value.bind(target);
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
},
|
|
465
|
+
});
|