@quilibrium/quorum-shared 2.1.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.
- package/dist/index.d.mts +2414 -0
- package/dist/index.d.ts +2414 -0
- package/dist/index.js +2788 -0
- package/dist/index.mjs +2678 -0
- package/package.json +49 -0
- package/src/api/client.ts +86 -0
- package/src/api/endpoints.ts +87 -0
- package/src/api/errors.ts +179 -0
- package/src/api/index.ts +35 -0
- package/src/crypto/encryption-state.ts +249 -0
- package/src/crypto/index.ts +55 -0
- package/src/crypto/types.ts +307 -0
- package/src/crypto/wasm-provider.ts +298 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/keys.ts +62 -0
- package/src/hooks/mutations/index.ts +15 -0
- package/src/hooks/mutations/useDeleteMessage.ts +67 -0
- package/src/hooks/mutations/useEditMessage.ts +87 -0
- package/src/hooks/mutations/useReaction.ts +163 -0
- package/src/hooks/mutations/useSendMessage.ts +131 -0
- package/src/hooks/useChannels.ts +49 -0
- package/src/hooks/useMessages.ts +77 -0
- package/src/hooks/useSpaces.ts +60 -0
- package/src/index.ts +32 -0
- package/src/signing/index.ts +10 -0
- package/src/signing/types.ts +83 -0
- package/src/signing/wasm-provider.ts +75 -0
- package/src/storage/adapter.ts +118 -0
- package/src/storage/index.ts +9 -0
- package/src/sync/index.ts +83 -0
- package/src/sync/service.test.ts +822 -0
- package/src/sync/service.ts +947 -0
- package/src/sync/types.ts +267 -0
- package/src/sync/utils.ts +588 -0
- package/src/transport/browser-websocket.ts +299 -0
- package/src/transport/index.ts +34 -0
- package/src/transport/rn-websocket.ts +321 -0
- package/src/transport/types.ts +56 -0
- package/src/transport/websocket.ts +212 -0
- package/src/types/bookmark.ts +29 -0
- package/src/types/conversation.ts +25 -0
- package/src/types/index.ts +57 -0
- package/src/types/message.ts +178 -0
- package/src/types/space.ts +75 -0
- package/src/types/user.ts +72 -0
- package/src/utils/encoding.ts +106 -0
- package/src/utils/formatting.ts +139 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/mentions.ts +135 -0
- package/src/utils/validation.ts +84 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncService Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the sync service functionality including:
|
|
5
|
+
* - Payload cache management
|
|
6
|
+
* - buildSyncRequest
|
|
7
|
+
* - buildSyncInfo
|
|
8
|
+
* - buildSyncInitiate
|
|
9
|
+
* - buildSyncManifest
|
|
10
|
+
* - buildSyncDelta
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
14
|
+
import { SyncService } from './service';
|
|
15
|
+
import type { StorageAdapter } from '../storage';
|
|
16
|
+
import type { Message, SpaceMember } from '../types';
|
|
17
|
+
import type { SyncSummary, SyncManifest, MemberDigest } from './types';
|
|
18
|
+
import { createSyncSummary, createManifest, createMemberDigest, MAX_CHUNK_SIZE } from './utils';
|
|
19
|
+
|
|
20
|
+
// Mock storage adapter
|
|
21
|
+
function createMockStorage(
|
|
22
|
+
messages: Message[] = [],
|
|
23
|
+
members: SpaceMember[] = []
|
|
24
|
+
): StorageAdapter {
|
|
25
|
+
return {
|
|
26
|
+
getMessages: vi.fn().mockResolvedValue({ messages, nextCursor: null }),
|
|
27
|
+
getMessage: vi.fn().mockImplementation(async ({ messageId }) => {
|
|
28
|
+
return messages.find((m) => m.messageId === messageId) || null;
|
|
29
|
+
}),
|
|
30
|
+
saveMessage: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
getSpaceMembers: vi.fn().mockResolvedValue(members),
|
|
33
|
+
saveSpaceMember: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
// Add other required methods as no-ops
|
|
35
|
+
getConversations: vi.fn().mockResolvedValue({ conversations: [] }),
|
|
36
|
+
saveConversation: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
deleteConversation: vi.fn().mockResolvedValue(undefined),
|
|
38
|
+
} as unknown as StorageAdapter;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper to create a test message
|
|
42
|
+
function createTestMessage(
|
|
43
|
+
id: string,
|
|
44
|
+
spaceId: string,
|
|
45
|
+
channelId: string,
|
|
46
|
+
text: string,
|
|
47
|
+
createdDate: number = Date.now()
|
|
48
|
+
): Message {
|
|
49
|
+
return {
|
|
50
|
+
messageId: id,
|
|
51
|
+
spaceId,
|
|
52
|
+
channelId,
|
|
53
|
+
digestAlgorithm: 'sha256',
|
|
54
|
+
nonce: 'test-nonce',
|
|
55
|
+
createdDate,
|
|
56
|
+
modifiedDate: createdDate,
|
|
57
|
+
lastModifiedHash: 'hash',
|
|
58
|
+
content: {
|
|
59
|
+
type: 'post',
|
|
60
|
+
senderId: 'sender-address',
|
|
61
|
+
text,
|
|
62
|
+
},
|
|
63
|
+
reactions: [],
|
|
64
|
+
mentions: { memberIds: [], roleIds: [], channelIds: [] },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper to create a test member
|
|
69
|
+
function createTestMember(address: string, displayName?: string): SpaceMember {
|
|
70
|
+
return {
|
|
71
|
+
address,
|
|
72
|
+
inbox_address: `inbox-${address}`,
|
|
73
|
+
display_name: displayName,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('SyncService', () => {
|
|
78
|
+
const spaceId = 'test-space-id';
|
|
79
|
+
const channelId = 'test-channel-id';
|
|
80
|
+
const inboxAddress = 'test-inbox-address';
|
|
81
|
+
|
|
82
|
+
describe('Cache Management', () => {
|
|
83
|
+
it('should load data from storage on first access', async () => {
|
|
84
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello')];
|
|
85
|
+
const members = [createTestMember('addr1', 'User1')];
|
|
86
|
+
const storage = createMockStorage(messages, members);
|
|
87
|
+
|
|
88
|
+
const service = new SyncService({ storage });
|
|
89
|
+
|
|
90
|
+
// First call should fetch from storage
|
|
91
|
+
const payload = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
92
|
+
|
|
93
|
+
expect(storage.getMessages).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(storage.getSpaceMembers).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(payload.summary.messageCount).toBe(1);
|
|
96
|
+
expect(payload.summary.memberCount).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should use cache on subsequent access', async () => {
|
|
100
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello')];
|
|
101
|
+
const members = [createTestMember('addr1', 'User1')];
|
|
102
|
+
const storage = createMockStorage(messages, members);
|
|
103
|
+
|
|
104
|
+
const service = new SyncService({ storage });
|
|
105
|
+
|
|
106
|
+
// First call
|
|
107
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
108
|
+
// Second call should use cache
|
|
109
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
110
|
+
|
|
111
|
+
// Storage should only be called once
|
|
112
|
+
expect(storage.getMessages).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(storage.getSpaceMembers).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should update cache with new message without storage query', async () => {
|
|
117
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello')];
|
|
118
|
+
const members = [createTestMember('addr1', 'User1')];
|
|
119
|
+
const storage = createMockStorage(messages, members);
|
|
120
|
+
|
|
121
|
+
const service = new SyncService({ storage });
|
|
122
|
+
|
|
123
|
+
// Initialize cache
|
|
124
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
125
|
+
expect(payload1.summary.messageCount).toBe(1);
|
|
126
|
+
|
|
127
|
+
// Update cache with new message
|
|
128
|
+
const newMessage = createTestMessage('msg2', spaceId, channelId, 'World');
|
|
129
|
+
service.updateCacheWithMessage(spaceId, channelId, newMessage);
|
|
130
|
+
|
|
131
|
+
// Get payload again - should reflect new message without storage query
|
|
132
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
133
|
+
expect(payload2.summary.messageCount).toBe(2);
|
|
134
|
+
|
|
135
|
+
// Storage should still only be called once (initial load)
|
|
136
|
+
expect(storage.getMessages).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should update cache with new member without storage query', async () => {
|
|
140
|
+
const messages: Message[] = [];
|
|
141
|
+
const members = [createTestMember('addr1', 'User1')];
|
|
142
|
+
const storage = createMockStorage(messages, members);
|
|
143
|
+
|
|
144
|
+
const service = new SyncService({ storage });
|
|
145
|
+
|
|
146
|
+
// Initialize cache
|
|
147
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
148
|
+
expect(payload1.summary.memberCount).toBe(1);
|
|
149
|
+
|
|
150
|
+
// Update cache with new member
|
|
151
|
+
const newMember = createTestMember('addr2', 'User2');
|
|
152
|
+
service.updateCacheWithMember(spaceId, channelId, newMember);
|
|
153
|
+
|
|
154
|
+
// Get payload again - should reflect new member without storage query
|
|
155
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
156
|
+
expect(payload2.summary.memberCount).toBe(2);
|
|
157
|
+
|
|
158
|
+
// Storage should still only be called once (initial load)
|
|
159
|
+
expect(storage.getSpaceMembers).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should invalidate cache and reload from storage', async () => {
|
|
163
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello')];
|
|
164
|
+
const members = [createTestMember('addr1', 'User1')];
|
|
165
|
+
const storage = createMockStorage(messages, members);
|
|
166
|
+
|
|
167
|
+
const service = new SyncService({ storage });
|
|
168
|
+
|
|
169
|
+
// Initialize cache
|
|
170
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
171
|
+
expect(storage.getMessages).toHaveBeenCalledTimes(1);
|
|
172
|
+
|
|
173
|
+
// Invalidate cache
|
|
174
|
+
service.invalidateCache(spaceId, channelId);
|
|
175
|
+
|
|
176
|
+
// Next call should fetch from storage again
|
|
177
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
178
|
+
expect(storage.getMessages).toHaveBeenCalledTimes(2);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('buildSyncRequest', () => {
|
|
183
|
+
it('should return correct payload structure', async () => {
|
|
184
|
+
const messages = [
|
|
185
|
+
createTestMessage('msg1', spaceId, channelId, 'Hello', 1000),
|
|
186
|
+
createTestMessage('msg2', spaceId, channelId, 'World', 2000),
|
|
187
|
+
];
|
|
188
|
+
const members = [createTestMember('addr1')];
|
|
189
|
+
const storage = createMockStorage(messages, members);
|
|
190
|
+
|
|
191
|
+
const service = new SyncService({ storage });
|
|
192
|
+
const payload = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
193
|
+
|
|
194
|
+
expect(payload.type).toBe('sync-request');
|
|
195
|
+
expect(payload.inboxAddress).toBe(inboxAddress);
|
|
196
|
+
expect(payload.expiry).toBeGreaterThan(Date.now());
|
|
197
|
+
expect(payload.summary.messageCount).toBe(2);
|
|
198
|
+
expect(payload.summary.memberCount).toBe(1);
|
|
199
|
+
expect(payload.summary.oldestMessageTimestamp).toBe(1000);
|
|
200
|
+
expect(payload.summary.newestMessageTimestamp).toBe(2000);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should create a sync session', async () => {
|
|
204
|
+
const storage = createMockStorage([], []);
|
|
205
|
+
const service = new SyncService({ storage });
|
|
206
|
+
|
|
207
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
208
|
+
|
|
209
|
+
expect(service.hasActiveSession(spaceId)).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('buildSyncInfo', () => {
|
|
214
|
+
it('should return null when we have no data', async () => {
|
|
215
|
+
const storage = createMockStorage([], []);
|
|
216
|
+
const service = new SyncService({ storage });
|
|
217
|
+
|
|
218
|
+
const theirSummary: SyncSummary = {
|
|
219
|
+
messageCount: 5,
|
|
220
|
+
memberCount: 3,
|
|
221
|
+
newestMessageTimestamp: 1000,
|
|
222
|
+
oldestMessageTimestamp: 500,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = await service.buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary);
|
|
226
|
+
expect(result).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should return null when manifests match', async () => {
|
|
230
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
231
|
+
const members = [createTestMember('addr1')];
|
|
232
|
+
const storage = createMockStorage(messages, members);
|
|
233
|
+
|
|
234
|
+
const service = new SyncService({ storage });
|
|
235
|
+
|
|
236
|
+
// Build our summary first to get the manifest hash
|
|
237
|
+
const ourPayload = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
238
|
+
|
|
239
|
+
// Their summary matches ours
|
|
240
|
+
const theirSummary: SyncSummary = {
|
|
241
|
+
messageCount: 1,
|
|
242
|
+
memberCount: 1,
|
|
243
|
+
newestMessageTimestamp: 1000,
|
|
244
|
+
oldestMessageTimestamp: 1000,
|
|
245
|
+
manifestHash: ourPayload.summary.manifestHash,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = await service.buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary);
|
|
249
|
+
expect(result).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should return sync-info when we have more messages', async () => {
|
|
253
|
+
const messages = [
|
|
254
|
+
createTestMessage('msg1', spaceId, channelId, 'Hello', 1000),
|
|
255
|
+
createTestMessage('msg2', spaceId, channelId, 'World', 2000),
|
|
256
|
+
];
|
|
257
|
+
const members = [createTestMember('addr1')];
|
|
258
|
+
const storage = createMockStorage(messages, members);
|
|
259
|
+
|
|
260
|
+
const service = new SyncService({ storage });
|
|
261
|
+
|
|
262
|
+
const theirSummary: SyncSummary = {
|
|
263
|
+
messageCount: 1,
|
|
264
|
+
memberCount: 1,
|
|
265
|
+
newestMessageTimestamp: 1000,
|
|
266
|
+
oldestMessageTimestamp: 1000,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await service.buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary);
|
|
270
|
+
expect(result).not.toBeNull();
|
|
271
|
+
expect(result!.type).toBe('sync-info');
|
|
272
|
+
expect(result!.summary.messageCount).toBe(2);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should return sync-info when we have newer messages', async () => {
|
|
276
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 2000)];
|
|
277
|
+
const members = [createTestMember('addr1')];
|
|
278
|
+
const storage = createMockStorage(messages, members);
|
|
279
|
+
|
|
280
|
+
const service = new SyncService({ storage });
|
|
281
|
+
|
|
282
|
+
const theirSummary: SyncSummary = {
|
|
283
|
+
messageCount: 1,
|
|
284
|
+
memberCount: 1,
|
|
285
|
+
newestMessageTimestamp: 1000,
|
|
286
|
+
oldestMessageTimestamp: 1000,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const result = await service.buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary);
|
|
290
|
+
expect(result).not.toBeNull();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('buildSyncManifest', () => {
|
|
295
|
+
it('should return correct manifest payload', async () => {
|
|
296
|
+
const messages = [
|
|
297
|
+
createTestMessage('msg1', spaceId, channelId, 'Hello', 1000),
|
|
298
|
+
createTestMessage('msg2', spaceId, channelId, 'World', 2000),
|
|
299
|
+
];
|
|
300
|
+
const members = [
|
|
301
|
+
createTestMember('addr1', 'User1'),
|
|
302
|
+
createTestMember('addr2', 'User2'),
|
|
303
|
+
];
|
|
304
|
+
const storage = createMockStorage(messages, members);
|
|
305
|
+
|
|
306
|
+
const service = new SyncService({ storage });
|
|
307
|
+
const peerIds = [1, 2, 3];
|
|
308
|
+
|
|
309
|
+
const payload = await service.buildSyncManifest(spaceId, channelId, peerIds, inboxAddress);
|
|
310
|
+
|
|
311
|
+
expect(payload.type).toBe('sync-manifest');
|
|
312
|
+
expect(payload.inboxAddress).toBe(inboxAddress);
|
|
313
|
+
expect(payload.manifest.messageCount).toBe(2);
|
|
314
|
+
expect(payload.manifest.digests).toHaveLength(2);
|
|
315
|
+
expect(payload.memberDigests).toHaveLength(2);
|
|
316
|
+
expect(payload.peerIds).toEqual(peerIds);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should use cached data', async () => {
|
|
320
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello')];
|
|
321
|
+
const members = [createTestMember('addr1')];
|
|
322
|
+
const storage = createMockStorage(messages, members);
|
|
323
|
+
|
|
324
|
+
const service = new SyncService({ storage });
|
|
325
|
+
|
|
326
|
+
// First call - loads cache
|
|
327
|
+
await service.buildSyncManifest(spaceId, channelId, [], inboxAddress);
|
|
328
|
+
// Second call - should use cache
|
|
329
|
+
await service.buildSyncManifest(spaceId, channelId, [], inboxAddress);
|
|
330
|
+
|
|
331
|
+
expect(storage.getMessages).toHaveBeenCalledTimes(1);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('buildSyncDelta', () => {
|
|
336
|
+
it('should return empty delta when no differences', async () => {
|
|
337
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
338
|
+
const members = [createTestMember('addr1')];
|
|
339
|
+
const storage = createMockStorage(messages, members);
|
|
340
|
+
|
|
341
|
+
const service = new SyncService({ storage });
|
|
342
|
+
|
|
343
|
+
// Build our manifest
|
|
344
|
+
const ourManifest = await service.buildSyncManifest(spaceId, channelId, [], inboxAddress);
|
|
345
|
+
|
|
346
|
+
// Their manifest is the same as ours
|
|
347
|
+
const deltas = await service.buildSyncDelta(
|
|
348
|
+
spaceId,
|
|
349
|
+
channelId,
|
|
350
|
+
ourManifest.manifest,
|
|
351
|
+
ourManifest.memberDigests,
|
|
352
|
+
[],
|
|
353
|
+
new Map()
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Should have one final delta with no data
|
|
357
|
+
expect(deltas).toHaveLength(1);
|
|
358
|
+
expect(deltas[0].isFinal).toBe(true);
|
|
359
|
+
expect(deltas[0].messageDelta).toBeUndefined();
|
|
360
|
+
expect(deltas[0].memberDelta).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should include messages they are missing', async () => {
|
|
364
|
+
const messages = [
|
|
365
|
+
createTestMessage('msg1', spaceId, channelId, 'Hello', 1000),
|
|
366
|
+
createTestMessage('msg2', spaceId, channelId, 'World', 2000),
|
|
367
|
+
];
|
|
368
|
+
const members = [createTestMember('addr1')];
|
|
369
|
+
const storage = createMockStorage(messages, members);
|
|
370
|
+
|
|
371
|
+
const service = new SyncService({ storage });
|
|
372
|
+
|
|
373
|
+
// Their manifest only has msg1
|
|
374
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, [messages[0]]);
|
|
375
|
+
const theirMemberDigests = members.map(createMemberDigest);
|
|
376
|
+
|
|
377
|
+
const deltas = await service.buildSyncDelta(
|
|
378
|
+
spaceId,
|
|
379
|
+
channelId,
|
|
380
|
+
theirManifest,
|
|
381
|
+
theirMemberDigests,
|
|
382
|
+
[],
|
|
383
|
+
new Map()
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Should include msg2 as new message
|
|
387
|
+
const allNewMessages = deltas.flatMap((d) => d.messageDelta?.newMessages || []);
|
|
388
|
+
expect(allNewMessages).toHaveLength(1);
|
|
389
|
+
expect(allNewMessages[0].messageId).toBe('msg2');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should include members they are missing', async () => {
|
|
393
|
+
const messages: Message[] = [];
|
|
394
|
+
const members = [
|
|
395
|
+
createTestMember('addr1', 'User1'),
|
|
396
|
+
createTestMember('addr2', 'User2'),
|
|
397
|
+
];
|
|
398
|
+
const storage = createMockStorage(messages, members);
|
|
399
|
+
|
|
400
|
+
const service = new SyncService({ storage });
|
|
401
|
+
|
|
402
|
+
// They only have addr1
|
|
403
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, []);
|
|
404
|
+
const theirMemberDigests = [createMemberDigest(members[0])];
|
|
405
|
+
|
|
406
|
+
const deltas = await service.buildSyncDelta(
|
|
407
|
+
spaceId,
|
|
408
|
+
channelId,
|
|
409
|
+
theirManifest,
|
|
410
|
+
theirMemberDigests,
|
|
411
|
+
[],
|
|
412
|
+
new Map()
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Should include addr2 as new member
|
|
416
|
+
const finalDelta = deltas.find((d) => d.isFinal);
|
|
417
|
+
expect(finalDelta?.memberDelta?.members).toHaveLength(1);
|
|
418
|
+
expect(finalDelta?.memberDelta?.members[0].address).toBe('addr2');
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('buildSyncDelta chunking', () => {
|
|
423
|
+
// Use 1MB chunk size for testing (matching realistic network limits)
|
|
424
|
+
const TEST_CHUNK_SIZE = 1 * 1024 * 1024; // 1MB
|
|
425
|
+
const msgSize = 200000; // 200KB per message
|
|
426
|
+
const numMessagesForChunking = 6; // 6 messages * 200KB = 1.2MB > 1MB
|
|
427
|
+
|
|
428
|
+
it('should chunk large message sets into multiple payloads', async () => {
|
|
429
|
+
// Create messages that will exceed chunk size
|
|
430
|
+
const largeText = 'x'.repeat(msgSize);
|
|
431
|
+
|
|
432
|
+
const messages: Message[] = [];
|
|
433
|
+
for (let i = 0; i < numMessagesForChunking; i++) {
|
|
434
|
+
messages.push(createTestMessage(`msg${i}`, spaceId, channelId, largeText, 1000 + i));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const members = [createTestMember('addr1')];
|
|
438
|
+
const storage = createMockStorage(messages, members);
|
|
439
|
+
const service = new SyncService({ storage });
|
|
440
|
+
|
|
441
|
+
// Their manifest is empty - they need all messages
|
|
442
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, []);
|
|
443
|
+
const theirMemberDigests: MemberDigest[] = [createMemberDigest(members[0])];
|
|
444
|
+
|
|
445
|
+
const deltas = await service.buildSyncDelta(
|
|
446
|
+
spaceId,
|
|
447
|
+
channelId,
|
|
448
|
+
theirManifest,
|
|
449
|
+
theirMemberDigests,
|
|
450
|
+
[],
|
|
451
|
+
new Map()
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Should have multiple chunks (6 messages at 200KB each = 1.2MB, chunked at 5MB default)
|
|
455
|
+
// With default 5MB chunks, this won't chunk, but verifies the mechanism works
|
|
456
|
+
expect(deltas.length).toBeGreaterThanOrEqual(1);
|
|
457
|
+
|
|
458
|
+
// Only last delta should be final
|
|
459
|
+
const finalDeltas = deltas.filter(d => d.isFinal);
|
|
460
|
+
expect(finalDeltas).toHaveLength(1);
|
|
461
|
+
expect(deltas[deltas.length - 1].isFinal).toBe(true);
|
|
462
|
+
|
|
463
|
+
// All messages should be included across chunks
|
|
464
|
+
const allNewMessages = deltas.flatMap(d => d.messageDelta?.newMessages || []);
|
|
465
|
+
expect(allNewMessages).toHaveLength(numMessagesForChunking);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should chunk when total size exceeds MAX_CHUNK_SIZE', async () => {
|
|
469
|
+
// Create enough messages to exceed MAX_CHUNK_SIZE (5MB)
|
|
470
|
+
// 1MB per message * 6 = 6MB > 5MB
|
|
471
|
+
const oneMbText = 'x'.repeat(1024 * 1024);
|
|
472
|
+
const numMessages = 6;
|
|
473
|
+
|
|
474
|
+
const messages: Message[] = [];
|
|
475
|
+
for (let i = 0; i < numMessages; i++) {
|
|
476
|
+
messages.push(createTestMessage(`msg${i}`, spaceId, channelId, oneMbText, 1000 + i));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const members = [createTestMember('addr1')];
|
|
480
|
+
const storage = createMockStorage(messages, members);
|
|
481
|
+
const service = new SyncService({ storage });
|
|
482
|
+
|
|
483
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, []);
|
|
484
|
+
const theirMemberDigests: MemberDigest[] = [createMemberDigest(members[0])];
|
|
485
|
+
|
|
486
|
+
const deltas = await service.buildSyncDelta(
|
|
487
|
+
spaceId,
|
|
488
|
+
channelId,
|
|
489
|
+
theirManifest,
|
|
490
|
+
theirMemberDigests,
|
|
491
|
+
[],
|
|
492
|
+
new Map()
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Should have multiple chunks since 6MB > 5MB limit
|
|
496
|
+
expect(deltas.length).toBeGreaterThan(1);
|
|
497
|
+
|
|
498
|
+
// All messages should be included across chunks
|
|
499
|
+
const allNewMessages = deltas.flatMap(d => d.messageDelta?.newMessages || []);
|
|
500
|
+
expect(allNewMessages).toHaveLength(numMessages);
|
|
501
|
+
|
|
502
|
+
// Only last delta should be final
|
|
503
|
+
expect(deltas[deltas.length - 1].isFinal).toBe(true);
|
|
504
|
+
expect(deltas.filter(d => d.isFinal)).toHaveLength(1);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should handle single oversized message', async () => {
|
|
508
|
+
// Create a single message larger than MAX_CHUNK_SIZE (5MB)
|
|
509
|
+
const hugeText = 'x'.repeat(MAX_CHUNK_SIZE + 1000);
|
|
510
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, hugeText, 1000)];
|
|
511
|
+
const members = [createTestMember('addr1')];
|
|
512
|
+
const storage = createMockStorage(messages, members);
|
|
513
|
+
const service = new SyncService({ storage });
|
|
514
|
+
|
|
515
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, []);
|
|
516
|
+
const theirMemberDigests: MemberDigest[] = [createMemberDigest(members[0])];
|
|
517
|
+
|
|
518
|
+
const deltas = await service.buildSyncDelta(
|
|
519
|
+
spaceId,
|
|
520
|
+
channelId,
|
|
521
|
+
theirManifest,
|
|
522
|
+
theirMemberDigests,
|
|
523
|
+
[],
|
|
524
|
+
new Map()
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Should still work - message sent in its own chunk
|
|
528
|
+
const allNewMessages = deltas.flatMap(d => d.messageDelta?.newMessages || []);
|
|
529
|
+
expect(allNewMessages).toHaveLength(1);
|
|
530
|
+
expect(allNewMessages[0].messageId).toBe('msg1');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should include reaction delta only in last message chunk', async () => {
|
|
534
|
+
// Create messages that will require chunking (1MB each, 6 total = 6MB > 5MB)
|
|
535
|
+
const oneMbText = 'x'.repeat(1024 * 1024);
|
|
536
|
+
const messages: Message[] = [];
|
|
537
|
+
for (let i = 0; i < 6; i++) {
|
|
538
|
+
const msg = createTestMessage(`msg${i}`, spaceId, channelId, oneMbText, 1000 + i);
|
|
539
|
+
// Add reactions to some messages
|
|
540
|
+
if (i % 2 === 0) {
|
|
541
|
+
msg.reactions = [{
|
|
542
|
+
emojiId: '👍',
|
|
543
|
+
emojiName: 'thumbsup',
|
|
544
|
+
spaceId,
|
|
545
|
+
count: 1,
|
|
546
|
+
memberIds: ['addr1'],
|
|
547
|
+
}];
|
|
548
|
+
}
|
|
549
|
+
messages.push(msg);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const members = [createTestMember('addr1')];
|
|
553
|
+
const storage = createMockStorage(messages, members);
|
|
554
|
+
const service = new SyncService({ storage });
|
|
555
|
+
|
|
556
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, []);
|
|
557
|
+
const theirMemberDigests: MemberDigest[] = [createMemberDigest(members[0])];
|
|
558
|
+
|
|
559
|
+
const deltas = await service.buildSyncDelta(
|
|
560
|
+
spaceId,
|
|
561
|
+
channelId,
|
|
562
|
+
theirManifest,
|
|
563
|
+
theirMemberDigests,
|
|
564
|
+
[],
|
|
565
|
+
new Map()
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
// Reaction delta should only be in the last message chunk (before member delta)
|
|
569
|
+
const deltasWithReactions = deltas.filter(d => d.reactionDelta && d.reactionDelta.added.length > 0);
|
|
570
|
+
expect(deltasWithReactions.length).toBeLessThanOrEqual(1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should put member delta in final chunk', async () => {
|
|
574
|
+
// Create messages that will require chunking (1MB each)
|
|
575
|
+
const oneMbText = 'x'.repeat(1024 * 1024);
|
|
576
|
+
const messages: Message[] = [];
|
|
577
|
+
for (let i = 0; i < 6; i++) {
|
|
578
|
+
messages.push(createTestMessage(`msg${i}`, spaceId, channelId, oneMbText, 1000 + i));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const members = [
|
|
582
|
+
createTestMember('addr1', 'User1'),
|
|
583
|
+
createTestMember('addr2', 'User2'),
|
|
584
|
+
];
|
|
585
|
+
const storage = createMockStorage(messages, members);
|
|
586
|
+
const service = new SyncService({ storage });
|
|
587
|
+
|
|
588
|
+
// They have no members
|
|
589
|
+
const theirManifest: SyncManifest = createManifest(spaceId, channelId, []);
|
|
590
|
+
const theirMemberDigests: MemberDigest[] = [];
|
|
591
|
+
|
|
592
|
+
const deltas = await service.buildSyncDelta(
|
|
593
|
+
spaceId,
|
|
594
|
+
channelId,
|
|
595
|
+
theirManifest,
|
|
596
|
+
theirMemberDigests,
|
|
597
|
+
[],
|
|
598
|
+
new Map()
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// Member delta should be in the final payload
|
|
602
|
+
const finalDelta = deltas.find(d => d.isFinal);
|
|
603
|
+
expect(finalDelta).toBeDefined();
|
|
604
|
+
expect(finalDelta?.memberDelta?.members).toHaveLength(2);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe('O(1) cache updates', () => {
|
|
609
|
+
it('should not recompute manifest hash when updating existing message', async () => {
|
|
610
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
611
|
+
const members = [createTestMember('addr1')];
|
|
612
|
+
const storage = createMockStorage(messages, members);
|
|
613
|
+
|
|
614
|
+
const service = new SyncService({ storage });
|
|
615
|
+
|
|
616
|
+
// Initialize cache and get initial hash
|
|
617
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
618
|
+
const hash1 = payload1.summary.manifestHash;
|
|
619
|
+
|
|
620
|
+
// Update existing message (not adding a new one)
|
|
621
|
+
const updatedMessage = createTestMessage('msg1', spaceId, channelId, 'Updated text', 1000);
|
|
622
|
+
service.updateCacheWithMessage(spaceId, channelId, updatedMessage);
|
|
623
|
+
|
|
624
|
+
// Hash should remain the same (message set didn't change, only content)
|
|
625
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
626
|
+
const hash2 = payload2.summary.manifestHash;
|
|
627
|
+
|
|
628
|
+
// Manifest hash is based on message IDs, not content, so should be same
|
|
629
|
+
expect(hash1).toBe(hash2);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should lazily recompute manifest hash only when needed after adding message', async () => {
|
|
633
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
634
|
+
const members = [createTestMember('addr1')];
|
|
635
|
+
const storage = createMockStorage(messages, members);
|
|
636
|
+
|
|
637
|
+
const service = new SyncService({ storage });
|
|
638
|
+
|
|
639
|
+
// Initialize cache
|
|
640
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
641
|
+
|
|
642
|
+
// Add multiple messages without accessing the hash
|
|
643
|
+
for (let i = 2; i <= 10; i++) {
|
|
644
|
+
service.updateCacheWithMessage(
|
|
645
|
+
spaceId,
|
|
646
|
+
channelId,
|
|
647
|
+
createTestMessage(`msg${i}`, spaceId, channelId, `Message ${i}`, 1000 + i)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Now request summary - hash should be computed once
|
|
652
|
+
const payload = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
653
|
+
expect(payload.summary.messageCount).toBe(10);
|
|
654
|
+
expect(payload.summary.manifestHash).toBeDefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('should update timestamps in O(1) when adding newer message', async () => {
|
|
658
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
659
|
+
const members = [createTestMember('addr1')];
|
|
660
|
+
const storage = createMockStorage(messages, members);
|
|
661
|
+
|
|
662
|
+
const service = new SyncService({ storage });
|
|
663
|
+
|
|
664
|
+
// Initialize cache
|
|
665
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
666
|
+
expect(payload1.summary.newestMessageTimestamp).toBe(1000);
|
|
667
|
+
expect(payload1.summary.oldestMessageTimestamp).toBe(1000);
|
|
668
|
+
|
|
669
|
+
// Add newer message
|
|
670
|
+
service.updateCacheWithMessage(
|
|
671
|
+
spaceId,
|
|
672
|
+
channelId,
|
|
673
|
+
createTestMessage('msg2', spaceId, channelId, 'Newer', 2000)
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
677
|
+
expect(payload2.summary.newestMessageTimestamp).toBe(2000);
|
|
678
|
+
expect(payload2.summary.oldestMessageTimestamp).toBe(1000);
|
|
679
|
+
|
|
680
|
+
// Add older message
|
|
681
|
+
service.updateCacheWithMessage(
|
|
682
|
+
spaceId,
|
|
683
|
+
channelId,
|
|
684
|
+
createTestMessage('msg0', spaceId, channelId, 'Older', 500)
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
const payload3 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
688
|
+
expect(payload3.summary.newestMessageTimestamp).toBe(2000);
|
|
689
|
+
expect(payload3.summary.oldestMessageTimestamp).toBe(500);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('should update member count in O(1)', async () => {
|
|
693
|
+
const messages: Message[] = [];
|
|
694
|
+
const members = [createTestMember('addr1')];
|
|
695
|
+
const storage = createMockStorage(messages, members);
|
|
696
|
+
|
|
697
|
+
const service = new SyncService({ storage });
|
|
698
|
+
|
|
699
|
+
// Initialize cache
|
|
700
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
701
|
+
expect(payload1.summary.memberCount).toBe(1);
|
|
702
|
+
|
|
703
|
+
// Add members without storage query
|
|
704
|
+
service.updateCacheWithMember(spaceId, channelId, createTestMember('addr2'));
|
|
705
|
+
service.updateCacheWithMember(spaceId, channelId, createTestMember('addr3'));
|
|
706
|
+
|
|
707
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
708
|
+
expect(payload2.summary.memberCount).toBe(3);
|
|
709
|
+
|
|
710
|
+
// Storage should only be called once (initial load)
|
|
711
|
+
expect(storage.getSpaceMembers).toHaveBeenCalledTimes(1);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should restore original hash after adding then removing a message (XOR property)', async () => {
|
|
715
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
716
|
+
const members = [createTestMember('addr1')];
|
|
717
|
+
const storage = createMockStorage(messages, members);
|
|
718
|
+
|
|
719
|
+
const service = new SyncService({ storage });
|
|
720
|
+
|
|
721
|
+
// Get initial hash
|
|
722
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
723
|
+
const originalHash = payload1.summary.manifestHash;
|
|
724
|
+
|
|
725
|
+
// Add a message
|
|
726
|
+
service.updateCacheWithMessage(
|
|
727
|
+
spaceId,
|
|
728
|
+
channelId,
|
|
729
|
+
createTestMessage('msg2', spaceId, channelId, 'World', 2000)
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
// Hash should be different
|
|
733
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
734
|
+
expect(payload2.summary.manifestHash).not.toBe(originalHash);
|
|
735
|
+
|
|
736
|
+
// Remove the message we added
|
|
737
|
+
service.removeCacheMessage(spaceId, channelId, 'msg2');
|
|
738
|
+
|
|
739
|
+
// Hash should be back to original (XOR is its own inverse)
|
|
740
|
+
const payload3 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
741
|
+
expect(payload3.summary.manifestHash).toBe(originalHash);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should produce same hash regardless of message add order (XOR commutativity)', async () => {
|
|
745
|
+
const members = [createTestMember('addr1')];
|
|
746
|
+
|
|
747
|
+
// First service: add msg1, msg2, msg3 in order
|
|
748
|
+
const storage1 = createMockStorage([], members);
|
|
749
|
+
const service1 = new SyncService({ storage: storage1 });
|
|
750
|
+
await service1.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
751
|
+
service1.updateCacheWithMessage(spaceId, channelId, createTestMessage('msg1', spaceId, channelId, 'A', 1000));
|
|
752
|
+
service1.updateCacheWithMessage(spaceId, channelId, createTestMessage('msg2', spaceId, channelId, 'B', 2000));
|
|
753
|
+
service1.updateCacheWithMessage(spaceId, channelId, createTestMessage('msg3', spaceId, channelId, 'C', 3000));
|
|
754
|
+
const hash1 = (await service1.buildSyncRequest(spaceId, channelId, inboxAddress)).summary.manifestHash;
|
|
755
|
+
|
|
756
|
+
// Second service: add msg3, msg1, msg2 in different order
|
|
757
|
+
const storage2 = createMockStorage([], members);
|
|
758
|
+
const service2 = new SyncService({ storage: storage2 });
|
|
759
|
+
await service2.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
760
|
+
service2.updateCacheWithMessage(spaceId, channelId, createTestMessage('msg3', spaceId, channelId, 'C', 3000));
|
|
761
|
+
service2.updateCacheWithMessage(spaceId, channelId, createTestMessage('msg1', spaceId, channelId, 'A', 1000));
|
|
762
|
+
service2.updateCacheWithMessage(spaceId, channelId, createTestMessage('msg2', spaceId, channelId, 'B', 2000));
|
|
763
|
+
const hash2 = (await service2.buildSyncRequest(spaceId, channelId, inboxAddress)).summary.manifestHash;
|
|
764
|
+
|
|
765
|
+
// Hashes should be identical (XOR is commutative)
|
|
766
|
+
expect(hash1).toBe(hash2);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
describe('Cache consistency after updates', () => {
|
|
771
|
+
it('should maintain consistent manifest hash after message updates', async () => {
|
|
772
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
773
|
+
const members = [createTestMember('addr1')];
|
|
774
|
+
const storage = createMockStorage(messages, members);
|
|
775
|
+
|
|
776
|
+
const service = new SyncService({ storage });
|
|
777
|
+
|
|
778
|
+
// Get initial hash
|
|
779
|
+
const payload1 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
780
|
+
const hash1 = payload1.summary.manifestHash;
|
|
781
|
+
|
|
782
|
+
// Add a message
|
|
783
|
+
const newMessage = createTestMessage('msg2', spaceId, channelId, 'World', 2000);
|
|
784
|
+
service.updateCacheWithMessage(spaceId, channelId, newMessage);
|
|
785
|
+
|
|
786
|
+
// Get new hash
|
|
787
|
+
const payload2 = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
788
|
+
const hash2 = payload2.summary.manifestHash;
|
|
789
|
+
|
|
790
|
+
// Hashes should be different
|
|
791
|
+
expect(hash1).not.toBe(hash2);
|
|
792
|
+
|
|
793
|
+
// Verify the new manifest includes both messages
|
|
794
|
+
const manifest = await service.buildSyncManifest(spaceId, channelId, [], inboxAddress);
|
|
795
|
+
expect(manifest.manifest.digests).toHaveLength(2);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('should correctly update existing message in cache', async () => {
|
|
799
|
+
const messages = [createTestMessage('msg1', spaceId, channelId, 'Hello', 1000)];
|
|
800
|
+
const members = [createTestMember('addr1')];
|
|
801
|
+
const storage = createMockStorage(messages, members);
|
|
802
|
+
|
|
803
|
+
const service = new SyncService({ storage });
|
|
804
|
+
|
|
805
|
+
// Initialize cache
|
|
806
|
+
await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
807
|
+
|
|
808
|
+
// Update existing message
|
|
809
|
+
const updatedMessage = createTestMessage('msg1', spaceId, channelId, 'Updated', 1000);
|
|
810
|
+
updatedMessage.modifiedDate = 2000;
|
|
811
|
+
service.updateCacheWithMessage(spaceId, channelId, updatedMessage);
|
|
812
|
+
|
|
813
|
+
// Message count should still be 1
|
|
814
|
+
const payload = await service.buildSyncRequest(spaceId, channelId, inboxAddress);
|
|
815
|
+
expect(payload.summary.messageCount).toBe(1);
|
|
816
|
+
|
|
817
|
+
// But the content hash should have changed
|
|
818
|
+
const manifest = await service.buildSyncManifest(spaceId, channelId, [], inboxAddress);
|
|
819
|
+
expect(manifest.manifest.digests[0].modifiedDate).toBe(2000);
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
});
|