@massalabs/gossip-sdk 0.0.2-dev.20260323092903 → 0.0.2-dev.20260324105033
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/core/SdkEventEmitter.d.ts +2 -0
- package/dist/core/SdkEventEmitter.js +2 -0
- package/dist/db/db.d.ts +4 -1
- package/dist/db/db.js +1 -0
- package/dist/db/generated-migrations.js +9 -0
- package/dist/db/queries/discussions.d.ts +1 -0
- package/dist/db/queries/discussions.js +6 -0
- package/dist/db/queries/messages.d.ts +8 -0
- package/dist/db/queries/messages.js +33 -1
- package/dist/db/schema/discussions.d.ts +34 -0
- package/dist/db/schema/discussions.js +2 -0
- package/dist/gossip.js +1 -0
- package/dist/proto/generated/message.d.ts +2 -1
- package/dist/proto/generated/message.js +1 -0
- package/dist/services/discussion.d.ts +12 -0
- package/dist/services/discussion.js +47 -0
- package/dist/services/message.d.ts +6 -0
- package/dist/services/message.js +42 -5
- package/dist/services/refresh.js +2 -0
- package/dist/services/selfMessage.d.ts +5 -0
- package/dist/services/selfMessage.js +14 -0
- package/dist/utils/messageSerialization.d.ts +8 -0
- package/dist/utils/messageSerialization.js +19 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ export declare enum SdkEventType {
|
|
|
16
16
|
SESSION_ACCEPTED = "sessionAccepted",
|
|
17
17
|
SEEKERS_UPDATED = "seekersUpdated",
|
|
18
18
|
SESSION_STATUS_CHANGED = "sessionStatusChanged",
|
|
19
|
+
DISCUSSION_UPDATED = "discussionUpdated",
|
|
19
20
|
ERROR = "error"
|
|
20
21
|
}
|
|
21
22
|
export interface SdkEventHandlers {
|
|
@@ -29,6 +30,7 @@ export interface SdkEventHandlers {
|
|
|
29
30
|
[SdkEventType.SESSION_ACCEPTED]: (contactUserId: string) => void;
|
|
30
31
|
[SdkEventType.SEEKERS_UPDATED]: (seekers: Uint8Array[]) => void;
|
|
31
32
|
[SdkEventType.SESSION_STATUS_CHANGED]: (contactUserId: string, status: SessionStatus) => void;
|
|
33
|
+
[SdkEventType.DISCUSSION_UPDATED]: (contactUserId: string) => void;
|
|
32
34
|
[SdkEventType.ERROR]: (error: Error, context: string) => void;
|
|
33
35
|
}
|
|
34
36
|
export declare class SdkEventEmitter {
|
|
@@ -18,6 +18,7 @@ export var SdkEventType;
|
|
|
18
18
|
SdkEventType["SESSION_ACCEPTED"] = "sessionAccepted";
|
|
19
19
|
SdkEventType["SEEKERS_UPDATED"] = "seekersUpdated";
|
|
20
20
|
SdkEventType["SESSION_STATUS_CHANGED"] = "sessionStatusChanged";
|
|
21
|
+
SdkEventType["DISCUSSION_UPDATED"] = "discussionUpdated";
|
|
21
22
|
SdkEventType["ERROR"] = "error";
|
|
22
23
|
})(SdkEventType || (SdkEventType = {}));
|
|
23
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -40,6 +41,7 @@ export class SdkEventEmitter {
|
|
|
40
41
|
[SdkEventType.SESSION_ACCEPTED]: new Set(),
|
|
41
42
|
[SdkEventType.SEEKERS_UPDATED]: new Set(),
|
|
42
43
|
[SdkEventType.SESSION_STATUS_CHANGED]: new Set(),
|
|
44
|
+
[SdkEventType.DISCUSSION_UPDATED]: new Set(),
|
|
43
45
|
[SdkEventType.ERROR]: new Set(),
|
|
44
46
|
}
|
|
45
47
|
});
|
package/dist/db/db.d.ts
CHANGED
|
@@ -100,7 +100,8 @@ export declare enum MessageType {
|
|
|
100
100
|
AUDIO = "audio",
|
|
101
101
|
VIDEO = "video",
|
|
102
102
|
DELETED = "deleted",
|
|
103
|
-
REACTION = "reaction"
|
|
103
|
+
REACTION = "reaction",
|
|
104
|
+
RETENTION_POLICY = "retention_policy"
|
|
104
105
|
}
|
|
105
106
|
export interface ReadyAnnouncement {
|
|
106
107
|
announcement_bytes: Uint8Array;
|
|
@@ -142,6 +143,8 @@ export interface Discussion {
|
|
|
142
143
|
lastMessageTimestamp: Date | null;
|
|
143
144
|
unreadCount: number;
|
|
144
145
|
pinned: boolean;
|
|
146
|
+
messageRetentionDuration: number | null;
|
|
147
|
+
retentionPolicySetAt: number | null;
|
|
145
148
|
killedNextRetryAt?: Date | null;
|
|
146
149
|
saturatedRetryAt?: Date | null;
|
|
147
150
|
saturatedRetryDone: boolean;
|
package/dist/db/db.js
CHANGED
|
@@ -38,6 +38,7 @@ export var MessageType;
|
|
|
38
38
|
MessageType["VIDEO"] = "video";
|
|
39
39
|
MessageType["DELETED"] = "deleted";
|
|
40
40
|
MessageType["REACTION"] = "reaction";
|
|
41
|
+
MessageType["RETENTION_POLICY"] = "retention_policy";
|
|
41
42
|
})(MessageType || (MessageType = {}));
|
|
42
43
|
/** Serialize a SendAnnouncement to a JSON string for SQLite text column */
|
|
43
44
|
export function serializeSendAnnouncement(announcement) {
|
|
@@ -54,4 +54,13 @@ export const MIGRATIONS = [
|
|
|
54
54
|
when: 1742000000000,
|
|
55
55
|
statements: ['ALTER TABLE `messages` ADD COLUMN `reactionOf` text;'],
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
idx: 4,
|
|
59
|
+
tag: '0004_discussions_retention',
|
|
60
|
+
when: 1743000000000,
|
|
61
|
+
statements: [
|
|
62
|
+
'ALTER TABLE `discussions` ADD COLUMN `messageRetentionDuration` integer;',
|
|
63
|
+
'ALTER TABLE `discussions` ADD COLUMN `retentionPolicySetAt` integer;',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
57
66
|
];
|
|
@@ -10,6 +10,7 @@ export declare class DiscussionQueries {
|
|
|
10
10
|
getById(id: number): Promise<DiscussionRow | undefined>;
|
|
11
11
|
insert(values: DiscussionInsert): Promise<number>;
|
|
12
12
|
updateById(id: number, data: Partial<DiscussionInsert>): Promise<void>;
|
|
13
|
+
updateByOwnerAndContact(ownerUserId: string, contactUserId: string, data: Partial<DiscussionInsert>): Promise<void>;
|
|
13
14
|
deleteById(id: number): Promise<void>;
|
|
14
15
|
deleteByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<void>;
|
|
15
16
|
incrementUnreadCount(discussionId: number): Promise<void>;
|
|
@@ -40,6 +40,12 @@ export class DiscussionQueries {
|
|
|
40
40
|
.set(data)
|
|
41
41
|
.where(eq(schema.discussions.id, id));
|
|
42
42
|
}
|
|
43
|
+
async updateByOwnerAndContact(ownerUserId, contactUserId, data) {
|
|
44
|
+
await this.conn.db
|
|
45
|
+
.update(schema.discussions)
|
|
46
|
+
.set(data)
|
|
47
|
+
.where(and(eq(schema.discussions.ownerUserId, ownerUserId), eq(schema.discussions.contactUserId, contactUserId)));
|
|
48
|
+
}
|
|
43
49
|
async deleteById(id) {
|
|
44
50
|
await this.conn.db
|
|
45
51
|
.delete(schema.discussions)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DiscussionRow } from './discussions.js';
|
|
1
2
|
import * as schema from '../schema/index.js';
|
|
2
3
|
import type { DatabaseConnection } from '../sqlite.js';
|
|
3
4
|
import { MessageStatus } from '../../db/db.js';
|
|
@@ -9,6 +10,7 @@ export declare class MessageQueries {
|
|
|
9
10
|
getById(id: number): Promise<MessageRow | undefined>;
|
|
10
11
|
getByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
|
|
11
12
|
getVisibleByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
|
|
13
|
+
getLastVisibleByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow | undefined>;
|
|
12
14
|
getReactionsByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
|
|
13
15
|
getByOwnerAndSeeker(ownerUserId: string, seeker: Uint8Array): Promise<MessageRow | undefined>;
|
|
14
16
|
findByMessageId(ownerUserId: string, contactUserId: string, messageId: Uint8Array): Promise<MessageRow | undefined>;
|
|
@@ -24,6 +26,12 @@ export declare class MessageQueries {
|
|
|
24
26
|
getByStatus(ownerUserId: string, status: MessageStatus): Promise<MessageRow[]>;
|
|
25
27
|
resetSendQueue(ownerUserId: string, contactUserId: string): Promise<void>;
|
|
26
28
|
getAnnouncementsByContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Hard-delete messages older than each discussion's retention duration.
|
|
31
|
+
* Only processes discussions that have a non-null messageRetentionDuration.
|
|
32
|
+
* Skips KEEP_ALIVE and ANNOUNCEMENT types.
|
|
33
|
+
*/
|
|
34
|
+
deleteExpiredByOwner(ownerUserId: string, discussions: DiscussionRow[]): Promise<void>;
|
|
27
35
|
findDuplicateIncoming(ownerUserId: string, contactUserId: string, content: string, windowStart: Date, windowEnd: Date): Promise<{
|
|
28
36
|
id: number;
|
|
29
37
|
} | undefined>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, and, or, sql, inArray, asc, ne } from 'drizzle-orm';
|
|
1
|
+
import { eq, and, or, sql, inArray, asc, ne, lt, gte } from 'drizzle-orm';
|
|
2
2
|
import * as schema from '../schema/index.js';
|
|
3
3
|
import { MessageDirection, MessageStatus, MessageType } from '../../db/db.js';
|
|
4
4
|
export class MessageQueries {
|
|
@@ -32,6 +32,8 @@ export class MessageQueries {
|
|
|
32
32
|
.where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, contactUserId),
|
|
33
33
|
// Hide keep-alive messages from UI
|
|
34
34
|
ne(schema.messages.type, MessageType.KEEP_ALIVE),
|
|
35
|
+
// Hide retention policy control messages from UI
|
|
36
|
+
ne(schema.messages.type, MessageType.RETENTION_POLICY),
|
|
35
37
|
// Hide reaction messages (and any deleted reaction rows) from main message list
|
|
36
38
|
ne(schema.messages.type, MessageType.REACTION), sql `reactionOf IS NULL`,
|
|
37
39
|
// Hide delete control messages (outgoing DELETED with empty content)
|
|
@@ -41,6 +43,15 @@ export class MessageQueries {
|
|
|
41
43
|
.orderBy(asc(schema.messages.id))
|
|
42
44
|
.all();
|
|
43
45
|
}
|
|
46
|
+
async getLastVisibleByOwnerAndContact(ownerUserId, contactUserId) {
|
|
47
|
+
return this.conn.db
|
|
48
|
+
.select()
|
|
49
|
+
.from(schema.messages)
|
|
50
|
+
.where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, contactUserId), ne(schema.messages.type, MessageType.KEEP_ALIVE), ne(schema.messages.type, MessageType.RETENTION_POLICY), ne(schema.messages.type, MessageType.REACTION), sql `reactionOf IS NULL`, or(ne(schema.messages.type, MessageType.DELETED), ne(schema.messages.direction, MessageDirection.OUTGOING), ne(schema.messages.content, '')), sql `(metadata IS NULL OR metadata NOT LIKE '%"control":"edit"%')`))
|
|
51
|
+
.orderBy(sql `${schema.messages.id} DESC`)
|
|
52
|
+
.limit(1)
|
|
53
|
+
.get();
|
|
54
|
+
}
|
|
44
55
|
async getReactionsByOwnerAndContact(ownerUserId, contactUserId) {
|
|
45
56
|
return this.conn.db
|
|
46
57
|
.select()
|
|
@@ -159,6 +170,27 @@ export class MessageQueries {
|
|
|
159
170
|
.where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, contactUserId), eq(schema.messages.direction, MessageDirection.INCOMING), eq(schema.messages.type, MessageType.ANNOUNCEMENT)))
|
|
160
171
|
.all();
|
|
161
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Hard-delete messages older than each discussion's retention duration.
|
|
175
|
+
* Only processes discussions that have a non-null messageRetentionDuration.
|
|
176
|
+
* Skips KEEP_ALIVE and ANNOUNCEMENT types.
|
|
177
|
+
*/
|
|
178
|
+
async deleteExpiredByOwner(ownerUserId, discussions) {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
for (const discussion of discussions) {
|
|
181
|
+
if (!discussion.messageRetentionDuration ||
|
|
182
|
+
discussion.messageRetentionDuration <= 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const expiryTs = now - discussion.messageRetentionDuration * 1000;
|
|
186
|
+
// Only delete messages that were sent AFTER the policy was activated.
|
|
187
|
+
// Messages that existed before the policy was set are left untouched.
|
|
188
|
+
const policySetAt = discussion.retentionPolicySetAt ?? 0;
|
|
189
|
+
await this.conn.db
|
|
190
|
+
.delete(schema.messages)
|
|
191
|
+
.where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, discussion.contactUserId), lt(schema.messages.timestamp, new Date(expiryTs)), gte(schema.messages.timestamp, new Date(policySetAt)), ne(schema.messages.type, MessageType.KEEP_ALIVE), ne(schema.messages.type, MessageType.ANNOUNCEMENT)));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
162
194
|
async findDuplicateIncoming(ownerUserId, contactUserId, content, windowStart, windowEnd) {
|
|
163
195
|
return this.conn.db
|
|
164
196
|
.select({ id: schema.messages.id })
|
|
@@ -328,6 +328,40 @@ export declare const discussions: import("drizzle-orm/sqlite-core").SQLiteTableW
|
|
|
328
328
|
identity: undefined;
|
|
329
329
|
generated: undefined;
|
|
330
330
|
}, {}, {}>;
|
|
331
|
+
messageRetentionDuration: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
332
|
+
name: "messageRetentionDuration";
|
|
333
|
+
tableName: "discussions";
|
|
334
|
+
dataType: "number";
|
|
335
|
+
columnType: "SQLiteInteger";
|
|
336
|
+
data: number;
|
|
337
|
+
driverParam: number;
|
|
338
|
+
notNull: false;
|
|
339
|
+
hasDefault: false;
|
|
340
|
+
isPrimaryKey: false;
|
|
341
|
+
isAutoincrement: false;
|
|
342
|
+
hasRuntimeDefault: false;
|
|
343
|
+
enumValues: undefined;
|
|
344
|
+
baseColumn: never;
|
|
345
|
+
identity: undefined;
|
|
346
|
+
generated: undefined;
|
|
347
|
+
}, {}, {}>;
|
|
348
|
+
retentionPolicySetAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
349
|
+
name: "retentionPolicySetAt";
|
|
350
|
+
tableName: "discussions";
|
|
351
|
+
dataType: "number";
|
|
352
|
+
columnType: "SQLiteInteger";
|
|
353
|
+
data: number;
|
|
354
|
+
driverParam: number;
|
|
355
|
+
notNull: false;
|
|
356
|
+
hasDefault: false;
|
|
357
|
+
isPrimaryKey: false;
|
|
358
|
+
isAutoincrement: false;
|
|
359
|
+
hasRuntimeDefault: false;
|
|
360
|
+
enumValues: undefined;
|
|
361
|
+
baseColumn: never;
|
|
362
|
+
identity: undefined;
|
|
363
|
+
generated: undefined;
|
|
364
|
+
}, {}, {}>;
|
|
331
365
|
saturatedRetryDone: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
332
366
|
name: "saturatedRetryDone";
|
|
333
367
|
tableName: "discussions";
|
|
@@ -25,6 +25,8 @@ export const discussions = sqliteTable('discussions', {
|
|
|
25
25
|
pinned: integer('pinned', { mode: 'boolean' }).notNull().default(false),
|
|
26
26
|
killedNextRetryAt: integer('killedNextRetryAt', { mode: 'timestamp_ms' }),
|
|
27
27
|
saturatedRetryAt: integer('saturatedRetryAt', { mode: 'timestamp_ms' }),
|
|
28
|
+
messageRetentionDuration: integer('messageRetentionDuration'),
|
|
29
|
+
retentionPolicySetAt: integer('retentionPolicySetAt'), // nullable, ms timestamp when policy was last configured
|
|
28
30
|
saturatedRetryDone: integer('saturatedRetryDone', { mode: 'boolean' })
|
|
29
31
|
.notNull()
|
|
30
32
|
.default(false),
|
package/dist/gossip.js
CHANGED
|
@@ -240,6 +240,7 @@ class GossipSdk {
|
|
|
240
240
|
this._announcement = new AnnouncementService(messageProtocol, session, this.eventEmitter, config, queries);
|
|
241
241
|
this._discussion = new DiscussionService(this._announcement, session, this.eventEmitter, queries);
|
|
242
242
|
this._message = new MessageService(messageProtocol, session, this.eventEmitter, config, queries);
|
|
243
|
+
this._discussion.setMessageService(this._message);
|
|
243
244
|
this._refresh = new RefreshService(this._message, this._discussion, this._announcement, session, this.eventEmitter, queries, this.config);
|
|
244
245
|
this._selfMessage = new SelfMessageService(queries, session.userIdEncoded, encryptionKey);
|
|
245
246
|
await this._selfMessage.ensureDiscussionExists();
|
|
@@ -5,7 +5,8 @@ export declare enum MessageType {
|
|
|
5
5
|
MESSAGE_TYPE_KEEP_ALIVE = 3,
|
|
6
6
|
MESSAGE_TYPE_DELETE = 4,
|
|
7
7
|
MESSAGE_TYPE_EDIT = 5,
|
|
8
|
-
MESSAGE_TYPE_REACTION = 6
|
|
8
|
+
MESSAGE_TYPE_REACTION = 6,
|
|
9
|
+
MESSAGE_TYPE_RETENTION_POLICY = 7
|
|
9
10
|
}
|
|
10
11
|
export interface Message {
|
|
11
12
|
messageType?: MessageType;
|
|
@@ -9,6 +9,7 @@ export var MessageType;
|
|
|
9
9
|
MessageType[MessageType["MESSAGE_TYPE_DELETE"] = 4] = "MESSAGE_TYPE_DELETE";
|
|
10
10
|
MessageType[MessageType["MESSAGE_TYPE_EDIT"] = 5] = "MESSAGE_TYPE_EDIT";
|
|
11
11
|
MessageType[MessageType["MESSAGE_TYPE_REACTION"] = 6] = "MESSAGE_TYPE_REACTION";
|
|
12
|
+
MessageType[MessageType["MESSAGE_TYPE_RETENTION_POLICY"] = 7] = "MESSAGE_TYPE_RETENTION_POLICY";
|
|
12
13
|
})(MessageType || (MessageType = {}));
|
|
13
14
|
const textEncoder = new TextEncoder();
|
|
14
15
|
const textDecoder = new TextDecoder();
|
|
@@ -10,6 +10,7 @@ import { SessionStatus } from '../wasm/bindings.js';
|
|
|
10
10
|
import { AnnouncementService } from './announcement.js';
|
|
11
11
|
import { SessionModule } from '../wasm/session.js';
|
|
12
12
|
import { RefreshService } from './refresh.js';
|
|
13
|
+
import type { MessageService } from './message.js';
|
|
13
14
|
import type { AuthService } from './auth.js';
|
|
14
15
|
import { Result } from '../utils/type.js';
|
|
15
16
|
import { SdkEventEmitter } from '../core/SdkEventEmitter.js';
|
|
@@ -40,10 +41,12 @@ export declare class DiscussionService {
|
|
|
40
41
|
private session;
|
|
41
42
|
private eventEmitter;
|
|
42
43
|
private refreshService?;
|
|
44
|
+
private messageService?;
|
|
43
45
|
private authService?;
|
|
44
46
|
private queries;
|
|
45
47
|
constructor(announcementService: AnnouncementService, session: SessionModule, eventEmitter: SdkEventEmitter, queries: Queries, refreshService?: RefreshService);
|
|
46
48
|
setRefreshService(refreshService: RefreshService): void;
|
|
49
|
+
setMessageService(messageService: MessageService): void;
|
|
47
50
|
setAuthService(authService: AuthService): void;
|
|
48
51
|
/**
|
|
49
52
|
* Initialize a discussion with a contact using SessionManager
|
|
@@ -103,4 +106,13 @@ export declare class DiscussionService {
|
|
|
103
106
|
success: boolean;
|
|
104
107
|
message?: string;
|
|
105
108
|
}>;
|
|
109
|
+
/**
|
|
110
|
+
* Set the auto-delete retention policy for a discussion.
|
|
111
|
+
* Updates the local DB and sends a control message to the peer so both sides
|
|
112
|
+
* apply the same policy (last-write-wins).
|
|
113
|
+
*
|
|
114
|
+
* @param contactUserId - The contact user ID of the discussion
|
|
115
|
+
* @param durationSeconds - Retention duration in seconds, or null to disable
|
|
116
|
+
*/
|
|
117
|
+
setRetentionPolicy(contactUserId: string, durationSeconds: number | null): Promise<void>;
|
|
106
118
|
}
|
|
@@ -55,6 +55,12 @@ export class DiscussionService {
|
|
|
55
55
|
writable: true,
|
|
56
56
|
value: void 0
|
|
57
57
|
});
|
|
58
|
+
Object.defineProperty(this, "messageService", {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
writable: true,
|
|
62
|
+
value: void 0
|
|
63
|
+
});
|
|
58
64
|
Object.defineProperty(this, "authService", {
|
|
59
65
|
enumerable: true,
|
|
60
66
|
configurable: true,
|
|
@@ -76,6 +82,9 @@ export class DiscussionService {
|
|
|
76
82
|
setRefreshService(refreshService) {
|
|
77
83
|
this.refreshService = refreshService;
|
|
78
84
|
}
|
|
85
|
+
setMessageService(messageService) {
|
|
86
|
+
this.messageService = messageService;
|
|
87
|
+
}
|
|
79
88
|
setAuthService(authService) {
|
|
80
89
|
this.authService = authService;
|
|
81
90
|
}
|
|
@@ -325,4 +334,42 @@ export class DiscussionService {
|
|
|
325
334
|
pin(discussionId, pinned) {
|
|
326
335
|
return updateDiscussionPin(discussionId, pinned, this.queries);
|
|
327
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Set the auto-delete retention policy for a discussion.
|
|
339
|
+
* Updates the local DB and sends a control message to the peer so both sides
|
|
340
|
+
* apply the same policy (last-write-wins).
|
|
341
|
+
*
|
|
342
|
+
* @param contactUserId - The contact user ID of the discussion
|
|
343
|
+
* @param durationSeconds - Retention duration in seconds, or null to disable
|
|
344
|
+
*/
|
|
345
|
+
async setRetentionPolicy(contactUserId, durationSeconds) {
|
|
346
|
+
const ownerUserId = this.session.userIdEncoded;
|
|
347
|
+
// Update local DB
|
|
348
|
+
await this.queries.discussions.updateByOwnerAndContact(ownerUserId, contactUserId, {
|
|
349
|
+
messageRetentionDuration: durationSeconds,
|
|
350
|
+
retentionPolicySetAt: durationSeconds ? Date.now() : null,
|
|
351
|
+
});
|
|
352
|
+
// Send control message to peer so they apply the same policy
|
|
353
|
+
if (this.messageService) {
|
|
354
|
+
const encodedDuration = durationSeconds && durationSeconds > 0 ? durationSeconds : 0;
|
|
355
|
+
await this.messageService.send({
|
|
356
|
+
ownerUserId,
|
|
357
|
+
contactUserId,
|
|
358
|
+
content: String(encodedDuration),
|
|
359
|
+
type: MessageType.RETENTION_POLICY,
|
|
360
|
+
direction: MessageDirection.OUTGOING,
|
|
361
|
+
status: MessageStatus.WAITING_SESSION,
|
|
362
|
+
timestamp: new Date(),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// Restore the correct lastMessageContent by re-deriving from the last
|
|
366
|
+
// visible message — the control message must not pollute the preview.
|
|
367
|
+
const lastVisible = await this.queries.messages.getLastVisibleByOwnerAndContact(ownerUserId, contactUserId);
|
|
368
|
+
await this.queries.discussions.updateByOwnerAndContact(ownerUserId, contactUserId, {
|
|
369
|
+
lastMessageContent: lastVisible?.content ?? null,
|
|
370
|
+
lastMessageTimestamp: lastVisible?.timestamp ?? null,
|
|
371
|
+
lastMessageId: lastVisible?.id ?? null,
|
|
372
|
+
});
|
|
373
|
+
await this.refreshService?.stateUpdate();
|
|
374
|
+
}
|
|
328
375
|
}
|
|
@@ -111,5 +111,11 @@ export declare class MessageService {
|
|
|
111
111
|
* control message so the peer can update their copy as well.
|
|
112
112
|
*/
|
|
113
113
|
editMessage(id: number, newContent: string): Promise<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* Hard-delete messages that have exceeded their discussion retention duration.
|
|
116
|
+
* Called periodically from the background refresh cycle.
|
|
117
|
+
* Emits MESSAGE_RECEIVED if any messages were deleted to trigger UI refresh.
|
|
118
|
+
*/
|
|
119
|
+
deleteExpiredMessages(ownerUserId: string): Promise<void>;
|
|
114
120
|
markAsRead(id: number): Promise<boolean>;
|
|
115
121
|
}
|
package/dist/services/message.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { MessageDirection, MessageStatus, MessageType, MESSAGE_ID_SIZE, } from '../db/index.js';
|
|
8
8
|
import { decodeUserId, encodeUserId } from '../utils/userId.js';
|
|
9
9
|
import { SessionStatus } from '../wasm/bindings.js';
|
|
10
|
-
import { serializeRegularMessage, serializeReplyMessage, serializeForwardMessage, serializeKeepAliveMessage, serializeDeleteMessage, serializeEditMessage, serializeReactionMessage, deserializeMessage, } from '../utils/messageSerialization.js';
|
|
10
|
+
import { serializeRegularMessage, serializeReplyMessage, serializeForwardMessage, serializeKeepAliveMessage, serializeDeleteMessage, serializeEditMessage, serializeReactionMessage, serializeRetentionPolicyMessage, deserializeMessage, } from '../utils/messageSerialization.js';
|
|
11
11
|
import { encodeToBase64, decodeFromBase64 } from '../utils/base64.js';
|
|
12
12
|
import { sessionStatusToString } from '../wasm/session.js';
|
|
13
13
|
import { Logger } from '../utils/logs.js';
|
|
@@ -316,7 +316,8 @@ export class MessageService {
|
|
|
316
316
|
const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId);
|
|
317
317
|
if (discussion &&
|
|
318
318
|
message.type !== MessageType.KEEP_ALIVE &&
|
|
319
|
-
message.type !== MessageType.REACTION
|
|
319
|
+
message.type !== MessageType.REACTION &&
|
|
320
|
+
message.type !== MessageType.RETENTION_POLICY) {
|
|
320
321
|
await this.queries.discussions.updateById(discussion.id, {
|
|
321
322
|
lastMessageId: messageId,
|
|
322
323
|
lastMessageContent: message.content,
|
|
@@ -426,6 +427,20 @@ export class MessageService {
|
|
|
426
427
|
// Do not insert a new message row for edit control messages
|
|
427
428
|
continue;
|
|
428
429
|
}
|
|
430
|
+
// Handle retention policy control messages by updating the discussion setting
|
|
431
|
+
if (message.type === MessageType.RETENTION_POLICY) {
|
|
432
|
+
const durationSeconds = parseInt(message.content, 10);
|
|
433
|
+
const duration = isNaN(durationSeconds) || durationSeconds <= 0
|
|
434
|
+
? null
|
|
435
|
+
: durationSeconds;
|
|
436
|
+
await this.queries.discussions.updateByOwnerAndContact(ownerUserId, message.senderId, {
|
|
437
|
+
messageRetentionDuration: duration,
|
|
438
|
+
retentionPolicySetAt: duration ? Date.now() : null,
|
|
439
|
+
});
|
|
440
|
+
this.eventEmitter.emit(SdkEventType.DISCUSSION_UPDATED, message.senderId);
|
|
441
|
+
// Do not insert a new message row for retention policy control messages
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
429
444
|
// Handle reaction messages by inserting a separate row
|
|
430
445
|
if (message.type === MessageType.REACTION &&
|
|
431
446
|
message.reactionOf?.originalMsgId) {
|
|
@@ -575,8 +590,9 @@ export class MessageService {
|
|
|
575
590
|
if (!discussion) {
|
|
576
591
|
return { success: false, error: 'Discussion not found' };
|
|
577
592
|
}
|
|
578
|
-
// Generate a random messageId for deduplication (not for keep-alive)
|
|
579
|
-
const randomMessageId = message.type !== MessageType.KEEP_ALIVE
|
|
593
|
+
// Generate a random messageId for deduplication (not for keep-alive or retention policy)
|
|
594
|
+
const randomMessageId = message.type !== MessageType.KEEP_ALIVE &&
|
|
595
|
+
message.type !== MessageType.RETENTION_POLICY
|
|
580
596
|
? crypto.getRandomValues(new Uint8Array(MESSAGE_ID_SIZE))
|
|
581
597
|
: undefined;
|
|
582
598
|
message.messageId = randomMessageId;
|
|
@@ -611,7 +627,9 @@ export class MessageService {
|
|
|
611
627
|
}
|
|
612
628
|
async serializeMessage(message) {
|
|
613
629
|
const log = logger.forMethod('serializeMessage');
|
|
614
|
-
if (!message.messageId &&
|
|
630
|
+
if (!message.messageId &&
|
|
631
|
+
message.type !== MessageType.KEEP_ALIVE &&
|
|
632
|
+
message.type !== MessageType.RETENTION_POLICY) {
|
|
615
633
|
return {
|
|
616
634
|
success: false,
|
|
617
635
|
error: 'Message ID is required',
|
|
@@ -636,6 +654,13 @@ export class MessageService {
|
|
|
636
654
|
data: serializeKeepAliveMessage(),
|
|
637
655
|
};
|
|
638
656
|
}
|
|
657
|
+
else if (message.type === MessageType.RETENTION_POLICY) {
|
|
658
|
+
const durationSeconds = parseInt(message.content, 10);
|
|
659
|
+
return {
|
|
660
|
+
success: true,
|
|
661
|
+
data: serializeRetentionPolicyMessage(isNaN(durationSeconds) || durationSeconds < 0 ? 0 : durationSeconds),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
639
664
|
else if (message.type === MessageType.DELETED && message.deleteOf) {
|
|
640
665
|
// Serialize a delete control message targeting an existing messageId
|
|
641
666
|
const originalMsgId = message.deleteOf.originalMsgId;
|
|
@@ -1102,6 +1127,18 @@ export class MessageService {
|
|
|
1102
1127
|
await this.refreshService?.stateUpdate();
|
|
1103
1128
|
return true;
|
|
1104
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Hard-delete messages that have exceeded their discussion retention duration.
|
|
1132
|
+
* Called periodically from the background refresh cycle.
|
|
1133
|
+
* Emits MESSAGE_RECEIVED if any messages were deleted to trigger UI refresh.
|
|
1134
|
+
*/
|
|
1135
|
+
async deleteExpiredMessages(ownerUserId) {
|
|
1136
|
+
const allRows = await this.queries.discussions.getByOwner(ownerUserId);
|
|
1137
|
+
const withRetention = allRows.filter(d => d.messageRetentionDuration != null && d.messageRetentionDuration > 0);
|
|
1138
|
+
if (withRetention.length === 0)
|
|
1139
|
+
return;
|
|
1140
|
+
await this.queries.messages.deleteExpiredByOwner(ownerUserId, withRetention);
|
|
1141
|
+
}
|
|
1105
1142
|
// Mark a message as read. Returns true if the message has been marked as read, false if it was already marked as read or doesn't exist.
|
|
1106
1143
|
async markAsRead(id) {
|
|
1107
1144
|
// Check current message status from DB to avoid race conditions
|
package/dist/services/refresh.js
CHANGED
|
@@ -231,6 +231,8 @@ export class RefreshService {
|
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
// Step 4: hard-delete messages that have exceeded retention duration
|
|
235
|
+
await this.messageService.deleteExpiredMessages(ownerUserId);
|
|
234
236
|
}
|
|
235
237
|
catch (error) {
|
|
236
238
|
log.error('error in update_state', { error });
|
|
@@ -20,6 +20,11 @@ export declare class SelfMessageService {
|
|
|
20
20
|
originalMessageId: number;
|
|
21
21
|
}>;
|
|
22
22
|
removeReaction(reactionId: number): Promise<void>;
|
|
23
|
+
getRetentionInfo(): Promise<{
|
|
24
|
+
duration: number | null;
|
|
25
|
+
setAt: number | null;
|
|
26
|
+
}>;
|
|
27
|
+
setRetentionPolicy(durationSeconds: number | null): Promise<void>;
|
|
23
28
|
getReactions(): Promise<{
|
|
24
29
|
id: number;
|
|
25
30
|
emoji: string;
|
|
@@ -173,6 +173,20 @@ export class SelfMessageService {
|
|
|
173
173
|
async removeReaction(reactionId) {
|
|
174
174
|
await this.queries.messages.deleteById(reactionId);
|
|
175
175
|
}
|
|
176
|
+
async getRetentionInfo() {
|
|
177
|
+
const row = await this.queries.discussions.getByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
|
|
178
|
+
return {
|
|
179
|
+
duration: row?.messageRetentionDuration ?? null,
|
|
180
|
+
setAt: row?.retentionPolicySetAt ?? null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async setRetentionPolicy(durationSeconds) {
|
|
184
|
+
const duration = durationSeconds != null && durationSeconds > 0 ? durationSeconds : null;
|
|
185
|
+
await this.queries.discussions.updateByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID, {
|
|
186
|
+
messageRetentionDuration: duration,
|
|
187
|
+
retentionPolicySetAt: duration ? Date.now() : null,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
176
190
|
async getReactions() {
|
|
177
191
|
const rows = await this.queries.messages.getReactionsByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
|
|
178
192
|
const result = [];
|
|
@@ -9,6 +9,7 @@ import { MessageType as ProtoMessageType } from '../proto/generated/message.js';
|
|
|
9
9
|
export declare const MESSAGE_TYPE_KEEP_ALIVE = ProtoMessageType.MESSAGE_TYPE_KEEP_ALIVE;
|
|
10
10
|
export declare const MESSAGE_TYPE_DELETE = ProtoMessageType.MESSAGE_TYPE_DELETE;
|
|
11
11
|
export declare const MESSAGE_TYPE_EDIT = ProtoMessageType.MESSAGE_TYPE_EDIT;
|
|
12
|
+
export declare const MESSAGE_TYPE_RETENTION_POLICY = ProtoMessageType.MESSAGE_TYPE_RETENTION_POLICY;
|
|
12
13
|
export interface DeserializedMessage {
|
|
13
14
|
content: string;
|
|
14
15
|
messageId?: Uint8Array;
|
|
@@ -91,6 +92,13 @@ export declare function serializeDeleteMessage(originalMsgId: Uint8Array, messag
|
|
|
91
92
|
*/
|
|
92
93
|
export declare function serializeEditMessage(newContent: string, originalMsgId: Uint8Array, messageId: Uint8Array): Uint8Array;
|
|
93
94
|
export declare function serializeReactionMessage(emoji: string, originalMsgId: Uint8Array, messageId: Uint8Array): Uint8Array;
|
|
95
|
+
/**
|
|
96
|
+
* Serialize a retention policy control message
|
|
97
|
+
*
|
|
98
|
+
* @param durationSeconds - Retention duration in seconds (0 to disable)
|
|
99
|
+
* @returns Serialized retention policy message bytes
|
|
100
|
+
*/
|
|
101
|
+
export declare function serializeRetentionPolicyMessage(durationSeconds: number): Uint8Array;
|
|
94
102
|
/**
|
|
95
103
|
* Deserialize a message from bytes
|
|
96
104
|
*
|
|
@@ -9,6 +9,7 @@ import { Message as ProtoMessage, MessageType as ProtoMessageType, } from '../pr
|
|
|
9
9
|
export const MESSAGE_TYPE_KEEP_ALIVE = ProtoMessageType.MESSAGE_TYPE_KEEP_ALIVE;
|
|
10
10
|
export const MESSAGE_TYPE_DELETE = ProtoMessageType.MESSAGE_TYPE_DELETE;
|
|
11
11
|
export const MESSAGE_TYPE_EDIT = ProtoMessageType.MESSAGE_TYPE_EDIT;
|
|
12
|
+
export const MESSAGE_TYPE_RETENTION_POLICY = ProtoMessageType.MESSAGE_TYPE_RETENTION_POLICY;
|
|
12
13
|
/**
|
|
13
14
|
* Serialize a keep-alive message
|
|
14
15
|
* Keep-alive messages are used to maintain session activity
|
|
@@ -150,6 +151,18 @@ export function serializeReactionMessage(emoji, originalMsgId, messageId) {
|
|
|
150
151
|
citedMsgId: originalMsgId,
|
|
151
152
|
});
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Serialize a retention policy control message
|
|
156
|
+
*
|
|
157
|
+
* @param durationSeconds - Retention duration in seconds (0 to disable)
|
|
158
|
+
* @returns Serialized retention policy message bytes
|
|
159
|
+
*/
|
|
160
|
+
export function serializeRetentionPolicyMessage(durationSeconds) {
|
|
161
|
+
return ProtoMessage.encode({
|
|
162
|
+
messageType: ProtoMessageType.MESSAGE_TYPE_RETENTION_POLICY,
|
|
163
|
+
content: String(durationSeconds),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
153
166
|
/**
|
|
154
167
|
* Deserialize a message from bytes
|
|
155
168
|
*
|
|
@@ -169,6 +182,12 @@ export function deserializeMessage(buffer) {
|
|
|
169
182
|
type: MessageType.KEEP_ALIVE,
|
|
170
183
|
};
|
|
171
184
|
}
|
|
185
|
+
if (protoType === ProtoMessageType.MESSAGE_TYPE_RETENTION_POLICY) {
|
|
186
|
+
return {
|
|
187
|
+
content: decoded.content ?? '0',
|
|
188
|
+
type: MessageType.RETENTION_POLICY,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
172
191
|
const content = decoded.content ?? '';
|
|
173
192
|
const messageId = decoded.messageId;
|
|
174
193
|
const citedMsgId = decoded.citedMsgId;
|
package/package.json
CHANGED