@massalabs/gossip-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +484 -0
  2. package/package.json +41 -0
  3. package/src/api/messageProtocol/index.ts +53 -0
  4. package/src/api/messageProtocol/mock.ts +13 -0
  5. package/src/api/messageProtocol/rest.ts +209 -0
  6. package/src/api/messageProtocol/types.ts +70 -0
  7. package/src/config/protocol.ts +97 -0
  8. package/src/config/sdk.ts +131 -0
  9. package/src/contacts.ts +210 -0
  10. package/src/core/SdkEventEmitter.ts +91 -0
  11. package/src/core/SdkPolling.ts +134 -0
  12. package/src/core/index.ts +9 -0
  13. package/src/crypto/bip39.ts +84 -0
  14. package/src/crypto/encryption.ts +77 -0
  15. package/src/db.ts +465 -0
  16. package/src/gossipSdk.ts +994 -0
  17. package/src/index.ts +211 -0
  18. package/src/services/announcement.ts +653 -0
  19. package/src/services/auth.ts +95 -0
  20. package/src/services/discussion.ts +380 -0
  21. package/src/services/message.ts +1055 -0
  22. package/src/services/refresh.ts +234 -0
  23. package/src/sw.ts +17 -0
  24. package/src/types/events.ts +108 -0
  25. package/src/types.ts +70 -0
  26. package/src/utils/base64.ts +39 -0
  27. package/src/utils/contacts.ts +161 -0
  28. package/src/utils/discussions.ts +55 -0
  29. package/src/utils/logs.ts +86 -0
  30. package/src/utils/messageSerialization.ts +257 -0
  31. package/src/utils/queue.ts +106 -0
  32. package/src/utils/type.ts +7 -0
  33. package/src/utils/userId.ts +114 -0
  34. package/src/utils/validation.ts +144 -0
  35. package/src/utils.ts +47 -0
  36. package/src/wasm/encryption.ts +108 -0
  37. package/src/wasm/index.ts +20 -0
  38. package/src/wasm/loader.ts +123 -0
  39. package/src/wasm/session.ts +276 -0
  40. package/src/wasm/userKeys.ts +31 -0
  41. package/test/config/protocol.spec.ts +31 -0
  42. package/test/config/sdk.spec.ts +163 -0
  43. package/test/db/helpers.spec.ts +142 -0
  44. package/test/db/operations.spec.ts +128 -0
  45. package/test/db/states.spec.ts +535 -0
  46. package/test/integration/discussion-flow.spec.ts +422 -0
  47. package/test/integration/messaging-flow.spec.ts +708 -0
  48. package/test/integration/sdk-lifecycle.spec.ts +325 -0
  49. package/test/mocks/index.ts +9 -0
  50. package/test/mocks/mockMessageProtocol.ts +100 -0
  51. package/test/services/auth.spec.ts +311 -0
  52. package/test/services/discussion.spec.ts +279 -0
  53. package/test/services/message-deduplication.spec.ts +299 -0
  54. package/test/services/message-startup.spec.ts +331 -0
  55. package/test/services/message.spec.ts +817 -0
  56. package/test/services/refresh.spec.ts +199 -0
  57. package/test/services/session-status.spec.ts +349 -0
  58. package/test/session/wasm.spec.ts +227 -0
  59. package/test/setup.ts +52 -0
  60. package/test/utils/contacts.spec.ts +156 -0
  61. package/test/utils/discussions.spec.ts +66 -0
  62. package/test/utils/queue.spec.ts +52 -0
  63. package/test/utils/serialization.spec.ts +120 -0
  64. package/test/utils/userId.spec.ts +120 -0
  65. package/test/utils/validation.spec.ts +223 -0
  66. package/test/utils.ts +212 -0
  67. package/tsconfig.json +26 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/vitest.config.ts +28 -0
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Real WASM Session Tests
3
+ *
4
+ * These tests use the actual WASM SessionModule with real crypto.
5
+ * No mocks - tests actual session establishment and message encryption.
6
+ */
7
+
8
+ import { describe, it, expect, afterEach } from 'vitest';
9
+ import { SessionStatus } from '../../src/assets/generated/wasm/gossip_wasm';
10
+ import {
11
+ createTestSession,
12
+ createTestSessionPair,
13
+ cleanupTestSession,
14
+ TestSessionData,
15
+ } from '../utils';
16
+
17
+ describe('Real WASM Session', () => {
18
+ const sessionsToCleanup: TestSessionData[] = [];
19
+
20
+ afterEach(() => {
21
+ // Cleanup all sessions created during tests
22
+ sessionsToCleanup.forEach(cleanupTestSession);
23
+ sessionsToCleanup.length = 0;
24
+ });
25
+
26
+ it('should create a session with real WASM keys', async () => {
27
+ const sessionData = await createTestSession();
28
+ sessionsToCleanup.push(sessionData);
29
+
30
+ expect(sessionData.session.userId).toBeInstanceOf(Uint8Array);
31
+ expect(sessionData.session.userId.length).toBe(32);
32
+ expect(sessionData.session.userIdEncoded).toMatch(/^gossip1/);
33
+ });
34
+
35
+ it('should establish outgoing session between Alice and Bob', async () => {
36
+ const { alice, bob } = await createTestSessionPair();
37
+ sessionsToCleanup.push(alice, bob);
38
+
39
+ // Alice establishes outgoing session to Bob
40
+ const announcement = await alice.session.establishOutgoingSession(
41
+ bob.session.ourPk
42
+ );
43
+
44
+ expect(announcement).toBeInstanceOf(Uint8Array);
45
+ expect(announcement.length).toBeGreaterThan(0);
46
+
47
+ // Alice should now have Bob as a peer with SelfRequested status
48
+ const alicePeers = alice.session.peerList();
49
+ expect(alicePeers.length).toBe(1);
50
+
51
+ const bobStatus = alice.session.peerSessionStatus(bob.session.userId);
52
+ expect(bobStatus).toBe(SessionStatus.SelfRequested);
53
+ });
54
+
55
+ it('should complete full session handshake', async () => {
56
+ const { alice, bob } = await createTestSessionPair();
57
+ sessionsToCleanup.push(alice, bob);
58
+
59
+ // Alice creates announcement for Bob
60
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
61
+ bob.session.ourPk
62
+ );
63
+
64
+ // Bob receives and processes Alice's announcement
65
+ const announcementResult =
66
+ await bob.session.feedIncomingAnnouncement(aliceAnnouncement);
67
+
68
+ expect(announcementResult).toBeDefined();
69
+ expect(announcementResult?.announcer_public_keys).toBeDefined();
70
+
71
+ // Bob should now have Alice as a peer with PeerRequested status
72
+ const aliceStatusFromBob = bob.session.peerSessionStatus(
73
+ alice.session.userId
74
+ );
75
+ expect(aliceStatusFromBob).toBe(SessionStatus.PeerRequested);
76
+
77
+ // Bob accepts by establishing outgoing session to Alice
78
+ const bobAnnouncement = await bob.session.establishOutgoingSession(
79
+ alice.session.ourPk
80
+ );
81
+
82
+ // Alice receives Bob's acceptance announcement
83
+ const bobAnnouncementResult =
84
+ await alice.session.feedIncomingAnnouncement(bobAnnouncement);
85
+
86
+ expect(bobAnnouncementResult).toBeDefined();
87
+
88
+ // Both should now have Active sessions
89
+ const aliceStatusOfBob = alice.session.peerSessionStatus(
90
+ bob.session.userId
91
+ );
92
+ const bobStatusOfAlice = bob.session.peerSessionStatus(
93
+ alice.session.userId
94
+ );
95
+
96
+ expect(aliceStatusOfBob).toBe(SessionStatus.Active);
97
+ expect(bobStatusOfAlice).toBe(SessionStatus.Active);
98
+ });
99
+
100
+ it('should include user data in announcement', async () => {
101
+ const { alice, bob } = await createTestSessionPair();
102
+ sessionsToCleanup.push(alice, bob);
103
+
104
+ // Alice sends announcement with custom user data
105
+ const userData = new TextEncoder().encode(
106
+ JSON.stringify({ u: 'Alice', m: 'Hello!' })
107
+ );
108
+ const announcement = await alice.session.establishOutgoingSession(
109
+ bob.session.ourPk,
110
+ userData
111
+ );
112
+
113
+ // Bob processes and extracts user data
114
+ const result = await bob.session.feedIncomingAnnouncement(announcement);
115
+
116
+ expect(result).toBeDefined();
117
+ expect(result?.user_data).toBeInstanceOf(Uint8Array);
118
+
119
+ const parsedUserData = JSON.parse(
120
+ new TextDecoder().decode(result?.user_data)
121
+ );
122
+ expect(parsedUserData.u).toBe('Alice');
123
+ expect(parsedUserData.m).toBe('Hello!');
124
+ });
125
+
126
+ it('should get message board read keys (seekers)', async () => {
127
+ const { alice, bob } = await createTestSessionPair();
128
+ sessionsToCleanup.push(alice, bob);
129
+
130
+ // Establish session
131
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
132
+ bob.session.ourPk
133
+ );
134
+ await bob.session.feedIncomingAnnouncement(aliceAnnouncement);
135
+
136
+ const bobAnnouncement = await bob.session.establishOutgoingSession(
137
+ alice.session.ourPk
138
+ );
139
+ await alice.session.feedIncomingAnnouncement(bobAnnouncement);
140
+
141
+ // Both should have seekers to monitor
142
+ const aliceSeekers = alice.session.getMessageBoardReadKeys();
143
+ const bobSeekers = bob.session.getMessageBoardReadKeys();
144
+
145
+ expect(aliceSeekers.length).toBeGreaterThan(0);
146
+ expect(bobSeekers.length).toBeGreaterThan(0);
147
+
148
+ // Each seeker should be a valid key
149
+ aliceSeekers.forEach(seeker => {
150
+ expect(seeker).toBeInstanceOf(Uint8Array);
151
+ expect(seeker.length).toBeGreaterThan(0);
152
+ });
153
+ });
154
+
155
+ it('should send and receive encrypted messages', async () => {
156
+ const { alice, bob } = await createTestSessionPair();
157
+ sessionsToCleanup.push(alice, bob);
158
+
159
+ // Establish session (Alice initiates, Bob accepts)
160
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
161
+ bob.session.ourPk
162
+ );
163
+ await bob.session.feedIncomingAnnouncement(aliceAnnouncement);
164
+
165
+ const bobAnnouncement = await bob.session.establishOutgoingSession(
166
+ alice.session.ourPk
167
+ );
168
+ await alice.session.feedIncomingAnnouncement(bobAnnouncement);
169
+
170
+ // Both sessions should be active
171
+ expect(alice.session.peerSessionStatus(bob.session.userId)).toBe(
172
+ SessionStatus.Active
173
+ );
174
+ expect(bob.session.peerSessionStatus(alice.session.userId)).toBe(
175
+ SessionStatus.Active
176
+ );
177
+
178
+ // Alice sends a message to Bob
179
+ const messageContent = new TextEncoder().encode('Hello Bob!');
180
+ const sendResult = await alice.session.sendMessage(
181
+ bob.session.userId,
182
+ messageContent
183
+ );
184
+
185
+ expect(sendResult).toBeDefined();
186
+ expect(sendResult?.seeker).toBeInstanceOf(Uint8Array);
187
+ expect(sendResult?.data).toBeInstanceOf(Uint8Array);
188
+
189
+ // Bob receives and decrypts the message
190
+ const receiveResult = await bob.session.feedIncomingMessageBoardRead(
191
+ sendResult!.seeker,
192
+ sendResult!.data
193
+ );
194
+
195
+ expect(receiveResult).toBeDefined();
196
+
197
+ // The plaintext should contain the original message
198
+ if (receiveResult?.plaintext) {
199
+ const decryptedMessage = new TextDecoder().decode(
200
+ receiveResult.plaintext
201
+ );
202
+ expect(decryptedMessage).toBe('Hello Bob!');
203
+ }
204
+ });
205
+
206
+ it('should discard peer session', async () => {
207
+ const { alice, bob } = await createTestSessionPair();
208
+ sessionsToCleanup.push(alice, bob);
209
+
210
+ // Alice establishes session
211
+ await alice.session.establishOutgoingSession(bob.session.ourPk);
212
+
213
+ expect(alice.session.peerList().length).toBe(1);
214
+
215
+ // Alice discards Bob
216
+ await alice.session.peerDiscard(bob.session.userId);
217
+
218
+ // Bob should no longer be in peer list (or status should be Killed)
219
+ const statusAfterDiscard = alice.session.peerSessionStatus(
220
+ bob.session.userId
221
+ );
222
+ expect(
223
+ statusAfterDiscard === SessionStatus.Killed ||
224
+ statusAfterDiscard === SessionStatus.UnknownPeer
225
+ ).toBe(true);
226
+ });
227
+ });
package/test/setup.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SDK Test Setup File
3
+ *
4
+ * Minimal environment setup for SDK tests:
5
+ * - fake-indexeddb for Dexie/IndexedDB in Node
6
+ * - IDBKeyRange polyfill
7
+ * - Shared database cleanup
8
+ */
9
+
10
+ import 'fake-indexeddb/auto';
11
+ import { IDBKeyRange } from 'fake-indexeddb';
12
+ import { afterAll, beforeAll, beforeEach } from 'vitest';
13
+ import { db } from '../src/db';
14
+
15
+ if (typeof process !== 'undefined') {
16
+ process.env.GOSSIP_API_URL = 'https://api.usegossip.com';
17
+ process.env.VITE_GOSSIP_API_URL = 'https://api.usegossip.com';
18
+ }
19
+
20
+ if (typeof globalThis.IDBKeyRange === 'undefined') {
21
+ (globalThis as { IDBKeyRange?: typeof IDBKeyRange }).IDBKeyRange =
22
+ IDBKeyRange;
23
+ }
24
+
25
+ async function clearDatabase(): Promise<void> {
26
+ await Promise.all(db.tables.map(table => table.clear()));
27
+ }
28
+
29
+ beforeAll(async () => {
30
+ if (!db.isOpen()) {
31
+ await db.open();
32
+ }
33
+ await clearDatabase();
34
+ });
35
+
36
+ beforeEach(async () => {
37
+ if (!db.isOpen()) {
38
+ await db.open();
39
+ }
40
+ await clearDatabase();
41
+ });
42
+
43
+ afterAll(async () => {
44
+ try {
45
+ await clearDatabase();
46
+ await db.close();
47
+ } catch (_) {
48
+ // Ignore errors - database might already be closed
49
+ }
50
+ });
51
+
52
+ console.log('SDK test setup complete: fake-indexeddb initialized');
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Contacts utilities tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+ import {
7
+ db,
8
+ DiscussionDirection,
9
+ DiscussionStatus,
10
+ MessageType,
11
+ MessageDirection,
12
+ MessageStatus,
13
+ } from '../../src/db';
14
+ import { encodeUserId } from '../../src/utils/userId';
15
+ import { addContact, getContact, getContacts } from '../../src/contacts';
16
+ import { updateContactName, deleteContact } from '../../src/utils/contacts';
17
+ import type { SessionModule } from '../../src/wasm/session';
18
+ import type { UserPublicKeys as UserPublicKeysType } from '../../src/assets/generated/wasm/gossip_wasm';
19
+
20
+ const CONTACTS_OWNER_USER_ID = encodeUserId(new Uint8Array(32).fill(1));
21
+ const CONTACTS_CONTACT_USER_ID = encodeUserId(new Uint8Array(32).fill(2));
22
+ const CONTACTS_CONTACT_USER_ID_2 = encodeUserId(new Uint8Array(32).fill(3));
23
+
24
+ const publicKeys = {
25
+ to_bytes: () => new Uint8Array([1, 2, 3]),
26
+ } as unknown as UserPublicKeysType;
27
+
28
+ const fakeSession = {
29
+ peerDiscard: vi.fn(),
30
+ } as unknown as SessionModule;
31
+
32
+ describe('Contacts utilities', () => {
33
+ beforeEach(async () => {
34
+ if (!db.isOpen()) {
35
+ await db.open();
36
+ }
37
+ await Promise.all(db.tables.map(table => table.clear()));
38
+ });
39
+
40
+ it('adds and fetches a contact', async () => {
41
+ const result = await addContact(
42
+ CONTACTS_OWNER_USER_ID,
43
+ CONTACTS_CONTACT_USER_ID,
44
+ 'Alice',
45
+ publicKeys,
46
+ db
47
+ );
48
+ expect(result.success).toBe(true);
49
+
50
+ const contact = await getContact(
51
+ CONTACTS_OWNER_USER_ID,
52
+ CONTACTS_CONTACT_USER_ID,
53
+ db
54
+ );
55
+ expect(contact?.name).toBe('Alice');
56
+ });
57
+
58
+ it('returns error when contact already exists', async () => {
59
+ await addContact(
60
+ CONTACTS_OWNER_USER_ID,
61
+ CONTACTS_CONTACT_USER_ID,
62
+ 'Alice',
63
+ publicKeys,
64
+ db
65
+ );
66
+ const result = await addContact(
67
+ CONTACTS_OWNER_USER_ID,
68
+ CONTACTS_CONTACT_USER_ID,
69
+ 'Alice 2',
70
+ publicKeys,
71
+ db
72
+ );
73
+
74
+ expect(result.success).toBe(false);
75
+ expect(result.error).toContain('exists');
76
+ });
77
+
78
+ it('updates contact name and rejects duplicates', async () => {
79
+ await addContact(
80
+ CONTACTS_OWNER_USER_ID,
81
+ CONTACTS_CONTACT_USER_ID,
82
+ 'Alice',
83
+ publicKeys,
84
+ db
85
+ );
86
+ await addContact(
87
+ CONTACTS_OWNER_USER_ID,
88
+ CONTACTS_CONTACT_USER_ID_2,
89
+ 'Bob',
90
+ publicKeys,
91
+ db
92
+ );
93
+
94
+ const updateResult = await updateContactName(
95
+ CONTACTS_OWNER_USER_ID,
96
+ CONTACTS_CONTACT_USER_ID,
97
+ 'Alice Updated',
98
+ db
99
+ );
100
+ expect(updateResult.success).toBe(true);
101
+
102
+ const duplicateResult = await updateContactName(
103
+ CONTACTS_OWNER_USER_ID,
104
+ CONTACTS_CONTACT_USER_ID,
105
+ 'Bob',
106
+ db
107
+ );
108
+ expect(duplicateResult.success).toBe(false);
109
+ if (!duplicateResult.success) {
110
+ expect(duplicateResult.reason).toBe('duplicate');
111
+ }
112
+ });
113
+
114
+ it('deletes contact and related data', async () => {
115
+ await addContact(
116
+ CONTACTS_OWNER_USER_ID,
117
+ CONTACTS_CONTACT_USER_ID,
118
+ 'Alice',
119
+ publicKeys,
120
+ db
121
+ );
122
+
123
+ await db.discussions.add({
124
+ ownerUserId: CONTACTS_OWNER_USER_ID,
125
+ contactUserId: CONTACTS_CONTACT_USER_ID,
126
+ direction: DiscussionDirection.INITIATED,
127
+ status: DiscussionStatus.ACTIVE,
128
+ unreadCount: 0,
129
+ createdAt: new Date(),
130
+ updatedAt: new Date(),
131
+ });
132
+
133
+ await db.messages.add({
134
+ ownerUserId: CONTACTS_OWNER_USER_ID,
135
+ contactUserId: CONTACTS_CONTACT_USER_ID,
136
+ content: 'Hello',
137
+ type: MessageType.TEXT,
138
+ direction: MessageDirection.OUTGOING,
139
+ status: MessageStatus.SENT,
140
+ timestamp: new Date(),
141
+ });
142
+
143
+ const result = await deleteContact(
144
+ CONTACTS_OWNER_USER_ID,
145
+ CONTACTS_CONTACT_USER_ID,
146
+ db,
147
+ fakeSession
148
+ );
149
+
150
+ expect(result.success).toBe(true);
151
+ expect(fakeSession.peerDiscard).toHaveBeenCalled();
152
+
153
+ const contacts = await getContacts(CONTACTS_OWNER_USER_ID, db);
154
+ expect(contacts.length).toBe(0);
155
+ });
156
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Discussion utilities tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { db, DiscussionDirection, DiscussionStatus } from '../../src/db';
7
+ import { encodeUserId } from '../../src/utils/userId';
8
+ import { updateDiscussionName } from '../../src/utils/discussions';
9
+
10
+ const OWNER_USER_ID = encodeUserId(new Uint8Array(32).fill(6));
11
+ const CONTACT_USER_ID = encodeUserId(new Uint8Array(32).fill(7));
12
+
13
+ describe('Discussion utilities', () => {
14
+ beforeEach(async () => {
15
+ if (!db.isOpen()) {
16
+ await db.open();
17
+ }
18
+ await Promise.all(db.tables.map(table => table.clear()));
19
+ });
20
+
21
+ it('updates the custom discussion name', async () => {
22
+ const discussionId = await db.discussions.add({
23
+ ownerUserId: OWNER_USER_ID,
24
+ contactUserId: CONTACT_USER_ID,
25
+ direction: DiscussionDirection.INITIATED,
26
+ status: DiscussionStatus.ACTIVE,
27
+ unreadCount: 0,
28
+ createdAt: new Date(),
29
+ updatedAt: new Date(),
30
+ });
31
+
32
+ const result = await updateDiscussionName(discussionId, 'Custom Name', db);
33
+
34
+ expect(result.success).toBe(true);
35
+ const discussion = await db.discussions.get(discussionId);
36
+ expect(discussion?.customName).toBe('Custom Name');
37
+ });
38
+
39
+ it('clears the custom name when empty', async () => {
40
+ const discussionId = await db.discussions.add({
41
+ ownerUserId: OWNER_USER_ID,
42
+ contactUserId: CONTACT_USER_ID,
43
+ direction: DiscussionDirection.INITIATED,
44
+ status: DiscussionStatus.ACTIVE,
45
+ customName: 'Old Name',
46
+ unreadCount: 0,
47
+ createdAt: new Date(),
48
+ updatedAt: new Date(),
49
+ });
50
+
51
+ const result = await updateDiscussionName(discussionId, ' ', db);
52
+
53
+ expect(result.success).toBe(true);
54
+ const discussion = await db.discussions.get(discussionId);
55
+ expect(discussion?.customName).toBeUndefined();
56
+ });
57
+
58
+ it('returns not_found for unknown discussion', async () => {
59
+ const result = await updateDiscussionName(99999, 'Name', db);
60
+
61
+ expect(result.success).toBe(false);
62
+ if (!result.success) {
63
+ expect(result.reason).toBe('not_found');
64
+ }
65
+ });
66
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Queue utilities tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { PromiseQueue, QueueManager } from '../../src/utils/queue';
7
+
8
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
9
+
10
+ describe('PromiseQueue', () => {
11
+ it('executes tasks sequentially', async () => {
12
+ const queue = new PromiseQueue();
13
+ const order: string[] = [];
14
+
15
+ queue.enqueue(async () => {
16
+ order.push('first-start');
17
+ await sleep(20);
18
+ order.push('first-end');
19
+ });
20
+
21
+ queue.enqueue(async () => {
22
+ order.push('second');
23
+ });
24
+
25
+ await sleep(50);
26
+ expect(order).toEqual(['first-start', 'first-end', 'second']);
27
+ });
28
+ });
29
+
30
+ describe('QueueManager', () => {
31
+ it('queues tasks per key independently', async () => {
32
+ const manager = new QueueManager();
33
+ const order: string[] = [];
34
+
35
+ manager.enqueue('a', async () => {
36
+ order.push('a-1-start');
37
+ await sleep(30);
38
+ order.push('a-1-end');
39
+ });
40
+
41
+ manager.enqueue('b', async () => {
42
+ order.push('b-1');
43
+ });
44
+
45
+ manager.enqueue('a', async () => {
46
+ order.push('a-2');
47
+ });
48
+
49
+ await sleep(70);
50
+ expect(order).toEqual(['a-1-start', 'b-1', 'a-1-end', 'a-2']);
51
+ });
52
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Message serialization utilities tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { MessageType } from '../../src/db';
7
+ import {
8
+ serializeRegularMessage,
9
+ serializeReplyMessage,
10
+ serializeForwardMessage,
11
+ serializeKeepAliveMessage,
12
+ deserializeMessage,
13
+ MESSAGE_TYPE_KEEP_ALIVE,
14
+ } from '../../src/utils/messageSerialization';
15
+
16
+ const serializationSeeker = new Uint8Array(34).fill(4);
17
+
18
+ describe('message serialization', () => {
19
+ it('serializes and deserializes regular messages', () => {
20
+ const serialized = serializeRegularMessage('hello');
21
+ const deserialized = deserializeMessage(serialized);
22
+ expect(deserialized.content).toBe('hello');
23
+ expect(deserialized.type).toBe(MessageType.TEXT);
24
+ });
25
+
26
+ it('serializes and deserializes reply messages', () => {
27
+ const serialized = serializeReplyMessage('new', 'old', serializationSeeker);
28
+ const deserialized = deserializeMessage(serialized);
29
+ expect(deserialized.content).toBe('new');
30
+ expect(deserialized.replyTo?.originalContent).toBe('old');
31
+ expect(deserialized.replyTo?.originalSeeker).toEqual(serializationSeeker);
32
+ });
33
+
34
+ it('serializes and deserializes forward messages', () => {
35
+ const serialized = serializeForwardMessage(
36
+ 'forward',
37
+ 'note',
38
+ serializationSeeker
39
+ );
40
+ const deserialized = deserializeMessage(serialized);
41
+ expect(deserialized.content).toBe('note');
42
+ expect(deserialized.forwardOf?.originalContent).toBe('forward');
43
+ expect(deserialized.forwardOf?.originalSeeker).toEqual(serializationSeeker);
44
+ });
45
+
46
+ it('serializes keep-alive messages', () => {
47
+ const serialized = serializeKeepAliveMessage();
48
+ expect(serialized[0]).toBe(MESSAGE_TYPE_KEEP_ALIVE);
49
+ const deserialized = deserializeMessage(serialized);
50
+ expect(deserialized.type).toBe(MessageType.KEEP_ALIVE);
51
+ });
52
+
53
+ it('serializes reply with empty original content', () => {
54
+ const serialized = serializeReplyMessage('reply', '', serializationSeeker);
55
+ const deserialized = deserializeMessage(serialized);
56
+ expect(deserialized.replyTo?.originalContent).toBe('');
57
+ expect(deserialized.content).toBe('reply');
58
+ });
59
+
60
+ it('serializes reply with unicode characters', () => {
61
+ const serialized = serializeReplyMessage(
62
+ 'Reply with emoji ',
63
+ 'Original with unicode ',
64
+ serializationSeeker
65
+ );
66
+ const deserialized = deserializeMessage(serialized);
67
+ expect(deserialized.content).toBe('Reply with emoji ');
68
+ expect(deserialized.replyTo?.originalContent).toBe(
69
+ 'Original with unicode '
70
+ );
71
+ });
72
+
73
+ it('serializes forward without additional content', () => {
74
+ const serialized = serializeForwardMessage(
75
+ 'Just forwarding this',
76
+ '',
77
+ serializationSeeker
78
+ );
79
+ const deserialized = deserializeMessage(serialized);
80
+ expect(deserialized.content).toBe('');
81
+ expect(deserialized.forwardOf?.originalContent).toBe(
82
+ 'Just forwarding this'
83
+ );
84
+ });
85
+ });
86
+
87
+ describe('Deserialization Failure Handling', () => {
88
+ it('should handle invalid message format gracefully', () => {
89
+ const invalidData = new Uint8Array([0, 1]);
90
+
91
+ try {
92
+ const result = deserializeMessage(invalidData);
93
+ expect(result).toBeDefined();
94
+ } catch {
95
+ expect(true).toBe(true);
96
+ }
97
+ });
98
+
99
+ it('should handle corrupted message bytes', () => {
100
+ const corruptedData = new Uint8Array([0, 255, 255, 255, 255, 0, 0, 0, 0]);
101
+
102
+ try {
103
+ const result = deserializeMessage(corruptedData);
104
+ expect(result).toBeDefined();
105
+ } catch {
106
+ expect(true).toBe(true);
107
+ }
108
+ });
109
+
110
+ it('should handle empty message data', () => {
111
+ const emptyData = new Uint8Array(0);
112
+
113
+ try {
114
+ const result = deserializeMessage(emptyData);
115
+ expect(result.content).toBe('');
116
+ } catch {
117
+ expect(true).toBe(true);
118
+ }
119
+ });
120
+ });