@massalabs/gossip-sdk 0.0.2-dev.20260410072437 → 0.0.2-dev.20260410093719
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 +63 -36
- package/dist/core/SdkEventEmitter.js +22 -50
- package/dist/core/SdkPolling.js +16 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/db/sqlite-worker.js +5 -8
- package/dist/db/sqlite.js +14 -11
- package/dist/gossip.d.ts +4 -6
- package/dist/gossip.js +12 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/announcement.js +8 -2
- package/dist/services/message.js +120 -79
- package/dist/services/refresh.js +8 -2
- package/package.json +2 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SDK Event Emitter
|
|
3
|
-
*
|
|
4
|
-
* Type-safe event emitter for SDK events.
|
|
2
|
+
* SDK Event Emitter — type-safe event bus backed by mitt.
|
|
5
3
|
*/
|
|
6
4
|
import type { Message, Discussion, Contact } from '../db';
|
|
7
5
|
import type { SessionStatus } from '../wasm/bindings';
|
|
@@ -19,42 +17,71 @@ export declare enum SdkEventType {
|
|
|
19
17
|
DISCUSSION_UPDATED = "discussionUpdated",
|
|
20
18
|
WRITE_FAILED = "writeFailed",
|
|
21
19
|
MESSAGE_OPTIMISTIC = "messageOptimistic",
|
|
20
|
+
MESSAGE_DELETED_OPTIMISTIC = "messageDeletedOptimistic",
|
|
21
|
+
MESSAGE_EDITED_OPTIMISTIC = "messageEditedOptimistic",
|
|
22
|
+
MESSAGE_DELETE_FAILED = "messageDeleteFailed",
|
|
23
|
+
MESSAGE_EDIT_FAILED = "messageEditFailed",
|
|
22
24
|
ERROR = "error"
|
|
23
25
|
}
|
|
24
|
-
export
|
|
25
|
-
[SdkEventType.MESSAGE_RECEIVED]:
|
|
26
|
+
export type SdkEvents = {
|
|
27
|
+
[SdkEventType.MESSAGE_RECEIVED]: Omit<Message, 'id'> & {
|
|
26
28
|
id?: number;
|
|
27
|
-
}
|
|
28
|
-
[SdkEventType.MESSAGE_SENT]:
|
|
29
|
-
[SdkEventType.MESSAGE_READ]:
|
|
30
|
-
[SdkEventType.MESSAGE_FAILED]:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[SdkEventType.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[SdkEventType.
|
|
39
|
-
[SdkEventType.
|
|
40
|
-
[SdkEventType.
|
|
41
|
-
|
|
29
|
+
};
|
|
30
|
+
[SdkEventType.MESSAGE_SENT]: Message;
|
|
31
|
+
[SdkEventType.MESSAGE_READ]: number;
|
|
32
|
+
[SdkEventType.MESSAGE_FAILED]: {
|
|
33
|
+
message: Message;
|
|
34
|
+
error: Error;
|
|
35
|
+
};
|
|
36
|
+
[SdkEventType.SESSION_REQUESTED]: {
|
|
37
|
+
discussion: Discussion;
|
|
38
|
+
contact: Contact;
|
|
39
|
+
};
|
|
40
|
+
[SdkEventType.SESSION_CREATED]: Discussion;
|
|
41
|
+
[SdkEventType.SESSION_RENEWED]: Discussion;
|
|
42
|
+
[SdkEventType.SESSION_ACCEPTED]: string;
|
|
43
|
+
[SdkEventType.SEEKERS_UPDATED]: Uint8Array[];
|
|
44
|
+
[SdkEventType.SESSION_STATUS_CHANGED]: {
|
|
45
|
+
contactUserId: string;
|
|
46
|
+
status: SessionStatus;
|
|
47
|
+
};
|
|
48
|
+
[SdkEventType.DISCUSSION_UPDATED]: string;
|
|
49
|
+
[SdkEventType.WRITE_FAILED]: {
|
|
50
|
+
messageId: Uint8Array | undefined;
|
|
51
|
+
entityType: 'message' | 'discussion' | 'contact';
|
|
52
|
+
error: Error;
|
|
53
|
+
};
|
|
54
|
+
[SdkEventType.MESSAGE_OPTIMISTIC]: Message;
|
|
55
|
+
[SdkEventType.MESSAGE_DELETED_OPTIMISTIC]: {
|
|
56
|
+
contactUserId: string;
|
|
57
|
+
messageDbId: number;
|
|
58
|
+
originalMsgId: Uint8Array;
|
|
59
|
+
};
|
|
60
|
+
[SdkEventType.MESSAGE_EDITED_OPTIMISTIC]: {
|
|
61
|
+
contactUserId: string;
|
|
62
|
+
messageDbId: number;
|
|
63
|
+
newContent: string;
|
|
64
|
+
metadata: Record<string, unknown>;
|
|
65
|
+
};
|
|
66
|
+
[SdkEventType.MESSAGE_DELETE_FAILED]: {
|
|
67
|
+
contactUserId: string;
|
|
68
|
+
messageDbId: number;
|
|
69
|
+
original: Message;
|
|
70
|
+
};
|
|
71
|
+
[SdkEventType.MESSAGE_EDIT_FAILED]: {
|
|
72
|
+
contactUserId: string;
|
|
73
|
+
messageDbId: number;
|
|
74
|
+
original: Message;
|
|
75
|
+
};
|
|
76
|
+
[SdkEventType.ERROR]: {
|
|
77
|
+
error: Error;
|
|
78
|
+
context: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
42
81
|
export declare class SdkEventEmitter {
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
on<K extends keyof SdkEventHandlers>(event: K, handler: SdkEventHandlers[K]): void;
|
|
48
|
-
/**
|
|
49
|
-
* Remove an event handler
|
|
50
|
-
*/
|
|
51
|
-
off<K extends keyof SdkEventHandlers>(event: K, handler: SdkEventHandlers[K]): void;
|
|
52
|
-
/**
|
|
53
|
-
* Emit an event to all registered handlers
|
|
54
|
-
*/
|
|
55
|
-
emit<K extends keyof SdkEventHandlers>(event: K, ...args: Parameters<SdkEventHandlers[K]>): void;
|
|
56
|
-
/**
|
|
57
|
-
* Remove all handlers for all events
|
|
58
|
-
*/
|
|
82
|
+
private bus;
|
|
83
|
+
on<K extends keyof SdkEvents>(event: K, handler: (payload: SdkEvents[K]) => void): void;
|
|
84
|
+
off<K extends keyof SdkEvents>(event: K, handler: (payload: SdkEvents[K]) => void): void;
|
|
85
|
+
emit<K extends keyof SdkEvents>(event: K, payload: SdkEvents[K]): void;
|
|
59
86
|
clear(): void;
|
|
60
87
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SDK Event Emitter
|
|
3
|
-
*
|
|
4
|
-
* Type-safe event emitter for SDK events.
|
|
2
|
+
* SDK Event Emitter — type-safe event bus backed by mitt.
|
|
5
3
|
*/
|
|
6
|
-
|
|
7
|
-
// Event Types
|
|
8
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
import mitt from 'mitt';
|
|
9
5
|
export var SdkEventType;
|
|
10
6
|
(function (SdkEventType) {
|
|
11
7
|
SdkEventType["MESSAGE_RECEIVED"] = "messageReceived";
|
|
@@ -21,65 +17,41 @@ export var SdkEventType;
|
|
|
21
17
|
SdkEventType["DISCUSSION_UPDATED"] = "discussionUpdated";
|
|
22
18
|
SdkEventType["WRITE_FAILED"] = "writeFailed";
|
|
23
19
|
SdkEventType["MESSAGE_OPTIMISTIC"] = "messageOptimistic";
|
|
20
|
+
SdkEventType["MESSAGE_DELETED_OPTIMISTIC"] = "messageDeletedOptimistic";
|
|
21
|
+
SdkEventType["MESSAGE_EDITED_OPTIMISTIC"] = "messageEditedOptimistic";
|
|
22
|
+
SdkEventType["MESSAGE_DELETE_FAILED"] = "messageDeleteFailed";
|
|
23
|
+
SdkEventType["MESSAGE_EDIT_FAILED"] = "messageEditFailed";
|
|
24
24
|
SdkEventType["ERROR"] = "error";
|
|
25
25
|
})(SdkEventType || (SdkEventType = {}));
|
|
26
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
-
// Event Emitter Class
|
|
28
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
26
|
export class SdkEventEmitter {
|
|
30
27
|
constructor() {
|
|
31
|
-
Object.defineProperty(this, "
|
|
28
|
+
Object.defineProperty(this, "bus", {
|
|
32
29
|
enumerable: true,
|
|
33
30
|
configurable: true,
|
|
34
31
|
writable: true,
|
|
35
|
-
value:
|
|
36
|
-
[SdkEventType.MESSAGE_RECEIVED]: new Set(),
|
|
37
|
-
[SdkEventType.MESSAGE_SENT]: new Set(),
|
|
38
|
-
[SdkEventType.MESSAGE_READ]: new Set(),
|
|
39
|
-
[SdkEventType.MESSAGE_FAILED]: new Set(),
|
|
40
|
-
[SdkEventType.SESSION_REQUESTED]: new Set(),
|
|
41
|
-
[SdkEventType.SESSION_CREATED]: new Set(),
|
|
42
|
-
[SdkEventType.SESSION_RENEWED]: new Set(),
|
|
43
|
-
[SdkEventType.SESSION_ACCEPTED]: new Set(),
|
|
44
|
-
[SdkEventType.SEEKERS_UPDATED]: new Set(),
|
|
45
|
-
[SdkEventType.SESSION_STATUS_CHANGED]: new Set(),
|
|
46
|
-
[SdkEventType.DISCUSSION_UPDATED]: new Set(),
|
|
47
|
-
[SdkEventType.WRITE_FAILED]: new Set(),
|
|
48
|
-
[SdkEventType.MESSAGE_OPTIMISTIC]: new Set(),
|
|
49
|
-
[SdkEventType.ERROR]: new Set(),
|
|
50
|
-
}
|
|
32
|
+
value: mitt()
|
|
51
33
|
});
|
|
52
34
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Register an event handler
|
|
55
|
-
*/
|
|
56
35
|
on(event, handler) {
|
|
57
|
-
this.
|
|
36
|
+
this.bus.on(event, handler);
|
|
58
37
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Remove an event handler
|
|
61
|
-
*/
|
|
62
38
|
off(event, handler) {
|
|
63
|
-
this.
|
|
39
|
+
this.bus.off(event, handler);
|
|
64
40
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.error(`[SdkEventEmitter] Error in ${event} handler:`, error);
|
|
41
|
+
emit(event, payload) {
|
|
42
|
+
const handlers = this.bus.all.get(event);
|
|
43
|
+
if (handlers) {
|
|
44
|
+
for (const handler of handlers) {
|
|
45
|
+
try {
|
|
46
|
+
handler(payload);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`[SdkEventEmitter] Error in ${String(event)} handler:`, error);
|
|
50
|
+
}
|
|
76
51
|
}
|
|
77
|
-
}
|
|
52
|
+
}
|
|
78
53
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Remove all handlers for all events
|
|
81
|
-
*/
|
|
82
54
|
clear() {
|
|
83
|
-
|
|
55
|
+
this.bus.all.clear();
|
|
84
56
|
}
|
|
85
57
|
}
|
package/dist/core/SdkPolling.js
CHANGED
|
@@ -54,7 +54,10 @@ export class SdkPolling {
|
|
|
54
54
|
}
|
|
55
55
|
catch (error) {
|
|
56
56
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
57
|
-
this.eventEmitter?.emit(SdkEventType.ERROR,
|
|
57
|
+
this.eventEmitter?.emit(SdkEventType.ERROR, {
|
|
58
|
+
error: err,
|
|
59
|
+
context: 'message_polling',
|
|
60
|
+
});
|
|
58
61
|
}
|
|
59
62
|
}, config.polling.messagesIntervalMs);
|
|
60
63
|
// Start announcement polling
|
|
@@ -64,7 +67,10 @@ export class SdkPolling {
|
|
|
64
67
|
}
|
|
65
68
|
catch (error) {
|
|
66
69
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
67
|
-
this.eventEmitter?.emit(SdkEventType.ERROR,
|
|
70
|
+
this.eventEmitter?.emit(SdkEventType.ERROR, {
|
|
71
|
+
error: err,
|
|
72
|
+
context: 'announcement_polling',
|
|
73
|
+
});
|
|
68
74
|
}
|
|
69
75
|
}, config.polling.announcementsIntervalMs);
|
|
70
76
|
// Start session refresh polling
|
|
@@ -74,7 +80,10 @@ export class SdkPolling {
|
|
|
74
80
|
}
|
|
75
81
|
catch (error) {
|
|
76
82
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
77
|
-
this.eventEmitter?.emit(SdkEventType.ERROR,
|
|
83
|
+
this.eventEmitter?.emit(SdkEventType.ERROR, {
|
|
84
|
+
error: err,
|
|
85
|
+
context: 'session_update',
|
|
86
|
+
});
|
|
78
87
|
}
|
|
79
88
|
}, config.polling.sessionRefreshIntervalMs);
|
|
80
89
|
// Start session status change polling (fixed 3s interval)
|
|
@@ -84,7 +93,10 @@ export class SdkPolling {
|
|
|
84
93
|
}
|
|
85
94
|
catch (error) {
|
|
86
95
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
87
|
-
this.eventEmitter?.emit(SdkEventType.ERROR,
|
|
96
|
+
this.eventEmitter?.emit(SdkEventType.ERROR, {
|
|
97
|
+
error: err,
|
|
98
|
+
context: 'session_status_polling',
|
|
99
|
+
});
|
|
88
100
|
}
|
|
89
101
|
}, SESSION_STATUS_POLL_INTERVAL_MS);
|
|
90
102
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Core SDK components
|
|
3
3
|
*/
|
|
4
4
|
export { SdkEventEmitter } from './SdkEventEmitter.js';
|
|
5
|
-
export { SdkEventType, type
|
|
5
|
+
export { SdkEventType, type SdkEvents } from './SdkEventEmitter.js';
|
|
6
6
|
export { SdkPolling } from './SdkPolling.js';
|
|
7
7
|
export type { PollingCallbacks } from './SdkPolling.js';
|
package/dist/db/sqlite-worker.js
CHANGED
|
@@ -51,16 +51,13 @@ async function handleMessage(e) {
|
|
|
51
51
|
try {
|
|
52
52
|
switch (type) {
|
|
53
53
|
case 'init': {
|
|
54
|
-
const { dbPath,
|
|
54
|
+
const { dbPath, wasmBinary, initSql, useOPFS } = e.data;
|
|
55
55
|
const moduleArg = {};
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
if (wasmUrl) {
|
|
60
|
-
const resp = await fetch(wasmUrl);
|
|
61
|
-
const bytes = await resp.arrayBuffer();
|
|
56
|
+
// WASM bytes are pre-fetched in the main thread to avoid
|
|
57
|
+
// Safari's chunked Transfer-Encoding bug in Worker fetch().
|
|
58
|
+
if (wasmBinary) {
|
|
62
59
|
moduleArg.instantiateWasm = (imports, successCallback) => {
|
|
63
|
-
WebAssembly.instantiate(
|
|
60
|
+
WebAssembly.instantiate(wasmBinary, imports).then(result => {
|
|
64
61
|
successCallback(result.instance, result.module);
|
|
65
62
|
});
|
|
66
63
|
return {};
|
package/dist/db/sqlite.js
CHANGED
|
@@ -92,12 +92,14 @@ export class DatabaseConnection {
|
|
|
92
92
|
return this.state.drizzleDb !== null;
|
|
93
93
|
}
|
|
94
94
|
// ─── Raw SQL execution ─────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
postToWorker(msg) {
|
|
95
|
+
postToWorker(msg, transfer = []) {
|
|
97
96
|
return new Promise((resolve, reject) => {
|
|
98
97
|
const id = ++this.state.msgId;
|
|
99
|
-
this.state.pending.set(id, {
|
|
100
|
-
|
|
98
|
+
this.state.pending.set(id, {
|
|
99
|
+
resolve: resolve,
|
|
100
|
+
reject,
|
|
101
|
+
});
|
|
102
|
+
this.state.worker.postMessage({ ...msg, id }, transfer);
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
105
|
createDrizzleInstance() {
|
|
@@ -152,13 +154,14 @@ export class DatabaseConnection {
|
|
|
152
154
|
this.state.worker.onmessage = this.handleWorkerMessage;
|
|
153
155
|
this.state.useWorker = true;
|
|
154
156
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
157
|
+
// Pre-fetch WASM in the main thread to avoid Safari's
|
|
158
|
+
// chunked Transfer-Encoding bug in Worker fetch().
|
|
159
|
+
let wasmBinary;
|
|
160
|
+
if (storage.wasmUrl) {
|
|
161
|
+
const resp = await fetch(storage.wasmUrl);
|
|
162
|
+
wasmBinary = await resp.arrayBuffer();
|
|
163
|
+
}
|
|
164
|
+
await this.postToWorker({ type: 'init', dbPath, useOPFS, wasmBinary, initSql: PRAGMAS }, wasmBinary ? [wasmBinary] : []);
|
|
162
165
|
}
|
|
163
166
|
catch (err) {
|
|
164
167
|
if (this.state.worker) {
|
package/dist/gossip.d.ts
CHANGED
|
@@ -11,9 +11,7 @@ import { type ValidationResult } from './utils/validation.js';
|
|
|
11
11
|
import { type StorageConfig } from './db/index.js';
|
|
12
12
|
import { Queries } from './db/queries/index.js';
|
|
13
13
|
import { type UserPublicKeys, type SessionConfig } from './wasm/bindings.js';
|
|
14
|
-
import { SdkEventType, type
|
|
15
|
-
export type { SdkEventHandlers };
|
|
16
|
-
export { SdkEventType };
|
|
14
|
+
import { SdkEventType, type SdkEvents } from './core/SdkEventEmitter.js';
|
|
17
15
|
export declare enum SdkStatus {
|
|
18
16
|
UNINITIALIZED = "uninitialized",
|
|
19
17
|
INITIALIZED = "initialized",
|
|
@@ -132,11 +130,11 @@ declare class GossipSdk {
|
|
|
132
130
|
/**
|
|
133
131
|
* Register an event handler
|
|
134
132
|
*/
|
|
135
|
-
on<K extends SdkEventType>(event: K, handler:
|
|
133
|
+
on<K extends SdkEventType>(event: K, handler: (payload: SdkEvents[K]) => void): void;
|
|
136
134
|
/**
|
|
137
135
|
* Remove an event handler
|
|
138
136
|
*/
|
|
139
|
-
off<K extends SdkEventType>(event: K, handler:
|
|
137
|
+
off<K extends SdkEventType>(event: K, handler: (payload: SdkEvents[K]) => void): void;
|
|
140
138
|
private requireSession;
|
|
141
139
|
private handleSessionPersist;
|
|
142
140
|
/**
|
|
@@ -174,4 +172,4 @@ interface PollingAPI {
|
|
|
174
172
|
}
|
|
175
173
|
/** A convenience singleton for apps that only need one SDK instance. */
|
|
176
174
|
export declare const gossipSdk: GossipSdk;
|
|
177
|
-
export { GossipSdk };
|
|
175
|
+
export { GossipSdk, SdkEventType };
|
package/dist/gossip.js
CHANGED
|
@@ -57,7 +57,9 @@ import { Queries } from './db/queries/index.js';
|
|
|
57
57
|
import { SessionManagerWrapper, } from './wasm/bindings.js';
|
|
58
58
|
import { SdkEventEmitter, SdkEventType, } from './core/SdkEventEmitter.js';
|
|
59
59
|
import { SdkPolling } from './core/SdkPolling.js';
|
|
60
|
-
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// Types
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
63
|
export var SdkStatus;
|
|
62
64
|
(function (SdkStatus) {
|
|
63
65
|
SdkStatus["UNINITIALIZED"] = "uninitialized";
|
|
@@ -247,7 +249,10 @@ class GossipSdk {
|
|
|
247
249
|
// Publish gossip ID (public key) on messageProtocol so the user is discoverable.
|
|
248
250
|
// Non-blocking: login must succeed even when the API is unreachable.
|
|
249
251
|
this._auth.publishPublicKey(session.ourPk, session.userIdEncoded, queries).catch(err => {
|
|
250
|
-
this.eventEmitter.emit(SdkEventType.ERROR,
|
|
252
|
+
this.eventEmitter.emit(SdkEventType.ERROR, {
|
|
253
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
254
|
+
context: 'publishPublicKey',
|
|
255
|
+
});
|
|
251
256
|
});
|
|
252
257
|
// Now set refreshService on services (circular dependency resolved via setter)
|
|
253
258
|
this._discussion.setRefreshService(this._refresh);
|
|
@@ -540,7 +545,10 @@ class GossipSdk {
|
|
|
540
545
|
await onPersist(blob, encryptionKey);
|
|
541
546
|
}
|
|
542
547
|
catch (error) {
|
|
543
|
-
this.eventEmitter.emit(SdkEventType.ERROR,
|
|
548
|
+
this.eventEmitter.emit(SdkEventType.ERROR, {
|
|
549
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
550
|
+
context: 'session_persist',
|
|
551
|
+
});
|
|
544
552
|
}
|
|
545
553
|
}
|
|
546
554
|
/**
|
|
@@ -581,4 +589,4 @@ class GossipSdk {
|
|
|
581
589
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
582
590
|
/** A convenience singleton for apps that only need one SDK instance. */
|
|
583
591
|
export const gossipSdk = new GossipSdk();
|
|
584
|
-
export { GossipSdk };
|
|
592
|
+
export { GossipSdk, SdkEventType };
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -426,7 +426,10 @@ export class AnnouncementService {
|
|
|
426
426
|
contactRow &&
|
|
427
427
|
this.session.peerSessionStatus(decodeUserId(contactUserId)) ===
|
|
428
428
|
SessionStatus.PeerRequested) {
|
|
429
|
-
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED,
|
|
429
|
+
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, {
|
|
430
|
+
discussion: toDiscussion(newDiscussion),
|
|
431
|
+
contact: contactRow,
|
|
432
|
+
});
|
|
430
433
|
}
|
|
431
434
|
return { discussionId: existing.id };
|
|
432
435
|
}
|
|
@@ -443,7 +446,10 @@ export class AnnouncementService {
|
|
|
443
446
|
const discussion = await this.queries.discussions.getById(discussionId);
|
|
444
447
|
const contactRow = await this.queries.contacts.getByOwnerAndUser(ownerUserId, contactUserId);
|
|
445
448
|
if (discussion && contactRow) {
|
|
446
|
-
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED,
|
|
449
|
+
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, {
|
|
450
|
+
discussion: toDiscussion(discussion),
|
|
451
|
+
contact: contactRow,
|
|
452
|
+
});
|
|
447
453
|
}
|
|
448
454
|
return { discussionId };
|
|
449
455
|
}
|
package/dist/services/message.js
CHANGED
|
@@ -961,17 +961,23 @@ export class MessageService {
|
|
|
961
961
|
type: msg.type,
|
|
962
962
|
direction: msg.direction,
|
|
963
963
|
});
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
964
|
+
// Skip emitting MESSAGE_SENT for control messages (delete/edit).
|
|
965
|
+
// These are internal transport details; the semantic optimistic
|
|
966
|
+
// events already handle UI state.
|
|
967
|
+
const isControlMessage = !!(msg.deleteOf || msg.editOf);
|
|
968
|
+
if (!isControlMessage) {
|
|
969
|
+
try {
|
|
970
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_SENT, {
|
|
971
|
+
...msg,
|
|
972
|
+
status: MessageStatus.SENT,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
log.error('failed to emit message sent event', {
|
|
977
|
+
messageId: msg.id,
|
|
978
|
+
error,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
975
981
|
}
|
|
976
982
|
}
|
|
977
983
|
}
|
|
@@ -1066,11 +1072,19 @@ export class MessageService {
|
|
|
1066
1072
|
// Persist in background (non-optimistic path)
|
|
1067
1073
|
this.send({ ...message, messageId }).then(result => {
|
|
1068
1074
|
if (!result.success) {
|
|
1069
|
-
this.eventEmitter.emit(SdkEventType.WRITE_FAILED,
|
|
1075
|
+
this.eventEmitter.emit(SdkEventType.WRITE_FAILED, {
|
|
1076
|
+
messageId,
|
|
1077
|
+
entityType: 'message',
|
|
1078
|
+
error: new Error(result.error ?? 'Unknown error'),
|
|
1079
|
+
});
|
|
1070
1080
|
}
|
|
1071
1081
|
}, error => {
|
|
1072
1082
|
log.error('optimistic send failed', { error });
|
|
1073
|
-
this.eventEmitter.emit(SdkEventType.WRITE_FAILED,
|
|
1083
|
+
this.eventEmitter.emit(SdkEventType.WRITE_FAILED, {
|
|
1084
|
+
messageId,
|
|
1085
|
+
entityType: 'message',
|
|
1086
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1087
|
+
});
|
|
1074
1088
|
});
|
|
1075
1089
|
return { success: true, message: optimisticMessage };
|
|
1076
1090
|
}
|
|
@@ -1105,41 +1119,59 @@ export class MessageService {
|
|
|
1105
1119
|
*/
|
|
1106
1120
|
async deleteMessage(id) {
|
|
1107
1121
|
const row = await this.queries.messages.getById(id);
|
|
1108
|
-
if (!row)
|
|
1122
|
+
if (!row)
|
|
1109
1123
|
return false;
|
|
1110
|
-
|
|
1111
|
-
// Only allow deleting our own outgoing messages
|
|
1112
|
-
if (row.direction !== MessageDirection.OUTGOING) {
|
|
1124
|
+
if (row.direction !== MessageDirection.OUTGOING)
|
|
1113
1125
|
return false;
|
|
1114
|
-
|
|
1115
|
-
if (!row.messageId) {
|
|
1126
|
+
if (!row.messageId)
|
|
1116
1127
|
throw new Error('Cannot delete a message that has no messageId');
|
|
1117
|
-
|
|
1128
|
+
const original = rowToMessage(row);
|
|
1118
1129
|
const ownerUserId = this.session.userIdEncoded;
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const controlMessage = {
|
|
1126
|
-
ownerUserId,
|
|
1127
|
-
contactUserId: row.contactUserId,
|
|
1128
|
-
content: '',
|
|
1129
|
-
type: MessageType.DELETED,
|
|
1130
|
-
direction: MessageDirection.OUTGOING,
|
|
1131
|
-
status: MessageStatus.WAITING_SESSION,
|
|
1132
|
-
timestamp: new Date(),
|
|
1133
|
-
deleteOf: {
|
|
1130
|
+
// Emit optimistic event so UI updates immediately (skip for reactions —
|
|
1131
|
+
// the store handles reaction removal separately).
|
|
1132
|
+
if (row.type !== MessageType.REACTION) {
|
|
1133
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_DELETED_OPTIMISTIC, {
|
|
1134
|
+
contactUserId: row.contactUserId,
|
|
1135
|
+
messageDbId: id,
|
|
1134
1136
|
originalMsgId: row.messageId,
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
await this.queries.messages.updateById(id, {
|
|
1141
|
+
content: '[Message deleted]',
|
|
1142
|
+
type: MessageType.DELETED,
|
|
1143
|
+
});
|
|
1144
|
+
const controlMessage = {
|
|
1145
|
+
ownerUserId,
|
|
1146
|
+
contactUserId: row.contactUserId,
|
|
1147
|
+
content: '',
|
|
1148
|
+
type: MessageType.DELETED,
|
|
1149
|
+
direction: MessageDirection.OUTGOING,
|
|
1150
|
+
status: MessageStatus.WAITING_SESSION,
|
|
1151
|
+
timestamp: new Date(),
|
|
1152
|
+
deleteOf: { originalMsgId: row.messageId },
|
|
1153
|
+
};
|
|
1154
|
+
const result = await this.send(controlMessage);
|
|
1155
|
+
if (!result.success)
|
|
1156
|
+
throw new Error(result.error ?? 'Failed to enqueue delete message');
|
|
1157
|
+
await this.refreshService?.stateUpdate();
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
// Rollback: emit failure so store can restore original
|
|
1162
|
+
if (row.type !== MessageType.REACTION) {
|
|
1163
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_DELETE_FAILED, {
|
|
1164
|
+
contactUserId: row.contactUserId,
|
|
1165
|
+
messageDbId: id,
|
|
1166
|
+
original,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
// Best-effort DB rollback
|
|
1170
|
+
await this.queries.messages
|
|
1171
|
+
.updateById(id, { content: original.content, type: original.type })
|
|
1172
|
+
.catch(() => { });
|
|
1173
|
+
throw error;
|
|
1140
1174
|
}
|
|
1141
|
-
await this.refreshService?.stateUpdate();
|
|
1142
|
-
return true;
|
|
1143
1175
|
}
|
|
1144
1176
|
async sendReaction(contactUserId, emoji, originalMsgId) {
|
|
1145
1177
|
const message = {
|
|
@@ -1163,50 +1195,59 @@ export class MessageService {
|
|
|
1163
1195
|
*/
|
|
1164
1196
|
async editMessage(id, newContent) {
|
|
1165
1197
|
const row = await this.queries.messages.getById(id);
|
|
1166
|
-
if (!row)
|
|
1198
|
+
if (!row)
|
|
1167
1199
|
return false;
|
|
1168
|
-
|
|
1169
|
-
// Only allow editing our own outgoing messages
|
|
1170
|
-
if (row.direction !== MessageDirection.OUTGOING) {
|
|
1200
|
+
if (row.direction !== MessageDirection.OUTGOING)
|
|
1171
1201
|
return false;
|
|
1172
|
-
|
|
1173
|
-
if (!row.messageId || row.messageId.length !== MESSAGE_ID_SIZE) {
|
|
1202
|
+
if (!row.messageId || row.messageId.length !== MESSAGE_ID_SIZE)
|
|
1174
1203
|
throw new Error('Cannot edit a message that has no valid messageId');
|
|
1175
|
-
|
|
1204
|
+
const original = rowToMessage(row);
|
|
1176
1205
|
const ownerUserId = this.session.userIdEncoded;
|
|
1177
|
-
// Merge existing metadata with edited flag
|
|
1178
1206
|
const existingMetadata = deserializeMetadata(row.metadata) ?? {};
|
|
1179
|
-
const mergedMetadata = {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
};
|
|
1183
|
-
// Update the original message content locally, preserving timestamp
|
|
1184
|
-
await this.queries.messages.updateById(id, {
|
|
1185
|
-
content: newContent,
|
|
1186
|
-
metadata: serializeMetadata(mergedMetadata),
|
|
1187
|
-
});
|
|
1188
|
-
// Enqueue an edit control message to notify the peer
|
|
1189
|
-
const controlMessage = {
|
|
1190
|
-
ownerUserId,
|
|
1207
|
+
const mergedMetadata = { ...existingMetadata, edited: true };
|
|
1208
|
+
// Emit optimistic event so UI updates immediately
|
|
1209
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_EDITED_OPTIMISTIC, {
|
|
1191
1210
|
contactUserId: row.contactUserId,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1211
|
+
messageDbId: id,
|
|
1212
|
+
newContent,
|
|
1213
|
+
metadata: mergedMetadata,
|
|
1214
|
+
});
|
|
1215
|
+
try {
|
|
1216
|
+
await this.queries.messages.updateById(id, {
|
|
1217
|
+
content: newContent,
|
|
1218
|
+
metadata: serializeMetadata(mergedMetadata),
|
|
1219
|
+
});
|
|
1220
|
+
const controlMessage = {
|
|
1221
|
+
ownerUserId,
|
|
1222
|
+
contactUserId: row.contactUserId,
|
|
1223
|
+
content: newContent,
|
|
1224
|
+
type: MessageType.TEXT,
|
|
1225
|
+
direction: MessageDirection.OUTGOING,
|
|
1226
|
+
status: MessageStatus.WAITING_SESSION,
|
|
1227
|
+
timestamp: new Date(),
|
|
1228
|
+
editOf: { originalMsgId: row.messageId },
|
|
1229
|
+
metadata: { control: 'edit' },
|
|
1230
|
+
};
|
|
1231
|
+
const result = await this.send(controlMessage);
|
|
1232
|
+
if (!result.success)
|
|
1233
|
+
throw new Error(result.error ?? 'Failed to enqueue edit message');
|
|
1234
|
+
await this.refreshService?.stateUpdate();
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
catch (error) {
|
|
1238
|
+
this.eventEmitter.emit(SdkEventType.MESSAGE_EDIT_FAILED, {
|
|
1239
|
+
contactUserId: row.contactUserId,
|
|
1240
|
+
messageDbId: id,
|
|
1241
|
+
original,
|
|
1242
|
+
});
|
|
1243
|
+
await this.queries.messages
|
|
1244
|
+
.updateById(id, {
|
|
1245
|
+
content: original.content,
|
|
1246
|
+
metadata: row.metadata ?? undefined,
|
|
1247
|
+
})
|
|
1248
|
+
.catch(() => { });
|
|
1249
|
+
throw error;
|
|
1207
1250
|
}
|
|
1208
|
-
await this.refreshService?.stateUpdate();
|
|
1209
|
-
return true;
|
|
1210
1251
|
}
|
|
1211
1252
|
/**
|
|
1212
1253
|
* Hard-delete messages that have exceeded their discussion retention duration.
|
package/dist/services/refresh.js
CHANGED
|
@@ -113,7 +113,10 @@ export class RefreshService {
|
|
|
113
113
|
const previous = this.sessionStatusMap.get(discussion.contactUserId);
|
|
114
114
|
if (previous !== status) {
|
|
115
115
|
this.sessionStatusMap.set(discussion.contactUserId, status);
|
|
116
|
-
this.eventEmitter.emit(SdkEventType.SESSION_STATUS_CHANGED,
|
|
116
|
+
this.eventEmitter.emit(SdkEventType.SESSION_STATUS_CHANGED, {
|
|
117
|
+
contactUserId: discussion.contactUserId,
|
|
118
|
+
status,
|
|
119
|
+
});
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
}
|
|
@@ -218,7 +221,10 @@ export class RefreshService {
|
|
|
218
221
|
timestamp: new Date(),
|
|
219
222
|
});
|
|
220
223
|
if (!result.success) {
|
|
221
|
-
this.eventEmitter.emit(SdkEventType.ERROR,
|
|
224
|
+
this.eventEmitter.emit(SdkEventType.ERROR, {
|
|
225
|
+
error: new Error(result.error || 'Unknown error'),
|
|
226
|
+
context: 'keep_alive_message',
|
|
227
|
+
});
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
230
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massalabs/gossip-sdk",
|
|
3
|
-
"version": "0.0.2-dev.
|
|
3
|
+
"version": "0.0.2-dev.20260410093719",
|
|
4
4
|
"description": "Gossip SDK for automation, chatbot, and integration use cases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@scure/base": "^2.0.0",
|
|
51
51
|
"@scure/bip39": "^2.0.1",
|
|
52
52
|
"drizzle-orm": "^0.45.2",
|
|
53
|
+
"mitt": "^3.0.1",
|
|
53
54
|
"wa-sqlite": "^1.0.0"
|
|
54
55
|
}
|
|
55
56
|
}
|