@massalabs/gossip-sdk 0.0.2-dev.20260226131013 → 0.0.2-dev.20260302112618
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/config/sdk.d.ts +13 -0
- package/dist/config/sdk.js +9 -0
- package/dist/core/SdkEventEmitter.d.ts +4 -1
- package/dist/core/SdkEventEmitter.js +2 -0
- package/dist/core/SdkPolling.d.ts +2 -0
- package/dist/core/SdkPolling.js +18 -1
- package/dist/db/db.d.ts +3 -9
- package/dist/db/db.js +0 -9
- package/dist/db/generated-migrations.js +3 -11
- package/dist/db/queries/discussions.d.ts +1 -4
- package/dist/db/queries/discussions.js +0 -7
- package/dist/db/schema/discussions.d.ts +52 -21
- package/dist/db/schema/discussions.js +6 -3
- package/dist/db/sqlite-worker.js +28 -1
- package/dist/gossip.js +4 -1
- package/dist/services/announcement.js +4 -2
- package/dist/services/discussion.js +1 -2
- package/dist/services/message.js +13 -6
- package/dist/services/refresh.d.ts +13 -2
- package/dist/services/refresh.js +168 -24
- package/dist/wasm/index.d.ts +1 -1
- package/dist/wasm/index.js +1 -1
- package/package.json +1 -1
package/dist/config/sdk.d.ts
CHANGED
|
@@ -57,6 +57,17 @@ export interface AnnouncementsConfig {
|
|
|
57
57
|
/** Delay before retrying a failed announcement send in ms (default: 15000 = 15 seconds) */
|
|
58
58
|
retryDelayMs: number;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Session recovery configuration
|
|
62
|
+
*/
|
|
63
|
+
export interface SessionRecoveryConfig {
|
|
64
|
+
/** Delay before retrying a killed session reset (default: 15 minutes) */
|
|
65
|
+
killedRetryDelayMs: number;
|
|
66
|
+
/** Jitter applied (default: 2 minutes) */
|
|
67
|
+
JitterMs: number;
|
|
68
|
+
/** Delay before retrying a saturated session reset (default: 5 minutes) */
|
|
69
|
+
saturatedRetryDelayMs: number;
|
|
70
|
+
}
|
|
60
71
|
/**
|
|
61
72
|
* Complete SDK configuration
|
|
62
73
|
*/
|
|
@@ -69,6 +80,8 @@ export interface SdkConfig {
|
|
|
69
80
|
messages: MessagesConfig;
|
|
70
81
|
/** Announcement settings */
|
|
71
82
|
announcements: AnnouncementsConfig;
|
|
83
|
+
/** Session recovery settings */
|
|
84
|
+
sessionRecovery: SessionRecoveryConfig;
|
|
72
85
|
}
|
|
73
86
|
/**
|
|
74
87
|
* Default SDK configuration values
|
package/dist/config/sdk.js
CHANGED
|
@@ -29,6 +29,11 @@ export const defaultSdkConfig = {
|
|
|
29
29
|
brokenThresholdMs: 60 * 60 * 1000, // 1 hour
|
|
30
30
|
retryDelayMs: 15000, // 15 seconds
|
|
31
31
|
},
|
|
32
|
+
sessionRecovery: {
|
|
33
|
+
killedRetryDelayMs: 15 * 60 * 1000,
|
|
34
|
+
JitterMs: 2 * 60 * 1000,
|
|
35
|
+
saturatedRetryDelayMs: 5 * 60 * 1000,
|
|
36
|
+
},
|
|
32
37
|
};
|
|
33
38
|
/**
|
|
34
39
|
* Deep merge partial config with defaults
|
|
@@ -53,5 +58,9 @@ export function mergeConfig(partial) {
|
|
|
53
58
|
...defaultSdkConfig.announcements,
|
|
54
59
|
...partial.announcements,
|
|
55
60
|
},
|
|
61
|
+
sessionRecovery: {
|
|
62
|
+
...defaultSdkConfig.sessionRecovery,
|
|
63
|
+
...partial.sessionRecovery,
|
|
64
|
+
},
|
|
56
65
|
};
|
|
57
66
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Type-safe event emitter for SDK events.
|
|
5
5
|
*/
|
|
6
|
-
import type { Message, Discussion, Contact } from '../db
|
|
6
|
+
import type { Message, Discussion, Contact } from '../db';
|
|
7
|
+
import type { SessionStatus } from '../wasm/bindings';
|
|
7
8
|
export declare enum SdkEventType {
|
|
8
9
|
MESSAGE_RECEIVED = "messageReceived",
|
|
9
10
|
MESSAGE_SENT = "messageSent",
|
|
@@ -14,6 +15,7 @@ export declare enum SdkEventType {
|
|
|
14
15
|
SESSION_RENEWED = "sessionRenewed",
|
|
15
16
|
SESSION_ACCEPTED = "sessionAccepted",
|
|
16
17
|
SEEKERS_UPDATED = "seekersUpdated",
|
|
18
|
+
SESSION_STATUS_CHANGED = "sessionStatusChanged",
|
|
17
19
|
ERROR = "error"
|
|
18
20
|
}
|
|
19
21
|
export interface SdkEventHandlers {
|
|
@@ -26,6 +28,7 @@ export interface SdkEventHandlers {
|
|
|
26
28
|
[SdkEventType.SESSION_RENEWED]: (discussion: Discussion) => void;
|
|
27
29
|
[SdkEventType.SESSION_ACCEPTED]: (contactUserId: string) => void;
|
|
28
30
|
[SdkEventType.SEEKERS_UPDATED]: (seekers: Uint8Array[]) => void;
|
|
31
|
+
[SdkEventType.SESSION_STATUS_CHANGED]: (contactUserId: string, status: SessionStatus) => void;
|
|
29
32
|
[SdkEventType.ERROR]: (error: Error, context: string) => void;
|
|
30
33
|
}
|
|
31
34
|
export declare class SdkEventEmitter {
|
|
@@ -17,6 +17,7 @@ export var SdkEventType;
|
|
|
17
17
|
SdkEventType["SESSION_RENEWED"] = "sessionRenewed";
|
|
18
18
|
SdkEventType["SESSION_ACCEPTED"] = "sessionAccepted";
|
|
19
19
|
SdkEventType["SEEKERS_UPDATED"] = "seekersUpdated";
|
|
20
|
+
SdkEventType["SESSION_STATUS_CHANGED"] = "sessionStatusChanged";
|
|
20
21
|
SdkEventType["ERROR"] = "error";
|
|
21
22
|
})(SdkEventType || (SdkEventType = {}));
|
|
22
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -38,6 +39,7 @@ export class SdkEventEmitter {
|
|
|
38
39
|
[SdkEventType.SESSION_RENEWED]: new Set(),
|
|
39
40
|
[SdkEventType.SESSION_ACCEPTED]: new Set(),
|
|
40
41
|
[SdkEventType.SEEKERS_UPDATED]: new Set(),
|
|
42
|
+
[SdkEventType.SESSION_STATUS_CHANGED]: new Set(),
|
|
41
43
|
[SdkEventType.ERROR]: new Set(),
|
|
42
44
|
}
|
|
43
45
|
});
|
|
@@ -12,6 +12,8 @@ export interface PollingCallbacks {
|
|
|
12
12
|
fetchAnnouncements: () => Promise<void>;
|
|
13
13
|
/** Handle session refresh (state update) */
|
|
14
14
|
handleSessionRefresh: () => Promise<void>;
|
|
15
|
+
/** Check for session status changes and emit events */
|
|
16
|
+
refreshSessionsStatusEvent: () => Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
export declare class SdkPolling {
|
|
17
19
|
private timers;
|
package/dist/core/SdkPolling.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Manages polling timers for messages, announcements, and session refresh.
|
|
5
5
|
*/
|
|
6
6
|
import { SdkEventType } from './SdkEventEmitter.js';
|
|
7
|
+
const SESSION_STATUS_POLL_INTERVAL_MS = 3000;
|
|
7
8
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
9
|
// Polling Manager Class
|
|
9
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -17,6 +18,7 @@ export class SdkPolling {
|
|
|
17
18
|
messages: null,
|
|
18
19
|
announcements: null,
|
|
19
20
|
sessionRefresh: null,
|
|
21
|
+
sessionStatus: null,
|
|
20
22
|
}
|
|
21
23
|
});
|
|
22
24
|
Object.defineProperty(this, "callbacks", {
|
|
@@ -75,6 +77,16 @@ export class SdkPolling {
|
|
|
75
77
|
this.eventEmitter?.emit(SdkEventType.ERROR, err, 'session_update');
|
|
76
78
|
}
|
|
77
79
|
}, config.polling.sessionRefreshIntervalMs);
|
|
80
|
+
// Start session status change polling (fixed 3s interval)
|
|
81
|
+
this.timers.sessionStatus = setInterval(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await this.callbacks?.refreshSessionsStatusEvent();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
87
|
+
this.eventEmitter?.emit(SdkEventType.ERROR, err, 'session_status_polling');
|
|
88
|
+
}
|
|
89
|
+
}, SESSION_STATUS_POLL_INTERVAL_MS);
|
|
78
90
|
}
|
|
79
91
|
/**
|
|
80
92
|
* Stop all polling timers.
|
|
@@ -92,6 +104,10 @@ export class SdkPolling {
|
|
|
92
104
|
clearInterval(this.timers.sessionRefresh);
|
|
93
105
|
this.timers.sessionRefresh = null;
|
|
94
106
|
}
|
|
107
|
+
if (this.timers.sessionStatus) {
|
|
108
|
+
clearInterval(this.timers.sessionStatus);
|
|
109
|
+
this.timers.sessionStatus = null;
|
|
110
|
+
}
|
|
95
111
|
this.callbacks = null;
|
|
96
112
|
this.eventEmitter = null;
|
|
97
113
|
}
|
|
@@ -101,6 +117,7 @@ export class SdkPolling {
|
|
|
101
117
|
isRunning() {
|
|
102
118
|
return (this.timers.messages !== null ||
|
|
103
119
|
this.timers.announcements !== null ||
|
|
104
|
-
this.timers.sessionRefresh !== null
|
|
120
|
+
this.timers.sessionRefresh !== null ||
|
|
121
|
+
this.timers.sessionStatus !== null);
|
|
105
122
|
}
|
|
106
123
|
}
|
package/dist/db/db.d.ts
CHANGED
|
@@ -65,14 +65,6 @@ export interface UserProfile {
|
|
|
65
65
|
updatedAt: Date;
|
|
66
66
|
lastPublicKeyPush?: Date;
|
|
67
67
|
}
|
|
68
|
-
export declare enum DiscussionStatus {
|
|
69
|
-
PENDING = "pending",
|
|
70
|
-
ACTIVE = "active",
|
|
71
|
-
CLOSED = "closed",// closed by the user
|
|
72
|
-
BROKEN = "broken",// The session is killed. Need to be reinitiated
|
|
73
|
-
SEND_FAILED = "sendFailed",// The discussion was initiated by the session manager but could not be broadcasted on network
|
|
74
|
-
RECONNECTING = "reconnecting"
|
|
75
|
-
}
|
|
76
68
|
export declare enum DiscussionDirection {
|
|
77
69
|
RECEIVED = "received",
|
|
78
70
|
INITIATED = "initiated"
|
|
@@ -128,7 +120,6 @@ export interface Discussion {
|
|
|
128
120
|
weAccepted: boolean;
|
|
129
121
|
sendAnnouncement: SendAnnouncement;
|
|
130
122
|
direction: DiscussionDirection;
|
|
131
|
-
status: DiscussionStatus;
|
|
132
123
|
nextSeeker: Uint8Array | null;
|
|
133
124
|
initiationAnnouncement: Uint8Array | null;
|
|
134
125
|
announcementMessage: string | null;
|
|
@@ -139,6 +130,9 @@ export interface Discussion {
|
|
|
139
130
|
lastMessageContent: string | null;
|
|
140
131
|
lastMessageTimestamp: Date | null;
|
|
141
132
|
unreadCount: number;
|
|
133
|
+
killedNextRetryAt?: Date | null;
|
|
134
|
+
saturatedRetryAt?: Date | null;
|
|
135
|
+
saturatedRetryDone: boolean;
|
|
142
136
|
createdAt: Date;
|
|
143
137
|
updatedAt: Date;
|
|
144
138
|
}
|
package/dist/db/db.js
CHANGED
|
@@ -7,15 +7,6 @@
|
|
|
7
7
|
// Constants
|
|
8
8
|
export const MESSAGE_ID_SIZE = 12;
|
|
9
9
|
// Unified discussion interface combining protocol state and UI metadata
|
|
10
|
-
export var DiscussionStatus;
|
|
11
|
-
(function (DiscussionStatus) {
|
|
12
|
-
DiscussionStatus["PENDING"] = "pending";
|
|
13
|
-
DiscussionStatus["ACTIVE"] = "active";
|
|
14
|
-
DiscussionStatus["CLOSED"] = "closed";
|
|
15
|
-
DiscussionStatus["BROKEN"] = "broken";
|
|
16
|
-
DiscussionStatus["SEND_FAILED"] = "sendFailed";
|
|
17
|
-
DiscussionStatus["RECONNECTING"] = "reconnecting";
|
|
18
|
-
})(DiscussionStatus || (DiscussionStatus = {}));
|
|
19
10
|
export var DiscussionDirection;
|
|
20
11
|
(function (DiscussionDirection) {
|
|
21
12
|
DiscussionDirection["RECEIVED"] = "received";
|
|
@@ -4,7 +4,7 @@ export const MIGRATIONS = [
|
|
|
4
4
|
{
|
|
5
5
|
idx: 0,
|
|
6
6
|
tag: '0000_nifty_molly_hayes',
|
|
7
|
-
when:
|
|
7
|
+
when: 1730000000000,
|
|
8
8
|
statements: [
|
|
9
9
|
'CREATE TABLE `activeSeekers` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`seeker` blob NOT NULL\n);',
|
|
10
10
|
'CREATE INDEX `active_seekers_seeker_idx` ON `activeSeekers` (`seeker`);',
|
|
@@ -12,9 +12,8 @@ export const MIGRATIONS = [
|
|
|
12
12
|
'CREATE TABLE `contacts` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`ownerUserId` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`name` text NOT NULL,\n\t`avatar` text,\n\t`publicKeys` blob NOT NULL,\n\t`isOnline` integer NOT NULL,\n\t`lastSeen` integer NOT NULL,\n\t`createdAt` integer NOT NULL\n);',
|
|
13
13
|
'CREATE INDEX `contacts_owner_user_idx` ON `contacts` (`ownerUserId`,`userId`);',
|
|
14
14
|
'CREATE INDEX `contacts_owner_name_idx` ON `contacts` (`ownerUserId`,`name`);',
|
|
15
|
-
'CREATE TABLE `discussions` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`ownerUserId` text NOT NULL,\n\t`contactUserId` text NOT NULL,\n\t`weAccepted` integer DEFAULT false NOT NULL,\n\t`sendAnnouncement` text,\n\t`direction` text NOT NULL,\n\t`
|
|
15
|
+
'CREATE TABLE `discussions` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`ownerUserId` text NOT NULL,\n\t`contactUserId` text NOT NULL,\n\t`weAccepted` integer DEFAULT false NOT NULL,\n\t`sendAnnouncement` text,\n\t`direction` text NOT NULL,\n\t`nextSeeker` blob,\n\t`initiationAnnouncement` blob,\n\t`announcementMessage` text,\n\t`lastSyncTimestamp` integer,\n\t`customName` text,\n\t`lastMessageId` integer,\n\t`lastMessageContent` text,\n\t`lastMessageTimestamp` integer,\n\t`unreadCount` integer DEFAULT 0 NOT NULL,\n\t`killedNextRetryAt` integer,\n\t`saturatedRetryAt` integer,\n\t`saturatedRetryDone` integer DEFAULT 0 NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`updatedAt` integer NOT NULL\n);',
|
|
16
16
|
'CREATE UNIQUE INDEX `discussions_owner_contact_idx` ON `discussions` (`ownerUserId`,`contactUserId`);',
|
|
17
|
-
'CREATE INDEX `discussions_owner_status_idx` ON `discussions` (`ownerUserId`,`status`);',
|
|
18
17
|
'CREATE TABLE `messages` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`ownerUserId` text NOT NULL,\n\t`contactUserId` text NOT NULL,\n\t`messageId` blob,\n\t`content` text NOT NULL,\n\t`serializedContent` blob,\n\t`type` text NOT NULL,\n\t`direction` text NOT NULL,\n\t`status` text NOT NULL,\n\t`timestamp` integer NOT NULL,\n\t`metadata` text,\n\t`seeker` blob,\n\t`replyTo` text,\n\t`forwardOf` text,\n\t`encryptedMessage` blob,\n\t`whenToSend` integer\n);',
|
|
19
18
|
'CREATE INDEX `messages_owner_contact_idx` ON `messages` (`ownerUserId`,`contactUserId`);',
|
|
20
19
|
'CREATE INDEX `messages_owner_status_idx` ON `messages` (`ownerUserId`,`status`);',
|
|
@@ -23,6 +22,7 @@ export const MIGRATIONS = [
|
|
|
23
22
|
'CREATE INDEX `messages_owner_contact_dir_idx` ON `messages` (`ownerUserId`,`contactUserId`,`direction`);',
|
|
24
23
|
'CREATE INDEX `messages_owner_dir_status_idx` ON `messages` (`ownerUserId`,`direction`,`status`);',
|
|
25
24
|
'CREATE INDEX `messages_timestamp_idx` ON `messages` (`timestamp`);',
|
|
25
|
+
'CREATE INDEX `messages_owner_contact_msgid_idx` ON `messages` (`ownerUserId`,`contactUserId`,`messageId`);',
|
|
26
26
|
'CREATE TABLE `pendingAnnouncements` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`announcement` blob NOT NULL,\n\t`fetchedAt` integer NOT NULL,\n\t`counter` text\n);',
|
|
27
27
|
'CREATE UNIQUE INDEX `pending_announcements_announcement_idx` ON `pendingAnnouncements` (`announcement`);',
|
|
28
28
|
'CREATE INDEX `pending_announcements_fetchedAt_idx` ON `pendingAnnouncements` (`fetchedAt`);',
|
|
@@ -34,12 +34,4 @@ export const MIGRATIONS = [
|
|
|
34
34
|
'CREATE INDEX `userProfile_status_idx` ON `userProfile` (`status`);',
|
|
35
35
|
],
|
|
36
36
|
},
|
|
37
|
-
{
|
|
38
|
-
idx: 1,
|
|
39
|
-
tag: '0001_flowery_storm',
|
|
40
|
-
when: 1771827090330,
|
|
41
|
-
statements: [
|
|
42
|
-
'CREATE INDEX `messages_owner_contact_msgid_idx` ON `messages` (`ownerUserId`,`contactUserId`,`messageId`);',
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
37
|
];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as schema from '../schema/index.js';
|
|
2
2
|
import type { DatabaseConnection } from '../sqlite.js';
|
|
3
|
-
import type { DiscussionStatus } from '../db';
|
|
4
3
|
export type DiscussionRow = typeof schema.discussions.$inferSelect;
|
|
5
|
-
type DiscussionInsert = typeof schema.discussions.$inferInsert;
|
|
4
|
+
export type DiscussionInsert = typeof schema.discussions.$inferInsert;
|
|
6
5
|
export declare class DiscussionQueries {
|
|
7
6
|
private conn;
|
|
8
7
|
constructor(conn: DatabaseConnection);
|
|
@@ -15,6 +14,4 @@ export declare class DiscussionQueries {
|
|
|
15
14
|
deleteByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<void>;
|
|
16
15
|
incrementUnreadCount(discussionId: number): Promise<void>;
|
|
17
16
|
decrementUnreadCount(discussionId: number): Promise<void>;
|
|
18
|
-
getByOwnerAndStatus(ownerUserId: string, status: DiscussionStatus): Promise<DiscussionRow[]>;
|
|
19
17
|
}
|
|
20
|
-
export {};
|
|
@@ -66,11 +66,4 @@ export class DiscussionQueries {
|
|
|
66
66
|
})
|
|
67
67
|
.where(and(eq(schema.discussions.id, discussionId), gt(schema.discussions.unreadCount, 0)));
|
|
68
68
|
}
|
|
69
|
-
async getByOwnerAndStatus(ownerUserId, status) {
|
|
70
|
-
return this.conn.db
|
|
71
|
-
.select()
|
|
72
|
-
.from(schema.discussions)
|
|
73
|
-
.where(and(eq(schema.discussions.ownerUserId, ownerUserId), eq(schema.discussions.status, status)))
|
|
74
|
-
.all();
|
|
75
|
-
}
|
|
76
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DiscussionDirection
|
|
1
|
+
import type { DiscussionDirection } from '../db';
|
|
2
2
|
export declare const discussions: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
3
3
|
name: "discussions";
|
|
4
4
|
schema: undefined;
|
|
@@ -114,26 +114,6 @@ export declare const discussions: import("drizzle-orm/sqlite-core").SQLiteTableW
|
|
|
114
114
|
length: number | undefined;
|
|
115
115
|
$type: DiscussionDirection;
|
|
116
116
|
}>;
|
|
117
|
-
status: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
118
|
-
name: "status";
|
|
119
|
-
tableName: "discussions";
|
|
120
|
-
dataType: "string";
|
|
121
|
-
columnType: "SQLiteText";
|
|
122
|
-
data: DiscussionStatus;
|
|
123
|
-
driverParam: string;
|
|
124
|
-
notNull: true;
|
|
125
|
-
hasDefault: false;
|
|
126
|
-
isPrimaryKey: false;
|
|
127
|
-
isAutoincrement: false;
|
|
128
|
-
hasRuntimeDefault: false;
|
|
129
|
-
enumValues: [string, ...string[]];
|
|
130
|
-
baseColumn: never;
|
|
131
|
-
identity: undefined;
|
|
132
|
-
generated: undefined;
|
|
133
|
-
}, {}, {
|
|
134
|
-
length: number | undefined;
|
|
135
|
-
$type: DiscussionStatus;
|
|
136
|
-
}>;
|
|
137
117
|
nextSeeker: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
138
118
|
name: "nextSeeker";
|
|
139
119
|
tableName: "discussions";
|
|
@@ -297,6 +277,57 @@ export declare const discussions: import("drizzle-orm/sqlite-core").SQLiteTableW
|
|
|
297
277
|
identity: undefined;
|
|
298
278
|
generated: undefined;
|
|
299
279
|
}, {}, {}>;
|
|
280
|
+
killedNextRetryAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
281
|
+
name: "killedNextRetryAt";
|
|
282
|
+
tableName: "discussions";
|
|
283
|
+
dataType: "date";
|
|
284
|
+
columnType: "SQLiteTimestamp";
|
|
285
|
+
data: Date;
|
|
286
|
+
driverParam: number;
|
|
287
|
+
notNull: false;
|
|
288
|
+
hasDefault: false;
|
|
289
|
+
isPrimaryKey: false;
|
|
290
|
+
isAutoincrement: false;
|
|
291
|
+
hasRuntimeDefault: false;
|
|
292
|
+
enumValues: undefined;
|
|
293
|
+
baseColumn: never;
|
|
294
|
+
identity: undefined;
|
|
295
|
+
generated: undefined;
|
|
296
|
+
}, {}, {}>;
|
|
297
|
+
saturatedRetryAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
298
|
+
name: "saturatedRetryAt";
|
|
299
|
+
tableName: "discussions";
|
|
300
|
+
dataType: "date";
|
|
301
|
+
columnType: "SQLiteTimestamp";
|
|
302
|
+
data: Date;
|
|
303
|
+
driverParam: number;
|
|
304
|
+
notNull: false;
|
|
305
|
+
hasDefault: false;
|
|
306
|
+
isPrimaryKey: false;
|
|
307
|
+
isAutoincrement: false;
|
|
308
|
+
hasRuntimeDefault: false;
|
|
309
|
+
enumValues: undefined;
|
|
310
|
+
baseColumn: never;
|
|
311
|
+
identity: undefined;
|
|
312
|
+
generated: undefined;
|
|
313
|
+
}, {}, {}>;
|
|
314
|
+
saturatedRetryDone: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
315
|
+
name: "saturatedRetryDone";
|
|
316
|
+
tableName: "discussions";
|
|
317
|
+
dataType: "boolean";
|
|
318
|
+
columnType: "SQLiteBoolean";
|
|
319
|
+
data: boolean;
|
|
320
|
+
driverParam: number;
|
|
321
|
+
notNull: true;
|
|
322
|
+
hasDefault: true;
|
|
323
|
+
isPrimaryKey: false;
|
|
324
|
+
isAutoincrement: false;
|
|
325
|
+
hasRuntimeDefault: false;
|
|
326
|
+
enumValues: undefined;
|
|
327
|
+
baseColumn: never;
|
|
328
|
+
identity: undefined;
|
|
329
|
+
generated: undefined;
|
|
330
|
+
}, {}, {}>;
|
|
300
331
|
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
301
332
|
name: "createdAt";
|
|
302
333
|
tableName: "discussions";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sqliteTable, text, integer,
|
|
1
|
+
import { sqliteTable, text, integer, uniqueIndex, } from 'drizzle-orm/sqlite-core';
|
|
2
2
|
import { bytes } from './_helpers.js';
|
|
3
3
|
export const discussions = sqliteTable('discussions', {
|
|
4
4
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
@@ -9,7 +9,6 @@ export const discussions = sqliteTable('discussions', {
|
|
|
9
9
|
.default(false),
|
|
10
10
|
sendAnnouncement: text('sendAnnouncement'),
|
|
11
11
|
direction: text('direction').$type().notNull(),
|
|
12
|
-
status: text('status').$type().notNull(),
|
|
13
12
|
nextSeeker: bytes('nextSeeker'),
|
|
14
13
|
initiationAnnouncement: bytes('initiationAnnouncement'),
|
|
15
14
|
announcementMessage: text('announcementMessage'),
|
|
@@ -23,9 +22,13 @@ export const discussions = sqliteTable('discussions', {
|
|
|
23
22
|
mode: 'timestamp_ms',
|
|
24
23
|
}),
|
|
25
24
|
unreadCount: integer('unreadCount').notNull().default(0),
|
|
25
|
+
killedNextRetryAt: integer('killedNextRetryAt', { mode: 'timestamp_ms' }),
|
|
26
|
+
saturatedRetryAt: integer('saturatedRetryAt', { mode: 'timestamp_ms' }),
|
|
27
|
+
saturatedRetryDone: integer('saturatedRetryDone', { mode: 'boolean' })
|
|
28
|
+
.notNull()
|
|
29
|
+
.default(false),
|
|
26
30
|
createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
|
|
27
31
|
updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(),
|
|
28
32
|
}, table => [
|
|
29
33
|
uniqueIndex('discussions_owner_contact_idx').on(table.ownerUserId, table.contactUserId),
|
|
30
|
-
index('discussions_owner_status_idx').on(table.ownerUserId, table.status),
|
|
31
34
|
]);
|
package/dist/db/sqlite-worker.js
CHANGED
|
@@ -14,6 +14,9 @@ import * as SQLite from 'wa-sqlite';
|
|
|
14
14
|
import { execStatements } from './exec-utils.js';
|
|
15
15
|
let sqlite3 = null;
|
|
16
16
|
let dbHandle = null;
|
|
17
|
+
// Add a message queue to process messages sequentially
|
|
18
|
+
const messageQueue = [];
|
|
19
|
+
let processing = false;
|
|
17
20
|
// Bind Worker's postMessage (avoids DOM lib signature mismatch at compile time)
|
|
18
21
|
const post = globalThis.postMessage.bind(globalThis);
|
|
19
22
|
async function execSql(sql, params) {
|
|
@@ -28,7 +31,22 @@ async function execSql(sql, params) {
|
|
|
28
31
|
}
|
|
29
32
|
return { rows, lastInsertRowId };
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
// Ensure that all messages are processed sequentially to avoid
|
|
35
|
+
// concurrent access to the underlying WASM/SQLite state, which
|
|
36
|
+
// can lead to heap corruption and "index out of bounds"/
|
|
37
|
+
// "unreachable executed" errors.
|
|
38
|
+
async function processMessageQueue() {
|
|
39
|
+
if (processing || messageQueue.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
processing = true;
|
|
42
|
+
while (messageQueue.length > 0) {
|
|
43
|
+
const { e, resolve } = messageQueue.shift();
|
|
44
|
+
await handleMessage(e);
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
processing = false;
|
|
48
|
+
}
|
|
49
|
+
async function handleMessage(e) {
|
|
32
50
|
const { id, type } = e.data;
|
|
33
51
|
try {
|
|
34
52
|
switch (type) {
|
|
@@ -87,4 +105,13 @@ addEventListener('message', async (e) => {
|
|
|
87
105
|
catch (err) {
|
|
88
106
|
post({ id, type: 'error', message: err.message });
|
|
89
107
|
}
|
|
108
|
+
}
|
|
109
|
+
addEventListener('message', (e) => {
|
|
110
|
+
// Chain each message onto the promise queue so that messages
|
|
111
|
+
// are handled strictly one after another.
|
|
112
|
+
let resolve;
|
|
113
|
+
const promise = new Promise(r => (resolve = r));
|
|
114
|
+
messageQueue.push({ e, resolve });
|
|
115
|
+
promise.then(() => processMessageQueue());
|
|
116
|
+
processMessageQueue();
|
|
90
117
|
});
|
package/dist/gossip.js
CHANGED
|
@@ -233,7 +233,7 @@ class GossipSdk {
|
|
|
233
233
|
this._announcement = new AnnouncementService(messageProtocol, session, this.eventEmitter, config, queries);
|
|
234
234
|
this._discussion = new DiscussionService(this._announcement, session, this.eventEmitter, queries);
|
|
235
235
|
this._message = new MessageService(messageProtocol, session, this.eventEmitter, config, queries);
|
|
236
|
-
this._refresh = new RefreshService(this._message, this._discussion, this._announcement, session, this.eventEmitter, queries);
|
|
236
|
+
this._refresh = new RefreshService(this._message, this._discussion, this._announcement, session, this.eventEmitter, queries, this.config);
|
|
237
237
|
// Publish gossip ID (public key) on messageProtocol so the user is discoverable
|
|
238
238
|
await this._auth.ensurePublicKeyPublished(session.ourPk, session.userIdEncoded);
|
|
239
239
|
// Now set refreshService on services (circular dependency resolved via setter)
|
|
@@ -470,6 +470,9 @@ class GossipSdk {
|
|
|
470
470
|
handleSessionRefresh: async () => {
|
|
471
471
|
await this.updateState();
|
|
472
472
|
},
|
|
473
|
+
refreshSessionsStatusEvent: async () => {
|
|
474
|
+
await this._refresh?.refreshSessionsStatusEvent();
|
|
475
|
+
},
|
|
473
476
|
}, this.eventEmitter);
|
|
474
477
|
}
|
|
475
478
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles broadcasting and processing of session announcements.
|
|
5
5
|
*/
|
|
6
|
-
import { DiscussionDirection,
|
|
6
|
+
import { DiscussionDirection, MessageDirection, MessageStatus, MessageType, serializeSendAnnouncement, } from '../db/index.js';
|
|
7
7
|
import { toDiscussion } from '../utils/discussions.js';
|
|
8
8
|
import { decodeUserId, encodeUserId } from '../utils/userId.js';
|
|
9
9
|
import { SessionStatus } from '../wasm/bindings.js';
|
|
@@ -153,6 +153,9 @@ export class AnnouncementService {
|
|
|
153
153
|
when_to_send: new Date(Date.now() + this.config.announcements.retryDelayMs),
|
|
154
154
|
}),
|
|
155
155
|
updatedAt: new Date(),
|
|
156
|
+
/* If send announcement failed, There are chances that the session will be killed again the next state_update call.
|
|
157
|
+
We don't want to wait a delay before reseting the session so we set killedNextRetryAt to null*/
|
|
158
|
+
killedNextRetryAt: null,
|
|
156
159
|
});
|
|
157
160
|
}
|
|
158
161
|
}
|
|
@@ -432,7 +435,6 @@ export class AnnouncementService {
|
|
|
432
435
|
ownerUserId,
|
|
433
436
|
contactUserId,
|
|
434
437
|
direction: DiscussionDirection.RECEIVED,
|
|
435
|
-
status: DiscussionStatus.PENDING,
|
|
436
438
|
announcementMessage: message,
|
|
437
439
|
unreadCount: 0,
|
|
438
440
|
createdAt: new Date(),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Class-based service for initializing, accepting, and managing discussions.
|
|
5
5
|
*/
|
|
6
|
-
import { DiscussionDirection,
|
|
6
|
+
import { DiscussionDirection, MessageDirection, MessageStatus, MessageType, serializeSendAnnouncement, } from '../db/index.js';
|
|
7
7
|
import { toDiscussion, toSortedDiscussions, updateDiscussionName, } from '../utils/discussions.js';
|
|
8
8
|
import { encodeAnnouncementPayload, } from '../utils/announcementPayload.js';
|
|
9
9
|
import { UserPublicKeys } from '../wasm/bindings.js';
|
|
@@ -113,7 +113,6 @@ export class DiscussionService {
|
|
|
113
113
|
contactUserId: contact.userId,
|
|
114
114
|
weAccepted: true,
|
|
115
115
|
direction: DiscussionDirection.INITIATED,
|
|
116
|
-
status: DiscussionStatus.PENDING,
|
|
117
116
|
unreadCount: 0,
|
|
118
117
|
createdAt: new Date(),
|
|
119
118
|
updatedAt: new Date(),
|
package/dist/services/message.js
CHANGED
|
@@ -581,14 +581,19 @@ export class MessageService {
|
|
|
581
581
|
}
|
|
582
582
|
const peerId = decodeUserId(contactUserId);
|
|
583
583
|
const sessionStatus = this.session.peerSessionStatus(peerId);
|
|
584
|
-
if (
|
|
585
|
-
|
|
584
|
+
if (![
|
|
585
|
+
SessionStatus.Active,
|
|
586
|
+
// saturated sessions can't send messages on session manager but it's still possible to send on network msg that have already been encrypted if any
|
|
587
|
+
SessionStatus.Saturated,
|
|
588
|
+
].includes(sessionStatus)) {
|
|
589
|
+
log.info('session neither active nor saturated, skipping send queue', {
|
|
586
590
|
contactUserId,
|
|
587
591
|
sessionStatus: sessionStatusToString(sessionStatus),
|
|
588
592
|
});
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
593
|
+
return {
|
|
594
|
+
success: false,
|
|
595
|
+
error: new Error('Session neither active nor saturated'),
|
|
596
|
+
};
|
|
592
597
|
}
|
|
593
598
|
// retrieve all messages in send queue that need to be updated for this contact
|
|
594
599
|
const pendingMessages = (await this.queries.messages.getSendQueue(ownerUserId, contactUserId)).map(rowToMessage);
|
|
@@ -613,7 +618,9 @@ export class MessageService {
|
|
|
613
618
|
let encryptedMessage = msg.encryptedMessage;
|
|
614
619
|
let seeker = msg.seeker;
|
|
615
620
|
let whenToSend = msg.whenToSend;
|
|
616
|
-
|
|
621
|
+
// If the sessions is saturated it can't send messages on session manager
|
|
622
|
+
if (currentStatus === MessageStatus.WAITING_SESSION &&
|
|
623
|
+
sessionStatus === SessionStatus.Active) {
|
|
617
624
|
let serializedContent = msg.serializedContent;
|
|
618
625
|
if (!serializedContent) {
|
|
619
626
|
const serializeResult = await this.serializeMessage(msg);
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Class-based service to periodically refresh sessions and handle
|
|
5
5
|
* keep-alive messages and broken sessions.
|
|
6
6
|
*/
|
|
7
|
+
import { type Queries } from '../db/index.js';
|
|
7
8
|
import type { SessionModule } from '../wasm/session.js';
|
|
8
9
|
import { MessageService } from './message.js';
|
|
9
10
|
import { AnnouncementService } from './announcement.js';
|
|
10
11
|
import { DiscussionService } from './discussion.js';
|
|
11
12
|
import { SdkEventEmitter } from '../core/SdkEventEmitter.js';
|
|
12
|
-
import {
|
|
13
|
+
import type { SdkConfig } from '../config/sdk.js';
|
|
13
14
|
/**
|
|
14
15
|
* Service for refreshing sessions and handling keep-alive messages.
|
|
15
16
|
*
|
|
@@ -35,7 +36,14 @@ export declare class RefreshService {
|
|
|
35
36
|
private isUpdating;
|
|
36
37
|
private eventEmitter;
|
|
37
38
|
private queries;
|
|
38
|
-
|
|
39
|
+
private config;
|
|
40
|
+
private sessionStatusMap;
|
|
41
|
+
constructor(messageService: MessageService, discussionService: DiscussionService, announcementService: AnnouncementService, session: SessionModule, eventEmitter: SdkEventEmitter, queries: Queries, config: SdkConfig);
|
|
42
|
+
/**
|
|
43
|
+
* Emit sessionStatusChanged events for discussions whose session status
|
|
44
|
+
* has changed since the last check.
|
|
45
|
+
*/
|
|
46
|
+
refreshSessionsStatusEvent(): Promise<void>;
|
|
39
47
|
/**
|
|
40
48
|
* Update state for all discussions:
|
|
41
49
|
* - Cleanup orphaned peers
|
|
@@ -46,4 +54,7 @@ export declare class RefreshService {
|
|
|
46
54
|
* @param discussions - Array of discussions to process
|
|
47
55
|
*/
|
|
48
56
|
stateUpdate(): Promise<void>;
|
|
57
|
+
private getJitteredDelayMs;
|
|
58
|
+
private updateSessionRecovery;
|
|
59
|
+
private handleSessionStatus;
|
|
49
60
|
}
|
package/dist/services/refresh.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Class-based service to periodically refresh sessions and handle
|
|
5
5
|
* keep-alive messages and broken sessions.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { MessageDirection, MessageStatus, MessageType, } from '../db/index.js';
|
|
8
8
|
import { toSortedDiscussions } from '../utils/discussions.js';
|
|
9
9
|
import { SessionStatus } from '../wasm/bindings.js';
|
|
10
10
|
import { decodeUserId, encodeUserId } from '../utils/userId.js';
|
|
@@ -29,7 +29,7 @@ const logger = new Logger('RefreshService');
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
export class RefreshService {
|
|
32
|
-
constructor(messageService, discussionService, announcementService, session, eventEmitter, queries) {
|
|
32
|
+
constructor(messageService, discussionService, announcementService, session, eventEmitter, queries, config) {
|
|
33
33
|
Object.defineProperty(this, "messageService", {
|
|
34
34
|
enumerable: true,
|
|
35
35
|
configurable: true,
|
|
@@ -72,12 +72,46 @@ export class RefreshService {
|
|
|
72
72
|
writable: true,
|
|
73
73
|
value: void 0
|
|
74
74
|
});
|
|
75
|
+
Object.defineProperty(this, "config", {
|
|
76
|
+
enumerable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
writable: true,
|
|
79
|
+
value: void 0
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(this, "sessionStatusMap", {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
value: new Map()
|
|
86
|
+
});
|
|
75
87
|
this.messageService = messageService;
|
|
76
88
|
this.discussionService = discussionService;
|
|
77
89
|
this.announcementService = announcementService;
|
|
78
90
|
this.session = session;
|
|
79
91
|
this.eventEmitter = eventEmitter;
|
|
80
92
|
this.queries = queries;
|
|
93
|
+
this.config = config;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Emit sessionStatusChanged events for discussions whose session status
|
|
97
|
+
* has changed since the last check.
|
|
98
|
+
*/
|
|
99
|
+
async refreshSessionsStatusEvent() {
|
|
100
|
+
const ownerUserId = this.session.userIdEncoded;
|
|
101
|
+
if (!ownerUserId) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const allRows = await this.queries.discussions.getByOwner(ownerUserId);
|
|
105
|
+
const discussions = toSortedDiscussions(allRows);
|
|
106
|
+
for (const discussion of discussions) {
|
|
107
|
+
const peerId = decodeUserId(discussion.contactUserId);
|
|
108
|
+
const status = this.session.peerSessionStatus(peerId);
|
|
109
|
+
const previous = this.sessionStatusMap.get(discussion.contactUserId);
|
|
110
|
+
if (previous !== status) {
|
|
111
|
+
this.sessionStatusMap.set(discussion.contactUserId, status);
|
|
112
|
+
this.eventEmitter.emit(SdkEventType.SESSION_STATUS_CHANGED, discussion.contactUserId, status);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
81
115
|
}
|
|
82
116
|
/**
|
|
83
117
|
* Update state for all discussions:
|
|
@@ -128,39 +162,27 @@ export class RefreshService {
|
|
|
128
162
|
for (const discussion of discussions) {
|
|
129
163
|
const peerId = decodeUserId(discussion.contactUserId);
|
|
130
164
|
const status = this.session.peerSessionStatus(peerId);
|
|
131
|
-
|
|
132
|
-
SessionStatus.Killed,
|
|
133
|
-
SessionStatus.Saturated,
|
|
134
|
-
SessionStatus.NoSession,
|
|
135
|
-
SessionStatus.UnknownPeer,
|
|
136
|
-
].includes(status)) {
|
|
137
|
-
if (discussion.weAccepted) {
|
|
138
|
-
const res = await this.discussionService.createSessionForContact(discussion.contactUserId, new Uint8Array(0));
|
|
139
|
-
if (!res.success) {
|
|
140
|
-
log.error('failed to create session for contact', {
|
|
141
|
-
contactUserId: discussion.contactUserId,
|
|
142
|
-
error: res.error,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
this.eventEmitter.emit(SdkEventType.SESSION_RENEWED, discussion);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
165
|
+
await this.handleSessionStatus(discussion, status);
|
|
150
166
|
}
|
|
151
167
|
// Step 2: send announcements
|
|
152
168
|
const refreshRows = await this.queries.discussions.getByOwner(ownerUserId);
|
|
153
169
|
const discussionsAfterRefresh = toSortedDiscussions(refreshRows);
|
|
154
170
|
const activePendingDiscussions = discussionsAfterRefresh.filter(discussion => {
|
|
155
171
|
const status = this.session.peerSessionStatus(decodeUserId(discussion.contactUserId));
|
|
156
|
-
return [
|
|
172
|
+
return [
|
|
173
|
+
SessionStatus.Active,
|
|
174
|
+
SessionStatus.SelfRequested,
|
|
175
|
+
SessionStatus.Saturated,
|
|
176
|
+
].includes(status);
|
|
157
177
|
});
|
|
158
178
|
await this.announcementService.processOutgoingAnnouncements(activePendingDiscussions);
|
|
159
179
|
// Step 3: send queued messages and keep-alives
|
|
160
|
-
// NOTE: only flush queued messages once the session is fully Active.
|
|
180
|
+
// NOTE: only flush queued messages once the session is fully Active or saturated.
|
|
161
181
|
// SelfRequested means the handshake is still establishing and
|
|
162
182
|
// session.sendMessage() can return null.
|
|
163
|
-
const activeEstablishedDiscussions = activePendingDiscussions.filter(discussion =>
|
|
183
|
+
const activeEstablishedDiscussions = activePendingDiscussions.filter(discussion =>
|
|
184
|
+
// saturated sessions can't send messages on session manager but it's still possible to send on network msg that have already been encrypted if any
|
|
185
|
+
[SessionStatus.Active, SessionStatus.Saturated].includes(this.session.peerSessionStatus(decodeUserId(discussion.contactUserId))));
|
|
164
186
|
const keepAliveSet = new Set(keepAlivePeerIds);
|
|
165
187
|
for (const discussion of activeEstablishedDiscussions) {
|
|
166
188
|
if (!discussion.weAccepted)
|
|
@@ -202,4 +224,126 @@ export class RefreshService {
|
|
|
202
224
|
this.isUpdating = false;
|
|
203
225
|
}
|
|
204
226
|
}
|
|
227
|
+
getJitteredDelayMs(baseMs, jitterMs) {
|
|
228
|
+
const jitter = (Math.random() * 2 - 1) * jitterMs;
|
|
229
|
+
return Math.max(0, Math.round(baseMs + jitter));
|
|
230
|
+
}
|
|
231
|
+
async updateSessionRecovery(discussion, updates) {
|
|
232
|
+
if (!discussion.id) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!updates) {
|
|
236
|
+
// Clear all recovery fields
|
|
237
|
+
if ((discussion.killedNextRetryAt === null ||
|
|
238
|
+
discussion.killedNextRetryAt === undefined) &&
|
|
239
|
+
(discussion.saturatedRetryAt === null ||
|
|
240
|
+
discussion.saturatedRetryAt === undefined) &&
|
|
241
|
+
(discussion.saturatedRetryDone === null ||
|
|
242
|
+
discussion.saturatedRetryDone === undefined)) {
|
|
243
|
+
return; // Already cleared
|
|
244
|
+
}
|
|
245
|
+
const updateData = {
|
|
246
|
+
killedNextRetryAt: null,
|
|
247
|
+
saturatedRetryAt: null,
|
|
248
|
+
saturatedRetryDone: false,
|
|
249
|
+
};
|
|
250
|
+
await this.queries.discussions.updateById(discussion.id, updateData);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const currentNormalized = {
|
|
254
|
+
killedNextRetryAt: discussion.killedNextRetryAt?.getTime() ?? null,
|
|
255
|
+
saturatedRetryAt: discussion.saturatedRetryAt?.getTime() ?? null,
|
|
256
|
+
};
|
|
257
|
+
const nextNormalized = {
|
|
258
|
+
killedNextRetryAt: updates.killedNextRetryAt?.getTime() ?? null,
|
|
259
|
+
saturatedRetryAt: updates.saturatedRetryAt?.getTime() ?? null,
|
|
260
|
+
};
|
|
261
|
+
const isSame = currentNormalized.killedNextRetryAt ===
|
|
262
|
+
nextNormalized.killedNextRetryAt &&
|
|
263
|
+
currentNormalized.saturatedRetryAt === nextNormalized.saturatedRetryAt &&
|
|
264
|
+
discussion.saturatedRetryDone === updates.saturatedRetryDone;
|
|
265
|
+
if (isSame) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const updateData = {
|
|
269
|
+
killedNextRetryAt: updates.killedNextRetryAt,
|
|
270
|
+
saturatedRetryAt: updates.saturatedRetryAt,
|
|
271
|
+
saturatedRetryDone: updates.saturatedRetryDone,
|
|
272
|
+
};
|
|
273
|
+
await this.queries.discussions.updateById(discussion.id, updateData);
|
|
274
|
+
}
|
|
275
|
+
async handleSessionStatus(discussion, status) {
|
|
276
|
+
const now = new Date();
|
|
277
|
+
const log = logger.forMethod('handleSessionStatus');
|
|
278
|
+
if (status === SessionStatus.Active) {
|
|
279
|
+
await this.updateSessionRecovery(discussion, undefined);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if ([SessionStatus.SelfRequested, SessionStatus.PeerRequested].includes(status)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!discussion.weAccepted) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (status === SessionStatus.NoSession ||
|
|
289
|
+
status === SessionStatus.UnknownPeer) {
|
|
290
|
+
log.error('no session or unknown peer', {
|
|
291
|
+
contactUserId: discussion.contactUserId,
|
|
292
|
+
status: status,
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (status === SessionStatus.Killed) {
|
|
297
|
+
const nextRetryAt = discussion.killedNextRetryAt;
|
|
298
|
+
if (nextRetryAt && nextRetryAt.getTime() > now.getTime()) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const res = await this.discussionService.createSessionForContact(discussion.contactUserId, new Uint8Array(0));
|
|
302
|
+
if (!res.success) {
|
|
303
|
+
log.error('failed to create session for contact', {
|
|
304
|
+
contactUserId: discussion.contactUserId,
|
|
305
|
+
error: res.error,
|
|
306
|
+
});
|
|
307
|
+
return; // if we failed to create session, we don't want to set killedNextRetryAt
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
this.eventEmitter.emit(SdkEventType.SESSION_RENEWED, discussion);
|
|
311
|
+
}
|
|
312
|
+
const delayMs = this.getJitteredDelayMs(this.config.sessionRecovery.killedRetryDelayMs, this.config.sessionRecovery.JitterMs);
|
|
313
|
+
await this.updateSessionRecovery(discussion, {
|
|
314
|
+
killedNextRetryAt: new Date(now.getTime() + delayMs),
|
|
315
|
+
saturatedRetryDone: false,
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (status === SessionStatus.Saturated) {
|
|
320
|
+
const retryAt = discussion.saturatedRetryAt;
|
|
321
|
+
if (discussion.saturatedRetryDone ||
|
|
322
|
+
(retryAt && retryAt.getTime() > now.getTime())) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!retryAt) {
|
|
326
|
+
const delayMs = this.getJitteredDelayMs(this.config.sessionRecovery.saturatedRetryDelayMs, this.config.sessionRecovery.JitterMs);
|
|
327
|
+
await this.updateSessionRecovery(discussion, {
|
|
328
|
+
saturatedRetryAt: new Date(now.getTime() + delayMs),
|
|
329
|
+
saturatedRetryDone: false,
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const res = await this.discussionService.createSessionForContact(discussion.contactUserId, new Uint8Array(0));
|
|
334
|
+
if (!res.success) {
|
|
335
|
+
log.error('failed to create session for contact', {
|
|
336
|
+
contactUserId: discussion.contactUserId,
|
|
337
|
+
error: res.error,
|
|
338
|
+
});
|
|
339
|
+
return; // if we failed to create session, we don't want to set saturatedRetryDone to true
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
this.eventEmitter.emit(SdkEventType.SESSION_RENEWED, discussion);
|
|
343
|
+
}
|
|
344
|
+
await this.updateSessionRecovery(discussion, {
|
|
345
|
+
saturatedRetryDone: true,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
205
349
|
}
|
package/dist/wasm/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { SessionModule, sessionStatusToString } from './session.js';
|
|
|
8
8
|
export { initializeWasm, ensureWasmInitialized, startWasmInitialization, } from './loader.js';
|
|
9
9
|
export * from './encryption.js';
|
|
10
10
|
export * from './userKeys.js';
|
|
11
|
-
export { SessionStatus, UserPublicKeys, UserSecretKeys, SendMessageOutput, ReceiveMessageOutput, AnnouncementResult, } from './bindings.js';
|
|
11
|
+
export { SessionStatus, SessionConfig, UserPublicKeys, UserSecretKeys, SendMessageOutput, ReceiveMessageOutput, AnnouncementResult, } from './bindings.js';
|
package/dist/wasm/index.js
CHANGED
|
@@ -10,4 +10,4 @@ export { SessionModule, sessionStatusToString } from './session.js';
|
|
|
10
10
|
export { initializeWasm, ensureWasmInitialized, startWasmInitialization, } from './loader.js';
|
|
11
11
|
export * from './encryption.js';
|
|
12
12
|
export * from './userKeys.js';
|
|
13
|
-
export { SessionStatus, UserPublicKeys, UserSecretKeys, SendMessageOutput, ReceiveMessageOutput, AnnouncementResult, } from './bindings.js';
|
|
13
|
+
export { SessionStatus, SessionConfig, UserPublicKeys, UserSecretKeys, SendMessageOutput, ReceiveMessageOutput, AnnouncementResult, } from './bindings.js';
|
package/package.json
CHANGED