@massalabs/gossip-sdk 0.0.2-dev.20260410095334 → 0.0.2-dev.20260410113627
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 +0 -3
- package/dist/gossip.d.ts +0 -2
- package/dist/gossip.js +1 -2
- package/dist/services/message.d.ts +24 -0
- package/dist/services/message.js +301 -46
- package/dist/services/profile.d.ts +0 -2
- package/dist/services/profile.js +0 -4
- package/dist/utils/validation.d.ts +4 -20
- package/dist/utils/validation.js +4 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -293,9 +293,6 @@ const utils = sdk.utils;
|
|
|
293
293
|
const result = utils.validateUserId(userId);
|
|
294
294
|
if (!result.valid) console.error(result.error);
|
|
295
295
|
|
|
296
|
-
// Validate username format
|
|
297
|
-
const result = utils.validateUsername(username);
|
|
298
|
-
|
|
299
296
|
// Encode/decode user IDs
|
|
300
297
|
const encoded = utils.encodeUserId(rawBytes);
|
|
301
298
|
const decoded = utils.decodeUserId(encodedString);
|
package/dist/gossip.d.ts
CHANGED
|
@@ -155,8 +155,6 @@ declare class GossipSdk {
|
|
|
155
155
|
interface SdkUtils {
|
|
156
156
|
/** Validate a user ID format */
|
|
157
157
|
validateUserId(userId: string): ValidationResult;
|
|
158
|
-
/** Validate a username format */
|
|
159
|
-
validateUsername(username: string): ValidationResult;
|
|
160
158
|
/** Encode raw bytes to user ID string */
|
|
161
159
|
encodeUserId(rawId: Uint8Array): string;
|
|
162
160
|
/** Decode user ID string to raw bytes */
|
package/dist/gossip.js
CHANGED
|
@@ -48,7 +48,7 @@ import { AuthService } from './services/auth.js';
|
|
|
48
48
|
import { ProfileService } from './services/profile.js';
|
|
49
49
|
import { ContactService } from './services/contact.js';
|
|
50
50
|
import { SelfMessageService } from './services/selfMessage.js';
|
|
51
|
-
import { validateUserIdFormat,
|
|
51
|
+
import { validateUserIdFormat, } from './utils/validation.js';
|
|
52
52
|
import { QueueManager } from './utils/queue.js';
|
|
53
53
|
import { encodeUserId, decodeUserId } from './utils/userId.js';
|
|
54
54
|
import { MessageStatus } from './db/index.js';
|
|
@@ -463,7 +463,6 @@ class GossipSdk {
|
|
|
463
463
|
get utils() {
|
|
464
464
|
return {
|
|
465
465
|
validateUserId: validateUserIdFormat,
|
|
466
|
-
validateUsername: validateUsernameFormat,
|
|
467
466
|
encodeUserId,
|
|
468
467
|
decodeUserId,
|
|
469
468
|
};
|
|
@@ -45,6 +45,14 @@ export declare class MessageService {
|
|
|
45
45
|
private processingContacts;
|
|
46
46
|
private isFetchingMessages;
|
|
47
47
|
private queries;
|
|
48
|
+
/**
|
|
49
|
+
* Message ids currently being sent via `sendMessageFastPath`. The row is
|
|
50
|
+
* WAITING_SESSION in the DB while the fast path runs INSERT + encrypt +
|
|
51
|
+
* POST in parallel. Without this guard, a concurrent `stateUpdate` could
|
|
52
|
+
* pick up the same WAITING_SESSION row via `processSendQueueForContact`
|
|
53
|
+
* and send a duplicate.
|
|
54
|
+
*/
|
|
55
|
+
private inFlightFastPath;
|
|
48
56
|
/** Emit MESSAGE_RECEIVED with a Message that may not have a DB id yet */
|
|
49
57
|
private emitMessageReceived;
|
|
50
58
|
constructor(messageProtocol: IMessageProtocol, session: SessionModule, eventEmitter: SdkEventEmitter, config: SdkConfig | undefined, queries: Queries);
|
|
@@ -66,6 +74,22 @@ export declare class MessageService {
|
|
|
66
74
|
findMessageBySeeker(seeker: Uint8Array, ownerUserId: string): Promise<Message | undefined>;
|
|
67
75
|
private acknowledgeMessages;
|
|
68
76
|
sendMessage(message: Message): Promise<SendMessageResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Happy-path send: peer session is Active, so we can encrypt locally,
|
|
79
|
+
* fire the network POST, and INSERT the row in parallel. The three
|
|
80
|
+
* pieces are independent up until the final `UPDATE → SENT`, which
|
|
81
|
+
* needs both the row id (from INSERT) and a successful network ack.
|
|
82
|
+
*
|
|
83
|
+
* ┌── INSERT (~440 ms) ───────────────┐
|
|
84
|
+
* │ ├── UPDATE → SENT (background)
|
|
85
|
+
* └── encrypt (~150 ms) → POST (~620 ms)
|
|
86
|
+
* │
|
|
87
|
+
* └── emit MESSAGE_SENT
|
|
88
|
+
*
|
|
89
|
+
* Returns `null` if the fast path can't run (encrypt declined, network
|
|
90
|
+
* threw, etc.) so the caller can fall back to the slow path.
|
|
91
|
+
*/
|
|
92
|
+
private sendMessageFastPath;
|
|
69
93
|
private serializeMessage;
|
|
70
94
|
resendMessages(messages: Map<string, Message[]>): Promise<void>;
|
|
71
95
|
/**
|
package/dist/services/message.js
CHANGED
|
@@ -202,6 +202,19 @@ export class MessageService {
|
|
|
202
202
|
writable: true,
|
|
203
203
|
value: void 0
|
|
204
204
|
});
|
|
205
|
+
/**
|
|
206
|
+
* Message ids currently being sent via `sendMessageFastPath`. The row is
|
|
207
|
+
* WAITING_SESSION in the DB while the fast path runs INSERT + encrypt +
|
|
208
|
+
* POST in parallel. Without this guard, a concurrent `stateUpdate` could
|
|
209
|
+
* pick up the same WAITING_SESSION row via `processSendQueueForContact`
|
|
210
|
+
* and send a duplicate.
|
|
211
|
+
*/
|
|
212
|
+
Object.defineProperty(this, "inFlightFastPath", {
|
|
213
|
+
enumerable: true,
|
|
214
|
+
configurable: true,
|
|
215
|
+
writable: true,
|
|
216
|
+
value: new Set()
|
|
217
|
+
});
|
|
205
218
|
this.messageProtocol = messageProtocol;
|
|
206
219
|
this.session = session;
|
|
207
220
|
this.eventEmitter = eventEmitter;
|
|
@@ -297,42 +310,44 @@ export class MessageService {
|
|
|
297
310
|
* Add a message to SQLite and update the corresponding discussion.
|
|
298
311
|
*/
|
|
299
312
|
async addMessageAndUpdateDiscussion(message) {
|
|
300
|
-
|
|
301
|
-
messageId
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId);
|
|
321
|
-
if (discussion &&
|
|
322
|
-
message.type !== MessageType.KEEP_ALIVE &&
|
|
323
|
-
message.type !== MessageType.REACTION &&
|
|
324
|
-
message.type !== MessageType.RETENTION_POLICY) {
|
|
325
|
-
await this.queries.discussions.updateById(discussion.id, {
|
|
326
|
-
lastMessageId: messageId,
|
|
327
|
-
lastMessageContent: message.content,
|
|
328
|
-
lastMessageTimestamp: message.timestamp,
|
|
329
|
-
updatedAt: new Date(),
|
|
313
|
+
return this.queries.conn.withTransaction(async () => {
|
|
314
|
+
const messageId = await this.queries.messages.insert({
|
|
315
|
+
messageId: message.messageId,
|
|
316
|
+
ownerUserId: message.ownerUserId,
|
|
317
|
+
contactUserId: message.contactUserId,
|
|
318
|
+
content: message.content,
|
|
319
|
+
serializedContent: message.serializedContent,
|
|
320
|
+
type: message.type,
|
|
321
|
+
direction: message.direction,
|
|
322
|
+
status: message.status,
|
|
323
|
+
timestamp: message.timestamp,
|
|
324
|
+
metadata: serializeMetadata(message.metadata),
|
|
325
|
+
seeker: message.seeker,
|
|
326
|
+
replyTo: serializeReplyTo(message.replyTo),
|
|
327
|
+
forwardOf: serializeForwardOf(message.forwardOf),
|
|
328
|
+
deleteOf: serializeDeleteOf(message.deleteOf),
|
|
329
|
+
editOf: serializeEditOf(message.editOf),
|
|
330
|
+
reactionOf: serializeReactionOf(message.reactionOf),
|
|
331
|
+
encryptedMessage: message.encryptedMessage,
|
|
332
|
+
whenToSend: message.whenToSend,
|
|
330
333
|
});
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId);
|
|
335
|
+
if (discussion &&
|
|
336
|
+
message.type !== MessageType.KEEP_ALIVE &&
|
|
337
|
+
message.type !== MessageType.REACTION &&
|
|
338
|
+
message.type !== MessageType.RETENTION_POLICY) {
|
|
339
|
+
await this.queries.discussions.updateById(discussion.id, {
|
|
340
|
+
lastMessageId: messageId,
|
|
341
|
+
lastMessageContent: message.content,
|
|
342
|
+
lastMessageTimestamp: message.timestamp,
|
|
343
|
+
updatedAt: new Date(),
|
|
344
|
+
});
|
|
345
|
+
if (message.direction === MessageDirection.INCOMING) {
|
|
346
|
+
await this.queries.discussions.incrementUnreadCount(discussion.id);
|
|
347
|
+
}
|
|
333
348
|
}
|
|
334
|
-
|
|
335
|
-
|
|
349
|
+
return messageId;
|
|
350
|
+
});
|
|
336
351
|
}
|
|
337
352
|
async decryptMessages(encrypted) {
|
|
338
353
|
const log = logger.forMethod('decryptMessages');
|
|
@@ -647,7 +662,23 @@ export class MessageService {
|
|
|
647
662
|
: undefined;
|
|
648
663
|
message.messageId = randomMessageId;
|
|
649
664
|
}
|
|
650
|
-
//
|
|
665
|
+
// Fast path: if the peer's session is already Active we can encrypt
|
|
666
|
+
// and ship the message in parallel with the local INSERT instead of
|
|
667
|
+
// waiting for `addMessageAndUpdateDiscussion` to commit before
|
|
668
|
+
// `processSendQueueForContact` even starts. The user-perceived
|
|
669
|
+
// latency drops from `INSERT + encrypt + POST` (sequential) to
|
|
670
|
+
// `max(INSERT, encrypt + POST)` — ~440 ms saved on prod where the
|
|
671
|
+
// network round-trip dominates.
|
|
672
|
+
const sessionStatus = this.session.peerSessionStatus(peerId);
|
|
673
|
+
if (sessionStatus === SessionStatus.Active) {
|
|
674
|
+
const fastPathResult = await this.sendMessageFastPath(message, peerId);
|
|
675
|
+
if (fastPathResult) {
|
|
676
|
+
return fastPathResult;
|
|
677
|
+
}
|
|
678
|
+
// Fast path bailed (encrypt returned null, etc.) — fall back to
|
|
679
|
+
// the slow path below.
|
|
680
|
+
}
|
|
681
|
+
// Slow path: INSERT first, let stateUpdate handle the encrypt + send.
|
|
651
682
|
let messageId;
|
|
652
683
|
try {
|
|
653
684
|
messageId = await this.addMessageAndUpdateDiscussion({
|
|
@@ -676,6 +707,173 @@ export class MessageService {
|
|
|
676
707
|
message: queuedMessage,
|
|
677
708
|
};
|
|
678
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* Happy-path send: peer session is Active, so we can encrypt locally,
|
|
712
|
+
* fire the network POST, and INSERT the row in parallel. The three
|
|
713
|
+
* pieces are independent up until the final `UPDATE → SENT`, which
|
|
714
|
+
* needs both the row id (from INSERT) and a successful network ack.
|
|
715
|
+
*
|
|
716
|
+
* ┌── INSERT (~440 ms) ───────────────┐
|
|
717
|
+
* │ ├── UPDATE → SENT (background)
|
|
718
|
+
* └── encrypt (~150 ms) → POST (~620 ms)
|
|
719
|
+
* │
|
|
720
|
+
* └── emit MESSAGE_SENT
|
|
721
|
+
*
|
|
722
|
+
* Returns `null` if the fast path can't run (encrypt declined, network
|
|
723
|
+
* threw, etc.) so the caller can fall back to the slow path.
|
|
724
|
+
*/
|
|
725
|
+
async sendMessageFastPath(message, peerId) {
|
|
726
|
+
const log = logger.forMethod('sendMessageFastPath');
|
|
727
|
+
// 1. Serialize synchronously — needed before encrypt.
|
|
728
|
+
const serializeResult = await this.serializeMessage(message);
|
|
729
|
+
if (!serializeResult.success) {
|
|
730
|
+
log.error('failed to serialize message for fast path', {
|
|
731
|
+
error: serializeResult.error,
|
|
732
|
+
});
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const serializedContent = serializeResult.data;
|
|
736
|
+
// 2. Kick off the local INSERT and the encrypt in parallel.
|
|
737
|
+
// encrypt is fast (~150 ms with rayon) and doesn't depend on
|
|
738
|
+
// the row id; INSERT is slow (~440 ms) and only the row id is
|
|
739
|
+
// later needed for the SENT update.
|
|
740
|
+
const insertPromise = this.addMessageAndUpdateDiscussion({
|
|
741
|
+
...message,
|
|
742
|
+
status: MessageStatus.WAITING_SESSION,
|
|
743
|
+
})
|
|
744
|
+
.then(id => {
|
|
745
|
+
this.inFlightFastPath.add(id);
|
|
746
|
+
return id;
|
|
747
|
+
})
|
|
748
|
+
.catch(error => {
|
|
749
|
+
log.error('addMessageAndUpdateDiscussion failed in fast path', {
|
|
750
|
+
error,
|
|
751
|
+
});
|
|
752
|
+
return null;
|
|
753
|
+
});
|
|
754
|
+
let sendOutput;
|
|
755
|
+
try {
|
|
756
|
+
sendOutput = await this.session.sendMessage(peerId, serializedContent);
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
log.error('session.sendMessage threw in fast path, falling back', {
|
|
760
|
+
error,
|
|
761
|
+
});
|
|
762
|
+
sendOutput = undefined;
|
|
763
|
+
}
|
|
764
|
+
if (!sendOutput) {
|
|
765
|
+
// Session became inactive between status check and encrypt.
|
|
766
|
+
// Wait for the INSERT to land then bail out so the slow path
|
|
767
|
+
// can take over.
|
|
768
|
+
const messageId = await insertPromise;
|
|
769
|
+
if (messageId === null) {
|
|
770
|
+
return {
|
|
771
|
+
success: false,
|
|
772
|
+
error: 'Failed to add message to database',
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
log.info('encrypt returned null in fast path, falling back to slow path', { messageId });
|
|
776
|
+
this.inFlightFastPath.delete(messageId);
|
|
777
|
+
void this.refreshService?.stateUpdate();
|
|
778
|
+
return {
|
|
779
|
+
success: true,
|
|
780
|
+
message: {
|
|
781
|
+
...message,
|
|
782
|
+
id: messageId,
|
|
783
|
+
status: MessageStatus.WAITING_SESSION,
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
// 3. Encrypt succeeded. Network POST runs in parallel with the
|
|
788
|
+
// still-in-flight INSERT.
|
|
789
|
+
const networkPromise = this.messageProtocol
|
|
790
|
+
.sendMessage({
|
|
791
|
+
seeker: sendOutput.seeker,
|
|
792
|
+
ciphertext: sendOutput.data,
|
|
793
|
+
})
|
|
794
|
+
.then(() => true)
|
|
795
|
+
.catch(error => {
|
|
796
|
+
log.error('network send failed in fast path', { error });
|
|
797
|
+
return false;
|
|
798
|
+
});
|
|
799
|
+
const [messageId, networkOk] = await Promise.all([
|
|
800
|
+
insertPromise,
|
|
801
|
+
networkPromise,
|
|
802
|
+
]);
|
|
803
|
+
if (messageId === null) {
|
|
804
|
+
return {
|
|
805
|
+
success: false,
|
|
806
|
+
error: 'Failed to add message to database',
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (!networkOk) {
|
|
810
|
+
// Network failed: persist READY so the next retry doesn't
|
|
811
|
+
// re-encrypt, and let stateUpdate pick it up.
|
|
812
|
+
this.inFlightFastPath.delete(messageId);
|
|
813
|
+
await this.queries.messages.updateById(messageId, {
|
|
814
|
+
status: MessageStatus.READY,
|
|
815
|
+
encryptedMessage: sendOutput.data,
|
|
816
|
+
seeker: sendOutput.seeker,
|
|
817
|
+
whenToSend: new Date(Date.now() + this.config.messages.retryDelayMs),
|
|
818
|
+
serializedContent,
|
|
819
|
+
});
|
|
820
|
+
void this.refreshService?.stateUpdate();
|
|
821
|
+
return {
|
|
822
|
+
success: true,
|
|
823
|
+
message: {
|
|
824
|
+
...message,
|
|
825
|
+
id: messageId,
|
|
826
|
+
status: MessageStatus.READY,
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// 4. Both succeeded. Race-check then UPDATE → SENT.
|
|
831
|
+
const latestRow = await this.queries.messages.getById(messageId);
|
|
832
|
+
if (!latestRow || latestRow.status !== MessageStatus.WAITING_SESSION) {
|
|
833
|
+
log.debug('message gone or status changed during fast-path send, skipping SENT update', { messageId, currentStatus: latestRow?.status });
|
|
834
|
+
this.inFlightFastPath.delete(messageId);
|
|
835
|
+
return {
|
|
836
|
+
success: true,
|
|
837
|
+
message: {
|
|
838
|
+
...message,
|
|
839
|
+
id: messageId,
|
|
840
|
+
status: MessageStatus.SENT,
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
await this.queries.messages.updateById(messageId, {
|
|
845
|
+
status: MessageStatus.SENT,
|
|
846
|
+
seeker: sendOutput.seeker,
|
|
847
|
+
encryptedMessage: null,
|
|
848
|
+
serializedContent: null,
|
|
849
|
+
whenToSend: null,
|
|
850
|
+
});
|
|
851
|
+
this.inFlightFastPath.delete(messageId);
|
|
852
|
+
const isControlMessage = !!(message.deleteOf || message.editOf);
|
|
853
|
+
if (!isControlMessage) {
|
|
854
|
+
try {
|
|
855
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_SENT, {
|
|
856
|
+
...message,
|
|
857
|
+
id: messageId,
|
|
858
|
+
status: MessageStatus.SENT,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
log.error('failed to emit message sent event from fast path', {
|
|
863
|
+
messageId,
|
|
864
|
+
error,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
success: true,
|
|
870
|
+
message: {
|
|
871
|
+
...message,
|
|
872
|
+
id: messageId,
|
|
873
|
+
status: MessageStatus.SENT,
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
679
877
|
async serializeMessage(message) {
|
|
680
878
|
const log = logger.forMethod('serializeMessage');
|
|
681
879
|
if (!message.messageId &&
|
|
@@ -853,11 +1051,17 @@ export class MessageService {
|
|
|
853
1051
|
for (const msg of pendingMessages) {
|
|
854
1052
|
if (!msg.id)
|
|
855
1053
|
continue;
|
|
856
|
-
|
|
1054
|
+
if (this.inFlightFastPath.has(msg.id))
|
|
1055
|
+
continue;
|
|
1056
|
+
const currentStatus = msg.status;
|
|
857
1057
|
let encryptedMessage = msg.encryptedMessage;
|
|
858
1058
|
let seeker = msg.seeker;
|
|
859
|
-
|
|
860
|
-
//
|
|
1059
|
+
const whenToSend = msg.whenToSend;
|
|
1060
|
+
// Happy path: WAITING_SESSION + Active session.
|
|
1061
|
+
// Encrypt → network → SENT directly, skipping the intermediate
|
|
1062
|
+
// READY SQL write. The READY block below still handles retries
|
|
1063
|
+
// (delayed sends, post-failure retries), where the encrypted
|
|
1064
|
+
// bytes need to survive a restart.
|
|
861
1065
|
if (currentStatus === MessageStatus.WAITING_SESSION &&
|
|
862
1066
|
sessionStatus === SessionStatus.Active) {
|
|
863
1067
|
let serializedContent = msg.serializedContent;
|
|
@@ -883,22 +1087,73 @@ export class MessageService {
|
|
|
883
1087
|
}
|
|
884
1088
|
encryptedMessage = sendOutput.data;
|
|
885
1089
|
seeker = sendOutput.seeker;
|
|
886
|
-
|
|
1090
|
+
// Try the network synchronously. On success, skip READY entirely
|
|
1091
|
+
// and write SENT in the background. On failure, fall back to
|
|
1092
|
+
// persisting READY so the next retry doesn't have to re-encrypt.
|
|
1093
|
+
try {
|
|
1094
|
+
await this.messageProtocol.sendMessage({
|
|
1095
|
+
seeker,
|
|
1096
|
+
ciphertext: encryptedMessage,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
catch (error) {
|
|
1100
|
+
log.error('network send failed for fresh message', {
|
|
1101
|
+
messageId: msg.id,
|
|
1102
|
+
error,
|
|
1103
|
+
});
|
|
1104
|
+
await this.queries.messages.updateById(msg.id, {
|
|
1105
|
+
status: MessageStatus.READY,
|
|
1106
|
+
encryptedMessage,
|
|
1107
|
+
seeker,
|
|
1108
|
+
whenToSend: new Date(Date.now() + this.config.messages.retryDelayMs),
|
|
1109
|
+
serializedContent,
|
|
1110
|
+
});
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
// Network success. Race-check: most resets only touch
|
|
1114
|
+
// READY/SENT rows so they leave us alone, but the discussion
|
|
1115
|
+
// could have been deleted while we were on the wire — bail
|
|
1116
|
+
// out if the row is gone or has moved to a non-WAITING state.
|
|
1117
|
+
const latestRow = await this.queries.messages.getById(msg.id);
|
|
1118
|
+
if (!latestRow ||
|
|
1119
|
+
latestRow.status !== MessageStatus.WAITING_SESSION) {
|
|
1120
|
+
log.debug('message gone or status changed during network send, skipping SENT update', {
|
|
1121
|
+
messageId: msg.id,
|
|
1122
|
+
currentStatus: latestRow?.status,
|
|
1123
|
+
});
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
887
1126
|
await this.queries.messages.updateById(msg.id, {
|
|
888
|
-
status: MessageStatus.
|
|
889
|
-
encryptedMessage,
|
|
1127
|
+
status: MessageStatus.SENT,
|
|
890
1128
|
seeker,
|
|
891
|
-
|
|
892
|
-
serializedContent,
|
|
1129
|
+
encryptedMessage: null,
|
|
1130
|
+
serializedContent: null,
|
|
1131
|
+
whenToSend: null,
|
|
893
1132
|
});
|
|
894
|
-
|
|
895
|
-
log.debug('message
|
|
1133
|
+
sentCount++;
|
|
1134
|
+
log.debug('message sent (skipped READY)', {
|
|
896
1135
|
messageId: msg.id,
|
|
897
|
-
status:
|
|
1136
|
+
status: MessageStatus.SENT,
|
|
898
1137
|
content: msg.content,
|
|
899
1138
|
type: msg.type,
|
|
900
1139
|
direction: msg.direction,
|
|
901
1140
|
});
|
|
1141
|
+
const isControlMessage = !!(msg.deleteOf || msg.editOf);
|
|
1142
|
+
if (!isControlMessage) {
|
|
1143
|
+
try {
|
|
1144
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_SENT, {
|
|
1145
|
+
...msg,
|
|
1146
|
+
status: MessageStatus.SENT,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
log.error('failed to emit message sent event', {
|
|
1151
|
+
messageId: msg.id,
|
|
1152
|
+
error,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
continue;
|
|
902
1157
|
}
|
|
903
1158
|
if (currentStatus === MessageStatus.READY) {
|
|
904
1159
|
const sendAt = whenToSend ?? new Date();
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type UserProfile } from '../db/index.js';
|
|
8
8
|
import { Queries } from '../db/queries/index.js';
|
|
9
|
-
import { type ValidationResult } from '../utils/validation.js';
|
|
10
9
|
export declare class ProfileService {
|
|
11
10
|
private queries;
|
|
12
11
|
constructor(queries: Queries);
|
|
@@ -16,7 +15,6 @@ export declare class ProfileService {
|
|
|
16
15
|
getCount(): Promise<number>;
|
|
17
16
|
save(profile: UserProfile): Promise<void>;
|
|
18
17
|
delete(userId: string): Promise<void>;
|
|
19
|
-
validateUsername(username: string): Promise<ValidationResult>;
|
|
20
18
|
isUsernameTaken(username: string, excludeUserId?: string): Promise<boolean>;
|
|
21
19
|
createOrUpdate(username: string, userId: string, security: UserProfile['security'], session: Uint8Array): Promise<UserProfile>;
|
|
22
20
|
}
|
package/dist/services/profile.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Only requires Queries — no session needed, so it can be created at init() time.
|
|
6
6
|
*/
|
|
7
7
|
import { rowToUserProfile, userProfileToRow, } from '../db/queries/index.js';
|
|
8
|
-
import { validateUsernameFormatAndAvailability, } from '../utils/validation.js';
|
|
9
8
|
export class ProfileService {
|
|
10
9
|
constructor(queries) {
|
|
11
10
|
Object.defineProperty(this, "queries", {
|
|
@@ -37,9 +36,6 @@ export class ProfileService {
|
|
|
37
36
|
delete(userId) {
|
|
38
37
|
return this.queries.userProfiles.delete(userId);
|
|
39
38
|
}
|
|
40
|
-
validateUsername(username) {
|
|
41
|
-
return validateUsernameFormatAndAvailability(username, this.queries);
|
|
42
|
-
}
|
|
43
39
|
async isUsernameTaken(username, excludeUserId) {
|
|
44
40
|
const match = excludeUserId
|
|
45
41
|
? await this.queries.userProfiles.getByUsernameLowerExcluding(username, excludeUserId)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Functions for validating user input like usernames, passwords, and user IDs.
|
|
5
5
|
*/
|
|
6
|
-
import { Queries } from '../db/queries/index.js';
|
|
7
6
|
export type ValidationResult = {
|
|
8
7
|
valid: true;
|
|
9
8
|
error?: never;
|
|
@@ -18,28 +17,13 @@ export type ValidationResult = {
|
|
|
18
17
|
* @returns Validation result
|
|
19
18
|
*/
|
|
20
19
|
export declare function validatePassword(value: string): ValidationResult;
|
|
21
|
-
/**
|
|
22
|
-
* Validate a username format (without checking availability)
|
|
23
|
-
*
|
|
24
|
-
* @param value - The username to validate
|
|
25
|
-
* @returns Validation result
|
|
26
|
-
*/
|
|
27
|
-
export declare function validateUsernameFormat(value: string): ValidationResult;
|
|
28
20
|
/**
|
|
29
21
|
* Validate a username is available (not already in use)
|
|
30
22
|
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate a username format and availability
|
|
37
|
-
*
|
|
38
|
-
* @param value - The username to validate
|
|
39
|
-
* @param db - Database instance
|
|
40
|
-
* @returns Validation result
|
|
41
|
-
*/
|
|
42
|
-
export declare function validateUsernameFormatAndAvailability(value: string, queries: Queries): Promise<ValidationResult>;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
43
27
|
/**
|
|
44
28
|
* Validate a user ID format (Bech32 gossip1... format)
|
|
45
29
|
*
|
package/dist/utils/validation.js
CHANGED
|
@@ -22,72 +22,13 @@ export function validatePassword(value) {
|
|
|
22
22
|
}
|
|
23
23
|
return { valid: true };
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Validate a username format (without checking availability)
|
|
27
|
-
*
|
|
28
|
-
* @param value - The username to validate
|
|
29
|
-
* @returns Validation result
|
|
30
|
-
*/
|
|
31
|
-
export function validateUsernameFormat(value) {
|
|
32
|
-
const trimmed = value.trim();
|
|
33
|
-
if (!trimmed) {
|
|
34
|
-
return { valid: false, error: 'Username is required' };
|
|
35
|
-
}
|
|
36
|
-
// Disallow any whitespace inside the username (single token only)
|
|
37
|
-
if (/\s/.test(trimmed)) {
|
|
38
|
-
return {
|
|
39
|
-
valid: false,
|
|
40
|
-
error: 'Username cannot contain spaces',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (trimmed.length < 3) {
|
|
44
|
-
return {
|
|
45
|
-
valid: false,
|
|
46
|
-
error: 'Username must be at least 3 characters long',
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return { valid: true };
|
|
50
|
-
}
|
|
51
25
|
/**
|
|
52
26
|
* Validate a username is available (not already in use)
|
|
53
27
|
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const existingProfile = await queries.userProfiles.getByUsernameLower(value);
|
|
60
|
-
if (existingProfile) {
|
|
61
|
-
return {
|
|
62
|
-
valid: false,
|
|
63
|
-
error: 'This username is already in use. Please choose another.',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return { valid: true };
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
return {
|
|
70
|
-
valid: false,
|
|
71
|
-
error: error instanceof Error
|
|
72
|
-
? error.message
|
|
73
|
-
: 'Unable to verify username availability. Please try again.',
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Validate a username format and availability
|
|
79
|
-
*
|
|
80
|
-
* @param value - The username to validate
|
|
81
|
-
* @param db - Database instance
|
|
82
|
-
* @returns Validation result
|
|
83
|
-
*/
|
|
84
|
-
export async function validateUsernameFormatAndAvailability(value, queries) {
|
|
85
|
-
const result = validateUsernameFormat(value);
|
|
86
|
-
if (!result.valid) {
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
return await validateUsernameAvailability(value, queries);
|
|
90
|
-
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
91
32
|
/**
|
|
92
33
|
* Validate a user ID format (Bech32 gossip1... format)
|
|
93
34
|
*
|
package/package.json
CHANGED